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.
- 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 +76 -0
- package/src/watch/server.mjs +27 -2
|
@@ -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.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
|
-
|
|
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; }
|
|
@@ -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 = '↻ 处理中...';
|
|
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>
|
package/src/watch/server.mjs
CHANGED
|
@@ -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
|
|
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, {
|
|
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
|
}
|