tt-help-cli-ycl 1.4.80 → 1.4.82

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tt-help-cli-ycl",
3
- "version": "1.4.80",
3
+ "version": "1.4.82",
4
4
  "description": "TikTok user & video data scraper - extract ttSeller, verified, locationCreated from HTML source",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Clash Verge (mihomo) RESTful API 客户端
3
+ *
4
+ * API 参考: https://wiki.metacubex.one/en/api
5
+ * 认证方式: Authorization: Bearer ${secret}
6
+ * 依赖 Clash Verge GUI 运行时绑定的 TCP 127.0.0.1:9097
7
+ */
8
+
9
+ const BASE_URL = "http://127.0.0.1:9097";
10
+ const SECRET = "abcdefg";
11
+
12
+ function headers() {
13
+ return {
14
+ Authorization: `Bearer ${SECRET}`,
15
+ "Content-Type": "application/json",
16
+ };
17
+ }
18
+
19
+ async function request(path, options = {}) {
20
+ const url = `${BASE_URL}${path}`;
21
+ const res = await fetch(url, {
22
+ ...options,
23
+ headers: { ...headers(), ...options.headers },
24
+ });
25
+ if (!res.ok) {
26
+ const body = await res.text().catch(() => "");
27
+ throw new Error(`HTTP ${res.status} ${res.statusText}: ${body}`);
28
+ }
29
+ if (res.status === 204) return null;
30
+ const text = await res.text();
31
+ return text ? JSON.parse(text) : null;
32
+ }
33
+
34
+ // ===== 代理相关 =====
35
+
36
+ /** 获取所有代理(含代理组和节点) */
37
+ export async function getProxies() {
38
+ return request("/proxies");
39
+ }
40
+
41
+ /** 获取单个代理/组详情 */
42
+ export async function getProxy(name) {
43
+ return request(`/proxies/${encodeURIComponent(name)}`);
44
+ }
45
+
46
+ /** 切换代理组的选中节点 */
47
+ export async function switchProxy(groupName, nodeName) {
48
+ return request(`/proxies/${encodeURIComponent(groupName)}`, {
49
+ method: "PUT",
50
+ body: JSON.stringify({ name: nodeName }),
51
+ });
52
+ }
53
+
54
+ /**
55
+ * 获取所有代理组(Selector/URLTest/Fallback/LoadBalance 类型)
56
+ * 返回: [{ name, type, now, all }]
57
+ */
58
+ export async function getGroups() {
59
+ const data = await getProxies();
60
+ const proxies = data.proxies || {};
61
+ const groups = [];
62
+ const groupTypes = new Set([
63
+ "Selector",
64
+ "URLTest",
65
+ "Fallback",
66
+ "LoadBalance",
67
+ ]);
68
+
69
+ for (const [name, info] of Object.entries(proxies)) {
70
+ if (groupTypes.has(info.type)) {
71
+ groups.push({
72
+ name,
73
+ type: info.type,
74
+ now: info.now || "",
75
+ all: info.all || [],
76
+ });
77
+ }
78
+ }
79
+ return groups;
80
+ }
81
+
82
+ /**
83
+ * 获取所有独立节点(非代理组)
84
+ * 返回: [{ name, type, alive, delay }]
85
+ */
86
+ export async function getNodes() {
87
+ const data = await getProxies();
88
+ const proxies = data.proxies || {};
89
+ const nodes = [];
90
+ const groupTypes = new Set([
91
+ "Selector",
92
+ "URLTest",
93
+ "Fallback",
94
+ "LoadBalance",
95
+ ]);
96
+
97
+ for (const [name, info] of Object.entries(proxies)) {
98
+ if (!groupTypes.has(info.type)) {
99
+ nodes.push({
100
+ name,
101
+ type: info.type,
102
+ alive: info.alive ?? false,
103
+ delay:
104
+ info.history?.length > 0
105
+ ? info.history[info.history.length - 1].delay
106
+ : null,
107
+ udp: info.udp ?? false,
108
+ });
109
+ }
110
+ }
111
+ return nodes;
112
+ }
113
+
114
+ // ===== 配置相关 =====
115
+
116
+ /** 获取当前运行配置 */
117
+ export async function getConfigs() {
118
+ return request("/configs");
119
+ }
120
+
121
+ /** 更新运行配置(部分更新) */
122
+ export async function patchConfigs(patch) {
123
+ return request("/configs", {
124
+ method: "PATCH",
125
+ body: JSON.stringify(patch),
126
+ });
127
+ }
128
+
129
+ /** 强制重载配置 */
130
+ export async function reloadConfigs() {
131
+ return request("/configs?force=true", { method: "PUT" });
132
+ }
133
+
134
+ // ===== 系统 =====
135
+
136
+ /** 获取内核版本 */
137
+ export async function getVersion() {
138
+ return request("/version");
139
+ }
140
+
141
+ /** 获取内存使用 */
142
+ export async function getMemory() {
143
+ return request("/memory");
144
+ }
145
+
146
+ // ===== 连接 =====
147
+
148
+ /** 获取当前活跃连接 */
149
+ export async function getConnections() {
150
+ return request("/connections");
151
+ }
152
+
153
+ /** 关闭指定连接 */
154
+ export async function closeConnection(id) {
155
+ return request(`/connections/${id}`, { method: "DELETE" });
156
+ }
157
+
158
+ /** 关闭所有连接 */
159
+ export async function closeAllConnections() {
160
+ return request("/connections", { method: "DELETE" });
161
+ }
162
+
163
+ // ===== 规则 =====
164
+
165
+ /** 获取规则列表 */
166
+ export async function getRules() {
167
+ return request("/rules");
168
+ }
169
+
170
+ // ===== 代理提供者 =====
171
+
172
+ /** 获取代理提供者列表 */
173
+ export async function getProxyProviders() {
174
+ return request("/providers/proxies");
175
+ }
176
+
177
+ /** 更新代理提供者 */
178
+ export async function updateProxyProvider(name) {
179
+ return request(`/providers/proxies/${encodeURIComponent(name)}`, {
180
+ method: "PUT",
181
+ });
182
+ }
183
+
184
+ /** 代理提供者健康检查 */
185
+ export async function healthCheckProvider(name) {
186
+ return request(`/providers/proxies/${encodeURIComponent(name)}/healthcheck`);
187
+ }
188
+
189
+ // ===== 便捷函数 =====
190
+
191
+ /**
192
+ * 获取指定组当前选中节点
193
+ */
194
+ export async function getCurrentNode(groupName) {
195
+ const proxy = await getProxy(groupName);
196
+ return proxy.now || null;
197
+ }
198
+
199
+ /**
200
+ * 列出指定组的所有可选节点
201
+ */
202
+ export async function listGroupNodes(groupName) {
203
+ const proxy = await getProxy(groupName);
204
+ return proxy.all || [];
205
+ }
206
+
207
+ /**
208
+ * 获取所有存活节点(按延迟排序)
209
+ */
210
+ export async function getAliveNodes() {
211
+ const nodes = await getNodes();
212
+ return nodes
213
+ .filter((n) => n.alive && n.delay !== null && n.delay > 0)
214
+ .sort((a, b) => (a.delay ?? Infinity) - (b.delay ?? Infinity));
215
+ }
216
+
217
+ /**
218
+ * 切换到延迟最低的存活节点
219
+ */
220
+ export async function switchToFastest(groupName) {
221
+ const alive = await getAliveNodes();
222
+ if (alive.length === 0) {
223
+ throw new Error("没有存活的节点");
224
+ }
225
+ // 需要确认目标节点在该组的 all 列表中
226
+ const groupNodes = await listGroupNodes(groupName);
227
+ for (const node of alive) {
228
+ if (groupNodes.includes(node.name)) {
229
+ await switchProxy(groupName, node.name);
230
+ return node;
231
+ }
232
+ }
233
+ throw new Error("存活节点都不在目标组中");
234
+ }
@@ -0,0 +1,431 @@
1
+ {
2
+ "testTime": "2026-08-10T08:47:10.485Z",
3
+ "target": "https://www.tiktok.com",
4
+ "summary": {
5
+ "total": 30,
6
+ "real": 17,
7
+ "redirect": 2,
8
+ "waf": 7,
9
+ "dead": 4,
10
+ "blocked": 0
11
+ },
12
+ "realNodes": [
13
+ {
14
+ "node": "🇯🇵日本Trojan1",
15
+ "type": "Trojan",
16
+ "timeTotal": 1.174005,
17
+ "timeConnect": 0.000338,
18
+ "sizeDownload": 12579
19
+ },
20
+ {
21
+ "node": "🇷🇺俄罗斯HY专线去广告",
22
+ "type": "Hysteria2",
23
+ "timeTotal": 1.269673,
24
+ "timeConnect": 0.00025,
25
+ "sizeDownload": 12579
26
+ },
27
+ {
28
+ "node": "🇬🇧英国HY1-1000M专线",
29
+ "type": "Hysteria2",
30
+ "timeTotal": 1.357936,
31
+ "timeConnect": 0.000343,
32
+ "sizeDownload": 364873
33
+ },
34
+ {
35
+ "node": "🇺🇸美国HY2轻量-小火箭也能用HY",
36
+ "type": "Hysteria2",
37
+ "timeTotal": 1.512879,
38
+ "timeConnect": 0.000324,
39
+ "sizeDownload": 363070
40
+ },
41
+ {
42
+ "node": "🇺🇸美国HY5神速-推荐",
43
+ "type": "Hysteria2",
44
+ "timeTotal": 1.632324,
45
+ "timeConnect": 0.000263,
46
+ "sizeDownload": 363348
47
+ },
48
+ {
49
+ "node": "🇺🇸美国HY3神速-小火箭无延迟也能用",
50
+ "type": "Hysteria2",
51
+ "timeTotal": 1.659374,
52
+ "timeConnect": 0.000399,
53
+ "sizeDownload": 361805
54
+ },
55
+ {
56
+ "node": "🇺🇸美国HY1轻量",
57
+ "type": "Hysteria2",
58
+ "timeTotal": 1.793751,
59
+ "timeConnect": 0.000252,
60
+ "sizeDownload": 362788
61
+ },
62
+ {
63
+ "node": "🇺🇸美国光速1-解锁GPT",
64
+ "type": "Trojan",
65
+ "timeTotal": 1.910699,
66
+ "timeConnect": 0.000271,
67
+ "sizeDownload": 362778
68
+ },
69
+ {
70
+ "node": "🇺🇸美国HY高速V6",
71
+ "type": "Hysteria2",
72
+ "timeTotal": 2.079931,
73
+ "timeConnect": 0.000351,
74
+ "sizeDownload": 362679
75
+ },
76
+ {
77
+ "node": "🇬🇧英国trojan1",
78
+ "type": "Trojan",
79
+ "timeTotal": 2.646707,
80
+ "timeConnect": 0.00038,
81
+ "sizeDownload": 364594
82
+ },
83
+ {
84
+ "node": "🇺🇸美国2-baipiaoyes.top",
85
+ "type": "Trojan",
86
+ "timeTotal": 3.019101,
87
+ "timeConnect": 0.000339,
88
+ "sizeDownload": 363541
89
+ },
90
+ {
91
+ "node": "🇷🇺去油管广告",
92
+ "type": "Trojan",
93
+ "timeTotal": 4.092772,
94
+ "timeConnect": 0.000307,
95
+ "sizeDownload": 390066
96
+ },
97
+ {
98
+ "node": "🇯🇵日本Trojan3",
99
+ "type": "Trojan",
100
+ "timeTotal": 4.212222,
101
+ "timeConnect": 0.000397,
102
+ "sizeDownload": 12579
103
+ },
104
+ {
105
+ "node": "🇺🇸美国HY4神速-1000M",
106
+ "type": "Hysteria2",
107
+ "timeTotal": 4.569531,
108
+ "timeConnect": 0.000262,
109
+ "sizeDownload": 362573
110
+ },
111
+ {
112
+ "node": "🇺🇸美国-快",
113
+ "type": "Vless",
114
+ "timeTotal": 4.572119,
115
+ "timeConnect": 0.000403,
116
+ "sizeDownload": 362706
117
+ },
118
+ {
119
+ "node": "🇺🇸美国V6666",
120
+ "type": "Vless",
121
+ "timeTotal": 5.088888,
122
+ "timeConnect": 0.000237,
123
+ "sizeDownload": 362522
124
+ },
125
+ {
126
+ "node": "🇯🇵日本Trojan2",
127
+ "type": "Trojan",
128
+ "timeTotal": 6.612371,
129
+ "timeConnect": 0.000275,
130
+ "sizeDownload": 12579
131
+ }
132
+ ],
133
+ "allResults": [
134
+ {
135
+ "node": "Youtube去广告1",
136
+ "type": "Trojan",
137
+ "category": "dead",
138
+ "httpCode": 0,
139
+ "timeTotal": 12,
140
+ "sizeDownload": 0,
141
+ "error": "exit 28"
142
+ },
143
+ {
144
+ "node": "🇬🇧英国HY1-1000M专线",
145
+ "type": "Hysteria2",
146
+ "category": "real",
147
+ "httpCode": 200,
148
+ "timeTotal": 1.357936,
149
+ "sizeDownload": 364873,
150
+ "title": "TikTok - Make Your Day",
151
+ "error": null
152
+ },
153
+ {
154
+ "node": "🇬🇧英国trojan1",
155
+ "type": "Trojan",
156
+ "category": "real",
157
+ "httpCode": 200,
158
+ "timeTotal": 2.646707,
159
+ "sizeDownload": 364594,
160
+ "title": "TikTok - Make Your Day",
161
+ "error": null
162
+ },
163
+ {
164
+ "node": "🇬🇧英国trojan2",
165
+ "type": "Trojan",
166
+ "category": "dead",
167
+ "httpCode": 0,
168
+ "timeTotal": 12,
169
+ "sizeDownload": 0,
170
+ "error": "exit 35"
171
+ },
172
+ {
173
+ "node": "🇭🇰香港Gemini",
174
+ "type": "Vless",
175
+ "category": "redirect",
176
+ "httpCode": 200,
177
+ "timeTotal": 1.655845,
178
+ "sizeDownload": 181106,
179
+ "title": "About | TikTok",
180
+ "error": null
181
+ },
182
+ {
183
+ "node": "🇭🇰香港HY1-1000M专线",
184
+ "type": "Hysteria2",
185
+ "category": "waf",
186
+ "httpCode": 200,
187
+ "timeTotal": 0.370995,
188
+ "sizeDownload": 1462,
189
+ "title": "",
190
+ "error": null
191
+ },
192
+ {
193
+ "node": "🇭🇰香港trojan2",
194
+ "type": "Trojan",
195
+ "category": "redirect",
196
+ "httpCode": 200,
197
+ "timeTotal": 4.38978,
198
+ "sizeDownload": 181106,
199
+ "title": "About | TikTok",
200
+ "error": null
201
+ },
202
+ {
203
+ "node": "🇯🇵日本Trojan1",
204
+ "type": "Trojan",
205
+ "category": "real",
206
+ "httpCode": 200,
207
+ "timeTotal": 1.174005,
208
+ "sizeDownload": 12579,
209
+ "title": "",
210
+ "error": null
211
+ },
212
+ {
213
+ "node": "🇯🇵日本Trojan2",
214
+ "type": "Trojan",
215
+ "category": "real",
216
+ "httpCode": 200,
217
+ "timeTotal": 6.612371,
218
+ "sizeDownload": 12579,
219
+ "title": "",
220
+ "error": null
221
+ },
222
+ {
223
+ "node": "🇯🇵日本Trojan3",
224
+ "type": "Trojan",
225
+ "category": "real",
226
+ "httpCode": 200,
227
+ "timeTotal": 4.212222,
228
+ "sizeDownload": 12579,
229
+ "title": "",
230
+ "error": null
231
+ },
232
+ {
233
+ "node": "🇲🇾马来西亚Trojan1",
234
+ "type": "Trojan",
235
+ "category": "waf",
236
+ "httpCode": 200,
237
+ "timeTotal": 0.702351,
238
+ "sizeDownload": 1462,
239
+ "title": "",
240
+ "error": null
241
+ },
242
+ {
243
+ "node": "🇲🇾马来西亚Trojan2",
244
+ "type": "Trojan",
245
+ "category": "waf",
246
+ "httpCode": 200,
247
+ "timeTotal": 1.593941,
248
+ "sizeDownload": 1462,
249
+ "title": "",
250
+ "error": null
251
+ },
252
+ {
253
+ "node": "🇷🇺俄罗斯HY专线去广告",
254
+ "type": "Hysteria2",
255
+ "category": "real",
256
+ "httpCode": 200,
257
+ "timeTotal": 1.269673,
258
+ "sizeDownload": 12579,
259
+ "title": "",
260
+ "error": null
261
+ },
262
+ {
263
+ "node": "🇷🇺俄罗斯V6-油管去广告",
264
+ "type": "Vless",
265
+ "category": "waf",
266
+ "httpCode": 200,
267
+ "timeTotal": 2.011837,
268
+ "sizeDownload": 1462,
269
+ "title": "",
270
+ "error": null
271
+ },
272
+ {
273
+ "node": "🇷🇺去油管广告",
274
+ "type": "Trojan",
275
+ "category": "real",
276
+ "httpCode": 200,
277
+ "timeTotal": 4.092772,
278
+ "sizeDownload": 390066,
279
+ "title": "TikTok - Make Your Day",
280
+ "error": null
281
+ },
282
+ {
283
+ "node": "🇸🇬新加坡Trojan1",
284
+ "type": "Trojan",
285
+ "category": "waf",
286
+ "httpCode": 200,
287
+ "timeTotal": 0.56045,
288
+ "sizeDownload": 1462,
289
+ "title": "",
290
+ "error": null
291
+ },
292
+ {
293
+ "node": "🇹🇼台湾trojan1",
294
+ "type": "Trojan",
295
+ "category": "waf",
296
+ "httpCode": 200,
297
+ "timeTotal": 3.067687,
298
+ "sizeDownload": 1462,
299
+ "title": "",
300
+ "error": null
301
+ },
302
+ {
303
+ "node": "🇹🇼台湾trojan2",
304
+ "type": "Trojan",
305
+ "category": "dead",
306
+ "httpCode": 0,
307
+ "timeTotal": 12,
308
+ "sizeDownload": 0,
309
+ "error": "exit 35"
310
+ },
311
+ {
312
+ "node": "🇹🇼台湾trojan直连",
313
+ "type": "Trojan",
314
+ "category": "waf",
315
+ "httpCode": 200,
316
+ "timeTotal": 6.785391,
317
+ "sizeDownload": 1462,
318
+ "title": "",
319
+ "error": null
320
+ },
321
+ {
322
+ "node": "🇺🇸美国-快",
323
+ "type": "Vless",
324
+ "category": "real",
325
+ "httpCode": 200,
326
+ "timeTotal": 4.572119,
327
+ "sizeDownload": 362706,
328
+ "title": "TikTok - Make Your Day",
329
+ "error": null
330
+ },
331
+ {
332
+ "node": "🇺🇸美国1-baipiaoyes.top",
333
+ "type": "Trojan",
334
+ "category": "dead",
335
+ "httpCode": 0,
336
+ "timeTotal": 12,
337
+ "sizeDownload": 0,
338
+ "error": "exit 28"
339
+ },
340
+ {
341
+ "node": "🇺🇸美国2-baipiaoyes.top",
342
+ "type": "Trojan",
343
+ "category": "real",
344
+ "httpCode": 200,
345
+ "timeTotal": 3.019101,
346
+ "sizeDownload": 363541,
347
+ "title": "TikTok - Make Your Day",
348
+ "error": null
349
+ },
350
+ {
351
+ "node": "🇺🇸美国HY1轻量",
352
+ "type": "Hysteria2",
353
+ "category": "real",
354
+ "httpCode": 200,
355
+ "timeTotal": 1.793751,
356
+ "sizeDownload": 362788,
357
+ "title": "TikTok - Make Your Day",
358
+ "error": null
359
+ },
360
+ {
361
+ "node": "🇺🇸美国HY2轻量-小火箭也能用HY",
362
+ "type": "Hysteria2",
363
+ "category": "real",
364
+ "httpCode": 200,
365
+ "timeTotal": 1.512879,
366
+ "sizeDownload": 363070,
367
+ "title": "TikTok - Make Your Day",
368
+ "error": null
369
+ },
370
+ {
371
+ "node": "🇺🇸美国HY3神速-小火箭无延迟也能用",
372
+ "type": "Hysteria2",
373
+ "category": "real",
374
+ "httpCode": 200,
375
+ "timeTotal": 1.659374,
376
+ "sizeDownload": 361805,
377
+ "title": "TikTok - Make Your Day",
378
+ "error": null
379
+ },
380
+ {
381
+ "node": "🇺🇸美国HY4神速-1000M",
382
+ "type": "Hysteria2",
383
+ "category": "real",
384
+ "httpCode": 200,
385
+ "timeTotal": 4.569531,
386
+ "sizeDownload": 362573,
387
+ "title": "TikTok - Make Your Day",
388
+ "error": null
389
+ },
390
+ {
391
+ "node": "🇺🇸美国HY5神速-推荐",
392
+ "type": "Hysteria2",
393
+ "category": "real",
394
+ "httpCode": 200,
395
+ "timeTotal": 1.632324,
396
+ "sizeDownload": 363348,
397
+ "title": "TikTok - Make Your Day",
398
+ "error": null
399
+ },
400
+ {
401
+ "node": "🇺🇸美国HY高速V6",
402
+ "type": "Hysteria2",
403
+ "category": "real",
404
+ "httpCode": 200,
405
+ "timeTotal": 2.079931,
406
+ "sizeDownload": 362679,
407
+ "title": "TikTok - Make Your Day",
408
+ "error": null
409
+ },
410
+ {
411
+ "node": "🇺🇸美国V6666",
412
+ "type": "Vless",
413
+ "category": "real",
414
+ "httpCode": 200,
415
+ "timeTotal": 5.088888,
416
+ "sizeDownload": 362522,
417
+ "title": "TikTok - Make Your Day",
418
+ "error": null
419
+ },
420
+ {
421
+ "node": "🇺🇸美国光速1-解锁GPT",
422
+ "type": "Trojan",
423
+ "category": "real",
424
+ "httpCode": 200,
425
+ "timeTotal": 1.910699,
426
+ "sizeDownload": 362778,
427
+ "title": "TikTok - Make Your Day",
428
+ "error": null
429
+ }
430
+ ]
431
+ }
@@ -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
+ });
@@ -373,6 +373,9 @@ function _buildCmdHandlers(daemon) {
373
373
 
374
374
  function _findBenchmark() {
375
375
  for (const p of [
376
+ // 1. 包内 benchmark(npm publish 后 src/clash/benchmark/speedtest-tiktok.mjs)
377
+ join(__dirname, "benchmark", "speedtest-tiktok.mjs"),
378
+ // 2. 开发模式:clash-verge-explore 同级目录
376
379
  join(
377
380
  __dirname,
378
381
  "..",
@@ -7,6 +7,7 @@
7
7
  * --server <URL> 服务端地址(默认使用 ~/.tt-help.json 中的 server)
8
8
  * --interval <秒> 心跳间隔,默认 15 秒
9
9
  * --client-id <ID> 客户端标识(默认本机 hostname)
10
+ * --no-daemon 不启动 clash daemon(默认启动)
10
11
  */
11
12
 
12
13
  import os from "os";
@@ -14,7 +15,12 @@ import { WatchdogAgent } from "../watchdog/agent.js";
14
15
  import { getClientConfig } from "../watchdog/reporter.js";
15
16
 
16
17
  export async function handleWatchdog(options) {
17
- const { watchdogServer, watchdogInterval, watchdogClientId } = options;
18
+ const {
19
+ watchdogServer,
20
+ watchdogInterval,
21
+ watchdogClientId,
22
+ watchdogNoDaemon,
23
+ } = options;
18
24
 
19
25
  // 服务端地址:--server 参数 > ~/.tt-help.json server > 默认值
20
26
  const clientConfig = getClientConfig();
@@ -48,6 +54,7 @@ export async function handleWatchdog(options) {
48
54
  serverUrl,
49
55
  clientId,
50
56
  intervalMs: intervalSec * 1000,
57
+ spawnDaemon: !watchdogNoDaemon,
51
58
  });
52
59
 
53
60
  await agent.start();
package/src/lib/args.js CHANGED
@@ -629,6 +629,7 @@ function parseWatchdogArgs(args) {
629
629
  let watchdogServer = null;
630
630
  let watchdogInterval = 15;
631
631
  let watchdogClientId = null;
632
+ let watchdogNoDaemon = false;
632
633
 
633
634
  for (let i = 0; i < args.length; i++) {
634
635
  const arg = args[i];
@@ -638,6 +639,8 @@ function parseWatchdogArgs(args) {
638
639
  watchdogInterval = parseInt(args[++i]) || 15;
639
640
  } else if (arg === "--client-id") {
640
641
  watchdogClientId = args[++i];
642
+ } else if (arg === "--no-daemon") {
643
+ watchdogNoDaemon = true;
641
644
  }
642
645
  }
643
646
 
@@ -646,6 +649,7 @@ function parseWatchdogArgs(args) {
646
649
  watchdogServer,
647
650
  watchdogInterval,
648
651
  watchdogClientId,
652
+ watchdogNoDaemon,
649
653
  urls: [],
650
654
  outputFormat: "json",
651
655
  exploreCount: 0,
@@ -383,6 +383,7 @@ const HELP_TEXT = [
383
383
  " --server <URL>, -s 服务端地址(默认使用 ~/.tt-help.json 中的 server)",
384
384
  " --interval <秒>, -i 心跳间隔,默认 15 秒(最小 5 秒)",
385
385
  " --client-id <ID> 客户端标识(默认本机 hostname)",
386
+ " --no-daemon 不启动 clash daemon(默认启动)",
386
387
  " 示例: tt-help watchdog",
387
388
  " tt-help watchdog --server http://117.71.53.99:17301",
388
389
  " tt-help watchdog -i 30 --client-id my-server-01",
@@ -32,7 +32,12 @@ const TT_HELP_CONFIG = join(homeDir, ".tt-help.json");
32
32
 
33
33
  /** 自动发现 daemon.js 路径 */
34
34
  function findDaemonPath() {
35
- // 1. 从 ~/.tt-help.json 读取配置
35
+ // 1. 内部打包路径优先(npm publish 后 src/clash/daemon.js,升级即最新)
36
+ // 避免 ~/.tt-help.json 的 clashDaemonPath 指向旧版外部 daemon
37
+ const internalPath = join(__dirname, "..", "clash", "daemon.js");
38
+ if (existsSync(internalPath)) return internalPath;
39
+
40
+ // 2. 显式配置覆盖(仅当包内缺失时)
36
41
  try {
37
42
  if (existsSync(TT_HELP_CONFIG)) {
38
43
  const cfg = JSON.parse(readFileSync(TT_HELP_CONFIG, "utf-8"));
@@ -42,10 +47,6 @@ function findDaemonPath() {
42
47
  }
43
48
  } catch {}
44
49
 
45
- // 2. 内部打包路径(npm publish 后 src/clash/daemon.js)
46
- const internalPath = join(__dirname, "..", "clash", "daemon.js");
47
- if (existsSync(internalPath)) return internalPath;
48
-
49
50
  // 3. 开发模式:clash-verge-explore 同级目录
50
51
  const candidates = [
51
52
  join(__dirname, "..", "..", "..", "clash-verge-explore", "daemon.js"),
@@ -117,11 +118,13 @@ export class WatchdogAgent {
117
118
  * @param {string} opts.serverUrl - 服务端地址
118
119
  * @param {string} opts.clientId - 客户端标识
119
120
  * @param {number} opts.intervalMs - 心跳间隔(毫秒)
121
+ * @param {boolean} [opts.spawnDaemon=true] - 是否自动启动 clash daemon
120
122
  */
121
- constructor({ serverUrl, clientId, intervalMs = 15000 }) {
123
+ constructor({ serverUrl, clientId, intervalMs = 15000, spawnDaemon = true }) {
122
124
  this.serverUrl = serverUrl.replace(/\/+$/, "");
123
125
  this.clientId = clientId;
124
126
  this.intervalMs = intervalMs;
127
+ this.spawnDaemon = !!spawnDaemon;
125
128
  this.running = false;
126
129
  this.heartbeatTimer = null;
127
130
  this.thresholdTimer = null;
@@ -160,8 +163,8 @@ export class WatchdogAgent {
160
163
  // 注册信号处理
161
164
  this._setupSignalHandlers();
162
165
 
163
- // 启动 clash daemon(保代理通道)
164
- this._spawnClashDaemon();
166
+ // 启动 clash daemon(保代理通道,--no-daemon 可关闭)
167
+ if (this.spawnDaemon) this._spawnClashDaemon();
165
168
 
166
169
  // 首次立即发送心跳
167
170
  await this._sendHeartbeat();
@@ -395,17 +398,21 @@ export class WatchdogAgent {
395
398
  this.upgradeChecker = startUpgradeChecker({
396
399
  onUpdateAvailable: async ({ current, latest }) => {
397
400
  console.error(`[upgrade] ${current} → ${latest}`);
401
+ // 停掉所有子进程与 clash daemon,避免新旧并存
398
402
  this.processManager.stopAll();
403
+ this._killClashDaemon();
399
404
 
400
405
  const result = await performUpgrade();
401
406
  if (result.success) {
402
407
  this.ttHelpVersion = latest;
403
- console.error(`[upgrade] 升级成功`);
404
- } else {
405
- console.error(`[upgrade] 失败: ${result.output.substring(0, 200)}`);
408
+ console.error(
409
+ `[upgrade] 升级成功 (${latest}),重启 watchdog 使新版 clash daemon 生效`,
410
+ );
411
+ // 重启自身进程加载新代码(内部 daemon 路径优先),新进程会拉起新版 daemon
412
+ this._restartSelf();
413
+ return;
406
414
  }
407
-
408
- // 立即心跳(同步命令,重启进程)
415
+ console.error(`[upgrade] 失败: ${result.output.substring(0, 200)}`);
409
416
  await this._sendHeartbeat();
410
417
  },
411
418
  });
@@ -481,6 +488,38 @@ export class WatchdogAgent {
481
488
  this.clashDaemon = null;
482
489
  }
483
490
 
491
+ /**
492
+ * 升级后重启自身进程(加载新代码)
493
+ * 用 cli.js watchdog 子进程替换当前进程,保证新版本内部 daemon 路径逻辑生效
494
+ */
495
+ _restartSelf() {
496
+ const cliPath = join(__dirname, "..", "..", "cli.js");
497
+ const args = ["watchdog"];
498
+ if (this.serverUrl) args.push("--server", this.serverUrl);
499
+ if (this.clientId) args.push("--client-id", this.clientId);
500
+ if (!this.spawnDaemon) args.push("--no-daemon");
501
+ args.push(
502
+ "--interval",
503
+ String(Math.max(5, Math.round(this.intervalMs / 1000))),
504
+ );
505
+ let child;
506
+ try {
507
+ child = spawn(process.execPath, [cliPath, ...args], {
508
+ detached: true,
509
+ stdio: "ignore",
510
+ });
511
+ child.unref();
512
+ } catch (e) {
513
+ console.error(`[upgrade] 重启自身失败: ${e.message}`);
514
+ return;
515
+ }
516
+ console.error(
517
+ `[upgrade] 新版 watchdog 已启动 (pid=${child.pid}),当前进程退出`,
518
+ );
519
+ this.stop();
520
+ setTimeout(() => process.exit(0), 500);
521
+ }
522
+
484
523
  /**
485
524
  * 信号处理
486
525
  */