@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.
- package/apps/desktop-console/dist/main/index.mjs +909 -105
- package/apps/desktop-console/dist/main/preload.mjs +3 -0
- package/apps/desktop-console/dist/renderer/index.html +9 -1
- package/apps/desktop-console/dist/renderer/index.js +796 -331
- package/apps/desktop-console/entry/ui-cli.mjs +59 -9
- package/apps/desktop-console/entry/ui-console.mjs +8 -3
- package/apps/webauto/entry/account.mjs +70 -9
- package/apps/webauto/entry/lib/account-detect.mjs +106 -25
- package/apps/webauto/entry/lib/account-store.mjs +122 -35
- package/apps/webauto/entry/lib/profilepool.mjs +45 -13
- package/apps/webauto/entry/lib/schedule-store.mjs +1 -25
- package/apps/webauto/entry/profilepool.mjs +45 -3
- package/apps/webauto/entry/schedule.mjs +44 -2
- package/apps/webauto/entry/weibo-unified.mjs +2 -2
- package/apps/webauto/entry/xhs-install.mjs +248 -52
- package/apps/webauto/entry/xhs-unified.mjs +33 -6
- package/bin/webauto.mjs +137 -5
- package/dist/modules/camo-runtime/src/utils/browser-service.mjs +4 -0
- package/dist/services/unified-api/server.js +5 -0
- package/dist/services/unified-api/task-state.js +2 -0
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +142 -14
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +16 -1
- package/modules/camo-runtime/src/autoscript/action-providers/xhs.mjs +104 -0
- package/modules/camo-runtime/src/autoscript/runtime.mjs +14 -4
- package/modules/camo-runtime/src/autoscript/schema.mjs +9 -0
- package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +9 -2
- package/modules/camo-runtime/src/container/runtime-core/checkpoint.mjs +107 -1
- package/modules/camo-runtime/src/container/runtime-core/subscription.mjs +24 -2
- package/modules/camo-runtime/src/utils/browser-service.mjs +4 -0
- package/package.json +7 -3
- package/runtime/infra/utils/README.md +13 -0
- package/runtime/infra/utils/scripts/README.md +0 -0
- package/runtime/infra/utils/scripts/development/eval-in-session.mjs +40 -0
- package/runtime/infra/utils/scripts/development/highlight-search-containers.mjs +35 -0
- package/runtime/infra/utils/scripts/service/kill-port.mjs +24 -0
- package/runtime/infra/utils/scripts/service/start-api.mjs +103 -0
- package/runtime/infra/utils/scripts/service/start-browser-service.mjs +173 -0
- package/runtime/infra/utils/scripts/service/stop-api.mjs +30 -0
- package/runtime/infra/utils/scripts/service/stop-browser-service.mjs +104 -0
- package/runtime/infra/utils/scripts/test-services.mjs +94 -0
- package/scripts/bump-version.mjs +120 -0
- package/services/unified-api/server.ts +4 -0
- 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
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
63
|
-
|
|
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
|
|
194
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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),
|