tt-help-cli-ycl 1.3.12 → 1.3.14
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/README.md +17 -17
- package/cli.js +9 -9
- package/package.json +47 -45
- package/scripts/run-explore.bat +68 -68
- package/scripts/run-explore.ps1 +81 -81
- package/scripts/run-explore.sh +73 -73
- package/scripts/test-captcha-lib.mjs +68 -0
- package/scripts/test-captcha.mjs +81 -0
- package/scripts/test-incognito-lib.mjs +36 -0
- package/scripts/test-login-state.mjs +128 -0
- package/scripts/test-safe-click.mjs +45 -0
- package/src/cli/attach.js +160 -0
- package/src/cli/auto.js +186 -157
- package/src/cli/config.js +39 -3
- package/src/cli/explore.js +234 -193
- package/src/cli/info.js +88 -0
- package/src/cli/progress.js +111 -111
- package/src/cli/refresh.js +216 -0
- package/src/cli/scrape.js +47 -47
- package/src/cli/utils.js +18 -18
- package/src/cli/videos.js +41 -41
- package/src/cli/watch.js +31 -31
- package/src/lib/args.js +517 -402
- package/src/lib/browser/anti-detect.js +23 -23
- package/src/lib/browser/cdp.js +52 -10
- package/src/lib/browser/launch.js +43 -43
- package/src/lib/browser/page.js +146 -87
- package/src/lib/constants.js +199 -115
- package/src/lib/delay.js +54 -54
- package/src/lib/explore-fetch.js +118 -118
- package/src/lib/fetcher.js +45 -45
- package/src/lib/filter.js +66 -66
- package/src/lib/io.js +54 -54
- package/src/lib/output.js +80 -80
- package/src/lib/parse-ssr.mjs +69 -0
- package/src/lib/parser.js +47 -47
- package/src/lib/retry.js +45 -45
- package/src/lib/scrape.js +89 -40
- package/src/lib/tiktok-scraper.mjs +176 -0
- package/src/lib/url.js +52 -52
- package/src/main.js +12 -16
- package/src/results/user-videos-bar.lar.lar.moeta.json +37 -0
- package/src/scraper/auto-core.js +203 -194
- package/src/scraper/core.js +211 -190
- package/src/scraper/explore-core.js +162 -171
- package/src/scraper/modules/captcha-handler.js +114 -114
- package/src/scraper/modules/comment-extractor.js +74 -69
- package/src/scraper/modules/follow-extractor.js +121 -121
- package/src/scraper/modules/guess-extractor.js +51 -51
- package/src/scraper/modules/page-helpers.js +48 -48
- package/src/scraper/refresh-core.js +179 -0
- package/src/videos/core.js +126 -126
- package/src/watch/data-store.js +536 -302
- package/src/watch/public/index.html +721 -701
- package/src/watch/server.js +527 -359
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { TikTokScraper } from '../lib/tiktok-scraper.mjs';
|
|
2
|
+
|
|
3
|
+
const MAX_RETRY_WAIT = 5 * 60 * 1000;
|
|
4
|
+
|
|
5
|
+
async function withRetry(label, fn) {
|
|
6
|
+
let backoff = 1000;
|
|
7
|
+
while (true) {
|
|
8
|
+
try {
|
|
9
|
+
return await fn();
|
|
10
|
+
} catch (err) {
|
|
11
|
+
console.error(`[连接] ${label} 失败: ${err.message},${backoff / 1000}秒后重试...`);
|
|
12
|
+
await new Promise(r => setTimeout(r, backoff));
|
|
13
|
+
if (backoff < MAX_RETRY_WAIT) backoff *= 2;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function apiGet(url) {
|
|
19
|
+
return withRetry(`GET ${url}`, async () => {
|
|
20
|
+
const res = await fetch(url);
|
|
21
|
+
return res.json();
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function apiPut(url, body) {
|
|
26
|
+
return withRetry(`PUT ${url}`, async () => {
|
|
27
|
+
const res = await fetch(url, {
|
|
28
|
+
method: 'PUT',
|
|
29
|
+
headers: { 'Content-Type': 'application/json' },
|
|
30
|
+
body: JSON.stringify(body),
|
|
31
|
+
});
|
|
32
|
+
return res.json();
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isBrowserClosedError(err) {
|
|
37
|
+
if (!err) return false;
|
|
38
|
+
const msg = err.message || err.toString() || '';
|
|
39
|
+
return msg.includes('Target page, context or browser has been closed') ||
|
|
40
|
+
msg.includes('browser has been closed') ||
|
|
41
|
+
msg.includes('browserContext.newPage') ||
|
|
42
|
+
msg.includes('Protocol error');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function handleAttach(options) {
|
|
46
|
+
const { attachParallel, attachInterval, serverUrl, showHelp } = options;
|
|
47
|
+
|
|
48
|
+
if (showHelp) {
|
|
49
|
+
console.error('用法: tt-help attach [-p 并行数] [-i 间隔秒数] [-s 服务端地址]');
|
|
50
|
+
console.error('');
|
|
51
|
+
console.error('参数:');
|
|
52
|
+
console.error(' -p, --parallel <N> 并行抓取数(默认: 1)');
|
|
53
|
+
console.error(' -i, --interval <N> 无任务时轮询间隔,单位秒(默认: 10)');
|
|
54
|
+
console.error(' -s, --server <URL> 服务端地址(默认: http://127.0.0.1:3001)');
|
|
55
|
+
console.error('');
|
|
56
|
+
console.error('说明:');
|
|
57
|
+
console.error(' 后台轮询服务端 /api/user-update-tasks 接口,自动抓取 TikTok 用户信息');
|
|
58
|
+
console.error(' 抓取完成后通过 PUT /api/user-update-result/{uniqueId} 回传结果');
|
|
59
|
+
console.error(' 浏览器崩溃时自动重启,支持长时间无人值守运行');
|
|
60
|
+
console.error('');
|
|
61
|
+
console.error('示例:');
|
|
62
|
+
console.error(' tt-help attach');
|
|
63
|
+
console.error(' tt-help attach -p 5 -i 10');
|
|
64
|
+
console.error(' tt-help attach -p 3 -i 5 -s http://127.0.0.1:3001');
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
console.error(`[Attach] 并行数: ${attachParallel}, 空闲间隔: ${attachInterval}秒, 服务端: ${serverUrl}`);
|
|
69
|
+
|
|
70
|
+
const scraper = new TikTokScraper();
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
await scraper.init();
|
|
74
|
+
console.error('[Attach] 浏览器初始化完成,开始循环接收任务...');
|
|
75
|
+
|
|
76
|
+
let loopCount = 0;
|
|
77
|
+
let browserRestartCount = 0;
|
|
78
|
+
|
|
79
|
+
while (true) {
|
|
80
|
+
loopCount++;
|
|
81
|
+
|
|
82
|
+
// 检查浏览器是否存活,不存活则重启
|
|
83
|
+
if (!scraper.isAlive()) {
|
|
84
|
+
console.error(`[Attach] 浏览器已关闭,正在重启 (${++browserRestartCount})...`);
|
|
85
|
+
await scraper.restart();
|
|
86
|
+
console.error('[Attach] 浏览器重启完成');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const { total, tasks } = await apiGet(`${serverUrl}/api/user-update-tasks?limit=${attachParallel}`);
|
|
90
|
+
|
|
91
|
+
if (!tasks || tasks.length === 0) {
|
|
92
|
+
if (loopCount === 1) {
|
|
93
|
+
console.error(`[Attach] 当前无待更新任务,${attachInterval}秒后重试...`);
|
|
94
|
+
}
|
|
95
|
+
await new Promise(r => setTimeout(r, attachInterval * 1000));
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.error(`\n[Attach] 获取到 ${tasks.length} 个待更新任务...`);
|
|
100
|
+
|
|
101
|
+
const results = await Promise.allSettled(
|
|
102
|
+
tasks.map(async (task) => {
|
|
103
|
+
const uniqueId = task.uniqueId;
|
|
104
|
+
console.error(` → 获取 @${uniqueId} 的用户信息...`);
|
|
105
|
+
try {
|
|
106
|
+
const info = await scraper.getUserInfo(uniqueId);
|
|
107
|
+
return { uniqueId, info, error: null };
|
|
108
|
+
} catch (err) {
|
|
109
|
+
return { uniqueId, info: null, error: err };
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
let successCount = 0;
|
|
115
|
+
let failCount = 0;
|
|
116
|
+
let needRestart = false;
|
|
117
|
+
|
|
118
|
+
for (const result of results) {
|
|
119
|
+
if (result.status === 'fulfilled') {
|
|
120
|
+
const { uniqueId, info, error } = result.value;
|
|
121
|
+
if (error) {
|
|
122
|
+
if (isBrowserClosedError(error)) {
|
|
123
|
+
needRestart = true;
|
|
124
|
+
}
|
|
125
|
+
console.error(` ✗ @${uniqueId} 获取失败: ${error.message || '未知错误'}`);
|
|
126
|
+
failCount++;
|
|
127
|
+
} else if (info) {
|
|
128
|
+
try {
|
|
129
|
+
await apiPut(`${serverUrl}/api/user-info/${encodeURIComponent(uniqueId)}`, info);
|
|
130
|
+
console.error(` ✓ @${uniqueId} 已提交更新`);
|
|
131
|
+
successCount++;
|
|
132
|
+
} catch (err) {
|
|
133
|
+
console.error(` ✗ @${uniqueId} 提交失败: ${err.message}`);
|
|
134
|
+
failCount++;
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
console.error(` ✗ @${uniqueId} 未获取到用户信息`);
|
|
138
|
+
failCount++;
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
console.error(` ✗ 任务执行异常: ${result.reason?.message || '未知错误'}`);
|
|
142
|
+
failCount++;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.error(` 本批结果: ${successCount} 成功, ${failCount} 失败\n`);
|
|
147
|
+
|
|
148
|
+
if (needRestart) {
|
|
149
|
+
console.error('[Attach] 检测到浏览器异常,将在下一轮重启...');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
await new Promise(r => setTimeout(r, 500));
|
|
153
|
+
}
|
|
154
|
+
} catch (err) {
|
|
155
|
+
console.error(`[Attach] 运行异常: ${err.message}`);
|
|
156
|
+
throw err;
|
|
157
|
+
} finally {
|
|
158
|
+
await scraper.close();
|
|
159
|
+
}
|
|
160
|
+
}
|