tt-help-cli-ycl 1.4.169 → 1.4.171

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.
Files changed (46) hide show
  1. package/package.json +1 -1
  2. package/scripts/test-subscription-allocator.mjs +220 -0
  3. package/scripts/test-subscription-auto-alloc-ui.mjs +208 -0
  4. package/scripts/test-subscription-auto-alloc.mjs +259 -0
  5. package/scripts/test-subscription-center-e2e.mjs +134 -0
  6. package/scripts/test-subscription-center-smoke.mjs +154 -0
  7. package/scripts/test-subscription-center-ui.mjs +111 -0
  8. package/scripts/vnc-login.sh +68 -0
  9. package/src/cli/info.js +6 -1
  10. package/src/lib/browser/cdp.js +7 -1
  11. package/src/watch/public/app.js +586 -15
  12. package/src/watch/public/index.html +70 -3
  13. package/src/watch/subscription-allocator.js +255 -0
  14. package/src/watch/subscription-center.js +590 -0
  15. package/src/watch/watchdog-server.js +210 -0
  16. package/src/watchdog/agent.js +5 -0
  17. package/scripts/test-api-interceptor-429.mjs +0 -122
  18. package/scripts/test-api-interceptor-captcha.mjs +0 -176
  19. package/scripts/test-claim-fix.mjs +0 -216
  20. package/scripts/test-clash-discover.mjs +0 -126
  21. package/scripts/test-clash-hysteresis.mjs +0 -131
  22. package/scripts/test-clash-multichannel.mjs +0 -120
  23. package/scripts/test-client-error-monitor.mjs +0 -261
  24. package/scripts/test-country-fix.mjs +0 -341
  25. package/scripts/test-daemon-failover.mjs +0 -301
  26. package/scripts/test-daemon-switch-history.mjs +0 -141
  27. package/scripts/test-daemon-whitelist.mjs +0 -308
  28. package/scripts/test-explore-captcha-wait.mjs +0 -170
  29. package/scripts/test-photo-post.mjs +0 -156
  30. package/scripts/test-precheck-perf.mjs +0 -270
  31. package/scripts/test-precise-index.mjs +0 -114
  32. package/scripts/test-probe-live.mjs +0 -83
  33. package/scripts/test-probe.mjs +0 -315
  34. package/scripts/test-product-fill-cdp.mjs +0 -336
  35. package/scripts/test-product-fill-db.mjs +0 -216
  36. package/scripts/test-product-fill-http.mjs +0 -117
  37. package/scripts/test-proxy-switch-restart.mjs +0 -198
  38. package/scripts/test-refill-order.mjs +0 -218
  39. package/scripts/test-sample-all-fail.mjs +0 -142
  40. package/scripts/test-seller-any-country.mjs +0 -167
  41. package/scripts/test-stats-smoke.mjs +0 -43
  42. package/scripts/test-upgrade-spawn.mjs +0 -179
  43. package/scripts/test-webserver-error-classify.mjs +0 -409
  44. package/scripts/test-wmi-free-upgrade.mjs +0 -98
  45. package/src/cli/test-real-attach.js +0 -0
  46. package/src/results/user-videos-bar.lar.lar.moeta.json +0 -37
@@ -1,301 +0,0 @@
1
- /**
2
- * daemon _handleFailover 单测(2026-08-25,时间锚点方案,无需 Clash 运行)
3
- *
4
- * 验证:
5
- * - 探针不健康 → 立即切换(无视冷却)
6
- * - 探针健康(含壳页)→ 距上次切换 < 冷却期仅检查不切换
7
- * - 探针健康 → 距上次切换 >= 冷却期 → 按业务级封禁轮换一次
8
- * - 强切(force=true)→ 探针健康且冷却期内仍强制轮换
9
- * - 切换后(含手动/巡检/failover)更新 _lastSwitchAt 锚点,冷却期内不再切
10
- * - 候选集为空 → 触发 delay 初测且不切换
11
- *
12
- * 运行: node --test scripts/test-daemon-failover.mjs
13
- * (若当前 Node 版本 module mock 仍需 flag: --experimental-test-module-mocks)
14
- */
15
- import { test, mock, before } from "node:test";
16
- import assert from "node:assert/strict";
17
-
18
- // ── 可配置假依赖状态 ──
19
- const state = {
20
- health: {
21
- healthy: true,
22
- httpCode: 200,
23
- timeTotal: 1.2,
24
- sizeDownload: 100000,
25
- numRedirects: 0,
26
- title: "TikTok - Make Your Day",
27
- error: null,
28
- },
29
- nodes: ["🇭🇰 香港01", "🇲🇾 马来02"],
30
- currentNode: "🇭🇰 香港01",
31
- switches: [],
32
- closes: 0, // closeAllConnections(全断)调用次数
33
- idleCloses: 0, // closeIdleConnections(优雅断连)调用次数
34
- failIdleClose: false, // 模拟优雅断连失败
35
- // 订阅切换感知(巡检开头候选集存在性校验)相关
36
- proxyMap: { "🇭🇰 香港01": {}, "🇲🇾 马来02": {} }, // 当前 /proxies 中的代理名
37
- pruneResult: { kept: 2, removed: 0, allGone: false },
38
- pruneCalls: 0,
39
- probeCalls: 0,
40
- failProxies: false,
41
- };
42
-
43
- function resetState() {
44
- state.health = {
45
- healthy: true,
46
- httpCode: 200,
47
- timeTotal: 1.2,
48
- sizeDownload: 100000,
49
- numRedirects: 0,
50
- title: "TikTok - Make Your Day",
51
- error: null,
52
- };
53
- state.nodes = ["🇭🇰 香港01", "🇲🇾 马来02"];
54
- state.currentNode = "🇭🇰 香港01";
55
- state.switches = [];
56
- state.closes = 0;
57
- state.idleCloses = 0;
58
- state.failIdleClose = false;
59
- state.proxyMap = { "🇭🇰 香港01": {}, "🇲🇾 马来02": {} };
60
- state.pruneResult = { kept: 2, removed: 0, allGone: false };
61
- state.pruneCalls = 0;
62
- state.probeCalls = 0;
63
- state.failProxies = false;
64
- }
65
-
66
- // ── 注册模块 mock(必须在 import daemon.js 之前) ──
67
- before(async () => {
68
- mock.module("../src/clash/checker.js", {
69
- namedExports: {
70
- normalizeCountries: (v) => (Array.isArray(v) ? [...v] : v ? [v] : []),
71
- CONFIG: {
72
- groupName: "GLOBAL",
73
- testUrl: "https://www.tiktok.com/explore",
74
- proxyUrl: "http://127.0.0.1:7897",
75
- nodeCountryFilter: null,
76
- },
77
- checkNodeHealth: () => ({ ...state.health }),
78
- loadHealthyList: () => ({ nodes: [...state.nodes] }),
79
- switchToNode: async (n) => {
80
- state.switches.push(n);
81
- state.currentNode = n;
82
- // 模拟真实世界:故障属于原节点,切换后的新节点健康(候选内容验证通过)
83
- state.health = { ...state.health, healthy: true, error: null };
84
- return true;
85
- },
86
- applyDiscovered: () => {},
87
- checkClashAlive: () => true,
88
- pruneHealthyListByCurrentProxies: () => {
89
- state.pruneCalls += 1;
90
- return { ...state.pruneResult };
91
- },
92
- runDelayProbe: async () => {
93
- state.probeCalls += 1;
94
- return {
95
- nodes: [...state.nodes],
96
- total: state.nodes.length,
97
- };
98
- },
99
- saveHealthyList: () => {},
100
- },
101
- });
102
- mock.module("../src/clash/api.js", {
103
- namedExports: {
104
- configure: () => {},
105
- getProxies: async () => {
106
- if (state.failProxies) throw new Error("mock /proxies 不可达");
107
- return { proxies: { ...state.proxyMap } };
108
- },
109
- getCurrentNode: async () => state.currentNode,
110
- closeAllConnections: async () => {
111
- state.closes += 1;
112
- },
113
- closeIdleConnections: async () => {
114
- if (state.failIdleClose) throw new Error("mock 优雅断连失败");
115
- state.idleCloses += 1;
116
- return { closed: 3, active: 2 };
117
- },
118
- },
119
- });
120
- mock.module("../src/clash/ipc.js", {
121
- namedExports: {
122
- writeHealth: () => {},
123
- listCommandChannels: () => [],
124
- readCommand: () => null,
125
- clearCommand: () => {},
126
- },
127
- });
128
- mock.module("../src/clash/discover.js", {
129
- namedExports: {
130
- discoverAll: async () => [],
131
- invalidateDiscoverCache: () => {},
132
- },
133
- });
134
- ({ ClashDaemon } = await import("../src/clash/daemon.js"));
135
- });
136
-
137
- let ClashDaemon = null;
138
-
139
- function makeCtx(opts = {}) {
140
- const d = new ClashDaemon({
141
- failoverCooldownMs: 600000,
142
- failoverEscapeAfter: 3,
143
- ...opts,
144
- });
145
- const c = { proxyPort: 7897 };
146
- return { d, c };
147
- }
148
-
149
- test("F1 探针健康:冷却期外首次 failover 即轮换,并更新锚点", async () => {
150
- resetState();
151
- const { d, c } = makeCtx();
152
- c._lastSwitchAt = Date.now() - 600_001; // 距上次切换 > 10min 冷却
153
- await d._handleFailover({ reason: "F1" }, c);
154
- assert.deepEqual(state.switches, ["🇲🇾 马来02"], "冷却期外应轮换切换");
155
- assert.equal(state.idleCloses, 1, "切换后应优雅断连(空闲回收)");
156
- assert.ok(
157
- c._lastSwitchAt && c._lastSwitchAt > Date.now() - 5000,
158
- "切换后应更新锚点",
159
- );
160
- });
161
-
162
- test("F2 探针不健康:立即切换(无视冷却)", async () => {
163
- resetState();
164
- const { d, c } = makeCtx();
165
- c._lastSwitchAt = Date.now(); // 刚切过,但探针不健康时不看锚点
166
- state.health = { ...state.health, healthy: false, error: "timeout" };
167
- await d._handleFailover({ reason: "F2" }, c);
168
- assert.deepEqual(state.switches, ["🇲🇾 马来02"], "不健康应立即切换");
169
- });
170
-
171
- test("F3 壳页视为链路正常:冷却期内仅检查不切换", async () => {
172
- resetState();
173
- const { d, c } = makeCtx();
174
- c._lastSwitchAt = Date.now(); // 冷却期内
175
- // 壳页签名:200 + 无跳转 + 无 error + 无 title
176
- state.health = {
177
- healthy: false,
178
- httpCode: 200,
179
- timeTotal: 1.0,
180
- sizeDownload: 43000,
181
- numRedirects: 0,
182
- title: "",
183
- error: null,
184
- };
185
- await d._handleFailover({ reason: "F3" }, c);
186
- assert.equal(state.switches.length, 0, "冷却期内壳页不应触发切换");
187
- });
188
-
189
- test("F4 冷却期约束:切换后冷却期内不连续切换,过期后再切", async () => {
190
- resetState();
191
- const { d, c } = makeCtx();
192
- // 首次 failover(无锚点)→ 按冷却期外处理 → 轮换
193
- await d._handleFailover({ reason: "F4-1" }, c);
194
- assert.equal(state.switches.length, 1, "首次应轮换切换");
195
- // 锚点刚更新,冷却期内再次 failover(探针健康)→ 不切
196
- await d._handleFailover({ reason: "F4-2" }, c);
197
- assert.equal(state.switches.length, 1, "冷却期内不应再切换");
198
- // 冷却期过后 → 再次轮换
199
- c._lastSwitchAt = Date.now() - 600_001;
200
- await d._handleFailover({ reason: "F4-3" }, c);
201
- assert.equal(state.switches.length, 2, "冷却期过后应再切换");
202
- });
203
-
204
- test("F5 手动切换同样更新锚点:切换后冷却期内 failover 不再切", async () => {
205
- resetState();
206
- const { d, c } = makeCtx();
207
- // 手动切换(用户主动切,立即执行)
208
- await d._handleSwitchNode({ node: "🇲🇾 马来02", proxyPort: 7897 }, c);
209
- assert.equal(state.switches.length, 1, "手动切换应立即执行");
210
- assert.ok(
211
- c._lastSwitchAt && c._lastSwitchAt > Date.now() - 5000,
212
- "手动切换应更新锚点",
213
- );
214
- // 手动切换后冷却期内,探针健康的 failover 不再切
215
- await d._handleFailover({ reason: "F5-2" }, c);
216
- assert.equal(
217
- state.switches.length,
218
- 1,
219
- "手动切换后冷却期内 failover 不应再切",
220
- );
221
- });
222
-
223
- // F6 已移除:failoverEscapeAfter 计数方案已弃用(2026-08-25 改为时间锚点),
224
- // 不再存在"连报 N 次"阈值;字段保留仅为兼容外部传参,不影响行为。
225
-
226
- test("F7 候选集为空:触发 delay 初测且不切换", async () => {
227
- resetState();
228
- const { d, c } = makeCtx();
229
- state.health = { ...state.health, healthy: false, error: "timeout" };
230
- state.nodes = [];
231
- await d._handleFailover({ reason: "F7" }, c);
232
- assert.equal(state.switches.length, 0, "无候选不应切换");
233
- assert.ok(c._lastProbeAt, "应触发过 delay 初测");
234
- });
235
-
236
- test("F8 优雅断连失败:回退全断,切换仍生效", async () => {
237
- resetState();
238
- const { d, c } = makeCtx();
239
- c._lastSwitchAt = Date.now() - 600_001; // 冷却期外
240
- state.failIdleClose = true; // 优雅断连抛错
241
- await d._handleFailover({ reason: "F8" }, c);
242
- assert.deepEqual(state.switches, ["🇲🇾 马来02"], "切换仍应执行");
243
- assert.equal(state.idleCloses, 0, "优雅断连失败未计数");
244
- assert.equal(state.closes, 1, "失败后应回退全断");
245
- });
246
-
247
- test("F9 disconnectMode=full:直接全断", async () => {
248
- resetState();
249
- const { d, c } = makeCtx({ disconnectMode: "full" });
250
- c._lastSwitchAt = Date.now() - 600_001;
251
- await d._handleFailover({ reason: "F9" }, c);
252
- assert.deepEqual(state.switches, ["🇲🇾 马来02"], "切换应执行");
253
- assert.equal(state.idleCloses, 0, "full 模式不走优雅断连");
254
- assert.equal(state.closes, 1, "full 模式直接全断");
255
- });
256
-
257
- test("F10 强切(force):冷却期内探针健康仍强制轮换", async () => {
258
- resetState();
259
- const { d, c } = makeCtx();
260
- c._lastSwitchAt = Date.now(); // 刚切过,处于 10min 冷却期内
261
- await d._handleFailover({ reason: "F10", force: true }, c);
262
- assert.deepEqual(state.switches, ["🇲🇾 马来02"], "force 应无视冷却直接轮换");
263
- assert.ok(
264
- c._lastSwitchAt && c._lastSwitchAt > Date.now() - 5000,
265
- "强切后应更新锚点(给新节点观察期)",
266
- );
267
- });
268
-
269
- // ── 订阅切换感知:巡检开头候选集存在性校验(2026-08-29) ──
270
-
271
- test("S1 候选集全部失效:当前节点健康也立即触发 delay 初测", async () => {
272
- resetState();
273
- const { d, c } = makeCtx();
274
- state.health = { ...state.health, healthy: true }; // 当前节点健康
275
- state.pruneResult = { kept: 0, removed: 2, allGone: true };
276
- await d._healthCheckController(c);
277
- assert.equal(state.pruneCalls, 1, "巡检应执行候选集校验");
278
- // 初测经串行锁排队执行,等队列清空后断言
279
- await d._withLock(() => {});
280
- assert.equal(state.probeCalls, 1, "全失效应立即触发 delay 初测");
281
- });
282
-
283
- test("S2 候选集部分失效:剔除后不触发初测,巡检正常", async () => {
284
- resetState();
285
- const { d, c } = makeCtx();
286
- state.pruneResult = { kept: 1, removed: 1, allGone: false };
287
- await d._healthCheckController(c);
288
- await d._withLock(() => {});
289
- assert.equal(state.pruneCalls, 1, "应执行候选集校验");
290
- assert.equal(state.probeCalls, 0, "部分失效不应触发初测");
291
- });
292
-
293
- test("S3 /proxies 拉取失败:静默跳过校验,不阻断巡检", async () => {
294
- resetState();
295
- const { d, c } = makeCtx();
296
- state.failProxies = true;
297
- await d._healthCheckController(c);
298
- await d._withLock(() => {});
299
- assert.equal(state.pruneCalls, 0, "API 失败不应执行校验");
300
- assert.equal(state.probeCalls, 0, "API 失败不应触发初测");
301
- });
@@ -1,141 +0,0 @@
1
- /**
2
- * daemon 切换历史排序单测(无需 Clash 运行)
3
- * 验证 _sortCandidates / _markSwitch 的纯时间轮换(后进后出)行为
4
- */
5
- import { ClashDaemon } from "../src/clash/daemon.js";
6
-
7
- let pass = 0;
8
- let fail = 0;
9
- function ok(cond, name, detail = "") {
10
- if (cond) {
11
- pass++;
12
- console.log(` ✓ ${name}`);
13
- } else {
14
- fail++;
15
- console.error(` ✗ ${name} ${detail}`);
16
- }
17
- }
18
-
19
- const d = new ClashDaemon({});
20
- const nodes = ["A", "B", "C", "D"];
21
- const T0 = Date.now();
22
-
23
- // T1 无历史 → 原序
24
- {
25
- const c = { proxyPort: 1 };
26
- ok(
27
- JSON.stringify(d._sortCandidates(nodes, c)) === JSON.stringify(nodes),
28
- "T1 无历史保持原序",
29
- );
30
- }
31
-
32
- // T2 切过沉底,未切过保持原序
33
- {
34
- const c = { proxyPort: 2 };
35
- c._switchHistory = new Map([["B", T0 - 10_000]]);
36
- const r = d._sortCandidates(nodes, c);
37
- ok(r.join() === "A,C,D,B", "T2 切过沉底,未切过保持原序", r.join());
38
- }
39
-
40
- // T3 切过按切换时间升序,未切过最前
41
- {
42
- const c = { proxyPort: 3 };
43
- c._switchHistory = new Map([
44
- ["C", T0 - 2_000],
45
- ["A", T0 - 1_000],
46
- ]);
47
- const r = d._sortCandidates(nodes, c);
48
- ok(r.join() === "B,D,C,A", "T3 切过按切换时间升序,未切过最前", r.join());
49
- }
50
-
51
- // T4 全部切过 → 完全按切换时间轮换
52
- {
53
- const c = { proxyPort: 4 };
54
- c._switchHistory = new Map([
55
- ["A", T0 - 4_000],
56
- ["B", T0 - 1_000],
57
- ["C", T0 - 3_000],
58
- ["D", T0 - 2_000],
59
- ]);
60
- const r = d._sortCandidates(nodes, c);
61
- ok(r.join() === "A,C,D,B", "T4 全切过按切换时间轮换", r.join());
62
- }
63
-
64
- // T5 无时间窗口:无论多久前切过都按时间排队(不恢复 delay 原序)
65
- {
66
- const c = { proxyPort: 5 };
67
- c._switchHistory = new Map([["C", T0 - 3_600_000]]); // 1 小时前切过
68
- const r = d._sortCandidates(nodes, c);
69
- ok(r.join() === "A,B,D,C", "T5 无窗口:1 小时前切过仍沉底", r.join());
70
- }
71
-
72
- // T6 排序不改原数组
73
- {
74
- ok(nodes.join() === "A,B,C,D", "T6 原数组未被修改");
75
- }
76
-
77
- // T7 _markSwitch 惰性初始化 + 更新
78
- {
79
- const c = { proxyPort: 6 };
80
- d._markSwitch(c, "A");
81
- ok(
82
- c._switchHistory instanceof Map && c._switchHistory.has("A"),
83
- "T7 _markSwitch 惰性初始化并记录",
84
- );
85
- const t1 = c._switchHistory.get("A");
86
- d._markSwitch(c, "A");
87
- ok(c._switchHistory.get("A") >= t1, "T8 _markSwitch 重复记录更新时间戳");
88
- }
89
-
90
- // T9 时间戳相同时稳定排序(V8 sort 稳定,保持原相对序)
91
- {
92
- const c = { proxyPort: 7 };
93
- c._switchHistory = new Map([
94
- ["A", T0 - 100],
95
- ["B", T0 - 100],
96
- ]);
97
- const r = d._sortCandidates(nodes, c);
98
- ok(r.join() === "C,D,A,B", "T9 相同时间戳保持原相对序(稳定排序)", r.join());
99
- }
100
-
101
- // T10 壳页判定:200 + 无跳转 + 无 error + 无 title → 壳页
102
- ok(
103
- d._isShellPage({ error: null, httpCode: 200, numRedirects: 0, title: "" }) ===
104
- true,
105
- "T10 壳页判定:200+无title → true",
106
- );
107
- // T11 健康页(有 title)→ 非壳页
108
- ok(
109
- d._isShellPage({
110
- error: null,
111
- httpCode: 200,
112
- numRedirects: 0,
113
- title: "TikTok - Make Your Day",
114
- }) === false,
115
- "T11 健康页有 title → false",
116
- );
117
- // T12 连接错误 → 非壳页
118
- ok(
119
- d._isShellPage({
120
- error: "timeout",
121
- httpCode: 0,
122
- numRedirects: 0,
123
- title: "",
124
- }) === false,
125
- "T12 连接错误 → false",
126
- );
127
- // T13 跳转 → 非壳页
128
- ok(
129
- d._isShellPage({ error: null, httpCode: 200, numRedirects: 1, title: "" }) ===
130
- false,
131
- "T13 跳转 → false",
132
- );
133
- // T14 非 200 → 非壳页
134
- ok(
135
- d._isShellPage({ error: null, httpCode: 403, numRedirects: 0, title: "" }) ===
136
- false,
137
- "T14 非 200 → false",
138
- );
139
-
140
- console.log(`\n结果: ${pass} 通过 / ${fail} 失败`);
141
- process.exit(fail ? 1 : 0);