tt-help-cli-ycl 1.3.30 → 1.3.31
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 +32 -4
- package/scripts/run-explore.ps1 +28 -2
- package/scripts/run-explore.sh +28 -3
- package/src/cli/comments.js +523 -0
- package/src/cli/explore.js +16 -8
- package/src/cli/videostats.js +81 -0
- package/src/lib/api-interceptor-comment.js +126 -0
- package/src/lib/api-interceptor.js +30 -38
- package/src/lib/args.js +278 -112
- package/src/lib/browser/cdp.js +93 -69
- package/src/lib/browser/health-checker.js +23 -11
- package/src/lib/browser/page.js +62 -35
- package/src/lib/constants.js +7 -0
- package/src/main.js +9 -5
- package/src/scraper/modules/comment-extractor.js +38 -59
- package/src/watch/data-store.js +28 -1
- package/src/watch/server.js +30 -2
package/src/lib/browser/cdp.js
CHANGED
|
@@ -1,93 +1,113 @@
|
|
|
1
|
-
import { exec } from
|
|
2
|
-
import { execSync } from
|
|
3
|
-
import fs from
|
|
4
|
-
import http from
|
|
5
|
-
import os from
|
|
6
|
-
import path from
|
|
7
|
-
import { chromium } from
|
|
1
|
+
import { exec } from "child_process";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import http from "http";
|
|
5
|
+
import os from "os";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { chromium } from "playwright";
|
|
8
8
|
|
|
9
9
|
const DEFAULT_CDP_PORT = 9222;
|
|
10
|
-
const DEFAULT_USER_DATA_DIR = path.join(
|
|
10
|
+
const DEFAULT_USER_DATA_DIR = path.join(
|
|
11
|
+
os.homedir(),
|
|
12
|
+
"Library",
|
|
13
|
+
"Application Support",
|
|
14
|
+
"Microsoft Edge For Testing",
|
|
15
|
+
);
|
|
11
16
|
|
|
12
17
|
function getEdgePath() {
|
|
13
18
|
const platform = os.platform();
|
|
14
|
-
if (platform ===
|
|
15
|
-
if (platform ===
|
|
16
|
-
return
|
|
19
|
+
if (platform === "darwin") return '"Microsoft Edge"';
|
|
20
|
+
if (platform === "win32") return "msedge.exe";
|
|
21
|
+
return "msedge";
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
function isEdgeRunning() {
|
|
20
|
-
return new Promise(resolve => {
|
|
25
|
+
return new Promise((resolve) => {
|
|
21
26
|
const platform = os.platform();
|
|
22
27
|
let command;
|
|
23
|
-
if (platform ===
|
|
24
|
-
command =
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
if (platform === "darwin") {
|
|
29
|
+
command =
|
|
30
|
+
'ps aux | grep -q "[M]icrosoft Edge.app/Contents/MacOS/Microsoft Edge" 2>/dev/null';
|
|
31
|
+
} else if (platform === "win32") {
|
|
32
|
+
command =
|
|
33
|
+
'tasklist /FI "IMAGENAME eq msedge.exe" 2>nul | findstr /I msedge';
|
|
27
34
|
} else {
|
|
28
|
-
command =
|
|
35
|
+
command = "pgrep -f msedge > /dev/null 2>&1";
|
|
29
36
|
}
|
|
30
37
|
exec(command, (err) => resolve(!err));
|
|
31
38
|
});
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
function checkCDPPort(port) {
|
|
35
|
-
return new Promise(resolve => {
|
|
36
|
-
const req = http.get(`http://127.0.0.1:${port}/json`, res => {
|
|
37
|
-
res.on(
|
|
38
|
-
res.on(
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
const req = http.get(`http://127.0.0.1:${port}/json`, (res) => {
|
|
44
|
+
res.on("data", () => {});
|
|
45
|
+
res.on("end", () => resolve(res.statusCode === 200));
|
|
46
|
+
});
|
|
47
|
+
req.on("error", () => resolve(false));
|
|
48
|
+
req.setTimeout(3000, () => {
|
|
49
|
+
resolve(false);
|
|
50
|
+
req.destroy();
|
|
39
51
|
});
|
|
40
|
-
req.on('error', () => resolve(false));
|
|
41
|
-
req.setTimeout(3000, () => { resolve(false); req.destroy(); });
|
|
42
52
|
});
|
|
43
53
|
}
|
|
44
54
|
|
|
45
55
|
function checkEdgeArgs() {
|
|
46
|
-
return new Promise(resolve => {
|
|
56
|
+
return new Promise((resolve) => {
|
|
47
57
|
const platform = os.platform();
|
|
48
58
|
let command;
|
|
49
|
-
if (platform ===
|
|
50
|
-
command =
|
|
51
|
-
|
|
52
|
-
|
|
59
|
+
if (platform === "darwin") {
|
|
60
|
+
command =
|
|
61
|
+
'ps aux | grep "[M]icrosoft Edge" | grep -v "Helper\\|crashpad" | grep "user-data-dir"';
|
|
62
|
+
} else if (platform === "win32") {
|
|
63
|
+
command =
|
|
64
|
+
'wmic process where "name like \\"%msedge%\\"" get commandline | findstr "user-data-dir"';
|
|
53
65
|
} else {
|
|
54
|
-
command =
|
|
66
|
+
command =
|
|
67
|
+
'ps aux | grep "[m]sedge" | grep -v "Helper\\|crashpad" | grep "user-data-dir"';
|
|
55
68
|
}
|
|
56
69
|
exec(command, (err, stdout) => resolve(!err && stdout.trim().length > 0));
|
|
57
70
|
});
|
|
58
71
|
}
|
|
59
72
|
|
|
60
73
|
function killEdgeProcesses(targetDir) {
|
|
61
|
-
return new Promise(resolve => {
|
|
74
|
+
return new Promise((resolve) => {
|
|
62
75
|
const platform = os.platform();
|
|
63
76
|
let command;
|
|
64
|
-
if (platform ===
|
|
77
|
+
if (platform === "darwin") {
|
|
65
78
|
if (targetDir) {
|
|
66
|
-
let pids =
|
|
79
|
+
let pids = "";
|
|
67
80
|
try {
|
|
68
81
|
// ps aux 输出中 --user-data-dir= 后面没有引号
|
|
69
82
|
// 用路径精确匹配,结尾加空格或行尾避免子串误杀
|
|
70
|
-
const escapedDir = targetDir.replace(
|
|
83
|
+
const escapedDir = targetDir.replace(
|
|
84
|
+
/[-\/\\^$*+?.()|[\]{}]/g,
|
|
85
|
+
"\\$&",
|
|
86
|
+
);
|
|
71
87
|
pids = execSync(
|
|
72
|
-
`ps aux | grep "[M]icrosoft Edge" | grep -v "Helper\\|crashpad" | grep -E -- '--user-data-dir=${escapedDir}($|[^A-Za-z0-9_])' | awk '{print $2}'
|
|
73
|
-
)
|
|
88
|
+
`ps aux | grep "[M]icrosoft Edge" | grep -v "Helper\\|crashpad" | grep -E -- '--user-data-dir=${escapedDir}($|[^A-Za-z0-9_])' | awk '{print $2}'`,
|
|
89
|
+
)
|
|
90
|
+
.toString()
|
|
91
|
+
.trim();
|
|
74
92
|
} catch (e) {
|
|
75
|
-
pids =
|
|
93
|
+
pids = "";
|
|
76
94
|
}
|
|
77
95
|
if (pids) {
|
|
78
96
|
command = `kill -9 ${pids} 2>/dev/null; rm -f ~/Library/Caches/Microsoft\\ Edge/Singleton*; true`;
|
|
79
97
|
} else {
|
|
80
|
-
command =
|
|
98
|
+
command = "true";
|
|
81
99
|
}
|
|
82
100
|
} else {
|
|
83
|
-
command =
|
|
101
|
+
command =
|
|
102
|
+
'killall -9 "Microsoft Edge" 2>/dev/null; rm -f ~/Library/Caches/Microsoft\\ Edge/Singleton*; true';
|
|
84
103
|
}
|
|
85
|
-
} else if (platform ===
|
|
104
|
+
} else if (platform === "win32") {
|
|
86
105
|
if (targetDir) {
|
|
87
106
|
try {
|
|
88
107
|
const ps1Path = path.join(os.tmpdir(), `kill-edge-${Date.now()}.ps1`);
|
|
89
108
|
const escapedDir = targetDir.replace(/'/g, "''");
|
|
90
|
-
const ps1Content =
|
|
109
|
+
const ps1Content =
|
|
110
|
+
`$procs = Get-CimInstance Win32_Process -Filter "Name='msedge.exe'" 2>$null; ` +
|
|
91
111
|
`$count = 0; ` +
|
|
92
112
|
`foreach ($p in $procs) { ` +
|
|
93
113
|
` if ($p.CommandLine -and $p.CommandLine -like "*${escapedDir}*") { ` +
|
|
@@ -99,22 +119,24 @@ function killEdgeProcesses(targetDir) {
|
|
|
99
119
|
`Write-Output $count`;
|
|
100
120
|
fs.writeFileSync(ps1Path, ps1Content);
|
|
101
121
|
const result = execSync(
|
|
102
|
-
`powershell.exe -ExecutionPolicy Bypass -File "${ps1Path}"
|
|
103
|
-
)
|
|
122
|
+
`powershell.exe -ExecutionPolicy Bypass -File "${ps1Path}"`,
|
|
123
|
+
)
|
|
124
|
+
.toString()
|
|
125
|
+
.trim();
|
|
104
126
|
fs.unlinkSync(ps1Path);
|
|
105
|
-
command =
|
|
127
|
+
command = "exit 0";
|
|
106
128
|
} catch {
|
|
107
|
-
command =
|
|
129
|
+
command = "taskkill /F /IM msedge.exe 2>nul || exit 0";
|
|
108
130
|
}
|
|
109
131
|
} else {
|
|
110
|
-
command =
|
|
132
|
+
command = "taskkill /F /IM msedge.exe 2>nul || exit 0";
|
|
111
133
|
}
|
|
112
134
|
} else {
|
|
113
135
|
if (targetDir) {
|
|
114
|
-
const escapedDir = targetDir.replace(/[-\/\\^$*+?.()|[\]{}]/g,
|
|
136
|
+
const escapedDir = targetDir.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
115
137
|
command = `ps aux | grep msedge | grep -v Helper | grep -E -- '--user-data-dir=${escapedDir}($|[^A-Za-z0-9_])' | awk '{print $2}' | xargs -r kill -9 2>/dev/null; true`;
|
|
116
138
|
} else {
|
|
117
|
-
command =
|
|
139
|
+
command = "pkill -9 -f msedge 2>/dev/null; true";
|
|
118
140
|
}
|
|
119
141
|
}
|
|
120
142
|
exec(command, () => resolve());
|
|
@@ -130,21 +152,21 @@ function launchEdgeWithCDP(port, userDataDir) {
|
|
|
130
152
|
const extraArgs = [
|
|
131
153
|
`--remote-debugging-port=${port}`,
|
|
132
154
|
`--user-data-dir="${userDataDir}"`,
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
].join(
|
|
144
|
-
|
|
145
|
-
if (platform ===
|
|
155
|
+
"--disable-blink-features=AutomationControlled",
|
|
156
|
+
"--no-first-run",
|
|
157
|
+
"--no-default-browser-check",
|
|
158
|
+
"--password-store=basic",
|
|
159
|
+
"--disable-background-mode",
|
|
160
|
+
"--disable-component-update",
|
|
161
|
+
"--disable-crash-reporter",
|
|
162
|
+
"--disable-breakpad",
|
|
163
|
+
"--disable-background-networking",
|
|
164
|
+
"--disable-sync",
|
|
165
|
+
].join(" ");
|
|
166
|
+
|
|
167
|
+
if (platform === "darwin") {
|
|
146
168
|
command = `open -a ${edgePath} --new --args ${extraArgs}`;
|
|
147
|
-
} else if (platform ===
|
|
169
|
+
} else if (platform === "win32") {
|
|
148
170
|
command = `start msedge ${extraArgs}`;
|
|
149
171
|
} else {
|
|
150
172
|
command = `msedge ${extraArgs} &`;
|
|
@@ -162,7 +184,7 @@ async function waitForCDP(port, timeout = 30000, interval = 1000) {
|
|
|
162
184
|
while (Date.now() - start < timeout) {
|
|
163
185
|
const ready = await checkCDPPort(port);
|
|
164
186
|
if (ready) return true;
|
|
165
|
-
await new Promise(r => setTimeout(r, interval));
|
|
187
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
166
188
|
}
|
|
167
189
|
return false;
|
|
168
190
|
}
|
|
@@ -183,7 +205,7 @@ export async function ensureBrowserReady(options = {}) {
|
|
|
183
205
|
if (!edgeArgsValid) {
|
|
184
206
|
console.error(`Edge 已运行但启动参数不完整,正在重启端口 ${port}...`);
|
|
185
207
|
await killEdgeProcesses(userDataDir);
|
|
186
|
-
await new Promise(r => setTimeout(r, 3000));
|
|
208
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
187
209
|
needLaunch = true;
|
|
188
210
|
}
|
|
189
211
|
}
|
|
@@ -197,22 +219,22 @@ export async function ensureBrowserReady(options = {}) {
|
|
|
197
219
|
if (edgeRunning) {
|
|
198
220
|
console.error(`Edge 已运行但 CDP 端口 ${port} 未启用,正在重启...`);
|
|
199
221
|
await killEdgeProcesses(userDataDir);
|
|
200
|
-
await new Promise(r => setTimeout(r, 3000));
|
|
222
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
201
223
|
} else {
|
|
202
224
|
console.error(`CDP 端口 ${port} 未就绪,正在启动 Edge 浏览器...`);
|
|
203
225
|
}
|
|
204
226
|
}
|
|
205
227
|
await launchEdgeWithCDP(port, userDataDir);
|
|
206
228
|
|
|
207
|
-
console.error(
|
|
229
|
+
console.error("等待浏览器启动...");
|
|
208
230
|
const launched = await waitForCDP(port);
|
|
209
231
|
if (!launched) {
|
|
210
232
|
throw new Error(
|
|
211
233
|
`等待 CDP 端口 ${port} 超时。请确认 Edge 浏览器已安装,\n` +
|
|
212
|
-
|
|
234
|
+
`或手动启动: Microsoft Edge --remote-debugging-port=${port} [参见 cdp.js extraArgs]`,
|
|
213
235
|
);
|
|
214
236
|
}
|
|
215
|
-
console.error(
|
|
237
|
+
console.error("浏览器启动成功");
|
|
216
238
|
}
|
|
217
239
|
|
|
218
240
|
const browser = await chromium.connectOverCDP(`http://127.0.0.1:${port}`);
|
|
@@ -221,9 +243,11 @@ export async function ensureBrowserReady(options = {}) {
|
|
|
221
243
|
|
|
222
244
|
export async function switchAccount(oldAccount, newAccount) {
|
|
223
245
|
console.error(`\n[账户切换] 从端口 ${oldAccount.port} -> ${newAccount.port}`);
|
|
246
|
+
console.error(` [账户切换] 等待 30 秒,请完成当前操作后自动切换...`);
|
|
247
|
+
await new Promise((r) => setTimeout(r, 30000));
|
|
224
248
|
|
|
225
249
|
await killEdgeProcesses(oldAccount.userDataDir);
|
|
226
|
-
await new Promise(r => setTimeout(r, 3000));
|
|
250
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
227
251
|
|
|
228
252
|
const newCdpOptions = {};
|
|
229
253
|
newCdpOptions.port = newAccount.port;
|
|
@@ -231,7 +255,7 @@ export async function switchAccount(oldAccount, newAccount) {
|
|
|
231
255
|
|
|
232
256
|
const browser = await ensureBrowserReady(newCdpOptions);
|
|
233
257
|
|
|
234
|
-
await new Promise(r => setTimeout(r, 10000));
|
|
258
|
+
await new Promise((r) => setTimeout(r, 10000));
|
|
235
259
|
|
|
236
260
|
return browser;
|
|
237
261
|
}
|
|
@@ -2,16 +2,19 @@ import os from "os";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { execSync } from "child_process";
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const MEMORY_THRESHOLD = 0.
|
|
8
|
-
const ROTATE_INTERVAL_MS =
|
|
5
|
+
const DEFAULT_BASE_PORT = 9222;
|
|
6
|
+
const DEFAULT_TOTAL_ACCOUNTS = 10;
|
|
7
|
+
const MEMORY_THRESHOLD = 0.95;
|
|
8
|
+
const ROTATE_INTERVAL_MS = 0.8 * 60 * 60 * 1000;
|
|
9
9
|
|
|
10
10
|
class HealthChecker {
|
|
11
|
-
constructor() {
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
const basePort = options.basePort ?? DEFAULT_BASE_PORT;
|
|
13
|
+
const totalAccounts = options.totalAccounts ?? DEFAULT_TOTAL_ACCOUNTS;
|
|
14
|
+
|
|
12
15
|
this.accounts = [];
|
|
13
|
-
for (let i = 0; i <
|
|
14
|
-
const port =
|
|
16
|
+
for (let i = 0; i < totalAccounts; i++) {
|
|
17
|
+
const port = basePort + i;
|
|
15
18
|
const profile = `p${port}`;
|
|
16
19
|
this.accounts.push({
|
|
17
20
|
port,
|
|
@@ -33,7 +36,7 @@ class HealthChecker {
|
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
getNextAccount() {
|
|
36
|
-
this.currentIndex = (this.currentIndex + 1) %
|
|
39
|
+
this.currentIndex = (this.currentIndex + 1) % this.accounts.length;
|
|
37
40
|
this.startTime = Date.now(); // 切换后重置运行时间
|
|
38
41
|
return this.accounts[this.currentIndex];
|
|
39
42
|
}
|
|
@@ -78,23 +81,32 @@ class HealthChecker {
|
|
|
78
81
|
const memPercent = (memUsage * 100).toFixed(1);
|
|
79
82
|
|
|
80
83
|
if (memUsage >= MEMORY_THRESHOLD) {
|
|
81
|
-
|
|
84
|
+
const currentPort = this.accounts[this.currentIndex].port;
|
|
85
|
+
return {
|
|
86
|
+
shouldSwitch: true,
|
|
87
|
+
reason: `端口 ${currentPort} | 系统内存 ${memPercent}%`,
|
|
88
|
+
};
|
|
82
89
|
}
|
|
83
90
|
|
|
84
91
|
const elapsed = Date.now() - this.startTime;
|
|
85
92
|
if (elapsed >= ROTATE_INTERVAL_MS) {
|
|
86
93
|
const mins = Math.round(elapsed / 60000);
|
|
87
|
-
|
|
94
|
+
const currentPort = this.accounts[this.currentIndex].port;
|
|
95
|
+
return {
|
|
96
|
+
shouldSwitch: true,
|
|
97
|
+
reason: `端口 ${currentPort} | 运行 ${mins} 分钟`,
|
|
98
|
+
};
|
|
88
99
|
}
|
|
89
100
|
|
|
90
101
|
// 计算预计切换时间
|
|
91
102
|
const remainingMs = ROTATE_INTERVAL_MS - elapsed;
|
|
92
103
|
const remainingMins = Math.round(remainingMs / 60000);
|
|
93
104
|
const memHeadroom = ((MEMORY_THRESHOLD - memUsage) * 100).toFixed(1);
|
|
105
|
+
const currentPort = this.accounts[this.currentIndex].port;
|
|
94
106
|
|
|
95
107
|
return {
|
|
96
108
|
shouldSwitch: false,
|
|
97
|
-
info:
|
|
109
|
+
info: `端口 ${currentPort} | 内存 ${memPercent}% (余量 ${memHeadroom}%) | 定时轮换 ${remainingMins} 分钟后`,
|
|
98
110
|
};
|
|
99
111
|
}
|
|
100
112
|
}
|
package/src/lib/browser/page.js
CHANGED
|
@@ -1,25 +1,30 @@
|
|
|
1
|
-
import os from
|
|
2
|
-
import path from
|
|
3
|
-
import { delay } from
|
|
4
|
-
import { retryWithBackoff } from
|
|
5
|
-
import { getDelayConfig } from
|
|
6
|
-
import { ensureBrowserReady, killEdgeProcesses } from
|
|
7
|
-
|
|
8
|
-
const DEFAULT_USER_DATA_DIR = path.join(
|
|
1
|
+
import os from "os";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { delay } from "../delay.js";
|
|
4
|
+
import { retryWithBackoff } from "../retry.js";
|
|
5
|
+
import { getDelayConfig } from "../delay.js";
|
|
6
|
+
import { ensureBrowserReady, killEdgeProcesses } from "./cdp.js";
|
|
7
|
+
|
|
8
|
+
const DEFAULT_USER_DATA_DIR = path.join(
|
|
9
|
+
os.homedir(),
|
|
10
|
+
"Library",
|
|
11
|
+
"Application Support",
|
|
12
|
+
"Microsoft Edge For Testing",
|
|
13
|
+
);
|
|
9
14
|
|
|
10
15
|
const BROWSER_CLOSED_PATTERNS = [
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
"Target page, context or browser has been closed",
|
|
17
|
+
"Target closed",
|
|
18
|
+
"Browser has been closed",
|
|
19
|
+
"Protocol error",
|
|
20
|
+
"Connection closed",
|
|
21
|
+
"net::ERR_CONNECTION",
|
|
17
22
|
];
|
|
18
23
|
|
|
19
24
|
export function isBrowserClosedError(error) {
|
|
20
25
|
if (!error) return false;
|
|
21
|
-
const msg = error.message || error.toString() ||
|
|
22
|
-
return BROWSER_CLOSED_PATTERNS.some(p => msg.includes(p));
|
|
26
|
+
const msg = error.message || error.toString() || "";
|
|
27
|
+
return BROWSER_CLOSED_PATTERNS.some((p) => msg.includes(p));
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
export async function relaunchBrowser(cdpOptions, port) {
|
|
@@ -27,25 +32,25 @@ export async function relaunchBrowser(cdpOptions, port) {
|
|
|
27
32
|
const targetDir = cdpOptions.userDataDir || DEFAULT_USER_DATA_DIR;
|
|
28
33
|
// kill 并清理 Singleton 锁文件,确保 Edge 能启动新实例
|
|
29
34
|
await killEdgeProcesses(targetDir);
|
|
30
|
-
await new Promise(r => setTimeout(r, 3000));
|
|
35
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
31
36
|
// 确保端口已释放后再启动
|
|
32
37
|
let retries = 0;
|
|
33
38
|
while (retries < 5) {
|
|
34
39
|
try {
|
|
35
40
|
return await ensureBrowserReady(cdpOptions);
|
|
36
41
|
} catch (e) {
|
|
37
|
-
if (e.message && e.message.includes(
|
|
42
|
+
if (e.message && e.message.includes("setDownloadBehavior")) {
|
|
38
43
|
retries++;
|
|
39
44
|
console.error(` [浏览器] CDP 连接异常,重试 ${retries}/5...`);
|
|
40
|
-
await new Promise(r => setTimeout(r, 3000));
|
|
45
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
41
46
|
await killEdgeProcesses(targetDir);
|
|
42
|
-
await new Promise(r => setTimeout(r, 5000));
|
|
47
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
43
48
|
continue;
|
|
44
49
|
}
|
|
45
50
|
throw e;
|
|
46
51
|
}
|
|
47
52
|
}
|
|
48
|
-
throw new Error(
|
|
53
|
+
throw new Error("浏览器重启失败,CDP 连接异常");
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
export async function withBrowserRecovery(fn, browser, page, cdpOptions, port) {
|
|
@@ -61,7 +66,22 @@ export async function withBrowserRecovery(fn, browser, page, cdpOptions, port) {
|
|
|
61
66
|
}
|
|
62
67
|
}
|
|
63
68
|
|
|
69
|
+
/**
|
|
70
|
+
* 通过 Cookie 判断登录状态(推荐,最可靠)
|
|
71
|
+
* 已登录:存在 sessionid Cookie
|
|
72
|
+
* 未登录:不存在 sessionid Cookie
|
|
73
|
+
*/
|
|
64
74
|
export async function isLoggedIn(page) {
|
|
75
|
+
const cookies = await page.context().cookies("https://www.tiktok.com");
|
|
76
|
+
return cookies.some((c) => c.name === "sessionid");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 通过 DOM 元素判断登录状态(备用方案)
|
|
81
|
+
* 依赖页面渲染完成,可能存在误判
|
|
82
|
+
* @deprecated 推荐使用 isLoggedIn(基于 Cookie)
|
|
83
|
+
*/
|
|
84
|
+
export async function isLoggedInByDom(page) {
|
|
65
85
|
// 先等客户端渲染完成:登录态元素或登录按钮,哪个先出现就停止等待
|
|
66
86
|
const loginOrLoggedInSelector = [
|
|
67
87
|
'[class*="DivProfileContainer"]',
|
|
@@ -71,19 +91,22 @@ export async function isLoggedIn(page) {
|
|
|
71
91
|
'button:has-text("登录")',
|
|
72
92
|
'button:has-text("Log in")',
|
|
73
93
|
'button:has-text("Sign in")',
|
|
74
|
-
].join(
|
|
94
|
+
].join(", ");
|
|
75
95
|
|
|
76
|
-
await page
|
|
96
|
+
await page
|
|
97
|
+
.waitForSelector(loginOrLoggedInSelector, { timeout: 10000 })
|
|
98
|
+
.catch(() => {});
|
|
77
99
|
|
|
78
100
|
return page.evaluate(() => {
|
|
79
101
|
const hasProfileContainer = !!document.querySelector(
|
|
80
|
-
'[class*="DivProfileContainer"], [class*="DivUserContainer"]'
|
|
102
|
+
'[class*="DivProfileContainer"], [class*="DivUserContainer"]',
|
|
81
103
|
);
|
|
82
104
|
const hasUserMenu = !!document.querySelector(
|
|
83
|
-
'[class*="UserMenu"], [class*="user-menu"], [class*="CurrentUserInfo"]'
|
|
105
|
+
'[class*="UserMenu"], [class*="user-menu"], [class*="CurrentUserInfo"]',
|
|
84
106
|
);
|
|
85
|
-
const hasLoginButton = Array.from(
|
|
86
|
-
.
|
|
107
|
+
const hasLoginButton = Array.from(
|
|
108
|
+
document.querySelectorAll('button, [role="button"]'),
|
|
109
|
+
).some((el) => /^(登录|Log in|Sign in)$/i.test(el.textContent.trim()));
|
|
87
110
|
|
|
88
111
|
return (hasProfileContainer || hasUserMenu) && !hasLoginButton;
|
|
89
112
|
});
|
|
@@ -95,7 +118,7 @@ export async function closeCommentPanel(page) {
|
|
|
95
118
|
if (rightPanel) {
|
|
96
119
|
const tabContainer = rightPanel.querySelector('[class*="TabContainer"]');
|
|
97
120
|
if (tabContainer) {
|
|
98
|
-
const closeOverlay = tabContainer.querySelector(
|
|
121
|
+
const closeOverlay = tabContainer.querySelector("div:last-child");
|
|
99
122
|
if (closeOverlay) closeOverlay.click();
|
|
100
123
|
}
|
|
101
124
|
}
|
|
@@ -108,7 +131,7 @@ export async function ensureTikTokPage(browser, url) {
|
|
|
108
131
|
|
|
109
132
|
for (const ctx of contexts) {
|
|
110
133
|
for (const p of ctx.pages()) {
|
|
111
|
-
if (p.url().includes(
|
|
134
|
+
if (p.url().includes("tiktok.com")) {
|
|
112
135
|
page = p;
|
|
113
136
|
break;
|
|
114
137
|
}
|
|
@@ -117,13 +140,15 @@ export async function ensureTikTokPage(browser, url) {
|
|
|
117
140
|
}
|
|
118
141
|
|
|
119
142
|
if (!page) {
|
|
120
|
-
console.error(
|
|
143
|
+
console.error("未找到 TikTok 页面,正在打开...");
|
|
121
144
|
const defaultCtx = browser.contexts()[0];
|
|
122
145
|
page = await defaultCtx.newPage();
|
|
123
|
-
await retryWithBackoff(() =>
|
|
146
|
+
await retryWithBackoff(() =>
|
|
147
|
+
page.goto(url, { waitUntil: "load", timeout: 30000 }),
|
|
148
|
+
);
|
|
124
149
|
const config = getDelayConfig();
|
|
125
150
|
await delay(Math.round(config.switchMax * 0.5), config.switchMax);
|
|
126
|
-
console.error(
|
|
151
|
+
console.error("TikTok 页面已打开");
|
|
127
152
|
}
|
|
128
153
|
|
|
129
154
|
return page;
|
|
@@ -133,7 +158,7 @@ export async function findTikTokPage(browser) {
|
|
|
133
158
|
const contexts = browser.contexts();
|
|
134
159
|
for (const ctx of contexts) {
|
|
135
160
|
for (const p of ctx.pages()) {
|
|
136
|
-
if (p.url().includes(
|
|
161
|
+
if (p.url().includes("tiktok.com")) return p;
|
|
137
162
|
}
|
|
138
163
|
}
|
|
139
164
|
return null;
|
|
@@ -142,7 +167,7 @@ export async function findTikTokPage(browser) {
|
|
|
142
167
|
export async function getOrCreatePage(browser) {
|
|
143
168
|
let page = await findTikTokPage(browser);
|
|
144
169
|
if (!page) {
|
|
145
|
-
const defaultCtx = browser.contexts()[0] || await browser.newContext();
|
|
170
|
+
const defaultCtx = browser.contexts()[0] || (await browser.newContext());
|
|
146
171
|
page = await defaultCtx.newPage();
|
|
147
172
|
}
|
|
148
173
|
return page;
|
|
@@ -151,6 +176,8 @@ export async function getOrCreatePage(browser) {
|
|
|
151
176
|
export function assertPageUrl(page, expectedPath) {
|
|
152
177
|
const actual = page.url();
|
|
153
178
|
if (!actual.includes(expectedPath)) {
|
|
154
|
-
throw new Error(
|
|
179
|
+
throw new Error(
|
|
180
|
+
`[代理错误] 预期访问 ${expectedPath},实际跳转到了 ${actual}`,
|
|
181
|
+
);
|
|
155
182
|
}
|
|
156
183
|
}
|
package/src/lib/constants.js
CHANGED
|
@@ -143,6 +143,12 @@ const HELP_TEXT = [
|
|
|
143
143
|
' 示例: tt-help open 9222',
|
|
144
144
|
' tt-help open --list',
|
|
145
145
|
'',
|
|
146
|
+
' videostats <文件路径> -p <并发数>',
|
|
147
|
+
' 批量刷新视频统计数据(playCount, diggCount, commentCount, shareCount, collectCount)',
|
|
148
|
+
' 读取视频文件,通过 getVideoInfo 获取最新数据,更新后写回原文件',
|
|
149
|
+
' -p, --parallel <N> 并发数(默认: 3)',
|
|
150
|
+
' 示例: tt-help videostats data/result-videos.json -p 3',
|
|
151
|
+
'',
|
|
146
152
|
' config [show|set|reset]',
|
|
147
153
|
' config 查看当前配置',
|
|
148
154
|
' config set <key> <value> 设置配置(key: proxy, server, browser, userId, maxFollowing, maxFollowers, maxVideos, maxComments)',
|
|
@@ -156,6 +162,7 @@ const HELP_TEXT = [
|
|
|
156
162
|
' tt-help explore qiqi23280 fast --location ES --max-comments 50',
|
|
157
163
|
' tt-help config set server http://127.0.0.1:3001',
|
|
158
164
|
' tt-help attach -p 5 -i 10',
|
|
165
|
+
' tt-help videostats data/result-videos.json -p 3',
|
|
159
166
|
];
|
|
160
167
|
|
|
161
168
|
function getConfigText() {
|
package/src/main.js
CHANGED
|
@@ -6,16 +6,20 @@ import { handleAttach } from './cli/attach.js';
|
|
|
6
6
|
import { handleWatch } from './cli/watch.js';
|
|
7
7
|
import { handleConfig, showConfig, showUsage, version } from './cli/config.js';
|
|
8
8
|
import { handleOpen } from './cli/open.js';
|
|
9
|
+
import { handleComments } from './cli/comments.js';
|
|
10
|
+
import { handleVideoStats } from './cli/videostats.js';
|
|
9
11
|
|
|
10
12
|
async function main() {
|
|
11
13
|
const parsed = parseArgs();
|
|
12
14
|
|
|
13
15
|
switch (parsed.subcommand) {
|
|
14
|
-
case 'explore':
|
|
15
|
-
case 'info':
|
|
16
|
-
case 'attach':
|
|
17
|
-
case 'watch':
|
|
18
|
-
case 'open':
|
|
16
|
+
case 'explore': return handleExplore(parsed);
|
|
17
|
+
case 'info': return handleInfo(parsed);
|
|
18
|
+
case 'attach': return handleAttach(parsed);
|
|
19
|
+
case 'watch': return handleWatch(parsed);
|
|
20
|
+
case 'open': return handleOpen(parsed);
|
|
21
|
+
case 'comments': return handleComments(parsed);
|
|
22
|
+
case 'videostats': return handleVideoStats(parsed);
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
const { urls, outputFile, outputFormat, exploreCount, showConfig: showCfg, showHelp, showVersion, customProxy, configAction, configKey, configValue } = parsed;
|