tt-help-cli-ycl 1.3.8 → 1.3.10

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.
@@ -0,0 +1,68 @@
1
+ @ECHO OFF
2
+ SETLOCAL
3
+
4
+ SET "PACKAGENAME=tt-help-cli-ycl"
5
+ SET "TARGET_SERVER=http://117.71.53.99:17301"
6
+ SET "CONFIG_PATH=%USERPROFILE%\.tt-help.json"
7
+
8
+ ECHO ========================================
9
+ ECHO tt-help-cli-ycl one-click launcher (Windows CMD)
10
+ ECHO ========================================
11
+
12
+ REM ---------- 1. Check/install latest version ----------
13
+ FOR /F "delims=" %%V IN ('npm view %PACKAGENAME% version 2^>nul') DO SET "LATEST_VERSION=%%V"
14
+
15
+ IF NOT DEFINED LATEST_VERSION (
16
+ ECHO [ERROR] Cannot get latest version from npm
17
+ EXIT /B 1
18
+ )
19
+
20
+ FOR /F "tokens=2 delims=@" %%V IN ('npm list -g %PACKAGENAME% --depth=0 2^>nul ^| findstr /i "%PACKAGENAME%"') DO SET "INSTALLED_VERSION=%%V"
21
+
22
+ IF NOT DEFINED INSTALLED_VERSION (
23
+ ECHO [INFO] %PACKAGENAME% not installed, installing latest...
24
+ CALL npm install -g %PACKAGENAME%
25
+ IF %ERRORLEVEL% EQU 0 (
26
+ ECHO [OK] Installed: %LATEST_VERSION%
27
+ ) ELSE (
28
+ ECHO [ERROR] Install failed, run manually: npm install -g %PACKAGENAME%
29
+ EXIT /B 1
30
+ )
31
+ ) ELSE IF "%INSTALLED_VERSION%"=="%LATEST_VERSION%" (
32
+ ECHO [OK] %PACKAGENAME% is up to date: %LATEST_VERSION%
33
+ ) ELSE (
34
+ ECHO [INFO] Current: %INSTALLED_VERSION%, Latest: %LATEST_VERSION%
35
+ ECHO [INFO] Upgrading to latest...
36
+ CALL npm install -g %PACKAGENAME%
37
+ IF %ERRORLEVEL% EQU 0 (
38
+ ECHO [OK] Upgraded: %LATEST_VERSION%
39
+ ) ELSE (
40
+ ECHO [WARN] Upgrade failed, run manually: npm install -g %PACKAGENAME%
41
+ )
42
+ )
43
+
44
+ REM ---------- 2. Check/set server config ----------
45
+ SET "CURRENT_SERVER="
46
+ IF EXIST "%CONFIG_PATH%" (
47
+ FOR /F "delims=" %%S IN ('node -e "try{const c=JSON.parse(require('fs').readFileSync('%CONFIG_PATH%','utf-8'));console.log(c.server||'')}catch(e){}" 2^>nul') DO SET "CURRENT_SERVER=%%S"
48
+ )
49
+
50
+ IF "%CURRENT_SERVER%"=="%TARGET_SERVER%" (
51
+ ECHO [OK] Server config is correct: %TARGET_SERVER%
52
+ ) ELSE (
53
+ IF "%CURRENT_SERVER%"=="" (
54
+ ECHO [INFO] Current server: not set, target: %TARGET_SERVER%
55
+ ) ELSE (
56
+ ECHO [INFO] Current server: %CURRENT_SERVER%, target: %TARGET_SERVER%
57
+ )
58
+ ECHO [INFO] Setting server config...
59
+ node -e "const fs=require('fs'),path=require('path');const p=path.join(require('os').homedir(),'.tt-help.json');let c={};try{c=JSON.parse(fs.readFileSync(p,'utf-8'))}catch(e){}c.server='%TARGET_SERVER%';fs.writeFileSync(p,JSON.stringify(c,null,2),'utf-8');console.log(' Written to: '+p);"
60
+ ECHO [OK] Server config set
61
+ )
62
+
63
+ REM ---------- 3. Start tt-help explore ----------
64
+ ECHO.
65
+ ECHO ========================================
66
+ ECHO Starting tt-help explore
67
+ ECHO ========================================
68
+ tt-help explore
@@ -0,0 +1,81 @@
1
+ $packageName = "tt-help-cli-ycl"
2
+ $targetServer = "http://117.71.53.99:17301"
3
+ $configPath = Join-Path $env:USERPROFILE ".tt-help.json"
4
+
5
+ Write-Host "========================================"
6
+ Write-Host " tt-help-cli-ycl 一键启动脚本 (Windows)"
7
+ Write-Host "========================================"
8
+
9
+ # ---------- 1. 检查/安装最新版本 ----------
10
+ $latestVersion = npm view $packageName version 2>$null
11
+
12
+ if (-not $latestVersion) {
13
+ Write-Host "[错误] 无法从 npm 获取最新版本信息"
14
+ exit 1
15
+ }
16
+
17
+ $installedVersion = ""
18
+ $output = npm list -g $packageName --depth=0 2>$null
19
+ if ($output -match "$packageName@([\d.]+)") {
20
+ $installedVersion = $matches[1]
21
+ }
22
+
23
+ if (-not $installedVersion) {
24
+ Write-Host "[提示] tt-help-cli-ycl 未安装,正在安装最新版本..."
25
+ npm install -g $packageName 2>$null
26
+ if ($LASTEXITCODE -eq 0) {
27
+ Write-Host "[OK] 安装完成: $latestVersion"
28
+ } else {
29
+ Write-Host "[错误] 安装失败,请手动执行: npm install -g $packageName"
30
+ exit 1
31
+ }
32
+ } elseif ($installedVersion -eq $latestVersion) {
33
+ Write-Host "[OK] tt-help-cli-ycl 已是最新版本: $latestVersion"
34
+ } else {
35
+ Write-Host "[提示] 当前版本: $installedVersion, 最新版本: $latestVersion"
36
+ Write-Host "[执行] 正在升级最新版本..."
37
+ npm install -g $packageName 2>$null
38
+ if ($LASTEXITCODE -eq 0) {
39
+ Write-Host "[OK] 升级完成: $latestVersion"
40
+ } else {
41
+ Write-Host "[警告] 升级失败,请手动执行: npm install -g $packageName"
42
+ }
43
+ }
44
+
45
+ # ---------- 2. 检查/设置 server 配置 ----------
46
+ $currentServer = ""
47
+ if (Test-Path $configPath) {
48
+ try {
49
+ $config = Get-Content $configPath -Raw | ConvertFrom-Json
50
+ $currentServer = $config.server
51
+ } catch {
52
+ $currentServer = ""
53
+ }
54
+ }
55
+
56
+ if ($currentServer -eq $targetServer) {
57
+ Write-Host "[OK] Server 配置正确: $targetServer"
58
+ } else {
59
+ $currentDisplay = if ($currentServer) { $currentServer } else { "未设置" }
60
+ Write-Host "[提示] 当前 server: $currentDisplay, 目标: $targetServer"
61
+ Write-Host "[执行] 正在设置 server 配置..."
62
+
63
+ node -e "
64
+ const fs = require('fs');
65
+ const path = require('path');
66
+ const configPath = path.join(require('os').homedir(), '.tt-help.json');
67
+ let cfg = {};
68
+ try { cfg = JSON.parse(fs.readFileSync(configPath, 'utf-8')); } catch(e) {}
69
+ cfg.server = '$targetServer';
70
+ fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2), 'utf-8');
71
+ console.log(' 已写入: ' + configPath);
72
+ "
73
+ Write-Host "[OK] Server 配置已设置"
74
+ }
75
+
76
+ # ---------- 3. 启动 tt-help explore ----------
77
+ Write-Host ""
78
+ Write-Host "========================================"
79
+ Write-Host " 启动 tt-help explore"
80
+ Write-Host "========================================"
81
+ tt-help explore
@@ -0,0 +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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tt-help-cli-ycl",
3
- "version": "1.3.8",
3
+ "version": "1.3.10",
4
4
  "description": "TikTok user & video data scraper - extract ttSeller, verified, locationCreated from HTML source",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,7 +9,8 @@
9
9
  "main": "src/main.mjs",
10
10
  "files": [
11
11
  "cli.js",
12
- "src/"
12
+ "src/",
13
+ "bat/"
13
14
  ],
14
15
  "scripts": {
15
16
  "start": "node src/main.mjs"
package/src/cli/auto.js CHANGED
@@ -77,6 +77,7 @@ export async function handleAuto(options) {
77
77
 
78
78
  let processedCount = 0;
79
79
  let errorCount = 0;
80
+ let consecutiveNetworkErrors = 0;
80
81
 
81
82
  while (true) {
82
83
  const job = await apiGet(`${serverUrl}/api/job?userId=${encodeURIComponent(userId)}`);
@@ -84,43 +85,68 @@ export async function handleAuto(options) {
84
85
 
85
86
  const username = job.user.uniqueId;
86
87
  processedCount++;
87
- let proxyRetry = 0;
88
88
 
89
- while (true) {
90
- console.error(`\n[${processedCount}] 处理 @${username}...${proxyRetry > 0 ? ` (代理重试 ${proxyRetry})` : ''}`);
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
+ }
91
100
 
92
- const result = await processUser(page, username, { ...runOptions, browser }, console.error);
101
+ console.error(`\n[${processedCount}] 处理 @${username}...`);
93
102
 
94
- if (result.restricted) {
95
- await apiPost(`${serverUrl}/api/job/${username}`, result);
96
- break;
97
- }
103
+ const result = await processUser(page, username, { ...runOptions, browser }, console.error);
98
104
 
99
- if (result.error && result.error.includes('代理错误')) {
100
- proxyRetry++;
101
- console.error(` [代理错误] ${result.error},等待 10s 后重试...`);
102
- await new Promise(r => setTimeout(r, 10000));
103
- continue;
104
- }
105
+ if (result.restricted) {
106
+ consecutiveNetworkErrors = 0;
107
+ await apiPost(`${serverUrl}/api/job/${username}`, result);
108
+ continue;
109
+ }
105
110
 
106
- if (result.error) {
107
- errorCount++;
108
- await apiPost(`${serverUrl}/api/job/${username}`, result);
109
- break;
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
+ }
111
126
 
112
- const payload = {
113
- userInfo: result.userInfo || {},
114
- discoveredVideoAuthors: result.discoveredVideoAuthors || [],
115
- discoveredCommentAuthors: result.discoveredCommentAuthors || [],
116
- discoveredGuessAuthors: result.discoveredGuessAuthors || [],
117
- discoveredFollowing: result.discoveredFollowing || [],
118
- discoveredFollowers: result.discoveredFollowers || [],
119
- };
120
- await apiPost(`${serverUrl}/api/job/${username}`, payload);
121
- console.error(' 已提交');
122
- break;
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(() => {});
123
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(' 已提交');
124
150
  }
125
151
 
126
152
  const stats = await apiGet(`${serverUrl}/api/stats`);
@@ -72,6 +72,7 @@ export async function handleExplore(options) {
72
72
 
73
73
  let processedCount = 0;
74
74
  let errorCount = 0;
75
+ let consecutiveNetworkErrors = 0;
75
76
 
76
77
  while (true) {
77
78
  const job = await apiGet(`${serverUrl}/api/job?userId=${encodeURIComponent(userId)}`);
@@ -79,60 +80,93 @@ export async function handleExplore(options) {
79
80
 
80
81
  const username = job.user.uniqueId;
81
82
  processedCount++;
82
- let proxyRetry = 0;
83
-
84
- while (true) {
85
- console.error(`\n[${processedCount}] 探索 @${username}...${proxyRetry > 0 ? ` (代理重试 ${proxyRetry})` : ''}`);
86
-
87
- const { switchMax } = getDelayConfig();
88
- await delay(switchMax, switchMax * 3);
89
-
90
- const result = await processExplore(page, username, {
91
- maxComments: exploreMaxComments,
92
- maxGuess: exploreMaxGuess,
93
- enableFollow: exploreEnableFollow,
94
- maxFollowing: exploreMaxFollowing,
95
- maxFollowers: exploreMaxFollowers,
96
- location: exploreLocation,
97
- browser,
98
- }, console.error);
99
-
100
- if (result.restricted) {
101
- await apiPost(`${serverUrl}/api/job/${username}`, { restricted: true, userInfo: result.userInfo || {} });
102
- break;
83
+
84
+ if (consecutiveNetworkErrors > 0) {
85
+ const waitTime = consecutiveNetworkErrors <= 2
86
+ ? 0
87
+ : consecutiveNetworkErrors <= 5
88
+ ? 30000
89
+ : 300000;
90
+ if (waitTime > 0) {
91
+ console.error(` [网络] 连续 ${consecutiveNetworkErrors} 次网络异常,等待 ${waitTime / 1000}s 后重试...`);
92
+ await new Promise(r => setTimeout(r, waitTime));
103
93
  }
94
+ }
104
95
 
105
- if (result.error && result.error.includes('代理错误')) {
106
- proxyRetry++;
107
- console.error(` [代理错误] ${result.error},等待 10s 后重试...`);
108
- await new Promise(r => setTimeout(r, 10000));
109
- continue;
96
+ console.error(`\n[${processedCount}] 探索 @${username}...`);
97
+
98
+ const { switchMax } = getDelayConfig();
99
+ await delay(switchMax, switchMax * 3);
100
+
101
+ const result = await processExplore(page, username, {
102
+ maxComments: exploreMaxComments,
103
+ maxGuess: exploreMaxGuess,
104
+ enableFollow: exploreEnableFollow,
105
+ maxFollowing: exploreMaxFollowing,
106
+ maxFollowers: exploreMaxFollowers,
107
+ location: exploreLocation,
108
+ browser,
109
+ }, console.error);
110
+
111
+ if (result.restricted) {
112
+ consecutiveNetworkErrors = 0;
113
+ await apiPost(`${serverUrl}/api/job/${username}`, { restricted: true, userInfo: result.userInfo || {} });
114
+ if (exploreMaxUsers > 0 && processedCount >= exploreMaxUsers) {
115
+ console.error(`\n已达上限 ${exploreMaxUsers} 个用户,停止处理`);
116
+ break;
110
117
  }
118
+ continue;
119
+ }
111
120
 
112
- if (result.error) {
113
- errorCount++;
114
- await apiPost(`${serverUrl}/api/job/${username}`, { error: result.error });
121
+ if (result.error) {
122
+ consecutiveNetworkErrors++;
123
+ errorCount++;
124
+ await apiPost(`${serverUrl}/api/job/${username}`, { error: result.error });
125
+ const errorType = consecutiveNetworkErrors > 1 ? 'network' : 'other';
126
+ await withRetry('report error', () =>
127
+ apiPost(`${serverUrl}/api/error-report`, {
128
+ userId,
129
+ username,
130
+ errorType,
131
+ errorMessage: result.error,
132
+ })
133
+ ).catch(() => {});
134
+ if (exploreMaxUsers > 0 && processedCount >= exploreMaxUsers) {
135
+ console.error(`\n已达上限 ${exploreMaxUsers} 个用户,停止处理`);
115
136
  break;
116
137
  }
138
+ continue;
139
+ }
117
140
 
118
- const payload = {
119
- userInfo: result.userInfo || {},
120
- discoveredVideoAuthors: result.discoveredVideoAuthors || [],
121
- discoveredCommentAuthors: result.discoveredCommentAuthors || [],
122
- discoveredGuessAuthors: result.discoveredGuessAuthors || [],
123
- discoveredFollowing: result.discoveredFollowing || [],
124
- discoveredFollowers: result.discoveredFollowers || [],
125
- processed: result.processed,
126
- hasFollowData: result.hasFollowData,
127
- keepFollow: result.keepFollow,
128
- locationCreated: result.locationCreated,
129
- noVideo: result.noVideo,
130
- };
131
- await apiPost(`${serverUrl}/api/job/${username}`, payload);
132
- console.error(' 已提交');
133
- break;
141
+ if (result.captchaDetected) {
142
+ await withRetry('report captcha', () =>
143
+ apiPost(`${serverUrl}/api/error-report`, {
144
+ userId,
145
+ username,
146
+ errorType: 'captcha',
147
+ errorMessage: '页面出现验证码',
148
+ })
149
+ ).catch(() => {});
134
150
  }
135
151
 
152
+ consecutiveNetworkErrors = 0;
153
+
154
+ const payload = {
155
+ userInfo: result.userInfo || {},
156
+ discoveredVideoAuthors: result.discoveredVideoAuthors || [],
157
+ discoveredCommentAuthors: result.discoveredCommentAuthors || [],
158
+ discoveredGuessAuthors: result.discoveredGuessAuthors || [],
159
+ discoveredFollowing: result.discoveredFollowing || [],
160
+ discoveredFollowers: result.discoveredFollowers || [],
161
+ processed: result.processed,
162
+ hasFollowData: result.hasFollowData,
163
+ keepFollow: result.keepFollow,
164
+ locationCreated: result.locationCreated,
165
+ noVideo: result.noVideo,
166
+ };
167
+ await apiPost(`${serverUrl}/api/job/${username}`, payload);
168
+ console.error(' 已提交');
169
+
136
170
  if (exploreMaxUsers > 0 && processedCount >= exploreMaxUsers) {
137
171
  console.error(`\n已达上限 ${exploreMaxUsers} 个用户,停止处理`);
138
172
  break;
@@ -71,8 +71,8 @@ const HELP_TEXT = [
71
71
  ' --max-users <数量> 最大处理用户数,默认无限制',
72
72
  ' 全局选项:',
73
73
  ' config 查看当前配置',
74
- ' config set <key> <value> 设置配置(key: proxy, server, browser)',
75
- ' config reset 重置代理为默认',
74
+ ' config set <key> <value> 设置配置(key: proxy, server, browser, userId)',
75
+ ' config reset 重置所有配置为默认',
76
76
  ' -h, --help 显示帮助',
77
77
  ' --version 显示版本号',
78
78
  '',
@@ -80,17 +80,27 @@ const HELP_TEXT = [
80
80
  ' tt-help config set server http://127.0.0.1:3001',
81
81
  ];
82
82
 
83
- const CONFIG_TEXT = [
84
- 'tt-help v1.0.1',
85
- '',
86
- '配置:',
87
- ` 代理: ${proxy}`,
88
- ` 服务端: ${server}`,
89
- ` 浏览器: ${browser || '未配置(将自动探测或回退)'}`,
90
- ` 输出格式: json`,
91
- ` 默认输出: ${DEFAULT_OUTPUT}`,
92
- ` 配置文件: ${configFile || '无(使用默认值)'}`,
93
- ];
83
+ function getConfigText() {
84
+ let currentUserId = userId;
85
+ if (!currentUserId && existsSync(configPath)) {
86
+ try {
87
+ const cfg = JSON.parse(readFileSync(configPath, 'utf-8'));
88
+ if (cfg.userId) currentUserId = cfg.userId;
89
+ } catch {}
90
+ }
91
+ return [
92
+ 'tt-help v1.0.1',
93
+ '',
94
+ '配置:',
95
+ ` 代理: ${proxy}`,
96
+ ` 服务端: ${server}`,
97
+ ` 浏览器: ${browser || '未配置(将自动探测或回退)'}`,
98
+ ` 用户号: ${currentUserId || '未设置(首次运行 auto 自动创建)'}`,
99
+ ` 输出格式: json`,
100
+ ` 默认输出: ${DEFAULT_OUTPUT}`,
101
+ ` 配置文件: ${configFile || '无(使用默认值)'}`,
102
+ ];
103
+ }
94
104
 
95
105
  export {
96
106
  proxy,
@@ -101,9 +111,9 @@ export {
101
111
  DEFAULT_OUTPUT,
102
112
  USER_SECTION_SIZE,
103
113
  HELP_TEXT,
104
- CONFIG_TEXT,
105
114
  browser,
106
115
  userId,
107
116
  saveBrowser,
108
117
  saveUserId,
118
+ getConfigText,
109
119
  };
package/src/lib/retry.js CHANGED
@@ -14,11 +14,12 @@ const RETRYABLE_PATTERNS = [
14
14
  'failed.*navigate',
15
15
  'target.*closed',
16
16
  'crash',
17
+ '代理错误',
17
18
  ];
18
19
 
19
20
  export function isRetryableError(error) {
20
21
  if (!error) return false;
21
- const msg = (error.message || error.toString() || '').toLowerCase();
22
+ const msg = error.message || error.toString() || '';
22
23
  return RETRYABLE_PATTERNS.some(p => new RegExp(p, 'i').test(msg));
23
24
  }
24
25
 
package/src/main.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { parseArgs } from './lib/args.js';
2
- import { HELP_TEXT, CONFIG_TEXT, proxy, configFile, configPath, DEFAULT_PROXY, saveBrowser } from './lib/constants.js';
2
+ import { HELP_TEXT, proxy, configFile, configPath, DEFAULT_PROXY, saveBrowser, saveUserId, getConfigText } from './lib/constants.js';
3
3
  import { parseFilter, applyFilter, formatFilterDescription } from './lib/filter.js';
4
4
  import { writeFileSync, readFileSync, existsSync } from 'fs';
5
5
  import { handleScrape } from './cli/scrape.js';
@@ -17,7 +17,7 @@ const pkgPath = join(__dirname, '..', 'package.json');
17
17
  const { version } = JSON.parse(readFileSync(pkgPath, 'utf-8'));
18
18
 
19
19
  function showConfig(urls, outputFile) {
20
- const lines = [...CONFIG_TEXT];
20
+ const lines = getConfigText();
21
21
  if (outputFile) lines.push(` 输出文件: ${outputFile}`);
22
22
  if (urls.length > 0) lines.push(` 待处理URL: ${urls.length}`);
23
23
  lines.push('', '参数:', ' -c, --config 显示当前配置', ' -h, --help 显示帮助');
@@ -37,7 +37,7 @@ function handleConfig(action, key, value) {
37
37
  if (action === 'set' || action === 'set-proxy') {
38
38
  if (!key) {
39
39
  console.error('用法: tt-help config set <key> <value>');
40
- console.error(' 可用 key: proxy, server, browser');
40
+ console.error(' 可用 key: proxy, server, browser, userId');
41
41
  process.exit(1);
42
42
  }
43
43
  if (!value && key.startsWith('http://')) {
@@ -57,9 +57,15 @@ function handleConfig(action, key, value) {
57
57
  if (key === 'proxy') cfg.proxy = value;
58
58
  else if (key === 'server') cfg.server = value;
59
59
  else if (key === 'browser') cfg.browser = value;
60
+ else if (key === 'userId') {
61
+ saveUserId(value);
62
+ console.log(`userId 已设置为: ${value}`);
63
+ console.log(`配置文件: ${configPath}`);
64
+ return;
65
+ }
60
66
  else {
61
67
  console.error(`未知配置项: ${key}`);
62
- console.error(' 可用 key: proxy, server, browser');
68
+ console.error(' 可用 key: proxy, server, browser, userId');
63
69
  process.exit(1);
64
70
  }
65
71
  writeFileSync(configPath, JSON.stringify(cfg, null, 2), 'utf-8');
@@ -90,16 +96,23 @@ function handleConfig(action, key, value) {
90
96
  if (existsSync(configPath)) {
91
97
  const cfg = JSON.parse(readFileSync(configPath, 'utf-8'));
92
98
  cfg.proxy = DEFAULT_PROXY;
99
+ cfg.server = 'http://127.0.0.1:3001';
100
+ delete cfg.browser;
101
+ delete cfg.userId;
93
102
  writeFileSync(configPath, JSON.stringify(cfg, null, 2), 'utf-8');
94
- console.log(`已恢复默认代理: ${DEFAULT_PROXY}`);
103
+ console.log('已重置所有配置:');
104
+ console.log(` 代理: ${DEFAULT_PROXY}`);
105
+ console.log(` 服务端: http://127.0.0.1:3001`);
106
+ console.log(' 浏览器: 已清空(自动探测)');
107
+ console.log(' 用户号: 已清空(下次运行 auto 自动创建)');
95
108
  console.log(`配置文件: ${configPath}`);
96
109
  } else {
97
- console.log('当前使用默认代理,无需重置');
110
+ console.log('当前使用默认配置,无需重置');
98
111
  }
99
112
  return;
100
113
  }
101
114
  console.error(`未知配置命令: ${action}`);
102
- console.error('用法: tt-help config [show|set|set-browser|reset]');
115
+ console.error('用法: tt-help config [show|set|reset]');
103
116
  process.exit(1);
104
117
  }
105
118
 
@@ -10,13 +10,14 @@ import {
10
10
  isLoggedIn,
11
11
  assertPageUrl,
12
12
  } from './modules/page-helpers.mjs';
13
+ import { detectCaptcha } from './modules/captcha-handler.mjs';
13
14
  export { ensureBrowserReady };
14
15
  import {
15
16
  getUserInfo,
16
17
  collectVideos,
17
18
  } from '../videos/core.mjs';
18
19
  import { runScrape } from './core.mjs';
19
- import { extractFollowAndFollowers } from './modules/follow-extractor.mjs';
20
+ import { extractFollowAndFollowers } from './modules/follow-extractor.mjs';
20
21
 
21
22
  function mergeUserInfo(existing, incoming, source) {
22
23
  const merged = { ...existing };
@@ -64,10 +65,12 @@ async function processUser(page, username, options, log) {
64
65
 
65
66
  try {
66
67
  log(`\n[processUser] 访问 @${username}...`);
67
- await retryWithBackoff(() => page.goto(`https://www.tiktok.com/@${username}`, {
68
- waitUntil: 'load', timeout: 30000,
69
- }), { log });
70
- assertPageUrl(page, `@${username}`);
68
+ await retryWithBackoff(async () => {
69
+ await page.goto(`https://www.tiktok.com/@${username}`, {
70
+ waitUntil: 'load', timeout: 30000,
71
+ });
72
+ assertPageUrl(page, `@${username}`);
73
+ }, { log });
71
74
  await page.waitForSelector('[class*="DivVideoList"]', { timeout: 10000 }).catch(() => {});
72
75
  await delay(1000, 2000);
73
76
 
@@ -101,6 +104,12 @@ async function processUser(page, username, options, log) {
101
104
  }
102
105
  }
103
106
 
107
+ const captcha = await detectCaptcha(page);
108
+ if (captcha && captcha.visible) {
109
+ log(`[验证码] @${username} 页面出现验证码`);
110
+ result.captchaDetected = true;
111
+ }
112
+
104
113
  const videos = await collectVideos(page, username, collectMax, log);
105
114
  const videoList = Array.from(videos.values()).slice(0, collectMax);
106
115
  result.collectedVideos = videoList.map(v => ({
@@ -8,11 +8,12 @@ import {
8
8
  isLoggedIn,
9
9
  assertPageUrl,
10
10
  } from './modules/page-helpers.mjs';
11
+ import { detectCaptcha } from './modules/captcha-handler.mjs';
11
12
  export { ensureBrowserReady };
12
13
  import {
13
14
  getUserInfo,
14
15
  collectVideos,
15
- } from '../videos/core.mjs';
16
+ } from '../videos/core.mjs';
16
17
  import { scrapeSingleVideo } from './core.mjs';
17
18
  import { extractFollowAndFollowers } from './modules/follow-extractor.mjs';
18
19
  import { extractCommentAuthors } from './modules/comment-extractor.mjs';
@@ -47,8 +48,10 @@ async function processExplore(page, username, options, log) {
47
48
  try {
48
49
  log(` 访问 @${username} 主页...`);
49
50
  const homeUrl = `https://www.tiktok.com/@${username}`;
50
- await retryWithBackoff(() => page.goto(homeUrl, { waitUntil: 'domcontentloaded', timeout: 30000 }), { log });
51
- assertPageUrl(page, `@${username}`);
51
+ await retryWithBackoff(async () => {
52
+ await page.goto(homeUrl, { waitUntil: 'domcontentloaded', timeout: 30000 });
53
+ assertPageUrl(page, `@${username}`);
54
+ }, { log });
52
55
  await page.waitForSelector('[class*="DivVideoList"]', { timeout: 10000 }).catch(() => {});
53
56
  await delay(1000, 2000);
54
57
 
@@ -59,6 +62,12 @@ async function processExplore(page, username, options, log) {
59
62
  log(` 用户: ${info.nickname || username} | 粉丝: ${info.followerCount || '-'} | 视频: ${info.videoCount || '-'}`);
60
63
  }
61
64
 
65
+ const captcha = await detectCaptcha(page);
66
+ if (captcha && captcha.visible) {
67
+ log(`[验证码] @${username} 页面出现验证码`);
68
+ result.captchaDetected = true;
69
+ }
70
+
62
71
  const videoList = await collectVideos(page, username, 1, log);
63
72
  const videoArray = videoList ? [...videoList.values()] : [];
64
73
  result.collectedVideos = videoArray.length;
@@ -10,6 +10,7 @@ function inferStatus(u) {
10
10
 
11
11
  export function createStore(filePath) {
12
12
  let data = [];
13
+ let clientErrors = new Map();
13
14
 
14
15
  let backupTimer = null;
15
16
 
@@ -248,10 +249,25 @@ export function createStore(filePath) {
248
249
  return { saved: true, pinned: user.pinned };
249
250
  }
250
251
 
252
+ function reportClientError(userId, errorType, errorMessage, username) {
253
+ clientErrors.set(userId, {
254
+ userId,
255
+ errorType,
256
+ errorMessage,
257
+ username,
258
+ timestamp: Date.now(),
259
+ });
260
+ }
261
+
262
+ function getClientErrors() {
263
+ return Array.from(clientErrors.values());
264
+ }
265
+
251
266
  return {
252
267
  save, getUser, hasUser, addUser,
253
268
  getPendingUsers, getProcessedUsers, getAllUsers,
254
269
  claimNextJob, commitJob, resetJob, togglePin,
270
+ reportClientError, getClientErrors,
255
271
  stopBackup,
256
272
  data,
257
273
  };
@@ -95,6 +95,19 @@
95
95
  ::-webkit-scrollbar { width: 6px; }
96
96
  ::-webkit-scrollbar-track { background: #1a1a24; }
97
97
  ::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }
98
+ .client-errors-section { margin-bottom: 16px; }
99
+ .section-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
100
+ .section-header h3 { font-size: 14px; color: #e0e0e0; }
101
+ .error-badge { background: #991b1b; color: #fff; font-size: 11px; padding: 2px 8px; border-radius: 10px; font-weight: 600; }
102
+ .client-errors-table { width: 100%; border-collapse: collapse; background: #1a1a24; border-radius: 8px; overflow: hidden; font-size: 13px; }
103
+ .client-errors-table thead tr { background: #22222e; }
104
+ .client-errors-table th { padding: 10px 12px; text-align: left; color: #888; font-weight: 600; font-size: 12px; border-bottom: 1px solid #2a2a3a; }
105
+ .client-errors-table td { padding: 8px 12px; border-bottom: 1px solid #1f1f2e; }
106
+ .client-errors-table tbody tr:last-child td { border-bottom: none; }
107
+ .client-errors-table tbody tr:hover { background: #22222e; }
108
+ .error-type-captcha { color: #f59e0b; }
109
+ .error-type-network { color: #f87171; }
110
+ .error-type-other { color: #a78bfa; }
98
111
  @media (max-width: 768px) {
99
112
  body { padding: 8px; }
100
113
  .header { flex-direction: column; gap: 6px; align-items: flex-start; }
@@ -108,6 +121,7 @@
108
121
  .controls { flex-wrap: wrap; gap: 6px; }
109
122
  .controls input { flex: 0 0 100%; width: 100%; }
110
123
  .controls button { flex: 0 0 calc(33.33% - 4px); min-width: 0; text-align: center; white-space: nowrap; font-size: 11px; padding: 8px 4px; }
124
+ #batchResetBtn { flex: 0 0 100% !important; font-size: 12px !important; padding: 8px 12px !important; }
111
125
  .controls select { flex: 0 0 100%; width: 100%; }
112
126
  .table-scroll { max-height: none; overflow: visible; }
113
127
  table, thead, tbody, th, td, tr { display: block; }
@@ -144,6 +158,23 @@
144
158
  <div class="stat-card"><div class="label">受限</div><div class="value error" id="statRestricted">0</div></div>
145
159
  <div class="stat-card clickable" id="statTargetCard"><div class="label">目标用户(ES商家)</div><div class="value target" id="statTarget">0</div></div>
146
160
  </div>
161
+ <div class="client-errors-section" id="clientErrorsSection" style="display:none">
162
+ <div class="section-header">
163
+ <h3>客户端异常</h3>
164
+ <span class="error-badge" id="clientErrorsBadge">0</span>
165
+ </div>
166
+ <table class="client-errors-table">
167
+ <thead>
168
+ <tr>
169
+ <th>客户端</th>
170
+ <th>错误类型</th>
171
+ <th>当时处理的 TikTok 用户</th>
172
+ <th>时间</th>
173
+ </tr>
174
+ </thead>
175
+ <tbody id="clientErrorsBody"></tbody>
176
+ </table>
177
+ </div>
147
178
  <div class="charts">
148
179
  <div class="chart-box">
149
180
  <h3>国家统计</h3>
@@ -169,6 +200,7 @@
169
200
  <button data-filter="error" onclick="setFilter('error')">错误</button>
170
201
  <button data-filter="restricted" onclick="setFilter('restricted')">受限</button>
171
202
  <button data-filter="target" onclick="setFilter('target')" style="background:#7c3aed;color:#fff">目标用户</button>
203
+ <button id="batchResetBtn" onclick="batchResetErrors()" style="display:none;padding:6px 10px;border:1px solid #f87171;border-radius:6px;background:transparent;color:#f87171;font-size:12px;cursor:pointer;font-weight:600;transition:all 0.2s;white-space:nowrap;">&#x21bb; 批量重新处理 (<span id="batchResetCount">0</span>)</button>
172
204
  <select id="locationFilter" onchange="onLocationChange()" style="padding:6px 10px;border:1px solid #333;border-radius:6px;background:#2a2a3a;color:#ccc;font-size:12px;cursor:pointer;outline:none;">
173
205
  <option value="">全部国家</option>
174
206
  </select>
@@ -221,6 +253,39 @@ async function fetchUsers() {
221
253
  } catch (e) {}
222
254
  }
223
255
 
256
+ function escapeHtml(str) {
257
+ const div = document.createElement('div');
258
+ div.textContent = str;
259
+ return div.innerHTML;
260
+ }
261
+
262
+ async function fetchClientErrors() {
263
+ try {
264
+ const res = await fetch('/api/client-errors');
265
+ const data = await res.json();
266
+ const clients = data.clients || [];
267
+ const section = document.getElementById('clientErrorsSection');
268
+ const badge = document.getElementById('clientErrorsBadge');
269
+ const tbody = document.getElementById('clientErrorsBody');
270
+ if (clients.length === 0) {
271
+ section.style.display = 'none';
272
+ return;
273
+ }
274
+ section.style.display = '';
275
+ badge.textContent = clients.length;
276
+ const typeMap = { captcha: ['验证码', 'error-type-captcha'], network: ['网络', 'error-type-network'], other: ['其他', 'error-type-other'] };
277
+ tbody.innerHTML = clients.map(c => {
278
+ const [typeText, typeClass] = typeMap[c.errorType] || ['未知', ''];
279
+ return `<tr>
280
+ <td style="font-family:monospace;font-weight:600;color:#60a5fa">${escapeHtml(c.userId)}</td>
281
+ <td class="${typeClass}">${typeText}</td>
282
+ <td style="color:#60a5fa">@${escapeHtml(c.username || '-')}</td>
283
+ <td style="color:#888;font-size:12px">${new Date(c.timestamp).toLocaleTimeString()}</td>
284
+ </tr>`;
285
+ }).join('');
286
+ } catch (e) {}
287
+ }
288
+
224
289
  function flashEl(id, value) {
225
290
  const el = document.getElementById(id);
226
291
  if (!el) return;
@@ -324,6 +389,10 @@ function renderTable(users) {
324
389
  </tr>`;
325
390
  }).join('');
326
391
 
392
+ const errorCount = users.filter(u => u.status === 'error').length;
393
+ const countEl = document.getElementById('batchResetCount');
394
+ if (countEl) countEl.textContent = errorCount;
395
+
327
396
  prevUserMap = newUserMap;
328
397
  }
329
398
 
@@ -344,6 +413,8 @@ function setFilter(f) {
344
413
  document.querySelectorAll('.controls button').forEach(b => {
345
414
  b.classList.toggle('active', b.dataset.filter === f);
346
415
  });
416
+ const btn = document.getElementById('batchResetBtn');
417
+ btn.style.display = f === 'error' ? '' : 'none';
347
418
  fetchUsers();
348
419
  }
349
420
 
@@ -548,6 +619,43 @@ async function resetJob(uniqueId) {
548
619
  }
549
620
  }
550
621
 
622
+ async function batchResetErrors() {
623
+ const btn = document.getElementById('batchResetBtn');
624
+ const errorUsers = currentUsers.filter(u => u.status === 'error');
625
+ if (errorUsers.length === 0) {
626
+ showToast('\u6ca1\u6709\u9700\u8981\u91cd\u7f6e\u7684\u9519\u8bef\u7528\u6237', true);
627
+ return;
628
+ }
629
+ const userIds = errorUsers.map(u => u.uniqueId);
630
+ const origText = btn.innerHTML;
631
+ btn.disabled = true;
632
+ btn.style.opacity = '0.6';
633
+ btn.style.cursor = 'not-allowed';
634
+ btn.innerHTML = '&#x21bb; 处理中...';
635
+ try {
636
+ const res = await fetch('/api/jobs/batch-reset', {
637
+ method: 'POST',
638
+ headers: { 'Content-Type': 'application/json' },
639
+ body: JSON.stringify({ userIds })
640
+ });
641
+ const data = await res.json();
642
+ if (data.error) {
643
+ showToast(data.error, true);
644
+ return;
645
+ }
646
+ showToast(`\u5df2\u91cd\u7f6e ${data.reset} / ${data.total} \u4e2a\u7528\u6237`);
647
+ fetchUsers();
648
+ fetchStats();
649
+ } catch (e) {
650
+ showToast('\u6279\u91cf\u91cd\u7f6e\u5931\u8d25: ' + e.message, true);
651
+ } finally {
652
+ btn.disabled = false;
653
+ btn.style.opacity = '1';
654
+ btn.style.cursor = 'pointer';
655
+ btn.innerHTML = origText;
656
+ }
657
+ }
658
+
551
659
  document.getElementById('statTargetCard').addEventListener('click', async () => {
552
660
  try {
553
661
  const res = await fetch('/api/target-users');
@@ -575,8 +683,10 @@ document.getElementById('statTargetCard').addEventListener('click', async () =>
575
683
 
576
684
  fetchStats();
577
685
  fetchUsers();
578
- setInterval(fetchStats, 1000);
579
- setInterval(fetchUsers, 2000);
686
+ fetchClientErrors();
687
+ setInterval(fetchStats, 10000);
688
+ setInterval(fetchUsers, 10000);
689
+ setInterval(fetchClientErrors, 10000);
580
690
  </script>
581
691
  </body>
582
692
  </html>
@@ -187,6 +187,22 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
187
187
  return;
188
188
  }
189
189
 
190
+ if (req.method === 'POST' && routePath === '/api/jobs/batch-reset') {
191
+ const body = await readBody(req);
192
+ const ids = Array.isArray(body.userIds) ? body.userIds : [];
193
+ if (ids.length === 0) {
194
+ sendJSON(res, 400, { error: 'userIds 不能为空' });
195
+ return;
196
+ }
197
+ let count = 0;
198
+ for (const uid of ids) {
199
+ const ret = store.resetJob(uid);
200
+ if (ret.saved) count++;
201
+ }
202
+ sendJSON(res, 200, { reset: count, total: ids.length });
203
+ return;
204
+ }
205
+
190
206
  const jobPinMatch = routePath.match(/^\/api\/job\/([^/]+)\/pin$/);
191
207
  if (req.method === 'POST' && jobPinMatch) {
192
208
  const uniqueId = jobPinMatch[1];
@@ -214,6 +230,27 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
214
230
  return;
215
231
  }
216
232
 
233
+ if (req.method === 'GET' && routePath === '/api/client-errors') {
234
+ sendJSON(res, 200, { clients: store.getClientErrors() });
235
+ return;
236
+ }
237
+
238
+ if (req.method === 'POST' && routePath === '/api/error-report') {
239
+ const body = await readBody(req);
240
+ if (body && body.userId) {
241
+ store.reportClientError(
242
+ body.userId,
243
+ body.errorType || 'other',
244
+ body.errorMessage || '',
245
+ body.username || ''
246
+ );
247
+ sendJSON(res, 200, { ok: true });
248
+ } else {
249
+ sendJSON(res, 400, { error: 'missing userId' });
250
+ }
251
+ return;
252
+ }
253
+
217
254
  if (req.method === 'GET' && routePath === '/api/users') {
218
255
  const all = store.getAllUsers();
219
256
  let filtered = [...all];