@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.
Files changed (174) hide show
  1. package/apps/desktop-console/default-settings.json +2 -2
  2. package/apps/desktop-console/dist/main/index.mjs +983 -128
  3. package/apps/desktop-console/dist/main/preload.mjs +7 -0
  4. package/apps/desktop-console/dist/renderer/index.html +622 -50
  5. package/apps/desktop-console/dist/renderer/index.js +2423 -469
  6. package/apps/desktop-console/dist/renderer/run.mts +6 -5
  7. package/apps/desktop-console/entry/ui-cli.mjs +672 -0
  8. package/apps/desktop-console/entry/ui-console.mjs +416 -29
  9. package/apps/webauto/entry/account.mjs +89 -53
  10. package/apps/webauto/entry/browser-status.mjs +7 -10
  11. package/apps/webauto/entry/lib/account-detect.mjs +254 -28
  12. package/apps/webauto/entry/lib/account-store.mjs +219 -30
  13. package/apps/webauto/entry/lib/bus-publish.mjs +63 -0
  14. package/apps/webauto/entry/lib/camo-cli.mjs +93 -0
  15. package/apps/webauto/entry/lib/profilepool.mjs +14 -5
  16. package/apps/webauto/entry/lib/quota-status.mjs +23 -0
  17. package/apps/webauto/entry/lib/schedule-store.mjs +1068 -0
  18. package/apps/webauto/entry/profilepool.mjs +106 -17
  19. package/apps/webauto/entry/schedule.mjs +612 -0
  20. package/apps/webauto/entry/weibo-unified.mjs +134 -0
  21. package/apps/webauto/entry/xhs-install.mjs +256 -31
  22. package/apps/webauto/entry/xhs-status.mjs +5 -2
  23. package/apps/webauto/entry/xhs-unified.mjs +631 -98
  24. package/apps/webauto/resources/container-library/weibo/weibo_detail_page/comment_item/container.json +40 -0
  25. package/apps/webauto/resources/container-library/weibo/weibo_detail_page/reply_expand_button/container.json +38 -0
  26. package/apps/webauto/resources/container-library/weibo/weibo_detail_page/reply_list/container.json +37 -0
  27. package/apps/webauto/resources/container-library/weibo/weibo_search_page/container.json +8 -3
  28. package/apps/webauto/resources/container-library/weibo/weibo_search_page/login_anchor/container.json +30 -0
  29. package/apps/webauto/resources/container-library/weibo/weibo_search_page/search_bar/container.json +47 -0
  30. package/apps/webauto/resources/container-library/weibo/weibo_search_page/search_button/container.json +39 -0
  31. package/bin/camoufox-cli.mjs +61 -0
  32. package/bin/webauto.mjs +301 -54
  33. package/dist/modules/camo-backend/src/index.js +49 -1
  34. package/dist/modules/camo-backend/src/internal/BrowserSession.js +572 -3
  35. package/dist/modules/camo-backend/src/internal/SessionManager.js +13 -1
  36. package/dist/modules/camo-backend/src/internal/storage-paths.js +6 -0
  37. package/dist/modules/collection-manager/bloom-filter.js +91 -0
  38. package/dist/modules/collection-manager/date-utils.js +275 -0
  39. package/dist/modules/collection-manager/index.js +258 -0
  40. package/dist/modules/collection-manager/storage.js +195 -0
  41. package/dist/modules/collection-manager/types.js +47 -0
  42. package/dist/modules/logging/src/index.js +1 -1
  43. package/dist/modules/process-registry/index.js +230 -0
  44. package/dist/modules/rate-limiter/index.js +242 -0
  45. package/dist/modules/workflow/blocks/ExecuteWeiboSearchBlock.js +128 -0
  46. package/dist/modules/workflow/blocks/PersistXhsNoteBlock.js +7 -3
  47. package/dist/modules/workflow/blocks/RenderMarkdown.js +4 -1
  48. package/dist/modules/workflow/blocks/WeiboCollectCommentsBlock.js +282 -0
  49. package/dist/modules/workflow/blocks/WeiboCollectFromLinksBlock.js +283 -0
  50. package/dist/modules/workflow/blocks/WeiboCollectSearchLinksBlock.js +208 -0
  51. package/dist/modules/workflow/blocks/WeiboCollectTimelineListBlock.js +128 -0
  52. package/dist/modules/workflow/blocks/WeiboCollectUserPostsListBlock.js +127 -0
  53. package/dist/modules/workflow/blocks/helpers/downloadPaths.js +21 -0
  54. package/dist/modules/workflow/config/workflowRegistry.js +2 -0
  55. package/dist/modules/workflow/definitions/weibo-search-workflow-v1.js +47 -0
  56. package/dist/modules/workflow/src/runner.js +6 -0
  57. package/dist/modules/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.js +4 -0
  58. package/dist/modules/xiaohongshu/app/src/blocks/Phase3InteractBlock.js +2 -2
  59. package/dist/modules/xiaohongshu/app/src/blocks/helpers/sharding.js +123 -0
  60. package/dist/modules/xiaohongshu/app/src/container-registry/src/index.d.ts +37 -0
  61. package/dist/modules/xiaohongshu/app/src/container-registry/src/index.js +184 -0
  62. package/dist/modules/xiaohongshu/app/src/workflow/blocks/AnchorVerificationBlock.d.ts +31 -0
  63. package/dist/modules/xiaohongshu/app/src/workflow/blocks/AnchorVerificationBlock.js +71 -0
  64. package/dist/modules/xiaohongshu/app/src/workflow/blocks/DetectPageStateBlock.d.ts +48 -0
  65. package/dist/modules/xiaohongshu/app/src/workflow/blocks/DetectPageStateBlock.js +259 -0
  66. package/dist/modules/xiaohongshu/app/src/workflow/blocks/ErrorRecoveryBlock.d.ts +28 -0
  67. package/dist/modules/xiaohongshu/app/src/workflow/blocks/ErrorRecoveryBlock.js +319 -0
  68. package/dist/modules/xiaohongshu/app/src/workflow/blocks/WaitSearchPermitBlock.d.ts +36 -0
  69. package/dist/modules/xiaohongshu/app/src/workflow/blocks/WaitSearchPermitBlock.js +162 -0
  70. package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/containerAnchors.d.ts +36 -0
  71. package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/containerAnchors.js +301 -0
  72. package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/operationLogger.d.ts +29 -0
  73. package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/operationLogger.js +195 -0
  74. package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/searchPageState.d.ts +25 -0
  75. package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/searchPageState.js +164 -0
  76. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/MatchCommentsBlock.d.ts +66 -0
  77. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/MatchCommentsBlock.js +139 -0
  78. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1EnsureServicesBlock.d.ts +16 -0
  79. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1EnsureServicesBlock.js +36 -0
  80. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1MonitorCookieBlock.d.ts +27 -0
  81. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1MonitorCookieBlock.js +213 -0
  82. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1StartProfileBlock.d.ts +18 -0
  83. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1StartProfileBlock.js +121 -0
  84. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2CollectLinksBlock.d.ts +34 -0
  85. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2CollectLinksBlock.js +1249 -0
  86. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2SearchBlock.d.ts +17 -0
  87. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2SearchBlock.js +703 -0
  88. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseDetailBlock.d.ts +15 -0
  89. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseDetailBlock.js +41 -0
  90. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseTabsBlock.d.ts +26 -0
  91. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseTabsBlock.js +44 -0
  92. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CollectCommentsBlock.d.ts +29 -0
  93. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CollectCommentsBlock.js +150 -0
  94. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ExtractDetailBlock.d.ts +38 -0
  95. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ExtractDetailBlock.js +117 -0
  96. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenDetailBlock.d.ts +30 -0
  97. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenDetailBlock.js +102 -0
  98. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenTabsBlock.d.ts +23 -0
  99. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenTabsBlock.js +109 -0
  100. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.d.ts +32 -0
  101. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.js +117 -0
  102. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ProcessSingleNoteBlock.d.ts +35 -0
  103. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ProcessSingleNoteBlock.js +114 -0
  104. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ValidateLinksBlock.d.ts +34 -0
  105. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ValidateLinksBlock.js +90 -0
  106. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase3InteractBlock.d.ts +111 -0
  107. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase3InteractBlock.js +1009 -0
  108. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase4MultiTabHarvestBlock.d.ts +20 -0
  109. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase4MultiTabHarvestBlock.js +233 -0
  110. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/ReplyInteractBlock.d.ts +48 -0
  111. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/ReplyInteractBlock.js +291 -0
  112. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/XhsDiscoverFallbackBlock.d.ts +23 -0
  113. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/XhsDiscoverFallbackBlock.js +240 -0
  114. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatchDsl.d.ts +55 -0
  115. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatchDsl.js +126 -0
  116. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatcher.d.ts +21 -0
  117. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatcher.js +99 -0
  118. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/evidence.d.ts +5 -0
  119. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/evidence.js +27 -0
  120. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/sharding.d.ts +37 -0
  121. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/sharding.js +165 -0
  122. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/xhsComments.d.ts +33 -0
  123. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/xhsComments.js +270 -0
  124. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/index.d.ts +9 -0
  125. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/index.js +9 -0
  126. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/checkpoints.d.ts +50 -0
  127. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/checkpoints.js +222 -0
  128. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/controllerAction.d.ts +10 -0
  129. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/controllerAction.js +43 -0
  130. package/dist/services/shared/serviceProcessLogger.js +1 -1
  131. package/dist/services/unified-api/server.js +105 -11
  132. package/modules/camo-backend/src/index.ts +46 -1
  133. package/modules/camo-backend/src/internal/BrowserSession.ts +619 -3
  134. package/modules/camo-backend/src/internal/SessionManager.ts +12 -1
  135. package/modules/camo-backend/src/internal/storage-paths.ts +5 -0
  136. package/modules/camo-runtime/src/autoscript/action-providers/xhs/comments.mjs +38 -2
  137. package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +47 -2
  138. package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +94 -11
  139. package/modules/camo-runtime/src/autoscript/action-providers/xhs.mjs +208 -2
  140. package/modules/camo-runtime/src/autoscript/runtime.mjs +7 -1
  141. package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +76 -43
  142. package/modules/camo-runtime/src/container/runtime-core/operations/index.mjs +75 -1
  143. package/modules/camo-runtime/src/container/runtime-core/operations/selector-scripts.mjs +71 -4
  144. package/modules/camo-runtime/src/container/runtime-core/operations/tab-pool.mjs +183 -27
  145. package/modules/collection-manager/bloom-filter.ts +112 -0
  146. package/modules/collection-manager/date-utils.ts +316 -0
  147. package/modules/collection-manager/index.ts +309 -0
  148. package/modules/collection-manager/package.json +10 -0
  149. package/modules/collection-manager/storage.ts +174 -0
  150. package/modules/collection-manager/types.ts +156 -0
  151. package/modules/logging/src/index.ts +1 -1
  152. package/modules/process-registry/index.ts +284 -0
  153. package/modules/rate-limiter/index.ts +322 -0
  154. package/modules/state/src/paths.ts +9 -1
  155. package/modules/task-scheduler/index.ts +293 -0
  156. package/modules/workflow/blocks/ExecuteWeiboSearchBlock.ts +167 -0
  157. package/modules/workflow/blocks/PersistXhsNoteBlock.ts +7 -3
  158. package/modules/workflow/blocks/RenderMarkdown.ts +4 -1
  159. package/modules/workflow/blocks/WeiboCollectCommentsBlock.ts +339 -0
  160. package/modules/workflow/blocks/WeiboCollectFromLinksBlock.ts +338 -0
  161. package/modules/workflow/blocks/helpers/downloadPaths.ts +16 -0
  162. package/modules/workflow/config/workflowRegistry.ts +2 -0
  163. package/modules/workflow/definitions/weibo-search-workflow-v1.ts +47 -0
  164. package/modules/workflow/src/runner.ts +6 -0
  165. package/modules/xiaohongshu/app/src/blocks/Phase1StartProfileBlock.ts +1 -1
  166. package/modules/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.ts +4 -0
  167. package/modules/xiaohongshu/app/src/blocks/Phase3InteractBlock.ts +2 -3
  168. package/modules/xiaohongshu/app/src/blocks/helpers/sharding.ts +152 -0
  169. package/package.json +13 -4
  170. package/scripts/postinstall-resources.mjs +62 -0
  171. package/scripts/test/run-coverage.mjs +76 -0
  172. package/scripts/weibo/search.ts +49 -0
  173. package/services/shared/serviceProcessLogger.ts +1 -1
  174. 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
@@ -14,7 +14,7 @@ function ensureLogDir() {
14
14
  }
15
15
  function safeAppend(filePath, line) {
16
16
  try {
17
- fs.appendFileSync(filePath, line);
17
+ fs.appendFileSync(filePath, line, 'utf8');
18
18
  }
19
19
  catch {
20
20
  // ignore
@@ -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
- this.taskRegistry.updateTask(runId, payload);
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 { ok: true, body: { ok: true, sessionId: res.sessionId, profileId: opts.profileId } };
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 } };