tt-help-cli-ycl 1.4.17 → 1.4.19
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/comments.js +6 -4
- package/src/cli/tag.js +5 -7
- package/src/cli/watch.js +15 -4
- package/src/lib/args.js +22 -7
- package/src/lib/constants.js +12 -2
- package/src/watch/data-store.js +206 -8
- package/src/watch/db-crud.js +16 -1
- package/src/watch/db-schema.js +5 -0
- package/src/watch/db-stats.js +2 -6
- package/src/watch/db-tags.js +106 -6
- package/src/watch/public/app.js +554 -11
- package/src/watch/public/index.html +4 -1
- package/src/watch/public/style.css +41 -0
- package/src/watch/server.js +273 -7
- package/src/watch/tag-service.js +317 -10
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
</head>
|
|
10
10
|
|
|
11
11
|
<body>
|
|
12
|
+
<canvas id="fireworkCanvas"></canvas>
|
|
12
13
|
<div class="header">
|
|
13
14
|
<h1>TikTok 采集监控</h1>
|
|
14
15
|
<div class="meta" id="fileMeta">加载中...</div>
|
|
@@ -325,6 +326,7 @@
|
|
|
325
326
|
<div class="label">🔄 刷新</div>
|
|
326
327
|
</div>
|
|
327
328
|
</div>
|
|
329
|
+
<div id="targetRefreshStats" style="margin-bottom:16px"></div>
|
|
328
330
|
<div class="target-page-layout">
|
|
329
331
|
<div class="target-side-card">
|
|
330
332
|
<h3>🎯 目标商家国家分布</h3>
|
|
@@ -370,7 +372,7 @@
|
|
|
370
372
|
</div>
|
|
371
373
|
</div>
|
|
372
374
|
</div>
|
|
373
|
-
<div id="backToTop" title="
|
|
375
|
+
<div id="backToTop" title="回到顶部(可拖动)">↑</div>
|
|
374
376
|
</div>
|
|
375
377
|
<div id="tagsPage">
|
|
376
378
|
<div class="stats" style="margin-bottom:16px">
|
|
@@ -397,6 +399,7 @@
|
|
|
397
399
|
<div class="label">🔄 刷新</div>
|
|
398
400
|
</div>
|
|
399
401
|
</div>
|
|
402
|
+
<div id="tagsRefreshStats" style="margin-bottom:16px"></div>
|
|
400
403
|
<div class="tags-layout">
|
|
401
404
|
<div class="tags-table-card">
|
|
402
405
|
<h3>关键词列表</h3>
|
|
@@ -306,6 +306,10 @@ body {
|
|
|
306
306
|
font-weight: 600;
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
+
.target-badge-other {
|
|
310
|
+
color: #666 !important;
|
|
311
|
+
}
|
|
312
|
+
|
|
309
313
|
.source-row {
|
|
310
314
|
display: flex;
|
|
311
315
|
align-items: center;
|
|
@@ -869,6 +873,12 @@ td.user-id:hover {
|
|
|
869
873
|
display: flex;
|
|
870
874
|
}
|
|
871
875
|
|
|
876
|
+
#backToTop.dragging {
|
|
877
|
+
transition: none;
|
|
878
|
+
opacity: 0.9;
|
|
879
|
+
cursor: grabbing;
|
|
880
|
+
}
|
|
881
|
+
|
|
872
882
|
#mainPage.hidden {
|
|
873
883
|
display: none;
|
|
874
884
|
}
|
|
@@ -1483,6 +1493,26 @@ td.user-id:hover {
|
|
|
1483
1493
|
color: #ef4444;
|
|
1484
1494
|
}
|
|
1485
1495
|
|
|
1496
|
+
/* 待确认按钮 */
|
|
1497
|
+
.btn-pending {
|
|
1498
|
+
padding: 4px 10px;
|
|
1499
|
+
border: 1px solid #fbbf24;
|
|
1500
|
+
border-radius: 4px;
|
|
1501
|
+
background: transparent;
|
|
1502
|
+
color: #fbbf24;
|
|
1503
|
+
font-size: 11px;
|
|
1504
|
+
font-weight: 600;
|
|
1505
|
+
cursor: pointer;
|
|
1506
|
+
transition: all 0.15s;
|
|
1507
|
+
white-space: nowrap;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
.btn-pending:hover {
|
|
1511
|
+
background: rgba(251, 191, 36, 0.12);
|
|
1512
|
+
border-color: #f59e0b;
|
|
1513
|
+
color: #f59e0b;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1486
1516
|
/* ===== 关键词页面 ===== */
|
|
1487
1517
|
#tagsPage {
|
|
1488
1518
|
display: none;
|
|
@@ -1582,3 +1612,14 @@ td.user-id:hover {
|
|
|
1582
1612
|
.checkbox-label input[type="checkbox"] {
|
|
1583
1613
|
accent-color: #a78bfa;
|
|
1584
1614
|
}
|
|
1615
|
+
|
|
1616
|
+
/* ========== 烟花画布 ========== */
|
|
1617
|
+
#fireworkCanvas {
|
|
1618
|
+
position: fixed;
|
|
1619
|
+
top: 0;
|
|
1620
|
+
left: 0;
|
|
1621
|
+
width: 100%;
|
|
1622
|
+
height: 100%;
|
|
1623
|
+
pointer-events: none;
|
|
1624
|
+
z-index: 9999;
|
|
1625
|
+
}
|
package/src/watch/server.js
CHANGED
|
@@ -103,12 +103,115 @@ function inferClientType(routePath) {
|
|
|
103
103
|
return null;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
// ── 简易登录鉴权 ──
|
|
107
|
+
let WATCH_PASSWORD = "";
|
|
108
|
+
let PASSWORD_ENABLED = false;
|
|
109
|
+
const AUTH_COOKIE = "watch_token";
|
|
110
|
+
const AUTH_MAX_AGE = 86400; // 24 小时
|
|
111
|
+
const authSessions = new Map(); // token -> expiry (ms timestamp)
|
|
112
|
+
|
|
113
|
+
function todayPassword() {
|
|
114
|
+
const d = new Date();
|
|
115
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
116
|
+
return `tt@${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function generateToken() {
|
|
120
|
+
return Array.from({ length: 32 }, () =>
|
|
121
|
+
Math.floor(Math.random() * 36).toString(36),
|
|
122
|
+
).join("");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function parseAuthToken(req, params) {
|
|
126
|
+
// Cookie
|
|
127
|
+
const cookie = req.headers.cookie || "";
|
|
128
|
+
const cookieMatch = cookie.match(new RegExp(`${AUTH_COOKIE}=([^;]+)`));
|
|
129
|
+
if (cookieMatch) return cookieMatch[1];
|
|
130
|
+
// Header
|
|
131
|
+
const headerToken = req.headers["x-watch-token"];
|
|
132
|
+
if (headerToken) return headerToken;
|
|
133
|
+
// Query param
|
|
134
|
+
if (params.token) return params.token;
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function serveLoginPage(res, errorMsg) {
|
|
139
|
+
const err = errorMsg
|
|
140
|
+
? `<p style="color:#fe2c55;margin-top:12px">${errorMsg}</p>`
|
|
141
|
+
: "";
|
|
142
|
+
const html = `<!DOCTYPE html>
|
|
143
|
+
<html lang="zh-CN">
|
|
144
|
+
<head>
|
|
145
|
+
<meta charset="UTF-8">
|
|
146
|
+
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
147
|
+
<title>登录 - TikTok 采集监控</title>
|
|
148
|
+
<style>
|
|
149
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
150
|
+
body{background:#0d0d0d;color:#e0e0e0;font-family:-apple-system,BlinkMacSystemFont,sans-serif;display:flex;align-items:center;justify-content:center;height:100vh}
|
|
151
|
+
.box{background:#1e1e1e;border:1px solid #333;border-radius:12px;padding:40px;text-align:center;min-width:320px}
|
|
152
|
+
h1{font-size:20px;margin-bottom:4px}
|
|
153
|
+
p.sub{color:#888;font-size:13px;margin-bottom:20px}
|
|
154
|
+
input[type="password"]{width:100%;padding:12px;border:1px solid #444;border-radius:6px;background:#2a2a2a;color:#e0e0e0;font-size:14px;margin-bottom:12px;outline:none}
|
|
155
|
+
input[type="password"]:focus{border-color:#fe2c55}
|
|
156
|
+
.remember-row{display:flex;align-items:center;justify-content:center;gap:8px;margin-bottom:14px;font-size:13px;color:#aaa;cursor:pointer}
|
|
157
|
+
.remember-row input[type="checkbox"]{width:auto;margin:0;accent-color:#fe2c55;cursor:pointer}
|
|
158
|
+
.remember-row label{cursor:pointer;user-select:none}
|
|
159
|
+
button{width:100%;padding:12px;background:#fe2c55;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:15px;font-weight:600}
|
|
160
|
+
button:hover{opacity:.9}
|
|
161
|
+
</style>
|
|
162
|
+
</head>
|
|
163
|
+
<body>
|
|
164
|
+
<div class="box">
|
|
165
|
+
<h1>TikTok 采集监控</h1>
|
|
166
|
+
<p class="sub">请输入监控密码</p>
|
|
167
|
+
<form method="post" action="/api/login" id="login-form">
|
|
168
|
+
<input name="password" id="password" type="password" placeholder="密码" autocomplete="off" autofocus>
|
|
169
|
+
<div class="remember-row">
|
|
170
|
+
<input type="checkbox" id="remember-pwd">
|
|
171
|
+
<label for="remember-pwd">记住密码</label>
|
|
172
|
+
</div>
|
|
173
|
+
<button type="submit">登录</button>
|
|
174
|
+
</form>
|
|
175
|
+
${err}
|
|
176
|
+
</div>
|
|
177
|
+
<script>
|
|
178
|
+
(function() {
|
|
179
|
+
var pw = document.getElementById('password');
|
|
180
|
+
var cb = document.getElementById('remember-pwd');
|
|
181
|
+
var form = document.getElementById('login-form');
|
|
182
|
+
// 页面加载时恢复已保存的密码
|
|
183
|
+
try {
|
|
184
|
+
var saved = localStorage.getItem('watch_pwd');
|
|
185
|
+
if (saved) { pw.value = saved; cb.checked = true; }
|
|
186
|
+
} catch(e) {}
|
|
187
|
+
// 提交时保存或清除密码
|
|
188
|
+
form.addEventListener('submit', function() {
|
|
189
|
+
try {
|
|
190
|
+
if (cb.checked) {
|
|
191
|
+
localStorage.setItem('watch_pwd', pw.value);
|
|
192
|
+
} else {
|
|
193
|
+
localStorage.removeItem('watch_pwd');
|
|
194
|
+
}
|
|
195
|
+
} catch(e) {}
|
|
196
|
+
});
|
|
197
|
+
})();
|
|
198
|
+
</script>
|
|
199
|
+
</body>
|
|
200
|
+
</html>`;
|
|
201
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
202
|
+
res.end(html);
|
|
203
|
+
}
|
|
204
|
+
|
|
106
205
|
export function startWatchServer(
|
|
107
206
|
dataAnchor,
|
|
108
207
|
port = 3000,
|
|
109
208
|
existingStore,
|
|
110
209
|
options = {},
|
|
210
|
+
password = null,
|
|
111
211
|
) {
|
|
212
|
+
PASSWORD_ENABLED = true;
|
|
213
|
+
if (password) WATCH_PASSWORD = password;
|
|
214
|
+
// 不传 -P 时 WATCH_PASSWORD 保持空,登录时动态计算当日密码
|
|
112
215
|
return new Promise((_resolve, reject) => {
|
|
113
216
|
const store = existingStore || createStore(dataAnchor, options);
|
|
114
217
|
|
|
@@ -124,6 +227,12 @@ export function startWatchServer(
|
|
|
124
227
|
}
|
|
125
228
|
|
|
126
229
|
// ── 标签发现机制(定时 + 按需双保险) ──
|
|
230
|
+
// --no-llm-discover 或 TAG_DISCOVER_DISABLE=1 可关闭 LLM tag 发现
|
|
231
|
+
const DISCOVER_DISABLED =
|
|
232
|
+
options.llmDiscover === false ||
|
|
233
|
+
["1", "true", "yes"].includes(
|
|
234
|
+
(process.env.TAG_DISCOVER_DISABLE || "").toLowerCase(),
|
|
235
|
+
);
|
|
127
236
|
const DISCOVER_INTERVAL_MS = Math.max(
|
|
128
237
|
180000,
|
|
129
238
|
parseInt(process.env.TAG_DISCOVER_INTERVAL_MS || "300000", 10) || 300000,
|
|
@@ -151,6 +260,10 @@ export function startWatchServer(
|
|
|
151
260
|
);
|
|
152
261
|
|
|
153
262
|
async function runAutoDiscover(trigger = "timer") {
|
|
263
|
+
if (DISCOVER_DISABLED) {
|
|
264
|
+
console.error("[discover] LLM tag 发现已禁用 (TAG_DISCOVER_DISABLE=1)");
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
154
267
|
if (discoverRunning) return;
|
|
155
268
|
const elapsed = Date.now() - lastDiscoverTime;
|
|
156
269
|
if (elapsed < DISCOVER_COOLDOWN_MS) {
|
|
@@ -369,6 +482,30 @@ export function startWatchServer(
|
|
|
369
482
|
return;
|
|
370
483
|
}
|
|
371
484
|
|
|
485
|
+
// 将 jobs 中 status='done' 且无国家数据的记录移动到 jobs_base
|
|
486
|
+
if (
|
|
487
|
+
req.method === "POST" &&
|
|
488
|
+
routePath === "/api/jobs/move-no-country-to-base"
|
|
489
|
+
) {
|
|
490
|
+
try {
|
|
491
|
+
const result = store.moveNoCountryJobsToBase();
|
|
492
|
+
const ts = new Date().toISOString().slice(11, 19);
|
|
493
|
+
if (result.ok) {
|
|
494
|
+
console.error(
|
|
495
|
+
`[JOB ${ts}] MOVE-NO-COUNTRY-TO-BASE: moved=${result.moved}, total=${result.total}`,
|
|
496
|
+
);
|
|
497
|
+
} else {
|
|
498
|
+
console.error(
|
|
499
|
+
`[JOB ${ts}] MOVE-NO-COUNTRY-TO-BASE ERROR: ${result.error}`,
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
sendJSON(res, result.ok ? 200 : 400, result);
|
|
503
|
+
} catch (e) {
|
|
504
|
+
sendJSON(res, 500, { error: e.message });
|
|
505
|
+
}
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
|
|
372
509
|
// 将 jobs/raw_jobs 中 tt_seller=1 且 video_count=0 的记录移动到 jobs_base
|
|
373
510
|
if (
|
|
374
511
|
req.method === "POST" &&
|
|
@@ -691,6 +828,27 @@ export function startWatchServer(
|
|
|
691
828
|
return;
|
|
692
829
|
}
|
|
693
830
|
|
|
831
|
+
// 移到 jobs_base 重走流程(待确认)
|
|
832
|
+
const moveToBaseMatch = routePath.match(
|
|
833
|
+
/^\/api\/user-move-to-base\/([^/]+)$/,
|
|
834
|
+
);
|
|
835
|
+
if (req.method === "PUT" && moveToBaseMatch) {
|
|
836
|
+
const uniqueId = moveToBaseMatch[1];
|
|
837
|
+
try {
|
|
838
|
+
const ret = store.moveSingleJobToBase(uniqueId);
|
|
839
|
+
if (ret.error) {
|
|
840
|
+
sendJSON(res, 404, { error: ret.error });
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
const ts = new Date().toISOString().slice(11, 19);
|
|
844
|
+
console.error(`[JOB ${ts}] MOVE-TO-BASE: ${uniqueId} → jobs_base`);
|
|
845
|
+
sendJSON(res, 200, ret);
|
|
846
|
+
} catch (e) {
|
|
847
|
+
sendJSON(res, 400, { error: e.message });
|
|
848
|
+
}
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
|
|
694
852
|
if (req.method === "GET" && routePath === "/api/comment-tasks") {
|
|
695
853
|
const limit = parseInt(params.limit) || 1;
|
|
696
854
|
const locationsParam = params.locations || "";
|
|
@@ -913,6 +1071,14 @@ export function startWatchServer(
|
|
|
913
1071
|
return;
|
|
914
1072
|
}
|
|
915
1073
|
|
|
1074
|
+
// GET /api/targets/refresh-stats — 目标商家最近 N 小时刷新进度
|
|
1075
|
+
if (req.method === "GET" && routePath === "/api/targets/refresh-stats") {
|
|
1076
|
+
const hours = Math.max(1, parseInt(params.hours) || 24);
|
|
1077
|
+
const data = store.getTargetRefreshStats(hours);
|
|
1078
|
+
sendJSON(res, 200, data);
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
916
1082
|
if (req.method === "GET" && routePath === "/api/client-errors") {
|
|
917
1083
|
sendJSON(res, 200, { clients: store.getClientErrors() });
|
|
918
1084
|
return;
|
|
@@ -1286,6 +1452,14 @@ export function startWatchServer(
|
|
|
1286
1452
|
return;
|
|
1287
1453
|
}
|
|
1288
1454
|
|
|
1455
|
+
// GET /api/tags/refresh-stats — 最近 24h 每国刷新进度
|
|
1456
|
+
if (req.method === "GET" && routePath === "/api/tags/refresh-stats") {
|
|
1457
|
+
const hours = Math.max(1, parseInt(params.hours) || 24);
|
|
1458
|
+
const data = store.getRefreshStats(hours);
|
|
1459
|
+
sendJSON(res, 200, data);
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1289
1463
|
// GET /api/tags?status=&country=&limit=&offset=
|
|
1290
1464
|
if (req.method === "GET" && routePath === "/api/tags") {
|
|
1291
1465
|
const status = params.status || null;
|
|
@@ -1907,12 +2081,77 @@ export function startWatchServer(
|
|
|
1907
2081
|
return;
|
|
1908
2082
|
}
|
|
1909
2083
|
|
|
1910
|
-
//
|
|
2084
|
+
// POST /api/login — HTML 表单提交(密码正确则设 Cookie 跳转,错误则显示错误页)
|
|
2085
|
+
// 同时也支持 JSON 请求
|
|
2086
|
+
if (req.method === "POST" && routePath === "/api/login") {
|
|
2087
|
+
try {
|
|
2088
|
+
const rawBody = await new Promise((resolve) => {
|
|
2089
|
+
let body = "";
|
|
2090
|
+
req.on("data", (chunk) => (body += chunk));
|
|
2091
|
+
req.on("end", () => resolve(body));
|
|
2092
|
+
});
|
|
2093
|
+
const isForm = req.headers["content-type"]?.includes(
|
|
2094
|
+
"x-www-form-urlencoded",
|
|
2095
|
+
);
|
|
2096
|
+
const pwd = isForm
|
|
2097
|
+
? new URLSearchParams(rawBody).get("password") || ""
|
|
2098
|
+
: (() => {
|
|
2099
|
+
try {
|
|
2100
|
+
return JSON.parse(rawBody).password || "";
|
|
2101
|
+
} catch {
|
|
2102
|
+
return "";
|
|
2103
|
+
}
|
|
2104
|
+
})();
|
|
2105
|
+
|
|
2106
|
+
const expectedPassword = WATCH_PASSWORD || todayPassword();
|
|
2107
|
+
if (pwd === expectedPassword) {
|
|
2108
|
+
const token = generateToken();
|
|
2109
|
+
authSessions.set(token, Date.now() + AUTH_MAX_AGE * 1000);
|
|
2110
|
+
if (isForm) {
|
|
2111
|
+
res.writeHead(302, {
|
|
2112
|
+
Location: "/",
|
|
2113
|
+
"Set-Cookie": `${AUTH_COOKIE}=${token}; Path=/; HttpOnly; Max-Age=${AUTH_MAX_AGE}; SameSite=Lax`,
|
|
2114
|
+
});
|
|
2115
|
+
res.end();
|
|
2116
|
+
} else {
|
|
2117
|
+
res.writeHead(200, {
|
|
2118
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
2119
|
+
"Set-Cookie": `${AUTH_COOKIE}=${token}; Path=/; HttpOnly; Max-Age=${AUTH_MAX_AGE}; SameSite=Lax`,
|
|
2120
|
+
});
|
|
2121
|
+
res.end(JSON.stringify({ ok: true }));
|
|
2122
|
+
}
|
|
2123
|
+
} else {
|
|
2124
|
+
if (isForm) {
|
|
2125
|
+
serveLoginPage(res, "密码错误");
|
|
2126
|
+
} else {
|
|
2127
|
+
sendJSON(res, 401, { error: "密码错误" });
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
} catch (e) {
|
|
2131
|
+
sendJSON(res, 400, { error: e.message });
|
|
2132
|
+
}
|
|
2133
|
+
return;
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
// ====== 静态资源(HTML 页面需登录) ======
|
|
1911
2137
|
|
|
1912
2138
|
if (
|
|
1913
2139
|
req.method === "GET" &&
|
|
1914
2140
|
(routePath === "/" || routePath === "/index.html")
|
|
1915
2141
|
) {
|
|
2142
|
+
// 设有密码时,检查登录态
|
|
2143
|
+
if (PASSWORD_ENABLED) {
|
|
2144
|
+
const token = parseAuthToken(req, params);
|
|
2145
|
+
const valid =
|
|
2146
|
+
token &&
|
|
2147
|
+
authSessions.has(token) &&
|
|
2148
|
+
authSessions.get(token) > Date.now();
|
|
2149
|
+
if (!valid) {
|
|
2150
|
+
serveLoginPage(res);
|
|
2151
|
+
return;
|
|
2152
|
+
}
|
|
2153
|
+
authSessions.set(token, Date.now() + AUTH_MAX_AGE * 1000);
|
|
2154
|
+
}
|
|
1916
2155
|
const html = readFileSync(join(publicDir, "index.html"), "utf-8");
|
|
1917
2156
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1918
2157
|
res.end(html);
|
|
@@ -1997,13 +2236,40 @@ export function startWatchServer(
|
|
|
1997
2236
|
}
|
|
1998
2237
|
|
|
1999
2238
|
// 启动后 30 秒首次发现(给服务启动缓冲),之后定时执行
|
|
2000
|
-
|
|
2001
|
-
(
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2239
|
+
if (DISCOVER_DISABLED) {
|
|
2240
|
+
console.error(
|
|
2241
|
+
`[discover] LLM tag 发现已禁用 (TAG_DISCOVER_DISABLE=1),不会启动定时发现`,
|
|
2242
|
+
);
|
|
2243
|
+
} else {
|
|
2244
|
+
const discoverTimer = setInterval(
|
|
2245
|
+
() => runAutoDiscover("timer"),
|
|
2246
|
+
DISCOVER_INTERVAL_MS,
|
|
2247
|
+
);
|
|
2248
|
+
setTimeout(() => runAutoDiscover("startup"), 30000);
|
|
2249
|
+
console.error(
|
|
2250
|
+
`[discover] 定时+按需发现已启动: 定时间隔 ${DISCOVER_INTERVAL_MS / 60000} 分钟, 每国 ${DISCOVER_COUNT_PER_COUNTRY} 个标签, 每国不足阈值 ${DISCOVER_PER_COUNTRY_THRESHOLD}, 按需触发阈值 ${DISCOVER_LOW_THRESHOLD}, 冷却期 ${DISCOVER_COOLDOWN_MS / 1000}s`,
|
|
2251
|
+
);
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
// 每 2 小时自动将无国家数据的 done job 移入 jobs_base 重走流程
|
|
2255
|
+
const NO_COUNTRY_MOVE_INTERVAL_MS = 2 * 60 * 60 * 1000;
|
|
2256
|
+
setInterval(() => {
|
|
2257
|
+
try {
|
|
2258
|
+
const result = store.moveNoCountryJobsToBase();
|
|
2259
|
+
const ts = new Date().toISOString().slice(11, 19);
|
|
2260
|
+
if (result.moved > 0) {
|
|
2261
|
+
console.error(
|
|
2262
|
+
`[JOB ${ts}] AUTO-MOVE-NO-COUNTRY: moved=${result.moved}, total=${result.total}`,
|
|
2263
|
+
);
|
|
2264
|
+
}
|
|
2265
|
+
} catch (e) {
|
|
2266
|
+
console.error(
|
|
2267
|
+
`[JOB] AUTO-MOVE-NO-COUNTRY 定时任务异常: ${e.message}`,
|
|
2268
|
+
);
|
|
2269
|
+
}
|
|
2270
|
+
}, NO_COUNTRY_MOVE_INTERVAL_MS);
|
|
2005
2271
|
console.error(
|
|
2006
|
-
`[
|
|
2272
|
+
`[job] 无国家数据自动回炉已启动: 间隔 ${NO_COUNTRY_MOVE_INTERVAL_MS / 3600000} 小时`,
|
|
2007
2273
|
);
|
|
2008
2274
|
|
|
2009
2275
|
_resolve({ server, port });
|