tt-help-cli-ycl 1.3.19 → 1.3.21
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 -47
- package/scripts/run-explore copy.bat +68 -68
- package/scripts/run-explore.bat +68 -68
- package/scripts/run-explore.ps1 +81 -81
- package/scripts/run-explore.sh +73 -73
- package/src/cli/attach.js +180 -180
- package/src/cli/auto.js +186 -186
- package/src/cli/config.js +152 -152
- package/src/cli/explore.js +249 -234
- package/src/cli/info.js +88 -88
- package/src/cli/progress.js +111 -111
- package/src/cli/refresh.js +216 -216
- 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/api-interceptor.js +129 -124
- package/src/lib/args.js +517 -517
- package/src/lib/browser/anti-detect.js +23 -23
- package/src/lib/browser/cdp.js +194 -194
- package/src/lib/browser/launch.js +43 -43
- package/src/lib/browser/page.js +156 -146
- package/src/lib/constants.js +199 -199
- 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 -69
- package/src/lib/parser.js +47 -47
- package/src/lib/retry.js +45 -45
- package/src/lib/scrape.js +89 -89
- package/src/lib/tiktok-scraper.mjs +194 -194
- package/src/lib/url.js +52 -52
- package/src/main.js +42 -42
- package/src/scraper/auto-core.js +203 -203
- package/src/scraper/core.js +211 -211
- package/src/scraper/explore-core.js +170 -162
- package/src/scraper/modules/captcha-handler.js +114 -114
- package/src/scraper/modules/comment-extractor.js +74 -74
- package/src/scraper/modules/follow-extractor.js +118 -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 -179
- package/src/utils/index.js +31 -0
- package/src/videos/core.js +97 -97
- package/src/watch/data-store.js +828 -705
- package/src/watch/public/index.html +753 -722
- package/src/watch/server.js +705 -562
- package/scripts/test-captcha-lib.mjs +0 -68
- package/scripts/test-captcha.mjs +0 -81
- package/scripts/test-incognito-lib.mjs +0 -36
- package/scripts/test-login-state.mjs +0 -128
- package/scripts/test-safe-click.mjs +0 -45
- package/src/results/user-videos-bar.lar.lar.moeta.json +0 -37
package/scripts/run-explore.sh
CHANGED
|
@@ -1,73 +1,73 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
|
|
3
|
-
PACKAGENAME="tt-help-cli-ycl"
|
|
4
|
-
TARGET_SERVER="http://117.71.53.99:17301"
|
|
5
|
-
CONFIG_PATH="$HOME/.tt-help.json"
|
|
6
|
-
|
|
7
|
-
echo "========================================"
|
|
8
|
-
echo " tt-help-cli-ycl 一键启动脚本 (macOS)"
|
|
9
|
-
echo "========================================"
|
|
10
|
-
|
|
11
|
-
# ---------- 1. 检查/安装最新版本 ----------
|
|
12
|
-
LATEST_VERSION=$(npm view "$PACKAGENAME" version 2>/dev/null)
|
|
13
|
-
|
|
14
|
-
if [ -z "$LATEST_VERSION" ]; then
|
|
15
|
-
echo "[错误] 无法从 npm 获取最新版本信息"
|
|
16
|
-
exit 1
|
|
17
|
-
fi
|
|
18
|
-
|
|
19
|
-
INSTALLED_VERSION=$(npm list -g "$PACKAGENAME" --depth=0 2>/dev/null | grep "$PACKAGENAME@" | head -1 | sed 's/.*@//' | sed 's/[) ].*//')
|
|
20
|
-
|
|
21
|
-
if [ -z "$INSTALLED_VERSION" ]; then
|
|
22
|
-
echo "[提示] tt-help-cli-ycl 未安装,正在安装最新版本..."
|
|
23
|
-
npm install -g "$PACKAGENAME"
|
|
24
|
-
if [ $? -eq 0 ]; then
|
|
25
|
-
echo "[OK] 安装完成: $LATEST_VERSION"
|
|
26
|
-
else
|
|
27
|
-
echo "[错误] 安装失败,请手动执行: npm install -g $PACKAGENAME"
|
|
28
|
-
exit 1
|
|
29
|
-
fi
|
|
30
|
-
elif [ "$INSTALLED_VERSION" = "$LATEST_VERSION" ]; then
|
|
31
|
-
echo "[OK] tt-help-cli-ycl 已是最新版本: $LATEST_VERSION"
|
|
32
|
-
else
|
|
33
|
-
echo "[提示] 当前版本: $INSTALLED_VERSION, 最新版本: $LATEST_VERSION"
|
|
34
|
-
echo "[执行] 正在升级最新版本..."
|
|
35
|
-
npm install -g "$PACKAGENAME" 2>/dev/null
|
|
36
|
-
if [ $? -eq 0 ]; then
|
|
37
|
-
echo "[OK] 安装完成: $LATEST_VERSION"
|
|
38
|
-
else
|
|
39
|
-
echo "[警告] 自动安装失败,尝试手动更新..."
|
|
40
|
-
npm update -g "$PACKAGENAME"
|
|
41
|
-
fi
|
|
42
|
-
fi
|
|
43
|
-
|
|
44
|
-
# ---------- 2. 检查/设置 server 配置 ----------
|
|
45
|
-
CURRENT_SERVER=""
|
|
46
|
-
if [ -f "$CONFIG_PATH" ]; then
|
|
47
|
-
CURRENT_SERVER=$(node -e "try{const c=JSON.parse(require('fs').readFileSync('$CONFIG_PATH','utf-8'));console.log(c.server||'')}catch(e){}" 2>/dev/null)
|
|
48
|
-
fi
|
|
49
|
-
|
|
50
|
-
if [ "$CURRENT_SERVER" = "$TARGET_SERVER" ]; then
|
|
51
|
-
echo "[OK] Server 配置正确: $TARGET_SERVER"
|
|
52
|
-
else
|
|
53
|
-
echo "[提示] 当前 server: ${CURRENT_SERVER:-未设置}, 目标: $TARGET_SERVER"
|
|
54
|
-
echo "[执行] 正在设置 server 配置..."
|
|
55
|
-
node -e "
|
|
56
|
-
const fs = require('fs');
|
|
57
|
-
const path = require('path');
|
|
58
|
-
const configPath = path.join(require('os').homedir(), '.tt-help.json');
|
|
59
|
-
let cfg = {};
|
|
60
|
-
try { cfg = JSON.parse(fs.readFileSync(configPath, 'utf-8')); } catch(e) {}
|
|
61
|
-
cfg.server = '$TARGET_SERVER';
|
|
62
|
-
fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2), 'utf-8');
|
|
63
|
-
console.log(' 已写入: ' + configPath);
|
|
64
|
-
"
|
|
65
|
-
echo "[OK] Server 配置已设置"
|
|
66
|
-
fi
|
|
67
|
-
|
|
68
|
-
# ---------- 3. 启动 tt-help explore ----------
|
|
69
|
-
echo ""
|
|
70
|
-
echo "========================================"
|
|
71
|
-
echo " 启动 tt-help explore"
|
|
72
|
-
echo "========================================"
|
|
73
|
-
tt-help explore
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
PACKAGENAME="tt-help-cli-ycl"
|
|
4
|
+
TARGET_SERVER="http://117.71.53.99:17301"
|
|
5
|
+
CONFIG_PATH="$HOME/.tt-help.json"
|
|
6
|
+
|
|
7
|
+
echo "========================================"
|
|
8
|
+
echo " tt-help-cli-ycl 一键启动脚本 (macOS)"
|
|
9
|
+
echo "========================================"
|
|
10
|
+
|
|
11
|
+
# ---------- 1. 检查/安装最新版本 ----------
|
|
12
|
+
LATEST_VERSION=$(npm view "$PACKAGENAME" version 2>/dev/null)
|
|
13
|
+
|
|
14
|
+
if [ -z "$LATEST_VERSION" ]; then
|
|
15
|
+
echo "[错误] 无法从 npm 获取最新版本信息"
|
|
16
|
+
exit 1
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
INSTALLED_VERSION=$(npm list -g "$PACKAGENAME" --depth=0 2>/dev/null | grep "$PACKAGENAME@" | head -1 | sed 's/.*@//' | sed 's/[) ].*//')
|
|
20
|
+
|
|
21
|
+
if [ -z "$INSTALLED_VERSION" ]; then
|
|
22
|
+
echo "[提示] tt-help-cli-ycl 未安装,正在安装最新版本..."
|
|
23
|
+
npm install -g "$PACKAGENAME"
|
|
24
|
+
if [ $? -eq 0 ]; then
|
|
25
|
+
echo "[OK] 安装完成: $LATEST_VERSION"
|
|
26
|
+
else
|
|
27
|
+
echo "[错误] 安装失败,请手动执行: npm install -g $PACKAGENAME"
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
elif [ "$INSTALLED_VERSION" = "$LATEST_VERSION" ]; then
|
|
31
|
+
echo "[OK] tt-help-cli-ycl 已是最新版本: $LATEST_VERSION"
|
|
32
|
+
else
|
|
33
|
+
echo "[提示] 当前版本: $INSTALLED_VERSION, 最新版本: $LATEST_VERSION"
|
|
34
|
+
echo "[执行] 正在升级最新版本..."
|
|
35
|
+
npm install -g "$PACKAGENAME" 2>/dev/null
|
|
36
|
+
if [ $? -eq 0 ]; then
|
|
37
|
+
echo "[OK] 安装完成: $LATEST_VERSION"
|
|
38
|
+
else
|
|
39
|
+
echo "[警告] 自动安装失败,尝试手动更新..."
|
|
40
|
+
npm update -g "$PACKAGENAME"
|
|
41
|
+
fi
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# ---------- 2. 检查/设置 server 配置 ----------
|
|
45
|
+
CURRENT_SERVER=""
|
|
46
|
+
if [ -f "$CONFIG_PATH" ]; then
|
|
47
|
+
CURRENT_SERVER=$(node -e "try{const c=JSON.parse(require('fs').readFileSync('$CONFIG_PATH','utf-8'));console.log(c.server||'')}catch(e){}" 2>/dev/null)
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
if [ "$CURRENT_SERVER" = "$TARGET_SERVER" ]; then
|
|
51
|
+
echo "[OK] Server 配置正确: $TARGET_SERVER"
|
|
52
|
+
else
|
|
53
|
+
echo "[提示] 当前 server: ${CURRENT_SERVER:-未设置}, 目标: $TARGET_SERVER"
|
|
54
|
+
echo "[执行] 正在设置 server 配置..."
|
|
55
|
+
node -e "
|
|
56
|
+
const fs = require('fs');
|
|
57
|
+
const path = require('path');
|
|
58
|
+
const configPath = path.join(require('os').homedir(), '.tt-help.json');
|
|
59
|
+
let cfg = {};
|
|
60
|
+
try { cfg = JSON.parse(fs.readFileSync(configPath, 'utf-8')); } catch(e) {}
|
|
61
|
+
cfg.server = '$TARGET_SERVER';
|
|
62
|
+
fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2), 'utf-8');
|
|
63
|
+
console.log(' 已写入: ' + configPath);
|
|
64
|
+
"
|
|
65
|
+
echo "[OK] Server 配置已设置"
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
# ---------- 3. 启动 tt-help explore ----------
|
|
69
|
+
echo ""
|
|
70
|
+
echo "========================================"
|
|
71
|
+
echo " 启动 tt-help explore"
|
|
72
|
+
echo "========================================"
|
|
73
|
+
tt-help explore
|
package/src/cli/attach.js
CHANGED
|
@@ -1,180 +1,180 @@
|
|
|
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
|
-
|
|
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
|
-
function isBrowserClosedError(err) {
|
|
38
|
-
if (!err) return false;
|
|
39
|
-
const msg = err.message || err.toString() || '';
|
|
40
|
-
return msg.includes('Target page, context or browser has been closed') ||
|
|
41
|
-
msg.includes('browser has been closed') ||
|
|
42
|
-
msg.includes('browserContext.newPage') ||
|
|
43
|
-
msg.includes('Protocol error');
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export async function handleAttach(options) {
|
|
47
|
-
const { attachParallel, attachInterval, serverUrl, showHelp } = options;
|
|
48
|
-
|
|
49
|
-
if (showHelp) {
|
|
50
|
-
console.error('用法: tt-help attach [-p 并行数] [-i 间隔秒数] [-s 服务端地址]');
|
|
51
|
-
console.error('');
|
|
52
|
-
console.error('参数:');
|
|
53
|
-
console.error(' -p, --parallel <N> 并行抓取数(默认: 1)');
|
|
54
|
-
console.error(' -i, --interval <N> 无任务时轮询间隔,单位秒(默认: 10)');
|
|
55
|
-
console.error(' -s, --server <URL> 服务端地址(默认: http://127.0.0.1:3001)');
|
|
56
|
-
console.error('');
|
|
57
|
-
console.error('说明:');
|
|
58
|
-
console.error(' 后台轮询服务端 /api/user-update-tasks 接口,自动抓取 TikTok 用户信息');
|
|
59
|
-
console.error(' 抓取完成后通过 PUT /api/user-update-result/{uniqueId} 回传结果');
|
|
60
|
-
console.error(' 浏览器崩溃时自动重启,支持长时间无人值守运行');
|
|
61
|
-
console.error('');
|
|
62
|
-
console.error('示例:');
|
|
63
|
-
console.error(' tt-help attach');
|
|
64
|
-
console.error(' tt-help attach -p 5 -i 10');
|
|
65
|
-
console.error(' tt-help attach -p 3 -i 5 -s http://127.0.0.1:3001');
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
console.error(`[Attach] 并行数: ${attachParallel}, 空闲间隔: ${attachInterval}秒, 服务端: ${serverUrl}`);
|
|
70
|
-
|
|
71
|
-
const scraper = new TikTokScraper();
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
await scraper.init();
|
|
75
|
-
console.error('[Attach] 浏览器初始化完成,开始循环接收任务...');
|
|
76
|
-
|
|
77
|
-
let loopCount = 0;
|
|
78
|
-
let browserRestartCount = 0;
|
|
79
|
-
|
|
80
|
-
while (true) {
|
|
81
|
-
loopCount++;
|
|
82
|
-
|
|
83
|
-
// 检查浏览器是否存活,不存活则重启
|
|
84
|
-
if (!scraper.isAlive()) {
|
|
85
|
-
console.error(`[Attach] 浏览器已关闭,正在重启 (${++browserRestartCount})...`);
|
|
86
|
-
await scraper.restart();
|
|
87
|
-
console.error('[Attach] 浏览器重启完成');
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const { total, tasks } = await apiGet(`${serverUrl}/api/user-update-tasks?limit=${attachParallel}`);
|
|
91
|
-
|
|
92
|
-
if (!tasks || tasks.length === 0) {
|
|
93
|
-
if (loopCount === 1) {
|
|
94
|
-
console.error(`[Attach] 当前无待更新任务,${attachInterval}秒后重试...`);
|
|
95
|
-
}
|
|
96
|
-
await new Promise(r => setTimeout(r, attachInterval * 1000));
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
console.error(`\n[Attach] 获取到 ${tasks.length} 个待更新任务...`);
|
|
101
|
-
|
|
102
|
-
const results = await Promise.allSettled(
|
|
103
|
-
tasks.map(async (task) => {
|
|
104
|
-
const uniqueId = task.uniqueId;
|
|
105
|
-
console.error(` → 获取 @${uniqueId} 的用户信息...`);
|
|
106
|
-
try {
|
|
107
|
-
const info = await scraper.getUserInfo(uniqueId);
|
|
108
|
-
return { uniqueId, info, error: null };
|
|
109
|
-
} catch (err) {
|
|
110
|
-
return { uniqueId, info: null, error: err };
|
|
111
|
-
}
|
|
112
|
-
})
|
|
113
|
-
);
|
|
114
|
-
|
|
115
|
-
let successCount = 0;
|
|
116
|
-
let failCount = 0;
|
|
117
|
-
let needRestart = false;
|
|
118
|
-
|
|
119
|
-
// 收集抓取成功的任务,记录抓取失败的
|
|
120
|
-
const successTasks = [];
|
|
121
|
-
for (const result of results) {
|
|
122
|
-
if (result.status === 'fulfilled') {
|
|
123
|
-
const { uniqueId, info, error } = result.value;
|
|
124
|
-
if (error) {
|
|
125
|
-
if (isBrowserClosedError(error)) {
|
|
126
|
-
needRestart = true;
|
|
127
|
-
}
|
|
128
|
-
console.error(` ✗ @${uniqueId} 获取失败: ${error.message || '未知错误'}`);
|
|
129
|
-
failCount++;
|
|
130
|
-
} else if (info) {
|
|
131
|
-
successTasks.push({ uniqueId, info });
|
|
132
|
-
} else {
|
|
133
|
-
console.error(` ✗ @${uniqueId} 未获取到用户信息`);
|
|
134
|
-
failCount++;
|
|
135
|
-
}
|
|
136
|
-
} else {
|
|
137
|
-
console.error(` ✗ 任务执行异常: ${result.reason?.message || '未知错误'}`);
|
|
138
|
-
failCount++;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// 批量提交成功的结果
|
|
143
|
-
if (successTasks.length > 0) {
|
|
144
|
-
try {
|
|
145
|
-
const batchRet = await apiPost(`${serverUrl}/api/user-info-batch`, { updates: successTasks });
|
|
146
|
-
if (batchRet && batchRet.results) {
|
|
147
|
-
for (const r of batchRet.results) {
|
|
148
|
-
if (r.ok) {
|
|
149
|
-
successCount++;
|
|
150
|
-
console.error(` ✓ @${r.uniqueId} 已提交更新`);
|
|
151
|
-
} else {
|
|
152
|
-
failCount++;
|
|
153
|
-
console.error(` ✗ @${r.uniqueId} 提交失败: ${r.error}`);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
} else {
|
|
157
|
-
successCount = successTasks.length;
|
|
158
|
-
console.error(` ✓ 批量提交完成 (${successTasks.length} 条)`);
|
|
159
|
-
}
|
|
160
|
-
} catch (err) {
|
|
161
|
-
failCount += successTasks.length;
|
|
162
|
-
console.error(` ✗ 批量提交失败: ${err.message}`);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
console.error(` 本批结果: ${successCount} 成功, ${failCount} 失败\n`);
|
|
167
|
-
|
|
168
|
-
if (needRestart) {
|
|
169
|
-
console.error('[Attach] 检测到浏览器异常,将在下一轮重启...');
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
await new Promise(r => setTimeout(r, 500));
|
|
173
|
-
}
|
|
174
|
-
} catch (err) {
|
|
175
|
-
console.error(`[Attach] 运行异常: ${err.message}`);
|
|
176
|
-
throw err;
|
|
177
|
-
} finally {
|
|
178
|
-
await scraper.close();
|
|
179
|
-
}
|
|
180
|
-
}
|
|
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
|
+
|
|
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
|
+
function isBrowserClosedError(err) {
|
|
38
|
+
if (!err) return false;
|
|
39
|
+
const msg = err.message || err.toString() || '';
|
|
40
|
+
return msg.includes('Target page, context or browser has been closed') ||
|
|
41
|
+
msg.includes('browser has been closed') ||
|
|
42
|
+
msg.includes('browserContext.newPage') ||
|
|
43
|
+
msg.includes('Protocol error');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function handleAttach(options) {
|
|
47
|
+
const { attachParallel, attachInterval, serverUrl, showHelp } = options;
|
|
48
|
+
|
|
49
|
+
if (showHelp) {
|
|
50
|
+
console.error('用法: tt-help attach [-p 并行数] [-i 间隔秒数] [-s 服务端地址]');
|
|
51
|
+
console.error('');
|
|
52
|
+
console.error('参数:');
|
|
53
|
+
console.error(' -p, --parallel <N> 并行抓取数(默认: 1)');
|
|
54
|
+
console.error(' -i, --interval <N> 无任务时轮询间隔,单位秒(默认: 10)');
|
|
55
|
+
console.error(' -s, --server <URL> 服务端地址(默认: http://127.0.0.1:3001)');
|
|
56
|
+
console.error('');
|
|
57
|
+
console.error('说明:');
|
|
58
|
+
console.error(' 后台轮询服务端 /api/user-update-tasks 接口,自动抓取 TikTok 用户信息');
|
|
59
|
+
console.error(' 抓取完成后通过 PUT /api/user-update-result/{uniqueId} 回传结果');
|
|
60
|
+
console.error(' 浏览器崩溃时自动重启,支持长时间无人值守运行');
|
|
61
|
+
console.error('');
|
|
62
|
+
console.error('示例:');
|
|
63
|
+
console.error(' tt-help attach');
|
|
64
|
+
console.error(' tt-help attach -p 5 -i 10');
|
|
65
|
+
console.error(' tt-help attach -p 3 -i 5 -s http://127.0.0.1:3001');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.error(`[Attach] 并行数: ${attachParallel}, 空闲间隔: ${attachInterval}秒, 服务端: ${serverUrl}`);
|
|
70
|
+
|
|
71
|
+
const scraper = new TikTokScraper();
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
await scraper.init();
|
|
75
|
+
console.error('[Attach] 浏览器初始化完成,开始循环接收任务...');
|
|
76
|
+
|
|
77
|
+
let loopCount = 0;
|
|
78
|
+
let browserRestartCount = 0;
|
|
79
|
+
|
|
80
|
+
while (true) {
|
|
81
|
+
loopCount++;
|
|
82
|
+
|
|
83
|
+
// 检查浏览器是否存活,不存活则重启
|
|
84
|
+
if (!scraper.isAlive()) {
|
|
85
|
+
console.error(`[Attach] 浏览器已关闭,正在重启 (${++browserRestartCount})...`);
|
|
86
|
+
await scraper.restart();
|
|
87
|
+
console.error('[Attach] 浏览器重启完成');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const { total, tasks } = await apiGet(`${serverUrl}/api/user-update-tasks?limit=${attachParallel}`);
|
|
91
|
+
|
|
92
|
+
if (!tasks || tasks.length === 0) {
|
|
93
|
+
if (loopCount === 1) {
|
|
94
|
+
console.error(`[Attach] 当前无待更新任务,${attachInterval}秒后重试...`);
|
|
95
|
+
}
|
|
96
|
+
await new Promise(r => setTimeout(r, attachInterval * 1000));
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.error(`\n[Attach] 获取到 ${tasks.length} 个待更新任务...`);
|
|
101
|
+
|
|
102
|
+
const results = await Promise.allSettled(
|
|
103
|
+
tasks.map(async (task) => {
|
|
104
|
+
const uniqueId = task.uniqueId;
|
|
105
|
+
console.error(` → 获取 @${uniqueId} 的用户信息...`);
|
|
106
|
+
try {
|
|
107
|
+
const info = await scraper.getUserInfo(uniqueId);
|
|
108
|
+
return { uniqueId, info, error: null };
|
|
109
|
+
} catch (err) {
|
|
110
|
+
return { uniqueId, info: null, error: err };
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
let successCount = 0;
|
|
116
|
+
let failCount = 0;
|
|
117
|
+
let needRestart = false;
|
|
118
|
+
|
|
119
|
+
// 收集抓取成功的任务,记录抓取失败的
|
|
120
|
+
const successTasks = [];
|
|
121
|
+
for (const result of results) {
|
|
122
|
+
if (result.status === 'fulfilled') {
|
|
123
|
+
const { uniqueId, info, error } = result.value;
|
|
124
|
+
if (error) {
|
|
125
|
+
if (isBrowserClosedError(error)) {
|
|
126
|
+
needRestart = true;
|
|
127
|
+
}
|
|
128
|
+
console.error(` ✗ @${uniqueId} 获取失败: ${error.message || '未知错误'}`);
|
|
129
|
+
failCount++;
|
|
130
|
+
} else if (info) {
|
|
131
|
+
successTasks.push({ uniqueId, info });
|
|
132
|
+
} else {
|
|
133
|
+
console.error(` ✗ @${uniqueId} 未获取到用户信息`);
|
|
134
|
+
failCount++;
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
console.error(` ✗ 任务执行异常: ${result.reason?.message || '未知错误'}`);
|
|
138
|
+
failCount++;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 批量提交成功的结果
|
|
143
|
+
if (successTasks.length > 0) {
|
|
144
|
+
try {
|
|
145
|
+
const batchRet = await apiPost(`${serverUrl}/api/user-info-batch`, { updates: successTasks });
|
|
146
|
+
if (batchRet && batchRet.results) {
|
|
147
|
+
for (const r of batchRet.results) {
|
|
148
|
+
if (r.ok) {
|
|
149
|
+
successCount++;
|
|
150
|
+
console.error(` ✓ @${r.uniqueId} 已提交更新`);
|
|
151
|
+
} else {
|
|
152
|
+
failCount++;
|
|
153
|
+
console.error(` ✗ @${r.uniqueId} 提交失败: ${r.error}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
successCount = successTasks.length;
|
|
158
|
+
console.error(` ✓ 批量提交完成 (${successTasks.length} 条)`);
|
|
159
|
+
}
|
|
160
|
+
} catch (err) {
|
|
161
|
+
failCount += successTasks.length;
|
|
162
|
+
console.error(` ✗ 批量提交失败: ${err.message}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
console.error(` 本批结果: ${successCount} 成功, ${failCount} 失败\n`);
|
|
167
|
+
|
|
168
|
+
if (needRestart) {
|
|
169
|
+
console.error('[Attach] 检测到浏览器异常,将在下一轮重启...');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
await new Promise(r => setTimeout(r, 500));
|
|
173
|
+
}
|
|
174
|
+
} catch (err) {
|
|
175
|
+
console.error(`[Attach] 运行异常: ${err.message}`);
|
|
176
|
+
throw err;
|
|
177
|
+
} finally {
|
|
178
|
+
await scraper.close();
|
|
179
|
+
}
|
|
180
|
+
}
|