tt-help-cli-ycl 1.4.2 → 1.4.4
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/tag.js +131 -68
- package/src/lib/args.js +0 -4
- package/src/lib/constants.js +2 -1
- package/src/lib/tag-discover.js +218 -10
- package/src/watch/data-store.js +42 -0
- package/src/watch/db-schema.js +19 -0
- package/src/watch/db-tags.js +749 -0
- package/src/watch/public/app.js +27 -0
- package/src/watch/public/index.html +1 -1
- package/src/watch/server.js +255 -2
- package/src/watch/tag-service.js +509 -23
package/src/watch/public/app.js
CHANGED
|
@@ -991,6 +991,33 @@ async function resetJob(uniqueId) {
|
|
|
991
991
|
}
|
|
992
992
|
}
|
|
993
993
|
|
|
994
|
+
async function moveRestrictedToBase() {
|
|
995
|
+
const count = currentStats?.restrictedUsers || 0;
|
|
996
|
+
if (count === 0) {
|
|
997
|
+
showToast("没有受限用户");
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
if (!confirm(`确定将 ${count} 个受限用户移到 jobs_base 吗?`)) return;
|
|
1001
|
+
showLoading("迁移中...");
|
|
1002
|
+
try {
|
|
1003
|
+
const res = await fetch("/api/jobs/move-restricted-to-base", {
|
|
1004
|
+
method: "POST",
|
|
1005
|
+
});
|
|
1006
|
+
const data = await res.json();
|
|
1007
|
+
if (data.error) {
|
|
1008
|
+
showToast(data.error, true);
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
showToast(`已迁移 ${data.moved} 个受限用户`);
|
|
1012
|
+
fetchStats();
|
|
1013
|
+
fetchUsers();
|
|
1014
|
+
} catch (e) {
|
|
1015
|
+
showToast("迁移失败: " + e.message, true);
|
|
1016
|
+
} finally {
|
|
1017
|
+
hideLoading();
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
994
1021
|
async function batchResetErrors() {
|
|
995
1022
|
const errorUsers = currentUsers.filter((u) => u.status === "error");
|
|
996
1023
|
if (errorUsers.length === 0) {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
<div class="label">错误</div>
|
|
42
42
|
<div class="value error" id="statError">0</div>
|
|
43
43
|
</div>
|
|
44
|
-
<div class="stat-card">
|
|
44
|
+
<div class="stat-card clickable" onclick="moveRestrictedToBase()">
|
|
45
45
|
<div class="label">受限</div>
|
|
46
46
|
<div class="value error" id="statRestricted">0</div>
|
|
47
47
|
</div>
|
package/src/watch/server.js
CHANGED
|
@@ -122,6 +122,93 @@ export function startWatchServer(
|
|
|
122
122
|
console.error(`[JOB ${ts}] ${action}${d}`);
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
// ── 标签发现机制(定时 + 按需双保险) ──
|
|
126
|
+
const DISCOVER_INTERVAL_MS = Math.max(
|
|
127
|
+
180000,
|
|
128
|
+
parseInt(process.env.TAG_DISCOVER_INTERVAL_MS || "300000", 10) || 300000,
|
|
129
|
+
);
|
|
130
|
+
const DISCOVER_COUNT_PER_COUNTRY = Math.max(
|
|
131
|
+
2,
|
|
132
|
+
parseInt(process.env.TAG_DISCOVER_COUNT || "6", 10) || 6,
|
|
133
|
+
);
|
|
134
|
+
// 按需触发阈值:new 标签总数低于此值时立即触发发现
|
|
135
|
+
const DISCOVER_LOW_THRESHOLD =
|
|
136
|
+
parseInt(process.env.TAG_DISCOVER_LOW_THRESHOLD || "5", 10) || 5;
|
|
137
|
+
// 冷却期:两次发现之间最小间隔(防止频繁触发 LLM)
|
|
138
|
+
const DISCOVER_COOLDOWN_MS = Math.max(
|
|
139
|
+
60000,
|
|
140
|
+
parseInt(process.env.TAG_DISCOVER_COOLDOWN_MS || "120000", 10) || 120000,
|
|
141
|
+
);
|
|
142
|
+
let discoverRunning = false;
|
|
143
|
+
let lastDiscoverTime = 0;
|
|
144
|
+
|
|
145
|
+
// 标签池充足阈值:new 标签总数超过此值时,定时触发直接跳过(不浪费 LLM token)
|
|
146
|
+
const DISCOVER_SUFFICIENT_TOTAL =
|
|
147
|
+
parseInt(process.env.TAG_DISCOVER_SUFFICIENT || "30", 10) || 30;
|
|
148
|
+
|
|
149
|
+
async function runAutoDiscover(trigger = "timer") {
|
|
150
|
+
if (discoverRunning) return;
|
|
151
|
+
const elapsed = Date.now() - lastDiscoverTime;
|
|
152
|
+
if (elapsed < DISCOVER_COOLDOWN_MS) {
|
|
153
|
+
const remainSec = Math.ceil((DISCOVER_COOLDOWN_MS - elapsed) / 1000);
|
|
154
|
+
console.error(
|
|
155
|
+
`[discover] 冷却中(还需 ${remainSec}s),跳过本次触发 (${trigger})`,
|
|
156
|
+
);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
// 先看全局总量:标签池充足时直接跳过,不浪费 LLM token
|
|
160
|
+
const globalStats = store.getTagStats();
|
|
161
|
+
const totalNew = globalStats?.new || 0;
|
|
162
|
+
if (totalNew >= DISCOVER_SUFFICIENT_TOTAL) {
|
|
163
|
+
console.error(
|
|
164
|
+
`[discover] 标签池充足 (${totalNew} 个 new ≥ ${DISCOVER_SUFFICIENT_TOTAL}),跳过 [触发: ${trigger}]`,
|
|
165
|
+
);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
discoverRunning = true;
|
|
169
|
+
try {
|
|
170
|
+
// 只为 new 标签不足的国家发现
|
|
171
|
+
const countriesToDiscover = [];
|
|
172
|
+
const { getTagsByStatus } = await import("./db-tags.js");
|
|
173
|
+
for (const country of DEFAULT_TARGET_LOCATIONS) {
|
|
174
|
+
const newTags = getTagsByStatus("new", 100, 0, country);
|
|
175
|
+
if (newTags.length < 10) {
|
|
176
|
+
countriesToDiscover.push(country);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (countriesToDiscover.length === 0) {
|
|
180
|
+
console.error("[discover] 所有国家标签充足,跳过本轮");
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
console.error(
|
|
184
|
+
`[discover] 开始为 ${countriesToDiscover.length} 个国家发现标签 (${countriesToDiscover.join(",")}) [触发: ${trigger}]...`,
|
|
185
|
+
);
|
|
186
|
+
const { discoverTagsForCountry } = await import("./tag-service.js");
|
|
187
|
+
let totalAdded = 0;
|
|
188
|
+
for (const country of countriesToDiscover) {
|
|
189
|
+
try {
|
|
190
|
+
const result = await discoverTagsForCountry(
|
|
191
|
+
store,
|
|
192
|
+
country,
|
|
193
|
+
DISCOVER_COUNT_PER_COUNTRY,
|
|
194
|
+
);
|
|
195
|
+
totalAdded += result.added || 0;
|
|
196
|
+
console.error(
|
|
197
|
+
`[discover] ${country}: 新增 ${result.added || 0} 个标签`,
|
|
198
|
+
);
|
|
199
|
+
} catch (e) {
|
|
200
|
+
console.error(`[discover] ${country}: 失败 - ${e.message}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
console.error(`[discover] 本轮完成,共新增 ${totalAdded} 个标签`);
|
|
204
|
+
lastDiscoverTime = Date.now();
|
|
205
|
+
} catch (e) {
|
|
206
|
+
console.error(`[discover] 异常: ${e.message}`);
|
|
207
|
+
} finally {
|
|
208
|
+
discoverRunning = false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
125
212
|
const server = http.createServer(async (req, res) => {
|
|
126
213
|
const { path: routePath, params } = parseQuery(req.url);
|
|
127
214
|
|
|
@@ -274,6 +361,19 @@ export function startWatchServer(
|
|
|
274
361
|
return;
|
|
275
362
|
}
|
|
276
363
|
|
|
364
|
+
if (
|
|
365
|
+
req.method === "POST" &&
|
|
366
|
+
routePath === "/api/jobs/move-restricted-to-base"
|
|
367
|
+
) {
|
|
368
|
+
try {
|
|
369
|
+
const result = store.moveRestrictedJobsToBase();
|
|
370
|
+
sendJSON(res, result.ok ? 200 : 400, result);
|
|
371
|
+
} catch (e) {
|
|
372
|
+
sendJSON(res, 500, { error: e.message });
|
|
373
|
+
}
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
277
377
|
// 将 jobs/raw_jobs 中 tt_seller=1 且 video_count=0 的记录移动到 jobs_base
|
|
278
378
|
if (
|
|
279
379
|
req.method === "POST" &&
|
|
@@ -1169,9 +1269,28 @@ export function startWatchServer(
|
|
|
1169
1269
|
tags = store.getAllTags(limit, offset, upperCountry);
|
|
1170
1270
|
}
|
|
1171
1271
|
|
|
1172
|
-
//
|
|
1272
|
+
// 获取总数(带国家过滤,按 status 返回对应计数)
|
|
1173
1273
|
const stats = store.getTagStats(upperCountry);
|
|
1174
|
-
|
|
1274
|
+
let total;
|
|
1275
|
+
if (stats) {
|
|
1276
|
+
if (status === "new") total = stats.new;
|
|
1277
|
+
else if (status === "productive") total = stats.productive;
|
|
1278
|
+
else if (status === "dead") total = stats.dead;
|
|
1279
|
+
else if (status === "scoring") total = stats.scoring;
|
|
1280
|
+
else total = stats.total;
|
|
1281
|
+
} else {
|
|
1282
|
+
total = tags.length;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
// 按需发现:客户端取 new 标签时,如果池子低于阈值,立即触发发现
|
|
1286
|
+
if (
|
|
1287
|
+
status === "new" &&
|
|
1288
|
+
total < DISCOVER_LOW_THRESHOLD &&
|
|
1289
|
+
!discoverRunning
|
|
1290
|
+
) {
|
|
1291
|
+
runAutoDiscover("low-pool"); // 异步触发,不阻塞响应
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1175
1294
|
sendJSON(res, 200, { tags, total, offset, limit });
|
|
1176
1295
|
return;
|
|
1177
1296
|
}
|
|
@@ -1199,6 +1318,24 @@ export function startWatchServer(
|
|
|
1199
1318
|
return;
|
|
1200
1319
|
}
|
|
1201
1320
|
|
|
1321
|
+
// GET /api/tags/recent-generated?country=ES&lookback=10 — 最近N轮生成的标签(去重)
|
|
1322
|
+
if (req.method === "GET" && routePath === "/api/tags/recent-generated") {
|
|
1323
|
+
try {
|
|
1324
|
+
const { getRecentGeneratedTags } = await import("./db-tags.js");
|
|
1325
|
+
const country = params.country;
|
|
1326
|
+
const lookback = parseInt(params.lookback) || 10;
|
|
1327
|
+
if (!country) {
|
|
1328
|
+
sendJSON(res, 400, { error: "country 参数必填" });
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
const tags = getRecentGeneratedTags(country, lookback);
|
|
1332
|
+
sendJSON(res, 200, { tags, total: tags.length });
|
|
1333
|
+
} catch (e) {
|
|
1334
|
+
sendJSON(res, 500, { error: e.message });
|
|
1335
|
+
}
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1202
1339
|
// POST /api/tags/batch-add — 批量添加关键词
|
|
1203
1340
|
if (req.method === "POST" && routePath === "/api/tags/batch-add") {
|
|
1204
1341
|
try {
|
|
@@ -1226,6 +1363,112 @@ export function startWatchServer(
|
|
|
1226
1363
|
return;
|
|
1227
1364
|
}
|
|
1228
1365
|
|
|
1366
|
+
// GET /api/tags/health?country=ES — 自闭环:获取国家 tag 健康指标
|
|
1367
|
+
if (req.method === "GET" && routePath === "/api/tags/health") {
|
|
1368
|
+
try {
|
|
1369
|
+
const { getCountryHealth } = await import("./db-tags.js");
|
|
1370
|
+
const country = params.country;
|
|
1371
|
+
if (!country) {
|
|
1372
|
+
sendJSON(res, 400, { error: "country 参数必填" });
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
const health = getCountryHealth(country);
|
|
1376
|
+
if (!health) {
|
|
1377
|
+
sendJSON(res, 404, { error: "未找到该国家的数据" });
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
sendJSON(res, 200, health);
|
|
1381
|
+
} catch (e) {
|
|
1382
|
+
sendJSON(res, 500, { error: e.message });
|
|
1383
|
+
}
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// POST /api/tags/cleanup — 自闭环:触发数据清理
|
|
1388
|
+
if (req.method === "POST" && routePath === "/api/tags/cleanup") {
|
|
1389
|
+
try {
|
|
1390
|
+
const {
|
|
1391
|
+
cleanupLowScoredTags,
|
|
1392
|
+
cleanupStaleScoringTags,
|
|
1393
|
+
cleanupErrorTags,
|
|
1394
|
+
} = await import("./db-tags.js");
|
|
1395
|
+
const body = await readBody(req);
|
|
1396
|
+
const actions = body || {};
|
|
1397
|
+
const result = {};
|
|
1398
|
+
|
|
1399
|
+
if (actions.staleScoringReset) {
|
|
1400
|
+
const r = cleanupStaleScoringTags();
|
|
1401
|
+
result.staleScoringReset = r.reset;
|
|
1402
|
+
}
|
|
1403
|
+
if (actions.errorReset) {
|
|
1404
|
+
const r = cleanupErrorTags();
|
|
1405
|
+
result.errorReset = r.reset;
|
|
1406
|
+
}
|
|
1407
|
+
if (actions.lowScoredCleanup) {
|
|
1408
|
+
const r = cleanupLowScoredTags(20);
|
|
1409
|
+
result.lowScoredCleanup = r.cleaned;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
sendJSON(res, 200, { ok: true, ...result });
|
|
1413
|
+
} catch (e) {
|
|
1414
|
+
sendJSON(res, 500, { error: e.message });
|
|
1415
|
+
}
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// POST /api/tags/reflect?country=DE — 自闭环:手动触发反思
|
|
1420
|
+
if (req.method === "POST" && routePath === "/api/tags/reflect") {
|
|
1421
|
+
try {
|
|
1422
|
+
const { reflectAndLearn } = await import("./tag-service.js");
|
|
1423
|
+
const country = params.country;
|
|
1424
|
+
if (!country) {
|
|
1425
|
+
sendJSON(res, 400, { error: "country 参数必填" });
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
const result = await reflectAndLearn(store, country);
|
|
1429
|
+
sendJSON(res, 200, result);
|
|
1430
|
+
} catch (e) {
|
|
1431
|
+
sendJSON(res, 500, { error: e.message });
|
|
1432
|
+
}
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
// GET /api/tags/rules?country=DE — 查看当前活跃的反思规则
|
|
1437
|
+
if (req.method === "GET" && routePath === "/api/tags/rules") {
|
|
1438
|
+
try {
|
|
1439
|
+
const { getActiveRules, shouldTriggerReflection } =
|
|
1440
|
+
await import("./db-tags.js");
|
|
1441
|
+
const country = params.country;
|
|
1442
|
+
if (!country) {
|
|
1443
|
+
sendJSON(res, 400, { error: "country 参数必填" });
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
const rules = getActiveRules(country);
|
|
1447
|
+
const triggerCheck = shouldTriggerReflection(country);
|
|
1448
|
+
sendJSON(res, 200, { country, rules, triggerCheck });
|
|
1449
|
+
} catch (e) {
|
|
1450
|
+
sendJSON(res, 500, { error: e.message });
|
|
1451
|
+
}
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// GET /api/tags/reflection-data?country=DE — 查看反思所需的全量数据
|
|
1456
|
+
if (req.method === "GET" && routePath === "/api/tags/reflection-data") {
|
|
1457
|
+
try {
|
|
1458
|
+
const { getReflectionData } = await import("./db-tags.js");
|
|
1459
|
+
const country = params.country;
|
|
1460
|
+
if (!country) {
|
|
1461
|
+
sendJSON(res, 400, { error: "country 参数必填" });
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
const data = getReflectionData(country);
|
|
1465
|
+
sendJSON(res, 200, data);
|
|
1466
|
+
} catch (e) {
|
|
1467
|
+
sendJSON(res, 500, { error: e.message });
|
|
1468
|
+
}
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1229
1472
|
// POST /api/tags/productive — CLI 模式上报 productive tag
|
|
1230
1473
|
if (req.method === "POST" && routePath === "/api/tags/productive") {
|
|
1231
1474
|
try {
|
|
@@ -1335,6 +1578,16 @@ export function startWatchServer(
|
|
|
1335
1578
|
console.error(`[启动] 清理 scoring 标签失败: ${e.message}`);
|
|
1336
1579
|
}
|
|
1337
1580
|
|
|
1581
|
+
// 启动后 30 秒首次发现(给服务启动缓冲),之后定时执行
|
|
1582
|
+
const discoverTimer = setInterval(
|
|
1583
|
+
() => runAutoDiscover("timer"),
|
|
1584
|
+
DISCOVER_INTERVAL_MS,
|
|
1585
|
+
);
|
|
1586
|
+
setTimeout(() => runAutoDiscover("startup"), 30000);
|
|
1587
|
+
console.error(
|
|
1588
|
+
`[discover] 定时+按需发现已启动: 定时间隔 ${DISCOVER_INTERVAL_MS / 60000} 分钟, 每国 ${DISCOVER_COUNT_PER_COUNTRY} 个标签, 按需阈值 ${DISCOVER_LOW_THRESHOLD}, 冷却期 ${DISCOVER_COOLDOWN_MS / 1000}s`,
|
|
1589
|
+
);
|
|
1590
|
+
|
|
1338
1591
|
_resolve({ server, port });
|
|
1339
1592
|
});
|
|
1340
1593
|
|