tt-help-cli-ycl 1.3.9 → 1.3.11

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.9",
3
+ "version": "1.3.11",
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; }
@@ -145,6 +158,23 @@
145
158
  <div class="stat-card"><div class="label">受限</div><div class="value error" id="statRestricted">0</div></div>
146
159
  <div class="stat-card clickable" id="statTargetCard"><div class="label">目标用户(ES商家)</div><div class="value target" id="statTarget">0</div></div>
147
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>
148
178
  <div class="charts">
149
179
  <div class="chart-box">
150
180
  <h3>国家统计</h3>
@@ -223,6 +253,39 @@ async function fetchUsers() {
223
253
  } catch (e) {}
224
254
  }
225
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
+
226
289
  function flashEl(id, value) {
227
290
  const el = document.getElementById(id);
228
291
  if (!el) return;
@@ -557,12 +620,18 @@ async function resetJob(uniqueId) {
557
620
  }
558
621
 
559
622
  async function batchResetErrors() {
623
+ const btn = document.getElementById('batchResetBtn');
560
624
  const errorUsers = currentUsers.filter(u => u.status === 'error');
561
625
  if (errorUsers.length === 0) {
562
626
  showToast('\u6ca1\u6709\u9700\u8981\u91cd\u7f6e\u7684\u9519\u8bef\u7528\u6237', true);
563
627
  return;
564
628
  }
565
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; 处理中...';
566
635
  try {
567
636
  const res = await fetch('/api/jobs/batch-reset', {
568
637
  method: 'POST',
@@ -579,6 +648,11 @@ async function batchResetErrors() {
579
648
  fetchStats();
580
649
  } catch (e) {
581
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;
582
656
  }
583
657
  }
584
658
 
@@ -609,8 +683,10 @@ document.getElementById('statTargetCard').addEventListener('click', async () =>
609
683
 
610
684
  fetchStats();
611
685
  fetchUsers();
686
+ fetchClientErrors();
612
687
  setInterval(fetchStats, 10000);
613
688
  setInterval(fetchUsers, 10000);
689
+ setInterval(fetchClientErrors, 10000);
614
690
  </script>
615
691
  </body>
616
692
  </html>
@@ -230,6 +230,27 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
230
230
  return;
231
231
  }
232
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
+
233
254
  if (req.method === 'GET' && routePath === '/api/users') {
234
255
  const all = store.getAllUsers();
235
256
  let filtered = [...all];
@@ -286,9 +307,13 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
286
307
  const scriptFile = join(batDir, scriptMatch[1]);
287
308
  if (existsSync(scriptFile)) {
288
309
  const content = readFileSync(scriptFile);
289
- const ext = scriptMatch[1].split('.').pop();
310
+ const fileName = scriptMatch[1];
311
+ const ext = fileName.split('.').pop();
290
312
  const mime = ext === 'sh' ? 'text/x-sh' : ext === 'bat' ? 'text/bat' : 'text/plain';
291
- res.writeHead(200, { 'Content-Type': `${mime}; charset=utf-8` });
313
+ res.writeHead(200, {
314
+ 'Content-Type': `${mime}; charset=utf-8`,
315
+ 'Content-Disposition': `attachment; filename="${fileName}"`,
316
+ });
292
317
  res.end(content);
293
318
  return;
294
319
  }