@web-auto/webauto 0.1.4 → 0.1.6
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 +915 -85
- 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 +2415 -470
- 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 +236 -29
- 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
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { execute as detectPageState } from '../../../../workflow/blocks/DetectPageStateBlock.js';
|
|
2
|
+
import { execute as anchorVerify } from '../../../../workflow/blocks/AnchorVerificationBlock.js';
|
|
3
|
+
import { execute as errorRecovery } from '../../../../workflow/blocks/ErrorRecoveryBlock.js';
|
|
4
|
+
function hasAny(matchIds, ids) {
|
|
5
|
+
const set = new Set(matchIds || []);
|
|
6
|
+
return ids.some((x) => set.has(x));
|
|
7
|
+
}
|
|
8
|
+
function hasAll(matchIds, ids) {
|
|
9
|
+
const set = new Set(matchIds || []);
|
|
10
|
+
return ids.every((x) => set.has(x));
|
|
11
|
+
}
|
|
12
|
+
export async function detectXhsCheckpoint(input) {
|
|
13
|
+
const { sessionId, serviceUrl = 'http://127.0.0.1:7701' } = input;
|
|
14
|
+
const state = await detectPageState({ sessionId, platform: 'xiaohongshu', serviceUrl });
|
|
15
|
+
const url = String(state?.url || '').trim();
|
|
16
|
+
const stage = String(state?.stage || 'unknown');
|
|
17
|
+
const rootId = state?.rootId ?? null;
|
|
18
|
+
const matchIds = Array.isArray(state?.matchIds) ? state.matchIds : [];
|
|
19
|
+
const dom = {
|
|
20
|
+
hasDetailMask: typeof state?.dom?.hasDetailMask === 'boolean' ? state.dom.hasDetailMask : undefined,
|
|
21
|
+
hasSearchInput: typeof state?.dom?.hasSearchInput === 'boolean' ? state.dom.hasSearchInput : undefined,
|
|
22
|
+
readyState: typeof state?.dom?.readyState === 'string' ? state.dom.readyState : undefined,
|
|
23
|
+
title: typeof state?.dom?.title === 'string' ? state.dom.title : undefined,
|
|
24
|
+
};
|
|
25
|
+
const signals = [];
|
|
26
|
+
const allIds = [String(rootId || ''), ...matchIds].filter(Boolean);
|
|
27
|
+
// Priority 0 (DOM-first): XHS may keep /explore/<id> in the URL even after closing the detail modal.
|
|
28
|
+
// In that case URL-based assumptions are wrong; prefer DOM signals.
|
|
29
|
+
if (dom.hasDetailMask === false && dom.hasSearchInput === true) {
|
|
30
|
+
const isInSearch = url.includes('/search_result') || url.includes('keyword=');
|
|
31
|
+
const checkpoint = isInSearch ? 'search_ready' : 'home_ready';
|
|
32
|
+
return {
|
|
33
|
+
success: true,
|
|
34
|
+
checkpoint,
|
|
35
|
+
stage,
|
|
36
|
+
url,
|
|
37
|
+
rootId,
|
|
38
|
+
matchIds,
|
|
39
|
+
signals: ['no_detail_mask', 'has_search_input', checkpoint],
|
|
40
|
+
dom,
|
|
41
|
+
error: state?.error,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
// Risk-control / captcha URL patterns are hard stops (even if container match fails).
|
|
45
|
+
// We must avoid any automated retries here to reduce further risk-control triggers.
|
|
46
|
+
const lowerUrl = url.toLowerCase();
|
|
47
|
+
if (lowerUrl.includes('/website-login/captcha') ||
|
|
48
|
+
lowerUrl.includes('verifyuuid=') ||
|
|
49
|
+
lowerUrl.includes('verifytype=') ||
|
|
50
|
+
lowerUrl.includes('verifybiz=') ||
|
|
51
|
+
lowerUrl.includes('/website-login/verify') ||
|
|
52
|
+
lowerUrl.includes('/website-login/security')) {
|
|
53
|
+
return {
|
|
54
|
+
success: true,
|
|
55
|
+
checkpoint: 'risk_control',
|
|
56
|
+
stage,
|
|
57
|
+
url,
|
|
58
|
+
rootId,
|
|
59
|
+
matchIds,
|
|
60
|
+
signals: ['risk_control_url'],
|
|
61
|
+
dom,
|
|
62
|
+
error: state?.error,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// offsite is a hard stop
|
|
66
|
+
if (!url || !url.includes('xiaohongshu.com')) {
|
|
67
|
+
return { success: false, checkpoint: 'offsite', stage, url, rootId, matchIds, signals: ['offsite'], dom, error: state?.error };
|
|
68
|
+
}
|
|
69
|
+
// login guard
|
|
70
|
+
if (hasAny(allIds, ['xiaohongshu_login.login_guard'])) {
|
|
71
|
+
return { success: true, checkpoint: 'login_guard', stage, url, rootId, matchIds, signals: ['login_guard'], dom };
|
|
72
|
+
}
|
|
73
|
+
// risk control (placeholder ids; extend as container library grows)
|
|
74
|
+
if (hasAny(allIds, ['xiaohongshu_login.qrcode_guard', 'xiaohongshu_login.captcha_guard'])) {
|
|
75
|
+
return { success: true, checkpoint: 'risk_control', stage, url, rootId, matchIds, signals: ['risk_control'], dom };
|
|
76
|
+
}
|
|
77
|
+
// comments_ready
|
|
78
|
+
if (hasAny(allIds, [
|
|
79
|
+
'xiaohongshu_detail.comment_section',
|
|
80
|
+
'xiaohongshu_detail.comment_section.comment_item',
|
|
81
|
+
'xiaohongshu_detail.end_marker',
|
|
82
|
+
])) {
|
|
83
|
+
return { success: true, checkpoint: 'comments_ready', stage, url, rootId, matchIds, signals: ['comments_anchor'], dom };
|
|
84
|
+
}
|
|
85
|
+
// detail_ready
|
|
86
|
+
if (hasAll(allIds, ['xiaohongshu_detail.modal_shell', 'xiaohongshu_detail.content_anchor'])) {
|
|
87
|
+
return { success: true, checkpoint: 'detail_ready', stage, url, rootId, matchIds, signals: ['detail_shell', 'content_anchor'], dom };
|
|
88
|
+
}
|
|
89
|
+
// search_ready
|
|
90
|
+
if (hasAll(allIds, ['xiaohongshu_search.search_bar', 'xiaohongshu_search.search_result_list'])) {
|
|
91
|
+
return { success: true, checkpoint: 'search_ready', stage, url, rootId, matchIds, signals: ['search_bar', 'search_result_list'], dom };
|
|
92
|
+
}
|
|
93
|
+
// home_ready
|
|
94
|
+
if (hasAny(allIds, ['xiaohongshu_home.search_input', 'xiaohongshu_home'])) {
|
|
95
|
+
// IMPORTANT:
|
|
96
|
+
// On XHS, the URL may still contain /explore/<noteId>?xsec_token=... even after the detail modal is closed.
|
|
97
|
+
// We must prefer DOM/container evidence over URL.
|
|
98
|
+
// If detail mask is absent and home/search anchors are present, treat as home_ready.
|
|
99
|
+
return { success: true, checkpoint: 'home_ready', stage, url, rootId, matchIds, signals: ['home'], dom };
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
success: stage !== 'unknown',
|
|
103
|
+
checkpoint: 'unknown',
|
|
104
|
+
stage,
|
|
105
|
+
url,
|
|
106
|
+
rootId,
|
|
107
|
+
matchIds,
|
|
108
|
+
signals,
|
|
109
|
+
dom,
|
|
110
|
+
error: state?.error,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
async function highlightAnchors(sessionId, serviceUrl, ids, ms) {
|
|
114
|
+
for (const id of ids) {
|
|
115
|
+
try {
|
|
116
|
+
await anchorVerify({ sessionId, containerId: id, operation: 'enter', serviceUrl });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// try next
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function fallbackTarget(target) {
|
|
125
|
+
// one-level-up policy
|
|
126
|
+
if (target === 'search_ready')
|
|
127
|
+
return 'home_ready';
|
|
128
|
+
if (target === 'comments_ready')
|
|
129
|
+
return 'detail_ready';
|
|
130
|
+
if (target === 'detail_ready')
|
|
131
|
+
return 'search_ready';
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
export async function ensureXhsCheckpoint(input) {
|
|
135
|
+
const { sessionId, target, serviceUrl = 'http://127.0.0.1:7701', timeoutMs = 15000, allowOneLevelUpFallback = true, evidence = { highlightMs: 1200 }, } = input;
|
|
136
|
+
const start = Date.now();
|
|
137
|
+
const attempts = [];
|
|
138
|
+
const det0 = await detectXhsCheckpoint({ sessionId, serviceUrl });
|
|
139
|
+
const from = det0.checkpoint;
|
|
140
|
+
let last = det0;
|
|
141
|
+
// quick success
|
|
142
|
+
if (from === target) {
|
|
143
|
+
return { success: true, from, to: target, reached: target, url: det0.url, stage: det0.stage, attempts, signals: det0.signals };
|
|
144
|
+
}
|
|
145
|
+
while (Date.now() - start < timeoutMs) {
|
|
146
|
+
last = await detectXhsCheckpoint({ sessionId, serviceUrl });
|
|
147
|
+
if (last.checkpoint === target) {
|
|
148
|
+
return { success: true, from, to: target, reached: target, url: last.url, stage: last.stage, attempts, signals: last.signals };
|
|
149
|
+
}
|
|
150
|
+
// Always highlight best-effort so user sees where we are.
|
|
151
|
+
const hm = Math.max(200, Math.min(2000, Number(evidence.highlightMs || 1200)));
|
|
152
|
+
if (last.checkpoint === 'detail_ready' || last.checkpoint === 'comments_ready') {
|
|
153
|
+
await highlightAnchors(sessionId, serviceUrl, ['xiaohongshu_detail.modal_shell', 'xiaohongshu_detail.content_anchor'], hm);
|
|
154
|
+
}
|
|
155
|
+
else if (last.checkpoint === 'search_ready') {
|
|
156
|
+
await highlightAnchors(sessionId, serviceUrl, ['xiaohongshu_search.search_bar', 'xiaohongshu_search.search_result_list'], hm);
|
|
157
|
+
}
|
|
158
|
+
else if (last.checkpoint === 'home_ready') {
|
|
159
|
+
await highlightAnchors(sessionId, serviceUrl, ['xiaohongshu_home.search_input'], hm);
|
|
160
|
+
}
|
|
161
|
+
// Recovery actions: conservative (ESC-based).
|
|
162
|
+
if ((target === 'search_ready' || target === 'home_ready') &&
|
|
163
|
+
(last.checkpoint === 'detail_ready' || last.checkpoint === 'comments_ready')) {
|
|
164
|
+
try {
|
|
165
|
+
await errorRecovery({
|
|
166
|
+
sessionId,
|
|
167
|
+
fromStage: 'detail',
|
|
168
|
+
targetStage: target === 'home_ready' ? 'home' : 'search',
|
|
169
|
+
recoveryMode: 'esc',
|
|
170
|
+
serviceUrl,
|
|
171
|
+
maxRetries: 3,
|
|
172
|
+
});
|
|
173
|
+
attempts.push({ action: 'esc', ok: true });
|
|
174
|
+
}
|
|
175
|
+
catch (e) {
|
|
176
|
+
attempts.push({ action: 'esc', ok: false, reason: e?.message || String(e) });
|
|
177
|
+
}
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
// No safe automated path; break to fallback or fail.
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
// one-level-up fallback
|
|
184
|
+
if (allowOneLevelUpFallback) {
|
|
185
|
+
const up = fallbackTarget(target);
|
|
186
|
+
if (up && up !== target) {
|
|
187
|
+
const res = await ensureXhsCheckpoint({
|
|
188
|
+
sessionId,
|
|
189
|
+
target: up,
|
|
190
|
+
serviceUrl,
|
|
191
|
+
timeoutMs,
|
|
192
|
+
allowOneLevelUpFallback: false,
|
|
193
|
+
evidence,
|
|
194
|
+
});
|
|
195
|
+
if (res.success) {
|
|
196
|
+
return {
|
|
197
|
+
success: false,
|
|
198
|
+
from,
|
|
199
|
+
to: target,
|
|
200
|
+
reached: up,
|
|
201
|
+
url: res.url,
|
|
202
|
+
stage: res.stage,
|
|
203
|
+
attempts: [...attempts, ...res.attempts, { action: 'need_user_action', ok: false, reason: `need to reach ${target}` }],
|
|
204
|
+
signals: res.signals,
|
|
205
|
+
error: `fallback_reached_${up}_need_${target}`,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
success: false,
|
|
212
|
+
from,
|
|
213
|
+
to: target,
|
|
214
|
+
reached: last.checkpoint,
|
|
215
|
+
url: last.url,
|
|
216
|
+
stage: last.stage,
|
|
217
|
+
attempts,
|
|
218
|
+
signals: last.signals,
|
|
219
|
+
error: `ensure_checkpoint_failed target=${target} reached=${last.checkpoint}`,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
//# sourceMappingURL=checkpoints.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Controller Action 工具函数
|
|
3
|
+
*/
|
|
4
|
+
export declare function controllerAction(action: string, payload: any, apiUrl: string): Promise<any>;
|
|
5
|
+
export declare function delay(ms: number): Promise<void>;
|
|
6
|
+
export declare function expandHome(p: string): string;
|
|
7
|
+
export declare function ensureDir(dir: string): Promise<void>;
|
|
8
|
+
export declare function writeFile(filePath: string, content: string): Promise<void>;
|
|
9
|
+
export declare function downloadImage(url: string, destPath: string): Promise<void>;
|
|
10
|
+
//# sourceMappingURL=controllerAction.d.ts.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Controller Action 工具函数
|
|
3
|
+
*/
|
|
4
|
+
export async function controllerAction(action, payload, apiUrl) {
|
|
5
|
+
const timeoutMs = typeof payload?.timeoutMs === 'number' && Number.isFinite(payload.timeoutMs) && payload.timeoutMs > 0
|
|
6
|
+
? Math.floor(payload.timeoutMs)
|
|
7
|
+
: 60000;
|
|
8
|
+
const res = await fetch(`${apiUrl}/v1/controller/action`, {
|
|
9
|
+
method: 'POST',
|
|
10
|
+
headers: { 'Content-Type': 'application/json' },
|
|
11
|
+
// Unified API expects payload nested under `payload`.
|
|
12
|
+
body: JSON.stringify({ action, payload: payload || {} }),
|
|
13
|
+
// 截图/容器操作在某些页面会更慢,允许调用方覆盖超时。
|
|
14
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
15
|
+
});
|
|
16
|
+
const data = await res.json().catch(() => ({}));
|
|
17
|
+
return data.data || data;
|
|
18
|
+
}
|
|
19
|
+
export function delay(ms) {
|
|
20
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
21
|
+
}
|
|
22
|
+
export function expandHome(p) {
|
|
23
|
+
if (!p)
|
|
24
|
+
return p;
|
|
25
|
+
if (p.startsWith('~/'))
|
|
26
|
+
return `${process.env.HOME}/${p.slice(2)}`;
|
|
27
|
+
return p;
|
|
28
|
+
}
|
|
29
|
+
export async function ensureDir(dir) {
|
|
30
|
+
const { mkdir } = await import('node:fs/promises');
|
|
31
|
+
await mkdir(dir, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
export async function writeFile(filePath, content) {
|
|
34
|
+
const { writeFile: wf } = await import('node:fs/promises');
|
|
35
|
+
await wf(filePath, content, 'utf8');
|
|
36
|
+
}
|
|
37
|
+
export async function downloadImage(url, destPath) {
|
|
38
|
+
const response = await fetch(url);
|
|
39
|
+
const buffer = await response.arrayBuffer();
|
|
40
|
+
const { writeFile: wf } = await import('node:fs/promises');
|
|
41
|
+
await wf(destPath, Buffer.from(buffer));
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=controllerAction.js.map
|
|
@@ -251,17 +251,119 @@ class UnifiedApiServer {
|
|
|
251
251
|
server.on('request', (req, res) => {
|
|
252
252
|
void (async () => {
|
|
253
253
|
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
254
|
+
const normalizeTaskPhase = (value) => {
|
|
255
|
+
const text = String(value || '').trim().toLowerCase();
|
|
256
|
+
if (text === 'phase1' || text === 'phase2' || text === 'phase3' || text === 'phase4' || text === 'unified' || text === 'orchestrate') {
|
|
257
|
+
return text;
|
|
258
|
+
}
|
|
259
|
+
return 'unknown';
|
|
260
|
+
};
|
|
261
|
+
const normalizeTaskStatus = (value) => {
|
|
262
|
+
const text = String(value || '').trim().toLowerCase();
|
|
263
|
+
if (text === 'starting' || text === 'running' || text === 'paused' || text === 'completed' || text === 'failed' || text === 'aborted') {
|
|
264
|
+
return text;
|
|
265
|
+
}
|
|
266
|
+
return '';
|
|
267
|
+
};
|
|
268
|
+
const ensureTask = (runId, seed = {}) => {
|
|
269
|
+
const normalizedRunId = String(runId || '').trim();
|
|
270
|
+
if (!normalizedRunId)
|
|
271
|
+
return null;
|
|
272
|
+
const existing = this.taskRegistry.getTask(normalizedRunId);
|
|
273
|
+
if (existing)
|
|
274
|
+
return existing;
|
|
275
|
+
const profileId = String(seed?.profileId || 'unknown').trim() || 'unknown';
|
|
276
|
+
const keyword = String(seed?.keyword || '').trim();
|
|
277
|
+
const phase = normalizeTaskPhase(seed?.phase);
|
|
278
|
+
return this.taskRegistry.createTask({
|
|
279
|
+
runId: normalizedRunId,
|
|
280
|
+
profileId,
|
|
281
|
+
keyword,
|
|
282
|
+
phase,
|
|
283
|
+
});
|
|
284
|
+
};
|
|
285
|
+
const applyTaskPatch = (runId, payload = {}) => {
|
|
286
|
+
const normalizedRunId = String(runId || '').trim();
|
|
287
|
+
if (!normalizedRunId)
|
|
288
|
+
return;
|
|
289
|
+
ensureTask(normalizedRunId, payload);
|
|
290
|
+
const phase = normalizeTaskPhase(payload?.phase);
|
|
291
|
+
const profileId = String(payload?.profileId || '').trim();
|
|
292
|
+
const keyword = String(payload?.keyword || '').trim();
|
|
293
|
+
const details = payload?.details && typeof payload.details === 'object' ? payload.details : undefined;
|
|
294
|
+
const patch = {};
|
|
295
|
+
if (phase !== 'unknown')
|
|
296
|
+
patch.phase = phase;
|
|
297
|
+
if (profileId)
|
|
298
|
+
patch.profileId = profileId;
|
|
299
|
+
if (keyword)
|
|
300
|
+
patch.keyword = keyword;
|
|
301
|
+
if (details)
|
|
302
|
+
patch.details = details;
|
|
303
|
+
if (Object.keys(patch).length > 0) {
|
|
304
|
+
this.taskRegistry.updateTask(normalizedRunId, patch);
|
|
305
|
+
}
|
|
306
|
+
if (payload?.progress && typeof payload.progress === 'object') {
|
|
307
|
+
this.taskRegistry.updateProgress(normalizedRunId, payload.progress);
|
|
308
|
+
}
|
|
309
|
+
if (payload?.stats && typeof payload.stats === 'object') {
|
|
310
|
+
this.taskRegistry.updateStats(normalizedRunId, payload.stats);
|
|
311
|
+
}
|
|
312
|
+
const status = normalizeTaskStatus(payload?.status);
|
|
313
|
+
if (status) {
|
|
314
|
+
this.taskRegistry.setStatus(normalizedRunId, status);
|
|
315
|
+
}
|
|
316
|
+
const errorPayload = payload?.error && typeof payload.error === 'object'
|
|
317
|
+
? payload.error
|
|
318
|
+
: (payload?.lastError && typeof payload.lastError === 'object' ? payload.lastError : null);
|
|
319
|
+
if (errorPayload) {
|
|
320
|
+
this.taskRegistry.setError(normalizedRunId, {
|
|
321
|
+
message: String(errorPayload.message || 'task_error'),
|
|
322
|
+
code: String(errorPayload.code || 'TASK_ERROR'),
|
|
323
|
+
timestamp: Number(errorPayload.timestamp || Date.now()),
|
|
324
|
+
recoverable: Boolean(errorPayload.recoverable),
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
};
|
|
254
328
|
// Container operations endpoints
|
|
255
329
|
const containerHandled = await handleContainerOperations(req, res, sessionManager, this.containerExecutor);
|
|
256
330
|
if (containerHandled)
|
|
257
331
|
return;
|
|
258
332
|
// Task state API endpoints
|
|
333
|
+
if (req.method === 'POST' && url.pathname === '/api/v1/tasks') {
|
|
334
|
+
try {
|
|
335
|
+
const payload = await this.readJsonBody(req);
|
|
336
|
+
const runId = String(payload?.runId || payload?.id || '').trim();
|
|
337
|
+
if (!runId)
|
|
338
|
+
throw new Error('runId is required');
|
|
339
|
+
ensureTask(runId, payload);
|
|
340
|
+
applyTaskPatch(runId, payload);
|
|
341
|
+
const task = this.taskRegistry.getTask(runId);
|
|
342
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
343
|
+
res.end(JSON.stringify({ success: true, data: task }));
|
|
344
|
+
}
|
|
345
|
+
catch (err) {
|
|
346
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
347
|
+
res.end(JSON.stringify({ success: false, error: err?.message || String(err) }));
|
|
348
|
+
}
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
259
351
|
if (req.method === 'GET' && url.pathname === '/api/v1/tasks') {
|
|
260
352
|
const tasks = this.taskRegistry.getAllTasks();
|
|
261
353
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
262
354
|
res.end(JSON.stringify({ success: true, data: tasks }));
|
|
263
355
|
return;
|
|
264
356
|
}
|
|
357
|
+
if (req.method === 'GET' && url.pathname.includes('/api/v1/tasks/') && url.pathname.includes('/events')) {
|
|
358
|
+
const parts = url.pathname.split('/');
|
|
359
|
+
const tasksIndex = parts.indexOf('tasks');
|
|
360
|
+
const runId = parts[tasksIndex + 1];
|
|
361
|
+
const since = url.searchParams.get('since');
|
|
362
|
+
const events = this.taskRegistry.getEvents(runId, since ? Number(since) : undefined);
|
|
363
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
364
|
+
res.end(JSON.stringify({ success: true, data: events }));
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
265
367
|
if (req.method === 'GET' && url.pathname.startsWith('/api/v1/tasks/')) {
|
|
266
368
|
const parts = url.pathname.split('/');
|
|
267
369
|
const runId = parts[parts.length - 1];
|
|
@@ -275,23 +377,13 @@ class UnifiedApiServer {
|
|
|
275
377
|
res.end(JSON.stringify({ success: true, data: task }));
|
|
276
378
|
return;
|
|
277
379
|
}
|
|
278
|
-
if (req.method === 'GET' && url.pathname.includes('/api/v1/tasks/') && url.pathname.includes('/events')) {
|
|
279
|
-
const parts = url.pathname.split('/');
|
|
280
|
-
const tasksIndex = parts.indexOf('tasks');
|
|
281
|
-
const runId = parts[tasksIndex + 1];
|
|
282
|
-
const since = url.searchParams.get('since');
|
|
283
|
-
const events = this.taskRegistry.getEvents(runId, since ? Number(since) : undefined);
|
|
284
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
285
|
-
res.end(JSON.stringify({ success: true, data: events }));
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
380
|
if (req.method === 'POST' && url.pathname.includes('/api/v1/tasks/') && url.pathname.includes('/update')) {
|
|
289
381
|
const parts = url.pathname.split('/');
|
|
290
382
|
const tasksIndex = parts.indexOf('tasks');
|
|
291
383
|
const runId = parts[tasksIndex + 1];
|
|
292
384
|
try {
|
|
293
385
|
const payload = await this.readJsonBody(req);
|
|
294
|
-
|
|
386
|
+
applyTaskPatch(runId, payload);
|
|
295
387
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
296
388
|
res.end(JSON.stringify({ success: true }));
|
|
297
389
|
}
|
|
@@ -307,6 +399,7 @@ class UnifiedApiServer {
|
|
|
307
399
|
const runId = parts[tasksIndex + 1];
|
|
308
400
|
try {
|
|
309
401
|
const event = await this.readJsonBody(req);
|
|
402
|
+
ensureTask(runId, event?.data || {});
|
|
310
403
|
this.taskRegistry.pushEvent(runId, event.type, event.data);
|
|
311
404
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
312
405
|
res.end(JSON.stringify({ success: true }));
|
|
@@ -323,6 +416,7 @@ class UnifiedApiServer {
|
|
|
323
416
|
const runId = parts[tasksIndex + 1];
|
|
324
417
|
const action = url.searchParams.get('action');
|
|
325
418
|
try {
|
|
419
|
+
ensureTask(runId, {});
|
|
326
420
|
if (action === 'pause') {
|
|
327
421
|
this.taskRegistry.setStatus(runId, 'paused');
|
|
328
422
|
}
|
|
@@ -308,9 +308,29 @@ async function handleCommand(
|
|
|
308
308
|
...(args.ownerPid ? { ownerPid: args.ownerPid } : {}),
|
|
309
309
|
};
|
|
310
310
|
const res = await manager.createSession(opts);
|
|
311
|
+
const session = manager.getSession(opts.profileId);
|
|
312
|
+
if (!session) {
|
|
313
|
+
throw new Error(`session for profile ${opts.profileId} not started`);
|
|
314
|
+
}
|
|
315
|
+
let recording = null;
|
|
316
|
+
if (args.record === true || args.recording === true) {
|
|
317
|
+
recording = await session.startRecording({
|
|
318
|
+
name: args.recordName || args.recordingName,
|
|
319
|
+
outputPath: args.recordOutput || args.recordingOutput || args.recordOutputPath,
|
|
320
|
+
overlay: typeof args.recordOverlay === 'boolean' ? args.recordOverlay : undefined,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
311
323
|
options.onSessionStart?.();
|
|
312
324
|
broadcast('browser:started', { profileId: opts.profileId, sessionId: res.sessionId });
|
|
313
|
-
return {
|
|
325
|
+
return {
|
|
326
|
+
ok: true,
|
|
327
|
+
body: {
|
|
328
|
+
ok: true,
|
|
329
|
+
sessionId: res.sessionId,
|
|
330
|
+
profileId: opts.profileId,
|
|
331
|
+
...(recording ? { recording } : {}),
|
|
332
|
+
},
|
|
333
|
+
};
|
|
314
334
|
}
|
|
315
335
|
case 'goto': {
|
|
316
336
|
const profileId = args.profileId || 'default';
|
|
@@ -354,6 +374,31 @@ async function handleCommand(
|
|
|
354
374
|
case 'getStatus': {
|
|
355
375
|
return { ok: true, body: { ok: true, sessions: manager.listSessions() } };
|
|
356
376
|
}
|
|
377
|
+
case 'record:start': {
|
|
378
|
+
const profileId = args.profileId || 'default';
|
|
379
|
+
const session = manager.getSession(profileId);
|
|
380
|
+
if (!session) throw new Error(`session for profile ${profileId} not started`);
|
|
381
|
+
const recording = await session.startRecording({
|
|
382
|
+
name: args.name || args.recordName,
|
|
383
|
+
outputPath: args.outputPath || args.output || args.recordOutput,
|
|
384
|
+
overlay: typeof args.overlay === 'boolean' ? args.overlay : undefined,
|
|
385
|
+
});
|
|
386
|
+
return { ok: true, body: { ok: true, profileId, recording } };
|
|
387
|
+
}
|
|
388
|
+
case 'record:stop': {
|
|
389
|
+
const profileId = args.profileId || 'default';
|
|
390
|
+
const session = manager.getSession(profileId);
|
|
391
|
+
if (!session) throw new Error(`session for profile ${profileId} not started`);
|
|
392
|
+
const recording = await session.stopRecording({ reason: String(args.reason || 'manual') });
|
|
393
|
+
return { ok: true, body: { ok: true, profileId, recording } };
|
|
394
|
+
}
|
|
395
|
+
case 'record:status': {
|
|
396
|
+
const profileId = args.profileId || 'default';
|
|
397
|
+
const session = manager.getSession(profileId);
|
|
398
|
+
if (!session) throw new Error(`session for profile ${profileId} not started`);
|
|
399
|
+
const recording = session.getRecordingStatus();
|
|
400
|
+
return { ok: true, body: { ok: true, profileId, recording } };
|
|
401
|
+
}
|
|
357
402
|
case 'system:display': {
|
|
358
403
|
const metrics = getDisplayMetrics();
|
|
359
404
|
return { ok: true, body: { ok: true, metrics: metrics || null } };
|