@web-auto/webauto 0.1.13 → 0.1.14

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.
@@ -524,6 +524,7 @@ async function runProfile(spec, argv, baseOverrides = {}) {
524
524
  'xhs.unified.stop_screenshot',
525
525
  'xhs.unified.profile_failed',
526
526
  'autoscript:operation_done',
527
+ 'autoscript:operation_progress',
527
528
  'autoscript:operation_error',
528
529
  'autoscript:operation_terminal',
529
530
  'autoscript:operation_recovery_failed',
@@ -601,6 +602,7 @@ async function runProfile(spec, argv, baseOverrides = {}) {
601
602
  || eventName === 'autoscript:stop'
602
603
  || eventName === 'autoscript:impact'
603
604
  || eventName === 'autoscript:operation_start'
605
+ || eventName === 'autoscript:operation_progress'
604
606
  || eventName === 'autoscript:operation_done'
605
607
  || eventName === 'autoscript:operation_error'
606
608
  || eventName === 'autoscript:operation_recovery_failed'
package/bin/webauto.mjs CHANGED
@@ -108,6 +108,7 @@ function resolveOnPath(candidates) {
108
108
  function wrapWindowsRunner(cmdPath, prefix = []) {
109
109
  if (process.platform !== 'win32') return { cmd: cmdPath, prefix };
110
110
  const lower = String(cmdPath || '').toLowerCase();
111
+ const quotedCmdPath = /\s/.test(String(cmdPath || '')) ? `"${cmdPath}"` : cmdPath;
111
112
  if (lower.endsWith('.ps1')) {
112
113
  return {
113
114
  cmd: 'powershell.exe',
@@ -117,7 +118,7 @@ function wrapWindowsRunner(cmdPath, prefix = []) {
117
118
  if (lower.endsWith('.cmd') || lower.endsWith('.bat')) {
118
119
  return {
119
120
  cmd: 'cmd.exe',
120
- prefix: ['/d', '/s', '/c', cmdPath, ...prefix],
121
+ prefix: ['/d', '/s', '/c', quotedCmdPath, ...prefix],
121
122
  };
122
123
  }
123
124
  return { cmd: cmdPath, prefix };
@@ -125,9 +126,9 @@ function wrapWindowsRunner(cmdPath, prefix = []) {
125
126
 
126
127
  function npmRunner() {
127
128
  if (process.platform !== 'win32') return { cmd: 'npm', prefix: [] };
128
- const npmNames = ['npm.cmd', 'npm.exe', 'npm.bat', 'npm.ps1'];
129
- const resolved = resolveOnPath(npmNames) || 'npm.cmd';
130
- return wrapWindowsRunner(resolved);
129
+ // Always prefer PATH-resolved npm.cmd to avoid space-path quoting issues
130
+ // like "C:\Program Files\..." when invoking via cmd /c.
131
+ return wrapWindowsRunner('npm.cmd');
131
132
  }
132
133
 
133
134
  function uiConsoleScriptPath() {
@@ -477,17 +478,12 @@ async function runInDir(dir, cmd, args) {
477
478
  }
478
479
 
479
480
  function checkDesktopConsoleDeps() {
480
- if (isGlobalInstall()) return true;
481
- // Check for electron in various locations:
482
- // 1. Global npm root (when installed globally alongside webauto)
483
- // 2. Package's own node_modules
484
- // 3. apps/desktop-console/node_modules (local dev)
485
- const globalRoot = path.resolve(ROOT, '..', '..');
486
- return (
487
- exists(path.join(globalRoot, 'electron')) ||
488
- exists(path.join(ROOT, 'node_modules', 'electron')) ||
489
- exists(path.join(ROOT, 'apps', 'desktop-console', 'node_modules', 'electron'))
490
- );
481
+ const electronName = process.platform === 'win32' ? 'electron.exe' : 'electron';
482
+ const candidates = [
483
+ path.join(ROOT, 'node_modules', 'electron', 'dist', electronName),
484
+ path.join(ROOT, 'apps', 'desktop-console', 'node_modules', 'electron', 'dist', electronName),
485
+ ];
486
+ return candidates.some((p) => exists(p));
491
487
  }
492
488
 
493
489
  function checkDesktopConsoleBuilt() {
@@ -510,6 +506,15 @@ async function ensureDepsAndBuild() {
510
506
 
511
507
  // Global package should already ship renderer build.
512
508
  if (isGlobalInstall()) {
509
+ if (!checkDesktopConsoleDeps()) {
510
+ console.log('[webauto] Installing desktop-console runtime dependencies...');
511
+ const npm = npmRunner();
512
+ await run(npm.cmd, [...npm.prefix, '--prefix', appDir, '--workspaces=false', 'install', '--omit=dev']);
513
+ }
514
+ if (!checkDesktopConsoleDeps()) {
515
+ console.error('❌ electron runtime installation failed for desktop-console.');
516
+ process.exit(1);
517
+ }
513
518
  if (!checkDesktopConsoleBuilt()) {
514
519
  console.error('❌ desktop-console dist missing from package. Please reinstall @web-auto/webauto.');
515
520
  process.exit(1);
@@ -540,61 +545,79 @@ async function ensureDepsAndBuild() {
540
545
  console.log('[webauto] Setup complete!');
541
546
  }
542
547
 
543
- async function uiConsole({ build, install, checkOnly, noDaemon }) {
544
- console.log(`[webauto] version ${ROOT_VERSION}`);
545
- const okServices = checkServicesBuilt();
546
- const okDeps = checkDesktopConsoleDeps();
547
- const okUiBuilt = checkDesktopConsoleBuilt();
548
-
549
- if (checkOnly) {
550
- console.log(`[check] repoRoot: ${ROOT}`);
551
- console.log(`[check] dist/services: ${okServices ? 'OK' : 'MISSING'}`);
552
- console.log(`[check] desktop-console deps: ${okDeps ? 'OK' : 'MISSING'}`);
553
- console.log(`[check] desktop-console dist: ${okUiBuilt ? 'OK' : 'MISSING'}`);
554
- console.log(`[check] isGlobalInstall: ${isGlobalInstall()}`);
555
- return;
556
- }
548
+ async function ensureUiRuntimeReady({ build, install }) {
549
+ let okServices = checkServicesBuilt();
550
+ let okDeps = checkDesktopConsoleDeps();
551
+ let okUiBuilt = checkDesktopConsoleBuilt();
557
552
 
558
- // For global install, auto-setup on first run
559
553
  if (isGlobalInstall()) {
560
554
  const state = loadState();
561
555
  const pkgJson = JSON.parse(readFileSync(path.join(ROOT, 'package.json'), 'utf-8'));
562
- if (!state.initialized || state.version !== pkgJson.version) {
556
+ if (!state.initialized || state.version !== pkgJson.version || !okDeps || !okUiBuilt) {
563
557
  await ensureDepsAndBuild();
558
+ okDeps = checkDesktopConsoleDeps();
559
+ okUiBuilt = checkDesktopConsoleBuilt();
564
560
  }
565
- } else {
566
- // Local dev mode - require explicit build
567
- if (!okServices) {
568
- if (!build) {
569
- console.error('❌ missing dist/ (services/modules). Run: npm run build:services');
570
- process.exit(2);
571
- }
572
- const npm = npmRunner();
573
- await run(npm.cmd, [...npm.prefix, 'run', 'build:services']);
561
+ } else if (!okServices) {
562
+ if (!build) {
563
+ console.error('❌ missing dist/ (services/modules). Run: npm run build:services');
564
+ process.exit(2);
574
565
  }
566
+ const npm = npmRunner();
567
+ await run(npm.cmd, [...npm.prefix, 'run', 'build:services']);
568
+ okServices = checkServicesBuilt();
575
569
  }
576
570
 
577
571
  if (!okDeps) {
578
- // For global install, okDeps is always true since we check global electron
579
- // For local dev, require explicit --install or --build
580
- if (!isGlobalInstall() && !install && !build) {
581
- console.error('❌ missing apps/desktop-console/node_modules. Run: npm --prefix apps/desktop-console install');
572
+ if (isGlobalInstall()) {
573
+ console.error('❌ electron runtime missing from installed package after setup.');
582
574
  process.exit(2);
583
575
  }
584
- if (!isGlobalInstall()) {
585
- const npm = npmRunner();
586
- await runInDir(path.join(ROOT, 'apps', 'desktop-console'), npm.cmd, [...npm.prefix, 'install']);
576
+ if (!install && !build) {
577
+ console.error('❌ missing apps/desktop-console/node_modules. Run: npm --prefix apps/desktop-console install');
578
+ process.exit(2);
587
579
  }
580
+ const npm = npmRunner();
581
+ await runInDir(path.join(ROOT, 'apps', 'desktop-console'), npm.cmd, [...npm.prefix, 'install']);
582
+ okDeps = checkDesktopConsoleDeps();
588
583
  }
589
584
 
590
585
  if (!okUiBuilt) {
586
+ if (isGlobalInstall()) {
587
+ console.error('❌ missing apps/desktop-console/dist in installed package after setup.');
588
+ process.exit(2);
589
+ }
591
590
  if (!build) {
592
591
  console.error('❌ missing apps/desktop-console/dist. Run: npm --prefix apps/desktop-console run build');
593
592
  process.exit(2);
594
593
  }
595
594
  const npm = npmRunner();
596
595
  await runInDir(path.join(ROOT, 'apps', 'desktop-console'), npm.cmd, [...npm.prefix, 'run', 'build']);
596
+ okUiBuilt = checkDesktopConsoleBuilt();
597
+ }
598
+
599
+ if (!okDeps || !okUiBuilt || (!isGlobalInstall() && !okServices)) {
600
+ console.error('❌ ui runtime prerequisites are not ready');
601
+ process.exit(2);
597
602
  }
603
+ }
604
+
605
+ async function uiConsole({ build, install, checkOnly, noDaemon }) {
606
+ console.log(`[webauto] version ${ROOT_VERSION}`);
607
+ const okServices = checkServicesBuilt();
608
+ const okDeps = checkDesktopConsoleDeps();
609
+ const okUiBuilt = checkDesktopConsoleBuilt();
610
+
611
+ if (checkOnly) {
612
+ console.log(`[check] repoRoot: ${ROOT}`);
613
+ console.log(`[check] dist/services: ${okServices ? 'OK' : 'MISSING'}`);
614
+ console.log(`[check] desktop-console deps: ${okDeps ? 'OK' : 'MISSING'}`);
615
+ console.log(`[check] desktop-console dist: ${okUiBuilt ? 'OK' : 'MISSING'}`);
616
+ console.log(`[check] isGlobalInstall: ${isGlobalInstall()}`);
617
+ return;
618
+ }
619
+
620
+ await ensureUiRuntimeReady({ build, install });
598
621
 
599
622
  const uiScript = uiConsoleScriptPath();
600
623
  const uiArgs = [];
@@ -746,6 +769,10 @@ async function main() {
746
769
  }
747
770
 
748
771
  if (cmd === 'ui' && sub === 'cli') {
772
+ await ensureUiRuntimeReady({
773
+ build: args.build === true,
774
+ install: args.install === true,
775
+ });
749
776
  const script = path.join(ROOT, 'apps', 'desktop-console', 'entry', 'ui-cli.mjs');
750
777
  await run(process.execPath, [script, ...rawArgv.slice(2)]);
751
778
  return;
@@ -1,9 +1,20 @@
1
1
  import { executeXhsAutoscriptOperation, isXhsAutoscriptAction } from './xhs.mjs';
2
2
 
3
- export async function executeAutoscriptAction({ profileId, action, params = {} }) {
3
+ export async function executeAutoscriptAction({
4
+ profileId,
5
+ action,
6
+ params = {},
7
+ operation = null,
8
+ context = {},
9
+ }) {
4
10
  if (isXhsAutoscriptAction(action)) {
5
- return executeXhsAutoscriptOperation({ profileId, action, params });
11
+ return executeXhsAutoscriptOperation({
12
+ profileId,
13
+ action,
14
+ params,
15
+ operation,
16
+ context,
17
+ });
6
18
  }
7
19
  return null;
8
20
  }
9
-
@@ -29,6 +29,13 @@ export function buildCommentsHarvestScript(params = {}) {
29
29
  const metricsState = state.metrics && typeof state.metrics === 'object' ? state.metrics : {};
30
30
  state.metrics = metricsState;
31
31
  metricsState.searchCount = Number(metricsState.searchCount || 0);
32
+ const actionTrace = [];
33
+ const pushTrace = (payload) => {
34
+ actionTrace.push({
35
+ ts: new Date().toISOString(),
36
+ ...payload,
37
+ });
38
+ };
32
39
  const detailSelectors = [
33
40
  '.note-detail-mask',
34
41
  '.note-detail-page',
@@ -208,7 +215,13 @@ export function buildCommentsHarvestScript(params = {}) {
208
215
  no_effect: 0,
209
216
  no_new_comments: 0,
210
217
  };
211
- const performScroll = async (deltaY, waitMs = settleMs) => {
218
+ const performScroll = async (deltaY, waitMs = settleMs, meta = {}) => {
219
+ pushTrace({
220
+ kind: 'scroll',
221
+ stage: 'xhs_comments_harvest',
222
+ deltaY: Number(deltaY),
223
+ ...meta,
224
+ });
212
225
  if (typeof scroller?.scrollBy === 'function') {
213
226
  scroller.scrollBy({ top: deltaY, behavior: 'auto' });
214
227
  } else {
@@ -240,17 +253,23 @@ export function buildCommentsHarvestScript(params = {}) {
240
253
  }
241
254
 
242
255
  const prevTop = beforeMetrics.scrollTop;
243
- if (typeof scroller?.scrollBy === 'function') {
244
- scroller.scrollBy({ top: scrollStep, behavior: 'auto' });
245
- } else {
246
- window.scrollBy({ top: scrollStep, behavior: 'auto' });
247
- }
248
- await new Promise((resolve) => setTimeout(resolve, settleMs));
256
+ await performScroll(scrollStep, settleMs, {
257
+ round,
258
+ reason: 'main_scroll',
259
+ });
249
260
  collect(round);
250
261
  let afterMetrics = readMetrics();
251
262
  let moved = Math.abs(afterMetrics.scrollTop - prevTop) > 1;
252
263
  if (!moved && typeof window.scrollBy === 'function') {
253
- window.scrollBy({ top: Math.max(120, Math.floor(scrollStep / 2)), behavior: 'auto' });
264
+ const fallbackStep = Math.max(120, Math.floor(scrollStep / 2));
265
+ pushTrace({
266
+ kind: 'scroll',
267
+ stage: 'xhs_comments_harvest',
268
+ round,
269
+ reason: 'fallback_scroll',
270
+ deltaY: Number(fallbackStep),
271
+ });
272
+ window.scrollBy({ top: fallbackStep, behavior: 'auto' });
254
273
  await new Promise((resolve) => setTimeout(resolve, settleMs));
255
274
  collect(round);
256
275
  afterMetrics = readMetrics();
@@ -298,13 +317,23 @@ export function buildCommentsHarvestScript(params = {}) {
298
317
  recoveries += 1;
299
318
  recoveryReasonCounts[recoveryTrigger] += 1;
300
319
  for (let i = 0; i < recoveryUpRounds; i += 1) {
301
- await performScroll(-recoveryUpStep, settleMs + 120);
320
+ await performScroll(-recoveryUpStep, settleMs + 120, {
321
+ round,
322
+ reason: 'recovery_up',
323
+ recoveryTrigger,
324
+ recoveryStep: i + 1,
325
+ });
302
326
  collect(round);
303
327
  }
304
328
  const downBoost = Math.min(maxRecoveryDownBoost, Math.max(0, recoveries - 1) * recoveryDownBoostPerAttempt);
305
329
  const downRounds = recoveryDownRounds + downBoost;
306
330
  for (let i = 0; i < downRounds; i += 1) {
307
- await performScroll(recoveryDownStep, settleMs + 180);
331
+ await performScroll(recoveryDownStep, settleMs + 180, {
332
+ round,
333
+ reason: 'recovery_down',
334
+ recoveryTrigger,
335
+ recoveryStep: i + 1,
336
+ });
308
337
  collect(round);
309
338
  }
310
339
  const recoveredMetrics = readMetrics();
@@ -405,6 +434,7 @@ export function buildCommentsHarvestScript(params = {}) {
405
434
  budgetExpectedCommentsCount,
406
435
  scroll: metrics,
407
436
  };
437
+ payload.actionTrace = actionTrace;
408
438
  if (includeComments) {
409
439
  const bounded = commentsLimit > 0 ? comments.slice(0, commentsLimit) : comments;
410
440
  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
- export async function executeCommentLikeOperation({ profileId, params = {} }) {
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,
@@ -6,6 +6,13 @@ export function buildSubmitSearchScript(params = {}) {
6
6
  state.metrics = metrics;
7
7
  metrics.searchCount = Number(metrics.searchCount || 0) + 1;
8
8
  metrics.lastSearchAt = new Date().toISOString();
9
+ const actionTrace = [];
10
+ const pushTrace = (payload) => {
11
+ actionTrace.push({
12
+ ts: new Date().toISOString(),
13
+ ...payload,
14
+ });
15
+ };
9
16
  const input = document.querySelector('#search-input, input.search-input');
10
17
  if (!(input instanceof HTMLInputElement)) {
11
18
  throw new Error('SEARCH_INPUT_NOT_FOUND');
@@ -28,8 +35,12 @@ export function buildSubmitSearchScript(params = {}) {
28
35
  for (const selector of candidates) {
29
36
  const button = document.querySelector(selector);
30
37
  if (!button) continue;
31
- if (button instanceof HTMLElement) button.scrollIntoView({ behavior: 'auto', block: 'center' });
38
+ if (button instanceof HTMLElement) {
39
+ pushTrace({ kind: 'scroll', stage: 'submit_search', selector, via: 'scrollIntoView' });
40
+ button.scrollIntoView({ behavior: 'auto', block: 'center' });
41
+ }
32
42
  await new Promise((resolve) => setTimeout(resolve, 80));
43
+ pushTrace({ kind: 'click', stage: 'submit_search', selector });
33
44
  button.click();
34
45
  clickedSelector = selector;
35
46
  break;
@@ -46,6 +57,7 @@ export function buildSubmitSearchScript(params = {}) {
46
57
  beforeUrl,
47
58
  afterUrl: window.location.href,
48
59
  searchCount: metrics.searchCount,
60
+ actionTrace,
49
61
  };
50
62
  })()`;
51
63
  }
@@ -100,6 +112,13 @@ export function buildOpenDetailScript(params = {}) {
100
112
  };
101
113
 
102
114
  const state = loadState();
115
+ const actionTrace = [];
116
+ const pushTrace = (payload) => {
117
+ actionTrace.push({
118
+ ts: new Date().toISOString(),
119
+ ...payload,
120
+ });
121
+ };
103
122
  if (!Array.isArray(state.visitedNoteIds)) state.visitedNoteIds = [];
104
123
  const requestedKeyword = ${JSON.stringify(keyword)};
105
124
  const mode = ${JSON.stringify(mode)};
@@ -156,11 +175,23 @@ export function buildOpenDetailScript(params = {}) {
156
175
  const maxRounds = Number(${seedCollectMaxRounds});
157
176
  const targetCount = Number(${seedCollectCount});
158
177
  for (let round = 0; round < maxRounds && seedCollectedSet.size < targetCount; round += 1) {
178
+ pushTrace({
179
+ kind: 'scroll',
180
+ stage: 'seed_collect',
181
+ round: round + 1,
182
+ deltaY: Number(${seedCollectStep}),
183
+ });
159
184
  window.scrollBy({ top: Number(${seedCollectStep}), left: 0, behavior: 'auto' });
160
185
  await new Promise((resolve) => setTimeout(resolve, Number(${seedCollectSettleMs})));
161
186
  collectVisible();
162
187
  }
163
188
  if (${seedResetToTop ? 'true' : 'false'}) {
189
+ pushTrace({
190
+ kind: 'scroll',
191
+ stage: 'seed_collect',
192
+ round: 'reset_to_top',
193
+ toTop: true,
194
+ });
164
195
  window.scrollTo({ top: 0, behavior: 'auto' });
165
196
  await new Promise((resolve) => setTimeout(resolve, Number(${seedCollectSettleMs})));
166
197
  }
@@ -190,6 +221,12 @@ export function buildOpenDetailScript(params = {}) {
190
221
  let stagnantRounds = 0;
191
222
  for (let round = 0; !next && round < Number(${nextSeekRounds}); round += 1) {
192
223
  const beforeTop = window.scrollY || document.documentElement.scrollTop || document.body.scrollTop || 0;
224
+ pushTrace({
225
+ kind: 'scroll',
226
+ stage: 'seek_next_detail',
227
+ round: round + 1,
228
+ deltaY: seekStep,
229
+ });
193
230
  window.scrollBy({ top: seekStep, left: 0, behavior: 'auto' });
194
231
  await new Promise((resolve) => setTimeout(resolve, Number(${nextSeekSettleMs})));
195
232
  nodes = mapNodes();
@@ -238,9 +275,21 @@ export function buildOpenDetailScript(params = {}) {
238
275
  return !searchVisible;
239
276
  };
240
277
 
278
+ pushTrace({
279
+ kind: 'scroll',
280
+ stage: 'open_detail',
281
+ noteId: next.noteId,
282
+ via: 'scrollIntoView',
283
+ });
241
284
  next.cover.scrollIntoView({ behavior: 'auto', block: 'center' });
242
285
  await new Promise((resolve) => setTimeout(resolve, 140));
243
286
  const beforeUrl = window.location.href;
287
+ pushTrace({
288
+ kind: 'click',
289
+ stage: 'open_detail',
290
+ noteId: next.noteId,
291
+ selector: 'a.cover',
292
+ });
244
293
  next.cover.click();
245
294
 
246
295
  let detailReady = false;
@@ -274,6 +323,7 @@ export function buildOpenDetailScript(params = {}) {
274
323
  excludedCount: excludedNoteIds.size,
275
324
  seedCollectedCount: seedCollectedSet.size,
276
325
  seedCollectedNoteIds: Array.from(seedCollectedSet),
326
+ actionTrace,
277
327
  };
278
328
  })()`;
279
329
  }