@moretea-labs/forge 1.5.0 → 1.6.0

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 (138) hide show
  1. package/CHANGELOG.md +28 -1
  2. package/DESIGN.md +7 -2
  3. package/README.md +7 -7
  4. package/README.zh-CN.md +5 -3
  5. package/THIRD_PARTY_NOTICES.md +1 -1
  6. package/assets/hooks/pre-edit-guard.sh +8 -39
  7. package/assets/hooks/prompt-guard.sh +2 -2
  8. package/assets/plugin-registry.v1.json +54 -3
  9. package/assets/reference-configs/hook-operations.md +1 -1
  10. package/assets/reference-configs/sprint-contracts.md +2 -2
  11. package/assets/skill-version.json +2 -2
  12. package/assets/templates/helpers/ensure-task-workflow.sh +2 -2
  13. package/assets/templates/helpers/sprint-backlog.sh +6 -6
  14. package/docs/architecture/current/automation-and-schedule-engine.md +9 -3
  15. package/docs/architecture/current/codegraph-context-provider.md +15 -3
  16. package/docs/architecture/current/dispatch-and-agent-strategy.md +32 -13
  17. package/docs/architecture/current/entity-model.md +20 -0
  18. package/docs/architecture/current/external-plugin-governance.md +3 -3
  19. package/docs/architecture/current/system-overview.md +26 -1
  20. package/docs/architecture/current/thin-harness-v1.md +10 -0
  21. package/docs/forge-chatgpt-mcp-setup.md +27 -3
  22. package/docs/operations/chatgpt-personal-assistant-runtime.md +12 -2
  23. package/docs/operations/controller-browser-plugin.md +12 -16
  24. package/docs/operations/features.md +1 -1
  25. package/docs/operations/homebrew.md +2 -2
  26. package/docs/public-usage-guide.md +1 -1
  27. package/docs/public-usage-guide.zh-CN.md +1 -1
  28. package/docs/tutorials/01-install-and-start.md +6 -4
  29. package/docs/tutorials/01-install-and-start.zh-CN.md +3 -1
  30. package/docs/tutorials/02-connect-chatgpt.md +8 -2
  31. package/docs/tutorials/02-connect-chatgpt.zh-CN.md +8 -2
  32. package/docs/wiki/Installation.md +1 -1
  33. package/docs/wiki/Quick-Start.md +1 -1
  34. package/package.json +1 -1
  35. package/scripts/check-public-docs.sh +14 -1
  36. package/scripts/migrate-chatgpt-automation-schedules.ts +64 -0
  37. package/scripts/sprint-backlog.sh +1 -1
  38. package/src/cli/commands/chatgpt.ts +31 -2
  39. package/src/cli/commands/init-hook.ts +3 -1
  40. package/src/cli/commands/mcp.ts +2 -0
  41. package/src/cli/commands/plugin.ts +36 -9
  42. package/src/cli/commands/runtime.ts +1 -0
  43. package/src/cli/commands/setup-profile.ts +49 -18
  44. package/src/cli/controller/context-pack.ts +158 -6
  45. package/src/cli/hook/prompt-guard-decision.ts +17 -18
  46. package/src/cli/hook/prompt-intents.ts +1 -1
  47. package/src/cli/local-bridge/console-assets.ts +17 -0
  48. package/src/cli/local-bridge/console-automations.ts +205 -23
  49. package/src/cli/local-bridge/console-view-models.ts +2 -0
  50. package/src/cli/local-bridge/facade-api.ts +2 -0
  51. package/src/cli/local-bridge/frontend/src/App.tsx +1 -1
  52. package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +143 -3
  53. package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +1 -1
  54. package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +181 -3
  55. package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +1 -1
  56. package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +4 -3
  57. package/src/cli/local-bridge/frontend/src/lib/api.ts +18 -6
  58. package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +1 -1
  59. package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +2 -1
  60. package/src/cli/local-bridge/frontend/src/styles.css +27 -11
  61. package/src/cli/local-bridge/frontend/src/types.ts +6 -2
  62. package/src/cli/local-bridge/server.ts +31 -20
  63. package/src/cli/local-bridge/ui-dist/app.css +1 -1
  64. package/src/cli/local-bridge/ui-dist/app.js +5003 -4815
  65. package/src/cli/mcp/auth.ts +2 -0
  66. package/src/cli/mcp/paths.ts +8 -2
  67. package/src/cli/mcp/repository-tools.ts +124 -9
  68. package/src/cli/mcp/server.ts +164 -19
  69. package/src/cli/mcp/setup.ts +9 -1
  70. package/src/cli/mcp/transports/http.ts +52 -2
  71. package/src/cli/mcp/transports/session-registry.ts +24 -0
  72. package/src/cli/repositories/command-classifier.ts +113 -0
  73. package/src/cli/repositories/safe-patch.ts +6 -4
  74. package/src/cli/repositories/structured-git.ts +49 -7
  75. package/src/cli/repository/inspector.ts +16 -3
  76. package/src/runtime/assistant/intent.ts +3 -3
  77. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +157 -2
  78. package/src/runtime/control-plane/execution/execution-identity.ts +65 -14
  79. package/src/runtime/control-plane/execution/validation.ts +15 -2
  80. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +32 -0
  81. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +6 -7
  82. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +2 -1
  83. package/src/runtime/control-plane/facade/capability-registry.ts +1 -1
  84. package/src/runtime/control-plane/facade/controller-session-store.ts +12 -13
  85. package/src/runtime/control-plane/facade/goal-workloop-access.ts +7 -2
  86. package/src/runtime/control-plane/facade/goal-workloop.ts +231 -8
  87. package/src/runtime/control-plane/facade/plan-contract-store.ts +32 -12
  88. package/src/runtime/control-plane/facade/requirement-board.ts +24 -8
  89. package/src/runtime/control-plane/facade/types.ts +33 -6
  90. package/src/runtime/control-plane/facade/work-contract-store.ts +31 -2
  91. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +375 -23
  92. package/src/runtime/control-plane/persistence/requirement-store.ts +15 -11
  93. package/src/runtime/diagnostics/mcp-timing.ts +6 -0
  94. package/src/runtime/execution/process-runtime/check-facade.ts +1 -10
  95. package/src/runtime/execution/process-runtime/check-result.ts +10 -2
  96. package/src/runtime/execution/process-runtime/command-facade.ts +1 -0
  97. package/src/runtime/execution/process-runtime/gc.ts +64 -10
  98. package/src/runtime/execution/process-runtime/persisted-check.ts +10 -2
  99. package/src/runtime/execution/process-runtime/resource-claims.ts +87 -2
  100. package/src/runtime/execution/process-runtime/runtime.ts +16 -1
  101. package/src/runtime/execution/process-runtime/store.ts +6 -0
  102. package/src/runtime/gateway/mcp/execution-tools.ts +27 -12
  103. package/src/runtime/gateway/mcp/router.ts +1 -1
  104. package/src/runtime/gateway/mcp/runtime-tools.ts +482 -178
  105. package/src/runtime/model-clients/deepseek-adapter.ts +0 -32
  106. package/src/runtime/plugins/browser-adapter.ts +19 -72
  107. package/src/runtime/plugins/browser-handoff-host.ts +6 -31
  108. package/src/runtime/plugins/browser-handoff.ts +0 -3
  109. package/src/runtime/plugins/browser-macos-bridge.ts +8 -0
  110. package/src/runtime/plugins/desktop-operator-registration.ts +8 -11
  111. package/src/runtime/plugins/external-registration.ts +84 -5
  112. package/src/runtime/plugins/first-party-registry.ts +2 -2
  113. package/src/runtime/plugins/gmail-adapter.ts +28 -2
  114. package/src/runtime/plugins/local-system-adapter.ts +2 -2
  115. package/src/runtime/plugins/plugin-management-adapter.ts +140 -0
  116. package/src/runtime/plugins/store.ts +107 -5
  117. package/src/runtime/plugins/types.ts +4 -0
  118. package/src/runtime/recovery/actions.ts +0 -10
  119. package/src/runtime/recovery/classifier.ts +0 -9
  120. package/src/runtime/recovery/maintenance-executor.ts +43 -11
  121. package/src/runtime/recovery/probe.ts +0 -1
  122. package/src/runtime/recovery/types.ts +0 -1
  123. package/src/runtime/root/package-connector-service.ts +136 -0
  124. package/src/runtime/root/package-runtime-service.ts +25 -12
  125. package/src/runtime/root/release-materialize.ts +161 -18
  126. package/src/runtime/root/service.ts +6 -4
  127. package/src/runtime/safe-tooling/index.ts +0 -1
  128. package/src/runtime/safe-tooling/types.ts +0 -30
  129. package/src/runtime/standalone-recovery/core.ts +2 -2
  130. package/src/runtime/workflow/schedules/browser-probe.ts +180 -0
  131. package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +40 -0
  132. package/src/runtime/workflow/schedules/engine.ts +357 -64
  133. package/src/runtime/workflow/schedules/types.ts +5 -0
  134. package/src/runtime/workflow/schedules/work-continuation.ts +100 -27
  135. package/bin/forge-desktop-helper.mjs +0 -204
  136. package/docs/operations/controller-desktop-plugin.md +0 -112
  137. package/src/runtime/plugins/desktop-adapter.ts +0 -374
  138. package/src/runtime/safe-tooling/browser-safe.ts +0 -133
@@ -112,40 +112,38 @@ function isExecutionIntent(
112
112
  );
113
113
  }
114
114
 
115
+ function explicitPlanExecution(intent: PromptGuardExecutionIntent): boolean {
116
+ return intent === 'embedded_approved_plan' || intent === 'plan_execution_projection';
117
+ }
118
+
115
119
  function decideNoActivePlanAction(
116
120
  intent: PromptGuardExecutionIntent,
117
121
  state: PromptGuardState,
118
122
  ): PromptGuardAction {
119
- if (intent !== 'bug_fix_execution' && state.pending === 'fresh') {
120
- return 'plan_capture_pending_advice';
123
+ // Host hooks never decide that ordinary implementation requires a Plan.
124
+ // ChatGPT may inspect first and then explicitly commit to Plan execution.
125
+ if (!explicitPlanExecution(intent)) {
126
+ return state.worktree === 'linked_target' ? 'worktree_execution_advice' : 'allow';
121
127
  }
128
+ if (state.pending === 'fresh') return 'plan_capture_pending_advice';
122
129
  if (state.worktree === 'linked_target') return 'worktree_execution_advice';
123
- if (intent === 'plan_execution_projection') {
124
- return 'plan_capture_missing_active_advice';
125
- }
126
- return 'plan_status_no_active_block';
130
+ return 'plan_capture_missing_active_advice';
127
131
  }
128
132
 
129
133
  function decideDraftPlanAction(
130
134
  intent: PromptGuardExecutionIntent,
131
135
  ): PromptGuardAction {
132
- if (intent === 'plan_execution_projection') return 'plan_capture_draft_advice';
133
- return 'plan_status_not_approved_block';
136
+ return explicitPlanExecution(intent) ? 'plan_capture_draft_advice' : 'allow';
134
137
  }
135
138
 
136
139
  function decideApprovedPlanAction(
137
140
  intent: PromptGuardExecutionIntent,
138
141
  state: PromptGuardState,
139
142
  ): PromptGuardAction {
140
- if (state.evidence === 'incomplete') return 'evidence_contract_block';
141
- if (
142
- state.plan === 'approved' &&
143
- intent === 'plan_execution_projection' &&
144
- state.contract !== 'present'
145
- ) {
146
- return 'plan_execution_scaffold_advice';
147
- }
148
- if (state.contract !== 'present') return 'contract_missing_block';
143
+ // Plan contracts may guide an explicitly selected Plan execution, but their
144
+ // presence/evidence is not a semantic permission gate for ordinary edits.
145
+ if (!explicitPlanExecution(intent)) return 'allow';
146
+ if (state.contract !== 'present') return 'plan_execution_scaffold_advice';
149
147
  return 'allow';
150
148
  }
151
149
 
@@ -233,6 +231,7 @@ export function decidePromptGuardAction(
233
231
  ): PromptGuardAction {
234
232
  if (intent === 'done') return decideDoneAction(state);
235
233
  if (!isExecutionIntent(intent)) return 'allow';
236
- if (state.spec === 'missing') return 'spec_block';
234
+ // Product/spec/Plan state is context for the Controller, not execution
235
+ // authorization. Hard path, permission and resource safety live elsewhere.
237
236
  return PROMPT_GUARD_EXECUTION_TABLE[state.plan][intent](state);
238
237
  }
@@ -401,7 +401,7 @@ export function isThinkPlanStartIntent(ctx: PromptIntentContext): boolean {
401
401
  if (isBugOrHuntIntent(ctx)) return false;
402
402
  if (THINK_COMMAND.test(ctx.text)) return true;
403
403
  if (isPlanConsultationIntent(ctx)) return false;
404
- return THINK_PLAN_PHRASE.test(ctx.text) || isPlainFeaturePlanStartIntent(ctx);
404
+ return THINK_PLAN_PHRASE.test(ctx.text);
405
405
  }
406
406
 
407
407
  export function normalizePlanSlug(value: string): string {
@@ -0,0 +1,17 @@
1
+ import { existsSync, readFileSync } from "fs";
2
+ import { dirname, join } from "path";
3
+
4
+ export type ConsoleAssetName = "app.js" | "app.css";
5
+
6
+ export function resolveConsoleAssetPath(
7
+ asset: ConsoleAssetName,
8
+ executablePath = process.execPath,
9
+ ): string | URL {
10
+ const releaseAssetPath = join(dirname(executablePath), "ui-dist", asset);
11
+ if (existsSync(releaseAssetPath)) return releaseAssetPath;
12
+ return new URL(`./ui-dist/${asset}`, import.meta.url);
13
+ }
14
+
15
+ export function readConsoleAsset(asset: ConsoleAssetName): string {
16
+ return readFileSync(resolveConsoleAssetPath(asset), "utf8");
17
+ }
@@ -2,10 +2,22 @@ import type { RepositoryRecord } from '../repositories/types';
2
2
  import { listAssistantRoutines } from '../../runtime/assistant/store';
3
3
  import { runAssistantRoutineNow } from '../../runtime/assistant/intent';
4
4
  import { getAssistantRoutineScheduleBinding, updateAssistantRoutineLifecycle } from '../../runtime/assistant/schedule-binding';
5
+ import { getWorkContract } from '../../runtime/control-plane/facade';
5
6
  import { evaluateSchedule } from '../../runtime/workflow/schedules/engine';
6
7
  import { getSchedule, listOccurrences, listSchedules, saveSchedule } from '../../runtime/workflow/schedules/store';
7
8
  import type { RepositorySchedule, ScheduleOccurrence } from '../../runtime/workflow/schedules/types';
8
9
 
10
+ export type ConsoleAutomationMode = 'browser_watch' | 'browser_keepalive' | 'continuation' | 'chatgpt_prompt' | 'routine' | 'schedule';
11
+
12
+ export interface ConsoleAutomationHistoryView {
13
+ id: string;
14
+ at: string;
15
+ result: string;
16
+ tone: 'green' | 'amber' | 'red' | 'blue' | 'gray';
17
+ reason?: string;
18
+ trigger?: string;
19
+ }
20
+
9
21
  export interface ConsoleAutomationView {
10
22
  id: string;
11
23
  source: 'schedule' | 'routine';
@@ -17,23 +29,46 @@ export interface ConsoleAutomationView {
17
29
  schedule: string;
18
30
  timezone?: string;
19
31
  delivery?: string;
32
+ mode: ConsoleAutomationMode;
33
+ modeLabel: string;
34
+ live?: boolean;
35
+ targetLabel?: string;
36
+ boundWorkId?: string;
37
+ boundWorkObjective?: string;
38
+ observationStatus?: RepositorySchedule['lastObservationStatus'];
39
+ observationAt?: string;
40
+ failureCount?: number;
41
+ policySummary?: string;
42
+ agentModel?: string;
43
+ reasoningLevel?: string;
44
+ tabPolicy?: string;
20
45
  lastRunAt?: string;
21
46
  lastResult?: string;
22
47
  nextRunHint?: string;
23
48
  pausedReason?: string;
49
+ history: ConsoleAutomationHistoryView[];
24
50
  actions: Array<'run' | 'pause' | 'resume'>;
25
51
  }
26
52
 
53
+ function dailyCronLabel(expression?: string): string | undefined {
54
+ const match = /^(\d{1,2})\s+(\d{1,2})\s+\*\s+\*\s+\*$/.exec(expression?.trim() ?? '');
55
+ if (!match) return undefined;
56
+ const minute = Number(match[1]);
57
+ const hour = Number(match[2]);
58
+ if (minute > 59 || hour > 23) return undefined;
59
+ return `每天 ${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
60
+ }
61
+
27
62
  function triggerLabel(schedule: RepositorySchedule): string {
28
63
  const trigger = schedule.trigger;
29
64
  switch (trigger.type) {
30
- case 'interval': return `Every ${trigger.everyMinutes ?? '?'} min`;
31
- case 'cron': return `Cron ${trigger.cronExpression ?? '—'}`;
32
- case 'calendar': return trigger.calendarAt ? `At ${trigger.calendarAt}` : 'Calendar';
33
- case 'condition': return `Watch ${trigger.condition?.kind ?? 'condition'}`;
34
- case 'repository-event': return `On ${trigger.eventName ?? 'repository event'}`;
35
- case 'dependency-checkpoint': return 'Dependency checkpoint';
36
- case 'manual': return 'Manual';
65
+ case 'interval': return `每 ${trigger.everyMinutes ?? '?'} 分钟`;
66
+ case 'cron': return dailyCronLabel(trigger.cronExpression) ?? `Cron ${trigger.cronExpression ?? '—'}`;
67
+ case 'calendar': return trigger.calendarAt ? `单次 · ${trigger.calendarAt}` : '指定时间';
68
+ case 'condition': return `条件满足时 · ${trigger.condition?.kind ?? 'condition'}`;
69
+ case 'repository-event': return `仓库事件 · ${trigger.eventName ?? 'repository event'}`;
70
+ case 'dependency-checkpoint': return '依赖完成时';
71
+ case 'manual': return '仅手动运行';
37
72
  }
38
73
  }
39
74
 
@@ -45,11 +80,11 @@ function nextScheduleHint(schedule: RepositorySchedule): string | undefined {
45
80
  const base = Date.parse(schedule.lastTriggeredAt ?? schedule.createdAt);
46
81
  if (Number.isFinite(base)) {
47
82
  const next = base + schedule.trigger.everyMinutes * 60_000;
48
- return next > Date.now() ? new Date(next).toISOString() : 'Due on next scheduler tick';
83
+ return next > Date.now() ? new Date(next).toISOString() : '下一次调度周期';
49
84
  }
50
85
  }
51
- if (schedule.trigger.type === 'cron') return 'Scheduler-calculated';
52
- if (schedule.trigger.type === 'condition') return 'When condition matches';
86
+ if (schedule.trigger.type === 'cron') return dailyCronLabel(schedule.trigger.cronExpression) ?? '由 Scheduler 计算';
87
+ if (schedule.trigger.type === 'condition') return '条件满足时';
53
88
  return undefined;
54
89
  }
55
90
 
@@ -60,13 +95,135 @@ function safeBoundSchedule(controllerHome: string, repoId: string, scheduleId?:
60
95
 
61
96
  function occurrenceResult(occurrence?: ScheduleOccurrence): string | undefined {
62
97
  if (!occurrence) return undefined;
63
- if (occurrence.decision === 'nothing_to_do') return 'No action needed';
64
- if (occurrence.status === 'failed') return 'Failed';
65
- if (occurrence.status === 'succeeded') return 'Succeeded';
66
- if (occurrence.status === 'running' || occurrence.status === 'queued' || occurrence.status === 'created') return 'Running';
98
+ if (occurrence.decision === 'nothing_to_do') return '无变化';
99
+ if (occurrence.decision === 'would_execute') return '预演完成';
100
+ if (occurrence.decision === 'stopped') return '已停止';
101
+ if (occurrence.status === 'failed') return '失败';
102
+ if (occurrence.status === 'succeeded') return '执行成功';
103
+ if (occurrence.status === 'running' || occurrence.status === 'queued' || occurrence.status === 'created') return '运行中';
104
+ if (occurrence.status === 'skipped') return '已跳过';
67
105
  return occurrence.status;
68
106
  }
69
107
 
108
+ function occurrenceTone(occurrence: ScheduleOccurrence): ConsoleAutomationHistoryView['tone'] {
109
+ if (occurrence.status === 'failed' || occurrence.decision === 'operation_blocked') return 'red';
110
+ if (occurrence.decision === 'stopped' || occurrence.decision === 'budget_exhausted') return 'amber';
111
+ if (occurrence.status === 'succeeded') return 'green';
112
+ if (occurrence.status === 'running' || occurrence.status === 'queued' || occurrence.status === 'created') return 'blue';
113
+ return 'gray';
114
+ }
115
+
116
+ function displayOccurrenceReason(reason?: string): string | undefined {
117
+ const value = reason?.trim();
118
+ if (!value) return undefined;
119
+ if (value === 'Browser session keepalive refreshed successfully.') return '登录会话刷新成功,未发现认证问题。';
120
+ if (value === 'Browser watcher baseline recorded; no Controller wake was emitted.') return '已建立观察基线;没有唤醒 ChatGPT。';
121
+ if (value === 'Browser watcher observation is unchanged; no Controller wake was emitted.') return '页面没有变化;保持静默。';
122
+ if (value.startsWith('Browser watcher requires authentication:')) return '目标登录状态已失效,需要重新认证。';
123
+ if (value.startsWith('Work ') && value.includes(' is terminal')) return '绑定的 Work 已完成,自动任务已停止。';
124
+ return value;
125
+ }
126
+
127
+ function occurrenceHistory(occurrences: ScheduleOccurrence[]): ConsoleAutomationHistoryView[] {
128
+ return occurrences.slice(0, 8).map((occurrence) => ({
129
+ id: occurrence.occurrenceId,
130
+ at: occurrence.updatedAt,
131
+ result: occurrenceResult(occurrence) ?? '已记录',
132
+ tone: occurrenceTone(occurrence),
133
+ reason: displayOccurrenceReason(occurrence.reason),
134
+ trigger: occurrence.triggerContext?.source === 'manual' ? '手动' : occurrence.triggerContext?.source === 'timer' ? '定时' : occurrence.triggerContext?.source,
135
+ }));
136
+ }
137
+
138
+ function scheduleMode(schedule: RepositorySchedule): ConsoleAutomationMode {
139
+ if (schedule.action.operation === 'external_controller_wake') return 'continuation';
140
+ if (schedule.action.operation === 'chatgpt_browser_prompt') return 'chatgpt_prompt';
141
+ if (schedule.action.operation === 'browser_probe') return schedule.action.arguments?.keepalive_only === true ? 'browser_keepalive' : 'browser_watch';
142
+ return 'schedule';
143
+ }
144
+
145
+ function modeLabel(mode: ConsoleAutomationMode): string {
146
+ if (mode === 'browser_watch') return '网页变更监听';
147
+ if (mode === 'browser_keepalive') return '登录保活';
148
+ if (mode === 'continuation') return '自动继续 Work';
149
+ if (mode === 'chatgpt_prompt') return 'ChatGPT 自动任务';
150
+ if (mode === 'routine') return '助手例行任务';
151
+ return '自动任务';
152
+ }
153
+
154
+ function scheduleSummary(mode: ConsoleAutomationMode): string {
155
+ if (mode === 'browser_watch') return '静默观察目标页面;只有内容发生变化时才恢复 ChatGPT。';
156
+ if (mode === 'browser_keepalive') return '静默刷新登录态;正常时不打扰,认证失效时再恢复 ChatGPT。';
157
+ if (mode === 'continuation') return '按计划恢复绑定的 Work,让外部 Controller 继续未完成目标。';
158
+ if (mode === 'chatgpt_prompt') return '由 Forge 定时唤醒 ChatGPT 执行保存的任务提示,不占用 ChatGPT Schedule 名额。';
159
+ return '由 Forge Schedule Engine 管理的持久自动任务。';
160
+ }
161
+
162
+ function scheduleDelivery(schedule: RepositorySchedule, mode: ConsoleAutomationMode): string | undefined {
163
+ const controller = typeof schedule.action.arguments?.controller_type === 'string' ? schedule.action.arguments.controller_type : 'chatgpt';
164
+ if (mode === 'browser_watch') return `变化时唤醒 ${controller === 'chatgpt' ? 'ChatGPT' : controller}`;
165
+ if (mode === 'browser_keepalive') return `登录失效时唤醒 ${controller === 'chatgpt' ? 'ChatGPT' : controller}`;
166
+ if (mode === 'continuation') return `恢复 ${controller === 'chatgpt' ? 'ChatGPT' : controller}`;
167
+ if (mode === 'chatgpt_prompt') return '发送到 ChatGPT';
168
+ return undefined;
169
+ }
170
+
171
+ function chatgptExecutionProfile(schedule: RepositorySchedule, mode: ConsoleAutomationMode): { agentModel?: string; reasoningLevel?: string; tabPolicy?: string } {
172
+ if (mode !== 'continuation' && mode !== 'chatgpt_prompt') return {};
173
+ const args = schedule.action.arguments ?? {};
174
+ const controller = typeof args.controller_type === 'string' ? args.controller_type : 'chatgpt';
175
+ if (mode === 'continuation' && controller !== 'chatgpt') return {};
176
+ return {
177
+ agentModel: typeof args.model === 'string' && args.model.trim() ? args.model.trim() : 'gpt-5.6',
178
+ reasoningLevel: typeof args.reasoning === 'string' && args.reasoning.trim() ? args.reasoning.trim() : 'high',
179
+ tabPolicy: typeof args.tab_policy === 'string' && args.tab_policy.trim() ? args.tab_policy.trim() : 'auto',
180
+ };
181
+ }
182
+
183
+ function scheduleTarget(schedule: RepositorySchedule, mode: ConsoleAutomationMode): string | undefined {
184
+ const args = schedule.action.arguments ?? {};
185
+ if (typeof args.probe_url === 'string') {
186
+ try { return new URL(args.probe_url).hostname; } catch { return '网页目标'; }
187
+ }
188
+ if (typeof args.probe_session_id === 'string') return mode === 'browser_keepalive' ? '已绑定登录会话' : '已绑定浏览器会话';
189
+ if (typeof args.browser_session_id === 'string') return '已绑定 ChatGPT 会话';
190
+ return undefined;
191
+ }
192
+
193
+ function observationResult(schedule: RepositorySchedule, fallback?: ScheduleOccurrence): string | undefined {
194
+ switch (schedule.lastObservationStatus) {
195
+ case 'baseline': return '已建立基线';
196
+ case 'unchanged': return '无变化';
197
+ case 'changed': return '检测到变化';
198
+ case 'keepalive': return '登录保持正常';
199
+ case 'auth_required': return '需要重新登录';
200
+ default: return occurrenceResult(fallback);
201
+ }
202
+ }
203
+
204
+ function scheduleNeedsAttention(schedule: RepositorySchedule): boolean {
205
+ if (schedule.lastObservationStatus === 'auth_required') return true;
206
+ if (schedule.consecutiveFailures >= schedule.policy.maxFailures) return true;
207
+ return Boolean(schedule.pausedReason && /(?:fail|error|auth|login|block|attention|maximum)/i.test(schedule.pausedReason));
208
+ }
209
+
210
+ function scheduleStatus(schedule: RepositorySchedule): ConsoleAutomationView['status'] {
211
+ if (schedule.enabled) return scheduleNeedsAttention(schedule) ? 'attention' : 'enabled';
212
+ return scheduleNeedsAttention(schedule) ? 'attention' : 'paused';
213
+ }
214
+
215
+ function workBinding(controllerHome: string, repoId: string, schedule: RepositorySchedule): { id?: string; objective?: string } {
216
+ const value = schedule.action.arguments?.work_id;
217
+ const workId = typeof value === 'string' && value.trim() ? value.trim() : undefined;
218
+ if (!workId) return {};
219
+ const work = getWorkContract({ controllerHome, repoId }, workId);
220
+ return { id: workId, objective: work?.objective };
221
+ }
222
+
223
+ function policySummary(schedule: RepositorySchedule): string {
224
+ return `连续失败 ${schedule.consecutiveFailures}/${schedule.policy.maxFailures} · 冷却 ${schedule.policy.cooldownMinutes} 分钟 · 每日预算 ${schedule.policy.dailyBudgetMinutes} 分钟`;
225
+ }
226
+
70
227
  export function listConsoleAutomations(controllerHome: string, repositories: RepositoryRecord[]): ConsoleAutomationView[] {
71
228
  const items: ConsoleAutomationView[] = [];
72
229
  for (const repository of repositories.filter((entry) => entry.enabled && !entry.removedAt)) {
@@ -76,22 +233,38 @@ export function listConsoleAutomations(controllerHome: string, repositories: Rep
76
233
 
77
234
  for (const schedule of listSchedules(controllerHome, repository.repoId)) {
78
235
  if (boundScheduleIds.has(schedule.scheduleId)) continue;
79
- const last = listOccurrences(controllerHome, repository.repoId, schedule.scheduleId, 1)[0];
80
- const status: ConsoleAutomationView['status'] = schedule.enabled ? 'enabled' : schedule.pausedReason ? 'attention' : 'paused';
236
+ const occurrences = listOccurrences(controllerHome, repository.repoId, schedule.scheduleId, 8);
237
+ const last = occurrences[0];
238
+ const mode = scheduleMode(schedule);
239
+ const work = workBinding(controllerHome, repository.repoId, schedule);
240
+ const executionProfile = chatgptExecutionProfile(schedule, mode);
81
241
  items.push({
82
242
  id: schedule.scheduleId,
83
243
  source: 'schedule',
84
244
  repoId: repository.repoId,
85
245
  repositoryName: repository.displayName,
86
246
  name: schedule.name,
87
- summary: schedule.action.operation,
88
- status,
247
+ summary: scheduleSummary(mode),
248
+ status: scheduleStatus(schedule),
89
249
  schedule: triggerLabel(schedule),
90
250
  timezone: schedule.trigger.timezone,
251
+ delivery: scheduleDelivery(schedule, mode),
252
+ mode,
253
+ modeLabel: modeLabel(mode),
254
+ live: !schedule.policy.shadowMode,
255
+ targetLabel: scheduleTarget(schedule, mode),
256
+ boundWorkId: work.id,
257
+ boundWorkObjective: work.objective,
258
+ observationStatus: schedule.lastObservationStatus,
259
+ observationAt: schedule.lastObservationAt,
260
+ failureCount: schedule.consecutiveFailures,
261
+ policySummary: policySummary(schedule),
262
+ ...executionProfile,
91
263
  lastRunAt: last?.updatedAt ?? schedule.lastTriggeredAt,
92
- lastResult: occurrenceResult(last),
264
+ lastResult: observationResult(schedule, last),
93
265
  nextRunHint: nextScheduleHint(schedule),
94
266
  pausedReason: schedule.pausedReason,
267
+ history: occurrenceHistory(occurrences),
95
268
  actions: schedule.enabled ? ['run', 'pause'] : ['resume'],
96
269
  });
97
270
  }
@@ -99,8 +272,9 @@ export function listConsoleAutomations(controllerHome: string, repositories: Rep
99
272
  for (const routine of routines) {
100
273
  const binding = bindings.get(routine.routineId);
101
274
  const boundSchedule = safeBoundSchedule(controllerHome, repository.repoId, binding?.scheduleId);
102
- const last = boundSchedule ? listOccurrences(controllerHome, repository.repoId, boundSchedule.scheduleId, 1)[0] : undefined;
103
- const scheduleAttention = routine.status === 'enabled' && boundSchedule && !boundSchedule.enabled && Boolean(boundSchedule.pausedReason);
275
+ const occurrences = boundSchedule ? listOccurrences(controllerHome, repository.repoId, boundSchedule.scheduleId, 8) : [];
276
+ const last = occurrences[0];
277
+ const scheduleAttention = routine.status === 'enabled' && boundSchedule && scheduleNeedsAttention(boundSchedule);
104
278
  const status: ConsoleAutomationView['status'] = scheduleAttention ? 'attention' : routine.status === 'enabled' ? 'enabled' : routine.status === 'paused' ? 'paused' : 'disabled';
105
279
  items.push({
106
280
  id: routine.routineId,
@@ -112,11 +286,19 @@ export function listConsoleAutomations(controllerHome: string, repositories: Rep
112
286
  status,
113
287
  schedule: routine.scheduleText,
114
288
  timezone: routine.timezone,
115
- delivery: routine.output === 'assistant_inbox' ? 'Assistant inbox' : routine.output === 'gmail_draft' ? 'Gmail draft' : 'None',
289
+ delivery: routine.output === 'assistant_inbox' ? 'ChatGPT 助手收件箱' : routine.output === 'gmail_draft' ? 'Gmail 草稿' : '不外发',
290
+ mode: 'routine',
291
+ modeLabel: modeLabel('routine'),
292
+ live: boundSchedule ? !boundSchedule.policy.shadowMode : undefined,
293
+ observationStatus: boundSchedule?.lastObservationStatus,
294
+ observationAt: boundSchedule?.lastObservationAt,
295
+ failureCount: boundSchedule?.consecutiveFailures,
296
+ policySummary: boundSchedule ? policySummary(boundSchedule) : undefined,
116
297
  lastRunAt: last?.updatedAt ?? routine.lastRunAt,
117
- lastResult: occurrenceResult(last) ?? (routine.lastRunAt ? 'Triggered' : undefined),
298
+ lastResult: boundSchedule ? observationResult(boundSchedule, last) : occurrenceResult(last) ?? (routine.lastRunAt ? '已触发' : undefined),
118
299
  nextRunHint: boundSchedule ? nextScheduleHint(boundSchedule) : routine.nextRunHint,
119
300
  pausedReason: boundSchedule?.pausedReason,
301
+ history: occurrenceHistory(occurrences),
120
302
  actions: scheduleAttention ? ['resume'] : routine.status === 'enabled' ? ['run', 'pause'] : routine.status === 'paused' ? ['resume'] : [],
121
303
  });
122
304
  }
@@ -100,6 +100,8 @@ export interface ConsoleErrorViewModel {
100
100
 
101
101
  export interface WorkSummaryViewModel {
102
102
  id: string;
103
+ repoId: string;
104
+ updatedAt: string;
103
105
  title: string;
104
106
  objective: string;
105
107
  modeLabel: string;
@@ -414,6 +414,8 @@ export function mapWorkSummary(
414
414
  || (work.status === 'running' ? '任务执行中…' : status.label);
415
415
  return {
416
416
  id: work.workId,
417
+ repoId: work.repoId,
418
+ updatedAt: work.updatedAt,
417
419
  title: work.objective.slice(0, 160) || '未命名任务',
418
420
  objective: work.objective,
419
421
  modeLabel: mode.label,
@@ -1,5 +1,5 @@
1
1
  import{useCallback,useEffect,useState}from'react';import'./design/tokens.css';import'./styles.css';import type{AutomationView,ConsoleData,LocalToolView,ProviderView,RouteId}from'./types';import{api}from'./lib/api';import{routeFromHash}from'./lib/routes';import{AppShell}from'./shell/AppShell';import{OverviewView}from'./features/overview/OverviewView';import{AutomationsView}from'./features/automations/AutomationsView';import{WorkView}from'./features/work/WorkView';import{CapabilitiesView}from'./features/capabilities/CapabilitiesView';import{RepositoriesView}from'./features/repositories/RepositoriesView';import{SettingsView}from'./features/settings/SettingsView';import{SystemView}from'./features/system/SystemView';
2
- async function loadData():Promise<ConsoleData>{const[commandCenter,work,automations,automationSettings,connector]=await Promise.all([api.commandCenter(),api.work(),api.automations(),api.automationSettings(),api.connector().catch(()=>undefined)]);return{commandCenter,work,automations,automationSettings,connector,generatedAt:new Date().toISOString()}}
2
+ async function loadData():Promise<ConsoleData>{const[commandCenter,work,workPortfolio,automations,automationSettings,connector]=await Promise.all([api.commandCenter(),api.work(),api.workPortfolio(),api.automations(),api.automationSettings(),api.connector().catch(()=>undefined)]);return{commandCenter,work,workPortfolio,automations,automationSettings,connector,generatedAt:new Date().toISOString()}}
3
3
  export default function App(){const[route,setRoute]=useState<RouteId>(routeFromHash());const[data,setData]=useState<ConsoleData>();const[busy,setBusy]=useState(false);const[error,setError]=useState('');
4
4
  const refresh=useCallback(async()=>{setBusy(true);setError('');try{setData(await loadData())}catch(e){setError(e instanceof Error?e.message:String(e))}finally{setBusy(false)}},[]);
5
5
  useEffect(()=>{void refresh();const onHash=()=>setRoute(routeFromHash());addEventListener('hashchange',onHash);return()=>removeEventListener('hashchange',onHash)},[refresh]);
@@ -1,3 +1,143 @@
1
- import{useMemo,useState}from'react';import type{AutomationView,ConsoleData}from'../../types';import{CommandBar}from'../../shell/CommandBar';import{Segmented}from'../../components/Segmented';import{DetailPane,DefinitionList}from'../../components/DetailPane';import{StatusText}from'../../components/StatusText';import{Button}from'../../components/Button';import{formatDate,compact}from'../../lib/format';
2
- type Filter='enabled'|'paused'|'attention'|'all';
3
- export function AutomationsView({data,busy,onRefresh,onAction}:{data:ConsoleData;busy:boolean;onRefresh:()=>void;onAction:(a:AutomationView,action:'run'|'pause'|'resume')=>Promise<void>}){const all=data.automations.automations;const[filter,setFilter]=useState<Filter>('enabled');const list=useMemo(()=>all.filter(a=>filter==='all'||a.status===filter),[all,filter]);const[selectedKey,setSelectedKey]=useState<string>();const key=(a:AutomationView)=>`${a.source}:${a.repoId}:${a.id}`;const selected=list.find(a=>key(a)===selectedKey)??list[0];return <><CommandBar eyebrow="LONG-RUNNING CONFIG" title="Automations" description="管理 Forge 持久化 Schedule 与 Assistant Routine;结果正文继续发送到 ChatGPT / Email。" refreshedAt={data.automations.generatedAt} busy={busy} onRefresh={onRefresh}/><div className="toolbar"><Segmented value={filter} onChange={setFilter} items={[{id:'enabled',label:'Enabled',count:all.filter(x=>x.status==='enabled').length},{id:'paused',label:'Paused',count:all.filter(x=>x.status==='paused').length},{id:'attention',label:'Attention',count:all.filter(x=>x.status==='attention').length},{id:'all',label:'All',count:all.length}]}/></div><div className="split-workspace automation-layout"><div className="table-wrap"><table className="data-table"><thead><tr><th>Automation</th><th>Schedule</th><th>Delivery</th><th>Status</th><th>Last</th><th>Next</th></tr></thead><tbody>{list.map(a=><tr key={key(a)} className={selected&&key(selected)===key(a)?'selected':''} onClick={()=>setSelectedKey(key(a))}><td><strong>{a.name}</strong><small>{a.repositoryName} · {a.source}</small></td><td>{a.schedule}</td><td>{a.delivery??'—'}</td><td><StatusText label={a.status} tone={a.status}/></td><td><span>{compact(a.lastResult,30)||'—'}</span><small>{formatDate(a.lastRunAt)}</small></td><td>{formatDate(a.nextRunHint)}</td></tr>)}</tbody></table>{!list.length&&<div className="quiet-empty">这个筛选条件下没有 Automation。</div>}</div><DetailPane title={selected?.name} subtitle={selected?.summary} empty="选择一个 Automation 查看配置">{selected&&<><DefinitionList items={[["Status",<StatusText label={selected.status} tone={selected.status}/>],["Schedule",selected.schedule],["Source",selected.source],["Repository",selected.repositoryName],["Delivery",selected.delivery??'—'],["Last result",selected.lastResult??'—'],["Last run",formatDate(selected.lastRunAt)],["Next",formatDate(selected.nextRunHint)]]}/>{selected.pausedReason&&<div className="detail-callout warning"><strong>Paused reason</strong><p>{selected.pausedReason}</p></div>}<div className="detail-button-row">{selected.actions.map(action=><Button key={action} disabled={busy} className={action==='pause'?'danger-text':''} onClick={()=>void onAction(selected,action)}>{action==='run'?'Run now':action==='pause'?'Pause':'Resume'}</Button>)}</div><p className="detail-note">这里只保存调度配置与最近一次结果摘要,不复制日报、SEO 或研究正文。</p></>}</DetailPane></div></>}
1
+ import { useMemo, useState } from 'react';
2
+ import type { AutomationHistoryView, AutomationView, ConsoleData } from '../../types';
3
+ import { CommandBar } from '../../shell/CommandBar';
4
+ import { Segmented } from '../../components/Segmented';
5
+ import { DetailPane, DefinitionList } from '../../components/DetailPane';
6
+ import { StatusText } from '../../components/StatusText';
7
+ import { Button } from '../../components/Button';
8
+ import { formatDate, compact } from '../../lib/format';
9
+
10
+ type Filter = 'enabled' | 'paused' | 'attention' | 'all';
11
+
12
+ function statusLabel(status: AutomationView['status']): string {
13
+ if (status === 'enabled') return '运行中';
14
+ if (status === 'paused') return '已暂停';
15
+ if (status === 'attention') return '需要处理';
16
+ return '已停用';
17
+ }
18
+
19
+ function statusTone(status: AutomationView['status']): string {
20
+ if (status === 'enabled') return 'success';
21
+ if (status === 'attention') return 'danger';
22
+ if (status === 'paused') return 'warning';
23
+ return 'neutral';
24
+ }
25
+
26
+ function observationLabel(value?: AutomationView['observationStatus']): string | undefined {
27
+ if (value === 'baseline') return '已建立基线';
28
+ if (value === 'unchanged') return '无变化';
29
+ if (value === 'changed') return '检测到变化';
30
+ if (value === 'keepalive') return '登录保持正常';
31
+ if (value === 'auth_required') return '需要重新登录';
32
+ return undefined;
33
+ }
34
+
35
+ function reasoningLabel(value?: string): string {
36
+ if (value === 'high') return '高';
37
+ if (value === 'medium') return '中';
38
+ if (value === 'xhigh') return '超高';
39
+ return value ?? '—';
40
+ }
41
+
42
+ function tabPolicyLabel(value?: string): string {
43
+ if (value === 'auto') return '自动 · 同一任务优先复用';
44
+ if (value === 'reuse') return '始终复用已绑定会话';
45
+ if (value === 'new') return '每次新开标签页';
46
+ return value ?? '—';
47
+ }
48
+
49
+ function executionDetails(automation: AutomationView): Array<[string, string]> {
50
+ if (!automation.agentModel) return [];
51
+ return [
52
+ ['执行模型', `${automation.agentModel} · ${reasoningLabel(automation.reasoningLevel)}推理`],
53
+ ['浏览器会话', tabPolicyLabel(automation.tabPolicy)],
54
+ ];
55
+ }
56
+
57
+ function historyTone(tone: AutomationHistoryView['tone']): string {
58
+ if (tone === 'green') return 'success';
59
+ if (tone === 'amber') return 'warning';
60
+ if (tone === 'red') return 'danger';
61
+ if (tone === 'blue') return 'info';
62
+ return 'neutral';
63
+ }
64
+
65
+ function History({ items }: { items: AutomationHistoryView[] }) {
66
+ if (!items.length) return <div className="automation-history-empty">还没有执行记录。</div>;
67
+ return <div className="automation-history">{items.map((item) => <div className="automation-history-row" key={item.id}>
68
+ <div className={`automation-history-mark ${item.tone}`} />
69
+ <div className="automation-history-copy">
70
+ <div className="automation-history-top"><StatusText label={item.result} tone={historyTone(item.tone)} /><time>{formatDate(item.at)}</time></div>
71
+ {item.reason && <p>{item.reason}</p>}
72
+ {item.trigger && <small>{item.trigger}触发</small>}
73
+ </div>
74
+ </div>)}</div>;
75
+ }
76
+
77
+ export function AutomationsView({ data, busy, onRefresh, onAction }: {
78
+ data: ConsoleData;
79
+ busy: boolean;
80
+ onRefresh: () => void;
81
+ onAction: (automation: AutomationView, action: 'run' | 'pause' | 'resume') => Promise<void>;
82
+ }) {
83
+ const all = data.automations.automations;
84
+ const [filter, setFilter] = useState<Filter>('enabled');
85
+ const list = useMemo(() => all.filter((automation) => filter === 'all'
86
+ || (filter === 'paused' ? automation.status === 'paused' || automation.status === 'disabled' : automation.status === filter)), [all, filter]);
87
+ const [selectedKey, setSelectedKey] = useState<string>();
88
+ const key = (automation: AutomationView) => `${automation.source}:${automation.repoId}:${automation.id}`;
89
+ const selected = list.find((automation) => key(automation) === selectedKey) ?? list[0];
90
+ const observation = selected ? observationLabel(selected.observationStatus) : undefined;
91
+
92
+ return <>
93
+ <CommandBar eyebrow="AUTOMATION" title="Automations" description="查看长期任务如何触发、当前是否正常,以及每一次实际执行发生了什么。" refreshedAt={data.automations.generatedAt} busy={busy} onRefresh={onRefresh} />
94
+ <div className="toolbar automation-toolbar">
95
+ <Segmented value={filter} onChange={setFilter} items={[
96
+ { id: 'enabled', label: '运行中', count: all.filter((item) => item.status === 'enabled').length },
97
+ { id: 'paused', label: '已暂停', count: all.filter((item) => item.status === 'paused' || item.status === 'disabled').length },
98
+ { id: 'attention', label: '需要处理', count: all.filter((item) => item.status === 'attention').length },
99
+ { id: 'all', label: '全部', count: all.length },
100
+ ]} />
101
+ </div>
102
+ <div className="split-workspace automation-layout">
103
+ <div className="table-wrap">
104
+ <table className="data-table automation-table">
105
+ <thead><tr><th>Automation</th><th>触发</th><th>行为</th><th>状态</th><th>最近结果</th></tr></thead>
106
+ <tbody>{list.map((automation) => <tr key={key(automation)} className={selected && key(selected) === key(automation) ? 'selected' : ''} onClick={() => setSelectedKey(key(automation))}>
107
+ <td><strong>{automation.name}</strong><small>{automation.repositoryName} · {automation.modeLabel}</small></td>
108
+ <td><span>{automation.schedule}</span><small>{automation.timezone ?? '本地时区'}</small></td>
109
+ <td><span>{automation.delivery ?? '本地执行'}</span>{automation.targetLabel && <small>{automation.targetLabel}</small>}</td>
110
+ <td><StatusText label={statusLabel(automation.status)} tone={statusTone(automation.status)} />{automation.live !== undefined && <small>{automation.live ? 'Live' : 'Shadow'}</small>}</td>
111
+ <td><span>{compact(automation.lastResult, 26) || '—'}</span><small>{formatDate(automation.lastRunAt)}</small></td>
112
+ </tr>)}</tbody>
113
+ </table>
114
+ {!list.length && <div className="quiet-empty">这个筛选条件下没有 Automation。</div>}
115
+ </div>
116
+ <DetailPane title={selected?.name} subtitle={selected?.summary} empty="选择一个 Automation 查看配置与执行历史">
117
+ {selected && <>
118
+ <div className="automation-detail-status">
119
+ <div><span className="eyebrow">CURRENT STATE</span><StatusText label={statusLabel(selected.status)} tone={statusTone(selected.status)} /></div>
120
+ <div className="automation-result"><strong>{selected.lastResult ?? '尚未执行'}</strong><small>{formatDate(selected.observationAt ?? selected.lastRunAt)}</small></div>
121
+ </div>
122
+ <DefinitionList items={[
123
+ ['类型', selected.modeLabel],
124
+ ['触发计划', `${selected.schedule}${selected.timezone ? ` · ${selected.timezone}` : ''}`],
125
+ ['触发后的行为', selected.delivery ?? '本地执行'],
126
+ ['观察目标', selected.targetLabel ?? '—'],
127
+ ['观察状态', observation ?? '—'],
128
+ ['绑定 Work', selected.boundWorkObjective ? compact(selected.boundWorkObjective, 96) : selected.boundWorkId ?? '—'],
129
+ ['运行模式', selected.live === undefined ? '—' : selected.live ? 'Live · 会产生实际动作' : 'Shadow · 只记录预演'],
130
+ ...executionDetails(selected),
131
+ ['运行策略', selected.policySummary ?? '—'],
132
+ ]} />
133
+ {selected.pausedReason && <div className="detail-callout warning"><strong>暂停原因</strong><p>{selected.pausedReason}</p></div>}
134
+ <div className="detail-button-row">{selected.actions.map((action) => <Button key={action} disabled={busy} className={action === 'pause' ? 'danger-text' : ''} onClick={() => void onAction(selected, action)}>{action === 'run' ? '立即运行' : action === 'pause' ? '暂停任务' : '开启任务'}</Button>)}</div>
135
+ <div className="automation-section-head"><div><span className="eyebrow">EXECUTION HISTORY</span><h3>最近执行</h3></div><span>{selected.history.length} 条</span></div>
136
+ <History items={selected.history} />
137
+ <details className="advanced automation-advanced"><summary>技术信息</summary><pre>{JSON.stringify({ scheduleId: selected.source === 'schedule' ? selected.id : undefined, workId: selected.boundWorkId, source: selected.source, next: selected.nextRunHint, failureCount: selected.failureCount }, null, 2)}</pre></details>
138
+ <p className="detail-note">这里只展示配置、状态与执行摘要;邮件正文、浏览器 Cookie、登录凭据和 continuation prompt 不会复制到控制台。</p>
139
+ </>}
140
+ </DetailPane>
141
+ </div>
142
+ </>;
143
+ }
@@ -1,4 +1,4 @@
1
1
  import{useMemo,useState}from'react';import type{ConsoleData,PluginView}from'../../types';import{CommandBar}from'../../shell/CommandBar';import{Segmented}from'../../components/Segmented';import{DetailPane,DefinitionList}from'../../components/DetailPane';import{StatusText}from'../../components/StatusText';import{compact,pretty}from'../../lib/format';
2
2
  type Filter='all'|'extensions'|'services'|'execution'|'models';
3
3
  function category(p:PluginView):Exclude<Filter,'all'|'models'>{const s=`${p.name} ${p.provider} ${(p.capabilityLabels??[]).join(' ')}`.toLowerCase();if(/gmail|calendar|github|google task|notion/.test(s))return'services';if(/browser|desktop|ios|repository|codegraph|local/.test(s))return'execution';return'extensions'}
4
- export function CapabilitiesView({data,busy,onRefresh}:{data:ConsoleData;busy:boolean;onRefresh:()=>void}){const plugins=data.commandCenter.plugins??[],providers=data.automationSettings.providers??[];const[filter,setFilter]=useState<Filter>('all');const[selectedId,setSelectedId]=useState<string>();const filtered=useMemo(()=>plugins.filter(p=>filter==='all'||filter==='models'||category(p)===filter),[plugins,filter]);const selected=filtered.find(x=>x.id===selectedId)??filtered[0];return <><CommandBar eyebrow="CAPABILITY CATALOG" title="Capabilities" description="从“Forge 能做什么”查看扩展、服务、执行能力和模型,而不是浏览 MCP tool 清单。" refreshedAt={data.generatedAt} busy={busy} onRefresh={onRefresh}/><div className="toolbar"><Segmented value={filter} onChange={setFilter} items={[{id:'all',label:'All',count:plugins.length+providers.length},{id:'extensions',label:'Extensions',count:plugins.filter(x=>category(x)==='extensions').length},{id:'services',label:'Services',count:plugins.filter(x=>category(x)==='services').length},{id:'execution',label:'Execution',count:plugins.filter(x=>category(x)==='execution').length},{id:'models',label:'Models',count:providers.length}]}/></div>{filter==='models'?<div className="single-list"><div className="scan-list">{providers.map(p=><div className="scan-row static" key={p.providerId}><div className="scan-main"><strong>{p.displayName}</strong><p>{compact(p.explanation??p.summary,110)}</p></div><StatusText label={p.statusLabel??p.status??'Unknown'} tone={p.status}/></div>)}</div></div>:<div className="split-workspace"><div className="scan-list">{filtered.map(p=><button key={p.id} className={`scan-row ${selected?.id===p.id?'selected':''}`} onClick={()=>setSelectedId(p.id)}><div className="scan-main"><span className="row-eyebrow">{category(p).toUpperCase()}</span><strong>{p.name}</strong><p>{compact(p.description,100)}</p></div><StatusText label={p.statusLabel??p.status??'Unknown'} tone={p.status??p.tone}/></button>)}</div><DetailPane title={selected?.name} subtitle={selected?.description}>{selected&&<><DefinitionList items={[["Status",<StatusText label={selected.statusLabel??selected.status??'Unknown'} tone={selected.status??selected.tone}/>],["Provider",selected.provider??'—'],["Health",selected.healthLabel??'—'],["Lifecycle",selected.lifecycleLabel??'—']]}/>{selected.nextStep&&<div className="detail-callout warning"><strong>Next step</strong><p>{selected.nextStep}</p></div>}{(selected.capabilityLabels??[]).length>0&&<div className="capability-lines">{selected.capabilityLabels!.map(x=><span key={x}>{x}</span>)}</div>}{(selected.warnings??[]).map(w=><div className="detail-callout warning" key={w}>{w}</div>)}<details className="advanced"><summary>Advanced · actions & protocol</summary><pre>{pretty({actions:selected.actions,advanced:selected.advanced})}</pre></details></>}</DetailPane></div>}</>}
4
+ export function CapabilitiesView({data,busy,onRefresh}:{data:ConsoleData;busy:boolean;onRefresh:()=>void}){const plugins=data.commandCenter.plugins??[],providers=data.automationSettings.providers??[];const[filter,setFilter]=useState<Filter>('all');const[selectedId,setSelectedId]=useState<string>();const filtered=useMemo(()=>plugins.filter(p=>filter==='all'||filter==='models'||category(p)===filter),[plugins,filter]);const selected=filtered.find(x=>x.id===selectedId)??filtered[0];return <><CommandBar eyebrow="CAPABILITY CATALOG" title="Capabilities" description="从“Forge 能做什么”查看扩展、服务、执行能力和模型,而不是浏览 MCP tool 清单。" refreshedAt={data.generatedAt} busy={busy} onRefresh={onRefresh}/><div className="toolbar"><Segmented value={filter} onChange={setFilter} items={[{id:'all',label:'All',count:plugins.length},{id:'extensions',label:'Extensions',count:plugins.filter(x=>category(x)==='extensions').length},{id:'services',label:'Services',count:plugins.filter(x=>category(x)==='services').length},{id:'execution',label:'Execution',count:plugins.filter(x=>category(x)==='execution').length},{id:'models',label:'Models',count:providers.length}]}/></div>{filter==='models'?<div className="single-list"><div className="scan-list">{providers.map(p=><div className="scan-row static" key={p.providerId}><div className="scan-main"><strong>{p.displayName}</strong><p>{compact(p.explanation??p.summary,110)}</p></div><StatusText label={p.statusLabel??p.status??'Unknown'} tone={p.status}/></div>)}</div></div>:<div className="split-workspace"><div className="scan-list">{filtered.map(p=><button key={p.id} className={`scan-row ${selected?.id===p.id?'selected':''}`} onClick={()=>setSelectedId(p.id)}><div className="scan-main"><span className="row-eyebrow">{category(p).toUpperCase()}</span><strong>{p.name}</strong><p>{compact(p.description,100)}</p></div><StatusText label={p.statusLabel??p.status??'Unknown'} tone={p.status??p.tone}/></button>)}</div><DetailPane title={selected?.name} subtitle={selected?.description}>{selected&&<><DefinitionList items={[["Status",<StatusText label={selected.statusLabel??selected.status??'Unknown'} tone={selected.status??selected.tone}/>],["Provider",selected.provider??'—'],["Health",selected.healthLabel??'—'],["Lifecycle",selected.lifecycleLabel??'—']]}/>{selected.nextStep&&<div className="detail-callout warning"><strong>Next step</strong><p>{selected.nextStep}</p></div>}{(selected.capabilityLabels??[]).length>0&&<div className="capability-lines">{selected.capabilityLabels!.map(x=><span key={x}>{x}</span>)}</div>}{(selected.warnings??[]).map(w=><div className="detail-callout warning" key={w}>{w}</div>)}<details className="advanced"><summary>Advanced · actions & protocol</summary><pre>{pretty({actions:selected.actions,advanced:selected.advanced})}</pre></details></>}</DetailPane></div>}</>}