tt-help-cli-ycl 1.3.12 → 1.3.13

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.
Files changed (50) hide show
  1. package/README.md +17 -17
  2. package/cli.js +9 -9
  3. package/package.json +45 -45
  4. package/scripts/run-explore.bat +68 -68
  5. package/scripts/run-explore.ps1 +81 -81
  6. package/scripts/run-explore.sh +73 -73
  7. package/scripts/test-captcha-lib.mjs +68 -0
  8. package/scripts/test-captcha.mjs +81 -0
  9. package/scripts/test-incognito-lib.mjs +36 -0
  10. package/scripts/test-login-state.mjs +128 -0
  11. package/scripts/test-safe-click.mjs +45 -0
  12. package/src/cli/auto.js +186 -157
  13. package/src/cli/explore.js +227 -193
  14. package/src/cli/progress.js +111 -111
  15. package/src/cli/refresh.js +216 -0
  16. package/src/cli/scrape.js +47 -47
  17. package/src/cli/utils.js +18 -18
  18. package/src/cli/videos.js +41 -41
  19. package/src/cli/watch.js +31 -31
  20. package/src/lib/args.js +456 -402
  21. package/src/lib/browser/anti-detect.js +23 -23
  22. package/src/lib/browser/cdp.js +52 -10
  23. package/src/lib/browser/launch.js +43 -43
  24. package/src/lib/browser/page.js +146 -87
  25. package/src/lib/constants.js +119 -115
  26. package/src/lib/delay.js +54 -54
  27. package/src/lib/explore-fetch.js +118 -118
  28. package/src/lib/fetcher.js +45 -45
  29. package/src/lib/filter.js +66 -66
  30. package/src/lib/io.js +54 -54
  31. package/src/lib/output.js +80 -80
  32. package/src/lib/parser.js +47 -47
  33. package/src/lib/retry.js +45 -45
  34. package/src/lib/scrape.js +40 -40
  35. package/src/lib/url.js +52 -52
  36. package/src/main.js +2 -0
  37. package/src/results/user-videos-bar.lar.lar.moeta.json +37 -0
  38. package/src/scraper/auto-core.js +203 -194
  39. package/src/scraper/core.js +211 -190
  40. package/src/scraper/explore-core.js +180 -171
  41. package/src/scraper/modules/captcha-handler.js +114 -114
  42. package/src/scraper/modules/comment-extractor.js +74 -69
  43. package/src/scraper/modules/follow-extractor.js +121 -121
  44. package/src/scraper/modules/guess-extractor.js +51 -51
  45. package/src/scraper/modules/page-helpers.js +48 -48
  46. package/src/scraper/refresh-core.js +179 -0
  47. package/src/videos/core.js +126 -126
  48. package/src/watch/data-store.js +431 -302
  49. package/src/watch/public/index.html +721 -701
  50. package/src/watch/server.js +483 -359
@@ -1,23 +1,23 @@
1
- export function getAntiDetectScript() {
2
- return () => {
3
- Object.defineProperty(navigator, 'webdriver', { get: () => false });
4
-
5
- if (!window.chrome) {
6
- window.chrome = { runtime: {} };
7
- }
8
-
9
- const originalQuery = window.navigator.permissions.query;
10
- window.navigator.permissions.query = (params) =>
11
- params.name === 'notifications'
12
- ? Promise.resolve({ state: Notification.permission })
13
- : originalQuery(params);
14
-
15
- Object.defineProperty(navigator, 'languages', {
16
- get: () => ['en-US', 'en'],
17
- });
18
-
19
- Object.defineProperty(navigator, 'plugins', {
20
- get: () => [1, 2, 3, 4, 5],
21
- });
22
- };
23
- }
1
+ export function getAntiDetectScript() {
2
+ return () => {
3
+ Object.defineProperty(navigator, 'webdriver', { get: () => false });
4
+
5
+ if (!window.chrome) {
6
+ window.chrome = { runtime: {} };
7
+ }
8
+
9
+ const originalQuery = window.navigator.permissions.query;
10
+ window.navigator.permissions.query = (params) =>
11
+ params.name === 'notifications'
12
+ ? Promise.resolve({ state: Notification.permission })
13
+ : originalQuery(params);
14
+
15
+ Object.defineProperty(navigator, 'languages', {
16
+ get: () => ['en-US', 'en'],
17
+ });
18
+
19
+ Object.defineProperty(navigator, 'plugins', {
20
+ get: () => [1, 2, 3, 4, 5],
21
+ });
22
+ };
23
+ }
@@ -1,4 +1,5 @@
1
1
  import { exec } from 'child_process';
2
+ import { execSync } from 'child_process';
2
3
  import http from 'http';
3
4
  import os from 'os';
4
5
  import path from 'path';
@@ -55,16 +56,40 @@ function checkEdgeArgs() {
55
56
  });
56
57
  }
57
58
 
58
- function killEdgeProcesses() {
59
+ function killEdgeProcesses(targetDir) {
59
60
  return new Promise(resolve => {
60
61
  const platform = os.platform();
61
62
  let command;
62
63
  if (platform === 'darwin') {
63
- command = 'killall -9 "Microsoft Edge" 2>/dev/null; rm -f ~/Library/Caches/Microsoft\\ Edge/Singleton*; true';
64
+ if (targetDir) {
65
+ let pids = '';
66
+ try {
67
+ // ps aux 输出中 --user-data-dir= 后面没有引号
68
+ // 用路径精确匹配,结尾加空格或行尾避免子串误杀
69
+ const escapedDir = targetDir.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
70
+ pids = execSync(
71
+ `ps aux | grep "[M]icrosoft Edge" | grep -v "Helper\\|crashpad" | grep -E -- '--user-data-dir=${escapedDir}($|[^A-Za-z0-9_])' | awk '{print $2}'`
72
+ ).toString().trim();
73
+ } catch (e) {
74
+ pids = '';
75
+ }
76
+ if (pids) {
77
+ command = `kill -9 ${pids} 2>/dev/null; rm -f ~/Library/Caches/Microsoft\\ Edge/Singleton*; true`;
78
+ } else {
79
+ command = 'true';
80
+ }
81
+ } else {
82
+ command = 'killall -9 "Microsoft Edge" 2>/dev/null; rm -f ~/Library/Caches/Microsoft\\ Edge/Singleton*; true';
83
+ }
64
84
  } else if (platform === 'win32') {
65
85
  command = 'taskkill /F /IM msedge.exe 2>nul || exit 0';
66
86
  } else {
67
- command = 'pkill -9 -f msedge 2>/dev/null; true';
87
+ if (targetDir) {
88
+ const escapedDir = targetDir.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
89
+ 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`;
90
+ } else {
91
+ command = 'pkill -9 -f msedge 2>/dev/null; true';
92
+ }
68
93
  }
69
94
  exec(command, () => resolve());
70
95
  });
@@ -76,12 +101,27 @@ function launchEdgeWithCDP(port, userDataDir) {
76
101
  const edgePath = getEdgePath();
77
102
  let command;
78
103
 
104
+ const extraArgs = [
105
+ `--remote-debugging-port=${port}`,
106
+ `--user-data-dir="${userDataDir}"`,
107
+ '--disable-blink-features=AutomationControlled',
108
+ '--no-first-run',
109
+ '--no-default-browser-check',
110
+ '--password-store=basic',
111
+ '--disable-background-mode',
112
+ '--disable-component-update',
113
+ '--disable-crash-reporter',
114
+ '--disable-breakpad',
115
+ '--disable-background-networking',
116
+ '--disable-sync',
117
+ ].join(' ');
118
+
79
119
  if (platform === 'darwin') {
80
- command = `open -a ${edgePath} --args --remote-debugging-port=${port} --user-data-dir="${userDataDir}"`;
120
+ command = `open -a ${edgePath} --new --args ${extraArgs}`;
81
121
  } else if (platform === 'win32') {
82
- command = `start msedge --remote-debugging-port=${port} --user-data-dir="${userDataDir}"`;
122
+ command = `start msedge ${extraArgs}`;
83
123
  } else {
84
- command = `msedge --remote-debugging-port=${port} --user-data-dir="${userDataDir}" &`;
124
+ command = `msedge ${extraArgs} &`;
85
125
  }
86
126
 
87
127
  exec(command, (err) => {
@@ -101,6 +141,8 @@ async function waitForCDP(port, timeout = 30000, interval = 1000) {
101
141
  return false;
102
142
  }
103
143
 
144
+ export { killEdgeProcesses };
145
+
104
146
  export async function ensureBrowserReady(options = {}) {
105
147
  const port = options.port || DEFAULT_CDP_PORT;
106
148
  const userDataDir = options.userDataDir || DEFAULT_USER_DATA_DIR;
@@ -113,8 +155,8 @@ export async function ensureBrowserReady(options = {}) {
113
155
  if (!isCustom) {
114
156
  const edgeArgsValid = await checkEdgeArgs();
115
157
  if (!edgeArgsValid) {
116
- console.error('Edge 已运行但启动参数不完整,正在重启...');
117
- await killEdgeProcesses();
158
+ console.error(`Edge 已运行但启动参数不完整,正在重启端口 ${port}...`);
159
+ await killEdgeProcesses(userDataDir);
118
160
  await new Promise(r => setTimeout(r, 3000));
119
161
  needLaunch = true;
120
162
  }
@@ -128,7 +170,7 @@ export async function ensureBrowserReady(options = {}) {
128
170
  const edgeRunning = await isEdgeRunning();
129
171
  if (edgeRunning) {
130
172
  console.error(`Edge 已运行但 CDP 端口 ${port} 未启用,正在重启...`);
131
- await killEdgeProcesses();
173
+ await killEdgeProcesses(userDataDir);
132
174
  await new Promise(r => setTimeout(r, 3000));
133
175
  } else {
134
176
  console.error(`CDP 端口 ${port} 未就绪,正在启动 Edge 浏览器...`);
@@ -141,7 +183,7 @@ export async function ensureBrowserReady(options = {}) {
141
183
  if (!launched) {
142
184
  throw new Error(
143
185
  `等待 CDP 端口 ${port} 超时。请确认 Edge 浏览器已安装,\n` +
144
- `或手动启动: Microsoft Edge --remote-debugging-port=${port}`
186
+ `或手动启动: Microsoft Edge --remote-debugging-port=${port} [参见 cdp.js extraArgs]`
145
187
  );
146
188
  }
147
189
  console.error('浏览器启动成功');
@@ -1,43 +1,43 @@
1
- import { accessSync } from 'fs';
2
-
3
- export function detectBrowser() {
4
- const isMac = process.platform === 'darwin';
5
- const isWin = process.platform === 'win32';
6
- const isLinux = process.platform === 'linux';
7
-
8
- const paths = [];
9
-
10
- if (isMac) {
11
- paths.push(
12
- '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
13
- '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
14
- '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
15
- '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
16
- );
17
- } else if (isWin) {
18
- const localAppData = process.env.LOCALAPPDATA || '';
19
- const programFiles = process.env.PROGRAMFILES || '';
20
- const programFilesX86 = process.env['PROGRAMFILES(X86)'] || '';
21
- paths.push(
22
- `${programFiles}\\Google\\Chrome\\Application\\chrome.exe`,
23
- `${programFilesX86}\\Google\\Chrome\\Application\\chrome.exe`,
24
- `${localAppData}\\Google\\Chrome\\Application\\chrome.exe`,
25
- `${programFiles}\\Microsoft\\Edge\\Application\\msedge.exe`,
26
- `${programFilesX86}\\Microsoft\\Edge\\Application\\msedge.exe`,
27
- );
28
- } else if (isLinux) {
29
- paths.push(
30
- '/usr/bin/google-chrome',
31
- '/usr/bin/google-chrome-stable',
32
- '/usr/bin/chromium-browser',
33
- '/usr/bin/chromium',
34
- '/snap/bin/chromium',
35
- '/usr/bin/microsoft-edge',
36
- );
37
- }
38
-
39
- for (const p of paths) {
40
- try { accessSync(p); return p; } catch { /* not found */ }
41
- }
42
- return null;
43
- }
1
+ import { accessSync } from 'fs';
2
+
3
+ export function detectBrowser() {
4
+ const isMac = process.platform === 'darwin';
5
+ const isWin = process.platform === 'win32';
6
+ const isLinux = process.platform === 'linux';
7
+
8
+ const paths = [];
9
+
10
+ if (isMac) {
11
+ paths.push(
12
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
13
+ '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
14
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
15
+ '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
16
+ );
17
+ } else if (isWin) {
18
+ const localAppData = process.env.LOCALAPPDATA || '';
19
+ const programFiles = process.env.PROGRAMFILES || '';
20
+ const programFilesX86 = process.env['PROGRAMFILES(X86)'] || '';
21
+ paths.push(
22
+ `${programFiles}\\Google\\Chrome\\Application\\chrome.exe`,
23
+ `${programFilesX86}\\Google\\Chrome\\Application\\chrome.exe`,
24
+ `${localAppData}\\Google\\Chrome\\Application\\chrome.exe`,
25
+ `${programFiles}\\Microsoft\\Edge\\Application\\msedge.exe`,
26
+ `${programFilesX86}\\Microsoft\\Edge\\Application\\msedge.exe`,
27
+ );
28
+ } else if (isLinux) {
29
+ paths.push(
30
+ '/usr/bin/google-chrome',
31
+ '/usr/bin/google-chrome-stable',
32
+ '/usr/bin/chromium-browser',
33
+ '/usr/bin/chromium',
34
+ '/snap/bin/chromium',
35
+ '/usr/bin/microsoft-edge',
36
+ );
37
+ }
38
+
39
+ for (const p of paths) {
40
+ try { accessSync(p); return p; } catch { /* not found */ }
41
+ }
42
+ return null;
43
+ }
@@ -1,87 +1,146 @@
1
- import { delay } from '../delay.js';
2
- import { retryWithBackoff } from '../retry.js';
3
- import { getDelayConfig } from '../delay.js';
4
-
5
- export async function isLoggedIn(page) {
6
- return page.evaluate(() => {
7
- // 已登录时会出现在个人主页区域的元素
8
- const hasProfileContainer = !!document.querySelector(
9
- '[class*="DivProfileContainer"], [class*="DivUserContainer"]'
10
- );
11
- // 已登录时顶部导航栏有用户相关菜单
12
- const hasUserMenu = !!document.querySelector(
13
- '[class*="UserMenu"], [class*="user-menu"], [class*="CurrentUserInfo"]'
14
- );
15
- // 有登录按钮说明未登录
16
- const hasLoginButton = Array.from(document.querySelectorAll('button, [role="button"]'))
17
- .some(el => /^(登录|Log in|Sign in)$/i.test(el.textContent.trim()));
18
-
19
- return (hasProfileContainer || hasUserMenu) && !hasLoginButton;
20
- });
21
- }
22
-
23
- export async function closeCommentPanel(page) {
24
- await page.evaluate(() => {
25
- const rightPanel = document.querySelector('[class*="RightPanelContainer"]');
26
- if (rightPanel) {
27
- const tabContainer = rightPanel.querySelector('[class*="TabContainer"]');
28
- if (tabContainer) {
29
- const closeOverlay = tabContainer.querySelector('div:last-child');
30
- if (closeOverlay) closeOverlay.click();
31
- }
32
- }
33
- });
34
- }
35
-
36
- export async function ensureTikTokPage(browser, url) {
37
- const contexts = browser.contexts();
38
- let page = null;
39
-
40
- for (const ctx of contexts) {
41
- for (const p of ctx.pages()) {
42
- if (p.url().includes('tiktok.com')) {
43
- page = p;
44
- break;
45
- }
46
- }
47
- if (page) break;
48
- }
49
-
50
- if (!page) {
51
- console.error('未找到 TikTok 页面,正在打开...');
52
- const defaultCtx = browser.contexts()[0];
53
- page = await defaultCtx.newPage();
54
- await retryWithBackoff(() => page.goto(url, { waitUntil: 'load', timeout: 30000 }));
55
- const config = getDelayConfig();
56
- await delay(Math.round(config.switchMax * 0.5), config.switchMax);
57
- console.error('TikTok 页面已打开');
58
- }
59
-
60
- return page;
61
- }
62
-
63
- export async function findTikTokPage(browser) {
64
- const contexts = browser.contexts();
65
- for (const ctx of contexts) {
66
- for (const p of ctx.pages()) {
67
- if (p.url().includes('tiktok.com')) return p;
68
- }
69
- }
70
- return null;
71
- }
72
-
73
- export async function getOrCreatePage(browser) {
74
- let page = await findTikTokPage(browser);
75
- if (!page) {
76
- const defaultCtx = browser.contexts()[0] || await browser.newContext();
77
- page = await defaultCtx.newPage();
78
- }
79
- return page;
80
- }
81
-
82
- export function assertPageUrl(page, expectedPath) {
83
- const actual = page.url();
84
- if (!actual.includes(expectedPath)) {
85
- throw new Error(`[代理错误] 预期访问 ${expectedPath},实际跳转到了 ${actual}`);
86
- }
87
- }
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');
9
+
10
+ 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',
17
+ ];
18
+
19
+ export function isBrowserClosedError(error) {
20
+ if (!error) return false;
21
+ const msg = error.message || error.toString() || '';
22
+ return BROWSER_CLOSED_PATTERNS.some(p => msg.includes(p));
23
+ }
24
+
25
+ export async function relaunchBrowser(cdpOptions, port) {
26
+ console.error(` [浏览器] 浏览器已关闭,正在重启 (端口 ${port})...`);
27
+ const targetDir = cdpOptions.userDataDir || DEFAULT_USER_DATA_DIR;
28
+ // kill 并清理 Singleton 锁文件,确保 Edge 能启动新实例
29
+ await killEdgeProcesses(targetDir);
30
+ await new Promise(r => setTimeout(r, 3000));
31
+ // 确保端口已释放后再启动
32
+ let retries = 0;
33
+ while (retries < 5) {
34
+ try {
35
+ return await ensureBrowserReady(cdpOptions);
36
+ } catch (e) {
37
+ if (e.message && e.message.includes('setDownloadBehavior')) {
38
+ retries++;
39
+ console.error(` [浏览器] CDP 连接异常,重试 ${retries}/5...`);
40
+ await new Promise(r => setTimeout(r, 3000));
41
+ await killEdgeProcesses(targetDir);
42
+ await new Promise(r => setTimeout(r, 5000));
43
+ continue;
44
+ }
45
+ throw e;
46
+ }
47
+ }
48
+ throw new Error('浏览器重启失败,CDP 连接异常');
49
+ }
50
+
51
+ export async function withBrowserRecovery(fn, browser, page, cdpOptions, port) {
52
+ try {
53
+ return await fn();
54
+ } catch (e) {
55
+ if (isBrowserClosedError(e)) {
56
+ const newBrowser = await relaunchBrowser(cdpOptions, port);
57
+ const newPage = await getOrCreatePage(newBrowser);
58
+ return await fn(newBrowser, newPage);
59
+ }
60
+ throw e;
61
+ }
62
+ }
63
+
64
+ export async function isLoggedIn(page) {
65
+ return page.evaluate(() => {
66
+ // 已登录时会出现在个人主页区域的元素
67
+ const hasProfileContainer = !!document.querySelector(
68
+ '[class*="DivProfileContainer"], [class*="DivUserContainer"]'
69
+ );
70
+ // 已登录时顶部导航栏有用户相关菜单
71
+ const hasUserMenu = !!document.querySelector(
72
+ '[class*="UserMenu"], [class*="user-menu"], [class*="CurrentUserInfo"]'
73
+ );
74
+ // 有登录按钮说明未登录
75
+ const hasLoginButton = Array.from(document.querySelectorAll('button, [role="button"]'))
76
+ .some(el => /^(登录|Log in|Sign in)$/i.test(el.textContent.trim()));
77
+
78
+ return (hasProfileContainer || hasUserMenu) && !hasLoginButton;
79
+ });
80
+ }
81
+
82
+ export async function closeCommentPanel(page) {
83
+ await page.evaluate(() => {
84
+ const rightPanel = document.querySelector('[class*="RightPanelContainer"]');
85
+ if (rightPanel) {
86
+ const tabContainer = rightPanel.querySelector('[class*="TabContainer"]');
87
+ if (tabContainer) {
88
+ const closeOverlay = tabContainer.querySelector('div:last-child');
89
+ if (closeOverlay) closeOverlay.click();
90
+ }
91
+ }
92
+ });
93
+ }
94
+
95
+ export async function ensureTikTokPage(browser, url) {
96
+ const contexts = browser.contexts();
97
+ let page = null;
98
+
99
+ for (const ctx of contexts) {
100
+ for (const p of ctx.pages()) {
101
+ if (p.url().includes('tiktok.com')) {
102
+ page = p;
103
+ break;
104
+ }
105
+ }
106
+ if (page) break;
107
+ }
108
+
109
+ if (!page) {
110
+ console.error('未找到 TikTok 页面,正在打开...');
111
+ const defaultCtx = browser.contexts()[0];
112
+ page = await defaultCtx.newPage();
113
+ await retryWithBackoff(() => page.goto(url, { waitUntil: 'load', timeout: 30000 }));
114
+ const config = getDelayConfig();
115
+ await delay(Math.round(config.switchMax * 0.5), config.switchMax);
116
+ console.error('TikTok 页面已打开');
117
+ }
118
+
119
+ return page;
120
+ }
121
+
122
+ export async function findTikTokPage(browser) {
123
+ const contexts = browser.contexts();
124
+ for (const ctx of contexts) {
125
+ for (const p of ctx.pages()) {
126
+ if (p.url().includes('tiktok.com')) return p;
127
+ }
128
+ }
129
+ return null;
130
+ }
131
+
132
+ export async function getOrCreatePage(browser) {
133
+ let page = await findTikTokPage(browser);
134
+ if (!page) {
135
+ const defaultCtx = browser.contexts()[0] || await browser.newContext();
136
+ page = await defaultCtx.newPage();
137
+ }
138
+ return page;
139
+ }
140
+
141
+ export function assertPageUrl(page, expectedPath) {
142
+ const actual = page.url();
143
+ if (!actual.includes(expectedPath)) {
144
+ throw new Error(`[代理错误] 预期访问 ${expectedPath},实际跳转到了 ${actual}`);
145
+ }
146
+ }