@web-auto/webauto 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/desktop-console/default-settings.json +2 -2
- package/apps/desktop-console/dist/main/index.mjs +915 -85
- package/apps/desktop-console/dist/main/preload.mjs +7 -0
- package/apps/desktop-console/dist/renderer/index.html +622 -50
- package/apps/desktop-console/dist/renderer/index.js +2415 -470
- package/apps/desktop-console/dist/renderer/run.mts +6 -5
- package/apps/desktop-console/entry/ui-cli.mjs +672 -0
- package/apps/desktop-console/entry/ui-console.mjs +416 -29
- package/apps/webauto/entry/account.mjs +89 -53
- package/apps/webauto/entry/browser-status.mjs +7 -10
- package/apps/webauto/entry/lib/account-detect.mjs +254 -28
- package/apps/webauto/entry/lib/account-store.mjs +219 -30
- package/apps/webauto/entry/lib/bus-publish.mjs +63 -0
- package/apps/webauto/entry/lib/camo-cli.mjs +93 -0
- package/apps/webauto/entry/lib/profilepool.mjs +14 -5
- package/apps/webauto/entry/lib/quota-status.mjs +23 -0
- package/apps/webauto/entry/lib/schedule-store.mjs +1068 -0
- package/apps/webauto/entry/profilepool.mjs +106 -17
- package/apps/webauto/entry/schedule.mjs +612 -0
- package/apps/webauto/entry/weibo-unified.mjs +134 -0
- package/apps/webauto/entry/xhs-install.mjs +236 -29
- package/apps/webauto/entry/xhs-status.mjs +5 -2
- package/apps/webauto/entry/xhs-unified.mjs +631 -98
- package/apps/webauto/resources/container-library/weibo/weibo_detail_page/comment_item/container.json +40 -0
- package/apps/webauto/resources/container-library/weibo/weibo_detail_page/reply_expand_button/container.json +38 -0
- package/apps/webauto/resources/container-library/weibo/weibo_detail_page/reply_list/container.json +37 -0
- package/apps/webauto/resources/container-library/weibo/weibo_search_page/container.json +8 -3
- package/apps/webauto/resources/container-library/weibo/weibo_search_page/login_anchor/container.json +30 -0
- package/apps/webauto/resources/container-library/weibo/weibo_search_page/search_bar/container.json +47 -0
- package/apps/webauto/resources/container-library/weibo/weibo_search_page/search_button/container.json +39 -0
- package/bin/camoufox-cli.mjs +61 -0
- package/bin/webauto.mjs +301 -54
- package/dist/modules/camo-backend/src/index.js +49 -1
- package/dist/modules/camo-backend/src/internal/BrowserSession.js +572 -3
- package/dist/modules/camo-backend/src/internal/SessionManager.js +13 -1
- package/dist/modules/camo-backend/src/internal/storage-paths.js +6 -0
- package/dist/modules/collection-manager/bloom-filter.js +91 -0
- package/dist/modules/collection-manager/date-utils.js +275 -0
- package/dist/modules/collection-manager/index.js +258 -0
- package/dist/modules/collection-manager/storage.js +195 -0
- package/dist/modules/collection-manager/types.js +47 -0
- package/dist/modules/logging/src/index.js +1 -1
- package/dist/modules/process-registry/index.js +230 -0
- package/dist/modules/rate-limiter/index.js +242 -0
- package/dist/modules/workflow/blocks/ExecuteWeiboSearchBlock.js +128 -0
- package/dist/modules/workflow/blocks/PersistXhsNoteBlock.js +7 -3
- package/dist/modules/workflow/blocks/RenderMarkdown.js +4 -1
- package/dist/modules/workflow/blocks/WeiboCollectCommentsBlock.js +282 -0
- package/dist/modules/workflow/blocks/WeiboCollectFromLinksBlock.js +283 -0
- package/dist/modules/workflow/blocks/WeiboCollectSearchLinksBlock.js +208 -0
- package/dist/modules/workflow/blocks/WeiboCollectTimelineListBlock.js +128 -0
- package/dist/modules/workflow/blocks/WeiboCollectUserPostsListBlock.js +127 -0
- package/dist/modules/workflow/blocks/helpers/downloadPaths.js +21 -0
- package/dist/modules/workflow/config/workflowRegistry.js +2 -0
- package/dist/modules/workflow/definitions/weibo-search-workflow-v1.js +47 -0
- package/dist/modules/workflow/src/runner.js +6 -0
- package/dist/modules/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.js +4 -0
- package/dist/modules/xiaohongshu/app/src/blocks/Phase3InteractBlock.js +2 -2
- package/dist/modules/xiaohongshu/app/src/blocks/helpers/sharding.js +123 -0
- package/dist/modules/xiaohongshu/app/src/container-registry/src/index.d.ts +37 -0
- package/dist/modules/xiaohongshu/app/src/container-registry/src/index.js +184 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/AnchorVerificationBlock.d.ts +31 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/AnchorVerificationBlock.js +71 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/DetectPageStateBlock.d.ts +48 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/DetectPageStateBlock.js +259 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/ErrorRecoveryBlock.d.ts +28 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/ErrorRecoveryBlock.js +319 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/WaitSearchPermitBlock.d.ts +36 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/WaitSearchPermitBlock.js +162 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/containerAnchors.d.ts +36 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/containerAnchors.js +301 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/operationLogger.d.ts +29 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/operationLogger.js +195 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/searchPageState.d.ts +25 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/searchPageState.js +164 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/MatchCommentsBlock.d.ts +66 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/MatchCommentsBlock.js +139 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1EnsureServicesBlock.d.ts +16 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1EnsureServicesBlock.js +36 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1MonitorCookieBlock.d.ts +27 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1MonitorCookieBlock.js +213 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1StartProfileBlock.d.ts +18 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1StartProfileBlock.js +121 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2CollectLinksBlock.d.ts +34 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2CollectLinksBlock.js +1249 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2SearchBlock.d.ts +17 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2SearchBlock.js +703 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseDetailBlock.d.ts +15 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseDetailBlock.js +41 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseTabsBlock.d.ts +26 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseTabsBlock.js +44 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CollectCommentsBlock.d.ts +29 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CollectCommentsBlock.js +150 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ExtractDetailBlock.d.ts +38 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ExtractDetailBlock.js +117 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenDetailBlock.d.ts +30 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenDetailBlock.js +102 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenTabsBlock.d.ts +23 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenTabsBlock.js +109 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.d.ts +32 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.js +117 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ProcessSingleNoteBlock.d.ts +35 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ProcessSingleNoteBlock.js +114 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ValidateLinksBlock.d.ts +34 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ValidateLinksBlock.js +90 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase3InteractBlock.d.ts +111 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase3InteractBlock.js +1009 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase4MultiTabHarvestBlock.d.ts +20 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase4MultiTabHarvestBlock.js +233 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/ReplyInteractBlock.d.ts +48 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/ReplyInteractBlock.js +291 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/XhsDiscoverFallbackBlock.d.ts +23 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/XhsDiscoverFallbackBlock.js +240 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatchDsl.d.ts +55 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatchDsl.js +126 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatcher.d.ts +21 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatcher.js +99 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/evidence.d.ts +5 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/evidence.js +27 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/sharding.d.ts +37 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/sharding.js +165 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/xhsComments.d.ts +33 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/xhsComments.js +270 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/index.d.ts +9 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/index.js +9 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/checkpoints.d.ts +50 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/checkpoints.js +222 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/controllerAction.d.ts +10 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/controllerAction.js +43 -0
- package/dist/services/shared/serviceProcessLogger.js +1 -1
- package/dist/services/unified-api/server.js +105 -11
- package/modules/camo-backend/src/index.ts +46 -1
- package/modules/camo-backend/src/internal/BrowserSession.ts +619 -3
- package/modules/camo-backend/src/internal/SessionManager.ts +12 -1
- package/modules/camo-backend/src/internal/storage-paths.ts +5 -0
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/comments.mjs +38 -2
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +47 -2
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +94 -11
- package/modules/camo-runtime/src/autoscript/action-providers/xhs.mjs +208 -2
- package/modules/camo-runtime/src/autoscript/runtime.mjs +7 -1
- package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +76 -43
- package/modules/camo-runtime/src/container/runtime-core/operations/index.mjs +75 -1
- package/modules/camo-runtime/src/container/runtime-core/operations/selector-scripts.mjs +71 -4
- package/modules/camo-runtime/src/container/runtime-core/operations/tab-pool.mjs +183 -27
- package/modules/collection-manager/bloom-filter.ts +112 -0
- package/modules/collection-manager/date-utils.ts +316 -0
- package/modules/collection-manager/index.ts +309 -0
- package/modules/collection-manager/package.json +10 -0
- package/modules/collection-manager/storage.ts +174 -0
- package/modules/collection-manager/types.ts +156 -0
- package/modules/logging/src/index.ts +1 -1
- package/modules/process-registry/index.ts +284 -0
- package/modules/rate-limiter/index.ts +322 -0
- package/modules/state/src/paths.ts +9 -1
- package/modules/task-scheduler/index.ts +293 -0
- package/modules/workflow/blocks/ExecuteWeiboSearchBlock.ts +167 -0
- package/modules/workflow/blocks/PersistXhsNoteBlock.ts +7 -3
- package/modules/workflow/blocks/RenderMarkdown.ts +4 -1
- package/modules/workflow/blocks/WeiboCollectCommentsBlock.ts +339 -0
- package/modules/workflow/blocks/WeiboCollectFromLinksBlock.ts +338 -0
- package/modules/workflow/blocks/helpers/downloadPaths.ts +16 -0
- package/modules/workflow/config/workflowRegistry.ts +2 -0
- package/modules/workflow/definitions/weibo-search-workflow-v1.ts +47 -0
- package/modules/workflow/src/runner.ts +6 -0
- package/modules/xiaohongshu/app/src/blocks/Phase1StartProfileBlock.ts +1 -1
- package/modules/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.ts +4 -0
- package/modules/xiaohongshu/app/src/blocks/Phase3InteractBlock.ts +2 -3
- package/modules/xiaohongshu/app/src/blocks/helpers/sharding.ts +152 -0
- package/package.json +13 -4
- package/scripts/postinstall-resources.mjs +62 -0
- package/scripts/test/run-coverage.mjs +76 -0
- package/scripts/weibo/search.ts +49 -0
- package/services/shared/serviceProcessLogger.ts +1 -1
- package/services/unified-api/server.ts +98 -12
|
@@ -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
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
+
}
|