@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.
- package/CHANGELOG.md +28 -1
- package/DESIGN.md +7 -2
- package/README.md +7 -7
- package/README.zh-CN.md +5 -3
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/assets/hooks/pre-edit-guard.sh +8 -39
- package/assets/hooks/prompt-guard.sh +2 -2
- package/assets/plugin-registry.v1.json +54 -3
- package/assets/reference-configs/hook-operations.md +1 -1
- package/assets/reference-configs/sprint-contracts.md +2 -2
- package/assets/skill-version.json +2 -2
- package/assets/templates/helpers/ensure-task-workflow.sh +2 -2
- package/assets/templates/helpers/sprint-backlog.sh +6 -6
- package/docs/architecture/current/automation-and-schedule-engine.md +9 -3
- package/docs/architecture/current/codegraph-context-provider.md +15 -3
- package/docs/architecture/current/dispatch-and-agent-strategy.md +32 -13
- package/docs/architecture/current/entity-model.md +20 -0
- package/docs/architecture/current/external-plugin-governance.md +3 -3
- package/docs/architecture/current/system-overview.md +26 -1
- package/docs/architecture/current/thin-harness-v1.md +10 -0
- package/docs/forge-chatgpt-mcp-setup.md +27 -3
- package/docs/operations/chatgpt-personal-assistant-runtime.md +12 -2
- package/docs/operations/controller-browser-plugin.md +12 -16
- package/docs/operations/features.md +1 -1
- package/docs/operations/homebrew.md +2 -2
- package/docs/public-usage-guide.md +1 -1
- package/docs/public-usage-guide.zh-CN.md +1 -1
- package/docs/tutorials/01-install-and-start.md +6 -4
- package/docs/tutorials/01-install-and-start.zh-CN.md +3 -1
- package/docs/tutorials/02-connect-chatgpt.md +8 -2
- package/docs/tutorials/02-connect-chatgpt.zh-CN.md +8 -2
- package/docs/wiki/Installation.md +1 -1
- package/docs/wiki/Quick-Start.md +1 -1
- package/package.json +1 -1
- package/scripts/check-public-docs.sh +14 -1
- package/scripts/migrate-chatgpt-automation-schedules.ts +64 -0
- package/scripts/sprint-backlog.sh +1 -1
- package/src/cli/commands/chatgpt.ts +31 -2
- package/src/cli/commands/init-hook.ts +3 -1
- package/src/cli/commands/mcp.ts +2 -0
- package/src/cli/commands/plugin.ts +36 -9
- package/src/cli/commands/runtime.ts +1 -0
- package/src/cli/commands/setup-profile.ts +49 -18
- package/src/cli/controller/context-pack.ts +158 -6
- package/src/cli/hook/prompt-guard-decision.ts +17 -18
- package/src/cli/hook/prompt-intents.ts +1 -1
- package/src/cli/local-bridge/console-assets.ts +17 -0
- package/src/cli/local-bridge/console-automations.ts +205 -23
- package/src/cli/local-bridge/console-view-models.ts +2 -0
- package/src/cli/local-bridge/facade-api.ts +2 -0
- package/src/cli/local-bridge/frontend/src/App.tsx +1 -1
- package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +143 -3
- package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +1 -1
- package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +181 -3
- package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +1 -1
- package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +4 -3
- package/src/cli/local-bridge/frontend/src/lib/api.ts +18 -6
- package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +1 -1
- package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +2 -1
- package/src/cli/local-bridge/frontend/src/styles.css +27 -11
- package/src/cli/local-bridge/frontend/src/types.ts +6 -2
- package/src/cli/local-bridge/server.ts +31 -20
- package/src/cli/local-bridge/ui-dist/app.css +1 -1
- package/src/cli/local-bridge/ui-dist/app.js +5003 -4815
- package/src/cli/mcp/auth.ts +2 -0
- package/src/cli/mcp/paths.ts +8 -2
- package/src/cli/mcp/repository-tools.ts +124 -9
- package/src/cli/mcp/server.ts +164 -19
- package/src/cli/mcp/setup.ts +9 -1
- package/src/cli/mcp/transports/http.ts +52 -2
- package/src/cli/mcp/transports/session-registry.ts +24 -0
- package/src/cli/repositories/command-classifier.ts +113 -0
- package/src/cli/repositories/safe-patch.ts +6 -4
- package/src/cli/repositories/structured-git.ts +49 -7
- package/src/cli/repository/inspector.ts +16 -3
- package/src/runtime/assistant/intent.ts +3 -3
- package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +157 -2
- package/src/runtime/control-plane/execution/execution-identity.ts +65 -14
- package/src/runtime/control-plane/execution/validation.ts +15 -2
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +32 -0
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +6 -7
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +2 -1
- package/src/runtime/control-plane/facade/capability-registry.ts +1 -1
- package/src/runtime/control-plane/facade/controller-session-store.ts +12 -13
- package/src/runtime/control-plane/facade/goal-workloop-access.ts +7 -2
- package/src/runtime/control-plane/facade/goal-workloop.ts +231 -8
- package/src/runtime/control-plane/facade/plan-contract-store.ts +32 -12
- package/src/runtime/control-plane/facade/requirement-board.ts +24 -8
- package/src/runtime/control-plane/facade/types.ts +33 -6
- package/src/runtime/control-plane/facade/work-contract-store.ts +31 -2
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +375 -23
- package/src/runtime/control-plane/persistence/requirement-store.ts +15 -11
- package/src/runtime/diagnostics/mcp-timing.ts +6 -0
- package/src/runtime/execution/process-runtime/check-facade.ts +1 -10
- package/src/runtime/execution/process-runtime/check-result.ts +10 -2
- package/src/runtime/execution/process-runtime/command-facade.ts +1 -0
- package/src/runtime/execution/process-runtime/gc.ts +64 -10
- package/src/runtime/execution/process-runtime/persisted-check.ts +10 -2
- package/src/runtime/execution/process-runtime/resource-claims.ts +87 -2
- package/src/runtime/execution/process-runtime/runtime.ts +16 -1
- package/src/runtime/execution/process-runtime/store.ts +6 -0
- package/src/runtime/gateway/mcp/execution-tools.ts +27 -12
- package/src/runtime/gateway/mcp/router.ts +1 -1
- package/src/runtime/gateway/mcp/runtime-tools.ts +482 -178
- package/src/runtime/model-clients/deepseek-adapter.ts +0 -32
- package/src/runtime/plugins/browser-adapter.ts +19 -72
- package/src/runtime/plugins/browser-handoff-host.ts +6 -31
- package/src/runtime/plugins/browser-handoff.ts +0 -3
- package/src/runtime/plugins/browser-macos-bridge.ts +8 -0
- package/src/runtime/plugins/desktop-operator-registration.ts +8 -11
- package/src/runtime/plugins/external-registration.ts +84 -5
- package/src/runtime/plugins/first-party-registry.ts +2 -2
- package/src/runtime/plugins/gmail-adapter.ts +28 -2
- package/src/runtime/plugins/local-system-adapter.ts +2 -2
- package/src/runtime/plugins/plugin-management-adapter.ts +140 -0
- package/src/runtime/plugins/store.ts +107 -5
- package/src/runtime/plugins/types.ts +4 -0
- package/src/runtime/recovery/actions.ts +0 -10
- package/src/runtime/recovery/classifier.ts +0 -9
- package/src/runtime/recovery/maintenance-executor.ts +43 -11
- package/src/runtime/recovery/probe.ts +0 -1
- package/src/runtime/recovery/types.ts +0 -1
- package/src/runtime/root/package-connector-service.ts +136 -0
- package/src/runtime/root/package-runtime-service.ts +25 -12
- package/src/runtime/root/release-materialize.ts +161 -18
- package/src/runtime/root/service.ts +6 -4
- package/src/runtime/safe-tooling/index.ts +0 -1
- package/src/runtime/safe-tooling/types.ts +0 -30
- package/src/runtime/standalone-recovery/core.ts +2 -2
- package/src/runtime/workflow/schedules/browser-probe.ts +180 -0
- package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +40 -0
- package/src/runtime/workflow/schedules/engine.ts +357 -64
- package/src/runtime/workflow/schedules/types.ts +5 -0
- package/src/runtime/workflow/schedules/work-continuation.ts +100 -27
- package/bin/forge-desktop-helper.mjs +0 -204
- package/docs/operations/controller-desktop-plugin.md +0 -112
- package/src/runtime/plugins/desktop-adapter.ts +0 -374
- 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
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
31
|
-
case 'cron': return `Cron ${trigger.cronExpression ?? '—'}`;
|
|
32
|
-
case 'calendar': return trigger.calendarAt ?
|
|
33
|
-
case 'condition': return
|
|
34
|
-
case 'repository-event': return
|
|
35
|
-
case 'dependency-checkpoint': return '
|
|
36
|
-
case 'manual': return '
|
|
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() : '
|
|
83
|
+
return next > Date.now() ? new Date(next).toISOString() : '下一次调度周期';
|
|
49
84
|
}
|
|
50
85
|
}
|
|
51
|
-
if (schedule.trigger.type === 'cron') return 'Scheduler
|
|
52
|
-
if (schedule.trigger.type === 'condition') return '
|
|
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 '
|
|
64
|
-
if (occurrence.
|
|
65
|
-
if (occurrence.
|
|
66
|
-
if (occurrence.status === '
|
|
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
|
|
80
|
-
const
|
|
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:
|
|
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:
|
|
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
|
|
103
|
-
const
|
|
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' ? '
|
|
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 ? '
|
|
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
|
}
|
|
@@ -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';
|
|
2
|
-
type
|
|
3
|
-
|
|
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
|
|
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>}</>}
|