tt-help-cli-ycl 1.4.11 → 1.4.13

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.4.11",
3
+ "version": "1.4.13",
4
4
  "description": "TikTok user & video data scraper - extract ttSeller, verified, locationCreated from HTML source",
5
5
  "type": "module",
6
6
  "bin": {
@@ -69,6 +69,8 @@ const CFG = {
69
69
  switchPortOnRecycle: true,
70
70
  /** 连续任务处理错误阈值(达到后上报服务端) */
71
71
  consecutiveTaskErrorsMax: 20,
72
+ /** 单端口最大运行时间(ms),超时自动切换端口(默认 48 分钟) */
73
+ maxSessionRunTimeMs: 48 * 60 * 1000,
72
74
  };
73
75
 
74
76
  /** 从环境变量覆写配置 */
@@ -105,6 +107,10 @@ function loadEnvConfig() {
105
107
  1,
106
108
  int("COMMENTS_CONSECUTIVE_TASK_ERRORS_MAX", CFG.consecutiveTaskErrorsMax),
107
109
  );
110
+ CFG.maxSessionRunTimeMs = Math.max(
111
+ 60000,
112
+ int("COMMENTS_MAX_SESSION_RUN_TIME_MS", CFG.maxSessionRunTimeMs),
113
+ );
108
114
  }
109
115
 
110
116
  // ────────────────────────────────────────
@@ -283,6 +289,7 @@ class CdpSessionManager {
283
289
  this.browser = null;
284
290
  this.page = null;
285
291
  this.lastRecycleAt = 0;
292
+ this.sessionStartTime = Date.now();
286
293
  this._lastMemCheck = 0;
287
294
  this._shuttingDown = false;
288
295
  }
@@ -362,14 +369,28 @@ class CdpSessionManager {
362
369
  this.browser = await ensureBrowserReady(opts);
363
370
  this.page = await this._ensurePage();
364
371
  this.lastRecycleAt = Date.now();
372
+ // 端口切换时重置运行计时
373
+ if (oldPort !== undefined && oldPort !== this.port) {
374
+ this.sessionStartTime = Date.now();
375
+ }
365
376
  }
366
377
 
367
- /** 内存检查 + 按需回收(每 30s 最多采样一次) */
378
+ /** 内存检查 + 定时轮换 + 按需回收(每 30s 最多采样一次) */
368
379
  async maybeRecycleForMemory(processedCount) {
369
380
  const now = Date.now();
370
381
  if (now - this._lastMemCheck < 30000) return;
371
382
  this._lastMemCheck = now;
372
383
 
384
+ // 定时轮换:单端口运行超时自动切换
385
+ const sessionElapsed = now - this.sessionStartTime;
386
+ if (sessionElapsed >= CFG.maxSessionRunTimeMs) {
387
+ const mins = Math.round(sessionElapsed / 60000);
388
+ await this.recycleSession(`定时轮换 (运行 ${mins} 分钟)`, {
389
+ switchPort: true,
390
+ });
391
+ return;
392
+ }
393
+
373
394
  const mem = process.memoryUsage();
374
395
  const rssMB = mem.rss / 1024 / 1024;
375
396
  const heapMB = mem.heapUsed / 1024 / 1024;
@@ -507,6 +528,7 @@ async function runAutoMode(options) {
507
528
  maxComments,
508
529
  basePort,
509
530
  proxy: proxyOption,
531
+ locations,
510
532
  } = options;
511
533
 
512
534
  const actualParallel = Math.max(1, parallel || CFG.parallel);
@@ -514,6 +536,7 @@ async function runAutoMode(options) {
514
536
  const actualMaxComments = maxComments || CFG.maxComments;
515
537
  const effectiveProxy = proxyOption || configuredProxy;
516
538
  const portPoolStart = basePort || CFG.portPoolStart;
539
+ const jobLocations = locations || null;
517
540
 
518
541
  loadEnvConfig();
519
542
 
@@ -576,8 +599,9 @@ async function runAutoMode(options) {
576
599
  console.error(`\n╔══════════════════════════════════════════╗`);
577
600
  console.error(`║ 自动评论采集模式(CDP 连接已登录浏览器)`);
578
601
  console.error(
579
- `║ 目标国家: ${DEFAULT_TARGET_LOCATIONS.slice(0, 6).join(",")}...`,
602
+ `║ 目标国家: ${jobLocations || DEFAULT_TARGET_LOCATIONS.slice(0, 6).join(",") + "..."}`,
580
603
  );
604
+ if (jobLocations) console.error(`║ 任务过滤: ${jobLocations}`);
581
605
  console.error(`║ 服务端: ${serverUrl}`);
582
606
  console.error(
583
607
  `║ CDP 端口池: ${portPoolStart}-${portPoolStart + CFG.portPoolSize - 1}(当前: ${session.port})`,
@@ -614,9 +638,10 @@ async function runAutoMode(options) {
614
638
  // ── 领取任务 ──
615
639
  let tasks;
616
640
  try {
617
- const resp = await apiGet(
618
- `${serverUrl}/api/comment-tasks?limit=${actualParallel}`,
619
- );
641
+ const apiUrl = jobLocations
642
+ ? `${serverUrl}/api/comment-tasks?limit=${actualParallel}&locations=${encodeURIComponent(jobLocations)}`
643
+ : `${serverUrl}/api/comment-tasks?limit=${actualParallel}`;
644
+ const resp = await apiGet(apiUrl);
620
645
  tasks = resp.tasks || [];
621
646
  } catch (err) {
622
647
  console.error(` × 获取任务失败: ${err.message}`);
@@ -783,8 +808,12 @@ async function runAutoMode(options) {
783
808
 
784
809
  // 每 10 个任务输出内存汇总
785
810
  if (totalTasks() % 10 === 0) {
811
+ const remainingMs =
812
+ session.sessionStartTime + CFG.maxSessionRunTimeMs - Date.now();
813
+ const remainingMins =
814
+ remainingMs > 0 ? Math.round(remainingMs / 60000) : 0;
786
815
  console.error(
787
- `── #${totalTasks()} ── ${formatMemoryWithBrowser(session.port)} 处理=${processedCount} 跳过=${skippedCount} 错误=${errorCount}`,
816
+ `── #${totalTasks()} ── ${formatMemoryWithBrowser(session.port)} 处理=${processedCount} 跳过=${skippedCount} 错误=${errorCount} 距切换≈${remainingMins}min`,
788
817
  );
789
818
  }
790
819
  } catch (err) {
@@ -1006,6 +1035,7 @@ export async function handleComments(options) {
1006
1035
  commentsServer,
1007
1036
  commentsBasePort,
1008
1037
  commentsProxy,
1038
+ commentsLocations,
1009
1039
  } = options;
1010
1040
 
1011
1041
  // 自动模式:无 URL
@@ -1017,6 +1047,7 @@ export async function handleComments(options) {
1017
1047
  maxComments: commentsMax,
1018
1048
  basePort: commentsBasePort,
1019
1049
  proxy: commentsProxy,
1050
+ locations: commentsLocations,
1020
1051
  });
1021
1052
  }
1022
1053
 
package/src/lib/args.js CHANGED
@@ -670,6 +670,7 @@ function parseCommentsArgs(args) {
670
670
  let commentsServer = defaultServer;
671
671
  let commentsBasePort = 6222;
672
672
  let commentsProxy = null;
673
+ let commentsLocations = null;
673
674
 
674
675
  const positional = [];
675
676
 
@@ -689,6 +690,12 @@ function parseCommentsArgs(args) {
689
690
  commentsBasePort = parseInt(args[++i]) || 6222;
690
691
  } else if (arg === "--proxy") {
691
692
  commentsProxy = args[++i];
693
+ } else if (
694
+ arg === "-l" ||
695
+ arg === "--location" ||
696
+ arg === "--job-locations"
697
+ ) {
698
+ commentsLocations = args[++i];
692
699
  } else {
693
700
  positional.push(arg);
694
701
  }
@@ -709,6 +716,7 @@ function parseCommentsArgs(args) {
709
716
  commentsServer,
710
717
  commentsBasePort,
711
718
  commentsProxy,
719
+ commentsLocations,
712
720
  urls: [],
713
721
  outputFormat: "json",
714
722
  exploreCount: 0,
@@ -217,10 +217,13 @@ const HELP_TEXT = [
217
217
  " -i, --interval 空闲间隔上限秒(默认: 10,实际 1~N 随机)",
218
218
  " -s, --server 服务端地址(默认: http://127.0.0.1:3001)",
219
219
  " -m, --max-comments 每视频最大评论数(默认: 200)",
220
- " --base-port 浏览器CDP端口(默认: 9222)",
220
+ " -l, --location 按国家领取任务,逗号分隔(如: DE,FR,IT)",
221
+ " 未指定时领取所有国家的任务",
222
+ " --base-port 浏览器CDP端口(默认: 6222)",
221
223
  " --save 去重后保存到服务端,来源标记为 comment",
222
224
  " 示例: tt-help comments",
223
- " tt-help comments -p 2 -i 15 --base-port 9223",
225
+ " tt-help comments -p 2 -i 15 --base-port 6223",
226
+ " tt-help comments -l DE,FR,IT -p 5",
224
227
  " tt-help comments <视频URL> 50 --save",
225
228
  "",
226
229
  " tag <标签名> [...] [选项]",
@@ -2647,8 +2647,7 @@ export function createStore(filePath, options = {}) {
2647
2647
  let sql = `
2648
2648
  SELECT *
2649
2649
  FROM jobs_base
2650
- WHERE (COALESCE(tt_seller, '') = '' OR tt_seller = 1)
2651
- AND COALESCE(user_update_count, 0) <= 0
2650
+ WHERE COALESCE(user_update_count, 0) <= 0
2652
2651
  `;
2653
2652
  const sqlParams = [];
2654
2653
 
@@ -2698,13 +2697,7 @@ export function createStore(filePath, options = {}) {
2698
2697
  const pending = data
2699
2698
  .filter((u) => {
2700
2699
  const updateCount = u.userUpdateCount;
2701
- // ttSeller 为空 或 ttSeller=1(商家重处理)都可以领取
2702
- const ttSellerEligible =
2703
- u.ttSeller === null ||
2704
- u.ttSeller === undefined ||
2705
- u.ttSeller === "" ||
2706
- u.ttSeller === 1;
2707
- if (!ttSellerEligible) return false;
2700
+ // jobs_base 中所有 user_update_count <= 0 的记录都可以领取
2708
2701
  if (
2709
2702
  updateCount === null ||
2710
2703
  updateCount === undefined ||
@@ -3137,20 +3130,32 @@ export function createStore(filePath, options = {}) {
3137
3130
  return videos.length;
3138
3131
  }
3139
3132
 
3140
- function getPendingCommentTasks(limit) {
3133
+ function getPendingCommentTasks(limit, locations = null) {
3141
3134
  if (getDb()) {
3142
3135
  const l = Math.max(1, parseInt(limit) || 1);
3136
+ const normalizedLocations =
3137
+ Array.isArray(locations) && locations.length
3138
+ ? locations
3139
+ .map((loc) => String(loc).trim().toUpperCase())
3140
+ .filter(Boolean)
3141
+ : null;
3142
+
3143
+ const locationFilter = normalizedLocations
3144
+ ? `AND UPPER(COALESCE(location_created, '')) IN (${normalizedLocations.map(() => "?").join(", ")})`
3145
+ : "";
3146
+
3143
3147
  const rows = getDb()
3144
3148
  .prepare(
3145
3149
  `
3146
3150
  SELECT *
3147
3151
  FROM videos
3148
- WHERE user_update_count IS NULL OR user_update_count <= 0
3152
+ WHERE (user_update_count IS NULL OR user_update_count <= 0)
3153
+ ${locationFilter}
3149
3154
  ORDER BY tt_seller DESC, registered_at ASC
3150
3155
  LIMIT ?
3151
3156
  `,
3152
3157
  )
3153
- .all(l);
3158
+ .all(...(normalizedLocations || []), l);
3154
3159
  if (rows.length === 0) return [];
3155
3160
  const bumpStmt = getDb().prepare(
3156
3161
  `
@@ -3171,7 +3176,19 @@ export function createStore(filePath, options = {}) {
3171
3176
  }
3172
3177
 
3173
3178
  // 筛选待处理视频(userUpdateCount <= 0 或 null/undefined)
3174
- const pending = videos.filter((v) => (v.userUpdateCount || 0) <= 0);
3179
+ let pending = videos.filter((v) => (v.userUpdateCount || 0) <= 0);
3180
+ // 按 locations 过滤
3181
+ if (Array.isArray(locations) && locations.length > 0) {
3182
+ const upperLocs = locations.map((loc) =>
3183
+ String(loc).trim().toUpperCase(),
3184
+ );
3185
+ pending = pending.filter((v) => {
3186
+ const loc = String(v.locationCreated || "")
3187
+ .trim()
3188
+ .toUpperCase();
3189
+ return upperLocs.includes(loc);
3190
+ });
3191
+ }
3175
3192
  // ttSeller=true 优先
3176
3193
  pending.sort((a, b) => {
3177
3194
  if (a.ttSeller && !b.ttSeller) return -1;
@@ -3395,14 +3412,13 @@ export function createStore(filePath, options = {}) {
3395
3412
  };
3396
3413
  }
3397
3414
 
3398
- // 5. 验证:统计 jobs_base 中可被 attach 领取的记录数(tt_seller=1 且 user_update_count<=0)
3415
+ // 5. 验证:统计 jobs_base 中可被 attach 领取的记录数(user_update_count<=0)
3399
3416
  let available = 0;
3400
3417
  try {
3401
3418
  const row = db
3402
3419
  .prepare(
3403
3420
  `SELECT COUNT(*) as total FROM jobs_base
3404
- WHERE tt_seller = 1
3405
- AND COALESCE(user_update_count, 0) <= 0
3421
+ WHERE COALESCE(user_update_count, 0) <= 0
3406
3422
  AND video_count = 0`,
3407
3423
  )
3408
3424
  .get();
@@ -278,8 +278,84 @@ export function initDb(filePath) {
278
278
  migrateColumns("tags", {
279
279
  discover_round: "INTEGER DEFAULT 0",
280
280
  strategy: "TEXT",
281
+ dead_reason: "TEXT",
282
+ direction_id: "INTEGER",
281
283
  });
282
284
 
285
+ // ===== tag_directions 表(分层探索:通用词/分类/产品) =====
286
+ db.exec(`
287
+ CREATE TABLE IF NOT EXISTS tag_directions (
288
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
289
+ name TEXT NOT NULL,
290
+ level INTEGER NOT NULL DEFAULT 1,
291
+ parent_id INTEGER,
292
+ country TEXT,
293
+ lang_group TEXT,
294
+ status TEXT NOT NULL DEFAULT 'active',
295
+ total_experiments INTEGER DEFAULT 0,
296
+ productive_experiments INTEGER DEFAULT 0,
297
+ best_score REAL DEFAULT 0,
298
+ notes TEXT,
299
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
300
+ updated_at TEXT
301
+ )
302
+ `);
303
+ migrateColumns("tag_directions", {
304
+ lang_group: "TEXT",
305
+ });
306
+
307
+ // ===== tag_experiments 表(每次探索的实验记录) =====
308
+ db.exec(`
309
+ CREATE TABLE IF NOT EXISTS tag_experiments (
310
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
311
+ direction_id INTEGER,
312
+ country TEXT NOT NULL,
313
+ hypothesis TEXT,
314
+ strategy TEXT,
315
+ tags_tried TEXT NOT NULL DEFAULT '[]',
316
+ tags_productive TEXT NOT NULL DEFAULT '[]',
317
+ tags_dead_not_exist TEXT NOT NULL DEFAULT '[]',
318
+ tags_dead_no_match TEXT NOT NULL DEFAULT '[]',
319
+ conclusion TEXT,
320
+ productive_count INTEGER DEFAULT 0,
321
+ dead_count INTEGER DEFAULT 0,
322
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
323
+ )
324
+ `);
325
+
326
+ // ===== tag_analyst_reports 表(LLM 分析师定期体检报告) =====
327
+ db.exec(`
328
+ CREATE TABLE IF NOT EXISTS tag_analyst_reports (
329
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
330
+ report_type TEXT NOT NULL DEFAULT 'daily',
331
+ countries TEXT NOT NULL DEFAULT '[]',
332
+ summary TEXT,
333
+ top_directions TEXT NOT NULL DEFAULT '[]',
334
+ worst_directions TEXT NOT NULL DEFAULT '[]',
335
+ cross_country_suggestions TEXT NOT NULL DEFAULT '[]',
336
+ cleanup_suggestions TEXT NOT NULL DEFAULT '[]',
337
+ next_strategy TEXT,
338
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
339
+ )
340
+ `);
341
+
342
+ // ===== cross_country_learnings 表(跨国家经验复用记录) =====
343
+ db.exec(`
344
+ CREATE TABLE IF NOT EXISTS cross_country_learnings (
345
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
346
+ source_country TEXT NOT NULL,
347
+ source_tag TEXT NOT NULL,
348
+ source_direction_id INTEGER,
349
+ target_country TEXT NOT NULL,
350
+ target_tag TEXT,
351
+ transferred_at TEXT,
352
+ result_status TEXT,
353
+ score REAL,
354
+ notes TEXT,
355
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
356
+ )
357
+ `);
358
+
283
359
  // ===== discover_log 表(记录每次 LLM discover 的策略和思考) =====
284
360
  db.exec(`
285
361
  CREATE TABLE IF NOT EXISTS discover_log (
@@ -334,6 +410,12 @@ export function initDb(filePath) {
334
410
  `CREATE INDEX IF NOT EXISTS idx_tags_score ON tags(score DESC)`,
335
411
  `CREATE INDEX IF NOT EXISTS idx_discover_log_country ON discover_log(country, round DESC)`,
336
412
  `CREATE INDEX IF NOT EXISTS idx_reflection_rules_active ON reflection_rules(country, is_active)`,
413
+ `CREATE INDEX IF NOT EXISTS idx_tag_directions_country ON tag_directions(country, level)`,
414
+ `CREATE INDEX IF NOT EXISTS idx_tag_experiments_country ON tag_experiments(country, created_at DESC)`,
415
+ `CREATE INDEX IF NOT EXISTS idx_tag_experiments_direction ON tag_experiments(direction_id)`,
416
+ `CREATE INDEX IF NOT EXISTS idx_cross_country_source ON cross_country_learnings(source_country, result_status)`,
417
+ `CREATE INDEX IF NOT EXISTS idx_cross_country_target ON cross_country_learnings(target_country)`,
418
+ `CREATE INDEX IF NOT EXISTS idx_tags_dead_reason ON tags(dead_reason)`,
337
419
  ];
338
420
  for (const sql of indexes) {
339
421
  db.exec(sql);
@@ -50,7 +50,7 @@ export function getDashboardStatsFromDb(targetLocations = []) {
50
50
 
51
51
  const userUpdateTasksRow = db
52
52
  .prepare(
53
- `SELECT COUNT(*) as userUpdateTasks FROM jobs_base WHERE COALESCE(tt_seller, '') = '' AND COALESCE(user_update_count, 0) <= 0`,
53
+ `SELECT COUNT(*) as userUpdateTasks FROM jobs_base WHERE COALESCE(user_update_count, 0) <= 0`,
54
54
  )
55
55
  .get();
56
56
 
@@ -147,7 +147,7 @@ export function getUserUpdateByCountryFromDb() {
147
147
  .prepare(
148
148
  `SELECT COALESCE(guessed_location, '未知') as country, COUNT(*) as count
149
149
  FROM jobs_base
150
- WHERE tt_seller IS NULL AND COALESCE(user_update_count, 0) <= 0
150
+ WHERE COALESCE(user_update_count, 0) <= 0
151
151
  GROUP BY COALESCE(guessed_location, '未知')
152
152
  ORDER BY count DESC`,
153
153
  )
@@ -161,7 +161,7 @@ export function getAttachStuckByCountryFromDb() {
161
161
  .prepare(
162
162
  `SELECT COALESCE(guessed_location, '未知') as country, COUNT(*) as count
163
163
  FROM jobs_base
164
- WHERE tt_seller IS NULL AND COALESCE(user_update_count, 0) = 1
164
+ WHERE COALESCE(user_update_count, 0) = 1
165
165
  GROUP BY COALESCE(guessed_location, '未知')
166
166
  ORDER BY count DESC`,
167
167
  )
@@ -194,7 +194,7 @@ export function restoreAttachStuckByCountry(country) {
194
194
  };
195
195
  }
196
196
 
197
- const whereSql = `COALESCE(tt_seller, '') = '' AND COALESCE(user_update_count, 0) = 1 AND COALESCE(guessed_location, '未知') = ?`;
197
+ const whereSql = `COALESCE(user_update_count, 0) = 1 AND COALESCE(guessed_location, '未知') = ?`;
198
198
  const count =
199
199
  db
200
200
  .prepare(`SELECT COUNT(*) as c FROM jobs_base WHERE ${whereSql}`)
@@ -171,6 +171,23 @@ export function reportTagScore(tag, fields) {
171
171
  : null;
172
172
  const now = new Date().toISOString();
173
173
 
174
+ // 自动分类 dead_reason
175
+ let deadReason = null;
176
+ if (status === "dead" && error) {
177
+ const errLower = error.toLowerCase();
178
+ if (
179
+ errLower.includes("no videos found") ||
180
+ errLower.includes("tag_not_found") ||
181
+ errLower.includes("找不到")
182
+ ) {
183
+ deadReason = "not_exist";
184
+ } else if ((matchedAuthors ?? 0) === 0) {
185
+ deadReason = "no_match";
186
+ } else {
187
+ deadReason = "wrong_country";
188
+ }
189
+ }
190
+
174
191
  try {
175
192
  const result = db
176
193
  .prepare(
@@ -183,6 +200,7 @@ export function reportTagScore(tag, fields) {
183
200
  matched_countries = COALESCE(?, matched_countries),
184
201
  pushed_users = COALESCE(?, pushed_users),
185
202
  last_error = COALESCE(?, last_error),
203
+ dead_reason = COALESCE(?, dead_reason),
186
204
  scored_at = ?,
187
205
  score_count = score_count + 1
188
206
  WHERE tag = ?`,
@@ -196,6 +214,7 @@ export function reportTagScore(tag, fields) {
196
214
  matchedCountriesJson,
197
215
  pushedUsers ?? null,
198
216
  error ?? null,
217
+ deadReason,
199
218
  now,
200
219
  tag,
201
220
  );
@@ -163,7 +163,6 @@ function renderStats() {
163
163
  if (statJobsBase)
164
164
  statJobsBase.textContent = `jobs_base: ${formatStatNum(d.jobsBaseTotal || 0)}`;
165
165
  flashEl("statError", d.errorUsers);
166
- flashEl("statRestricted", d.restrictedUsers);
167
166
  flashEl("statTarget", d.targetUsers, { full: true });
168
167
  flashEl("statUserUpdateTasks", d.userUpdateTasks || 0, { full: true });
169
168
  flashEl("statRawJobs", d.rawJobs || 0);
@@ -376,7 +375,6 @@ function renderSourceChart(sources) {
376
375
  following: "关注",
377
376
  follower: "粉丝",
378
377
  processed: "已处理",
379
- restricted: "受限(跳过)",
380
378
  error: "错误(待重试)",
381
379
  noVideo: "无视频",
382
380
  };
@@ -419,13 +417,10 @@ function renderTable(users) {
419
417
  const nowStatus = u.status;
420
418
  const changed =
421
419
  wasStatus !== nowStatus &&
422
- (nowStatus === "done" ||
423
- nowStatus === "restricted" ||
424
- nowStatus === "error");
420
+ (nowStatus === "done" || nowStatus === "error");
425
421
  const rowClass = changed ? ' class="row-flash"' : "";
426
422
 
427
423
  const statusTags = {
428
- restricted: '<span class="tag error">受限(跳过)</span>',
429
424
  error: '<span class="tag error">错误(待重试)</span>',
430
425
  done: '<span class="tag processed">已完成</span>',
431
426
  processing: '<span class="tag processing">处理中</span>',
@@ -991,33 +986,6 @@ async function resetJob(uniqueId) {
991
986
  }
992
987
  }
993
988
 
994
- async function moveRestrictedToBase() {
995
- const count = currentStats?.restrictedUsers || 0;
996
- if (count === 0) {
997
- showToast("没有受限用户");
998
- return;
999
- }
1000
- if (!confirm(`确定将 ${count} 个受限用户移到 jobs_base 吗?`)) return;
1001
- showLoading("迁移中...");
1002
- try {
1003
- const res = await fetch("/api/jobs/move-restricted-to-base", {
1004
- method: "POST",
1005
- });
1006
- const data = await res.json();
1007
- if (data.error) {
1008
- showToast(data.error, true);
1009
- return;
1010
- }
1011
- showToast(`已迁移 ${data.moved} 个受限用户`);
1012
- fetchStats();
1013
- fetchUsers();
1014
- } catch (e) {
1015
- showToast("迁移失败: " + e.message, true);
1016
- } finally {
1017
- hideLoading();
1018
- }
1019
- }
1020
-
1021
989
  async function batchResetErrors() {
1022
990
  const errorUsers = currentUsers.filter((u) => u.status === "error");
1023
991
  if (errorUsers.length === 0) {
@@ -1762,8 +1730,6 @@ function renderTargetTable() {
1762
1730
  statusTag = '<span class="tag pending">待处理</span>';
1763
1731
  else if (u.status === "error")
1764
1732
  statusTag = '<span class="tag error">错误</span>';
1765
- else if (u.status === "restricted")
1766
- statusTag = '<span class="tag error">受限</span>';
1767
1733
  else statusTag = u.status || "-";
1768
1734
 
1769
1735
  return `<tr data-user="${u.uniqueId}">
@@ -41,10 +41,6 @@
41
41
  <div class="label">错误</div>
42
42
  <div class="value error" id="statError">0</div>
43
43
  </div>
44
- <div class="stat-card clickable" onclick="moveRestrictedToBase()">
45
- <div class="label">受限</div>
46
- <div class="value error" id="statRestricted">0</div>
47
- </div>
48
44
  <div class="stat-card clickable pending-card" id="statUserUpdateCard" onclick="navigateToUserUpdate()">
49
45
  <div class="label">待补资料</div>
50
46
  <div class="value target" id="statUserUpdateTasks">0</div>
@@ -122,7 +118,6 @@
122
118
  <button data-filter="pending" onclick="setFilter('pending')">待处理</button>
123
119
  <button data-filter="done" onclick="setFilter('done')">已完成</button>
124
120
  <button data-filter="error" onclick="setFilter('error')">错误</button>
125
- <button data-filter="restricted" onclick="setFilter('restricted')">受限</button>
126
121
  <button data-filter="target" onclick="setFilter('target')" style="background:#7c3aed;color:#fff">目标商家</button>
127
122
  <select id="targetLocationFilter" onchange="onTargetLocationChange()"
128
123
  style="padding:6px 10px;border:1px solid #7c3aed;border-radius:6px;background:#2a2a3a;color:#ccc;font-size:12px;cursor:pointer;outline:none;display:none">