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 +1 -1
- 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/daemon.js +3 -0
- package/src/cli/watchdog.js +8 -1
- package/src/lib/args.js +4 -0
- package/src/lib/constants.js +1 -0
- package/src/watchdog/agent.js +52 -13
package/package.json
CHANGED
|
@@ -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
|
+
});
|
package/src/clash/daemon.js
CHANGED
|
@@ -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
|
"..",
|
package/src/cli/watchdog.js
CHANGED
|
@@ -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 {
|
|
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,
|
package/src/lib/constants.js
CHANGED
|
@@ -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",
|
package/src/watchdog/agent.js
CHANGED
|
@@ -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.
|
|
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(
|
|
404
|
-
|
|
405
|
-
|
|
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
|
*/
|