tt-help-cli-ycl 1.3.44 → 1.3.45

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tt-help-cli-ycl",
3
- "version": "1.3.44",
3
+ "version": "1.3.45",
4
4
  "description": "TikTok user & video data scraper - extract ttSeller, verified, locationCreated from HTML source",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli/attach.js CHANGED
@@ -76,12 +76,12 @@ async function recycleScraper(scraper, reason) {
76
76
  }
77
77
 
78
78
  export async function handleAttach(options) {
79
- const { attachParallel, attachInterval, serverUrl, showHelp } = options;
79
+ const { attachParallel, attachInterval, serverUrl, attachCountries, showHelp } = options;
80
80
  let shuttingDown = false;
81
81
  let forceExitTimer = null;
82
82
 
83
83
  if (showHelp) {
84
- attachLog("用法: tt-help attach [-p 并行数] [-i 间隔秒数] [-s 服务端地址]");
84
+ attachLog("用法: tt-help attach [-p 并行数] [-i 间隔秒数] [-s 服务端地址] [-c 国家列表]");
85
85
  attachLog("");
86
86
  attachLog("参数:");
87
87
  attachLog(" -p, --parallel <N> 并行抓取数(默认: 1)");
@@ -89,6 +89,9 @@ export async function handleAttach(options) {
89
89
  attachLog(
90
90
  " -s, --server <URL> 服务端地址(默认: http://127.0.0.1:3001)",
91
91
  );
92
+ attachLog(
93
+ " -c, --countries <A,B,C> 猜测国家列表(逗号分隔,如 PL,DE,FR),服务端优先返回这些国家的任务",
94
+ );
92
95
  attachLog("");
93
96
  attachLog("说明:");
94
97
  attachLog(
@@ -101,11 +104,16 @@ export async function handleAttach(options) {
101
104
  attachLog(" tt-help attach");
102
105
  attachLog(" tt-help attach -p 5 -i 10");
103
106
  attachLog(" tt-help attach -p 3 -i 5 -s http://127.0.0.1:3001");
107
+ attachLog(" tt-help attach -c PL,DE,FR -p 5");
104
108
  return;
105
109
  }
106
110
 
111
+ const countryStr =
112
+ attachCountries && attachCountries.length > 0
113
+ ? `, 猜测国家: ${attachCountries.join(", ")}`
114
+ : "";
107
115
  attachLog(
108
- `[Attach] 并行数: ${attachParallel}, 空闲间隔: ${attachInterval}秒, 服务端: ${serverUrl}`,
116
+ `[Attach] 并行数: ${attachParallel}, 空闲间隔: ${attachInterval}秒, 服务端: ${serverUrl}${countryStr}`,
109
117
  );
110
118
 
111
119
  const scraper = new TikTokScraper();
@@ -162,8 +170,12 @@ export async function handleAttach(options) {
162
170
  process.exit(0);
163
171
  }
164
172
 
173
+ const countryParam =
174
+ attachCountries && attachCountries.length > 0
175
+ ? `&countries=${encodeURIComponent(attachCountries.join(","))}`
176
+ : "";
165
177
  const { total, tasks } = await apiGet(
166
- `${serverUrl}/api/user-update-tasks?limit=${attachParallel}`,
178
+ `${serverUrl}/api/user-update-tasks?limit=${attachParallel}${countryParam}`,
167
179
  );
168
180
 
169
181
  if (!tasks || tasks.length === 0) {
package/src/lib/args.js CHANGED
@@ -463,6 +463,7 @@ function parseAttachArgs(args) {
463
463
  let parallel = 1;
464
464
  let interval = 10;
465
465
  let serverUrl = defaultServer;
466
+ let countries = [];
466
467
 
467
468
  for (let i = 0; i < args.length; i++) {
468
469
  const arg = args[i];
@@ -472,6 +473,11 @@ function parseAttachArgs(args) {
472
473
  interval = parseInt(args[++i], 10) || 10;
473
474
  } else if (arg === "-s" || arg === "--server") {
474
475
  serverUrl = args[++i];
476
+ } else if (arg === "-c" || arg === "--countries") {
477
+ countries = args[++i]
478
+ .split(",")
479
+ .map((c) => c.trim().toUpperCase())
480
+ .filter(Boolean);
475
481
  }
476
482
  }
477
483
 
@@ -480,6 +486,7 @@ function parseAttachArgs(args) {
480
486
  attachParallel: parallel,
481
487
  attachInterval: interval,
482
488
  serverUrl,
489
+ attachCountries: countries,
483
490
  urls: [],
484
491
  outputFormat: "json",
485
492
  exploreCount: 0,
@@ -146,12 +146,14 @@ const HELP_TEXT = [
146
146
  " 示例: tt-help info https://www.tiktok.com/@nike",
147
147
  " tt-help info https://www.tiktok.com/@nike/video/7234567890 --onlyvideo",
148
148
  "",
149
- " tt-help attach [-p 并行数] [-i 间隔秒数] [-s 服务端地址]",
149
+ " tt-help attach [-p 并行数] [-i 间隔秒数] [-s 服务端地址] [-c 国家列表]",
150
150
  " 后台轮询服务端任务接口,自动抓取 TikTok 用户信息",
151
151
  " -p, --parallel <N> 并行抓取数(默认: 1)",
152
152
  " -i, --interval <N> 无任务时轮询间隔,单位秒(默认: 10)",
153
153
  " -s, --server <URL> 服务端地址(默认: http://127.0.0.1:3001)",
154
+ " -c, --countries <A,B> 猜测国家列表(逗号分隔,如 PL,DE,FR)",
154
155
  " 示例: tt-help attach -p 5 -i 10",
156
+ " tt-help attach -c PL,DE,FR -p 5",
155
157
  "",
156
158
  " tt-help watch -o <db路径> [-p 端口]",
157
159
  " 启动监控服务;运行期主数据仅来自 SQLite",
@@ -990,6 +990,117 @@ function restoreRawJobsByCountry(country) {
990
990
  return { restored: count, country: normalizedCountry };
991
991
  }
992
992
 
993
+ function restoreRawJobById(uniqueId) {
994
+ if (!db) {
995
+ return { restored: 0, uniqueId, error: "db not ready" };
996
+ }
997
+
998
+ const safeId = String(uniqueId).trim();
999
+ if (!safeId) {
1000
+ return { restored: 0, uniqueId: safeId, error: "uniqueId is required" };
1001
+ }
1002
+
1003
+ const exists =
1004
+ db
1005
+ .prepare("SELECT COUNT(*) as c FROM raw_jobs WHERE unique_id = ?")
1006
+ .get(safeId)?.c || 0;
1007
+
1008
+ if (!exists) {
1009
+ return { restored: 0, uniqueId: safeId };
1010
+ }
1011
+
1012
+ const restoreTxn = db.transaction(() => {
1013
+ db.prepare(
1014
+ `
1015
+ INSERT OR REPLACE INTO jobs (
1016
+ unique_id, nickname, status, sources, claimed_by, claimed_at, error,
1017
+ pinned, no_video, restricted, user_update_count, tt_seller, verified,
1018
+ video_count, comment_count, guessed_location, location_created,
1019
+ follower_count, following_count, heart_count, refresh_time,
1020
+ processed, processed_at, created_at, updated_at, region, signature, sec_uid
1021
+ )
1022
+ SELECT
1023
+ unique_id, nickname, status, sources, claimed_by, claimed_at, error,
1024
+ pinned, no_video, restricted, user_update_count, tt_seller, verified,
1025
+ video_count, comment_count, guessed_location, location_created,
1026
+ follower_count, following_count, heart_count, refresh_time,
1027
+ processed, processed_at, created_at, updated_at, region, signature, sec_uid
1028
+ FROM raw_jobs WHERE unique_id = ?
1029
+ `,
1030
+ ).run(safeId);
1031
+
1032
+ db.prepare("DELETE FROM raw_jobs WHERE unique_id = ?").run(safeId);
1033
+ });
1034
+
1035
+ restoreTxn();
1036
+ return { restored: 1, uniqueId: safeId };
1037
+ }
1038
+
1039
+ function restoreRawJobsByFilter({ search, location }) {
1040
+ if (!db) {
1041
+ return { restored: 0, error: "db not ready" };
1042
+ }
1043
+
1044
+ const where = [];
1045
+ const args = [];
1046
+
1047
+ if (search) {
1048
+ where.push(
1049
+ "(LOWER(unique_id) LIKE ? OR LOWER(COALESCE(nickname, '')) LIKE ?)",
1050
+ );
1051
+ const likeVal = `%${search.toLowerCase()}%`;
1052
+ args.push(likeVal, likeVal);
1053
+ }
1054
+
1055
+ if (location) {
1056
+ where.push("COALESCE(guessed_location, '未知') = ?");
1057
+ args.push(location);
1058
+ }
1059
+
1060
+ if (where.length === 0) {
1061
+ return { restored: 0, error: "at least one filter is required" };
1062
+ }
1063
+
1064
+ const whereSql = where.join(" AND ");
1065
+
1066
+ const count =
1067
+ db
1068
+ .prepare(
1069
+ `SELECT COUNT(*) as c FROM raw_jobs WHERE ${whereSql}`,
1070
+ )
1071
+ .get(...args)?.c || 0;
1072
+
1073
+ if (!count) {
1074
+ return { restored: 0 };
1075
+ }
1076
+
1077
+ const restoreTxn = db.transaction(() => {
1078
+ db.prepare(
1079
+ `
1080
+ INSERT OR REPLACE INTO jobs (
1081
+ unique_id, nickname, status, sources, claimed_by, claimed_at, error,
1082
+ pinned, no_video, restricted, user_update_count, tt_seller, verified,
1083
+ video_count, comment_count, guessed_location, location_created,
1084
+ follower_count, following_count, heart_count, refresh_time,
1085
+ processed, processed_at, created_at, updated_at, region, signature, sec_uid
1086
+ )
1087
+ SELECT
1088
+ unique_id, nickname, status, sources, claimed_by, claimed_at, error,
1089
+ pinned, no_video, restricted, user_update_count, tt_seller, verified,
1090
+ video_count, comment_count, guessed_location, location_created,
1091
+ follower_count, following_count, heart_count, refresh_time,
1092
+ processed, processed_at, created_at, updated_at, region, signature, sec_uid
1093
+ FROM raw_jobs WHERE ${whereSql}
1094
+ `,
1095
+ ).run(...args);
1096
+
1097
+ db.prepare(`DELETE FROM raw_jobs WHERE ${whereSql}`).run(...args);
1098
+ });
1099
+
1100
+ restoreTxn();
1101
+ return { restored: count };
1102
+ }
1103
+
993
1104
  function getRawJobsPageFromDb({ search, location, limit, offset }) {
994
1105
  if (!db) return null;
995
1106
 
@@ -2474,21 +2585,33 @@ export function createStore(filePath) {
2474
2585
  return Array.from(clientErrors.values());
2475
2586
  }
2476
2587
 
2477
- function getPendingUserUpdateTasks(limit) {
2588
+ function getPendingUserUpdateTasks(limit, countries) {
2589
+ const targetCountries = countries
2590
+ ? countries.map((c) => String(c).trim().toUpperCase())
2591
+ : [];
2592
+ const hasCountryFilter = targetCountries.length > 0;
2593
+
2478
2594
  if (db) {
2479
2595
  const l = Math.max(1, parseInt(limit) || 5);
2480
- const rows = db
2481
- .prepare(
2482
- `
2483
- SELECT *
2484
- FROM jobs
2485
- WHERE COALESCE(tt_seller, '') = ''
2486
- AND COALESCE(user_update_count, 0) <= 0
2487
- ORDER BY created_at ASC, unique_id ASC
2488
- LIMIT ?
2489
- `,
2490
- )
2491
- .all(l);
2596
+
2597
+ let sql = `
2598
+ SELECT *
2599
+ FROM jobs
2600
+ WHERE COALESCE(tt_seller, '') = ''
2601
+ AND COALESCE(user_update_count, 0) <= 0
2602
+ `;
2603
+ const sqlParams = [];
2604
+
2605
+ if (hasCountryFilter) {
2606
+ const placeholders = targetCountries.map(() => "?").join(", ");
2607
+ sql += ` AND UPPER(COALESCE(guessed_location, '')) IN (${placeholders})`;
2608
+ sqlParams.push(...targetCountries);
2609
+ }
2610
+
2611
+ sql += ` ORDER BY created_at ASC, unique_id ASC LIMIT ?`;
2612
+ sqlParams.push(l);
2613
+
2614
+ const rows = db.prepare(sql).all(...sqlParams);
2492
2615
  if (rows.length === 0) return [];
2493
2616
  const now = Date.now();
2494
2617
  const bumpStmt = db.prepare(
@@ -2520,9 +2643,16 @@ export function createStore(filePath) {
2520
2643
  const ttSellerEmpty =
2521
2644
  u.ttSeller === null || u.ttSeller === undefined || u.ttSeller === "";
2522
2645
  if (!ttSellerEmpty) return false;
2523
- return (
2646
+ if (
2524
2647
  updateCount === null || updateCount === undefined || updateCount <= 0
2525
- );
2648
+ ) {
2649
+ if (hasCountryFilter) {
2650
+ const loc = (u.guessedLocation || "").toUpperCase();
2651
+ return targetCountries.includes(loc);
2652
+ }
2653
+ return true;
2654
+ }
2655
+ return false;
2526
2656
  })
2527
2657
  .slice(0, l);
2528
2658
  // 接受任务时 userUpdateCount + 1
@@ -2814,6 +2944,8 @@ export function createStore(filePath) {
2814
2944
  moveJobsToRawByCountry,
2815
2945
  restoreAttachStuckByCountry,
2816
2946
  restoreRawJobsByCountry,
2947
+ restoreRawJobById,
2948
+ restoreRawJobsByFilter,
2817
2949
  getUsersPage: getUsersPageFromDb,
2818
2950
  getRawJobsPage: getRawJobsPageFromDb,
2819
2951
  getTargetUsers: getTargetUsersFromDb,
@@ -1265,6 +1265,7 @@
1265
1265
  <option value="">全部国家</option>
1266
1266
  </select>
1267
1267
  <button onclick="clearRawFilters()">清空筛选</button>
1268
+ <button onclick="restoreFilteredRawJobs()" style="background:#22c55e;color:#fff;border:none;padding:6px 12px;border-radius:6px;cursor:pointer;font-size:12px;">恢复当前筛选到 jobs</button>
1268
1269
  </div>
1269
1270
  <div class="table-scroll">
1270
1271
  <table>
@@ -1279,6 +1280,7 @@
1279
1280
  <th>来源</th>
1280
1281
  <th>状态</th>
1281
1282
  <th>创建时间</th>
1283
+ <th>操作</th>
1282
1284
  </tr>
1283
1285
  </thead>
1284
1286
  <tbody id="rawTable"></tbody>
@@ -2181,7 +2183,7 @@
2181
2183
  function renderRawJobsTable(users) {
2182
2184
  const el = document.getElementById('rawTable');
2183
2185
  if (!users.length) {
2184
- el.innerHTML = '<tr><td colspan="9" style="color:#666;text-align:center;padding:24px">暂无毛料库任务</td></tr>';
2186
+ el.innerHTML = '<tr><td colspan="10" style="color:#666;text-align:center;padding:24px">暂无毛料库任务</td></tr>';
2185
2187
  return;
2186
2188
  }
2187
2189
  el.innerHTML = users.map(u => {
@@ -2203,6 +2205,7 @@
2203
2205
  <td data-label="来源">${sources || '-'}</td>
2204
2206
  <td data-label="状态">${statusTag}</td>
2205
2207
  <td data-label="创建时间" style="font-size:11px;color:#888">${created}</td>
2208
+ <td data-label="操作" style="text-align:center"><button onclick="restoreRawJob('${u.uniqueId}')" style="background:#22c55e;color:#fff;border:none;padding:4px 10px;border-radius:4px;cursor:pointer;font-size:11px;">恢复</button></td>
2206
2209
  </tr>`;
2207
2210
  }).join('');
2208
2211
  }
@@ -2237,6 +2240,63 @@
2237
2240
  fetchRawJobs();
2238
2241
  }
2239
2242
 
2243
+ async function restoreRawJob(uniqueId) {
2244
+ if (!window.confirm(`确认将 @${uniqueId} 从毛料库恢复到 jobs 队列吗?`)) {
2245
+ return;
2246
+ }
2247
+ try {
2248
+ const res = await fetch('/api/raw-jobs/restore', {
2249
+ method: 'POST',
2250
+ headers: { 'Content-Type': 'application/json' },
2251
+ body: JSON.stringify({ uniqueId })
2252
+ });
2253
+ const data = await res.json();
2254
+ if (!res.ok || data.error) {
2255
+ showToast(data.error || '恢复失败', true);
2256
+ return;
2257
+ }
2258
+ showToast(`@${uniqueId} 已恢复到队列`);
2259
+ await fetchStats();
2260
+ await fetchRawByCountry();
2261
+ await fetchRawJobs();
2262
+ } catch (e) {
2263
+ showToast('恢复失败: ' + e.message, true);
2264
+ }
2265
+ }
2266
+
2267
+ async function restoreFilteredRawJobs() {
2268
+ const search = document.getElementById('rawSearchInput').value.trim();
2269
+ const location = currentRawLocation;
2270
+ let desc = '当前筛选条件';
2271
+ if (search && location) desc = `搜索="${search}" + 国家=${location}`;
2272
+ else if (search) desc = `搜索="${search}"`;
2273
+ else if (location) desc = `国家=${location}`;
2274
+ if (!window.confirm(`确认将毛料库中符合【${desc}】的任务恢复到 jobs 队列吗?`)) {
2275
+ return;
2276
+ }
2277
+ try {
2278
+ const body = {};
2279
+ if (search) body.search = search;
2280
+ if (location) body.location = location;
2281
+ const res = await fetch('/api/raw-jobs/restore', {
2282
+ method: 'POST',
2283
+ headers: { 'Content-Type': 'application/json' },
2284
+ body: JSON.stringify(body)
2285
+ });
2286
+ const data = await res.json();
2287
+ if (!res.ok || data.error) {
2288
+ showToast(data.error || '恢复失败', true);
2289
+ return;
2290
+ }
2291
+ showToast(`已恢复 ${data.restored} 条任务到队列`);
2292
+ await fetchStats();
2293
+ await fetchRawByCountry();
2294
+ await fetchRawJobs();
2295
+ } catch (e) {
2296
+ showToast('恢复失败: ' + e.message, true);
2297
+ }
2298
+ }
2299
+
2240
2300
  async function restoreRawJobsByCountry(country, count) {
2241
2301
  const countText = count != null ? `将恢复 ${formatStatNum(count)} 条。` : '';
2242
2302
  if (!window.confirm(`确认将 ${country} 从毛料库恢复到 jobs 队列吗?${countText}`)) {
@@ -333,9 +333,12 @@ export function startWatchServer(dataAnchor, port = 3000, existingStore) {
333
333
 
334
334
  if (req.method === "GET" && routePath === "/api/user-update-tasks") {
335
335
  const limit = params.limit;
336
- const tasks = store.getPendingUserUpdateTasks(limit);
336
+ const countries = params.countries
337
+ ? params.countries.split(",").map((c) => c.trim().toUpperCase()).filter(Boolean)
338
+ : [];
339
+ const tasks = store.getPendingUserUpdateTasks(limit, countries);
337
340
  const ts = new Date().toISOString().slice(11, 19);
338
- console.error(`[JOB ${ts}] USER-UPDATE-TASKS: ${tasks.length} tasks`);
341
+ console.error(`[JOB ${ts}] USER-UPDATE-TASKS: ${tasks.length} tasks${countries.length ? ` (countries: ${countries.join(",")})` : ""}`);
339
342
  sendJSON(res, 200, { total: tasks.length, tasks });
340
343
  return;
341
344
  }
@@ -540,7 +543,20 @@ export function startWatchServer(dataAnchor, port = 3000, existingStore) {
540
543
  if (req.method === "POST" && routePath === "/api/raw-jobs/restore") {
541
544
  try {
542
545
  const body = await readBody(req);
543
- const result = store.restoreRawJobsByCountry(body.country);
546
+ let result;
547
+ if (body.uniqueId) {
548
+ result = store.restoreRawJobById(body.uniqueId);
549
+ } else if (body.search || body.location) {
550
+ result = store.restoreRawJobsByFilter({
551
+ search: body.search || "",
552
+ location: body.location || "",
553
+ });
554
+ } else if (body.country) {
555
+ result = store.restoreRawJobsByCountry(body.country);
556
+ } else {
557
+ sendJSON(res, 400, { error: "missing filter: uniqueId, country, or search/location" });
558
+ return;
559
+ }
544
560
  if (result.error) {
545
561
  sendJSON(res, 400, result);
546
562
  return;