feat(swarm): add goal_summary to swarm status view (#45/#28 consumer ask)
@Mem0ried 客户端 consumer 验收(#59)指出 GET /swarms/:id 缺 goal_summary —— Run 列表 只能显示 deployment_id/status,体验差。补:swarmDeploymentView 增加 goal_summary,从持久化 plan_json 顶层 objective 提取,折叠空白为单行 + 截断(200 rune)+ RedactText 兜底;取不到 (无 plan / 无 objective / 坏 JSON)返回空串,不臆造。list 与 detail 同走 swarmDeploymentView, 两处都带上。 文档 docs/integration/heicode-desktop-client-api.md §5.2 状态样例补 goal_summary 字段说明。 测试 TestSwarmGoalSummary 覆盖:空/坏 JSON/无 objective→空;多行多空格折叠;误入 sk- 被脱敏; 超长截断带省略号。go build ./... 与 controller 测试全绿。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -60,8 +60,9 @@ func swarmDeploymentView(dep model.AgentDeployment) gin.H {
|
||||
"swarm_id": dep.RuntimeSwarmID,
|
||||
"runtime_deployment_id": dep.RuntimeDeploymentID,
|
||||
"correlation_id": dep.CorrelationID,
|
||||
"status": dep.Status, // 运行时真实状态(契约 §4)
|
||||
"status": dep.Status, // 运行时真实状态(契约 §4)
|
||||
"display_status": swarmDisplayStatus(dep.Status), // 客户端展示态(契约 §4.1 映射)
|
||||
"goal_summary": swarmGoalSummary(dep.PlanJSON), // 单行目标(客户端 Run 列表展示;@Mem0ried #28 消费需求)
|
||||
"phase": dep.Phase,
|
||||
"runtime_state": dep.RuntimeState,
|
||||
"failure_reason": dep.FailureReason,
|
||||
@@ -71,6 +72,31 @@ func swarmDeploymentView(dep model.AgentDeployment) gin.H {
|
||||
}
|
||||
}
|
||||
|
||||
// swarmGoalSummary 从持久化的 plan_json 提取顶层 objective,折叠为单行供客户端 Run 列表展示
|
||||
// (#28 消费需求:列表不止 deployment_id/status)。空白折叠 + 截断 + RedactText 兜底(objective
|
||||
// 是用户目标文本而非密钥,但仍按统一红线剥离误入的 sk-/token);取不到则返回空串(不臆造)。
|
||||
func swarmGoalSummary(planJSON string) string {
|
||||
if strings.TrimSpace(planJSON) == "" {
|
||||
return ""
|
||||
}
|
||||
var plan struct {
|
||||
Objective string `json:"objective"`
|
||||
}
|
||||
if err := common.UnmarshalJsonStr(planJSON, &plan); err != nil {
|
||||
return ""
|
||||
}
|
||||
s := strings.TrimSpace(plan.Objective)
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
s = strings.Join(strings.Fields(s), " ") // 折叠所有空白(含换行)为单空格
|
||||
const maxLen = 200
|
||||
if len([]rune(s)) > maxLen {
|
||||
s = strings.TrimSpace(string([]rune(s)[:maxLen])) + "…"
|
||||
}
|
||||
return model.RedactText(s)
|
||||
}
|
||||
|
||||
// swarmDisplayStatus 把运行时真实状态映射到客户端展示态(agent_swarm runtime-contract §4.1)。
|
||||
// 运行时只有 waiting_approval/running/blocked/completed/failed/stopped;`blocked` 展示为
|
||||
// `degraded`,其余直通。preparing/verifying 是 running 的子态、由运行时阶段决定,HM 未单独存,
|
||||
@@ -138,7 +164,7 @@ func sanitizeSwarmPayload(raw string) map[string]any {
|
||||
// swarmEventView maps a persisted callback event to the client view (payload 脱敏)。
|
||||
func swarmEventView(e model.AgentCallbackEvent) gin.H {
|
||||
return gin.H{
|
||||
"id": e.Id, // HM 不透明分页游标(next_after);单调,兼容 sequence 未上线
|
||||
"id": e.Id, // HM 不透明分页游标(next_after);单调,兼容 sequence 未上线
|
||||
"sequence": e.Sequence, // agent_swarm v1 per-swarm 序号(客户端去重/排序;0=envelope 未带)
|
||||
"event_id": e.EventID,
|
||||
"event_type": e.EventType,
|
||||
|
||||
@@ -2,6 +2,7 @@ package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/heicode/manager/model"
|
||||
@@ -91,3 +92,25 @@ func TestSwarmFrozenEventTypesRegistered(t *testing.T) {
|
||||
require.True(t, inReq, "event_type %s 应在 requiredFields 注册", et)
|
||||
}
|
||||
}
|
||||
|
||||
// #28(@Mem0ried 消费需求):goal_summary 从 plan_json 顶层 objective 提取 —— 折叠空白为单行、
|
||||
// 截断、脱敏;无 plan / 无 objective / 坏 JSON 返回空串(不臆造)。
|
||||
func TestSwarmGoalSummary(t *testing.T) {
|
||||
require.Empty(t, swarmGoalSummary(""))
|
||||
require.Empty(t, swarmGoalSummary("not-json"))
|
||||
require.Empty(t, swarmGoalSummary(`{"sub_mode":"swarm"}`)) // 无 objective
|
||||
|
||||
// 多行/多空格折叠成单行(JSON 里的 \n 是合法转义,用原始串避免再转义)
|
||||
got := swarmGoalSummary(`{"objective":" 迁移 支付服务\n 到 K8s "}`)
|
||||
require.Equal(t, "迁移 支付服务 到 K8s", got)
|
||||
|
||||
// 误入的 sk- 被 RedactText 兜底
|
||||
red := swarmGoalSummary(`{"objective":"deploy with key sk-abcdEFGH1234567890XYZ now"}`)
|
||||
require.NotContains(t, red, "sk-abcdEFGH1234567890XYZ")
|
||||
|
||||
// 超长截断带省略号
|
||||
long := `{"objective":"` + strings.Repeat("x", 400) + `"}`
|
||||
out := swarmGoalSummary(long)
|
||||
require.LessOrEqual(t, len([]rune(out)), 201) // 200 + …
|
||||
require.True(t, strings.HasSuffix(out, "…"))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user