tt-help-cli-ycl 1.3.11 → 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 (61) hide show
  1. package/README.md +17 -17
  2. package/cli.js +9 -9
  3. package/package.json +45 -46
  4. package/{bat → scripts}/run-explore.bat +68 -68
  5. package/{bat → scripts}/run-explore.ps1 +81 -81
  6. package/{bat → 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/config.js +116 -0
  14. package/src/cli/explore-default.js +83 -0
  15. package/src/cli/explore.js +227 -181
  16. package/src/cli/progress.js +111 -111
  17. package/src/cli/refresh.js +216 -0
  18. package/src/cli/scrape.js +47 -47
  19. package/src/cli/utils.js +18 -18
  20. package/src/cli/videos.js +41 -41
  21. package/src/cli/watch.js +31 -31
  22. package/src/lib/args.js +456 -391
  23. package/src/lib/browser/anti-detect.js +23 -23
  24. package/src/lib/browser/cdp.js +194 -142
  25. package/src/lib/browser/launch.js +43 -43
  26. package/src/lib/browser/page.js +146 -87
  27. package/src/lib/constants.js +119 -119
  28. package/src/lib/delay.js +54 -54
  29. package/src/lib/explore-fetch.js +118 -118
  30. package/src/lib/fetcher.js +45 -45
  31. package/src/lib/filter.js +66 -66
  32. package/src/lib/io.js +54 -54
  33. package/src/lib/output.js +80 -80
  34. package/src/{scraper/modules/page-error-detector.mjs → lib/page-error-detector.js} +70 -70
  35. package/src/lib/parser.js +47 -47
  36. package/src/lib/retry.js +45 -45
  37. package/src/lib/scrape.js +40 -40
  38. package/src/{scraper/modules/scroll-collector.mjs → lib/scroll-collector.js} +231 -189
  39. package/src/lib/url.js +52 -52
  40. package/src/main.js +48 -0
  41. package/src/results/user-videos-bar.lar.lar.moeta.json +37 -0
  42. package/src/scraper/{auto-core.mjs → auto-core.js} +203 -194
  43. package/src/scraper/{core.mjs → core.js} +211 -190
  44. package/src/scraper/{explore-core.mjs → explore-core.js} +180 -171
  45. package/src/scraper/modules/{captcha-handler.mjs → captcha-handler.js} +114 -114
  46. package/src/scraper/modules/{comment-extractor.mjs → comment-extractor.js} +74 -69
  47. package/src/scraper/modules/{follow-extractor.mjs → follow-extractor.js} +121 -121
  48. package/src/scraper/modules/{guess-extractor.mjs → guess-extractor.js} +51 -51
  49. package/src/scraper/modules/page-error-detector.js +1 -0
  50. package/src/scraper/modules/{page-helpers.mjs → page-helpers.js} +48 -48
  51. package/src/scraper/modules/scroll-collector.js +8 -0
  52. package/src/scraper/refresh-core.js +179 -0
  53. package/src/videos/{core.mjs → core.js} +126 -126
  54. package/src/watch/data-store.js +431 -0
  55. package/src/watch/public/index.html +721 -690
  56. package/src/watch/{server.mjs → server.js} +484 -349
  57. package/src/main.mjs +0 -234
  58. package/src/test-auto-follow.cjs +0 -109
  59. package/src/test-extractors.cjs +0 -75
  60. package/src/test-follow.cjs +0 -41
  61. package/src/watch/data-store.mjs +0 -274
@@ -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
+ }
@@ -1,119 +1,119 @@
1
- import { join, dirname } from 'path';
2
- import { readFileSync, writeFileSync, existsSync } from 'fs';
3
- import { fileURLToPath } from 'url';
4
- import { USER_SECTION_SIZE } from './parser.js';
5
-
6
- const __filename = fileURLToPath(import.meta.url);
7
- const __dirname = dirname(__filename);
8
- const homeDir = process.env.HOME || process.env.USERPROFILE || '';
9
- const configPath = join(homeDir, '.tt-help.json');
10
-
11
- const DEFAULT_PROXY = 'http://127.0.0.1:7897';
12
- const DEFAULT_OUTPUT = 'tiktok_data.json';
13
-
14
- let proxy = DEFAULT_PROXY;
15
- let server = 'http://127.0.0.1:3001';
16
- let configFile = null;
17
- let browser = null;
18
- let userId = null;
19
-
20
- try {
21
- if (existsSync(configPath)) {
22
- const cfg = JSON.parse(readFileSync(configPath, 'utf-8'));
23
- if (cfg.proxy) {
24
- proxy = cfg.proxy;
25
- }
26
- if (cfg.server) {
27
- server = cfg.server;
28
- }
29
- if (cfg.browser) {
30
- browser = cfg.browser;
31
- }
32
- if (cfg.userId) {
33
- userId = cfg.userId;
34
- }
35
- configFile = configPath;
36
- }
37
- } catch {
38
- // no config file
39
- }
40
-
41
- function saveBrowser(path) {
42
- const cfg = existsSync(configPath) ? JSON.parse(readFileSync(configPath, 'utf-8')) : {};
43
- cfg.browser = path;
44
- writeFileSync(configPath, JSON.stringify(cfg, null, 2), 'utf-8');
45
- browser = path;
46
- configFile = configPath;
47
- }
48
-
49
- function saveUserId(id) {
50
- const cfg = existsSync(configPath) ? JSON.parse(readFileSync(configPath, 'utf-8')) : {};
51
- cfg.userId = id;
52
- writeFileSync(configPath, JSON.stringify(cfg, null, 2), 'utf-8');
53
- userId = id;
54
- configFile = configPath;
55
- }
56
-
57
- const HELP_TEXT = [
58
- '用法: tt-help explore <用户名> [preset] [选项]',
59
- '',
60
- ' 支持多个用户名: tt-help explore @user1 @user2 --server http://127.0.0.1:3001',
61
- ' 预设: fast, normal(默认), slow, stealth',
62
- ' 选项:',
63
- ' --server <URL> 服务端地址,默认 http://127.0.0.1:3001',
64
- ' --location <国家代码> 国家筛选,默认 ES',
65
- ' --max-comments <数量> 每视频最大评论数,默认 10',
66
- ' --max-guess <数量> 每视频最大猜你喜欢数,默认 0',
67
- ' --enable-follow 启用关注/粉丝提取(默认启用)',
68
- ' --disable-follow 禁用关注/粉丝提取',
69
- ' --max-following <数量> 最大获取关注数,默认 5',
70
- ' --max-followers <数量> 最大获取粉丝数,默认 5',
71
- ' --max-users <数量> 最大处理用户数,默认无限制',
72
- ' 全局选项:',
73
- ' config 查看当前配置',
74
- ' config set <key> <value> 设置配置(key: proxy, server, browser, userId)',
75
- ' config reset 重置所有配置为默认',
76
- ' -h, --help 显示帮助',
77
- ' --version 显示版本号',
78
- '',
79
- ' 示例: tt-help explore qiqi23280 fast --location ES --max-comments 50',
80
- ' tt-help config set server http://127.0.0.1:3001',
81
- ];
82
-
83
- function getConfigText() {
84
- let currentUserId = userId;
85
- if (!currentUserId && existsSync(configPath)) {
86
- try {
87
- const cfg = JSON.parse(readFileSync(configPath, 'utf-8'));
88
- if (cfg.userId) currentUserId = cfg.userId;
89
- } catch {}
90
- }
91
- return [
92
- 'tt-help v1.0.1',
93
- '',
94
- '配置:',
95
- ` 代理: ${proxy}`,
96
- ` 服务端: ${server}`,
97
- ` 浏览器: ${browser || '未配置(将自动探测或回退)'}`,
98
- ` 用户号: ${currentUserId || '未设置(首次运行 auto 自动创建)'}`,
99
- ` 输出格式: json`,
100
- ` 默认输出: ${DEFAULT_OUTPUT}`,
101
- ` 配置文件: ${configFile || '无(使用默认值)'}`,
102
- ];
103
- }
104
-
105
- export {
106
- proxy,
107
- server,
108
- configFile,
109
- configPath,
110
- DEFAULT_PROXY,
111
- DEFAULT_OUTPUT,
112
- USER_SECTION_SIZE,
113
- HELP_TEXT,
114
- browser,
115
- userId,
116
- saveBrowser,
117
- saveUserId,
118
- getConfigText,
119
- };
1
+ import { join, dirname } from 'path';
2
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '';
8
+ const configPath = join(homeDir, '.tt-help.json');
9
+
10
+ const DEFAULT_PROXY = 'http://127.0.0.1:7897';
11
+ const DEFAULT_OUTPUT = 'tiktok_data.json';
12
+
13
+ let proxy = DEFAULT_PROXY;
14
+ let server = 'http://127.0.0.1:3001';
15
+ let configFile = null;
16
+ let browser = null;
17
+ let userId = null;
18
+
19
+ try {
20
+ if (existsSync(configPath)) {
21
+ const cfg = JSON.parse(readFileSync(configPath, 'utf-8'));
22
+ if (cfg.proxy) {
23
+ proxy = cfg.proxy;
24
+ }
25
+ if (cfg.server) {
26
+ server = cfg.server;
27
+ }
28
+ if (cfg.browser) {
29
+ browser = cfg.browser;
30
+ }
31
+ if (cfg.userId) {
32
+ userId = cfg.userId;
33
+ }
34
+ configFile = configPath;
35
+ }
36
+ } catch {
37
+ // no config file
38
+ }
39
+
40
+ function saveBrowser(path) {
41
+ const cfg = existsSync(configPath) ? JSON.parse(readFileSync(configPath, 'utf-8')) : {};
42
+ cfg.browser = path;
43
+ writeFileSync(configPath, JSON.stringify(cfg, null, 2), 'utf-8');
44
+ browser = path;
45
+ configFile = configPath;
46
+ }
47
+
48
+ function saveUserId(id) {
49
+ const cfg = existsSync(configPath) ? JSON.parse(readFileSync(configPath, 'utf-8')) : {};
50
+ cfg.userId = id;
51
+ writeFileSync(configPath, JSON.stringify(cfg, null, 2), 'utf-8');
52
+ userId = id;
53
+ configFile = configPath;
54
+ }
55
+
56
+ const HELP_TEXT = [
57
+ '用法: tt-help explore <用户名> [preset] [选项]',
58
+ '',
59
+ ' 支持多个用户名: tt-help explore @user1 @user2 --server http://127.0.0.1:3001',
60
+ ' 预设: fast, normal(默认), slow, stealth',
61
+ ' 选项:',
62
+ ' --server <URL> 服务端地址,默认 http://127.0.0.1:3001',
63
+ ' --location <国家代码> 国家筛选,逗号分隔,默认 PL,NL,BE,DE,FR,IT,ES,IE',
64
+ ' --max-comments <数量> 每视频最大评论数,默认 10',
65
+ ' --max-guess <数量> 每视频最大猜你喜欢数,默认 0',
66
+ ' --enable-follow 启用关注/粉丝提取(默认启用)',
67
+ ' --disable-follow 禁用关注/粉丝提取',
68
+ ' --max-following <数量> 最大获取关注数,默认 5',
69
+ ' --max-followers <数量> 最大获取粉丝数,默认 5',
70
+ ' --max-users <数量> 最大处理用户数,默认无限制',
71
+ ' --port <端口号> CDP 端口,默认 9222',
72
+ ' --profile <名称> 浏览器 Profile 名称',
73
+ ' --user-id <编号> 客户端编号(设备ID),默认自动生成',
74
+ ' 全局选项:',
75
+ ' config 查看当前配置',
76
+ ' config set <key> <value> 设置配置(key: proxy, server, browser, userId)',
77
+ ' config reset 重置所有配置为默认',
78
+ ' -h, --help 显示帮助',
79
+ ' --version 显示版本号',
80
+ '',
81
+ ' 示例: tt-help explore qiqi23280 fast --location ES --max-comments 50',
82
+ ' tt-help explore @user1 --user-id my-device-001 --port 9333',
83
+ ' tt-help config set server http://127.0.0.1:3001',
84
+ ];
85
+
86
+ function getConfigText() {
87
+ let currentUserId = userId;
88
+ if (!currentUserId && existsSync(configPath)) {
89
+ try {
90
+ const cfg = JSON.parse(readFileSync(configPath, 'utf-8'));
91
+ if (cfg.userId) currentUserId = cfg.userId;
92
+ } catch {}
93
+ }
94
+ return [
95
+ 'tt-help v1.0.1',
96
+ '',
97
+ '配置:',
98
+ ` 代理: ${proxy}`,
99
+ ` 服务端: ${server}`,
100
+ ` 浏览器: ${browser || '未配置(将自动探测或回退)'}`,
101
+ ` 用户号: ${currentUserId || '未设置(首次运行 auto 自动创建)'}`,
102
+ ` 输出格式: json`,
103
+ ` 默认输出: ${DEFAULT_OUTPUT}`,
104
+ ` 配置文件: ${configFile || '无(使用默认值)'}`,
105
+ ];
106
+ }
107
+
108
+ export {
109
+ proxy,
110
+ server,
111
+ configPath,
112
+ DEFAULT_PROXY,
113
+ HELP_TEXT,
114
+ browser,
115
+ userId,
116
+ saveBrowser,
117
+ saveUserId,
118
+ getConfigText,
119
+ };
package/src/lib/delay.js CHANGED
@@ -1,54 +1,54 @@
1
- export const DELAY_PRESETS = {
2
- fast: { switchMax: 300, commentMax: 200, fast: true },
3
- normal: { switchMax: 1500, commentMax: 800 },
4
- slow: { switchMax: 3000, commentMax: 2000 },
5
- stealth: { switchMax: 5000, commentMax: 3500 },
6
- };
7
-
8
- const delayConfig = {
9
- switchMax: 2500,
10
- commentMax: 1500,
11
- fast: false,
12
- };
13
-
14
- export function setDelayConfig(config) {
15
- if (typeof config === 'string') {
16
- const preset = DELAY_PRESETS[config.toLowerCase()];
17
- if (!preset) {
18
- throw new Error(
19
- `未知的延迟预设: ${config}\n可用预设: ${Object.keys(DELAY_PRESETS).join(', ')}`
20
- );
21
- }
22
- delayConfig.switchMax = preset.switchMax;
23
- delayConfig.commentMax = preset.commentMax;
24
- delayConfig.fast = preset.fast || false;
25
- } else if (typeof config === 'object') {
26
- if (config.switchMax) delayConfig.switchMax = config.switchMax;
27
- if (config.commentMax) delayConfig.commentMax = config.commentMax;
28
- delayConfig.fast = config.fast || false;
29
- }
30
- }
31
-
32
- export function getDelayConfig() {
33
- return { ...delayConfig };
34
- }
35
-
36
- export function listDelayPresets() {
37
- return DELAY_PRESETS;
38
- }
39
-
40
- export function delay(min, max) {
41
- const lo = Math.min(min, max);
42
- const hi = Math.max(min, max);
43
- let ms;
44
- if (delayConfig.fast) {
45
- ms = 0;
46
- } else {
47
- ms = Math.floor(Math.random() * (hi - lo + 1)) + lo;
48
- }
49
- return new Promise(r => setTimeout(r, ms));
50
- }
51
-
52
- export function randomDelay(min = 200, max = 600) {
53
- return delay(min, max);
54
- }
1
+ export const DELAY_PRESETS = {
2
+ fast: { switchMax: 300, commentMax: 200, fast: true },
3
+ normal: { switchMax: 1500, commentMax: 800 },
4
+ slow: { switchMax: 3000, commentMax: 2000 },
5
+ stealth: { switchMax: 5000, commentMax: 3500 },
6
+ };
7
+
8
+ const delayConfig = {
9
+ switchMax: 2500,
10
+ commentMax: 1500,
11
+ fast: false,
12
+ };
13
+
14
+ export function setDelayConfig(config) {
15
+ if (typeof config === 'string') {
16
+ const preset = DELAY_PRESETS[config.toLowerCase()];
17
+ if (!preset) {
18
+ throw new Error(
19
+ `未知的延迟预设: ${config}\n可用预设: ${Object.keys(DELAY_PRESETS).join(', ')}`
20
+ );
21
+ }
22
+ delayConfig.switchMax = preset.switchMax;
23
+ delayConfig.commentMax = preset.commentMax;
24
+ delayConfig.fast = preset.fast || false;
25
+ } else if (typeof config === 'object') {
26
+ if (config.switchMax) delayConfig.switchMax = config.switchMax;
27
+ if (config.commentMax) delayConfig.commentMax = config.commentMax;
28
+ delayConfig.fast = config.fast || false;
29
+ }
30
+ }
31
+
32
+ export function getDelayConfig() {
33
+ return { ...delayConfig };
34
+ }
35
+
36
+ export function listDelayPresets() {
37
+ return DELAY_PRESETS;
38
+ }
39
+
40
+ export function delay(min, max) {
41
+ const lo = Math.min(min, max);
42
+ const hi = Math.max(min, max);
43
+ let ms;
44
+ if (delayConfig.fast) {
45
+ ms = 0;
46
+ } else {
47
+ ms = Math.floor(Math.random() * (hi - lo + 1)) + lo;
48
+ }
49
+ return new Promise(r => setTimeout(r, ms));
50
+ }
51
+
52
+ export function randomDelay(min = 200, max = 600) {
53
+ return delay(min, max);
54
+ }