tt-help-cli-ycl 1.4.132 → 1.4.135

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/src/cli/tag.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { writeFileSync, unlinkSync } from "fs";
2
2
  import { randomUUID } from "crypto";
3
3
  import { execSync } from "child_process";
4
+ import http from "http";
4
5
  import os from "os";
5
6
  import path from "path";
6
7
  import readline from "readline";
@@ -66,6 +67,38 @@ function isBrowserDisconnected(browser, e) {
66
67
  return false;
67
68
  }
68
69
 
70
+ /**
71
+ * 探测端口是否已被监听(CDP /json 端点)。
72
+ * 与 cdp.js 的 checkCDPPort 等价,但 tag.js 独立实现,不依赖未导出的内部函数。
73
+ * 用于端口池挑选时跳过被占用端口、以及关闭旧浏览器后确认端口已释放。
74
+ */
75
+ function checkPortListening(port) {
76
+ return new Promise((resolve) => {
77
+ const req = http.get(`http://127.0.0.1:${port}/json`, (res) => {
78
+ res.on("data", () => {});
79
+ res.on("end", () => resolve(res.statusCode === 200));
80
+ });
81
+ req.on("error", () => resolve(false));
82
+ req.setTimeout(2000, () => {
83
+ resolve(false);
84
+ req.destroy();
85
+ });
86
+ });
87
+ }
88
+
89
+ /**
90
+ * 等待端口不再被监听(浏览器进程真正退出、端口释放)。
91
+ * @returns {Promise<boolean>} 端口已释放返回 true
92
+ */
93
+ async function waitForPortReleased(port, timeoutMs = 10000) {
94
+ const deadline = Date.now() + timeoutMs;
95
+ while (Date.now() < deadline) {
96
+ if (!(await checkPortListening(port))) return true;
97
+ await new Promise((r) => setTimeout(r, 500));
98
+ }
99
+ return !(await checkPortListening(port));
100
+ }
101
+
69
102
  function formatMemoryUsage(mem = process.memoryUsage()) {
70
103
  return `rss:${(mem.rss / 1024 / 1024).toFixed(0)}MB heap:${(mem.heapUsed / 1024 / 1024).toFixed(0)}MB ext:${(mem.external / 1024 / 1024).toFixed(0)}MB ab:${(mem.arrayBuffers / 1024 / 1024).toFixed(0)}MB`;
71
104
  }
@@ -927,6 +960,9 @@ export async function handleScoreAll(parsed) {
927
960
 
928
961
  await setupPageRequestBlocking(page);
929
962
 
963
+ // 运行期用过的所有 CDP 端口(含换端口历史),退出时统一清理,防止旧浏览器窗口残留
964
+ const usedPorts = new Set([cdpPort]);
965
+
930
966
  // 端口池起始以 CLI --port / 环境变量 / 默认 7222 为准
931
967
  const portPoolStart = cdpPort;
932
968
  const portPoolSize = Math.max(
@@ -934,14 +970,21 @@ export async function handleScoreAll(parsed) {
934
970
  parseInt(process.env.TAG_SCOREALL_PORT_POOL_SIZE || "10", 10) || 10,
935
971
  );
936
972
 
937
- function pickNextPort(currentPort) {
973
+ async function pickNextPort(currentPort) {
938
974
  const candidates = [];
939
975
  for (let i = 0; i < portPoolSize; i++) {
940
976
  const p = portPoolStart + i;
941
977
  if (p !== currentPort) candidates.push(p);
942
978
  }
943
- if (candidates.length === 0) return currentPort;
944
- return candidates[Math.floor(Math.random() * candidates.length)];
979
+ // 跳过已被监听的端口(其他 score-all/attach 等程序的浏览器),
980
+ // 避免 attach 到别人的实例、后续 close/kill 时互相干扰导致浏览器多开
981
+ const freePorts = [];
982
+ for (const p of candidates) {
983
+ if (!(await checkPortListening(p))) freePorts.push(p);
984
+ }
985
+ const pool = freePorts.length > 0 ? freePorts : candidates;
986
+ if (pool.length === 0) return currentPort;
987
+ return pool[Math.floor(Math.random() * pool.length)];
945
988
  }
946
989
 
947
990
  // ── 内存回收配置 ──
@@ -996,9 +1039,10 @@ export async function handleScoreAll(parsed) {
996
1039
  const oldPort = cdpPort;
997
1040
  const shouldSwitch = switchPort && RECYCLE_CONFIG.switchPortOnRecycle;
998
1041
  if (shouldSwitch) {
999
- cdpPort = pickNextPort(cdpPort);
1042
+ cdpPort = await pickNextPort(cdpPort);
1000
1043
  cdpOpts.port = cdpPort;
1001
1044
  clientMeta.port = cdpPort;
1045
+ usedPorts.add(cdpPort);
1002
1046
  }
1003
1047
  const portHint =
1004
1048
  oldPort === cdpPort ? `port=${cdpPort}` : `port ${oldPort}→${cdpPort}`;
@@ -1007,10 +1051,21 @@ export async function handleScoreAll(parsed) {
1007
1051
  await page.close().catch(() => {});
1008
1052
  page = null;
1009
1053
  }
1010
- await browser.close().catch(() => {});
1054
+ // CDP 模式下 browser.close() 只断开连接不杀进程,必须显式 kill 旧端口实例;
1055
+ // kill 后确认端口已释放,再启动新实例,防止旧浏览器窗口残留(多开)
1011
1056
  if (oldPort !== cdpPort) {
1012
1057
  await killEdgeProcesses(null, oldPort).catch(() => {});
1058
+ if (!(await waitForPortReleased(oldPort))) {
1059
+ log(` ⚠️ 旧端口 ${oldPort} 未释放,再次尝试关闭...`);
1060
+ await killEdgeProcesses(null, oldPort).catch(() => {});
1061
+ if (!(await waitForPortReleased(oldPort, 5000))) {
1062
+ log(
1063
+ ` ⚠️ 旧端口 ${oldPort} 仍未释放,继续使用新端口(旧浏览器窗口可能残留,退出时会统一清理)`,
1064
+ );
1065
+ }
1066
+ }
1013
1067
  }
1068
+ await browser.close().catch(() => {});
1014
1069
  browser = await ensureBrowserReady(cdpOpts);
1015
1070
  page = await getOrCreatePage(browser);
1016
1071
  await setupPageRequestBlocking(page);
@@ -1103,10 +1158,14 @@ export async function handleScoreAll(parsed) {
1103
1158
  if (enrichScraper) await enrichScraper.close().catch(() => {});
1104
1159
  log("正在关闭 CDP 浏览器...");
1105
1160
  try {
1106
- await Promise.race([
1107
- killEdgeProcesses(null, cdpPort),
1108
- new Promise((r) => setTimeout(r, 5000)),
1109
- ]);
1161
+ // 逐个关闭运行期用过的所有端口(含换端口历史),防止旧浏览器窗口残留
1162
+ const ports = [...new Set([...usedPorts, cdpPort])];
1163
+ for (const p of ports) {
1164
+ await Promise.race([
1165
+ killEdgeProcesses(null, p),
1166
+ new Promise((r) => setTimeout(r, 5000)),
1167
+ ]).catch(() => {});
1168
+ }
1110
1169
  log("浏览器已关闭");
1111
1170
  } catch (e) {
1112
1171
  log(`关闭浏览器失败: ${e.message}`);
@@ -253,6 +253,27 @@ async function discoverTagsForCountryCli(
253
253
  process.stderr.write(
254
254
  ` [LLM] 正在生成标签 (国家: ${country}, 语言: ${getLang(country)}, 目标: ${count} 个, temp=${temperature}, health=${health?.healthStatus || "unknown"})...\n`,
255
255
  );
256
+ // 如果服务商被设置为 none,则跳过 LLM 调用并返回空结果
257
+ try {
258
+ const { getLlmConfig } = await import("../watch/llm-config.js");
259
+ const cfg = getLlmConfig();
260
+ if (cfg.provider === "none") {
261
+ process.stderr.write(` [LLM] 已禁用(provider=none),跳过生成\n`);
262
+ return {
263
+ country,
264
+ tags: [],
265
+ niche: null,
266
+ health: health || null,
267
+ autoNiche: null,
268
+ temperature,
269
+ hardRulesInjected: hardRules.length,
270
+ reflection: null,
271
+ };
272
+ }
273
+ } catch (e) {
274
+ // ignore, proceed to call LLM
275
+ }
276
+
256
277
  const content = await callLLM(prompt, temperature);
257
278
  const parsed = parseDiscoverResponse(content);
258
279
  const tags = parsed.tags || parseTagsFromResponse(content);
@@ -29,7 +29,10 @@ import {
29
29
  getDb,
30
30
  getDbPath,
31
31
  initDb,
32
+ initTagDb,
33
+ deriveTagDbPath,
32
34
  resetDbConnection,
35
+ resetTagDbConnection,
33
36
  loadLegacyUsersFromFiles,
34
37
  loadLegacyVideosFromFile,
35
38
  } from "./db-schema.js";
@@ -123,7 +126,9 @@ export function importLegacyJsonToDb({
123
126
  videosFilePath,
124
127
  }) {
125
128
  resetDbConnection();
129
+ resetTagDbConnection();
126
130
  initDb(dbFilePath);
131
+ initTagDb(deriveTagDbPath(dbFilePath));
127
132
 
128
133
  const db = getDb();
129
134
  const dbPath = getDbPath();
@@ -193,6 +198,7 @@ export function importLegacyJsonToDb({
193
198
 
194
199
  export function closeStoreDb() {
195
200
  resetDbConnection();
201
+ resetTagDbConnection();
196
202
  }
197
203
 
198
204
  function getUsersPageFromDb({
@@ -207,7 +213,7 @@ function getUsersPageFromDb({
207
213
  }) {
208
214
  if (!getDb()) return null;
209
215
 
210
- const safeLimit = Math.max(1, Math.min(200, parseInt(limit) || 50));
216
+ const safeLimit = Math.max(1, Math.min(50, parseInt(limit) || 50));
211
217
  const safeOffset = Math.max(0, parseInt(offset) || 0);
212
218
  const where = [];
213
219
  const args = [];
@@ -258,16 +264,20 @@ function getUsersPageFromDb({
258
264
  const jobsOrder = `pinned DESC, COALESCE(processed_at, 0) DESC, COALESCE(follower_count, 0) DESC, unique_id ASC`;
259
265
 
260
266
  // ── COUNT 查询 ──
267
+ // done 状态分布在 jobs(商家 done)+ jobs_done(非商家 done 冷归档),需 UNION
268
+ const doneUnionSql = `SELECT ${selectCols} FROM jobs ${whereSql}
269
+ UNION ALL
270
+ SELECT ${selectCols} FROM jobs_done ${whereSql}`;
261
271
  let countSql;
262
272
  let countArgs = args;
263
273
 
264
274
  if (status === "pending" || status === "processing") {
265
275
  countSql = `SELECT COUNT(*) as c FROM task_queue ${whereSql}`;
266
- } else if (
267
- status === "done" ||
268
- status === "error" ||
269
- status === "restricted"
270
- ) {
276
+ } else if (status === "done") {
277
+ countSql = `SELECT COUNT(*) as c FROM (${doneUnionSql})`;
278
+ // UNION 的两个子查询各自含 whereSql 占位符,参数需复制两份
279
+ countArgs = [...args, ...args];
280
+ } else if (status === "error" || status === "restricted") {
271
281
  countSql = `SELECT COUNT(*) as c FROM jobs ${whereSql}`;
272
282
  } else {
273
283
  // all: 只查 task_queue(处理中+待处理),不碰 jobs 表
@@ -303,11 +313,16 @@ function getUsersPageFromDb({
303
313
  .prepare(sql)
304
314
  .all(...args, safeLimit, safeOffset)
305
315
  .map(mapJobRow);
306
- } else if (
307
- status === "done" ||
308
- status === "error" ||
309
- status === "restricted"
310
- ) {
316
+ } else if (status === "done") {
317
+ // done 跨 jobs + jobs_done 两表(参数复制两份给两个子查询)
318
+ const sql = `SELECT * FROM (${doneUnionSql})
319
+ ORDER BY ${jobsOrder}
320
+ LIMIT ? OFFSET ?`;
321
+ rows = getDb()
322
+ .prepare(sql)
323
+ .all(...args, ...args, safeLimit, safeOffset)
324
+ .map(mapJobRow);
325
+ } else if (status === "error" || status === "restricted") {
311
326
  // 单表:直接查
312
327
  const sql = `SELECT ${selectCols} FROM jobs ${whereSql}
313
328
  ORDER BY ${jobsOrder}
@@ -752,6 +767,8 @@ export function createStore(filePath, options = {}) {
752
767
  if (filePath) {
753
768
  // 初始化 SQLite 用户表(用于判重)
754
769
  initDb(filePath);
770
+ // 初始化独立 tag 库(tags.db 由主库路径派生)
771
+ initTagDb(deriveTagDbPath(filePath));
755
772
  // 从数据库恢复偏移量
756
773
  }
757
774
 
@@ -778,7 +795,11 @@ export function createStore(filePath, options = {}) {
778
795
  .prepare(
779
796
  `
780
797
  SELECT status, COUNT(*) as count
781
- FROM jobs
798
+ FROM (
799
+ SELECT status FROM jobs
800
+ UNION ALL
801
+ SELECT status FROM jobs_done
802
+ )
782
803
  GROUP BY status
783
804
  `,
784
805
  )
@@ -2027,15 +2048,19 @@ export function createStore(filePath, options = {}) {
2027
2048
  updateUserFromResult(user, result);
2028
2049
  user.claimedAt = null;
2029
2050
  const newUsers = processDiscoveredUsers(result);
2030
- // Phase 1: 先更新 task_queue,再搬移到 jobs 归档,最后从 task_queue 删除
2051
+ // Phase 1: 先更新 task_queue,再搬移到归档表,最后从 task_queue 删除
2031
2052
  const persistRet = updateJobInfo(uniqueId, user, false, "task_queue");
2032
2053
  if (persistRet.error) {
2033
2054
  return { saved: false, error: persistRet.error };
2034
2055
  }
2035
- // 整行搬移到 jobs 归档
2056
+ // 分流:status='done' 且非商家(ttSeller 非 true)→ jobs_done 冷归档;
2057
+ // 其余(商家 done / error 等)→ jobs
2058
+ const isDoneNonSeller = user.status === "done" && !user.ttSeller;
2059
+ const targetTable = isDoneNonSeller ? "jobs_done" : "jobs";
2060
+ // 整行搬移到归档表
2036
2061
  getDb()
2037
2062
  .prepare(
2038
- `INSERT OR REPLACE INTO jobs SELECT * FROM task_queue WHERE unique_id = ?`,
2063
+ `INSERT OR REPLACE INTO ${targetTable} SELECT * FROM task_queue WHERE unique_id = ?`,
2039
2064
  )
2040
2065
  .run(uniqueId);
2041
2066
  getDb()
@@ -2135,18 +2160,24 @@ export function createStore(filePath, options = {}) {
2135
2160
  const inTq = getDb()
2136
2161
  .prepare("SELECT 1 FROM task_queue WHERE unique_id = ?")
2137
2162
  .get(uniqueId);
2138
- const tableName = inTq ? "task_queue" : "jobs";
2163
+ const inJobs = getDb()
2164
+ .prepare("SELECT 1 FROM jobs WHERE unique_id = ?")
2165
+ .get(uniqueId);
2166
+ // 非商家 done 在 jobs_done 冷归档,也支持直接重置回流程
2167
+ const tableName = inTq ? "task_queue" : inJobs ? "jobs" : "jobs_done";
2139
2168
  const ret = updateJobInfo(uniqueId, user, false, tableName);
2140
2169
  if (ret.error) return { saved: false, error: ret.error };
2141
2170
 
2142
- // 如果原来在 jobs,重置后需移入 task_queue(pending 应在热表)
2171
+ // 如果原来在 jobs / jobs_done,重置后需移入 task_queue(pending 应在热表)
2143
2172
  if (!inTq) {
2144
2173
  getDb()
2145
2174
  .prepare(
2146
- "INSERT OR REPLACE INTO task_queue SELECT * FROM jobs WHERE unique_id = ?",
2175
+ `INSERT OR REPLACE INTO task_queue SELECT * FROM ${tableName} WHERE unique_id = ?`,
2147
2176
  )
2148
2177
  .run(uniqueId);
2149
- getDb().prepare("DELETE FROM jobs WHERE unique_id = ?").run(uniqueId);
2178
+ getDb()
2179
+ .prepare(`DELETE FROM ${tableName} WHERE unique_id = ?`)
2180
+ .run(uniqueId);
2150
2181
  }
2151
2182
  markStatsDirty();
2152
2183
  return { saved: true };
@@ -2171,11 +2202,8 @@ export function createStore(filePath, options = {}) {
2171
2202
  const user = getJob(uniqueId);
2172
2203
  if (!user) return { saved: false, error: "user not found" };
2173
2204
  const nextPinned = !user.pinned;
2174
- // Phase 2: 判断用户在哪个表
2175
- const inTq = getDb()
2176
- .prepare("SELECT 1 FROM task_queue WHERE unique_id = ?")
2177
- .get(uniqueId);
2178
- const tableName = inTq ? "task_queue" : "jobs";
2205
+ // Phase 2: 判断用户在哪个表(task_queue / jobs / jobs_done)
2206
+ const tableName = getUserTable(uniqueId) || "jobs";
2179
2207
  const ret = updateJobInfo(
2180
2208
  uniqueId,
2181
2209
  { pinned: nextPinned },
@@ -2733,7 +2761,7 @@ export function createStore(filePath, options = {}) {
2733
2761
  return { ok: false, error: "db not initialized" };
2734
2762
  }
2735
2763
 
2736
- // ===== recheck-jobs:重检 jobs 表中 done 非商家用户 =====
2764
+ // ===== recheck-jobs:重检 jobs_done 表中 done 非商家用户 =====
2737
2765
 
2738
2766
  function getRecheckJobs(limit, countries) {
2739
2767
  const targetCountries = countries
@@ -2745,9 +2773,10 @@ export function createStore(filePath, options = {}) {
2745
2773
  const l = Math.max(1, parseInt(limit) || 5);
2746
2774
  const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
2747
2775
 
2776
+ // 非商家 done 已迁移到 jobs_done 冷归档表
2748
2777
  let sql = `
2749
2778
  SELECT *
2750
- FROM jobs
2779
+ FROM jobs_done
2751
2780
  WHERE status = 'done'
2752
2781
  AND (tt_seller IS NULL OR tt_seller != 1)
2753
2782
  AND (updated_at IS NULL OR updated_at < ?)
@@ -2770,7 +2799,7 @@ export function createStore(filePath, options = {}) {
2770
2799
 
2771
2800
  const now = Date.now();
2772
2801
  const bumpStmt = getDb().prepare(
2773
- `UPDATE jobs SET updated_at = ? WHERE unique_id = ?`,
2802
+ `UPDATE jobs_done SET updated_at = ? WHERE unique_id = ?`,
2774
2803
  );
2775
2804
  const bumpTxn = getDb().transaction((items) => {
2776
2805
  for (const item of items) {
@@ -2813,7 +2842,7 @@ export function createStore(filePath, options = {}) {
2813
2842
  const results = [];
2814
2843
  const now = Date.now();
2815
2844
  const bumpStmt = getDb().prepare(
2816
- `UPDATE jobs SET updated_at = ? WHERE unique_id = ?`,
2845
+ `UPDATE jobs_done SET updated_at = ? WHERE unique_id = ?`,
2817
2846
  );
2818
2847
  const txn = getDb().transaction((items) => {
2819
2848
  items.forEach((item) => {
@@ -2824,19 +2853,35 @@ export function createStore(filePath, options = {}) {
2824
2853
  bumpStmt.run(now, uniqueId);
2825
2854
  results.push({ uniqueId, ok: true, skipped: true });
2826
2855
  } else {
2827
- // 过滤 info 中的 null/undefined/空值,避免覆盖 jobs 表已有数据
2856
+ // 过滤 info 中的 null/undefined/空值,避免覆盖 jobs_done 表已有数据
2828
2857
  const cleanInfo = {};
2829
2858
  for (const [k, v] of Object.entries(info || {})) {
2830
2859
  if (v !== null && v !== undefined && v !== "") {
2831
2860
  cleanInfo[k] = v;
2832
2861
  }
2833
2862
  }
2834
- const ret = updateJobInfo(uniqueId, cleanInfo, true);
2835
- results.push(
2836
- ret.error
2837
- ? { uniqueId, error: ret.error }
2838
- : { uniqueId, ok: true },
2839
- );
2863
+ const ret = updateJobInfo(uniqueId, cleanInfo, true, "jobs_done");
2864
+ if (ret.error) {
2865
+ results.push({ uniqueId, error: ret.error });
2866
+ } else {
2867
+ // 重检后确认是商家(tt_seller=1)→ 从 jobs_done 回移到 jobs,
2868
+ // 保持"jobs_done=非商家 done / jobs=商家 done"的冷热表语义,
2869
+ // 否则该商家会从 targetUsers/product-fill/redo 等商家查询中消失
2870
+ const row = getDb()
2871
+ .prepare("SELECT tt_seller FROM jobs_done WHERE unique_id = ?")
2872
+ .get(uniqueId);
2873
+ if (row && row.tt_seller === 1) {
2874
+ getDb()
2875
+ .prepare(
2876
+ `INSERT OR REPLACE INTO jobs SELECT * FROM jobs_done WHERE unique_id = ?`,
2877
+ )
2878
+ .run(uniqueId);
2879
+ getDb()
2880
+ .prepare(`DELETE FROM jobs_done WHERE unique_id = ?`)
2881
+ .run(uniqueId);
2882
+ }
2883
+ results.push({ uniqueId, ok: true });
2884
+ }
2840
2885
  }
2841
2886
  });
2842
2887
  });
@@ -2872,7 +2917,9 @@ export function createStore(filePath, options = {}) {
2872
2917
 
2873
2918
  function updateUserInfo(uniqueId, info) {
2874
2919
  if (getDb()) {
2875
- return updateJobInfo(uniqueId, info, true);
2920
+ // 按用户实际所在表更新(task_queue / jobs / jobs_done)
2921
+ const tableName = getUserTable(uniqueId) || "jobs";
2922
+ return updateJobInfo(uniqueId, info, true, tableName);
2876
2923
  }
2877
2924
 
2878
2925
  const user = getUser(uniqueId);
@@ -2891,14 +2938,13 @@ export function createStore(filePath, options = {}) {
2891
2938
 
2892
2939
  function updateUserLocation(uniqueId, location) {
2893
2940
  if (getDb()) {
2894
- const existing = getDb()
2895
- .prepare("SELECT * FROM jobs WHERE unique_id = ?")
2896
- .get(uniqueId);
2941
+ const existing = getJobRow(uniqueId);
2897
2942
  if (!existing) return { error: "user not found" };
2943
+ const tableName = getUserTable(uniqueId) || "jobs";
2898
2944
  const now = Date.now();
2899
2945
  getDb()
2900
2946
  .prepare(
2901
- "UPDATE jobs SET location_created = ?, modified_at = ?, updated_at = ? WHERE unique_id = ?",
2947
+ `UPDATE ${tableName} SET location_created = ?, modified_at = ?, updated_at = ? WHERE unique_id = ?`,
2902
2948
  )
2903
2949
  .run(location, now, now, uniqueId);
2904
2950
  return { ok: true, location, modifiedAt: now };
@@ -2916,14 +2962,13 @@ export function createStore(filePath, options = {}) {
2916
2962
 
2917
2963
  function setNonSeller(uniqueId) {
2918
2964
  if (getDb()) {
2919
- const existing = getDb()
2920
- .prepare("SELECT * FROM jobs WHERE unique_id = ?")
2921
- .get(uniqueId);
2965
+ const existing = getJobRow(uniqueId);
2922
2966
  if (!existing) return { error: "user not found" };
2967
+ const tableName = getUserTable(uniqueId) || "jobs";
2923
2968
  const now = Date.now();
2924
2969
  getDb()
2925
2970
  .prepare(
2926
- "UPDATE jobs SET tt_seller = 0, updated_at = ? WHERE unique_id = ?",
2971
+ `UPDATE ${tableName} SET tt_seller = 0, updated_at = ? WHERE unique_id = ?`,
2927
2972
  )
2928
2973
  .run(now, uniqueId);
2929
2974
  console.error(`[DB] setNonSeller: ${uniqueId} → tt_seller=0`);
@@ -2982,12 +3027,14 @@ export function createStore(filePath, options = {}) {
2982
3027
  ].join(", ");
2983
3028
 
2984
3029
  const moveSingleTxn = getDb().transaction(() => {
2985
- // Phase 2: 先查 task_queue,再查 jobs
3030
+ // 先查 task_queue,再查 jobs,最后 jobs_done
2986
3031
  const fromTable = getDb()
2987
3032
  .prepare("SELECT 1 FROM task_queue WHERE unique_id = ?")
2988
3033
  .get(safeId)
2989
3034
  ? "task_queue"
2990
- : "jobs";
3035
+ : getDb().prepare("SELECT 1 FROM jobs WHERE unique_id = ?").get(safeId)
3036
+ ? "jobs"
3037
+ : "jobs_done";
2991
3038
 
2992
3039
  getDb()
2993
3040
  .prepare(
@@ -3734,7 +3781,7 @@ export function createStore(filePath, options = {}) {
3734
3781
  return { ok: true, moved: deleted.changes || 0 };
3735
3782
  }
3736
3783
 
3737
- // 将 jobs 中 status='done' 且无国家数据的记录移动到 jobs_base
3784
+ // 将 jobs / jobs_done 中 status='done' 且无国家数据的记录移动到 jobs_base
3738
3785
  function moveNoCountryJobsToBase() {
3739
3786
  const db = getDb();
3740
3787
  if (!db) return { ok: false, error: "db not ready" };
@@ -3745,41 +3792,48 @@ export function createStore(filePath, options = {}) {
3745
3792
  AND (location_created IS NULL OR location_created = '')
3746
3793
  AND (confirmed_location IS NULL OR confirmed_location = '')`;
3747
3794
 
3748
- const countRow = db
3749
- .prepare(`SELECT COUNT(*) as c FROM jobs WHERE ${condition}`)
3750
- .get();
3751
- if (!countRow?.c) return { ok: true, moved: 0 };
3795
+ let total = 0;
3796
+ for (const srcTable of ["jobs", "jobs_done"]) {
3797
+ const countRow = db
3798
+ .prepare(`SELECT COUNT(*) as c FROM ${srcTable} WHERE ${condition}`)
3799
+ .get();
3800
+ const cnt = countRow?.c || 0;
3801
+ if (!cnt) continue;
3752
3802
 
3753
- db.prepare(
3754
- `
3755
- INSERT OR REPLACE INTO jobs_base (
3756
- unique_id, nickname, sources, pinned, no_video, user_update_count,
3757
- tt_seller, verified, video_count, comment_count, guessed_location,
3758
- location_created, follower_count, following_count, heart_count,
3759
- refresh_time, region, signature, sec_uid, status_code,
3760
- latest_video_time, bio_link, status, restricted, error, claimed_by,
3761
- claimed_at, processed, processed_at, created_at, updated_at
3762
- )
3763
- SELECT
3764
- unique_id, nickname, sources, pinned, no_video, 0,
3765
- CASE WHEN tt_seller = 0 THEN NULL ELSE tt_seller END,
3766
- verified, video_count, comment_count, guessed_location,
3767
- location_created, follower_count, following_count, heart_count,
3768
- refresh_time, region, signature, sec_uid, status_code,
3769
- latest_video_time, bio_link,
3770
- 'pending', 0, NULL, NULL, NULL, 0, NULL,
3771
- COALESCE(created_at, ?), ?
3772
- FROM jobs WHERE ${condition}
3773
- `,
3774
- ).run(now, now);
3803
+ db.prepare(
3804
+ `
3805
+ INSERT OR REPLACE INTO jobs_base (
3806
+ unique_id, nickname, sources, pinned, no_video, user_update_count,
3807
+ tt_seller, verified, video_count, comment_count, guessed_location,
3808
+ location_created, follower_count, following_count, heart_count,
3809
+ refresh_time, region, signature, sec_uid, status_code,
3810
+ latest_video_time, bio_link, status, restricted, error, claimed_by,
3811
+ claimed_at, processed, processed_at, created_at, updated_at
3812
+ )
3813
+ SELECT
3814
+ unique_id, nickname, sources, pinned, no_video, 0,
3815
+ CASE WHEN tt_seller = 0 THEN NULL ELSE tt_seller END,
3816
+ verified, video_count, comment_count, guessed_location,
3817
+ location_created, follower_count, following_count, heart_count,
3818
+ refresh_time, region, signature, sec_uid, status_code,
3819
+ latest_video_time, bio_link,
3820
+ 'pending', 0, NULL, NULL, NULL, 0, NULL,
3821
+ COALESCE(created_at, ?), ?
3822
+ FROM ${srcTable} WHERE ${condition}
3823
+ `,
3824
+ ).run(now, now);
3775
3825
 
3776
- const deleted = db.prepare(`DELETE FROM jobs WHERE ${condition}`).run();
3826
+ const deleted = db
3827
+ .prepare(`DELETE FROM ${srcTable} WHERE ${condition}`)
3828
+ .run();
3829
+ total += deleted.changes || 0;
3830
+ }
3777
3831
 
3778
- markStatsDirty();
3832
+ if (total > 0) markStatsDirty();
3779
3833
  return {
3780
3834
  ok: true,
3781
- moved: deleted.changes || 0,
3782
- total: countRow.c,
3835
+ moved: total,
3836
+ total,
3783
3837
  };
3784
3838
  }
3785
3839
 
@@ -312,25 +312,37 @@ export function addJob(user) {
312
312
  export function getJobRow(uniqueId) {
313
313
  const db = getDb();
314
314
  if (!db) return null;
315
- // 优先查 task_queue(热数据),回退 jobs(归档)
315
+ // 优先查 task_queue(热数据),回退 jobs(归档),再回退 jobs_done(冷归档)
316
316
  const row = db
317
317
  .prepare("SELECT * FROM task_queue WHERE unique_id = ?")
318
318
  .get(uniqueId);
319
319
  if (row) return row;
320
- return db.prepare("SELECT * FROM jobs WHERE unique_id = ?").get(uniqueId);
320
+ const row2 = db
321
+ .prepare("SELECT * FROM jobs WHERE unique_id = ?")
322
+ .get(uniqueId);
323
+ if (row2) return row2;
324
+ return db
325
+ .prepare("SELECT * FROM jobs_done WHERE unique_id = ?")
326
+ .get(uniqueId);
321
327
  }
322
328
 
323
329
  /**
324
- * 判断用户在哪个表,返回 'task_queue' 或 'jobs'
330
+ * 判断用户在哪个表,返回 'task_queue' | 'jobs' | 'jobs_done'
325
331
  * 用于需要更新正确表的场景(置顶、重置、移动等)
326
332
  */
327
333
  export function getUserTable(uniqueId) {
328
334
  const db = getDb();
329
335
  if (!db) return null;
330
- return db
331
- .prepare("SELECT 1 FROM task_queue WHERE unique_id = ?")
332
- .get(uniqueId)
333
- ? "task_queue"
336
+ if (
337
+ db.prepare("SELECT 1 FROM task_queue WHERE unique_id = ?").get(uniqueId)
338
+ ) {
339
+ return "task_queue";
340
+ }
341
+ if (db.prepare("SELECT 1 FROM jobs WHERE unique_id = ?").get(uniqueId)) {
342
+ return "jobs";
343
+ }
344
+ return db.prepare("SELECT 1 FROM jobs_done WHERE unique_id = ?").get(uniqueId)
345
+ ? "jobs_done"
334
346
  : "jobs";
335
347
  }
336
348
 
@@ -361,10 +373,13 @@ export function getBulkAuthorLocations(uniqueIds) {
361
373
  SELECT unique_id, location_created FROM jobs
362
374
  WHERE unique_id IN (${placeholders}) AND location_created IS NOT NULL AND location_created != ''
363
375
  UNION
376
+ SELECT unique_id, location_created FROM jobs_done
377
+ WHERE unique_id IN (${placeholders}) AND location_created IS NOT NULL AND location_created != ''
378
+ UNION
364
379
  SELECT unique_id, location_created FROM jobs_base
365
380
  WHERE unique_id IN (${placeholders}) AND location_created IS NOT NULL AND location_created != ''
366
381
  `;
367
- const rows = db.prepare(sql).all(...uniqueIds, ...uniqueIds);
382
+ const rows = db.prepare(sql).all(...uniqueIds, ...uniqueIds, ...uniqueIds);
368
383
  const result = {};
369
384
  for (const r of rows) {
370
385
  if (!result[r.unique_id]) {
@@ -377,7 +392,11 @@ export function getBulkAuthorLocations(uniqueIds) {
377
392
  export function getAllJobs() {
378
393
  const db = getDb();
379
394
  if (!db) return [];
380
- return db.prepare("SELECT * FROM jobs").all().map(mapJobRow);
395
+ // jobs(商家 done + error/restricted 等)+ jobs_done(非商家 done 冷归档)
396
+ return db
397
+ .prepare("SELECT * FROM jobs UNION ALL SELECT * FROM jobs_done")
398
+ .all()
399
+ .map(mapJobRow);
381
400
  }
382
401
 
383
402
  // ===== 查询 Video =====
@@ -482,7 +501,13 @@ export function getUserDbCount() {
482
501
  export function getJobsCount() {
483
502
  const db = getDb();
484
503
  if (!db) return 0;
485
- return db.prepare("SELECT COUNT(*) as c FROM jobs").get().c;
504
+ // jobs + jobs_done 冷归档合并计数
505
+ const row = db
506
+ .prepare(
507
+ "SELECT (SELECT COUNT(*) FROM jobs) + (SELECT COUNT(*) FROM jobs_done) as c",
508
+ )
509
+ .get();
510
+ return row?.c || 0;
486
511
  }
487
512
 
488
513
  export function getPendingJobsCount() {