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 +1 -1
- package/src/cli/comments.js +10 -4
- package/src/lib/args.js +8 -0
- package/src/lib/constants.js +5 -2
- package/src/watch/data-store.js +28 -4
- package/src/watch/db-schema.js +82 -0
- package/src/watch/db-tags.js +19 -0
- package/src/watch/server.js +332 -2
- package/src/watch/tag-selflearn.js +986 -0
package/package.json
CHANGED
package/src/cli/comments.js
CHANGED
|
@@ -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
|
|
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,
|
package/src/lib/constants.js
CHANGED
|
@@ -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
|
-
"
|
|
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
|
|
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 <标签名> [...] [选项]",
|
package/src/watch/data-store.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/src/watch/db-schema.js
CHANGED
|
@@ -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);
|
package/src/watch/db-tags.js
CHANGED
|
@@ -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
|
);
|
package/src/watch/server.js
CHANGED
|
@@ -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
|
|
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(
|
|
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")
|