@web-auto/webauto 0.1.9 → 0.1.11

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.
@@ -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 existsSync("D:\\");
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 existsSync2 } from "fs";
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 (existsSync2(full)) return full;
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 < 20; i += 1) {
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(300);
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 existsSync3 } from "node:fs";
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 portableRoot = String(process.env.WEBAUTO_PORTABLE_ROOT || process.env.WEBAUTO_ROOT || "").trim();
747
- return portableRoot ? path4.join(portableRoot, ".webauto") : path4.join(os3.homedir(), ".webauto");
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 (existsSync3(full)) return full;
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 || !existsSync3(installRoot)) return false;
834
- return existsSync3(resolveCamoufoxExecutable(installRoot));
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 (!existsSync3(candidate)) continue;
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 (existsSync3(geoIpPath)) {
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
- var CONTROL_FILE = path5.join(os4.homedir(), ".webauto", "run", "ui-cli.json");
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/accounts(可用 WEBAUTO_PATHS_ACCOUNTS 覆盖)
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 resolvePortableRoot() {
6
- const root = String(process.env.WEBAUTO_PORTABLE_ROOT || process.env.WEBAUTO_ROOT || '').trim();
7
- return root ? path.join(root, '.webauto') : '';
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 resolveHomeDir() {
11
- if (process.platform === 'win32') {
12
- return process.env.USERPROFILE || os.homedir();
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
- const portableRoot = resolvePortableRoot();
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
- const portableRoot = resolvePortableRoot();
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 resolveWebautoRoot() {
256
- const portableRoot = String(process.env.WEBAUTO_PORTABLE_ROOT || process.env.WEBAUTO_ROOT || '').trim();
257
- if (portableRoot) return path.join(portableRoot, '.webauto');
258
- return path.join(os.homedir(), '.webauto');
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
- const STATE_FILE = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.webauto', 'cli-state.json');
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.9",
3
+ "version": "0.1.11",
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/",
@@ -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
+ });