tt-help-cli-ycl 1.3.11 → 1.3.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.
Files changed (61) hide show
  1. package/README.md +17 -17
  2. package/cli.js +9 -9
  3. package/package.json +45 -46
  4. package/{bat → scripts}/run-explore.bat +68 -68
  5. package/{bat → scripts}/run-explore.ps1 +81 -81
  6. package/{bat → scripts}/run-explore.sh +73 -73
  7. package/scripts/test-captcha-lib.mjs +68 -0
  8. package/scripts/test-captcha.mjs +81 -0
  9. package/scripts/test-incognito-lib.mjs +36 -0
  10. package/scripts/test-login-state.mjs +128 -0
  11. package/scripts/test-safe-click.mjs +45 -0
  12. package/src/cli/auto.js +186 -157
  13. package/src/cli/config.js +116 -0
  14. package/src/cli/explore-default.js +83 -0
  15. package/src/cli/explore.js +227 -181
  16. package/src/cli/progress.js +111 -111
  17. package/src/cli/refresh.js +216 -0
  18. package/src/cli/scrape.js +47 -47
  19. package/src/cli/utils.js +18 -18
  20. package/src/cli/videos.js +41 -41
  21. package/src/cli/watch.js +31 -31
  22. package/src/lib/args.js +456 -391
  23. package/src/lib/browser/anti-detect.js +23 -23
  24. package/src/lib/browser/cdp.js +194 -142
  25. package/src/lib/browser/launch.js +43 -43
  26. package/src/lib/browser/page.js +146 -87
  27. package/src/lib/constants.js +119 -119
  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/{scraper/modules/page-error-detector.mjs → lib/page-error-detector.js} +70 -70
  35. package/src/lib/parser.js +47 -47
  36. package/src/lib/retry.js +45 -45
  37. package/src/lib/scrape.js +40 -40
  38. package/src/{scraper/modules/scroll-collector.mjs → lib/scroll-collector.js} +231 -189
  39. package/src/lib/url.js +52 -52
  40. package/src/main.js +48 -0
  41. package/src/results/user-videos-bar.lar.lar.moeta.json +37 -0
  42. package/src/scraper/{auto-core.mjs → auto-core.js} +203 -194
  43. package/src/scraper/{core.mjs → core.js} +211 -190
  44. package/src/scraper/{explore-core.mjs → explore-core.js} +180 -171
  45. package/src/scraper/modules/{captcha-handler.mjs → captcha-handler.js} +114 -114
  46. package/src/scraper/modules/{comment-extractor.mjs → comment-extractor.js} +74 -69
  47. package/src/scraper/modules/{follow-extractor.mjs → follow-extractor.js} +121 -121
  48. package/src/scraper/modules/{guess-extractor.mjs → guess-extractor.js} +51 -51
  49. package/src/scraper/modules/page-error-detector.js +1 -0
  50. package/src/scraper/modules/{page-helpers.mjs → page-helpers.js} +48 -48
  51. package/src/scraper/modules/scroll-collector.js +8 -0
  52. package/src/scraper/refresh-core.js +179 -0
  53. package/src/videos/{core.mjs → core.js} +126 -126
  54. package/src/watch/data-store.js +431 -0
  55. package/src/watch/public/index.html +721 -690
  56. package/src/watch/{server.mjs → server.js} +484 -349
  57. package/src/main.mjs +0 -234
  58. package/src/test-auto-follow.cjs +0 -109
  59. package/src/test-extractors.cjs +0 -75
  60. package/src/test-follow.cjs +0 -41
  61. package/src/watch/data-store.mjs +0 -274
@@ -0,0 +1,81 @@
1
+ import { ensureBrowserReady } from '../src/lib/browser/cdp.js';
2
+
3
+ const url = 'https://www.tiktok.com/@mariaelenasanchez607/video/7630110959650000150';
4
+
5
+ async function main() {
6
+ const browser = await ensureBrowserReady();
7
+ const defaultContext = browser.contexts()[0];
8
+ const pages = defaultContext.pages();
9
+ const page = pages[0] || await defaultContext.newPage();
10
+
11
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
12
+ await page.waitForTimeout(5000);
13
+
14
+ // 用 force: true 点击评论按钮
15
+ const clicked = await page.evaluate(() => {
16
+ const btn = document.querySelector('[data-e2e="comments"]');
17
+ if (btn && btn.getBoundingClientRect().width > 0) {
18
+ btn.click();
19
+ return { success: true, rect: btn.getBoundingClientRect() };
20
+ }
21
+ return { success: false };
22
+ });
23
+
24
+ console.error('点击结果:', JSON.stringify(clicked));
25
+
26
+ // 等待可能的验证码
27
+ await page.waitForTimeout(5000);
28
+
29
+ // 截图
30
+ await page.screenshot({ path: '/tmp/tiktok-comment-clicked.png' });
31
+ console.error('截图: /tmp/tiktok-comment-clicked.png');
32
+
33
+ // 全面检测验证码
34
+ const captcha = await page.evaluate(() => {
35
+ const result = {};
36
+
37
+ // 大尺寸 Verify 元素
38
+ const verifyEls = Array.from(document.querySelectorAll('[class*="Verify"], [class*="verify"]'));
39
+ result.verifyElements = verifyEls.filter(el => {
40
+ const r = el.getBoundingClientRect();
41
+ return r.width > 100 && r.height > 100 && el.offsetParent !== null;
42
+ }).map(el => ({
43
+ class: el.className.substring(0, 200),
44
+ text: el.textContent?.substring(0, 300),
45
+ rect: { w: Math.round(el.getBoundingClientRect().width), h: Math.round(el.getBoundingClientRect().height), x: Math.round(el.getBoundingClientRect().x), y: Math.round(el.getBoundingClientRect().y) }
46
+ }));
47
+
48
+ // 全屏遮罩
49
+ result.fullScreenOverlays = Array.from(document.querySelectorAll('div')).filter(d => {
50
+ const r = d.getBoundingClientRect();
51
+ const style = window.getComputedStyle(d);
52
+ return r.width > 500 && r.height > 500 && parseInt(style.zIndex) > 900 && d.offsetParent !== null;
53
+ }).map(d => ({
54
+ class: d.className.substring(0, 100),
55
+ zIndex: window.getComputedStyle(d).zIndex,
56
+ rect: { w: Math.round(d.getBoundingClientRect().width), h: Math.round(d.getBoundingClientRect().height) }
57
+ }));
58
+
59
+ result.iframes = Array.from(document.querySelectorAll('iframe')).map(f => ({
60
+ src: (f.src || f.getAttribute('src') || '').substring(0, 300)
61
+ }));
62
+
63
+ return result;
64
+ });
65
+
66
+ console.error('\n=== 验证码检测 ===');
67
+ console.error(JSON.stringify(captcha, null, 2));
68
+
69
+ if (captcha.verifyElements.length > 0 || captcha.fullScreenOverlays.length > 0 || captcha.iframes.length > 0) {
70
+ console.error('\n⚠️ 检测到验证码或遮罩层!');
71
+ } else {
72
+ console.error('\n✅ 未检测到验证码');
73
+ }
74
+
75
+ await browser.close();
76
+ }
77
+
78
+ main().catch(err => {
79
+ console.error('错误:', err);
80
+ process.exit(1);
81
+ });
@@ -0,0 +1,36 @@
1
+ import { chromium } from 'playwright';
2
+ import { detectCaptcha, closeCaptcha, handleCaptcha, getIncognitoPage } from '../src/scraper/modules/captcha-handler.mjs';
3
+
4
+ async function main() {
5
+ const browser = await chromium.connectOverCDP('http://127.0.0.1:9222');
6
+ const url = 'https://www.tiktok.com/@mariaelenasanchez607/video/7630110959650000150';
7
+
8
+ // 测试1: 无痕模式打开 + 点击评论
9
+ console.error('=== 测试: 无痕模式 ===');
10
+ const { page, context } = await getIncognitoPage(browser, url);
11
+ console.error('URL:', page.url());
12
+
13
+ await page.evaluate(() => {
14
+ const all = document.querySelectorAll('button');
15
+ for (const el of all) {
16
+ if (/^评论$/.test(el.textContent?.trim()) && el.offsetParent !== null && el.getBoundingClientRect().width > 0) {
17
+ el.click();
18
+ break;
19
+ }
20
+ }
21
+ });
22
+ await new Promise(r => setTimeout(r, 3000));
23
+
24
+ const captcha = await detectCaptcha(page);
25
+ console.error('验证码:', captcha);
26
+
27
+ await page.screenshot({ path: '/tmp/incognito-lib-test.png' });
28
+ console.error('截图: /tmp/incognito-lib-test.png');
29
+
30
+ await context.close();
31
+ }
32
+
33
+ main().catch(err => {
34
+ console.error('错误:', err);
35
+ process.exit(1);
36
+ });
@@ -0,0 +1,128 @@
1
+ import { ensureBrowserReady } from '../src/lib/browser/cdp.js';
2
+
3
+ async function main() {
4
+ const browser = await ensureBrowserReady();
5
+ const defaultContext = browser.contexts()[0];
6
+ const page = defaultContext.pages()[0] || await defaultContext.newPage();
7
+
8
+ // 确保在 tiktok 页面
9
+ if (!page.url().includes('tiktok.com')) {
10
+ console.error('当前不在 TikTok 页面:', page.url());
11
+ await page.goto('https://www.tiktok.com', { waitUntil: 'domcontentloaded', timeout: 30000 });
12
+ await new Promise(r => setTimeout(r, 3000));
13
+ }
14
+
15
+ console.error('当前 URL:', page.url());
16
+
17
+ // 1. 检测现有 isLoggedIn 的逻辑
18
+ const currentUserMenu = await page.evaluate(() => {
19
+ const selectors = [
20
+ '[class*="UserMenu"]',
21
+ '[class*="user-menu"]',
22
+ '[class*="CurrentUserInfo"]',
23
+ ];
24
+ const results = {};
25
+ for (const sel of selectors) {
26
+ const el = document.querySelector(sel);
27
+ results[sel] = !!el;
28
+ if (el) {
29
+ results[sel + '_class'] = el.className;
30
+ }
31
+ }
32
+ return results;
33
+ });
34
+
35
+ console.error('\n=== 当前选择器检测结果 ===');
36
+ console.error(JSON.stringify(currentUserMenu, null, 2));
37
+
38
+ const hasLoginButton = await page.evaluate(() => {
39
+ const buttons = Array.from(document.querySelectorAll('button, [role="button"]'));
40
+ const loginButtons = buttons.filter(el =>
41
+ /^(登录|Log in|Sign in)$/i.test(el.textContent.trim())
42
+ );
43
+ return {
44
+ total: buttons.length,
45
+ loginCount: loginButtons.length,
46
+ samples: loginButtons.slice(0, 5).map(el => ({
47
+ text: el.textContent.trim(),
48
+ class: el.className,
49
+ })),
50
+ };
51
+ });
52
+
53
+ console.error('\n=== 登录按钮检测 ===');
54
+ console.error(JSON.stringify(hasLoginButton, null, 2));
55
+
56
+ // 2. 用更宽泛的方式找用户相关元素
57
+ const broadSearch = await page.evaluate(() => {
58
+ const allClasses = [];
59
+
60
+ // 搜索可能的用户头像/菜单元素
61
+ const candidates = [];
62
+
63
+ // 顶部导航区域
64
+ const navArea = document.querySelector('[class*="nav"], [class*="Header"], [class*="header"]');
65
+ if (navArea) {
66
+ const avatars = navArea.querySelectorAll('[class*="avatar"], [class*="Avatar"], [class*="photo"], [class*="Photo"], [class*="image"], [class*="Image"]');
67
+ avatars.forEach(el => {
68
+ candidates.push({
69
+ tag: el.tagName,
70
+ class: el.className.substring(0, 200),
71
+ text: el.textContent?.substring(0, 50),
72
+ parent: el.parentElement?.className?.substring(0, 100),
73
+ });
74
+ });
75
+ }
76
+
77
+ // 搜索包含用户信息的链接
78
+ const profileLinks = Array.from(document.querySelectorAll('a[href*="/@"]'));
79
+ const profileSamples = profileLinks.slice(0, 10).map(el => ({
80
+ href: el.href,
81
+ class: el.className?.substring(0, 100),
82
+ text: el.textContent?.substring(0, 50),
83
+ }));
84
+
85
+ // 搜索所有包含 User 关键字的类名
86
+ const userElements = Array.from(document.querySelectorAll('*')).filter(el =>
87
+ el.className && typeof el.className === 'string' &&
88
+ /User|user|Profile|profile/.test(el.className) &&
89
+ el.tagName !== 'STYLE' && el.tagName !== 'SCRIPT'
90
+ ).slice(0, 30).map(el => ({
91
+ tag: el.tagName,
92
+ class: el.className.substring(0, 150),
93
+ text: el.textContent?.substring(0, 30),
94
+ }));
95
+
96
+ return {
97
+ navAreaFound: !!navArea,
98
+ avatarCandidates: candidates,
99
+ profileLinks: profileSamples,
100
+ userElements,
101
+ };
102
+ });
103
+
104
+ console.error('\n=== 宽泛搜索 ===');
105
+ console.error(JSON.stringify(broadSearch, null, 2));
106
+
107
+ // 3. 截图
108
+ await page.screenshot({ path: '/tmp/login-debug.png' });
109
+ console.error('\n截图已保存到 /tmp/login-debug.png');
110
+
111
+ // 4. 汇总判断
112
+ const isLoggedIn = currentUserMenu['[class*="UserMenu"]'] ||
113
+ currentUserMenu['[class*="user-menu"]'] ||
114
+ currentUserMenu['[class*="CurrentUserInfo"]'] ||
115
+ broadSearch.userElements.length > 0;
116
+
117
+ console.error('\n=== 结论 ===');
118
+ console.error('isLoggedIn 函数返回:', !!(currentUserMenu['[class*="UserMenu"]'] || currentUserMenu['[class*="user-menu"]'] || currentUserMenu['[class*="CurrentUserInfo"]']) && !hasLoginButton.loginCount);
119
+ console.error('宽泛搜索是否找到用户元素:', broadSearch.userElements.length > 0);
120
+ console.error('找到用户相关元素数量:', broadSearch.userElements.length);
121
+ console.error('头像候选数量:', broadSearch.avatarCandidates.length);
122
+ console.error('Profile 链接数量:', broadSearch.profileLinks.length);
123
+ }
124
+
125
+ main().catch(err => {
126
+ console.error('错误:', err);
127
+ process.exit(1);
128
+ });
@@ -0,0 +1,45 @@
1
+ import { chromium } from 'playwright';
2
+ import { safeClickComment, detectCaptcha } from '../src/scraper/modules/captcha-handler.mjs';
3
+ import { ensureBrowserReady } from '../src/lib/browser/cdp.js';
4
+
5
+ const URL = 'https://www.tiktok.com/@mariaelenasanchez607/video/7630110959650000150';
6
+
7
+ async function main() {
8
+ const browser = await ensureBrowserReady();
9
+ const defaultContext = browser.contexts()[0];
10
+ const page = defaultContext.pages()[0] || await defaultContext.newPage();
11
+
12
+ for (let i = 1; i <= 3; i++) {
13
+ console.error(`\n===== 第 ${i} 轮 =====`);
14
+ await page.goto(URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
15
+ await new Promise(r => setTimeout(r, 5000));
16
+
17
+ const result = await safeClickComment(page);
18
+ console.error('结果:', JSON.stringify(result));
19
+
20
+ const stillThere = await detectCaptcha(page);
21
+ console.error('验证码残留:', !!stillThere);
22
+
23
+ await page.screenshot({ path: `/tmp/safe-click-run-${i}.png` });
24
+
25
+ // 关闭评论面板
26
+ await page.evaluate(() => {
27
+ const rightPanel = document.querySelector('[class*="RightPanelContainer"]');
28
+ if (rightPanel) {
29
+ const tabContainer = rightPanel.querySelector('[class*="TabContainer"]');
30
+ if (tabContainer) {
31
+ const closeOverlay = tabContainer.querySelector('div:last-child');
32
+ if (closeOverlay) closeOverlay.click();
33
+ }
34
+ }
35
+ });
36
+ await new Promise(r => setTimeout(r, 2000));
37
+ }
38
+
39
+ console.error('\n完成');
40
+ }
41
+
42
+ main().catch(err => {
43
+ console.error('错误:', err);
44
+ process.exit(1);
45
+ });
package/src/cli/auto.js CHANGED
@@ -1,157 +1,186 @@
1
- import { getOrCreatePage } from '../lib/browser/page.js';
2
- import { userId as configuredUserId, saveUserId } from '../lib/constants.js';
3
- import { getMacOrUuid } from '../lib/mac-or-uuid.js';
4
-
5
- const MAX_RETRY_WAIT = 5 * 60 * 1000;
6
-
7
- async function withRetry(label, fn) {
8
- let backoff = 1000;
9
- while (true) {
10
- try {
11
- return await fn();
12
- } catch (err) {
13
- console.error(`[连接] ${label} 失败: ${err.message},${backoff / 1000}秒后重试...`);
14
- await new Promise(r => setTimeout(r, backoff));
15
- if (backoff < MAX_RETRY_WAIT) backoff *= 2;
16
- }
17
- }
18
- }
19
-
20
- async function apiPost(url, body) {
21
- return withRetry(`POST ${url}`, async () => {
22
- const res = await fetch(url, {
23
- method: 'POST',
24
- headers: { 'Content-Type': 'application/json' },
25
- body: JSON.stringify(body),
26
- });
27
- return res.json();
28
- });
29
- }
30
-
31
- async function apiGet(url) {
32
- return withRetry(`GET ${url}`, async () => {
33
- const res = await fetch(url);
34
- return res.json();
35
- });
36
- }
37
-
38
- export async function handleAuto(options) {
39
- const { autoUsernames, autoCollectMax, autoScrapeDepth, autoMaxComments, autoMaxGuess,
40
- autoPreset, autoSwitchDelay, autoCommentDelay, serverUrl,
41
- autoEnableFollow, autoMaxFollowing, autoMaxFollowers } = options;
42
-
43
- let userId = configuredUserId;
44
- if (!userId) {
45
- userId = await getMacOrUuid();
46
- saveUserId(userId);
47
- console.error(`[初始化] 未检测到本地用户编号,已生成并使用: ${userId}`);
48
- }
49
-
50
- const runOptions = {
51
- collectMax: autoCollectMax,
52
- scrapeDepth: autoScrapeDepth,
53
- maxComments: autoMaxComments,
54
- maxGuess: autoMaxGuess,
55
- preset: autoPreset,
56
- switchMax: autoSwitchDelay,
57
- commentMax: autoCommentDelay,
58
- enableFollow: autoEnableFollow,
59
- maxFollowing: autoMaxFollowing,
60
- maxFollowers: autoMaxFollowers,
61
- userId,
62
- };
63
-
64
- await apiGet(`${serverUrl}/api/stats`);
65
-
66
- if (autoUsernames.length > 0) {
67
- const { added, skipped } = await apiPost(`${serverUrl}/api/users`, { usernames: autoUsernames });
68
- console.error(`种子用户: ${added} 个新增, ${skipped} 个已存在`);
69
- }
70
-
71
- console.error(`服务器: ${serverUrl}(断开会自动重连)`);
72
-
73
- const { ensureBrowserReady, processUser } = await import('../scraper/auto-core.mjs');
74
- const browser = await ensureBrowserReady();
75
-
76
- const page = await getOrCreatePage(browser);
77
-
78
- let processedCount = 0;
79
- let errorCount = 0;
80
- let consecutiveNetworkErrors = 0;
81
-
82
- while (true) {
83
- const job = await apiGet(`${serverUrl}/api/job?userId=${encodeURIComponent(userId)}`);
84
- if (!job.hasJob) break;
85
-
86
- const username = job.user.uniqueId;
87
- processedCount++;
88
-
89
- if (consecutiveNetworkErrors > 0) {
90
- const waitTime = consecutiveNetworkErrors <= 2
91
- ? 0
92
- : consecutiveNetworkErrors <= 5
93
- ? 30000
94
- : 300000;
95
- if (waitTime > 0) {
96
- console.error(` [网络] 连续 ${consecutiveNetworkErrors} 次网络异常,等待 ${waitTime / 1000}s 后重试...`);
97
- await new Promise(r => setTimeout(r, waitTime));
98
- }
99
- }
100
-
101
- console.error(`\n[${processedCount}] 处理 @${username}...`);
102
-
103
- const result = await processUser(page, username, { ...runOptions, browser }, console.error);
104
-
105
- if (result.restricted) {
106
- consecutiveNetworkErrors = 0;
107
- await apiPost(`${serverUrl}/api/job/${username}`, result);
108
- continue;
109
- }
110
-
111
- if (result.error) {
112
- consecutiveNetworkErrors++;
113
- errorCount++;
114
- await apiPost(`${serverUrl}/api/job/${username}`, result);
115
- const errorType = consecutiveNetworkErrors > 1 ? 'network' : 'other';
116
- await withRetry('report error', () =>
117
- apiPost(`${serverUrl}/api/error-report`, {
118
- userId,
119
- username,
120
- errorType,
121
- errorMessage: result.error,
122
- })
123
- ).catch(() => {});
124
- continue;
125
- }
126
-
127
- if (result.captchaDetected) {
128
- await withRetry('report captcha', () =>
129
- apiPost(`${serverUrl}/api/error-report`, {
130
- userId,
131
- username,
132
- errorType: 'captcha',
133
- errorMessage: '页面出现验证码',
134
- })
135
- ).catch(() => {});
136
- }
137
-
138
- consecutiveNetworkErrors = 0;
139
-
140
- const payload = {
141
- userInfo: result.userInfo || {},
142
- discoveredVideoAuthors: result.discoveredVideoAuthors || [],
143
- discoveredCommentAuthors: result.discoveredCommentAuthors || [],
144
- discoveredGuessAuthors: result.discoveredGuessAuthors || [],
145
- discoveredFollowing: result.discoveredFollowing || [],
146
- discoveredFollowers: result.discoveredFollowers || [],
147
- };
148
- await apiPost(`${serverUrl}/api/job/${username}`, payload);
149
- console.error(' 已提交');
150
- }
151
-
152
- const stats = await apiGet(`${serverUrl}/api/stats`);
153
- console.error(`\n完成: ${processedCount} 个用户处理, ${errorCount} 个出错`);
154
- console.error(` 总用户: ${stats.totalUsers}, 已完成: ${stats.processedUsers}, 待处理: ${stats.pendingUsers}, 错误: ${stats.errorUsers}`);
155
-
156
- await browser.close().catch(() => {});
157
- }
1
+ import { getOrCreatePage, isBrowserClosedError, relaunchBrowser } from '../lib/browser/page.js';
2
+ import { userId as configuredUserId, saveUserId } from '../lib/constants.js';
3
+ import { getMacOrUuid } from '../lib/mac-or-uuid.js';
4
+ import { ensureBrowserReady as ensureBrowserReadyCDP } from '../lib/browser/cdp.js';
5
+
6
+ const MAX_RETRY_WAIT = 5 * 60 * 1000;
7
+
8
+ async function withRetry(label, fn) {
9
+ let backoff = 1000;
10
+ while (true) {
11
+ try {
12
+ return await fn();
13
+ } catch (err) {
14
+ console.error(`[连接] ${label} 失败: ${err.message},${backoff / 1000}秒后重试...`);
15
+ await new Promise(r => setTimeout(r, backoff));
16
+ if (backoff < MAX_RETRY_WAIT) backoff *= 2;
17
+ }
18
+ }
19
+ }
20
+
21
+ async function apiPost(url, body) {
22
+ return withRetry(`POST ${url}`, async () => {
23
+ const res = await fetch(url, {
24
+ method: 'POST',
25
+ headers: { 'Content-Type': 'application/json' },
26
+ body: JSON.stringify(body),
27
+ });
28
+ return res.json();
29
+ });
30
+ }
31
+
32
+ async function apiGet(url) {
33
+ return withRetry(`GET ${url}`, async () => {
34
+ const res = await fetch(url);
35
+ return res.json();
36
+ });
37
+ }
38
+
39
+ export async function handleAuto(options) {
40
+ const { autoUsernames, autoCollectMax, autoScrapeDepth, autoMaxComments, autoMaxGuess,
41
+ autoPreset, autoSwitchDelay, autoCommentDelay, serverUrl,
42
+ autoEnableFollow, autoMaxFollowing, autoMaxFollowers } = options;
43
+
44
+ let userId = configuredUserId;
45
+ if (!userId) {
46
+ userId = await getMacOrUuid();
47
+ saveUserId(userId);
48
+ console.error(`[初始化] 未检测到本地用户编号,已生成并使用: ${userId}`);
49
+ }
50
+
51
+ const runOptions = {
52
+ collectMax: autoCollectMax,
53
+ scrapeDepth: autoScrapeDepth,
54
+ maxComments: autoMaxComments,
55
+ maxGuess: autoMaxGuess,
56
+ preset: autoPreset,
57
+ switchMax: autoSwitchDelay,
58
+ commentMax: autoCommentDelay,
59
+ enableFollow: autoEnableFollow,
60
+ maxFollowing: autoMaxFollowing,
61
+ maxFollowers: autoMaxFollowers,
62
+ userId,
63
+ };
64
+
65
+ await apiGet(`${serverUrl}/api/stats`);
66
+
67
+ if (autoUsernames.length > 0) {
68
+ const { added, skipped } = await apiPost(`${serverUrl}/api/users`, { usernames: autoUsernames });
69
+ console.error(`种子用户: ${added} 个新增, ${skipped} 个已存在`);
70
+ }
71
+
72
+ console.error(`服务器: ${serverUrl}(断开会自动重连)`);
73
+
74
+ const { ensureBrowserReady, processUser } = await import('../scraper/auto-core.js');
75
+ let browser = await ensureBrowserReady();
76
+
77
+ const page = await getOrCreatePage(browser);
78
+
79
+ let processedCount = 0;
80
+ let errorCount = 0;
81
+ let consecutiveNetworkErrors = 0;
82
+
83
+ while (true) {
84
+ const job = await apiGet(`${serverUrl}/api/job?userId=${encodeURIComponent(userId)}`);
85
+ if (!job.hasJob) break;
86
+
87
+ const username = job.user.uniqueId;
88
+ processedCount++;
89
+
90
+ if (consecutiveNetworkErrors > 0) {
91
+ const waitTime = consecutiveNetworkErrors <= 2
92
+ ? 0
93
+ : consecutiveNetworkErrors <= 5
94
+ ? 30000
95
+ : 300000;
96
+ if (waitTime > 0) {
97
+ console.error(` [网络] 连续 ${consecutiveNetworkErrors} 次网络异常,等待 ${waitTime / 1000}s 后重试...`);
98
+ await new Promise(r => setTimeout(r, waitTime));
99
+ }
100
+ }
101
+
102
+ console.error(`\n[${processedCount}] 处理 @${username}...`);
103
+
104
+ const result = await processUser(page, username, { ...runOptions, browser }, console.error);
105
+
106
+ if (result.restricted) {
107
+ consecutiveNetworkErrors = 0;
108
+ await apiPost(`${serverUrl}/api/job/${username}`, result);
109
+ continue;
110
+ }
111
+
112
+ if (result.error) {
113
+ // 浏览器关闭检测
114
+ if (isBrowserClosedError(new Error(result.error))) {
115
+ const newBrowser = await relaunchBrowser({}, 9222);
116
+ browser = newBrowser;
117
+ const newPage = await getOrCreatePage(browser);
118
+ Object.assign(page, newPage);
119
+ // 重试当前用户
120
+ const retryResult = await processUser(page, username, { ...runOptions, browser }, console.error);
121
+ Object.assign(result, retryResult);
122
+ // 继续下方逻辑
123
+ } else {
124
+ consecutiveNetworkErrors++;
125
+ errorCount++;
126
+ await apiPost(`${serverUrl}/api/job/${username}`, result);
127
+ const errorType = consecutiveNetworkErrors > 1 ? 'network' : 'other';
128
+ await withRetry('report error', () =>
129
+ apiPost(`${serverUrl}/api/error-report`, {
130
+ userId,
131
+ username,
132
+ errorType,
133
+ errorMessage: result.error,
134
+ stage: 'process',
135
+ errorStack: result.errorStack || '',
136
+ })
137
+ ).catch(() => {});
138
+ continue;
139
+ }
140
+ }
141
+
142
+ if (result.captchaDetected) {
143
+ await withRetry('report captcha', () =>
144
+ apiPost(`${serverUrl}/api/error-report`, {
145
+ userId,
146
+ username,
147
+ errorType: 'captcha',
148
+ errorMessage: result.captchaMessage || '页面出现验证码',
149
+ stage: result.captchaStage || 'video-page',
150
+ errorStack: '',
151
+ })
152
+ ).catch(() => {});
153
+ }
154
+
155
+ consecutiveNetworkErrors = 0;
156
+
157
+ const guessedLocation = result.locationCreated || null;
158
+
159
+ const payload = {
160
+ userInfo: result.userInfo || {},
161
+ discoveredVideoAuthors: (result.discoveredVideoAuthors || []).map(item =>
162
+ typeof item === 'object' ? { ...item, guessedLocation } : item
163
+ ),
164
+ discoveredCommentAuthors: (result.discoveredCommentAuthors || []).map(author => ({ author, guessedLocation })),
165
+ discoveredGuessAuthors: (result.discoveredGuessAuthors || []).map(author => ({ author, guessedLocation })),
166
+ discoveredFollowing: (result.discoveredFollowing || []).map(f => ({
167
+ handle: Array.isArray(f) ? f[0] : f,
168
+ displayName: Array.isArray(f) ? f[1] : null,
169
+ guessedLocation,
170
+ })),
171
+ discoveredFollowers: (result.discoveredFollowers || []).map(f => ({
172
+ handle: Array.isArray(f) ? f[0] : f,
173
+ displayName: Array.isArray(f) ? f[1] : null,
174
+ guessedLocation,
175
+ })),
176
+ };
177
+ await apiPost(`${serverUrl}/api/job/${username}`, payload);
178
+ console.error(' 已提交');
179
+ }
180
+
181
+ const stats = await apiGet(`${serverUrl}/api/stats`);
182
+ console.error(`\n完成: ${processedCount} 个用户处理, ${errorCount} 个出错`);
183
+ console.error(` 总用户: ${stats.totalUsers}, 已完成: ${stats.processedUsers}, 待处理: ${stats.pendingUsers}, 错误: ${stats.errorUsers}`);
184
+
185
+ await browser.close().catch(() => {});
186
+ }