tt-help-cli-ycl 1.4.139 → 1.4.140
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/scripts/test-webserver-error-classify.mjs +379 -0
- package/src/clash/checker.js +14 -2
- package/src/clash/daemon.js +7 -1
- package/src/cli/comments.js +5 -4
- package/src/lib/args.js +4 -4
- package/src/lib/constants.js +1 -1
- package/src/lib/parse-ssr.mjs +13 -1
- package/src/lib/port.js +72 -0
- package/src/lib/tiktok-scraper.mjs +62 -4
- package/src/watch/public/app.js +67 -23
- package/src/watch/public/style.css +18 -0
- package/src/webserver/server.mjs +262 -79
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
parseVideoInfo,
|
|
6
6
|
isRetryableFailure,
|
|
7
7
|
CDNBlockedError,
|
|
8
|
+
TikTokStatusError,
|
|
8
9
|
detectAccessDenied,
|
|
9
10
|
} from "./parse-ssr.mjs";
|
|
10
11
|
|
|
@@ -58,6 +59,30 @@ class PageSlot {
|
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
/**
|
|
63
|
+
* 对 page.goto 抛出的错误做分类(goto 阶段失败时拿不到 HTML,
|
|
64
|
+
* detectAccessDenied 无法执行,必须在这里拦截风控类错误)。
|
|
65
|
+
* status 为导航期间捕获的最后一个 tiktok 响应状态码(可能为 null):
|
|
66
|
+
* - 404 → TikTokStatusError(视频/用户不存在,业务结果)
|
|
67
|
+
* - 403/429/未知 → CDNBlockedError,由 webserver 走 CDN 冷却自愈链路
|
|
68
|
+
* (503 + 冷却 + 躺平 + nginx failover),而不是 consecutiveErrors
|
|
69
|
+
* 浏览器重启链——重启对 IP/节点风控无效,只会形成死亡螺旋。
|
|
70
|
+
* - 其余(ERR_ABORTED / Target closed 等重启竞态产物)→ 返回 null,
|
|
71
|
+
* 原样冒泡交由 webserver 分类(503 retryable 且不计连续错误)。
|
|
72
|
+
*/
|
|
73
|
+
export function classifyGotoError(message, status) {
|
|
74
|
+
if (typeof message !== "string") return null;
|
|
75
|
+
if (message.includes("ERR_HTTP_RESPONSE_CODE_FAILURE")) {
|
|
76
|
+
if (status === 404) {
|
|
77
|
+
return new TikTokStatusError(404, "TikTok HTTP 404 (视频/用户不存在)");
|
|
78
|
+
}
|
|
79
|
+
return new CDNBlockedError(
|
|
80
|
+
`goto 非2xx (疑似 IP/节点风控, status=${status ?? "unknown"}): ${message.split("\n")[0]}`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
61
86
|
class PromiseQueue {
|
|
62
87
|
constructor() {
|
|
63
88
|
this._queue = [];
|
|
@@ -110,9 +135,14 @@ export class TikTokScraper {
|
|
|
110
135
|
this.slotIdx = 0;
|
|
111
136
|
this.lastWarmTime = 0;
|
|
112
137
|
this.warmPromise = null;
|
|
138
|
+
// 关闭中标记:restart 时旧实例先 close,在途请求凭此立即失败(503),
|
|
139
|
+
// 而不是打到已销毁的 context 上报 "Cannot read properties of null"。
|
|
140
|
+
this.closing = false;
|
|
113
141
|
}
|
|
114
142
|
|
|
115
143
|
async init() {
|
|
144
|
+
// restart() = close() + init():重建后恢复可用状态
|
|
145
|
+
this.closing = false;
|
|
116
146
|
const executablePath = detectBrowser();
|
|
117
147
|
if (!executablePath) {
|
|
118
148
|
throw new Error(
|
|
@@ -143,6 +173,8 @@ export class TikTokScraper {
|
|
|
143
173
|
}
|
|
144
174
|
|
|
145
175
|
async close() {
|
|
176
|
+
// 先置关闭标记:在途请求的入口检查会立即失败,避免与关闭流程竞态
|
|
177
|
+
this.closing = true;
|
|
146
178
|
if (this.browser) {
|
|
147
179
|
const browser = this.browser;
|
|
148
180
|
let closeTimedOut = false;
|
|
@@ -213,6 +245,10 @@ export class TikTokScraper {
|
|
|
213
245
|
}
|
|
214
246
|
|
|
215
247
|
async _ensurePage(slot) {
|
|
248
|
+
// 实例正在关闭/已关闭(restart 竞态)→ 抛可识别错误,webserver 返回 503 且不计连续错误
|
|
249
|
+
if (this.closing || !this.context) {
|
|
250
|
+
throw new Error("Server is shutting down");
|
|
251
|
+
}
|
|
216
252
|
try {
|
|
217
253
|
if (
|
|
218
254
|
!slot.page.isClosed() &&
|
|
@@ -234,10 +270,32 @@ export class TikTokScraper {
|
|
|
234
270
|
|
|
235
271
|
const page = await this._ensurePage(slot);
|
|
236
272
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
273
|
+
// 捕获导航期间最后一个 tiktok 响应的真实状态码:
|
|
274
|
+
// Chromium 的 ERR_HTTP_RESPONSE_CODE_FAILURE 不携带状态码,
|
|
275
|
+
// 通过 response 事件补齐,用于区分 403/429 风控与 404 不存在。
|
|
276
|
+
let lastStatus = null;
|
|
277
|
+
const onResponse = (resp) => {
|
|
278
|
+
try {
|
|
279
|
+
if (resp.url().includes("tiktok.com")) lastStatus = resp.status();
|
|
280
|
+
} catch {}
|
|
281
|
+
};
|
|
282
|
+
page.on("response", onResponse);
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
await page.goto("view-source:" + url, {
|
|
286
|
+
waitUntil: "domcontentloaded",
|
|
287
|
+
timeout: 15000,
|
|
288
|
+
});
|
|
289
|
+
} catch (e) {
|
|
290
|
+
// goto 阶段失败(HTTP 非 2xx 等)拿不到 HTML,detectAccessDenied 没机会执行。
|
|
291
|
+
// 分类后抛出:风控类 → CDNBlockedError(走 webserver 冷却自愈而非浏览器重启);
|
|
292
|
+
// 404 → TikTokStatusError(业务结果);其余(ERR_ABORTED 等竞态)→ 原样冒泡。
|
|
293
|
+
const classified = classifyGotoError(e?.message, lastStatus);
|
|
294
|
+
if (classified) throw classified;
|
|
295
|
+
throw e;
|
|
296
|
+
} finally {
|
|
297
|
+
page.off("response", onResponse);
|
|
298
|
+
}
|
|
241
299
|
|
|
242
300
|
const content = await page.evaluate(() => {
|
|
243
301
|
const rows = document.querySelectorAll("tr");
|
package/src/watch/public/app.js
CHANGED
|
@@ -3645,9 +3645,13 @@ function renderOfflineCard(
|
|
|
3645
3645
|
loginPorts,
|
|
3646
3646
|
) {
|
|
3647
3647
|
var cmdCount = cmds ? cmds.length : 0;
|
|
3648
|
-
return
|
|
3648
|
+
return tag(
|
|
3649
3649
|
"div",
|
|
3650
|
-
|
|
3650
|
+
{
|
|
3651
|
+
class: "wd-client-card",
|
|
3652
|
+
style:
|
|
3653
|
+
"background:#1e1e2e;border:2px solid #f87171;border-radius:12px;padding:16px;opacity:0.9",
|
|
3654
|
+
},
|
|
3651
3655
|
styled(
|
|
3652
3656
|
"div",
|
|
3653
3657
|
"display:flex;justify-content:space-between;align-items:center;margin-bottom:8px",
|
|
@@ -3873,11 +3877,15 @@ function renderClientCard(
|
|
|
3873
3877
|
),
|
|
3874
3878
|
);
|
|
3875
3879
|
|
|
3876
|
-
return
|
|
3880
|
+
return tag(
|
|
3877
3881
|
"div",
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3882
|
+
{
|
|
3883
|
+
class: "wd-client-card",
|
|
3884
|
+
style:
|
|
3885
|
+
"background:#1e1e2e;border:2px solid " +
|
|
3886
|
+
borderColor +
|
|
3887
|
+
";border-radius:12px;padding:16px",
|
|
3888
|
+
},
|
|
3881
3889
|
header +
|
|
3882
3890
|
styled("div", "color:#888;font-size:12px", osStr) +
|
|
3883
3891
|
styled("div", "color:#888;font-size:12px;margin-bottom:6px", infoLine) +
|
|
@@ -4353,10 +4361,11 @@ function renderProxySection(c, userId) {
|
|
|
4353
4361
|
var h = c.clashHealth;
|
|
4354
4362
|
if (!h || !h.node) return "";
|
|
4355
4363
|
var nodes = (h.meta && h.meta.healthyNodes) || [];
|
|
4364
|
+
var delays = (h.meta && h.meta.healthyNodeDelays) || {};
|
|
4356
4365
|
var hasNodes = nodes.length > 0;
|
|
4357
4366
|
return (
|
|
4358
4367
|
renderProxyLine(h, nodes, userId, 0, hasNodes) +
|
|
4359
|
-
renderNodeList(nodes, h.node, userId, 0)
|
|
4368
|
+
renderNodeList(nodes, h.node, userId, 0, delays)
|
|
4360
4369
|
);
|
|
4361
4370
|
}
|
|
4362
4371
|
|
|
@@ -4364,10 +4373,11 @@ function renderProxySection(c, userId) {
|
|
|
4364
4373
|
function renderProxyChannel(ch, userId) {
|
|
4365
4374
|
if (!ch || !ch.node) return "";
|
|
4366
4375
|
var nodes = (ch.meta && ch.meta.healthyNodes) || [];
|
|
4376
|
+
var delays = (ch.meta && ch.meta.healthyNodeDelays) || {};
|
|
4367
4377
|
var hasNodes = nodes.length > 0;
|
|
4368
4378
|
return (
|
|
4369
4379
|
renderProxyLine(ch, nodes, userId, ch.port || 0, hasNodes) +
|
|
4370
|
-
renderNodeList(nodes, ch.node, userId, ch.port || 0)
|
|
4380
|
+
renderNodeList(nodes, ch.node, userId, ch.port || 0, delays)
|
|
4371
4381
|
);
|
|
4372
4382
|
}
|
|
4373
4383
|
|
|
@@ -4417,31 +4427,49 @@ function renderProxyLine(h, nodes, userId, port, hasNodes) {
|
|
|
4417
4427
|
}
|
|
4418
4428
|
|
|
4419
4429
|
/** 节点列表面板(默认隐藏) */
|
|
4420
|
-
function renderNodeList(nodes, currentNode, userId, port) {
|
|
4430
|
+
function renderNodeList(nodes, currentNode, userId, port, delays) {
|
|
4421
4431
|
if (!nodes || !nodes.length) return "";
|
|
4432
|
+
delays = delays || {};
|
|
4422
4433
|
|
|
4423
4434
|
var items = "";
|
|
4424
4435
|
for (var i = 0; i < nodes.length; i++) {
|
|
4425
|
-
items += renderNodeItem(
|
|
4436
|
+
items += renderNodeItem(
|
|
4437
|
+
nodes[i],
|
|
4438
|
+
nodes[i] === currentNode,
|
|
4439
|
+
userId,
|
|
4440
|
+
port,
|
|
4441
|
+
delays[nodes[i]],
|
|
4442
|
+
);
|
|
4426
4443
|
}
|
|
4427
4444
|
|
|
4428
4445
|
var id = "wdNodes-" + wdEscId(userId) + "-" + port;
|
|
4446
|
+
var expanded = _wdExpandedNodes[wdEscId(userId) + "-" + (port || 0)];
|
|
4429
4447
|
return (
|
|
4430
4448
|
'<div id="' +
|
|
4431
4449
|
id +
|
|
4432
4450
|
'" style="margin-top:4px;border:1px solid #2a2a3a;' +
|
|
4433
|
-
|
|
4451
|
+
"border-radius:6px;overflow:hidden;display:" +
|
|
4452
|
+
(expanded ? "block" : "none") +
|
|
4453
|
+
'">' +
|
|
4434
4454
|
items +
|
|
4435
4455
|
"</div>"
|
|
4436
4456
|
);
|
|
4437
4457
|
}
|
|
4438
4458
|
|
|
4439
4459
|
/** 单个节点行 */
|
|
4440
|
-
function renderNodeItem(name, isActive, userId, port) {
|
|
4460
|
+
function renderNodeItem(name, isActive, userId, port, delay) {
|
|
4441
4461
|
var color = isActive ? "#4ade80" : "#888";
|
|
4442
4462
|
var bg = isActive ? "background:#1a3a2a;" : "";
|
|
4443
4463
|
var dot = isActive ? "●" : "○";
|
|
4444
4464
|
var textColor = isActive ? "#4ade80" : "#aaa";
|
|
4465
|
+
var delayTag =
|
|
4466
|
+
typeof delay === "number" && delay > 0
|
|
4467
|
+
? '<span style="color:' +
|
|
4468
|
+
(isActive ? "#6ee7a0" : "#777") +
|
|
4469
|
+
';font-size:10px">' +
|
|
4470
|
+
Math.round(delay) +
|
|
4471
|
+
"ms</span>"
|
|
4472
|
+
: "";
|
|
4445
4473
|
var action = isActive
|
|
4446
4474
|
? '<span style="margin-left:auto;color:#555;font-size:10px">当前</span>'
|
|
4447
4475
|
: "<button onclick=\"wdSwitchNode('" +
|
|
@@ -4455,7 +4483,7 @@ function renderNodeItem(name, isActive, userId, port) {
|
|
|
4455
4483
|
'color:#fff;border:none;border-radius:2px;cursor:pointer">切换</button>';
|
|
4456
4484
|
|
|
4457
4485
|
return (
|
|
4458
|
-
'<div style="padding:3px 8px;display:flex;align-items:center;gap:6px;font-size:11px;' +
|
|
4486
|
+
'<div class="wd-hover-row" style="padding:3px 8px;display:flex;align-items:center;gap:6px;font-size:11px;' +
|
|
4459
4487
|
bg +
|
|
4460
4488
|
'">' +
|
|
4461
4489
|
'<span style="color:' +
|
|
@@ -4468,6 +4496,7 @@ function renderNodeItem(name, isActive, userId, port) {
|
|
|
4468
4496
|
'">' +
|
|
4469
4497
|
name +
|
|
4470
4498
|
"</span>" +
|
|
4499
|
+
delayTag +
|
|
4471
4500
|
action +
|
|
4472
4501
|
"</div>"
|
|
4473
4502
|
);
|
|
@@ -4475,16 +4504,24 @@ function renderNodeItem(name, isActive, userId, port) {
|
|
|
4475
4504
|
|
|
4476
4505
|
// ── 交互函数 ──
|
|
4477
4506
|
|
|
4507
|
+
// 节点列表面板展开状态(key: userId-port → true),自动刷新重渲染时保持展开
|
|
4508
|
+
var _wdExpandedNodes = {};
|
|
4509
|
+
|
|
4478
4510
|
function wdEscId(s) {
|
|
4479
4511
|
return (s || "").replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
4480
4512
|
}
|
|
4481
4513
|
|
|
4482
4514
|
function wdToggleNodes(userId, port) {
|
|
4483
|
-
var
|
|
4484
|
-
|
|
4485
|
-
);
|
|
4515
|
+
var key = wdEscId(userId) + "-" + (port || 0);
|
|
4516
|
+
var el = document.getElementById("wdNodes-" + key);
|
|
4486
4517
|
if (!el) return;
|
|
4487
|
-
|
|
4518
|
+
var nowHidden = el.style.display === "none";
|
|
4519
|
+
el.style.display = nowHidden ? "block" : "none";
|
|
4520
|
+
if (nowHidden) {
|
|
4521
|
+
_wdExpandedNodes[key] = true;
|
|
4522
|
+
} else {
|
|
4523
|
+
delete _wdExpandedNodes[key];
|
|
4524
|
+
}
|
|
4488
4525
|
}
|
|
4489
4526
|
|
|
4490
4527
|
function wdSwitchNode(userId, node, port) {
|
|
@@ -4578,9 +4615,13 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis, clientId) {
|
|
|
4578
4615
|
var restart = p && p.restartCount > 0 ? String(p.restartCount) : "0";
|
|
4579
4616
|
|
|
4580
4617
|
var prefix = "wd-" + esc(userId) + "-" + idx;
|
|
4581
|
-
html +=
|
|
4618
|
+
html += tag(
|
|
4582
4619
|
"div",
|
|
4583
|
-
|
|
4620
|
+
{
|
|
4621
|
+
class: "wd-hover-row",
|
|
4622
|
+
style:
|
|
4623
|
+
"display:grid;grid-template-columns:1fr 50px 55px 55px 50px 80px 40px;gap:4px;padding:4px 0;align-items:center;font-size:12px;border-top:1px solid #1a1a2a",
|
|
4624
|
+
},
|
|
4584
4625
|
styled(
|
|
4585
4626
|
"span",
|
|
4586
4627
|
"color:" +
|
|
@@ -4710,9 +4751,13 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis, clientId) {
|
|
|
4710
4751
|
var sc = p.status === "running" ? "#4ade80" : "#f87171";
|
|
4711
4752
|
var oid = p.pid || p.cmdId || "?";
|
|
4712
4753
|
var opre = "wd-" + esc(userId) + "-orphan-" + esc(String(oid));
|
|
4713
|
-
html +=
|
|
4754
|
+
html += tag(
|
|
4714
4755
|
"div",
|
|
4715
|
-
|
|
4756
|
+
{
|
|
4757
|
+
class: "wd-hover-row",
|
|
4758
|
+
style:
|
|
4759
|
+
"display:grid;grid-template-columns:1fr 50px 55px 55px 50px 80px 40px;gap:4px;padding:4px 0;align-items:center;font-size:12px;border-top:1px solid #1a1a2a;opacity:0.5",
|
|
4760
|
+
},
|
|
4716
4761
|
styled(
|
|
4717
4762
|
"span",
|
|
4718
4763
|
"color:#666;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:monospace",
|
|
@@ -4795,8 +4840,7 @@ function wdShowLogsModal(clientId, cmdId, title) {
|
|
|
4795
4840
|
_wdLogs.deadline = Date.now() + 90000;
|
|
4796
4841
|
_wdLogs.gotFirst = false;
|
|
4797
4842
|
document.getElementById("wdLogsTitle").textContent = "📄 " + title;
|
|
4798
|
-
document.getElementById("wdLogsStatus").textContent =
|
|
4799
|
-
"⏳ 已请求客户端回传…";
|
|
4843
|
+
document.getElementById("wdLogsStatus").textContent = "⏳ 已请求客户端回传…";
|
|
4800
4844
|
document.getElementById("wdLogsContent").textContent =
|
|
4801
4845
|
"等待客户端心跳回传日志…\n(心跳周期 ≤15s,最长等待 90s;拿到后会实时刷新)";
|
|
4802
4846
|
document.getElementById("wdLogsModal").style.display = "flex";
|
|
@@ -118,6 +118,24 @@ body {
|
|
|
118
118
|
margin-top: 2px;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/* 看门狗面板:列表行 hover 高亮(内联背景用 !important 覆盖,移开后自动恢复) */
|
|
122
|
+
.wd-hover-row {
|
|
123
|
+
transition: background 0.08s ease;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.wd-hover-row:hover {
|
|
127
|
+
background: rgba(96, 165, 250, 0.12) !important;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* 看门狗面板:客户端卡片 hover 微亮(不改边框,保留离线红色警示) */
|
|
131
|
+
.wd-client-card {
|
|
132
|
+
transition: background 0.15s ease;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.wd-client-card:hover {
|
|
136
|
+
background: #232338 !important;
|
|
137
|
+
}
|
|
138
|
+
|
|
121
139
|
.stat-card.clickable {
|
|
122
140
|
cursor: pointer;
|
|
123
141
|
}
|