tt-help-cli-ycl 1.3.31 → 1.3.33
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/package.json +1 -1
- package/scripts/run-explore copy.bat +35 -2
- package/scripts/run-explore.bat +49 -18
- package/scripts/run-explore.ps1 +51 -5
- package/scripts/run-explore.sh +20 -5
- package/src/cli/explore.js +6 -3
- package/src/lib/api-interceptor.js +110 -37
- package/src/lib/args.js +4 -0
- package/src/scraper/modules/follow-extractor.js +82 -6
- package/src/watch/data-store.js +340 -165
- package/src/watch/server.js +400 -201
package/package.json
CHANGED
|
@@ -1,8 +1,40 @@
|
|
|
1
1
|
@ECHO OFF
|
|
2
|
-
SETLOCAL
|
|
2
|
+
SETLOCAL EnableDelayedExpansion
|
|
3
3
|
|
|
4
4
|
SET "PACKAGENAME=tt-help-cli-ycl"
|
|
5
5
|
SET "TARGET_SERVER=http://117.71.53.99:17301"
|
|
6
|
+
SET "LOCAL_IP="
|
|
7
|
+
SET "GET_IP_PS1=%TEMP%\tt_get_local_ip.ps1"
|
|
8
|
+
>"%GET_IP_PS1%" ECHO $ip = $null
|
|
9
|
+
>>"%GET_IP_PS1%" ECHO try {
|
|
10
|
+
>>"%GET_IP_PS1%" ECHO $ip = Get-WmiObject Win32_NetworkAdapterConfiguration ^| Where-Object { $_.IPEnabled -eq $true } ^| ForEach-Object { $_.IPAddress } ^| Where-Object { $_ -match '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' -and $_ -ne '127.0.0.1' -and $_ -notlike '169.254.*' } ^| Select-Object -First 1
|
|
11
|
+
>>"%GET_IP_PS1%" ECHO } catch {}
|
|
12
|
+
>>"%GET_IP_PS1%" ECHO if (-not $ip) {
|
|
13
|
+
>>"%GET_IP_PS1%" ECHO try {
|
|
14
|
+
>>"%GET_IP_PS1%" ECHO $ip = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction Stop ^| Where-Object { $_.IPAddress -ne '127.0.0.1' -and $_.IPAddress -notlike '169.254.*' } ^| Select-Object -First 1 -ExpandProperty IPAddress
|
|
15
|
+
>>"%GET_IP_PS1%" ECHO } catch {}
|
|
16
|
+
>>"%GET_IP_PS1%" ECHO }
|
|
17
|
+
>>"%GET_IP_PS1%" ECHO if (-not $ip) {
|
|
18
|
+
>>"%GET_IP_PS1%" ECHO try {
|
|
19
|
+
>>"%GET_IP_PS1%" ECHO $ip = [System.Net.Dns]::GetHostAddresses([System.Net.Dns]::GetHostName()) ^| Where-Object { $_.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork -and $_.IPAddressToString -ne '127.0.0.1' -and $_.IPAddressToString -notlike '169.254.*' } ^| Select-Object -First 1 -ExpandProperty IPAddressToString
|
|
20
|
+
>>"%GET_IP_PS1%" ECHO } catch {}
|
|
21
|
+
>>"%GET_IP_PS1%" ECHO }
|
|
22
|
+
>>"%GET_IP_PS1%" ECHO if ($ip) { [Console]::Write($ip) }
|
|
23
|
+
FOR /F "usebackq delims=" %%I IN (`powershell -NoProfile -ExecutionPolicy Bypass -File "%GET_IP_PS1%"`) DO (
|
|
24
|
+
SET "LOCAL_IP=%%I"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
ECHO [INFO] Local IP: %LOCAL_IP%
|
|
28
|
+
|
|
29
|
+
IF DEFINED LOCAL_IP IF "%LOCAL_IP:~0,11%"=="172.18.154." SET "TARGET_SERVER=http://172.18.154.201:3001"
|
|
30
|
+
|
|
31
|
+
IF NOT DEFINED LOCAL_IP (
|
|
32
|
+
ECHO [INFO] No local IPv4 detected, using public server
|
|
33
|
+
) ELSE IF "%LOCAL_IP:~0,11%"=="172.18.154." (
|
|
34
|
+
ECHO [INFO] Intranet IP detected, using intranet server
|
|
35
|
+
) ELSE (
|
|
36
|
+
ECHO [INFO] No intranet IP detected, using public server
|
|
37
|
+
)
|
|
6
38
|
SET "CONFIG_PATH=%USERPROFILE%\.tt-help.json"
|
|
7
39
|
|
|
8
40
|
ECHO ========================================
|
|
@@ -44,7 +76,7 @@ IF NOT DEFINED INSTALLED_VERSION (
|
|
|
44
76
|
REM ---------- 2. Check/set server config ----------
|
|
45
77
|
SET "CURRENT_SERVER="
|
|
46
78
|
IF EXIST "%CONFIG_PATH%" (
|
|
47
|
-
FOR /F "delims=" %%S IN (
|
|
79
|
+
FOR /F "usebackq delims=" %%S IN (`powershell -NoProfile -Command "$p=$env:CONFIG_PATH; if (Test-Path $p) { try { $cfg = Get-Content $p -Raw | ConvertFrom-Json; if ($null -ne $cfg.server) { [Console]::Write($cfg.server) } } catch {} }"`) DO SET "CURRENT_SERVER=%%S"
|
|
48
80
|
)
|
|
49
81
|
|
|
50
82
|
IF "%CURRENT_SERVER%"=="%TARGET_SERVER%" (
|
|
@@ -66,3 +98,4 @@ ECHO ========================================
|
|
|
66
98
|
ECHO Starting tt-help explore
|
|
67
99
|
ECHO ========================================
|
|
68
100
|
tt-help explore --port 9223 --profile p9223
|
|
101
|
+
DEL "%GET_IP_PS1%" 2>NUL
|
package/scripts/run-explore.bat
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
@ECHO OFF
|
|
2
|
-
SETLOCAL
|
|
2
|
+
SETLOCAL EnableDelayedExpansion
|
|
3
3
|
|
|
4
4
|
SET "PACKAGENAME=tt-help-cli-ycl"
|
|
5
5
|
SET "CONFIG_PATH=%USERPROFILE%\.tt-help.json"
|
|
@@ -8,18 +8,41 @@ ECHO ========================================
|
|
|
8
8
|
ECHO tt-help-cli-ycl one-click launcher (Windows CMD)
|
|
9
9
|
ECHO ========================================
|
|
10
10
|
|
|
11
|
-
REM ---------- 0.
|
|
12
|
-
|
|
11
|
+
REM ---------- 0. Auto-select server based on local IP ----------
|
|
12
|
+
SET "TARGET_SERVER=http://117.71.53.99:17301"
|
|
13
|
+
SET "LOCAL_IP="
|
|
14
|
+
SET "GET_IP_PS1=%TEMP%\tt_get_local_ip.ps1"
|
|
15
|
+
>"%GET_IP_PS1%" ECHO $ip = $null
|
|
16
|
+
>>"%GET_IP_PS1%" ECHO try {
|
|
17
|
+
>>"%GET_IP_PS1%" ECHO $ip = Get-WmiObject Win32_NetworkAdapterConfiguration ^| Where-Object { $_.IPEnabled -eq $true } ^| ForEach-Object { $_.IPAddress } ^| Where-Object { $_ -match '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' -and $_ -ne '127.0.0.1' -and $_ -notlike '169.254.*' } ^| Select-Object -First 1
|
|
18
|
+
>>"%GET_IP_PS1%" ECHO } catch {}
|
|
19
|
+
>>"%GET_IP_PS1%" ECHO if (-not $ip) {
|
|
20
|
+
>>"%GET_IP_PS1%" ECHO try {
|
|
21
|
+
>>"%GET_IP_PS1%" ECHO $ip = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction Stop ^| Where-Object { $_.IPAddress -ne '127.0.0.1' -and $_.IPAddress -notlike '169.254.*' } ^| Select-Object -First 1 -ExpandProperty IPAddress
|
|
22
|
+
>>"%GET_IP_PS1%" ECHO } catch {}
|
|
23
|
+
>>"%GET_IP_PS1%" ECHO }
|
|
24
|
+
>>"%GET_IP_PS1%" ECHO if (-not $ip) {
|
|
25
|
+
>>"%GET_IP_PS1%" ECHO try {
|
|
26
|
+
>>"%GET_IP_PS1%" ECHO $ip = [System.Net.Dns]::GetHostAddresses([System.Net.Dns]::GetHostName()) ^| Where-Object { $_.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork -and $_.IPAddressToString -ne '127.0.0.1' -and $_.IPAddressToString -notlike '169.254.*' } ^| Select-Object -First 1 -ExpandProperty IPAddressToString
|
|
27
|
+
>>"%GET_IP_PS1%" ECHO } catch {}
|
|
28
|
+
>>"%GET_IP_PS1%" ECHO }
|
|
29
|
+
>>"%GET_IP_PS1%" ECHO if ($ip) { [Console]::Write($ip) }
|
|
30
|
+
FOR /F "usebackq delims=" %%I IN (`powershell -NoProfile -ExecutionPolicy Bypass -File "%GET_IP_PS1%"`) DO (
|
|
13
31
|
SET "LOCAL_IP=%%I"
|
|
14
|
-
SET "TARGET_SERVER=http://172.18.154.201:3001"
|
|
15
32
|
)
|
|
16
33
|
|
|
34
|
+
ECHO [INFO] Local IP: %LOCAL_IP%
|
|
35
|
+
|
|
36
|
+
IF DEFINED LOCAL_IP IF "%LOCAL_IP:~0,11%"=="172.18.154." SET "TARGET_SERVER=http://172.18.154.201:3001"
|
|
37
|
+
|
|
17
38
|
IF NOT DEFINED LOCAL_IP (
|
|
18
|
-
|
|
19
|
-
|
|
39
|
+
ECHO [INFO] No local IPv4 detected, using public server
|
|
40
|
+
) ELSE IF "%LOCAL_IP:~0,11%"=="172.18.154." (
|
|
41
|
+
ECHO [INFO] Intranet IP detected, using intranet server
|
|
20
42
|
) ELSE (
|
|
21
|
-
ECHO [INFO]
|
|
43
|
+
ECHO [INFO] No intranet IP detected, using public server
|
|
22
44
|
)
|
|
45
|
+
DEL "%GET_IP_PS1%" 2>NUL
|
|
23
46
|
|
|
24
47
|
REM ---------- 1. Check/install latest version ----------
|
|
25
48
|
FOR /F "delims=" %%V IN ('npm view %PACKAGENAME% version 2^>nul') DO SET "LATEST_VERSION=%%V"
|
|
@@ -56,7 +79,7 @@ IF NOT DEFINED INSTALLED_VERSION (
|
|
|
56
79
|
REM ---------- 2. Check/set server config ----------
|
|
57
80
|
SET "CURRENT_SERVER="
|
|
58
81
|
IF EXIST "%CONFIG_PATH%" (
|
|
59
|
-
FOR /F "delims=" %%S IN (
|
|
82
|
+
FOR /F "usebackq delims=" %%S IN (`powershell -NoProfile -Command "$p=$env:CONFIG_PATH; if (Test-Path $p) { try { $cfg = Get-Content $p -Raw | ConvertFrom-Json; if ($null -ne $cfg.server) { [Console]::Write($cfg.server) } } catch {} }"`) DO SET "CURRENT_SERVER=%%S"
|
|
60
83
|
)
|
|
61
84
|
|
|
62
85
|
IF "%CURRENT_SERVER%"=="%TARGET_SERVER%" (
|
|
@@ -72,25 +95,33 @@ IF "%CURRENT_SERVER%"=="%TARGET_SERVER%" (
|
|
|
72
95
|
ECHO [OK] Server config set
|
|
73
96
|
)
|
|
74
97
|
|
|
75
|
-
REM ---------- 3.
|
|
98
|
+
REM ---------- 3. Ask user for parameters ----------
|
|
76
99
|
ECHO.
|
|
77
100
|
ECHO ========================================
|
|
78
|
-
ECHO
|
|
101
|
+
ECHO Parameter Configuration
|
|
79
102
|
ECHO ========================================
|
|
80
103
|
|
|
81
|
-
SET /P "BASE_PORT
|
|
104
|
+
SET /P "BASE_PORT=Base port (--base-port) [default: 9222]: "
|
|
82
105
|
IF "%BASE_PORT%"=="" SET "BASE_PORT=9222"
|
|
83
106
|
|
|
84
|
-
SET /P "PORT_COUNT
|
|
107
|
+
SET /P "PORT_COUNT=Port count (--port-count) [default: 10]: "
|
|
85
108
|
IF "%PORT_COUNT%"=="" SET "PORT_COUNT=10"
|
|
86
109
|
|
|
87
|
-
REM ---------- 4.
|
|
110
|
+
REM ---------- 4. Get user ID from config and compose ----------
|
|
111
|
+
SET "CONFIG_USER_ID=user"
|
|
112
|
+
IF EXIST "%CONFIG_PATH%" (
|
|
113
|
+
FOR /F "usebackq delims=" %%S IN (`powershell -NoProfile -Command "$p=$env:CONFIG_PATH; if (Test-Path $p) { try { $cfg = Get-Content $p -Raw | ConvertFrom-Json; if ($null -ne $cfg.userId) { [Console]::Write($cfg.userId) } } catch {} }"`) DO SET "CONFIG_USER_ID=%%S"
|
|
114
|
+
)
|
|
115
|
+
CALL SET "USER_ID=%%CONFIG_USER_ID%%_%%BASE_PORT%%_%%PORT_COUNT%%"
|
|
116
|
+
|
|
117
|
+
REM ---------- 5. Start tt-help explore ----------
|
|
88
118
|
ECHO.
|
|
89
119
|
ECHO ========================================
|
|
90
|
-
ECHO
|
|
120
|
+
ECHO Starting tt-help explore
|
|
91
121
|
ECHO ========================================
|
|
92
|
-
ECHO
|
|
93
|
-
ECHO
|
|
94
|
-
ECHO
|
|
122
|
+
CALL ECHO User ID: %%USER_ID%%
|
|
123
|
+
CALL ECHO Base port: %%BASE_PORT%%
|
|
124
|
+
CALL ECHO Port count: %%PORT_COUNT%%
|
|
125
|
+
ECHO Speed: stealth (slowest)
|
|
95
126
|
ECHO ========================================
|
|
96
|
-
tt-help explore stealth --base-port
|
|
127
|
+
CALL tt-help explore stealth --user-id %%USER_ID%% --base-port %%BASE_PORT%% --port-count %%PORT_COUNT%%
|
package/scripts/run-explore.ps1
CHANGED
|
@@ -6,8 +6,41 @@ Write-Host " tt-help-cli-ycl 一键启动脚本 (Windows)"
|
|
|
6
6
|
Write-Host "========================================"
|
|
7
7
|
|
|
8
8
|
# ---------- 0. 根据本机 IP 自动选择 server ----------
|
|
9
|
-
$localIp =
|
|
10
|
-
|
|
9
|
+
$localIp = $null
|
|
10
|
+
try {
|
|
11
|
+
$localIp = Get-WmiObject Win32_NetworkAdapterConfiguration |
|
|
12
|
+
Where-Object { $_.IPEnabled -eq $true } |
|
|
13
|
+
ForEach-Object { $_.IPAddress } |
|
|
14
|
+
Where-Object {
|
|
15
|
+
$_ -match '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' -and
|
|
16
|
+
$_ -ne '127.0.0.1' -and
|
|
17
|
+
$_ -notlike '169.254.*'
|
|
18
|
+
} |
|
|
19
|
+
Select-Object -First 1
|
|
20
|
+
} catch {
|
|
21
|
+
}
|
|
22
|
+
if (-not $localIp) {
|
|
23
|
+
try {
|
|
24
|
+
$localIp = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction Stop |
|
|
25
|
+
Where-Object { $_.IPAddress -ne '127.0.0.1' -and $_.IPAddress -notlike '169.254.*' } |
|
|
26
|
+
Select-Object -First 1 -ExpandProperty IPAddress
|
|
27
|
+
} catch {
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (-not $localIp) {
|
|
31
|
+
try {
|
|
32
|
+
$localIp = [System.Net.Dns]::GetHostAddresses([System.Net.Dns]::GetHostName()) |
|
|
33
|
+
Where-Object {
|
|
34
|
+
$_.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork -and
|
|
35
|
+
$_.IPAddressToString -ne '127.0.0.1' -and
|
|
36
|
+
$_.IPAddressToString -notlike '169.254.*'
|
|
37
|
+
} |
|
|
38
|
+
Select-Object -First 1 -ExpandProperty IPAddressToString
|
|
39
|
+
} catch {
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
Write-Host "[提示] 本机 IP: $(if ($localIp) { $localIp } else { '未检测到' })"
|
|
43
|
+
if ($localIp -and $localIp -match '^172\.18\.154\.') {
|
|
11
44
|
$targetServer = "http://172.18.154.201:3001"
|
|
12
45
|
Write-Host "[提示] 检测到内网 IP: $localIp,使用内网 server"
|
|
13
46
|
} else {
|
|
@@ -94,14 +127,27 @@ $basePort = if ($inputBasePort) { $inputBasePort } else { "9222" }
|
|
|
94
127
|
$inputPortCount = Read-Host "端口数量 (--port-count) [默认: 10]"
|
|
95
128
|
$portCount = if ($inputPortCount) { $inputPortCount } else { "10" }
|
|
96
129
|
|
|
97
|
-
# ---------- 4.
|
|
130
|
+
# ---------- 4. 获取配置中的用户编号并拼接 ----------
|
|
131
|
+
$configUserId = ""
|
|
132
|
+
if (Test-Path $configPath) {
|
|
133
|
+
try {
|
|
134
|
+
$config = Get-Content $configPath -Raw | ConvertFrom-Json
|
|
135
|
+
$configUserId = $config.userId
|
|
136
|
+
} catch {
|
|
137
|
+
$configUserId = ""
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (-not $configUserId) { $configUserId = "user" }
|
|
141
|
+
$userId = "${configUserId}_${basePort}_${portCount}"
|
|
142
|
+
|
|
143
|
+
# ---------- 5. 启动 tt-help explore ----------
|
|
98
144
|
Write-Host ""
|
|
99
145
|
Write-Host "========================================"
|
|
100
146
|
Write-Host " 启动 tt-help explore"
|
|
101
147
|
Write-Host "========================================"
|
|
148
|
+
Write-Host " 用户编号: $userId"
|
|
102
149
|
Write-Host " 起始端口: $basePort"
|
|
103
150
|
Write-Host " 端口数量: $portCount"
|
|
104
151
|
Write-Host " 速度: stealth (最慢)"
|
|
105
152
|
Write-Host "========================================"
|
|
106
|
-
tt-help explore stealth --base-port $basePort --port-count $portCount
|
|
107
|
-
tt-help explore
|
|
153
|
+
tt-help explore stealth --user-id $userId --base-port $basePort --port-count $portCount
|
package/scripts/run-explore.sh
CHANGED
|
@@ -8,9 +8,11 @@ echo " tt-help-cli-ycl 一键启动脚本 (macOS)"
|
|
|
8
8
|
echo "========================================"
|
|
9
9
|
|
|
10
10
|
# ---------- 0. 根据本机 IP 自动选择 server ----------
|
|
11
|
-
LOCAL_IP=$(ifconfig |
|
|
11
|
+
LOCAL_IP=$(ifconfig | awk '/inet / && $2 != "127.0.0.1" { print $2; exit }')
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
echo "[提示] 本机 IP: ${LOCAL_IP:-未检测到}"
|
|
14
|
+
|
|
15
|
+
if [[ "$LOCAL_IP" =~ ^172\.18\.154\.[0-9]+$ ]]; then
|
|
14
16
|
TARGET_SERVER="http://172.18.154.201:3001"
|
|
15
17
|
echo "[提示] 检测到内网 IP: $LOCAL_IP,使用内网 server"
|
|
16
18
|
else
|
|
@@ -81,18 +83,31 @@ echo "========================================"
|
|
|
81
83
|
echo " 参数配置"
|
|
82
84
|
echo "========================================"
|
|
83
85
|
|
|
84
|
-
|
|
86
|
+
printf "起始端口 (--base-port) [默认: 9222]: "
|
|
87
|
+
read INPUT_BASE_PORT
|
|
85
88
|
BASE_PORT="${INPUT_BASE_PORT:-9222}"
|
|
86
89
|
|
|
87
|
-
|
|
90
|
+
printf "端口数量 (--port-count) [默认: 10]: "
|
|
91
|
+
read INPUT_PORT_COUNT
|
|
88
92
|
PORT_COUNT="${INPUT_PORT_COUNT:-10}"
|
|
89
93
|
|
|
94
|
+
# ---------- 4. 获取配置中的用户编号并拼接 ----------
|
|
95
|
+
CONFIG_USER_ID=""
|
|
96
|
+
if [ -f "$CONFIG_PATH" ]; then
|
|
97
|
+
CONFIG_USER_ID=$(node -e "try{const c=JSON.parse(require('fs').readFileSync('$CONFIG_PATH','utf-8'));console.log(c.userId||'')}catch(e){}" 2>/dev/null)
|
|
98
|
+
fi
|
|
99
|
+
if [ -z "$CONFIG_USER_ID" ]; then
|
|
100
|
+
CONFIG_USER_ID="user"
|
|
101
|
+
fi
|
|
102
|
+
USER_ID="${CONFIG_USER_ID}_${BASE_PORT}_${PORT_COUNT}"
|
|
103
|
+
|
|
90
104
|
echo ""
|
|
91
105
|
echo "========================================"
|
|
92
106
|
echo " 启动 tt-help explore"
|
|
93
107
|
echo "========================================"
|
|
108
|
+
echo " 用户编号: $USER_ID"
|
|
94
109
|
echo " 起始端口: $BASE_PORT"
|
|
95
110
|
echo " 端口数量: $PORT_COUNT"
|
|
96
111
|
echo " 速度: stealth (最慢)"
|
|
97
112
|
echo "========================================"
|
|
98
|
-
tt-help explore stealth --base-port "$BASE_PORT" --port-count "$PORT_COUNT"
|
|
113
|
+
tt-help explore stealth --user-id "$USER_ID" --base-port "$BASE_PORT" --port-count "$PORT_COUNT"
|
package/src/cli/explore.js
CHANGED
|
@@ -68,6 +68,7 @@ export async function handleExplore(options) {
|
|
|
68
68
|
exploreMaxFollowing,
|
|
69
69
|
exploreMaxFollowers,
|
|
70
70
|
exploreLocation,
|
|
71
|
+
exploreJobLocations,
|
|
71
72
|
exploreMaxUsers,
|
|
72
73
|
serverUrl,
|
|
73
74
|
explorePort,
|
|
@@ -96,6 +97,7 @@ export async function handleExplore(options) {
|
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
console.error(`\n国家筛选: ${exploreLocation}`);
|
|
100
|
+
if (exploreJobLocations) console.error(`任务国家: ${exploreJobLocations}`);
|
|
99
101
|
console.error(`视频采集: ${exploreMaxVideos || 1}`);
|
|
100
102
|
console.error(`关注/粉丝: ${exploreEnableFollow ? "启用" : "禁用"}`);
|
|
101
103
|
console.error(`服务器: ${serverUrl}(断开会自动重连)`);
|
|
@@ -206,9 +208,10 @@ export async function handleExplore(options) {
|
|
|
206
208
|
const FOLLOW_BLOCK_THRESHOLD = 10 * 60 * 1000; // 10分钟
|
|
207
209
|
|
|
208
210
|
while (true) {
|
|
209
|
-
const
|
|
210
|
-
`${serverUrl}/api/job?userId=${encodeURIComponent(userId)}
|
|
211
|
-
|
|
211
|
+
const jobQuery = exploreJobLocations
|
|
212
|
+
? `${serverUrl}/api/job?userId=${encodeURIComponent(userId)}&locations=${encodeURIComponent(exploreJobLocations)}`
|
|
213
|
+
: `${serverUrl}/api/job?userId=${encodeURIComponent(userId)}`;
|
|
214
|
+
const job = await apiGet(jobQuery);
|
|
212
215
|
if (!job.hasJob) break;
|
|
213
216
|
|
|
214
217
|
const username = job.user.uniqueId;
|
|
@@ -78,16 +78,21 @@ async function processAPIResponse(
|
|
|
78
78
|
async function tryInterceptAPI(page, username, log, apiTimeout = 15000) {
|
|
79
79
|
const url = `https://www.tiktok.com/@${username}`;
|
|
80
80
|
let apiRequestUrl = null;
|
|
81
|
+
let sawApiRequest = false;
|
|
82
|
+
let sawApiResponse = false;
|
|
81
83
|
|
|
82
84
|
const responseHandler = async (response) => {
|
|
83
85
|
if (response.url().includes("/api/post/item_list/")) {
|
|
84
|
-
|
|
86
|
+
sawApiResponse = true;
|
|
85
87
|
}
|
|
86
88
|
};
|
|
87
89
|
|
|
88
90
|
const requestHandler = (request) => {
|
|
89
91
|
if (request.url().includes("/api/post/item_list/") && !apiRequestUrl) {
|
|
92
|
+
sawApiRequest = true;
|
|
90
93
|
apiRequestUrl = request.url();
|
|
94
|
+
} else if (request.url().includes("/api/post/item_list/")) {
|
|
95
|
+
sawApiRequest = true;
|
|
91
96
|
}
|
|
92
97
|
};
|
|
93
98
|
|
|
@@ -106,29 +111,88 @@ async function tryInterceptAPI(page, username, log, apiTimeout = 15000) {
|
|
|
106
111
|
{ timeout: apiTimeout },
|
|
107
112
|
);
|
|
108
113
|
|
|
114
|
+
sawApiResponse = true;
|
|
109
115
|
const data = await response.json();
|
|
110
116
|
const elapsed = Date.now() - t0;
|
|
111
|
-
return { data, apiRequestUrl, elapsed };
|
|
117
|
+
return { data, apiRequestUrl, elapsed, sawApiRequest, sawApiResponse };
|
|
112
118
|
} catch (e) {
|
|
113
119
|
const elapsed = Date.now() - t0;
|
|
114
|
-
return {
|
|
120
|
+
return {
|
|
121
|
+
data: null,
|
|
122
|
+
apiRequestUrl,
|
|
123
|
+
elapsed,
|
|
124
|
+
error: e.message,
|
|
125
|
+
sawApiRequest,
|
|
126
|
+
sawApiResponse,
|
|
127
|
+
};
|
|
115
128
|
} finally {
|
|
116
129
|
page.off("response", responseHandler);
|
|
117
130
|
page.off("request", requestHandler);
|
|
118
131
|
}
|
|
119
132
|
}
|
|
120
133
|
|
|
134
|
+
/**
|
|
135
|
+
* 软重试:不重新 goto,直接在当前页继续等待 item_list API。
|
|
136
|
+
*/
|
|
137
|
+
async function tryWaitCurrentPageAPI(page, apiTimeout = 12000) {
|
|
138
|
+
let apiRequestUrl = null;
|
|
139
|
+
let sawApiRequest = false;
|
|
140
|
+
let sawApiResponse = false;
|
|
141
|
+
|
|
142
|
+
const requestHandler = (request) => {
|
|
143
|
+
if (request.url().includes("/api/post/item_list/") && !apiRequestUrl) {
|
|
144
|
+
sawApiRequest = true;
|
|
145
|
+
apiRequestUrl = request.url();
|
|
146
|
+
} else if (request.url().includes("/api/post/item_list/")) {
|
|
147
|
+
sawApiRequest = true;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const responseHandler = (response) => {
|
|
152
|
+
if (response.url().includes("/api/post/item_list/")) {
|
|
153
|
+
sawApiResponse = true;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const t0 = Date.now();
|
|
158
|
+
page.on("request", requestHandler);
|
|
159
|
+
page.on("response", responseHandler);
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const response = await page.waitForResponse(
|
|
163
|
+
(res) => res.url().includes("/api/post/item_list/"),
|
|
164
|
+
{ timeout: apiTimeout },
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
sawApiResponse = true;
|
|
168
|
+
const data = await response.json();
|
|
169
|
+
const elapsed = Date.now() - t0;
|
|
170
|
+
return { data, apiRequestUrl, elapsed, sawApiRequest, sawApiResponse };
|
|
171
|
+
} catch (e) {
|
|
172
|
+
const elapsed = Date.now() - t0;
|
|
173
|
+
return {
|
|
174
|
+
data: null,
|
|
175
|
+
apiRequestUrl,
|
|
176
|
+
elapsed,
|
|
177
|
+
error: e.message,
|
|
178
|
+
sawApiRequest,
|
|
179
|
+
sawApiResponse,
|
|
180
|
+
};
|
|
181
|
+
} finally {
|
|
182
|
+
page.off("request", requestHandler);
|
|
183
|
+
page.off("response", responseHandler);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
121
187
|
/**
|
|
122
188
|
* 通过拦截 TikTok 内部 API 获取用户视频列表
|
|
123
189
|
* 比 DOM 滚动解析快 5-10 倍
|
|
124
190
|
*
|
|
125
191
|
* 弱网优化:
|
|
192
|
+
* - 第 1 轮:正常 goto,等待 15s
|
|
193
|
+
* - 第 1.5 轮:不重新 goto,留在当前页继续等待 12s
|
|
194
|
+
* - 第 2 轮:重新 goto,等待 30s
|
|
126
195
|
* - 使用 waitForResponse 精确等待 API 响应(而非固定超时)
|
|
127
|
-
* - 最多重试 3 次,递增等待 + 递增超时:
|
|
128
|
-
* - 第 1 次:等待 0s, 超时 15s → 正常尝试
|
|
129
|
-
* - 第 2 次:等待 3s, 超时 20s → 短暂波动恢复
|
|
130
|
-
* - 第 3 次:等待 10s, 超时 30s → 网络缓慢给更多时间
|
|
131
|
-
* - 每次重试前清理拦截器
|
|
132
196
|
*
|
|
133
197
|
* @param {import('playwright').Page} page - Playwright page (CDP 连接)
|
|
134
198
|
* @param {string} username - TikTok 用户名
|
|
@@ -139,13 +203,6 @@ async function tryInterceptAPI(page, username, log, apiTimeout = 15000) {
|
|
|
139
203
|
*/
|
|
140
204
|
async function fetchUserVideosAPI(page, username, maxVideos, log) {
|
|
141
205
|
const items = [];
|
|
142
|
-
const maxRetries = 3;
|
|
143
|
-
// 重试策略:[等待时间(ms), API超时(ms)]
|
|
144
|
-
const retryStrategy = [
|
|
145
|
-
[0, 15000], // 第 1 次:不等待,15s 超时
|
|
146
|
-
[3000, 20000], // 第 2 次:等 3s,20s 超时(短暂波动)
|
|
147
|
-
[10000, 30000], // 第 3 次:等 10s,30s 超时(网络缓慢)
|
|
148
|
-
];
|
|
149
206
|
const noRetryErrors = new Set([
|
|
150
207
|
"service_error",
|
|
151
208
|
"not_found",
|
|
@@ -153,22 +210,39 @@ async function fetchUserVideosAPI(page, username, maxVideos, log) {
|
|
|
153
210
|
"login_required",
|
|
154
211
|
]);
|
|
155
212
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
213
|
+
const attempts = [
|
|
214
|
+
{
|
|
215
|
+
label: ` [API拦截] 获取 @${username} 视频 ...`,
|
|
216
|
+
mode: "goto",
|
|
217
|
+
apiTimeout: 15000,
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
label: " [API拦截] 首轮未命中,当前页继续补等 12s ...",
|
|
221
|
+
mode: "wait",
|
|
222
|
+
apiTimeout: 12000,
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
label: " [API拦截] 重新加载主页后重试 (超时 30s)",
|
|
226
|
+
mode: "goto",
|
|
227
|
+
apiTimeout: 30000,
|
|
228
|
+
},
|
|
229
|
+
];
|
|
165
230
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
231
|
+
for (let attempt = 0; attempt < attempts.length; attempt++) {
|
|
232
|
+
const currentAttempt = attempts[attempt];
|
|
233
|
+
log(currentAttempt.label);
|
|
234
|
+
|
|
235
|
+
const {
|
|
236
|
+
data,
|
|
237
|
+
apiRequestUrl,
|
|
238
|
+
elapsed,
|
|
239
|
+
error,
|
|
240
|
+
sawApiRequest,
|
|
241
|
+
sawApiResponse,
|
|
242
|
+
} =
|
|
243
|
+
currentAttempt.mode === "goto"
|
|
244
|
+
? await tryInterceptAPI(page, username, log, currentAttempt.apiTimeout)
|
|
245
|
+
: await tryWaitCurrentPageAPI(page, currentAttempt.apiTimeout);
|
|
172
246
|
|
|
173
247
|
if (data && Array.isArray(data.itemList) && data.itemList.length > 0) {
|
|
174
248
|
// 成功拦截且拿到了视频,处理数据
|
|
@@ -208,20 +282,19 @@ async function fetchUserVideosAPI(page, username, maxVideos, log) {
|
|
|
208
282
|
return new Map();
|
|
209
283
|
}
|
|
210
284
|
|
|
285
|
+
log(
|
|
286
|
+
` [API拦截] 未命中: request=${sawApiRequest ? "Y" : "N"}, response=${sawApiResponse ? "Y" : "N"}, elapsed=${elapsed}ms${error ? `, error=${error}` : ""}${apiRequestUrl ? ", apiUrl=Y" : ", apiUrl=N"}`,
|
|
287
|
+
);
|
|
288
|
+
|
|
211
289
|
// 最后一次重试也失败了
|
|
212
|
-
if (attempt ===
|
|
290
|
+
if (attempt === attempts.length - 1) {
|
|
213
291
|
if (!pageError) {
|
|
214
292
|
throw new Error(
|
|
215
|
-
`@${username}
|
|
293
|
+
`@${username} 页面异常(首轮 + 补等 + 重载后仍未拦截到 API)`,
|
|
216
294
|
);
|
|
217
295
|
}
|
|
218
296
|
return new Map();
|
|
219
297
|
}
|
|
220
|
-
|
|
221
|
-
// 还有重试次数,等待后重试
|
|
222
|
-
const [waitTime] = retryStrategy[attempt - 1];
|
|
223
|
-
log(` [API拦截] 失败,${waitTime / 1000}s 后重试...`);
|
|
224
|
-
await new Promise((r) => setTimeout(r, waitTime));
|
|
225
298
|
}
|
|
226
299
|
}
|
|
227
300
|
|
package/src/lib/args.js
CHANGED
|
@@ -171,6 +171,7 @@ function parseExploreArgs(args) {
|
|
|
171
171
|
let exploreMaxFollowing = 50;
|
|
172
172
|
let exploreMaxFollowers = 50;
|
|
173
173
|
let exploreLocation = "PL,NL,BE,DE,FR,IT,ES,IE";
|
|
174
|
+
let exploreJobLocations = null;
|
|
174
175
|
let exploreMaxUsers = 0;
|
|
175
176
|
let explorePort = null;
|
|
176
177
|
let exploreBasePort = null;
|
|
@@ -191,6 +192,8 @@ function parseExploreArgs(args) {
|
|
|
191
192
|
exploreMaxGuess = parseInt(args[++i]) || 0;
|
|
192
193
|
} else if (arg === "--location") {
|
|
193
194
|
exploreLocation = args[++i];
|
|
195
|
+
} else if (arg === "--job-locations") {
|
|
196
|
+
exploreJobLocations = args[++i];
|
|
194
197
|
} else if (arg === "--enable-follow") {
|
|
195
198
|
exploreEnableFollow = true;
|
|
196
199
|
} else if (arg === "--disable-follow") {
|
|
@@ -242,6 +245,7 @@ function parseExploreArgs(args) {
|
|
|
242
245
|
exploreMaxFollowing,
|
|
243
246
|
exploreMaxFollowers,
|
|
244
247
|
exploreLocation,
|
|
248
|
+
exploreJobLocations,
|
|
245
249
|
serverUrl,
|
|
246
250
|
exploreMaxUsers,
|
|
247
251
|
explorePort,
|