@web-auto/webauto 0.1.13 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,137 @@
1
+ # @web-auto/webauto
2
+
3
+ Windows 优先的 WebAuto CLI + Desktop UI 使用说明。
4
+
5
+ ## 1. 安装(Windows)
6
+
7
+ 要求:
8
+ - Node.js 18+(建议 20+)
9
+ - npm 可用
10
+
11
+ 全局安装:
12
+
13
+ ```bat
14
+ npm install -g @web-auto/webauto
15
+ ```
16
+
17
+ 验证:
18
+
19
+ ```bat
20
+ webauto --help
21
+ ```
22
+
23
+ ## 2. 直接启动 UI
24
+
25
+ 最直接方式:
26
+
27
+ ```bat
28
+ webauto ui console
29
+ ```
30
+
31
+ 或用 UI CLI 自动拉起(适合脚本化):
32
+
33
+ ```bat
34
+ webauto ui cli start --json
35
+ webauto ui cli status --json
36
+ ```
37
+
38
+ 说明:
39
+ - 首次启动会自动准备 Desktop runtime 依赖(Electron),不需要手工安装。
40
+ - 启动成功后,UI CLI bridge 默认端口是 `7716`。
41
+
42
+ ## 3. UI CLI 常用命令(模拟真实 UI 操作)
43
+
44
+ ```bat
45
+ webauto ui cli --help
46
+
47
+ :: 切换到任务页
48
+ webauto ui cli tab --tab tasks --json
49
+
50
+ :: 输入任务参数
51
+ webauto ui cli input --selector "#task-keyword" --value "deepseek" --json
52
+ webauto ui cli input --selector "#task-target" --value "20" --json
53
+
54
+ :: 点击执行
55
+ webauto ui cli click --selector "#task-run-ephemeral-btn" --json
56
+
57
+ :: 读取当前 UI 快照
58
+ webauto ui cli status --json
59
+ webauto ui cli snapshot --json
60
+
61
+ :: 元素探测
62
+ webauto ui cli probe --selector "#task-likes" --json
63
+ ```
64
+
65
+ 任务状态查看(后端状态):
66
+
67
+ ```bat
68
+ webauto xhs status --json
69
+ webauto xhs status --run-id <runId> --json
70
+ ```
71
+
72
+ ## 4. Windows 路径与设置
73
+
74
+ 默认数据目录(未设置环境变量时):
75
+ - 有 `D:` 盘:`D:\webauto`
76
+ - 无 `D:` 盘:`%USERPROFILE%\.webauto`
77
+
78
+ 可选环境变量:
79
+ - `WEBAUTO_HOME`:显式指定数据根目录(推荐)
80
+ - `WEBAUTO_ROOT` / `WEBAUTO_PORTABLE_ROOT`:兼容旧变量(会归一化到 `.webauto`)
81
+
82
+ PowerShell 设置示例:
83
+
84
+ ```powershell
85
+ $env:WEBAUTO_HOME = "D:\webauto"
86
+ webauto ui console
87
+ ```
88
+
89
+ CMD 设置示例:
90
+
91
+ ```bat
92
+ set WEBAUTO_HOME=D:\webauto
93
+ webauto ui console
94
+ ```
95
+
96
+ 不设置也可以直接用,系统会按默认规则落盘。
97
+
98
+ ## 5. 首次安装常见问题
99
+
100
+ ### 5.1 `Lock file can not be created` / 启动失败
101
+
102
+ 说明:通常是残留的 Electron/Node 进程占用。
103
+
104
+ ```bat
105
+ taskkill /F /IM electron.exe /T
106
+ taskkill /F /IM node.exe /T
107
+ webauto ui console
108
+ ```
109
+
110
+ ### 5.2 `ui cli fetch failed`
111
+
112
+ 先确认 UI 已启动并就绪:
113
+
114
+ ```bat
115
+ webauto ui cli start --json
116
+ webauto ui cli status --json
117
+ ```
118
+
119
+ 若仍失败,前台启动看日志:
120
+
121
+ ```bat
122
+ webauto ui console --no-daemon
123
+ ```
124
+
125
+ ## 6. 资源检查/安装
126
+
127
+ ```bat
128
+ webauto xhs install --check --json
129
+ webauto xhs install --download-browser --json
130
+ webauto xhs install --download-geoip --json
131
+ ```
132
+
133
+ ## 7. 开发者(仓库模式)
134
+
135
+ 在仓库中开发请看:
136
+ - `apps/desktop-console/README.md`
137
+
@@ -272,14 +272,14 @@ import { spawn } from "child_process";
272
272
  import path2 from "path";
273
273
  import { existsSync as existsSync3 } from "fs";
274
274
  import { fileURLToPath } from "url";
275
+ import { createRequire } from "module";
275
276
  var REPO_ROOT = path2.resolve(path2.dirname(fileURLToPath(import.meta.url)), "../../../..");
276
277
  var UNIFIED_API_HEALTH_URL = "http://127.0.0.1:7701/health";
277
278
  var CAMO_RUNTIME_HEALTH_URL = "http://127.0.0.1:7704/health";
278
279
  var CORE_HEALTH_URLS = [UNIFIED_API_HEALTH_URL, CAMO_RUNTIME_HEALTH_URL];
279
280
  var START_API_SCRIPT = path2.join(REPO_ROOT, "runtime", "infra", "utils", "scripts", "service", "start-api.mjs");
280
281
  var STOP_API_SCRIPT = path2.join(REPO_ROOT, "runtime", "infra", "utils", "scripts", "service", "stop-api.mjs");
281
- var DIST_UNIFIED_ENTRY = path2.join(REPO_ROOT, "dist", "apps", "webauto", "server.js");
282
- var fallbackUnifiedApiPid = null;
282
+ var requireFromRepo = createRequire(path2.join(REPO_ROOT, "package.json"));
283
283
  function sleep(ms) {
284
284
  return new Promise((resolve) => setTimeout(resolve, ms));
285
285
  }
@@ -292,13 +292,6 @@ function resolveNodeBin() {
292
292
  if (fromPath) return fromPath;
293
293
  return process.execPath;
294
294
  }
295
- function resolveNpxBin() {
296
- const fromPath = resolveOnPath(
297
- process.platform === "win32" ? ["npx.cmd", "npx.exe", "npx.bat", "npx.ps1"] : ["npx"]
298
- );
299
- if (fromPath) return fromPath;
300
- return process.platform === "win32" ? "npx.cmd" : "npx";
301
- }
302
295
  function resolveOnPath(candidates) {
303
296
  const pathEnv = process.env.PATH || process.env.Path || "";
304
297
  const dirs = pathEnv.split(path2.delimiter).filter(Boolean);
@@ -310,10 +303,15 @@ function resolveOnPath(candidates) {
310
303
  }
311
304
  return null;
312
305
  }
313
- function quoteCmdArg(value) {
314
- if (!value) return '""';
315
- if (!/[\s"]/u.test(value)) return value;
316
- return `"${value.replace(/"/g, '""')}"`;
306
+ function resolveCamoCliEntry() {
307
+ const direct = path2.join(REPO_ROOT, "node_modules", "@web-auto", "camo", "bin", "camo.mjs");
308
+ if (existsSync3(direct)) return direct;
309
+ try {
310
+ const resolved = requireFromRepo.resolve("@web-auto/camo/bin/camo.mjs");
311
+ if (resolved && existsSync3(resolved)) return resolved;
312
+ } catch {
313
+ }
314
+ return null;
317
315
  }
318
316
  async function checkHttpHealth(url) {
319
317
  try {
@@ -327,156 +325,100 @@ async function areCoreServicesHealthy() {
327
325
  const health = await Promise.all(CORE_HEALTH_URLS.map((url) => checkHttpHealth(url)));
328
326
  return health.every(Boolean);
329
327
  }
330
- async function isUnifiedApiHealthy() {
331
- return checkHttpHealth(UNIFIED_API_HEALTH_URL);
332
- }
333
- async function runNodeScript(scriptPath, timeoutMs) {
334
- return new Promise((resolve) => {
335
- const nodeBin = resolveNodeBin();
336
- const child = spawn(nodeBin, [scriptPath], {
337
- cwd: REPO_ROOT,
338
- stdio: "ignore",
339
- windowsHide: true,
340
- detached: false,
341
- env: {
342
- ...process.env,
343
- BROWSER_SERVICE_AUTO_EXIT: "0"
344
- }
345
- });
346
- const timer = setTimeout(() => {
347
- try {
348
- child.kill("SIGTERM");
349
- } catch {
350
- }
351
- resolve(false);
352
- }, timeoutMs);
353
- child.once("error", () => {
354
- clearTimeout(timer);
355
- resolve(false);
356
- });
357
- child.once("exit", (code) => {
358
- clearTimeout(timer);
359
- resolve(code === 0);
360
- });
361
- });
362
- }
363
- async function runLongLivedNodeScript(scriptPath) {
328
+ async function runNodeScript(scriptPath, timeoutMs, args = [], envExtra = {}) {
364
329
  return new Promise((resolve) => {
365
330
  const nodeBin = resolveNodeBin();
366
- const child = spawn(nodeBin, [scriptPath], {
331
+ const child = spawn(nodeBin, [scriptPath, ...args], {
367
332
  cwd: REPO_ROOT,
368
- stdio: "ignore",
333
+ stdio: ["ignore", "pipe", "pipe"],
369
334
  windowsHide: true,
370
335
  detached: false,
371
336
  env: {
372
337
  ...process.env,
373
- WEBAUTO_RUNTIME_MODE: "unified"
338
+ ...envExtra
374
339
  }
375
340
  });
376
- let settled = false;
377
- const timer = setTimeout(() => {
378
- if (settled) return;
379
- settled = true;
380
- fallbackUnifiedApiPid = typeof child.pid === "number" ? child.pid : null;
381
- resolve(true);
382
- }, 200);
383
- child.once("error", () => {
384
- if (settled) return;
385
- settled = true;
386
- clearTimeout(timer);
387
- resolve(false);
341
+ const stderrLines = [];
342
+ const stdoutLines = [];
343
+ child.stdout?.on("data", (chunk) => {
344
+ const text = String(chunk || "").trim();
345
+ if (!text) return;
346
+ stdoutLines.push(text);
347
+ if (stdoutLines.length > 6) stdoutLines.shift();
388
348
  });
389
- child.once("exit", () => {
390
- if (!settled) {
391
- settled = true;
392
- clearTimeout(timer);
393
- resolve(false);
394
- return;
395
- }
396
- if (fallbackUnifiedApiPid && child.pid === fallbackUnifiedApiPid) {
397
- fallbackUnifiedApiPid = null;
398
- }
399
- });
400
- });
401
- }
402
- async function runCommand(command, args, timeoutMs) {
403
- return new Promise((resolve) => {
404
- const lower = String(command || "").toLowerCase();
405
- let spawnCommand2 = command;
406
- let spawnArgs = args;
407
- if (process.platform === "win32" && (lower.endsWith(".cmd") || lower.endsWith(".bat"))) {
408
- spawnCommand2 = "cmd.exe";
409
- const cmdLine = [quoteCmdArg(command), ...args.map(quoteCmdArg)].join(" ");
410
- spawnArgs = ["/d", "/s", "/c", cmdLine];
411
- } else if (process.platform === "win32" && lower.endsWith(".ps1")) {
412
- spawnCommand2 = "powershell.exe";
413
- spawnArgs = ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", command, ...args];
414
- }
415
- const child = spawn(spawnCommand2, spawnArgs, {
416
- cwd: REPO_ROOT,
417
- stdio: "ignore",
418
- windowsHide: true,
419
- detached: false,
420
- env: {
421
- ...process.env
422
- }
349
+ child.stderr?.on("data", (chunk) => {
350
+ const text = String(chunk || "").trim();
351
+ if (!text) return;
352
+ stderrLines.push(text);
353
+ if (stderrLines.length > 6) stderrLines.shift();
423
354
  });
355
+ const summarize = (prefix) => {
356
+ const stderr = stderrLines.join("\n").trim();
357
+ const stdout = stdoutLines.join("\n").trim();
358
+ if (stderr) return `${prefix}: ${stderr}`;
359
+ if (stdout) return `${prefix}: ${stdout}`;
360
+ return prefix;
361
+ };
424
362
  const timer = setTimeout(() => {
425
363
  try {
426
364
  child.kill("SIGTERM");
427
365
  } catch {
428
366
  }
429
- resolve(false);
367
+ resolve({ ok: false, code: null, error: summarize("timeout") });
430
368
  }, timeoutMs);
431
- child.once("error", () => {
369
+ child.once("error", (err) => {
432
370
  clearTimeout(timer);
433
- resolve(false);
371
+ resolve({ ok: false, code: null, error: summarize(err?.message || "spawn_error") });
434
372
  });
435
373
  child.once("exit", (code) => {
436
374
  clearTimeout(timer);
437
- resolve(code === 0);
375
+ if (code === 0) {
376
+ resolve({ ok: true, code, error: "" });
377
+ return;
378
+ }
379
+ resolve({ ok: false, code, error: summarize(`exit ${code ?? "null"}`) });
438
380
  });
439
381
  });
440
382
  }
441
383
  async function startCoreDaemon() {
442
384
  if (await areCoreServicesHealthy()) return true;
443
- const startedApi = existsSync3(START_API_SCRIPT) ? await runNodeScript(START_API_SCRIPT, 4e4) : existsSync3(DIST_UNIFIED_ENTRY) ? await runLongLivedNodeScript(DIST_UNIFIED_ENTRY) : false;
444
- if (!startedApi) {
445
- console.error("[CoreDaemonManager] Failed to start unified API service");
385
+ if (!existsSync3(START_API_SCRIPT)) {
386
+ console.error("[CoreDaemonManager] Unified API start script not found");
446
387
  return false;
447
388
  }
448
- const startedBrowser = await runCommand(
449
- resolveNpxBin(),
450
- ["--yes", "--package=@web-auto/camo", "camo", "init"],
451
- 4e4
452
- );
453
- if (!startedBrowser) {
454
- console.warn("[CoreDaemonManager] Failed to start camo browser backend, continue in degraded mode");
389
+ const startedApi = await runNodeScript(START_API_SCRIPT, 4e4, [], {
390
+ BROWSER_SERVICE_AUTO_EXIT: "0"
391
+ });
392
+ if (!startedApi.ok) {
393
+ console.error(`[CoreDaemonManager] Failed to start unified API service (${startedApi.error || "unknown"})`);
394
+ return false;
395
+ }
396
+ const camoEntry = resolveCamoCliEntry();
397
+ if (!camoEntry) {
398
+ console.error("[CoreDaemonManager] Camo CLI entry not found: @web-auto/camo/bin/camo.mjs");
399
+ return false;
400
+ }
401
+ const startedBrowser = await runNodeScript(camoEntry, 6e4, ["init"]);
402
+ if (!startedBrowser.ok) {
403
+ console.error(`[CoreDaemonManager] Failed to start camo browser backend (${startedBrowser.error || "unknown"})`);
404
+ return false;
455
405
  }
456
406
  for (let i = 0; i < 60; i += 1) {
457
- const [allHealthy, unifiedHealthy] = await Promise.all([
458
- areCoreServicesHealthy(),
459
- isUnifiedApiHealthy()
460
- ]);
407
+ const allHealthy = await areCoreServicesHealthy();
461
408
  if (allHealthy) return true;
462
- if (unifiedHealthy) return true;
463
409
  await sleep(500);
464
410
  }
465
- console.error("[CoreDaemonManager] Unified API still unhealthy after start");
411
+ console.error("[CoreDaemonManager] Core services still unhealthy after start");
466
412
  return false;
467
413
  }
468
414
  async function stopCoreDaemon() {
469
- if (fallbackUnifiedApiPid) {
470
- try {
471
- process.kill(fallbackUnifiedApiPid, "SIGTERM");
472
- } catch {
473
- }
474
- fallbackUnifiedApiPid = null;
415
+ if (!existsSync3(STOP_API_SCRIPT)) {
416
+ console.error("[CoreDaemonManager] Unified API stop script not found");
417
+ return false;
475
418
  }
476
- if (!existsSync3(STOP_API_SCRIPT)) return true;
477
419
  const stoppedApi = await runNodeScript(STOP_API_SCRIPT, 2e4);
478
- if (!stoppedApi) {
479
- console.error("[CoreDaemonManager] Failed to stop core services");
420
+ if (!stoppedApi.ok) {
421
+ console.error(`[CoreDaemonManager] Failed to stop core services (${stoppedApi.error || "unknown"})`);
480
422
  return false;
481
423
  }
482
424
  return true;
@@ -813,22 +755,6 @@ function resolveWebautoRoot() {
813
755
  }
814
756
  return path4.join(os3.homedir(), ".webauto");
815
757
  }
816
- function resolveNpxBin2() {
817
- if (process.platform !== "win32") return "npx";
818
- const resolved = resolveOnPath2(["npx.cmd", "npx.exe", "npx.bat", "npx.ps1"]);
819
- return resolved || "npx.cmd";
820
- }
821
- function resolveOnPath2(candidates) {
822
- const pathEnv = process.env.PATH || process.env.Path || "";
823
- const dirs = pathEnv.split(path4.delimiter).filter(Boolean);
824
- for (const dir of dirs) {
825
- for (const name of candidates) {
826
- const full = path4.join(dir, name);
827
- if (existsSync4(full)) return full;
828
- }
829
- }
830
- return null;
831
- }
832
758
  function resolveCamoVersionFromText(stdout, stderr) {
833
759
  const merged = `${String(stdout || "")}
834
760
  ${String(stderr || "")}`.trim();
@@ -841,7 +767,7 @@ ${String(stderr || "")}`.trim();
841
767
  }
842
768
  return "unknown";
843
769
  }
844
- function quoteCmdArg2(value) {
770
+ function quoteCmdArg(value) {
845
771
  if (!value) return '""';
846
772
  if (!/[\s"]/u.test(value)) return value;
847
773
  return `"${value.replace(/"/g, '""')}"`;
@@ -851,7 +777,7 @@ function runVersionCheck(command, args, explicitPath) {
851
777
  const lower = String(command || "").toLowerCase();
852
778
  let ret;
853
779
  if (process.platform === "win32" && (lower.endsWith(".cmd") || lower.endsWith(".bat"))) {
854
- const cmdLine = [quoteCmdArg2(command), ...args.map(quoteCmdArg2)].join(" ");
780
+ const cmdLine = [quoteCmdArg(command), ...args.map(quoteCmdArg)].join(" ");
855
781
  ret = spawnSync("cmd.exe", ["/d", "/s", "/c", cmdLine], {
856
782
  encoding: "utf8",
857
783
  timeout: 8e3,
@@ -920,15 +846,9 @@ async function checkCamoCli() {
920
846
  if (ret.installed) return ret;
921
847
  }
922
848
  }
923
- const npxCheck = runVersionCheck(
924
- resolveNpxBin2(),
925
- ["--yes", "--package=@web-auto/camo", "camo", "help"],
926
- "npx:@web-auto/camo"
927
- );
928
- if (npxCheck.installed) return npxCheck;
929
849
  return {
930
850
  installed: false,
931
- error: "camo not found in PATH/local bin, and npx @web-auto/camo failed"
851
+ error: "camo not found in PATH/local bin"
932
852
  };
933
853
  }
934
854
  async function checkServices() {
@@ -943,12 +863,10 @@ async function checkFirefox() {
943
863
  const candidates = process.platform === "win32" ? [
944
864
  { command: "camoufox", args: ["path"] },
945
865
  { command: "python", args: ["-m", "camoufox", "path"] },
946
- { command: "py", args: ["-3", "-m", "camoufox", "path"] },
947
- { command: resolveNpxBin2(), args: ["--yes", "--package=camoufox", "camoufox", "path"] }
866
+ { command: "py", args: ["-3", "-m", "camoufox", "path"] }
948
867
  ] : [
949
868
  { command: "camoufox", args: ["path"] },
950
- { command: "python3", args: ["-m", "camoufox", "path"] },
951
- { command: resolveNpxBin2(), args: ["--yes", "--package=camoufox", "camoufox", "path"] }
869
+ { command: "python3", args: ["-m", "camoufox", "path"] }
952
870
  ];
953
871
  for (const candidate of candidates) {
954
872
  try {
@@ -982,7 +900,7 @@ async function checkEnvironment() {
982
900
  checkFirefox(),
983
901
  checkGeoIP()
984
902
  ]);
985
- const browserReady = Boolean(firefox.installed || services.camoRuntime);
903
+ const browserReady = Boolean(services.camoRuntime);
986
904
  const missing = {
987
905
  core: !services.unifiedApi,
988
906
  runtimeService: !services.camoRuntime,
@@ -990,7 +908,7 @@ async function checkEnvironment() {
990
908
  runtime: !browserReady,
991
909
  geoip: !geoip.installed
992
910
  };
993
- const allReady = camo.installed && services.unifiedApi && browserReady;
911
+ const allReady = camo.installed && services.unifiedApi && services.camoRuntime;
994
912
  return { camo, services, firefox, geoip, browserReady, missing, allReady };
995
913
  }
996
914
 
@@ -2822,16 +2740,30 @@ app.on("will-quit", () => {
2822
2740
  });
2823
2741
  app.whenReady().then(async () => {
2824
2742
  startCoreServiceHeartbeat();
2825
- const started = await startCoreDaemon().catch(() => false);
2743
+ const started = await startCoreDaemon().catch((err) => {
2744
+ console.error("[desktop-console] core services startup failed", err);
2745
+ return false;
2746
+ });
2826
2747
  if (!started) {
2827
- console.warn("[desktop-console] core services are not healthy at startup");
2748
+ console.error("[desktop-console] core services are not healthy at startup; exiting");
2749
+ await ensureAppExitCleanup("core_startup_failed", { stopStateBridge: true }).catch(() => null);
2750
+ app.exit(1);
2751
+ return;
2828
2752
  }
2829
2753
  markUiHeartbeat("main_ready");
2830
2754
  ensureHeartbeatWatchdog();
2831
2755
  createWindow();
2832
- await uiCliBridge.start().catch((err) => {
2833
- console.warn("[desktop-console] ui-cli bridge start failed", err);
2834
- });
2756
+ try {
2757
+ await uiCliBridge.start();
2758
+ } catch (err) {
2759
+ console.error("[desktop-console] ui-cli bridge start failed", err);
2760
+ await ensureAppExitCleanup("ui_cli_bridge_start_failed", { stopStateBridge: true }).catch(() => null);
2761
+ app.exit(1);
2762
+ }
2763
+ }).catch(async (err) => {
2764
+ console.error("[desktop-console] fatal startup error", err);
2765
+ await ensureAppExitCleanup("startup_exception", { stopStateBridge: true }).catch(() => null);
2766
+ app.exit(1);
2835
2767
  });
2836
2768
  ipcMain2.on("preload:test", () => {
2837
2769
  console.log("[preload-test] window.api OK");
@@ -1582,7 +1582,7 @@ function renderSetupWizard(root, ctx2) {
1582
1582
  <div class="env-item" id="env-browser" style="display:flex; align-items:center; justify-content:space-between; gap:8px;">
1583
1583
  <span style="display:flex; align-items:center; gap:8px; min-width:0;">
1584
1584
  <span class="icon" style="color: var(--text-4);">\u25CB</span>
1585
- <span class="env-label">Camo Runtime Service (7704\uFF0C\u53EF\u9009)</span>
1585
+ <span class="env-label">Camo Runtime Service (7704)</span>
1586
1586
  </span>
1587
1587
  <button id="repair-core2-btn" class="secondary" style="display:none; flex:0 0 auto;">\u4E00\u952E\u4FEE\u590D</button>
1588
1588
  </div>
@@ -1862,13 +1862,8 @@ function renderSetupWizard(root, ctx2) {
1862
1862
  if (missingFlags.core) missing.push("unified-api");
1863
1863
  if (missingFlags.runtime) missing.push("browser-kernel");
1864
1864
  setupStatusText.textContent = `\u5B58\u5728\u5F85\u4FEE\u590D\u9879: ${missing.join(", ")}`;
1865
- if (missingFlags.runtimeService) {
1866
- setupStatusText.textContent += "\uFF08camo-runtime \u672A\u5C31\u7EEA\uFF0C\u5F53\u524D\u4E3A\u53EF\u9009\uFF09";
1867
- }
1868
1865
  } else if (!snapshot?.geoip?.installed) {
1869
1866
  setupStatusText.textContent = "\u73AF\u5883\u5C31\u7EEA\uFF08GeoIP \u53EF\u9009\uFF0C\u672A\u5B89\u88C5\u4E0D\u5F71\u54CD\u4F7F\u7528\uFF09";
1870
- } else if (!snapshot?.services?.camoRuntime) {
1871
- setupStatusText.textContent = "\u73AF\u5883\u5C31\u7EEA\uFF08camo-runtime \u672A\u5C31\u7EEA\uFF0C\u5F53\u524D\u4E0D\u963B\u585E\uFF09";
1872
1867
  }
1873
1868
  } catch (err) {
1874
1869
  console.error("Environment check failed:", err);
@@ -4094,7 +4089,7 @@ function renderAccountManager(root, ctx2) {
4094
4089
  </div>
4095
4090
  <div class="env-item" id="env-browser">
4096
4091
  <span class="icon" style="color: var(--text-4);">\u25CB</span>
4097
- <span>Camo Runtime Service (7704\uFF0C\u53EF\u9009)</span>
4092
+ <span>Camo Runtime Service (7704)</span>
4098
4093
  </div>
4099
4094
  <div class="env-item" id="env-firefox">
4100
4095
  <span class="icon" style="color: var(--text-4);">\u25CB</span>
@@ -94,6 +94,45 @@ function checkBuildStatus() {
94
94
  return existsSync(DIST_MAIN);
95
95
  }
96
96
 
97
+ function quotePsSingle(value) {
98
+ return String(value || '').replace(/'/g, "''");
99
+ }
100
+
101
+ function sleep(ms) {
102
+ return new Promise((resolve) => setTimeout(resolve, ms));
103
+ }
104
+
105
+ async function waitForCoreServicesHealthy(timeoutMs = 90000) {
106
+ const startedAt = Date.now();
107
+ while ((Date.now() - startedAt) <= timeoutMs) {
108
+ try {
109
+ const [coreRes, runtimeRes] = await Promise.all([
110
+ fetch('http://127.0.0.1:7701/health', { signal: AbortSignal.timeout(1500) }),
111
+ fetch('http://127.0.0.1:7704/health', { signal: AbortSignal.timeout(1500) }),
112
+ ]);
113
+ if (coreRes.ok && runtimeRes.ok) return true;
114
+ } catch {
115
+ // keep polling
116
+ }
117
+ await sleep(500);
118
+ }
119
+ return false;
120
+ }
121
+
122
+ function terminateProcessTree(pid) {
123
+ const target = Number(pid || 0);
124
+ if (!Number.isFinite(target) || target <= 0) return;
125
+ try {
126
+ if (process.platform === 'win32') {
127
+ spawn('taskkill', ['/PID', String(target), '/T', '/F'], { stdio: 'ignore', windowsHide: true });
128
+ return;
129
+ }
130
+ process.kill(target, 'SIGTERM');
131
+ } catch {
132
+ // ignore cleanup errors
133
+ }
134
+ }
135
+
97
136
  async function build() {
98
137
  return new Promise((resolve, reject) => {
99
138
  console.log('[ui-console] Building...');
@@ -131,23 +170,60 @@ async function startConsole(noDaemon = false) {
131
170
  }
132
171
 
133
172
  console.log('[ui-console] Starting Desktop Console...');
134
- const npxBin = process.platform === 'win32' ? 'npx.cmd' : 'npx';
135
173
  const env = { ...process.env };
136
174
  if (noDaemon) env.WEBAUTO_NO_DAEMON = '1';
137
175
  const detached = !noDaemon;
138
176
  const stdio = detached ? 'ignore' : 'inherit';
139
-
140
- const useCmd = process.platform === 'win32';
141
- const spawnCmd = useCmd ? 'cmd.exe' : npxBin;
142
- const spawnArgs = useCmd
143
- ? ['/d', '/s', '/c', npxBin, 'electron', DIST_MAIN]
144
- : ['electron', DIST_MAIN];
177
+ const electronBin = process.platform === 'win32'
178
+ ? path.join(APP_ROOT, 'node_modules', 'electron', 'dist', 'electron.exe')
179
+ : path.join(APP_ROOT, 'node_modules', 'electron', 'dist', 'electron');
180
+ if (!existsSync(electronBin)) {
181
+ throw new Error(`electron binary not found: ${electronBin}`);
182
+ }
183
+ const spawnCmd = electronBin;
184
+ const spawnArgs = [DIST_MAIN];
185
+
186
+ if (process.platform === 'win32' && detached) {
187
+ const filePath = electronBin;
188
+ const argList = [DIST_MAIN];
189
+ const psArgList = argList.map((item) => `'${quotePsSingle(item)}'`).join(',');
190
+ const psScript = `$p = Start-Process -FilePath '${quotePsSingle(filePath)}' -ArgumentList @(${psArgList}) -WorkingDirectory '${quotePsSingle(APP_ROOT)}' -PassThru; Write-Output $p.Id`;
191
+ const pid = await new Promise((resolve, reject) => {
192
+ const child = spawn('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', psScript], {
193
+ cwd: APP_ROOT,
194
+ env,
195
+ stdio: ['ignore', 'pipe', 'pipe'],
196
+ windowsHide: true,
197
+ });
198
+ let stdout = '';
199
+ let stderr = '';
200
+ child.stdout.on('data', (chunk) => { stdout += String(chunk || ''); });
201
+ child.stderr.on('data', (chunk) => { stderr += String(chunk || ''); });
202
+ child.on('error', reject);
203
+ child.on('close', (code) => {
204
+ if (code === 0) {
205
+ const pid = String(stdout || '').trim().split(/\s+/).pop();
206
+ resolve(pid || 'unknown');
207
+ } else {
208
+ reject(new Error(`Start-Process failed (${code}): ${stderr.trim() || stdout.trim() || 'unknown error'}`));
209
+ }
210
+ });
211
+ });
212
+ const healthy = await waitForCoreServicesHealthy();
213
+ if (!healthy) {
214
+ terminateProcessTree(pid);
215
+ throw new Error('desktop console started but core services did not become healthy');
216
+ }
217
+ console.log(`[ui-console] Started (PID: ${pid || 'unknown'})`);
218
+ return;
219
+ }
145
220
 
146
221
  const child = spawn(spawnCmd, spawnArgs, {
147
222
  cwd: APP_ROOT,
148
223
  env,
149
224
  stdio,
150
- detached
225
+ detached,
226
+ windowsHide: true,
151
227
  });
152
228
 
153
229
  if (noDaemon) {
@@ -156,6 +232,11 @@ async function startConsole(noDaemon = false) {
156
232
  process.exit(code);
157
233
  });
158
234
  } else {
235
+ const healthy = await waitForCoreServicesHealthy();
236
+ if (!healthy) {
237
+ terminateProcessTree(child.pid);
238
+ throw new Error('desktop console started but core services did not become healthy');
239
+ }
159
240
  child.unref();
160
241
  console.log(`[ui-console] Started (PID: ${child.pid})`);
161
242
  }