@web-auto/webauto 0.1.8 → 0.1.11

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.
Files changed (43) hide show
  1. package/apps/desktop-console/dist/main/index.mjs +909 -105
  2. package/apps/desktop-console/dist/main/preload.mjs +3 -0
  3. package/apps/desktop-console/dist/renderer/index.html +9 -1
  4. package/apps/desktop-console/dist/renderer/index.js +796 -331
  5. package/apps/desktop-console/entry/ui-cli.mjs +59 -9
  6. package/apps/desktop-console/entry/ui-console.mjs +8 -3
  7. package/apps/webauto/entry/account.mjs +70 -9
  8. package/apps/webauto/entry/lib/account-detect.mjs +106 -25
  9. package/apps/webauto/entry/lib/account-store.mjs +122 -35
  10. package/apps/webauto/entry/lib/profilepool.mjs +45 -13
  11. package/apps/webauto/entry/lib/schedule-store.mjs +1 -25
  12. package/apps/webauto/entry/profilepool.mjs +45 -3
  13. package/apps/webauto/entry/schedule.mjs +44 -2
  14. package/apps/webauto/entry/weibo-unified.mjs +2 -2
  15. package/apps/webauto/entry/xhs-install.mjs +248 -52
  16. package/apps/webauto/entry/xhs-unified.mjs +33 -6
  17. package/bin/webauto.mjs +137 -5
  18. package/dist/modules/camo-runtime/src/utils/browser-service.mjs +4 -0
  19. package/dist/services/unified-api/server.js +5 -0
  20. package/dist/services/unified-api/task-state.js +2 -0
  21. package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +142 -14
  22. package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +16 -1
  23. package/modules/camo-runtime/src/autoscript/action-providers/xhs.mjs +104 -0
  24. package/modules/camo-runtime/src/autoscript/runtime.mjs +14 -4
  25. package/modules/camo-runtime/src/autoscript/schema.mjs +9 -0
  26. package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +9 -2
  27. package/modules/camo-runtime/src/container/runtime-core/checkpoint.mjs +107 -1
  28. package/modules/camo-runtime/src/container/runtime-core/subscription.mjs +24 -2
  29. package/modules/camo-runtime/src/utils/browser-service.mjs +4 -0
  30. package/package.json +7 -3
  31. package/runtime/infra/utils/README.md +13 -0
  32. package/runtime/infra/utils/scripts/README.md +0 -0
  33. package/runtime/infra/utils/scripts/development/eval-in-session.mjs +40 -0
  34. package/runtime/infra/utils/scripts/development/highlight-search-containers.mjs +35 -0
  35. package/runtime/infra/utils/scripts/service/kill-port.mjs +24 -0
  36. package/runtime/infra/utils/scripts/service/start-api.mjs +103 -0
  37. package/runtime/infra/utils/scripts/service/start-browser-service.mjs +173 -0
  38. package/runtime/infra/utils/scripts/service/stop-api.mjs +30 -0
  39. package/runtime/infra/utils/scripts/service/stop-browser-service.mjs +104 -0
  40. package/runtime/infra/utils/scripts/test-services.mjs +94 -0
  41. package/scripts/bump-version.mjs +120 -0
  42. package/services/unified-api/server.ts +4 -0
  43. package/services/unified-api/task-state.ts +5 -0
package/bin/webauto.mjs CHANGED
@@ -2,11 +2,67 @@
2
2
  import minimist from 'minimist';
3
3
  import { spawn } from 'node:child_process';
4
4
  import { existsSync, writeFileSync, readFileSync, mkdirSync } from 'node:fs';
5
+ import os from 'node:os';
5
6
  import path from 'node:path';
6
7
  import { fileURLToPath } from 'node:url';
7
8
 
8
9
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
9
- const STATE_FILE = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.webauto', 'cli-state.json');
10
+
11
+ function normalizePathForPlatform(raw, platform = process.platform) {
12
+ const input = String(raw || '').trim();
13
+ const isWinPath = platform === 'win32' || /^[A-Za-z]:[\\/]/.test(input);
14
+ const pathApi = isWinPath ? path.win32 : path;
15
+ return isWinPath ? pathApi.normalize(input) : path.resolve(input);
16
+ }
17
+
18
+ function normalizeLegacyWebautoRoot(raw, platform = process.platform) {
19
+ const pathApi = platform === 'win32' ? path.win32 : path;
20
+ const resolved = normalizePathForPlatform(raw, platform);
21
+ const base = pathApi.basename(resolved).toLowerCase();
22
+ return (base === '.webauto' || base === 'webauto')
23
+ ? resolved
24
+ : pathApi.join(resolved, '.webauto');
25
+ }
26
+
27
+ function resolveWebautoHome(env = process.env, platform = process.platform) {
28
+ const explicitHome = String(env.WEBAUTO_HOME || '').trim();
29
+ if (explicitHome) return normalizePathForPlatform(explicitHome, platform);
30
+
31
+ const legacyRoot = String(env.WEBAUTO_ROOT || env.WEBAUTO_PORTABLE_ROOT || '').trim();
32
+ if (legacyRoot) return normalizeLegacyWebautoRoot(legacyRoot, platform);
33
+
34
+ const homeDir = platform === 'win32'
35
+ ? (env.USERPROFILE || os.homedir())
36
+ : (env.HOME || os.homedir());
37
+ if (platform === 'win32') {
38
+ try {
39
+ if (existsSync('D:\\')) return 'D:\\webauto';
40
+ } catch {
41
+ // ignore drive detection errors
42
+ }
43
+ return path.win32.join(homeDir, '.webauto');
44
+ }
45
+ return path.join(homeDir, '.webauto');
46
+ }
47
+
48
+ function applyDefaultRuntimeEnv() {
49
+ if (!String(process.env.WEBAUTO_REPO_ROOT || '').trim()) {
50
+ process.env.WEBAUTO_REPO_ROOT = ROOT;
51
+ }
52
+ if (isGlobalInstall() && !String(process.env.WEBAUTO_SKIP_BUILD_CHECK || '').trim()) {
53
+ process.env.WEBAUTO_SKIP_BUILD_CHECK = '1';
54
+ }
55
+ if (
56
+ !String(process.env.WEBAUTO_HOME || '').trim()
57
+ && !String(process.env.WEBAUTO_ROOT || process.env.WEBAUTO_PORTABLE_ROOT || '').trim()
58
+ ) {
59
+ process.env.WEBAUTO_HOME = resolveWebautoHome();
60
+ }
61
+ }
62
+
63
+ applyDefaultRuntimeEnv();
64
+
65
+ const STATE_FILE = path.join(resolveWebautoHome(), 'cli-state.json');
10
66
 
11
67
  function loadState() {
12
68
  try {
@@ -78,6 +134,17 @@ function uiConsoleScriptPath() {
78
134
  return path.join(ROOT, 'apps', 'desktop-console', 'entry', 'ui-console.mjs');
79
135
  }
80
136
 
137
+ function readRootVersion() {
138
+ try {
139
+ const pkg = JSON.parse(readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
140
+ return String(pkg.version || '').trim() || '0.0.0';
141
+ } catch {
142
+ return '0.0.0';
143
+ }
144
+ }
145
+
146
+ const ROOT_VERSION = readRootVersion();
147
+
81
148
  function printMainHelp() {
82
149
  console.log(`webauto CLI
83
150
 
@@ -100,10 +167,14 @@ Core Commands:
100
167
  webauto xhs unified [xhs options...]
101
168
  webauto xhs status [--run-id <id>] [--json]
102
169
  webauto xhs orchestrate [xhs options...]
170
+ webauto version [--json]
171
+ webauto version bump [patch|minor|major]
103
172
 
104
173
  Build & Release:
105
174
  webauto build:dev # Local link mode
106
- webauto build:release # Full release gate (tests/build/pack)
175
+ webauto build:release # Full release gate (默认自动 bump patch 版本)
176
+ webauto build:release -- --bump minor
177
+ webauto build:release -- --no-bump
107
178
  webauto build:release -- --skip-tests
108
179
  webauto build:release -- --skip-pack
109
180
 
@@ -132,6 +203,7 @@ Tips:
132
203
  - account 命令会转发到 apps/webauto/entry/account.mjs
133
204
  - schedule 命令会转发到 apps/webauto/entry/schedule.mjs
134
205
  - 全量参数请看: webauto xhs --help
206
+ - 当前 CLI 版本: ${ROOT_VERSION}
135
207
  `);
136
208
  }
137
209
 
@@ -342,6 +414,21 @@ Examples:
342
414
  `);
343
415
  }
344
416
 
417
+ function printVersionHelp() {
418
+ console.log(`webauto version
419
+
420
+ Usage:
421
+ webauto version [--json]
422
+ webauto version bump [patch|minor|major] [--json]
423
+
424
+ Examples:
425
+ webauto version
426
+ webauto version --json
427
+ webauto version bump
428
+ webauto version bump minor
429
+ `);
430
+ }
431
+
345
432
  function exists(p) {
346
433
  try {
347
434
  return existsSync(p);
@@ -454,6 +541,7 @@ async function ensureDepsAndBuild() {
454
541
  }
455
542
 
456
543
  async function uiConsole({ build, install, checkOnly, noDaemon }) {
544
+ console.log(`[webauto] version ${ROOT_VERSION}`);
457
545
  const okServices = checkServicesBuilt();
458
546
  const okDeps = checkDesktopConsoleDeps();
459
547
  const okUiBuilt = checkDesktopConsoleBuilt();
@@ -517,12 +605,14 @@ async function uiConsole({ build, install, checkOnly, noDaemon }) {
517
605
  async function main() {
518
606
  const rawArgv = process.argv.slice(2);
519
607
  const args = minimist(process.argv.slice(2), {
520
- boolean: ['help', 'build', 'install', 'check', 'full', 'link', 'skip-tests', 'skip-pack', 'no-daemon'],
608
+ boolean: ['help', 'build', 'install', 'check', 'full', 'link', 'skip-tests', 'skip-pack', 'no-daemon', 'no-bump', 'json'],
609
+ string: ['bump'],
521
610
  alias: { h: 'help' },
522
611
  });
523
612
 
524
613
  const cmd = String(args._[0] || '').trim();
525
614
  const sub = String(args._[1] || '').trim();
615
+ const noDaemon = rawArgv.includes('--no-daemon') || rawArgv.includes('--foreground') || args['no-daemon'] === true;
526
616
 
527
617
  if (args.help) {
528
618
  if (cmd === 'account') {
@@ -551,6 +641,10 @@ async function main() {
551
641
  printXhsHelp();
552
642
  return;
553
643
  }
644
+ if (cmd === 'version') {
645
+ printVersionHelp();
646
+ return;
647
+ }
554
648
  printMainHelp();
555
649
  return;
556
650
  }
@@ -560,11 +654,37 @@ async function main() {
560
654
  build: false,
561
655
  install: false,
562
656
  checkOnly: false,
563
- noDaemon: args['no-daemon'] === true,
657
+ noDaemon,
564
658
  });
565
659
  return;
566
660
  }
567
661
 
662
+ if (cmd === 'version') {
663
+ const jsonMode = args.json === true;
664
+ const action = String(args._[1] || '').trim();
665
+ if (!action) {
666
+ const out = { name: '@web-auto/webauto', version: ROOT_VERSION };
667
+ if (jsonMode) console.log(JSON.stringify(out, null, 2));
668
+ else console.log(`@web-auto/webauto v${ROOT_VERSION}`);
669
+ return;
670
+ }
671
+ if (action !== 'bump') {
672
+ console.error(`Unknown version action: ${action}`);
673
+ printVersionHelp();
674
+ process.exit(2);
675
+ }
676
+ const bumpType = String(args._[2] || args.bump || 'patch').trim().toLowerCase();
677
+ if (!['patch', 'minor', 'major'].includes(bumpType)) {
678
+ console.error(`Unsupported bump type: ${bumpType}`);
679
+ process.exit(2);
680
+ }
681
+ const script = path.join(ROOT, 'scripts', 'bump-version.mjs');
682
+ const cmdArgs = [script, bumpType];
683
+ if (jsonMode) cmdArgs.push('--json');
684
+ await run(process.execPath, cmdArgs);
685
+ return;
686
+ }
687
+
568
688
  // build:dev - local development mode
569
689
  if (cmd === 'build:dev') {
570
690
  console.log('[webauto] Running local dev setup...');
@@ -580,8 +700,20 @@ async function main() {
580
700
  if (cmd === 'build:release') {
581
701
  const skipTests = args['skip-tests'] === true;
582
702
  const skipPack = args['skip-pack'] === true;
703
+ const noBump = args['no-bump'] === true;
704
+ const bumpType = String(args.bump || 'patch').trim().toLowerCase();
705
+ if (!['patch', 'minor', 'major'].includes(bumpType)) {
706
+ console.error(`Unsupported --bump value: ${bumpType}`);
707
+ process.exit(2);
708
+ }
583
709
  console.log('[webauto] Running release gate...');
584
710
  const npm = npmRunner();
711
+ if (!noBump) {
712
+ const bumpScript = path.join(ROOT, 'scripts', 'bump-version.mjs');
713
+ await run(process.execPath, [bumpScript, bumpType]);
714
+ } else {
715
+ console.log('[webauto] Skip version bump (--no-bump)');
716
+ }
585
717
  await run(npm.cmd, [...npm.prefix, 'run', 'prebuild']);
586
718
  if (!skipTests) {
587
719
  await run(npm.cmd, [...npm.prefix, 'run', 'test:ci']);
@@ -608,7 +740,7 @@ async function main() {
608
740
  build: args.build === true,
609
741
  install: args.install === true,
610
742
  checkOnly: args.check === true,
611
- noDaemon: args['no-daemon'] === true,
743
+ noDaemon,
612
744
  });
613
745
  return;
614
746
  }
@@ -213,6 +213,7 @@ function buildDomSnapshotScript(maxDepth, maxChildren) {
213
213
  const root = collect(document.body || document.documentElement, 0, 'root');
214
214
  return {
215
215
  dom_tree: root,
216
+ current_url: String(window.location.href || ''),
216
217
  viewport: {
217
218
  width: viewportWidth,
218
219
  height: viewportHeight,
@@ -235,6 +236,9 @@ export async function getDomSnapshotByProfile(profileId, options = {}) {
235
236
  height: Number(payload.viewport.height) || 0,
236
237
  };
237
238
  }
239
+ if (tree && payload.current_url) {
240
+ tree.__url = String(payload.current_url);
241
+ }
238
242
  return tree;
239
243
  }
240
244
  export async function getViewportByProfile(profileId) {
@@ -274,11 +274,13 @@ class UnifiedApiServer {
274
274
  return existing;
275
275
  const profileId = String(seed?.profileId || 'unknown').trim() || 'unknown';
276
276
  const keyword = String(seed?.keyword || '').trim();
277
+ const uiTriggerId = String(seed?.uiTriggerId || seed?.triggerId || '').trim();
277
278
  const phase = normalizeTaskPhase(seed?.phase);
278
279
  return this.taskRegistry.createTask({
279
280
  runId: normalizedRunId,
280
281
  profileId,
281
282
  keyword,
283
+ uiTriggerId,
282
284
  phase,
283
285
  });
284
286
  };
@@ -290,6 +292,7 @@ class UnifiedApiServer {
290
292
  const phase = normalizeTaskPhase(payload?.phase);
291
293
  const profileId = String(payload?.profileId || '').trim();
292
294
  const keyword = String(payload?.keyword || '').trim();
295
+ const uiTriggerId = String(payload?.uiTriggerId || payload?.triggerId || '').trim();
293
296
  const details = payload?.details && typeof payload.details === 'object' ? payload.details : undefined;
294
297
  const patch = {};
295
298
  if (phase !== 'unknown')
@@ -298,6 +301,8 @@ class UnifiedApiServer {
298
301
  patch.profileId = profileId;
299
302
  if (keyword)
300
303
  patch.keyword = keyword;
304
+ if (uiTriggerId)
305
+ patch.uiTriggerId = uiTriggerId;
301
306
  if (details)
302
307
  patch.details = details;
303
308
  if (Object.keys(patch).length > 0) {
@@ -11,6 +11,7 @@ class TaskStateRegistry {
11
11
  runId: partial.runId,
12
12
  profileId: partial.profileId,
13
13
  keyword: partial.keyword,
14
+ uiTriggerId: partial.uiTriggerId ? String(partial.uiTriggerId).trim() : undefined,
14
15
  phase: partial.phase || 'unknown',
15
16
  status: 'starting',
16
17
  progress: { total: 0, processed: 0, failed: 0 },
@@ -22,6 +23,7 @@ class TaskStateRegistry {
22
23
  imagesDownloaded: 0,
23
24
  ocrProcessed: 0,
24
25
  },
26
+ createdAt: now,
25
27
  startedAt: now,
26
28
  updatedAt: now,
27
29
  details: {
@@ -59,12 +59,10 @@ function buildCollectLikeTargetsScript() {
59
59
  const className = String(node.className || '').toLowerCase();
60
60
  const ariaPressed = String(node.getAttribute?.('aria-pressed') || '').toLowerCase();
61
61
  const text = String(node.textContent || '');
62
- const useNode = node.querySelector('use');
63
- const useHref = String(useNode?.getAttribute?.('xlink:href') || useNode?.getAttribute?.('href') || '').toLowerCase();
64
- return className.includes('like-active')
62
+ const hasActiveClass = /(?:^|\\s)like-active(?:\\s|$)/.test(className);
63
+ return hasActiveClass
65
64
  || ariaPressed === 'true'
66
- || /已赞|取消赞/.test(text)
67
- || useHref.includes('liked');
65
+ || /已赞|取消赞/.test(text);
68
66
  };
69
67
  const readText = (item, selectors) => {
70
68
  for (const selector of selectors) {
@@ -164,7 +162,7 @@ function buildCollectLikeTargetsScript() {
164
162
  })()`;
165
163
  }
166
164
 
167
- function buildClickLikeByIndexScript(index, highlight) {
165
+ function buildClickLikeByIndexScript(index, highlight, skipAlreadyCheck = false) {
168
166
  return `(async () => {
169
167
  const idx = Number(${JSON.stringify(index)});
170
168
  const items = Array.from(document.querySelectorAll('.comment-item'));
@@ -190,18 +188,16 @@ function buildClickLikeByIndexScript(index, highlight) {
190
188
  const className = String(node.className || '').toLowerCase();
191
189
  const ariaPressed = String(node.getAttribute?.('aria-pressed') || '').toLowerCase();
192
190
  const text = String(node.textContent || '');
193
- const useNode = node.querySelector('use');
194
- const useHref = String(useNode?.getAttribute?.('xlink:href') || useNode?.getAttribute?.('href') || '').toLowerCase();
195
- return className.includes('like-active')
191
+ const hasActiveClass = /(?:^|\\s)like-active(?:\\s|$)/.test(className);
192
+ return hasActiveClass
196
193
  || ariaPressed === 'true'
197
- || /已赞|取消赞/.test(text)
198
- || useHref.includes('liked');
194
+ || /已赞|取消赞/.test(text);
199
195
  };
200
196
 
201
197
  const likeControl = findLikeControl(item);
202
198
  if (!likeControl) return { clicked: false, reason: 'like_control_not_found', index: idx };
203
199
  const beforeLiked = isAlreadyLiked(likeControl);
204
- if (beforeLiked) {
200
+ if (beforeLiked && !${skipAlreadyCheck ? 'true' : 'false'}) {
205
201
  return { clicked: false, alreadyLiked: true, reason: 'already_liked', index: idx };
206
202
  }
207
203
  item.scrollIntoView({ behavior: 'auto', block: 'center' });
@@ -216,7 +212,7 @@ function buildClickLikeByIndexScript(index, highlight) {
216
212
  await new Promise((resolve) => setTimeout(resolve, 220));
217
213
  return {
218
214
  clicked: true,
219
- alreadyLiked: false,
215
+ alreadyLiked: beforeLiked,
220
216
  likedAfter: isAlreadyLiked(likeControl),
221
217
  reason: 'clicked',
222
218
  index: idx,
@@ -244,6 +240,7 @@ export async function executeCommentLikeOperation({ profileId, params = {} }) {
244
240
  const saveEvidence = params.saveEvidence !== false;
245
241
  const persistLikeState = params.persistLikeState !== false;
246
242
  const persistComments = params.persistComments === true || params.persistCollectedComments === true;
243
+ const fallbackPickOne = params.pickOneIfNoNew !== false;
247
244
 
248
245
  const stateRaw = await runEvaluateScript({
249
246
  profileId,
@@ -273,7 +270,8 @@ export async function executeCommentLikeOperation({ profileId, params = {} }) {
273
270
  const likedSignatures = persistLikeState ? await loadLikedSignatures(output.likeStatePath) : new Set();
274
271
  const likedComments = [];
275
272
  const matchedByStateCount = Number(collected.matchedByStateCount || 0);
276
- const useStateMatches = matchedByStateCount > 0;
273
+ // If explicit like rules are provided, honor them instead of inheriting state matches.
274
+ const useStateMatches = matchedByStateCount > 0 && rules.length === 0;
277
275
 
278
276
  let hitCount = 0;
279
277
  let likedCount = 0;
@@ -411,6 +409,136 @@ export async function executeCommentLikeOperation({ profileId, params = {} }) {
411
409
  });
412
410
  }
413
411
 
412
+ if (!dryRun && fallbackPickOne && likedCount < maxLikes) {
413
+ for (const row of rows) {
414
+ if (likedCount >= maxLikes) break;
415
+ if (!row || typeof row !== 'object') continue;
416
+ const text = normalizeText(row.text);
417
+ if (!text) continue;
418
+
419
+ const signature = makeLikeSignature({
420
+ noteId: output.noteId,
421
+ userId: String(row.userId || ''),
422
+ userName: String(row.userName || ''),
423
+ text,
424
+ });
425
+ if (!row.hasLikeControl) {
426
+ missingLikeControl += 1;
427
+ continue;
428
+ }
429
+
430
+ hitCount += 1;
431
+ if (row.alreadyLiked) {
432
+ alreadyLikedSkipped += 1;
433
+ if (persistLikeState && signature) {
434
+ likedSignatures.add(signature);
435
+ await appendLikedSignature(output.likeStatePath, signature, {
436
+ noteId: output.noteId,
437
+ userId: String(row.userId || ''),
438
+ userName: String(row.userName || ''),
439
+ reason: 'already_liked_fallback',
440
+ }).catch(() => null);
441
+ }
442
+ continue;
443
+ }
444
+
445
+ const beforePath = saveEvidence
446
+ ? await captureScreenshotToFile({
447
+ profileId,
448
+ filePath: path.join(evidenceDir, `like-before-fallback-idx-${String(row.index).padStart(3, '0')}-${Date.now()}.png`),
449
+ })
450
+ : null;
451
+ const clickRaw = await runEvaluateScript({
452
+ profileId,
453
+ script: buildClickLikeByIndexScript(row.index, highlight, true),
454
+ highlight: false,
455
+ });
456
+ const clickResult = extractEvaluateResultData(clickRaw) || {};
457
+ const afterPath = saveEvidence
458
+ ? await captureScreenshotToFile({
459
+ profileId,
460
+ filePath: path.join(evidenceDir, `like-after-fallback-idx-${String(row.index).padStart(3, '0')}-${Date.now()}.png`),
461
+ })
462
+ : null;
463
+
464
+ if (clickResult.alreadyLiked) {
465
+ alreadyLikedSkipped += 1;
466
+ if (persistLikeState && signature) {
467
+ likedSignatures.add(signature);
468
+ await appendLikedSignature(output.likeStatePath, signature, {
469
+ noteId: output.noteId,
470
+ userId: String(row.userId || ''),
471
+ userName: String(row.userName || ''),
472
+ reason: 'already_liked_after_click_fallback',
473
+ }).catch(() => null);
474
+ }
475
+ continue;
476
+ }
477
+ if (!clickResult.clicked) {
478
+ clickFailed += 1;
479
+ continue;
480
+ }
481
+ if (!clickResult.likedAfter) {
482
+ if (clickResult.alreadyLiked) {
483
+ // Fallback proof path: toggle back to keep original state, but keep one verified candidate.
484
+ await runEvaluateScript({
485
+ profileId,
486
+ script: buildClickLikeByIndexScript(row.index, false, true),
487
+ highlight: false,
488
+ }).catch(() => null);
489
+ likedCount += 1;
490
+ likedComments.push({
491
+ index: Number(row.index),
492
+ userId: String(row.userId || ''),
493
+ userName: String(row.userName || ''),
494
+ content: text,
495
+ timestamp: String(row.timestamp || ''),
496
+ matchedRule: 'fallback_already_liked_verified',
497
+ screenshots: {
498
+ before: beforePath,
499
+ after: afterPath,
500
+ },
501
+ });
502
+ if (persistLikeState && signature) {
503
+ likedSignatures.add(signature);
504
+ await appendLikedSignature(output.likeStatePath, signature, {
505
+ noteId: output.noteId,
506
+ userId: String(row.userId || ''),
507
+ userName: String(row.userName || ''),
508
+ reason: 'already_liked_verified',
509
+ }).catch(() => null);
510
+ }
511
+ break;
512
+ }
513
+ verifyFailed += 1;
514
+ continue;
515
+ }
516
+
517
+ likedCount += 1;
518
+ if (persistLikeState && signature) {
519
+ likedSignatures.add(signature);
520
+ await appendLikedSignature(output.likeStatePath, signature, {
521
+ noteId: output.noteId,
522
+ userId: String(row.userId || ''),
523
+ userName: String(row.userName || ''),
524
+ reason: 'liked_fallback',
525
+ }).catch(() => null);
526
+ }
527
+ likedComments.push({
528
+ index: Number(row.index),
529
+ userId: String(row.userId || ''),
530
+ userName: String(row.userName || ''),
531
+ content: text,
532
+ timestamp: String(row.timestamp || ''),
533
+ matchedRule: 'fallback_first_available',
534
+ screenshots: {
535
+ before: beforePath,
536
+ after: afterPath,
537
+ },
538
+ });
539
+ }
540
+ }
541
+
414
542
  const skippedCount = missingLikeControl + clickFailed + verifyFailed;
415
543
  const likedTotal = likedCount + dedupSkipped + alreadyLikedSkipped;
416
544
  const hitCheckOk = likedTotal + skippedCount === hitCount;
@@ -213,6 +213,12 @@ export function buildOpenDetailScript(params = {}) {
213
213
  '.note-detail-mask .interaction-container',
214
214
  '.note-detail-mask .comments-container',
215
215
  ];
216
+ const searchSelectors = [
217
+ '.note-item',
218
+ '.search-result-list',
219
+ '#search-input',
220
+ '.feeds-page',
221
+ ];
216
222
  const isVisible = (node) => {
217
223
  if (!node || !(node instanceof HTMLElement)) return false;
218
224
  const style = window.getComputedStyle(node);
@@ -221,7 +227,16 @@ export function buildOpenDetailScript(params = {}) {
221
227
  const rect = node.getBoundingClientRect();
222
228
  return rect.width > 1 && rect.height > 1;
223
229
  };
224
- const isDetailReady = () => detailSelectors.some((selector) => isVisible(document.querySelector(selector)));
230
+ const hasVisible = (selectors) => selectors.some((selector) => isVisible(document.querySelector(selector)));
231
+ const isDetailReady = () => {
232
+ const detailVisible = hasVisible(detailSelectors);
233
+ if (!detailVisible) return false;
234
+ const href = String(window.location.href || '');
235
+ const isDetailUrl = href.includes('/explore/') && !href.includes('/search_result');
236
+ if (isDetailUrl) return true;
237
+ const searchVisible = hasVisible(searchSelectors);
238
+ return !searchVisible;
239
+ };
225
240
 
226
241
  next.cover.scrollIntoView({ behavior: 'auto', block: 'center' });
227
242
  await new Promise((resolve) => setTimeout(resolve, 140));
@@ -253,6 +253,109 @@ async function readXhsRuntimeState(profileId) {
253
253
  }
254
254
  }
255
255
 
256
+ function buildAssertLoggedInScript(params = {}) {
257
+ const selectors = Array.isArray(params.loginSelectors) && params.loginSelectors.length > 0
258
+ ? params.loginSelectors.map((item) => String(item || '').trim()).filter(Boolean)
259
+ : [
260
+ '.login-container',
261
+ '.login-dialog',
262
+ '#login-container',
263
+ ];
264
+ const loginPattern = String(
265
+ params.loginPattern || '登录|扫码|验证码|手机号|请先登录|注册|sign\\s*in',
266
+ ).trim();
267
+
268
+ return `(() => {
269
+ const guardSelectors = ${JSON.stringify(selectors)};
270
+ const loginPattern = new RegExp(${JSON.stringify(loginPattern || '登录|扫码|验证码|手机号|请先登录|注册|sign\\\\s*in')}, 'i');
271
+
272
+ const normalize = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
273
+ const isVisible = (node) => {
274
+ if (!(node instanceof HTMLElement)) return false;
275
+ const style = window.getComputedStyle(node);
276
+ if (!style) return false;
277
+ if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') === 0) return false;
278
+ const rect = node.getBoundingClientRect();
279
+ return rect.width > 0 && rect.height > 0;
280
+ };
281
+
282
+ const guardNodes = guardSelectors.flatMap((selector) => Array.from(document.querySelectorAll(selector)));
283
+ const visibleGuardNodes = guardNodes.filter((node) => isVisible(node));
284
+ const guardTexts = visibleGuardNodes
285
+ .slice(0, 10)
286
+ .map((node) => normalize(node.textContent || ''))
287
+ .filter(Boolean);
288
+ const mergedGuardText = guardTexts.join(' ');
289
+ const hasLoginText = loginPattern.test(mergedGuardText);
290
+ const loginUrl = /\\/login|signin|passport|account\\/login/i.test(String(location.href || ''));
291
+
292
+ let accountId = '';
293
+ try {
294
+ const initialState = (typeof window !== 'undefined' && window.__INITIAL_STATE__) || null;
295
+ const rawUserInfo = initialState && initialState.user && initialState.user.userInfo
296
+ ? (
297
+ (initialState.user.userInfo._rawValue && typeof initialState.user.userInfo._rawValue === 'object' && initialState.user.userInfo._rawValue)
298
+ || (initialState.user.userInfo._value && typeof initialState.user.userInfo._value === 'object' && initialState.user.userInfo._value)
299
+ || (typeof initialState.user.userInfo === 'object' ? initialState.user.userInfo : null)
300
+ )
301
+ : null;
302
+ accountId = normalize(rawUserInfo?.user_id || rawUserInfo?.userId || '');
303
+ } catch {}
304
+
305
+ if (!accountId) {
306
+ const selfAnchor = Array.from(document.querySelectorAll('a[href*="/user/profile/"]'))
307
+ .find((node) => {
308
+ const text = normalize(node.textContent || '');
309
+ const title = normalize(node.getAttribute('title') || '');
310
+ const aria = normalize(node.getAttribute('aria-label') || '');
311
+ return ['我', '我的', '个人主页', '我的主页'].includes(text)
312
+ || ['我', '我的', '个人主页', '我的主页'].includes(title)
313
+ || ['我', '我的', '个人主页', '我的主页'].includes(aria);
314
+ });
315
+ if (selfAnchor) {
316
+ const href = normalize(selfAnchor.getAttribute('href') || '');
317
+ const matched = href.match(/\\/user\\/profile\\/([^/?#]+)/);
318
+ if (matched && matched[1]) accountId = normalize(matched[1]);
319
+ }
320
+ }
321
+
322
+ const hasAccountSignal = Boolean(accountId);
323
+ const hasLoginGuard = (visibleGuardNodes.length > 0 && hasLoginText) || loginUrl;
324
+
325
+ return {
326
+ hasLoginGuard,
327
+ hasAccountSignal,
328
+ accountId: accountId || null,
329
+ url: String(location.href || ''),
330
+ visibleGuardCount: visibleGuardNodes.length,
331
+ guardTextPreview: mergedGuardText.slice(0, 240),
332
+ loginUrl,
333
+ hasLoginText,
334
+ guardSelectors,
335
+ };
336
+ })()`;
337
+ }
338
+
339
+ async function executeAssertLoggedInOperation({ profileId, params = {} }) {
340
+ const highlight = params.highlight !== false;
341
+ const payload = await runEvaluateScript({
342
+ profileId,
343
+ script: buildAssertLoggedInScript(params),
344
+ highlight,
345
+ });
346
+ const data = extractEvaluateResultData(payload) || {};
347
+ if (data?.hasLoginGuard === true) {
348
+ const code = String(params.code || 'LOGIN_GUARD_DETECTED').trim() || 'LOGIN_GUARD_DETECTED';
349
+ return asErrorPayload('OPERATION_FAILED', code, { guard: data });
350
+ }
351
+ return {
352
+ ok: true,
353
+ code: 'OPERATION_DONE',
354
+ message: 'xhs_assert_logged_in done',
355
+ data,
356
+ };
357
+ }
358
+
256
359
  async function handleRaiseError({ params }) {
257
360
  const code = String(params.code || params.message || 'AUTOSCRIPT_ABORT').trim();
258
361
  return asErrorPayload('OPERATION_FAILED', code || 'AUTOSCRIPT_ABORT');
@@ -314,6 +417,7 @@ async function executeCommentsHarvestOperation({ profileId, params = {} }) {
314
417
 
315
418
  const XHS_ACTION_HANDLERS = {
316
419
  raise_error: handleRaiseError,
420
+ xhs_assert_logged_in: executeAssertLoggedInOperation,
317
421
  xhs_submit_search: executeSubmitSearchOperation,
318
422
  xhs_open_detail: executeOpenDetailOperation,
319
423
  xhs_detail_harvest: createEvaluateHandler('xhs_detail_harvest done', buildDetailHarvestScript),