@web-auto/webauto 0.1.8 → 0.1.9

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 (32) hide show
  1. package/apps/desktop-console/dist/main/index.mjs +800 -89
  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 +784 -331
  5. package/apps/desktop-console/entry/ui-cli.mjs +23 -8
  6. package/apps/desktop-console/entry/ui-console.mjs +8 -3
  7. package/apps/webauto/entry/account.mjs +69 -8
  8. package/apps/webauto/entry/lib/account-detect.mjs +106 -25
  9. package/apps/webauto/entry/lib/account-store.mjs +121 -22
  10. package/apps/webauto/entry/lib/schedule-store.mjs +0 -12
  11. package/apps/webauto/entry/profilepool.mjs +45 -3
  12. package/apps/webauto/entry/schedule.mjs +44 -2
  13. package/apps/webauto/entry/weibo-unified.mjs +2 -2
  14. package/apps/webauto/entry/xhs-install.mjs +220 -51
  15. package/apps/webauto/entry/xhs-unified.mjs +33 -6
  16. package/bin/webauto.mjs +80 -4
  17. package/dist/modules/camo-runtime/src/utils/browser-service.mjs +4 -0
  18. package/dist/services/unified-api/server.js +5 -0
  19. package/dist/services/unified-api/task-state.js +2 -0
  20. package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +142 -14
  21. package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +16 -1
  22. package/modules/camo-runtime/src/autoscript/action-providers/xhs.mjs +104 -0
  23. package/modules/camo-runtime/src/autoscript/runtime.mjs +14 -4
  24. package/modules/camo-runtime/src/autoscript/schema.mjs +9 -0
  25. package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +9 -2
  26. package/modules/camo-runtime/src/container/runtime-core/checkpoint.mjs +107 -1
  27. package/modules/camo-runtime/src/container/runtime-core/subscription.mjs +24 -2
  28. package/modules/camo-runtime/src/utils/browser-service.mjs +4 -0
  29. package/package.json +6 -3
  30. package/scripts/bump-version.mjs +120 -0
  31. package/services/unified-api/server.ts +4 -0
  32. package/services/unified-api/task-state.ts +5 -0
@@ -3,34 +3,213 @@ import minimist from 'minimist';
3
3
  import { spawnSync } from 'node:child_process';
4
4
  import os from 'node:os';
5
5
  import path from 'node:path';
6
- import { existsSync, rmSync } from 'node:fs';
6
+ import { existsSync, rmSync, mkdirSync, writeFileSync, renameSync, statSync } from 'node:fs';
7
7
  import { fileURLToPath } from 'node:url';
8
8
 
9
- function run(cmd, args) {
10
- const lower = String(cmd || '').toLowerCase();
9
+ const DEFAULT_COMMAND_TIMEOUT_MS = (() => {
10
+ const raw = Number(process.env.WEBAUTO_INSTALL_TIMEOUT_MS || 600000);
11
+ return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 600000;
12
+ })();
13
+
14
+ function run(cmd, args, options = {}) {
15
+ const command = normalizeCommand(cmd);
16
+ const timeoutMs = Number.isFinite(Number(options?.timeoutMs))
17
+ ? Math.max(1000, Number(options.timeoutMs))
18
+ : DEFAULT_COMMAND_TIMEOUT_MS;
19
+ const lower = String(command || '').toLowerCase();
11
20
  const spawnOptions = {
12
21
  encoding: 'utf8',
13
22
  windowsHide: true,
14
- timeout: 120000,
23
+ timeout: timeoutMs,
15
24
  };
16
25
  if (process.platform === 'win32' && (lower.endsWith('.cmd') || lower.endsWith('.bat'))) {
17
- const cmdLine = [quoteCmdArg(cmd), ...args.map(quoteCmdArg)].join(' ');
26
+ const cmdLine = [quoteCmdArg(command), ...args.map(quoteCmdArg)].join(' ');
18
27
  return spawnSync('cmd.exe', ['/d', '/s', '/c', cmdLine], spawnOptions);
19
28
  }
20
29
  if (process.platform === 'win32' && lower.endsWith('.ps1')) {
21
30
  return spawnSync(
22
31
  'powershell.exe',
23
- ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', cmd, ...args],
32
+ ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', command, ...args],
24
33
  spawnOptions,
25
34
  );
26
35
  }
27
- return spawnSync(cmd, args, spawnOptions);
36
+ return spawnSync(command, args, spawnOptions);
28
37
  }
29
38
 
30
39
  function quoteCmdArg(value) {
31
- if (!value) return '""';
32
- if (!/[\s"]/u.test(value)) return value;
33
- return `"${String(value).replace(/"/g, '""')}"`;
40
+ const normalized = normalizeCommand(value);
41
+ if (!normalized) return '""';
42
+ if (!/[\s"]/u.test(normalized)) return normalized;
43
+ return `"${String(normalized).replace(/"/g, '""')}"`;
44
+ }
45
+
46
+ function normalizeCommand(value) {
47
+ const text = String(value || '').trim();
48
+ if (!text) return '';
49
+ if ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'"))) {
50
+ return text.slice(1, -1).trim();
51
+ }
52
+ return text;
53
+ }
54
+
55
+ function hasValidGeoIPFile(filePath) {
56
+ try {
57
+ if (!existsSync(filePath)) return false;
58
+ const stat = statSync(filePath);
59
+ return Number(stat?.size || 0) > 1024;
60
+ } catch {
61
+ return false;
62
+ }
63
+ }
64
+
65
+ function stripAnsi(input) {
66
+ return String(input || '').replace(/\x1b\[[0-9;]*m/g, '');
67
+ }
68
+
69
+ function resolvePathFromOutput(stdout, stderr) {
70
+ const merged = `${stripAnsi(stdout)}\n${stripAnsi(stderr)}`;
71
+ const lines = merged
72
+ .split(/\r?\n/)
73
+ .map((x) => x.trim())
74
+ .filter(Boolean);
75
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
76
+ const line = lines[i];
77
+ if (line.startsWith('/') || /^[A-Z]:\\/i.test(line)) return line;
78
+ }
79
+ return '';
80
+ }
81
+
82
+ function commandReportsExistingPath(ret) {
83
+ if (!ret || ret.status !== 0) return false;
84
+ const resolvedPath = resolvePathFromOutput(ret.stdout, ret.stderr);
85
+ if (!resolvedPath) return false;
86
+ if (!existsSync(resolvedPath)) return false;
87
+ const executable =
88
+ process.platform === 'win32'
89
+ ? path.join(resolvedPath, 'camoufox.exe')
90
+ : path.join(resolvedPath, 'camoufox');
91
+ return existsSync(executable);
92
+ }
93
+
94
+ function summarizeCommand(ret) {
95
+ if (!ret) return '';
96
+ const bits = [
97
+ ret?.error?.message || '',
98
+ stripAnsi(ret?.stderr || '').trim(),
99
+ stripAnsi(ret?.stdout || '').trim(),
100
+ ].filter(Boolean);
101
+ return bits.length ? bits.join(' | ').slice(0, 900) : '';
102
+ }
103
+
104
+ function runCamoufoxCommand(args) {
105
+ const pyArgs = ['-m', 'camoufox', ...args];
106
+ const candidates =
107
+ process.platform === 'win32'
108
+ ? [
109
+ { cmd: 'camoufox', args },
110
+ { cmd: 'python', args: pyArgs },
111
+ { cmd: 'py', args: ['-3', ...pyArgs] },
112
+ ]
113
+ : [
114
+ { cmd: 'camoufox', args },
115
+ { cmd: 'python3', args: pyArgs },
116
+ ];
117
+ const attempts = [];
118
+ for (const candidate of candidates) {
119
+ const ret = run(candidate.cmd, candidate.args);
120
+ attempts.push({
121
+ command: `${candidate.cmd} ${candidate.args.join(' ')}`.trim(),
122
+ status: ret?.status ?? null,
123
+ error: ret?.error?.message || null,
124
+ detail: summarizeCommand(ret),
125
+ });
126
+ if (ret.status === 0) return { ok: true, ret, attempts };
127
+ }
128
+ const viaPackage = runPackageCommand('camoufox', ['camoufox', ...args]);
129
+ attempts.push({
130
+ command: `npx/npm exec camoufox ${args.join(' ')}`.trim(),
131
+ status: viaPackage?.status ?? null,
132
+ error: viaPackage?.error?.message || null,
133
+ detail: summarizeCommand(viaPackage),
134
+ });
135
+ return { ok: viaPackage.status === 0, ret: viaPackage, attempts };
136
+ }
137
+
138
+ function runCamoCommand(args) {
139
+ const candidates = [{ cmd: 'camo', args }];
140
+ const attempts = [];
141
+ for (const candidate of candidates) {
142
+ const ret = run(candidate.cmd, candidate.args);
143
+ attempts.push({
144
+ command: `${candidate.cmd} ${candidate.args.join(' ')}`.trim(),
145
+ status: ret?.status ?? null,
146
+ error: ret?.error?.message || null,
147
+ detail: summarizeCommand(ret),
148
+ });
149
+ if (ret.status === 0) return { ok: true, ret, attempts };
150
+ }
151
+ const viaPackage = runPackageCommand('@web-auto/camo', ['camo', ...args]);
152
+ attempts.push({
153
+ command: `npx/npm exec camo ${args.join(' ')}`.trim(),
154
+ status: viaPackage?.status ?? null,
155
+ error: viaPackage?.error?.message || null,
156
+ detail: summarizeCommand(viaPackage),
157
+ });
158
+ return { ok: viaPackage.status === 0, ret: viaPackage, attempts };
159
+ }
160
+
161
+ function checkCamoufoxInstalled() {
162
+ const pathCheck = runCamoufoxCommand(['path']);
163
+ if (!pathCheck.ok) return false;
164
+ return commandReportsExistingPath(pathCheck.ret);
165
+ }
166
+
167
+ function installCamoufox() {
168
+ return runCamoufoxCommand(['fetch']);
169
+ }
170
+
171
+ function checkGeoIPInstalled() {
172
+ return hasValidGeoIPFile(resolveGeoIPPath());
173
+ }
174
+
175
+ function installGeoIP() {
176
+ return runCamoCommand(['init', 'geoip']);
177
+ }
178
+
179
+ async function installGeoIPDirect() {
180
+ const target = resolveGeoIPPath();
181
+ const tmp = `${target}.tmp`;
182
+ const timeoutMs = (() => {
183
+ const raw = Number(process.env.WEBAUTO_GEOIP_TIMEOUT_MS || 300000);
184
+ return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 300000;
185
+ })();
186
+ const url = String(process.env.WEBAUTO_GEOIP_URL || 'https://git.io/GeoLite2-City.mmdb').trim();
187
+ mkdirSync(path.dirname(target), { recursive: true });
188
+ const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
189
+ if (!res.ok) throw new Error(`geoip_download_http_${res.status}`);
190
+ const bytes = Buffer.from(await res.arrayBuffer());
191
+ if (!bytes || bytes.length < 1024) throw new Error('geoip_download_too_small');
192
+ writeFileSync(tmp, bytes);
193
+ renameSync(tmp, target);
194
+ return target;
195
+ }
196
+
197
+ async function ensureGeoIPInstalled() {
198
+ const commandResult = installGeoIP();
199
+ if (checkGeoIPInstalled()) {
200
+ return { ok: true, source: 'camo', ret: commandResult.ret };
201
+ }
202
+ try {
203
+ await installGeoIPDirect();
204
+ return { ok: checkGeoIPInstalled(), source: 'direct', ret: commandResult.ret };
205
+ } catch (error) {
206
+ return {
207
+ ok: false,
208
+ source: 'none',
209
+ ret: commandResult.ret,
210
+ detail: error?.message || String(error),
211
+ };
212
+ }
34
213
  }
35
214
 
36
215
  function resolveOnPath(candidates, pathEnv = process.env.PATH || process.env.Path || '', delimiter = path.delimiter) {
@@ -83,40 +262,9 @@ function resolveGeoIPPath() {
83
262
  return path.join(resolveWebautoRoot(), 'geoip', 'GeoLite2-City.mmdb');
84
263
  }
85
264
 
86
- function checkCamoufoxInstalled() {
87
- const candidates =
88
- process.platform === 'win32'
89
- ? [
90
- { cmd: 'python', args: ['-m', 'camoufox', 'path'] },
91
- { cmd: 'py', args: ['-3', '-m', 'camoufox', 'path'] },
92
- ]
93
- : [{ cmd: 'python3', args: ['-m', 'camoufox', 'path'] }];
94
- for (const candidate of candidates) {
95
- const ret = run(candidate.cmd, candidate.args);
96
- if (ret.status === 0) return true;
97
- }
98
- const npxRet = runPackageCommand('camoufox', ['camoufox', 'path']);
99
- if (npxRet.status === 0) return true;
100
- return false;
101
- }
102
-
103
- function installCamoufox() {
104
- const ret = runPackageCommand('camoufox', ['camoufox', 'fetch']);
105
- return ret.status === 0;
106
- }
107
-
108
- function checkGeoIPInstalled() {
109
- return existsSync(resolveGeoIPPath());
110
- }
111
-
112
- function installGeoIP() {
113
- const ret = runPackageCommand('@web-auto/camo', ['camo', 'init', 'geoip']);
114
- return ret.status === 0;
115
- }
116
265
 
117
266
  function uninstallCamoufox() {
118
- const ret = runPackageCommand('camoufox', ['camoufox', 'remove']);
119
- return ret.status === 0;
267
+ return runCamoufoxCommand(['remove']);
120
268
  }
121
269
 
122
270
  function uninstallGeoIP() {
@@ -203,6 +351,7 @@ async function main() {
203
351
  let camoufoxInstalled = before.camoufoxInstalled;
204
352
  let geoipInstalled = before.geoipInstalled;
205
353
  let operationError = null;
354
+ let operationDetail = '';
206
355
 
207
356
  if (mode === 'auto' || mode === 'install') {
208
357
  if (browser && !camoufoxInstalled) {
@@ -211,17 +360,25 @@ async function main() {
211
360
  if (!camoufoxInstalled) operationError = operationError || 'camoufox_install_failed';
212
361
  }
213
362
  if (geoip && !geoipInstalled) {
214
- actions.geoipInstalled = installGeoIP();
363
+ const geoResult = await ensureGeoIPInstalled();
364
+ actions.geoipInstalled = geoResult.ok;
215
365
  geoipInstalled = checkGeoIPInstalled();
216
- if (!geoipInstalled) operationError = operationError || 'geoip_install_failed';
366
+ if (!geoipInstalled) {
367
+ operationError = operationError || 'geoip_install_failed';
368
+ operationDetail = operationDetail || geoResult.detail || summarizeCommand(geoResult.ret);
369
+ }
217
370
  }
218
371
  }
219
372
 
220
373
  if (mode === 'uninstall' || mode === 'reinstall') {
221
374
  if (browser) {
222
- actions.browserUninstalled = uninstallCamoufox();
375
+ const removeResult = uninstallCamoufox();
376
+ actions.browserUninstalled = removeResult.ok;
223
377
  camoufoxInstalled = checkCamoufoxInstalled();
224
- if (camoufoxInstalled) operationError = operationError || 'camoufox_uninstall_failed';
378
+ if (camoufoxInstalled) {
379
+ operationError = operationError || 'camoufox_uninstall_failed';
380
+ operationDetail = operationDetail || summarizeCommand(removeResult.ret);
381
+ }
225
382
  }
226
383
  if (geoip) {
227
384
  actions.geoipUninstalled = uninstallGeoIP();
@@ -232,14 +389,22 @@ async function main() {
232
389
 
233
390
  if (mode === 'reinstall') {
234
391
  if (browser) {
235
- actions.browserInstalled = installCamoufox();
392
+ const installResult = installCamoufox();
393
+ actions.browserInstalled = installResult.ok;
236
394
  camoufoxInstalled = checkCamoufoxInstalled();
237
- if (!camoufoxInstalled) operationError = operationError || 'camoufox_install_failed';
395
+ if (!camoufoxInstalled) {
396
+ operationError = operationError || 'camoufox_install_failed';
397
+ operationDetail = operationDetail || summarizeCommand(installResult.ret);
398
+ }
238
399
  }
239
400
  if (geoip) {
240
- actions.geoipInstalled = installGeoIP();
401
+ const geoResult = await ensureGeoIPInstalled();
402
+ actions.geoipInstalled = geoResult.ok;
241
403
  geoipInstalled = checkGeoIPInstalled();
242
- if (!geoipInstalled) operationError = operationError || 'geoip_install_failed';
404
+ if (!geoipInstalled) {
405
+ operationError = operationError || 'geoip_install_failed';
406
+ operationDetail = operationDetail || geoResult.detail || summarizeCommand(geoResult.ret);
407
+ }
243
408
  }
244
409
  }
245
410
 
@@ -289,10 +454,11 @@ async function main() {
289
454
  backendHealthy: after.backendHealthy,
290
455
  geoipInstalled: after.geoipInstalled,
291
456
  operationError,
457
+ operationDetail: operationDetail || null,
292
458
  message: ok
293
459
  ? '资源状态就绪'
294
460
  : operationError
295
- ? `资源操作失败: ${operationError}`
461
+ ? `资源操作失败: ${operationError}${operationDetail ? ` (${operationDetail})` : ''}`
296
462
  : browser && !after.camoufoxInstalled
297
463
  ? 'Camoufox 未安装'
298
464
  : geoip && !after.geoipInstalled
@@ -318,4 +484,7 @@ export const __internals = {
318
484
  resolveWebautoRoot,
319
485
  resolveGeoIPPath,
320
486
  resolveModeAndSelection,
487
+ resolvePathFromOutput,
488
+ commandReportsExistingPath,
489
+ runCamoufoxCommand,
321
490
  };
@@ -9,7 +9,7 @@ import { buildXhsUnifiedAutoscript } from '../../../modules/camo-runtime/src/aut
9
9
  import { normalizeAutoscript, validateAutoscript } from '../../../modules/camo-runtime/src/autoscript/schema.mjs';
10
10
  import { AutoscriptRunner } from '../../../modules/camo-runtime/src/autoscript/runtime.mjs';
11
11
  import { syncXhsAccountsByProfiles } from './lib/account-detect.mjs';
12
- import { markProfileInvalid } from './lib/account-store.mjs';
12
+ import { listAccountProfiles, markProfileInvalid } from './lib/account-store.mjs';
13
13
  import { listProfilesForPool } from './lib/profilepool.mjs';
14
14
  import { runCamo } from './lib/camo-cli.mjs';
15
15
  import { publishBusEvent } from './lib/bus-publish.mjs';
@@ -60,6 +60,19 @@ function parseProfiles(argv) {
60
60
  return [];
61
61
  }
62
62
 
63
+ function resolveDefaultXhsProfiles() {
64
+ const rows = listAccountProfiles({ platform: 'xiaohongshu' }).profiles || [];
65
+ const valid = rows
66
+ .filter((row) => row?.valid === true && String(row?.accountId || '').trim())
67
+ .sort((a, b) => {
68
+ const ta = Date.parse(String(a?.updatedAt || '')) || 0;
69
+ const tb = Date.parse(String(b?.updatedAt || '')) || 0;
70
+ if (tb !== ta) return tb - ta;
71
+ return String(a?.profileId || '').localeCompare(String(b?.profileId || ''));
72
+ });
73
+ return Array.from(new Set(valid.map((row) => String(row.profileId || '').trim()).filter(Boolean)));
74
+ }
75
+
63
76
  function sanitizeForPath(name, fallback = 'unknown') {
64
77
  const text = String(name || '').trim();
65
78
  if (!text) return fallback;
@@ -230,6 +243,7 @@ function createTaskReporter(seed = {}) {
230
243
  profileId: String(seed.profileId || 'unknown').trim() || 'unknown',
231
244
  keyword: String(seed.keyword || '').trim(),
232
245
  phase: 'unified',
246
+ uiTriggerId: String(seed.uiTriggerId || '').trim(),
233
247
  };
234
248
  const createdRunIds = new Set();
235
249
 
@@ -305,6 +319,7 @@ function buildTemplateOptions(argv, profileId, overrides = {}) {
305
319
  const likeKeywords = String(argv['like-keywords'] || '').trim();
306
320
  const replyText = String(argv['reply-text'] || '感谢分享,已关注').trim() || '感谢分享,已关注';
307
321
  const outputRoot = String(argv['output-root'] || '').trim();
322
+ const uiTriggerId = String(argv['ui-trigger-id'] || process.env.WEBAUTO_UI_TRIGGER_ID || '').trim();
308
323
  const resume = parseBool(argv.resume, false);
309
324
  const incrementalMax = parseBool(argv['incremental-max'], true);
310
325
  const sharedHarvestPath = String(overrides.sharedHarvestPath ?? argv['shared-harvest-path'] ?? '').trim();
@@ -329,6 +344,7 @@ function buildTemplateOptions(argv, profileId, overrides = {}) {
329
344
  inputMode,
330
345
  headless,
331
346
  ocrCommand,
347
+ uiTriggerId,
332
348
  outputRoot,
333
349
  throttle,
334
350
  tabCount,
@@ -537,6 +553,7 @@ async function runProfile(spec, argv, baseOverrides = {}) {
537
553
  const reporter = createTaskReporter({
538
554
  profileId,
539
555
  keyword: options.keyword,
556
+ uiTriggerId: options.uiTriggerId,
540
557
  });
541
558
  let activeRunId = '';
542
559
  const pushTaskSnapshot = (status = 'running') => {
@@ -600,9 +617,9 @@ async function runProfile(spec, argv, baseOverrides = {}) {
600
617
  pushTaskSnapshot('running');
601
618
  }
602
619
  if (
603
- merged.event === 'autoscript:event'
604
- && merged.subscriptionId === 'login_guard'
605
- && (merged.type === 'appear' || merged.type === 'exist')
620
+ merged.event === 'autoscript:operation_error'
621
+ && String(merged.operationId || '').trim() === 'abort_on_login_guard'
622
+ && String(merged.message || '').includes('LOGIN_GUARD_DETECTED')
606
623
  ) {
607
624
  try {
608
625
  markProfileInvalid(profileId, 'login_guard_runtime');
@@ -919,8 +936,18 @@ export async function runUnified(argv, overrides = {}) {
919
936
 
920
937
  const env = String(argv.env || 'prod').trim() || 'prod';
921
938
  const busEnabled = parseBool(argv['bus-events'], false) || process.env.WEBAUTO_BUS_EVENTS === '1';
922
- const profiles = parseProfiles(argv);
923
- if (profiles.length === 0) throw new Error('missing --profile or --profiles or --profilepool');
939
+ let profiles = parseProfiles(argv);
940
+ if (profiles.length === 0) {
941
+ profiles = resolveDefaultXhsProfiles();
942
+ if (profiles.length > 0) {
943
+ console.log(JSON.stringify({
944
+ event: 'xhs.unified.auto_profiles_selected',
945
+ platform: 'xiaohongshu',
946
+ profiles,
947
+ }));
948
+ }
949
+ }
950
+ if (profiles.length === 0) throw new Error('missing --profile/--profiles/--profilepool and no valid xiaohongshu account profile found');
924
951
  await Promise.all(profiles.map((profileId) => ensureProfileSession(profileId)));
925
952
  const defaultMaxNotes = parseIntFlag(argv['max-notes'] ?? argv.target, 30, 1);
926
953
  const totalNotes = parseNonNegativeInt(argv['total-notes'] ?? argv['total-target'], 0);
package/bin/webauto.mjs CHANGED
@@ -78,6 +78,17 @@ function uiConsoleScriptPath() {
78
78
  return path.join(ROOT, 'apps', 'desktop-console', 'entry', 'ui-console.mjs');
79
79
  }
80
80
 
81
+ function readRootVersion() {
82
+ try {
83
+ const pkg = JSON.parse(readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
84
+ return String(pkg.version || '').trim() || '0.0.0';
85
+ } catch {
86
+ return '0.0.0';
87
+ }
88
+ }
89
+
90
+ const ROOT_VERSION = readRootVersion();
91
+
81
92
  function printMainHelp() {
82
93
  console.log(`webauto CLI
83
94
 
@@ -100,10 +111,14 @@ Core Commands:
100
111
  webauto xhs unified [xhs options...]
101
112
  webauto xhs status [--run-id <id>] [--json]
102
113
  webauto xhs orchestrate [xhs options...]
114
+ webauto version [--json]
115
+ webauto version bump [patch|minor|major]
103
116
 
104
117
  Build & Release:
105
118
  webauto build:dev # Local link mode
106
- webauto build:release # Full release gate (tests/build/pack)
119
+ webauto build:release # Full release gate (默认自动 bump patch 版本)
120
+ webauto build:release -- --bump minor
121
+ webauto build:release -- --no-bump
107
122
  webauto build:release -- --skip-tests
108
123
  webauto build:release -- --skip-pack
109
124
 
@@ -132,6 +147,7 @@ Tips:
132
147
  - account 命令会转发到 apps/webauto/entry/account.mjs
133
148
  - schedule 命令会转发到 apps/webauto/entry/schedule.mjs
134
149
  - 全量参数请看: webauto xhs --help
150
+ - 当前 CLI 版本: ${ROOT_VERSION}
135
151
  `);
136
152
  }
137
153
 
@@ -342,6 +358,21 @@ Examples:
342
358
  `);
343
359
  }
344
360
 
361
+ function printVersionHelp() {
362
+ console.log(`webauto version
363
+
364
+ Usage:
365
+ webauto version [--json]
366
+ webauto version bump [patch|minor|major] [--json]
367
+
368
+ Examples:
369
+ webauto version
370
+ webauto version --json
371
+ webauto version bump
372
+ webauto version bump minor
373
+ `);
374
+ }
375
+
345
376
  function exists(p) {
346
377
  try {
347
378
  return existsSync(p);
@@ -454,6 +485,7 @@ async function ensureDepsAndBuild() {
454
485
  }
455
486
 
456
487
  async function uiConsole({ build, install, checkOnly, noDaemon }) {
488
+ console.log(`[webauto] version ${ROOT_VERSION}`);
457
489
  const okServices = checkServicesBuilt();
458
490
  const okDeps = checkDesktopConsoleDeps();
459
491
  const okUiBuilt = checkDesktopConsoleBuilt();
@@ -517,12 +549,14 @@ async function uiConsole({ build, install, checkOnly, noDaemon }) {
517
549
  async function main() {
518
550
  const rawArgv = process.argv.slice(2);
519
551
  const args = minimist(process.argv.slice(2), {
520
- boolean: ['help', 'build', 'install', 'check', 'full', 'link', 'skip-tests', 'skip-pack', 'no-daemon'],
552
+ boolean: ['help', 'build', 'install', 'check', 'full', 'link', 'skip-tests', 'skip-pack', 'no-daemon', 'no-bump', 'json'],
553
+ string: ['bump'],
521
554
  alias: { h: 'help' },
522
555
  });
523
556
 
524
557
  const cmd = String(args._[0] || '').trim();
525
558
  const sub = String(args._[1] || '').trim();
559
+ const noDaemon = rawArgv.includes('--no-daemon') || rawArgv.includes('--foreground') || args['no-daemon'] === true;
526
560
 
527
561
  if (args.help) {
528
562
  if (cmd === 'account') {
@@ -551,6 +585,10 @@ async function main() {
551
585
  printXhsHelp();
552
586
  return;
553
587
  }
588
+ if (cmd === 'version') {
589
+ printVersionHelp();
590
+ return;
591
+ }
554
592
  printMainHelp();
555
593
  return;
556
594
  }
@@ -560,11 +598,37 @@ async function main() {
560
598
  build: false,
561
599
  install: false,
562
600
  checkOnly: false,
563
- noDaemon: args['no-daemon'] === true,
601
+ noDaemon,
564
602
  });
565
603
  return;
566
604
  }
567
605
 
606
+ if (cmd === 'version') {
607
+ const jsonMode = args.json === true;
608
+ const action = String(args._[1] || '').trim();
609
+ if (!action) {
610
+ const out = { name: '@web-auto/webauto', version: ROOT_VERSION };
611
+ if (jsonMode) console.log(JSON.stringify(out, null, 2));
612
+ else console.log(`@web-auto/webauto v${ROOT_VERSION}`);
613
+ return;
614
+ }
615
+ if (action !== 'bump') {
616
+ console.error(`Unknown version action: ${action}`);
617
+ printVersionHelp();
618
+ process.exit(2);
619
+ }
620
+ const bumpType = String(args._[2] || args.bump || 'patch').trim().toLowerCase();
621
+ if (!['patch', 'minor', 'major'].includes(bumpType)) {
622
+ console.error(`Unsupported bump type: ${bumpType}`);
623
+ process.exit(2);
624
+ }
625
+ const script = path.join(ROOT, 'scripts', 'bump-version.mjs');
626
+ const cmdArgs = [script, bumpType];
627
+ if (jsonMode) cmdArgs.push('--json');
628
+ await run(process.execPath, cmdArgs);
629
+ return;
630
+ }
631
+
568
632
  // build:dev - local development mode
569
633
  if (cmd === 'build:dev') {
570
634
  console.log('[webauto] Running local dev setup...');
@@ -580,8 +644,20 @@ async function main() {
580
644
  if (cmd === 'build:release') {
581
645
  const skipTests = args['skip-tests'] === true;
582
646
  const skipPack = args['skip-pack'] === true;
647
+ const noBump = args['no-bump'] === true;
648
+ const bumpType = String(args.bump || 'patch').trim().toLowerCase();
649
+ if (!['patch', 'minor', 'major'].includes(bumpType)) {
650
+ console.error(`Unsupported --bump value: ${bumpType}`);
651
+ process.exit(2);
652
+ }
583
653
  console.log('[webauto] Running release gate...');
584
654
  const npm = npmRunner();
655
+ if (!noBump) {
656
+ const bumpScript = path.join(ROOT, 'scripts', 'bump-version.mjs');
657
+ await run(process.execPath, [bumpScript, bumpType]);
658
+ } else {
659
+ console.log('[webauto] Skip version bump (--no-bump)');
660
+ }
585
661
  await run(npm.cmd, [...npm.prefix, 'run', 'prebuild']);
586
662
  if (!skipTests) {
587
663
  await run(npm.cmd, [...npm.prefix, 'run', 'test:ci']);
@@ -608,7 +684,7 @@ async function main() {
608
684
  build: args.build === true,
609
685
  install: args.install === true,
610
686
  checkOnly: args.check === true,
611
- noDaemon: args['no-daemon'] === true,
687
+ noDaemon,
612
688
  });
613
689
  return;
614
690
  }
@@ -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: {