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.
- package/package.json +1 -1
- package/src/cli/comments.js +10 -4
- package/src/cli/tag.js +513 -13
- package/src/lib/api-interceptor.js +2 -0
- package/src/lib/args.js +35 -0
- package/src/lib/constants.js +5 -2
- package/src/main.js +3 -0
- package/src/scraper/explore-core.js +1 -0
- package/src/watch/data-store.js +90 -4
- package/src/watch/db-columns.js +2 -0
- package/src/watch/db-crud.js +28 -0
- package/src/watch/db-schema.js +85 -0
- package/src/watch/db-tags.js +114 -0
- package/src/watch/public/app.js +80 -13
- package/src/watch/public/index.html +7 -1
- package/src/watch/public/style.css +43 -0
- package/src/watch/server.js +422 -19
- package/src/watch/tag-selflearn.js +986 -0
|
@@ -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="
|
|
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;
|
package/src/watch/server.js
CHANGED
|
@@ -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
|
-
//
|
|
146
|
-
const
|
|
147
|
-
|
|
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
|
-
//
|
|
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 <
|
|
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
|
|
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(
|
|
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 和
|
|
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 和
|
|
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} 个标签,
|
|
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 });
|