tt-help-cli-ycl 1.4.124 → 1.4.126
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/cli/open.js +15 -8
- package/src/lib/api-interceptor.js +49 -42
- package/src/lib/browser/cdp.js +70 -29
- package/src/watchdog/process-manager.js +30 -21
- package/src/watchdog/upgrader.js +3 -1
package/package.json
CHANGED
package/src/cli/open.js
CHANGED
|
@@ -90,20 +90,27 @@ export async function handleOpen(parsed) {
|
|
|
90
90
|
|
|
91
91
|
browser = await ensureBrowserReady(cdpOptions);
|
|
92
92
|
|
|
93
|
-
// Ctrl+C
|
|
94
|
-
|
|
93
|
+
// Ctrl+C 关闭浏览器:优雅断开优先(超时保护),强杀进程兜底,最后强制退出
|
|
94
|
+
const shutdown = async () => {
|
|
95
95
|
console.error("\n正在关闭浏览器...");
|
|
96
|
+
// 先优雅断开 CDP 连接(带 3s 超时保护):进程已被强杀时 close() 会挂起等 close 事件
|
|
96
97
|
try {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
await Promise.race([
|
|
99
|
+
browser.close(),
|
|
100
|
+
new Promise((r) => setTimeout(r, 3000)),
|
|
101
|
+
]);
|
|
101
102
|
} catch (e) {
|
|
102
|
-
console.error(`[SIGINT]
|
|
103
|
+
console.error(`[SIGINT] 断开浏览器连接时出错: ${e.message}`);
|
|
103
104
|
}
|
|
105
|
+
// 再强杀 Edge 进程兜底(优雅关闭不保证退出浏览器进程)
|
|
106
|
+
console.error(`[SIGINT] 正在终止 Edge 进程 (端口: ${port})`);
|
|
107
|
+
await killEdgeProcesses(userDataDir, port).catch((e) => {
|
|
108
|
+
console.error(`[SIGINT] 终止 Edge 进程失败: ${e.message}`);
|
|
109
|
+
});
|
|
104
110
|
console.error("浏览器已关闭");
|
|
105
111
|
process.exit(0);
|
|
106
|
-
}
|
|
112
|
+
};
|
|
113
|
+
process.on("SIGINT", shutdown);
|
|
107
114
|
|
|
108
115
|
console.error("");
|
|
109
116
|
console.error("浏览器已启动并连接成功");
|
|
@@ -251,17 +251,16 @@ async function attemptItemListFetch(page, username, log, apiRequestUrlRef) {
|
|
|
251
251
|
};
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
254
|
+
/**
|
|
255
|
+
* CDP 健康检查:带超时保护,区分"页面忙"与"页面死"。
|
|
256
|
+
* evaluate 排队在页面主线程上,加载中的慢页面(肉眼可见转圈)10s 轮不到执行很正常,
|
|
257
|
+
* 此时是忙不是死:先观察等待(默认 2 轮 x 20s),慢页面通常在此期间自然加载完自愈。
|
|
258
|
+
* 观察后仍无响应才刷新页面(实测刷新可解除主线程死锁);刷新后页面处于最忙的加载期,
|
|
259
|
+
* 先缓冲等待再以更宽松的窗口重检,避免误杀正在恢复的页面。
|
|
260
|
+
* 全部失败抛 "CDP 连接异常(刷新后仍失败)"。
|
|
261
|
+
*/
|
|
262
|
+
async function checkCdpHealth(page, options, log) {
|
|
263
263
|
const {
|
|
264
|
-
blockedRetryWaitsMs = DEFAULT_BLOCKED_RETRY_WAITS,
|
|
265
264
|
cdpProbeTimeoutMs = 10000, // 单次探活超时
|
|
266
265
|
cdpObserveWaitMs = 20000, // 观察等待间隔(页面转圈加载期)
|
|
267
266
|
cdpObserveRounds = 2, // 观察轮数
|
|
@@ -269,11 +268,6 @@ async function fetchUserVideosAPI(
|
|
|
269
268
|
cdpReloadProbeTimeoutMs = 20000, // 刷新后重检窗口
|
|
270
269
|
} = options;
|
|
271
270
|
|
|
272
|
-
// CDP 健康检查:带超时保护,区分"页面忙"与"页面死"。
|
|
273
|
-
// evaluate 排队在页面主线程上,加载中的慢页面(肉眼可见转圈)10s 轮不到执行很正常,
|
|
274
|
-
// 此时是忙不是死:先观察等待(默认 2 轮 x 20s),慢页面通常在此期间自然加载完自愈。
|
|
275
|
-
// 观察后仍无响应才刷新页面(实测刷新可解除主线程死锁);刷新后页面处于最忙的加载期,
|
|
276
|
-
// 先缓冲等待再以更宽松的窗口重检,避免误杀正在恢复的页面。
|
|
277
271
|
const evalWithTimeout = (label, ms) =>
|
|
278
272
|
Promise.race([
|
|
279
273
|
page.evaluate(() => document.readyState),
|
|
@@ -285,49 +279,62 @@ async function fetchUserVideosAPI(
|
|
|
285
279
|
),
|
|
286
280
|
]);
|
|
287
281
|
|
|
288
|
-
|
|
282
|
+
// 首探:成功(即使页面仍在加载)即认为健康
|
|
289
283
|
try {
|
|
290
284
|
const state = await evalWithTimeout("CDP 健康检查", cdpProbeTimeoutMs);
|
|
291
|
-
healthy = true;
|
|
292
285
|
if (state !== "complete") {
|
|
293
286
|
log(` [API拦截] 健康检查通过,页面加载中 (readyState=${state})`);
|
|
294
287
|
}
|
|
288
|
+
return;
|
|
295
289
|
} catch (e) {
|
|
296
|
-
// 观察阶段:慢页面几十秒内通常自愈,期间不做任何操作
|
|
297
290
|
log(
|
|
298
291
|
` [API拦截] CDP 探活超时: ${e.message},页面可能仍在加载,观察等待...`,
|
|
299
292
|
);
|
|
300
|
-
for (let round = 1; round <= cdpObserveRounds && !healthy; round++) {
|
|
301
|
-
await new Promise((r) => setTimeout(r, cdpObserveWaitMs));
|
|
302
|
-
try {
|
|
303
|
-
await evalWithTimeout(
|
|
304
|
-
`观察重检 ${round}/${cdpObserveRounds}`,
|
|
305
|
-
cdpProbeTimeoutMs,
|
|
306
|
-
);
|
|
307
|
-
healthy = true;
|
|
308
|
-
log(` [API拦截] 页面已恢复(观察 ${round} 轮后)`);
|
|
309
|
-
} catch (e2) {
|
|
310
|
-
log(
|
|
311
|
-
` [API拦截] 观察 ${round}/${cdpObserveRounds} 后仍无响应: ${e2.message}`,
|
|
312
|
-
);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
293
|
}
|
|
316
294
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
await
|
|
320
|
-
.reload({ waitUntil: "domcontentloaded", timeout: 30000 })
|
|
321
|
-
.catch(() => {});
|
|
322
|
-
await new Promise((r) => setTimeout(r, cdpReloadSettleMs));
|
|
295
|
+
// 观察阶段:慢页面几十秒内通常自愈,期间不做任何操作
|
|
296
|
+
for (let round = 1; round <= cdpObserveRounds; round++) {
|
|
297
|
+
await new Promise((r) => setTimeout(r, cdpObserveWaitMs));
|
|
323
298
|
try {
|
|
324
|
-
await evalWithTimeout(
|
|
325
|
-
|
|
299
|
+
await evalWithTimeout(
|
|
300
|
+
`观察重检 ${round}/${cdpObserveRounds}`,
|
|
301
|
+
cdpProbeTimeoutMs,
|
|
302
|
+
);
|
|
303
|
+
log(` [API拦截] 页面已恢复(观察 ${round} 轮后)`);
|
|
304
|
+
return;
|
|
326
305
|
} catch (e2) {
|
|
327
|
-
|
|
306
|
+
log(
|
|
307
|
+
` [API拦截] 观察 ${round}/${cdpObserveRounds} 后仍无响应: ${e2.message}`,
|
|
308
|
+
);
|
|
328
309
|
}
|
|
329
310
|
}
|
|
330
311
|
|
|
312
|
+
// 观察后仍无响应才刷新页面
|
|
313
|
+
log(` [API拦截] 观察后仍无响应,刷新页面恢复...`);
|
|
314
|
+
await page
|
|
315
|
+
.reload({ waitUntil: "domcontentloaded", timeout: 30000 })
|
|
316
|
+
.catch(() => {});
|
|
317
|
+
await new Promise((r) => setTimeout(r, cdpReloadSettleMs));
|
|
318
|
+
try {
|
|
319
|
+
await evalWithTimeout("刷新后重检", cdpReloadProbeTimeoutMs);
|
|
320
|
+
log(` [API拦截] 页面已恢复(刷新后)`);
|
|
321
|
+
} catch (e2) {
|
|
322
|
+
throw new Error(`CDP 连接异常(刷新后仍失败): ${e2.message}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function fetchUserVideosAPI(
|
|
327
|
+
page,
|
|
328
|
+
username,
|
|
329
|
+
maxVideos,
|
|
330
|
+
log,
|
|
331
|
+
options = {},
|
|
332
|
+
) {
|
|
333
|
+
log(` [API拦截] 获取 @${username} 视频 ...`);
|
|
334
|
+
|
|
335
|
+
await checkCdpHealth(page, options, log);
|
|
336
|
+
|
|
337
|
+
const { blockedRetryWaitsMs = DEFAULT_BLOCKED_RETRY_WAITS } = options;
|
|
331
338
|
let apiRequestUrlRef = { value: null };
|
|
332
339
|
const retryWaits = Array.isArray(blockedRetryWaitsMs)
|
|
333
340
|
? blockedRetryWaitsMs
|
package/src/lib/browser/cdp.js
CHANGED
|
@@ -70,6 +70,19 @@ function checkEdgeArgs() {
|
|
|
70
70
|
});
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* 解码 Windows 命令输出:中文系统 taskkill 输出为 GBK,
|
|
75
|
+
* 先按 UTF-8 解码,出现乱码替换符(U+FFFD)时改用 GBK 重解。
|
|
76
|
+
*/
|
|
77
|
+
function decodeWinOutput(data) {
|
|
78
|
+
if (!Buffer.isBuffer(data)) return String(data ?? "").trim();
|
|
79
|
+
let text = new TextDecoder("utf-8").decode(data);
|
|
80
|
+
if (text.includes("\uFFFD")) {
|
|
81
|
+
text = new TextDecoder("gbk").decode(data);
|
|
82
|
+
}
|
|
83
|
+
return text.trim();
|
|
84
|
+
}
|
|
85
|
+
|
|
73
86
|
/**
|
|
74
87
|
* 解析 netstat -ano 输出,返回监听指定端口的 PID 列表(去重)。
|
|
75
88
|
* 不依赖 WMI,适配 WMI 损坏/禁用的 Windows 机器。
|
|
@@ -89,6 +102,58 @@ export function parseNetstatListeningPids(netstatOutput, port) {
|
|
|
89
102
|
return pids;
|
|
90
103
|
}
|
|
91
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Windows:按监听端口杀掉 Edge 主进程及其进程树(不依赖 WMI,
|
|
107
|
+
* Get-CimInstance 在 WMI 损坏/禁用的机器上查不到进程)。
|
|
108
|
+
* 返回后续清理 Singleton 锁文件的命令。
|
|
109
|
+
*/
|
|
110
|
+
function killEdgeOnWindowsByPort(port) {
|
|
111
|
+
let killCmds = "";
|
|
112
|
+
try {
|
|
113
|
+
const netstatOut = execSync("netstat -ano", {
|
|
114
|
+
encoding: "utf-8",
|
|
115
|
+
timeout: 5000,
|
|
116
|
+
windowsHide: true,
|
|
117
|
+
});
|
|
118
|
+
const pids = parseNetstatListeningPids(netstatOut, port);
|
|
119
|
+
if (pids.length > 0) {
|
|
120
|
+
// 不重定向 stderr:taskkill 的错误信息(拒绝访问/找不到进程)需要可见
|
|
121
|
+
killCmds = pids.map((pid) => `taskkill /F /T /PID ${pid}`).join(" & ");
|
|
122
|
+
console.error(
|
|
123
|
+
`[killEdgeProcesses] 端口 ${port} 监听进程 PID: ${pids.join(", ")}`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
} catch (e) {
|
|
127
|
+
console.error(`[killEdgeProcesses] netstat 查询失败: ${e.message}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (killCmds) {
|
|
131
|
+
// 同步执行并打印结果:taskkill 失败(权限/会话不匹配等)必须可见,不能静默吞掉
|
|
132
|
+
try {
|
|
133
|
+
const out = execSync(`(${killCmds})`, {
|
|
134
|
+
encoding: "buffer",
|
|
135
|
+
timeout: 15000,
|
|
136
|
+
windowsHide: true,
|
|
137
|
+
});
|
|
138
|
+
const lines = decodeWinOutput(out).split(/\r?\n/).filter(Boolean);
|
|
139
|
+
console.error(
|
|
140
|
+
`[killEdgeProcesses] taskkill 结果: ${lines.join(" | ") || "(无输出)"}`,
|
|
141
|
+
);
|
|
142
|
+
} catch (e) {
|
|
143
|
+
console.error(`[killEdgeProcesses] taskkill 失败: ${e.message}`);
|
|
144
|
+
const detail = decodeWinOutput(
|
|
145
|
+
Buffer.concat([
|
|
146
|
+
Buffer.isBuffer(e.stdout) ? e.stdout : Buffer.alloc(0),
|
|
147
|
+
Buffer.isBuffer(e.stderr) ? e.stderr : Buffer.alloc(0),
|
|
148
|
+
]),
|
|
149
|
+
);
|
|
150
|
+
if (detail) console.error(`[killEdgeProcesses] 详情: ${detail}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return `del /f /q "%LOCALAPPDATA%\\Microsoft\\Edge\\Singleton*" 2>nul; exit 0`;
|
|
155
|
+
}
|
|
156
|
+
|
|
92
157
|
function killEdgeProcesses(targetDir, port) {
|
|
93
158
|
return new Promise((resolve) => {
|
|
94
159
|
const platform = os.platform();
|
|
@@ -138,35 +203,11 @@ function killEdgeProcesses(targetDir, port) {
|
|
|
138
203
|
'killall -9 "Microsoft Edge" 2>/dev/null; rm -f ~/Library/Caches/Microsoft\\ Edge/Singleton*; true';
|
|
139
204
|
}
|
|
140
205
|
} else if (platform === "win32") {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const netstatOut = execSync("netstat -ano", {
|
|
147
|
-
encoding: "utf-8",
|
|
148
|
-
timeout: 5000,
|
|
149
|
-
windowsHide: true,
|
|
150
|
-
});
|
|
151
|
-
const pids = parseNetstatListeningPids(netstatOut, port);
|
|
152
|
-
if (pids.length > 0) {
|
|
153
|
-
killCmds = pids
|
|
154
|
-
.map((pid) => `taskkill /F /T /PID ${pid} 2>nul`)
|
|
155
|
-
.join(" & ");
|
|
156
|
-
console.error(
|
|
157
|
-
`[killEdgeProcesses] 端口 ${port} 监听进程 PID: ${pids.join(", ")}`,
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
} catch (e) {
|
|
161
|
-
console.error(`[killEdgeProcesses] netstat 查询失败: ${e.message}`);
|
|
162
|
-
}
|
|
163
|
-
// 找不到监听进程 = 浏览器可能已退出,不做按进程名的兜底(避免误杀其他端口实例)
|
|
164
|
-
command = killCmds
|
|
165
|
-
? `(${killCmds}); del /f /q "%LOCALAPPDATA%\\Microsoft\\Edge\\Singleton*" 2>nul; exit 0`
|
|
166
|
-
: "exit 0";
|
|
167
|
-
} else {
|
|
168
|
-
command = "taskkill /F /IM msedge.exe 2>nul || exit 0";
|
|
169
|
-
}
|
|
206
|
+
// 有端口:按监听端口杀进程树(找不到监听进程 = 浏览器可能已退出,
|
|
207
|
+
// 不做按进程名的兜底,避免误杀其他端口实例)
|
|
208
|
+
command = port
|
|
209
|
+
? killEdgeOnWindowsByPort(port)
|
|
210
|
+
: "taskkill /F /IM msedge.exe 2>nul || exit 0";
|
|
170
211
|
} else {
|
|
171
212
|
if (targetDir) {
|
|
172
213
|
const escapedDir = targetDir.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
@@ -175,36 +175,45 @@ function getProcessStats(pid) {
|
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Windows: 执行 PowerShell -Command 并返回 stdout(trim 后)。
|
|
180
|
+
* 统一 execSync 选项,避免各处重复配置。
|
|
181
|
+
*/
|
|
182
|
+
function runPowerShell(psCommand, timeoutMs = 3000) {
|
|
183
|
+
return execSync(`powershell -NoProfile -Command "${psCommand}"`, {
|
|
184
|
+
encoding: "utf-8",
|
|
185
|
+
timeout: timeoutMs,
|
|
186
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
187
|
+
windowsHide: true, // Windows 隐藏控制台窗口
|
|
188
|
+
}).trim();
|
|
189
|
+
}
|
|
190
|
+
|
|
178
191
|
// Windows: 从 cmd.exe 往下找真正的 node.exe PID(shell:true 时 cmd.exe 是中间层)
|
|
179
192
|
function getChildPid(parentPid) {
|
|
180
|
-
// 主路径:WMI
|
|
193
|
+
// 主路径:WMI 查询(WMI 正常机器零变化)
|
|
181
194
|
try {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
).trim();
|
|
191
|
-
return parseInt(out, 10) || 0;
|
|
195
|
+
return (
|
|
196
|
+
parseInt(
|
|
197
|
+
runPowerShell(
|
|
198
|
+
`Get-CimInstance Win32_Process -Filter 'ParentProcessId=${parentPid}' | Where-Object { $_.Name -notlike 'conhost*' } | Select-Object -First 1 -ExpandProperty ProcessId`,
|
|
199
|
+
),
|
|
200
|
+
10,
|
|
201
|
+
) || 0
|
|
202
|
+
);
|
|
192
203
|
} catch {
|
|
193
204
|
// WMI 损坏/禁用时 Get-CimInstance 直接报错,落入 fallback
|
|
194
205
|
}
|
|
195
206
|
// fallback:不依赖 WMI,用 Get-Process(.NET API)按启动时间找
|
|
196
207
|
// cmd.exe 之后最早启动的 node.exe(watchdog spawn 后 3s 才调用,node 应已启动)
|
|
197
208
|
try {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
).trim();
|
|
207
|
-
return parseInt(out, 10) || 0;
|
|
209
|
+
return (
|
|
210
|
+
parseInt(
|
|
211
|
+
runPowerShell(
|
|
212
|
+
`$cmd = Get-Process -Id ${parentPid} -ErrorAction SilentlyContinue; if ($cmd) { Get-Process node -ErrorAction SilentlyContinue | Where-Object { $_.StartTime -ge $cmd.StartTime } | Sort-Object StartTime | Select-Object -First 1 -ExpandProperty Id }`,
|
|
213
|
+
),
|
|
214
|
+
10,
|
|
215
|
+
) || 0
|
|
216
|
+
);
|
|
208
217
|
} catch {
|
|
209
218
|
return 0;
|
|
210
219
|
}
|
package/src/watchdog/upgrader.js
CHANGED
|
@@ -467,7 +467,9 @@ async function listWatchdogPids() {
|
|
|
467
467
|
.map((s) => parseInt(s.trim(), 10))
|
|
468
468
|
.filter((n) => n > 0);
|
|
469
469
|
} catch (e) {
|
|
470
|
-
console.error(
|
|
470
|
+
console.error(
|
|
471
|
+
`[upgrader] 枚举 watchdog 进程失败(WMI 不可用?): ${e.message}`,
|
|
472
|
+
);
|
|
471
473
|
return [];
|
|
472
474
|
}
|
|
473
475
|
}
|