Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba289e7b17 | ||
|
|
89f3906c39 | ||
|
|
bbebd153f4 | ||
|
|
a563d31b1e | ||
|
|
39632c2e7a | ||
|
|
37b96fd94f | ||
|
|
7a98a7e7fe | ||
|
|
360a8a8946 | ||
|
|
452daf3ffa |
+100
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
+47
-1
@@ -98,7 +98,8 @@ export type StreamEventType =
|
||||
| 'agent.spawn' | 'agent.progress' | 'agent.result'
|
||||
| 'artifact.created' | 'artifact.updated'
|
||||
| 'run.completed' | 'run.failed' | 'error'
|
||||
| 'card.created' | 'card.updated' | 'card.status' | 'card.resolved';
|
||||
| 'card.created' | 'card.updated' | 'card.status' | 'card.resolved'
|
||||
| 'surface.updated';
|
||||
|
||||
export interface StreamEvent<P = unknown> {
|
||||
seq: number; runId?: ID; conversationId: ID; ts: string; v: number;
|
||||
@@ -184,6 +185,9 @@ export type UINodeType =
|
||||
|
||||
export interface UINode {
|
||||
type: UINodeType;
|
||||
/** Surface 层节点级标注(Generative UI U2):整页唯一稳定 id 与事实来源标识,旧路径不填。 */
|
||||
id?: string;
|
||||
source?: string[];
|
||||
title?: string;
|
||||
items?: string[];
|
||||
kv?: [string, string][];
|
||||
@@ -351,3 +355,45 @@ export interface UploadInfo {
|
||||
artifactId: ID; filename: string; mime?: string; size: number;
|
||||
parsed?: ParsedDocument;
|
||||
}
|
||||
|
||||
// ---- Generative UI Surface(提案 U2/U4;与 Go surface.go 同步)----
|
||||
|
||||
export interface SurfaceCompat {
|
||||
schemaVersion: number;
|
||||
catalogVersion: string;
|
||||
pluginVersion?: string;
|
||||
}
|
||||
|
||||
export interface SurfaceIdentity {
|
||||
surfaceId: ID;
|
||||
owner: string;
|
||||
projectId?: ID;
|
||||
conversationId?: ID;
|
||||
sourceRunId?: ID;
|
||||
pluginId: string;
|
||||
}
|
||||
|
||||
export interface SurfaceSpec {
|
||||
title: string;
|
||||
nodes: UINode[];
|
||||
}
|
||||
|
||||
export interface SurfaceSnapshot {
|
||||
identity: SurfaceIdentity;
|
||||
compat: SurfaceCompat;
|
||||
revision: number;
|
||||
baseRevision?: number;
|
||||
contextVersion?: string;
|
||||
activationEpoch: number;
|
||||
spec: SurfaceSpec;
|
||||
semanticText?: string;
|
||||
}
|
||||
|
||||
export interface SurfaceMeta {
|
||||
surfaceId: ID;
|
||||
title: string;
|
||||
pluginId: string;
|
||||
revision: number;
|
||||
conversationId?: ID;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user