tt-help-cli-ycl 1.3.30 → 1.3.32
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 +1 -1
- package/scripts/run-explore.bat +32 -3
- package/scripts/run-explore.ps1 +28 -2
- package/scripts/run-explore.sh +28 -3
- package/src/cli/comments.js +523 -0
- package/src/cli/explore.js +16 -8
- package/src/cli/videostats.js +81 -0
- package/src/lib/api-interceptor-comment.js +126 -0
- package/src/lib/api-interceptor.js +30 -38
- package/src/lib/args.js +278 -112
- package/src/lib/browser/cdp.js +93 -69
- package/src/lib/browser/health-checker.js +23 -11
- package/src/lib/browser/page.js +62 -35
- package/src/lib/constants.js +7 -0
- package/src/main.js +9 -5
- package/src/scraper/modules/comment-extractor.js +38 -59
- package/src/watch/data-store.js +28 -1
- package/src/watch/server.js +30 -2
|
@@ -1,73 +1,52 @@
|
|
|
1
1
|
import { delay, getDelayConfig, closeCommentPanel } from "./page-helpers.js";
|
|
2
|
-
import { scrollAndCollect } from "./scroll-collector.js";
|
|
3
2
|
import { waitAndGetCaptcha } from "./captcha-handler.js";
|
|
4
|
-
|
|
5
|
-
async function openCommentPanel(page) {
|
|
6
|
-
const tabs = page.locator('[class*="tabbar-item"]');
|
|
7
|
-
const commentTab = tabs.filter({ hasText: "评论" }).first();
|
|
8
|
-
await commentTab.click();
|
|
9
|
-
|
|
10
|
-
// 等待短暂时间让页面渲染
|
|
11
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
12
|
-
|
|
13
|
-
// 检测验证码
|
|
14
|
-
const captchaResult = await waitAndGetCaptcha(page, {
|
|
15
|
-
waitMs: 180000,
|
|
16
|
-
pollInterval: 5000,
|
|
17
|
-
log: console.error,
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
await page
|
|
21
|
-
.waitForSelector('[class*="CommentListContainer"]', { timeout: 5000 })
|
|
22
|
-
.catch(() => {});
|
|
23
|
-
await page
|
|
24
|
-
.waitForFunction(
|
|
25
|
-
() => {
|
|
26
|
-
const list = document.querySelector('[class*="CommentListContainer"]');
|
|
27
|
-
return list && list.children.length > 0;
|
|
28
|
-
},
|
|
29
|
-
{ timeout: 10000 },
|
|
30
|
-
)
|
|
31
|
-
.catch(() => {});
|
|
32
|
-
|
|
33
|
-
return { captchaDetected: !!captchaResult.detected };
|
|
34
|
-
}
|
|
3
|
+
import { fetchUserCommentsAPI } from "../../lib/api-interceptor-comment.js";
|
|
35
4
|
|
|
36
5
|
async function extractCommentAuthors(page, maxComments = 10) {
|
|
37
|
-
const panelResult = await openCommentPanel(page);
|
|
38
|
-
|
|
39
6
|
const config = getDelayConfig();
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
return { items: authors };
|
|
58
|
-
},
|
|
59
|
-
uniqueKey: (a) => a,
|
|
60
|
-
maxItems: maxComments,
|
|
61
|
-
delayRange: [Math.round(config.commentMax * 0.3), config.commentMax],
|
|
62
|
-
staleThreshold: 2,
|
|
7
|
+
|
|
8
|
+
// 关闭可能已打开的评论面板(用 evaluate 而非 locator,
|
|
9
|
+
// 因为 closeCommentPanel 是点 overlay 不是点 tab)
|
|
10
|
+
await closeCommentPanel(page);
|
|
11
|
+
await delay(800, 1500);
|
|
12
|
+
|
|
13
|
+
// API 拦截获取评论
|
|
14
|
+
const apiResult = await fetchUserCommentsAPI(page, {
|
|
15
|
+
maxComments,
|
|
16
|
+
log: console.log,
|
|
17
|
+
onCaptcha: (pg) => waitAndGetCaptcha(pg, {
|
|
18
|
+
waitMs: 180000,
|
|
19
|
+
pollInterval: 5000,
|
|
20
|
+
log: console.error,
|
|
21
|
+
}),
|
|
63
22
|
});
|
|
64
23
|
|
|
24
|
+
// 关闭评论面板
|
|
65
25
|
await closeCommentPanel(page);
|
|
66
26
|
await delay(Math.round(config.commentMax * 0.3), config.commentMax);
|
|
67
27
|
|
|
28
|
+
if (apiResult.error || !apiResult.comments || apiResult.comments.length === 0) {
|
|
29
|
+
return {
|
|
30
|
+
authors: [],
|
|
31
|
+
captchaDetected: apiResult.captchaDetected || false,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 提取作者名(保持原有接口)
|
|
36
|
+
const authors = [];
|
|
37
|
+
for (const comment of apiResult.comments) {
|
|
38
|
+
if (authors.length >= maxComments) break;
|
|
39
|
+
const uniqueId = comment.user?.unique_id;
|
|
40
|
+
if (uniqueId) {
|
|
41
|
+
authors.push("@" + uniqueId);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const uniqueAuthors = [...new Set(authors)].slice(0, maxComments);
|
|
46
|
+
|
|
68
47
|
return {
|
|
69
|
-
authors:
|
|
70
|
-
captchaDetected:
|
|
48
|
+
authors: uniqueAuthors,
|
|
49
|
+
captchaDetected: apiResult.captchaDetected || false,
|
|
71
50
|
};
|
|
72
51
|
}
|
|
73
52
|
|
package/src/watch/data-store.js
CHANGED
|
@@ -813,6 +813,33 @@ export function createStore(filePath) {
|
|
|
813
813
|
return videos.length;
|
|
814
814
|
}
|
|
815
815
|
|
|
816
|
+
function getPendingCommentTasks(limit) {
|
|
817
|
+
// 筛选待处理视频(userUpdateCount <= 0 或 null/undefined)
|
|
818
|
+
const pending = videos.filter(v => (v.userUpdateCount || 0) <= 0);
|
|
819
|
+
// ttSeller=true 优先
|
|
820
|
+
pending.sort((a, b) => {
|
|
821
|
+
if (a.ttSeller && !b.ttSeller) return -1;
|
|
822
|
+
if (!a.ttSeller && b.ttSeller) return 1;
|
|
823
|
+
return (a.registeredAt || 0) - (b.registeredAt || 0);
|
|
824
|
+
});
|
|
825
|
+
// 取前 limit 个
|
|
826
|
+
const tasks = pending.slice(0, limit);
|
|
827
|
+
// userUpdateCount +1
|
|
828
|
+
for (const task of tasks) {
|
|
829
|
+
task.userUpdateCount = (task.userUpdateCount || 0) + 1;
|
|
830
|
+
}
|
|
831
|
+
saveVideos();
|
|
832
|
+
return tasks;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function commitCommentTask(videoId) {
|
|
836
|
+
const video = videos.find(v => v.id === videoId);
|
|
837
|
+
if (!video) return { ok: false, error: 'video not found' };
|
|
838
|
+
video.userUpdateCount = (video.userUpdateCount || 0) + 1;
|
|
839
|
+
saveVideos();
|
|
840
|
+
return { ok: true, userUpdateCount: video.userUpdateCount };
|
|
841
|
+
}
|
|
842
|
+
|
|
816
843
|
return {
|
|
817
844
|
save, flushSave, getUser, hasUser, userExists, addUser,
|
|
818
845
|
getPendingUsers, getProcessedUsers, getAllUsers, getStats,
|
|
@@ -821,7 +848,7 @@ export function createStore(filePath) {
|
|
|
821
848
|
getNextRedoJob, commitRedoJob,
|
|
822
849
|
getPendingUserUpdateTasks, updateUserInfo, batchUpdateUserInfo,
|
|
823
850
|
reportClientError, deleteClientError, getClientErrors,
|
|
824
|
-
registerVideos, getVideos, getVideoCount,
|
|
851
|
+
registerVideos, getVideos, getVideoCount, getPendingCommentTasks, commitCommentTask,
|
|
825
852
|
stopBackup,
|
|
826
853
|
data,
|
|
827
854
|
};
|
package/src/watch/server.js
CHANGED
|
@@ -204,17 +204,18 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
|
|
|
204
204
|
|
|
205
205
|
if (req.method === 'POST' && routePath === '/api/users') {
|
|
206
206
|
try {
|
|
207
|
-
const { usernames } = await readBody(req);
|
|
207
|
+
const { usernames, sources, guessedLocation } = await readBody(req);
|
|
208
208
|
if (!Array.isArray(usernames) || usernames.length === 0) {
|
|
209
209
|
sendJSON(res, 400, { error: 'usernames \u6570\u7ec4\u4e0d\u80fd\u4e3a\u7a7a' });
|
|
210
210
|
return;
|
|
211
211
|
}
|
|
212
|
+
const userSources = sources || ['seed'];
|
|
212
213
|
const existingIds = new Set(store.getAllUsers().map(u => u.uniqueId));
|
|
213
214
|
const newUsers = usernames
|
|
214
215
|
.map(u => u.replace(/^@/, '').trim())
|
|
215
216
|
.filter(u => u && !existingIds.has(u));
|
|
216
217
|
for (const nu of newUsers) {
|
|
217
|
-
store.addUser({ uniqueId: nu, sources:
|
|
218
|
+
store.addUser({ uniqueId: nu, sources: userSources, status: 'pending', guessedLocation: guessedLocation || null });
|
|
218
219
|
}
|
|
219
220
|
store.save();
|
|
220
221
|
sendJSON(res, 200, {
|
|
@@ -416,6 +417,33 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
|
|
|
416
417
|
return;
|
|
417
418
|
}
|
|
418
419
|
|
|
420
|
+
if (req.method === 'GET' && routePath === '/api/comment-tasks') {
|
|
421
|
+
const limit = parseInt(params.limit) || 1;
|
|
422
|
+
const tasks = store.getPendingCommentTasks(limit);
|
|
423
|
+
const ts = new Date().toISOString().slice(11, 19);
|
|
424
|
+
console.error(`[JOB ${ts}] COMMENT-TASKS: ${tasks.length} tasks`);
|
|
425
|
+
sendJSON(res, 200, { total: tasks.length, tasks });
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const commentTaskMatch = routePath.match(/^\/api\/comment-task\/([^/]+)$/);
|
|
430
|
+
if (req.method === 'PUT' && commentTaskMatch) {
|
|
431
|
+
const videoId = commentTaskMatch[1];
|
|
432
|
+
try {
|
|
433
|
+
const ret = store.commitCommentTask(videoId);
|
|
434
|
+
if (ret.error) {
|
|
435
|
+
sendJSON(res, 404, { error: ret.error });
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
const ts = new Date().toISOString().slice(11, 19);
|
|
439
|
+
console.error(`[JOB ${ts}] COMMENT-TASK-COMMIT: ${videoId} (userUpdateCount=${ret.userUpdateCount})`);
|
|
440
|
+
sendJSON(res, 200, ret);
|
|
441
|
+
} catch (e) {
|
|
442
|
+
sendJSON(res, 400, { error: e.message });
|
|
443
|
+
}
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
419
447
|
const redoCommitMatch = routePath.match(/^\/api\/redo-job\/([^/]+)$/);
|
|
420
448
|
if (req.method === 'POST' && redoCommitMatch) {
|
|
421
449
|
const uniqueId = redoCommitMatch[1];
|