feat(web): 用小队子页面展示思考与工具过程

气泡只聚合过程摘要并链到 hash 子页面;成员编制来自只读 squad 端点,思考与工具默认折叠。
This commit is contained in:
weichen
2026-09-28 10:30:53 +00:00
parent 265616e997
commit 3d6dcc3ae5
30 changed files with 1003 additions and 8 deletions
+1
View File
@@ -232,6 +232,7 @@ func (s *Server) Routes() http.Handler {
mux.HandleFunc("POST /api/v1/tasks/runs", s.handleCreateTaskRun)
mux.HandleFunc("GET /api/v1/tasks/runs/{run_id}", s.handleGetTaskRun)
mux.HandleFunc("GET /api/v1/tasks/runs/{run_id}/events", s.handleTaskRunEvents)
mux.HandleFunc("GET /api/v1/tasks/runs/{run_id}/squad", s.handleGetTaskRunSquad)
mux.HandleFunc("POST /api/v1/tasks/runs/{run_id}/cancel", s.handleCancelTaskRun)
mux.HandleFunc("POST /api/v1/tasks/runs/{run_id}/resume", s.handleResumeTaskRun)
mux.HandleFunc("POST /api/v1/admin/"+workspace.WorkspacesDirName+"/reconcile", s.handleReconcileWorkspace)
+42
View File
@@ -630,6 +630,48 @@ func (s *Server) handleGetTaskRun(w http.ResponseWriter, r *http.Request) {
writeJSONResponse(w, http.StatusOK, s.buildTaskRunResponse(rec))
}
// handleGetTaskRunSquad 只读投影工作区 squad.json,供小队过程子页面左栏。
func (s *Server) handleGetTaskRunSquad(w http.ResponseWriter, r *http.Request) {
user, ok := s.requirePrincipal(w, r)
if !ok {
return
}
runID := r.PathValue("run_id")
if runID == "" {
writeSafeError(w, r, http.StatusBadRequest, "run_id required")
return
}
if !s.assertTaskRunOwnedByUser(w, r.Context(), runID, user) {
return
}
rec, err := s.taskRuns().GetRun(runID)
if err != nil {
writeDomainError(w, r, http.StatusNotFound, err)
return
}
if rec == nil || strings.TrimSpace(rec.WorkspaceID) == "" || s.resolver() == nil {
writeSafeError(w, r, http.StatusNotFound, "workspace not found")
return
}
dir, err := s.resolver().ResolveStoredWorkDir(rec.WorkspaceID)
if err != nil {
writeSafeError(w, r, http.StatusNotFound, "workspace not found")
return
}
proj, err := squad.ReadSquadProjection(dir)
if err != nil {
writeSafeError(w, r, http.StatusNotFound, "squad not found")
return
}
if proj.TaskKey == "" {
proj.TaskKey = rec.TaskKey
}
if proj.Title == "" {
proj.Title = rec.TaskKey
}
writeJSONResponse(w, http.StatusOK, proj)
}
func (s *Server) handleResumeTaskRun(w http.ResponseWriter, r *http.Request) {
user, ok := s.requirePrincipal(w, r)
if !ok {
+60
View File
@@ -0,0 +1,60 @@
package squad
import (
"encoding/json"
"os"
"path/filepath"
)
// SquadMemberView 是子页面左栏的只读编制投影(不含模型名与工具权限)。
type SquadMemberView struct {
Role string `json:"role"`
Name string `json:"name"`
Responsibilities []string `json:"responsibilities"`
Source string `json:"source,omitempty"`
}
// SquadProjection GET /tasks/runs/{id}/squad 的响应体。
type SquadProjection struct {
Members []SquadMemberView `json:"members"`
TaskKey string `json:"task_key"`
Title string `json:"title"`
}
// ReadSquadProjection 读取工作区 squad.json(及可选 workspace.json 标题)。
// 缺少 squad.json 时返回错误,由调用方决定 404。
func ReadSquadProjection(workDir string) (SquadProjection, error) {
data, err := os.ReadFile(filepath.Join(workDir, "squad.json"))
if err != nil {
return SquadProjection{}, err
}
var cfg SquadConfig
if err := json.Unmarshal(data, &cfg); err != nil {
return SquadProjection{}, err
}
members := make([]SquadMemberView, 0, len(cfg.Agents))
for _, agent := range cfg.Agents {
resp := agent.Responsibilities
if resp == nil {
resp = []string{}
}
members = append(members, SquadMemberView{
Role: agent.Role,
Name: agent.Name,
Responsibilities: resp,
Source: string(agent.Source),
})
}
title := cfg.SquadName
taskKey := ""
if metaRaw, err := os.ReadFile(filepath.Join(workDir, "workspace.json")); err == nil {
var meta WorkspaceMetadata
if json.Unmarshal(metaRaw, &meta) == nil {
if meta.StageTitle != "" {
title = meta.StageTitle
}
taskKey = meta.TaskKey
}
}
return SquadProjection{Members: members, TaskKey: taskKey, Title: title}, nil
}
+38
View File
@@ -0,0 +1,38 @@
package squad
import (
"os"
"path/filepath"
"testing"
)
func TestReadSquadProjection(t *testing.T) {
dir := t.TempDir()
squadJSON := `{
"squad_name": "立项小队",
"version": "1",
"agents": [
{"role": "regulatory", "name": "法规专员", "source": "standard", "responsibilities": ["筛法规"]}
]
}`
if err := os.WriteFile(filepath.Join(dir, "squad.json"), []byte(squadJSON), 0o644); err != nil {
t.Fatal(err)
}
ws := `{"task_key":"intake","stage_title":"用户需求收集"}`
if err := os.WriteFile(filepath.Join(dir, "workspace.json"), []byte(ws), 0o644); err != nil {
t.Fatal(err)
}
got, err := ReadSquadProjection(dir)
if err != nil {
t.Fatal(err)
}
if got.TaskKey != "intake" || got.Title != "用户需求收集" {
t.Fatalf("meta: %+v", got)
}
if len(got.Members) != 1 || got.Members[0].Role != "regulatory" || got.Members[0].Name != "法规专员" {
t.Fatalf("members: %+v", got.Members)
}
if _, err := ReadSquadProjection(t.TempDir()); err == nil {
t.Fatal("missing squad.json must error")
}
}