feat: connect frontend readonly apis

This commit is contained in:
Yoilun
2026-05-25 20:28:23 +08:00
parent 8463d43e2e
commit 5cbf4f8b3d
17 changed files with 805 additions and 102 deletions

View File

@@ -6,7 +6,7 @@
## Target Architecture
计划架构为Go 后端提供 localhost HTTP APIVue 3 + Vite 前端提供中文工作台界面。当前后端已提供健康检查、`.codex/agents/*.toml` 只读读取、项目配置只读读取、运行线程只读模型和动态工作流事件模型;目标后端后续仅在用户确认后写回 `.codex/agents/*.toml`
计划架构为Go 后端提供 localhost HTTP APIVue 3 + Vite 前端提供中文工作台界面。当前后端已提供健康检查、`.codex/agents/*.toml` 只读读取、项目配置只读读取、运行线程只读模型和动态工作流事件模型;前端已通过集中 API client 接入这些只读端点并显示加载中、连接失败和空数据状态。目标后端后续仅在用户确认后写回 `.codex/agents/*.toml`
## Configuration
@@ -60,6 +60,7 @@ curl http://127.0.0.1:18083/api/workflow/events
```bash
cd web
pnpm install
pnpm test
pnpm build
```
@@ -70,7 +71,7 @@ cd web
pnpm dev
```
前端开发地址为 `http://127.0.0.1:13083/`。Phase 4 前端仅使用本地静态示例数据,页面文案会标明“示例数据”或“等待连接后端 API”Phase 5 才接入真实只读 API。前端可保留 `local_sample``api_missing` 等内部 source kind但用户界面必须显示中文来源和 `高 / 中 / 低` 置信度。
前端开发地址为 `http://127.0.0.1:13083/`。Phase 5 前端按视图调用只读 API项目视图读取 `/api/projects``/api/runtime/threads`,工作流视图读取 `/api/workflow/events`,智能体视图读取 `/api/agents`。静态示例数据仅在接口连接失败时作为 fallback 展示,并必须明确标注“示例/等待连接”;真实接口返回空列表时展示空状态和来源证据,不回退到示例数据。前端可保留 `local_sample``api_missing` 等内部 source kind但用户界面必须显示中文来源和 `高 / 中 / 低` 置信度。
## Security Boundaries
@@ -84,6 +85,8 @@ pnpm dev
- 当前 `/api/projects` 只读解析 `.codex/config.toml` 中的 `[projects."..."]`,展示路径、显示名、信任等级和目录存在性。
- 当前 `/api/workflow/events` 从运行线程、spawn edges、goals 和计划文件证据生成事件流/交接边/阶段状态,不写死固定流程。
- Phase 4 前端不调用真实 API、不保存草稿、不写回 `.codex/agents/*.toml`;所有写回相关控件仅作为只读步骤展示。
- Phase 5 前端只调用 GET 只读端点,不新增保存、草稿写入或后端写入逻辑。
- Phase 5 前端 normalizer 负责把 source kind、confidence、状态和 parseStatus 转成中文显示,并过滤工作流中非阶段表格行。
## Known Risks
@@ -91,3 +94,4 @@ pnpm dev
- 运行状态由多来源推断,必须显示置信度。
- Phase 3 真实 SQLite 读取已覆盖临时测试库;如果真实 Codex schema 新增字段或缺少可选字段,应继续走 schema-aware 查询和来源证据,而不是让 API 500。
- Phase 4 只读工作台是静态壳;如果用户误以为它展示真实状态,应优先检查界面是否仍清楚显示来源、置信度和“等待连接 API”提示。
- Phase 5 agent 接口当前不返回原始 TOML 文本;智能体只读编辑区展示的是接口返回的已解析字段,并在代码区说明“接口未返回原始 TOML”。

View File

@@ -33,3 +33,6 @@
- Workflow store 未配置 runtime reader 时返回空视图和 `runtime_missing`/`low` 证据,不 panic、不让 API 500。
- 动态工作流事件从 threads、spawn edges、goals、`task_plan.md` 证据构建,不假设固定角色顺序。
- Phase 4 前端使用 Vue 3 + Vite 和本地静态示例数据实现中文只读工作台;所有示例状态显示来源和置信度,不调用真实 API不提供保存或写回按钮。
- Phase 5 前端新增集中 API client仅调用 `/api/agents``/api/projects``/api/runtime/threads``/api/workflow/events` 四个 GET 只读端点。
- Phase 5 前端 normalizer 统一把后端 source kind、confidence、状态、parseStatus 转为中文展示;空 runtime/workflow 不回退到示例数据,连接失败时示例数据必须标注“示例/等待连接”。
- 工作流 phases 需要在前端过滤计划文件中误解析出的非阶段表格行,并把数字阶段名展示为“阶段 N”避免出现内部英文或无效状态。

View File

@@ -21,6 +21,7 @@
| 2026-05-25 | 4 | coding agent | 实现 Vue 中文只读工作台外壳 | 完成;提交 `feat: add chinese vue workbench shell` |
| 2026-05-25 | 4 | spec review | 规格审查未通过:状态徽标和部分视图直接展示 `local_sample``low` 等内部英文值 | 已修复为中文来源和置信度展示 |
| 2026-05-25 | 4 | coding agent | 创建 Vue 3 + Vite 中文只读前端工作台,包含五个 tabs、静态示例数据、来源/置信度和空状态 | 完成;未接入真实 API未提供写回入口 |
| 2026-05-25 | 5 | coding agent | TDD 接入前端只读 API client、normalizer 和项目/工作流/智能体真实数据视图 | 完成;提交前已通过测试、构建和本地接口 smoke 验证 |
## Test Results
@@ -106,6 +107,18 @@
| 2026-05-25 | `git diff --check` | PASS | Phase 4 whitespace 检查通过 |
| 2026-05-25 | `pnpm build` | PASS | Phase 4 规格修复后前端构建通过 |
| 2026-05-25 | `git diff --check` | PASS | Phase 4 规格修复 whitespace 检查通过 |
| 2026-05-25 | `pnpm test` | FAIL | TDD 红灯:`normalizers.js` 尚未实现,新增 normalizer 测试无法导入模块 |
| 2026-05-25 | `pnpm test` | PASS | source/confidence 中文映射、invalid agent TOML、空 runtime/workflow 测试通过 |
| 2026-05-25 | `pnpm build` | PASS | Phase 5 首轮 Vue/Vite 构建通过 |
| 2026-05-25 | `curl --max-time 5 -sS http://127.0.0.1:18083/api/agents` | PASS | 后端真实 agents 只读接口可达 |
| 2026-05-25 | `curl --max-time 5 -sS http://127.0.0.1:18083/api/projects` | PASS | 后端真实 projects 只读接口可达 |
| 2026-05-25 | `curl --max-time 5 -sS http://127.0.0.1:13083/` | PASS | Vite 前端页面可达 |
| 2026-05-25 | `curl --max-time 5 -sS http://127.0.0.1:13083/api/workflow/events` | PASS | 前端代理到后端 workflow 只读接口可达 |
| 2026-05-25 | `pnpm test` | FAIL | TDD 红灯:真实 workflow 返回的非阶段表格行未被 normalizer 过滤 |
| 2026-05-25 | `pnpm test` | PASS | workflow phase 过滤测试通过;共 5 个前端单测通过 |
| 2026-05-25 | `pnpm build` | PASS | Phase 5 修复后前端构建通过 |
| 2026-05-25 | `git diff --check` | PASS | Phase 5 whitespace 检查通过 |
| 2026-05-25 | `go test ./...` | PASS | Phase 5 未改 Go 行为;全量 Go 回归通过 |
## Bug Loop
@@ -126,3 +139,4 @@
| 3 | `SourceEvidence.Confidence` 出现设计外值 `partial` | 保留 `Kind: sqlite_partial`,将 `Confidence` 改为 `medium` | `go test ./internal/runtime ./internal/server` PASS |
| 3 | `workflow.Store` 未配置 Runtime 会 panic | nil Runtime 返回空 view 和 `runtime_missing`/`low` 证据 | `go test ./internal/workflow` PASS |
| 4 | UI 直接展示 `local_sample``api_missing``low``medium` 等内部英文值 | `StatusBadge` 增加中文映射,并将示例数据来源/置信度改为中文展示值 | `pnpm build` PASS |
| 5 | workflow phases 会把 `task_plan.md` 里错误记录表的 `Time/Phase` 行显示到 UI | normalizer 过滤非阶段状态,并把数字阶段名转为“阶段 N” | `pnpm test` PASS |

View File

@@ -22,7 +22,7 @@
| 2 | complete | Agent TOML 只读读取 | 能安全读取 `.codex/agents/*.toml`;坏 TOML 不导致崩溃;提供 `/api/agents` |
| 3 | complete | 项目配置、运行线程和工作流模型 | 能读取 projects、threads、spawn edges、goals状态含来源/置信度;工作流不写死固定流程 |
| 4 | complete | 中文 UI 只读工作台 | 项目/工作流/智能体视图可浏览;空数据可用 |
| 5 | pending | API 集成和只读数据显示 | 前端连接只读 API显示真实 agent 数据和错误状态 |
| 5 | complete | API 集成和只读数据显示 | 前端连接只读 API显示真实 agent 数据和错误状态 |
| 6 | pending | 草稿、TOML 校验、diff、备份、写回 | 草稿不覆盖原文件;无效 TOML 阻止写回;备份成功后才能写回 |
| 7 | pending | 集成验证与文档 | 测试/构建/浏览器验证通过;文档完整 |

View File

@@ -6,6 +6,7 @@
"scripts": {
"dev": "vite --host 127.0.0.1 --port 13083",
"build": "vite build",
"test": "node --test src/**/*.test.mjs",
"preview": "vite preview --host 127.0.0.1 --port 13084"
},
"dependencies": {

View File

@@ -6,7 +6,6 @@ import AgentView from './views/AgentView.vue'
import DraftsView from './views/DraftsView.vue'
import SettingsView from './views/SettingsView.vue'
import StatusBadge from './components/StatusBadge.vue'
import { connection } from './data'
const tabs = [
{ id: 'projects', label: '项目视图', component: ProjectView },
@@ -26,12 +25,12 @@ const activeComponent = computed(() => tabs.find((tab) => tab.id === activeTab.v
<div>
<p class="eyebrow">本地只读工作台</p>
<h1>Codex 智能体管理台</h1>
<p class="lede">中文前端壳已就位真实 API 接入将在 Phase 5 进行</p>
<p class="lede">前端按视图连接后端只读接口显示真实数据加载中连接失败和空数据状态</p>
</div>
<div class="connection-card" aria-label="连接状态">
<StatusBadge label="未连接" status="unknown" confidence="" source="本地示例" />
<strong>{{ connection.label }}</strong>
<span>{{ connection.detail }}</span>
<StatusBadge label="只读接口" status="complete" confidence="" source="计划文件" />
<strong>按需读取后端数据</strong>
<span>项目运行线程工作流和智能体视图只调用只读端点失败时显示示例/等待连接标注</span>
</div>
</header>

59
web/src/api/client.js Normal file
View File

@@ -0,0 +1,59 @@
async function requestJSON(path, options = {}) {
const response = await fetch(path, {
method: 'GET',
headers: { Accept: 'application/json' },
...options,
})
const body = await readBody(response)
if (!response.ok) {
throw new APIError(parseErrorMessage(body, response), response.status, body)
}
return body
}
async function readBody(response) {
const text = await response.text()
if (!text) {
return null
}
try {
return JSON.parse(text)
} catch {
return text
}
}
function parseErrorMessage(body, response) {
if (body && typeof body === 'object' && typeof body.error === 'string') {
return body.error
}
if (typeof body === 'string' && body.trim() !== '') {
return body
}
return `请求失败HTTP ${response.status}`
}
export class APIError extends Error {
constructor(message, status, body) {
super(message)
this.name = 'APIError'
this.status = status
this.body = body
}
}
export const apiClient = {
getAgents() {
return requestJSON('/api/agents')
},
getProjects() {
return requestJSON('/api/projects')
},
getRuntimeThreads() {
return requestJSON('/api/runtime/threads')
},
getWorkflowEvents() {
return requestJSON('/api/workflow/events')
},
}

318
web/src/api/normalizers.js Normal file
View File

@@ -0,0 +1,318 @@
const SOURCE_KIND_LABELS = {
api_missing: '等待接口',
config_toml: '配置文件',
local_sample: '示例数据',
plan_file: '计划文件',
runtime_missing: '运行数据缺失',
sqlite_missing: 'SQLite 缺失',
sqlite_missing_table: 'SQLite 表缺失',
sqlite_partial: 'SQLite 部分可用',
sqlite_readonly: 'SQLite 只读',
sqlite_schema_drift: 'SQLite 结构变化',
sqlite_table: 'SQLite 表',
test: '测试数据',
}
const CONFIDENCE_LABELS = {
high: '高',
medium: '中',
low: '低',
: '高',
: '中',
: '低',
}
const STATUS_LABELS = {
blocked: '受阻',
clean: '无草稿',
complete: '已完成',
done: '已完成',
failed: '失败',
idle: '空闲',
invalid: '无效',
pending: '待处理',
recent: '最近活跃',
running: '运行中',
unknown: '未知',
valid: '有效',
}
const PARSE_STATUS_LABELS = {
invalid: '解析失败',
valid: '解析通过',
}
const TRUST_LABELS = {
trusted: '受信任',
untrusted: '未信任',
unknown: '未知',
}
const PHASE_STATUSES = new Set(['blocked', 'complete', 'done', 'failed', 'pending', 'running', 'unknown'])
export function formatSourceKind(kind) {
if (!kind) {
return '来源未知'
}
return SOURCE_KIND_LABELS[kind] ?? kind
}
export function formatConfidence(confidence) {
if (!confidence) {
return '低'
}
return CONFIDENCE_LABELS[confidence] ?? confidence
}
export function formatStatus(status) {
if (!status) {
return '未知'
}
return STATUS_LABELS[status] ?? status
}
export function formatParseStatus(status) {
if (!status) {
return '未知'
}
return PARSE_STATUS_LABELS[status] ?? formatStatus(status)
}
export function normalizeSource(source, fallback = {}) {
const kind = source?.kind ?? fallback.kind ?? 'api_missing'
const confidence = source?.confidence ?? fallback.confidence ?? 'low'
return {
kind,
label: formatSourceKind(kind),
confidence,
confidenceLabel: formatConfidence(confidence),
path: source?.path ?? fallback.path ?? '',
message: source?.message ?? fallback.message ?? '',
}
}
export function normalizeProject(project) {
const source = normalizeSource(project?.source, { kind: 'config_toml', confidence: 'high' })
const path = project?.path ?? ''
return {
id: path || project?.displayName || 'project',
name: project?.displayName || basename(path) || '未命名项目',
path,
status: project?.directoryExists ? 'complete' : 'unknown',
statusZh: project?.directoryExists ? '目录存在' : '目录不可用',
trust: TRUST_LABELS[project?.trustLevel] ?? project?.trustLevel ?? '未知',
directoryExists: Boolean(project?.directoryExists),
source: source.label,
confidence: source.confidenceLabel,
sourceDetail: source,
}
}
export function normalizeProjects(payload = {}) {
const projects = Array.isArray(payload.items) ? payload.items.map(normalizeProject) : []
return {
projects,
isEmpty: projects.length === 0,
emptyTitle: '没有项目配置',
emptyText: '后端没有返回项目条目。请确认 .codex/config.toml 中是否存在项目配置;这里不会用示例数据伪装真实结果。',
}
}
export function normalizeAgent(agent = {}) {
const fileName = agent.fileName || basename(agent.filePath) || agent.id || '未命名智能体'
const parseStatus = agent.parseStatus || 'unknown'
const isInvalid = parseStatus === 'invalid'
const name = agent.name || fileName
const description = isInvalid
? `TOML 解析失败:${agent.parseError || '后端未返回具体错误'}`
: agent.description || '没有描述'
const source = normalizeSource(null, {
kind: isInvalid ? 'api_missing' : 'config_toml',
confidence: isInvalid ? 'low' : 'high',
})
return {
id: agent.id || fileName,
file: agent.filePath || fileName,
fileName,
name,
description,
role: agent.developerInstructions || '没有 developer_instructions 字段',
status: isInvalid ? 'unknown' : 'complete',
statusLabel: isInvalid ? 'TOML 无效' : '已读取',
parseStatus,
parseStatusLabel: formatParseStatus(parseStatus),
parseError: agent.parseError || '',
draftStatus: agent.draftStatus || 'unknown',
draftStatusLabel: formatStatus(agent.draftStatus),
extraFields: agent.extraFields || {},
modifiedAt: formatDateTime(agent.modifiedAt),
source: source.label,
confidence: source.confidenceLabel,
toml: synthesizeAgentPreview(agent, { isInvalid, description }),
}
}
export function normalizeAgents(payload = {}) {
const agents = Array.isArray(payload.items) ? payload.items.map(normalizeAgent) : []
return {
agents,
isEmpty: agents.length === 0,
emptyTitle: '没有智能体定义',
emptyText: '后端没有返回 .codex/agents/*.toml 条目。当前显示为空状态,不会把示例智能体当作真实数据。',
}
}
export function normalizeRuntime(payload = {}) {
const threads = Array.isArray(payload.items) ? payload.items : []
const goals = Array.isArray(payload.goals) ? payload.goals : []
const goalsByThread = groupBy(goals, (goal) => goal.threadId)
const agents = threads.map((thread) => {
const source = normalizeSource(thread.source, payload.source)
const threadGoals = goalsByThread.get(thread.id) ?? []
return {
id: thread.id,
name: thread.role || thread.id || '未命名线程',
role: thread.id,
status: thread.status || 'unknown',
statusZh: formatStatus(thread.status),
goal: threadGoals.map((goal) => goal.goal).filter(Boolean).join('') || '没有目标记录',
process: 'SQLite 只读快照',
source: source.label,
confidence: source.confidenceLabel,
lastActivity: thread.updatedAt || thread.createdAt || '没有时间记录',
}
})
const source = normalizeSource(payload.source)
return {
agents,
threads,
goals,
edges: Array.isArray(payload.edges) ? payload.edges : [],
source,
isEmpty: threads.length === 0,
emptyTitle: '没有运行线程',
emptyText: source.message || '后端返回了空的运行线程列表;这里保持空状态并展示来源证据。',
}
}
export function normalizeWorkflow(payload = {}) {
const source = normalizeSource(payload.source)
const phases = Array.isArray(payload.phases)
? payload.phases.filter(isDisplayPhase).map((phase) => {
const phaseSource = normalizeSource(phase.source, payload.source)
const phaseName = normalizePhaseName(phase.name)
return {
name: phaseName,
status: phase.status || 'unknown',
label: formatStatus(phase.status),
gate: phaseName,
evidence: phaseSource.label,
confidence: phaseSource.confidenceLabel,
source: phaseSource,
}
})
: []
const handoffs = Array.isArray(payload.handoffEdges)
? payload.handoffEdges.map((edge) => {
const edgeSource = normalizeSource(edge.source, payload.source)
return {
from: edge.fromThreadId || '未知线程',
to: edge.toThreadId || '未知线程',
summary: edge.label || '没有交接说明',
time: '后端事件',
source: edgeSource.label,
confidence: edgeSource.confidenceLabel,
}
})
: []
const edges = handoffs.map((handoff) => ({
parent: handoff.from,
child: handoff.to,
status: handoff.summary,
source: handoff.source,
confidence: handoff.confidence,
}))
const events = Array.isArray(payload.items) ? payload.items : []
return {
events,
phases,
handoffs,
edges,
source,
isEmpty: events.length === 0 && phases.length === 0 && handoffs.length === 0,
emptyTitle: '没有工作流事件',
emptyText: source.message || '后端返回了空的工作流事件流;这里不会回退到伪装真实的示例关系。',
}
}
function synthesizeAgentPreview(agent, { isInvalid, description }) {
if (isInvalid) {
return `# TOML 无效,仅只读展示解析错误\n# ${description}`
}
const lines = [
'# 接口未返回原始 TOML以下为只读展示的已解析字段',
`name = ${quote(agent.name || '')}`,
`description = ${quote(agent.description || '')}`,
]
if (agent.developerInstructions) {
lines.push(`developer_instructions = ${quote(agent.developerInstructions)}`)
}
for (const [key, value] of Object.entries(agent.extraFields || {})) {
lines.push(`${key} = ${quote(value)}`)
}
return lines.join('\n')
}
function basename(path) {
if (!path) {
return ''
}
return String(path).split('/').filter(Boolean).at(-1) ?? String(path)
}
function formatDateTime(value) {
if (!value) {
return '没有时间记录'
}
const date = new Date(value)
if (Number.isNaN(date.getTime())) {
return value
}
return new Intl.DateTimeFormat('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
}).format(date)
}
function groupBy(items, keyOf) {
const groups = new Map()
for (const item of items) {
const key = keyOf(item)
if (!groups.has(key)) {
groups.set(key, [])
}
groups.get(key).push(item)
}
return groups
}
function quote(value) {
return JSON.stringify(String(value ?? ''))
}
function isDisplayPhase(phase) {
return PHASE_STATUSES.has(phase?.status || 'unknown')
}
function normalizePhaseName(name) {
if (!name) {
return '未命名阶段'
}
return /^\d+$/.test(String(name)) ? `阶段 ${name}` : String(name)
}

View File

@@ -0,0 +1,90 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import {
formatConfidence,
formatSourceKind,
normalizeAgent,
normalizeRuntime,
normalizeWorkflow,
} from './normalizers.js'
test('maps source kind and confidence to Chinese display text', () => {
assert.equal(formatSourceKind('sqlite_readonly'), 'SQLite 只读')
assert.equal(formatSourceKind('sqlite_missing'), 'SQLite 缺失')
assert.equal(formatSourceKind('runtime_missing'), '运行数据缺失')
assert.equal(formatSourceKind('local_sample'), '示例数据')
assert.equal(formatConfidence('high'), '高')
assert.equal(formatConfidence('medium'), '中')
assert.equal(formatConfidence('low'), '低')
})
test('normalizes invalid agent TOML as readonly error display', () => {
const agent = normalizeAgent({
id: 'broken',
filePath: '/Users/yoilun/.codex/agents/broken.toml',
fileName: 'broken.toml',
name: '',
description: '',
developerInstructions: '',
parseStatus: 'invalid',
parseError: 'duplicate key name',
draftStatus: 'none',
})
assert.equal(agent.name, 'broken.toml')
assert.equal(agent.statusLabel, 'TOML 无效')
assert.equal(agent.parseStatusLabel, '解析失败')
assert.equal(agent.description, 'TOML 解析失败duplicate key name')
assert.match(agent.toml, /只读展示/)
})
test('normalizes empty runtime without falling back to fake real data', () => {
const runtime = normalizeRuntime({
items: [],
edges: [],
goals: [],
source: { kind: 'sqlite_missing', confidence: 'low', message: 'state_5.sqlite missing' },
sources: {
state: { kind: 'sqlite_missing', confidence: 'low' },
goals: { kind: 'sqlite_missing', confidence: 'low' },
},
})
assert.equal(runtime.isEmpty, true)
assert.equal(runtime.emptyTitle, '没有运行线程')
assert.equal(runtime.source.label, 'SQLite 缺失')
assert.equal(runtime.source.confidenceLabel, '低')
assert.deepEqual(runtime.agents, [])
})
test('normalizes empty workflow with source evidence and no sample edges', () => {
const workflow = normalizeWorkflow({
items: [],
handoffEdges: [],
phases: [],
source: { kind: 'runtime_missing', confidence: 'low', message: 'runtime reader missing' },
})
assert.equal(workflow.isEmpty, true)
assert.equal(workflow.emptyTitle, '没有工作流事件')
assert.equal(workflow.source.label, '运行数据缺失')
assert.deepEqual(workflow.handoffs, [])
assert.deepEqual(workflow.edges, [])
})
test('filters non-phase rows from workflow phase display', () => {
const workflow = normalizeWorkflow({
items: [],
handoffEdges: [],
phases: [
{ name: '5', status: 'pending', source: { kind: 'plan_file', confidence: 'medium' } },
{ name: 'Time', status: 'Phase', source: { kind: 'plan_file', confidence: 'medium' } },
],
source: { kind: 'plan_file', confidence: 'medium' },
})
assert.equal(workflow.phases.length, 1)
assert.equal(workflow.phases[0].name, '阶段 5')
assert.equal(workflow.phases[0].label, '待处理')
})

View File

@@ -1,15 +1,6 @@
<script setup>
import { computed } from 'vue'
const sourceLabels = {
local_sample: '本地示例',
api_missing: '等待 API',
pending_api: '等待 API',
'task_plan.md': '计划文件',
low: '低',
medium: '中',
high: '高',
}
import { formatConfidence, formatSourceKind } from '../api/normalizers'
const props = defineProps({
label: { type: String, required: true },
@@ -18,8 +9,8 @@ const props = defineProps({
source: { type: String, default: '本地示例' },
})
const sourceText = computed(() => sourceLabels[props.source] ?? props.source)
const confidenceText = computed(() => sourceLabels[props.confidence] ?? props.confidence)
const sourceText = computed(() => formatSourceKind(props.source))
const confidenceText = computed(() => formatConfidence(props.confidence))
</script>
<template>

View File

@@ -1,6 +1,6 @@
export const connection = {
label: '等待连接后端 API',
detail: 'Phase 4 仅展示只读工作台外壳;以下内容均为本地示例数据。',
label: '等待连接后端接口',
detail: '示例数据仅在接口未连接时显示,并会明确标注。',
source: '本地示例',
confidence: '低',
}
@@ -30,7 +30,7 @@ export const projects = [
activeAgents: 0,
uncertain: true,
drafts: 0,
lastActivity: '等待 API',
lastActivity: '等待接口',
source: '未连接',
confidence: '低',
},
@@ -57,7 +57,7 @@ export const agentMatrix = [
role: '规划与监管',
status: 'recent',
statusZh: '最近活跃',
goal: 'Phase 4 前端工作台',
goal: '阶段 4 前端工作台',
process: '等待进程表',
source: '本地示例',
confidence: '低',
@@ -70,7 +70,7 @@ export const agentMatrix = [
status: 'running',
statusZh: '运行中',
goal: '搭建中文工作台',
process: '等待 API 连接',
process: '等待接口连接',
source: '本地示例',
confidence: '低',
lastActivity: '示例:刚刚',
@@ -83,7 +83,7 @@ export const agentMatrix = [
statusZh: '未知',
goal: '等待阶段 5 数据',
process: '未连接',
source: '等待 API',
source: '等待接口',
confidence: '低',
lastActivity: '等待连接',
},
@@ -91,19 +91,19 @@ export const agentMatrix = [
export const workflow = {
phases: [
{ name: 'Phase 0-3', status: 'complete', label: '已完成', gate: 'Go 后端只读模型', evidence: 'task_plan.md / progress.md', confidence: '中' },
{ name: 'Phase 4', status: 'running', label: '进行中', gate: '中文只读前端工作台', evidence: '本地示例视图', confidence: '低' },
{ name: 'Phase 5', status: 'pending', label: '未开始', gate: '连接只读 API', evidence: '等待后续阶段', confidence: '低' },
{ name: 'Phase 6', status: 'pending', label: '未开始', gate: '草稿校验与写回', evidence: '当前禁用写回', confidence: '低' },
{ name: '阶段 0-3', status: 'complete', label: '已完成', gate: 'Go 后端只读模型', evidence: 'task_plan.md / progress.md', confidence: '中' },
{ name: '阶段 4', status: 'running', label: '进行中', gate: '中文只读前端工作台', evidence: '本地示例视图', confidence: '低' },
{ name: '阶段 5', status: 'pending', label: '未开始', gate: '连接只读接口', evidence: '等待后续阶段', confidence: '低' },
{ name: '阶段 6', status: 'pending', label: '未开始', gate: '草稿校验与写回', evidence: '当前禁用写回', confidence: '低' },
],
handoffs: [
{ from: '主智能体', to: '前端实现智能体', summary: '派发 Phase 4构建只读工作台外壳', time: '示例18:45', source: '本地示例', confidence: '低' },
{ from: '前端实现智能体', to: '审查智能体', summary: '等待构建和界面验证证据', time: '等待连接', source: '等待 API', confidence: '低' },
{ from: '审查智能体', to: '主智能体', summary: '阶段 5 接入真实 API 和错误态', time: '计划中', source: '计划文件', confidence: '中' },
{ from: '主智能体', to: '前端实现智能体', summary: '派发阶段 4构建只读工作台外壳', time: '示例18:45', source: '本地示例', confidence: '低' },
{ from: '前端实现智能体', to: '审查智能体', summary: '等待构建和界面验证证据', time: '等待连接', source: '等待接口', confidence: '低' },
{ from: '审查智能体', to: '主智能体', summary: '阶段 5 接入真实接口和错误态', time: '计划中', source: '计划文件', confidence: '中' },
],
edges: [
{ parent: '主线程', child: '前端实现', status: '示例运行', source: '本地示例', confidence: '低' },
{ parent: '前端实现', child: '界面审查', status: '等待', source: '等待 API', confidence: '低' },
{ parent: '前端实现', child: '界面审查', status: '等待', source: '等待接口', confidence: '低' },
{ parent: '界面审查', child: '修复回路', status: '未开始', source: '计划文件', confidence: '中' },
],
}
@@ -127,9 +127,9 @@ export const agents = [
description: '优先发现行为回归、安全边界和遗漏测试。',
role: '审查 / 风险 / 验证',
status: '等待读取',
source: '等待 API',
source: '等待接口',
confidence: '低',
toml: '# 等待 Phase 5 从 /api/agents 读取真实 TOML',
toml: '# 等待只读接口从 /api/agents 读取真实 TOML',
},
]
@@ -138,7 +138,7 @@ export const drafts = [
file: '~/.codex/agents/frontend-developer.toml',
changedFields: ['描述', '角色设定'],
validation: '示例:未校验',
backup: '等待 Phase 6',
backup: '等待阶段 6',
source: '本地示例',
confidence: '低',
steps: ['草稿'],
@@ -148,16 +148,16 @@ export const drafts = [
changedFields: [],
validation: '无草稿',
backup: '无',
source: '等待 API',
source: '等待接口',
confidence: '低',
steps: [],
},
]
export const settings = [
{ name: 'Codex home', value: '/Users/yoilun/.codex', detail: '阶段 5 后由后端配置返回', enabled: true },
{ name: 'SQLite 状态读取', value: '只读 mode=ro&immutable=1', detail: '当前界面未连接 API', enabled: true },
{ name: 'Codex home', value: '/Users/yoilun/.codex', detail: '由后端配置提供', enabled: true },
{ name: 'SQLite 状态读取', value: '只读 mode=ro&immutable=1', detail: '通过只读接口展示', enabled: true },
{ name: '本机进程辅助判断', value: '等待后端聚合', detail: '只显示来源与置信度,不伪装确定状态', enabled: true },
{ name: '写回能力', value: 'Phase 6 才启用', detail: '当前没有保存、写入或批量写回按钮', enabled: false },
{ name: '写回能力', value: '阶段 6 才启用', detail: '当前没有保存、写入或批量写回按钮', enabled: false },
{ name: '敏感文件黑名单', value: 'auth.json', detail: '前端只展示规则摘要,不读取内容', enabled: true },
]

View File

@@ -393,6 +393,33 @@ button {
padding: 14px;
}
.load-state {
display: grid;
min-height: 76px;
place-items: center;
color: var(--muted);
background: var(--panel-muted);
border: 1px dashed var(--line);
border-radius: 8px;
font-weight: 700;
}
.error-state {
border-color: #d8a08a;
background: #f8e7dd;
}
.sample-fallback {
margin-top: 14px;
}
.project-item[role="button"]:focus-visible,
.agent-list-item:focus-visible,
.tab-button:focus-visible {
outline: 3px solid color-mix(in srgb, var(--terracotta) 48%, transparent);
outline-offset: 2px;
}
.workflow-layout {
display: grid;
grid-template-columns: minmax(0, 1.2fr) minmax(320px, 0.8fr);

View File

@@ -1,10 +1,34 @@
<script setup>
import { computed, ref } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
import StatusBadge from '../components/StatusBadge.vue'
import { agents } from '../data'
import { apiClient } from '../api/client'
import { normalizeAgents } from '../api/normalizers'
import { agents as sampleAgents } from '../data'
const selectedId = ref(agents[0]?.id)
const selectedAgent = computed(() => agents.find((agent) => agent.id === selectedId.value) ?? agents[0])
const loading = ref(true)
const error = ref('')
const agentState = ref(normalizeAgents())
const selectedId = ref('')
const displayAgents = computed(() => (error.value ? sampleAgents.map((agent) => ({ ...agent, name: `示例:${agent.name}`, statusLabel: agent.status })) : agentState.value.agents))
const selectedAgent = computed(() => displayAgents.value.find((agent) => agent.id === selectedId.value) ?? displayAgents.value[0])
watch(displayAgents, (items) => {
selectedId.value = items[0]?.id ?? ''
})
onMounted(loadAgents)
async function loadAgents() {
loading.value = true
error.value = ''
try {
agentState.value = normalizeAgents(await apiClient.getAgents())
} catch (err) {
error.value = err?.message || '连接后端接口失败'
} finally {
loading.value = false
}
}
</script>
<template>
@@ -12,14 +36,24 @@ const selectedAgent = computed(() => agents.find((agent) => agent.id === selecte
<aside class="panel agent-list" aria-label="智能体列表">
<div class="panel-heading">
<p class="eyebrow">智能体</p>
<h2>只读定义列表</h2>
<h2>真实只读定义</h2>
</div>
<label class="search-box">
<span>搜索</span>
<input value="示例过滤:前端 / 审查" readonly aria-label="智能体搜索占位" />
<input value="只读列表,搜索将在后续阶段实现" readonly aria-label="智能体搜索占位" />
</label>
<div v-if="loading" class="load-state">加载中</div>
<div v-else-if="error" class="empty-state compact error-state">
<strong>连接失败</strong>
<p>{{ error }}</p>
<p>下方仅显示示例/等待连接数据</p>
</div>
<div v-else-if="agentState.isEmpty" class="empty-state compact">
<strong>{{ agentState.emptyTitle }}</strong>
<p>{{ agentState.emptyText }}</p>
</div>
<button
v-for="agent in agents"
v-for="agent in displayAgents"
:key="agent.id"
class="agent-list-item"
:class="{ selected: agent.id === selectedId }"
@@ -36,27 +70,37 @@ const selectedAgent = computed(() => agents.find((agent) => agent.id === selecte
<div class="panel-heading horizontal">
<div>
<p class="eyebrow">只读编辑区</p>
<h2>{{ selectedAgent.name }}</h2>
<h2>{{ selectedAgent?.name || '没有可显示的智能体' }}</h2>
</div>
<StatusBadge :label="selectedAgent.status" status="unknown" :source="selectedAgent.source" :confidence="selectedAgent.confidence" />
<StatusBadge
:label="selectedAgent?.statusLabel || selectedAgent?.status || (error ? '连接失败' : '空数据')"
:status="selectedAgent?.status || 'unknown'"
:source="selectedAgent?.source || (error ? '等待接口' : '来源未知')"
:confidence="selectedAgent?.confidence || '低'"
/>
</div>
<div class="form-grid" aria-label="智能体字段预览">
<div v-if="!selectedAgent && !loading" class="empty-state">
<strong>没有智能体内容</strong>
<p>后端返回空列表当前保持只读空状态不使用示例数据伪装真实 agent</p>
</div>
<div v-else class="form-grid" aria-label="智能体字段预览">
<label>
<span>名称</span>
<input :value="selectedAgent.name" readonly />
<input :value="selectedAgent?.name" readonly />
</label>
<label>
<span>文件路径</span>
<input :value="selectedAgent.file" readonly />
<input :value="selectedAgent?.file" readonly />
</label>
<label class="wide">
<span>描述</span>
<textarea :value="selectedAgent.description" readonly rows="3"></textarea>
<textarea :value="selectedAgent?.description" readonly rows="3"></textarea>
</label>
<label class="wide">
<span>角色设定</span>
<textarea :value="selectedAgent.role" readonly rows="4"></textarea>
<textarea :value="selectedAgent?.role" readonly rows="4"></textarea>
</label>
</div>
@@ -67,13 +111,13 @@ const selectedAgent = computed(() => agents.find((agent) => agent.id === selecte
<span>备份</span>
</div>
<div class="readonly-code">
<div v-if="selectedAgent" class="readonly-code">
<pre>{{ selectedAgent.toml }}</pre>
</div>
<div class="empty-state compact">
<strong>当前阶段没有保存入口</strong>
<p>这里模拟编辑工作区的阅读体验真实草稿校验diff 和写回会在 Phase 6 实现</p>
<p>这里只读展示接口返回的智能体字段真实草稿校验差异和写回会在阶段 6 实现</p>
</div>
</section>
</section>

View File

@@ -37,11 +37,11 @@ import { drafts } from '../data'
<aside class="panel draft-side">
<div class="panel-heading">
<p class="eyebrow">空状态</p>
<h2>等待 Phase 6 写回流程</h2>
<h2>等待阶段 6 写回流程</h2>
</div>
<div class="empty-state">
<strong>没有真实草稿队列</strong>
<p>Phase 4 只展示步骤结构草稿已校验已备份已写回当前不会创建放弃或写回任何文件</p>
<p>当前只展示步骤结构草稿已校验已备份已写回不会创建放弃或写回任何文件</p>
</div>
</aside>
</section>

View File

@@ -1,6 +1,38 @@
<script setup>
import { computed, onMounted, ref } from 'vue'
import StatusBadge from '../components/StatusBadge.vue'
import { agentMatrix, projects } from '../data'
import { apiClient } from '../api/client'
import { normalizeProjects, normalizeRuntime } from '../api/normalizers'
import { agentMatrix as sampleAgentMatrix, projects as sampleProjects } from '../data'
const loading = ref(true)
const error = ref('')
const projectState = ref(normalizeProjects())
const runtimeState = ref(normalizeRuntime())
const selectedProjectId = ref('')
const selectedProject = computed(() => projectState.value.projects.find((project) => project.id === selectedProjectId.value))
const selectedAgent = computed(() => runtimeState.value.agents[0])
onMounted(loadReadonlyData)
async function loadReadonlyData() {
loading.value = true
error.value = ''
try {
const [projectsPayload, runtimePayload] = await Promise.all([
apiClient.getProjects(),
apiClient.getRuntimeThreads(),
])
projectState.value = normalizeProjects(projectsPayload)
runtimeState.value = normalizeRuntime(runtimePayload)
selectedProjectId.value = projectState.value.projects[0]?.id ?? ''
} catch (err) {
error.value = err?.message || '连接后端接口失败'
} finally {
loading.value = false
}
}
</script>
<template>
@@ -8,25 +40,55 @@ import { agentMatrix, projects } from '../data'
<aside class="panel project-list" aria-label="项目列表">
<div class="panel-heading">
<p class="eyebrow">项目</p>
<h2>等待连接的项目清单</h2>
<h2>真实项目清单</h2>
</div>
<article v-for="project in projects" :key="project.id" class="project-item" :class="{ selected: project.id === 'manager' }">
<div>
<strong>{{ project.name }}</strong>
<span>{{ project.path }}</span>
</div>
<StatusBadge :label="project.statusZh" :status="project.status" :source="project.source" :confidence="project.confidence" />
<dl>
<div v-if="loading" class="load-state">加载中</div>
<div v-else-if="error" class="empty-state compact error-state">
<strong>连接失败</strong>
<p>{{ error }}</p>
</div>
<div v-else-if="projectState.isEmpty" class="empty-state compact">
<strong>{{ projectState.emptyTitle }}</strong>
<p>{{ projectState.emptyText }}</p>
</div>
<template v-else>
<article
v-for="project in projectState.projects"
:key="project.id"
class="project-item"
:class="{ selected: project.id === selectedProjectId }"
role="button"
tabindex="0"
@click="selectedProjectId = project.id"
@keyup.enter="selectedProjectId = project.id"
>
<div>
<dt>运行中</dt>
<dd>{{ project.activeAgents }}</dd>
<strong>{{ project.name }}</strong>
<span>{{ project.path }}</span>
</div>
<StatusBadge :label="project.statusZh" :status="project.status" :source="project.source" :confidence="project.confidence" />
<dl>
<div>
<dt>信任</dt>
<dd>{{ project.trust }}</dd>
</div>
<div>
<dt>来源</dt>
<dd>{{ project.source }}</dd>
</div>
</dl>
</article>
</template>
<div v-if="error" class="sample-fallback">
<p class="eyebrow">示例 / 等待连接</p>
<article v-for="project in sampleProjects" :key="project.id" class="project-item">
<div>
<dt>草稿</dt>
<dd>{{ project.drafts }}</dd>
<strong>示例{{ project.name }}</strong>
<span>{{ project.path }}</span>
</div>
</dl>
</article>
<StatusBadge :label="project.statusZh" :status="project.status" source="示例数据" confidence="低" />
</article>
</div>
</aside>
<section class="panel matrix-panel" aria-label="项目状态矩阵">
@@ -35,10 +97,21 @@ import { agentMatrix, projects } from '../data'
<p class="eyebrow">状态矩阵</p>
<h2>智能体运行状态</h2>
</div>
<span class="read-only-chip">只读 · 示例数据</span>
<span class="read-only-chip">只读 · 接口数据</span>
</div>
<div class="matrix-table" role="table" aria-label="智能体状态矩阵">
<div v-if="loading" class="load-state">加载中</div>
<div v-else-if="error" class="empty-state compact error-state">
<strong>连接失败</strong>
<p>无法读取 `/api/runtime/threads`下面仅显示明确标注的示例状态</p>
</div>
<div v-else-if="runtimeState.isEmpty" class="empty-state compact">
<strong>{{ runtimeState.emptyTitle }}</strong>
<p>{{ runtimeState.emptyText }}</p>
<StatusBadge label="空数据" status="unknown" :source="runtimeState.source.label" :confidence="runtimeState.source.confidenceLabel" />
</div>
<div v-if="!loading && !error && !runtimeState.isEmpty" class="matrix-table" role="table" aria-label="智能体状态矩阵">
<div class="matrix-row head" role="row">
<span role="columnheader">智能体</span>
<span role="columnheader">状态</span>
@@ -46,7 +119,7 @@ import { agentMatrix, projects } from '../data'
<span role="columnheader">进程</span>
<span role="columnheader">最近活动</span>
</div>
<div v-for="agent in agentMatrix" :key="agent.id" class="matrix-row" role="row">
<div v-for="agent in runtimeState.agents" :key="agent.id" class="matrix-row" role="row">
<span role="cell">
<strong>{{ agent.name }}</strong>
<small>{{ agent.role }}</small>
@@ -60,30 +133,52 @@ import { agentMatrix, projects } from '../data'
</div>
</div>
<div class="empty-state compact">
<strong>真实运行线程尚未接入</strong>
<p>Phase 5 会从 `/api/runtime/threads` `/api/workflow/events` 填充这里当前不会轮询或写入任何数据</p>
<div v-if="error" class="sample-fallback matrix-table" role="table" aria-label="示例智能体状态矩阵">
<div class="matrix-row head" role="row">
<span role="columnheader">示例智能体</span>
<span role="columnheader">状态</span>
<span role="columnheader">目标</span>
<span role="columnheader">进程</span>
<span role="columnheader">最近活动</span>
</div>
<div v-for="agent in sampleAgentMatrix" :key="agent.id" class="matrix-row" role="row">
<span role="cell">
<strong>示例{{ agent.name }}</strong>
<small>{{ agent.role }}</small>
</span>
<span role="cell">
<StatusBadge :label="agent.statusZh" :status="agent.status" source="示例数据" confidence="低" />
</span>
<span role="cell">{{ agent.goal }}</span>
<span role="cell">{{ agent.process }}</span>
<span role="cell">{{ agent.lastActivity }}</span>
</div>
</div>
</section>
<aside class="panel detail-panel" aria-label="详情面板">
<div class="panel-heading">
<p class="eyebrow">详情</p>
<h2>前端实现智能体</h2>
<h2>{{ selectedAgent?.name || selectedProject?.name || '只读详情' }}</h2>
</div>
<StatusBadge label="运行中" status="running" source="本地示例" confidence="低" />
<StatusBadge
:label="selectedAgent?.statusZh || selectedProject?.statusZh || (error ? '连接失败' : '等待数据')"
:status="selectedAgent?.status || selectedProject?.status || 'unknown'"
:source="selectedAgent?.source || selectedProject?.source || (error ? '等待接口' : '来源未知')"
:confidence="selectedAgent?.confidence || selectedProject?.confidence || '低'"
/>
<div class="detail-block">
<h3>角色摘要</h3>
<p>负责 Vue 3 + Vite 只读工作台中文界面移动响应式布局和空数据状态</p>
<p>{{ selectedAgent?.goal || '选择项目或等待运行线程后显示真实目标。' }}</p>
</div>
<div class="detail-block">
<h3>证据说明</h3>
<p>当前为静态示例不代表真实 Codex 运行状态来源和置信度已明确标出避免伪装真实数据</p>
<p>数据来自只读接口连接失败时只显示明确标注的示例不会把示例伪装真实状态</p>
</div>
<div class="detail-grid">
<span>备份</span><strong>阶段 6 启用</strong>
<span>草稿</span><strong>1 个示例</strong>
<span>API</span><strong>等待连接</strong>
<span>项目数</span><strong>{{ projectState.projects.length }}</strong>
<span>线程数</span><strong>{{ runtimeState.threads.length }}</strong>
<span>接口</span><strong>{{ error ? '连接失败' : '只读连接' }}</strong>
</div>
</aside>
</section>

View File

@@ -11,7 +11,7 @@ import { settings } from '../data'
<p class="eyebrow">设置</p>
<h2>只读配置摘要</h2>
</div>
<StatusBadge label="等待 API" status="unknown" source="本地示例" confidence="" />
<StatusBadge label="接口只读" status="complete" source="计划文件" confidence="" />
</div>
<div class="settings-list">
@@ -34,8 +34,8 @@ import { settings } from '../data'
<ul class="safety-list">
<li>不读取或展示 `.codex/auth.json`</li>
<li>不写入 Codex SQLite</li>
<li>不保存 agent TOML</li>
<li>所有真实数据等待 Phase 5 只读 API</li>
<li>不保存智能体 TOML</li>
<li>真实数据通过本阶段只读接口展示</li>
</ul>
</aside>
</section>

View File

@@ -1,8 +1,29 @@
<script setup>
import { onMounted, ref } from 'vue'
import HandoffTimeline from '../components/HandoffTimeline.vue'
import StatusBadge from '../components/StatusBadge.vue'
import WorkflowGraph from '../components/WorkflowGraph.vue'
import { workflow } from '../data'
import { apiClient } from '../api/client'
import { normalizeWorkflow } from '../api/normalizers'
import { workflow as sampleWorkflow } from '../data'
const loading = ref(true)
const error = ref('')
const workflowState = ref(normalizeWorkflow())
onMounted(loadWorkflow)
async function loadWorkflow() {
loading.value = true
error.value = ''
try {
workflowState.value = normalizeWorkflow(await apiClient.getWorkflowEvents())
} catch (err) {
error.value = err?.message || '连接后端接口失败'
} finally {
loading.value = false
}
}
</script>
<template>
@@ -13,10 +34,21 @@ import { workflow } from '../data'
<p class="eyebrow">阶段</p>
<h2>工作流阶段与门禁</h2>
</div>
<span class="read-only-chip">动态模型占位</span>
<span class="read-only-chip">只读 · 接口数据</span>
</div>
<ol class="phase-list">
<li v-for="phase in workflow.phases" :key="phase.name" :data-status="phase.status">
<div v-if="loading" class="load-state">加载中</div>
<div v-else-if="error" class="empty-state compact error-state">
<strong>连接失败</strong>
<p>{{ error }}</p>
<p>下方仅显示示例/等待连接阶段</p>
</div>
<div v-else-if="workflowState.isEmpty" class="empty-state compact">
<strong>{{ workflowState.emptyTitle }}</strong>
<p>{{ workflowState.emptyText }}</p>
<StatusBadge label="空数据" status="unknown" :source="workflowState.source.label" :confidence="workflowState.source.confidenceLabel" />
</div>
<ol v-if="!loading && !error && !workflowState.isEmpty" class="phase-list">
<li v-for="phase in workflowState.phases" :key="phase.name" :data-status="phase.status">
<div class="phase-dot" aria-hidden="true"></div>
<div>
<strong>{{ phase.name }}</strong>
@@ -26,6 +58,17 @@ import { workflow } from '../data'
<StatusBadge :label="phase.label" :status="phase.status" :source="phase.evidence" :confidence="phase.confidence" />
</li>
</ol>
<ol v-if="error" class="phase-list sample-fallback">
<li v-for="phase in sampleWorkflow.phases" :key="phase.name" :data-status="phase.status">
<div class="phase-dot" aria-hidden="true"></div>
<div>
<strong>示例{{ phase.name }}</strong>
<p>{{ phase.gate }}</p>
<span>证据 示例数据 · 置信度 </span>
</div>
<StatusBadge :label="phase.label" :status="phase.status" source="示例数据" confidence="低" />
</li>
</ol>
</div>
<div class="panel">
@@ -33,7 +76,12 @@ import { workflow } from '../data'
<p class="eyebrow">交接</p>
<h2>智能体交接流</h2>
</div>
<HandoffTimeline :items="workflow.handoffs" />
<div v-if="loading" class="load-state">加载中</div>
<div v-else-if="!error && workflowState.handoffs.length === 0" class="empty-state compact">
<strong>没有交接边</strong>
<p>后端没有返回 handoffEdges当前保持空状态</p>
</div>
<HandoffTimeline v-else :items="error ? sampleWorkflow.handoffs.map((item) => ({ ...item, from: `示例:${item.from}` })) : workflowState.handoffs" />
</div>
<div class="panel graph-panel">
@@ -41,7 +89,12 @@ import { workflow } from '../data'
<p class="eyebrow">关系图</p>
<h2>主线程与子线程</h2>
</div>
<WorkflowGraph :edges="workflow.edges" />
<div v-if="loading" class="load-state">加载中</div>
<div v-else-if="!error && workflowState.edges.length === 0" class="empty-state compact">
<strong>没有关系图数据</strong>
<p>后端没有返回可绘制的交接边</p>
</div>
<WorkflowGraph v-else :edges="error ? sampleWorkflow.edges.map((edge) => ({ ...edge, parent: `示例:${edge.parent}` })) : workflowState.edges" />
</div>
<aside class="panel supervision-panel">
@@ -49,16 +102,21 @@ import { workflow } from '../data'
<p class="eyebrow">监管</p>
<h2> agent 监管状态</h2>
</div>
<StatusBadge label="最近活跃" status="recent" source="本地示例" confidence="低" />
<StatusBadge
:label="error ? '连接失败' : (workflowState.isEmpty ? '空数据' : '已读取')"
:status="error || workflowState.isEmpty ? 'unknown' : 'complete'"
:source="error ? '等待接口' : workflowState.source.label"
:confidence="error ? '低' : workflowState.source.confidenceLabel"
/>
<dl class="detail-grid">
<span>当前门禁</span><strong>Phase 4 构建通过</strong>
<span>计划文件</span><strong>已存在</strong>
<span>审查循环</span><strong>等待验证</strong>
<span>事件</span><strong>{{ workflowState.events.length }}</strong>
<span>阶段</span><strong>{{ workflowState.phases.length }}</strong>
<span>交接边</span><strong>{{ workflowState.handoffs.length }}</strong>
<span>写回能力</span><strong>未启用</strong>
</dl>
<div class="empty-state">
<strong>没有真实工作流事件</strong>
<p>连接 API 这里会展示从 SQLite 和计划文件推断出的事件流交接边和阶段状态</p>
<strong>{{ error ? '只读接口连接失败' : '只读工作流证据' }}</strong>
<p>{{ error ? '示例内容已明确标注;真实事件需要后端接口可达。' : '数据从 SQLite 和计划文件聚合而来,界面仅展示来源和置信度,不写回。' }}</p>
</div>
</aside>
</section>