tt-help-cli-ycl 1.4.12 → 1.4.13

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tt-help-cli-ycl",
3
- "version": "1.4.12",
3
+ "version": "1.4.13",
4
4
  "description": "TikTok user & video data scraper - extract ttSeller, verified, locationCreated from HTML source",
5
5
  "type": "module",
6
6
  "bin": {
@@ -528,6 +528,7 @@ async function runAutoMode(options) {
528
528
  maxComments,
529
529
  basePort,
530
530
  proxy: proxyOption,
531
+ locations,
531
532
  } = options;
532
533
 
533
534
  const actualParallel = Math.max(1, parallel || CFG.parallel);
@@ -535,6 +536,7 @@ async function runAutoMode(options) {
535
536
  const actualMaxComments = maxComments || CFG.maxComments;
536
537
  const effectiveProxy = proxyOption || configuredProxy;
537
538
  const portPoolStart = basePort || CFG.portPoolStart;
539
+ const jobLocations = locations || null;
538
540
 
539
541
  loadEnvConfig();
540
542
 
@@ -597,8 +599,9 @@ async function runAutoMode(options) {
597
599
  console.error(`\n╔══════════════════════════════════════════╗`);
598
600
  console.error(`║ 自动评论采集模式(CDP 连接已登录浏览器)`);
599
601
  console.error(
600
- `║ 目标国家: ${DEFAULT_TARGET_LOCATIONS.slice(0, 6).join(",")}...`,
602
+ `║ 目标国家: ${jobLocations || DEFAULT_TARGET_LOCATIONS.slice(0, 6).join(",") + "..."}`,
601
603
  );
604
+ if (jobLocations) console.error(`║ 任务过滤: ${jobLocations}`);
602
605
  console.error(`║ 服务端: ${serverUrl}`);
603
606
  console.error(
604
607
  `║ CDP 端口池: ${portPoolStart}-${portPoolStart + CFG.portPoolSize - 1}(当前: ${session.port})`,
@@ -635,9 +638,10 @@ async function runAutoMode(options) {
635
638
  // ── 领取任务 ──
636
639
  let tasks;
637
640
  try {
638
- const resp = await apiGet(
639
- `${serverUrl}/api/comment-tasks?limit=${actualParallel}`,
640
- );
641
+ const apiUrl = jobLocations
642
+ ? `${serverUrl}/api/comment-tasks?limit=${actualParallel}&locations=${encodeURIComponent(jobLocations)}`
643
+ : `${serverUrl}/api/comment-tasks?limit=${actualParallel}`;
644
+ const resp = await apiGet(apiUrl);
641
645
  tasks = resp.tasks || [];
642
646
  } catch (err) {
643
647
  console.error(` × 获取任务失败: ${err.message}`);
@@ -1031,6 +1035,7 @@ export async function handleComments(options) {
1031
1035
  commentsServer,
1032
1036
  commentsBasePort,
1033
1037
  commentsProxy,
1038
+ commentsLocations,
1034
1039
  } = options;
1035
1040
 
1036
1041
  // 自动模式:无 URL
@@ -1042,6 +1047,7 @@ export async function handleComments(options) {
1042
1047
  maxComments: commentsMax,
1043
1048
  basePort: commentsBasePort,
1044
1049
  proxy: commentsProxy,
1050
+ locations: commentsLocations,
1045
1051
  });
1046
1052
  }
1047
1053
 
package/src/lib/args.js CHANGED
@@ -670,6 +670,7 @@ function parseCommentsArgs(args) {
670
670
  let commentsServer = defaultServer;
671
671
  let commentsBasePort = 6222;
672
672
  let commentsProxy = null;
673
+ let commentsLocations = null;
673
674
 
674
675
  const positional = [];
675
676
 
@@ -689,6 +690,12 @@ function parseCommentsArgs(args) {
689
690
  commentsBasePort = parseInt(args[++i]) || 6222;
690
691
  } else if (arg === "--proxy") {
691
692
  commentsProxy = args[++i];
693
+ } else if (
694
+ arg === "-l" ||
695
+ arg === "--location" ||
696
+ arg === "--job-locations"
697
+ ) {
698
+ commentsLocations = args[++i];
692
699
  } else {
693
700
  positional.push(arg);
694
701
  }
@@ -709,6 +716,7 @@ function parseCommentsArgs(args) {
709
716
  commentsServer,
710
717
  commentsBasePort,
711
718
  commentsProxy,
719
+ commentsLocations,
712
720
  urls: [],
713
721
  outputFormat: "json",
714
722
  exploreCount: 0,
@@ -217,10 +217,13 @@ const HELP_TEXT = [
217
217
  " -i, --interval 空闲间隔上限秒(默认: 10,实际 1~N 随机)",
218
218
  " -s, --server 服务端地址(默认: http://127.0.0.1:3001)",
219
219
  " -m, --max-comments 每视频最大评论数(默认: 200)",
220
- " --base-port 浏览器CDP端口(默认: 9222)",
220
+ " -l, --location 按国家领取任务,逗号分隔(如: DE,FR,IT)",
221
+ " 未指定时领取所有国家的任务",
222
+ " --base-port 浏览器CDP端口(默认: 6222)",
221
223
  " --save 去重后保存到服务端,来源标记为 comment",
222
224
  " 示例: tt-help comments",
223
- " tt-help comments -p 2 -i 15 --base-port 9223",
225
+ " tt-help comments -p 2 -i 15 --base-port 6223",
226
+ " tt-help comments -l DE,FR,IT -p 5",
224
227
  " tt-help comments <视频URL> 50 --save",
225
228
  "",
226
229
  " tag <标签名> [...] [选项]",
@@ -3130,20 +3130,32 @@ export function createStore(filePath, options = {}) {
3130
3130
  return videos.length;
3131
3131
  }
3132
3132
 
3133
- function getPendingCommentTasks(limit) {
3133
+ function getPendingCommentTasks(limit, locations = null) {
3134
3134
  if (getDb()) {
3135
3135
  const l = Math.max(1, parseInt(limit) || 1);
3136
+ const normalizedLocations =
3137
+ Array.isArray(locations) && locations.length
3138
+ ? locations
3139
+ .map((loc) => String(loc).trim().toUpperCase())
3140
+ .filter(Boolean)
3141
+ : null;
3142
+
3143
+ const locationFilter = normalizedLocations
3144
+ ? `AND UPPER(COALESCE(location_created, '')) IN (${normalizedLocations.map(() => "?").join(", ")})`
3145
+ : "";
3146
+
3136
3147
  const rows = getDb()
3137
3148
  .prepare(
3138
3149
  `
3139
3150
  SELECT *
3140
3151
  FROM videos
3141
- WHERE user_update_count IS NULL OR user_update_count <= 0
3152
+ WHERE (user_update_count IS NULL OR user_update_count <= 0)
3153
+ ${locationFilter}
3142
3154
  ORDER BY tt_seller DESC, registered_at ASC
3143
3155
  LIMIT ?
3144
3156
  `,
3145
3157
  )
3146
- .all(l);
3158
+ .all(...(normalizedLocations || []), l);
3147
3159
  if (rows.length === 0) return [];
3148
3160
  const bumpStmt = getDb().prepare(
3149
3161
  `
@@ -3164,7 +3176,19 @@ export function createStore(filePath, options = {}) {
3164
3176
  }
3165
3177
 
3166
3178
  // 筛选待处理视频(userUpdateCount <= 0 或 null/undefined)
3167
- const pending = videos.filter((v) => (v.userUpdateCount || 0) <= 0);
3179
+ let pending = videos.filter((v) => (v.userUpdateCount || 0) <= 0);
3180
+ // 按 locations 过滤
3181
+ if (Array.isArray(locations) && locations.length > 0) {
3182
+ const upperLocs = locations.map((loc) =>
3183
+ String(loc).trim().toUpperCase(),
3184
+ );
3185
+ pending = pending.filter((v) => {
3186
+ const loc = String(v.locationCreated || "")
3187
+ .trim()
3188
+ .toUpperCase();
3189
+ return upperLocs.includes(loc);
3190
+ });
3191
+ }
3168
3192
  // ttSeller=true 优先
3169
3193
  pending.sort((a, b) => {
3170
3194
  if (a.ttSeller && !b.ttSeller) return -1;
@@ -278,8 +278,84 @@ export function initDb(filePath) {
278
278
  migrateColumns("tags", {
279
279
  discover_round: "INTEGER DEFAULT 0",
280
280
  strategy: "TEXT",
281
+ dead_reason: "TEXT",
282
+ direction_id: "INTEGER",
281
283
  });
282
284
 
285
+ // ===== tag_directions 表(分层探索:通用词/分类/产品) =====
286
+ db.exec(`
287
+ CREATE TABLE IF NOT EXISTS tag_directions (
288
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
289
+ name TEXT NOT NULL,
290
+ level INTEGER NOT NULL DEFAULT 1,
291
+ parent_id INTEGER,
292
+ country TEXT,
293
+ lang_group TEXT,
294
+ status TEXT NOT NULL DEFAULT 'active',
295
+ total_experiments INTEGER DEFAULT 0,
296
+ productive_experiments INTEGER DEFAULT 0,
297
+ best_score REAL DEFAULT 0,
298
+ notes TEXT,
299
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
300
+ updated_at TEXT
301
+ )
302
+ `);
303
+ migrateColumns("tag_directions", {
304
+ lang_group: "TEXT",
305
+ });
306
+
307
+ // ===== tag_experiments 表(每次探索的实验记录) =====
308
+ db.exec(`
309
+ CREATE TABLE IF NOT EXISTS tag_experiments (
310
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
311
+ direction_id INTEGER,
312
+ country TEXT NOT NULL,
313
+ hypothesis TEXT,
314
+ strategy TEXT,
315
+ tags_tried TEXT NOT NULL DEFAULT '[]',
316
+ tags_productive TEXT NOT NULL DEFAULT '[]',
317
+ tags_dead_not_exist TEXT NOT NULL DEFAULT '[]',
318
+ tags_dead_no_match TEXT NOT NULL DEFAULT '[]',
319
+ conclusion TEXT,
320
+ productive_count INTEGER DEFAULT 0,
321
+ dead_count INTEGER DEFAULT 0,
322
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
323
+ )
324
+ `);
325
+
326
+ // ===== tag_analyst_reports 表(LLM 分析师定期体检报告) =====
327
+ db.exec(`
328
+ CREATE TABLE IF NOT EXISTS tag_analyst_reports (
329
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
330
+ report_type TEXT NOT NULL DEFAULT 'daily',
331
+ countries TEXT NOT NULL DEFAULT '[]',
332
+ summary TEXT,
333
+ top_directions TEXT NOT NULL DEFAULT '[]',
334
+ worst_directions TEXT NOT NULL DEFAULT '[]',
335
+ cross_country_suggestions TEXT NOT NULL DEFAULT '[]',
336
+ cleanup_suggestions TEXT NOT NULL DEFAULT '[]',
337
+ next_strategy TEXT,
338
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
339
+ )
340
+ `);
341
+
342
+ // ===== cross_country_learnings 表(跨国家经验复用记录) =====
343
+ db.exec(`
344
+ CREATE TABLE IF NOT EXISTS cross_country_learnings (
345
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
346
+ source_country TEXT NOT NULL,
347
+ source_tag TEXT NOT NULL,
348
+ source_direction_id INTEGER,
349
+ target_country TEXT NOT NULL,
350
+ target_tag TEXT,
351
+ transferred_at TEXT,
352
+ result_status TEXT,
353
+ score REAL,
354
+ notes TEXT,
355
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
356
+ )
357
+ `);
358
+
283
359
  // ===== discover_log 表(记录每次 LLM discover 的策略和思考) =====
284
360
  db.exec(`
285
361
  CREATE TABLE IF NOT EXISTS discover_log (
@@ -334,6 +410,12 @@ export function initDb(filePath) {
334
410
  `CREATE INDEX IF NOT EXISTS idx_tags_score ON tags(score DESC)`,
335
411
  `CREATE INDEX IF NOT EXISTS idx_discover_log_country ON discover_log(country, round DESC)`,
336
412
  `CREATE INDEX IF NOT EXISTS idx_reflection_rules_active ON reflection_rules(country, is_active)`,
413
+ `CREATE INDEX IF NOT EXISTS idx_tag_directions_country ON tag_directions(country, level)`,
414
+ `CREATE INDEX IF NOT EXISTS idx_tag_experiments_country ON tag_experiments(country, created_at DESC)`,
415
+ `CREATE INDEX IF NOT EXISTS idx_tag_experiments_direction ON tag_experiments(direction_id)`,
416
+ `CREATE INDEX IF NOT EXISTS idx_cross_country_source ON cross_country_learnings(source_country, result_status)`,
417
+ `CREATE INDEX IF NOT EXISTS idx_cross_country_target ON cross_country_learnings(target_country)`,
418
+ `CREATE INDEX IF NOT EXISTS idx_tags_dead_reason ON tags(dead_reason)`,
337
419
  ];
338
420
  for (const sql of indexes) {
339
421
  db.exec(sql);
@@ -171,6 +171,23 @@ export function reportTagScore(tag, fields) {
171
171
  : null;
172
172
  const now = new Date().toISOString();
173
173
 
174
+ // 自动分类 dead_reason
175
+ let deadReason = null;
176
+ if (status === "dead" && error) {
177
+ const errLower = error.toLowerCase();
178
+ if (
179
+ errLower.includes("no videos found") ||
180
+ errLower.includes("tag_not_found") ||
181
+ errLower.includes("找不到")
182
+ ) {
183
+ deadReason = "not_exist";
184
+ } else if ((matchedAuthors ?? 0) === 0) {
185
+ deadReason = "no_match";
186
+ } else {
187
+ deadReason = "wrong_country";
188
+ }
189
+ }
190
+
174
191
  try {
175
192
  const result = db
176
193
  .prepare(
@@ -183,6 +200,7 @@ export function reportTagScore(tag, fields) {
183
200
  matched_countries = COALESCE(?, matched_countries),
184
201
  pushed_users = COALESCE(?, pushed_users),
185
202
  last_error = COALESCE(?, last_error),
203
+ dead_reason = COALESCE(?, dead_reason),
186
204
  scored_at = ?,
187
205
  score_count = score_count + 1
188
206
  WHERE tag = ?`,
@@ -196,6 +214,7 @@ export function reportTagScore(tag, fields) {
196
214
  matchedCountriesJson,
197
215
  pushedUsers ?? null,
198
216
  error ?? null,
217
+ deadReason,
199
218
  now,
200
219
  tag,
201
220
  );
@@ -683,9 +683,18 @@ export function startWatchServer(
683
683
 
684
684
  if (req.method === "GET" && routePath === "/api/comment-tasks") {
685
685
  const limit = parseInt(params.limit) || 1;
686
- const tasks = store.getPendingCommentTasks(limit);
686
+ const locationsParam = params.locations || "";
687
+ const locations = locationsParam
688
+ ? locationsParam
689
+ .split(",")
690
+ .map((s) => s.trim().toUpperCase())
691
+ .filter(Boolean)
692
+ : null;
693
+ const tasks = store.getPendingCommentTasks(limit, locations);
687
694
  const ts = new Date().toISOString().slice(11, 19);
688
- console.error(`[JOB ${ts}] COMMENT-TASKS: ${tasks.length} tasks`);
695
+ console.error(
696
+ `[JOB ${ts}] COMMENT-TASKS: ${tasks.length} tasks${locations ? " (locations: " + locations.join(",") + ")" : ""}`,
697
+ );
689
698
  sendJSON(res, 200, { total: tasks.length, tasks });
690
699
  return;
691
700
  }
@@ -1491,6 +1500,327 @@ export function startWatchServer(
1491
1500
  return;
1492
1501
  }
1493
1502
 
1503
+ // ====== 自学习系统 API ======
1504
+
1505
+ // GET /api/tags/selflearn/status — 自学习系统状态总览
1506
+ if (req.method === "GET" && routePath === "/api/tags/selflearn/status") {
1507
+ try {
1508
+ const { getSelfLearnStatus } = await import("./tag-selflearn.js");
1509
+ const status = getSelfLearnStatus();
1510
+ sendJSON(res, 200, status);
1511
+ } catch (e) {
1512
+ sendJSON(res, 500, { error: e.message });
1513
+ }
1514
+ return;
1515
+ }
1516
+
1517
+ // POST /api/tags/selflearn/classify-dead — 分类所有 dead tag
1518
+ if (
1519
+ req.method === "POST" &&
1520
+ routePath === "/api/tags/selflearn/classify-dead"
1521
+ ) {
1522
+ try {
1523
+ const { classifyDeadTags } = await import("./tag-selflearn.js");
1524
+ const body = await readBody(req);
1525
+ const result = classifyDeadTags(body?.country || null);
1526
+ sendJSON(res, 200, result);
1527
+ } catch (e) {
1528
+ sendJSON(res, 500, { error: e.message });
1529
+ }
1530
+ return;
1531
+ }
1532
+
1533
+ // POST /api/tags/selflearn/cleanup-truly-dead — 删除真正不存在的 dead tag
1534
+ if (
1535
+ req.method === "POST" &&
1536
+ routePath === "/api/tags/selflearn/cleanup-truly-dead"
1537
+ ) {
1538
+ try {
1539
+ const { cleanupTrulyDeadTags } = await import("./tag-selflearn.js");
1540
+ const result = cleanupTrulyDeadTags();
1541
+ sendJSON(res, 200, result);
1542
+ } catch (e) {
1543
+ sendJSON(res, 500, { error: e.message });
1544
+ }
1545
+ return;
1546
+ }
1547
+
1548
+ // GET /api/tags/selflearn/dead-stats?country=DE — dead tag 分类统计
1549
+ if (
1550
+ req.method === "GET" &&
1551
+ routePath === "/api/tags/selflearn/dead-stats"
1552
+ ) {
1553
+ try {
1554
+ const { getDeadTagStats } = await import("./tag-selflearn.js");
1555
+ const country = params.country || null;
1556
+ const stats = getDeadTagStats(country);
1557
+ sendJSON(res, 200, stats || {});
1558
+ } catch (e) {
1559
+ sendJSON(res, 500, { error: e.message });
1560
+ }
1561
+ return;
1562
+ }
1563
+
1564
+ // POST /api/tags/selflearn/init-directions — 初始化某国的探索方向
1565
+ if (
1566
+ req.method === "POST" &&
1567
+ routePath === "/api/tags/selflearn/init-directions"
1568
+ ) {
1569
+ try {
1570
+ const { initDefaultDirections } = await import("./tag-selflearn.js");
1571
+ const body = await readBody(req);
1572
+ const country = body?.country;
1573
+ if (!country) {
1574
+ sendJSON(res, 400, { error: "country 参数必填" });
1575
+ return;
1576
+ }
1577
+ const result = initDefaultDirections(country);
1578
+ sendJSON(res, 200, {
1579
+ ok: true,
1580
+ count: result.length,
1581
+ directions: result,
1582
+ });
1583
+ } catch (e) {
1584
+ sendJSON(res, 500, { error: e.message });
1585
+ }
1586
+ return;
1587
+ }
1588
+
1589
+ // GET /api/tags/selflearn/directions?country=DE&level=1 — 获取方向列表
1590
+ if (
1591
+ req.method === "GET" &&
1592
+ routePath === "/api/tags/selflearn/directions"
1593
+ ) {
1594
+ try {
1595
+ const { getDirections } = await import("./tag-selflearn.js");
1596
+ const country = params.country;
1597
+ const level = parseInt(params.level) || 1;
1598
+ if (!country) {
1599
+ sendJSON(res, 400, { error: "country 参数必填" });
1600
+ return;
1601
+ }
1602
+ const directions = getDirections(country, level);
1603
+ sendJSON(res, 200, directions);
1604
+ } catch (e) {
1605
+ sendJSON(res, 500, { error: e.message });
1606
+ }
1607
+ return;
1608
+ }
1609
+
1610
+ // GET /api/tags/selflearn/cross-country?country=ES — 跨国家推荐
1611
+ if (
1612
+ req.method === "GET" &&
1613
+ routePath === "/api/tags/selflearn/cross-country"
1614
+ ) {
1615
+ try {
1616
+ const { getCrossCountryRecommendations } =
1617
+ await import("./tag-selflearn.js");
1618
+ const country = params.country;
1619
+ if (!country) {
1620
+ sendJSON(res, 400, { error: "country 参数必填" });
1621
+ return;
1622
+ }
1623
+ const limit = parseInt(params.limit) || 20;
1624
+ const recommendations = getCrossCountryRecommendations(
1625
+ country,
1626
+ limit,
1627
+ );
1628
+ sendJSON(res, 200, recommendations);
1629
+ } catch (e) {
1630
+ sendJSON(res, 500, { error: e.message });
1631
+ }
1632
+ return;
1633
+ }
1634
+
1635
+ // POST /api/tags/selflearn/record-cross-country — 记录跨国家学习结果
1636
+ if (
1637
+ req.method === "POST" &&
1638
+ routePath === "/api/tags/selflearn/record-cross-country"
1639
+ ) {
1640
+ try {
1641
+ const { recordCrossCountryLearning } =
1642
+ await import("./tag-selflearn.js");
1643
+ const body = await readBody(req);
1644
+ const {
1645
+ sourceCountry,
1646
+ sourceTag,
1647
+ targetCountry,
1648
+ targetTag,
1649
+ resultStatus,
1650
+ score,
1651
+ } = body || {};
1652
+ recordCrossCountryLearning(
1653
+ sourceCountry,
1654
+ sourceTag,
1655
+ targetCountry,
1656
+ targetTag,
1657
+ resultStatus,
1658
+ score,
1659
+ );
1660
+ sendJSON(res, 200, { ok: true });
1661
+ } catch (e) {
1662
+ sendJSON(res, 500, { error: e.message });
1663
+ }
1664
+ return;
1665
+ }
1666
+
1667
+ // POST /api/tags/selflearn/record-experiment — 记录实验
1668
+ if (
1669
+ req.method === "POST" &&
1670
+ routePath === "/api/tags/selflearn/record-experiment"
1671
+ ) {
1672
+ try {
1673
+ const { recordExperiment } = await import("./tag-selflearn.js");
1674
+ const body = await readBody(req);
1675
+ const result = recordExperiment(body);
1676
+ sendJSON(res, 200, result);
1677
+ } catch (e) {
1678
+ sendJSON(res, 500, { error: e.message });
1679
+ }
1680
+ return;
1681
+ }
1682
+
1683
+ // GET /api/tags/selflearn/experiments?country=DE&limit=10 — 获取实验历史
1684
+ if (
1685
+ req.method === "GET" &&
1686
+ routePath === "/api/tags/selflearn/experiments"
1687
+ ) {
1688
+ try {
1689
+ const { getExperimentHistory } = await import("./tag-selflearn.js");
1690
+ const country = params.country;
1691
+ if (!country) {
1692
+ sendJSON(res, 400, { error: "country 参数必填" });
1693
+ return;
1694
+ }
1695
+ const limit = parseInt(params.limit) || 10;
1696
+ const experiments = getExperimentHistory(country, limit);
1697
+ sendJSON(res, 200, experiments);
1698
+ } catch (e) {
1699
+ sendJSON(res, 500, { error: e.message });
1700
+ }
1701
+ return;
1702
+ }
1703
+
1704
+ // POST /api/tags/selflearn/analyst — 运行 LLM 分析师
1705
+ if (
1706
+ req.method === "POST" &&
1707
+ routePath === "/api/tags/selflearn/analyst"
1708
+ ) {
1709
+ try {
1710
+ const { runLLMAnalyst } = await import("./tag-selflearn.js");
1711
+ const body = await readBody(req);
1712
+ const countries = body?.countries || null;
1713
+ const report = await runLLMAnalyst(countries);
1714
+ sendJSON(res, 200, report);
1715
+ } catch (e) {
1716
+ sendJSON(res, 500, { error: e.message });
1717
+ }
1718
+ return;
1719
+ }
1720
+
1721
+ // GET /api/tags/selflearn/analyst/latest — 获取最新分析师报告
1722
+ if (
1723
+ req.method === "GET" &&
1724
+ routePath === "/api/tags/selflearn/analyst/latest"
1725
+ ) {
1726
+ try {
1727
+ const { getLatestAnalystReport } = await import("./tag-selflearn.js");
1728
+ const report = getLatestAnalystReport();
1729
+ sendJSON(res, 200, report || {});
1730
+ } catch (e) {
1731
+ sendJSON(res, 500, { error: e.message });
1732
+ }
1733
+ return;
1734
+ }
1735
+
1736
+ // POST /api/tags/selflearn/analyst/execute-cleanup — 执行分析师建议的清理
1737
+ if (
1738
+ req.method === "POST" &&
1739
+ routePath === "/api/tags/selflearn/analyst/execute-cleanup"
1740
+ ) {
1741
+ try {
1742
+ const { executeAnalystCleanup } = await import("./tag-selflearn.js");
1743
+ const result = executeAnalystCleanup();
1744
+ sendJSON(res, 200, result);
1745
+ } catch (e) {
1746
+ sendJSON(res, 500, { error: e.message });
1747
+ }
1748
+ return;
1749
+ }
1750
+
1751
+ // POST /api/tags/selflearn/discover-layered — 分层探索生成 tag
1752
+ if (
1753
+ req.method === "POST" &&
1754
+ routePath === "/api/tags/selflearn/discover-layered"
1755
+ ) {
1756
+ try {
1757
+ const {
1758
+ buildLayeredDiscoverPrompt,
1759
+ getCrossCountryRecommendations,
1760
+ getExperimentHistory,
1761
+ } = await import("./tag-selflearn.js");
1762
+ const { callLLM, parseTagsFromResponse, normalizeTag } =
1763
+ await import("./tag-service.js");
1764
+ const body = await readBody(req);
1765
+ const { country, level = 1, directionName, count = 10 } = body || {};
1766
+
1767
+ if (!country) {
1768
+ sendJSON(res, 400, { error: "country 参数必填" });
1769
+ return;
1770
+ }
1771
+
1772
+ // 收集分层探索所需数据
1773
+ const storeTags = store.getTagsByCountry(country, 50);
1774
+ const storeDead = store.getDeadTags(country);
1775
+ const crossCountryTips = getCrossCountryRecommendations(country, 5);
1776
+ const experimentHistory = getExperimentHistory(country, 5);
1777
+
1778
+ const prompt = buildLayeredDiscoverPrompt(country, {
1779
+ level,
1780
+ existingProductive: storeTags,
1781
+ existingDead: storeDead,
1782
+ directionName,
1783
+ experimentHistory,
1784
+ crossCountryTips,
1785
+ });
1786
+
1787
+ console.error(
1788
+ `[selflearn] ${country} L${level} discover (direction: ${directionName || "auto"})`,
1789
+ );
1790
+ const content = await callLLM(prompt, 0.7);
1791
+ const tags = parseTagsFromResponse(content);
1792
+
1793
+ // 全局去重
1794
+ const { getAllExistingTagNames } = await import("./db-tags.js");
1795
+ const allExisting = getAllExistingTagNames();
1796
+ const unique = [...new Set(tags)]
1797
+ .filter((t) => !allExisting.has(t))
1798
+ .slice(0, count);
1799
+
1800
+ // 入库
1801
+ const inserted = [];
1802
+ for (const tag of unique) {
1803
+ const result = store.insertTag(tag, [country], "llm");
1804
+ if (result.inserted) inserted.push(result.tag);
1805
+ }
1806
+
1807
+ sendJSON(res, 200, {
1808
+ country,
1809
+ level,
1810
+ directionName,
1811
+ generated: tags.length,
1812
+ afterDedup: unique.length,
1813
+ inserted: inserted.length,
1814
+ tags: inserted,
1815
+ });
1816
+ } catch (e) {
1817
+ sendJSON(res, 500, { error: e.message });
1818
+ }
1819
+ return;
1820
+ }
1821
+
1822
+ // ====== 原有静态资源 ======
1823
+
1494
1824
  if (
1495
1825
  req.method === "GET" &&
1496
1826
  (routePath === "/" || routePath === "/index.html")