feat(web): 用小队子页面展示思考与工具过程
气泡只聚合过程摘要并链到 hash 子页面;成员编制来自只读 squad 端点,思考与工具默认折叠。
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user