@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 +137 -0
- package/apps/desktop-console/dist/main/index.mjs +93 -161
- package/apps/desktop-console/dist/renderer/index.js +2 -7
- package/apps/desktop-console/entry/ui-console.mjs +89 -8
- package/apps/desktop-console/package.json +24 -0
- package/apps/webauto/entry/flow-gate.mjs +139 -0
- package/apps/webauto/entry/lib/camo-cli.mjs +10 -3
- package/apps/webauto/entry/lib/flow-gate.mjs +466 -0
- package/apps/webauto/entry/xhs-install.mjs +3 -11
- package/apps/webauto/entry/xhs-status.mjs +3 -1
- package/apps/webauto/entry/xhs-unified.mjs +111 -5
- package/bin/webauto.mjs +89 -49
- package/dist/modules/camo-runtime/src/utils/browser-service.mjs +64 -44
- package/modules/camo-runtime/src/autoscript/action-providers/index.mjs +14 -3
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/comments.mjs +69 -19
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +56 -4
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +130 -21
- package/modules/camo-runtime/src/autoscript/action-providers/xhs.mjs +90 -14
- package/modules/camo-runtime/src/autoscript/runtime.mjs +14 -0
- package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +69 -8
- package/modules/camo-runtime/src/utils/browser-service.mjs +67 -45
- package/package.json +4 -3
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
|
|
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
|
|
314
|
-
|
|
315
|
-
if (
|
|
316
|
-
|
|
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
|
|
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
|
-
|
|
338
|
+
...envExtra
|
|
374
339
|
}
|
|
375
340
|
});
|
|
376
|
-
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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.
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
|
|
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
|
-
|
|
444
|
-
|
|
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
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
|
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]
|
|
411
|
+
console.error("[CoreDaemonManager] Core services still unhealthy after start");
|
|
466
412
|
return false;
|
|
467
413
|
}
|
|
468
414
|
async function stopCoreDaemon() {
|
|
469
|
-
if (
|
|
470
|
-
|
|
471
|
-
|
|
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(
|
|
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
|
|
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 = [
|
|
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
|
|
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(
|
|
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 &&
|
|
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(() =>
|
|
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.
|
|
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
|
-
|
|
2833
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
}
|