@moretea-labs/forge 1.5.0-rc.1 → 1.5.1

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 (264) hide show
  1. package/AGENTS.md +9 -11
  2. package/CHANGELOG.md +27 -1
  3. package/CLAUDE.md +8 -10
  4. package/DESIGN.md +278 -0
  5. package/README.md +70 -90
  6. package/README.zh-CN.md +52 -55
  7. package/SKILL.md +8 -11
  8. package/THIRD_PARTY_NOTICES.md +9 -3
  9. package/assets/branding/README.md +4 -2
  10. package/assets/hooks/lib/workflow-state.sh +19 -18
  11. package/assets/hooks/post-edit-guard.sh +6 -75
  12. package/assets/hooks/prompt-guard.sh +24 -12
  13. package/assets/hooks/session-start-context.sh +17 -17
  14. package/assets/hooks/stop-orchestrator.sh +2 -2
  15. package/assets/partials/04-project-structure.partial.md +6 -6
  16. package/assets/partials/05-workflow.partial.md +6 -6
  17. package/assets/partials/07-footer.partial.md +1 -1
  18. package/assets/partials/08-orchestration.partial.md +2 -2
  19. package/assets/partials-agents/02-operating-mode.partial.md +5 -5
  20. package/assets/partials-agents/03-orchestration.partial.md +3 -3
  21. package/assets/partials-agents/04-task-protocol.partial.md +3 -3
  22. package/assets/partials-agents/08-deep-docs.partial.md +1 -1
  23. package/assets/reference-configs/agentic-development-flow.md +49 -86
  24. package/assets/reference-configs/external-tooling.md +21 -61
  25. package/assets/reference-configs/handoff-protocol.md +25 -20
  26. package/assets/reference-configs/harness-overview.md +8 -8
  27. package/assets/skill-version.json +2 -2
  28. package/assets/templates/helpers/archive-workflow.sh +0 -1
  29. package/assets/templates/helpers/capture-plan.sh +3 -3
  30. package/assets/templates/helpers/check-task-workflow.sh +2 -2
  31. package/assets/templates/helpers/codex-handoff-resume.sh +5 -5
  32. package/assets/templates/helpers/ensure-task-workflow.sh +37 -35
  33. package/assets/templates/helpers/new-plan.sh +1 -1
  34. package/assets/templates/helpers/plan-to-todo.sh +2 -2
  35. package/assets/templates/helpers/prepare-codex-handoff.sh +2 -2
  36. package/assets/templates/helpers/prepare-handoff.sh +9 -9
  37. package/assets/templates/helpers/refresh-current-status.sh +2 -2
  38. package/assets/templates/helpers/ship-worktrees.sh +1 -1
  39. package/assets/templates/helpers/sprint-backlog.sh +1 -1
  40. package/assets/templates/helpers/verify-sprint.sh +7 -7
  41. package/assets/templates/helpers/workstream-sync.sh +1 -1
  42. package/assets/templates/plan.template.md +1 -1
  43. package/assets/workflow-contract.v1.json +6 -3
  44. package/docs/architecture/current/README.md +10 -9
  45. package/docs/architecture/current/approved-target-architecture.zh-CN.md +2 -2
  46. package/docs/architecture/current/architecture-invariants.md +2 -2
  47. package/docs/architecture/current/automation-and-schedule-engine.md +15 -15
  48. package/docs/architecture/current/codegraph-context-provider.md +2 -2
  49. package/docs/architecture/current/control-plane-authority-inventory.md +12 -0
  50. package/docs/architecture/current/dispatch-and-agent-strategy.md +11 -11
  51. package/docs/architecture/current/entity-model.md +0 -27
  52. package/docs/architecture/current/failure-recovery.md +0 -24
  53. package/docs/architecture/current/governance.md +1 -1
  54. package/docs/architecture/current/implementation-status.md +1 -1
  55. package/docs/architecture/current/legacy-governance.md +58 -0
  56. package/docs/architecture/current/multi-repository-execution.md +1 -1
  57. package/docs/architecture/current/peer-supercontroller-runtime.md +1 -1
  58. package/docs/architecture/current/personal-assistant-plugin-baseline.md +4 -5
  59. package/docs/architecture/current/scheduler-and-resource-claims.md +0 -10
  60. package/docs/architecture/current/target-requirements-traceability.md +2 -2
  61. package/docs/architecture/current/thin-harness-governance-policy.md +1 -1
  62. package/docs/architecture/current/thin-harness-v1.md +9 -11
  63. package/docs/architecture/domains/runtime-harness.md +1 -1
  64. package/docs/forge-autonomous-goal-loop.md +3 -1
  65. package/docs/forge-chatgpt-mcp-setup.md +67 -176
  66. package/docs/images/forge-banner-cn.svg +26 -1
  67. package/docs/images/forge-banner.svg +26 -1
  68. package/docs/operations/features.md +1 -1
  69. package/docs/operations/homebrew.md +3 -3
  70. package/docs/operations/mcp-tool-exposure.md +5 -5
  71. package/docs/operations/platform-support.md +45 -37
  72. package/docs/operations/platform-support.zh-CN.md +40 -38
  73. package/docs/operations/releasing.md +3 -3
  74. package/docs/operations/releasing.zh-CN.md +3 -3
  75. package/docs/operations/troubleshooting.md +11 -6
  76. package/docs/operations/troubleshooting.zh-CN.md +11 -6
  77. package/docs/public-usage-guide.md +28 -21
  78. package/docs/public-usage-guide.zh-CN.md +28 -21
  79. package/docs/tutorials/01-install-and-start.md +58 -35
  80. package/docs/tutorials/01-install-and-start.zh-CN.md +58 -35
  81. package/docs/tutorials/02-connect-chatgpt.md +118 -46
  82. package/docs/tutorials/02-connect-chatgpt.zh-CN.md +119 -45
  83. package/docs/wiki/Architecture.md +1 -1
  84. package/docs/wiki/Installation.md +2 -2
  85. package/docs/wiki/Quick-Start.md +1 -1
  86. package/docs/wiki/Work-Lifecycle.md +1 -1
  87. package/install.ps1 +4 -5
  88. package/install.sh +4 -6
  89. package/package.json +15 -5
  90. package/scripts/archive-workflow.sh +0 -1
  91. package/scripts/benchmark-thin-harness-mcp-e2e.ts +27 -14
  92. package/scripts/build-controller-ui.ts +28 -0
  93. package/scripts/capture-plan.sh +4 -4
  94. package/scripts/check-agent-tooling.sh +12 -1
  95. package/scripts/check-platform-support.mjs +2 -2
  96. package/scripts/check-public-docs.sh +16 -5
  97. package/scripts/check-release-readiness.sh +5 -11
  98. package/scripts/check-runtime-architecture.mjs +14 -22
  99. package/scripts/check-tarball-install-smoke.sh +3 -2
  100. package/scripts/check-task-workflow.sh +2 -2
  101. package/scripts/codex-handoff-resume.sh +5 -5
  102. package/scripts/create-project-dirs.sh +3 -3
  103. package/scripts/ensure-task-workflow.sh +45 -35
  104. package/scripts/init-project.sh +4 -4
  105. package/scripts/install-desktop-operator-registration.ts +2 -2
  106. package/scripts/lib/project-init-lib.sh +46 -44
  107. package/scripts/migrate-project-template.sh +7 -4
  108. package/scripts/new-plan.sh +1 -1
  109. package/scripts/plan-to-todo.sh +2 -2
  110. package/scripts/prepare-codex-handoff.sh +2 -2
  111. package/scripts/prepare-handoff.sh +9 -9
  112. package/scripts/refresh-current-status.sh +2 -2
  113. package/scripts/setup-plugins.sh +3 -3
  114. package/scripts/ship-worktrees.sh +1 -1
  115. package/scripts/sprint-backlog.sh +5 -5
  116. package/scripts/sync-codex-installed-copies.sh +2 -3
  117. package/scripts/verify-sprint.sh +7 -7
  118. package/scripts/workstream-sync.sh +1 -1
  119. package/src/cli/chatgpt-browser/bridge-extension.ts +1 -1
  120. package/src/cli/chatgpt-browser/bridge-provider.ts +29 -0
  121. package/src/cli/chatgpt-browser/file-policy.ts +3 -1
  122. package/src/cli/commands/chatgpt.ts +51 -0
  123. package/src/cli/commands/doctor.ts +1 -1
  124. package/src/cli/commands/init-hook.ts +213 -70
  125. package/src/cli/commands/init.ts +5 -6
  126. package/src/cli/commands/mcp.ts +31 -5
  127. package/src/cli/commands/runtime.ts +61 -8
  128. package/src/cli/commands/setup-profile.ts +472 -0
  129. package/src/cli/controller/check-runner.ts +20 -3
  130. package/src/cli/controller/codex-continuation.ts +4 -7
  131. package/src/cli/controller/context-pack.ts +118 -14
  132. package/src/cli/controller/operational-plan.ts +4 -4
  133. package/src/cli/controller/task-ledger.ts +10 -10
  134. package/src/cli/controller/work-mode.ts +6 -13
  135. package/src/cli/index.ts +14 -35
  136. package/src/cli/local-bridge/connector-freshness.ts +1 -1
  137. package/src/cli/local-bridge/console-assets.ts +17 -0
  138. package/src/cli/local-bridge/console-automations.ts +160 -0
  139. package/src/cli/local-bridge/dashboard.ts +8 -1303
  140. package/src/cli/local-bridge/facade-api.ts +16 -7
  141. package/src/cli/local-bridge/frontend/src/App.tsx +10 -0
  142. package/src/cli/local-bridge/frontend/src/components/Button.tsx +1 -0
  143. package/src/cli/local-bridge/frontend/src/components/DetailPane.tsx +3 -0
  144. package/src/cli/local-bridge/frontend/src/components/SearchField.tsx +1 -0
  145. package/src/cli/local-bridge/frontend/src/components/SectionHeader.tsx +1 -0
  146. package/src/cli/local-bridge/frontend/src/components/Segmented.tsx +1 -0
  147. package/src/cli/local-bridge/frontend/src/components/StatusText.tsx +3 -0
  148. package/src/cli/local-bridge/frontend/src/design/icons.tsx +11 -0
  149. package/src/cli/local-bridge/frontend/src/design/tokens.css +8 -0
  150. package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +3 -0
  151. package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +4 -0
  152. package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +3 -0
  153. package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +2 -0
  154. package/src/cli/local-bridge/frontend/src/features/settings/SettingsView.tsx +2 -0
  155. package/src/cli/local-bridge/frontend/src/features/system/SystemView.tsx +2 -0
  156. package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +3 -0
  157. package/src/cli/local-bridge/frontend/src/lib/api.ts +23 -0
  158. package/src/cli/local-bridge/frontend/src/lib/format.ts +4 -0
  159. package/src/cli/local-bridge/frontend/src/lib/routes.ts +7 -0
  160. package/src/cli/local-bridge/frontend/src/main.tsx +1 -0
  161. package/src/cli/local-bridge/frontend/src/shell/AppShell.tsx +1 -0
  162. package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +2 -0
  163. package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +3 -0
  164. package/src/cli/local-bridge/frontend/src/styles.css +12 -0
  165. package/src/cli/local-bridge/frontend/src/types.ts +14 -0
  166. package/src/cli/local-bridge/frontend/src/vite-env.d.ts +1 -0
  167. package/src/cli/local-bridge/frontend/vite.config.ts +27 -0
  168. package/src/cli/local-bridge/server.ts +45 -0
  169. package/src/cli/local-bridge/ui-dist/app.css +1 -0
  170. package/src/cli/local-bridge/ui-dist/app.js +11094 -0
  171. package/src/cli/mcp/auth.ts +2 -0
  172. package/src/cli/mcp/instructions.ts +3 -3
  173. package/src/cli/mcp/legacy-tool-service.ts +27 -23
  174. package/src/cli/mcp/multi-repository.ts +2 -0
  175. package/src/cli/mcp/policy.ts +7 -5
  176. package/src/cli/mcp/repository-tools.ts +53 -14
  177. package/src/cli/mcp/server.ts +5 -10
  178. package/src/cli/mcp/setup.ts +220 -276
  179. package/src/cli/mcp/tool-capability-cache.ts +1 -1
  180. package/src/cli/mcp/toolset-names.ts +1 -1
  181. package/src/cli/mcp/toolset.ts +0 -1
  182. package/src/cli/mcp/transports/http.ts +51 -4
  183. package/src/cli/repositories/branch-name-policy.ts +0 -7
  184. package/src/cli/repositories/controller-home.ts +110 -3
  185. package/src/cli/repositories/safe-patch.ts +1 -1
  186. package/src/cli/repositories/selected-path-actions.ts +26 -24
  187. package/src/core/adoption/gitignore-plan.ts +2 -3
  188. package/src/core/adoption/plan.ts +4 -1
  189. package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +556 -0
  190. package/src/runtime/control-plane/execution/work-handle-store.ts +128 -1
  191. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +277 -0
  192. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +169 -0
  193. package/src/runtime/control-plane/facade/capability-registry.ts +0 -10
  194. package/src/runtime/control-plane/facade/handoff-inbox-store.ts +44 -25
  195. package/src/runtime/control-plane/facade/suggested-actions.ts +7 -1
  196. package/src/runtime/control-plane/facade/types.ts +0 -1
  197. package/src/runtime/control-plane/global-scheduler/scheduler.ts +30 -8
  198. package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +114 -0
  199. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +175 -0
  200. package/src/runtime/control-plane/launcher/launch-reservation-store.ts +161 -0
  201. package/src/runtime/control-plane/launcher/provider-mcp-bootstrap.ts +64 -0
  202. package/src/runtime/control-plane/launcher/thin-launcher.ts +117 -24
  203. package/src/runtime/control-plane/routing/route-policy.ts +14 -14
  204. package/src/runtime/control-plane/runtime-cleanup.ts +0 -1
  205. package/src/runtime/evidence/event-ledger.ts +1 -1
  206. package/src/runtime/{workflow/campaigns/workspace.ts → execution/managed-workspace.ts} +41 -43
  207. package/src/runtime/execution/process-runtime/check-facade.ts +6 -0
  208. package/src/runtime/execution/process-runtime/check-result.ts +45 -0
  209. package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +42 -16
  210. package/src/runtime/execution/process-runtime/command-facade.ts +4 -2
  211. package/src/runtime/execution/process-runtime/gc.ts +1 -1
  212. package/src/runtime/execution/process-runtime/index.ts +3 -0
  213. package/src/runtime/execution/process-runtime/interactive-admission.ts +14 -5
  214. package/src/runtime/execution/process-runtime/persisted-check.ts +285 -0
  215. package/src/runtime/execution/process-runtime/resource-claims.ts +30 -0
  216. package/src/runtime/execution/process-runtime/runtime.ts +18 -1
  217. package/src/runtime/execution/process-runtime/store.ts +1 -19
  218. package/src/runtime/execution/process-runtime/types.ts +4 -0
  219. package/src/runtime/execution/thin-harness/execution-router.ts +0 -1
  220. package/src/runtime/execution/thin-harness/fast-executor.ts +1 -1
  221. package/src/runtime/execution/thin-harness/lightweight-lanes.ts +1 -2
  222. package/src/runtime/execution/thin-harness/types.ts +0 -1
  223. package/src/runtime/execution/workers/executor.ts +1 -1
  224. package/src/runtime/gateway/mcp/execution-tools.ts +1 -1
  225. package/src/runtime/gateway/mcp/persisted-check-process.ts +3 -255
  226. package/src/runtime/gateway/mcp/process-tools.ts +16 -3
  227. package/src/runtime/gateway/mcp/resource-policy.ts +0 -9
  228. package/src/runtime/gateway/mcp/router.ts +117 -182
  229. package/src/runtime/gateway/mcp/runtime-tools.ts +331 -409
  230. package/src/runtime/gateway/mcp/work-validation-reconciler.ts +1 -219
  231. package/src/runtime/plugins/browser-adapter.ts +5 -0
  232. package/src/runtime/plugins/desktop-operator-registration.ts +1 -1
  233. package/src/runtime/plugins/store.ts +14 -6
  234. package/src/runtime/plugins/types.ts +1 -1
  235. package/src/runtime/projections/materialized-view.ts +1 -23
  236. package/src/runtime/recovery/maintenance-executor.ts +9 -0
  237. package/src/runtime/resources/ownership.ts +0 -1
  238. package/src/runtime/root/gateway-adapter.ts +2 -0
  239. package/src/runtime/root/mcp-transport.ts +10 -2
  240. package/src/runtime/root/package-connector-service.ts +136 -0
  241. package/src/runtime/root/package-runtime-release.ts +161 -0
  242. package/src/runtime/root/package-runtime-service.ts +185 -0
  243. package/src/runtime/root/release-materialize.ts +19 -0
  244. package/src/runtime/root/runtime.ts +2 -1
  245. package/src/runtime/root/scheduler.ts +5 -0
  246. package/src/runtime/root/service.ts +18 -5
  247. package/src/runtime/standalone-recovery/core.ts +65 -16
  248. package/src/runtime/workflow/findings/store.ts +145 -106
  249. package/src/runtime/workflow/findings/types.ts +22 -0
  250. package/src/runtime/workflow/schedules/engine.ts +132 -2
  251. package/src/runtime/workflow/schedules/pending-activation.ts +139 -0
  252. package/src/runtime/workflow/schedules/work-continuation.ts +218 -0
  253. package/docs/CHANGELOG.md +0 -978
  254. package/docs/README.zh-CN.md +0 -58
  255. package/scripts/smoke-chatgpt-supervised-automation.ts +0 -208
  256. package/src/cli/commands/controller.ts +0 -680
  257. package/src/runtime/workflow/campaigns/cleanup.ts +0 -263
  258. package/src/runtime/workflow/campaigns/engine.ts +0 -112
  259. package/src/runtime/workflow/campaigns/normalize.ts +0 -100
  260. package/src/runtime/workflow/campaigns/review.ts +0 -315
  261. package/src/runtime/workflow/campaigns/store.ts +0 -526
  262. package/src/runtime/workflow/campaigns/supervisor.ts +0 -112
  263. package/src/runtime/workflow/campaigns/types.ts +0 -290
  264. /package/src/runtime/{workflow/campaigns → execution}/workspace-agent.ts +0 -0
@@ -2,1311 +2,16 @@ export function localBridgeDashboardHtml(): string {
2
2
  return `<!doctype html>
3
3
  <html lang="zh-CN">
4
4
  <head>
5
- <meta charset="utf-8" />
6
- <meta name="viewport" content="width=device-width,initial-scale=1" />
7
- <title>forge · 执行助手控制台</title>
8
- <style>
9
- :root{color-scheme:dark;--bg:#070b10;--panel:#0f1620;--panel2:#131c28;--line:rgba(255,255,255,.08);--text:#f3f6f9;--muted:#95a0ab;--faint:#6d7784;--green:#34d399;--amber:#fbbf24;--red:#f87171;--blue:#60a5fa;--radius:16px}
10
- *{box-sizing:border-box}html,body{height:100%;margin:0}body{background:radial-gradient(circle at 80% -10%,rgba(52,211,153,.14),transparent 32%),linear-gradient(160deg,#05080c,#0a1118 55%,#06090d);color:var(--text);font:14px/1.5 Inter,"PingFang SC","Hiragino Sans GB","Microsoft YaHei",system-ui,sans-serif}
11
- button,input,textarea{font:inherit}button{cursor:pointer}.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
12
- .app{min-height:100vh;display:grid;grid-template-rows:64px 1fr}
13
- .top{display:flex;align-items:center;gap:12px;padding:0 18px;border-bottom:1px solid var(--line);background:rgba(8,12,18,.88);backdrop-filter:blur(16px);position:sticky;top:0;z-index:20}
14
- .brand{display:flex;align-items:center;gap:10px;font-weight:800;min-width:180px}
15
- .logo{width:28px;height:28px;border-radius:9px;display:grid;place-items:center;background:rgba(52,211,153,.12);color:var(--green);border:1px solid rgba(52,211,153,.28)}
16
- .top-actions{margin-left:auto;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
17
- .chip,.btn,.pill{border:1px solid var(--line);background:rgba(255,255,255,.04);color:var(--text);border-radius:12px;padding:8px 12px}
18
- .chip{display:inline-flex;align-items:center;gap:8px;min-height:36px}.chip strong{font-weight:700}
19
- .btn{min-height:38px}.btn.primary{background:linear-gradient(180deg,#2f9f6b,#1f7a51);border-color:rgba(52,211,153,.4)}.btn.danger{background:rgba(248,113,113,.12);border-color:rgba(248,113,113,.35);color:#fecaca}.btn.ghost{background:transparent}
20
- .dot{width:8px;height:8px;border-radius:50%;background:var(--green);box-shadow:0 0 0 4px rgba(52,211,153,.12)}.dot.amber{background:var(--amber);box-shadow:0 0 0 4px rgba(251,191,36,.12)}.dot.red{background:var(--red);box-shadow:0 0 0 4px rgba(248,113,113,.12)}.dot.blue{background:var(--blue);box-shadow:0 0 0 4px rgba(96,165,250,.12)}.dot.gray{background:#7b8794}
21
- .body{display:grid;grid-template-columns:220px 1fr;min-height:0}
22
- .side{border-right:1px solid var(--line);padding:16px 12px;background:rgba(8,12,18,.55)}
23
- .nav{display:grid;gap:6px}.nav button{text-align:left;width:100%;padding:11px 12px;border-radius:12px;border:1px solid transparent;background:transparent;color:var(--text)}.nav button.active,.nav button:hover{background:rgba(255,255,255,.05);border-color:var(--line)}
24
- .nav .count{margin-left:6px;display:inline-grid;place-items:center;min-width:20px;height:20px;border-radius:999px;background:rgba(248,113,113,.18);color:#fecaca;font-size:12px;font-weight:700}
25
- .main{padding:24px 28px 48px;overflow:auto}.main>.view{width:min(1240px,100%);margin:0 auto}
26
- .view{display:none}.view.active{display:block}
27
- .page-head{display:flex;justify-content:space-between;gap:12px;align-items:flex-start;margin-bottom:16px}.page-head h1{margin:0 0 4px;font-size:24px}.page-head p{margin:0;color:var(--muted)}
28
- .grid{display:grid;gap:14px}.grid.two{grid-template-columns:1.4fr .9fr}.grid.cards{grid-template-columns:repeat(3,1fr)}.grid.stats{grid-template-columns:repeat(4,minmax(0,1fr))}
29
- .panel{background:rgba(15,22,32,.92);border:1px solid var(--line);border-radius:var(--radius);padding:16px;box-shadow:0 18px 40px rgba(0,0,0,.22)}
30
- .panel h2,.panel h3{margin:0 0 8px}.muted{color:var(--muted)}.faint{color:var(--faint)}
31
- .stat strong{display:block;font-size:22px;margin-top:4px}.stat span{color:var(--muted);font-size:12px}
32
- .plugin-card{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;align-items:start}
33
- .action-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:10px;align-items:center;padding:10px 0;border-top:1px solid var(--line)}
34
- .action-row:first-child{border-top:0;padding-top:0}
35
- .composer textarea,.composer input,.input,textarea,input[type=text],input[type=number]{width:100%;border:1px solid var(--line);background:rgba(0,0,0,.22);color:var(--text);border-radius:12px;padding:12px 14px}
36
- .composer textarea{min-height:120px;resize:vertical}
37
- .composer .row{display:flex;gap:10px;flex-wrap:wrap;margin-top:10px;align-items:center}
38
- .mode-card{display:grid;gap:6px}.mode-card .label{font-weight:800;font-size:16px}
39
- .pill{display:inline-flex;align-items:center;gap:6px;padding:5px 10px;font-size:12px;font-weight:700;border-radius:999px}
40
- .pill.green{background:rgba(52,211,153,.12);color:#a7f3d0;border-color:rgba(52,211,153,.28)}
41
- .pill.amber{background:rgba(251,191,36,.12);color:#fde68a;border-color:rgba(251,191,36,.28)}
42
- .pill.red{background:rgba(248,113,113,.12);color:#fecaca;border-color:rgba(248,113,113,.28)}
43
- .pill.blue{background:rgba(96,165,250,.12);color:#bfdbfe;border-color:rgba(96,165,250,.28)}
44
- .pill.gray{background:rgba(255,255,255,.05);color:#cbd5e1}
45
- .steps{display:flex;flex-wrap:wrap;gap:8px;margin:10px 0}
46
- .step{padding:6px 10px;border-radius:999px;border:1px solid var(--line);color:var(--faint);font-size:12px}.step.done{color:#a7f3d0;border-color:rgba(52,211,153,.3)}.step.active{color:#bfdbfe;border-color:rgba(96,165,250,.35)}
47
- .actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}
48
- .list{display:grid;gap:10px}
49
- .card-row{display:grid;grid-template-columns:1fr auto;gap:12px;align-items:start;padding:14px;border:1px solid var(--line);border-radius:14px;background:rgba(255,255,255,.02)}
50
- .card-row h3{margin:0 0 6px;font-size:15px}.card-row p{margin:0 0 6px;color:var(--muted)}
51
- .evidence{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}.evidence span{font-size:12px;padding:4px 8px;border-radius:999px;background:rgba(255,255,255,.04);border:1px solid var(--line);color:var(--muted)}
52
- details.advanced{margin-top:12px;border-top:1px dashed var(--line);padding-top:10px}.advanced summary{cursor:pointer;color:var(--muted)}
53
- .toast{position:fixed;right:18px;bottom:18px;background:#111827;border:1px solid var(--line);padding:12px 14px;border-radius:12px;opacity:0;transform:translateY(8px);transition:.2s;z-index:50;max-width:360px}.toast.show{opacity:1;transform:none}
54
- .empty{padding:22px;border:1px dashed var(--line);border-radius:14px;color:var(--muted);text-align:center}
55
- .section-title{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px}
56
- .warn{padding:10px 12px;border-radius:12px;background:rgba(251,191,36,.08);border:1px solid rgba(251,191,36,.25);color:#fde68a;margin-bottom:12px}
57
- .errbox{padding:12px 14px;border-radius:12px;background:rgba(248,113,113,.08);border:1px solid rgba(248,113,113,.28);color:#fecaca;margin:10px 0}
58
- .errbox h4{margin:0 0 6px;font-size:14px}.errbox .muted{color:#fca5a5}
59
- .setup{padding:14px 16px;border-radius:14px;background:rgba(96,165,250,.08);border:1px solid rgba(96,165,250,.28);margin-bottom:14px}
60
- .files{display:grid;gap:6px;margin-top:8px}.file-row{display:flex;justify-content:space-between;gap:10px;padding:6px 8px;border-radius:8px;background:rgba(255,255,255,.03);border:1px solid var(--line);font-size:12px}
61
- .opbar{margin-bottom:12px;padding:10px 12px;border-radius:12px;border:1px solid var(--line);background:rgba(255,255,255,.03);display:flex;gap:10px;align-items:center;flex-wrap:wrap}
62
- .opbar.running{border-color:rgba(96,165,250,.35);background:rgba(96,165,250,.08)}
63
- .opbar.ok{border-color:rgba(52,211,153,.35);background:rgba(52,211,153,.08)}
64
- .opbar.err{border-color:rgba(248,113,113,.35);background:rgba(248,113,113,.08)}
65
- .btn:disabled{opacity:.55;cursor:not-allowed}
66
- .busy-banner{font-size:12px;color:var(--muted)}
67
- .refresh-meta{font-size:12px;color:var(--faint);margin-left:auto}
68
- .composer h2{font-size:20px}.composer .hint{color:var(--muted);margin:0 0 10px;font-size:13px}
69
- .decision-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin:12px 0}.decision-box{padding:12px;border:1px solid var(--line);border-radius:12px;background:rgba(255,255,255,.025)}.decision-box strong{display:block;margin-bottom:5px}.decision-box p{margin:0;color:var(--muted)}
70
- .flow{display:flex;align-items:stretch;gap:6px;margin:12px 0;overflow:auto}.flow-step{min-width:130px;flex:1;padding:9px 10px;border-radius:10px;border:1px solid var(--line);background:rgba(255,255,255,.025);color:var(--muted);font-size:12px}.flow-arrow{display:grid;place-items:center;color:var(--faint)}
71
- @media(max-width:980px){.body{grid-template-columns:1fr}.side{display:none}.grid.two,.grid.cards,.grid.stats,.decision-grid{grid-template-columns:1fr}.top{flex-wrap:wrap;height:auto;padding:10px 12px}}
72
- </style>
5
+ <meta charset="utf-8" />
6
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
7
+ <meta name="color-scheme" content="light" />
8
+ <title>Forge · Utility Console</title>
9
+ <link rel="stylesheet" href="/console-assets/app.css" />
73
10
  </head>
74
11
  <body>
75
- <div class="app">
76
- <header class="top">
77
- <div class="brand"><span class="logo">∞</span><span>执行助手控制台</span></div>
78
- <button class="chip" id="repoChip" onclick="switchView('repositories')">仓库 · <strong id="topRepo">—</strong></button>
79
- <span class="chip"><span class="dot" id="readyDot"></span><strong id="topReady">检查中</strong></span>
80
- <span class="chip"><span class="dot" id="connectorDot"></span><span id="topConnector">连接</span></span>
81
- <button class="chip" onclick="switchView('capabilities')">插件 <strong id="topPlugins">—</strong></button>
82
- <button class="chip" onclick="switchView('inbox')">需要处理 <span class="count" id="topHandoffs">0</span></button>
83
- <div class="top-actions">
84
- <button class="btn ghost" onclick="refreshAll()">刷新</button>
85
- <button class="btn ghost" onclick="switchView('advanced')">高级</button>
86
- </div>
87
- </header>
88
- <div class="body">
89
- <aside class="side">
90
- <nav class="nav">
91
- <button class="active" data-view="home">⌂ 指挥中心</button>
92
- <button data-view="inbox">△ 需要处理 <span class="count" id="sideHandoffs">0</span></button>
93
- <button data-view="work">▷ 当前任务</button>
94
- <button data-view="capabilities">◇ 能力 / 插件 <span class="count" id="sidePlugins" style="display:none">0</span></button>
95
- <button data-view="automation">⚙ 模型与工具</button>
96
- <button data-view="readiness">◎ 系统状态</button>
97
- <button data-view="repositories">□ 仓库</button>
98
- <button data-view="advanced">⌁ 高级诊断</button>
99
- </nav>
100
- </aside>
101
- <main class="main">
102
- <section class="view active" id="view-home"></section>
103
- <section class="view" id="view-inbox"></section>
104
- <section class="view" id="view-work"></section>
105
- <section class="view" id="view-capabilities"></section>
106
- <section class="view" id="view-automation"></section>
107
- <section class="view" id="view-readiness"></section>
108
- <section class="view" id="view-repositories"></section>
109
- <section class="view" id="view-advanced"></section>
110
- </main>
111
- </div>
112
- </div>
113
- <div class="toast" id="toast"></div>
114
- <script>
115
- function safeGet(k){try{return sessionStorage.getItem(k)||''}catch(_e){return ''}}
116
- function safeSet(k,v){try{if(v)sessionStorage.setItem(k,v);else sessionStorage.removeItem(k)}catch(_e){}}
117
- function queryRepoId(){try{return new URLSearchParams(location.search).get('repoId')||''}catch(_e){return ''}}
118
- function setQueryRepoId(id){try{var u=new URL(location.href);if(id)u.searchParams.set('repoId',id);else u.searchParams.delete('repoId');history.replaceState(null,'',u)}catch(_e){}}
119
- function esc(v){return String(v==null?'':v).replace(/[&<>"']/g,function(c){return({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'})[c]})}
120
- function arr(v){return Array.isArray(v)?v:[]}
121
- function obj(v){return v&&typeof v==='object'&&!Array.isArray(v)?v:{}}
122
- function toast(msg){var t=document.getElementById('toast');t.textContent=msg;t.classList.add('show');setTimeout(function(){t.classList.remove('show')},2800)}
123
- function pill(tone,label){return '<span class="pill '+esc(tone||'gray')+'">'+esc(label||'')+'</span>'}
124
- function setDot(id,tone){document.getElementById(id).className='dot '+(tone&&tone!=='green'?tone:'')}
125
-
126
- var selectedRepoId=queryRepoId()||safeGet('forgeSelectedRepoId')||'';
127
- var commandCenter=null;
128
- var automationSettings=null;
129
- var selectedWorkId=safeGet('forgeSelectedWorkId')||'';
130
- var selectedHandoffId='';
131
- var selectedPluginId=safeGet('forgeSelectedPluginId')||'';
132
- var modePreview=null;
133
- var advancedRaw=null;
134
- var lastFacade=null;
135
- var lastOperation=null; // {phase,statusLabel,summary,errorMessage,at}
136
- var pluginActionResult=null;
137
- var busy=false;
138
- var pollTimer=null;
139
- var refreshInFlight=false;
140
- var lastRefreshedAt='';
141
- var pageVisible=!document.hidden;
142
- var workExtras={}; // workId -> {changedFiles, error, summary}
143
-
144
- document.addEventListener('visibilitychange',function(){
145
- pageVisible=!document.hidden;
146
- if(!pageVisible){
147
- if(pollTimer){clearInterval(pollTimer);pollTimer=null}
148
- return;
149
- }
150
- ensurePoll();
151
- if(shouldPoll())refreshAll({silent:true});
152
- });
153
-
154
- function repoQuery(){return selectedRepoId?'?repoId='+encodeURIComponent(selectedRepoId):''}
155
- function rememberRepo(id){selectedRepoId=id||'';safeSet('forgeSelectedRepoId',selectedRepoId);setQueryRepoId(selectedRepoId)}
156
- function rememberWork(id){selectedWorkId=id||'';safeSet('forgeSelectedWorkId',selectedWorkId)}
157
- function api(path,opts){opts=opts||{};var headers=Object.assign({'content-type':'application/json'},opts.headers||{});return fetch(path,Object.assign({},opts,{credentials:'same-origin',headers:headers})).then(function(r){return r.json().catch(function(){return{}}).then(function(body){if(!r.ok){var msg=(body.error&&body.error.title)||body.summary||body.errorMessage||body.error||body.message||('请求失败 '+r.status);if(!msg||msg==='undefined')msg='请求失败,请查看高级诊断';var err=new Error(msg);err.payload=body;err.status=r.status;throw err}return body})})}
158
- function setBusy(on,label){
159
- busy=!!on;
160
- document.querySelectorAll('button.btn').forEach(function(b){
161
- if(on)b.setAttribute('disabled','disabled');else b.removeAttribute('disabled');
162
- });
163
- if(on){
164
- lastOperation={phase:'running',statusLabel:label||'执行中',summary:label||'正在处理…',at:new Date().toISOString()};
165
- renderOpBar();
166
- }
167
- }
168
- function setLastOp(res,fallback){
169
- res=obj(res);
170
- var feedback=obj(res.feedback);
171
- var phase=feedback.phase||res.phase||(res.status==='ok'?'succeeded':(res.status==='failed'||res.status==='blocked'||res.status==='approval_required')?'failed':'succeeded');
172
- var next='';
173
- var actions=arr(feedback.suggestedNextActions||res.suggestedNextActions);
174
- if(actions.length)next=actions[0].label||'';
175
- if(feedback.error&&feedback.error.nextActions&&feedback.error.nextActions[0])next=next||feedback.error.nextActions[0];
176
- lastOperation={
177
- phase:phase,
178
- statusLabel:feedback.statusLabel||res.statusLabel||fallback||'已更新',
179
- summary:feedback.summary||res.summary||fallback||'操作已完成',
180
- errorMessage:(feedback.error&&feedback.error.title)||res.errorMessage||'',
181
- error:feedback.error||res.error||null,
182
- next:next,
183
- changedFiles:feedback.changedFiles||res.changedFiles||null,
184
- at:new Date().toISOString()
185
- };
186
- // Remember changed files / errors against active work for card rendering.
187
- var wid=selectedWorkId||(res.data&&(res.data.work&&(res.data.work.workId||res.data.work.id)||res.data.workId));
188
- if(wid){
189
- workExtras[wid]=workExtras[wid]||{};
190
- if(lastOperation.changedFiles)workExtras[wid].changedFiles=lastOperation.changedFiles;
191
- if(lastOperation.error)workExtras[wid].error=lastOperation.error;
192
- if(lastOperation.summary)workExtras[wid].summary=lastOperation.summary;
193
- }
194
- lastFacade=res;
195
- renderOpBar();
196
- }
197
- function renderOpBar(){
198
- var nodes=document.querySelectorAll('#opBar');
199
- if(!nodes.length)return;
200
- var html='';
201
- if(lastOperation){
202
- var cls=lastOperation.phase==='running'||lastOperation.phase==='queued'||lastOperation.phase==='waiting'||lastOperation.phase==='submitting'?'running':(lastOperation.phase==='failed'||lastOperation.phase==='timed_out'||lastOperation.phase==='blocked'||lastOperation.phase==='needs_attention')?'err':'ok';
203
- html='<div class="opbar '+cls+'">'+
204
- '<span class="pill '+(cls==='ok'?'green':cls==='err'?'red':'blue')+'">'+esc(lastOperation.statusLabel)+'</span>'+
205
- '<span>'+esc(lastOperation.summary||'')+'</span>'+
206
- (lastOperation.errorMessage?'<span class="muted">'+esc(lastOperation.errorMessage)+'</span>':'')+
207
- (lastOperation.next?'<span class="muted">下一步:'+esc(lastOperation.next)+'</span>':'')+
208
- (busy?'<span class="busy-banner">请稍候,勿重复点击…</span>':'')+
209
- (lastRefreshedAt?'<span class="refresh-meta">刷新于 '+esc(lastRefreshedAt)+'</span>':'')+
210
- '</div>';
211
- if(lastOperation.error&&lastOperation.error.title){
212
- html+=renderErrorBox(lastOperation.error);
213
- }
214
- } else if(lastRefreshedAt){
215
- html='<div class="opbar"><span class="refresh-meta">刷新于 '+esc(lastRefreshedAt)+'</span></div>';
216
- }
217
- nodes.forEach(function(n){n.innerHTML=html});
218
- }
219
- function renderErrorBox(err){
220
- err=obj(err);
221
- if(!err.title)return '';
222
- var next=arr(err.nextActions).map(function(a){return '<span class="pill amber">'+esc(a)+'</span>'}).join(' ');
223
- return '<div class="errbox"><h4>'+esc(err.title)+'</h4><div class="muted">'+esc(err.explanation||'')+'</div>'+(next?'<div class="actions" style="margin-top:8px">'+next+'</div>':'')+
224
- '<details class="advanced"><summary>高级详情(仅调试)</summary><pre class="mono" style="white-space:pre-wrap;font-size:12px">'+esc(JSON.stringify(err,null,2))+'</pre></details></div>';
225
- }
226
- function renderChangedFiles(summary){
227
- summary=obj(summary);
228
- if(!summary.total&&!arr(summary.files).length)return '';
229
- var files=arr(summary.files).slice(0,12);
230
- return '<div style="margin-top:10px"><div class="section-title"><strong>改动文件</strong>'+pill('blue',summary.summaryLabel||((summary.total||files.length)+' 个文件'))+'</div>'+
231
- '<div class="files">'+files.map(function(f){
232
- return '<div class="file-row"><span class="mono">'+esc(f.path)+'</span><span class="pill gray">'+esc(f.statusLabel||f.status||'修改')+'</span></div>';
233
- }).join('')+'</div>'+
234
- (files.length<(summary.total||0)?'<div class="faint" style="margin-top:6px">仅显示前 '+files.length+' 项</div>':'')+
235
- '</div>';
236
- }
237
- function shouldPoll(){
238
- if(!pageVisible||refreshInFlight)return false;
239
- var work=obj(commandCenter&&commandCenter.currentWork);
240
- if(!work||!work.id)return false;
241
- var phase=work.phase||'';
242
- return phase==='running'||phase==='queued'||phase==='waiting'||phase==='submitting'||work.tone==='blue'||work.statusLabel==='进行中'||work.statusLabel==='待开始';
243
- }
244
- function ensurePoll(){
245
- if(!pageVisible||pollTimer)return;
246
- pollTimer=setInterval(function(){
247
- if(!shouldPoll()){
248
- if(pollTimer){clearInterval(pollTimer);pollTimer=null}
249
- return;
250
- }
251
- refreshAll({silent:true});
252
- },3500);
253
- }
254
-
255
- function switchView(name){
256
- document.querySelectorAll('.nav button').forEach(function(b){b.classList.toggle('active',b.dataset.view===name)});
257
- document.querySelectorAll('.view').forEach(function(v){v.classList.toggle('active',v.id==='view-'+name)});
258
- renderAll();
259
- }
260
- document.querySelectorAll('.nav button[data-view]').forEach(function(b){b.addEventListener('click',function(){switchView(b.dataset.view)})});
261
-
262
- function btn(label, attrs, cls){
263
- return '<button class="btn'+(cls?(' '+cls):'')+'" '+attrs+'>'+esc(label)+'</button>';
264
- }
265
- function stepsHtml(steps){
266
- return '<div class="steps">'+arr(steps).map(function(s){return '<span class="step '+(s.done?'done':'')+(s.active?' active':'')+'">'+esc(s.label)+'</span>'}).join('')+'</div>';
267
- }
268
-
269
- function evidenceHtml(labels){
270
- if(!arr(labels).length)return '';
271
- return '<div class="evidence">'+arr(labels).map(function(x){return '<span>'+esc(x)+'</span>'}).join('')+'</div>';
272
- }
273
-
274
- function advancedBlock(data){
275
- if(!data)return '';
276
- return '<details class="advanced"><summary>高级详情</summary><pre class="mono" style="white-space:pre-wrap;color:var(--muted);font-size:12px">'+esc(JSON.stringify(data,null,2))+'</pre></details>';
277
- }
278
-
279
- function renderChrome(){
280
- var cc=obj(commandCenter), ready=obj(cc.readiness), repo=obj(cc.currentRepository);
281
- var plug=obj(cc.pluginSummary);
282
- document.getElementById('topRepo').textContent=repo.name||'未选择';
283
- document.getElementById('topReady').textContent=ready.label||'未知';
284
- setDot('readyDot', ready.state==='blocked'?'red':ready.state==='needs_setup'?'amber':'green');
285
- document.getElementById('topConnector').textContent=ready.connectorLabel||'连接';
286
- setDot('connectorDot', ready.connectorTone||'gray');
287
- var n=ready.pendingHandoffCount||arr(cc.handoffs).length||0;
288
- document.getElementById('topHandoffs').textContent=n;
289
- document.getElementById('sideHandoffs').textContent=n;
290
- var readyPlugins=plug.ready||0, totalPlugins=plug.total||arr(cc.plugins).length||0;
291
- document.getElementById('topPlugins').textContent=totalPlugins?(readyPlugins+'/'+totalPlugins):'—';
292
- var need=plug.needsAttention||0;
293
- var sidePlugins=document.getElementById('sidePlugins');
294
- if(sidePlugins){
295
- if(need>0){sidePlugins.style.display='inline-grid';sidePlugins.textContent=String(need)}
296
- else {sidePlugins.style.display='none'}
297
- }
298
- }
299
-
300
- function renderHome(){
301
- var cc=obj(commandCenter), ready=obj(cc.readiness), work=obj(cc.currentWork), handoffs=arr(cc.handoffs).slice(0,3);
302
- var repo=obj(cc.currentRepository);
303
- var setup=obj(cc.setupGuide);
304
- var warnings=arr(cc.warnings).map(function(w){return '<div class="warn">'+esc(w)+'</div>'}).join('');
305
- var mode=obj(modePreview||cc.modePreviewDefault);
306
- var el=document.getElementById('view-home');
307
- var setupHtml=setup.needed
308
- ? '<div class="setup"><strong>'+esc(setup.title||'需要先设置仓库')+'</strong><p class="muted" style="margin:6px 0 10px">'+esc(setup.body||'')+'</p>'+btn(setup.actionLabel||'去设置仓库','data-nav="repositories"','primary')+'</div>'
309
- : '';
310
- el.innerHTML=
311
- '<div id="opBar"></div>'+
312
- warnings+
313
- setupHtml+
314
- '<div class="page-head"><div><h1>指挥中心</h1><p>描述目标,开始执行,然后只关注当前进度和需要你决定的事项。</p></div>'+
315
- '<div class="muted">当前仓库:<strong>'+esc(repo.name||'未选择')+'</strong>'+(repo.branchLabel?' · '+esc(repo.branchLabel):'')+(repo.dirtyLabel?' · '+esc(repo.dirtyLabel):'')+'</div></div>'+
316
- '<div class="panel composer">'+
317
- '<div class="section-title"><div><h2>你想让它完成什么?</h2><p class="hint">直接描述目标。路径、验收标准和执行模式由系统先判断,你仍可在开始前预览。</p></div>'+pill(ready.state==='ready'?'green':ready.state==='needs_setup'?'amber':'red',ready.label||'未知')+'</div>'+
318
- '<textarea id="taskObjective" placeholder="例如:优化 Controller 首页的信息层级,让当前任务和待决定事项更容易找到,不改变后端行为。"></textarea>'+
319
- '<details class="advanced" style="margin-top:10px"><summary>补充验收标准和允许修改的路径</summary>'+
320
- '<div class="row" style="margin-top:10px"><input class="input" id="taskAcceptance" placeholder="验收标准(可选,用分号分隔)" style="flex:1"></div>'+
321
- '<div class="row"><input class="input" id="taskPaths" placeholder="允许修改的路径(可选,逗号分隔)" style="flex:1"><input class="input" id="taskFiles" type="number" min="0" placeholder="预计文件数" style="width:140px"></div>'+
322
- '</details>'+
323
- '<div class="row" style="margin-top:14px">'+
324
- '<button class="btn primary" id="btnStart" onclick="startTask()">开始执行</button>'+
325
- '<button class="btn" id="btnPreview" onclick="previewMode()">预览执行方式</button>'+
326
- '<button class="btn ghost" onclick="diagnoseFirst()">只做诊断</button>'+
327
- '<span class="muted" style="margin-left:auto">建议:<strong>'+esc(mode.label||'直接执行')+'</strong></span>'+
328
- '</div>'+
329
- '</div>'+
330
- '<div class="grid two" style="margin-top:16px">'+
331
- renderWorkCard(work, '当前任务')+
332
- '<div class="panel"><div class="section-title"><h2>需要处理</h2>'+btn('查看全部','data-nav="inbox"','ghost')+'</div>'+
333
- (handoffs.length?handoffs.map(function(h){return handoffMini(h)}).join(''):'<div class="empty">目前没有需要你拍板的事项</div>')+
334
- '</div>'+
335
- '</div>'+
336
- '<div class="panel" style="margin-top:16px">'+
337
- '<div class="section-title"><div><h2>运行状态</h2><p class="muted" style="margin:0">'+esc(ready.headline||ready.description||'系统状态可用')+'</p></div>'+pill(ready.connectorTone||'gray',ready.connectorLabel||'连接')+'</div>'+
338
- '<div class="actions">'+btn('系统状态','data-nav="readiness"')+btn('能力与插件','data-nav="capabilities"')+btn('模型与工具','data-nav="automation"')+btn('切换仓库','data-nav="repositories"')+'</div>'+
339
- '</div>'+
340
- renderGoalLoopPanel(cc.goalLoop);
341
- bindNav(el);
342
- renderOpBar();
343
- }
344
-
345
- function renderGoalLoopPanel(gl){
346
- gl=obj(gl);
347
- var goals=arr(gl.goals);
348
- var inv=arr(gl.invokableProviders);
349
- var handoff=arr(gl.handoffOnlyProviders);
350
- var active=gl.activeCount||goals.length||0;
351
- return '<details class="panel advanced" style="margin-top:16px">'+
352
- '<summary style="display:flex;align-items:center;gap:10px;list-style:none">'+
353
- '<strong>自动化目标</strong>'+pill(active?'blue':'gray',active+' 个活跃目标')+
354
- '<span class="muted">'+inv.length+' 个提供方可直接调用 · '+handoff.length+' 个仅支持 Handoff</span>'+
355
- '<span class="muted" style="margin-left:auto">展开查看</span>'+
356
- '</summary>'+
357
- '<div style="margin-top:14px">'+
358
- (gl.automationSummary?'<p class="muted">'+esc(gl.automationSummary)+'</p>':'')+
359
- (goals.length?goals.slice(0,3).map(function(g){
360
- return '<div class="card-row" style="margin-top:10px"><div>'+
361
- '<h3>'+esc(g.title||'目标')+'</h3>'+
362
- '<p class="muted">阶段:'+esc(g.stage)+' · 提供方:'+esc(g.providerSelected||'—')+'</p>'+
363
- (g.whatIsBlocked?'<p class="muted">阻塞:'+esc(g.whatIsBlocked)+'</p>':'')+
364
- (g.whatHappensNext?'<p class="faint">下一步:'+esc(g.whatHappensNext)+'</p>':'')+
365
- '</div>'+pill(g.approvalRequired?'amber':g.handoffPacketAvailable?'blue':'green',g.handoffPacketAvailable?'有 Handoff 包':g.stage)+'</div>';
366
- }).join(''):'<div class="empty">暂无活跃自动化目标</div>')+
367
- '<div class="actions">'+btn('打开模型与工具','data-nav="automation"')+'</div>'+
368
- '</div>'+
369
- '</details>';
370
- }
371
-
372
- function mergeWorkExtras(work){
373
- work=obj(work);
374
- var extra=obj(workExtras[work.id]);
375
- if(extra.changedFiles&&!work.changedFiles)work.changedFiles=extra.changedFiles;
376
- if(extra.error&&!work.error)work.error=extra.error;
377
- if(extra.summary&&!work.latestSummary)work.latestSummary=extra.summary;
378
- return work;
379
- }
380
-
381
- function renderWorkCard(work, title){
382
- work=mergeWorkExtras(obj(work));
383
- if(!work.id){
384
- return '<div class="panel"><div class="section-title"><h2>'+esc(title||'当前任务')+'</h2></div><div class="empty">还没有任务。在左侧描述目标并点击“开始”。</div></div>';
385
- }
386
- var v=obj(work.latestVerification);
387
- var id=esc(work.id);
388
- var primary=work.primaryActionLabel||work.nextAction||'继续';
389
- var primaryKind='continue';
390
- var suggested=arr(work.suggestedActions);
391
- if(suggested.length&&suggested[0].kind)primaryKind=suggested[0].kind;
392
- return '<div class="panel work-card" data-work-id="'+id+'">'+
393
- '<div class="section-title"><h2>'+esc(title||'当前任务')+'</h2>'+pill(work.tone, work.statusLabel||work.phaseLabel||'状态')+'</div>'+
394
- '<h3 style="margin:0 0 6px">'+esc(work.title||work.objective||'未命名任务')+'</h3>'+
395
- '<div class="muted">模式:'+esc(work.modeLabel||'—')+' · 阶段:'+esc(work.phaseLabel||work.statusLabel||'—')+'</div>'+
396
- '<div class="muted" style="margin-top:4px">最近动作:'+esc(work.latestAction||primary)+'</div>'+
397
- (work.latestSummary?'<p class="muted" style="margin:8px 0 0">'+esc(work.latestSummary)+'</p>':'')+
398
- stepsHtml(work.progressSteps)+
399
- (v.summary?'<div style="margin-top:8px">'+pill(v.tone,v.label)+' <span class="muted">'+esc(v.summary)+'</span></div>':'')+
400
- (work.error?renderErrorBox(work.error):'')+
401
- renderChangedFiles(work.changedFiles)+
402
- (work.delegateSummary?'<p class="muted" style="margin-top:8px">'+esc(work.delegateSummary)+'</p>':'')+
403
- evidenceHtml(work.evidenceLabels)+
404
- (arr(work.acceptanceCriteria).length?'<p class="faint" style="margin-top:8px">验收:'+esc(arr(work.acceptanceCriteria).join(' · '))+'</p>':'')+
405
- '<div class="actions">'+
406
- btn(primary,'data-work-act="'+(primaryKind==='verify'?'verify':primaryKind==='finalize'?'finalize':primaryKind==='repair'?'repair':'continue')+'" data-work-id="'+id+'"','primary')+
407
- btn('验证','data-work-act="verify" data-work-id="'+id+'"')+
408
- btn('委派 Codex','data-work-act="delegate-codex" data-work-id="'+id+'"')+
409
- btn('请 Grok 审阅','data-work-act="delegate-grok" data-work-id="'+id+'"')+
410
- btn('修复诊断','data-work-act="repair" data-work-id="'+id+'"')+
411
- btn('收尾','data-work-act="finalize" data-work-id="'+id+'"')+
412
- btn('停止','data-work-act="stop" data-work-id="'+id+'"','danger')+
413
- btn('打开详情','data-open-work="'+id+'"','ghost')+
414
- '</div>'+
415
- advancedBlock(work.advanced)+
416
- '</div>';
417
- }
418
-
419
- function handoffMini(h){
420
- var id=esc(h.id);
421
- var d=obj(h.decision);
422
- return '<div class="card-row" style="margin-bottom:8px">'+
423
- '<div><div style="margin-bottom:6px">'+pill(h.tone,d.typeLabel||h.severityLabel||'需要决定')+'</div><h3>'+esc(h.title)+'</h3>'+
424
- '<p><strong>需要你决定:</strong>'+esc(d.requestedAction||h.reason||'请打开查看详情')+'</p>'+
425
- '<div class="faint">'+esc(d.afterApproval||h.recommendedDecision||'请先阅读详情')+'</div></div>'+
426
- '<div class="actions" style="flex-direction:column">'+
427
- btn(d.canApproveAndContinue?'查看审批详情':'查看并决定','data-open-handoff="'+id+'"','primary')+
428
- '</div></div>';
429
- }
430
-
431
- function renderInbox(){
432
- var items=arr(obj(commandCenter).handoffs);
433
- var detail=selectedHandoffId?items.find(function(x){return x.id===selectedHandoffId}):null;
434
- if(!detail&&items.length){selectedHandoffId=items[0].id;detail=items[0]}
435
- var root=document.getElementById('view-inbox');
436
- root.innerHTML=
437
- '<div class="page-head"><div><h1>需要处理</h1><p>这里只展示真正阻塞执行的业务决定、不可逆操作和无法自动恢复的问题。</p></div><button class="btn" onclick="refreshAll()">刷新</button></div>'+
438
- '<div class="grid two">'+
439
- '<div class="list">'+(items.length?items.map(function(h){
440
- var id=esc(h.id);var d=obj(h.decision);
441
- return '<div class="panel" style="padding:14px;cursor:pointer;border-color:'+(selectedHandoffId===h.id?'rgba(96,165,250,.45)':'')+'" data-open-handoff="'+id+'">'+
442
- '<div class="section-title"><h3 style="margin:0">'+esc(h.title)+'</h3>'+pill(h.tone,d.typeLabel||h.severityLabel)+'</div>'+
443
- '<p class="muted"><strong>需要决定:</strong>'+esc(d.requestedAction||h.reason)+'</p>'+
444
- '<p class="faint"><strong>处理后:</strong>'+esc(d.afterApproval||h.recommendedDecision||'—')+'</p></div>';
445
- }).join(''):'<div class="empty">没有待审批或待决定事项。你可以回到指挥中心开始新任务。</div>')+'</div>'+
446
- '<div class="panel" id="handoffDetail">'+(detail?renderHandoffDetail(detail):'<div class="empty">选择左侧事项查看详情</div>')+'</div>'+
447
- '</div>';
448
- bindActions(root);
449
- }
450
-
451
- function renderHandoffDetail(h){
452
- var id=esc(h.id);var d=obj(h.decision);
453
- var primary=d.canApproveAndContinue
454
- ? btn(d.primaryActionLabel||'批准并继续执行','data-handoff-act="approve" data-handoff-id="'+id+'"','primary')
455
- : btn(d.primaryActionLabel||'记录决定','data-handoff-act="resolve" data-handoff-id="'+id+'"','primary');
456
- return '<div class="section-title"><div><h2>'+esc(h.title)+'</h2><div>'+pill(h.tone,d.typeLabel||h.severityLabel)+' '+pill(d.necessityLabel==='必须确认'?'amber':'gray',d.necessityLabel||'需要判断')+'</div></div>'+pill(h.tone,h.statusLabel)+'</div>'+
457
- '<div class="setup" style="margin:12px 0"><strong>你正在批准 / 决定什么</strong><p class="muted" style="margin:6px 0 0">'+esc(d.requestedAction||h.reason||'—')+'</p></div>'+
458
- '<div class="decision-grid">'+
459
- '<div class="decision-box"><strong>是否有必要</strong><p>'+esc(d.necessityExplanation||h.reason||'—')+'</p></div>'+
460
- '<div class="decision-box"><strong>会产生什么影响</strong><p>'+esc(d.impact||'—')+'</p></div>'+
461
- '<div class="decision-box"><strong>批准 / 记录后</strong><p>'+esc(d.afterApproval||'—')+'</p></div>'+
462
- '<div class="decision-box"><strong>不批准会怎样</strong><p>'+esc(d.ifRejected||'—')+'</p></div>'+
463
- '</div>'+
464
- '<div class="flow"><div class="flow-step">1. 当前停在安全点</div><div class="flow-arrow">→</div><div class="flow-step">2. 阅读对象、必要性和影响</div><div class="flow-arrow">→</div><div class="flow-step">3. '+esc(d.canApproveAndContinue?'批准后创建任务':'记录决定或补充信息')+'</div><div class="flow-arrow">→</div><div class="flow-step">4. 打开任务详情并显示下一步</div></div>'+
465
- '<p><strong>为什么停下来</strong><br><span class="muted">'+esc(h.reason)+'</span></p>'+
466
- '<p><strong>相关任务</strong><br><span class="muted">'+esc(h.workTitle||'尚未创建任务')+'</span></p>'+
467
- '<div class="setup" style="margin:10px 0"><strong>系统建议</strong><p class="muted" style="margin:6px 0 0">'+esc(h.recommendedDecision||'请阅读后自行判断')+'</p></div>'+
468
- (arr(h.attemptedActions).length?'<p><strong>已尝试</strong><br><span class="muted">'+esc(h.attemptedActions.join(' · '))+'</span></p>':'')+
469
- evidenceHtml(h.evidenceLabels)+
470
- '<div class="actions">'+primary+
471
- (d.canApproveAndContinue?btn('仅记录其他决定','data-handoff-act="resolve" data-handoff-id="'+id+'"'):'')+
472
- btn('确认已知晓','data-handoff-act="ack" data-handoff-id="'+id+'"')+
473
- btn('暂不处理','data-handoff-act="dismiss" data-handoff-id="'+id+'"')+
474
- (h.continuationPrompt?btn('复制人工继续提示','data-copy="1" data-copy-text="'+esc(h.continuationPrompt)+'"'):'')+
475
- (h.advanced&&h.advanced.workId?btn('查看相关任务','data-open-work="'+esc(h.advanced.workId)+'"'):'')+
476
- '</div>'+
477
- advancedBlock(h.advanced);
478
- }
479
-
480
- function renderWork(){
481
- var work=null;
482
- if(selectedWorkId){
483
- var all=[obj(commandCenter).currentWork].concat(arr(obj(commandCenter).recentWork));
484
- work=all.find(function(w){return w&&w.id===selectedWorkId})||obj(commandCenter).currentWork;
485
- } else {
486
- work=obj(commandCenter).currentWork;
487
- }
488
- var recent=arr(obj(commandCenter).recentWork);
489
- var root=document.getElementById('view-work');
490
- root.innerHTML=
491
- '<div class="page-head"><div><h1>任务详情</h1><p>用自然语言理解进度,不必关心内部 ID。</p></div></div>'+
492
- renderWorkCard(work,'任务')+
493
- '<div class="panel" style="margin-top:14px"><div class="section-title"><h2>最近任务</h2></div>'+
494
- (recent.length?recent.map(function(w){
495
- return '<div class="card-row" style="margin-bottom:8px"><div><h3>'+esc(w.title)+'</h3><div class="muted">'+esc(w.modeLabel)+' · '+esc(w.statusLabel)+'</div></div>'+
496
- btn('打开','data-open-work="'+esc(w.id)+'"')+'</div>';
497
- }).join(''):'<div class="empty">暂无任务记录</div>')+
498
- '</div>';
499
- bindActions(root);
500
- }
501
-
502
- function renderConnectorHowToFix(fresh){
503
- fresh=obj(fresh);
504
- var steps=arr(fresh.howToFix);
505
- if(!steps.length && !fresh.summary) return '';
506
- var actions=arr(fresh.suggestedActions);
507
- return '<div class="panel" style="margin-top:14px">'+
508
- '<div class="section-title"><h2 style="margin:0">连接器诊断</h2>'+pill(fresh.severity==='error'?'red':fresh.severity==='warning'?'amber':fresh.severity==='ok'?'green':'blue', fresh.severity||'info')+'</div>'+
509
- '<p class="muted">'+esc(fresh.summary||'')+'</p>'+
510
- (arr(fresh.expectedFacadeTools).length?'<p class="faint">期望 facade:'+esc(arr(fresh.expectedFacadeTools).join(' · '))+'</p>':'')+
511
- (arr(fresh.missingLocalTools).length?'<p class="muted">本地缺失:'+esc(arr(fresh.missingLocalTools).join(', '))+'</p>':'')+
512
- (arr(fresh.missingConnectorTools).length?'<p class="muted">连接器快照缺失:'+esc(arr(fresh.missingConnectorTools).join(', '))+'</p>':'')+
513
- (steps.length
514
- ? '<details class="advanced" style="margin-top:10px"><summary>如何修复</summary><ol style="margin:10px 0 0 18px;color:var(--muted);font-size:13px;line-height:1.55">'+
515
- steps.map(function(s){return '<li>'+esc(s)+'</li>';}).join('')+
516
- '</ol>'+
517
- (actions.length?'<p class="faint" style="margin-top:10px">建议:'+esc(actions.join(' · '))+'</p>':'')+
518
- '</details>'
519
- : '')+
520
- '</div>';
521
- }
522
-
523
- function renderReadiness(){
524
- var ready=obj(obj(commandCenter).readiness);
525
- var fresh=obj(ready.connectorFreshness);
526
- document.getElementById('view-readiness').innerHTML=
527
- '<div class="page-head"><div><h1>系统状态</h1><p>用白话告诉你 forge 是否可用。</p></div>'+
528
- '<div class="actions"><button class="btn primary" onclick="diagnoseFirst()">诊断</button><button class="btn" onclick="repairDryRun()">预览修复</button></div></div>'+
529
- '<div class="panel" style="margin-bottom:14px"><h2>'+esc(ready.headline||'')+'</h2><p class="muted">'+esc(ready.description||'')+'</p></div>'+
530
- '<div class="grid cards">'+arr(ready.sections).map(function(s){
531
- return '<div class="panel"><div class="section-title"><h3 style="margin:0">'+esc(s.title)+'</h3>'+pill(s.tone,s.statusLabel)+'</div><p class="muted">'+esc(s.detail)+'</p></div>';
532
- }).join('')+'</div>'+
533
- renderConnectorHowToFix(fresh);
534
- }
535
-
536
- function renderRepositories(){
537
- var repos=arr(obj(commandCenter).repositories);
538
- var root=document.getElementById('view-repositories');
539
- root.innerHTML=
540
- '<div class="page-head"><div><h1>仓库</h1><p>选择当前工作仓库。显示名称、分支和是否干净会直接影响你是否能开始任务。</p></div>'+
541
- '<button class="btn primary" onclick="toggleAddRepo()">添加本地仓库</button></div>'+
542
- '<div class="panel" id="repoAdd" style="display:none;margin-bottom:14px">'+
543
- '<div class="row" style="display:grid;grid-template-columns:1.4fr .8fr auto;gap:10px">'+
544
- '<input class="input" id="repoPath" placeholder="./YourRepo">'+
545
- '<input class="input" id="repoName" placeholder="显示名称(可选)">'+
546
- '<button class="btn primary" onclick="registerRepo()">注册</button>'+
547
- '</div></div>'+
548
- (repos.length?'':'<div class="setup"><strong>还没有仓库</strong><p class="muted" style="margin:6px 0 0">添加一个本地 Git 仓库后,指挥中心才能启动任务。</p></div>')+
549
- '<div class="list">'+repos.map(function(r){
550
- return '<div class="panel card-row"><div><h3>'+esc(r.name)+'</h3><div class="muted mono">'+esc(r.path)+'</div>'+
551
- '<div class="muted" style="margin-top:6px">'+(r.branchLabel?('分支 '+esc(r.branchLabel)):'')+(r.dirtyLabel?' · '+esc(r.dirtyLabel):'')+(r.readinessLabel?' · '+esc(r.readinessLabel):'')+'</div></div>'+
552
- '<div class="actions">'+
553
- btn(r.current?'当前仓库':'设为当前','data-select-repo="'+esc(r.id)+'"',r.current?'primary':'')+
554
- btn('删除注册','data-remove-repo="'+esc(r.id)+'" data-remove-repo-name="'+esc(r.name||r.id)+'"','danger')+
555
- '</div>'+
556
- advancedBlock(r.advanced)+
557
- '</div>';
558
- }).join('')+'</div>';
559
- bindActions(root);
560
- }
561
-
562
- function rememberPlugin(id){
563
- selectedPluginId=id||'';
564
- safeSet('forgeSelectedPluginId', selectedPluginId);
565
- }
566
-
567
- function pluginById(id){
568
- return arr(obj(commandCenter).plugins).find(function(p){return p.id===id})||null;
569
- }
570
-
571
- function renderCapabilities(){
572
- var cc=obj(commandCenter);
573
- var plugins=arr(cc.plugins);
574
- var summary=obj(cc.pluginSummary);
575
- var selected=selectedPluginId?pluginById(selectedPluginId):null;
576
- if(selectedPluginId&&!selected&&plugins.length){
577
- selected=plugins[0];
578
- rememberPlugin(selected.id);
579
- }
580
- var ready=summary.ready||plugins.filter(function(p){return p.status==='ready'}).length;
581
- var need=summary.needsAttention||plugins.filter(function(p){return p.status!=='ready'&&p.status!=='disabled'}).length;
582
- var failed=plugins.filter(function(p){return p.status==='failed'||p.status==='disabled'}).length;
583
- var root=document.getElementById('view-capabilities');
584
- root.innerHTML=
585
- '<div class="page-head"><div><h1>能力 / 插件</h1><p>预览并管理 forge 可调用的助手插件;普通动作直接执行,不可逆操作需要强确认。</p></div>'+
586
- '<div class="actions"><button class="btn" onclick="refreshPlugins()">检查连接</button></div></div>'+
587
- '<div class="grid stats" style="margin-bottom:14px">'+
588
- '<div class="panel stat"><span>能力总数</span><strong>'+plugins.length+'</strong></div>'+
589
- '<div class="panel stat"><span>可用</span><strong>'+ready+'</strong></div>'+
590
- '<div class="panel stat"><span>需要配置</span><strong>'+need+'</strong></div>'+
591
- '<div class="panel stat"><span>不可用</span><strong>'+failed+'</strong></div>'+
592
- '</div>'+
593
- '<div class="grid two">'+
594
- '<div class="list">'+
595
- (plugins.length?plugins.map(function(p){
596
- var active=selected&&selected.id===p.id;
597
- return '<div class="panel plugin-card" style="cursor:pointer;border-color:'+(active?'rgba(96,165,250,.45)':'')+'" data-open-plugin="'+esc(p.id)+'">'+
598
- '<div><h3 style="margin:0 0 6px">'+esc(p.name)+'</h3>'+
599
- '<div class="muted">'+esc(p.description||'')+'</div>'+
600
- '<div class="muted" style="margin-top:6px">'+esc(p.nextStep||'')+'</div>'+
601
- (arr(p.capabilityLabels).length?'<div class="evidence" style="margin-top:8px">'+arr(p.capabilityLabels).map(function(c){return '<span>'+esc(c)+'</span>'}).join('')+'</div>':'')+
602
- '</div>'+
603
- '<div style="text-align:right">'+pill(p.tone,p.statusLabel)+
604
- '<div class="actions" style="margin-top:8px;justify-content:flex-end">'+btn(active?'已选中':'查看', 'data-open-plugin="'+esc(p.id)+'"', active?'primary':'')+'</div>'+
605
- '</div></div>';
606
- }).join(''):'<div class="empty">当前仓库没有发现插件</div>')+
607
- '</div>'+
608
- '<div class="panel" id="pluginDetail">'+renderPluginDetail(selected)+'</div>'+
609
- '</div>'+
610
- (pluginActionResult?'<div class="panel" style="margin-top:14px"><div class="section-title"><h2>最近动作结果</h2></div><pre class="mono" style="white-space:pre-wrap;font-size:12px;color:var(--muted)">'+esc(JSON.stringify(pluginActionResult,null,2))+'</pre></div>':'');
611
- bindActions(root);
612
- }
613
-
614
- function renderPluginDetail(p){
615
- if(!p||!p.id){
616
- return '<div class="empty">选择左侧插件查看能力、健康状态与可执行动作</div>';
617
- }
618
- var actions=arr(p.actions);
619
- var previewActions=actions.filter(function(a){return a.canPreview});
620
- var otherActions=actions.filter(function(a){return !a.canPreview});
621
- return '<div class="section-title"><h2 style="margin:0">'+esc(p.name)+'</h2>'+pill(p.tone,p.statusLabel)+'</div>'+
622
- '<p class="muted">'+esc(p.nextStep||'')+'</p>'+
623
- '<div class="meta muted" style="display:flex;gap:10px;flex-wrap:wrap;margin:8px 0 12px">'+
624
- '<span>提供方:'+esc(p.provider||'—')+'</span>'+
625
- '<span>健康:'+esc(p.healthLabel||'—')+'</span>'+
626
- '<span>生命周期:'+esc(p.lifecycleLabel||'—')+'</span>'+
627
- '<span>动作:'+esc(String(p.actionCount||actions.length))+'</span>'+
628
- '</div>'+
629
- (arr(p.warnings).length?'<div class="warn">'+esc(arr(p.warnings).join(' · '))+'</div>':'')+
630
- (arr(p.capabilityLabels).length?'<p><strong>能力域</strong></p><div class="evidence">'+arr(p.capabilityLabels).map(function(c){return '<span>'+esc(c)+'</span>'}).join('')+'</div>':'')+
631
- '<div style="margin-top:14px"><div class="section-title"><h3 style="margin:0">预览 / 状态动作</h3><span class="muted">只读或配置检查,优先使用</span></div>'+
632
- (previewActions.length?previewActions.map(function(a){return pluginActionRow(p.id,a,true)}).join(''):'<div class="empty">没有可预览动作</div>')+
633
- '</div>'+
634
- '<div style="margin-top:14px"><div class="section-title"><h3 style="margin:0">写入动作</h3><span class="muted">只有不可逆操作需要强确认</span></div>'+
635
- (otherActions.length?otherActions.map(function(a){return pluginActionRow(p.id,a,false)}).join(''):'<div class="empty">没有写入动作</div>')+
636
- '</div>'+
637
- advancedBlock(p.advanced);
638
- }
639
-
640
- function pluginActionRow(pluginId, action, isPreview){
641
- var id=esc(pluginId), aid=esc(action.id);
642
- return '<div class="action-row">'+
643
- '<div><div><strong>'+esc(action.title||action.id)+'</strong> '+pill(action.readOnly?'green':action.risk==='destructive'?'red':'amber', action.riskLabel)+'</div>'+
644
- '<div class="muted">'+esc(action.description||'')+'</div>'+
645
- '<div class="faint">'+esc(action.confirmationLabel||'')+'</div></div>'+
646
- '<div class="actions">'+
647
- btn(isPreview?'运行预览':'执行', 'data-plugin-act="run" data-plugin-id="'+id+'" data-action-id="'+aid+'"', isPreview?'primary':'')+
648
- '</div></div>';
649
- }
650
-
651
- function fmtDuration(ms){
652
- if(typeof ms!=='number'||!isFinite(ms)||ms<0)return '—';
653
- if(ms<1000)return Math.round(ms)+' ms';
654
- var sec=ms/1000;
655
- if(sec<60)return (sec<10?sec.toFixed(1):Math.round(sec))+' s';
656
- var min=Math.floor(sec/60), rem=Math.round(sec%60);
657
- return min+'m '+rem+'s';
658
- }
659
-
660
- function renderConnectivitySection(payload){
661
- payload=obj(payload);
662
- var conn=obj(payload.connectivity);
663
- var summary=obj(conn.summary);
664
- if(!summary.headline)return '';
665
- var latest=obj(conn.latestEvent);
666
- var attr=obj(latest.attribution||summary.latestAttribution);
667
- var observations=arr(latest.observations);
668
- var jobs=arr(latest.jobTimings);
669
- var counts=obj(summary.attributionCounts);
670
- var evidence=arr(attr.evidence);
671
- return '<div class="panel" style="margin-bottom:14px">'+
672
- '<div class="section-title"><h2 style="margin:0">连接稳定性</h2>'+pill(summary.tone||attr.tone||'gray', summary.statusLabel||attr.overallStatusLabel||'观察中')+'</div>'+
673
- '<p class="muted">'+esc(summary.headline||'')+'</p>'+
674
- '<div class="muted" style="display:flex;gap:10px;flex-wrap:wrap;margin-top:8px">'+
675
- '<span>归因:'+esc(attr.categoryLabel||'—')+'</span>'+
676
- '<span>置信度:'+esc(attr.confidenceLabel||'—')+'</span>'+
677
- '<span>最近探测:'+esc(summary.latestObservedAt||'—')+'</span>'+
678
- '<span>样本:'+esc(String(summary.eventsStored||0))+'</span>'+
679
- '</div>'+
680
- '<div class="evidence" style="margin-top:10px">'+
681
- '<span>公网路径 '+esc(String(counts.public_path||0))+'</span>'+
682
- '<span>Gateway '+esc(String(counts.gateway||0))+'</span>'+
683
- '<span>Local Bridge '+esc(String(counts.local_bridge||0))+'</span>'+
684
- '<span>Controller '+esc(String(counts.controller||0))+'</span>'+
685
- '<span>连接器/未知 '+esc(String(counts.connector_or_unknown||0))+'</span>'+
686
- '</div>'+
687
- (observations.length?'<div class="grid cards" style="margin-top:14px">'+observations.map(function(item){
688
- item=obj(item);
689
- return '<div class="panel" style="padding:12px;background:rgba(255,255,255,.02)">'+
690
- '<div class="section-title"><strong>'+esc(item.label||item.component||'组件')+'</strong>'+pill(item.tone||'gray', item.stateLabel||item.state||'未知')+'</div>'+
691
- '<p class="muted" style="margin:0">'+esc(item.detail||'')+'</p>'+
692
- '</div>';
693
- }).join('')+'</div>':'')+
694
- (jobs.length?'<div style="margin-top:14px"><div class="section-title"><h3 style="margin:0">最近 durable Job 时序</h3><span class="muted">基于 queued/dispatched/started/finished 持久时间戳</span></div><div class="list">'+jobs.map(function(job){
695
- job=obj(job);
696
- return '<div class="card-row" style="padding:12px"><div><h3 style="margin:0 0 6px">'+esc(job.operation||job.jobId||'job')+'</h3>'+
697
- '<p class="muted">Job '+esc(job.jobId||'—')+' · '+esc(job.statusLabel||job.status||'未知')+' · '+esc(job.stageLabel||'—')+'</p>'+
698
- '<div class="evidence">'+
699
- '<span>排队 '+esc(fmtDuration(job.queuedDelayMs))+'</span>'+
700
- '<span>派发到启动 '+esc(fmtDuration(job.dispatchToStartMs))+'</span>'+
701
- '<span>执行 '+esc(fmtDuration(job.executionDurationMs))+'</span>'+
702
- '<span>总计 '+esc(fmtDuration(job.totalMs))+'</span>'+
703
- '</div></div>'+pill((job.status==='failed'||job.status==='timed_out'||job.status==='orphaned'||job.status==='stale')?'red':(job.status==='running'||job.status==='dispatched')?'blue':'gray', job.statusLabel||job.status||'')+'</div>';
704
- }).join('')+'</div></div>':'')+
705
- (evidence.length?'<details class="advanced" style="margin-top:14px"><summary>归因证据</summary><ul style="margin:10px 0 0 18px;color:var(--muted);font-size:13px;line-height:1.55">'+evidence.map(function(line){return '<li>'+esc(line)+'</li>';}).join('')+'</ul></details>':'')+
706
- '</div>';
707
- }
708
-
709
- function renderAdvanced(){
710
- var root=document.getElementById('view-advanced');
711
- var payload=advancedRaw||lastFacade||{note:'点击“读取原始快照”加载调试数据。日常任务请使用指挥中心。'};
712
- root.innerHTML=
713
- '<div class="page-head"><div><h1>高级诊断</h1><p>仅用于排错。默认任务流请回到指挥中心,不要从这里开始日常开发。</p></div>'+
714
- '<div class="actions"><button class="btn" onclick="loadAdvanced()">读取原始快照</button>'+
715
- '<button class="btn" onclick="copyText(JSON.stringify(advancedRaw||lastFacade||{},null,2))">复制 JSON</button>'+
716
- btn('返回指挥中心','data-nav="home"','primary')+'</div></div>'+
717
- renderConnectivitySection(payload)+
718
- '<div class="panel"><div class="warn">调试信息默认折叠,避免干扰主流程。</div>'+
719
- '<details class="advanced" open><summary>原始 JSON(调试)</summary>'+
720
- '<pre class="mono" style="white-space:pre-wrap;font-size:12px;color:var(--muted);max-height:60vh;overflow:auto">'+esc(JSON.stringify(payload,null,2))+'</pre></details></div>';
721
- bindNav(root);
722
- }
723
-
724
- function renderAutomation(){
725
- var root=document.getElementById('view-automation');
726
- if(!root)return;
727
- var s=obj(automationSettings);
728
- if(!s.schemaVersion){
729
- root.innerHTML='<div class="page-head"><div><h1>模型与工具提供方</h1><p>配置 LLM、本地工具与路由偏好。</p></div>'+
730
- '<button class="btn primary" onclick="loadAutomationSettings()">加载配置</button></div>'+
731
- '<div class="empty">正在读取 Automation Settings…</div>';
732
- loadAutomationSettings();
733
- return;
734
- }
735
- var ov=obj(s.overview);
736
- var providers=arr(s.providers);
737
- var tools=arr(s.localTools);
738
- var creds=arr(s.credentials);
739
- var routing=obj(s.routing);
740
- var warnings=arr(s.warnings).map(function(w){return '<div class="warn">'+esc(w)+'</div>'}).join('');
741
- root.innerHTML=
742
- '<div class="page-head"><div><h1>模型与工具提供方</h1><p>控制 goal loop 可用哪些 LLM / 本地工具、优先级与安全策略。密钥只读状态,永不展示值。</p></div>'+
743
- '<div class="actions"><button class="btn" onclick="loadAutomationSettings()">刷新</button>'+
744
- '<button class="btn" onclick="previewRoute()">预览路由</button>'+
745
- '<button class="btn danger" onclick="resetProviders()">恢复默认</button></div></div>'+
746
- warnings+
747
- '<div class="panel" style="margin-bottom:14px"><h2>总览</h2>'+
748
- '<p class="muted">'+esc(ov.plainLanguageSummary||'')+'</p>'+
749
- '<div class="grid stats" style="margin-top:10px">'+
750
- '<div class="stat"><span>Goal loop</span><strong>'+(ov.goalLoopEnabled?'开':'关')+'</strong></div>'+
751
- '<div class="stat"><span>Live API(有效)</span><strong>'+(ov.liveModelProvidersEffective?'开':'关')+'</strong></div>'+
752
- '<div class="stat"><span>直接可用</span><strong>'+esc(String(ov.directProvidersReady||0))+'</strong></div>'+
753
- '<div class="stat"><span>需配置</span><strong>'+esc(String(ov.providersNeedingConfig||0))+'</strong></div>'+
754
- '</div>'+
755
- '<p class="faint" style="margin-top:10px">环境 Live 标志:'+(ov.liveModelProvidersEnv?'已设置':'未设置')+' · GUI 偏好:'+(ov.liveModelProvidersPreference?'已开启':'未开启')+' · 两者同时满足才允许远程 API 直接调度。</p>'+
756
- '<div class="actions" style="margin-top:10px">'+
757
- '<button class="btn" onclick="toggleLivePref('+(ov.liveModelProvidersPreference?'false':'true')+')">'+(ov.liveModelProvidersPreference?'关闭 GUI Live 偏好':'开启 GUI Live 偏好')+'</button>'+
758
- '<button class="btn" onclick="toggleGoalLoop('+(ov.goalLoopEnabled?'false':'true')+')">'+(ov.goalLoopEnabled?'暂停 Goal Loop':'启用 Goal Loop')+'</button>'+
759
- '</div>'+
760
- '<p class="muted" style="margin-top:10px">默认路由:小改动 <strong>'+esc(obj(ov.defaultRoutes).small_edit||'—')+'</strong> · 实现 <strong>'+esc(obj(ov.defaultRoutes).normal_code_task||'—')+'</strong> · 修复 <strong>'+esc(obj(ov.defaultRoutes).repair||'—')+'</strong> · 规划 <strong>'+esc(obj(ov.defaultRoutes).planning||'—')+'</strong></p>'+
761
- '</div>'+
762
- '<div class="panel" style="margin-bottom:14px"><div class="section-title"><h2>LLM 提供方</h2></div><div class="list">'+
763
- providers.map(function(p){return providerCardHtml(p)}).join('')+
764
- '</div></div>'+
765
- '<div class="panel" style="margin-bottom:14px"><div class="section-title"><h2>API 凭证状态</h2><span class="muted">env 或页面存储 · 永不显示明文</span></div>'+
766
- (creds.length?creds.map(function(c){
767
- return '<div class="card-row" style="margin-bottom:8px"><div>'+
768
- '<h3>'+esc(c.displayName)+'</h3>'+
769
- '<p class="muted">URL:'+esc(c.baseUrl||'—')+' · model:'+esc(c.model||'—')+'</p>'+
770
- '<p class="muted">env:'+(arr(c.presentEnvVars).length?esc(arr(c.presentEnvVars).join(', ')):'未设置')+
771
- ' · 页面密钥:'+(c.storedAuthPresent?esc(c.storedKeyHint||'已保存'):'未保存')+'</p>'+
772
- '<p class="faint">模式:'+esc(c.storageMode)+'</p>'+
773
- '<details class="advanced"><summary>Shell 示例</summary><pre class="mono" style="white-space:pre-wrap;font-size:12px;color:var(--muted)">'+esc(c.setupExample||'')+'</pre></details>'+
774
- '</div>'+pill(c.authPresent?'green':'amber', c.authPresent?'已配置':'缺失')+'</div>';
775
- }).join(''):'<div class="empty">无 API 凭证条目</div>')+
776
- '</div>'+
777
- '<div class="panel" style="margin-bottom:14px"><div class="section-title"><h2>本地执行工具</h2></div><div class="list">'+
778
- tools.map(function(t){
779
- return '<div class="card-row" style="margin-bottom:8px"><div>'+
780
- '<h3>'+esc(t.displayName)+'</h3>'+
781
- '<p class="muted">'+esc(t.summary||'')+'</p>'+
782
- (t.executablePath?'<p class="faint mono">'+esc(t.executablePath)+(t.version?(' · '+esc(t.version)):'')+'</p>':'')+
783
- '<div class="evidence">'+(arr(t.usedByWorkflows).map(function(w){return '<span>'+esc(w)+'</span>'}).join(''))+'</div>'+
784
- '</div><div class="actions" style="flex-direction:column">'+
785
- pill(t.status==='detected'?'green':t.status==='disabled'?'gray':'amber', t.status)+
786
- (t.enabled
787
- ? '<button class="btn" onclick="setToolEnabled(\\''+esc(t.toolId)+'\\',false)">禁用</button>'
788
- : '<button class="btn primary" onclick="setToolEnabled(\\''+esc(t.toolId)+'\\',true)">启用</button>')+
789
- '<button class="btn ghost" onclick="healthTool(\\''+esc(t.toolId)+'\\')">健康检查</button>'+
790
- '</div></div>';
791
- }).join('')+
792
- '</div></div>'+
793
- '<div class="panel" style="margin-bottom:14px"><div class="section-title"><h2>路由偏好</h2><button class="btn" onclick="resetRouting()">使用自动路由</button></div>'+
794
- '<p class="muted">自动路由会按提供方优先级、能力和健康状态选择;显式路由会覆盖通用优先级。Handoff-only(ChatGPT)只能作为回退。</p>'+
795
- ['implementation','repair','planning','review','browser_planning','ios_analysis'].map(function(key){
796
- var order=arr(obj(routing.orders)[key]);
797
- return '<div style="margin:10px 0;padding:10px;border:1px solid var(--line);border-radius:12px">'+
798
- '<strong>'+esc(key)+'</strong><div class="muted mono" style="margin-top:6px">'+esc(order.join(' → ')||'—')+'</div></div>';
799
- }).join('')+
800
- '<p class="faint">若无可直接调用提供方:forge 会创建 continuation packet,而不是假装调用 ChatGPT 会话。</p>'+
801
- '</div>';
802
- bindNav(root);
803
- }
804
-
805
- function providerCardHtml(p){
806
- p=obj(p);
807
- var id=esc(p.providerId);
808
- var handoff=!!p.handoffOnly;
809
- var api=obj(p.apiSettings);
810
- var apiForm='';
811
- if(api.configurable){
812
- var keyHint=api.hasStoredApiKey?('已保存密钥 '+(api.storedKeyHint||'••••')):(p.credential&&p.credential.envAuthPresent?'使用环境变量':'未配置密钥');
813
- apiForm=
814
- '<div class="setup" style="margin-top:10px">'+
815
- '<strong>API 配置</strong>'+
816
- '<p class="faint" style="margin:4px 0 8px">密钥仅存 controllerHome,不进 git;列表接口永不回显明文。</p>'+
817
- '<label class="faint">Base URL</label>'+
818
- '<input class="input" id="api-url-'+id+'" value="'+esc(api.baseUrl||api.defaultBaseUrl||'')+'" placeholder="'+esc(api.defaultBaseUrl||'https://…')+'">'+
819
- '<label class="faint" style="display:block;margin-top:8px">Model</label>'+
820
- '<input class="input" id="api-model-'+id+'" value="'+esc(api.model||api.defaultModel||'')+'" placeholder="'+esc(api.defaultModel||'model-id')+'">'+
821
- '<label class="faint" style="display:block;margin-top:8px">API Key(留空则保留原密钥)</label>'+
822
- '<input class="input" id="api-key-'+id+'" type="password" autocomplete="off" placeholder="'+esc(keyHint)+'">'+
823
- '<div class="actions" style="margin-top:8px">'+
824
- '<button class="btn primary" onclick="saveProviderApi(\\''+id+'\\')">保存 API 配置</button>'+
825
- (api.hasStoredApiKey?'<button class="btn danger" onclick="clearProviderApiKey(\\''+id+'\\')">清除已存密钥</button>':'')+
826
- '</div>'+
827
- '<p class="faint" style="margin-top:6px">状态:'+esc(keyHint)+
828
- (p.credential&&p.credential.envAuthPresent?' · 环境变量已就绪':'')+
829
- '</p>'+
830
- '</div>';
831
- }
832
- return '<div class="card-row" style="margin-bottom:10px;grid-template-columns:1fr"><div>'+
833
- '<div style="display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;align-items:start">'+
834
- '<div>'+
835
- '<div class="section-title" style="margin:0"><h3 style="margin:0">'+esc(p.displayName)+'</h3>'+pill(p.directDispatch?'green':handoff?'blue':p.status==='disabled'?'gray':'amber', p.statusLabel)+'</div>'+
836
- '<p class="muted">'+esc(p.kindLabel)+' · 优先级 '+esc(String(p.priority))+'</p>'+
837
- '<p class="muted">'+esc(p.explanation||p.summary||'')+'</p>'+
838
- '<div class="evidence">'+(arr(p.capabilities).slice(0,8).map(function(c){return '<span>'+esc(c)+'</span>'}).join(''))+'</div>'+
839
- '<p class="faint" style="margin-top:6px">文件直接改动:'+(p.safety&&p.safety.canMutateFilesDirectly?'是':'否')+
840
- ' · harness apply:'+(p.safety&&p.safety.requiresForgeApply?'是':'否')+
841
- ' · 外部副作用:'+esc((p.safety&&p.safety.externalSideEffects)||'approval_required')+'</p>'+
842
- (p.lastErrorSummary?'<p class="faint">最近错误码:'+esc(p.lastErrorSummary)+'</p>':'')+
843
- (handoff?'<div class="setup" style="margin-top:8px"><strong>Direct dispatch:不支持</strong><p class="muted" style="margin:6px 0 0">'+esc(p.explanation||'')+'</p></div>':'')+
844
- apiForm+
845
- '</div>'+
846
- '<div class="actions" style="flex-direction:column">'+
847
- (!handoff?(p.enabled
848
- ? '<button class="btn" onclick="setProviderEnabled(\\''+id+'\\',false)">禁用</button>'
849
- : '<button class="btn primary" onclick="setProviderEnabled(\\''+id+'\\',true)">启用</button>'):'<span class="pill blue">Handoff-only</span>')+
850
- (!handoff?'<button class="btn ghost" onclick="moveProvider(\\''+id+'\\',\\'up\\')">优先级↑</button><button class="btn ghost" onclick="moveProvider(\\''+id+'\\',\\'down\\')">优先级↓</button>':'')+
851
- '<button class="btn ghost" onclick="healthProvider(\\''+id+'\\')">健康检查</button>'+
852
- '</div>'+
853
- '</div>'+
854
- '</div></div>';
855
- }
856
-
857
- function loadAutomationSettings(){
858
- return api('/api/console/automation-settings'+repoQuery()).then(function(res){
859
- automationSettings=res;
860
- renderAutomation();
861
- }).catch(function(e){toast(e.message||'加载配置失败')});
862
- }
863
- function setProviderEnabled(id,on){
864
- setBusy(true,on?'启用提供方':'禁用提供方');
865
- api('/api/console/providers/'+encodeURIComponent(id)+'/'+(on?'enable':'disable')+repoQuery(),{method:'POST',body:'{}'}).then(function(){
866
- toast(on?'已启用':'已禁用');return loadAutomationSettings();
867
- }).catch(function(e){toast(e.message)}).finally(function(){setBusy(false)});
868
- }
869
- function moveProvider(id,dir){
870
- setBusy(true,'调整优先级');
871
- api('/api/console/providers/'+encodeURIComponent(id)+'/priority'+repoQuery(),{method:'POST',body:JSON.stringify({direction:dir})}).then(function(){
872
- toast('优先级已更新');return loadAutomationSettings();
873
- }).catch(function(e){toast(e.message)}).finally(function(){setBusy(false)});
874
- }
875
- function healthProvider(id){
876
- api('/api/console/providers/health'+repoQuery(),{method:'POST',body:JSON.stringify({providerId:id})}).then(function(res){
877
- toast((res.health&&res.health.summary)||'健康检查完成');
878
- advancedRaw=res;
879
- }).catch(function(e){toast(e.message)});
880
- }
881
- function saveProviderApi(id){
882
- var urlEl=document.getElementById('api-url-'+id);
883
- var modelEl=document.getElementById('api-model-'+id);
884
- var keyEl=document.getElementById('api-key-'+id);
885
- var body={
886
- baseUrl:urlEl?String(urlEl.value||'').trim():undefined,
887
- model:modelEl?String(modelEl.value||'').trim():undefined
888
- };
889
- if(keyEl&&String(keyEl.value||'').trim())body.apiKey=String(keyEl.value).trim();
890
- setBusy(true,'保存 API 配置');
891
- api('/api/console/providers/'+encodeURIComponent(id)+'/api-settings'+repoQuery(),{method:'POST',body:JSON.stringify(body)}).then(function(res){
892
- toast('已保存 '+id+(res.storedKeyHint?(' · 密钥 '+res.storedKeyHint):''));
893
- if(keyEl)keyEl.value='';
894
- return loadAutomationSettings();
895
- }).catch(function(e){toast(e.message)}).finally(function(){setBusy(false)});
896
- }
897
- function clearProviderApiKey(id){
898
- if(!confirm('清除页面保存的 API Key?(环境变量密钥不受影响)'))return;
899
- setBusy(true,'清除密钥');
900
- api('/api/console/providers/'+encodeURIComponent(id)+'/api-settings'+repoQuery(),{method:'POST',body:JSON.stringify({clearApiKey:true})}).then(function(){
901
- toast('已清除存储密钥');return loadAutomationSettings();
902
- }).catch(function(e){toast(e.message)}).finally(function(){setBusy(false)});
903
- }
904
- function setToolEnabled(id,on){
905
- setBusy(true,on?'启用工具':'禁用工具');
906
- api('/api/console/local-tools/'+encodeURIComponent(id)+'/'+(on?'enable':'disable')+repoQuery(),{method:'POST',body:'{}'}).then(function(){
907
- toast(on?'工具已启用':'工具已禁用');return loadAutomationSettings();
908
- }).catch(function(e){toast(e.message)}).finally(function(){setBusy(false)});
909
- }
910
- function healthTool(id){
911
- api('/api/console/local-tools/health'+repoQuery(),{method:'POST',body:JSON.stringify({toolId:id})}).then(function(res){
912
- toast((res.tool&&res.tool.summary)||'工具检查完成');
913
- }).catch(function(e){toast(e.message)});
914
- }
915
- function toggleLivePref(on){
916
- setBusy(true,'更新 Live 偏好');
917
- api('/api/console/provider-config'+repoQuery(),{method:'POST',body:JSON.stringify({preferLiveModelProviders:!!on})}).then(function(){
918
- toast(on?'已开启 GUI Live 偏好(仍需环境变量)':'已关闭 GUI Live 偏好');return loadAutomationSettings();
919
- }).catch(function(e){toast(e.message)}).finally(function(){setBusy(false)});
920
- }
921
- function toggleGoalLoop(on){
922
- setBusy(true,'更新 Goal Loop');
923
- api('/api/console/provider-config'+repoQuery(),{method:'POST',body:JSON.stringify({goalLoopEnabled:!!on})}).then(function(){
924
- toast(on?'Goal Loop 已启用':'Goal Loop 已暂停');return loadAutomationSettings();
925
- }).catch(function(e){toast(e.message)}).finally(function(){setBusy(false)});
926
- }
927
- function previewRoute(){
928
- api('/api/console/executor-route-preview'+repoQuery(),{method:'POST',body:JSON.stringify({task_intent:'code_implementation'})}).then(function(res){
929
- toast(res.explanation||res.whyThisProvider||'路由预览完成');
930
- advancedRaw=res;
931
- }).catch(function(e){toast(e.message)});
932
- }
933
- function resetProviders(){
934
- if(!confirm('恢复提供方默认配置?(不会删除环境变量)'))return;
935
- setBusy(true,'恢复默认');
936
- api('/api/console/providers/reset'+repoQuery(),{method:'POST',body:'{}'}).then(function(){
937
- toast('已恢复默认');return loadAutomationSettings();
938
- }).catch(function(e){toast(e.message)}).finally(function(){setBusy(false)});
939
- }
940
- function resetRouting(){
941
- if(!confirm('移除显式路由并恢复按提供方优先级、能力与健康状态自动选择?'))return;
942
- setBusy(true,'恢复自动路由');
943
- api('/api/console/executor-routing/reset'+repoQuery(),{method:'POST',body:'{}'}).then(function(){
944
- toast('已恢复自动路由');return loadAutomationSettings();
945
- }).catch(function(e){toast(e.message)}).finally(function(){setBusy(false)});
946
- }
947
-
948
- function bindNav(root){
949
- if(!root)return;
950
- root.querySelectorAll('[data-nav]').forEach(function(el){
951
- el.addEventListener('click',function(){switchView(el.getAttribute('data-nav'))});
952
- });
953
- bindActions(root);
954
- }
955
-
956
- function bindActions(root){
957
- if(!root)return;
958
- root.querySelectorAll('[data-work-act]').forEach(function(el){
959
- el.addEventListener('click',function(ev){ev.stopPropagation();workAction(el.getAttribute('data-work-act'),el.getAttribute('data-work-id'))});
960
- });
961
- root.querySelectorAll('[data-open-work]').forEach(function(el){
962
- el.addEventListener('click',function(ev){ev.stopPropagation();openWork(el.getAttribute('data-open-work'))});
963
- });
964
- root.querySelectorAll('[data-open-handoff]').forEach(function(el){
965
- el.addEventListener('click',function(ev){ev.stopPropagation();openHandoff(el.getAttribute('data-open-handoff'))});
966
- });
967
- root.querySelectorAll('[data-handoff-act]').forEach(function(el){
968
- el.addEventListener('click',function(ev){ev.stopPropagation();handoffAction(el.getAttribute('data-handoff-act'),el.getAttribute('data-handoff-id'))});
969
- });
970
- root.querySelectorAll('[data-select-repo]').forEach(function(el){
971
- el.addEventListener('click',function(ev){ev.stopPropagation();selectRepo(el.getAttribute('data-select-repo'))});
972
- });
973
- root.querySelectorAll('[data-remove-repo]').forEach(function(el){
974
- el.addEventListener('click',function(ev){
975
- ev.stopPropagation();
976
- removeRepo(el.getAttribute('data-remove-repo'), el.getAttribute('data-remove-repo-name'));
977
- });
978
- });
979
- root.querySelectorAll('[data-open-plugin]').forEach(function(el){
980
- el.addEventListener('click',function(ev){ev.stopPropagation();openPlugin(el.getAttribute('data-open-plugin'))});
981
- });
982
- root.querySelectorAll('[data-plugin-act]').forEach(function(el){
983
- el.addEventListener('click',function(ev){
984
- ev.stopPropagation();
985
- runPluginAction(el.getAttribute('data-plugin-id'), el.getAttribute('data-action-id'));
986
- });
987
- });
988
- root.querySelectorAll('[data-copy]').forEach(function(el){
989
- el.addEventListener('click',function(ev){ev.stopPropagation();copyText(el.getAttribute('data-copy-text')||'')});
990
- });
991
- }
992
-
993
- function renderAll(){
994
- if(!commandCenter){
995
- document.getElementById('view-home').innerHTML='<div class="panel"><h2>正在读取控制台状态</h2><p class="muted">如果长时间无响应,请刷新或打开高级诊断。</p><button class="btn" onclick="refreshAll()">重新载入</button></div>';
996
- return;
997
- }
998
- try{
999
- renderChrome();
1000
- renderHome();
1001
- renderInbox();
1002
- renderWork();
1003
- renderCapabilities();
1004
- renderAutomation();
1005
- renderReadiness();
1006
- renderRepositories();
1007
- renderAdvanced();
1008
- }catch(e){
1009
- toast(e.message||String(e));
1010
- }
1011
- }
1012
-
1013
- function openWork(id){rememberWork(id);switchView('work');refreshWorkDetail(id)}
1014
- function openHandoff(id){selectedHandoffId=id;switchView('inbox');renderInbox()}
1015
- function openPlugin(id){
1016
- rememberPlugin(id);
1017
- switchView('capabilities');
1018
- renderCapabilities();
1019
- // Refresh detail from console API for latest health/actions.
1020
- api('/api/console/plugins/'+encodeURIComponent(id)+repoQuery()).then(function(res){
1021
- if(res&&res.plugin){
1022
- commandCenter=commandCenter||{};
1023
- var list=arr(commandCenter.plugins);
1024
- var idx=list.findIndex(function(p){return p.id===id});
1025
- if(idx>=0)list[idx]=res.plugin; else list.push(res.plugin);
1026
- commandCenter.plugins=list;
1027
- commandCenter.pluginSummary=obj(commandCenter.pluginSummary);
1028
- renderCapabilities();
1029
- }
1030
- }).catch(function(){});
1031
- }
1032
- function refreshPlugins(){
1033
- api('/api/console/plugins'+repoQuery()).then(function(res){
1034
- commandCenter=commandCenter||{};
1035
- commandCenter.plugins=arr(res.plugins);
1036
- commandCenter.pluginSummary=obj(res.summary);
1037
- renderChrome();
1038
- renderCapabilities();
1039
- toast('插件状态已刷新');
1040
- }).catch(function(e){toast(e.message)});
1041
- }
1042
- function runPluginAction(pluginId, actionId){
1043
- if(busy){toast('上一步仍在处理,请稍候');return}
1044
- var plugin=pluginById(pluginId)||{};
1045
- var action=arr(plugin.actions).find(function(a){return a.id===actionId})||{id:actionId,title:actionId};
1046
- var confirmAuth=false;
1047
- var confirmationText=undefined;
1048
- if(action.requiredConfirmationText||action.confirmation==='strong_confirmation'){
1049
- var required=action.requiredConfirmationText||('CONFIRM '+actionId);
1050
- var typed=prompt('该动作不可逆,请输入强确认文本:'+required,'');
1051
- if(typed==null)return;
1052
- if(typed!==required){toast('确认文本不匹配');return}
1053
- confirmationText=typed;
1054
- confirmAuth=true;
1055
- }
1056
- var requestId='gui-'+Date.now().toString(36)+'-'+Math.random().toString(36).slice(2,8);
1057
- setBusy(true,'运行插件动作…');
1058
- api('/api/plugins/'+encodeURIComponent(pluginId)+'/actions/'+encodeURIComponent(actionId)+repoQuery(),{
1059
- method:'POST',
1060
- body:JSON.stringify({
1061
- requestId:requestId,
1062
- arguments:{},
1063
- confirmAuthorization:confirmAuth,
1064
- confirmationText:confirmationText
1065
- })
1066
- }).then(function(res){
1067
- pluginActionResult={
1068
- at:new Date().toISOString(),
1069
- pluginId:pluginId,
1070
- actionId:actionId,
1071
- accepted:res.accepted,
1072
- deduplicated:res.deduplicated,
1073
- job:res.job,
1074
- action:res.action
1075
- };
1076
- setLastOp({phase:'succeeded',statusLabel:'动作已提交',summary:'插件动作 '+(action.title||actionId)+' 已提交'},'插件动作已提交');
1077
- toast(res.deduplicated?'动作已去重复用':'插件动作已提交');
1078
- return refreshAll({silent:true}).then(function(){renderCapabilities()});
1079
- }).catch(function(e){
1080
- pluginActionResult={at:new Date().toISOString(),pluginId:pluginId,actionId:actionId,error:e.message,payload:e.payload||null};
1081
- setLastOp(e.payload||{phase:'failed',summary:e.message},e.message);
1082
- toast(e.message);
1083
- renderCapabilities();
1084
- }).finally(function(){setBusy(false)});
1085
- }
1086
- function copyText(text){if(!text){toast('没有可复制内容');return}navigator.clipboard.writeText(text).then(function(){toast('已复制')}).catch(function(){toast('复制失败')})}
1087
- function toggleAddRepo(){var el=document.getElementById('repoAdd');if(el)el.style.display=el.style.display==='none'?'block':'none'}
1088
-
1089
- function taskPayload(){
1090
- var objective=(document.getElementById('taskObjective')||{}).value||'';
1091
- var acceptance=((document.getElementById('taskAcceptance')||{}).value||'').split(/[;;\\n]/).map(function(s){return s.trim()}).filter(Boolean);
1092
- var paths=((document.getElementById('taskPaths')||{}).value||'').split(',').map(function(s){return s.trim()}).filter(Boolean);
1093
- var files=Number((document.getElementById('taskFiles')||{}).value||0)||undefined;
1094
- return {
1095
- objective:objective.trim(),
1096
- acceptanceCriteria:acceptance,
1097
- allowedPaths:paths,
1098
- expectedFiles:files,
1099
- scopeClear:true
1100
- };
1101
- }
1102
-
1103
- function previewMode(){
1104
- if(busy){toast('上一步仍在处理,请稍候');return}
1105
- var body=taskPayload();
1106
- if(!body.objective){toast('请先输入任务目标');return}
1107
- setBusy(true,'预览模式…');
1108
- api('/api/console/mode-preview'+repoQuery(),{method:'POST',body:JSON.stringify(body)}).then(function(res){
1109
- modePreview=res.modePreview||res;
1110
- setLastOp({phase:'succeeded',statusLabel:'模式已预览',summary:'建议:'+(modePreview.label||'直接执行')+'。'+(modePreview.explanation||'')},'模式已预览');
1111
- renderHome();
1112
- toast('建议模式:'+(modePreview.label||''));
1113
- }).catch(function(e){setLastOp(e.payload||{phase:'failed',summary:e.message},e.message);toast(e.message||'预览失败')}).finally(function(){setBusy(false)});
1114
- }
1115
-
1116
- function startTask(){
1117
- if(busy){toast('上一步仍在处理,请勿重复提交');return}
1118
- var body=taskPayload();
1119
- if(!body.objective){toast('请先输入任务目标');return}
1120
- setBusy(true,'正在提交任务…');
1121
- api('/api/console/work/start'+repoQuery(),{method:'POST',body:JSON.stringify(body)}).then(function(res){
1122
- setLastOp(res,'任务已提交');
1123
- var data=obj(res.data);
1124
- if(data.work&&data.work.workId){rememberWork(data.work.workId)}
1125
- else if(data.work&&data.work.id){rememberWork(data.work.id)}
1126
- else if(data.workId){rememberWork(data.workId)}
1127
- toast(res.summary||(res.status==='blocked'?'已创建待决定事项':'任务已提交'));
1128
- return refreshAll();
1129
- }).then(function(){
1130
- if(arr(obj(commandCenter).handoffs).length&&!selectedWorkId)switchView('inbox');
1131
- else if(selectedWorkId)switchView('work');
1132
- else switchView('home');
1133
- }).catch(function(e){
1134
- setLastOp(e.payload||{phase:'failed',summary:e.message||'启动失败'},e.message||'启动失败');
1135
- toast(e.message||'启动失败');
1136
- }).finally(function(){setBusy(false)});
1137
- }
1138
-
1139
- function diagnoseFirst(){
1140
- if(busy){toast('上一步仍在处理,请稍候');return}
1141
- setBusy(true,'正在诊断…');
1142
- api('/api/console/repair'+repoQuery(),{method:'POST',body:JSON.stringify({operation:'diagnose',dryRun:true})}).then(function(res){
1143
- setLastOp(res,'诊断完成');toast(res.summary||'诊断完成');switchView('readiness');
1144
- }).catch(function(e){setLastOp(e.payload||{phase:'failed',summary:e.message},e.message);toast(e.message)}).finally(function(){setBusy(false)});
1145
- }
1146
- function repairDryRun(){
1147
- if(busy){toast('上一步仍在处理,请稍候');return}
1148
- setBusy(true,'正在预览修复…');
1149
- api('/api/console/repair'+repoQuery(),{method:'POST',body:JSON.stringify({operation:'repair',dryRun:true})}).then(function(res){
1150
- setLastOp(res,'已生成修复预览');advancedRaw=res;toast(res.summary||'已生成修复预览');switchView('advanced');
1151
- }).catch(function(e){setLastOp(e.payload||{phase:'failed',summary:e.message},e.message);toast(e.message)}).finally(function(){setBusy(false)});
1152
- }
1153
-
1154
- function workAction(kind,workId){
1155
- if(busy){toast('上一步仍在处理,请稍候');return}
1156
- rememberWork(workId);
1157
- if(kind==='continue')return postWork('/api/console/work/continue',{workId:workId},'继续任务');
1158
- if(kind==='verify')return postWork('/api/console/work/verify',{workId:workId},'运行验证');
1159
- if(kind==='finalize')return postWork('/api/console/work/finalize',{workId:workId},'收尾任务');
1160
- if(kind==='stop')return postWork('/api/console/work/stop',{workId:workId,reason:'stopped from console'},'停止任务');
1161
- if(kind==='repair'){
1162
- setBusy(true,'正在诊断…');
1163
- return api('/api/console/repair'+repoQuery(),{method:'POST',body:JSON.stringify({operation:'diagnose',dryRun:true,workId:workId})}).then(function(res){setLastOp(res,'诊断完成');toast(res.summary||'诊断完成')}).catch(function(e){setLastOp(e.payload||{phase:'failed',summary:e.message},e.message);toast(e.message)}).finally(function(){setBusy(false)});
1164
- }
1165
- if(kind==='delegate-codex')return postWork('/api/console/work/delegate',{workId:workId,target:'codex',available:true},'委派 Codex');
1166
- if(kind==='delegate-grok')return postWork('/api/console/work/delegate',{workId:workId,target:'grok'},'准备 Grok 审阅包');
1167
- }
1168
- function postWork(path,body,label){
1169
- setBusy(true,label||'处理中…');
1170
- return api(path+repoQuery(),{method:'POST',body:JSON.stringify(body)}).then(function(res){
1171
- setLastOp(res,label||'已更新');
1172
- // Clarify handoff-only / request-packet outcomes for small-brain delegation.
1173
- if(res&&res.data&&res.data.directExecutionAvailable===false){
1174
- toast((res.summary||'已准备外部审阅请求')+'(非自动执行)');
1175
- } else {
1176
- toast(res.summary||'已更新');
1177
- }
1178
- return refreshAll();
1179
- }).catch(function(e){setLastOp(e.payload||{phase:'failed',summary:e.message},e.message);toast(e.message)}).finally(function(){setBusy(false)});
1180
- }
1181
- function refreshWorkDetail(id){
1182
- api('/api/console/work/'+encodeURIComponent(id)+repoQuery()).then(function(res){
1183
- if(res.work){
1184
- // merge into command center recent
1185
- commandCenter=commandCenter||{};
1186
- commandCenter.currentWork=res.work;
1187
- renderWork();
1188
- }
1189
- }).catch(function(){});
1190
- }
1191
-
1192
- function handoffAction(kind,id){
1193
- if(busy){toast('上一步仍在处理,请稍候');return}
1194
- var item=arr(obj(commandCenter).handoffs).find(function(h){return h.id===id})||{};
1195
- var d=obj(item.decision);var body={};
1196
- if(kind==='approve'){
1197
- var approvalText='确认'+(d.primaryActionLabel||'批准并创建任务')+'?\\n\\n批准对象:'+(d.requestedAction||item.title||'当前操作')+'\\n影响:'+(d.impact||'按当前任务范围执行')+'\\n后续:'+(d.afterApproval||'打开任务详情');
1198
- if(!confirm(approvalText))return;
1199
- }
1200
- if(kind==='resolve'){
1201
- var decision=prompt('记录你的决定。注意:此操作只保存决定并移出待处理,不会自动继续任务。',item.recommendedDecision||'');
1202
- if(decision==null)return;
1203
- if(!String(decision).trim()){toast('请填写决定内容');return}
1204
- body={decision:decision,resolver:'user'};
1205
- }
1206
- if(kind==='dismiss'){
1207
- if(!confirm('确认暂不处理此事项?它会从待处理列表移除,相关操作不会执行。'))return;
1208
- body={decision:'dismissed',resolver:'user'};
1209
- }
1210
- setBusy(true,kind==='approve'?'正在批准并创建任务…':kind==='resolve'?'仅记录决定…':kind==='dismiss'?'暂不处理…':'更新中…');
1211
- api('/api/console/inbox/'+encodeURIComponent(id)+'/'+kind+repoQuery(),{method:'POST',body:JSON.stringify(body)}).then(function(res){
1212
- if(kind==='approve'){
1213
- setLastOp(res,'已批准并创建任务');
1214
- var data=obj(obj(res.actionResult).data);var work=obj(data.work);
1215
- if(work.workId)rememberWork(work.workId);else if(work.id)rememberWork(work.id);
1216
- toast(res.summary||'已批准并创建任务');
1217
- } else {
1218
- setLastOp({phase:'succeeded',statusLabel:kind==='resolve'?'已记录决定':kind==='dismiss'?'已暂不处理':'已确认',summary:kind==='resolve'?'决定已保存;未触发任何后续执行':kind==='dismiss'?'事项已移出列表,相关操作未执行':'已确认已知晓'}, '已更新');
1219
- toast(kind==='resolve'?'已记录决定(未自动继续)':kind==='dismiss'?'已暂不处理':'已确认');
1220
- }
1221
- selectedHandoffId='';
1222
- return refreshAll().then(function(){if(kind==='approve'&&selectedWorkId)switchView('work')});
1223
- }).catch(function(e){setLastOp(e.payload||{phase:'failed',summary:e.message||'更新失败'},e.message||'更新失败');toast(e.message||'更新失败')}).finally(function(){setBusy(false)});
1224
- }
1225
-
1226
- function selectRepo(id){
1227
- rememberRepo(id);
1228
- toast('已切换仓库');
1229
- refreshAll().then(function(){switchView('home')});
1230
- }
1231
- function registerRepo(){
1232
- var path=(document.getElementById('repoPath')||{}).value||'';
1233
- var name=(document.getElementById('repoName')||{}).value||'';
1234
- if(!path.trim()){toast('请输入仓库路径');return}
1235
- api('/api/repositories/register',{method:'POST',body:JSON.stringify({path:path.trim(),displayName:name.trim()||undefined})}).then(function(res){
1236
- var repo=obj(res.repository);if(repo.repoId)rememberRepo(repo.repoId);
1237
- toast('仓库已注册');return refreshAll();
1238
- }).catch(function(e){toast(e.message)});
1239
- }
1240
- function removeRepo(id, name){
1241
- if(!id)return;
1242
- var label=name||id;
1243
- if(!confirm('从控制台删除「'+label+'」的注册?\\n不会删除磁盘上的仓库文件,仅从 Controller 注册表中移除(可重新注册)。'))return;
1244
- if(busy)return;
1245
- setBusy(true,'删除注册中');
1246
- api('/api/repositories/'+encodeURIComponent(id)+'/remove',{method:'POST',body:JSON.stringify({})}).then(function(res){
1247
- if(selectedRepoId===id){
1248
- var remaining=arr(res.repositories);
1249
- var next=remaining.find(function(r){return r.id&&r.id!==id})||remaining[0];
1250
- rememberRepo(next&&next.id?next.id:'');
1251
- }
1252
- toast(res.summary||'已删除仓库注册');
1253
- return refreshAll();
1254
- }).catch(function(e){
1255
- var msg=e.message||'删除失败';
1256
- if(String(msg).indexOf('REPOSITORY_SELF_PROTECTED')===0||(e.payload&&e.payload.errorMessage)){
1257
- msg=(e.payload&&e.payload.errorMessage)||'不能删除当前进程所在仓库的注册(自我保护)';
1258
- }
1259
- toast(msg);
1260
- }).finally(function(){setBusy(false)});
1261
- }
1262
- function loadAdvanced(){
1263
- api('/api/console/advanced'+repoQuery()).then(function(res){advancedRaw=res;renderAdvanced();toast('原始快照已刷新')}).catch(function(e){toast(e.message)});
1264
- }
1265
-
1266
- function refreshAll(opts){
1267
- opts=opts||{};
1268
- if(refreshInFlight)return Promise.resolve();
1269
- refreshInFlight=true;
1270
- return api('/api/console/command-center'+repoQuery()).then(function(res){
1271
- commandCenter=res;
1272
- lastRefreshedAt=new Date().toLocaleTimeString();
1273
- var repo=obj(res.currentRepository);
1274
- if(repo.id)rememberRepo(repo.id);
1275
- if(!selectedWorkId&&res.currentWork&&res.currentWork.id)rememberWork(res.currentWork.id);
1276
- // Merge extras into current work for card display after silent polls.
1277
- if(res.currentWork&&res.currentWork.id)res.currentWork=mergeWorkExtras(res.currentWork);
1278
- var loadSettings=(!opts.silent||!automationSettings)
1279
- ? api('/api/console/automation-settings'+repoQuery()).then(function(s){automationSettings=s}).catch(function(){/* optional */})
1280
- : Promise.resolve();
1281
- return loadSettings.then(function(){
1282
- if(!opts.silent)renderAll();
1283
- else {
1284
- renderChrome();
1285
- renderOpBar();
1286
- try{renderHome();renderWork();renderInbox()}catch(_e){renderAll()}
1287
- }
1288
- ensurePoll();
1289
- });
1290
- }).catch(function(e){
1291
- if(opts.silent){
1292
- lastOperation={phase:'failed',statusLabel:'重连中',summary:e.message||'控制台暂时不可用,正在重试…',at:new Date().toISOString()};
1293
- renderOpBar();
1294
- return;
1295
- }
1296
- commandCenter={
1297
- readiness:{state:'blocked',label:'读取失败',headline:'控制台不可用',description:e.message||'本地 API 暂时不可用',connectorLabel:'未知',connectorTone:'red',pendingHandoffCount:0,sections:[]},
1298
- handoffs:[],recentWork:[],repositories:[],warnings:[e.message||'读取失败'],modePreviewDefault:{label:'—',explanation:''},
1299
- setupGuide:{needed:true,title:'控制台暂不可用',body:e.message||'请确认 controller 已启动。',actionLabel:'重试'}
1300
- };
1301
- setLastOp({phase:'failed',statusLabel:'读取失败',summary:e.message||'本地 API 暂时不可用',error:{title:'控制器暂不可用',explanation:e.message||'本地 API 暂时不可用',nextActions:['刷新页面','重启 controller']}},e.message);
1302
- renderAll();
1303
- toast(e.message||'读取失败');
1304
- }).finally(function(){refreshInFlight=false});
1305
- }
1306
-
1307
- window.onerror=function(message){toast(String(message));return false};
1308
- refreshAll();
1309
- </script>
12
+ <div id="app"><div class="boot-state" role="status">正在读取 Forge 配置…</div></div>
13
+ <noscript>Forge Utility Console 需要启用 JavaScript。</noscript>
14
+ <script type="module" src="/console-assets/app.js"></script>
1310
15
  </body>
1311
16
  </html>`;
1312
17
  }