tt-help-cli-ycl 1.4.27 → 1.4.28
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/scripts/search-authors.mjs +21 -0
- package/scripts/sync-raw-jobs.mjs +450 -181
- package/src/cli/raw-sync.js +2 -5
- package/src/cli/search.js +160 -0
- package/src/lib/args.js +81 -8
- package/src/lib/constants.js +24 -5
- package/src/lib/search-collector.js +194 -0
- package/src/lib/target-locations.js +1 -0
- package/src/main.js +3 -0
- package/src/npm-main.js +3 -0
- package/src/watch/data-store.js +266 -577
- package/src/watch/db-columns.js +1 -0
- package/src/watch/db-crud.js +27 -3
- package/src/watch/db-raw-jobs.js +3 -3
- package/src/watch/db-schema.js +100 -0
- package/src/watch/db-stats.js +46 -23
- package/src/watch/db-tags.js +100 -1
- package/src/watch/llm-config.js +145 -1
- package/src/watch/llm-scoring.js +60 -117
- package/src/watch/public/app.js +5 -5
- package/src/watch/public/index.html +4 -5
- package/src/watch/public/style.css +16 -0
- package/src/watch/server.js +170 -59
- package/src/watch/tag-service.js +11 -67
package/src/watch/data-store.js
CHANGED
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
getJobRow,
|
|
51
51
|
getJobBaseRow,
|
|
52
52
|
getJob,
|
|
53
|
+
getUserTable,
|
|
53
54
|
getBulkAuthorLocations,
|
|
54
55
|
getAllJobs,
|
|
55
56
|
getVideoRow,
|
|
@@ -102,14 +103,13 @@ import {
|
|
|
102
103
|
claimRefreshTag,
|
|
103
104
|
reportRefreshResult,
|
|
104
105
|
getRefreshStats,
|
|
106
|
+
getProductiveSearchTask,
|
|
107
|
+
claimSearchTag,
|
|
108
|
+
reportSearchResult,
|
|
105
109
|
} from "./db-tags.js";
|
|
106
110
|
|
|
107
111
|
// LLM 打分
|
|
108
|
-
import {
|
|
109
|
-
scoreJobLocation,
|
|
110
|
-
scoreJobsBatch,
|
|
111
|
-
createLlmOffsetStore,
|
|
112
|
-
} from "./llm-scoring.js";
|
|
112
|
+
import { scoreJobLocation, scoreJobsBatch } from "./llm-scoring.js";
|
|
113
113
|
|
|
114
114
|
// ===== 薄包装函数(保持外部 API 不变)=====
|
|
115
115
|
|
|
@@ -245,59 +245,87 @@ function getUsersPageFromDb({
|
|
|
245
245
|
|
|
246
246
|
const whereSql = where.length ? `WHERE ${where.join(" AND ")}` : "";
|
|
247
247
|
|
|
248
|
-
|
|
249
|
-
|
|
248
|
+
const selectCols = `unique_id, nickname, sec_uid, status, sources,
|
|
249
|
+
tt_seller, verified, follower_count, following_count,
|
|
250
|
+
location_created, latest_video_time, refresh_time,
|
|
251
|
+
guessed_location, pinned, processed_at, video_count,
|
|
252
|
+
no_video, claimed_by, claimed_at, created_at, updated_at`;
|
|
253
|
+
|
|
254
|
+
// ── 排序子句 ──
|
|
255
|
+
// task_queue: 处理中优先于待处理
|
|
256
|
+
// jobs: 单状态表,无需状态 CASE(所有行状态相同)
|
|
257
|
+
const tqOrder = `pinned DESC, CASE status WHEN 'processing' THEN 0 ELSE 1 END, COALESCE(processed_at, 0) DESC, COALESCE(follower_count, 0) DESC, unique_id ASC`;
|
|
258
|
+
const jobsOrder = `pinned DESC, COALESCE(processed_at, 0) DESC, COALESCE(follower_count, 0) DESC, unique_id ASC`;
|
|
259
|
+
|
|
260
|
+
// ── COUNT 查询 ──
|
|
261
|
+
let countSql;
|
|
262
|
+
let countArgs = args;
|
|
263
|
+
|
|
264
|
+
if (status === "pending" || status === "processing") {
|
|
265
|
+
countSql = `SELECT COUNT(*) as c FROM task_queue ${whereSql}`;
|
|
266
|
+
} else if (
|
|
267
|
+
status === "done" ||
|
|
268
|
+
status === "error" ||
|
|
269
|
+
status === "restricted"
|
|
270
|
+
) {
|
|
271
|
+
countSql = `SELECT COUNT(*) as c FROM jobs ${whereSql}`;
|
|
272
|
+
} else {
|
|
273
|
+
// all: 只查 task_queue(处理中+待处理),不碰 jobs 表
|
|
274
|
+
countSql = `SELECT COUNT(*) as c FROM task_queue ${whereSql}`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const cacheKey = whereSql + "|" + args.join(",") + "|" + (status || "all");
|
|
250
278
|
if (!getUsersPageFromDb._countCache)
|
|
251
279
|
getUsersPageFromDb._countCache = new Map();
|
|
252
280
|
const cachedCount = getUsersPageFromDb._countCache.get(cacheKey);
|
|
253
281
|
let total;
|
|
254
|
-
if (cachedCount && Date.now() - cachedCount.time <
|
|
282
|
+
if (cachedCount && Date.now() - cachedCount.time < 30000) {
|
|
255
283
|
total = cachedCount.c;
|
|
256
284
|
} else {
|
|
257
285
|
total = getDb()
|
|
258
|
-
.prepare(
|
|
259
|
-
.get(...
|
|
286
|
+
.prepare(countSql)
|
|
287
|
+
.get(...countArgs).c;
|
|
260
288
|
getUsersPageFromDb._countCache.set(cacheKey, {
|
|
261
289
|
c: total,
|
|
262
290
|
time: Date.now(),
|
|
263
291
|
});
|
|
264
292
|
}
|
|
265
293
|
|
|
266
|
-
//
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
294
|
+
// ── 数据查询:单表直查 + 简化排序,all 拆分为独立查询 JS 合并 ──
|
|
295
|
+
let rows;
|
|
296
|
+
|
|
297
|
+
if (status === "pending" || status === "processing") {
|
|
298
|
+
// 单表:直接查,处理中排前
|
|
299
|
+
const sql = `SELECT ${selectCols} FROM task_queue ${whereSql}
|
|
300
|
+
ORDER BY ${tqOrder}
|
|
301
|
+
LIMIT ? OFFSET ?`;
|
|
302
|
+
rows = getDb()
|
|
303
|
+
.prepare(sql)
|
|
304
|
+
.all(...args, safeLimit, safeOffset)
|
|
305
|
+
.map(mapJobRow);
|
|
306
|
+
} else if (
|
|
307
|
+
status === "done" ||
|
|
308
|
+
status === "error" ||
|
|
309
|
+
status === "restricted"
|
|
310
|
+
) {
|
|
311
|
+
// 单表:直接查
|
|
312
|
+
const sql = `SELECT ${selectCols} FROM jobs ${whereSql}
|
|
313
|
+
ORDER BY ${jobsOrder}
|
|
314
|
+
LIMIT ? OFFSET ?`;
|
|
315
|
+
rows = getDb()
|
|
316
|
+
.prepare(sql)
|
|
317
|
+
.all(...args, safeLimit, safeOffset)
|
|
318
|
+
.map(mapJobRow);
|
|
319
|
+
} else {
|
|
320
|
+
// all: 只查 task_queue(处理中+待处理),不碰 jobs 表
|
|
321
|
+
const sql = `SELECT ${selectCols} FROM task_queue ${whereSql}
|
|
322
|
+
ORDER BY ${tqOrder}
|
|
323
|
+
LIMIT ? OFFSET ?`;
|
|
324
|
+
rows = getDb()
|
|
325
|
+
.prepare(sql)
|
|
326
|
+
.all(...args, safeLimit, safeOffset)
|
|
327
|
+
.map(mapJobRow);
|
|
328
|
+
}
|
|
301
329
|
|
|
302
330
|
return {
|
|
303
331
|
total,
|
|
@@ -670,14 +698,6 @@ export function createStore(filePath, options = {}) {
|
|
|
670
698
|
}
|
|
671
699
|
|
|
672
700
|
// refillJobsFromRaw 的 LLM 打分配置(自动补充任务时使用)
|
|
673
|
-
const refillLlmConfig = {
|
|
674
|
-
llmScore: false,
|
|
675
|
-
llmMinScore: 60,
|
|
676
|
-
llmSampleSize: 100,
|
|
677
|
-
llmTagRatio: 0.4,
|
|
678
|
-
llmCommentRatio: 0.3,
|
|
679
|
-
...options.refillLlm,
|
|
680
|
-
};
|
|
681
701
|
|
|
682
702
|
let data = [];
|
|
683
703
|
// uniqueId → index 内存索引,O(1) 查找
|
|
@@ -687,16 +707,12 @@ export function createStore(filePath, options = {}) {
|
|
|
687
707
|
let clientLoginStatus = new Map();
|
|
688
708
|
// 活跃客户端追踪:clientId → { type, ip, port, userId, lastSeen }
|
|
689
709
|
let activeClients = new Map();
|
|
690
|
-
// refill 锁:防止多个 claimNextJob 同时触发 LLM refill
|
|
691
|
-
let refillLock = null; // Promise | null
|
|
692
710
|
// LLM 采样偏移量记忆:按猜测国家记录上次查询位置,避免重复采样
|
|
693
711
|
// 格式: { "ES": 300, "PL": 500, "NL": 400 }
|
|
694
|
-
const offsetStore = createLlmOffsetStore();
|
|
695
712
|
if (filePath) {
|
|
696
713
|
// 初始化 SQLite 用户表(用于判重)
|
|
697
714
|
initDb(filePath);
|
|
698
715
|
// 从数据库恢复偏移量
|
|
699
|
-
offsetStore.load();
|
|
700
716
|
}
|
|
701
717
|
|
|
702
718
|
// stats 缓存
|
|
@@ -860,7 +876,6 @@ export function createStore(filePath, options = {}) {
|
|
|
860
876
|
// 数据库模式:先保存 LLM 偏移量,再备份数据库
|
|
861
877
|
if (getDb() && getDbPath()) {
|
|
862
878
|
try {
|
|
863
|
-
offsetStore.save();
|
|
864
879
|
} catch (e) {
|
|
865
880
|
console.error(`[data-store] 保存 LLM 偏移量失败: ${e.message}`);
|
|
866
881
|
}
|
|
@@ -1082,443 +1097,6 @@ export function createStore(filePath, options = {}) {
|
|
|
1082
1097
|
return data;
|
|
1083
1098
|
}
|
|
1084
1099
|
|
|
1085
|
-
/**
|
|
1086
|
-
* 从 raw_jobs 中移动一批符合条件的任务到 jobs 表
|
|
1087
|
-
* @param {string[]} locations - 目标国家列表(null 表示不限制)
|
|
1088
|
-
* @param {number} limit - 每次移动的最大数量,默认 500
|
|
1089
|
-
* @param {Object} options - 可选配置
|
|
1090
|
-
* @param {boolean} options.llmScore - 是否启用 LLM 打分过滤,默认 false
|
|
1091
|
-
* @param {number} options.llmMinScore - LLM 最低分数阈值,默认 60
|
|
1092
|
-
* @param {number} options.llmSampleSize - LLM 打分的采样数量,默认 100
|
|
1093
|
-
* @returns {{ moved: number }} 实际移动的数量
|
|
1094
|
-
*/
|
|
1095
|
-
function refillJobsFromRaw(locations = null, limit = 500, options = {}) {
|
|
1096
|
-
if (!getDb()) {
|
|
1097
|
-
return { moved: 0, error: "getDb() not ready" };
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
const safeLimit = Math.max(1, Math.min(2000, parseInt(limit) || 500));
|
|
1101
|
-
const normalizedLocations = locations
|
|
1102
|
-
? locations.map((loc) => String(loc).trim().toUpperCase()).filter(Boolean)
|
|
1103
|
-
: null;
|
|
1104
|
-
|
|
1105
|
-
const useLlm = !!options.llmScore;
|
|
1106
|
-
const llmMinScore = options.llmMinScore ?? 60;
|
|
1107
|
-
const llmSampleSize = options.llmSampleSize ?? 100;
|
|
1108
|
-
|
|
1109
|
-
// 构建 WHERE 条件
|
|
1110
|
-
const conditions = [
|
|
1111
|
-
"COALESCE(video_count, 0) > 6",
|
|
1112
|
-
"COALESCE(follower_count, 0) > 0",
|
|
1113
|
-
"COALESCE(following_count, 0) > 0",
|
|
1114
|
-
];
|
|
1115
|
-
const args = [];
|
|
1116
|
-
|
|
1117
|
-
if (normalizedLocations && normalizedLocations.length > 0) {
|
|
1118
|
-
conditions.push(
|
|
1119
|
-
`(UPPER(COALESCE(guessed_location, '')) IN (${normalizedLocations.map(() => "?").join(", ")}) OR COALESCE(guessed_location, '') = '')`,
|
|
1120
|
-
);
|
|
1121
|
-
args.push(...normalizedLocations);
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
const whereSql = conditions.join(" AND ");
|
|
1125
|
-
|
|
1126
|
-
// 统计符合条件的数量
|
|
1127
|
-
const count =
|
|
1128
|
-
getDb()
|
|
1129
|
-
.prepare(`SELECT COUNT(*) as c FROM raw_jobs WHERE ${whereSql}`)
|
|
1130
|
-
.get(...args)?.c || 0;
|
|
1131
|
-
|
|
1132
|
-
if (!count) {
|
|
1133
|
-
return { moved: 0 };
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
// 如果启用 LLM 打分:tag 和 comment 免打分直接入围,其余走 LLM 打分
|
|
1137
|
-
if (useLlm && normalizedLocations && normalizedLocations.length > 0) {
|
|
1138
|
-
const llmTotal = options.llmTotal ?? 200; // 总条数
|
|
1139
|
-
const llmTagRatio = options.llmTagRatio ?? 0.4; // tag 占比 40%
|
|
1140
|
-
const llmCommentRatio = options.llmCommentRatio ?? 0.3; // comment 占比 30%
|
|
1141
|
-
const llmTagLimit = Math.floor(llmTotal * llmTagRatio); // tag 上限
|
|
1142
|
-
const llmCommentLimit = Math.floor(llmTotal * llmCommentRatio); // comment 上限
|
|
1143
|
-
const llmLLMTarget = llmTotal - llmTagLimit - llmCommentLimit; // LLM 打分目标
|
|
1144
|
-
const llmMinScore = options.llmMinScore ?? 60;
|
|
1145
|
-
const llmSampleSize = options.llmSampleSize ?? 100;
|
|
1146
|
-
const maxBatches = options.llmMaxBatches ?? 10;
|
|
1147
|
-
|
|
1148
|
-
console.error(
|
|
1149
|
-
`[data-store] LLM 调度开始: 总目标 ${llmTotal} 条,tag ${llmTagLimit} 条,comment ${llmCommentLimit} 条(免打分),LLM 打分 ${llmLLMTarget} 条`,
|
|
1150
|
-
);
|
|
1151
|
-
|
|
1152
|
-
// 返回 Promise,调用方需要 await
|
|
1153
|
-
return (async () => {
|
|
1154
|
-
const allTagQualified = [];
|
|
1155
|
-
const allCommentQualified = [];
|
|
1156
|
-
const allLLMQualified = [];
|
|
1157
|
-
const allScores = [];
|
|
1158
|
-
|
|
1159
|
-
// ===== 第一步:一次性取 tag(全局,最多 llmTagLimit 条,免打分)=====
|
|
1160
|
-
let tagOffset = offsetStore.get("_tag") || 0;
|
|
1161
|
-
const tagGlobalCount =
|
|
1162
|
-
getDb()
|
|
1163
|
-
.prepare(
|
|
1164
|
-
`SELECT COUNT(*) as c FROM raw_jobs WHERE ${whereSql} AND sources LIKE '%tag%'`,
|
|
1165
|
-
)
|
|
1166
|
-
.get(...args)?.c || 0;
|
|
1167
|
-
|
|
1168
|
-
if (tagOffset >= tagGlobalCount) {
|
|
1169
|
-
tagOffset = 0;
|
|
1170
|
-
offsetStore.set("_tag", 0);
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
console.error(
|
|
1174
|
-
`[data-store] Tag 全局共 ${tagGlobalCount} 条,从偏移量 ${tagOffset} 开始`,
|
|
1175
|
-
);
|
|
1176
|
-
|
|
1177
|
-
while (
|
|
1178
|
-
allTagQualified.length < llmTagLimit &&
|
|
1179
|
-
tagOffset < tagGlobalCount
|
|
1180
|
-
) {
|
|
1181
|
-
const batch = getDb()
|
|
1182
|
-
.prepare(
|
|
1183
|
-
`
|
|
1184
|
-
SELECT * FROM raw_jobs WHERE ${whereSql} AND sources LIKE '%tag%'
|
|
1185
|
-
ORDER BY COALESCE(video_count, 0) DESC, created_at DESC
|
|
1186
|
-
LIMIT ? OFFSET ?
|
|
1187
|
-
`,
|
|
1188
|
-
)
|
|
1189
|
-
.all(
|
|
1190
|
-
...args,
|
|
1191
|
-
Math.min(llmSampleSize, llmTagLimit - allTagQualified.length),
|
|
1192
|
-
tagOffset,
|
|
1193
|
-
);
|
|
1194
|
-
|
|
1195
|
-
if (!batch.length) break;
|
|
1196
|
-
|
|
1197
|
-
allTagQualified.push(...batch.map((s) => s.unique_id));
|
|
1198
|
-
tagOffset += batch.length;
|
|
1199
|
-
|
|
1200
|
-
console.error(
|
|
1201
|
-
`[data-store] Tag 本批 ${batch.length} 条,累计 ${allTagQualified.length}/${llmTagLimit}`,
|
|
1202
|
-
);
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
offsetStore.set("_tag", tagOffset);
|
|
1206
|
-
|
|
1207
|
-
// 让出事件循环,避免长时间阻塞监控页面及其他客户端请求
|
|
1208
|
-
await new Promise(setImmediate);
|
|
1209
|
-
|
|
1210
|
-
// ===== 第二步:一次性取 comment(全局,最多 llmCommentLimit 条,免打分)=====
|
|
1211
|
-
let commentOffset = offsetStore.get("_comment") || 0;
|
|
1212
|
-
const commentGlobalCount =
|
|
1213
|
-
getDb()
|
|
1214
|
-
.prepare(
|
|
1215
|
-
`SELECT COUNT(*) as c FROM raw_jobs WHERE ${whereSql} AND sources LIKE '%comment%' AND sources NOT LIKE '%tag%'`,
|
|
1216
|
-
)
|
|
1217
|
-
.get(...args)?.c || 0;
|
|
1218
|
-
|
|
1219
|
-
if (commentOffset >= commentGlobalCount) {
|
|
1220
|
-
commentOffset = 0;
|
|
1221
|
-
offsetStore.set("_comment", 0);
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
console.error(
|
|
1225
|
-
`[data-store] Comment 全局共 ${commentGlobalCount} 条,从偏移量 ${commentOffset} 开始`,
|
|
1226
|
-
);
|
|
1227
|
-
|
|
1228
|
-
while (
|
|
1229
|
-
allCommentQualified.length < llmCommentLimit &&
|
|
1230
|
-
commentOffset < commentGlobalCount
|
|
1231
|
-
) {
|
|
1232
|
-
const batch = getDb()
|
|
1233
|
-
.prepare(
|
|
1234
|
-
`
|
|
1235
|
-
SELECT * FROM raw_jobs WHERE ${whereSql} AND sources LIKE '%comment%' AND sources NOT LIKE '%tag%'
|
|
1236
|
-
ORDER BY COALESCE(video_count, 0) DESC, created_at DESC
|
|
1237
|
-
LIMIT ? OFFSET ?
|
|
1238
|
-
`,
|
|
1239
|
-
)
|
|
1240
|
-
.all(
|
|
1241
|
-
...args,
|
|
1242
|
-
Math.min(
|
|
1243
|
-
llmSampleSize,
|
|
1244
|
-
llmCommentLimit - allCommentQualified.length,
|
|
1245
|
-
),
|
|
1246
|
-
commentOffset,
|
|
1247
|
-
);
|
|
1248
|
-
|
|
1249
|
-
if (!batch.length) break;
|
|
1250
|
-
|
|
1251
|
-
allCommentQualified.push(...batch.map((s) => s.unique_id));
|
|
1252
|
-
commentOffset += batch.length;
|
|
1253
|
-
|
|
1254
|
-
console.error(
|
|
1255
|
-
`[data-store] Comment 本批 ${batch.length} 条,累计 ${allCommentQualified.length}/${llmCommentLimit}`,
|
|
1256
|
-
);
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
offsetStore.set("_comment", commentOffset);
|
|
1260
|
-
|
|
1261
|
-
// 让出事件循环,避免长时间阻塞监控页面及其他客户端请求
|
|
1262
|
-
await new Promise(setImmediate);
|
|
1263
|
-
|
|
1264
|
-
// ===== 第三步:按国家取非 tag 非 comment,走 LLM 打分,直到合格数达到 llmLLMTarget =====
|
|
1265
|
-
for (const location of normalizedLocations) {
|
|
1266
|
-
if (allLLMQualified.length >= llmLLMTarget) break;
|
|
1267
|
-
|
|
1268
|
-
const llmOffsetKey = `${location}:llm`;
|
|
1269
|
-
let offset = offsetStore.get(llmOffsetKey) || 0;
|
|
1270
|
-
|
|
1271
|
-
const locationArgs = [...args, location];
|
|
1272
|
-
const llmCount =
|
|
1273
|
-
getDb()
|
|
1274
|
-
.prepare(
|
|
1275
|
-
`SELECT COUNT(*) as c FROM raw_jobs WHERE ${whereSql} AND (guessed_location = ? OR COALESCE(guessed_location, '') = '') AND (sources NOT LIKE '%tag%' OR sources IS NULL) AND (sources NOT LIKE '%comment%' OR sources IS NULL)`,
|
|
1276
|
-
)
|
|
1277
|
-
.get(...locationArgs)?.c || 0;
|
|
1278
|
-
|
|
1279
|
-
if (llmCount === 0) {
|
|
1280
|
-
console.error(
|
|
1281
|
-
`[data-store] 国家 ${location}: 无 LLM 打分候选数据,跳过`,
|
|
1282
|
-
);
|
|
1283
|
-
continue;
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
if (offset >= llmCount) {
|
|
1287
|
-
offset = 0;
|
|
1288
|
-
offsetStore.set(llmOffsetKey, 0);
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
console.error(
|
|
1292
|
-
`[data-store] 国家 ${location}: LLM 候选共 ${llmCount} 条,从偏移量 ${offset} 开始`,
|
|
1293
|
-
);
|
|
1294
|
-
|
|
1295
|
-
for (let batch = 0; batch < maxBatches; batch++) {
|
|
1296
|
-
if (allLLMQualified.length >= llmLLMTarget) break;
|
|
1297
|
-
|
|
1298
|
-
const samples = getDb()
|
|
1299
|
-
.prepare(
|
|
1300
|
-
`
|
|
1301
|
-
SELECT * FROM raw_jobs WHERE ${whereSql} AND (guessed_location = ? OR COALESCE(guessed_location, '') = '')
|
|
1302
|
-
AND (sources NOT LIKE '%tag%' OR sources IS NULL)
|
|
1303
|
-
AND (sources NOT LIKE '%comment%' OR sources IS NULL)
|
|
1304
|
-
ORDER BY COALESCE(video_count, 0) DESC, created_at DESC
|
|
1305
|
-
LIMIT ? OFFSET ?
|
|
1306
|
-
`,
|
|
1307
|
-
)
|
|
1308
|
-
.all(...locationArgs, llmSampleSize, offset);
|
|
1309
|
-
|
|
1310
|
-
if (!samples.length) break;
|
|
1311
|
-
|
|
1312
|
-
// 让出事件循环,LLM API 调用期间允许处理其他请求
|
|
1313
|
-
await new Promise(setImmediate);
|
|
1314
|
-
|
|
1315
|
-
const scores = await scoreJobsBatch(
|
|
1316
|
-
samples,
|
|
1317
|
-
DEFAULT_TARGET_LOCATIONS,
|
|
1318
|
-
);
|
|
1319
|
-
const qualified = scores.filter((s) => s.score >= llmMinScore);
|
|
1320
|
-
allLLMQualified.push(...qualified.map((s) => s.uniqueId));
|
|
1321
|
-
allScores.push(...scores);
|
|
1322
|
-
|
|
1323
|
-
offset += samples.length;
|
|
1324
|
-
offsetStore.set(llmOffsetKey, offset);
|
|
1325
|
-
|
|
1326
|
-
console.error(
|
|
1327
|
-
`[data-store] ${location} 第 ${batch + 1} 批: 采样 ${samples.length} 条,本批合格 ${qualified.length} 条,LLM 累计 ${allLLMQualified.length}/${llmLLMTarget}`,
|
|
1328
|
-
);
|
|
1329
|
-
}
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
// ===== 最终结果 =====
|
|
1333
|
-
const qualified = [
|
|
1334
|
-
...allTagQualified,
|
|
1335
|
-
...allCommentQualified,
|
|
1336
|
-
...allLLMQualified,
|
|
1337
|
-
];
|
|
1338
|
-
|
|
1339
|
-
if (!qualified.length) {
|
|
1340
|
-
console.error(
|
|
1341
|
-
`[data-store] LLM 调度后无符合条件的任务(tag: ${allTagQualified.length}, comment: ${allCommentQualified.length}, LLM: ${allLLMQualified.length})`,
|
|
1342
|
-
);
|
|
1343
|
-
return {
|
|
1344
|
-
moved: 0,
|
|
1345
|
-
scored: allScores.length,
|
|
1346
|
-
qualified: 0,
|
|
1347
|
-
scores: allScores,
|
|
1348
|
-
};
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
console.error(
|
|
1352
|
-
`[data-store] LLM 调度完成: tag ${allTagQualified.length} + comment ${allCommentQualified.length} + LLM ${allLLMQualified.length} = 共 ${qualified.length} 条`,
|
|
1353
|
-
);
|
|
1354
|
-
|
|
1355
|
-
// 让出事件循环,避免事务写入阻塞监控页面
|
|
1356
|
-
await new Promise(setImmediate);
|
|
1357
|
-
|
|
1358
|
-
// 移动符合条件的记录
|
|
1359
|
-
const placeholders = qualified.map(() => "?").join(", ");
|
|
1360
|
-
const moveTxn = getDb().transaction(() => {
|
|
1361
|
-
getDb()
|
|
1362
|
-
.prepare(
|
|
1363
|
-
`
|
|
1364
|
-
INSERT OR IGNORE INTO jobs (
|
|
1365
|
-
unique_id, nickname, status, sources, pinned,
|
|
1366
|
-
tt_seller, verified, video_count, comment_count,
|
|
1367
|
-
guessed_location, location_created, confirmed_location,
|
|
1368
|
-
follower_count, following_count, heart_count,
|
|
1369
|
-
created_at, updated_at, region, signature, bio_link, sec_uid,
|
|
1370
|
-
status_code, latest_video_time
|
|
1371
|
-
)
|
|
1372
|
-
SELECT
|
|
1373
|
-
unique_id, nickname, 'pending', sources, pinned,
|
|
1374
|
-
tt_seller, verified, video_count, comment_count,
|
|
1375
|
-
guessed_location, location_created, confirmed_location,
|
|
1376
|
-
follower_count, following_count, heart_count,
|
|
1377
|
-
created_at, updated_at, region, signature, bio_link, sec_uid,
|
|
1378
|
-
status_code, latest_video_time
|
|
1379
|
-
FROM raw_jobs
|
|
1380
|
-
WHERE unique_id IN (${placeholders})
|
|
1381
|
-
`,
|
|
1382
|
-
)
|
|
1383
|
-
.run(...qualified);
|
|
1384
|
-
|
|
1385
|
-
getDb()
|
|
1386
|
-
.prepare(
|
|
1387
|
-
`DELETE FROM raw_jobs WHERE unique_id IN (${placeholders})`,
|
|
1388
|
-
)
|
|
1389
|
-
.run(...qualified);
|
|
1390
|
-
});
|
|
1391
|
-
moveTxn();
|
|
1392
|
-
markStatsDirty();
|
|
1393
|
-
|
|
1394
|
-
// 持久化偏移量到数据库
|
|
1395
|
-
offsetStore.save();
|
|
1396
|
-
|
|
1397
|
-
// 打印最终偏移量状态
|
|
1398
|
-
const finalOffsetSummary = Array.from(offsetStore.entries())
|
|
1399
|
-
.map(([k, v]) => `${k}:${v}`)
|
|
1400
|
-
.join(", ");
|
|
1401
|
-
console.error(`[data-store] 偏移量记忆更新: ${finalOffsetSummary}`);
|
|
1402
|
-
|
|
1403
|
-
return {
|
|
1404
|
-
moved: qualified.length,
|
|
1405
|
-
scored: allScores.length,
|
|
1406
|
-
qualified: qualified.length,
|
|
1407
|
-
scores: allScores,
|
|
1408
|
-
};
|
|
1409
|
-
})();
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
// 常规移动:多国家时先按国家均衡补充,再用全局兜底补齐剩余额度
|
|
1413
|
-
const insertFromRawSql = `
|
|
1414
|
-
INSERT OR IGNORE INTO jobs (
|
|
1415
|
-
unique_id, nickname, status, sources, pinned,
|
|
1416
|
-
tt_seller, verified, video_count, comment_count,
|
|
1417
|
-
guessed_location, location_created, confirmed_location,
|
|
1418
|
-
follower_count, following_count, heart_count,
|
|
1419
|
-
created_at, updated_at, region, signature, bio_link, sec_uid,
|
|
1420
|
-
status_code, latest_video_time, user_create_time
|
|
1421
|
-
)
|
|
1422
|
-
SELECT
|
|
1423
|
-
unique_id, nickname, 'pending', sources, pinned,
|
|
1424
|
-
tt_seller, verified, video_count, comment_count,
|
|
1425
|
-
guessed_location, location_created, confirmed_location,
|
|
1426
|
-
follower_count, following_count, heart_count,
|
|
1427
|
-
created_at, updated_at, region, signature, bio_link, sec_uid,
|
|
1428
|
-
status_code, latest_video_time, user_create_time
|
|
1429
|
-
FROM raw_jobs
|
|
1430
|
-
WHERE __WHERE__
|
|
1431
|
-
ORDER BY
|
|
1432
|
-
CASE WHEN sources LIKE '%tag%' THEN 0 ELSE 1 END,
|
|
1433
|
-
CASE WHEN sources LIKE '%comment%' THEN 0 ELSE 1 END,
|
|
1434
|
-
COALESCE(video_count, 0) DESC, created_at DESC
|
|
1435
|
-
LIMIT ?
|
|
1436
|
-
`;
|
|
1437
|
-
|
|
1438
|
-
const deleteFromRawSql = `
|
|
1439
|
-
DELETE FROM raw_jobs
|
|
1440
|
-
WHERE unique_id IN (
|
|
1441
|
-
SELECT unique_id FROM raw_jobs
|
|
1442
|
-
WHERE __WHERE__
|
|
1443
|
-
ORDER BY
|
|
1444
|
-
CASE WHEN sources LIKE '%tag%' THEN 0 ELSE 1 END,
|
|
1445
|
-
CASE WHEN sources LIKE '%comment%' THEN 0 ELSE 1 END,
|
|
1446
|
-
COALESCE(video_count, 0) DESC, created_at DESC
|
|
1447
|
-
LIMIT ?
|
|
1448
|
-
)
|
|
1449
|
-
`;
|
|
1450
|
-
|
|
1451
|
-
const uniqueLocations = normalizedLocations
|
|
1452
|
-
? Array.from(new Set(normalizedLocations))
|
|
1453
|
-
: [];
|
|
1454
|
-
const shouldBalanceByCountry = uniqueLocations.length > 1;
|
|
1455
|
-
|
|
1456
|
-
const moveTxn = getDb().transaction(() => {
|
|
1457
|
-
let moved = 0;
|
|
1458
|
-
const movedByCountry = {};
|
|
1459
|
-
|
|
1460
|
-
if (shouldBalanceByCountry) {
|
|
1461
|
-
const baseQuota = Math.floor(safeLimit / uniqueLocations.length);
|
|
1462
|
-
const remainder = safeLimit % uniqueLocations.length;
|
|
1463
|
-
|
|
1464
|
-
for (let i = 0; i < uniqueLocations.length; i++) {
|
|
1465
|
-
if (moved >= safeLimit) break;
|
|
1466
|
-
|
|
1467
|
-
const location = uniqueLocations[i];
|
|
1468
|
-
const quota = baseQuota + (i < remainder ? 1 : 0);
|
|
1469
|
-
const currentLimit = Math.max(0, Math.min(quota, safeLimit - moved));
|
|
1470
|
-
if (!currentLimit) continue;
|
|
1471
|
-
|
|
1472
|
-
const locationWhere = `${whereSql} AND (UPPER(COALESCE(guessed_location, '')) = ? OR COALESCE(guessed_location, '') = '')`;
|
|
1473
|
-
const locationArgs = [...args, location];
|
|
1474
|
-
|
|
1475
|
-
getDb()
|
|
1476
|
-
.prepare(insertFromRawSql.replace("__WHERE__", locationWhere))
|
|
1477
|
-
.run(...locationArgs, currentLimit);
|
|
1478
|
-
const del = getDb()
|
|
1479
|
-
.prepare(deleteFromRawSql.replace("__WHERE__", locationWhere))
|
|
1480
|
-
.run(...locationArgs, currentLimit);
|
|
1481
|
-
|
|
1482
|
-
const movedThisCountry = del?.changes || 0;
|
|
1483
|
-
moved += movedThisCountry;
|
|
1484
|
-
movedByCountry[location] = movedThisCountry;
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
// 某些国家库存不足时,用全局查询补齐剩余额度(仍受 whereSql 国家范围约束)
|
|
1488
|
-
const remaining = safeLimit - moved;
|
|
1489
|
-
if (remaining > 0) {
|
|
1490
|
-
getDb()
|
|
1491
|
-
.prepare(insertFromRawSql.replace("__WHERE__", whereSql))
|
|
1492
|
-
.run(...args, remaining);
|
|
1493
|
-
const del = getDb()
|
|
1494
|
-
.prepare(deleteFromRawSql.replace("__WHERE__", whereSql))
|
|
1495
|
-
.run(...args, remaining);
|
|
1496
|
-
moved += del?.changes || 0;
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
console.error(
|
|
1500
|
-
`[data-store] refill 国家均衡: ${uniqueLocations
|
|
1501
|
-
.map((loc) => `${loc}:${movedByCountry[loc] || 0}`)
|
|
1502
|
-
.join(", ")} | total=${moved}`,
|
|
1503
|
-
);
|
|
1504
|
-
} else {
|
|
1505
|
-
getDb()
|
|
1506
|
-
.prepare(insertFromRawSql.replace("__WHERE__", whereSql))
|
|
1507
|
-
.run(...args, safeLimit);
|
|
1508
|
-
const del = getDb()
|
|
1509
|
-
.prepare(deleteFromRawSql.replace("__WHERE__", whereSql))
|
|
1510
|
-
.run(...args, safeLimit);
|
|
1511
|
-
moved = del?.changes || 0;
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
|
-
return moved;
|
|
1515
|
-
});
|
|
1516
|
-
|
|
1517
|
-
const moved = moveTxn();
|
|
1518
|
-
markStatsDirty();
|
|
1519
|
-
return { moved };
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
1100
|
async function claimNextJob(
|
|
1523
1101
|
userId,
|
|
1524
1102
|
expireMs = 5 * 60 * 1000,
|
|
@@ -1533,7 +1111,7 @@ export function createStore(filePath, options = {}) {
|
|
|
1533
1111
|
.prepare(
|
|
1534
1112
|
`
|
|
1535
1113
|
SELECT *
|
|
1536
|
-
FROM
|
|
1114
|
+
FROM task_queue
|
|
1537
1115
|
WHERE status = 'processing'
|
|
1538
1116
|
AND claimed_by = ?
|
|
1539
1117
|
AND claimed_at IS NOT NULL
|
|
@@ -1545,7 +1123,7 @@ export function createStore(filePath, options = {}) {
|
|
|
1545
1123
|
.get(userId, now, expireMs);
|
|
1546
1124
|
if (ongoingRow) {
|
|
1547
1125
|
getDb()
|
|
1548
|
-
.prepare("UPDATE
|
|
1126
|
+
.prepare("UPDATE task_queue SET claimed_at = ? WHERE unique_id = ?")
|
|
1549
1127
|
.run(now, ongoingRow.unique_id);
|
|
1550
1128
|
return {
|
|
1551
1129
|
uniqueId: ongoingRow.unique_id,
|
|
@@ -1566,7 +1144,7 @@ export function createStore(filePath, options = {}) {
|
|
|
1566
1144
|
// ---- 过期 processing 回收 ----
|
|
1567
1145
|
const expiredRow = getDb()
|
|
1568
1146
|
.prepare(
|
|
1569
|
-
`SELECT * FROM
|
|
1147
|
+
`SELECT * FROM task_queue WHERE status = 'processing'
|
|
1570
1148
|
AND claimed_at IS NOT NULL AND ? - claimed_at > ?
|
|
1571
1149
|
ORDER BY claimed_at ASC LIMIT 1`,
|
|
1572
1150
|
)
|
|
@@ -1575,7 +1153,7 @@ export function createStore(filePath, options = {}) {
|
|
|
1575
1153
|
if (expiredRow) {
|
|
1576
1154
|
getDb()
|
|
1577
1155
|
.prepare(
|
|
1578
|
-
"UPDATE
|
|
1156
|
+
"UPDATE task_queue SET status = 'pending', claimed_at = NULL WHERE unique_id = ?",
|
|
1579
1157
|
)
|
|
1580
1158
|
.run(expiredRow.unique_id);
|
|
1581
1159
|
expiredCandidate = expiredRow;
|
|
@@ -1637,7 +1215,7 @@ export function createStore(filePath, options = {}) {
|
|
|
1637
1215
|
|
|
1638
1216
|
const addPart = (prio, subPrio, conditions, params) => {
|
|
1639
1217
|
sqlParts.push(
|
|
1640
|
-
`SELECT *, ${prio} as _prio, ${subPrio} as _sub_prio FROM
|
|
1218
|
+
`SELECT *, ${prio} as _prio, ${subPrio} as _sub_prio FROM task_queue WHERE ${conditions}`,
|
|
1641
1219
|
);
|
|
1642
1220
|
allParams.push(...params);
|
|
1643
1221
|
};
|
|
@@ -1732,7 +1310,7 @@ export function createStore(filePath, options = {}) {
|
|
|
1732
1310
|
if (selectedRow) {
|
|
1733
1311
|
getDb()
|
|
1734
1312
|
.prepare(
|
|
1735
|
-
"UPDATE
|
|
1313
|
+
"UPDATE task_queue SET status = 'processing', claimed_at = ?, claimed_by = ? WHERE unique_id = ?",
|
|
1736
1314
|
)
|
|
1737
1315
|
.run(now, userId, selectedRow.unique_id);
|
|
1738
1316
|
markStatsDirty();
|
|
@@ -1744,32 +1322,7 @@ export function createStore(filePath, options = {}) {
|
|
|
1744
1322
|
};
|
|
1745
1323
|
}
|
|
1746
1324
|
|
|
1747
|
-
// refill
|
|
1748
|
-
if (!refillLock) {
|
|
1749
|
-
refillLock = (async () => {
|
|
1750
|
-
try {
|
|
1751
|
-
const result = refillJobsFromRaw(
|
|
1752
|
-
normalizedLocations.length ? normalizedLocations : null,
|
|
1753
|
-
500,
|
|
1754
|
-
refillLlmConfig,
|
|
1755
|
-
);
|
|
1756
|
-
const r =
|
|
1757
|
-
result && typeof result.then === "function"
|
|
1758
|
-
? await result
|
|
1759
|
-
: result;
|
|
1760
|
-
if (r.moved > 0) {
|
|
1761
|
-
console.error(
|
|
1762
|
-
`[data-store] 从 raw_jobs 补充了 ${r.moved} 条任务到 jobs`,
|
|
1763
|
-
);
|
|
1764
|
-
}
|
|
1765
|
-
} catch (e) {
|
|
1766
|
-
console.error(`[data-store] refill 异常: ${e.message}`);
|
|
1767
|
-
} finally {
|
|
1768
|
-
refillLock = null;
|
|
1769
|
-
}
|
|
1770
|
-
})();
|
|
1771
|
-
}
|
|
1772
|
-
|
|
1325
|
+
// refill 已移至 sync-raw-jobs.mjs 独立进程处理
|
|
1773
1326
|
return null;
|
|
1774
1327
|
}
|
|
1775
1328
|
|
|
@@ -1925,7 +1478,7 @@ export function createStore(filePath, options = {}) {
|
|
|
1925
1478
|
.prepare(
|
|
1926
1479
|
`
|
|
1927
1480
|
SELECT *
|
|
1928
|
-
FROM
|
|
1481
|
+
FROM task_queue
|
|
1929
1482
|
WHERE status = 'processing'
|
|
1930
1483
|
AND claimed_by = ?
|
|
1931
1484
|
AND claimed_at IS NOT NULL
|
|
@@ -2016,7 +1569,7 @@ export function createStore(filePath, options = {}) {
|
|
|
2016
1569
|
}
|
|
2017
1570
|
const sql = `
|
|
2018
1571
|
SELECT *
|
|
2019
|
-
FROM
|
|
1572
|
+
FROM task_queue
|
|
2020
1573
|
WHERE ${where.join(" AND ")}
|
|
2021
1574
|
ORDER BY follower_count DESC, created_at ASC, unique_id ASC
|
|
2022
1575
|
LIMIT 1
|
|
@@ -2059,7 +1612,7 @@ export function createStore(filePath, options = {}) {
|
|
|
2059
1612
|
}
|
|
2060
1613
|
const sql = `
|
|
2061
1614
|
SELECT *
|
|
2062
|
-
FROM
|
|
1615
|
+
FROM task_queue
|
|
2063
1616
|
WHERE ${where.join(" AND ")}
|
|
2064
1617
|
ORDER BY created_at ASC, unique_id ASC
|
|
2065
1618
|
LIMIT 1
|
|
@@ -2072,7 +1625,7 @@ export function createStore(filePath, options = {}) {
|
|
|
2072
1625
|
|
|
2073
1626
|
const expiredSql = `
|
|
2074
1627
|
SELECT *
|
|
2075
|
-
FROM
|
|
1628
|
+
FROM task_queue
|
|
2076
1629
|
WHERE status = 'processing'
|
|
2077
1630
|
AND claimed_at IS NOT NULL
|
|
2078
1631
|
AND ? - claimed_at > ?
|
|
@@ -2423,10 +1976,20 @@ export function createStore(filePath, options = {}) {
|
|
|
2423
1976
|
updateUserFromResult(user, result);
|
|
2424
1977
|
user.claimedAt = null;
|
|
2425
1978
|
const newUsers = processDiscoveredUsers(result);
|
|
2426
|
-
|
|
1979
|
+
// Phase 1: 先更新 task_queue,再搬移到 jobs 归档,最后从 task_queue 删除
|
|
1980
|
+
const persistRet = updateJobInfo(uniqueId, user, false, "task_queue");
|
|
2427
1981
|
if (persistRet.error) {
|
|
2428
1982
|
return { saved: false, error: persistRet.error };
|
|
2429
1983
|
}
|
|
1984
|
+
// 整行搬移到 jobs 归档
|
|
1985
|
+
getDb()
|
|
1986
|
+
.prepare(
|
|
1987
|
+
`INSERT OR REPLACE INTO jobs SELECT * FROM task_queue WHERE unique_id = ?`,
|
|
1988
|
+
)
|
|
1989
|
+
.run(uniqueId);
|
|
1990
|
+
getDb()
|
|
1991
|
+
.prepare(`DELETE FROM task_queue WHERE unique_id = ?`)
|
|
1992
|
+
.run(uniqueId);
|
|
2430
1993
|
return { saved: true, status: user.status, newUsers };
|
|
2431
1994
|
}
|
|
2432
1995
|
|
|
@@ -2502,8 +2065,24 @@ export function createStore(filePath, options = {}) {
|
|
|
2502
2065
|
user.error = null;
|
|
2503
2066
|
user.restricted = false;
|
|
2504
2067
|
user.noVideo = false;
|
|
2505
|
-
|
|
2068
|
+
|
|
2069
|
+
// Phase 2: 判断用户在哪个表,更新后确保 pending 行在 task_queue
|
|
2070
|
+
const inTq = getDb()
|
|
2071
|
+
.prepare("SELECT 1 FROM task_queue WHERE unique_id = ?")
|
|
2072
|
+
.get(uniqueId);
|
|
2073
|
+
const tableName = inTq ? "task_queue" : "jobs";
|
|
2074
|
+
const ret = updateJobInfo(uniqueId, user, false, tableName);
|
|
2506
2075
|
if (ret.error) return { saved: false, error: ret.error };
|
|
2076
|
+
|
|
2077
|
+
// 如果原来在 jobs,重置后需移入 task_queue(pending 应在热表)
|
|
2078
|
+
if (!inTq) {
|
|
2079
|
+
getDb()
|
|
2080
|
+
.prepare(
|
|
2081
|
+
"INSERT OR REPLACE INTO task_queue SELECT * FROM jobs WHERE unique_id = ?",
|
|
2082
|
+
)
|
|
2083
|
+
.run(uniqueId);
|
|
2084
|
+
getDb().prepare("DELETE FROM jobs WHERE unique_id = ?").run(uniqueId);
|
|
2085
|
+
}
|
|
2507
2086
|
markStatsDirty();
|
|
2508
2087
|
return { saved: true };
|
|
2509
2088
|
}
|
|
@@ -2527,7 +2106,17 @@ export function createStore(filePath, options = {}) {
|
|
|
2527
2106
|
const user = getJob(uniqueId);
|
|
2528
2107
|
if (!user) return { saved: false, error: "user not found" };
|
|
2529
2108
|
const nextPinned = !user.pinned;
|
|
2530
|
-
|
|
2109
|
+
// Phase 2: 判断用户在哪个表
|
|
2110
|
+
const inTq = getDb()
|
|
2111
|
+
.prepare("SELECT 1 FROM task_queue WHERE unique_id = ?")
|
|
2112
|
+
.get(uniqueId);
|
|
2113
|
+
const tableName = inTq ? "task_queue" : "jobs";
|
|
2114
|
+
const ret = updateJobInfo(
|
|
2115
|
+
uniqueId,
|
|
2116
|
+
{ pinned: nextPinned },
|
|
2117
|
+
false,
|
|
2118
|
+
tableName,
|
|
2119
|
+
);
|
|
2531
2120
|
if (ret.error) return { saved: false, error: ret.error };
|
|
2532
2121
|
return { saved: true, pinned: nextPinned };
|
|
2533
2122
|
}
|
|
@@ -3097,35 +2686,60 @@ export function createStore(filePath, options = {}) {
|
|
|
3097
2686
|
const safeId = String(uniqueId).trim();
|
|
3098
2687
|
if (!safeId) return false;
|
|
3099
2688
|
|
|
2689
|
+
const rawCols = [
|
|
2690
|
+
"unique_id",
|
|
2691
|
+
"nickname",
|
|
2692
|
+
"status",
|
|
2693
|
+
"sources",
|
|
2694
|
+
"claimed_by",
|
|
2695
|
+
"claimed_at",
|
|
2696
|
+
"error",
|
|
2697
|
+
"pinned",
|
|
2698
|
+
"no_video",
|
|
2699
|
+
"restricted",
|
|
2700
|
+
"user_update_count",
|
|
2701
|
+
"tt_seller",
|
|
2702
|
+
"verified",
|
|
2703
|
+
"video_count",
|
|
2704
|
+
"comment_count",
|
|
2705
|
+
"guessed_location",
|
|
2706
|
+
"location_created",
|
|
2707
|
+
"confirmed_location",
|
|
2708
|
+
"modified_at",
|
|
2709
|
+
"follower_count",
|
|
2710
|
+
"following_count",
|
|
2711
|
+
"heart_count",
|
|
2712
|
+
"refresh_time",
|
|
2713
|
+
"processed",
|
|
2714
|
+
"processed_at",
|
|
2715
|
+
"created_at",
|
|
2716
|
+
"updated_at",
|
|
2717
|
+
"region",
|
|
2718
|
+
"signature",
|
|
2719
|
+
"bio_link",
|
|
2720
|
+
"sec_uid",
|
|
2721
|
+
"status_code",
|
|
2722
|
+
"latest_video_time",
|
|
2723
|
+
"user_create_time",
|
|
2724
|
+
].join(", ");
|
|
2725
|
+
|
|
3100
2726
|
const moveSingleTxn = getDb().transaction(() => {
|
|
2727
|
+
// Phase 2: 先查 task_queue,再查 jobs
|
|
2728
|
+
const fromTable = getDb()
|
|
2729
|
+
.prepare("SELECT 1 FROM task_queue WHERE unique_id = ?")
|
|
2730
|
+
.get(safeId)
|
|
2731
|
+
? "task_queue"
|
|
2732
|
+
: "jobs";
|
|
2733
|
+
|
|
3101
2734
|
getDb()
|
|
3102
2735
|
.prepare(
|
|
3103
|
-
`
|
|
3104
|
-
INSERT OR REPLACE INTO raw_jobs (
|
|
3105
|
-
unique_id, nickname, status, sources, claimed_by, claimed_at,
|
|
3106
|
-
error, pinned, no_video, restricted, user_update_count,
|
|
3107
|
-
tt_seller, verified, video_count, comment_count,
|
|
3108
|
-
guessed_location, location_created, confirmed_location, modified_at,
|
|
3109
|
-
follower_count, following_count, heart_count, refresh_time,
|
|
3110
|
-
processed, processed_at, created_at, updated_at,
|
|
3111
|
-
region, signature, bio_link, sec_uid, status_code, latest_video_time,
|
|
3112
|
-
user_create_time
|
|
3113
|
-
)
|
|
3114
|
-
SELECT
|
|
3115
|
-
unique_id, nickname, status, sources, claimed_by, claimed_at,
|
|
3116
|
-
error, pinned, no_video, restricted, user_update_count,
|
|
3117
|
-
tt_seller, verified, video_count, comment_count,
|
|
3118
|
-
guessed_location, location_created, confirmed_location, modified_at,
|
|
3119
|
-
follower_count, following_count, heart_count, refresh_time,
|
|
3120
|
-
processed, processed_at, created_at, updated_at,
|
|
3121
|
-
region, signature, bio_link, sec_uid, status_code, latest_video_time,
|
|
3122
|
-
user_create_time
|
|
3123
|
-
FROM jobs WHERE unique_id = ?
|
|
3124
|
-
`,
|
|
2736
|
+
`INSERT OR REPLACE INTO raw_jobs (${rawCols}) SELECT ${rawCols} FROM ${fromTable} WHERE unique_id = ?`,
|
|
3125
2737
|
)
|
|
3126
2738
|
.run(safeId);
|
|
3127
2739
|
|
|
3128
|
-
getDb()
|
|
2740
|
+
getDb()
|
|
2741
|
+
.prepare(`DELETE FROM ${fromTable} WHERE unique_id = ?`)
|
|
2742
|
+
.run(safeId);
|
|
3129
2743
|
});
|
|
3130
2744
|
moveSingleTxn();
|
|
3131
2745
|
return true;
|
|
@@ -3192,7 +2806,7 @@ export function createStore(filePath, options = {}) {
|
|
|
3192
2806
|
getDb()
|
|
3193
2807
|
.prepare(
|
|
3194
2808
|
`
|
|
3195
|
-
INSERT OR REPLACE INTO
|
|
2809
|
+
INSERT OR REPLACE INTO task_queue (
|
|
3196
2810
|
unique_id, nickname, status, sources, claimed_by, claimed_at,
|
|
3197
2811
|
error, pinned, no_video, restricted, user_update_count,
|
|
3198
2812
|
tt_seller, verified, video_count, comment_count,
|
|
@@ -3423,6 +3037,7 @@ export function createStore(filePath, options = {}) {
|
|
|
3423
3037
|
}
|
|
3424
3038
|
|
|
3425
3039
|
function getPendingCommentTasks(limit, locations = null) {
|
|
3040
|
+
const STALE_TIMEOUT_MS = 10 * 60 * 1000; // 10分钟超时
|
|
3426
3041
|
if (getDb()) {
|
|
3427
3042
|
const l = Math.max(1, parseInt(limit) || 1);
|
|
3428
3043
|
const normalizedLocations =
|
|
@@ -3436,33 +3051,40 @@ export function createStore(filePath, options = {}) {
|
|
|
3436
3051
|
? `AND UPPER(COALESCE(location_created, '')) IN (${normalizedLocations.map(() => "?").join(", ")})`
|
|
3437
3052
|
: "";
|
|
3438
3053
|
|
|
3054
|
+
const staleThreshold = Date.now() - STALE_TIMEOUT_MS;
|
|
3439
3055
|
const rows = getDb()
|
|
3440
3056
|
.prepare(
|
|
3441
3057
|
`
|
|
3442
3058
|
SELECT *
|
|
3443
3059
|
FROM videos
|
|
3444
3060
|
WHERE (user_update_count IS NULL OR user_update_count <= 0)
|
|
3061
|
+
OR (user_update_count > 0 AND (claimed_at IS NULL OR claimed_at < ?))
|
|
3445
3062
|
${locationFilter}
|
|
3446
|
-
ORDER BY tt_seller DESC,
|
|
3063
|
+
ORDER BY tt_seller DESC,
|
|
3064
|
+
CASE WHEN user_update_count > 0 THEN 1 ELSE 0 END,
|
|
3065
|
+
registered_at ASC
|
|
3447
3066
|
LIMIT ?
|
|
3448
3067
|
`,
|
|
3449
3068
|
)
|
|
3450
|
-
.all(...(normalizedLocations || []), l);
|
|
3069
|
+
.all(staleThreshold, ...(normalizedLocations || []), l);
|
|
3451
3070
|
if (rows.length === 0) return [];
|
|
3071
|
+
const now = Date.now();
|
|
3452
3072
|
const bumpStmt = getDb().prepare(
|
|
3453
3073
|
`
|
|
3454
3074
|
UPDATE videos
|
|
3455
|
-
SET user_update_count = COALESCE(user_update_count, 0) + 1
|
|
3075
|
+
SET user_update_count = COALESCE(user_update_count, 0) + 1,
|
|
3076
|
+
claimed_at = ?
|
|
3456
3077
|
WHERE id = ?
|
|
3457
3078
|
`,
|
|
3458
3079
|
);
|
|
3459
3080
|
const bumpTxn = getDb().transaction((items) => {
|
|
3460
|
-
for (const item of items) bumpStmt.run(item.id);
|
|
3081
|
+
for (const item of items) bumpStmt.run(now, item.id);
|
|
3461
3082
|
});
|
|
3462
3083
|
bumpTxn(rows);
|
|
3463
3084
|
return rows.map((row) => {
|
|
3464
3085
|
const mapped = mapVideoRow(row);
|
|
3465
3086
|
mapped.userUpdateCount = (mapped.userUpdateCount || 0) + 1;
|
|
3087
|
+
mapped.claimedAt = now;
|
|
3466
3088
|
return mapped;
|
|
3467
3089
|
});
|
|
3468
3090
|
}
|
|
@@ -3497,21 +3119,53 @@ export function createStore(filePath, options = {}) {
|
|
|
3497
3119
|
return tasks;
|
|
3498
3120
|
}
|
|
3499
3121
|
|
|
3122
|
+
/**
|
|
3123
|
+
* 将 videos_old 中符合条件的视频恢复到 videos(通用条件 + 分批)
|
|
3124
|
+
* @param {object} options
|
|
3125
|
+
* @param {number} options.limit - 每批最多移动条数
|
|
3126
|
+
* @param {string} options.where - SQL WHERE 条件(不含 WHERE 关键字),如 "tt_seller = 1"、"registered_at >= 1234567890"
|
|
3127
|
+
* @returns {{ moved: number, total: number }}
|
|
3128
|
+
*/
|
|
3129
|
+
function restoreVideosFromOld({ limit = 500, where = "" } = {}) {
|
|
3130
|
+
if (!getDb()) return { moved: 0, total: 0, error: "db not ready" };
|
|
3131
|
+
if (!where || !where.trim()) {
|
|
3132
|
+
return { moved: 0, total: 0, error: "no where condition specified" };
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
const whereSql = `WHERE ${where.trim()}`;
|
|
3136
|
+
const count =
|
|
3137
|
+
getDb().prepare(`SELECT COUNT(*) as c FROM videos_old ${whereSql}`).get()
|
|
3138
|
+
?.c || 0;
|
|
3139
|
+
if (!count) return { moved: 0, total: 0 };
|
|
3140
|
+
|
|
3141
|
+
const safeLimit = Math.min(limit, count);
|
|
3142
|
+
const moved = getDb().transaction(() => {
|
|
3143
|
+
getDb()
|
|
3144
|
+
.prepare(
|
|
3145
|
+
`INSERT OR REPLACE INTO videos SELECT * FROM videos_old ${whereSql} LIMIT ?`,
|
|
3146
|
+
)
|
|
3147
|
+
.run(safeLimit);
|
|
3148
|
+
return getDb()
|
|
3149
|
+
.prepare(
|
|
3150
|
+
`DELETE FROM videos_old WHERE id IN (SELECT id FROM videos_old ${whereSql} LIMIT ?)`,
|
|
3151
|
+
)
|
|
3152
|
+
.run(safeLimit).changes;
|
|
3153
|
+
})();
|
|
3154
|
+
|
|
3155
|
+
return { moved, total: count };
|
|
3156
|
+
}
|
|
3157
|
+
|
|
3500
3158
|
function commitCommentTask(videoId) {
|
|
3501
3159
|
if (getDb()) {
|
|
3502
3160
|
const video = getVideoRow(videoId);
|
|
3503
3161
|
if (!video) return { ok: false, error: "video not found" };
|
|
3504
|
-
const nextCount = (video.user_update_count || 0) + 1;
|
|
3505
3162
|
getDb()
|
|
3506
3163
|
.prepare(
|
|
3507
|
-
|
|
3508
|
-
UPDATE videos
|
|
3509
|
-
SET user_update_count = ?
|
|
3510
|
-
WHERE id = ?
|
|
3511
|
-
`,
|
|
3164
|
+
"INSERT OR REPLACE INTO videos_old SELECT * FROM videos WHERE id = ?",
|
|
3512
3165
|
)
|
|
3513
|
-
.run(
|
|
3514
|
-
|
|
3166
|
+
.run(videoId);
|
|
3167
|
+
getDb().prepare("DELETE FROM videos WHERE id = ?").run(videoId);
|
|
3168
|
+
return { ok: true };
|
|
3515
3169
|
}
|
|
3516
3170
|
|
|
3517
3171
|
const video = videos.find((v) => v.id === videoId);
|
|
@@ -3521,6 +3175,42 @@ export function createStore(filePath, options = {}) {
|
|
|
3521
3175
|
return { ok: true, userUpdateCount: video.userUpdateCount };
|
|
3522
3176
|
}
|
|
3523
3177
|
|
|
3178
|
+
/**
|
|
3179
|
+
* 将 videos 中符合条件的视频归档到 videos_old(通用条件 + 分批)
|
|
3180
|
+
* @param {object} options
|
|
3181
|
+
* @param {number} options.limit - 每批最多移动条数
|
|
3182
|
+
* @param {string} options.where - SQL WHERE 条件(不含 WHERE 关键字),如 "tt_seller = 0"、"user_update_count > 1"
|
|
3183
|
+
* @returns {{ moved: number, total: number }}
|
|
3184
|
+
*/
|
|
3185
|
+
function archiveVideosToOld({ limit = 500, where = "" } = {}) {
|
|
3186
|
+
if (!getDb()) return { moved: 0, total: 0, error: "db not ready" };
|
|
3187
|
+
if (!where || !where.trim()) {
|
|
3188
|
+
return { moved: 0, total: 0, error: "no where condition specified" };
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3191
|
+
const whereSql = `WHERE ${where.trim()}`;
|
|
3192
|
+
const count =
|
|
3193
|
+
getDb().prepare(`SELECT COUNT(*) as c FROM videos ${whereSql}`).get()
|
|
3194
|
+
?.c || 0;
|
|
3195
|
+
if (!count) return { moved: 0, total: 0 };
|
|
3196
|
+
|
|
3197
|
+
const safeLimit = Math.min(limit, count);
|
|
3198
|
+
const moved = getDb().transaction(() => {
|
|
3199
|
+
getDb()
|
|
3200
|
+
.prepare(
|
|
3201
|
+
`INSERT OR REPLACE INTO videos_old SELECT * FROM videos ${whereSql} LIMIT ?`,
|
|
3202
|
+
)
|
|
3203
|
+
.run(safeLimit);
|
|
3204
|
+
return getDb()
|
|
3205
|
+
.prepare(
|
|
3206
|
+
`DELETE FROM videos WHERE id IN (SELECT id FROM videos ${whereSql} LIMIT ?)`,
|
|
3207
|
+
)
|
|
3208
|
+
.run(safeLimit).changes;
|
|
3209
|
+
})();
|
|
3210
|
+
|
|
3211
|
+
return { moved, total: count };
|
|
3212
|
+
}
|
|
3213
|
+
|
|
3524
3214
|
return {
|
|
3525
3215
|
save,
|
|
3526
3216
|
flushSave,
|
|
@@ -3558,7 +3248,6 @@ export function createStore(filePath, options = {}) {
|
|
|
3558
3248
|
getStats,
|
|
3559
3249
|
getStatusGroups,
|
|
3560
3250
|
markGroupsDirty,
|
|
3561
|
-
refillJobsFromRaw,
|
|
3562
3251
|
scoreJobLocation,
|
|
3563
3252
|
scoreJobsBatch,
|
|
3564
3253
|
claimNextJob,
|
|
@@ -3592,11 +3281,12 @@ export function createStore(filePath, options = {}) {
|
|
|
3592
3281
|
getVideoCount,
|
|
3593
3282
|
getPendingCommentTasks,
|
|
3594
3283
|
commitCommentTask,
|
|
3284
|
+
restoreVideosFromOld,
|
|
3285
|
+
archiveVideosToOld,
|
|
3595
3286
|
debugClaimNextJob,
|
|
3596
3287
|
stopBackup,
|
|
3597
3288
|
backupDatabase, // 手动备份数据库
|
|
3598
3289
|
rawQuery,
|
|
3599
|
-
getLlmSampleOffsets, // 获取 LLM 采样偏移量状态
|
|
3600
3290
|
// Tag 发现与打分
|
|
3601
3291
|
insertTag,
|
|
3602
3292
|
insertTagsIfNew,
|
|
@@ -3615,14 +3305,13 @@ export function createStore(filePath, options = {}) {
|
|
|
3615
3305
|
reportRefreshResult,
|
|
3616
3306
|
getRefreshStats,
|
|
3617
3307
|
getTargetRefreshStats,
|
|
3308
|
+
// 搜索
|
|
3309
|
+
getProductiveSearchTask,
|
|
3310
|
+
claimSearchTag,
|
|
3311
|
+
reportSearchResult,
|
|
3618
3312
|
data,
|
|
3619
3313
|
};
|
|
3620
3314
|
|
|
3621
|
-
// 辅助函数:获取 LLM 采样偏移量
|
|
3622
|
-
function getLlmSampleOffsets() {
|
|
3623
|
-
return Object.fromEntries(offsetStore.entries());
|
|
3624
|
-
}
|
|
3625
|
-
|
|
3626
3315
|
// ===== 将 jobs/raw_jobs 中商家用户移动到 jobs_base =====
|
|
3627
3316
|
|
|
3628
3317
|
/**
|