Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38fe861bca | ||
|
|
fa9108e2d1 | ||
|
|
5773e74086 | ||
|
|
d6f62af0ed | ||
|
|
0f94e4b062 | ||
|
|
aced665ba6 | ||
|
|
9150e6f90b | ||
|
|
8e4f4e0382 | ||
|
|
ba289e7b17 | ||
|
|
89f3906c39 | ||
|
|
bbebd153f4 | ||
|
|
a563d31b1e | ||
|
|
39632c2e7a | ||
|
|
37b96fd94f | ||
|
|
7a98a7e7fe | ||
|
|
360a8a8946 | ||
|
|
452daf3ffa |
+150
-20
@@ -1,45 +1,48 @@
|
||||
package core
|
||||
|
||||
import "encoding/json"
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 智能体画像(4.7):核心定制机制,全部数据化、可导入导出。
|
||||
type AgentProfile struct {
|
||||
ID string `json:"id"`
|
||||
Owner string `json:"owner,omitempty"` // 租户;空 = 共享(builtin)
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Builtin bool `json:"builtin,omitempty"`
|
||||
Source string `json:"source,omitempty"` // builtin | plugin | user
|
||||
SystemPrompt string `json:"systemPrompt"`
|
||||
ID string `json:"id"`
|
||||
Owner string `json:"owner,omitempty"` // 租户;空 = 共享(builtin)
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Builtin bool `json:"builtin,omitempty"`
|
||||
Source string `json:"source,omitempty"` // builtin | plugin | user
|
||||
SystemPrompt string `json:"systemPrompt"`
|
||||
// PromptModules 引用提示词库模块 ID(有序;空 = 组装默认基座全集)。
|
||||
// SystemPrompt 兼容保留:非空时作为附加段追加在模块组装结果之后。
|
||||
PromptModules []string `json:"promptModules,omitempty"`
|
||||
Model ModelRoute `json:"model"`
|
||||
Tools ToolPolicy `json:"tools"`
|
||||
Context ContextPolicy `json:"context"`
|
||||
HITL HITLPolicy `json:"hitl"`
|
||||
Budget RunBudget `json:"budget"`
|
||||
Model ModelRoute `json:"model"`
|
||||
Tools ToolPolicy `json:"tools"`
|
||||
Context ContextPolicy `json:"context"`
|
||||
HITL HITLPolicy `json:"hitl"`
|
||||
Budget RunBudget `json:"budget"`
|
||||
}
|
||||
|
||||
// ModelRoute 按用途选择模型(4.7 kind 路由):空串回落到 chat 默认。
|
||||
type ModelRoute struct {
|
||||
Chat string `json:"chat,omitempty"`
|
||||
Chat string `json:"chat,omitempty"`
|
||||
Lightweight string `json:"lightweight,omitempty"`
|
||||
Embedding string `json:"embedding,omitempty"`
|
||||
Rerank string `json:"rerank,omitempty"`
|
||||
Embedding string `json:"embedding,omitempty"`
|
||||
Rerank string `json:"rerank,omitempty"`
|
||||
}
|
||||
|
||||
// ToolPolicy 三清单:allow/deny 决定可见工具,confirm 强制执行前 HITL 确认。
|
||||
// 匹配语义:精确名或前缀通配("http.*" 表示 http 前缀);deny 优先于 allow。
|
||||
type ToolPolicy struct {
|
||||
Allow []string `json:"allow"` // ["*"] 全部
|
||||
Allow []string `json:"allow"` // ["*"] 全部
|
||||
Deny []string `json:"deny,omitempty"`
|
||||
Confirm []string `json:"confirm,omitempty"`
|
||||
}
|
||||
|
||||
// ContextPolicy(4.10):ctxasm 的输入;逐版本增加策略项不动组装器接口。
|
||||
type ContextPolicy struct {
|
||||
MaxHistoryMessages int `json:"maxHistoryMessages"` // 0 = 默认 40
|
||||
MaxHistoryMessages int `json:"maxHistoryMessages"` // 0 = 默认 40
|
||||
SummarizeBeyond bool `json:"summarizeBeyond,omitempty"` // 远期;先存标志
|
||||
MemoryBudgetPct int `json:"memoryBudgetPct,omitempty"` // 注入槽份额上限
|
||||
KnowledgeBudgetPct int `json:"knowledgeBudgetPct,omitempty"`
|
||||
@@ -102,11 +105,27 @@ const (
|
||||
type SettingEffect string
|
||||
|
||||
const (
|
||||
EffectHot SettingEffect = "hot" // 立即生效
|
||||
EffectNewRun SettingEffect = "new-run" // 仅影响新 run
|
||||
EffectHot SettingEffect = "hot" // 立即生效
|
||||
EffectNewRun SettingEffect = "new-run" // 仅影响新 run
|
||||
EffectRestart SettingEffect = "restart" // 需重启
|
||||
)
|
||||
|
||||
// SettingScope 声明设置项的写入归属(提案 2026-10-13-rdm-settings-members-duties-perms-increment P2)。
|
||||
// 空串保留旧个人配置兼容,不表示共享项目策略或系统管理资格。
|
||||
type SettingScope string
|
||||
|
||||
const (
|
||||
// SettingScopeSystem 受管项:仅系统管理面(admin)可写;user/project 写入被拒,
|
||||
// 生效链忽略这两层的存量覆盖值。
|
||||
SettingScopeSystem SettingScope = "system"
|
||||
// SettingScopeUser is a private account preference, never a shared project policy.
|
||||
SettingScopeUser SettingScope = "user"
|
||||
// SettingScopeProject is shared business configuration under project authority.
|
||||
SettingScopeProject SettingScope = "project"
|
||||
// SettingScopeProjectPersonal is a private preference within one project.
|
||||
SettingScopeProjectPersonal SettingScope = "project-personal"
|
||||
)
|
||||
|
||||
// SettingDesc 描述一个设置项;设置页按此渲染并标注生效时机(schema 驱动)。
|
||||
type SettingDesc struct {
|
||||
Key string `json:"key"`
|
||||
@@ -118,6 +137,117 @@ type SettingDesc struct {
|
||||
Options []string `json:"options,omitempty"` // enum
|
||||
Effect SettingEffect `json:"effect"`
|
||||
Secret bool `json:"secret,omitempty"`
|
||||
Scope SettingScope `json:"scope,omitempty"` // Empty is legacy personal compatibility; explicit scopes own their write surface.
|
||||
}
|
||||
|
||||
// ManagedPaths 返回 schema 中 scope=system 的键路径(点路径形式)。
|
||||
func ManagedPaths(descs []SettingDesc) []string {
|
||||
out := make([]string, 0, len(descs))
|
||||
for _, d := range descs {
|
||||
if d.Scope == SettingScopeSystem {
|
||||
out = append(out, d.Key)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// FlattenSettingLeafPaths 列出 values 的全部叶子点路径。子树为标量或数组时该子树
|
||||
// 根即叶子。用于受管触碰检测:深合并语义下叶子决定最终覆盖面。
|
||||
func FlattenSettingLeafPaths(values map[string]any) []string {
|
||||
var out []string
|
||||
var walk func(prefix string, m map[string]any)
|
||||
walk = func(prefix string, m map[string]any) {
|
||||
for k, v := range m {
|
||||
path := k
|
||||
if prefix != "" {
|
||||
path = prefix + "." + k
|
||||
}
|
||||
if vm, ok := v.(map[string]any); ok && len(vm) > 0 {
|
||||
walk(path, vm)
|
||||
continue
|
||||
}
|
||||
out = append(out, path)
|
||||
}
|
||||
}
|
||||
walk("", values)
|
||||
return out
|
||||
}
|
||||
|
||||
// pathTouchesManaged 判断 path 是否触碰 managed:等于受管路径、位于受管子树内
|
||||
// (受管路径是 path 的前缀),或整体覆盖受管子树(path 是受管路径的前缀——
|
||||
// 标量覆盖整棵受管 map 同样破坏受管值)。
|
||||
func pathTouchesManaged(path string, managed []string) bool {
|
||||
for _, mp := range managed {
|
||||
if path == mp ||
|
||||
strings.HasPrefix(path, mp+".") ||
|
||||
strings.HasPrefix(mp, path+".") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// TouchesManagedSettings 返回 values 中触碰受管路径的叶子;空切片 = 无触碰。
|
||||
// 调用方须传入已展开点路径的嵌套 map(normalize 后)。
|
||||
func TouchesManagedSettings(values map[string]any, managed []string) []string {
|
||||
var hits []string
|
||||
for _, p := range FlattenSettingLeafPaths(values) {
|
||||
if pathTouchesManaged(p, managed) {
|
||||
hits = append(hits, p)
|
||||
}
|
||||
}
|
||||
return hits
|
||||
}
|
||||
|
||||
// StripManagedSettings 返回删除受管路径后的浅拷贝(不改入参)。受管路径的祖先
|
||||
// 若因此只剩空 map 一并删除。入参须为嵌套形式(点路径已展开)。
|
||||
func StripManagedSettings(values map[string]any, managed []string) map[string]any {
|
||||
if len(values) == 0 || len(managed) == 0 {
|
||||
return values
|
||||
}
|
||||
// 叶子命中或祖先整体命中都删;逐叶子重建最简单可靠。
|
||||
leaves := FlattenSettingLeafPaths(values)
|
||||
keep := map[string]any{}
|
||||
for _, leaf := range leaves {
|
||||
if pathTouchesManaged(leaf, managed) {
|
||||
continue
|
||||
}
|
||||
setPath(keep, leaf, lookupAny(values, leaf))
|
||||
}
|
||||
return keep
|
||||
}
|
||||
|
||||
func setPath(m map[string]any, path string, val any) {
|
||||
parts := strings.Split(path, ".")
|
||||
cur := m
|
||||
for i, p := range parts {
|
||||
if i == len(parts)-1 {
|
||||
cur[p] = val
|
||||
return
|
||||
}
|
||||
next, ok := cur[p].(map[string]any)
|
||||
if !ok {
|
||||
next = map[string]any{}
|
||||
cur[p] = next
|
||||
}
|
||||
cur = next
|
||||
}
|
||||
}
|
||||
|
||||
func lookupAny(m map[string]any, path string) any {
|
||||
parts := strings.Split(path, ".")
|
||||
var cur any = m
|
||||
for _, p := range parts {
|
||||
cm, ok := cur.(map[string]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
cur, ok = cm[p]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return cur
|
||||
}
|
||||
|
||||
// EffectiveSettings 是覆盖链(system → user → project → conversation)深合并结果。
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestManagedPathsFiltersSystemScope(t *testing.T) {
|
||||
descs := []SettingDesc{
|
||||
{Key: "models.chat.model", Scope: SettingScopeSystem},
|
||||
{Key: "ui.theme"},
|
||||
{Key: "tools.tavily.apiKeys", Scope: SettingScopeSystem},
|
||||
}
|
||||
got := ManagedPaths(descs)
|
||||
sort.Strings(got)
|
||||
want := []string{"models.chat.model", "tools.tavily.apiKeys"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("ManagedPaths = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTouchesManagedSettings(t *testing.T) {
|
||||
managed := []string{"models.chat.model", "tools.tavily.apiKeys", "telemetry.otlpEndpoint"}
|
||||
cases := []struct {
|
||||
name string
|
||||
in map[string]any
|
||||
want []string
|
||||
}{
|
||||
{"exact leaf", map[string]any{"models": map[string]any{"chat": map[string]any{"model": "x"}}}, []string{"models.chat.model"}},
|
||||
{"sibling untouched", map[string]any{"models": map[string]any{"chat": map[string]any{"baseUrl": "x"}}}, nil},
|
||||
{"ancestor scalar overwrite", map[string]any{"models": "oops"}, []string{"models"}},
|
||||
{"subtree inside managed", map[string]any{"telemetry": map[string]any{"otlpEndpoint": "http://x"}}, []string{"telemetry.otlpEndpoint"}},
|
||||
{"other group untouched", map[string]any{"ui": map[string]any{"theme": "light"}}, nil},
|
||||
{"empty map", map[string]any{}, nil},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := TouchesManagedSettings(tc.in, managed)
|
||||
sort.Strings(got)
|
||||
if tc.want == nil && len(got) != 0 {
|
||||
t.Fatalf("TouchesManagedSettings = %v, want none", got)
|
||||
}
|
||||
if tc.want != nil && !reflect.DeepEqual(got, tc.want) {
|
||||
t.Fatalf("TouchesManagedSettings = %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStripManagedSettings(t *testing.T) {
|
||||
managed := []string{"models.chat.model", "models.chat.apiKey"}
|
||||
in := map[string]any{
|
||||
"models": map[string]any{
|
||||
"chat": map[string]any{"model": "x", "baseUrl": "https://b"},
|
||||
"lightweight": map[string]any{"model": "y"},
|
||||
},
|
||||
"ui": map[string]any{"theme": "dark"},
|
||||
}
|
||||
got := StripManagedSettings(in, managed)
|
||||
want := map[string]any{
|
||||
"models": map[string]any{
|
||||
"chat": map[string]any{"baseUrl": "https://b"},
|
||||
"lightweight": map[string]any{"model": "y"},
|
||||
},
|
||||
"ui": map[string]any{"theme": "dark"},
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("StripManagedSettings = %v, want %v", got, want)
|
||||
}
|
||||
if in["models"].(map[string]any)["chat"].(map[string]any)["model"] != "x" {
|
||||
t.Fatal("StripManagedSettings 不应修改入参")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStripManagedSettingsWholeSubtree(t *testing.T) {
|
||||
managed := []string{"models.chat.model", "models.chat.baseUrl", "models.chat.apiKey", "models.chat.fallbacks"}
|
||||
in := map[string]any{
|
||||
"models": map[string]any{"chat": map[string]any{"model": "x", "baseUrl": "b", "apiKey": "k"}},
|
||||
}
|
||||
got := StripManagedSettings(in, managed)
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("整棵受管子树应被剥除(含空父),got %v", got)
|
||||
}
|
||||
}
|
||||
+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")
|
||||
}
|
||||
}
|
||||
+49
-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][];
|
||||
@@ -265,6 +269,8 @@ export interface SettingDesc {
|
||||
default?: unknown; options?: string[];
|
||||
effect: 'hot' | 'new-run' | 'restart';
|
||||
secret?: boolean;
|
||||
/** Setting ownership; an omitted scope retains legacy personal compatibility. */
|
||||
scope?: 'system' | 'user' | 'project' | 'project-personal';
|
||||
}
|
||||
|
||||
// ---- 插件 ----
|
||||
@@ -351,3 +357,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