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.
- package/bat/run-explore.bat +68 -0
- package/bat/run-explore.ps1 +81 -0
- package/bat/run-explore.sh +73 -0
- package/package.json +3 -2
- package/src/cli/auto.js +56 -30
- package/src/cli/explore.js +79 -45
- package/src/lib/constants.js +24 -14
- package/src/lib/retry.js +2 -1
- package/src/main.mjs +20 -7
- package/src/scraper/auto-core.mjs +14 -5
- package/src/scraper/explore-core.mjs +12 -3
- package/src/watch/data-store.mjs +16 -0
- package/src/watch/public/index.html +112 -2
- package/src/watch/server.mjs +37 -0
|
@@ -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.
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
101
|
+
console.error(`\n[${processedCount}] 处理 @${username}...`);
|
|
93
102
|
|
|
94
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
+
if (result.restricted) {
|
|
106
|
+
consecutiveNetworkErrors = 0;
|
|
107
|
+
await apiPost(`${serverUrl}/api/job/${username}`, result);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
105
110
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
+
if (result.error) {
|
|
112
|
+
consecutiveNetworkErrors++;
|
|
113
|
+
errorCount++;
|
|
114
|
+
await apiPost(`${serverUrl}/api/job/${username}`, result);
|
|
115
|
+
const errorType = consecutiveNetworkErrors > 1 ? 'network' : 'other';
|
|
116
|
+
await withRetry('report error', () =>
|
|
117
|
+
apiPost(`${serverUrl}/api/error-report`, {
|
|
118
|
+
userId,
|
|
119
|
+
username,
|
|
120
|
+
errorType,
|
|
121
|
+
errorMessage: result.error,
|
|
122
|
+
})
|
|
123
|
+
).catch(() => {});
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
111
126
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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`);
|
package/src/cli/explore.js
CHANGED
|
@@ -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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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;
|
package/src/lib/constants.js
CHANGED
|
@@ -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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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 =
|
|
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,
|
|
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 =
|
|
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(
|
|
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|
|
|
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(() =>
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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(() =>
|
|
51
|
-
|
|
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;
|
package/src/watch/data-store.mjs
CHANGED
|
@@ -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;">↻ 批量重新处理 (<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 = '↻ 处理中...';
|
|
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
|
-
|
|
579
|
-
setInterval(
|
|
686
|
+
fetchClientErrors();
|
|
687
|
+
setInterval(fetchStats, 10000);
|
|
688
|
+
setInterval(fetchUsers, 10000);
|
|
689
|
+
setInterval(fetchClientErrors, 10000);
|
|
580
690
|
</script>
|
|
581
691
|
</body>
|
|
582
692
|
</html>
|
package/src/watch/server.mjs
CHANGED
|
@@ -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];
|