tt-help-cli-ycl 1.4.144 → 1.4.145
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-daemon-failover.mjs +61 -4
- package/src/clash/checker.js +41 -0
- package/src/clash/daemon.js +26 -0
- package/src/cli/tag.js +104 -329
- package/src/lib/constants.js +6 -5
- package/src/watch/data-store.js +88 -0
- package/src/watch/public/app.js +232 -0
- package/src/watch/public/index.html +53 -0
- package/src/watch/public/style.css +202 -0
- package/src/watch/server.js +60 -0
- package/src/watchdog/process-manager.js +11 -6
package/src/cli/tag.js
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import { writeFileSync } from "fs";
|
|
2
2
|
import { randomUUID } from "crypto";
|
|
3
|
-
import http from "http";
|
|
4
3
|
import readline from "readline";
|
|
5
4
|
import os from "os";
|
|
6
5
|
import {
|
|
7
6
|
getBrowserMemory,
|
|
8
7
|
formatMemoryWithBrowser,
|
|
9
8
|
} from "../lib/browser-memory.js";
|
|
10
|
-
import {
|
|
11
|
-
reportConsecutiveTaskErrors,
|
|
12
|
-
reportClientErrorAndFailover,
|
|
13
|
-
} from "../lib/client-error-report.js";
|
|
9
|
+
import { reportClientErrorAndFailover } from "../lib/client-error-report.js";
|
|
14
10
|
import { fetchTagData, enrichVideosWithLocation } from "../lib/tag-fetcher.js";
|
|
15
11
|
import { killEdgeProcesses, ensureBrowserReady } from "../lib/browser/cdp.js";
|
|
16
12
|
import { getOrCreatePage, isBrowserClosedError } from "../lib/browser/page.js";
|
|
@@ -55,38 +51,6 @@ function isBrowserDisconnected(browser, e) {
|
|
|
55
51
|
return false;
|
|
56
52
|
}
|
|
57
53
|
|
|
58
|
-
/**
|
|
59
|
-
* 探测端口是否已被监听(CDP /json 端点)。
|
|
60
|
-
* 与 cdp.js 的 checkCDPPort 等价,但 tag.js 独立实现,不依赖未导出的内部函数。
|
|
61
|
-
* 用于端口池挑选时跳过被占用端口、以及关闭旧浏览器后确认端口已释放。
|
|
62
|
-
*/
|
|
63
|
-
function checkPortListening(port) {
|
|
64
|
-
return new Promise((resolve) => {
|
|
65
|
-
const req = http.get(`http://127.0.0.1:${port}/json`, (res) => {
|
|
66
|
-
res.on("data", () => {});
|
|
67
|
-
res.on("end", () => resolve(res.statusCode === 200));
|
|
68
|
-
});
|
|
69
|
-
req.on("error", () => resolve(false));
|
|
70
|
-
req.setTimeout(2000, () => {
|
|
71
|
-
resolve(false);
|
|
72
|
-
req.destroy();
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* 等待端口不再被监听(浏览器进程真正退出、端口释放)。
|
|
79
|
-
* @returns {Promise<boolean>} 端口已释放返回 true
|
|
80
|
-
*/
|
|
81
|
-
async function waitForPortReleased(port, timeoutMs = 10000) {
|
|
82
|
-
const deadline = Date.now() + timeoutMs;
|
|
83
|
-
while (Date.now() < deadline) {
|
|
84
|
-
if (!(await checkPortListening(port))) return true;
|
|
85
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
86
|
-
}
|
|
87
|
-
return !(await checkPortListening(port));
|
|
88
|
-
}
|
|
89
|
-
|
|
90
54
|
// 构建带客户端追踪 header 的 fetch 封装
|
|
91
55
|
function buildClientHeaders(clientId, meta, extra = {}) {
|
|
92
56
|
return {
|
|
@@ -808,9 +772,8 @@ export async function handleScoreAll(parsed) {
|
|
|
808
772
|
} = tagScoreAll || {};
|
|
809
773
|
|
|
810
774
|
const baseUrl = serverUrl || DEFAULT_SERVER;
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
let cdpPort = port || defaultScoreAllPort;
|
|
775
|
+
// 固定端口:一次会话一个浏览器,全程不换端口、不重启浏览器(错误只打印,不自愈)
|
|
776
|
+
const cdpPort = port || 7222;
|
|
814
777
|
const effectiveProxy = cliProxy || configuredProxy;
|
|
815
778
|
const targetCountries = resolveTargetCountries(countries);
|
|
816
779
|
|
|
@@ -863,62 +826,7 @@ export async function handleScoreAll(parsed) {
|
|
|
863
826
|
|
|
864
827
|
await setupPageRequestBlocking(page);
|
|
865
828
|
|
|
866
|
-
//
|
|
867
|
-
const usedPorts = new Set([cdpPort]);
|
|
868
|
-
|
|
869
|
-
// 端口池起始以 CLI --port / 环境变量 / 默认 7222 为准
|
|
870
|
-
const portPoolStart = cdpPort;
|
|
871
|
-
const portPoolSize = Math.max(
|
|
872
|
-
2,
|
|
873
|
-
parseInt(process.env.TAG_SCOREALL_PORT_POOL_SIZE || "10", 10) || 10,
|
|
874
|
-
);
|
|
875
|
-
|
|
876
|
-
async function pickNextPort(currentPort) {
|
|
877
|
-
const candidates = [];
|
|
878
|
-
for (let i = 0; i < portPoolSize; i++) {
|
|
879
|
-
const p = portPoolStart + i;
|
|
880
|
-
if (p !== currentPort) candidates.push(p);
|
|
881
|
-
}
|
|
882
|
-
// 跳过已被监听的端口(其他 score-all/attach 等程序的浏览器),
|
|
883
|
-
// 避免 attach 到别人的实例、后续 close/kill 时互相干扰导致浏览器多开
|
|
884
|
-
const freePorts = [];
|
|
885
|
-
for (const p of candidates) {
|
|
886
|
-
if (!(await checkPortListening(p))) freePorts.push(p);
|
|
887
|
-
}
|
|
888
|
-
const pool = freePorts.length > 0 ? freePorts : candidates;
|
|
889
|
-
if (pool.length === 0) return currentPort;
|
|
890
|
-
return pool[Math.floor(Math.random() * pool.length)];
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
// ── 内存回收配置 ──
|
|
894
|
-
const RECYCLE_CONFIG = {
|
|
895
|
-
nodeRssMb: Math.max(
|
|
896
|
-
256,
|
|
897
|
-
parseInt(process.env.TAG_SCOREALL_RECYCLE_RSS_MB || "2000", 10) || 2000,
|
|
898
|
-
),
|
|
899
|
-
nodeHeapMb: Math.max(
|
|
900
|
-
128,
|
|
901
|
-
parseInt(process.env.TAG_SCOREALL_RECYCLE_HEAP_MB || "2000", 10) || 2000,
|
|
902
|
-
),
|
|
903
|
-
edgeMb: Math.max(
|
|
904
|
-
500,
|
|
905
|
-
parseInt(process.env.TAG_SCOREALL_RECYCLE_EDGE_MB || "2000", 10) || 2000,
|
|
906
|
-
),
|
|
907
|
-
cooldownMs: Math.max(
|
|
908
|
-
0,
|
|
909
|
-
parseInt(process.env.TAG_SCOREALL_RECYCLE_COOLDOWN_MS || "180000", 10) ||
|
|
910
|
-
180000,
|
|
911
|
-
),
|
|
912
|
-
periodicEvery: Math.max(
|
|
913
|
-
0,
|
|
914
|
-
parseInt(process.env.TAG_SCOREALL_PERIODIC_RECYCLE_EVERY || "0", 10) || 0,
|
|
915
|
-
),
|
|
916
|
-
switchPortOnRecycle:
|
|
917
|
-
String(process.env.TAG_SCOREALL_SWITCH_PORT_ON_RECYCLE || "0") !== "0",
|
|
918
|
-
};
|
|
919
|
-
let lastRecycleAt = 0;
|
|
920
|
-
|
|
921
|
-
// ── 轻量回收:只清空当前 tab ──
|
|
829
|
+
// ── 轻量回收:只清空当前 tab(不重启浏览器) ──
|
|
922
830
|
async function recyclePage() {
|
|
923
831
|
if (!page || page.isClosed()) {
|
|
924
832
|
page = await getOrCreatePage(browser);
|
|
@@ -937,117 +845,29 @@ export async function handleScoreAll(parsed) {
|
|
|
937
845
|
}
|
|
938
846
|
}
|
|
939
847
|
|
|
940
|
-
// ── 深度回收:关闭浏览器 + 可选切换端口 ──
|
|
941
|
-
async function recycleCdpSession(reason, { switchPort = true } = {}) {
|
|
942
|
-
const oldPort = cdpPort;
|
|
943
|
-
const shouldSwitch = switchPort && RECYCLE_CONFIG.switchPortOnRecycle;
|
|
944
|
-
if (shouldSwitch) {
|
|
945
|
-
cdpPort = await pickNextPort(cdpPort);
|
|
946
|
-
cdpOpts.port = cdpPort;
|
|
947
|
-
clientMeta.port = cdpPort;
|
|
948
|
-
usedPorts.add(cdpPort);
|
|
949
|
-
}
|
|
950
|
-
const portHint =
|
|
951
|
-
oldPort === cdpPort ? `port=${cdpPort}` : `port ${oldPort}→${cdpPort}`;
|
|
952
|
-
log(` ♻️ 重建 CDP 会话 (${reason}; ${portHint})...`);
|
|
953
|
-
if (page) {
|
|
954
|
-
await page.close().catch(() => {});
|
|
955
|
-
page = null;
|
|
956
|
-
}
|
|
957
|
-
// CDP 模式下 browser.close() 只断开连接不杀进程,必须显式 kill 旧端口实例;
|
|
958
|
-
// kill 后确认端口已释放,再启动新实例,防止旧浏览器窗口残留(多开)
|
|
959
|
-
if (oldPort !== cdpPort) {
|
|
960
|
-
await killEdgeProcesses(null, oldPort).catch(() => {});
|
|
961
|
-
if (!(await waitForPortReleased(oldPort))) {
|
|
962
|
-
log(` ⚠️ 旧端口 ${oldPort} 未释放,再次尝试关闭...`);
|
|
963
|
-
await killEdgeProcesses(null, oldPort).catch(() => {});
|
|
964
|
-
if (!(await waitForPortReleased(oldPort, 5000))) {
|
|
965
|
-
log(
|
|
966
|
-
` ⚠️ 旧端口 ${oldPort} 仍未释放,继续使用新端口(旧浏览器窗口可能残留,退出时会统一清理)`,
|
|
967
|
-
);
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
await browser.close().catch(() => {});
|
|
972
|
-
browser = await ensureBrowserReady(cdpOpts);
|
|
973
|
-
page = await getOrCreatePage(browser);
|
|
974
|
-
await setupPageRequestBlocking(page);
|
|
975
|
-
lastRecycleAt = Date.now();
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
// ── 内存检查 + 自动回收 ──
|
|
979
|
-
// 策略:Node.js 内存超阈值 → 不换端口(只重建连接)
|
|
980
|
-
// Edge 内存超阈值 → 换端口(杀旧浏览器 + 启新实例,彻底释放)
|
|
981
|
-
// 定期轮次 → 不换端口
|
|
982
|
-
async function maybeRecycleForMemory() {
|
|
983
|
-
const mem = process.memoryUsage();
|
|
984
|
-
const rssMB = mem.rss / 1024 / 1024;
|
|
985
|
-
const heapMB = mem.heapUsed / 1024 / 1024;
|
|
986
|
-
const edgeMB = getBrowserMemory(cdpPort);
|
|
987
|
-
|
|
988
|
-
const nodeOver =
|
|
989
|
-
rssMB >= RECYCLE_CONFIG.nodeRssMb || heapMB >= RECYCLE_CONFIG.nodeHeapMb;
|
|
990
|
-
const edgeOver = edgeMB !== null && edgeMB >= RECYCLE_CONFIG.edgeMb;
|
|
991
|
-
|
|
992
|
-
if (nodeOver || edgeOver) {
|
|
993
|
-
// 冷却期内跳过
|
|
994
|
-
if (
|
|
995
|
-
RECYCLE_CONFIG.cooldownMs > 0 &&
|
|
996
|
-
Date.now() - lastRecycleAt < RECYCLE_CONFIG.cooldownMs
|
|
997
|
-
)
|
|
998
|
-
return;
|
|
999
|
-
|
|
1000
|
-
const parts = [];
|
|
1001
|
-
if (rssMB >= RECYCLE_CONFIG.nodeRssMb)
|
|
1002
|
-
parts.push(`rss=${rssMB.toFixed(0)}≥${RECYCLE_CONFIG.nodeRssMb}`);
|
|
1003
|
-
if (heapMB >= RECYCLE_CONFIG.nodeHeapMb)
|
|
1004
|
-
parts.push(`heap=${heapMB.toFixed(0)}≥${RECYCLE_CONFIG.nodeHeapMb}`);
|
|
1005
|
-
if (edgeOver)
|
|
1006
|
-
parts.push(`edge:${cdpPort}=${edgeMB}≥${RECYCLE_CONFIG.edgeMb}`);
|
|
1007
|
-
|
|
1008
|
-
await recycleCdpSession(`mem ${parts.join("+")}`, {
|
|
1009
|
-
switchPort: edgeOver,
|
|
1010
|
-
});
|
|
1011
|
-
return;
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
// 定期轮次回收(默认关闭)
|
|
1015
|
-
if (
|
|
1016
|
-
RECYCLE_CONFIG.periodicEvery > 0 &&
|
|
1017
|
-
totalScored > 0 &&
|
|
1018
|
-
totalScored % RECYCLE_CONFIG.periodicEvery === 0
|
|
1019
|
-
) {
|
|
1020
|
-
await recycleCdpSession(`periodic #${totalScored}`, {
|
|
1021
|
-
switchPort: false,
|
|
1022
|
-
});
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
848
|
let totalScored = 0;
|
|
1027
849
|
let emptyRounds = 0; // 连续无任务的轮数
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
(parseInt(process.env.TAG_SCOREALL_403_RETRY_3 || "50", 10) || 50) * 1000,
|
|
1050
|
-
];
|
|
850
|
+
|
|
851
|
+
// ── 错误计数(纯观察,不做任何自愈) ──
|
|
852
|
+
// 阈值语义:连续超过 N 次触发动作(即第 N+1 次)
|
|
853
|
+
// 403 连续 >2 次 → 上报服务端 + 强制 fallback(daemon 换节点),清零后继续跑
|
|
854
|
+
// 网络错误 连续 >10 次 → 打印退出(服务端不可达时 fallback 请求也发不出去)
|
|
855
|
+
// 验证码 连续 >3 次(人工一直未解决)→ 打印退出
|
|
856
|
+
// 其他错误 连续 >10 次 → 打印退出
|
|
857
|
+
// 清零规则:成功处理一个 tag → 全部清零;fallback 触发 → 403 计数清零;
|
|
858
|
+
// 人工点掉验证码 → 验证码计数清零
|
|
859
|
+
const errorCounts = { http403: 0, network: 0, captcha: 0, other: 0 };
|
|
860
|
+
const ERROR_LIMITS = { http403: 2, network: 10, captcha: 3, other: 10 };
|
|
861
|
+
const resetAllErrorCounts = () => {
|
|
862
|
+
errorCounts.http403 = 0;
|
|
863
|
+
errorCounts.network = 0;
|
|
864
|
+
errorCounts.captcha = 0;
|
|
865
|
+
errorCounts.other = 0;
|
|
866
|
+
};
|
|
867
|
+
const bumpErrorCount = (type) => {
|
|
868
|
+
errorCounts[type]++;
|
|
869
|
+
return errorCounts[type] > ERROR_LIMITS[type];
|
|
870
|
+
};
|
|
1051
871
|
|
|
1052
872
|
// 生成客户端 ID,用于服务端追踪
|
|
1053
873
|
const clientId = randomUUID();
|
|
@@ -1080,14 +900,10 @@ export async function handleScoreAll(parsed) {
|
|
|
1080
900
|
if (enrichScraper) await enrichScraper.close().catch(() => {});
|
|
1081
901
|
log("正在关闭 CDP 浏览器...");
|
|
1082
902
|
try {
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
killEdgeProcesses(null, p),
|
|
1088
|
-
new Promise((r) => setTimeout(r, 5000)),
|
|
1089
|
-
]).catch(() => {});
|
|
1090
|
-
}
|
|
903
|
+
await Promise.race([
|
|
904
|
+
killEdgeProcesses(null, cdpPort),
|
|
905
|
+
new Promise((r) => setTimeout(r, 5000)),
|
|
906
|
+
]).catch(() => {});
|
|
1091
907
|
log("浏览器已关闭");
|
|
1092
908
|
} catch (e) {
|
|
1093
909
|
log(`关闭浏览器失败: ${e.message}`);
|
|
@@ -1116,43 +932,38 @@ export async function handleScoreAll(parsed) {
|
|
|
1116
932
|
return;
|
|
1117
933
|
}
|
|
1118
934
|
try {
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
{
|
|
1128
|
-
headers: buildClientHeaders(clientId, clientMeta),
|
|
1129
|
-
},
|
|
1130
|
-
);
|
|
1131
|
-
const tagsData = await tagsRes.json();
|
|
935
|
+
// 从服务端取下一个 new 标签(同时查剩余数量)
|
|
936
|
+
const tagsRes = await fetch(
|
|
937
|
+
`${baseUrl}/api/tags?status=new&limit=1&userId=${encodeURIComponent(userId)}`,
|
|
938
|
+
{
|
|
939
|
+
headers: buildClientHeaders(clientId, clientMeta),
|
|
940
|
+
},
|
|
941
|
+
);
|
|
942
|
+
const tagsData = await tagsRes.json();
|
|
1132
943
|
|
|
1133
|
-
|
|
1134
|
-
|
|
944
|
+
if (!tagsData.tags || tagsData.tags.length === 0) {
|
|
945
|
+
emptyRounds++;
|
|
1135
946
|
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
continue;
|
|
1144
|
-
}
|
|
1145
|
-
log(`⏳ 暂无待打分标签(连续 ${emptyRounds} 轮),10 秒后重试...`);
|
|
1146
|
-
await new Promise((r) => setTimeout(r, 10000));
|
|
947
|
+
// 服务端定时发现会自动补充标签,客户端只需等待
|
|
948
|
+
if (emptyRounds >= 5) {
|
|
949
|
+
log(
|
|
950
|
+
`⏳ 连续 ${emptyRounds} 轮无标签,等待服务端发现补充(30 秒后重试)...`,
|
|
951
|
+
);
|
|
952
|
+
await new Promise((r) => setTimeout(r, 30000));
|
|
953
|
+
emptyRounds = 0;
|
|
1147
954
|
continue;
|
|
1148
955
|
}
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
tag = tagsData.tags[0].tag.replace(/^#+/, "").trim().toLowerCase();
|
|
1153
|
-
currentTag = tag;
|
|
1154
|
-
tag403Attempts = 0;
|
|
956
|
+
log(`⏳ 暂无待打分标签(连续 ${emptyRounds} 轮),10 秒后重试...`);
|
|
957
|
+
await new Promise((r) => setTimeout(r, 10000));
|
|
958
|
+
continue;
|
|
1155
959
|
}
|
|
960
|
+
|
|
961
|
+
// 有任务了,重置计数器
|
|
962
|
+
emptyRounds = 0;
|
|
963
|
+
const tag = tagsData.tags[0].tag
|
|
964
|
+
.replace(/^#+/, "")
|
|
965
|
+
.trim()
|
|
966
|
+
.toLowerCase();
|
|
1156
967
|
const startTime = Date.now();
|
|
1157
968
|
|
|
1158
969
|
log(`[${totalScored + 1}] 正在打分 #${tag} ...`);
|
|
@@ -1170,8 +981,7 @@ export async function handleScoreAll(parsed) {
|
|
|
1170
981
|
};
|
|
1171
982
|
|
|
1172
983
|
// 锁定 tag(meta 中不放入 tag,避免非 ASCII 字符导致 header ByteString 报错)
|
|
1173
|
-
|
|
1174
|
-
if (!retrySameTag) {
|
|
984
|
+
{
|
|
1175
985
|
const claimRes = await fetch(
|
|
1176
986
|
`${baseUrl}/api/tags/claim?userId=${encodeURIComponent(userId)}`,
|
|
1177
987
|
{
|
|
@@ -1233,7 +1043,6 @@ export async function handleScoreAll(parsed) {
|
|
|
1233
1043
|
totalScored++;
|
|
1234
1044
|
await randomDelay(0, 5000);
|
|
1235
1045
|
await recyclePage();
|
|
1236
|
-
await maybeRecycleForMemory();
|
|
1237
1046
|
continue;
|
|
1238
1047
|
}
|
|
1239
1048
|
// 非 tag_not_found 的 0 视频 → 抛异常让其走 error 重试路径,避免误标 dead
|
|
@@ -1295,18 +1104,14 @@ export async function handleScoreAll(parsed) {
|
|
|
1295
1104
|
);
|
|
1296
1105
|
videos = enriched.videos;
|
|
1297
1106
|
|
|
1298
|
-
// CDN
|
|
1107
|
+
// CDN 限流:打印并计入其他错误计数(不自愈、不重启 scraper)
|
|
1299
1108
|
const cdnBlocked = enriched.cdnBlockedCount || 0;
|
|
1300
1109
|
if (cdnBlocked > 0) {
|
|
1301
1110
|
const cdnRatio = cdnBlocked / (videos.length || 1);
|
|
1302
|
-
|
|
1111
|
+
bumpErrorCount("other");
|
|
1303
1112
|
log(
|
|
1304
|
-
` ⚠️ CDN 限流: ${cdnBlocked}/${videos.length} (${(cdnRatio * 100).toFixed(0)}%)
|
|
1113
|
+
` ⚠️ CDN 限流: ${cdnBlocked}/${videos.length} (${(cdnRatio * 100).toFixed(0)}%),计入其他错误计数 (${errorCounts.other}/${ERROR_LIMITS.other})`,
|
|
1305
1114
|
);
|
|
1306
|
-
await new Promise((r) => setTimeout(r, coolSec * 1000));
|
|
1307
|
-
log(` 正在重启 TikTokScraper...`);
|
|
1308
|
-
await enrichScraper.restart();
|
|
1309
|
-
log(` ✅ TikTokScraper 已重启`);
|
|
1310
1115
|
}
|
|
1311
1116
|
}
|
|
1312
1117
|
|
|
@@ -1334,7 +1139,6 @@ export async function handleScoreAll(parsed) {
|
|
|
1334
1139
|
` ⏭️ 重试 ${COUNTRY_MAX_RETRIES} 次后国家识别率仍 ${(rate * 100).toFixed(0)}% < ${(COUNTRY_RATE_MIN * 100).toFixed(0)}%,跳过该标签,处理下一个`,
|
|
1335
1140
|
);
|
|
1336
1141
|
await recyclePage();
|
|
1337
|
-
await maybeRecycleForMemory();
|
|
1338
1142
|
await randomDelay(3000, 7000);
|
|
1339
1143
|
continue;
|
|
1340
1144
|
}
|
|
@@ -1379,10 +1183,7 @@ export async function handleScoreAll(parsed) {
|
|
|
1379
1183
|
` ${icon} ${result.status} score=${result.score} authors=${result.authorCount} matched=${result.matchedAuthors} (${elapsed}s)${mc ? " " + mc : ""}${memStr}`,
|
|
1380
1184
|
);
|
|
1381
1185
|
log("");
|
|
1382
|
-
|
|
1383
|
-
retrySameTag = false; // 成功,不再重试
|
|
1384
|
-
tag403Attempts = 0;
|
|
1385
|
-
bannedCount = 0; // 成功 tag,重置连续判封计数
|
|
1186
|
+
resetAllErrorCounts(); // 成功一个 tag → 所有错误计数清零
|
|
1386
1187
|
// 每 10 个标签输出一次内存汇总
|
|
1387
1188
|
if (totalScored % 10 === 0) {
|
|
1388
1189
|
log(
|
|
@@ -1390,7 +1191,6 @@ export async function handleScoreAll(parsed) {
|
|
|
1390
1191
|
);
|
|
1391
1192
|
}
|
|
1392
1193
|
await recyclePage();
|
|
1393
|
-
await maybeRecycleForMemory();
|
|
1394
1194
|
await randomDelay(3000, 7000);
|
|
1395
1195
|
} catch (e) {
|
|
1396
1196
|
// 浏览器窗口被手动关闭(CDP 断开)→ 自动退出,避免无限重试循环
|
|
@@ -1409,7 +1209,17 @@ export async function handleScoreAll(parsed) {
|
|
|
1409
1209
|
e.message.includes("fetch failed") ||
|
|
1410
1210
|
e.message.includes("network")));
|
|
1411
1211
|
if (isNetworkError) {
|
|
1412
|
-
|
|
1212
|
+
const over = bumpErrorCount("network");
|
|
1213
|
+
log(
|
|
1214
|
+
` ⚠️ 服务端连接失败 (${e.message}),15 秒后重试(连续网络错误 ${errorCounts.network} 次)...`,
|
|
1215
|
+
);
|
|
1216
|
+
if (over) {
|
|
1217
|
+
log(
|
|
1218
|
+
` ❌ 连续网络错误超过 ${ERROR_LIMITS.network} 次 → 打印并退出(服务端不可达,fallback 发不出去)`,
|
|
1219
|
+
);
|
|
1220
|
+
await cleanup();
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1413
1223
|
await new Promise((r) => setTimeout(r, 15000));
|
|
1414
1224
|
continue;
|
|
1415
1225
|
}
|
|
@@ -1423,35 +1233,16 @@ export async function handleScoreAll(parsed) {
|
|
|
1423
1233
|
e.message &&
|
|
1424
1234
|
(e.message.includes("验证码") || e.message.includes("captcha"));
|
|
1425
1235
|
|
|
1426
|
-
// ── 403
|
|
1427
|
-
//
|
|
1236
|
+
// ── 403:当前 tag 不上报(留在服务端 scoring 状态超时回收),
|
|
1237
|
+
// 只计数;连续 >2 次上报服务端 + 强制 fallback(daemon 换节点),
|
|
1238
|
+
// 计数清零后继续跑 ──
|
|
1428
1239
|
if (is403) {
|
|
1429
|
-
const
|
|
1430
|
-
|
|
1431
|
-
if (
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
log(
|
|
1436
|
-
` 🚫 HTTP 403,判定被封,重启浏览器后 ${waitMs / 1000}s 第 ${tag403Attempts}/${BAN_RETRY_WAITS.length} 次重试 #${currentTag} ...`,
|
|
1437
|
-
);
|
|
1438
|
-
await recycleCdpSession(`首次被封`, { switchPort: false });
|
|
1439
|
-
} else {
|
|
1440
|
-
log(
|
|
1441
|
-
` 🚫 HTTP 403,判定被封,${waitMs / 1000}s 后第 ${tag403Attempts}/${BAN_RETRY_WAITS.length} 次重试 #${currentTag} ...`,
|
|
1442
|
-
);
|
|
1443
|
-
await recyclePage();
|
|
1444
|
-
}
|
|
1445
|
-
await new Promise((r) => setTimeout(r, waitMs));
|
|
1446
|
-
continue;
|
|
1447
|
-
}
|
|
1448
|
-
// 重试耗尽:判定“被封”
|
|
1449
|
-
bannedCount++;
|
|
1450
|
-
log(
|
|
1451
|
-
` 🚫 30/40/50s 重试 ${BAN_RETRY_WAITS.length} 次仍 403,判定账号被封(连续判封 ${bannedCount} 次)`,
|
|
1452
|
-
);
|
|
1453
|
-
if (bannedCount >= 2) {
|
|
1454
|
-
log(` ⚠️ 连续两次判封 → 汇报服务端 + 触发 daemon fallback`);
|
|
1240
|
+
const over = bumpErrorCount("http403");
|
|
1241
|
+
log(` 🚫 HTTP 403(连续第 ${errorCounts.http403} 次)`);
|
|
1242
|
+
if (over) {
|
|
1243
|
+
log(
|
|
1244
|
+
` ⚠️ 连续 403 超过 ${ERROR_LIMITS.http403} 次 → 上报服务端 + 触发 daemon fallback`,
|
|
1245
|
+
);
|
|
1455
1246
|
await reportClientErrorAndFailover({
|
|
1456
1247
|
post: (url, body) =>
|
|
1457
1248
|
fetch(url, {
|
|
@@ -1464,16 +1255,13 @@ export async function handleScoreAll(parsed) {
|
|
|
1464
1255
|
baseUrl,
|
|
1465
1256
|
userId,
|
|
1466
1257
|
errorType: "consecutive_blocked",
|
|
1467
|
-
message:
|
|
1258
|
+
message: `连续 ${ERROR_LIMITS.http403 + 1} 次 403 (port=${cdpPort})`,
|
|
1468
1259
|
stage: "tag-scoring",
|
|
1469
1260
|
log,
|
|
1470
1261
|
});
|
|
1471
|
-
|
|
1262
|
+
errorCounts.http403 = 0; // fallback 触发后清零
|
|
1472
1263
|
}
|
|
1473
|
-
retrySameTag = false;
|
|
1474
|
-
tag403Attempts = 0;
|
|
1475
1264
|
await recyclePage();
|
|
1476
|
-
await maybeRecycleForMemory();
|
|
1477
1265
|
await randomDelay(3000, 7000);
|
|
1478
1266
|
continue;
|
|
1479
1267
|
}
|
|
@@ -1482,9 +1270,9 @@ export async function handleScoreAll(parsed) {
|
|
|
1482
1270
|
log(` ❌ 失败: ${e.message}`);
|
|
1483
1271
|
|
|
1484
1272
|
if (isCaptcha) {
|
|
1485
|
-
|
|
1273
|
+
bumpErrorCount("captcha");
|
|
1486
1274
|
log(
|
|
1487
|
-
` 🔐
|
|
1275
|
+
` 🔐 验证码(连续第 ${errorCounts.captcha} 次),等待用户手动解决...`,
|
|
1488
1276
|
);
|
|
1489
1277
|
|
|
1490
1278
|
// 上报验证码错误
|
|
@@ -1507,24 +1295,31 @@ export async function handleScoreAll(parsed) {
|
|
|
1507
1295
|
} catch {}
|
|
1508
1296
|
|
|
1509
1297
|
// 等待用户手动解决验证码(最多 3 分钟)
|
|
1298
|
+
let captchaResolved = false;
|
|
1510
1299
|
try {
|
|
1511
1300
|
const captchaResult = await waitAndGetCaptcha(page, {
|
|
1512
1301
|
waitMs: 180000,
|
|
1513
1302
|
pollInterval: 5000,
|
|
1514
1303
|
log: () => {},
|
|
1515
1304
|
});
|
|
1305
|
+
captchaResolved = !!captchaResult.resolved;
|
|
1516
1306
|
if (captchaResult.detected && !captchaResult.resolved) {
|
|
1517
1307
|
await closeCaptcha(page).catch(() => {});
|
|
1518
1308
|
}
|
|
1519
1309
|
} catch {}
|
|
1520
1310
|
|
|
1521
|
-
if (
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1311
|
+
if (captchaResolved) {
|
|
1312
|
+
// 人工点掉验证码 → 验证码计数清零
|
|
1313
|
+
log(` ✅ 验证码已被人工处理,验证码计数清零`);
|
|
1314
|
+
errorCounts.captcha = 0;
|
|
1315
|
+
} else if (errorCounts.captcha > ERROR_LIMITS.captcha) {
|
|
1316
|
+
log(
|
|
1317
|
+
` ❌ 连续验证码超过 ${ERROR_LIMITS.captcha} 次且未解决 → 打印并退出`,
|
|
1318
|
+
);
|
|
1319
|
+
await cleanup();
|
|
1320
|
+
return;
|
|
1525
1321
|
}
|
|
1526
|
-
|
|
1527
|
-
consecutiveCaptcha = 0;
|
|
1322
|
+
continue;
|
|
1528
1323
|
}
|
|
1529
1324
|
|
|
1530
1325
|
try {
|
|
@@ -1543,34 +1338,14 @@ export async function handleScoreAll(parsed) {
|
|
|
1543
1338
|
);
|
|
1544
1339
|
} catch {}
|
|
1545
1340
|
|
|
1546
|
-
// 连续任务错误达到阈值,上报服务端 + 触发 daemon fallback 换节点
|
|
1547
|
-
consecutiveTaskErrors++;
|
|
1548
|
-
if (consecutiveTaskErrors >= CONSECUTIVE_TASK_ERRORS_MAX) {
|
|
1549
|
-
log(
|
|
1550
|
-
` ⚠️ 连续任务错误 ${consecutiveTaskErrors}/${CONSECUTIVE_TASK_ERRORS_MAX} → 汇报服务端 + 触发 fallback`,
|
|
1551
|
-
);
|
|
1552
|
-
await reportConsecutiveTaskErrors({
|
|
1553
|
-
post: (url, body) =>
|
|
1554
|
-
fetch(url, {
|
|
1555
|
-
method: "POST",
|
|
1556
|
-
headers: buildClientHeaders(clientId, clientMeta, {
|
|
1557
|
-
"Content-Type": "application/json",
|
|
1558
|
-
}),
|
|
1559
|
-
body: JSON.stringify(body),
|
|
1560
|
-
}),
|
|
1561
|
-
baseUrl,
|
|
1562
|
-
userId,
|
|
1563
|
-
message: `连续 ${consecutiveTaskErrors} 个标签处理失败 (port=${cdpPort})`,
|
|
1564
|
-
stage: "tag-scoring",
|
|
1565
|
-
log,
|
|
1566
|
-
});
|
|
1567
|
-
consecutiveTaskErrors = 0;
|
|
1568
|
-
}
|
|
1569
1341
|
totalScored++;
|
|
1570
|
-
|
|
1571
|
-
|
|
1342
|
+
const over = bumpErrorCount("other");
|
|
1343
|
+
if (over) {
|
|
1344
|
+
log(` ❌ 连续其他错误超过 ${ERROR_LIMITS.other} 次 → 打印并退出`);
|
|
1345
|
+
await cleanup();
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1572
1348
|
await recyclePage();
|
|
1573
|
-
await maybeRecycleForMemory();
|
|
1574
1349
|
}
|
|
1575
1350
|
}
|
|
1576
1351
|
} finally {
|
package/src/lib/constants.js
CHANGED
|
@@ -360,15 +360,16 @@ const HELP_TEXT = [
|
|
|
360
360
|
" 选项:",
|
|
361
361
|
" --countries <CSV> 目标国家,逗号分隔(默认 13 个欧洲国家)",
|
|
362
362
|
" -s, --server <URL> 服务端地址(默认 http://127.0.0.1:3000)",
|
|
363
|
-
" --port <N>
|
|
363
|
+
" --port <N> 固定 CDP 端口(默认 7222,全程不切换)",
|
|
364
364
|
" --discover 开启自动发现:剩余 new 标签 < 5 时后台异步生成,连续 3 轮空兜底",
|
|
365
365
|
" --discover-count <N> 每次发现每个国家生成标签数(默认 10,上限 20)",
|
|
366
366
|
" 行为:",
|
|
367
|
-
" 连续 3 次
|
|
367
|
+
" 错误只打印不自愈:403 连续 3 次 → 上报服务端并触发 daemon 换节点;",
|
|
368
|
+
" 网络错误/其他错误连续 11 次 → 退出;验证码连续 4 次未人工解决 → 退出",
|
|
369
|
+
" 成功一个 tag 全部计数清零;浏览器窗口被手动关闭 → 自动退出",
|
|
368
370
|
" 环境变量:",
|
|
369
|
-
"
|
|
370
|
-
"
|
|
371
|
-
" TAG_SCOREALL_RECYCLE_HEAP_MB=N 内存回收阈值 Heap MB(默认 2000)",
|
|
371
|
+
" TAG_SCOREALL_MIN_COUNTRY_RATE=0.8 国家识别率下限(低于则重试)",
|
|
372
|
+
" TAG_SCOREALL_COUNTRY_MAX_RETRIES=2 国家识别率不足最大重试次数",
|
|
372
373
|
" 示例: tt-help tag score-all",
|
|
373
374
|
" tt-help tag score-all --countries ES -s http://127.0.0.1:3001",
|
|
374
375
|
" tt-help tag score-all --port 9222 -s http://127.0.0.1:3001 --discover",
|