tt-help-cli-ycl 1.4.80 → 1.4.83
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/src/clash/api.js +31 -0
- package/src/clash/benchmark/lib/clash-api.js +234 -0
- package/src/clash/benchmark/speedtest-result.json +431 -0
- package/src/clash/benchmark/speedtest-tiktok.mjs +384 -0
- package/src/clash/checker.js +178 -41
- package/src/clash/daemon.js +62 -94
- package/src/cli/clash-daemon.js +1 -1
- package/src/cli/watchdog.js +8 -1
- package/src/lib/args.js +6 -2
- package/src/lib/constants.js +4 -3
- package/src/watchdog/agent.js +52 -13
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TikTok 节点速度测试 (v2 — 内容验证版)
|
|
3
|
+
*
|
|
4
|
+
* 逐个将 GLOBAL 组切换到每个存活节点,
|
|
5
|
+
* 通过 Clash HTTP 代理 (127.0.0.1:7897) 请求 tiktok.com,
|
|
6
|
+
* 分析返回内容区分:真页面 / WAF挑战 / 地区重定向 / 不通
|
|
7
|
+
*
|
|
8
|
+
* 用法: node speedtest-tiktok.mjs [--top N] [--timeout S]
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as api from "./lib/clash-api.js";
|
|
12
|
+
import { spawnSync } from "child_process";
|
|
13
|
+
|
|
14
|
+
const TARGET_URL = "https://www.tiktok.com";
|
|
15
|
+
const PROXY = "http://127.0.0.1:7897";
|
|
16
|
+
const MAX_TIME =
|
|
17
|
+
parseInt(process.argv[process.argv.indexOf("--timeout") + 1]) || 15;
|
|
18
|
+
const TOP_N = parseInt(process.argv[process.argv.indexOf("--top") + 1]) || 0;
|
|
19
|
+
|
|
20
|
+
const GROUP = "GLOBAL";
|
|
21
|
+
const DELAY_MS = 800;
|
|
22
|
+
|
|
23
|
+
// ---- 通过 curl 测试 + 内容分析 ----
|
|
24
|
+
function testViaCurl() {
|
|
25
|
+
// 第一步:用 -w 获取元数据,-o 输出到临时文件
|
|
26
|
+
const args = [
|
|
27
|
+
"-s",
|
|
28
|
+
"-w",
|
|
29
|
+
"\n__CURL_META__%{http_code}|%{time_total}|%{time_connect}|%{time_starttransfer}|%{size_download}|%{redirect_url}",
|
|
30
|
+
"--proxy",
|
|
31
|
+
PROXY,
|
|
32
|
+
"--max-time",
|
|
33
|
+
String(MAX_TIME),
|
|
34
|
+
"-L",
|
|
35
|
+
"-A",
|
|
36
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
|
|
37
|
+
TARGET_URL,
|
|
38
|
+
];
|
|
39
|
+
const result = spawnSync("curl", args, {
|
|
40
|
+
encoding: "utf-8",
|
|
41
|
+
timeout: (MAX_TIME + 5) * 1000,
|
|
42
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
43
|
+
maxBuffer: 10 * 1024 * 1024, // 10MB
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const stderr = (result.stderr || "").trim();
|
|
47
|
+
|
|
48
|
+
if (result.error) {
|
|
49
|
+
let errorMsg = result.error.message || "unknown";
|
|
50
|
+
if (errorMsg.includes("ETIMEDOUT") || errorMsg.includes("timed out"))
|
|
51
|
+
errorMsg = "timeout";
|
|
52
|
+
return {
|
|
53
|
+
httpCode: 0,
|
|
54
|
+
timeTotal: MAX_TIME,
|
|
55
|
+
timeConnect: 0,
|
|
56
|
+
timeStartTransfer: 0,
|
|
57
|
+
sizeDownload: 0,
|
|
58
|
+
redirectUrl: "",
|
|
59
|
+
error: errorMsg,
|
|
60
|
+
category: "dead",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (result.status !== 0) {
|
|
65
|
+
let errorMsg = `exit ${result.status}`;
|
|
66
|
+
if (stderr.includes("timed out")) errorMsg = "timeout";
|
|
67
|
+
else if (stderr.includes("Connection refused"))
|
|
68
|
+
errorMsg = "connection refused";
|
|
69
|
+
else if (stderr.includes("Could not resolve")) errorMsg = "DNS failed";
|
|
70
|
+
else if (stderr.includes("SSL")) errorMsg = "SSL error";
|
|
71
|
+
return {
|
|
72
|
+
httpCode: 0,
|
|
73
|
+
timeTotal: MAX_TIME,
|
|
74
|
+
timeConnect: 0,
|
|
75
|
+
timeStartTransfer: 0,
|
|
76
|
+
sizeDownload: 0,
|
|
77
|
+
redirectUrl: "",
|
|
78
|
+
error: errorMsg,
|
|
79
|
+
category: "dead",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// stdout: body + sentinel line with metrics
|
|
84
|
+
const stdout = result.stdout || "";
|
|
85
|
+
const sentinel = "__CURL_META__";
|
|
86
|
+
const sentinelIdx = stdout.lastIndexOf(sentinel);
|
|
87
|
+
|
|
88
|
+
let body = "";
|
|
89
|
+
let metaLine = "";
|
|
90
|
+
if (sentinelIdx >= 0) {
|
|
91
|
+
body = stdout.substring(0, sentinelIdx).trimEnd();
|
|
92
|
+
metaLine = stdout.substring(sentinelIdx + sentinel.length).trim();
|
|
93
|
+
} else {
|
|
94
|
+
// fallback: try last line
|
|
95
|
+
const lines = stdout.split("\n");
|
|
96
|
+
metaLine = (lines[lines.length - 1] || "").trim();
|
|
97
|
+
body = lines.slice(0, -1).join("\n");
|
|
98
|
+
}
|
|
99
|
+
const parts = metaLine.split("|");
|
|
100
|
+
|
|
101
|
+
const httpCode = parseInt(parts[0]) || 0;
|
|
102
|
+
const timeTotal = parseFloat(parts[1]) || 0;
|
|
103
|
+
const timeConnect = parseFloat(parts[2]) || 0;
|
|
104
|
+
const timeStartTransfer = parseFloat(parts[3]) || 0;
|
|
105
|
+
const sizeDownload = parseInt(parts[4]) || 0;
|
|
106
|
+
const redirectUrl = parts[5] || "";
|
|
107
|
+
|
|
108
|
+
// 内容分类
|
|
109
|
+
let category = "unknown";
|
|
110
|
+
let title = "";
|
|
111
|
+
const titleMatch = body.match(/<title>([^<]*)<\/title>/i);
|
|
112
|
+
if (titleMatch) title = titleMatch[1].trim();
|
|
113
|
+
|
|
114
|
+
if (httpCode !== 200) {
|
|
115
|
+
category = "blocked";
|
|
116
|
+
} else if (body.includes("SlardarWAF") || body.includes("_wafchallengeid")) {
|
|
117
|
+
category = "waf"; // WAF 挑战页 — TikTok 拦截了该 IP
|
|
118
|
+
} else if (title === "About | TikTok" || title.startsWith("About |")) {
|
|
119
|
+
category = "redirect"; // 地区不支持,重定向到 About 页
|
|
120
|
+
} else if (title === "TikTok - Make Your Day") {
|
|
121
|
+
category = "real"; // 真正的 TikTok 首页
|
|
122
|
+
} else if (sizeDownload < 5000) {
|
|
123
|
+
category = "waf"; // 极小页面,大概率是被拦截
|
|
124
|
+
} else {
|
|
125
|
+
category = "real"; // 有内容但不是标准首页(也算能访问)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
httpCode,
|
|
130
|
+
timeTotal,
|
|
131
|
+
timeConnect,
|
|
132
|
+
timeStartTransfer,
|
|
133
|
+
sizeDownload,
|
|
134
|
+
redirectUrl,
|
|
135
|
+
title,
|
|
136
|
+
category,
|
|
137
|
+
error: null,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ---- 等待函数 ----
|
|
142
|
+
function sleep(ms) {
|
|
143
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---- 主流程 ----
|
|
147
|
+
async function main() {
|
|
148
|
+
console.log(`TikTok 节点速度测试 — ${TARGET_URL}`);
|
|
149
|
+
console.log(`超时: ${MAX_TIME}s | 代理: ${PROXY}\n`);
|
|
150
|
+
|
|
151
|
+
// 1. 记录当前节点(用于恢复)
|
|
152
|
+
const originalNode = await api.getCurrentNode(GROUP);
|
|
153
|
+
console.log(`当前节点: ${originalNode}`);
|
|
154
|
+
|
|
155
|
+
// 2. 获取所有存活节点
|
|
156
|
+
const allNodes = await api.getNodes();
|
|
157
|
+
const aliveNodes = allNodes.filter(
|
|
158
|
+
(n) =>
|
|
159
|
+
n.alive &&
|
|
160
|
+
n.name !== "REJECT" &&
|
|
161
|
+
n.name !== "REJECT-DROP" &&
|
|
162
|
+
n.name !== "DIRECT" &&
|
|
163
|
+
n.name !== "PASS" &&
|
|
164
|
+
n.name !== "PASS-RULE" &&
|
|
165
|
+
n.name !== "COMPATIBLE" &&
|
|
166
|
+
!n.name.includes("剩余流量") &&
|
|
167
|
+
!n.name.includes("套餐到期") &&
|
|
168
|
+
!n.name.includes("距离下次重置") &&
|
|
169
|
+
!n.name.includes("若无HY") &&
|
|
170
|
+
!n.name.includes("请每月更") &&
|
|
171
|
+
!n.name.includes("白嫖机场") &&
|
|
172
|
+
!n.name.includes("ipv6免流") &&
|
|
173
|
+
!n.name.includes("ios-小火箭") &&
|
|
174
|
+
!n.name.includes("安卓-clashmeta") &&
|
|
175
|
+
!n.name.includes("电脑-clashverge") &&
|
|
176
|
+
n.type !== "Reject" &&
|
|
177
|
+
n.type !== "RejectDrop",
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
console.log(`待测存活节点: ${aliveNodes.length} 个\n`);
|
|
181
|
+
|
|
182
|
+
// 先测 GLOBAL 组可选列表中的节点
|
|
183
|
+
const groupNodes = await api.listGroupNodes(GROUP);
|
|
184
|
+
const testNodes = aliveNodes.filter((n) => groupNodes.includes(n.name));
|
|
185
|
+
|
|
186
|
+
console.log(`其中在 GLOBAL 组中的: ${testNodes.length} 个\n`);
|
|
187
|
+
|
|
188
|
+
const results = [];
|
|
189
|
+
let count = 0;
|
|
190
|
+
|
|
191
|
+
for (const node of testNodes) {
|
|
192
|
+
count++;
|
|
193
|
+
process.stdout.write(
|
|
194
|
+
`[${String(count).padStart(2)}/${testNodes.length}] ${node.name.padEnd(40)} `,
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// 切换节点
|
|
198
|
+
try {
|
|
199
|
+
await api.switchProxy(GROUP, node.name);
|
|
200
|
+
} catch (err) {
|
|
201
|
+
console.log(`✗ 切换失败: ${err.message}`);
|
|
202
|
+
results.push({
|
|
203
|
+
node: node.name,
|
|
204
|
+
type: node.type,
|
|
205
|
+
httpCode: 0,
|
|
206
|
+
timeTotal: 0,
|
|
207
|
+
error: `switch failed: ${err.message}`,
|
|
208
|
+
});
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 等待代理生效
|
|
213
|
+
await sleep(DELAY_MS);
|
|
214
|
+
|
|
215
|
+
// 测试
|
|
216
|
+
const r = testViaCurl();
|
|
217
|
+
results.push({ node: node.name, type: node.type, ...r });
|
|
218
|
+
|
|
219
|
+
// 输出结果(按类别)
|
|
220
|
+
const icons = {
|
|
221
|
+
real: "✅",
|
|
222
|
+
redirect: "🔄",
|
|
223
|
+
waf: "🛡",
|
|
224
|
+
blocked: "🚫",
|
|
225
|
+
dead: "❌",
|
|
226
|
+
unknown: "❓",
|
|
227
|
+
};
|
|
228
|
+
const icon = icons[r.category] || "❓";
|
|
229
|
+
const sizeStr =
|
|
230
|
+
r.sizeDownload > 0 ? ` (${(r.sizeDownload / 1024).toFixed(0)}KB)` : "";
|
|
231
|
+
console.log(
|
|
232
|
+
`${icon} ${r.httpCode} ${r.timeTotal.toFixed(1)}s${sizeStr} ${r.category}`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// 3. 恢复原节点
|
|
237
|
+
console.log(`\n恢复原节点: ${originalNode} ...`);
|
|
238
|
+
await api.switchProxy(GROUP, originalNode);
|
|
239
|
+
await sleep(500);
|
|
240
|
+
const restored = await api.getCurrentNode(GROUP);
|
|
241
|
+
console.log(`当前节点: ${restored} ${restored === originalNode ? "✓" : "✗"}`);
|
|
242
|
+
|
|
243
|
+
// 4. 汇总报告
|
|
244
|
+
console.log("\n" + "=".repeat(80));
|
|
245
|
+
console.log("TikTok 节点速度测试 — 结果汇总");
|
|
246
|
+
console.log("=".repeat(80));
|
|
247
|
+
|
|
248
|
+
const real = results.filter((r) => r.category === "real");
|
|
249
|
+
const redirect = results.filter((r) => r.category === "redirect");
|
|
250
|
+
const waf = results.filter((r) => r.category === "waf");
|
|
251
|
+
const dead = results.filter((r) => r.category === "dead");
|
|
252
|
+
const blocked = results.filter((r) => r.category === "blocked");
|
|
253
|
+
|
|
254
|
+
// 真正可用的节点按速度排序
|
|
255
|
+
real.sort((a, b) => a.timeTotal - b.timeTotal);
|
|
256
|
+
|
|
257
|
+
if (real.length > 0) {
|
|
258
|
+
console.log(`\n✅ 真正可访问 TikTok (${real.length} 个节点):\n`);
|
|
259
|
+
console.log(` 标题为 "TikTok - Make Your Day",返回 350KB+ 完整页面\n`);
|
|
260
|
+
const display = TOP_N > 0 ? real.slice(0, TOP_N) : real;
|
|
261
|
+
for (let i = 0; i < display.length; i++) {
|
|
262
|
+
const r = display[i];
|
|
263
|
+
const rank = i + 1;
|
|
264
|
+
const speedBar = "█".repeat(Math.min(25, Math.round(r.timeTotal * 3)));
|
|
265
|
+
console.log(
|
|
266
|
+
` ${String(rank).padStart(2)}. [${r.type.padEnd(12)}] ${r.node.padEnd(40)} ${r.timeTotal.toFixed(1)}s ${speedBar}`,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (redirect.length > 0) {
|
|
272
|
+
console.log(
|
|
273
|
+
`\n🔄 地区不支持,重定向到 About 页 (${redirect.length} 个节点):`,
|
|
274
|
+
);
|
|
275
|
+
console.log(` 这些节点的 IP 被 TikTok 识别为非支持地区\n`);
|
|
276
|
+
for (const r of redirect) {
|
|
277
|
+
console.log(
|
|
278
|
+
` [${r.type.padEnd(12)}] ${r.node.padEnd(40)} ${r.timeTotal.toFixed(1)}s → "${r.title}"`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (waf.length > 0) {
|
|
284
|
+
console.log(`\n🛡 WAF 挑战拦截 (${waf.length} 个节点):`);
|
|
285
|
+
console.log(` TikTok 的 SlardarWAF 拦截了这些节点的 IP\n`);
|
|
286
|
+
for (const r of waf) {
|
|
287
|
+
console.log(
|
|
288
|
+
` [${r.type.padEnd(12)}] ${r.node.padEnd(40)} ${r.timeTotal.toFixed(1)}s`,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (blocked.length > 0) {
|
|
294
|
+
console.log(`\n🚫 HTTP 非 200 (${blocked.length} 个节点):\n`);
|
|
295
|
+
for (const r of blocked) {
|
|
296
|
+
console.log(
|
|
297
|
+
` [${r.type.padEnd(12)}] ${r.node.padEnd(40)} HTTP ${r.httpCode}`,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (dead.length > 0) {
|
|
303
|
+
console.log(`\n❌ 完全不通 (${dead.length} 个节点):\n`);
|
|
304
|
+
for (const r of dead) {
|
|
305
|
+
console.log(
|
|
306
|
+
` [${r.type.padEnd(12)}] ${r.node.padEnd(40)} ${r.error || "no response"}`,
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
console.log(`\n`);
|
|
312
|
+
console.log(
|
|
313
|
+
`┌───────────────┬───────┬────────────────────────────────────────────┐`,
|
|
314
|
+
);
|
|
315
|
+
console.log(
|
|
316
|
+
`│ 类别 │ 数量 │ 说明 │`,
|
|
317
|
+
);
|
|
318
|
+
console.log(
|
|
319
|
+
`├───────────────┼───────┼────────────────────────────────────────────┤`,
|
|
320
|
+
);
|
|
321
|
+
console.log(
|
|
322
|
+
`│ ✅ 真正可用 │ ${String(real.length).padStart(5)} │ 完整 TikTok 首页,可以正常使用 │`,
|
|
323
|
+
);
|
|
324
|
+
console.log(
|
|
325
|
+
`│ 🔄 地区限制 │ ${String(redirect.length).padStart(5)} │ TikTok 不支持该 IP 所在地区 │`,
|
|
326
|
+
);
|
|
327
|
+
console.log(
|
|
328
|
+
`│ 🛡 WAF 拦截 │ ${String(waf.length).padStart(5)} │ SlardarWAF 机器人检测,需浏览器解挑战 │`,
|
|
329
|
+
);
|
|
330
|
+
console.log(
|
|
331
|
+
`│ ❌ 不通 │ ${String(dead.length).padStart(5)} │ 超时或连接失败 │`,
|
|
332
|
+
);
|
|
333
|
+
console.log(
|
|
334
|
+
`│ 🚫 HTTP 错误 │ ${String(blocked.length).padStart(5)} │ 返回非 200 状态码 │`,
|
|
335
|
+
);
|
|
336
|
+
console.log(
|
|
337
|
+
`├───────────────┼───────┼────────────────────────────────────────────┤`,
|
|
338
|
+
);
|
|
339
|
+
console.log(
|
|
340
|
+
`│ 总计 │ ${String(results.length).padStart(5)} │ │`,
|
|
341
|
+
);
|
|
342
|
+
console.log(
|
|
343
|
+
`└───────────────┴───────┴────────────────────────────────────────────┘`,
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
// 导出 JSON
|
|
347
|
+
const fs = await import("fs");
|
|
348
|
+
const report = {
|
|
349
|
+
testTime: new Date().toISOString(),
|
|
350
|
+
target: TARGET_URL,
|
|
351
|
+
summary: {
|
|
352
|
+
total: results.length,
|
|
353
|
+
real: real.length,
|
|
354
|
+
redirect: redirect.length,
|
|
355
|
+
waf: waf.length,
|
|
356
|
+
dead: dead.length,
|
|
357
|
+
blocked: blocked.length,
|
|
358
|
+
},
|
|
359
|
+
realNodes: real.map((r) => ({
|
|
360
|
+
node: r.node,
|
|
361
|
+
type: r.type,
|
|
362
|
+
timeTotal: r.timeTotal,
|
|
363
|
+
timeConnect: r.timeConnect,
|
|
364
|
+
sizeDownload: r.sizeDownload,
|
|
365
|
+
})),
|
|
366
|
+
allResults: results.map((r) => ({
|
|
367
|
+
node: r.node,
|
|
368
|
+
type: r.type,
|
|
369
|
+
category: r.category,
|
|
370
|
+
httpCode: r.httpCode,
|
|
371
|
+
timeTotal: r.timeTotal,
|
|
372
|
+
sizeDownload: r.sizeDownload,
|
|
373
|
+
title: r.title,
|
|
374
|
+
error: r.error || null,
|
|
375
|
+
})),
|
|
376
|
+
};
|
|
377
|
+
fs.writeFileSync("speedtest-result.json", JSON.stringify(report, null, 2));
|
|
378
|
+
console.log(`\n详细结果已保存到 speedtest-result.json`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
main().catch((err) => {
|
|
382
|
+
console.error("脚本错误:", err);
|
|
383
|
+
process.exit(1);
|
|
384
|
+
});
|