@moretea-labs/forge 1.5.1 → 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 +20 -0
- package/DESIGN.md +7 -2
- package/README.md +4 -4
- package/README.zh-CN.md +2 -2
- 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/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/tutorials/01-install-and-start.md +1 -1
- package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
- 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/plugin.ts +36 -9
- 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-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 +29 -19
- 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/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/transports/http.ts +25 -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/release-materialize.ts +143 -19
- 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
|
@@ -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>}</>}
|
|
@@ -1,3 +1,181 @@
|
|
|
1
|
-
import type{ConsoleData,Dict}from'../../types';
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import type { ConsoleData, Dict, WorkPortfolioItem } from '../../types';
|
|
2
|
+
import { CommandBar } from '../../shell/CommandBar';
|
|
3
|
+
import { StatusText } from '../../components/StatusText';
|
|
4
|
+
import { SectionHeader } from '../../components/SectionHeader';
|
|
5
|
+
import { compact } from '../../lib/format';
|
|
6
|
+
|
|
7
|
+
type AttentionItem = {
|
|
8
|
+
key: string;
|
|
9
|
+
source: string;
|
|
10
|
+
title: string;
|
|
11
|
+
summary: string;
|
|
12
|
+
statusLabel: string;
|
|
13
|
+
tone: string;
|
|
14
|
+
href: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type WorkspaceRow = {
|
|
18
|
+
key: string;
|
|
19
|
+
label: string;
|
|
20
|
+
summary: string;
|
|
21
|
+
href: string;
|
|
22
|
+
statusLabel?: string;
|
|
23
|
+
tone?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function workStatus(item: WorkPortfolioItem): string {
|
|
27
|
+
return item.advanced?.status ?? '';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isAttentionWork(item: WorkPortfolioItem): boolean {
|
|
31
|
+
return ['blocked', 'failed'].includes(workStatus(item));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function attentionKey(item: Dict): string {
|
|
35
|
+
return `${String(item.title ?? '')}:${String(item.reason ?? '')}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function repositoryNeedsAttention(repository: NonNullable<ConsoleData['commandCenter']['repositories']>[number]): boolean {
|
|
39
|
+
const state = `${repository.readinessLabel ?? ''} ${repository.statusLabel ?? ''}`;
|
|
40
|
+
return /error|failed|blocked|unavailable|degraded|warning|attention/i.test(state);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function OverviewView({ data, busy, onRefresh }: { data: ConsoleData; busy: boolean; onRefresh: () => void }) {
|
|
44
|
+
const cc = data.commandCenter;
|
|
45
|
+
const portfolio = data.workPortfolio;
|
|
46
|
+
const automations = data.automations.summary;
|
|
47
|
+
const plugins = cc.pluginSummary ?? {};
|
|
48
|
+
const repositories = cc.repositories ?? [];
|
|
49
|
+
const readiness = cc.readiness ?? {};
|
|
50
|
+
const capabilityTotal = plugins.total ?? (cc.plugins ?? []).length;
|
|
51
|
+
const repositoryAttention = repositories.filter(repositoryNeedsAttention).length;
|
|
52
|
+
const readinessState = String(readiness.state ?? readiness.status ?? 'ready');
|
|
53
|
+
const runtimeNeedsAttention = /error|failed|blocked|unavailable|degraded|warning|attention/i.test(readinessState);
|
|
54
|
+
const runtimeLabel = String(readiness.label ?? readiness.headline ?? (runtimeNeedsAttention ? 'Needs attention' : 'Ready'));
|
|
55
|
+
|
|
56
|
+
const attentionWork = portfolio.items.filter(isAttentionWork).slice(0, 4);
|
|
57
|
+
const uniqueHandoffs = [...(cc.handoffs ?? [])].filter((handoff, index, all) => all.findIndex((candidate) => attentionKey(candidate) === attentionKey(handoff)) === index);
|
|
58
|
+
|
|
59
|
+
const attention: AttentionItem[] = [
|
|
60
|
+
...(runtimeNeedsAttention ? [{
|
|
61
|
+
key: 'runtime',
|
|
62
|
+
source: 'System',
|
|
63
|
+
title: runtimeLabel,
|
|
64
|
+
summary: compact(String(readiness.explanation ?? readiness.summary ?? 'Inspect Runtime status.'), 112),
|
|
65
|
+
statusLabel: 'Inspect',
|
|
66
|
+
tone: readinessState,
|
|
67
|
+
href: '#/system',
|
|
68
|
+
}] : []),
|
|
69
|
+
...attentionWork.map((item) => ({
|
|
70
|
+
key: `work:${item.id}`,
|
|
71
|
+
source: `Work · ${item.repositoryName}`,
|
|
72
|
+
title: item.title,
|
|
73
|
+
summary: compact(item.latestSummary || item.nextAction || item.objective, 112),
|
|
74
|
+
statusLabel: item.statusLabel,
|
|
75
|
+
tone: item.tone ?? 'warning',
|
|
76
|
+
href: '#/work',
|
|
77
|
+
})),
|
|
78
|
+
...uniqueHandoffs.slice(0, 2).map((handoff, index) => ({
|
|
79
|
+
key: `handoff:${index}:${attentionKey(handoff)}`,
|
|
80
|
+
source: 'Decision',
|
|
81
|
+
title: String(handoff.title ?? 'Needs review'),
|
|
82
|
+
summary: compact(String(handoff.reason ?? handoff.summary ?? 'Review in ChatGPT.'), 112),
|
|
83
|
+
statusLabel: String(handoff.statusLabel ?? 'Review'),
|
|
84
|
+
tone: String(handoff.tone ?? 'warning'),
|
|
85
|
+
href: '#/work',
|
|
86
|
+
})),
|
|
87
|
+
...((automations.needsAttention ?? 0) > 0 ? [{
|
|
88
|
+
key: 'automations',
|
|
89
|
+
source: 'Automations',
|
|
90
|
+
title: `${automations.needsAttention} automation${automations.needsAttention === 1 ? '' : 's'} need attention`,
|
|
91
|
+
summary: 'Inspect configured routines and schedules.',
|
|
92
|
+
statusLabel: 'Inspect',
|
|
93
|
+
tone: 'warning',
|
|
94
|
+
href: '#/automations',
|
|
95
|
+
}] : []),
|
|
96
|
+
...((plugins.needsAttention ?? 0) > 0 ? [{
|
|
97
|
+
key: 'capabilities',
|
|
98
|
+
source: 'Capabilities',
|
|
99
|
+
title: `${plugins.needsAttention} ${plugins.needsAttention === 1 ? 'capability' : 'capabilities'} need attention`,
|
|
100
|
+
summary: 'Inspect configured capability readiness.',
|
|
101
|
+
statusLabel: 'Inspect',
|
|
102
|
+
tone: 'warning',
|
|
103
|
+
href: '#/capabilities',
|
|
104
|
+
}] : []),
|
|
105
|
+
...(repositoryAttention > 0 ? [{
|
|
106
|
+
key: 'repositories',
|
|
107
|
+
source: 'Repositories',
|
|
108
|
+
title: `${repositoryAttention} ${repositoryAttention === 1 ? 'repository' : 'repositories'} need attention`,
|
|
109
|
+
summary: 'Inspect repository registration and readiness.',
|
|
110
|
+
statusLabel: 'Inspect',
|
|
111
|
+
tone: 'warning',
|
|
112
|
+
href: '#/repositories',
|
|
113
|
+
}] : []),
|
|
114
|
+
].filter((item, index, all) => all.findIndex((candidate) => candidate.key === item.key) === index).slice(0, 6);
|
|
115
|
+
|
|
116
|
+
const workSummary = portfolio.summary.needsAttention
|
|
117
|
+
? `${portfolio.summary.open} open · ${portfolio.summary.needsAttention} need attention`
|
|
118
|
+
: `${portfolio.summary.open} open · no attention needed`;
|
|
119
|
+
const automationSummary = automations.needsAttention
|
|
120
|
+
? `${automations.enabled} enabled · ${automations.needsAttention} need attention`
|
|
121
|
+
: automations.paused
|
|
122
|
+
? `${automations.enabled} enabled · ${automations.paused} paused`
|
|
123
|
+
: `${automations.enabled} enabled · healthy`;
|
|
124
|
+
const capabilitySummary = (plugins.needsAttention ?? 0) > 0
|
|
125
|
+
? `${plugins.ready ?? 0} / ${capabilityTotal} ready · ${plugins.needsAttention} need attention`
|
|
126
|
+
: `${plugins.ready ?? 0} / ${capabilityTotal} ready`;
|
|
127
|
+
const repositorySummary = repositoryAttention
|
|
128
|
+
? `${repositories.length} registered · ${repositoryAttention} need attention`
|
|
129
|
+
: `${repositories.length} registered`;
|
|
130
|
+
|
|
131
|
+
const workspaces: WorkspaceRow[] = [
|
|
132
|
+
{ key: 'work', label: 'Work', summary: workSummary, href: '#/work', statusLabel: portfolio.summary.needsAttention ? 'Attention' : undefined, tone: portfolio.summary.needsAttention ? 'warning' : undefined },
|
|
133
|
+
{ key: 'automations', label: 'Automations', summary: automationSummary, href: '#/automations', statusLabel: automations.needsAttention ? 'Attention' : undefined, tone: automations.needsAttention ? 'warning' : undefined },
|
|
134
|
+
{ key: 'capabilities', label: 'Capabilities', summary: capabilitySummary, href: '#/capabilities', statusLabel: (plugins.needsAttention ?? 0) > 0 ? 'Attention' : undefined, tone: (plugins.needsAttention ?? 0) > 0 ? 'warning' : undefined },
|
|
135
|
+
{ key: 'repositories', label: 'Repositories', summary: repositorySummary, href: '#/repositories', statusLabel: repositoryAttention ? 'Attention' : undefined, tone: repositoryAttention ? 'warning' : undefined },
|
|
136
|
+
{ key: 'system', label: 'System', summary: runtimeNeedsAttention ? runtimeLabel : 'Runtime ready', href: '#/system', statusLabel: runtimeNeedsAttention ? 'Attention' : 'Ready', tone: runtimeNeedsAttention ? readinessState : 'success' },
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
return <>
|
|
140
|
+
<CommandBar
|
|
141
|
+
eyebrow="FORGE CONTROL PLANE"
|
|
142
|
+
title="Overview"
|
|
143
|
+
description="需要处理的事项,以及 Forge 各工作区当前状态。"
|
|
144
|
+
refreshedAt={data.generatedAt}
|
|
145
|
+
busy={busy}
|
|
146
|
+
onRefresh={onRefresh}
|
|
147
|
+
/>
|
|
148
|
+
|
|
149
|
+
<div className="overview-home">
|
|
150
|
+
<section className="page-section overview-attention-section">
|
|
151
|
+
<SectionHeader title="Needs attention" meta={attention.length ? `${attention.length} items` : 'All clear'} />
|
|
152
|
+
{attention.length ? <div className="overview-attention-list">
|
|
153
|
+
{attention.map((item) => <a className="overview-attention-row" href={item.href} key={item.key}>
|
|
154
|
+
<div className="overview-attention-source">{item.source}</div>
|
|
155
|
+
<div className="overview-attention-copy">
|
|
156
|
+
<strong>{compact(item.title, 82)}</strong>
|
|
157
|
+
<p>{item.summary}</p>
|
|
158
|
+
</div>
|
|
159
|
+
<StatusText label={item.statusLabel} tone={item.tone} />
|
|
160
|
+
<span className="overview-row-arrow" aria-hidden="true">→</span>
|
|
161
|
+
</a>)}
|
|
162
|
+
</div> : <div className="overview-clear-state">
|
|
163
|
+
<StatusText label="No action needed" tone="success" />
|
|
164
|
+
<span>Forge is operating normally.</span>
|
|
165
|
+
</div>}
|
|
166
|
+
</section>
|
|
167
|
+
|
|
168
|
+
<section className="page-section overview-workspace-section">
|
|
169
|
+
<SectionHeader title="Workspace" meta="Current state" />
|
|
170
|
+
<div className="overview-workspace-list">
|
|
171
|
+
{workspaces.map((workspace) => <a className="overview-workspace-row" href={workspace.href} key={workspace.key}>
|
|
172
|
+
<strong>{workspace.label}</strong>
|
|
173
|
+
<span>{workspace.summary}</span>
|
|
174
|
+
{workspace.statusLabel && <StatusText label={workspace.statusLabel} tone={workspace.tone} />}
|
|
175
|
+
<span className="overview-row-arrow" aria-hidden="true">→</span>
|
|
176
|
+
</a>)}
|
|
177
|
+
</div>
|
|
178
|
+
</section>
|
|
179
|
+
</div>
|
|
180
|
+
</>;
|
|
181
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import{useMemo,useState}from'react';import type{ConsoleData}from'../../types';import{CommandBar}from'../../shell/CommandBar';import{SearchField}from'../../components/SearchField';import{DetailPane,DefinitionList}from'../../components/DetailPane';import{StatusText}from'../../components/StatusText';import{Button}from'../../components/Button';import{compact,pretty}from'../../lib/format';
|
|
2
|
-
export function RepositoriesView({data,busy,onRefresh,onRegister,onRemove}:{data:ConsoleData;busy:boolean;onRefresh:()=>void;onRegister:(path:string,name?:string)=>Promise<void>;onRemove:(id:string)=>Promise<void>}){const all=data.commandCenter.repositories??[];const[q,setQ]=useState('');const[selectedId,setSelectedId]=useState<string>();const[path,setPath]=useState('');const[name,setName]=useState('');const list=useMemo(()=>all.filter(r=>`${r.name} ${r.path} ${r.branchLabel}`.toLowerCase().includes(q.toLowerCase())),[all,q]);const selected=list.find(x=>x.id===selectedId)??list[0];return <><CommandBar eyebrow="CONTROLLER REGISTRY" title="Repositories" description="
|
|
2
|
+
export function RepositoriesView({data,busy,onRefresh,onRegister,onRemove}:{data:ConsoleData;busy:boolean;onRefresh:()=>void;onRegister:(path:string,name?:string)=>Promise<void>;onRemove:(id:string)=>Promise<void>}){const all=data.commandCenter.repositories??[];const[q,setQ]=useState('');const[selectedId,setSelectedId]=useState<string>();const[path,setPath]=useState('');const[name,setName]=useState('');const[adding,setAdding]=useState(false);const list=useMemo(()=>all.filter(r=>`${r.name} ${r.path} ${r.branchLabel}`.toLowerCase().includes(q.toLowerCase())),[all,q]);const selected=list.find(x=>x.id===selectedId)??list[0];return <><CommandBar eyebrow="CONTROLLER REGISTRY" title="Repositories" description="查看和管理 Forge 的持久化仓库边界;临时目录不需要出现在这里。" refreshedAt={data.generatedAt} busy={busy} onRefresh={onRefresh} actions={<Button onClick={()=>setAdding(v=>!v)} aria-expanded={adding}>{adding?'Cancel':'Add repository'}</Button>}/><div className="repository-tools"><SearchField value={q} onChange={setQ} placeholder="Search repositories…"/><span className="repository-count">{list.length===all.length?`${all.length} registered`:`${list.length} of ${all.length}`}</span></div>{adding&&<form className="repository-add-panel" onSubmit={e=>{e.preventDefault();if(path.trim())void onRegister(path.trim(),name.trim()||undefined).then(()=>{setPath('');setName('');setAdding(false)})}}><div className="repository-add-fields"><label><span>Local path</span><input autoFocus value={path} onChange={e=>setPath(e.target.value)} placeholder="/absolute/path"/></label><label><span>Display name</span><input value={name} onChange={e=>setName(e.target.value)} placeholder="Optional"/></label><Button type="submit" disabled={busy||!path.trim()}>Register</Button></div><p>只为需要持久化 Work、缓存、并发隔离或发布治理的仓库建立注册项。</p></form>}<div className="split-workspace repository-workspace"><div className="scan-list">{list.map(r=><button key={r.id} className={`scan-row ${selected?.id===r.id?'selected':''}`} onClick={()=>setSelectedId(r.id)}><div className="scan-main"><strong>{r.name}</strong><p>{compact(r.path,100)}</p></div><div className="scan-meta"><StatusText label={r.readinessLabel??r.statusLabel??'Registered'} tone={r.readinessLabel??r.statusLabel}/><span>{r.branchLabel??'—'}</span></div></button>)}</div><DetailPane title={selected?.name} subtitle={selected?.path}>{selected&&<><DefinitionList items={[["Repository id",<code>{selected.id}</code>],["Branch",selected.branchLabel??'—'],["Working tree",selected.dirtyLabel??'—'],["Readiness",<StatusText label={selected.readinessLabel??selected.statusLabel??'Registered'} tone={selected.readinessLabel??selected.statusLabel}/>]]}/><details className="advanced"><summary>Advanced registry metadata</summary><pre>{pretty(selected.advanced)}</pre></details><div className="detail-button-row"><Button className="danger-text" disabled={busy} onClick={()=>void onRemove(selected.id)}>Remove registry entry</Button></div></>}</DetailPane></div></>}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import{useMemo,useState}from'react';import type{ConsoleData,
|
|
2
|
-
type Filter='
|
|
3
|
-
|
|
1
|
+
import{useMemo,useState}from'react';import type{ConsoleData,WorkPortfolioItem}from'../../types';import{CommandBar}from'../../shell/CommandBar';import{Segmented}from'../../components/Segmented';import{DetailPane,DefinitionList}from'../../components/DetailPane';import{StatusText}from'../../components/StatusText';import{formatDate,compact}from'../../lib/format';
|
|
2
|
+
type Filter='open'|'attention'|'completed'|'all';
|
|
3
|
+
function statusOf(w:WorkPortfolioItem){return w.advanced?.status??''}function match(w:WorkPortfolioItem,f:Filter){const s=statusOf(w);if(f==='all')return true;if(f==='attention')return s==='blocked'||s==='failed';if(f==='completed')return s==='completed'||s==='cancelled';return s==='open'||s==='running'||s==='ready'}
|
|
4
|
+
export function WorkView({data,busy,onRefresh}:{data:ConsoleData;busy:boolean;onRefresh:()=>void}){const portfolio=data.workPortfolio;const all=portfolio.items??[];const[filter,setFilter]=useState<Filter>('open');const[repoId,setRepoId]=useState('all');const[selectedId,setSelectedId]=useState<string>();const list=useMemo(()=>all.filter(w=>match(w,filter)&&(repoId==='all'||w.repoId===repoId)),[all,filter,repoId]);const selected=list.find(x=>x.id===selectedId)??list[0];return <><CommandBar eyebrow="EXECUTION WORK" title="Work" description="查看所有已注册仓库的持久 Work;仓库是归属维度,默认聚合展示。" refreshedAt={portfolio.generatedAt} busy={busy} onRefresh={onRefresh}/><div className="toolbar work-toolbar"><Segmented value={filter} onChange={setFilter} items={[{id:'open',label:'Open',count:all.filter(x=>match(x,'open')).length},{id:'attention',label:'Needs attention',count:all.filter(x=>match(x,'attention')).length},{id:'completed',label:'Completed',count:all.filter(x=>match(x,'completed')).length},{id:'all',label:'All',count:all.length}]}/><label className="repository-filter"><span>Repository</span><select value={repoId} onChange={e=>{setRepoId(e.target.value);setSelectedId(undefined)}}><option value="all">All repositories</option>{portfolio.repositories.map(r=><option key={r.repoId} value={r.repoId}>{r.repositoryName}</option>)}</select></label></div><div className="split-workspace"><div className="scan-list">{list.map(w=><button key={w.id} className={`scan-row work-row ${selected?.id===w.id?'selected':''}`} onClick={()=>setSelectedId(w.id)}><div className="scan-main"><span className="row-eyebrow">{w.repositoryName}</span><strong>{w.title}</strong><p>{compact(w.latestSummary||w.objective,108)}</p></div><div className="scan-meta"><StatusText label={w.statusLabel} tone={w.tone}/><time>{formatDate(w.updatedAt)}</time></div></button>)}{!list.length&&<div className="quiet-empty">这个筛选条件下没有 Work。</div>}</div><DetailPane title={selected?.title} subtitle={selected?.objective} empty="选择一个 Work 查看完整上下文">{selected&&<><DefinitionList items={[["Repository",selected.repositoryName],["Status",<StatusText label={selected.statusLabel} tone={selected.tone}/>],["Updated",formatDate(selected.updatedAt)],["Work id",<code>{selected.id}</code>]]}/>{selected.latestSummary&&<div className="detail-callout"><strong>Latest</strong><p>{selected.latestSummary}</p></div>}<p className="detail-note">这里聚合所有仓库的持久 Work。具体执行、检查和继续操作仍由 ChatGPT 主控。</p></>}</DetailPane></div></>}
|
|
@@ -1,14 +1,26 @@
|
|
|
1
|
-
import type { AutomationSettingsView, AutomationsResponse, CommandCenterView, Dict, WorkResponse } from '../types';
|
|
1
|
+
import type { AutomationSettingsView, AutomationsResponse, CommandCenterView, Dict, WorkPortfolioResponse, WorkResponse } from '../types';
|
|
2
2
|
export class ApiError extends Error { constructor(message:string, readonly status:number, readonly payload:unknown){super(message);} }
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
const CONSOLE_REQUEST_TIMEOUT_MS=15_000;
|
|
4
|
+
export async function requestJson<T>(path:string,init:RequestInit={}):Promise<T>{
|
|
5
|
+
const headers=new Headers(init.headers);if(init.body&&!headers.has('content-type'))headers.set('content-type','application/json');
|
|
6
|
+
const controller=new AbortController();let timedOut=false;const callerSignal=init.signal;const abortFromCaller=()=>controller.abort();
|
|
7
|
+
if(callerSignal){if(callerSignal.aborted)abortFromCaller();else callerSignal.addEventListener('abort',abortFromCaller,{once:true});}
|
|
8
|
+
const timer=setTimeout(()=>{timedOut=true;controller.abort();},CONSOLE_REQUEST_TIMEOUT_MS);
|
|
9
|
+
try{
|
|
10
|
+
const response=await fetch(path,{...init,headers,signal:controller.signal,credentials:'same-origin'});let payload:unknown={};try{payload=await response.json();}catch{}
|
|
11
|
+
if(!response.ok){const r=payload&&typeof payload==='object'?payload as Dict:{};const message=typeof r.error==='string'?r.error:typeof r.message==='string'?r.message:`Request failed (${response.status})`;throw new ApiError(message,response.status,payload);}
|
|
12
|
+
return payload as T;
|
|
13
|
+
}catch(error){
|
|
14
|
+
if(timedOut)throw new Error('Forge Runtime 暂时没有响应。请在连接恢复后重试。');
|
|
15
|
+
throw error;
|
|
16
|
+
}finally{
|
|
17
|
+
clearTimeout(timer);callerSignal?.removeEventListener('abort',abortFromCaller);
|
|
18
|
+
}
|
|
8
19
|
}
|
|
9
20
|
export const api={
|
|
10
21
|
commandCenter:()=>requestJson<CommandCenterView>('/api/console/command-center'),
|
|
11
22
|
work:()=>requestJson<WorkResponse>('/api/console/requirements'),
|
|
23
|
+
workPortfolio:()=>requestJson<WorkPortfolioResponse>('/api/console/work-portfolio'),
|
|
12
24
|
automations:()=>requestJson<AutomationsResponse>('/api/console/automations'),
|
|
13
25
|
automationSettings:()=>requestJson<AutomationSettingsView>('/api/console/automation-settings'),
|
|
14
26
|
connector:()=>requestJson<Dict>('/api/console/connector/status'),
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type{ReactNode}from'react';import{RefreshIcon}from'../design/icons';import{formatDate}from'../lib/format';
|
|
2
|
-
export function CommandBar({eyebrow,title,description,refreshedAt,busy,onRefresh,actions}:{eyebrow?:string;title:string;description:string;refreshedAt?:string;busy?:boolean;onRefresh:()=>void;actions?:ReactNode}){return <header className="command-bar"><div className="command-title">{eyebrow&&<div className="eyebrow">{eyebrow}</div>}<h1>{title}</h1><p>{description}</p></div><div className="command-actions"><
|
|
2
|
+
export function CommandBar({eyebrow,title,description,refreshedAt,busy,onRefresh,actions}:{eyebrow?:string;title:string;description:string;refreshedAt?:string;busy?:boolean;onRefresh:()=>void;actions?:ReactNode}){return <header className="command-bar"><div className="command-title">{eyebrow&&<div className="eyebrow">{eyebrow}</div>}<h1>{title}</h1><p>{description}</p></div><div className="command-actions"><div className="command-meta"><span>Last synced</span><strong>{formatDate(refreshedAt)}</strong></div><button className="icon-button" onClick={onRefresh} disabled={busy} title="Refresh"><RefreshIcon/></button>{actions}<a className="button ghost-link" href="https://chatgpt.com" target="_blank" rel="noreferrer">Open ChatGPT ↗</a></div></header>}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type{ComponentType,SVGProps}from'react';import type{RouteId}from'../types';import{routes}from'../lib/routes';import{AutomationIcon,CapabilityIcon,OverviewIcon,RepoIcon,SettingsIcon,SystemIcon,WorkIcon}from'../design/icons';
|
|
2
2
|
const icons:Record<RouteId,ComponentType<SVGProps<SVGSVGElement>>>={overview:OverviewIcon,automations:AutomationIcon,work:WorkIcon,capabilities:CapabilityIcon,repositories:RepoIcon,settings:SettingsIcon,system:SystemIcon};
|
|
3
|
-
|
|
3
|
+
const groupLabels={daily:'Workspace',manage:'Configure',system:'System'} as const;
|
|
4
|
+
export function Sidebar({route}:{route:RouteId}){let last='';return <aside className="sidebar"><div className="brand"><span className="brand-mark">F</span><div><strong>Forge</strong><small>Utility Console</small></div></div><nav>{routes.map(item=>{const Icon=icons[item.id];const first=item.group!==last;last=item.group;return <div key={item.id} className={first?'nav-group-start':'nav-item'}>{first&&<div className="nav-group-label">{groupLabels[item.group]}</div>}<a href={`#/${item.id}`} className={route===item.id?'active':''}><Icon/><span>{item.label}</span></a></div>})}</nav><div className="sidebar-foot"><span className="pulse-dot"/><span><strong>Runtime connected</strong><small>ChatGPT remains the primary workspace</small></span></div></aside>}
|