@web-auto/webauto 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/apps/desktop-console/dist/main/index.mjs +802 -90
  2. package/apps/desktop-console/dist/main/preload.mjs +3 -0
  3. package/apps/desktop-console/dist/renderer/index.html +9 -1
  4. package/apps/desktop-console/dist/renderer/index.js +784 -332
  5. package/apps/desktop-console/entry/ui-cli.mjs +23 -8
  6. package/apps/desktop-console/entry/ui-console.mjs +8 -3
  7. package/apps/webauto/entry/account.mjs +69 -8
  8. package/apps/webauto/entry/lib/account-detect.mjs +106 -25
  9. package/apps/webauto/entry/lib/account-store.mjs +121 -22
  10. package/apps/webauto/entry/lib/schedule-store.mjs +0 -12
  11. package/apps/webauto/entry/profilepool.mjs +45 -3
  12. package/apps/webauto/entry/schedule.mjs +44 -2
  13. package/apps/webauto/entry/weibo-unified.mjs +2 -2
  14. package/apps/webauto/entry/xhs-install.mjs +220 -51
  15. package/apps/webauto/entry/xhs-unified.mjs +33 -6
  16. package/bin/webauto.mjs +80 -4
  17. package/dist/modules/camo-runtime/src/utils/browser-service.mjs +4 -0
  18. package/dist/services/unified-api/server.js +5 -0
  19. package/dist/services/unified-api/task-state.js +2 -0
  20. package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +142 -14
  21. package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +16 -1
  22. package/modules/camo-runtime/src/autoscript/action-providers/xhs.mjs +104 -0
  23. package/modules/camo-runtime/src/autoscript/runtime.mjs +14 -4
  24. package/modules/camo-runtime/src/autoscript/schema.mjs +9 -0
  25. package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +9 -2
  26. package/modules/camo-runtime/src/container/runtime-core/checkpoint.mjs +107 -1
  27. package/modules/camo-runtime/src/container/runtime-core/subscription.mjs +24 -2
  28. package/modules/camo-runtime/src/utils/browser-service.mjs +4 -0
  29. package/package.json +6 -3
  30. package/scripts/bump-version.mjs +120 -0
  31. package/services/unified-api/server.ts +4 -0
  32. package/services/unified-api/task-state.ts +5 -0
@@ -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" });
@@ -1019,6 +1019,7 @@ function renderSettings(root, ctx2) {
1019
1019
  const keyword = createEl("input", { value: ctx2.settings?.defaultKeyword || "" });
1020
1020
  const loginTimeout = createEl("input", { value: String(ctx2.settings?.timeouts?.loginTimeoutSec || 900), type: "number", min: "30" });
1021
1021
  const cmdTimeout = createEl("input", { value: String(ctx2.settings?.timeouts?.cmdTimeoutSec || 0), type: "number", min: "0" });
1022
+ const idleTimeout = createEl("input", { value: ctx2.settings?.idleTimeout || "30m", placeholder: "30m" });
1022
1023
  const aiEnabled = createEl("input", { type: "checkbox", checked: ctx2.settings?.aiReply?.enabled ?? false });
1023
1024
  const aiBaseUrl = createEl("input", { value: ctx2.settings?.aiReply?.baseUrl || "http://127.0.0.1:5520", placeholder: "http://127.0.0.1:5520" });
1024
1025
  const aiApiKey = createEl("input", { value: ctx2.settings?.aiReply?.apiKey || "", type: "password", placeholder: "sk-..." });
@@ -1080,6 +1081,7 @@ function renderSettings(root, ctx2) {
1080
1081
  loginTimeoutSec: Number(loginTimeout.value || "900"),
1081
1082
  cmdTimeoutSec: Number(cmdTimeout.value || "0")
1082
1083
  },
1084
+ idleTimeout: idleTimeout.value.trim() || "30m",
1083
1085
  aiReply: {
1084
1086
  enabled: aiEnabled.checked,
1085
1087
  baseUrl: aiBaseUrl.value.trim(),
@@ -1107,7 +1109,8 @@ function renderSettings(root, ctx2) {
1107
1109
  ]),
1108
1110
  createEl("div", { className: "row" }, [
1109
1111
  labeledInput("loginTimeoutSec", loginTimeout),
1110
- labeledInput("cmdTimeoutSec", cmdTimeout)
1112
+ labeledInput("cmdTimeoutSec", cmdTimeout),
1113
+ labeledInput("idleTimeout (e.g., 30m, 1h)", idleTimeout)
1111
1114
  ]),
1112
1115
  createEl("div", { className: "row" }, [
1113
1116
  createEl("button", {}, ["\u4FDD\u5B58"])
@@ -1524,12 +1527,13 @@ function normalizeRow(row) {
1524
1527
  updatedAt: asText(row?.updatedAt)
1525
1528
  };
1526
1529
  }
1527
- async function listAccountProfiles(api) {
1530
+ async function listAccountProfiles(api, options = {}) {
1528
1531
  const script = api.pathJoin("apps", "webauto", "entry", "account.mjs");
1532
+ const platform = asText(options?.platform);
1529
1533
  const out = await api.cmdRunJson({
1530
1534
  title: "account list",
1531
1535
  cwd: "",
1532
- args: [script, "list", "--json"],
1536
+ args: [script, "list", ...platform ? ["--platform", platform] : [], "--json"],
1533
1537
  timeoutMs: 2e4
1534
1538
  });
1535
1539
  const rows = Array.isArray(out?.json?.profiles) ? out.json.profiles : [];
@@ -1537,6 +1541,14 @@ async function listAccountProfiles(api) {
1537
1541
  }
1538
1542
 
1539
1543
  // src/renderer/tabs-new/setup-wizard.mts
1544
+ function formatProfileTag(profileId) {
1545
+ const id = String(profileId || "").trim();
1546
+ const m = id.match(/^profile-(\d+)$/i);
1547
+ if (!m) return id;
1548
+ const seq = Number(m[1]);
1549
+ if (!Number.isFinite(seq)) return id;
1550
+ return `P${String(seq).padStart(3, "0")}`;
1551
+ }
1540
1552
  function renderSetupWizard(root, ctx2) {
1541
1553
  root.innerHTML = "";
1542
1554
  const autoSyncTimers = /* @__PURE__ */ new Map();
@@ -1574,7 +1586,7 @@ function renderSetupWizard(root, ctx2) {
1574
1586
  <div class="env-item" id="env-firefox" style="display:flex; align-items:center; justify-content:space-between; gap:8px;">
1575
1587
  <span style="display:flex; align-items:center; gap:8px; min-width:0;">
1576
1588
  <span class="icon" style="color: var(--text-4);">\u25CB</span>
1577
- <span class="env-label">Camoufox Runtime (python -m camoufox)</span>
1589
+ <span class="env-label">\u6D4F\u89C8\u5668\u5185\u6838\uFF08Camoufox Firefox\uFF09</span>
1578
1590
  </span>
1579
1591
  <button id="repair-runtime-btn" class="secondary" style="display:none; flex:0 0 auto;">\u4E00\u952E\u4FEE\u590D</button>
1580
1592
  </div>
@@ -1648,30 +1660,31 @@ function renderSetupWizard(root, ctx2) {
1648
1660
  let envCheckInFlight = false;
1649
1661
  let accountCheckInFlight = false;
1650
1662
  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
- });
1663
+ const getMissing = (snapshot) => snapshot?.missing || {
1664
+ core: true,
1665
+ runtimeService: true,
1666
+ camo: true,
1667
+ runtime: true,
1668
+ geoip: true
1669
+ };
1670
+ const isEnvReady = (snapshot) => Boolean(snapshot?.allReady);
1661
1671
  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 };
1672
+ if (typeof ctx2.api?.envCheckAll !== "function") {
1673
+ throw new Error("envCheckAll unavailable");
1674
+ }
1675
+ const snapshot = await ctx2.api.envCheckAll();
1676
+ if (snapshot && typeof snapshot === "object" && snapshot.camo && snapshot.services) {
1677
+ return snapshot;
1678
+ }
1679
+ throw new Error("invalid envCheckAll response");
1669
1680
  }
1670
1681
  function applyEnvironment(snapshot) {
1682
+ const browserReady = Boolean(snapshot.browserReady);
1683
+ const browserDetail = snapshot.firefox?.installed ? "\u5DF2\u5B89\u88C5" : snapshot.services?.camoRuntime ? "\u7531 Runtime \u670D\u52A1\u63D0\u4F9B" : "\u672A\u5B89\u88C5";
1671
1684
  updateEnvItem("env-camo", snapshot.camo?.installed, snapshot.camo?.version || (snapshot.camo?.installed ? "\u5DF2\u5B89\u88C5" : "\u672A\u5B89\u88C5"));
1672
1685
  updateEnvItem("env-unified", snapshot.services?.unifiedApi, "7701");
1673
1686
  updateEnvItem("env-browser", snapshot.services?.camoRuntime, "7704");
1674
- updateEnvItem("env-firefox", snapshot.firefox?.installed, snapshot.firefox?.path ? "\u5DF2\u5B89\u88C5" : "\u672A\u5B89\u88C5");
1687
+ updateEnvItem("env-firefox", browserReady, snapshot.firefox?.path || browserDetail);
1675
1688
  updateEnvItem("env-geoip", snapshot.geoip?.installed, snapshot.geoip?.installed ? "\u5DF2\u5B89\u88C5\uFF08\u53EF\u9009\uFF09" : "\u672A\u5B89\u88C5\uFF08\u53EF\u9009\uFF09");
1676
1689
  envReady = isEnvReady(snapshot);
1677
1690
  syncRepairButtons(snapshot);
@@ -1720,7 +1733,7 @@ function renderSetupWizard(root, ctx2) {
1720
1733
  }
1721
1734
  async function repairInstall({ browser, geoip, reinstall, uninstall }) {
1722
1735
  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...";
1736
+ 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
1737
  const res = await ctx2.api.envRepairDeps({
1725
1738
  browser: Boolean(browser),
1726
1739
  geoip: Boolean(geoip),
@@ -1732,14 +1745,13 @@ function renderSetupWizard(root, ctx2) {
1732
1745
  return { ok, detail };
1733
1746
  }
1734
1747
  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...";
1748
+ 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
1749
  const script = ctx2.api.pathJoin("apps", "webauto", "entry", "xhs-install.mjs");
1737
1750
  const args = [script];
1738
1751
  if (reinstall) args.push("--reinstall");
1739
1752
  else if (uninstall) args.push("--uninstall");
1740
1753
  if (browser) args.push("--download-browser");
1741
1754
  if (geoip) args.push("--download-geoip");
1742
- if (!uninstall) args.push("--ensure-backend");
1743
1755
  const res = await ctx2.api.cmdRunJson({
1744
1756
  title: "setup auto repair",
1745
1757
  cwd: "",
@@ -1811,14 +1823,14 @@ function renderSetupWizard(root, ctx2) {
1811
1823
  applyEnvironment(latest);
1812
1824
  updateCompleteStatus();
1813
1825
  if (!detail) {
1814
- if (label.includes("Camoufox") || label.includes("Runtime")) {
1815
- ok = Boolean(latest.firefox?.installed);
1816
- } else if (label.includes("Camoufox CLI") || label.includes("CLI") || label.includes("camo")) {
1826
+ if (label.includes("\u6D4F\u89C8\u5668\u5185\u6838") || label.includes("Camoufox") || label.includes("Runtime")) {
1827
+ ok = Boolean(latest.browserReady);
1828
+ } else if (label.includes("CLI") || label.includes("camo")) {
1817
1829
  ok = Boolean(latest.camo?.installed);
1818
1830
  } else if (label.includes("\u6838\u5FC3")) {
1819
1831
  ok = Boolean(latest.services?.unifiedApi && latest.services?.camoRuntime);
1820
1832
  } else {
1821
- ok = isEnvReady(latest);
1833
+ ok = Boolean(latest.allReady);
1822
1834
  }
1823
1835
  }
1824
1836
  }
@@ -1841,12 +1853,13 @@ function renderSetupWizard(root, ctx2) {
1841
1853
  applyEnvironment(snapshot);
1842
1854
  updateCompleteStatus();
1843
1855
  if (!envReady) {
1856
+ const missingFlags = getMissing(snapshot);
1844
1857
  const missing = [];
1845
- if (!snapshot?.camo?.installed) missing.push("camo-cli");
1846
- if (!snapshot?.services?.unifiedApi) missing.push("unified-api");
1847
- if (!snapshot?.firefox?.installed) missing.push("camoufox-runtime");
1858
+ if (missingFlags.camo) missing.push("camo-cli");
1859
+ if (missingFlags.core) missing.push("unified-api");
1860
+ if (missingFlags.runtime) missing.push("browser-kernel");
1848
1861
  setupStatusText.textContent = `\u5B58\u5728\u5F85\u4FEE\u590D\u9879: ${missing.join(", ")}`;
1849
- if (!snapshot?.services?.camoRuntime) {
1862
+ if (missingFlags.runtimeService) {
1850
1863
  setupStatusText.textContent += "\uFF08camo-runtime \u672A\u5C31\u7EEA\uFF0C\u5F53\u524D\u4E3A\u53EF\u9009\uFF09";
1851
1864
  }
1852
1865
  } else if (!snapshot?.geoip?.installed) {
@@ -1918,8 +1931,8 @@ function renderSetupWizard(root, ctx2) {
1918
1931
  style: "display:flex; justify-content:space-between; align-items:center; padding:8px 12px; border-bottom:1px solid var(--border);"
1919
1932
  }, [
1920
1933
  createEl("div", {}, [
1921
- createEl("div", { style: "font-weight:600; margin-bottom:2px;" }, [acc.alias || acc.name || acc.profileId]),
1922
- createEl("div", { className: "muted", style: "font-size:11px;" }, [acc.profileId])
1934
+ createEl("div", { style: "font-weight:600; margin-bottom:2px;" }, [acc.alias || acc.name || formatProfileTag(acc.profileId)]),
1935
+ createEl("div", { className: "muted", style: "font-size:11px;" }, [`${formatProfileTag(acc.profileId)} (${acc.profileId}) \xB7 ${String(acc.platform || "xiaohongshu")}`])
1923
1936
  ]),
1924
1937
  createEl("span", {
1925
1938
  className: `status-badge ${statusClass}`
@@ -2026,6 +2039,7 @@ function renderSetupWizard(root, ctx2) {
2026
2039
  "sync",
2027
2040
  id,
2028
2041
  "--pending-while-login",
2042
+ "--resolve-alias",
2029
2043
  "--json"
2030
2044
  ],
2031
2045
  timeoutMs: 2e4
@@ -2082,12 +2096,12 @@ function renderSetupWizard(root, ctx2) {
2082
2096
  repairCoreBtn.onclick = () => void runRepair("\u4FEE\u590D\u6838\u5FC3\u670D\u52A1", repairCoreServices);
2083
2097
  repairCore2Btn.onclick = () => void runRepair("\u4FEE\u590D\u6838\u5FC3\u670D\u52A1", repairCoreServices);
2084
2098
  repairCamoBtn.onclick = () => void runRepair("\u4FEE\u590D Camo CLI", repairCoreServices);
2085
- repairRuntimeBtn.onclick = () => void runRepair("\u4FEE\u590D Camoufox Runtime", () => repairInstall({ browser: true }));
2099
+ repairRuntimeBtn.onclick = () => void runRepair("\u4FEE\u590D\u6D4F\u89C8\u5668\u5185\u6838", () => repairInstall({ browser: true }));
2086
2100
  repairGeoipBtn.onclick = () => void runRepair("\u5B89\u88C5 GeoIP", () => repairInstall({ geoip: true }));
2087
2101
  addAccountBtn.onclick = addAccount;
2088
2102
  enterMainBtn.onclick = () => {
2089
2103
  if (typeof ctx2.setActiveTab === "function") {
2090
- ctx2.setActiveTab("config");
2104
+ ctx2.setActiveTab("tasks");
2091
2105
  }
2092
2106
  };
2093
2107
  void tickEnvironment();
@@ -2183,6 +2197,21 @@ function parseTaskRows(payload) {
2183
2197
  runHistory: parseRunHistory(row?.runHistory)
2184
2198
  })).filter((row) => row.id);
2185
2199
  }
2200
+ function inferUiScheduleEditorState(task, nowMs = Date.now()) {
2201
+ const scheduleType = normalizeScheduleType(task?.scheduleType);
2202
+ if (scheduleType === "interval") {
2203
+ return { mode: "periodic", periodicType: "interval" };
2204
+ }
2205
+ if (scheduleType === "daily" || scheduleType === "weekly") {
2206
+ return { mode: "periodic", periodicType: scheduleType };
2207
+ }
2208
+ const runAtText = String(task?.runAt || "").trim();
2209
+ const runAtMs = Date.parse(runAtText);
2210
+ if (Number.isFinite(runAtMs) && runAtMs > nowMs + 6e4) {
2211
+ return { mode: "scheduled", periodicType: "interval" };
2212
+ }
2213
+ return { mode: "immediate", periodicType: "interval" };
2214
+ }
2186
2215
  function getTasksForPlatform(platform) {
2187
2216
  const p = platform;
2188
2217
  return PLATFORM_TASKS[p] || [];
@@ -2209,7 +2238,8 @@ var DEFAULT_FORM = {
2209
2238
  collectBody: true,
2210
2239
  doLikes: false,
2211
2240
  likeKeywords: "",
2212
- scheduleType: "interval",
2241
+ scheduleMode: "immediate",
2242
+ periodicType: "interval",
2213
2243
  intervalMinutes: 30,
2214
2244
  runAt: null,
2215
2245
  maxRuns: null
@@ -2218,19 +2248,6 @@ function parseSortableTime(value) {
2218
2248
  const ts = Date.parse(String(value || ""));
2219
2249
  return Number.isFinite(ts) ? ts : 0;
2220
2250
  }
2221
- function isKeywordRequired(taskType) {
2222
- return taskType === "xhs-unified" || taskType === "weibo-search" || taskType === "1688-search";
2223
- }
2224
- function commandTypeToWeiboTaskType(commandType) {
2225
- if (commandType === "weibo-search") return "search";
2226
- if (commandType === "weibo-monitor") return "monitor";
2227
- return "timeline";
2228
- }
2229
- function fallbackTaskName(data) {
2230
- const keyword = String(data.keyword || "").trim();
2231
- const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
2232
- return keyword ? `${data.taskType}-${keyword}` : `${data.taskType}-${stamp}`;
2233
- }
2234
2251
  function renderTasksPanel(root, ctx2) {
2235
2252
  root.innerHTML = "";
2236
2253
  const pageIndicator = createEl("div", { className: "page-indicator" }, [
@@ -2285,8 +2302,9 @@ function renderTasksPanel(root, ctx2) {
2285
2302
  <input id="task-target" type="number" min="1" value="50" style="width: 80px;" />
2286
2303
  </div>
2287
2304
  <div>
2288
- <label>Profile</label>
2289
- <input id="task-profile" placeholder="xiaohongshu-batch-1" style="width: 160px;" />
2305
+ <label>Profile\uFF08\u53EF\u7559\u7A7A\u81EA\u52A8\u9009\uFF09</label>
2306
+ <input id="task-profile" placeholder="\u7559\u7A7A\u81EA\u52A8\u9009\u62E9\u8BE5\u5E73\u53F0\u6709\u6548\u8D26\u53F7" style="width: 220px;" />
2307
+ <div id="task-profile-hint" class="muted" style="font-size:11px; margin-top:2px;">\u63A8\u8350: -</div>
2290
2308
  </div>
2291
2309
  <div>
2292
2310
  <label>\u73AF\u5883</label>
@@ -2324,14 +2342,20 @@ function renderTasksPanel(root, ctx2) {
2324
2342
  <div style="font-size:12px; color:var(--text-secondary); margin-bottom:var(--gap-sm);">\u8C03\u5EA6\u8BBE\u7F6E\uFF08\u53EF\u9009\uFF09</div>
2325
2343
  <div class="row">
2326
2344
  <div>
2327
- <select id="task-schedule-type" style="width: 100px;">
2328
- <option value="interval">\u5FAA\u73AF\u95F4\u9694</option>
2329
- <option value="once">\u4E00\u6B21\u6027</option>
2345
+ <select id="task-schedule-type" style="width: 140px;">
2346
+ <option value="immediate">\u9A6C\u4E0A\u6267\u884C\uFF08\u4EC5\u4E00\u6B21\uFF09</option>
2347
+ <option value="periodic">\u5468\u671F\u4EFB\u52A1</option>
2348
+ <option value="scheduled">\u5B9A\u65F6\u4EFB\u52A1</option>
2349
+ </select>
2350
+ </div>
2351
+ <div id="task-periodic-type-wrap" style="display:none;">
2352
+ <select id="task-periodic-type" style="width: 100px;">
2353
+ <option value="interval">\u6309\u95F4\u9694</option>
2330
2354
  <option value="daily">\u6BCF\u5929</option>
2331
2355
  <option value="weekly">\u6BCF\u5468</option>
2332
2356
  </select>
2333
2357
  </div>
2334
- <div id="task-interval-wrap">
2358
+ <div id="task-interval-wrap" style="display:none;">
2335
2359
  <input id="task-interval" type="number" min="1" value="30" style="width: 70px;" />
2336
2360
  <span style="font-size:11px;color:var(--text-tertiary);">\u5206\u949F</span>
2337
2361
  </div>
@@ -2348,7 +2372,7 @@ function renderTasksPanel(root, ctx2) {
2348
2372
  <div class="btn-group" style="margin-top: var(--gap);">
2349
2373
  <button id="task-save-btn" style="flex:1;">\u4FDD\u5B58\u4EFB\u52A1</button>
2350
2374
  <button id="task-run-btn" class="primary" style="flex:1;">\u4FDD\u5B58\u5E76\u6267\u884C</button>
2351
- <button id="task-run-ephemeral-btn" class="secondary" style="flex:1;">\u4EC5\u6267\u884C(\u4E0D\u4FDD\u5B58)</button>
2375
+ <button id="task-run-ephemeral-btn" class="secondary" style="flex:1;">\u7ACB\u5373\u6267\u884C(\u4E0D\u4FDD\u5B58)</button>
2352
2376
  <button id="task-reset-btn" class="secondary" style="flex:0.6;">\u91CD\u7F6E</button>
2353
2377
  </div>
2354
2378
  `;
@@ -2378,15 +2402,17 @@ function renderTasksPanel(root, ctx2) {
2378
2402
  root.appendChild(mainGrid);
2379
2403
  const recentCard = createEl("div", { className: "bento-cell", style: "margin-top: var(--gap);" });
2380
2404
  recentCard.innerHTML = `
2381
- <div class="bento-title">\u5386\u53F2\u4EFB\u52A1</div>
2405
+ <div class="bento-title">\u5DF2\u4FDD\u5B58\u4EFB\u52A1\u5217\u8868</div>
2382
2406
  <div class="row" style="margin-bottom: var(--gap-sm);">
2383
2407
  <select id="task-history-select" style="min-width: 320px;">
2384
2408
  <option value="">\u9009\u62E9\u5386\u53F2\u4EFB\u52A1...</option>
2385
2409
  </select>
2386
2410
  <button id="task-history-edit-btn" class="secondary">\u8F7D\u5165\u7F16\u8F91</button>
2387
2411
  <button id="task-history-clone-btn" class="secondary">\u8F7D\u5165\u53E6\u5B58</button>
2412
+ <button id="task-history-run-btn">\u7ACB\u5373\u6267\u884C</button>
2388
2413
  <button id="task-history-refresh-btn" class="secondary">\u5237\u65B0</button>
2389
2414
  </div>
2415
+ <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>
2390
2416
  <div id="recent-tasks-list"></div>
2391
2417
  `;
2392
2418
  root.appendChild(recentCard);
@@ -2397,6 +2423,7 @@ function renderTasksPanel(root, ctx2) {
2397
2423
  const keywordInput = formCard.querySelector("#task-keyword");
2398
2424
  const targetInput = formCard.querySelector("#task-target");
2399
2425
  const profileInput = formCard.querySelector("#task-profile");
2426
+ const profileHint = formCard.querySelector("#task-profile-hint");
2400
2427
  const envSelect = formCard.querySelector("#task-env");
2401
2428
  const userIdWrap = formCard.querySelector("#task-user-id-wrap");
2402
2429
  const userIdInput = formCard.querySelector("#task-user-id");
@@ -2405,6 +2432,8 @@ function renderTasksPanel(root, ctx2) {
2405
2432
  const likesInput = formCard.querySelector("#task-likes");
2406
2433
  const likeKeywordsInput = formCard.querySelector("#task-like-keywords");
2407
2434
  const scheduleTypeSelect = formCard.querySelector("#task-schedule-type");
2435
+ const periodicTypeWrap = formCard.querySelector("#task-periodic-type-wrap");
2436
+ const periodicTypeSelect = formCard.querySelector("#task-periodic-type");
2408
2437
  const intervalInput = formCard.querySelector("#task-interval");
2409
2438
  const intervalWrap = formCard.querySelector("#task-interval-wrap");
2410
2439
  const runAtInput = formCard.querySelector("#task-runat");
@@ -2420,22 +2449,62 @@ function renderTasksPanel(root, ctx2) {
2420
2449
  const historySelect = recentCard.querySelector("#task-history-select");
2421
2450
  const historyEditBtn = recentCard.querySelector("#task-history-edit-btn");
2422
2451
  const historyCloneBtn = recentCard.querySelector("#task-history-clone-btn");
2452
+ const historyRunBtn = recentCard.querySelector("#task-history-run-btn");
2423
2453
  const historyRefreshBtn = recentCard.querySelector("#task-history-refresh-btn");
2424
2454
  const recentTasksList = recentCard.querySelector("#recent-tasks-list");
2425
2455
  const statRunning = statsCard.querySelector("#stat-running");
2426
2456
  const statToday = statsCard.querySelector("#stat-today");
2427
2457
  const statSaved = statsCard.querySelector("#stat-saved");
2428
2458
  let tasks = [];
2459
+ let accountRows = [];
2429
2460
  const activeRunIds = /* @__PURE__ */ new Set();
2430
2461
  let unsubscribeActiveRuns = null;
2431
2462
  const joinPath2 = (...parts) => {
2432
2463
  if (typeof ctx2?.api?.pathJoin === "function") return ctx2.api.pathJoin(...parts);
2433
2464
  return parts.filter(Boolean).join("/");
2434
2465
  };
2435
- const scheduleScript = joinPath2("apps", "webauto", "entry", "schedule.mjs");
2436
2466
  const quotaScript = joinPath2("apps", "webauto", "entry", "lib", "quota-status.mjs");
2437
- const xhsScript = joinPath2("apps", "webauto", "entry", "xhs-unified.mjs");
2438
- const weiboScript = joinPath2("apps", "webauto", "entry", "weibo-unified.mjs");
2467
+ function normalizePlatform2(value) {
2468
+ const raw = String(value || "").trim().toLowerCase();
2469
+ if (raw === "weibo") return "weibo";
2470
+ if (raw === "1688") return "1688";
2471
+ return "xiaohongshu";
2472
+ }
2473
+ function platformToAccountPlatform(value) {
2474
+ return value === "xiaohongshu" ? "xiaohongshu" : value;
2475
+ }
2476
+ async function refreshPlatformAccountRows(platform) {
2477
+ try {
2478
+ accountRows = await listAccountProfiles(ctx2.api, { platform: platformToAccountPlatform(platform) });
2479
+ } catch {
2480
+ accountRows = [];
2481
+ }
2482
+ }
2483
+ function getRecommendedProfile(platform) {
2484
+ const candidates = accountRows.filter((row) => row.valid).sort((a, b) => {
2485
+ const ta = Date.parse(String(a.updatedAt || "")) || 0;
2486
+ const tb = Date.parse(String(b.updatedAt || "")) || 0;
2487
+ if (tb !== ta) return tb - ta;
2488
+ return String(a.profileId || "").localeCompare(String(b.profileId || ""));
2489
+ });
2490
+ return candidates[0] || null;
2491
+ }
2492
+ function updateProfileHint(platform) {
2493
+ const recommended = getRecommendedProfile(platform);
2494
+ if (!recommended) {
2495
+ profileHint.textContent = `\u63A8\u8350: \u5F53\u524D\u5E73\u53F0(${platform})\u65E0\u6709\u6548\u8D26\u53F7\uFF0C\u8BF7\u5148\u5230\u8D26\u53F7\u9875\u767B\u5F55`;
2496
+ return;
2497
+ }
2498
+ const label = recommended.alias || recommended.name || recommended.profileId;
2499
+ profileHint.textContent = `\u63A8\u8350: ${label} (${recommended.profileId})`;
2500
+ }
2501
+ function maybeAutofillProfile(platform) {
2502
+ const current = String(profileInput.value || "").trim();
2503
+ if (current) return;
2504
+ const recommended = getRecommendedProfile(platform);
2505
+ if (!recommended) return;
2506
+ profileInput.value = recommended.profileId;
2507
+ }
2439
2508
  function getTaskById(taskId) {
2440
2509
  const id = String(taskId || "").trim();
2441
2510
  if (!id) return null;
@@ -2453,13 +2522,17 @@ function renderTasksPanel(root, ctx2) {
2453
2522
  formTitle.textContent = "\u65B0\u5EFA\u4EFB\u52A1";
2454
2523
  }
2455
2524
  function updateTaskTypeOptions(preferredType = "") {
2456
- const platform = platformSelect.value;
2525
+ const platform = normalizePlatform2(platformSelect.value);
2457
2526
  const options = getTasksForPlatform(platform);
2458
2527
  taskTypeSelect.innerHTML = options.map((item) => `<option value="${item.type}">${item.icon} ${item.label}</option>`).join("");
2459
2528
  const target = String(preferredType || "").trim();
2460
2529
  const matched = options.find((item) => item.type === target);
2461
2530
  taskTypeSelect.value = matched?.type || options[0]?.type || "";
2462
2531
  updatePlatformFields();
2532
+ void refreshPlatformAccountRows(platform).then(() => {
2533
+ updateProfileHint(platform);
2534
+ maybeAutofillProfile(platform);
2535
+ });
2463
2536
  }
2464
2537
  function updatePlatformFields() {
2465
2538
  const taskType = String(taskTypeSelect.value || "").trim();
@@ -2467,9 +2540,17 @@ function renderTasksPanel(root, ctx2) {
2467
2540
  userIdWrap.style.display = isWeiboMonitor ? "" : "none";
2468
2541
  }
2469
2542
  function updateScheduleVisibility() {
2470
- const scheduleType = String(scheduleTypeSelect.value || "interval").trim();
2471
- intervalWrap.style.display = scheduleType === "interval" ? "inline-flex" : "none";
2472
- runAtWrap.style.display = scheduleType === "once" || scheduleType === "daily" || scheduleType === "weekly" ? "inline-flex" : "none";
2543
+ const mode = String(scheduleTypeSelect.value || "immediate").trim();
2544
+ const periodicType = String(periodicTypeSelect.value || "interval").trim();
2545
+ const periodic = mode === "periodic";
2546
+ const scheduled = mode === "scheduled";
2547
+ periodicTypeWrap.style.display = periodic ? "inline-flex" : "none";
2548
+ intervalWrap.style.display = periodic && periodicType === "interval" ? "inline-flex" : "none";
2549
+ runAtWrap.style.display = scheduled || periodic && periodicType !== "interval" ? "inline-flex" : "none";
2550
+ maxRunsInput.disabled = mode === "immediate";
2551
+ if (mode === "immediate") {
2552
+ maxRunsInput.value = "";
2553
+ }
2473
2554
  }
2474
2555
  function updateLikeKeywordsState() {
2475
2556
  likeKeywordsInput.disabled = !likesInput.checked;
@@ -2477,6 +2558,9 @@ function renderTasksPanel(root, ctx2) {
2477
2558
  function collectFormData() {
2478
2559
  const maxRunsRaw = String(maxRunsInput.value || "").trim();
2479
2560
  const maxRunsNum = maxRunsRaw ? Number(maxRunsRaw) : 0;
2561
+ const scheduleMode = scheduleTypeSelect.value;
2562
+ const periodicType = periodicTypeSelect.value;
2563
+ const runAtText = String(runAtInput.value || "").trim();
2480
2564
  return {
2481
2565
  id: String(editingIdInput.value || "").trim() || void 0,
2482
2566
  name: String(nameInput.value || "").trim(),
@@ -2492,9 +2576,10 @@ function renderTasksPanel(root, ctx2) {
2492
2576
  collectBody: bodyInput.checked,
2493
2577
  doLikes: likesInput.checked,
2494
2578
  likeKeywords: String(likeKeywordsInput.value || "").trim(),
2495
- scheduleType: scheduleTypeSelect.value,
2579
+ scheduleMode,
2580
+ periodicType,
2496
2581
  intervalMinutes: Math.max(1, Number(intervalInput.value || 30) || 30),
2497
- runAt: toIsoOrNull(String(runAtInput.value || "")),
2582
+ runAt: toIsoOrNull(runAtText),
2498
2583
  maxRuns: Number.isFinite(maxRunsNum) && maxRunsNum > 0 ? Math.max(1, Math.floor(maxRunsNum)) : null
2499
2584
  };
2500
2585
  }
@@ -2514,7 +2599,9 @@ function renderTasksPanel(root, ctx2) {
2514
2599
  bodyInput.checked = task.commandArgv?.["fetch-body"] !== false;
2515
2600
  likesInput.checked = task.commandArgv?.["do-likes"] === true;
2516
2601
  likeKeywordsInput.value = String(task.commandArgv?.["like-keywords"] || "").trim();
2517
- scheduleTypeSelect.value = String(task.scheduleType || "interval");
2602
+ const uiSchedule = inferUiScheduleEditorState(task);
2603
+ scheduleTypeSelect.value = uiSchedule.mode;
2604
+ periodicTypeSelect.value = uiSchedule.periodicType;
2518
2605
  intervalInput.value = String(task.intervalMinutes || 30);
2519
2606
  runAtInput.value = toLocalDatetimeValue(task.runAt);
2520
2607
  maxRunsInput.value = task.maxRuns ? String(task.maxRuns) : "";
@@ -2537,7 +2624,8 @@ function renderTasksPanel(root, ctx2) {
2537
2624
  bodyInput.checked = DEFAULT_FORM.collectBody;
2538
2625
  likesInput.checked = DEFAULT_FORM.doLikes;
2539
2626
  likeKeywordsInput.value = DEFAULT_FORM.likeKeywords;
2540
- scheduleTypeSelect.value = DEFAULT_FORM.scheduleType;
2627
+ scheduleTypeSelect.value = DEFAULT_FORM.scheduleMode;
2628
+ periodicTypeSelect.value = DEFAULT_FORM.periodicType;
2541
2629
  intervalInput.value = String(DEFAULT_FORM.intervalMinutes);
2542
2630
  runAtInput.value = "";
2543
2631
  maxRunsInput.value = "";
@@ -2571,19 +2659,29 @@ function renderTasksPanel(root, ctx2) {
2571
2659
  }
2572
2660
  }
2573
2661
  function renderRecentTasks() {
2574
- const rows = sortedTasksByRecent().slice(0, 8);
2662
+ const rows = sortedTasksByRecent();
2575
2663
  if (rows.length === 0) {
2576
2664
  recentTasksList.innerHTML = '<div class="muted" style="font-size:12px;">\u6682\u65E0\u4EFB\u52A1</div>';
2577
2665
  return;
2578
2666
  }
2579
2667
  recentTasksList.innerHTML = rows.map((task) => `
2580
- <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;">
2668
+ <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;">
2581
2669
  <span style="flex:1;font-size:12px;">${task.name || task.id}</span>
2582
2670
  <span style="font-size:11px;color:var(--text-tertiary);">${task.commandType}</span>
2583
2671
  <span style="font-size:11px;color:${task.enabled ? "var(--accent-success)" : "var(--text-muted)"};">${task.enabled ? "\u542F\u7528" : "\u7981\u7528"}</span>
2584
2672
  <button class="secondary edit-task-btn" data-id="${task.id}" style="padding:2px 6px;font-size:10px;height:auto;">\u7F16\u8F91</button>
2673
+ <button class="run-task-btn" data-id="${task.id}" style="padding:2px 6px;font-size:10px;height:auto;">\u7ACB\u5373\u6267\u884C</button>
2585
2674
  </div>
2586
2675
  `).join("");
2676
+ recentTasksList.querySelectorAll(".task-item").forEach((item) => {
2677
+ item.addEventListener("dblclick", () => {
2678
+ const taskId = item.dataset.id || "";
2679
+ const task = getTaskById(taskId);
2680
+ if (!task) return;
2681
+ historySelect.value = task.id;
2682
+ applyTaskToForm(task, "edit");
2683
+ });
2684
+ });
2587
2685
  recentTasksList.querySelectorAll(".edit-task-btn").forEach((btn) => {
2588
2686
  btn.addEventListener("click", () => {
2589
2687
  const taskId = btn.dataset.id || "";
@@ -2593,6 +2691,14 @@ function renderTasksPanel(root, ctx2) {
2593
2691
  applyTaskToForm(task, "edit");
2594
2692
  });
2595
2693
  });
2694
+ recentTasksList.querySelectorAll(".run-task-btn").forEach((btn) => {
2695
+ btn.addEventListener("click", () => {
2696
+ const taskId = btn.dataset.id || "";
2697
+ const task = getTaskById(taskId);
2698
+ if (!task) return;
2699
+ void runTaskImmediately(task);
2700
+ });
2701
+ });
2596
2702
  }
2597
2703
  function updateStats() {
2598
2704
  statSaved.textContent = String(tasks.length);
@@ -2600,21 +2706,27 @@ function renderTasksPanel(root, ctx2) {
2600
2706
  const totalRunCount = tasks.reduce((sum, row) => sum + (Number(row.runCount) || 0), 0);
2601
2707
  statToday.textContent = String(totalRunCount);
2602
2708
  }
2603
- async function runJsonScript(scriptPath, args, timeoutMs = 6e4) {
2604
- const ret = await ctx2.api.cmdRunJson({
2605
- title: `task-panel ${args.join(" ")}`.trim(),
2606
- cwd: "",
2607
- args: [scriptPath, ...args, "--json"],
2608
- timeoutMs
2609
- });
2709
+ async function invokeSchedule(input) {
2710
+ if (typeof ctx2.api?.scheduleInvoke !== "function") {
2711
+ throw new Error("scheduleInvoke unavailable");
2712
+ }
2713
+ const ret = await ctx2.api.scheduleInvoke(input);
2610
2714
  if (!ret?.ok) {
2611
- const reason = String(ret?.error || ret?.stderr || ret?.stdout || "unknown_error").trim();
2612
- throw new Error(reason || "command failed");
2715
+ const reason = String(ret?.error || "schedule command failed").trim();
2716
+ throw new Error(reason || "schedule command failed");
2613
2717
  }
2614
- return ret.json || {};
2718
+ return ret?.json ?? ret;
2615
2719
  }
2616
- async function runScheduleJson(args, timeoutMs = 6e4) {
2617
- return runJsonScript(scheduleScript, args, timeoutMs);
2720
+ async function invokeTaskRunEphemeral(input) {
2721
+ if (typeof ctx2.api?.taskRunEphemeral !== "function") {
2722
+ throw new Error("taskRunEphemeral unavailable");
2723
+ }
2724
+ const ret = await ctx2.api.taskRunEphemeral(input);
2725
+ if (!ret?.ok) {
2726
+ const reason = String(ret?.error || "run ephemeral failed").trim();
2727
+ throw new Error(reason || "run ephemeral failed");
2728
+ }
2729
+ return ret;
2618
2730
  }
2619
2731
  async function loadQuotaStatus() {
2620
2732
  try {
@@ -2643,7 +2755,7 @@ function renderTasksPanel(root, ctx2) {
2643
2755
  }
2644
2756
  async function loadTasks() {
2645
2757
  try {
2646
- const out = await runScheduleJson(["list"]);
2758
+ const out = await invokeSchedule({ action: "list" });
2647
2759
  tasks = parseTaskRows(out);
2648
2760
  renderHistorySelect();
2649
2761
  renderRecentTasks();
@@ -2654,7 +2766,6 @@ function renderTasksPanel(root, ctx2) {
2654
2766
  }
2655
2767
  function buildCommandArgv(data) {
2656
2768
  const argv = {
2657
- profile: data.profileId,
2658
2769
  keyword: data.keyword,
2659
2770
  "max-notes": data.targetCount,
2660
2771
  target: data.targetCount,
@@ -2664,36 +2775,70 @@ function renderTasksPanel(root, ctx2) {
2664
2775
  "do-likes": data.doLikes,
2665
2776
  "like-keywords": data.likeKeywords
2666
2777
  };
2778
+ const profileId = String(data.profileId || "").trim();
2779
+ if (profileId) argv.profile = profileId;
2667
2780
  if (String(data.taskType || "").startsWith("weibo-")) {
2668
- argv["task-type"] = commandTypeToWeiboTaskType(data.taskType);
2669
2781
  if (data.userId) argv["user-id"] = data.userId;
2670
2782
  }
2671
2783
  return argv;
2672
2784
  }
2673
- function validateBeforeSave(data) {
2674
- if (!data.profileId) return "\u8BF7\u8F93\u5165 Profile ID";
2675
- if (isKeywordRequired(data.taskType) && !data.keyword) return "\u8BF7\u8F93\u5165\u5173\u952E\u8BCD";
2676
- if (data.taskType === "weibo-monitor" && !data.userId) return "\u5FAE\u535A monitor \u4EFB\u52A1\u9700\u8981 user-id";
2677
- if (data.scheduleType !== "interval" && !data.runAt) return `${data.scheduleType} \u4EFB\u52A1\u9700\u8981\u6267\u884C\u65F6\u95F4`;
2678
- return null;
2679
- }
2680
- function buildSaveArgs(data) {
2681
- const args = data.id ? ["update", data.id] : ["add"];
2682
- args.push("--name", data.name || fallbackTaskName(data));
2683
- args.push("--enabled", String(data.enabled));
2684
- args.push("--command-type", data.taskType || "xhs-unified");
2685
- args.push("--schedule-type", data.scheduleType);
2686
- if (data.scheduleType === "interval") {
2687
- args.push("--interval-minutes", String(data.intervalMinutes));
2688
- } else {
2689
- args.push("--run-at", String(data.runAt || ""));
2785
+ function resolveSchedule(data) {
2786
+ if (data.scheduleMode === "immediate") {
2787
+ return {
2788
+ scheduleType: "once",
2789
+ intervalMinutes: data.intervalMinutes,
2790
+ runAt: (/* @__PURE__ */ new Date()).toISOString(),
2791
+ maxRuns: 1
2792
+ };
2690
2793
  }
2691
- args.push("--max-runs", data.maxRuns === null ? "0" : String(data.maxRuns));
2692
- args.push("--argv-json", JSON.stringify(buildCommandArgv(data)));
2693
- return args;
2794
+ if (data.scheduleMode === "scheduled") {
2795
+ return {
2796
+ scheduleType: "once",
2797
+ intervalMinutes: data.intervalMinutes,
2798
+ runAt: data.runAt,
2799
+ maxRuns: 1
2800
+ };
2801
+ }
2802
+ const periodicType = data.periodicType;
2803
+ if (periodicType === "daily" || periodicType === "weekly") {
2804
+ return {
2805
+ scheduleType: periodicType,
2806
+ intervalMinutes: data.intervalMinutes,
2807
+ runAt: data.runAt,
2808
+ maxRuns: data.maxRuns
2809
+ };
2810
+ }
2811
+ return {
2812
+ scheduleType: "interval",
2813
+ intervalMinutes: data.intervalMinutes,
2814
+ runAt: null,
2815
+ maxRuns: data.maxRuns
2816
+ };
2817
+ }
2818
+ function toSchedulePayload(data) {
2819
+ const schedule = resolveSchedule(data);
2820
+ return {
2821
+ id: data.id || "",
2822
+ name: data.name || "",
2823
+ enabled: data.enabled,
2824
+ commandType: data.taskType || "xhs-unified",
2825
+ scheduleType: schedule.scheduleType,
2826
+ intervalMinutes: schedule.intervalMinutes,
2827
+ runAt: schedule.runAt,
2828
+ maxRuns: schedule.maxRuns,
2829
+ argv: buildCommandArgv(data)
2830
+ };
2831
+ }
2832
+ function taskToRunMeta(task) {
2833
+ return {
2834
+ taskType: String(task.commandType || "xhs-unified").trim() || "xhs-unified",
2835
+ profileId: String(task.commandArgv?.profile || task.commandArgv?.profileId || "").trim(),
2836
+ keyword: String(task.commandArgv?.keyword || task.commandArgv?.k || "").trim(),
2837
+ targetCount: Math.max(1, Number(task.commandArgv?.["max-notes"] ?? task.commandArgv?.target ?? 50) || 50)
2838
+ };
2694
2839
  }
2695
2840
  async function runSavedTask(taskId, data) {
2696
- const out = await runScheduleJson(["run", taskId], 0);
2841
+ const out = await invokeSchedule({ action: "run", taskId, timeoutMs: 0 });
2697
2842
  const runId = String(
2698
2843
  out?.result?.runResult?.lastRunId || out?.result?.runResult?.runId || out?.runResult?.runId || ""
2699
2844
  ).trim();
@@ -2709,23 +2854,30 @@ function renderTasksPanel(root, ctx2) {
2709
2854
  target: data.targetCount,
2710
2855
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
2711
2856
  };
2857
+ ctx2.activeRunId = runId || null;
2712
2858
  }
2713
2859
  if (typeof ctx2.setActiveTab === "function") {
2714
2860
  ctx2.setActiveTab(data.taskType === "xhs-unified" ? "dashboard" : "scheduler");
2715
2861
  }
2716
2862
  }
2863
+ async function runTaskImmediately(task) {
2864
+ const taskId = String(task.id || "").trim();
2865
+ if (!taskId) return;
2866
+ historySelect.value = taskId;
2867
+ applyTaskToForm(task, "edit");
2868
+ await runSavedTask(taskId, taskToRunMeta(task));
2869
+ }
2717
2870
  async function saveTask(runImmediately = false) {
2718
2871
  const data = collectFormData();
2719
- const invalidReason = validateBeforeSave(data);
2720
- if (invalidReason) {
2721
- alert(invalidReason);
2872
+ if (runImmediately && data.scheduleMode === "immediate") {
2873
+ await runWithoutSave();
2722
2874
  return;
2723
2875
  }
2724
2876
  saveBtn.disabled = true;
2725
2877
  runBtn.disabled = true;
2726
2878
  runEphemeralBtn.disabled = true;
2727
2879
  try {
2728
- const out = await runScheduleJson(buildSaveArgs(data));
2880
+ const out = await invokeSchedule({ action: "save", payload: toSchedulePayload(data) });
2729
2881
  const taskId = String(out?.task?.id || data.id || "").trim();
2730
2882
  if (!taskId) {
2731
2883
  throw new Error("task id missing after save");
@@ -2735,7 +2887,13 @@ function renderTasksPanel(root, ctx2) {
2735
2887
  await loadTasks();
2736
2888
  historySelect.value = taskId;
2737
2889
  if (runImmediately) {
2738
- await runSavedTask(taskId, data);
2890
+ const resolvedProfile = String(out?.task?.commandArgv?.profile || data.profileId || "").trim();
2891
+ await runSavedTask(taskId, {
2892
+ taskType: data.taskType,
2893
+ profileId: resolvedProfile,
2894
+ keyword: data.keyword,
2895
+ targetCount: data.targetCount
2896
+ });
2739
2897
  } else {
2740
2898
  alert("\u4EFB\u52A1\u5DF2\u4FDD\u5B58");
2741
2899
  }
@@ -2747,83 +2905,13 @@ function renderTasksPanel(root, ctx2) {
2747
2905
  runEphemeralBtn.disabled = false;
2748
2906
  }
2749
2907
  }
2750
- function buildEphemeralRunSpec(data) {
2751
- if (!data.profileId) return null;
2752
- if (data.taskType === "xhs-unified") {
2753
- if (!data.keyword) return null;
2754
- return {
2755
- title: `xhs: ${data.keyword}`,
2756
- groupKey: "xhs-unified",
2757
- args: [
2758
- xhsScript,
2759
- "--profile",
2760
- data.profileId,
2761
- "--keyword",
2762
- data.keyword,
2763
- "--target",
2764
- String(data.targetCount),
2765
- "--max-notes",
2766
- String(data.targetCount),
2767
- "--env",
2768
- data.env,
2769
- "--do-comments",
2770
- String(data.collectComments),
2771
- "--fetch-body",
2772
- String(data.collectBody),
2773
- "--do-likes",
2774
- String(data.doLikes),
2775
- "--like-keywords",
2776
- data.likeKeywords
2777
- ]
2778
- };
2779
- }
2780
- if (data.taskType === "weibo-search") {
2781
- if (!data.keyword) return null;
2782
- return {
2783
- title: `weibo: ${data.keyword}`,
2784
- groupKey: "weibo-search",
2785
- args: [
2786
- weiboScript,
2787
- "search",
2788
- "--profile",
2789
- data.profileId,
2790
- "--keyword",
2791
- data.keyword,
2792
- "--target",
2793
- String(data.targetCount),
2794
- "--env",
2795
- data.env
2796
- ]
2797
- };
2798
- }
2799
- return null;
2800
- }
2801
2908
  async function runWithoutSave() {
2802
2909
  const data = collectFormData();
2803
- if (!data.profileId) {
2804
- alert("\u8BF7\u8F93\u5165 Profile ID");
2805
- return;
2806
- }
2807
- if (isKeywordRequired(data.taskType) && !data.keyword) {
2808
- alert("\u8BF7\u8F93\u5165\u5173\u952E\u8BCD");
2809
- return;
2810
- }
2811
- if (data.taskType === "weibo-monitor" && !data.userId) {
2812
- alert("\u5FAE\u535A monitor \u4EFB\u52A1\u9700\u8981 user-id");
2813
- return;
2814
- }
2815
- const spec = buildEphemeralRunSpec(data);
2816
- if (!spec) {
2817
- alert(`\u5F53\u524D\u4EFB\u52A1\u7C7B\u578B\u6682\u4E0D\u652F\u6301\u4EC5\u6267\u884C(\u4E0D\u4FDD\u5B58): ${data.taskType}`);
2818
- return;
2819
- }
2820
2910
  runEphemeralBtn.disabled = true;
2821
2911
  try {
2822
- const ret = await ctx2.api.cmdSpawn({
2823
- title: spec.title,
2824
- cwd: "",
2825
- args: spec.args,
2826
- groupKey: spec.groupKey
2912
+ const ret = await invokeTaskRunEphemeral({
2913
+ commandType: data.taskType,
2914
+ argv: buildCommandArgv(data)
2827
2915
  });
2828
2916
  const runId = String(ret?.runId || "").trim();
2829
2917
  if (runId) {
@@ -2831,17 +2919,19 @@ function renderTasksPanel(root, ctx2) {
2831
2919
  updateStats();
2832
2920
  }
2833
2921
  if (typeof ctx2.setStatus === "function") {
2834
- ctx2.setStatus(`started: ${spec.title}`);
2922
+ ctx2.setStatus(`started: ${data.taskType}`);
2835
2923
  }
2836
2924
  if (data.taskType === "xhs-unified" && ctx2 && typeof ctx2 === "object") {
2925
+ const resolvedProfile = String(ret?.profile || data.profileId || "").trim();
2837
2926
  ctx2.xhsCurrentRun = {
2838
2927
  runId: runId || null,
2839
2928
  taskId: null,
2840
- profileId: data.profileId,
2929
+ profileId: resolvedProfile,
2841
2930
  keyword: data.keyword,
2842
2931
  target: data.targetCount,
2843
2932
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
2844
2933
  };
2934
+ ctx2.activeRunId = runId || null;
2845
2935
  }
2846
2936
  if (typeof ctx2.setActiveTab === "function") {
2847
2937
  ctx2.setActiveTab(data.taskType === "xhs-unified" ? "dashboard" : "scheduler");
@@ -2874,6 +2964,7 @@ function renderTasksPanel(root, ctx2) {
2874
2964
  });
2875
2965
  taskTypeSelect.addEventListener("change", () => updatePlatformFields());
2876
2966
  scheduleTypeSelect.addEventListener("change", () => updateScheduleVisibility());
2967
+ periodicTypeSelect.addEventListener("change", () => updateScheduleVisibility());
2877
2968
  likesInput.addEventListener("change", () => updateLikeKeywordsState());
2878
2969
  saveBtn.addEventListener("click", () => {
2879
2970
  void saveTask(false);
@@ -2907,6 +2998,14 @@ function renderTasksPanel(root, ctx2) {
2907
2998
  }
2908
2999
  applyTaskToForm(task, "clone");
2909
3000
  });
3001
+ historyRunBtn.addEventListener("click", () => {
3002
+ const task = selectedHistoryTask();
3003
+ if (!task) {
3004
+ alert("\u8BF7\u5148\u9009\u62E9\u5386\u53F2\u4EFB\u52A1");
3005
+ return;
3006
+ }
3007
+ void runTaskImmediately(task);
3008
+ });
2910
3009
  gotoSchedulerBtn.addEventListener("click", () => {
2911
3010
  if (typeof ctx2.setActiveTab === "function") {
2912
3011
  ctx2.setActiveTab("scheduler");
@@ -2994,6 +3093,11 @@ function renderDashboard(root, ctx2) {
2994
3093
  <div id="recent-errors-empty" class="muted" style="font-size: 12px;">\u6682\u65E0\u9519\u8BEF</div>
2995
3094
  <ul id="recent-errors-list" style="margin: 6px 0 0 16px; padding: 0; font-size: 12px; line-height: 1.5; display:none;"></ul>
2996
3095
  </div>
3096
+ <div style="margin-top: 10px;">
3097
+ <label>\u70B9\u8D5E\u94FE\u63A5\uFF08\u6700\u591A 30 \u6761\uFF09</label>
3098
+ <div id="liked-links-empty" class="muted" style="font-size: 12px;">\u6682\u65E0\u70B9\u8D5E\u8BB0\u5F55</div>
3099
+ <ul id="liked-links-list" style="margin: 6px 0 0 16px; padding: 0; font-size: 12px; line-height: 1.5; display:none;"></ul>
3100
+ </div>
2997
3101
  `;
2998
3102
  runSummaryGrid.appendChild(runSummaryCard);
2999
3103
  root.appendChild(runSummaryGrid);
@@ -3088,6 +3192,8 @@ function renderDashboard(root, ctx2) {
3088
3192
  const errorCountText = root.querySelector("#error-count-text");
3089
3193
  const recentErrorsEmpty = root.querySelector("#recent-errors-empty");
3090
3194
  const recentErrorsList = root.querySelector("#recent-errors-list");
3195
+ const likedLinksEmpty = root.querySelector("#liked-links-empty");
3196
+ const likedLinksList = root.querySelector("#liked-links-list");
3091
3197
  const logsContainer = root.querySelector("#logs-container");
3092
3198
  const toggleLogsBtn = root.querySelector("#toggle-logs-btn");
3093
3199
  const pauseBtn = root.querySelector("#pause-btn");
@@ -3104,23 +3210,175 @@ function renderDashboard(root, ctx2) {
3104
3210
  let startTime = Date.now();
3105
3211
  let stoppedAt = null;
3106
3212
  let elapsedTimer = null;
3213
+ let statePollTimer = null;
3107
3214
  let unsubscribeState = null;
3108
3215
  let unsubscribeCmd = null;
3109
- let activeRunId = String(ctx2?.xhsCurrentRun?.runId || "").trim();
3216
+ const contextRun = ctx2?.xhsCurrentRun && typeof ctx2.xhsCurrentRun === "object" ? ctx2.xhsCurrentRun : null;
3217
+ const contextStartedAtMs = Date.parse(String(contextRun?.startedAt || ""));
3218
+ let activeRunId = String(contextRun?.runId || ctx2?.activeRunId || "").trim();
3219
+ let activeProfileId = String(contextRun?.profileId || "").trim();
3110
3220
  let activeStatus = "";
3111
3221
  let errorCountTotal = 0;
3112
3222
  const recentErrors = [];
3223
+ const likedLinks = /* @__PURE__ */ new Map();
3113
3224
  const maxLogs = 500;
3114
3225
  const maxRecentErrors = 8;
3115
- const initialTaskId = String(ctx2?.xhsCurrentRun?.taskId || ctx2?.activeTaskConfigId || "").trim();
3226
+ const maxLikedLinks = 30;
3227
+ const initialTaskId = String(contextRun?.taskId || ctx2?.activeTaskConfigId || "").trim();
3116
3228
  if (initialTaskId) {
3117
3229
  taskConfigId.textContent = initialTaskId;
3118
3230
  }
3119
3231
  const normalizeStatus = (value) => String(value || "").trim().toLowerCase();
3120
3232
  const isRunningStatus = (value) => ["running", "queued", "pending", "starting"].includes(normalizeStatus(value));
3121
3233
  const isTerminalStatus = (value) => ["completed", "done", "success", "succeeded", "failed", "error", "stopped", "canceled"].includes(normalizeStatus(value));
3234
+ const isXhsCommandTitle = (title) => {
3235
+ const normalized = String(title || "").trim().toLowerCase();
3236
+ if (!normalized) return false;
3237
+ return normalized.includes("xhs unified") || normalized.startsWith("xhs:") || normalized.startsWith("xhs unified:");
3238
+ };
3239
+ const hasRenderableValue = (value) => {
3240
+ const text = String(value ?? "").trim();
3241
+ return text.length > 0 && text !== "-";
3242
+ };
3243
+ if (contextRun) {
3244
+ if (hasRenderableValue(contextRun.keyword)) taskKeyword.textContent = String(contextRun.keyword);
3245
+ if (Number(contextRun.target) > 0) taskTarget.textContent = String(Number(contextRun.target));
3246
+ if (hasRenderableValue(contextRun.profileId)) {
3247
+ const aliases = ctx2.api?.settings?.profileAliases || {};
3248
+ const profileId = String(contextRun.profileId);
3249
+ taskAccount.textContent = aliases[profileId] || profileId;
3250
+ activeProfileId = profileId;
3251
+ }
3252
+ if (hasRenderableValue(contextRun.taskId)) taskConfigId.textContent = String(contextRun.taskId);
3253
+ const startedAtTs = Date.parse(String(contextRun.startedAt || ""));
3254
+ if (Number.isFinite(startedAtTs) && startedAtTs > 0) {
3255
+ startTime = startedAtTs;
3256
+ updateElapsed();
3257
+ }
3258
+ }
3259
+ function normalizeDetails(details) {
3260
+ if (details === void 0 || details === null) return null;
3261
+ try {
3262
+ const text = typeof details === "string" ? details : JSON.stringify(details, null, 2);
3263
+ const trimmed = String(text || "").trim();
3264
+ if (!trimmed) return null;
3265
+ return trimmed.length > 2e3 ? `${trimmed.slice(0, 2e3)}
3266
+ ...` : trimmed;
3267
+ } catch {
3268
+ return String(details || "").trim() || null;
3269
+ }
3270
+ }
3271
+ function normalizeNoteId(value) {
3272
+ const text = String(value || "").trim();
3273
+ if (!text) return null;
3274
+ if (/^[a-zA-Z0-9_-]{6,}$/.test(text)) return text;
3275
+ return null;
3276
+ }
3277
+ function normalizeLink(urlLike, noteIdLike) {
3278
+ const rawUrl = String(urlLike || "").trim();
3279
+ const noteId = normalizeNoteId(noteIdLike);
3280
+ if (rawUrl) {
3281
+ if (/^https?:\/\//i.test(rawUrl)) return { url: rawUrl, noteId };
3282
+ if (rawUrl.startsWith("/")) return { url: `https://www.xiaohongshu.com${rawUrl}`, noteId };
3283
+ if (/^[a-zA-Z0-9_-]{6,}$/.test(rawUrl)) {
3284
+ return { url: `https://www.xiaohongshu.com/explore/${rawUrl}`, noteId: noteId || rawUrl };
3285
+ }
3286
+ }
3287
+ if (noteId) {
3288
+ return { url: `https://www.xiaohongshu.com/explore/${noteId}`, noteId };
3289
+ }
3290
+ return null;
3291
+ }
3292
+ function pushLikedLink(entry) {
3293
+ const url = String(entry.url || "").trim();
3294
+ if (!url) return;
3295
+ const previous = likedLinks.get(url);
3296
+ likedLinks.set(url, {
3297
+ url,
3298
+ noteId: entry.noteId || previous?.noteId || null,
3299
+ source: entry.source || previous?.source || "comment_like",
3300
+ profileId: entry.profileId || previous?.profileId || activeProfileId || null,
3301
+ ts: entry.ts || previous?.ts || (/* @__PURE__ */ new Date()).toLocaleTimeString("zh-CN", { hour12: false }),
3302
+ count: (previous?.count || 0) + 1
3303
+ });
3304
+ const keys = Array.from(likedLinks.keys());
3305
+ if (keys.length > maxLikedLinks) {
3306
+ likedLinks.delete(keys[0]);
3307
+ }
3308
+ }
3309
+ function renderLikedLinks() {
3310
+ likedLinksList.innerHTML = "";
3311
+ const entries = Array.from(likedLinks.values());
3312
+ if (entries.length === 0) {
3313
+ likedLinksEmpty.style.display = "block";
3314
+ likedLinksList.style.display = "none";
3315
+ return;
3316
+ }
3317
+ likedLinksEmpty.style.display = "none";
3318
+ likedLinksList.style.display = "block";
3319
+ for (const item of entries) {
3320
+ const li = document.createElement("li");
3321
+ const wrap = document.createElement("div");
3322
+ wrap.style.display = "flex";
3323
+ wrap.style.alignItems = "center";
3324
+ wrap.style.gap = "6px";
3325
+ wrap.style.flexWrap = "wrap";
3326
+ const label = document.createElement("span");
3327
+ label.textContent = `[${item.ts}]`;
3328
+ wrap.appendChild(label);
3329
+ const link = document.createElement("a");
3330
+ link.href = "#";
3331
+ link.textContent = item.noteId ? `note:${item.noteId}` : "\u6253\u5F00\u94FE\u63A5";
3332
+ link.onclick = (evt) => {
3333
+ evt.preventDefault();
3334
+ void openLikedLink(item.url, item.profileId || activeProfileId || null);
3335
+ };
3336
+ wrap.appendChild(link);
3337
+ const hint = document.createElement("span");
3338
+ hint.style.color = "var(--text-4)";
3339
+ hint.textContent = `(${item.source}${item.count > 1 ? ` x${item.count}` : ""})`;
3340
+ wrap.appendChild(hint);
3341
+ li.appendChild(wrap);
3342
+ likedLinksList.appendChild(li);
3343
+ }
3344
+ }
3345
+ async function openLikedLink(url, profileId) {
3346
+ const targetUrl = String(url || "").trim();
3347
+ if (!targetUrl) return;
3348
+ const pid = String(profileId || "").trim();
3349
+ try {
3350
+ if (pid && typeof ctx2.api?.cmdRunJson === "function") {
3351
+ const ret = await ctx2.api.cmdRunJson({
3352
+ title: `goto ${pid}`,
3353
+ cwd: "",
3354
+ args: [
3355
+ ctx2.api.pathJoin("apps", "webauto", "entry", "profilepool.mjs"),
3356
+ "goto-profile",
3357
+ pid,
3358
+ "--url",
3359
+ targetUrl,
3360
+ "--json"
3361
+ ],
3362
+ timeoutMs: 3e4
3363
+ });
3364
+ if (ret?.ok) {
3365
+ addLog(`\u5DF2\u5728 ${pid} \u6253\u5F00\u70B9\u8D5E\u94FE\u63A5`, "info");
3366
+ return;
3367
+ }
3368
+ }
3369
+ if (typeof ctx2.api?.osOpenPath === "function") {
3370
+ await ctx2.api.osOpenPath(targetUrl);
3371
+ addLog("\u5DF2\u901A\u8FC7\u7CFB\u7EDF\u6253\u5F00\u70B9\u8D5E\u94FE\u63A5", "warn");
3372
+ }
3373
+ } catch (err) {
3374
+ pushRecentError("\u70B9\u8D5E\u94FE\u63A5\u6253\u5F00\u5931\u8D25", "like_link", err?.message || String(err));
3375
+ }
3376
+ }
3122
3377
  function renderRunSummary() {
3123
3378
  runIdText.textContent = activeRunId || "-";
3379
+ if (ctx2 && typeof ctx2 === "object") {
3380
+ ctx2.activeRunId = activeRunId || null;
3381
+ }
3124
3382
  errorCountText.textContent = String(errorCountTotal);
3125
3383
  recentErrorsList.innerHTML = "";
3126
3384
  if (recentErrors.length === 0) {
@@ -3132,18 +3390,35 @@ function renderDashboard(root, ctx2) {
3132
3390
  recentErrorsList.style.display = "block";
3133
3391
  recentErrors.forEach((item) => {
3134
3392
  const li = document.createElement("li");
3135
- li.textContent = `[${item.ts}] ${item.source}: ${item.message}`;
3393
+ const line = document.createElement("div");
3394
+ line.textContent = `[${item.ts}] ${item.source}: ${item.message}`;
3395
+ li.appendChild(line);
3396
+ if (item.details) {
3397
+ const details = document.createElement("details");
3398
+ const summary = document.createElement("summary");
3399
+ summary.textContent = "\u8BE6\u60C5";
3400
+ const pre = document.createElement("pre");
3401
+ pre.style.margin = "4px 0 0 0";
3402
+ pre.style.whiteSpace = "pre-wrap";
3403
+ pre.style.wordBreak = "break-word";
3404
+ pre.textContent = item.details;
3405
+ details.appendChild(summary);
3406
+ details.appendChild(pre);
3407
+ li.appendChild(details);
3408
+ }
3136
3409
  recentErrorsList.appendChild(li);
3137
3410
  });
3411
+ renderLikedLinks();
3138
3412
  }
3139
- function pushRecentError(message, source = "runtime") {
3413
+ function pushRecentError(message, source = "runtime", details = null) {
3140
3414
  const msg = String(message || "").trim();
3141
3415
  if (!msg) return;
3142
3416
  errorCountTotal += 1;
3143
3417
  recentErrors.push({
3144
3418
  ts: (/* @__PURE__ */ new Date()).toLocaleTimeString("zh-CN", { hour12: false }),
3145
3419
  source: String(source || "runtime").trim() || "runtime",
3146
- message: msg
3420
+ message: msg,
3421
+ details: normalizeDetails(details)
3147
3422
  });
3148
3423
  while (recentErrors.length > maxRecentErrors) recentErrors.shift();
3149
3424
  renderRunSummary();
@@ -3165,6 +3440,19 @@ function renderDashboard(root, ctx2) {
3165
3440
  clearInterval(elapsedTimer);
3166
3441
  elapsedTimer = null;
3167
3442
  }
3443
+ function startStatePoll() {
3444
+ if (statePollTimer) return;
3445
+ if (typeof ctx2.api?.stateGetTasks !== "function") return;
3446
+ statePollTimer = setInterval(() => {
3447
+ if (paused) return;
3448
+ void fetchCurrentState();
3449
+ }, 5e3);
3450
+ }
3451
+ function stopStatePoll() {
3452
+ if (!statePollTimer) return;
3453
+ clearInterval(statePollTimer);
3454
+ statePollTimer = null;
3455
+ }
3168
3456
  function addLog(line, type = "info") {
3169
3457
  const ts = (/* @__PURE__ */ new Date()).toLocaleTimeString("zh-CN", { hour12: false });
3170
3458
  const logLine = createEl("div", { className: "log-line" });
@@ -3177,8 +3465,39 @@ function renderDashboard(root, ctx2) {
3177
3465
  logsContainer.scrollTop = logsContainer.scrollHeight;
3178
3466
  }
3179
3467
  }
3468
+ function resetDashboardForNewRun(reason, startedAtMs) {
3469
+ commentsCount = 0;
3470
+ likesCount = 0;
3471
+ likesSkippedCount = 0;
3472
+ likesAlreadyCount = 0;
3473
+ likesDedupCount = 0;
3474
+ errorCountTotal = 0;
3475
+ recentErrors.length = 0;
3476
+ likedLinks.clear();
3477
+ logsContainer.innerHTML = "";
3478
+ statCollected.textContent = "0";
3479
+ statSuccess.textContent = "0";
3480
+ statFailed.textContent = "0";
3481
+ statRemaining.textContent = "0";
3482
+ statComments.textContent = "0\u6761";
3483
+ statLikes.textContent = "0\u6B21 (\u8DF3\u8FC7:0, \u5DF2\u8D5E:0, \u53BB\u91CD:0)";
3484
+ progressPercent.textContent = "0%";
3485
+ progressBar.style.width = "0%";
3486
+ currentAction.textContent = reason || "-";
3487
+ currentPhase.textContent = "\u8FD0\u884C\u4E2D";
3488
+ startTime = Number.isFinite(Number(startedAtMs)) && Number(startedAtMs) > 0 ? Number(startedAtMs) : Date.now();
3489
+ stoppedAt = null;
3490
+ updateElapsed();
3491
+ startElapsedTimer();
3492
+ renderRunSummary();
3493
+ }
3180
3494
  function updateFromTaskState(state) {
3181
3495
  if (!state) return;
3496
+ const incomingRunId = String(state.runId || "").trim();
3497
+ if (incomingRunId && activeRunId && incomingRunId !== activeRunId && (isTerminalStatus(activeStatus) || !isRunningStatus(activeStatus)) && isRunningStatus(state.status)) {
3498
+ activeRunId = incomingRunId;
3499
+ resetDashboardForNewRun("\u5207\u6362\u5230\u65B0\u4EFB\u52A1");
3500
+ }
3182
3501
  const progressObj = state.progress && typeof state.progress === "object" ? state.progress : null;
3183
3502
  const processedRaw = progressObj?.processed ?? progressObj?.current ?? state.progress ?? state.collected ?? state.current ?? 0;
3184
3503
  const totalRaw = progressObj?.total ?? state.total ?? state.target ?? state.maxNotes ?? 0;
@@ -3231,6 +3550,7 @@ function renderDashboard(root, ctx2) {
3231
3550
  if (state.profileId) {
3232
3551
  const aliases = ctx2.api?.settings?.profileAliases || {};
3233
3552
  taskAccount.textContent = aliases[state.profileId] || state.profileId;
3553
+ activeProfileId = String(state.profileId || "").trim();
3234
3554
  }
3235
3555
  const taskId = String(state.taskId || state.scheduleTaskId || state.configTaskId || "").trim();
3236
3556
  if (taskId) {
@@ -3272,26 +3592,32 @@ function renderDashboard(root, ctx2) {
3272
3592
  }
3273
3593
  }
3274
3594
  if (state.error) {
3275
- pushRecentError(String(state.error), "state");
3595
+ pushRecentError(String(state.error), "state", state);
3276
3596
  }
3277
3597
  }
3278
3598
  function pickTaskFromList(tasks) {
3279
3599
  const target = activeRunId;
3280
- const running = tasks.find((item) => isRunningStatus(item?.status));
3281
3600
  const sorted = [...tasks].sort((a, b) => {
3282
3601
  const aTs = Number(a?.updatedAt ?? a?.completedAt ?? a?.startedAt ?? 0) || 0;
3283
3602
  const bTs = Number(b?.updatedAt ?? b?.completedAt ?? b?.startedAt ?? 0) || 0;
3284
3603
  return bTs - aTs;
3285
3604
  });
3605
+ const running = sorted.find((item) => isRunningStatus(item?.status)) || null;
3286
3606
  const latest = sorted[0] || null;
3607
+ const launchingFresh = Number.isFinite(contextStartedAtMs) && contextStartedAtMs > 0 && Date.now() - contextStartedAtMs < 12e4;
3287
3608
  if (target) {
3288
3609
  const matched = tasks.find((item) => String(item?.runId || "").trim() === target);
3289
3610
  if (matched) {
3290
- if (isRunningStatus(matched?.status)) return matched;
3291
- if (running) return running;
3292
- if (latest && String(latest?.runId || "").trim() !== target) return latest;
3293
3611
  return matched;
3294
3612
  }
3613
+ if (launchingFresh) {
3614
+ return null;
3615
+ }
3616
+ if (running) return running;
3617
+ return null;
3618
+ }
3619
+ if (launchingFresh) {
3620
+ return running || null;
3295
3621
  }
3296
3622
  return running || latest || null;
3297
3623
  }
@@ -3302,29 +3628,14 @@ function renderDashboard(root, ctx2) {
3302
3628
  currentPhase.textContent = "\u8FD0\u884C\u4E2D";
3303
3629
  currentAction.textContent = "\u542F\u52A8 autoscript";
3304
3630
  activeStatus = "running";
3305
- statCollected.textContent = "0";
3306
- statSuccess.textContent = "0";
3307
- statFailed.textContent = "0";
3308
- statRemaining.textContent = "0";
3309
- progressPercent.textContent = "0%";
3310
- progressBar.style.width = "0%";
3311
- commentsCount = 0;
3312
- likesCount = 0;
3313
- likesSkippedCount = 0;
3314
- likesAlreadyCount = 0;
3315
- likesDedupCount = 0;
3316
- statComments.textContent = `0\u6761`;
3317
- statLikes.textContent = `0\u6B21 (\u8DF3\u8FC7:0, \u5DF2\u8D5E:0, \u53BB\u91CD:0)`;
3318
3631
  const ts = Date.parse(String(payload.ts || "")) || Date.now();
3319
- startTime = ts;
3320
- stoppedAt = null;
3321
- updateElapsed();
3322
- startElapsedTimer();
3323
3632
  if (payload.runId) {
3324
3633
  activeRunId = String(payload.runId || "").trim() || activeRunId;
3325
3634
  }
3635
+ resetDashboardForNewRun("\u65B0\u4EFB\u52A1\u542F\u52A8", ts);
3326
3636
  if (payload.keyword) taskKeyword.textContent = String(payload.keyword);
3327
3637
  if (payload.maxNotes) taskTarget.textContent = String(payload.maxNotes);
3638
+ if (payload.profileId) activeProfileId = String(payload.profileId || "").trim();
3328
3639
  if (payload.taskId) {
3329
3640
  const taskId = String(payload.taskId || "").trim();
3330
3641
  if (taskId) {
@@ -3373,6 +3684,23 @@ function renderDashboard(root, ctx2) {
3373
3684
  likesAlreadyCount = Math.max(0, likesAlreadyCount + already);
3374
3685
  likesDedupCount = Math.max(0, likesDedupCount + dedup);
3375
3686
  statLikes.textContent = `${likesCount}\u6B21 (\u8DF3\u8FC7:${likesSkippedCount}, \u5DF2\u8D5E:${likesAlreadyCount}, \u53BB\u91CD:${likesDedupCount})`;
3687
+ const candidates = [];
3688
+ const direct = normalizeLink(opResult?.noteUrl || opResult?.url || opResult?.href || opResult?.link, opResult?.noteId);
3689
+ if (direct) candidates.push({ ...direct, source: "comment_like" });
3690
+ const likedComments = Array.isArray(opResult?.likedComments) ? opResult.likedComments : [];
3691
+ for (const row of likedComments) {
3692
+ const item = normalizeLink(row?.noteUrl || row?.url || row?.href || row?.link, row?.noteId || opResult?.noteId);
3693
+ if (!item) continue;
3694
+ candidates.push({ ...item, source: "liked_comment" });
3695
+ }
3696
+ for (const item of candidates) {
3697
+ pushLikedLink({
3698
+ ...item,
3699
+ profileId: activeProfileId || null,
3700
+ ts: (/* @__PURE__ */ new Date()).toLocaleTimeString("zh-CN", { hour12: false })
3701
+ });
3702
+ }
3703
+ renderRunSummary();
3376
3704
  }
3377
3705
  return;
3378
3706
  }
@@ -3381,7 +3709,7 @@ function renderDashboard(root, ctx2) {
3381
3709
  statFailed.textContent = String(failed + 1);
3382
3710
  const opId = String(payload?.operationId || "").trim();
3383
3711
  const err = String(payload?.error || payload?.message || payload?.code || event).trim();
3384
- pushRecentError(opId ? `${opId}: ${err}` : err, event);
3712
+ pushRecentError(opId ? `${opId}: ${err}` : err, event, payload);
3385
3713
  return;
3386
3714
  }
3387
3715
  if (event === "xhs.unified.merged") {
@@ -3403,7 +3731,7 @@ function renderDashboard(root, ctx2) {
3403
3731
  currentPhase.textContent = reason && reason !== "script_failure" ? "\u5DF2\u7ED3\u675F" : "\u5931\u8D25";
3404
3732
  currentAction.textContent = reason || "stop";
3405
3733
  if (reason && !successReasons.has(reason)) {
3406
- pushRecentError(`stop reason=${reason}`, event);
3734
+ pushRecentError(`stop reason=${reason}`, event, payload);
3407
3735
  }
3408
3736
  renderRunSummary();
3409
3737
  }
@@ -3461,6 +3789,7 @@ function renderDashboard(root, ctx2) {
3461
3789
  if (profile?.profileId) {
3462
3790
  const aliases = ctx2.api?.settings?.profileAliases || {};
3463
3791
  taskAccount.textContent = aliases[profile.profileId] || profile.profileId;
3792
+ activeProfileId = String(profile.profileId || "").trim();
3464
3793
  }
3465
3794
  const runId = String(profile?.runId || summary?.runId || "").trim();
3466
3795
  if (runId) {
@@ -3536,7 +3865,7 @@ function renderDashboard(root, ctx2) {
3536
3865
  if (payload.action) addLog(String(payload.action), "info");
3537
3866
  if (payload.error) {
3538
3867
  addLog(String(payload.error), "error");
3539
- pushRecentError(String(payload.error), "state");
3868
+ pushRecentError(String(payload.error), "state", payload);
3540
3869
  }
3541
3870
  }
3542
3871
  });
@@ -3553,10 +3882,12 @@ function renderDashboard(root, ctx2) {
3553
3882
  unsubscribeCmd = ctx2.api.onCmdEvent((evt) => {
3554
3883
  if (paused) return;
3555
3884
  const runId = String(evt?.runId || "").trim();
3556
- if ((isTerminalStatus(activeStatus) || !activeRunId) && evt?.type === "started" && String(evt?.title || "").includes("xhs unified")) {
3885
+ const preferredRunId = String(ctx2?.activeRunId || "").trim();
3886
+ const shouldAdoptStartedRun = evt?.type === "started" && runId && isXhsCommandTitle(evt?.title) && (!activeRunId || isTerminalStatus(activeStatus) || preferredRunId && preferredRunId === runId);
3887
+ if (shouldAdoptStartedRun) {
3557
3888
  activeRunId = runId;
3558
3889
  activeStatus = "running";
3559
- stoppedAt = null;
3890
+ resetDashboardForNewRun("\u8FDB\u7A0B\u542F\u52A8");
3560
3891
  renderRunSummary();
3561
3892
  }
3562
3893
  if (activeRunId && runId && runId !== activeRunId) return;
@@ -3565,7 +3896,7 @@ function renderDashboard(root, ctx2) {
3565
3896
  parseLineEvent(String(evt.line || "").trim());
3566
3897
  } else if (evt.type === "stderr") {
3567
3898
  addLog(evt.line, "error");
3568
- pushRecentError(String(evt.line || ""), "stderr");
3899
+ pushRecentError(String(evt.line || ""), "stderr", evt);
3569
3900
  const failed = Number(statFailed.textContent || "0") || 0;
3570
3901
  statFailed.textContent = String(failed + 1);
3571
3902
  } else if (evt.type === "exit") {
@@ -3580,7 +3911,7 @@ function renderDashboard(root, ctx2) {
3580
3911
  stopElapsedTimer();
3581
3912
  }
3582
3913
  if (Number(evt.exitCode || 0) !== 0) {
3583
- pushRecentError(`\u8FDB\u7A0B\u9000\u51FA code=${evt.exitCode ?? "null"}`, "exit");
3914
+ pushRecentError(`\u8FDB\u7A0B\u9000\u51FA code=${evt.exitCode ?? "null"}`, "exit", evt);
3584
3915
  }
3585
3916
  renderRunSummary();
3586
3917
  }
@@ -3591,13 +3922,17 @@ function renderDashboard(root, ctx2) {
3591
3922
  try {
3592
3923
  const config = await ctx2.api.configLoadLast();
3593
3924
  if (config) {
3594
- taskKeyword.textContent = config.keyword || "-";
3595
- taskTarget.textContent = String(config.target || 50);
3596
- if (config.lastProfileId) {
3925
+ if (!hasRenderableValue(contextRun?.keyword)) {
3926
+ taskKeyword.textContent = config.keyword || "-";
3927
+ }
3928
+ if (!(Number(contextRun?.target) > 0)) {
3929
+ taskTarget.textContent = String(config.target || 50);
3930
+ }
3931
+ if (!hasRenderableValue(contextRun?.profileId) && config.lastProfileId) {
3597
3932
  const aliases = ctx2.api?.settings?.profileAliases || {};
3598
3933
  taskAccount.textContent = aliases[config.lastProfileId] || config.lastProfileId;
3599
3934
  }
3600
- const taskId = String(config.taskId || ctx2?.activeTaskConfigId || "").trim();
3935
+ const taskId = String(contextRun?.taskId || config.taskId || ctx2?.activeTaskConfigId || "").trim();
3601
3936
  if (taskId) {
3602
3937
  taskConfigId.textContent = taskId;
3603
3938
  }
@@ -3661,23 +3996,25 @@ function renderDashboard(root, ctx2) {
3661
3996
  }
3662
3997
  setTimeout(() => {
3663
3998
  if (typeof ctx2.setActiveTab === "function") {
3664
- ctx2.setActiveTab("config");
3999
+ ctx2.setActiveTab("tasks");
3665
4000
  }
3666
4001
  }, 1500);
3667
4002
  }
3668
4003
  };
3669
4004
  backConfigBtn.onclick = () => {
3670
4005
  if (typeof ctx2.setActiveTab === "function") {
3671
- ctx2.setActiveTab("config");
4006
+ ctx2.setActiveTab("tasks");
3672
4007
  }
3673
4008
  };
3674
4009
  renderRunSummary();
3675
4010
  loadTaskInfo();
3676
4011
  subscribeToUpdates();
3677
4012
  fetchCurrentState();
4013
+ startStatePoll();
3678
4014
  startElapsedTimer();
3679
4015
  return () => {
3680
4016
  stopElapsedTimer();
4017
+ stopStatePoll();
3681
4018
  if (unsubscribeState) unsubscribeState();
3682
4019
  if (unsubscribeCmd) unsubscribeCmd();
3683
4020
  if (unsubscribeBus) unsubscribeBus();
@@ -3725,6 +4062,14 @@ function toTimestamp(value) {
3725
4062
  if (!Number.isFinite(parsed)) return null;
3726
4063
  return parsed;
3727
4064
  }
4065
+ function formatProfileTag2(profileId) {
4066
+ const id = String(profileId || "").trim();
4067
+ const m = id.match(/^profile-(\d+)$/i);
4068
+ if (!m) return id;
4069
+ const seq = Number(m[1]);
4070
+ if (!Number.isFinite(seq)) return id;
4071
+ return `P${String(seq).padStart(3, "0")}`;
4072
+ }
3728
4073
  function renderAccountManager(root, ctx2) {
3729
4074
  root.innerHTML = "";
3730
4075
  const autoSyncTimers = /* @__PURE__ */ new Map();
@@ -3747,7 +4092,7 @@ function renderAccountManager(root, ctx2) {
3747
4092
  </div>
3748
4093
  <div class="env-item" id="env-firefox">
3749
4094
  <span class="icon" style="color: var(--text-4);">\u25CB</span>
3750
- <span>Camoufox Runtime (python -m camoufox)</span>
4095
+ <span>\u6D4F\u89C8\u5668\u5185\u6838\uFF08Camoufox Firefox\uFF09</span>
3751
4096
  </div>
3752
4097
  </div>
3753
4098
  <div class="btn-group" style="margin-top: var(--gap);">
@@ -3790,15 +4135,18 @@ function renderAccountManager(root, ctx2) {
3790
4135
  let busUnsubscribe = null;
3791
4136
  async function checkEnvironment() {
3792
4137
  try {
3793
- const [camo, services, firefox] = await Promise.all([
3794
- ctx2.api.envCheckCamo(),
3795
- ctx2.api.envCheckServices(),
3796
- ctx2.api.envCheckFirefox()
3797
- ]);
3798
- updateEnvItem("env-camo", camo.installed);
3799
- updateEnvItem("env-unified", services.unifiedApi);
3800
- updateEnvItem("env-browser", services.camoRuntime);
3801
- updateEnvItem("env-firefox", firefox.installed);
4138
+ if (typeof ctx2.api?.envCheckAll !== "function") {
4139
+ throw new Error("envCheckAll unavailable");
4140
+ }
4141
+ const unified = await ctx2.api.envCheckAll();
4142
+ if (!unified || typeof unified !== "object") {
4143
+ throw new Error("invalid envCheckAll response");
4144
+ }
4145
+ const browserReady = Boolean(unified.browserReady);
4146
+ updateEnvItem("env-camo", Boolean(unified.camo?.installed));
4147
+ updateEnvItem("env-unified", Boolean(unified.services?.unifiedApi));
4148
+ updateEnvItem("env-browser", Boolean(unified.services?.camoRuntime));
4149
+ updateEnvItem("env-firefox", browserReady);
3802
4150
  } catch (err) {
3803
4151
  console.error("Environment check failed:", err);
3804
4152
  }
@@ -3812,6 +4160,18 @@ function renderAccountManager(root, ctx2) {
3812
4160
  envCheckInFlight = false;
3813
4161
  }
3814
4162
  }
4163
+ async function cleanupEnvironment() {
4164
+ try {
4165
+ console.log("[account-manager] Starting environment cleanup...");
4166
+ const result = await ctx2.api.envCleanup();
4167
+ console.log("[account-manager] Cleanup result:", result);
4168
+ alert("\u73AF\u5883\u6E05\u7406\u5B8C\u6210\uFF01\\n" + JSON.stringify(result, null, 2));
4169
+ await checkEnvironment();
4170
+ } catch (err) {
4171
+ console.error("Environment cleanup failed:", err);
4172
+ alert("\u73AF\u5883\u6E05\u7406\u5931\u8D25\uFF1A" + err.message);
4173
+ }
4174
+ }
3815
4175
  function updateEnvItem(id, ok) {
3816
4176
  const el = root.querySelector(`#${id}`);
3817
4177
  if (!el) return;
@@ -3827,7 +4187,11 @@ function renderAccountManager(root, ctx2) {
3827
4187
  platform: normalizePlatform(row.platform),
3828
4188
  statusView: row.valid ? "valid" : row.status === "pending" ? "pending" : "expired",
3829
4189
  lastCheckAt: toTimestamp(row.updatedAt)
3830
- }));
4190
+ })).sort((a, b) => {
4191
+ const p = String(a.profileId || "").localeCompare(String(b.profileId || ""));
4192
+ if (p !== 0) return p;
4193
+ return String(a.platform || "").localeCompare(String(b.platform || ""));
4194
+ });
3831
4195
  renderAccountList();
3832
4196
  } catch (err) {
3833
4197
  console.error("Failed to load accounts:", err);
@@ -3861,11 +4225,11 @@ function renderAccountManager(root, ctx2) {
3861
4225
  const nameDiv = createEl("div", { style: "min-width: 0; flex: 1;" }, [
3862
4226
  createEl("div", { className: "account-name", style: "display: flex; gap: 6px; align-items: center;" }, [
3863
4227
  createEl("span", { style: "font-size: 13px;" }, [platform.icon]),
3864
- createEl("span", {}, [acc.alias || acc.name || acc.profileId]),
4228
+ createEl("span", {}, [acc.alias || acc.name || formatProfileTag2(acc.profileId)]),
3865
4229
  createEl("span", { style: "font-size: 11px; color: var(--text-3);" }, [platform.label])
3866
4230
  ]),
3867
4231
  createEl("div", { className: "account-alias", style: "font-size: 11px; color: var(--text-3);" }, [
3868
- `profile: ${acc.profileId} \xB7 \u4E0A\u6B21\u68C0\u67E5: ${formatTs(acc.lastCheckAt)}`
4232
+ `profile: ${formatProfileTag2(acc.profileId)} (${acc.profileId}) \xB7 \u4E0A\u6B21\u68C0\u67E5: ${formatTs(acc.lastCheckAt)}`
3869
4233
  ])
3870
4234
  ]);
3871
4235
  const statusBadge = createEl("span", {
@@ -4187,11 +4551,6 @@ Profile ID: ${acc.profileId}
4187
4551
  }
4188
4552
 
4189
4553
  // src/renderer/tabs-new/scheduler.mts
4190
- function commandTypeToWeiboTaskType2(commandType) {
4191
- if (commandType === "weibo-search") return "search";
4192
- if (commandType === "weibo-monitor") return "monitor";
4193
- return "timeline";
4194
- }
4195
4554
  function renderSchedulerPanel(root, ctx2) {
4196
4555
  root.innerHTML = "";
4197
4556
  const pageIndicator = createEl("div", { className: "page-indicator" }, [
@@ -4260,18 +4619,25 @@ function renderSchedulerPanel(root, ctx2) {
4260
4619
  <div>
4261
4620
  <label>\u8C03\u5EA6\u7C7B\u578B</label>
4262
4621
  <select id="scheduler-type" style="width: 140px;">
4263
- <option value="interval">\u5FAA\u73AF\u95F4\u9694</option>
4264
- <option value="once">\u4E00\u6B21\u6027</option>
4622
+ <option value="immediate">\u9A6C\u4E0A\u6267\u884C\uFF08\u4EC5\u4E00\u6B21\uFF09</option>
4623
+ <option value="periodic">\u5468\u671F\u4EFB\u52A1</option>
4624
+ <option value="scheduled">\u5B9A\u65F6\u4EFB\u52A1</option>
4625
+ </select>
4626
+ </div>
4627
+ <div id="scheduler-periodic-type-wrap" style="display:none;">
4628
+ <label>\u5468\u671F\u7C7B\u578B</label>
4629
+ <select id="scheduler-periodic-type" style="width: 120px;">
4630
+ <option value="interval">\u6309\u95F4\u9694</option>
4265
4631
  <option value="daily">\u6BCF\u5929</option>
4266
4632
  <option value="weekly">\u6BCF\u5468</option>
4267
4633
  </select>
4268
4634
  </div>
4269
- <div id="scheduler-interval-wrap">
4635
+ <div id="scheduler-interval-wrap" style="display:none;">
4270
4636
  <label>\u95F4\u9694\u5206\u949F</label>
4271
4637
  <input id="scheduler-interval" type="number" min="1" value="30" style="width: 120px;" />
4272
4638
  </div>
4273
4639
  <div id="scheduler-runat-wrap" style="display:none;">
4274
- <label>\u951A\u70B9\u65F6\u95F4</label>
4640
+ <label>\u6267\u884C\u65F6\u95F4</label>
4275
4641
  <input id="scheduler-runat" type="datetime-local" style="width: 220px;" />
4276
4642
  </div>
4277
4643
  <div>
@@ -4281,8 +4647,9 @@ function renderSchedulerPanel(root, ctx2) {
4281
4647
  </div>
4282
4648
  <div class="row">
4283
4649
  <div>
4284
- <label>Profile</label>
4285
- <input id="scheduler-profile" placeholder="xiaohongshu-batch-1" style="width: 220px;" />
4650
+ <label>Profile\uFF08\u53EF\u7559\u7A7A\u81EA\u52A8\u9009\uFF09</label>
4651
+ <input id="scheduler-profile" placeholder="\u7559\u7A7A\u81EA\u52A8\u9009\u62E9\u8BE5\u5E73\u53F0\u6709\u6548\u8D26\u53F7" style="width: 260px;" />
4652
+ <div id="scheduler-profile-hint" class="muted" style="font-size:11px; margin-top:2px;">\u63A8\u8350: -</div>
4286
4653
  </div>
4287
4654
  <div>
4288
4655
  <label>\u5173\u952E\u8BCD</label>
@@ -4332,6 +4699,7 @@ function renderSchedulerPanel(root, ctx2) {
4332
4699
  </div>
4333
4700
  <div class="btn-group" style="margin-top: var(--gap);">
4334
4701
  <button id="scheduler-save-btn" style="flex:1;">\u4FDD\u5B58\u4EFB\u52A1</button>
4702
+ <button id="scheduler-run-now-btn" class="secondary" style="flex:1;">\u7ACB\u5373\u6267\u884C(\u4E0D\u4FDD\u5B58)</button>
4335
4703
  <button id="scheduler-reset-btn" class="secondary" style="flex:1;">\u6E05\u7A7A\u8868\u5355</button>
4336
4704
  </div>
4337
4705
  `;
@@ -4360,12 +4728,15 @@ function renderSchedulerPanel(root, ctx2) {
4360
4728
  const nameInput = root.querySelector("#scheduler-name");
4361
4729
  const enabledInput = root.querySelector("#scheduler-enabled");
4362
4730
  const typeSelect = root.querySelector("#scheduler-type");
4731
+ const periodicTypeWrap = root.querySelector("#scheduler-periodic-type-wrap");
4732
+ const periodicTypeSelect = root.querySelector("#scheduler-periodic-type");
4363
4733
  const intervalWrap = root.querySelector("#scheduler-interval-wrap");
4364
4734
  const runAtWrap = root.querySelector("#scheduler-runat-wrap");
4365
4735
  const intervalInput = root.querySelector("#scheduler-interval");
4366
4736
  const runAtInput = root.querySelector("#scheduler-runat");
4367
4737
  const maxRunsInput = root.querySelector("#scheduler-max-runs");
4368
4738
  const profileInput = root.querySelector("#scheduler-profile");
4739
+ const profileHint = root.querySelector("#scheduler-profile-hint");
4369
4740
  const keywordInput = root.querySelector("#scheduler-keyword");
4370
4741
  const userIdWrap = root.querySelector("#scheduler-user-id-wrap");
4371
4742
  const userIdInput = root.querySelector("#scheduler-user-id");
@@ -4377,8 +4748,10 @@ function renderSchedulerPanel(root, ctx2) {
4377
4748
  const dryRunInput = root.querySelector("#scheduler-dryrun");
4378
4749
  const likeKeywordsInput = root.querySelector("#scheduler-like-keywords");
4379
4750
  const saveBtn = root.querySelector("#scheduler-save-btn");
4751
+ const runNowBtn = root.querySelector("#scheduler-run-now-btn");
4380
4752
  const resetBtn = root.querySelector("#scheduler-reset-btn");
4381
4753
  let tasks = [];
4754
+ let accountRows = [];
4382
4755
  let daemonRunId = "";
4383
4756
  let unsubscribeCmd = null;
4384
4757
  let pendingFocusTaskId = String(ctx2?.activeTaskConfigId || "").trim();
@@ -4395,14 +4768,21 @@ function renderSchedulerPanel(root, ctx2) {
4395
4768
  function openConfigTab(taskId) {
4396
4769
  setActiveTaskContext(taskId);
4397
4770
  if (typeof ctx2.setActiveTab === "function") {
4398
- ctx2.setActiveTab("config");
4771
+ ctx2.setActiveTab("tasks");
4399
4772
  }
4400
4773
  }
4401
4774
  function updateTypeFields() {
4402
- const mode = typeSelect.value;
4403
- const useRunAt = mode === "once" || mode === "daily" || mode === "weekly";
4404
- runAtWrap.style.display = useRunAt ? "" : "none";
4405
- intervalWrap.style.display = useRunAt ? "none" : "";
4775
+ const mode = String(typeSelect.value || "immediate").trim();
4776
+ const periodicType = String(periodicTypeSelect.value || "interval").trim();
4777
+ const periodic = mode === "periodic";
4778
+ const scheduled = mode === "scheduled";
4779
+ periodicTypeWrap.style.display = periodic ? "" : "none";
4780
+ runAtWrap.style.display = scheduled || periodic && periodicType !== "interval" ? "" : "none";
4781
+ intervalWrap.style.display = periodic && periodicType === "interval" ? "" : "none";
4782
+ maxRunsInput.disabled = mode === "immediate" || mode === "scheduled";
4783
+ if (mode === "immediate" || mode === "scheduled") {
4784
+ maxRunsInput.value = "";
4785
+ }
4406
4786
  }
4407
4787
  function updateTaskTypeOptions() {
4408
4788
  const platform = platformSelect.value;
@@ -4412,6 +4792,36 @@ function renderSchedulerPanel(root, ctx2) {
4412
4792
  taskTypeSelect.value = taskTypeSelect.options[0]?.value || "";
4413
4793
  }
4414
4794
  updatePlatformFields();
4795
+ void refreshPlatformAccounts(platform);
4796
+ }
4797
+ function normalizePlatform2(value) {
4798
+ const raw = String(value || "").trim().toLowerCase();
4799
+ if (raw === "weibo") return "weibo";
4800
+ if (raw === "1688") return "1688";
4801
+ return "xiaohongshu";
4802
+ }
4803
+ async function refreshPlatformAccounts(platformValue) {
4804
+ const platform = normalizePlatform2(platformValue);
4805
+ try {
4806
+ accountRows = await listAccountProfiles(ctx2.api, { platform: platform === "xiaohongshu" ? "xiaohongshu" : platform });
4807
+ } catch {
4808
+ accountRows = [];
4809
+ }
4810
+ const recommended = accountRows.filter((row) => row.valid).sort((a, b) => {
4811
+ const ta = Date.parse(String(a.updatedAt || "")) || 0;
4812
+ const tb = Date.parse(String(b.updatedAt || "")) || 0;
4813
+ if (tb !== ta) return tb - ta;
4814
+ return String(a.profileId || "").localeCompare(String(b.profileId || ""));
4815
+ })[0];
4816
+ if (!recommended) {
4817
+ profileHint.textContent = `\u63A8\u8350: \u5F53\u524D\u5E73\u53F0(${platform})\u65E0\u6709\u6548\u8D26\u53F7`;
4818
+ return;
4819
+ }
4820
+ const label = recommended.alias || recommended.name || recommended.profileId;
4821
+ profileHint.textContent = `\u63A8\u8350: ${label} (${recommended.profileId})`;
4822
+ if (!String(profileInput.value || "").trim()) {
4823
+ profileInput.value = recommended.profileId;
4824
+ }
4415
4825
  }
4416
4826
  function updatePlatformFields() {
4417
4827
  const commandType = String(taskTypeSelect.value || "").trim();
@@ -4424,7 +4834,8 @@ function renderSchedulerPanel(root, ctx2) {
4424
4834
  editingIdInput.value = "";
4425
4835
  nameInput.value = "";
4426
4836
  enabledInput.checked = true;
4427
- typeSelect.value = "interval";
4837
+ typeSelect.value = "immediate";
4838
+ periodicTypeSelect.value = "interval";
4428
4839
  intervalInput.value = "30";
4429
4840
  runAtInput.value = "";
4430
4841
  maxRunsInput.value = "";
@@ -4447,7 +4858,6 @@ function renderSchedulerPanel(root, ctx2) {
4447
4858
  const maxRuns = maxRunsRaw ? Math.max(1, Number(maxRunsRaw) || 1) : null;
4448
4859
  const commandType = String(taskTypeSelect.value || "xhs-unified").trim();
4449
4860
  const argv = {
4450
- profile: profileInput.value.trim(),
4451
4861
  keyword: keywordInput.value.trim(),
4452
4862
  "max-notes": Number(maxNotesInput.value || 50) || 50,
4453
4863
  env: envSelect.value,
@@ -4457,19 +4867,40 @@ function renderSchedulerPanel(root, ctx2) {
4457
4867
  headless: headlessInput.checked,
4458
4868
  "dry-run": dryRunInput.checked
4459
4869
  };
4870
+ const profileValue = profileInput.value.trim();
4871
+ if (profileValue) argv.profile = profileValue;
4460
4872
  if (commandType.startsWith("weibo")) {
4461
- argv["task-type"] = commandTypeToWeiboTaskType2(commandType);
4462
4873
  argv["user-id"] = userIdInput.value.trim();
4463
4874
  }
4875
+ const mode = String(typeSelect.value || "immediate").trim();
4876
+ const periodicType = String(periodicTypeSelect.value || "interval").trim();
4877
+ let scheduleType = "once";
4878
+ let runAt = toIsoOrNull(runAtInput.value);
4879
+ let maxRunsFinal = maxRuns;
4880
+ if (mode === "immediate") {
4881
+ scheduleType = "once";
4882
+ runAt = (/* @__PURE__ */ new Date()).toISOString();
4883
+ maxRunsFinal = 1;
4884
+ } else if (mode === "periodic") {
4885
+ if (periodicType === "daily" || periodicType === "weekly") {
4886
+ scheduleType = periodicType;
4887
+ } else {
4888
+ scheduleType = "interval";
4889
+ runAt = null;
4890
+ }
4891
+ } else {
4892
+ scheduleType = "once";
4893
+ maxRunsFinal = 1;
4894
+ }
4464
4895
  return {
4465
4896
  id: editingIdInput.value.trim(),
4466
4897
  name: nameInput.value.trim(),
4467
4898
  enabled: enabledInput.checked,
4468
4899
  commandType,
4469
- scheduleType: typeSelect.value,
4900
+ scheduleType,
4470
4901
  intervalMinutes: Number(intervalInput.value || 30) || 30,
4471
- runAt: toIsoOrNull(runAtInput.value),
4472
- maxRuns,
4902
+ runAt,
4903
+ maxRuns: maxRunsFinal,
4473
4904
  argv
4474
4905
  };
4475
4906
  }
@@ -4482,7 +4913,9 @@ function renderSchedulerPanel(root, ctx2) {
4482
4913
  editingIdInput.value = task.id;
4483
4914
  nameInput.value = task.name || "";
4484
4915
  enabledInput.checked = task.enabled !== false;
4485
- typeSelect.value = task.scheduleType;
4916
+ const uiSchedule = inferUiScheduleEditorState(task);
4917
+ typeSelect.value = uiSchedule.mode;
4918
+ periodicTypeSelect.value = uiSchedule.periodicType;
4486
4919
  intervalInput.value = String(task.intervalMinutes || 30);
4487
4920
  runAtInput.value = toLocalDatetimeValue(task.runAt);
4488
4921
  maxRunsInput.value = task.maxRuns ? String(task.maxRuns) : "";
@@ -4500,19 +4933,27 @@ function renderSchedulerPanel(root, ctx2) {
4500
4933
  updatePlatformFields();
4501
4934
  updateTypeFields();
4502
4935
  }
4503
- async function runScheduleJson(args, timeoutMs = 6e4) {
4504
- const script = ctx2.api.pathJoin("apps", "webauto", "entry", "schedule.mjs");
4505
- const ret = await ctx2.api.cmdRunJson({
4506
- title: `schedule ${args.join(" ")}`,
4507
- cwd: "",
4508
- args: [script, ...args, "--json"],
4509
- timeoutMs
4510
- });
4936
+ async function invokeSchedule(input) {
4937
+ if (typeof ctx2.api?.scheduleInvoke !== "function") {
4938
+ throw new Error("scheduleInvoke unavailable");
4939
+ }
4940
+ const ret = await ctx2.api.scheduleInvoke(input);
4511
4941
  if (!ret?.ok) {
4512
- const reason = String(ret?.error || ret?.stderr || ret?.stdout || "unknown_error").trim();
4942
+ const reason = String(ret?.error || "schedule command failed").trim();
4513
4943
  throw new Error(reason || "schedule command failed");
4514
4944
  }
4515
- return ret.json || {};
4945
+ return ret?.json ?? ret;
4946
+ }
4947
+ async function invokeTaskRunEphemeral(input) {
4948
+ if (typeof ctx2.api?.taskRunEphemeral !== "function") {
4949
+ throw new Error("taskRunEphemeral unavailable");
4950
+ }
4951
+ const ret = await ctx2.api.taskRunEphemeral(input);
4952
+ if (!ret?.ok) {
4953
+ const reason = String(ret?.error || "run ephemeral failed").trim();
4954
+ throw new Error(reason || "run ephemeral failed");
4955
+ }
4956
+ return ret;
4516
4957
  }
4517
4958
  function downloadJson(fileName, payload) {
4518
4959
  const text = JSON.stringify(payload, null, 2);
@@ -4534,7 +4975,7 @@ function renderSchedulerPanel(root, ctx2) {
4534
4975
  const card = createEl("div", {
4535
4976
  style: "border:1px solid var(--border); border-radius:10px; padding:10px; margin-bottom:10px; background: var(--panel-soft);"
4536
4977
  });
4537
- const scheduleText = task.scheduleType === "once" ? `once @ ${task.runAt || "-"}` : task.scheduleType === "daily" ? `daily @ ${task.runAt || "-"}` : task.scheduleType === "weekly" ? `weekly @ ${task.runAt || "-"}` : `interval ${task.intervalMinutes}m`;
4978
+ 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)`;
4538
4979
  const statusText = task.lastStatus ? `${task.lastStatus} / run=${task.runCount} / fail=${task.failCount}` : "never run";
4539
4980
  const headRow = createEl("div", { style: "display:flex; justify-content:space-between; gap:8px; margin-bottom:6px;" });
4540
4981
  headRow.appendChild(createEl("div", { style: "font-weight:600;" }, [task.name || task.id]));
@@ -4583,7 +5024,7 @@ function renderSchedulerPanel(root, ctx2) {
4583
5024
  runBtn.onclick = async () => {
4584
5025
  try {
4585
5026
  setActiveTaskContext(task.id);
4586
- const out = await runScheduleJson(["run", task.id], 0);
5027
+ const out = await invokeSchedule({ action: "run", taskId: task.id, timeoutMs: 0 });
4587
5028
  const runId = String(
4588
5029
  out?.result?.runResult?.lastRunId || out?.result?.runResult?.runId || out?.runResult?.runId || ""
4589
5030
  ).trim();
@@ -4597,6 +5038,7 @@ function renderSchedulerPanel(root, ctx2) {
4597
5038
  target: Number(argv["max-notes"] || argv.target || 0) || 0,
4598
5039
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
4599
5040
  };
5041
+ ctx2.activeRunId = runId || null;
4600
5042
  }
4601
5043
  if (typeof ctx2.setStatus === "function") {
4602
5044
  ctx2.setStatus(`running: ${task.id}`);
@@ -4611,7 +5053,7 @@ function renderSchedulerPanel(root, ctx2) {
4611
5053
  };
4612
5054
  exportBtn.onclick = async () => {
4613
5055
  try {
4614
- const out = await runScheduleJson(["export", task.id]);
5056
+ const out = await invokeSchedule({ action: "export", taskId: task.id });
4615
5057
  downloadJson(`${task.id}.json`, out);
4616
5058
  } catch (err) {
4617
5059
  alert(`\u5BFC\u51FA\u5931\u8D25: ${err?.message || String(err)}`);
@@ -4620,7 +5062,7 @@ function renderSchedulerPanel(root, ctx2) {
4620
5062
  delBtn.onclick = async () => {
4621
5063
  if (!confirm(`\u786E\u8BA4\u5220\u9664\u4EFB\u52A1 ${task.id} ?`)) return;
4622
5064
  try {
4623
- await runScheduleJson(["delete", task.id]);
5065
+ await invokeSchedule({ action: "delete", taskId: task.id });
4624
5066
  await refreshList();
4625
5067
  } catch (err) {
4626
5068
  alert(`\u5220\u9664\u5931\u8D25: ${err?.message || String(err)}`);
@@ -4630,7 +5072,7 @@ function renderSchedulerPanel(root, ctx2) {
4630
5072
  }
4631
5073
  }
4632
5074
  async function refreshList() {
4633
- const out = await runScheduleJson(["list"]);
5075
+ const out = await invokeSchedule({ action: "list" });
4634
5076
  tasks = parseTaskRows(out);
4635
5077
  if (!pendingFocusTaskId) {
4636
5078
  pendingFocusTaskId = String(ctx2?.activeTaskConfigId || "").trim();
@@ -4650,48 +5092,8 @@ function renderSchedulerPanel(root, ctx2) {
4650
5092
  }
4651
5093
  async function saveTask() {
4652
5094
  const payload = readFormAsPayload();
4653
- if (!payload.name) {
4654
- alert("\u4EFB\u52A1\u540D\u4E0D\u80FD\u4E3A\u7A7A");
4655
- return;
4656
- }
4657
- if (!payload.argv.profile && !payload.argv.profiles && !payload.argv.profilepool) {
4658
- alert("profile/profiles/profilepool \u81F3\u5C11\u586B\u5199\u4E00\u4E2A");
4659
- return;
4660
- }
4661
- const commandType = String(payload.commandType || "").trim();
4662
- const keywordRequired = commandType === "xhs-unified" || commandType === "weibo-search" || commandType === "1688-search";
4663
- if (keywordRequired && !payload.argv.keyword) {
4664
- alert("\u5173\u952E\u8BCD\u4E0D\u80FD\u4E3A\u7A7A");
4665
- return;
4666
- }
4667
- if (commandType === "weibo-monitor" && !payload.argv["user-id"]) {
4668
- alert("\u5FAE\u535A monitor \u4EFB\u52A1\u9700\u8981 user-id");
4669
- return;
4670
- }
4671
- const args = payload.id ? ["update", payload.id] : ["add"];
4672
- args.push("--name", payload.name);
4673
- args.push("--enabled", String(payload.enabled));
4674
- args.push("--command-type", commandType || "xhs-unified");
4675
- args.push("--schedule-type", payload.scheduleType);
4676
- if (payload.scheduleType === "once") {
4677
- if (!payload.runAt) {
4678
- alert("\u4E00\u6B21\u6027\u4EFB\u52A1\u9700\u8981\u951A\u70B9\u65F6\u95F4");
4679
- return;
4680
- }
4681
- args.push("--run-at", payload.runAt);
4682
- } else if (payload.scheduleType === "daily" || payload.scheduleType === "weekly") {
4683
- if (!payload.runAt) {
4684
- alert(`${payload.scheduleType} \u4EFB\u52A1\u9700\u8981\u951A\u70B9\u65F6\u95F4`);
4685
- return;
4686
- }
4687
- args.push("--run-at", payload.runAt);
4688
- } else {
4689
- args.push("--interval-minutes", String(Math.max(1, payload.intervalMinutes)));
4690
- }
4691
- args.push("--max-runs", payload.maxRuns === null ? "0" : String(payload.maxRuns));
4692
- args.push("--argv-json", JSON.stringify(payload.argv));
4693
5095
  try {
4694
- const out = await runScheduleJson(args);
5096
+ const out = await invokeSchedule({ action: "save", payload });
4695
5097
  const savedId = String(out?.task?.id || payload.id || "").trim();
4696
5098
  pendingFocusTaskId = savedId;
4697
5099
  if (savedId) setActiveTaskContext(savedId);
@@ -4700,9 +5102,44 @@ function renderSchedulerPanel(root, ctx2) {
4700
5102
  alert(`\u4FDD\u5B58\u5931\u8D25: ${err?.message || String(err)}`);
4701
5103
  }
4702
5104
  }
5105
+ async function runNowFromForm() {
5106
+ runNowBtn.disabled = true;
5107
+ const prevText = runNowBtn.textContent;
5108
+ runNowBtn.textContent = "\u6267\u884C\u4E2D...";
5109
+ try {
5110
+ const payload = readFormAsPayload();
5111
+ const ret = await invokeTaskRunEphemeral({
5112
+ commandType: payload.commandType,
5113
+ argv: payload.argv
5114
+ });
5115
+ const runId = String(ret?.runId || "").trim();
5116
+ if (payload.commandType === "xhs-unified" && ctx2 && typeof ctx2 === "object") {
5117
+ ctx2.xhsCurrentRun = {
5118
+ runId: runId || null,
5119
+ taskId: null,
5120
+ profileId: String(payload.argv.profile || ""),
5121
+ keyword: String(payload.argv.keyword || ""),
5122
+ target: Number(payload.argv["max-notes"] || payload.argv.target || 0) || 0,
5123
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
5124
+ };
5125
+ ctx2.activeRunId = runId || null;
5126
+ }
5127
+ if (typeof ctx2.setStatus === "function") {
5128
+ ctx2.setStatus(`started: ${payload.commandType}`);
5129
+ }
5130
+ if (payload.commandType === "xhs-unified" && typeof ctx2.setActiveTab === "function") {
5131
+ ctx2.setActiveTab("dashboard");
5132
+ }
5133
+ } catch (err) {
5134
+ alert(`\u6267\u884C\u5931\u8D25: ${err?.message || String(err)}`);
5135
+ } finally {
5136
+ runNowBtn.disabled = false;
5137
+ runNowBtn.textContent = prevText || "\u7ACB\u5373\u6267\u884C(\u4E0D\u4FDD\u5B58)";
5138
+ }
5139
+ }
4703
5140
  async function runDueNow() {
4704
5141
  try {
4705
- const out = await runScheduleJson(["run-due", "--limit", "20"], 0);
5142
+ const out = await invokeSchedule({ action: "run-due", limit: 20, timeoutMs: 0 });
4706
5143
  alert(`\u5230\u70B9\u4EFB\u52A1\u6267\u884C\u5B8C\u6210\uFF1Adue=${out.count || 0}, success=${out.success || 0}, failed=${out.failed || 0}`);
4707
5144
  await refreshList();
4708
5145
  } catch (err) {
@@ -4711,7 +5148,7 @@ function renderSchedulerPanel(root, ctx2) {
4711
5148
  }
4712
5149
  async function exportAll() {
4713
5150
  try {
4714
- const out = await runScheduleJson(["export"]);
5151
+ const out = await invokeSchedule({ action: "export" });
4715
5152
  downloadJson("webauto-schedules.json", out);
4716
5153
  } catch (err) {
4717
5154
  alert(`\u5BFC\u51FA\u5931\u8D25: ${err?.message || String(err)}`);
@@ -4726,7 +5163,7 @@ function renderSchedulerPanel(root, ctx2) {
4726
5163
  if (!file) return;
4727
5164
  try {
4728
5165
  const text = await file.text();
4729
- await runScheduleJson(["import", "--payload-json", text, "--mode", "merge"]);
5166
+ await invokeSchedule({ action: "import", payloadJson: text, mode: "merge" });
4730
5167
  await refreshList();
4731
5168
  } catch (err) {
4732
5169
  alert(`\u5BFC\u5165\u5931\u8D25: ${err?.message || String(err)}`);
@@ -4740,13 +5177,7 @@ function renderSchedulerPanel(root, ctx2) {
4740
5177
  return;
4741
5178
  }
4742
5179
  const interval = Math.max(5, Number(daemonIntervalInput.value || 30) || 30);
4743
- const script = ctx2.api.pathJoin("apps", "webauto", "entry", "schedule.mjs");
4744
- const ret = await ctx2.api.cmdSpawn({
4745
- title: `schedule daemon ${interval}s`,
4746
- cwd: "",
4747
- args: [script, "daemon", "--interval-sec", String(interval), "--limit", "20", "--json"],
4748
- groupKey: "scheduler"
4749
- });
5180
+ const ret = await invokeSchedule({ action: "daemon-start", intervalSec: interval, limit: 20 });
4750
5181
  daemonRunId = String(ret?.runId || "").trim();
4751
5182
  setDaemonStatus(daemonRunId ? `daemon: \u8FD0\u884C\u4E2D (${daemonRunId})` : "daemon: \u542F\u52A8\u5931\u8D25");
4752
5183
  }
@@ -4765,7 +5196,9 @@ function renderSchedulerPanel(root, ctx2) {
4765
5196
  platformSelect.addEventListener("change", updateTaskTypeOptions);
4766
5197
  taskTypeSelect.addEventListener("change", updatePlatformFields);
4767
5198
  typeSelect.addEventListener("change", updateTypeFields);
5199
+ periodicTypeSelect.addEventListener("change", updateTypeFields);
4768
5200
  saveBtn.onclick = () => void saveTask();
5201
+ runNowBtn.onclick = () => void runNowFromForm();
4769
5202
  resetBtn.onclick = () => resetForm();
4770
5203
  refreshBtn.onclick = () => void refreshList();
4771
5204
  runDueBtn.onclick = () => void runDueNow();
@@ -4817,6 +5250,8 @@ var tabs = [
4817
5250
  var tabsEl = document.getElementById("tabs");
4818
5251
  var contentEl = document.getElementById("content");
4819
5252
  var statusEl = document.getElementById("status");
5253
+ var appTitleEl = document.getElementById("app-title");
5254
+ var appVersionEl = document.getElementById("app-version");
4820
5255
  var activeTabCleanup = null;
4821
5256
  var mutableApi = { ...window.api || {}, settings: null };
4822
5257
  var tabIcons = {
@@ -4892,6 +5327,22 @@ function startDesktopHeartbeat() {
4892
5327
  async function loadSettings() {
4893
5328
  await ctx.refreshSettings();
4894
5329
  }
5330
+ async function applyVersionBadge() {
5331
+ try {
5332
+ if (typeof window.api?.appGetVersion !== "function") return;
5333
+ const info = await window.api.appGetVersion();
5334
+ const webauto = String(info?.webauto || "").trim();
5335
+ const desktop = String(info?.desktop || "").trim();
5336
+ const badge = String(info?.badge || "").trim();
5337
+ if (appTitleEl && webauto) {
5338
+ appTitleEl.textContent = `WebAuto Console v${webauto}`;
5339
+ }
5340
+ if (appVersionEl) {
5341
+ appVersionEl.textContent = badge || (desktop && desktop !== webauto ? `webauto v${webauto} \xB7 console v${desktop}` : webauto ? `v${webauto}` : "v-");
5342
+ }
5343
+ } catch {
5344
+ }
5345
+ }
4895
5346
  function focusTabButton(tabId) {
4896
5347
  const button = tabsEl.querySelector(`[data-tab-id="${tabId}"]`);
4897
5348
  button?.focus();
@@ -4999,6 +5450,7 @@ async function detectStartupTab() {
4999
5450
  }
5000
5451
  async function main() {
5001
5452
  startDesktopHeartbeat();
5453
+ await applyVersionBadge();
5002
5454
  await loadSettings();
5003
5455
  installCmdEvents();
5004
5456
  const startupTab = await detectStartupTab();