@web-auto/webauto 0.1.13 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
  }
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@webauto/desktop-console",
3
+ "version": "0.1.11",
4
+ "private": true,
5
+ "type": "module",
6
+ "main": "dist/main/index.mjs",
7
+ "description": "Cross-platform desktop console for WebAuto (scripts runner)",
8
+ "scripts": {
9
+ "build": "node scripts/build.mjs",
10
+ "start": "electron .",
11
+ "test-preload": "node scripts/test-preload.mjs",
12
+ "test:renderer": "tsx --test src/renderer/index.test.mts src/renderer/index.runtime.test.mts src/renderer/path-helpers.test.mts src/renderer/tabs/debug.test.mts src/renderer/tabs/logs.test.mts src/renderer/tabs/preflight.test.mts src/renderer/tabs/preflight.runtime.test.mts src/renderer/tabs/run.test.mts src/renderer/tabs/runtime-smoke.test.mts src/renderer/tabs/xiaohongshu.test.mts src/renderer/tabs-new/setup-wizard.test.mts src/renderer/tabs-new/setup-wizard.runtime.test.mts src/renderer/tabs-new/account-manager.test.mts src/renderer/tabs-new/account-manager.runtime.test.mts src/renderer/tabs-new/config-panel.runtime.test.mts src/renderer/tabs-new/dashboard.runtime.test.mts src/renderer/tabs-new/scheduler.test.mts src/renderer/tabs-new/scheduler.runtime.test.mts src/renderer/tabs-new/tasks.runtime.test.mts src/renderer/ui-runtime.test.mts src/renderer/tabs/xiaohongshu/helpers.runtime.test.mts src/renderer/tabs/xiaohongshu-state.runtime.test.mts src/renderer/tabs/xiaohongshu/guide-browser-check.runtime.test.mts src/renderer/tabs/xiaohongshu/live-stats/runtime.test.mts",
13
+ "test:renderer:coverage": "c8 --all --extension .mts --extension .ts --reporter=text --reporter=text-summary --check-coverage --lines 90 --functions 85 --branches 55 --statements 90 --include src/renderer/index.mts --include src/renderer/account-source.mts --include src/renderer/hooks/use-task-state.mts --include src/renderer/path-helpers.mts --include src/renderer/ui-components.mts --include src/renderer/tabs/preflight.mts --include src/renderer/tabs/logs.mts --include src/renderer/tabs/profile-pool.mts --include src/renderer/tabs/results.mts --include src/renderer/tabs/run.mts --include src/renderer/tabs/runtime.mts --include src/renderer/tabs/settings.mts --include src/renderer/tabs-new/setup-wizard.mts --include src/renderer/tabs-new/account-manager.mts --include src/renderer/tabs-new/config-panel.mts --include src/renderer/tabs-new/dashboard.mts --include src/renderer/tabs-new/scheduler.mts --include src/renderer/tabs-new/tasks.mts --include src/renderer/tabs/xiaohongshu/helpers.mts --include src/renderer/tabs/xiaohongshu-state.mts --include src/renderer/tabs/xiaohongshu/guide-browser-check.mts --include src/renderer/tabs/xiaohongshu/live-stats/state-patch.mts --include src/renderer/tabs/xiaohongshu/live-stats/stdout-parser.mts tsx --test src/renderer/index.test.mts src/renderer/index.runtime.test.mts src/renderer/path-helpers.test.mts src/renderer/tabs/debug.test.mts src/renderer/tabs/logs.test.mts src/renderer/tabs/preflight.test.mts src/renderer/tabs/preflight.runtime.test.mts src/renderer/tabs/run.test.mts src/renderer/tabs/runtime-smoke.test.mts src/renderer/tabs/xiaohongshu.test.mts src/renderer/tabs-new/setup-wizard.test.mts src/renderer/tabs-new/setup-wizard.runtime.test.mts src/renderer/tabs-new/account-manager.test.mts src/renderer/tabs-new/account-manager.runtime.test.mts src/renderer/tabs-new/config-panel.runtime.test.mts src/renderer/tabs-new/dashboard.runtime.test.mts src/renderer/tabs-new/scheduler.test.mts src/renderer/tabs-new/scheduler.runtime.test.mts src/renderer/tabs-new/tasks.runtime.test.mts src/renderer/ui-runtime.test.mts src/renderer/tabs/xiaohongshu/helpers.runtime.test.mts src/renderer/tabs/xiaohongshu-state.runtime.test.mts src/renderer/tabs/xiaohongshu/guide-browser-check.runtime.test.mts src/renderer/tabs/xiaohongshu/live-stats/runtime.test.mts",
14
+ "clean": "node --input-type=module -e \"import('node:fs').then((fs)=>fs.rmSync('dist',{recursive:true,force:true}))\""
15
+ },
16
+ "devDependencies": {
17
+ "@types/node": "^20.19.33",
18
+ "c8": "^10.1.3",
19
+ "jsdom": "^26.1.0"
20
+ },
21
+ "dependencies": {
22
+ "electron": "^39.6.0"
23
+ }
24
+ }
@@ -44,7 +44,6 @@ export function getCamoRunner(rootDir = process.cwd()) {
44
44
  const isWin = process.platform === 'win32';
45
45
  const localBin = path.join(rootDir, 'node_modules', '.bin');
46
46
  const camoNames = isWin ? ['camo.cmd', 'camo.exe', 'camo.bat', 'camo.ps1'] : ['camo'];
47
- const npxNames = isWin ? ['npx.cmd', 'npx.exe', 'npx.bat', 'npx.ps1'] : ['npx'];
48
47
 
49
48
  const local = resolveInDir(localBin, camoNames);
50
49
  if (local) return wrapWindowsRunner(local);
@@ -52,8 +51,7 @@ export function getCamoRunner(rootDir = process.cwd()) {
52
51
  const global = resolveOnPath(camoNames);
53
52
  if (global) return wrapWindowsRunner(global);
54
53
 
55
- const npx = resolveOnPath(npxNames) || (isWin ? 'npx.cmd' : 'npx');
56
- return wrapWindowsRunner(npx, ['--yes', '--package=@web-auto/camo', 'camo']);
54
+ return null;
57
55
  }
58
56
 
59
57
  function parseLastJson(stdout) {
@@ -74,6 +72,15 @@ export function runCamo(args, options = {}) {
74
72
  const rootDir = String(options.rootDir || process.cwd());
75
73
  const timeoutMs = Number(options.timeoutMs) > 0 ? Number(options.timeoutMs) : 60000;
76
74
  const runner = getCamoRunner(rootDir);
75
+ if (!runner) {
76
+ return {
77
+ ok: false,
78
+ code: null,
79
+ stdout: '',
80
+ stderr: 'camo cli not found in node_modules/.bin or PATH',
81
+ json: null,
82
+ };
83
+ }
77
84
  const ret = spawnSync(runner.cmd, [...runner.prefix, ...args], {
78
85
  cwd: rootDir,
79
86
  env: { ...process.env, ...(options.env || {}) },
@@ -172,10 +172,6 @@ function checkGeoIPInstalled() {
172
172
  return hasValidGeoIPFile(resolveGeoIPPath());
173
173
  }
174
174
 
175
- function installGeoIP() {
176
- return runCamoCommand(['init', 'geoip']);
177
- }
178
-
179
175
  async function installGeoIPDirect() {
180
176
  const target = resolveGeoIPPath();
181
177
  const tmp = `${target}.tmp`;
@@ -195,18 +191,14 @@ async function installGeoIPDirect() {
195
191
  }
196
192
 
197
193
  async function ensureGeoIPInstalled() {
198
- const commandResult = installGeoIP();
199
- if (checkGeoIPInstalled()) {
200
- return { ok: true, source: 'camo', ret: commandResult.ret };
201
- }
202
194
  try {
203
195
  await installGeoIPDirect();
204
- return { ok: checkGeoIPInstalled(), source: 'direct', ret: commandResult.ret };
196
+ return { ok: checkGeoIPInstalled(), source: 'direct', ret: null, detail: '' };
205
197
  } catch (error) {
206
198
  return {
207
199
  ok: false,
208
- source: 'none',
209
- ret: commandResult.ret,
200
+ source: 'direct',
201
+ ret: null,
210
202
  detail: error?.message || String(error),
211
203
  };
212
204
  }
@@ -60,9 +60,11 @@ function extractErrorEvents(events = [], limit = 20) {
60
60
  for (const event of events) {
61
61
  const payload = event?.data && typeof event.data === 'object' ? event.data : event;
62
62
  const type = asText(payload?.type || payload?.event || '').toLowerCase();
63
+ if (type.includes('operation_progress')) continue;
63
64
  const hasErrorType = type.includes('error') || type.includes('fail');
65
+ const hasExplicitError = Boolean(payload?.error);
64
66
  const errText = asText(payload?.error || payload?.message || payload?.reason || '');
65
- if (!hasErrorType && !errText) continue;
67
+ if (!hasErrorType && !hasExplicitError) continue;
66
68
  items.push({
67
69
  ts: asText(payload?.timestamp || payload?.ts || ''),
68
70
  type: type || 'error',