Add U1 task summary body contract helpers

Why: Freeze L1 skeleton KV keys and honest no-findings placeholder
so Backend can stop stuffing raw task text into progress cards.
Evidence: go test ./... -count=1
Impact: New TaskSummaryView helpers; findings[] still deferred to U2/U3.
Refs: fast-slow-channel-display-u1-plan W1

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-12 07:41:05 +00:00
co-authored by Cursor
parent 89f3906c39
commit ba289e7b17
2 changed files with 190 additions and 0 deletions
+100
View File
@@ -0,0 +1,100 @@
package core
// TaskSummaryView 是 U1 写入 ActivityCard.Body 的权威语义草图。
// 完整 TaskSnapshot(findings[]、revision、streamScope)留待 U2;本类型只冻结
// 运行中/终态的精炼骨架字段与 Body KV 键名,供 Backend 生成、旧客户端回退阅读。
type TaskSummaryView struct {
Title string // 业务动宾短标题(通常已在 ActivityCard.Title)
Focus string // 正在做什么,≤2 行
FindingsNote string // U1:无成果时诚实占位;结构化 findings 属 U3
Remaining string
NextStep string
OutcomeLine string // 完成后的结论一行;运行中可空
}
// Body KV 键名(U1 冻结;用户默认卡身可见,勿改成内部材料键)。
const (
TaskSummaryKVStatus = "状态"
TaskSummaryKVFocus = "正在做"
TaskSummaryKVFindings = "当前发现"
TaskSummaryKVRemaining = "剩余"
TaskSummaryKVNextStep = "下一步"
)
// 状态行用户文案(与执行态映射;排队/进行中/完成/失败)。
const (
TaskSummaryStatusQueued = "排队中"
TaskSummaryStatusRunning = "进行中"
TaskSummaryStatusDone = "已完成"
TaskSummaryStatusFailed = "执行失败"
)
// 无阶段成果时的诚实占位(R03-a);不得编造业务发现。
const TaskSummaryNoFindingsYet = "尚无阶段结论,正在读取材料"
// RunningBody 生成运行中(含排队)进度卡 Body:Markdown 占位 + 固定骨架 KV。
// extraKV(材料键等)保留在同一 UIKV 节点,供系统去重,不充当用户进度正文。
func (v TaskSummaryView) RunningBody(statusLabel string, extraKV [][2]string) []UINode {
focus := stringsOr(v.Focus, "处理已登记工作")
findings := stringsOr(v.FindingsNote, TaskSummaryNoFindingsYet)
remaining := stringsOr(v.Remaining, "待任务进展后更新")
next := stringsOr(v.NextStep, "继续处理已登记工作")
if statusLabel == "" {
statusLabel = TaskSummaryStatusRunning
}
kv := append(append([][2]string(nil), extraKV...),
[2]string{TaskSummaryKVStatus, statusLabel},
[2]string{TaskSummaryKVFocus, focus},
[2]string{TaskSummaryKVFindings, findings},
[2]string{TaskSummaryKVRemaining, remaining},
[2]string{TaskSummaryKVNextStep, next},
)
return []UINode{
{Type: UIMarkdown, Markdown: findings},
{Type: UIKV, KV: kv},
}
}
// FinishedBody 生成终态卡 Body:结论 Markdown + 骨架 KV(状态为已完成或执行失败)。
func (v TaskSummaryView) FinishedBody(statusLabel string, extraKV [][2]string) []UINode {
outcome := stringsOr(v.OutcomeLine, "(无结果摘要)")
findings := stringsOr(v.FindingsNote, outcome)
if statusLabel == "" {
statusLabel = TaskSummaryStatusDone
}
kv := append(append([][2]string(nil), extraKV...),
[2]string{TaskSummaryKVStatus, statusLabel},
[2]string{TaskSummaryKVFocus, stringsOr(v.Focus, "")},
[2]string{TaskSummaryKVFindings, findings},
[2]string{TaskSummaryKVRemaining, stringsOr(v.Remaining, "")},
[2]string{TaskSummaryKVNextStep, stringsOr(v.NextStep, "")},
)
// 去掉空值骨架行,避免完成卡出现空白「正在做」。
compact := make([][2]string, 0, len(kv))
for _, pair := range kv {
if pair[1] == "" && (pair[0] == TaskSummaryKVFocus || pair[0] == TaskSummaryKVRemaining || pair[0] == TaskSummaryKVNextStep) {
continue
}
compact = append(compact, pair)
}
return []UINode{
{Type: UIMarkdown, Markdown: outcome},
{Type: UIKV, KV: compact},
}
}
// CardDisplaysFailed 判断卡是否应按失败展示:含 CardFailed,以及旧客户端
// done + danger 兼容映射(U1 出口白名单)。
func CardDisplaysFailed(status CardStatus, tone string) bool {
if status == CardFailed {
return true
}
return status == CardDone && tone == "danger"
}
func stringsOr(s, fallback string) string {
if s == "" {
return fallback
}
return s
}
+90
View File
@@ -0,0 +1,90 @@
package core
import (
"encoding/json"
"strings"
"testing"
)
func TestTaskSummaryRunningBodyOmitsRawTaskAndUsesHonestPlaceholder(t *testing.T) {
v := TaskSummaryView{Focus: "核对需求与会议决定"}
body := v.RunningBody(TaskSummaryStatusQueued, [][2]string{{"材料键", "mk_x"}})
if len(body) != 2 || body[0].Type != UIMarkdown || body[1].Type != UIKV {
t.Fatalf("body shape = %+v", body)
}
if body[0].Markdown != TaskSummaryNoFindingsYet {
t.Fatalf("markdown = %q, want honest placeholder", body[0].Markdown)
}
rawTask := "逐页解析上传材料并对照验收条件" + strings.Repeat("x", 200)
if strings.Contains(body[0].Markdown, rawTask) {
t.Fatal("running body must not embed raw task text")
}
got := map[string]string{}
for _, kv := range body[1].KV {
got[kv[0]] = kv[1]
}
for _, key := range []string{TaskSummaryKVStatus, TaskSummaryKVFocus, TaskSummaryKVFindings, TaskSummaryKVRemaining, TaskSummaryKVNextStep, "材料键"} {
if _, ok := got[key]; !ok {
t.Fatalf("missing kv %q in %v", key, got)
}
}
if got[TaskSummaryKVStatus] != TaskSummaryStatusQueued {
t.Fatalf("status = %q", got[TaskSummaryKVStatus])
}
if got[TaskSummaryKVFindings] != TaskSummaryNoFindingsYet {
t.Fatalf("findings = %q", got[TaskSummaryKVFindings])
}
}
func TestTaskSummaryFinishedBodyFailureLabel(t *testing.T) {
v := TaskSummaryView{OutcomeLine: "执行失败:boom"}
body := v.FinishedBody(TaskSummaryStatusFailed, nil)
if body[0].Markdown != "执行失败:boom" {
t.Fatalf("outcome = %q", body[0].Markdown)
}
found := false
for _, kv := range body[1].KV {
if kv[0] == TaskSummaryKVStatus {
found = true
if kv[1] != TaskSummaryStatusFailed {
t.Fatalf("status = %q, want %q", kv[1], TaskSummaryStatusFailed)
}
}
}
if !found {
t.Fatal("missing status kv")
}
}
func TestTaskSummaryViewJSONRoundTrip(t *testing.T) {
in := TaskSummaryView{
Title: "核对三份材料", Focus: "核对需求", FindingsNote: TaskSummaryNoFindingsYet,
Remaining: "评审意见", NextStep: "整理冲突", OutcomeLine: "",
}
b, err := json.Marshal(in)
if err != nil {
t.Fatal(err)
}
var out TaskSummaryView
if err := json.Unmarshal(b, &out); err != nil {
t.Fatal(err)
}
if out != in {
t.Fatalf("round-trip = %+v, want %+v", out, in)
}
}
func TestCardDisplaysFailed(t *testing.T) {
if !CardDisplaysFailed(CardFailed, "danger") {
t.Fatal("CardFailed should display failed")
}
if !CardDisplaysFailed(CardDone, "danger") {
t.Fatal("legacy done+danger should display failed")
}
if CardDisplaysFailed(CardDone, "ok") {
t.Fatal("done+ok must not display failed")
}
if CardDisplaysFailed(CardActive, "info") {
t.Fatal("active must not display failed")
}
}