@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
|
@@ -19,6 +19,13 @@ function toPositiveInt(value, fallback, min = 1) {
|
|
|
19
19
|
return Math.max(min, Math.floor(num));
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
function toNonNegativeInt(value, fallback = 0) {
|
|
23
|
+
if (value === null || value === undefined || value === '') return fallback;
|
|
24
|
+
const num = Number(value);
|
|
25
|
+
if (!Number.isFinite(num)) return fallback;
|
|
26
|
+
return Math.max(0, Math.floor(num));
|
|
27
|
+
}
|
|
28
|
+
|
|
22
29
|
function splitCsv(value) {
|
|
23
30
|
if (Array.isArray(value)) {
|
|
24
31
|
return value
|
|
@@ -32,9 +39,6 @@ function splitCsv(value) {
|
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
function pickCloseDependency(options) {
|
|
35
|
-
if (options.doReply || options.doLikes) return 'comment_match_gate';
|
|
36
|
-
if (options.matchGateEnabled) return 'comment_match_gate';
|
|
37
|
-
if (options.commentsHarvestEnabled) return 'comments_harvest';
|
|
38
42
|
if (options.detailHarvestEnabled) return 'detail_harvest';
|
|
39
43
|
return 'open_first_detail';
|
|
40
44
|
}
|
|
@@ -73,7 +77,9 @@ function buildOpenFirstDetailScript(maxNotes, keyword) {
|
|
|
73
77
|
const cover = item.querySelector('a.cover');
|
|
74
78
|
if (!cover) return null;
|
|
75
79
|
const href = String(cover.getAttribute('href') || '').trim();
|
|
76
|
-
const
|
|
80
|
+
const lastSegment = href.split('/').filter(Boolean).pop() || '';
|
|
81
|
+
const normalized = lastSegment.split('?')[0].split('#')[0];
|
|
82
|
+
const noteId = normalized || ('idx_' + index);
|
|
77
83
|
return { item, cover, href, noteId };
|
|
78
84
|
})
|
|
79
85
|
.filter(Boolean);
|
|
@@ -137,7 +143,9 @@ function buildOpenNextDetailScript(maxNotes) {
|
|
|
137
143
|
const cover = item.querySelector('a.cover');
|
|
138
144
|
if (!cover) return null;
|
|
139
145
|
const href = String(cover.getAttribute('href') || '').trim();
|
|
140
|
-
const
|
|
146
|
+
const lastSegment = href.split('/').filter(Boolean).pop() || '';
|
|
147
|
+
const normalized = lastSegment.split('?')[0].split('#')[0];
|
|
148
|
+
const noteId = normalized || ('idx_' + index);
|
|
141
149
|
return { item, cover, href, noteId };
|
|
142
150
|
})
|
|
143
151
|
.filter(Boolean);
|
|
@@ -320,6 +328,7 @@ function buildCommentLikeScript(likeKeywords, maxLikesPerRound) {
|
|
|
320
328
|
const state = window.__camoXhsState || (window.__camoXhsState = {});
|
|
321
329
|
const keywords = ${JSON.stringify(likeKeywords)};
|
|
322
330
|
const maxLikes = Number(${maxLikesPerRound});
|
|
331
|
+
const maxLikesLimit = maxLikes > 0 ? maxLikes : Number.POSITIVE_INFINITY;
|
|
323
332
|
const nodes = Array.from(document.querySelectorAll('.comment-item'));
|
|
324
333
|
const matches = Array.isArray(state.matchedComments) ? state.matchedComments : [];
|
|
325
334
|
const targetIndexes = new Set(matches.map((row) => Number(row.index)));
|
|
@@ -327,7 +336,7 @@ function buildCommentLikeScript(likeKeywords, maxLikesPerRound) {
|
|
|
327
336
|
let likedCount = 0;
|
|
328
337
|
let skippedActive = 0;
|
|
329
338
|
for (let idx = 0; idx < nodes.length; idx += 1) {
|
|
330
|
-
if (likedCount >=
|
|
339
|
+
if (likedCount >= maxLikesLimit) break;
|
|
331
340
|
if (targetIndexes.size > 0 && !targetIndexes.has(idx)) continue;
|
|
332
341
|
const item = nodes[idx];
|
|
333
342
|
const text = String((item.querySelector('.content, .comment-content, p') || {}).textContent || '').trim();
|
|
@@ -460,16 +469,23 @@ function buildAbortScript(code) {
|
|
|
460
469
|
export function buildXhsUnifiedAutoscript(rawOptions = {}) {
|
|
461
470
|
const profileId = toTrimmedString(rawOptions.profileId, 'xiaohongshu-batch-1');
|
|
462
471
|
const keyword = toTrimmedString(rawOptions.keyword, '手机膜');
|
|
463
|
-
const env = toTrimmedString(rawOptions.env, '
|
|
472
|
+
const env = toTrimmedString(rawOptions.env, 'prod');
|
|
464
473
|
const outputRoot = toTrimmedString(rawOptions.outputRoot, '');
|
|
465
474
|
const throttle = toPositiveInt(rawOptions.throttle, 900, 100);
|
|
466
475
|
const tabCount = toPositiveInt(rawOptions.tabCount, 4, 1);
|
|
467
476
|
const noteIntervalMs = toPositiveInt(rawOptions.noteIntervalMs, 1200, 200);
|
|
468
477
|
const maxNotes = toPositiveInt(rawOptions.maxNotes, 30, 1);
|
|
469
|
-
const
|
|
478
|
+
const maxComments = toNonNegativeInt(rawOptions.maxComments, 0);
|
|
479
|
+
const resume = toBoolean(rawOptions.resume, false);
|
|
480
|
+
const incrementalMax = toBoolean(rawOptions.incrementalMax, true);
|
|
481
|
+
const maxLikesPerRound = toNonNegativeInt(rawOptions.maxLikesPerRound ?? rawOptions.maxLikes, 0);
|
|
470
482
|
const matchMode = toTrimmedString(rawOptions.matchMode, 'any');
|
|
471
483
|
const matchMinHits = toPositiveInt(rawOptions.matchMinHits, 1, 1);
|
|
472
484
|
const replyText = toTrimmedString(rawOptions.replyText, '感谢分享,已关注');
|
|
485
|
+
const sharedHarvestPath = toTrimmedString(rawOptions.sharedHarvestPath, '');
|
|
486
|
+
const searchSerialKey = toTrimmedString(rawOptions.searchSerialKey, `${env}:${keyword}`);
|
|
487
|
+
const seedCollectCount = toNonNegativeInt(rawOptions.seedCollectCount, 0);
|
|
488
|
+
const seedCollectMaxRounds = toNonNegativeInt(rawOptions.seedCollectMaxRounds, 0);
|
|
473
489
|
|
|
474
490
|
const doHomepage = toBoolean(rawOptions.doHomepage, true);
|
|
475
491
|
const doImages = toBoolean(rawOptions.doImages, false);
|
|
@@ -506,6 +522,7 @@ export function buildXhsUnifiedAutoscript(rawOptions = {}) {
|
|
|
506
522
|
profileId,
|
|
507
523
|
throttle,
|
|
508
524
|
defaults: {
|
|
525
|
+
disableTimeout: true,
|
|
509
526
|
retry: { attempts: 2, backoffMs: 500 },
|
|
510
527
|
impact: 'subscription',
|
|
511
528
|
onFailure: 'chain_stop',
|
|
@@ -516,9 +533,9 @@ export function buildXhsUnifiedAutoscript(rawOptions = {}) {
|
|
|
516
533
|
eventCooldownMs: 300,
|
|
517
534
|
jitterMs: 220,
|
|
518
535
|
navigationMinIntervalMs: 1800,
|
|
519
|
-
timeoutMs:
|
|
536
|
+
timeoutMs: 0,
|
|
520
537
|
},
|
|
521
|
-
timeoutMs:
|
|
538
|
+
timeoutMs: 0,
|
|
522
539
|
},
|
|
523
540
|
metadata: {
|
|
524
541
|
keyword,
|
|
@@ -527,6 +544,10 @@ export function buildXhsUnifiedAutoscript(rawOptions = {}) {
|
|
|
527
544
|
tabCount,
|
|
528
545
|
noteIntervalMs,
|
|
529
546
|
maxNotes,
|
|
547
|
+
maxComments,
|
|
548
|
+
maxLikesPerRound,
|
|
549
|
+
resume,
|
|
550
|
+
incrementalMax,
|
|
530
551
|
doHomepage,
|
|
531
552
|
doImages,
|
|
532
553
|
doComments,
|
|
@@ -539,9 +560,14 @@ export function buildXhsUnifiedAutoscript(rawOptions = {}) {
|
|
|
539
560
|
matchKeywords,
|
|
540
561
|
likeKeywords,
|
|
541
562
|
replyText,
|
|
563
|
+
sharedHarvestPath,
|
|
564
|
+
searchSerialKey,
|
|
565
|
+
seedCollectCount,
|
|
566
|
+
seedCollectMaxRounds,
|
|
542
567
|
notes: [
|
|
543
568
|
'open_next_detail intentionally stops script by throwing AUTOSCRIPT_DONE_* when exhausted.',
|
|
544
569
|
'dev mode uses deterministic no-recovery policy (checkpoint recovery disabled).',
|
|
570
|
+
'resume=true keeps visited note history for断点续传; incrementalMax=true treats maxNotes as增量配额.',
|
|
545
571
|
'when persistComments=true, xhs_comments_harvest emits full comments in operation result for downstream jsonl/file persistence.',
|
|
546
572
|
],
|
|
547
573
|
},
|
|
@@ -608,7 +634,11 @@ export function buildXhsUnifiedAutoscript(rawOptions = {}) {
|
|
|
608
634
|
{
|
|
609
635
|
id: 'submit_search',
|
|
610
636
|
action: 'xhs_submit_search',
|
|
611
|
-
params: {
|
|
637
|
+
params: {
|
|
638
|
+
keyword,
|
|
639
|
+
searchSerialKey,
|
|
640
|
+
sharedHarvestPath,
|
|
641
|
+
},
|
|
612
642
|
trigger: 'home_search_input.exist',
|
|
613
643
|
dependsOn: ['fill_keyword'],
|
|
614
644
|
once: true,
|
|
@@ -626,15 +656,22 @@ export function buildXhsUnifiedAutoscript(rawOptions = {}) {
|
|
|
626
656
|
{
|
|
627
657
|
id: 'open_first_detail',
|
|
628
658
|
action: 'xhs_open_detail',
|
|
629
|
-
params: {
|
|
659
|
+
params: {
|
|
660
|
+
mode: 'first',
|
|
661
|
+
maxNotes,
|
|
662
|
+
keyword,
|
|
663
|
+
resume,
|
|
664
|
+
incrementalMax,
|
|
665
|
+
sharedHarvestPath,
|
|
666
|
+
seedCollectCount,
|
|
667
|
+
seedCollectMaxRounds,
|
|
668
|
+
},
|
|
630
669
|
trigger: 'search_result_item.exist',
|
|
631
670
|
dependsOn: ['submit_search'],
|
|
632
671
|
once: true,
|
|
633
672
|
timeoutMs: 90000,
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
post: { page: { hostIncludes: ['xiaohongshu.com'], checkpointIn: ['detail_ready', 'comments_ready', 'search_ready'] } },
|
|
637
|
-
},
|
|
673
|
+
onFailure: 'continue',
|
|
674
|
+
validation: { mode: 'none' },
|
|
638
675
|
checkpoint: {
|
|
639
676
|
containerId: 'xiaohongshu_search.search_result_item',
|
|
640
677
|
targetCheckpoint: 'detail_ready',
|
|
@@ -655,17 +692,7 @@ export function buildXhsUnifiedAutoscript(rawOptions = {}) {
|
|
|
655
692
|
impact: 'op',
|
|
656
693
|
onFailure: 'continue',
|
|
657
694
|
pacing: { operationMinIntervalMs: 2000, eventCooldownMs: 1200, jitterMs: 260 },
|
|
658
|
-
validation: {
|
|
659
|
-
mode: 'both',
|
|
660
|
-
pre: {
|
|
661
|
-
page: { hostIncludes: ['xiaohongshu.com'], checkpointIn: ['detail_ready', 'comments_ready'] },
|
|
662
|
-
container: { selector: '.note-detail-mask, .note-detail-page, .note-detail-dialog', mustExist: true, minCount: 1 },
|
|
663
|
-
},
|
|
664
|
-
post: {
|
|
665
|
-
page: { hostIncludes: ['xiaohongshu.com'], checkpointIn: ['detail_ready', 'comments_ready'] },
|
|
666
|
-
container: { selector: '.note-content, .note-scroller, .media-container', mustExist: true, minCount: 1 },
|
|
667
|
-
},
|
|
668
|
-
},
|
|
695
|
+
validation: { mode: 'none' },
|
|
669
696
|
checkpoint: {
|
|
670
697
|
containerId: 'xiaohongshu_detail.modal_shell',
|
|
671
698
|
targetCheckpoint: 'detail_ready',
|
|
@@ -696,6 +723,7 @@ export function buildXhsUnifiedAutoscript(rawOptions = {}) {
|
|
|
696
723
|
keyword,
|
|
697
724
|
outputRoot,
|
|
698
725
|
persistComments,
|
|
726
|
+
commentsLimit: maxComments,
|
|
699
727
|
maxRounds: 48,
|
|
700
728
|
scrollStep: 360,
|
|
701
729
|
settleMs: 260,
|
|
@@ -717,9 +745,9 @@ export function buildXhsUnifiedAutoscript(rawOptions = {}) {
|
|
|
717
745
|
dependsOn: [detailHarvestEnabled ? 'detail_harvest' : 'open_first_detail'],
|
|
718
746
|
once: false,
|
|
719
747
|
oncePerAppear: true,
|
|
720
|
-
timeoutMs:
|
|
748
|
+
timeoutMs: 180000,
|
|
721
749
|
retry: { attempts: 1, backoffMs: 0 },
|
|
722
|
-
impact: '
|
|
750
|
+
impact: 'script',
|
|
723
751
|
onFailure: 'continue',
|
|
724
752
|
pacing: { operationMinIntervalMs: 2400, eventCooldownMs: 1500, jitterMs: 280 },
|
|
725
753
|
validation: {
|
|
@@ -745,7 +773,7 @@ export function buildXhsUnifiedAutoscript(rawOptions = {}) {
|
|
|
745
773
|
action: 'xhs_comment_match',
|
|
746
774
|
params: { keywords: matchKeywords, mode: matchMode, minHits: matchMinHits },
|
|
747
775
|
trigger: 'detail_modal.exist',
|
|
748
|
-
dependsOn: [
|
|
776
|
+
dependsOn: [detailHarvestEnabled ? 'detail_harvest' : 'open_first_detail'],
|
|
749
777
|
once: false,
|
|
750
778
|
oncePerAppear: true,
|
|
751
779
|
pacing: { operationMinIntervalMs: 2400, eventCooldownMs: 1200, jitterMs: 160 },
|
|
@@ -797,10 +825,7 @@ export function buildXhsUnifiedAutoscript(rawOptions = {}) {
|
|
|
797
825
|
once: false,
|
|
798
826
|
oncePerAppear: true,
|
|
799
827
|
pacing: { operationMinIntervalMs: 2500, eventCooldownMs: 1300, jitterMs: 180 },
|
|
800
|
-
validation: {
|
|
801
|
-
mode: 'post',
|
|
802
|
-
post: { page: { hostIncludes: ['xiaohongshu.com'], checkpointIn: ['home_ready', 'search_ready'] } },
|
|
803
|
-
},
|
|
828
|
+
validation: { mode: 'none' },
|
|
804
829
|
checkpoint: {
|
|
805
830
|
containerId: 'xiaohongshu_detail.discover_button',
|
|
806
831
|
targetCheckpoint: 'search_ready',
|
|
@@ -814,7 +839,7 @@ export function buildXhsUnifiedAutoscript(rawOptions = {}) {
|
|
|
814
839
|
trigger: 'search_result_item.exist',
|
|
815
840
|
dependsOn: ['close_detail'],
|
|
816
841
|
once: false,
|
|
817
|
-
oncePerAppear:
|
|
842
|
+
oncePerAppear: false,
|
|
818
843
|
retry: { attempts: 1, backoffMs: 0 },
|
|
819
844
|
impact: 'op',
|
|
820
845
|
onFailure: 'continue',
|
|
@@ -827,7 +852,7 @@ export function buildXhsUnifiedAutoscript(rawOptions = {}) {
|
|
|
827
852
|
trigger: 'search_result_item.exist',
|
|
828
853
|
dependsOn: ['wait_between_notes', 'ensure_tab_pool'],
|
|
829
854
|
once: false,
|
|
830
|
-
oncePerAppear:
|
|
855
|
+
oncePerAppear: false,
|
|
831
856
|
timeoutMs: 180000,
|
|
832
857
|
retry: { attempts: 2, backoffMs: 500 },
|
|
833
858
|
impact: 'op',
|
|
@@ -836,15 +861,21 @@ export function buildXhsUnifiedAutoscript(rawOptions = {}) {
|
|
|
836
861
|
{
|
|
837
862
|
id: 'open_next_detail',
|
|
838
863
|
action: 'xhs_open_detail',
|
|
839
|
-
params: {
|
|
864
|
+
params: {
|
|
865
|
+
mode: 'next',
|
|
866
|
+
maxNotes,
|
|
867
|
+
resume,
|
|
868
|
+
incrementalMax,
|
|
869
|
+
sharedHarvestPath,
|
|
870
|
+
},
|
|
840
871
|
trigger: 'search_result_item.exist',
|
|
841
872
|
dependsOn: ['switch_tab_round_robin'],
|
|
842
873
|
once: false,
|
|
843
|
-
oncePerAppear:
|
|
874
|
+
oncePerAppear: false,
|
|
844
875
|
timeoutMs: 90000,
|
|
845
|
-
retry: { attempts:
|
|
846
|
-
impact: '
|
|
847
|
-
onFailure: '
|
|
876
|
+
retry: { attempts: 3, backoffMs: 1000 },
|
|
877
|
+
impact: 'op',
|
|
878
|
+
onFailure: 'continue',
|
|
848
879
|
checkpoint: {
|
|
849
880
|
containerId: 'xiaohongshu_search.search_result_item',
|
|
850
881
|
targetCheckpoint: 'detail_ready',
|
|
@@ -914,6 +945,8 @@ export function buildXhsUnifiedAutoscript(rawOptions = {}) {
|
|
|
914
945
|
params: {
|
|
915
946
|
acrossPages: true,
|
|
916
947
|
settleMs: 320,
|
|
948
|
+
pageUrlIncludes: ['/search_result'],
|
|
949
|
+
requireMatchedPages: true,
|
|
917
950
|
selectors: [
|
|
918
951
|
{ id: 'home_search_input', selector: '#search-input, input.search-input' },
|
|
919
952
|
{ id: 'search_result_item', selector: '.note-item', visible: false, minCount: 1 },
|
|
@@ -923,8 +956,8 @@ export function buildXhsUnifiedAutoscript(rawOptions = {}) {
|
|
|
923
956
|
dependsOn: ['ensure_tab_pool'],
|
|
924
957
|
once: true,
|
|
925
958
|
timeoutMs: 90000,
|
|
926
|
-
impact: '
|
|
927
|
-
onFailure: '
|
|
959
|
+
impact: 'op',
|
|
960
|
+
onFailure: 'continue',
|
|
928
961
|
},
|
|
929
962
|
],
|
|
930
963
|
};
|
|
@@ -137,6 +137,56 @@ async function executeVerifySubscriptions({ profileId, params }) {
|
|
|
137
137
|
|
|
138
138
|
const acrossPages = params.acrossPages === true;
|
|
139
139
|
const settleMs = Math.max(0, Number(params.settleMs ?? 280) || 280);
|
|
140
|
+
const pageUrlIncludes = normalizeArray(params.pageUrlIncludes)
|
|
141
|
+
.map((item) => String(item || '').trim())
|
|
142
|
+
.filter(Boolean);
|
|
143
|
+
const pageUrlExcludes = normalizeArray(params.pageUrlExcludes)
|
|
144
|
+
.map((item) => String(item || '').trim())
|
|
145
|
+
.filter(Boolean);
|
|
146
|
+
const pageUrlRegex = String(params.pageUrlRegex || '').trim();
|
|
147
|
+
const pageUrlNotRegex = String(params.pageUrlNotRegex || '').trim();
|
|
148
|
+
const requireMatchedPages = params.requireMatchedPages !== false;
|
|
149
|
+
|
|
150
|
+
let includeRegex = null;
|
|
151
|
+
if (pageUrlRegex) {
|
|
152
|
+
try {
|
|
153
|
+
includeRegex = new RegExp(pageUrlRegex);
|
|
154
|
+
} catch {
|
|
155
|
+
return asErrorPayload('OPERATION_FAILED', `invalid pageUrlRegex: ${pageUrlRegex}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
let excludeRegex = null;
|
|
159
|
+
if (pageUrlNotRegex) {
|
|
160
|
+
try {
|
|
161
|
+
excludeRegex = new RegExp(pageUrlNotRegex);
|
|
162
|
+
} catch {
|
|
163
|
+
return asErrorPayload('OPERATION_FAILED', `invalid pageUrlNotRegex: ${pageUrlNotRegex}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const hasPageFilter = (
|
|
168
|
+
pageUrlIncludes.length > 0
|
|
169
|
+
|| pageUrlExcludes.length > 0
|
|
170
|
+
|| Boolean(includeRegex)
|
|
171
|
+
|| Boolean(excludeRegex)
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const shouldVerifyPage = (rawUrl) => {
|
|
175
|
+
const url = String(rawUrl || '').trim();
|
|
176
|
+
if (pageUrlIncludes.length > 0 && !pageUrlIncludes.some((part) => url.includes(part))) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
if (pageUrlExcludes.length > 0 && pageUrlExcludes.some((part) => url.includes(part))) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
if (includeRegex && !includeRegex.test(url)) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
if (excludeRegex && excludeRegex.test(url)) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
return true;
|
|
189
|
+
};
|
|
140
190
|
|
|
141
191
|
const collectForCurrentPage = async () => {
|
|
142
192
|
const snapshot = await getDomSnapshotByProfile(profileId);
|
|
@@ -156,6 +206,7 @@ async function executeVerifySubscriptions({ profileId, params }) {
|
|
|
156
206
|
|
|
157
207
|
let pagesResult = [];
|
|
158
208
|
let overallOk = true;
|
|
209
|
+
let matchedPageCount = 0;
|
|
159
210
|
if (!acrossPages) {
|
|
160
211
|
const current = await collectForCurrentPage();
|
|
161
212
|
overallOk = current.matches.every((item) => item.count >= item.minCount);
|
|
@@ -165,6 +216,16 @@ async function executeVerifySubscriptions({ profileId, params }) {
|
|
|
165
216
|
const { pages, activeIndex } = extractPageList(listed);
|
|
166
217
|
for (const page of pages) {
|
|
167
218
|
const pageIndex = Number(page.index);
|
|
219
|
+
const listedUrl = String(page.url || '');
|
|
220
|
+
if (!shouldVerifyPage(listedUrl)) {
|
|
221
|
+
pagesResult.push({
|
|
222
|
+
index: pageIndex,
|
|
223
|
+
url: listedUrl,
|
|
224
|
+
skipped: true,
|
|
225
|
+
ok: true,
|
|
226
|
+
});
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
168
229
|
if (Number.isFinite(activeIndex) && activeIndex !== pageIndex) {
|
|
169
230
|
await callAPI('page:switch', { profileId, index: pageIndex });
|
|
170
231
|
if (settleMs > 0) await new Promise((resolve) => setTimeout(resolve, settleMs));
|
|
@@ -173,15 +234,28 @@ async function executeVerifySubscriptions({ profileId, params }) {
|
|
|
173
234
|
const pageOk = current.matches.every((item) => item.count >= item.minCount);
|
|
174
235
|
overallOk = overallOk && pageOk;
|
|
175
236
|
pagesResult.push({ index: pageIndex, ...current, ok: pageOk });
|
|
237
|
+
matchedPageCount += 1;
|
|
176
238
|
}
|
|
177
239
|
if (Number.isFinite(activeIndex)) {
|
|
178
240
|
await callAPI('page:switch', { profileId, index: activeIndex });
|
|
179
241
|
}
|
|
180
242
|
}
|
|
181
243
|
|
|
244
|
+
if (acrossPages && hasPageFilter && requireMatchedPages && matchedPageCount === 0) {
|
|
245
|
+
return asErrorPayload('SUBSCRIPTION_MISMATCH', 'no page matched verify_subscriptions pageUrl filter', {
|
|
246
|
+
acrossPages,
|
|
247
|
+
pageUrlIncludes,
|
|
248
|
+
pageUrlExcludes,
|
|
249
|
+
pageUrlRegex: pageUrlRegex || null,
|
|
250
|
+
pageUrlNotRegex: pageUrlNotRegex || null,
|
|
251
|
+
pages: pagesResult,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
182
255
|
if (!overallOk) {
|
|
183
256
|
return asErrorPayload('SUBSCRIPTION_MISMATCH', 'subscription selectors missing on one or more pages', {
|
|
184
257
|
acrossPages,
|
|
258
|
+
matchedPageCount,
|
|
185
259
|
pages: pagesResult,
|
|
186
260
|
});
|
|
187
261
|
}
|
|
@@ -190,7 +264,7 @@ async function executeVerifySubscriptions({ profileId, params }) {
|
|
|
190
264
|
ok: true,
|
|
191
265
|
code: 'OPERATION_DONE',
|
|
192
266
|
message: 'verify_subscriptions done',
|
|
193
|
-
data: { acrossPages, pages: pagesResult },
|
|
267
|
+
data: { acrossPages, matchedPageCount, pages: pagesResult },
|
|
194
268
|
};
|
|
195
269
|
}
|
|
196
270
|
|
|
@@ -33,7 +33,20 @@ export function buildSelectorClickScript({ selector, highlight }) {
|
|
|
33
33
|
}
|
|
34
34
|
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
35
35
|
await new Promise((r) => setTimeout(r, 150));
|
|
36
|
-
el
|
|
36
|
+
if (el instanceof HTMLElement) {
|
|
37
|
+
try { el.focus({ preventScroll: true }); } catch {}
|
|
38
|
+
const common = { bubbles: true, cancelable: true, view: window };
|
|
39
|
+
try {
|
|
40
|
+
if (typeof PointerEvent === 'function') {
|
|
41
|
+
el.dispatchEvent(new PointerEvent('pointerdown', { ...common, pointerType: 'mouse', button: 0 }));
|
|
42
|
+
el.dispatchEvent(new PointerEvent('pointerup', { ...common, pointerType: 'mouse', button: 0 }));
|
|
43
|
+
}
|
|
44
|
+
} catch {}
|
|
45
|
+
try { el.dispatchEvent(new MouseEvent('mousedown', { ...common, button: 0 })); } catch {}
|
|
46
|
+
try { el.dispatchEvent(new MouseEvent('mouseup', { ...common, button: 0 })); } catch {}
|
|
47
|
+
}
|
|
48
|
+
if (typeof el.click === 'function') el.click();
|
|
49
|
+
else el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window, button: 0 }));
|
|
37
50
|
if (${highlightLiteral} && el instanceof HTMLElement) {
|
|
38
51
|
setTimeout(() => { el.style.outline = restoreOutline; }, 260);
|
|
39
52
|
}
|
|
@@ -56,9 +69,63 @@ export function buildSelectorTypeScript({ selector, highlight, text }) {
|
|
|
56
69
|
}
|
|
57
70
|
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
58
71
|
await new Promise((r) => setTimeout(r, 150));
|
|
59
|
-
el
|
|
60
|
-
|
|
61
|
-
|
|
72
|
+
if (el instanceof HTMLElement) {
|
|
73
|
+
try { el.focus({ preventScroll: true }); } catch {}
|
|
74
|
+
if (typeof el.click === 'function') el.click();
|
|
75
|
+
}
|
|
76
|
+
const value = ${textLiteral};
|
|
77
|
+
const fireInputEvent = (target, name, init) => {
|
|
78
|
+
try {
|
|
79
|
+
if (typeof InputEvent === 'function') {
|
|
80
|
+
target.dispatchEvent(new InputEvent(name, init));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
} catch {}
|
|
84
|
+
target.dispatchEvent(new Event(name, { bubbles: true, cancelable: init?.cancelable === true }));
|
|
85
|
+
};
|
|
86
|
+
const assignControlValue = (target, next) => {
|
|
87
|
+
if (target instanceof HTMLInputElement) {
|
|
88
|
+
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
|
|
89
|
+
if (setter) setter.call(target, next);
|
|
90
|
+
else target.value = next;
|
|
91
|
+
if (typeof target.setSelectionRange === 'function') {
|
|
92
|
+
const cursor = String(next).length;
|
|
93
|
+
try { target.setSelectionRange(cursor, cursor); } catch {}
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
if (target instanceof HTMLTextAreaElement) {
|
|
98
|
+
const setter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')?.set;
|
|
99
|
+
if (setter) setter.call(target, next);
|
|
100
|
+
else target.value = next;
|
|
101
|
+
if (typeof target.setSelectionRange === 'function') {
|
|
102
|
+
const cursor = String(next).length;
|
|
103
|
+
try { target.setSelectionRange(cursor, cursor); } catch {}
|
|
104
|
+
}
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
};
|
|
109
|
+
const editableAssigned = assignControlValue(el, value);
|
|
110
|
+
if (!editableAssigned) {
|
|
111
|
+
if (el instanceof HTMLElement && el.isContentEditable) {
|
|
112
|
+
el.textContent = value;
|
|
113
|
+
} else {
|
|
114
|
+
throw new Error('Element not editable: ' + ${selectorLiteral});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
fireInputEvent(el, 'beforeinput', {
|
|
118
|
+
bubbles: true,
|
|
119
|
+
cancelable: true,
|
|
120
|
+
data: value,
|
|
121
|
+
inputType: 'insertText',
|
|
122
|
+
});
|
|
123
|
+
fireInputEvent(el, 'input', {
|
|
124
|
+
bubbles: true,
|
|
125
|
+
cancelable: false,
|
|
126
|
+
data: value,
|
|
127
|
+
inputType: 'insertText',
|
|
128
|
+
});
|
|
62
129
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
63
130
|
if (${highlightLiteral} && el instanceof HTMLElement) {
|
|
64
131
|
setTimeout(() => { el.style.outline = restoreOutline; }, 260);
|