@web-auto/webauto 0.1.8 → 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.
Files changed (43) hide show
  1. package/apps/desktop-console/dist/main/index.mjs +909 -105
  2. package/apps/desktop-console/dist/main/preload.mjs +3 -0
  3. package/apps/desktop-console/dist/renderer/index.html +9 -1
  4. package/apps/desktop-console/dist/renderer/index.js +796 -331
  5. package/apps/desktop-console/entry/ui-cli.mjs +59 -9
  6. package/apps/desktop-console/entry/ui-console.mjs +8 -3
  7. package/apps/webauto/entry/account.mjs +70 -9
  8. package/apps/webauto/entry/lib/account-detect.mjs +106 -25
  9. package/apps/webauto/entry/lib/account-store.mjs +122 -35
  10. package/apps/webauto/entry/lib/profilepool.mjs +45 -13
  11. package/apps/webauto/entry/lib/schedule-store.mjs +1 -25
  12. package/apps/webauto/entry/profilepool.mjs +45 -3
  13. package/apps/webauto/entry/schedule.mjs +44 -2
  14. package/apps/webauto/entry/weibo-unified.mjs +2 -2
  15. package/apps/webauto/entry/xhs-install.mjs +248 -52
  16. package/apps/webauto/entry/xhs-unified.mjs +33 -6
  17. package/bin/webauto.mjs +137 -5
  18. package/dist/modules/camo-runtime/src/utils/browser-service.mjs +4 -0
  19. package/dist/services/unified-api/server.js +5 -0
  20. package/dist/services/unified-api/task-state.js +2 -0
  21. package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +142 -14
  22. package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +16 -1
  23. package/modules/camo-runtime/src/autoscript/action-providers/xhs.mjs +104 -0
  24. package/modules/camo-runtime/src/autoscript/runtime.mjs +14 -4
  25. package/modules/camo-runtime/src/autoscript/schema.mjs +9 -0
  26. package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +9 -2
  27. package/modules/camo-runtime/src/container/runtime-core/checkpoint.mjs +107 -1
  28. package/modules/camo-runtime/src/container/runtime-core/subscription.mjs +24 -2
  29. package/modules/camo-runtime/src/utils/browser-service.mjs +4 -0
  30. package/package.json +7 -3
  31. package/runtime/infra/utils/README.md +13 -0
  32. package/runtime/infra/utils/scripts/README.md +0 -0
  33. package/runtime/infra/utils/scripts/development/eval-in-session.mjs +40 -0
  34. package/runtime/infra/utils/scripts/development/highlight-search-containers.mjs +35 -0
  35. package/runtime/infra/utils/scripts/service/kill-port.mjs +24 -0
  36. package/runtime/infra/utils/scripts/service/start-api.mjs +103 -0
  37. package/runtime/infra/utils/scripts/service/start-browser-service.mjs +173 -0
  38. package/runtime/infra/utils/scripts/service/stop-api.mjs +30 -0
  39. package/runtime/infra/utils/scripts/service/stop-browser-service.mjs +104 -0
  40. package/runtime/infra/utils/scripts/test-services.mjs +94 -0
  41. package/scripts/bump-version.mjs +120 -0
  42. package/services/unified-api/server.ts +4 -0
  43. package/services/unified-api/task-state.ts +5 -0
@@ -1,13 +1,13 @@
1
1
  // src/main/index.mts
2
2
  import electron from "electron";
3
- import { spawn as spawn2 } from "node:child_process";
3
+ import { spawn as spawn2, spawnSync as spawnSync2 } from "node:child_process";
4
4
  import os5 from "node:os";
5
- import path6 from "node:path";
5
+ import path7 from "node:path";
6
6
  import { fileURLToPath as fileURLToPath2, pathToFileURL as pathToFileURL3 } from "node:url";
7
- import { mkdirSync, promises as fs4 } from "node:fs";
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,12 +270,16 @@ 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
- var CORE_HEALTH_URLS = ["http://127.0.0.1:7701/health", "http://127.0.0.1:7704/health"];
276
+ var UNIFIED_API_HEALTH_URL = "http://127.0.0.1:7701/health";
277
+ var CAMO_RUNTIME_HEALTH_URL = "http://127.0.0.1:7704/health";
278
+ var CORE_HEALTH_URLS = [UNIFIED_API_HEALTH_URL, CAMO_RUNTIME_HEALTH_URL];
277
279
  var START_API_SCRIPT = path2.join(REPO_ROOT, "runtime", "infra", "utils", "scripts", "service", "start-api.mjs");
278
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;
279
283
  function sleep(ms) {
280
284
  return new Promise((resolve) => setTimeout(resolve, ms));
281
285
  }
@@ -301,7 +305,7 @@ function resolveOnPath(candidates) {
301
305
  for (const dir of dirs) {
302
306
  for (const name of candidates) {
303
307
  const full = path2.join(dir, name);
304
- if (existsSync2(full)) return full;
308
+ if (existsSync3(full)) return full;
305
309
  }
306
310
  }
307
311
  return null;
@@ -323,6 +327,9 @@ async function areCoreServicesHealthy() {
323
327
  const health = await Promise.all(CORE_HEALTH_URLS.map((url) => checkHttpHealth(url)));
324
328
  return health.every(Boolean);
325
329
  }
330
+ async function isUnifiedApiHealthy() {
331
+ return checkHttpHealth(UNIFIED_API_HEALTH_URL);
332
+ }
326
333
  async function runNodeScript(scriptPath, timeoutMs) {
327
334
  return new Promise((resolve) => {
328
335
  const nodeBin = resolveNodeBin();
@@ -353,6 +360,45 @@ async function runNodeScript(scriptPath, timeoutMs) {
353
360
  });
354
361
  });
355
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
+ }
356
402
  async function runCommand(command, args, timeoutMs) {
357
403
  return new Promise((resolve) => {
358
404
  const lower = String(command || "").toLowerCase();
@@ -394,7 +440,7 @@ async function runCommand(command, args, timeoutMs) {
394
440
  }
395
441
  async function startCoreDaemon() {
396
442
  if (await areCoreServicesHealthy()) return true;
397
- 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;
398
444
  if (!startedApi) {
399
445
  console.error("[CoreDaemonManager] Failed to start unified API service");
400
446
  return false;
@@ -405,17 +451,29 @@ async function startCoreDaemon() {
405
451
  4e4
406
452
  );
407
453
  if (!startedBrowser) {
408
- console.error("[CoreDaemonManager] Failed to start camo browser backend");
409
- return false;
410
- }
411
- for (let i = 0; i < 20; i += 1) {
412
- if (await areCoreServicesHealthy()) return true;
413
- await sleep(300);
414
- }
415
- console.error("[CoreDaemonManager] Services still unhealthy after start");
454
+ console.warn("[CoreDaemonManager] Failed to start camo browser backend, continue in degraded mode");
455
+ }
456
+ for (let i = 0; i < 60; i += 1) {
457
+ const [allHealthy, unifiedHealthy] = await Promise.all([
458
+ areCoreServicesHealthy(),
459
+ isUnifiedApiHealthy()
460
+ ]);
461
+ if (allHealthy) return true;
462
+ if (unifiedHealthy) return true;
463
+ await sleep(500);
464
+ }
465
+ console.error("[CoreDaemonManager] Unified API still unhealthy after start");
416
466
  return false;
417
467
  }
418
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;
419
477
  const stoppedApi = await runNodeScript(STOP_API_SCRIPT, 2e4);
420
478
  if (!stoppedApi) {
421
479
  console.error("[CoreDaemonManager] Failed to stop core services");
@@ -730,12 +788,30 @@ var stateBridge = new StateBridge();
730
788
 
731
789
  // src/main/env-check.mts
732
790
  import { spawnSync } from "node:child_process";
733
- import { existsSync as existsSync3 } from "node:fs";
791
+ import { existsSync as existsSync4 } from "node:fs";
734
792
  import path4 from "node:path";
735
793
  import os3 from "node:os";
736
794
  function resolveWebautoRoot() {
737
- const portableRoot = String(process.env.WEBAUTO_PORTABLE_ROOT || process.env.WEBAUTO_ROOT || "").trim();
738
- 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");
739
815
  }
740
816
  function resolveNpxBin2() {
741
817
  if (process.platform !== "win32") return "npx";
@@ -748,7 +824,7 @@ function resolveOnPath2(candidates) {
748
824
  for (const dir of dirs) {
749
825
  for (const name of candidates) {
750
826
  const full = path4.join(dir, name);
751
- if (existsSync3(full)) return full;
827
+ if (existsSync4(full)) return full;
752
828
  }
753
829
  }
754
830
  return null;
@@ -817,6 +893,13 @@ function resolvePathFromOutput(stdout) {
817
893
  }
818
894
  return "";
819
895
  }
896
+ function resolveCamoufoxExecutable(installRoot) {
897
+ return process.platform === "win32" ? path4.join(installRoot, "camoufox.exe") : path4.join(installRoot, "camoufox");
898
+ }
899
+ function isValidCamoufoxInstallRoot(installRoot) {
900
+ if (!installRoot || !existsSync4(installRoot)) return false;
901
+ return existsSync4(resolveCamoufoxExecutable(installRoot));
902
+ }
820
903
  async function checkCamoCli() {
821
904
  const camoCandidates = process.platform === "win32" ? ["camo.cmd", "camo.exe", "camo.bat", "camo.ps1"] : ["camo"];
822
905
  for (const candidate of camoCandidates) {
@@ -832,7 +915,7 @@ async function checkCamoCli() {
832
915
  for (const localRoot of localRoots) {
833
916
  for (const suffix of camoCandidates) {
834
917
  const candidate = path4.resolve(localRoot, suffix);
835
- if (!existsSync3(candidate)) continue;
918
+ if (!existsSync4(candidate)) continue;
836
919
  const ret = runVersionCheck(candidate, ["help"], candidate);
837
920
  if (ret.installed) return ret;
838
921
  }
@@ -858,10 +941,12 @@ async function checkServices() {
858
941
  }
859
942
  async function checkFirefox() {
860
943
  const candidates = process.platform === "win32" ? [
944
+ { command: "camoufox", args: ["path"] },
861
945
  { command: "python", args: ["-m", "camoufox", "path"] },
862
946
  { command: "py", args: ["-3", "-m", "camoufox", "path"] },
863
947
  { command: resolveNpxBin2(), args: ["--yes", "--package=camoufox", "camoufox", "path"] }
864
948
  ] : [
949
+ { command: "camoufox", args: ["path"] },
865
950
  { command: "python3", args: ["-m", "camoufox", "path"] },
866
951
  { command: resolveNpxBin2(), args: ["--yes", "--package=camoufox", "camoufox", "path"] }
867
952
  ];
@@ -874,7 +959,10 @@ async function checkFirefox() {
874
959
  });
875
960
  if (ret.status !== 0) continue;
876
961
  const resolvedPath = resolvePathFromOutput(String(ret.stdout || ""));
877
- return resolvedPath ? { installed: true, path: resolvedPath } : { installed: true };
962
+ if (resolvedPath && isValidCamoufoxInstallRoot(resolvedPath)) {
963
+ return { installed: true, path: resolvedPath };
964
+ }
965
+ if (!resolvedPath) return { installed: true };
878
966
  } catch {
879
967
  }
880
968
  }
@@ -882,7 +970,7 @@ async function checkFirefox() {
882
970
  }
883
971
  async function checkGeoIP() {
884
972
  const geoIpPath = path4.join(resolveWebautoRoot(), "geoip", "GeoLite2-City.mmdb");
885
- if (existsSync3(geoIpPath)) {
973
+ if (existsSync4(geoIpPath)) {
886
974
  return { installed: true, path: geoIpPath };
887
975
  }
888
976
  return { installed: false };
@@ -894,8 +982,16 @@ async function checkEnvironment() {
894
982
  checkFirefox(),
895
983
  checkGeoIP()
896
984
  ]);
897
- const allReady = camo.installed && services.unifiedApi && firefox.installed;
898
- return { camo, services, firefox, geoip, allReady };
985
+ const browserReady = Boolean(firefox.installed || services.camoRuntime);
986
+ const missing = {
987
+ core: !services.unifiedApi,
988
+ runtimeService: !services.camoRuntime,
989
+ camo: !camo.installed,
990
+ runtime: !browserReady,
991
+ geoip: !geoip.installed
992
+ };
993
+ const allReady = camo.installed && services.unifiedApi && browserReady;
994
+ return { camo, services, firefox, geoip, browserReady, missing, allReady };
899
995
  }
900
996
 
901
997
  // src/main/ui-cli-bridge.mts
@@ -905,7 +1001,33 @@ import path5 from "node:path";
905
1001
  import { promises as fs3 } from "node:fs";
906
1002
  var DEFAULT_HOST = "127.0.0.1";
907
1003
  var DEFAULT_PORT = 7716;
908
- 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");
909
1031
  function readInt(input, fallback) {
910
1032
  const n = Number(input);
911
1033
  return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
@@ -985,6 +1107,20 @@ function buildSnapshotScript() {
985
1107
  if ('value' in el) return String(el.value ?? '');
986
1108
  return String(el.textContent || '').trim();
987
1109
  };
1110
+ const firstText = (selectors) => {
1111
+ for (const sel of selectors) {
1112
+ const v = text(sel);
1113
+ if (v) return v;
1114
+ }
1115
+ return '';
1116
+ };
1117
+ const firstValue = (selectors) => {
1118
+ for (const sel of selectors) {
1119
+ const v = value(sel);
1120
+ if (v) return v;
1121
+ }
1122
+ return '';
1123
+ };
988
1124
  const activeTab = document.querySelector('.tab.active');
989
1125
  const errors = Array.from(document.querySelectorAll('#recent-errors-list li'))
990
1126
  .map((el) => String(el.textContent || '').trim())
@@ -1000,10 +1136,10 @@ function buildSnapshotScript() {
1000
1136
  currentPhase: text('#current-phase'),
1001
1137
  currentAction: text('#current-action'),
1002
1138
  progressPercent: text('#progress-percent'),
1003
- keyword: value('#keyword-input'),
1004
- target: value('#target-input'),
1005
- account: value('#account-select'),
1006
- env: value('#env-select'),
1139
+ keyword: firstValue(['#task-keyword', '#keyword-input', '#task-keyword-input']),
1140
+ target: firstValue(['#task-target', '#target-input', '#task-target-input']),
1141
+ account: firstValue(['#task-account', '#task-profile', '#account-select']),
1142
+ env: firstValue(['#task-env', '#env-select']),
1007
1143
  recentErrors: errors,
1008
1144
  ts: new Date().toISOString(),
1009
1145
  };
@@ -1451,28 +1587,337 @@ var UiCliBridge = class {
1451
1587
  }
1452
1588
  };
1453
1589
 
1590
+ // src/main/task-gateway.mts
1591
+ import path6 from "node:path";
1592
+ var KEYWORD_REQUIRED_TYPES = /* @__PURE__ */ new Set(["xhs-unified", "weibo-search", "1688-search"]);
1593
+ var RUN_AT_TYPES = /* @__PURE__ */ new Set(["once", "daily", "weekly"]);
1594
+ function asText(value) {
1595
+ return String(value ?? "").trim();
1596
+ }
1597
+ function asPositiveInt(value, fallback) {
1598
+ const n = Number(value);
1599
+ if (!Number.isFinite(n) || n <= 0) return fallback;
1600
+ return Math.max(1, Math.floor(n));
1601
+ }
1602
+ function asBool(value, fallback) {
1603
+ if (typeof value === "boolean") return value;
1604
+ const text = asText(value).toLowerCase();
1605
+ if (!text) return fallback;
1606
+ if (["1", "true", "yes", "y", "on"].includes(text)) return true;
1607
+ if (["0", "false", "no", "n", "off"].includes(text)) return false;
1608
+ return fallback;
1609
+ }
1610
+ function asOptionalRunAt(value) {
1611
+ const runAt = asText(value);
1612
+ return runAt ? runAt : null;
1613
+ }
1614
+ function normalizeScheduleType(value) {
1615
+ const raw = asText(value);
1616
+ if (raw === "once" || raw === "daily" || raw === "weekly") return raw;
1617
+ return "interval";
1618
+ }
1619
+ function deriveTaskName(commandType, argv) {
1620
+ const keyword = asText(argv.keyword);
1621
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
1622
+ return keyword ? `${commandType}-${keyword}` : `${commandType}-${stamp}`;
1623
+ }
1624
+ function getPlatformFromCommandType(commandType) {
1625
+ const value = asText(commandType).toLowerCase();
1626
+ if (value.startsWith("weibo")) return "weibo";
1627
+ if (value.startsWith("1688")) return "1688";
1628
+ return "xiaohongshu";
1629
+ }
1630
+ function hasExplicitProfileArg(argv) {
1631
+ return Boolean(asText(argv?.profile) || asText(argv?.profiles) || asText(argv?.profilepool));
1632
+ }
1633
+ async function pickDefaultProfileForPlatform(options, platform) {
1634
+ const accountScript = path6.join(options.repoRoot, "apps", "webauto", "entry", "account.mjs");
1635
+ const ret = await options.runJson({
1636
+ title: `account list --platform ${platform}`,
1637
+ cwd: options.repoRoot,
1638
+ args: [accountScript, "list", "--platform", platform, "--json"],
1639
+ timeoutMs: 2e4
1640
+ });
1641
+ if (!ret?.ok) return "";
1642
+ const rows = Array.isArray(ret?.json?.profiles) ? ret.json.profiles : [];
1643
+ const validRows = rows.filter((row) => row?.valid === true && asText(row?.accountId)).sort((a, b) => {
1644
+ const ta = Date.parse(asText(a?.updatedAt) || "") || 0;
1645
+ const tb = Date.parse(asText(b?.updatedAt) || "") || 0;
1646
+ if (tb !== ta) return tb - ta;
1647
+ return asText(a?.profileId).localeCompare(asText(b?.profileId));
1648
+ });
1649
+ return asText(validRows[0]?.profileId) || "";
1650
+ }
1651
+ async function ensureProfileArg(options, commandType, argv) {
1652
+ if (hasExplicitProfileArg(argv)) return argv;
1653
+ const platform = getPlatformFromCommandType(commandType);
1654
+ const profileId = await pickDefaultProfileForPlatform(options, platform);
1655
+ if (!profileId) {
1656
+ throw new Error(`\u672A\u6307\u5B9A Profile\uFF0C\u4E14\u672A\u627E\u5230\u5E73\u53F0(${platform})\u6709\u6548\u8D26\u53F7\u3002\u8BF7\u5148\u5728\u8D26\u53F7\u9875\u767B\u5F55\u5E76\u6821\u9A8C\u540E\u91CD\u8BD5\u3002`);
1657
+ }
1658
+ return {
1659
+ ...argv,
1660
+ profile: profileId
1661
+ };
1662
+ }
1663
+ function normalizeWeiboTaskType(commandType) {
1664
+ if (commandType === "weibo-search") return "search";
1665
+ if (commandType === "weibo-monitor") return "monitor";
1666
+ return "timeline";
1667
+ }
1668
+ function createUiTriggerId() {
1669
+ return `ui-${Date.now()}-${Math.random().toString(16).slice(2, 10)}`;
1670
+ }
1671
+ function normalizeSavePayload(payload) {
1672
+ const commandType = asText(payload?.commandType) || "xhs-unified";
1673
+ const argv = payload?.argv && typeof payload.argv === "object" ? { ...payload.argv } : {};
1674
+ const scheduleType = normalizeScheduleType(payload?.scheduleType);
1675
+ const runAt = asOptionalRunAt(payload?.runAt);
1676
+ const intervalMinutes = asPositiveInt(payload?.intervalMinutes, 30);
1677
+ const maxRunsRaw = Number(payload?.maxRuns);
1678
+ const maxRuns = Number.isFinite(maxRunsRaw) && maxRunsRaw > 0 ? Math.max(1, Math.floor(maxRunsRaw)) : null;
1679
+ const name = asText(payload?.name) || deriveTaskName(commandType, argv);
1680
+ const enabled = asBool(payload?.enabled, true);
1681
+ const id = asText(payload?.id);
1682
+ if (KEYWORD_REQUIRED_TYPES.has(commandType) && !asText(argv.keyword)) {
1683
+ throw new Error("\u5173\u952E\u8BCD\u4E0D\u80FD\u4E3A\u7A7A");
1684
+ }
1685
+ if (commandType.startsWith("weibo-")) {
1686
+ argv["task-type"] = asText(argv["task-type"]) || normalizeWeiboTaskType(commandType);
1687
+ if (commandType === "weibo-monitor" && !asText(argv["user-id"])) {
1688
+ throw new Error("\u5FAE\u535A monitor \u4EFB\u52A1\u9700\u8981 user-id");
1689
+ }
1690
+ }
1691
+ if (RUN_AT_TYPES.has(scheduleType) && !runAt) {
1692
+ throw new Error(`${scheduleType} \u4EFB\u52A1\u9700\u8981\u951A\u70B9\u65F6\u95F4`);
1693
+ }
1694
+ return {
1695
+ id,
1696
+ name,
1697
+ enabled,
1698
+ commandType,
1699
+ scheduleType,
1700
+ intervalMinutes,
1701
+ runAt,
1702
+ maxRuns,
1703
+ argv
1704
+ };
1705
+ }
1706
+ async function runScheduleJson(options, args, timeoutMs) {
1707
+ const script = path6.join(options.repoRoot, "apps", "webauto", "entry", "schedule.mjs");
1708
+ const ret = await options.runJson({
1709
+ title: `schedule ${args.join(" ")}`.trim(),
1710
+ cwd: options.repoRoot,
1711
+ args: [script, ...args, "--json"],
1712
+ timeoutMs: typeof timeoutMs === "number" && timeoutMs > 0 ? timeoutMs : void 0
1713
+ });
1714
+ if (!ret?.ok) {
1715
+ const reason = asText(ret?.error) || asText(ret?.stderr) || asText(ret?.stdout) || "schedule command failed";
1716
+ return { ok: false, error: reason };
1717
+ }
1718
+ return { ok: true, json: ret?.json || {} };
1719
+ }
1720
+ async function scheduleInvoke(options, input) {
1721
+ try {
1722
+ const action = asText(input?.action);
1723
+ const timeoutMs = input?.timeoutMs;
1724
+ if (action === "list") return runScheduleJson(options, ["list"], timeoutMs);
1725
+ if (action === "get") {
1726
+ const taskId = asText(input?.taskId);
1727
+ if (!taskId) return { ok: false, error: "missing taskId" };
1728
+ return runScheduleJson(options, ["get", taskId], timeoutMs);
1729
+ }
1730
+ if (action === "save") {
1731
+ const payload = normalizeSavePayload(input?.payload);
1732
+ payload.argv = await ensureProfileArg(options, payload.commandType, payload.argv);
1733
+ const args = payload.id ? ["update", payload.id] : ["add"];
1734
+ args.push("--name", payload.name);
1735
+ args.push("--enabled", String(payload.enabled));
1736
+ args.push("--command-type", payload.commandType);
1737
+ args.push("--schedule-type", payload.scheduleType);
1738
+ if (RUN_AT_TYPES.has(payload.scheduleType)) {
1739
+ args.push("--run-at", String(payload.runAt || ""));
1740
+ } else {
1741
+ args.push("--interval-minutes", String(payload.intervalMinutes));
1742
+ }
1743
+ args.push("--max-runs", payload.maxRuns === null ? "0" : String(payload.maxRuns));
1744
+ args.push("--argv-json", JSON.stringify(payload.argv));
1745
+ return runScheduleJson(options, args, timeoutMs);
1746
+ }
1747
+ if (action === "run") {
1748
+ const taskId = asText(input?.taskId);
1749
+ if (!taskId) return { ok: false, error: "missing taskId" };
1750
+ return runScheduleJson(options, ["run", taskId], timeoutMs);
1751
+ }
1752
+ if (action === "delete") {
1753
+ const taskId = asText(input?.taskId);
1754
+ if (!taskId) return { ok: false, error: "missing taskId" };
1755
+ return runScheduleJson(options, ["delete", taskId], timeoutMs);
1756
+ }
1757
+ if (action === "export") {
1758
+ const taskId = asText(input?.taskId);
1759
+ return taskId ? runScheduleJson(options, ["export", taskId], timeoutMs) : runScheduleJson(options, ["export"], timeoutMs);
1760
+ }
1761
+ if (action === "import") {
1762
+ const payloadJson = asText(input?.payloadJson);
1763
+ if (!payloadJson) return { ok: false, error: "missing payloadJson" };
1764
+ const mode = asText(input?.mode) === "replace" ? "replace" : "merge";
1765
+ return runScheduleJson(options, ["import", "--payload-json", payloadJson, "--mode", mode], timeoutMs);
1766
+ }
1767
+ if (action === "run-due") {
1768
+ const limit = asPositiveInt(input?.limit, 20);
1769
+ return runScheduleJson(options, ["run-due", "--limit", String(limit)], timeoutMs);
1770
+ }
1771
+ if (action === "daemon-start") {
1772
+ const interval = asPositiveInt(input?.intervalSec, 30);
1773
+ const limit = asPositiveInt(input?.limit, 20);
1774
+ const script = path6.join(options.repoRoot, "apps", "webauto", "entry", "schedule.mjs");
1775
+ const ret = await options.spawnCommand({
1776
+ title: `schedule daemon ${interval}s`,
1777
+ cwd: options.repoRoot,
1778
+ args: [script, "daemon", "--interval-sec", String(Math.max(5, interval)), "--limit", String(limit), "--json"],
1779
+ groupKey: "scheduler"
1780
+ });
1781
+ return { ok: true, runId: asText(ret?.runId) };
1782
+ }
1783
+ return { ok: false, error: `unsupported action: ${action || "<empty>"}` };
1784
+ } catch (err) {
1785
+ return { ok: false, error: err?.message || String(err) };
1786
+ }
1787
+ }
1788
+ async function runEphemeralTask(options, input) {
1789
+ try {
1790
+ const commandType = asText(input?.commandType) || "xhs-unified";
1791
+ let argv = input?.argv && typeof input.argv === "object" ? { ...input.argv } : {};
1792
+ argv = await ensureProfileArg(options, commandType, argv);
1793
+ const profile = asText(argv.profile);
1794
+ const keyword = asText(argv.keyword);
1795
+ const target = asPositiveInt(argv["max-notes"] ?? argv.target, 50);
1796
+ const env = asText(argv.env) || "debug";
1797
+ if (!profile) return { ok: false, error: "\u8BF7\u8F93\u5165 Profile ID" };
1798
+ if (KEYWORD_REQUIRED_TYPES.has(commandType) && !keyword) {
1799
+ return { ok: false, error: "\u8BF7\u8F93\u5165\u5173\u952E\u8BCD" };
1800
+ }
1801
+ if (commandType === "xhs-unified") {
1802
+ const uiTriggerId = createUiTriggerId();
1803
+ const script = path6.join(options.repoRoot, "apps", "webauto", "entry", "xhs-unified.mjs");
1804
+ const ret = await options.spawnCommand({
1805
+ title: `xhs unified: ${keyword}`,
1806
+ cwd: options.repoRoot,
1807
+ groupKey: "xhs-unified",
1808
+ args: [
1809
+ script,
1810
+ "--profile",
1811
+ profile,
1812
+ "--keyword",
1813
+ keyword,
1814
+ "--target",
1815
+ String(target),
1816
+ "--max-notes",
1817
+ String(target),
1818
+ "--env",
1819
+ env,
1820
+ "--do-comments",
1821
+ String(asBool(argv["do-comments"], true)),
1822
+ "--fetch-body",
1823
+ String(asBool(argv["fetch-body"], true)),
1824
+ "--do-likes",
1825
+ String(asBool(argv["do-likes"], false)),
1826
+ "--like-keywords",
1827
+ asText(argv["like-keywords"]),
1828
+ "--ui-trigger-id",
1829
+ uiTriggerId
1830
+ ]
1831
+ });
1832
+ return { ok: true, runId: asText(ret?.runId), commandType, profile, uiTriggerId };
1833
+ }
1834
+ if (commandType === "weibo-search") {
1835
+ const script = path6.join(options.repoRoot, "apps", "webauto", "entry", "weibo-unified.mjs");
1836
+ const ret = await options.spawnCommand({
1837
+ title: `weibo: ${keyword}`,
1838
+ cwd: options.repoRoot,
1839
+ groupKey: "weibo-search",
1840
+ args: [
1841
+ script,
1842
+ "search",
1843
+ "--profile",
1844
+ profile,
1845
+ "--keyword",
1846
+ keyword,
1847
+ "--target",
1848
+ String(target),
1849
+ "--env",
1850
+ env
1851
+ ]
1852
+ });
1853
+ return { ok: true, runId: asText(ret?.runId), commandType, profile };
1854
+ }
1855
+ return { ok: false, error: `\u5F53\u524D\u4EFB\u52A1\u7C7B\u578B\u6682\u4E0D\u652F\u6301\u4EC5\u6267\u884C(\u4E0D\u4FDD\u5B58): ${commandType}` };
1856
+ } catch (err) {
1857
+ return { ok: false, error: err?.message || String(err) };
1858
+ }
1859
+ }
1860
+
1454
1861
  // src/main/index.mts
1455
1862
  var { app, BrowserWindow: BrowserWindow2, ipcMain: ipcMain2, shell, clipboard } = electron;
1456
- var __dirname = path6.dirname(fileURLToPath2(import.meta.url));
1457
- var APP_ROOT = path6.resolve(__dirname, "../..");
1458
- var REPO_ROOT2 = path6.resolve(APP_ROOT, "../..");
1459
- var DESKTOP_HEARTBEAT_FILE = path6.join(
1863
+ var spawnedBrowserProcesses = /* @__PURE__ */ new Set();
1864
+ function trackBrowserProcess(pid) {
1865
+ if (pid > 0) {
1866
+ spawnedBrowserProcesses.add(pid);
1867
+ }
1868
+ }
1869
+ function cleanupAllBrowserProcesses(reason = "ui_close") {
1870
+ console.log(`[process-cleanup] Cleaning up ${spawnedBrowserProcesses.size} browser process(s) (${reason})`);
1871
+ for (const pid of spawnedBrowserProcesses) {
1872
+ try {
1873
+ if (process.platform === "win32") {
1874
+ spawn2("taskkill", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
1875
+ } else {
1876
+ process.kill(pid, "SIGTERM");
1877
+ }
1878
+ } catch (err) {
1879
+ console.warn(`[process-cleanup] Failed to kill PID ${pid}:`, err);
1880
+ }
1881
+ }
1882
+ spawnedBrowserProcesses.clear();
1883
+ console.log(`[process-cleanup] Cleanup complete`);
1884
+ }
1885
+ var __dirname = path7.dirname(fileURLToPath2(import.meta.url));
1886
+ var APP_ROOT = path7.resolve(__dirname, "../..");
1887
+ var REPO_ROOT2 = path7.resolve(APP_ROOT, "../..");
1888
+ function readJsonVersion(filePath) {
1889
+ try {
1890
+ const json = JSON.parse(readFileSync(filePath, "utf8"));
1891
+ return String(json?.version || "").trim();
1892
+ } catch {
1893
+ return "";
1894
+ }
1895
+ }
1896
+ function resolveVersionInfo() {
1897
+ const webauto = readJsonVersion(path7.join(REPO_ROOT2, "package.json")) || "0.0.0";
1898
+ const desktop = readJsonVersion(path7.join(APP_ROOT, "package.json")) || webauto;
1899
+ const windowTitle = `WebAuto Desktop v${webauto}`;
1900
+ const badge = desktop === webauto ? `v${webauto}` : `webauto v${webauto} \xB7 console v${desktop}`;
1901
+ return { webauto, desktop, windowTitle, badge };
1902
+ }
1903
+ var VERSION_INFO = resolveVersionInfo();
1904
+ var DESKTOP_HEARTBEAT_FILE = path7.join(
1460
1905
  os5.homedir(),
1461
1906
  ".webauto",
1462
1907
  "run",
1463
1908
  "desktop-console-heartbeat.json"
1464
1909
  );
1465
1910
  var profileStore = createProfileStore({ repoRoot: REPO_ROOT2 });
1466
- var XHS_SCRIPTS_ROOT = path6.join(REPO_ROOT2, "scripts", "xiaohongshu");
1911
+ var XHS_SCRIPTS_ROOT = path7.join(REPO_ROOT2, "scripts", "xiaohongshu");
1467
1912
  var XHS_FULL_COLLECT_RE = /collect-content\.mjs$/;
1468
1913
  function configureElectronPaths() {
1469
1914
  try {
1470
1915
  const downloadRoot = resolveDefaultDownloadRoot();
1471
- const normalized = path6.normalize(downloadRoot);
1472
- const baseDir = path6.basename(normalized).toLowerCase() === "download" ? path6.dirname(normalized) : normalized;
1473
- const userDataRoot = path6.join(baseDir, "desktop-console");
1474
- const cacheRoot = path6.join(userDataRoot, "cache");
1475
- const gpuCacheRoot = path6.join(cacheRoot, "gpu");
1916
+ const normalized = path7.normalize(downloadRoot);
1917
+ const baseDir = path7.basename(normalized).toLowerCase() === "download" ? path7.dirname(normalized) : normalized;
1918
+ const userDataRoot = path7.join(baseDir, "desktop-console");
1919
+ const cacheRoot = path7.join(userDataRoot, "cache");
1920
+ const gpuCacheRoot = path7.join(cacheRoot, "gpu");
1476
1921
  try {
1477
1922
  mkdirSync(cacheRoot, { recursive: true });
1478
1923
  } catch {
@@ -1485,6 +1930,17 @@ function configureElectronPaths() {
1485
1930
  app.setPath("cache", cacheRoot);
1486
1931
  app.commandLine.appendSwitch("disk-cache-dir", cacheRoot);
1487
1932
  app.commandLine.appendSwitch("gpu-cache-dir", gpuCacheRoot);
1933
+ const disableGpuByDefault = process.platform === "win32" && String(process.env.WEBAUTO_ELECTRON_DISABLE_GPU || "1").trim() !== "0";
1934
+ if (disableGpuByDefault) {
1935
+ try {
1936
+ app.disableHardwareAcceleration();
1937
+ } catch {
1938
+ }
1939
+ app.commandLine.appendSwitch("disable-gpu");
1940
+ app.commandLine.appendSwitch("disable-gpu-compositing");
1941
+ app.commandLine.appendSwitch("disable-direct-composition");
1942
+ app.commandLine.appendSwitch("use-angle", "swiftshader");
1943
+ }
1488
1944
  } catch (err) {
1489
1945
  console.warn("[desktop-console] failed to configure cache paths", err);
1490
1946
  }
@@ -1514,8 +1970,35 @@ var GroupQueue = class {
1514
1970
  };
1515
1971
  var groupQueues = /* @__PURE__ */ new Map();
1516
1972
  var runs = /* @__PURE__ */ new Map();
1973
+ var runLifecycle = /* @__PURE__ */ new Map();
1517
1974
  var trackedRunPids = /* @__PURE__ */ new Set();
1518
1975
  var appExitCleanupPromise = null;
1976
+ function setRunLifecycle(runId, patch) {
1977
+ const rid = String(runId || "").trim();
1978
+ if (!rid) return;
1979
+ const current = runLifecycle.get(rid) || {
1980
+ runId: rid,
1981
+ state: "queued",
1982
+ title: "",
1983
+ queuedAt: now()
1984
+ };
1985
+ const next = {
1986
+ ...current,
1987
+ ...patch,
1988
+ runId: rid
1989
+ };
1990
+ runLifecycle.set(rid, next);
1991
+ if (runLifecycle.size > 400) {
1992
+ const rows = Array.from(runLifecycle.values()).sort((a, b) => (a.queuedAt || 0) - (b.queuedAt || 0));
1993
+ const toDrop = rows.slice(0, Math.max(0, rows.length - 200));
1994
+ for (const row of toDrop) runLifecycle.delete(row.runId);
1995
+ }
1996
+ }
1997
+ function getRunLifecycle(runId) {
1998
+ const rid = String(runId || "").trim();
1999
+ if (!rid) return null;
2000
+ return runLifecycle.get(rid) || null;
2001
+ }
1519
2002
  var UI_HEARTBEAT_TIMEOUT_MS = resolveUiHeartbeatTimeoutMs(process.env);
1520
2003
  var lastUiHeartbeatAt = Date.now();
1521
2004
  var heartbeatWatchdog = null;
@@ -1523,6 +2006,16 @@ var heartbeatTimeoutHandled = false;
1523
2006
  var coreServicesStopRequested = false;
1524
2007
  var coreServiceHeartbeatTimer = null;
1525
2008
  var coreServiceHeartbeatStopped = false;
2009
+ var RUN_LOG_DIR = path7.join(os5.homedir(), ".webauto", "logs");
2010
+ function appendRunLog(runId, line) {
2011
+ const rid = String(runId || "").trim();
2012
+ const text = String(line || "").replace(/\r?\n/g, " ").trim();
2013
+ if (!rid || !text) return;
2014
+ const logPath = path7.join(RUN_LOG_DIR, `ui-run-${rid}.log`);
2015
+ void fs4.mkdir(RUN_LOG_DIR, { recursive: true }).then(() => fs4.appendFile(logPath, `${text}
2016
+ `, "utf8")).catch(() => {
2017
+ });
2018
+ }
1526
2019
  async function writeCoreServiceHeartbeat(status) {
1527
2020
  const filePath = String(process.env.WEBAUTO_HEARTBEAT_FILE || DESKTOP_HEARTBEAT_FILE).trim() || DESKTOP_HEARTBEAT_FILE;
1528
2021
  const payload = {
@@ -1532,7 +2025,7 @@ async function writeCoreServiceHeartbeat(status) {
1532
2025
  source: "desktop-console"
1533
2026
  };
1534
2027
  try {
1535
- await fs4.mkdir(path6.dirname(filePath), { recursive: true });
2028
+ await fs4.mkdir(path7.dirname(filePath), { recursive: true });
1536
2029
  await fs4.writeFile(filePath, JSON.stringify(payload), "utf8");
1537
2030
  } catch {
1538
2031
  }
@@ -1682,7 +2175,7 @@ async function cleanupTrackedRunPidsBestEffort(reason) {
1682
2175
  }
1683
2176
  }
1684
2177
  async function cleanupCamoSessionsBestEffort(reason, includeLocks) {
1685
- const camoCli = path6.join(REPO_ROOT2, "bin", "camoufox-cli.mjs");
2178
+ const camoCli = path7.join(REPO_ROOT2, "bin", "camoufox-cli.mjs");
1686
2179
  const invoke = async (args, timeoutMs = 6e4) => runJson({
1687
2180
  title: `camo ${args.join(" ")}`,
1688
2181
  cwd: REPO_ROOT2,
@@ -1703,6 +2196,7 @@ async function cleanupRuntimeEnvironment(reason, options = {}) {
1703
2196
  killAllRuns(reason);
1704
2197
  await cleanupTrackedRunPidsBestEffort(reason);
1705
2198
  await cleanupCamoSessionsBestEffort(reason, options.includeLockCleanup !== false);
2199
+ cleanupAllBrowserProcesses(reason);
1706
2200
  if (options.stopUiBridge) {
1707
2201
  await uiCliBridge.stop().catch(() => null);
1708
2202
  }
@@ -1775,12 +2269,13 @@ function getQueue(groupKey) {
1775
2269
  function generateRunId() {
1776
2270
  return `run_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`;
1777
2271
  }
1778
- function createLineEmitter(runId, type) {
2272
+ function createLineEmitter(runId, type, onLine) {
1779
2273
  let pending = "";
1780
2274
  const emit = (line) => {
1781
2275
  const normalized = String(line || "").replace(/\r$/, "");
1782
2276
  if (!normalized) return;
1783
2277
  sendEvent({ type, runId, line: normalized, ts: now() });
2278
+ if (typeof onLine === "function") onLine(normalized);
1784
2279
  };
1785
2280
  return {
1786
2281
  push(chunk) {
@@ -1804,19 +2299,44 @@ function resolveNodeBin2() {
1804
2299
  const explicit = String(process.env.WEBAUTO_NODE_BIN || "").trim();
1805
2300
  if (explicit) return explicit;
1806
2301
  const npmNode = String(process.env.npm_node_execpath || "").trim();
1807
- if (npmNode) return npmNode;
2302
+ if (npmNode) {
2303
+ const base = path7.basename(npmNode).toLowerCase();
2304
+ const isNode = base === "node" || base === "node.exe";
2305
+ if (isNode) return npmNode;
2306
+ }
1808
2307
  return process.platform === "win32" ? "node.exe" : "node";
1809
2308
  }
1810
2309
  function resolveCwd(input) {
1811
2310
  const raw = String(input || "").trim();
1812
2311
  if (!raw) return REPO_ROOT2;
1813
- return path6.isAbsolute(raw) ? raw : path6.resolve(REPO_ROOT2, raw);
2312
+ return path7.isAbsolute(raw) ? raw : path7.resolve(REPO_ROOT2, raw);
2313
+ }
2314
+ function isPidAlive(pid) {
2315
+ const target = Number(pid || 0);
2316
+ if (!Number.isFinite(target) || target <= 0) return false;
2317
+ if (process.platform === "win32") {
2318
+ const ret = spawnSync2("tasklist", ["/FI", `PID eq ${target}`], {
2319
+ windowsHide: true,
2320
+ encoding: "utf8",
2321
+ stdio: ["ignore", "pipe", "ignore"]
2322
+ });
2323
+ const out = String(ret.stdout || "");
2324
+ if (!out) return false;
2325
+ const lines = out.split(/\r?\n/g).map((line) => line.trim()).filter(Boolean);
2326
+ return lines.some((line) => line.includes(` ${target}`));
2327
+ }
2328
+ try {
2329
+ process.kill(target, 0);
2330
+ return true;
2331
+ } catch {
2332
+ return false;
2333
+ }
1814
2334
  }
1815
2335
  var cachedStateMod = null;
1816
2336
  async function getStateModule() {
1817
2337
  if (cachedStateMod) return cachedStateMod;
1818
2338
  try {
1819
- const p = path6.join(REPO_ROOT2, "dist", "modules", "state", "src", "xiaohongshu-collect-state.js");
2339
+ const p = path7.join(REPO_ROOT2, "dist", "modules", "state", "src", "xiaohongshu-collect-state.js");
1820
2340
  cachedStateMod = await import(pathToFileURL3(p).href);
1821
2341
  return cachedStateMod;
1822
2342
  } catch {
@@ -1830,6 +2350,12 @@ async function spawnCommand(spec) {
1830
2350
  const q = getQueue(groupKey);
1831
2351
  const cwd = resolveCwd(spec.cwd);
1832
2352
  const args = Array.isArray(spec.args) ? spec.args : [];
2353
+ appendRunLog(runId, `[queued] title=${String(spec.title || "").trim() || "-"} cwd=${cwd}`);
2354
+ setRunLifecycle(runId, {
2355
+ state: "queued",
2356
+ title: String(spec.title || ""),
2357
+ queuedAt: now()
2358
+ });
1833
2359
  const isXhsRunCommand = args.some((item) => /xhs-(orchestrate|unified)\.mjs$/i.test(String(item || "").replace(/\\/g, "/")));
1834
2360
  const extractProfilesFromArgs = (argv) => {
1835
2361
  const out = [];
@@ -1862,49 +2388,133 @@ async function spawnCommand(spec) {
1862
2388
  let finished = false;
1863
2389
  let exitCode = null;
1864
2390
  let exitSignal = null;
2391
+ let orphanCheckTimer = null;
1865
2392
  const finalize = (code, signal) => {
1866
2393
  if (finished) return;
1867
2394
  finished = true;
2395
+ if (orphanCheckTimer) {
2396
+ clearInterval(orphanCheckTimer);
2397
+ orphanCheckTimer = null;
2398
+ }
2399
+ setRunLifecycle(runId, {
2400
+ state: "exited",
2401
+ exitedAt: now(),
2402
+ exitCode: code,
2403
+ signal
2404
+ });
1868
2405
  sendEvent({ type: "exit", runId, exitCode: code, signal, ts: now() });
2406
+ appendRunLog(runId, `[exit] code=${code ?? "null"} signal=${signal ?? "null"}`);
1869
2407
  runs.delete(runId);
1870
2408
  resolve();
1871
2409
  };
1872
- const child = spawn2(resolveNodeBin2(), args, {
1873
- cwd,
1874
- env: {
1875
- ...process.env,
1876
- WEBAUTO_DAEMON: "1",
1877
- WEBAUTO_UI_HEARTBEAT: "1",
1878
- ...spec.env || {}
1879
- },
1880
- stdio: ["ignore", "pipe", "pipe"],
1881
- windowsHide: true
1882
- });
2410
+ let child;
2411
+ try {
2412
+ const nodeBin = resolveNodeBin2();
2413
+ appendRunLog(runId, `[cmd] ${nodeBin}`);
2414
+ child = spawn2(nodeBin, args, {
2415
+ cwd,
2416
+ env: {
2417
+ ...process.env,
2418
+ ...spec.env || {}
2419
+ },
2420
+ stdio: ["ignore", "pipe", "pipe"],
2421
+ windowsHide: true
2422
+ });
2423
+ } catch (err) {
2424
+ const message = err?.message || String(err);
2425
+ setRunLifecycle(runId, {
2426
+ state: "exited",
2427
+ exitedAt: now(),
2428
+ exitCode: null,
2429
+ signal: "spawn_exception",
2430
+ lastError: message
2431
+ });
2432
+ sendEvent({ type: "stderr", runId, line: `[spawn-throw] ${message}`, ts: now() });
2433
+ appendRunLog(runId, `[spawn-throw] ${message}`);
2434
+ finalize(null, "spawn_exception");
2435
+ return;
2436
+ }
2437
+ const stdoutLines = createLineEmitter(runId, "stdout", (line) => appendRunLog(runId, `[stdout] ${line}`));
2438
+ const stderrLines = createLineEmitter(runId, "stderr", (line) => appendRunLog(runId, `[stderr] ${line}`));
2439
+ try {
2440
+ child.stdout?.on("data", (chunk) => {
2441
+ stdoutLines.push(chunk);
2442
+ });
2443
+ child.stderr?.on("data", (chunk) => {
2444
+ const text = chunk.toString("utf8");
2445
+ const lines = text.split(/\r?\n/g).map((line) => line.trim()).filter(Boolean);
2446
+ if (lines.length > 0) {
2447
+ setRunLifecycle(runId, { lastError: lines[lines.length - 1] });
2448
+ }
2449
+ stderrLines.push(chunk);
2450
+ });
2451
+ child.on("error", (err) => {
2452
+ const message = err?.message || String(err);
2453
+ setRunLifecycle(runId, { lastError: message });
2454
+ sendEvent({ type: "stderr", runId, line: `[spawn-error] ${message}`, ts: now() });
2455
+ appendRunLog(runId, `[spawn-error] ${message}`);
2456
+ finalize(null, "error");
2457
+ });
2458
+ child.on("exit", (code, signal) => {
2459
+ exitCode = code;
2460
+ exitSignal = signal;
2461
+ const timer = setTimeout(() => {
2462
+ if (finished) return;
2463
+ untrackRunPid(child);
2464
+ stdoutLines.flush();
2465
+ stderrLines.flush();
2466
+ finalize(exitCode ?? null, exitSignal ?? null);
2467
+ }, 200);
2468
+ if (timer && typeof timer.unref === "function") {
2469
+ timer.unref();
2470
+ }
2471
+ });
2472
+ child.on("close", (code, signal) => {
2473
+ untrackRunPid(child);
2474
+ stdoutLines.flush();
2475
+ stderrLines.flush();
2476
+ finalize(exitCode ?? code ?? null, exitSignal ?? signal ?? null);
2477
+ });
2478
+ } catch (err) {
2479
+ const message = err?.message || String(err);
2480
+ setRunLifecycle(runId, { lastError: message });
2481
+ sendEvent({ type: "stderr", runId, line: `[spawn-setup-error] ${message}`, ts: now() });
2482
+ appendRunLog(runId, `[spawn-setup-error] ${message}`);
2483
+ finalize(null, "setup_error");
2484
+ return;
2485
+ }
1883
2486
  trackRunPid(child);
2487
+ if (child.pid) {
2488
+ trackBrowserProcess(child.pid);
2489
+ }
1884
2490
  runs.set(runId, { child, title: spec.title, startedAt: now(), profiles: requestedProfiles });
1885
- sendEvent({ type: "started", runId, title: spec.title, pid: child.pid ?? -1, ts: now() });
1886
- const stdoutLines = createLineEmitter(runId, "stdout");
1887
- const stderrLines = createLineEmitter(runId, "stderr");
1888
- child.stdout?.on("data", (chunk) => {
1889
- stdoutLines.push(chunk);
1890
- });
1891
- child.stderr?.on("data", (chunk) => {
1892
- stderrLines.push(chunk);
1893
- });
1894
- child.on("error", (err) => {
1895
- sendEvent({ type: "stderr", runId, line: `[spawn-error] ${err?.message || String(err)}`, ts: now() });
1896
- finalize(null, "error");
1897
- });
1898
- child.on("exit", (code, signal) => {
1899
- exitCode = code;
1900
- exitSignal = signal;
1901
- });
1902
- child.on("close", (code, signal) => {
1903
- untrackRunPid(child);
1904
- stdoutLines.flush();
1905
- stderrLines.flush();
1906
- finalize(exitCode ?? code ?? null, exitSignal ?? signal ?? null);
2491
+ setRunLifecycle(runId, {
2492
+ state: "running",
2493
+ startedAt: now(),
2494
+ pid: child.pid || -1,
2495
+ title: String(spec.title || "")
1907
2496
  });
2497
+ sendEvent({ type: "started", runId, title: spec.title, pid: child.pid ?? -1, ts: now() });
2498
+ appendRunLog(runId, `[started] pid=${child.pid ?? -1} title=${String(spec.title || "").trim() || "-"}`);
2499
+ if (args.length > 0) {
2500
+ appendRunLog(runId, `[args] ${args.join(" ")}`);
2501
+ }
2502
+ const pid = Number(child.pid || 0);
2503
+ if (pid > 0) {
2504
+ orphanCheckTimer = setInterval(() => {
2505
+ if (finished) return;
2506
+ if (!isPidAlive(pid)) {
2507
+ untrackRunPid(child);
2508
+ stdoutLines.flush();
2509
+ stderrLines.flush();
2510
+ appendRunLog(runId, "[watchdog] child pid disappeared before close/exit event");
2511
+ finalize(exitCode, exitSignal || "pid_gone");
2512
+ }
2513
+ }, 1e3);
2514
+ if (orphanCheckTimer && typeof orphanCheckTimer.unref === "function") {
2515
+ orphanCheckTimer.unref();
2516
+ }
2517
+ }
1908
2518
  })
1909
2519
  );
1910
2520
  return { runId };
@@ -1944,9 +2554,106 @@ async function runJson(spec) {
1944
2554
  return { ok: true, code, stdout: out, stderr: err };
1945
2555
  }
1946
2556
  }
2557
+ function sleep2(ms) {
2558
+ return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
2559
+ }
2560
+ function toEpochMs(value) {
2561
+ const raw = String(value ?? "").trim();
2562
+ if (!raw) return 0;
2563
+ const asNum = Number(raw);
2564
+ if (Number.isFinite(asNum) && asNum > 0) return asNum;
2565
+ const asDate = Date.parse(raw);
2566
+ return Number.isFinite(asDate) ? asDate : 0;
2567
+ }
2568
+ function resolveUnifiedApiBaseUrl() {
2569
+ return String(
2570
+ process.env.WEBAUTO_UNIFIED_API || process.env.WEBAUTO_UNIFIED_URL || "http://127.0.0.1:7701"
2571
+ ).trim().replace(/\/+$/, "");
2572
+ }
2573
+ async function listUnifiedTasks() {
2574
+ const baseUrl = resolveUnifiedApiBaseUrl();
2575
+ const res = await fetch(`${baseUrl}/api/v1/tasks`, { signal: AbortSignal.timeout(3e3) });
2576
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
2577
+ const payload = await res.json().catch(() => ({}));
2578
+ return Array.isArray(payload?.data) ? payload.data : [];
2579
+ }
2580
+ function pickUnifiedRunId(tasks, options) {
2581
+ const profileId = String(options.profileId || "").trim();
2582
+ const keyword = String(options.keyword || "").trim();
2583
+ const uiTriggerId = String(options.uiTriggerId || "").trim();
2584
+ const minTs = Number(options.minTs || 0);
2585
+ const baselineRunIds = options.baselineRunIds instanceof Set ? options.baselineRunIds : /* @__PURE__ */ new Set();
2586
+ const rows = tasks.filter((task) => {
2587
+ const runId = String(task?.runId || task?.id || "").trim();
2588
+ if (!runId) return false;
2589
+ if (baselineRunIds.has(runId)) return false;
2590
+ const phase = String(task?.phase || task?.lastPhase || "").trim().toLowerCase();
2591
+ if (phase !== "unified") return false;
2592
+ const taskProfile = String(task?.profileId || "").trim();
2593
+ const taskKeyword = String(task?.keyword || "").trim();
2594
+ const taskTrigger = String(
2595
+ task?.uiTriggerId || task?.triggerId || task?.meta?.uiTriggerId || task?.context?.uiTriggerId || ""
2596
+ ).trim();
2597
+ if (uiTriggerId && taskTrigger !== uiTriggerId) return false;
2598
+ if (profileId && taskProfile && taskProfile !== profileId) return false;
2599
+ if (keyword && taskKeyword && taskKeyword !== keyword) return false;
2600
+ const createdTs = toEpochMs(task?.createdAt);
2601
+ const startedTs = toEpochMs(task?.startedAt);
2602
+ const ts = createdTs || startedTs;
2603
+ return ts >= minTs;
2604
+ }).sort((a, b) => {
2605
+ const ta = toEpochMs(a?.createdAt) || toEpochMs(a?.startedAt);
2606
+ const tb = toEpochMs(b?.createdAt) || toEpochMs(b?.startedAt);
2607
+ return tb - ta;
2608
+ });
2609
+ const picked = rows[0] || null;
2610
+ return String(picked?.runId || picked?.id || "").trim();
2611
+ }
2612
+ async function waitForUnifiedRunRegistration(input) {
2613
+ const desktopRunId = String(input.desktopRunId || "").trim();
2614
+ const profileId = String(input.profileId || "").trim();
2615
+ const keyword = String(input.keyword || "").trim();
2616
+ const uiTriggerId = String(input.uiTriggerId || "").trim();
2617
+ const timeoutMs = Math.max(2e3, Number(input.timeoutMs || 2e4) || 2e4);
2618
+ const startedAt = Date.now();
2619
+ const minTs = startedAt - 3e4;
2620
+ const baselineRunIds = input.baselineRunIds instanceof Set ? input.baselineRunIds : /* @__PURE__ */ new Set();
2621
+ let lastFetchError = "";
2622
+ while (Date.now() - startedAt < timeoutMs) {
2623
+ try {
2624
+ const tasks = await listUnifiedTasks();
2625
+ const unifiedRunId = pickUnifiedRunId(tasks, {
2626
+ profileId,
2627
+ keyword,
2628
+ uiTriggerId,
2629
+ minTs,
2630
+ baselineRunIds
2631
+ });
2632
+ if (unifiedRunId) return { ok: true, runId: unifiedRunId };
2633
+ lastFetchError = "";
2634
+ } catch (err) {
2635
+ lastFetchError = err?.message || String(err);
2636
+ }
2637
+ const lifecycle2 = getRunLifecycle(desktopRunId);
2638
+ if (lifecycle2?.state === "exited") {
2639
+ const detail = lifecycle2.lastError || `exit=${lifecycle2.exitCode ?? "null"}`;
2640
+ return { ok: false, error: `\u4EFB\u52A1\u8FDB\u7A0B\u63D0\u524D\u9000\u51FA\uFF0C\u672A\u6CE8\u518C unified runId (${detail})` };
2641
+ }
2642
+ await sleep2(500);
2643
+ }
2644
+ const lifecycle = getRunLifecycle(desktopRunId);
2645
+ if (lifecycle?.state === "queued") {
2646
+ return { ok: false, error: "\u4EFB\u52A1\u4ECD\u5728\u6392\u961F\uFF0C\u672A\u8FDB\u5165\u8FD0\u884C\u6001\uFF08\u8BF7\u68C0\u67E5\u662F\u5426\u6709\u540C\u7C7B\u4EFB\u52A1\u5360\u7528\uFF09" };
2647
+ }
2648
+ if (lifecycle?.state === "running") {
2649
+ return { ok: false, error: "\u4EFB\u52A1\u8FDB\u7A0B\u5DF2\u542F\u52A8\uFF0C\u4F46\u5728\u8D85\u65F6\u5185\u672A\u6CE8\u518C unified runId" };
2650
+ }
2651
+ const suffix = lastFetchError ? `: ${lastFetchError}` : "";
2652
+ return { ok: false, error: `\u672A\u68C0\u6D4B\u5230 unified runId${suffix}` };
2653
+ }
1947
2654
  async function scanResults(input) {
1948
2655
  const downloadRoot = String(input.downloadRoot || resolveDefaultDownloadRoot());
1949
- const root = path6.join(downloadRoot, "xiaohongshu");
2656
+ const root = path7.join(downloadRoot, "xiaohongshu");
1950
2657
  const result = { ok: true, root, entries: [] };
1951
2658
  try {
1952
2659
  const stateMod = await getStateModule();
@@ -1954,12 +2661,12 @@ async function scanResults(input) {
1954
2661
  for (const envEnt of envDirs) {
1955
2662
  if (!envEnt.isDirectory()) continue;
1956
2663
  const env = envEnt.name;
1957
- const envPath = path6.join(root, env);
2664
+ const envPath = path7.join(root, env);
1958
2665
  const keywordDirs = await fs4.readdir(envPath, { withFileTypes: true });
1959
2666
  for (const kwEnt of keywordDirs) {
1960
2667
  if (!kwEnt.isDirectory()) continue;
1961
2668
  const keyword = kwEnt.name;
1962
- const kwPath = path6.join(envPath, keyword);
2669
+ const kwPath = path7.join(envPath, keyword);
1963
2670
  const stat = await fs4.stat(kwPath).catch(() => null);
1964
2671
  let stateSummary = null;
1965
2672
  if (stateMod?.loadXhsCollectState) {
@@ -1994,7 +2701,7 @@ async function listXhsFullCollectScripts() {
1994
2701
  return {
1995
2702
  id: `xhs:${name}`,
1996
2703
  label: `Full Collect (${name})`,
1997
- path: path6.join(XHS_SCRIPTS_ROOT, name)
2704
+ path: path7.join(XHS_SCRIPTS_ROOT, name)
1998
2705
  };
1999
2706
  });
2000
2707
  return { ok: true, scripts };
@@ -2064,11 +2771,11 @@ async function listDir(input) {
2064
2771
  const items = await fs4.readdir(dir, { withFileTypes: true }).catch(() => []);
2065
2772
  for (const ent of items) {
2066
2773
  if (entries.length >= maxEntries) break;
2067
- const full = path6.join(dir, ent.name);
2774
+ const full = path7.join(dir, ent.name);
2068
2775
  const st = await fs4.stat(full).catch(() => null);
2069
2776
  entries.push({
2070
2777
  path: full,
2071
- rel: path6.relative(root, full),
2778
+ rel: path7.relative(root, full),
2072
2779
  name: ent.name,
2073
2780
  isDir: ent.isDirectory(),
2074
2781
  size: st?.size || 0,
@@ -2085,13 +2792,13 @@ async function listDir(input) {
2085
2792
  }
2086
2793
  function createWindow() {
2087
2794
  win = new BrowserWindow2({
2088
- title: "WebAuto Desktop v0.1.1",
2795
+ title: VERSION_INFO.windowTitle,
2089
2796
  width: 1280,
2090
2797
  height: 900,
2091
2798
  minWidth: 920,
2092
2799
  minHeight: 800,
2093
2800
  webPreferences: {
2094
- preload: path6.join(APP_ROOT, "dist", "main", "preload.mjs"),
2801
+ preload: path7.join(APP_ROOT, "dist", "main", "preload.mjs"),
2095
2802
  contextIsolation: true,
2096
2803
  nodeIntegration: false,
2097
2804
  sandbox: false,
@@ -2099,7 +2806,7 @@ function createWindow() {
2099
2806
  backgroundThrottling: false
2100
2807
  }
2101
2808
  });
2102
- const htmlPath = path6.join(APP_ROOT, "dist", "renderer", "index.html");
2809
+ const htmlPath = path7.join(APP_ROOT, "dist", "renderer", "index.html");
2103
2810
  void win.loadFile(htmlPath);
2104
2811
  ensureStateBridge();
2105
2812
  }
@@ -2131,6 +2838,7 @@ ipcMain2.on("preload:test", () => {
2131
2838
  setTimeout(() => app.quit(), 200);
2132
2839
  });
2133
2840
  ipcMain2.handle("settings:get", async () => readDesktopConsoleSettings({ appRoot: APP_ROOT, repoRoot: REPO_ROOT2 }));
2841
+ ipcMain2.handle("app:getVersion", async () => VERSION_INFO);
2134
2842
  ipcMain2.handle("settings:set", async (_evt, next) => {
2135
2843
  const updated = await writeDesktopConsoleSettings({ appRoot: APP_ROOT, repoRoot: REPO_ROOT2 }, next || {});
2136
2844
  const w = getWin();
@@ -2238,6 +2946,73 @@ ipcMain2.handle("cmd:runJson", async (_evt, spec) => {
2238
2946
  const args = Array.isArray(spec?.args) ? spec.args : [];
2239
2947
  return runJson({ ...spec, cwd, args });
2240
2948
  });
2949
+ ipcMain2.handle("schedule:invoke", async (_evt, input) => scheduleInvoke(
2950
+ {
2951
+ repoRoot: REPO_ROOT2,
2952
+ runJson: (spec) => runJson(spec),
2953
+ spawnCommand: (spec) => spawnCommand(spec)
2954
+ },
2955
+ input || { action: "list" }
2956
+ ));
2957
+ ipcMain2.handle("task:runEphemeral", async (_evt, input) => {
2958
+ const payload = input || {};
2959
+ let baselineRunIds = /* @__PURE__ */ new Set();
2960
+ try {
2961
+ const baselineTasks = await listUnifiedTasks();
2962
+ baselineRunIds = new Set(
2963
+ baselineTasks.map((task) => String(task?.runId || task?.id || "").trim()).filter(Boolean)
2964
+ );
2965
+ } catch {
2966
+ baselineRunIds = /* @__PURE__ */ new Set();
2967
+ }
2968
+ const result = await runEphemeralTask(
2969
+ {
2970
+ repoRoot: REPO_ROOT2,
2971
+ runJson: (spec) => runJson(spec),
2972
+ spawnCommand: (spec) => spawnCommand(spec)
2973
+ },
2974
+ payload
2975
+ );
2976
+ if (!result?.ok) return result;
2977
+ const commandType = String(result?.commandType || payload?.commandType || "").trim().toLowerCase();
2978
+ const desktopRunId = String(result?.runId || "").trim();
2979
+ if (commandType !== "xhs-unified" || !desktopRunId) {
2980
+ return result;
2981
+ }
2982
+ const profileId = String(result?.profile || payload?.argv?.profile || "").trim();
2983
+ const keyword = String(payload?.argv?.keyword || payload?.argv?.k || "").trim();
2984
+ const uiTriggerId = String(result?.uiTriggerId || payload?.argv?.["ui-trigger-id"] || "").trim();
2985
+ appendRunLog(
2986
+ desktopRunId,
2987
+ `[wait-register] profile=${profileId || "-"} keyword=${keyword || "-"} uiTriggerId=${uiTriggerId || "-"}`
2988
+ );
2989
+ const waited = await waitForUnifiedRunRegistration({
2990
+ desktopRunId,
2991
+ profileId,
2992
+ keyword,
2993
+ uiTriggerId,
2994
+ baselineRunIds,
2995
+ timeoutMs: 2e4
2996
+ });
2997
+ if (!waited.ok) {
2998
+ appendRunLog(desktopRunId, `[wait-register-failed] ${String(waited.error || "unknown_error")}`);
2999
+ return {
3000
+ ok: false,
3001
+ error: waited.error,
3002
+ runId: desktopRunId,
3003
+ commandType: result?.commandType || "xhs-unified",
3004
+ profile: profileId,
3005
+ uiTriggerId
3006
+ };
3007
+ }
3008
+ appendRunLog(desktopRunId, `[wait-register-ok] unifiedRunId=${waited.runId || "-"} uiTriggerId=${uiTriggerId || "-"}`);
3009
+ return {
3010
+ ...result,
3011
+ runId: desktopRunId,
3012
+ unifiedRunId: waited.runId,
3013
+ uiTriggerId
3014
+ };
3015
+ });
2241
3016
  ipcMain2.handle("results:scan", async (_evt, spec) => scanResults(spec || {}));
2242
3017
  ipcMain2.handle("fs:listDir", async (_evt, spec) => listDir(spec));
2243
3018
  ipcMain2.handle(
@@ -2277,6 +3052,49 @@ ipcMain2.handle("env:repairCore", async () => {
2277
3052
  const services = await checkServices().catch(() => ({ unifiedApi: false, camoRuntime: false }));
2278
3053
  return { ok, services };
2279
3054
  });
3055
+ ipcMain2.handle("env:cleanup", async () => {
3056
+ markUiHeartbeat("env_cleanup");
3057
+ console.log("[env:cleanup] Starting environment cleanup...");
3058
+ await cleanupRuntimeEnvironment("env_cleanup_requested", {
3059
+ stopUiBridge: false,
3060
+ stopHeartbeat: false,
3061
+ stopCoreServices: false,
3062
+ stopStateBridge: false,
3063
+ includeLockCleanup: true
3064
+ });
3065
+ let locksCleared = 0;
3066
+ try {
3067
+ const profiles = await profileStore.listProfiles();
3068
+ for (const p of profiles) {
3069
+ const lockFile = path7.join(p.path, ".lock");
3070
+ try {
3071
+ await fs4.unlink(lockFile);
3072
+ locksCleared++;
3073
+ console.log(`[env:cleanup] Cleared lock for profile ${p.profileId}`);
3074
+ } catch (err) {
3075
+ if (err?.code !== "ENOENT") {
3076
+ console.warn(`[env:cleanup] Failed to clear lock for ${p.profileId}:`, err.message);
3077
+ }
3078
+ }
3079
+ }
3080
+ } catch (err) {
3081
+ console.warn("[env:cleanup] Failed to list profiles:", err);
3082
+ }
3083
+ await stopCoreDaemon().catch(() => null);
3084
+ const restarted = await startCoreDaemon().catch(() => false);
3085
+ const services = await checkServices().catch(() => ({ unifiedApi: false, camoRuntime: false }));
3086
+ const firefox = await checkFirefox().catch(() => ({ installed: false }));
3087
+ const camo = await checkCamoCli().catch(() => ({ installed: false }));
3088
+ console.log("[env:cleanup] Cleanup complete:", { locksCleared, restarted, services, firefox, camo });
3089
+ return {
3090
+ ok: true,
3091
+ locksCleared,
3092
+ coreRestarted: restarted,
3093
+ services,
3094
+ firefox,
3095
+ camo
3096
+ };
3097
+ });
2280
3098
  ipcMain2.handle("env:repairDeps", async (_evt, input) => {
2281
3099
  const wantCore = Boolean(input?.core);
2282
3100
  const wantBrowser = Boolean(input?.browser);
@@ -2294,7 +3112,7 @@ ipcMain2.handle("env:repairDeps", async (_evt, input) => {
2294
3112
  if (!coreOk) result.ok = false;
2295
3113
  }
2296
3114
  if (wantBrowser || wantGeoip) {
2297
- const args = [path6.join("apps", "webauto", "entry", "xhs-install.mjs")];
3115
+ const args = [path7.join("apps", "webauto", "entry", "xhs-install.mjs")];
2298
3116
  if (wantReinstall) args.push("--reinstall");
2299
3117
  else if (wantUninstall) args.push("--uninstall");
2300
3118
  else args.push("--install");
@@ -2313,20 +3131,6 @@ ipcMain2.handle("env:repairDeps", async (_evt, input) => {
2313
3131
  result.env = await checkEnvironment().catch(() => null);
2314
3132
  return result;
2315
3133
  });
2316
- ipcMain2.handle("env:cleanup", async () => {
2317
- markUiHeartbeat("env_cleanup");
2318
- await cleanupRuntimeEnvironment("env_cleanup", {
2319
- stopUiBridge: false,
2320
- stopHeartbeat: false,
2321
- stopCoreServices: false,
2322
- stopStateBridge: false,
2323
- includeLockCleanup: true
2324
- });
2325
- return {
2326
- ok: true,
2327
- services: await checkServices().catch(() => ({ unifiedApi: false, camoRuntime: false }))
2328
- };
2329
- });
2330
3134
  ipcMain2.handle("config:saveLast", async (_evt, config) => {
2331
3135
  await saveCrawlConfig({ appRoot: APP_ROOT, repoRoot: REPO_ROOT2 }, config);
2332
3136
  return { ok: true };
@@ -2417,7 +3221,7 @@ ipcMain2.handle("runtime:kill", async (_evt, input) => {
2417
3221
  ipcMain2.handle("runtime:restartPhase1", async (_evt, input) => {
2418
3222
  const profileId = String(input?.profileId || "").trim();
2419
3223
  if (!profileId) return { ok: false, error: "missing profileId" };
2420
- const args = [path6.join(REPO_ROOT2, "scripts", "xiaohongshu", "phase1-boot.mjs"), "--profile", profileId, "--headless", "false"];
3224
+ const args = [path7.join(REPO_ROOT2, "scripts", "xiaohongshu", "phase1-boot.mjs"), "--profile", profileId, "--headless", "false"];
2421
3225
  return spawnCommand({ title: `Phase1 restart ${profileId}`, cwd: REPO_ROOT2, args, groupKey: "phase1" });
2422
3226
  });
2423
3227
  ipcMain2.handle("runtime:setBrowserTitle", async (_evt, input) => {