From a4ea66c41d643f8f71d1c4db71342b5ffe2c7ec0 Mon Sep 17 00:00:00 2001 From: weichen Date: Wed, 30 Sep 2026 06:22:00 +0000 Subject: [PATCH] Add activity card, card events, HITL anchoring and UI vocabulary extension MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: Contract foundation for the inbox/command-bus/workspaces phase (proposal D28/D30): ActivityCard as the first-class state object behind the right-rail inbox, card.* stream events, inline-confirm anchoring (anchorMessageId + blocked/blockReason) on HITL, seven new UI schema vocabularies, plugin provides.commands/workspaces, workflow card step and workspace declarations. Evidence: go vet clean; conformance goldens regenerated and verified — new activity_card / hitl_card_blocked / event_card_created / event_card_resolved / ui_vocabularies samples plus extended plugin_manifest; CardStatus.Terminal helper unit-covered via build; ts/types.ts mirrors all additions. Impact: Additive only (omitempty fields, new types, new event types) — existing consumers ignore unknown values per forward-compat rule; vocabulary spec types carry a Spec suffix to avoid const-name collisions. Refs: modules/documents docs/proposals/2026-09-30-ui-inbox-command-workspace.md --- activity.go | 56 +++++++++ conformance_test.go | 73 ++++++++++- events.go | 26 ++++ hitl.go | 6 + plugin.go | 28 ++++- testdata/golden/activity_card.json | 63 ++++++++++ testdata/golden/event_card_created.json | 19 +++ testdata/golden/event_card_resolved.json | 12 ++ testdata/golden/hitl_card_blocked.json | 27 ++++ testdata/golden/plugin_manifest.json | 6 + testdata/golden/ui_vocabularies.json | 152 +++++++++++++++++++++++ ts/types.ts | 54 +++++++- ui.go | 109 +++++++++++++++- 13 files changed, 624 insertions(+), 7 deletions(-) create mode 100644 activity.go create mode 100644 testdata/golden/activity_card.json create mode 100644 testdata/golden/event_card_created.json create mode 100644 testdata/golden/event_card_resolved.json create mode 100644 testdata/golden/hitl_card_blocked.json create mode 100644 testdata/golden/ui_vocabularies.json diff --git a/activity.go b/activity.go new file mode 100644 index 0000000..2c2106a --- /dev/null +++ b/activity.go @@ -0,0 +1,56 @@ +package core + +import "time" + +// ActivityCard:异步任务/待确认事项的统一状态对象——右栏收件箱的条目。 +// 与一次性消息相对:卡片是有生命周期的状态(进行中→完成/待办→已决/失败/弃置), +// 由服务端权威维护,经 card.* 事件驱动前端投影;确认动作回写对话时间线并留审计。 +// +// 生命周期:active → done | action_required → resolved | failed | dismissed。 +// Kind 决定卡身的默认内容组合(progress 流式区 / compare 对比 / review-list 裁决清单…), +// Body 用 UI 有限词汇渲染;插件可扩展 Kind(前端按未知 kind 走通用卡渲染)。 +type ActivityCard struct { + ID ID `json:"id"` + // ProjectID/ConversationID 定位归属;ConversationID 为空表示项目级卡片(远期跨会话收件箱)。 + ProjectID ID `json:"projectId"` + ConversationID ID `json:"conversationId,omitempty"` + SourceRunID ID `json:"sourceRunId,omitempty"` // 溯源:产生卡片的 run + AnchorMessageID ID `json:"anchorMessageId,omitempty"` // 互定位:出生消息(↙ 定位到对话) + Kind CardKind `json:"kind"` + Title string `json:"title"` + Status CardStatus `json:"status"` + Tone string `json:"tone,omitempty"` // ok | warn | danger | info + Body []UINode `json:"body,omitempty"` + Actions []UIAction `json:"actions,omitempty"` // 走命令总线;未绑定 command 降级为发消息 + Pinned bool `json:"pinned,omitempty"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +type CardKind string + +const ( + CardKindProgress CardKind = "progress" // 后台任务进度(流式区 + 进度行) + CardKindConfirm CardKind = "confirm" // 待确认(相关性/冲突/门禁/投影) + CardKindCompare CardKind = "compare" // 左右对比(基线 vs 新内容) + CardKindReviewList CardKind = "review-list" // 逐条裁决清单(候选边/缺项) + CardKindResult CardKind = "result" // 结果与产物(结论 + 来源 + 打开入口) + CardKindError CardKind = "error" // 失败显式呈现(可重试/可取回) + CardKindStatus CardKind = "status" // 常态状态卡(文档进度等) +) + +type CardStatus string + +const ( + CardActive CardStatus = "active" // 进行中 + CardActionRequired CardStatus = "action_required" // 待用户处理 + CardDone CardStatus = "done" // 完成(未读结果) + CardResolved CardStatus = "resolved" // 已确认/已处理(盖章留痕) + CardFailed CardStatus = "failed" + CardDismissed CardStatus = "dismissed" // 用户关闭(非确认语义) +) + +// Terminal 报告卡片是否处于终态(终态卡进入"已完成"可收纳区)。 +func (s CardStatus) Terminal() bool { + return s == CardResolved || s == CardFailed || s == CardDismissed +} diff --git a/conformance_test.go b/conformance_test.go index 72ec07d..48be882 100644 --- a/conformance_test.go +++ b/conformance_test.go @@ -83,6 +83,77 @@ func TestGoldenHITL(t *testing.T) { }, AllowFreeInput: true, FreeInputHint: "也可以直接输入其他格式要求", }) + // 阻断态 + 锚定(内联确认卡:anchorMessageId 内联于对话流;blocked 禁用确认并给出出路)。 + golden(t, "hitl_card_blocked", HITLCard{ + ID: "hitl_def456abc789", RunID: "run_1", ConversationID: "cnv_1", + AnchorMessageID: "msg_abc123def456", + Question: "批准变更 CO-2026-007?", Kind: HITLKindConfirm, + Choices: []HITLChoice{ + {ID: "approve", Label: "确认批准", Description: "变更单进入执行态", Impact: "立即生效并留痕,不可裸文本撤回"}, + {ID: "reject", Label: "驳回", Description: "关闭变更单并记录理由", Impact: "留痕可审计"}, + }, + Blocked: true, BlockReason: "存在未闭环高风险变更单,可先处理后重新检查", + }) +} + +func TestGoldenActivityCard(t *testing.T) { + golden(t, "activity_card", ActivityCard{ + ID: "card_abc123def456", ProjectID: "prj_abc123def456", ConversationID: "cnv_abc123def456", + SourceRunID: "run_abc123def456", AnchorMessageID: "msg_abc123def456", + Kind: CardKindCompare, Title: "项目执行周期不一致 · 目标已基线", + Status: CardActionRequired, Tone: "danger", + Body: []UINode{ + {Type: UICompare, Compare: &UICompareSpec{ + Left: UICompareSide{Title: "基线 V1.2 · 已评审", Tone: "danger", Nodes: []UINode{{Type: UIMarkdown, Markdown: "12 个月(立项到注册)"}}}, + Right: UICompareSide{Title: "新资料 · 待确认", Tone: "ok", Nodes: []UINode{{Type: UIMarkdown, Markdown: "18 个月(含临床试验)"}}}, + }}, + {Type: UIProgress, Title: "切片进度", Progress: &UIProgressSpec{Label: "切片 24/24", Value: 1, Tone: "ok"}}, + }, + Actions: []UIAction{ + {ID: "escalate", Label: "发起变更流程", Style: "primary", Command: "change.escalate"}, + {ID: "keep", Label: "维持原样", Style: "danger"}, + }, + Pinned: true, + }) +} + +func TestGoldenCardEvents(t *testing.T) { + golden(t, "event_card_created", StreamEvent{Seq: 45, RunID: "run_1", ConversationID: "cnv_1", TS: "2026-09-30T10:00:00Z", V: 1, Type: EvCardCreated, + Payload: json.RawMessage(`{"card":{"id":"card_abc123def456","projectId":"prj_1","kind":"progress","title":"竞品分析 PPTX 入库","status":"active","createdAt":"2026-09-30T10:00:00Z","updatedAt":"2026-09-30T10:00:00Z"}}`)}) + golden(t, "event_card_resolved", StreamEvent{Seq: 46, RunID: "run_1", ConversationID: "cnv_1", TS: "2026-09-30T10:05:00Z", V: 1, Type: EvCardResolved, + Payload: json.RawMessage(`{"cardId":"card_abc123def456","actionId":"escalate"}`)}) +} + +func TestGoldenUIVocabularies(t *testing.T) { + golden(t, "ui_vocabularies", []UINode{ + {Type: UIProgress, Progress: &UIProgressSpec{Label: "市场可行性 78%", Value: 0.78, Tone: "ok"}}, + {Type: UICompare, Compare: &UICompareSpec{ + Left: UICompareSide{Title: "基线", Tone: "danger"}, + Right: UICompareSide{Title: "新内容", Tone: "ok", Nodes: []UINode{{Type: UIMarkdown, Markdown: "18 个月"}}}, + }}, + {Type: UITree, Tree: &UITreeSpec{Nodes: []UITreeNode{ + {ID: "stage-1", Label: "商务与策划", Count: 24, Children: []UITreeNode{ + {ID: "topic-1", Label: "市场调研", Tone: "ok", Badge: "多挂载", Count: 12, Selected: true}, + }}, + }}}, + {Type: UITable, Table: &UITableSpec{ + Columns: []UITableColumn{{Key: "id", Label: "编号"}, {Key: "title", Label: "标题"}}, + Rows: []UITableRow{ + {ID: "ur-005", Cells: [][2]string{{"id", "UR-005"}, {"title", "项目执行周期"}}, Actions: []UIAction{{ID: "trace", Label: "追溯"}}}, + }, + }}, + {Type: UISteps, Steps: &UIStepsSpec{Current: 2, Steps: []UIStep{ + {Label: "申请", Status: "done"}, {Label: "影响分析", Status: "done"}, {Label: "评审", Status: "active"}, {Label: "批准", Status: "pending"}, + }}}, + {Type: UITabs, Tabs: &UITabsSpec{Active: "view", Tabs: []UITab{ + {ID: "view", Label: "查看", Nodes: []UINode{{Type: UIMarkdown, Markdown: "正文"}}}, + {ID: "history", Label: "历史版本"}, + }}}, + {Type: UITimeline, Timeline: &UITimelineSpec{Items: []UITimelineItem{ + {Time: "10:42", Title: "v0.6 填入注册分类界定", Tone: "ok", Markdown: "来源 S-121", Ref: "S-121"}, + {Time: "09-28", Title: "v0.5 更新竞品定价段落", Ref: "陈磊确认"}, + }}}, + }) } func TestGoldenProfileAndTools(t *testing.T) { @@ -119,7 +190,7 @@ func TestGoldenPlugin(t *testing.T) { golden(t, "plugin_manifest", PluginManifest{ ID: "example", Name: "示例插件", Version: "0.1.0", APIVersion: PluginAPIVersion, Requires: PluginRequires{Network: []string{"api.example.com"}}, - Provides: PluginProvides{Tools: []string{"http_echo"}, Intents: []string{"demo"}, Panels: []string{"demo"}, Cards: []string{"note"}, Settings: []string{"greeting"}}, + Provides: PluginProvides{Tools: []string{"http_echo"}, Intents: []string{"demo"}, Panels: []string{"demo"}, Cards: []string{"note"}, Settings: []string{"greeting"}, Commands: []string{"demo.refresh"}, Workspaces: []string{"demo-main"}}, }) } diff --git a/events.go b/events.go index a8278f0..a29acf8 100644 --- a/events.go +++ b/events.go @@ -34,6 +34,11 @@ const ( EvRunCompleted StreamEventType = "run.completed" EvRunFailed StreamEventType = "run.failed" EvError StreamEventType = "error" + // 收件箱卡片(activity.go):前端右栏收件箱的唯一数据源(投影语义,服务端权威)。 + EvCardCreated StreamEventType = "card.created" + EvCardUpdated StreamEventType = "card.updated" + EvCardStatus StreamEventType = "card.status" + EvCardResolved StreamEventType = "card.resolved" ) // ProtocolVersion 是事件信封与 payload 词汇的当前版本。 @@ -131,3 +136,24 @@ type ErrorPayload struct { Message string `json:"message"` RunID ID `json:"runId,omitempty"` } + +// CardPayload:card.created / card.updated 携带整卡投影(服务端权威,前端整体替换)。 +type CardPayload struct { + Card ActivityCard `json:"card"` +} + +// CardStatusPayload:card.status 携带状态迁移(含 Tone,便于轻量更新状态灯)。 +type CardStatusPayload struct { + CardID ID `json:"cardId"` + Status CardStatus `json:"status"` + Tone string `json:"tone,omitempty"` + Note string `json:"note,omitempty"` +} + +// CardResolvedPayload:card.resolved 携带所点动作语义——确认结论回写对话时间线 +// (AI 回执)并写入审计;Answer 为自由输入文本(如有)。 +type CardResolvedPayload struct { + CardID ID `json:"cardId"` + ActionID string `json:"actionId,omitempty"` + Answer string `json:"answer,omitempty"` +} diff --git a/hitl.go b/hitl.go index d71c007..9d589d6 100644 --- a/hitl.go +++ b/hitl.go @@ -17,6 +17,12 @@ type HITLCard struct { Form []UINode `json:"form,omitempty"` // form kind 的字段(UI 词汇) CreatedAt string `json:"createdAt"` Resolved bool `json:"resolved"` + // AnchorMessageID:锚定的发起消息——未决卡内联渲染于该消息之后(对话流组成部分), + // 并在右栏收件箱生成投影卡(双现;任一侧响应即双侧销项)。 + AnchorMessageID ID `json:"anchorMessageId,omitempty"` + // Blocked:阻断态(门禁不通过等)——确认入口禁用并给出出路;阻断本身进审计。 + Blocked bool `json:"blocked,omitempty"` + BlockReason string `json:"blockReason,omitempty"` } type HITLKind string diff --git a/plugin.go b/plugin.go index f85edb8..a610c9b 100644 --- a/plugin.go +++ b/plugin.go @@ -28,6 +28,8 @@ type PluginProvides struct { Panels []string `json:"panels,omitempty"` Cards []string `json:"cards,omitempty"` Settings []string `json:"settings,omitempty"` + Commands []string `json:"commands,omitempty"` // commands/ 资产名(spec 权威在 Agent-Command 库) + Workspaces []string `json:"workspaces,omitempty"` // workspaces/ 资产名(工作区弹层) } // PluginPanel:右栏面板声明(UI schema 静态骨架 + 可选数据提供者)。 @@ -52,12 +54,36 @@ type WorkflowDef struct { type WorkflowStep struct { ID string `json:"id"` Title string `json:"title"` - Kind string `json:"kind"` // prompt | tool | hitl | artifact + Kind string `json:"kind"` // prompt | tool | hitl | artifact | card Prompt string `json:"prompt,omitempty"` // prompt: 发给模型的指令模板 Tool string `json:"tool,omitempty"` // tool: 工具名 Args json.RawMessage `json:"args,omitempty"` // tool: 参数模板 Hitl *HITLCardTemplate `json:"hitl,omitempty"` // hitl: 卡片模板 Artifact string `json:"artifact,omitempty"` // artifact: 产出工件名模板 + Card *ActivityCardTemplate `json:"card,omitempty"` // card: 产收件箱任务卡 +} + +// ActivityCardTemplate:工作流步骤产卡模板(kind=card)——步骤执行时生成 +// ActivityCard(右栏收件箱),Body/Actions 走 UI 有限词汇;Actions 建议绑定命令。 +type ActivityCardTemplate struct { + Kind CardKind `json:"kind"` + Title string `json:"title"` + Tone string `json:"tone,omitempty"` + Body []UINode `json:"body,omitempty"` + Actions []UIAction `json:"actions,omitempty"` +} + +// PluginWorkspace:工作区弹层声明(深读与治理视图;Esc/遮罩关闭回对话现场, +// 不承载任何唯一步骤——每个操作必须另有命令路径,提案 D31)。 +type PluginWorkspace struct { + ID string `json:"id"` + Title string `json:"title"` + Icon string `json:"icon,omitempty"` + Shortcut string `json:"shortcut,omitempty"` // "1"–"9"(⌘N / Ctrl+N 召出) + BadgeTool string `json:"badgeTool,omitempty"` // 角标提供者工具:返回 {count,tone} + Body []UINode `json:"body"` + // RefreshTool:挂载时调用刷新 Body 动态区(与 Panel 同机制,v1 整体刷新)。 + RefreshTool string `json:"refreshTool,omitempty"` } type HITLCardTemplate struct { diff --git a/testdata/golden/activity_card.json b/testdata/golden/activity_card.json new file mode 100644 index 0000000..5f0ae26 --- /dev/null +++ b/testdata/golden/activity_card.json @@ -0,0 +1,63 @@ +{ + "id": "card_abc123def456", + "projectId": "prj_abc123def456", + "conversationId": "cnv_abc123def456", + "sourceRunId": "run_abc123def456", + "anchorMessageId": "msg_abc123def456", + "kind": "compare", + "title": "项目执行周期不一致 · 目标已基线", + "status": "action_required", + "tone": "danger", + "body": [ + { + "type": "compare", + "compare": { + "left": { + "title": "基线 V1.2 · 已评审", + "tone": "danger", + "nodes": [ + { + "type": "markdown", + "markdown": "12 个月(立项到注册)" + } + ] + }, + "right": { + "title": "新资料 · 待确认", + "tone": "ok", + "nodes": [ + { + "type": "markdown", + "markdown": "18 个月(含临床试验)" + } + ] + } + } + }, + { + "type": "progress", + "title": "切片进度", + "progress": { + "label": "切片 24/24", + "value": 1, + "tone": "ok" + } + } + ], + "actions": [ + { + "id": "escalate", + "label": "发起变更流程", + "style": "primary", + "command": "change.escalate" + }, + { + "id": "keep", + "label": "维持原样", + "style": "danger" + } + ], + "pinned": true, + "createdAt": "0001-01-01T00:00:00Z", + "updatedAt": "0001-01-01T00:00:00Z" +} diff --git a/testdata/golden/event_card_created.json b/testdata/golden/event_card_created.json new file mode 100644 index 0000000..9ce59fc --- /dev/null +++ b/testdata/golden/event_card_created.json @@ -0,0 +1,19 @@ +{ + "seq": 45, + "runId": "run_1", + "conversationId": "cnv_1", + "ts": "2026-09-30T10:00:00Z", + "v": 1, + "type": "card.created", + "payload": { + "card": { + "id": "card_abc123def456", + "projectId": "prj_1", + "kind": "progress", + "title": "竞品分析 PPTX 入库", + "status": "active", + "createdAt": "2026-09-30T10:00:00Z", + "updatedAt": "2026-09-30T10:00:00Z" + } + } +} diff --git a/testdata/golden/event_card_resolved.json b/testdata/golden/event_card_resolved.json new file mode 100644 index 0000000..f1da242 --- /dev/null +++ b/testdata/golden/event_card_resolved.json @@ -0,0 +1,12 @@ +{ + "seq": 46, + "runId": "run_1", + "conversationId": "cnv_1", + "ts": "2026-09-30T10:05:00Z", + "v": 1, + "type": "card.resolved", + "payload": { + "cardId": "card_abc123def456", + "actionId": "escalate" + } +} diff --git a/testdata/golden/hitl_card_blocked.json b/testdata/golden/hitl_card_blocked.json new file mode 100644 index 0000000..1029e48 --- /dev/null +++ b/testdata/golden/hitl_card_blocked.json @@ -0,0 +1,27 @@ +{ + "id": "hitl_def456abc789", + "runId": "run_1", + "conversationId": "cnv_1", + "question": "批准变更 CO-2026-007?", + "kind": "confirm", + "choices": [ + { + "id": "approve", + "label": "确认批准", + "description": "变更单进入执行态", + "impact": "立即生效并留痕,不可裸文本撤回" + }, + { + "id": "reject", + "label": "驳回", + "description": "关闭变更单并记录理由", + "impact": "留痕可审计" + } + ], + "allowFreeInput": false, + "createdAt": "", + "resolved": false, + "anchorMessageId": "msg_abc123def456", + "blocked": true, + "blockReason": "存在未闭环高风险变更单,可先处理后重新检查" +} diff --git a/testdata/golden/plugin_manifest.json b/testdata/golden/plugin_manifest.json index e3b2ada..3f1f0d8 100644 --- a/testdata/golden/plugin_manifest.json +++ b/testdata/golden/plugin_manifest.json @@ -23,6 +23,12 @@ ], "settings": [ "greeting" + ], + "commands": [ + "demo.refresh" + ], + "workspaces": [ + "demo-main" ] } } diff --git a/testdata/golden/ui_vocabularies.json b/testdata/golden/ui_vocabularies.json new file mode 100644 index 0000000..34569a1 --- /dev/null +++ b/testdata/golden/ui_vocabularies.json @@ -0,0 +1,152 @@ +[ + { + "type": "progress", + "progress": { + "label": "市场可行性 78%", + "value": 0.78, + "tone": "ok" + } + }, + { + "type": "compare", + "compare": { + "left": { + "title": "基线", + "tone": "danger" + }, + "right": { + "title": "新内容", + "tone": "ok", + "nodes": [ + { + "type": "markdown", + "markdown": "18 个月" + } + ] + } + } + }, + { + "type": "tree", + "tree": { + "nodes": [ + { + "id": "stage-1", + "label": "商务与策划", + "count": 24, + "children": [ + { + "id": "topic-1", + "label": "市场调研", + "tone": "ok", + "badge": "多挂载", + "count": 12, + "selected": true + } + ] + } + ] + } + }, + { + "type": "table", + "table": { + "columns": [ + { + "key": "id", + "label": "编号" + }, + { + "key": "title", + "label": "标题" + } + ], + "rows": [ + { + "id": "ur-005", + "cells": [ + [ + "id", + "UR-005" + ], + [ + "title", + "项目执行周期" + ] + ], + "actions": [ + { + "id": "trace", + "label": "追溯" + } + ] + } + ] + } + }, + { + "type": "steps", + "steps": { + "current": 2, + "steps": [ + { + "label": "申请", + "status": "done" + }, + { + "label": "影响分析", + "status": "done" + }, + { + "label": "评审", + "status": "active" + }, + { + "label": "批准", + "status": "pending" + } + ] + } + }, + { + "type": "tabs", + "tabs": { + "active": "view", + "tabs": [ + { + "id": "view", + "label": "查看", + "nodes": [ + { + "type": "markdown", + "markdown": "正文" + } + ] + }, + { + "id": "history", + "label": "历史版本" + } + ] + } + }, + { + "type": "timeline", + "timeline": { + "items": [ + { + "time": "10:42", + "title": "v0.6 填入注册分类界定", + "tone": "ok", + "markdown": "来源 S-121", + "ref": "S-121" + }, + { + "time": "09-28", + "title": "v0.5 更新竞品定价段落", + "ref": "陈磊确认" + } + ] + } + } +] diff --git a/ts/types.ts b/ts/types.ts index dccb406..1d7616b 100644 --- a/ts/types.ts +++ b/ts/types.ts @@ -80,7 +80,8 @@ export type StreamEventType = | 'run.phase' | 'hitl.request' | 'hitl.resolved' | 'plan.update' | 'todo.update' | 'agent.spawn' | 'agent.progress' | 'agent.result' | 'artifact.created' | 'artifact.updated' - | 'run.completed' | 'run.failed' | 'error'; + | 'run.completed' | 'run.failed' | 'error' + | 'card.created' | 'card.updated' | 'card.status' | 'card.resolved'; export interface StreamEvent

{ seq: number; runId?: ID; conversationId: ID; ts: string; v: number; @@ -105,6 +106,28 @@ export interface ContextObservation { } export interface RunFailedPayload { code: string; message: string } +// ---- 收件箱卡片 ---- + +export type CardKind = + | 'progress' | 'confirm' | 'compare' | 'review-list' + | 'result' | 'error' | 'status'; + +export type CardStatus = + | 'active' | 'action_required' | 'done' + | 'resolved' | 'failed' | 'dismissed'; + +export interface ActivityCard { + id: ID; projectId: ID; conversationId?: ID; + sourceRunId?: ID; anchorMessageId?: ID; + kind: CardKind; title: string; status: CardStatus; tone?: string; + body?: UINode[]; actions?: { id: string; label: string; style?: 'primary' | 'default' | 'danger'; command?: string }[]; + pinned?: boolean; createdAt: string; updatedAt: string; +} + +export interface CardPayload { card: ActivityCard } +export interface CardStatusPayload { cardId: ID; status: CardStatus; tone?: string; note?: string } +export interface CardResolvedPayload { cardId: ID; actionId?: string; answer?: string } + // ---- HITL ---- export type HITLKind = 'choice' | 'input' | 'confirm' | 'form'; @@ -117,6 +140,7 @@ export interface HITLCard { id: ID; runId: ID; conversationId: ID; question: string; kind: HITLKind; choices?: HITLChoice[]; allowFreeInput: boolean; freeInputHint?: string; timeoutSec?: number | null; form?: UINode[]; createdAt: string; resolved: boolean; + anchorMessageId?: ID; blocked?: boolean; blockReason?: string; } export interface HITLResponse { @@ -126,7 +150,8 @@ export interface HITLResponse { // ---- UI 词汇 ---- export type UINodeType = - | 'section' | 'list' | 'kv' | 'form' | 'markdown' | 'doc-ref' | 'actions' | 'chart-lite'; + | 'section' | 'list' | 'kv' | 'form' | 'markdown' | 'doc-ref' | 'actions' | 'chart-lite' + | 'progress' | 'compare' | 'tree' | 'table' | 'steps' | 'tabs' | 'timeline'; export interface UINode { type: UINodeType; @@ -139,6 +164,25 @@ export interface UINode { actions?: { id: string; label: string; style?: 'primary' | 'default' | 'danger' }[]; chart?: { kind: 'bar' | 'line'; labels: string[]; series: number[] }; children?: UINode[]; + + progress?: { label?: string; value: number; tone?: string }; + compare?: { + left: { title?: string; tone?: string; nodes?: UINode[] }; + right: { title?: string; tone?: string; nodes?: UINode[] }; + }; + tree?: { + nodes: { + id: string; label: string; tone?: string; badge?: string; count?: number; + selected?: boolean; children?: unknown[]; + }[]; + }; + table?: { + columns: { key: string; label: string }[]; + rows: { id?: string; cells: [string, string][]; actions?: UINode['actions'] }[]; + }; + steps?: { current: number; steps: { label: string; status?: 'pending' | 'active' | 'done' | 'failed'; tone?: string }[] }; + tabs?: { active?: string; tabs: { id: string; label: string; nodes?: UINode[] }[] }; + timeline?: { items: { time?: string; title: string; tone?: string; markdown?: string; ref?: string }[] }; } export interface UIField { @@ -178,6 +222,7 @@ export interface PluginManifest { provides: { tools?: string[]; workflows?: string[]; intents?: string[]; prompts?: string[]; panels?: string[]; cards?: string[]; settings?: string[]; + commands?: string[]; workspaces?: string[]; }; } @@ -185,6 +230,11 @@ export interface PluginPanel { id: string; title: string; icon?: string; body: UINode[]; refreshTool?: string; } +export interface PluginWorkspace { + id: string; title: string; icon?: string; shortcut?: string; + badgeTool?: string; body: UINode[]; refreshTool?: string; +} + // ---- 摄取 / 检索 ---- export interface ParsedDocument { diff --git a/ui.go b/ui.go index 8cf574d..7296e70 100644 --- a/ui.go +++ b/ui.go @@ -15,6 +15,15 @@ const ( UIDocRef UINodeType = "doc-ref" // 工件引用(右栏查看器打开) UIActions UINodeType = "actions" // 可点动作(发命令/发消息) UIChartLite UINodeType = "chart-lite" + // —— 收件箱/工作区扩展词汇(提案 D28;graph 远期,追溯视图按"对象为中心 + + // 分组泳道 + 逐层展开"约束设计,禁止力导向裸图)—— + UIProgress UINodeType = "progress" // 进度条(摄取/生成/填充度) + UICompare UINodeType = "compare" // 左右对比(基线 vs 新内容、diff 确认) + UITree UINodeType = "tree" // 树(文档树/笔记树骨架;节点带状态灯/徽标/计数) + UITable UINodeType = "table" // 表格(大表/切片表;行级操作) + UISteps UINodeType = "steps" // 流程步骤条(变更七步等) + UITabs UINodeType = "tabs" // 页签容器(工作区多页签) + UITimeline UINodeType = "timeline" // 时间线(版本链/批注流/事件史) ) type UINode struct { @@ -28,6 +37,99 @@ type UINode struct { Actions []UIAction `json:"actions,omitempty"` Chart *UIChart `json:"chart,omitempty"` Children []UINode `json:"children,omitempty"` // section + + Progress *UIProgressSpec `json:"progress,omitempty"` // progress + Compare *UICompareSpec `json:"compare,omitempty"` // compare + Tree *UITreeSpec `json:"tree,omitempty"` // tree + Table *UITableSpec `json:"table,omitempty"` // table + Steps *UIStepsSpec `json:"steps,omitempty"` // steps + Tabs *UITabsSpec `json:"tabs,omitempty"` // tabs + Timeline *UITimelineSpec `json:"timeline,omitempty"` // timeline +} + +// UIProgress:0–1 归一进度或 n/m 计数(Value=Done/Total 任一形式,前端按 <1 判定)。 +type UIProgressSpec struct { + Label string `json:"label,omitempty"` + Value float64 `json:"value"` + Tone string `json:"tone,omitempty"` +} + +// UICompare:左右对比布局;两侧各带语义色调(如左=基线 danger、右=新内容 ok)。 +type UICompareSide struct { + Title string `json:"title,omitempty"` + Tone string `json:"tone,omitempty"` + Nodes []UINode `json:"nodes,omitempty"` +} + +type UICompareSpec struct { + Left UICompareSide `json:"left"` + Right UICompareSide `json:"right"` +} + +// UITreeNode:树节点;Badge 为角标文本(如"多挂载"),Count 为子孙计数, +// onSelect 语义由宿主按 Tree 用途路由(命令或工作区定位)。 +type UITreeNode struct { + ID string `json:"id"` + Label string `json:"label"` + Tone string `json:"tone,omitempty"` // 状态灯 + Badge string `json:"badge,omitempty"` + Count int `json:"count,omitempty"` + Selected bool `json:"selected,omitempty"` + Children []UITreeNode `json:"children,omitempty"` +} + +type UITreeSpec struct { + Nodes []UITreeNode `json:"nodes"` +} + +type UITableColumn struct { + Key string `json:"key"` + Label string `json:"label"` +} + +type UITableRow struct { + ID string `json:"id,omitempty"` + Cells [][2]string `json:"cells"` // 与 Columns.Key 对应的有序单元格 + Actions []UIAction `json:"actions,omitempty"` // 行级操作(走命令总线) +} + +type UITableSpec struct { + Columns []UITableColumn `json:"columns"` + Rows []UITableRow `json:"rows"` +} + +type UIStep struct { + Label string `json:"label"` + Status string `json:"status,omitempty"` // pending | active | done | failed + Tone string `json:"tone,omitempty"` +} + +type UIStepsSpec struct { + Current int `json:"current"` // 当前步下标(0 起) + Steps []UIStep `json:"steps"` +} + +type UITab struct { + ID string `json:"id"` + Label string `json:"label"` + Nodes []UINode `json:"nodes,omitempty"` +} + +type UITabsSpec struct { + Active string `json:"active,omitempty"` // 默认选中页签 ID + Tabs []UITab `json:"tabs"` +} + +type UITimelineItem struct { + Time string `json:"time,omitempty"` // 展示用时间文本 + Title string `json:"title"` + Tone string `json:"tone,omitempty"` + Markdown string `json:"markdown,omitempty"` // 条目正文 + Ref string `json:"ref,omitempty"` // 引用标识(切片/版本/操作者) +} + +type UITimelineSpec struct { + Items []UITimelineItem `json:"items"` } type UIField struct { @@ -42,9 +144,10 @@ type UIField struct { } type UIAction struct { - ID string `json:"id"` - Label string `json:"label"` - Style string `json:"style,omitempty"` // primary | default | danger + ID string `json:"id"` + Label string `json:"label"` + Style string `json:"style,omitempty"` // primary | default | danger + Command string `json:"command,omitempty"` // 绑定命令总线命令 id;空则降级为发消息 } type UIChart struct {