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.
@@ -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>
@@ -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
- const total = stats ? stats.total : tags.length;
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