tt-help-cli-ycl 1.4.12 → 1.4.14

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.
@@ -328,6 +328,11 @@
328
328
  <span style="color:#666;font-size:12px">加载中...</span>
329
329
  </div>
330
330
  <div class="muted-tip">点击国家卡片可筛选列表。</div>
331
+ <div class="target-other-section" id="targetOtherSection" style="display:none">
332
+ <h3 style="color:#f59e0b;margin-top:20px;margin-bottom:12px">🌍 其他国家 (已修改)</h3>
333
+ <div class="pending-country-grid" id="targetOtherGrid"></div>
334
+ <div class="muted-tip">这些商家已被手动修改到非目标国家。</div>
335
+ </div>
331
336
  </div>
332
337
  <div class="target-table-card">
333
338
  <h3>目标商家列表</h3>
@@ -361,6 +366,7 @@
361
366
  </div>
362
367
  </div>
363
368
  </div>
369
+ <div id="backToTop" title="回到顶部" onclick="scrollToTop()">↑</div>
364
370
  </div>
365
371
  <div id="tagsPage">
366
372
  <div class="stats" style="margin-bottom:16px">
@@ -415,7 +421,7 @@
415
421
  <th style="min-width:120px">关键词</th>
416
422
  <th class="sortable-tag" data-sort="score">评分 <span class="sort-icon">↕</span></th>
417
423
  <th class="sortable-tag" data-sort="author_count">作者数 <span class="sort-icon">↕</span></th>
418
- <th class="sortable-tag" data-sort="total_posts">帖子数 <span class="sort-icon">↕</span></th>
424
+ <th class="sortable-tag" data-sort="refreshed_at">最近刷新 <span class="sort-icon">↕</span></th>
419
425
  <th class="sortable-tag" data-sort="matched_authors">匹配作者 <span class="sort-icon">↕</span></th>
420
426
  <th class="sortable-tag" data-sort="pushed_users">推送用户 <span class="sort-icon">↕</span></th>
421
427
  <th>国家</th>
@@ -839,6 +839,36 @@ td.user-id:hover {
839
839
  display: block;
840
840
  }
841
841
 
842
+ #backToTop {
843
+ position: fixed;
844
+ bottom: 32px;
845
+ right: 32px;
846
+ width: 44px;
847
+ height: 44px;
848
+ border-radius: 50%;
849
+ background: rgba(254, 44, 85, 0.85);
850
+ color: #fff;
851
+ font-size: 20px;
852
+ display: none;
853
+ align-items: center;
854
+ justify-content: center;
855
+ cursor: pointer;
856
+ z-index: 999;
857
+ box-shadow: 0 4px 16px rgba(254, 44, 85, 0.35);
858
+ transition: opacity 0.25s, transform 0.25s, background 0.2s;
859
+ user-select: none;
860
+ }
861
+
862
+ #backToTop:hover {
863
+ background: #e61944;
864
+ transform: translateY(-2px);
865
+ box-shadow: 0 6px 20px rgba(254, 44, 85, 0.5);
866
+ }
867
+
868
+ #backToTop.visible {
869
+ display: flex;
870
+ }
871
+
842
872
  #mainPage.hidden {
843
873
  display: none;
844
874
  }
@@ -1004,6 +1034,19 @@ td.user-id:hover {
1004
1034
  color: #ddd6fe;
1005
1035
  }
1006
1036
 
1037
+ .other-country-item {
1038
+ border-color: #f59e0b;
1039
+ }
1040
+
1041
+ .other-country-item .country-name {
1042
+ color: #f59e0b;
1043
+ }
1044
+
1045
+ .other-country-item:hover {
1046
+ border-color: #fbbf24;
1047
+ background: #3a3520;
1048
+ }
1049
+
1007
1050
  .back-btn {
1008
1051
  padding: 6px 14px;
1009
1052
  border: 1px solid #333;
@@ -93,6 +93,7 @@ function inferClientType(routePath) {
93
93
  if (routePath.startsWith("/api/redo-job")) return "refresh";
94
94
  if (routePath.startsWith("/api/user-update-tasks")) return "attach";
95
95
  if (routePath.startsWith("/api/comment-task")) return "comments";
96
+ if (routePath.startsWith("/api/tags/refresh")) return "tag-refresh";
96
97
  if (routePath.startsWith("/api/tags")) return "scoring";
97
98
  if (
98
99
  routePath.startsWith("/api/job") ||
@@ -142,9 +143,12 @@ export function startWatchServer(
142
143
  let discoverRunning = false;
143
144
  let lastDiscoverTime = 0;
144
145
 
145
- // 标签池充足阈值:new 标签总数超过此值时,定时触发直接跳过(不浪费 LLM token)
146
- const DISCOVER_SUFFICIENT_TOTAL =
147
- parseInt(process.env.TAG_DISCOVER_SUFFICIENT || "30", 10) || 30;
146
+ // 每国标签不足阈值:某国 new 标签低于此值时触发 discover
147
+ const DISCOVER_PER_COUNTRY_THRESHOLD = Math.max(
148
+ 3,
149
+ parseInt(process.env.TAG_DISCOVER_PER_COUNTRY_THRESHOLD || "10", 10) ||
150
+ 10,
151
+ );
148
152
 
149
153
  async function runAutoDiscover(trigger = "timer") {
150
154
  if (discoverRunning) return;
@@ -156,23 +160,14 @@ export function startWatchServer(
156
160
  );
157
161
  return;
158
162
  }
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
163
  discoverRunning = true;
169
164
  try {
170
- // 只为 new 标签不足的国家发现
165
+ // 按国家维度判断:只为 new 标签不足的国家发现
171
166
  const countriesToDiscover = [];
172
167
  const { getTagsByStatus } = await import("./db-tags.js");
173
168
  for (const country of DEFAULT_TARGET_LOCATIONS) {
174
169
  const newTags = getTagsByStatus("new", 100, 0, country);
175
- if (newTags.length < 10) {
170
+ if (newTags.length < DISCOVER_PER_COUNTRY_THRESHOLD) {
176
171
  countriesToDiscover.push(country);
177
172
  }
178
173
  }
@@ -683,9 +678,18 @@ export function startWatchServer(
683
678
 
684
679
  if (req.method === "GET" && routePath === "/api/comment-tasks") {
685
680
  const limit = parseInt(params.limit) || 1;
686
- const tasks = store.getPendingCommentTasks(limit);
681
+ const locationsParam = params.locations || "";
682
+ const locations = locationsParam
683
+ ? locationsParam
684
+ .split(",")
685
+ .map((s) => s.trim().toUpperCase())
686
+ .filter(Boolean)
687
+ : null;
688
+ const tasks = store.getPendingCommentTasks(limit, locations);
687
689
  const ts = new Date().toISOString().slice(11, 19);
688
- console.error(`[JOB ${ts}] COMMENT-TASKS: ${tasks.length} tasks`);
690
+ console.error(
691
+ `[JOB ${ts}] COMMENT-TASKS: ${tasks.length} tasks${locations ? " (locations: " + locations.join(",") + ")" : ""}`,
692
+ );
689
693
  sendJSON(res, 200, { total: tasks.length, tasks });
690
694
  return;
691
695
  }
@@ -851,12 +855,13 @@ export function startWatchServer(
851
855
  offset: parseInt(params.offset || "0", 10),
852
856
  },
853
857
  );
854
- // 确保每个用户对象包含 topVideoPlayCount 和 topVideoHref
858
+ // 确保每个用户对象包含 topVideoPlayCount、topVideoHref 和 topVideoIsAd
855
859
  if (result.users && Array.isArray(result.users)) {
856
860
  result.users = result.users.map((u) => ({
857
861
  ...u,
858
862
  topVideoPlayCount: u.topVideoPlayCount || null,
859
863
  topVideoHref: u.topVideoHref || null,
864
+ topVideoIsAd: u.topVideoIsAd ?? null,
860
865
  }));
861
866
  }
862
867
  sendJSON(res, 200, result);
@@ -865,7 +870,7 @@ export function startWatchServer(
865
870
 
866
871
  // 默认:全量(兼容旧调用)
867
872
  const result = store.getTargetUsersByCountry(DEFAULT_TARGET_LOCATIONS);
868
- // 确保每个用户对象包含 topVideoPlayCount 和 topVideoHref
873
+ // 确保每个用户对象包含 topVideoPlayCount、topVideoHref 和 topVideoIsAd
869
874
  if (result.countries && Array.isArray(result.countries)) {
870
875
  for (const country of result.countries) {
871
876
  if (country.users && Array.isArray(country.users)) {
@@ -873,6 +878,7 @@ export function startWatchServer(
873
878
  ...u,
874
879
  topVideoPlayCount: u.topVideoPlayCount || null,
875
880
  topVideoHref: u.topVideoHref || null,
881
+ topVideoIsAd: u.topVideoIsAd ?? null,
876
882
  }));
877
883
  }
878
884
  }
@@ -881,6 +887,17 @@ export function startWatchServer(
881
887
  return;
882
888
  }
883
889
 
890
+ if (
891
+ req.method === "GET" &&
892
+ routePath === "/api/target-users-other-countries"
893
+ ) {
894
+ const result = store.getModifiedTargetUsersOutsideLocations(
895
+ DEFAULT_TARGET_LOCATIONS,
896
+ );
897
+ sendJSON(res, 200, result);
898
+ return;
899
+ }
900
+
884
901
  if (req.method === "GET" && routePath === "/api/client-errors") {
885
902
  sendJSON(res, 200, { clients: store.getClientErrors() });
886
903
  return;
@@ -1318,6 +1335,71 @@ export function startWatchServer(
1318
1335
  return;
1319
1336
  }
1320
1337
 
1338
+ // POST /api/bulk-author-locations — 批量查询作者国家(用于 tag 客户端 enrich 前跳过已知数据)
1339
+ if (req.method === "POST" && routePath === "/api/bulk-author-locations") {
1340
+ try {
1341
+ const body = await readBody(req);
1342
+ const uniqueIds = Array.isArray(body.uniqueIds) ? body.uniqueIds : [];
1343
+ const locations = store.getBulkAuthorLocations(uniqueIds);
1344
+ sendJSON(res, 200, { locations });
1345
+ } catch (e) {
1346
+ sendJSON(res, 500, { error: e.message });
1347
+ }
1348
+ return;
1349
+ }
1350
+
1351
+ // GET /api/tags/refresh-task — 领取一个 productive 标签用于刷新(score>70,未被锁定)
1352
+ if (req.method === "GET" && routePath === "/api/tags/refresh-task") {
1353
+ const task = store.getProductiveRefreshTask();
1354
+ if (!task) {
1355
+ sendJSON(res, 200, {
1356
+ tag: null,
1357
+ message: "no productive tags available for refresh",
1358
+ });
1359
+ return;
1360
+ }
1361
+ sendJSON(res, 200, {
1362
+ tag: task.tag,
1363
+ score: task.score,
1364
+ refreshedAt: task.refreshed_at,
1365
+ });
1366
+ return;
1367
+ }
1368
+
1369
+ // POST /api/tags/refresh-claim { tag } — 锁定 productive 标签用于刷新(防并发冲突)
1370
+ if (req.method === "POST" && routePath === "/api/tags/refresh-claim") {
1371
+ try {
1372
+ const body = await readBody(req);
1373
+ const tag = (body.tag || "").trim().toLowerCase();
1374
+ if (!tag) {
1375
+ sendJSON(res, 400, { error: "tag 不能为空" });
1376
+ return;
1377
+ }
1378
+ const result = store.claimRefreshTag(tag);
1379
+ sendJSON(res, result.ok ? 200 : 409, result);
1380
+ } catch (e) {
1381
+ sendJSON(res, 500, { error: e.message });
1382
+ }
1383
+ return;
1384
+ }
1385
+
1386
+ // POST /api/tags/refresh-result { tag, pushedUsers, error } — 报告刷新结果并解锁
1387
+ if (req.method === "POST" && routePath === "/api/tags/refresh-result") {
1388
+ try {
1389
+ const body = await readBody(req);
1390
+ const tag = (body.tag || "").trim().toLowerCase();
1391
+ if (!tag) {
1392
+ sendJSON(res, 400, { error: "tag 不能为空" });
1393
+ return;
1394
+ }
1395
+ const result = store.reportRefreshResult(tag, body);
1396
+ sendJSON(res, result.ok ? 200 : 400, result);
1397
+ } catch (e) {
1398
+ sendJSON(res, 500, { error: e.message });
1399
+ }
1400
+ return;
1401
+ }
1402
+
1321
1403
  // GET /api/tags/recent-generated?country=ES&lookback=10 — 最近N轮生成的标签(去重)
1322
1404
  if (req.method === "GET" && routePath === "/api/tags/recent-generated") {
1323
1405
  try {
@@ -1491,6 +1573,327 @@ export function startWatchServer(
1491
1573
  return;
1492
1574
  }
1493
1575
 
1576
+ // ====== 自学习系统 API ======
1577
+
1578
+ // GET /api/tags/selflearn/status — 自学习系统状态总览
1579
+ if (req.method === "GET" && routePath === "/api/tags/selflearn/status") {
1580
+ try {
1581
+ const { getSelfLearnStatus } = await import("./tag-selflearn.js");
1582
+ const status = getSelfLearnStatus();
1583
+ sendJSON(res, 200, status);
1584
+ } catch (e) {
1585
+ sendJSON(res, 500, { error: e.message });
1586
+ }
1587
+ return;
1588
+ }
1589
+
1590
+ // POST /api/tags/selflearn/classify-dead — 分类所有 dead tag
1591
+ if (
1592
+ req.method === "POST" &&
1593
+ routePath === "/api/tags/selflearn/classify-dead"
1594
+ ) {
1595
+ try {
1596
+ const { classifyDeadTags } = await import("./tag-selflearn.js");
1597
+ const body = await readBody(req);
1598
+ const result = classifyDeadTags(body?.country || null);
1599
+ sendJSON(res, 200, result);
1600
+ } catch (e) {
1601
+ sendJSON(res, 500, { error: e.message });
1602
+ }
1603
+ return;
1604
+ }
1605
+
1606
+ // POST /api/tags/selflearn/cleanup-truly-dead — 删除真正不存在的 dead tag
1607
+ if (
1608
+ req.method === "POST" &&
1609
+ routePath === "/api/tags/selflearn/cleanup-truly-dead"
1610
+ ) {
1611
+ try {
1612
+ const { cleanupTrulyDeadTags } = await import("./tag-selflearn.js");
1613
+ const result = cleanupTrulyDeadTags();
1614
+ sendJSON(res, 200, result);
1615
+ } catch (e) {
1616
+ sendJSON(res, 500, { error: e.message });
1617
+ }
1618
+ return;
1619
+ }
1620
+
1621
+ // GET /api/tags/selflearn/dead-stats?country=DE — dead tag 分类统计
1622
+ if (
1623
+ req.method === "GET" &&
1624
+ routePath === "/api/tags/selflearn/dead-stats"
1625
+ ) {
1626
+ try {
1627
+ const { getDeadTagStats } = await import("./tag-selflearn.js");
1628
+ const country = params.country || null;
1629
+ const stats = getDeadTagStats(country);
1630
+ sendJSON(res, 200, stats || {});
1631
+ } catch (e) {
1632
+ sendJSON(res, 500, { error: e.message });
1633
+ }
1634
+ return;
1635
+ }
1636
+
1637
+ // POST /api/tags/selflearn/init-directions — 初始化某国的探索方向
1638
+ if (
1639
+ req.method === "POST" &&
1640
+ routePath === "/api/tags/selflearn/init-directions"
1641
+ ) {
1642
+ try {
1643
+ const { initDefaultDirections } = await import("./tag-selflearn.js");
1644
+ const body = await readBody(req);
1645
+ const country = body?.country;
1646
+ if (!country) {
1647
+ sendJSON(res, 400, { error: "country 参数必填" });
1648
+ return;
1649
+ }
1650
+ const result = initDefaultDirections(country);
1651
+ sendJSON(res, 200, {
1652
+ ok: true,
1653
+ count: result.length,
1654
+ directions: result,
1655
+ });
1656
+ } catch (e) {
1657
+ sendJSON(res, 500, { error: e.message });
1658
+ }
1659
+ return;
1660
+ }
1661
+
1662
+ // GET /api/tags/selflearn/directions?country=DE&level=1 — 获取方向列表
1663
+ if (
1664
+ req.method === "GET" &&
1665
+ routePath === "/api/tags/selflearn/directions"
1666
+ ) {
1667
+ try {
1668
+ const { getDirections } = await import("./tag-selflearn.js");
1669
+ const country = params.country;
1670
+ const level = parseInt(params.level) || 1;
1671
+ if (!country) {
1672
+ sendJSON(res, 400, { error: "country 参数必填" });
1673
+ return;
1674
+ }
1675
+ const directions = getDirections(country, level);
1676
+ sendJSON(res, 200, directions);
1677
+ } catch (e) {
1678
+ sendJSON(res, 500, { error: e.message });
1679
+ }
1680
+ return;
1681
+ }
1682
+
1683
+ // GET /api/tags/selflearn/cross-country?country=ES — 跨国家推荐
1684
+ if (
1685
+ req.method === "GET" &&
1686
+ routePath === "/api/tags/selflearn/cross-country"
1687
+ ) {
1688
+ try {
1689
+ const { getCrossCountryRecommendations } =
1690
+ await import("./tag-selflearn.js");
1691
+ const country = params.country;
1692
+ if (!country) {
1693
+ sendJSON(res, 400, { error: "country 参数必填" });
1694
+ return;
1695
+ }
1696
+ const limit = parseInt(params.limit) || 20;
1697
+ const recommendations = getCrossCountryRecommendations(
1698
+ country,
1699
+ limit,
1700
+ );
1701
+ sendJSON(res, 200, recommendations);
1702
+ } catch (e) {
1703
+ sendJSON(res, 500, { error: e.message });
1704
+ }
1705
+ return;
1706
+ }
1707
+
1708
+ // POST /api/tags/selflearn/record-cross-country — 记录跨国家学习结果
1709
+ if (
1710
+ req.method === "POST" &&
1711
+ routePath === "/api/tags/selflearn/record-cross-country"
1712
+ ) {
1713
+ try {
1714
+ const { recordCrossCountryLearning } =
1715
+ await import("./tag-selflearn.js");
1716
+ const body = await readBody(req);
1717
+ const {
1718
+ sourceCountry,
1719
+ sourceTag,
1720
+ targetCountry,
1721
+ targetTag,
1722
+ resultStatus,
1723
+ score,
1724
+ } = body || {};
1725
+ recordCrossCountryLearning(
1726
+ sourceCountry,
1727
+ sourceTag,
1728
+ targetCountry,
1729
+ targetTag,
1730
+ resultStatus,
1731
+ score,
1732
+ );
1733
+ sendJSON(res, 200, { ok: true });
1734
+ } catch (e) {
1735
+ sendJSON(res, 500, { error: e.message });
1736
+ }
1737
+ return;
1738
+ }
1739
+
1740
+ // POST /api/tags/selflearn/record-experiment — 记录实验
1741
+ if (
1742
+ req.method === "POST" &&
1743
+ routePath === "/api/tags/selflearn/record-experiment"
1744
+ ) {
1745
+ try {
1746
+ const { recordExperiment } = await import("./tag-selflearn.js");
1747
+ const body = await readBody(req);
1748
+ const result = recordExperiment(body);
1749
+ sendJSON(res, 200, result);
1750
+ } catch (e) {
1751
+ sendJSON(res, 500, { error: e.message });
1752
+ }
1753
+ return;
1754
+ }
1755
+
1756
+ // GET /api/tags/selflearn/experiments?country=DE&limit=10 — 获取实验历史
1757
+ if (
1758
+ req.method === "GET" &&
1759
+ routePath === "/api/tags/selflearn/experiments"
1760
+ ) {
1761
+ try {
1762
+ const { getExperimentHistory } = await import("./tag-selflearn.js");
1763
+ const country = params.country;
1764
+ if (!country) {
1765
+ sendJSON(res, 400, { error: "country 参数必填" });
1766
+ return;
1767
+ }
1768
+ const limit = parseInt(params.limit) || 10;
1769
+ const experiments = getExperimentHistory(country, limit);
1770
+ sendJSON(res, 200, experiments);
1771
+ } catch (e) {
1772
+ sendJSON(res, 500, { error: e.message });
1773
+ }
1774
+ return;
1775
+ }
1776
+
1777
+ // POST /api/tags/selflearn/analyst — 运行 LLM 分析师
1778
+ if (
1779
+ req.method === "POST" &&
1780
+ routePath === "/api/tags/selflearn/analyst"
1781
+ ) {
1782
+ try {
1783
+ const { runLLMAnalyst } = await import("./tag-selflearn.js");
1784
+ const body = await readBody(req);
1785
+ const countries = body?.countries || null;
1786
+ const report = await runLLMAnalyst(countries);
1787
+ sendJSON(res, 200, report);
1788
+ } catch (e) {
1789
+ sendJSON(res, 500, { error: e.message });
1790
+ }
1791
+ return;
1792
+ }
1793
+
1794
+ // GET /api/tags/selflearn/analyst/latest — 获取最新分析师报告
1795
+ if (
1796
+ req.method === "GET" &&
1797
+ routePath === "/api/tags/selflearn/analyst/latest"
1798
+ ) {
1799
+ try {
1800
+ const { getLatestAnalystReport } = await import("./tag-selflearn.js");
1801
+ const report = getLatestAnalystReport();
1802
+ sendJSON(res, 200, report || {});
1803
+ } catch (e) {
1804
+ sendJSON(res, 500, { error: e.message });
1805
+ }
1806
+ return;
1807
+ }
1808
+
1809
+ // POST /api/tags/selflearn/analyst/execute-cleanup — 执行分析师建议的清理
1810
+ if (
1811
+ req.method === "POST" &&
1812
+ routePath === "/api/tags/selflearn/analyst/execute-cleanup"
1813
+ ) {
1814
+ try {
1815
+ const { executeAnalystCleanup } = await import("./tag-selflearn.js");
1816
+ const result = executeAnalystCleanup();
1817
+ sendJSON(res, 200, result);
1818
+ } catch (e) {
1819
+ sendJSON(res, 500, { error: e.message });
1820
+ }
1821
+ return;
1822
+ }
1823
+
1824
+ // POST /api/tags/selflearn/discover-layered — 分层探索生成 tag
1825
+ if (
1826
+ req.method === "POST" &&
1827
+ routePath === "/api/tags/selflearn/discover-layered"
1828
+ ) {
1829
+ try {
1830
+ const {
1831
+ buildLayeredDiscoverPrompt,
1832
+ getCrossCountryRecommendations,
1833
+ getExperimentHistory,
1834
+ } = await import("./tag-selflearn.js");
1835
+ const { callLLM, parseTagsFromResponse, normalizeTag } =
1836
+ await import("./tag-service.js");
1837
+ const body = await readBody(req);
1838
+ const { country, level = 1, directionName, count = 10 } = body || {};
1839
+
1840
+ if (!country) {
1841
+ sendJSON(res, 400, { error: "country 参数必填" });
1842
+ return;
1843
+ }
1844
+
1845
+ // 收集分层探索所需数据
1846
+ const storeTags = store.getTagsByCountry(country, 50);
1847
+ const storeDead = store.getDeadTags(country);
1848
+ const crossCountryTips = getCrossCountryRecommendations(country, 5);
1849
+ const experimentHistory = getExperimentHistory(country, 5);
1850
+
1851
+ const prompt = buildLayeredDiscoverPrompt(country, {
1852
+ level,
1853
+ existingProductive: storeTags,
1854
+ existingDead: storeDead,
1855
+ directionName,
1856
+ experimentHistory,
1857
+ crossCountryTips,
1858
+ });
1859
+
1860
+ console.error(
1861
+ `[selflearn] ${country} L${level} discover (direction: ${directionName || "auto"})`,
1862
+ );
1863
+ const content = await callLLM(prompt, 0.7);
1864
+ const tags = parseTagsFromResponse(content);
1865
+
1866
+ // 全局去重
1867
+ const { getAllExistingTagNames } = await import("./db-tags.js");
1868
+ const allExisting = getAllExistingTagNames();
1869
+ const unique = [...new Set(tags)]
1870
+ .filter((t) => !allExisting.has(t))
1871
+ .slice(0, count);
1872
+
1873
+ // 入库
1874
+ const inserted = [];
1875
+ for (const tag of unique) {
1876
+ const result = store.insertTag(tag, [country], "llm");
1877
+ if (result.inserted) inserted.push(result.tag);
1878
+ }
1879
+
1880
+ sendJSON(res, 200, {
1881
+ country,
1882
+ level,
1883
+ directionName,
1884
+ generated: tags.length,
1885
+ afterDedup: unique.length,
1886
+ inserted: inserted.length,
1887
+ tags: inserted,
1888
+ });
1889
+ } catch (e) {
1890
+ sendJSON(res, 500, { error: e.message });
1891
+ }
1892
+ return;
1893
+ }
1894
+
1895
+ // ====== 原有静态资源 ======
1896
+
1494
1897
  if (
1495
1898
  req.method === "GET" &&
1496
1899
  (routePath === "/" || routePath === "/index.html")
@@ -1585,7 +1988,7 @@ export function startWatchServer(
1585
1988
  );
1586
1989
  setTimeout(() => runAutoDiscover("startup"), 30000);
1587
1990
  console.error(
1588
- `[discover] 定时+按需发现已启动: 定时间隔 ${DISCOVER_INTERVAL_MS / 60000} 分钟, 每国 ${DISCOVER_COUNT_PER_COUNTRY} 个标签, 按需阈值 ${DISCOVER_LOW_THRESHOLD}, 冷却期 ${DISCOVER_COOLDOWN_MS / 1000}s`,
1991
+ `[discover] 定时+按需发现已启动: 定时间隔 ${DISCOVER_INTERVAL_MS / 60000} 分钟, 每国 ${DISCOVER_COUNT_PER_COUNTRY} 个标签, 每国不足阈值 ${DISCOVER_PER_COUNTRY_THRESHOLD}, 按需触发阈值 ${DISCOVER_LOW_THRESHOLD}, 冷却期 ${DISCOVER_COOLDOWN_MS / 1000}s`,
1589
1992
  );
1590
1993
 
1591
1994
  _resolve({ server, port });