tt-help-cli-ycl 1.4.126 → 1.4.130

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.126",
3
+ "version": "1.4.130",
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,197 @@
1
+ /**
2
+ * daemon _handleFailover 三级响应单测(2026-08-22,无需 Clash 运行)
3
+ *
4
+ * 验证:
5
+ * - 探针健康 → 窗口内 failover 仅检查不切换(前 N-1 次)
6
+ * - 探针不健康 → 立即切换(无视窗口)
7
+ * - 壳页 → 视为链路正常,仅检查不切换(与巡检口径一致)
8
+ * - 窗口过期 → 计数重置
9
+ * - 窗口内到达 failoverEscapeAfter 次 → 按业务级封禁轮换一次
10
+ * - 实际切换后 → 窗口与计数重置
11
+ * - escapeAfter 可配置
12
+ * - 候选集为空 → 触发 delay 初测且不切换
13
+ *
14
+ * 运行: node --test scripts/test-daemon-failover.mjs
15
+ * (若当前 Node 版本 module mock 仍需 flag: --experimental-test-module-mocks)
16
+ */
17
+ import { test, mock, before } from "node:test";
18
+ import assert from "node:assert/strict";
19
+
20
+ // ── 可配置假依赖状态 ──
21
+ const state = {
22
+ health: {
23
+ healthy: true,
24
+ httpCode: 200,
25
+ timeTotal: 1.2,
26
+ sizeDownload: 100000,
27
+ numRedirects: 0,
28
+ title: "TikTok - Make Your Day",
29
+ error: null,
30
+ },
31
+ nodes: ["🇭🇰 香港01", "🇲🇾 马来02"],
32
+ currentNode: "🇭🇰 香港01",
33
+ switches: [],
34
+ closes: 0,
35
+ };
36
+
37
+ function resetState() {
38
+ state.health = {
39
+ healthy: true,
40
+ httpCode: 200,
41
+ timeTotal: 1.2,
42
+ sizeDownload: 100000,
43
+ numRedirects: 0,
44
+ title: "TikTok - Make Your Day",
45
+ error: null,
46
+ };
47
+ state.nodes = ["🇭🇰 香港01", "🇲🇾 马来02"];
48
+ state.currentNode = "🇭🇰 香港01";
49
+ state.switches = [];
50
+ state.closes = 0;
51
+ }
52
+
53
+ // ── 注册模块 mock(必须在 import daemon.js 之前) ──
54
+ before(async () => {
55
+ mock.module("../src/clash/checker.js", {
56
+ namedExports: {
57
+ normalizeCountries: (v) => (Array.isArray(v) ? [...v] : v ? [v] : []),
58
+ CONFIG: {
59
+ groupName: "GLOBAL",
60
+ testUrl: "https://www.tiktok.com/explore",
61
+ proxyUrl: "http://127.0.0.1:7897",
62
+ nodeCountryFilter: null,
63
+ },
64
+ checkNodeHealth: () => ({ ...state.health }),
65
+ loadHealthyList: () => ({ nodes: [...state.nodes] }),
66
+ switchToNode: async (n) => {
67
+ state.switches.push(n);
68
+ state.currentNode = n;
69
+ // 模拟真实世界:故障属于原节点,切换后的新节点健康(候选内容验证通过)
70
+ state.health = { ...state.health, healthy: true, error: null };
71
+ return true;
72
+ },
73
+ applyDiscovered: () => {},
74
+ runDelayProbe: async () => ({
75
+ nodes: [...state.nodes],
76
+ total: state.nodes.length,
77
+ }),
78
+ saveHealthyList: () => {},
79
+ },
80
+ });
81
+ mock.module("../src/clash/api.js", {
82
+ namedExports: {
83
+ configure: () => {},
84
+ getCurrentNode: async () => state.currentNode,
85
+ closeAllConnections: async () => {
86
+ state.closes += 1;
87
+ },
88
+ },
89
+ });
90
+ mock.module("../src/clash/ipc.js", {
91
+ namedExports: {
92
+ writeHealth: () => {},
93
+ listCommandChannels: () => [],
94
+ readCommand: () => null,
95
+ clearCommand: () => {},
96
+ },
97
+ });
98
+ mock.module("../src/clash/discover.js", {
99
+ namedExports: {
100
+ discoverAll: async () => [],
101
+ invalidateDiscoverCache: () => {},
102
+ },
103
+ });
104
+ ({ ClashDaemon } = await import("../src/clash/daemon.js"));
105
+ });
106
+
107
+ let ClashDaemon = null;
108
+
109
+ function makeCtx(opts = {}) {
110
+ const d = new ClashDaemon({
111
+ failoverCooldownMs: 600000,
112
+ failoverEscapeAfter: 3,
113
+ ...opts,
114
+ });
115
+ const c = { proxyPort: 7897 };
116
+ return { d, c };
117
+ }
118
+
119
+ test("F1 探针健康:前 2 次 failover 仅检查不切换,第 3 次轮换", async () => {
120
+ resetState();
121
+ const { d, c } = makeCtx();
122
+ await d._handleFailover({ reason: "F1-1" }, c);
123
+ await d._handleFailover({ reason: "F1-2" }, c);
124
+ assert.equal(state.switches.length, 0, "前 2 次不应切换");
125
+ assert.equal(state.closes, 0, "前 2 次不应断开连接");
126
+ await d._handleFailover({ reason: "F1-3" }, c);
127
+ assert.deepEqual(state.switches, ["🇲🇾 马来02"], "第 3 次按轮换切换到候选");
128
+ assert.equal(state.closes, 1, "切换后应断开旧连接");
129
+ });
130
+
131
+ test("F2 探针不健康:立即切换(无视窗口)", async () => {
132
+ resetState();
133
+ const { d, c } = makeCtx();
134
+ state.health = { ...state.health, healthy: false, error: "timeout" };
135
+ await d._handleFailover({ reason: "F2" }, c);
136
+ assert.deepEqual(state.switches, ["🇲🇾 马来02"], "不健康应立即切换");
137
+ });
138
+
139
+ test("F3 壳页视为链路正常:仅检查不切换", async () => {
140
+ resetState();
141
+ const { d, c } = makeCtx();
142
+ // 壳页签名:200 + 无跳转 + 无 error + 无 title
143
+ state.health = {
144
+ healthy: false,
145
+ httpCode: 200,
146
+ timeTotal: 1.0,
147
+ sizeDownload: 43000,
148
+ numRedirects: 0,
149
+ title: "",
150
+ error: null,
151
+ };
152
+ await d._handleFailover({ reason: "F3" }, c);
153
+ assert.equal(state.switches.length, 0, "壳页不应触发切换");
154
+ });
155
+
156
+ test("F4 窗口过期:计数重置,不累计到逃生阈值", async () => {
157
+ resetState();
158
+ const { d, c } = makeCtx({ failoverCooldownMs: 150 });
159
+ await d._handleFailover({ reason: "F4-1" }, c); // count=1
160
+ await new Promise((r) => setTimeout(r, 200)); // 窗口过期
161
+ await d._handleFailover({ reason: "F4-2" }, c); // 重置后 count=1
162
+ await d._handleFailover({ reason: "F4-3" }, c); // count=2
163
+ assert.equal(state.switches.length, 0, "窗口过期后不应累计旧计数");
164
+ await d._handleFailover({ reason: "F4-4" }, c); // count=3 → 轮换
165
+ assert.equal(state.switches.length, 1, "第 3 次才应轮换");
166
+ });
167
+
168
+ test("F5 切换后窗口重置:重新从仅检查开始", async () => {
169
+ resetState();
170
+ const { d, c } = makeCtx();
171
+ await d._handleFailover({ reason: "F5-1" }, c); // skip
172
+ await d._handleFailover({ reason: "F5-2" }, c); // skip
173
+ await d._handleFailover({ reason: "F5-3" }, c); // switch
174
+ assert.equal(state.switches.length, 1, "第 3 次应切换");
175
+ await d._handleFailover({ reason: "F5-4" }, c); // 重置后 count=1 → skip
176
+ assert.equal(state.switches.length, 1, "切换后应重置窗口,不连续切换");
177
+ await d._handleFailover({ reason: "F5-5" }, c); // count=2 → skip
178
+ await d._handleFailover({ reason: "F5-6" }, c); // count=3 → switch
179
+ assert.equal(state.switches.length, 2, "第 6 次才应再次轮换");
180
+ });
181
+
182
+ test("F6 escapeAfter 可配置:=1 时首次健康 failover 即轮换", async () => {
183
+ resetState();
184
+ const { d, c } = makeCtx({ failoverEscapeAfter: 1 });
185
+ await d._handleFailover({ reason: "F6" }, c);
186
+ assert.equal(state.switches.length, 1, "escapeAfter=1 应首次即切换");
187
+ });
188
+
189
+ test("F7 候选集为空:触发 delay 初测且不切换", async () => {
190
+ resetState();
191
+ const { d, c } = makeCtx();
192
+ state.health = { ...state.health, healthy: false, error: "timeout" };
193
+ state.nodes = [];
194
+ await d._handleFailover({ reason: "F7" }, c);
195
+ assert.equal(state.switches.length, 0, "无候选不应切换");
196
+ assert.ok(c._lastProbeAt, "应触发过 delay 初测");
197
+ });
@@ -0,0 +1,141 @@
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);
@@ -0,0 +1,98 @@
1
+ /**
2
+ * 测试:升级链绕开 WMI 的新逻辑(2026-08-21)
3
+ * 范围(macOS 可测部分):
4
+ * 1. parseCsvLine:tasklist /FO CSV 输出解析(引号/逗号边界)
5
+ * 2. 模拟 tasklist /V 输出的窗口标题匹配过滤逻辑(复算与实现一致的筛选规则)
6
+ * 3. agent 构造即写存活标记文件(STARTED_FILE),内容含 pid/version
7
+ */
8
+ import assert from "assert";
9
+ import { readFileSync, rmSync } from "fs";
10
+ import { join, dirname } from "path";
11
+ import { tmpdir } from "os";
12
+ import { fileURLToPath } from "url";
13
+
14
+ const { parseCsvLine } = await import("../src/watchdog/upgrader.js");
15
+
16
+ let pass = 0;
17
+ const ok = (msg) => {
18
+ pass++;
19
+ console.log(` ✓ ${msg}`);
20
+ };
21
+
22
+ // ── 1. parseCsvLine ──
23
+ const L1 = '"conhost.exe","5234","Console","2","8,312 K","Running","x\\user","0:00:01","tt-help-watchdog (F-7@WIN)"';
24
+ const f1 = parseCsvLine(L1);
25
+ assert.strictEqual(f1[0], "conhost.exe", "映像名");
26
+ assert.strictEqual(f1[1], "5234", "PID");
27
+ assert.strictEqual(f1[3], "2", "会话#");
28
+ assert.strictEqual(f1[4], "8,312 K", "带逗号字段 8,312 K");
29
+ assert.strictEqual(f1[8], "tt-help-watchdog (F-7@WIN)", "窗口标题");
30
+ ok("parseCsvLine: 常规行(含带逗号字段)");
31
+
32
+ // 标题内含转义引号(CSV 标准:双引号 "" 转义)
33
+ const L2 = '"cmd.exe","999","Console","2","1,024 K","Running","x\\user","0:00:00","say ""hi"", tt-help-watchdog"';
34
+ const f2 = parseCsvLine(L2);
35
+ assert.strictEqual(f2[8], 'say "hi", tt-help-watchdog', "标题含转义引号与逗号");
36
+ ok("parseCsvLine: 标题含转义引号+逗号");
37
+
38
+ // 无引号行(表头)
39
+ const f3 = parseCsvLine("映像名称,PID,会话名,会话#,内存使用,状态,用户名,CPU 时间,窗口标题");
40
+ assert.strictEqual(f3.length, 9, "表头 9 列");
41
+ ok("parseCsvLine: 无引号表头");
42
+
43
+ // ── 2. 模拟 tasklist /V 输出做标题匹配过滤(复算实现逻辑) ──
44
+ const mockTasklist = [
45
+ '"映像名称","PID","会话名","会话#","内存使用","状态","用户名","CPU 时间","窗口标题"',
46
+ '"System Idle Process","0","Services","0","8 K","Unknown","NT AUTHORITY\\SYSTEM","1:23:45:55","N/A"',
47
+ '"conhost.exe","5234","Console","2","8,312 K","Running","x\\user","0:00:01","tt-help-watchdog (F-7@WIN-AADJ1HTCCAQ)"',
48
+ '"cmd.exe","4000","Console","2","4,444 K","Running","x\\user","0:00:00","N/A"',
49
+ '"conhost.exe","7777","Console","2","2,048 K","Running","x\\user","0:00:00","C:\\Windows\\system32\\cmd.exe"',
50
+ '"node.exe","6400","Console","2","120,000 K","Running","x\\user","0:00:10","N/A"',
51
+ '"cmd.exe","6401","Console","2","3,000 K","Running","x\\user","0:00:00","tt-help-watchdog (old)"',
52
+ ];
53
+ const TAG = "tt-help-watchdog";
54
+ const matched = [];
55
+ for (const line of mockTasklist) {
56
+ if (!line.toLowerCase().includes(TAG.toLowerCase())) continue;
57
+ const fields = parseCsvLine(line);
58
+ if (fields.length < 2) continue;
59
+ const name = (fields[0] || "").toLowerCase();
60
+ if (!name.includes("conhost") && !name.includes("cmd")) continue;
61
+ const pid = parseInt(fields[1], 10);
62
+ if (pid > 0) matched.push(pid);
63
+ }
64
+ assert.deepStrictEqual(
65
+ [...new Set(matched)].sort((a, b) => a - b),
66
+ [5234, 6401],
67
+ "只匹配到标题含 tag 的 conhost/cmd,普通窗口/其他进程不误匹配",
68
+ );
69
+ ok("标题匹配过滤: 命中旧窗口 conhost+cmd,排除无关进程");
70
+
71
+ // ── 3. agent 构造即写存活标记文件 ──
72
+ const STARTED_FILE = join(tmpdir(), "tt-help-watchdog-started.json");
73
+ rmSync(STARTED_FILE, { force: true });
74
+ const { WatchdogAgent } = await import("../src/watchdog/agent.js");
75
+ const agent = new WatchdogAgent({
76
+ serverUrl: "http://127.0.0.1:3001",
77
+ clientId: "test@unit",
78
+ intervalMs: 15000,
79
+ });
80
+ assert.ok(agent, "agent 构造成功");
81
+ const st = JSON.parse(readFileSync(STARTED_FILE, "utf-8"));
82
+ assert.strictEqual(st.pid, process.pid, "pid=当前进程");
83
+ assert.ok(typeof st.version === "string" && st.version.length > 0, "version 非空");
84
+ assert.ok(st.ts > 0, "ts 存在");
85
+ ok(`存活文件写入: pid=${st.pid}, version=${st.version}`);
86
+
87
+ // 重复构造覆盖写(模拟新版 watchdog 启动覆盖旧文件)
88
+ const agent2 = new WatchdogAgent({
89
+ serverUrl: "http://127.0.0.1:3001",
90
+ clientId: "test2@unit",
91
+ });
92
+ const st2 = JSON.parse(readFileSync(STARTED_FILE, "utf-8"));
93
+ assert.strictEqual(st2.pid, process.pid, "覆盖写仍有效");
94
+ ok("存活文件: 重复构造覆盖写正常");
95
+
96
+ // 清理测试文件
97
+ rmSync(STARTED_FILE, { force: true });
98
+ console.log(`\n全部通过: ${pass} 项`);
@@ -0,0 +1,108 @@
1
+ // verify-kill-edge.mjs — 在 Windows 上端到端验证 killEdgeProcesses(netstat 方案)
2
+ // 场景模拟:业务开一个带 --remote-debugging-port 的 Edge 实例,
3
+ // 看门狗关闭时调用 killEdgeProcesses(null, port) 杀浏览器。
4
+ // 安全:只用独立端口 19222 + 独立 user-data-dir,不碰机器上其他 Edge。
5
+ import { exec, execSync } from "node:child_process";
6
+
7
+ const PKG =
8
+ process.env.APPDATA.replace(/\\/g, "/") +
9
+ "/npm/node_modules/tt-help-cli-ycl";
10
+ const { killEdgeProcesses, parseNetstatListeningPids } = await import(
11
+ "file:///" + PKG + "/src/lib/browser/cdp.js"
12
+ );
13
+
14
+ const PORT = 19222;
15
+ const USER_DATA_DIR =
16
+ process.env.TEMP.replace(/\\/g, "/") + "/edge-test-" + PORT;
17
+
18
+ const delay = (ms) => new Promise((r) => setTimeout(r, ms));
19
+
20
+ function netstatPids(port) {
21
+ const out = execSync("netstat -ano", {
22
+ encoding: "utf-8",
23
+ timeout: 5000,
24
+ windowsHide: true,
25
+ });
26
+ return parseNetstatListeningPids(out, port);
27
+ }
28
+
29
+ async function cleanup() {
30
+ const pids = netstatPids(PORT);
31
+ for (const pid of pids) {
32
+ try {
33
+ execSync(`taskkill /F /T /PID ${pid}`, {
34
+ stdio: "ignore",
35
+ windowsHide: true,
36
+ });
37
+ } catch {}
38
+ }
39
+ try {
40
+ execSync(`rd /s /q "${USER_DATA_DIR}"`, {
41
+ stdio: "ignore",
42
+ windowsHide: true,
43
+ });
44
+ } catch {}
45
+ }
46
+
47
+ async function main() {
48
+ // [1] 解析器静态验证:用当前真实 netstat 输出
49
+ {
50
+ const out = execSync("netstat -ano", {
51
+ encoding: "utf-8",
52
+ timeout: 5000,
53
+ windowsHide: true,
54
+ });
55
+ const sample = out
56
+ .split(/\r?\n/)
57
+ .filter((l) => l.includes("LISTENING"))
58
+ .slice(0, 5);
59
+ console.log("[1] 当前真实 LISTENING 行:");
60
+ for (const l of sample) console.log(" " + l.trim());
61
+ const clash = parseNetstatListeningPids(out, 9097);
62
+ console.log("[1] 9097 解析 PID = " + JSON.stringify(clash));
63
+ }
64
+
65
+ // [2] 启动真实 Edge CDP 测试实例
66
+ console.log("[2] 启动 Edge CDP 测试实例 (port=" + PORT + ") ...");
67
+ exec(
68
+ `start "" msedge --remote-debugging-port=${PORT} --user-data-dir="${USER_DATA_DIR}" --no-first-run --no-default-browser-check --disable-background-mode --disable-component-update --disable-crash-reporter --disable-breakpad --disable-sync --disable-background-networking`,
69
+ { windowsHide: true },
70
+ );
71
+
72
+ let readyPids = [];
73
+ for (let i = 0; i < 30; i++) {
74
+ await delay(1000);
75
+ readyPids = netstatPids(PORT);
76
+ if (readyPids.length > 0) break;
77
+ }
78
+ console.log("[2] 端口 " + PORT + " 监听 PID = " + JSON.stringify(readyPids));
79
+ if (readyPids.length === 0) {
80
+ console.log("RESULT: SETUP_FAIL(Edge 未监听端口)");
81
+ await cleanup();
82
+ process.exit(2);
83
+ }
84
+
85
+ // [3] 调用包内真实 killEdgeProcesses(与看门狗关闭时同一函数)
86
+ console.log("[3] 调用 killEdgeProcesses(null, " + PORT + ") ...");
87
+ const t0 = Date.now();
88
+ await killEdgeProcesses(null, PORT);
89
+ await delay(2000);
90
+
91
+ const after = netstatPids(PORT);
92
+ console.log(
93
+ "[3] 调用耗时 " +
94
+ (Date.now() - t0) +
95
+ "ms,杀后端口监听 PID = " +
96
+ JSON.stringify(after),
97
+ );
98
+
99
+ console.log(after.length === 0 ? "RESULT: PASS" : "RESULT: FAIL");
100
+ await cleanup();
101
+ process.exit(after.length === 0 ? 0 : 1);
102
+ }
103
+
104
+ main().catch(async (e) => {
105
+ console.error("脚本异常: " + e.message);
106
+ await cleanup();
107
+ process.exit(3);
108
+ });