智能体工作台
+项目活动、角色编辑、工作流交接、审查循环
+diff --git a/docs/superpowers/plans/2026-05-25-codex-agent-manager-implementation.md b/docs/superpowers/plans/2026-05-25-codex-agent-manager-implementation.md new file mode 100644 index 0000000..429a50b --- /dev/null +++ b/docs/superpowers/plans/2026-05-25-codex-agent-manager-implementation.md @@ -0,0 +1,1701 @@ +# 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 + + +
+ + +项目活动、角色编辑、工作流交接、审查循环
+按项目查看智能体执行情况、状态来源和置信度。
+查看阶段进度、智能体交接、审查循环和主智能体监管。
+查看和编辑智能体名称、描述、角色设定。
+查看 TOML 校验、差异、备份和待写回变更。
+配置 Codex 路径、数据源和备份策略。
+查看和编辑智能体名称、描述、角色设定。
+{{ error }}
+