tt-help-cli-ycl 1.4.18 → 1.4.20

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.
@@ -556,6 +556,64 @@ function getModifiedTargetUsersOutsideLocations(targetLocations = []) {
556
556
  return { total: rows.length, countries };
557
557
  }
558
558
 
559
+ /**
560
+ * 目标商家刷新进度统计:按国家统计最近 N 小时内刷新过的 tt_seller 用户数。
561
+ * 返回格式与 getRefreshStats(tags)一致,便于前端复用渲染逻辑。
562
+ */
563
+ function getTargetRefreshStats(hours = 24) {
564
+ if (!getDb()) return { countries: [], total: 0, refreshed: 0 };
565
+
566
+ const targetLocations = DEFAULT_TARGET_LOCATIONS;
567
+ const placeholders = targetLocations.map(() => "?").join(", ");
568
+
569
+ // 按国家统计总数
570
+ const totalRows = getDb()
571
+ .prepare(
572
+ `
573
+ SELECT location_created as country, COUNT(*) as cnt
574
+ FROM jobs
575
+ WHERE tt_seller = 1
576
+ AND verified = 0
577
+ AND location_created IN (${placeholders})
578
+ GROUP BY location_created
579
+ ORDER BY cnt DESC
580
+ `,
581
+ )
582
+ .all(...targetLocations);
583
+
584
+ if (!totalRows.length) return { countries: [], total: 0, refreshed: 0 };
585
+
586
+ // 按国家统计最近 N 小时内刷新过的数量
587
+ const threshold = Date.now() - hours * 3600 * 1000;
588
+ const refreshedRows = getDb()
589
+ .prepare(
590
+ `
591
+ SELECT location_created as country, COUNT(*) as cnt
592
+ FROM jobs
593
+ WHERE tt_seller = 1
594
+ AND verified = 0
595
+ AND location_created IN (${placeholders})
596
+ AND refresh_time >= ?
597
+ GROUP BY location_created
598
+ `,
599
+ )
600
+ .all(...targetLocations, threshold);
601
+ const refreshedMap = new Map();
602
+ for (const r of refreshedRows) refreshedMap.set(r.country, r.cnt);
603
+
604
+ const results = [];
605
+ let totalAll = 0;
606
+ let refreshedAll = 0;
607
+ for (const r of totalRows) {
608
+ const refreshed = refreshedMap.get(r.country) || 0;
609
+ totalAll += r.cnt;
610
+ refreshedAll += refreshed;
611
+ results.push({ country: r.country, total: r.cnt, refreshed });
612
+ }
613
+
614
+ return { countries: results, total: totalAll, refreshed: refreshedAll };
615
+ }
616
+
559
617
  export function createStore(filePath, options = {}) {
560
618
  if (!filePath) {
561
619
  throw new Error("createStore requires an explicit .db path");
@@ -1008,7 +1066,7 @@ export function createStore(filePath, options = {}) {
1008
1066
 
1009
1067
  if (normalizedLocations && normalizedLocations.length > 0) {
1010
1068
  conditions.push(
1011
- `UPPER(COALESCE(guessed_location, '')) IN (${normalizedLocations.map(() => "?").join(", ")})`,
1069
+ `(UPPER(COALESCE(guessed_location, '')) IN (${normalizedLocations.map(() => "?").join(", ")}) OR COALESCE(guessed_location, '') = '')`,
1012
1070
  );
1013
1071
  args.push(...normalizedLocations);
1014
1072
  }
@@ -1164,7 +1222,7 @@ export function createStore(filePath, options = {}) {
1164
1222
  const llmCount =
1165
1223
  getDb()
1166
1224
  .prepare(
1167
- `SELECT COUNT(*) as c FROM raw_jobs WHERE ${whereSql} AND guessed_location = ? AND (sources NOT LIKE '%tag%' OR sources IS NULL) AND (sources NOT LIKE '%comment%' OR sources IS NULL)`,
1225
+ `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)`,
1168
1226
  )
1169
1227
  .get(...locationArgs)?.c || 0;
1170
1228
 
@@ -1190,7 +1248,7 @@ export function createStore(filePath, options = {}) {
1190
1248
  const samples = getDb()
1191
1249
  .prepare(
1192
1250
  `
1193
- SELECT * FROM raw_jobs WHERE ${whereSql} AND guessed_location = ?
1251
+ SELECT * FROM raw_jobs WHERE ${whereSql} AND (guessed_location = ? OR COALESCE(guessed_location, '') = '')
1194
1252
  AND (sources NOT LIKE '%tag%' OR sources IS NULL)
1195
1253
  AND (sources NOT LIKE '%comment%' OR sources IS NULL)
1196
1254
  ORDER BY COALESCE(video_count, 0) DESC, created_at DESC
@@ -1361,7 +1419,7 @@ export function createStore(filePath, options = {}) {
1361
1419
  const currentLimit = Math.max(0, Math.min(quota, safeLimit - moved));
1362
1420
  if (!currentLimit) continue;
1363
1421
 
1364
- const locationWhere = `${whereSql} AND UPPER(COALESCE(guessed_location, '')) = ?`;
1422
+ const locationWhere = `${whereSql} AND (UPPER(COALESCE(guessed_location, '')) = ? OR COALESCE(guessed_location, '') = '')`;
1365
1423
  const locationArgs = [...args, location];
1366
1424
 
1367
1425
  getDb()
@@ -1488,7 +1546,7 @@ export function createStore(filePath, options = {}) {
1488
1546
  if (!group) return;
1489
1547
  if (group.type === "include") {
1490
1548
  where.push(
1491
- `UPPER(COALESCE(guessed_location, '')) IN (${group.values.map(() => "?").join(", ")})`,
1549
+ `(UPPER(COALESCE(guessed_location, '')) IN (${group.values.map(() => "?").join(", ")}) OR COALESCE(guessed_location, '') = '')`,
1492
1550
  );
1493
1551
  args.push(...group.values);
1494
1552
  return;
@@ -1559,7 +1617,7 @@ export function createStore(filePath, options = {}) {
1559
1617
  }
1560
1618
  if (normalizedLocations.length > 0) {
1561
1619
  where.push(
1562
- `UPPER(COALESCE(guessed_location, '')) IN (${normalizedLocations.map(() => "?").join(", ")})`,
1620
+ `(UPPER(COALESCE(guessed_location, '')) IN (${normalizedLocations.map(() => "?").join(", ")}) OR COALESCE(guessed_location, '') = '')`,
1563
1621
  );
1564
1622
  args.push(...normalizedLocations);
1565
1623
  }
@@ -1957,7 +2015,7 @@ export function createStore(filePath, options = {}) {
1957
2015
  if (!group) return;
1958
2016
  if (group.type === "include") {
1959
2017
  where.push(
1960
- `UPPER(COALESCE(guessed_location, '')) IN (${group.values.map(() => "?").join(", ")})`,
2018
+ `(UPPER(COALESCE(guessed_location, '')) IN (${group.values.map(() => "?").join(", ")}) OR COALESCE(guessed_location, '') = '')`,
1961
2019
  );
1962
2020
  args.push(...group.values);
1963
2021
  return;
@@ -2025,7 +2083,7 @@ export function createStore(filePath, options = {}) {
2025
2083
  }
2026
2084
  if (normalizedLocations.length > 0) {
2027
2085
  where.push(
2028
- `UPPER(COALESCE(guessed_location, '')) IN (${normalizedLocations.map(() => "?").join(", ")})`,
2086
+ `(UPPER(COALESCE(guessed_location, '')) IN (${normalizedLocations.map(() => "?").join(", ")}) OR COALESCE(guessed_location, '') = '')`,
2029
2087
  );
2030
2088
  args.push(...normalizedLocations);
2031
2089
  }
@@ -2677,7 +2735,20 @@ export function createStore(filePath, options = {}) {
2677
2735
  }
2678
2736
 
2679
2737
  function getClientErrors() {
2680
- return Array.from(clientErrors.values());
2738
+ const now = Date.now();
2739
+ const expired = [];
2740
+ const result = [];
2741
+ for (const [userId, err] of clientErrors) {
2742
+ if (now - err.timestamp > 10 * 60 * 1000) {
2743
+ expired.push(userId);
2744
+ } else {
2745
+ result.push(err);
2746
+ }
2747
+ }
2748
+ for (const userId of expired) {
2749
+ clientErrors.delete(userId);
2750
+ }
2751
+ return result;
2681
2752
  }
2682
2753
 
2683
2754
  function getClientLoginStatus() {
@@ -2817,6 +2888,143 @@ export function createStore(filePath, options = {}) {
2817
2888
  return pending;
2818
2889
  }
2819
2890
 
2891
+ // ===== recheck-jobs:重检 jobs 表中 done 非商家用户 =====
2892
+
2893
+ function getRecheckJobs(limit, countries) {
2894
+ const targetCountries = countries
2895
+ ? countries.map((c) => String(c).trim().toUpperCase())
2896
+ : [];
2897
+ const hasCountryFilter = targetCountries.length > 0;
2898
+
2899
+ if (getDb()) {
2900
+ const l = Math.max(1, parseInt(limit) || 5);
2901
+ const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
2902
+
2903
+ let sql = `
2904
+ SELECT *
2905
+ FROM jobs
2906
+ WHERE status = 'done'
2907
+ AND (tt_seller IS NULL OR tt_seller != 1)
2908
+ AND (updated_at IS NULL OR updated_at < ?)
2909
+ `;
2910
+ const sqlParams = [sevenDaysAgo];
2911
+
2912
+ if (hasCountryFilter) {
2913
+ const placeholders = targetCountries.map(() => "?").join(", ");
2914
+ sql += ` AND UPPER(COALESCE(guessed_location, '')) IN (${placeholders})`;
2915
+ sqlParams.push(...targetCountries);
2916
+ }
2917
+
2918
+ sql += ` ORDER BY updated_at ASC LIMIT ?`;
2919
+ sqlParams.push(l);
2920
+
2921
+ const rows = getDb()
2922
+ .prepare(sql)
2923
+ .all(...sqlParams);
2924
+ if (rows.length === 0) return [];
2925
+
2926
+ const now = Date.now();
2927
+ const bumpStmt = getDb().prepare(
2928
+ `UPDATE jobs SET updated_at = ? WHERE unique_id = ?`,
2929
+ );
2930
+ const bumpTxn = getDb().transaction((items) => {
2931
+ for (const item of items) {
2932
+ bumpStmt.run(now, item.unique_id);
2933
+ }
2934
+ });
2935
+ bumpTxn(rows);
2936
+
2937
+ return rows.map((row) => {
2938
+ const mapped = mapJobRow(row);
2939
+ mapped.updatedAt = now;
2940
+ return mapped;
2941
+ });
2942
+ }
2943
+
2944
+ const l = Math.max(1, parseInt(limit) || 5);
2945
+ const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
2946
+ const pending = data
2947
+ .filter((u) => {
2948
+ if (u.status !== "done") return false;
2949
+ if (u.ttSeller === 1) return false;
2950
+ if (u.updatedAt && u.updatedAt >= sevenDaysAgo) return false;
2951
+ if (hasCountryFilter) {
2952
+ const loc = (u.guessedLocation || "").toUpperCase();
2953
+ return targetCountries.includes(loc);
2954
+ }
2955
+ return true;
2956
+ })
2957
+ .sort((a, b) => (a.updatedAt || 0) - (b.updatedAt || 0))
2958
+ .slice(0, l);
2959
+ pending.forEach((u) => {
2960
+ u.updatedAt = Date.now();
2961
+ });
2962
+ save();
2963
+ return pending;
2964
+ }
2965
+
2966
+ function batchUpdateRecheckJobs(updates) {
2967
+ if (getDb()) {
2968
+ const results = [];
2969
+ const now = Date.now();
2970
+ const bumpStmt = getDb().prepare(
2971
+ `UPDATE jobs SET updated_at = ? WHERE unique_id = ?`,
2972
+ );
2973
+ const txn = getDb().transaction((items) => {
2974
+ items.forEach((item) => {
2975
+ const uniqueId = item.uniqueId;
2976
+ const info = item.info;
2977
+ if (info && info.error && info.statusCode !== undefined) {
2978
+ // 无效账号:只更新 updated_at,不污染其他字段
2979
+ bumpStmt.run(now, uniqueId);
2980
+ results.push({ uniqueId, ok: true, skipped: true });
2981
+ } else {
2982
+ // 过滤 info 中的 null/undefined/空值,避免覆盖 jobs 表已有数据
2983
+ const cleanInfo = {};
2984
+ for (const [k, v] of Object.entries(info || {})) {
2985
+ if (v !== null && v !== undefined && v !== "") {
2986
+ cleanInfo[k] = v;
2987
+ }
2988
+ }
2989
+ const ret = updateJobInfo(uniqueId, cleanInfo, true);
2990
+ results.push(
2991
+ ret.error
2992
+ ? { uniqueId, error: ret.error }
2993
+ : { uniqueId, ok: true },
2994
+ );
2995
+ }
2996
+ });
2997
+ });
2998
+ txn(updates);
2999
+ return { results };
3000
+ }
3001
+
3002
+ const results = [];
3003
+ const now = Date.now();
3004
+ for (const item of updates) {
3005
+ const { uniqueId, info } = item;
3006
+ if (info && info.error && info.statusCode !== undefined) {
3007
+ // 无效账号:只更新 updated_at
3008
+ const existing = data.find((u) => u.uniqueId === uniqueId);
3009
+ if (existing) existing.updatedAt = now;
3010
+ results.push({ uniqueId, ok: true, skipped: true });
3011
+ } else {
3012
+ const cleanInfo = {};
3013
+ for (const [k, v] of Object.entries(info || {})) {
3014
+ if (v !== null && v !== undefined && v !== "") {
3015
+ cleanInfo[k] = v;
3016
+ }
3017
+ }
3018
+ const ret = updateJobInfo(uniqueId, cleanInfo, true);
3019
+ results.push(
3020
+ ret.error ? { uniqueId, error: ret.error } : { uniqueId, ok: true },
3021
+ );
3022
+ }
3023
+ }
3024
+ save();
3025
+ return { results };
3026
+ }
3027
+
2820
3028
  function updateUserInfo(uniqueId, info) {
2821
3029
  if (getDb()) {
2822
3030
  return updateJobInfo(uniqueId, info, true);
@@ -3363,6 +3571,8 @@ export function createStore(filePath, options = {}) {
3363
3571
  getNextRedoJob,
3364
3572
  commitRedoJob,
3365
3573
  getPendingUserUpdateTasks,
3574
+ getRecheckJobs,
3575
+ batchUpdateRecheckJobs,
3366
3576
  updateUserInfo,
3367
3577
  updateUserLocation,
3368
3578
  setNonSeller,
@@ -3375,6 +3585,7 @@ export function createStore(filePath, options = {}) {
3375
3585
  getActiveClients,
3376
3586
  moveSellerJobsToBase, // 将 jobs/raw_jobs 中 tt_seller=1 且 video_count=0 的记录移动到 jobs_base
3377
3587
  moveRestrictedJobsToBase, // 将 jobs 中 status='restricted' 的记录移动到 jobs_base
3588
+ moveNoCountryJobsToBase, // 将 jobs 中 status='done' 且无国家数据的记录移动到 jobs_base
3378
3589
  moveSingleJobToBase, // 将单个 job 移动到 jobs_base 并重置(目标商家页面用)
3379
3590
  registerVideos,
3380
3591
  getVideo,
@@ -3404,6 +3615,7 @@ export function createStore(filePath, options = {}) {
3404
3615
  claimRefreshTag,
3405
3616
  reportRefreshResult,
3406
3617
  getRefreshStats,
3618
+ getTargetRefreshStats,
3407
3619
  data,
3408
3620
  };
3409
3621
 
@@ -3572,6 +3784,55 @@ export function createStore(filePath, options = {}) {
3572
3784
  return { ok: true, moved: deleted.changes || 0 };
3573
3785
  }
3574
3786
 
3787
+ // 将 jobs 中 status='done' 且无国家数据的记录移动到 jobs_base
3788
+ function moveNoCountryJobsToBase() {
3789
+ const db = getDb();
3790
+ if (!db) return { ok: false, error: "db not ready" };
3791
+
3792
+ const now = Date.now();
3793
+ const condition = `status = 'done'
3794
+ AND (guessed_location IS NULL OR guessed_location = '')
3795
+ AND (location_created IS NULL OR location_created = '')
3796
+ AND (confirmed_location IS NULL OR confirmed_location = '')`;
3797
+
3798
+ const countRow = db
3799
+ .prepare(`SELECT COUNT(*) as c FROM jobs WHERE ${condition}`)
3800
+ .get();
3801
+ if (!countRow?.c) return { ok: true, moved: 0 };
3802
+
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 jobs WHERE ${condition}
3823
+ `,
3824
+ ).run(now, now);
3825
+
3826
+ const deleted = db.prepare(`DELETE FROM jobs WHERE ${condition}`).run();
3827
+
3828
+ markStatsDirty();
3829
+ return {
3830
+ ok: true,
3831
+ moved: deleted.changes || 0,
3832
+ total: countRow.c,
3833
+ };
3834
+ }
3835
+
3575
3836
  // 将单个 job 移动到 jobs_base(用于目标商家页面"移到base重走流程")
3576
3837
  function moveSingleJobToBase(uniqueId) {
3577
3838
  const db = getDb();
@@ -112,7 +112,7 @@ export function getRefreshStats(hours = 24) {
112
112
 
113
113
  const rows = db
114
114
  .prepare(
115
- "SELECT countries FROM tags WHERE status = 'productive' AND score > 70",
115
+ "SELECT countries FROM tags WHERE status = 'productive' AND score > 60",
116
116
  )
117
117
  .all();
118
118
  const countrySet = new Set();
@@ -128,10 +128,10 @@ export function getRefreshStats(hours = 24) {
128
128
  const countryList = [...countrySet].sort();
129
129
 
130
130
  const stmtTotal = db.prepare(
131
- "SELECT COUNT(*) as cnt FROM tags WHERE status = 'productive' AND score > 70 AND countries LIKE ?",
131
+ "SELECT COUNT(*) as cnt FROM tags WHERE status = 'productive' AND score > 60 AND countries LIKE ?",
132
132
  );
133
133
  const stmtRefresh = db.prepare(
134
- `SELECT COUNT(*) as cnt FROM tags WHERE status = 'productive' AND score > 70 AND countries LIKE ? AND refreshed_at >= datetime('now', ?)`,
134
+ `SELECT COUNT(*) as cnt FROM tags WHERE status = 'productive' AND score > 60 AND countries LIKE ? AND refreshed_at >= datetime('now', ?)`,
135
135
  );
136
136
 
137
137
  for (const c of countryList) {
@@ -1338,7 +1338,7 @@ function getLang(country) {
1338
1338
  // ===== Tag 刷新 CRUD(productive 标签轮询重新抓取) =====
1339
1339
 
1340
1340
  /**
1341
- * 获取一个待刷新的 productive 标签(score > 70 且未被刷新锁定)
1341
+ * 获取一个待刷新的 productive 标签(score > 60 且未被刷新锁定)
1342
1342
  * 按 refreshed_at ASC 排序(最久未刷新的优先),NULL 排最前
1343
1343
  */
1344
1344
  export function getProductiveRefreshTask() {
@@ -1355,7 +1355,7 @@ export function getProductiveRefreshTask() {
1355
1355
  const row = db
1356
1356
  .prepare(
1357
1357
  `SELECT * FROM tags
1358
- WHERE status = 'productive' AND score > 70 AND refreshing = 0
1358
+ WHERE status = 'productive' AND score > 60 AND refreshing = 0
1359
1359
  ORDER BY refreshed_at IS NULL DESC, refreshed_at ASC
1360
1360
  LIMIT 1`,
1361
1361
  )
@@ -1379,7 +1379,7 @@ export function claimRefreshTag(tag) {
1379
1379
  const result = db
1380
1380
  .prepare(
1381
1381
  `UPDATE tags SET refreshing = 1, refreshed_at = datetime('now')
1382
- WHERE (tag = ? OR tag = ?) AND status = 'productive' AND score > 70 AND refreshing = 0`,
1382
+ WHERE (tag = ? OR tag = ?) AND status = 'productive' AND score > 60 AND refreshing = 0`,
1383
1383
  )
1384
1384
  .run(normalized, `#${normalized}`);
1385
1385
  if (result.changes === 0) {