@web-auto/webauto 0.1.3 → 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.
Files changed (174) hide show
  1. package/apps/desktop-console/default-settings.json +2 -2
  2. package/apps/desktop-console/dist/main/index.mjs +915 -85
  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 +2415 -470
  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 +236 -29
  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 +14 -5
  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
@@ -134,7 +134,17 @@ export class SessionManager {
134
134
  (process as any).emit(SESSION_CLOSED_EVENT, id);
135
135
  }
136
136
  };
137
- await session.start(options.initialUrl);
137
+ try {
138
+ await session.start(options.initialUrl);
139
+ } catch (err) {
140
+ this.debugLog('createSession:start_failed', {
141
+ profileId,
142
+ error: (err as Error)?.message || String(err),
143
+ });
144
+ session.onExit = undefined;
145
+ await session.close().catch(() => {});
146
+ throw err;
147
+ }
138
148
  this.sessions.set(profileId, session);
139
149
 
140
150
  this.debugLog('createSession:started', { profileId });
@@ -156,6 +166,7 @@ export class SessionManager {
156
166
  current_url: session.getCurrentUrl(),
157
167
  mode: session.modeName,
158
168
  owner_pid: this.owners.get(session.id)?.pid || null,
169
+ recording: session.getRecordingStatus(),
159
170
  }));
160
171
  }
161
172
 
@@ -36,3 +36,8 @@ export function resolveLocksRoot() {
36
36
  return path.join(resolveDataRoot(), 'locks');
37
37
  }
38
38
 
39
+ export function resolveRecordsRoot() {
40
+ const envRoot = String(process.env.WEBAUTO_PATHS_RECORDS || '').trim();
41
+ if (envRoot) return envRoot;
42
+ return path.join(resolveDataRoot(), 'records');
43
+ }
@@ -86,6 +86,43 @@ export function buildCommentsHarvestScript(params = {}) {
86
86
  }
87
87
  return null;
88
88
  };
89
+ const normalizeInlineText = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
90
+ const sanitizeAuthorText = (raw, commentText = '') => {
91
+ const text = normalizeInlineText(raw);
92
+ if (!text) return '';
93
+ if (commentText && text === commentText) return '';
94
+ if (text.length > 40) return '';
95
+ if (/^(回复|展开|收起|查看更多|评论|赞|分享|发送)$/.test(text)) return '';
96
+ return text;
97
+ };
98
+ const readAuthor = (item, commentText = '') => {
99
+ const attrNames = ['data-user-name', 'data-username', 'data-user_nickname', 'data-nickname'];
100
+ for (const attr of attrNames) {
101
+ const value = sanitizeAuthorText(item.getAttribute?.(attr), commentText);
102
+ if (value) return value;
103
+ }
104
+ const selectors = [
105
+ '.comment-user .name',
106
+ '.comment-user .username',
107
+ '.comment-user .user-name',
108
+ '.author .name',
109
+ '.author',
110
+ '.user-name',
111
+ '.username',
112
+ '.name',
113
+ 'a[href*="/user/profile/"]',
114
+ 'a[href*="/user/"]',
115
+ ];
116
+ for (const selector of selectors) {
117
+ const node = item.querySelector(selector);
118
+ if (!node) continue;
119
+ const title = sanitizeAuthorText(node.getAttribute?.('title'), commentText);
120
+ if (title) return title;
121
+ const text = sanitizeAuthorText(node.textContent, commentText);
122
+ if (text) return text;
123
+ }
124
+ return '';
125
+ };
89
126
 
90
127
  const scroller = document.querySelector('.note-scroller')
91
128
  || document.querySelector('.comments-el')
@@ -105,9 +142,8 @@ export function buildCommentsHarvestScript(params = {}) {
105
142
  const nodes = Array.from(document.querySelectorAll('.comment-item, [class*="comment-item"]'));
106
143
  for (const item of nodes) {
107
144
  const textNode = item.querySelector('.content, .comment-content, p');
108
- const authorNode = item.querySelector('.name, .author, .user-name, [class*="author"], [class*="name"]');
109
145
  const text = String((textNode && textNode.textContent) || '').trim();
110
- const author = String((authorNode && authorNode.textContent) || '').trim();
146
+ const author = readAuthor(item, text);
111
147
  if (!text) continue;
112
148
  const key = author + '::' + text;
113
149
  if (commentMap.has(key)) continue;
@@ -74,6 +74,42 @@ function buildCollectLikeTargetsScript() {
74
74
  }
75
75
  return '';
76
76
  };
77
+ const sanitizeUserName = (raw, commentText = '') => {
78
+ const text = String(raw || '').replace(/\\s+/g, ' ').trim();
79
+ if (!text) return '';
80
+ if (commentText && text === commentText) return '';
81
+ if (text.length > 40) return '';
82
+ if (/^(回复|展开|收起|查看更多|评论|赞|分享|发送)$/.test(text)) return '';
83
+ return text;
84
+ };
85
+ const readUserName = (item, commentText = '') => {
86
+ const attrNames = ['data-user-name', 'data-username', 'data-user_nickname', 'data-nickname'];
87
+ for (const attr of attrNames) {
88
+ const value = sanitizeUserName(item.getAttribute?.(attr), commentText);
89
+ if (value) return value;
90
+ }
91
+ const selectors = [
92
+ '.comment-user .name',
93
+ '.comment-user .username',
94
+ '.comment-user .user-name',
95
+ '.author .name',
96
+ '.author',
97
+ '.user-name',
98
+ '.username',
99
+ '.name',
100
+ 'a[href*="/user/profile/"]',
101
+ 'a[href*="/user/"]',
102
+ ];
103
+ for (const selector of selectors) {
104
+ const node = item.querySelector(selector);
105
+ if (!node) continue;
106
+ const title = sanitizeUserName(node.getAttribute?.('title'), commentText);
107
+ if (title) return title;
108
+ const text = sanitizeUserName(node.textContent, commentText);
109
+ if (text) return text;
110
+ }
111
+ return '';
112
+ };
77
113
  const readAttr = (item, attrNames) => {
78
114
  for (const attr of attrNames) {
79
115
  const value = String(item.getAttribute?.(attr) || '').trim();
@@ -81,6 +117,15 @@ function buildCollectLikeTargetsScript() {
81
117
  }
82
118
  return '';
83
119
  };
120
+ const readUserId = (item) => {
121
+ const value = readAttr(item, ['data-user-id', 'data-userid', 'data-user_id']);
122
+ if (value) return value;
123
+ const anchor = item.querySelector('a[href*="/user/profile/"], a[href*="/user/"]');
124
+ const href = String(anchor?.getAttribute?.('href') || '').trim();
125
+ if (!href) return '';
126
+ const matched = href.match(/\\/user\\/(?:profile\\/)?([a-zA-Z0-9_-]+)/);
127
+ return matched && matched[1] ? matched[1] : '';
128
+ };
84
129
 
85
130
  const matchedSet = new Set(
86
131
  Array.isArray(state.matchedComments)
@@ -92,8 +137,8 @@ function buildCollectLikeTargetsScript() {
92
137
  const item = items[index];
93
138
  const text = readText(item, ['.content', '.comment-content', 'p']);
94
139
  if (!text) continue;
95
- const userName = readText(item, ['.name', '.author', '.user-name', '.username', '[class*="author"]', '[class*="name"]']);
96
- const userId = readAttr(item, ['data-user-id', 'data-userid', 'data-user_id']);
140
+ const userName = readUserName(item, text);
141
+ const userId = readUserId(item);
97
142
  const timestamp = readText(item, ['.date', '.time', '.timestamp', '[class*="time"]']);
98
143
  const likeControl = findLikeControl(item);
99
144
  rows.push({
@@ -54,6 +54,19 @@ export function buildOpenDetailScript(params = {}) {
54
54
  const mode = String(params.mode || 'first').trim().toLowerCase();
55
55
  const maxNotes = Math.max(1, Number(params.maxNotes ?? params.limit ?? 20) || 20);
56
56
  const keyword = String(params.keyword || '').trim();
57
+ const resume = params.resume !== false;
58
+ const incrementalMax = params.incrementalMax !== false;
59
+ const excludeNoteIds = Array.isArray(params.excludeNoteIds)
60
+ ? params.excludeNoteIds.map((item) => String(item || '').trim()).filter(Boolean)
61
+ : [];
62
+ const seedCollectCount = Math.max(0, Number(params.seedCollectCount || 0) || 0);
63
+ const seedCollectMaxRounds = Math.max(0, Number(params.seedCollectMaxRounds || 0) || 0);
64
+ const seedCollectStep = Math.max(120, Number(params.seedCollectStep || 360) || 360);
65
+ const seedCollectSettleMs = Math.max(100, Number(params.seedCollectSettleMs || 260) || 260);
66
+ const seedResetToTop = params.seedResetToTop !== false;
67
+ const nextSeekRounds = Math.max(0, Number(params.nextSeekRounds || 8) || 8);
68
+ const nextSeekStep = Math.max(0, Number(params.nextSeekStep || 0) || 0);
69
+ const nextSeekSettleMs = Math.max(120, Number(params.nextSeekSettleMs || 320) || 320);
57
70
 
58
71
  return `(async () => {
59
72
  const STATE_KEY = '__camoXhsState';
@@ -91,36 +104,103 @@ export function buildOpenDetailScript(params = {}) {
91
104
  const requestedKeyword = ${JSON.stringify(keyword)};
92
105
  const mode = ${JSON.stringify(mode)};
93
106
  const previousKeyword = String(state.keyword || '').trim();
107
+ const keywordChanged = Boolean(requestedKeyword && previousKeyword && requestedKeyword !== previousKeyword);
94
108
  if (mode === 'first') {
109
+ if (!${resume ? 'true' : 'false'} || keywordChanged) {
110
+ state.visitedNoteIds = [];
111
+ }
112
+ } else if (keywordChanged) {
95
113
  state.visitedNoteIds = [];
96
- } else if (requestedKeyword && previousKeyword && requestedKeyword !== previousKeyword) {
97
- state.visitedNoteIds = [];
98
114
  }
99
- state.maxNotes = Number(${maxNotes});
115
+ const requestedMaxNotes = Number(${maxNotes});
116
+ if (mode === 'first') {
117
+ if (${incrementalMax ? 'true' : 'false'} && ${resume ? 'true' : 'false'} && !keywordChanged) {
118
+ state.maxNotes = Number(state.visitedNoteIds.length || 0) + requestedMaxNotes;
119
+ } else {
120
+ state.maxNotes = requestedMaxNotes;
121
+ }
122
+ } else if (!Number.isFinite(Number(state.maxNotes)) || Number(state.maxNotes) <= 0) {
123
+ state.maxNotes = requestedMaxNotes;
124
+ }
100
125
  if (requestedKeyword) state.keyword = requestedKeyword;
101
126
 
102
127
  if (mode === 'next' && state.visitedNoteIds.length >= state.maxNotes) {
103
128
  throw new Error('AUTOSCRIPT_DONE_MAX_NOTES');
104
129
  }
105
130
 
106
- const nodes = Array.from(document.querySelectorAll('.note-item'))
131
+ const excludedNoteIds = new Set(${JSON.stringify(excludeNoteIds)});
132
+ const mapNodes = () => Array.from(document.querySelectorAll('.note-item'))
107
133
  .map((item, index) => {
108
134
  const cover = item.querySelector('a.cover');
109
135
  if (!cover) return null;
110
136
  const href = String(cover.getAttribute('href') || '').trim();
111
- const noteId = href.split('/').filter(Boolean).pop() || ('idx_' + index);
137
+ const lastSegment = href.split('/').filter(Boolean).pop() || '';
138
+ const normalized = lastSegment.split('?')[0].split('#')[0];
139
+ const noteId = normalized || ('idx_' + index);
112
140
  return { cover, href, noteId };
113
141
  })
114
142
  .filter(Boolean);
143
+ let nodes = mapNodes();
144
+ const seedCollectedSet = new Set();
145
+ const seedCollectEnabled = mode === 'first'
146
+ && Number(${seedCollectCount}) > 0
147
+ && Number(${seedCollectMaxRounds}) > 0;
148
+ if (seedCollectEnabled) {
149
+ const collectVisible = () => {
150
+ for (const row of mapNodes()) {
151
+ if (!row || !row.noteId) continue;
152
+ seedCollectedSet.add(row.noteId);
153
+ }
154
+ };
155
+ collectVisible();
156
+ const maxRounds = Number(${seedCollectMaxRounds});
157
+ const targetCount = Number(${seedCollectCount});
158
+ for (let round = 0; round < maxRounds && seedCollectedSet.size < targetCount; round += 1) {
159
+ window.scrollBy({ top: Number(${seedCollectStep}), left: 0, behavior: 'auto' });
160
+ await new Promise((resolve) => setTimeout(resolve, Number(${seedCollectSettleMs})));
161
+ collectVisible();
162
+ }
163
+ if (${seedResetToTop ? 'true' : 'false'}) {
164
+ window.scrollTo({ top: 0, behavior: 'auto' });
165
+ await new Promise((resolve) => setTimeout(resolve, Number(${seedCollectSettleMs})));
166
+ }
167
+ nodes = mapNodes();
168
+ }
115
169
  if (nodes.length === 0) throw new Error('NO_SEARCH_RESULT_ITEM');
116
170
 
117
- let next = null;
118
- if (mode === 'next') {
119
- next = nodes.find((row) => !state.visitedNoteIds.includes(row.noteId));
120
- if (!next) throw new Error('AUTOSCRIPT_DONE_NO_MORE_NOTES');
121
- } else {
122
- next = nodes.find((row) => !state.visitedNoteIds.includes(row.noteId)) || nodes[0];
171
+ const isEligible = (row) => (
172
+ row
173
+ && row.noteId
174
+ && !excludedNoteIds.has(row.noteId)
175
+ && !state.visitedNoteIds.includes(row.noteId)
176
+ );
177
+ const resolveSeekStep = () => {
178
+ const configured = Number(${nextSeekStep});
179
+ if (Number.isFinite(configured) && configured > 0) return configured;
180
+ const viewportHeight = Math.max(
181
+ Number(window.innerHeight || 0) || 0,
182
+ Number(document.documentElement?.clientHeight || 0) || 0,
183
+ );
184
+ return Math.max(240, Math.floor(viewportHeight * 0.9));
185
+ };
186
+ const seekStep = resolveSeekStep();
187
+
188
+ let next = nodes.find((row) => isEligible(row));
189
+ if (!next) {
190
+ let stagnantRounds = 0;
191
+ for (let round = 0; !next && round < Number(${nextSeekRounds}); round += 1) {
192
+ const beforeTop = window.scrollY || document.documentElement.scrollTop || document.body.scrollTop || 0;
193
+ window.scrollBy({ top: seekStep, left: 0, behavior: 'auto' });
194
+ await new Promise((resolve) => setTimeout(resolve, Number(${nextSeekSettleMs})));
195
+ nodes = mapNodes();
196
+ next = nodes.find((row) => isEligible(row));
197
+ const afterTop = window.scrollY || document.documentElement.scrollTop || document.body.scrollTop || 0;
198
+ if (Math.abs(afterTop - beforeTop) < 2) stagnantRounds += 1;
199
+ else stagnantRounds = 0;
200
+ if (stagnantRounds >= 2) break;
201
+ }
123
202
  }
203
+ if (!next) throw new Error('AUTOSCRIPT_DONE_NO_MORE_NOTES');
124
204
 
125
205
  const detailSelectors = [
126
206
  '.note-detail-mask',
@@ -176,6 +256,9 @@ export function buildOpenDetailScript(params = {}) {
176
256
  openByClick: true,
177
257
  beforeUrl,
178
258
  afterUrl,
259
+ excludedCount: excludedNoteIds.size,
260
+ seedCollectedCount: seedCollectedSet.size,
261
+ seedCollectedNoteIds: Array.from(seedCollectedSet),
179
262
  };
180
263
  })()`;
181
264
  }
@@ -1,3 +1,5 @@
1
+ import path from 'node:path';
2
+ import fsp from 'node:fs/promises';
1
3
  import { asErrorPayload } from '../../container/runtime-core/utils.mjs';
2
4
  import {
3
5
  createEvaluateHandler,
@@ -21,6 +23,210 @@ import {
21
23
  } from './xhs/persistence.mjs';
22
24
  import { buildOpenDetailScript, buildSubmitSearchScript } from './xhs/search.mjs';
23
25
 
26
+ const XHS_OPERATION_LOCKS = new Map();
27
+
28
+ function toLockKey(text, fallback = '') {
29
+ const value = String(text || '').trim();
30
+ return value || fallback;
31
+ }
32
+
33
+ async function withSerializedLock(lockKey, fn) {
34
+ const key = toLockKey(lockKey);
35
+ if (!key) return fn();
36
+ const previous = XHS_OPERATION_LOCKS.get(key) || Promise.resolve();
37
+ let release;
38
+ const gate = new Promise((resolve) => {
39
+ release = resolve;
40
+ });
41
+ XHS_OPERATION_LOCKS.set(key, previous.catch(() => null).then(() => gate));
42
+ await previous.catch(() => null);
43
+ try {
44
+ return await fn();
45
+ } finally {
46
+ release();
47
+ if (XHS_OPERATION_LOCKS.get(key) === gate) XHS_OPERATION_LOCKS.delete(key);
48
+ }
49
+ }
50
+
51
+ function normalizeNoteIdList(items) {
52
+ if (!Array.isArray(items)) return [];
53
+ const out = [];
54
+ const seen = new Set();
55
+ for (const item of items) {
56
+ const noteId = String(item || '').trim();
57
+ if (!noteId || seen.has(noteId)) continue;
58
+ seen.add(noteId);
59
+ out.push(noteId);
60
+ }
61
+ return out;
62
+ }
63
+
64
+ function resolveSharedClaimPath(params = {}) {
65
+ const raw = String(params.sharedHarvestPath || params.sharedClaimPath || '').trim();
66
+ return raw ? path.resolve(raw) : '';
67
+ }
68
+
69
+ function resolveSearchLockKey(params = {}) {
70
+ const raw = String(params.searchSerialKey || params.searchLockKey || '').trim();
71
+ if (raw) return raw;
72
+ const claimPath = resolveSharedClaimPath(params);
73
+ return claimPath ? `claim:${claimPath}` : '';
74
+ }
75
+
76
+ async function loadSharedClaimDoc(filePath) {
77
+ if (!filePath) {
78
+ return { noteIds: [], byNoteId: {}, updatedAt: null };
79
+ }
80
+ try {
81
+ const raw = await fsp.readFile(filePath, 'utf8');
82
+ const parsed = JSON.parse(raw);
83
+ const noteIds = normalizeNoteIdList(parsed?.noteIds);
84
+ const byNoteId = parsed?.byNoteId && typeof parsed.byNoteId === 'object' ? parsed.byNoteId : {};
85
+ return {
86
+ noteIds,
87
+ byNoteId,
88
+ updatedAt: parsed?.updatedAt || null,
89
+ };
90
+ } catch {
91
+ return { noteIds: [], byNoteId: {}, updatedAt: null };
92
+ }
93
+ }
94
+
95
+ async function saveSharedClaimDoc(filePath, doc) {
96
+ if (!filePath) return;
97
+ const noteIds = normalizeNoteIdList(doc?.noteIds);
98
+ const payload = {
99
+ updatedAt: new Date().toISOString(),
100
+ noteIds,
101
+ byNoteId: doc?.byNoteId && typeof doc.byNoteId === 'object' ? doc.byNoteId : {},
102
+ };
103
+ await fsp.mkdir(path.dirname(filePath), { recursive: true });
104
+ await fsp.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
105
+ }
106
+
107
+ async function executeSubmitSearchOperation({ profileId, params = {} }) {
108
+ const script = buildSubmitSearchScript(params);
109
+ const highlight = params.highlight !== false;
110
+ const lockKey = resolveSearchLockKey(params);
111
+ return withSerializedLock(lockKey ? `xhs_submit_search:${lockKey}` : '', () => evaluateWithScript({
112
+ profileId,
113
+ script,
114
+ message: 'xhs_submit_search done',
115
+ highlight,
116
+ }));
117
+ }
118
+
119
+ async function executeOpenDetailOperation({ profileId, params = {} }) {
120
+ const highlight = params.highlight !== false;
121
+ const claimPath = resolveSharedClaimPath(params);
122
+ const lockKey = claimPath ? `xhs_open_detail:${claimPath}` : '';
123
+
124
+ const mapOpenDetailError = (err, paramsRef = {}) => {
125
+ const message = String(err?.message || err || '');
126
+ const mode = String(paramsRef?.mode || '').trim().toLowerCase();
127
+ if (message.includes('AUTOSCRIPT_DONE_NO_MORE_NOTES')) {
128
+ return {
129
+ ok: true,
130
+ code: 'AUTOSCRIPT_DONE_NO_MORE_NOTES',
131
+ message: 'no more notes',
132
+ data: { stopReason: 'no_more_notes' },
133
+ };
134
+ }
135
+ if (message.includes('NO_SEARCH_RESULT_ITEM')) {
136
+ if (mode === 'first') return null;
137
+ return {
138
+ ok: true,
139
+ code: 'OPERATION_SKIPPED_NO_SEARCH_RESULT_ITEM',
140
+ message: 'search result item missing',
141
+ data: { skipped: true },
142
+ };
143
+ }
144
+ return null;
145
+ };
146
+
147
+ const runWithExclude = async (excludeNoteIds) => {
148
+ const script = buildOpenDetailScript({
149
+ ...params,
150
+ excludeNoteIds,
151
+ });
152
+ const operationResult = await evaluateWithScript({
153
+ profileId,
154
+ script,
155
+ message: 'xhs_open_detail done',
156
+ highlight,
157
+ });
158
+ const payload = extractEvaluateResultData(operationResult.data) || {};
159
+ return {
160
+ operationResult,
161
+ payload: payload && typeof payload === 'object' ? payload : {},
162
+ };
163
+ };
164
+
165
+ if (!claimPath) {
166
+ try {
167
+ const { operationResult } = await runWithExclude([]);
168
+ return operationResult;
169
+ } catch (err) {
170
+ const mapped = mapOpenDetailError(err, params);
171
+ if (mapped) return mapped;
172
+ throw err;
173
+ }
174
+ }
175
+
176
+ const runLocked = async () => {
177
+ const claimDoc = await loadSharedClaimDoc(claimPath);
178
+ const excludeNoteIds = normalizeNoteIdList(claimDoc.noteIds);
179
+ const { operationResult, payload } = await runWithExclude(excludeNoteIds);
180
+
181
+ const claimSet = new Set(excludeNoteIds);
182
+ const claimAdded = [];
183
+ const markClaim = (noteId, source = 'open_detail') => {
184
+ const id = String(noteId || '').trim();
185
+ if (!id || claimSet.has(id)) return;
186
+ claimSet.add(id);
187
+ claimAdded.push(id);
188
+ claimDoc.byNoteId[id] = {
189
+ noteId: id,
190
+ profileId,
191
+ source,
192
+ ts: new Date().toISOString(),
193
+ };
194
+ };
195
+
196
+ const seeded = normalizeNoteIdList(payload.seedCollectedNoteIds);
197
+ for (const noteId of seeded) markClaim(noteId, 'seed_collect');
198
+ if (payload.opened === true) markClaim(payload.noteId, 'open_detail');
199
+ claimDoc.noteIds = Array.from(claimSet);
200
+ if (claimAdded.length > 0) {
201
+ await saveSharedClaimDoc(claimPath, claimDoc);
202
+ }
203
+
204
+ const mergedPayload = {
205
+ ...payload,
206
+ sharedClaimPath: claimPath,
207
+ sharedClaimCount: claimDoc.noteIds.length,
208
+ sharedClaimAdded: claimAdded,
209
+ dedupExcluded: excludeNoteIds.length,
210
+ };
211
+ const mergedData = operationResult.data && typeof operationResult.data === 'object'
212
+ ? { ...operationResult.data, result: mergedPayload }
213
+ : { result: mergedPayload };
214
+
215
+ return {
216
+ ...operationResult,
217
+ data: mergedData,
218
+ };
219
+ };
220
+
221
+ try {
222
+ return await withSerializedLock(lockKey, runLocked);
223
+ } catch (err) {
224
+ const mapped = mapOpenDetailError(err, params);
225
+ if (mapped) return mapped;
226
+ throw err;
227
+ }
228
+ }
229
+
24
230
  function buildReadStateScript() {
25
231
  return `(() => {
26
232
  const state = window.__camoXhsState || {};
@@ -108,8 +314,8 @@ async function executeCommentsHarvestOperation({ profileId, params = {} }) {
108
314
 
109
315
  const XHS_ACTION_HANDLERS = {
110
316
  raise_error: handleRaiseError,
111
- xhs_submit_search: createEvaluateHandler('xhs_submit_search done', buildSubmitSearchScript),
112
- xhs_open_detail: createEvaluateHandler('xhs_open_detail done', buildOpenDetailScript),
317
+ xhs_submit_search: executeSubmitSearchOperation,
318
+ xhs_open_detail: executeOpenDetailOperation,
113
319
  xhs_detail_harvest: createEvaluateHandler('xhs_detail_harvest done', buildDetailHarvestScript),
114
320
  xhs_expand_replies: createEvaluateHandler('xhs_expand_replies done', buildExpandRepliesScript),
115
321
  xhs_comments_harvest: executeCommentsHarvestOperation,
@@ -359,6 +359,11 @@ export class AutoscriptRunner {
359
359
 
360
360
  resolveTimeoutMs(operation) {
361
361
  const pacing = this.resolvePacing(operation);
362
+ const disableTimeout = Boolean(
363
+ operation?.disableTimeout ?? this.script?.defaults?.disableTimeout,
364
+ );
365
+ if (disableTimeout) return 0;
366
+ if (pacing.timeoutMs === 0) return 0;
362
367
  if (Number.isFinite(pacing.timeoutMs) && pacing.timeoutMs > 0) return pacing.timeoutMs;
363
368
  return this.getDefaultTimeoutMs(operation);
364
369
  }
@@ -778,8 +783,9 @@ export class AutoscriptRunner {
778
783
  };
779
784
  }
780
785
 
786
+ const failureStatus = operation?.onFailure === 'continue' ? 'skipped' : 'failed';
781
787
  this.operationState.set(operation.id, {
782
- status: 'failed',
788
+ status: failureStatus,
783
789
  runs: Number(this.operationState.get(operation.id)?.runs || 0) + 1,
784
790
  lastError: result.message || 'operation failed',
785
791
  updatedAt: nowIso(),