@web-auto/webauto 0.1.17 → 0.1.19
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/README.md +122 -53
- package/apps/desktop-console/dist/main/index.mjs +229 -14
- package/apps/desktop-console/dist/renderer/index.js +237 -8
- package/apps/desktop-console/entry/ui-cli.mjs +290 -21
- package/apps/desktop-console/entry/ui-console.mjs +46 -15
- package/apps/webauto/entry/account.mjs +126 -27
- package/apps/webauto/entry/lib/account-detect.mjs +399 -9
- package/apps/webauto/entry/lib/account-store.mjs +201 -109
- package/apps/webauto/entry/lib/iflow-reply.mjs +194 -0
- package/apps/webauto/entry/lib/profile-policy.mjs +48 -0
- package/apps/webauto/entry/lib/profilepool.mjs +12 -0
- package/apps/webauto/entry/lib/schedule-store.mjs +29 -2
- package/apps/webauto/entry/lib/session-init.mjs +227 -0
- package/apps/webauto/entry/lib/upgrade-check.mjs +269 -0
- package/apps/webauto/entry/lib/xhs-unified-blocks.mjs +160 -0
- package/apps/webauto/entry/lib/xhs-unified-output-blocks.mjs +83 -0
- package/apps/webauto/entry/lib/xhs-unified-plan-blocks.mjs +55 -0
- package/apps/webauto/entry/lib/xhs-unified-profile-blocks.mjs +542 -0
- package/apps/webauto/entry/lib/xhs-unified-runtime-blocks.mjs +436 -0
- package/apps/webauto/entry/profilepool.mjs +56 -9
- package/apps/webauto/entry/smart-reply-cli.mjs +267 -0
- package/apps/webauto/entry/weibo-unified.mjs +84 -11
- package/apps/webauto/entry/xhs-orchestrate.mjs +43 -1
- package/apps/webauto/entry/xhs-unified.mjs +92 -997
- package/bin/webauto.mjs +22 -4
- package/dist/modules/camo-backend/src/index.js +33 -0
- package/dist/modules/camo-backend/src/internal/BrowserSession.js +232 -49
- package/dist/modules/camo-backend/src/internal/engine-manager.js +14 -13
- package/dist/modules/camo-backend/src/internal/ws-server.js +16 -19
- package/dist/modules/camo-runtime/src/utils/browser-service.mjs +38 -6
- package/dist/modules/workflow/blocks/EnsureSession.js +0 -8
- package/dist/modules/workflow/blocks/WeiboCollectFromLinksBlock.js +78 -6
- package/dist/modules/workflow/blocks/WeiboCollectSearchLinksBlock.js +266 -192
- package/dist/modules/workflow/definitions/weibo-search-workflow-v1.js +2 -0
- package/dist/modules/workflow/src/runner.js +2 -0
- package/dist/modules/xiaohongshu/app/src/blocks/ReplyInteractBlock.js +150 -37
- package/dist/modules/xiaohongshu/app/src/blocks/SmartReplyBlock.js +491 -0
- package/modules/camo-backend/src/index.ts +31 -0
- package/modules/camo-backend/src/internal/BrowserSession.ts +224 -53
- package/modules/camo-backend/src/internal/engine-manager.ts +14 -15
- package/modules/camo-backend/src/internal/ws-server.ts +17 -17
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/common.mjs +12 -2
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/persistence.mjs +57 -0
- package/modules/camo-runtime/src/autoscript/action-providers/xhs.mjs +2475 -243
- package/modules/camo-runtime/src/autoscript/runtime.mjs +35 -30
- package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +80 -443
- package/modules/camo-runtime/src/container/runtime-core/checkpoint.mjs +39 -6
- package/modules/camo-runtime/src/container/runtime-core/operations/index.mjs +206 -39
- package/modules/camo-runtime/src/container/runtime-core/operations/tab-pool.mjs +0 -79
- package/modules/camo-runtime/src/container/runtime-core/operations/viewport.mjs +46 -0
- package/modules/camo-runtime/src/utils/browser-service.mjs +41 -6
- package/modules/camo-runtime/src/utils/js-policy.mjs +28 -0
- package/modules/workflow/blocks/EnsureSession.ts +0 -4
- package/modules/workflow/blocks/WeiboCollectFromLinksBlock.ts +81 -6
- package/modules/workflow/blocks/WeiboCollectSearchLinksBlock.ts +316 -0
- package/modules/workflow/definitions/weibo-search-workflow-v1.ts +2 -0
- package/modules/workflow/src/runner.ts +2 -0
- package/modules/xiaohongshu/app/src/blocks/ReplyInteractBlock.ts +198 -53
- package/modules/xiaohongshu/app/src/blocks/SmartReplyBlock.ts +706 -0
- package/package.json +2 -2
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/comments.mjs +0 -498
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/detail.mjs +0 -181
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +0 -691
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +0 -388
- package/modules/camo-runtime/src/container/runtime-core/operations/selector-scripts.mjs +0 -135
|
@@ -1,498 +0,0 @@
|
|
|
1
|
-
export function buildCommentsHarvestScript(params = {}) {
|
|
2
|
-
const maxRounds = Math.max(1, Number(params.maxRounds ?? params.maxScrollRounds ?? 14) || 14);
|
|
3
|
-
const scrollStepMin = Math.max(120, Number(params.scrollStepMin ?? params.scrollStep ?? 420) || 420);
|
|
4
|
-
const scrollStepMax = Math.max(scrollStepMin, Number(params.scrollStepMax ?? scrollStepMin) || scrollStepMin);
|
|
5
|
-
const scrollStepBase = Math.max(scrollStepMin, Math.floor((scrollStepMin + scrollStepMax) / 2));
|
|
6
|
-
const settleMinMs = Math.max(80, Number(params.settleMinMs ?? params.settleMs ?? 180) || 180);
|
|
7
|
-
const settleMaxMs = Math.max(settleMinMs, Number(params.settleMaxMs ?? settleMinMs) || settleMinMs);
|
|
8
|
-
const settleMs = settleMinMs;
|
|
9
|
-
const stallRounds = Math.max(1, Number(params.stallRounds ?? 2) || 2);
|
|
10
|
-
const requireBottom = params.requireBottom !== false;
|
|
11
|
-
const includeComments = params.includeComments !== false;
|
|
12
|
-
const commentsLimit = Math.max(0, Number(params.commentsLimit ?? 0) || 0);
|
|
13
|
-
const recoveryStuckRounds = Math.max(1, Number(params.recoveryStuckRounds ?? 2) || 2);
|
|
14
|
-
const recoveryUpRounds = Math.max(1, Number(params.recoveryUpRounds ?? 2) || 2);
|
|
15
|
-
const recoveryDownRounds = Math.max(1, Number(params.recoveryDownRounds ?? 3) || 3);
|
|
16
|
-
const maxRecoveries = Math.max(0, Number(params.maxRecoveries ?? 3) || 3);
|
|
17
|
-
const recoveryUpStep = Math.max(80, Number(params.recoveryUpStep ?? Math.floor(scrollStepBase * 0.75)) || Math.floor(scrollStepBase * 0.75));
|
|
18
|
-
const recoveryDownStep = Math.max(120, Number(params.recoveryDownStep ?? Math.floor(scrollStepBase * 1.3)) || Math.floor(scrollStepBase * 1.3));
|
|
19
|
-
const recoveryNoProgressRounds = Math.max(1, Number(params.recoveryNoProgressRounds ?? 3) || 3);
|
|
20
|
-
const progressDiffThreshold = Math.max(2, Number(
|
|
21
|
-
params.progressDiffThreshold ?? Math.max(12, Math.floor(scrollStepBase * 0.08)),
|
|
22
|
-
) || Math.max(12, Math.floor(scrollStepBase * 0.08)));
|
|
23
|
-
const recoveryDownBoostPerAttempt = Math.max(0, Number(params.recoveryDownBoostPerAttempt ?? 1) || 1);
|
|
24
|
-
const maxRecoveryDownBoost = Math.max(0, Number(params.maxRecoveryDownBoost ?? 2) || 2);
|
|
25
|
-
const adaptiveMaxRounds = params.adaptiveMaxRounds !== false;
|
|
26
|
-
const adaptiveExpectedPerRound = Math.max(1, Number(params.adaptiveExpectedPerRound ?? 6) || 6);
|
|
27
|
-
const adaptiveBufferRounds = Math.max(0, Number(params.adaptiveBufferRounds ?? 22) || 22);
|
|
28
|
-
const adaptiveMinBoostRounds = Math.max(0, Number(params.adaptiveMinBoostRounds ?? 36) || 36);
|
|
29
|
-
const adaptiveMaxRoundsCap = Math.max(maxRounds, Number(params.adaptiveMaxRoundsCap ?? 320) || 320);
|
|
30
|
-
|
|
31
|
-
return `(async () => {
|
|
32
|
-
const state = window.__camoXhsState || (window.__camoXhsState = {});
|
|
33
|
-
const metricsState = state.metrics && typeof state.metrics === 'object' ? state.metrics : {};
|
|
34
|
-
state.metrics = metricsState;
|
|
35
|
-
metricsState.searchCount = Number(metricsState.searchCount || 0);
|
|
36
|
-
const actionTrace = [];
|
|
37
|
-
const pushTrace = (payload) => {
|
|
38
|
-
actionTrace.push({
|
|
39
|
-
ts: new Date().toISOString(),
|
|
40
|
-
...payload,
|
|
41
|
-
});
|
|
42
|
-
};
|
|
43
|
-
const detailSelectors = [
|
|
44
|
-
'.note-detail-mask',
|
|
45
|
-
'.note-detail-page',
|
|
46
|
-
'.note-detail-dialog',
|
|
47
|
-
'.note-detail-mask .detail-container',
|
|
48
|
-
'.note-detail-mask .media-container',
|
|
49
|
-
'.note-detail-mask .note-scroller',
|
|
50
|
-
'.note-detail-mask .note-content',
|
|
51
|
-
'.note-detail-mask .interaction-container',
|
|
52
|
-
'.note-detail-mask .comments-container',
|
|
53
|
-
];
|
|
54
|
-
const isVisible = (node) => {
|
|
55
|
-
if (!node || !(node instanceof HTMLElement)) return false;
|
|
56
|
-
const style = window.getComputedStyle(node);
|
|
57
|
-
if (!style) return false;
|
|
58
|
-
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') === 0) return false;
|
|
59
|
-
const rect = node.getBoundingClientRect();
|
|
60
|
-
return rect.width > 1 && rect.height > 1;
|
|
61
|
-
};
|
|
62
|
-
const isDetailVisible = () => detailSelectors.some((selector) => isVisible(document.querySelector(selector)));
|
|
63
|
-
const parseCountToken = (raw) => {
|
|
64
|
-
const token = String(raw || '').trim();
|
|
65
|
-
const matched = token.match(/^([0-9]+(?:\\.[0-9]+)?)(万|w|W)?$/);
|
|
66
|
-
if (!matched) return null;
|
|
67
|
-
const base = Number(matched[1]);
|
|
68
|
-
if (!Number.isFinite(base)) return null;
|
|
69
|
-
if (!matched[2]) return Math.round(base);
|
|
70
|
-
return Math.round(base * 10000);
|
|
71
|
-
};
|
|
72
|
-
const readExpectedCommentsCount = () => {
|
|
73
|
-
const scopeSelectors = [
|
|
74
|
-
'.note-detail-mask .interaction-container',
|
|
75
|
-
'.note-detail-mask .comments-container',
|
|
76
|
-
'.note-detail-page .interaction-container',
|
|
77
|
-
'.note-detail-page .comments-container',
|
|
78
|
-
'.note-detail-mask',
|
|
79
|
-
'.note-detail-page',
|
|
80
|
-
];
|
|
81
|
-
const patterns = [
|
|
82
|
-
/([0-9]+(?:\\.[0-9]+)?(?:万|w|W)?)\\s*条?评论/,
|
|
83
|
-
/评论\\s*([0-9]+(?:\\.[0-9]+)?(?:万|w|W)?)/,
|
|
84
|
-
/共\\s*([0-9]+(?:\\.[0-9]+)?(?:万|w|W)?)\\s*条/,
|
|
85
|
-
];
|
|
86
|
-
for (const selector of scopeSelectors) {
|
|
87
|
-
const root = document.querySelector(selector);
|
|
88
|
-
if (!root) continue;
|
|
89
|
-
const text = String(root.textContent || '').replace(/\\s+/g, ' ').trim();
|
|
90
|
-
if (!text) continue;
|
|
91
|
-
for (const re of patterns) {
|
|
92
|
-
const matched = text.match(re);
|
|
93
|
-
if (!matched || !matched[1]) continue;
|
|
94
|
-
const parsed = parseCountToken(matched[1]);
|
|
95
|
-
if (Number.isFinite(parsed) && parsed >= 0) return parsed;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return null;
|
|
99
|
-
};
|
|
100
|
-
const normalizeInlineText = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
|
|
101
|
-
const sanitizeAuthorText = (raw, commentText = '') => {
|
|
102
|
-
const text = normalizeInlineText(raw);
|
|
103
|
-
if (!text) return '';
|
|
104
|
-
if (commentText && text === commentText) return '';
|
|
105
|
-
if (text.length > 40) return '';
|
|
106
|
-
if (/^(回复|展开|收起|查看更多|评论|赞|分享|发送)$/.test(text)) return '';
|
|
107
|
-
return text;
|
|
108
|
-
};
|
|
109
|
-
const readAuthor = (item, commentText = '') => {
|
|
110
|
-
const attrNames = ['data-user-name', 'data-username', 'data-user_nickname', 'data-nickname'];
|
|
111
|
-
for (const attr of attrNames) {
|
|
112
|
-
const value = sanitizeAuthorText(item.getAttribute?.(attr), commentText);
|
|
113
|
-
if (value) return value;
|
|
114
|
-
}
|
|
115
|
-
const selectors = [
|
|
116
|
-
'.comment-user .name',
|
|
117
|
-
'.comment-user .username',
|
|
118
|
-
'.comment-user .user-name',
|
|
119
|
-
'.author .name',
|
|
120
|
-
'.author',
|
|
121
|
-
'.user-name',
|
|
122
|
-
'.username',
|
|
123
|
-
'.name',
|
|
124
|
-
'a[href*="/user/profile/"]',
|
|
125
|
-
'a[href*="/user/"]',
|
|
126
|
-
];
|
|
127
|
-
for (const selector of selectors) {
|
|
128
|
-
const node = item.querySelector(selector);
|
|
129
|
-
if (!node) continue;
|
|
130
|
-
const title = sanitizeAuthorText(node.getAttribute?.('title'), commentText);
|
|
131
|
-
if (title) return title;
|
|
132
|
-
const text = sanitizeAuthorText(node.textContent, commentText);
|
|
133
|
-
if (text) return text;
|
|
134
|
-
}
|
|
135
|
-
return '';
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
const scroller = document.querySelector('.note-scroller')
|
|
139
|
-
|| document.querySelector('.comments-el')
|
|
140
|
-
|| document.querySelector('.comments-container')
|
|
141
|
-
|| document.scrollingElement
|
|
142
|
-
|| document.documentElement;
|
|
143
|
-
const readMetrics = () => {
|
|
144
|
-
const target = scroller || document.documentElement;
|
|
145
|
-
return {
|
|
146
|
-
scrollTop: Number(target?.scrollTop || 0),
|
|
147
|
-
scrollHeight: Number(target?.scrollHeight || 0),
|
|
148
|
-
clientHeight: Number(target?.clientHeight || window.innerHeight || 0),
|
|
149
|
-
};
|
|
150
|
-
};
|
|
151
|
-
const commentMap = new Map();
|
|
152
|
-
const collect = (round) => {
|
|
153
|
-
const nodes = Array.from(document.querySelectorAll('.comment-item, [class*="comment-item"]'));
|
|
154
|
-
for (const item of nodes) {
|
|
155
|
-
const textNode = item.querySelector('.content, .comment-content, p');
|
|
156
|
-
const text = String((textNode && textNode.textContent) || '').trim();
|
|
157
|
-
const author = readAuthor(item, text);
|
|
158
|
-
if (!text) continue;
|
|
159
|
-
const key = author + '::' + text;
|
|
160
|
-
if (commentMap.has(key)) continue;
|
|
161
|
-
const likeNode = item.querySelector('.like-wrapper, .comment-like, [class*="like"]');
|
|
162
|
-
commentMap.set(key, {
|
|
163
|
-
author,
|
|
164
|
-
text,
|
|
165
|
-
liked: Boolean(likeNode && /like-active/.test(String(likeNode.className || ''))),
|
|
166
|
-
firstSeenRound: round,
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
const configuredMaxRounds = Number(${maxRounds});
|
|
172
|
-
const scrollStepMin = Number(${scrollStepMin});
|
|
173
|
-
const scrollStepMax = Number(${scrollStepMax});
|
|
174
|
-
const settleMinMs = Number(${settleMinMs});
|
|
175
|
-
const settleMaxMs = Number(${settleMaxMs});
|
|
176
|
-
const settleMs = Number(${settleMs});
|
|
177
|
-
const stallRounds = Number(${stallRounds});
|
|
178
|
-
const requireBottom = ${requireBottom ? 'true' : 'false'};
|
|
179
|
-
const includeComments = ${includeComments ? 'true' : 'false'};
|
|
180
|
-
const commentsLimit = Number(${commentsLimit});
|
|
181
|
-
const recoveryStuckRounds = Number(${recoveryStuckRounds});
|
|
182
|
-
const recoveryUpRounds = Number(${recoveryUpRounds});
|
|
183
|
-
const recoveryDownRounds = Number(${recoveryDownRounds});
|
|
184
|
-
const maxRecoveries = Number(${maxRecoveries});
|
|
185
|
-
const recoveryUpStep = Number(${recoveryUpStep});
|
|
186
|
-
const recoveryDownStep = Number(${recoveryDownStep});
|
|
187
|
-
const recoveryNoProgressRounds = Number(${recoveryNoProgressRounds});
|
|
188
|
-
const progressDiffThreshold = Number(${progressDiffThreshold});
|
|
189
|
-
const recoveryDownBoostPerAttempt = Number(${recoveryDownBoostPerAttempt});
|
|
190
|
-
const maxRecoveryDownBoost = Number(${maxRecoveryDownBoost});
|
|
191
|
-
const adaptiveMaxRounds = ${adaptiveMaxRounds ? 'true' : 'false'};
|
|
192
|
-
const adaptiveExpectedPerRound = Number(${adaptiveExpectedPerRound});
|
|
193
|
-
const adaptiveBufferRounds = Number(${adaptiveBufferRounds});
|
|
194
|
-
const adaptiveMinBoostRounds = Number(${adaptiveMinBoostRounds});
|
|
195
|
-
const adaptiveMaxRoundsCap = Number(${adaptiveMaxRoundsCap});
|
|
196
|
-
const randomBetween = (min, max) => {
|
|
197
|
-
const lo = Math.max(0, Math.floor(Number(min) || 0));
|
|
198
|
-
const hi = Math.max(lo, Math.floor(Number(max) || 0));
|
|
199
|
-
if (hi <= lo) return lo;
|
|
200
|
-
return lo + Math.floor(Math.random() * (hi - lo + 1));
|
|
201
|
-
};
|
|
202
|
-
let maxRounds = configuredMaxRounds;
|
|
203
|
-
let maxRoundsSource = 'configured';
|
|
204
|
-
let budgetExpectedCommentsCount = null;
|
|
205
|
-
const applyAdaptiveRounds = (expectedCommentsCount) => {
|
|
206
|
-
const expected = Number(expectedCommentsCount);
|
|
207
|
-
if (!adaptiveMaxRounds || !Number.isFinite(expected) || expected <= 0) return false;
|
|
208
|
-
const estimatedRounds = Math.ceil(expected / adaptiveExpectedPerRound) + adaptiveBufferRounds;
|
|
209
|
-
if (estimatedRounds <= configuredMaxRounds) return false;
|
|
210
|
-
const boostedRounds = Math.max(configuredMaxRounds + adaptiveMinBoostRounds, estimatedRounds);
|
|
211
|
-
const nextRounds = Math.max(configuredMaxRounds, Math.min(adaptiveMaxRoundsCap, boostedRounds));
|
|
212
|
-
maxRounds = nextRounds;
|
|
213
|
-
maxRoundsSource = 'adaptive_expected_comments';
|
|
214
|
-
budgetExpectedCommentsCount = Math.round(expected);
|
|
215
|
-
return true;
|
|
216
|
-
};
|
|
217
|
-
applyAdaptiveRounds(readExpectedCommentsCount());
|
|
218
|
-
let rounds = 0;
|
|
219
|
-
let reachedBottom = false;
|
|
220
|
-
let exitReason = 'max_rounds_reached';
|
|
221
|
-
let noProgressRounds = 0;
|
|
222
|
-
let noNewCommentsStreak = 0;
|
|
223
|
-
let stalledScrollRounds = 0;
|
|
224
|
-
let noEffectStreak = 0;
|
|
225
|
-
let recoveries = 0;
|
|
226
|
-
let bestRemainingDiff = Number.POSITIVE_INFINITY;
|
|
227
|
-
const recoveryReasonCounts = {
|
|
228
|
-
no_effect: 0,
|
|
229
|
-
no_new_comments: 0,
|
|
230
|
-
};
|
|
231
|
-
const performScroll = async (deltaY, waitMs = settleMs, meta = {}) => {
|
|
232
|
-
const waitFloor = Math.max(settleMinMs, Math.floor(Number(waitMs) || settleMinMs));
|
|
233
|
-
const waitCeil = Math.max(waitFloor, Math.max(settleMaxMs, Math.floor(Number(waitMs) || settleMaxMs)));
|
|
234
|
-
const waitActual = randomBetween(waitFloor, waitCeil);
|
|
235
|
-
pushTrace({
|
|
236
|
-
kind: 'scroll',
|
|
237
|
-
stage: 'xhs_comments_harvest',
|
|
238
|
-
deltaY: Number(deltaY),
|
|
239
|
-
waitMs: waitActual,
|
|
240
|
-
...meta,
|
|
241
|
-
});
|
|
242
|
-
if (typeof scroller?.scrollBy === 'function') {
|
|
243
|
-
scroller.scrollBy({ top: deltaY, behavior: 'auto' });
|
|
244
|
-
} else {
|
|
245
|
-
window.scrollBy({ top: deltaY, behavior: 'auto' });
|
|
246
|
-
}
|
|
247
|
-
await new Promise((resolve) => setTimeout(resolve, waitActual));
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
for (let round = 1; round <= maxRounds; round += 1) {
|
|
251
|
-
rounds = round;
|
|
252
|
-
if (!isDetailVisible()) {
|
|
253
|
-
exitReason = 'detail_hidden';
|
|
254
|
-
break;
|
|
255
|
-
}
|
|
256
|
-
const beforeCount = commentMap.size;
|
|
257
|
-
collect(round);
|
|
258
|
-
if ((budgetExpectedCommentsCount === null || budgetExpectedCommentsCount === undefined) && round <= 6) {
|
|
259
|
-
applyAdaptiveRounds(readExpectedCommentsCount());
|
|
260
|
-
}
|
|
261
|
-
const beforeMetrics = readMetrics();
|
|
262
|
-
const beforeDiff = beforeMetrics.scrollHeight - (beforeMetrics.scrollTop + beforeMetrics.clientHeight);
|
|
263
|
-
if (Number.isFinite(beforeDiff) && beforeDiff >= 0) {
|
|
264
|
-
bestRemainingDiff = Math.min(bestRemainingDiff, beforeDiff);
|
|
265
|
-
}
|
|
266
|
-
if (beforeDiff <= 6) {
|
|
267
|
-
reachedBottom = true;
|
|
268
|
-
exitReason = 'bottom_reached';
|
|
269
|
-
break;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const prevTop = beforeMetrics.scrollTop;
|
|
273
|
-
const roundScrollStep = randomBetween(scrollStepMin, scrollStepMax);
|
|
274
|
-
await performScroll(roundScrollStep, settleMs, {
|
|
275
|
-
round,
|
|
276
|
-
reason: 'main_scroll',
|
|
277
|
-
});
|
|
278
|
-
collect(round);
|
|
279
|
-
let afterMetrics = readMetrics();
|
|
280
|
-
let moved = Math.abs(afterMetrics.scrollTop - prevTop) > 1;
|
|
281
|
-
if (!moved && typeof window.scrollBy === 'function') {
|
|
282
|
-
const fallbackStep = Math.max(120, Math.floor(roundScrollStep / 2));
|
|
283
|
-
const fallbackWaitMs = randomBetween(settleMinMs, settleMaxMs);
|
|
284
|
-
pushTrace({
|
|
285
|
-
kind: 'scroll',
|
|
286
|
-
stage: 'xhs_comments_harvest',
|
|
287
|
-
round,
|
|
288
|
-
reason: 'fallback_scroll',
|
|
289
|
-
deltaY: Number(fallbackStep),
|
|
290
|
-
waitMs: fallbackWaitMs,
|
|
291
|
-
});
|
|
292
|
-
window.scrollBy({ top: fallbackStep, behavior: 'auto' });
|
|
293
|
-
await new Promise((resolve) => setTimeout(resolve, fallbackWaitMs));
|
|
294
|
-
collect(round);
|
|
295
|
-
afterMetrics = readMetrics();
|
|
296
|
-
moved = Math.abs(afterMetrics.scrollTop - prevTop) > 1;
|
|
297
|
-
}
|
|
298
|
-
const increased = commentMap.size > beforeCount;
|
|
299
|
-
const afterDiff = afterMetrics.scrollHeight - (afterMetrics.scrollTop + afterMetrics.clientHeight);
|
|
300
|
-
const diffImproved = Number.isFinite(afterDiff) && Number.isFinite(beforeDiff)
|
|
301
|
-
? afterDiff <= (beforeDiff - progressDiffThreshold)
|
|
302
|
-
: false;
|
|
303
|
-
const bestImproved = Number.isFinite(afterDiff) && Number.isFinite(bestRemainingDiff)
|
|
304
|
-
? afterDiff <= (bestRemainingDiff - progressDiffThreshold)
|
|
305
|
-
: false;
|
|
306
|
-
if (Number.isFinite(afterDiff) && afterDiff >= 0) {
|
|
307
|
-
bestRemainingDiff = Math.min(bestRemainingDiff, afterDiff);
|
|
308
|
-
}
|
|
309
|
-
const progressedByScroll = diffImproved || bestImproved;
|
|
310
|
-
const progressed = increased || progressedByScroll;
|
|
311
|
-
if (!progressed) {
|
|
312
|
-
noProgressRounds += 1;
|
|
313
|
-
noNewCommentsStreak += 1;
|
|
314
|
-
} else {
|
|
315
|
-
noProgressRounds = 0;
|
|
316
|
-
noNewCommentsStreak = 0;
|
|
317
|
-
}
|
|
318
|
-
if (!moved) stalledScrollRounds += 1;
|
|
319
|
-
else stalledScrollRounds = 0;
|
|
320
|
-
if (!moved) noEffectStreak += 1;
|
|
321
|
-
else noEffectStreak = 0;
|
|
322
|
-
if (afterDiff <= 6) {
|
|
323
|
-
reachedBottom = true;
|
|
324
|
-
exitReason = 'bottom_reached';
|
|
325
|
-
break;
|
|
326
|
-
}
|
|
327
|
-
let recoveryTrigger = null;
|
|
328
|
-
if (noNewCommentsStreak >= recoveryNoProgressRounds) recoveryTrigger = 'no_new_comments';
|
|
329
|
-
if (!recoveryTrigger && noEffectStreak >= recoveryStuckRounds) recoveryTrigger = 'no_effect';
|
|
330
|
-
if (recoveryTrigger) {
|
|
331
|
-
if (recoveries >= maxRecoveries) {
|
|
332
|
-
exitReason = recoveryTrigger === 'no_new_comments'
|
|
333
|
-
? 'no_new_comments_after_recovery_budget'
|
|
334
|
-
: 'scroll_stalled_after_recovery';
|
|
335
|
-
break;
|
|
336
|
-
}
|
|
337
|
-
recoveries += 1;
|
|
338
|
-
recoveryReasonCounts[recoveryTrigger] += 1;
|
|
339
|
-
for (let i = 0; i < recoveryUpRounds; i += 1) {
|
|
340
|
-
await performScroll(-recoveryUpStep, settleMs + 120, {
|
|
341
|
-
round,
|
|
342
|
-
reason: 'recovery_up',
|
|
343
|
-
recoveryTrigger,
|
|
344
|
-
recoveryStep: i + 1,
|
|
345
|
-
});
|
|
346
|
-
collect(round);
|
|
347
|
-
}
|
|
348
|
-
const downBoost = Math.min(maxRecoveryDownBoost, Math.max(0, recoveries - 1) * recoveryDownBoostPerAttempt);
|
|
349
|
-
const downRounds = recoveryDownRounds + downBoost;
|
|
350
|
-
for (let i = 0; i < downRounds; i += 1) {
|
|
351
|
-
await performScroll(recoveryDownStep, settleMs + 180, {
|
|
352
|
-
round,
|
|
353
|
-
reason: 'recovery_down',
|
|
354
|
-
recoveryTrigger,
|
|
355
|
-
recoveryStep: i + 1,
|
|
356
|
-
});
|
|
357
|
-
collect(round);
|
|
358
|
-
}
|
|
359
|
-
const recoveredMetrics = readMetrics();
|
|
360
|
-
const recoveredDiff = recoveredMetrics.scrollHeight - (recoveredMetrics.scrollTop + recoveredMetrics.clientHeight);
|
|
361
|
-
const recoveredDiffImproved = Number.isFinite(recoveredDiff) && Number.isFinite(afterDiff)
|
|
362
|
-
? recoveredDiff <= (afterDiff - progressDiffThreshold)
|
|
363
|
-
: false;
|
|
364
|
-
const recoveredBestImproved = Number.isFinite(recoveredDiff) && Number.isFinite(bestRemainingDiff)
|
|
365
|
-
? recoveredDiff <= (bestRemainingDiff - progressDiffThreshold)
|
|
366
|
-
: false;
|
|
367
|
-
if (Number.isFinite(recoveredDiff) && recoveredDiff >= 0) {
|
|
368
|
-
bestRemainingDiff = Math.min(bestRemainingDiff, recoveredDiff);
|
|
369
|
-
}
|
|
370
|
-
if (recoveredDiff <= 6) {
|
|
371
|
-
reachedBottom = true;
|
|
372
|
-
exitReason = 'bottom_reached';
|
|
373
|
-
break;
|
|
374
|
-
}
|
|
375
|
-
if (commentMap.size > beforeCount || recoveredDiffImproved || recoveredBestImproved) {
|
|
376
|
-
noProgressRounds = 0;
|
|
377
|
-
noNewCommentsStreak = 0;
|
|
378
|
-
}
|
|
379
|
-
noEffectStreak = 0;
|
|
380
|
-
stalledScrollRounds = 0;
|
|
381
|
-
continue;
|
|
382
|
-
}
|
|
383
|
-
if (stalledScrollRounds >= stallRounds) {
|
|
384
|
-
exitReason = 'scroll_stalled';
|
|
385
|
-
break;
|
|
386
|
-
}
|
|
387
|
-
if (noProgressRounds >= stallRounds) {
|
|
388
|
-
if (!requireBottom) {
|
|
389
|
-
exitReason = 'no_new_comments';
|
|
390
|
-
break;
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
if (round === maxRounds) {
|
|
394
|
-
exitReason = 'max_rounds_reached';
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
const comments = Array.from(commentMap.values())
|
|
399
|
-
.sort((a, b) => Number(a.firstSeenRound || 0) - Number(b.firstSeenRound || 0))
|
|
400
|
-
.map((item, index) => ({
|
|
401
|
-
index,
|
|
402
|
-
author: item.author,
|
|
403
|
-
text: item.text,
|
|
404
|
-
liked: item.liked,
|
|
405
|
-
}));
|
|
406
|
-
const metrics = readMetrics();
|
|
407
|
-
const detectedExpectedCommentsCount = readExpectedCommentsCount();
|
|
408
|
-
const expectedCommentsCount = Number.isFinite(Number(detectedExpectedCommentsCount))
|
|
409
|
-
? Number(detectedExpectedCommentsCount)
|
|
410
|
-
: (Number.isFinite(Number(budgetExpectedCommentsCount)) ? Number(budgetExpectedCommentsCount) : null);
|
|
411
|
-
const commentCoverageRate = Number.isFinite(Number(expectedCommentsCount)) && Number(expectedCommentsCount) > 0
|
|
412
|
-
? Number(Math.min(1, comments.length / Number(expectedCommentsCount)).toFixed(4))
|
|
413
|
-
: null;
|
|
414
|
-
|
|
415
|
-
state.currentComments = comments;
|
|
416
|
-
state.commentsCollectedAt = new Date().toISOString();
|
|
417
|
-
state.lastCommentsHarvest = {
|
|
418
|
-
noteId: state.currentNoteId || null,
|
|
419
|
-
searchCount: Number(metricsState.searchCount || 0),
|
|
420
|
-
collected: comments.length,
|
|
421
|
-
expectedCommentsCount,
|
|
422
|
-
commentCoverageRate,
|
|
423
|
-
recoveries,
|
|
424
|
-
recoveryReasonCounts,
|
|
425
|
-
maxRecoveries,
|
|
426
|
-
recoveryNoProgressRounds,
|
|
427
|
-
reachedBottom,
|
|
428
|
-
exitReason,
|
|
429
|
-
rounds,
|
|
430
|
-
configuredMaxRounds,
|
|
431
|
-
maxRounds,
|
|
432
|
-
maxRoundsSource,
|
|
433
|
-
budgetExpectedCommentsCount,
|
|
434
|
-
scroll: metrics,
|
|
435
|
-
at: state.commentsCollectedAt,
|
|
436
|
-
};
|
|
437
|
-
const payload = {
|
|
438
|
-
noteId: state.currentNoteId || null,
|
|
439
|
-
searchCount: Number(metricsState.searchCount || 0),
|
|
440
|
-
collected: comments.length,
|
|
441
|
-
expectedCommentsCount,
|
|
442
|
-
commentCoverageRate,
|
|
443
|
-
recoveries,
|
|
444
|
-
recoveryReasonCounts,
|
|
445
|
-
maxRecoveries,
|
|
446
|
-
recoveryNoProgressRounds,
|
|
447
|
-
firstComment: comments[0] || null,
|
|
448
|
-
reachedBottom,
|
|
449
|
-
exitReason,
|
|
450
|
-
rounds,
|
|
451
|
-
configuredMaxRounds,
|
|
452
|
-
maxRounds,
|
|
453
|
-
maxRoundsSource,
|
|
454
|
-
budgetExpectedCommentsCount,
|
|
455
|
-
scroll: metrics,
|
|
456
|
-
};
|
|
457
|
-
payload.actionTrace = actionTrace;
|
|
458
|
-
if (includeComments) {
|
|
459
|
-
const bounded = commentsLimit > 0 ? comments.slice(0, commentsLimit) : comments;
|
|
460
|
-
payload.comments = bounded;
|
|
461
|
-
payload.commentsTruncated = commentsLimit > 0 && comments.length > commentsLimit;
|
|
462
|
-
}
|
|
463
|
-
return payload;
|
|
464
|
-
})()`;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
export function buildCommentMatchScript(params = {}) {
|
|
468
|
-
const keywords = (Array.isArray(params.keywords || params.matchKeywords)
|
|
469
|
-
? (params.keywords || params.matchKeywords)
|
|
470
|
-
: String(params.keywords || params.matchKeywords || '').split(','))
|
|
471
|
-
.map((item) => String(item))
|
|
472
|
-
.filter(Boolean);
|
|
473
|
-
const mode = String(params.mode || params.matchMode || 'any');
|
|
474
|
-
const minHits = Math.max(1, Number(params.minHits ?? params.matchMinHits ?? 1) || 1);
|
|
475
|
-
|
|
476
|
-
return `(async () => {
|
|
477
|
-
const state = window.__camoXhsState || (window.__camoXhsState = {});
|
|
478
|
-
const rows = Array.isArray(state.currentComments) ? state.currentComments : [];
|
|
479
|
-
const keywords = ${JSON.stringify(keywords)};
|
|
480
|
-
const mode = ${JSON.stringify(mode)};
|
|
481
|
-
const minHits = Number(${minHits});
|
|
482
|
-
const normalize = (value) => String(value || '').toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
483
|
-
const tokens = keywords.map((item) => normalize(item)).filter(Boolean);
|
|
484
|
-
const matches = [];
|
|
485
|
-
for (const row of rows) {
|
|
486
|
-
const text = normalize(row.text);
|
|
487
|
-
if (!text || tokens.length === 0) continue;
|
|
488
|
-
const hits = tokens.filter((token) => text.includes(token));
|
|
489
|
-
if (mode === 'all' && hits.length < tokens.length) continue;
|
|
490
|
-
if (mode === 'atLeast' && hits.length < Math.max(1, minHits)) continue;
|
|
491
|
-
if (mode !== 'all' && mode !== 'atLeast' && hits.length === 0) continue;
|
|
492
|
-
matches.push({ index: row.index, hits });
|
|
493
|
-
}
|
|
494
|
-
state.matchedComments = matches;
|
|
495
|
-
state.matchRule = { tokens, mode, minHits };
|
|
496
|
-
return { matchCount: matches.length, mode, minHits: Math.max(1, minHits) };
|
|
497
|
-
})()`;
|
|
498
|
-
}
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
export function buildDetailHarvestScript() {
|
|
2
|
-
return `(async () => {
|
|
3
|
-
const state = window.__camoXhsState || (window.__camoXhsState = {});
|
|
4
|
-
const scroller = document.querySelector('.note-scroller')
|
|
5
|
-
|| document.querySelector('.comments-el')
|
|
6
|
-
|| document.scrollingElement
|
|
7
|
-
|| document.documentElement;
|
|
8
|
-
for (let i = 0; i < 3; i += 1) {
|
|
9
|
-
scroller.scrollBy({ top: 360, behavior: 'auto' });
|
|
10
|
-
await new Promise((resolve) => setTimeout(resolve, 120));
|
|
11
|
-
}
|
|
12
|
-
const title = (document.querySelector('.note-title') || {}).textContent || '';
|
|
13
|
-
const content = (document.querySelector('.note-content') || {}).textContent || '';
|
|
14
|
-
state.lastDetail = {
|
|
15
|
-
title: String(title).trim().slice(0, 200),
|
|
16
|
-
contentLength: String(content).trim().length,
|
|
17
|
-
capturedAt: new Date().toISOString(),
|
|
18
|
-
};
|
|
19
|
-
return { harvested: true, detail: state.lastDetail };
|
|
20
|
-
})()`;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function buildExpandRepliesScript() {
|
|
24
|
-
return `(async () => {
|
|
25
|
-
const buttons = Array.from(document.querySelectorAll([
|
|
26
|
-
'.note-detail-mask .show-more',
|
|
27
|
-
'.note-detail-mask .reply-expand',
|
|
28
|
-
'.note-detail-mask [class*="expand"]',
|
|
29
|
-
'.note-detail-page .show-more',
|
|
30
|
-
'.note-detail-page .reply-expand',
|
|
31
|
-
'.note-detail-page [class*="expand"]',
|
|
32
|
-
].join(',')));
|
|
33
|
-
let clicked = 0;
|
|
34
|
-
for (const button of buttons.slice(0, 8)) {
|
|
35
|
-
if (!(button instanceof HTMLElement)) continue;
|
|
36
|
-
const text = (button.textContent || '').trim();
|
|
37
|
-
if (!text) continue;
|
|
38
|
-
button.scrollIntoView({ behavior: 'auto', block: 'center' });
|
|
39
|
-
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
40
|
-
button.click();
|
|
41
|
-
clicked += 1;
|
|
42
|
-
await new Promise((resolve) => setTimeout(resolve, 120));
|
|
43
|
-
}
|
|
44
|
-
return { expanded: clicked, scanned: buttons.length };
|
|
45
|
-
})()`;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function buildCloseDetailScript() {
|
|
49
|
-
return `(async () => {
|
|
50
|
-
const state = window.__camoXhsState || (window.__camoXhsState = {});
|
|
51
|
-
const metrics = state.metrics && typeof state.metrics === 'object' ? state.metrics : {};
|
|
52
|
-
state.metrics = metrics;
|
|
53
|
-
metrics.searchCount = Number(metrics.searchCount || 0);
|
|
54
|
-
metrics.rollbackCount = Number(metrics.rollbackCount || 0);
|
|
55
|
-
metrics.returnToSearchCount = Number(metrics.returnToSearchCount || 0);
|
|
56
|
-
const harvest = state.lastCommentsHarvest && typeof state.lastCommentsHarvest === 'object'
|
|
57
|
-
? state.lastCommentsHarvest
|
|
58
|
-
: null;
|
|
59
|
-
const exitMeta = {
|
|
60
|
-
pageExitReason: String(harvest?.exitReason || 'close_without_harvest').trim(),
|
|
61
|
-
reachedBottom: typeof harvest?.reachedBottom === 'boolean' ? harvest.reachedBottom : null,
|
|
62
|
-
commentsCollected: Number.isFinite(Number(harvest?.collected)) ? Number(harvest.collected) : null,
|
|
63
|
-
expectedCommentsCount: Number.isFinite(Number(harvest?.expectedCommentsCount)) ? Number(harvest.expectedCommentsCount) : null,
|
|
64
|
-
commentCoverageRate: Number.isFinite(Number(harvest?.commentCoverageRate)) ? Number(harvest.commentCoverageRate) : null,
|
|
65
|
-
scrollRecoveries: Number.isFinite(Number(harvest?.recoveries)) ? Number(harvest.recoveries) : 0,
|
|
66
|
-
harvestRounds: Number.isFinite(Number(harvest?.rounds)) ? Number(harvest.rounds) : null,
|
|
67
|
-
};
|
|
68
|
-
const detailSelectors = [
|
|
69
|
-
'.note-detail-mask',
|
|
70
|
-
'.note-detail-page',
|
|
71
|
-
'.note-detail-dialog',
|
|
72
|
-
'.note-detail-mask .detail-container',
|
|
73
|
-
'.note-detail-mask .media-container',
|
|
74
|
-
'.note-detail-mask .note-scroller',
|
|
75
|
-
'.note-detail-mask .note-content',
|
|
76
|
-
'.note-detail-mask .interaction-container',
|
|
77
|
-
'.note-detail-mask .comments-container',
|
|
78
|
-
];
|
|
79
|
-
const searchSelectors = ['.note-item', '.search-result-list', '#search-input', '.feeds-page'];
|
|
80
|
-
const hasVisible = (selectors) => selectors.some((selector) => {
|
|
81
|
-
const node = document.querySelector(selector);
|
|
82
|
-
if (!node || !(node instanceof HTMLElement)) return false;
|
|
83
|
-
const style = window.getComputedStyle(node);
|
|
84
|
-
if (!style) return false;
|
|
85
|
-
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') === 0) return false;
|
|
86
|
-
const rect = node.getBoundingClientRect();
|
|
87
|
-
return rect.width > 1 && rect.height > 1;
|
|
88
|
-
});
|
|
89
|
-
const isDetailVisible = () => hasVisible(detailSelectors);
|
|
90
|
-
const isSearchVisible = () => hasVisible(searchSelectors);
|
|
91
|
-
const dispatchEscape = () => {
|
|
92
|
-
const target = document.activeElement || document.body || document.documentElement;
|
|
93
|
-
const opts = { key: 'Escape', code: 'Escape', keyCode: 27, which: 27, bubbles: true, cancelable: true };
|
|
94
|
-
target.dispatchEvent(new KeyboardEvent('keydown', opts));
|
|
95
|
-
target.dispatchEvent(new KeyboardEvent('keyup', opts));
|
|
96
|
-
document.dispatchEvent(new KeyboardEvent('keydown', opts));
|
|
97
|
-
document.dispatchEvent(new KeyboardEvent('keyup', opts));
|
|
98
|
-
};
|
|
99
|
-
const waitForCloseAnimation = async () => {
|
|
100
|
-
for (let i = 0; i < 45; i += 1) {
|
|
101
|
-
if (!isDetailVisible() && isSearchVisible()) return true;
|
|
102
|
-
await new Promise((resolve) => setTimeout(resolve, 120));
|
|
103
|
-
}
|
|
104
|
-
return !isDetailVisible() && isSearchVisible();
|
|
105
|
-
};
|
|
106
|
-
const buildCounterMeta = (returnedToSearch) => ({
|
|
107
|
-
searchCount: Number(metrics.searchCount || 0),
|
|
108
|
-
rollbackCount: Number(metrics.rollbackCount || 0),
|
|
109
|
-
returnToSearchCount: Number(metrics.returnToSearchCount || 0),
|
|
110
|
-
returnedToSearch: Boolean(returnedToSearch),
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
if (!isDetailVisible()) {
|
|
114
|
-
return {
|
|
115
|
-
closed: true,
|
|
116
|
-
via: 'already_closed',
|
|
117
|
-
searchVisible: isSearchVisible(),
|
|
118
|
-
...buildCounterMeta(false),
|
|
119
|
-
...exitMeta,
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
metrics.rollbackCount += 1;
|
|
124
|
-
metrics.lastRollbackAt = new Date().toISOString();
|
|
125
|
-
|
|
126
|
-
for (let attempt = 1; attempt <= 4; attempt += 1) {
|
|
127
|
-
dispatchEscape();
|
|
128
|
-
await new Promise((resolve) => setTimeout(resolve, 220));
|
|
129
|
-
if (await waitForCloseAnimation()) {
|
|
130
|
-
const searchVisible = isSearchVisible();
|
|
131
|
-
if (searchVisible) {
|
|
132
|
-
metrics.returnToSearchCount += 1;
|
|
133
|
-
metrics.lastReturnToSearchAt = new Date().toISOString();
|
|
134
|
-
}
|
|
135
|
-
return {
|
|
136
|
-
closed: true,
|
|
137
|
-
via: 'escape',
|
|
138
|
-
attempts: attempt,
|
|
139
|
-
searchVisible,
|
|
140
|
-
...buildCounterMeta(searchVisible),
|
|
141
|
-
...exitMeta,
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const selectors = ['.note-detail-mask .close-box', '.note-detail-mask .close-circle', '.close-box', '.close-circle'];
|
|
147
|
-
for (const selector of selectors) {
|
|
148
|
-
const target = document.querySelector(selector);
|
|
149
|
-
if (!target || !(target instanceof HTMLElement)) continue;
|
|
150
|
-
target.scrollIntoView({ behavior: 'auto', block: 'center' });
|
|
151
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
152
|
-
target.click();
|
|
153
|
-
await new Promise((resolve) => setTimeout(resolve, 220));
|
|
154
|
-
if (await waitForCloseAnimation()) {
|
|
155
|
-
const searchVisible = isSearchVisible();
|
|
156
|
-
if (searchVisible) {
|
|
157
|
-
metrics.returnToSearchCount += 1;
|
|
158
|
-
metrics.lastReturnToSearchAt = new Date().toISOString();
|
|
159
|
-
}
|
|
160
|
-
return {
|
|
161
|
-
closed: true,
|
|
162
|
-
via: selector,
|
|
163
|
-
attempts: 5,
|
|
164
|
-
searchVisible,
|
|
165
|
-
...buildCounterMeta(searchVisible),
|
|
166
|
-
...exitMeta,
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return {
|
|
172
|
-
closed: false,
|
|
173
|
-
via: 'escape_failed',
|
|
174
|
-
detailVisible: isDetailVisible(),
|
|
175
|
-
searchVisible: isSearchVisible(),
|
|
176
|
-
...buildCounterMeta(false),
|
|
177
|
-
...exitMeta,
|
|
178
|
-
};
|
|
179
|
-
})()`;
|
|
180
|
-
}
|
|
181
|
-
|