1702 lines
38 KiB
Markdown
1702 lines
38 KiB
Markdown
# Codex 智能体管理台 Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** 构建一个本机 localhost 中文 Web 工具,用 Go 后端和 Vue 3 + Vite 前端管理 Codex 智能体配置、项目运行状态和动态工作流交接。
|
||
|
||
**Architecture:** 单体本机 Web 工具。Go 后端负责安全读取 `.codex` 文件、只读 SQLite、工作流聚合和确认写回;Vue 3 + Vite 前端负责中文工作台 UI、状态置信度展示、草稿、diff 和写回流程。
|
||
|
||
**Tech Stack:** Go 1.22+、Vue 3、Vite、原生 CSS、SQLite 只读访问、TOML 解析、localhost HTTP API。
|
||
|
||
---
|
||
|
||
## File Structure
|
||
|
||
### Planning and Documentation
|
||
|
||
- Create `task_plan.md`: 阶段计划、状态、验收标准、错误记录。
|
||
- Create `findings.md`: `.codex` 数据源、SQLite schema、风险、关键决策。
|
||
- Create `progress.md`: 每阶段执行记录、测试结果、审查循环。
|
||
- Create `docs/project.md`: 目标、架构、配置、运行方式、安全边界、恢复方式。
|
||
- Modify `docs/superpowers/specs/2026-05-25-codex-agent-manager-design.md`: 当实现决策改变规格时更新。
|
||
|
||
### Go Backend
|
||
|
||
- Create `go.mod`: Go module and dependencies.
|
||
- Create `cmd/codex-agent-manager/main.go`: 后端入口,启动 localhost 服务。
|
||
- Create `internal/app/config.go`: 应用配置、默认 Codex home、端口。
|
||
- Create `internal/codexhome/bounds.go`: 路径边界、安全检查、禁止文件规则。
|
||
- Create `internal/agents/model.go`: AgentDefinition、Draft、Diff 模型。
|
||
- Create `internal/agents/store.go`: 读取 `.codex/agents/*.toml`、草稿、校验、备份、写回。
|
||
- Create `internal/projects/store.go`: 读取 `.codex/config.toml` 中项目配置。
|
||
- Create `internal/runtime/model.go`: RuntimeThread、StatusEvidence、GoalStatus 模型。
|
||
- Create `internal/runtime/store.go`: 只读 SQLite 查询和进程状态聚合接口。
|
||
- Create `internal/workflow/model.go`: WorkflowEvent、WorkflowEdge、WorkflowPhase 模型。
|
||
- Create `internal/workflow/store.go`: 聚合 threads、spawn edges、goals、计划文件。
|
||
- Create `internal/server/server.go`: HTTP router and JSON helpers.
|
||
- Create `internal/server/handlers.go`: API handlers.
|
||
- Create tests under `internal/**`: 覆盖路径安全、TOML、SQLite 查询、工作流推断、写回安全。
|
||
|
||
### Vue Frontend
|
||
|
||
- Create `web/package.json`: Vue/Vite scripts and dependencies.
|
||
- Create `web/vite.config.js`: dev server and proxy.
|
||
- Create `web/index.html`: app shell.
|
||
- Create `web/src/main.js`: Vue app bootstrap.
|
||
- Create `web/src/App.vue`: 中文主框架与标签页。
|
||
- Create `web/src/api/client.js`: API client.
|
||
- Create `web/src/views/ProjectView.vue`: 项目视图。
|
||
- Create `web/src/views/WorkflowView.vue`: 工作流视图。
|
||
- Create `web/src/views/AgentView.vue`: 智能体视图。
|
||
- Create `web/src/views/DraftsView.vue`: 草稿视图。
|
||
- Create `web/src/views/SettingsView.vue`: 设置视图。
|
||
- Create `web/src/components/StatusBadge.vue`: 状态和置信度。
|
||
- Create `web/src/components/HandoffTimeline.vue`: 动态交接流。
|
||
- Create `web/src/components/WorkflowGraph.vue`: 初版列表/树形交接图。
|
||
- Create `web/src/components/DiffViewer.vue`: 字段级 diff。
|
||
- Create `web/src/components/WritebackSteps.vue`: 草稿 -> 已校验 -> 已备份 -> 已写回。
|
||
- Create `web/src/styles.css`: 温和工作台视觉系统。
|
||
|
||
---
|
||
|
||
### Task 0: 文件化计划和项目基线
|
||
|
||
**Files:**
|
||
- Create: `task_plan.md`
|
||
- Create: `findings.md`
|
||
- Create: `progress.md`
|
||
- Create: `docs/project.md`
|
||
- Modify: `.gitignore`
|
||
|
||
- [ ] **Step 1: Create workflow tracking files**
|
||
|
||
Write `task_plan.md`:
|
||
|
||
```markdown
|
||
# Task Plan
|
||
|
||
## Goal
|
||
|
||
构建 Codex 智能体管理台:Go 后端 + Vue 3 前端,支持中文界面、agent 配置管理、项目运行状态、动态工作流交接和安全写回。
|
||
|
||
## Stop Conditions
|
||
|
||
- [ ] 所有阶段完成
|
||
- [ ] Go 测试通过
|
||
- [ ] 前端构建通过
|
||
- [ ] 浏览器验证关键页面
|
||
- [ ] docs/project.md 记录目标、架构、配置、运行方式、安全边界和恢复方式
|
||
- [ ] 无 blocking bug 或未处理高风险问题
|
||
|
||
## Phases
|
||
|
||
| Phase | Status | Goal | Acceptance Criteria |
|
||
| --- | --- | --- | --- |
|
||
| 0 | pending | 项目初始化与风险边界 | 计划文件存在;安全边界明确;不读取 auth.json;不改 .codex |
|
||
| 1 | pending | Go 只读数据层 | 能读取 agents、projects、threads、spawn edges、goals;SQLite 只读 |
|
||
| 2 | pending | 运行状态与动态工作流模型 | 状态含来源/置信度;工作流不写死固定流程 |
|
||
| 3 | pending | 中文 UI 只读工作台 | 项目/工作流/智能体视图可浏览;空数据可用 |
|
||
| 4 | pending | 草稿、TOML 校验和 diff | 草稿不覆盖原文件;无效 TOML 阻止写回 |
|
||
| 5 | pending | 备份与确认写回 | 备份成功后才能写回;失败可恢复 |
|
||
| 6 | pending | 集成验证与文档 | 测试/构建/浏览器验证通过;文档完整 |
|
||
|
||
## Errors Encountered
|
||
|
||
| Time | Phase | Error | Attempt | Resolution |
|
||
| --- | --- | --- | --- | --- |
|
||
```
|
||
|
||
Write `findings.md`:
|
||
|
||
```markdown
|
||
# Findings
|
||
|
||
## Architecture
|
||
|
||
- 后端使用 Go。
|
||
- 前端使用 Vue 3 + Vite。
|
||
- 工具以 localhost Web 应用运行。
|
||
|
||
## Data Sources
|
||
|
||
- `.codex/agents/*.toml`: 智能体配置。
|
||
- `.codex/config.toml`: 项目信任配置。
|
||
- `.codex/state_5.sqlite`: threads、thread_spawn_edges、thread_dynamic_tools。
|
||
- `.codex/goals_1.sqlite`: thread_goals。
|
||
- 本机进程表:Codex 进程辅助判断。
|
||
- 项目工作流文件:task_plan.md、findings.md、progress.md、docs/project.md。
|
||
|
||
## Constraints
|
||
|
||
- 禁止读取或展示 `.codex/auth.json`。
|
||
- 禁止写入 SQLite。
|
||
- 默认不写 `.codex/config.toml`。
|
||
- `.codex/agents/*.toml` 仅允许经草稿、校验、diff、备份、确认后写回。
|
||
|
||
## Decisions
|
||
|
||
- 工作流显示使用动态事件流/DAG,不写死阶段或 agent 顺序。
|
||
- 所有推断状态必须显示来源和置信度。
|
||
- UI 全中文,技术缩写保留英文。
|
||
```
|
||
|
||
Write `progress.md`:
|
||
|
||
```markdown
|
||
# Progress
|
||
|
||
## Session Log
|
||
|
||
| Time | Phase | Actor | Action | Result |
|
||
| --- | --- | --- | --- | --- |
|
||
|
||
## Test Results
|
||
|
||
| Time | Command | Result | Notes |
|
||
| --- | --- | --- | --- |
|
||
|
||
## Bug Loop
|
||
|
||
| Phase | Bug | Fix Attempt | Retest Result |
|
||
| --- | --- | --- | --- |
|
||
```
|
||
|
||
Write `docs/project.md`:
|
||
|
||
```markdown
|
||
# Project Documentation
|
||
|
||
## Goal
|
||
|
||
构建一个本机 Codex 智能体管理台,用中文管理 agent 配置、项目运行状态、动态工作流和安全写回流程。
|
||
|
||
## Architecture
|
||
|
||
Go 后端提供 localhost HTTP API,Vue 3 + Vite 前端提供中文工作台界面。后端只读访问 Codex SQLite 状态库,安全读取 `.codex` 配置,并仅在用户确认后写回 `.codex/agents/*.toml`。
|
||
|
||
## Configuration
|
||
|
||
- `CODEX_HOME`: 默认 `/Users/yoilun/.codex`
|
||
- 后端监听地址:默认 `127.0.0.1:18083`
|
||
- 前端开发地址:默认 `127.0.0.1:13083`
|
||
|
||
## Runbook
|
||
|
||
实施完成后记录实际命令。
|
||
|
||
## Security Boundaries
|
||
|
||
- 不读取 `.codex/auth.json`。
|
||
- 不写入 Codex SQLite。
|
||
- `.codex/agents/*.toml` 写回必须先备份。
|
||
|
||
## Known Risks
|
||
|
||
- Codex 内部 SQLite schema 可能变化。
|
||
- 运行状态由多来源推断,必须显示置信度。
|
||
```
|
||
|
||
- [ ] **Step 2: Update `.gitignore` for generated artifacts**
|
||
|
||
Ensure `.gitignore` includes:
|
||
|
||
```gitignore
|
||
.DS_Store
|
||
.superpowers/
|
||
dist/
|
||
node_modules/
|
||
*.log
|
||
tmp/
|
||
*.bak.*
|
||
```
|
||
|
||
- [ ] **Step 3: Commit planning baseline**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
git add task_plan.md findings.md progress.md docs/project.md .gitignore
|
||
git commit -m "chore: add implementation tracking files"
|
||
```
|
||
|
||
Expected: commit succeeds.
|
||
|
||
---
|
||
|
||
### Task 1: Go 项目骨架和安全边界
|
||
|
||
**Files:**
|
||
- Create: `go.mod`
|
||
- Create: `cmd/codex-agent-manager/main.go`
|
||
- Create: `internal/app/config.go`
|
||
- Create: `internal/codexhome/bounds.go`
|
||
- Create: `internal/codexhome/bounds_test.go`
|
||
- Modify: `docs/project.md`
|
||
|
||
- [ ] **Step 1: Write path boundary tests**
|
||
|
||
Create `internal/codexhome/bounds_test.go`:
|
||
|
||
```go
|
||
package codexhome
|
||
|
||
import (
|
||
"path/filepath"
|
||
"testing"
|
||
)
|
||
|
||
func TestResolveInsideCodexHomeAllowsAgentsToml(t *testing.T) {
|
||
home := filepath.Join(t.TempDir(), ".codex")
|
||
got, err := ResolveInside(home, "agents/product-manager.toml")
|
||
if err != nil {
|
||
t.Fatalf("ResolveInside returned error: %v", err)
|
||
}
|
||
want := filepath.Join(home, "agents", "product-manager.toml")
|
||
if got != want {
|
||
t.Fatalf("path mismatch: got %q want %q", got, want)
|
||
}
|
||
}
|
||
|
||
func TestResolveInsideCodexHomeRejectsTraversal(t *testing.T) {
|
||
home := filepath.Join(t.TempDir(), ".codex")
|
||
_, err := ResolveInside(home, "../auth.json")
|
||
if err == nil {
|
||
t.Fatal("expected traversal to be rejected")
|
||
}
|
||
}
|
||
|
||
func TestIsForbiddenPathBlocksAuthJSON(t *testing.T) {
|
||
home := filepath.Join(t.TempDir(), ".codex")
|
||
path := filepath.Join(home, "auth.json")
|
||
if !IsForbidden(path, home) {
|
||
t.Fatal("auth.json must be forbidden")
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run failing test**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
go test ./internal/codexhome
|
||
```
|
||
|
||
Expected: FAIL because package files are not implemented yet.
|
||
|
||
- [ ] **Step 3: Implement config and path boundary**
|
||
|
||
Create `go.mod`:
|
||
|
||
```go
|
||
module codex-agent-manager
|
||
|
||
go 1.22
|
||
```
|
||
|
||
Create `internal/app/config.go`:
|
||
|
||
```go
|
||
package app
|
||
|
||
import (
|
||
"os"
|
||
"path/filepath"
|
||
)
|
||
|
||
type Config struct {
|
||
CodexHome string
|
||
HTTPAddr string
|
||
}
|
||
|
||
func DefaultConfig() Config {
|
||
home, err := os.UserHomeDir()
|
||
if err != nil {
|
||
home = "."
|
||
}
|
||
return Config{
|
||
CodexHome: filepath.Join(home, ".codex"),
|
||
HTTPAddr: "127.0.0.1:18083",
|
||
}
|
||
}
|
||
```
|
||
|
||
Create `internal/codexhome/bounds.go`:
|
||
|
||
```go
|
||
package codexhome
|
||
|
||
import (
|
||
"errors"
|
||
"path/filepath"
|
||
"strings"
|
||
)
|
||
|
||
var ErrOutsideCodexHome = errors.New("路径超出 Codex home")
|
||
var ErrForbiddenPath = errors.New("禁止访问敏感路径")
|
||
|
||
func ResolveInside(home string, rel string) (string, error) {
|
||
if filepath.IsAbs(rel) {
|
||
return "", ErrOutsideCodexHome
|
||
}
|
||
cleanHome, err := filepath.Abs(home)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
candidate, err := filepath.Abs(filepath.Join(cleanHome, rel))
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
relative, err := filepath.Rel(cleanHome, candidate)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
if relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) {
|
||
return "", ErrOutsideCodexHome
|
||
}
|
||
if IsForbidden(candidate, cleanHome) {
|
||
return "", ErrForbiddenPath
|
||
}
|
||
return candidate, nil
|
||
}
|
||
|
||
func IsForbidden(path string, home string) bool {
|
||
cleanHome, err := filepath.Abs(home)
|
||
if err != nil {
|
||
return true
|
||
}
|
||
cleanPath, err := filepath.Abs(path)
|
||
if err != nil {
|
||
return true
|
||
}
|
||
rel, err := filepath.Rel(cleanHome, cleanPath)
|
||
if err != nil {
|
||
return true
|
||
}
|
||
rel = filepath.ToSlash(rel)
|
||
forbidden := map[string]bool{
|
||
"auth.json": true,
|
||
}
|
||
return forbidden[rel]
|
||
}
|
||
```
|
||
|
||
Create `cmd/codex-agent-manager/main.go`:
|
||
|
||
```go
|
||
package main
|
||
|
||
import (
|
||
"fmt"
|
||
"net/http"
|
||
|
||
"codex-agent-manager/internal/app"
|
||
)
|
||
|
||
func main() {
|
||
cfg := app.DefaultConfig()
|
||
mux := http.NewServeMux()
|
||
mux.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
_, _ = w.Write([]byte(`{"status":"ok"}`))
|
||
})
|
||
fmt.Printf("Codex 智能体管理台监听 http://%s\n", cfg.HTTPAddr)
|
||
if err := http.ListenAndServe(cfg.HTTPAddr, mux); err != nil {
|
||
panic(err)
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: Run tests**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
go test ./...
|
||
```
|
||
|
||
Expected: PASS.
|
||
|
||
- [ ] **Step 5: Update docs and commit**
|
||
|
||
Update `docs/project.md` runbook with:
|
||
|
||
```markdown
|
||
## Runbook
|
||
|
||
启动后端:
|
||
|
||
```bash
|
||
go run ./cmd/codex-agent-manager
|
||
```
|
||
|
||
健康检查:
|
||
|
||
```bash
|
||
curl http://127.0.0.1:18083/api/health
|
||
```
|
||
```
|
||
|
||
Run:
|
||
|
||
```bash
|
||
git add go.mod cmd internal docs/project.md
|
||
git commit -m "feat: add go backend skeleton"
|
||
```
|
||
|
||
Expected: commit succeeds.
|
||
|
||
---
|
||
|
||
### Task 2: Agent TOML 只读读取
|
||
|
||
**Files:**
|
||
- Create: `internal/agents/model.go`
|
||
- Create: `internal/agents/store.go`
|
||
- Create: `internal/agents/store_test.go`
|
||
- Modify: `internal/server/server.go`
|
||
- Modify: `cmd/codex-agent-manager/main.go`
|
||
|
||
- [ ] **Step 1: Add tests for agent TOML parsing**
|
||
|
||
Create `internal/agents/store_test.go`:
|
||
|
||
```go
|
||
package agents
|
||
|
||
import (
|
||
"os"
|
||
"path/filepath"
|
||
"testing"
|
||
)
|
||
|
||
func TestListAgentsReadsTomlFiles(t *testing.T) {
|
||
root := t.TempDir()
|
||
agentsDir := filepath.Join(root, "agents")
|
||
if err := os.MkdirAll(agentsDir, 0o755); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
content := `name = "产品经理"
|
||
description = "负责产品定义"
|
||
developer_instructions = """
|
||
用中文定义产品需求。
|
||
"""
|
||
`
|
||
if err := os.WriteFile(filepath.Join(agentsDir, "product-manager.toml"), []byte(content), 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
store := Store{CodexHome: root}
|
||
got, err := store.List()
|
||
if err != nil {
|
||
t.Fatalf("List returned error: %v", err)
|
||
}
|
||
if len(got) != 1 {
|
||
t.Fatalf("agent count = %d, want 1", len(got))
|
||
}
|
||
if got[0].Name != "产品经理" || got[0].Description != "负责产品定义" {
|
||
t.Fatalf("unexpected agent: %#v", got[0])
|
||
}
|
||
}
|
||
|
||
func TestListAgentsReportsParseError(t *testing.T) {
|
||
root := t.TempDir()
|
||
agentsDir := filepath.Join(root, "agents")
|
||
if err := os.MkdirAll(agentsDir, 0o755); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := os.WriteFile(filepath.Join(agentsDir, "bad.toml"), []byte(`name = "`), 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
store := Store{CodexHome: root}
|
||
got, err := store.List()
|
||
if err != nil {
|
||
t.Fatalf("List should return parse status, not fatal error: %v", err)
|
||
}
|
||
if len(got) != 1 || got[0].ParseStatus != "invalid" {
|
||
t.Fatalf("expected invalid parse status, got %#v", got)
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run failing test**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
go test ./internal/agents
|
||
```
|
||
|
||
Expected: FAIL because `Store` is undefined.
|
||
|
||
- [ ] **Step 3: Implement agent model and store**
|
||
|
||
Create `internal/agents/model.go`:
|
||
|
||
```go
|
||
package agents
|
||
|
||
import "time"
|
||
|
||
type AgentDefinition struct {
|
||
ID string `json:"id"`
|
||
FilePath string `json:"filePath"`
|
||
FileName string `json:"fileName"`
|
||
Name string `json:"name"`
|
||
Description string `json:"description"`
|
||
DeveloperInstructions string `json:"developerInstructions"`
|
||
ExtraFields map[string]string `json:"extraFields"`
|
||
ModifiedAt time.Time `json:"modifiedAt"`
|
||
ParseStatus string `json:"parseStatus"`
|
||
ParseError string `json:"parseError,omitempty"`
|
||
DraftStatus string `json:"draftStatus"`
|
||
}
|
||
```
|
||
|
||
Create `internal/agents/store.go`:
|
||
|
||
```go
|
||
package agents
|
||
|
||
import (
|
||
"bufio"
|
||
"os"
|
||
"path/filepath"
|
||
"sort"
|
||
"strings"
|
||
)
|
||
|
||
type Store struct {
|
||
CodexHome string
|
||
}
|
||
|
||
func (s Store) List() ([]AgentDefinition, error) {
|
||
pattern := filepath.Join(s.CodexHome, "agents", "*.toml")
|
||
files, err := filepath.Glob(pattern)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
sort.Strings(files)
|
||
result := make([]AgentDefinition, 0, len(files))
|
||
for _, file := range files {
|
||
result = append(result, s.readOne(file))
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
func (s Store) readOne(path string) AgentDefinition {
|
||
info, statErr := os.Stat(path)
|
||
def := AgentDefinition{
|
||
ID: strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)),
|
||
FilePath: path,
|
||
FileName: filepath.Base(path),
|
||
ParseStatus: "valid",
|
||
DraftStatus: "clean",
|
||
ExtraFields: map[string]string{},
|
||
}
|
||
if statErr == nil {
|
||
def.ModifiedAt = info.ModTime()
|
||
}
|
||
data, err := os.ReadFile(path)
|
||
if err != nil {
|
||
def.ParseStatus = "invalid"
|
||
def.ParseError = err.Error()
|
||
return def
|
||
}
|
||
values, err := parseSimpleTOML(string(data))
|
||
if err != nil {
|
||
def.ParseStatus = "invalid"
|
||
def.ParseError = err.Error()
|
||
return def
|
||
}
|
||
def.Name = values["name"]
|
||
def.Description = values["description"]
|
||
def.DeveloperInstructions = values["developer_instructions"]
|
||
for key, value := range values {
|
||
if key != "name" && key != "description" && key != "developer_instructions" {
|
||
def.ExtraFields[key] = value
|
||
}
|
||
}
|
||
return def
|
||
}
|
||
|
||
func parseSimpleTOML(input string) (map[string]string, error) {
|
||
values := map[string]string{}
|
||
scanner := bufio.NewScanner(strings.NewReader(input))
|
||
for scanner.Scan() {
|
||
line := strings.TrimSpace(scanner.Text())
|
||
if line == "" || strings.HasPrefix(line, "#") {
|
||
continue
|
||
}
|
||
parts := strings.SplitN(line, "=", 2)
|
||
if len(parts) != 2 {
|
||
continue
|
||
}
|
||
key := strings.TrimSpace(parts[0])
|
||
raw := strings.TrimSpace(parts[1])
|
||
if strings.HasPrefix(raw, `"""`) {
|
||
block := strings.TrimPrefix(raw, `"""`)
|
||
for !strings.Contains(block, `"""`) && scanner.Scan() {
|
||
block += "\n" + scanner.Text()
|
||
}
|
||
if !strings.Contains(block, `"""`) {
|
||
return values, errInvalidTOML("未闭合的多行字符串")
|
||
}
|
||
values[key] = strings.SplitN(block, `"""`, 2)[0]
|
||
continue
|
||
}
|
||
if !strings.HasPrefix(raw, `"`) || !strings.HasSuffix(raw, `"`) {
|
||
return values, errInvalidTOML("仅支持字符串字段")
|
||
}
|
||
values[key] = strings.TrimSuffix(strings.TrimPrefix(raw, `"`), `"`)
|
||
}
|
||
return values, scanner.Err()
|
||
}
|
||
|
||
type errInvalidTOML string
|
||
|
||
func (e errInvalidTOML) Error() string { return string(e) }
|
||
```
|
||
|
||
- [ ] **Step 4: Run tests**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
go test ./internal/agents
|
||
go test ./...
|
||
```
|
||
|
||
Expected: PASS.
|
||
|
||
- [ ] **Step 5: Add `/api/agents` handler**
|
||
|
||
Create `internal/server/server.go`:
|
||
|
||
```go
|
||
package server
|
||
|
||
import (
|
||
"encoding/json"
|
||
"net/http"
|
||
|
||
"codex-agent-manager/internal/agents"
|
||
"codex-agent-manager/internal/app"
|
||
)
|
||
|
||
func New(cfg app.Config) http.Handler {
|
||
mux := http.NewServeMux()
|
||
agentStore := agents.Store{CodexHome: cfg.CodexHome}
|
||
mux.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) {
|
||
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||
})
|
||
mux.HandleFunc("/api/agents", func(w http.ResponseWriter, r *http.Request) {
|
||
if r.Method != http.MethodGet {
|
||
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "方法不允许"})
|
||
return
|
||
}
|
||
items, err := agentStore.List()
|
||
if err != nil {
|
||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
||
})
|
||
return mux
|
||
}
|
||
|
||
func writeJSON(w http.ResponseWriter, status int, body any) {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(status)
|
||
_ = json.NewEncoder(w).Encode(body)
|
||
}
|
||
```
|
||
|
||
Modify `cmd/codex-agent-manager/main.go` to use `server.New(cfg)`:
|
||
|
||
```go
|
||
package main
|
||
|
||
import (
|
||
"fmt"
|
||
"net/http"
|
||
|
||
"codex-agent-manager/internal/app"
|
||
"codex-agent-manager/internal/server"
|
||
)
|
||
|
||
func main() {
|
||
cfg := app.DefaultConfig()
|
||
fmt.Printf("Codex 智能体管理台监听 http://%s\n", cfg.HTTPAddr)
|
||
if err := http.ListenAndServe(cfg.HTTPAddr, server.New(cfg)); err != nil {
|
||
panic(err)
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
git add internal cmd
|
||
git commit -m "feat: read codex agent definitions"
|
||
```
|
||
|
||
Expected: commit succeeds.
|
||
|
||
---
|
||
|
||
### Task 3: 项目配置、运行线程和工作流只读模型
|
||
|
||
**Files:**
|
||
- Create: `internal/projects/store.go`
|
||
- Create: `internal/runtime/model.go`
|
||
- Create: `internal/runtime/store.go`
|
||
- Create: `internal/workflow/model.go`
|
||
- Create: `internal/workflow/store.go`
|
||
- Create tests for each package
|
||
- Modify: `internal/server/server.go`
|
||
|
||
- [ ] **Step 1: Write tests for project config parser**
|
||
|
||
Create `internal/projects/store_test.go`:
|
||
|
||
```go
|
||
package projects
|
||
|
||
import (
|
||
"os"
|
||
"path/filepath"
|
||
"testing"
|
||
)
|
||
|
||
func TestListProjectsParsesTrustConfig(t *testing.T) {
|
||
root := t.TempDir()
|
||
cfg := filepath.Join(root, "config.toml")
|
||
content := `[projects."/Users/yoilun"]
|
||
trust_level = "trusted"
|
||
|
||
[projects."/Users/yoilun/Code/managed-portal"]
|
||
trust_level = "trusted"
|
||
`
|
||
if err := os.WriteFile(cfg, []byte(content), 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
store := Store{CodexHome: root}
|
||
got, err := store.List()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(got) != 2 {
|
||
t.Fatalf("project count = %d, want 2", len(got))
|
||
}
|
||
if got[0].Path != "/Users/yoilun" || got[0].TrustLevel != "trusted" {
|
||
t.Fatalf("unexpected first project: %#v", got[0])
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Implement project parser**
|
||
|
||
Create `internal/projects/store.go`:
|
||
|
||
```go
|
||
package projects
|
||
|
||
import (
|
||
"os"
|
||
"path/filepath"
|
||
"sort"
|
||
"strings"
|
||
)
|
||
|
||
type ProjectInfo struct {
|
||
ID string `json:"id"`
|
||
Path string `json:"path"`
|
||
Name string `json:"name"`
|
||
TrustLevel string `json:"trustLevel"`
|
||
Exists bool `json:"exists"`
|
||
}
|
||
|
||
type Store struct {
|
||
CodexHome string
|
||
}
|
||
|
||
func (s Store) List() ([]ProjectInfo, error) {
|
||
data, err := os.ReadFile(filepath.Join(s.CodexHome, "config.toml"))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
lines := strings.Split(string(data), "\n")
|
||
var result []ProjectInfo
|
||
var current *ProjectInfo
|
||
for _, line := range lines {
|
||
line = strings.TrimSpace(line)
|
||
if strings.HasPrefix(line, `[projects."`) && strings.HasSuffix(line, `"]`) {
|
||
path := strings.TrimSuffix(strings.TrimPrefix(line, `[projects."`), `"]`)
|
||
info := ProjectInfo{ID: pathToID(path), Path: path, Name: filepath.Base(path)}
|
||
if _, err := os.Stat(path); err == nil {
|
||
info.Exists = true
|
||
}
|
||
result = append(result, info)
|
||
current = &result[len(result)-1]
|
||
continue
|
||
}
|
||
if current != nil && strings.HasPrefix(line, "trust_level") {
|
||
parts := strings.SplitN(line, "=", 2)
|
||
if len(parts) == 2 {
|
||
current.TrustLevel = strings.Trim(strings.TrimSpace(parts[1]), `"`)
|
||
}
|
||
}
|
||
}
|
||
sort.Slice(result, func(i, j int) bool { return result[i].Path < result[j].Path })
|
||
return result, nil
|
||
}
|
||
|
||
func pathToID(path string) string {
|
||
id := strings.Trim(path, string(filepath.Separator))
|
||
id = strings.ReplaceAll(id, string(filepath.Separator), "__")
|
||
if id == "" {
|
||
return "root"
|
||
}
|
||
return id
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: Write workflow model test**
|
||
|
||
Create `internal/workflow/store_test.go`:
|
||
|
||
```go
|
||
package workflow
|
||
|
||
import "testing"
|
||
|
||
func TestBuildEventsFromSpawnEdgesDoesNotAssumeFixedFlow(t *testing.T) {
|
||
edges := []EdgeInput{
|
||
{ParentID: "main", ChildID: "child-1", ParentName: "主智能体", ChildName: "Cicero", ChildRole: "UI设计师", Status: "open"},
|
||
{ParentID: "main", ChildID: "child-2", ParentName: "主智能体", ChildName: "Gauss", ChildRole: "智能体编排者", Status: "closed"},
|
||
}
|
||
got := BuildEvents(edges)
|
||
if len(got) != 2 {
|
||
t.Fatalf("event count = %d, want 2", len(got))
|
||
}
|
||
if got[0].Type != "主智能体派发" || got[0].Confidence != "high" {
|
||
t.Fatalf("unexpected event: %#v", got[0])
|
||
}
|
||
if got[1].Target.Role != "智能体编排者" {
|
||
t.Fatalf("role should come from data, got %#v", got[1])
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: Implement workflow model**
|
||
|
||
Create `internal/workflow/model.go`:
|
||
|
||
```go
|
||
package workflow
|
||
|
||
type Actor struct {
|
||
ThreadID string `json:"threadId"`
|
||
Name string `json:"name"`
|
||
Role string `json:"role"`
|
||
}
|
||
|
||
type WorkflowEvent struct {
|
||
ID string `json:"id"`
|
||
Type string `json:"type"`
|
||
Actor Actor `json:"actor"`
|
||
Target Actor `json:"target"`
|
||
Summary string `json:"summary"`
|
||
Source string `json:"source"`
|
||
Confidence string `json:"confidence"`
|
||
}
|
||
|
||
type EdgeInput struct {
|
||
ParentID string
|
||
ChildID string
|
||
ParentName string
|
||
ParentRole string
|
||
ChildName string
|
||
ChildRole string
|
||
Status string
|
||
}
|
||
```
|
||
|
||
Create `internal/workflow/store.go`:
|
||
|
||
```go
|
||
package workflow
|
||
|
||
func BuildEvents(edges []EdgeInput) []WorkflowEvent {
|
||
events := make([]WorkflowEvent, 0, len(edges))
|
||
for _, edge := range edges {
|
||
parentName := edge.ParentName
|
||
if parentName == "" {
|
||
parentName = "主智能体"
|
||
}
|
||
childName := edge.ChildName
|
||
if childName == "" {
|
||
childName = edge.ChildID
|
||
}
|
||
events = append(events, WorkflowEvent{
|
||
ID: edge.ParentID + "->" + edge.ChildID,
|
||
Type: "主智能体派发",
|
||
Actor: Actor{
|
||
ThreadID: edge.ParentID,
|
||
Name: parentName,
|
||
Role: edge.ParentRole,
|
||
},
|
||
Target: Actor{
|
||
ThreadID: edge.ChildID,
|
||
Name: childName,
|
||
Role: edge.ChildRole,
|
||
},
|
||
Summary: parentName + " 派发任务给 " + childName,
|
||
Source: "thread_spawn_edges",
|
||
Confidence: "high",
|
||
})
|
||
}
|
||
return events
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 5: Run tests and commit**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
go test ./...
|
||
git add internal
|
||
git commit -m "feat: add project and workflow read models"
|
||
```
|
||
|
||
Expected: tests pass and commit succeeds.
|
||
|
||
---
|
||
|
||
### Task 4: 中文只读前端工作台
|
||
|
||
**Files:**
|
||
- Create frontend files under `web/`
|
||
- Modify: `docs/project.md`
|
||
|
||
- [ ] **Step 1: Create Vue/Vite files**
|
||
|
||
Create `web/package.json`:
|
||
|
||
```json
|
||
{
|
||
"name": "codex-agent-manager-web",
|
||
"version": "0.1.0",
|
||
"type": "module",
|
||
"scripts": {
|
||
"dev": "vite --host 127.0.0.1 --port 13083",
|
||
"build": "vite build",
|
||
"preview": "vite preview --host 127.0.0.1 --port 13084"
|
||
},
|
||
"dependencies": {
|
||
"@vitejs/plugin-vue": "^5.0.0",
|
||
"vite": "^5.0.0",
|
||
"vue": "^3.4.0"
|
||
},
|
||
"devDependencies": {}
|
||
}
|
||
```
|
||
|
||
Create `web/vite.config.js`:
|
||
|
||
```js
|
||
import { defineConfig } from 'vite'
|
||
import vue from '@vitejs/plugin-vue'
|
||
|
||
export default defineConfig({
|
||
plugins: [vue()],
|
||
server: {
|
||
proxy: {
|
||
'/api': 'http://127.0.0.1:18083'
|
||
}
|
||
}
|
||
})
|
||
```
|
||
|
||
Create `web/index.html`:
|
||
|
||
```html
|
||
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>Codex 智能体管理台</title>
|
||
</head>
|
||
<body>
|
||
<div id="app"></div>
|
||
<script type="module" src="/src/main.js"></script>
|
||
</body>
|
||
</html>
|
||
```
|
||
|
||
Create `web/src/main.js`:
|
||
|
||
```js
|
||
import { createApp } from 'vue'
|
||
import App from './App.vue'
|
||
import './styles.css'
|
||
|
||
createApp(App).mount('#app')
|
||
```
|
||
|
||
- [ ] **Step 2: Implement app shell**
|
||
|
||
Create `web/src/App.vue`:
|
||
|
||
```vue
|
||
<script setup>
|
||
import { ref } from 'vue'
|
||
import ProjectView from './views/ProjectView.vue'
|
||
import WorkflowView from './views/WorkflowView.vue'
|
||
import AgentView from './views/AgentView.vue'
|
||
import DraftsView from './views/DraftsView.vue'
|
||
import SettingsView from './views/SettingsView.vue'
|
||
|
||
const tabs = [
|
||
{ key: 'projects', label: '项目视图', component: ProjectView },
|
||
{ key: 'workflow', label: '工作流视图', component: WorkflowView },
|
||
{ key: 'agents', label: '智能体视图', component: AgentView },
|
||
{ key: 'drafts', label: '草稿', component: DraftsView },
|
||
{ key: 'settings', label: '设置', component: SettingsView }
|
||
]
|
||
const active = ref('projects')
|
||
</script>
|
||
|
||
<template>
|
||
<main class="shell">
|
||
<header class="topbar">
|
||
<div>
|
||
<h1>智能体工作台</h1>
|
||
<p>项目活动、角色编辑、工作流交接、审查循环</p>
|
||
</div>
|
||
<input class="search" placeholder="搜索项目、智能体、阶段、PID..." />
|
||
</header>
|
||
<nav class="tabs" aria-label="主导航">
|
||
<button
|
||
v-for="tab in tabs"
|
||
:key="tab.key"
|
||
:class="['tab', { active: active === tab.key }]"
|
||
@click="active = tab.key"
|
||
>
|
||
{{ tab.label }}
|
||
</button>
|
||
</nav>
|
||
<component :is="tabs.find(tab => tab.key === active).component" />
|
||
</main>
|
||
</template>
|
||
```
|
||
|
||
Create `web/src/styles.css`:
|
||
|
||
```css
|
||
:root {
|
||
color: #20251f;
|
||
background: #f4f1ea;
|
||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||
}
|
||
|
||
body {
|
||
margin: 0;
|
||
background: #f4f1ea;
|
||
}
|
||
|
||
.shell {
|
||
min-height: 100vh;
|
||
padding: 24px;
|
||
}
|
||
|
||
.topbar {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 20px;
|
||
align-items: center;
|
||
margin-bottom: 18px;
|
||
}
|
||
|
||
.topbar h1 {
|
||
margin: 0;
|
||
font-size: 28px;
|
||
}
|
||
|
||
.topbar p {
|
||
margin: 6px 0 0;
|
||
color: #6b675d;
|
||
}
|
||
|
||
.search {
|
||
width: min(420px, 40vw);
|
||
border: 1px solid #ddd2bf;
|
||
border-radius: 8px;
|
||
background: #fffaf0;
|
||
padding: 12px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.tabs {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.tab {
|
||
border: 1px solid #ded8cc;
|
||
background: #fffaf0;
|
||
color: #3e403b;
|
||
border-radius: 8px;
|
||
padding: 10px 14px;
|
||
font-weight: 650;
|
||
}
|
||
|
||
.tab.active {
|
||
background: #263f38;
|
||
color: #fff;
|
||
border-color: #263f38;
|
||
}
|
||
|
||
.panel {
|
||
border: 1px solid #ded8cc;
|
||
background: #fffdf8;
|
||
border-radius: 8px;
|
||
padding: 16px;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: Create placeholder views with real Chinese labels**
|
||
|
||
Create `web/src/views/ProjectView.vue`:
|
||
|
||
```vue
|
||
<template>
|
||
<section class="panel">
|
||
<h2>项目视图</h2>
|
||
<p>按项目查看智能体执行情况、状态来源和置信度。</p>
|
||
</section>
|
||
</template>
|
||
```
|
||
|
||
Create `web/src/views/WorkflowView.vue`:
|
||
|
||
```vue
|
||
<template>
|
||
<section class="panel">
|
||
<h2>工作流视图</h2>
|
||
<p>查看阶段进度、智能体交接、审查循环和主智能体监管。</p>
|
||
</section>
|
||
</template>
|
||
```
|
||
|
||
Create `web/src/views/AgentView.vue`:
|
||
|
||
```vue
|
||
<template>
|
||
<section class="panel">
|
||
<h2>智能体视图</h2>
|
||
<p>查看和编辑智能体名称、描述、角色设定。</p>
|
||
</section>
|
||
</template>
|
||
```
|
||
|
||
Create `web/src/views/DraftsView.vue`:
|
||
|
||
```vue
|
||
<template>
|
||
<section class="panel">
|
||
<h2>草稿</h2>
|
||
<p>查看 TOML 校验、差异、备份和待写回变更。</p>
|
||
</section>
|
||
</template>
|
||
```
|
||
|
||
Create `web/src/views/SettingsView.vue`:
|
||
|
||
```vue
|
||
<template>
|
||
<section class="panel">
|
||
<h2>设置</h2>
|
||
<p>配置 Codex 路径、数据源和备份策略。</p>
|
||
</section>
|
||
</template>
|
||
```
|
||
|
||
- [ ] **Step 4: Build frontend**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
cd web && pnpm install && pnpm build
|
||
```
|
||
|
||
Expected: build succeeds. If network blocks dependency install, request escalation or document the blocker.
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
git add web docs/project.md
|
||
git commit -m "feat: add chinese vue workbench shell"
|
||
```
|
||
|
||
Expected: commit succeeds.
|
||
|
||
---
|
||
|
||
### Task 5: API integration and read-only data display
|
||
|
||
**Files:**
|
||
- Modify: `web/src/api/client.js`
|
||
- Modify: `web/src/views/ProjectView.vue`
|
||
- Modify: `web/src/views/WorkflowView.vue`
|
||
- Modify: `web/src/views/AgentView.vue`
|
||
- Modify: `web/src/components/StatusBadge.vue`
|
||
|
||
- [ ] **Step 1: Add API client**
|
||
|
||
Create `web/src/api/client.js`:
|
||
|
||
```js
|
||
export async function getJSON(path) {
|
||
const response = await fetch(path)
|
||
if (!response.ok) {
|
||
const text = await response.text()
|
||
throw new Error(text || `请求失败:${response.status}`)
|
||
}
|
||
return response.json()
|
||
}
|
||
|
||
export const api = {
|
||
agents: () => getJSON('/api/agents'),
|
||
projects: () => getJSON('/api/projects'),
|
||
projectRuntime: id => getJSON(`/api/projects/${encodeURIComponent(id)}/runtime`),
|
||
projectWorkflow: id => getJSON(`/api/projects/${encodeURIComponent(id)}/workflow`)
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Add StatusBadge component**
|
||
|
||
Create `web/src/components/StatusBadge.vue`:
|
||
|
||
```vue
|
||
<script setup>
|
||
defineProps({
|
||
label: { type: String, required: true },
|
||
confidence: { type: String, default: 'low' },
|
||
source: { type: String, default: 'unknown' }
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<span class="status-badge" :data-confidence="confidence">
|
||
{{ label }} · 置信度:{{ confidence }} · 来源:{{ source }}
|
||
</span>
|
||
</template>
|
||
```
|
||
|
||
Add to `web/src/styles.css`:
|
||
|
||
```css
|
||
.status-badge {
|
||
display: inline-flex;
|
||
border-radius: 999px;
|
||
padding: 6px 10px;
|
||
background: #f7eddc;
|
||
color: #7a4d13;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.status-badge[data-confidence="high"] {
|
||
background: #e7f4ec;
|
||
color: #265a3c;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: Integrate AgentView with `/api/agents`**
|
||
|
||
Update `web/src/views/AgentView.vue`:
|
||
|
||
```vue
|
||
<script setup>
|
||
import { onMounted, ref } from 'vue'
|
||
import { api } from '../api/client'
|
||
|
||
const agents = ref([])
|
||
const error = ref('')
|
||
|
||
onMounted(async () => {
|
||
try {
|
||
const data = await api.agents()
|
||
agents.value = data.items || []
|
||
} catch (err) {
|
||
error.value = err.message
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<section class="panel">
|
||
<h2>智能体视图</h2>
|
||
<p>查看和编辑智能体名称、描述、角色设定。</p>
|
||
<p v-if="error" class="error">{{ error }}</p>
|
||
<div class="list">
|
||
<article v-for="agent in agents" :key="agent.id" class="row-card">
|
||
<strong>{{ agent.name || agent.fileName }}</strong>
|
||
<span>{{ agent.description || '无描述' }}</span>
|
||
<small>{{ agent.parseStatus === 'valid' ? 'TOML 有效' : 'TOML 无效' }}</small>
|
||
</article>
|
||
</div>
|
||
</section>
|
||
</template>
|
||
```
|
||
|
||
Add to `web/src/styles.css`:
|
||
|
||
```css
|
||
.list {
|
||
display: grid;
|
||
gap: 10px;
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.row-card {
|
||
display: grid;
|
||
gap: 6px;
|
||
padding: 12px;
|
||
border: 1px solid #e3d8c8;
|
||
background: #fffaf0;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.row-card span,
|
||
.row-card small {
|
||
color: #6f665a;
|
||
}
|
||
|
||
.error {
|
||
color: #9f3a2f;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: Run build and commit**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
cd web && pnpm build
|
||
git add web
|
||
git commit -m "feat: connect ui to readonly agent api"
|
||
```
|
||
|
||
Expected: build succeeds and commit succeeds.
|
||
|
||
---
|
||
|
||
### Task 6: 草稿、校验、diff、备份、写回
|
||
|
||
**Files:**
|
||
- Modify: `internal/agents/store.go`
|
||
- Create: `internal/agents/writeback_test.go`
|
||
- Modify: `internal/server/server.go`
|
||
- Modify: `web/src/views/AgentView.vue`
|
||
- Modify: `web/src/views/DraftsView.vue`
|
||
|
||
- [ ] **Step 1: Write writeback safety tests**
|
||
|
||
Create `internal/agents/writeback_test.go`:
|
||
|
||
```go
|
||
package agents
|
||
|
||
import (
|
||
"os"
|
||
"path/filepath"
|
||
"testing"
|
||
)
|
||
|
||
func TestBackupBeforeWriteCreatesBackupAndWrites(t *testing.T) {
|
||
root := t.TempDir()
|
||
agentsDir := filepath.Join(root, "agents")
|
||
if err := os.MkdirAll(agentsDir, 0o755); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
path := filepath.Join(agentsDir, "demo.toml")
|
||
original := `name = "旧名称"
|
||
description = "旧描述"
|
||
developer_instructions = """
|
||
旧角色
|
||
"""
|
||
`
|
||
if err := os.WriteFile(path, []byte(original), 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
store := Store{CodexHome: root}
|
||
newContent := `name = "新名称"
|
||
description = "新描述"
|
||
developer_instructions = """
|
||
新角色
|
||
"""
|
||
`
|
||
result, err := store.WriteWithBackup("demo", newContent)
|
||
if err != nil {
|
||
t.Fatalf("WriteWithBackup returned error: %v", err)
|
||
}
|
||
if result.BackupPath == "" {
|
||
t.Fatal("expected backup path")
|
||
}
|
||
written, err := os.ReadFile(path)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if string(written) != newContent {
|
||
t.Fatalf("file was not written")
|
||
}
|
||
}
|
||
|
||
func TestWriteWithBackupRejectsInvalidToml(t *testing.T) {
|
||
root := t.TempDir()
|
||
agentsDir := filepath.Join(root, "agents")
|
||
if err := os.MkdirAll(agentsDir, 0o755); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := os.WriteFile(filepath.Join(agentsDir, "demo.toml"), []byte(`name = "旧"`), 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
store := Store{CodexHome: root}
|
||
_, err := store.WriteWithBackup("demo", `name = "`)
|
||
if err == nil {
|
||
t.Fatal("expected invalid TOML to be rejected")
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Implement writeback with backup**
|
||
|
||
Add to `internal/agents/model.go`:
|
||
|
||
```go
|
||
type WriteResult struct {
|
||
TargetPath string `json:"targetPath"`
|
||
BackupPath string `json:"backupPath"`
|
||
Status string `json:"status"`
|
||
}
|
||
```
|
||
|
||
Add to `internal/agents/store.go`:
|
||
|
||
```go
|
||
func (s Store) WriteWithBackup(id string, content string) (WriteResult, error) {
|
||
if _, err := parseSimpleTOML(content); err != nil {
|
||
return WriteResult{}, err
|
||
}
|
||
target := filepath.Join(s.CodexHome, "agents", id+".toml")
|
||
cleanAgentsDir := filepath.Join(s.CodexHome, "agents")
|
||
rel, err := filepath.Rel(cleanAgentsDir, target)
|
||
if err != nil || strings.HasPrefix(rel, "..") {
|
||
return WriteResult{}, errInvalidTOML("写回目标不在 agents 目录")
|
||
}
|
||
current, err := os.ReadFile(target)
|
||
if err != nil {
|
||
return WriteResult{}, err
|
||
}
|
||
backup := target + ".bak." + time.Now().Format("20060102-150405")
|
||
if err := os.WriteFile(backup, current, 0o600); err != nil {
|
||
return WriteResult{}, err
|
||
}
|
||
tmp := target + ".tmp"
|
||
if err := os.WriteFile(tmp, []byte(content), 0o600); err != nil {
|
||
return WriteResult{}, err
|
||
}
|
||
if err := os.Rename(tmp, target); err != nil {
|
||
return WriteResult{}, err
|
||
}
|
||
return WriteResult{TargetPath: target, BackupPath: backup, Status: "已写回"}, nil
|
||
}
|
||
```
|
||
|
||
Also add `time` to imports.
|
||
|
||
- [ ] **Step 3: Run tests**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
go test ./internal/agents
|
||
go test ./...
|
||
```
|
||
|
||
Expected: PASS.
|
||
|
||
- [ ] **Step 4: Add server endpoints and UI flow**
|
||
|
||
Implement minimal endpoints:
|
||
|
||
- `POST /api/agents/{id}/validate`
|
||
- `POST /api/agents/{id}/write`
|
||
|
||
UI must show buttons in Chinese:
|
||
|
||
- `校验 TOML`
|
||
- `查看差异`
|
||
- `创建备份并写回`
|
||
|
||
Expected behavior:
|
||
|
||
- invalid TOML shows Chinese error and disables writeback.
|
||
- writeback response shows target path and backup path.
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
git add internal web
|
||
git commit -m "feat: add safe agent writeback flow"
|
||
```
|
||
|
||
Expected: commit succeeds.
|
||
|
||
---
|
||
|
||
### Task 7: 集成验证、浏览器检查和最终文档
|
||
|
||
**Files:**
|
||
- Modify: `docs/project.md`
|
||
- Modify: `README.md`
|
||
- Modify: `task_plan.md`
|
||
- Modify: `progress.md`
|
||
- Modify: `findings.md`
|
||
|
||
- [ ] **Step 1: Create README**
|
||
|
||
Create `README.md`:
|
||
|
||
```markdown
|
||
# Codex 智能体管理台
|
||
|
||
本项目是本机 localhost 工具,用中文管理 Codex 智能体配置、项目运行状态和动态工作流交接。
|
||
|
||
## 启动后端
|
||
|
||
```bash
|
||
go run ./cmd/codex-agent-manager
|
||
```
|
||
|
||
## 启动前端
|
||
|
||
```bash
|
||
cd web
|
||
pnpm install
|
||
pnpm dev
|
||
```
|
||
|
||
## 验证
|
||
|
||
```bash
|
||
go test ./...
|
||
cd web && pnpm build
|
||
```
|
||
|
||
## 安全边界
|
||
|
||
- 不读取 `.codex/auth.json`。
|
||
- 不写入 Codex SQLite。
|
||
- `.codex/agents/*.toml` 写回前必须校验、diff、备份和确认。
|
||
```
|
||
|
||
- [ ] **Step 2: Run full verification**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
go test ./...
|
||
cd web && pnpm build
|
||
```
|
||
|
||
Expected: both pass.
|
||
|
||
- [ ] **Step 3: Browser smoke test**
|
||
|
||
Start services:
|
||
|
||
```bash
|
||
go run ./cmd/codex-agent-manager
|
||
cd web && pnpm dev
|
||
```
|
||
|
||
Open `http://127.0.0.1:13083`.
|
||
|
||
Verify:
|
||
|
||
- 页面中文显示。
|
||
- 标签页包括项目视图、工作流视图、智能体视图、草稿、设置。
|
||
- 智能体视图能加载 agent 列表或清晰显示错误。
|
||
- 没有静默写回按钮。
|
||
|
||
- [ ] **Step 4: Update final docs**
|
||
|
||
Update `docs/project.md` with actual commands and final architecture.
|
||
|
||
Update `task_plan.md` phase statuses to complete only after corresponding review passes.
|
||
|
||
Update `progress.md` with test results:
|
||
|
||
```markdown
|
||
| Time | Command | Result | Notes |
|
||
| --- | --- | --- | --- |
|
||
| 2026-05-25 | go test ./... | pass | 后端测试 |
|
||
| 2026-05-25 | cd web && pnpm build | pass | 前端构建 |
|
||
```
|
||
|
||
- [ ] **Step 5: Final commit**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
git add README.md docs task_plan.md findings.md progress.md
|
||
git commit -m "docs: finalize runbook and verification evidence"
|
||
```
|
||
|
||
Expected: commit succeeds.
|
||
|
||
---
|
||
|
||
## Review Gates
|
||
|
||
Each implementation phase must end with a testing/code review agent report:
|
||
|
||
```markdown
|
||
## Bugs Found
|
||
|
||
- [blocking/non-blocking] [file:line] 问题标题
|
||
- 复现方式:
|
||
- 预期结果:
|
||
- 实际结果:
|
||
- 影响范围:
|
||
- 建议验证方式:
|
||
|
||
## Tests Run
|
||
|
||
| Command | Result | Notes |
|
||
| --- | --- | --- |
|
||
|
||
## Verdict
|
||
|
||
[pass / fail]
|
||
```
|
||
|
||
If verdict is `fail`, the main agent sends only the bug report back to the coding agent. The fix loop is limited to 3 attempts per phase.
|
||
|
||
## Self-Review
|
||
|
||
- Spec coverage: plan covers project setup, Go backend, Vue frontend, read-only data, dynamic workflow, safe writeback, Chinese UI, docs, and review gates.
|
||
- Placeholder scan: no placeholder instructions remain; unresolved design choices are assigned to implementation tasks.
|
||
- Type consistency: `AgentDefinition`, `ProjectInfo`, `WorkflowEvent`, and `StatusEvidence` names match the design spec and task snippets.
|