@web-auto/webauto 0.1.4 → 0.1.7

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 +983 -128
  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 +2423 -469
  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 +256 -31
  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 (@web-auto/camo)</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 Service (7704\uFF0C\u53EF\u9009)</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 Runtime (python -m camoufox)</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,189 @@ 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 || res?.core?.error || res?.core?.services?.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 Runtime/GeoIP\uFF09..." : geoip && browser ? "\u6B63\u5728\u5B89\u88C5\u4F9D\u8D56\uFF08Camoufox Runtime/GeoIP\uFF09..." : geoip ? "\u6B63\u5728\u5B89\u88C5 GeoIP\uFF08\u53EF\u9009\uFF09..." : "\u6B63\u5728\u5B89\u88C5 Camoufox Runtime...";
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 || res?.install?.error || res?.install?.stderr || res?.install?.stdout || res?.install?.json?.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 Runtime/GeoIP\uFF09..." : geoip && browser ? "\u6B63\u5728\u5B89\u88C5\u4F9D\u8D56\uFF08Camoufox Runtime/GeoIP\uFF09..." : geoip ? "\u6B63\u5728\u5B89\u88C5 GeoIP\uFF08\u53EF\u9009\uFF09..." : "\u6B63\u5728\u5B89\u88C5 Camoufox Runtime...";
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.core) {
1765
+ const res = await repairCoreServices();
1766
+ if (!res.ok) ok = false;
1767
+ if (res.detail) detail = res.detail;
1768
+ }
1769
+ if (missing.runtime) {
1770
+ const res = await repairInstall({ browser: true });
1771
+ if (!res.ok) ok = false;
1772
+ if (res.detail) detail = res.detail;
1773
+ }
1774
+ if (missing.geoip) {
1775
+ const res = await repairInstall({ geoip: true });
1776
+ if (!res.ok) ok = false;
1777
+ if (res.detail) detail = res.detail;
1778
+ }
1779
+ return { ok, detail };
1780
+ }
1781
+ function syncRepairButtons(snapshot) {
1782
+ const missing = getMissing(snapshot);
1783
+ repairCoreBtn.style.display = missing.core ? "" : "none";
1784
+ repairCore2Btn.style.display = missing.runtimeService ? "" : "none";
1785
+ repairCamoBtn.style.display = missing.camo ? "" : "none";
1786
+ repairRuntimeBtn.style.display = missing.runtime ? "" : "none";
1787
+ repairGeoipBtn.style.display = missing.geoip ? "" : "none";
1788
+ const hasRequiredMissing = missing.core || missing.camo || missing.runtime;
1789
+ envRepairAllBtn.style.display = hasRequiredMissing ? "" : "none";
1790
+ envRepairAllBtn.disabled = !hasRequiredMissing;
1791
+ }
1792
+ async function runRepair(label, action) {
1670
1793
  envCheckBtn.disabled = true;
1671
- envCheckBtn.textContent = "\u68C0\u67E5/\u4FEE\u590D\u4E2D...";
1794
+ envRepairAllBtn.disabled = true;
1795
+ envReinstallAllBtn.disabled = true;
1796
+ repairCamoBtn.disabled = true;
1797
+ repairCoreBtn.disabled = true;
1798
+ repairCore2Btn.disabled = true;
1799
+ repairRuntimeBtn.disabled = true;
1800
+ repairGeoipBtn.disabled = true;
1801
+ setupStatusText.textContent = `${label}\u4E2D...`;
1802
+ let ok = false;
1803
+ let detail = "";
1672
1804
  try {
1673
- const before = await collectEnvironment();
1674
- applyEnvironment(before);
1675
- if (!isEnvReady(before)) {
1676
- await autoRepairEnvironment(before);
1805
+ const result = await action();
1806
+ ok = result?.ok !== false;
1807
+ if (result?.detail) detail = String(result.detail || "");
1808
+ } finally {
1809
+ const latest = await collectEnvironment().catch(() => null);
1810
+ if (latest) {
1811
+ applyEnvironment(latest);
1812
+ updateCompleteStatus();
1813
+ if (!detail) {
1814
+ if (label.includes("Camoufox") || label.includes("Runtime")) {
1815
+ ok = Boolean(latest.firefox?.installed);
1816
+ } else if (label.includes("Camoufox CLI") || label.includes("CLI") || label.includes("camo")) {
1817
+ ok = Boolean(latest.camo?.installed);
1818
+ } else if (label.includes("\u6838\u5FC3")) {
1819
+ ok = Boolean(latest.services?.unifiedApi && latest.services?.camoRuntime);
1820
+ } else {
1821
+ ok = isEnvReady(latest);
1822
+ }
1823
+ }
1677
1824
  }
1678
- const after = await collectEnvironment();
1679
- applyEnvironment(after);
1825
+ setupStatusText.textContent = `${label}${ok ? "\u6210\u529F" : "\u5931\u8D25"}${detail ? `\uFF1A${detail}` : ""}`;
1826
+ await pushRepairHistory({
1827
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1828
+ action: label,
1829
+ ok,
1830
+ detail: detail || void 0
1831
+ });
1832
+ envCheckBtn.disabled = false;
1833
+ envReinstallAllBtn.disabled = false;
1834
+ }
1835
+ }
1836
+ async function checkEnvironment() {
1837
+ envCheckBtn.disabled = true;
1838
+ envCheckBtn.textContent = "\u68C0\u67E5\u4E2D...";
1839
+ try {
1840
+ const snapshot = await collectEnvironment();
1841
+ applyEnvironment(snapshot);
1680
1842
  updateCompleteStatus();
1681
1843
  if (!envReady) {
1682
1844
  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(", ")}`;
1845
+ if (!snapshot?.camo?.installed) missing.push("camo-cli");
1846
+ if (!snapshot?.services?.unifiedApi) missing.push("unified-api");
1847
+ if (!snapshot?.firefox?.installed) missing.push("camoufox-runtime");
1848
+ setupStatusText.textContent = `\u5B58\u5728\u5F85\u4FEE\u590D\u9879: ${missing.join(", ")}`;
1849
+ if (!snapshot?.services?.camoRuntime) {
1850
+ setupStatusText.textContent += "\uFF08camo-runtime \u672A\u5C31\u7EEA\uFF0C\u5F53\u524D\u4E3A\u53EF\u9009\uFF09";
1851
+ }
1852
+ } else if (!snapshot?.geoip?.installed) {
1853
+ setupStatusText.textContent = "\u73AF\u5883\u5C31\u7EEA\uFF08GeoIP \u53EF\u9009\uFF0C\u672A\u5B89\u88C5\u4E0D\u5F71\u54CD\u4F7F\u7528\uFF09";
1854
+ } else if (!snapshot?.services?.camoRuntime) {
1855
+ setupStatusText.textContent = "\u73AF\u5883\u5C31\u7EEA\uFF08camo-runtime \u672A\u5C31\u7EEA\uFF0C\u5F53\u524D\u4E0D\u963B\u585E\uFF09";
1689
1856
  }
1690
1857
  } catch (err) {
1691
1858
  console.error("Environment check failed:", err);
@@ -1694,16 +1861,40 @@ function renderSetupWizard(root, ctx2) {
1694
1861
  envCheckBtn.disabled = false;
1695
1862
  envCheckBtn.textContent = "\u91CD\u65B0\u68C0\u67E5";
1696
1863
  }
1864
+ async function tickEnvironment() {
1865
+ if (envCheckInFlight) return;
1866
+ envCheckInFlight = true;
1867
+ try {
1868
+ await checkEnvironment();
1869
+ } finally {
1870
+ envCheckInFlight = false;
1871
+ }
1872
+ }
1697
1873
  function updateEnvItem(id, ok, detail) {
1698
1874
  const el = root.querySelector(`#${id}`);
1699
1875
  if (!el) return;
1700
1876
  const icon = el.querySelector(".icon");
1701
- const text = el.querySelector("span:last-child");
1877
+ const text = el.querySelector(".env-label");
1702
1878
  const baseLabel = el.dataset.label || text.textContent || "";
1703
1879
  el.dataset.label = baseLabel;
1704
1880
  icon.textContent = ok ? "\u2713" : "\u2717";
1705
1881
  icon.style.color = ok ? "var(--success)" : "var(--danger)";
1706
- text.textContent = detail ? `${baseLabel} \xB7 ${detail}` : baseLabel;
1882
+ const safeDetail = String(detail || "").trim();
1883
+ const shouldAppend = safeDetail && !String(baseLabel || "").includes(safeDetail);
1884
+ text.textContent = shouldAppend ? `${baseLabel} \xB7 ${safeDetail}` : baseLabel;
1885
+ }
1886
+ async function tickAccounts() {
1887
+ if (accountCheckInFlight) return;
1888
+ accountCheckInFlight = true;
1889
+ try {
1890
+ await refreshAccounts();
1891
+ const pending = accounts.filter((acc) => acc.status === "pending");
1892
+ for (const acc of pending) {
1893
+ await syncProfileAccount(acc.profileId);
1894
+ }
1895
+ } finally {
1896
+ accountCheckInFlight = false;
1897
+ }
1707
1898
  }
1708
1899
  async function refreshAccounts() {
1709
1900
  try {
@@ -1739,46 +1930,56 @@ function renderSetupWizard(root, ctx2) {
1739
1930
  }
1740
1931
  async function addAccount() {
1741
1932
  const alias = newAliasInput.value.trim();
1742
- if (!alias) {
1743
- alert("\u8BF7\u8F93\u5165\u8D26\u6237\u522B\u540D");
1744
- return;
1745
- }
1746
1933
  addAccountBtn.disabled = true;
1747
1934
  addAccountBtn.textContent = "\u521B\u5EFA\u4E2D...";
1748
1935
  try {
1749
- const batchKey = "xiaohongshu";
1750
1936
  const out = await ctx2.api.cmdRunJson({
1751
- title: "profilepool add",
1937
+ title: "account add",
1752
1938
  cwd: "",
1753
- args: [ctx2.api.pathJoin("apps", "webauto", "entry", "profilepool.mjs"), "add", batchKey, "--json"]
1939
+ args: [
1940
+ ctx2.api.pathJoin("apps", "webauto", "entry", "account.mjs"),
1941
+ "add",
1942
+ "--platform",
1943
+ "xiaohongshu",
1944
+ "--status",
1945
+ "pending",
1946
+ ...alias ? ["--alias", alias] : [],
1947
+ "--json"
1948
+ ]
1754
1949
  });
1755
- if (!out?.ok || !out?.json?.profileId) {
1950
+ const profileId = String(out?.json?.account?.profileId || "").trim();
1951
+ if (!out?.ok || !profileId) {
1756
1952
  alert("\u521B\u5EFA\u8D26\u53F7\u5931\u8D25: " + (out?.error || "\u672A\u77E5\u9519\u8BEF"));
1757
1953
  return;
1758
1954
  }
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();
1955
+ if (alias) {
1956
+ const aliases = { ...ctx2.api.settings?.profileAliases, [profileId]: alias };
1957
+ await ctx2.api.settingsSet({ profileAliases: aliases });
1958
+ if (typeof ctx2.refreshSettings === "function") {
1959
+ await ctx2.refreshSettings();
1960
+ }
1764
1961
  }
1962
+ await refreshAccounts();
1963
+ setupStatusText.textContent = `\u8D26\u53F7 ${profileId} \u5DF2\u521B\u5EFA\uFF0C\u7B49\u5F85\u767B\u5F55...`;
1765
1964
  const timeoutSec = ctx2.api.settings?.timeouts?.loginTimeoutSec || 900;
1766
1965
  const loginArgs = [
1767
1966
  ctx2.api.pathJoin("apps", "webauto", "entry", "profilepool.mjs"),
1768
1967
  "login-profile",
1769
1968
  profileId,
1969
+ "--wait-sync",
1970
+ "false",
1770
1971
  "--timeout-sec",
1771
1972
  String(timeoutSec),
1772
1973
  "--keep-session"
1773
1974
  ];
1774
1975
  await ctx2.api.cmdSpawn({
1775
- title: `\u767B\u5F55 ${alias}`,
1976
+ title: `\u767B\u5F55 ${alias || profileId}`,
1776
1977
  cwd: "",
1777
1978
  args: loginArgs,
1778
1979
  groupKey: "profilepool"
1779
1980
  });
1780
1981
  newAliasInput.value = "";
1781
- await refreshAccounts();
1982
+ startAutoSyncProfile(profileId);
1782
1983
  } catch (err) {
1783
1984
  alert("\u6DFB\u52A0\u8D26\u53F7\u5931\u8D25: " + (err?.message || String(err)));
1784
1985
  } finally {
@@ -1788,395 +1989,964 @@ function renderSetupWizard(root, ctx2) {
1788
1989
  }
1789
1990
  function updateCompleteStatus() {
1790
1991
  const hasValidAccount = accounts.some((a) => a.valid);
1791
- const canProceed = envReady && hasValidAccount;
1992
+ const canProceed = envReady;
1792
1993
  enterMainBtn.disabled = !canProceed;
1793
1994
  if (canProceed) {
1794
- setupStatusText.textContent = `\u73AF\u5883\u5C31\u7EEA\uFF0C${accounts.length} \u4E2A\u8D26\u6237\u914D\u7F6E\u5B8C\u6210`;
1995
+ 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
1996
  enterMainBtn.className = "";
1796
1997
  } else {
1797
1998
  const missing = [];
1798
1999
  if (!envReady) missing.push("\u73AF\u5883\u68C0\u67E5");
1799
- if (!hasValidAccount) missing.push("\u81F3\u5C11\u4E00\u4E2A\u8D26\u6237");
2000
+ if (!hasValidAccount) missing.push("\u8D26\u6237\u767B\u5F55\uFF08\u53EF\u7A0D\u540E\uFF09");
1800
2001
  setupStatusText.textContent = `\u5C1A\u672A\u5B8C\u6210: ${missing.join("\u3001")}`;
1801
2002
  }
1802
2003
  }
2004
+ function getSettingsAlias(profileId) {
2005
+ return String(ctx2.api?.settings?.profileAliases?.[profileId] || "").trim();
2006
+ }
2007
+ async function upsertAliasFromProfile(profile) {
2008
+ const profileId = String(profile?.profileId || "").trim();
2009
+ const alias = String(profile?.alias || "").trim();
2010
+ if (!profileId || !alias) return;
2011
+ if (getSettingsAlias(profileId) === alias) return;
2012
+ const aliases = { ...ctx2.api?.settings?.profileAliases || {}, [profileId]: alias };
2013
+ await ctx2.api.settingsSet({ profileAliases: aliases }).catch(() => null);
2014
+ if (typeof ctx2.refreshSettings === "function") {
2015
+ await ctx2.refreshSettings().catch(() => null);
2016
+ }
2017
+ }
2018
+ async function syncProfileAccount(profileId) {
2019
+ const id = String(profileId || "").trim();
2020
+ if (!id) return false;
2021
+ const result = await ctx2.api.cmdRunJson({
2022
+ title: `account sync ${id}`,
2023
+ cwd: "",
2024
+ args: [
2025
+ ctx2.api.pathJoin("apps", "webauto", "entry", "account.mjs"),
2026
+ "sync",
2027
+ id,
2028
+ "--pending-while-login",
2029
+ "--json"
2030
+ ],
2031
+ timeoutMs: 2e4
2032
+ }).catch(() => null);
2033
+ const profile = result?.json?.profile;
2034
+ if (!profile || String(profile.profileId || "").trim() !== id) return false;
2035
+ await upsertAliasFromProfile(profile);
2036
+ await refreshAccounts();
2037
+ const hasAccountId = Boolean(String(profile.accountId || "").trim());
2038
+ if (hasAccountId) {
2039
+ setupStatusText.textContent = `\u8D26\u53F7 ${id} \u5DF2\u8BC6\u522B\uFF0Calias=${String(profile.alias || "").trim() || "\u672A\u547D\u540D"}`;
2040
+ return true;
2041
+ }
2042
+ if (String(profile.status || "").trim() === "pending") {
2043
+ setupStatusText.textContent = `\u8D26\u53F7 ${id} \u5F85\u767B\u5F55\uFF0C\u7B49\u5F85\u68C0\u6D4B\u767B\u5F55\u5B8C\u6210...`;
2044
+ }
2045
+ return false;
2046
+ }
2047
+ function startAutoSyncProfile(profileId) {
2048
+ const id = String(profileId || "").trim();
2049
+ if (!id) return;
2050
+ const existing = autoSyncTimers.get(id);
2051
+ if (existing) clearInterval(existing);
2052
+ const timeoutSec = Math.max(30, Number(ctx2.api?.settings?.timeouts?.loginTimeoutSec || 900));
2053
+ const intervalMs = 2e3;
2054
+ const maxAttempts = Math.ceil(timeoutSec * 1e3 / intervalMs);
2055
+ let attempts = 0;
2056
+ void syncProfileAccount(id);
2057
+ const timer = setInterval(() => {
2058
+ attempts += 1;
2059
+ void syncProfileAccount(id).then((done) => {
2060
+ if (done || attempts >= maxAttempts) {
2061
+ const current = autoSyncTimers.get(id);
2062
+ if (current) clearInterval(current);
2063
+ autoSyncTimers.delete(id);
2064
+ if (!done) {
2065
+ setupStatusText.textContent = `\u8D26\u53F7 ${id} \u767B\u5F55\u68C0\u6D4B\u8D85\u65F6\uFF0C\u8BF7\u786E\u8BA4\u5DF2\u5728\u6D4F\u89C8\u5668\u5B8C\u6210\u767B\u5F55`;
2066
+ }
2067
+ }
2068
+ });
2069
+ }, intervalMs);
2070
+ autoSyncTimers.set(id, timer);
2071
+ }
1803
2072
  envCheckBtn.onclick = checkEnvironment;
2073
+ envRepairAllBtn.onclick = () => void runRepair("\u4E00\u952E\u4FEE\u590D\u7F3A\u5931\u9879", async () => {
2074
+ const snapshot = await collectEnvironment();
2075
+ return await repairMissing(snapshot);
2076
+ });
2077
+ envReinstallAllBtn.onclick = () => void runRepair("\u4E00\u952E\u5378\u8F7D\u91CD\u88C5\u8D44\u6E90", () => (async () => {
2078
+ const core = await repairCoreServices();
2079
+ if (!core.ok) return core;
2080
+ return repairInstall({ browser: true, geoip: true, reinstall: true });
2081
+ })());
2082
+ repairCoreBtn.onclick = () => void runRepair("\u4FEE\u590D\u6838\u5FC3\u670D\u52A1", repairCoreServices);
2083
+ repairCore2Btn.onclick = () => void runRepair("\u4FEE\u590D\u6838\u5FC3\u670D\u52A1", repairCoreServices);
2084
+ repairCamoBtn.onclick = () => void runRepair("\u4FEE\u590D Camo CLI", repairCoreServices);
2085
+ repairRuntimeBtn.onclick = () => void runRepair("\u4FEE\u590D Camoufox Runtime", () => repairInstall({ browser: true }));
2086
+ repairGeoipBtn.onclick = () => void runRepair("\u5B89\u88C5 GeoIP", () => repairInstall({ geoip: true }));
1804
2087
  addAccountBtn.onclick = addAccount;
1805
2088
  enterMainBtn.onclick = () => {
1806
2089
  if (typeof ctx2.setActiveTab === "function") {
1807
2090
  ctx2.setActiveTab("config");
1808
2091
  }
1809
2092
  };
1810
- void checkEnvironment();
1811
- void refreshAccounts();
2093
+ void tickEnvironment();
2094
+ void tickAccounts();
2095
+ if (typeof ctx2.api?.onBusEvent === "function") {
2096
+ busUnsubscribe = ctx2.api.onBusEvent((evt) => {
2097
+ const type = String(evt?.type || evt?.event || "").trim().toLowerCase();
2098
+ if (!type) return;
2099
+ if (type.startsWith("account:")) {
2100
+ void tickAccounts();
2101
+ }
2102
+ if (type.startsWith("env:")) {
2103
+ void tickEnvironment();
2104
+ }
2105
+ });
2106
+ }
2107
+ renderRepairHistory();
2108
+ return () => {
2109
+ for (const timer of autoSyncTimers.values()) clearInterval(timer);
2110
+ autoSyncTimers.clear();
2111
+ if (typeof busUnsubscribe === "function") busUnsubscribe();
2112
+ };
2113
+ }
2114
+
2115
+ // src/renderer/tabs-new/schedule-task-bridge.mts
2116
+ var PLATFORM_TASKS = {
2117
+ xiaohongshu: [
2118
+ { type: "xhs-unified", label: "\u641C\u7D22\u4EFB\u52A1", icon: "\u{1F4D5}", platform: "xiaohongshu" }
2119
+ ],
2120
+ weibo: [
2121
+ { type: "weibo-timeline", label: "\u4E3B\u9875\u65F6\u95F4\u7EBF", icon: "\u{1F4F0}", platform: "weibo" },
2122
+ { type: "weibo-search", label: "\u641C\u7D22\u4EFB\u52A1", icon: "\u{1F50D}", platform: "weibo" },
2123
+ { type: "weibo-monitor", label: "\u76D1\u63A7\u4E2A\u4EBA\u4E3B\u9875", icon: "\u{1F441}\uFE0F", platform: "weibo" }
2124
+ ],
2125
+ "1688": [
2126
+ { type: "1688-search", label: "\u641C\u7D22\u4EFB\u52A1", icon: "\u{1F6D2}", platform: "1688" }
2127
+ ]
2128
+ };
2129
+ function normalizeScheduleType(value) {
2130
+ const text = String(value || "interval").trim().toLowerCase();
2131
+ if (text === "once" || text === "daily" || text === "weekly") return text;
2132
+ return "interval";
2133
+ }
2134
+ function toLocalDatetimeValue(iso) {
2135
+ const text = String(iso || "").trim();
2136
+ if (!text) return "";
2137
+ const ts = Date.parse(text);
2138
+ if (!Number.isFinite(ts)) return "";
2139
+ const date = new Date(ts);
2140
+ const yyyy = date.getFullYear();
2141
+ const mm = String(date.getMonth() + 1).padStart(2, "0");
2142
+ const dd = String(date.getDate()).padStart(2, "0");
2143
+ const hh = String(date.getHours()).padStart(2, "0");
2144
+ const min = String(date.getMinutes()).padStart(2, "0");
2145
+ return `${yyyy}-${mm}-${dd}T${hh}:${min}`;
2146
+ }
2147
+ function toIsoOrNull(localDateTime) {
2148
+ const text = String(localDateTime || "").trim();
2149
+ if (!text) return null;
2150
+ const ts = Date.parse(text);
2151
+ if (!Number.isFinite(ts)) return null;
2152
+ return new Date(ts).toISOString();
2153
+ }
2154
+ function parseRunHistory(items) {
2155
+ if (!Array.isArray(items)) return [];
2156
+ return items.map((item) => ({
2157
+ timestamp: String(item?.timestamp || "").trim(),
2158
+ status: String(item?.status || "").trim() === "failure" ? "failure" : "success",
2159
+ durationMs: Number.isFinite(Number(item?.durationMs)) ? Math.max(0, Number(item.durationMs)) : 0
2160
+ })).filter((item) => item.timestamp);
2161
+ }
2162
+ function parseTaskRows(payload) {
2163
+ const rows = Array.isArray(payload?.tasks) ? payload.tasks : [];
2164
+ return rows.map((row) => ({
2165
+ id: String(row?.id || "").trim(),
2166
+ seq: Number.isFinite(Number(row?.seq)) ? Math.max(0, Math.floor(Number(row.seq))) : 0,
2167
+ name: String(row?.name || row?.id || "").trim(),
2168
+ enabled: row?.enabled !== false,
2169
+ scheduleType: normalizeScheduleType(row?.scheduleType),
2170
+ intervalMinutes: Number.isFinite(Number(row?.intervalMinutes)) ? Math.max(1, Math.floor(Number(row.intervalMinutes))) : 30,
2171
+ runAt: String(row?.runAt || "").trim() || null,
2172
+ maxRuns: Number.isFinite(Number(row?.maxRuns)) && Number(row.maxRuns) > 0 ? Math.floor(Number(row.maxRuns)) : null,
2173
+ nextRunAt: String(row?.nextRunAt || "").trim() || null,
2174
+ commandType: String(row?.commandType || "xhs-unified").trim() || "xhs-unified",
2175
+ commandArgv: row?.commandArgv && typeof row.commandArgv === "object" ? row.commandArgv : {},
2176
+ createdAt: String(row?.createdAt || "").trim() || null,
2177
+ updatedAt: String(row?.updatedAt || "").trim() || null,
2178
+ lastRunAt: String(row?.lastRunAt || "").trim() || null,
2179
+ lastStatus: String(row?.lastStatus || "").trim() || null,
2180
+ lastError: String(row?.lastError || "").trim() || null,
2181
+ runCount: Number(row?.runCount || 0) || 0,
2182
+ failCount: Number(row?.failCount || 0) || 0,
2183
+ runHistory: parseRunHistory(row?.runHistory)
2184
+ })).filter((row) => row.id);
2185
+ }
2186
+ function getTasksForPlatform(platform) {
2187
+ const p = platform;
2188
+ return PLATFORM_TASKS[p] || [];
2189
+ }
2190
+ function getPlatformForCommandType(commandType) {
2191
+ if (commandType.startsWith("xhs")) return "xiaohongshu";
2192
+ if (commandType.startsWith("weibo")) return "weibo";
2193
+ if (commandType.startsWith("1688")) return "1688";
2194
+ return "xiaohongshu";
1812
2195
  }
1813
2196
 
1814
- // src/renderer/tabs-new/config-panel.mts
1815
- function renderConfigPanel(root, ctx2) {
2197
+ // src/renderer/tabs-new/tasks.mts
2198
+ var DEFAULT_FORM = {
2199
+ name: "",
2200
+ enabled: true,
2201
+ platform: "xiaohongshu",
2202
+ taskType: "xhs-unified",
2203
+ profileId: "",
2204
+ keyword: "",
2205
+ targetCount: 50,
2206
+ env: "debug",
2207
+ userId: "",
2208
+ collectComments: true,
2209
+ collectBody: true,
2210
+ doLikes: false,
2211
+ likeKeywords: "",
2212
+ scheduleType: "interval",
2213
+ intervalMinutes: 30,
2214
+ runAt: null,
2215
+ maxRuns: null
2216
+ };
2217
+ function parseSortableTime(value) {
2218
+ const ts = Date.parse(String(value || ""));
2219
+ return Number.isFinite(ts) ? ts : 0;
2220
+ }
2221
+ function isKeywordRequired(taskType) {
2222
+ return taskType === "xhs-unified" || taskType === "weibo-search" || taskType === "1688-search";
2223
+ }
2224
+ function commandTypeToWeiboTaskType(commandType) {
2225
+ if (commandType === "weibo-search") return "search";
2226
+ if (commandType === "weibo-monitor") return "monitor";
2227
+ return "timeline";
2228
+ }
2229
+ function fallbackTaskName(data) {
2230
+ const keyword = String(data.keyword || "").trim();
2231
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
2232
+ return keyword ? `${data.taskType}-${keyword}` : `${data.taskType}-${stamp}`;
2233
+ }
2234
+ function renderTasksPanel(root, ctx2) {
1816
2235
  root.innerHTML = "";
1817
2236
  const pageIndicator = createEl("div", { className: "page-indicator" }, [
1818
2237
  "\u5F53\u524D: ",
1819
- createEl("span", {}, ["\u914D\u7F6E\u9875"]),
1820
- " \u2192 \u5B8C\u6210\u540E\u8DF3\u8F6C ",
1821
- createEl("span", {}, ["\u770B\u677F\u9875"])
2238
+ createEl("span", {}, ["\u4EFB\u52A1\u7BA1\u7406"]),
2239
+ " \u2192 \u521B\u5EFA\u3001\u7F16\u8F91\u3001\u6267\u884C\u4EFB\u52A1"
1822
2240
  ]);
1823
2241
  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>
2242
+ const quotaBar = createEl("div", { className: "bento-cell", style: "margin-bottom: var(--gap); padding: var(--gap-sm);" });
2243
+ quotaBar.innerHTML = `
2244
+ <div style="display: flex; gap: var(--gap); align-items: center; flex-wrap: wrap;">
2245
+ <span style="font-size: 12px; color: var(--text-secondary);">\u914D\u989D\u72B6\u6001:</span>
2246
+ <span id="quota-search" class="quota-item" style="font-size: 11px;">\u641C\u7D22: -/-</span>
2247
+ <span id="quota-like" class="quota-item" style="font-size: 11px;">\u70B9\u8D5E: -/-</span>
2248
+ <span id="quota-comment" class="quota-item" style="font-size: 11px;">\u8BC4\u8BBA: -/-</span>
2249
+ <button id="quota-refresh-btn" class="secondary" style="padding: 4px 8px; font-size: 11px; height: auto;">\u5237\u65B0</button>
2250
+ </div>
2251
+ `;
2252
+ root.appendChild(quotaBar);
2253
+ const mainGrid = createEl("div", { className: "bento-grid bento-sidebar" });
2254
+ const formCard = createEl("div", { className: "bento-cell" });
2255
+ formCard.innerHTML = `
2256
+ <div id="task-form-title" class="bento-title">\u65B0\u5EFA\u4EFB\u52A1</div>
2257
+ <input type="hidden" id="task-editing-id" />
1828
2258
 
1829
2259
  <div class="row">
1830
2260
  <div>
1831
- <label>\u641C\u7D22\u5173\u952E\u8BCD</label>
1832
- <input id="keyword-input" placeholder="\u8F93\u5165\u5173\u952E\u8BCD" style="width: 200px;" />
2261
+ <label>\u5E73\u53F0</label>
2262
+ <select id="task-platform" style="width: 130px;">
2263
+ <option value="xiaohongshu">\u{1F4D5} \u5C0F\u7EA2\u4E66</option>
2264
+ <option value="weibo">\u{1F4F0} \u5FAE\u535A</option>
2265
+ <option value="1688">\u{1F6D2} 1688</option>
2266
+ </select>
2267
+ </div>
2268
+ <div>
2269
+ <label>\u4EFB\u52A1\u7C7B\u578B</label>
2270
+ <select id="task-type" style="width: 140px;"></select>
2271
+ </div>
2272
+ <div>
2273
+ <label>\u4EFB\u52A1\u540D</label>
2274
+ <input id="task-name" placeholder="\u53EF\u9009\uFF0C\u4FBF\u4E8E\u8BC6\u522B" style="width: 180px;" />
1833
2275
  </div>
1834
2276
  </div>
1835
2277
 
1836
2278
  <div class="row">
1837
2279
  <div>
1838
- <label>\u76EE\u6807\u6570\u91CF</label>
1839
- <input id="target-input" type="number" value="50" min="1" style="width: 100px;" />
2280
+ <label>\u5173\u952E\u8BCD</label>
2281
+ <input id="task-keyword" placeholder="\u641C\u7D22\u5173\u952E\u8BCD" style="width: 180px;" />
2282
+ </div>
2283
+ <div>
2284
+ <label>\u76EE\u6807\u6570</label>
2285
+ <input id="task-target" type="number" min="1" value="50" style="width: 80px;" />
2286
+ </div>
2287
+ <div>
2288
+ <label>Profile</label>
2289
+ <input id="task-profile" placeholder="xiaohongshu-batch-1" style="width: 160px;" />
1840
2290
  </div>
1841
2291
  <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>
2292
+ <label>\u73AF\u5883</label>
2293
+ <select id="task-env" style="width: 80px;">
2294
+ <option value="debug">debug</option>
2295
+ <option value="prod">prod</option>
1846
2296
  </select>
1847
2297
  </div>
1848
2298
  </div>
1849
2299
 
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>
2300
+ <div id="task-user-id-wrap" class="row" style="display:none;">
2301
+ <div>
2302
+ <label>\u5FAE\u535A\u7528\u6237ID (monitor \u5FC5\u586B)</label>
2303
+ <input id="task-user-id" placeholder="\u4F8B\u5982: 1234567890" style="width: 220px;" />
1870
2304
  </div>
1871
2305
  </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
2306
 
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>
2307
+ <div class="row">
2308
+ <label style="display:flex;align-items:center;gap:6px;">
2309
+ <input id="task-comments" type="checkbox" checked />
2310
+ <span style="font-size:12px;">\u8BC4\u8BBA</span>
1882
2311
  </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>
2312
+ <label style="display:flex;align-items:center;gap:6px;">
2313
+ <input id="task-body" type="checkbox" checked />
2314
+ <span style="font-size:12px;">\u6B63\u6587</span>
1886
2315
  </label>
2316
+ <label style="display:flex;align-items:center;gap:6px;">
2317
+ <input id="task-likes" type="checkbox" />
2318
+ <span style="font-size:12px;">\u70B9\u8D5E</span>
2319
+ </label>
2320
+ <input id="task-like-keywords" placeholder="\u70B9\u8D5E\u5173\u952E\u8BCD(\u9017\u53F7\u5206\u9694)" style="flex:1; min-width:120px;" disabled />
1887
2321
  </div>
1888
2322
 
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;" />
2323
+ <div style="margin-top: var(--gap); padding-top: var(--gap-sm); border-top: 1px solid var(--border);">
2324
+ <div style="font-size:12px; color:var(--text-secondary); margin-bottom:var(--gap-sm);">\u8C03\u5EA6\u8BBE\u7F6E\uFF08\u53EF\u9009\uFF09</div>
2325
+ <div class="row">
2326
+ <div>
2327
+ <select id="task-schedule-type" style="width: 100px;">
2328
+ <option value="interval">\u5FAA\u73AF\u95F4\u9694</option>
2329
+ <option value="once">\u4E00\u6B21\u6027</option>
2330
+ <option value="daily">\u6BCF\u5929</option>
2331
+ <option value="weekly">\u6BCF\u5468</option>
2332
+ </select>
2333
+ </div>
2334
+ <div id="task-interval-wrap">
2335
+ <input id="task-interval" type="number" min="1" value="30" style="width: 70px;" />
2336
+ <span style="font-size:11px;color:var(--text-tertiary);">\u5206\u949F</span>
2337
+ </div>
2338
+ <div id="task-runat-wrap" style="display:none;">
2339
+ <input id="task-runat" type="datetime-local" style="width: 160px;" />
2340
+ </div>
2341
+ <div>
2342
+ <input id="task-max-runs" type="number" min="1" placeholder="\u4E0D\u9650" style="width: 70px;" />
2343
+ <span style="font-size:11px;color:var(--text-tertiary);">\u6B21</span>
2344
+ </div>
1893
2345
  </div>
1894
2346
  </div>
1895
2347
 
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>
2348
+ <div class="btn-group" style="margin-top: var(--gap);">
2349
+ <button id="task-save-btn" style="flex:1;">\u4FDD\u5B58\u4EFB\u52A1</button>
2350
+ <button id="task-run-btn" class="primary" style="flex:1;">\u4FDD\u5B58\u5E76\u6267\u884C</button>
2351
+ <button id="task-run-ephemeral-btn" class="secondary" style="flex:1;">\u4EC5\u6267\u884C(\u4E0D\u4FDD\u5B58)</button>
2352
+ <button id="task-reset-btn" class="secondary" style="flex:0.6;">\u91CD\u7F6E</button>
2353
+ </div>
2354
+ `;
2355
+ mainGrid.appendChild(formCard);
2356
+ const statsCard = createEl("div", { className: "bento-cell", style: "max-width: 300px;" });
2357
+ statsCard.innerHTML = `
2358
+ <div class="bento-title">\u5FEB\u901F\u72B6\u6001</div>
2359
+ <div id="quick-stats">
2360
+ <div style="margin-bottom: var(--gap-sm);">
2361
+ <span style="font-size:11px;color:var(--text-tertiary);">\u8FD0\u884C\u4E2D\u4EFB\u52A1</span>
2362
+ <div id="stat-running" style="font-size:18px;font-weight:700;color:var(--accent-success);">0</div>
2363
+ </div>
2364
+ <div style="margin-bottom: var(--gap-sm);">
2365
+ <span style="font-size:11px;color:var(--text-tertiary);">\u7D2F\u8BA1\u6267\u884C</span>
2366
+ <div id="stat-today" style="font-size:18px;font-weight:700;">0</div>
2367
+ </div>
1902
2368
  <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 />
2369
+ <span style="font-size:11px;color:var(--text-tertiary);">\u5DF2\u4FDD\u5B58\u4EFB\u52A1</span>
2370
+ <div id="stat-saved" style="font-size:18px;font-weight:700;">0</div>
1905
2371
  </div>
1906
2372
  </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>
2373
+ <div style="margin-top: var(--gap);">
2374
+ <button id="goto-scheduler-btn" class="secondary" style="width:100%;">\u67E5\u770B\u4EFB\u52A1\u5217\u8868</button>
1920
2375
  </div>
1921
2376
  `;
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>
2377
+ mainGrid.appendChild(statsCard);
2378
+ root.appendChild(mainGrid);
2379
+ const recentCard = createEl("div", { className: "bento-cell", style: "margin-top: var(--gap);" });
2380
+ recentCard.innerHTML = `
2381
+ <div class="bento-title">\u5386\u53F2\u4EFB\u52A1</div>
2382
+ <div class="row" style="margin-bottom: var(--gap-sm);">
2383
+ <select id="task-history-select" style="min-width: 320px;">
2384
+ <option value="">\u9009\u62E9\u5386\u53F2\u4EFB\u52A1...</option>
2385
+ </select>
2386
+ <button id="task-history-edit-btn" class="secondary">\u8F7D\u5165\u7F16\u8F91</button>
2387
+ <button id="task-history-clone-btn" class="secondary">\u8F7D\u5165\u53E6\u5B58</button>
2388
+ <button id="task-history-refresh-btn" class="secondary">\u5237\u65B0</button>
1929
2389
  </div>
2390
+ <div id="recent-tasks-list"></div>
1930
2391
  `;
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() {
2392
+ root.appendChild(recentCard);
2393
+ const formTitle = formCard.querySelector("#task-form-title");
2394
+ const platformSelect = formCard.querySelector("#task-platform");
2395
+ const taskTypeSelect = formCard.querySelector("#task-type");
2396
+ const nameInput = formCard.querySelector("#task-name");
2397
+ const keywordInput = formCard.querySelector("#task-keyword");
2398
+ const targetInput = formCard.querySelector("#task-target");
2399
+ const profileInput = formCard.querySelector("#task-profile");
2400
+ const envSelect = formCard.querySelector("#task-env");
2401
+ const userIdWrap = formCard.querySelector("#task-user-id-wrap");
2402
+ const userIdInput = formCard.querySelector("#task-user-id");
2403
+ const commentsInput = formCard.querySelector("#task-comments");
2404
+ const bodyInput = formCard.querySelector("#task-body");
2405
+ const likesInput = formCard.querySelector("#task-likes");
2406
+ const likeKeywordsInput = formCard.querySelector("#task-like-keywords");
2407
+ const scheduleTypeSelect = formCard.querySelector("#task-schedule-type");
2408
+ const intervalInput = formCard.querySelector("#task-interval");
2409
+ const intervalWrap = formCard.querySelector("#task-interval-wrap");
2410
+ const runAtInput = formCard.querySelector("#task-runat");
2411
+ const runAtWrap = formCard.querySelector("#task-runat-wrap");
2412
+ const maxRunsInput = formCard.querySelector("#task-max-runs");
2413
+ const editingIdInput = formCard.querySelector("#task-editing-id");
2414
+ const saveBtn = formCard.querySelector("#task-save-btn");
2415
+ const runBtn = formCard.querySelector("#task-run-btn");
2416
+ const runEphemeralBtn = formCard.querySelector("#task-run-ephemeral-btn");
2417
+ const resetBtn = formCard.querySelector("#task-reset-btn");
2418
+ const quotaRefreshBtn = quotaBar.querySelector("#quota-refresh-btn");
2419
+ const gotoSchedulerBtn = statsCard.querySelector("#goto-scheduler-btn");
2420
+ const historySelect = recentCard.querySelector("#task-history-select");
2421
+ const historyEditBtn = recentCard.querySelector("#task-history-edit-btn");
2422
+ const historyCloneBtn = recentCard.querySelector("#task-history-clone-btn");
2423
+ const historyRefreshBtn = recentCard.querySelector("#task-history-refresh-btn");
2424
+ const recentTasksList = recentCard.querySelector("#recent-tasks-list");
2425
+ const statRunning = statsCard.querySelector("#stat-running");
2426
+ const statToday = statsCard.querySelector("#stat-today");
2427
+ const statSaved = statsCard.querySelector("#stat-saved");
2428
+ let tasks = [];
2429
+ const activeRunIds = /* @__PURE__ */ new Set();
2430
+ let unsubscribeActiveRuns = null;
2431
+ const joinPath2 = (...parts) => {
2432
+ if (typeof ctx2?.api?.pathJoin === "function") return ctx2.api.pathJoin(...parts);
2433
+ return parts.filter(Boolean).join("/");
2434
+ };
2435
+ const scheduleScript = joinPath2("apps", "webauto", "entry", "schedule.mjs");
2436
+ const quotaScript = joinPath2("apps", "webauto", "entry", "lib", "quota-status.mjs");
2437
+ const xhsScript = joinPath2("apps", "webauto", "entry", "xhs-unified.mjs");
2438
+ const weiboScript = joinPath2("apps", "webauto", "entry", "weibo-unified.mjs");
2439
+ function getTaskById(taskId) {
2440
+ const id = String(taskId || "").trim();
2441
+ if (!id) return null;
2442
+ return tasks.find((row) => row.id === id) || null;
2443
+ }
2444
+ function updateFormTitle(mode) {
2445
+ if (mode === "edit") {
2446
+ formTitle.textContent = "\u7F16\u8F91\u4EFB\u52A1";
2447
+ return;
2448
+ }
2449
+ if (mode === "clone") {
2450
+ formTitle.textContent = "\u53E6\u5B58\u4E3A\u65B0\u4EFB\u52A1";
2451
+ return;
2452
+ }
2453
+ formTitle.textContent = "\u65B0\u5EFA\u4EFB\u52A1";
2454
+ }
2455
+ function updateTaskTypeOptions(preferredType = "") {
2456
+ const platform = platformSelect.value;
2457
+ const options = getTasksForPlatform(platform);
2458
+ taskTypeSelect.innerHTML = options.map((item) => `<option value="${item.type}">${item.icon} ${item.label}</option>`).join("");
2459
+ const target = String(preferredType || "").trim();
2460
+ const matched = options.find((item) => item.type === target);
2461
+ taskTypeSelect.value = matched?.type || options[0]?.type || "";
2462
+ updatePlatformFields();
2463
+ }
2464
+ function updatePlatformFields() {
2465
+ const taskType = String(taskTypeSelect.value || "").trim();
2466
+ const isWeiboMonitor = taskType === "weibo-monitor";
2467
+ userIdWrap.style.display = isWeiboMonitor ? "" : "none";
2468
+ }
2469
+ function updateScheduleVisibility() {
2470
+ const scheduleType = String(scheduleTypeSelect.value || "interval").trim();
2471
+ intervalWrap.style.display = scheduleType === "interval" ? "inline-flex" : "none";
2472
+ runAtWrap.style.display = scheduleType === "once" || scheduleType === "daily" || scheduleType === "weekly" ? "inline-flex" : "none";
2473
+ }
2474
+ function updateLikeKeywordsState() {
2475
+ likeKeywordsInput.disabled = !likesInput.checked;
2476
+ }
2477
+ function collectFormData() {
2478
+ const maxRunsRaw = String(maxRunsInput.value || "").trim();
2479
+ const maxRunsNum = maxRunsRaw ? Number(maxRunsRaw) : 0;
1952
2480
  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
2481
+ id: String(editingIdInput.value || "").trim() || void 0,
2482
+ name: String(nameInput.value || "").trim(),
2483
+ enabled: true,
2484
+ platform: platformSelect.value,
2485
+ taskType: String(taskTypeSelect.value || "").trim(),
2486
+ profileId: String(profileInput.value || "").trim(),
2487
+ keyword: String(keywordInput.value || "").trim(),
2488
+ targetCount: Math.max(1, Number(targetInput.value || 50) || 50),
2489
+ env: String(envSelect.value || "debug").trim() === "prod" ? "prod" : "debug",
2490
+ userId: String(userIdInput.value || "").trim(),
2491
+ collectComments: commentsInput.checked,
2492
+ collectBody: bodyInput.checked,
2493
+ doLikes: likesInput.checked,
2494
+ likeKeywords: String(likeKeywordsInput.value || "").trim(),
2495
+ scheduleType: scheduleTypeSelect.value,
2496
+ intervalMinutes: Math.max(1, Number(intervalInput.value || 30) || 30),
2497
+ runAt: toIsoOrNull(String(runAtInput.value || "")),
2498
+ maxRuns: Number.isFinite(maxRunsNum) && maxRunsNum > 0 ? Math.max(1, Math.floor(maxRunsNum)) : null
1964
2499
  };
1965
2500
  }
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);
2501
+ function applyTaskToForm(task, mode) {
2502
+ const taskType = String(task.commandType || "xhs-unified").trim() || "xhs-unified";
2503
+ const platform = getPlatformForCommandType(taskType);
2504
+ platformSelect.value = platform;
2505
+ updateTaskTypeOptions(taskType);
2506
+ editingIdInput.value = mode === "edit" ? String(task.id || "") : "";
2507
+ nameInput.value = mode === "clone" ? `${String(task.name || task.id || "").trim()}-copy` : String(task.name || "").trim();
2508
+ keywordInput.value = String(task.commandArgv?.keyword || task.commandArgv?.k || "").trim();
2509
+ targetInput.value = String(task.commandArgv?.["max-notes"] ?? task.commandArgv?.target ?? 50);
2510
+ profileInput.value = String(task.commandArgv?.profile || task.commandArgv?.profileId || "").trim();
2511
+ envSelect.value = String(task.commandArgv?.env || "debug").trim() === "prod" ? "prod" : "debug";
2512
+ userIdInput.value = String(task.commandArgv?.["user-id"] || task.commandArgv?.userId || "").trim();
2513
+ commentsInput.checked = task.commandArgv?.["do-comments"] !== false;
2514
+ bodyInput.checked = task.commandArgv?.["fetch-body"] !== false;
2515
+ likesInput.checked = task.commandArgv?.["do-likes"] === true;
2516
+ likeKeywordsInput.value = String(task.commandArgv?.["like-keywords"] || "").trim();
2517
+ scheduleTypeSelect.value = String(task.scheduleType || "interval");
2518
+ intervalInput.value = String(task.intervalMinutes || 30);
2519
+ runAtInput.value = toLocalDatetimeValue(task.runAt);
2520
+ maxRunsInput.value = task.maxRuns ? String(task.maxRuns) : "";
2521
+ updatePlatformFields();
2522
+ updateScheduleVisibility();
2523
+ updateLikeKeywordsState();
2524
+ updateFormTitle(mode);
2525
+ }
2526
+ function resetForm() {
2527
+ editingIdInput.value = "";
2528
+ nameInput.value = DEFAULT_FORM.name;
2529
+ platformSelect.value = DEFAULT_FORM.platform;
2530
+ updateTaskTypeOptions(DEFAULT_FORM.taskType);
2531
+ keywordInput.value = DEFAULT_FORM.keyword;
2532
+ targetInput.value = String(DEFAULT_FORM.targetCount);
2533
+ profileInput.value = DEFAULT_FORM.profileId;
2534
+ envSelect.value = DEFAULT_FORM.env;
2535
+ userIdInput.value = DEFAULT_FORM.userId;
2536
+ commentsInput.checked = DEFAULT_FORM.collectComments;
2537
+ bodyInput.checked = DEFAULT_FORM.collectBody;
2538
+ likesInput.checked = DEFAULT_FORM.doLikes;
2539
+ likeKeywordsInput.value = DEFAULT_FORM.likeKeywords;
2540
+ scheduleTypeSelect.value = DEFAULT_FORM.scheduleType;
2541
+ intervalInput.value = String(DEFAULT_FORM.intervalMinutes);
2542
+ runAtInput.value = "";
2543
+ maxRunsInput.value = "";
2544
+ updatePlatformFields();
2545
+ updateScheduleVisibility();
2546
+ updateLikeKeywordsState();
2547
+ updateFormTitle("new");
2548
+ }
2549
+ function sortedTasksByRecent() {
2550
+ return [...tasks].sort((a, b) => {
2551
+ const byUpdated = parseSortableTime(b.updatedAt) - parseSortableTime(a.updatedAt);
2552
+ if (byUpdated !== 0) return byUpdated;
2553
+ const byCreated = parseSortableTime(b.createdAt) - parseSortableTime(a.createdAt);
2554
+ if (byCreated !== 0) return byCreated;
2555
+ return (Number(b.seq) || 0) - (Number(a.seq) || 0);
2556
+ });
2557
+ }
2558
+ function renderHistorySelect() {
2559
+ const previous = String(historySelect.value || "").trim();
2560
+ const rows = sortedTasksByRecent();
2561
+ historySelect.innerHTML = '<option value="">\u9009\u62E9\u5386\u53F2\u4EFB\u52A1...</option>';
2562
+ for (const row of rows) {
2563
+ const label = `${row.name || row.id} (${row.id})`;
2564
+ const option = document.createElement("option");
2565
+ option.value = row.id;
2566
+ option.textContent = label;
2567
+ historySelect.appendChild(option);
2568
+ }
2569
+ if (previous && rows.some((row) => row.id === previous)) {
2570
+ historySelect.value = previous;
1985
2571
  }
1986
2572
  }
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);
2573
+ function renderRecentTasks() {
2574
+ const rows = sortedTasksByRecent().slice(0, 8);
2575
+ if (rows.length === 0) {
2576
+ recentTasksList.innerHTML = '<div class="muted" style="font-size:12px;">\u6682\u65E0\u4EFB\u52A1</div>';
2577
+ return;
2578
+ }
2579
+ recentTasksList.innerHTML = rows.map((task) => `
2580
+ <div class="task-row" style="display:flex;gap:var(--gap-sm);padding:var(--gap-xs)0;border-bottom:1px solid var(--border-subtle);align-items:center;">
2581
+ <span style="flex:1;font-size:12px;">${task.name || task.id}</span>
2582
+ <span style="font-size:11px;color:var(--text-tertiary);">${task.commandType}</span>
2583
+ <span style="font-size:11px;color:${task.enabled ? "var(--accent-success)" : "var(--text-muted)"};">${task.enabled ? "\u542F\u7528" : "\u7981\u7528"}</span>
2584
+ <button class="secondary edit-task-btn" data-id="${task.id}" style="padding:2px 6px;font-size:10px;height:auto;">\u7F16\u8F91</button>
2585
+ </div>
2586
+ `).join("");
2587
+ recentTasksList.querySelectorAll(".edit-task-btn").forEach((btn) => {
2588
+ btn.addEventListener("click", () => {
2589
+ const taskId = btn.dataset.id || "";
2590
+ const task = getTaskById(taskId);
2591
+ if (!task) return;
2592
+ historySelect.value = task.id;
2593
+ applyTaskToForm(task, "edit");
2594
+ });
2595
+ });
1997
2596
  }
1998
- async function loadAccounts() {
2597
+ function updateStats() {
2598
+ statSaved.textContent = String(tasks.length);
2599
+ statRunning.textContent = String(activeRunIds.size);
2600
+ const totalRunCount = tasks.reduce((sum, row) => sum + (Number(row.runCount) || 0), 0);
2601
+ statToday.textContent = String(totalRunCount);
2602
+ }
2603
+ async function runJsonScript(scriptPath, args, timeoutMs = 6e4) {
2604
+ const ret = await ctx2.api.cmdRunJson({
2605
+ title: `task-panel ${args.join(" ")}`.trim(),
2606
+ cwd: "",
2607
+ args: [scriptPath, ...args, "--json"],
2608
+ timeoutMs
2609
+ });
2610
+ if (!ret?.ok) {
2611
+ const reason = String(ret?.error || ret?.stderr || ret?.stdout || "unknown_error").trim();
2612
+ throw new Error(reason || "command failed");
2613
+ }
2614
+ return ret.json || {};
2615
+ }
2616
+ async function runScheduleJson(args, timeoutMs = 6e4) {
2617
+ return runJsonScript(scheduleScript, args, timeoutMs);
2618
+ }
2619
+ async function loadQuotaStatus() {
1999
2620
  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);
2621
+ const ret = await ctx2.api.cmdRunJson({
2622
+ title: "quota status",
2623
+ cwd: "",
2624
+ args: [quotaScript],
2625
+ timeoutMs: 3e4
2008
2626
  });
2009
- if (preferredProfileId && validRows.some((row) => row.profileId === preferredProfileId)) {
2010
- accountSelect.value = preferredProfileId;
2627
+ if (!ret?.ok) return;
2628
+ const payload = ret?.json || {};
2629
+ const quotas = Array.isArray(payload?.quotas) ? payload.quotas : [];
2630
+ for (const quota of quotas) {
2631
+ const type = String(quota?.type || "").trim();
2632
+ if (!type) continue;
2633
+ const count = Number(quota?.count || 0);
2634
+ const max = Number(quota?.max || 0);
2635
+ const el = quotaBar.querySelector(`#quota-${type}`);
2636
+ if (!el) continue;
2637
+ el.textContent = `${type}: ${count}/${max || "-"}`;
2638
+ el.style.color = max > 0 && count >= max ? "var(--accent-danger)" : "";
2011
2639
  }
2012
2640
  } catch (err) {
2013
- console.error("Failed to load accounts:", err);
2641
+ console.error("load quota failed:", err);
2014
2642
  }
2015
2643
  }
2016
- async function exportConfig() {
2644
+ async function loadTasks() {
2017
2645
  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
- }
2646
+ const out = await runScheduleJson(["list"]);
2647
+ tasks = parseTaskRows(out);
2648
+ renderHistorySelect();
2649
+ renderRecentTasks();
2650
+ updateStats();
2027
2651
  } catch (err) {
2028
- alert("\u5BFC\u51FA\u5931\u8D25: " + (err?.message || String(err)));
2652
+ console.error("load tasks failed:", err);
2029
2653
  }
2030
2654
  }
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
- }
2655
+ function buildCommandArgv(data) {
2656
+ const argv = {
2657
+ profile: data.profileId,
2658
+ keyword: data.keyword,
2659
+ "max-notes": data.targetCount,
2660
+ target: data.targetCount,
2661
+ env: data.env,
2662
+ "do-comments": data.collectComments,
2663
+ "fetch-body": data.collectBody,
2664
+ "do-likes": data.doLikes,
2665
+ "like-keywords": data.likeKeywords
2057
2666
  };
2058
- input.click();
2667
+ if (String(data.taskType || "").startsWith("weibo-")) {
2668
+ argv["task-type"] = commandTypeToWeiboTaskType(data.taskType);
2669
+ if (data.userId) argv["user-id"] = data.userId;
2670
+ }
2671
+ return argv;
2059
2672
  }
2060
- function updateLikeKeywordsState() {
2061
- likeKeywordsInput.disabled = !autoLikeCb.checked;
2062
- likeKeywordsInput.style.opacity = autoLikeCb.checked ? "1" : "0.5";
2673
+ function validateBeforeSave(data) {
2674
+ if (!data.profileId) return "\u8BF7\u8F93\u5165 Profile ID";
2675
+ if (isKeywordRequired(data.taskType) && !data.keyword) return "\u8BF7\u8F93\u5165\u5173\u952E\u8BCD";
2676
+ if (data.taskType === "weibo-monitor" && !data.userId) return "\u5FAE\u535A monitor \u4EFB\u52A1\u9700\u8981 user-id";
2677
+ if (data.scheduleType !== "interval" && !data.runAt) return `${data.scheduleType} \u4EFB\u52A1\u9700\u8981\u6267\u884C\u65F6\u95F4`;
2678
+ return null;
2063
2679
  }
2064
- async function startCrawl() {
2065
- const keyword = keywordInput.value.trim();
2066
- if (!keyword) {
2067
- alert("\u8BF7\u8F93\u5165\u5173\u952E\u8BCD");
2680
+ function buildSaveArgs(data) {
2681
+ const args = data.id ? ["update", data.id] : ["add"];
2682
+ args.push("--name", data.name || fallbackTaskName(data));
2683
+ args.push("--enabled", String(data.enabled));
2684
+ args.push("--command-type", data.taskType || "xhs-unified");
2685
+ args.push("--schedule-type", data.scheduleType);
2686
+ if (data.scheduleType === "interval") {
2687
+ args.push("--interval-minutes", String(data.intervalMinutes));
2688
+ } else {
2689
+ args.push("--run-at", String(data.runAt || ""));
2690
+ }
2691
+ args.push("--max-runs", data.maxRuns === null ? "0" : String(data.maxRuns));
2692
+ args.push("--argv-json", JSON.stringify(buildCommandArgv(data)));
2693
+ return args;
2694
+ }
2695
+ async function runSavedTask(taskId, data) {
2696
+ const out = await runScheduleJson(["run", taskId], 0);
2697
+ const runId = String(
2698
+ out?.result?.runResult?.lastRunId || out?.result?.runResult?.runId || out?.runResult?.runId || ""
2699
+ ).trim();
2700
+ if (typeof ctx2.setStatus === "function") {
2701
+ ctx2.setStatus(`running: ${taskId}`);
2702
+ }
2703
+ if (data.taskType === "xhs-unified" && ctx2 && typeof ctx2 === "object") {
2704
+ ctx2.xhsCurrentRun = {
2705
+ runId: runId || null,
2706
+ taskId,
2707
+ profileId: data.profileId,
2708
+ keyword: data.keyword,
2709
+ target: data.targetCount,
2710
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
2711
+ };
2712
+ }
2713
+ if (typeof ctx2.setActiveTab === "function") {
2714
+ ctx2.setActiveTab(data.taskType === "xhs-unified" ? "dashboard" : "scheduler");
2715
+ }
2716
+ }
2717
+ async function saveTask(runImmediately = false) {
2718
+ const data = collectFormData();
2719
+ const invalidReason = validateBeforeSave(data);
2720
+ if (invalidReason) {
2721
+ alert(invalidReason);
2722
+ return;
2723
+ }
2724
+ saveBtn.disabled = true;
2725
+ runBtn.disabled = true;
2726
+ runEphemeralBtn.disabled = true;
2727
+ try {
2728
+ const out = await runScheduleJson(buildSaveArgs(data));
2729
+ const taskId = String(out?.task?.id || data.id || "").trim();
2730
+ if (!taskId) {
2731
+ throw new Error("task id missing after save");
2732
+ }
2733
+ editingIdInput.value = taskId;
2734
+ updateFormTitle("edit");
2735
+ await loadTasks();
2736
+ historySelect.value = taskId;
2737
+ if (runImmediately) {
2738
+ await runSavedTask(taskId, data);
2739
+ } else {
2740
+ alert("\u4EFB\u52A1\u5DF2\u4FDD\u5B58");
2741
+ }
2742
+ } catch (err) {
2743
+ alert(`\u4FDD\u5B58\u5931\u8D25: ${err?.message || String(err)}`);
2744
+ } finally {
2745
+ saveBtn.disabled = false;
2746
+ runBtn.disabled = false;
2747
+ runEphemeralBtn.disabled = false;
2748
+ }
2749
+ }
2750
+ function buildEphemeralRunSpec(data) {
2751
+ if (!data.profileId) return null;
2752
+ if (data.taskType === "xhs-unified") {
2753
+ if (!data.keyword) return null;
2754
+ return {
2755
+ title: `xhs: ${data.keyword}`,
2756
+ groupKey: "xhs-unified",
2757
+ args: [
2758
+ xhsScript,
2759
+ "--profile",
2760
+ data.profileId,
2761
+ "--keyword",
2762
+ data.keyword,
2763
+ "--target",
2764
+ String(data.targetCount),
2765
+ "--max-notes",
2766
+ String(data.targetCount),
2767
+ "--env",
2768
+ data.env,
2769
+ "--do-comments",
2770
+ String(data.collectComments),
2771
+ "--fetch-body",
2772
+ String(data.collectBody),
2773
+ "--do-likes",
2774
+ String(data.doLikes),
2775
+ "--like-keywords",
2776
+ data.likeKeywords
2777
+ ]
2778
+ };
2779
+ }
2780
+ if (data.taskType === "weibo-search") {
2781
+ if (!data.keyword) return null;
2782
+ return {
2783
+ title: `weibo: ${data.keyword}`,
2784
+ groupKey: "weibo-search",
2785
+ args: [
2786
+ weiboScript,
2787
+ "search",
2788
+ "--profile",
2789
+ data.profileId,
2790
+ "--keyword",
2791
+ data.keyword,
2792
+ "--target",
2793
+ String(data.targetCount),
2794
+ "--env",
2795
+ data.env
2796
+ ]
2797
+ };
2798
+ }
2799
+ return null;
2800
+ }
2801
+ async function runWithoutSave() {
2802
+ const data = collectFormData();
2803
+ if (!data.profileId) {
2804
+ alert("\u8BF7\u8F93\u5165 Profile ID");
2068
2805
  return;
2069
2806
  }
2070
- const profileId = accountSelect.value;
2071
- if (!profileId) {
2072
- alert("\u8BF7\u9009\u62E9\u8D26\u6237");
2807
+ if (isKeywordRequired(data.taskType) && !data.keyword) {
2808
+ alert("\u8BF7\u8F93\u5165\u5173\u952E\u8BCD");
2073
2809
  return;
2074
2810
  }
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");
2811
+ if (data.taskType === "weibo-monitor" && !data.userId) {
2812
+ alert("\u5FAE\u535A monitor \u4EFB\u52A1\u9700\u8981 user-id");
2078
2813
  return;
2079
2814
  }
2080
- const config = buildConfigPayload();
2081
- try {
2082
- await ctx2.api.configSaveLast(config);
2083
- } catch {
2815
+ const spec = buildEphemeralRunSpec(data);
2816
+ if (!spec) {
2817
+ alert(`\u5F53\u524D\u4EFB\u52A1\u7C7B\u578B\u6682\u4E0D\u652F\u6301\u4EC5\u6267\u884C(\u4E0D\u4FDD\u5B58): ${data.taskType}`);
2818
+ return;
2084
2819
  }
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...";
2820
+ runEphemeralBtn.disabled = true;
2114
2821
  try {
2115
2822
  const ret = await ctx2.api.cmdSpawn({
2116
- title: `xhs unified ${config.keyword}`.trim(),
2823
+ title: spec.title,
2117
2824
  cwd: "",
2118
- args,
2119
- groupKey: "xiaohongshu"
2825
+ args: spec.args,
2826
+ groupKey: spec.groupKey
2120
2827
  });
2121
2828
  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}`);
2829
+ if (runId) {
2830
+ activeRunIds.add(runId);
2831
+ updateStats();
2134
2832
  }
2135
2833
  if (typeof ctx2.setStatus === "function") {
2136
- ctx2.setStatus(`running: xhs-unified ${config.keyword}`);
2834
+ ctx2.setStatus(`started: ${spec.title}`);
2137
2835
  }
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)}`);
2836
+ if (data.taskType === "xhs-unified" && ctx2 && typeof ctx2 === "object") {
2837
+ ctx2.xhsCurrentRun = {
2838
+ runId: runId || null,
2839
+ taskId: null,
2840
+ profileId: data.profileId,
2841
+ keyword: data.keyword,
2842
+ target: data.targetCount,
2843
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
2844
+ };
2142
2845
  }
2143
- return;
2846
+ if (typeof ctx2.setActiveTab === "function") {
2847
+ ctx2.setActiveTab(data.taskType === "xhs-unified" ? "dashboard" : "scheduler");
2848
+ }
2849
+ } catch (err) {
2850
+ alert(`\u6267\u884C\u5931\u8D25: ${err?.message || String(err)}`);
2144
2851
  } finally {
2145
- startBtn.disabled = false;
2146
- startBtn.textContent = prevText || "\u5F00\u59CB\u722C\u53D6";
2147
- }
2148
- if (typeof ctx2.setActiveTab === "function") {
2149
- ctx2.setActiveTab("dashboard");
2852
+ runEphemeralBtn.disabled = false;
2150
2853
  }
2151
2854
  }
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;
2855
+ function selectedHistoryTask() {
2856
+ const taskId = String(historySelect.value || "").trim();
2857
+ if (!taskId) return null;
2858
+ return getTaskById(taskId);
2859
+ }
2860
+ async function loadLastProfile() {
2861
+ try {
2862
+ const config = await ctx2.api.configLoadLast();
2863
+ if (!profileInput.value && config?.lastProfileId) {
2864
+ profileInput.value = String(config.lastProfileId || "");
2865
+ }
2866
+ if (!keywordInput.value && config?.keyword) {
2867
+ keywordInput.value = String(config.keyword || "");
2868
+ }
2869
+ } catch {
2172
2870
  }
2871
+ }
2872
+ platformSelect.addEventListener("change", () => {
2873
+ updateTaskTypeOptions();
2173
2874
  });
2174
- void loadConfig();
2175
- void loadAccounts();
2176
- }
2177
-
2178
- // src/renderer/tabs-new/dashboard.mts
2179
- function renderDashboard(root, ctx2) {
2875
+ taskTypeSelect.addEventListener("change", () => updatePlatformFields());
2876
+ scheduleTypeSelect.addEventListener("change", () => updateScheduleVisibility());
2877
+ likesInput.addEventListener("change", () => updateLikeKeywordsState());
2878
+ saveBtn.addEventListener("click", () => {
2879
+ void saveTask(false);
2880
+ });
2881
+ runBtn.addEventListener("click", () => {
2882
+ void saveTask(true);
2883
+ });
2884
+ runEphemeralBtn.addEventListener("click", () => {
2885
+ void runWithoutSave();
2886
+ });
2887
+ resetBtn.addEventListener("click", resetForm);
2888
+ quotaRefreshBtn.addEventListener("click", () => {
2889
+ void loadQuotaStatus();
2890
+ });
2891
+ historyRefreshBtn.addEventListener("click", () => {
2892
+ void loadTasks();
2893
+ });
2894
+ historyEditBtn.addEventListener("click", () => {
2895
+ const task = selectedHistoryTask();
2896
+ if (!task) {
2897
+ alert("\u8BF7\u5148\u9009\u62E9\u5386\u53F2\u4EFB\u52A1");
2898
+ return;
2899
+ }
2900
+ applyTaskToForm(task, "edit");
2901
+ });
2902
+ historyCloneBtn.addEventListener("click", () => {
2903
+ const task = selectedHistoryTask();
2904
+ if (!task) {
2905
+ alert("\u8BF7\u5148\u9009\u62E9\u5386\u53F2\u4EFB\u52A1");
2906
+ return;
2907
+ }
2908
+ applyTaskToForm(task, "clone");
2909
+ });
2910
+ gotoSchedulerBtn.addEventListener("click", () => {
2911
+ if (typeof ctx2.setActiveTab === "function") {
2912
+ ctx2.setActiveTab("scheduler");
2913
+ }
2914
+ });
2915
+ if (typeof ctx2.api?.onCmdEvent === "function") {
2916
+ unsubscribeActiveRuns = ctx2.api.onCmdEvent((evt) => {
2917
+ const runId = String(evt?.runId || "").trim();
2918
+ if (!runId) return;
2919
+ if (evt?.type === "started") {
2920
+ activeRunIds.add(runId);
2921
+ updateStats();
2922
+ return;
2923
+ }
2924
+ if (evt?.type === "exit") {
2925
+ activeRunIds.delete(runId);
2926
+ updateStats();
2927
+ }
2928
+ });
2929
+ }
2930
+ resetForm();
2931
+ updateTaskTypeOptions(DEFAULT_FORM.taskType);
2932
+ updateScheduleVisibility();
2933
+ updateLikeKeywordsState();
2934
+ void loadQuotaStatus();
2935
+ void loadTasks();
2936
+ void loadLastProfile();
2937
+ return () => {
2938
+ if (unsubscribeActiveRuns) {
2939
+ try {
2940
+ unsubscribeActiveRuns();
2941
+ } catch {
2942
+ }
2943
+ unsubscribeActiveRuns = null;
2944
+ }
2945
+ };
2946
+ }
2947
+
2948
+ // src/renderer/tabs-new/dashboard.mts
2949
+ function renderDashboard(root, ctx2) {
2180
2950
  root.innerHTML = "";
2181
2951
  const pageIndicator = createEl("div", { className: "page-indicator" }, [
2182
2952
  "\u5F53\u524D: ",
@@ -2245,6 +3015,10 @@ function renderDashboard(root, ctx2) {
2245
3015
  <label>\u4F7F\u7528\u8D26\u6237</label>
2246
3016
  <div id="task-account" style="font-weight: 600; color: var(--text-1);">-</div>
2247
3017
  </div>
3018
+ <div>
3019
+ <label>\u914D\u7F6EID</label>
3020
+ <div id="task-config-id" style="font-weight: 600; color: var(--text-1);">-</div>
3021
+ </div>
2248
3022
  </div>
2249
3023
 
2250
3024
  <div class="phase-indicator" style="margin-bottom: var(--gap);">
@@ -2282,12 +3056,13 @@ function renderDashboard(root, ctx2) {
2282
3056
  \u5B9E\u65F6\u65E5\u5FD7
2283
3057
  <button id="toggle-logs-btn" class="secondary" style="margin-left: auto; padding: 4px 10px; font-size: 11px;">\u5C55\u5F00</button>
2284
3058
  </div>
2285
- <div id="logs-container" class="log-container" style="display: none; max-height: 300px;"></div>
3059
+ <div id="logs-container" class="log-container" style="display: none;"></div>
2286
3060
 
2287
3061
  <div style="margin-top: var(--gap);">
2288
3062
  <div class="btn-group">
2289
3063
  <button id="pause-btn" class="secondary" style="flex: 1;">\u6682\u505C</button>
2290
3064
  <button id="stop-btn" class="danger" style="flex: 1;">\u505C\u6B62</button>
3065
+ <button id="back-config-btn" class="secondary" style="flex: 1;">\u8FD4\u56DE\u914D\u7F6E</button>
2291
3066
  </div>
2292
3067
  </div>
2293
3068
  `;
@@ -2300,6 +3075,7 @@ function renderDashboard(root, ctx2) {
2300
3075
  const taskKeyword = root.querySelector("#task-keyword");
2301
3076
  const taskTarget = root.querySelector("#task-target");
2302
3077
  const taskAccount = root.querySelector("#task-account");
3078
+ const taskConfigId = root.querySelector("#task-config-id");
2303
3079
  const currentPhase = root.querySelector("#current-phase");
2304
3080
  const currentAction = root.querySelector("#current-action");
2305
3081
  const progressPercent = root.querySelector("#progress-percent");
@@ -2316,17 +3092,33 @@ function renderDashboard(root, ctx2) {
2316
3092
  const toggleLogsBtn = root.querySelector("#toggle-logs-btn");
2317
3093
  const pauseBtn = root.querySelector("#pause-btn");
2318
3094
  const stopBtn = root.querySelector("#stop-btn");
3095
+ const backConfigBtn = root.querySelector("#back-config-btn");
2319
3096
  let logsExpanded = false;
2320
3097
  let paused = false;
3098
+ let unsubscribeBus = null;
3099
+ let commentsCount = 0;
3100
+ let likesCount = 0;
3101
+ let likesSkippedCount = 0;
3102
+ let likesAlreadyCount = 0;
3103
+ let likesDedupCount = 0;
2321
3104
  let startTime = Date.now();
3105
+ let stoppedAt = null;
2322
3106
  let elapsedTimer = null;
2323
3107
  let unsubscribeState = null;
2324
3108
  let unsubscribeCmd = null;
2325
3109
  let activeRunId = String(ctx2?.xhsCurrentRun?.runId || "").trim();
3110
+ let activeStatus = "";
2326
3111
  let errorCountTotal = 0;
2327
3112
  const recentErrors = [];
2328
3113
  const maxLogs = 500;
2329
3114
  const maxRecentErrors = 8;
3115
+ const initialTaskId = String(ctx2?.xhsCurrentRun?.taskId || ctx2?.activeTaskConfigId || "").trim();
3116
+ if (initialTaskId) {
3117
+ taskConfigId.textContent = initialTaskId;
3118
+ }
3119
+ const normalizeStatus = (value) => String(value || "").trim().toLowerCase();
3120
+ const isRunningStatus = (value) => ["running", "queued", "pending", "starting"].includes(normalizeStatus(value));
3121
+ const isTerminalStatus = (value) => ["completed", "done", "success", "succeeded", "failed", "error", "stopped", "canceled"].includes(normalizeStatus(value));
2330
3122
  function renderRunSummary() {
2331
3123
  runIdText.textContent = activeRunId || "-";
2332
3124
  errorCountText.textContent = String(errorCountTotal);
@@ -2357,12 +3149,22 @@ function renderDashboard(root, ctx2) {
2357
3149
  renderRunSummary();
2358
3150
  }
2359
3151
  function updateElapsed() {
2360
- const elapsed = Math.floor((Date.now() - startTime) / 1e3);
3152
+ const base = stoppedAt ?? Date.now();
3153
+ const elapsed = Math.max(0, Math.floor((base - startTime) / 1e3));
2361
3154
  const h = Math.floor(elapsed / 3600);
2362
3155
  const m = Math.floor(elapsed % 3600 / 60);
2363
3156
  const s = elapsed % 60;
2364
3157
  statElapsed.textContent = `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
2365
3158
  }
3159
+ function startElapsedTimer() {
3160
+ if (elapsedTimer) return;
3161
+ elapsedTimer = setInterval(updateElapsed, 1e3);
3162
+ }
3163
+ function stopElapsedTimer() {
3164
+ if (!elapsedTimer) return;
3165
+ clearInterval(elapsedTimer);
3166
+ elapsedTimer = null;
3167
+ }
2366
3168
  function addLog(line, type = "info") {
2367
3169
  const ts = (/* @__PURE__ */ new Date()).toLocaleTimeString("zh-CN", { hour12: false });
2368
3170
  const logLine = createEl("div", { className: "log-line" });
@@ -2377,29 +3179,45 @@ function renderDashboard(root, ctx2) {
2377
3179
  }
2378
3180
  function updateFromTaskState(state) {
2379
3181
  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;
3182
+ const progressObj = state.progress && typeof state.progress === "object" ? state.progress : null;
3183
+ const processedRaw = progressObj?.processed ?? progressObj?.current ?? state.progress ?? state.collected ?? state.current ?? 0;
3184
+ const totalRaw = progressObj?.total ?? state.total ?? state.target ?? state.maxNotes ?? 0;
3185
+ const failedRaw = progressObj?.failed ?? state.failed ?? state.errors ?? 0;
3186
+ const collected = Number(processedRaw) || 0;
3187
+ const target = Number(totalRaw) || 0;
3188
+ const success = Number(state.success ?? collected) || 0;
3189
+ const failed = Number(failedRaw) || 0;
2384
3190
  const remaining = Math.max(0, target - collected);
2385
3191
  statCollected.textContent = String(collected);
2386
3192
  statSuccess.textContent = String(success);
2387
3193
  statFailed.textContent = String(failed);
2388
3194
  statRemaining.textContent = String(remaining);
2389
- const percent = target > 0 ? Math.round(collected / target * 100) : 0;
3195
+ let percent = 0;
3196
+ if (target > 0) {
3197
+ percent = Math.round(collected / target * 100);
3198
+ } else if (progressObj && Number.isFinite(Number(progressObj.percent))) {
3199
+ const pct = Number(progressObj.percent);
3200
+ percent = pct <= 1 ? Math.round(pct * 100) : Math.round(pct);
3201
+ }
2390
3202
  progressPercent.textContent = `${percent}%`;
2391
3203
  progressBar.style.width = `${percent}%`;
2392
3204
  if (state.phase) {
2393
3205
  currentPhase.textContent = state.phase;
2394
3206
  }
2395
- if (state.action) {
2396
- currentAction.textContent = state.action;
3207
+ const action = String(state.action || state.message || state.step || "").trim();
3208
+ if (action) {
3209
+ currentAction.textContent = action;
2397
3210
  }
2398
- if (state.comments) {
2399
- statComments.textContent = `${state.comments}\u6761`;
3211
+ const stats = state.stats && typeof state.stats === "object" ? state.stats : null;
3212
+ const comments = Number(stats?.commentsCollected ?? state.comments);
3213
+ if (Number.isFinite(comments)) {
3214
+ commentsCount = Math.max(0, Math.floor(comments));
3215
+ statComments.textContent = `${commentsCount}\u6761`;
2400
3216
  }
2401
- if (state.likes) {
2402
- statLikes.textContent = `${state.likes}\u6B21`;
3217
+ const likes = Number(stats?.likesPerformed ?? state.likes);
3218
+ if (Number.isFinite(likes)) {
3219
+ likesCount = Math.max(0, Math.floor(likes));
3220
+ statLikes.textContent = `${likesCount}\u6B21 (\u8DF3\u8FC7:${likesSkippedCount}, \u5DF2\u8D5E:${likesAlreadyCount}, \u53BB\u91CD:${likesDedupCount})`;
2403
3221
  }
2404
3222
  if (state.ratelimits) {
2405
3223
  statRatelimit.textContent = `${state.ratelimits}\u6B21`;
@@ -2414,22 +3232,68 @@ function renderDashboard(root, ctx2) {
2414
3232
  const aliases = ctx2.api?.settings?.profileAliases || {};
2415
3233
  taskAccount.textContent = aliases[state.profileId] || state.profileId;
2416
3234
  }
3235
+ const taskId = String(state.taskId || state.scheduleTaskId || state.configTaskId || "").trim();
3236
+ if (taskId) {
3237
+ taskConfigId.textContent = taskId;
3238
+ if (ctx2 && typeof ctx2 === "object") {
3239
+ ctx2.activeTaskConfigId = taskId;
3240
+ }
3241
+ }
2417
3242
  if (state.runId) {
2418
3243
  activeRunId = String(state.runId);
2419
3244
  renderRunSummary();
2420
3245
  }
3246
+ if (state.startedAt) {
3247
+ const ts = Number(state.startedAt) || Date.parse(String(state.startedAt));
3248
+ if (Number.isFinite(ts) && ts > 0) {
3249
+ startTime = ts;
3250
+ if (!stoppedAt) {
3251
+ updateElapsed();
3252
+ startElapsedTimer();
3253
+ }
3254
+ }
3255
+ }
3256
+ const status = normalizeStatus(state.status);
3257
+ if (status) {
3258
+ activeStatus = status;
3259
+ }
3260
+ if (status === "completed" || status === "done" || status === "success" || status === "succeeded") {
3261
+ if (!stoppedAt) {
3262
+ stoppedAt = Date.now();
3263
+ updateElapsed();
3264
+ stopElapsedTimer();
3265
+ }
3266
+ }
3267
+ if (status === "failed" || status === "error") {
3268
+ if (!stoppedAt) {
3269
+ stoppedAt = Date.now();
3270
+ updateElapsed();
3271
+ stopElapsedTimer();
3272
+ }
3273
+ }
2421
3274
  if (state.error) {
2422
3275
  pushRecentError(String(state.error), "state");
2423
3276
  }
2424
3277
  }
2425
3278
  function pickTaskFromList(tasks) {
2426
3279
  const target = activeRunId;
3280
+ const running = tasks.find((item) => isRunningStatus(item?.status));
3281
+ const sorted = [...tasks].sort((a, b) => {
3282
+ const aTs = Number(a?.updatedAt ?? a?.completedAt ?? a?.startedAt ?? 0) || 0;
3283
+ const bTs = Number(b?.updatedAt ?? b?.completedAt ?? b?.startedAt ?? 0) || 0;
3284
+ return bTs - aTs;
3285
+ });
3286
+ const latest = sorted[0] || null;
2427
3287
  if (target) {
2428
3288
  const matched = tasks.find((item) => String(item?.runId || "").trim() === target);
2429
- if (matched) return matched;
3289
+ if (matched) {
3290
+ if (isRunningStatus(matched?.status)) return matched;
3291
+ if (running) return running;
3292
+ if (latest && String(latest?.runId || "").trim() !== target) return latest;
3293
+ return matched;
3294
+ }
2430
3295
  }
2431
- const running = tasks.find((item) => ["running", "queued", "pending", "starting"].includes(String(item?.status || "").toLowerCase()));
2432
- return running || tasks[0] || null;
3296
+ return running || latest || null;
2433
3297
  }
2434
3298
  function updateFromEventPayload(payload) {
2435
3299
  const event = String(payload?.event || "").trim();
@@ -2437,11 +3301,39 @@ function renderDashboard(root, ctx2) {
2437
3301
  if (event === "xhs.unified.start") {
2438
3302
  currentPhase.textContent = "\u8FD0\u884C\u4E2D";
2439
3303
  currentAction.textContent = "\u542F\u52A8 autoscript";
3304
+ activeStatus = "running";
3305
+ statCollected.textContent = "0";
3306
+ statSuccess.textContent = "0";
3307
+ statFailed.textContent = "0";
3308
+ statRemaining.textContent = "0";
3309
+ progressPercent.textContent = "0%";
3310
+ progressBar.style.width = "0%";
3311
+ commentsCount = 0;
3312
+ likesCount = 0;
3313
+ likesSkippedCount = 0;
3314
+ likesAlreadyCount = 0;
3315
+ likesDedupCount = 0;
3316
+ statComments.textContent = `0\u6761`;
3317
+ statLikes.textContent = `0\u6B21 (\u8DF3\u8FC7:0, \u5DF2\u8D5E:0, \u53BB\u91CD:0)`;
3318
+ const ts = Date.parse(String(payload.ts || "")) || Date.now();
3319
+ startTime = ts;
3320
+ stoppedAt = null;
3321
+ updateElapsed();
3322
+ startElapsedTimer();
2440
3323
  if (payload.runId) {
2441
3324
  activeRunId = String(payload.runId || "").trim() || activeRunId;
2442
3325
  }
2443
3326
  if (payload.keyword) taskKeyword.textContent = String(payload.keyword);
2444
3327
  if (payload.maxNotes) taskTarget.textContent = String(payload.maxNotes);
3328
+ if (payload.taskId) {
3329
+ const taskId = String(payload.taskId || "").trim();
3330
+ if (taskId) {
3331
+ taskConfigId.textContent = taskId;
3332
+ if (ctx2 && typeof ctx2 === "object") {
3333
+ ctx2.activeTaskConfigId = taskId;
3334
+ }
3335
+ }
3336
+ }
2445
3337
  renderRunSummary();
2446
3338
  return;
2447
3339
  }
@@ -2449,15 +3341,38 @@ function renderDashboard(root, ctx2) {
2449
3341
  const opId = String(payload.operationId || "").trim();
2450
3342
  currentAction.textContent = opId || currentAction.textContent;
2451
3343
  const result = payload.result && typeof payload.result === "object" ? payload.result : {};
3344
+ const opResult = result && typeof result === "object" && "result" in result ? result.result : result;
3345
+ if (opId === "open_first_detail" || opId === "open_next_detail") {
3346
+ const visited = Number(opResult?.visited || 0);
3347
+ const maxNotes = Number(opResult?.maxNotes || 0);
3348
+ if (visited > 0) {
3349
+ statCollected.textContent = String(visited);
3350
+ statSuccess.textContent = String(visited);
3351
+ if (maxNotes > 0) {
3352
+ const remaining = Math.max(0, maxNotes - visited);
3353
+ statRemaining.textContent = String(remaining);
3354
+ taskTarget.textContent = String(maxNotes);
3355
+ const pct = Math.round(visited / maxNotes * 100);
3356
+ progressPercent.textContent = `${pct}%`;
3357
+ progressBar.style.width = `${pct}%`;
3358
+ }
3359
+ }
3360
+ }
2452
3361
  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`;
3362
+ const added = Number(opResult?.collected || 0);
3363
+ commentsCount = Math.max(0, commentsCount + added);
3364
+ statComments.textContent = `${commentsCount}\u6761`;
2456
3365
  }
2457
3366
  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`;
3367
+ const added = Number(opResult?.likedCount || 0);
3368
+ const skipped = Number(opResult?.skippedCount || 0);
3369
+ const already = Number(opResult?.alreadyLikedSkipped || 0);
3370
+ const dedup = Number(opResult?.dedupSkipped || 0);
3371
+ likesCount = Math.max(0, likesCount + added);
3372
+ likesSkippedCount = Math.max(0, likesSkippedCount + skipped);
3373
+ likesAlreadyCount = Math.max(0, likesAlreadyCount + already);
3374
+ likesDedupCount = Math.max(0, likesDedupCount + dedup);
3375
+ statLikes.textContent = `${likesCount}\u6B21 (\u8DF3\u8FC7:${likesSkippedCount}, \u5DF2\u8D5E:${likesAlreadyCount}, \u53BB\u91CD:${likesDedupCount})`;
2461
3376
  }
2462
3377
  return;
2463
3378
  }
@@ -2479,13 +3394,24 @@ function renderDashboard(root, ctx2) {
2479
3394
  }
2480
3395
  if (event === "xhs.unified.stop") {
2481
3396
  const reason = String(payload.reason || "").trim();
3397
+ const stoppedTs = Date.parse(String(payload.stoppedAt || payload.ts || "")) || Date.now();
3398
+ stoppedAt = stoppedTs;
3399
+ activeStatus = reason ? normalizeStatus(reason) || "stopped" : "stopped";
3400
+ updateElapsed();
3401
+ stopElapsedTimer();
3402
+ const successReasons = /* @__PURE__ */ new Set(["completed", "script_complete"]);
2482
3403
  currentPhase.textContent = reason && reason !== "script_failure" ? "\u5DF2\u7ED3\u675F" : "\u5931\u8D25";
2483
3404
  currentAction.textContent = reason || "stop";
2484
- if (reason && reason !== "completed") {
3405
+ if (reason && !successReasons.has(reason)) {
2485
3406
  pushRecentError(`stop reason=${reason}`, event);
2486
3407
  }
2487
3408
  renderRunSummary();
2488
3409
  }
3410
+ if (event === "autoscript:operation_terminal") {
3411
+ const code = String(payload.code || "").trim();
3412
+ currentAction.textContent = code ? `terminal:${code}` : "terminal";
3413
+ renderRunSummary();
3414
+ }
2489
3415
  }
2490
3416
  function parseLineEvent(line) {
2491
3417
  const text = String(line || "").trim();
@@ -2496,12 +3422,110 @@ function renderDashboard(root, ctx2) {
2496
3422
  } catch {
2497
3423
  }
2498
3424
  }
3425
+ function applySummary(summary) {
3426
+ if (!summary || typeof summary !== "object") return;
3427
+ const totals = summary?.totals && typeof summary.totals === "object" ? summary.totals : {};
3428
+ const profiles = Array.isArray(summary?.profiles) ? summary.profiles : [];
3429
+ const profile = profiles[0] || null;
3430
+ const stats = profile?.stats && typeof profile.stats === "object" ? profile.stats : totals;
3431
+ const assigned = Number(stats?.assignedNotes ?? totals?.assignedNotes ?? summary?.target ?? 0) || 0;
3432
+ const opened = Number(stats?.openedNotes ?? totals?.openedNotes ?? totals?.assignedNotes ?? 0) || 0;
3433
+ const failed = Number(totals?.operationErrors ?? 0) || 0;
3434
+ const remaining = Math.max(0, assigned - opened);
3435
+ statCollected.textContent = String(opened);
3436
+ statSuccess.textContent = String(opened);
3437
+ statFailed.textContent = String(failed);
3438
+ statRemaining.textContent = String(remaining);
3439
+ let percent = 0;
3440
+ if (assigned > 0) {
3441
+ percent = Math.round(opened / assigned * 100);
3442
+ }
3443
+ progressPercent.textContent = `${percent}%`;
3444
+ progressBar.style.width = `${percent}%`;
3445
+ const comments = Number(stats?.commentsCollected ?? totals?.commentsCollected ?? 0);
3446
+ if (Number.isFinite(comments)) {
3447
+ commentsCount = Math.max(0, Math.floor(comments));
3448
+ statComments.textContent = `${commentsCount}\u6761`;
3449
+ }
3450
+ const likesNew = Number(stats?.likesNewCount ?? totals?.likesNewCount ?? 0);
3451
+ const likesSkipped = Number(stats?.likesSkippedCount ?? totals?.likesSkippedCount ?? 0);
3452
+ const likesAlready = Number(stats?.likesAlreadyCount ?? totals?.likesAlreadyCount ?? 0);
3453
+ const likesDedup = Number(stats?.likesDedupCount ?? totals?.likesDedupCount ?? 0);
3454
+ likesCount = Math.max(0, Math.floor(likesNew || 0));
3455
+ likesSkippedCount = Math.max(0, Math.floor(likesSkipped || 0));
3456
+ likesAlreadyCount = Math.max(0, Math.floor(likesAlready || 0));
3457
+ likesDedupCount = Math.max(0, Math.floor(likesDedup || 0));
3458
+ statLikes.textContent = `${likesCount}\u6B21(\u8DF3\u8FC7:${likesSkippedCount}, \u5DF2\u8D5E:${likesAlreadyCount}, \u53BB\u91CD:${likesDedupCount})`;
3459
+ if (summary.keyword) taskKeyword.textContent = String(summary.keyword);
3460
+ if (assigned) taskTarget.textContent = String(assigned);
3461
+ if (profile?.profileId) {
3462
+ const aliases = ctx2.api?.settings?.profileAliases || {};
3463
+ taskAccount.textContent = aliases[profile.profileId] || profile.profileId;
3464
+ }
3465
+ const runId = String(profile?.runId || summary?.runId || "").trim();
3466
+ if (runId) {
3467
+ activeRunId = runId;
3468
+ renderRunSummary();
3469
+ }
3470
+ const reason = String(profile?.reason || summary?.status || "").trim();
3471
+ if (reason) {
3472
+ const okReasons = /* @__PURE__ */ new Set(["script_complete", "completed", "success", "succeeded"]);
3473
+ currentPhase.textContent = okReasons.has(reason) ? "\u5DF2\u7ED3\u675F" : "\u5931\u8D25";
3474
+ currentAction.textContent = reason;
3475
+ activeStatus = normalizeStatus(reason) || activeStatus;
3476
+ }
3477
+ const summaryTs = Date.parse(String(summary?.generatedAt || "")) || Date.now();
3478
+ stoppedAt = summaryTs;
3479
+ updateElapsed();
3480
+ stopElapsedTimer();
3481
+ const errorTotal = Number(totals?.operationErrors ?? 0) + Number(totals?.recoveryFailed ?? 0);
3482
+ if (Number.isFinite(errorTotal)) {
3483
+ errorCountTotal = Math.max(0, Math.floor(errorTotal));
3484
+ renderRunSummary();
3485
+ }
3486
+ }
3487
+ async function loadLatestSummary() {
3488
+ if (typeof ctx2.api?.resultsScan !== "function") return null;
3489
+ if (typeof ctx2.api?.fsListDir !== "function") return null;
3490
+ if (typeof ctx2.api?.fsReadTextPreview !== "function") return null;
3491
+ const res = await ctx2.api.resultsScan({ downloadRoot: ctx2.settings?.downloadRoot });
3492
+ if (!res?.ok || !Array.isArray(res?.entries) || res.entries.length === 0) return null;
3493
+ const keyword = String(taskKeyword.textContent || "").trim();
3494
+ const matched = keyword ? res.entries.find((e) => e?.keyword === keyword) : null;
3495
+ const entry = matched || res.entries[0];
3496
+ if (!entry?.path) return null;
3497
+ const mergedRoot = ctx2.api.pathJoin(entry.path, "merged");
3498
+ const list = await ctx2.api.fsListDir({ root: mergedRoot, recursive: true, maxEntries: 3e3 });
3499
+ if (!list?.ok || !Array.isArray(list?.entries)) return null;
3500
+ const summaries = list.entries.filter((e) => !e?.isDir && e?.name === "summary.json");
3501
+ if (summaries.length === 0) return null;
3502
+ summaries.sort((a, b) => (b?.mtimeMs || 0) - (a?.mtimeMs || 0));
3503
+ const summaryPath = summaries[0].path;
3504
+ if (!summaryPath) return null;
3505
+ const textRes = await ctx2.api.fsReadTextPreview({ path: summaryPath, maxBytes: 1e6, maxLines: 2e4 });
3506
+ if (!textRes?.ok || !textRes?.text) return null;
3507
+ try {
3508
+ return JSON.parse(textRes.text);
3509
+ } catch {
3510
+ return null;
3511
+ }
3512
+ }
2499
3513
  function subscribeToUpdates() {
2500
3514
  if (typeof ctx2.api?.onStateUpdate === "function") {
2501
3515
  unsubscribeState = ctx2.api.onStateUpdate((update) => {
2502
3516
  if (paused) return;
2503
3517
  const runId = String(update?.runId || "").trim();
2504
- if (activeRunId && runId && runId !== activeRunId) return;
3518
+ const status = normalizeStatus(update?.data?.status);
3519
+ if (activeRunId && runId && runId !== activeRunId) {
3520
+ if (isTerminalStatus(activeStatus) && (isRunningStatus(status) || status)) {
3521
+ activeRunId = runId;
3522
+ activeStatus = status || "running";
3523
+ stoppedAt = null;
3524
+ renderRunSummary();
3525
+ } else {
3526
+ return;
3527
+ }
3528
+ }
2505
3529
  if (!activeRunId && runId) {
2506
3530
  activeRunId = runId;
2507
3531
  renderRunSummary();
@@ -2517,12 +3541,22 @@ function renderDashboard(root, ctx2) {
2517
3541
  }
2518
3542
  });
2519
3543
  }
3544
+ if (typeof ctx2.api?.onBusEvent === "function") {
3545
+ unsubscribeBus = ctx2.api.onBusEvent((payload) => {
3546
+ if (paused) return;
3547
+ if (payload && payload.event) {
3548
+ updateFromEventPayload(payload);
3549
+ }
3550
+ });
3551
+ }
2520
3552
  if (typeof ctx2.api?.onCmdEvent === "function") {
2521
3553
  unsubscribeCmd = ctx2.api.onCmdEvent((evt) => {
2522
3554
  if (paused) return;
2523
3555
  const runId = String(evt?.runId || "").trim();
2524
- if (!activeRunId && evt?.type === "started" && String(evt?.title || "").includes("xhs unified")) {
3556
+ if ((isTerminalStatus(activeStatus) || !activeRunId) && evt?.type === "started" && String(evt?.title || "").includes("xhs unified")) {
2525
3557
  activeRunId = runId;
3558
+ activeStatus = "running";
3559
+ stoppedAt = null;
2526
3560
  renderRunSummary();
2527
3561
  }
2528
3562
  if (activeRunId && runId && runId !== activeRunId) return;
@@ -2535,9 +3569,16 @@ function renderDashboard(root, ctx2) {
2535
3569
  const failed = Number(statFailed.textContent || "0") || 0;
2536
3570
  statFailed.textContent = String(failed + 1);
2537
3571
  } 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"})`;
3572
+ if (!stoppedAt) {
3573
+ currentPhase.textContent = Number(evt.exitCode || 0) === 0 ? "\u5DF2\u7ED3\u675F" : "\u5931\u8D25";
3574
+ currentAction.textContent = `exit(${evt.exitCode ?? "null"})`;
3575
+ }
2540
3576
  addLog(`\u8FDB\u7A0B\u9000\u51FA: code=${evt.exitCode}`, evt.exitCode === 0 ? "success" : "error");
3577
+ if (!stoppedAt) {
3578
+ stoppedAt = Date.now();
3579
+ updateElapsed();
3580
+ stopElapsedTimer();
3581
+ }
2541
3582
  if (Number(evt.exitCode || 0) !== 0) {
2542
3583
  pushRecentError(`\u8FDB\u7A0B\u9000\u51FA code=${evt.exitCode ?? "null"}`, "exit");
2543
3584
  }
@@ -2556,6 +3597,10 @@ function renderDashboard(root, ctx2) {
2556
3597
  const aliases = ctx2.api?.settings?.profileAliases || {};
2557
3598
  taskAccount.textContent = aliases[config.lastProfileId] || config.lastProfileId;
2558
3599
  }
3600
+ const taskId = String(config.taskId || ctx2?.activeTaskConfigId || "").trim();
3601
+ if (taskId) {
3602
+ taskConfigId.textContent = taskId;
3603
+ }
2559
3604
  }
2560
3605
  } catch (err) {
2561
3606
  console.error("Failed to load task info:", err);
@@ -2574,6 +3619,9 @@ function renderDashboard(root, ctx2) {
2574
3619
  }
2575
3620
  updateFromTaskState(picked);
2576
3621
  }
3622
+ } else {
3623
+ const summary = await loadLatestSummary();
3624
+ if (summary) applySummary(summary);
2577
3625
  }
2578
3626
  } catch (err) {
2579
3627
  console.error("Failed to fetch state:", err);
@@ -2597,9 +3645,16 @@ function renderDashboard(root, ctx2) {
2597
3645
  if (confirm("\u786E\u5B9A\u8981\u505C\u6B62\u5F53\u524D\u4EFB\u52A1\u5417\uFF1F")) {
2598
3646
  try {
2599
3647
  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);
3648
+ let runIdToStop = String(activeRunId || "").trim();
3649
+ if (!runIdToStop && Array.isArray(tasks)) {
3650
+ const running = tasks.find((item) => ["running", "queued", "pending", "starting"].includes(String(item?.status || "").toLowerCase()));
3651
+ runIdToStop = String(running?.runId || tasks[0]?.runId || "").trim();
3652
+ }
3653
+ if (runIdToStop) {
3654
+ await ctx2.api.cmdKill(runIdToStop);
2602
3655
  addLog("\u4EFB\u52A1\u5DF2\u505C\u6B62", "warn");
3656
+ } else {
3657
+ addLog("\u672A\u627E\u5230\u53EF\u505C\u6B62\u7684\u8FD0\u884C\u4EFB\u52A1", "warn");
2603
3658
  }
2604
3659
  } catch (err) {
2605
3660
  console.error("Failed to stop task:", err);
@@ -2611,21 +3666,68 @@ function renderDashboard(root, ctx2) {
2611
3666
  }, 1500);
2612
3667
  }
2613
3668
  };
3669
+ backConfigBtn.onclick = () => {
3670
+ if (typeof ctx2.setActiveTab === "function") {
3671
+ ctx2.setActiveTab("config");
3672
+ }
3673
+ };
2614
3674
  renderRunSummary();
2615
3675
  loadTaskInfo();
2616
3676
  subscribeToUpdates();
2617
3677
  fetchCurrentState();
2618
- elapsedTimer = setInterval(updateElapsed, 1e3);
3678
+ startElapsedTimer();
2619
3679
  return () => {
2620
- if (elapsedTimer) clearInterval(elapsedTimer);
3680
+ stopElapsedTimer();
2621
3681
  if (unsubscribeState) unsubscribeState();
2622
3682
  if (unsubscribeCmd) unsubscribeCmd();
3683
+ if (unsubscribeBus) unsubscribeBus();
2623
3684
  };
2624
3685
  }
2625
3686
 
2626
3687
  // src/renderer/tabs-new/account-manager.mts
3688
+ var PLATFORM_ICON = {
3689
+ xiaohongshu: "\u{1F4D5}",
3690
+ xhs: "\u{1F4D5}",
3691
+ weibo: "\u{1F9E3}"
3692
+ };
3693
+ var PLATFORM_LABEL = {
3694
+ xiaohongshu: "\u5C0F\u7EA2\u4E66",
3695
+ xhs: "\u5C0F\u7EA2\u4E66",
3696
+ weibo: "\u5FAE\u535A"
3697
+ };
3698
+ function normalizePlatform(value) {
3699
+ const normalized = String(value || "").trim().toLowerCase();
3700
+ if (!normalized) return "xiaohongshu";
3701
+ if (normalized === "xhs") return "xiaohongshu";
3702
+ return normalized;
3703
+ }
3704
+ function getPlatformInfo(platform) {
3705
+ const key = normalizePlatform(platform);
3706
+ return {
3707
+ key,
3708
+ icon: PLATFORM_ICON[key] || "\u{1F310}",
3709
+ label: PLATFORM_LABEL[key] || key,
3710
+ loginUrl: key === "weibo" ? "https://weibo.com" : "https://www.xiaohongshu.com"
3711
+ };
3712
+ }
3713
+ function formatTs(value) {
3714
+ if (!Number.isFinite(value) || Number(value) <= 0) return "\u672A\u68C0\u67E5";
3715
+ try {
3716
+ return new Date(Number(value)).toLocaleString("zh-CN");
3717
+ } catch {
3718
+ return "\u672A\u68C0\u67E5";
3719
+ }
3720
+ }
3721
+ function toTimestamp(value) {
3722
+ const text = String(value || "").trim();
3723
+ if (!text) return null;
3724
+ const parsed = Date.parse(text);
3725
+ if (!Number.isFinite(parsed)) return null;
3726
+ return parsed;
3727
+ }
2627
3728
  function renderAccountManager(root, ctx2) {
2628
3729
  root.innerHTML = "";
3730
+ const autoSyncTimers = /* @__PURE__ */ new Map();
2629
3731
  const bentoGrid = createEl("div", { className: "bento-grid bento-sidebar" });
2630
3732
  const envCard = createEl("div", { className: "bento-cell" });
2631
3733
  envCard.innerHTML = `
@@ -2633,7 +3735,7 @@ function renderAccountManager(root, ctx2) {
2633
3735
  <div class="env-status-grid">
2634
3736
  <div class="env-item" id="env-camo">
2635
3737
  <span class="icon" style="color: var(--text-4);">\u25CB</span>
2636
- <span>Camoufox CLI</span>
3738
+ <span>Camo CLI (@web-auto/camo)</span>
2637
3739
  </div>
2638
3740
  <div class="env-item" id="env-unified">
2639
3741
  <span class="icon" style="color: var(--text-4);">\u25CB</span>
@@ -2641,15 +3743,16 @@ function renderAccountManager(root, ctx2) {
2641
3743
  </div>
2642
3744
  <div class="env-item" id="env-browser">
2643
3745
  <span class="icon" style="color: var(--text-4);">\u25CB</span>
2644
- <span>Browser Service</span>
3746
+ <span>Camo Runtime Service (7704\uFF0C\u53EF\u9009)</span>
2645
3747
  </div>
2646
3748
  <div class="env-item" id="env-firefox">
2647
3749
  <span class="icon" style="color: var(--text-4);">\u25CB</span>
2648
- <span>Firefox</span>
3750
+ <span>Camoufox Runtime (python -m camoufox)</span>
2649
3751
  </div>
2650
3752
  </div>
2651
- <div style="margin-top: var(--gap);">
2652
- <button id="recheck-env-btn" class="secondary" style="width: 100%;">\u91CD\u65B0\u68C0\u67E5</button>
3753
+ <div class="btn-group" style="margin-top: var(--gap);">
3754
+ <button id="recheck-env-btn" class="secondary" style="flex: 1;">\u91CD\u65B0\u68C0\u67E5</button>
3755
+ <button id="env-cleanup-btn" class="secondary" style="flex: 1;">\u4E00\u952E\u6E05\u7406</button>
2653
3756
  </div>
2654
3757
  `;
2655
3758
  bentoGrid.appendChild(envCard);
@@ -2659,7 +3762,11 @@ function renderAccountManager(root, ctx2) {
2659
3762
  \u8D26\u6237\u5217\u8868
2660
3763
  <button id="add-account-btn" style="margin-left: auto; padding: 6px 12px; font-size: 12px;">\u6DFB\u52A0\u8D26\u6237</button>
2661
3764
  </div>
2662
- <div id="account-list" class="account-list" style="margin-bottom: var(--gap); max-height: 300px; overflow: auto;"></div>
3765
+ <div class="row" style="margin: 8px 0; gap: 8px; align-items: center;">
3766
+ <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;" />
3767
+ <button id="add-account-confirm-btn" class="secondary" style="flex: 0 0 auto;">\u521B\u5EFA\u5E76\u767B\u5F55</button>
3768
+ </div>
3769
+ <div id="account-list" class="account-list" style="margin-bottom: var(--gap);"></div>
2663
3770
  <div style="margin-top: var(--gap);">
2664
3771
  <div class="btn-group">
2665
3772
  <button id="check-all-btn" class="secondary" style="flex: 1;">\u68C0\u67E5\u6240\u6709</button>
@@ -2670,11 +3777,17 @@ function renderAccountManager(root, ctx2) {
2670
3777
  bentoGrid.appendChild(accountCard);
2671
3778
  root.appendChild(bentoGrid);
2672
3779
  const recheckEnvBtn = root.querySelector("#recheck-env-btn");
3780
+ const envCleanupBtn = root.querySelector("#env-cleanup-btn");
2673
3781
  const addAccountBtn = root.querySelector("#add-account-btn");
3782
+ const addAccountConfirmBtn = root.querySelector("#add-account-confirm-btn");
3783
+ const newAccountAliasInput = root.querySelector("#new-account-alias-input");
2674
3784
  const checkAllBtn = root.querySelector("#check-all-btn");
2675
3785
  const refreshExpiredBtn = root.querySelector("#refresh-expired-btn");
2676
3786
  const accountListEl = root.querySelector("#account-list");
2677
3787
  let accounts = [];
3788
+ let envCheckInFlight = false;
3789
+ let accountCheckInFlight = false;
3790
+ let busUnsubscribe = null;
2678
3791
  async function checkEnvironment() {
2679
3792
  try {
2680
3793
  const [camo, services, firefox] = await Promise.all([
@@ -2684,12 +3797,21 @@ function renderAccountManager(root, ctx2) {
2684
3797
  ]);
2685
3798
  updateEnvItem("env-camo", camo.installed);
2686
3799
  updateEnvItem("env-unified", services.unifiedApi);
2687
- updateEnvItem("env-browser", services.browserService);
3800
+ updateEnvItem("env-browser", services.camoRuntime);
2688
3801
  updateEnvItem("env-firefox", firefox.installed);
2689
3802
  } catch (err) {
2690
3803
  console.error("Environment check failed:", err);
2691
3804
  }
2692
3805
  }
3806
+ async function tickEnvironment() {
3807
+ if (envCheckInFlight) return;
3808
+ envCheckInFlight = true;
3809
+ try {
3810
+ await checkEnvironment();
3811
+ } finally {
3812
+ envCheckInFlight = false;
3813
+ }
3814
+ }
2693
3815
  function updateEnvItem(id, ok) {
2694
3816
  const el = root.querySelector(`#${id}`);
2695
3817
  if (!el) return;
@@ -2702,13 +3824,28 @@ function renderAccountManager(root, ctx2) {
2702
3824
  const rows = await listAccountProfiles(ctx2.api);
2703
3825
  accounts = rows.map((row) => ({
2704
3826
  ...row,
2705
- statusView: row.valid ? "valid" : row.status === "pending" ? "pending" : "expired"
3827
+ platform: normalizePlatform(row.platform),
3828
+ statusView: row.valid ? "valid" : row.status === "pending" ? "pending" : "expired",
3829
+ lastCheckAt: toTimestamp(row.updatedAt)
2706
3830
  }));
2707
3831
  renderAccountList();
2708
3832
  } catch (err) {
2709
3833
  console.error("Failed to load accounts:", err);
2710
3834
  }
2711
3835
  }
3836
+ async function tickAccounts() {
3837
+ if (accountCheckInFlight) return;
3838
+ accountCheckInFlight = true;
3839
+ try {
3840
+ await loadAccounts();
3841
+ const pending = accounts.filter((acc) => acc.statusView === "pending");
3842
+ for (const acc of pending) {
3843
+ await checkAccountStatus(acc.profileId, { pendingWhileLogin: true });
3844
+ }
3845
+ } finally {
3846
+ accountCheckInFlight = false;
3847
+ }
3848
+ }
2712
3849
  function renderAccountList() {
2713
3850
  accountListEl.innerHTML = "";
2714
3851
  if (accounts.length === 0) {
@@ -2716,24 +3853,34 @@ function renderAccountManager(root, ctx2) {
2716
3853
  return;
2717
3854
  }
2718
3855
  accounts.forEach((acc) => {
3856
+ const platform = getPlatformInfo(acc.platform);
2719
3857
  const row = createEl("div", {
2720
3858
  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);"
3859
+ style: "display: flex; gap: var(--gap-sm); padding: var(--gap-sm); align-items: center; border-bottom: 1px solid var(--border);"
2722
3860
  });
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])
3861
+ const nameDiv = createEl("div", { style: "min-width: 0; flex: 1;" }, [
3862
+ createEl("div", { className: "account-name", style: "display: flex; gap: 6px; align-items: center;" }, [
3863
+ createEl("span", { style: "font-size: 13px;" }, [platform.icon]),
3864
+ createEl("span", {}, [acc.alias || acc.name || acc.profileId]),
3865
+ createEl("span", { style: "font-size: 11px; color: var(--text-3);" }, [platform.label])
3866
+ ]),
3867
+ createEl("div", { className: "account-alias", style: "font-size: 11px; color: var(--text-3);" }, [
3868
+ `profile: ${acc.profileId} \xB7 \u4E0A\u6B21\u68C0\u67E5: ${formatTs(acc.lastCheckAt)}`
3869
+ ])
2726
3870
  ]);
2727
3871
  const statusBadge = createEl("span", {
2728
- className: `status-badge ${acc.statusView === "valid" ? "status-valid" : acc.statusView === "expired" ? "status-expired" : "status-pending"}`
3872
+ className: `status-badge ${acc.statusView === "valid" ? "status-valid" : acc.statusView === "expired" ? "status-expired" : "status-pending"}`,
3873
+ style: "min-width: 76px; text-align: center;"
2729
3874
  }, [
2730
- acc.statusView === "valid" ? "\u2713 \u6709\u6548" : acc.statusView === "expired" ? "\u2717 \u5931\u6548" : acc.statusView === "checking" ? "\u23F3 \u68C0\u67E5\u4E2D" : "\u23F3 \u5F85\u68C0\u67E5"
3875
+ acc.statusView === "valid" ? "\u2713 \u6709\u6548" : acc.statusView === "expired" ? "\u2717 \u5931\u6548" : acc.statusView === "checking" ? "\u23F3 \u68C0\u67E5\u4E2D" : "\u23F3 \u5F85\u767B\u5F55"
2731
3876
  ]);
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;" });
3877
+ const actionsDiv = createEl("div", {
3878
+ className: "btn-group",
3879
+ style: "display: flex; flex-wrap: wrap; gap: 4px; justify-content: flex-end; flex: 0 0 auto;"
3880
+ });
3881
+ const checkBtn = createEl("button", { className: "secondary", style: "padding: 6px 8px; font-size: 10px;" }, ["\u68C0\u67E5"]);
3882
+ const openBtn = createEl("button", { className: "secondary", style: "padding: 6px 8px; font-size: 10px;" }, ["\u6253\u5F00"]);
3883
+ const fixBtn = createEl("button", { className: "secondary", style: "padding: 6px 8px; font-size: 10px;" }, ["\u4FEE\u590D"]);
2737
3884
  const detailBtn = createEl("button", {
2738
3885
  className: "secondary",
2739
3886
  style: "padding: 6px 8px; font-size: 10px;"
@@ -2742,22 +3889,34 @@ function renderAccountManager(root, ctx2) {
2742
3889
  className: "danger",
2743
3890
  style: "padding: 6px 8px; font-size: 10px;"
2744
3891
  }, ["\u5220\u9664"]);
3892
+ actionsDiv.appendChild(checkBtn);
3893
+ actionsDiv.appendChild(openBtn);
3894
+ actionsDiv.appendChild(fixBtn);
2745
3895
  actionsDiv.appendChild(detailBtn);
2746
3896
  actionsDiv.appendChild(deleteBtn);
2747
3897
  row.appendChild(nameDiv);
2748
3898
  row.appendChild(statusBadge);
2749
- row.appendChild(checkBtn);
2750
3899
  row.appendChild(actionsDiv);
2751
- checkBtn.onclick = () => checkAccountStatus(acc.profileId);
3900
+ checkBtn.onclick = () => {
3901
+ void checkAccountStatus(acc.profileId, { resolveAlias: true });
3902
+ };
3903
+ openBtn.onclick = () => {
3904
+ void openAccountLogin(acc, { reason: "manual_open" });
3905
+ };
3906
+ fixBtn.onclick = () => {
3907
+ void fixAccount(acc);
3908
+ };
2752
3909
  detailBtn.onclick = () => {
2753
3910
  alert(`\u8D26\u6237\u8BE6\u60C5:
2754
3911
 
3912
+ \u5E73\u53F0: ${platform.label}
2755
3913
  Profile ID: ${acc.profileId}
2756
3914
  \u8D26\u53F7ID: ${acc.accountId || "\u672A\u8BC6\u522B"}
2757
3915
  \u522B\u540D: ${acc.alias || "\u672A\u8BBE\u7F6E"}
2758
3916
  \u72B6\u6001: ${acc.status}
2759
3917
  \u539F\u56E0: ${acc.reason || "-"}
2760
- \u6700\u540E\u68C0\u67E5: ${acc.lastCheck || "\u672A\u68C0\u67E5"}`);
3918
+ \u6700\u540E\u68C0\u67E5: ${formatTs(acc.lastCheckAt)}
3919
+ \u767B\u5F55\u5165\u53E3: ${platform.loginUrl}`);
2761
3920
  };
2762
3921
  deleteBtn.onclick = async () => {
2763
3922
  if (confirm(`\u786E\u5B9A\u5220\u9664\u8D26\u6237 "${acc.alias || acc.profileId}" \u5417\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\u3002`)) {
@@ -2787,9 +3946,9 @@ Profile ID: ${acc.profileId}
2787
3946
  accountListEl.appendChild(row);
2788
3947
  });
2789
3948
  }
2790
- async function checkAccountStatus(profileId) {
3949
+ async function checkAccountStatus(profileId, options = {}) {
2791
3950
  const account = accounts.find((a) => a.profileId === profileId);
2792
- if (!account) return;
3951
+ if (!account) return false;
2793
3952
  account.statusView = "checking";
2794
3953
  renderAccountList();
2795
3954
  try {
@@ -2800,51 +3959,110 @@ Profile ID: ${acc.profileId}
2800
3959
  ctx2.api.pathJoin("apps", "webauto", "entry", "account.mjs"),
2801
3960
  "sync",
2802
3961
  profileId,
3962
+ ...options.pendingWhileLogin ? ["--pending-while-login"] : [],
3963
+ ...options.resolveAlias ? ["--resolve-alias"] : [],
2803
3964
  "--json"
2804
3965
  ]
2805
3966
  });
2806
3967
  const profile = result?.json?.profile;
2807
3968
  if (profile && String(profile.profileId || "").trim() === profileId) {
2808
3969
  account.accountId = String(profile.accountId || "").trim() || null;
2809
- account.alias = String(profile.alias || "").trim() || account.alias;
3970
+ const detectedAlias = String(profile.alias || "").trim();
3971
+ account.alias = detectedAlias || account.alias;
3972
+ account.platform = normalizePlatform(String(profile.platform || account.platform || "").trim());
2810
3973
  account.status = String(profile.status || "").trim() || account.status;
2811
3974
  account.valid = profile.valid === true && Boolean(account.accountId);
2812
3975
  account.reason = String(profile.reason || "").trim() || null;
3976
+ if (detectedAlias) {
3977
+ const aliases = { ...ctx2.api?.settings?.profileAliases || {}, [profileId]: detectedAlias };
3978
+ await ctx2.api.settingsSet({ profileAliases: aliases }).catch(() => null);
3979
+ if (typeof ctx2.refreshSettings === "function") {
3980
+ await ctx2.refreshSettings().catch(() => null);
3981
+ }
3982
+ }
2813
3983
  }
2814
- account.statusView = account.valid ? "valid" : "expired";
2815
- account.lastCheck = (/* @__PURE__ */ new Date()).toLocaleString("zh-CN");
3984
+ account.statusView = account.valid ? "valid" : account.status === "pending" ? "pending" : "expired";
3985
+ account.lastCheckAt = Date.now();
2816
3986
  } catch (err) {
2817
- account.statusView = "expired";
3987
+ account.statusView = options.pendingWhileLogin ? "pending" : "expired";
3988
+ account.lastCheckAt = Date.now();
2818
3989
  }
2819
3990
  renderAccountList();
3991
+ return Boolean(account.valid);
3992
+ }
3993
+ async function openAccountLogin(account, options = {}) {
3994
+ if (!String(account.profileId || "").trim()) return false;
3995
+ const platform = getPlatformInfo(account.platform);
3996
+ const timeoutSec = Math.max(30, Number(ctx2.api?.settings?.timeouts?.loginTimeoutSec || 900));
3997
+ account.status = "pending";
3998
+ account.statusView = "pending";
3999
+ account.reason = String(options.reason || "manual_relogin");
4000
+ account.lastCheckAt = Date.now();
4001
+ renderAccountList();
4002
+ await ctx2.api.cmdSpawn({
4003
+ title: `\u767B\u5F55 ${account.alias || account.profileId}`,
4004
+ cwd: "",
4005
+ args: [
4006
+ ctx2.api.pathJoin("apps", "webauto", "entry", "profilepool.mjs"),
4007
+ "login-profile",
4008
+ account.profileId,
4009
+ "--url",
4010
+ platform.loginUrl,
4011
+ "--wait-sync",
4012
+ "false",
4013
+ "--timeout-sec",
4014
+ String(timeoutSec),
4015
+ "--keep-session"
4016
+ ],
4017
+ groupKey: "profilepool"
4018
+ });
4019
+ startAutoSyncProfile(account.profileId);
4020
+ return true;
4021
+ }
4022
+ async function fixAccount(account) {
4023
+ const ok = await checkAccountStatus(account.profileId, { resolveAlias: true });
4024
+ if (ok) return;
4025
+ await openAccountLogin(account, { reason: "fix_relogin" });
2820
4026
  }
2821
4027
  async function addAccount() {
2822
- const alias = prompt("\u8BF7\u8F93\u5165\u65B0\u8D26\u6237\u522B\u540D:");
2823
- if (!alias?.trim()) return;
4028
+ const alias = newAccountAliasInput.value.trim();
2824
4029
  try {
2825
4030
  const out = await ctx2.api.cmdRunJson({
2826
- title: "profilepool add",
4031
+ title: "account add",
2827
4032
  cwd: "",
2828
- args: [ctx2.api.pathJoin("apps", "webauto", "entry", "profilepool.mjs"), "add", "xiaohongshu", "--json"]
4033
+ args: [
4034
+ ctx2.api.pathJoin("apps", "webauto", "entry", "account.mjs"),
4035
+ "add",
4036
+ "--platform",
4037
+ "xiaohongshu",
4038
+ "--status",
4039
+ "pending",
4040
+ ...alias ? ["--alias", alias] : [],
4041
+ "--json"
4042
+ ]
2829
4043
  });
2830
- if (!out?.ok || !out?.json?.profileId) {
4044
+ const profileId = String(out?.json?.account?.profileId || "").trim();
4045
+ if (!out?.ok || !profileId) {
2831
4046
  alert("\u521B\u5EFA\u8D26\u53F7\u5931\u8D25: " + (out?.error || "\u672A\u77E5\u9519\u8BEF"));
2832
4047
  return;
2833
4048
  }
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();
4049
+ if (alias) {
4050
+ const aliases = { ...ctx2.api.settings?.profileAliases, [profileId]: alias };
4051
+ await ctx2.api.settingsSet({ profileAliases: aliases });
4052
+ if (typeof ctx2.refreshSettings === "function") {
4053
+ await ctx2.refreshSettings();
4054
+ }
2839
4055
  }
2840
4056
  const timeoutSec = ctx2.api.settings?.timeouts?.loginTimeoutSec || 900;
2841
4057
  await ctx2.api.cmdSpawn({
2842
- title: `\u767B\u5F55 ${alias}`,
4058
+ title: `\u767B\u5F55 ${alias || profileId}`,
2843
4059
  cwd: "",
2844
4060
  args: [
2845
4061
  ctx2.api.pathJoin("apps", "webauto", "entry", "profilepool.mjs"),
2846
4062
  "login-profile",
2847
4063
  profileId,
4064
+ "--wait-sync",
4065
+ "false",
2848
4066
  "--timeout-sec",
2849
4067
  String(timeoutSec),
2850
4068
  "--keep-session"
@@ -2852,21 +4070,52 @@ Profile ID: ${acc.profileId}
2852
4070
  groupKey: "profilepool"
2853
4071
  });
2854
4072
  await loadAccounts();
4073
+ newAccountAliasInput.value = "";
4074
+ startAutoSyncProfile(profileId);
2855
4075
  } catch (err) {
2856
4076
  alert("\u6DFB\u52A0\u8D26\u53F7\u5931\u8D25: " + (err?.message || String(err)));
2857
4077
  }
2858
4078
  }
4079
+ function startAutoSyncProfile(profileId) {
4080
+ const id = String(profileId || "").trim();
4081
+ if (!id) return;
4082
+ const existing = autoSyncTimers.get(id);
4083
+ if (existing) clearInterval(existing);
4084
+ const timeoutSec = Math.max(30, Number(ctx2.api?.settings?.timeouts?.loginTimeoutSec || 900));
4085
+ const intervalMs = 2e3;
4086
+ const maxAttempts = Math.ceil(timeoutSec * 1e3 / intervalMs);
4087
+ let attempts = 0;
4088
+ void checkAccountStatus(id, { pendingWhileLogin: true }).then((ok) => {
4089
+ if (ok) {
4090
+ const timer2 = autoSyncTimers.get(id);
4091
+ if (timer2) clearInterval(timer2);
4092
+ autoSyncTimers.delete(id);
4093
+ void checkAccountStatus(id, { resolveAlias: true }).catch(() => null);
4094
+ }
4095
+ });
4096
+ const timer = setInterval(() => {
4097
+ attempts += 1;
4098
+ void checkAccountStatus(id, { pendingWhileLogin: true }).then((ok) => {
4099
+ if (ok || attempts >= maxAttempts) {
4100
+ const current = autoSyncTimers.get(id);
4101
+ if (current) clearInterval(current);
4102
+ autoSyncTimers.delete(id);
4103
+ }
4104
+ });
4105
+ }, intervalMs);
4106
+ autoSyncTimers.set(id, timer);
4107
+ }
2859
4108
  async function checkAllAccounts() {
2860
4109
  checkAllBtn.disabled = true;
2861
4110
  checkAllBtn.textContent = "\u68C0\u67E5\u4E2D...";
2862
4111
  for (const acc of accounts) {
2863
- await checkAccountStatus(acc.profileId);
4112
+ await checkAccountStatus(acc.profileId, { resolveAlias: true });
2864
4113
  }
2865
4114
  checkAllBtn.disabled = false;
2866
4115
  checkAllBtn.textContent = "\u68C0\u67E5\u6240\u6709";
2867
4116
  }
2868
4117
  async function refreshExpiredAccounts() {
2869
- const expired = accounts.filter((a) => !a.valid);
4118
+ const expired = accounts.filter((a) => !a.valid && a.status !== "pending");
2870
4119
  if (expired.length === 0) {
2871
4120
  alert("\u6CA1\u6709\u5931\u6548\u7684\u8D26\u6237\u9700\u8981\u5237\u65B0");
2872
4121
  return;
@@ -2875,20 +4124,7 @@ Profile ID: ${acc.profileId}
2875
4124
  refreshExpiredBtn.textContent = "\u5237\u65B0\u4E2D...";
2876
4125
  for (const acc of expired) {
2877
4126
  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
- });
4127
+ await openAccountLogin(acc, { reason: "refresh_expired" });
2892
4128
  } catch (err) {
2893
4129
  console.error(`Failed to refresh ${acc.profileId}:`, err);
2894
4130
  }
@@ -2896,19 +4132,683 @@ Profile ID: ${acc.profileId}
2896
4132
  refreshExpiredBtn.disabled = false;
2897
4133
  refreshExpiredBtn.textContent = "\u5237\u65B0\u5931\u6548";
2898
4134
  }
4135
+ async function cleanupEnvironment() {
4136
+ if (!envCleanupBtn) return;
4137
+ envCleanupBtn.disabled = true;
4138
+ const previous = envCleanupBtn.textContent;
4139
+ envCleanupBtn.textContent = "\u6E05\u7406\u4E2D...";
4140
+ try {
4141
+ const result = typeof ctx2.api?.envCleanup === "function" ? await ctx2.api.envCleanup() : await ctx2.api.invoke?.("env:cleanup");
4142
+ if (!result?.ok) {
4143
+ alert(`\u73AF\u5883\u6E05\u7406\u5931\u8D25: ${result?.error || "\u672A\u77E5\u9519\u8BEF"}`);
4144
+ }
4145
+ await tickEnvironment();
4146
+ await tickAccounts();
4147
+ } catch (err) {
4148
+ alert(`\u73AF\u5883\u6E05\u7406\u5931\u8D25: ${err?.message || String(err)}`);
4149
+ } finally {
4150
+ envCleanupBtn.disabled = false;
4151
+ envCleanupBtn.textContent = previous || "\u4E00\u952E\u6E05\u7406";
4152
+ }
4153
+ }
2899
4154
  recheckEnvBtn.onclick = checkEnvironment;
4155
+ if (envCleanupBtn) envCleanupBtn.onclick = () => {
4156
+ void cleanupEnvironment();
4157
+ };
2900
4158
  addAccountBtn.onclick = addAccount;
4159
+ addAccountConfirmBtn.onclick = addAccount;
4160
+ newAccountAliasInput.onkeydown = (ev) => {
4161
+ if (ev.key === "Enter") {
4162
+ ev.preventDefault();
4163
+ void addAccount();
4164
+ }
4165
+ };
2901
4166
  checkAllBtn.onclick = checkAllAccounts;
2902
4167
  refreshExpiredBtn.onclick = refreshExpiredAccounts;
2903
- void checkEnvironment();
2904
- void loadAccounts();
4168
+ void tickEnvironment();
4169
+ void tickAccounts();
4170
+ if (typeof ctx2.api?.onBusEvent === "function") {
4171
+ busUnsubscribe = ctx2.api.onBusEvent((evt) => {
4172
+ const type = String(evt?.type || evt?.event || "").trim().toLowerCase();
4173
+ if (!type) return;
4174
+ if (type.startsWith("account:")) {
4175
+ void tickAccounts();
4176
+ }
4177
+ if (type.startsWith("env:")) {
4178
+ void tickEnvironment();
4179
+ }
4180
+ });
4181
+ }
4182
+ return () => {
4183
+ for (const timer of autoSyncTimers.values()) clearInterval(timer);
4184
+ autoSyncTimers.clear();
4185
+ if (typeof busUnsubscribe === "function") busUnsubscribe();
4186
+ };
4187
+ }
4188
+
4189
+ // src/renderer/tabs-new/scheduler.mts
4190
+ function commandTypeToWeiboTaskType2(commandType) {
4191
+ if (commandType === "weibo-search") return "search";
4192
+ if (commandType === "weibo-monitor") return "monitor";
4193
+ return "timeline";
4194
+ }
4195
+ function renderSchedulerPanel(root, ctx2) {
4196
+ root.innerHTML = "";
4197
+ const pageIndicator = createEl("div", { className: "page-indicator" }, [
4198
+ "\u5F53\u524D: ",
4199
+ createEl("span", {}, ["\u5B9A\u65F6\u4EFB\u52A1"]),
4200
+ " \u2192 \u914D\u7F6E\u5E76\u5B9A\u65F6\u6267\u884C\u591A\u6761\u4EFB\u52A1"
4201
+ ]);
4202
+ root.appendChild(pageIndicator);
4203
+ const toolbar = createEl("div", { className: "bento-grid", style: "margin-bottom: var(--gap);" });
4204
+ const toolbarCell = createEl("div", { className: "bento-cell" });
4205
+ toolbarCell.innerHTML = `
4206
+ <div class="bento-title">\u8C03\u5EA6\u63A7\u5236</div>
4207
+ <div class="row">
4208
+ <button id="scheduler-refresh-btn" class="secondary">\u5237\u65B0\u5217\u8868</button>
4209
+ <button id="scheduler-run-due-btn" class="secondary">\u7ACB\u5373\u6267\u884C\u5230\u70B9\u4EFB\u52A1</button>
4210
+ <button id="scheduler-export-all-btn" class="secondary">\u5BFC\u51FA\u5168\u90E8</button>
4211
+ <button id="scheduler-import-btn" class="secondary">\u5BFC\u5165</button>
4212
+ </div>
4213
+ <div class="row" style="margin-top: 8px; align-items: center;">
4214
+ <span class="muted">\u5F53\u524D\u914D\u7F6E: <strong id="scheduler-active-task-id">-</strong></span>
4215
+ <button id="scheduler-open-config-btn" class="secondary">\u6253\u5F00\u914D\u7F6E\u9875</button>
4216
+ </div>
4217
+ <div class="row" style="margin-top: 8px; align-items: end;">
4218
+ <div>
4219
+ <label>Daemon \u95F4\u9694(\u79D2)</label>
4220
+ <input id="scheduler-daemon-interval" type="number" min="5" value="30" style="width: 120px;" />
4221
+ </div>
4222
+ <button id="scheduler-daemon-start-btn">\u542F\u52A8 Daemon</button>
4223
+ <button id="scheduler-daemon-stop-btn" class="danger">\u505C\u6B62 Daemon</button>
4224
+ <span id="scheduler-daemon-status" class="muted">daemon: \u672A\u542F\u52A8</span>
4225
+ </div>
4226
+ `;
4227
+ toolbar.appendChild(toolbarCell);
4228
+ root.appendChild(toolbar);
4229
+ const grid = createEl("div", { className: "bento-grid bento-sidebar" });
4230
+ const formCell = createEl("div", { className: "bento-cell" });
4231
+ formCell.innerHTML = `
4232
+ <div class="bento-title">\u4EFB\u52A1\u7F16\u8F91</div>
4233
+ <input id="scheduler-editing-id" type="hidden" />
4234
+ <div class="row">
4235
+ <div>
4236
+ <label>\u5E73\u53F0</label>
4237
+ <select id="scheduler-platform" style="width: 140px;">
4238
+ <option value="xiaohongshu">\u{1F4D5} \u5C0F\u7EA2\u4E66</option>
4239
+ <option value="weibo">\u{1F4F0} \u5FAE\u535A</option>
4240
+ <option value="1688">\u{1F6D2} 1688</option>
4241
+ </select>
4242
+ </div>
4243
+ <div>
4244
+ <label>\u4EFB\u52A1\u7C7B\u578B</label>
4245
+ <select id="scheduler-task-type" style="width: 160px;">
4246
+ </select>
4247
+ </div>
4248
+ </div>
4249
+ <div class="row">
4250
+ <div>
4251
+ <label>\u4EFB\u52A1\u540D</label>
4252
+ <input id="scheduler-name" placeholder="\u4F8B\u5982\uFF1Adeepseek-\u6BCF30\u5206\u949F" style="width: 240px;" />
4253
+ </div>
4254
+ <label style="display:flex; align-items:center; gap:8px; margin-top: 22px;">
4255
+ <input id="scheduler-enabled" type="checkbox" checked />
4256
+ <span>\u542F\u7528</span>
4257
+ </label>
4258
+ </div>
4259
+ <div class="row">
4260
+ <div>
4261
+ <label>\u8C03\u5EA6\u7C7B\u578B</label>
4262
+ <select id="scheduler-type" style="width: 140px;">
4263
+ <option value="interval">\u5FAA\u73AF\u95F4\u9694</option>
4264
+ <option value="once">\u4E00\u6B21\u6027</option>
4265
+ <option value="daily">\u6BCF\u5929</option>
4266
+ <option value="weekly">\u6BCF\u5468</option>
4267
+ </select>
4268
+ </div>
4269
+ <div id="scheduler-interval-wrap">
4270
+ <label>\u95F4\u9694\u5206\u949F</label>
4271
+ <input id="scheduler-interval" type="number" min="1" value="30" style="width: 120px;" />
4272
+ </div>
4273
+ <div id="scheduler-runat-wrap" style="display:none;">
4274
+ <label>\u951A\u70B9\u65F6\u95F4</label>
4275
+ <input id="scheduler-runat" type="datetime-local" style="width: 220px;" />
4276
+ </div>
4277
+ <div>
4278
+ <label>\u6700\u5927\u6267\u884C\u6B21\u6570</label>
4279
+ <input id="scheduler-max-runs" type="number" min="1" placeholder="\u4E0D\u9650" style="width: 120px;" />
4280
+ </div>
4281
+ </div>
4282
+ <div class="row">
4283
+ <div>
4284
+ <label>Profile</label>
4285
+ <input id="scheduler-profile" placeholder="xiaohongshu-batch-1" style="width: 220px;" />
4286
+ </div>
4287
+ <div>
4288
+ <label>\u5173\u952E\u8BCD</label>
4289
+ <input id="scheduler-keyword" placeholder="deepseek\u65B0\u6A21\u578B" style="width: 220px;" />
4290
+ </div>
4291
+ </div>
4292
+ <div class="row" id="scheduler-user-id-wrap" style="display:none;">
4293
+ <div>
4294
+ <label>\u5FAE\u535A\u7528\u6237ID (monitor \u5FC5\u586B)</label>
4295
+ <input id="scheduler-user-id" placeholder="\u4F8B\u5982: 1234567890" style="width: 220px;" />
4296
+ </div>
4297
+ </div>
4298
+ <div class="row">
4299
+ <div>
4300
+ <label>\u76EE\u6807\u5E16\u5B50\u6570</label>
4301
+ <input id="scheduler-max-notes" type="number" min="1" value="50" style="width: 120px;" />
4302
+ </div>
4303
+ <div>
4304
+ <label>\u73AF\u5883</label>
4305
+ <select id="scheduler-env" style="width: 120px;">
4306
+ <option value="debug">debug</option>
4307
+ <option value="prod">prod</option>
4308
+ </select>
4309
+ </div>
4310
+ </div>
4311
+ <div class="row">
4312
+ <label style="display:flex; align-items:center; gap:8px;">
4313
+ <input id="scheduler-comments" type="checkbox" checked />
4314
+ <span>\u6293\u8BC4\u8BBA</span>
4315
+ </label>
4316
+ <label style="display:flex; align-items:center; gap:8px;">
4317
+ <input id="scheduler-likes" type="checkbox" />
4318
+ <span>\u70B9\u8D5E</span>
4319
+ </label>
4320
+ <label style="display:flex; align-items:center; gap:8px;">
4321
+ <input id="scheduler-headless" type="checkbox" />
4322
+ <span>headless</span>
4323
+ </label>
4324
+ <label style="display:flex; align-items:center; gap:8px;">
4325
+ <input id="scheduler-dryrun" type="checkbox" />
4326
+ <span>dry-run</span>
4327
+ </label>
4328
+ </div>
4329
+ <div>
4330
+ <label>\u70B9\u8D5E\u5173\u952E\u8BCD\uFF08\u9017\u53F7\u5206\u9694\uFF09</label>
4331
+ <input id="scheduler-like-keywords" placeholder="\u771F\u725B\u903C,\u8D2D\u4E70\u94FE\u63A5" />
4332
+ </div>
4333
+ <div class="btn-group" style="margin-top: var(--gap);">
4334
+ <button id="scheduler-save-btn" style="flex:1;">\u4FDD\u5B58\u4EFB\u52A1</button>
4335
+ <button id="scheduler-reset-btn" class="secondary" style="flex:1;">\u6E05\u7A7A\u8868\u5355</button>
4336
+ </div>
4337
+ `;
4338
+ grid.appendChild(formCell);
4339
+ const listCell = createEl("div", { className: "bento-cell" });
4340
+ listCell.innerHTML = `
4341
+ <div class="bento-title">\u4EFB\u52A1\u5217\u8868</div>
4342
+ <div id="scheduler-list"></div>
4343
+ `;
4344
+ grid.appendChild(listCell);
4345
+ root.appendChild(grid);
4346
+ const refreshBtn = root.querySelector("#scheduler-refresh-btn");
4347
+ const runDueBtn = root.querySelector("#scheduler-run-due-btn");
4348
+ const exportAllBtn = root.querySelector("#scheduler-export-all-btn");
4349
+ const importBtn = root.querySelector("#scheduler-import-btn");
4350
+ const openConfigBtn = root.querySelector("#scheduler-open-config-btn");
4351
+ const daemonStartBtn = root.querySelector("#scheduler-daemon-start-btn");
4352
+ const daemonStopBtn = root.querySelector("#scheduler-daemon-stop-btn");
4353
+ const daemonIntervalInput = root.querySelector("#scheduler-daemon-interval");
4354
+ const daemonStatus = root.querySelector("#scheduler-daemon-status");
4355
+ const activeTaskIdText = root.querySelector("#scheduler-active-task-id");
4356
+ const listEl = root.querySelector("#scheduler-list");
4357
+ const platformSelect = root.querySelector("#scheduler-platform");
4358
+ const taskTypeSelect = root.querySelector("#scheduler-task-type");
4359
+ const editingIdInput = root.querySelector("#scheduler-editing-id");
4360
+ const nameInput = root.querySelector("#scheduler-name");
4361
+ const enabledInput = root.querySelector("#scheduler-enabled");
4362
+ const typeSelect = root.querySelector("#scheduler-type");
4363
+ const intervalWrap = root.querySelector("#scheduler-interval-wrap");
4364
+ const runAtWrap = root.querySelector("#scheduler-runat-wrap");
4365
+ const intervalInput = root.querySelector("#scheduler-interval");
4366
+ const runAtInput = root.querySelector("#scheduler-runat");
4367
+ const maxRunsInput = root.querySelector("#scheduler-max-runs");
4368
+ const profileInput = root.querySelector("#scheduler-profile");
4369
+ const keywordInput = root.querySelector("#scheduler-keyword");
4370
+ const userIdWrap = root.querySelector("#scheduler-user-id-wrap");
4371
+ const userIdInput = root.querySelector("#scheduler-user-id");
4372
+ const maxNotesInput = root.querySelector("#scheduler-max-notes");
4373
+ const envSelect = root.querySelector("#scheduler-env");
4374
+ const commentsInput = root.querySelector("#scheduler-comments");
4375
+ const likesInput = root.querySelector("#scheduler-likes");
4376
+ const headlessInput = root.querySelector("#scheduler-headless");
4377
+ const dryRunInput = root.querySelector("#scheduler-dryrun");
4378
+ const likeKeywordsInput = root.querySelector("#scheduler-like-keywords");
4379
+ const saveBtn = root.querySelector("#scheduler-save-btn");
4380
+ const resetBtn = root.querySelector("#scheduler-reset-btn");
4381
+ let tasks = [];
4382
+ let daemonRunId = "";
4383
+ let unsubscribeCmd = null;
4384
+ let pendingFocusTaskId = String(ctx2?.activeTaskConfigId || "").trim();
4385
+ function setDaemonStatus(text) {
4386
+ daemonStatus.textContent = text;
4387
+ }
4388
+ function setActiveTaskContext(taskId) {
4389
+ const id = String(taskId || "").trim();
4390
+ activeTaskIdText.textContent = id || "-";
4391
+ if (ctx2 && typeof ctx2 === "object") {
4392
+ ctx2.activeTaskConfigId = id;
4393
+ }
4394
+ }
4395
+ function openConfigTab(taskId) {
4396
+ setActiveTaskContext(taskId);
4397
+ if (typeof ctx2.setActiveTab === "function") {
4398
+ ctx2.setActiveTab("config");
4399
+ }
4400
+ }
4401
+ function updateTypeFields() {
4402
+ const mode = typeSelect.value;
4403
+ const useRunAt = mode === "once" || mode === "daily" || mode === "weekly";
4404
+ runAtWrap.style.display = useRunAt ? "" : "none";
4405
+ intervalWrap.style.display = useRunAt ? "none" : "";
4406
+ }
4407
+ function updateTaskTypeOptions() {
4408
+ const platform = platformSelect.value;
4409
+ const tasks2 = getTasksForPlatform(platform);
4410
+ taskTypeSelect.innerHTML = tasks2.map((t) => `<option value="${t.type}">${t.icon} ${t.label}</option>`).join("");
4411
+ if (taskTypeSelect.options.length > 0) {
4412
+ taskTypeSelect.value = taskTypeSelect.options[0]?.value || "";
4413
+ }
4414
+ updatePlatformFields();
4415
+ }
4416
+ function updatePlatformFields() {
4417
+ const commandType = String(taskTypeSelect.value || "").trim();
4418
+ const isWeiboMonitor = commandType === "weibo-monitor";
4419
+ userIdWrap.style.display = isWeiboMonitor ? "" : "none";
4420
+ }
4421
+ function resetForm() {
4422
+ platformSelect.value = "xiaohongshu";
4423
+ updateTaskTypeOptions();
4424
+ editingIdInput.value = "";
4425
+ nameInput.value = "";
4426
+ enabledInput.checked = true;
4427
+ typeSelect.value = "interval";
4428
+ intervalInput.value = "30";
4429
+ runAtInput.value = "";
4430
+ maxRunsInput.value = "";
4431
+ profileInput.value = "";
4432
+ keywordInput.value = "";
4433
+ userIdInput.value = "";
4434
+ maxNotesInput.value = "50";
4435
+ envSelect.value = "debug";
4436
+ commentsInput.checked = true;
4437
+ likesInput.checked = false;
4438
+ headlessInput.checked = false;
4439
+ dryRunInput.checked = false;
4440
+ likeKeywordsInput.value = "";
4441
+ setActiveTaskContext("");
4442
+ updatePlatformFields();
4443
+ updateTypeFields();
4444
+ }
4445
+ function readFormAsPayload() {
4446
+ const maxRunsRaw = maxRunsInput.value.trim();
4447
+ const maxRuns = maxRunsRaw ? Math.max(1, Number(maxRunsRaw) || 1) : null;
4448
+ const commandType = String(taskTypeSelect.value || "xhs-unified").trim();
4449
+ const argv = {
4450
+ profile: profileInput.value.trim(),
4451
+ keyword: keywordInput.value.trim(),
4452
+ "max-notes": Number(maxNotesInput.value || 50) || 50,
4453
+ env: envSelect.value,
4454
+ "do-comments": commentsInput.checked,
4455
+ "do-likes": likesInput.checked,
4456
+ "like-keywords": likeKeywordsInput.value.trim(),
4457
+ headless: headlessInput.checked,
4458
+ "dry-run": dryRunInput.checked
4459
+ };
4460
+ if (commandType.startsWith("weibo")) {
4461
+ argv["task-type"] = commandTypeToWeiboTaskType2(commandType);
4462
+ argv["user-id"] = userIdInput.value.trim();
4463
+ }
4464
+ return {
4465
+ id: editingIdInput.value.trim(),
4466
+ name: nameInput.value.trim(),
4467
+ enabled: enabledInput.checked,
4468
+ commandType,
4469
+ scheduleType: typeSelect.value,
4470
+ intervalMinutes: Number(intervalInput.value || 30) || 30,
4471
+ runAt: toIsoOrNull(runAtInput.value),
4472
+ maxRuns,
4473
+ argv
4474
+ };
4475
+ }
4476
+ function applyTaskToForm(task) {
4477
+ pendingFocusTaskId = "";
4478
+ const platform = getPlatformForCommandType(String(task.commandType || "xhs-unified"));
4479
+ platformSelect.value = platform;
4480
+ updateTaskTypeOptions();
4481
+ taskTypeSelect.value = String(task.commandType || taskTypeSelect.value || "xhs-unified");
4482
+ editingIdInput.value = task.id;
4483
+ nameInput.value = task.name || "";
4484
+ enabledInput.checked = task.enabled !== false;
4485
+ typeSelect.value = task.scheduleType;
4486
+ intervalInput.value = String(task.intervalMinutes || 30);
4487
+ runAtInput.value = toLocalDatetimeValue(task.runAt);
4488
+ maxRunsInput.value = task.maxRuns ? String(task.maxRuns) : "";
4489
+ profileInput.value = String(task.commandArgv?.profile || "");
4490
+ keywordInput.value = String(task.commandArgv?.keyword || task.commandArgv?.k || "");
4491
+ userIdInput.value = String(task.commandArgv?.["user-id"] || task.commandArgv?.userId || "");
4492
+ maxNotesInput.value = String(task.commandArgv?.["max-notes"] ?? task.commandArgv?.target ?? 50);
4493
+ envSelect.value = String(task.commandArgv?.env || "debug");
4494
+ commentsInput.checked = task.commandArgv?.["do-comments"] !== false;
4495
+ likesInput.checked = task.commandArgv?.["do-likes"] === true;
4496
+ headlessInput.checked = task.commandArgv?.headless === true;
4497
+ dryRunInput.checked = task.commandArgv?.["dry-run"] === true;
4498
+ likeKeywordsInput.value = String(task.commandArgv?.["like-keywords"] || "");
4499
+ setActiveTaskContext(task.id);
4500
+ updatePlatformFields();
4501
+ updateTypeFields();
4502
+ }
4503
+ async function runScheduleJson(args, timeoutMs = 6e4) {
4504
+ const script = ctx2.api.pathJoin("apps", "webauto", "entry", "schedule.mjs");
4505
+ const ret = await ctx2.api.cmdRunJson({
4506
+ title: `schedule ${args.join(" ")}`,
4507
+ cwd: "",
4508
+ args: [script, ...args, "--json"],
4509
+ timeoutMs
4510
+ });
4511
+ if (!ret?.ok) {
4512
+ const reason = String(ret?.error || ret?.stderr || ret?.stdout || "unknown_error").trim();
4513
+ throw new Error(reason || "schedule command failed");
4514
+ }
4515
+ return ret.json || {};
4516
+ }
4517
+ function downloadJson(fileName, payload) {
4518
+ const text = JSON.stringify(payload, null, 2);
4519
+ const blob = new Blob([text], { type: "application/json" });
4520
+ const url = URL.createObjectURL(blob);
4521
+ const a = document.createElement("a");
4522
+ a.href = url;
4523
+ a.download = fileName;
4524
+ a.click();
4525
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
4526
+ }
4527
+ function renderTaskList() {
4528
+ listEl.innerHTML = "";
4529
+ if (tasks.length === 0) {
4530
+ listEl.innerHTML = '<div class="muted" style="padding: 12px;">\u6682\u65E0\u4EFB\u52A1</div>';
4531
+ return;
4532
+ }
4533
+ for (const task of tasks) {
4534
+ const card = createEl("div", {
4535
+ style: "border:1px solid var(--border); border-radius:10px; padding:10px; margin-bottom:10px; background: var(--panel-soft);"
4536
+ });
4537
+ const scheduleText = task.scheduleType === "once" ? `once @ ${task.runAt || "-"}` : task.scheduleType === "daily" ? `daily @ ${task.runAt || "-"}` : task.scheduleType === "weekly" ? `weekly @ ${task.runAt || "-"}` : `interval ${task.intervalMinutes}m`;
4538
+ const statusText = task.lastStatus ? `${task.lastStatus} / run=${task.runCount} / fail=${task.failCount}` : "never run";
4539
+ const headRow = createEl("div", { style: "display:flex; justify-content:space-between; gap:8px; margin-bottom:6px;" });
4540
+ headRow.appendChild(createEl("div", { style: "font-weight:600;" }, [task.name || task.id]));
4541
+ headRow.appendChild(
4542
+ createEl(
4543
+ "span",
4544
+ { style: `font-size:12px; color:${task.enabled ? "var(--accent-success)" : "var(--accent-danger)"};` },
4545
+ [task.enabled ? "enabled" : "disabled"]
4546
+ )
4547
+ );
4548
+ card.appendChild(headRow);
4549
+ card.appendChild(createEl("div", { className: "muted", style: "font-size:12px; margin-bottom:4px;" }, [`id=${task.id}`]));
4550
+ card.appendChild(createEl("div", { style: "font-size:12px;" }, [`schedule: ${scheduleText}`]));
4551
+ card.appendChild(createEl("div", { style: "font-size:12px;" }, [`maxRuns: ${task.maxRuns || "unlimited"}`]));
4552
+ card.appendChild(createEl("div", { style: "font-size:12px;" }, [`nextRunAt: ${task.nextRunAt || "-"}`]));
4553
+ card.appendChild(createEl("div", { style: "font-size:12px;" }, [`status: ${statusText}`]));
4554
+ const recent = (task.runHistory || []).slice(-5);
4555
+ if (recent.length > 0) {
4556
+ const recentRow = createEl("div", { style: "font-size:12px;" }, ["recent: "]);
4557
+ for (const h of recent) {
4558
+ const icon = h.status === "success" ? "\u2705" : "\u274C";
4559
+ const duration = h.durationMs ? `${Math.round(h.durationMs / 1e3)}s` : "-";
4560
+ const badge = createEl("span", { title: `${h.timestamp} ${duration}` }, [icon]);
4561
+ recentRow.appendChild(badge);
4562
+ recentRow.appendChild(document.createTextNode(" "));
4563
+ }
4564
+ card.appendChild(recentRow);
4565
+ }
4566
+ if (task.lastError) {
4567
+ card.appendChild(createEl("div", { style: "font-size:12px; color:var(--accent-danger);" }, [`error: ${task.lastError}`]));
4568
+ }
4569
+ const actions = createEl("div", { className: "btn-group", style: "margin-top: 8px;" });
4570
+ const editBtn = createEl("button", { className: "secondary" }, ["\u7F16\u8F91"]);
4571
+ const loadBtn = createEl("button", { className: "secondary" }, ["\u8F7D\u5165\u914D\u7F6E"]);
4572
+ const runBtn = createEl("button", { className: "secondary" }, ["\u6267\u884C"]);
4573
+ const exportBtn = createEl("button", { className: "secondary" }, ["\u5BFC\u51FA"]);
4574
+ const delBtn = createEl("button", { className: "danger" }, ["\u5220\u9664"]);
4575
+ actions.appendChild(editBtn);
4576
+ actions.appendChild(loadBtn);
4577
+ actions.appendChild(runBtn);
4578
+ actions.appendChild(exportBtn);
4579
+ actions.appendChild(delBtn);
4580
+ card.appendChild(actions);
4581
+ editBtn.onclick = () => applyTaskToForm(task);
4582
+ loadBtn.onclick = () => openConfigTab(task.id);
4583
+ runBtn.onclick = async () => {
4584
+ try {
4585
+ setActiveTaskContext(task.id);
4586
+ const out = await runScheduleJson(["run", task.id], 0);
4587
+ const runId = String(
4588
+ out?.result?.runResult?.lastRunId || out?.result?.runResult?.runId || out?.runResult?.runId || ""
4589
+ ).trim();
4590
+ if (task.commandType === "xhs-unified" && ctx2 && typeof ctx2 === "object") {
4591
+ const argv = task.commandArgv || {};
4592
+ ctx2.xhsCurrentRun = {
4593
+ runId: runId || null,
4594
+ taskId: task.id,
4595
+ profileId: String(argv.profile || ""),
4596
+ keyword: String(argv.keyword || argv.k || ""),
4597
+ target: Number(argv["max-notes"] || argv.target || 0) || 0,
4598
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
4599
+ };
4600
+ }
4601
+ if (typeof ctx2.setStatus === "function") {
4602
+ ctx2.setStatus(`running: ${task.id}`);
4603
+ }
4604
+ if (task.commandType === "xhs-unified" && typeof ctx2.setActiveTab === "function") {
4605
+ ctx2.setActiveTab("dashboard");
4606
+ }
4607
+ await refreshList();
4608
+ } catch (err) {
4609
+ alert(`\u6267\u884C\u5931\u8D25: ${err?.message || String(err)}`);
4610
+ }
4611
+ };
4612
+ exportBtn.onclick = async () => {
4613
+ try {
4614
+ const out = await runScheduleJson(["export", task.id]);
4615
+ downloadJson(`${task.id}.json`, out);
4616
+ } catch (err) {
4617
+ alert(`\u5BFC\u51FA\u5931\u8D25: ${err?.message || String(err)}`);
4618
+ }
4619
+ };
4620
+ delBtn.onclick = async () => {
4621
+ if (!confirm(`\u786E\u8BA4\u5220\u9664\u4EFB\u52A1 ${task.id} ?`)) return;
4622
+ try {
4623
+ await runScheduleJson(["delete", task.id]);
4624
+ await refreshList();
4625
+ } catch (err) {
4626
+ alert(`\u5220\u9664\u5931\u8D25: ${err?.message || String(err)}`);
4627
+ }
4628
+ };
4629
+ listEl.appendChild(card);
4630
+ }
4631
+ }
4632
+ async function refreshList() {
4633
+ const out = await runScheduleJson(["list"]);
4634
+ tasks = parseTaskRows(out);
4635
+ if (!pendingFocusTaskId) {
4636
+ pendingFocusTaskId = String(ctx2?.activeTaskConfigId || "").trim();
4637
+ }
4638
+ if (pendingFocusTaskId) {
4639
+ const target = tasks.find((item) => item.id === pendingFocusTaskId);
4640
+ if (target) {
4641
+ applyTaskToForm(target);
4642
+ } else {
4643
+ setActiveTaskContext("");
4644
+ }
4645
+ pendingFocusTaskId = "";
4646
+ } else {
4647
+ setActiveTaskContext(String(ctx2?.activeTaskConfigId || "").trim());
4648
+ }
4649
+ renderTaskList();
4650
+ }
4651
+ async function saveTask() {
4652
+ const payload = readFormAsPayload();
4653
+ if (!payload.name) {
4654
+ alert("\u4EFB\u52A1\u540D\u4E0D\u80FD\u4E3A\u7A7A");
4655
+ return;
4656
+ }
4657
+ if (!payload.argv.profile && !payload.argv.profiles && !payload.argv.profilepool) {
4658
+ alert("profile/profiles/profilepool \u81F3\u5C11\u586B\u5199\u4E00\u4E2A");
4659
+ return;
4660
+ }
4661
+ const commandType = String(payload.commandType || "").trim();
4662
+ const keywordRequired = commandType === "xhs-unified" || commandType === "weibo-search" || commandType === "1688-search";
4663
+ if (keywordRequired && !payload.argv.keyword) {
4664
+ alert("\u5173\u952E\u8BCD\u4E0D\u80FD\u4E3A\u7A7A");
4665
+ return;
4666
+ }
4667
+ if (commandType === "weibo-monitor" && !payload.argv["user-id"]) {
4668
+ alert("\u5FAE\u535A monitor \u4EFB\u52A1\u9700\u8981 user-id");
4669
+ return;
4670
+ }
4671
+ const args = payload.id ? ["update", payload.id] : ["add"];
4672
+ args.push("--name", payload.name);
4673
+ args.push("--enabled", String(payload.enabled));
4674
+ args.push("--command-type", commandType || "xhs-unified");
4675
+ args.push("--schedule-type", payload.scheduleType);
4676
+ if (payload.scheduleType === "once") {
4677
+ if (!payload.runAt) {
4678
+ alert("\u4E00\u6B21\u6027\u4EFB\u52A1\u9700\u8981\u951A\u70B9\u65F6\u95F4");
4679
+ return;
4680
+ }
4681
+ args.push("--run-at", payload.runAt);
4682
+ } else if (payload.scheduleType === "daily" || payload.scheduleType === "weekly") {
4683
+ if (!payload.runAt) {
4684
+ alert(`${payload.scheduleType} \u4EFB\u52A1\u9700\u8981\u951A\u70B9\u65F6\u95F4`);
4685
+ return;
4686
+ }
4687
+ args.push("--run-at", payload.runAt);
4688
+ } else {
4689
+ args.push("--interval-minutes", String(Math.max(1, payload.intervalMinutes)));
4690
+ }
4691
+ args.push("--max-runs", payload.maxRuns === null ? "0" : String(payload.maxRuns));
4692
+ args.push("--argv-json", JSON.stringify(payload.argv));
4693
+ try {
4694
+ const out = await runScheduleJson(args);
4695
+ const savedId = String(out?.task?.id || payload.id || "").trim();
4696
+ pendingFocusTaskId = savedId;
4697
+ if (savedId) setActiveTaskContext(savedId);
4698
+ await refreshList();
4699
+ } catch (err) {
4700
+ alert(`\u4FDD\u5B58\u5931\u8D25: ${err?.message || String(err)}`);
4701
+ }
4702
+ }
4703
+ async function runDueNow() {
4704
+ try {
4705
+ const out = await runScheduleJson(["run-due", "--limit", "20"], 0);
4706
+ alert(`\u5230\u70B9\u4EFB\u52A1\u6267\u884C\u5B8C\u6210\uFF1Adue=${out.count || 0}, success=${out.success || 0}, failed=${out.failed || 0}`);
4707
+ await refreshList();
4708
+ } catch (err) {
4709
+ alert(`\u6267\u884C\u5931\u8D25: ${err?.message || String(err)}`);
4710
+ }
4711
+ }
4712
+ async function exportAll() {
4713
+ try {
4714
+ const out = await runScheduleJson(["export"]);
4715
+ downloadJson("webauto-schedules.json", out);
4716
+ } catch (err) {
4717
+ alert(`\u5BFC\u51FA\u5931\u8D25: ${err?.message || String(err)}`);
4718
+ }
4719
+ }
4720
+ async function importFromFile() {
4721
+ const input = document.createElement("input");
4722
+ input.type = "file";
4723
+ input.accept = ".json";
4724
+ input.onchange = async (evt) => {
4725
+ const file = evt.target.files?.[0];
4726
+ if (!file) return;
4727
+ try {
4728
+ const text = await file.text();
4729
+ await runScheduleJson(["import", "--payload-json", text, "--mode", "merge"]);
4730
+ await refreshList();
4731
+ } catch (err) {
4732
+ alert(`\u5BFC\u5165\u5931\u8D25: ${err?.message || String(err)}`);
4733
+ }
4734
+ };
4735
+ input.click();
4736
+ }
4737
+ async function startDaemon() {
4738
+ if (daemonRunId) {
4739
+ alert("daemon \u5DF2\u542F\u52A8");
4740
+ return;
4741
+ }
4742
+ const interval = Math.max(5, Number(daemonIntervalInput.value || 30) || 30);
4743
+ const script = ctx2.api.pathJoin("apps", "webauto", "entry", "schedule.mjs");
4744
+ const ret = await ctx2.api.cmdSpawn({
4745
+ title: `schedule daemon ${interval}s`,
4746
+ cwd: "",
4747
+ args: [script, "daemon", "--interval-sec", String(interval), "--limit", "20", "--json"],
4748
+ groupKey: "scheduler"
4749
+ });
4750
+ daemonRunId = String(ret?.runId || "").trim();
4751
+ setDaemonStatus(daemonRunId ? `daemon: \u8FD0\u884C\u4E2D (${daemonRunId})` : "daemon: \u542F\u52A8\u5931\u8D25");
4752
+ }
4753
+ async function stopDaemon() {
4754
+ if (!daemonRunId) {
4755
+ setDaemonStatus("daemon: \u672A\u542F\u52A8");
4756
+ return;
4757
+ }
4758
+ try {
4759
+ await ctx2.api.cmdKill({ runId: daemonRunId });
4760
+ } catch {
4761
+ }
4762
+ daemonRunId = "";
4763
+ setDaemonStatus("daemon: \u5DF2\u505C\u6B62");
4764
+ }
4765
+ platformSelect.addEventListener("change", updateTaskTypeOptions);
4766
+ taskTypeSelect.addEventListener("change", updatePlatformFields);
4767
+ typeSelect.addEventListener("change", updateTypeFields);
4768
+ saveBtn.onclick = () => void saveTask();
4769
+ resetBtn.onclick = () => resetForm();
4770
+ refreshBtn.onclick = () => void refreshList();
4771
+ runDueBtn.onclick = () => void runDueNow();
4772
+ exportAllBtn.onclick = () => void exportAll();
4773
+ importBtn.onclick = () => void importFromFile();
4774
+ openConfigBtn.onclick = () => {
4775
+ const id = String(editingIdInput.value || activeTaskIdText.textContent || "").trim();
4776
+ openConfigTab(id);
4777
+ };
4778
+ daemonStartBtn.onclick = () => void startDaemon();
4779
+ daemonStopBtn.onclick = () => void stopDaemon();
4780
+ if (typeof ctx2.api?.onCmdEvent === "function") {
4781
+ unsubscribeCmd = ctx2.api.onCmdEvent((evt) => {
4782
+ const runId = String(evt?.runId || "").trim();
4783
+ if (!daemonRunId || runId !== daemonRunId) return;
4784
+ if (evt?.type === "exit") {
4785
+ daemonRunId = "";
4786
+ setDaemonStatus("daemon: \u5DF2\u9000\u51FA");
4787
+ }
4788
+ });
4789
+ }
4790
+ resetForm();
4791
+ updateTaskTypeOptions();
4792
+ void refreshList().catch((err) => {
4793
+ listEl.innerHTML = `<div class="muted" style="padding: 12px;">\u52A0\u8F7D\u5931\u8D25: ${err?.message || String(err)}</div>`;
4794
+ });
4795
+ return () => {
4796
+ if (unsubscribeCmd) {
4797
+ try {
4798
+ unsubscribeCmd();
4799
+ } catch {
4800
+ }
4801
+ unsubscribeCmd = null;
4802
+ }
4803
+ };
2905
4804
  }
2906
4805
 
2907
4806
  // src/renderer/index.mts
2908
4807
  var tabs = [
2909
4808
  { id: "setup-wizard", label: "\u521D\u59CB\u5316", render: renderSetupWizard },
2910
- { id: "config", label: "\u914D\u7F6E", render: renderConfigPanel },
4809
+ { id: "tasks", label: "\u4EFB\u52A1", render: renderTasksPanel },
2911
4810
  { id: "dashboard", label: "\u770B\u677F", render: renderDashboard },
4811
+ { id: "scheduler", label: "\u5B9A\u65F6\u4EFB\u52A1", render: renderSchedulerPanel },
2912
4812
  { id: "account-manager", label: "\u8D26\u6237\u7BA1\u7406", render: renderAccountManager },
2913
4813
  { id: "preflight", label: "\u65E7\u9884\u5904\u7406", render: renderPreflight, hidden: true },
2914
4814
  { id: "logs", label: "\u65E5\u5FD7", render: renderLogs },
@@ -2918,8 +4818,19 @@ var tabsEl = document.getElementById("tabs");
2918
4818
  var contentEl = document.getElementById("content");
2919
4819
  var statusEl = document.getElementById("status");
2920
4820
  var activeTabCleanup = null;
4821
+ var mutableApi = { ...window.api || {}, settings: null };
4822
+ var tabIcons = {
4823
+ "setup-wizard": "\u26A1",
4824
+ "tasks": "\u{1F4DD}",
4825
+ "dashboard": "\u{1F4CA}",
4826
+ "scheduler": "\u23F0",
4827
+ "account-manager": "\u{1F464}",
4828
+ "preflight": "\u{1F527}",
4829
+ "logs": "\u{1F4DD}",
4830
+ "settings": "\u{1F528}"
4831
+ };
2921
4832
  var ctx = {
2922
- api: window.api,
4833
+ api: mutableApi,
2923
4834
  settings: null,
2924
4835
  xhsCurrentRun: null,
2925
4836
  activeRunId: null,
@@ -2981,6 +4892,10 @@ function startDesktopHeartbeat() {
2981
4892
  async function loadSettings() {
2982
4893
  await ctx.refreshSettings();
2983
4894
  }
4895
+ function focusTabButton(tabId) {
4896
+ const button = tabsEl.querySelector(`[data-tab-id="${tabId}"]`);
4897
+ button?.focus();
4898
+ }
2984
4899
  function setActiveTab(id) {
2985
4900
  if (activeTabCleanup) {
2986
4901
  try {
@@ -2989,13 +4904,54 @@ function setActiveTab(id) {
2989
4904
  }
2990
4905
  activeTabCleanup = null;
2991
4906
  }
4907
+ const visibleTabs = tabs.filter((x) => !x.hidden);
4908
+ tabsEl.setAttribute("role", "tablist");
2992
4909
  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]);
4910
+ for (let index = 0; index < visibleTabs.length; index += 1) {
4911
+ const t = visibleTabs[index];
4912
+ const isActive = t.id === id;
4913
+ const icon = tabIcons[t.id] || "";
4914
+ const el = createEl("button", { className: `tab ${isActive ? "active" : ""}`, type: "button" }, [
4915
+ createEl("span", { className: "tab-icon" }, [icon]),
4916
+ t.label
4917
+ ]);
4918
+ el.dataset.tabId = t.id;
4919
+ el.setAttribute("role", "tab");
4920
+ el.setAttribute("aria-selected", String(isActive));
4921
+ el.tabIndex = isActive ? 0 : -1;
2995
4922
  el.addEventListener("click", () => setActiveTab(t.id));
4923
+ el.addEventListener("keydown", (evt) => {
4924
+ const key = evt.key;
4925
+ if (key === "Enter" || key === " ") {
4926
+ evt.preventDefault();
4927
+ setActiveTab(t.id);
4928
+ return;
4929
+ }
4930
+ let nextIndex = -1;
4931
+ if (key === "ArrowRight") nextIndex = (index + 1) % visibleTabs.length;
4932
+ else if (key === "ArrowLeft") nextIndex = (index - 1 + visibleTabs.length) % visibleTabs.length;
4933
+ else if (key === "Home") nextIndex = 0;
4934
+ else if (key === "End") nextIndex = visibleTabs.length - 1;
4935
+ if (nextIndex >= 0) {
4936
+ evt.preventDefault();
4937
+ const nextTab = visibleTabs[nextIndex];
4938
+ if (!nextTab) return;
4939
+ setActiveTab(nextTab.id);
4940
+ requestAnimationFrame(() => focusTabButton(nextTab.id));
4941
+ }
4942
+ });
2996
4943
  tabsEl.appendChild(el);
2997
4944
  }
2998
4945
  contentEl.textContent = "";
4946
+ const reducedMotion = typeof window.matchMedia === "function" ? window.matchMedia("(prefers-reduced-motion: reduce)").matches : false;
4947
+ if (!reducedMotion) {
4948
+ contentEl.classList.remove("animate-fade-in");
4949
+ if (typeof requestAnimationFrame === "function") {
4950
+ requestAnimationFrame(() => contentEl.classList.add("animate-fade-in"));
4951
+ } else {
4952
+ contentEl.classList.add("animate-fade-in");
4953
+ }
4954
+ }
2999
4955
  const tab = tabs.find((x) => x.id === id);
3000
4956
  const dispose = tab.render(contentEl, ctx);
3001
4957
  if (typeof dispose === "function") activeTabCleanup = dispose;
@@ -3035,10 +4991,8 @@ function installCmdEvents() {
3035
4991
  async function detectStartupTab() {
3036
4992
  try {
3037
4993
  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
4994
  const envReady = Boolean(env?.allReady);
3041
- if (envReady && hasAccount) return "config";
4995
+ if (envReady) return "tasks";
3042
4996
  } catch {
3043
4997
  }
3044
4998
  return "setup-wizard";