tt-help-cli-ycl 1.4.27 → 1.4.29

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.
@@ -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
- // COUNT 缓存:134 万条数据全表扫描慢,5 秒内返回缓存值
249
- const cacheKey = whereSql + "|" + args.join(",");
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 < 5000) {
282
+ if (cachedCount && Date.now() - cachedCount.time < 30000) {
255
283
  total = cachedCount.c;
256
284
  } else {
257
285
  total = getDb()
258
- .prepare(`SELECT COUNT(*) as c FROM jobs ${whereSql}`)
259
- .get(...args).c;
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
- // 只查询前端需要的列,避免 SELECT * 带来的大字段传输和 mapJobRow 开销
267
- const rows = getDb()
268
- .prepare(
269
- `
270
- SELECT
271
- unique_id, nickname, sec_uid, status, sources,
272
- tt_seller, verified, follower_count, following_count,
273
- location_created, latest_video_time, refresh_time,
274
- guessed_location, pinned, processed_at, video_count,
275
- no_video, claimed_by, claimed_at, created_at, updated_at
276
- FROM jobs
277
- ${whereSql}
278
- ORDER BY
279
- pinned DESC,
280
- CASE
281
- WHEN ? = 'done' THEN COALESCE(processed_at, 0) * -1
282
- WHEN ? = '1' THEN COALESCE(refresh_time, 0) * -1
283
- ELSE 0
284
- END ASC,
285
- CASE status
286
- WHEN 'processing' THEN 0
287
- WHEN 'pending' THEN 1
288
- WHEN 'done' THEN 2
289
- WHEN 'error' THEN 3
290
- WHEN 'restricted' THEN 4
291
- ELSE 9
292
- END ASC,
293
- COALESCE(follower_count, 0) DESC,
294
- COALESCE(processed_at, 0) DESC,
295
- unique_id ASC
296
- LIMIT ? OFFSET ?
297
- `,
298
- )
299
- .all(...args, status || "", target, safeLimit, safeOffset)
300
- .map(mapJobRow);
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 jobs
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 jobs SET claimed_at = ? WHERE unique_id = ?")
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 jobs WHERE status = 'processing'
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 jobs SET status = 'pending', claimed_at = NULL WHERE unique_id = ?",
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 jobs WHERE ${conditions}`,
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 jobs SET status = 'processing', claimed_at = ?, claimed_by = ? WHERE unique_id = ?",
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 jobs
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 jobs
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 jobs
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 jobs
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
- const persistRet = updateJobInfo(uniqueId, user, false);
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
- const ret = updateJobInfo(uniqueId, user, false);
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
- const ret = updateJobInfo(uniqueId, { pinned: nextPinned }, false);
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().prepare("DELETE FROM jobs WHERE unique_id = ?").run(safeId);
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 jobs (
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, registered_at ASC
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(nextCount, videoId);
3514
- return { ok: true, userUpdateCount: nextCount };
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
  /**