tt-help-cli-ycl 1.3.29 → 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.
@@ -1,93 +1,113 @@
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';
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(os.homedir(), 'Library', 'Application Support', 'Microsoft Edge For Testing');
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 === 'darwin') return '"Microsoft Edge"';
15
- if (platform === 'win32') return 'msedge.exe';
16
- return 'msedge';
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 === 'darwin') {
24
- command = 'ps aux | grep -q "[M]icrosoft Edge.app/Contents/MacOS/Microsoft Edge" 2>/dev/null';
25
- } else if (platform === 'win32') {
26
- command = 'tasklist /FI "IMAGENAME eq msedge.exe" 2>nul | findstr /I msedge';
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 = 'pgrep -f msedge > /dev/null 2>&1';
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('data', () => {});
38
- res.on('end', () => resolve(res.statusCode === 200));
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 === 'darwin') {
50
- command = 'ps aux | grep "[M]icrosoft Edge" | grep -v "Helper\\|crashpad" | grep "user-data-dir"';
51
- } else if (platform === 'win32') {
52
- command = 'wmic process where "name like \\"%msedge%\\"" get commandline | findstr "user-data-dir"';
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 = 'ps aux | grep "[m]sedge" | grep -v "Helper\\|crashpad" | grep "user-data-dir"';
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 === 'darwin') {
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(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
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
- ).toString().trim();
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 = 'true';
98
+ command = "true";
81
99
  }
82
100
  } else {
83
- command = 'killall -9 "Microsoft Edge" 2>/dev/null; rm -f ~/Library/Caches/Microsoft\\ Edge/Singleton*; true';
101
+ command =
102
+ 'killall -9 "Microsoft Edge" 2>/dev/null; rm -f ~/Library/Caches/Microsoft\\ Edge/Singleton*; true';
84
103
  }
85
- } else if (platform === 'win32') {
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 = `$procs = Get-CimInstance Win32_Process -Filter "Name='msedge.exe'" 2>$null; ` +
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
- ).toString().trim();
122
+ `powershell.exe -ExecutionPolicy Bypass -File "${ps1Path}"`,
123
+ )
124
+ .toString()
125
+ .trim();
104
126
  fs.unlinkSync(ps1Path);
105
- command = 'exit 0';
127
+ command = "exit 0";
106
128
  } catch {
107
- command = 'taskkill /F /IM msedge.exe 2>nul || exit 0';
129
+ command = "taskkill /F /IM msedge.exe 2>nul || exit 0";
108
130
  }
109
131
  } else {
110
- command = 'taskkill /F /IM msedge.exe 2>nul || exit 0';
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 = 'pkill -9 -f msedge 2>/dev/null; true';
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
- '--disable-blink-features=AutomationControlled',
134
- '--no-first-run',
135
- '--no-default-browser-check',
136
- '--password-store=basic',
137
- '--disable-background-mode',
138
- '--disable-component-update',
139
- '--disable-crash-reporter',
140
- '--disable-breakpad',
141
- '--disable-background-networking',
142
- '--disable-sync',
143
- ].join(' ');
144
-
145
- if (platform === 'darwin') {
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 === 'win32') {
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
- `或手动启动: Microsoft Edge --remote-debugging-port=${port} [参见 cdp.js extraArgs]`
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 TOTAL_ACCOUNTS = 10;
6
- const BASE_PORT = 9222;
7
- const MEMORY_THRESHOLD = 0.9;
8
- const ROTATE_INTERVAL_MS = 1 * 60 * 60 * 1000; // 1小时
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 < TOTAL_ACCOUNTS; i++) {
14
- const port = BASE_PORT + i;
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) % TOTAL_ACCOUNTS;
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
- return { shouldSwitch: true, reason: `系统内存 ${memPercent}%` };
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
- return { shouldSwitch: true, reason: `运行 ${mins} 分钟` };
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: `内存 ${memPercent}% (余量 ${memHeadroom}%) | 定时轮换 ${remainingMins} 分钟后`,
109
+ info: `端口 ${currentPort} | 内存 ${memPercent}% (余量 ${memHeadroom}%) | 定时轮换 ${remainingMins} 分钟后`,
98
110
  };
99
111
  }
100
112
  }
@@ -1,25 +1,30 @@
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(os.homedir(), 'Library', 'Application Support', 'Microsoft Edge For Testing');
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
- 'Target page, context or browser has been closed',
12
- 'Target closed',
13
- 'Browser has been closed',
14
- 'Protocol error',
15
- 'Connection closed',
16
- 'net::ERR_CONNECTION',
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('setDownloadBehavior')) {
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('浏览器重启失败,CDP 连接异常');
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.waitForSelector(loginOrLoggedInSelector, { timeout: 10000 }).catch(() => {});
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(document.querySelectorAll('button, [role="button"]'))
86
- .some(el => /^(登录|Log in|Sign in)$/i.test(el.textContent.trim()));
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('div:last-child');
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('tiktok.com')) {
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('未找到 TikTok 页面,正在打开...');
143
+ console.error("未找到 TikTok 页面,正在打开...");
121
144
  const defaultCtx = browser.contexts()[0];
122
145
  page = await defaultCtx.newPage();
123
- await retryWithBackoff(() => page.goto(url, { waitUntil: 'load', timeout: 30000 }));
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('TikTok 页面已打开');
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('tiktok.com')) return p;
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(`[代理错误] 预期访问 ${expectedPath},实际跳转到了 ${actual}`);
179
+ throw new Error(
180
+ `[代理错误] 预期访问 ${expectedPath},实际跳转到了 ${actual}`,
181
+ );
155
182
  }
156
183
  }
@@ -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': return handleExplore(parsed);
15
- case 'info': return handleInfo(parsed);
16
- case 'attach': return handleAttach(parsed);
17
- case 'watch': return handleWatch(parsed);
18
- case 'open': return handleOpen(parsed);
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;