tt-help-cli-ycl 1.4.11 → 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 +37 -6
- package/src/lib/args.js +8 -0
- package/src/lib/constants.js +5 -2
- package/src/watch/data-store.js +32 -16
- package/src/watch/db-schema.js +82 -0
- package/src/watch/db-stats.js +4 -4
- package/src/watch/db-tags.js +19 -0
- package/src/watch/public/app.js +1 -35
- package/src/watch/public/index.html +0 -5
- 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
|
@@ -69,6 +69,8 @@ const CFG = {
|
|
|
69
69
|
switchPortOnRecycle: true,
|
|
70
70
|
/** 连续任务处理错误阈值(达到后上报服务端) */
|
|
71
71
|
consecutiveTaskErrorsMax: 20,
|
|
72
|
+
/** 单端口最大运行时间(ms),超时自动切换端口(默认 48 分钟) */
|
|
73
|
+
maxSessionRunTimeMs: 48 * 60 * 1000,
|
|
72
74
|
};
|
|
73
75
|
|
|
74
76
|
/** 从环境变量覆写配置 */
|
|
@@ -105,6 +107,10 @@ function loadEnvConfig() {
|
|
|
105
107
|
1,
|
|
106
108
|
int("COMMENTS_CONSECUTIVE_TASK_ERRORS_MAX", CFG.consecutiveTaskErrorsMax),
|
|
107
109
|
);
|
|
110
|
+
CFG.maxSessionRunTimeMs = Math.max(
|
|
111
|
+
60000,
|
|
112
|
+
int("COMMENTS_MAX_SESSION_RUN_TIME_MS", CFG.maxSessionRunTimeMs),
|
|
113
|
+
);
|
|
108
114
|
}
|
|
109
115
|
|
|
110
116
|
// ────────────────────────────────────────
|
|
@@ -283,6 +289,7 @@ class CdpSessionManager {
|
|
|
283
289
|
this.browser = null;
|
|
284
290
|
this.page = null;
|
|
285
291
|
this.lastRecycleAt = 0;
|
|
292
|
+
this.sessionStartTime = Date.now();
|
|
286
293
|
this._lastMemCheck = 0;
|
|
287
294
|
this._shuttingDown = false;
|
|
288
295
|
}
|
|
@@ -362,14 +369,28 @@ class CdpSessionManager {
|
|
|
362
369
|
this.browser = await ensureBrowserReady(opts);
|
|
363
370
|
this.page = await this._ensurePage();
|
|
364
371
|
this.lastRecycleAt = Date.now();
|
|
372
|
+
// 端口切换时重置运行计时
|
|
373
|
+
if (oldPort !== undefined && oldPort !== this.port) {
|
|
374
|
+
this.sessionStartTime = Date.now();
|
|
375
|
+
}
|
|
365
376
|
}
|
|
366
377
|
|
|
367
|
-
/** 内存检查 + 按需回收(每 30s 最多采样一次) */
|
|
378
|
+
/** 内存检查 + 定时轮换 + 按需回收(每 30s 最多采样一次) */
|
|
368
379
|
async maybeRecycleForMemory(processedCount) {
|
|
369
380
|
const now = Date.now();
|
|
370
381
|
if (now - this._lastMemCheck < 30000) return;
|
|
371
382
|
this._lastMemCheck = now;
|
|
372
383
|
|
|
384
|
+
// 定时轮换:单端口运行超时自动切换
|
|
385
|
+
const sessionElapsed = now - this.sessionStartTime;
|
|
386
|
+
if (sessionElapsed >= CFG.maxSessionRunTimeMs) {
|
|
387
|
+
const mins = Math.round(sessionElapsed / 60000);
|
|
388
|
+
await this.recycleSession(`定时轮换 (运行 ${mins} 分钟)`, {
|
|
389
|
+
switchPort: true,
|
|
390
|
+
});
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
373
394
|
const mem = process.memoryUsage();
|
|
374
395
|
const rssMB = mem.rss / 1024 / 1024;
|
|
375
396
|
const heapMB = mem.heapUsed / 1024 / 1024;
|
|
@@ -507,6 +528,7 @@ async function runAutoMode(options) {
|
|
|
507
528
|
maxComments,
|
|
508
529
|
basePort,
|
|
509
530
|
proxy: proxyOption,
|
|
531
|
+
locations,
|
|
510
532
|
} = options;
|
|
511
533
|
|
|
512
534
|
const actualParallel = Math.max(1, parallel || CFG.parallel);
|
|
@@ -514,6 +536,7 @@ async function runAutoMode(options) {
|
|
|
514
536
|
const actualMaxComments = maxComments || CFG.maxComments;
|
|
515
537
|
const effectiveProxy = proxyOption || configuredProxy;
|
|
516
538
|
const portPoolStart = basePort || CFG.portPoolStart;
|
|
539
|
+
const jobLocations = locations || null;
|
|
517
540
|
|
|
518
541
|
loadEnvConfig();
|
|
519
542
|
|
|
@@ -576,8 +599,9 @@ async function runAutoMode(options) {
|
|
|
576
599
|
console.error(`\n╔══════════════════════════════════════════╗`);
|
|
577
600
|
console.error(`║ 自动评论采集模式(CDP 连接已登录浏览器)`);
|
|
578
601
|
console.error(
|
|
579
|
-
`║ 目标国家: ${DEFAULT_TARGET_LOCATIONS.slice(0, 6).join(",")}
|
|
602
|
+
`║ 目标国家: ${jobLocations || DEFAULT_TARGET_LOCATIONS.slice(0, 6).join(",") + "..."}`,
|
|
580
603
|
);
|
|
604
|
+
if (jobLocations) console.error(`║ 任务过滤: ${jobLocations}`);
|
|
581
605
|
console.error(`║ 服务端: ${serverUrl}`);
|
|
582
606
|
console.error(
|
|
583
607
|
`║ CDP 端口池: ${portPoolStart}-${portPoolStart + CFG.portPoolSize - 1}(当前: ${session.port})`,
|
|
@@ -614,9 +638,10 @@ async function runAutoMode(options) {
|
|
|
614
638
|
// ── 领取任务 ──
|
|
615
639
|
let tasks;
|
|
616
640
|
try {
|
|
617
|
-
const
|
|
618
|
-
`${serverUrl}/api/comment-tasks?limit=${actualParallel}
|
|
619
|
-
|
|
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);
|
|
620
645
|
tasks = resp.tasks || [];
|
|
621
646
|
} catch (err) {
|
|
622
647
|
console.error(` × 获取任务失败: ${err.message}`);
|
|
@@ -783,8 +808,12 @@ async function runAutoMode(options) {
|
|
|
783
808
|
|
|
784
809
|
// 每 10 个任务输出内存汇总
|
|
785
810
|
if (totalTasks() % 10 === 0) {
|
|
811
|
+
const remainingMs =
|
|
812
|
+
session.sessionStartTime + CFG.maxSessionRunTimeMs - Date.now();
|
|
813
|
+
const remainingMins =
|
|
814
|
+
remainingMs > 0 ? Math.round(remainingMs / 60000) : 0;
|
|
786
815
|
console.error(
|
|
787
|
-
`── #${totalTasks()} ── ${formatMemoryWithBrowser(session.port)} 处理=${processedCount} 跳过=${skippedCount} 错误=${errorCount}`,
|
|
816
|
+
`── #${totalTasks()} ── ${formatMemoryWithBrowser(session.port)} 处理=${processedCount} 跳过=${skippedCount} 错误=${errorCount} 距切换≈${remainingMins}min`,
|
|
788
817
|
);
|
|
789
818
|
}
|
|
790
819
|
} catch (err) {
|
|
@@ -1006,6 +1035,7 @@ export async function handleComments(options) {
|
|
|
1006
1035
|
commentsServer,
|
|
1007
1036
|
commentsBasePort,
|
|
1008
1037
|
commentsProxy,
|
|
1038
|
+
commentsLocations,
|
|
1009
1039
|
} = options;
|
|
1010
1040
|
|
|
1011
1041
|
// 自动模式:无 URL
|
|
@@ -1017,6 +1047,7 @@ export async function handleComments(options) {
|
|
|
1017
1047
|
maxComments: commentsMax,
|
|
1018
1048
|
basePort: commentsBasePort,
|
|
1019
1049
|
proxy: commentsProxy,
|
|
1050
|
+
locations: commentsLocations,
|
|
1020
1051
|
});
|
|
1021
1052
|
}
|
|
1022
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
|
@@ -2647,8 +2647,7 @@ export function createStore(filePath, options = {}) {
|
|
|
2647
2647
|
let sql = `
|
|
2648
2648
|
SELECT *
|
|
2649
2649
|
FROM jobs_base
|
|
2650
|
-
WHERE
|
|
2651
|
-
AND COALESCE(user_update_count, 0) <= 0
|
|
2650
|
+
WHERE COALESCE(user_update_count, 0) <= 0
|
|
2652
2651
|
`;
|
|
2653
2652
|
const sqlParams = [];
|
|
2654
2653
|
|
|
@@ -2698,13 +2697,7 @@ export function createStore(filePath, options = {}) {
|
|
|
2698
2697
|
const pending = data
|
|
2699
2698
|
.filter((u) => {
|
|
2700
2699
|
const updateCount = u.userUpdateCount;
|
|
2701
|
-
//
|
|
2702
|
-
const ttSellerEligible =
|
|
2703
|
-
u.ttSeller === null ||
|
|
2704
|
-
u.ttSeller === undefined ||
|
|
2705
|
-
u.ttSeller === "" ||
|
|
2706
|
-
u.ttSeller === 1;
|
|
2707
|
-
if (!ttSellerEligible) return false;
|
|
2700
|
+
// jobs_base 中所有 user_update_count <= 0 的记录都可以领取
|
|
2708
2701
|
if (
|
|
2709
2702
|
updateCount === null ||
|
|
2710
2703
|
updateCount === undefined ||
|
|
@@ -3137,20 +3130,32 @@ export function createStore(filePath, options = {}) {
|
|
|
3137
3130
|
return videos.length;
|
|
3138
3131
|
}
|
|
3139
3132
|
|
|
3140
|
-
function getPendingCommentTasks(limit) {
|
|
3133
|
+
function getPendingCommentTasks(limit, locations = null) {
|
|
3141
3134
|
if (getDb()) {
|
|
3142
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
|
+
|
|
3143
3147
|
const rows = getDb()
|
|
3144
3148
|
.prepare(
|
|
3145
3149
|
`
|
|
3146
3150
|
SELECT *
|
|
3147
3151
|
FROM videos
|
|
3148
|
-
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}
|
|
3149
3154
|
ORDER BY tt_seller DESC, registered_at ASC
|
|
3150
3155
|
LIMIT ?
|
|
3151
3156
|
`,
|
|
3152
3157
|
)
|
|
3153
|
-
.all(l);
|
|
3158
|
+
.all(...(normalizedLocations || []), l);
|
|
3154
3159
|
if (rows.length === 0) return [];
|
|
3155
3160
|
const bumpStmt = getDb().prepare(
|
|
3156
3161
|
`
|
|
@@ -3171,7 +3176,19 @@ export function createStore(filePath, options = {}) {
|
|
|
3171
3176
|
}
|
|
3172
3177
|
|
|
3173
3178
|
// 筛选待处理视频(userUpdateCount <= 0 或 null/undefined)
|
|
3174
|
-
|
|
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
|
+
}
|
|
3175
3192
|
// ttSeller=true 优先
|
|
3176
3193
|
pending.sort((a, b) => {
|
|
3177
3194
|
if (a.ttSeller && !b.ttSeller) return -1;
|
|
@@ -3395,14 +3412,13 @@ export function createStore(filePath, options = {}) {
|
|
|
3395
3412
|
};
|
|
3396
3413
|
}
|
|
3397
3414
|
|
|
3398
|
-
// 5. 验证:统计 jobs_base 中可被 attach 领取的记录数(
|
|
3415
|
+
// 5. 验证:统计 jobs_base 中可被 attach 领取的记录数(user_update_count<=0)
|
|
3399
3416
|
let available = 0;
|
|
3400
3417
|
try {
|
|
3401
3418
|
const row = db
|
|
3402
3419
|
.prepare(
|
|
3403
3420
|
`SELECT COUNT(*) as total FROM jobs_base
|
|
3404
|
-
WHERE
|
|
3405
|
-
AND COALESCE(user_update_count, 0) <= 0
|
|
3421
|
+
WHERE COALESCE(user_update_count, 0) <= 0
|
|
3406
3422
|
AND video_count = 0`,
|
|
3407
3423
|
)
|
|
3408
3424
|
.get();
|
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-stats.js
CHANGED
|
@@ -50,7 +50,7 @@ export function getDashboardStatsFromDb(targetLocations = []) {
|
|
|
50
50
|
|
|
51
51
|
const userUpdateTasksRow = db
|
|
52
52
|
.prepare(
|
|
53
|
-
`SELECT COUNT(*) as userUpdateTasks FROM jobs_base WHERE COALESCE(
|
|
53
|
+
`SELECT COUNT(*) as userUpdateTasks FROM jobs_base WHERE COALESCE(user_update_count, 0) <= 0`,
|
|
54
54
|
)
|
|
55
55
|
.get();
|
|
56
56
|
|
|
@@ -147,7 +147,7 @@ export function getUserUpdateByCountryFromDb() {
|
|
|
147
147
|
.prepare(
|
|
148
148
|
`SELECT COALESCE(guessed_location, '未知') as country, COUNT(*) as count
|
|
149
149
|
FROM jobs_base
|
|
150
|
-
WHERE
|
|
150
|
+
WHERE COALESCE(user_update_count, 0) <= 0
|
|
151
151
|
GROUP BY COALESCE(guessed_location, '未知')
|
|
152
152
|
ORDER BY count DESC`,
|
|
153
153
|
)
|
|
@@ -161,7 +161,7 @@ export function getAttachStuckByCountryFromDb() {
|
|
|
161
161
|
.prepare(
|
|
162
162
|
`SELECT COALESCE(guessed_location, '未知') as country, COUNT(*) as count
|
|
163
163
|
FROM jobs_base
|
|
164
|
-
WHERE
|
|
164
|
+
WHERE COALESCE(user_update_count, 0) = 1
|
|
165
165
|
GROUP BY COALESCE(guessed_location, '未知')
|
|
166
166
|
ORDER BY count DESC`,
|
|
167
167
|
)
|
|
@@ -194,7 +194,7 @@ export function restoreAttachStuckByCountry(country) {
|
|
|
194
194
|
};
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
const whereSql = `COALESCE(
|
|
197
|
+
const whereSql = `COALESCE(user_update_count, 0) = 1 AND COALESCE(guessed_location, '未知') = ?`;
|
|
198
198
|
const count =
|
|
199
199
|
db
|
|
200
200
|
.prepare(`SELECT COUNT(*) as c FROM jobs_base WHERE ${whereSql}`)
|
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/public/app.js
CHANGED
|
@@ -163,7 +163,6 @@ function renderStats() {
|
|
|
163
163
|
if (statJobsBase)
|
|
164
164
|
statJobsBase.textContent = `jobs_base: ${formatStatNum(d.jobsBaseTotal || 0)}`;
|
|
165
165
|
flashEl("statError", d.errorUsers);
|
|
166
|
-
flashEl("statRestricted", d.restrictedUsers);
|
|
167
166
|
flashEl("statTarget", d.targetUsers, { full: true });
|
|
168
167
|
flashEl("statUserUpdateTasks", d.userUpdateTasks || 0, { full: true });
|
|
169
168
|
flashEl("statRawJobs", d.rawJobs || 0);
|
|
@@ -376,7 +375,6 @@ function renderSourceChart(sources) {
|
|
|
376
375
|
following: "关注",
|
|
377
376
|
follower: "粉丝",
|
|
378
377
|
processed: "已处理",
|
|
379
|
-
restricted: "受限(跳过)",
|
|
380
378
|
error: "错误(待重试)",
|
|
381
379
|
noVideo: "无视频",
|
|
382
380
|
};
|
|
@@ -419,13 +417,10 @@ function renderTable(users) {
|
|
|
419
417
|
const nowStatus = u.status;
|
|
420
418
|
const changed =
|
|
421
419
|
wasStatus !== nowStatus &&
|
|
422
|
-
(nowStatus === "done" ||
|
|
423
|
-
nowStatus === "restricted" ||
|
|
424
|
-
nowStatus === "error");
|
|
420
|
+
(nowStatus === "done" || nowStatus === "error");
|
|
425
421
|
const rowClass = changed ? ' class="row-flash"' : "";
|
|
426
422
|
|
|
427
423
|
const statusTags = {
|
|
428
|
-
restricted: '<span class="tag error">受限(跳过)</span>',
|
|
429
424
|
error: '<span class="tag error">错误(待重试)</span>',
|
|
430
425
|
done: '<span class="tag processed">已完成</span>',
|
|
431
426
|
processing: '<span class="tag processing">处理中</span>',
|
|
@@ -991,33 +986,6 @@ async function resetJob(uniqueId) {
|
|
|
991
986
|
}
|
|
992
987
|
}
|
|
993
988
|
|
|
994
|
-
async function moveRestrictedToBase() {
|
|
995
|
-
const count = currentStats?.restrictedUsers || 0;
|
|
996
|
-
if (count === 0) {
|
|
997
|
-
showToast("没有受限用户");
|
|
998
|
-
return;
|
|
999
|
-
}
|
|
1000
|
-
if (!confirm(`确定将 ${count} 个受限用户移到 jobs_base 吗?`)) return;
|
|
1001
|
-
showLoading("迁移中...");
|
|
1002
|
-
try {
|
|
1003
|
-
const res = await fetch("/api/jobs/move-restricted-to-base", {
|
|
1004
|
-
method: "POST",
|
|
1005
|
-
});
|
|
1006
|
-
const data = await res.json();
|
|
1007
|
-
if (data.error) {
|
|
1008
|
-
showToast(data.error, true);
|
|
1009
|
-
return;
|
|
1010
|
-
}
|
|
1011
|
-
showToast(`已迁移 ${data.moved} 个受限用户`);
|
|
1012
|
-
fetchStats();
|
|
1013
|
-
fetchUsers();
|
|
1014
|
-
} catch (e) {
|
|
1015
|
-
showToast("迁移失败: " + e.message, true);
|
|
1016
|
-
} finally {
|
|
1017
|
-
hideLoading();
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
989
|
async function batchResetErrors() {
|
|
1022
990
|
const errorUsers = currentUsers.filter((u) => u.status === "error");
|
|
1023
991
|
if (errorUsers.length === 0) {
|
|
@@ -1762,8 +1730,6 @@ function renderTargetTable() {
|
|
|
1762
1730
|
statusTag = '<span class="tag pending">待处理</span>';
|
|
1763
1731
|
else if (u.status === "error")
|
|
1764
1732
|
statusTag = '<span class="tag error">错误</span>';
|
|
1765
|
-
else if (u.status === "restricted")
|
|
1766
|
-
statusTag = '<span class="tag error">受限</span>';
|
|
1767
1733
|
else statusTag = u.status || "-";
|
|
1768
1734
|
|
|
1769
1735
|
return `<tr data-user="${u.uniqueId}">
|
|
@@ -41,10 +41,6 @@
|
|
|
41
41
|
<div class="label">错误</div>
|
|
42
42
|
<div class="value error" id="statError">0</div>
|
|
43
43
|
</div>
|
|
44
|
-
<div class="stat-card clickable" onclick="moveRestrictedToBase()">
|
|
45
|
-
<div class="label">受限</div>
|
|
46
|
-
<div class="value error" id="statRestricted">0</div>
|
|
47
|
-
</div>
|
|
48
44
|
<div class="stat-card clickable pending-card" id="statUserUpdateCard" onclick="navigateToUserUpdate()">
|
|
49
45
|
<div class="label">待补资料</div>
|
|
50
46
|
<div class="value target" id="statUserUpdateTasks">0</div>
|
|
@@ -122,7 +118,6 @@
|
|
|
122
118
|
<button data-filter="pending" onclick="setFilter('pending')">待处理</button>
|
|
123
119
|
<button data-filter="done" onclick="setFilter('done')">已完成</button>
|
|
124
120
|
<button data-filter="error" onclick="setFilter('error')">错误</button>
|
|
125
|
-
<button data-filter="restricted" onclick="setFilter('restricted')">受限</button>
|
|
126
121
|
<button data-filter="target" onclick="setFilter('target')" style="background:#7c3aed;color:#fff">目标商家</button>
|
|
127
122
|
<select id="targetLocationFilter" onchange="onTargetLocationChange()"
|
|
128
123
|
style="padding:6px 10px;border:1px solid #7c3aed;border-radius:6px;background:#2a2a3a;color:#ccc;font-size:12px;cursor:pointer;outline:none;display:none">
|