@web-auto/webauto 0.1.8 → 0.1.9

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 (32) hide show
  1. package/apps/desktop-console/dist/main/index.mjs +800 -89
  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 +784 -331
  5. package/apps/desktop-console/entry/ui-cli.mjs +23 -8
  6. package/apps/desktop-console/entry/ui-console.mjs +8 -3
  7. package/apps/webauto/entry/account.mjs +69 -8
  8. package/apps/webauto/entry/lib/account-detect.mjs +106 -25
  9. package/apps/webauto/entry/lib/account-store.mjs +121 -22
  10. package/apps/webauto/entry/lib/schedule-store.mjs +0 -12
  11. package/apps/webauto/entry/profilepool.mjs +45 -3
  12. package/apps/webauto/entry/schedule.mjs +44 -2
  13. package/apps/webauto/entry/weibo-unified.mjs +2 -2
  14. package/apps/webauto/entry/xhs-install.mjs +220 -51
  15. package/apps/webauto/entry/xhs-unified.mjs +33 -6
  16. package/bin/webauto.mjs +80 -4
  17. package/dist/modules/camo-runtime/src/utils/browser-service.mjs +4 -0
  18. package/dist/services/unified-api/server.js +5 -0
  19. package/dist/services/unified-api/task-state.js +2 -0
  20. package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +142 -14
  21. package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +16 -1
  22. package/modules/camo-runtime/src/autoscript/action-providers/xhs.mjs +104 -0
  23. package/modules/camo-runtime/src/autoscript/runtime.mjs +14 -4
  24. package/modules/camo-runtime/src/autoscript/schema.mjs +9 -0
  25. package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +9 -2
  26. package/modules/camo-runtime/src/container/runtime-core/checkpoint.mjs +107 -1
  27. package/modules/camo-runtime/src/container/runtime-core/subscription.mjs +24 -2
  28. package/modules/camo-runtime/src/utils/browser-service.mjs +4 -0
  29. package/package.json +6 -3
  30. package/scripts/bump-version.mjs +120 -0
  31. package/services/unified-api/server.ts +4 -0
  32. package/services/unified-api/task-state.ts +5 -0
@@ -1,10 +1,10 @@
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
10
  import { existsSync, promises as fs } from "node:fs";
@@ -273,7 +273,9 @@ import path2 from "path";
273
273
  import { existsSync as existsSync2 } 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");
279
281
  function sleep(ms) {
@@ -323,6 +325,9 @@ async function areCoreServicesHealthy() {
323
325
  const health = await Promise.all(CORE_HEALTH_URLS.map((url) => checkHttpHealth(url)));
324
326
  return health.every(Boolean);
325
327
  }
328
+ async function isUnifiedApiHealthy() {
329
+ return checkHttpHealth(UNIFIED_API_HEALTH_URL);
330
+ }
326
331
  async function runNodeScript(scriptPath, timeoutMs) {
327
332
  return new Promise((resolve) => {
328
333
  const nodeBin = resolveNodeBin();
@@ -405,14 +410,18 @@ async function startCoreDaemon() {
405
410
  4e4
406
411
  );
407
412
  if (!startedBrowser) {
408
- console.error("[CoreDaemonManager] Failed to start camo browser backend");
409
- return false;
413
+ console.warn("[CoreDaemonManager] Failed to start camo browser backend, continue in degraded mode");
410
414
  }
411
415
  for (let i = 0; i < 20; i += 1) {
412
- if (await areCoreServicesHealthy()) return true;
416
+ const [allHealthy, unifiedHealthy] = await Promise.all([
417
+ areCoreServicesHealthy(),
418
+ isUnifiedApiHealthy()
419
+ ]);
420
+ if (allHealthy) return true;
421
+ if (unifiedHealthy) return true;
413
422
  await sleep(300);
414
423
  }
415
- console.error("[CoreDaemonManager] Services still unhealthy after start");
424
+ console.error("[CoreDaemonManager] Unified API still unhealthy after start");
416
425
  return false;
417
426
  }
418
427
  async function stopCoreDaemon() {
@@ -817,6 +826,13 @@ function resolvePathFromOutput(stdout) {
817
826
  }
818
827
  return "";
819
828
  }
829
+ function resolveCamoufoxExecutable(installRoot) {
830
+ return process.platform === "win32" ? path4.join(installRoot, "camoufox.exe") : path4.join(installRoot, "camoufox");
831
+ }
832
+ function isValidCamoufoxInstallRoot(installRoot) {
833
+ if (!installRoot || !existsSync3(installRoot)) return false;
834
+ return existsSync3(resolveCamoufoxExecutable(installRoot));
835
+ }
820
836
  async function checkCamoCli() {
821
837
  const camoCandidates = process.platform === "win32" ? ["camo.cmd", "camo.exe", "camo.bat", "camo.ps1"] : ["camo"];
822
838
  for (const candidate of camoCandidates) {
@@ -858,10 +874,12 @@ async function checkServices() {
858
874
  }
859
875
  async function checkFirefox() {
860
876
  const candidates = process.platform === "win32" ? [
877
+ { command: "camoufox", args: ["path"] },
861
878
  { command: "python", args: ["-m", "camoufox", "path"] },
862
879
  { command: "py", args: ["-3", "-m", "camoufox", "path"] },
863
880
  { command: resolveNpxBin2(), args: ["--yes", "--package=camoufox", "camoufox", "path"] }
864
881
  ] : [
882
+ { command: "camoufox", args: ["path"] },
865
883
  { command: "python3", args: ["-m", "camoufox", "path"] },
866
884
  { command: resolveNpxBin2(), args: ["--yes", "--package=camoufox", "camoufox", "path"] }
867
885
  ];
@@ -874,7 +892,10 @@ async function checkFirefox() {
874
892
  });
875
893
  if (ret.status !== 0) continue;
876
894
  const resolvedPath = resolvePathFromOutput(String(ret.stdout || ""));
877
- return resolvedPath ? { installed: true, path: resolvedPath } : { installed: true };
895
+ if (resolvedPath && isValidCamoufoxInstallRoot(resolvedPath)) {
896
+ return { installed: true, path: resolvedPath };
897
+ }
898
+ if (!resolvedPath) return { installed: true };
878
899
  } catch {
879
900
  }
880
901
  }
@@ -894,8 +915,16 @@ async function checkEnvironment() {
894
915
  checkFirefox(),
895
916
  checkGeoIP()
896
917
  ]);
897
- const allReady = camo.installed && services.unifiedApi && firefox.installed;
898
- return { camo, services, firefox, geoip, allReady };
918
+ const browserReady = Boolean(firefox.installed || services.camoRuntime);
919
+ const missing = {
920
+ core: !services.unifiedApi,
921
+ runtimeService: !services.camoRuntime,
922
+ camo: !camo.installed,
923
+ runtime: !browserReady,
924
+ geoip: !geoip.installed
925
+ };
926
+ const allReady = camo.installed && services.unifiedApi && browserReady;
927
+ return { camo, services, firefox, geoip, browserReady, missing, allReady };
899
928
  }
900
929
 
901
930
  // src/main/ui-cli-bridge.mts
@@ -985,6 +1014,20 @@ function buildSnapshotScript() {
985
1014
  if ('value' in el) return String(el.value ?? '');
986
1015
  return String(el.textContent || '').trim();
987
1016
  };
1017
+ const firstText = (selectors) => {
1018
+ for (const sel of selectors) {
1019
+ const v = text(sel);
1020
+ if (v) return v;
1021
+ }
1022
+ return '';
1023
+ };
1024
+ const firstValue = (selectors) => {
1025
+ for (const sel of selectors) {
1026
+ const v = value(sel);
1027
+ if (v) return v;
1028
+ }
1029
+ return '';
1030
+ };
988
1031
  const activeTab = document.querySelector('.tab.active');
989
1032
  const errors = Array.from(document.querySelectorAll('#recent-errors-list li'))
990
1033
  .map((el) => String(el.textContent || '').trim())
@@ -1000,10 +1043,10 @@ function buildSnapshotScript() {
1000
1043
  currentPhase: text('#current-phase'),
1001
1044
  currentAction: text('#current-action'),
1002
1045
  progressPercent: text('#progress-percent'),
1003
- keyword: value('#keyword-input'),
1004
- target: value('#target-input'),
1005
- account: value('#account-select'),
1006
- env: value('#env-select'),
1046
+ keyword: firstValue(['#task-keyword', '#keyword-input', '#task-keyword-input']),
1047
+ target: firstValue(['#task-target', '#target-input', '#task-target-input']),
1048
+ account: firstValue(['#task-account', '#task-profile', '#account-select']),
1049
+ env: firstValue(['#task-env', '#env-select']),
1007
1050
  recentErrors: errors,
1008
1051
  ts: new Date().toISOString(),
1009
1052
  };
@@ -1451,28 +1494,337 @@ var UiCliBridge = class {
1451
1494
  }
1452
1495
  };
1453
1496
 
1497
+ // src/main/task-gateway.mts
1498
+ import path6 from "node:path";
1499
+ var KEYWORD_REQUIRED_TYPES = /* @__PURE__ */ new Set(["xhs-unified", "weibo-search", "1688-search"]);
1500
+ var RUN_AT_TYPES = /* @__PURE__ */ new Set(["once", "daily", "weekly"]);
1501
+ function asText(value) {
1502
+ return String(value ?? "").trim();
1503
+ }
1504
+ function asPositiveInt(value, fallback) {
1505
+ const n = Number(value);
1506
+ if (!Number.isFinite(n) || n <= 0) return fallback;
1507
+ return Math.max(1, Math.floor(n));
1508
+ }
1509
+ function asBool(value, fallback) {
1510
+ if (typeof value === "boolean") return value;
1511
+ const text = asText(value).toLowerCase();
1512
+ if (!text) return fallback;
1513
+ if (["1", "true", "yes", "y", "on"].includes(text)) return true;
1514
+ if (["0", "false", "no", "n", "off"].includes(text)) return false;
1515
+ return fallback;
1516
+ }
1517
+ function asOptionalRunAt(value) {
1518
+ const runAt = asText(value);
1519
+ return runAt ? runAt : null;
1520
+ }
1521
+ function normalizeScheduleType(value) {
1522
+ const raw = asText(value);
1523
+ if (raw === "once" || raw === "daily" || raw === "weekly") return raw;
1524
+ return "interval";
1525
+ }
1526
+ function deriveTaskName(commandType, argv) {
1527
+ const keyword = asText(argv.keyword);
1528
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
1529
+ return keyword ? `${commandType}-${keyword}` : `${commandType}-${stamp}`;
1530
+ }
1531
+ function getPlatformFromCommandType(commandType) {
1532
+ const value = asText(commandType).toLowerCase();
1533
+ if (value.startsWith("weibo")) return "weibo";
1534
+ if (value.startsWith("1688")) return "1688";
1535
+ return "xiaohongshu";
1536
+ }
1537
+ function hasExplicitProfileArg(argv) {
1538
+ return Boolean(asText(argv?.profile) || asText(argv?.profiles) || asText(argv?.profilepool));
1539
+ }
1540
+ async function pickDefaultProfileForPlatform(options, platform) {
1541
+ const accountScript = path6.join(options.repoRoot, "apps", "webauto", "entry", "account.mjs");
1542
+ const ret = await options.runJson({
1543
+ title: `account list --platform ${platform}`,
1544
+ cwd: options.repoRoot,
1545
+ args: [accountScript, "list", "--platform", platform, "--json"],
1546
+ timeoutMs: 2e4
1547
+ });
1548
+ if (!ret?.ok) return "";
1549
+ const rows = Array.isArray(ret?.json?.profiles) ? ret.json.profiles : [];
1550
+ const validRows = rows.filter((row) => row?.valid === true && asText(row?.accountId)).sort((a, b) => {
1551
+ const ta = Date.parse(asText(a?.updatedAt) || "") || 0;
1552
+ const tb = Date.parse(asText(b?.updatedAt) || "") || 0;
1553
+ if (tb !== ta) return tb - ta;
1554
+ return asText(a?.profileId).localeCompare(asText(b?.profileId));
1555
+ });
1556
+ return asText(validRows[0]?.profileId) || "";
1557
+ }
1558
+ async function ensureProfileArg(options, commandType, argv) {
1559
+ if (hasExplicitProfileArg(argv)) return argv;
1560
+ const platform = getPlatformFromCommandType(commandType);
1561
+ const profileId = await pickDefaultProfileForPlatform(options, platform);
1562
+ if (!profileId) {
1563
+ 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`);
1564
+ }
1565
+ return {
1566
+ ...argv,
1567
+ profile: profileId
1568
+ };
1569
+ }
1570
+ function normalizeWeiboTaskType(commandType) {
1571
+ if (commandType === "weibo-search") return "search";
1572
+ if (commandType === "weibo-monitor") return "monitor";
1573
+ return "timeline";
1574
+ }
1575
+ function createUiTriggerId() {
1576
+ return `ui-${Date.now()}-${Math.random().toString(16).slice(2, 10)}`;
1577
+ }
1578
+ function normalizeSavePayload(payload) {
1579
+ const commandType = asText(payload?.commandType) || "xhs-unified";
1580
+ const argv = payload?.argv && typeof payload.argv === "object" ? { ...payload.argv } : {};
1581
+ const scheduleType = normalizeScheduleType(payload?.scheduleType);
1582
+ const runAt = asOptionalRunAt(payload?.runAt);
1583
+ const intervalMinutes = asPositiveInt(payload?.intervalMinutes, 30);
1584
+ const maxRunsRaw = Number(payload?.maxRuns);
1585
+ const maxRuns = Number.isFinite(maxRunsRaw) && maxRunsRaw > 0 ? Math.max(1, Math.floor(maxRunsRaw)) : null;
1586
+ const name = asText(payload?.name) || deriveTaskName(commandType, argv);
1587
+ const enabled = asBool(payload?.enabled, true);
1588
+ const id = asText(payload?.id);
1589
+ if (KEYWORD_REQUIRED_TYPES.has(commandType) && !asText(argv.keyword)) {
1590
+ throw new Error("\u5173\u952E\u8BCD\u4E0D\u80FD\u4E3A\u7A7A");
1591
+ }
1592
+ if (commandType.startsWith("weibo-")) {
1593
+ argv["task-type"] = asText(argv["task-type"]) || normalizeWeiboTaskType(commandType);
1594
+ if (commandType === "weibo-monitor" && !asText(argv["user-id"])) {
1595
+ throw new Error("\u5FAE\u535A monitor \u4EFB\u52A1\u9700\u8981 user-id");
1596
+ }
1597
+ }
1598
+ if (RUN_AT_TYPES.has(scheduleType) && !runAt) {
1599
+ throw new Error(`${scheduleType} \u4EFB\u52A1\u9700\u8981\u951A\u70B9\u65F6\u95F4`);
1600
+ }
1601
+ return {
1602
+ id,
1603
+ name,
1604
+ enabled,
1605
+ commandType,
1606
+ scheduleType,
1607
+ intervalMinutes,
1608
+ runAt,
1609
+ maxRuns,
1610
+ argv
1611
+ };
1612
+ }
1613
+ async function runScheduleJson(options, args, timeoutMs) {
1614
+ const script = path6.join(options.repoRoot, "apps", "webauto", "entry", "schedule.mjs");
1615
+ const ret = await options.runJson({
1616
+ title: `schedule ${args.join(" ")}`.trim(),
1617
+ cwd: options.repoRoot,
1618
+ args: [script, ...args, "--json"],
1619
+ timeoutMs: typeof timeoutMs === "number" && timeoutMs > 0 ? timeoutMs : void 0
1620
+ });
1621
+ if (!ret?.ok) {
1622
+ const reason = asText(ret?.error) || asText(ret?.stderr) || asText(ret?.stdout) || "schedule command failed";
1623
+ return { ok: false, error: reason };
1624
+ }
1625
+ return { ok: true, json: ret?.json || {} };
1626
+ }
1627
+ async function scheduleInvoke(options, input) {
1628
+ try {
1629
+ const action = asText(input?.action);
1630
+ const timeoutMs = input?.timeoutMs;
1631
+ if (action === "list") return runScheduleJson(options, ["list"], timeoutMs);
1632
+ if (action === "get") {
1633
+ const taskId = asText(input?.taskId);
1634
+ if (!taskId) return { ok: false, error: "missing taskId" };
1635
+ return runScheduleJson(options, ["get", taskId], timeoutMs);
1636
+ }
1637
+ if (action === "save") {
1638
+ const payload = normalizeSavePayload(input?.payload);
1639
+ payload.argv = await ensureProfileArg(options, payload.commandType, payload.argv);
1640
+ const args = payload.id ? ["update", payload.id] : ["add"];
1641
+ args.push("--name", payload.name);
1642
+ args.push("--enabled", String(payload.enabled));
1643
+ args.push("--command-type", payload.commandType);
1644
+ args.push("--schedule-type", payload.scheduleType);
1645
+ if (RUN_AT_TYPES.has(payload.scheduleType)) {
1646
+ args.push("--run-at", String(payload.runAt || ""));
1647
+ } else {
1648
+ args.push("--interval-minutes", String(payload.intervalMinutes));
1649
+ }
1650
+ args.push("--max-runs", payload.maxRuns === null ? "0" : String(payload.maxRuns));
1651
+ args.push("--argv-json", JSON.stringify(payload.argv));
1652
+ return runScheduleJson(options, args, timeoutMs);
1653
+ }
1654
+ if (action === "run") {
1655
+ const taskId = asText(input?.taskId);
1656
+ if (!taskId) return { ok: false, error: "missing taskId" };
1657
+ return runScheduleJson(options, ["run", taskId], timeoutMs);
1658
+ }
1659
+ if (action === "delete") {
1660
+ const taskId = asText(input?.taskId);
1661
+ if (!taskId) return { ok: false, error: "missing taskId" };
1662
+ return runScheduleJson(options, ["delete", taskId], timeoutMs);
1663
+ }
1664
+ if (action === "export") {
1665
+ const taskId = asText(input?.taskId);
1666
+ return taskId ? runScheduleJson(options, ["export", taskId], timeoutMs) : runScheduleJson(options, ["export"], timeoutMs);
1667
+ }
1668
+ if (action === "import") {
1669
+ const payloadJson = asText(input?.payloadJson);
1670
+ if (!payloadJson) return { ok: false, error: "missing payloadJson" };
1671
+ const mode = asText(input?.mode) === "replace" ? "replace" : "merge";
1672
+ return runScheduleJson(options, ["import", "--payload-json", payloadJson, "--mode", mode], timeoutMs);
1673
+ }
1674
+ if (action === "run-due") {
1675
+ const limit = asPositiveInt(input?.limit, 20);
1676
+ return runScheduleJson(options, ["run-due", "--limit", String(limit)], timeoutMs);
1677
+ }
1678
+ if (action === "daemon-start") {
1679
+ const interval = asPositiveInt(input?.intervalSec, 30);
1680
+ const limit = asPositiveInt(input?.limit, 20);
1681
+ const script = path6.join(options.repoRoot, "apps", "webauto", "entry", "schedule.mjs");
1682
+ const ret = await options.spawnCommand({
1683
+ title: `schedule daemon ${interval}s`,
1684
+ cwd: options.repoRoot,
1685
+ args: [script, "daemon", "--interval-sec", String(Math.max(5, interval)), "--limit", String(limit), "--json"],
1686
+ groupKey: "scheduler"
1687
+ });
1688
+ return { ok: true, runId: asText(ret?.runId) };
1689
+ }
1690
+ return { ok: false, error: `unsupported action: ${action || "<empty>"}` };
1691
+ } catch (err) {
1692
+ return { ok: false, error: err?.message || String(err) };
1693
+ }
1694
+ }
1695
+ async function runEphemeralTask(options, input) {
1696
+ try {
1697
+ const commandType = asText(input?.commandType) || "xhs-unified";
1698
+ let argv = input?.argv && typeof input.argv === "object" ? { ...input.argv } : {};
1699
+ argv = await ensureProfileArg(options, commandType, argv);
1700
+ const profile = asText(argv.profile);
1701
+ const keyword = asText(argv.keyword);
1702
+ const target = asPositiveInt(argv["max-notes"] ?? argv.target, 50);
1703
+ const env = asText(argv.env) || "debug";
1704
+ if (!profile) return { ok: false, error: "\u8BF7\u8F93\u5165 Profile ID" };
1705
+ if (KEYWORD_REQUIRED_TYPES.has(commandType) && !keyword) {
1706
+ return { ok: false, error: "\u8BF7\u8F93\u5165\u5173\u952E\u8BCD" };
1707
+ }
1708
+ if (commandType === "xhs-unified") {
1709
+ const uiTriggerId = createUiTriggerId();
1710
+ const script = path6.join(options.repoRoot, "apps", "webauto", "entry", "xhs-unified.mjs");
1711
+ const ret = await options.spawnCommand({
1712
+ title: `xhs unified: ${keyword}`,
1713
+ cwd: options.repoRoot,
1714
+ groupKey: "xhs-unified",
1715
+ args: [
1716
+ script,
1717
+ "--profile",
1718
+ profile,
1719
+ "--keyword",
1720
+ keyword,
1721
+ "--target",
1722
+ String(target),
1723
+ "--max-notes",
1724
+ String(target),
1725
+ "--env",
1726
+ env,
1727
+ "--do-comments",
1728
+ String(asBool(argv["do-comments"], true)),
1729
+ "--fetch-body",
1730
+ String(asBool(argv["fetch-body"], true)),
1731
+ "--do-likes",
1732
+ String(asBool(argv["do-likes"], false)),
1733
+ "--like-keywords",
1734
+ asText(argv["like-keywords"]),
1735
+ "--ui-trigger-id",
1736
+ uiTriggerId
1737
+ ]
1738
+ });
1739
+ return { ok: true, runId: asText(ret?.runId), commandType, profile, uiTriggerId };
1740
+ }
1741
+ if (commandType === "weibo-search") {
1742
+ const script = path6.join(options.repoRoot, "apps", "webauto", "entry", "weibo-unified.mjs");
1743
+ const ret = await options.spawnCommand({
1744
+ title: `weibo: ${keyword}`,
1745
+ cwd: options.repoRoot,
1746
+ groupKey: "weibo-search",
1747
+ args: [
1748
+ script,
1749
+ "search",
1750
+ "--profile",
1751
+ profile,
1752
+ "--keyword",
1753
+ keyword,
1754
+ "--target",
1755
+ String(target),
1756
+ "--env",
1757
+ env
1758
+ ]
1759
+ });
1760
+ return { ok: true, runId: asText(ret?.runId), commandType, profile };
1761
+ }
1762
+ return { ok: false, error: `\u5F53\u524D\u4EFB\u52A1\u7C7B\u578B\u6682\u4E0D\u652F\u6301\u4EC5\u6267\u884C(\u4E0D\u4FDD\u5B58): ${commandType}` };
1763
+ } catch (err) {
1764
+ return { ok: false, error: err?.message || String(err) };
1765
+ }
1766
+ }
1767
+
1454
1768
  // src/main/index.mts
1455
1769
  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(
1770
+ var spawnedBrowserProcesses = /* @__PURE__ */ new Set();
1771
+ function trackBrowserProcess(pid) {
1772
+ if (pid > 0) {
1773
+ spawnedBrowserProcesses.add(pid);
1774
+ }
1775
+ }
1776
+ function cleanupAllBrowserProcesses(reason = "ui_close") {
1777
+ console.log(`[process-cleanup] Cleaning up ${spawnedBrowserProcesses.size} browser process(s) (${reason})`);
1778
+ for (const pid of spawnedBrowserProcesses) {
1779
+ try {
1780
+ if (process.platform === "win32") {
1781
+ spawn2("taskkill", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
1782
+ } else {
1783
+ process.kill(pid, "SIGTERM");
1784
+ }
1785
+ } catch (err) {
1786
+ console.warn(`[process-cleanup] Failed to kill PID ${pid}:`, err);
1787
+ }
1788
+ }
1789
+ spawnedBrowserProcesses.clear();
1790
+ console.log(`[process-cleanup] Cleanup complete`);
1791
+ }
1792
+ var __dirname = path7.dirname(fileURLToPath2(import.meta.url));
1793
+ var APP_ROOT = path7.resolve(__dirname, "../..");
1794
+ var REPO_ROOT2 = path7.resolve(APP_ROOT, "../..");
1795
+ function readJsonVersion(filePath) {
1796
+ try {
1797
+ const json = JSON.parse(readFileSync(filePath, "utf8"));
1798
+ return String(json?.version || "").trim();
1799
+ } catch {
1800
+ return "";
1801
+ }
1802
+ }
1803
+ function resolveVersionInfo() {
1804
+ const webauto = readJsonVersion(path7.join(REPO_ROOT2, "package.json")) || "0.0.0";
1805
+ const desktop = readJsonVersion(path7.join(APP_ROOT, "package.json")) || webauto;
1806
+ const windowTitle = `WebAuto Desktop v${webauto}`;
1807
+ const badge = desktop === webauto ? `v${webauto}` : `webauto v${webauto} \xB7 console v${desktop}`;
1808
+ return { webauto, desktop, windowTitle, badge };
1809
+ }
1810
+ var VERSION_INFO = resolveVersionInfo();
1811
+ var DESKTOP_HEARTBEAT_FILE = path7.join(
1460
1812
  os5.homedir(),
1461
1813
  ".webauto",
1462
1814
  "run",
1463
1815
  "desktop-console-heartbeat.json"
1464
1816
  );
1465
1817
  var profileStore = createProfileStore({ repoRoot: REPO_ROOT2 });
1466
- var XHS_SCRIPTS_ROOT = path6.join(REPO_ROOT2, "scripts", "xiaohongshu");
1818
+ var XHS_SCRIPTS_ROOT = path7.join(REPO_ROOT2, "scripts", "xiaohongshu");
1467
1819
  var XHS_FULL_COLLECT_RE = /collect-content\.mjs$/;
1468
1820
  function configureElectronPaths() {
1469
1821
  try {
1470
1822
  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");
1823
+ const normalized = path7.normalize(downloadRoot);
1824
+ const baseDir = path7.basename(normalized).toLowerCase() === "download" ? path7.dirname(normalized) : normalized;
1825
+ const userDataRoot = path7.join(baseDir, "desktop-console");
1826
+ const cacheRoot = path7.join(userDataRoot, "cache");
1827
+ const gpuCacheRoot = path7.join(cacheRoot, "gpu");
1476
1828
  try {
1477
1829
  mkdirSync(cacheRoot, { recursive: true });
1478
1830
  } catch {
@@ -1485,6 +1837,17 @@ function configureElectronPaths() {
1485
1837
  app.setPath("cache", cacheRoot);
1486
1838
  app.commandLine.appendSwitch("disk-cache-dir", cacheRoot);
1487
1839
  app.commandLine.appendSwitch("gpu-cache-dir", gpuCacheRoot);
1840
+ const disableGpuByDefault = process.platform === "win32" && String(process.env.WEBAUTO_ELECTRON_DISABLE_GPU || "1").trim() !== "0";
1841
+ if (disableGpuByDefault) {
1842
+ try {
1843
+ app.disableHardwareAcceleration();
1844
+ } catch {
1845
+ }
1846
+ app.commandLine.appendSwitch("disable-gpu");
1847
+ app.commandLine.appendSwitch("disable-gpu-compositing");
1848
+ app.commandLine.appendSwitch("disable-direct-composition");
1849
+ app.commandLine.appendSwitch("use-angle", "swiftshader");
1850
+ }
1488
1851
  } catch (err) {
1489
1852
  console.warn("[desktop-console] failed to configure cache paths", err);
1490
1853
  }
@@ -1514,8 +1877,35 @@ var GroupQueue = class {
1514
1877
  };
1515
1878
  var groupQueues = /* @__PURE__ */ new Map();
1516
1879
  var runs = /* @__PURE__ */ new Map();
1880
+ var runLifecycle = /* @__PURE__ */ new Map();
1517
1881
  var trackedRunPids = /* @__PURE__ */ new Set();
1518
1882
  var appExitCleanupPromise = null;
1883
+ function setRunLifecycle(runId, patch) {
1884
+ const rid = String(runId || "").trim();
1885
+ if (!rid) return;
1886
+ const current = runLifecycle.get(rid) || {
1887
+ runId: rid,
1888
+ state: "queued",
1889
+ title: "",
1890
+ queuedAt: now()
1891
+ };
1892
+ const next = {
1893
+ ...current,
1894
+ ...patch,
1895
+ runId: rid
1896
+ };
1897
+ runLifecycle.set(rid, next);
1898
+ if (runLifecycle.size > 400) {
1899
+ const rows = Array.from(runLifecycle.values()).sort((a, b) => (a.queuedAt || 0) - (b.queuedAt || 0));
1900
+ const toDrop = rows.slice(0, Math.max(0, rows.length - 200));
1901
+ for (const row of toDrop) runLifecycle.delete(row.runId);
1902
+ }
1903
+ }
1904
+ function getRunLifecycle(runId) {
1905
+ const rid = String(runId || "").trim();
1906
+ if (!rid) return null;
1907
+ return runLifecycle.get(rid) || null;
1908
+ }
1519
1909
  var UI_HEARTBEAT_TIMEOUT_MS = resolveUiHeartbeatTimeoutMs(process.env);
1520
1910
  var lastUiHeartbeatAt = Date.now();
1521
1911
  var heartbeatWatchdog = null;
@@ -1523,6 +1913,16 @@ var heartbeatTimeoutHandled = false;
1523
1913
  var coreServicesStopRequested = false;
1524
1914
  var coreServiceHeartbeatTimer = null;
1525
1915
  var coreServiceHeartbeatStopped = false;
1916
+ var RUN_LOG_DIR = path7.join(os5.homedir(), ".webauto", "logs");
1917
+ function appendRunLog(runId, line) {
1918
+ const rid = String(runId || "").trim();
1919
+ const text = String(line || "").replace(/\r?\n/g, " ").trim();
1920
+ if (!rid || !text) return;
1921
+ const logPath = path7.join(RUN_LOG_DIR, `ui-run-${rid}.log`);
1922
+ void fs4.mkdir(RUN_LOG_DIR, { recursive: true }).then(() => fs4.appendFile(logPath, `${text}
1923
+ `, "utf8")).catch(() => {
1924
+ });
1925
+ }
1526
1926
  async function writeCoreServiceHeartbeat(status) {
1527
1927
  const filePath = String(process.env.WEBAUTO_HEARTBEAT_FILE || DESKTOP_HEARTBEAT_FILE).trim() || DESKTOP_HEARTBEAT_FILE;
1528
1928
  const payload = {
@@ -1532,7 +1932,7 @@ async function writeCoreServiceHeartbeat(status) {
1532
1932
  source: "desktop-console"
1533
1933
  };
1534
1934
  try {
1535
- await fs4.mkdir(path6.dirname(filePath), { recursive: true });
1935
+ await fs4.mkdir(path7.dirname(filePath), { recursive: true });
1536
1936
  await fs4.writeFile(filePath, JSON.stringify(payload), "utf8");
1537
1937
  } catch {
1538
1938
  }
@@ -1682,7 +2082,7 @@ async function cleanupTrackedRunPidsBestEffort(reason) {
1682
2082
  }
1683
2083
  }
1684
2084
  async function cleanupCamoSessionsBestEffort(reason, includeLocks) {
1685
- const camoCli = path6.join(REPO_ROOT2, "bin", "camoufox-cli.mjs");
2085
+ const camoCli = path7.join(REPO_ROOT2, "bin", "camoufox-cli.mjs");
1686
2086
  const invoke = async (args, timeoutMs = 6e4) => runJson({
1687
2087
  title: `camo ${args.join(" ")}`,
1688
2088
  cwd: REPO_ROOT2,
@@ -1703,6 +2103,7 @@ async function cleanupRuntimeEnvironment(reason, options = {}) {
1703
2103
  killAllRuns(reason);
1704
2104
  await cleanupTrackedRunPidsBestEffort(reason);
1705
2105
  await cleanupCamoSessionsBestEffort(reason, options.includeLockCleanup !== false);
2106
+ cleanupAllBrowserProcesses(reason);
1706
2107
  if (options.stopUiBridge) {
1707
2108
  await uiCliBridge.stop().catch(() => null);
1708
2109
  }
@@ -1775,12 +2176,13 @@ function getQueue(groupKey) {
1775
2176
  function generateRunId() {
1776
2177
  return `run_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`;
1777
2178
  }
1778
- function createLineEmitter(runId, type) {
2179
+ function createLineEmitter(runId, type, onLine) {
1779
2180
  let pending = "";
1780
2181
  const emit = (line) => {
1781
2182
  const normalized = String(line || "").replace(/\r$/, "");
1782
2183
  if (!normalized) return;
1783
2184
  sendEvent({ type, runId, line: normalized, ts: now() });
2185
+ if (typeof onLine === "function") onLine(normalized);
1784
2186
  };
1785
2187
  return {
1786
2188
  push(chunk) {
@@ -1804,19 +2206,44 @@ function resolveNodeBin2() {
1804
2206
  const explicit = String(process.env.WEBAUTO_NODE_BIN || "").trim();
1805
2207
  if (explicit) return explicit;
1806
2208
  const npmNode = String(process.env.npm_node_execpath || "").trim();
1807
- if (npmNode) return npmNode;
2209
+ if (npmNode) {
2210
+ const base = path7.basename(npmNode).toLowerCase();
2211
+ const isNode = base === "node" || base === "node.exe";
2212
+ if (isNode) return npmNode;
2213
+ }
1808
2214
  return process.platform === "win32" ? "node.exe" : "node";
1809
2215
  }
1810
2216
  function resolveCwd(input) {
1811
2217
  const raw = String(input || "").trim();
1812
2218
  if (!raw) return REPO_ROOT2;
1813
- return path6.isAbsolute(raw) ? raw : path6.resolve(REPO_ROOT2, raw);
2219
+ return path7.isAbsolute(raw) ? raw : path7.resolve(REPO_ROOT2, raw);
2220
+ }
2221
+ function isPidAlive(pid) {
2222
+ const target = Number(pid || 0);
2223
+ if (!Number.isFinite(target) || target <= 0) return false;
2224
+ if (process.platform === "win32") {
2225
+ const ret = spawnSync2("tasklist", ["/FI", `PID eq ${target}`], {
2226
+ windowsHide: true,
2227
+ encoding: "utf8",
2228
+ stdio: ["ignore", "pipe", "ignore"]
2229
+ });
2230
+ const out = String(ret.stdout || "");
2231
+ if (!out) return false;
2232
+ const lines = out.split(/\r?\n/g).map((line) => line.trim()).filter(Boolean);
2233
+ return lines.some((line) => line.includes(` ${target}`));
2234
+ }
2235
+ try {
2236
+ process.kill(target, 0);
2237
+ return true;
2238
+ } catch {
2239
+ return false;
2240
+ }
1814
2241
  }
1815
2242
  var cachedStateMod = null;
1816
2243
  async function getStateModule() {
1817
2244
  if (cachedStateMod) return cachedStateMod;
1818
2245
  try {
1819
- const p = path6.join(REPO_ROOT2, "dist", "modules", "state", "src", "xiaohongshu-collect-state.js");
2246
+ const p = path7.join(REPO_ROOT2, "dist", "modules", "state", "src", "xiaohongshu-collect-state.js");
1820
2247
  cachedStateMod = await import(pathToFileURL3(p).href);
1821
2248
  return cachedStateMod;
1822
2249
  } catch {
@@ -1830,6 +2257,12 @@ async function spawnCommand(spec) {
1830
2257
  const q = getQueue(groupKey);
1831
2258
  const cwd = resolveCwd(spec.cwd);
1832
2259
  const args = Array.isArray(spec.args) ? spec.args : [];
2260
+ appendRunLog(runId, `[queued] title=${String(spec.title || "").trim() || "-"} cwd=${cwd}`);
2261
+ setRunLifecycle(runId, {
2262
+ state: "queued",
2263
+ title: String(spec.title || ""),
2264
+ queuedAt: now()
2265
+ });
1833
2266
  const isXhsRunCommand = args.some((item) => /xhs-(orchestrate|unified)\.mjs$/i.test(String(item || "").replace(/\\/g, "/")));
1834
2267
  const extractProfilesFromArgs = (argv) => {
1835
2268
  const out = [];
@@ -1862,49 +2295,133 @@ async function spawnCommand(spec) {
1862
2295
  let finished = false;
1863
2296
  let exitCode = null;
1864
2297
  let exitSignal = null;
2298
+ let orphanCheckTimer = null;
1865
2299
  const finalize = (code, signal) => {
1866
2300
  if (finished) return;
1867
2301
  finished = true;
2302
+ if (orphanCheckTimer) {
2303
+ clearInterval(orphanCheckTimer);
2304
+ orphanCheckTimer = null;
2305
+ }
2306
+ setRunLifecycle(runId, {
2307
+ state: "exited",
2308
+ exitedAt: now(),
2309
+ exitCode: code,
2310
+ signal
2311
+ });
1868
2312
  sendEvent({ type: "exit", runId, exitCode: code, signal, ts: now() });
2313
+ appendRunLog(runId, `[exit] code=${code ?? "null"} signal=${signal ?? "null"}`);
1869
2314
  runs.delete(runId);
1870
2315
  resolve();
1871
2316
  };
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
- });
2317
+ let child;
2318
+ try {
2319
+ const nodeBin = resolveNodeBin2();
2320
+ appendRunLog(runId, `[cmd] ${nodeBin}`);
2321
+ child = spawn2(nodeBin, args, {
2322
+ cwd,
2323
+ env: {
2324
+ ...process.env,
2325
+ ...spec.env || {}
2326
+ },
2327
+ stdio: ["ignore", "pipe", "pipe"],
2328
+ windowsHide: true
2329
+ });
2330
+ } catch (err) {
2331
+ const message = err?.message || String(err);
2332
+ setRunLifecycle(runId, {
2333
+ state: "exited",
2334
+ exitedAt: now(),
2335
+ exitCode: null,
2336
+ signal: "spawn_exception",
2337
+ lastError: message
2338
+ });
2339
+ sendEvent({ type: "stderr", runId, line: `[spawn-throw] ${message}`, ts: now() });
2340
+ appendRunLog(runId, `[spawn-throw] ${message}`);
2341
+ finalize(null, "spawn_exception");
2342
+ return;
2343
+ }
2344
+ const stdoutLines = createLineEmitter(runId, "stdout", (line) => appendRunLog(runId, `[stdout] ${line}`));
2345
+ const stderrLines = createLineEmitter(runId, "stderr", (line) => appendRunLog(runId, `[stderr] ${line}`));
2346
+ try {
2347
+ child.stdout?.on("data", (chunk) => {
2348
+ stdoutLines.push(chunk);
2349
+ });
2350
+ child.stderr?.on("data", (chunk) => {
2351
+ const text = chunk.toString("utf8");
2352
+ const lines = text.split(/\r?\n/g).map((line) => line.trim()).filter(Boolean);
2353
+ if (lines.length > 0) {
2354
+ setRunLifecycle(runId, { lastError: lines[lines.length - 1] });
2355
+ }
2356
+ stderrLines.push(chunk);
2357
+ });
2358
+ child.on("error", (err) => {
2359
+ const message = err?.message || String(err);
2360
+ setRunLifecycle(runId, { lastError: message });
2361
+ sendEvent({ type: "stderr", runId, line: `[spawn-error] ${message}`, ts: now() });
2362
+ appendRunLog(runId, `[spawn-error] ${message}`);
2363
+ finalize(null, "error");
2364
+ });
2365
+ child.on("exit", (code, signal) => {
2366
+ exitCode = code;
2367
+ exitSignal = signal;
2368
+ const timer = setTimeout(() => {
2369
+ if (finished) return;
2370
+ untrackRunPid(child);
2371
+ stdoutLines.flush();
2372
+ stderrLines.flush();
2373
+ finalize(exitCode ?? null, exitSignal ?? null);
2374
+ }, 200);
2375
+ if (timer && typeof timer.unref === "function") {
2376
+ timer.unref();
2377
+ }
2378
+ });
2379
+ child.on("close", (code, signal) => {
2380
+ untrackRunPid(child);
2381
+ stdoutLines.flush();
2382
+ stderrLines.flush();
2383
+ finalize(exitCode ?? code ?? null, exitSignal ?? signal ?? null);
2384
+ });
2385
+ } catch (err) {
2386
+ const message = err?.message || String(err);
2387
+ setRunLifecycle(runId, { lastError: message });
2388
+ sendEvent({ type: "stderr", runId, line: `[spawn-setup-error] ${message}`, ts: now() });
2389
+ appendRunLog(runId, `[spawn-setup-error] ${message}`);
2390
+ finalize(null, "setup_error");
2391
+ return;
2392
+ }
1883
2393
  trackRunPid(child);
2394
+ if (child.pid) {
2395
+ trackBrowserProcess(child.pid);
2396
+ }
1884
2397
  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);
2398
+ setRunLifecycle(runId, {
2399
+ state: "running",
2400
+ startedAt: now(),
2401
+ pid: child.pid || -1,
2402
+ title: String(spec.title || "")
1907
2403
  });
2404
+ sendEvent({ type: "started", runId, title: spec.title, pid: child.pid ?? -1, ts: now() });
2405
+ appendRunLog(runId, `[started] pid=${child.pid ?? -1} title=${String(spec.title || "").trim() || "-"}`);
2406
+ if (args.length > 0) {
2407
+ appendRunLog(runId, `[args] ${args.join(" ")}`);
2408
+ }
2409
+ const pid = Number(child.pid || 0);
2410
+ if (pid > 0) {
2411
+ orphanCheckTimer = setInterval(() => {
2412
+ if (finished) return;
2413
+ if (!isPidAlive(pid)) {
2414
+ untrackRunPid(child);
2415
+ stdoutLines.flush();
2416
+ stderrLines.flush();
2417
+ appendRunLog(runId, "[watchdog] child pid disappeared before close/exit event");
2418
+ finalize(exitCode, exitSignal || "pid_gone");
2419
+ }
2420
+ }, 1e3);
2421
+ if (orphanCheckTimer && typeof orphanCheckTimer.unref === "function") {
2422
+ orphanCheckTimer.unref();
2423
+ }
2424
+ }
1908
2425
  })
1909
2426
  );
1910
2427
  return { runId };
@@ -1944,9 +2461,106 @@ async function runJson(spec) {
1944
2461
  return { ok: true, code, stdout: out, stderr: err };
1945
2462
  }
1946
2463
  }
2464
+ function sleep2(ms) {
2465
+ return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
2466
+ }
2467
+ function toEpochMs(value) {
2468
+ const raw = String(value ?? "").trim();
2469
+ if (!raw) return 0;
2470
+ const asNum = Number(raw);
2471
+ if (Number.isFinite(asNum) && asNum > 0) return asNum;
2472
+ const asDate = Date.parse(raw);
2473
+ return Number.isFinite(asDate) ? asDate : 0;
2474
+ }
2475
+ function resolveUnifiedApiBaseUrl() {
2476
+ return String(
2477
+ process.env.WEBAUTO_UNIFIED_API || process.env.WEBAUTO_UNIFIED_URL || "http://127.0.0.1:7701"
2478
+ ).trim().replace(/\/+$/, "");
2479
+ }
2480
+ async function listUnifiedTasks() {
2481
+ const baseUrl = resolveUnifiedApiBaseUrl();
2482
+ const res = await fetch(`${baseUrl}/api/v1/tasks`, { signal: AbortSignal.timeout(3e3) });
2483
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
2484
+ const payload = await res.json().catch(() => ({}));
2485
+ return Array.isArray(payload?.data) ? payload.data : [];
2486
+ }
2487
+ function pickUnifiedRunId(tasks, options) {
2488
+ const profileId = String(options.profileId || "").trim();
2489
+ const keyword = String(options.keyword || "").trim();
2490
+ const uiTriggerId = String(options.uiTriggerId || "").trim();
2491
+ const minTs = Number(options.minTs || 0);
2492
+ const baselineRunIds = options.baselineRunIds instanceof Set ? options.baselineRunIds : /* @__PURE__ */ new Set();
2493
+ const rows = tasks.filter((task) => {
2494
+ const runId = String(task?.runId || task?.id || "").trim();
2495
+ if (!runId) return false;
2496
+ if (baselineRunIds.has(runId)) return false;
2497
+ const phase = String(task?.phase || task?.lastPhase || "").trim().toLowerCase();
2498
+ if (phase !== "unified") return false;
2499
+ const taskProfile = String(task?.profileId || "").trim();
2500
+ const taskKeyword = String(task?.keyword || "").trim();
2501
+ const taskTrigger = String(
2502
+ task?.uiTriggerId || task?.triggerId || task?.meta?.uiTriggerId || task?.context?.uiTriggerId || ""
2503
+ ).trim();
2504
+ if (uiTriggerId && taskTrigger !== uiTriggerId) return false;
2505
+ if (profileId && taskProfile && taskProfile !== profileId) return false;
2506
+ if (keyword && taskKeyword && taskKeyword !== keyword) return false;
2507
+ const createdTs = toEpochMs(task?.createdAt);
2508
+ const startedTs = toEpochMs(task?.startedAt);
2509
+ const ts = createdTs || startedTs;
2510
+ return ts >= minTs;
2511
+ }).sort((a, b) => {
2512
+ const ta = toEpochMs(a?.createdAt) || toEpochMs(a?.startedAt);
2513
+ const tb = toEpochMs(b?.createdAt) || toEpochMs(b?.startedAt);
2514
+ return tb - ta;
2515
+ });
2516
+ const picked = rows[0] || null;
2517
+ return String(picked?.runId || picked?.id || "").trim();
2518
+ }
2519
+ async function waitForUnifiedRunRegistration(input) {
2520
+ const desktopRunId = String(input.desktopRunId || "").trim();
2521
+ const profileId = String(input.profileId || "").trim();
2522
+ const keyword = String(input.keyword || "").trim();
2523
+ const uiTriggerId = String(input.uiTriggerId || "").trim();
2524
+ const timeoutMs = Math.max(2e3, Number(input.timeoutMs || 2e4) || 2e4);
2525
+ const startedAt = Date.now();
2526
+ const minTs = startedAt - 3e4;
2527
+ const baselineRunIds = input.baselineRunIds instanceof Set ? input.baselineRunIds : /* @__PURE__ */ new Set();
2528
+ let lastFetchError = "";
2529
+ while (Date.now() - startedAt < timeoutMs) {
2530
+ try {
2531
+ const tasks = await listUnifiedTasks();
2532
+ const unifiedRunId = pickUnifiedRunId(tasks, {
2533
+ profileId,
2534
+ keyword,
2535
+ uiTriggerId,
2536
+ minTs,
2537
+ baselineRunIds
2538
+ });
2539
+ if (unifiedRunId) return { ok: true, runId: unifiedRunId };
2540
+ lastFetchError = "";
2541
+ } catch (err) {
2542
+ lastFetchError = err?.message || String(err);
2543
+ }
2544
+ const lifecycle2 = getRunLifecycle(desktopRunId);
2545
+ if (lifecycle2?.state === "exited") {
2546
+ const detail = lifecycle2.lastError || `exit=${lifecycle2.exitCode ?? "null"}`;
2547
+ return { ok: false, error: `\u4EFB\u52A1\u8FDB\u7A0B\u63D0\u524D\u9000\u51FA\uFF0C\u672A\u6CE8\u518C unified runId (${detail})` };
2548
+ }
2549
+ await sleep2(500);
2550
+ }
2551
+ const lifecycle = getRunLifecycle(desktopRunId);
2552
+ if (lifecycle?.state === "queued") {
2553
+ 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" };
2554
+ }
2555
+ if (lifecycle?.state === "running") {
2556
+ return { ok: false, error: "\u4EFB\u52A1\u8FDB\u7A0B\u5DF2\u542F\u52A8\uFF0C\u4F46\u5728\u8D85\u65F6\u5185\u672A\u6CE8\u518C unified runId" };
2557
+ }
2558
+ const suffix = lastFetchError ? `: ${lastFetchError}` : "";
2559
+ return { ok: false, error: `\u672A\u68C0\u6D4B\u5230 unified runId${suffix}` };
2560
+ }
1947
2561
  async function scanResults(input) {
1948
2562
  const downloadRoot = String(input.downloadRoot || resolveDefaultDownloadRoot());
1949
- const root = path6.join(downloadRoot, "xiaohongshu");
2563
+ const root = path7.join(downloadRoot, "xiaohongshu");
1950
2564
  const result = { ok: true, root, entries: [] };
1951
2565
  try {
1952
2566
  const stateMod = await getStateModule();
@@ -1954,12 +2568,12 @@ async function scanResults(input) {
1954
2568
  for (const envEnt of envDirs) {
1955
2569
  if (!envEnt.isDirectory()) continue;
1956
2570
  const env = envEnt.name;
1957
- const envPath = path6.join(root, env);
2571
+ const envPath = path7.join(root, env);
1958
2572
  const keywordDirs = await fs4.readdir(envPath, { withFileTypes: true });
1959
2573
  for (const kwEnt of keywordDirs) {
1960
2574
  if (!kwEnt.isDirectory()) continue;
1961
2575
  const keyword = kwEnt.name;
1962
- const kwPath = path6.join(envPath, keyword);
2576
+ const kwPath = path7.join(envPath, keyword);
1963
2577
  const stat = await fs4.stat(kwPath).catch(() => null);
1964
2578
  let stateSummary = null;
1965
2579
  if (stateMod?.loadXhsCollectState) {
@@ -1994,7 +2608,7 @@ async function listXhsFullCollectScripts() {
1994
2608
  return {
1995
2609
  id: `xhs:${name}`,
1996
2610
  label: `Full Collect (${name})`,
1997
- path: path6.join(XHS_SCRIPTS_ROOT, name)
2611
+ path: path7.join(XHS_SCRIPTS_ROOT, name)
1998
2612
  };
1999
2613
  });
2000
2614
  return { ok: true, scripts };
@@ -2064,11 +2678,11 @@ async function listDir(input) {
2064
2678
  const items = await fs4.readdir(dir, { withFileTypes: true }).catch(() => []);
2065
2679
  for (const ent of items) {
2066
2680
  if (entries.length >= maxEntries) break;
2067
- const full = path6.join(dir, ent.name);
2681
+ const full = path7.join(dir, ent.name);
2068
2682
  const st = await fs4.stat(full).catch(() => null);
2069
2683
  entries.push({
2070
2684
  path: full,
2071
- rel: path6.relative(root, full),
2685
+ rel: path7.relative(root, full),
2072
2686
  name: ent.name,
2073
2687
  isDir: ent.isDirectory(),
2074
2688
  size: st?.size || 0,
@@ -2085,13 +2699,13 @@ async function listDir(input) {
2085
2699
  }
2086
2700
  function createWindow() {
2087
2701
  win = new BrowserWindow2({
2088
- title: "WebAuto Desktop v0.1.1",
2702
+ title: VERSION_INFO.windowTitle,
2089
2703
  width: 1280,
2090
2704
  height: 900,
2091
2705
  minWidth: 920,
2092
2706
  minHeight: 800,
2093
2707
  webPreferences: {
2094
- preload: path6.join(APP_ROOT, "dist", "main", "preload.mjs"),
2708
+ preload: path7.join(APP_ROOT, "dist", "main", "preload.mjs"),
2095
2709
  contextIsolation: true,
2096
2710
  nodeIntegration: false,
2097
2711
  sandbox: false,
@@ -2099,7 +2713,7 @@ function createWindow() {
2099
2713
  backgroundThrottling: false
2100
2714
  }
2101
2715
  });
2102
- const htmlPath = path6.join(APP_ROOT, "dist", "renderer", "index.html");
2716
+ const htmlPath = path7.join(APP_ROOT, "dist", "renderer", "index.html");
2103
2717
  void win.loadFile(htmlPath);
2104
2718
  ensureStateBridge();
2105
2719
  }
@@ -2131,6 +2745,7 @@ ipcMain2.on("preload:test", () => {
2131
2745
  setTimeout(() => app.quit(), 200);
2132
2746
  });
2133
2747
  ipcMain2.handle("settings:get", async () => readDesktopConsoleSettings({ appRoot: APP_ROOT, repoRoot: REPO_ROOT2 }));
2748
+ ipcMain2.handle("app:getVersion", async () => VERSION_INFO);
2134
2749
  ipcMain2.handle("settings:set", async (_evt, next) => {
2135
2750
  const updated = await writeDesktopConsoleSettings({ appRoot: APP_ROOT, repoRoot: REPO_ROOT2 }, next || {});
2136
2751
  const w = getWin();
@@ -2238,6 +2853,73 @@ ipcMain2.handle("cmd:runJson", async (_evt, spec) => {
2238
2853
  const args = Array.isArray(spec?.args) ? spec.args : [];
2239
2854
  return runJson({ ...spec, cwd, args });
2240
2855
  });
2856
+ ipcMain2.handle("schedule:invoke", async (_evt, input) => scheduleInvoke(
2857
+ {
2858
+ repoRoot: REPO_ROOT2,
2859
+ runJson: (spec) => runJson(spec),
2860
+ spawnCommand: (spec) => spawnCommand(spec)
2861
+ },
2862
+ input || { action: "list" }
2863
+ ));
2864
+ ipcMain2.handle("task:runEphemeral", async (_evt, input) => {
2865
+ const payload = input || {};
2866
+ let baselineRunIds = /* @__PURE__ */ new Set();
2867
+ try {
2868
+ const baselineTasks = await listUnifiedTasks();
2869
+ baselineRunIds = new Set(
2870
+ baselineTasks.map((task) => String(task?.runId || task?.id || "").trim()).filter(Boolean)
2871
+ );
2872
+ } catch {
2873
+ baselineRunIds = /* @__PURE__ */ new Set();
2874
+ }
2875
+ const result = await runEphemeralTask(
2876
+ {
2877
+ repoRoot: REPO_ROOT2,
2878
+ runJson: (spec) => runJson(spec),
2879
+ spawnCommand: (spec) => spawnCommand(spec)
2880
+ },
2881
+ payload
2882
+ );
2883
+ if (!result?.ok) return result;
2884
+ const commandType = String(result?.commandType || payload?.commandType || "").trim().toLowerCase();
2885
+ const desktopRunId = String(result?.runId || "").trim();
2886
+ if (commandType !== "xhs-unified" || !desktopRunId) {
2887
+ return result;
2888
+ }
2889
+ const profileId = String(result?.profile || payload?.argv?.profile || "").trim();
2890
+ const keyword = String(payload?.argv?.keyword || payload?.argv?.k || "").trim();
2891
+ const uiTriggerId = String(result?.uiTriggerId || payload?.argv?.["ui-trigger-id"] || "").trim();
2892
+ appendRunLog(
2893
+ desktopRunId,
2894
+ `[wait-register] profile=${profileId || "-"} keyword=${keyword || "-"} uiTriggerId=${uiTriggerId || "-"}`
2895
+ );
2896
+ const waited = await waitForUnifiedRunRegistration({
2897
+ desktopRunId,
2898
+ profileId,
2899
+ keyword,
2900
+ uiTriggerId,
2901
+ baselineRunIds,
2902
+ timeoutMs: 2e4
2903
+ });
2904
+ if (!waited.ok) {
2905
+ appendRunLog(desktopRunId, `[wait-register-failed] ${String(waited.error || "unknown_error")}`);
2906
+ return {
2907
+ ok: false,
2908
+ error: waited.error,
2909
+ runId: desktopRunId,
2910
+ commandType: result?.commandType || "xhs-unified",
2911
+ profile: profileId,
2912
+ uiTriggerId
2913
+ };
2914
+ }
2915
+ appendRunLog(desktopRunId, `[wait-register-ok] unifiedRunId=${waited.runId || "-"} uiTriggerId=${uiTriggerId || "-"}`);
2916
+ return {
2917
+ ...result,
2918
+ runId: desktopRunId,
2919
+ unifiedRunId: waited.runId,
2920
+ uiTriggerId
2921
+ };
2922
+ });
2241
2923
  ipcMain2.handle("results:scan", async (_evt, spec) => scanResults(spec || {}));
2242
2924
  ipcMain2.handle("fs:listDir", async (_evt, spec) => listDir(spec));
2243
2925
  ipcMain2.handle(
@@ -2277,6 +2959,49 @@ ipcMain2.handle("env:repairCore", async () => {
2277
2959
  const services = await checkServices().catch(() => ({ unifiedApi: false, camoRuntime: false }));
2278
2960
  return { ok, services };
2279
2961
  });
2962
+ ipcMain2.handle("env:cleanup", async () => {
2963
+ markUiHeartbeat("env_cleanup");
2964
+ console.log("[env:cleanup] Starting environment cleanup...");
2965
+ await cleanupRuntimeEnvironment("env_cleanup_requested", {
2966
+ stopUiBridge: false,
2967
+ stopHeartbeat: false,
2968
+ stopCoreServices: false,
2969
+ stopStateBridge: false,
2970
+ includeLockCleanup: true
2971
+ });
2972
+ let locksCleared = 0;
2973
+ try {
2974
+ const profiles = await profileStore.listProfiles();
2975
+ for (const p of profiles) {
2976
+ const lockFile = path7.join(p.path, ".lock");
2977
+ try {
2978
+ await fs4.unlink(lockFile);
2979
+ locksCleared++;
2980
+ console.log(`[env:cleanup] Cleared lock for profile ${p.profileId}`);
2981
+ } catch (err) {
2982
+ if (err?.code !== "ENOENT") {
2983
+ console.warn(`[env:cleanup] Failed to clear lock for ${p.profileId}:`, err.message);
2984
+ }
2985
+ }
2986
+ }
2987
+ } catch (err) {
2988
+ console.warn("[env:cleanup] Failed to list profiles:", err);
2989
+ }
2990
+ await stopCoreDaemon().catch(() => null);
2991
+ const restarted = await startCoreDaemon().catch(() => false);
2992
+ const services = await checkServices().catch(() => ({ unifiedApi: false, camoRuntime: false }));
2993
+ const firefox = await checkFirefox().catch(() => ({ installed: false }));
2994
+ const camo = await checkCamoCli().catch(() => ({ installed: false }));
2995
+ console.log("[env:cleanup] Cleanup complete:", { locksCleared, restarted, services, firefox, camo });
2996
+ return {
2997
+ ok: true,
2998
+ locksCleared,
2999
+ coreRestarted: restarted,
3000
+ services,
3001
+ firefox,
3002
+ camo
3003
+ };
3004
+ });
2280
3005
  ipcMain2.handle("env:repairDeps", async (_evt, input) => {
2281
3006
  const wantCore = Boolean(input?.core);
2282
3007
  const wantBrowser = Boolean(input?.browser);
@@ -2294,7 +3019,7 @@ ipcMain2.handle("env:repairDeps", async (_evt, input) => {
2294
3019
  if (!coreOk) result.ok = false;
2295
3020
  }
2296
3021
  if (wantBrowser || wantGeoip) {
2297
- const args = [path6.join("apps", "webauto", "entry", "xhs-install.mjs")];
3022
+ const args = [path7.join("apps", "webauto", "entry", "xhs-install.mjs")];
2298
3023
  if (wantReinstall) args.push("--reinstall");
2299
3024
  else if (wantUninstall) args.push("--uninstall");
2300
3025
  else args.push("--install");
@@ -2313,20 +3038,6 @@ ipcMain2.handle("env:repairDeps", async (_evt, input) => {
2313
3038
  result.env = await checkEnvironment().catch(() => null);
2314
3039
  return result;
2315
3040
  });
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
3041
  ipcMain2.handle("config:saveLast", async (_evt, config) => {
2331
3042
  await saveCrawlConfig({ appRoot: APP_ROOT, repoRoot: REPO_ROOT2 }, config);
2332
3043
  return { ok: true };
@@ -2417,7 +3128,7 @@ ipcMain2.handle("runtime:kill", async (_evt, input) => {
2417
3128
  ipcMain2.handle("runtime:restartPhase1", async (_evt, input) => {
2418
3129
  const profileId = String(input?.profileId || "").trim();
2419
3130
  if (!profileId) return { ok: false, error: "missing profileId" };
2420
- const args = [path6.join(REPO_ROOT2, "scripts", "xiaohongshu", "phase1-boot.mjs"), "--profile", profileId, "--headless", "false"];
3131
+ const args = [path7.join(REPO_ROOT2, "scripts", "xiaohongshu", "phase1-boot.mjs"), "--profile", profileId, "--headless", "false"];
2421
3132
  return spawnCommand({ title: `Phase1 restart ${profileId}`, cwd: REPO_ROOT2, args, groupKey: "phase1" });
2422
3133
  });
2423
3134
  ipcMain2.handle("runtime:setBrowserTitle", async (_evt, input) => {