@web-auto/webauto 0.1.9 → 0.1.12
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 +109 -16
- package/apps/desktop-console/dist/renderer/index.js +12 -0
- package/apps/desktop-console/entry/ui-cli.mjs +36 -1
- package/apps/webauto/entry/account.mjs +1 -1
- package/apps/webauto/entry/lib/account-store.mjs +1 -13
- package/apps/webauto/entry/lib/profilepool.mjs +45 -13
- package/apps/webauto/entry/lib/schedule-store.mjs +1 -13
- package/apps/webauto/entry/xhs-install.mjs +31 -4
- package/bin/webauto.mjs +57 -1
- package/package.json +10 -3
- package/runtime/infra/utils/README.md +13 -0
- package/runtime/infra/utils/scripts/README.md +0 -0
- package/runtime/infra/utils/scripts/development/eval-in-session.mjs +40 -0
- package/runtime/infra/utils/scripts/development/highlight-search-containers.mjs +35 -0
- package/runtime/infra/utils/scripts/service/kill-port.mjs +24 -0
- package/runtime/infra/utils/scripts/service/start-api.mjs +103 -0
- package/runtime/infra/utils/scripts/service/start-browser-service.mjs +173 -0
- package/runtime/infra/utils/scripts/service/stop-api.mjs +30 -0
- package/runtime/infra/utils/scripts/service/stop-browser-service.mjs +104 -0
- package/runtime/infra/utils/scripts/test-services.mjs +94 -0
|
@@ -7,7 +7,7 @@ import { fileURLToPath as fileURLToPath2, pathToFileURL as pathToFileURL3 } from
|
|
|
7
7
|
import { mkdirSync, readFileSync, promises as fs4 } from "node:fs";
|
|
8
8
|
|
|
9
9
|
// src/main/desktop-settings.mts
|
|
10
|
-
import { existsSync, promises as fs } from "node:fs";
|
|
10
|
+
import { existsSync as existsSync2, promises as fs } from "node:fs";
|
|
11
11
|
import os from "node:os";
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
import { pathToFileURL } from "node:url";
|
|
@@ -21,7 +21,7 @@ function resolveLegacySettingsPath() {
|
|
|
21
21
|
}
|
|
22
22
|
function hasWindowsDriveD() {
|
|
23
23
|
try {
|
|
24
|
-
return
|
|
24
|
+
return existsSync2("D:\\");
|
|
25
25
|
} catch {
|
|
26
26
|
return false;
|
|
27
27
|
}
|
|
@@ -270,7 +270,7 @@ async function importConfigFromFile(filePath) {
|
|
|
270
270
|
// src/main/core-daemon-manager.mts
|
|
271
271
|
import { spawn } from "child_process";
|
|
272
272
|
import path2 from "path";
|
|
273
|
-
import { existsSync as
|
|
273
|
+
import { existsSync as existsSync3 } from "fs";
|
|
274
274
|
import { fileURLToPath } from "url";
|
|
275
275
|
var REPO_ROOT = path2.resolve(path2.dirname(fileURLToPath(import.meta.url)), "../../../..");
|
|
276
276
|
var UNIFIED_API_HEALTH_URL = "http://127.0.0.1:7701/health";
|
|
@@ -278,6 +278,8 @@ var CAMO_RUNTIME_HEALTH_URL = "http://127.0.0.1:7704/health";
|
|
|
278
278
|
var CORE_HEALTH_URLS = [UNIFIED_API_HEALTH_URL, CAMO_RUNTIME_HEALTH_URL];
|
|
279
279
|
var START_API_SCRIPT = path2.join(REPO_ROOT, "runtime", "infra", "utils", "scripts", "service", "start-api.mjs");
|
|
280
280
|
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;
|
|
281
283
|
function sleep(ms) {
|
|
282
284
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
283
285
|
}
|
|
@@ -303,7 +305,7 @@ function resolveOnPath(candidates) {
|
|
|
303
305
|
for (const dir of dirs) {
|
|
304
306
|
for (const name of candidates) {
|
|
305
307
|
const full = path2.join(dir, name);
|
|
306
|
-
if (
|
|
308
|
+
if (existsSync3(full)) return full;
|
|
307
309
|
}
|
|
308
310
|
}
|
|
309
311
|
return null;
|
|
@@ -358,6 +360,45 @@ async function runNodeScript(scriptPath, timeoutMs) {
|
|
|
358
360
|
});
|
|
359
361
|
});
|
|
360
362
|
}
|
|
363
|
+
async function runLongLivedNodeScript(scriptPath) {
|
|
364
|
+
return new Promise((resolve) => {
|
|
365
|
+
const nodeBin = resolveNodeBin();
|
|
366
|
+
const child = spawn(nodeBin, [scriptPath], {
|
|
367
|
+
cwd: REPO_ROOT,
|
|
368
|
+
stdio: "ignore",
|
|
369
|
+
windowsHide: true,
|
|
370
|
+
detached: false,
|
|
371
|
+
env: {
|
|
372
|
+
...process.env,
|
|
373
|
+
WEBAUTO_RUNTIME_MODE: "unified"
|
|
374
|
+
}
|
|
375
|
+
});
|
|
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);
|
|
388
|
+
});
|
|
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
|
+
}
|
|
361
402
|
async function runCommand(command, args, timeoutMs) {
|
|
362
403
|
return new Promise((resolve) => {
|
|
363
404
|
const lower = String(command || "").toLowerCase();
|
|
@@ -399,7 +440,7 @@ async function runCommand(command, args, timeoutMs) {
|
|
|
399
440
|
}
|
|
400
441
|
async function startCoreDaemon() {
|
|
401
442
|
if (await areCoreServicesHealthy()) return true;
|
|
402
|
-
const startedApi = await runNodeScript(START_API_SCRIPT, 4e4);
|
|
443
|
+
const startedApi = existsSync3(START_API_SCRIPT) ? await runNodeScript(START_API_SCRIPT, 4e4) : existsSync3(DIST_UNIFIED_ENTRY) ? await runLongLivedNodeScript(DIST_UNIFIED_ENTRY) : false;
|
|
403
444
|
if (!startedApi) {
|
|
404
445
|
console.error("[CoreDaemonManager] Failed to start unified API service");
|
|
405
446
|
return false;
|
|
@@ -412,19 +453,27 @@ async function startCoreDaemon() {
|
|
|
412
453
|
if (!startedBrowser) {
|
|
413
454
|
console.warn("[CoreDaemonManager] Failed to start camo browser backend, continue in degraded mode");
|
|
414
455
|
}
|
|
415
|
-
for (let i = 0; i <
|
|
456
|
+
for (let i = 0; i < 60; i += 1) {
|
|
416
457
|
const [allHealthy, unifiedHealthy] = await Promise.all([
|
|
417
458
|
areCoreServicesHealthy(),
|
|
418
459
|
isUnifiedApiHealthy()
|
|
419
460
|
]);
|
|
420
461
|
if (allHealthy) return true;
|
|
421
462
|
if (unifiedHealthy) return true;
|
|
422
|
-
await sleep(
|
|
463
|
+
await sleep(500);
|
|
423
464
|
}
|
|
424
465
|
console.error("[CoreDaemonManager] Unified API still unhealthy after start");
|
|
425
466
|
return false;
|
|
426
467
|
}
|
|
427
468
|
async function stopCoreDaemon() {
|
|
469
|
+
if (fallbackUnifiedApiPid) {
|
|
470
|
+
try {
|
|
471
|
+
process.kill(fallbackUnifiedApiPid, "SIGTERM");
|
|
472
|
+
} catch {
|
|
473
|
+
}
|
|
474
|
+
fallbackUnifiedApiPid = null;
|
|
475
|
+
}
|
|
476
|
+
if (!existsSync3(STOP_API_SCRIPT)) return true;
|
|
428
477
|
const stoppedApi = await runNodeScript(STOP_API_SCRIPT, 2e4);
|
|
429
478
|
if (!stoppedApi) {
|
|
430
479
|
console.error("[CoreDaemonManager] Failed to stop core services");
|
|
@@ -739,12 +788,30 @@ var stateBridge = new StateBridge();
|
|
|
739
788
|
|
|
740
789
|
// src/main/env-check.mts
|
|
741
790
|
import { spawnSync } from "node:child_process";
|
|
742
|
-
import { existsSync as
|
|
791
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
743
792
|
import path4 from "node:path";
|
|
744
793
|
import os3 from "node:os";
|
|
745
794
|
function resolveWebautoRoot() {
|
|
746
|
-
const
|
|
747
|
-
|
|
795
|
+
const normalizePathForPlatform2 = (raw, platform = process.platform) => {
|
|
796
|
+
const input = String(raw || "").trim();
|
|
797
|
+
const isWinPath = platform === "win32" || /^[A-Za-z]:[\\/]/.test(input);
|
|
798
|
+
const pathApi = isWinPath ? path4.win32 : path4;
|
|
799
|
+
return isWinPath ? pathApi.normalize(input) : path4.resolve(input);
|
|
800
|
+
};
|
|
801
|
+
const normalizeLegacyWebautoRoot2 = (raw, platform = process.platform) => {
|
|
802
|
+
const pathApi = platform === "win32" ? path4.win32 : path4;
|
|
803
|
+
const resolved = normalizePathForPlatform2(raw, platform);
|
|
804
|
+
const base = pathApi.basename(resolved).toLowerCase();
|
|
805
|
+
return base === ".webauto" || base === "webauto" ? resolved : pathApi.join(resolved, ".webauto");
|
|
806
|
+
};
|
|
807
|
+
const explicitHome = String(process.env.WEBAUTO_HOME || "").trim();
|
|
808
|
+
if (explicitHome) return normalizePathForPlatform2(explicitHome);
|
|
809
|
+
const legacyRoot = String(process.env.WEBAUTO_ROOT || process.env.WEBAUTO_PORTABLE_ROOT || "").trim();
|
|
810
|
+
if (legacyRoot) return normalizeLegacyWebautoRoot2(legacyRoot);
|
|
811
|
+
if (process.platform === "win32") {
|
|
812
|
+
return existsSync4("D:\\") ? "D:\\webauto" : path4.join(os3.homedir(), ".webauto");
|
|
813
|
+
}
|
|
814
|
+
return path4.join(os3.homedir(), ".webauto");
|
|
748
815
|
}
|
|
749
816
|
function resolveNpxBin2() {
|
|
750
817
|
if (process.platform !== "win32") return "npx";
|
|
@@ -757,7 +824,7 @@ function resolveOnPath2(candidates) {
|
|
|
757
824
|
for (const dir of dirs) {
|
|
758
825
|
for (const name of candidates) {
|
|
759
826
|
const full = path4.join(dir, name);
|
|
760
|
-
if (
|
|
827
|
+
if (existsSync4(full)) return full;
|
|
761
828
|
}
|
|
762
829
|
}
|
|
763
830
|
return null;
|
|
@@ -830,8 +897,8 @@ function resolveCamoufoxExecutable(installRoot) {
|
|
|
830
897
|
return process.platform === "win32" ? path4.join(installRoot, "camoufox.exe") : path4.join(installRoot, "camoufox");
|
|
831
898
|
}
|
|
832
899
|
function isValidCamoufoxInstallRoot(installRoot) {
|
|
833
|
-
if (!installRoot || !
|
|
834
|
-
return
|
|
900
|
+
if (!installRoot || !existsSync4(installRoot)) return false;
|
|
901
|
+
return existsSync4(resolveCamoufoxExecutable(installRoot));
|
|
835
902
|
}
|
|
836
903
|
async function checkCamoCli() {
|
|
837
904
|
const camoCandidates = process.platform === "win32" ? ["camo.cmd", "camo.exe", "camo.bat", "camo.ps1"] : ["camo"];
|
|
@@ -848,7 +915,7 @@ async function checkCamoCli() {
|
|
|
848
915
|
for (const localRoot of localRoots) {
|
|
849
916
|
for (const suffix of camoCandidates) {
|
|
850
917
|
const candidate = path4.resolve(localRoot, suffix);
|
|
851
|
-
if (!
|
|
918
|
+
if (!existsSync4(candidate)) continue;
|
|
852
919
|
const ret = runVersionCheck(candidate, ["help"], candidate);
|
|
853
920
|
if (ret.installed) return ret;
|
|
854
921
|
}
|
|
@@ -903,7 +970,7 @@ async function checkFirefox() {
|
|
|
903
970
|
}
|
|
904
971
|
async function checkGeoIP() {
|
|
905
972
|
const geoIpPath = path4.join(resolveWebautoRoot(), "geoip", "GeoLite2-City.mmdb");
|
|
906
|
-
if (
|
|
973
|
+
if (existsSync4(geoIpPath)) {
|
|
907
974
|
return { installed: true, path: geoIpPath };
|
|
908
975
|
}
|
|
909
976
|
return { installed: false };
|
|
@@ -934,7 +1001,33 @@ import path5 from "node:path";
|
|
|
934
1001
|
import { promises as fs3 } from "node:fs";
|
|
935
1002
|
var DEFAULT_HOST = "127.0.0.1";
|
|
936
1003
|
var DEFAULT_PORT = 7716;
|
|
937
|
-
|
|
1004
|
+
function normalizePathForPlatform(raw, platform = process.platform) {
|
|
1005
|
+
const input = String(raw || "").trim();
|
|
1006
|
+
const isWinPath = platform === "win32" || /^[A-Za-z]:[\\/]/.test(input);
|
|
1007
|
+
const pathApi = isWinPath ? path5.win32 : path5;
|
|
1008
|
+
return isWinPath ? pathApi.normalize(input) : path5.resolve(input);
|
|
1009
|
+
}
|
|
1010
|
+
function normalizeLegacyWebautoRoot(raw, platform = process.platform) {
|
|
1011
|
+
const pathApi = platform === "win32" ? path5.win32 : path5;
|
|
1012
|
+
const resolved = normalizePathForPlatform(raw, platform);
|
|
1013
|
+
const base = pathApi.basename(resolved).toLowerCase();
|
|
1014
|
+
return base === ".webauto" || base === "webauto" ? resolved : pathApi.join(resolved, ".webauto");
|
|
1015
|
+
}
|
|
1016
|
+
function resolveWebautoRoot2() {
|
|
1017
|
+
const explicitHome = String(process.env.WEBAUTO_HOME || "").trim();
|
|
1018
|
+
if (explicitHome) return normalizePathForPlatform(explicitHome);
|
|
1019
|
+
const legacyRoot = String(process.env.WEBAUTO_ROOT || process.env.WEBAUTO_PORTABLE_ROOT || "").trim();
|
|
1020
|
+
if (legacyRoot) return normalizeLegacyWebautoRoot(legacyRoot);
|
|
1021
|
+
if (process.platform === "win32") {
|
|
1022
|
+
try {
|
|
1023
|
+
if (existsSync("D:\\")) return "D:\\webauto";
|
|
1024
|
+
} catch {
|
|
1025
|
+
}
|
|
1026
|
+
return path5.join(os4.homedir(), ".webauto");
|
|
1027
|
+
}
|
|
1028
|
+
return path5.join(os4.homedir(), ".webauto");
|
|
1029
|
+
}
|
|
1030
|
+
var CONTROL_FILE = path5.join(resolveWebautoRoot2(), "run", "ui-cli.json");
|
|
938
1031
|
function readInt(input, fallback) {
|
|
939
1032
|
const n = Number(input);
|
|
940
1033
|
return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
|
|
@@ -414,10 +414,13 @@ ${mergedOutput}`);
|
|
|
414
414
|
if (!createdProfileId) return;
|
|
415
415
|
if (typeof window.api?.cmdSpawn !== "function") return;
|
|
416
416
|
const timeoutSec = Math.max(30, Math.floor(Number(timeoutInput.value || "900")));
|
|
417
|
+
const idleTimeout = String(window.api?.settings?.idleTimeout || "30m").trim() || "30m";
|
|
417
418
|
const loginArgs = buildArgs([
|
|
418
419
|
window.api.pathJoin("apps", "webauto", "entry", "profilepool.mjs"),
|
|
419
420
|
"login-profile",
|
|
420
421
|
createdProfileId,
|
|
422
|
+
"--idle-timeout",
|
|
423
|
+
idleTimeout,
|
|
421
424
|
"--timeout-sec",
|
|
422
425
|
String(timeoutSec),
|
|
423
426
|
"--check-interval-sec",
|
|
@@ -1975,10 +1978,13 @@ function renderSetupWizard(root, ctx2) {
|
|
|
1975
1978
|
await refreshAccounts();
|
|
1976
1979
|
setupStatusText.textContent = `\u8D26\u53F7 ${profileId} \u5DF2\u521B\u5EFA\uFF0C\u7B49\u5F85\u767B\u5F55...`;
|
|
1977
1980
|
const timeoutSec = ctx2.api.settings?.timeouts?.loginTimeoutSec || 900;
|
|
1981
|
+
const idleTimeout = String(ctx2.api?.settings?.idleTimeout || "30m").trim() || "30m";
|
|
1978
1982
|
const loginArgs = [
|
|
1979
1983
|
ctx2.api.pathJoin("apps", "webauto", "entry", "profilepool.mjs"),
|
|
1980
1984
|
"login-profile",
|
|
1981
1985
|
profileId,
|
|
1986
|
+
"--idle-timeout",
|
|
1987
|
+
idleTimeout,
|
|
1982
1988
|
"--wait-sync",
|
|
1983
1989
|
"false",
|
|
1984
1990
|
"--timeout-sec",
|
|
@@ -4357,6 +4363,7 @@ Profile ID: ${acc.profileId}
|
|
|
4357
4363
|
async function openAccountLogin(account, options = {}) {
|
|
4358
4364
|
if (!String(account.profileId || "").trim()) return false;
|
|
4359
4365
|
const platform = getPlatformInfo(account.platform);
|
|
4366
|
+
const idleTimeout = String(ctx2.api?.settings?.idleTimeout || "30m").trim() || "30m";
|
|
4360
4367
|
const timeoutSec = Math.max(30, Number(ctx2.api?.settings?.timeouts?.loginTimeoutSec || 900));
|
|
4361
4368
|
account.status = "pending";
|
|
4362
4369
|
account.statusView = "pending";
|
|
@@ -4372,6 +4379,8 @@ Profile ID: ${acc.profileId}
|
|
|
4372
4379
|
account.profileId,
|
|
4373
4380
|
"--url",
|
|
4374
4381
|
platform.loginUrl,
|
|
4382
|
+
"--idle-timeout",
|
|
4383
|
+
idleTimeout,
|
|
4375
4384
|
"--wait-sync",
|
|
4376
4385
|
"false",
|
|
4377
4386
|
"--timeout-sec",
|
|
@@ -4417,6 +4426,7 @@ Profile ID: ${acc.profileId}
|
|
|
4417
4426
|
await ctx2.refreshSettings();
|
|
4418
4427
|
}
|
|
4419
4428
|
}
|
|
4429
|
+
const idleTimeout = String(ctx2.api?.settings?.idleTimeout || "30m").trim() || "30m";
|
|
4420
4430
|
const timeoutSec = ctx2.api.settings?.timeouts?.loginTimeoutSec || 900;
|
|
4421
4431
|
await ctx2.api.cmdSpawn({
|
|
4422
4432
|
title: `\u767B\u5F55 ${alias || profileId}`,
|
|
@@ -4425,6 +4435,8 @@ Profile ID: ${acc.profileId}
|
|
|
4425
4435
|
ctx2.api.pathJoin("apps", "webauto", "entry", "profilepool.mjs"),
|
|
4426
4436
|
"login-profile",
|
|
4427
4437
|
profileId,
|
|
4438
|
+
"--idle-timeout",
|
|
4439
|
+
idleTimeout,
|
|
4428
4440
|
"--wait-sync",
|
|
4429
4441
|
"false",
|
|
4430
4442
|
"--timeout-sec",
|
|
@@ -9,10 +9,45 @@ import { fileURLToPath } from 'node:url';
|
|
|
9
9
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
10
|
const APP_ROOT = path.resolve(__dirname, '..');
|
|
11
11
|
const ROOT = path.resolve(APP_ROOT, '..', '..');
|
|
12
|
-
const CONTROL_FILE = path.join(os.homedir(), '.webauto', 'run', 'ui-cli.json');
|
|
13
12
|
const DEFAULT_HOST = process.env.WEBAUTO_UI_CLI_HOST || '127.0.0.1';
|
|
14
13
|
const DEFAULT_PORT = Number(process.env.WEBAUTO_UI_CLI_PORT || 7716);
|
|
15
14
|
|
|
15
|
+
function normalizePathForPlatform(raw, platform = process.platform) {
|
|
16
|
+
const input = String(raw || '').trim();
|
|
17
|
+
const isWinPath = platform === 'win32' || /^[A-Za-z]:[\\/]/.test(input);
|
|
18
|
+
const pathApi = isWinPath ? path.win32 : path;
|
|
19
|
+
return isWinPath ? pathApi.normalize(input) : path.resolve(input);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeLegacyWebautoRoot(raw, platform = process.platform) {
|
|
23
|
+
const pathApi = platform === 'win32' ? path.win32 : path;
|
|
24
|
+
const resolved = normalizePathForPlatform(raw, platform);
|
|
25
|
+
const base = pathApi.basename(resolved).toLowerCase();
|
|
26
|
+
return (base === '.webauto' || base === 'webauto')
|
|
27
|
+
? resolved
|
|
28
|
+
: pathApi.join(resolved, '.webauto');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resolveWebautoRoot() {
|
|
32
|
+
const explicitHome = String(process.env.WEBAUTO_HOME || '').trim();
|
|
33
|
+
if (explicitHome) return normalizePathForPlatform(explicitHome);
|
|
34
|
+
|
|
35
|
+
const legacyRoot = String(process.env.WEBAUTO_ROOT || process.env.WEBAUTO_PORTABLE_ROOT || '').trim();
|
|
36
|
+
if (legacyRoot) return normalizeLegacyWebautoRoot(legacyRoot);
|
|
37
|
+
|
|
38
|
+
if (process.platform === 'win32') {
|
|
39
|
+
try {
|
|
40
|
+
if (existsSync('D:\\')) return 'D:\\webauto';
|
|
41
|
+
} catch {
|
|
42
|
+
// ignore drive probing errors
|
|
43
|
+
}
|
|
44
|
+
return path.join(os.homedir(), '.webauto');
|
|
45
|
+
}
|
|
46
|
+
return path.join(os.homedir(), '.webauto');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const CONTROL_FILE = path.join(resolveWebautoRoot(), 'run', 'ui-cli.json');
|
|
50
|
+
|
|
16
51
|
const args = minimist(process.argv.slice(2), {
|
|
17
52
|
boolean: ['help', 'json', 'auto-start', 'build', 'install', 'continue-on-error', 'exact', 'keep-open', 'detailed'],
|
|
18
53
|
string: ['host', 'port', 'selector', 'value', 'text', 'key', 'tab', 'label', 'state', 'file', 'output', 'timeout', 'interval', 'nth'],
|
|
@@ -169,7 +169,7 @@ Usage:
|
|
|
169
169
|
webauto account sync <profileId|all> [--pending-while-login] [--resolve-alias] [--json]
|
|
170
170
|
|
|
171
171
|
Notes:
|
|
172
|
-
- 账号数据默认保存到 ~/.webauto
|
|
172
|
+
- 账号数据默认保存到 WEBAUTO 根目录下的 accounts(Windows 优先 D:/webauto,缺失时回落 ~/.webauto,可用 WEBAUTO_HOME 覆盖)
|
|
173
173
|
- list 默认按 profile 展示账号有效态(valid/invalid)
|
|
174
174
|
- add 会自动创建并关联 profile/fingerprint(未指定时自动编号)
|
|
175
175
|
- login 会通过 @web-auto/camo 拉起浏览器并绑定账号 profile
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import os from 'node:os';
|
|
3
2
|
import path from 'node:path';
|
|
4
3
|
import {
|
|
5
4
|
ensureProfile,
|
|
6
5
|
resolveFingerprintsRoot,
|
|
7
6
|
resolveNextProfileId,
|
|
8
7
|
resolveProfilesRoot,
|
|
8
|
+
resolveWebautoRoot,
|
|
9
9
|
} from './profilepool.mjs';
|
|
10
10
|
|
|
11
11
|
const INDEX_FILE = 'index.json';
|
|
@@ -119,18 +119,6 @@ function ensureSafeName(name, field) {
|
|
|
119
119
|
return value;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
function resolvePortableRoot() {
|
|
123
|
-
const root = String(process.env.WEBAUTO_PORTABLE_ROOT || process.env.WEBAUTO_ROOT || '').trim();
|
|
124
|
-
return root ? path.join(root, '.webauto') : '';
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export function resolveWebautoRoot() {
|
|
128
|
-
const portableRoot = resolvePortableRoot();
|
|
129
|
-
if (portableRoot) return portableRoot;
|
|
130
|
-
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
131
|
-
return path.join(home, '.webauto');
|
|
132
|
-
}
|
|
133
|
-
|
|
134
122
|
export function resolveAccountsRoot() {
|
|
135
123
|
const envRoot = String(process.env.WEBAUTO_PATHS_ACCOUNTS || '').trim();
|
|
136
124
|
if (envRoot) return envRoot;
|
|
@@ -2,32 +2,64 @@ import os from 'node:os';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
|
|
5
|
-
function
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
function hasDrive(letter) {
|
|
6
|
+
if (process.platform !== 'win32') return false;
|
|
7
|
+
try {
|
|
8
|
+
return fs.existsSync(`${String(letter || '').replace(/[^A-Za-z]/g, '').toUpperCase()}:\\`);
|
|
9
|
+
} catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
8
12
|
}
|
|
9
13
|
|
|
10
|
-
function
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
function normalizePathForPlatform(input, platform = process.platform) {
|
|
15
|
+
const raw = String(input || '').trim();
|
|
16
|
+
const isWinPath = platform === 'win32' || /^[A-Za-z]:[\\/]/.test(raw);
|
|
17
|
+
const pathApi = isWinPath ? path.win32 : path;
|
|
18
|
+
return isWinPath ? pathApi.normalize(raw) : path.resolve(raw);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeLegacyWebautoRoot(input, platform = process.platform) {
|
|
22
|
+
const pathApi = platform === 'win32' ? path.win32 : path;
|
|
23
|
+
const resolved = normalizePathForPlatform(input, platform);
|
|
24
|
+
const base = pathApi.basename(resolved).toLowerCase();
|
|
25
|
+
if (base === '.webauto' || base === 'webauto') return resolved;
|
|
26
|
+
return pathApi.join(resolved, '.webauto');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveHomeDir(platform = process.platform) {
|
|
30
|
+
if (platform === 'win32') return process.env.USERPROFILE || os.homedir();
|
|
14
31
|
return process.env.HOME || os.homedir();
|
|
15
32
|
}
|
|
16
33
|
|
|
34
|
+
export function resolveWebautoRoot(options = {}) {
|
|
35
|
+
const env = options.env || process.env;
|
|
36
|
+
const platform = String(options.platform || process.platform);
|
|
37
|
+
const homeDir = String(options.homeDir || resolveHomeDir(platform));
|
|
38
|
+
const pathApi = platform === 'win32' ? path.win32 : path;
|
|
39
|
+
|
|
40
|
+
const explicitHome = String(env.WEBAUTO_HOME || '').trim();
|
|
41
|
+
if (explicitHome) return normalizePathForPlatform(explicitHome, platform);
|
|
42
|
+
|
|
43
|
+
const legacyRoot = String(env.WEBAUTO_ROOT || env.WEBAUTO_PORTABLE_ROOT || '').trim();
|
|
44
|
+
if (legacyRoot) return normalizeLegacyWebautoRoot(legacyRoot, platform);
|
|
45
|
+
|
|
46
|
+
if (platform === 'win32') {
|
|
47
|
+
const dDriveExists = typeof options.hasDDrive === 'boolean' ? options.hasDDrive : hasDrive('D');
|
|
48
|
+
return dDriveExists ? 'D:\\webauto' : pathApi.join(homeDir, '.webauto');
|
|
49
|
+
}
|
|
50
|
+
return pathApi.join(homeDir, '.webauto');
|
|
51
|
+
}
|
|
52
|
+
|
|
17
53
|
export function resolveProfilesRoot() {
|
|
18
54
|
const envProfiles = String(process.env.WEBAUTO_PATHS_PROFILES || '').trim();
|
|
19
55
|
if (envProfiles) return envProfiles;
|
|
20
|
-
|
|
21
|
-
if (portableRoot) return path.join(portableRoot, 'profiles');
|
|
22
|
-
return path.join(resolveHomeDir(), '.webauto', 'profiles');
|
|
56
|
+
return path.join(resolveWebautoRoot(), 'profiles');
|
|
23
57
|
}
|
|
24
58
|
|
|
25
59
|
export function resolveFingerprintsRoot() {
|
|
26
60
|
const envFps = String(process.env.WEBAUTO_PATHS_FINGERPRINTS || '').trim();
|
|
27
61
|
if (envFps) return envFps;
|
|
28
|
-
|
|
29
|
-
if (portableRoot) return path.join(portableRoot, 'fingerprints');
|
|
30
|
-
return path.join(resolveHomeDir(), '.webauto', 'fingerprints');
|
|
62
|
+
return path.join(resolveWebautoRoot(), 'fingerprints');
|
|
31
63
|
}
|
|
32
64
|
|
|
33
65
|
export function listProfiles() {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import os from 'node:os';
|
|
3
2
|
import path from 'node:path';
|
|
3
|
+
import { resolveWebautoRoot } from './profilepool.mjs';
|
|
4
4
|
|
|
5
5
|
const INDEX_FILE = 'index.json';
|
|
6
6
|
const DEFAULT_COMMAND_TYPE = 'xhs-unified';
|
|
@@ -78,18 +78,6 @@ function normalizeMaxRuns(value, fallback = null) {
|
|
|
78
78
|
return Math.max(1, Math.floor(n));
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
function resolvePortableRoot() {
|
|
82
|
-
const root = String(process.env.WEBAUTO_PORTABLE_ROOT || process.env.WEBAUTO_ROOT || '').trim();
|
|
83
|
-
return root ? path.join(root, '.webauto') : '';
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function resolveWebautoRoot() {
|
|
87
|
-
const portableRoot = resolvePortableRoot();
|
|
88
|
-
if (portableRoot) return portableRoot;
|
|
89
|
-
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
90
|
-
return path.join(home, '.webauto');
|
|
91
|
-
}
|
|
92
|
-
|
|
93
81
|
export function resolveSchedulesRoot() {
|
|
94
82
|
const explicit = String(process.env.WEBAUTO_PATHS_SCHEDULES || '').trim();
|
|
95
83
|
if (explicit) return explicit;
|
|
@@ -252,10 +252,37 @@ function runPackageCommand(packageName, commandArgs) {
|
|
|
252
252
|
return run(resolveNpmBin(), ['exec', '--yes', `--package=${packageName}`, '--', ...commandArgs]);
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
-
function
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
255
|
+
function normalizePathForPlatform(raw, platform = process.platform) {
|
|
256
|
+
const input = String(raw || '').trim();
|
|
257
|
+
const isWinPath = platform === 'win32' || /^[A-Za-z]:[\\/]/.test(input);
|
|
258
|
+
const pathApi = isWinPath ? path.win32 : path;
|
|
259
|
+
return isWinPath ? pathApi.normalize(input) : path.resolve(input);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function normalizeLegacyWebautoRoot(raw, platform = process.platform) {
|
|
263
|
+
const pathApi = platform === 'win32' ? path.win32 : path;
|
|
264
|
+
const resolved = normalizePathForPlatform(raw, platform);
|
|
265
|
+
const base = pathApi.basename(resolved).toLowerCase();
|
|
266
|
+
if (base === '.webauto' || base === 'webauto') return resolved;
|
|
267
|
+
return pathApi.join(resolved, '.webauto');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function resolveWebautoRoot(options = {}) {
|
|
271
|
+
const env = options.env || process.env;
|
|
272
|
+
const platform = String(options.platform || process.platform);
|
|
273
|
+
const pathApi = platform === 'win32' ? path.win32 : path;
|
|
274
|
+
const homeDir = String(options.homeDir || os.homedir());
|
|
275
|
+
const explicitHome = String(env.WEBAUTO_HOME || '').trim();
|
|
276
|
+
if (explicitHome) return normalizePathForPlatform(explicitHome, platform);
|
|
277
|
+
|
|
278
|
+
const legacyRoot = String(env.WEBAUTO_ROOT || env.WEBAUTO_PORTABLE_ROOT || '').trim();
|
|
279
|
+
if (legacyRoot) return normalizeLegacyWebautoRoot(legacyRoot, platform);
|
|
280
|
+
|
|
281
|
+
const hasDDrive = typeof options.hasDDrive === 'boolean'
|
|
282
|
+
? options.hasDDrive
|
|
283
|
+
: (platform === 'win32' && existsSync('D:\\'));
|
|
284
|
+
if (platform === 'win32') return hasDDrive ? 'D:\\webauto' : pathApi.join(homeDir, '.webauto');
|
|
285
|
+
return pathApi.join(homeDir, '.webauto');
|
|
259
286
|
}
|
|
260
287
|
|
|
261
288
|
function resolveGeoIPPath() {
|
package/bin/webauto.mjs
CHANGED
|
@@ -2,11 +2,67 @@
|
|
|
2
2
|
import minimist from 'minimist';
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
4
|
import { existsSync, writeFileSync, readFileSync, mkdirSync } from 'node:fs';
|
|
5
|
+
import os from 'node:os';
|
|
5
6
|
import path from 'node:path';
|
|
6
7
|
import { fileURLToPath } from 'node:url';
|
|
7
8
|
|
|
8
9
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
9
|
-
|
|
10
|
+
|
|
11
|
+
function normalizePathForPlatform(raw, platform = process.platform) {
|
|
12
|
+
const input = String(raw || '').trim();
|
|
13
|
+
const isWinPath = platform === 'win32' || /^[A-Za-z]:[\\/]/.test(input);
|
|
14
|
+
const pathApi = isWinPath ? path.win32 : path;
|
|
15
|
+
return isWinPath ? pathApi.normalize(input) : path.resolve(input);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function normalizeLegacyWebautoRoot(raw, platform = process.platform) {
|
|
19
|
+
const pathApi = platform === 'win32' ? path.win32 : path;
|
|
20
|
+
const resolved = normalizePathForPlatform(raw, platform);
|
|
21
|
+
const base = pathApi.basename(resolved).toLowerCase();
|
|
22
|
+
return (base === '.webauto' || base === 'webauto')
|
|
23
|
+
? resolved
|
|
24
|
+
: pathApi.join(resolved, '.webauto');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function resolveWebautoHome(env = process.env, platform = process.platform) {
|
|
28
|
+
const explicitHome = String(env.WEBAUTO_HOME || '').trim();
|
|
29
|
+
if (explicitHome) return normalizePathForPlatform(explicitHome, platform);
|
|
30
|
+
|
|
31
|
+
const legacyRoot = String(env.WEBAUTO_ROOT || env.WEBAUTO_PORTABLE_ROOT || '').trim();
|
|
32
|
+
if (legacyRoot) return normalizeLegacyWebautoRoot(legacyRoot, platform);
|
|
33
|
+
|
|
34
|
+
const homeDir = platform === 'win32'
|
|
35
|
+
? (env.USERPROFILE || os.homedir())
|
|
36
|
+
: (env.HOME || os.homedir());
|
|
37
|
+
if (platform === 'win32') {
|
|
38
|
+
try {
|
|
39
|
+
if (existsSync('D:\\')) return 'D:\\webauto';
|
|
40
|
+
} catch {
|
|
41
|
+
// ignore drive detection errors
|
|
42
|
+
}
|
|
43
|
+
return path.win32.join(homeDir, '.webauto');
|
|
44
|
+
}
|
|
45
|
+
return path.join(homeDir, '.webauto');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function applyDefaultRuntimeEnv() {
|
|
49
|
+
if (!String(process.env.WEBAUTO_REPO_ROOT || '').trim()) {
|
|
50
|
+
process.env.WEBAUTO_REPO_ROOT = ROOT;
|
|
51
|
+
}
|
|
52
|
+
if (isGlobalInstall() && !String(process.env.WEBAUTO_SKIP_BUILD_CHECK || '').trim()) {
|
|
53
|
+
process.env.WEBAUTO_SKIP_BUILD_CHECK = '1';
|
|
54
|
+
}
|
|
55
|
+
if (
|
|
56
|
+
!String(process.env.WEBAUTO_HOME || '').trim()
|
|
57
|
+
&& !String(process.env.WEBAUTO_ROOT || process.env.WEBAUTO_PORTABLE_ROOT || '').trim()
|
|
58
|
+
) {
|
|
59
|
+
process.env.WEBAUTO_HOME = resolveWebautoHome();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
applyDefaultRuntimeEnv();
|
|
64
|
+
|
|
65
|
+
const STATE_FILE = path.join(resolveWebautoHome(), 'cli-state.json');
|
|
10
66
|
|
|
11
67
|
function loadState() {
|
|
12
68
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@web-auto/webauto",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"webauto": "bin/webauto.mjs"
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"bin/",
|
|
10
10
|
"dist/",
|
|
11
11
|
"modules/",
|
|
12
|
+
"runtime/",
|
|
12
13
|
"scripts/",
|
|
13
14
|
"services/",
|
|
14
15
|
"apps/desktop-console/dist/",
|
|
@@ -35,7 +36,7 @@
|
|
|
35
36
|
"!apps/desktop-console/dist/**/*.map"
|
|
36
37
|
],
|
|
37
38
|
"dependencies": {
|
|
38
|
-
"@web-auto/camo": "^0.1.
|
|
39
|
+
"@web-auto/camo": "^0.1.13",
|
|
39
40
|
"ajv": "^8.18.0",
|
|
40
41
|
"esbuild-register": "^3.6.0",
|
|
41
42
|
"iconv-lite": "^0.6.3",
|
|
@@ -65,6 +66,11 @@
|
|
|
65
66
|
"test:webauto:schedule:unit": "node --test tests/unit/webauto/schedule-store.test.mjs tests/unit/webauto/schedule-cli.test.mjs",
|
|
66
67
|
"test:webauto:ui-cli:unit": "node --test tests/unit/webauto/ui-cli-command.test.mjs",
|
|
67
68
|
"test:webauto:install:unit": "node --test tests/unit/webauto/xhs-install.test.mjs",
|
|
69
|
+
"test:e2e-ui": "npx vitest --config tests/e2e-ui/vitest.config.ts",
|
|
70
|
+
"test:e2e-ui:l0": "npx vitest --config tests/e2e-ui/vitest.config.ts tests/e2e-ui/contracts/api.test.ts",
|
|
71
|
+
"test:e2e-ui:l1": "npx vitest --config tests/e2e-ui/vitest.config.ts tests/e2e-ui/controls/full-cover.test.ts",
|
|
72
|
+
"test:e2e-ui:l2": "npx vitest --config tests/e2e-ui/vitest.config.ts tests/e2e-ui/flows/task-run.test.ts",
|
|
73
|
+
"test:e2e-ui:l3": "npx vitest --config tests/e2e-ui/vitest.config.ts tests/e2e-ui/stability/restart.test.ts",
|
|
68
74
|
"test:webauto:schedule:coverage": "c8 --reporter=text --reporter=text-summary --check-coverage --lines 85 --functions 85 --branches 60 --statements 85 --include apps/webauto/entry/lib/schedule-store.mjs --include apps/webauto/entry/schedule.mjs node --test tests/unit/webauto/schedule-store.test.mjs tests/unit/webauto/schedule-cli.test.mjs",
|
|
69
75
|
"test:ci": "npm test && npm --prefix apps/desktop-console run test:renderer",
|
|
70
76
|
"coverage:ci": "node scripts/test/run-coverage.mjs",
|
|
@@ -103,6 +109,7 @@
|
|
|
103
109
|
"playwright": "^1.58.2",
|
|
104
110
|
"ts-node": "^10.9.2",
|
|
105
111
|
"tsx": "^4.16.5",
|
|
106
|
-
"typescript": "^5.9.3"
|
|
112
|
+
"typescript": "^5.9.3",
|
|
113
|
+
"vitest": "^4.0.18"
|
|
107
114
|
}
|
|
108
115
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Runtime Utilities
|
|
2
|
+
|
|
3
|
+
运行时工具统一收敛到 `runtime/infra/utils/scripts/`。
|
|
4
|
+
|
|
5
|
+
当前保留目录:
|
|
6
|
+
- `scripts/development/`:开发期调试脚本(会话内执行、高亮等)
|
|
7
|
+
- `scripts/service/`:服务启停与端口清理脚本
|
|
8
|
+
- `scripts/` 根脚本:跨场景工具(例如 `test-services.mjs`)
|
|
9
|
+
|
|
10
|
+
已移除:
|
|
11
|
+
- `local-dev/`
|
|
12
|
+
- `scripts/local-dev/`
|
|
13
|
+
- 其它历史遗留脚本目录
|
|
File without changes
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Post a local JS file (or inline) to Workflow API /browser/eval for the given session
|
|
3
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
4
|
+
|
|
5
|
+
async function main() {
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
if (args.length < 2) {
|
|
8
|
+
console.log('Usage: node scripts/dev/eval-in-session.mjs <sessionId> <file.js|--code="JS..."> [--host=http://127.0.0.1:7701]');
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
const sessionId = args[0];
|
|
12
|
+
let codeArg = args[1];
|
|
13
|
+
let host = 'http://127.0.0.1:7701';
|
|
14
|
+
for (const a of args.slice(2)) {
|
|
15
|
+
if (a.startsWith('--host=')) host = a.slice('--host='.length);
|
|
16
|
+
}
|
|
17
|
+
let script = '';
|
|
18
|
+
if (codeArg.startsWith('--code=')) {
|
|
19
|
+
script = codeArg.slice('--code='.length);
|
|
20
|
+
} else {
|
|
21
|
+
if (!existsSync(codeArg)) {
|
|
22
|
+
console.error('File not found:', codeArg);
|
|
23
|
+
process.exit(2);
|
|
24
|
+
}
|
|
25
|
+
script = readFileSync(codeArg, 'utf8');
|
|
26
|
+
}
|
|
27
|
+
const res = await fetch(host + '/browser/eval', {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: { 'Content-Type': 'application/json' },
|
|
30
|
+
body: JSON.stringify({ sessionId, script })
|
|
31
|
+
}).then(r => r.json()).catch(e => ({ success: false, error: String(e) }));
|
|
32
|
+
if (!res || !res.success) {
|
|
33
|
+
console.error('Eval failed:', res?.error || 'unknown');
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
console.log('Eval OK:', JSON.stringify(res.value));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
main().catch(e => { console.error(e); process.exit(1); });
|
|
40
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Highlight search containers for 3 seconds: root -> listContainer -> first item
|
|
3
|
+
const host = process.env.WORKFLOW_HOST || 'http://127.0.0.1:7701';
|
|
4
|
+
|
|
5
|
+
function arg(k, d){ const a = process.argv.find(x=>x.startsWith(`--${k}=`)); return a ? a.split('=')[1] : d; }
|
|
6
|
+
const sleep = (ms)=>new Promise(r=>setTimeout(r,ms));
|
|
7
|
+
|
|
8
|
+
async function j(u,opt){ const r = await fetch(host+u, { headers:{'Content-Type':'application/json'}, ...(opt||{}) }); return await r.json(); }
|
|
9
|
+
async function lastSession(){ const s = await j('/v1/sessions'); const arr=s.sessions||[]; return arr[arr.length-1]||null; }
|
|
10
|
+
|
|
11
|
+
async function highlight({ sessionId, selector, label }){
|
|
12
|
+
return await j('/v1/containers/highlight', { method:'POST', body: JSON.stringify({ sessionId, containerSelector: selector, label, durationMs: 3000 }) });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function validate({ sessionId, selector }){ return await j('/v1/containers/validate', { method:'POST', body: JSON.stringify({ sessionId, containerSelector: selector }) }); }
|
|
16
|
+
|
|
17
|
+
async function main(){
|
|
18
|
+
const sessionId = arg('sessionId', null) || await lastSession();
|
|
19
|
+
if (!sessionId) { console.error('No active session.'); process.exit(1); }
|
|
20
|
+
// container definitions (must match container registry)
|
|
21
|
+
const rootSel = '.search-ui2024, .search-i18nUi, body';
|
|
22
|
+
const listSel = '.space-common-offerlist, .offer-list, #offer-list';
|
|
23
|
+
const itemSel = '.offer-item, .sm-offer, [class*=offer]';
|
|
24
|
+
|
|
25
|
+
const out = { sessionId, checks: [] };
|
|
26
|
+
for (const [label, sel] of [['ROOT', rootSel], ['LIST', listSel], ['ITEM', itemSel]]){
|
|
27
|
+
const v = await validate({ sessionId, selector: sel });
|
|
28
|
+
out.checks.push({ label, selector: sel, found: !!v.found });
|
|
29
|
+
if (v.found) await highlight({ sessionId, selector: sel, label });
|
|
30
|
+
await sleep(400);
|
|
31
|
+
}
|
|
32
|
+
console.log(JSON.stringify({ ok:true, ...out }, null, 2));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
main().catch(e=>{ console.error(e); process.exit(1); });
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 杀掉占用指定端口的进程(跨平台尽力)
|
|
3
|
+
import { execSync } from 'node:child_process';
|
|
4
|
+
|
|
5
|
+
const port = Number(process.argv[2] || 0);
|
|
6
|
+
if (!port) { console.error('Usage: node runtime/infra/utils/scripts/service/kill-port.mjs <port>'); process.exit(1); }
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
if (process.platform === 'win32') {
|
|
10
|
+
const out = execSync(`netstat -ano | findstr :${port}`, { encoding: 'utf8' });
|
|
11
|
+
const pids = new Set();
|
|
12
|
+
out.split(/\r?\n/).forEach(line=>{ const m=line.trim().match(/\s(\d+)\s*$/); if (m) pids.add(Number(m[1])); });
|
|
13
|
+
for (const pid of pids) { try { execSync(`taskkill /F /PID ${pid}`); console.log(`killed pid ${pid}`); } catch {} }
|
|
14
|
+
if (pids.size===0) console.log('no process found');
|
|
15
|
+
} else {
|
|
16
|
+
const out = execSync(`lsof -ti :${port}`, { encoding: 'utf8' });
|
|
17
|
+
const pids = out.split(/\s+/).map(s=>Number(s.trim())).filter(Boolean);
|
|
18
|
+
for (const pid of pids) { try { process.kill(pid, 'SIGKILL'); console.log(`killed pid ${pid}`); } catch {} }
|
|
19
|
+
if (pids.length===0) console.log('no process found');
|
|
20
|
+
}
|
|
21
|
+
} catch (e) {
|
|
22
|
+
console.log('nothing to kill or command failed');
|
|
23
|
+
}
|
|
24
|
+
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
3
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { setTimeout as wait } from 'node:timers/promises';
|
|
7
|
+
|
|
8
|
+
const API_PID_FILE = process.env.WEBAUTO_API_PID_FILE
|
|
9
|
+
? path.resolve(process.env.WEBAUTO_API_PID_FILE)
|
|
10
|
+
: path.join(os.tmpdir(), 'webauto-api.pid');
|
|
11
|
+
const DIST_SERVER = path.resolve(process.cwd(), 'dist', 'apps', 'webauto', 'server.js');
|
|
12
|
+
|
|
13
|
+
function resolveOnPath(candidates) {
|
|
14
|
+
const pathEnv = process.env.PATH || process.env.Path || '';
|
|
15
|
+
const dirs = String(pathEnv).split(path.delimiter).map((x) => x.trim()).filter(Boolean);
|
|
16
|
+
for (const dir of dirs) {
|
|
17
|
+
for (const name of candidates) {
|
|
18
|
+
const full = path.join(dir, name);
|
|
19
|
+
if (existsSync(full)) return full;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resolveNodeBin() {
|
|
26
|
+
const explicit = String(process.env.WEBAUTO_NODE_BIN || '').trim();
|
|
27
|
+
if (explicit) return explicit;
|
|
28
|
+
const npmNode = String(process.env.npm_node_execpath || '').trim();
|
|
29
|
+
if (npmNode) return npmNode;
|
|
30
|
+
const fromPath = resolveOnPath(process.platform === 'win32' ? ['node.exe', 'node.cmd', 'node'] : ['node']);
|
|
31
|
+
if (fromPath) return fromPath;
|
|
32
|
+
return process.execPath;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function resolveNpmBin() {
|
|
36
|
+
if (process.platform !== 'win32') return 'npm';
|
|
37
|
+
const fromPath = resolveOnPath(['npm.cmd', 'npm.exe', 'npm.bat', 'npm.ps1']);
|
|
38
|
+
return fromPath || 'npm.cmd';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function quoteCmdArg(value) {
|
|
42
|
+
if (!value) return '""';
|
|
43
|
+
if (!/[\s"]/u.test(value)) return value;
|
|
44
|
+
return `"${String(value).replace(/"/g, '""')}"`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function run(command, args) {
|
|
48
|
+
const lower = String(command || '').toLowerCase();
|
|
49
|
+
if (process.platform === 'win32' && (lower.endsWith('.cmd') || lower.endsWith('.bat'))) {
|
|
50
|
+
const cmdLine = [quoteCmdArg(command), ...args.map(quoteCmdArg)].join(' ');
|
|
51
|
+
return spawnSync('cmd.exe', ['/d', '/s', '/c', cmdLine], { stdio: 'inherit', windowsHide: true });
|
|
52
|
+
}
|
|
53
|
+
if (process.platform === 'win32' && lower.endsWith('.ps1')) {
|
|
54
|
+
return spawnSync(
|
|
55
|
+
'powershell.exe',
|
|
56
|
+
['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', command, ...args],
|
|
57
|
+
{ stdio: 'inherit', windowsHide: true },
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
return spawnSync(command, args, { stdio: 'inherit', windowsHide: true });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function health(url='http://127.0.0.1:7701/health'){
|
|
64
|
+
try{
|
|
65
|
+
const res = await fetch(url);
|
|
66
|
+
if (!res.ok) return false;
|
|
67
|
+
const j = await res.json().catch(() => ({}));
|
|
68
|
+
return Boolean(j?.ok ?? true);
|
|
69
|
+
}catch{
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function main(){
|
|
75
|
+
if (!existsSync(DIST_SERVER)) {
|
|
76
|
+
const npmBin = resolveNpmBin();
|
|
77
|
+
const buildRet = run(npmBin, ['run', '-s', 'build:services']);
|
|
78
|
+
if (buildRet.status !== 0) {
|
|
79
|
+
console.error('build:services failed');
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// start
|
|
84
|
+
const child = spawn(resolveNodeBin(), [DIST_SERVER], {
|
|
85
|
+
detached: true,
|
|
86
|
+
stdio: 'ignore',
|
|
87
|
+
windowsHide: true,
|
|
88
|
+
env: {
|
|
89
|
+
...process.env,
|
|
90
|
+
WEBAUTO_RUNTIME_MODE: 'unified',
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
child.unref();
|
|
94
|
+
try {
|
|
95
|
+
mkdirSync(path.dirname(API_PID_FILE), { recursive: true });
|
|
96
|
+
} catch {}
|
|
97
|
+
writeFileSync(API_PID_FILE, String(child.pid));
|
|
98
|
+
// wait for health
|
|
99
|
+
for(let i=0;i<20;i++){ if (await health()) { console.log('API started. PID', child.pid); return; } await wait(500); }
|
|
100
|
+
console.error('API did not become healthy in time'); process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
main().catch(e=>{ console.error(e); process.exit(1); });
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execSync, spawn } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { setTimeout as wait } from 'node:timers/promises';
|
|
7
|
+
|
|
8
|
+
function resolveWebautoRoot() {
|
|
9
|
+
const explicitHome = String(process.env.WEBAUTO_HOME || '').trim();
|
|
10
|
+
if (explicitHome) return path.resolve(explicitHome);
|
|
11
|
+
|
|
12
|
+
const legacyRoot = String(process.env.WEBAUTO_ROOT || process.env.WEBAUTO_PORTABLE_ROOT || '').trim();
|
|
13
|
+
if (legacyRoot) {
|
|
14
|
+
const normalized = path.resolve(legacyRoot);
|
|
15
|
+
const base = path.basename(normalized).toLowerCase();
|
|
16
|
+
if (base === '.webauto' || base === 'webauto') return normalized;
|
|
17
|
+
return path.join(normalized, '.webauto');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (process.platform === 'win32') {
|
|
21
|
+
try {
|
|
22
|
+
if (fs.existsSync('D:\\')) return 'D:\\webauto';
|
|
23
|
+
} catch {
|
|
24
|
+
// ignore probing errors
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return path.join(os.homedir(), '.webauto');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const RUN_DIR = path.join(resolveWebautoRoot(), 'run');
|
|
31
|
+
const PID_FILE = path.join(RUN_DIR, 'browser-service.pid');
|
|
32
|
+
const DEFAULT_HOST = process.env.WEBAUTO_BROWSER_HOST || '127.0.0.1';
|
|
33
|
+
const DEFAULT_PORT = Number(process.env.WEBAUTO_BROWSER_PORT || 7704);
|
|
34
|
+
|
|
35
|
+
function isAlive(pid) {
|
|
36
|
+
try {
|
|
37
|
+
process.kill(pid, 0);
|
|
38
|
+
return true;
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function health(host = DEFAULT_HOST, port = DEFAULT_PORT) {
|
|
45
|
+
try {
|
|
46
|
+
const res = await fetch(`http://${host}:${port}/health`);
|
|
47
|
+
if (!res.ok) return false;
|
|
48
|
+
const body = await res.json().catch(() => ({}));
|
|
49
|
+
return Boolean(body?.ok ?? true);
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseArgs() {
|
|
56
|
+
const args = process.argv.slice(2);
|
|
57
|
+
const hostIdx = args.findIndex((item) => item === '--host');
|
|
58
|
+
const portIdx = args.findIndex((item) => item === '--port');
|
|
59
|
+
const host = hostIdx >= 0 && args[hostIdx + 1] ? String(args[hostIdx + 1]) : DEFAULT_HOST;
|
|
60
|
+
const port = portIdx >= 0 && args[portIdx + 1] ? Number(args[portIdx + 1]) : DEFAULT_PORT;
|
|
61
|
+
return {
|
|
62
|
+
host,
|
|
63
|
+
port: Number.isFinite(port) && port > 0 ? port : DEFAULT_PORT,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function latestMtimeMs(targetPath) {
|
|
68
|
+
if (!fs.existsSync(targetPath)) return 0;
|
|
69
|
+
const stat = fs.statSync(targetPath);
|
|
70
|
+
if (stat.isFile()) return Number(stat.mtimeMs || 0);
|
|
71
|
+
if (!stat.isDirectory()) return 0;
|
|
72
|
+
let latest = Number(stat.mtimeMs || 0);
|
|
73
|
+
const stack = [targetPath];
|
|
74
|
+
while (stack.length) {
|
|
75
|
+
const current = stack.pop();
|
|
76
|
+
if (!current) continue;
|
|
77
|
+
let entries = [];
|
|
78
|
+
try {
|
|
79
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
80
|
+
} catch {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
for (const entry of entries) {
|
|
84
|
+
const full = path.join(current, entry.name);
|
|
85
|
+
let entryStat;
|
|
86
|
+
try {
|
|
87
|
+
entryStat = fs.statSync(full);
|
|
88
|
+
} catch {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const mtime = Number(entryStat.mtimeMs || 0);
|
|
92
|
+
if (mtime > latest) latest = mtime;
|
|
93
|
+
if (entryStat.isDirectory()) stack.push(full);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return latest;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function shouldRebuild(distEntry) {
|
|
100
|
+
if (!fs.existsSync(distEntry)) return true;
|
|
101
|
+
if (String(process.env.WEBAUTO_SKIP_BUILD_CHECK || '') === '1') return false;
|
|
102
|
+
if (!fs.existsSync(path.resolve('tsconfig.services.json'))) return false;
|
|
103
|
+
const distMtime = Number(fs.statSync(distEntry).mtimeMs || 0);
|
|
104
|
+
const watchRoots = [
|
|
105
|
+
path.resolve('modules/camo-backend/src'),
|
|
106
|
+
path.resolve('modules/logging/src'),
|
|
107
|
+
];
|
|
108
|
+
const latestSourceMtime = Math.max(...watchRoots.map((root) => latestMtimeMs(root)));
|
|
109
|
+
return latestSourceMtime > distMtime;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function ensureBuild() {
|
|
113
|
+
const distEntry = path.resolve('dist/modules/camo-backend/src/index.js');
|
|
114
|
+
if (!shouldRebuild(distEntry)) return distEntry;
|
|
115
|
+
console.log('[browser-service] backend entry missing/stale, running npm run -s build:services');
|
|
116
|
+
execSync('npm run -s build:services', { stdio: 'inherit' });
|
|
117
|
+
if (!fs.existsSync(distEntry)) {
|
|
118
|
+
throw new Error(`backend entry missing after build: ${distEntry}`);
|
|
119
|
+
}
|
|
120
|
+
return distEntry;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function main() {
|
|
124
|
+
const { host, port } = parseArgs();
|
|
125
|
+
fs.mkdirSync(RUN_DIR, { recursive: true });
|
|
126
|
+
|
|
127
|
+
if (await health(host, port)) {
|
|
128
|
+
console.log(`Browser service already healthy on http://${host}:${port}`);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (fs.existsSync(PID_FILE)) {
|
|
133
|
+
try {
|
|
134
|
+
const oldPid = Number(fs.readFileSync(PID_FILE, 'utf8'));
|
|
135
|
+
if (oldPid && isAlive(oldPid)) {
|
|
136
|
+
console.log(`Browser service already running (pid=${oldPid}).`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
// stale pid file will be overwritten
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const entry = ensureBuild();
|
|
145
|
+
const child = spawn(process.execPath, [entry], {
|
|
146
|
+
detached: true,
|
|
147
|
+
stdio: 'ignore',
|
|
148
|
+
env: {
|
|
149
|
+
...process.env,
|
|
150
|
+
BROWSER_SERVICE_HOST: host,
|
|
151
|
+
BROWSER_SERVICE_PORT: String(port),
|
|
152
|
+
WEBAUTO_BROWSER_HOST: host,
|
|
153
|
+
WEBAUTO_BROWSER_PORT: String(port),
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
child.unref();
|
|
157
|
+
fs.writeFileSync(PID_FILE, String(child.pid));
|
|
158
|
+
|
|
159
|
+
for (let i = 0; i < 30; i += 1) {
|
|
160
|
+
if (await health(host, port)) {
|
|
161
|
+
console.log(`Browser service started (pid=${child.pid}) on http://${host}:${port}`);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
await wait(300);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
throw new Error('browser service did not become healthy in time');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
main().catch((err) => {
|
|
171
|
+
console.error(`[browser-service] start failed: ${err?.message || String(err)}`);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, existsSync, unlinkSync } from 'node:fs';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
const PID_FILE = process.env.WEBAUTO_API_PID_FILE
|
|
8
|
+
? path.resolve(process.env.WEBAUTO_API_PID_FILE)
|
|
9
|
+
: path.join(os.tmpdir(), 'webauto-api.pid');
|
|
10
|
+
|
|
11
|
+
function killPid(pid){
|
|
12
|
+
if (process.platform === 'win32') {
|
|
13
|
+
try {
|
|
14
|
+
spawnSync('taskkill', ['/PID', String(pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true });
|
|
15
|
+
} catch {}
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
try { process.kill(pid, 'SIGTERM'); } catch {}
|
|
19
|
+
setTimeout(() => { try { process.kill(pid, 'SIGKILL'); } catch {} }, 1000);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function main(){
|
|
23
|
+
if (!existsSync(PID_FILE)) { console.log('No pid file.'); return; }
|
|
24
|
+
const pid = Number(readFileSync(PID_FILE,'utf8').trim());
|
|
25
|
+
if (pid>0) killPid(pid);
|
|
26
|
+
try { unlinkSync(PID_FILE); } catch {}
|
|
27
|
+
console.log('API stopped.');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
main().catch(e=>{ console.error(e); process.exit(1); });
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execSync } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { setTimeout as wait } from 'node:timers/promises';
|
|
7
|
+
|
|
8
|
+
const RUN_DIR = path.join(os.homedir(), '.webauto', 'run');
|
|
9
|
+
const PID_FILE = path.join(RUN_DIR, 'browser-service.pid');
|
|
10
|
+
const DEFAULT_PORT = Number(process.env.WEBAUTO_BROWSER_PORT || 7704);
|
|
11
|
+
|
|
12
|
+
function isAlive(pid) {
|
|
13
|
+
try {
|
|
14
|
+
process.kill(pid, 0);
|
|
15
|
+
return true;
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function killByPort(port) {
|
|
22
|
+
try {
|
|
23
|
+
if (process.platform === 'win32') {
|
|
24
|
+
const out = execSync(`netstat -ano | findstr :${port}`, { encoding: 'utf8' });
|
|
25
|
+
const pids = new Set();
|
|
26
|
+
out.split(/\r?\n/).forEach((line) => {
|
|
27
|
+
const match = line.trim().match(/\s(\d+)\s*$/);
|
|
28
|
+
if (match) pids.add(Number(match[1]));
|
|
29
|
+
});
|
|
30
|
+
for (const pid of pids) {
|
|
31
|
+
try {
|
|
32
|
+
execSync(`taskkill /F /PID ${pid}`, { stdio: 'ignore' });
|
|
33
|
+
} catch {
|
|
34
|
+
// ignore
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return pids.size > 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const out = execSync(`lsof -ti :${port} || true`, { encoding: 'utf8' });
|
|
41
|
+
const pids = out.split(/\s+/).map((item) => Number(item.trim())).filter(Boolean);
|
|
42
|
+
for (const pid of pids) {
|
|
43
|
+
try {
|
|
44
|
+
process.kill(pid, 'SIGKILL');
|
|
45
|
+
} catch {
|
|
46
|
+
// ignore
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return pids.length > 0;
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function main() {
|
|
56
|
+
if (!fs.existsSync(PID_FILE)) {
|
|
57
|
+
const killed = killByPort(DEFAULT_PORT);
|
|
58
|
+
console.log(killed ? `Killed processes on :${DEFAULT_PORT}` : 'No PID file found. Service may not be running.');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const pid = Number(fs.readFileSync(PID_FILE, 'utf8'));
|
|
63
|
+
if (!pid) {
|
|
64
|
+
fs.rmSync(PID_FILE, { force: true });
|
|
65
|
+
killByPort(DEFAULT_PORT);
|
|
66
|
+
console.log('Invalid PID file. Performed port cleanup.');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!isAlive(pid)) {
|
|
71
|
+
fs.rmSync(PID_FILE, { force: true });
|
|
72
|
+
killByPort(DEFAULT_PORT);
|
|
73
|
+
console.log(`Process ${pid} is not running. Cleaned stale PID.`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
process.kill(pid, 'SIGTERM');
|
|
79
|
+
} catch {
|
|
80
|
+
// ignore
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (let i = 0; i < 15; i += 1) {
|
|
84
|
+
if (!isAlive(pid)) break;
|
|
85
|
+
await wait(200);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (isAlive(pid)) {
|
|
89
|
+
try {
|
|
90
|
+
process.kill(pid, 'SIGKILL');
|
|
91
|
+
} catch {
|
|
92
|
+
// ignore
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
fs.rmSync(PID_FILE, { force: true });
|
|
97
|
+
killByPort(DEFAULT_PORT);
|
|
98
|
+
console.log(`Browser service stopped (pid=${pid}).`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
main().catch((err) => {
|
|
102
|
+
console.error(`[browser-service] stop failed: ${err?.message || String(err)}`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 测试脚本:验证服务是否能正常启动
|
|
3
|
+
import { spawn, execSync } from 'node:child_process';
|
|
4
|
+
import { setTimeout as wait } from 'node:timers/promises';
|
|
5
|
+
|
|
6
|
+
function log(msg){ console.log(`[test-services] ${msg}`); }
|
|
7
|
+
|
|
8
|
+
async function testHealth(port, name, timeout = 10000) {
|
|
9
|
+
const startTime = Date.now();
|
|
10
|
+
|
|
11
|
+
while (Date.now() - startTime < timeout) {
|
|
12
|
+
try {
|
|
13
|
+
const response = await fetch(`http://127.0.0.1:${port}/health`);
|
|
14
|
+
if (response.ok) {
|
|
15
|
+
const data = await response.json();
|
|
16
|
+
log(`✅ ${name} (${port}): 健康检查通过 - ${JSON.stringify(data)}`);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
} catch (e) {
|
|
20
|
+
// 继续等待
|
|
21
|
+
}
|
|
22
|
+
await wait(500);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
log(`❌ ${name} (${port}): 健康检查失败`);
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function main() {
|
|
30
|
+
log('🧪 测试服务启动...');
|
|
31
|
+
|
|
32
|
+
// 1. 构建服务
|
|
33
|
+
try {
|
|
34
|
+
log('构建服务...');
|
|
35
|
+
execSync('npm run -s build:services', { stdio: 'inherit' });
|
|
36
|
+
log('✅ 构建完成');
|
|
37
|
+
} catch (e) {
|
|
38
|
+
log('❌ 构建失败');
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 2. 清理端口
|
|
43
|
+
const ports = [7704, 7705, 7706];
|
|
44
|
+
for (const port of ports) {
|
|
45
|
+
try {
|
|
46
|
+
execSync(`lsof -ti :${port} | xargs kill -9 || true`, { stdio: 'ignore' });
|
|
47
|
+
} catch {}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 3. 测试 Unified API
|
|
51
|
+
log('测试 Unified API...');
|
|
52
|
+
const unifiedProc = spawn('node', ['dist/apps/webauto/server.js'], {
|
|
53
|
+
env: { ...process.env, WEBAUTO_RUNTIME_MODE: 'unified', WEBAUTO_UNIFIED_PORT: '7704' },
|
|
54
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
unifiedProc.stdout.on('data', (data) => {
|
|
58
|
+
log(`[Unified API] ${data.toString().trim()}`);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
unifiedProc.stderr.on('data', (data) => {
|
|
62
|
+
log(`[Unified API ERROR] ${data.toString().trim()}`);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
unifiedProc.on('error', (err) => {
|
|
66
|
+
log(`❌ Unified API 启动错误: ${err.message}`);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const unifiedOk = await testHealth(7704, 'Unified API', 15000);
|
|
70
|
+
|
|
71
|
+
if (unifiedOk) {
|
|
72
|
+
// 4. 测试基本 API
|
|
73
|
+
try {
|
|
74
|
+
const response = await fetch('http://127.0.0.1:7704/v1/system/state');
|
|
75
|
+
const data = await response.json();
|
|
76
|
+
log(`📋 系统状态: ${JSON.stringify(data)}`);
|
|
77
|
+
} catch (e) {
|
|
78
|
+
log(`❌ 系统状态获取失败: ${e.message}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 5. 清理
|
|
83
|
+
unifiedProc.kill('SIGTERM');
|
|
84
|
+
await wait(1000);
|
|
85
|
+
unifiedProc.kill('SIGKILL');
|
|
86
|
+
|
|
87
|
+
log('🧹 测试完成');
|
|
88
|
+
process.exit(unifiedOk ? 0 : 1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
main().catch(e => {
|
|
92
|
+
console.error('❌ 测试失败:', e.message);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
});
|