tt-help-cli-ycl 1.3.62 → 1.3.64

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/lib/args.js CHANGED
@@ -180,6 +180,7 @@ function parseExploreArgs(args) {
180
180
  let explorePortCount = null;
181
181
  let exploreUserId = null;
182
182
  let exploreMaxVideos = 16;
183
+ let exploreProxy = null;
183
184
 
184
185
  const positional = [];
185
186
  const PRESETS = ["fast", "normal", "slow", "stealth"];
@@ -218,6 +219,8 @@ function parseExploreArgs(args) {
218
219
  exploreUserId = args[++i];
219
220
  } else if (arg === "--max-videos") {
220
221
  exploreMaxVideos = parseInt(args[++i]) || 16;
222
+ } else if (arg === "--proxy") {
223
+ exploreProxy = args[++i];
221
224
  } else {
222
225
  positional.push(arg);
223
226
  }
@@ -258,6 +261,7 @@ function parseExploreArgs(args) {
258
261
  explorePortCount,
259
262
  exploreUserId,
260
263
  exploreMaxVideos,
264
+ exploreProxy,
261
265
  urls: [],
262
266
  outputFormat: "json",
263
267
  exploreCount: 0,
@@ -439,39 +443,77 @@ function parseDbImportArgs(args) {
439
443
  function parseRefreshArgs(args) {
440
444
  let serverUrl = defaultServer;
441
445
  let explorePreset = "normal";
442
- let exploreMaxComments = 10;
443
- let exploreMaxGuess = 0;
446
+ let exploreInterval = 30;
447
+ let exploreEnableFollow = true;
448
+ let exploreMaxFollowing = 100;
449
+ let exploreMaxFollowers = 100;
450
+ let exploreLocation = DEFAULT_TARGET_LOCATIONS_CSV;
451
+ let exploreMaxUsers = 0;
444
452
  let explorePort = null;
453
+ let exploreBasePort = null;
454
+ let explorePortCount = null;
445
455
  let exploreProfile = null;
446
456
  let exploreUserId = null;
457
+ let exploreMaxVideos = 16;
458
+ let exploreRedoMaxAge = 0;
459
+ let exploreProxy = null;
447
460
 
448
461
  for (let i = 0; i < args.length; i++) {
449
462
  const arg = args[i];
450
463
  if (arg === "--server") {
451
464
  serverUrl = args[++i];
452
- } else if (arg === "--comments") {
453
- exploreMaxComments = parseInt(args[++i]) || 10;
454
- } else if (arg === "--guess") {
455
- exploreMaxGuess = parseInt(args[++i]) || 0;
456
465
  } else if (arg === "--preset") {
457
466
  explorePreset = args[++i];
467
+ } else if (arg === "-i" || arg === "--interval") {
468
+ exploreInterval = parseInt(args[++i], 10) || 30;
458
469
  } else if (arg === "--port") {
459
470
  explorePort = parseInt(args[++i]) || 9222;
471
+ } else if (arg === "--base-port") {
472
+ exploreBasePort = parseInt(args[++i]) || 9222;
473
+ } else if (arg === "--port-count") {
474
+ explorePortCount = parseInt(args[++i]) || 10;
460
475
  } else if (arg === "--profile") {
461
476
  exploreProfile = args[++i];
462
477
  } else if (arg === "--user-id") {
463
478
  exploreUserId = args[++i];
479
+ } else if (arg === "--max-videos") {
480
+ exploreMaxVideos = parseInt(args[++i]) || 16;
481
+ } else if (arg === "--max-following") {
482
+ exploreMaxFollowing = parseInt(args[++i]) || 100;
483
+ } else if (arg === "--max-followers") {
484
+ exploreMaxFollowers = parseInt(args[++i]) || 100;
485
+ } else if (arg === "--max-age") {
486
+ exploreRedoMaxAge = parseInt(args[++i]) || 43200;
487
+ } else if (arg === "--proxy") {
488
+ exploreProxy = args[++i];
489
+ } else if (arg === "--location") {
490
+ exploreLocation = args[++i];
491
+ } else if (arg === "--enable-follow") {
492
+ exploreEnableFollow = true;
493
+ } else if (arg === "--disable-follow") {
494
+ exploreEnableFollow = false;
495
+ } else if (arg === "--max-users") {
496
+ exploreMaxUsers = parseInt(args[++i]) || 0;
464
497
  }
465
498
  }
466
499
 
467
500
  return {
468
501
  subcommand: "refresh",
469
502
  explorePreset,
470
- exploreMaxComments,
471
- exploreMaxGuess,
503
+ exploreInterval,
504
+ exploreEnableFollow,
505
+ exploreMaxFollowing,
506
+ exploreMaxFollowers,
507
+ exploreLocation,
508
+ exploreMaxUsers,
472
509
  explorePort,
510
+ exploreBasePort,
511
+ explorePortCount,
473
512
  exploreProfile,
474
513
  exploreUserId,
514
+ exploreMaxVideos,
515
+ exploreRedoMaxAge,
516
+ exploreProxy,
475
517
  serverUrl,
476
518
  urls: [],
477
519
  outputFormat: "json",
@@ -721,6 +763,10 @@ export function parseArgs() {
721
763
  return parseDbImportArgs(args.slice(1));
722
764
  }
723
765
 
766
+ if (args.length > 0 && args[0] === "refresh") {
767
+ return parseRefreshArgs(args.slice(1));
768
+ }
769
+
724
770
  const urls = [];
725
771
  let inputFile = null;
726
772
  let outputFile = null;
@@ -143,7 +143,7 @@ function killEdgeProcesses(targetDir) {
143
143
  });
144
144
  }
145
145
 
146
- function launchEdgeWithCDP(port, userDataDir) {
146
+ function launchEdgeWithCDP(port, userDataDir, proxyServer) {
147
147
  return new Promise((resolve, reject) => {
148
148
  const platform = os.platform();
149
149
  const edgePath = getEdgePath();
@@ -162,14 +162,19 @@ function launchEdgeWithCDP(port, userDataDir) {
162
162
  "--disable-breakpad",
163
163
  "--disable-background-networking",
164
164
  "--disable-sync",
165
- ].join(" ");
165
+ ];
166
+ if (proxyServer) {
167
+ extraArgs.push(`--proxy-server="${proxyServer}"`);
168
+ }
169
+
170
+ const argsStr = extraArgs.join(" ");
166
171
 
167
172
  if (platform === "darwin") {
168
- command = `open -a ${edgePath} --new --args ${extraArgs}`;
173
+ command = `open -a ${edgePath} --new --args ${argsStr}`;
169
174
  } else if (platform === "win32") {
170
- command = `start msedge ${extraArgs}`;
175
+ command = `start msedge ${argsStr}`;
171
176
  } else {
172
- command = `msedge ${extraArgs} &`;
177
+ command = `msedge ${argsStr} &`;
173
178
  }
174
179
 
175
180
  exec(command, (err) => {
@@ -194,6 +199,7 @@ export { killEdgeProcesses };
194
199
  export async function ensureBrowserReady(options = {}) {
195
200
  const port = options.port || DEFAULT_CDP_PORT;
196
201
  const userDataDir = options.userDataDir || DEFAULT_USER_DATA_DIR;
202
+ const proxyServer = options.proxyServer || null;
197
203
  const isCustom = port !== DEFAULT_CDP_PORT || !!options.userDataDir;
198
204
 
199
205
  const isReady = await checkCDPPort(port);
@@ -68,6 +68,8 @@ export async function withBrowserRecovery(fn, browser, page, cdpOptions, port) {
68
68
 
69
69
  const DOM_CHECK_TIMEOUT = 20000; // 单次 DOM 检测超时 20 秒
70
70
  const DOM_CHECK_RETRIES = 3; // DOM 检测最大重试次数
71
+ const SAFE_CHECK_ROUNDS = 2; // 安全登录检测轮数(首次检测 + 额外1轮确认)
72
+ const SAFE_CHECK_INTERVAL = 5000; // 安全登录检测每轮间隔 5 秒
71
73
 
72
74
  /**
73
75
  * 判断登录状态:Cookie 为主,DOM 验真为辅。
@@ -102,6 +104,45 @@ export async function isLoggedIn(page) {
102
104
  return true;
103
105
  }
104
106
 
107
+ /**
108
+ * 安全登录检测:发现未登录时多检测几轮,避免因 TikTok 页面渲染延迟导致误判。
109
+ * - 首次检测为已登录 → 直接返回 true
110
+ * - 首次检测为未登录 → 等待后重新导航并检测,连续 SAFE_CHECK_ROUNDS 轮都为未登录才确认
111
+ * - 任何一轮检测为已登录 → 立即返回 true
112
+ */
113
+ export async function safeCheckLogin(page) {
114
+ // 第一轮检测
115
+ let loggedIn = await isLoggedIn(page);
116
+ if (loggedIn) {
117
+ console.error(`[安全登录检测] 第 1 轮: 已登录 ✓`);
118
+ return true;
119
+ }
120
+ console.error(
121
+ `[安全登录检测] 第 1 轮: 未登录 ✗,等待 ${SAFE_CHECK_INTERVAL / 1000}s 后重检...`,
122
+ );
123
+
124
+ // 后续轮次:等待后重新导航到 TikTok 页面再检测
125
+ for (let round = 2; round <= SAFE_CHECK_ROUNDS; round++) {
126
+ await new Promise((r) => setTimeout(r, SAFE_CHECK_INTERVAL));
127
+ // 重新导航到 TikTok 页面,确保页面状态刷新
128
+ await page.goto("https://www.tiktok.com", {
129
+ waitUntil: "domcontentloaded",
130
+ });
131
+ loggedIn = await isLoggedIn(page);
132
+ if (loggedIn) {
133
+ console.error(`[安全登录检测] 第 ${round} 轮: 已登录 ✓`);
134
+ return true;
135
+ }
136
+ console.error(`[安全登录检测] 第 ${round} 轮: 未登录 ✗`);
137
+ }
138
+
139
+ // 连续 SAFE_CHECK_ROUNDS 轮都为未登录
140
+ console.error(
141
+ `[安全登录检测] 连续 ${SAFE_CHECK_ROUNDS} 轮均为未登录,确认为未登录`,
142
+ );
143
+ return false;
144
+ }
145
+
105
146
  /**
106
147
  * 通过 DOM 元素判断登录状态(验真方案)
107
148
  * 使用 locator API + state: 'attached' 来避免 CDP 连接下 waitForSelector 的可见性问题
@@ -131,12 +131,33 @@ const HELP_TEXT = [
131
131
  " --disable-follow 禁用关注/粉丝提取",
132
132
  " --max-following <数量> 最大获取关注数,默认 50",
133
133
  " --max-followers <数量> 最大获取粉丝数,默认 50",
134
- " --max-users <数量> 最大处理用户数,默认无限制",
135
- " -i, --interval <秒数> 无任务时轮询间隔,默认 10 秒",
136
- " --port <端口号> 固定 CDP 端口(调试用,关闭自动轮换)",
137
- " --base-port <端口号> 起始端口,默认 9222",
138
- " --port-count <数量> 端口数量(账户数),默认 10",
139
- " --user-id <编号> 客户端编号(设备ID),默认自动生成",
134
+ " --max-users <数量> 最大处理用户数,默认无限制",
135
+ " -i, --interval <秒数> 无任务时轮询间隔,默认 10 秒",
136
+ " --port <端口号> 固定 CDP 端口(调试用,关闭自动轮换)",
137
+ " --base-port <端口号> 起始端口,默认 9222",
138
+ " --port-count <数量> 端口数量(账户数),默认 10",
139
+ " --user-id <编号> 客户端编号(设备ID),默认自动生成",
140
+ " --proxy <地址> 浏览器代理(如 socks5://127.0.0.1:1080)",
141
+ "",
142
+ " tt-help refresh [选项]",
143
+ " 对目标商家用户进行轮回刷新,重新采集视频 + 关注 + 粉丝",
144
+ " 筛选条件: tt_seller=1, verified=0, 目标国家",
145
+ " 选项:",
146
+ " --server <URL> 服务端地址,默认 http://127.0.0.1:3001",
147
+ ` --location <国家代码> 国家筛选,逗号分隔,默认 ${DEFAULT_TARGET_LOCATIONS_CSV}`,
148
+ " --max-videos <数量> 每用户最大视频数,默认 16",
149
+ " --enable-follow 启用关注/粉丝提取(默认启用)",
150
+ " --disable-follow 禁用关注/粉丝提取",
151
+ " --max-following <数量> 最大获取关注数,默认 100",
152
+ " --max-followers <数量> 最大获取粉丝数,默认 100",
153
+ " --max-users <数量> 最大处理用户数,默认无限制",
154
+ " -i, --interval <秒数> 无任务时轮询间隔,默认 30 秒",
155
+ " --max-age <秒数> 最小刷新间隔,默认 43200(12小时)",
156
+ " --port <端口号> 固定 CDP 端口(调试用,关闭自动轮换)",
157
+ " --base-port <端口号> 起始端口,默认 9222",
158
+ " --port-count <数量> 端口数量(账户数),默认 10",
159
+ " --user-id <编号> 客户端编号(设备ID),默认自动生成",
160
+ " --proxy <地址> 浏览器代理(如 socks5://127.0.0.1:1080)",
140
161
  "",
141
162
  " tt-help info <URL> [URL2 ...] [--onlyvideo]",
142
163
  " 获取用户/视频信息,支持多个 URL",
@@ -196,9 +217,10 @@ const HELP_TEXT = [
196
217
  " -h, --help 显示帮助",
197
218
  " --version 显示版本号",
198
219
  "",
199
- " 示例: tt-help info https://www.tiktok.com/@nike https://www.tiktok.com/@adidas",
200
- " tt-help explore qiqi23280 fast --location ES --max-comments 50",
201
- " tt-help config set server http://127.0.0.1:3001",
220
+ " 示例: tt-help info https://www.tiktok.com/@nike https://www.tiktok.com/@adidas",
221
+ " tt-help explore qiqi23280 fast --location ES --max-comments 50",
222
+ " tt-help refresh --server http://127.0.0.1:3001 --port 9222",
223
+ " tt-help config set server http://127.0.0.1:3001",
202
224
  " tt-help attach -p 5 -i 10",
203
225
  " tt-help watch -o data/result.db",
204
226
  " tt-help videostats data/result.db -p 3",
package/src/main.js CHANGED
@@ -10,6 +10,7 @@ import { handleComments } from "./cli/comments.js";
10
10
  import { handleVideoStats } from "./cli/videostats.js";
11
11
  import { handleDbImport } from "./cli/db-import.js";
12
12
  import { handleWebserver } from "./cli/webserver.js";
13
+ import { handleRefresh } from "./cli/refresh.js";
13
14
 
14
15
  async function main() {
15
16
  const parsed = parseArgs();
@@ -33,6 +34,8 @@ async function main() {
33
34
  return handleVideoStats(parsed);
34
35
  case "db-import":
35
36
  return handleDbImport(parsed);
37
+ case "refresh":
38
+ return handleRefresh(parsed);
36
39
  }
37
40
 
38
41
  const {
package/src/npm-main.js CHANGED
@@ -6,6 +6,7 @@ import { handleAttach } from "./cli/attach.js";
6
6
  import { handleConfig, showConfig, showUsage, version } from "./cli/config.js";
7
7
  import { handleOpen } from "./cli/open.js";
8
8
  import { handleComments } from "./cli/comments.js";
9
+ import { handleRefresh } from "./cli/refresh.js";
9
10
 
10
11
  function exitUnsupportedCommand(command) {
11
12
  console.error(
@@ -33,6 +34,8 @@ async function main() {
33
34
  return handleOpen(parsed);
34
35
  case "comments":
35
36
  return handleComments(parsed);
37
+ case "refresh":
38
+ return handleRefresh(parsed);
36
39
  }
37
40
 
38
41
  const {
@@ -775,6 +775,53 @@ function restoreAttachStuckByCountry(country) {
775
775
  return { restored: count, country: normalizedCountry };
776
776
  }
777
777
 
778
+ function resetPendingByCountry(country) {
779
+ if (!db) {
780
+ return { reset: 0, country, error: "db not ready" };
781
+ }
782
+
783
+ const normalizedCountry = String(country == null ? "未知" : country).trim();
784
+ if (!normalizedCountry) {
785
+ return {
786
+ reset: 0,
787
+ country: normalizedCountry,
788
+ error: "country is required",
789
+ };
790
+ }
791
+
792
+ const whereSql = `
793
+ status = 'pending'
794
+ AND COALESCE(guessed_location, '未知') = ?
795
+ `;
796
+ const count =
797
+ db
798
+ .prepare(
799
+ `
800
+ SELECT COUNT(*) as c
801
+ FROM jobs
802
+ WHERE ${whereSql}
803
+ `,
804
+ )
805
+ .get(normalizedCountry)?.c || 0;
806
+
807
+ if (!count) {
808
+ return { reset: 0, country: normalizedCountry };
809
+ }
810
+
811
+ db.prepare(
812
+ `
813
+ UPDATE jobs
814
+ SET user_update_count = 0,
815
+ updated_at = ?,
816
+ claimed_by = NULL,
817
+ claimed_at = NULL
818
+ WHERE ${whereSql}
819
+ `,
820
+ ).run(Date.now(), normalizedCountry);
821
+
822
+ return { reset: count, country: normalizedCountry };
823
+ }
824
+
778
825
  function getRawByCountryFromDb() {
779
826
  if (!db) return [];
780
827
 
@@ -1077,7 +1124,7 @@ function restoreRawJobById(uniqueId) {
1077
1124
  return { restored: 1, uniqueId: safeId };
1078
1125
  }
1079
1126
 
1080
- function restoreRawJobsByFilter({ search, location }) {
1127
+ function restoreRawJobsByFilter({ search, location, hasVideo, hasFollower }) {
1081
1128
  if (!db) {
1082
1129
  return { restored: 0, error: "db not ready" };
1083
1130
  }
@@ -1098,6 +1145,14 @@ function restoreRawJobsByFilter({ search, location }) {
1098
1145
  args.push(location);
1099
1146
  }
1100
1147
 
1148
+ if (hasVideo) {
1149
+ where.push("COALESCE(video_count, 0) > 0");
1150
+ }
1151
+
1152
+ if (hasFollower) {
1153
+ where.push("COALESCE(follower_count, 0) > 0");
1154
+ }
1155
+
1101
1156
  if (where.length === 0) {
1102
1157
  return { restored: 0, error: "at least one filter is required" };
1103
1158
  }
@@ -1140,7 +1195,14 @@ function restoreRawJobsByFilter({ search, location }) {
1140
1195
  return { restored: count };
1141
1196
  }
1142
1197
 
1143
- function getRawJobsPageFromDb({ search, location, limit, offset }) {
1198
+ function getRawJobsPageFromDb({
1199
+ search,
1200
+ location,
1201
+ limit,
1202
+ offset,
1203
+ hasVideo,
1204
+ hasFollower,
1205
+ }) {
1144
1206
  if (!db) return null;
1145
1207
 
1146
1208
  const safeLimit = Math.max(1, Math.min(200, parseInt(limit) || 50));
@@ -1159,6 +1221,12 @@ function getRawJobsPageFromDb({ search, location, limit, offset }) {
1159
1221
  where.push("COALESCE(guessed_location, '未知') = ?");
1160
1222
  args.push(location);
1161
1223
  }
1224
+ if (hasVideo) {
1225
+ where.push("COALESCE(video_count, 0) > 0");
1226
+ }
1227
+ if (hasFollower) {
1228
+ where.push("COALESCE(follower_count, 0) > 0");
1229
+ }
1162
1230
 
1163
1231
  const whereSql = where.length ? `WHERE ${where.join(" AND ")}` : "";
1164
1232
  const total = db
@@ -1329,6 +1397,62 @@ function getTargetUsersFromDb(targetLocations = []) {
1329
1397
  };
1330
1398
  }
1331
1399
 
1400
+ function getTargetUsersByCountryFromDb(targetLocations = []) {
1401
+ if (!db) return null;
1402
+ if (!targetLocations.length) {
1403
+ return { countries: [] };
1404
+ }
1405
+
1406
+ const placeholders = targetLocations.map(() => "?").join(", ");
1407
+ const rows = db
1408
+ .prepare(
1409
+ `
1410
+ SELECT
1411
+ unique_id,
1412
+ nickname,
1413
+ follower_count,
1414
+ video_count,
1415
+ tt_seller,
1416
+ verified,
1417
+ location_created,
1418
+ latest_video_time,
1419
+ refresh_time,
1420
+ status,
1421
+ sources
1422
+ FROM jobs
1423
+ WHERE tt_seller = 1
1424
+ AND verified = 0
1425
+ AND location_created IN (${placeholders})
1426
+ ORDER BY location_created ASC, COALESCE(latest_video_time, 0) DESC
1427
+ `,
1428
+ )
1429
+ .all(...targetLocations)
1430
+ .map(mapJobRow);
1431
+
1432
+ const countryMap = new Map();
1433
+ for (const row of rows) {
1434
+ const country = row.locationCreated || "未知";
1435
+ if (!countryMap.has(country)) {
1436
+ countryMap.set(country, []);
1437
+ }
1438
+ countryMap.get(country).push(row);
1439
+ }
1440
+
1441
+ const countries = [];
1442
+ for (const [country, users] of countryMap) {
1443
+ countries.push({
1444
+ country,
1445
+ count: users.length,
1446
+ users,
1447
+ });
1448
+ }
1449
+
1450
+ return {
1451
+ total: rows.length,
1452
+ countries,
1453
+ };
1454
+ }
1455
+
1332
1456
  function snakeToCamel(key) {
1333
1457
  return key.replace(/_([a-z])/g, (_, ch) => ch.toUpperCase());
1334
1458
  }
@@ -2786,9 +2910,10 @@ export function createStore(filePath) {
2786
2910
  return { saved: true, pinned: user.pinned };
2787
2911
  }
2788
2912
 
2789
- function getNextRedoJob(userId) {
2913
+ function getNextRedoJob(userId, maxAgeSeconds = 43200) {
2790
2914
  if (db) {
2791
2915
  const now = Date.now();
2916
+ const threshold = now - maxAgeSeconds * 1000;
2792
2917
  const defaultTime = new Date("2016-01-01T00:00:00Z").getTime();
2793
2918
  const targetLocations = [
2794
2919
  "CZ",
@@ -2813,11 +2938,12 @@ export function createStore(filePath) {
2813
2938
  WHERE tt_seller = 1
2814
2939
  AND verified = 0
2815
2940
  AND location_created IN (${placeholders})
2941
+ AND COALESCE(refresh_time, ?) < ?
2816
2942
  ORDER BY COALESCE(refresh_time, ?) ASC
2817
2943
  LIMIT 1
2818
2944
  `,
2819
2945
  )
2820
- .get(...targetLocations, defaultTime);
2946
+ .get(...targetLocations, defaultTime, threshold, defaultTime);
2821
2947
  if (!row) return null;
2822
2948
  db.prepare(
2823
2949
  "UPDATE jobs SET refresh_time = ?, updated_at = ? WHERE unique_id = ?",
@@ -2830,6 +2956,7 @@ export function createStore(filePath) {
2830
2956
  }
2831
2957
 
2832
2958
  const now = Date.now();
2959
+ const threshold = now - maxAgeSeconds * 1000;
2833
2960
  const defaultTime = new Date("2016-01-01T00:00:00Z").getTime();
2834
2961
 
2835
2962
  // 筛选目标国家用户,按 refreshTime 升序取最远的(没有则默认 2016-01-01)
@@ -2855,13 +2982,19 @@ export function createStore(filePath) {
2855
2982
  );
2856
2983
  if (targetUsers.length === 0) return null;
2857
2984
 
2858
- targetUsers.sort((a, b) => {
2985
+ const recentEnough = targetUsers.filter((u) => {
2986
+ const rt = u.refreshTime || defaultTime;
2987
+ return rt < threshold;
2988
+ });
2989
+ if (recentEnough.length === 0) return null;
2990
+
2991
+ recentEnough.sort((a, b) => {
2859
2992
  const ta = a.refreshTime || defaultTime;
2860
2993
  const tb = b.refreshTime || defaultTime;
2861
2994
  return ta - tb;
2862
2995
  });
2863
2996
 
2864
- const next = targetUsers[0];
2997
+ const next = recentEnough[0];
2865
2998
  next.refreshTime = now;
2866
2999
  save();
2867
3000
  return {
@@ -2889,9 +3022,10 @@ export function createStore(filePath) {
2889
3022
  }
2890
3023
  }
2891
3024
  }
3025
+ const newUsers = processDiscoveredUsers(result);
2892
3026
  const ret = updateJobInfo(uniqueId, user, false);
2893
3027
  if (ret.error) return { saved: false, error: ret.error };
2894
- return { saved: true };
3028
+ return { saved: true, newUsers };
2895
3029
  }
2896
3030
 
2897
3031
  const user = getUser(uniqueId);
@@ -2908,8 +3042,8 @@ export function createStore(filePath) {
2908
3042
  }
2909
3043
  }
2910
3044
  }
2911
-
2912
- return { saved: true };
3045
+ const newUsers = processDiscoveredUsers(result);
3046
+ return { saved: true, newUsers };
2913
3047
  }
2914
3048
 
2915
3049
  function reportClientError(
@@ -3349,12 +3483,14 @@ export function createStore(filePath) {
3349
3483
  getRawByCountry: getRawByCountryFromDb,
3350
3484
  moveJobsToRawByCountry,
3351
3485
  restoreAttachStuckByCountry,
3486
+ resetPendingByCountry,
3352
3487
  restoreRawJobsByCountry,
3353
3488
  restoreRawJobById,
3354
3489
  restoreRawJobsByFilter,
3355
3490
  getUsersPage: getUsersPageFromDb,
3356
3491
  getRawJobsPage: getRawJobsPageFromDb,
3357
3492
  getTargetUsers: getTargetUsersFromDb,
3493
+ getTargetUsersByCountry: getTargetUsersByCountryFromDb,
3358
3494
  getStats,
3359
3495
  getStatusGroups,
3360
3496
  markGroupsDirty,