tt-help-cli-ycl 1.3.24 → 1.3.25

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tt-help-cli-ycl",
3
- "version": "1.3.24",
3
+ "version": "1.3.25",
4
4
  "description": "TikTok user & video data scraper - extract ttSeller, verified, locationCreated from HTML source",
5
5
  "type": "module",
6
6
  "bin": {
@@ -2,8 +2,9 @@ import { getOrCreatePage, isBrowserClosedError, relaunchBrowser } from '../lib/b
2
2
  import { delay, getDelayConfig, setDelayConfig } from '../scraper/modules/page-helpers.js';
3
3
  import { userId as configuredUserId, saveUserId } from '../lib/constants.js';
4
4
  import { getMacOrUuid } from '../lib/mac-or-uuid.js';
5
- import { ensureBrowserReady as ensureBrowserReadyCDP } from '../lib/browser/cdp.js';
5
+ import { ensureBrowserReady as ensureBrowserReadyCDP, switchAccount } from '../lib/browser/cdp.js';
6
6
  import { showResourceUsage } from '../utils/index.js';
7
+ import { HealthChecker } from '../lib/browser/health-checker.js';
7
8
  import path from 'path';
8
9
  import os from 'os';
9
10
 
@@ -70,15 +71,22 @@ export async function handleExplore(options) {
70
71
  console.error(`关注/粉丝: ${exploreEnableFollow ? '启用' : '禁用'}`);
71
72
  console.error(`服务器: ${serverUrl}(断开会自动重连)`);
72
73
  if (exploreMaxUsers > 0) console.error(`上限: ${exploreMaxUsers} 个用户`);
73
- console.error(`CDP 端口: ${explorePort || 9222}, 用户编号: ${userId}`);
74
- if (exploreProfile) console.error(`浏览器配置: ${exploreProfile}`);
74
+
75
+ const healthChecker = new HealthChecker();
76
+ let currentAccount = healthChecker.getCurrentAccount();
75
77
 
76
78
  const cdpOptions = {};
77
79
  if (explorePort) cdpOptions.port = explorePort;
78
80
  if (exploreProfile) {
79
81
  cdpOptions.userDataDir = path.join(os.homedir(), 'Library', 'Application Support', `Microsoft Edge For Testing_${exploreProfile}`);
82
+ } else {
83
+ cdpOptions.port = currentAccount.port;
84
+ cdpOptions.userDataDir = currentAccount.userDataDir;
80
85
  }
81
86
 
87
+ console.error(`CDP 端口: ${cdpOptions.port || 9222}, 用户编号: ${userId}`);
88
+ if (exploreProfile || cdpOptions.userDataDir) console.error(`浏览器配置: ${exploreProfile || path.basename(cdpOptions.userDataDir)}`);
89
+
82
90
  let browser = await ensureBrowserReadyCDP(cdpOptions);
83
91
  const { processExplore } = await import('../scraper/explore-core.js');
84
92
 
@@ -94,6 +102,36 @@ export async function handleExplore(options) {
94
102
  }
95
103
  });
96
104
 
105
+ // 账户切换后,重新初始化 page
106
+ async function setupNewPage(newBrowser) {
107
+ const newPage = await getOrCreatePage(newBrowser);
108
+ await newPage.route("**/*", (route) => {
109
+ const resourceType = route.request().resourceType();
110
+ if (resourceType === 'image' || resourceType === 'stylesheet') {
111
+ route.abort();
112
+ } else {
113
+ route.continue();
114
+ }
115
+ });
116
+ return newPage;
117
+ }
118
+
119
+ async function handleAccountSwitch(reason) {
120
+ console.error(`\n[健康检查] ${reason},切换到下一个账户...`);
121
+ const oldAccount = currentAccount;
122
+ const nextAccount = healthChecker.getNextAccount();
123
+ currentAccount = nextAccount;
124
+ const newBrowser = await switchAccount(
125
+ { port: oldAccount.port, userDataDir: oldAccount.userDataDir },
126
+ { port: nextAccount.port, userDataDir: nextAccount.userDataDir }
127
+ );
128
+ browser = newBrowser;
129
+ const newPage = await setupNewPage(browser);
130
+ Object.assign(page, newPage);
131
+ Object.assign(cdpOptions, { port: nextAccount.port, userDataDir: nextAccount.userDataDir });
132
+ console.error(`[健康检查] 已切换到端口 ${nextAccount.port}`);
133
+ }
134
+
97
135
  let processedCount = 0;
98
136
  let errorCount = 0;
99
137
  let consecutiveNetworkErrors = 0;
@@ -109,6 +147,11 @@ export async function handleExplore(options) {
109
147
  showResourceUsage();
110
148
  }
111
149
 
150
+ const checkResult = healthChecker.check();
151
+ if (checkResult.shouldSwitch) {
152
+ await handleAccountSwitch(checkResult.reason);
153
+ }
154
+
112
155
  if (consecutiveNetworkErrors > 0) {
113
156
  const waitTime = consecutiveNetworkErrors <= 2
114
157
  ? 0
@@ -137,9 +180,9 @@ export async function handleExplore(options) {
137
180
 
138
181
  // 浏览器关闭检测:processExplore 内部 catch 了异常,需要从 result.error 判断
139
182
  if (result.error && isBrowserClosedError(new Error(result.error))) {
140
- const newBrowser = await relaunchBrowser(cdpOptions, explorePort || 9222);
183
+ const newBrowser = await relaunchBrowser(cdpOptions, cdpOptions.port || 9222);
141
184
  browser = newBrowser;
142
- const newPage = await getOrCreatePage(browser);
185
+ const newPage = await setupNewPage(browser);
143
186
  Object.assign(page, newPage);
144
187
  // 重试当前用户
145
188
  result = await processExplore(page, username, {
@@ -178,8 +221,8 @@ export async function handleExplore(options) {
178
221
  })
179
222
  ).catch(() => {});
180
223
  if (errorType === '被封') {
181
- console.error(`\n账号被封,停止处理`);
182
- break;
224
+ await handleAccountSwitch('账号被封');
225
+ continue;
183
226
  }
184
227
  if (exploreMaxUsers > 0 && processedCount >= exploreMaxUsers) {
185
228
  console.error(`\n已达上限 ${exploreMaxUsers} 个用户,停止处理`);
@@ -0,0 +1,102 @@
1
+ import { ensureBrowserReady, killEdgeProcesses } from '../lib/browser/cdp.js';
2
+ import path from 'path';
3
+ import os from 'os';
4
+
5
+ const BASE_PORT = 9222;
6
+ const TOTAL_ACCOUNTS = 10;
7
+
8
+ export async function handleOpen(parsed) {
9
+ const { openPort, openList } = parsed;
10
+
11
+ if (openList) {
12
+ console.error('内置浏览器配置:');
13
+ for (let i = 0; i < TOTAL_ACCOUNTS; i++) {
14
+ const port = BASE_PORT + i;
15
+ const profile = `p${port}`;
16
+ const userDataDir = path.join(os.homedir(), 'Library', 'Application Support', `Microsoft Edge For Testing_${profile}`);
17
+ console.error(` open ${port} → profile: ${profile}, userDataDir: ${userDataDir}`);
18
+ }
19
+ console.error('');
20
+ console.error('用法: tt-help open <端口>');
21
+ console.error('示例: tt-help open 9222');
22
+ process.exit(0);
23
+ }
24
+
25
+ if (!openPort) {
26
+ console.error('用法: tt-help open <端口>');
27
+ console.error('示例: tt-help open 9222');
28
+ console.error('');
29
+ console.error('可用端口: 9222 - 9231 (共 10 个)');
30
+ console.error('运行 "tt-help open --list" 查看所有配置');
31
+ process.exit(1);
32
+ }
33
+
34
+ const port = parseInt(openPort);
35
+ if (isNaN(port) || port < BASE_PORT || port >= BASE_PORT + TOTAL_ACCOUNTS) {
36
+ console.error(`端口 ${openPort} 不在有效范围内 (9222 - 9231)`);
37
+ process.exit(1);
38
+ }
39
+
40
+ const profile = `p${port}`;
41
+ const userDataDir = path.join(os.homedir(), 'Library', 'Application Support', `Microsoft Edge For Testing_${profile}`);
42
+
43
+ console.error(`正在启动浏览器... 端口: ${port}, profile: ${profile}`);
44
+ console.error(`userDataDir: ${userDataDir}`);
45
+ console.error('');
46
+ console.error('启动后请在浏览器中登录 TikTok 账户');
47
+ console.error('登录完成后关闭浏览器即可,下次 open 会保留登录状态');
48
+ console.error('');
49
+ console.error('按 Ctrl+C 退出并关闭浏览器');
50
+
51
+ let browser = null;
52
+
53
+ try {
54
+ const cdpOptions = {};
55
+ cdpOptions.port = port;
56
+ cdpOptions.userDataDir = userDataDir;
57
+
58
+ browser = await ensureBrowserReady(cdpOptions);
59
+
60
+ // Ctrl+C 关闭浏览器
61
+ process.on('SIGINT', async () => {
62
+ console.error('\n正在关闭浏览器...');
63
+ try {
64
+ await killEdgeProcesses(userDataDir);
65
+ await browser.close();
66
+ } catch { /* ignore */ }
67
+ console.error('浏览器已关闭');
68
+ process.exit(0);
69
+ });
70
+
71
+ console.error('');
72
+ console.error('浏览器已启动并连接成功');
73
+ console.error('TikTok 页面将在浏览器中自动打开...');
74
+
75
+ // 打开 TikTok 首页
76
+ try {
77
+ const pages = await browser.pages();
78
+ let page = pages.find(p => !p.url().startsWith('devtools'));
79
+ if (!page) {
80
+ const context = await browser.newBrowserCDPSession();
81
+ await context.send('Target.createTarget', { url: '' });
82
+ await new Promise(r => setTimeout(r, 1000));
83
+ const newPages = await browser.pages();
84
+ page = newPages.find(p => !p.url().startsWith('devtools'));
85
+ }
86
+
87
+ if (page) {
88
+ await page.goto('https://www.tiktok.com', { waitUntil: 'domcontentloaded', timeout: 30000 });
89
+ console.error('已打开 https://www.tiktok.com');
90
+ }
91
+ } catch (e) {
92
+ console.error(`打开页面时出错: ${e.message}`);
93
+ console.error('浏览器已启动,可以手动在浏览器中打开 TikTok');
94
+ }
95
+
96
+ // 保持进程运行
97
+ await new Promise(() => {});
98
+ } catch (err) {
99
+ console.error(`启动失败: ${err.message}`);
100
+ process.exit(1);
101
+ }
102
+ }
package/src/lib/args.js CHANGED
@@ -426,6 +426,36 @@ function parseAttachArgs(args) {
426
426
  };
427
427
  }
428
428
 
429
+ function parseOpenArgs(args) {
430
+ let openPort = null;
431
+ let openList = false;
432
+
433
+ for (let i = 0; i < args.length; i++) {
434
+ const arg = args[i];
435
+ if (arg === '--list') {
436
+ openList = true;
437
+ } else if (!arg.startsWith('-')) {
438
+ openPort = arg;
439
+ }
440
+ }
441
+
442
+ return {
443
+ subcommand: 'open',
444
+ openPort,
445
+ openList,
446
+ urls: [],
447
+ outputFormat: 'json',
448
+ exploreCount: 0,
449
+ showConfig: false,
450
+ showHelp: false,
451
+ customProxy: null,
452
+ configAction: null,
453
+ configValue: null,
454
+ pipeMode: false,
455
+ filterStr: null,
456
+ };
457
+ }
458
+
429
459
  export function parseArgs() {
430
460
  const args = process.argv.slice(2);
431
461
 
@@ -452,6 +482,10 @@ export function parseArgs() {
452
482
  return parseAttachArgs(args.slice(1));
453
483
  }
454
484
 
485
+ if (args.length > 0 && args[0] === 'open') {
486
+ return parseOpenArgs(args.slice(1));
487
+ }
488
+
455
489
  const urls = [];
456
490
  let inputFile = null;
457
491
  let outputFile = null;
@@ -1,5 +1,6 @@
1
1
  import { exec } from 'child_process';
2
2
  import { execSync } from 'child_process';
3
+ import fs from 'fs';
3
4
  import http from 'http';
4
5
  import os from 'os';
5
6
  import path from 'path';
@@ -82,8 +83,33 @@ function killEdgeProcesses(targetDir) {
82
83
  command = 'killall -9 "Microsoft Edge" 2>/dev/null; rm -f ~/Library/Caches/Microsoft\\ Edge/Singleton*; true';
83
84
  }
84
85
  } else if (platform === 'win32') {
85
- command = 'taskkill /F /IM msedge.exe 2>nul || exit 0';
86
- } else {
86
+ if (targetDir) {
87
+ try {
88
+ const ps1Path = path.join(os.tmpdir(), `kill-edge-${Date.now()}.ps1`);
89
+ const escapedDir = targetDir.replace(/'/g, "''");
90
+ const ps1Content = `$procs = Get-CimInstance Win32_Process -Filter "Name='msedge.exe'" 2>$null; ` +
91
+ `$count = 0; ` +
92
+ `foreach ($p in $procs) { ` +
93
+ ` if ($p.CommandLine -and $p.CommandLine -like "*${escapedDir}*") { ` +
94
+ ` Stop-Process -Id $p.ProcessId -Force -ErrorAction SilentlyContinue; ` +
95
+ ` $count++ ` +
96
+ ` } ` +
97
+ `}; ` +
98
+ `Get-ChildItem "$env:LOCALAPPDATA\\Microsoft\\Edge\\Singleton*" -ErrorAction SilentlyContinue | Remove-Item -Force; ` +
99
+ `Write-Output $count`;
100
+ fs.writeFileSync(ps1Path, ps1Content);
101
+ const result = execSync(
102
+ `powershell.exe -ExecutionPolicy Bypass -File "${ps1Path}"`
103
+ ).toString().trim();
104
+ fs.unlinkSync(ps1Path);
105
+ command = 'exit 0';
106
+ } catch {
107
+ command = 'taskkill /F /IM msedge.exe 2>nul || exit 0';
108
+ }
109
+ } else {
110
+ command = 'taskkill /F /IM msedge.exe 2>nul || exit 0';
111
+ }
112
+ } else {
87
113
  if (targetDir) {
88
114
  const escapedDir = targetDir.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
89
115
  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`;
@@ -192,3 +218,20 @@ export async function ensureBrowserReady(options = {}) {
192
218
  const browser = await chromium.connectOverCDP(`http://127.0.0.1:${port}`);
193
219
  return browser;
194
220
  }
221
+
222
+ export async function switchAccount(oldAccount, newAccount) {
223
+ console.error(`\n[账户切换] 从端口 ${oldAccount.port} -> ${newAccount.port}`);
224
+
225
+ await killEdgeProcesses(oldAccount.userDataDir);
226
+ await new Promise(r => setTimeout(r, 3000));
227
+
228
+ const newCdpOptions = {};
229
+ newCdpOptions.port = newAccount.port;
230
+ newCdpOptions.userDataDir = newAccount.userDataDir;
231
+
232
+ const browser = await ensureBrowserReady(newCdpOptions);
233
+
234
+ await new Promise(r => setTimeout(r, 10000));
235
+
236
+ return browser;
237
+ }
@@ -0,0 +1,54 @@
1
+ import os from 'os';
2
+ import path from 'path';
3
+
4
+ const TOTAL_ACCOUNTS = 10;
5
+ const BASE_PORT = 9222;
6
+ const MEMORY_THRESHOLD = 0.9;
7
+ const ROTATE_INTERVAL_MS = 2 * 60 * 60 * 1000;
8
+
9
+ class HealthChecker {
10
+ constructor() {
11
+ this.accounts = [];
12
+ for (let i = 0; i < TOTAL_ACCOUNTS; i++) {
13
+ const port = BASE_PORT + i;
14
+ const profile = `p${port}`;
15
+ this.accounts.push({
16
+ port,
17
+ profile,
18
+ userDataDir: path.join(
19
+ os.homedir(), 'Library', 'Application Support', `Microsoft Edge For Testing_${profile}`
20
+ ),
21
+ });
22
+ }
23
+ this.currentIndex = 0;
24
+ this.startTime = Date.now();
25
+ }
26
+
27
+ getCurrentAccount() {
28
+ return this.accounts[this.currentIndex];
29
+ }
30
+
31
+ getNextAccount() {
32
+ this.currentIndex = (this.currentIndex + 1) % TOTAL_ACCOUNTS;
33
+ return this.accounts[this.currentIndex];
34
+ }
35
+
36
+ check() {
37
+ const memUsage = (1 - os.freemem() / os.totalmem());
38
+ const memPercent = (memUsage * 100).toFixed(1);
39
+
40
+ if (memUsage >= MEMORY_THRESHOLD) {
41
+ return { shouldSwitch: true, reason: `系统内存 ${memPercent}%` };
42
+ }
43
+
44
+ const elapsed = Date.now() - this.startTime;
45
+ if (elapsed >= ROTATE_INTERVAL_MS) {
46
+ const mins = Math.round(elapsed / 60000);
47
+ return { shouldSwitch: true, reason: `运行 ${mins} 分钟` };
48
+ }
49
+
50
+ return { shouldSwitch: false };
51
+ }
52
+ }
53
+
54
+ export { HealthChecker };
@@ -136,6 +136,13 @@ const HELP_TEXT = [
136
136
  ' -s, --server <URL> 服务端地址(默认: http://127.0.0.1:3001)',
137
137
  ' 示例: tt-help attach -p 5 -i 10',
138
138
  '',
139
+ ' tt-help open <端口号>',
140
+ ' 打开指定端口的浏览器,用于配置 TikTok 登录账户',
141
+ ' 端口范围: 9222 - 9231(对应 explore 的 10 个内置账户)',
142
+ ' --list 列出所有端口和配置',
143
+ ' 示例: tt-help open 9222',
144
+ ' tt-help open --list',
145
+ '',
139
146
  ' config [show|set|reset]',
140
147
  ' config 查看当前配置',
141
148
  ' config set <key> <value> 设置配置(key: proxy, server, browser, userId, maxFollowing, maxFollowers, maxVideos, maxComments)',
package/src/main.js CHANGED
@@ -5,6 +5,7 @@ import { handleExplore } from './cli/explore.js';
5
5
  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
+ import { handleOpen } from './cli/open.js';
8
9
 
9
10
  async function main() {
10
11
  const parsed = parseArgs();
@@ -14,6 +15,7 @@ async function main() {
14
15
  case 'info': return handleInfo(parsed);
15
16
  case 'attach': return handleAttach(parsed);
16
17
  case 'watch': return handleWatch(parsed);
18
+ case 'open': return handleOpen(parsed);
17
19
  }
18
20
 
19
21
  const { urls, outputFile, outputFormat, exploreCount, showConfig: showCfg, showHelp, showVersion, customProxy, configAction, configKey, configValue } = parsed;
@@ -65,29 +65,18 @@ async function processExplore(page, username, options, log) {
65
65
  if (videoArray.length <= 0) {
66
66
  result.processed = true;
67
67
  result.noVideo = true;
68
- const hasErrorPage = await page.waitForFunction(
69
- (keywords) => {
70
- const text = document.body.innerText;
71
- return keywords.some(k => text.includes(k));
72
- },
73
- ["出错了", "很抱歉"],
74
- { timeout: 5000 }
75
- ).then(() => true).catch(() => false);
76
-
77
- if (hasErrorPage) {
78
- const pageError = await detectPageError(page);
79
- if (pageError === 'service_error') {
80
- result.error = `被封: ${username}`;
81
- result.errorStack = '';
82
- result.processed = false;
83
- result.noVideo = false;
84
- log(` @${username} 服务异常(service_error),标记错误`);
85
- } else if (pageError) {
86
- result.restricted = true;
87
- log(` @${username} 页面受限(${pageError}),标记跳过`);
88
- } else {
89
- log(` @${username} 没有视频,标记已处理`);
90
- }
68
+ const pageError = await detectPageError(page);
69
+ if (pageError === 'service_error') {
70
+ result.error = `被封: ${username}`;
71
+ result.errorStack = '';
72
+ result.processed = false;
73
+ result.noVideo = false;
74
+ log(` @${username} 服务异常(service_error),标记被封`);
75
+ } else if (pageError) {
76
+ result.restricted = true;
77
+ result.processed = true;
78
+ result.noVideo = true;
79
+ log(` @${username} 页面受限(${pageError}),标记跳过`);
91
80
  } else {
92
81
  log(` @${username} 没有视频,标记已处理`);
93
82
  }
@@ -181,6 +170,26 @@ async function processExplore(page, username, options, log) {
181
170
  result.error = e.message;
182
171
  result.errorStack = e.stack || "";
183
172
  log(` [错误] ${e.message}`);
173
+
174
+ // API 拦截失败时,复用 videoArray.length <= 0 的被封检测逻辑
175
+ if (e.message.includes('API拦截')) {
176
+ log(` [被封检测] 检查页面状态...`);
177
+ const hasErrorPage = await page.waitForFunction(
178
+ (keywords) => {
179
+ const text = document.body.innerText;
180
+ return keywords.some(k => text.includes(k));
181
+ },
182
+ ["出错了", "很抱歉"],
183
+ { timeout: 5000 }
184
+ ).then(() => true).catch(() => false);
185
+
186
+ if (hasErrorPage) {
187
+ result.error = `被封: ${username}`;
188
+ result.processed = false;
189
+ result.noVideo = false;
190
+ log(` @${username} API 拦截失败且页面异常,认定为被封`);
191
+ }
192
+ }
184
193
  }
185
194
 
186
195
  return result;