tt-help-cli-ycl 1.3.35 → 1.3.37
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 +17 -1
- package/cli.js +3 -3
- package/package.json +7 -2
- package/scripts/test-watch-db-smoke.mjs +246 -0
- package/src/cli/attach.js +149 -89
- package/src/cli/auto.js +180 -155
- package/src/cli/comments.js +301 -210
- package/src/cli/config.js +62 -44
- package/src/cli/db-import.js +51 -0
- package/src/cli/explore.js +373 -342
- package/src/cli/refresh.js +223 -151
- package/src/cli/videostats.js +140 -25
- package/src/cli/watch.js +15 -16
- package/src/lib/args.js +50 -6
- package/src/lib/constants.js +159 -92
- package/src/lib/tiktok-scraper.mjs +59 -21
- package/src/main.js +42 -20
- package/src/npm-main.js +69 -0
- package/src/watch/data-store.js +1560 -236
- package/src/watch/public/index.html +51 -15
- package/src/watch/server.js +63 -374
package/src/cli/comments.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { chromium } from
|
|
2
|
-
import { fetchUserCommentsAPI } from
|
|
3
|
-
import { closeCommentPanel } from
|
|
4
|
-
import { server as defaultServer } from
|
|
1
|
+
import { chromium } from "playwright";
|
|
2
|
+
import { fetchUserCommentsAPI } from "../lib/api-interceptor-comment.js";
|
|
3
|
+
import { closeCommentPanel } from "../lib/browser/page.js";
|
|
4
|
+
import { server as defaultServer } from "../lib/constants.js";
|
|
5
5
|
|
|
6
|
-
const TARGET_LOCATIONS = [
|
|
6
|
+
const TARGET_LOCATIONS = ["ES", "PL", "NL", "BE", "DE", "FR", "IT", "IE", "AT"];
|
|
7
7
|
|
|
8
8
|
async function waitForPageReady(page, timeout = 30000) {
|
|
9
9
|
const startTime = Date.now();
|
|
@@ -16,7 +16,7 @@ async function waitForPageReady(page, timeout = 30000) {
|
|
|
16
16
|
} catch {
|
|
17
17
|
// Page may have navigated, retry
|
|
18
18
|
}
|
|
19
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
19
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
20
20
|
}
|
|
21
21
|
return false;
|
|
22
22
|
}
|
|
@@ -36,8 +36,10 @@ async function withRetry(label, fn, maxRetries = 3) {
|
|
|
36
36
|
return await fn();
|
|
37
37
|
} catch (err) {
|
|
38
38
|
if (i < maxRetries - 1) {
|
|
39
|
-
console.error(
|
|
40
|
-
|
|
39
|
+
console.error(
|
|
40
|
+
` [${label}] 失败: ${err.message},${backoff / 1000}s 后重试...`,
|
|
41
|
+
);
|
|
42
|
+
await new Promise((r) => setTimeout(r, backoff));
|
|
41
43
|
backoff *= 2;
|
|
42
44
|
} else {
|
|
43
45
|
throw err;
|
|
@@ -49,8 +51,8 @@ async function withRetry(label, fn, maxRetries = 3) {
|
|
|
49
51
|
async function apiPost(url, body) {
|
|
50
52
|
return withRetry(`POST ${url}`, async () => {
|
|
51
53
|
const res = await fetch(url, {
|
|
52
|
-
method:
|
|
53
|
-
headers: {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: { "Content-Type": "application/json" },
|
|
54
56
|
body: JSON.stringify(body),
|
|
55
57
|
});
|
|
56
58
|
if (!res.ok) {
|
|
@@ -64,8 +66,8 @@ async function apiPost(url, body) {
|
|
|
64
66
|
async function apiPut(url) {
|
|
65
67
|
return withRetry(`PUT ${url}`, async () => {
|
|
66
68
|
const res = await fetch(url, {
|
|
67
|
-
method:
|
|
68
|
-
headers: {
|
|
69
|
+
method: "PUT",
|
|
70
|
+
headers: { "Content-Type": "application/json" },
|
|
69
71
|
});
|
|
70
72
|
if (!res.ok) {
|
|
71
73
|
const errText = await res.text();
|
|
@@ -88,14 +90,16 @@ async function apiGet(url) {
|
|
|
88
90
|
|
|
89
91
|
function isBrowserClosedError(err) {
|
|
90
92
|
if (!err) return false;
|
|
91
|
-
const msg = err.message || err.toString() ||
|
|
92
|
-
return
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
93
|
+
const msg = err.message || err.toString() || "";
|
|
94
|
+
return (
|
|
95
|
+
(msg.includes("Target") && msg.includes("closed")) ||
|
|
96
|
+
(msg.includes("Session") && msg.includes("deleted")) ||
|
|
97
|
+
(msg.includes("Session") && msg.includes("not found")) ||
|
|
98
|
+
(msg.includes("Browser") && msg.includes("closed")) ||
|
|
99
|
+
msg.includes("Execution context was destroyed") ||
|
|
100
|
+
msg.includes("Protocol error") ||
|
|
101
|
+
msg.includes("Cannot find context")
|
|
102
|
+
);
|
|
99
103
|
}
|
|
100
104
|
|
|
101
105
|
/**
|
|
@@ -106,10 +110,13 @@ async function runAutoMode(options) {
|
|
|
106
110
|
const actualParallel = Math.max(1, parallel || 1);
|
|
107
111
|
const actualInterval = interval || 10;
|
|
108
112
|
const actualMaxComments = maxComments || 200;
|
|
113
|
+
let shuttingDown = false;
|
|
109
114
|
|
|
110
|
-
console.error(
|
|
115
|
+
console.error(
|
|
116
|
+
`\n[Comments Auto] 并行: ${actualParallel}, 间隔: ${actualInterval}s, 评论数: ${actualMaxComments}`,
|
|
117
|
+
);
|
|
111
118
|
console.error(`服务器: ${serverUrl}`);
|
|
112
|
-
console.error(`目标国家: ${TARGET_LOCATIONS.join(
|
|
119
|
+
console.error(`目标国家: ${TARGET_LOCATIONS.join(", ")}`);
|
|
113
120
|
console.error(`开始循环接收任务...\n`);
|
|
114
121
|
|
|
115
122
|
let browser;
|
|
@@ -119,17 +126,38 @@ async function runAutoMode(options) {
|
|
|
119
126
|
let errorCount = 0;
|
|
120
127
|
let consecutiveErrors = 0;
|
|
121
128
|
|
|
129
|
+
const shutdown = async (signal) => {
|
|
130
|
+
if (shuttingDown) return;
|
|
131
|
+
shuttingDown = true;
|
|
132
|
+
console.error(`\n[Comments Auto] 收到 ${signal},正在关闭浏览器...`);
|
|
133
|
+
await browser?.close().catch(() => {});
|
|
134
|
+
console.error("[Comments Auto] 已退出");
|
|
135
|
+
process.exit(0);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const onSigint = () => {
|
|
139
|
+
void shutdown("SIGINT");
|
|
140
|
+
};
|
|
141
|
+
const onSigterm = () => {
|
|
142
|
+
void shutdown("SIGTERM");
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
process.once("SIGINT", onSigint);
|
|
146
|
+
process.once("SIGTERM", onSigterm);
|
|
147
|
+
|
|
122
148
|
async function ensureBrowser() {
|
|
123
149
|
if (browser) {
|
|
124
150
|
try {
|
|
125
151
|
await browser.contexts()[0]?.pages()[0]?.url();
|
|
126
152
|
return browser;
|
|
127
153
|
} catch {
|
|
128
|
-
try {
|
|
154
|
+
try {
|
|
155
|
+
await browser.close();
|
|
156
|
+
} catch {}
|
|
129
157
|
browser = null;
|
|
130
158
|
}
|
|
131
159
|
}
|
|
132
|
-
browser = await chromium.connectOverCDP(
|
|
160
|
+
browser = await chromium.connectOverCDP("http://127.0.0.1:9222");
|
|
133
161
|
return browser;
|
|
134
162
|
}
|
|
135
163
|
|
|
@@ -142,160 +170,200 @@ async function runAutoMode(options) {
|
|
|
142
170
|
return null;
|
|
143
171
|
}
|
|
144
172
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
browser = await ensureBrowser();
|
|
149
|
-
page = await getPage(browser);
|
|
150
|
-
if (!page) {
|
|
151
|
-
console.error('[Comments Auto] 未找到可用页面,等待中...');
|
|
152
|
-
await new Promise(r => setTimeout(r, actualInterval * 1000));
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// 获取任务
|
|
157
|
-
let tasks;
|
|
173
|
+
try {
|
|
174
|
+
while (!shuttingDown) {
|
|
175
|
+
let page;
|
|
158
176
|
try {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
console.error('[Comments Auto] 连续获取失败超过10次,请检查服务端');
|
|
166
|
-
process.exit(1);
|
|
177
|
+
browser = await ensureBrowser();
|
|
178
|
+
page = await getPage(browser);
|
|
179
|
+
if (!page) {
|
|
180
|
+
console.error("[Comments Auto] 未找到可用页面,等待中...");
|
|
181
|
+
await new Promise((r) => setTimeout(r, actualInterval * 1000));
|
|
182
|
+
continue;
|
|
167
183
|
}
|
|
168
|
-
await new Promise(r => setTimeout(r, actualInterval * 1000));
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
consecutiveErrors = 0;
|
|
172
184
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
185
|
+
// 获取任务
|
|
186
|
+
let tasks;
|
|
187
|
+
try {
|
|
188
|
+
const resp = await apiGet(
|
|
189
|
+
`${serverUrl}/api/comment-tasks?limit=${actualParallel}`,
|
|
190
|
+
);
|
|
191
|
+
tasks = resp.tasks || [];
|
|
192
|
+
} catch (err) {
|
|
193
|
+
console.error(`[Comments Auto] 获取任务失败: ${err.message}`);
|
|
194
|
+
consecutiveErrors++;
|
|
195
|
+
if (consecutiveErrors > 10) {
|
|
196
|
+
console.error("[Comments Auto] 连续获取失败超过10次,请检查服务端");
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
await new Promise((r) => setTimeout(r, actualInterval * 1000));
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
consecutiveErrors = 0;
|
|
178
203
|
|
|
179
|
-
|
|
204
|
+
if (tasks.length === 0) {
|
|
205
|
+
console.error(
|
|
206
|
+
`[Comments Auto] 暂无任务,${actualInterval}s 后重试...`,
|
|
207
|
+
);
|
|
208
|
+
await new Promise((r) => setTimeout(r, actualInterval * 1000));
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
180
211
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
212
|
+
console.error(`[Comments Auto] 获取 ${tasks.length} 个任务`);
|
|
213
|
+
|
|
214
|
+
for (const task of tasks) {
|
|
215
|
+
try {
|
|
216
|
+
const { id, href, locationCreated, ttSeller } = task;
|
|
217
|
+
const loc = (locationCreated || "").toUpperCase();
|
|
218
|
+
|
|
219
|
+
// 检查目标国家
|
|
220
|
+
if (loc && !TARGET_LOCATIONS.includes(loc)) {
|
|
221
|
+
// 非目标国家,直接 commit 跳过
|
|
222
|
+
await apiPut(`${serverUrl}/api/comment-task/${id}`);
|
|
223
|
+
skippedCount++;
|
|
224
|
+
console.error(
|
|
225
|
+
` [跳过] ${id.substring(0, 15)}... 国家: ${loc} (非目标)`,
|
|
226
|
+
);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
185
229
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
await apiPut(`${serverUrl}/api/comment-task/${id}`);
|
|
190
|
-
skippedCount++;
|
|
191
|
-
console.error(` [跳过] ${id.substring(0, 15)}... 国家: ${loc} (非目标)`);
|
|
192
|
-
continue;
|
|
193
|
-
}
|
|
230
|
+
console.error(
|
|
231
|
+
` [处理] ${id.substring(0, 15)}... 国家: ${loc || "?"} ttSeller: ${ttSeller} -> ${href}`,
|
|
232
|
+
);
|
|
194
233
|
|
|
195
|
-
|
|
234
|
+
// 确保浏览器可用
|
|
235
|
+
browser = await ensureBrowser();
|
|
236
|
+
page = await getPage(browser);
|
|
237
|
+
if (!page) {
|
|
238
|
+
throw new Error("未找到可用页面");
|
|
239
|
+
}
|
|
196
240
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
241
|
+
// 导航到视频页
|
|
242
|
+
await page.goto(href, {
|
|
243
|
+
waitUntil: "domcontentloaded",
|
|
244
|
+
timeout: 30000,
|
|
245
|
+
});
|
|
246
|
+
const ready = await waitForPageReady(page, 30000);
|
|
247
|
+
if (!ready) {
|
|
248
|
+
throw new Error("页面加载超时");
|
|
249
|
+
}
|
|
203
250
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
251
|
+
// 关闭弹窗
|
|
252
|
+
await safeEvaluate(page, () => {
|
|
253
|
+
document
|
|
254
|
+
.querySelectorAll('[id*="modal-overlay"]')
|
|
255
|
+
.forEach((o) => o.click());
|
|
256
|
+
});
|
|
257
|
+
await page.keyboard.press("Escape");
|
|
258
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
259
|
+
|
|
260
|
+
// 如果评论面板已打开,先关闭
|
|
261
|
+
const panelOpen = await safeEvaluate(page, () => {
|
|
262
|
+
return !!document.querySelector('[class*="RightPanelContainer"]');
|
|
263
|
+
});
|
|
264
|
+
if (panelOpen) {
|
|
265
|
+
await closeCommentPanel(page);
|
|
266
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
267
|
+
}
|
|
210
268
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
269
|
+
// 获取评论
|
|
270
|
+
const result = await fetchUserCommentsAPI(page, {
|
|
271
|
+
maxComments: actualMaxComments,
|
|
272
|
+
log: () => {},
|
|
273
|
+
});
|
|
217
274
|
|
|
218
|
-
|
|
219
|
-
const panelOpen = await safeEvaluate(page, () => {
|
|
220
|
-
return !!document.querySelector('[class*="RightPanelContainer"]');
|
|
221
|
-
});
|
|
222
|
-
if (panelOpen) {
|
|
275
|
+
// 关闭评论面板
|
|
223
276
|
await closeCommentPanel(page);
|
|
224
|
-
await new Promise(r => setTimeout(r, 800));
|
|
225
|
-
}
|
|
226
277
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
// 关闭评论面板
|
|
234
|
-
await closeCommentPanel(page);
|
|
235
|
-
|
|
236
|
-
if (result.error || !result.comments || result.comments.length === 0) {
|
|
237
|
-
console.error(` [无评论] ${result.error || '未获取到评论'}`);
|
|
238
|
-
} else {
|
|
239
|
-
// 提取去重作者
|
|
240
|
-
const authors = [...new Set(
|
|
241
|
-
result.comments
|
|
242
|
-
.map(c => c.user?.unique_id)
|
|
243
|
-
.filter(Boolean)
|
|
244
|
-
.map(u => u.replace(/^@/, ''))
|
|
245
|
-
)];
|
|
246
|
-
|
|
247
|
-
if (authors.length > 0) {
|
|
248
|
-
// 提交作者
|
|
249
|
-
const saveResult = await apiPost(`${serverUrl}/api/users`, {
|
|
250
|
-
usernames: authors.map(a => '@' + a),
|
|
251
|
-
sources: ['comment'],
|
|
252
|
-
guessedLocation: loc || null,
|
|
253
|
-
});
|
|
254
|
-
console.error(` [提交] ${saveResult.added} 新增, ${saveResult.skipped} 跳过`);
|
|
278
|
+
if (
|
|
279
|
+
result.error ||
|
|
280
|
+
!result.comments ||
|
|
281
|
+
result.comments.length === 0
|
|
282
|
+
) {
|
|
283
|
+
console.error(` [无评论] ${result.error || "未获取到评论"}`);
|
|
255
284
|
} else {
|
|
256
|
-
|
|
285
|
+
// 提取去重作者
|
|
286
|
+
const authors = [
|
|
287
|
+
...new Set(
|
|
288
|
+
result.comments
|
|
289
|
+
.map((c) => c.user?.unique_id)
|
|
290
|
+
.filter(Boolean)
|
|
291
|
+
.map((u) => u.replace(/^@/, "")),
|
|
292
|
+
),
|
|
293
|
+
];
|
|
294
|
+
|
|
295
|
+
if (authors.length > 0) {
|
|
296
|
+
// 提交作者
|
|
297
|
+
const saveResult = await apiPost(`${serverUrl}/api/users`, {
|
|
298
|
+
usernames: authors.map((a) => "@" + a),
|
|
299
|
+
sources: ["comment"],
|
|
300
|
+
guessedLocation: loc || null,
|
|
301
|
+
});
|
|
302
|
+
console.error(
|
|
303
|
+
` [提交] ${saveResult.added} 新增, ${saveResult.skipped} 跳过`,
|
|
304
|
+
);
|
|
305
|
+
} else {
|
|
306
|
+
console.error(
|
|
307
|
+
` [无作者] 获取了 ${result.comments.length} 条评论但无作者信息`,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
257
310
|
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// commit 任务
|
|
261
|
-
await apiPut(`${serverUrl}/api/comment-task/${id}`);
|
|
262
|
-
processedCount++;
|
|
263
|
-
console.error(` [完成] 累计 ${processedCount} 个视频`);
|
|
264
311
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
312
|
+
// commit 任务
|
|
313
|
+
await apiPut(`${serverUrl}/api/comment-task/${id}`);
|
|
314
|
+
processedCount++;
|
|
315
|
+
console.error(` [完成] 累计 ${processedCount} 个视频`);
|
|
316
|
+
} catch (err) {
|
|
317
|
+
errorCount++;
|
|
318
|
+
const isBrowserClosed = isBrowserClosedError(err);
|
|
319
|
+
if (isBrowserClosed) {
|
|
320
|
+
console.error(` [浏览器异常] ${err.message},将重启浏览器`);
|
|
321
|
+
try {
|
|
322
|
+
await browser.close();
|
|
323
|
+
} catch {}
|
|
324
|
+
browser = null;
|
|
325
|
+
browserRestartCount++;
|
|
326
|
+
} else {
|
|
327
|
+
console.error(` [错误] ${err.message}`);
|
|
328
|
+
}
|
|
329
|
+
continue;
|
|
275
330
|
}
|
|
276
|
-
|
|
331
|
+
}
|
|
332
|
+
} catch (err) {
|
|
333
|
+
errorCount++;
|
|
334
|
+
const isBrowserClosed = isBrowserClosedError(err);
|
|
335
|
+
if (isBrowserClosed) {
|
|
336
|
+
console.error(
|
|
337
|
+
`[Comments Auto] 浏览器异常 (${++browserRestartCount}),正在重启...`,
|
|
338
|
+
);
|
|
339
|
+
try {
|
|
340
|
+
await browser.close();
|
|
341
|
+
} catch {}
|
|
342
|
+
browser = null;
|
|
343
|
+
} else {
|
|
344
|
+
console.error(`[Comments Auto] 异常: ${err.message}`);
|
|
277
345
|
}
|
|
278
346
|
}
|
|
279
347
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
const isBrowserClosed = isBrowserClosedError(err);
|
|
283
|
-
if (isBrowserClosed) {
|
|
284
|
-
console.error(`[Comments Auto] 浏览器异常 (${++browserRestartCount}),正在重启...`);
|
|
285
|
-
try { await browser.close(); } catch {}
|
|
286
|
-
browser = null;
|
|
287
|
-
} else {
|
|
288
|
-
console.error(`[Comments Auto] 异常: ${err.message}`);
|
|
289
|
-
}
|
|
348
|
+
// 等待间隔
|
|
349
|
+
await new Promise((r) => setTimeout(r, actualInterval * 1000));
|
|
290
350
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
351
|
+
} finally {
|
|
352
|
+
process.removeListener("SIGINT", onSigint);
|
|
353
|
+
process.removeListener("SIGTERM", onSigterm);
|
|
354
|
+
await browser?.close().catch(() => {});
|
|
294
355
|
}
|
|
295
356
|
}
|
|
296
357
|
|
|
297
358
|
export async function handleComments(options) {
|
|
298
|
-
const {
|
|
359
|
+
const {
|
|
360
|
+
commentsUrl,
|
|
361
|
+
commentsMax,
|
|
362
|
+
commentsSave,
|
|
363
|
+
commentsParallel,
|
|
364
|
+
commentsInterval,
|
|
365
|
+
commentsServer,
|
|
366
|
+
} = options;
|
|
299
367
|
|
|
300
368
|
// 自动模式:无URL且传了 -s/--server 参数(或任何参数)
|
|
301
369
|
if (!commentsUrl && process.argv.length > 3) {
|
|
@@ -309,17 +377,21 @@ export async function handleComments(options) {
|
|
|
309
377
|
|
|
310
378
|
// 手动模式(需要 URL)
|
|
311
379
|
if (!commentsUrl) {
|
|
312
|
-
console.error(
|
|
313
|
-
console.error(
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
console.error(
|
|
317
|
-
console.error(
|
|
318
|
-
console.error(
|
|
319
|
-
console.error(
|
|
320
|
-
console.error(
|
|
321
|
-
|
|
322
|
-
|
|
380
|
+
console.error("用法: tt-help comments <视频URL> [最大评论数] [--save]");
|
|
381
|
+
console.error(
|
|
382
|
+
" tt-help comments [-p N] [-i N] [-s server] [-m maxComments]",
|
|
383
|
+
);
|
|
384
|
+
console.error("");
|
|
385
|
+
console.error("手动模式: tt-help comments <URL> [N] [--save]");
|
|
386
|
+
console.error("自动模式: tt-help comments -p 1 -i 10 -m 200 -s <server>");
|
|
387
|
+
console.error("");
|
|
388
|
+
console.error(
|
|
389
|
+
"选项: --save 去重后保存到服务端,来源标记为 comment",
|
|
390
|
+
);
|
|
391
|
+
console.error(" -p, --parallel 并行数 (默认 1)");
|
|
392
|
+
console.error(" -i, --interval 空闲间隔秒 (默认 10)");
|
|
393
|
+
console.error(" -s, --server 服务端地址");
|
|
394
|
+
console.error(" -m, --max-comments 每视频最大评论数 (默认 200)");
|
|
323
395
|
process.exit(1);
|
|
324
396
|
}
|
|
325
397
|
|
|
@@ -327,7 +399,7 @@ export async function handleComments(options) {
|
|
|
327
399
|
// 手动模式 --save
|
|
328
400
|
let browser;
|
|
329
401
|
try {
|
|
330
|
-
browser = await chromium.connectOverCDP(
|
|
402
|
+
browser = await chromium.connectOverCDP("http://127.0.0.1:9222");
|
|
331
403
|
const contexts = browser.contexts();
|
|
332
404
|
let page;
|
|
333
405
|
|
|
@@ -339,28 +411,33 @@ export async function handleComments(options) {
|
|
|
339
411
|
}
|
|
340
412
|
|
|
341
413
|
if (!page) {
|
|
342
|
-
console.error(
|
|
414
|
+
console.error("未找到可用页面");
|
|
343
415
|
process.exit(1);
|
|
344
416
|
}
|
|
345
417
|
|
|
346
418
|
console.error(`正在打开: ${commentsUrl}`);
|
|
347
|
-
await page.goto(commentsUrl, {
|
|
419
|
+
await page.goto(commentsUrl, {
|
|
420
|
+
waitUntil: "domcontentloaded",
|
|
421
|
+
timeout: 30000,
|
|
422
|
+
});
|
|
348
423
|
const ready = await waitForPageReady(page, 30000);
|
|
349
424
|
if (!ready) {
|
|
350
|
-
console.error(
|
|
425
|
+
console.error("页面加载超时,tab 未出现");
|
|
351
426
|
process.exit(1);
|
|
352
427
|
}
|
|
353
428
|
|
|
354
429
|
await safeEvaluate(page, () => {
|
|
355
|
-
document
|
|
430
|
+
document
|
|
431
|
+
.querySelectorAll('[id*="modal-overlay"]')
|
|
432
|
+
.forEach((o) => o.click());
|
|
356
433
|
});
|
|
357
|
-
await page.keyboard.press(
|
|
358
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
434
|
+
await page.keyboard.press("Escape");
|
|
435
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
359
436
|
|
|
360
437
|
const videoInfo = await safeEvaluate(page, () => {
|
|
361
438
|
const result = {};
|
|
362
439
|
const m = window.location.href.match(/@([^/]+)\/video/);
|
|
363
|
-
result.author = m ?
|
|
440
|
+
result.author = m ? "@" + m[1] : "未知";
|
|
364
441
|
const html = document.documentElement.outerHTML;
|
|
365
442
|
const locMatch = html.match(/"locationCreated":"([^"]*)/);
|
|
366
443
|
result.locationCreated = locMatch ? locMatch[1] : null;
|
|
@@ -372,7 +449,7 @@ export async function handleComments(options) {
|
|
|
372
449
|
});
|
|
373
450
|
if (panelOpen) {
|
|
374
451
|
await closeCommentPanel(page);
|
|
375
|
-
await new Promise(r => setTimeout(r, 800));
|
|
452
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
376
453
|
}
|
|
377
454
|
|
|
378
455
|
const result = await fetchUserCommentsAPI(page, {
|
|
@@ -383,23 +460,25 @@ export async function handleComments(options) {
|
|
|
383
460
|
await closeCommentPanel(page);
|
|
384
461
|
|
|
385
462
|
if (result.error || !result.comments || result.comments.length === 0) {
|
|
386
|
-
console.error(`获取评论失败: ${result.error ||
|
|
463
|
+
console.error(`获取评论失败: ${result.error || "未获取到评论"}`);
|
|
387
464
|
process.exit(1);
|
|
388
465
|
}
|
|
389
466
|
|
|
390
|
-
const authors = [
|
|
391
|
-
|
|
392
|
-
.
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
467
|
+
const authors = [
|
|
468
|
+
...new Set(
|
|
469
|
+
result.comments
|
|
470
|
+
.map((c) => c.user?.unique_id)
|
|
471
|
+
.filter(Boolean)
|
|
472
|
+
.map((u) => u.replace(/^@/, "")),
|
|
473
|
+
),
|
|
474
|
+
];
|
|
396
475
|
|
|
397
|
-
const guessedLocation = (videoInfo?.locationCreated ||
|
|
476
|
+
const guessedLocation = (videoInfo?.locationCreated || "").toUpperCase();
|
|
398
477
|
const serverUrl = commentsServer || defaultServer;
|
|
399
478
|
|
|
400
479
|
if (guessedLocation && !TARGET_LOCATIONS.includes(guessedLocation)) {
|
|
401
480
|
console.error(`\n猜测国家: ${guessedLocation},非目标国家,跳过保存`);
|
|
402
|
-
console.error(`目标国家: ${TARGET_LOCATIONS.join(
|
|
481
|
+
console.error(`目标国家: ${TARGET_LOCATIONS.join(", ")}`);
|
|
403
482
|
} else {
|
|
404
483
|
console.error(`\n正在提交 ${authors.length} 个评论作者到服务端...`);
|
|
405
484
|
console.error(`服务端: ${serverUrl}`);
|
|
@@ -407,17 +486,18 @@ export async function handleComments(options) {
|
|
|
407
486
|
|
|
408
487
|
try {
|
|
409
488
|
const saveResult = await apiPost(`${serverUrl}/api/users`, {
|
|
410
|
-
usernames: authors.map(a =>
|
|
411
|
-
sources: [
|
|
489
|
+
usernames: authors.map((a) => "@" + a),
|
|
490
|
+
sources: ["comment"],
|
|
412
491
|
guessedLocation: guessedLocation || null,
|
|
413
492
|
});
|
|
414
|
-
console.error(
|
|
493
|
+
console.error(
|
|
494
|
+
`\n提交结果: ${saveResult.added} 个新增, ${saveResult.skipped} 个跳过`,
|
|
495
|
+
);
|
|
415
496
|
} catch (err) {
|
|
416
497
|
console.error(`\n提交失败: ${err.message}`);
|
|
417
498
|
process.exit(1);
|
|
418
499
|
}
|
|
419
500
|
}
|
|
420
|
-
|
|
421
501
|
} catch (err) {
|
|
422
502
|
console.error(`获取评论失败: ${err.message}`);
|
|
423
503
|
process.exit(1);
|
|
@@ -430,7 +510,7 @@ export async function handleComments(options) {
|
|
|
430
510
|
// 手动模式:打印到控制台
|
|
431
511
|
let browser;
|
|
432
512
|
try {
|
|
433
|
-
browser = await chromium.connectOverCDP(
|
|
513
|
+
browser = await chromium.connectOverCDP("http://127.0.0.1:9222");
|
|
434
514
|
const contexts = browser.contexts();
|
|
435
515
|
let page;
|
|
436
516
|
|
|
@@ -442,28 +522,33 @@ export async function handleComments(options) {
|
|
|
442
522
|
}
|
|
443
523
|
|
|
444
524
|
if (!page) {
|
|
445
|
-
console.error(
|
|
525
|
+
console.error("未找到可用页面");
|
|
446
526
|
process.exit(1);
|
|
447
527
|
}
|
|
448
528
|
|
|
449
529
|
console.error(`正在打开: ${commentsUrl}`);
|
|
450
|
-
await page.goto(commentsUrl, {
|
|
530
|
+
await page.goto(commentsUrl, {
|
|
531
|
+
waitUntil: "domcontentloaded",
|
|
532
|
+
timeout: 30000,
|
|
533
|
+
});
|
|
451
534
|
const ready = await waitForPageReady(page, 30000);
|
|
452
535
|
if (!ready) {
|
|
453
|
-
console.error(
|
|
536
|
+
console.error("页面加载超时,tab 未出现");
|
|
454
537
|
process.exit(1);
|
|
455
538
|
}
|
|
456
539
|
|
|
457
540
|
await safeEvaluate(page, () => {
|
|
458
|
-
document
|
|
541
|
+
document
|
|
542
|
+
.querySelectorAll('[id*="modal-overlay"]')
|
|
543
|
+
.forEach((o) => o.click());
|
|
459
544
|
});
|
|
460
|
-
await page.keyboard.press(
|
|
461
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
545
|
+
await page.keyboard.press("Escape");
|
|
546
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
462
547
|
|
|
463
548
|
const videoInfo = await safeEvaluate(page, () => {
|
|
464
549
|
const result = {};
|
|
465
550
|
const m = window.location.href.match(/@([^/]+)\/video/);
|
|
466
|
-
result.author = m ?
|
|
551
|
+
result.author = m ? "@" + m[1] : "未知";
|
|
467
552
|
const html = document.documentElement.outerHTML;
|
|
468
553
|
const locMatch = html.match(/"locationCreated":"([^"]*)/);
|
|
469
554
|
result.locationCreated = locMatch ? locMatch[1] : null;
|
|
@@ -475,7 +560,7 @@ export async function handleComments(options) {
|
|
|
475
560
|
});
|
|
476
561
|
if (panelOpen) {
|
|
477
562
|
await closeCommentPanel(page);
|
|
478
|
-
await new Promise(r => setTimeout(r, 800));
|
|
563
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
479
564
|
}
|
|
480
565
|
|
|
481
566
|
const result = await fetchUserCommentsAPI(page, {
|
|
@@ -486,34 +571,40 @@ export async function handleComments(options) {
|
|
|
486
571
|
await closeCommentPanel(page);
|
|
487
572
|
|
|
488
573
|
if (result.error || !result.comments || result.comments.length === 0) {
|
|
489
|
-
console.error(`获取评论失败: ${result.error ||
|
|
574
|
+
console.error(`获取评论失败: ${result.error || "未获取到评论"}`);
|
|
490
575
|
process.exit(1);
|
|
491
576
|
}
|
|
492
577
|
|
|
493
|
-
const authors = [
|
|
494
|
-
|
|
495
|
-
.
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
578
|
+
const authors = [
|
|
579
|
+
...new Set(
|
|
580
|
+
result.comments
|
|
581
|
+
.map((c) => c.user?.unique_id)
|
|
582
|
+
.filter(Boolean)
|
|
583
|
+
.map((u) => u.replace(/^@/, "")),
|
|
584
|
+
),
|
|
585
|
+
];
|
|
499
586
|
|
|
500
587
|
console.log(`视频: ${commentsUrl}`);
|
|
501
|
-
console.log(`作者: ${videoInfo?.author ||
|
|
502
|
-
if (videoInfo?.locationCreated)
|
|
588
|
+
console.log(`作者: ${videoInfo?.author || "未知"}`);
|
|
589
|
+
if (videoInfo?.locationCreated)
|
|
590
|
+
console.log(`猜测国家: ${videoInfo.locationCreated}`);
|
|
503
591
|
console.log(`评论数: ${result.comments.length}`);
|
|
504
592
|
console.log(`评论作者: ${authors.length}`);
|
|
505
|
-
console.log(
|
|
593
|
+
console.log("");
|
|
506
594
|
|
|
507
595
|
result.comments.forEach((c, i) => {
|
|
508
|
-
const author = c.user?.unique_id
|
|
509
|
-
|
|
596
|
+
const author = c.user?.unique_id
|
|
597
|
+
? "@" + c.user.unique_id
|
|
598
|
+
: c.user?.nickname || "未知";
|
|
599
|
+
const text = c.text || "";
|
|
510
600
|
const likes = c.digg_count || 0;
|
|
511
601
|
console.log(`${i + 1}. [${author}] ${text} (\u2764 ${likes})`);
|
|
512
602
|
});
|
|
513
603
|
|
|
514
|
-
console.log(
|
|
515
|
-
console.log(
|
|
516
|
-
|
|
604
|
+
console.log("");
|
|
605
|
+
console.log(
|
|
606
|
+
`共 ${result.comments.length} 条评论, ${authors.length} 个唯一作者`,
|
|
607
|
+
);
|
|
517
608
|
} catch (err) {
|
|
518
609
|
console.error(`获取评论失败: ${err.message}`);
|
|
519
610
|
process.exit(1);
|