@web-auto/webauto 0.1.4 → 0.1.7
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/apps/desktop-console/default-settings.json +2 -2
- package/apps/desktop-console/dist/main/index.mjs +983 -128
- package/apps/desktop-console/dist/main/preload.mjs +7 -0
- package/apps/desktop-console/dist/renderer/index.html +622 -50
- package/apps/desktop-console/dist/renderer/index.js +2423 -469
- package/apps/desktop-console/dist/renderer/run.mts +6 -5
- package/apps/desktop-console/entry/ui-cli.mjs +672 -0
- package/apps/desktop-console/entry/ui-console.mjs +416 -29
- package/apps/webauto/entry/account.mjs +89 -53
- package/apps/webauto/entry/browser-status.mjs +7 -10
- package/apps/webauto/entry/lib/account-detect.mjs +254 -28
- package/apps/webauto/entry/lib/account-store.mjs +219 -30
- package/apps/webauto/entry/lib/bus-publish.mjs +63 -0
- package/apps/webauto/entry/lib/camo-cli.mjs +93 -0
- package/apps/webauto/entry/lib/profilepool.mjs +14 -5
- package/apps/webauto/entry/lib/quota-status.mjs +23 -0
- package/apps/webauto/entry/lib/schedule-store.mjs +1068 -0
- package/apps/webauto/entry/profilepool.mjs +106 -17
- package/apps/webauto/entry/schedule.mjs +612 -0
- package/apps/webauto/entry/weibo-unified.mjs +134 -0
- package/apps/webauto/entry/xhs-install.mjs +256 -31
- package/apps/webauto/entry/xhs-status.mjs +5 -2
- package/apps/webauto/entry/xhs-unified.mjs +631 -98
- package/apps/webauto/resources/container-library/weibo/weibo_detail_page/comment_item/container.json +40 -0
- package/apps/webauto/resources/container-library/weibo/weibo_detail_page/reply_expand_button/container.json +38 -0
- package/apps/webauto/resources/container-library/weibo/weibo_detail_page/reply_list/container.json +37 -0
- package/apps/webauto/resources/container-library/weibo/weibo_search_page/container.json +8 -3
- package/apps/webauto/resources/container-library/weibo/weibo_search_page/login_anchor/container.json +30 -0
- package/apps/webauto/resources/container-library/weibo/weibo_search_page/search_bar/container.json +47 -0
- package/apps/webauto/resources/container-library/weibo/weibo_search_page/search_button/container.json +39 -0
- package/bin/camoufox-cli.mjs +61 -0
- package/bin/webauto.mjs +301 -54
- package/dist/modules/camo-backend/src/index.js +49 -1
- package/dist/modules/camo-backend/src/internal/BrowserSession.js +572 -3
- package/dist/modules/camo-backend/src/internal/SessionManager.js +13 -1
- package/dist/modules/camo-backend/src/internal/storage-paths.js +6 -0
- package/dist/modules/collection-manager/bloom-filter.js +91 -0
- package/dist/modules/collection-manager/date-utils.js +275 -0
- package/dist/modules/collection-manager/index.js +258 -0
- package/dist/modules/collection-manager/storage.js +195 -0
- package/dist/modules/collection-manager/types.js +47 -0
- package/dist/modules/logging/src/index.js +1 -1
- package/dist/modules/process-registry/index.js +230 -0
- package/dist/modules/rate-limiter/index.js +242 -0
- package/dist/modules/workflow/blocks/ExecuteWeiboSearchBlock.js +128 -0
- package/dist/modules/workflow/blocks/PersistXhsNoteBlock.js +7 -3
- package/dist/modules/workflow/blocks/RenderMarkdown.js +4 -1
- package/dist/modules/workflow/blocks/WeiboCollectCommentsBlock.js +282 -0
- package/dist/modules/workflow/blocks/WeiboCollectFromLinksBlock.js +283 -0
- package/dist/modules/workflow/blocks/WeiboCollectSearchLinksBlock.js +208 -0
- package/dist/modules/workflow/blocks/WeiboCollectTimelineListBlock.js +128 -0
- package/dist/modules/workflow/blocks/WeiboCollectUserPostsListBlock.js +127 -0
- package/dist/modules/workflow/blocks/helpers/downloadPaths.js +21 -0
- package/dist/modules/workflow/config/workflowRegistry.js +2 -0
- package/dist/modules/workflow/definitions/weibo-search-workflow-v1.js +47 -0
- package/dist/modules/workflow/src/runner.js +6 -0
- package/dist/modules/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.js +4 -0
- package/dist/modules/xiaohongshu/app/src/blocks/Phase3InteractBlock.js +2 -2
- package/dist/modules/xiaohongshu/app/src/blocks/helpers/sharding.js +123 -0
- package/dist/modules/xiaohongshu/app/src/container-registry/src/index.d.ts +37 -0
- package/dist/modules/xiaohongshu/app/src/container-registry/src/index.js +184 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/AnchorVerificationBlock.d.ts +31 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/AnchorVerificationBlock.js +71 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/DetectPageStateBlock.d.ts +48 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/DetectPageStateBlock.js +259 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/ErrorRecoveryBlock.d.ts +28 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/ErrorRecoveryBlock.js +319 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/WaitSearchPermitBlock.d.ts +36 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/WaitSearchPermitBlock.js +162 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/containerAnchors.d.ts +36 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/containerAnchors.js +301 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/operationLogger.d.ts +29 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/operationLogger.js +195 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/searchPageState.d.ts +25 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/searchPageState.js +164 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/MatchCommentsBlock.d.ts +66 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/MatchCommentsBlock.js +139 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1EnsureServicesBlock.d.ts +16 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1EnsureServicesBlock.js +36 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1MonitorCookieBlock.d.ts +27 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1MonitorCookieBlock.js +213 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1StartProfileBlock.d.ts +18 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1StartProfileBlock.js +121 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2CollectLinksBlock.d.ts +34 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2CollectLinksBlock.js +1249 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2SearchBlock.d.ts +17 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2SearchBlock.js +703 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseDetailBlock.d.ts +15 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseDetailBlock.js +41 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseTabsBlock.d.ts +26 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseTabsBlock.js +44 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CollectCommentsBlock.d.ts +29 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CollectCommentsBlock.js +150 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ExtractDetailBlock.d.ts +38 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ExtractDetailBlock.js +117 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenDetailBlock.d.ts +30 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenDetailBlock.js +102 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenTabsBlock.d.ts +23 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenTabsBlock.js +109 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.d.ts +32 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.js +117 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ProcessSingleNoteBlock.d.ts +35 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ProcessSingleNoteBlock.js +114 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ValidateLinksBlock.d.ts +34 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ValidateLinksBlock.js +90 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase3InteractBlock.d.ts +111 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase3InteractBlock.js +1009 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase4MultiTabHarvestBlock.d.ts +20 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase4MultiTabHarvestBlock.js +233 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/ReplyInteractBlock.d.ts +48 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/ReplyInteractBlock.js +291 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/XhsDiscoverFallbackBlock.d.ts +23 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/XhsDiscoverFallbackBlock.js +240 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatchDsl.d.ts +55 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatchDsl.js +126 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatcher.d.ts +21 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatcher.js +99 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/evidence.d.ts +5 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/evidence.js +27 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/sharding.d.ts +37 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/sharding.js +165 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/xhsComments.d.ts +33 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/xhsComments.js +270 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/index.d.ts +9 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/index.js +9 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/checkpoints.d.ts +50 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/checkpoints.js +222 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/controllerAction.d.ts +10 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/controllerAction.js +43 -0
- package/dist/services/shared/serviceProcessLogger.js +1 -1
- package/dist/services/unified-api/server.js +105 -11
- package/modules/camo-backend/src/index.ts +46 -1
- package/modules/camo-backend/src/internal/BrowserSession.ts +619 -3
- package/modules/camo-backend/src/internal/SessionManager.ts +12 -1
- package/modules/camo-backend/src/internal/storage-paths.ts +5 -0
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/comments.mjs +38 -2
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +47 -2
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +94 -11
- package/modules/camo-runtime/src/autoscript/action-providers/xhs.mjs +208 -2
- package/modules/camo-runtime/src/autoscript/runtime.mjs +7 -1
- package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +76 -43
- package/modules/camo-runtime/src/container/runtime-core/operations/index.mjs +75 -1
- package/modules/camo-runtime/src/container/runtime-core/operations/selector-scripts.mjs +71 -4
- package/modules/camo-runtime/src/container/runtime-core/operations/tab-pool.mjs +183 -27
- package/modules/collection-manager/bloom-filter.ts +112 -0
- package/modules/collection-manager/date-utils.ts +316 -0
- package/modules/collection-manager/index.ts +309 -0
- package/modules/collection-manager/package.json +10 -0
- package/modules/collection-manager/storage.ts +174 -0
- package/modules/collection-manager/types.ts +156 -0
- package/modules/logging/src/index.ts +1 -1
- package/modules/process-registry/index.ts +284 -0
- package/modules/rate-limiter/index.ts +322 -0
- package/modules/state/src/paths.ts +9 -1
- package/modules/task-scheduler/index.ts +293 -0
- package/modules/workflow/blocks/ExecuteWeiboSearchBlock.ts +167 -0
- package/modules/workflow/blocks/PersistXhsNoteBlock.ts +7 -3
- package/modules/workflow/blocks/RenderMarkdown.ts +4 -1
- package/modules/workflow/blocks/WeiboCollectCommentsBlock.ts +339 -0
- package/modules/workflow/blocks/WeiboCollectFromLinksBlock.ts +338 -0
- package/modules/workflow/blocks/helpers/downloadPaths.ts +16 -0
- package/modules/workflow/config/workflowRegistry.ts +2 -0
- package/modules/workflow/definitions/weibo-search-workflow-v1.ts +47 -0
- package/modules/workflow/src/runner.ts +6 -0
- package/modules/xiaohongshu/app/src/blocks/Phase1StartProfileBlock.ts +1 -1
- package/modules/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.ts +4 -0
- package/modules/xiaohongshu/app/src/blocks/Phase3InteractBlock.ts +2 -3
- package/modules/xiaohongshu/app/src/blocks/helpers/sharding.ts +152 -0
- package/package.json +13 -4
- package/scripts/postinstall-resources.mjs +62 -0
- package/scripts/test/run-coverage.mjs +76 -0
- package/scripts/weibo/search.ts +49 -0
- package/services/shared/serviceProcessLogger.ts +1 -1
- package/services/unified-api/server.ts +98 -12
package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase4MultiTabHarvestBlock.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface MultiTabHarvestInput {
|
|
2
|
+
profile: string;
|
|
3
|
+
keyword: string;
|
|
4
|
+
env: string;
|
|
5
|
+
links: Array<{
|
|
6
|
+
noteId: string;
|
|
7
|
+
safeUrl: string;
|
|
8
|
+
searchUrl?: string;
|
|
9
|
+
}>;
|
|
10
|
+
maxCommentsPerNote?: number;
|
|
11
|
+
unifiedApiUrl?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface MultiTabHarvestOutput {
|
|
14
|
+
success: boolean;
|
|
15
|
+
totalNotes: number;
|
|
16
|
+
totalComments: number;
|
|
17
|
+
errors: string[];
|
|
18
|
+
}
|
|
19
|
+
export declare function execute(input: MultiTabHarvestInput): Promise<MultiTabHarvestOutput>;
|
|
20
|
+
//# sourceMappingURL=Phase4MultiTabHarvestBlock.d.ts.map
|
package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase4MultiTabHarvestBlock.js
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
async function controllerAction(action, payload, apiUrl) {
|
|
4
|
+
const res = await fetch(`${apiUrl}/v1/controller/action`, {
|
|
5
|
+
method: 'POST',
|
|
6
|
+
headers: { 'Content-Type': 'application/json' },
|
|
7
|
+
body: JSON.stringify({ action, payload }),
|
|
8
|
+
signal: AbortSignal.timeout(30000),
|
|
9
|
+
});
|
|
10
|
+
const data = await res.json().catch(() => ({}));
|
|
11
|
+
return data.data || data;
|
|
12
|
+
}
|
|
13
|
+
async function delay(ms) {
|
|
14
|
+
return new Promise(r => setTimeout(r, ms));
|
|
15
|
+
}
|
|
16
|
+
async function ensureDir(pathname) {
|
|
17
|
+
const { mkdir } = await import('node:fs/promises');
|
|
18
|
+
await mkdir(pathname, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
async function appendJsonl(filePath, rows) {
|
|
21
|
+
const { appendFile } = await import('node:fs/promises');
|
|
22
|
+
const lines = rows.map((row) => JSON.stringify(row)).join('\n') + '\n';
|
|
23
|
+
await appendFile(filePath, lines, 'utf8');
|
|
24
|
+
}
|
|
25
|
+
function resolveDownloadRoot() {
|
|
26
|
+
const custom = process.env.WEBAUTO_DOWNLOAD_ROOT || process.env.WEBAUTO_DOWNLOAD_DIR;
|
|
27
|
+
if (custom && custom.trim())
|
|
28
|
+
return custom;
|
|
29
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
30
|
+
return path.join(home, '.webauto', 'download');
|
|
31
|
+
}
|
|
32
|
+
async function listPages(profile, apiUrl) {
|
|
33
|
+
const res = await controllerAction('browser:page:list', { profileId: profile }, apiUrl);
|
|
34
|
+
const pages = res?.pages || res?.data?.pages || [];
|
|
35
|
+
const activeIndex = res?.activeIndex ?? res?.data?.activeIndex ?? -1;
|
|
36
|
+
return { pages, activeIndex };
|
|
37
|
+
}
|
|
38
|
+
async function switchToTab(profile, apiUrl, index) {
|
|
39
|
+
await controllerAction('browser:page:switch', { profileId: profile, index }, apiUrl).catch(() => null);
|
|
40
|
+
await delay(500);
|
|
41
|
+
const listRes = await listPages(profile, apiUrl);
|
|
42
|
+
const activeIndex = listRes.activeIndex;
|
|
43
|
+
const pages = listRes.pages || [];
|
|
44
|
+
const activeUrl = pages.find((p) => p.active)?.url || 'N/A';
|
|
45
|
+
return { activeIndex, activeUrl };
|
|
46
|
+
}
|
|
47
|
+
async function openTabViaWindowOpen(profile, apiUrl) {
|
|
48
|
+
await controllerAction('browser:page:switch', { profileId: profile, index: 0 }, apiUrl).catch(() => null);
|
|
49
|
+
await controllerAction('system:shortcut', { app: 'camoufox', shortcut: 'new-tab' }, apiUrl).catch(() => null);
|
|
50
|
+
await delay(500);
|
|
51
|
+
}
|
|
52
|
+
async function ensureTabPool(profile, apiUrl, requiredTotal = 5) {
|
|
53
|
+
const existing = await listPages(profile, apiUrl);
|
|
54
|
+
const currentCount = existing.pages.length;
|
|
55
|
+
const needed = Math.max(0, requiredTotal - currentCount);
|
|
56
|
+
if (needed > 0) {
|
|
57
|
+
for (let i = 0; i < needed; i += 1) {
|
|
58
|
+
await openTabViaWindowOpen(profile, apiUrl);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const finalPages = await listPages(profile, apiUrl);
|
|
62
|
+
return finalPages.pages.slice(1, requiredTotal).map((p) => ({ index: p.index, pageId: p.pageId }));
|
|
63
|
+
}
|
|
64
|
+
async function scrollCommentSection(profile, apiUrl, distance = 600) {
|
|
65
|
+
const res = await controllerAction('container:operation', {
|
|
66
|
+
containerId: 'xiaohongshu_detail.comment_section',
|
|
67
|
+
operationId: 'scroll',
|
|
68
|
+
sessionId: profile,
|
|
69
|
+
config: { direction: 'down', distance, repeat: 1 },
|
|
70
|
+
}, apiUrl).catch(() => null);
|
|
71
|
+
return res?.success !== false;
|
|
72
|
+
}
|
|
73
|
+
async function isCommentEnd(profile, apiUrl) {
|
|
74
|
+
const res = await controllerAction('container:operation', {
|
|
75
|
+
containerId: 'xiaohongshu_detail.comment_section.end_marker',
|
|
76
|
+
operationId: 'extract',
|
|
77
|
+
sessionId: profile,
|
|
78
|
+
config: { max_items: 1, visibleOnly: true },
|
|
79
|
+
}, apiUrl).catch(() => null);
|
|
80
|
+
const items = Array.isArray(res?.extracted) ? res.extracted : [];
|
|
81
|
+
return items.length > 0;
|
|
82
|
+
}
|
|
83
|
+
async function isCommentEmpty(profile, apiUrl) {
|
|
84
|
+
const res = await controllerAction('container:operation', {
|
|
85
|
+
containerId: 'xiaohongshu_detail.comment_section.empty_state',
|
|
86
|
+
operationId: 'extract',
|
|
87
|
+
sessionId: profile,
|
|
88
|
+
config: { max_items: 1, visibleOnly: true },
|
|
89
|
+
}, apiUrl).catch(() => null);
|
|
90
|
+
const items = Array.isArray(res?.extracted) ? res.extracted : [];
|
|
91
|
+
return items.length > 0;
|
|
92
|
+
}
|
|
93
|
+
async function extractComments(profile, apiUrl, limit) {
|
|
94
|
+
const res = await controllerAction('container:operation', {
|
|
95
|
+
containerId: 'xiaohongshu_detail.comment_section.comment_item',
|
|
96
|
+
operationId: 'extract',
|
|
97
|
+
sessionId: profile,
|
|
98
|
+
config: { max_items: limit, visibleOnly: true },
|
|
99
|
+
}, apiUrl).catch(() => null);
|
|
100
|
+
return Array.isArray(res?.extracted) ? res.extracted : [];
|
|
101
|
+
}
|
|
102
|
+
async function clickShowMore(profile, apiUrl) {
|
|
103
|
+
await controllerAction('container:operation', {
|
|
104
|
+
containerId: 'xiaohongshu_detail.comment_section.show_more_button',
|
|
105
|
+
operationId: 'click',
|
|
106
|
+
sessionId: profile,
|
|
107
|
+
config: { scroll_to_view: true, wait_after: 500, visibleOnly: true },
|
|
108
|
+
}, apiUrl).catch(() => null);
|
|
109
|
+
}
|
|
110
|
+
export async function execute(input) {
|
|
111
|
+
const { profile, keyword, env, links, maxCommentsPerNote = 50, unifiedApiUrl = 'http://127.0.0.1:7701', } = input;
|
|
112
|
+
const downloadRoot = resolveDownloadRoot();
|
|
113
|
+
console.log(`[Phase4MultiTab] 开始多Tab轮转采集,总链接: ${links.length}`);
|
|
114
|
+
const tabPool = await ensureTabPool(profile, unifiedApiUrl, 5);
|
|
115
|
+
const workers = [
|
|
116
|
+
{ slot: 1, tabIndex: tabPool[0]?.index ?? 1, currentLinkIndex: 0, commentsThisRound: 0, totalComments: 0 },
|
|
117
|
+
{ slot: 2, tabIndex: tabPool[1]?.index ?? 2, currentLinkIndex: 1, commentsThisRound: 0, totalComments: 0 },
|
|
118
|
+
{ slot: 3, tabIndex: tabPool[2]?.index ?? 3, currentLinkIndex: 2, commentsThisRound: 0, totalComments: 0 },
|
|
119
|
+
{ slot: 4, tabIndex: tabPool[3]?.index ?? 4, currentLinkIndex: 3, commentsThisRound: 0, totalComments: 0 },
|
|
120
|
+
];
|
|
121
|
+
const linkStates = links.map((l, i) => ({
|
|
122
|
+
...l,
|
|
123
|
+
index: i,
|
|
124
|
+
done: false,
|
|
125
|
+
totalComments: 0,
|
|
126
|
+
}));
|
|
127
|
+
let completedLinks = 0;
|
|
128
|
+
const errors = [];
|
|
129
|
+
let round = 0;
|
|
130
|
+
const maxRounds = links.length * 2;
|
|
131
|
+
while (completedLinks < links.length && round < maxRounds) {
|
|
132
|
+
round++;
|
|
133
|
+
const worker = workers[(round - 1) % workers.length];
|
|
134
|
+
let linkToProcess = linkStates.find(l => !l.done && l.index >= worker.currentLinkIndex);
|
|
135
|
+
if (!linkToProcess) {
|
|
136
|
+
linkToProcess = linkStates.find(l => !l.done);
|
|
137
|
+
}
|
|
138
|
+
if (!linkToProcess) {
|
|
139
|
+
console.log(`[Phase4MultiTab] 所有链接已完成`);
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
worker.currentLinkIndex = linkToProcess.index;
|
|
143
|
+
console.log(`\n[Round ${round}] slot-${worker.slot}(tab-${worker.tabIndex}) -> note ${linkToProcess.noteId}`);
|
|
144
|
+
try {
|
|
145
|
+
const activeInfo = await switchToTab(profile, unifiedApiUrl, worker.tabIndex);
|
|
146
|
+
console.log(`[Phase4MultiTab] activeIndex=${activeInfo.activeIndex} url=${String(activeInfo.activeUrl).slice(0, 60)}`);
|
|
147
|
+
const navRes = await controllerAction('browser:goto', {
|
|
148
|
+
profile,
|
|
149
|
+
url: linkToProcess.safeUrl,
|
|
150
|
+
}, unifiedApiUrl);
|
|
151
|
+
const navOk = navRes?.success === true || navRes?.result?.ok === true || navRes?.ok === true;
|
|
152
|
+
if (!navOk) {
|
|
153
|
+
console.log(`[Phase4MultiTab] navRes=${JSON.stringify(navRes)}`);
|
|
154
|
+
throw new Error(`导航失败: ${navRes?.error || JSON.stringify(navRes)}`);
|
|
155
|
+
}
|
|
156
|
+
await delay(2000);
|
|
157
|
+
await controllerAction('container:operation', {
|
|
158
|
+
containerId: 'xiaohongshu_detail.comment_button',
|
|
159
|
+
operationId: 'highlight',
|
|
160
|
+
sessionId: profile,
|
|
161
|
+
}, unifiedApiUrl).catch(() => null);
|
|
162
|
+
await controllerAction('container:operation', {
|
|
163
|
+
containerId: 'xiaohongshu_detail.comment_button',
|
|
164
|
+
operationId: 'click',
|
|
165
|
+
sessionId: profile,
|
|
166
|
+
config: { useSystemMouse: true, visibleOnly: true },
|
|
167
|
+
}, unifiedApiUrl).catch(() => null);
|
|
168
|
+
await delay(1200);
|
|
169
|
+
let collected = 0;
|
|
170
|
+
let scrollAttempts = 0;
|
|
171
|
+
const maxScrolls = Infinity;
|
|
172
|
+
await clickShowMore(profile, unifiedApiUrl);
|
|
173
|
+
await delay(800);
|
|
174
|
+
while (collected < maxCommentsPerNote && scrollAttempts < maxScrolls) {
|
|
175
|
+
const items = await extractComments(profile, unifiedApiUrl, maxCommentsPerNote - collected);
|
|
176
|
+
if (items.length === 0) {
|
|
177
|
+
const empty = await isCommentEmpty(profile, unifiedApiUrl);
|
|
178
|
+
if (empty) {
|
|
179
|
+
console.log(`[Phase4MultiTab] slot-${worker.slot} 评论区空`);
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
await scrollCommentSection(profile, unifiedApiUrl, 650);
|
|
183
|
+
await delay(900);
|
|
184
|
+
scrollAttempts++;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const noteDir = path.join(downloadRoot, 'xiaohongshu', env, keyword, linkToProcess.noteId);
|
|
188
|
+
await ensureDir(noteDir);
|
|
189
|
+
const payload = items.map((item) => ({
|
|
190
|
+
...item,
|
|
191
|
+
noteId: linkToProcess.noteId,
|
|
192
|
+
ts: new Date().toISOString(),
|
|
193
|
+
}));
|
|
194
|
+
await appendJsonl(path.join(noteDir, 'comments.jsonl'), payload);
|
|
195
|
+
collected += items.length;
|
|
196
|
+
scrollAttempts = 0;
|
|
197
|
+
const isEnd = await isCommentEnd(profile, unifiedApiUrl);
|
|
198
|
+
if (isEnd) {
|
|
199
|
+
console.log(`[Phase4MultiTab] slot-${worker.slot} 评论到底`);
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
await clickShowMore(profile, unifiedApiUrl);
|
|
203
|
+
await delay(600);
|
|
204
|
+
await scrollCommentSection(profile, unifiedApiUrl, 650);
|
|
205
|
+
await delay(900);
|
|
206
|
+
}
|
|
207
|
+
worker.commentsThisRound = collected;
|
|
208
|
+
worker.totalComments += collected;
|
|
209
|
+
linkToProcess.totalComments += collected;
|
|
210
|
+
linkToProcess.done = true;
|
|
211
|
+
completedLinks++;
|
|
212
|
+
console.log(`[Phase4MultiTab] slot-${worker.slot} ✅ 采集 ${collected} 条,该帖累计 ${linkToProcess.totalComments} 条`);
|
|
213
|
+
console.log(`[Phase4MultiTab] 进度: ${completedLinks}/${links.length} 帖子完成`);
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
const errorMsg = err?.message || String(err);
|
|
217
|
+
console.error(`[Phase4MultiTab] slot-${worker.slot} ❌ 失败: ${errorMsg}`);
|
|
218
|
+
errors.push(`${linkToProcess.noteId}: ${errorMsg}`);
|
|
219
|
+
linkToProcess.done = true;
|
|
220
|
+
completedLinks++;
|
|
221
|
+
}
|
|
222
|
+
await delay(800);
|
|
223
|
+
}
|
|
224
|
+
const totalComments = linkStates.reduce((sum, l) => sum + l.totalComments, 0);
|
|
225
|
+
console.log(`\n[Phase4MultiTab] 完成: ${completedLinks} 帖子, ${totalComments} 条评论`);
|
|
226
|
+
return {
|
|
227
|
+
success: true,
|
|
228
|
+
totalNotes: completedLinks,
|
|
229
|
+
totalComments,
|
|
230
|
+
errors,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
//# sourceMappingURL=Phase4MultiTabHarvestBlock.js.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Block: ReplyInteract(开发态:模拟回复)
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* 1) 在指定评论上点击“回复”
|
|
6
|
+
* 2) 定位回复输入框并输入内容(系统级键盘)
|
|
7
|
+
* 3) 截图留证(包含高亮与 DEV 叠加文案)
|
|
8
|
+
*
|
|
9
|
+
* 约束:
|
|
10
|
+
* - 点击必须走坐标点击(mouse:click),禁止 DOM click
|
|
11
|
+
* - 不提交(不发送真正回复)
|
|
12
|
+
*/
|
|
13
|
+
export interface ReplyInteractInput {
|
|
14
|
+
sessionId: string;
|
|
15
|
+
noteId: string;
|
|
16
|
+
commentVisibleIndex: number;
|
|
17
|
+
replyText: string;
|
|
18
|
+
dryRun?: boolean;
|
|
19
|
+
unifiedApiUrl?: string;
|
|
20
|
+
env?: string;
|
|
21
|
+
keyword?: string;
|
|
22
|
+
dev?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface ReplyInteractOutput {
|
|
25
|
+
success: boolean;
|
|
26
|
+
noteId: string;
|
|
27
|
+
typed: boolean;
|
|
28
|
+
evidence?: {
|
|
29
|
+
screenshot?: string | null;
|
|
30
|
+
};
|
|
31
|
+
debug?: {
|
|
32
|
+
replyButtonRect?: {
|
|
33
|
+
x: number;
|
|
34
|
+
y: number;
|
|
35
|
+
width: number;
|
|
36
|
+
height: number;
|
|
37
|
+
};
|
|
38
|
+
replyInputRect?: {
|
|
39
|
+
x: number;
|
|
40
|
+
y: number;
|
|
41
|
+
width: number;
|
|
42
|
+
height: number;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
error?: string;
|
|
46
|
+
}
|
|
47
|
+
export declare function execute(input: ReplyInteractInput): Promise<ReplyInteractOutput>;
|
|
48
|
+
//# sourceMappingURL=ReplyInteractBlock.d.ts.map
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Block: ReplyInteract(开发态:模拟回复)
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* 1) 在指定评论上点击“回复”
|
|
6
|
+
* 2) 定位回复输入框并输入内容(系统级键盘)
|
|
7
|
+
* 3) 截图留证(包含高亮与 DEV 叠加文案)
|
|
8
|
+
*
|
|
9
|
+
* 约束:
|
|
10
|
+
* - 点击必须走坐标点击(mouse:click),禁止 DOM click
|
|
11
|
+
* - 不提交(不发送真正回复)
|
|
12
|
+
*/
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { controllerAction, delay } from '../utils/controllerAction.js';
|
|
15
|
+
import { resolveDownloadRoot, savePngBase64, takeScreenshotBase64 } from './helpers/evidence.js';
|
|
16
|
+
async function findReplyButtonTarget(sessionId, apiUrl, commentVisibleIndex) {
|
|
17
|
+
const res = await controllerAction('browser:execute', {
|
|
18
|
+
profile: sessionId,
|
|
19
|
+
script: `(() => {
|
|
20
|
+
const idx = ${JSON.stringify(commentVisibleIndex)};
|
|
21
|
+
const isVisible = (el) => {
|
|
22
|
+
const r = el.getBoundingClientRect();
|
|
23
|
+
return r.width > 0 && r.height > 0 && r.bottom > 0 && r.top < window.innerHeight && r.right > 0 && r.left < window.innerWidth;
|
|
24
|
+
};
|
|
25
|
+
const items = Array.from(document.querySelectorAll('.comment-item')).filter(isVisible);
|
|
26
|
+
const root = items[idx];
|
|
27
|
+
if (!root) return { ok: false, reason: 'comment-not-found', total: items.length };
|
|
28
|
+
|
|
29
|
+
const textEq = (el, s) => (el && (el.textContent || '').replace(/\\s+/g,' ').trim() === s);
|
|
30
|
+
const candidates = Array.from(root.querySelectorAll('span.count,button,a,span,div'));
|
|
31
|
+
const raw = candidates.find(el => textEq(el, '回复')) || null;
|
|
32
|
+
if (!raw) return { ok: false, reason: 'reply-button-not-found' };
|
|
33
|
+
|
|
34
|
+
const target = raw.closest && (raw.closest('button,a,[role=\"button\"]') || raw) || raw;
|
|
35
|
+
const r = target.getBoundingClientRect();
|
|
36
|
+
if (!r || !r.width || !r.height) return { ok: false, reason: 'reply-rect-empty' };
|
|
37
|
+
|
|
38
|
+
const x1 = Math.max(0, r.left);
|
|
39
|
+
const y1 = Math.max(0, r.top);
|
|
40
|
+
const x2 = Math.min(window.innerWidth, r.right);
|
|
41
|
+
const y2 = Math.min(window.innerHeight, r.bottom);
|
|
42
|
+
const mx = Math.round((x1 + x2) / 2);
|
|
43
|
+
const my = Math.round((y1 + y2) / 2);
|
|
44
|
+
const pad = 10;
|
|
45
|
+
const points = [
|
|
46
|
+
{ x: mx, y: my },
|
|
47
|
+
{ x: Math.round(x1 + pad), y: my },
|
|
48
|
+
{ x: Math.round(x2 - pad), y: my },
|
|
49
|
+
{ x: mx, y: Math.round(y1 + pad) },
|
|
50
|
+
{ x: mx, y: Math.round(y2 - pad) },
|
|
51
|
+
].filter(p => Number.isFinite(p.x) && Number.isFinite(p.y) && p.x >= 0 && p.y >= 0 && p.x <= window.innerWidth && p.y <= window.innerHeight);
|
|
52
|
+
|
|
53
|
+
let clickPoint = points[0] || { x: mx, y: my };
|
|
54
|
+
for (const p of points) {
|
|
55
|
+
const hit = document.elementFromPoint(p.x, p.y);
|
|
56
|
+
if (hit && (hit === target || target.contains(hit))) { clickPoint = p; break; }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
ok: true,
|
|
61
|
+
rect: { x: Math.round(r.left), y: Math.round(r.top), width: Math.round(r.width), height: Math.round(r.height) },
|
|
62
|
+
clickPoint,
|
|
63
|
+
};
|
|
64
|
+
})()`,
|
|
65
|
+
}, apiUrl);
|
|
66
|
+
const payload = res?.result || res?.data?.result || res;
|
|
67
|
+
return payload;
|
|
68
|
+
}
|
|
69
|
+
async function findReplyInputTarget(sessionId, apiUrl) {
|
|
70
|
+
const res = await controllerAction('browser:execute', {
|
|
71
|
+
profile: sessionId,
|
|
72
|
+
script: `(() => {
|
|
73
|
+
const isVisible = (el) => {
|
|
74
|
+
if (!el || !el.getBoundingClientRect) return false;
|
|
75
|
+
const r = el.getBoundingClientRect();
|
|
76
|
+
return r.width > 0 && r.height > 0 && r.bottom > 0 && r.top < window.innerHeight && r.right > 0 && r.left < window.innerWidth;
|
|
77
|
+
};
|
|
78
|
+
const score = (el) => {
|
|
79
|
+
const ph = (el.getAttribute && (el.getAttribute('placeholder') || el.getAttribute('aria-label') || '')) || '';
|
|
80
|
+
const cls = (el.className || '').toString();
|
|
81
|
+
const hint = (ph + ' ' + cls).replace(/\\s+/g,' ').trim();
|
|
82
|
+
let s = 0;
|
|
83
|
+
if (hint.includes('回复')) s += 4;
|
|
84
|
+
if (hint.includes('评论') || hint.includes('说点什么') || hint.includes('说说')) s += 2;
|
|
85
|
+
if (cls.includes('reply')) s += 3;
|
|
86
|
+
if (cls.includes('comment')) s += 1;
|
|
87
|
+
return s;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const active = document.activeElement;
|
|
91
|
+
const isInputLike = (el) => el && (el.tagName === 'TEXTAREA' || (el.tagName === 'INPUT') || (el.isContentEditable === true));
|
|
92
|
+
if (isInputLike(active) && isVisible(active)) {
|
|
93
|
+
const r = active.getBoundingClientRect();
|
|
94
|
+
const mx = Math.round((r.left + r.right) / 2);
|
|
95
|
+
const my = Math.round((r.top + r.bottom) / 2);
|
|
96
|
+
return { ok: true, rect: { x: Math.round(r.left), y: Math.round(r.top), width: Math.round(r.width), height: Math.round(r.height) }, clickPoint: { x: mx, y: my } };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const all = Array.from(document.querySelectorAll('textarea, input[type=\"text\"], input:not([type]), [contenteditable=\"true\"], [contenteditable=\"plaintext-only\"]'))
|
|
100
|
+
.filter(isVisible);
|
|
101
|
+
if (!all.length) return { ok: false, reason: 'no-visible-input' };
|
|
102
|
+
|
|
103
|
+
const best = all
|
|
104
|
+
.map((el) => ({ el, s: score(el) }))
|
|
105
|
+
.sort((a,b) => b.s - a.s)[0];
|
|
106
|
+
const el = best.el;
|
|
107
|
+
const r = el.getBoundingClientRect();
|
|
108
|
+
const mx = Math.round((r.left + r.right) / 2);
|
|
109
|
+
const my = Math.round((r.top + r.bottom) / 2);
|
|
110
|
+
return { ok: true, rect: { x: Math.round(r.left), y: Math.round(r.top), width: Math.round(r.width), height: Math.round(r.height) }, clickPoint: { x: mx, y: my } };
|
|
111
|
+
})()`,
|
|
112
|
+
}, apiUrl);
|
|
113
|
+
const payload = res?.result || res?.data?.result || res;
|
|
114
|
+
return payload;
|
|
115
|
+
}
|
|
116
|
+
async function drawOverlay(sessionId, apiUrl, opts) {
|
|
117
|
+
const ttlMs = typeof opts.ttlMs === 'number' ? Math.max(600, Math.min(15000, Math.floor(opts.ttlMs))) : 4000;
|
|
118
|
+
await controllerAction('browser:execute', {
|
|
119
|
+
profile: sessionId,
|
|
120
|
+
script: `(() => {
|
|
121
|
+
const id = ${JSON.stringify(opts.id)};
|
|
122
|
+
const rect = ${JSON.stringify(opts.rect || null)};
|
|
123
|
+
const color = ${JSON.stringify(opts.color)};
|
|
124
|
+
const label = ${JSON.stringify(opts.label || '')};
|
|
125
|
+
const ttl = ${JSON.stringify(ttlMs)};
|
|
126
|
+
|
|
127
|
+
const ensure = (elId, baseStyle) => {
|
|
128
|
+
let el = document.getElementById(elId);
|
|
129
|
+
if (!el) {
|
|
130
|
+
el = document.createElement('div');
|
|
131
|
+
el.id = elId;
|
|
132
|
+
document.body.appendChild(el);
|
|
133
|
+
}
|
|
134
|
+
Object.assign(el.style, baseStyle);
|
|
135
|
+
return el;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
if (rect) {
|
|
139
|
+
ensure(id, {
|
|
140
|
+
position: 'fixed',
|
|
141
|
+
left: rect.x + 'px',
|
|
142
|
+
top: rect.y + 'px',
|
|
143
|
+
width: rect.width + 'px',
|
|
144
|
+
height: rect.height + 'px',
|
|
145
|
+
border: '3px solid ' + color,
|
|
146
|
+
boxSizing: 'border-box',
|
|
147
|
+
zIndex: '2147483647',
|
|
148
|
+
pointerEvents: 'none',
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (label) {
|
|
153
|
+
ensure(id + '-label', {
|
|
154
|
+
position: 'fixed',
|
|
155
|
+
left: '12px',
|
|
156
|
+
top: '12px',
|
|
157
|
+
maxWidth: '70vw',
|
|
158
|
+
padding: '8px 10px',
|
|
159
|
+
background: 'rgba(0,0,0,0.75)',
|
|
160
|
+
color: '#fff',
|
|
161
|
+
fontSize: '12px',
|
|
162
|
+
lineHeight: '1.3',
|
|
163
|
+
borderRadius: '8px',
|
|
164
|
+
zIndex: '2147483647',
|
|
165
|
+
pointerEvents: 'none',
|
|
166
|
+
whiteSpace: 'pre-wrap',
|
|
167
|
+
}).textContent = label;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
setTimeout(() => {
|
|
171
|
+
try {
|
|
172
|
+
const a = document.getElementById(id);
|
|
173
|
+
if (a && a.parentElement) a.parentElement.removeChild(a);
|
|
174
|
+
const b = document.getElementById(id + '-label');
|
|
175
|
+
if (b && b.parentElement) b.parentElement.removeChild(b);
|
|
176
|
+
} catch {}
|
|
177
|
+
}, ttl);
|
|
178
|
+
|
|
179
|
+
return true;
|
|
180
|
+
})()`,
|
|
181
|
+
}, apiUrl).catch(() => null);
|
|
182
|
+
}
|
|
183
|
+
async function verifyTyped(sessionId, apiUrl, expected) {
|
|
184
|
+
const res = await controllerAction('browser:execute', {
|
|
185
|
+
profile: sessionId,
|
|
186
|
+
script: `(() => {
|
|
187
|
+
const exp = ${JSON.stringify(expected)};
|
|
188
|
+
const el = document.activeElement;
|
|
189
|
+
if (!el) return { ok: false, value: '' };
|
|
190
|
+
let v = '';
|
|
191
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) v = el.value || '';
|
|
192
|
+
else if (el instanceof HTMLElement && el.isContentEditable) v = (el.textContent || '');
|
|
193
|
+
v = (v || '').replace(/\\s+/g,' ').trim();
|
|
194
|
+
return { ok: true, value: v, contains: exp ? v.includes(exp) : false };
|
|
195
|
+
})()`,
|
|
196
|
+
}, apiUrl).catch(() => null);
|
|
197
|
+
const payload = res?.result || res?.data?.result || res;
|
|
198
|
+
return Boolean(payload?.contains);
|
|
199
|
+
}
|
|
200
|
+
export async function execute(input) {
|
|
201
|
+
const { sessionId, noteId, commentVisibleIndex, replyText, dryRun = false, unifiedApiUrl = 'http://127.0.0.1:7701', env = 'debug', keyword = 'unknown', dev = true, } = input;
|
|
202
|
+
let screenshot = null;
|
|
203
|
+
let replyButtonRect = undefined;
|
|
204
|
+
let replyInputRect = undefined;
|
|
205
|
+
try {
|
|
206
|
+
const btn = await findReplyButtonTarget(sessionId, unifiedApiUrl, commentVisibleIndex);
|
|
207
|
+
if (!btn.ok || !btn.clickPoint || !btn.rect) {
|
|
208
|
+
throw new Error(`reply button not found: ${btn.reason || 'unknown'}`);
|
|
209
|
+
}
|
|
210
|
+
replyButtonRect = btn.rect;
|
|
211
|
+
await drawOverlay(sessionId, unifiedApiUrl, {
|
|
212
|
+
id: 'webauto-reply-button-rect',
|
|
213
|
+
rect: btn.rect,
|
|
214
|
+
color: '#00e5ff',
|
|
215
|
+
ttlMs: 6000,
|
|
216
|
+
});
|
|
217
|
+
await delay(350);
|
|
218
|
+
if (!dryRun) {
|
|
219
|
+
// ✅ 坐标点击(系统点击)
|
|
220
|
+
await controllerAction('mouse:click', { profileId: sessionId, x: Math.round(btn.clickPoint.x), y: Math.round(btn.clickPoint.y) }, unifiedApiUrl);
|
|
221
|
+
await delay(700);
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
await delay(450);
|
|
225
|
+
}
|
|
226
|
+
const inputTarget = await findReplyInputTarget(sessionId, unifiedApiUrl);
|
|
227
|
+
if (inputTarget.ok && inputTarget.clickPoint && inputTarget.rect) {
|
|
228
|
+
replyInputRect = inputTarget.rect;
|
|
229
|
+
await drawOverlay(sessionId, unifiedApiUrl, {
|
|
230
|
+
id: 'webauto-reply-input-rect',
|
|
231
|
+
rect: inputTarget.rect,
|
|
232
|
+
color: '#ff00ff',
|
|
233
|
+
ttlMs: 6000,
|
|
234
|
+
});
|
|
235
|
+
await delay(250);
|
|
236
|
+
if (!dryRun) {
|
|
237
|
+
// ✅ 坐标点击聚焦输入框
|
|
238
|
+
await controllerAction('mouse:click', { profileId: sessionId, x: Math.round(inputTarget.clickPoint.x), y: Math.round(inputTarget.clickPoint.y) }, unifiedApiUrl);
|
|
239
|
+
await delay(220);
|
|
240
|
+
// 清空(可选):dev 模式仍执行,避免误拼接
|
|
241
|
+
const isMac = process.platform === 'darwin';
|
|
242
|
+
await controllerAction('keyboard:press', { profileId: sessionId, key: isMac ? 'Meta+A' : 'Control+A' }, unifiedApiUrl).catch(() => { });
|
|
243
|
+
await delay(80);
|
|
244
|
+
await controllerAction('keyboard:press', { profileId: sessionId, key: 'Backspace' }, unifiedApiUrl).catch(() => { });
|
|
245
|
+
await delay(120);
|
|
246
|
+
// ✅ 系统级输入(不提交)
|
|
247
|
+
await controllerAction('keyboard:type', { profileId: sessionId, text: String(replyText || ''), delay: 90, submit: false }, unifiedApiUrl);
|
|
248
|
+
await delay(260);
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
await delay(180);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const typed = !dryRun && replyInputRect
|
|
255
|
+
? await verifyTyped(sessionId, unifiedApiUrl, String(replyText || '').slice(0, 6))
|
|
256
|
+
: false;
|
|
257
|
+
if (dev || dryRun) {
|
|
258
|
+
await drawOverlay(sessionId, unifiedApiUrl, {
|
|
259
|
+
id: 'webauto-dev-reply-label',
|
|
260
|
+
color: '#00ff00',
|
|
261
|
+
label: `[${dryRun ? 'DRYRUN' : 'DEV'}] note=${noteId} commentIdx=${commentVisibleIndex}\nreply: ${String(replyText || '').slice(0, 80)}`,
|
|
262
|
+
ttlMs: 9000,
|
|
263
|
+
});
|
|
264
|
+
await delay(180);
|
|
265
|
+
}
|
|
266
|
+
const base64 = await takeScreenshotBase64(sessionId, unifiedApiUrl);
|
|
267
|
+
if (base64) {
|
|
268
|
+
const outDir = path.join(resolveDownloadRoot(), 'xiaohongshu', env, keyword, 'smart-reply', noteId);
|
|
269
|
+
const name = `reply-dev-${String(commentVisibleIndex).padStart(3, '0')}-${Date.now()}.png`;
|
|
270
|
+
screenshot = await savePngBase64(base64, path.join(outDir, name));
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
success: true,
|
|
274
|
+
noteId,
|
|
275
|
+
typed,
|
|
276
|
+
evidence: { screenshot },
|
|
277
|
+
debug: { replyButtonRect, replyInputRect },
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
catch (e) {
|
|
281
|
+
return {
|
|
282
|
+
success: false,
|
|
283
|
+
noteId,
|
|
284
|
+
typed: false,
|
|
285
|
+
evidence: { screenshot },
|
|
286
|
+
debug: { replyButtonRect, replyInputRect },
|
|
287
|
+
error: e?.message || String(e),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
//# sourceMappingURL=ReplyInteractBlock.js.map
|
package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/XhsDiscoverFallbackBlock.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XHS Discover Fallback Block
|
|
3
|
+
*
|
|
4
|
+
* 独立的回退 Block:当出现 shell-page(URL=/explore/<id> 但 DOM=搜索结果)时,
|
|
5
|
+
* 通过点击「发现」按钮重置状态,然后重新搜索以获取正确的 /search_result URL。
|
|
6
|
+
*
|
|
7
|
+
* 约束:全系统级操作(container + keyboard/mouse),禁止 goto/refresh/url 拼接。
|
|
8
|
+
*/
|
|
9
|
+
export interface DiscoverFallbackInput {
|
|
10
|
+
keyword: string;
|
|
11
|
+
profile?: string;
|
|
12
|
+
unifiedApiUrl?: string;
|
|
13
|
+
env?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface DiscoverFallbackOutput {
|
|
16
|
+
success: boolean;
|
|
17
|
+
finalUrl: string;
|
|
18
|
+
finalCheckpoint: string;
|
|
19
|
+
screenshotPath?: string;
|
|
20
|
+
domDumpPath?: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function execute(input: DiscoverFallbackInput): Promise<DiscoverFallbackOutput>;
|
|
23
|
+
//# sourceMappingURL=XhsDiscoverFallbackBlock.d.ts.map
|