@web-auto/webauto 0.1.8 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/apps/desktop-console/dist/main/index.mjs +800 -89
  2. package/apps/desktop-console/dist/main/preload.mjs +3 -0
  3. package/apps/desktop-console/dist/renderer/index.html +9 -1
  4. package/apps/desktop-console/dist/renderer/index.js +784 -331
  5. package/apps/desktop-console/entry/ui-cli.mjs +23 -8
  6. package/apps/desktop-console/entry/ui-console.mjs +8 -3
  7. package/apps/webauto/entry/account.mjs +69 -8
  8. package/apps/webauto/entry/lib/account-detect.mjs +106 -25
  9. package/apps/webauto/entry/lib/account-store.mjs +121 -22
  10. package/apps/webauto/entry/lib/schedule-store.mjs +0 -12
  11. package/apps/webauto/entry/profilepool.mjs +45 -3
  12. package/apps/webauto/entry/schedule.mjs +44 -2
  13. package/apps/webauto/entry/weibo-unified.mjs +2 -2
  14. package/apps/webauto/entry/xhs-install.mjs +220 -51
  15. package/apps/webauto/entry/xhs-unified.mjs +33 -6
  16. package/bin/webauto.mjs +80 -4
  17. package/dist/modules/camo-runtime/src/utils/browser-service.mjs +4 -0
  18. package/dist/services/unified-api/server.js +5 -0
  19. package/dist/services/unified-api/task-state.js +2 -0
  20. package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +142 -14
  21. package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +16 -1
  22. package/modules/camo-runtime/src/autoscript/action-providers/xhs.mjs +104 -0
  23. package/modules/camo-runtime/src/autoscript/runtime.mjs +14 -4
  24. package/modules/camo-runtime/src/autoscript/schema.mjs +9 -0
  25. package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +9 -2
  26. package/modules/camo-runtime/src/container/runtime-core/checkpoint.mjs +107 -1
  27. package/modules/camo-runtime/src/container/runtime-core/subscription.mjs +24 -2
  28. package/modules/camo-runtime/src/utils/browser-service.mjs +4 -0
  29. package/package.json +6 -3
  30. package/scripts/bump-version.mjs +120 -0
  31. package/services/unified-api/server.ts +4 -0
  32. package/services/unified-api/task-state.ts +5 -0
@@ -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,7 +1745,7 @@ 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");
@@ -1810,14 +1823,14 @@ function renderSetupWizard(root, ctx2) {
1810
1823
  applyEnvironment(latest);
1811
1824
  updateCompleteStatus();
1812
1825
  if (!detail) {
1813
- if (label.includes("Camoufox") || label.includes("Runtime")) {
1814
- ok = Boolean(latest.firefox?.installed);
1815
- } else if (label.includes("Camoufox CLI") || label.includes("CLI") || label.includes("camo")) {
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")) {
1816
1829
  ok = Boolean(latest.camo?.installed);
1817
1830
  } else if (label.includes("\u6838\u5FC3")) {
1818
1831
  ok = Boolean(latest.services?.unifiedApi && latest.services?.camoRuntime);
1819
1832
  } else {
1820
- ok = isEnvReady(latest);
1833
+ ok = Boolean(latest.allReady);
1821
1834
  }
1822
1835
  }
1823
1836
  }
@@ -1840,12 +1853,13 @@ function renderSetupWizard(root, ctx2) {
1840
1853
  applyEnvironment(snapshot);
1841
1854
  updateCompleteStatus();
1842
1855
  if (!envReady) {
1856
+ const missingFlags = getMissing(snapshot);
1843
1857
  const missing = [];
1844
- if (!snapshot?.camo?.installed) missing.push("camo-cli");
1845
- if (!snapshot?.services?.unifiedApi) missing.push("unified-api");
1846
- if (!snapshot?.firefox?.installed) missing.push("camoufox-runtime");
1858
+ if (missingFlags.camo) missing.push("camo-cli");
1859
+ if (missingFlags.core) missing.push("unified-api");
1860
+ if (missingFlags.runtime) missing.push("browser-kernel");
1847
1861
  setupStatusText.textContent = `\u5B58\u5728\u5F85\u4FEE\u590D\u9879: ${missing.join(", ")}`;
1848
- if (!snapshot?.services?.camoRuntime) {
1862
+ if (missingFlags.runtimeService) {
1849
1863
  setupStatusText.textContent += "\uFF08camo-runtime \u672A\u5C31\u7EEA\uFF0C\u5F53\u524D\u4E3A\u53EF\u9009\uFF09";
1850
1864
  }
1851
1865
  } else if (!snapshot?.geoip?.installed) {
@@ -1917,8 +1931,8 @@ function renderSetupWizard(root, ctx2) {
1917
1931
  style: "display:flex; justify-content:space-between; align-items:center; padding:8px 12px; border-bottom:1px solid var(--border);"
1918
1932
  }, [
1919
1933
  createEl("div", {}, [
1920
- createEl("div", { style: "font-weight:600; margin-bottom:2px;" }, [acc.alias || acc.name || acc.profileId]),
1921
- createEl("div", { className: "muted", style: "font-size:11px;" }, [acc.profileId])
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")}`])
1922
1936
  ]),
1923
1937
  createEl("span", {
1924
1938
  className: `status-badge ${statusClass}`
@@ -2025,6 +2039,7 @@ function renderSetupWizard(root, ctx2) {
2025
2039
  "sync",
2026
2040
  id,
2027
2041
  "--pending-while-login",
2042
+ "--resolve-alias",
2028
2043
  "--json"
2029
2044
  ],
2030
2045
  timeoutMs: 2e4
@@ -2081,12 +2096,12 @@ function renderSetupWizard(root, ctx2) {
2081
2096
  repairCoreBtn.onclick = () => void runRepair("\u4FEE\u590D\u6838\u5FC3\u670D\u52A1", repairCoreServices);
2082
2097
  repairCore2Btn.onclick = () => void runRepair("\u4FEE\u590D\u6838\u5FC3\u670D\u52A1", repairCoreServices);
2083
2098
  repairCamoBtn.onclick = () => void runRepair("\u4FEE\u590D Camo CLI", repairCoreServices);
2084
- 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 }));
2085
2100
  repairGeoipBtn.onclick = () => void runRepair("\u5B89\u88C5 GeoIP", () => repairInstall({ geoip: true }));
2086
2101
  addAccountBtn.onclick = addAccount;
2087
2102
  enterMainBtn.onclick = () => {
2088
2103
  if (typeof ctx2.setActiveTab === "function") {
2089
- ctx2.setActiveTab("config");
2104
+ ctx2.setActiveTab("tasks");
2090
2105
  }
2091
2106
  };
2092
2107
  void tickEnvironment();
@@ -2182,6 +2197,21 @@ function parseTaskRows(payload) {
2182
2197
  runHistory: parseRunHistory(row?.runHistory)
2183
2198
  })).filter((row) => row.id);
2184
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
+ }
2185
2215
  function getTasksForPlatform(platform) {
2186
2216
  const p = platform;
2187
2217
  return PLATFORM_TASKS[p] || [];
@@ -2208,7 +2238,8 @@ var DEFAULT_FORM = {
2208
2238
  collectBody: true,
2209
2239
  doLikes: false,
2210
2240
  likeKeywords: "",
2211
- scheduleType: "interval",
2241
+ scheduleMode: "immediate",
2242
+ periodicType: "interval",
2212
2243
  intervalMinutes: 30,
2213
2244
  runAt: null,
2214
2245
  maxRuns: null
@@ -2217,19 +2248,6 @@ function parseSortableTime(value) {
2217
2248
  const ts = Date.parse(String(value || ""));
2218
2249
  return Number.isFinite(ts) ? ts : 0;
2219
2250
  }
2220
- function isKeywordRequired(taskType) {
2221
- return taskType === "xhs-unified" || taskType === "weibo-search" || taskType === "1688-search";
2222
- }
2223
- function commandTypeToWeiboTaskType(commandType) {
2224
- if (commandType === "weibo-search") return "search";
2225
- if (commandType === "weibo-monitor") return "monitor";
2226
- return "timeline";
2227
- }
2228
- function fallbackTaskName(data) {
2229
- const keyword = String(data.keyword || "").trim();
2230
- const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
2231
- return keyword ? `${data.taskType}-${keyword}` : `${data.taskType}-${stamp}`;
2232
- }
2233
2251
  function renderTasksPanel(root, ctx2) {
2234
2252
  root.innerHTML = "";
2235
2253
  const pageIndicator = createEl("div", { className: "page-indicator" }, [
@@ -2284,8 +2302,9 @@ function renderTasksPanel(root, ctx2) {
2284
2302
  <input id="task-target" type="number" min="1" value="50" style="width: 80px;" />
2285
2303
  </div>
2286
2304
  <div>
2287
- <label>Profile</label>
2288
- <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>
2289
2308
  </div>
2290
2309
  <div>
2291
2310
  <label>\u73AF\u5883</label>
@@ -2323,14 +2342,20 @@ function renderTasksPanel(root, ctx2) {
2323
2342
  <div style="font-size:12px; color:var(--text-secondary); margin-bottom:var(--gap-sm);">\u8C03\u5EA6\u8BBE\u7F6E\uFF08\u53EF\u9009\uFF09</div>
2324
2343
  <div class="row">
2325
2344
  <div>
2326
- <select id="task-schedule-type" style="width: 100px;">
2327
- <option value="interval">\u5FAA\u73AF\u95F4\u9694</option>
2328
- <option value="once">\u4E00\u6B21\u6027</option>
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>
2329
2354
  <option value="daily">\u6BCF\u5929</option>
2330
2355
  <option value="weekly">\u6BCF\u5468</option>
2331
2356
  </select>
2332
2357
  </div>
2333
- <div id="task-interval-wrap">
2358
+ <div id="task-interval-wrap" style="display:none;">
2334
2359
  <input id="task-interval" type="number" min="1" value="30" style="width: 70px;" />
2335
2360
  <span style="font-size:11px;color:var(--text-tertiary);">\u5206\u949F</span>
2336
2361
  </div>
@@ -2347,7 +2372,7 @@ function renderTasksPanel(root, ctx2) {
2347
2372
  <div class="btn-group" style="margin-top: var(--gap);">
2348
2373
  <button id="task-save-btn" style="flex:1;">\u4FDD\u5B58\u4EFB\u52A1</button>
2349
2374
  <button id="task-run-btn" class="primary" style="flex:1;">\u4FDD\u5B58\u5E76\u6267\u884C</button>
2350
- <button id="task-run-ephemeral-btn" class="secondary" style="flex:1;">\u4EC5\u6267\u884C(\u4E0D\u4FDD\u5B58)</button>
2375
+ <button id="task-run-ephemeral-btn" class="secondary" style="flex:1;">\u7ACB\u5373\u6267\u884C(\u4E0D\u4FDD\u5B58)</button>
2351
2376
  <button id="task-reset-btn" class="secondary" style="flex:0.6;">\u91CD\u7F6E</button>
2352
2377
  </div>
2353
2378
  `;
@@ -2377,15 +2402,17 @@ function renderTasksPanel(root, ctx2) {
2377
2402
  root.appendChild(mainGrid);
2378
2403
  const recentCard = createEl("div", { className: "bento-cell", style: "margin-top: var(--gap);" });
2379
2404
  recentCard.innerHTML = `
2380
- <div class="bento-title">\u5386\u53F2\u4EFB\u52A1</div>
2405
+ <div class="bento-title">\u5DF2\u4FDD\u5B58\u4EFB\u52A1\u5217\u8868</div>
2381
2406
  <div class="row" style="margin-bottom: var(--gap-sm);">
2382
2407
  <select id="task-history-select" style="min-width: 320px;">
2383
2408
  <option value="">\u9009\u62E9\u5386\u53F2\u4EFB\u52A1...</option>
2384
2409
  </select>
2385
2410
  <button id="task-history-edit-btn" class="secondary">\u8F7D\u5165\u7F16\u8F91</button>
2386
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>
2387
2413
  <button id="task-history-refresh-btn" class="secondary">\u5237\u65B0</button>
2388
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>
2389
2416
  <div id="recent-tasks-list"></div>
2390
2417
  `;
2391
2418
  root.appendChild(recentCard);
@@ -2396,6 +2423,7 @@ function renderTasksPanel(root, ctx2) {
2396
2423
  const keywordInput = formCard.querySelector("#task-keyword");
2397
2424
  const targetInput = formCard.querySelector("#task-target");
2398
2425
  const profileInput = formCard.querySelector("#task-profile");
2426
+ const profileHint = formCard.querySelector("#task-profile-hint");
2399
2427
  const envSelect = formCard.querySelector("#task-env");
2400
2428
  const userIdWrap = formCard.querySelector("#task-user-id-wrap");
2401
2429
  const userIdInput = formCard.querySelector("#task-user-id");
@@ -2404,6 +2432,8 @@ function renderTasksPanel(root, ctx2) {
2404
2432
  const likesInput = formCard.querySelector("#task-likes");
2405
2433
  const likeKeywordsInput = formCard.querySelector("#task-like-keywords");
2406
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");
2407
2437
  const intervalInput = formCard.querySelector("#task-interval");
2408
2438
  const intervalWrap = formCard.querySelector("#task-interval-wrap");
2409
2439
  const runAtInput = formCard.querySelector("#task-runat");
@@ -2419,22 +2449,62 @@ function renderTasksPanel(root, ctx2) {
2419
2449
  const historySelect = recentCard.querySelector("#task-history-select");
2420
2450
  const historyEditBtn = recentCard.querySelector("#task-history-edit-btn");
2421
2451
  const historyCloneBtn = recentCard.querySelector("#task-history-clone-btn");
2452
+ const historyRunBtn = recentCard.querySelector("#task-history-run-btn");
2422
2453
  const historyRefreshBtn = recentCard.querySelector("#task-history-refresh-btn");
2423
2454
  const recentTasksList = recentCard.querySelector("#recent-tasks-list");
2424
2455
  const statRunning = statsCard.querySelector("#stat-running");
2425
2456
  const statToday = statsCard.querySelector("#stat-today");
2426
2457
  const statSaved = statsCard.querySelector("#stat-saved");
2427
2458
  let tasks = [];
2459
+ let accountRows = [];
2428
2460
  const activeRunIds = /* @__PURE__ */ new Set();
2429
2461
  let unsubscribeActiveRuns = null;
2430
2462
  const joinPath2 = (...parts) => {
2431
2463
  if (typeof ctx2?.api?.pathJoin === "function") return ctx2.api.pathJoin(...parts);
2432
2464
  return parts.filter(Boolean).join("/");
2433
2465
  };
2434
- const scheduleScript = joinPath2("apps", "webauto", "entry", "schedule.mjs");
2435
2466
  const quotaScript = joinPath2("apps", "webauto", "entry", "lib", "quota-status.mjs");
2436
- const xhsScript = joinPath2("apps", "webauto", "entry", "xhs-unified.mjs");
2437
- const weiboScript = joinPath2("apps", "webauto", "entry", "weibo-unified.mjs");
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
+ }
2438
2508
  function getTaskById(taskId) {
2439
2509
  const id = String(taskId || "").trim();
2440
2510
  if (!id) return null;
@@ -2452,13 +2522,17 @@ function renderTasksPanel(root, ctx2) {
2452
2522
  formTitle.textContent = "\u65B0\u5EFA\u4EFB\u52A1";
2453
2523
  }
2454
2524
  function updateTaskTypeOptions(preferredType = "") {
2455
- const platform = platformSelect.value;
2525
+ const platform = normalizePlatform2(platformSelect.value);
2456
2526
  const options = getTasksForPlatform(platform);
2457
2527
  taskTypeSelect.innerHTML = options.map((item) => `<option value="${item.type}">${item.icon} ${item.label}</option>`).join("");
2458
2528
  const target = String(preferredType || "").trim();
2459
2529
  const matched = options.find((item) => item.type === target);
2460
2530
  taskTypeSelect.value = matched?.type || options[0]?.type || "";
2461
2531
  updatePlatformFields();
2532
+ void refreshPlatformAccountRows(platform).then(() => {
2533
+ updateProfileHint(platform);
2534
+ maybeAutofillProfile(platform);
2535
+ });
2462
2536
  }
2463
2537
  function updatePlatformFields() {
2464
2538
  const taskType = String(taskTypeSelect.value || "").trim();
@@ -2466,9 +2540,17 @@ function renderTasksPanel(root, ctx2) {
2466
2540
  userIdWrap.style.display = isWeiboMonitor ? "" : "none";
2467
2541
  }
2468
2542
  function updateScheduleVisibility() {
2469
- const scheduleType = String(scheduleTypeSelect.value || "interval").trim();
2470
- intervalWrap.style.display = scheduleType === "interval" ? "inline-flex" : "none";
2471
- runAtWrap.style.display = scheduleType === "once" || scheduleType === "daily" || scheduleType === "weekly" ? "inline-flex" : "none";
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
+ }
2472
2554
  }
2473
2555
  function updateLikeKeywordsState() {
2474
2556
  likeKeywordsInput.disabled = !likesInput.checked;
@@ -2476,6 +2558,9 @@ function renderTasksPanel(root, ctx2) {
2476
2558
  function collectFormData() {
2477
2559
  const maxRunsRaw = String(maxRunsInput.value || "").trim();
2478
2560
  const maxRunsNum = maxRunsRaw ? Number(maxRunsRaw) : 0;
2561
+ const scheduleMode = scheduleTypeSelect.value;
2562
+ const periodicType = periodicTypeSelect.value;
2563
+ const runAtText = String(runAtInput.value || "").trim();
2479
2564
  return {
2480
2565
  id: String(editingIdInput.value || "").trim() || void 0,
2481
2566
  name: String(nameInput.value || "").trim(),
@@ -2491,9 +2576,10 @@ function renderTasksPanel(root, ctx2) {
2491
2576
  collectBody: bodyInput.checked,
2492
2577
  doLikes: likesInput.checked,
2493
2578
  likeKeywords: String(likeKeywordsInput.value || "").trim(),
2494
- scheduleType: scheduleTypeSelect.value,
2579
+ scheduleMode,
2580
+ periodicType,
2495
2581
  intervalMinutes: Math.max(1, Number(intervalInput.value || 30) || 30),
2496
- runAt: toIsoOrNull(String(runAtInput.value || "")),
2582
+ runAt: toIsoOrNull(runAtText),
2497
2583
  maxRuns: Number.isFinite(maxRunsNum) && maxRunsNum > 0 ? Math.max(1, Math.floor(maxRunsNum)) : null
2498
2584
  };
2499
2585
  }
@@ -2513,7 +2599,9 @@ function renderTasksPanel(root, ctx2) {
2513
2599
  bodyInput.checked = task.commandArgv?.["fetch-body"] !== false;
2514
2600
  likesInput.checked = task.commandArgv?.["do-likes"] === true;
2515
2601
  likeKeywordsInput.value = String(task.commandArgv?.["like-keywords"] || "").trim();
2516
- scheduleTypeSelect.value = String(task.scheduleType || "interval");
2602
+ const uiSchedule = inferUiScheduleEditorState(task);
2603
+ scheduleTypeSelect.value = uiSchedule.mode;
2604
+ periodicTypeSelect.value = uiSchedule.periodicType;
2517
2605
  intervalInput.value = String(task.intervalMinutes || 30);
2518
2606
  runAtInput.value = toLocalDatetimeValue(task.runAt);
2519
2607
  maxRunsInput.value = task.maxRuns ? String(task.maxRuns) : "";
@@ -2536,7 +2624,8 @@ function renderTasksPanel(root, ctx2) {
2536
2624
  bodyInput.checked = DEFAULT_FORM.collectBody;
2537
2625
  likesInput.checked = DEFAULT_FORM.doLikes;
2538
2626
  likeKeywordsInput.value = DEFAULT_FORM.likeKeywords;
2539
- scheduleTypeSelect.value = DEFAULT_FORM.scheduleType;
2627
+ scheduleTypeSelect.value = DEFAULT_FORM.scheduleMode;
2628
+ periodicTypeSelect.value = DEFAULT_FORM.periodicType;
2540
2629
  intervalInput.value = String(DEFAULT_FORM.intervalMinutes);
2541
2630
  runAtInput.value = "";
2542
2631
  maxRunsInput.value = "";
@@ -2570,19 +2659,29 @@ function renderTasksPanel(root, ctx2) {
2570
2659
  }
2571
2660
  }
2572
2661
  function renderRecentTasks() {
2573
- const rows = sortedTasksByRecent().slice(0, 8);
2662
+ const rows = sortedTasksByRecent();
2574
2663
  if (rows.length === 0) {
2575
2664
  recentTasksList.innerHTML = '<div class="muted" style="font-size:12px;">\u6682\u65E0\u4EFB\u52A1</div>';
2576
2665
  return;
2577
2666
  }
2578
2667
  recentTasksList.innerHTML = rows.map((task) => `
2579
- <div class="task-row" style="display:flex;gap:var(--gap-sm);padding:var(--gap-xs)0;border-bottom:1px solid var(--border-subtle);align-items:center;">
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;">
2580
2669
  <span style="flex:1;font-size:12px;">${task.name || task.id}</span>
2581
2670
  <span style="font-size:11px;color:var(--text-tertiary);">${task.commandType}</span>
2582
2671
  <span style="font-size:11px;color:${task.enabled ? "var(--accent-success)" : "var(--text-muted)"};">${task.enabled ? "\u542F\u7528" : "\u7981\u7528"}</span>
2583
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>
2584
2674
  </div>
2585
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
+ });
2586
2685
  recentTasksList.querySelectorAll(".edit-task-btn").forEach((btn) => {
2587
2686
  btn.addEventListener("click", () => {
2588
2687
  const taskId = btn.dataset.id || "";
@@ -2592,6 +2691,14 @@ function renderTasksPanel(root, ctx2) {
2592
2691
  applyTaskToForm(task, "edit");
2593
2692
  });
2594
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
+ });
2595
2702
  }
2596
2703
  function updateStats() {
2597
2704
  statSaved.textContent = String(tasks.length);
@@ -2599,21 +2706,27 @@ function renderTasksPanel(root, ctx2) {
2599
2706
  const totalRunCount = tasks.reduce((sum, row) => sum + (Number(row.runCount) || 0), 0);
2600
2707
  statToday.textContent = String(totalRunCount);
2601
2708
  }
2602
- async function runJsonScript(scriptPath, args, timeoutMs = 6e4) {
2603
- const ret = await ctx2.api.cmdRunJson({
2604
- title: `task-panel ${args.join(" ")}`.trim(),
2605
- cwd: "",
2606
- args: [scriptPath, ...args, "--json"],
2607
- timeoutMs
2608
- });
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);
2609
2714
  if (!ret?.ok) {
2610
- const reason = String(ret?.error || ret?.stderr || ret?.stdout || "unknown_error").trim();
2611
- throw new Error(reason || "command failed");
2715
+ const reason = String(ret?.error || "schedule command failed").trim();
2716
+ throw new Error(reason || "schedule command failed");
2612
2717
  }
2613
- return ret.json || {};
2718
+ return ret?.json ?? ret;
2614
2719
  }
2615
- async function runScheduleJson(args, timeoutMs = 6e4) {
2616
- 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;
2617
2730
  }
2618
2731
  async function loadQuotaStatus() {
2619
2732
  try {
@@ -2642,7 +2755,7 @@ function renderTasksPanel(root, ctx2) {
2642
2755
  }
2643
2756
  async function loadTasks() {
2644
2757
  try {
2645
- const out = await runScheduleJson(["list"]);
2758
+ const out = await invokeSchedule({ action: "list" });
2646
2759
  tasks = parseTaskRows(out);
2647
2760
  renderHistorySelect();
2648
2761
  renderRecentTasks();
@@ -2653,7 +2766,6 @@ function renderTasksPanel(root, ctx2) {
2653
2766
  }
2654
2767
  function buildCommandArgv(data) {
2655
2768
  const argv = {
2656
- profile: data.profileId,
2657
2769
  keyword: data.keyword,
2658
2770
  "max-notes": data.targetCount,
2659
2771
  target: data.targetCount,
@@ -2663,36 +2775,70 @@ function renderTasksPanel(root, ctx2) {
2663
2775
  "do-likes": data.doLikes,
2664
2776
  "like-keywords": data.likeKeywords
2665
2777
  };
2778
+ const profileId = String(data.profileId || "").trim();
2779
+ if (profileId) argv.profile = profileId;
2666
2780
  if (String(data.taskType || "").startsWith("weibo-")) {
2667
- argv["task-type"] = commandTypeToWeiboTaskType(data.taskType);
2668
2781
  if (data.userId) argv["user-id"] = data.userId;
2669
2782
  }
2670
2783
  return argv;
2671
2784
  }
2672
- function validateBeforeSave(data) {
2673
- if (!data.profileId) return "\u8BF7\u8F93\u5165 Profile ID";
2674
- if (isKeywordRequired(data.taskType) && !data.keyword) return "\u8BF7\u8F93\u5165\u5173\u952E\u8BCD";
2675
- if (data.taskType === "weibo-monitor" && !data.userId) return "\u5FAE\u535A monitor \u4EFB\u52A1\u9700\u8981 user-id";
2676
- if (data.scheduleType !== "interval" && !data.runAt) return `${data.scheduleType} \u4EFB\u52A1\u9700\u8981\u6267\u884C\u65F6\u95F4`;
2677
- return null;
2678
- }
2679
- function buildSaveArgs(data) {
2680
- const args = data.id ? ["update", data.id] : ["add"];
2681
- args.push("--name", data.name || fallbackTaskName(data));
2682
- args.push("--enabled", String(data.enabled));
2683
- args.push("--command-type", data.taskType || "xhs-unified");
2684
- args.push("--schedule-type", data.scheduleType);
2685
- if (data.scheduleType === "interval") {
2686
- args.push("--interval-minutes", String(data.intervalMinutes));
2687
- } else {
2688
- args.push("--run-at", String(data.runAt || ""));
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
+ };
2689
2793
  }
2690
- args.push("--max-runs", data.maxRuns === null ? "0" : String(data.maxRuns));
2691
- args.push("--argv-json", JSON.stringify(buildCommandArgv(data)));
2692
- 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
+ };
2693
2839
  }
2694
2840
  async function runSavedTask(taskId, data) {
2695
- const out = await runScheduleJson(["run", taskId], 0);
2841
+ const out = await invokeSchedule({ action: "run", taskId, timeoutMs: 0 });
2696
2842
  const runId = String(
2697
2843
  out?.result?.runResult?.lastRunId || out?.result?.runResult?.runId || out?.runResult?.runId || ""
2698
2844
  ).trim();
@@ -2708,23 +2854,30 @@ function renderTasksPanel(root, ctx2) {
2708
2854
  target: data.targetCount,
2709
2855
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
2710
2856
  };
2857
+ ctx2.activeRunId = runId || null;
2711
2858
  }
2712
2859
  if (typeof ctx2.setActiveTab === "function") {
2713
2860
  ctx2.setActiveTab(data.taskType === "xhs-unified" ? "dashboard" : "scheduler");
2714
2861
  }
2715
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
+ }
2716
2870
  async function saveTask(runImmediately = false) {
2717
2871
  const data = collectFormData();
2718
- const invalidReason = validateBeforeSave(data);
2719
- if (invalidReason) {
2720
- alert(invalidReason);
2872
+ if (runImmediately && data.scheduleMode === "immediate") {
2873
+ await runWithoutSave();
2721
2874
  return;
2722
2875
  }
2723
2876
  saveBtn.disabled = true;
2724
2877
  runBtn.disabled = true;
2725
2878
  runEphemeralBtn.disabled = true;
2726
2879
  try {
2727
- const out = await runScheduleJson(buildSaveArgs(data));
2880
+ const out = await invokeSchedule({ action: "save", payload: toSchedulePayload(data) });
2728
2881
  const taskId = String(out?.task?.id || data.id || "").trim();
2729
2882
  if (!taskId) {
2730
2883
  throw new Error("task id missing after save");
@@ -2734,7 +2887,13 @@ function renderTasksPanel(root, ctx2) {
2734
2887
  await loadTasks();
2735
2888
  historySelect.value = taskId;
2736
2889
  if (runImmediately) {
2737
- 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
+ });
2738
2897
  } else {
2739
2898
  alert("\u4EFB\u52A1\u5DF2\u4FDD\u5B58");
2740
2899
  }
@@ -2746,83 +2905,13 @@ function renderTasksPanel(root, ctx2) {
2746
2905
  runEphemeralBtn.disabled = false;
2747
2906
  }
2748
2907
  }
2749
- function buildEphemeralRunSpec(data) {
2750
- if (!data.profileId) return null;
2751
- if (data.taskType === "xhs-unified") {
2752
- if (!data.keyword) return null;
2753
- return {
2754
- title: `xhs: ${data.keyword}`,
2755
- groupKey: "xhs-unified",
2756
- args: [
2757
- xhsScript,
2758
- "--profile",
2759
- data.profileId,
2760
- "--keyword",
2761
- data.keyword,
2762
- "--target",
2763
- String(data.targetCount),
2764
- "--max-notes",
2765
- String(data.targetCount),
2766
- "--env",
2767
- data.env,
2768
- "--do-comments",
2769
- String(data.collectComments),
2770
- "--fetch-body",
2771
- String(data.collectBody),
2772
- "--do-likes",
2773
- String(data.doLikes),
2774
- "--like-keywords",
2775
- data.likeKeywords
2776
- ]
2777
- };
2778
- }
2779
- if (data.taskType === "weibo-search") {
2780
- if (!data.keyword) return null;
2781
- return {
2782
- title: `weibo: ${data.keyword}`,
2783
- groupKey: "weibo-search",
2784
- args: [
2785
- weiboScript,
2786
- "search",
2787
- "--profile",
2788
- data.profileId,
2789
- "--keyword",
2790
- data.keyword,
2791
- "--target",
2792
- String(data.targetCount),
2793
- "--env",
2794
- data.env
2795
- ]
2796
- };
2797
- }
2798
- return null;
2799
- }
2800
2908
  async function runWithoutSave() {
2801
2909
  const data = collectFormData();
2802
- if (!data.profileId) {
2803
- alert("\u8BF7\u8F93\u5165 Profile ID");
2804
- return;
2805
- }
2806
- if (isKeywordRequired(data.taskType) && !data.keyword) {
2807
- alert("\u8BF7\u8F93\u5165\u5173\u952E\u8BCD");
2808
- return;
2809
- }
2810
- if (data.taskType === "weibo-monitor" && !data.userId) {
2811
- alert("\u5FAE\u535A monitor \u4EFB\u52A1\u9700\u8981 user-id");
2812
- return;
2813
- }
2814
- const spec = buildEphemeralRunSpec(data);
2815
- if (!spec) {
2816
- alert(`\u5F53\u524D\u4EFB\u52A1\u7C7B\u578B\u6682\u4E0D\u652F\u6301\u4EC5\u6267\u884C(\u4E0D\u4FDD\u5B58): ${data.taskType}`);
2817
- return;
2818
- }
2819
2910
  runEphemeralBtn.disabled = true;
2820
2911
  try {
2821
- const ret = await ctx2.api.cmdSpawn({
2822
- title: spec.title,
2823
- cwd: "",
2824
- args: spec.args,
2825
- groupKey: spec.groupKey
2912
+ const ret = await invokeTaskRunEphemeral({
2913
+ commandType: data.taskType,
2914
+ argv: buildCommandArgv(data)
2826
2915
  });
2827
2916
  const runId = String(ret?.runId || "").trim();
2828
2917
  if (runId) {
@@ -2830,17 +2919,19 @@ function renderTasksPanel(root, ctx2) {
2830
2919
  updateStats();
2831
2920
  }
2832
2921
  if (typeof ctx2.setStatus === "function") {
2833
- ctx2.setStatus(`started: ${spec.title}`);
2922
+ ctx2.setStatus(`started: ${data.taskType}`);
2834
2923
  }
2835
2924
  if (data.taskType === "xhs-unified" && ctx2 && typeof ctx2 === "object") {
2925
+ const resolvedProfile = String(ret?.profile || data.profileId || "").trim();
2836
2926
  ctx2.xhsCurrentRun = {
2837
2927
  runId: runId || null,
2838
2928
  taskId: null,
2839
- profileId: data.profileId,
2929
+ profileId: resolvedProfile,
2840
2930
  keyword: data.keyword,
2841
2931
  target: data.targetCount,
2842
2932
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
2843
2933
  };
2934
+ ctx2.activeRunId = runId || null;
2844
2935
  }
2845
2936
  if (typeof ctx2.setActiveTab === "function") {
2846
2937
  ctx2.setActiveTab(data.taskType === "xhs-unified" ? "dashboard" : "scheduler");
@@ -2873,6 +2964,7 @@ function renderTasksPanel(root, ctx2) {
2873
2964
  });
2874
2965
  taskTypeSelect.addEventListener("change", () => updatePlatformFields());
2875
2966
  scheduleTypeSelect.addEventListener("change", () => updateScheduleVisibility());
2967
+ periodicTypeSelect.addEventListener("change", () => updateScheduleVisibility());
2876
2968
  likesInput.addEventListener("change", () => updateLikeKeywordsState());
2877
2969
  saveBtn.addEventListener("click", () => {
2878
2970
  void saveTask(false);
@@ -2906,6 +2998,14 @@ function renderTasksPanel(root, ctx2) {
2906
2998
  }
2907
2999
  applyTaskToForm(task, "clone");
2908
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
+ });
2909
3009
  gotoSchedulerBtn.addEventListener("click", () => {
2910
3010
  if (typeof ctx2.setActiveTab === "function") {
2911
3011
  ctx2.setActiveTab("scheduler");
@@ -2993,6 +3093,11 @@ function renderDashboard(root, ctx2) {
2993
3093
  <div id="recent-errors-empty" class="muted" style="font-size: 12px;">\u6682\u65E0\u9519\u8BEF</div>
2994
3094
  <ul id="recent-errors-list" style="margin: 6px 0 0 16px; padding: 0; font-size: 12px; line-height: 1.5; display:none;"></ul>
2995
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>
2996
3101
  `;
2997
3102
  runSummaryGrid.appendChild(runSummaryCard);
2998
3103
  root.appendChild(runSummaryGrid);
@@ -3087,6 +3192,8 @@ function renderDashboard(root, ctx2) {
3087
3192
  const errorCountText = root.querySelector("#error-count-text");
3088
3193
  const recentErrorsEmpty = root.querySelector("#recent-errors-empty");
3089
3194
  const recentErrorsList = root.querySelector("#recent-errors-list");
3195
+ const likedLinksEmpty = root.querySelector("#liked-links-empty");
3196
+ const likedLinksList = root.querySelector("#liked-links-list");
3090
3197
  const logsContainer = root.querySelector("#logs-container");
3091
3198
  const toggleLogsBtn = root.querySelector("#toggle-logs-btn");
3092
3199
  const pauseBtn = root.querySelector("#pause-btn");
@@ -3103,23 +3210,175 @@ function renderDashboard(root, ctx2) {
3103
3210
  let startTime = Date.now();
3104
3211
  let stoppedAt = null;
3105
3212
  let elapsedTimer = null;
3213
+ let statePollTimer = null;
3106
3214
  let unsubscribeState = null;
3107
3215
  let unsubscribeCmd = null;
3108
- 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();
3109
3220
  let activeStatus = "";
3110
3221
  let errorCountTotal = 0;
3111
3222
  const recentErrors = [];
3223
+ const likedLinks = /* @__PURE__ */ new Map();
3112
3224
  const maxLogs = 500;
3113
3225
  const maxRecentErrors = 8;
3114
- const initialTaskId = String(ctx2?.xhsCurrentRun?.taskId || ctx2?.activeTaskConfigId || "").trim();
3226
+ const maxLikedLinks = 30;
3227
+ const initialTaskId = String(contextRun?.taskId || ctx2?.activeTaskConfigId || "").trim();
3115
3228
  if (initialTaskId) {
3116
3229
  taskConfigId.textContent = initialTaskId;
3117
3230
  }
3118
3231
  const normalizeStatus = (value) => String(value || "").trim().toLowerCase();
3119
3232
  const isRunningStatus = (value) => ["running", "queued", "pending", "starting"].includes(normalizeStatus(value));
3120
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
+ }
3121
3377
  function renderRunSummary() {
3122
3378
  runIdText.textContent = activeRunId || "-";
3379
+ if (ctx2 && typeof ctx2 === "object") {
3380
+ ctx2.activeRunId = activeRunId || null;
3381
+ }
3123
3382
  errorCountText.textContent = String(errorCountTotal);
3124
3383
  recentErrorsList.innerHTML = "";
3125
3384
  if (recentErrors.length === 0) {
@@ -3131,18 +3390,35 @@ function renderDashboard(root, ctx2) {
3131
3390
  recentErrorsList.style.display = "block";
3132
3391
  recentErrors.forEach((item) => {
3133
3392
  const li = document.createElement("li");
3134
- 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
+ }
3135
3409
  recentErrorsList.appendChild(li);
3136
3410
  });
3411
+ renderLikedLinks();
3137
3412
  }
3138
- function pushRecentError(message, source = "runtime") {
3413
+ function pushRecentError(message, source = "runtime", details = null) {
3139
3414
  const msg = String(message || "").trim();
3140
3415
  if (!msg) return;
3141
3416
  errorCountTotal += 1;
3142
3417
  recentErrors.push({
3143
3418
  ts: (/* @__PURE__ */ new Date()).toLocaleTimeString("zh-CN", { hour12: false }),
3144
3419
  source: String(source || "runtime").trim() || "runtime",
3145
- message: msg
3420
+ message: msg,
3421
+ details: normalizeDetails(details)
3146
3422
  });
3147
3423
  while (recentErrors.length > maxRecentErrors) recentErrors.shift();
3148
3424
  renderRunSummary();
@@ -3164,6 +3440,19 @@ function renderDashboard(root, ctx2) {
3164
3440
  clearInterval(elapsedTimer);
3165
3441
  elapsedTimer = null;
3166
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
+ }
3167
3456
  function addLog(line, type = "info") {
3168
3457
  const ts = (/* @__PURE__ */ new Date()).toLocaleTimeString("zh-CN", { hour12: false });
3169
3458
  const logLine = createEl("div", { className: "log-line" });
@@ -3176,8 +3465,39 @@ function renderDashboard(root, ctx2) {
3176
3465
  logsContainer.scrollTop = logsContainer.scrollHeight;
3177
3466
  }
3178
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
+ }
3179
3494
  function updateFromTaskState(state) {
3180
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
+ }
3181
3501
  const progressObj = state.progress && typeof state.progress === "object" ? state.progress : null;
3182
3502
  const processedRaw = progressObj?.processed ?? progressObj?.current ?? state.progress ?? state.collected ?? state.current ?? 0;
3183
3503
  const totalRaw = progressObj?.total ?? state.total ?? state.target ?? state.maxNotes ?? 0;
@@ -3230,6 +3550,7 @@ function renderDashboard(root, ctx2) {
3230
3550
  if (state.profileId) {
3231
3551
  const aliases = ctx2.api?.settings?.profileAliases || {};
3232
3552
  taskAccount.textContent = aliases[state.profileId] || state.profileId;
3553
+ activeProfileId = String(state.profileId || "").trim();
3233
3554
  }
3234
3555
  const taskId = String(state.taskId || state.scheduleTaskId || state.configTaskId || "").trim();
3235
3556
  if (taskId) {
@@ -3271,26 +3592,32 @@ function renderDashboard(root, ctx2) {
3271
3592
  }
3272
3593
  }
3273
3594
  if (state.error) {
3274
- pushRecentError(String(state.error), "state");
3595
+ pushRecentError(String(state.error), "state", state);
3275
3596
  }
3276
3597
  }
3277
3598
  function pickTaskFromList(tasks) {
3278
3599
  const target = activeRunId;
3279
- const running = tasks.find((item) => isRunningStatus(item?.status));
3280
3600
  const sorted = [...tasks].sort((a, b) => {
3281
3601
  const aTs = Number(a?.updatedAt ?? a?.completedAt ?? a?.startedAt ?? 0) || 0;
3282
3602
  const bTs = Number(b?.updatedAt ?? b?.completedAt ?? b?.startedAt ?? 0) || 0;
3283
3603
  return bTs - aTs;
3284
3604
  });
3605
+ const running = sorted.find((item) => isRunningStatus(item?.status)) || null;
3285
3606
  const latest = sorted[0] || null;
3607
+ const launchingFresh = Number.isFinite(contextStartedAtMs) && contextStartedAtMs > 0 && Date.now() - contextStartedAtMs < 12e4;
3286
3608
  if (target) {
3287
3609
  const matched = tasks.find((item) => String(item?.runId || "").trim() === target);
3288
3610
  if (matched) {
3289
- if (isRunningStatus(matched?.status)) return matched;
3290
- if (running) return running;
3291
- if (latest && String(latest?.runId || "").trim() !== target) return latest;
3292
3611
  return matched;
3293
3612
  }
3613
+ if (launchingFresh) {
3614
+ return null;
3615
+ }
3616
+ if (running) return running;
3617
+ return null;
3618
+ }
3619
+ if (launchingFresh) {
3620
+ return running || null;
3294
3621
  }
3295
3622
  return running || latest || null;
3296
3623
  }
@@ -3301,29 +3628,14 @@ function renderDashboard(root, ctx2) {
3301
3628
  currentPhase.textContent = "\u8FD0\u884C\u4E2D";
3302
3629
  currentAction.textContent = "\u542F\u52A8 autoscript";
3303
3630
  activeStatus = "running";
3304
- statCollected.textContent = "0";
3305
- statSuccess.textContent = "0";
3306
- statFailed.textContent = "0";
3307
- statRemaining.textContent = "0";
3308
- progressPercent.textContent = "0%";
3309
- progressBar.style.width = "0%";
3310
- commentsCount = 0;
3311
- likesCount = 0;
3312
- likesSkippedCount = 0;
3313
- likesAlreadyCount = 0;
3314
- likesDedupCount = 0;
3315
- statComments.textContent = `0\u6761`;
3316
- statLikes.textContent = `0\u6B21 (\u8DF3\u8FC7:0, \u5DF2\u8D5E:0, \u53BB\u91CD:0)`;
3317
3631
  const ts = Date.parse(String(payload.ts || "")) || Date.now();
3318
- startTime = ts;
3319
- stoppedAt = null;
3320
- updateElapsed();
3321
- startElapsedTimer();
3322
3632
  if (payload.runId) {
3323
3633
  activeRunId = String(payload.runId || "").trim() || activeRunId;
3324
3634
  }
3635
+ resetDashboardForNewRun("\u65B0\u4EFB\u52A1\u542F\u52A8", ts);
3325
3636
  if (payload.keyword) taskKeyword.textContent = String(payload.keyword);
3326
3637
  if (payload.maxNotes) taskTarget.textContent = String(payload.maxNotes);
3638
+ if (payload.profileId) activeProfileId = String(payload.profileId || "").trim();
3327
3639
  if (payload.taskId) {
3328
3640
  const taskId = String(payload.taskId || "").trim();
3329
3641
  if (taskId) {
@@ -3372,6 +3684,23 @@ function renderDashboard(root, ctx2) {
3372
3684
  likesAlreadyCount = Math.max(0, likesAlreadyCount + already);
3373
3685
  likesDedupCount = Math.max(0, likesDedupCount + dedup);
3374
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();
3375
3704
  }
3376
3705
  return;
3377
3706
  }
@@ -3380,7 +3709,7 @@ function renderDashboard(root, ctx2) {
3380
3709
  statFailed.textContent = String(failed + 1);
3381
3710
  const opId = String(payload?.operationId || "").trim();
3382
3711
  const err = String(payload?.error || payload?.message || payload?.code || event).trim();
3383
- pushRecentError(opId ? `${opId}: ${err}` : err, event);
3712
+ pushRecentError(opId ? `${opId}: ${err}` : err, event, payload);
3384
3713
  return;
3385
3714
  }
3386
3715
  if (event === "xhs.unified.merged") {
@@ -3402,7 +3731,7 @@ function renderDashboard(root, ctx2) {
3402
3731
  currentPhase.textContent = reason && reason !== "script_failure" ? "\u5DF2\u7ED3\u675F" : "\u5931\u8D25";
3403
3732
  currentAction.textContent = reason || "stop";
3404
3733
  if (reason && !successReasons.has(reason)) {
3405
- pushRecentError(`stop reason=${reason}`, event);
3734
+ pushRecentError(`stop reason=${reason}`, event, payload);
3406
3735
  }
3407
3736
  renderRunSummary();
3408
3737
  }
@@ -3460,6 +3789,7 @@ function renderDashboard(root, ctx2) {
3460
3789
  if (profile?.profileId) {
3461
3790
  const aliases = ctx2.api?.settings?.profileAliases || {};
3462
3791
  taskAccount.textContent = aliases[profile.profileId] || profile.profileId;
3792
+ activeProfileId = String(profile.profileId || "").trim();
3463
3793
  }
3464
3794
  const runId = String(profile?.runId || summary?.runId || "").trim();
3465
3795
  if (runId) {
@@ -3535,7 +3865,7 @@ function renderDashboard(root, ctx2) {
3535
3865
  if (payload.action) addLog(String(payload.action), "info");
3536
3866
  if (payload.error) {
3537
3867
  addLog(String(payload.error), "error");
3538
- pushRecentError(String(payload.error), "state");
3868
+ pushRecentError(String(payload.error), "state", payload);
3539
3869
  }
3540
3870
  }
3541
3871
  });
@@ -3552,10 +3882,12 @@ function renderDashboard(root, ctx2) {
3552
3882
  unsubscribeCmd = ctx2.api.onCmdEvent((evt) => {
3553
3883
  if (paused) return;
3554
3884
  const runId = String(evt?.runId || "").trim();
3555
- 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) {
3556
3888
  activeRunId = runId;
3557
3889
  activeStatus = "running";
3558
- stoppedAt = null;
3890
+ resetDashboardForNewRun("\u8FDB\u7A0B\u542F\u52A8");
3559
3891
  renderRunSummary();
3560
3892
  }
3561
3893
  if (activeRunId && runId && runId !== activeRunId) return;
@@ -3564,7 +3896,7 @@ function renderDashboard(root, ctx2) {
3564
3896
  parseLineEvent(String(evt.line || "").trim());
3565
3897
  } else if (evt.type === "stderr") {
3566
3898
  addLog(evt.line, "error");
3567
- pushRecentError(String(evt.line || ""), "stderr");
3899
+ pushRecentError(String(evt.line || ""), "stderr", evt);
3568
3900
  const failed = Number(statFailed.textContent || "0") || 0;
3569
3901
  statFailed.textContent = String(failed + 1);
3570
3902
  } else if (evt.type === "exit") {
@@ -3579,7 +3911,7 @@ function renderDashboard(root, ctx2) {
3579
3911
  stopElapsedTimer();
3580
3912
  }
3581
3913
  if (Number(evt.exitCode || 0) !== 0) {
3582
- pushRecentError(`\u8FDB\u7A0B\u9000\u51FA code=${evt.exitCode ?? "null"}`, "exit");
3914
+ pushRecentError(`\u8FDB\u7A0B\u9000\u51FA code=${evt.exitCode ?? "null"}`, "exit", evt);
3583
3915
  }
3584
3916
  renderRunSummary();
3585
3917
  }
@@ -3590,13 +3922,17 @@ function renderDashboard(root, ctx2) {
3590
3922
  try {
3591
3923
  const config = await ctx2.api.configLoadLast();
3592
3924
  if (config) {
3593
- taskKeyword.textContent = config.keyword || "-";
3594
- taskTarget.textContent = String(config.target || 50);
3595
- 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) {
3596
3932
  const aliases = ctx2.api?.settings?.profileAliases || {};
3597
3933
  taskAccount.textContent = aliases[config.lastProfileId] || config.lastProfileId;
3598
3934
  }
3599
- const taskId = String(config.taskId || ctx2?.activeTaskConfigId || "").trim();
3935
+ const taskId = String(contextRun?.taskId || config.taskId || ctx2?.activeTaskConfigId || "").trim();
3600
3936
  if (taskId) {
3601
3937
  taskConfigId.textContent = taskId;
3602
3938
  }
@@ -3660,23 +3996,25 @@ function renderDashboard(root, ctx2) {
3660
3996
  }
3661
3997
  setTimeout(() => {
3662
3998
  if (typeof ctx2.setActiveTab === "function") {
3663
- ctx2.setActiveTab("config");
3999
+ ctx2.setActiveTab("tasks");
3664
4000
  }
3665
4001
  }, 1500);
3666
4002
  }
3667
4003
  };
3668
4004
  backConfigBtn.onclick = () => {
3669
4005
  if (typeof ctx2.setActiveTab === "function") {
3670
- ctx2.setActiveTab("config");
4006
+ ctx2.setActiveTab("tasks");
3671
4007
  }
3672
4008
  };
3673
4009
  renderRunSummary();
3674
4010
  loadTaskInfo();
3675
4011
  subscribeToUpdates();
3676
4012
  fetchCurrentState();
4013
+ startStatePoll();
3677
4014
  startElapsedTimer();
3678
4015
  return () => {
3679
4016
  stopElapsedTimer();
4017
+ stopStatePoll();
3680
4018
  if (unsubscribeState) unsubscribeState();
3681
4019
  if (unsubscribeCmd) unsubscribeCmd();
3682
4020
  if (unsubscribeBus) unsubscribeBus();
@@ -3724,6 +4062,14 @@ function toTimestamp(value) {
3724
4062
  if (!Number.isFinite(parsed)) return null;
3725
4063
  return parsed;
3726
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
+ }
3727
4073
  function renderAccountManager(root, ctx2) {
3728
4074
  root.innerHTML = "";
3729
4075
  const autoSyncTimers = /* @__PURE__ */ new Map();
@@ -3746,7 +4092,7 @@ function renderAccountManager(root, ctx2) {
3746
4092
  </div>
3747
4093
  <div class="env-item" id="env-firefox">
3748
4094
  <span class="icon" style="color: var(--text-4);">\u25CB</span>
3749
- <span>Camoufox Runtime (python -m camoufox)</span>
4095
+ <span>\u6D4F\u89C8\u5668\u5185\u6838\uFF08Camoufox Firefox\uFF09</span>
3750
4096
  </div>
3751
4097
  </div>
3752
4098
  <div class="btn-group" style="margin-top: var(--gap);">
@@ -3789,15 +4135,18 @@ function renderAccountManager(root, ctx2) {
3789
4135
  let busUnsubscribe = null;
3790
4136
  async function checkEnvironment() {
3791
4137
  try {
3792
- const [camo, services, firefox] = await Promise.all([
3793
- ctx2.api.envCheckCamo(),
3794
- ctx2.api.envCheckServices(),
3795
- ctx2.api.envCheckFirefox()
3796
- ]);
3797
- updateEnvItem("env-camo", camo.installed);
3798
- updateEnvItem("env-unified", services.unifiedApi);
3799
- updateEnvItem("env-browser", services.camoRuntime);
3800
- updateEnvItem("env-firefox", firefox.installed);
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);
3801
4150
  } catch (err) {
3802
4151
  console.error("Environment check failed:", err);
3803
4152
  }
@@ -3811,6 +4160,18 @@ function renderAccountManager(root, ctx2) {
3811
4160
  envCheckInFlight = false;
3812
4161
  }
3813
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
+ }
3814
4175
  function updateEnvItem(id, ok) {
3815
4176
  const el = root.querySelector(`#${id}`);
3816
4177
  if (!el) return;
@@ -3826,7 +4187,11 @@ function renderAccountManager(root, ctx2) {
3826
4187
  platform: normalizePlatform(row.platform),
3827
4188
  statusView: row.valid ? "valid" : row.status === "pending" ? "pending" : "expired",
3828
4189
  lastCheckAt: toTimestamp(row.updatedAt)
3829
- }));
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
+ });
3830
4195
  renderAccountList();
3831
4196
  } catch (err) {
3832
4197
  console.error("Failed to load accounts:", err);
@@ -3860,11 +4225,11 @@ function renderAccountManager(root, ctx2) {
3860
4225
  const nameDiv = createEl("div", { style: "min-width: 0; flex: 1;" }, [
3861
4226
  createEl("div", { className: "account-name", style: "display: flex; gap: 6px; align-items: center;" }, [
3862
4227
  createEl("span", { style: "font-size: 13px;" }, [platform.icon]),
3863
- createEl("span", {}, [acc.alias || acc.name || acc.profileId]),
4228
+ createEl("span", {}, [acc.alias || acc.name || formatProfileTag2(acc.profileId)]),
3864
4229
  createEl("span", { style: "font-size: 11px; color: var(--text-3);" }, [platform.label])
3865
4230
  ]),
3866
4231
  createEl("div", { className: "account-alias", style: "font-size: 11px; color: var(--text-3);" }, [
3867
- `profile: ${acc.profileId} \xB7 \u4E0A\u6B21\u68C0\u67E5: ${formatTs(acc.lastCheckAt)}`
4232
+ `profile: ${formatProfileTag2(acc.profileId)} (${acc.profileId}) \xB7 \u4E0A\u6B21\u68C0\u67E5: ${formatTs(acc.lastCheckAt)}`
3868
4233
  ])
3869
4234
  ]);
3870
4235
  const statusBadge = createEl("span", {
@@ -4186,11 +4551,6 @@ Profile ID: ${acc.profileId}
4186
4551
  }
4187
4552
 
4188
4553
  // src/renderer/tabs-new/scheduler.mts
4189
- function commandTypeToWeiboTaskType2(commandType) {
4190
- if (commandType === "weibo-search") return "search";
4191
- if (commandType === "weibo-monitor") return "monitor";
4192
- return "timeline";
4193
- }
4194
4554
  function renderSchedulerPanel(root, ctx2) {
4195
4555
  root.innerHTML = "";
4196
4556
  const pageIndicator = createEl("div", { className: "page-indicator" }, [
@@ -4259,18 +4619,25 @@ function renderSchedulerPanel(root, ctx2) {
4259
4619
  <div>
4260
4620
  <label>\u8C03\u5EA6\u7C7B\u578B</label>
4261
4621
  <select id="scheduler-type" style="width: 140px;">
4262
- <option value="interval">\u5FAA\u73AF\u95F4\u9694</option>
4263
- <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>
4264
4631
  <option value="daily">\u6BCF\u5929</option>
4265
4632
  <option value="weekly">\u6BCF\u5468</option>
4266
4633
  </select>
4267
4634
  </div>
4268
- <div id="scheduler-interval-wrap">
4635
+ <div id="scheduler-interval-wrap" style="display:none;">
4269
4636
  <label>\u95F4\u9694\u5206\u949F</label>
4270
4637
  <input id="scheduler-interval" type="number" min="1" value="30" style="width: 120px;" />
4271
4638
  </div>
4272
4639
  <div id="scheduler-runat-wrap" style="display:none;">
4273
- <label>\u951A\u70B9\u65F6\u95F4</label>
4640
+ <label>\u6267\u884C\u65F6\u95F4</label>
4274
4641
  <input id="scheduler-runat" type="datetime-local" style="width: 220px;" />
4275
4642
  </div>
4276
4643
  <div>
@@ -4280,8 +4647,9 @@ function renderSchedulerPanel(root, ctx2) {
4280
4647
  </div>
4281
4648
  <div class="row">
4282
4649
  <div>
4283
- <label>Profile</label>
4284
- <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>
4285
4653
  </div>
4286
4654
  <div>
4287
4655
  <label>\u5173\u952E\u8BCD</label>
@@ -4331,6 +4699,7 @@ function renderSchedulerPanel(root, ctx2) {
4331
4699
  </div>
4332
4700
  <div class="btn-group" style="margin-top: var(--gap);">
4333
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>
4334
4703
  <button id="scheduler-reset-btn" class="secondary" style="flex:1;">\u6E05\u7A7A\u8868\u5355</button>
4335
4704
  </div>
4336
4705
  `;
@@ -4359,12 +4728,15 @@ function renderSchedulerPanel(root, ctx2) {
4359
4728
  const nameInput = root.querySelector("#scheduler-name");
4360
4729
  const enabledInput = root.querySelector("#scheduler-enabled");
4361
4730
  const typeSelect = root.querySelector("#scheduler-type");
4731
+ const periodicTypeWrap = root.querySelector("#scheduler-periodic-type-wrap");
4732
+ const periodicTypeSelect = root.querySelector("#scheduler-periodic-type");
4362
4733
  const intervalWrap = root.querySelector("#scheduler-interval-wrap");
4363
4734
  const runAtWrap = root.querySelector("#scheduler-runat-wrap");
4364
4735
  const intervalInput = root.querySelector("#scheduler-interval");
4365
4736
  const runAtInput = root.querySelector("#scheduler-runat");
4366
4737
  const maxRunsInput = root.querySelector("#scheduler-max-runs");
4367
4738
  const profileInput = root.querySelector("#scheduler-profile");
4739
+ const profileHint = root.querySelector("#scheduler-profile-hint");
4368
4740
  const keywordInput = root.querySelector("#scheduler-keyword");
4369
4741
  const userIdWrap = root.querySelector("#scheduler-user-id-wrap");
4370
4742
  const userIdInput = root.querySelector("#scheduler-user-id");
@@ -4376,8 +4748,10 @@ function renderSchedulerPanel(root, ctx2) {
4376
4748
  const dryRunInput = root.querySelector("#scheduler-dryrun");
4377
4749
  const likeKeywordsInput = root.querySelector("#scheduler-like-keywords");
4378
4750
  const saveBtn = root.querySelector("#scheduler-save-btn");
4751
+ const runNowBtn = root.querySelector("#scheduler-run-now-btn");
4379
4752
  const resetBtn = root.querySelector("#scheduler-reset-btn");
4380
4753
  let tasks = [];
4754
+ let accountRows = [];
4381
4755
  let daemonRunId = "";
4382
4756
  let unsubscribeCmd = null;
4383
4757
  let pendingFocusTaskId = String(ctx2?.activeTaskConfigId || "").trim();
@@ -4394,14 +4768,21 @@ function renderSchedulerPanel(root, ctx2) {
4394
4768
  function openConfigTab(taskId) {
4395
4769
  setActiveTaskContext(taskId);
4396
4770
  if (typeof ctx2.setActiveTab === "function") {
4397
- ctx2.setActiveTab("config");
4771
+ ctx2.setActiveTab("tasks");
4398
4772
  }
4399
4773
  }
4400
4774
  function updateTypeFields() {
4401
- const mode = typeSelect.value;
4402
- const useRunAt = mode === "once" || mode === "daily" || mode === "weekly";
4403
- runAtWrap.style.display = useRunAt ? "" : "none";
4404
- intervalWrap.style.display = useRunAt ? "none" : "";
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
+ }
4405
4786
  }
4406
4787
  function updateTaskTypeOptions() {
4407
4788
  const platform = platformSelect.value;
@@ -4411,6 +4792,36 @@ function renderSchedulerPanel(root, ctx2) {
4411
4792
  taskTypeSelect.value = taskTypeSelect.options[0]?.value || "";
4412
4793
  }
4413
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
+ }
4414
4825
  }
4415
4826
  function updatePlatformFields() {
4416
4827
  const commandType = String(taskTypeSelect.value || "").trim();
@@ -4423,7 +4834,8 @@ function renderSchedulerPanel(root, ctx2) {
4423
4834
  editingIdInput.value = "";
4424
4835
  nameInput.value = "";
4425
4836
  enabledInput.checked = true;
4426
- typeSelect.value = "interval";
4837
+ typeSelect.value = "immediate";
4838
+ periodicTypeSelect.value = "interval";
4427
4839
  intervalInput.value = "30";
4428
4840
  runAtInput.value = "";
4429
4841
  maxRunsInput.value = "";
@@ -4446,7 +4858,6 @@ function renderSchedulerPanel(root, ctx2) {
4446
4858
  const maxRuns = maxRunsRaw ? Math.max(1, Number(maxRunsRaw) || 1) : null;
4447
4859
  const commandType = String(taskTypeSelect.value || "xhs-unified").trim();
4448
4860
  const argv = {
4449
- profile: profileInput.value.trim(),
4450
4861
  keyword: keywordInput.value.trim(),
4451
4862
  "max-notes": Number(maxNotesInput.value || 50) || 50,
4452
4863
  env: envSelect.value,
@@ -4456,19 +4867,40 @@ function renderSchedulerPanel(root, ctx2) {
4456
4867
  headless: headlessInput.checked,
4457
4868
  "dry-run": dryRunInput.checked
4458
4869
  };
4870
+ const profileValue = profileInput.value.trim();
4871
+ if (profileValue) argv.profile = profileValue;
4459
4872
  if (commandType.startsWith("weibo")) {
4460
- argv["task-type"] = commandTypeToWeiboTaskType2(commandType);
4461
4873
  argv["user-id"] = userIdInput.value.trim();
4462
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
+ }
4463
4895
  return {
4464
4896
  id: editingIdInput.value.trim(),
4465
4897
  name: nameInput.value.trim(),
4466
4898
  enabled: enabledInput.checked,
4467
4899
  commandType,
4468
- scheduleType: typeSelect.value,
4900
+ scheduleType,
4469
4901
  intervalMinutes: Number(intervalInput.value || 30) || 30,
4470
- runAt: toIsoOrNull(runAtInput.value),
4471
- maxRuns,
4902
+ runAt,
4903
+ maxRuns: maxRunsFinal,
4472
4904
  argv
4473
4905
  };
4474
4906
  }
@@ -4481,7 +4913,9 @@ function renderSchedulerPanel(root, ctx2) {
4481
4913
  editingIdInput.value = task.id;
4482
4914
  nameInput.value = task.name || "";
4483
4915
  enabledInput.checked = task.enabled !== false;
4484
- typeSelect.value = task.scheduleType;
4916
+ const uiSchedule = inferUiScheduleEditorState(task);
4917
+ typeSelect.value = uiSchedule.mode;
4918
+ periodicTypeSelect.value = uiSchedule.periodicType;
4485
4919
  intervalInput.value = String(task.intervalMinutes || 30);
4486
4920
  runAtInput.value = toLocalDatetimeValue(task.runAt);
4487
4921
  maxRunsInput.value = task.maxRuns ? String(task.maxRuns) : "";
@@ -4499,19 +4933,27 @@ function renderSchedulerPanel(root, ctx2) {
4499
4933
  updatePlatformFields();
4500
4934
  updateTypeFields();
4501
4935
  }
4502
- async function runScheduleJson(args, timeoutMs = 6e4) {
4503
- const script = ctx2.api.pathJoin("apps", "webauto", "entry", "schedule.mjs");
4504
- const ret = await ctx2.api.cmdRunJson({
4505
- title: `schedule ${args.join(" ")}`,
4506
- cwd: "",
4507
- args: [script, ...args, "--json"],
4508
- timeoutMs
4509
- });
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);
4510
4941
  if (!ret?.ok) {
4511
- const reason = String(ret?.error || ret?.stderr || ret?.stdout || "unknown_error").trim();
4942
+ const reason = String(ret?.error || "schedule command failed").trim();
4512
4943
  throw new Error(reason || "schedule command failed");
4513
4944
  }
4514
- 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;
4515
4957
  }
4516
4958
  function downloadJson(fileName, payload) {
4517
4959
  const text = JSON.stringify(payload, null, 2);
@@ -4533,7 +4975,7 @@ function renderSchedulerPanel(root, ctx2) {
4533
4975
  const card = createEl("div", {
4534
4976
  style: "border:1px solid var(--border); border-radius:10px; padding:10px; margin-bottom:10px; background: var(--panel-soft);"
4535
4977
  });
4536
- const scheduleText = task.scheduleType === "once" ? `once @ ${task.runAt || "-"}` : task.scheduleType === "daily" ? `daily @ ${task.runAt || "-"}` : task.scheduleType === "weekly" ? `weekly @ ${task.runAt || "-"}` : `interval ${task.intervalMinutes}m`;
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)`;
4537
4979
  const statusText = task.lastStatus ? `${task.lastStatus} / run=${task.runCount} / fail=${task.failCount}` : "never run";
4538
4980
  const headRow = createEl("div", { style: "display:flex; justify-content:space-between; gap:8px; margin-bottom:6px;" });
4539
4981
  headRow.appendChild(createEl("div", { style: "font-weight:600;" }, [task.name || task.id]));
@@ -4582,7 +5024,7 @@ function renderSchedulerPanel(root, ctx2) {
4582
5024
  runBtn.onclick = async () => {
4583
5025
  try {
4584
5026
  setActiveTaskContext(task.id);
4585
- const out = await runScheduleJson(["run", task.id], 0);
5027
+ const out = await invokeSchedule({ action: "run", taskId: task.id, timeoutMs: 0 });
4586
5028
  const runId = String(
4587
5029
  out?.result?.runResult?.lastRunId || out?.result?.runResult?.runId || out?.runResult?.runId || ""
4588
5030
  ).trim();
@@ -4596,6 +5038,7 @@ function renderSchedulerPanel(root, ctx2) {
4596
5038
  target: Number(argv["max-notes"] || argv.target || 0) || 0,
4597
5039
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
4598
5040
  };
5041
+ ctx2.activeRunId = runId || null;
4599
5042
  }
4600
5043
  if (typeof ctx2.setStatus === "function") {
4601
5044
  ctx2.setStatus(`running: ${task.id}`);
@@ -4610,7 +5053,7 @@ function renderSchedulerPanel(root, ctx2) {
4610
5053
  };
4611
5054
  exportBtn.onclick = async () => {
4612
5055
  try {
4613
- const out = await runScheduleJson(["export", task.id]);
5056
+ const out = await invokeSchedule({ action: "export", taskId: task.id });
4614
5057
  downloadJson(`${task.id}.json`, out);
4615
5058
  } catch (err) {
4616
5059
  alert(`\u5BFC\u51FA\u5931\u8D25: ${err?.message || String(err)}`);
@@ -4619,7 +5062,7 @@ function renderSchedulerPanel(root, ctx2) {
4619
5062
  delBtn.onclick = async () => {
4620
5063
  if (!confirm(`\u786E\u8BA4\u5220\u9664\u4EFB\u52A1 ${task.id} ?`)) return;
4621
5064
  try {
4622
- await runScheduleJson(["delete", task.id]);
5065
+ await invokeSchedule({ action: "delete", taskId: task.id });
4623
5066
  await refreshList();
4624
5067
  } catch (err) {
4625
5068
  alert(`\u5220\u9664\u5931\u8D25: ${err?.message || String(err)}`);
@@ -4629,7 +5072,7 @@ function renderSchedulerPanel(root, ctx2) {
4629
5072
  }
4630
5073
  }
4631
5074
  async function refreshList() {
4632
- const out = await runScheduleJson(["list"]);
5075
+ const out = await invokeSchedule({ action: "list" });
4633
5076
  tasks = parseTaskRows(out);
4634
5077
  if (!pendingFocusTaskId) {
4635
5078
  pendingFocusTaskId = String(ctx2?.activeTaskConfigId || "").trim();
@@ -4649,48 +5092,8 @@ function renderSchedulerPanel(root, ctx2) {
4649
5092
  }
4650
5093
  async function saveTask() {
4651
5094
  const payload = readFormAsPayload();
4652
- if (!payload.name) {
4653
- alert("\u4EFB\u52A1\u540D\u4E0D\u80FD\u4E3A\u7A7A");
4654
- return;
4655
- }
4656
- if (!payload.argv.profile && !payload.argv.profiles && !payload.argv.profilepool) {
4657
- alert("profile/profiles/profilepool \u81F3\u5C11\u586B\u5199\u4E00\u4E2A");
4658
- return;
4659
- }
4660
- const commandType = String(payload.commandType || "").trim();
4661
- const keywordRequired = commandType === "xhs-unified" || commandType === "weibo-search" || commandType === "1688-search";
4662
- if (keywordRequired && !payload.argv.keyword) {
4663
- alert("\u5173\u952E\u8BCD\u4E0D\u80FD\u4E3A\u7A7A");
4664
- return;
4665
- }
4666
- if (commandType === "weibo-monitor" && !payload.argv["user-id"]) {
4667
- alert("\u5FAE\u535A monitor \u4EFB\u52A1\u9700\u8981 user-id");
4668
- return;
4669
- }
4670
- const args = payload.id ? ["update", payload.id] : ["add"];
4671
- args.push("--name", payload.name);
4672
- args.push("--enabled", String(payload.enabled));
4673
- args.push("--command-type", commandType || "xhs-unified");
4674
- args.push("--schedule-type", payload.scheduleType);
4675
- if (payload.scheduleType === "once") {
4676
- if (!payload.runAt) {
4677
- alert("\u4E00\u6B21\u6027\u4EFB\u52A1\u9700\u8981\u951A\u70B9\u65F6\u95F4");
4678
- return;
4679
- }
4680
- args.push("--run-at", payload.runAt);
4681
- } else if (payload.scheduleType === "daily" || payload.scheduleType === "weekly") {
4682
- if (!payload.runAt) {
4683
- alert(`${payload.scheduleType} \u4EFB\u52A1\u9700\u8981\u951A\u70B9\u65F6\u95F4`);
4684
- return;
4685
- }
4686
- args.push("--run-at", payload.runAt);
4687
- } else {
4688
- args.push("--interval-minutes", String(Math.max(1, payload.intervalMinutes)));
4689
- }
4690
- args.push("--max-runs", payload.maxRuns === null ? "0" : String(payload.maxRuns));
4691
- args.push("--argv-json", JSON.stringify(payload.argv));
4692
5095
  try {
4693
- const out = await runScheduleJson(args);
5096
+ const out = await invokeSchedule({ action: "save", payload });
4694
5097
  const savedId = String(out?.task?.id || payload.id || "").trim();
4695
5098
  pendingFocusTaskId = savedId;
4696
5099
  if (savedId) setActiveTaskContext(savedId);
@@ -4699,9 +5102,44 @@ function renderSchedulerPanel(root, ctx2) {
4699
5102
  alert(`\u4FDD\u5B58\u5931\u8D25: ${err?.message || String(err)}`);
4700
5103
  }
4701
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
+ }
4702
5140
  async function runDueNow() {
4703
5141
  try {
4704
- const out = await runScheduleJson(["run-due", "--limit", "20"], 0);
5142
+ const out = await invokeSchedule({ action: "run-due", limit: 20, timeoutMs: 0 });
4705
5143
  alert(`\u5230\u70B9\u4EFB\u52A1\u6267\u884C\u5B8C\u6210\uFF1Adue=${out.count || 0}, success=${out.success || 0}, failed=${out.failed || 0}`);
4706
5144
  await refreshList();
4707
5145
  } catch (err) {
@@ -4710,7 +5148,7 @@ function renderSchedulerPanel(root, ctx2) {
4710
5148
  }
4711
5149
  async function exportAll() {
4712
5150
  try {
4713
- const out = await runScheduleJson(["export"]);
5151
+ const out = await invokeSchedule({ action: "export" });
4714
5152
  downloadJson("webauto-schedules.json", out);
4715
5153
  } catch (err) {
4716
5154
  alert(`\u5BFC\u51FA\u5931\u8D25: ${err?.message || String(err)}`);
@@ -4725,7 +5163,7 @@ function renderSchedulerPanel(root, ctx2) {
4725
5163
  if (!file) return;
4726
5164
  try {
4727
5165
  const text = await file.text();
4728
- await runScheduleJson(["import", "--payload-json", text, "--mode", "merge"]);
5166
+ await invokeSchedule({ action: "import", payloadJson: text, mode: "merge" });
4729
5167
  await refreshList();
4730
5168
  } catch (err) {
4731
5169
  alert(`\u5BFC\u5165\u5931\u8D25: ${err?.message || String(err)}`);
@@ -4739,13 +5177,7 @@ function renderSchedulerPanel(root, ctx2) {
4739
5177
  return;
4740
5178
  }
4741
5179
  const interval = Math.max(5, Number(daemonIntervalInput.value || 30) || 30);
4742
- const script = ctx2.api.pathJoin("apps", "webauto", "entry", "schedule.mjs");
4743
- const ret = await ctx2.api.cmdSpawn({
4744
- title: `schedule daemon ${interval}s`,
4745
- cwd: "",
4746
- args: [script, "daemon", "--interval-sec", String(interval), "--limit", "20", "--json"],
4747
- groupKey: "scheduler"
4748
- });
5180
+ const ret = await invokeSchedule({ action: "daemon-start", intervalSec: interval, limit: 20 });
4749
5181
  daemonRunId = String(ret?.runId || "").trim();
4750
5182
  setDaemonStatus(daemonRunId ? `daemon: \u8FD0\u884C\u4E2D (${daemonRunId})` : "daemon: \u542F\u52A8\u5931\u8D25");
4751
5183
  }
@@ -4764,7 +5196,9 @@ function renderSchedulerPanel(root, ctx2) {
4764
5196
  platformSelect.addEventListener("change", updateTaskTypeOptions);
4765
5197
  taskTypeSelect.addEventListener("change", updatePlatformFields);
4766
5198
  typeSelect.addEventListener("change", updateTypeFields);
5199
+ periodicTypeSelect.addEventListener("change", updateTypeFields);
4767
5200
  saveBtn.onclick = () => void saveTask();
5201
+ runNowBtn.onclick = () => void runNowFromForm();
4768
5202
  resetBtn.onclick = () => resetForm();
4769
5203
  refreshBtn.onclick = () => void refreshList();
4770
5204
  runDueBtn.onclick = () => void runDueNow();
@@ -4816,6 +5250,8 @@ var tabs = [
4816
5250
  var tabsEl = document.getElementById("tabs");
4817
5251
  var contentEl = document.getElementById("content");
4818
5252
  var statusEl = document.getElementById("status");
5253
+ var appTitleEl = document.getElementById("app-title");
5254
+ var appVersionEl = document.getElementById("app-version");
4819
5255
  var activeTabCleanup = null;
4820
5256
  var mutableApi = { ...window.api || {}, settings: null };
4821
5257
  var tabIcons = {
@@ -4891,6 +5327,22 @@ function startDesktopHeartbeat() {
4891
5327
  async function loadSettings() {
4892
5328
  await ctx.refreshSettings();
4893
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
+ }
4894
5346
  function focusTabButton(tabId) {
4895
5347
  const button = tabsEl.querySelector(`[data-tab-id="${tabId}"]`);
4896
5348
  button?.focus();
@@ -4998,6 +5450,7 @@ async function detectStartupTab() {
4998
5450
  }
4999
5451
  async function main() {
5000
5452
  startDesktopHeartbeat();
5453
+ await applyVersionBadge();
5001
5454
  await loadSettings();
5002
5455
  installCmdEvents();
5003
5456
  const startupTab = await detectStartupTab();