Skip to content

Commit b4baa2e

Browse files
adaexclaude
andcommitted
feat: replace curated with best1/best2 tiers and add base config
- best1: speed-tested nodes only (HK:50, US:30, others:20) - best2: all qualified nodes from 7 sources, no cap - Each outputs 3 formats: .yaml, -nodes.yaml, -singbox.json - Fix AI group name: OpenAi/Ai平台 → AI平台 - Add mihomo base config for best: mixed-port, unified-delay, tcp-concurrent, profile, fake-ip DNS - Remove unused USEFUL_TAGS_RE and COUNTRY_GROUPS.prefix - Data-driven topByCountry limits instead of stringly-typed dispatch Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a334555 commit b4baa2e

3 files changed

Lines changed: 105 additions & 53 deletions

File tree

src/check.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -161,8 +161,8 @@ const COUNTRY_FLAGS: Record<string, string> = {
161161
HK: '🇭🇰', JP: '🇯🇵', US: '🇺🇸', TW: '🇨🇳', SG: '🇸🇬', KR: '🇰🇷',
162162
};
163163

164-
const CURATED_LIMITS: Record<string, number> = { HK: 100, US: 50 };
165-
const CURATED_DEFAULT_LIMIT = 20;
164+
const BEST1_LIMITS: Record<string, number> = { HK: 50, US: 30 };
165+
const BEST1_DEFAULT_LIMIT = 20;
166166

167167
function parseSpeed(name: string): number {
168168
const m = name.match(/(\d+(?:\.\d+)?)\s*(MB|KB)\/s/);
@@ -203,7 +203,7 @@ function extractTags(name: string): string {
203203
return `${speed}${multStr}${lossStr}`;
204204
}
205205

206-
function topByCountry(proxies: Proxy[]): Proxy[] {
206+
function topByCountry(proxies: Proxy[], limits?: Record<string, number>, defaultLimit?: number): Proxy[] {
207207
const groups = new Map<string, Proxy[]>();
208208
for (const p of proxies) {
209209
const code = extractCountryCode(p.name) ?? '??';
@@ -212,9 +212,14 @@ function topByCountry(proxies: Proxy[]): Proxy[] {
212212
}
213213
const result: Proxy[] = [];
214214
for (const [code, members] of groups) {
215-
const limit = CURATED_LIMITS[code] ?? CURATED_DEFAULT_LIMIT;
216215
members.sort((a, b) => sortScore(b.name) - sortScore(a.name));
217-
const top = members.slice(0, limit);
216+
let top: Proxy[];
217+
if (limits && defaultLimit !== undefined) {
218+
const limit = limits[code] ?? defaultLimit;
219+
top = members.slice(0, limit);
220+
} else {
221+
top = members;
222+
}
218223
for (let i = 0; i < top.length; i++) {
219224
const flag = COUNTRY_FLAGS[code] ?? '';
220225
const tags = extractTags(top[i].name);
@@ -232,22 +237,23 @@ async function main() {
232237
ensureTempDir();
233238

234239
try {
235-
const categories = [
240+
const categories: Array<{ name: string; file: string; limits?: Record<string, number>; defaultLimit?: number }> = [
236241
{ name: '全部', file: 'all-raw.yaml' },
237242
{ name: 'ACL4SSR', file: 'acl4ssr-raw.yaml' },
238243
{ name: 'freeSub', file: 'freesub-raw.yaml' },
239-
{ name: '精选', file: 'curated-raw.yaml' },
244+
{ name: 'best1', file: 'best1-raw.yaml', limits: BEST1_LIMITS, defaultLimit: BEST1_DEFAULT_LIMIT },
245+
{ name: 'best2', file: 'best2-raw.yaml', limits: {} },
240246
];
241247

242-
console.log(`\n精选 Top 筛选\n`);
248+
console.log(`\nbest Top 筛选\n`);
243249

244250
for (const cat of categories) {
245251
const filePath = path.join(DATA_DIR, cat.file);
246252
let proxies = readYaml<{ proxies: Proxy[] }>(filePath).proxies;
247253
console.log(`${cat.name}节点 (${proxies.length}):`);
248-
if (cat.file === 'curated-raw.yaml') {
254+
if (cat.limits) {
249255
const before = proxies.length;
250-
proxies = topByCountry(proxies);
256+
proxies = topByCountry(proxies, cat.limits, cat.defaultLimit);
251257
console.log(` Top 筛选: ${proxies.length}/${before}`);
252258
}
253259
writeYaml(filePath, { proxies });

src/fetch.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -119,11 +119,10 @@ function matchCuratedCountry(name: string): boolean {
119119
return code ? CURATED_COUNTRIES.has(code) : false;
120120
}
121121

122-
const USEFUL_TAGS_RE = /\|(?:GPT?|GM|YT)/;
122+
const HAS_SPEED_RE = /\d+(?:\.\d+)?\s*[MK]B\/s/;
123123

124-
function isCuratedQualified(name: string): boolean {
125-
const hasSpeed = /\d+(?:\.\d+)?\s*[MK]B\/s/.test(name);
126-
if (hasSpeed) return true;
124+
function isBest2Qualified(name: string): boolean {
125+
if (HAS_SPEED_RE.test(name)) return true;
127126
const mult = parseMultiplier(name);
128127
if (mult !== 2 && mult !== 1) return false;
129128
const lossMatch = name.match(/\|(\d+)%/);
@@ -132,10 +131,15 @@ function isCuratedQualified(name: string): boolean {
132131
return true;
133132
}
134133

134+
function isBest1Qualified(name: string): boolean {
135+
return HAS_SPEED_RE.test(name);
136+
}
137+
135138
function dedup(results: FetchResult[]): {
136139
acl4ssr: Proxy[];
137140
freesub: Proxy[];
138-
curated: Proxy[];
141+
best1: Proxy[];
142+
best2: Proxy[];
139143
all: Proxy[];
140144
templateAcl4ssr: ParsedConfig | null;
141145
templateFreesub: ParsedConfig | null;
@@ -149,8 +153,11 @@ function dedup(results: FetchResult[]): {
149153

150154
const acl4ssr = collectProxies(results, (s) => s.category === 'acl4ssr');
151155
const freesub = collectProxies(results, (s) => s.category === 'freesub');
152-
const curated = collectProxies(results, (s, p) =>
153-
CURATED_SOURCES.has(s.name) && matchCuratedCountry(p.name) && isCuratedQualified(p.name),
156+
const best1 = collectProxies(results, (s, p) =>
157+
CURATED_SOURCES.has(s.name) && matchCuratedCountry(p.name) && isBest1Qualified(p.name),
158+
);
159+
const best2 = collectProxies(results, (s, p) =>
160+
CURATED_SOURCES.has(s.name) && matchCuratedCountry(p.name) && isBest2Qualified(p.name),
154161
);
155162
const all = collectProxies(results, () => true);
156163

@@ -161,7 +168,7 @@ function dedup(results: FetchResult[]): {
161168
if (source.category === 'freesub' && !templateFreesub) templateFreesub = config;
162169
}
163170

164-
return { acl4ssr, freesub, curated, all, templateAcl4ssr, templateFreesub };
171+
return { acl4ssr, freesub, best1, best2, all, templateAcl4ssr, templateFreesub };
165172
}
166173

167174
function buildNodesOnly(proxies: Proxy[]): { proxies: Proxy[] } {
@@ -241,26 +248,28 @@ async function main() {
241248

242249
console.log(`\n成功 ${results.length}/${sources.length} 个源`);
243250

244-
const { acl4ssr, freesub, curated, all, templateAcl4ssr, templateFreesub } = dedup(results);
251+
const { acl4ssr, freesub, best1, best2, all, templateAcl4ssr, templateFreesub } = dedup(results);
245252

246-
console.log(`\n去重后: ACL4SSR ${acl4ssr.length}, freeSub ${freesub.length}, 精选 ${curated.length}, 全部 ${all.length}`);
253+
console.log(`\n去重后: ACL4SSR ${acl4ssr.length}, freeSub ${freesub.length}, best1 ${best1.length}, best2 ${best2.length}, 全部 ${all.length}`);
247254

248255
const aliveSet = await tcpFilterAll(all);
249256

250257
const acl4ssrAlive = filterByAlive(acl4ssr, aliveSet);
251258
const freesubAlive = filterByAlive(freesub, aliveSet);
252-
const curatedAlive = filterByAlive(curated, aliveSet);
259+
const best1Alive = filterByAlive(best1, aliveSet);
260+
const best2Alive = filterByAlive(best2, aliveSet);
253261
const allAlive = filterByAlive(all, aliveSet);
254262

255-
console.log(`\nTCP 过滤后: ACL4SSR ${acl4ssrAlive.length}, freeSub ${freesubAlive.length}, 精选 ${curatedAlive.length}, 全部 ${allAlive.length}`);
263+
console.log(`\nTCP 过滤后: ACL4SSR ${acl4ssrAlive.length}, freeSub ${freesubAlive.length}, best1 ${best1Alive.length}, best2 ${best2Alive.length}, 全部 ${allAlive.length}`);
256264

257265
// save intermediate data
258266
fs.mkdirSync(DATA_DIR, { recursive: true });
259267

260268
writeYaml(path.join(DATA_DIR, 'all-raw.yaml'), buildNodesOnly(allAlive));
261269
writeYaml(path.join(DATA_DIR, 'acl4ssr-raw.yaml'), buildNodesOnly(acl4ssrAlive));
262270
writeYaml(path.join(DATA_DIR, 'freesub-raw.yaml'), buildNodesOnly(freesubAlive));
263-
writeYaml(path.join(DATA_DIR, 'curated-raw.yaml'), buildNodesOnly(curatedAlive));
271+
writeYaml(path.join(DATA_DIR, 'best1-raw.yaml'), buildNodesOnly(best1Alive));
272+
writeYaml(path.join(DATA_DIR, 'best2-raw.yaml'), buildNodesOnly(best2Alive));
264273

265274
// save templates
266275
if (templateAcl4ssr) {

src/output.ts

Lines changed: 67 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -218,13 +218,13 @@ function convertProxy(p: Proxy): SingboxOutbound | null {
218218
}
219219
}
220220

221-
const COUNTRY_GROUPS: Array<{ tag: string; prefix: string; re: RegExp }> = [
222-
{ tag: '🇭🇰 香港', prefix: 'HK', re: /HK_\d+/ },
223-
{ tag: '🇯🇵 日本', prefix: 'JP', re: /JP_\d+/ },
224-
{ tag: '🇺🇸 美国', prefix: 'US', re: /US_\d+/ },
225-
{ tag: '🇨🇳 台湾', prefix: 'TW', re: /TW_\d+/ },
226-
{ tag: '🇸🇬 新加坡', prefix: 'SG', re: /SG_\d+/ },
227-
{ tag: '🇰🇷 韩国', prefix: 'KR', re: /KR_\d+/ },
221+
const COUNTRY_GROUPS: Array<{ tag: string; re: RegExp }> = [
222+
{ tag: '🇭🇰 香港', re: /HK_\d+/ },
223+
{ tag: '🇯🇵 日本', re: /JP_\d+/ },
224+
{ tag: '🇺🇸 美国', re: /US_\d+/ },
225+
{ tag: '🇨🇳 台湾', re: /TW_\d+/ },
226+
{ tag: '🇸🇬 新加坡', re: /SG_\d+/ },
227+
{ tag: '🇰🇷 韩国', re: /KR_\d+/ },
228228
];
229229

230230
function mihomoToSingbox(proxies: Proxy[]): Record<string, unknown> {
@@ -333,15 +333,28 @@ function mihomoToSingbox(proxies: Proxy[]): Record<string, unknown> {
333333
};
334334
}
335335

336+
function fixAiName(s: string): string {
337+
return s.replace('💬 Ai平台', '💬 AI平台').replace('💬 OpenAi', '💬 AI平台');
338+
}
339+
340+
function fixAiGroupName(groups: Record<string, unknown>[]): Record<string, unknown>[] {
341+
return groups.map(g => ({ ...g, name: fixAiName(String(g.name)) }));
342+
}
343+
344+
function fixAiRules(rules: string[]): string[] {
345+
return rules.map(fixAiName);
346+
}
347+
336348
// --- main ---
337349

338350
function main() {
339351
const acl4ssrProxies = readYaml<{ proxies: Proxy[] }>(path.join(DATA_DIR, 'acl4ssr-raw.yaml')).proxies;
340352
const freesubProxies = readYaml<{ proxies: Proxy[] }>(path.join(DATA_DIR, 'freesub-raw.yaml')).proxies;
341-
const curatedProxies = readYaml<{ proxies: Proxy[] }>(path.join(DATA_DIR, 'curated-raw.yaml')).proxies;
353+
const best1Proxies = readYaml<{ proxies: Proxy[] }>(path.join(DATA_DIR, 'best1-raw.yaml')).proxies;
354+
const best2Proxies = readYaml<{ proxies: Proxy[] }>(path.join(DATA_DIR, 'best2-raw.yaml')).proxies;
342355
const allProxies = readYaml<{ proxies: Proxy[] }>(path.join(DATA_DIR, 'all-raw.yaml')).proxies;
343356

344-
console.log(`ACL4SSR: ${acl4ssrProxies.length}, freeSub: ${freesubProxies.length}, 精选: ${curatedProxies.length}, 全部: ${allProxies.length}`);
357+
console.log(`ACL4SSR: ${acl4ssrProxies.length}, freeSub: ${freesubProxies.length}, best1: ${best1Proxies.length}, best2: ${best2Proxies.length}, 全部: ${allProxies.length}`);
345358

346359
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
347360

@@ -386,36 +399,60 @@ function main() {
386399
console.log(`已写入 output/freesub-nodes.yaml`);
387400
}
388401

389-
// curated full config (uses acl4ssr template)
390-
if (acl4ssrTemplate && curatedProxies.length > 0) {
391-
writeYaml(path.join(OUTPUT_DIR, 'curated.yaml'), {
392-
proxies: curatedProxies,
393-
'proxy-groups': acl4ssrTemplate['proxy-groups'],
394-
rules: acl4ssrTemplate.rules,
395-
'rule-providers': acl4ssrTemplate['rule-providers'],
396-
dns: acl4ssrTemplate.dns,
397-
});
398-
console.log(`已写入 output/curated.yaml (${curatedProxies.length} 节点)`);
399-
}
402+
// best1/best2 outputs (both use acl4ssr template with OpenAi→AI平台 fix)
403+
const fixedGroups = acl4ssrTemplate ? fixAiGroupName(acl4ssrTemplate['proxy-groups'] as Record<string, unknown>[]) : null;
404+
const fixedRules = acl4ssrTemplate ? fixAiRules(acl4ssrTemplate.rules as string[]) : null;
405+
406+
const bestBase = {
407+
'mixed-port': 7890,
408+
'allow-lan': false,
409+
mode: 'rule',
410+
'log-level': 'warning',
411+
'unified-delay': true,
412+
'tcp-concurrent': true,
413+
profile: { 'store-selected': true },
414+
dns: {
415+
enable: true,
416+
'enhanced-mode': 'fake-ip',
417+
'fake-ip-range': '198.18.0.1/16',
418+
'fake-ip-filter': ['*.lan', '*.local', '+.msftconnecttest.com', '+.msftncsi.com', 'localhost.ptlogin2.qq.com'],
419+
'default-nameserver': ['223.5.5.5', '119.29.29.29'],
420+
nameserver: ['https://dns.alidns.com/dns-query', 'https://doh.pub/dns-query'],
421+
},
422+
};
400423

401-
// curated nodes only
402-
if (curatedProxies.length > 0) {
403-
writeYaml(path.join(OUTPUT_DIR, 'curated-nodes.yaml'), { proxies: curatedProxies });
404-
console.log(`已写入 output/curated-nodes.yaml`);
405-
}
424+
const bestGroups: Array<{ label: string; proxies: Proxy[] }> = [
425+
{ label: 'best1', proxies: best1Proxies },
426+
{ label: 'best2', proxies: best2Proxies },
427+
];
428+
429+
for (const { label, proxies } of bestGroups) {
430+
if (proxies.length === 0) continue;
431+
432+
if (acl4ssrTemplate && fixedGroups && fixedRules) {
433+
writeYaml(path.join(OUTPUT_DIR, `${label}.yaml`), {
434+
...bestBase,
435+
proxies,
436+
'proxy-groups': fixedGroups,
437+
rules: fixedRules,
438+
'rule-providers': acl4ssrTemplate['rule-providers'],
439+
});
440+
console.log(`已写入 output/${label}.yaml (${proxies.length} 节点)`);
441+
}
442+
443+
writeYaml(path.join(OUTPUT_DIR, `${label}-nodes.yaml`), { proxies });
444+
console.log(`已写入 output/${label}-nodes.yaml`);
406445

407-
// curated sing-box format
408-
if (curatedProxies.length > 0) {
409-
const singboxConfig = mihomoToSingbox(curatedProxies);
446+
const singboxConfig = mihomoToSingbox(proxies);
410447
fs.writeFileSync(
411-
path.join(OUTPUT_DIR, 'curated-singbox.json'),
448+
path.join(OUTPUT_DIR, `${label}-singbox.json`),
412449
JSON.stringify(singboxConfig, null, 2),
413450
'utf-8',
414451
);
415452
const proxyCount = (singboxConfig.outbounds as unknown[]).filter(
416453
(o: unknown) => !['selector', 'urltest', 'direct', 'block', 'dns'].includes((o as Record<string, string>).type),
417454
).length;
418-
console.log(`已写入 output/curated-singbox.json (${proxyCount} 节点)`);
455+
console.log(`已写入 output/${label}-singbox.json (${proxyCount} 节点)`);
419456
}
420457

421458
// all nodes

0 commit comments

Comments
 (0)