@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
@@ -75,7 +75,7 @@ function renderPreflight(root, ctx2) {
75
75
  const batchDeleteFp = createEl("input", { type: "checkbox" });
76
76
  const onboardingSummary = createEl("div", { className: "muted" }, ["\u6B63\u5728\u52A0\u8F7D profile \u4FE1\u606F..."]);
77
77
  const onboardingTips = createEl("div", { className: "muted", style: "font-size:12px; margin-top:6px;" }, [
78
- "\u9996\u6B21\u4F7F\u7528\u5EFA\u8BAE\uFF1A\u8F93\u5165\u8D26\u53F7\u540D\u540E\uFF0C\u7CFB\u7EDF\u81EA\u52A8\u6309 <\u8D26\u53F7\u540D>-batch-1/2/3 \u547D\u540D\uFF1B\u7559\u7A7A\u9ED8\u8BA4 xiaohongshu-batch-1/2/3\u3002\u767B\u5F55\u540E\u53EF\u8BBE\u7F6E alias\uFF08\u8D26\u53F7\u540D\uFF09\u7528\u4E8E\u533A\u5206\uFF0C\u9ED8\u8BA4\u4F1A\u81EA\u52A8\u83B7\u53D6\u7528\u6237\u540D\u3002"
78
+ "\u9996\u6B21\u4F7F\u7528\u5EFA\u8BAE\uFF1AProfile \u4F7F\u7528\u4E2D\u6027\u547D\u540D profile-0/1/2\uFF1B\u767B\u5F55\u540E\u53EF\u8BBE\u7F6E alias\uFF08\u8D26\u53F7\u540D\uFF09\u7528\u4E8E\u533A\u5206\uFF0C\u9ED8\u8BA4\u4F1A\u81EA\u52A8\u83B7\u53D6\u7528\u6237\u540D\u3002"
79
79
  ]);
80
80
  const gotoXhsBtn = createEl("button", { className: "secondary", type: "button" }, ["\u53BB\u5C0F\u7EA2\u4E66\u9996\u9875"]);
81
81
  const browserStatus = createEl("div", { className: "muted" }, ["\u6D4F\u89C8\u5668\u72B6\u6001\uFF1A\u672A\u68C0\u67E5"]);
@@ -372,7 +372,7 @@ ${mergedOutput}`);
372
372
  createEl("div", { className: "muted" }, [`\u63D0\u793A\uFF1Aprofile \u4E0E fingerprint \u7684\u771F\u5B9E\u8DEF\u5F84\u5747\u5728 ${webautoRoot} \u4E0B\uFF1Balias \u53EA\u5F71\u54CD UI \u663E\u793A\uFF0C\u4E0D\u5F71\u54CD profileId\u3002`])
373
373
  ])
374
374
  );
375
- const keywordInput = createEl("input", { value: "xiaohongshu", placeholder: "\u8D26\u53F7\u540D\uFF08\u53EF\u9009\uFF09\uFF0C\u7CFB\u7EDF\u81EA\u52A8\u62FC\u63A5\u4E3A <\u8D26\u53F7\u540D>-batch-1/2/3\uFF1B\u7559\u7A7A\u9ED8\u8BA4 xiaohongshu-batch" });
375
+ const keywordInput = createEl("input", { value: "profile", placeholder: "Profile \u524D\u7F00\uFF08\u53EF\u9009\uFF09\uFF0C\u9ED8\u8BA4 profile\uFF0C\u751F\u6210\u5982 profile-0/profile-1" });
376
376
  const ensureCountInput = createEl("input", { value: "0", type: "number", min: "0" });
377
377
  const timeoutInput = createEl("input", { value: String(ctx2.settings?.timeouts?.loginTimeoutSec || 900), type: "number", min: "30" });
378
378
  const keepSession = createEl("input", { type: "checkbox" });
@@ -414,10 +414,13 @@ ${mergedOutput}`);
414
414
  if (!createdProfileId) return;
415
415
  if (typeof window.api?.cmdSpawn !== "function") return;
416
416
  const timeoutSec = Math.max(30, Math.floor(Number(timeoutInput.value || "900")));
417
+ const idleTimeout = String(window.api?.settings?.idleTimeout || "30m").trim() || "30m";
417
418
  const loginArgs = buildArgs([
418
419
  window.api.pathJoin("apps", "webauto", "entry", "profilepool.mjs"),
419
420
  "login-profile",
420
421
  createdProfileId,
422
+ "--idle-timeout",
423
+ idleTimeout,
421
424
  "--timeout-sec",
422
425
  String(timeoutSec),
423
426
  "--check-interval-sec",
@@ -1019,6 +1022,7 @@ function renderSettings(root, ctx2) {
1019
1022
  const keyword = createEl("input", { value: ctx2.settings?.defaultKeyword || "" });
1020
1023
  const loginTimeout = createEl("input", { value: String(ctx2.settings?.timeouts?.loginTimeoutSec || 900), type: "number", min: "30" });
1021
1024
  const cmdTimeout = createEl("input", { value: String(ctx2.settings?.timeouts?.cmdTimeoutSec || 0), type: "number", min: "0" });
1025
+ const idleTimeout = createEl("input", { value: ctx2.settings?.idleTimeout || "30m", placeholder: "30m" });
1022
1026
  const aiEnabled = createEl("input", { type: "checkbox", checked: ctx2.settings?.aiReply?.enabled ?? false });
1023
1027
  const aiBaseUrl = createEl("input", { value: ctx2.settings?.aiReply?.baseUrl || "http://127.0.0.1:5520", placeholder: "http://127.0.0.1:5520" });
1024
1028
  const aiApiKey = createEl("input", { value: ctx2.settings?.aiReply?.apiKey || "", type: "password", placeholder: "sk-..." });
@@ -1080,6 +1084,7 @@ function renderSettings(root, ctx2) {
1080
1084
  loginTimeoutSec: Number(loginTimeout.value || "900"),
1081
1085
  cmdTimeoutSec: Number(cmdTimeout.value || "0")
1082
1086
  },
1087
+ idleTimeout: idleTimeout.value.trim() || "30m",
1083
1088
  aiReply: {
1084
1089
  enabled: aiEnabled.checked,
1085
1090
  baseUrl: aiBaseUrl.value.trim(),
@@ -1107,7 +1112,8 @@ function renderSettings(root, ctx2) {
1107
1112
  ]),
1108
1113
  createEl("div", { className: "row" }, [
1109
1114
  labeledInput("loginTimeoutSec", loginTimeout),
1110
- labeledInput("cmdTimeoutSec", cmdTimeout)
1115
+ labeledInput("cmdTimeoutSec", cmdTimeout),
1116
+ labeledInput("idleTimeout (e.g., 30m, 1h)", idleTimeout)
1111
1117
  ]),
1112
1118
  createEl("div", { className: "row" }, [
1113
1119
  createEl("button", {}, ["\u4FDD\u5B58"])
@@ -1524,12 +1530,13 @@ function normalizeRow(row) {
1524
1530
  updatedAt: asText(row?.updatedAt)
1525
1531
  };
1526
1532
  }
1527
- async function listAccountProfiles(api) {
1533
+ async function listAccountProfiles(api, options = {}) {
1528
1534
  const script = api.pathJoin("apps", "webauto", "entry", "account.mjs");
1535
+ const platform = asText(options?.platform);
1529
1536
  const out = await api.cmdRunJson({
1530
1537
  title: "account list",
1531
1538
  cwd: "",
1532
- args: [script, "list", "--json"],
1539
+ args: [script, "list", ...platform ? ["--platform", platform] : [], "--json"],
1533
1540
  timeoutMs: 2e4
1534
1541
  });
1535
1542
  const rows = Array.isArray(out?.json?.profiles) ? out.json.profiles : [];
@@ -1537,6 +1544,14 @@ async function listAccountProfiles(api) {
1537
1544
  }
1538
1545
 
1539
1546
  // src/renderer/tabs-new/setup-wizard.mts
1547
+ function formatProfileTag(profileId) {
1548
+ const id = String(profileId || "").trim();
1549
+ const m = id.match(/^profile-(\d+)$/i);
1550
+ if (!m) return id;
1551
+ const seq = Number(m[1]);
1552
+ if (!Number.isFinite(seq)) return id;
1553
+ return `P${String(seq).padStart(3, "0")}`;
1554
+ }
1540
1555
  function renderSetupWizard(root, ctx2) {
1541
1556
  root.innerHTML = "";
1542
1557
  const autoSyncTimers = /* @__PURE__ */ new Map();
@@ -1574,7 +1589,7 @@ function renderSetupWizard(root, ctx2) {
1574
1589
  <div class="env-item" id="env-firefox" style="display:flex; align-items:center; justify-content:space-between; gap:8px;">
1575
1590
  <span style="display:flex; align-items:center; gap:8px; min-width:0;">
1576
1591
  <span class="icon" style="color: var(--text-4);">\u25CB</span>
1577
- <span class="env-label">Camoufox Runtime (python -m camoufox)</span>
1592
+ <span class="env-label">\u6D4F\u89C8\u5668\u5185\u6838\uFF08Camoufox Firefox\uFF09</span>
1578
1593
  </span>
1579
1594
  <button id="repair-runtime-btn" class="secondary" style="display:none; flex:0 0 auto;">\u4E00\u952E\u4FEE\u590D</button>
1580
1595
  </div>
@@ -1648,30 +1663,31 @@ function renderSetupWizard(root, ctx2) {
1648
1663
  let envCheckInFlight = false;
1649
1664
  let accountCheckInFlight = false;
1650
1665
  let busUnsubscribe = null;
1651
- const isEnvReady = (snapshot) => Boolean(
1652
- snapshot?.camo?.installed && snapshot?.services?.unifiedApi && snapshot?.firefox?.installed
1653
- );
1654
- const getMissing = (snapshot) => ({
1655
- core: !snapshot?.services?.unifiedApi,
1656
- runtimeService: !snapshot?.services?.camoRuntime,
1657
- camo: !snapshot?.camo?.installed,
1658
- runtime: !snapshot?.firefox?.installed,
1659
- geoip: !snapshot?.geoip?.installed
1660
- });
1666
+ const getMissing = (snapshot) => snapshot?.missing || {
1667
+ core: true,
1668
+ runtimeService: true,
1669
+ camo: true,
1670
+ runtime: true,
1671
+ geoip: true
1672
+ };
1673
+ const isEnvReady = (snapshot) => Boolean(snapshot?.allReady);
1661
1674
  async function collectEnvironment() {
1662
- const [camo, services, firefox, geoip] = await Promise.all([
1663
- ctx2.api.envCheckCamo(),
1664
- ctx2.api.envCheckServices(),
1665
- ctx2.api.envCheckFirefox(),
1666
- ctx2.api.envCheckGeoIP()
1667
- ]);
1668
- return { camo, services, firefox, geoip };
1675
+ if (typeof ctx2.api?.envCheckAll !== "function") {
1676
+ throw new Error("envCheckAll unavailable");
1677
+ }
1678
+ const snapshot = await ctx2.api.envCheckAll();
1679
+ if (snapshot && typeof snapshot === "object" && snapshot.camo && snapshot.services) {
1680
+ return snapshot;
1681
+ }
1682
+ throw new Error("invalid envCheckAll response");
1669
1683
  }
1670
1684
  function applyEnvironment(snapshot) {
1685
+ const browserReady = Boolean(snapshot.browserReady);
1686
+ const browserDetail = snapshot.firefox?.installed ? "\u5DF2\u5B89\u88C5" : snapshot.services?.camoRuntime ? "\u7531 Runtime \u670D\u52A1\u63D0\u4F9B" : "\u672A\u5B89\u88C5";
1671
1687
  updateEnvItem("env-camo", snapshot.camo?.installed, snapshot.camo?.version || (snapshot.camo?.installed ? "\u5DF2\u5B89\u88C5" : "\u672A\u5B89\u88C5"));
1672
1688
  updateEnvItem("env-unified", snapshot.services?.unifiedApi, "7701");
1673
1689
  updateEnvItem("env-browser", snapshot.services?.camoRuntime, "7704");
1674
- updateEnvItem("env-firefox", snapshot.firefox?.installed, snapshot.firefox?.path ? "\u5DF2\u5B89\u88C5" : "\u672A\u5B89\u88C5");
1690
+ updateEnvItem("env-firefox", browserReady, snapshot.firefox?.path || browserDetail);
1675
1691
  updateEnvItem("env-geoip", snapshot.geoip?.installed, snapshot.geoip?.installed ? "\u5DF2\u5B89\u88C5\uFF08\u53EF\u9009\uFF09" : "\u672A\u5B89\u88C5\uFF08\u53EF\u9009\uFF09");
1676
1692
  envReady = isEnvReady(snapshot);
1677
1693
  syncRepairButtons(snapshot);
@@ -1720,7 +1736,7 @@ function renderSetupWizard(root, ctx2) {
1720
1736
  }
1721
1737
  async function repairInstall({ browser, geoip, reinstall, uninstall }) {
1722
1738
  if (typeof ctx2.api?.envRepairDeps === "function") {
1723
- setupStatusText.textContent = reinstall ? "\u6B63\u5728\u5378\u8F7D\u5E76\u91CD\u88C5\u8D44\u6E90\uFF08Camoufox Runtime/GeoIP\uFF09..." : geoip && browser ? "\u6B63\u5728\u5B89\u88C5\u4F9D\u8D56\uFF08Camoufox Runtime/GeoIP\uFF09..." : geoip ? "\u6B63\u5728\u5B89\u88C5 GeoIP\uFF08\u53EF\u9009\uFF09..." : "\u6B63\u5728\u5B89\u88C5 Camoufox Runtime...";
1739
+ setupStatusText.textContent = reinstall ? "\u6B63\u5728\u5378\u8F7D\u5E76\u91CD\u88C5\u8D44\u6E90\uFF08\u6D4F\u89C8\u5668\u5185\u6838/GeoIP\uFF09..." : geoip && browser ? "\u6B63\u5728\u5B89\u88C5\u4F9D\u8D56\uFF08\u6D4F\u89C8\u5668\u5185\u6838/GeoIP\uFF09..." : geoip ? "\u6B63\u5728\u5B89\u88C5 GeoIP\uFF08\u53EF\u9009\uFF09..." : "\u6B63\u5728\u5B89\u88C5\u6D4F\u89C8\u5668\u5185\u6838\uFF08Camoufox\uFF09...";
1724
1740
  const res = await ctx2.api.envRepairDeps({
1725
1741
  browser: Boolean(browser),
1726
1742
  geoip: Boolean(geoip),
@@ -1732,7 +1748,7 @@ function renderSetupWizard(root, ctx2) {
1732
1748
  return { ok, detail };
1733
1749
  }
1734
1750
  if (typeof ctx2.api?.cmdRunJson === "function") {
1735
- setupStatusText.textContent = reinstall ? "\u6B63\u5728\u5378\u8F7D\u5E76\u91CD\u88C5\u8D44\u6E90\uFF08Camoufox Runtime/GeoIP\uFF09..." : geoip && browser ? "\u6B63\u5728\u5B89\u88C5\u4F9D\u8D56\uFF08Camoufox Runtime/GeoIP\uFF09..." : geoip ? "\u6B63\u5728\u5B89\u88C5 GeoIP\uFF08\u53EF\u9009\uFF09..." : "\u6B63\u5728\u5B89\u88C5 Camoufox Runtime...";
1751
+ setupStatusText.textContent = reinstall ? "\u6B63\u5728\u5378\u8F7D\u5E76\u91CD\u88C5\u8D44\u6E90\uFF08\u6D4F\u89C8\u5668\u5185\u6838/GeoIP\uFF09..." : geoip && browser ? "\u6B63\u5728\u5B89\u88C5\u4F9D\u8D56\uFF08\u6D4F\u89C8\u5668\u5185\u6838/GeoIP\uFF09..." : geoip ? "\u6B63\u5728\u5B89\u88C5 GeoIP\uFF08\u53EF\u9009\uFF09..." : "\u6B63\u5728\u5B89\u88C5\u6D4F\u89C8\u5668\u5185\u6838\uFF08Camoufox\uFF09...";
1736
1752
  const script = ctx2.api.pathJoin("apps", "webauto", "entry", "xhs-install.mjs");
1737
1753
  const args = [script];
1738
1754
  if (reinstall) args.push("--reinstall");
@@ -1810,14 +1826,14 @@ function renderSetupWizard(root, ctx2) {
1810
1826
  applyEnvironment(latest);
1811
1827
  updateCompleteStatus();
1812
1828
  if (!detail) {
1813
- if (label.includes("Camoufox") || label.includes("Runtime")) {
1814
- ok = Boolean(latest.firefox?.installed);
1815
- } else if (label.includes("Camoufox CLI") || label.includes("CLI") || label.includes("camo")) {
1829
+ if (label.includes("\u6D4F\u89C8\u5668\u5185\u6838") || label.includes("Camoufox") || label.includes("Runtime")) {
1830
+ ok = Boolean(latest.browserReady);
1831
+ } else if (label.includes("CLI") || label.includes("camo")) {
1816
1832
  ok = Boolean(latest.camo?.installed);
1817
1833
  } else if (label.includes("\u6838\u5FC3")) {
1818
1834
  ok = Boolean(latest.services?.unifiedApi && latest.services?.camoRuntime);
1819
1835
  } else {
1820
- ok = isEnvReady(latest);
1836
+ ok = Boolean(latest.allReady);
1821
1837
  }
1822
1838
  }
1823
1839
  }
@@ -1840,12 +1856,13 @@ function renderSetupWizard(root, ctx2) {
1840
1856
  applyEnvironment(snapshot);
1841
1857
  updateCompleteStatus();
1842
1858
  if (!envReady) {
1859
+ const missingFlags = getMissing(snapshot);
1843
1860
  const missing = [];
1844
- if (!snapshot?.camo?.installed) missing.push("camo-cli");
1845
- if (!snapshot?.services?.unifiedApi) missing.push("unified-api");
1846
- if (!snapshot?.firefox?.installed) missing.push("camoufox-runtime");
1861
+ if (missingFlags.camo) missing.push("camo-cli");
1862
+ if (missingFlags.core) missing.push("unified-api");
1863
+ if (missingFlags.runtime) missing.push("browser-kernel");
1847
1864
  setupStatusText.textContent = `\u5B58\u5728\u5F85\u4FEE\u590D\u9879: ${missing.join(", ")}`;
1848
- if (!snapshot?.services?.camoRuntime) {
1865
+ if (missingFlags.runtimeService) {
1849
1866
  setupStatusText.textContent += "\uFF08camo-runtime \u672A\u5C31\u7EEA\uFF0C\u5F53\u524D\u4E3A\u53EF\u9009\uFF09";
1850
1867
  }
1851
1868
  } else if (!snapshot?.geoip?.installed) {
@@ -1917,8 +1934,8 @@ function renderSetupWizard(root, ctx2) {
1917
1934
  style: "display:flex; justify-content:space-between; align-items:center; padding:8px 12px; border-bottom:1px solid var(--border);"
1918
1935
  }, [
1919
1936
  createEl("div", {}, [
1920
- createEl("div", { style: "font-weight:600; margin-bottom:2px;" }, [acc.alias || acc.name || acc.profileId]),
1921
- createEl("div", { className: "muted", style: "font-size:11px;" }, [acc.profileId])
1937
+ createEl("div", { style: "font-weight:600; margin-bottom:2px;" }, [acc.alias || acc.name || formatProfileTag(acc.profileId)]),
1938
+ createEl("div", { className: "muted", style: "font-size:11px;" }, [`${formatProfileTag(acc.profileId)} (${acc.profileId}) \xB7 ${String(acc.platform || "xiaohongshu")}`])
1922
1939
  ]),
1923
1940
  createEl("span", {
1924
1941
  className: `status-badge ${statusClass}`
@@ -1961,10 +1978,13 @@ function renderSetupWizard(root, ctx2) {
1961
1978
  await refreshAccounts();
1962
1979
  setupStatusText.textContent = `\u8D26\u53F7 ${profileId} \u5DF2\u521B\u5EFA\uFF0C\u7B49\u5F85\u767B\u5F55...`;
1963
1980
  const timeoutSec = ctx2.api.settings?.timeouts?.loginTimeoutSec || 900;
1981
+ const idleTimeout = String(ctx2.api?.settings?.idleTimeout || "30m").trim() || "30m";
1964
1982
  const loginArgs = [
1965
1983
  ctx2.api.pathJoin("apps", "webauto", "entry", "profilepool.mjs"),
1966
1984
  "login-profile",
1967
1985
  profileId,
1986
+ "--idle-timeout",
1987
+ idleTimeout,
1968
1988
  "--wait-sync",
1969
1989
  "false",
1970
1990
  "--timeout-sec",
@@ -2025,6 +2045,7 @@ function renderSetupWizard(root, ctx2) {
2025
2045
  "sync",
2026
2046
  id,
2027
2047
  "--pending-while-login",
2048
+ "--resolve-alias",
2028
2049
  "--json"
2029
2050
  ],
2030
2051
  timeoutMs: 2e4
@@ -2081,12 +2102,12 @@ function renderSetupWizard(root, ctx2) {
2081
2102
  repairCoreBtn.onclick = () => void runRepair("\u4FEE\u590D\u6838\u5FC3\u670D\u52A1", repairCoreServices);
2082
2103
  repairCore2Btn.onclick = () => void runRepair("\u4FEE\u590D\u6838\u5FC3\u670D\u52A1", repairCoreServices);
2083
2104
  repairCamoBtn.onclick = () => void runRepair("\u4FEE\u590D Camo CLI", repairCoreServices);
2084
- repairRuntimeBtn.onclick = () => void runRepair("\u4FEE\u590D Camoufox Runtime", () => repairInstall({ browser: true }));
2105
+ repairRuntimeBtn.onclick = () => void runRepair("\u4FEE\u590D\u6D4F\u89C8\u5668\u5185\u6838", () => repairInstall({ browser: true }));
2085
2106
  repairGeoipBtn.onclick = () => void runRepair("\u5B89\u88C5 GeoIP", () => repairInstall({ geoip: true }));
2086
2107
  addAccountBtn.onclick = addAccount;
2087
2108
  enterMainBtn.onclick = () => {
2088
2109
  if (typeof ctx2.setActiveTab === "function") {
2089
- ctx2.setActiveTab("config");
2110
+ ctx2.setActiveTab("tasks");
2090
2111
  }
2091
2112
  };
2092
2113
  void tickEnvironment();
@@ -2182,6 +2203,21 @@ function parseTaskRows(payload) {
2182
2203
  runHistory: parseRunHistory(row?.runHistory)
2183
2204
  })).filter((row) => row.id);
2184
2205
  }
2206
+ function inferUiScheduleEditorState(task, nowMs = Date.now()) {
2207
+ const scheduleType = normalizeScheduleType(task?.scheduleType);
2208
+ if (scheduleType === "interval") {
2209
+ return { mode: "periodic", periodicType: "interval" };
2210
+ }
2211
+ if (scheduleType === "daily" || scheduleType === "weekly") {
2212
+ return { mode: "periodic", periodicType: scheduleType };
2213
+ }
2214
+ const runAtText = String(task?.runAt || "").trim();
2215
+ const runAtMs = Date.parse(runAtText);
2216
+ if (Number.isFinite(runAtMs) && runAtMs > nowMs + 6e4) {
2217
+ return { mode: "scheduled", periodicType: "interval" };
2218
+ }
2219
+ return { mode: "immediate", periodicType: "interval" };
2220
+ }
2185
2221
  function getTasksForPlatform(platform) {
2186
2222
  const p = platform;
2187
2223
  return PLATFORM_TASKS[p] || [];
@@ -2208,7 +2244,8 @@ var DEFAULT_FORM = {
2208
2244
  collectBody: true,
2209
2245
  doLikes: false,
2210
2246
  likeKeywords: "",
2211
- scheduleType: "interval",
2247
+ scheduleMode: "immediate",
2248
+ periodicType: "interval",
2212
2249
  intervalMinutes: 30,
2213
2250
  runAt: null,
2214
2251
  maxRuns: null
@@ -2217,19 +2254,6 @@ function parseSortableTime(value) {
2217
2254
  const ts = Date.parse(String(value || ""));
2218
2255
  return Number.isFinite(ts) ? ts : 0;
2219
2256
  }
2220
- function isKeywordRequired(taskType) {
2221
- return taskType === "xhs-unified" || taskType === "weibo-search" || taskType === "1688-search";
2222
- }
2223
- function commandTypeToWeiboTaskType(commandType) {
2224
- if (commandType === "weibo-search") return "search";
2225
- if (commandType === "weibo-monitor") return "monitor";
2226
- return "timeline";
2227
- }
2228
- function fallbackTaskName(data) {
2229
- const keyword = String(data.keyword || "").trim();
2230
- const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
2231
- return keyword ? `${data.taskType}-${keyword}` : `${data.taskType}-${stamp}`;
2232
- }
2233
2257
  function renderTasksPanel(root, ctx2) {
2234
2258
  root.innerHTML = "";
2235
2259
  const pageIndicator = createEl("div", { className: "page-indicator" }, [
@@ -2284,8 +2308,9 @@ function renderTasksPanel(root, ctx2) {
2284
2308
  <input id="task-target" type="number" min="1" value="50" style="width: 80px;" />
2285
2309
  </div>
2286
2310
  <div>
2287
- <label>Profile</label>
2288
- <input id="task-profile" placeholder="xiaohongshu-batch-1" style="width: 160px;" />
2311
+ <label>Profile\uFF08\u53EF\u7559\u7A7A\u81EA\u52A8\u9009\uFF09</label>
2312
+ <input id="task-profile" placeholder="\u7559\u7A7A\u81EA\u52A8\u9009\u62E9\u8BE5\u5E73\u53F0\u6709\u6548\u8D26\u53F7" style="width: 220px;" />
2313
+ <div id="task-profile-hint" class="muted" style="font-size:11px; margin-top:2px;">\u63A8\u8350: -</div>
2289
2314
  </div>
2290
2315
  <div>
2291
2316
  <label>\u73AF\u5883</label>
@@ -2323,14 +2348,20 @@ function renderTasksPanel(root, ctx2) {
2323
2348
  <div style="font-size:12px; color:var(--text-secondary); margin-bottom:var(--gap-sm);">\u8C03\u5EA6\u8BBE\u7F6E\uFF08\u53EF\u9009\uFF09</div>
2324
2349
  <div class="row">
2325
2350
  <div>
2326
- <select id="task-schedule-type" style="width: 100px;">
2327
- <option value="interval">\u5FAA\u73AF\u95F4\u9694</option>
2328
- <option value="once">\u4E00\u6B21\u6027</option>
2351
+ <select id="task-schedule-type" style="width: 140px;">
2352
+ <option value="immediate">\u9A6C\u4E0A\u6267\u884C\uFF08\u4EC5\u4E00\u6B21\uFF09</option>
2353
+ <option value="periodic">\u5468\u671F\u4EFB\u52A1</option>
2354
+ <option value="scheduled">\u5B9A\u65F6\u4EFB\u52A1</option>
2355
+ </select>
2356
+ </div>
2357
+ <div id="task-periodic-type-wrap" style="display:none;">
2358
+ <select id="task-periodic-type" style="width: 100px;">
2359
+ <option value="interval">\u6309\u95F4\u9694</option>
2329
2360
  <option value="daily">\u6BCF\u5929</option>
2330
2361
  <option value="weekly">\u6BCF\u5468</option>
2331
2362
  </select>
2332
2363
  </div>
2333
- <div id="task-interval-wrap">
2364
+ <div id="task-interval-wrap" style="display:none;">
2334
2365
  <input id="task-interval" type="number" min="1" value="30" style="width: 70px;" />
2335
2366
  <span style="font-size:11px;color:var(--text-tertiary);">\u5206\u949F</span>
2336
2367
  </div>
@@ -2347,7 +2378,7 @@ function renderTasksPanel(root, ctx2) {
2347
2378
  <div class="btn-group" style="margin-top: var(--gap);">
2348
2379
  <button id="task-save-btn" style="flex:1;">\u4FDD\u5B58\u4EFB\u52A1</button>
2349
2380
  <button id="task-run-btn" class="primary" style="flex:1;">\u4FDD\u5B58\u5E76\u6267\u884C</button>
2350
- <button id="task-run-ephemeral-btn" class="secondary" style="flex:1;">\u4EC5\u6267\u884C(\u4E0D\u4FDD\u5B58)</button>
2381
+ <button id="task-run-ephemeral-btn" class="secondary" style="flex:1;">\u7ACB\u5373\u6267\u884C(\u4E0D\u4FDD\u5B58)</button>
2351
2382
  <button id="task-reset-btn" class="secondary" style="flex:0.6;">\u91CD\u7F6E</button>
2352
2383
  </div>
2353
2384
  `;
@@ -2377,15 +2408,17 @@ function renderTasksPanel(root, ctx2) {
2377
2408
  root.appendChild(mainGrid);
2378
2409
  const recentCard = createEl("div", { className: "bento-cell", style: "margin-top: var(--gap);" });
2379
2410
  recentCard.innerHTML = `
2380
- <div class="bento-title">\u5386\u53F2\u4EFB\u52A1</div>
2411
+ <div class="bento-title">\u5DF2\u4FDD\u5B58\u4EFB\u52A1\u5217\u8868</div>
2381
2412
  <div class="row" style="margin-bottom: var(--gap-sm);">
2382
2413
  <select id="task-history-select" style="min-width: 320px;">
2383
2414
  <option value="">\u9009\u62E9\u5386\u53F2\u4EFB\u52A1...</option>
2384
2415
  </select>
2385
2416
  <button id="task-history-edit-btn" class="secondary">\u8F7D\u5165\u7F16\u8F91</button>
2386
2417
  <button id="task-history-clone-btn" class="secondary">\u8F7D\u5165\u53E6\u5B58</button>
2418
+ <button id="task-history-run-btn">\u7ACB\u5373\u6267\u884C</button>
2387
2419
  <button id="task-history-refresh-btn" class="secondary">\u5237\u65B0</button>
2388
2420
  </div>
2421
+ <div class="muted" style="font-size:12px; margin-bottom:6px;">\u53CC\u51FB\u5217\u8868\u9879\u53EF\u76F4\u63A5\u5207\u6362\u4E3A\u5F53\u524D\u4EFB\u52A1\u3002</div>
2389
2422
  <div id="recent-tasks-list"></div>
2390
2423
  `;
2391
2424
  root.appendChild(recentCard);
@@ -2396,6 +2429,7 @@ function renderTasksPanel(root, ctx2) {
2396
2429
  const keywordInput = formCard.querySelector("#task-keyword");
2397
2430
  const targetInput = formCard.querySelector("#task-target");
2398
2431
  const profileInput = formCard.querySelector("#task-profile");
2432
+ const profileHint = formCard.querySelector("#task-profile-hint");
2399
2433
  const envSelect = formCard.querySelector("#task-env");
2400
2434
  const userIdWrap = formCard.querySelector("#task-user-id-wrap");
2401
2435
  const userIdInput = formCard.querySelector("#task-user-id");
@@ -2404,6 +2438,8 @@ function renderTasksPanel(root, ctx2) {
2404
2438
  const likesInput = formCard.querySelector("#task-likes");
2405
2439
  const likeKeywordsInput = formCard.querySelector("#task-like-keywords");
2406
2440
  const scheduleTypeSelect = formCard.querySelector("#task-schedule-type");
2441
+ const periodicTypeWrap = formCard.querySelector("#task-periodic-type-wrap");
2442
+ const periodicTypeSelect = formCard.querySelector("#task-periodic-type");
2407
2443
  const intervalInput = formCard.querySelector("#task-interval");
2408
2444
  const intervalWrap = formCard.querySelector("#task-interval-wrap");
2409
2445
  const runAtInput = formCard.querySelector("#task-runat");
@@ -2419,22 +2455,62 @@ function renderTasksPanel(root, ctx2) {
2419
2455
  const historySelect = recentCard.querySelector("#task-history-select");
2420
2456
  const historyEditBtn = recentCard.querySelector("#task-history-edit-btn");
2421
2457
  const historyCloneBtn = recentCard.querySelector("#task-history-clone-btn");
2458
+ const historyRunBtn = recentCard.querySelector("#task-history-run-btn");
2422
2459
  const historyRefreshBtn = recentCard.querySelector("#task-history-refresh-btn");
2423
2460
  const recentTasksList = recentCard.querySelector("#recent-tasks-list");
2424
2461
  const statRunning = statsCard.querySelector("#stat-running");
2425
2462
  const statToday = statsCard.querySelector("#stat-today");
2426
2463
  const statSaved = statsCard.querySelector("#stat-saved");
2427
2464
  let tasks = [];
2465
+ let accountRows = [];
2428
2466
  const activeRunIds = /* @__PURE__ */ new Set();
2429
2467
  let unsubscribeActiveRuns = null;
2430
2468
  const joinPath2 = (...parts) => {
2431
2469
  if (typeof ctx2?.api?.pathJoin === "function") return ctx2.api.pathJoin(...parts);
2432
2470
  return parts.filter(Boolean).join("/");
2433
2471
  };
2434
- const scheduleScript = joinPath2("apps", "webauto", "entry", "schedule.mjs");
2435
2472
  const quotaScript = joinPath2("apps", "webauto", "entry", "lib", "quota-status.mjs");
2436
- const xhsScript = joinPath2("apps", "webauto", "entry", "xhs-unified.mjs");
2437
- const weiboScript = joinPath2("apps", "webauto", "entry", "weibo-unified.mjs");
2473
+ function normalizePlatform2(value) {
2474
+ const raw = String(value || "").trim().toLowerCase();
2475
+ if (raw === "weibo") return "weibo";
2476
+ if (raw === "1688") return "1688";
2477
+ return "xiaohongshu";
2478
+ }
2479
+ function platformToAccountPlatform(value) {
2480
+ return value === "xiaohongshu" ? "xiaohongshu" : value;
2481
+ }
2482
+ async function refreshPlatformAccountRows(platform) {
2483
+ try {
2484
+ accountRows = await listAccountProfiles(ctx2.api, { platform: platformToAccountPlatform(platform) });
2485
+ } catch {
2486
+ accountRows = [];
2487
+ }
2488
+ }
2489
+ function getRecommendedProfile(platform) {
2490
+ const candidates = accountRows.filter((row) => row.valid).sort((a, b) => {
2491
+ const ta = Date.parse(String(a.updatedAt || "")) || 0;
2492
+ const tb = Date.parse(String(b.updatedAt || "")) || 0;
2493
+ if (tb !== ta) return tb - ta;
2494
+ return String(a.profileId || "").localeCompare(String(b.profileId || ""));
2495
+ });
2496
+ return candidates[0] || null;
2497
+ }
2498
+ function updateProfileHint(platform) {
2499
+ const recommended = getRecommendedProfile(platform);
2500
+ if (!recommended) {
2501
+ profileHint.textContent = `\u63A8\u8350: \u5F53\u524D\u5E73\u53F0(${platform})\u65E0\u6709\u6548\u8D26\u53F7\uFF0C\u8BF7\u5148\u5230\u8D26\u53F7\u9875\u767B\u5F55`;
2502
+ return;
2503
+ }
2504
+ const label = recommended.alias || recommended.name || recommended.profileId;
2505
+ profileHint.textContent = `\u63A8\u8350: ${label} (${recommended.profileId})`;
2506
+ }
2507
+ function maybeAutofillProfile(platform) {
2508
+ const current = String(profileInput.value || "").trim();
2509
+ if (current) return;
2510
+ const recommended = getRecommendedProfile(platform);
2511
+ if (!recommended) return;
2512
+ profileInput.value = recommended.profileId;
2513
+ }
2438
2514
  function getTaskById(taskId) {
2439
2515
  const id = String(taskId || "").trim();
2440
2516
  if (!id) return null;
@@ -2452,13 +2528,17 @@ function renderTasksPanel(root, ctx2) {
2452
2528
  formTitle.textContent = "\u65B0\u5EFA\u4EFB\u52A1";
2453
2529
  }
2454
2530
  function updateTaskTypeOptions(preferredType = "") {
2455
- const platform = platformSelect.value;
2531
+ const platform = normalizePlatform2(platformSelect.value);
2456
2532
  const options = getTasksForPlatform(platform);
2457
2533
  taskTypeSelect.innerHTML = options.map((item) => `<option value="${item.type}">${item.icon} ${item.label}</option>`).join("");
2458
2534
  const target = String(preferredType || "").trim();
2459
2535
  const matched = options.find((item) => item.type === target);
2460
2536
  taskTypeSelect.value = matched?.type || options[0]?.type || "";
2461
2537
  updatePlatformFields();
2538
+ void refreshPlatformAccountRows(platform).then(() => {
2539
+ updateProfileHint(platform);
2540
+ maybeAutofillProfile(platform);
2541
+ });
2462
2542
  }
2463
2543
  function updatePlatformFields() {
2464
2544
  const taskType = String(taskTypeSelect.value || "").trim();
@@ -2466,9 +2546,17 @@ function renderTasksPanel(root, ctx2) {
2466
2546
  userIdWrap.style.display = isWeiboMonitor ? "" : "none";
2467
2547
  }
2468
2548
  function updateScheduleVisibility() {
2469
- const scheduleType = String(scheduleTypeSelect.value || "interval").trim();
2470
- intervalWrap.style.display = scheduleType === "interval" ? "inline-flex" : "none";
2471
- runAtWrap.style.display = scheduleType === "once" || scheduleType === "daily" || scheduleType === "weekly" ? "inline-flex" : "none";
2549
+ const mode = String(scheduleTypeSelect.value || "immediate").trim();
2550
+ const periodicType = String(periodicTypeSelect.value || "interval").trim();
2551
+ const periodic = mode === "periodic";
2552
+ const scheduled = mode === "scheduled";
2553
+ periodicTypeWrap.style.display = periodic ? "inline-flex" : "none";
2554
+ intervalWrap.style.display = periodic && periodicType === "interval" ? "inline-flex" : "none";
2555
+ runAtWrap.style.display = scheduled || periodic && periodicType !== "interval" ? "inline-flex" : "none";
2556
+ maxRunsInput.disabled = mode === "immediate";
2557
+ if (mode === "immediate") {
2558
+ maxRunsInput.value = "";
2559
+ }
2472
2560
  }
2473
2561
  function updateLikeKeywordsState() {
2474
2562
  likeKeywordsInput.disabled = !likesInput.checked;
@@ -2476,6 +2564,9 @@ function renderTasksPanel(root, ctx2) {
2476
2564
  function collectFormData() {
2477
2565
  const maxRunsRaw = String(maxRunsInput.value || "").trim();
2478
2566
  const maxRunsNum = maxRunsRaw ? Number(maxRunsRaw) : 0;
2567
+ const scheduleMode = scheduleTypeSelect.value;
2568
+ const periodicType = periodicTypeSelect.value;
2569
+ const runAtText = String(runAtInput.value || "").trim();
2479
2570
  return {
2480
2571
  id: String(editingIdInput.value || "").trim() || void 0,
2481
2572
  name: String(nameInput.value || "").trim(),
@@ -2491,9 +2582,10 @@ function renderTasksPanel(root, ctx2) {
2491
2582
  collectBody: bodyInput.checked,
2492
2583
  doLikes: likesInput.checked,
2493
2584
  likeKeywords: String(likeKeywordsInput.value || "").trim(),
2494
- scheduleType: scheduleTypeSelect.value,
2585
+ scheduleMode,
2586
+ periodicType,
2495
2587
  intervalMinutes: Math.max(1, Number(intervalInput.value || 30) || 30),
2496
- runAt: toIsoOrNull(String(runAtInput.value || "")),
2588
+ runAt: toIsoOrNull(runAtText),
2497
2589
  maxRuns: Number.isFinite(maxRunsNum) && maxRunsNum > 0 ? Math.max(1, Math.floor(maxRunsNum)) : null
2498
2590
  };
2499
2591
  }
@@ -2513,7 +2605,9 @@ function renderTasksPanel(root, ctx2) {
2513
2605
  bodyInput.checked = task.commandArgv?.["fetch-body"] !== false;
2514
2606
  likesInput.checked = task.commandArgv?.["do-likes"] === true;
2515
2607
  likeKeywordsInput.value = String(task.commandArgv?.["like-keywords"] || "").trim();
2516
- scheduleTypeSelect.value = String(task.scheduleType || "interval");
2608
+ const uiSchedule = inferUiScheduleEditorState(task);
2609
+ scheduleTypeSelect.value = uiSchedule.mode;
2610
+ periodicTypeSelect.value = uiSchedule.periodicType;
2517
2611
  intervalInput.value = String(task.intervalMinutes || 30);
2518
2612
  runAtInput.value = toLocalDatetimeValue(task.runAt);
2519
2613
  maxRunsInput.value = task.maxRuns ? String(task.maxRuns) : "";
@@ -2536,7 +2630,8 @@ function renderTasksPanel(root, ctx2) {
2536
2630
  bodyInput.checked = DEFAULT_FORM.collectBody;
2537
2631
  likesInput.checked = DEFAULT_FORM.doLikes;
2538
2632
  likeKeywordsInput.value = DEFAULT_FORM.likeKeywords;
2539
- scheduleTypeSelect.value = DEFAULT_FORM.scheduleType;
2633
+ scheduleTypeSelect.value = DEFAULT_FORM.scheduleMode;
2634
+ periodicTypeSelect.value = DEFAULT_FORM.periodicType;
2540
2635
  intervalInput.value = String(DEFAULT_FORM.intervalMinutes);
2541
2636
  runAtInput.value = "";
2542
2637
  maxRunsInput.value = "";
@@ -2570,19 +2665,29 @@ function renderTasksPanel(root, ctx2) {
2570
2665
  }
2571
2666
  }
2572
2667
  function renderRecentTasks() {
2573
- const rows = sortedTasksByRecent().slice(0, 8);
2668
+ const rows = sortedTasksByRecent();
2574
2669
  if (rows.length === 0) {
2575
2670
  recentTasksList.innerHTML = '<div class="muted" style="font-size:12px;">\u6682\u65E0\u4EFB\u52A1</div>';
2576
2671
  return;
2577
2672
  }
2578
2673
  recentTasksList.innerHTML = rows.map((task) => `
2579
- <div class="task-row" style="display:flex;gap:var(--gap-sm);padding:var(--gap-xs)0;border-bottom:1px solid var(--border-subtle);align-items:center;">
2674
+ <div class="task-row task-item" data-id="${task.id}" style="display:flex;gap:var(--gap-sm);padding:var(--gap-xs)0;border-bottom:1px solid var(--border-subtle);align-items:center;cursor:pointer;">
2580
2675
  <span style="flex:1;font-size:12px;">${task.name || task.id}</span>
2581
2676
  <span style="font-size:11px;color:var(--text-tertiary);">${task.commandType}</span>
2582
2677
  <span style="font-size:11px;color:${task.enabled ? "var(--accent-success)" : "var(--text-muted)"};">${task.enabled ? "\u542F\u7528" : "\u7981\u7528"}</span>
2583
2678
  <button class="secondary edit-task-btn" data-id="${task.id}" style="padding:2px 6px;font-size:10px;height:auto;">\u7F16\u8F91</button>
2679
+ <button class="run-task-btn" data-id="${task.id}" style="padding:2px 6px;font-size:10px;height:auto;">\u7ACB\u5373\u6267\u884C</button>
2584
2680
  </div>
2585
2681
  `).join("");
2682
+ recentTasksList.querySelectorAll(".task-item").forEach((item) => {
2683
+ item.addEventListener("dblclick", () => {
2684
+ const taskId = item.dataset.id || "";
2685
+ const task = getTaskById(taskId);
2686
+ if (!task) return;
2687
+ historySelect.value = task.id;
2688
+ applyTaskToForm(task, "edit");
2689
+ });
2690
+ });
2586
2691
  recentTasksList.querySelectorAll(".edit-task-btn").forEach((btn) => {
2587
2692
  btn.addEventListener("click", () => {
2588
2693
  const taskId = btn.dataset.id || "";
@@ -2592,6 +2697,14 @@ function renderTasksPanel(root, ctx2) {
2592
2697
  applyTaskToForm(task, "edit");
2593
2698
  });
2594
2699
  });
2700
+ recentTasksList.querySelectorAll(".run-task-btn").forEach((btn) => {
2701
+ btn.addEventListener("click", () => {
2702
+ const taskId = btn.dataset.id || "";
2703
+ const task = getTaskById(taskId);
2704
+ if (!task) return;
2705
+ void runTaskImmediately(task);
2706
+ });
2707
+ });
2595
2708
  }
2596
2709
  function updateStats() {
2597
2710
  statSaved.textContent = String(tasks.length);
@@ -2599,21 +2712,27 @@ function renderTasksPanel(root, ctx2) {
2599
2712
  const totalRunCount = tasks.reduce((sum, row) => sum + (Number(row.runCount) || 0), 0);
2600
2713
  statToday.textContent = String(totalRunCount);
2601
2714
  }
2602
- async function runJsonScript(scriptPath, args, timeoutMs = 6e4) {
2603
- const ret = await ctx2.api.cmdRunJson({
2604
- title: `task-panel ${args.join(" ")}`.trim(),
2605
- cwd: "",
2606
- args: [scriptPath, ...args, "--json"],
2607
- timeoutMs
2608
- });
2715
+ async function invokeSchedule(input) {
2716
+ if (typeof ctx2.api?.scheduleInvoke !== "function") {
2717
+ throw new Error("scheduleInvoke unavailable");
2718
+ }
2719
+ const ret = await ctx2.api.scheduleInvoke(input);
2609
2720
  if (!ret?.ok) {
2610
- const reason = String(ret?.error || ret?.stderr || ret?.stdout || "unknown_error").trim();
2611
- throw new Error(reason || "command failed");
2721
+ const reason = String(ret?.error || "schedule command failed").trim();
2722
+ throw new Error(reason || "schedule command failed");
2612
2723
  }
2613
- return ret.json || {};
2724
+ return ret?.json ?? ret;
2614
2725
  }
2615
- async function runScheduleJson(args, timeoutMs = 6e4) {
2616
- return runJsonScript(scheduleScript, args, timeoutMs);
2726
+ async function invokeTaskRunEphemeral(input) {
2727
+ if (typeof ctx2.api?.taskRunEphemeral !== "function") {
2728
+ throw new Error("taskRunEphemeral unavailable");
2729
+ }
2730
+ const ret = await ctx2.api.taskRunEphemeral(input);
2731
+ if (!ret?.ok) {
2732
+ const reason = String(ret?.error || "run ephemeral failed").trim();
2733
+ throw new Error(reason || "run ephemeral failed");
2734
+ }
2735
+ return ret;
2617
2736
  }
2618
2737
  async function loadQuotaStatus() {
2619
2738
  try {
@@ -2642,7 +2761,7 @@ function renderTasksPanel(root, ctx2) {
2642
2761
  }
2643
2762
  async function loadTasks() {
2644
2763
  try {
2645
- const out = await runScheduleJson(["list"]);
2764
+ const out = await invokeSchedule({ action: "list" });
2646
2765
  tasks = parseTaskRows(out);
2647
2766
  renderHistorySelect();
2648
2767
  renderRecentTasks();
@@ -2653,7 +2772,6 @@ function renderTasksPanel(root, ctx2) {
2653
2772
  }
2654
2773
  function buildCommandArgv(data) {
2655
2774
  const argv = {
2656
- profile: data.profileId,
2657
2775
  keyword: data.keyword,
2658
2776
  "max-notes": data.targetCount,
2659
2777
  target: data.targetCount,
@@ -2663,36 +2781,70 @@ function renderTasksPanel(root, ctx2) {
2663
2781
  "do-likes": data.doLikes,
2664
2782
  "like-keywords": data.likeKeywords
2665
2783
  };
2784
+ const profileId = String(data.profileId || "").trim();
2785
+ if (profileId) argv.profile = profileId;
2666
2786
  if (String(data.taskType || "").startsWith("weibo-")) {
2667
- argv["task-type"] = commandTypeToWeiboTaskType(data.taskType);
2668
2787
  if (data.userId) argv["user-id"] = data.userId;
2669
2788
  }
2670
2789
  return argv;
2671
2790
  }
2672
- function validateBeforeSave(data) {
2673
- if (!data.profileId) return "\u8BF7\u8F93\u5165 Profile ID";
2674
- if (isKeywordRequired(data.taskType) && !data.keyword) return "\u8BF7\u8F93\u5165\u5173\u952E\u8BCD";
2675
- if (data.taskType === "weibo-monitor" && !data.userId) return "\u5FAE\u535A monitor \u4EFB\u52A1\u9700\u8981 user-id";
2676
- if (data.scheduleType !== "interval" && !data.runAt) return `${data.scheduleType} \u4EFB\u52A1\u9700\u8981\u6267\u884C\u65F6\u95F4`;
2677
- return null;
2678
- }
2679
- function buildSaveArgs(data) {
2680
- const args = data.id ? ["update", data.id] : ["add"];
2681
- args.push("--name", data.name || fallbackTaskName(data));
2682
- args.push("--enabled", String(data.enabled));
2683
- args.push("--command-type", data.taskType || "xhs-unified");
2684
- args.push("--schedule-type", data.scheduleType);
2685
- if (data.scheduleType === "interval") {
2686
- args.push("--interval-minutes", String(data.intervalMinutes));
2687
- } else {
2688
- args.push("--run-at", String(data.runAt || ""));
2791
+ function resolveSchedule(data) {
2792
+ if (data.scheduleMode === "immediate") {
2793
+ return {
2794
+ scheduleType: "once",
2795
+ intervalMinutes: data.intervalMinutes,
2796
+ runAt: (/* @__PURE__ */ new Date()).toISOString(),
2797
+ maxRuns: 1
2798
+ };
2689
2799
  }
2690
- args.push("--max-runs", data.maxRuns === null ? "0" : String(data.maxRuns));
2691
- args.push("--argv-json", JSON.stringify(buildCommandArgv(data)));
2692
- return args;
2800
+ if (data.scheduleMode === "scheduled") {
2801
+ return {
2802
+ scheduleType: "once",
2803
+ intervalMinutes: data.intervalMinutes,
2804
+ runAt: data.runAt,
2805
+ maxRuns: 1
2806
+ };
2807
+ }
2808
+ const periodicType = data.periodicType;
2809
+ if (periodicType === "daily" || periodicType === "weekly") {
2810
+ return {
2811
+ scheduleType: periodicType,
2812
+ intervalMinutes: data.intervalMinutes,
2813
+ runAt: data.runAt,
2814
+ maxRuns: data.maxRuns
2815
+ };
2816
+ }
2817
+ return {
2818
+ scheduleType: "interval",
2819
+ intervalMinutes: data.intervalMinutes,
2820
+ runAt: null,
2821
+ maxRuns: data.maxRuns
2822
+ };
2823
+ }
2824
+ function toSchedulePayload(data) {
2825
+ const schedule = resolveSchedule(data);
2826
+ return {
2827
+ id: data.id || "",
2828
+ name: data.name || "",
2829
+ enabled: data.enabled,
2830
+ commandType: data.taskType || "xhs-unified",
2831
+ scheduleType: schedule.scheduleType,
2832
+ intervalMinutes: schedule.intervalMinutes,
2833
+ runAt: schedule.runAt,
2834
+ maxRuns: schedule.maxRuns,
2835
+ argv: buildCommandArgv(data)
2836
+ };
2837
+ }
2838
+ function taskToRunMeta(task) {
2839
+ return {
2840
+ taskType: String(task.commandType || "xhs-unified").trim() || "xhs-unified",
2841
+ profileId: String(task.commandArgv?.profile || task.commandArgv?.profileId || "").trim(),
2842
+ keyword: String(task.commandArgv?.keyword || task.commandArgv?.k || "").trim(),
2843
+ targetCount: Math.max(1, Number(task.commandArgv?.["max-notes"] ?? task.commandArgv?.target ?? 50) || 50)
2844
+ };
2693
2845
  }
2694
2846
  async function runSavedTask(taskId, data) {
2695
- const out = await runScheduleJson(["run", taskId], 0);
2847
+ const out = await invokeSchedule({ action: "run", taskId, timeoutMs: 0 });
2696
2848
  const runId = String(
2697
2849
  out?.result?.runResult?.lastRunId || out?.result?.runResult?.runId || out?.runResult?.runId || ""
2698
2850
  ).trim();
@@ -2708,23 +2860,30 @@ function renderTasksPanel(root, ctx2) {
2708
2860
  target: data.targetCount,
2709
2861
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
2710
2862
  };
2863
+ ctx2.activeRunId = runId || null;
2711
2864
  }
2712
2865
  if (typeof ctx2.setActiveTab === "function") {
2713
2866
  ctx2.setActiveTab(data.taskType === "xhs-unified" ? "dashboard" : "scheduler");
2714
2867
  }
2715
2868
  }
2869
+ async function runTaskImmediately(task) {
2870
+ const taskId = String(task.id || "").trim();
2871
+ if (!taskId) return;
2872
+ historySelect.value = taskId;
2873
+ applyTaskToForm(task, "edit");
2874
+ await runSavedTask(taskId, taskToRunMeta(task));
2875
+ }
2716
2876
  async function saveTask(runImmediately = false) {
2717
2877
  const data = collectFormData();
2718
- const invalidReason = validateBeforeSave(data);
2719
- if (invalidReason) {
2720
- alert(invalidReason);
2878
+ if (runImmediately && data.scheduleMode === "immediate") {
2879
+ await runWithoutSave();
2721
2880
  return;
2722
2881
  }
2723
2882
  saveBtn.disabled = true;
2724
2883
  runBtn.disabled = true;
2725
2884
  runEphemeralBtn.disabled = true;
2726
2885
  try {
2727
- const out = await runScheduleJson(buildSaveArgs(data));
2886
+ const out = await invokeSchedule({ action: "save", payload: toSchedulePayload(data) });
2728
2887
  const taskId = String(out?.task?.id || data.id || "").trim();
2729
2888
  if (!taskId) {
2730
2889
  throw new Error("task id missing after save");
@@ -2734,7 +2893,13 @@ function renderTasksPanel(root, ctx2) {
2734
2893
  await loadTasks();
2735
2894
  historySelect.value = taskId;
2736
2895
  if (runImmediately) {
2737
- await runSavedTask(taskId, data);
2896
+ const resolvedProfile = String(out?.task?.commandArgv?.profile || data.profileId || "").trim();
2897
+ await runSavedTask(taskId, {
2898
+ taskType: data.taskType,
2899
+ profileId: resolvedProfile,
2900
+ keyword: data.keyword,
2901
+ targetCount: data.targetCount
2902
+ });
2738
2903
  } else {
2739
2904
  alert("\u4EFB\u52A1\u5DF2\u4FDD\u5B58");
2740
2905
  }
@@ -2746,83 +2911,13 @@ function renderTasksPanel(root, ctx2) {
2746
2911
  runEphemeralBtn.disabled = false;
2747
2912
  }
2748
2913
  }
2749
- function buildEphemeralRunSpec(data) {
2750
- if (!data.profileId) return null;
2751
- if (data.taskType === "xhs-unified") {
2752
- if (!data.keyword) return null;
2753
- return {
2754
- title: `xhs: ${data.keyword}`,
2755
- groupKey: "xhs-unified",
2756
- args: [
2757
- xhsScript,
2758
- "--profile",
2759
- data.profileId,
2760
- "--keyword",
2761
- data.keyword,
2762
- "--target",
2763
- String(data.targetCount),
2764
- "--max-notes",
2765
- String(data.targetCount),
2766
- "--env",
2767
- data.env,
2768
- "--do-comments",
2769
- String(data.collectComments),
2770
- "--fetch-body",
2771
- String(data.collectBody),
2772
- "--do-likes",
2773
- String(data.doLikes),
2774
- "--like-keywords",
2775
- data.likeKeywords
2776
- ]
2777
- };
2778
- }
2779
- if (data.taskType === "weibo-search") {
2780
- if (!data.keyword) return null;
2781
- return {
2782
- title: `weibo: ${data.keyword}`,
2783
- groupKey: "weibo-search",
2784
- args: [
2785
- weiboScript,
2786
- "search",
2787
- "--profile",
2788
- data.profileId,
2789
- "--keyword",
2790
- data.keyword,
2791
- "--target",
2792
- String(data.targetCount),
2793
- "--env",
2794
- data.env
2795
- ]
2796
- };
2797
- }
2798
- return null;
2799
- }
2800
2914
  async function runWithoutSave() {
2801
2915
  const data = collectFormData();
2802
- if (!data.profileId) {
2803
- alert("\u8BF7\u8F93\u5165 Profile ID");
2804
- return;
2805
- }
2806
- if (isKeywordRequired(data.taskType) && !data.keyword) {
2807
- alert("\u8BF7\u8F93\u5165\u5173\u952E\u8BCD");
2808
- return;
2809
- }
2810
- if (data.taskType === "weibo-monitor" && !data.userId) {
2811
- alert("\u5FAE\u535A monitor \u4EFB\u52A1\u9700\u8981 user-id");
2812
- return;
2813
- }
2814
- const spec = buildEphemeralRunSpec(data);
2815
- if (!spec) {
2816
- alert(`\u5F53\u524D\u4EFB\u52A1\u7C7B\u578B\u6682\u4E0D\u652F\u6301\u4EC5\u6267\u884C(\u4E0D\u4FDD\u5B58): ${data.taskType}`);
2817
- return;
2818
- }
2819
2916
  runEphemeralBtn.disabled = true;
2820
2917
  try {
2821
- const ret = await ctx2.api.cmdSpawn({
2822
- title: spec.title,
2823
- cwd: "",
2824
- args: spec.args,
2825
- groupKey: spec.groupKey
2918
+ const ret = await invokeTaskRunEphemeral({
2919
+ commandType: data.taskType,
2920
+ argv: buildCommandArgv(data)
2826
2921
  });
2827
2922
  const runId = String(ret?.runId || "").trim();
2828
2923
  if (runId) {
@@ -2830,17 +2925,19 @@ function renderTasksPanel(root, ctx2) {
2830
2925
  updateStats();
2831
2926
  }
2832
2927
  if (typeof ctx2.setStatus === "function") {
2833
- ctx2.setStatus(`started: ${spec.title}`);
2928
+ ctx2.setStatus(`started: ${data.taskType}`);
2834
2929
  }
2835
2930
  if (data.taskType === "xhs-unified" && ctx2 && typeof ctx2 === "object") {
2931
+ const resolvedProfile = String(ret?.profile || data.profileId || "").trim();
2836
2932
  ctx2.xhsCurrentRun = {
2837
2933
  runId: runId || null,
2838
2934
  taskId: null,
2839
- profileId: data.profileId,
2935
+ profileId: resolvedProfile,
2840
2936
  keyword: data.keyword,
2841
2937
  target: data.targetCount,
2842
2938
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
2843
2939
  };
2940
+ ctx2.activeRunId = runId || null;
2844
2941
  }
2845
2942
  if (typeof ctx2.setActiveTab === "function") {
2846
2943
  ctx2.setActiveTab(data.taskType === "xhs-unified" ? "dashboard" : "scheduler");
@@ -2873,6 +2970,7 @@ function renderTasksPanel(root, ctx2) {
2873
2970
  });
2874
2971
  taskTypeSelect.addEventListener("change", () => updatePlatformFields());
2875
2972
  scheduleTypeSelect.addEventListener("change", () => updateScheduleVisibility());
2973
+ periodicTypeSelect.addEventListener("change", () => updateScheduleVisibility());
2876
2974
  likesInput.addEventListener("change", () => updateLikeKeywordsState());
2877
2975
  saveBtn.addEventListener("click", () => {
2878
2976
  void saveTask(false);
@@ -2906,6 +3004,14 @@ function renderTasksPanel(root, ctx2) {
2906
3004
  }
2907
3005
  applyTaskToForm(task, "clone");
2908
3006
  });
3007
+ historyRunBtn.addEventListener("click", () => {
3008
+ const task = selectedHistoryTask();
3009
+ if (!task) {
3010
+ alert("\u8BF7\u5148\u9009\u62E9\u5386\u53F2\u4EFB\u52A1");
3011
+ return;
3012
+ }
3013
+ void runTaskImmediately(task);
3014
+ });
2909
3015
  gotoSchedulerBtn.addEventListener("click", () => {
2910
3016
  if (typeof ctx2.setActiveTab === "function") {
2911
3017
  ctx2.setActiveTab("scheduler");
@@ -2993,6 +3099,11 @@ function renderDashboard(root, ctx2) {
2993
3099
  <div id="recent-errors-empty" class="muted" style="font-size: 12px;">\u6682\u65E0\u9519\u8BEF</div>
2994
3100
  <ul id="recent-errors-list" style="margin: 6px 0 0 16px; padding: 0; font-size: 12px; line-height: 1.5; display:none;"></ul>
2995
3101
  </div>
3102
+ <div style="margin-top: 10px;">
3103
+ <label>\u70B9\u8D5E\u94FE\u63A5\uFF08\u6700\u591A 30 \u6761\uFF09</label>
3104
+ <div id="liked-links-empty" class="muted" style="font-size: 12px;">\u6682\u65E0\u70B9\u8D5E\u8BB0\u5F55</div>
3105
+ <ul id="liked-links-list" style="margin: 6px 0 0 16px; padding: 0; font-size: 12px; line-height: 1.5; display:none;"></ul>
3106
+ </div>
2996
3107
  `;
2997
3108
  runSummaryGrid.appendChild(runSummaryCard);
2998
3109
  root.appendChild(runSummaryGrid);
@@ -3087,6 +3198,8 @@ function renderDashboard(root, ctx2) {
3087
3198
  const errorCountText = root.querySelector("#error-count-text");
3088
3199
  const recentErrorsEmpty = root.querySelector("#recent-errors-empty");
3089
3200
  const recentErrorsList = root.querySelector("#recent-errors-list");
3201
+ const likedLinksEmpty = root.querySelector("#liked-links-empty");
3202
+ const likedLinksList = root.querySelector("#liked-links-list");
3090
3203
  const logsContainer = root.querySelector("#logs-container");
3091
3204
  const toggleLogsBtn = root.querySelector("#toggle-logs-btn");
3092
3205
  const pauseBtn = root.querySelector("#pause-btn");
@@ -3103,23 +3216,175 @@ function renderDashboard(root, ctx2) {
3103
3216
  let startTime = Date.now();
3104
3217
  let stoppedAt = null;
3105
3218
  let elapsedTimer = null;
3219
+ let statePollTimer = null;
3106
3220
  let unsubscribeState = null;
3107
3221
  let unsubscribeCmd = null;
3108
- let activeRunId = String(ctx2?.xhsCurrentRun?.runId || "").trim();
3222
+ const contextRun = ctx2?.xhsCurrentRun && typeof ctx2.xhsCurrentRun === "object" ? ctx2.xhsCurrentRun : null;
3223
+ const contextStartedAtMs = Date.parse(String(contextRun?.startedAt || ""));
3224
+ let activeRunId = String(contextRun?.runId || ctx2?.activeRunId || "").trim();
3225
+ let activeProfileId = String(contextRun?.profileId || "").trim();
3109
3226
  let activeStatus = "";
3110
3227
  let errorCountTotal = 0;
3111
3228
  const recentErrors = [];
3229
+ const likedLinks = /* @__PURE__ */ new Map();
3112
3230
  const maxLogs = 500;
3113
3231
  const maxRecentErrors = 8;
3114
- const initialTaskId = String(ctx2?.xhsCurrentRun?.taskId || ctx2?.activeTaskConfigId || "").trim();
3232
+ const maxLikedLinks = 30;
3233
+ const initialTaskId = String(contextRun?.taskId || ctx2?.activeTaskConfigId || "").trim();
3115
3234
  if (initialTaskId) {
3116
3235
  taskConfigId.textContent = initialTaskId;
3117
3236
  }
3118
3237
  const normalizeStatus = (value) => String(value || "").trim().toLowerCase();
3119
3238
  const isRunningStatus = (value) => ["running", "queued", "pending", "starting"].includes(normalizeStatus(value));
3120
3239
  const isTerminalStatus = (value) => ["completed", "done", "success", "succeeded", "failed", "error", "stopped", "canceled"].includes(normalizeStatus(value));
3240
+ const isXhsCommandTitle = (title) => {
3241
+ const normalized = String(title || "").trim().toLowerCase();
3242
+ if (!normalized) return false;
3243
+ return normalized.includes("xhs unified") || normalized.startsWith("xhs:") || normalized.startsWith("xhs unified:");
3244
+ };
3245
+ const hasRenderableValue = (value) => {
3246
+ const text = String(value ?? "").trim();
3247
+ return text.length > 0 && text !== "-";
3248
+ };
3249
+ if (contextRun) {
3250
+ if (hasRenderableValue(contextRun.keyword)) taskKeyword.textContent = String(contextRun.keyword);
3251
+ if (Number(contextRun.target) > 0) taskTarget.textContent = String(Number(contextRun.target));
3252
+ if (hasRenderableValue(contextRun.profileId)) {
3253
+ const aliases = ctx2.api?.settings?.profileAliases || {};
3254
+ const profileId = String(contextRun.profileId);
3255
+ taskAccount.textContent = aliases[profileId] || profileId;
3256
+ activeProfileId = profileId;
3257
+ }
3258
+ if (hasRenderableValue(contextRun.taskId)) taskConfigId.textContent = String(contextRun.taskId);
3259
+ const startedAtTs = Date.parse(String(contextRun.startedAt || ""));
3260
+ if (Number.isFinite(startedAtTs) && startedAtTs > 0) {
3261
+ startTime = startedAtTs;
3262
+ updateElapsed();
3263
+ }
3264
+ }
3265
+ function normalizeDetails(details) {
3266
+ if (details === void 0 || details === null) return null;
3267
+ try {
3268
+ const text = typeof details === "string" ? details : JSON.stringify(details, null, 2);
3269
+ const trimmed = String(text || "").trim();
3270
+ if (!trimmed) return null;
3271
+ return trimmed.length > 2e3 ? `${trimmed.slice(0, 2e3)}
3272
+ ...` : trimmed;
3273
+ } catch {
3274
+ return String(details || "").trim() || null;
3275
+ }
3276
+ }
3277
+ function normalizeNoteId(value) {
3278
+ const text = String(value || "").trim();
3279
+ if (!text) return null;
3280
+ if (/^[a-zA-Z0-9_-]{6,}$/.test(text)) return text;
3281
+ return null;
3282
+ }
3283
+ function normalizeLink(urlLike, noteIdLike) {
3284
+ const rawUrl = String(urlLike || "").trim();
3285
+ const noteId = normalizeNoteId(noteIdLike);
3286
+ if (rawUrl) {
3287
+ if (/^https?:\/\//i.test(rawUrl)) return { url: rawUrl, noteId };
3288
+ if (rawUrl.startsWith("/")) return { url: `https://www.xiaohongshu.com${rawUrl}`, noteId };
3289
+ if (/^[a-zA-Z0-9_-]{6,}$/.test(rawUrl)) {
3290
+ return { url: `https://www.xiaohongshu.com/explore/${rawUrl}`, noteId: noteId || rawUrl };
3291
+ }
3292
+ }
3293
+ if (noteId) {
3294
+ return { url: `https://www.xiaohongshu.com/explore/${noteId}`, noteId };
3295
+ }
3296
+ return null;
3297
+ }
3298
+ function pushLikedLink(entry) {
3299
+ const url = String(entry.url || "").trim();
3300
+ if (!url) return;
3301
+ const previous = likedLinks.get(url);
3302
+ likedLinks.set(url, {
3303
+ url,
3304
+ noteId: entry.noteId || previous?.noteId || null,
3305
+ source: entry.source || previous?.source || "comment_like",
3306
+ profileId: entry.profileId || previous?.profileId || activeProfileId || null,
3307
+ ts: entry.ts || previous?.ts || (/* @__PURE__ */ new Date()).toLocaleTimeString("zh-CN", { hour12: false }),
3308
+ count: (previous?.count || 0) + 1
3309
+ });
3310
+ const keys = Array.from(likedLinks.keys());
3311
+ if (keys.length > maxLikedLinks) {
3312
+ likedLinks.delete(keys[0]);
3313
+ }
3314
+ }
3315
+ function renderLikedLinks() {
3316
+ likedLinksList.innerHTML = "";
3317
+ const entries = Array.from(likedLinks.values());
3318
+ if (entries.length === 0) {
3319
+ likedLinksEmpty.style.display = "block";
3320
+ likedLinksList.style.display = "none";
3321
+ return;
3322
+ }
3323
+ likedLinksEmpty.style.display = "none";
3324
+ likedLinksList.style.display = "block";
3325
+ for (const item of entries) {
3326
+ const li = document.createElement("li");
3327
+ const wrap = document.createElement("div");
3328
+ wrap.style.display = "flex";
3329
+ wrap.style.alignItems = "center";
3330
+ wrap.style.gap = "6px";
3331
+ wrap.style.flexWrap = "wrap";
3332
+ const label = document.createElement("span");
3333
+ label.textContent = `[${item.ts}]`;
3334
+ wrap.appendChild(label);
3335
+ const link = document.createElement("a");
3336
+ link.href = "#";
3337
+ link.textContent = item.noteId ? `note:${item.noteId}` : "\u6253\u5F00\u94FE\u63A5";
3338
+ link.onclick = (evt) => {
3339
+ evt.preventDefault();
3340
+ void openLikedLink(item.url, item.profileId || activeProfileId || null);
3341
+ };
3342
+ wrap.appendChild(link);
3343
+ const hint = document.createElement("span");
3344
+ hint.style.color = "var(--text-4)";
3345
+ hint.textContent = `(${item.source}${item.count > 1 ? ` x${item.count}` : ""})`;
3346
+ wrap.appendChild(hint);
3347
+ li.appendChild(wrap);
3348
+ likedLinksList.appendChild(li);
3349
+ }
3350
+ }
3351
+ async function openLikedLink(url, profileId) {
3352
+ const targetUrl = String(url || "").trim();
3353
+ if (!targetUrl) return;
3354
+ const pid = String(profileId || "").trim();
3355
+ try {
3356
+ if (pid && typeof ctx2.api?.cmdRunJson === "function") {
3357
+ const ret = await ctx2.api.cmdRunJson({
3358
+ title: `goto ${pid}`,
3359
+ cwd: "",
3360
+ args: [
3361
+ ctx2.api.pathJoin("apps", "webauto", "entry", "profilepool.mjs"),
3362
+ "goto-profile",
3363
+ pid,
3364
+ "--url",
3365
+ targetUrl,
3366
+ "--json"
3367
+ ],
3368
+ timeoutMs: 3e4
3369
+ });
3370
+ if (ret?.ok) {
3371
+ addLog(`\u5DF2\u5728 ${pid} \u6253\u5F00\u70B9\u8D5E\u94FE\u63A5`, "info");
3372
+ return;
3373
+ }
3374
+ }
3375
+ if (typeof ctx2.api?.osOpenPath === "function") {
3376
+ await ctx2.api.osOpenPath(targetUrl);
3377
+ addLog("\u5DF2\u901A\u8FC7\u7CFB\u7EDF\u6253\u5F00\u70B9\u8D5E\u94FE\u63A5", "warn");
3378
+ }
3379
+ } catch (err) {
3380
+ pushRecentError("\u70B9\u8D5E\u94FE\u63A5\u6253\u5F00\u5931\u8D25", "like_link", err?.message || String(err));
3381
+ }
3382
+ }
3121
3383
  function renderRunSummary() {
3122
3384
  runIdText.textContent = activeRunId || "-";
3385
+ if (ctx2 && typeof ctx2 === "object") {
3386
+ ctx2.activeRunId = activeRunId || null;
3387
+ }
3123
3388
  errorCountText.textContent = String(errorCountTotal);
3124
3389
  recentErrorsList.innerHTML = "";
3125
3390
  if (recentErrors.length === 0) {
@@ -3131,18 +3396,35 @@ function renderDashboard(root, ctx2) {
3131
3396
  recentErrorsList.style.display = "block";
3132
3397
  recentErrors.forEach((item) => {
3133
3398
  const li = document.createElement("li");
3134
- li.textContent = `[${item.ts}] ${item.source}: ${item.message}`;
3399
+ const line = document.createElement("div");
3400
+ line.textContent = `[${item.ts}] ${item.source}: ${item.message}`;
3401
+ li.appendChild(line);
3402
+ if (item.details) {
3403
+ const details = document.createElement("details");
3404
+ const summary = document.createElement("summary");
3405
+ summary.textContent = "\u8BE6\u60C5";
3406
+ const pre = document.createElement("pre");
3407
+ pre.style.margin = "4px 0 0 0";
3408
+ pre.style.whiteSpace = "pre-wrap";
3409
+ pre.style.wordBreak = "break-word";
3410
+ pre.textContent = item.details;
3411
+ details.appendChild(summary);
3412
+ details.appendChild(pre);
3413
+ li.appendChild(details);
3414
+ }
3135
3415
  recentErrorsList.appendChild(li);
3136
3416
  });
3417
+ renderLikedLinks();
3137
3418
  }
3138
- function pushRecentError(message, source = "runtime") {
3419
+ function pushRecentError(message, source = "runtime", details = null) {
3139
3420
  const msg = String(message || "").trim();
3140
3421
  if (!msg) return;
3141
3422
  errorCountTotal += 1;
3142
3423
  recentErrors.push({
3143
3424
  ts: (/* @__PURE__ */ new Date()).toLocaleTimeString("zh-CN", { hour12: false }),
3144
3425
  source: String(source || "runtime").trim() || "runtime",
3145
- message: msg
3426
+ message: msg,
3427
+ details: normalizeDetails(details)
3146
3428
  });
3147
3429
  while (recentErrors.length > maxRecentErrors) recentErrors.shift();
3148
3430
  renderRunSummary();
@@ -3164,6 +3446,19 @@ function renderDashboard(root, ctx2) {
3164
3446
  clearInterval(elapsedTimer);
3165
3447
  elapsedTimer = null;
3166
3448
  }
3449
+ function startStatePoll() {
3450
+ if (statePollTimer) return;
3451
+ if (typeof ctx2.api?.stateGetTasks !== "function") return;
3452
+ statePollTimer = setInterval(() => {
3453
+ if (paused) return;
3454
+ void fetchCurrentState();
3455
+ }, 5e3);
3456
+ }
3457
+ function stopStatePoll() {
3458
+ if (!statePollTimer) return;
3459
+ clearInterval(statePollTimer);
3460
+ statePollTimer = null;
3461
+ }
3167
3462
  function addLog(line, type = "info") {
3168
3463
  const ts = (/* @__PURE__ */ new Date()).toLocaleTimeString("zh-CN", { hour12: false });
3169
3464
  const logLine = createEl("div", { className: "log-line" });
@@ -3176,8 +3471,39 @@ function renderDashboard(root, ctx2) {
3176
3471
  logsContainer.scrollTop = logsContainer.scrollHeight;
3177
3472
  }
3178
3473
  }
3474
+ function resetDashboardForNewRun(reason, startedAtMs) {
3475
+ commentsCount = 0;
3476
+ likesCount = 0;
3477
+ likesSkippedCount = 0;
3478
+ likesAlreadyCount = 0;
3479
+ likesDedupCount = 0;
3480
+ errorCountTotal = 0;
3481
+ recentErrors.length = 0;
3482
+ likedLinks.clear();
3483
+ logsContainer.innerHTML = "";
3484
+ statCollected.textContent = "0";
3485
+ statSuccess.textContent = "0";
3486
+ statFailed.textContent = "0";
3487
+ statRemaining.textContent = "0";
3488
+ statComments.textContent = "0\u6761";
3489
+ statLikes.textContent = "0\u6B21 (\u8DF3\u8FC7:0, \u5DF2\u8D5E:0, \u53BB\u91CD:0)";
3490
+ progressPercent.textContent = "0%";
3491
+ progressBar.style.width = "0%";
3492
+ currentAction.textContent = reason || "-";
3493
+ currentPhase.textContent = "\u8FD0\u884C\u4E2D";
3494
+ startTime = Number.isFinite(Number(startedAtMs)) && Number(startedAtMs) > 0 ? Number(startedAtMs) : Date.now();
3495
+ stoppedAt = null;
3496
+ updateElapsed();
3497
+ startElapsedTimer();
3498
+ renderRunSummary();
3499
+ }
3179
3500
  function updateFromTaskState(state) {
3180
3501
  if (!state) return;
3502
+ const incomingRunId = String(state.runId || "").trim();
3503
+ if (incomingRunId && activeRunId && incomingRunId !== activeRunId && (isTerminalStatus(activeStatus) || !isRunningStatus(activeStatus)) && isRunningStatus(state.status)) {
3504
+ activeRunId = incomingRunId;
3505
+ resetDashboardForNewRun("\u5207\u6362\u5230\u65B0\u4EFB\u52A1");
3506
+ }
3181
3507
  const progressObj = state.progress && typeof state.progress === "object" ? state.progress : null;
3182
3508
  const processedRaw = progressObj?.processed ?? progressObj?.current ?? state.progress ?? state.collected ?? state.current ?? 0;
3183
3509
  const totalRaw = progressObj?.total ?? state.total ?? state.target ?? state.maxNotes ?? 0;
@@ -3230,6 +3556,7 @@ function renderDashboard(root, ctx2) {
3230
3556
  if (state.profileId) {
3231
3557
  const aliases = ctx2.api?.settings?.profileAliases || {};
3232
3558
  taskAccount.textContent = aliases[state.profileId] || state.profileId;
3559
+ activeProfileId = String(state.profileId || "").trim();
3233
3560
  }
3234
3561
  const taskId = String(state.taskId || state.scheduleTaskId || state.configTaskId || "").trim();
3235
3562
  if (taskId) {
@@ -3271,26 +3598,32 @@ function renderDashboard(root, ctx2) {
3271
3598
  }
3272
3599
  }
3273
3600
  if (state.error) {
3274
- pushRecentError(String(state.error), "state");
3601
+ pushRecentError(String(state.error), "state", state);
3275
3602
  }
3276
3603
  }
3277
3604
  function pickTaskFromList(tasks) {
3278
3605
  const target = activeRunId;
3279
- const running = tasks.find((item) => isRunningStatus(item?.status));
3280
3606
  const sorted = [...tasks].sort((a, b) => {
3281
3607
  const aTs = Number(a?.updatedAt ?? a?.completedAt ?? a?.startedAt ?? 0) || 0;
3282
3608
  const bTs = Number(b?.updatedAt ?? b?.completedAt ?? b?.startedAt ?? 0) || 0;
3283
3609
  return bTs - aTs;
3284
3610
  });
3611
+ const running = sorted.find((item) => isRunningStatus(item?.status)) || null;
3285
3612
  const latest = sorted[0] || null;
3613
+ const launchingFresh = Number.isFinite(contextStartedAtMs) && contextStartedAtMs > 0 && Date.now() - contextStartedAtMs < 12e4;
3286
3614
  if (target) {
3287
3615
  const matched = tasks.find((item) => String(item?.runId || "").trim() === target);
3288
3616
  if (matched) {
3289
- if (isRunningStatus(matched?.status)) return matched;
3290
- if (running) return running;
3291
- if (latest && String(latest?.runId || "").trim() !== target) return latest;
3292
3617
  return matched;
3293
3618
  }
3619
+ if (launchingFresh) {
3620
+ return null;
3621
+ }
3622
+ if (running) return running;
3623
+ return null;
3624
+ }
3625
+ if (launchingFresh) {
3626
+ return running || null;
3294
3627
  }
3295
3628
  return running || latest || null;
3296
3629
  }
@@ -3301,29 +3634,14 @@ function renderDashboard(root, ctx2) {
3301
3634
  currentPhase.textContent = "\u8FD0\u884C\u4E2D";
3302
3635
  currentAction.textContent = "\u542F\u52A8 autoscript";
3303
3636
  activeStatus = "running";
3304
- statCollected.textContent = "0";
3305
- statSuccess.textContent = "0";
3306
- statFailed.textContent = "0";
3307
- statRemaining.textContent = "0";
3308
- progressPercent.textContent = "0%";
3309
- progressBar.style.width = "0%";
3310
- commentsCount = 0;
3311
- likesCount = 0;
3312
- likesSkippedCount = 0;
3313
- likesAlreadyCount = 0;
3314
- likesDedupCount = 0;
3315
- statComments.textContent = `0\u6761`;
3316
- statLikes.textContent = `0\u6B21 (\u8DF3\u8FC7:0, \u5DF2\u8D5E:0, \u53BB\u91CD:0)`;
3317
3637
  const ts = Date.parse(String(payload.ts || "")) || Date.now();
3318
- startTime = ts;
3319
- stoppedAt = null;
3320
- updateElapsed();
3321
- startElapsedTimer();
3322
3638
  if (payload.runId) {
3323
3639
  activeRunId = String(payload.runId || "").trim() || activeRunId;
3324
3640
  }
3641
+ resetDashboardForNewRun("\u65B0\u4EFB\u52A1\u542F\u52A8", ts);
3325
3642
  if (payload.keyword) taskKeyword.textContent = String(payload.keyword);
3326
3643
  if (payload.maxNotes) taskTarget.textContent = String(payload.maxNotes);
3644
+ if (payload.profileId) activeProfileId = String(payload.profileId || "").trim();
3327
3645
  if (payload.taskId) {
3328
3646
  const taskId = String(payload.taskId || "").trim();
3329
3647
  if (taskId) {
@@ -3372,6 +3690,23 @@ function renderDashboard(root, ctx2) {
3372
3690
  likesAlreadyCount = Math.max(0, likesAlreadyCount + already);
3373
3691
  likesDedupCount = Math.max(0, likesDedupCount + dedup);
3374
3692
  statLikes.textContent = `${likesCount}\u6B21 (\u8DF3\u8FC7:${likesSkippedCount}, \u5DF2\u8D5E:${likesAlreadyCount}, \u53BB\u91CD:${likesDedupCount})`;
3693
+ const candidates = [];
3694
+ const direct = normalizeLink(opResult?.noteUrl || opResult?.url || opResult?.href || opResult?.link, opResult?.noteId);
3695
+ if (direct) candidates.push({ ...direct, source: "comment_like" });
3696
+ const likedComments = Array.isArray(opResult?.likedComments) ? opResult.likedComments : [];
3697
+ for (const row of likedComments) {
3698
+ const item = normalizeLink(row?.noteUrl || row?.url || row?.href || row?.link, row?.noteId || opResult?.noteId);
3699
+ if (!item) continue;
3700
+ candidates.push({ ...item, source: "liked_comment" });
3701
+ }
3702
+ for (const item of candidates) {
3703
+ pushLikedLink({
3704
+ ...item,
3705
+ profileId: activeProfileId || null,
3706
+ ts: (/* @__PURE__ */ new Date()).toLocaleTimeString("zh-CN", { hour12: false })
3707
+ });
3708
+ }
3709
+ renderRunSummary();
3375
3710
  }
3376
3711
  return;
3377
3712
  }
@@ -3380,7 +3715,7 @@ function renderDashboard(root, ctx2) {
3380
3715
  statFailed.textContent = String(failed + 1);
3381
3716
  const opId = String(payload?.operationId || "").trim();
3382
3717
  const err = String(payload?.error || payload?.message || payload?.code || event).trim();
3383
- pushRecentError(opId ? `${opId}: ${err}` : err, event);
3718
+ pushRecentError(opId ? `${opId}: ${err}` : err, event, payload);
3384
3719
  return;
3385
3720
  }
3386
3721
  if (event === "xhs.unified.merged") {
@@ -3402,7 +3737,7 @@ function renderDashboard(root, ctx2) {
3402
3737
  currentPhase.textContent = reason && reason !== "script_failure" ? "\u5DF2\u7ED3\u675F" : "\u5931\u8D25";
3403
3738
  currentAction.textContent = reason || "stop";
3404
3739
  if (reason && !successReasons.has(reason)) {
3405
- pushRecentError(`stop reason=${reason}`, event);
3740
+ pushRecentError(`stop reason=${reason}`, event, payload);
3406
3741
  }
3407
3742
  renderRunSummary();
3408
3743
  }
@@ -3460,6 +3795,7 @@ function renderDashboard(root, ctx2) {
3460
3795
  if (profile?.profileId) {
3461
3796
  const aliases = ctx2.api?.settings?.profileAliases || {};
3462
3797
  taskAccount.textContent = aliases[profile.profileId] || profile.profileId;
3798
+ activeProfileId = String(profile.profileId || "").trim();
3463
3799
  }
3464
3800
  const runId = String(profile?.runId || summary?.runId || "").trim();
3465
3801
  if (runId) {
@@ -3535,7 +3871,7 @@ function renderDashboard(root, ctx2) {
3535
3871
  if (payload.action) addLog(String(payload.action), "info");
3536
3872
  if (payload.error) {
3537
3873
  addLog(String(payload.error), "error");
3538
- pushRecentError(String(payload.error), "state");
3874
+ pushRecentError(String(payload.error), "state", payload);
3539
3875
  }
3540
3876
  }
3541
3877
  });
@@ -3552,10 +3888,12 @@ function renderDashboard(root, ctx2) {
3552
3888
  unsubscribeCmd = ctx2.api.onCmdEvent((evt) => {
3553
3889
  if (paused) return;
3554
3890
  const runId = String(evt?.runId || "").trim();
3555
- if ((isTerminalStatus(activeStatus) || !activeRunId) && evt?.type === "started" && String(evt?.title || "").includes("xhs unified")) {
3891
+ const preferredRunId = String(ctx2?.activeRunId || "").trim();
3892
+ const shouldAdoptStartedRun = evt?.type === "started" && runId && isXhsCommandTitle(evt?.title) && (!activeRunId || isTerminalStatus(activeStatus) || preferredRunId && preferredRunId === runId);
3893
+ if (shouldAdoptStartedRun) {
3556
3894
  activeRunId = runId;
3557
3895
  activeStatus = "running";
3558
- stoppedAt = null;
3896
+ resetDashboardForNewRun("\u8FDB\u7A0B\u542F\u52A8");
3559
3897
  renderRunSummary();
3560
3898
  }
3561
3899
  if (activeRunId && runId && runId !== activeRunId) return;
@@ -3564,7 +3902,7 @@ function renderDashboard(root, ctx2) {
3564
3902
  parseLineEvent(String(evt.line || "").trim());
3565
3903
  } else if (evt.type === "stderr") {
3566
3904
  addLog(evt.line, "error");
3567
- pushRecentError(String(evt.line || ""), "stderr");
3905
+ pushRecentError(String(evt.line || ""), "stderr", evt);
3568
3906
  const failed = Number(statFailed.textContent || "0") || 0;
3569
3907
  statFailed.textContent = String(failed + 1);
3570
3908
  } else if (evt.type === "exit") {
@@ -3579,7 +3917,7 @@ function renderDashboard(root, ctx2) {
3579
3917
  stopElapsedTimer();
3580
3918
  }
3581
3919
  if (Number(evt.exitCode || 0) !== 0) {
3582
- pushRecentError(`\u8FDB\u7A0B\u9000\u51FA code=${evt.exitCode ?? "null"}`, "exit");
3920
+ pushRecentError(`\u8FDB\u7A0B\u9000\u51FA code=${evt.exitCode ?? "null"}`, "exit", evt);
3583
3921
  }
3584
3922
  renderRunSummary();
3585
3923
  }
@@ -3590,13 +3928,17 @@ function renderDashboard(root, ctx2) {
3590
3928
  try {
3591
3929
  const config = await ctx2.api.configLoadLast();
3592
3930
  if (config) {
3593
- taskKeyword.textContent = config.keyword || "-";
3594
- taskTarget.textContent = String(config.target || 50);
3595
- if (config.lastProfileId) {
3931
+ if (!hasRenderableValue(contextRun?.keyword)) {
3932
+ taskKeyword.textContent = config.keyword || "-";
3933
+ }
3934
+ if (!(Number(contextRun?.target) > 0)) {
3935
+ taskTarget.textContent = String(config.target || 50);
3936
+ }
3937
+ if (!hasRenderableValue(contextRun?.profileId) && config.lastProfileId) {
3596
3938
  const aliases = ctx2.api?.settings?.profileAliases || {};
3597
3939
  taskAccount.textContent = aliases[config.lastProfileId] || config.lastProfileId;
3598
3940
  }
3599
- const taskId = String(config.taskId || ctx2?.activeTaskConfigId || "").trim();
3941
+ const taskId = String(contextRun?.taskId || config.taskId || ctx2?.activeTaskConfigId || "").trim();
3600
3942
  if (taskId) {
3601
3943
  taskConfigId.textContent = taskId;
3602
3944
  }
@@ -3660,23 +4002,25 @@ function renderDashboard(root, ctx2) {
3660
4002
  }
3661
4003
  setTimeout(() => {
3662
4004
  if (typeof ctx2.setActiveTab === "function") {
3663
- ctx2.setActiveTab("config");
4005
+ ctx2.setActiveTab("tasks");
3664
4006
  }
3665
4007
  }, 1500);
3666
4008
  }
3667
4009
  };
3668
4010
  backConfigBtn.onclick = () => {
3669
4011
  if (typeof ctx2.setActiveTab === "function") {
3670
- ctx2.setActiveTab("config");
4012
+ ctx2.setActiveTab("tasks");
3671
4013
  }
3672
4014
  };
3673
4015
  renderRunSummary();
3674
4016
  loadTaskInfo();
3675
4017
  subscribeToUpdates();
3676
4018
  fetchCurrentState();
4019
+ startStatePoll();
3677
4020
  startElapsedTimer();
3678
4021
  return () => {
3679
4022
  stopElapsedTimer();
4023
+ stopStatePoll();
3680
4024
  if (unsubscribeState) unsubscribeState();
3681
4025
  if (unsubscribeCmd) unsubscribeCmd();
3682
4026
  if (unsubscribeBus) unsubscribeBus();
@@ -3724,6 +4068,14 @@ function toTimestamp(value) {
3724
4068
  if (!Number.isFinite(parsed)) return null;
3725
4069
  return parsed;
3726
4070
  }
4071
+ function formatProfileTag2(profileId) {
4072
+ const id = String(profileId || "").trim();
4073
+ const m = id.match(/^profile-(\d+)$/i);
4074
+ if (!m) return id;
4075
+ const seq = Number(m[1]);
4076
+ if (!Number.isFinite(seq)) return id;
4077
+ return `P${String(seq).padStart(3, "0")}`;
4078
+ }
3727
4079
  function renderAccountManager(root, ctx2) {
3728
4080
  root.innerHTML = "";
3729
4081
  const autoSyncTimers = /* @__PURE__ */ new Map();
@@ -3746,7 +4098,7 @@ function renderAccountManager(root, ctx2) {
3746
4098
  </div>
3747
4099
  <div class="env-item" id="env-firefox">
3748
4100
  <span class="icon" style="color: var(--text-4);">\u25CB</span>
3749
- <span>Camoufox Runtime (python -m camoufox)</span>
4101
+ <span>\u6D4F\u89C8\u5668\u5185\u6838\uFF08Camoufox Firefox\uFF09</span>
3750
4102
  </div>
3751
4103
  </div>
3752
4104
  <div class="btn-group" style="margin-top: var(--gap);">
@@ -3789,15 +4141,18 @@ function renderAccountManager(root, ctx2) {
3789
4141
  let busUnsubscribe = null;
3790
4142
  async function checkEnvironment() {
3791
4143
  try {
3792
- const [camo, services, firefox] = await Promise.all([
3793
- ctx2.api.envCheckCamo(),
3794
- ctx2.api.envCheckServices(),
3795
- ctx2.api.envCheckFirefox()
3796
- ]);
3797
- updateEnvItem("env-camo", camo.installed);
3798
- updateEnvItem("env-unified", services.unifiedApi);
3799
- updateEnvItem("env-browser", services.camoRuntime);
3800
- updateEnvItem("env-firefox", firefox.installed);
4144
+ if (typeof ctx2.api?.envCheckAll !== "function") {
4145
+ throw new Error("envCheckAll unavailable");
4146
+ }
4147
+ const unified = await ctx2.api.envCheckAll();
4148
+ if (!unified || typeof unified !== "object") {
4149
+ throw new Error("invalid envCheckAll response");
4150
+ }
4151
+ const browserReady = Boolean(unified.browserReady);
4152
+ updateEnvItem("env-camo", Boolean(unified.camo?.installed));
4153
+ updateEnvItem("env-unified", Boolean(unified.services?.unifiedApi));
4154
+ updateEnvItem("env-browser", Boolean(unified.services?.camoRuntime));
4155
+ updateEnvItem("env-firefox", browserReady);
3801
4156
  } catch (err) {
3802
4157
  console.error("Environment check failed:", err);
3803
4158
  }
@@ -3811,6 +4166,18 @@ function renderAccountManager(root, ctx2) {
3811
4166
  envCheckInFlight = false;
3812
4167
  }
3813
4168
  }
4169
+ async function cleanupEnvironment() {
4170
+ try {
4171
+ console.log("[account-manager] Starting environment cleanup...");
4172
+ const result = await ctx2.api.envCleanup();
4173
+ console.log("[account-manager] Cleanup result:", result);
4174
+ alert("\u73AF\u5883\u6E05\u7406\u5B8C\u6210\uFF01\\n" + JSON.stringify(result, null, 2));
4175
+ await checkEnvironment();
4176
+ } catch (err) {
4177
+ console.error("Environment cleanup failed:", err);
4178
+ alert("\u73AF\u5883\u6E05\u7406\u5931\u8D25\uFF1A" + err.message);
4179
+ }
4180
+ }
3814
4181
  function updateEnvItem(id, ok) {
3815
4182
  const el = root.querySelector(`#${id}`);
3816
4183
  if (!el) return;
@@ -3826,7 +4193,11 @@ function renderAccountManager(root, ctx2) {
3826
4193
  platform: normalizePlatform(row.platform),
3827
4194
  statusView: row.valid ? "valid" : row.status === "pending" ? "pending" : "expired",
3828
4195
  lastCheckAt: toTimestamp(row.updatedAt)
3829
- }));
4196
+ })).sort((a, b) => {
4197
+ const p = String(a.profileId || "").localeCompare(String(b.profileId || ""));
4198
+ if (p !== 0) return p;
4199
+ return String(a.platform || "").localeCompare(String(b.platform || ""));
4200
+ });
3830
4201
  renderAccountList();
3831
4202
  } catch (err) {
3832
4203
  console.error("Failed to load accounts:", err);
@@ -3860,11 +4231,11 @@ function renderAccountManager(root, ctx2) {
3860
4231
  const nameDiv = createEl("div", { style: "min-width: 0; flex: 1;" }, [
3861
4232
  createEl("div", { className: "account-name", style: "display: flex; gap: 6px; align-items: center;" }, [
3862
4233
  createEl("span", { style: "font-size: 13px;" }, [platform.icon]),
3863
- createEl("span", {}, [acc.alias || acc.name || acc.profileId]),
4234
+ createEl("span", {}, [acc.alias || acc.name || formatProfileTag2(acc.profileId)]),
3864
4235
  createEl("span", { style: "font-size: 11px; color: var(--text-3);" }, [platform.label])
3865
4236
  ]),
3866
4237
  createEl("div", { className: "account-alias", style: "font-size: 11px; color: var(--text-3);" }, [
3867
- `profile: ${acc.profileId} \xB7 \u4E0A\u6B21\u68C0\u67E5: ${formatTs(acc.lastCheckAt)}`
4238
+ `profile: ${formatProfileTag2(acc.profileId)} (${acc.profileId}) \xB7 \u4E0A\u6B21\u68C0\u67E5: ${formatTs(acc.lastCheckAt)}`
3868
4239
  ])
3869
4240
  ]);
3870
4241
  const statusBadge = createEl("span", {
@@ -3992,6 +4363,7 @@ Profile ID: ${acc.profileId}
3992
4363
  async function openAccountLogin(account, options = {}) {
3993
4364
  if (!String(account.profileId || "").trim()) return false;
3994
4365
  const platform = getPlatformInfo(account.platform);
4366
+ const idleTimeout = String(ctx2.api?.settings?.idleTimeout || "30m").trim() || "30m";
3995
4367
  const timeoutSec = Math.max(30, Number(ctx2.api?.settings?.timeouts?.loginTimeoutSec || 900));
3996
4368
  account.status = "pending";
3997
4369
  account.statusView = "pending";
@@ -4007,6 +4379,8 @@ Profile ID: ${acc.profileId}
4007
4379
  account.profileId,
4008
4380
  "--url",
4009
4381
  platform.loginUrl,
4382
+ "--idle-timeout",
4383
+ idleTimeout,
4010
4384
  "--wait-sync",
4011
4385
  "false",
4012
4386
  "--timeout-sec",
@@ -4052,6 +4426,7 @@ Profile ID: ${acc.profileId}
4052
4426
  await ctx2.refreshSettings();
4053
4427
  }
4054
4428
  }
4429
+ const idleTimeout = String(ctx2.api?.settings?.idleTimeout || "30m").trim() || "30m";
4055
4430
  const timeoutSec = ctx2.api.settings?.timeouts?.loginTimeoutSec || 900;
4056
4431
  await ctx2.api.cmdSpawn({
4057
4432
  title: `\u767B\u5F55 ${alias || profileId}`,
@@ -4060,6 +4435,8 @@ Profile ID: ${acc.profileId}
4060
4435
  ctx2.api.pathJoin("apps", "webauto", "entry", "profilepool.mjs"),
4061
4436
  "login-profile",
4062
4437
  profileId,
4438
+ "--idle-timeout",
4439
+ idleTimeout,
4063
4440
  "--wait-sync",
4064
4441
  "false",
4065
4442
  "--timeout-sec",
@@ -4186,11 +4563,6 @@ Profile ID: ${acc.profileId}
4186
4563
  }
4187
4564
 
4188
4565
  // src/renderer/tabs-new/scheduler.mts
4189
- function commandTypeToWeiboTaskType2(commandType) {
4190
- if (commandType === "weibo-search") return "search";
4191
- if (commandType === "weibo-monitor") return "monitor";
4192
- return "timeline";
4193
- }
4194
4566
  function renderSchedulerPanel(root, ctx2) {
4195
4567
  root.innerHTML = "";
4196
4568
  const pageIndicator = createEl("div", { className: "page-indicator" }, [
@@ -4259,18 +4631,25 @@ function renderSchedulerPanel(root, ctx2) {
4259
4631
  <div>
4260
4632
  <label>\u8C03\u5EA6\u7C7B\u578B</label>
4261
4633
  <select id="scheduler-type" style="width: 140px;">
4262
- <option value="interval">\u5FAA\u73AF\u95F4\u9694</option>
4263
- <option value="once">\u4E00\u6B21\u6027</option>
4634
+ <option value="immediate">\u9A6C\u4E0A\u6267\u884C\uFF08\u4EC5\u4E00\u6B21\uFF09</option>
4635
+ <option value="periodic">\u5468\u671F\u4EFB\u52A1</option>
4636
+ <option value="scheduled">\u5B9A\u65F6\u4EFB\u52A1</option>
4637
+ </select>
4638
+ </div>
4639
+ <div id="scheduler-periodic-type-wrap" style="display:none;">
4640
+ <label>\u5468\u671F\u7C7B\u578B</label>
4641
+ <select id="scheduler-periodic-type" style="width: 120px;">
4642
+ <option value="interval">\u6309\u95F4\u9694</option>
4264
4643
  <option value="daily">\u6BCF\u5929</option>
4265
4644
  <option value="weekly">\u6BCF\u5468</option>
4266
4645
  </select>
4267
4646
  </div>
4268
- <div id="scheduler-interval-wrap">
4647
+ <div id="scheduler-interval-wrap" style="display:none;">
4269
4648
  <label>\u95F4\u9694\u5206\u949F</label>
4270
4649
  <input id="scheduler-interval" type="number" min="1" value="30" style="width: 120px;" />
4271
4650
  </div>
4272
4651
  <div id="scheduler-runat-wrap" style="display:none;">
4273
- <label>\u951A\u70B9\u65F6\u95F4</label>
4652
+ <label>\u6267\u884C\u65F6\u95F4</label>
4274
4653
  <input id="scheduler-runat" type="datetime-local" style="width: 220px;" />
4275
4654
  </div>
4276
4655
  <div>
@@ -4280,8 +4659,9 @@ function renderSchedulerPanel(root, ctx2) {
4280
4659
  </div>
4281
4660
  <div class="row">
4282
4661
  <div>
4283
- <label>Profile</label>
4284
- <input id="scheduler-profile" placeholder="xiaohongshu-batch-1" style="width: 220px;" />
4662
+ <label>Profile\uFF08\u53EF\u7559\u7A7A\u81EA\u52A8\u9009\uFF09</label>
4663
+ <input id="scheduler-profile" placeholder="\u7559\u7A7A\u81EA\u52A8\u9009\u62E9\u8BE5\u5E73\u53F0\u6709\u6548\u8D26\u53F7" style="width: 260px;" />
4664
+ <div id="scheduler-profile-hint" class="muted" style="font-size:11px; margin-top:2px;">\u63A8\u8350: -</div>
4285
4665
  </div>
4286
4666
  <div>
4287
4667
  <label>\u5173\u952E\u8BCD</label>
@@ -4331,6 +4711,7 @@ function renderSchedulerPanel(root, ctx2) {
4331
4711
  </div>
4332
4712
  <div class="btn-group" style="margin-top: var(--gap);">
4333
4713
  <button id="scheduler-save-btn" style="flex:1;">\u4FDD\u5B58\u4EFB\u52A1</button>
4714
+ <button id="scheduler-run-now-btn" class="secondary" style="flex:1;">\u7ACB\u5373\u6267\u884C(\u4E0D\u4FDD\u5B58)</button>
4334
4715
  <button id="scheduler-reset-btn" class="secondary" style="flex:1;">\u6E05\u7A7A\u8868\u5355</button>
4335
4716
  </div>
4336
4717
  `;
@@ -4359,12 +4740,15 @@ function renderSchedulerPanel(root, ctx2) {
4359
4740
  const nameInput = root.querySelector("#scheduler-name");
4360
4741
  const enabledInput = root.querySelector("#scheduler-enabled");
4361
4742
  const typeSelect = root.querySelector("#scheduler-type");
4743
+ const periodicTypeWrap = root.querySelector("#scheduler-periodic-type-wrap");
4744
+ const periodicTypeSelect = root.querySelector("#scheduler-periodic-type");
4362
4745
  const intervalWrap = root.querySelector("#scheduler-interval-wrap");
4363
4746
  const runAtWrap = root.querySelector("#scheduler-runat-wrap");
4364
4747
  const intervalInput = root.querySelector("#scheduler-interval");
4365
4748
  const runAtInput = root.querySelector("#scheduler-runat");
4366
4749
  const maxRunsInput = root.querySelector("#scheduler-max-runs");
4367
4750
  const profileInput = root.querySelector("#scheduler-profile");
4751
+ const profileHint = root.querySelector("#scheduler-profile-hint");
4368
4752
  const keywordInput = root.querySelector("#scheduler-keyword");
4369
4753
  const userIdWrap = root.querySelector("#scheduler-user-id-wrap");
4370
4754
  const userIdInput = root.querySelector("#scheduler-user-id");
@@ -4376,8 +4760,10 @@ function renderSchedulerPanel(root, ctx2) {
4376
4760
  const dryRunInput = root.querySelector("#scheduler-dryrun");
4377
4761
  const likeKeywordsInput = root.querySelector("#scheduler-like-keywords");
4378
4762
  const saveBtn = root.querySelector("#scheduler-save-btn");
4763
+ const runNowBtn = root.querySelector("#scheduler-run-now-btn");
4379
4764
  const resetBtn = root.querySelector("#scheduler-reset-btn");
4380
4765
  let tasks = [];
4766
+ let accountRows = [];
4381
4767
  let daemonRunId = "";
4382
4768
  let unsubscribeCmd = null;
4383
4769
  let pendingFocusTaskId = String(ctx2?.activeTaskConfigId || "").trim();
@@ -4394,14 +4780,21 @@ function renderSchedulerPanel(root, ctx2) {
4394
4780
  function openConfigTab(taskId) {
4395
4781
  setActiveTaskContext(taskId);
4396
4782
  if (typeof ctx2.setActiveTab === "function") {
4397
- ctx2.setActiveTab("config");
4783
+ ctx2.setActiveTab("tasks");
4398
4784
  }
4399
4785
  }
4400
4786
  function updateTypeFields() {
4401
- const mode = typeSelect.value;
4402
- const useRunAt = mode === "once" || mode === "daily" || mode === "weekly";
4403
- runAtWrap.style.display = useRunAt ? "" : "none";
4404
- intervalWrap.style.display = useRunAt ? "none" : "";
4787
+ const mode = String(typeSelect.value || "immediate").trim();
4788
+ const periodicType = String(periodicTypeSelect.value || "interval").trim();
4789
+ const periodic = mode === "periodic";
4790
+ const scheduled = mode === "scheduled";
4791
+ periodicTypeWrap.style.display = periodic ? "" : "none";
4792
+ runAtWrap.style.display = scheduled || periodic && periodicType !== "interval" ? "" : "none";
4793
+ intervalWrap.style.display = periodic && periodicType === "interval" ? "" : "none";
4794
+ maxRunsInput.disabled = mode === "immediate" || mode === "scheduled";
4795
+ if (mode === "immediate" || mode === "scheduled") {
4796
+ maxRunsInput.value = "";
4797
+ }
4405
4798
  }
4406
4799
  function updateTaskTypeOptions() {
4407
4800
  const platform = platformSelect.value;
@@ -4411,6 +4804,36 @@ function renderSchedulerPanel(root, ctx2) {
4411
4804
  taskTypeSelect.value = taskTypeSelect.options[0]?.value || "";
4412
4805
  }
4413
4806
  updatePlatformFields();
4807
+ void refreshPlatformAccounts(platform);
4808
+ }
4809
+ function normalizePlatform2(value) {
4810
+ const raw = String(value || "").trim().toLowerCase();
4811
+ if (raw === "weibo") return "weibo";
4812
+ if (raw === "1688") return "1688";
4813
+ return "xiaohongshu";
4814
+ }
4815
+ async function refreshPlatformAccounts(platformValue) {
4816
+ const platform = normalizePlatform2(platformValue);
4817
+ try {
4818
+ accountRows = await listAccountProfiles(ctx2.api, { platform: platform === "xiaohongshu" ? "xiaohongshu" : platform });
4819
+ } catch {
4820
+ accountRows = [];
4821
+ }
4822
+ const recommended = accountRows.filter((row) => row.valid).sort((a, b) => {
4823
+ const ta = Date.parse(String(a.updatedAt || "")) || 0;
4824
+ const tb = Date.parse(String(b.updatedAt || "")) || 0;
4825
+ if (tb !== ta) return tb - ta;
4826
+ return String(a.profileId || "").localeCompare(String(b.profileId || ""));
4827
+ })[0];
4828
+ if (!recommended) {
4829
+ profileHint.textContent = `\u63A8\u8350: \u5F53\u524D\u5E73\u53F0(${platform})\u65E0\u6709\u6548\u8D26\u53F7`;
4830
+ return;
4831
+ }
4832
+ const label = recommended.alias || recommended.name || recommended.profileId;
4833
+ profileHint.textContent = `\u63A8\u8350: ${label} (${recommended.profileId})`;
4834
+ if (!String(profileInput.value || "").trim()) {
4835
+ profileInput.value = recommended.profileId;
4836
+ }
4414
4837
  }
4415
4838
  function updatePlatformFields() {
4416
4839
  const commandType = String(taskTypeSelect.value || "").trim();
@@ -4423,7 +4846,8 @@ function renderSchedulerPanel(root, ctx2) {
4423
4846
  editingIdInput.value = "";
4424
4847
  nameInput.value = "";
4425
4848
  enabledInput.checked = true;
4426
- typeSelect.value = "interval";
4849
+ typeSelect.value = "immediate";
4850
+ periodicTypeSelect.value = "interval";
4427
4851
  intervalInput.value = "30";
4428
4852
  runAtInput.value = "";
4429
4853
  maxRunsInput.value = "";
@@ -4446,7 +4870,6 @@ function renderSchedulerPanel(root, ctx2) {
4446
4870
  const maxRuns = maxRunsRaw ? Math.max(1, Number(maxRunsRaw) || 1) : null;
4447
4871
  const commandType = String(taskTypeSelect.value || "xhs-unified").trim();
4448
4872
  const argv = {
4449
- profile: profileInput.value.trim(),
4450
4873
  keyword: keywordInput.value.trim(),
4451
4874
  "max-notes": Number(maxNotesInput.value || 50) || 50,
4452
4875
  env: envSelect.value,
@@ -4456,19 +4879,40 @@ function renderSchedulerPanel(root, ctx2) {
4456
4879
  headless: headlessInput.checked,
4457
4880
  "dry-run": dryRunInput.checked
4458
4881
  };
4882
+ const profileValue = profileInput.value.trim();
4883
+ if (profileValue) argv.profile = profileValue;
4459
4884
  if (commandType.startsWith("weibo")) {
4460
- argv["task-type"] = commandTypeToWeiboTaskType2(commandType);
4461
4885
  argv["user-id"] = userIdInput.value.trim();
4462
4886
  }
4887
+ const mode = String(typeSelect.value || "immediate").trim();
4888
+ const periodicType = String(periodicTypeSelect.value || "interval").trim();
4889
+ let scheduleType = "once";
4890
+ let runAt = toIsoOrNull(runAtInput.value);
4891
+ let maxRunsFinal = maxRuns;
4892
+ if (mode === "immediate") {
4893
+ scheduleType = "once";
4894
+ runAt = (/* @__PURE__ */ new Date()).toISOString();
4895
+ maxRunsFinal = 1;
4896
+ } else if (mode === "periodic") {
4897
+ if (periodicType === "daily" || periodicType === "weekly") {
4898
+ scheduleType = periodicType;
4899
+ } else {
4900
+ scheduleType = "interval";
4901
+ runAt = null;
4902
+ }
4903
+ } else {
4904
+ scheduleType = "once";
4905
+ maxRunsFinal = 1;
4906
+ }
4463
4907
  return {
4464
4908
  id: editingIdInput.value.trim(),
4465
4909
  name: nameInput.value.trim(),
4466
4910
  enabled: enabledInput.checked,
4467
4911
  commandType,
4468
- scheduleType: typeSelect.value,
4912
+ scheduleType,
4469
4913
  intervalMinutes: Number(intervalInput.value || 30) || 30,
4470
- runAt: toIsoOrNull(runAtInput.value),
4471
- maxRuns,
4914
+ runAt,
4915
+ maxRuns: maxRunsFinal,
4472
4916
  argv
4473
4917
  };
4474
4918
  }
@@ -4481,7 +4925,9 @@ function renderSchedulerPanel(root, ctx2) {
4481
4925
  editingIdInput.value = task.id;
4482
4926
  nameInput.value = task.name || "";
4483
4927
  enabledInput.checked = task.enabled !== false;
4484
- typeSelect.value = task.scheduleType;
4928
+ const uiSchedule = inferUiScheduleEditorState(task);
4929
+ typeSelect.value = uiSchedule.mode;
4930
+ periodicTypeSelect.value = uiSchedule.periodicType;
4485
4931
  intervalInput.value = String(task.intervalMinutes || 30);
4486
4932
  runAtInput.value = toLocalDatetimeValue(task.runAt);
4487
4933
  maxRunsInput.value = task.maxRuns ? String(task.maxRuns) : "";
@@ -4499,19 +4945,27 @@ function renderSchedulerPanel(root, ctx2) {
4499
4945
  updatePlatformFields();
4500
4946
  updateTypeFields();
4501
4947
  }
4502
- async function runScheduleJson(args, timeoutMs = 6e4) {
4503
- const script = ctx2.api.pathJoin("apps", "webauto", "entry", "schedule.mjs");
4504
- const ret = await ctx2.api.cmdRunJson({
4505
- title: `schedule ${args.join(" ")}`,
4506
- cwd: "",
4507
- args: [script, ...args, "--json"],
4508
- timeoutMs
4509
- });
4948
+ async function invokeSchedule(input) {
4949
+ if (typeof ctx2.api?.scheduleInvoke !== "function") {
4950
+ throw new Error("scheduleInvoke unavailable");
4951
+ }
4952
+ const ret = await ctx2.api.scheduleInvoke(input);
4510
4953
  if (!ret?.ok) {
4511
- const reason = String(ret?.error || ret?.stderr || ret?.stdout || "unknown_error").trim();
4954
+ const reason = String(ret?.error || "schedule command failed").trim();
4512
4955
  throw new Error(reason || "schedule command failed");
4513
4956
  }
4514
- return ret.json || {};
4957
+ return ret?.json ?? ret;
4958
+ }
4959
+ async function invokeTaskRunEphemeral(input) {
4960
+ if (typeof ctx2.api?.taskRunEphemeral !== "function") {
4961
+ throw new Error("taskRunEphemeral unavailable");
4962
+ }
4963
+ const ret = await ctx2.api.taskRunEphemeral(input);
4964
+ if (!ret?.ok) {
4965
+ const reason = String(ret?.error || "run ephemeral failed").trim();
4966
+ throw new Error(reason || "run ephemeral failed");
4967
+ }
4968
+ return ret;
4515
4969
  }
4516
4970
  function downloadJson(fileName, payload) {
4517
4971
  const text = JSON.stringify(payload, null, 2);
@@ -4533,7 +4987,7 @@ function renderSchedulerPanel(root, ctx2) {
4533
4987
  const card = createEl("div", {
4534
4988
  style: "border:1px solid var(--border); border-radius:10px; padding:10px; margin-bottom:10px; background: var(--panel-soft);"
4535
4989
  });
4536
- const scheduleText = task.scheduleType === "once" ? `once @ ${task.runAt || "-"}` : task.scheduleType === "daily" ? `daily @ ${task.runAt || "-"}` : task.scheduleType === "weekly" ? `weekly @ ${task.runAt || "-"}` : `interval ${task.intervalMinutes}m`;
4990
+ const scheduleText = task.scheduleType === "once" ? `\u5B9A\u65F6\u4EFB\u52A1 @ ${task.runAt || "-"}` : task.scheduleType === "daily" ? `\u5468\u671F\u4EFB\u52A1(\u65E5) @ ${task.runAt || "-"}` : task.scheduleType === "weekly" ? `\u5468\u671F\u4EFB\u52A1(\u5468) @ ${task.runAt || "-"}` : `\u5468\u671F\u4EFB\u52A1(\u95F4\u9694 ${task.intervalMinutes}m)`;
4537
4991
  const statusText = task.lastStatus ? `${task.lastStatus} / run=${task.runCount} / fail=${task.failCount}` : "never run";
4538
4992
  const headRow = createEl("div", { style: "display:flex; justify-content:space-between; gap:8px; margin-bottom:6px;" });
4539
4993
  headRow.appendChild(createEl("div", { style: "font-weight:600;" }, [task.name || task.id]));
@@ -4582,7 +5036,7 @@ function renderSchedulerPanel(root, ctx2) {
4582
5036
  runBtn.onclick = async () => {
4583
5037
  try {
4584
5038
  setActiveTaskContext(task.id);
4585
- const out = await runScheduleJson(["run", task.id], 0);
5039
+ const out = await invokeSchedule({ action: "run", taskId: task.id, timeoutMs: 0 });
4586
5040
  const runId = String(
4587
5041
  out?.result?.runResult?.lastRunId || out?.result?.runResult?.runId || out?.runResult?.runId || ""
4588
5042
  ).trim();
@@ -4596,6 +5050,7 @@ function renderSchedulerPanel(root, ctx2) {
4596
5050
  target: Number(argv["max-notes"] || argv.target || 0) || 0,
4597
5051
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
4598
5052
  };
5053
+ ctx2.activeRunId = runId || null;
4599
5054
  }
4600
5055
  if (typeof ctx2.setStatus === "function") {
4601
5056
  ctx2.setStatus(`running: ${task.id}`);
@@ -4610,7 +5065,7 @@ function renderSchedulerPanel(root, ctx2) {
4610
5065
  };
4611
5066
  exportBtn.onclick = async () => {
4612
5067
  try {
4613
- const out = await runScheduleJson(["export", task.id]);
5068
+ const out = await invokeSchedule({ action: "export", taskId: task.id });
4614
5069
  downloadJson(`${task.id}.json`, out);
4615
5070
  } catch (err) {
4616
5071
  alert(`\u5BFC\u51FA\u5931\u8D25: ${err?.message || String(err)}`);
@@ -4619,7 +5074,7 @@ function renderSchedulerPanel(root, ctx2) {
4619
5074
  delBtn.onclick = async () => {
4620
5075
  if (!confirm(`\u786E\u8BA4\u5220\u9664\u4EFB\u52A1 ${task.id} ?`)) return;
4621
5076
  try {
4622
- await runScheduleJson(["delete", task.id]);
5077
+ await invokeSchedule({ action: "delete", taskId: task.id });
4623
5078
  await refreshList();
4624
5079
  } catch (err) {
4625
5080
  alert(`\u5220\u9664\u5931\u8D25: ${err?.message || String(err)}`);
@@ -4629,7 +5084,7 @@ function renderSchedulerPanel(root, ctx2) {
4629
5084
  }
4630
5085
  }
4631
5086
  async function refreshList() {
4632
- const out = await runScheduleJson(["list"]);
5087
+ const out = await invokeSchedule({ action: "list" });
4633
5088
  tasks = parseTaskRows(out);
4634
5089
  if (!pendingFocusTaskId) {
4635
5090
  pendingFocusTaskId = String(ctx2?.activeTaskConfigId || "").trim();
@@ -4649,48 +5104,8 @@ function renderSchedulerPanel(root, ctx2) {
4649
5104
  }
4650
5105
  async function saveTask() {
4651
5106
  const payload = readFormAsPayload();
4652
- if (!payload.name) {
4653
- alert("\u4EFB\u52A1\u540D\u4E0D\u80FD\u4E3A\u7A7A");
4654
- return;
4655
- }
4656
- if (!payload.argv.profile && !payload.argv.profiles && !payload.argv.profilepool) {
4657
- alert("profile/profiles/profilepool \u81F3\u5C11\u586B\u5199\u4E00\u4E2A");
4658
- return;
4659
- }
4660
- const commandType = String(payload.commandType || "").trim();
4661
- const keywordRequired = commandType === "xhs-unified" || commandType === "weibo-search" || commandType === "1688-search";
4662
- if (keywordRequired && !payload.argv.keyword) {
4663
- alert("\u5173\u952E\u8BCD\u4E0D\u80FD\u4E3A\u7A7A");
4664
- return;
4665
- }
4666
- if (commandType === "weibo-monitor" && !payload.argv["user-id"]) {
4667
- alert("\u5FAE\u535A monitor \u4EFB\u52A1\u9700\u8981 user-id");
4668
- return;
4669
- }
4670
- const args = payload.id ? ["update", payload.id] : ["add"];
4671
- args.push("--name", payload.name);
4672
- args.push("--enabled", String(payload.enabled));
4673
- args.push("--command-type", commandType || "xhs-unified");
4674
- args.push("--schedule-type", payload.scheduleType);
4675
- if (payload.scheduleType === "once") {
4676
- if (!payload.runAt) {
4677
- alert("\u4E00\u6B21\u6027\u4EFB\u52A1\u9700\u8981\u951A\u70B9\u65F6\u95F4");
4678
- return;
4679
- }
4680
- args.push("--run-at", payload.runAt);
4681
- } else if (payload.scheduleType === "daily" || payload.scheduleType === "weekly") {
4682
- if (!payload.runAt) {
4683
- alert(`${payload.scheduleType} \u4EFB\u52A1\u9700\u8981\u951A\u70B9\u65F6\u95F4`);
4684
- return;
4685
- }
4686
- args.push("--run-at", payload.runAt);
4687
- } else {
4688
- args.push("--interval-minutes", String(Math.max(1, payload.intervalMinutes)));
4689
- }
4690
- args.push("--max-runs", payload.maxRuns === null ? "0" : String(payload.maxRuns));
4691
- args.push("--argv-json", JSON.stringify(payload.argv));
4692
5107
  try {
4693
- const out = await runScheduleJson(args);
5108
+ const out = await invokeSchedule({ action: "save", payload });
4694
5109
  const savedId = String(out?.task?.id || payload.id || "").trim();
4695
5110
  pendingFocusTaskId = savedId;
4696
5111
  if (savedId) setActiveTaskContext(savedId);
@@ -4699,9 +5114,44 @@ function renderSchedulerPanel(root, ctx2) {
4699
5114
  alert(`\u4FDD\u5B58\u5931\u8D25: ${err?.message || String(err)}`);
4700
5115
  }
4701
5116
  }
5117
+ async function runNowFromForm() {
5118
+ runNowBtn.disabled = true;
5119
+ const prevText = runNowBtn.textContent;
5120
+ runNowBtn.textContent = "\u6267\u884C\u4E2D...";
5121
+ try {
5122
+ const payload = readFormAsPayload();
5123
+ const ret = await invokeTaskRunEphemeral({
5124
+ commandType: payload.commandType,
5125
+ argv: payload.argv
5126
+ });
5127
+ const runId = String(ret?.runId || "").trim();
5128
+ if (payload.commandType === "xhs-unified" && ctx2 && typeof ctx2 === "object") {
5129
+ ctx2.xhsCurrentRun = {
5130
+ runId: runId || null,
5131
+ taskId: null,
5132
+ profileId: String(payload.argv.profile || ""),
5133
+ keyword: String(payload.argv.keyword || ""),
5134
+ target: Number(payload.argv["max-notes"] || payload.argv.target || 0) || 0,
5135
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
5136
+ };
5137
+ ctx2.activeRunId = runId || null;
5138
+ }
5139
+ if (typeof ctx2.setStatus === "function") {
5140
+ ctx2.setStatus(`started: ${payload.commandType}`);
5141
+ }
5142
+ if (payload.commandType === "xhs-unified" && typeof ctx2.setActiveTab === "function") {
5143
+ ctx2.setActiveTab("dashboard");
5144
+ }
5145
+ } catch (err) {
5146
+ alert(`\u6267\u884C\u5931\u8D25: ${err?.message || String(err)}`);
5147
+ } finally {
5148
+ runNowBtn.disabled = false;
5149
+ runNowBtn.textContent = prevText || "\u7ACB\u5373\u6267\u884C(\u4E0D\u4FDD\u5B58)";
5150
+ }
5151
+ }
4702
5152
  async function runDueNow() {
4703
5153
  try {
4704
- const out = await runScheduleJson(["run-due", "--limit", "20"], 0);
5154
+ const out = await invokeSchedule({ action: "run-due", limit: 20, timeoutMs: 0 });
4705
5155
  alert(`\u5230\u70B9\u4EFB\u52A1\u6267\u884C\u5B8C\u6210\uFF1Adue=${out.count || 0}, success=${out.success || 0}, failed=${out.failed || 0}`);
4706
5156
  await refreshList();
4707
5157
  } catch (err) {
@@ -4710,7 +5160,7 @@ function renderSchedulerPanel(root, ctx2) {
4710
5160
  }
4711
5161
  async function exportAll() {
4712
5162
  try {
4713
- const out = await runScheduleJson(["export"]);
5163
+ const out = await invokeSchedule({ action: "export" });
4714
5164
  downloadJson("webauto-schedules.json", out);
4715
5165
  } catch (err) {
4716
5166
  alert(`\u5BFC\u51FA\u5931\u8D25: ${err?.message || String(err)}`);
@@ -4725,7 +5175,7 @@ function renderSchedulerPanel(root, ctx2) {
4725
5175
  if (!file) return;
4726
5176
  try {
4727
5177
  const text = await file.text();
4728
- await runScheduleJson(["import", "--payload-json", text, "--mode", "merge"]);
5178
+ await invokeSchedule({ action: "import", payloadJson: text, mode: "merge" });
4729
5179
  await refreshList();
4730
5180
  } catch (err) {
4731
5181
  alert(`\u5BFC\u5165\u5931\u8D25: ${err?.message || String(err)}`);
@@ -4739,13 +5189,7 @@ function renderSchedulerPanel(root, ctx2) {
4739
5189
  return;
4740
5190
  }
4741
5191
  const interval = Math.max(5, Number(daemonIntervalInput.value || 30) || 30);
4742
- const script = ctx2.api.pathJoin("apps", "webauto", "entry", "schedule.mjs");
4743
- const ret = await ctx2.api.cmdSpawn({
4744
- title: `schedule daemon ${interval}s`,
4745
- cwd: "",
4746
- args: [script, "daemon", "--interval-sec", String(interval), "--limit", "20", "--json"],
4747
- groupKey: "scheduler"
4748
- });
5192
+ const ret = await invokeSchedule({ action: "daemon-start", intervalSec: interval, limit: 20 });
4749
5193
  daemonRunId = String(ret?.runId || "").trim();
4750
5194
  setDaemonStatus(daemonRunId ? `daemon: \u8FD0\u884C\u4E2D (${daemonRunId})` : "daemon: \u542F\u52A8\u5931\u8D25");
4751
5195
  }
@@ -4764,7 +5208,9 @@ function renderSchedulerPanel(root, ctx2) {
4764
5208
  platformSelect.addEventListener("change", updateTaskTypeOptions);
4765
5209
  taskTypeSelect.addEventListener("change", updatePlatformFields);
4766
5210
  typeSelect.addEventListener("change", updateTypeFields);
5211
+ periodicTypeSelect.addEventListener("change", updateTypeFields);
4767
5212
  saveBtn.onclick = () => void saveTask();
5213
+ runNowBtn.onclick = () => void runNowFromForm();
4768
5214
  resetBtn.onclick = () => resetForm();
4769
5215
  refreshBtn.onclick = () => void refreshList();
4770
5216
  runDueBtn.onclick = () => void runDueNow();
@@ -4816,6 +5262,8 @@ var tabs = [
4816
5262
  var tabsEl = document.getElementById("tabs");
4817
5263
  var contentEl = document.getElementById("content");
4818
5264
  var statusEl = document.getElementById("status");
5265
+ var appTitleEl = document.getElementById("app-title");
5266
+ var appVersionEl = document.getElementById("app-version");
4819
5267
  var activeTabCleanup = null;
4820
5268
  var mutableApi = { ...window.api || {}, settings: null };
4821
5269
  var tabIcons = {
@@ -4891,6 +5339,22 @@ function startDesktopHeartbeat() {
4891
5339
  async function loadSettings() {
4892
5340
  await ctx.refreshSettings();
4893
5341
  }
5342
+ async function applyVersionBadge() {
5343
+ try {
5344
+ if (typeof window.api?.appGetVersion !== "function") return;
5345
+ const info = await window.api.appGetVersion();
5346
+ const webauto = String(info?.webauto || "").trim();
5347
+ const desktop = String(info?.desktop || "").trim();
5348
+ const badge = String(info?.badge || "").trim();
5349
+ if (appTitleEl && webauto) {
5350
+ appTitleEl.textContent = `WebAuto Console v${webauto}`;
5351
+ }
5352
+ if (appVersionEl) {
5353
+ appVersionEl.textContent = badge || (desktop && desktop !== webauto ? `webauto v${webauto} \xB7 console v${desktop}` : webauto ? `v${webauto}` : "v-");
5354
+ }
5355
+ } catch {
5356
+ }
5357
+ }
4894
5358
  function focusTabButton(tabId) {
4895
5359
  const button = tabsEl.querySelector(`[data-tab-id="${tabId}"]`);
4896
5360
  button?.focus();
@@ -4998,6 +5462,7 @@ async function detectStartupTab() {
4998
5462
  }
4999
5463
  async function main() {
5000
5464
  startDesktopHeartbeat();
5465
+ await applyVersionBadge();
5001
5466
  await loadSettings();
5002
5467
  installCmdEvents();
5003
5468
  const startupTab = await detectStartupTab();