tt-help-cli-ycl 1.3.23 → 1.3.25

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.
Files changed (60) hide show
  1. package/README.md +17 -17
  2. package/cli.js +9 -9
  3. package/package.json +47 -47
  4. package/scripts/run-explore copy.bat +68 -68
  5. package/scripts/run-explore.bat +68 -68
  6. package/scripts/run-explore.ps1 +81 -81
  7. package/scripts/run-explore.sh +73 -73
  8. package/src/cli/attach.js +180 -180
  9. package/src/cli/auto.js +186 -186
  10. package/src/cli/config.js +152 -152
  11. package/src/cli/explore.js +296 -253
  12. package/src/cli/info.js +88 -88
  13. package/src/cli/open.js +102 -0
  14. package/src/cli/progress.js +111 -111
  15. package/src/cli/refresh.js +216 -216
  16. package/src/cli/scrape.js +47 -47
  17. package/src/cli/utils.js +18 -18
  18. package/src/cli/videos.js +41 -41
  19. package/src/cli/watch.js +31 -31
  20. package/src/lib/api-interceptor.js +191 -129
  21. package/src/lib/args.js +551 -517
  22. package/src/lib/browser/anti-detect.js +23 -23
  23. package/src/lib/browser/cdp.js +236 -193
  24. package/src/lib/browser/health-checker.js +54 -0
  25. package/src/lib/browser/launch.js +43 -43
  26. package/src/lib/browser/page.js +156 -156
  27. package/src/lib/constants.js +206 -199
  28. package/src/lib/delay.js +54 -54
  29. package/src/lib/explore-fetch.js +118 -118
  30. package/src/lib/fetcher.js +45 -45
  31. package/src/lib/filter.js +66 -66
  32. package/src/lib/io.js +54 -54
  33. package/src/lib/output.js +80 -80
  34. package/src/lib/page-error-detector.js +71 -71
  35. package/src/lib/parse-ssr.mjs +69 -69
  36. package/src/lib/parser.js +47 -47
  37. package/src/lib/retry.js +45 -45
  38. package/src/lib/scrape.js +89 -89
  39. package/src/lib/tiktok-scraper.mjs +194 -194
  40. package/src/lib/url.js +52 -52
  41. package/src/main.js +44 -42
  42. package/src/scraper/auto-core.js +203 -203
  43. package/src/scraper/core.js +211 -211
  44. package/src/scraper/explore-core.js +198 -189
  45. package/src/scraper/modules/captcha-handler.js +114 -114
  46. package/src/scraper/modules/comment-extractor.js +74 -74
  47. package/src/scraper/modules/follow-extractor.js +118 -118
  48. package/src/scraper/modules/guess-extractor.js +51 -51
  49. package/src/scraper/modules/page-helpers.js +48 -48
  50. package/src/scraper/refresh-core.js +179 -179
  51. package/src/videos/core.js +101 -97
  52. package/src/watch/data-store.js +828 -828
  53. package/src/watch/public/index.html +753 -753
  54. package/src/watch/server.js +705 -705
  55. package/scripts/test-captcha-lib.mjs +0 -68
  56. package/scripts/test-captcha.mjs +0 -81
  57. package/scripts/test-incognito-lib.mjs +0 -36
  58. package/scripts/test-login-state.mjs +0 -128
  59. package/scripts/test-safe-click.mjs +0 -45
  60. package/src/results/user-videos-bar.lar.lar.moeta.json +0 -37
@@ -1,253 +1,296 @@
1
- import { getOrCreatePage, isBrowserClosedError, relaunchBrowser } from '../lib/browser/page.js';
2
- import { delay, getDelayConfig, setDelayConfig } from '../scraper/modules/page-helpers.js';
3
- import { userId as configuredUserId, saveUserId } from '../lib/constants.js';
4
- import { getMacOrUuid } from '../lib/mac-or-uuid.js';
5
- import { ensureBrowserReady as ensureBrowserReadyCDP } from '../lib/browser/cdp.js';
6
- import { showResourceUsage } from '../utils/index.js';
7
- import path from 'path';
8
- import os from 'os';
9
-
10
- const MAX_RETRY_WAIT = 5 * 60 * 1000;
11
-
12
- async function withRetry(label, fn) {
13
- let backoff = 1000;
14
- while (true) {
15
- try {
16
- return await fn();
17
- } catch (err) {
18
- console.error(`[连接] ${label} 失败: ${err.message},${backoff / 1000}秒后重试...`);
19
- await new Promise(r => setTimeout(r, backoff));
20
- if (backoff < MAX_RETRY_WAIT) backoff *= 2;
21
- }
22
- }
23
- }
24
-
25
- async function apiPost(url, body) {
26
- return withRetry(`POST ${url}`, async () => {
27
- const res = await fetch(url, {
28
- method: 'POST',
29
- headers: { 'Content-Type': 'application/json' },
30
- body: JSON.stringify(body),
31
- });
32
- return res.json();
33
- });
34
- }
35
-
36
- async function apiGet(url) {
37
- return withRetry(`GET ${url}`, async () => {
38
- const res = await fetch(url);
39
- return res.json();
40
- });
41
- }
42
-
43
- export async function handleExplore(options) {
44
- const {
45
- exploreUsernames, explorePreset, exploreMaxComments, exploreMaxGuess,
46
- exploreEnableFollow, exploreMaxFollowing, exploreMaxFollowers,
47
- exploreLocation, exploreMaxUsers, serverUrl,
48
- explorePort, exploreProfile, exploreUserId,
49
- exploreMaxVideos,
50
- } = options;
51
-
52
- let userId = exploreUserId || configuredUserId;
53
- if (!userId) {
54
- userId = await getMacOrUuid();
55
- saveUserId(userId);
56
- console.error(`[初始化] 未检测到本地用户编号,已生成并使用: ${userId}`);
57
- }
58
-
59
- setDelayConfig(explorePreset);
60
-
61
- await apiGet(`${serverUrl}/api/stats`);
62
-
63
- if (exploreUsernames && exploreUsernames.length > 0) {
64
- const { added, skipped } = await apiPost(`${serverUrl}/api/users`, { usernames: exploreUsernames });
65
- console.error(`种子用户: ${added} 个新增, ${skipped} 个已存在`);
66
- }
67
-
68
- console.error(`\n国家筛选: ${exploreLocation}`);
69
- console.error(`视频采集: ${exploreMaxVideos || 1}`);
70
- console.error(`关注/粉丝: ${exploreEnableFollow ? '启用' : '禁用'}`);
71
- console.error(`服务器: ${serverUrl}(断开会自动重连)`);
72
- if (exploreMaxUsers > 0) console.error(`上限: ${exploreMaxUsers} 个用户`);
73
- console.error(`CDP 端口: ${explorePort || 9222}, 用户编号: ${userId}`);
74
- if (exploreProfile) console.error(`浏览器配置: ${exploreProfile}`);
75
-
76
- const cdpOptions = {};
77
- if (explorePort) cdpOptions.port = explorePort;
78
- if (exploreProfile) {
79
- cdpOptions.userDataDir = path.join(os.homedir(), 'Library', 'Application Support', `Microsoft Edge For Testing_${exploreProfile}`);
80
- }
81
-
82
- let browser = await ensureBrowserReadyCDP(cdpOptions);
83
- const { processExplore } = await import('../scraper/explore-core.js');
84
-
85
- const page = await getOrCreatePage(browser);
86
-
87
- // 全局拦截图片资源,减少内存占用和加载时间
88
- await page.route("**/*", (route) => {
89
- const resourceType = route.request().resourceType();
90
- if (resourceType === 'image' || resourceType === 'stylesheet') {
91
- route.abort();
92
- } else {
93
- route.continue();
94
- }
95
- });
96
-
97
- let processedCount = 0;
98
- let errorCount = 0;
99
- let consecutiveNetworkErrors = 0;
100
-
101
- while (true) {
102
- const job = await apiGet(`${serverUrl}/api/job?userId=${encodeURIComponent(userId)}`);
103
- if (!job.hasJob) break;
104
-
105
- const username = job.user.uniqueId;
106
- processedCount++;
107
-
108
- if (processedCount % 10 === 0) {
109
- showResourceUsage();
110
- }
111
-
112
- if (consecutiveNetworkErrors > 0) {
113
- const waitTime = consecutiveNetworkErrors <= 2
114
- ? 0
115
- : consecutiveNetworkErrors <= 5
116
- ? 30000
117
- : 300000;
118
- if (waitTime > 0) {
119
- console.error(` [网络] 连续 ${consecutiveNetworkErrors} 次网络异常,等待 ${waitTime / 1000}s 后重试...`);
120
- await new Promise(r => setTimeout(r, waitTime));
121
- }
122
- }
123
-
124
- console.error(`\n[${processedCount}] 探索 @${username}...`);
125
-
126
- const { switchMax } = getDelayConfig();
127
- await delay(switchMax, switchMax * 3);
128
-
129
- let result = await processExplore(page, username, {
130
- maxVideos: exploreMaxVideos,
131
- enableFollow: exploreEnableFollow,
132
- maxFollowing: exploreMaxFollowing,
133
- maxFollowers: exploreMaxFollowers,
134
- location: exploreLocation,
135
- browser,
136
- }, console.error);
137
-
138
- // 浏览器关闭检测:processExplore 内部 catch 了异常,需要从 result.error 判断
139
- if (result.error && isBrowserClosedError(new Error(result.error))) {
140
- const newBrowser = await relaunchBrowser(cdpOptions, explorePort || 9222);
141
- browser = newBrowser;
142
- const newPage = await getOrCreatePage(browser);
143
- Object.assign(page, newPage);
144
- // 重试当前用户
145
- result = await processExplore(page, username, {
146
- maxVideos: exploreMaxVideos,
147
- enableFollow: exploreEnableFollow,
148
- maxFollowing: exploreMaxFollowing,
149
- maxFollowers: exploreMaxFollowers,
150
- location: exploreLocation,
151
- browser,
152
- }, console.error);
153
- }
154
-
155
- if (result.restricted) {
156
- consecutiveNetworkErrors = 0;
157
- await apiPost(`${serverUrl}/api/job/${username}`, { restricted: true, userInfo: result.userInfo || {} });
158
- if (exploreMaxUsers > 0 && processedCount >= exploreMaxUsers) {
159
- console.error(`\n已达上限 ${exploreMaxUsers} 个用户,停止处理`);
160
- break;
161
- }
162
- continue;
163
- }
164
-
165
- if (result.error) {
166
- consecutiveNetworkErrors++;
167
- errorCount++;
168
- await apiPost(`${serverUrl}/api/job/${username}`, { error: result.error });
169
- const errorType = result.error.startsWith('被封:') ? '被封' : (consecutiveNetworkErrors > 1 ? 'network' : 'other');
170
- await withRetry('report error', () =>
171
- apiPost(`${serverUrl}/api/error-report`, {
172
- userId,
173
- username,
174
- errorType,
175
- errorMessage: result.error,
176
- stage: 'process',
177
- errorStack: result.errorStack || '',
178
- })
179
- ).catch(() => {});
180
- if (errorType === '被封') {
181
- console.error(`\n账号被封,停止处理`);
182
- break;
183
- }
184
- if (exploreMaxUsers > 0 && processedCount >= exploreMaxUsers) {
185
- console.error(`\n已达上限 ${exploreMaxUsers} 个用户,停止处理`);
186
- break;
187
- }
188
- continue;
189
- }
190
-
191
- if (result.captchaDetected) {
192
- await withRetry('report captcha', () =>
193
- apiPost(`${serverUrl}/api/error-report`, {
194
- userId,
195
- username,
196
- errorType: 'captcha',
197
- errorMessage: result.captchaMessage || '页面出现验证码',
198
- stage: result.captchaStage || 'video-page',
199
- errorStack: '',
200
- })
201
- ).catch(() => {});
202
- }
203
-
204
- consecutiveNetworkErrors = 0;
205
-
206
- const guessedLocation = result.locationCreated || null;
207
-
208
- const payload = {
209
- userInfo: result.userInfo || {},
210
- discoveredFollowing: (result.discoveredFollowing || []).map(f => ({
211
- handle: Array.isArray(f) ? f[0] : f,
212
- displayName: Array.isArray(f) ? f[1] : null,
213
- guessedLocation,
214
- })),
215
- discoveredFollowers: (result.discoveredFollowers || []).map(f => ({
216
- handle: Array.isArray(f) ? f[0] : f,
217
- displayName: Array.isArray(f) ? f[1] : null,
218
- guessedLocation,
219
- })),
220
- processed: result.processed,
221
- hasFollowData: result.hasFollowData,
222
- keepFollow: result.keepFollow,
223
- locationCreated: result.locationCreated,
224
- noVideo: result.noVideo,
225
- collectedVideos: result.collectedVideos,
226
- };
227
- await apiPost(`${serverUrl}/api/job/${username}`, payload);
228
-
229
- // 视频登记
230
- if (result.videoList && result.videoList.length > 0) {
231
- const { registered, skipped } = await apiPost(`${serverUrl}/api/videos`, {
232
- sourceUser: username,
233
- videoList: result.videoList,
234
- locationCreated: result.locationCreated,
235
- ttSeller: result.userInfo?.ttSeller || false,
236
- });
237
- console.error(` 视频登记: ${registered} 条新增, ${skipped} 条已存在`);
238
- }
239
-
240
- console.error(' 已提交');
241
-
242
- if (exploreMaxUsers > 0 && processedCount >= exploreMaxUsers) {
243
- console.error(`\n已达上限 ${exploreMaxUsers} 个用户,停止处理`);
244
- break;
245
- }
246
- }
247
-
248
- const stats = await apiGet(`${serverUrl}/api/stats`);
249
- console.error(`\n完成: ${processedCount} 个用户处理, ${errorCount} 个出错`);
250
- console.error(` 总用户: ${stats.totalUsers}, 已完成: ${stats.processedUsers}, 待处理: ${stats.pendingUsers}, 错误: ${stats.errorUsers}`);
251
-
252
- await browser.close().catch(() => {});
253
- }
1
+ import { getOrCreatePage, isBrowserClosedError, relaunchBrowser } from '../lib/browser/page.js';
2
+ import { delay, getDelayConfig, setDelayConfig } from '../scraper/modules/page-helpers.js';
3
+ import { userId as configuredUserId, saveUserId } from '../lib/constants.js';
4
+ import { getMacOrUuid } from '../lib/mac-or-uuid.js';
5
+ import { ensureBrowserReady as ensureBrowserReadyCDP, switchAccount } from '../lib/browser/cdp.js';
6
+ import { showResourceUsage } from '../utils/index.js';
7
+ import { HealthChecker } from '../lib/browser/health-checker.js';
8
+ import path from 'path';
9
+ import os from 'os';
10
+
11
+ const MAX_RETRY_WAIT = 5 * 60 * 1000;
12
+
13
+ async function withRetry(label, fn) {
14
+ let backoff = 1000;
15
+ while (true) {
16
+ try {
17
+ return await fn();
18
+ } catch (err) {
19
+ console.error(`[连接] ${label} 失败: ${err.message},${backoff / 1000}秒后重试...`);
20
+ await new Promise(r => setTimeout(r, backoff));
21
+ if (backoff < MAX_RETRY_WAIT) backoff *= 2;
22
+ }
23
+ }
24
+ }
25
+
26
+ async function apiPost(url, body) {
27
+ return withRetry(`POST ${url}`, async () => {
28
+ const res = await fetch(url, {
29
+ method: 'POST',
30
+ headers: { 'Content-Type': 'application/json' },
31
+ body: JSON.stringify(body),
32
+ });
33
+ return res.json();
34
+ });
35
+ }
36
+
37
+ async function apiGet(url) {
38
+ return withRetry(`GET ${url}`, async () => {
39
+ const res = await fetch(url);
40
+ return res.json();
41
+ });
42
+ }
43
+
44
+ export async function handleExplore(options) {
45
+ const {
46
+ exploreUsernames, explorePreset, exploreMaxComments, exploreMaxGuess,
47
+ exploreEnableFollow, exploreMaxFollowing, exploreMaxFollowers,
48
+ exploreLocation, exploreMaxUsers, serverUrl,
49
+ explorePort, exploreProfile, exploreUserId,
50
+ exploreMaxVideos,
51
+ } = options;
52
+
53
+ let userId = exploreUserId || configuredUserId;
54
+ if (!userId) {
55
+ userId = await getMacOrUuid();
56
+ saveUserId(userId);
57
+ console.error(`[初始化] 未检测到本地用户编号,已生成并使用: ${userId}`);
58
+ }
59
+
60
+ setDelayConfig(explorePreset);
61
+
62
+ await apiGet(`${serverUrl}/api/stats`);
63
+
64
+ if (exploreUsernames && exploreUsernames.length > 0) {
65
+ const { added, skipped } = await apiPost(`${serverUrl}/api/users`, { usernames: exploreUsernames });
66
+ console.error(`种子用户: ${added} 个新增, ${skipped} 个已存在`);
67
+ }
68
+
69
+ console.error(`\n国家筛选: ${exploreLocation}`);
70
+ console.error(`视频采集: ${exploreMaxVideos || 1}`);
71
+ console.error(`关注/粉丝: ${exploreEnableFollow ? '启用' : '禁用'}`);
72
+ console.error(`服务器: ${serverUrl}(断开会自动重连)`);
73
+ if (exploreMaxUsers > 0) console.error(`上限: ${exploreMaxUsers} 个用户`);
74
+
75
+ const healthChecker = new HealthChecker();
76
+ let currentAccount = healthChecker.getCurrentAccount();
77
+
78
+ const cdpOptions = {};
79
+ if (explorePort) cdpOptions.port = explorePort;
80
+ if (exploreProfile) {
81
+ cdpOptions.userDataDir = path.join(os.homedir(), 'Library', 'Application Support', `Microsoft Edge For Testing_${exploreProfile}`);
82
+ } else {
83
+ cdpOptions.port = currentAccount.port;
84
+ cdpOptions.userDataDir = currentAccount.userDataDir;
85
+ }
86
+
87
+ console.error(`CDP 端口: ${cdpOptions.port || 9222}, 用户编号: ${userId}`);
88
+ if (exploreProfile || cdpOptions.userDataDir) console.error(`浏览器配置: ${exploreProfile || path.basename(cdpOptions.userDataDir)}`);
89
+
90
+ let browser = await ensureBrowserReadyCDP(cdpOptions);
91
+ const { processExplore } = await import('../scraper/explore-core.js');
92
+
93
+ const page = await getOrCreatePage(browser);
94
+
95
+ // 全局拦截图片资源,减少内存占用和加载时间
96
+ await page.route("**/*", (route) => {
97
+ const resourceType = route.request().resourceType();
98
+ if (resourceType === 'image' || resourceType === 'stylesheet') {
99
+ route.abort();
100
+ } else {
101
+ route.continue();
102
+ }
103
+ });
104
+
105
+ // 账户切换后,重新初始化 page
106
+ async function setupNewPage(newBrowser) {
107
+ const newPage = await getOrCreatePage(newBrowser);
108
+ await newPage.route("**/*", (route) => {
109
+ const resourceType = route.request().resourceType();
110
+ if (resourceType === 'image' || resourceType === 'stylesheet') {
111
+ route.abort();
112
+ } else {
113
+ route.continue();
114
+ }
115
+ });
116
+ return newPage;
117
+ }
118
+
119
+ async function handleAccountSwitch(reason) {
120
+ console.error(`\n[健康检查] ${reason},切换到下一个账户...`);
121
+ const oldAccount = currentAccount;
122
+ const nextAccount = healthChecker.getNextAccount();
123
+ currentAccount = nextAccount;
124
+ const newBrowser = await switchAccount(
125
+ { port: oldAccount.port, userDataDir: oldAccount.userDataDir },
126
+ { port: nextAccount.port, userDataDir: nextAccount.userDataDir }
127
+ );
128
+ browser = newBrowser;
129
+ const newPage = await setupNewPage(browser);
130
+ Object.assign(page, newPage);
131
+ Object.assign(cdpOptions, { port: nextAccount.port, userDataDir: nextAccount.userDataDir });
132
+ console.error(`[健康检查] 已切换到端口 ${nextAccount.port}`);
133
+ }
134
+
135
+ let processedCount = 0;
136
+ let errorCount = 0;
137
+ let consecutiveNetworkErrors = 0;
138
+
139
+ while (true) {
140
+ const job = await apiGet(`${serverUrl}/api/job?userId=${encodeURIComponent(userId)}`);
141
+ if (!job.hasJob) break;
142
+
143
+ const username = job.user.uniqueId;
144
+ processedCount++;
145
+
146
+ if (processedCount % 10 === 0) {
147
+ showResourceUsage();
148
+ }
149
+
150
+ const checkResult = healthChecker.check();
151
+ if (checkResult.shouldSwitch) {
152
+ await handleAccountSwitch(checkResult.reason);
153
+ }
154
+
155
+ if (consecutiveNetworkErrors > 0) {
156
+ const waitTime = consecutiveNetworkErrors <= 2
157
+ ? 0
158
+ : consecutiveNetworkErrors <= 5
159
+ ? 30000
160
+ : 300000;
161
+ if (waitTime > 0) {
162
+ console.error(` [网络] 连续 ${consecutiveNetworkErrors} 次网络异常,等待 ${waitTime / 1000}s 后重试...`);
163
+ await new Promise(r => setTimeout(r, waitTime));
164
+ }
165
+ }
166
+
167
+ console.error(`\n[${processedCount}] 探索 @${username}...`);
168
+
169
+ const { switchMax } = getDelayConfig();
170
+ await delay(switchMax, switchMax * 3);
171
+
172
+ let result = await processExplore(page, username, {
173
+ maxVideos: exploreMaxVideos,
174
+ enableFollow: exploreEnableFollow,
175
+ maxFollowing: exploreMaxFollowing,
176
+ maxFollowers: exploreMaxFollowers,
177
+ location: exploreLocation,
178
+ browser,
179
+ }, console.error);
180
+
181
+ // 浏览器关闭检测:processExplore 内部 catch 了异常,需要从 result.error 判断
182
+ if (result.error && isBrowserClosedError(new Error(result.error))) {
183
+ const newBrowser = await relaunchBrowser(cdpOptions, cdpOptions.port || 9222);
184
+ browser = newBrowser;
185
+ const newPage = await setupNewPage(browser);
186
+ Object.assign(page, newPage);
187
+ // 重试当前用户
188
+ result = await processExplore(page, username, {
189
+ maxVideos: exploreMaxVideos,
190
+ enableFollow: exploreEnableFollow,
191
+ maxFollowing: exploreMaxFollowing,
192
+ maxFollowers: exploreMaxFollowers,
193
+ location: exploreLocation,
194
+ browser,
195
+ }, console.error);
196
+ }
197
+
198
+ if (result.restricted) {
199
+ consecutiveNetworkErrors = 0;
200
+ await apiPost(`${serverUrl}/api/job/${username}`, { restricted: true, userInfo: result.userInfo || {} });
201
+ if (exploreMaxUsers > 0 && processedCount >= exploreMaxUsers) {
202
+ console.error(`\n已达上限 ${exploreMaxUsers} 个用户,停止处理`);
203
+ break;
204
+ }
205
+ continue;
206
+ }
207
+
208
+ if (result.error) {
209
+ consecutiveNetworkErrors++;
210
+ errorCount++;
211
+ await apiPost(`${serverUrl}/api/job/${username}`, { error: result.error });
212
+ const errorType = result.error.startsWith('被封:') ? '被封' : (consecutiveNetworkErrors > 1 ? 'network' : 'other');
213
+ await withRetry('report error', () =>
214
+ apiPost(`${serverUrl}/api/error-report`, {
215
+ userId,
216
+ username,
217
+ errorType,
218
+ errorMessage: result.error,
219
+ stage: 'process',
220
+ errorStack: result.errorStack || '',
221
+ })
222
+ ).catch(() => {});
223
+ if (errorType === '被封') {
224
+ await handleAccountSwitch('账号被封');
225
+ continue;
226
+ }
227
+ if (exploreMaxUsers > 0 && processedCount >= exploreMaxUsers) {
228
+ console.error(`\n已达上限 ${exploreMaxUsers} 个用户,停止处理`);
229
+ break;
230
+ }
231
+ continue;
232
+ }
233
+
234
+ if (result.captchaDetected) {
235
+ await withRetry('report captcha', () =>
236
+ apiPost(`${serverUrl}/api/error-report`, {
237
+ userId,
238
+ username,
239
+ errorType: 'captcha',
240
+ errorMessage: result.captchaMessage || '页面出现验证码',
241
+ stage: result.captchaStage || 'video-page',
242
+ errorStack: '',
243
+ })
244
+ ).catch(() => {});
245
+ }
246
+
247
+ consecutiveNetworkErrors = 0;
248
+
249
+ const guessedLocation = result.locationCreated || null;
250
+
251
+ const payload = {
252
+ userInfo: result.userInfo || {},
253
+ discoveredFollowing: (result.discoveredFollowing || []).map(f => ({
254
+ handle: Array.isArray(f) ? f[0] : f,
255
+ displayName: Array.isArray(f) ? f[1] : null,
256
+ guessedLocation,
257
+ })),
258
+ discoveredFollowers: (result.discoveredFollowers || []).map(f => ({
259
+ handle: Array.isArray(f) ? f[0] : f,
260
+ displayName: Array.isArray(f) ? f[1] : null,
261
+ guessedLocation,
262
+ })),
263
+ processed: result.processed,
264
+ hasFollowData: result.hasFollowData,
265
+ keepFollow: result.keepFollow,
266
+ locationCreated: result.locationCreated,
267
+ noVideo: result.noVideo,
268
+ collectedVideos: result.collectedVideos,
269
+ };
270
+ await apiPost(`${serverUrl}/api/job/${username}`, payload);
271
+
272
+ // 视频登记
273
+ if (result.videoList && result.videoList.length > 0) {
274
+ const { registered, skipped } = await apiPost(`${serverUrl}/api/videos`, {
275
+ sourceUser: username,
276
+ videoList: result.videoList,
277
+ locationCreated: result.locationCreated,
278
+ ttSeller: result.userInfo?.ttSeller || false,
279
+ });
280
+ console.error(` 视频登记: ${registered} 条新增, ${skipped} 条已存在`);
281
+ }
282
+
283
+ console.error(' 已提交');
284
+
285
+ if (exploreMaxUsers > 0 && processedCount >= exploreMaxUsers) {
286
+ console.error(`\n已达上限 ${exploreMaxUsers} 个用户,停止处理`);
287
+ break;
288
+ }
289
+ }
290
+
291
+ const stats = await apiGet(`${serverUrl}/api/stats`);
292
+ console.error(`\n完成: ${processedCount} 个用户处理, ${errorCount} 个出错`);
293
+ console.error(` 总用户: ${stats.totalUsers}, 已完成: ${stats.processedUsers}, 待处理: ${stats.pendingUsers}, 错误: ${stats.errorUsers}`);
294
+
295
+ await browser.close().catch(() => {});
296
+ }