tt-help-cli-ycl 1.4.148 → 1.4.149
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scripts/test-country-fix.mjs +61 -0
- package/src/clash/checker.js +42 -2
- package/src/lib/api-client.js +12 -2
package/package.json
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* - 多国配置且已生效时,幂等不重复下发
|
|
17
17
|
* B. daemon 逗号分隔多国 --country 参数解析
|
|
18
18
|
* C. checker normalizeCountries / filterNodesByCountry 回归
|
|
19
|
+
* + 内置策略/机场信息节点过滤(isUsableProxyNode,2026-09-03 剩余流量 GB 修复)
|
|
19
20
|
* D. 客户端 daemon 启动兜底:读 clash-health.json 上次生效国家 → join(",")
|
|
20
21
|
*/
|
|
21
22
|
|
|
@@ -239,6 +240,66 @@ ok(
|
|
|
239
240
|
fr = checker.filterNodesByCountry(nodes, null);
|
|
240
241
|
ok(fr.filtered.length === 4 && fr.excluded === 0, "C8 null 不过滤");
|
|
241
242
|
|
|
243
|
+
// ── C9-C14 信息节点/内置策略过滤(2026-09-03:剩余流量 GB 混入候选集修复) ──
|
|
244
|
+
fr = checker.filterNodesByCountry(["剩余流量:5319.72 GB"], ["US", "GB", "JP"]);
|
|
245
|
+
ok(
|
|
246
|
+
fr.filtered.length === 0,
|
|
247
|
+
"C9 GB 模式不再命中流量单位(剩余流量:xxx GB)",
|
|
248
|
+
JSON.stringify(fr),
|
|
249
|
+
);
|
|
250
|
+
fr = checker.filterNodesByCountry(
|
|
251
|
+
["🇬🇧英国HY1-1000M专线", "🇬🇧英国trojan1", "UK Premium"],
|
|
252
|
+
["GB"],
|
|
253
|
+
);
|
|
254
|
+
ok(
|
|
255
|
+
fr.filtered.length === 3,
|
|
256
|
+
"C10 英国节点仍正常命中(英国/UK)",
|
|
257
|
+
JSON.stringify(fr),
|
|
258
|
+
);
|
|
259
|
+
const junkNodes = [
|
|
260
|
+
"DIRECT",
|
|
261
|
+
"REJECT",
|
|
262
|
+
"REJECT-DROP",
|
|
263
|
+
"GLOBAL",
|
|
264
|
+
"剩余流量:5319.72 GB",
|
|
265
|
+
"剩余流量:1862.27 MB",
|
|
266
|
+
"官网:example.com",
|
|
267
|
+
"套餐到期:2099-12-31",
|
|
268
|
+
"1.5TB",
|
|
269
|
+
];
|
|
270
|
+
ok(
|
|
271
|
+
junkNodes.every((n) => !checker.isUsableProxyNode(n)),
|
|
272
|
+
"C11 内置策略+信息节点全部被 isUsableProxyNode 拒绝",
|
|
273
|
+
JSON.stringify(junkNodes.filter((n) => checker.isUsableProxyNode(n))),
|
|
274
|
+
);
|
|
275
|
+
const goodNodes = [
|
|
276
|
+
"🇺🇸美国HY1轻量",
|
|
277
|
+
"🇬🇧英国HY1-1000M专线",
|
|
278
|
+
"🇯🇵日本Trojan1",
|
|
279
|
+
"🇹🇼台湾trojan直连AI",
|
|
280
|
+
];
|
|
281
|
+
ok(
|
|
282
|
+
goodNodes.every((n) => checker.isUsableProxyNode(n)),
|
|
283
|
+
"C12 正常业务节点全部通过 isUsableProxyNode",
|
|
284
|
+
JSON.stringify(goodNodes.filter((n) => !checker.isUsableProxyNode(n))),
|
|
285
|
+
);
|
|
286
|
+
// 读口兜底:已持久化的脏候选文件在读取时被过滤
|
|
287
|
+
writeFileSync(
|
|
288
|
+
join(tmpdir(), "clash-healthy-nodes.json"),
|
|
289
|
+
JSON.stringify({
|
|
290
|
+
updated: new Date().toISOString(),
|
|
291
|
+
nodes: ["🇺🇸美国A", "剩余流量:5319.72 GB", "DIRECT", "🇯🇵日本A"],
|
|
292
|
+
delays: {},
|
|
293
|
+
}),
|
|
294
|
+
);
|
|
295
|
+
const hl = checker.loadHealthyList();
|
|
296
|
+
ok(
|
|
297
|
+
hl.nodes.length === 2 &&
|
|
298
|
+
!hl.nodes.some((n) => checker.isUsableProxyNode(n) === false),
|
|
299
|
+
"C13 loadHealthyList 读口剔除已持久化的脏候选",
|
|
300
|
+
JSON.stringify(hl.nodes),
|
|
301
|
+
);
|
|
302
|
+
|
|
242
303
|
console.log("\n═══ D. 客户端 daemon 启动兜底(读上次生效国家)═══");
|
|
243
304
|
const { getClashHealth } = await import("../src/watchdog/reporter.js");
|
|
244
305
|
const healthPath = join(tmpdir(), "clash-health.json");
|
package/src/clash/checker.js
CHANGED
|
@@ -45,7 +45,10 @@ const CLASH_PROCESS_NAMES = ["verge-mihomo", "69cloud-core"];
|
|
|
45
45
|
// 拉丁代码用词边界防误匹配(如 \bUS\b 不匹配 PLUS)
|
|
46
46
|
const COUNTRY_PATTERNS = {
|
|
47
47
|
US: /(?:[🇺🇸]\s*)?(美国|USA|United\s*States|\bUS\b)/i,
|
|
48
|
-
GB
|
|
48
|
+
// GB 代码已移除(2026-09-03):机场信息节点"剩余流量:xxx GB"的流量单位会被
|
|
49
|
+
// \bGB\b 误判为英国节点,导致其通过国家过滤混入 delay 初测候选集
|
|
50
|
+
// (生产实锤:12 台机器候选集混入、D-1/D-7 切上去驻留)。英国节点靠 英国/UK/🇬🇧 命中。
|
|
51
|
+
GB: /(?:[🇬🇧]\s*)?(英国|UK|United\s*Kingdom)/i,
|
|
49
52
|
DE: /(?:[🇩🇪]\s*)?(德国|Germany|\bDE\b)/i,
|
|
50
53
|
FR: /(?:[🇫🇷]\s*)?(法国|France|\bFR\b)/i,
|
|
51
54
|
JP: /(?:[🇯🇵]\s*)?(日本|Japan|\bJP\b)/i,
|
|
@@ -327,6 +330,37 @@ export function getSupportedCountries() {
|
|
|
327
330
|
return Object.keys(COUNTRY_PATTERNS);
|
|
328
331
|
}
|
|
329
332
|
|
|
333
|
+
// ══════════════════════════════════════════════
|
|
334
|
+
// 4.0 内置策略 / 机场信息节点过滤
|
|
335
|
+
// ══════════════════════════════════════════════
|
|
336
|
+
|
|
337
|
+
// mihomo GLOBAL 组的 all 列表包含内置策略;这类节点不可用作业务出口
|
|
338
|
+
const BUILTIN_POLICY_NAMES = new Set([
|
|
339
|
+
"DIRECT",
|
|
340
|
+
"REJECT",
|
|
341
|
+
"REJECT-DROP",
|
|
342
|
+
"PASS",
|
|
343
|
+
"COMPATIBLE",
|
|
344
|
+
"GLOBAL",
|
|
345
|
+
]);
|
|
346
|
+
|
|
347
|
+
// 机场订阅普遍带"剩余流量/到期/官网"等信息节点;部分实际可翻墙,会被 delay
|
|
348
|
+
// 初测选入候选集并被巡检/failover 轮换切上去驻留(2026-09-03 生产实锤)
|
|
349
|
+
const INFO_NODE_PATTERN =
|
|
350
|
+
/剩余|流量|到期|过期|官网|重置|套餐|订阅|工单|[\d.]\s*[KMGTP]B$/i;
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* 判定节点名是否可用作业务出口(内置策略与机场信息节点返回 false)
|
|
354
|
+
* 在候选集写口(runDelayProbe)与读口(loadHealthyList)统一调用
|
|
355
|
+
*/
|
|
356
|
+
export function isUsableProxyNode(name) {
|
|
357
|
+
const n = String(name || "").trim();
|
|
358
|
+
if (!n) return false;
|
|
359
|
+
if (BUILTIN_POLICY_NAMES.has(n.toUpperCase())) return false;
|
|
360
|
+
if (INFO_NODE_PATTERN.test(n)) return false;
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
|
|
330
364
|
// ══════════════════════════════════════════════
|
|
331
365
|
// 4.5 delay 初测(零切换,生成候选集)
|
|
332
366
|
// ══════════════════════════════════════════════
|
|
@@ -355,6 +389,8 @@ export async function runDelayProbe({
|
|
|
355
389
|
console.error(`[checker] 获取 GLOBAL 节点列表失败: ${e.message}`);
|
|
356
390
|
return { nodes: [], total: 0, results: [] };
|
|
357
391
|
}
|
|
392
|
+
// 先剔除内置策略与机场信息节点(不可用作业务出口),再走国家过滤
|
|
393
|
+
all = all.filter(isUsableProxyNode);
|
|
358
394
|
const { filtered, excluded } = filterNodesByCountry(all, filter);
|
|
359
395
|
const total = all.length;
|
|
360
396
|
|
|
@@ -402,7 +438,11 @@ export function loadHealthyList(country = null) {
|
|
|
402
438
|
try {
|
|
403
439
|
if (!existsSync(_healthyFile)) return _emptyList();
|
|
404
440
|
const d = JSON.parse(readFileSync(_healthyFile, "utf-8"));
|
|
405
|
-
|
|
441
|
+
// 读口兜底:剔除内置策略/信息节点。覆盖"修复前已写入文件的脏候选"——
|
|
442
|
+
// 写口过滤后文件会在下轮 delay 初测自愈,这里保证修复生效前也不切错。
|
|
443
|
+
const allNodes = (Array.isArray(d.nodes) ? d.nodes : []).filter(
|
|
444
|
+
isUsableProxyNode,
|
|
445
|
+
);
|
|
406
446
|
const allDelays = d.delays && typeof d.delays === "object" ? d.delays : {};
|
|
407
447
|
const filter = country || CONFIG.nodeCountryFilter;
|
|
408
448
|
const { filtered, excluded } = filterNodesByCountry(allNodes, filter);
|
package/src/lib/api-client.js
CHANGED
|
@@ -74,9 +74,19 @@ export function createApiClient(opts = {}) {
|
|
|
74
74
|
clientId,
|
|
75
75
|
meta,
|
|
76
76
|
);
|
|
77
|
-
// error-report 自动带上本机 hostname
|
|
77
|
+
// error-report 自动带上本机 hostname,供看门狗按机器关联异常。
|
|
78
|
+
// ⚠️ header 值必须是 ASCII(ByteString):中文主机名(如"霞霞笔记本")会让
|
|
79
|
+
// undici fetch 在校验 header 时抛 "Cannot convert argument to a ByteString
|
|
80
|
+
// ... value of XXXX which is greater than 255",请求根本没发出去且被
|
|
81
|
+
// withRetry 无限重试刷屏。因此:
|
|
82
|
+
// - header 放净化后的 ASCII 值(仅保证请求能发出)
|
|
83
|
+
// - 原始 hostname 塞进 body(JSON 为 UTF-8,无 ByteString 限制),
|
|
84
|
+
// 服务端 body.hostname 优先,看门狗仍能按原始主机名关联本机异常
|
|
78
85
|
if (/\/api\/error-report(\?|$)/.test(String(url))) {
|
|
79
|
-
headers["X-Client-Hostname"] = os
|
|
86
|
+
headers["X-Client-Hostname"] = os
|
|
87
|
+
.hostname()
|
|
88
|
+
.replace(/[^\x20-\x7e]/g, "_");
|
|
89
|
+
body = { ...body, hostname: os.hostname() };
|
|
80
90
|
}
|
|
81
91
|
const res = await fetch(url, {
|
|
82
92
|
method: "POST",
|