@web-auto/webauto 0.1.13 → 0.1.15
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 +137 -0
- package/apps/desktop-console/dist/main/index.mjs +93 -161
- package/apps/desktop-console/dist/renderer/index.js +2 -7
- package/apps/desktop-console/entry/ui-console.mjs +89 -8
- package/apps/desktop-console/package.json +24 -0
- package/apps/webauto/entry/flow-gate.mjs +139 -0
- package/apps/webauto/entry/lib/camo-cli.mjs +10 -3
- package/apps/webauto/entry/lib/flow-gate.mjs +466 -0
- package/apps/webauto/entry/xhs-install.mjs +3 -11
- package/apps/webauto/entry/xhs-status.mjs +3 -1
- package/apps/webauto/entry/xhs-unified.mjs +111 -5
- package/bin/webauto.mjs +89 -49
- package/dist/modules/camo-runtime/src/utils/browser-service.mjs +64 -44
- package/modules/camo-runtime/src/autoscript/action-providers/index.mjs +14 -3
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/comments.mjs +69 -19
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +56 -4
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +130 -21
- package/modules/camo-runtime/src/autoscript/action-providers/xhs.mjs +90 -14
- package/modules/camo-runtime/src/autoscript/runtime.mjs +14 -0
- package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +69 -8
- package/modules/camo-runtime/src/utils/browser-service.mjs +67 -45
- package/package.json +4 -3
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
export function buildCommentsHarvestScript(params = {}) {
|
|
2
2
|
const maxRounds = Math.max(1, Number(params.maxRounds ?? params.maxScrollRounds ?? 14) || 14);
|
|
3
|
-
const
|
|
4
|
-
const
|
|
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;
|
|
5
9
|
const stallRounds = Math.max(1, Number(params.stallRounds ?? 2) || 2);
|
|
6
10
|
const requireBottom = params.requireBottom !== false;
|
|
7
11
|
const includeComments = params.includeComments !== false;
|
|
@@ -10,12 +14,12 @@ export function buildCommentsHarvestScript(params = {}) {
|
|
|
10
14
|
const recoveryUpRounds = Math.max(1, Number(params.recoveryUpRounds ?? 2) || 2);
|
|
11
15
|
const recoveryDownRounds = Math.max(1, Number(params.recoveryDownRounds ?? 3) || 3);
|
|
12
16
|
const maxRecoveries = Math.max(0, Number(params.maxRecoveries ?? 3) || 3);
|
|
13
|
-
const recoveryUpStep = Math.max(80, Number(params.recoveryUpStep ?? Math.floor(
|
|
14
|
-
const recoveryDownStep = Math.max(120, Number(params.recoveryDownStep ?? Math.floor(
|
|
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));
|
|
15
19
|
const recoveryNoProgressRounds = Math.max(1, Number(params.recoveryNoProgressRounds ?? 3) || 3);
|
|
16
20
|
const progressDiffThreshold = Math.max(2, Number(
|
|
17
|
-
params.progressDiffThreshold ?? Math.max(12, Math.floor(
|
|
18
|
-
) || Math.max(12, Math.floor(
|
|
21
|
+
params.progressDiffThreshold ?? Math.max(12, Math.floor(scrollStepBase * 0.08)),
|
|
22
|
+
) || Math.max(12, Math.floor(scrollStepBase * 0.08)));
|
|
19
23
|
const recoveryDownBoostPerAttempt = Math.max(0, Number(params.recoveryDownBoostPerAttempt ?? 1) || 1);
|
|
20
24
|
const maxRecoveryDownBoost = Math.max(0, Number(params.maxRecoveryDownBoost ?? 2) || 2);
|
|
21
25
|
const adaptiveMaxRounds = params.adaptiveMaxRounds !== false;
|
|
@@ -29,6 +33,13 @@ export function buildCommentsHarvestScript(params = {}) {
|
|
|
29
33
|
const metricsState = state.metrics && typeof state.metrics === 'object' ? state.metrics : {};
|
|
30
34
|
state.metrics = metricsState;
|
|
31
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
|
+
};
|
|
32
43
|
const detailSelectors = [
|
|
33
44
|
'.note-detail-mask',
|
|
34
45
|
'.note-detail-page',
|
|
@@ -158,7 +169,10 @@ export function buildCommentsHarvestScript(params = {}) {
|
|
|
158
169
|
};
|
|
159
170
|
|
|
160
171
|
const configuredMaxRounds = Number(${maxRounds});
|
|
161
|
-
const
|
|
172
|
+
const scrollStepMin = Number(${scrollStepMin});
|
|
173
|
+
const scrollStepMax = Number(${scrollStepMax});
|
|
174
|
+
const settleMinMs = Number(${settleMinMs});
|
|
175
|
+
const settleMaxMs = Number(${settleMaxMs});
|
|
162
176
|
const settleMs = Number(${settleMs});
|
|
163
177
|
const stallRounds = Number(${stallRounds});
|
|
164
178
|
const requireBottom = ${requireBottom ? 'true' : 'false'};
|
|
@@ -179,6 +193,12 @@ export function buildCommentsHarvestScript(params = {}) {
|
|
|
179
193
|
const adaptiveBufferRounds = Number(${adaptiveBufferRounds});
|
|
180
194
|
const adaptiveMinBoostRounds = Number(${adaptiveMinBoostRounds});
|
|
181
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
|
+
};
|
|
182
202
|
let maxRounds = configuredMaxRounds;
|
|
183
203
|
let maxRoundsSource = 'configured';
|
|
184
204
|
let budgetExpectedCommentsCount = null;
|
|
@@ -208,13 +228,23 @@ export function buildCommentsHarvestScript(params = {}) {
|
|
|
208
228
|
no_effect: 0,
|
|
209
229
|
no_new_comments: 0,
|
|
210
230
|
};
|
|
211
|
-
const performScroll = async (deltaY, waitMs = settleMs) => {
|
|
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
|
+
});
|
|
212
242
|
if (typeof scroller?.scrollBy === 'function') {
|
|
213
243
|
scroller.scrollBy({ top: deltaY, behavior: 'auto' });
|
|
214
244
|
} else {
|
|
215
245
|
window.scrollBy({ top: deltaY, behavior: 'auto' });
|
|
216
246
|
}
|
|
217
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
247
|
+
await new Promise((resolve) => setTimeout(resolve, waitActual));
|
|
218
248
|
};
|
|
219
249
|
|
|
220
250
|
for (let round = 1; round <= maxRounds; round += 1) {
|
|
@@ -240,18 +270,27 @@ export function buildCommentsHarvestScript(params = {}) {
|
|
|
240
270
|
}
|
|
241
271
|
|
|
242
272
|
const prevTop = beforeMetrics.scrollTop;
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
await new Promise((resolve) => setTimeout(resolve, settleMs));
|
|
273
|
+
const roundScrollStep = randomBetween(scrollStepMin, scrollStepMax);
|
|
274
|
+
await performScroll(roundScrollStep, settleMs, {
|
|
275
|
+
round,
|
|
276
|
+
reason: 'main_scroll',
|
|
277
|
+
});
|
|
249
278
|
collect(round);
|
|
250
279
|
let afterMetrics = readMetrics();
|
|
251
280
|
let moved = Math.abs(afterMetrics.scrollTop - prevTop) > 1;
|
|
252
281
|
if (!moved && typeof window.scrollBy === 'function') {
|
|
253
|
-
|
|
254
|
-
|
|
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));
|
|
255
294
|
collect(round);
|
|
256
295
|
afterMetrics = readMetrics();
|
|
257
296
|
moved = Math.abs(afterMetrics.scrollTop - prevTop) > 1;
|
|
@@ -298,13 +337,23 @@ export function buildCommentsHarvestScript(params = {}) {
|
|
|
298
337
|
recoveries += 1;
|
|
299
338
|
recoveryReasonCounts[recoveryTrigger] += 1;
|
|
300
339
|
for (let i = 0; i < recoveryUpRounds; i += 1) {
|
|
301
|
-
await performScroll(-recoveryUpStep, settleMs + 120
|
|
340
|
+
await performScroll(-recoveryUpStep, settleMs + 120, {
|
|
341
|
+
round,
|
|
342
|
+
reason: 'recovery_up',
|
|
343
|
+
recoveryTrigger,
|
|
344
|
+
recoveryStep: i + 1,
|
|
345
|
+
});
|
|
302
346
|
collect(round);
|
|
303
347
|
}
|
|
304
348
|
const downBoost = Math.min(maxRecoveryDownBoost, Math.max(0, recoveries - 1) * recoveryDownBoostPerAttempt);
|
|
305
349
|
const downRounds = recoveryDownRounds + downBoost;
|
|
306
350
|
for (let i = 0; i < downRounds; i += 1) {
|
|
307
|
-
await performScroll(recoveryDownStep, settleMs + 180
|
|
351
|
+
await performScroll(recoveryDownStep, settleMs + 180, {
|
|
352
|
+
round,
|
|
353
|
+
reason: 'recovery_down',
|
|
354
|
+
recoveryTrigger,
|
|
355
|
+
recoveryStep: i + 1,
|
|
356
|
+
});
|
|
308
357
|
collect(round);
|
|
309
358
|
}
|
|
310
359
|
const recoveredMetrics = readMetrics();
|
|
@@ -405,6 +454,7 @@ export function buildCommentsHarvestScript(params = {}) {
|
|
|
405
454
|
budgetExpectedCommentsCount,
|
|
406
455
|
scroll: metrics,
|
|
407
456
|
};
|
|
457
|
+
payload.actionTrace = actionTrace;
|
|
408
458
|
if (includeComments) {
|
|
409
459
|
const bounded = commentsLimit > 0 ? comments.slice(0, commentsLimit) : comments;
|
|
410
460
|
payload.comments = bounded;
|
|
@@ -165,9 +165,16 @@ function buildCollectLikeTargetsScript() {
|
|
|
165
165
|
function buildClickLikeByIndexScript(index, highlight, skipAlreadyCheck = false) {
|
|
166
166
|
return `(async () => {
|
|
167
167
|
const idx = Number(${JSON.stringify(index)});
|
|
168
|
+
const actionTrace = [];
|
|
169
|
+
const pushTrace = (payload) => {
|
|
170
|
+
actionTrace.push({
|
|
171
|
+
ts: new Date().toISOString(),
|
|
172
|
+
...payload,
|
|
173
|
+
});
|
|
174
|
+
};
|
|
168
175
|
const items = Array.from(document.querySelectorAll('.comment-item'));
|
|
169
176
|
const item = items[idx];
|
|
170
|
-
if (!item) return { clicked: false, reason: 'comment_item_not_found', index: idx };
|
|
177
|
+
if (!item) return { clicked: false, reason: 'comment_item_not_found', index: idx, actionTrace };
|
|
171
178
|
const findLikeControl = (node) => {
|
|
172
179
|
const selectors = [
|
|
173
180
|
'.like-wrapper',
|
|
@@ -195,12 +202,14 @@ function buildClickLikeByIndexScript(index, highlight, skipAlreadyCheck = false)
|
|
|
195
202
|
};
|
|
196
203
|
|
|
197
204
|
const likeControl = findLikeControl(item);
|
|
198
|
-
if (!likeControl) return { clicked: false, reason: 'like_control_not_found', index: idx };
|
|
205
|
+
if (!likeControl) return { clicked: false, reason: 'like_control_not_found', index: idx, actionTrace };
|
|
199
206
|
const beforeLiked = isAlreadyLiked(likeControl);
|
|
200
207
|
if (beforeLiked && !${skipAlreadyCheck ? 'true' : 'false'}) {
|
|
201
|
-
return { clicked: false, alreadyLiked: true, reason: 'already_liked', index: idx };
|
|
208
|
+
return { clicked: false, alreadyLiked: true, reason: 'already_liked', index: idx, actionTrace };
|
|
202
209
|
}
|
|
210
|
+
pushTrace({ kind: 'scroll', stage: 'xhs_comment_like', index: idx, target: 'comment_item', via: 'scrollIntoView' });
|
|
203
211
|
item.scrollIntoView({ behavior: 'auto', block: 'center' });
|
|
212
|
+
pushTrace({ kind: 'scroll', stage: 'xhs_comment_like', index: idx, target: 'like_control', via: 'scrollIntoView' });
|
|
204
213
|
likeControl.scrollIntoView({ behavior: 'auto', block: 'center' });
|
|
205
214
|
await new Promise((resolve) => setTimeout(resolve, 120));
|
|
206
215
|
if (${highlight ? 'true' : 'false'}) {
|
|
@@ -208,6 +217,7 @@ function buildClickLikeByIndexScript(index, highlight, skipAlreadyCheck = false)
|
|
|
208
217
|
likeControl.style.outline = '2px solid #00d6ff';
|
|
209
218
|
setTimeout(() => { likeControl.style.outline = prev; }, 450);
|
|
210
219
|
}
|
|
220
|
+
pushTrace({ kind: 'click', stage: 'xhs_comment_like', index: idx, target: 'like_control' });
|
|
211
221
|
likeControl.click();
|
|
212
222
|
await new Promise((resolve) => setTimeout(resolve, 220));
|
|
213
223
|
return {
|
|
@@ -216,6 +226,7 @@ function buildClickLikeByIndexScript(index, highlight, skipAlreadyCheck = false)
|
|
|
216
226
|
likedAfter: isAlreadyLiked(likeControl),
|
|
217
227
|
reason: 'clicked',
|
|
218
228
|
index: idx,
|
|
229
|
+
actionTrace,
|
|
219
230
|
};
|
|
220
231
|
})()`;
|
|
221
232
|
}
|
|
@@ -231,7 +242,28 @@ async function captureScreenshotToFile({ profileId, filePath }) {
|
|
|
231
242
|
}
|
|
232
243
|
}
|
|
233
244
|
|
|
234
|
-
|
|
245
|
+
function emitProgress(context, payload = {}) {
|
|
246
|
+
const emit = context?.emitProgress;
|
|
247
|
+
if (typeof emit !== 'function') return;
|
|
248
|
+
emit(payload);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function emitActionTrace(context, actionTrace = [], extra = {}) {
|
|
252
|
+
if (!Array.isArray(actionTrace) || actionTrace.length === 0) return;
|
|
253
|
+
for (let i = 0; i < actionTrace.length; i += 1) {
|
|
254
|
+
const row = actionTrace[i];
|
|
255
|
+
if (!row || typeof row !== 'object') continue;
|
|
256
|
+
const kind = String(row.kind || row.action || '').trim().toLowerCase() || 'trace';
|
|
257
|
+
emitProgress(context, {
|
|
258
|
+
kind,
|
|
259
|
+
step: i + 1,
|
|
260
|
+
...extra,
|
|
261
|
+
...row,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export async function executeCommentLikeOperation({ profileId, params = {}, context = {} }) {
|
|
235
267
|
const maxLikes = Math.max(1, Number(params.maxLikes ?? params.maxLikesPerRound ?? 1) || 1);
|
|
236
268
|
const rawKeywords = normalizeArray(params.keywords || params.likeKeywords);
|
|
237
269
|
const rules = compileLikeRules(rawKeywords);
|
|
@@ -353,6 +385,16 @@ export async function executeCommentLikeOperation({ profileId, params = {} }) {
|
|
|
353
385
|
highlight: false,
|
|
354
386
|
});
|
|
355
387
|
const clickResult = extractEvaluateResultData(clickRaw) || {};
|
|
388
|
+
const clickTrace = Array.isArray(clickResult.actionTrace) ? clickResult.actionTrace : [];
|
|
389
|
+
if (clickTrace.length > 0) {
|
|
390
|
+
emitActionTrace(context, clickTrace, {
|
|
391
|
+
stage: 'xhs_comment_like',
|
|
392
|
+
noteId: output.noteId,
|
|
393
|
+
commentIndex: Number(row.index),
|
|
394
|
+
fallback: false,
|
|
395
|
+
});
|
|
396
|
+
delete clickResult.actionTrace;
|
|
397
|
+
}
|
|
356
398
|
|
|
357
399
|
const afterPath = saveEvidence
|
|
358
400
|
? await captureScreenshotToFile({
|
|
@@ -454,6 +496,16 @@ export async function executeCommentLikeOperation({ profileId, params = {} }) {
|
|
|
454
496
|
highlight: false,
|
|
455
497
|
});
|
|
456
498
|
const clickResult = extractEvaluateResultData(clickRaw) || {};
|
|
499
|
+
const clickTrace = Array.isArray(clickResult.actionTrace) ? clickResult.actionTrace : [];
|
|
500
|
+
if (clickTrace.length > 0) {
|
|
501
|
+
emitActionTrace(context, clickTrace, {
|
|
502
|
+
stage: 'xhs_comment_like',
|
|
503
|
+
noteId: output.noteId,
|
|
504
|
+
commentIndex: Number(row.index),
|
|
505
|
+
fallback: true,
|
|
506
|
+
});
|
|
507
|
+
delete clickResult.actionTrace;
|
|
508
|
+
}
|
|
457
509
|
const afterPath = saveEvidence
|
|
458
510
|
? await captureScreenshotToFile({
|
|
459
511
|
profileId,
|
|
@@ -1,15 +1,39 @@
|
|
|
1
1
|
export function buildSubmitSearchScript(params = {}) {
|
|
2
2
|
const keyword = String(params.keyword || '').trim();
|
|
3
|
+
const method = String(params.method || params.submitMethod || 'click').trim().toLowerCase();
|
|
4
|
+
const actionDelayMinMs = Math.max(20, Number(params.actionDelayMinMs ?? 180) || 180);
|
|
5
|
+
const actionDelayMaxMs = Math.max(actionDelayMinMs, Number(params.actionDelayMaxMs ?? 620) || 620);
|
|
6
|
+
const settleMinMs = Math.max(60, Number(params.settleMinMs ?? 1200) || 1200);
|
|
7
|
+
const settleMaxMs = Math.max(settleMinMs, Number(params.settleMaxMs ?? 2600) || 2600);
|
|
3
8
|
return `(async () => {
|
|
4
9
|
const state = window.__camoXhsState || (window.__camoXhsState = {});
|
|
5
10
|
const metrics = state.metrics && typeof state.metrics === 'object' ? state.metrics : {};
|
|
6
11
|
state.metrics = metrics;
|
|
7
12
|
metrics.searchCount = Number(metrics.searchCount || 0) + 1;
|
|
8
13
|
metrics.lastSearchAt = new Date().toISOString();
|
|
14
|
+
const actionTrace = [];
|
|
15
|
+
const pushTrace = (payload) => {
|
|
16
|
+
actionTrace.push({
|
|
17
|
+
ts: new Date().toISOString(),
|
|
18
|
+
...payload,
|
|
19
|
+
});
|
|
20
|
+
};
|
|
9
21
|
const input = document.querySelector('#search-input, input.search-input');
|
|
10
22
|
if (!(input instanceof HTMLInputElement)) {
|
|
11
23
|
throw new Error('SEARCH_INPUT_NOT_FOUND');
|
|
12
24
|
}
|
|
25
|
+
const randomBetween = (min, max) => {
|
|
26
|
+
const lo = Math.max(0, Math.floor(Number(min) || 0));
|
|
27
|
+
const hi = Math.max(lo, Math.floor(Number(max) || 0));
|
|
28
|
+
if (hi <= lo) return lo;
|
|
29
|
+
return lo + Math.floor(Math.random() * (hi - lo + 1));
|
|
30
|
+
};
|
|
31
|
+
const waitRandom = async (min, max, stage) => {
|
|
32
|
+
const waitMs = randomBetween(min, max);
|
|
33
|
+
pushTrace({ kind: 'wait', stage, waitMs });
|
|
34
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
35
|
+
return waitMs;
|
|
36
|
+
};
|
|
13
37
|
const targetKeyword = ${JSON.stringify(keyword)};
|
|
14
38
|
if (targetKeyword && input.value !== targetKeyword) {
|
|
15
39
|
input.focus();
|
|
@@ -17,35 +41,65 @@ export function buildSubmitSearchScript(params = {}) {
|
|
|
17
41
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
18
42
|
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
19
43
|
}
|
|
20
|
-
const
|
|
44
|
+
const requestedMethod = ${JSON.stringify(method)};
|
|
45
|
+
const normalizedMethod = ['click', 'enter', 'form'].includes(requestedMethod) ? requestedMethod : 'click';
|
|
21
46
|
const beforeUrl = window.location.href;
|
|
22
47
|
input.focus();
|
|
23
|
-
input.dispatchEvent(new KeyboardEvent('keydown', enterEvent));
|
|
24
|
-
input.dispatchEvent(new KeyboardEvent('keypress', enterEvent));
|
|
25
|
-
input.dispatchEvent(new KeyboardEvent('keyup', enterEvent));
|
|
26
48
|
const candidates = ['.input-button .search-icon', '.input-button', 'button.min-width-search-icon'];
|
|
49
|
+
let methodUsed = normalizedMethod;
|
|
27
50
|
let clickedSelector = null;
|
|
28
|
-
for (const selector of candidates) {
|
|
29
|
-
const button = document.querySelector(selector);
|
|
30
|
-
if (!button) continue;
|
|
31
|
-
if (button instanceof HTMLElement) button.scrollIntoView({ behavior: 'auto', block: 'center' });
|
|
32
|
-
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
33
|
-
button.click();
|
|
34
|
-
clickedSelector = selector;
|
|
35
|
-
break;
|
|
36
|
-
}
|
|
37
51
|
const form = input.closest('form');
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
52
|
+
if (normalizedMethod === 'click') {
|
|
53
|
+
let clicked = false;
|
|
54
|
+
for (const selector of candidates) {
|
|
55
|
+
const button = document.querySelector(selector);
|
|
56
|
+
if (!button) continue;
|
|
57
|
+
if (button instanceof HTMLElement) {
|
|
58
|
+
pushTrace({ kind: 'scroll', stage: 'submit_search', selector, via: 'scrollIntoView' });
|
|
59
|
+
button.scrollIntoView({ behavior: 'auto', block: 'center' });
|
|
60
|
+
}
|
|
61
|
+
await waitRandom(${actionDelayMinMs}, ${actionDelayMaxMs}, 'submit_pre_click');
|
|
62
|
+
pushTrace({ kind: 'click', stage: 'submit_search', selector });
|
|
63
|
+
button.click();
|
|
64
|
+
clickedSelector = selector;
|
|
65
|
+
clicked = true;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
if (!clicked) {
|
|
69
|
+
methodUsed = 'form';
|
|
70
|
+
}
|
|
41
71
|
}
|
|
42
|
-
|
|
72
|
+
if (methodUsed === 'enter') {
|
|
73
|
+
await waitRandom(${actionDelayMinMs}, ${actionDelayMaxMs}, 'submit_pre_enter');
|
|
74
|
+
const enterEvent = { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true };
|
|
75
|
+
pushTrace({ kind: 'key', stage: 'submit_search', key: 'Enter' });
|
|
76
|
+
input.dispatchEvent(new KeyboardEvent('keydown', enterEvent));
|
|
77
|
+
input.dispatchEvent(new KeyboardEvent('keypress', enterEvent));
|
|
78
|
+
input.dispatchEvent(new KeyboardEvent('keyup', enterEvent));
|
|
79
|
+
} else if (methodUsed === 'form') {
|
|
80
|
+
await waitRandom(${actionDelayMinMs}, ${actionDelayMaxMs}, 'submit_pre_form');
|
|
81
|
+
if (form) {
|
|
82
|
+
pushTrace({ kind: 'submit', stage: 'submit_search', via: 'form' });
|
|
83
|
+
if (typeof form.requestSubmit === 'function') form.requestSubmit();
|
|
84
|
+
else form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
|
85
|
+
} else {
|
|
86
|
+
methodUsed = 'enter';
|
|
87
|
+
const enterEvent = { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true };
|
|
88
|
+
pushTrace({ kind: 'key', stage: 'submit_search', key: 'Enter', fallback: true });
|
|
89
|
+
input.dispatchEvent(new KeyboardEvent('keydown', enterEvent));
|
|
90
|
+
input.dispatchEvent(new KeyboardEvent('keypress', enterEvent));
|
|
91
|
+
input.dispatchEvent(new KeyboardEvent('keyup', enterEvent));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
await waitRandom(${settleMinMs}, ${settleMaxMs}, 'submit_settle');
|
|
43
95
|
return {
|
|
44
96
|
submitted: true,
|
|
45
|
-
via: clickedSelector ||
|
|
97
|
+
via: clickedSelector || methodUsed,
|
|
46
98
|
beforeUrl,
|
|
47
99
|
afterUrl: window.location.href,
|
|
100
|
+
method: methodUsed,
|
|
48
101
|
searchCount: metrics.searchCount,
|
|
102
|
+
actionTrace,
|
|
49
103
|
};
|
|
50
104
|
})()`;
|
|
51
105
|
}
|
|
@@ -67,9 +121,21 @@ export function buildOpenDetailScript(params = {}) {
|
|
|
67
121
|
const nextSeekRounds = Math.max(0, Number(params.nextSeekRounds || 8) || 8);
|
|
68
122
|
const nextSeekStep = Math.max(0, Number(params.nextSeekStep || 0) || 0);
|
|
69
123
|
const nextSeekSettleMs = Math.max(120, Number(params.nextSeekSettleMs || 320) || 320);
|
|
124
|
+
const preClickDelayMinMs = Math.max(60, Number(params.preClickDelayMinMs ?? 220) || 220);
|
|
125
|
+
const preClickDelayMaxMs = Math.max(preClickDelayMinMs, Number(params.preClickDelayMaxMs ?? 700) || 700);
|
|
126
|
+
const pollDelayMinMs = Math.max(80, Number(params.pollDelayMinMs ?? 130) || 130);
|
|
127
|
+
const pollDelayMaxMs = Math.max(pollDelayMinMs, Number(params.pollDelayMaxMs ?? 320) || 320);
|
|
128
|
+
const postOpenDelayMinMs = Math.max(120, Number(params.postOpenDelayMinMs ?? 420) || 420);
|
|
129
|
+
const postOpenDelayMaxMs = Math.max(postOpenDelayMinMs, Number(params.postOpenDelayMaxMs ?? 1100) || 1100);
|
|
70
130
|
|
|
71
131
|
return `(async () => {
|
|
72
132
|
const STATE_KEY = '__camoXhsState';
|
|
133
|
+
const randomBetween = (min, max) => {
|
|
134
|
+
const lo = Math.max(0, Math.floor(Number(min) || 0));
|
|
135
|
+
const hi = Math.max(lo, Math.floor(Number(max) || 0));
|
|
136
|
+
if (hi <= lo) return lo;
|
|
137
|
+
return lo + Math.floor(Math.random() * (hi - lo + 1));
|
|
138
|
+
};
|
|
73
139
|
const normalizeVisited = (value) => {
|
|
74
140
|
if (!Array.isArray(value)) return [];
|
|
75
141
|
return value
|
|
@@ -100,6 +166,13 @@ export function buildOpenDetailScript(params = {}) {
|
|
|
100
166
|
};
|
|
101
167
|
|
|
102
168
|
const state = loadState();
|
|
169
|
+
const actionTrace = [];
|
|
170
|
+
const pushTrace = (payload) => {
|
|
171
|
+
actionTrace.push({
|
|
172
|
+
ts: new Date().toISOString(),
|
|
173
|
+
...payload,
|
|
174
|
+
});
|
|
175
|
+
};
|
|
103
176
|
if (!Array.isArray(state.visitedNoteIds)) state.visitedNoteIds = [];
|
|
104
177
|
const requestedKeyword = ${JSON.stringify(keyword)};
|
|
105
178
|
const mode = ${JSON.stringify(mode)};
|
|
@@ -156,11 +229,23 @@ export function buildOpenDetailScript(params = {}) {
|
|
|
156
229
|
const maxRounds = Number(${seedCollectMaxRounds});
|
|
157
230
|
const targetCount = Number(${seedCollectCount});
|
|
158
231
|
for (let round = 0; round < maxRounds && seedCollectedSet.size < targetCount; round += 1) {
|
|
232
|
+
pushTrace({
|
|
233
|
+
kind: 'scroll',
|
|
234
|
+
stage: 'seed_collect',
|
|
235
|
+
round: round + 1,
|
|
236
|
+
deltaY: Number(${seedCollectStep}),
|
|
237
|
+
});
|
|
159
238
|
window.scrollBy({ top: Number(${seedCollectStep}), left: 0, behavior: 'auto' });
|
|
160
239
|
await new Promise((resolve) => setTimeout(resolve, Number(${seedCollectSettleMs})));
|
|
161
240
|
collectVisible();
|
|
162
241
|
}
|
|
163
242
|
if (${seedResetToTop ? 'true' : 'false'}) {
|
|
243
|
+
pushTrace({
|
|
244
|
+
kind: 'scroll',
|
|
245
|
+
stage: 'seed_collect',
|
|
246
|
+
round: 'reset_to_top',
|
|
247
|
+
toTop: true,
|
|
248
|
+
});
|
|
164
249
|
window.scrollTo({ top: 0, behavior: 'auto' });
|
|
165
250
|
await new Promise((resolve) => setTimeout(resolve, Number(${seedCollectSettleMs})));
|
|
166
251
|
}
|
|
@@ -190,6 +275,12 @@ export function buildOpenDetailScript(params = {}) {
|
|
|
190
275
|
let stagnantRounds = 0;
|
|
191
276
|
for (let round = 0; !next && round < Number(${nextSeekRounds}); round += 1) {
|
|
192
277
|
const beforeTop = window.scrollY || document.documentElement.scrollTop || document.body.scrollTop || 0;
|
|
278
|
+
pushTrace({
|
|
279
|
+
kind: 'scroll',
|
|
280
|
+
stage: 'seek_next_detail',
|
|
281
|
+
round: round + 1,
|
|
282
|
+
deltaY: seekStep,
|
|
283
|
+
});
|
|
193
284
|
window.scrollBy({ top: seekStep, left: 0, behavior: 'auto' });
|
|
194
285
|
await new Promise((resolve) => setTimeout(resolve, Number(${nextSeekSettleMs})));
|
|
195
286
|
nodes = mapNodes();
|
|
@@ -238,9 +329,23 @@ export function buildOpenDetailScript(params = {}) {
|
|
|
238
329
|
return !searchVisible;
|
|
239
330
|
};
|
|
240
331
|
|
|
332
|
+
pushTrace({
|
|
333
|
+
kind: 'scroll',
|
|
334
|
+
stage: 'open_detail',
|
|
335
|
+
noteId: next.noteId,
|
|
336
|
+
via: 'scrollIntoView',
|
|
337
|
+
});
|
|
241
338
|
next.cover.scrollIntoView({ behavior: 'auto', block: 'center' });
|
|
242
|
-
|
|
339
|
+
const preClickDelay = randomBetween(${preClickDelayMinMs}, ${preClickDelayMaxMs});
|
|
340
|
+
pushTrace({ kind: 'wait', stage: 'open_detail_pre_click', noteId: next.noteId, waitMs: preClickDelay });
|
|
341
|
+
await new Promise((resolve) => setTimeout(resolve, preClickDelay));
|
|
243
342
|
const beforeUrl = window.location.href;
|
|
343
|
+
pushTrace({
|
|
344
|
+
kind: 'click',
|
|
345
|
+
stage: 'open_detail',
|
|
346
|
+
noteId: next.noteId,
|
|
347
|
+
selector: 'a.cover',
|
|
348
|
+
});
|
|
244
349
|
next.cover.click();
|
|
245
350
|
|
|
246
351
|
let detailReady = false;
|
|
@@ -249,12 +354,15 @@ export function buildOpenDetailScript(params = {}) {
|
|
|
249
354
|
detailReady = true;
|
|
250
355
|
break;
|
|
251
356
|
}
|
|
252
|
-
|
|
357
|
+
const pollDelay = randomBetween(${pollDelayMinMs}, ${pollDelayMaxMs});
|
|
358
|
+
await new Promise((resolve) => setTimeout(resolve, pollDelay));
|
|
253
359
|
}
|
|
254
360
|
if (!detailReady) {
|
|
255
361
|
throw new Error('DETAIL_OPEN_TIMEOUT');
|
|
256
362
|
}
|
|
257
|
-
|
|
363
|
+
const postOpenDelay = randomBetween(${postOpenDelayMinMs}, ${postOpenDelayMaxMs});
|
|
364
|
+
pushTrace({ kind: 'wait', stage: 'open_detail_post_open', noteId: next.noteId, waitMs: postOpenDelay });
|
|
365
|
+
await new Promise((resolve) => setTimeout(resolve, postOpenDelay));
|
|
258
366
|
const afterUrl = window.location.href;
|
|
259
367
|
|
|
260
368
|
if (!state.visitedNoteIds.includes(next.noteId)) state.visitedNoteIds.push(next.noteId);
|
|
@@ -274,6 +382,7 @@ export function buildOpenDetailScript(params = {}) {
|
|
|
274
382
|
excludedCount: excludedNoteIds.size,
|
|
275
383
|
seedCollectedCount: seedCollectedSet.size,
|
|
276
384
|
seedCollectedNoteIds: Array.from(seedCollectedSet),
|
|
385
|
+
actionTrace,
|
|
277
386
|
};
|
|
278
387
|
})()`;
|
|
279
388
|
}
|