From ba289e7b1780a29855aef8477bf1ac49bd563eb2 Mon Sep 17 00:00:00 2001 From: weichen Date: Mon, 12 Oct 2026 07:41:05 +0000 Subject: [PATCH] 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 --- task_summary.go | 100 +++++++++++++++++++++++++++++++++++++++++++ task_summary_test.go | 90 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 190 insertions(+) create mode 100644 task_summary.go create mode 100644 task_summary_test.go diff --git a/task_summary.go b/task_summary.go new file mode 100644 index 0000000..7fe7f17 --- /dev/null +++ b/task_summary.go @@ -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 +} diff --git a/task_summary_test.go b/task_summary_test.go new file mode 100644 index 0000000..a705bd4 --- /dev/null +++ b/task_summary_test.go @@ -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") + } +} -- 2.54.0