@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
@@ -205,6 +205,71 @@ async function tryOpenTabWithShortcut(profileId, timeoutMs) {
205
205
  return { ok: false, error: lastError };
206
206
  }
207
207
 
208
+ async function waitForTabCountIncrease({
209
+ profileId,
210
+ beforeCount,
211
+ apiTimeoutMs,
212
+ maxWaitMs,
213
+ pollMs = 250,
214
+ }) {
215
+ const startedAt = Date.now();
216
+ const effectivePollMs = Math.max(80, Number(pollMs) || 250);
217
+ const waitMs = Math.max(400, Number(maxWaitMs) || 4000);
218
+ const listTimeoutMs = Math.max(500, Math.min(resolveTimeoutMs(apiTimeoutMs, DEFAULT_API_TIMEOUT_MS), 2500));
219
+ let lastError = null;
220
+
221
+ while (Date.now() - startedAt <= waitMs) {
222
+ try {
223
+ const listed = await callApiWithTimeout('page:list', { profileId }, listTimeoutMs);
224
+ const { pages } = extractPageList(listed);
225
+ if (pages.length > beforeCount) {
226
+ return {
227
+ ok: true,
228
+ elapsedMs: Date.now() - startedAt,
229
+ afterCount: pages.length,
230
+ };
231
+ }
232
+ } catch (err) {
233
+ lastError = err;
234
+ }
235
+ await sleep(effectivePollMs);
236
+ }
237
+
238
+ return {
239
+ ok: false,
240
+ elapsedMs: Date.now() - startedAt,
241
+ error: lastError,
242
+ };
243
+ }
244
+
245
+ async function tryOpenTabWithAnchor(profileId, seedUrl, timeoutMs) {
246
+ try {
247
+ const popupResult = await callApiWithTimeout('evaluate', {
248
+ profileId,
249
+ script: `(() => {
250
+ const href = ${JSON.stringify(seedUrl || 'about:blank')};
251
+ const anchor = document.createElement('a');
252
+ anchor.href = href;
253
+ anchor.target = '_blank';
254
+ anchor.rel = 'noopener noreferrer';
255
+ anchor.style.position = 'fixed';
256
+ anchor.style.left = '-9999px';
257
+ anchor.style.top = '-9999px';
258
+ document.body.appendChild(anchor);
259
+ const evt = new MouseEvent('click', { bubbles: true, cancelable: true, view: window });
260
+ const dispatched = anchor.dispatchEvent(evt);
261
+ anchor.click();
262
+ anchor.remove();
263
+ return { opened: true, dispatched };
264
+ })()`,
265
+ }, timeoutMs);
266
+ const popupData = popupResult?.result || popupResult || {};
267
+ return { ok: Boolean(popupData?.opened || popupData?.ok), error: null };
268
+ } catch (err) {
269
+ return { ok: false, error: err };
270
+ }
271
+ }
272
+
208
273
  async function openTabBestEffort({
209
274
  profileId,
210
275
  seedUrl,
@@ -213,13 +278,15 @@ async function openTabBestEffort({
213
278
  apiTimeoutMs,
214
279
  navigationTimeoutMs,
215
280
  shortcutTimeoutMs,
281
+ tabAppearTimeoutMs,
216
282
  syncConfig,
217
283
  }) {
218
- const hasNewTab = async () => {
219
- const listed = await callApiWithTimeout('page:list', { profileId }, apiTimeoutMs);
220
- const { pages } = extractPageList(listed);
221
- return pages.length > beforeCount;
222
- };
284
+ const waitForTab = async () => waitForTabCountIncrease({
285
+ profileId,
286
+ beforeCount,
287
+ apiTimeoutMs,
288
+ maxWaitMs: tabAppearTimeoutMs,
289
+ });
223
290
  const settle = async () => {
224
291
  if (openDelayMs > 0) {
225
292
  await new Promise((resolve) => setTimeout(resolve, openDelayMs));
@@ -230,7 +297,8 @@ async function openTabBestEffort({
230
297
  const shortcutResult = await tryOpenTabWithShortcut(profileId, shortcutTimeoutMs);
231
298
  if (shortcutResult.ok) {
232
299
  await settle();
233
- if (await hasNewTab()) {
300
+ const shortcutOpened = await waitForTab();
301
+ if (shortcutOpened.ok) {
234
302
  await seedNewestTabIfNeeded({
235
303
  profileId,
236
304
  seedUrl,
@@ -249,9 +317,10 @@ async function openTabBestEffort({
249
317
  ? { profileId, url: seedUrl }
250
318
  : { profileId };
251
319
  try {
252
- await callApiWithTimeout('newPage', payload, apiTimeoutMs);
320
+ await callApiWithTimeout('newPage', payload, Math.max(1200, Math.min(apiTimeoutMs, 6000)));
253
321
  await settle();
254
- if (await hasNewTab()) {
322
+ const newPageOpened = await waitForTab();
323
+ if (newPageOpened.ok) {
255
324
  await seedNewestTabIfNeeded({
256
325
  profileId,
257
326
  seedUrl,
@@ -277,7 +346,8 @@ async function openTabBestEffort({
277
346
  const popupData = popupResult?.result || popupResult || {};
278
347
  if (Boolean(popupData?.opened || popupData?.ok)) {
279
348
  await settle();
280
- if (await hasNewTab()) {
349
+ const popupOpened = await waitForTab();
350
+ if (popupOpened.ok) {
281
351
  await seedNewestTabIfNeeded({
282
352
  profileId,
283
353
  seedUrl,
@@ -293,6 +363,29 @@ async function openTabBestEffort({
293
363
  openError = err;
294
364
  }
295
365
 
366
+ const anchorResult = await tryOpenTabWithAnchor(
367
+ profileId,
368
+ seedUrl,
369
+ Math.max(1200, Math.min(apiTimeoutMs, 5000)),
370
+ );
371
+ if (anchorResult.ok) {
372
+ await settle();
373
+ const anchorOpened = await waitForTab();
374
+ if (anchorOpened.ok) {
375
+ await seedNewestTabIfNeeded({
376
+ profileId,
377
+ seedUrl,
378
+ openDelayMs,
379
+ apiTimeoutMs,
380
+ navigationTimeoutMs,
381
+ syncConfig,
382
+ });
383
+ return { ok: true, mode: 'anchor_click', error: null };
384
+ }
385
+ } else {
386
+ openError = anchorResult.error || openError;
387
+ }
388
+
296
389
  return { ok: false, mode: null, error: openError };
297
390
  }
298
391
 
@@ -306,6 +399,10 @@ export async function executeTabPoolOperation({ profileId, action, params = {},
306
399
  const apiTimeoutMs = resolveTimeoutMs(params.apiTimeoutMs, DEFAULT_API_TIMEOUT_MS);
307
400
  const navigationTimeoutMs = resolveTimeoutMs(params.navigationTimeoutMs ?? params.gotoTimeoutMs, DEFAULT_NAV_TIMEOUT_MS);
308
401
  const shortcutTimeoutMs = resolveTimeoutMs(params.shortcutTimeoutMs, SHORTCUT_OPEN_TIMEOUT_MS);
402
+ const tabAppearTimeoutMs = resolveTimeoutMs(
403
+ params.tabAppearTimeoutMs,
404
+ Math.max(3000, openDelayMs + 2200),
405
+ );
309
406
  const syncConfig = resolveViewportSyncConfig({ params });
310
407
  const configuredSeedUrl = normalizeSeedUrl(String(params.url || '').trim());
311
408
 
@@ -334,6 +431,7 @@ export async function executeTabPoolOperation({ profileId, action, params = {},
334
431
  apiTimeoutMs,
335
432
  navigationTimeoutMs,
336
433
  shortcutTimeoutMs,
434
+ tabAppearTimeoutMs,
337
435
  syncConfig,
338
436
  });
339
437
  listed = await callApiWithTimeout('page:list', { profileId }, apiTimeoutMs);
@@ -364,17 +462,48 @@ export async function executeTabPoolOperation({ profileId, action, params = {},
364
462
  for (const page of selected) {
365
463
  const pageIndex = Number(page.index);
366
464
  if (!Number.isFinite(pageIndex)) continue;
367
- await callApiWithTimeout('page:switch', { profileId, index: pageIndex }, apiTimeoutMs);
368
- if (shouldNavigateToSeed(page.url, fallbackSeedUrl)) {
369
- await callApiWithTimeout('goto', { profileId, url: fallbackSeedUrl }, navigationTimeoutMs);
370
- if (openDelayMs > 0) await sleep(Math.min(openDelayMs, 1200));
371
- }
372
- const syncResult = await syncTabViewportIfNeeded({ profileId, syncConfig });
373
- if (!syncResult?.ok) {
374
- return asErrorPayload('OPERATION_FAILED', 'tab viewport sync failed', {
375
- pageIndex,
376
- syncResult,
377
- });
465
+ try {
466
+ await callApiWithTimeout('page:switch', { profileId, index: pageIndex }, apiTimeoutMs);
467
+ if (shouldNavigateToSeed(page.url, fallbackSeedUrl)) {
468
+ await callApiWithTimeout('goto', { profileId, url: fallbackSeedUrl }, navigationTimeoutMs);
469
+ if (openDelayMs > 0) await sleep(Math.min(openDelayMs, 1200));
470
+ }
471
+ const syncResult = await syncTabViewportIfNeeded({ profileId, syncConfig });
472
+ if (!syncResult?.ok) {
473
+ throw new Error(syncResult?.message || 'tab viewport sync failed');
474
+ }
475
+ } catch (err) {
476
+ const activePage = selected.find((item) => Number(item.index) === Number(activeIndex)) || selected[0] || null;
477
+ const slots = activePage
478
+ ? [{
479
+ slotIndex: 1,
480
+ tabRealIndex: Number(activePage.index),
481
+ url: String(activePage.url || ''),
482
+ }]
483
+ : [];
484
+ if (runtimeState) {
485
+ runtimeState.tabPool = {
486
+ slots,
487
+ cursor: 0,
488
+ count: slots.length,
489
+ syncConfig,
490
+ apiTimeoutMs,
491
+ initializedAt: new Date().toISOString(),
492
+ };
493
+ }
494
+ return {
495
+ ok: true,
496
+ code: 'OPERATION_DEGRADED',
497
+ message: 'ensure_tab_pool degraded to active tab',
498
+ data: {
499
+ tabCount: slots.length,
500
+ normalized: false,
501
+ degraded: true,
502
+ reason: err?.message || 'page switch failed',
503
+ slots,
504
+ pages: activePage ? [activePage] : [],
505
+ },
506
+ };
378
507
  }
379
508
  }
380
509
  listed = await callApiWithTimeout('page:list', { profileId }, apiTimeoutMs);
@@ -391,13 +520,40 @@ export async function executeTabPoolOperation({ profileId, action, params = {},
391
520
  }));
392
521
 
393
522
  if (slots.length > 0) {
394
- await callApiWithTimeout('page:switch', {
395
- profileId,
396
- index: Number(slots[0].tabRealIndex),
397
- }, apiTimeoutMs);
398
- const syncResult = await syncTabViewportIfNeeded({ profileId, syncConfig });
399
- if (!syncResult?.ok) {
400
- return asErrorPayload('OPERATION_FAILED', 'tab viewport sync failed', { slotIndex: 1, syncResult });
523
+ try {
524
+ await callApiWithTimeout('page:switch', {
525
+ profileId,
526
+ index: Number(slots[0].tabRealIndex),
527
+ }, apiTimeoutMs);
528
+ const syncResult = await syncTabViewportIfNeeded({ profileId, syncConfig });
529
+ if (!syncResult?.ok) {
530
+ throw new Error(syncResult?.message || 'tab viewport sync failed');
531
+ }
532
+ } catch (err) {
533
+ const activePage = slots[0];
534
+ if (runtimeState) {
535
+ runtimeState.tabPool = {
536
+ slots: activePage ? [activePage] : [],
537
+ cursor: 0,
538
+ count: activePage ? 1 : 0,
539
+ syncConfig,
540
+ apiTimeoutMs,
541
+ initializedAt: new Date().toISOString(),
542
+ };
543
+ }
544
+ return {
545
+ ok: true,
546
+ code: 'OPERATION_DEGRADED',
547
+ message: 'ensure_tab_pool degraded on final switch',
548
+ data: {
549
+ tabCount: activePage ? 1 : 0,
550
+ normalized: false,
551
+ degraded: true,
552
+ reason: err?.message || 'page switch failed',
553
+ slots: activePage ? [activePage] : [],
554
+ pages: activePage ? [activePage] : [],
555
+ },
556
+ };
401
557
  }
402
558
  }
403
559
 
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Simple Bloom Filter for memory-efficient deduplication
3
+ * Uses non-cryptographic hash functions for speed
4
+ */
5
+
6
+ export class BloomFilter {
7
+ private bitmap: Uint8Array;
8
+ private size: number;
9
+ private hashCount: number;
10
+ private count: number = 0;
11
+
12
+ constructor(expectedItems: number = 500000, falsePositiveRate: number = 0.001) {
13
+ // Calculate optimal size and hash count
14
+ this.size = Math.ceil(-expectedItems * Math.log(falsePositiveRate) / Math.pow(Math.LN2, 2));
15
+ this.hashCount = Math.ceil((this.size / expectedItems) * Math.LN2);
16
+ this.bitmap = new Uint8Array(Math.ceil(this.size / 8));
17
+ }
18
+
19
+ private hash(str: string, seed: number): number {
20
+ // FNV-1a variant with seed
21
+ let hash = 2166136261 ^ seed;
22
+ for (let i = 0; i < str.length; i++) {
23
+ hash ^= str.charCodeAt(i);
24
+ hash = Math.imul(hash, 16777619);
25
+ }
26
+ return Math.abs(hash) % this.size;
27
+ }
28
+
29
+ add(item: string): void {
30
+ for (let i = 0; i < this.hashCount; i++) {
31
+ const bit = this.hash(item, i);
32
+ const byteIndex = Math.floor(bit / 8);
33
+ const bitIndex = bit % 8;
34
+ this.bitmap[byteIndex] |= (1 << bitIndex);
35
+ }
36
+ this.count++;
37
+ }
38
+
39
+ mightContain(item: string): boolean {
40
+ for (let i = 0; i < this.hashCount; i++) {
41
+ const bit = this.hash(item, i);
42
+ const byteIndex = Math.floor(bit / 8);
43
+ const bitIndex = bit % 8;
44
+ if ((this.bitmap[byteIndex] & (1 << bitIndex)) === 0) {
45
+ return false;
46
+ }
47
+ }
48
+ return true;
49
+ }
50
+
51
+ getCount(): number {
52
+ return this.count;
53
+ }
54
+
55
+ /**
56
+ * Export to base64 string for persistence
57
+ */
58
+ export(): string {
59
+ // Header: hashCount (4), count (4), size (4) = 12 bytes
60
+ const header = Buffer.alloc(12);
61
+ header.writeUInt32LE(this.hashCount, 0);
62
+ header.writeUInt32LE(this.count, 4);
63
+ header.writeUInt32LE(this.size, 8);
64
+ const buffer = Buffer.concat([header, Buffer.from(this.bitmap)]);
65
+ return buffer.toString('base64');
66
+ }
67
+
68
+ /**
69
+ * Import from base64 string
70
+ */
71
+ static import(base64: string, expectedItems: number = 100000): BloomFilter {
72
+ const buffer = Buffer.from(base64, 'base64');
73
+
74
+ // Read header (12 bytes)
75
+ const hashCount = buffer.readUInt32LE(0);
76
+ const count = buffer.readUInt32LE(4);
77
+ const size = buffer.readUInt32LE(8);
78
+ const bitmapBuffer = buffer.slice(12);
79
+
80
+ const filter = Object.create(BloomFilter.prototype);
81
+ filter.bitmap = new Uint8Array(bitmapBuffer);
82
+ filter.size = size;
83
+ filter.hashCount = hashCount;
84
+ filter.count = count;
85
+
86
+ return filter;
87
+ }
88
+
89
+ /**
90
+ * Serialize to JSON
91
+ */
92
+ toJSON(): { bitmap: string; size: number; hashCount: number; count: number } {
93
+ return {
94
+ bitmap: this.export(),
95
+ size: this.size,
96
+ hashCount: this.hashCount,
97
+ count: this.count
98
+ };
99
+ }
100
+
101
+ /**
102
+ * Deserialize from JSON
103
+ */
104
+ static fromJSON(json: { bitmap: string; size: number; hashCount: number; count: number }): BloomFilter {
105
+ const filter = new BloomFilter(1000); // Dummy, will be overwritten
106
+ filter.bitmap = new Uint8Array(Buffer.from(json.bitmap, 'base64'));
107
+ filter.size = json.size;
108
+ filter.hashCount = json.hashCount;
109
+ filter.count = json.count;
110
+ return filter;
111
+ }
112
+ }
@@ -0,0 +1,316 @@
1
+ /**
2
+ * Date extraction utilities for social media posts
3
+ * Handles various time formats from platforms like Weibo, Xiaohongshu
4
+ */
5
+
6
+ /**
7
+ * Parse relative time strings to absolute date
8
+ *
9
+ * Supported formats:
10
+ * - "刚刚" / "刚刚来自..." → now
11
+ * - "5分钟前" / "30秒前" → relative to now
12
+ * - "今天 08:30" / "今天08:30" → today
13
+ * - "昨天 14:20" / "昨天14:20" → yesterday
14
+ * - "前天 10:00" → 2 days ago
15
+ * - "01-15" / "1月15日" → this year
16
+ * - "2025-12-01" / "2025年12月01日" → exact date
17
+ * - "12-01 15:30" → this year with time
18
+ */
19
+ export function parsePlatformDate(
20
+ text: string,
21
+ options: {
22
+ now?: Date;
23
+ timezone?: string;
24
+ } = {}
25
+ ): { date: string; time: string; fullText: string } | null {
26
+ const { now = new Date(), timezone = 'Asia/Shanghai' } = options;
27
+ const trimmed = text.trim();
28
+
29
+ if (!trimmed) return null;
30
+
31
+ // Get current date in specified timezone
32
+ const currentDate = new Date(now.toLocaleString('en-US', { timeZone: timezone }));
33
+ const currentYear = currentDate.getFullYear();
34
+
35
+ // "刚刚" / "刚刚来自..."
36
+ if (trimmed.includes('刚刚')) {
37
+ return {
38
+ date: formatDate(currentDate),
39
+ time: formatTime(currentDate),
40
+ fullText: formatDateTime(currentDate)
41
+ };
42
+ }
43
+
44
+ // "X分钟前" / "X秒前" / "X小时前"
45
+ const relativeMatch = trimmed.match(/(\d+)\s*(秒|分钟|小时)前/);
46
+ if (relativeMatch) {
47
+ const amount = parseInt(relativeMatch[1], 10);
48
+ const unit = relativeMatch[2];
49
+ const result = new Date(currentDate);
50
+
51
+ if (unit === '秒') result.setSeconds(result.getSeconds() - amount);
52
+ else if (unit === '分钟') result.setMinutes(result.getMinutes() - amount);
53
+ else if (unit === '小时') result.setHours(result.getHours() - amount);
54
+
55
+ return {
56
+ date: formatDate(result),
57
+ time: formatTime(result),
58
+ fullText: formatDateTime(result)
59
+ };
60
+ }
61
+
62
+ // "今天 08:30" / "今天08:30"
63
+ const todayMatch = trimmed.match(/今天\s*(\d{1,2}):(\d{2})/);
64
+ if (todayMatch) {
65
+ const hour = parseInt(todayMatch[1], 10);
66
+ const minute = parseInt(todayMatch[2], 10);
67
+ const result = new Date(currentDate);
68
+ result.setHours(hour, minute, 0, 0);
69
+
70
+ return {
71
+ date: formatDate(result),
72
+ time: formatTime(result),
73
+ fullText: formatDateTime(result)
74
+ };
75
+ }
76
+
77
+ // "昨天 14:20" / "昨天14:20"
78
+ const yesterdayMatch = trimmed.match(/昨天\s*(\d{1,2}):(\d{2})/);
79
+ if (yesterdayMatch) {
80
+ const hour = parseInt(yesterdayMatch[1], 10);
81
+ const minute = parseInt(yesterdayMatch[2], 10);
82
+ const result = new Date(currentDate);
83
+ result.setDate(result.getDate() - 1);
84
+ result.setHours(hour, minute, 0, 0);
85
+
86
+ return {
87
+ date: formatDate(result),
88
+ time: formatTime(result),
89
+ fullText: formatDateTime(result)
90
+ };
91
+ }
92
+
93
+ // "前天 10:00"
94
+ const dayBeforeYesterdayMatch = trimmed.match(/前天\s*(\d{1,2}):(\d{2})/);
95
+ if (dayBeforeYesterdayMatch) {
96
+ const hour = parseInt(dayBeforeYesterdayMatch[1], 10);
97
+ const minute = parseInt(dayBeforeYesterdayMatch[2], 10);
98
+ const result = new Date(currentDate);
99
+ result.setDate(result.getDate() - 2);
100
+ result.setHours(hour, minute, 0, 0);
101
+
102
+ return {
103
+ date: formatDate(result),
104
+ time: formatTime(result),
105
+ fullText: formatDateTime(result)
106
+ };
107
+ }
108
+
109
+ // "2天前" / "3天前"
110
+ const daysAgoMatch = trimmed.match(/(\d+)\s*天前/);
111
+ if (daysAgoMatch) {
112
+ const days = parseInt(daysAgoMatch[1], 10);
113
+ const result = new Date(currentDate);
114
+ result.setDate(result.getDate() - days);
115
+
116
+ return {
117
+ date: formatDate(result),
118
+ time: '',
119
+ fullText: formatDate(result)
120
+ };
121
+ }
122
+
123
+ // "2025-12-01" / "2025年12月01日" - Full date (must match before MM-DD)
124
+ const fullDateMatch = trimmed.match(/(\d{4})[-年](\d{1,2})[-月](\d{1,2})/);
125
+ if (fullDateMatch) {
126
+ const year = parseInt(fullDateMatch[1], 10);
127
+ const month = parseInt(fullDateMatch[2], 10);
128
+ const day = parseInt(fullDateMatch[3], 10);
129
+ const result = new Date(year, month - 1, day);
130
+
131
+ // Check if there's time info
132
+ const timeMatch = trimmed.match(/(\d{1,2}):(\d{2})/);
133
+ if (timeMatch) {
134
+ result.setHours(parseInt(timeMatch[1], 10), parseInt(timeMatch[2], 10), 0, 0);
135
+ if (result.getTime() > currentDate.getTime()) {
136
+ result.setFullYear(currentYear - 1);
137
+ }
138
+ return {
139
+ date: formatDate(result),
140
+ time: formatTime(result),
141
+ fullText: formatDateTime(result)
142
+ };
143
+ }
144
+
145
+ return {
146
+ date: formatDate(result),
147
+ time: '',
148
+ fullText: formatDate(result)
149
+ };
150
+ }
151
+
152
+ // "01-15" / "1月15日" (this year, fallback to previous year when parsed date is in future)
153
+ const monthDayMatch = trimmed.match(/(\d{1,2})[-月](\d{1,2})日?/);
154
+ if (monthDayMatch && !trimmed.includes('年')) {
155
+ const month = parseInt(monthDayMatch[1], 10);
156
+ const day = parseInt(monthDayMatch[2], 10);
157
+ const result = new Date(currentYear, month - 1, day);
158
+
159
+ // Check if there's time info
160
+ const timeMatch = trimmed.match(/(\d{1,2}):(\d{2})/);
161
+ if (timeMatch) {
162
+ result.setHours(parseInt(timeMatch[1], 10), parseInt(timeMatch[2], 10), 0, 0);
163
+ if (result.getTime() > currentDate.getTime()) {
164
+ result.setFullYear(currentYear - 1);
165
+ }
166
+ return {
167
+ date: formatDate(result),
168
+ time: formatTime(result),
169
+ fullText: formatDateTime(result)
170
+ };
171
+ }
172
+ if (result.getTime() > currentDate.getTime()) {
173
+ result.setFullYear(currentYear - 1);
174
+ }
175
+
176
+ return {
177
+ date: formatDate(result),
178
+ time: '',
179
+ fullText: formatDate(result)
180
+ };
181
+ }
182
+
183
+
184
+ return null;
185
+ }
186
+
187
+ /**
188
+ * Format date as YYYY-MM-DD
189
+ */
190
+ function formatDate(date: Date): string {
191
+ const year = date.getFullYear();
192
+ const month = String(date.getMonth() + 1).padStart(2, '0');
193
+ const day = String(date.getDate()).padStart(2, '0');
194
+ return `${year}-${month}-${day}`;
195
+ }
196
+
197
+ /**
198
+ * Format time as HH:MM
199
+ */
200
+ function formatTime(date: Date): string {
201
+ const hour = String(date.getHours()).padStart(2, '0');
202
+ const minute = String(date.getMinutes()).padStart(2, '0');
203
+ return `${hour}:${minute}`;
204
+ }
205
+
206
+ /**
207
+ * Format datetime as YYYY-MM-DD HH:MM
208
+ */
209
+ function formatDateTime(date: Date): string {
210
+ return `${formatDate(date)} ${formatTime(date)}`;
211
+ }
212
+
213
+ /**
214
+ * Get current timestamp in ISO format with timezone
215
+ */
216
+ export function getCurrentTimestamp(timezone: string = 'Asia/Shanghai'): {
217
+ collectedAt: string; // ISO 8601 UTC: 2026-02-20T14:58:44.494Z
218
+ collectedAtLocal: string; // Local with TZ: 2026-02-20 22:58:44.494 +08:00
219
+ collectedDate: string; // Date only: 2026-02-20
220
+ } {
221
+ const now = new Date();
222
+
223
+ // UTC ISO string
224
+ const collectedAt = now.toISOString();
225
+
226
+ // Local time with timezone
227
+ const formatter = new Intl.DateTimeFormat('en-US', {
228
+ timeZone: timezone,
229
+ year: 'numeric',
230
+ month: '2-digit',
231
+ day: '2-digit',
232
+ hour: '2-digit',
233
+ minute: '2-digit',
234
+ second: '2-digit',
235
+ fractionalSecondDigits: 3,
236
+ hour12: false
237
+ });
238
+
239
+ const parts = formatter.formatToParts(now);
240
+ const get = (type: string) => parts.find(p => p.type === type)?.value || '00';
241
+
242
+ const year = get('year');
243
+ const month = get('month');
244
+ const day = get('day');
245
+ const hour = get('hour');
246
+ const minute = get('minute');
247
+ const second = get('second');
248
+ const ms = get('fractionalSecond');
249
+
250
+ // Get timezone offset
251
+ const tzOffset = new Intl.DateTimeFormat('en-US', {
252
+ timeZone: timezone,
253
+ timeZoneName: 'shortOffset'
254
+ }).format(now).split(' ').pop() || '+08:00';
255
+
256
+ const collectedAtLocal = `${year}-${month}-${day} ${hour}:${minute}:${second}.${ms} ${tzOffset}`;
257
+ const collectedDate = `${year}-${month}-${day}`;
258
+
259
+ return {
260
+ collectedAt,
261
+ collectedAtLocal,
262
+ collectedDate
263
+ };
264
+ }
265
+
266
+ /**
267
+ * Weibo-specific date extraction from post element
268
+ */
269
+ export function extractWeiboPostDate(
270
+ postElement: Element,
271
+ now: Date = new Date()
272
+ ): { date: string; time: string; fullText: string } | null {
273
+ // Weibo post time is usually in:
274
+ // - <a class="head-info_time_..."> or similar
275
+ // - Element with from, time info
276
+
277
+ const timeSelectors = [
278
+ 'a[class*="time"]',
279
+ 'a[class*="date"]',
280
+ 'span[class*="time"]',
281
+ '.from a',
282
+ 'a[href*="weibo.com"]'
283
+ ];
284
+
285
+ for (const selector of timeSelectors) {
286
+ const timeEl = postElement.querySelector(selector);
287
+ if (timeEl) {
288
+ const text = timeEl.textContent?.trim();
289
+ if (text) {
290
+ const parsed = parsePlatformDate(text, { now });
291
+ if (parsed) return parsed;
292
+ }
293
+ }
294
+ }
295
+
296
+ // Fallback: search all text content for date patterns
297
+ const allText = postElement.textContent || '';
298
+ const datePatterns = [
299
+ /刚刚/,
300
+ /\d+\s*(秒|分钟|小时)前/,
301
+ /今天\s*\d{1,2}:\d{2}/,
302
+ /昨天\s*\d{1,2}:\d{2}/,
303
+ /\d{1,2}-\d{1,2}/,
304
+ /\d{4}-\d{1,2}-\d{1,2}/
305
+ ];
306
+
307
+ for (const pattern of datePatterns) {
308
+ const match = allText.match(pattern);
309
+ if (match) {
310
+ const parsed = parsePlatformDate(match[0], { now });
311
+ if (parsed) return parsed;
312
+ }
313
+ }
314
+
315
+ return null;
316
+ }