feat: connect frontend readonly apis
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
|
||||
## Target Architecture
|
||||
|
||||
计划架构为:Go 后端提供 localhost HTTP API,Vue 3 + Vite 前端提供中文工作台界面。当前后端已提供健康检查、`.codex/agents/*.toml` 只读读取、项目配置只读读取、运行线程只读模型和动态工作流事件模型;目标后端后续仅在用户确认后写回 `.codex/agents/*.toml`。
|
||||
计划架构为:Go 后端提供 localhost HTTP API,Vue 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”。
|
||||
|
||||
@@ -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”,避免出现内部英文或无效状态。
|
||||
|
||||
14
progress.md
14
progress.md
@@ -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 |
|
||||
|
||||
@@ -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 | 集成验证与文档 | 测试/构建/浏览器验证通过;文档完整 |
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
59
web/src/api/client.js
Normal 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
318
web/src/api/normalizers.js
Normal 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)
|
||||
}
|
||||
90
web/src/api/normalizers.test.mjs
Normal file
90
web/src/api/normalizers.test.mjs
Normal 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, '待处理')
|
||||
})
|
||||
@@ -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>
|
||||
|
||||
@@ -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 },
|
||||
]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,9 +40,28 @@ 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 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>
|
||||
<strong>{{ project.name }}</strong>
|
||||
<span>{{ project.path }}</span>
|
||||
@@ -18,15 +69,26 @@ import { agentMatrix, projects } from '../data'
|
||||
<StatusBadge :label="project.statusZh" :status="project.status" :source="project.source" :confidence="project.confidence" />
|
||||
<dl>
|
||||
<div>
|
||||
<dt>运行中</dt>
|
||||
<dd>{{ project.activeAgents }}</dd>
|
||||
<dt>信任</dt>
|
||||
<dd>{{ project.trust }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>草稿</dt>
|
||||
<dd>{{ project.drafts }}</dd>
|
||||
<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>
|
||||
<strong>示例:{{ project.name }}</strong>
|
||||
<span>{{ project.path }}</span>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user