@web-auto/webauto 0.1.4 → 0.1.6

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 (174) hide show
  1. package/apps/desktop-console/default-settings.json +2 -2
  2. package/apps/desktop-console/dist/main/index.mjs +915 -85
  3. package/apps/desktop-console/dist/main/preload.mjs +7 -0
  4. package/apps/desktop-console/dist/renderer/index.html +622 -50
  5. package/apps/desktop-console/dist/renderer/index.js +2415 -470
  6. package/apps/desktop-console/dist/renderer/run.mts +6 -5
  7. package/apps/desktop-console/entry/ui-cli.mjs +672 -0
  8. package/apps/desktop-console/entry/ui-console.mjs +416 -29
  9. package/apps/webauto/entry/account.mjs +89 -53
  10. package/apps/webauto/entry/browser-status.mjs +7 -10
  11. package/apps/webauto/entry/lib/account-detect.mjs +254 -28
  12. package/apps/webauto/entry/lib/account-store.mjs +219 -30
  13. package/apps/webauto/entry/lib/bus-publish.mjs +63 -0
  14. package/apps/webauto/entry/lib/camo-cli.mjs +93 -0
  15. package/apps/webauto/entry/lib/profilepool.mjs +14 -5
  16. package/apps/webauto/entry/lib/quota-status.mjs +23 -0
  17. package/apps/webauto/entry/lib/schedule-store.mjs +1068 -0
  18. package/apps/webauto/entry/profilepool.mjs +106 -17
  19. package/apps/webauto/entry/schedule.mjs +612 -0
  20. package/apps/webauto/entry/weibo-unified.mjs +134 -0
  21. package/apps/webauto/entry/xhs-install.mjs +236 -29
  22. package/apps/webauto/entry/xhs-status.mjs +5 -2
  23. package/apps/webauto/entry/xhs-unified.mjs +631 -98
  24. package/apps/webauto/resources/container-library/weibo/weibo_detail_page/comment_item/container.json +40 -0
  25. package/apps/webauto/resources/container-library/weibo/weibo_detail_page/reply_expand_button/container.json +38 -0
  26. package/apps/webauto/resources/container-library/weibo/weibo_detail_page/reply_list/container.json +37 -0
  27. package/apps/webauto/resources/container-library/weibo/weibo_search_page/container.json +8 -3
  28. package/apps/webauto/resources/container-library/weibo/weibo_search_page/login_anchor/container.json +30 -0
  29. package/apps/webauto/resources/container-library/weibo/weibo_search_page/search_bar/container.json +47 -0
  30. package/apps/webauto/resources/container-library/weibo/weibo_search_page/search_button/container.json +39 -0
  31. package/bin/camoufox-cli.mjs +61 -0
  32. package/bin/webauto.mjs +301 -54
  33. package/dist/modules/camo-backend/src/index.js +49 -1
  34. package/dist/modules/camo-backend/src/internal/BrowserSession.js +572 -3
  35. package/dist/modules/camo-backend/src/internal/SessionManager.js +13 -1
  36. package/dist/modules/camo-backend/src/internal/storage-paths.js +6 -0
  37. package/dist/modules/collection-manager/bloom-filter.js +91 -0
  38. package/dist/modules/collection-manager/date-utils.js +275 -0
  39. package/dist/modules/collection-manager/index.js +258 -0
  40. package/dist/modules/collection-manager/storage.js +195 -0
  41. package/dist/modules/collection-manager/types.js +47 -0
  42. package/dist/modules/logging/src/index.js +1 -1
  43. package/dist/modules/process-registry/index.js +230 -0
  44. package/dist/modules/rate-limiter/index.js +242 -0
  45. package/dist/modules/workflow/blocks/ExecuteWeiboSearchBlock.js +128 -0
  46. package/dist/modules/workflow/blocks/PersistXhsNoteBlock.js +7 -3
  47. package/dist/modules/workflow/blocks/RenderMarkdown.js +4 -1
  48. package/dist/modules/workflow/blocks/WeiboCollectCommentsBlock.js +282 -0
  49. package/dist/modules/workflow/blocks/WeiboCollectFromLinksBlock.js +283 -0
  50. package/dist/modules/workflow/blocks/WeiboCollectSearchLinksBlock.js +208 -0
  51. package/dist/modules/workflow/blocks/WeiboCollectTimelineListBlock.js +128 -0
  52. package/dist/modules/workflow/blocks/WeiboCollectUserPostsListBlock.js +127 -0
  53. package/dist/modules/workflow/blocks/helpers/downloadPaths.js +21 -0
  54. package/dist/modules/workflow/config/workflowRegistry.js +2 -0
  55. package/dist/modules/workflow/definitions/weibo-search-workflow-v1.js +47 -0
  56. package/dist/modules/workflow/src/runner.js +6 -0
  57. package/dist/modules/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.js +4 -0
  58. package/dist/modules/xiaohongshu/app/src/blocks/Phase3InteractBlock.js +2 -2
  59. package/dist/modules/xiaohongshu/app/src/blocks/helpers/sharding.js +123 -0
  60. package/dist/modules/xiaohongshu/app/src/container-registry/src/index.d.ts +37 -0
  61. package/dist/modules/xiaohongshu/app/src/container-registry/src/index.js +184 -0
  62. package/dist/modules/xiaohongshu/app/src/workflow/blocks/AnchorVerificationBlock.d.ts +31 -0
  63. package/dist/modules/xiaohongshu/app/src/workflow/blocks/AnchorVerificationBlock.js +71 -0
  64. package/dist/modules/xiaohongshu/app/src/workflow/blocks/DetectPageStateBlock.d.ts +48 -0
  65. package/dist/modules/xiaohongshu/app/src/workflow/blocks/DetectPageStateBlock.js +259 -0
  66. package/dist/modules/xiaohongshu/app/src/workflow/blocks/ErrorRecoveryBlock.d.ts +28 -0
  67. package/dist/modules/xiaohongshu/app/src/workflow/blocks/ErrorRecoveryBlock.js +319 -0
  68. package/dist/modules/xiaohongshu/app/src/workflow/blocks/WaitSearchPermitBlock.d.ts +36 -0
  69. package/dist/modules/xiaohongshu/app/src/workflow/blocks/WaitSearchPermitBlock.js +162 -0
  70. package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/containerAnchors.d.ts +36 -0
  71. package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/containerAnchors.js +301 -0
  72. package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/operationLogger.d.ts +29 -0
  73. package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/operationLogger.js +195 -0
  74. package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/searchPageState.d.ts +25 -0
  75. package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/searchPageState.js +164 -0
  76. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/MatchCommentsBlock.d.ts +66 -0
  77. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/MatchCommentsBlock.js +139 -0
  78. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1EnsureServicesBlock.d.ts +16 -0
  79. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1EnsureServicesBlock.js +36 -0
  80. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1MonitorCookieBlock.d.ts +27 -0
  81. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1MonitorCookieBlock.js +213 -0
  82. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1StartProfileBlock.d.ts +18 -0
  83. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1StartProfileBlock.js +121 -0
  84. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2CollectLinksBlock.d.ts +34 -0
  85. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2CollectLinksBlock.js +1249 -0
  86. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2SearchBlock.d.ts +17 -0
  87. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2SearchBlock.js +703 -0
  88. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseDetailBlock.d.ts +15 -0
  89. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseDetailBlock.js +41 -0
  90. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseTabsBlock.d.ts +26 -0
  91. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseTabsBlock.js +44 -0
  92. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CollectCommentsBlock.d.ts +29 -0
  93. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CollectCommentsBlock.js +150 -0
  94. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ExtractDetailBlock.d.ts +38 -0
  95. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ExtractDetailBlock.js +117 -0
  96. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenDetailBlock.d.ts +30 -0
  97. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenDetailBlock.js +102 -0
  98. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenTabsBlock.d.ts +23 -0
  99. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenTabsBlock.js +109 -0
  100. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.d.ts +32 -0
  101. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.js +117 -0
  102. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ProcessSingleNoteBlock.d.ts +35 -0
  103. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ProcessSingleNoteBlock.js +114 -0
  104. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ValidateLinksBlock.d.ts +34 -0
  105. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ValidateLinksBlock.js +90 -0
  106. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase3InteractBlock.d.ts +111 -0
  107. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase3InteractBlock.js +1009 -0
  108. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase4MultiTabHarvestBlock.d.ts +20 -0
  109. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase4MultiTabHarvestBlock.js +233 -0
  110. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/ReplyInteractBlock.d.ts +48 -0
  111. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/ReplyInteractBlock.js +291 -0
  112. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/XhsDiscoverFallbackBlock.d.ts +23 -0
  113. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/XhsDiscoverFallbackBlock.js +240 -0
  114. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatchDsl.d.ts +55 -0
  115. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatchDsl.js +126 -0
  116. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatcher.d.ts +21 -0
  117. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatcher.js +99 -0
  118. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/evidence.d.ts +5 -0
  119. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/evidence.js +27 -0
  120. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/sharding.d.ts +37 -0
  121. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/sharding.js +165 -0
  122. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/xhsComments.d.ts +33 -0
  123. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/xhsComments.js +270 -0
  124. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/index.d.ts +9 -0
  125. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/index.js +9 -0
  126. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/checkpoints.d.ts +50 -0
  127. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/checkpoints.js +222 -0
  128. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/controllerAction.d.ts +10 -0
  129. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/controllerAction.js +43 -0
  130. package/dist/services/shared/serviceProcessLogger.js +1 -1
  131. package/dist/services/unified-api/server.js +105 -11
  132. package/modules/camo-backend/src/index.ts +46 -1
  133. package/modules/camo-backend/src/internal/BrowserSession.ts +619 -3
  134. package/modules/camo-backend/src/internal/SessionManager.ts +12 -1
  135. package/modules/camo-backend/src/internal/storage-paths.ts +5 -0
  136. package/modules/camo-runtime/src/autoscript/action-providers/xhs/comments.mjs +38 -2
  137. package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +47 -2
  138. package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +94 -11
  139. package/modules/camo-runtime/src/autoscript/action-providers/xhs.mjs +208 -2
  140. package/modules/camo-runtime/src/autoscript/runtime.mjs +7 -1
  141. package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +76 -43
  142. package/modules/camo-runtime/src/container/runtime-core/operations/index.mjs +75 -1
  143. package/modules/camo-runtime/src/container/runtime-core/operations/selector-scripts.mjs +71 -4
  144. package/modules/camo-runtime/src/container/runtime-core/operations/tab-pool.mjs +183 -27
  145. package/modules/collection-manager/bloom-filter.ts +112 -0
  146. package/modules/collection-manager/date-utils.ts +316 -0
  147. package/modules/collection-manager/index.ts +309 -0
  148. package/modules/collection-manager/package.json +10 -0
  149. package/modules/collection-manager/storage.ts +174 -0
  150. package/modules/collection-manager/types.ts +156 -0
  151. package/modules/logging/src/index.ts +1 -1
  152. package/modules/process-registry/index.ts +284 -0
  153. package/modules/rate-limiter/index.ts +322 -0
  154. package/modules/state/src/paths.ts +9 -1
  155. package/modules/task-scheduler/index.ts +293 -0
  156. package/modules/workflow/blocks/ExecuteWeiboSearchBlock.ts +167 -0
  157. package/modules/workflow/blocks/PersistXhsNoteBlock.ts +7 -3
  158. package/modules/workflow/blocks/RenderMarkdown.ts +4 -1
  159. package/modules/workflow/blocks/WeiboCollectCommentsBlock.ts +339 -0
  160. package/modules/workflow/blocks/WeiboCollectFromLinksBlock.ts +338 -0
  161. package/modules/workflow/blocks/helpers/downloadPaths.ts +16 -0
  162. package/modules/workflow/config/workflowRegistry.ts +2 -0
  163. package/modules/workflow/definitions/weibo-search-workflow-v1.ts +47 -0
  164. package/modules/workflow/src/runner.ts +6 -0
  165. package/modules/xiaohongshu/app/src/blocks/Phase1StartProfileBlock.ts +1 -1
  166. package/modules/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.ts +4 -0
  167. package/modules/xiaohongshu/app/src/blocks/Phase3InteractBlock.ts +2 -3
  168. package/modules/xiaohongshu/app/src/blocks/helpers/sharding.ts +152 -0
  169. package/package.json +13 -4
  170. package/scripts/postinstall-resources.mjs +62 -0
  171. package/scripts/test/run-coverage.mjs +76 -0
  172. package/scripts/weibo/search.ts +49 -0
  173. package/services/shared/serviceProcessLogger.ts +1 -1
  174. package/services/unified-api/server.ts +98 -12
@@ -422,9 +422,7 @@ ${mergedOutput}`);
422
422
  String(timeoutSec),
423
423
  "--check-interval-sec",
424
424
  "2",
425
- "--keep-session",
426
- ...ctx2.settings?.unifiedApiUrl ? ["--unified-api", String(ctx2.settings.unifiedApiUrl)] : [],
427
- ...ctx2.settings?.browserServiceUrl ? ["--browser-service", String(ctx2.settings.browserServiceUrl)] : []
425
+ "--keep-session"
428
426
  ]);
429
427
  await window.api.cmdSpawn({
430
428
  title: `profilepool login-profile ${createdProfileId}`,
@@ -444,8 +442,6 @@ ${mergedOutput}`);
444
442
  window.api.pathJoin("apps", "webauto", "entry", "profilepool.mjs"),
445
443
  "login",
446
444
  kw,
447
- ...ctx2.settings?.unifiedApiUrl ? ["--unified-api", String(ctx2.settings.unifiedApiUrl)] : [],
448
- ...ctx2.settings?.browserServiceUrl ? ["--browser-service", String(ctx2.settings.browserServiceUrl)] : [],
449
445
  "--timeout-sec",
450
446
  String(timeoutSec),
451
447
  ...ensureCount > 0 ? ["--ensure-count", String(ensureCount)] : [],
@@ -497,7 +493,7 @@ function renderRun(root, ctx2) {
497
493
  const targetInput = createEl("input", { value: String(ctx2.settings?.defaultTarget || ""), placeholder: "target", type: "number", min: "1" });
498
494
  const envSel = createEl("select");
499
495
  ["debug", "prod"].forEach((x) => envSel.appendChild(createEl("option", { value: x }, [x])));
500
- envSel.value = ctx2.settings?.defaultEnv || "debug";
496
+ envSel.value = ctx2.settings?.defaultEnv || "prod";
501
497
  const dryRun = createEl("input", { type: "checkbox" });
502
498
  dryRun.checked = ctx2.settings?.defaultDryRun === true;
503
499
  const profileModeSel = createEl("select");
@@ -702,8 +698,6 @@ function renderRun(root, ctx2) {
702
698
  return;
703
699
  }
704
700
  }
705
- const targetNum = Number(target);
706
- persistRunInputs({ keyword, target: Number.isFinite(targetNum) ? targetNum : void 0, env, dryRun: dryRun.checked });
707
701
  const common = buildArgs2([
708
702
  ...keyword ? ["--keyword", keyword] : [],
709
703
  ...target ? ["--target", target] : [],
@@ -716,6 +710,8 @@ function renderRun(root, ctx2) {
716
710
  return;
717
711
  }
718
712
  const profileArgs = resolved.args;
713
+ const targetNum = Number(target);
714
+ persistRunInputs({ keyword, target: Number.isFinite(targetNum) ? targetNum : void 0, env, dryRun: dryRun.checked });
719
715
  const extraArgs = extra ? extra.split(" ").filter(Boolean) : [];
720
716
  let script = "";
721
717
  let args = [];
@@ -1019,7 +1015,7 @@ function renderSettings(root, ctx2) {
1019
1015
  const download = createEl("input", { value: ctx2.settings?.downloadRoot || "" });
1020
1016
  const env = createEl("select");
1021
1017
  ["debug", "prod"].forEach((x) => env.appendChild(createEl("option", { value: x }, [x])));
1022
- env.value = ctx2.settings?.defaultEnv || "debug";
1018
+ env.value = ctx2.settings?.defaultEnv || "prod";
1023
1019
  const keyword = createEl("input", { value: ctx2.settings?.defaultKeyword || "" });
1024
1020
  const loginTimeout = createEl("input", { value: String(ctx2.settings?.timeouts?.loginTimeoutSec || 900), type: "number", min: "30" });
1025
1021
  const cmdTimeout = createEl("input", { value: String(ctx2.settings?.timeouts?.cmdTimeoutSec || 0), type: "number", min: "0" });
@@ -1518,6 +1514,7 @@ function normalizeRow(row) {
1518
1514
  return {
1519
1515
  profileId,
1520
1516
  accountRecordId: asText(row?.accountRecordId),
1517
+ platform: asText(row?.platform) || "xiaohongshu",
1521
1518
  accountId: asText(row?.accountId),
1522
1519
  alias: asText(row?.alias),
1523
1520
  name: asText(row?.name),
@@ -1542,9 +1539,10 @@ async function listAccountProfiles(api) {
1542
1539
  // src/renderer/tabs-new/setup-wizard.mts
1543
1540
  function renderSetupWizard(root, ctx2) {
1544
1541
  root.innerHTML = "";
1542
+ const autoSyncTimers = /* @__PURE__ */ new Map();
1545
1543
  const header = createEl("div", { style: "margin-bottom:20px;" }, [
1546
1544
  createEl("h2", { style: "margin:0 0 8px 0; font-size:20px; color:#dbeafe;" }, ["\u73AF\u5883\u4E0E\u8D26\u6237\u521D\u59CB\u5316"]),
1547
- createEl("div", { className: "muted", style: "font-size:13px;" }, ['\u9996\u6B21\u4F7F\u7528\u5FC5\u987B\u5B8C\u6210\u73AF\u5883\u68C0\u67E5\u4E0E\u8D26\u6237\u767B\u5F55\uFF0C\u4E4B\u540E\u53EF\u5728"\u8D26\u6237\u7BA1\u7406"Tab\u4E2D\u7EF4\u62A4'])
1545
+ createEl("div", { className: "muted", style: "font-size:13px;" }, ["\u5EFA\u8BAE\u5148\u5B8C\u6210\u73AF\u5883\u68C0\u67E5\uFF1B\u8D26\u53F7\u53EF\u5148\u4E0D\u767B\u5F55\uFF0C\u540E\u7EED\u81EA\u52A8\u8BC6\u522B\u8D26\u6237\u540D\u5E76\u56DE\u586B alias"])
1548
1546
  ]);
1549
1547
  root.appendChild(header);
1550
1548
  const bentoGrid = createEl("div", { className: "bento-grid bento-sidebar" });
@@ -1552,42 +1550,60 @@ function renderSetupWizard(root, ctx2) {
1552
1550
  envCard.innerHTML = `
1553
1551
  <div class="bento-title"><span style="color: var(--warning);">\u25CF</span> \u73AF\u5883\u68C0\u67E5</div>
1554
1552
  <div class="env-status-grid">
1555
- <div class="env-item" id="env-camo">
1556
- <span class="icon" style="color: var(--text-4);">\u25CB</span>
1557
- <span>Camoufox CLI</span>
1553
+ <div class="env-item" id="env-camo" style="display:flex; align-items:center; justify-content:space-between; gap:8px;">
1554
+ <span style="display:flex; align-items:center; gap:8px; min-width:0;">
1555
+ <span class="icon" style="color: var(--text-4);">\u25CB</span>
1556
+ <span class="env-label">Camo CLI</span>
1557
+ </span>
1558
+ <button id="repair-camo-btn" class="secondary" style="display:none; flex:0 0 auto;">\u4E00\u952E\u4FEE\u590D</button>
1558
1559
  </div>
1559
- <div class="env-item" id="env-unified">
1560
- <span class="icon" style="color: var(--text-4);">\u25CB</span>
1561
- <span>Unified API (7701)</span>
1560
+ <div class="env-item" id="env-unified" style="display:flex; align-items:center; justify-content:space-between; gap:8px;">
1561
+ <span style="display:flex; align-items:center; gap:8px; min-width:0;">
1562
+ <span class="icon" style="color: var(--text-4);">\u25CB</span>
1563
+ <span class="env-label">Unified API (7701)</span>
1564
+ </span>
1565
+ <button id="repair-core-btn" class="secondary" style="display:none; flex:0 0 auto;">\u4E00\u952E\u4FEE\u590D</button>
1562
1566
  </div>
1563
- <div class="env-item" id="env-browser">
1564
- <span class="icon" style="color: var(--text-4);">\u25CB</span>
1565
- <span>Browser Service (7704)</span>
1567
+ <div class="env-item" id="env-browser" style="display:flex; align-items:center; justify-content:space-between; gap:8px;">
1568
+ <span style="display:flex; align-items:center; gap:8px; min-width:0;">
1569
+ <span class="icon" style="color: var(--text-4);">\u25CB</span>
1570
+ <span class="env-label">Camo Runtime\uFF08\u53EF\u9009\uFF09</span>
1571
+ </span>
1572
+ <button id="repair-core2-btn" class="secondary" style="display:none; flex:0 0 auto;">\u4E00\u952E\u4FEE\u590D</button>
1566
1573
  </div>
1567
- <div class="env-item" id="env-firefox">
1568
- <span class="icon" style="color: var(--text-4);">\u25CB</span>
1569
- <span>Camoufox Runtime</span>
1574
+ <div class="env-item" id="env-firefox" style="display:flex; align-items:center; justify-content:space-between; gap:8px;">
1575
+ <span style="display:flex; align-items:center; gap:8px; min-width:0;">
1576
+ <span class="icon" style="color: var(--text-4);">\u25CB</span>
1577
+ <span class="env-label">Camoufox Browser</span>
1578
+ </span>
1579
+ <button id="repair-runtime-btn" class="secondary" style="display:none; flex:0 0 auto;">\u4E00\u952E\u4FEE\u590D</button>
1570
1580
  </div>
1571
- <div class="env-item" id="env-geoip">
1572
- <span class="icon" style="color: var(--text-4);">\u25CB</span>
1573
- <span>GeoIP Database</span>
1581
+ <div class="env-item" id="env-geoip" style="display:flex; align-items:center; justify-content:space-between; gap:8px;">
1582
+ <span style="display:flex; align-items:center; gap:8px; min-width:0;">
1583
+ <span class="icon" style="color: var(--text-4);">\u25CB</span>
1584
+ <span class="env-label">GeoIP Database\uFF08\u53EF\u9009\uFF09</span>
1585
+ </span>
1586
+ <button id="repair-geoip-btn" class="secondary" style="display:none; flex:0 0 auto;">\u53EF\u9009\u5B89\u88C5</button>
1574
1587
  </div>
1575
1588
  </div>
1576
1589
  <div style="margin-top: var(--gap);">
1577
1590
  <button id="env-check-btn" class="secondary" style="width: 100%;">\u68C0\u67E5\u73AF\u5883</button>
1591
+ <button id="env-repair-all-btn" style="width: 100%; margin-top: 8px; display: none;">\u4E00\u952E\u4FEE\u590D\u7F3A\u5931\u9879</button>
1592
+ <button id="env-reinstall-all-btn" class="secondary" style="width: 100%; margin-top: 8px;">\u4E00\u952E\u5378\u8F7D\u91CD\u88C5\u8D44\u6E90</button>
1578
1593
  </div>
1594
+ <div id="env-repair-history" class="muted" style="margin-top: 10px; font-size: 12px;"></div>
1579
1595
  `;
1580
1596
  bentoGrid.appendChild(envCard);
1581
1597
  const accountCard = createEl("div", { className: "bento-cell" });
1582
1598
  accountCard.innerHTML = `
1583
1599
  <div class="bento-title">\u8D26\u6237\u8BBE\u7F6E</div>
1584
- <div class="account-list" id="account-list" style="margin-bottom: var(--gap); max-height: 200px; overflow: auto;">
1600
+ <div class="account-list" id="account-list" style="margin-bottom: var(--gap);">
1585
1601
  <div style="padding:12px; text-align:center; color:#8b93a6;">\u6682\u65E0\u8D26\u6237\uFF0C\u8BF7\u70B9\u51FB\u4E0B\u65B9\u6309\u94AE\u6DFB\u52A0</div>
1586
1602
  </div>
1587
1603
  <div class="row">
1588
1604
  <div>
1589
- <label>\u65B0\u8D26\u6237\u522B\u540D</label>
1590
- <input id="new-alias-input" placeholder="\u4F8B\u5982: \u7F8E\u98DF\u63A2\u5E97\u8D26\u53F7" style="width: 200px;" />
1605
+ <label>\u65B0\u8D26\u6237\u522B\u540D\uFF08\u53EF\u9009\uFF09</label>
1606
+ <input id="new-alias-input" placeholder="\u53EF\u7559\u7A7A\uFF0C\u767B\u5F55\u540E\u81EA\u52A8\u8BC6\u522B" style="width: 200px;" />
1591
1607
  </div>
1592
1608
  <button id="add-account-btn" style="flex: 0 0 auto; min-width: 120px;">\u6DFB\u52A0\u8D26\u6237</button>
1593
1609
  </div>
@@ -1604,7 +1620,7 @@ function renderSetupWizard(root, ctx2) {
1604
1620
  \u8BF7\u5B8C\u6210\u4E0A\u8FF0\u6B65\u9AA4
1605
1621
  </div>
1606
1622
  <div style="font-size: 12px; color: var(--text-3);" id="setup-status-text">
1607
- \u73AF\u5883\u68C0\u67E5\u548C\u8D26\u6237\u767B\u5F55\u540E\u624D\u80FD\u5F00\u59CB\u4F7F\u7528
1623
+ \u5B8C\u6210\u73AF\u5883\u68C0\u67E5\u540E\u5373\u53EF\u8FDB\u5165\u4E3B\u754C\u9762\uFF1B\u8D26\u53F7\u53EF\u7A0D\u540E\u767B\u5F55
1608
1624
  </div>
1609
1625
  </div>
1610
1626
  <button id="enter-main-btn" class="secondary" disabled style="padding: 12px 32px; font-size: 14px;">\u8FDB\u5165\u4E3B\u754C\u9762</button>
@@ -1613,16 +1629,35 @@ function renderSetupWizard(root, ctx2) {
1613
1629
  statusRow.appendChild(statusCard);
1614
1630
  root.appendChild(statusRow);
1615
1631
  const envCheckBtn = root.querySelector("#env-check-btn");
1632
+ const envRepairAllBtn = root.querySelector("#env-repair-all-btn");
1633
+ const envReinstallAllBtn = root.querySelector("#env-reinstall-all-btn");
1634
+ const repairCamoBtn = root.querySelector("#repair-camo-btn");
1635
+ const repairCoreBtn = root.querySelector("#repair-core-btn");
1636
+ const repairCore2Btn = root.querySelector("#repair-core2-btn");
1637
+ const repairRuntimeBtn = root.querySelector("#repair-runtime-btn");
1638
+ const repairGeoipBtn = root.querySelector("#repair-geoip-btn");
1616
1639
  const addAccountBtn = root.querySelector("#add-account-btn");
1617
1640
  const newAliasInput = root.querySelector("#new-alias-input");
1618
1641
  const enterMainBtn = root.querySelector("#enter-main-btn");
1619
1642
  const accountListEl = root.querySelector("#account-list");
1620
1643
  const setupStatusText = root.querySelector("#setup-status-text");
1644
+ const envRepairHistoryEl = root.querySelector("#env-repair-history");
1621
1645
  let envReady = false;
1622
1646
  let accounts = [];
1647
+ let repairHistory = Array.isArray(ctx2.api?.settings?.envRepairHistory) ? [...ctx2.api.settings.envRepairHistory] : [];
1648
+ let envCheckInFlight = false;
1649
+ let accountCheckInFlight = false;
1650
+ let busUnsubscribe = null;
1623
1651
  const isEnvReady = (snapshot) => Boolean(
1624
- snapshot?.camo?.installed && snapshot?.services?.unifiedApi && snapshot?.services?.browserService && snapshot?.firefox?.installed && snapshot?.geoip?.installed
1652
+ snapshot?.camo?.installed && snapshot?.services?.unifiedApi && snapshot?.firefox?.installed
1625
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
+ });
1626
1661
  async function collectEnvironment() {
1627
1662
  const [camo, services, firefox, geoip] = await Promise.all([
1628
1663
  ctx2.api.envCheckCamo(),
@@ -1635,57 +1670,184 @@ function renderSetupWizard(root, ctx2) {
1635
1670
  function applyEnvironment(snapshot) {
1636
1671
  updateEnvItem("env-camo", snapshot.camo?.installed, snapshot.camo?.version || (snapshot.camo?.installed ? "\u5DF2\u5B89\u88C5" : "\u672A\u5B89\u88C5"));
1637
1672
  updateEnvItem("env-unified", snapshot.services?.unifiedApi, "7701");
1638
- updateEnvItem("env-browser", snapshot.services?.browserService, "7704");
1673
+ updateEnvItem("env-browser", snapshot.services?.camoRuntime, "7704");
1639
1674
  updateEnvItem("env-firefox", snapshot.firefox?.installed, snapshot.firefox?.path ? "\u5DF2\u5B89\u88C5" : "\u672A\u5B89\u88C5");
1640
- updateEnvItem("env-geoip", snapshot.geoip?.installed, snapshot.geoip?.installed ? "\u5DF2\u5B89\u88C5" : "\u672A\u5B89\u88C5");
1675
+ updateEnvItem("env-geoip", snapshot.geoip?.installed, snapshot.geoip?.installed ? "\u5DF2\u5B89\u88C5\uFF08\u53EF\u9009\uFF09" : "\u672A\u5B89\u88C5\uFF08\u53EF\u9009\uFF09");
1641
1676
  envReady = isEnvReady(snapshot);
1677
+ syncRepairButtons(snapshot);
1678
+ }
1679
+ function renderRepairHistory() {
1680
+ if (!envRepairHistoryEl) return;
1681
+ if (!repairHistory.length) {
1682
+ envRepairHistoryEl.textContent = "\u4FEE\u590D\u8BB0\u5F55\uFF1A\u6682\u65E0";
1683
+ return;
1684
+ }
1685
+ const lines = repairHistory.slice(-5).map((item) => {
1686
+ const stamp = item.ts.replace("T", " ").replace("Z", "");
1687
+ const status = item.ok ? "\u6210\u529F" : "\u5931\u8D25";
1688
+ const detail = item.detail ? ` \xB7 ${item.detail}` : "";
1689
+ return `${stamp} ${item.action}\uFF1A${status}${detail}`;
1690
+ });
1691
+ envRepairHistoryEl.textContent = `\u4FEE\u590D\u8BB0\u5F55\uFF1A${lines.join(" | ")}`;
1692
+ }
1693
+ async function pushRepairHistory(entry) {
1694
+ repairHistory = [...repairHistory, entry].slice(-30);
1695
+ if (typeof ctx2.api?.settingsSet === "function") {
1696
+ const updated = await ctx2.api.settingsSet({ envRepairHistory: repairHistory }).catch(() => null);
1697
+ if (updated) {
1698
+ ctx2.api.settings = updated;
1699
+ repairHistory = Array.isArray(updated.envRepairHistory) ? [...updated.envRepairHistory] : repairHistory;
1700
+ }
1701
+ }
1702
+ renderRepairHistory();
1642
1703
  }
1643
- async function autoRepairEnvironment(snapshot) {
1644
- const missingCore = !snapshot.services?.unifiedApi || !snapshot.services?.browserService;
1645
- const missingCamo = !snapshot.camo?.installed;
1646
- const missingRuntime = !snapshot.firefox?.installed;
1647
- const missingGeoIP = !snapshot.geoip?.installed;
1648
- if (!missingCore && !missingCamo && !missingRuntime && !missingGeoIP) return;
1649
- setupStatusText.textContent = "\u68C0\u6D4B\u5230\u4F9D\u8D56\u7F3A\u5931\uFF0C\u6B63\u5728\u81EA\u52A8\u4FEE\u590D...";
1650
- if (missingCore && typeof ctx2.api?.envRepairCore === "function") {
1704
+ async function repairCoreServices() {
1705
+ if (typeof ctx2.api?.envRepairDeps === "function") {
1706
+ setupStatusText.textContent = "\u6B63\u5728\u62C9\u8D77\u6838\u5FC3\u670D\u52A1...";
1707
+ const res = await ctx2.api.envRepairDeps({ core: true }).catch((err) => ({ ok: false, error: err?.message || String(err) }));
1708
+ const ok = res?.ok !== false;
1709
+ const detail = res?.error || (ok ? "" : "\u6838\u5FC3\u670D\u52A1\u542F\u52A8\u5931\u8D25");
1710
+ return { ok, detail };
1711
+ }
1712
+ if (typeof ctx2.api?.envRepairCore === "function") {
1651
1713
  setupStatusText.textContent = "\u6B63\u5728\u62C9\u8D77\u6838\u5FC3\u670D\u52A1...";
1652
- await ctx2.api.envRepairCore().catch(() => null);
1714
+ const res = await ctx2.api.envRepairCore().catch(() => null);
1715
+ const ok = res?.ok !== false;
1716
+ const detail = ok ? "" : "\u6838\u5FC3\u670D\u52A1\u542F\u52A8\u5931\u8D25";
1717
+ return { ok, detail };
1718
+ }
1719
+ return { ok: false, detail: "\u4E0D\u652F\u6301\u4FEE\u590D\u6838\u5FC3\u670D\u52A1" };
1720
+ }
1721
+ async function repairInstall({ browser, geoip, reinstall, uninstall }) {
1722
+ if (typeof ctx2.api?.envRepairDeps === "function") {
1723
+ setupStatusText.textContent = reinstall ? "\u6B63\u5728\u5378\u8F7D\u5E76\u91CD\u88C5\u8D44\u6E90\uFF08Camoufox/GeoIP\uFF09..." : geoip && browser ? "\u6B63\u5728\u5B89\u88C5\u4F9D\u8D56\uFF08Camoufox/GeoIP\uFF09..." : geoip ? "\u6B63\u5728\u5B89\u88C5 GeoIP\uFF08\u53EF\u9009\uFF09..." : "\u6B63\u5728\u5B89\u88C5 Camoufox...";
1724
+ const res = await ctx2.api.envRepairDeps({
1725
+ browser: Boolean(browser),
1726
+ geoip: Boolean(geoip),
1727
+ reinstall: Boolean(reinstall),
1728
+ uninstall: Boolean(uninstall)
1729
+ }).catch((err) => ({ ok: false, error: err?.message || String(err) }));
1730
+ const ok = res?.ok !== false;
1731
+ const detail = res?.error || (ok ? "" : "\u4F9D\u8D56\u5B89\u88C5\u5931\u8D25");
1732
+ return { ok, detail };
1653
1733
  }
1654
- if ((missingCamo || missingRuntime || missingGeoIP) && typeof ctx2.api?.cmdRunJson === "function") {
1655
- setupStatusText.textContent = "\u6B63\u5728\u5B89\u88C5\u4F9D\u8D56\uFF08Camoufox/GeoIP\uFF09...";
1734
+ if (typeof ctx2.api?.cmdRunJson === "function") {
1735
+ setupStatusText.textContent = reinstall ? "\u6B63\u5728\u5378\u8F7D\u5E76\u91CD\u88C5\u8D44\u6E90\uFF08Camoufox/GeoIP\uFF09..." : geoip && browser ? "\u6B63\u5728\u5B89\u88C5\u4F9D\u8D56\uFF08Camoufox/GeoIP\uFF09..." : geoip ? "\u6B63\u5728\u5B89\u88C5 GeoIP\uFF08\u53EF\u9009\uFF09..." : "\u6B63\u5728\u5B89\u88C5 Camoufox...";
1656
1736
  const script = ctx2.api.pathJoin("apps", "webauto", "entry", "xhs-install.mjs");
1657
1737
  const args = [script];
1658
- if (missingRuntime || missingCamo) args.push("--download-browser");
1659
- if (missingGeoIP) args.push("--download-geoip");
1660
- args.push("--ensure-backend");
1661
- await ctx2.api.cmdRunJson({
1738
+ if (reinstall) args.push("--reinstall");
1739
+ else if (uninstall) args.push("--uninstall");
1740
+ if (browser) args.push("--download-browser");
1741
+ if (geoip) args.push("--download-geoip");
1742
+ if (!uninstall) args.push("--ensure-backend");
1743
+ const res = await ctx2.api.cmdRunJson({
1662
1744
  title: "setup auto repair",
1663
1745
  cwd: "",
1664
1746
  args,
1665
1747
  timeoutMs: 3e5
1666
- }).catch(() => null);
1748
+ }).catch((err) => ({ ok: false, error: err?.message || String(err) }));
1749
+ const ok = res?.ok !== false;
1750
+ const detail = res?.error || (ok ? "" : "\u4F9D\u8D56\u5B89\u88C5\u5931\u8D25");
1751
+ return { ok, detail };
1667
1752
  }
1753
+ return { ok: false, detail: "\u4E0D\u652F\u6301\u5B89\u88C5\u4F9D\u8D56" };
1668
1754
  }
1669
- async function checkEnvironment() {
1755
+ async function repairMissing(snapshot) {
1756
+ const missing = getMissing(snapshot);
1757
+ let ok = true;
1758
+ let detail = "";
1759
+ if (missing.core) {
1760
+ const res = await repairCoreServices();
1761
+ if (!res.ok) ok = false;
1762
+ if (res.detail) detail = res.detail;
1763
+ }
1764
+ if (missing.camo || missing.runtime) {
1765
+ const res = await repairInstall({ browser: true });
1766
+ if (!res.ok) ok = false;
1767
+ if (res.detail) detail = res.detail;
1768
+ }
1769
+ if (missing.geoip) {
1770
+ const res = await repairInstall({ geoip: true });
1771
+ if (!res.ok) ok = false;
1772
+ if (res.detail) detail = res.detail;
1773
+ }
1774
+ return { ok, detail };
1775
+ }
1776
+ function syncRepairButtons(snapshot) {
1777
+ const missing = getMissing(snapshot);
1778
+ repairCoreBtn.style.display = missing.core ? "" : "none";
1779
+ repairCore2Btn.style.display = missing.runtimeService ? "" : "none";
1780
+ repairCamoBtn.style.display = missing.camo ? "" : "none";
1781
+ repairRuntimeBtn.style.display = missing.runtime ? "" : "none";
1782
+ repairGeoipBtn.style.display = missing.geoip ? "" : "none";
1783
+ const hasRequiredMissing = missing.core || missing.camo || missing.runtime;
1784
+ envRepairAllBtn.style.display = hasRequiredMissing ? "" : "none";
1785
+ envRepairAllBtn.disabled = !hasRequiredMissing;
1786
+ }
1787
+ async function runRepair(label, action) {
1670
1788
  envCheckBtn.disabled = true;
1671
- envCheckBtn.textContent = "\u68C0\u67E5/\u4FEE\u590D\u4E2D...";
1789
+ envRepairAllBtn.disabled = true;
1790
+ envReinstallAllBtn.disabled = true;
1791
+ repairCamoBtn.disabled = true;
1792
+ repairCoreBtn.disabled = true;
1793
+ repairCore2Btn.disabled = true;
1794
+ repairRuntimeBtn.disabled = true;
1795
+ repairGeoipBtn.disabled = true;
1796
+ setupStatusText.textContent = `${label}\u4E2D...`;
1797
+ let ok = false;
1798
+ let detail = "";
1672
1799
  try {
1673
- const before = await collectEnvironment();
1674
- applyEnvironment(before);
1675
- if (!isEnvReady(before)) {
1676
- await autoRepairEnvironment(before);
1800
+ const result = await action();
1801
+ ok = result?.ok !== false;
1802
+ if (result?.detail) detail = String(result.detail || "");
1803
+ } finally {
1804
+ const latest = await collectEnvironment().catch(() => null);
1805
+ if (latest) {
1806
+ applyEnvironment(latest);
1807
+ updateCompleteStatus();
1808
+ if (!detail) {
1809
+ if (label.includes("Camoufox") || label.includes("Runtime")) {
1810
+ ok = Boolean(latest.firefox?.installed);
1811
+ } else if (label.includes("Camoufox CLI") || label.includes("CLI") || label.includes("camo")) {
1812
+ ok = Boolean(latest.camo?.installed);
1813
+ } else if (label.includes("\u6838\u5FC3")) {
1814
+ ok = Boolean(latest.services?.unifiedApi && latest.services?.camoRuntime);
1815
+ } else {
1816
+ ok = isEnvReady(latest);
1817
+ }
1818
+ }
1677
1819
  }
1678
- const after = await collectEnvironment();
1679
- applyEnvironment(after);
1820
+ setupStatusText.textContent = `${label}${ok ? "\u6210\u529F" : "\u5931\u8D25"}${detail ? `\uFF1A${detail}` : ""}`;
1821
+ await pushRepairHistory({
1822
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1823
+ action: label,
1824
+ ok,
1825
+ detail: detail || void 0
1826
+ });
1827
+ envCheckBtn.disabled = false;
1828
+ envReinstallAllBtn.disabled = false;
1829
+ }
1830
+ }
1831
+ async function checkEnvironment() {
1832
+ envCheckBtn.disabled = true;
1833
+ envCheckBtn.textContent = "\u68C0\u67E5\u4E2D...";
1834
+ try {
1835
+ const snapshot = await collectEnvironment();
1836
+ applyEnvironment(snapshot);
1680
1837
  updateCompleteStatus();
1681
1838
  if (!envReady) {
1682
1839
  const missing = [];
1683
- if (!after?.camo?.installed) missing.push("camo");
1684
- if (!after?.services?.unifiedApi) missing.push("unified-api");
1685
- if (!after?.services?.browserService) missing.push("browser-service");
1686
- if (!after?.firefox?.installed) missing.push("camoufox-runtime");
1687
- if (!after?.geoip?.installed) missing.push("geoip");
1688
- setupStatusText.textContent = `\u81EA\u52A8\u4FEE\u590D\u540E\u4ECD\u7F3A\u5931: ${missing.join(", ")}`;
1840
+ if (!snapshot?.camo?.installed) missing.push("camo");
1841
+ if (!snapshot?.services?.unifiedApi) missing.push("unified-api");
1842
+ if (!snapshot?.firefox?.installed) missing.push("camoufox-runtime");
1843
+ setupStatusText.textContent = `\u5B58\u5728\u5F85\u4FEE\u590D\u9879: ${missing.join(", ")}`;
1844
+ if (!snapshot?.services?.camoRuntime) {
1845
+ setupStatusText.textContent += "\uFF08camo-runtime \u672A\u5C31\u7EEA\uFF0C\u5F53\u524D\u4E3A\u53EF\u9009\uFF09";
1846
+ }
1847
+ } else if (!snapshot?.geoip?.installed) {
1848
+ setupStatusText.textContent = "\u73AF\u5883\u5C31\u7EEA\uFF08GeoIP \u53EF\u9009\uFF0C\u672A\u5B89\u88C5\u4E0D\u5F71\u54CD\u4F7F\u7528\uFF09";
1849
+ } else if (!snapshot?.services?.camoRuntime) {
1850
+ setupStatusText.textContent = "\u73AF\u5883\u5C31\u7EEA\uFF08camo-runtime \u672A\u5C31\u7EEA\uFF0C\u5F53\u524D\u4E0D\u963B\u585E\uFF09";
1689
1851
  }
1690
1852
  } catch (err) {
1691
1853
  console.error("Environment check failed:", err);
@@ -1694,16 +1856,40 @@ function renderSetupWizard(root, ctx2) {
1694
1856
  envCheckBtn.disabled = false;
1695
1857
  envCheckBtn.textContent = "\u91CD\u65B0\u68C0\u67E5";
1696
1858
  }
1859
+ async function tickEnvironment() {
1860
+ if (envCheckInFlight) return;
1861
+ envCheckInFlight = true;
1862
+ try {
1863
+ await checkEnvironment();
1864
+ } finally {
1865
+ envCheckInFlight = false;
1866
+ }
1867
+ }
1697
1868
  function updateEnvItem(id, ok, detail) {
1698
1869
  const el = root.querySelector(`#${id}`);
1699
1870
  if (!el) return;
1700
1871
  const icon = el.querySelector(".icon");
1701
- const text = el.querySelector("span:last-child");
1872
+ const text = el.querySelector(".env-label");
1702
1873
  const baseLabel = el.dataset.label || text.textContent || "";
1703
1874
  el.dataset.label = baseLabel;
1704
1875
  icon.textContent = ok ? "\u2713" : "\u2717";
1705
1876
  icon.style.color = ok ? "var(--success)" : "var(--danger)";
1706
- text.textContent = detail ? `${baseLabel} \xB7 ${detail}` : baseLabel;
1877
+ const safeDetail = String(detail || "").trim();
1878
+ const shouldAppend = safeDetail && !String(baseLabel || "").includes(safeDetail);
1879
+ text.textContent = shouldAppend ? `${baseLabel} \xB7 ${safeDetail}` : baseLabel;
1880
+ }
1881
+ async function tickAccounts() {
1882
+ if (accountCheckInFlight) return;
1883
+ accountCheckInFlight = true;
1884
+ try {
1885
+ await refreshAccounts();
1886
+ const pending = accounts.filter((acc) => acc.status === "pending");
1887
+ for (const acc of pending) {
1888
+ await syncProfileAccount(acc.profileId);
1889
+ }
1890
+ } finally {
1891
+ accountCheckInFlight = false;
1892
+ }
1707
1893
  }
1708
1894
  async function refreshAccounts() {
1709
1895
  try {
@@ -1739,46 +1925,56 @@ function renderSetupWizard(root, ctx2) {
1739
1925
  }
1740
1926
  async function addAccount() {
1741
1927
  const alias = newAliasInput.value.trim();
1742
- if (!alias) {
1743
- alert("\u8BF7\u8F93\u5165\u8D26\u6237\u522B\u540D");
1744
- return;
1745
- }
1746
1928
  addAccountBtn.disabled = true;
1747
1929
  addAccountBtn.textContent = "\u521B\u5EFA\u4E2D...";
1748
1930
  try {
1749
- const batchKey = "xiaohongshu";
1750
1931
  const out = await ctx2.api.cmdRunJson({
1751
- title: "profilepool add",
1932
+ title: "account add",
1752
1933
  cwd: "",
1753
- args: [ctx2.api.pathJoin("apps", "webauto", "entry", "profilepool.mjs"), "add", batchKey, "--json"]
1934
+ args: [
1935
+ ctx2.api.pathJoin("apps", "webauto", "entry", "account.mjs"),
1936
+ "add",
1937
+ "--platform",
1938
+ "xiaohongshu",
1939
+ "--status",
1940
+ "pending",
1941
+ ...alias ? ["--alias", alias] : [],
1942
+ "--json"
1943
+ ]
1754
1944
  });
1755
- if (!out?.ok || !out?.json?.profileId) {
1945
+ const profileId = String(out?.json?.account?.profileId || "").trim();
1946
+ if (!out?.ok || !profileId) {
1756
1947
  alert("\u521B\u5EFA\u8D26\u53F7\u5931\u8D25: " + (out?.error || "\u672A\u77E5\u9519\u8BEF"));
1757
1948
  return;
1758
1949
  }
1759
- const profileId = out.json.profileId;
1760
- const aliases = { ...ctx2.api.settings?.profileAliases, [profileId]: alias };
1761
- await ctx2.api.settingsSet({ profileAliases: aliases });
1762
- if (typeof ctx2.refreshSettings === "function") {
1763
- await ctx2.refreshSettings();
1950
+ if (alias) {
1951
+ const aliases = { ...ctx2.api.settings?.profileAliases, [profileId]: alias };
1952
+ await ctx2.api.settingsSet({ profileAliases: aliases });
1953
+ if (typeof ctx2.refreshSettings === "function") {
1954
+ await ctx2.refreshSettings();
1955
+ }
1764
1956
  }
1957
+ await refreshAccounts();
1958
+ setupStatusText.textContent = `\u8D26\u53F7 ${profileId} \u5DF2\u521B\u5EFA\uFF0C\u7B49\u5F85\u767B\u5F55...`;
1765
1959
  const timeoutSec = ctx2.api.settings?.timeouts?.loginTimeoutSec || 900;
1766
1960
  const loginArgs = [
1767
1961
  ctx2.api.pathJoin("apps", "webauto", "entry", "profilepool.mjs"),
1768
1962
  "login-profile",
1769
1963
  profileId,
1964
+ "--wait-sync",
1965
+ "false",
1770
1966
  "--timeout-sec",
1771
1967
  String(timeoutSec),
1772
1968
  "--keep-session"
1773
1969
  ];
1774
1970
  await ctx2.api.cmdSpawn({
1775
- title: `\u767B\u5F55 ${alias}`,
1971
+ title: `\u767B\u5F55 ${alias || profileId}`,
1776
1972
  cwd: "",
1777
1973
  args: loginArgs,
1778
1974
  groupKey: "profilepool"
1779
1975
  });
1780
1976
  newAliasInput.value = "";
1781
- await refreshAccounts();
1977
+ startAutoSyncProfile(profileId);
1782
1978
  } catch (err) {
1783
1979
  alert("\u6DFB\u52A0\u8D26\u53F7\u5931\u8D25: " + (err?.message || String(err)));
1784
1980
  } finally {
@@ -1788,396 +1984,961 @@ function renderSetupWizard(root, ctx2) {
1788
1984
  }
1789
1985
  function updateCompleteStatus() {
1790
1986
  const hasValidAccount = accounts.some((a) => a.valid);
1791
- const canProceed = envReady && hasValidAccount;
1987
+ const canProceed = envReady;
1792
1988
  enterMainBtn.disabled = !canProceed;
1793
1989
  if (canProceed) {
1794
- setupStatusText.textContent = `\u73AF\u5883\u5C31\u7EEA\uFF0C${accounts.length} \u4E2A\u8D26\u6237\u914D\u7F6E\u5B8C\u6210`;
1990
+ setupStatusText.textContent = hasValidAccount ? `\u73AF\u5883\u5C31\u7EEA\uFF0C${accounts.length} \u4E2A\u8D26\u6237\u914D\u7F6E\u5B8C\u6210` : "\u73AF\u5883\u5C31\u7EEA\uFF0C\u53EF\u5148\u8FDB\u5165\u4E3B\u754C\u9762\u540E\u767B\u5F55\u8D26\u53F7\uFF08alias \u5C06\u5728\u767B\u5F55\u540E\u81EA\u52A8\u8BC6\u522B\uFF09";
1795
1991
  enterMainBtn.className = "";
1796
1992
  } else {
1797
1993
  const missing = [];
1798
1994
  if (!envReady) missing.push("\u73AF\u5883\u68C0\u67E5");
1799
- if (!hasValidAccount) missing.push("\u81F3\u5C11\u4E00\u4E2A\u8D26\u6237");
1995
+ if (!hasValidAccount) missing.push("\u8D26\u6237\u767B\u5F55\uFF08\u53EF\u7A0D\u540E\uFF09");
1800
1996
  setupStatusText.textContent = `\u5C1A\u672A\u5B8C\u6210: ${missing.join("\u3001")}`;
1801
1997
  }
1802
1998
  }
1999
+ function getSettingsAlias(profileId) {
2000
+ return String(ctx2.api?.settings?.profileAliases?.[profileId] || "").trim();
2001
+ }
2002
+ async function upsertAliasFromProfile(profile) {
2003
+ const profileId = String(profile?.profileId || "").trim();
2004
+ const alias = String(profile?.alias || "").trim();
2005
+ if (!profileId || !alias) return;
2006
+ if (getSettingsAlias(profileId) === alias) return;
2007
+ const aliases = { ...ctx2.api?.settings?.profileAliases || {}, [profileId]: alias };
2008
+ await ctx2.api.settingsSet({ profileAliases: aliases }).catch(() => null);
2009
+ if (typeof ctx2.refreshSettings === "function") {
2010
+ await ctx2.refreshSettings().catch(() => null);
2011
+ }
2012
+ }
2013
+ async function syncProfileAccount(profileId) {
2014
+ const id = String(profileId || "").trim();
2015
+ if (!id) return false;
2016
+ const result = await ctx2.api.cmdRunJson({
2017
+ title: `account sync ${id}`,
2018
+ cwd: "",
2019
+ args: [
2020
+ ctx2.api.pathJoin("apps", "webauto", "entry", "account.mjs"),
2021
+ "sync",
2022
+ id,
2023
+ "--pending-while-login",
2024
+ "--json"
2025
+ ],
2026
+ timeoutMs: 2e4
2027
+ }).catch(() => null);
2028
+ const profile = result?.json?.profile;
2029
+ if (!profile || String(profile.profileId || "").trim() !== id) return false;
2030
+ await upsertAliasFromProfile(profile);
2031
+ await refreshAccounts();
2032
+ const hasAccountId = Boolean(String(profile.accountId || "").trim());
2033
+ if (hasAccountId) {
2034
+ setupStatusText.textContent = `\u8D26\u53F7 ${id} \u5DF2\u8BC6\u522B\uFF0Calias=${String(profile.alias || "").trim() || "\u672A\u547D\u540D"}`;
2035
+ return true;
2036
+ }
2037
+ if (String(profile.status || "").trim() === "pending") {
2038
+ setupStatusText.textContent = `\u8D26\u53F7 ${id} \u5F85\u767B\u5F55\uFF0C\u7B49\u5F85\u68C0\u6D4B\u767B\u5F55\u5B8C\u6210...`;
2039
+ }
2040
+ return false;
2041
+ }
2042
+ function startAutoSyncProfile(profileId) {
2043
+ const id = String(profileId || "").trim();
2044
+ if (!id) return;
2045
+ const existing = autoSyncTimers.get(id);
2046
+ if (existing) clearInterval(existing);
2047
+ const timeoutSec = Math.max(30, Number(ctx2.api?.settings?.timeouts?.loginTimeoutSec || 900));
2048
+ const intervalMs = 2e3;
2049
+ const maxAttempts = Math.ceil(timeoutSec * 1e3 / intervalMs);
2050
+ let attempts = 0;
2051
+ void syncProfileAccount(id);
2052
+ const timer = setInterval(() => {
2053
+ attempts += 1;
2054
+ void syncProfileAccount(id).then((done) => {
2055
+ if (done || attempts >= maxAttempts) {
2056
+ const current = autoSyncTimers.get(id);
2057
+ if (current) clearInterval(current);
2058
+ autoSyncTimers.delete(id);
2059
+ if (!done) {
2060
+ setupStatusText.textContent = `\u8D26\u53F7 ${id} \u767B\u5F55\u68C0\u6D4B\u8D85\u65F6\uFF0C\u8BF7\u786E\u8BA4\u5DF2\u5728\u6D4F\u89C8\u5668\u5B8C\u6210\u767B\u5F55`;
2061
+ }
2062
+ }
2063
+ });
2064
+ }, intervalMs);
2065
+ autoSyncTimers.set(id, timer);
2066
+ }
1803
2067
  envCheckBtn.onclick = checkEnvironment;
2068
+ envRepairAllBtn.onclick = () => void runRepair("\u4E00\u952E\u4FEE\u590D\u7F3A\u5931\u9879", async () => {
2069
+ const snapshot = await collectEnvironment();
2070
+ return await repairMissing(snapshot);
2071
+ });
2072
+ envReinstallAllBtn.onclick = () => void runRepair("\u4E00\u952E\u5378\u8F7D\u91CD\u88C5\u8D44\u6E90", () => repairInstall({ browser: true, geoip: true, reinstall: true }));
2073
+ repairCoreBtn.onclick = () => void runRepair("\u4FEE\u590D\u6838\u5FC3\u670D\u52A1", repairCoreServices);
2074
+ repairCore2Btn.onclick = () => void runRepair("\u4FEE\u590D\u6838\u5FC3\u670D\u52A1", repairCoreServices);
2075
+ repairCamoBtn.onclick = () => void runRepair("\u4FEE\u590D Camoufox CLI/Runtime", () => repairInstall({ browser: true }));
2076
+ repairRuntimeBtn.onclick = () => void runRepair("\u4FEE\u590D Camoufox Runtime", () => repairInstall({ browser: true }));
2077
+ repairGeoipBtn.onclick = () => void runRepair("\u5B89\u88C5 GeoIP", () => repairInstall({ geoip: true }));
1804
2078
  addAccountBtn.onclick = addAccount;
1805
2079
  enterMainBtn.onclick = () => {
1806
2080
  if (typeof ctx2.setActiveTab === "function") {
1807
2081
  ctx2.setActiveTab("config");
1808
2082
  }
1809
2083
  };
1810
- void checkEnvironment();
1811
- void refreshAccounts();
2084
+ void tickEnvironment();
2085
+ void tickAccounts();
2086
+ if (typeof ctx2.api?.onBusEvent === "function") {
2087
+ busUnsubscribe = ctx2.api.onBusEvent((evt) => {
2088
+ const type = String(evt?.type || evt?.event || "").trim().toLowerCase();
2089
+ if (!type) return;
2090
+ if (type.startsWith("account:")) {
2091
+ void tickAccounts();
2092
+ }
2093
+ if (type.startsWith("env:")) {
2094
+ void tickEnvironment();
2095
+ }
2096
+ });
2097
+ }
2098
+ renderRepairHistory();
2099
+ return () => {
2100
+ for (const timer of autoSyncTimers.values()) clearInterval(timer);
2101
+ autoSyncTimers.clear();
2102
+ if (typeof busUnsubscribe === "function") busUnsubscribe();
2103
+ };
2104
+ }
2105
+
2106
+ // src/renderer/tabs-new/schedule-task-bridge.mts
2107
+ var PLATFORM_TASKS = {
2108
+ xiaohongshu: [
2109
+ { type: "xhs-unified", label: "\u641C\u7D22\u4EFB\u52A1", icon: "\u{1F4D5}", platform: "xiaohongshu" }
2110
+ ],
2111
+ weibo: [
2112
+ { type: "weibo-timeline", label: "\u4E3B\u9875\u65F6\u95F4\u7EBF", icon: "\u{1F4F0}", platform: "weibo" },
2113
+ { type: "weibo-search", label: "\u641C\u7D22\u4EFB\u52A1", icon: "\u{1F50D}", platform: "weibo" },
2114
+ { type: "weibo-monitor", label: "\u76D1\u63A7\u4E2A\u4EBA\u4E3B\u9875", icon: "\u{1F441}\uFE0F", platform: "weibo" }
2115
+ ],
2116
+ "1688": [
2117
+ { type: "1688-search", label: "\u641C\u7D22\u4EFB\u52A1", icon: "\u{1F6D2}", platform: "1688" }
2118
+ ]
2119
+ };
2120
+ function normalizeScheduleType(value) {
2121
+ const text = String(value || "interval").trim().toLowerCase();
2122
+ if (text === "once" || text === "daily" || text === "weekly") return text;
2123
+ return "interval";
2124
+ }
2125
+ function toLocalDatetimeValue(iso) {
2126
+ const text = String(iso || "").trim();
2127
+ if (!text) return "";
2128
+ const ts = Date.parse(text);
2129
+ if (!Number.isFinite(ts)) return "";
2130
+ const date = new Date(ts);
2131
+ const yyyy = date.getFullYear();
2132
+ const mm = String(date.getMonth() + 1).padStart(2, "0");
2133
+ const dd = String(date.getDate()).padStart(2, "0");
2134
+ const hh = String(date.getHours()).padStart(2, "0");
2135
+ const min = String(date.getMinutes()).padStart(2, "0");
2136
+ return `${yyyy}-${mm}-${dd}T${hh}:${min}`;
2137
+ }
2138
+ function toIsoOrNull(localDateTime) {
2139
+ const text = String(localDateTime || "").trim();
2140
+ if (!text) return null;
2141
+ const ts = Date.parse(text);
2142
+ if (!Number.isFinite(ts)) return null;
2143
+ return new Date(ts).toISOString();
2144
+ }
2145
+ function parseRunHistory(items) {
2146
+ if (!Array.isArray(items)) return [];
2147
+ return items.map((item) => ({
2148
+ timestamp: String(item?.timestamp || "").trim(),
2149
+ status: String(item?.status || "").trim() === "failure" ? "failure" : "success",
2150
+ durationMs: Number.isFinite(Number(item?.durationMs)) ? Math.max(0, Number(item.durationMs)) : 0
2151
+ })).filter((item) => item.timestamp);
2152
+ }
2153
+ function parseTaskRows(payload) {
2154
+ const rows = Array.isArray(payload?.tasks) ? payload.tasks : [];
2155
+ return rows.map((row) => ({
2156
+ id: String(row?.id || "").trim(),
2157
+ seq: Number.isFinite(Number(row?.seq)) ? Math.max(0, Math.floor(Number(row.seq))) : 0,
2158
+ name: String(row?.name || row?.id || "").trim(),
2159
+ enabled: row?.enabled !== false,
2160
+ scheduleType: normalizeScheduleType(row?.scheduleType),
2161
+ intervalMinutes: Number.isFinite(Number(row?.intervalMinutes)) ? Math.max(1, Math.floor(Number(row.intervalMinutes))) : 30,
2162
+ runAt: String(row?.runAt || "").trim() || null,
2163
+ maxRuns: Number.isFinite(Number(row?.maxRuns)) && Number(row.maxRuns) > 0 ? Math.floor(Number(row.maxRuns)) : null,
2164
+ nextRunAt: String(row?.nextRunAt || "").trim() || null,
2165
+ commandType: String(row?.commandType || "xhs-unified").trim() || "xhs-unified",
2166
+ commandArgv: row?.commandArgv && typeof row.commandArgv === "object" ? row.commandArgv : {},
2167
+ createdAt: String(row?.createdAt || "").trim() || null,
2168
+ updatedAt: String(row?.updatedAt || "").trim() || null,
2169
+ lastRunAt: String(row?.lastRunAt || "").trim() || null,
2170
+ lastStatus: String(row?.lastStatus || "").trim() || null,
2171
+ lastError: String(row?.lastError || "").trim() || null,
2172
+ runCount: Number(row?.runCount || 0) || 0,
2173
+ failCount: Number(row?.failCount || 0) || 0,
2174
+ runHistory: parseRunHistory(row?.runHistory)
2175
+ })).filter((row) => row.id);
2176
+ }
2177
+ function getTasksForPlatform(platform) {
2178
+ const p = platform;
2179
+ return PLATFORM_TASKS[p] || [];
2180
+ }
2181
+ function getPlatformForCommandType(commandType) {
2182
+ if (commandType.startsWith("xhs")) return "xiaohongshu";
2183
+ if (commandType.startsWith("weibo")) return "weibo";
2184
+ if (commandType.startsWith("1688")) return "1688";
2185
+ return "xiaohongshu";
1812
2186
  }
1813
2187
 
1814
- // src/renderer/tabs-new/config-panel.mts
1815
- function renderConfigPanel(root, ctx2) {
2188
+ // src/renderer/tabs-new/tasks.mts
2189
+ var DEFAULT_FORM = {
2190
+ name: "",
2191
+ enabled: true,
2192
+ platform: "xiaohongshu",
2193
+ taskType: "xhs-unified",
2194
+ profileId: "",
2195
+ keyword: "",
2196
+ targetCount: 50,
2197
+ env: "debug",
2198
+ userId: "",
2199
+ collectComments: true,
2200
+ collectBody: true,
2201
+ doLikes: false,
2202
+ likeKeywords: "",
2203
+ scheduleType: "interval",
2204
+ intervalMinutes: 30,
2205
+ runAt: null,
2206
+ maxRuns: null
2207
+ };
2208
+ function parseSortableTime(value) {
2209
+ const ts = Date.parse(String(value || ""));
2210
+ return Number.isFinite(ts) ? ts : 0;
2211
+ }
2212
+ function isKeywordRequired(taskType) {
2213
+ return taskType === "xhs-unified" || taskType === "weibo-search" || taskType === "1688-search";
2214
+ }
2215
+ function commandTypeToWeiboTaskType(commandType) {
2216
+ if (commandType === "weibo-search") return "search";
2217
+ if (commandType === "weibo-monitor") return "monitor";
2218
+ return "timeline";
2219
+ }
2220
+ function fallbackTaskName(data) {
2221
+ const keyword = String(data.keyword || "").trim();
2222
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
2223
+ return keyword ? `${data.taskType}-${keyword}` : `${data.taskType}-${stamp}`;
2224
+ }
2225
+ function renderTasksPanel(root, ctx2) {
1816
2226
  root.innerHTML = "";
1817
2227
  const pageIndicator = createEl("div", { className: "page-indicator" }, [
1818
2228
  "\u5F53\u524D: ",
1819
- createEl("span", {}, ["\u914D\u7F6E\u9875"]),
1820
- " \u2192 \u5B8C\u6210\u540E\u8DF3\u8F6C ",
1821
- createEl("span", {}, ["\u770B\u677F\u9875"])
2229
+ createEl("span", {}, ["\u4EFB\u52A1\u7BA1\u7406"]),
2230
+ " \u2192 \u521B\u5EFA\u3001\u7F16\u8F91\u3001\u6267\u884C\u4EFB\u52A1"
1822
2231
  ]);
1823
2232
  root.appendChild(pageIndicator);
1824
- const bentoGrid = createEl("div", { className: "bento-grid bento-sidebar" });
1825
- const targetCard = createEl("div", { className: "bento-cell" });
1826
- targetCard.innerHTML = `
1827
- <div class="bento-title">\u76EE\u6807\u8BBE\u5B9A</div>
2233
+ const quotaBar = createEl("div", { className: "bento-cell", style: "margin-bottom: var(--gap); padding: var(--gap-sm);" });
2234
+ quotaBar.innerHTML = `
2235
+ <div style="display: flex; gap: var(--gap); align-items: center; flex-wrap: wrap;">
2236
+ <span style="font-size: 12px; color: var(--text-secondary);">\u914D\u989D\u72B6\u6001:</span>
2237
+ <span id="quota-search" class="quota-item" style="font-size: 11px;">\u641C\u7D22: -/-</span>
2238
+ <span id="quota-like" class="quota-item" style="font-size: 11px;">\u70B9\u8D5E: -/-</span>
2239
+ <span id="quota-comment" class="quota-item" style="font-size: 11px;">\u8BC4\u8BBA: -/-</span>
2240
+ <button id="quota-refresh-btn" class="secondary" style="padding: 4px 8px; font-size: 11px; height: auto;">\u5237\u65B0</button>
2241
+ </div>
2242
+ `;
2243
+ root.appendChild(quotaBar);
2244
+ const mainGrid = createEl("div", { className: "bento-grid bento-sidebar" });
2245
+ const formCard = createEl("div", { className: "bento-cell" });
2246
+ formCard.innerHTML = `
2247
+ <div id="task-form-title" class="bento-title">\u65B0\u5EFA\u4EFB\u52A1</div>
2248
+ <input type="hidden" id="task-editing-id" />
1828
2249
 
1829
2250
  <div class="row">
1830
2251
  <div>
1831
- <label>\u641C\u7D22\u5173\u952E\u8BCD</label>
1832
- <input id="keyword-input" placeholder="\u8F93\u5165\u5173\u952E\u8BCD" style="width: 200px;" />
2252
+ <label>\u5E73\u53F0</label>
2253
+ <select id="task-platform" style="width: 130px;">
2254
+ <option value="xiaohongshu">\u{1F4D5} \u5C0F\u7EA2\u4E66</option>
2255
+ <option value="weibo">\u{1F4F0} \u5FAE\u535A</option>
2256
+ <option value="1688">\u{1F6D2} 1688</option>
2257
+ </select>
2258
+ </div>
2259
+ <div>
2260
+ <label>\u4EFB\u52A1\u7C7B\u578B</label>
2261
+ <select id="task-type" style="width: 140px;"></select>
2262
+ </div>
2263
+ <div>
2264
+ <label>\u4EFB\u52A1\u540D</label>
2265
+ <input id="task-name" placeholder="\u53EF\u9009\uFF0C\u4FBF\u4E8E\u8BC6\u522B" style="width: 180px;" />
1833
2266
  </div>
1834
2267
  </div>
1835
2268
 
1836
2269
  <div class="row">
1837
2270
  <div>
1838
- <label>\u76EE\u6807\u6570\u91CF</label>
1839
- <input id="target-input" type="number" value="50" min="1" style="width: 100px;" />
2271
+ <label>\u5173\u952E\u8BCD</label>
2272
+ <input id="task-keyword" placeholder="\u641C\u7D22\u5173\u952E\u8BCD" style="width: 180px;" />
2273
+ </div>
2274
+ <div>
2275
+ <label>\u76EE\u6807\u6570</label>
2276
+ <input id="task-target" type="number" min="1" value="50" style="width: 80px;" />
1840
2277
  </div>
1841
2278
  <div>
1842
- <label>\u8FD0\u884C\u73AF\u5883</label>
1843
- <select id="env-select" style="width: 120px;">
1844
- <option value="debug">\u8C03\u8BD5\u6A21\u5F0F</option>
1845
- <option value="prod">\u751F\u4EA7\u6A21\u5F0F</option>
2279
+ <label>Profile</label>
2280
+ <input id="task-profile" placeholder="xiaohongshu-batch-1" style="width: 160px;" />
2281
+ </div>
2282
+ <div>
2283
+ <label>\u73AF\u5883</label>
2284
+ <select id="task-env" style="width: 80px;">
2285
+ <option value="debug">debug</option>
2286
+ <option value="prod">prod</option>
1846
2287
  </select>
1847
2288
  </div>
1848
2289
  </div>
1849
2290
 
1850
- <div>
1851
- <label>\u9009\u62E9\u8D26\u6237</label>
1852
- <select id="account-select" style="min-width: 200px;">
1853
- <option value="">\u8BF7\u9009\u62E9\u8D26\u6237...</option>
1854
- </select>
1855
- </div>
1856
-
1857
- <div style="margin-top: var(--gap); padding-top: var(--gap); border-top: 1px solid var(--border);">
1858
- <div class="bento-title" style="font-size: 13px;">\u914D\u7F6E\u9884\u8BBE</div>
1859
- <div class="row">
1860
- <select id="preset-select" style="width: 200px;">
1861
- <option value="last">\u4E0A\u6B21\u914D\u7F6E</option>
1862
- <option value="full">\u9884\u8BBE1\uFF1A\u5168\u91CF\u722C\u53D6</option>
1863
- <option value="body-only">\u9884\u8BBE2\uFF1A\u4EC5\u6B63\u6587</option>
1864
- <option value="quick">\u9884\u8BBE3\uFF1A\u5FEB\u901F\u91C7\u96C6</option>
1865
- </select>
1866
- </div>
1867
- <div class="btn-group">
1868
- <button id="import-btn" class="secondary" style="flex: 1;">\u5BFC\u5165\u914D\u7F6E</button>
1869
- <button id="export-btn" class="secondary" style="flex: 1;">\u5BFC\u51FA\u914D\u7F6E</button>
2291
+ <div id="task-user-id-wrap" class="row" style="display:none;">
2292
+ <div>
2293
+ <label>\u5FAE\u535A\u7528\u6237ID (monitor \u5FC5\u586B)</label>
2294
+ <input id="task-user-id" placeholder="\u4F8B\u5982: 1234567890" style="width: 220px;" />
1870
2295
  </div>
1871
2296
  </div>
1872
- `;
1873
- bentoGrid.appendChild(targetCard);
1874
- const optionsCard = createEl("div", { className: "bento-cell" });
1875
- optionsCard.innerHTML = `
1876
- <div class="bento-title">\u722C\u53D6\u9009\u9879</div>
1877
2297
 
1878
- <div style="display: flex; gap: var(--gap); margin-bottom: var(--gap);">
1879
- <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
1880
- <input id="fetch-body-cb" type="checkbox" checked />
1881
- <span>\u722C\u53D6\u6B63\u6587</span>
2298
+ <div class="row">
2299
+ <label style="display:flex;align-items:center;gap:6px;">
2300
+ <input id="task-comments" type="checkbox" checked />
2301
+ <span style="font-size:12px;">\u8BC4\u8BBA</span>
2302
+ </label>
2303
+ <label style="display:flex;align-items:center;gap:6px;">
2304
+ <input id="task-body" type="checkbox" checked />
2305
+ <span style="font-size:12px;">\u6B63\u6587</span>
1882
2306
  </label>
1883
- <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
1884
- <input id="fetch-comments-cb" type="checkbox" checked />
1885
- <span>\u722C\u53D6\u8BC4\u8BBA</span>
2307
+ <label style="display:flex;align-items:center;gap:6px;">
2308
+ <input id="task-likes" type="checkbox" />
2309
+ <span style="font-size:12px;">\u70B9\u8D5E</span>
1886
2310
  </label>
2311
+ <input id="task-like-keywords" placeholder="\u70B9\u8D5E\u5173\u952E\u8BCD(\u9017\u53F7\u5206\u9694)" style="flex:1; min-width:120px;" disabled />
1887
2312
  </div>
1888
2313
 
1889
- <div class="row">
1890
- <div>
1891
- <label>\u6700\u591A\u8BC4\u8BBA\u6570</label>
1892
- <input id="max-comments-input" type="number" value="100" min="0" style="width: 100px;" />
2314
+ <div style="margin-top: var(--gap); padding-top: var(--gap-sm); border-top: 1px solid var(--border);">
2315
+ <div style="font-size:12px; color:var(--text-secondary); margin-bottom:var(--gap-sm);">\u8C03\u5EA6\u8BBE\u7F6E\uFF08\u53EF\u9009\uFF09</div>
2316
+ <div class="row">
2317
+ <div>
2318
+ <select id="task-schedule-type" style="width: 100px;">
2319
+ <option value="interval">\u5FAA\u73AF\u95F4\u9694</option>
2320
+ <option value="once">\u4E00\u6B21\u6027</option>
2321
+ <option value="daily">\u6BCF\u5929</option>
2322
+ <option value="weekly">\u6BCF\u5468</option>
2323
+ </select>
2324
+ </div>
2325
+ <div id="task-interval-wrap">
2326
+ <input id="task-interval" type="number" min="1" value="30" style="width: 70px;" />
2327
+ <span style="font-size:11px;color:var(--text-tertiary);">\u5206\u949F</span>
2328
+ </div>
2329
+ <div id="task-runat-wrap" style="display:none;">
2330
+ <input id="task-runat" type="datetime-local" style="width: 160px;" />
2331
+ </div>
2332
+ <div>
2333
+ <input id="task-max-runs" type="number" min="1" placeholder="\u4E0D\u9650" style="width: 70px;" />
2334
+ <span style="font-size:11px;color:var(--text-tertiary);">\u6B21</span>
2335
+ </div>
1893
2336
  </div>
1894
2337
  </div>
1895
2338
 
1896
- <div style="margin-top: var(--gap); padding-top: var(--gap); border-top: 1px solid var(--border);">
1897
- <div class="bento-title" style="font-size: 13px;">\u70B9\u8D5E\u8BBE\u7F6E</div>
1898
- <label style="display: flex; align-items: center; gap: 8px; cursor: pointer; margin-bottom: var(--gap-sm);">
1899
- <input id="auto-like-cb" type="checkbox" />
1900
- <span>\u81EA\u52A8\u70B9\u8D5E</span>
1901
- </label>
2339
+ <div class="btn-group" style="margin-top: var(--gap);">
2340
+ <button id="task-save-btn" style="flex:1;">\u4FDD\u5B58\u4EFB\u52A1</button>
2341
+ <button id="task-run-btn" class="primary" style="flex:1;">\u4FDD\u5B58\u5E76\u6267\u884C</button>
2342
+ <button id="task-run-ephemeral-btn" class="secondary" style="flex:1;">\u4EC5\u6267\u884C(\u4E0D\u4FDD\u5B58)</button>
2343
+ <button id="task-reset-btn" class="secondary" style="flex:0.6;">\u91CD\u7F6E</button>
2344
+ </div>
2345
+ `;
2346
+ mainGrid.appendChild(formCard);
2347
+ const statsCard = createEl("div", { className: "bento-cell", style: "max-width: 300px;" });
2348
+ statsCard.innerHTML = `
2349
+ <div class="bento-title">\u5FEB\u901F\u72B6\u6001</div>
2350
+ <div id="quick-stats">
2351
+ <div style="margin-bottom: var(--gap-sm);">
2352
+ <span style="font-size:11px;color:var(--text-tertiary);">\u8FD0\u884C\u4E2D\u4EFB\u52A1</span>
2353
+ <div id="stat-running" style="font-size:18px;font-weight:700;color:var(--accent-success);">0</div>
2354
+ </div>
2355
+ <div style="margin-bottom: var(--gap-sm);">
2356
+ <span style="font-size:11px;color:var(--text-tertiary);">\u7D2F\u8BA1\u6267\u884C</span>
2357
+ <div id="stat-today" style="font-size:18px;font-weight:700;">0</div>
2358
+ </div>
1902
2359
  <div>
1903
- <label>\u70B9\u8D5E\u5173\u952E\u8BCD (\u9017\u53F7\u5206\u9694)</label>
1904
- <input id="like-keywords-input" placeholder="\u4F8B\u5982: \u7F8E\u98DF,\u65C5\u6E38,\u6444\u5F71" disabled />
2360
+ <span style="font-size:11px;color:var(--text-tertiary);">\u5DF2\u4FDD\u5B58\u4EFB\u52A1</span>
2361
+ <div id="stat-saved" style="font-size:18px;font-weight:700;">0</div>
1905
2362
  </div>
1906
2363
  </div>
1907
-
1908
- <div style="margin-top: var(--gap); padding-top: var(--gap); border-top: 1px solid var(--border);">
1909
- <div class="bento-title" style="font-size: 13px;">\u9AD8\u7EA7\u9009\u9879</div>
1910
- <div style="display: flex; gap: var(--gap);">
1911
- <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
1912
- <input id="headless-cb" type="checkbox" />
1913
- <span>Headless</span>
1914
- </label>
1915
- <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
1916
- <input id="dry-run-cb" type="checkbox" />
1917
- <span>Dry Run</span>
1918
- </label>
1919
- </div>
2364
+ <div style="margin-top: var(--gap);">
2365
+ <button id="goto-scheduler-btn" class="secondary" style="width:100%;">\u67E5\u770B\u4EFB\u52A1\u5217\u8868</button>
1920
2366
  </div>
1921
2367
  `;
1922
- bentoGrid.appendChild(optionsCard);
1923
- root.appendChild(bentoGrid);
1924
- const actionRow = createEl("div", { className: "bento-grid", style: "margin-top: var(--gap);" });
1925
- const actionCard = createEl("div", { className: "bento-cell highlight" });
1926
- actionCard.innerHTML = `
1927
- <div style="text-align: center;">
1928
- <button id="start-btn" style="padding: 14px 64px; font-size: 15px;">\u5F00\u59CB\u722C\u53D6</button>
2368
+ mainGrid.appendChild(statsCard);
2369
+ root.appendChild(mainGrid);
2370
+ const recentCard = createEl("div", { className: "bento-cell", style: "margin-top: var(--gap);" });
2371
+ recentCard.innerHTML = `
2372
+ <div class="bento-title">\u5386\u53F2\u4EFB\u52A1</div>
2373
+ <div class="row" style="margin-bottom: var(--gap-sm);">
2374
+ <select id="task-history-select" style="min-width: 320px;">
2375
+ <option value="">\u9009\u62E9\u5386\u53F2\u4EFB\u52A1...</option>
2376
+ </select>
2377
+ <button id="task-history-edit-btn" class="secondary">\u8F7D\u5165\u7F16\u8F91</button>
2378
+ <button id="task-history-clone-btn" class="secondary">\u8F7D\u5165\u53E6\u5B58</button>
2379
+ <button id="task-history-refresh-btn" class="secondary">\u5237\u65B0</button>
1929
2380
  </div>
2381
+ <div id="recent-tasks-list"></div>
1930
2382
  `;
1931
- actionRow.appendChild(actionCard);
1932
- root.appendChild(actionRow);
1933
- const keywordInput = root.querySelector("#keyword-input");
1934
- const targetInput = root.querySelector("#target-input");
1935
- const envSelect = root.querySelector("#env-select");
1936
- const accountSelect = root.querySelector("#account-select");
1937
- const presetSelect = root.querySelector("#preset-select");
1938
- const fetchBodyCb = root.querySelector("#fetch-body-cb");
1939
- const fetchCommentsCb = root.querySelector("#fetch-comments-cb");
1940
- const maxCommentsInput = root.querySelector("#max-comments-input");
1941
- const autoLikeCb = root.querySelector("#auto-like-cb");
1942
- const likeKeywordsInput = root.querySelector("#like-keywords-input");
1943
- const headlessCb = root.querySelector("#headless-cb");
1944
- const dryRunCb = root.querySelector("#dry-run-cb");
1945
- const startBtn = root.querySelector("#start-btn");
1946
- const importBtn = root.querySelector("#import-btn");
1947
- const exportBtn = root.querySelector("#export-btn");
1948
- let saveTimeout = null;
1949
- let accountRows = [];
1950
- let preferredProfileId = "";
1951
- function buildConfigPayload() {
2383
+ root.appendChild(recentCard);
2384
+ const formTitle = formCard.querySelector("#task-form-title");
2385
+ const platformSelect = formCard.querySelector("#task-platform");
2386
+ const taskTypeSelect = formCard.querySelector("#task-type");
2387
+ const nameInput = formCard.querySelector("#task-name");
2388
+ const keywordInput = formCard.querySelector("#task-keyword");
2389
+ const targetInput = formCard.querySelector("#task-target");
2390
+ const profileInput = formCard.querySelector("#task-profile");
2391
+ const envSelect = formCard.querySelector("#task-env");
2392
+ const userIdWrap = formCard.querySelector("#task-user-id-wrap");
2393
+ const userIdInput = formCard.querySelector("#task-user-id");
2394
+ const commentsInput = formCard.querySelector("#task-comments");
2395
+ const bodyInput = formCard.querySelector("#task-body");
2396
+ const likesInput = formCard.querySelector("#task-likes");
2397
+ const likeKeywordsInput = formCard.querySelector("#task-like-keywords");
2398
+ const scheduleTypeSelect = formCard.querySelector("#task-schedule-type");
2399
+ const intervalInput = formCard.querySelector("#task-interval");
2400
+ const intervalWrap = formCard.querySelector("#task-interval-wrap");
2401
+ const runAtInput = formCard.querySelector("#task-runat");
2402
+ const runAtWrap = formCard.querySelector("#task-runat-wrap");
2403
+ const maxRunsInput = formCard.querySelector("#task-max-runs");
2404
+ const editingIdInput = formCard.querySelector("#task-editing-id");
2405
+ const saveBtn = formCard.querySelector("#task-save-btn");
2406
+ const runBtn = formCard.querySelector("#task-run-btn");
2407
+ const runEphemeralBtn = formCard.querySelector("#task-run-ephemeral-btn");
2408
+ const resetBtn = formCard.querySelector("#task-reset-btn");
2409
+ const quotaRefreshBtn = quotaBar.querySelector("#quota-refresh-btn");
2410
+ const gotoSchedulerBtn = statsCard.querySelector("#goto-scheduler-btn");
2411
+ const historySelect = recentCard.querySelector("#task-history-select");
2412
+ const historyEditBtn = recentCard.querySelector("#task-history-edit-btn");
2413
+ const historyCloneBtn = recentCard.querySelector("#task-history-clone-btn");
2414
+ const historyRefreshBtn = recentCard.querySelector("#task-history-refresh-btn");
2415
+ const recentTasksList = recentCard.querySelector("#recent-tasks-list");
2416
+ const statRunning = statsCard.querySelector("#stat-running");
2417
+ const statToday = statsCard.querySelector("#stat-today");
2418
+ const statSaved = statsCard.querySelector("#stat-saved");
2419
+ let tasks = [];
2420
+ const activeRunIds = /* @__PURE__ */ new Set();
2421
+ let unsubscribeActiveRuns = null;
2422
+ const joinPath2 = (...parts) => {
2423
+ if (typeof ctx2?.api?.pathJoin === "function") return ctx2.api.pathJoin(...parts);
2424
+ return parts.filter(Boolean).join("/");
2425
+ };
2426
+ const scheduleScript = joinPath2("apps", "webauto", "entry", "schedule.mjs");
2427
+ const quotaScript = joinPath2("apps", "webauto", "entry", "lib", "quota-status.mjs");
2428
+ const xhsScript = joinPath2("apps", "webauto", "entry", "xhs-unified.mjs");
2429
+ const weiboScript = joinPath2("apps", "webauto", "entry", "weibo-unified.mjs");
2430
+ function getTaskById(taskId) {
2431
+ const id = String(taskId || "").trim();
2432
+ if (!id) return null;
2433
+ return tasks.find((row) => row.id === id) || null;
2434
+ }
2435
+ function updateFormTitle(mode) {
2436
+ if (mode === "edit") {
2437
+ formTitle.textContent = "\u7F16\u8F91\u4EFB\u52A1";
2438
+ return;
2439
+ }
2440
+ if (mode === "clone") {
2441
+ formTitle.textContent = "\u53E6\u5B58\u4E3A\u65B0\u4EFB\u52A1";
2442
+ return;
2443
+ }
2444
+ formTitle.textContent = "\u65B0\u5EFA\u4EFB\u52A1";
2445
+ }
2446
+ function updateTaskTypeOptions(preferredType = "") {
2447
+ const platform = platformSelect.value;
2448
+ const options = getTasksForPlatform(platform);
2449
+ taskTypeSelect.innerHTML = options.map((item) => `<option value="${item.type}">${item.icon} ${item.label}</option>`).join("");
2450
+ const target = String(preferredType || "").trim();
2451
+ const matched = options.find((item) => item.type === target);
2452
+ taskTypeSelect.value = matched?.type || options[0]?.type || "";
2453
+ updatePlatformFields();
2454
+ }
2455
+ function updatePlatformFields() {
2456
+ const taskType = String(taskTypeSelect.value || "").trim();
2457
+ const isWeiboMonitor = taskType === "weibo-monitor";
2458
+ userIdWrap.style.display = isWeiboMonitor ? "" : "none";
2459
+ }
2460
+ function updateScheduleVisibility() {
2461
+ const scheduleType = String(scheduleTypeSelect.value || "interval").trim();
2462
+ intervalWrap.style.display = scheduleType === "interval" ? "inline-flex" : "none";
2463
+ runAtWrap.style.display = scheduleType === "once" || scheduleType === "daily" || scheduleType === "weekly" ? "inline-flex" : "none";
2464
+ }
2465
+ function updateLikeKeywordsState() {
2466
+ likeKeywordsInput.disabled = !likesInput.checked;
2467
+ }
2468
+ function collectFormData() {
2469
+ const maxRunsRaw = String(maxRunsInput.value || "").trim();
2470
+ const maxRunsNum = maxRunsRaw ? Number(maxRunsRaw) : 0;
1952
2471
  return {
1953
- keyword: keywordInput.value.trim(),
1954
- target: parseInt(targetInput.value) || 50,
1955
- env: envSelect.value,
1956
- fetchBody: fetchBodyCb.checked,
1957
- fetchComments: fetchCommentsCb.checked,
1958
- maxComments: parseInt(maxCommentsInput.value) || 100,
1959
- autoLike: autoLikeCb.checked,
1960
- likeKeywords: likeKeywordsInput.value.trim(),
1961
- headless: headlessCb.checked,
1962
- dryRun: dryRunCb.checked,
1963
- lastProfileId: accountSelect.value || void 0
2472
+ id: String(editingIdInput.value || "").trim() || void 0,
2473
+ name: String(nameInput.value || "").trim(),
2474
+ enabled: true,
2475
+ platform: platformSelect.value,
2476
+ taskType: String(taskTypeSelect.value || "").trim(),
2477
+ profileId: String(profileInput.value || "").trim(),
2478
+ keyword: String(keywordInput.value || "").trim(),
2479
+ targetCount: Math.max(1, Number(targetInput.value || 50) || 50),
2480
+ env: String(envSelect.value || "debug").trim() === "prod" ? "prod" : "debug",
2481
+ userId: String(userIdInput.value || "").trim(),
2482
+ collectComments: commentsInput.checked,
2483
+ collectBody: bodyInput.checked,
2484
+ doLikes: likesInput.checked,
2485
+ likeKeywords: String(likeKeywordsInput.value || "").trim(),
2486
+ scheduleType: scheduleTypeSelect.value,
2487
+ intervalMinutes: Math.max(1, Number(intervalInput.value || 30) || 30),
2488
+ runAt: toIsoOrNull(String(runAtInput.value || "")),
2489
+ maxRuns: Number.isFinite(maxRunsNum) && maxRunsNum > 0 ? Math.max(1, Math.floor(maxRunsNum)) : null
1964
2490
  };
1965
2491
  }
1966
- async function loadConfig() {
1967
- try {
1968
- const config = await ctx2.api.configLoadLast();
1969
- if (config) {
1970
- keywordInput.value = config.keyword || "";
1971
- targetInput.value = String(config.target || 50);
1972
- envSelect.value = config.env || "debug";
1973
- fetchBodyCb.checked = config.fetchBody !== false;
1974
- fetchCommentsCb.checked = config.fetchComments !== false;
1975
- maxCommentsInput.value = String(config.maxComments || 100);
1976
- autoLikeCb.checked = config.autoLike === true;
1977
- likeKeywordsInput.value = config.likeKeywords || "";
1978
- headlessCb.checked = config.headless === true;
1979
- dryRunCb.checked = config.dryRun === true;
1980
- preferredProfileId = String(config.lastProfileId || "").trim();
1981
- updateLikeKeywordsState();
1982
- }
1983
- } catch (err) {
1984
- console.error("Failed to load config:", err);
2492
+ function applyTaskToForm(task, mode) {
2493
+ const taskType = String(task.commandType || "xhs-unified").trim() || "xhs-unified";
2494
+ const platform = getPlatformForCommandType(taskType);
2495
+ platformSelect.value = platform;
2496
+ updateTaskTypeOptions(taskType);
2497
+ editingIdInput.value = mode === "edit" ? String(task.id || "") : "";
2498
+ nameInput.value = mode === "clone" ? `${String(task.name || task.id || "").trim()}-copy` : String(task.name || "").trim();
2499
+ keywordInput.value = String(task.commandArgv?.keyword || task.commandArgv?.k || "").trim();
2500
+ targetInput.value = String(task.commandArgv?.["max-notes"] ?? task.commandArgv?.target ?? 50);
2501
+ profileInput.value = String(task.commandArgv?.profile || task.commandArgv?.profileId || "").trim();
2502
+ envSelect.value = String(task.commandArgv?.env || "debug").trim() === "prod" ? "prod" : "debug";
2503
+ userIdInput.value = String(task.commandArgv?.["user-id"] || task.commandArgv?.userId || "").trim();
2504
+ commentsInput.checked = task.commandArgv?.["do-comments"] !== false;
2505
+ bodyInput.checked = task.commandArgv?.["fetch-body"] !== false;
2506
+ likesInput.checked = task.commandArgv?.["do-likes"] === true;
2507
+ likeKeywordsInput.value = String(task.commandArgv?.["like-keywords"] || "").trim();
2508
+ scheduleTypeSelect.value = String(task.scheduleType || "interval");
2509
+ intervalInput.value = String(task.intervalMinutes || 30);
2510
+ runAtInput.value = toLocalDatetimeValue(task.runAt);
2511
+ maxRunsInput.value = task.maxRuns ? String(task.maxRuns) : "";
2512
+ updatePlatformFields();
2513
+ updateScheduleVisibility();
2514
+ updateLikeKeywordsState();
2515
+ updateFormTitle(mode);
2516
+ }
2517
+ function resetForm() {
2518
+ editingIdInput.value = "";
2519
+ nameInput.value = DEFAULT_FORM.name;
2520
+ platformSelect.value = DEFAULT_FORM.platform;
2521
+ updateTaskTypeOptions(DEFAULT_FORM.taskType);
2522
+ keywordInput.value = DEFAULT_FORM.keyword;
2523
+ targetInput.value = String(DEFAULT_FORM.targetCount);
2524
+ profileInput.value = DEFAULT_FORM.profileId;
2525
+ envSelect.value = DEFAULT_FORM.env;
2526
+ userIdInput.value = DEFAULT_FORM.userId;
2527
+ commentsInput.checked = DEFAULT_FORM.collectComments;
2528
+ bodyInput.checked = DEFAULT_FORM.collectBody;
2529
+ likesInput.checked = DEFAULT_FORM.doLikes;
2530
+ likeKeywordsInput.value = DEFAULT_FORM.likeKeywords;
2531
+ scheduleTypeSelect.value = DEFAULT_FORM.scheduleType;
2532
+ intervalInput.value = String(DEFAULT_FORM.intervalMinutes);
2533
+ runAtInput.value = "";
2534
+ maxRunsInput.value = "";
2535
+ updatePlatformFields();
2536
+ updateScheduleVisibility();
2537
+ updateLikeKeywordsState();
2538
+ updateFormTitle("new");
2539
+ }
2540
+ function sortedTasksByRecent() {
2541
+ return [...tasks].sort((a, b) => {
2542
+ const byUpdated = parseSortableTime(b.updatedAt) - parseSortableTime(a.updatedAt);
2543
+ if (byUpdated !== 0) return byUpdated;
2544
+ const byCreated = parseSortableTime(b.createdAt) - parseSortableTime(a.createdAt);
2545
+ if (byCreated !== 0) return byCreated;
2546
+ return (Number(b.seq) || 0) - (Number(a.seq) || 0);
2547
+ });
2548
+ }
2549
+ function renderHistorySelect() {
2550
+ const previous = String(historySelect.value || "").trim();
2551
+ const rows = sortedTasksByRecent();
2552
+ historySelect.innerHTML = '<option value="">\u9009\u62E9\u5386\u53F2\u4EFB\u52A1...</option>';
2553
+ for (const row of rows) {
2554
+ const label = `${row.name || row.id} (${row.id})`;
2555
+ const option = document.createElement("option");
2556
+ option.value = row.id;
2557
+ option.textContent = label;
2558
+ historySelect.appendChild(option);
2559
+ }
2560
+ if (previous && rows.some((row) => row.id === previous)) {
2561
+ historySelect.value = previous;
1985
2562
  }
1986
2563
  }
1987
- function saveConfig() {
1988
- if (saveTimeout) clearTimeout(saveTimeout);
1989
- saveTimeout = setTimeout(async () => {
1990
- const config = buildConfigPayload();
1991
- try {
1992
- await ctx2.api.configSaveLast(config);
1993
- } catch (err) {
1994
- console.error("Failed to save config:", err);
1995
- }
1996
- }, 1e3);
2564
+ function renderRecentTasks() {
2565
+ const rows = sortedTasksByRecent().slice(0, 8);
2566
+ if (rows.length === 0) {
2567
+ recentTasksList.innerHTML = '<div class="muted" style="font-size:12px;">\u6682\u65E0\u4EFB\u52A1</div>';
2568
+ return;
2569
+ }
2570
+ recentTasksList.innerHTML = rows.map((task) => `
2571
+ <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;">
2572
+ <span style="flex:1;font-size:12px;">${task.name || task.id}</span>
2573
+ <span style="font-size:11px;color:var(--text-tertiary);">${task.commandType}</span>
2574
+ <span style="font-size:11px;color:${task.enabled ? "var(--accent-success)" : "var(--text-muted)"};">${task.enabled ? "\u542F\u7528" : "\u7981\u7528"}</span>
2575
+ <button class="secondary edit-task-btn" data-id="${task.id}" style="padding:2px 6px;font-size:10px;height:auto;">\u7F16\u8F91</button>
2576
+ </div>
2577
+ `).join("");
2578
+ recentTasksList.querySelectorAll(".edit-task-btn").forEach((btn) => {
2579
+ btn.addEventListener("click", () => {
2580
+ const taskId = btn.dataset.id || "";
2581
+ const task = getTaskById(taskId);
2582
+ if (!task) return;
2583
+ historySelect.value = task.id;
2584
+ applyTaskToForm(task, "edit");
2585
+ });
2586
+ });
1997
2587
  }
1998
- async function loadAccounts() {
2588
+ function updateStats() {
2589
+ statSaved.textContent = String(tasks.length);
2590
+ statRunning.textContent = String(activeRunIds.size);
2591
+ const totalRunCount = tasks.reduce((sum, row) => sum + (Number(row.runCount) || 0), 0);
2592
+ statToday.textContent = String(totalRunCount);
2593
+ }
2594
+ async function runJsonScript(scriptPath, args, timeoutMs = 6e4) {
2595
+ const ret = await ctx2.api.cmdRunJson({
2596
+ title: `task-panel ${args.join(" ")}`.trim(),
2597
+ cwd: "",
2598
+ args: [scriptPath, ...args, "--json"],
2599
+ timeoutMs
2600
+ });
2601
+ if (!ret?.ok) {
2602
+ const reason = String(ret?.error || ret?.stderr || ret?.stdout || "unknown_error").trim();
2603
+ throw new Error(reason || "command failed");
2604
+ }
2605
+ return ret.json || {};
2606
+ }
2607
+ async function runScheduleJson(args, timeoutMs = 6e4) {
2608
+ return runJsonScript(scheduleScript, args, timeoutMs);
2609
+ }
2610
+ async function loadQuotaStatus() {
1999
2611
  try {
2000
- accountRows = await listAccountProfiles(ctx2.api);
2001
- const validRows = accountRows.filter((row) => row.valid);
2002
- accountSelect.innerHTML = '<option value="">\u8BF7\u9009\u62E9\u8D26\u6237...</option>';
2003
- validRows.forEach((row) => {
2004
- const profileId = String(row.profileId || "");
2005
- const label = row.alias ? `${row.alias} (${profileId})` : row.name || profileId;
2006
- const opt = createEl("option", { value: profileId }, [label]);
2007
- accountSelect.appendChild(opt);
2612
+ const ret = await ctx2.api.cmdRunJson({
2613
+ title: "quota status",
2614
+ cwd: "",
2615
+ args: [quotaScript],
2616
+ timeoutMs: 3e4
2008
2617
  });
2009
- if (preferredProfileId && validRows.some((row) => row.profileId === preferredProfileId)) {
2010
- accountSelect.value = preferredProfileId;
2618
+ if (!ret?.ok) return;
2619
+ const payload = ret?.json || {};
2620
+ const quotas = Array.isArray(payload?.quotas) ? payload.quotas : [];
2621
+ for (const quota of quotas) {
2622
+ const type = String(quota?.type || "").trim();
2623
+ if (!type) continue;
2624
+ const count = Number(quota?.count || 0);
2625
+ const max = Number(quota?.max || 0);
2626
+ const el = quotaBar.querySelector(`#quota-${type}`);
2627
+ if (!el) continue;
2628
+ el.textContent = `${type}: ${count}/${max || "-"}`;
2629
+ el.style.color = max > 0 && count >= max ? "var(--accent-danger)" : "";
2011
2630
  }
2012
2631
  } catch (err) {
2013
- console.error("Failed to load accounts:", err);
2632
+ console.error("load quota failed:", err);
2014
2633
  }
2015
2634
  }
2016
- async function exportConfig() {
2635
+ async function loadTasks() {
2017
2636
  try {
2018
- const config = buildConfigPayload();
2019
- const home = ctx2.api.osHomedir();
2020
- const downloadsPath = ctx2.api.pathJoin(home, "Downloads");
2021
- const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
2022
- const filePath = ctx2.api.pathJoin(downloadsPath, `webauto-config-${timestamp}.json`);
2023
- const result = await ctx2.api.configExport({ filePath, config });
2024
- if (result.ok) {
2025
- alert(`\u914D\u7F6E\u5DF2\u5BFC\u51FA\u5230: ${result.path}`);
2026
- }
2637
+ const out = await runScheduleJson(["list"]);
2638
+ tasks = parseTaskRows(out);
2639
+ renderHistorySelect();
2640
+ renderRecentTasks();
2641
+ updateStats();
2027
2642
  } catch (err) {
2028
- alert("\u5BFC\u51FA\u5931\u8D25: " + (err?.message || String(err)));
2643
+ console.error("load tasks failed:", err);
2029
2644
  }
2030
2645
  }
2031
- async function importConfig() {
2032
- const input = document.createElement("input");
2033
- input.type = "file";
2034
- input.accept = ".json";
2035
- input.onchange = async (e) => {
2036
- const file = e.target.files?.[0];
2037
- if (!file) return;
2038
- try {
2039
- const text = await file.text();
2040
- const config = JSON.parse(text.replace(/^\uFEFF/, ""));
2041
- keywordInput.value = config.keyword || "";
2042
- targetInput.value = String(config.target || 50);
2043
- envSelect.value = config.env || "debug";
2044
- fetchBodyCb.checked = config.fetchBody !== false;
2045
- fetchCommentsCb.checked = config.fetchComments !== false;
2046
- maxCommentsInput.value = String(config.maxComments || 100);
2047
- autoLikeCb.checked = config.autoLike === true;
2048
- likeKeywordsInput.value = config.likeKeywords || "";
2049
- headlessCb.checked = config.headless === true;
2050
- dryRunCb.checked = config.dryRun === true;
2051
- updateLikeKeywordsState();
2052
- saveConfig();
2053
- alert("\u914D\u7F6E\u5DF2\u5BFC\u5165");
2054
- } catch (err) {
2055
- alert("\u5BFC\u5165\u5931\u8D25: " + (err?.message || String(err)));
2056
- }
2646
+ function buildCommandArgv(data) {
2647
+ const argv = {
2648
+ profile: data.profileId,
2649
+ keyword: data.keyword,
2650
+ "max-notes": data.targetCount,
2651
+ target: data.targetCount,
2652
+ env: data.env,
2653
+ "do-comments": data.collectComments,
2654
+ "fetch-body": data.collectBody,
2655
+ "do-likes": data.doLikes,
2656
+ "like-keywords": data.likeKeywords
2057
2657
  };
2058
- input.click();
2658
+ if (String(data.taskType || "").startsWith("weibo-")) {
2659
+ argv["task-type"] = commandTypeToWeiboTaskType(data.taskType);
2660
+ if (data.userId) argv["user-id"] = data.userId;
2661
+ }
2662
+ return argv;
2059
2663
  }
2060
- function updateLikeKeywordsState() {
2061
- likeKeywordsInput.disabled = !autoLikeCb.checked;
2062
- likeKeywordsInput.style.opacity = autoLikeCb.checked ? "1" : "0.5";
2664
+ function validateBeforeSave(data) {
2665
+ if (!data.profileId) return "\u8BF7\u8F93\u5165 Profile ID";
2666
+ if (isKeywordRequired(data.taskType) && !data.keyword) return "\u8BF7\u8F93\u5165\u5173\u952E\u8BCD";
2667
+ if (data.taskType === "weibo-monitor" && !data.userId) return "\u5FAE\u535A monitor \u4EFB\u52A1\u9700\u8981 user-id";
2668
+ if (data.scheduleType !== "interval" && !data.runAt) return `${data.scheduleType} \u4EFB\u52A1\u9700\u8981\u6267\u884C\u65F6\u95F4`;
2669
+ return null;
2063
2670
  }
2064
- async function startCrawl() {
2065
- const keyword = keywordInput.value.trim();
2066
- if (!keyword) {
2067
- alert("\u8BF7\u8F93\u5165\u5173\u952E\u8BCD");
2671
+ function buildSaveArgs(data) {
2672
+ const args = data.id ? ["update", data.id] : ["add"];
2673
+ args.push("--name", data.name || fallbackTaskName(data));
2674
+ args.push("--enabled", String(data.enabled));
2675
+ args.push("--command-type", data.taskType || "xhs-unified");
2676
+ args.push("--schedule-type", data.scheduleType);
2677
+ if (data.scheduleType === "interval") {
2678
+ args.push("--interval-minutes", String(data.intervalMinutes));
2679
+ } else {
2680
+ args.push("--run-at", String(data.runAt || ""));
2681
+ }
2682
+ args.push("--max-runs", data.maxRuns === null ? "0" : String(data.maxRuns));
2683
+ args.push("--argv-json", JSON.stringify(buildCommandArgv(data)));
2684
+ return args;
2685
+ }
2686
+ async function runSavedTask(taskId, data) {
2687
+ const out = await runScheduleJson(["run", taskId], 0);
2688
+ const runId = String(
2689
+ out?.result?.runResult?.lastRunId || out?.result?.runResult?.runId || out?.runResult?.runId || ""
2690
+ ).trim();
2691
+ if (typeof ctx2.setStatus === "function") {
2692
+ ctx2.setStatus(`running: ${taskId}`);
2693
+ }
2694
+ if (data.taskType === "xhs-unified" && ctx2 && typeof ctx2 === "object") {
2695
+ ctx2.xhsCurrentRun = {
2696
+ runId: runId || null,
2697
+ taskId,
2698
+ profileId: data.profileId,
2699
+ keyword: data.keyword,
2700
+ target: data.targetCount,
2701
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
2702
+ };
2703
+ }
2704
+ if (typeof ctx2.setActiveTab === "function") {
2705
+ ctx2.setActiveTab(data.taskType === "xhs-unified" ? "dashboard" : "scheduler");
2706
+ }
2707
+ }
2708
+ async function saveTask(runImmediately = false) {
2709
+ const data = collectFormData();
2710
+ const invalidReason = validateBeforeSave(data);
2711
+ if (invalidReason) {
2712
+ alert(invalidReason);
2713
+ return;
2714
+ }
2715
+ saveBtn.disabled = true;
2716
+ runBtn.disabled = true;
2717
+ runEphemeralBtn.disabled = true;
2718
+ try {
2719
+ const out = await runScheduleJson(buildSaveArgs(data));
2720
+ const taskId = String(out?.task?.id || data.id || "").trim();
2721
+ if (!taskId) {
2722
+ throw new Error("task id missing after save");
2723
+ }
2724
+ editingIdInput.value = taskId;
2725
+ updateFormTitle("edit");
2726
+ await loadTasks();
2727
+ historySelect.value = taskId;
2728
+ if (runImmediately) {
2729
+ await runSavedTask(taskId, data);
2730
+ } else {
2731
+ alert("\u4EFB\u52A1\u5DF2\u4FDD\u5B58");
2732
+ }
2733
+ } catch (err) {
2734
+ alert(`\u4FDD\u5B58\u5931\u8D25: ${err?.message || String(err)}`);
2735
+ } finally {
2736
+ saveBtn.disabled = false;
2737
+ runBtn.disabled = false;
2738
+ runEphemeralBtn.disabled = false;
2739
+ }
2740
+ }
2741
+ function buildEphemeralRunSpec(data) {
2742
+ if (!data.profileId) return null;
2743
+ if (data.taskType === "xhs-unified") {
2744
+ if (!data.keyword) return null;
2745
+ return {
2746
+ title: `xhs: ${data.keyword}`,
2747
+ groupKey: "xhs-unified",
2748
+ args: [
2749
+ xhsScript,
2750
+ "--profile",
2751
+ data.profileId,
2752
+ "--keyword",
2753
+ data.keyword,
2754
+ "--target",
2755
+ String(data.targetCount),
2756
+ "--max-notes",
2757
+ String(data.targetCount),
2758
+ "--env",
2759
+ data.env,
2760
+ "--do-comments",
2761
+ String(data.collectComments),
2762
+ "--fetch-body",
2763
+ String(data.collectBody),
2764
+ "--do-likes",
2765
+ String(data.doLikes),
2766
+ "--like-keywords",
2767
+ data.likeKeywords
2768
+ ]
2769
+ };
2770
+ }
2771
+ if (data.taskType === "weibo-search") {
2772
+ if (!data.keyword) return null;
2773
+ return {
2774
+ title: `weibo: ${data.keyword}`,
2775
+ groupKey: "weibo-search",
2776
+ args: [
2777
+ weiboScript,
2778
+ "search",
2779
+ "--profile",
2780
+ data.profileId,
2781
+ "--keyword",
2782
+ data.keyword,
2783
+ "--target",
2784
+ String(data.targetCount),
2785
+ "--env",
2786
+ data.env
2787
+ ]
2788
+ };
2789
+ }
2790
+ return null;
2791
+ }
2792
+ async function runWithoutSave() {
2793
+ const data = collectFormData();
2794
+ if (!data.profileId) {
2795
+ alert("\u8BF7\u8F93\u5165 Profile ID");
2068
2796
  return;
2069
2797
  }
2070
- const profileId = accountSelect.value;
2071
- if (!profileId) {
2072
- alert("\u8BF7\u9009\u62E9\u8D26\u6237");
2798
+ if (isKeywordRequired(data.taskType) && !data.keyword) {
2799
+ alert("\u8BF7\u8F93\u5165\u5173\u952E\u8BCD");
2073
2800
  return;
2074
2801
  }
2075
- const account = accountRows.find((row) => row.profileId === profileId);
2076
- if (!account || !account.valid) {
2077
- alert("\u5F53\u524D\u8D26\u6237\u65E0\u6548\uFF0C\u8BF7\u5148\u5230\u201C\u8D26\u6237\u7BA1\u7406\u201D\u5B8C\u6210\u767B\u5F55\u5E76\u6821\u9A8C");
2802
+ if (data.taskType === "weibo-monitor" && !data.userId) {
2803
+ alert("\u5FAE\u535A monitor \u4EFB\u52A1\u9700\u8981 user-id");
2078
2804
  return;
2079
2805
  }
2080
- const config = buildConfigPayload();
2081
- try {
2082
- await ctx2.api.configSaveLast(config);
2083
- } catch {
2806
+ const spec = buildEphemeralRunSpec(data);
2807
+ if (!spec) {
2808
+ alert(`\u5F53\u524D\u4EFB\u52A1\u7C7B\u578B\u6682\u4E0D\u652F\u6301\u4EC5\u6267\u884C(\u4E0D\u4FDD\u5B58): ${data.taskType}`);
2809
+ return;
2084
2810
  }
2085
- const script = ctx2.api.pathJoin("apps", "webauto", "entry", "xhs-unified.mjs");
2086
- const outputRoot = String(ctx2?.settings?.downloadRoot || "").trim();
2087
- const args = [
2088
- script,
2089
- "--profile",
2090
- profileId,
2091
- "--keyword",
2092
- config.keyword,
2093
- "--max-notes",
2094
- String(config.target),
2095
- "--env",
2096
- config.env,
2097
- "--do-comments",
2098
- config.fetchComments ? "true" : "false",
2099
- "--persist-comments",
2100
- config.fetchComments ? "true" : "false",
2101
- "--do-likes",
2102
- config.autoLike ? "true" : "false",
2103
- "--like-keywords",
2104
- config.likeKeywords || "",
2105
- "--headless",
2106
- config.headless ? "true" : "false"
2107
- ];
2108
- if (outputRoot) args.push("--output-root", outputRoot);
2109
- if (config.dryRun) args.push("--dry-run");
2110
- else args.push("--no-dry-run");
2111
- startBtn.disabled = true;
2112
- const prevText = startBtn.textContent;
2113
- startBtn.textContent = "\u542F\u52A8\u4E2D...";
2811
+ runEphemeralBtn.disabled = true;
2114
2812
  try {
2115
2813
  const ret = await ctx2.api.cmdSpawn({
2116
- title: `xhs unified ${config.keyword}`.trim(),
2814
+ title: spec.title,
2117
2815
  cwd: "",
2118
- args,
2119
- groupKey: "xiaohongshu"
2816
+ args: spec.args,
2817
+ groupKey: spec.groupKey
2120
2818
  });
2121
2819
  const runId = String(ret?.runId || "").trim();
2122
- if (!runId) {
2123
- throw new Error("runId \u4E3A\u7A7A");
2124
- }
2125
- ctx2.xhsCurrentRun = {
2126
- runId,
2127
- profileId,
2128
- keyword: config.keyword,
2129
- target: config.target,
2130
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
2131
- };
2132
- if (typeof ctx2.appendLog === "function") {
2133
- ctx2.appendLog(`[ui] started xhs-unified runId=${runId} profile=${profileId} keyword=${config.keyword}`);
2820
+ if (runId) {
2821
+ activeRunIds.add(runId);
2822
+ updateStats();
2134
2823
  }
2135
2824
  if (typeof ctx2.setStatus === "function") {
2136
- ctx2.setStatus(`running: xhs-unified ${config.keyword}`);
2825
+ ctx2.setStatus(`started: ${spec.title}`);
2137
2826
  }
2138
- } catch (err) {
2139
- alert(`\u542F\u52A8\u5931\u8D25: ${err?.message || String(err)}`);
2140
- if (typeof ctx2.appendLog === "function") {
2141
- ctx2.appendLog(`[ui][error] xhs-unified \u542F\u52A8\u5931\u8D25: ${err?.message || String(err)}`);
2827
+ if (data.taskType === "xhs-unified" && ctx2 && typeof ctx2 === "object") {
2828
+ ctx2.xhsCurrentRun = {
2829
+ runId: runId || null,
2830
+ taskId: null,
2831
+ profileId: data.profileId,
2832
+ keyword: data.keyword,
2833
+ target: data.targetCount,
2834
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
2835
+ };
2142
2836
  }
2143
- return;
2837
+ if (typeof ctx2.setActiveTab === "function") {
2838
+ ctx2.setActiveTab(data.taskType === "xhs-unified" ? "dashboard" : "scheduler");
2839
+ }
2840
+ } catch (err) {
2841
+ alert(`\u6267\u884C\u5931\u8D25: ${err?.message || String(err)}`);
2144
2842
  } finally {
2145
- startBtn.disabled = false;
2146
- startBtn.textContent = prevText || "\u5F00\u59CB\u722C\u53D6";
2147
- }
2148
- if (typeof ctx2.setActiveTab === "function") {
2149
- ctx2.setActiveTab("dashboard");
2843
+ runEphemeralBtn.disabled = false;
2150
2844
  }
2151
2845
  }
2152
- autoLikeCb.onchange = updateLikeKeywordsState;
2153
- importBtn.onclick = importConfig;
2154
- exportBtn.onclick = exportConfig;
2155
- startBtn.onclick = startCrawl;
2156
- [
2157
- keywordInput,
2158
- targetInput,
2159
- envSelect,
2160
- accountSelect,
2161
- fetchBodyCb,
2162
- fetchCommentsCb,
2163
- maxCommentsInput,
2164
- autoLikeCb,
2165
- likeKeywordsInput,
2166
- headlessCb,
2167
- dryRunCb
2168
- ].forEach((el) => {
2169
- el.onchange = saveConfig;
2170
- if (el.tagName === "INPUT" && el.type !== "checkbox") {
2171
- el.oninput = saveConfig;
2846
+ function selectedHistoryTask() {
2847
+ const taskId = String(historySelect.value || "").trim();
2848
+ if (!taskId) return null;
2849
+ return getTaskById(taskId);
2850
+ }
2851
+ async function loadLastProfile() {
2852
+ try {
2853
+ const config = await ctx2.api.configLoadLast();
2854
+ if (!profileInput.value && config?.lastProfileId) {
2855
+ profileInput.value = String(config.lastProfileId || "");
2856
+ }
2857
+ if (!keywordInput.value && config?.keyword) {
2858
+ keywordInput.value = String(config.keyword || "");
2859
+ }
2860
+ } catch {
2172
2861
  }
2862
+ }
2863
+ platformSelect.addEventListener("change", () => {
2864
+ updateTaskTypeOptions();
2173
2865
  });
2174
- void loadConfig();
2175
- void loadAccounts();
2176
- }
2177
-
2178
- // src/renderer/tabs-new/dashboard.mts
2179
- function renderDashboard(root, ctx2) {
2180
- root.innerHTML = "";
2866
+ taskTypeSelect.addEventListener("change", () => updatePlatformFields());
2867
+ scheduleTypeSelect.addEventListener("change", () => updateScheduleVisibility());
2868
+ likesInput.addEventListener("change", () => updateLikeKeywordsState());
2869
+ saveBtn.addEventListener("click", () => {
2870
+ void saveTask(false);
2871
+ });
2872
+ runBtn.addEventListener("click", () => {
2873
+ void saveTask(true);
2874
+ });
2875
+ runEphemeralBtn.addEventListener("click", () => {
2876
+ void runWithoutSave();
2877
+ });
2878
+ resetBtn.addEventListener("click", resetForm);
2879
+ quotaRefreshBtn.addEventListener("click", () => {
2880
+ void loadQuotaStatus();
2881
+ });
2882
+ historyRefreshBtn.addEventListener("click", () => {
2883
+ void loadTasks();
2884
+ });
2885
+ historyEditBtn.addEventListener("click", () => {
2886
+ const task = selectedHistoryTask();
2887
+ if (!task) {
2888
+ alert("\u8BF7\u5148\u9009\u62E9\u5386\u53F2\u4EFB\u52A1");
2889
+ return;
2890
+ }
2891
+ applyTaskToForm(task, "edit");
2892
+ });
2893
+ historyCloneBtn.addEventListener("click", () => {
2894
+ const task = selectedHistoryTask();
2895
+ if (!task) {
2896
+ alert("\u8BF7\u5148\u9009\u62E9\u5386\u53F2\u4EFB\u52A1");
2897
+ return;
2898
+ }
2899
+ applyTaskToForm(task, "clone");
2900
+ });
2901
+ gotoSchedulerBtn.addEventListener("click", () => {
2902
+ if (typeof ctx2.setActiveTab === "function") {
2903
+ ctx2.setActiveTab("scheduler");
2904
+ }
2905
+ });
2906
+ if (typeof ctx2.api?.onCmdEvent === "function") {
2907
+ unsubscribeActiveRuns = ctx2.api.onCmdEvent((evt) => {
2908
+ const runId = String(evt?.runId || "").trim();
2909
+ if (!runId) return;
2910
+ if (evt?.type === "started") {
2911
+ activeRunIds.add(runId);
2912
+ updateStats();
2913
+ return;
2914
+ }
2915
+ if (evt?.type === "exit") {
2916
+ activeRunIds.delete(runId);
2917
+ updateStats();
2918
+ }
2919
+ });
2920
+ }
2921
+ resetForm();
2922
+ updateTaskTypeOptions(DEFAULT_FORM.taskType);
2923
+ updateScheduleVisibility();
2924
+ updateLikeKeywordsState();
2925
+ void loadQuotaStatus();
2926
+ void loadTasks();
2927
+ void loadLastProfile();
2928
+ return () => {
2929
+ if (unsubscribeActiveRuns) {
2930
+ try {
2931
+ unsubscribeActiveRuns();
2932
+ } catch {
2933
+ }
2934
+ unsubscribeActiveRuns = null;
2935
+ }
2936
+ };
2937
+ }
2938
+
2939
+ // src/renderer/tabs-new/dashboard.mts
2940
+ function renderDashboard(root, ctx2) {
2941
+ root.innerHTML = "";
2181
2942
  const pageIndicator = createEl("div", { className: "page-indicator" }, [
2182
2943
  "\u5F53\u524D: ",
2183
2944
  createEl("span", {}, ["\u770B\u677F\u9875"]),
@@ -2245,6 +3006,10 @@ function renderDashboard(root, ctx2) {
2245
3006
  <label>\u4F7F\u7528\u8D26\u6237</label>
2246
3007
  <div id="task-account" style="font-weight: 600; color: var(--text-1);">-</div>
2247
3008
  </div>
3009
+ <div>
3010
+ <label>\u914D\u7F6EID</label>
3011
+ <div id="task-config-id" style="font-weight: 600; color: var(--text-1);">-</div>
3012
+ </div>
2248
3013
  </div>
2249
3014
 
2250
3015
  <div class="phase-indicator" style="margin-bottom: var(--gap);">
@@ -2282,12 +3047,13 @@ function renderDashboard(root, ctx2) {
2282
3047
  \u5B9E\u65F6\u65E5\u5FD7
2283
3048
  <button id="toggle-logs-btn" class="secondary" style="margin-left: auto; padding: 4px 10px; font-size: 11px;">\u5C55\u5F00</button>
2284
3049
  </div>
2285
- <div id="logs-container" class="log-container" style="display: none; max-height: 300px;"></div>
3050
+ <div id="logs-container" class="log-container" style="display: none;"></div>
2286
3051
 
2287
3052
  <div style="margin-top: var(--gap);">
2288
3053
  <div class="btn-group">
2289
3054
  <button id="pause-btn" class="secondary" style="flex: 1;">\u6682\u505C</button>
2290
3055
  <button id="stop-btn" class="danger" style="flex: 1;">\u505C\u6B62</button>
3056
+ <button id="back-config-btn" class="secondary" style="flex: 1;">\u8FD4\u56DE\u914D\u7F6E</button>
2291
3057
  </div>
2292
3058
  </div>
2293
3059
  `;
@@ -2300,6 +3066,7 @@ function renderDashboard(root, ctx2) {
2300
3066
  const taskKeyword = root.querySelector("#task-keyword");
2301
3067
  const taskTarget = root.querySelector("#task-target");
2302
3068
  const taskAccount = root.querySelector("#task-account");
3069
+ const taskConfigId = root.querySelector("#task-config-id");
2303
3070
  const currentPhase = root.querySelector("#current-phase");
2304
3071
  const currentAction = root.querySelector("#current-action");
2305
3072
  const progressPercent = root.querySelector("#progress-percent");
@@ -2316,17 +3083,33 @@ function renderDashboard(root, ctx2) {
2316
3083
  const toggleLogsBtn = root.querySelector("#toggle-logs-btn");
2317
3084
  const pauseBtn = root.querySelector("#pause-btn");
2318
3085
  const stopBtn = root.querySelector("#stop-btn");
3086
+ const backConfigBtn = root.querySelector("#back-config-btn");
2319
3087
  let logsExpanded = false;
2320
3088
  let paused = false;
3089
+ let unsubscribeBus = null;
3090
+ let commentsCount = 0;
3091
+ let likesCount = 0;
3092
+ let likesSkippedCount = 0;
3093
+ let likesAlreadyCount = 0;
3094
+ let likesDedupCount = 0;
2321
3095
  let startTime = Date.now();
3096
+ let stoppedAt = null;
2322
3097
  let elapsedTimer = null;
2323
3098
  let unsubscribeState = null;
2324
3099
  let unsubscribeCmd = null;
2325
3100
  let activeRunId = String(ctx2?.xhsCurrentRun?.runId || "").trim();
3101
+ let activeStatus = "";
2326
3102
  let errorCountTotal = 0;
2327
3103
  const recentErrors = [];
2328
3104
  const maxLogs = 500;
2329
3105
  const maxRecentErrors = 8;
3106
+ const initialTaskId = String(ctx2?.xhsCurrentRun?.taskId || ctx2?.activeTaskConfigId || "").trim();
3107
+ if (initialTaskId) {
3108
+ taskConfigId.textContent = initialTaskId;
3109
+ }
3110
+ const normalizeStatus = (value) => String(value || "").trim().toLowerCase();
3111
+ const isRunningStatus = (value) => ["running", "queued", "pending", "starting"].includes(normalizeStatus(value));
3112
+ const isTerminalStatus = (value) => ["completed", "done", "success", "succeeded", "failed", "error", "stopped", "canceled"].includes(normalizeStatus(value));
2330
3113
  function renderRunSummary() {
2331
3114
  runIdText.textContent = activeRunId || "-";
2332
3115
  errorCountText.textContent = String(errorCountTotal);
@@ -2357,12 +3140,22 @@ function renderDashboard(root, ctx2) {
2357
3140
  renderRunSummary();
2358
3141
  }
2359
3142
  function updateElapsed() {
2360
- const elapsed = Math.floor((Date.now() - startTime) / 1e3);
3143
+ const base = stoppedAt ?? Date.now();
3144
+ const elapsed = Math.max(0, Math.floor((base - startTime) / 1e3));
2361
3145
  const h = Math.floor(elapsed / 3600);
2362
3146
  const m = Math.floor(elapsed % 3600 / 60);
2363
3147
  const s = elapsed % 60;
2364
3148
  statElapsed.textContent = `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
2365
3149
  }
3150
+ function startElapsedTimer() {
3151
+ if (elapsedTimer) return;
3152
+ elapsedTimer = setInterval(updateElapsed, 1e3);
3153
+ }
3154
+ function stopElapsedTimer() {
3155
+ if (!elapsedTimer) return;
3156
+ clearInterval(elapsedTimer);
3157
+ elapsedTimer = null;
3158
+ }
2366
3159
  function addLog(line, type = "info") {
2367
3160
  const ts = (/* @__PURE__ */ new Date()).toLocaleTimeString("zh-CN", { hour12: false });
2368
3161
  const logLine = createEl("div", { className: "log-line" });
@@ -2377,29 +3170,45 @@ function renderDashboard(root, ctx2) {
2377
3170
  }
2378
3171
  function updateFromTaskState(state) {
2379
3172
  if (!state) return;
2380
- const collected = state.collected || state.progress || 0;
2381
- const target = state.target || 50;
2382
- const success = state.success || collected;
2383
- const failed = state.failed || state.errors || 0;
3173
+ const progressObj = state.progress && typeof state.progress === "object" ? state.progress : null;
3174
+ const processedRaw = progressObj?.processed ?? progressObj?.current ?? state.progress ?? state.collected ?? state.current ?? 0;
3175
+ const totalRaw = progressObj?.total ?? state.total ?? state.target ?? state.maxNotes ?? 0;
3176
+ const failedRaw = progressObj?.failed ?? state.failed ?? state.errors ?? 0;
3177
+ const collected = Number(processedRaw) || 0;
3178
+ const target = Number(totalRaw) || 0;
3179
+ const success = Number(state.success ?? collected) || 0;
3180
+ const failed = Number(failedRaw) || 0;
2384
3181
  const remaining = Math.max(0, target - collected);
2385
3182
  statCollected.textContent = String(collected);
2386
3183
  statSuccess.textContent = String(success);
2387
3184
  statFailed.textContent = String(failed);
2388
3185
  statRemaining.textContent = String(remaining);
2389
- const percent = target > 0 ? Math.round(collected / target * 100) : 0;
3186
+ let percent = 0;
3187
+ if (target > 0) {
3188
+ percent = Math.round(collected / target * 100);
3189
+ } else if (progressObj && Number.isFinite(Number(progressObj.percent))) {
3190
+ const pct = Number(progressObj.percent);
3191
+ percent = pct <= 1 ? Math.round(pct * 100) : Math.round(pct);
3192
+ }
2390
3193
  progressPercent.textContent = `${percent}%`;
2391
3194
  progressBar.style.width = `${percent}%`;
2392
3195
  if (state.phase) {
2393
3196
  currentPhase.textContent = state.phase;
2394
3197
  }
2395
- if (state.action) {
2396
- currentAction.textContent = state.action;
3198
+ const action = String(state.action || state.message || state.step || "").trim();
3199
+ if (action) {
3200
+ currentAction.textContent = action;
2397
3201
  }
2398
- if (state.comments) {
2399
- statComments.textContent = `${state.comments}\u6761`;
3202
+ const stats = state.stats && typeof state.stats === "object" ? state.stats : null;
3203
+ const comments = Number(stats?.commentsCollected ?? state.comments);
3204
+ if (Number.isFinite(comments)) {
3205
+ commentsCount = Math.max(0, Math.floor(comments));
3206
+ statComments.textContent = `${commentsCount}\u6761`;
2400
3207
  }
2401
- if (state.likes) {
2402
- statLikes.textContent = `${state.likes}\u6B21`;
3208
+ const likes = Number(stats?.likesPerformed ?? state.likes);
3209
+ if (Number.isFinite(likes)) {
3210
+ likesCount = Math.max(0, Math.floor(likes));
3211
+ statLikes.textContent = `${likesCount}\u6B21 (\u8DF3\u8FC7:${likesSkippedCount}, \u5DF2\u8D5E:${likesAlreadyCount}, \u53BB\u91CD:${likesDedupCount})`;
2403
3212
  }
2404
3213
  if (state.ratelimits) {
2405
3214
  statRatelimit.textContent = `${state.ratelimits}\u6B21`;
@@ -2414,22 +3223,68 @@ function renderDashboard(root, ctx2) {
2414
3223
  const aliases = ctx2.api?.settings?.profileAliases || {};
2415
3224
  taskAccount.textContent = aliases[state.profileId] || state.profileId;
2416
3225
  }
3226
+ const taskId = String(state.taskId || state.scheduleTaskId || state.configTaskId || "").trim();
3227
+ if (taskId) {
3228
+ taskConfigId.textContent = taskId;
3229
+ if (ctx2 && typeof ctx2 === "object") {
3230
+ ctx2.activeTaskConfigId = taskId;
3231
+ }
3232
+ }
2417
3233
  if (state.runId) {
2418
3234
  activeRunId = String(state.runId);
2419
3235
  renderRunSummary();
2420
3236
  }
3237
+ if (state.startedAt) {
3238
+ const ts = Number(state.startedAt) || Date.parse(String(state.startedAt));
3239
+ if (Number.isFinite(ts) && ts > 0) {
3240
+ startTime = ts;
3241
+ if (!stoppedAt) {
3242
+ updateElapsed();
3243
+ startElapsedTimer();
3244
+ }
3245
+ }
3246
+ }
3247
+ const status = normalizeStatus(state.status);
3248
+ if (status) {
3249
+ activeStatus = status;
3250
+ }
3251
+ if (status === "completed" || status === "done" || status === "success" || status === "succeeded") {
3252
+ if (!stoppedAt) {
3253
+ stoppedAt = Date.now();
3254
+ updateElapsed();
3255
+ stopElapsedTimer();
3256
+ }
3257
+ }
3258
+ if (status === "failed" || status === "error") {
3259
+ if (!stoppedAt) {
3260
+ stoppedAt = Date.now();
3261
+ updateElapsed();
3262
+ stopElapsedTimer();
3263
+ }
3264
+ }
2421
3265
  if (state.error) {
2422
3266
  pushRecentError(String(state.error), "state");
2423
3267
  }
2424
3268
  }
2425
3269
  function pickTaskFromList(tasks) {
2426
3270
  const target = activeRunId;
3271
+ const running = tasks.find((item) => isRunningStatus(item?.status));
3272
+ const sorted = [...tasks].sort((a, b) => {
3273
+ const aTs = Number(a?.updatedAt ?? a?.completedAt ?? a?.startedAt ?? 0) || 0;
3274
+ const bTs = Number(b?.updatedAt ?? b?.completedAt ?? b?.startedAt ?? 0) || 0;
3275
+ return bTs - aTs;
3276
+ });
3277
+ const latest = sorted[0] || null;
2427
3278
  if (target) {
2428
3279
  const matched = tasks.find((item) => String(item?.runId || "").trim() === target);
2429
- if (matched) return matched;
3280
+ if (matched) {
3281
+ if (isRunningStatus(matched?.status)) return matched;
3282
+ if (running) return running;
3283
+ if (latest && String(latest?.runId || "").trim() !== target) return latest;
3284
+ return matched;
3285
+ }
2430
3286
  }
2431
- const running = tasks.find((item) => ["running", "queued", "pending", "starting"].includes(String(item?.status || "").toLowerCase()));
2432
- return running || tasks[0] || null;
3287
+ return running || latest || null;
2433
3288
  }
2434
3289
  function updateFromEventPayload(payload) {
2435
3290
  const event = String(payload?.event || "").trim();
@@ -2437,11 +3292,39 @@ function renderDashboard(root, ctx2) {
2437
3292
  if (event === "xhs.unified.start") {
2438
3293
  currentPhase.textContent = "\u8FD0\u884C\u4E2D";
2439
3294
  currentAction.textContent = "\u542F\u52A8 autoscript";
3295
+ activeStatus = "running";
3296
+ statCollected.textContent = "0";
3297
+ statSuccess.textContent = "0";
3298
+ statFailed.textContent = "0";
3299
+ statRemaining.textContent = "0";
3300
+ progressPercent.textContent = "0%";
3301
+ progressBar.style.width = "0%";
3302
+ commentsCount = 0;
3303
+ likesCount = 0;
3304
+ likesSkippedCount = 0;
3305
+ likesAlreadyCount = 0;
3306
+ likesDedupCount = 0;
3307
+ statComments.textContent = `0\u6761`;
3308
+ statLikes.textContent = `0\u6B21 (\u8DF3\u8FC7:0, \u5DF2\u8D5E:0, \u53BB\u91CD:0)`;
3309
+ const ts = Date.parse(String(payload.ts || "")) || Date.now();
3310
+ startTime = ts;
3311
+ stoppedAt = null;
3312
+ updateElapsed();
3313
+ startElapsedTimer();
2440
3314
  if (payload.runId) {
2441
3315
  activeRunId = String(payload.runId || "").trim() || activeRunId;
2442
3316
  }
2443
3317
  if (payload.keyword) taskKeyword.textContent = String(payload.keyword);
2444
3318
  if (payload.maxNotes) taskTarget.textContent = String(payload.maxNotes);
3319
+ if (payload.taskId) {
3320
+ const taskId = String(payload.taskId || "").trim();
3321
+ if (taskId) {
3322
+ taskConfigId.textContent = taskId;
3323
+ if (ctx2 && typeof ctx2 === "object") {
3324
+ ctx2.activeTaskConfigId = taskId;
3325
+ }
3326
+ }
3327
+ }
2445
3328
  renderRunSummary();
2446
3329
  return;
2447
3330
  }
@@ -2449,15 +3332,38 @@ function renderDashboard(root, ctx2) {
2449
3332
  const opId = String(payload.operationId || "").trim();
2450
3333
  currentAction.textContent = opId || currentAction.textContent;
2451
3334
  const result = payload.result && typeof payload.result === "object" ? payload.result : {};
3335
+ const opResult = result && typeof result === "object" && "result" in result ? result.result : result;
3336
+ if (opId === "open_first_detail" || opId === "open_next_detail") {
3337
+ const visited = Number(opResult?.visited || 0);
3338
+ const maxNotes = Number(opResult?.maxNotes || 0);
3339
+ if (visited > 0) {
3340
+ statCollected.textContent = String(visited);
3341
+ statSuccess.textContent = String(visited);
3342
+ if (maxNotes > 0) {
3343
+ const remaining = Math.max(0, maxNotes - visited);
3344
+ statRemaining.textContent = String(remaining);
3345
+ taskTarget.textContent = String(maxNotes);
3346
+ const pct = Math.round(visited / maxNotes * 100);
3347
+ progressPercent.textContent = `${pct}%`;
3348
+ progressBar.style.width = `${pct}%`;
3349
+ }
3350
+ }
3351
+ }
2452
3352
  if (opId === "comments_harvest") {
2453
- const nowComments = Number((statComments.textContent || "0").replace(/[^\d]/g, "")) || 0;
2454
- const added = Number(result.collected || 0);
2455
- statComments.textContent = `${Math.max(0, nowComments + added)}\u6761`;
3353
+ const added = Number(opResult?.collected || 0);
3354
+ commentsCount = Math.max(0, commentsCount + added);
3355
+ statComments.textContent = `${commentsCount}\u6761`;
2456
3356
  }
2457
3357
  if (opId === "comment_like") {
2458
- const nowLikes = Number((statLikes.textContent || "0").replace(/[^\d]/g, "")) || 0;
2459
- const added = Number(result.likedCount || 0);
2460
- statLikes.textContent = `${Math.max(0, nowLikes + added)}\u6B21`;
3358
+ const added = Number(opResult?.likedCount || 0);
3359
+ const skipped = Number(opResult?.skippedCount || 0);
3360
+ const already = Number(opResult?.alreadyLikedSkipped || 0);
3361
+ const dedup = Number(opResult?.dedupSkipped || 0);
3362
+ likesCount = Math.max(0, likesCount + added);
3363
+ likesSkippedCount = Math.max(0, likesSkippedCount + skipped);
3364
+ likesAlreadyCount = Math.max(0, likesAlreadyCount + already);
3365
+ likesDedupCount = Math.max(0, likesDedupCount + dedup);
3366
+ statLikes.textContent = `${likesCount}\u6B21 (\u8DF3\u8FC7:${likesSkippedCount}, \u5DF2\u8D5E:${likesAlreadyCount}, \u53BB\u91CD:${likesDedupCount})`;
2461
3367
  }
2462
3368
  return;
2463
3369
  }
@@ -2479,13 +3385,24 @@ function renderDashboard(root, ctx2) {
2479
3385
  }
2480
3386
  if (event === "xhs.unified.stop") {
2481
3387
  const reason = String(payload.reason || "").trim();
3388
+ const stoppedTs = Date.parse(String(payload.stoppedAt || payload.ts || "")) || Date.now();
3389
+ stoppedAt = stoppedTs;
3390
+ activeStatus = reason ? normalizeStatus(reason) || "stopped" : "stopped";
3391
+ updateElapsed();
3392
+ stopElapsedTimer();
3393
+ const successReasons = /* @__PURE__ */ new Set(["completed", "script_complete"]);
2482
3394
  currentPhase.textContent = reason && reason !== "script_failure" ? "\u5DF2\u7ED3\u675F" : "\u5931\u8D25";
2483
3395
  currentAction.textContent = reason || "stop";
2484
- if (reason && reason !== "completed") {
3396
+ if (reason && !successReasons.has(reason)) {
2485
3397
  pushRecentError(`stop reason=${reason}`, event);
2486
3398
  }
2487
3399
  renderRunSummary();
2488
3400
  }
3401
+ if (event === "autoscript:operation_terminal") {
3402
+ const code = String(payload.code || "").trim();
3403
+ currentAction.textContent = code ? `terminal:${code}` : "terminal";
3404
+ renderRunSummary();
3405
+ }
2489
3406
  }
2490
3407
  function parseLineEvent(line) {
2491
3408
  const text = String(line || "").trim();
@@ -2496,12 +3413,110 @@ function renderDashboard(root, ctx2) {
2496
3413
  } catch {
2497
3414
  }
2498
3415
  }
3416
+ function applySummary(summary) {
3417
+ if (!summary || typeof summary !== "object") return;
3418
+ const totals = summary?.totals && typeof summary.totals === "object" ? summary.totals : {};
3419
+ const profiles = Array.isArray(summary?.profiles) ? summary.profiles : [];
3420
+ const profile = profiles[0] || null;
3421
+ const stats = profile?.stats && typeof profile.stats === "object" ? profile.stats : totals;
3422
+ const assigned = Number(stats?.assignedNotes ?? totals?.assignedNotes ?? summary?.target ?? 0) || 0;
3423
+ const opened = Number(stats?.openedNotes ?? totals?.openedNotes ?? totals?.assignedNotes ?? 0) || 0;
3424
+ const failed = Number(totals?.operationErrors ?? 0) || 0;
3425
+ const remaining = Math.max(0, assigned - opened);
3426
+ statCollected.textContent = String(opened);
3427
+ statSuccess.textContent = String(opened);
3428
+ statFailed.textContent = String(failed);
3429
+ statRemaining.textContent = String(remaining);
3430
+ let percent = 0;
3431
+ if (assigned > 0) {
3432
+ percent = Math.round(opened / assigned * 100);
3433
+ }
3434
+ progressPercent.textContent = `${percent}%`;
3435
+ progressBar.style.width = `${percent}%`;
3436
+ const comments = Number(stats?.commentsCollected ?? totals?.commentsCollected ?? 0);
3437
+ if (Number.isFinite(comments)) {
3438
+ commentsCount = Math.max(0, Math.floor(comments));
3439
+ statComments.textContent = `${commentsCount}\u6761`;
3440
+ }
3441
+ const likesNew = Number(stats?.likesNewCount ?? totals?.likesNewCount ?? 0);
3442
+ const likesSkipped = Number(stats?.likesSkippedCount ?? totals?.likesSkippedCount ?? 0);
3443
+ const likesAlready = Number(stats?.likesAlreadyCount ?? totals?.likesAlreadyCount ?? 0);
3444
+ const likesDedup = Number(stats?.likesDedupCount ?? totals?.likesDedupCount ?? 0);
3445
+ likesCount = Math.max(0, Math.floor(likesNew || 0));
3446
+ likesSkippedCount = Math.max(0, Math.floor(likesSkipped || 0));
3447
+ likesAlreadyCount = Math.max(0, Math.floor(likesAlready || 0));
3448
+ likesDedupCount = Math.max(0, Math.floor(likesDedup || 0));
3449
+ statLikes.textContent = `${likesCount}\u6B21(\u8DF3\u8FC7:${likesSkippedCount}, \u5DF2\u8D5E:${likesAlreadyCount}, \u53BB\u91CD:${likesDedupCount})`;
3450
+ if (summary.keyword) taskKeyword.textContent = String(summary.keyword);
3451
+ if (assigned) taskTarget.textContent = String(assigned);
3452
+ if (profile?.profileId) {
3453
+ const aliases = ctx2.api?.settings?.profileAliases || {};
3454
+ taskAccount.textContent = aliases[profile.profileId] || profile.profileId;
3455
+ }
3456
+ const runId = String(profile?.runId || summary?.runId || "").trim();
3457
+ if (runId) {
3458
+ activeRunId = runId;
3459
+ renderRunSummary();
3460
+ }
3461
+ const reason = String(profile?.reason || summary?.status || "").trim();
3462
+ if (reason) {
3463
+ const okReasons = /* @__PURE__ */ new Set(["script_complete", "completed", "success", "succeeded"]);
3464
+ currentPhase.textContent = okReasons.has(reason) ? "\u5DF2\u7ED3\u675F" : "\u5931\u8D25";
3465
+ currentAction.textContent = reason;
3466
+ activeStatus = normalizeStatus(reason) || activeStatus;
3467
+ }
3468
+ const summaryTs = Date.parse(String(summary?.generatedAt || "")) || Date.now();
3469
+ stoppedAt = summaryTs;
3470
+ updateElapsed();
3471
+ stopElapsedTimer();
3472
+ const errorTotal = Number(totals?.operationErrors ?? 0) + Number(totals?.recoveryFailed ?? 0);
3473
+ if (Number.isFinite(errorTotal)) {
3474
+ errorCountTotal = Math.max(0, Math.floor(errorTotal));
3475
+ renderRunSummary();
3476
+ }
3477
+ }
3478
+ async function loadLatestSummary() {
3479
+ if (typeof ctx2.api?.resultsScan !== "function") return null;
3480
+ if (typeof ctx2.api?.fsListDir !== "function") return null;
3481
+ if (typeof ctx2.api?.fsReadTextPreview !== "function") return null;
3482
+ const res = await ctx2.api.resultsScan({ downloadRoot: ctx2.settings?.downloadRoot });
3483
+ if (!res?.ok || !Array.isArray(res?.entries) || res.entries.length === 0) return null;
3484
+ const keyword = String(taskKeyword.textContent || "").trim();
3485
+ const matched = keyword ? res.entries.find((e) => e?.keyword === keyword) : null;
3486
+ const entry = matched || res.entries[0];
3487
+ if (!entry?.path) return null;
3488
+ const mergedRoot = ctx2.api.pathJoin(entry.path, "merged");
3489
+ const list = await ctx2.api.fsListDir({ root: mergedRoot, recursive: true, maxEntries: 3e3 });
3490
+ if (!list?.ok || !Array.isArray(list?.entries)) return null;
3491
+ const summaries = list.entries.filter((e) => !e?.isDir && e?.name === "summary.json");
3492
+ if (summaries.length === 0) return null;
3493
+ summaries.sort((a, b) => (b?.mtimeMs || 0) - (a?.mtimeMs || 0));
3494
+ const summaryPath = summaries[0].path;
3495
+ if (!summaryPath) return null;
3496
+ const textRes = await ctx2.api.fsReadTextPreview({ path: summaryPath, maxBytes: 1e6, maxLines: 2e4 });
3497
+ if (!textRes?.ok || !textRes?.text) return null;
3498
+ try {
3499
+ return JSON.parse(textRes.text);
3500
+ } catch {
3501
+ return null;
3502
+ }
3503
+ }
2499
3504
  function subscribeToUpdates() {
2500
3505
  if (typeof ctx2.api?.onStateUpdate === "function") {
2501
3506
  unsubscribeState = ctx2.api.onStateUpdate((update) => {
2502
3507
  if (paused) return;
2503
3508
  const runId = String(update?.runId || "").trim();
2504
- if (activeRunId && runId && runId !== activeRunId) return;
3509
+ const status = normalizeStatus(update?.data?.status);
3510
+ if (activeRunId && runId && runId !== activeRunId) {
3511
+ if (isTerminalStatus(activeStatus) && (isRunningStatus(status) || status)) {
3512
+ activeRunId = runId;
3513
+ activeStatus = status || "running";
3514
+ stoppedAt = null;
3515
+ renderRunSummary();
3516
+ } else {
3517
+ return;
3518
+ }
3519
+ }
2505
3520
  if (!activeRunId && runId) {
2506
3521
  activeRunId = runId;
2507
3522
  renderRunSummary();
@@ -2517,12 +3532,22 @@ function renderDashboard(root, ctx2) {
2517
3532
  }
2518
3533
  });
2519
3534
  }
3535
+ if (typeof ctx2.api?.onBusEvent === "function") {
3536
+ unsubscribeBus = ctx2.api.onBusEvent((payload) => {
3537
+ if (paused) return;
3538
+ if (payload && payload.event) {
3539
+ updateFromEventPayload(payload);
3540
+ }
3541
+ });
3542
+ }
2520
3543
  if (typeof ctx2.api?.onCmdEvent === "function") {
2521
3544
  unsubscribeCmd = ctx2.api.onCmdEvent((evt) => {
2522
3545
  if (paused) return;
2523
3546
  const runId = String(evt?.runId || "").trim();
2524
- if (!activeRunId && evt?.type === "started" && String(evt?.title || "").includes("xhs unified")) {
3547
+ if ((isTerminalStatus(activeStatus) || !activeRunId) && evt?.type === "started" && String(evt?.title || "").includes("xhs unified")) {
2525
3548
  activeRunId = runId;
3549
+ activeStatus = "running";
3550
+ stoppedAt = null;
2526
3551
  renderRunSummary();
2527
3552
  }
2528
3553
  if (activeRunId && runId && runId !== activeRunId) return;
@@ -2535,9 +3560,16 @@ function renderDashboard(root, ctx2) {
2535
3560
  const failed = Number(statFailed.textContent || "0") || 0;
2536
3561
  statFailed.textContent = String(failed + 1);
2537
3562
  } else if (evt.type === "exit") {
2538
- currentPhase.textContent = Number(evt.exitCode || 0) === 0 ? "\u5DF2\u7ED3\u675F" : "\u5931\u8D25";
2539
- currentAction.textContent = `exit(${evt.exitCode ?? "null"})`;
3563
+ if (!stoppedAt) {
3564
+ currentPhase.textContent = Number(evt.exitCode || 0) === 0 ? "\u5DF2\u7ED3\u675F" : "\u5931\u8D25";
3565
+ currentAction.textContent = `exit(${evt.exitCode ?? "null"})`;
3566
+ }
2540
3567
  addLog(`\u8FDB\u7A0B\u9000\u51FA: code=${evt.exitCode}`, evt.exitCode === 0 ? "success" : "error");
3568
+ if (!stoppedAt) {
3569
+ stoppedAt = Date.now();
3570
+ updateElapsed();
3571
+ stopElapsedTimer();
3572
+ }
2541
3573
  if (Number(evt.exitCode || 0) !== 0) {
2542
3574
  pushRecentError(`\u8FDB\u7A0B\u9000\u51FA code=${evt.exitCode ?? "null"}`, "exit");
2543
3575
  }
@@ -2556,6 +3588,10 @@ function renderDashboard(root, ctx2) {
2556
3588
  const aliases = ctx2.api?.settings?.profileAliases || {};
2557
3589
  taskAccount.textContent = aliases[config.lastProfileId] || config.lastProfileId;
2558
3590
  }
3591
+ const taskId = String(config.taskId || ctx2?.activeTaskConfigId || "").trim();
3592
+ if (taskId) {
3593
+ taskConfigId.textContent = taskId;
3594
+ }
2559
3595
  }
2560
3596
  } catch (err) {
2561
3597
  console.error("Failed to load task info:", err);
@@ -2574,6 +3610,9 @@ function renderDashboard(root, ctx2) {
2574
3610
  }
2575
3611
  updateFromTaskState(picked);
2576
3612
  }
3613
+ } else {
3614
+ const summary = await loadLatestSummary();
3615
+ if (summary) applySummary(summary);
2577
3616
  }
2578
3617
  } catch (err) {
2579
3618
  console.error("Failed to fetch state:", err);
@@ -2597,9 +3636,16 @@ function renderDashboard(root, ctx2) {
2597
3636
  if (confirm("\u786E\u5B9A\u8981\u505C\u6B62\u5F53\u524D\u4EFB\u52A1\u5417\uFF1F")) {
2598
3637
  try {
2599
3638
  const tasks = await ctx2.api.stateGetTasks();
2600
- if (Array.isArray(tasks) && tasks.length > 0 && tasks[0].runId) {
2601
- await ctx2.api.cmdKill(tasks[0].runId);
3639
+ let runIdToStop = String(activeRunId || "").trim();
3640
+ if (!runIdToStop && Array.isArray(tasks)) {
3641
+ const running = tasks.find((item) => ["running", "queued", "pending", "starting"].includes(String(item?.status || "").toLowerCase()));
3642
+ runIdToStop = String(running?.runId || tasks[0]?.runId || "").trim();
3643
+ }
3644
+ if (runIdToStop) {
3645
+ await ctx2.api.cmdKill(runIdToStop);
2602
3646
  addLog("\u4EFB\u52A1\u5DF2\u505C\u6B62", "warn");
3647
+ } else {
3648
+ addLog("\u672A\u627E\u5230\u53EF\u505C\u6B62\u7684\u8FD0\u884C\u4EFB\u52A1", "warn");
2603
3649
  }
2604
3650
  } catch (err) {
2605
3651
  console.error("Failed to stop task:", err);
@@ -2611,21 +3657,68 @@ function renderDashboard(root, ctx2) {
2611
3657
  }, 1500);
2612
3658
  }
2613
3659
  };
3660
+ backConfigBtn.onclick = () => {
3661
+ if (typeof ctx2.setActiveTab === "function") {
3662
+ ctx2.setActiveTab("config");
3663
+ }
3664
+ };
2614
3665
  renderRunSummary();
2615
3666
  loadTaskInfo();
2616
3667
  subscribeToUpdates();
2617
3668
  fetchCurrentState();
2618
- elapsedTimer = setInterval(updateElapsed, 1e3);
3669
+ startElapsedTimer();
2619
3670
  return () => {
2620
- if (elapsedTimer) clearInterval(elapsedTimer);
3671
+ stopElapsedTimer();
2621
3672
  if (unsubscribeState) unsubscribeState();
2622
3673
  if (unsubscribeCmd) unsubscribeCmd();
3674
+ if (unsubscribeBus) unsubscribeBus();
2623
3675
  };
2624
3676
  }
2625
3677
 
2626
3678
  // src/renderer/tabs-new/account-manager.mts
3679
+ var PLATFORM_ICON = {
3680
+ xiaohongshu: "\u{1F4D5}",
3681
+ xhs: "\u{1F4D5}",
3682
+ weibo: "\u{1F9E3}"
3683
+ };
3684
+ var PLATFORM_LABEL = {
3685
+ xiaohongshu: "\u5C0F\u7EA2\u4E66",
3686
+ xhs: "\u5C0F\u7EA2\u4E66",
3687
+ weibo: "\u5FAE\u535A"
3688
+ };
3689
+ function normalizePlatform(value) {
3690
+ const normalized = String(value || "").trim().toLowerCase();
3691
+ if (!normalized) return "xiaohongshu";
3692
+ if (normalized === "xhs") return "xiaohongshu";
3693
+ return normalized;
3694
+ }
3695
+ function getPlatformInfo(platform) {
3696
+ const key = normalizePlatform(platform);
3697
+ return {
3698
+ key,
3699
+ icon: PLATFORM_ICON[key] || "\u{1F310}",
3700
+ label: PLATFORM_LABEL[key] || key,
3701
+ loginUrl: key === "weibo" ? "https://weibo.com" : "https://www.xiaohongshu.com"
3702
+ };
3703
+ }
3704
+ function formatTs(value) {
3705
+ if (!Number.isFinite(value) || Number(value) <= 0) return "\u672A\u68C0\u67E5";
3706
+ try {
3707
+ return new Date(Number(value)).toLocaleString("zh-CN");
3708
+ } catch {
3709
+ return "\u672A\u68C0\u67E5";
3710
+ }
3711
+ }
3712
+ function toTimestamp(value) {
3713
+ const text = String(value || "").trim();
3714
+ if (!text) return null;
3715
+ const parsed = Date.parse(text);
3716
+ if (!Number.isFinite(parsed)) return null;
3717
+ return parsed;
3718
+ }
2627
3719
  function renderAccountManager(root, ctx2) {
2628
3720
  root.innerHTML = "";
3721
+ const autoSyncTimers = /* @__PURE__ */ new Map();
2629
3722
  const bentoGrid = createEl("div", { className: "bento-grid bento-sidebar" });
2630
3723
  const envCard = createEl("div", { className: "bento-cell" });
2631
3724
  envCard.innerHTML = `
@@ -2633,7 +3726,7 @@ function renderAccountManager(root, ctx2) {
2633
3726
  <div class="env-status-grid">
2634
3727
  <div class="env-item" id="env-camo">
2635
3728
  <span class="icon" style="color: var(--text-4);">\u25CB</span>
2636
- <span>Camoufox CLI</span>
3729
+ <span>Camo CLI</span>
2637
3730
  </div>
2638
3731
  <div class="env-item" id="env-unified">
2639
3732
  <span class="icon" style="color: var(--text-4);">\u25CB</span>
@@ -2641,15 +3734,16 @@ function renderAccountManager(root, ctx2) {
2641
3734
  </div>
2642
3735
  <div class="env-item" id="env-browser">
2643
3736
  <span class="icon" style="color: var(--text-4);">\u25CB</span>
2644
- <span>Browser Service</span>
3737
+ <span>Camo Runtime\uFF08\u53EF\u9009\uFF09</span>
2645
3738
  </div>
2646
3739
  <div class="env-item" id="env-firefox">
2647
3740
  <span class="icon" style="color: var(--text-4);">\u25CB</span>
2648
- <span>Firefox</span>
3741
+ <span>Camoufox Browser</span>
2649
3742
  </div>
2650
3743
  </div>
2651
- <div style="margin-top: var(--gap);">
2652
- <button id="recheck-env-btn" class="secondary" style="width: 100%;">\u91CD\u65B0\u68C0\u67E5</button>
3744
+ <div class="btn-group" style="margin-top: var(--gap);">
3745
+ <button id="recheck-env-btn" class="secondary" style="flex: 1;">\u91CD\u65B0\u68C0\u67E5</button>
3746
+ <button id="env-cleanup-btn" class="secondary" style="flex: 1;">\u4E00\u952E\u6E05\u7406</button>
2653
3747
  </div>
2654
3748
  `;
2655
3749
  bentoGrid.appendChild(envCard);
@@ -2659,7 +3753,11 @@ function renderAccountManager(root, ctx2) {
2659
3753
  \u8D26\u6237\u5217\u8868
2660
3754
  <button id="add-account-btn" style="margin-left: auto; padding: 6px 12px; font-size: 12px;">\u6DFB\u52A0\u8D26\u6237</button>
2661
3755
  </div>
2662
- <div id="account-list" class="account-list" style="margin-bottom: var(--gap); max-height: 300px; overflow: auto;"></div>
3756
+ <div class="row" style="margin: 8px 0; gap: 8px; align-items: center;">
3757
+ <input id="new-account-alias-input" placeholder="\u522B\u540D\u53EF\u9009\uFF08\u767B\u5F55\u540E\u81EA\u52A8\u8BC6\u522B\uFF09" style="flex: 1; min-width: 180px;" />
3758
+ <button id="add-account-confirm-btn" class="secondary" style="flex: 0 0 auto;">\u521B\u5EFA\u5E76\u767B\u5F55</button>
3759
+ </div>
3760
+ <div id="account-list" class="account-list" style="margin-bottom: var(--gap);"></div>
2663
3761
  <div style="margin-top: var(--gap);">
2664
3762
  <div class="btn-group">
2665
3763
  <button id="check-all-btn" class="secondary" style="flex: 1;">\u68C0\u67E5\u6240\u6709</button>
@@ -2670,11 +3768,17 @@ function renderAccountManager(root, ctx2) {
2670
3768
  bentoGrid.appendChild(accountCard);
2671
3769
  root.appendChild(bentoGrid);
2672
3770
  const recheckEnvBtn = root.querySelector("#recheck-env-btn");
3771
+ const envCleanupBtn = root.querySelector("#env-cleanup-btn");
2673
3772
  const addAccountBtn = root.querySelector("#add-account-btn");
3773
+ const addAccountConfirmBtn = root.querySelector("#add-account-confirm-btn");
3774
+ const newAccountAliasInput = root.querySelector("#new-account-alias-input");
2674
3775
  const checkAllBtn = root.querySelector("#check-all-btn");
2675
3776
  const refreshExpiredBtn = root.querySelector("#refresh-expired-btn");
2676
3777
  const accountListEl = root.querySelector("#account-list");
2677
3778
  let accounts = [];
3779
+ let envCheckInFlight = false;
3780
+ let accountCheckInFlight = false;
3781
+ let busUnsubscribe = null;
2678
3782
  async function checkEnvironment() {
2679
3783
  try {
2680
3784
  const [camo, services, firefox] = await Promise.all([
@@ -2684,12 +3788,21 @@ function renderAccountManager(root, ctx2) {
2684
3788
  ]);
2685
3789
  updateEnvItem("env-camo", camo.installed);
2686
3790
  updateEnvItem("env-unified", services.unifiedApi);
2687
- updateEnvItem("env-browser", services.browserService);
3791
+ updateEnvItem("env-browser", services.camoRuntime);
2688
3792
  updateEnvItem("env-firefox", firefox.installed);
2689
3793
  } catch (err) {
2690
3794
  console.error("Environment check failed:", err);
2691
3795
  }
2692
3796
  }
3797
+ async function tickEnvironment() {
3798
+ if (envCheckInFlight) return;
3799
+ envCheckInFlight = true;
3800
+ try {
3801
+ await checkEnvironment();
3802
+ } finally {
3803
+ envCheckInFlight = false;
3804
+ }
3805
+ }
2693
3806
  function updateEnvItem(id, ok) {
2694
3807
  const el = root.querySelector(`#${id}`);
2695
3808
  if (!el) return;
@@ -2702,13 +3815,28 @@ function renderAccountManager(root, ctx2) {
2702
3815
  const rows = await listAccountProfiles(ctx2.api);
2703
3816
  accounts = rows.map((row) => ({
2704
3817
  ...row,
2705
- statusView: row.valid ? "valid" : row.status === "pending" ? "pending" : "expired"
3818
+ platform: normalizePlatform(row.platform),
3819
+ statusView: row.valid ? "valid" : row.status === "pending" ? "pending" : "expired",
3820
+ lastCheckAt: toTimestamp(row.updatedAt)
2706
3821
  }));
2707
3822
  renderAccountList();
2708
3823
  } catch (err) {
2709
3824
  console.error("Failed to load accounts:", err);
2710
3825
  }
2711
3826
  }
3827
+ async function tickAccounts() {
3828
+ if (accountCheckInFlight) return;
3829
+ accountCheckInFlight = true;
3830
+ try {
3831
+ await loadAccounts();
3832
+ const pending = accounts.filter((acc) => acc.statusView === "pending");
3833
+ for (const acc of pending) {
3834
+ await checkAccountStatus(acc.profileId, { pendingWhileLogin: true });
3835
+ }
3836
+ } finally {
3837
+ accountCheckInFlight = false;
3838
+ }
3839
+ }
2712
3840
  function renderAccountList() {
2713
3841
  accountListEl.innerHTML = "";
2714
3842
  if (accounts.length === 0) {
@@ -2716,24 +3844,34 @@ function renderAccountManager(root, ctx2) {
2716
3844
  return;
2717
3845
  }
2718
3846
  accounts.forEach((acc) => {
3847
+ const platform = getPlatformInfo(acc.platform);
2719
3848
  const row = createEl("div", {
2720
3849
  className: "account-item",
2721
- style: "display: grid; grid-template-columns: 1fr 120px 100px 100px; gap: var(--gap-sm); padding: var(--gap-sm); align-items: center; border-bottom: 1px solid var(--border);"
3850
+ style: "display: flex; gap: var(--gap-sm); padding: var(--gap-sm); align-items: center; border-bottom: 1px solid var(--border);"
2722
3851
  });
2723
- const nameDiv = createEl("div", {}, [
2724
- createEl("div", { className: "account-name" }, [acc.alias || acc.name || acc.profileId]),
2725
- createEl("div", { className: "account-alias" }, [acc.profileId])
3852
+ const nameDiv = createEl("div", { style: "min-width: 0; flex: 1;" }, [
3853
+ createEl("div", { className: "account-name", style: "display: flex; gap: 6px; align-items: center;" }, [
3854
+ createEl("span", { style: "font-size: 13px;" }, [platform.icon]),
3855
+ createEl("span", {}, [acc.alias || acc.name || acc.profileId]),
3856
+ createEl("span", { style: "font-size: 11px; color: var(--text-3);" }, [platform.label])
3857
+ ]),
3858
+ createEl("div", { className: "account-alias", style: "font-size: 11px; color: var(--text-3);" }, [
3859
+ `profile: ${acc.profileId} \xB7 \u4E0A\u6B21\u68C0\u67E5: ${formatTs(acc.lastCheckAt)}`
3860
+ ])
2726
3861
  ]);
2727
3862
  const statusBadge = createEl("span", {
2728
- className: `status-badge ${acc.statusView === "valid" ? "status-valid" : acc.statusView === "expired" ? "status-expired" : "status-pending"}`
3863
+ className: `status-badge ${acc.statusView === "valid" ? "status-valid" : acc.statusView === "expired" ? "status-expired" : "status-pending"}`,
3864
+ style: "min-width: 76px; text-align: center;"
2729
3865
  }, [
2730
- acc.statusView === "valid" ? "\u2713 \u6709\u6548" : acc.statusView === "expired" ? "\u2717 \u5931\u6548" : acc.statusView === "checking" ? "\u23F3 \u68C0\u67E5\u4E2D" : "\u23F3 \u5F85\u68C0\u67E5"
3866
+ acc.statusView === "valid" ? "\u2713 \u6709\u6548" : acc.statusView === "expired" ? "\u2717 \u5931\u6548" : acc.statusView === "checking" ? "\u23F3 \u68C0\u67E5\u4E2D" : "\u23F3 \u5F85\u767B\u5F55"
2731
3867
  ]);
2732
- const checkBtn = createEl("button", {
2733
- className: "secondary",
2734
- style: "padding: 6px 10px; font-size: 11px;"
2735
- }, ["\u68C0\u67E5"]);
2736
- const actionsDiv = createEl("div", { className: "btn-group", style: "flex: 0;" });
3868
+ const actionsDiv = createEl("div", {
3869
+ className: "btn-group",
3870
+ style: "display: flex; flex-wrap: wrap; gap: 4px; justify-content: flex-end; flex: 0 0 auto;"
3871
+ });
3872
+ const checkBtn = createEl("button", { className: "secondary", style: "padding: 6px 8px; font-size: 10px;" }, ["\u68C0\u67E5"]);
3873
+ const openBtn = createEl("button", { className: "secondary", style: "padding: 6px 8px; font-size: 10px;" }, ["\u6253\u5F00"]);
3874
+ const fixBtn = createEl("button", { className: "secondary", style: "padding: 6px 8px; font-size: 10px;" }, ["\u4FEE\u590D"]);
2737
3875
  const detailBtn = createEl("button", {
2738
3876
  className: "secondary",
2739
3877
  style: "padding: 6px 8px; font-size: 10px;"
@@ -2742,22 +3880,34 @@ function renderAccountManager(root, ctx2) {
2742
3880
  className: "danger",
2743
3881
  style: "padding: 6px 8px; font-size: 10px;"
2744
3882
  }, ["\u5220\u9664"]);
3883
+ actionsDiv.appendChild(checkBtn);
3884
+ actionsDiv.appendChild(openBtn);
3885
+ actionsDiv.appendChild(fixBtn);
2745
3886
  actionsDiv.appendChild(detailBtn);
2746
3887
  actionsDiv.appendChild(deleteBtn);
2747
3888
  row.appendChild(nameDiv);
2748
3889
  row.appendChild(statusBadge);
2749
- row.appendChild(checkBtn);
2750
3890
  row.appendChild(actionsDiv);
2751
- checkBtn.onclick = () => checkAccountStatus(acc.profileId);
3891
+ checkBtn.onclick = () => {
3892
+ void checkAccountStatus(acc.profileId, { resolveAlias: true });
3893
+ };
3894
+ openBtn.onclick = () => {
3895
+ void openAccountLogin(acc, { reason: "manual_open" });
3896
+ };
3897
+ fixBtn.onclick = () => {
3898
+ void fixAccount(acc);
3899
+ };
2752
3900
  detailBtn.onclick = () => {
2753
3901
  alert(`\u8D26\u6237\u8BE6\u60C5:
2754
3902
 
3903
+ \u5E73\u53F0: ${platform.label}
2755
3904
  Profile ID: ${acc.profileId}
2756
3905
  \u8D26\u53F7ID: ${acc.accountId || "\u672A\u8BC6\u522B"}
2757
3906
  \u522B\u540D: ${acc.alias || "\u672A\u8BBE\u7F6E"}
2758
3907
  \u72B6\u6001: ${acc.status}
2759
3908
  \u539F\u56E0: ${acc.reason || "-"}
2760
- \u6700\u540E\u68C0\u67E5: ${acc.lastCheck || "\u672A\u68C0\u67E5"}`);
3909
+ \u6700\u540E\u68C0\u67E5: ${formatTs(acc.lastCheckAt)}
3910
+ \u767B\u5F55\u5165\u53E3: ${platform.loginUrl}`);
2761
3911
  };
2762
3912
  deleteBtn.onclick = async () => {
2763
3913
  if (confirm(`\u786E\u5B9A\u5220\u9664\u8D26\u6237 "${acc.alias || acc.profileId}" \u5417\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\u3002`)) {
@@ -2787,9 +3937,9 @@ Profile ID: ${acc.profileId}
2787
3937
  accountListEl.appendChild(row);
2788
3938
  });
2789
3939
  }
2790
- async function checkAccountStatus(profileId) {
3940
+ async function checkAccountStatus(profileId, options = {}) {
2791
3941
  const account = accounts.find((a) => a.profileId === profileId);
2792
- if (!account) return;
3942
+ if (!account) return false;
2793
3943
  account.statusView = "checking";
2794
3944
  renderAccountList();
2795
3945
  try {
@@ -2800,51 +3950,110 @@ Profile ID: ${acc.profileId}
2800
3950
  ctx2.api.pathJoin("apps", "webauto", "entry", "account.mjs"),
2801
3951
  "sync",
2802
3952
  profileId,
3953
+ ...options.pendingWhileLogin ? ["--pending-while-login"] : [],
3954
+ ...options.resolveAlias ? ["--resolve-alias"] : [],
2803
3955
  "--json"
2804
3956
  ]
2805
3957
  });
2806
3958
  const profile = result?.json?.profile;
2807
3959
  if (profile && String(profile.profileId || "").trim() === profileId) {
2808
3960
  account.accountId = String(profile.accountId || "").trim() || null;
2809
- account.alias = String(profile.alias || "").trim() || account.alias;
3961
+ const detectedAlias = String(profile.alias || "").trim();
3962
+ account.alias = detectedAlias || account.alias;
3963
+ account.platform = normalizePlatform(String(profile.platform || account.platform || "").trim());
2810
3964
  account.status = String(profile.status || "").trim() || account.status;
2811
3965
  account.valid = profile.valid === true && Boolean(account.accountId);
2812
3966
  account.reason = String(profile.reason || "").trim() || null;
3967
+ if (detectedAlias) {
3968
+ const aliases = { ...ctx2.api?.settings?.profileAliases || {}, [profileId]: detectedAlias };
3969
+ await ctx2.api.settingsSet({ profileAliases: aliases }).catch(() => null);
3970
+ if (typeof ctx2.refreshSettings === "function") {
3971
+ await ctx2.refreshSettings().catch(() => null);
3972
+ }
3973
+ }
2813
3974
  }
2814
- account.statusView = account.valid ? "valid" : "expired";
2815
- account.lastCheck = (/* @__PURE__ */ new Date()).toLocaleString("zh-CN");
3975
+ account.statusView = account.valid ? "valid" : account.status === "pending" ? "pending" : "expired";
3976
+ account.lastCheckAt = Date.now();
2816
3977
  } catch (err) {
2817
- account.statusView = "expired";
3978
+ account.statusView = options.pendingWhileLogin ? "pending" : "expired";
3979
+ account.lastCheckAt = Date.now();
2818
3980
  }
2819
3981
  renderAccountList();
3982
+ return Boolean(account.valid);
3983
+ }
3984
+ async function openAccountLogin(account, options = {}) {
3985
+ if (!String(account.profileId || "").trim()) return false;
3986
+ const platform = getPlatformInfo(account.platform);
3987
+ const timeoutSec = Math.max(30, Number(ctx2.api?.settings?.timeouts?.loginTimeoutSec || 900));
3988
+ account.status = "pending";
3989
+ account.statusView = "pending";
3990
+ account.reason = String(options.reason || "manual_relogin");
3991
+ account.lastCheckAt = Date.now();
3992
+ renderAccountList();
3993
+ await ctx2.api.cmdSpawn({
3994
+ title: `\u767B\u5F55 ${account.alias || account.profileId}`,
3995
+ cwd: "",
3996
+ args: [
3997
+ ctx2.api.pathJoin("apps", "webauto", "entry", "profilepool.mjs"),
3998
+ "login-profile",
3999
+ account.profileId,
4000
+ "--url",
4001
+ platform.loginUrl,
4002
+ "--wait-sync",
4003
+ "false",
4004
+ "--timeout-sec",
4005
+ String(timeoutSec),
4006
+ "--keep-session"
4007
+ ],
4008
+ groupKey: "profilepool"
4009
+ });
4010
+ startAutoSyncProfile(account.profileId);
4011
+ return true;
4012
+ }
4013
+ async function fixAccount(account) {
4014
+ const ok = await checkAccountStatus(account.profileId, { resolveAlias: true });
4015
+ if (ok) return;
4016
+ await openAccountLogin(account, { reason: "fix_relogin" });
2820
4017
  }
2821
4018
  async function addAccount() {
2822
- const alias = prompt("\u8BF7\u8F93\u5165\u65B0\u8D26\u6237\u522B\u540D:");
2823
- if (!alias?.trim()) return;
4019
+ const alias = newAccountAliasInput.value.trim();
2824
4020
  try {
2825
4021
  const out = await ctx2.api.cmdRunJson({
2826
- title: "profilepool add",
4022
+ title: "account add",
2827
4023
  cwd: "",
2828
- args: [ctx2.api.pathJoin("apps", "webauto", "entry", "profilepool.mjs"), "add", "xiaohongshu", "--json"]
4024
+ args: [
4025
+ ctx2.api.pathJoin("apps", "webauto", "entry", "account.mjs"),
4026
+ "add",
4027
+ "--platform",
4028
+ "xiaohongshu",
4029
+ "--status",
4030
+ "pending",
4031
+ ...alias ? ["--alias", alias] : [],
4032
+ "--json"
4033
+ ]
2829
4034
  });
2830
- if (!out?.ok || !out?.json?.profileId) {
4035
+ const profileId = String(out?.json?.account?.profileId || "").trim();
4036
+ if (!out?.ok || !profileId) {
2831
4037
  alert("\u521B\u5EFA\u8D26\u53F7\u5931\u8D25: " + (out?.error || "\u672A\u77E5\u9519\u8BEF"));
2832
4038
  return;
2833
4039
  }
2834
- const profileId = out.json.profileId;
2835
- const aliases = { ...ctx2.api.settings?.profileAliases, [profileId]: alias.trim() };
2836
- await ctx2.api.settingsSet({ profileAliases: aliases });
2837
- if (typeof ctx2.refreshSettings === "function") {
2838
- await ctx2.refreshSettings();
4040
+ if (alias) {
4041
+ const aliases = { ...ctx2.api.settings?.profileAliases, [profileId]: alias };
4042
+ await ctx2.api.settingsSet({ profileAliases: aliases });
4043
+ if (typeof ctx2.refreshSettings === "function") {
4044
+ await ctx2.refreshSettings();
4045
+ }
2839
4046
  }
2840
4047
  const timeoutSec = ctx2.api.settings?.timeouts?.loginTimeoutSec || 900;
2841
4048
  await ctx2.api.cmdSpawn({
2842
- title: `\u767B\u5F55 ${alias}`,
4049
+ title: `\u767B\u5F55 ${alias || profileId}`,
2843
4050
  cwd: "",
2844
4051
  args: [
2845
4052
  ctx2.api.pathJoin("apps", "webauto", "entry", "profilepool.mjs"),
2846
4053
  "login-profile",
2847
4054
  profileId,
4055
+ "--wait-sync",
4056
+ "false",
2848
4057
  "--timeout-sec",
2849
4058
  String(timeoutSec),
2850
4059
  "--keep-session"
@@ -2852,21 +4061,52 @@ Profile ID: ${acc.profileId}
2852
4061
  groupKey: "profilepool"
2853
4062
  });
2854
4063
  await loadAccounts();
4064
+ newAccountAliasInput.value = "";
4065
+ startAutoSyncProfile(profileId);
2855
4066
  } catch (err) {
2856
4067
  alert("\u6DFB\u52A0\u8D26\u53F7\u5931\u8D25: " + (err?.message || String(err)));
2857
4068
  }
2858
4069
  }
4070
+ function startAutoSyncProfile(profileId) {
4071
+ const id = String(profileId || "").trim();
4072
+ if (!id) return;
4073
+ const existing = autoSyncTimers.get(id);
4074
+ if (existing) clearInterval(existing);
4075
+ const timeoutSec = Math.max(30, Number(ctx2.api?.settings?.timeouts?.loginTimeoutSec || 900));
4076
+ const intervalMs = 2e3;
4077
+ const maxAttempts = Math.ceil(timeoutSec * 1e3 / intervalMs);
4078
+ let attempts = 0;
4079
+ void checkAccountStatus(id, { pendingWhileLogin: true }).then((ok) => {
4080
+ if (ok) {
4081
+ const timer2 = autoSyncTimers.get(id);
4082
+ if (timer2) clearInterval(timer2);
4083
+ autoSyncTimers.delete(id);
4084
+ void checkAccountStatus(id, { resolveAlias: true }).catch(() => null);
4085
+ }
4086
+ });
4087
+ const timer = setInterval(() => {
4088
+ attempts += 1;
4089
+ void checkAccountStatus(id, { pendingWhileLogin: true }).then((ok) => {
4090
+ if (ok || attempts >= maxAttempts) {
4091
+ const current = autoSyncTimers.get(id);
4092
+ if (current) clearInterval(current);
4093
+ autoSyncTimers.delete(id);
4094
+ }
4095
+ });
4096
+ }, intervalMs);
4097
+ autoSyncTimers.set(id, timer);
4098
+ }
2859
4099
  async function checkAllAccounts() {
2860
4100
  checkAllBtn.disabled = true;
2861
4101
  checkAllBtn.textContent = "\u68C0\u67E5\u4E2D...";
2862
4102
  for (const acc of accounts) {
2863
- await checkAccountStatus(acc.profileId);
4103
+ await checkAccountStatus(acc.profileId, { resolveAlias: true });
2864
4104
  }
2865
4105
  checkAllBtn.disabled = false;
2866
4106
  checkAllBtn.textContent = "\u68C0\u67E5\u6240\u6709";
2867
4107
  }
2868
4108
  async function refreshExpiredAccounts() {
2869
- const expired = accounts.filter((a) => !a.valid);
4109
+ const expired = accounts.filter((a) => !a.valid && a.status !== "pending");
2870
4110
  if (expired.length === 0) {
2871
4111
  alert("\u6CA1\u6709\u5931\u6548\u7684\u8D26\u6237\u9700\u8981\u5237\u65B0");
2872
4112
  return;
@@ -2875,20 +4115,7 @@ Profile ID: ${acc.profileId}
2875
4115
  refreshExpiredBtn.textContent = "\u5237\u65B0\u4E2D...";
2876
4116
  for (const acc of expired) {
2877
4117
  try {
2878
- const accountKey = acc.accountRecordId || acc.profileId;
2879
- await ctx2.api.cmdSpawn({
2880
- title: `\u91CD\u65B0\u767B\u5F55 ${acc.alias || acc.profileId}`,
2881
- cwd: "",
2882
- args: [
2883
- ctx2.api.pathJoin("apps", "webauto", "entry", "account.mjs"),
2884
- "login",
2885
- accountKey,
2886
- "--url",
2887
- "https://www.xiaohongshu.com",
2888
- "--json"
2889
- ],
2890
- groupKey: "profilepool"
2891
- });
4118
+ await openAccountLogin(acc, { reason: "refresh_expired" });
2892
4119
  } catch (err) {
2893
4120
  console.error(`Failed to refresh ${acc.profileId}:`, err);
2894
4121
  }
@@ -2896,19 +4123,683 @@ Profile ID: ${acc.profileId}
2896
4123
  refreshExpiredBtn.disabled = false;
2897
4124
  refreshExpiredBtn.textContent = "\u5237\u65B0\u5931\u6548";
2898
4125
  }
4126
+ async function cleanupEnvironment() {
4127
+ if (!envCleanupBtn) return;
4128
+ envCleanupBtn.disabled = true;
4129
+ const previous = envCleanupBtn.textContent;
4130
+ envCleanupBtn.textContent = "\u6E05\u7406\u4E2D...";
4131
+ try {
4132
+ const result = typeof ctx2.api?.envCleanup === "function" ? await ctx2.api.envCleanup() : await ctx2.api.invoke?.("env:cleanup");
4133
+ if (!result?.ok) {
4134
+ alert(`\u73AF\u5883\u6E05\u7406\u5931\u8D25: ${result?.error || "\u672A\u77E5\u9519\u8BEF"}`);
4135
+ }
4136
+ await tickEnvironment();
4137
+ await tickAccounts();
4138
+ } catch (err) {
4139
+ alert(`\u73AF\u5883\u6E05\u7406\u5931\u8D25: ${err?.message || String(err)}`);
4140
+ } finally {
4141
+ envCleanupBtn.disabled = false;
4142
+ envCleanupBtn.textContent = previous || "\u4E00\u952E\u6E05\u7406";
4143
+ }
4144
+ }
2899
4145
  recheckEnvBtn.onclick = checkEnvironment;
4146
+ if (envCleanupBtn) envCleanupBtn.onclick = () => {
4147
+ void cleanupEnvironment();
4148
+ };
2900
4149
  addAccountBtn.onclick = addAccount;
4150
+ addAccountConfirmBtn.onclick = addAccount;
4151
+ newAccountAliasInput.onkeydown = (ev) => {
4152
+ if (ev.key === "Enter") {
4153
+ ev.preventDefault();
4154
+ void addAccount();
4155
+ }
4156
+ };
2901
4157
  checkAllBtn.onclick = checkAllAccounts;
2902
4158
  refreshExpiredBtn.onclick = refreshExpiredAccounts;
2903
- void checkEnvironment();
2904
- void loadAccounts();
4159
+ void tickEnvironment();
4160
+ void tickAccounts();
4161
+ if (typeof ctx2.api?.onBusEvent === "function") {
4162
+ busUnsubscribe = ctx2.api.onBusEvent((evt) => {
4163
+ const type = String(evt?.type || evt?.event || "").trim().toLowerCase();
4164
+ if (!type) return;
4165
+ if (type.startsWith("account:")) {
4166
+ void tickAccounts();
4167
+ }
4168
+ if (type.startsWith("env:")) {
4169
+ void tickEnvironment();
4170
+ }
4171
+ });
4172
+ }
4173
+ return () => {
4174
+ for (const timer of autoSyncTimers.values()) clearInterval(timer);
4175
+ autoSyncTimers.clear();
4176
+ if (typeof busUnsubscribe === "function") busUnsubscribe();
4177
+ };
4178
+ }
4179
+
4180
+ // src/renderer/tabs-new/scheduler.mts
4181
+ function commandTypeToWeiboTaskType2(commandType) {
4182
+ if (commandType === "weibo-search") return "search";
4183
+ if (commandType === "weibo-monitor") return "monitor";
4184
+ return "timeline";
4185
+ }
4186
+ function renderSchedulerPanel(root, ctx2) {
4187
+ root.innerHTML = "";
4188
+ const pageIndicator = createEl("div", { className: "page-indicator" }, [
4189
+ "\u5F53\u524D: ",
4190
+ createEl("span", {}, ["\u5B9A\u65F6\u4EFB\u52A1"]),
4191
+ " \u2192 \u914D\u7F6E\u5E76\u5B9A\u65F6\u6267\u884C\u591A\u6761\u4EFB\u52A1"
4192
+ ]);
4193
+ root.appendChild(pageIndicator);
4194
+ const toolbar = createEl("div", { className: "bento-grid", style: "margin-bottom: var(--gap);" });
4195
+ const toolbarCell = createEl("div", { className: "bento-cell" });
4196
+ toolbarCell.innerHTML = `
4197
+ <div class="bento-title">\u8C03\u5EA6\u63A7\u5236</div>
4198
+ <div class="row">
4199
+ <button id="scheduler-refresh-btn" class="secondary">\u5237\u65B0\u5217\u8868</button>
4200
+ <button id="scheduler-run-due-btn" class="secondary">\u7ACB\u5373\u6267\u884C\u5230\u70B9\u4EFB\u52A1</button>
4201
+ <button id="scheduler-export-all-btn" class="secondary">\u5BFC\u51FA\u5168\u90E8</button>
4202
+ <button id="scheduler-import-btn" class="secondary">\u5BFC\u5165</button>
4203
+ </div>
4204
+ <div class="row" style="margin-top: 8px; align-items: center;">
4205
+ <span class="muted">\u5F53\u524D\u914D\u7F6E: <strong id="scheduler-active-task-id">-</strong></span>
4206
+ <button id="scheduler-open-config-btn" class="secondary">\u6253\u5F00\u914D\u7F6E\u9875</button>
4207
+ </div>
4208
+ <div class="row" style="margin-top: 8px; align-items: end;">
4209
+ <div>
4210
+ <label>Daemon \u95F4\u9694(\u79D2)</label>
4211
+ <input id="scheduler-daemon-interval" type="number" min="5" value="30" style="width: 120px;" />
4212
+ </div>
4213
+ <button id="scheduler-daemon-start-btn">\u542F\u52A8 Daemon</button>
4214
+ <button id="scheduler-daemon-stop-btn" class="danger">\u505C\u6B62 Daemon</button>
4215
+ <span id="scheduler-daemon-status" class="muted">daemon: \u672A\u542F\u52A8</span>
4216
+ </div>
4217
+ `;
4218
+ toolbar.appendChild(toolbarCell);
4219
+ root.appendChild(toolbar);
4220
+ const grid = createEl("div", { className: "bento-grid bento-sidebar" });
4221
+ const formCell = createEl("div", { className: "bento-cell" });
4222
+ formCell.innerHTML = `
4223
+ <div class="bento-title">\u4EFB\u52A1\u7F16\u8F91</div>
4224
+ <input id="scheduler-editing-id" type="hidden" />
4225
+ <div class="row">
4226
+ <div>
4227
+ <label>\u5E73\u53F0</label>
4228
+ <select id="scheduler-platform" style="width: 140px;">
4229
+ <option value="xiaohongshu">\u{1F4D5} \u5C0F\u7EA2\u4E66</option>
4230
+ <option value="weibo">\u{1F4F0} \u5FAE\u535A</option>
4231
+ <option value="1688">\u{1F6D2} 1688</option>
4232
+ </select>
4233
+ </div>
4234
+ <div>
4235
+ <label>\u4EFB\u52A1\u7C7B\u578B</label>
4236
+ <select id="scheduler-task-type" style="width: 160px;">
4237
+ </select>
4238
+ </div>
4239
+ </div>
4240
+ <div class="row">
4241
+ <div>
4242
+ <label>\u4EFB\u52A1\u540D</label>
4243
+ <input id="scheduler-name" placeholder="\u4F8B\u5982\uFF1Adeepseek-\u6BCF30\u5206\u949F" style="width: 240px;" />
4244
+ </div>
4245
+ <label style="display:flex; align-items:center; gap:8px; margin-top: 22px;">
4246
+ <input id="scheduler-enabled" type="checkbox" checked />
4247
+ <span>\u542F\u7528</span>
4248
+ </label>
4249
+ </div>
4250
+ <div class="row">
4251
+ <div>
4252
+ <label>\u8C03\u5EA6\u7C7B\u578B</label>
4253
+ <select id="scheduler-type" style="width: 140px;">
4254
+ <option value="interval">\u5FAA\u73AF\u95F4\u9694</option>
4255
+ <option value="once">\u4E00\u6B21\u6027</option>
4256
+ <option value="daily">\u6BCF\u5929</option>
4257
+ <option value="weekly">\u6BCF\u5468</option>
4258
+ </select>
4259
+ </div>
4260
+ <div id="scheduler-interval-wrap">
4261
+ <label>\u95F4\u9694\u5206\u949F</label>
4262
+ <input id="scheduler-interval" type="number" min="1" value="30" style="width: 120px;" />
4263
+ </div>
4264
+ <div id="scheduler-runat-wrap" style="display:none;">
4265
+ <label>\u951A\u70B9\u65F6\u95F4</label>
4266
+ <input id="scheduler-runat" type="datetime-local" style="width: 220px;" />
4267
+ </div>
4268
+ <div>
4269
+ <label>\u6700\u5927\u6267\u884C\u6B21\u6570</label>
4270
+ <input id="scheduler-max-runs" type="number" min="1" placeholder="\u4E0D\u9650" style="width: 120px;" />
4271
+ </div>
4272
+ </div>
4273
+ <div class="row">
4274
+ <div>
4275
+ <label>Profile</label>
4276
+ <input id="scheduler-profile" placeholder="xiaohongshu-batch-1" style="width: 220px;" />
4277
+ </div>
4278
+ <div>
4279
+ <label>\u5173\u952E\u8BCD</label>
4280
+ <input id="scheduler-keyword" placeholder="deepseek\u65B0\u6A21\u578B" style="width: 220px;" />
4281
+ </div>
4282
+ </div>
4283
+ <div class="row" id="scheduler-user-id-wrap" style="display:none;">
4284
+ <div>
4285
+ <label>\u5FAE\u535A\u7528\u6237ID (monitor \u5FC5\u586B)</label>
4286
+ <input id="scheduler-user-id" placeholder="\u4F8B\u5982: 1234567890" style="width: 220px;" />
4287
+ </div>
4288
+ </div>
4289
+ <div class="row">
4290
+ <div>
4291
+ <label>\u76EE\u6807\u5E16\u5B50\u6570</label>
4292
+ <input id="scheduler-max-notes" type="number" min="1" value="50" style="width: 120px;" />
4293
+ </div>
4294
+ <div>
4295
+ <label>\u73AF\u5883</label>
4296
+ <select id="scheduler-env" style="width: 120px;">
4297
+ <option value="debug">debug</option>
4298
+ <option value="prod">prod</option>
4299
+ </select>
4300
+ </div>
4301
+ </div>
4302
+ <div class="row">
4303
+ <label style="display:flex; align-items:center; gap:8px;">
4304
+ <input id="scheduler-comments" type="checkbox" checked />
4305
+ <span>\u6293\u8BC4\u8BBA</span>
4306
+ </label>
4307
+ <label style="display:flex; align-items:center; gap:8px;">
4308
+ <input id="scheduler-likes" type="checkbox" />
4309
+ <span>\u70B9\u8D5E</span>
4310
+ </label>
4311
+ <label style="display:flex; align-items:center; gap:8px;">
4312
+ <input id="scheduler-headless" type="checkbox" />
4313
+ <span>headless</span>
4314
+ </label>
4315
+ <label style="display:flex; align-items:center; gap:8px;">
4316
+ <input id="scheduler-dryrun" type="checkbox" />
4317
+ <span>dry-run</span>
4318
+ </label>
4319
+ </div>
4320
+ <div>
4321
+ <label>\u70B9\u8D5E\u5173\u952E\u8BCD\uFF08\u9017\u53F7\u5206\u9694\uFF09</label>
4322
+ <input id="scheduler-like-keywords" placeholder="\u771F\u725B\u903C,\u8D2D\u4E70\u94FE\u63A5" />
4323
+ </div>
4324
+ <div class="btn-group" style="margin-top: var(--gap);">
4325
+ <button id="scheduler-save-btn" style="flex:1;">\u4FDD\u5B58\u4EFB\u52A1</button>
4326
+ <button id="scheduler-reset-btn" class="secondary" style="flex:1;">\u6E05\u7A7A\u8868\u5355</button>
4327
+ </div>
4328
+ `;
4329
+ grid.appendChild(formCell);
4330
+ const listCell = createEl("div", { className: "bento-cell" });
4331
+ listCell.innerHTML = `
4332
+ <div class="bento-title">\u4EFB\u52A1\u5217\u8868</div>
4333
+ <div id="scheduler-list"></div>
4334
+ `;
4335
+ grid.appendChild(listCell);
4336
+ root.appendChild(grid);
4337
+ const refreshBtn = root.querySelector("#scheduler-refresh-btn");
4338
+ const runDueBtn = root.querySelector("#scheduler-run-due-btn");
4339
+ const exportAllBtn = root.querySelector("#scheduler-export-all-btn");
4340
+ const importBtn = root.querySelector("#scheduler-import-btn");
4341
+ const openConfigBtn = root.querySelector("#scheduler-open-config-btn");
4342
+ const daemonStartBtn = root.querySelector("#scheduler-daemon-start-btn");
4343
+ const daemonStopBtn = root.querySelector("#scheduler-daemon-stop-btn");
4344
+ const daemonIntervalInput = root.querySelector("#scheduler-daemon-interval");
4345
+ const daemonStatus = root.querySelector("#scheduler-daemon-status");
4346
+ const activeTaskIdText = root.querySelector("#scheduler-active-task-id");
4347
+ const listEl = root.querySelector("#scheduler-list");
4348
+ const platformSelect = root.querySelector("#scheduler-platform");
4349
+ const taskTypeSelect = root.querySelector("#scheduler-task-type");
4350
+ const editingIdInput = root.querySelector("#scheduler-editing-id");
4351
+ const nameInput = root.querySelector("#scheduler-name");
4352
+ const enabledInput = root.querySelector("#scheduler-enabled");
4353
+ const typeSelect = root.querySelector("#scheduler-type");
4354
+ const intervalWrap = root.querySelector("#scheduler-interval-wrap");
4355
+ const runAtWrap = root.querySelector("#scheduler-runat-wrap");
4356
+ const intervalInput = root.querySelector("#scheduler-interval");
4357
+ const runAtInput = root.querySelector("#scheduler-runat");
4358
+ const maxRunsInput = root.querySelector("#scheduler-max-runs");
4359
+ const profileInput = root.querySelector("#scheduler-profile");
4360
+ const keywordInput = root.querySelector("#scheduler-keyword");
4361
+ const userIdWrap = root.querySelector("#scheduler-user-id-wrap");
4362
+ const userIdInput = root.querySelector("#scheduler-user-id");
4363
+ const maxNotesInput = root.querySelector("#scheduler-max-notes");
4364
+ const envSelect = root.querySelector("#scheduler-env");
4365
+ const commentsInput = root.querySelector("#scheduler-comments");
4366
+ const likesInput = root.querySelector("#scheduler-likes");
4367
+ const headlessInput = root.querySelector("#scheduler-headless");
4368
+ const dryRunInput = root.querySelector("#scheduler-dryrun");
4369
+ const likeKeywordsInput = root.querySelector("#scheduler-like-keywords");
4370
+ const saveBtn = root.querySelector("#scheduler-save-btn");
4371
+ const resetBtn = root.querySelector("#scheduler-reset-btn");
4372
+ let tasks = [];
4373
+ let daemonRunId = "";
4374
+ let unsubscribeCmd = null;
4375
+ let pendingFocusTaskId = String(ctx2?.activeTaskConfigId || "").trim();
4376
+ function setDaemonStatus(text) {
4377
+ daemonStatus.textContent = text;
4378
+ }
4379
+ function setActiveTaskContext(taskId) {
4380
+ const id = String(taskId || "").trim();
4381
+ activeTaskIdText.textContent = id || "-";
4382
+ if (ctx2 && typeof ctx2 === "object") {
4383
+ ctx2.activeTaskConfigId = id;
4384
+ }
4385
+ }
4386
+ function openConfigTab(taskId) {
4387
+ setActiveTaskContext(taskId);
4388
+ if (typeof ctx2.setActiveTab === "function") {
4389
+ ctx2.setActiveTab("config");
4390
+ }
4391
+ }
4392
+ function updateTypeFields() {
4393
+ const mode = typeSelect.value;
4394
+ const useRunAt = mode === "once" || mode === "daily" || mode === "weekly";
4395
+ runAtWrap.style.display = useRunAt ? "" : "none";
4396
+ intervalWrap.style.display = useRunAt ? "none" : "";
4397
+ }
4398
+ function updateTaskTypeOptions() {
4399
+ const platform = platformSelect.value;
4400
+ const tasks2 = getTasksForPlatform(platform);
4401
+ taskTypeSelect.innerHTML = tasks2.map((t) => `<option value="${t.type}">${t.icon} ${t.label}</option>`).join("");
4402
+ if (taskTypeSelect.options.length > 0) {
4403
+ taskTypeSelect.value = taskTypeSelect.options[0]?.value || "";
4404
+ }
4405
+ updatePlatformFields();
4406
+ }
4407
+ function updatePlatformFields() {
4408
+ const commandType = String(taskTypeSelect.value || "").trim();
4409
+ const isWeiboMonitor = commandType === "weibo-monitor";
4410
+ userIdWrap.style.display = isWeiboMonitor ? "" : "none";
4411
+ }
4412
+ function resetForm() {
4413
+ platformSelect.value = "xiaohongshu";
4414
+ updateTaskTypeOptions();
4415
+ editingIdInput.value = "";
4416
+ nameInput.value = "";
4417
+ enabledInput.checked = true;
4418
+ typeSelect.value = "interval";
4419
+ intervalInput.value = "30";
4420
+ runAtInput.value = "";
4421
+ maxRunsInput.value = "";
4422
+ profileInput.value = "";
4423
+ keywordInput.value = "";
4424
+ userIdInput.value = "";
4425
+ maxNotesInput.value = "50";
4426
+ envSelect.value = "debug";
4427
+ commentsInput.checked = true;
4428
+ likesInput.checked = false;
4429
+ headlessInput.checked = false;
4430
+ dryRunInput.checked = false;
4431
+ likeKeywordsInput.value = "";
4432
+ setActiveTaskContext("");
4433
+ updatePlatformFields();
4434
+ updateTypeFields();
4435
+ }
4436
+ function readFormAsPayload() {
4437
+ const maxRunsRaw = maxRunsInput.value.trim();
4438
+ const maxRuns = maxRunsRaw ? Math.max(1, Number(maxRunsRaw) || 1) : null;
4439
+ const commandType = String(taskTypeSelect.value || "xhs-unified").trim();
4440
+ const argv = {
4441
+ profile: profileInput.value.trim(),
4442
+ keyword: keywordInput.value.trim(),
4443
+ "max-notes": Number(maxNotesInput.value || 50) || 50,
4444
+ env: envSelect.value,
4445
+ "do-comments": commentsInput.checked,
4446
+ "do-likes": likesInput.checked,
4447
+ "like-keywords": likeKeywordsInput.value.trim(),
4448
+ headless: headlessInput.checked,
4449
+ "dry-run": dryRunInput.checked
4450
+ };
4451
+ if (commandType.startsWith("weibo")) {
4452
+ argv["task-type"] = commandTypeToWeiboTaskType2(commandType);
4453
+ argv["user-id"] = userIdInput.value.trim();
4454
+ }
4455
+ return {
4456
+ id: editingIdInput.value.trim(),
4457
+ name: nameInput.value.trim(),
4458
+ enabled: enabledInput.checked,
4459
+ commandType,
4460
+ scheduleType: typeSelect.value,
4461
+ intervalMinutes: Number(intervalInput.value || 30) || 30,
4462
+ runAt: toIsoOrNull(runAtInput.value),
4463
+ maxRuns,
4464
+ argv
4465
+ };
4466
+ }
4467
+ function applyTaskToForm(task) {
4468
+ pendingFocusTaskId = "";
4469
+ const platform = getPlatformForCommandType(String(task.commandType || "xhs-unified"));
4470
+ platformSelect.value = platform;
4471
+ updateTaskTypeOptions();
4472
+ taskTypeSelect.value = String(task.commandType || taskTypeSelect.value || "xhs-unified");
4473
+ editingIdInput.value = task.id;
4474
+ nameInput.value = task.name || "";
4475
+ enabledInput.checked = task.enabled !== false;
4476
+ typeSelect.value = task.scheduleType;
4477
+ intervalInput.value = String(task.intervalMinutes || 30);
4478
+ runAtInput.value = toLocalDatetimeValue(task.runAt);
4479
+ maxRunsInput.value = task.maxRuns ? String(task.maxRuns) : "";
4480
+ profileInput.value = String(task.commandArgv?.profile || "");
4481
+ keywordInput.value = String(task.commandArgv?.keyword || task.commandArgv?.k || "");
4482
+ userIdInput.value = String(task.commandArgv?.["user-id"] || task.commandArgv?.userId || "");
4483
+ maxNotesInput.value = String(task.commandArgv?.["max-notes"] ?? task.commandArgv?.target ?? 50);
4484
+ envSelect.value = String(task.commandArgv?.env || "debug");
4485
+ commentsInput.checked = task.commandArgv?.["do-comments"] !== false;
4486
+ likesInput.checked = task.commandArgv?.["do-likes"] === true;
4487
+ headlessInput.checked = task.commandArgv?.headless === true;
4488
+ dryRunInput.checked = task.commandArgv?.["dry-run"] === true;
4489
+ likeKeywordsInput.value = String(task.commandArgv?.["like-keywords"] || "");
4490
+ setActiveTaskContext(task.id);
4491
+ updatePlatformFields();
4492
+ updateTypeFields();
4493
+ }
4494
+ async function runScheduleJson(args, timeoutMs = 6e4) {
4495
+ const script = ctx2.api.pathJoin("apps", "webauto", "entry", "schedule.mjs");
4496
+ const ret = await ctx2.api.cmdRunJson({
4497
+ title: `schedule ${args.join(" ")}`,
4498
+ cwd: "",
4499
+ args: [script, ...args, "--json"],
4500
+ timeoutMs
4501
+ });
4502
+ if (!ret?.ok) {
4503
+ const reason = String(ret?.error || ret?.stderr || ret?.stdout || "unknown_error").trim();
4504
+ throw new Error(reason || "schedule command failed");
4505
+ }
4506
+ return ret.json || {};
4507
+ }
4508
+ function downloadJson(fileName, payload) {
4509
+ const text = JSON.stringify(payload, null, 2);
4510
+ const blob = new Blob([text], { type: "application/json" });
4511
+ const url = URL.createObjectURL(blob);
4512
+ const a = document.createElement("a");
4513
+ a.href = url;
4514
+ a.download = fileName;
4515
+ a.click();
4516
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
4517
+ }
4518
+ function renderTaskList() {
4519
+ listEl.innerHTML = "";
4520
+ if (tasks.length === 0) {
4521
+ listEl.innerHTML = '<div class="muted" style="padding: 12px;">\u6682\u65E0\u4EFB\u52A1</div>';
4522
+ return;
4523
+ }
4524
+ for (const task of tasks) {
4525
+ const card = createEl("div", {
4526
+ style: "border:1px solid var(--border); border-radius:10px; padding:10px; margin-bottom:10px; background: var(--panel-soft);"
4527
+ });
4528
+ const scheduleText = task.scheduleType === "once" ? `once @ ${task.runAt || "-"}` : task.scheduleType === "daily" ? `daily @ ${task.runAt || "-"}` : task.scheduleType === "weekly" ? `weekly @ ${task.runAt || "-"}` : `interval ${task.intervalMinutes}m`;
4529
+ const statusText = task.lastStatus ? `${task.lastStatus} / run=${task.runCount} / fail=${task.failCount}` : "never run";
4530
+ const headRow = createEl("div", { style: "display:flex; justify-content:space-between; gap:8px; margin-bottom:6px;" });
4531
+ headRow.appendChild(createEl("div", { style: "font-weight:600;" }, [task.name || task.id]));
4532
+ headRow.appendChild(
4533
+ createEl(
4534
+ "span",
4535
+ { style: `font-size:12px; color:${task.enabled ? "var(--accent-success)" : "var(--accent-danger)"};` },
4536
+ [task.enabled ? "enabled" : "disabled"]
4537
+ )
4538
+ );
4539
+ card.appendChild(headRow);
4540
+ card.appendChild(createEl("div", { className: "muted", style: "font-size:12px; margin-bottom:4px;" }, [`id=${task.id}`]));
4541
+ card.appendChild(createEl("div", { style: "font-size:12px;" }, [`schedule: ${scheduleText}`]));
4542
+ card.appendChild(createEl("div", { style: "font-size:12px;" }, [`maxRuns: ${task.maxRuns || "unlimited"}`]));
4543
+ card.appendChild(createEl("div", { style: "font-size:12px;" }, [`nextRunAt: ${task.nextRunAt || "-"}`]));
4544
+ card.appendChild(createEl("div", { style: "font-size:12px;" }, [`status: ${statusText}`]));
4545
+ const recent = (task.runHistory || []).slice(-5);
4546
+ if (recent.length > 0) {
4547
+ const recentRow = createEl("div", { style: "font-size:12px;" }, ["recent: "]);
4548
+ for (const h of recent) {
4549
+ const icon = h.status === "success" ? "\u2705" : "\u274C";
4550
+ const duration = h.durationMs ? `${Math.round(h.durationMs / 1e3)}s` : "-";
4551
+ const badge = createEl("span", { title: `${h.timestamp} ${duration}` }, [icon]);
4552
+ recentRow.appendChild(badge);
4553
+ recentRow.appendChild(document.createTextNode(" "));
4554
+ }
4555
+ card.appendChild(recentRow);
4556
+ }
4557
+ if (task.lastError) {
4558
+ card.appendChild(createEl("div", { style: "font-size:12px; color:var(--accent-danger);" }, [`error: ${task.lastError}`]));
4559
+ }
4560
+ const actions = createEl("div", { className: "btn-group", style: "margin-top: 8px;" });
4561
+ const editBtn = createEl("button", { className: "secondary" }, ["\u7F16\u8F91"]);
4562
+ const loadBtn = createEl("button", { className: "secondary" }, ["\u8F7D\u5165\u914D\u7F6E"]);
4563
+ const runBtn = createEl("button", { className: "secondary" }, ["\u6267\u884C"]);
4564
+ const exportBtn = createEl("button", { className: "secondary" }, ["\u5BFC\u51FA"]);
4565
+ const delBtn = createEl("button", { className: "danger" }, ["\u5220\u9664"]);
4566
+ actions.appendChild(editBtn);
4567
+ actions.appendChild(loadBtn);
4568
+ actions.appendChild(runBtn);
4569
+ actions.appendChild(exportBtn);
4570
+ actions.appendChild(delBtn);
4571
+ card.appendChild(actions);
4572
+ editBtn.onclick = () => applyTaskToForm(task);
4573
+ loadBtn.onclick = () => openConfigTab(task.id);
4574
+ runBtn.onclick = async () => {
4575
+ try {
4576
+ setActiveTaskContext(task.id);
4577
+ const out = await runScheduleJson(["run", task.id], 0);
4578
+ const runId = String(
4579
+ out?.result?.runResult?.lastRunId || out?.result?.runResult?.runId || out?.runResult?.runId || ""
4580
+ ).trim();
4581
+ if (task.commandType === "xhs-unified" && ctx2 && typeof ctx2 === "object") {
4582
+ const argv = task.commandArgv || {};
4583
+ ctx2.xhsCurrentRun = {
4584
+ runId: runId || null,
4585
+ taskId: task.id,
4586
+ profileId: String(argv.profile || ""),
4587
+ keyword: String(argv.keyword || argv.k || ""),
4588
+ target: Number(argv["max-notes"] || argv.target || 0) || 0,
4589
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
4590
+ };
4591
+ }
4592
+ if (typeof ctx2.setStatus === "function") {
4593
+ ctx2.setStatus(`running: ${task.id}`);
4594
+ }
4595
+ if (task.commandType === "xhs-unified" && typeof ctx2.setActiveTab === "function") {
4596
+ ctx2.setActiveTab("dashboard");
4597
+ }
4598
+ await refreshList();
4599
+ } catch (err) {
4600
+ alert(`\u6267\u884C\u5931\u8D25: ${err?.message || String(err)}`);
4601
+ }
4602
+ };
4603
+ exportBtn.onclick = async () => {
4604
+ try {
4605
+ const out = await runScheduleJson(["export", task.id]);
4606
+ downloadJson(`${task.id}.json`, out);
4607
+ } catch (err) {
4608
+ alert(`\u5BFC\u51FA\u5931\u8D25: ${err?.message || String(err)}`);
4609
+ }
4610
+ };
4611
+ delBtn.onclick = async () => {
4612
+ if (!confirm(`\u786E\u8BA4\u5220\u9664\u4EFB\u52A1 ${task.id} ?`)) return;
4613
+ try {
4614
+ await runScheduleJson(["delete", task.id]);
4615
+ await refreshList();
4616
+ } catch (err) {
4617
+ alert(`\u5220\u9664\u5931\u8D25: ${err?.message || String(err)}`);
4618
+ }
4619
+ };
4620
+ listEl.appendChild(card);
4621
+ }
4622
+ }
4623
+ async function refreshList() {
4624
+ const out = await runScheduleJson(["list"]);
4625
+ tasks = parseTaskRows(out);
4626
+ if (!pendingFocusTaskId) {
4627
+ pendingFocusTaskId = String(ctx2?.activeTaskConfigId || "").trim();
4628
+ }
4629
+ if (pendingFocusTaskId) {
4630
+ const target = tasks.find((item) => item.id === pendingFocusTaskId);
4631
+ if (target) {
4632
+ applyTaskToForm(target);
4633
+ } else {
4634
+ setActiveTaskContext("");
4635
+ }
4636
+ pendingFocusTaskId = "";
4637
+ } else {
4638
+ setActiveTaskContext(String(ctx2?.activeTaskConfigId || "").trim());
4639
+ }
4640
+ renderTaskList();
4641
+ }
4642
+ async function saveTask() {
4643
+ const payload = readFormAsPayload();
4644
+ if (!payload.name) {
4645
+ alert("\u4EFB\u52A1\u540D\u4E0D\u80FD\u4E3A\u7A7A");
4646
+ return;
4647
+ }
4648
+ if (!payload.argv.profile && !payload.argv.profiles && !payload.argv.profilepool) {
4649
+ alert("profile/profiles/profilepool \u81F3\u5C11\u586B\u5199\u4E00\u4E2A");
4650
+ return;
4651
+ }
4652
+ const commandType = String(payload.commandType || "").trim();
4653
+ const keywordRequired = commandType === "xhs-unified" || commandType === "weibo-search" || commandType === "1688-search";
4654
+ if (keywordRequired && !payload.argv.keyword) {
4655
+ alert("\u5173\u952E\u8BCD\u4E0D\u80FD\u4E3A\u7A7A");
4656
+ return;
4657
+ }
4658
+ if (commandType === "weibo-monitor" && !payload.argv["user-id"]) {
4659
+ alert("\u5FAE\u535A monitor \u4EFB\u52A1\u9700\u8981 user-id");
4660
+ return;
4661
+ }
4662
+ const args = payload.id ? ["update", payload.id] : ["add"];
4663
+ args.push("--name", payload.name);
4664
+ args.push("--enabled", String(payload.enabled));
4665
+ args.push("--command-type", commandType || "xhs-unified");
4666
+ args.push("--schedule-type", payload.scheduleType);
4667
+ if (payload.scheduleType === "once") {
4668
+ if (!payload.runAt) {
4669
+ alert("\u4E00\u6B21\u6027\u4EFB\u52A1\u9700\u8981\u951A\u70B9\u65F6\u95F4");
4670
+ return;
4671
+ }
4672
+ args.push("--run-at", payload.runAt);
4673
+ } else if (payload.scheduleType === "daily" || payload.scheduleType === "weekly") {
4674
+ if (!payload.runAt) {
4675
+ alert(`${payload.scheduleType} \u4EFB\u52A1\u9700\u8981\u951A\u70B9\u65F6\u95F4`);
4676
+ return;
4677
+ }
4678
+ args.push("--run-at", payload.runAt);
4679
+ } else {
4680
+ args.push("--interval-minutes", String(Math.max(1, payload.intervalMinutes)));
4681
+ }
4682
+ args.push("--max-runs", payload.maxRuns === null ? "0" : String(payload.maxRuns));
4683
+ args.push("--argv-json", JSON.stringify(payload.argv));
4684
+ try {
4685
+ const out = await runScheduleJson(args);
4686
+ const savedId = String(out?.task?.id || payload.id || "").trim();
4687
+ pendingFocusTaskId = savedId;
4688
+ if (savedId) setActiveTaskContext(savedId);
4689
+ await refreshList();
4690
+ } catch (err) {
4691
+ alert(`\u4FDD\u5B58\u5931\u8D25: ${err?.message || String(err)}`);
4692
+ }
4693
+ }
4694
+ async function runDueNow() {
4695
+ try {
4696
+ const out = await runScheduleJson(["run-due", "--limit", "20"], 0);
4697
+ alert(`\u5230\u70B9\u4EFB\u52A1\u6267\u884C\u5B8C\u6210\uFF1Adue=${out.count || 0}, success=${out.success || 0}, failed=${out.failed || 0}`);
4698
+ await refreshList();
4699
+ } catch (err) {
4700
+ alert(`\u6267\u884C\u5931\u8D25: ${err?.message || String(err)}`);
4701
+ }
4702
+ }
4703
+ async function exportAll() {
4704
+ try {
4705
+ const out = await runScheduleJson(["export"]);
4706
+ downloadJson("webauto-schedules.json", out);
4707
+ } catch (err) {
4708
+ alert(`\u5BFC\u51FA\u5931\u8D25: ${err?.message || String(err)}`);
4709
+ }
4710
+ }
4711
+ async function importFromFile() {
4712
+ const input = document.createElement("input");
4713
+ input.type = "file";
4714
+ input.accept = ".json";
4715
+ input.onchange = async (evt) => {
4716
+ const file = evt.target.files?.[0];
4717
+ if (!file) return;
4718
+ try {
4719
+ const text = await file.text();
4720
+ await runScheduleJson(["import", "--payload-json", text, "--mode", "merge"]);
4721
+ await refreshList();
4722
+ } catch (err) {
4723
+ alert(`\u5BFC\u5165\u5931\u8D25: ${err?.message || String(err)}`);
4724
+ }
4725
+ };
4726
+ input.click();
4727
+ }
4728
+ async function startDaemon() {
4729
+ if (daemonRunId) {
4730
+ alert("daemon \u5DF2\u542F\u52A8");
4731
+ return;
4732
+ }
4733
+ const interval = Math.max(5, Number(daemonIntervalInput.value || 30) || 30);
4734
+ const script = ctx2.api.pathJoin("apps", "webauto", "entry", "schedule.mjs");
4735
+ const ret = await ctx2.api.cmdSpawn({
4736
+ title: `schedule daemon ${interval}s`,
4737
+ cwd: "",
4738
+ args: [script, "daemon", "--interval-sec", String(interval), "--limit", "20", "--json"],
4739
+ groupKey: "scheduler"
4740
+ });
4741
+ daemonRunId = String(ret?.runId || "").trim();
4742
+ setDaemonStatus(daemonRunId ? `daemon: \u8FD0\u884C\u4E2D (${daemonRunId})` : "daemon: \u542F\u52A8\u5931\u8D25");
4743
+ }
4744
+ async function stopDaemon() {
4745
+ if (!daemonRunId) {
4746
+ setDaemonStatus("daemon: \u672A\u542F\u52A8");
4747
+ return;
4748
+ }
4749
+ try {
4750
+ await ctx2.api.cmdKill({ runId: daemonRunId });
4751
+ } catch {
4752
+ }
4753
+ daemonRunId = "";
4754
+ setDaemonStatus("daemon: \u5DF2\u505C\u6B62");
4755
+ }
4756
+ platformSelect.addEventListener("change", updateTaskTypeOptions);
4757
+ taskTypeSelect.addEventListener("change", updatePlatformFields);
4758
+ typeSelect.addEventListener("change", updateTypeFields);
4759
+ saveBtn.onclick = () => void saveTask();
4760
+ resetBtn.onclick = () => resetForm();
4761
+ refreshBtn.onclick = () => void refreshList();
4762
+ runDueBtn.onclick = () => void runDueNow();
4763
+ exportAllBtn.onclick = () => void exportAll();
4764
+ importBtn.onclick = () => void importFromFile();
4765
+ openConfigBtn.onclick = () => {
4766
+ const id = String(editingIdInput.value || activeTaskIdText.textContent || "").trim();
4767
+ openConfigTab(id);
4768
+ };
4769
+ daemonStartBtn.onclick = () => void startDaemon();
4770
+ daemonStopBtn.onclick = () => void stopDaemon();
4771
+ if (typeof ctx2.api?.onCmdEvent === "function") {
4772
+ unsubscribeCmd = ctx2.api.onCmdEvent((evt) => {
4773
+ const runId = String(evt?.runId || "").trim();
4774
+ if (!daemonRunId || runId !== daemonRunId) return;
4775
+ if (evt?.type === "exit") {
4776
+ daemonRunId = "";
4777
+ setDaemonStatus("daemon: \u5DF2\u9000\u51FA");
4778
+ }
4779
+ });
4780
+ }
4781
+ resetForm();
4782
+ updateTaskTypeOptions();
4783
+ void refreshList().catch((err) => {
4784
+ listEl.innerHTML = `<div class="muted" style="padding: 12px;">\u52A0\u8F7D\u5931\u8D25: ${err?.message || String(err)}</div>`;
4785
+ });
4786
+ return () => {
4787
+ if (unsubscribeCmd) {
4788
+ try {
4789
+ unsubscribeCmd();
4790
+ } catch {
4791
+ }
4792
+ unsubscribeCmd = null;
4793
+ }
4794
+ };
2905
4795
  }
2906
4796
 
2907
4797
  // src/renderer/index.mts
2908
4798
  var tabs = [
2909
4799
  { id: "setup-wizard", label: "\u521D\u59CB\u5316", render: renderSetupWizard },
2910
- { id: "config", label: "\u914D\u7F6E", render: renderConfigPanel },
4800
+ { id: "tasks", label: "\u4EFB\u52A1", render: renderTasksPanel },
2911
4801
  { id: "dashboard", label: "\u770B\u677F", render: renderDashboard },
4802
+ { id: "scheduler", label: "\u5B9A\u65F6\u4EFB\u52A1", render: renderSchedulerPanel },
2912
4803
  { id: "account-manager", label: "\u8D26\u6237\u7BA1\u7406", render: renderAccountManager },
2913
4804
  { id: "preflight", label: "\u65E7\u9884\u5904\u7406", render: renderPreflight, hidden: true },
2914
4805
  { id: "logs", label: "\u65E5\u5FD7", render: renderLogs },
@@ -2918,8 +4809,19 @@ var tabsEl = document.getElementById("tabs");
2918
4809
  var contentEl = document.getElementById("content");
2919
4810
  var statusEl = document.getElementById("status");
2920
4811
  var activeTabCleanup = null;
4812
+ var mutableApi = { ...window.api || {}, settings: null };
4813
+ var tabIcons = {
4814
+ "setup-wizard": "\u26A1",
4815
+ "tasks": "\u{1F4DD}",
4816
+ "dashboard": "\u{1F4CA}",
4817
+ "scheduler": "\u23F0",
4818
+ "account-manager": "\u{1F464}",
4819
+ "preflight": "\u{1F527}",
4820
+ "logs": "\u{1F4DD}",
4821
+ "settings": "\u{1F528}"
4822
+ };
2921
4823
  var ctx = {
2922
- api: window.api,
4824
+ api: mutableApi,
2923
4825
  settings: null,
2924
4826
  xhsCurrentRun: null,
2925
4827
  activeRunId: null,
@@ -2981,6 +4883,10 @@ function startDesktopHeartbeat() {
2981
4883
  async function loadSettings() {
2982
4884
  await ctx.refreshSettings();
2983
4885
  }
4886
+ function focusTabButton(tabId) {
4887
+ const button = tabsEl.querySelector(`[data-tab-id="${tabId}"]`);
4888
+ button?.focus();
4889
+ }
2984
4890
  function setActiveTab(id) {
2985
4891
  if (activeTabCleanup) {
2986
4892
  try {
@@ -2989,13 +4895,54 @@ function setActiveTab(id) {
2989
4895
  }
2990
4896
  activeTabCleanup = null;
2991
4897
  }
4898
+ const visibleTabs = tabs.filter((x) => !x.hidden);
4899
+ tabsEl.setAttribute("role", "tablist");
2992
4900
  tabsEl.textContent = "";
2993
- for (const t of tabs.filter((x) => !x.hidden)) {
2994
- const el = createEl("div", { className: `tab ${t.id === id ? "active" : ""}` }, [t.label]);
4901
+ for (let index = 0; index < visibleTabs.length; index += 1) {
4902
+ const t = visibleTabs[index];
4903
+ const isActive = t.id === id;
4904
+ const icon = tabIcons[t.id] || "";
4905
+ const el = createEl("button", { className: `tab ${isActive ? "active" : ""}`, type: "button" }, [
4906
+ createEl("span", { className: "tab-icon" }, [icon]),
4907
+ t.label
4908
+ ]);
4909
+ el.dataset.tabId = t.id;
4910
+ el.setAttribute("role", "tab");
4911
+ el.setAttribute("aria-selected", String(isActive));
4912
+ el.tabIndex = isActive ? 0 : -1;
2995
4913
  el.addEventListener("click", () => setActiveTab(t.id));
4914
+ el.addEventListener("keydown", (evt) => {
4915
+ const key = evt.key;
4916
+ if (key === "Enter" || key === " ") {
4917
+ evt.preventDefault();
4918
+ setActiveTab(t.id);
4919
+ return;
4920
+ }
4921
+ let nextIndex = -1;
4922
+ if (key === "ArrowRight") nextIndex = (index + 1) % visibleTabs.length;
4923
+ else if (key === "ArrowLeft") nextIndex = (index - 1 + visibleTabs.length) % visibleTabs.length;
4924
+ else if (key === "Home") nextIndex = 0;
4925
+ else if (key === "End") nextIndex = visibleTabs.length - 1;
4926
+ if (nextIndex >= 0) {
4927
+ evt.preventDefault();
4928
+ const nextTab = visibleTabs[nextIndex];
4929
+ if (!nextTab) return;
4930
+ setActiveTab(nextTab.id);
4931
+ requestAnimationFrame(() => focusTabButton(nextTab.id));
4932
+ }
4933
+ });
2996
4934
  tabsEl.appendChild(el);
2997
4935
  }
2998
4936
  contentEl.textContent = "";
4937
+ const reducedMotion = typeof window.matchMedia === "function" ? window.matchMedia("(prefers-reduced-motion: reduce)").matches : false;
4938
+ if (!reducedMotion) {
4939
+ contentEl.classList.remove("animate-fade-in");
4940
+ if (typeof requestAnimationFrame === "function") {
4941
+ requestAnimationFrame(() => contentEl.classList.add("animate-fade-in"));
4942
+ } else {
4943
+ contentEl.classList.add("animate-fade-in");
4944
+ }
4945
+ }
2999
4946
  const tab = tabs.find((x) => x.id === id);
3000
4947
  const dispose = tab.render(contentEl, ctx);
3001
4948
  if (typeof dispose === "function") activeTabCleanup = dispose;
@@ -3035,10 +4982,8 @@ function installCmdEvents() {
3035
4982
  async function detectStartupTab() {
3036
4983
  try {
3037
4984
  const env = typeof window.api?.envCheckAll === "function" ? await window.api.envCheckAll() : null;
3038
- const rows = await listAccountProfiles(window.api).catch(() => []);
3039
- const hasAccount = rows.some((row) => row.valid);
3040
4985
  const envReady = Boolean(env?.allReady);
3041
- if (envReady && hasAccount) return "config";
4986
+ if (envReady) return "tasks";
3042
4987
  } catch {
3043
4988
  }
3044
4989
  return "setup-wizard";