tt-help-cli-ycl 1.3.41 → 1.3.44
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.bat +3 -1
- package/scripts/run-explore.ps1 +3 -1
- package/scripts/run-explore.sh +3 -1
- package/src/cli/comments.js +14 -22
- package/src/cli/explore.js +5 -4
- package/src/lib/args.js +3 -2
- package/src/lib/browser/page.js +8 -7
- package/src/lib/constants.js +3 -2
- package/src/lib/target-locations.js +61 -0
- package/src/scraper/core.js +8 -9
- package/src/scraper/explore-core.js +32 -14
- package/src/scraper/modules/follow-extractor.js +35 -6
- package/src/scraper/refresh-core.js +2 -1
- package/src/watch/data-store.js +458 -2
- package/src/watch/public/index.html +878 -100
- package/src/watch/server.js +88 -20
package/package.json
CHANGED
package/scripts/run-explore.bat
CHANGED
|
@@ -127,6 +127,8 @@ CALL ECHO User ID: %%USER_ID%%
|
|
|
127
127
|
CALL ECHO Base port: %%BASE_PORT%%
|
|
128
128
|
CALL ECHO Port count: %%PORT_COUNT%%
|
|
129
129
|
IF NOT "%JOB_LOCATIONS%"=="" ECHO Job locations: %JOB_LOCATIONS%
|
|
130
|
+
ECHO Max following: 5
|
|
131
|
+
ECHO Max followers: 5
|
|
130
132
|
ECHO Speed: stealth (slowest)
|
|
131
133
|
ECHO ========================================
|
|
132
|
-
CALL tt-help explore stealth --user-id %%USER_ID%% --base-port %%BASE_PORT%% --port-count %%PORT_COUNT%% %%JOB_LOC_ARGS%%
|
|
134
|
+
CALL tt-help explore stealth --user-id %%USER_ID%% --base-port %%BASE_PORT%% --port-count %%PORT_COUNT%% --max-following 5 --max-followers 5 %%JOB_LOC_ARGS%%
|
package/scripts/run-explore.ps1
CHANGED
|
@@ -152,6 +152,8 @@ Write-Host " 用户编号: $userId"
|
|
|
152
152
|
Write-Host " 起始端口: $basePort"
|
|
153
153
|
Write-Host " 端口数量: $portCount"
|
|
154
154
|
if ($inputJobLocations) { Write-Host " 任务国家: $inputJobLocations" }
|
|
155
|
+
Write-Host " 最大关注: 5"
|
|
156
|
+
Write-Host " 最大粉丝: 5"
|
|
155
157
|
Write-Host " 速度: stealth (最慢)"
|
|
156
158
|
Write-Host "========================================"
|
|
157
|
-
tt-help explore stealth --user-id $userId --base-port $basePort --port-count $portCount $jobLocationsArg
|
|
159
|
+
tt-help explore stealth --user-id $userId --base-port $basePort --port-count $portCount --max-following 5 --max-followers 5 $jobLocationsArg
|
package/scripts/run-explore.sh
CHANGED
|
@@ -112,8 +112,10 @@ echo " 用户编号: $USER_ID"
|
|
|
112
112
|
echo " 起始端口: $BASE_PORT"
|
|
113
113
|
echo " 端口数量: $PORT_COUNT"
|
|
114
114
|
if [ -n "$INPUT_JOB_LOCATIONS" ]; then echo " 任务国家: $INPUT_JOB_LOCATIONS"; fi
|
|
115
|
+
echo " 最大关注: 5"
|
|
116
|
+
echo " 最大粉丝: 5"
|
|
115
117
|
echo " 速度: stealth (最慢)"
|
|
116
118
|
echo "========================================"
|
|
117
119
|
JOB_LOC_ARGS=""
|
|
118
120
|
if [ -n "$INPUT_JOB_LOCATIONS" ]; then JOB_LOC_ARGS="--job-locations $INPUT_JOB_LOCATIONS"; fi
|
|
119
|
-
tt-help explore stealth --user-id "$USER_ID" --base-port "$BASE_PORT" --port-count "$PORT_COUNT" $JOB_LOC_ARGS
|
|
121
|
+
tt-help explore stealth --user-id "$USER_ID" --base-port "$BASE_PORT" --port-count "$PORT_COUNT" --max-following 5 --max-followers 5 $JOB_LOC_ARGS
|
package/src/cli/comments.js
CHANGED
|
@@ -2,22 +2,11 @@ import { chromium } from "playwright";
|
|
|
2
2
|
import { fetchUserCommentsAPI } from "../lib/api-interceptor-comment.js";
|
|
3
3
|
import { closeCommentPanel } from "../lib/browser/page.js";
|
|
4
4
|
import { server as defaultServer } from "../lib/constants.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"PT",
|
|
11
|
-
"ES",
|
|
12
|
-
"PL",
|
|
13
|
-
"NL",
|
|
14
|
-
"BE",
|
|
15
|
-
"DE",
|
|
16
|
-
"FR",
|
|
17
|
-
"IT",
|
|
18
|
-
"IE",
|
|
19
|
-
"AT",
|
|
20
|
-
];
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_TARGET_LOCATIONS,
|
|
7
|
+
isLocationInList,
|
|
8
|
+
normalizeLocation,
|
|
9
|
+
} from "../lib/target-locations.js";
|
|
21
10
|
|
|
22
11
|
async function waitForPageReady(page, timeout = 30000) {
|
|
23
12
|
const startTime = Date.now();
|
|
@@ -130,7 +119,7 @@ async function runAutoMode(options) {
|
|
|
130
119
|
`\n[Comments Auto] 并行: ${actualParallel}, 间隔: ${actualInterval}s, 评论数: ${actualMaxComments}`,
|
|
131
120
|
);
|
|
132
121
|
console.error(`服务器: ${serverUrl}`);
|
|
133
|
-
console.error(`目标国家: ${
|
|
122
|
+
console.error(`目标国家: ${DEFAULT_TARGET_LOCATIONS.join(", ")}`);
|
|
134
123
|
console.error(`开始循环接收任务...\n`);
|
|
135
124
|
|
|
136
125
|
let browser;
|
|
@@ -228,10 +217,10 @@ async function runAutoMode(options) {
|
|
|
228
217
|
for (const task of tasks) {
|
|
229
218
|
try {
|
|
230
219
|
const { id, href, locationCreated, ttSeller } = task;
|
|
231
|
-
const loc = (locationCreated
|
|
220
|
+
const loc = normalizeLocation(locationCreated);
|
|
232
221
|
|
|
233
222
|
// 检查目标国家
|
|
234
|
-
if (loc && !
|
|
223
|
+
if (loc && !isLocationInList(loc, DEFAULT_TARGET_LOCATIONS)) {
|
|
235
224
|
// 非目标国家,直接 commit 跳过
|
|
236
225
|
await apiPut(`${serverUrl}/api/comment-task/${id}`);
|
|
237
226
|
skippedCount++;
|
|
@@ -487,12 +476,15 @@ export async function handleComments(options) {
|
|
|
487
476
|
),
|
|
488
477
|
];
|
|
489
478
|
|
|
490
|
-
const guessedLocation = (videoInfo?.locationCreated
|
|
479
|
+
const guessedLocation = normalizeLocation(videoInfo?.locationCreated);
|
|
491
480
|
const serverUrl = commentsServer || defaultServer;
|
|
492
481
|
|
|
493
|
-
if (
|
|
482
|
+
if (
|
|
483
|
+
guessedLocation &&
|
|
484
|
+
!isLocationInList(guessedLocation, DEFAULT_TARGET_LOCATIONS)
|
|
485
|
+
) {
|
|
494
486
|
console.error(`\n猜测国家: ${guessedLocation},非目标国家,跳过保存`);
|
|
495
|
-
console.error(`目标国家: ${
|
|
487
|
+
console.error(`目标国家: ${DEFAULT_TARGET_LOCATIONS.join(", ")}`);
|
|
496
488
|
} else {
|
|
497
489
|
console.error(`\n正在提交 ${authors.length} 个评论作者到服务端...`);
|
|
498
490
|
console.error(`服务端: ${serverUrl}`);
|
package/src/cli/explore.js
CHANGED
|
@@ -24,6 +24,7 @@ import path from "path";
|
|
|
24
24
|
import os from "os";
|
|
25
25
|
|
|
26
26
|
const MAX_RETRY_WAIT = 5 * 60 * 1000;
|
|
27
|
+
const STARTUP_TIKTOK_URL = "https://www.tiktok.com/@ycl5007";
|
|
27
28
|
|
|
28
29
|
async function withRetry(label, fn) {
|
|
29
30
|
let backoff = 1000;
|
|
@@ -164,8 +165,8 @@ export async function handleExplore(options) {
|
|
|
164
165
|
|
|
165
166
|
const page = await getOrCreatePage(browser);
|
|
166
167
|
|
|
167
|
-
// 先导航到 TikTok
|
|
168
|
-
await page.goto(
|
|
168
|
+
// 先导航到 TikTok 页面,再检测登录状态
|
|
169
|
+
await page.goto(STARTUP_TIKTOK_URL, {
|
|
169
170
|
waitUntil: "domcontentloaded",
|
|
170
171
|
});
|
|
171
172
|
|
|
@@ -214,8 +215,8 @@ export async function handleExplore(options) {
|
|
|
214
215
|
userDataDir: nextAccount.userDataDir,
|
|
215
216
|
});
|
|
216
217
|
console.error(`[健康检查] 已切换到端口 ${nextAccount.port}`);
|
|
217
|
-
// 切换账户后先导航到 TikTok
|
|
218
|
-
await page.goto(
|
|
218
|
+
// 切换账户后先导航到 TikTok 页面,再重新检测登录状态
|
|
219
|
+
await page.goto(STARTUP_TIKTOK_URL, {
|
|
219
220
|
waitUntil: "domcontentloaded",
|
|
220
221
|
});
|
|
221
222
|
loggedIn = await isLoggedIn(page);
|
package/src/lib/args.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from "fs";
|
|
2
2
|
import { server as defaultServer } from "./constants.js";
|
|
3
|
+
import { DEFAULT_TARGET_LOCATIONS_CSV } from "./target-locations.js";
|
|
3
4
|
|
|
4
5
|
const PRESETS = ["fast", "normal", "slow", "stealth"];
|
|
5
6
|
|
|
@@ -171,7 +172,7 @@ function parseExploreArgs(args) {
|
|
|
171
172
|
let exploreEnableFollow = true;
|
|
172
173
|
let exploreMaxFollowing = 50;
|
|
173
174
|
let exploreMaxFollowers = 50;
|
|
174
|
-
let exploreLocation =
|
|
175
|
+
let exploreLocation = DEFAULT_TARGET_LOCATIONS_CSV;
|
|
175
176
|
let exploreJobLocations = null;
|
|
176
177
|
let exploreMaxUsers = 0;
|
|
177
178
|
let explorePort = null;
|
|
@@ -203,7 +204,7 @@ function parseExploreArgs(args) {
|
|
|
203
204
|
exploreEnableFollow = false;
|
|
204
205
|
} else if (arg === "--max-following") {
|
|
205
206
|
exploreMaxFollowing = parseInt(args[++i]) || 50;
|
|
206
|
-
|
|
207
|
+
} else if (arg === "--max-followers") {
|
|
207
208
|
exploreMaxFollowers = parseInt(args[++i]) || 50;
|
|
208
209
|
} else if (arg === "--max-users") {
|
|
209
210
|
exploreMaxUsers = parseInt(args[++i]) || 0;
|
package/src/lib/browser/page.js
CHANGED
|
@@ -67,19 +67,20 @@ export async function withBrowserRecovery(fn, browser, page, cdpOptions, port) {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* 未登录:不存在 sessionid Cookie
|
|
70
|
+
* 稳定判断登录状态:先检查 sessionid Cookie,再用页面 DOM 做验真。
|
|
71
|
+
* 仅有 sessionid 不足以说明会话仍然有效,因此需要二次确认。
|
|
73
72
|
*/
|
|
74
73
|
export async function isLoggedIn(page) {
|
|
75
74
|
const cookies = await page.context().cookies("https://www.tiktok.com");
|
|
76
|
-
|
|
75
|
+
const hasSessionId = cookies.some((c) => c.name === "sessionid");
|
|
76
|
+
if (!hasSessionId) return false;
|
|
77
|
+
|
|
78
|
+
return await isLoggedInByDom(page);
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
/**
|
|
80
|
-
* 通过 DOM
|
|
81
|
-
*
|
|
82
|
-
* @deprecated 推荐使用 isLoggedIn(基于 Cookie)
|
|
82
|
+
* 通过 DOM 元素判断登录状态(验真方案)
|
|
83
|
+
* 依赖页面渲染完成,因此不单独作为主判断,而是和 Cookie 组合使用。
|
|
83
84
|
*/
|
|
84
85
|
export async function isLoggedInByDom(page) {
|
|
85
86
|
// 先等客户端渲染完成:登录态元素或登录按钮,哪个先出现就停止等待
|
package/src/lib/constants.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join, dirname } from "path";
|
|
2
2
|
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
|
+
import { DEFAULT_TARGET_LOCATIONS_CSV } from "./target-locations.js";
|
|
4
5
|
|
|
5
6
|
const __filename = fileURLToPath(import.meta.url);
|
|
6
7
|
const __dirname = dirname(__filename);
|
|
@@ -121,14 +122,14 @@ const HELP_TEXT = [
|
|
|
121
122
|
" 预设: fast, normal(默认), slow, stealth",
|
|
122
123
|
" 选项:",
|
|
123
124
|
" --server <URL> 服务端地址,默认 http://127.0.0.1:3001",
|
|
124
|
-
|
|
125
|
+
` --location <国家代码> 国家筛选,逗号分隔,默认 ${DEFAULT_TARGET_LOCATIONS_CSV}`,
|
|
125
126
|
" --job-locations <国家> 任务国家筛选,逗号分隔(仅筛选服务端任务)",
|
|
126
127
|
" --max-comments <数量> 每视频最大评论数,默认 10",
|
|
127
128
|
" --max-guess <数量> 每视频最大猜你喜欢数,默认 0",
|
|
128
129
|
" --max-videos <数量> 每用户最大视频数,默认 16",
|
|
129
130
|
" --enable-follow 启用关注/粉丝提取(默认启用)",
|
|
130
131
|
" --disable-follow 禁用关注/粉丝提取",
|
|
131
|
-
" --max-following <数量> 最大获取关注数,默认 50
|
|
132
|
+
" --max-following <数量> 最大获取关注数,默认 50",
|
|
132
133
|
" --max-followers <数量> 最大获取粉丝数,默认 50",
|
|
133
134
|
" --max-users <数量> 最大处理用户数,默认无限制",
|
|
134
135
|
" -i, --interval <秒数> 无任务时轮询间隔,默认 10 秒",
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const DEFAULT_TARGET_LOCATIONS = [
|
|
2
|
+
"CZ",
|
|
3
|
+
"GR",
|
|
4
|
+
"HU",
|
|
5
|
+
"PT",
|
|
6
|
+
"ES",
|
|
7
|
+
"PL",
|
|
8
|
+
"NL",
|
|
9
|
+
"BE",
|
|
10
|
+
"DE",
|
|
11
|
+
"FR",
|
|
12
|
+
"IT",
|
|
13
|
+
"IE",
|
|
14
|
+
"AT",
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const DEFAULT_TARGET_LOCATIONS_CSV = DEFAULT_TARGET_LOCATIONS.join(",");
|
|
18
|
+
|
|
19
|
+
function normalizeLocation(location) {
|
|
20
|
+
if (location == null) return null;
|
|
21
|
+
const normalized = String(location).trim().toUpperCase();
|
|
22
|
+
return normalized || null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizeLocationList(locations, fallback = DEFAULT_TARGET_LOCATIONS) {
|
|
26
|
+
const source = Array.isArray(locations)
|
|
27
|
+
? locations
|
|
28
|
+
: String(locations == null ? "" : locations).split(",");
|
|
29
|
+
const normalized = source
|
|
30
|
+
.map((location) => normalizeLocation(location))
|
|
31
|
+
.filter(Boolean);
|
|
32
|
+
return normalized.length > 0 ? [...new Set(normalized)] : [...fallback];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isLocationInList(location, locations) {
|
|
36
|
+
const normalizedLocation = normalizeLocation(location);
|
|
37
|
+
if (!normalizedLocation) return false;
|
|
38
|
+
return normalizeLocationList(locations).includes(normalizedLocation);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function findFirstMatchingLocation(
|
|
42
|
+
locations,
|
|
43
|
+
targetLocations = DEFAULT_TARGET_LOCATIONS,
|
|
44
|
+
) {
|
|
45
|
+
const normalizedTargetLocations = normalizeLocationList(targetLocations);
|
|
46
|
+
const normalizedLocations = normalizeLocationList(locations, []);
|
|
47
|
+
return (
|
|
48
|
+
normalizedLocations.find((location) =>
|
|
49
|
+
normalizedTargetLocations.includes(location),
|
|
50
|
+
) || null
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export {
|
|
55
|
+
DEFAULT_TARGET_LOCATIONS,
|
|
56
|
+
DEFAULT_TARGET_LOCATIONS_CSV,
|
|
57
|
+
findFirstMatchingLocation,
|
|
58
|
+
isLocationInList,
|
|
59
|
+
normalizeLocation,
|
|
60
|
+
normalizeLocationList,
|
|
61
|
+
};
|
package/src/scraper/core.js
CHANGED
|
@@ -10,13 +10,18 @@ import {
|
|
|
10
10
|
} from "./modules/page-helpers.js";
|
|
11
11
|
import { extractCommentAuthors } from "./modules/comment-extractor.js";
|
|
12
12
|
import { extractGuessVideos } from "./modules/guess-extractor.js";
|
|
13
|
+
import {
|
|
14
|
+
DEFAULT_TARGET_LOCATIONS_CSV,
|
|
15
|
+
isLocationInList,
|
|
16
|
+
normalizeLocationList,
|
|
17
|
+
} from "../lib/target-locations.js";
|
|
13
18
|
|
|
14
19
|
async function scrapeSingleVideo(
|
|
15
20
|
page,
|
|
16
21
|
maxComments,
|
|
17
22
|
maxGuess,
|
|
18
23
|
log,
|
|
19
|
-
location =
|
|
24
|
+
location = DEFAULT_TARGET_LOCATIONS_CSV,
|
|
20
25
|
) {
|
|
21
26
|
const config = getDelayConfig();
|
|
22
27
|
|
|
@@ -52,14 +57,8 @@ async function scrapeSingleVideo(
|
|
|
52
57
|
let captchaStage = "";
|
|
53
58
|
let captchaMessage = "";
|
|
54
59
|
|
|
55
|
-
const locationList = (location
|
|
56
|
-
|
|
57
|
-
.map((s) => s.trim().toUpperCase());
|
|
58
|
-
if (
|
|
59
|
-
locationList.includes(
|
|
60
|
-
userData.locationCreated?.toUpperCase?.() || userData.locationCreated,
|
|
61
|
-
)
|
|
62
|
-
) {
|
|
60
|
+
const locationList = normalizeLocationList(location);
|
|
61
|
+
if (isLocationInList(userData.locationCreated, locationList)) {
|
|
63
62
|
if (maxGuess > 0) {
|
|
64
63
|
guessVideos = await extractGuessVideos(page, maxGuess);
|
|
65
64
|
}
|
|
@@ -4,6 +4,12 @@ export { ensureBrowserReady };
|
|
|
4
4
|
import { getUserInfo, collectVideos } from "../videos/core.js";
|
|
5
5
|
import { extractFollowAndFollowers } from "./modules/follow-extractor.js";
|
|
6
6
|
import { extractVideoLocation } from "../lib/scrape.js";
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_TARGET_LOCATIONS_CSV,
|
|
9
|
+
findFirstMatchingLocation,
|
|
10
|
+
isLocationInList,
|
|
11
|
+
normalizeLocationList,
|
|
12
|
+
} from "../lib/target-locations.js";
|
|
7
13
|
import {
|
|
8
14
|
maxFollowing as globalMaxFollowing,
|
|
9
15
|
maxFollowers as globalMaxFollowers,
|
|
@@ -16,7 +22,7 @@ async function processExplore(page, username, options, log) {
|
|
|
16
22
|
loggedIn = false, // 由外部传入登录状态,避免每次调用 isLoggedIn(page)
|
|
17
23
|
maxFollowing = 50,
|
|
18
24
|
maxFollowers = 50,
|
|
19
|
-
location =
|
|
25
|
+
location = DEFAULT_TARGET_LOCATIONS_CSV,
|
|
20
26
|
} = options;
|
|
21
27
|
|
|
22
28
|
const result = {
|
|
@@ -35,6 +41,8 @@ async function processExplore(page, username, options, log) {
|
|
|
35
41
|
error: null,
|
|
36
42
|
};
|
|
37
43
|
|
|
44
|
+
const locationList = normalizeLocationList(location);
|
|
45
|
+
|
|
38
46
|
try {
|
|
39
47
|
log(` 访问 @${username} 主页...`);
|
|
40
48
|
const videoList = await collectVideos(page, username, maxVideos, log);
|
|
@@ -66,9 +74,11 @@ async function processExplore(page, username, options, log) {
|
|
|
66
74
|
return result;
|
|
67
75
|
}
|
|
68
76
|
|
|
69
|
-
// 从最多 5 个视频并发获取 locationCreated
|
|
77
|
+
// 从最多 5 个视频并发获取 locationCreated。
|
|
78
|
+
// 新规则:采样列表里只要有任一国家命中目标列表,就直接采用该国家;否则回退到众数。
|
|
70
79
|
const SAMPLE_SIZE = 5;
|
|
71
80
|
let locationCreated = null;
|
|
81
|
+
let locationDecision = null;
|
|
72
82
|
const sampleVideos = videoArray.slice(0, SAMPLE_SIZE);
|
|
73
83
|
if (sampleVideos.length > 0) {
|
|
74
84
|
const sampleUrls = sampleVideos.map((v) =>
|
|
@@ -80,26 +90,34 @@ async function processExplore(page, username, options, log) {
|
|
|
80
90
|
log(
|
|
81
91
|
` 国家采样(${locations.length}个): [${locations.filter(Boolean).join(", ") || "无数据"}]`,
|
|
82
92
|
);
|
|
93
|
+
const normalizedLocations = normalizeLocationList(locations, []);
|
|
94
|
+
const matchedTargetLocation = findFirstMatchingLocation(
|
|
95
|
+
normalizedLocations,
|
|
96
|
+
locationList,
|
|
97
|
+
);
|
|
83
98
|
const freq = {};
|
|
84
|
-
for (const
|
|
85
|
-
|
|
86
|
-
const key = loc.toUpperCase();
|
|
87
|
-
freq[key] = (freq[key] || 0) + 1;
|
|
88
|
-
}
|
|
99
|
+
for (const key of normalizedLocations) {
|
|
100
|
+
freq[key] = (freq[key] || 0) + 1;
|
|
89
101
|
}
|
|
90
102
|
const entries = Object.entries(freq).sort((a, b) => b[1] - a[1]);
|
|
91
|
-
|
|
103
|
+
if (matchedTargetLocation) {
|
|
104
|
+
locationCreated = matchedTargetLocation;
|
|
105
|
+
locationDecision = "命中目标国家";
|
|
106
|
+
} else if (entries.length > 0) {
|
|
107
|
+
locationCreated = entries[0][0];
|
|
108
|
+
locationDecision = "回退众数";
|
|
109
|
+
}
|
|
92
110
|
}
|
|
93
111
|
|
|
94
112
|
result.locationCreated = locationCreated || null;
|
|
95
|
-
log(
|
|
113
|
+
log(
|
|
114
|
+
` 国家: ${result.locationCreated || "未知"}${locationDecision ? ` (${locationDecision})` : ""}`,
|
|
115
|
+
);
|
|
96
116
|
|
|
97
117
|
// 国家筛选
|
|
98
|
-
const
|
|
99
|
-
.
|
|
100
|
-
|
|
101
|
-
const isTargetLocation = locationList.includes(
|
|
102
|
-
result.locationCreated?.toUpperCase?.() || result.locationCreated,
|
|
118
|
+
const isTargetLocation = isLocationInList(
|
|
119
|
+
result.locationCreated,
|
|
120
|
+
locationList,
|
|
103
121
|
);
|
|
104
122
|
|
|
105
123
|
if (isTargetLocation) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { delay, getDelayConfig } from "./page-helpers.js";
|
|
2
2
|
import { scrollAndCollect } from "./scroll-collector.js";
|
|
3
|
+
import { extractUniqueId, toProfileUrl } from "../../lib/url.js";
|
|
3
4
|
|
|
4
5
|
const FILTER_WORDS = ["主页", "已关注", "粉丝", "推荐"];
|
|
5
6
|
|
|
@@ -48,7 +49,14 @@ async function waitForListContent(page, minChildren = 1, timeout = 15000) {
|
|
|
48
49
|
.catch(() => {});
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
|
|
52
|
+
function getProfileUrlForRetry(page) {
|
|
53
|
+
const currentUrl = page.url();
|
|
54
|
+
const uniqueId = extractUniqueId(currentUrl);
|
|
55
|
+
if (uniqueId) return toProfileUrl(uniqueId);
|
|
56
|
+
return currentUrl;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function openFollowModal(page, log = () => {}) {
|
|
52
60
|
const tryOpen = async () =>
|
|
53
61
|
page.evaluate((selectors) => {
|
|
54
62
|
const clickTarget = (node) => {
|
|
@@ -83,7 +91,16 @@ async function openFollowModal(page) {
|
|
|
83
91
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
84
92
|
await waitForFollowTrigger(page, attempt === 1 ? 15000 : 8000);
|
|
85
93
|
opened = await tryOpen();
|
|
86
|
-
if (opened)
|
|
94
|
+
if (opened) {
|
|
95
|
+
if (attempt >= 2) {
|
|
96
|
+
log(` [关注弹窗] 第 ${attempt} 次已点击关注入口: ${opened}`);
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (attempt >= 2) {
|
|
102
|
+
log(` [关注弹窗] 第 ${attempt} 次仍未找到可点击的关注入口`);
|
|
103
|
+
}
|
|
87
104
|
|
|
88
105
|
await page
|
|
89
106
|
.evaluate(() => {
|
|
@@ -94,6 +111,7 @@ async function openFollowModal(page) {
|
|
|
94
111
|
}
|
|
95
112
|
|
|
96
113
|
if (!opened) {
|
|
114
|
+
log(" [关注弹窗] 重试后仍未找到关注入口");
|
|
97
115
|
throw new Error(
|
|
98
116
|
"未找到关注入口元素,请确认当前页面为用户主页或页面结构已变化",
|
|
99
117
|
);
|
|
@@ -107,20 +125,31 @@ async function openFollowModal(page) {
|
|
|
107
125
|
timeout: 30000,
|
|
108
126
|
});
|
|
109
127
|
containerReady = true;
|
|
128
|
+
if (attempt >= 2) {
|
|
129
|
+
log(` [关注弹窗] 第 ${attempt} 次等待后,关注列表容器已出现`);
|
|
130
|
+
}
|
|
110
131
|
break;
|
|
111
132
|
} catch (e) {
|
|
112
133
|
if (attempt === 1) {
|
|
113
|
-
//
|
|
114
|
-
|
|
134
|
+
// 第一次超时,重新 goto 用户主页后重试
|
|
135
|
+
const retryUrl = getProfileUrlForRetry(page);
|
|
136
|
+
log(
|
|
137
|
+
` [关注弹窗] 第 1 次等待列表超时,准备 goto 主页重试: ${retryUrl} | ${e.message}`,
|
|
138
|
+
);
|
|
139
|
+
await page.goto(retryUrl, { waitUntil: "domcontentloaded" });
|
|
115
140
|
await delay(2000, 3000);
|
|
141
|
+
log(" [关注弹窗] 主页跳转完成,重新点击关注入口");
|
|
116
142
|
// 重新点击 follow 入口
|
|
117
143
|
opened = await tryOpen();
|
|
118
144
|
if (!opened) {
|
|
145
|
+
log(" [关注弹窗] goto 主页后仍未找到关注入口");
|
|
119
146
|
throw new Error(
|
|
120
|
-
"
|
|
147
|
+
"goto 主页后仍未找到关注入口元素,请确认当前页面为用户主页",
|
|
121
148
|
);
|
|
122
149
|
}
|
|
150
|
+
log(` [关注弹窗] goto 主页后已重新点击关注入口: ${opened}`);
|
|
123
151
|
} else {
|
|
152
|
+
log(` [关注弹窗] 第 2 次等待列表仍超时,结束本次提取: ${e.message}`);
|
|
124
153
|
throw e;
|
|
125
154
|
}
|
|
126
155
|
}
|
|
@@ -200,7 +229,7 @@ async function extractUsersFromModal(page, maxUsers) {
|
|
|
200
229
|
async function extractFollowAndFollowers(page, options = {}) {
|
|
201
230
|
const { maxFollowing = 999, maxFollowers = 999, log = () => {} } = options;
|
|
202
231
|
|
|
203
|
-
await openFollowModal(page);
|
|
232
|
+
await openFollowModal(page, log);
|
|
204
233
|
|
|
205
234
|
const following = await extractUsersFromModal(page, maxFollowing);
|
|
206
235
|
log(` 已关注: ${following.length}`);
|
|
@@ -8,6 +8,7 @@ import { detectCaptcha } from "./modules/captcha-handler.js";
|
|
|
8
8
|
import { getUserInfo, collectVideos } from "../videos/core.js";
|
|
9
9
|
import { extractFollowAndFollowers } from "./modules/follow-extractor.js";
|
|
10
10
|
import { processExplore } from "./explore-core.js";
|
|
11
|
+
import { DEFAULT_TARGET_LOCATIONS_CSV } from "../lib/target-locations.js";
|
|
11
12
|
|
|
12
13
|
export async function processRefresh(page, username, serverUrl, options, log) {
|
|
13
14
|
const { maxFollowing = 100, maxFollowers = 100, maxVideos = 100 } = options;
|
|
@@ -131,7 +132,7 @@ export async function processRefresh(page, username, serverUrl, options, log) {
|
|
|
131
132
|
enableFollow: true,
|
|
132
133
|
maxFollowing: 5,
|
|
133
134
|
maxFollowers: 5,
|
|
134
|
-
location:
|
|
135
|
+
location: DEFAULT_TARGET_LOCATIONS_CSV,
|
|
135
136
|
},
|
|
136
137
|
log,
|
|
137
138
|
);
|