Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37b96fd94f | ||
|
|
7a98a7e7fe | ||
|
|
360a8a8946 | ||
|
|
452daf3ffa | ||
|
|
626f8e1062 | ||
|
|
a253a40b8e | ||
|
|
b8913dca6a | ||
|
|
1b7e2c0f47 | ||
|
|
8a3ff92518 | ||
|
|
f4798ac852 | ||
|
|
cf2615def6 | ||
|
|
b12f9e29e6 | ||
|
|
4eae71864c | ||
|
|
a7db6a4d0f |
+9
-2
@@ -2,5 +2,12 @@
|
||||
|
||||
- 用途:见 README。定位与边界见设计文档(Agent-System-Documents)。
|
||||
- 技术栈:Go(静态链接,CGO_ENABLED=0)
|
||||
- 构建/验证:见仓内 Makefile 或 Dev 仓顶层 Makefile(待 P1 起补充并保持本节同步)。
|
||||
- 状态:P0 骨架;当前阶段见 Dev 仓任务记录。
|
||||
|
||||
## 验证
|
||||
|
||||
在本仓根目录执行 `go test ./...` 验证 Go 契约与不变量;纯文档修改执行
|
||||
`git diff --check` 并核对受影响链接、命令和来源。本仓没有 Makefile。
|
||||
|
||||
修改 `ts/types.ts` 后在 Dev 仓运行 `make contracts` 同步消费者,再以 `make test`
|
||||
验证固定组件组合;单独运行 Core 测试不能代替提供方与消费者的兼容验证。
|
||||
本仓提供公共契约,不是独立部署服务;当前能力以源码和对应验证记录为准。
|
||||
|
||||
@@ -61,6 +61,9 @@ const (
|
||||
EvCardUpdated StreamEventType = "card.updated"
|
||||
EvCardStatus StreamEventType = "card.status"
|
||||
EvCardResolved StreamEventType = "card.resolved"
|
||||
// EvSurfaceUpdated:Generative UI Surface 快照保存后的投影事件(提案 U2;
|
||||
// 载荷 core.SurfaceSnapshot,按会话流发布,前端消费在 U4)。
|
||||
EvSurfaceUpdated StreamEventType = "surface.updated"
|
||||
)
|
||||
|
||||
// ProtocolVersion 是事件信封与 payload 词汇的当前版本。
|
||||
|
||||
@@ -208,6 +208,12 @@ type Usage struct {
|
||||
TotalTokens int `json:"totalTokens"`
|
||||
DurationMs float64 `json:"durationMs"`
|
||||
ToolCalls int `json:"toolCalls"`
|
||||
// 以下四项是 N3 的计费拆分(additive;旧记录为零值省略):
|
||||
// Last* 是最近一次请求的用量(上一请求),Cache* 是缓存命中累计。
|
||||
LastPromptTokens int `json:"lastPromptTokens,omitempty"`
|
||||
LastCompletionTokens int `json:"lastCompletionTokens,omitempty"`
|
||||
CacheReadTokens int `json:"cacheReadTokens,omitempty"`
|
||||
CacheWriteTokens int `json:"cacheWriteTokens,omitempty"`
|
||||
// Model 是实际给出这段用量的模型。空表示当时没有记录。
|
||||
Model string `json:"model,omitempty"`
|
||||
}
|
||||
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
package core
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Surface 契约(Generative UI 提案 U2;依据 r2「U1 结果与 r2 修订」):
|
||||
// 有版本的呈现产物信封 + 页面规格校验。信封与版本字段由服务端分配/校验,
|
||||
// 模型只产出 SurfaceSpec;节点级稳定 ID 与来源标注由 UINode 的可选字段承载
|
||||
// (ui.go),旧卡片/表单路径零影响。
|
||||
|
||||
const (
|
||||
// SurfaceSchemaVersion 是 Surface 信封/规格的结构版本。
|
||||
SurfaceSchemaVersion = 1
|
||||
// SurfaceCatalogVersion 是当前认可的组件目录版本(现有 16 类 UINode 词表)。
|
||||
SurfaceCatalogVersion = "ui-v1"
|
||||
// SurfaceMaxNodes / SurfaceMaxDepth / SurfaceMaxBytes 是 U1 试验采纳的资源
|
||||
// 预算(提案 KTD5):超限整页拒绝,不静默裁剪。
|
||||
SurfaceMaxNodes = 200
|
||||
SurfaceMaxDepth = 8
|
||||
SurfaceMaxBytes = 256 * 1024
|
||||
)
|
||||
|
||||
// SurfaceCompat 兼容标识(KTD3):由宿主与已安装组件协商记录,不采信自报值。
|
||||
type SurfaceCompat struct {
|
||||
SchemaVersion int `json:"schemaVersion"`
|
||||
CatalogVersion string `json:"catalogVersion"`
|
||||
PluginVersion string `json:"pluginVersion,omitempty"`
|
||||
}
|
||||
|
||||
// SurfaceIdentity 身份信封:全部由服务端分配或从当前请求绑定;提交方不能自报。
|
||||
type SurfaceIdentity struct {
|
||||
SurfaceID ID `json:"surfaceId"`
|
||||
Owner string `json:"owner"`
|
||||
ProjectID ID `json:"projectId,omitempty"`
|
||||
ConversationID ID `json:"conversationId,omitempty"`
|
||||
SourceRunID ID `json:"sourceRunId,omitempty"`
|
||||
PluginID string `json:"pluginId"`
|
||||
}
|
||||
|
||||
// SurfaceSpec 是模型可产出的页面规格(KTD3「页面」层)。
|
||||
type SurfaceSpec struct {
|
||||
Title string `json:"title"`
|
||||
Nodes []UINode `json:"nodes"`
|
||||
}
|
||||
|
||||
// SurfaceSnapshot 是已保存的呈现产物:信封+兼容+版本+页面+回放摘要。
|
||||
// Revision 单调递增;更新必须携带 BaseRevision,冲突由服务端拒绝(不自动覆盖)。
|
||||
type SurfaceSnapshot struct {
|
||||
Identity SurfaceIdentity `json:"identity"`
|
||||
Compat SurfaceCompat `json:"compat"`
|
||||
Revision int `json:"revision"`
|
||||
BaseRevision int `json:"baseRevision,omitempty"`
|
||||
ContextVersion string `json:"contextVersion,omitempty"`
|
||||
ActivationEpoch int64 `json:"activationEpoch"`
|
||||
Spec SurfaceSpec `json:"spec"`
|
||||
SemanticText string `json:"semanticText,omitempty"`
|
||||
}
|
||||
|
||||
// SurfaceMeta 是列表投影(不携带整页规格)。
|
||||
type SurfaceMeta struct {
|
||||
SurfaceID ID `json:"surfaceId"`
|
||||
Title string `json:"title"`
|
||||
PluginID string `json:"pluginId"`
|
||||
Revision int `json:"revision"`
|
||||
ConversationID ID `json:"conversationId,omitempty"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// ValidateSurfaceSpec 按目录纪律校验页面规格,返回违规清单(空即合法)。
|
||||
// knownSources 为 nil 表示本层不校验来源授权(U3 数据绑定层职责),仅做格式
|
||||
// 检查;非空时引用/doc-ref 未知来源即为违规。未知类型、非法 props、重复 ID、
|
||||
// 超限均拒绝(U1 试验校验器的 Go 版本:目录=提示=校验三同源)。
|
||||
func ValidateSurfaceSpec(spec SurfaceSpec, knownSources map[string]bool) []string {
|
||||
var errs []string
|
||||
seen := map[string]bool{}
|
||||
nodes := 0
|
||||
|
||||
var walk func(n UINode, path string, depth int)
|
||||
walk = func(n UINode, path string, depth int) {
|
||||
nodes++
|
||||
if nodes > SurfaceMaxNodes {
|
||||
errs = append(errs, fmt.Sprintf("%s: 节点数超过 %d", path, SurfaceMaxNodes))
|
||||
return
|
||||
}
|
||||
if depth > SurfaceMaxDepth {
|
||||
errs = append(errs, fmt.Sprintf("%s: 嵌套深度超过 %d", path, SurfaceMaxDepth))
|
||||
return
|
||||
}
|
||||
if n.ID != "" {
|
||||
if seen[n.ID] {
|
||||
errs = append(errs, fmt.Sprintf("%s: 重复节点 id %q", path, n.ID))
|
||||
}
|
||||
seen[n.ID] = true
|
||||
}
|
||||
for _, s := range n.Source {
|
||||
if s == "" {
|
||||
errs = append(errs, path+": source 含空标识")
|
||||
continue
|
||||
}
|
||||
if knownSources != nil && !knownSources[s] {
|
||||
errs = append(errs, fmt.Sprintf("%s: 引用未知来源 %q", path, s))
|
||||
}
|
||||
}
|
||||
switch n.Type {
|
||||
case UISection:
|
||||
for i, c := range n.Children {
|
||||
walk(c, fmt.Sprintf("%s/child[%d]", path, i), depth+1)
|
||||
}
|
||||
case UIList:
|
||||
if n.Items == nil {
|
||||
errs = append(errs, path+": list 需要 items")
|
||||
}
|
||||
case UIKV:
|
||||
if n.KV == nil {
|
||||
errs = append(errs, path+": kv 需要键值对数组")
|
||||
}
|
||||
case UIForm:
|
||||
if len(n.Fields) == 0 {
|
||||
errs = append(errs, path+": form.fields 需非空")
|
||||
}
|
||||
for _, f := range n.Fields {
|
||||
switch f.Type {
|
||||
case "text", "textarea", "number", "boolean", "select":
|
||||
default:
|
||||
errs = append(errs, fmt.Sprintf("%s: 字段 %q 非法 type %q", path, f.Key, f.Type))
|
||||
}
|
||||
if f.Type == "select" && len(f.Options) == 0 {
|
||||
errs = append(errs, fmt.Sprintf("%s: select 字段 %q 缺 options", path, f.Key))
|
||||
}
|
||||
}
|
||||
case UIMarkdown:
|
||||
if n.Markdown == "" {
|
||||
errs = append(errs, path+": markdown 需要正文")
|
||||
}
|
||||
case UIDocRef:
|
||||
if n.DocID == "" {
|
||||
errs = append(errs, path+": doc-ref 需要 docId")
|
||||
} else if knownSources != nil && !knownSources[string(n.DocID)] {
|
||||
errs = append(errs, fmt.Sprintf("%s: doc-ref 引用未知工件 %q", path, n.DocID))
|
||||
}
|
||||
case UIActions:
|
||||
if len(n.Actions) == 0 {
|
||||
errs = append(errs, path+": actions 需非空")
|
||||
}
|
||||
for _, a := range n.Actions {
|
||||
if a.ID == "" || a.Label == "" {
|
||||
errs = append(errs, fmt.Sprintf("%s: action 缺 id/label", path))
|
||||
}
|
||||
switch a.Style {
|
||||
case "", "primary", "default", "danger":
|
||||
default:
|
||||
errs = append(errs, fmt.Sprintf("%s: action %q 非法 style %q", path, a.ID, a.Style))
|
||||
}
|
||||
}
|
||||
case UIChartLite:
|
||||
if n.Chart == nil || (n.Chart.Kind != "bar" && n.Chart.Kind != "line") ||
|
||||
len(n.Chart.Labels) != len(n.Chart.Series) {
|
||||
errs = append(errs, path+": chart 需要 kind∈{bar,line} 且 labels/series 等长")
|
||||
}
|
||||
case UIAvatar:
|
||||
if n.Avatar == nil || n.Avatar.Name == "" {
|
||||
errs = append(errs, path+": avatar 需要 name")
|
||||
}
|
||||
case UIProgress:
|
||||
if n.Progress == nil || n.Progress.Value < 0 || n.Progress.Value > 1 {
|
||||
errs = append(errs, path+": progress.value 需在 0~1")
|
||||
}
|
||||
case UICompare:
|
||||
if n.Compare == nil {
|
||||
errs = append(errs, path+": compare 需要 left/right")
|
||||
} else {
|
||||
for side, sd := range map[string]UICompareSide{"left": n.Compare.Left, "right": n.Compare.Right} {
|
||||
for i, c := range sd.Nodes {
|
||||
walk(c, fmt.Sprintf("%s/%s[%d]", path, side, i), depth+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
case UITree:
|
||||
if n.Tree == nil || len(n.Tree.Nodes) == 0 {
|
||||
errs = append(errs, path+": tree.nodes 需非空")
|
||||
}
|
||||
case UITable:
|
||||
if n.Table == nil || len(n.Table.Columns) == 0 {
|
||||
errs = append(errs, path+": table.columns 需非空")
|
||||
} else {
|
||||
for i, r := range n.Table.Rows {
|
||||
if len(r.Cells) != len(n.Table.Columns) {
|
||||
errs = append(errs, fmt.Sprintf("%s: 行[%d] cells 与 columns 不等长", path, i))
|
||||
}
|
||||
}
|
||||
}
|
||||
case UISteps:
|
||||
if n.Steps == nil || len(n.Steps.Steps) == 0 || n.Steps.Current < 0 || n.Steps.Current >= len(n.Steps.Steps) {
|
||||
errs = append(errs, path+": steps 需非空且 current 在范围内")
|
||||
}
|
||||
case UITabs:
|
||||
if n.Tabs == nil || len(n.Tabs.Tabs) == 0 {
|
||||
errs = append(errs, path+": tabs.tabs 需非空")
|
||||
} else {
|
||||
for i, tb := range n.Tabs.Tabs {
|
||||
for j, c := range tb.Nodes {
|
||||
walk(c, fmt.Sprintf("%s/tab[%d][%d]", path, i, j), depth+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
case UITimeline:
|
||||
if n.Timeline == nil || len(n.Timeline.Items) == 0 {
|
||||
errs = append(errs, path+": timeline.items 需非空")
|
||||
}
|
||||
default:
|
||||
errs = append(errs, fmt.Sprintf("%s: 未知节点类型 %q", path, n.Type))
|
||||
}
|
||||
}
|
||||
|
||||
if spec.Title == "" {
|
||||
errs = append(errs, "$: title 必填")
|
||||
}
|
||||
if len(spec.Nodes) == 0 {
|
||||
errs = append(errs, "$: nodes 需非空")
|
||||
}
|
||||
for i, n := range spec.Nodes {
|
||||
walk(n, fmt.Sprintf("$[%d]", i), 1)
|
||||
}
|
||||
return errs
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func validSpec() SurfaceSpec {
|
||||
return SurfaceSpec{
|
||||
Title: "三家候选工具选型对比",
|
||||
Nodes: []UINode{
|
||||
{Type: UISection, ID: "n1", Title: "一、概览", Children: []UINode{
|
||||
{Type: UIMarkdown, ID: "n1a", Source: []string{"doc:eval/rc-01/vendor1"}, Markdown: "本期评估三家候选。"},
|
||||
}},
|
||||
{Type: UITable, ID: "n2", Source: []string{"doc:eval/rc-01/vendor1", "doc:eval/rc-01/vendor2"}, Table: &UITableSpec{
|
||||
Columns: []UITableColumn{{Key: "k", Label: "维度"}, {Key: "v", Label: "值"}},
|
||||
Rows: []UITableRow{{ID: "r1", Cells: [][2]string{{"k", "价格"}, {"v", "¥8/人/天"}}}},
|
||||
}},
|
||||
{Type: UISteps, ID: "n3", Steps: &UIStepsSpec{Current: 1, Steps: []UIStep{
|
||||
{Label: "需求冻结", Status: "done"}, {Label: "契约定义", Status: "active"},
|
||||
}}},
|
||||
{Type: UIForm, ID: "n4", Fields: []UIField{
|
||||
{Key: "years", Label: "归档年限", Type: "select", Options: []string{"3", "5"}, Required: true},
|
||||
}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSurfaceSpecValid(t *testing.T) {
|
||||
if errs := ValidateSurfaceSpec(validSpec(), nil); len(errs) != 0 {
|
||||
t.Fatalf("合法规格不应有违规: %v", errs)
|
||||
}
|
||||
// JSON 往返后仍合法(信封序列化面)。
|
||||
b, err := json.Marshal(validSpec())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var back SurfaceSpec
|
||||
if err := json.Unmarshal(b, &back); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if errs := ValidateSurfaceSpec(back, nil); len(errs) != 0 {
|
||||
t.Fatalf("JSON 往返后不应有违规: %v", errs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSurfaceSpecViolations(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
mut func(*SurfaceSpec)
|
||||
want string
|
||||
}{
|
||||
{"未知类型", func(s *SurfaceSpec) { s.Nodes[0].Type = " hologram" }, "未知节点类型"},
|
||||
{"重复节点 id", func(s *SurfaceSpec) { s.Nodes[1].ID = "n1" }, "重复节点 id"},
|
||||
{"表格行列不齐", func(s *SurfaceSpec) {
|
||||
s.Nodes[1].Table.Rows = []UITableRow{{Cells: [][2]string{{"k", "只有一列"}}}}
|
||||
}, "不等长"},
|
||||
{"select 缺 options", func(s *SurfaceSpec) {
|
||||
s.Nodes[3].Fields[0].Options = nil
|
||||
}, "缺 options"},
|
||||
{"steps 越界", func(s *SurfaceSpec) { s.Nodes[2].Steps.Current = 5 }, "current 在范围内"},
|
||||
{"空 title", func(s *SurfaceSpec) { s.Title = "" }, "title 必填"},
|
||||
{"空 nodes", func(s *SurfaceSpec) { s.Nodes = nil }, "nodes 需非空"},
|
||||
{"markdown 空", func(s *SurfaceSpec) {
|
||||
s.Nodes[0].Children[0].Markdown = ""
|
||||
}, "markdown 需要正文"},
|
||||
{"progress 越界", func(s *SurfaceSpec) {
|
||||
s.Nodes = append(s.Nodes, UINode{Type: UIProgress, Progress: &UIProgressSpec{Value: 1.5}})
|
||||
}, "progress.value"},
|
||||
{"未知来源", func(s *SurfaceSpec) {}, ""},
|
||||
}
|
||||
for _, c := range cases {
|
||||
s := validSpec()
|
||||
c.mut(&s)
|
||||
errs := ValidateSurfaceSpec(s, nil)
|
||||
if c.want == "" {
|
||||
continue
|
||||
}
|
||||
found := false
|
||||
for _, e := range errs {
|
||||
if strings.Contains(e, c.want) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("%s: 期望包含 %q,得到 %v", c.name, c.want, errs)
|
||||
}
|
||||
}
|
||||
// knownSources 非空时:未知来源拒绝、已知来源放行。
|
||||
s := validSpec()
|
||||
errs := ValidateSurfaceSpec(s, map[string]bool{"doc:eval/rc-01/vendor1": true, "doc:eval/rc-01/vendor2": true})
|
||||
if len(errs) != 0 {
|
||||
t.Fatalf("已知来源全引用不应违规: %v", errs)
|
||||
}
|
||||
s.Nodes[0].Children[0].Source = append(s.Nodes[0].Children[0].Source, "doc:eval/rc-99/ghost")
|
||||
errs = ValidateSurfaceSpec(s, map[string]bool{"doc:eval/rc-01/vendor1": true})
|
||||
if len(errs) == 0 {
|
||||
t.Fatal("引用未知来源应拒绝")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSurfaceSpecLimits(t *testing.T) {
|
||||
deep := UINode{Type: UISection, ID: "d0"}
|
||||
cur := &deep
|
||||
for i := 1; i <= SurfaceMaxDepth+1; i++ {
|
||||
cur.Children = []UINode{{Type: UISection, ID: "d" + string(rune('0'+i%10))}}
|
||||
cur = &cur.Children[0]
|
||||
}
|
||||
if errs := ValidateSurfaceSpec(SurfaceSpec{Title: "t", Nodes: []UINode{deep}}, nil); len(errs) == 0 {
|
||||
t.Fatal("超过嵌套深度应拒绝")
|
||||
}
|
||||
many := make([]UINode, SurfaceMaxNodes+1)
|
||||
for i := range many {
|
||||
many[i] = UINode{Type: UIMarkdown, Markdown: "x"}
|
||||
}
|
||||
if errs := ValidateSurfaceSpec(SurfaceSpec{Title: "t", Nodes: many}, nil); len(errs) == 0 {
|
||||
t.Fatal("超过节点数应拒绝")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUINodeBackwardCompat(t *testing.T) {
|
||||
// 旧卡片路径的 UINode JSON(无 id/source)解出后字段为零值,不受影响。
|
||||
var n UINode
|
||||
if err := json.Unmarshal([]byte(`{"type":"list","items":["a"]}`), &n); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n.ID != "" || n.Source != nil || n.Type != UIList || len(n.Items) != 1 {
|
||||
t.Fatalf("旧负载解引用异常: %+v", n)
|
||||
}
|
||||
// 新字段双向序列化。
|
||||
b, _ := json.Marshal(UINode{Type: UIMarkdown, ID: "m1", Source: []string{"s1"}, Markdown: "x"})
|
||||
if !strings.Contains(string(b), `"id":"m1"`) || !strings.Contains(string(b), `"source":["s1"]`) {
|
||||
t.Fatalf("新字段序列化缺失: %s", b)
|
||||
}
|
||||
}
|
||||
+51
@@ -59,6 +59,12 @@ export interface Usage {
|
||||
promptTokens: number; completionTokens: number; totalTokens: number;
|
||||
durationMs: number; toolCalls: number;
|
||||
model?: string;
|
||||
// N3 计费拆分(additive;旧记录缺省为 0):
|
||||
// Last* 是最近一次请求的用量,Cache* 是缓存命中累计。
|
||||
lastPromptTokens?: number;
|
||||
lastCompletionTokens?: number;
|
||||
cacheReadTokens?: number;
|
||||
cacheWriteTokens?: number;
|
||||
}
|
||||
|
||||
export interface Run {
|
||||
@@ -178,6 +184,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][];
|
||||
@@ -345,3 +354,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;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,12 @@ const (
|
||||
)
|
||||
|
||||
type UINode struct {
|
||||
Type UINodeType `json:"type"`
|
||||
Type UINodeType `json:"type"`
|
||||
// ID / Source 是 Generative UI Surface 层的节点级标注(提案 U2):ID 整页
|
||||
// 唯一且稳定(交互与修订对位);Source 为支撑该节点事实内容的来源标识,
|
||||
// 授权校验在数据绑定层(U3)。均为可选:旧卡片/表单路径不填不受影响。
|
||||
ID string `json:"id,omitempty"`
|
||||
Source []string `json:"source,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Items []string `json:"items,omitempty"` // list
|
||||
KV [][2]string `json:"kv,omitempty"` // kv
|
||||
|
||||
Reference in New Issue
Block a user