@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.
- 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/lib/camo-cli.mjs +10 -3
- 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 +2 -0
- package/bin/webauto.mjs +74 -47
- package/modules/camo-runtime/src/autoscript/action-providers/index.mjs +14 -3
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/comments.mjs +40 -10
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +56 -4
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +51 -1
- 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/utils/browser-service.mjs +67 -45
- package/package.json +4 -3
|
@@ -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
|
}
|
|
@@ -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
|
-
|
|
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:
|
|
196
|
+
return { ok: checkGeoIPInstalled(), source: 'direct', ret: null, detail: '' };
|
|
205
197
|
} catch (error) {
|
|
206
198
|
return {
|
|
207
199
|
ok: false,
|
|
208
|
-
source: '
|
|
209
|
-
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 && !
|
|
67
|
+
if (!hasErrorType && !hasExplicitError) continue;
|
|
66
68
|
items.push({
|
|
67
69
|
ts: asText(payload?.timestamp || payload?.ts || ''),
|
|
68
70
|
type: type || 'error',
|