tt-help-cli-ycl 1.3.48 → 1.3.50
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/README.md +33 -33
- package/cli.js +9 -9
- package/package.json +52 -52
- package/scripts/run-explore copy.bat +101 -101
- package/scripts/run-explore.bat +134 -134
- package/scripts/run-explore.ps1 +159 -159
- package/scripts/run-explore.sh +121 -121
- package/scripts/test-captcha-lib.mjs +68 -0
- package/scripts/test-captcha.mjs +81 -0
- package/scripts/test-incognito-lib.mjs +36 -0
- package/scripts/test-login-state.mjs +128 -0
- package/scripts/test-safe-click.mjs +45 -0
- package/scripts/test-watch-db-smoke.mjs +246 -0
- package/src/cli/attach.js +331 -331
- package/src/cli/auto.js +265 -265
- package/src/cli/comments.js +620 -620
- package/src/cli/config.js +170 -170
- package/src/cli/db-import.js +51 -51
- package/src/cli/explore.js +555 -555
- package/src/cli/open.js +109 -111
- package/src/cli/progress.js +111 -111
- package/src/cli/refresh.js +288 -288
- package/src/cli/scrape.js +47 -47
- package/src/cli/utils.js +18 -18
- package/src/cli/videos.js +41 -41
- package/src/cli/videostats.js +196 -196
- package/src/cli/watch.js +30 -30
- package/src/lib/api-interceptor.js +161 -161
- package/src/lib/args.js +809 -809
- package/src/lib/browser/anti-detect.js +23 -23
- package/src/lib/browser/cdp.js +261 -261
- package/src/lib/browser/health-checker.js +114 -114
- package/src/lib/browser/launch.js +43 -43
- package/src/lib/browser/page.js +184 -184
- package/src/lib/constants.js +297 -297
- package/src/lib/delay.js +54 -54
- package/src/lib/explore-fetch.js +118 -118
- package/src/lib/fetcher.js +45 -45
- package/src/lib/filter.js +66 -66
- package/src/lib/io.js +54 -54
- package/src/lib/output.js +80 -80
- package/src/lib/page-error-detector.js +109 -109
- package/src/lib/parse-ssr.mjs +69 -69
- package/src/lib/parser.js +47 -47
- package/src/lib/retry.js +45 -45
- package/src/lib/scrape.js +90 -90
- package/src/lib/target-locations.js +61 -61
- package/src/lib/tiktok-scraper.mjs +98 -61
- package/src/lib/url.js +52 -52
- package/src/main.js +73 -73
- package/src/npm-main.js +70 -70
- package/src/results/user-videos-bar.lar.lar.moeta.json +37 -0
- package/src/scraper/auto-core.js +203 -203
- package/src/scraper/core.js +255 -255
- package/src/scraper/explore-core.js +208 -208
- package/src/scraper/modules/captcha-handler.js +114 -114
- package/src/scraper/modules/follow-extractor.js +250 -250
- package/src/scraper/modules/guess-extractor.js +51 -51
- package/src/scraper/modules/page-helpers.js +48 -48
- package/src/scraper/refresh-core.js +213 -213
- package/src/videos/core.js +143 -143
- package/src/watch/data-store.js +2980 -2980
- package/src/watch/public/index.html +2355 -2355
- package/src/watch/server.js +727 -727
package/src/lib/browser/page.js
CHANGED
|
@@ -1,184 +1,184 @@
|
|
|
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
|
-
);
|
|
14
|
-
|
|
15
|
-
const BROWSER_CLOSED_PATTERNS = [
|
|
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",
|
|
22
|
-
];
|
|
23
|
-
|
|
24
|
-
export function isBrowserClosedError(error) {
|
|
25
|
-
if (!error) return false;
|
|
26
|
-
const msg = error.message || error.toString() || "";
|
|
27
|
-
return BROWSER_CLOSED_PATTERNS.some((p) => msg.includes(p));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export async function relaunchBrowser(cdpOptions, port) {
|
|
31
|
-
console.error(` [浏览器] 浏览器已关闭,正在重启 (端口 ${port})...`);
|
|
32
|
-
const targetDir = cdpOptions.userDataDir || DEFAULT_USER_DATA_DIR;
|
|
33
|
-
// kill 并清理 Singleton 锁文件,确保 Edge 能启动新实例
|
|
34
|
-
await killEdgeProcesses(targetDir);
|
|
35
|
-
await new Promise((r) => setTimeout(r, 3000));
|
|
36
|
-
// 确保端口已释放后再启动
|
|
37
|
-
let retries = 0;
|
|
38
|
-
while (retries < 5) {
|
|
39
|
-
try {
|
|
40
|
-
return await ensureBrowserReady(cdpOptions);
|
|
41
|
-
} catch (e) {
|
|
42
|
-
if (e.message && e.message.includes("setDownloadBehavior")) {
|
|
43
|
-
retries++;
|
|
44
|
-
console.error(` [浏览器] CDP 连接异常,重试 ${retries}/5...`);
|
|
45
|
-
await new Promise((r) => setTimeout(r, 3000));
|
|
46
|
-
await killEdgeProcesses(targetDir);
|
|
47
|
-
await new Promise((r) => setTimeout(r, 5000));
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
throw e;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
throw new Error("浏览器重启失败,CDP 连接异常");
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export async function withBrowserRecovery(fn, browser, page, cdpOptions, port) {
|
|
57
|
-
try {
|
|
58
|
-
return await fn();
|
|
59
|
-
} catch (e) {
|
|
60
|
-
if (isBrowserClosedError(e)) {
|
|
61
|
-
const newBrowser = await relaunchBrowser(cdpOptions, port);
|
|
62
|
-
const newPage = await getOrCreatePage(newBrowser);
|
|
63
|
-
return await fn(newBrowser, newPage);
|
|
64
|
-
}
|
|
65
|
-
throw e;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* 稳定判断登录状态:先检查 sessionid Cookie,再用页面 DOM 做验真。
|
|
71
|
-
* 仅有 sessionid 不足以说明会话仍然有效,因此需要二次确认。
|
|
72
|
-
*/
|
|
73
|
-
export async function isLoggedIn(page) {
|
|
74
|
-
const cookies = await page.context().cookies("https://www.tiktok.com");
|
|
75
|
-
const hasSessionId = cookies.some((c) => c.name === "sessionid");
|
|
76
|
-
if (!hasSessionId) return false;
|
|
77
|
-
|
|
78
|
-
return await isLoggedInByDom(page);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* 通过 DOM 元素判断登录状态(验真方案)
|
|
83
|
-
* 依赖页面渲染完成,因此不单独作为主判断,而是和 Cookie 组合使用。
|
|
84
|
-
*/
|
|
85
|
-
export async function isLoggedInByDom(page) {
|
|
86
|
-
// 先等客户端渲染完成:登录态元素或登录按钮,哪个先出现就停止等待
|
|
87
|
-
const loginOrLoggedInSelector = [
|
|
88
|
-
'[class*="DivProfileContainer"]',
|
|
89
|
-
'[class*="DivUserContainer"]',
|
|
90
|
-
'[class*="UserMenu"]',
|
|
91
|
-
'[class*="CurrentUserInfo"]',
|
|
92
|
-
'button:has-text("登录")',
|
|
93
|
-
'button:has-text("Log in")',
|
|
94
|
-
'button:has-text("Sign in")',
|
|
95
|
-
].join(", ");
|
|
96
|
-
|
|
97
|
-
await page
|
|
98
|
-
.waitForSelector(loginOrLoggedInSelector, { timeout: 10000 })
|
|
99
|
-
.catch(() => {});
|
|
100
|
-
|
|
101
|
-
return page.evaluate(() => {
|
|
102
|
-
const hasProfileContainer = !!document.querySelector(
|
|
103
|
-
'[class*="DivProfileContainer"], [class*="DivUserContainer"]',
|
|
104
|
-
);
|
|
105
|
-
const hasUserMenu = !!document.querySelector(
|
|
106
|
-
'[class*="UserMenu"], [class*="user-menu"], [class*="CurrentUserInfo"]',
|
|
107
|
-
);
|
|
108
|
-
const hasLoginButton = Array.from(
|
|
109
|
-
document.querySelectorAll('button, [role="button"]'),
|
|
110
|
-
).some((el) => /^(登录|Log in|Sign in)$/i.test(el.textContent.trim()));
|
|
111
|
-
|
|
112
|
-
return (hasProfileContainer || hasUserMenu) && !hasLoginButton;
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export async function closeCommentPanel(page) {
|
|
117
|
-
await page.evaluate(() => {
|
|
118
|
-
const rightPanel = document.querySelector('[class*="RightPanelContainer"]');
|
|
119
|
-
if (rightPanel) {
|
|
120
|
-
const tabContainer = rightPanel.querySelector('[class*="TabContainer"]');
|
|
121
|
-
if (tabContainer) {
|
|
122
|
-
const closeOverlay = tabContainer.querySelector("div:last-child");
|
|
123
|
-
if (closeOverlay) closeOverlay.click();
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export async function ensureTikTokPage(browser, url) {
|
|
130
|
-
const contexts = browser.contexts();
|
|
131
|
-
let page = null;
|
|
132
|
-
|
|
133
|
-
for (const ctx of contexts) {
|
|
134
|
-
for (const p of ctx.pages()) {
|
|
135
|
-
if (p.url().includes("tiktok.com")) {
|
|
136
|
-
page = p;
|
|
137
|
-
break;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
if (page) break;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (!page) {
|
|
144
|
-
console.error("未找到 TikTok 页面,正在打开...");
|
|
145
|
-
const defaultCtx = browser.contexts()[0];
|
|
146
|
-
page = await defaultCtx.newPage();
|
|
147
|
-
await retryWithBackoff(() =>
|
|
148
|
-
page.goto(url, { waitUntil: "load", timeout: 30000 }),
|
|
149
|
-
);
|
|
150
|
-
const config = getDelayConfig();
|
|
151
|
-
await delay(Math.round(config.switchMax * 0.5), config.switchMax);
|
|
152
|
-
console.error("TikTok 页面已打开");
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return page;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export async function findTikTokPage(browser) {
|
|
159
|
-
const contexts = browser.contexts();
|
|
160
|
-
for (const ctx of contexts) {
|
|
161
|
-
for (const p of ctx.pages()) {
|
|
162
|
-
if (p.url().includes("tiktok.com")) return p;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
return null;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export async function getOrCreatePage(browser) {
|
|
169
|
-
let page = await findTikTokPage(browser);
|
|
170
|
-
if (!page) {
|
|
171
|
-
const defaultCtx = browser.contexts()[0] || (await browser.newContext());
|
|
172
|
-
page = await defaultCtx.newPage();
|
|
173
|
-
}
|
|
174
|
-
return page;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
export function assertPageUrl(page, expectedPath) {
|
|
178
|
-
const actual = page.url();
|
|
179
|
-
if (!actual.includes(expectedPath)) {
|
|
180
|
-
throw new Error(
|
|
181
|
-
`[代理错误] 预期访问 ${expectedPath},实际跳转到了 ${actual}`,
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
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
|
+
);
|
|
14
|
+
|
|
15
|
+
const BROWSER_CLOSED_PATTERNS = [
|
|
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",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export function isBrowserClosedError(error) {
|
|
25
|
+
if (!error) return false;
|
|
26
|
+
const msg = error.message || error.toString() || "";
|
|
27
|
+
return BROWSER_CLOSED_PATTERNS.some((p) => msg.includes(p));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function relaunchBrowser(cdpOptions, port) {
|
|
31
|
+
console.error(` [浏览器] 浏览器已关闭,正在重启 (端口 ${port})...`);
|
|
32
|
+
const targetDir = cdpOptions.userDataDir || DEFAULT_USER_DATA_DIR;
|
|
33
|
+
// kill 并清理 Singleton 锁文件,确保 Edge 能启动新实例
|
|
34
|
+
await killEdgeProcesses(targetDir);
|
|
35
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
36
|
+
// 确保端口已释放后再启动
|
|
37
|
+
let retries = 0;
|
|
38
|
+
while (retries < 5) {
|
|
39
|
+
try {
|
|
40
|
+
return await ensureBrowserReady(cdpOptions);
|
|
41
|
+
} catch (e) {
|
|
42
|
+
if (e.message && e.message.includes("setDownloadBehavior")) {
|
|
43
|
+
retries++;
|
|
44
|
+
console.error(` [浏览器] CDP 连接异常,重试 ${retries}/5...`);
|
|
45
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
46
|
+
await killEdgeProcesses(targetDir);
|
|
47
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
throw e;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
throw new Error("浏览器重启失败,CDP 连接异常");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function withBrowserRecovery(fn, browser, page, cdpOptions, port) {
|
|
57
|
+
try {
|
|
58
|
+
return await fn();
|
|
59
|
+
} catch (e) {
|
|
60
|
+
if (isBrowserClosedError(e)) {
|
|
61
|
+
const newBrowser = await relaunchBrowser(cdpOptions, port);
|
|
62
|
+
const newPage = await getOrCreatePage(newBrowser);
|
|
63
|
+
return await fn(newBrowser, newPage);
|
|
64
|
+
}
|
|
65
|
+
throw e;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 稳定判断登录状态:先检查 sessionid Cookie,再用页面 DOM 做验真。
|
|
71
|
+
* 仅有 sessionid 不足以说明会话仍然有效,因此需要二次确认。
|
|
72
|
+
*/
|
|
73
|
+
export async function isLoggedIn(page) {
|
|
74
|
+
const cookies = await page.context().cookies("https://www.tiktok.com");
|
|
75
|
+
const hasSessionId = cookies.some((c) => c.name === "sessionid");
|
|
76
|
+
if (!hasSessionId) return false;
|
|
77
|
+
|
|
78
|
+
return await isLoggedInByDom(page);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 通过 DOM 元素判断登录状态(验真方案)
|
|
83
|
+
* 依赖页面渲染完成,因此不单独作为主判断,而是和 Cookie 组合使用。
|
|
84
|
+
*/
|
|
85
|
+
export async function isLoggedInByDom(page) {
|
|
86
|
+
// 先等客户端渲染完成:登录态元素或登录按钮,哪个先出现就停止等待
|
|
87
|
+
const loginOrLoggedInSelector = [
|
|
88
|
+
'[class*="DivProfileContainer"]',
|
|
89
|
+
'[class*="DivUserContainer"]',
|
|
90
|
+
'[class*="UserMenu"]',
|
|
91
|
+
'[class*="CurrentUserInfo"]',
|
|
92
|
+
'button:has-text("登录")',
|
|
93
|
+
'button:has-text("Log in")',
|
|
94
|
+
'button:has-text("Sign in")',
|
|
95
|
+
].join(", ");
|
|
96
|
+
|
|
97
|
+
await page
|
|
98
|
+
.waitForSelector(loginOrLoggedInSelector, { timeout: 10000 })
|
|
99
|
+
.catch(() => {});
|
|
100
|
+
|
|
101
|
+
return page.evaluate(() => {
|
|
102
|
+
const hasProfileContainer = !!document.querySelector(
|
|
103
|
+
'[class*="DivProfileContainer"], [class*="DivUserContainer"]',
|
|
104
|
+
);
|
|
105
|
+
const hasUserMenu = !!document.querySelector(
|
|
106
|
+
'[class*="UserMenu"], [class*="user-menu"], [class*="CurrentUserInfo"]',
|
|
107
|
+
);
|
|
108
|
+
const hasLoginButton = Array.from(
|
|
109
|
+
document.querySelectorAll('button, [role="button"]'),
|
|
110
|
+
).some((el) => /^(登录|Log in|Sign in)$/i.test(el.textContent.trim()));
|
|
111
|
+
|
|
112
|
+
return (hasProfileContainer || hasUserMenu) && !hasLoginButton;
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function closeCommentPanel(page) {
|
|
117
|
+
await page.evaluate(() => {
|
|
118
|
+
const rightPanel = document.querySelector('[class*="RightPanelContainer"]');
|
|
119
|
+
if (rightPanel) {
|
|
120
|
+
const tabContainer = rightPanel.querySelector('[class*="TabContainer"]');
|
|
121
|
+
if (tabContainer) {
|
|
122
|
+
const closeOverlay = tabContainer.querySelector("div:last-child");
|
|
123
|
+
if (closeOverlay) closeOverlay.click();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function ensureTikTokPage(browser, url) {
|
|
130
|
+
const contexts = browser.contexts();
|
|
131
|
+
let page = null;
|
|
132
|
+
|
|
133
|
+
for (const ctx of contexts) {
|
|
134
|
+
for (const p of ctx.pages()) {
|
|
135
|
+
if (p.url().includes("tiktok.com")) {
|
|
136
|
+
page = p;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (page) break;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!page) {
|
|
144
|
+
console.error("未找到 TikTok 页面,正在打开...");
|
|
145
|
+
const defaultCtx = browser.contexts()[0];
|
|
146
|
+
page = await defaultCtx.newPage();
|
|
147
|
+
await retryWithBackoff(() =>
|
|
148
|
+
page.goto(url, { waitUntil: "load", timeout: 30000 }),
|
|
149
|
+
);
|
|
150
|
+
const config = getDelayConfig();
|
|
151
|
+
await delay(Math.round(config.switchMax * 0.5), config.switchMax);
|
|
152
|
+
console.error("TikTok 页面已打开");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return page;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function findTikTokPage(browser) {
|
|
159
|
+
const contexts = browser.contexts();
|
|
160
|
+
for (const ctx of contexts) {
|
|
161
|
+
for (const p of ctx.pages()) {
|
|
162
|
+
if (p.url().includes("tiktok.com")) return p;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function getOrCreatePage(browser) {
|
|
169
|
+
let page = await findTikTokPage(browser);
|
|
170
|
+
if (!page) {
|
|
171
|
+
const defaultCtx = browser.contexts()[0] || (await browser.newContext());
|
|
172
|
+
page = await defaultCtx.newPage();
|
|
173
|
+
}
|
|
174
|
+
return page;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function assertPageUrl(page, expectedPath) {
|
|
178
|
+
const actual = page.url();
|
|
179
|
+
if (!actual.includes(expectedPath)) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`[代理错误] 预期访问 ${expectedPath},实际跳转到了 ${actual}`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|