Compare commits
45
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba289e7b17 | ||
|
|
89f3906c39 | ||
|
|
bbebd153f4 | ||
|
|
a563d31b1e | ||
|
|
39632c2e7a | ||
|
|
37b96fd94f | ||
|
|
7a98a7e7fe | ||
|
|
360a8a8946 | ||
|
|
452daf3ffa | ||
|
|
626f8e1062 | ||
|
|
a253a40b8e | ||
|
|
b8913dca6a | ||
|
|
1b7e2c0f47 | ||
|
|
8a3ff92518 | ||
|
|
f4798ac852 | ||
|
|
cf2615def6 | ||
|
|
b12f9e29e6 | ||
|
|
4eae71864c | ||
|
|
0b5c7747a3 | ||
|
|
bd632236c2 | ||
|
|
a7db6a4d0f | ||
|
|
5dc190ee63 | ||
|
|
b7e5baf15c | ||
|
|
cf7df54874 | ||
|
|
88e481afcd | ||
|
|
7c66075ee9 | ||
|
|
3f7067fb2d | ||
|
|
f08a4a90c6 | ||
|
|
4d1123e45c | ||
|
|
f93f473a0e | ||
|
|
a8e81e078d | ||
|
|
9b8cf79c96 | ||
|
|
493a846f07 | ||
|
|
447ea5c5ae | ||
|
|
fdc0c1b933 | ||
|
|
b80407a9a6 | ||
|
|
d4375300cd | ||
|
|
1b04a34bbc | ||
|
|
c8266fef42 | ||
|
|
ed579bf29c | ||
|
|
f8b77c0d02 | ||
|
|
0b3392acea | ||
|
|
90b57477bb | ||
|
|
6bdc4f2ada | ||
|
|
cafd9a54e5 |
+9
-2
@@ -2,5 +2,12 @@
|
|||||||
|
|
||||||
- 用途:见 README。定位与边界见设计文档(Agent-System-Documents)。
|
- 用途:见 README。定位与边界见设计文档(Agent-System-Documents)。
|
||||||
- 技术栈:Go(静态链接,CGO_ENABLED=0)
|
- 技术栈: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 测试不能代替提供方与消费者的兼容验证。
|
||||||
|
本仓提供公共契约,不是独立部署服务;当前能力以源码和对应验证记录为准。
|
||||||
|
|||||||
+35
-1
@@ -42,7 +42,7 @@ func sampleMessage() Message {
|
|||||||
ID: "msg_abc123def456", ConversationID: "cnv_abc123def456", RunID: "run_abc123def456",
|
ID: "msg_abc123def456", ConversationID: "cnv_abc123def456", RunID: "run_abc123def456",
|
||||||
Role: RoleAssistant,
|
Role: RoleAssistant,
|
||||||
Parts: []Part{
|
Parts: []Part{
|
||||||
{Type: PartThinking, Text: "考虑先检索…", Process: true},
|
{Type: PartThinking, Text: "考虑先检索…", Process: true, DurationMs: 1500},
|
||||||
{Type: PartToolCall, Process: true, ToolCall: &ToolCallPart{ID: "call_1", Name: "history_search", Arguments: `{"q":"部署"}`, Duration: 12.5}},
|
{Type: PartToolCall, Process: true, ToolCall: &ToolCallPart{ID: "call_1", Name: "history_search", Arguments: `{"q":"部署"}`, Duration: 12.5}},
|
||||||
{Type: PartToolResult, Process: true, ToolResult: &ToolResultPart{CallID: "call_1", Status: "ok", Summary: "3 条命中"}},
|
{Type: PartToolResult, Process: true, ToolResult: &ToolResultPart{CallID: "call_1", Status: "ok", Summary: "3 条命中"}},
|
||||||
{Type: PartText, Text: "根据历史记录……"},
|
{Type: PartText, Text: "根据历史记录……"},
|
||||||
@@ -60,6 +60,14 @@ func sampleRun() Run {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGoldenPromptModule(t *testing.T) {
|
||||||
|
golden(t, "prompt_module_subagent", PromptModule{
|
||||||
|
ID: "agent.sub.explore", Layer: LayerAgent, Version: 1,
|
||||||
|
Title: "探索子代理", Body: "只读检索。",
|
||||||
|
Tools: []string{"knowledge_search", "web_search"}, Model: "lightweight", MaxTurns: 8,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestGoldenDomain(t *testing.T) {
|
func TestGoldenDomain(t *testing.T) {
|
||||||
golden(t, "message", sampleMessage())
|
golden(t, "message", sampleMessage())
|
||||||
golden(t, "run", sampleRun())
|
golden(t, "run", sampleRun())
|
||||||
@@ -125,6 +133,12 @@ func TestGoldenCardEvents(t *testing.T) {
|
|||||||
Payload: json.RawMessage(`{"cardId":"card_abc123def456","actionId":"escalate"}`)})
|
Payload: json.RawMessage(`{"cardId":"card_abc123def456","actionId":"escalate"}`)})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGoldenProjectStreamEvent(t *testing.T) {
|
||||||
|
// 项目流(D50):伪会话键 "project:<projectId>" 复用会话流机制;信封带 ProjectID。
|
||||||
|
golden(t, "event_project_stream", StreamEvent{Seq: 47, ConversationID: "project:prj_abc123def456", ProjectID: "prj_abc123def456", TS: "2026-09-30T11:00:00Z", V: 1, Type: EvCardCreated,
|
||||||
|
Payload: json.RawMessage(`{"card":{"id":"card_def456abc789","projectId":"prj_abc123def456","kind":"result","title":"项目级卡","status":"done","createdAt":"2026-09-30T11:00:00Z","updatedAt":"2026-09-30T11:00:00Z"}}`)})
|
||||||
|
}
|
||||||
|
|
||||||
func TestGoldenUIVocabularies(t *testing.T) {
|
func TestGoldenUIVocabularies(t *testing.T) {
|
||||||
golden(t, "ui_avatar", []UINode{
|
golden(t, "ui_avatar", []UINode{
|
||||||
{Type: UIAvatar, Avatar: &UIAvatarSpec{Name: "weichen", Sub: "OWNER"}},
|
{Type: UIAvatar, Avatar: &UIAvatarSpec{Name: "weichen", Sub: "OWNER"}},
|
||||||
@@ -193,6 +207,26 @@ func TestGoldenIngest(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGoldenWorkflowDef(t *testing.T) {
|
||||||
|
golden(t, "workflow_def", WorkflowDef{
|
||||||
|
ID: "demo.brief", Title: "简报", Description: "按主题写一份简报",
|
||||||
|
IntentID: "demo.brief", MaxRetries: 6,
|
||||||
|
Steps: []WorkflowStep{
|
||||||
|
{ID: "length", Title: "篇幅", Kind: "hitl", Hitl: &HITLCardTemplate{
|
||||||
|
Question: "篇幅?", Kind: HITLKindChoice, AllowFreeInput: false,
|
||||||
|
Choices: []HITLChoice{{ID: "short", Label: "短", Description: "一页", Impact: "更快"}},
|
||||||
|
}},
|
||||||
|
{ID: "draft", Title: "起草", Kind: "prompt", Agent: "research",
|
||||||
|
Prompt: "写一份关于 {{slots.topic}} 的简报。用户说:{{input}}。上一步:{{steps.length.output}}",
|
||||||
|
Critic: &StepCritic{Criteria: "必须点明主题", MaxRetries: 1}},
|
||||||
|
{ID: "lookup", Title: "查时间", Kind: "tool", Tool: "time_now", Args: json.RawMessage(`{"tz":"UTC"}`)},
|
||||||
|
{ID: "save", Title: "存稿", Kind: "artifact", Artifact: "brief.md", Prompt: "{{steps.draft.output}}"},
|
||||||
|
{ID: "note", Title: "通知", Kind: "card", Card: &ActivityCardTemplate{Kind: CardKindResult, Title: "简报已写好"}},
|
||||||
|
{ID: "run", Title: "脚本", Kind: "script", Script: "scripts/demo.py"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestGoldenPlugin(t *testing.T) {
|
func TestGoldenPlugin(t *testing.T) {
|
||||||
golden(t, "plugin_manifest", PluginManifest{
|
golden(t, "plugin_manifest", PluginManifest{
|
||||||
ID: "example", Name: "示例插件", Version: "0.1.0", APIVersion: PluginAPIVersion,
|
ID: "example", Name: "示例插件", Version: "0.1.0", APIVersion: PluginAPIVersion,
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import "encoding/json"
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
// 事件信封:后端 → 前端的唯一增量通道(SSE),持久化后转发,Seq 在会话内单调。
|
// 事件信封:后端 → 前端的唯一增量通道(SSE),持久化后转发,Seq 在会话内单调。
|
||||||
// 消费方对未知 Type 静默忽略(原则 6 前向兼容)。
|
// 消费方对未知 Type 静默忽略(原则 6 前向兼容)。
|
||||||
|
// 项目流(D50):项目频道事件以伪会话键 "project:<projectId>" 复用同一机制
|
||||||
|
// (独立 seq、持久化、断线重放),信封以 ProjectID 标识归属(会话流事件为空)。
|
||||||
type StreamEvent struct {
|
type StreamEvent struct {
|
||||||
Seq int64 `json:"seq"`
|
Seq int64 `json:"seq"`
|
||||||
RunID ID `json:"runId,omitempty"`
|
RunID ID `json:"runId,omitempty"`
|
||||||
ConversationID ID `json:"conversationId"`
|
ConversationID ID `json:"conversationId"`
|
||||||
TS string `json:"ts"` // RFC3339
|
ProjectID ID `json:"projectID,omitempty"` // 项目流事件的项目归属(D50;只增不删)
|
||||||
|
TS string `json:"ts"` // RFC3339
|
||||||
V int `json:"v"`
|
V int `json:"v"`
|
||||||
Type StreamEventType `json:"type"`
|
Type StreamEventType `json:"type"`
|
||||||
Payload json.RawMessage `json:"payload,omitempty"`
|
Payload json.RawMessage `json:"payload,omitempty"`
|
||||||
@@ -16,12 +22,28 @@ type StreamEvent struct {
|
|||||||
|
|
||||||
type StreamEventType string
|
type StreamEventType string
|
||||||
|
|
||||||
|
// 项目流频道约定(D50):项目频道以伪会话键 "project:<projectId>" 复用会话
|
||||||
|
// 事件的持久化/seq/重放机制;信封 ProjectID 由该键派生(发布与重放同源)。
|
||||||
|
const projectChannelPrefix = "project:"
|
||||||
|
|
||||||
|
// ProjectChannelKey 返回项目流的事件频道键。
|
||||||
|
func ProjectChannelKey(projectID ID) ID { return ID(projectChannelPrefix + string(projectID)) }
|
||||||
|
|
||||||
|
// ProjectFromChannel 反解项目流频道键;非项目流频道返回 false。
|
||||||
|
func ProjectFromChannel(key ID) (ID, bool) {
|
||||||
|
pid, ok := strings.CutPrefix(string(key), projectChannelPrefix)
|
||||||
|
return ID(pid), ok && pid != ""
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
EvMessageDelta StreamEventType = "message.delta"
|
EvMessageDelta StreamEventType = "message.delta"
|
||||||
EvThinkingDelta StreamEventType = "thinking.delta"
|
EvThinkingDelta StreamEventType = "thinking.delta"
|
||||||
|
EvToolCallDelta StreamEventType = "tool.call.delta" // 参数增量:工具开始执行前即可上屏
|
||||||
EvToolCall StreamEventType = "tool.call"
|
EvToolCall StreamEventType = "tool.call"
|
||||||
|
EvToolOutput StreamEventType = "tool.output" // 执行中的 stdout/stderr 增量
|
||||||
EvToolResult StreamEventType = "tool.result"
|
EvToolResult StreamEventType = "tool.result"
|
||||||
EvRunPhase StreamEventType = "run.phase"
|
EvRunPhase StreamEventType = "run.phase"
|
||||||
|
EvRunCheck StreamEventType = "run.check" // 终轮自动检查,过程区一行
|
||||||
EvHITLRequest StreamEventType = "hitl.request"
|
EvHITLRequest StreamEventType = "hitl.request"
|
||||||
EvHITLResolved StreamEventType = "hitl.resolved"
|
EvHITLResolved StreamEventType = "hitl.resolved"
|
||||||
EvPlanUpdate StreamEventType = "plan.update"
|
EvPlanUpdate StreamEventType = "plan.update"
|
||||||
@@ -39,6 +61,9 @@ const (
|
|||||||
EvCardUpdated StreamEventType = "card.updated"
|
EvCardUpdated StreamEventType = "card.updated"
|
||||||
EvCardStatus StreamEventType = "card.status"
|
EvCardStatus StreamEventType = "card.status"
|
||||||
EvCardResolved StreamEventType = "card.resolved"
|
EvCardResolved StreamEventType = "card.resolved"
|
||||||
|
// EvSurfaceUpdated:Generative UI Surface 快照保存后的投影事件(提案 U2;
|
||||||
|
// 载荷 core.SurfaceSnapshot,按会话流发布,前端消费在 U4)。
|
||||||
|
EvSurfaceUpdated StreamEventType = "surface.updated"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ProtocolVersion 是事件信封与 payload 词汇的当前版本。
|
// ProtocolVersion 是事件信封与 payload 词汇的当前版本。
|
||||||
@@ -63,17 +88,40 @@ type ToolCallPayload struct {
|
|||||||
Arguments string `json:"arguments,omitempty"`
|
Arguments string `json:"arguments,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToolCallDeltaPayload 是模型正在编写的工具参数增量。Index 标识同轮第几次调用
|
||||||
|
// (与供应商 tool_calls 下标一致);Name/CallID 往往只在首个分片出现。
|
||||||
|
type ToolCallDeltaPayload struct {
|
||||||
|
Index int `json:"index"`
|
||||||
|
CallID string `json:"callId,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Delta string `json:"delta"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToolOutputPayload 是工具执行过程中的输出增量。Stream 为 stdout 或 stderr。
|
||||||
|
type ToolOutputPayload struct {
|
||||||
|
CallID string `json:"callId"`
|
||||||
|
Stream string `json:"stream"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
}
|
||||||
|
|
||||||
type ToolResultPayload struct {
|
type ToolResultPayload struct {
|
||||||
CallID string `json:"callId"`
|
CallID string `json:"callId"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Summary string `json:"summary,omitempty"`
|
Summary string `json:"summary,omitempty"`
|
||||||
Truncated bool `json:"truncated,omitempty"`
|
Truncated bool `json:"truncated,omitempty"`
|
||||||
|
DurationMs float64 `json:"durationMs,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RunPhasePayload struct {
|
type RunPhasePayload struct {
|
||||||
Phase RunPhase `json:"phase"`
|
Phase RunPhase `json:"phase"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RunCheckPayload 与 CheckPart 同形,让运行中和重新加载后的检查行一致。
|
||||||
|
type RunCheckPayload struct {
|
||||||
|
Passed bool `json:"passed"`
|
||||||
|
Reason string `json:"reason,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type HITLResolvedPayload struct {
|
type HITLResolvedPayload struct {
|
||||||
CardID ID `json:"cardId"`
|
CardID ID `json:"cardId"`
|
||||||
Answer string `json:"answer,omitempty"`
|
Answer string `json:"answer,omitempty"`
|
||||||
@@ -96,15 +144,15 @@ type AgentSpawnPayload struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AgentProgressPayload struct {
|
type AgentProgressPayload struct {
|
||||||
AgentRun ID `json:"agentRun"`
|
AgentRun ID `json:"agentRun"`
|
||||||
Note string `json:"note,omitempty"`
|
Note string `json:"note,omitempty"`
|
||||||
Phase RunPhase `json:"phase,omitempty"`
|
Phase RunPhase `json:"phase,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentResultPayload struct {
|
type AgentResultPayload struct {
|
||||||
AgentRun ID `json:"agentRun"`
|
AgentRun ID `json:"agentRun"`
|
||||||
Status RunStatus `json:"status"`
|
Status RunStatus `json:"status"`
|
||||||
Summary string `json:"summary,omitempty"`
|
Summary string `json:"summary,omitempty"`
|
||||||
// Text 为子代理完整文本产出(squad 步骤交付物用;截断摘要见 Summary)。
|
// Text 为子代理完整文本产出(squad 步骤交付物用;截断摘要见 Summary)。
|
||||||
Text string `json:"text,omitempty"`
|
Text string `json:"text,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -119,11 +167,20 @@ type RunCompletedPayload struct {
|
|||||||
Context *ContextObservation `json:"context,omitempty"`
|
Context *ContextObservation `json:"context,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ContextSection 是组装结果里一段的估算。EstTokens 用 UTF-8 字节数 / 3.35,
|
||||||
|
// 再按上一请求的真实用量校准。
|
||||||
|
type ContextSection struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
EstTokens int `json:"estTokens"`
|
||||||
|
}
|
||||||
|
|
||||||
// ContextObservation 记录一次模型输入的实际构成与预算裁剪(ctxasm 产出)。
|
// ContextObservation 记录一次模型输入的实际构成与预算裁剪(ctxasm 产出)。
|
||||||
type ContextObservation struct {
|
type ContextObservation struct {
|
||||||
Items []string `json:"items"` // 如 system(画像) / history(12) / attachments(1) / knowledge(3)
|
Items []string `json:"items"` // 如 system(画像) / history(12) / attachments(1) / knowledge(3)
|
||||||
Truncations []string `json:"truncations,omitempty"` // 裁剪记录(历史超窗/工具结果超限/附件截断)
|
Truncations []string `json:"truncations,omitempty"` // 裁剪记录(历史超窗/工具结果超限/附件截断)
|
||||||
EstChars int `json:"estChars"` // 估算输入字符量
|
EstChars int `json:"estChars"` // 估算输入量(token;字段名保持兼容)
|
||||||
|
Sections []ContextSection `json:"sections,omitempty"`
|
||||||
|
PrefixHash string `json:"prefixHash,omitempty"` // 系统前缀哈希,前缀不变时保持稳定
|
||||||
}
|
}
|
||||||
|
|
||||||
type RunFailedPayload struct {
|
type RunFailedPayload struct {
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ type HITLCard struct {
|
|||||||
// Blocked:阻断态(门禁不通过等)——确认入口禁用并给出出路;阻断本身进审计。
|
// Blocked:阻断态(门禁不通过等)——确认入口禁用并给出出路;阻断本身进审计。
|
||||||
Blocked bool `json:"blocked,omitempty"`
|
Blocked bool `json:"blocked,omitempty"`
|
||||||
BlockReason string `json:"blockReason,omitempty"`
|
BlockReason string `json:"blockReason,omitempty"`
|
||||||
|
// Detail 是确认卡上的参数预览或其他补充说明。
|
||||||
|
Detail string `json:"detail,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HITLKind string
|
type HITLKind string
|
||||||
|
|||||||
@@ -17,26 +17,30 @@ type ID = string
|
|||||||
// Owner 是租户标识(多租户主线,设计 D5/D16):空串 = 单租户默认(嵌入式档/
|
// Owner 是租户标识(多租户主线,设计 D5/D16):空串 = 单租户默认(嵌入式档/
|
||||||
// 未启用认证时的隐式 owner)。API 层强制 owner 过滤,跨租户访问一律 NotFound。
|
// 未启用认证时的隐式 owner)。API 层强制 owner 过滤,跨租户访问一律 NotFound。
|
||||||
type Project struct {
|
type Project struct {
|
||||||
ID ID `json:"id"`
|
ID ID `json:"id"`
|
||||||
Owner ID `json:"owner,omitempty"`
|
Owner ID `json:"owner,omitempty"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
UpdatedAt time.Time `json:"updatedAt"`
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
Settings map[string]any `json:"settings,omitempty"` // 项目层设置覆盖(4.9 覆盖链)
|
Settings map[string]any `json:"settings,omitempty"` // 项目层设置覆盖(4.9 覆盖链)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Conversation struct {
|
type Conversation struct {
|
||||||
ID ID `json:"id"`
|
ID ID `json:"id"`
|
||||||
ProjectID ID `json:"projectId"`
|
ProjectID ID `json:"projectId"`
|
||||||
Owner ID `json:"owner,omitempty"` // 冗余自项目,查询过滤用
|
Owner ID `json:"owner,omitempty"` // 冗余自项目,查询过滤用
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
ProfileID ID `json:"profileId,omitempty"` // 空为默认画像
|
ProfileID ID `json:"profileId,omitempty"` // 空为默认画像
|
||||||
Pinned bool `json:"pinned,omitempty"` // 侧栏置顶(方案 A P6/P8:本端组织偏好,服务端权威)
|
Pinned bool `json:"pinned,omitempty"` // 侧栏置顶(方案 A P6/P8:本端组织偏好,服务端权威)
|
||||||
Archived bool `json:"archived,omitempty"` // 归档:列表默认隐藏,数据保留
|
Archived bool `json:"archived,omitempty"` // 归档:列表默认隐藏,数据保留
|
||||||
AllowCrossRead bool `json:"allowCrossRead,omitempty"` // 跨会话出站:本会话读取同项目其他会话记录(需项目开关 crossSession.read)
|
AllowCrossRead bool `json:"allowCrossRead,omitempty"` // 跨会话出站:本会话读取同项目其他会话记录(需项目开关 crossSession.read)
|
||||||
CrossPrivate bool `json:"crossPrivate,omitempty"` // 跨会话入站:私有会话,不被他读、不出现在他方列表
|
CrossPrivate bool `json:"crossPrivate,omitempty"` // 跨会话入站:私有会话,不被他读、不出现在他方列表
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
// PermissionMode 是会话权限:空或 default 沿用画像;plan 只读加计划工具;auto_edit 放行工作区写文件。
|
||||||
UpdatedAt time.Time `json:"updatedAt"`
|
PermissionMode string `json:"permissionMode,omitempty"`
|
||||||
|
// SessionNote 是本会话要求,组装时附在上下文末尾,不进入系统前缀。
|
||||||
|
SessionNote string `json:"sessionNote,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 消息与部件 ----
|
// ---- 消息与部件 ----
|
||||||
@@ -61,16 +65,22 @@ const (
|
|||||||
PartHITL PartType = "hitl"
|
PartHITL PartType = "hitl"
|
||||||
PartPlan PartType = "plan"
|
PartPlan PartType = "plan"
|
||||||
PartError PartType = "error"
|
PartError PartType = "error"
|
||||||
PartCard PartType = "card" // 插件自定义卡片(Data 为 UI schema)
|
PartCard PartType = "card" // 插件自定义卡片(Data 为 UI schema)
|
||||||
|
PartCheck PartType = "check" // 终轮自动检查,过程区一行,不是给用户的回复
|
||||||
)
|
)
|
||||||
|
|
||||||
type Message struct {
|
type Message struct {
|
||||||
ID ID `json:"id"`
|
ID ID `json:"id"`
|
||||||
ConversationID ID `json:"conversationId"`
|
ConversationID ID `json:"conversationId"`
|
||||||
RunID ID `json:"runId,omitempty"`
|
RunID ID `json:"runId,omitempty"`
|
||||||
Role Role `json:"role"`
|
Role Role `json:"role"`
|
||||||
Parts []Part `json:"parts"`
|
Parts []Part `json:"parts"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
// Excluded 为真时界面仍显示,但组装上下文时整条(及同一 run 的成组消息)不送入模型。
|
||||||
|
Excluded bool `json:"excluded,omitempty"`
|
||||||
|
// VariantGroup 把同一轮的候选回复绑在一起。只把未排除的那条送入模型。
|
||||||
|
VariantGroup string `json:"variantGroup,omitempty"`
|
||||||
|
VariantIndex int `json:"variantIndex,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Part 是判别联合:Type 决定哪些字段有意义;未用字段为零值。
|
// Part 是判别联合:Type 决定哪些字段有意义;未用字段为零值。
|
||||||
@@ -84,25 +94,30 @@ type Part struct {
|
|||||||
Artifact *ArtifactRef `json:"artifact,omitempty"`
|
Artifact *ArtifactRef `json:"artifact,omitempty"`
|
||||||
Hitl *HITLSummary `json:"hitl,omitempty"`
|
Hitl *HITLSummary `json:"hitl,omitempty"`
|
||||||
Plan *PlanSnapshot `json:"plan,omitempty"`
|
Plan *PlanSnapshot `json:"plan,omitempty"`
|
||||||
Card *CardPart `json:"card,omitempty"`
|
Card *CardPart `json:"card,omitempty"`
|
||||||
Err *ErrorPart `json:"error,omitempty"`
|
Err *ErrorPart `json:"error,omitempty"`
|
||||||
|
Check *CheckPart `json:"check,omitempty"`
|
||||||
|
|
||||||
// 过程标记:true 的部件在 run 终态后由前端折叠为摘要行(4.2)。
|
// 过程标记:true 的部件在 run 终态后由前端折叠为摘要行(4.2)。
|
||||||
Process bool `json:"process,omitempty"`
|
Process bool `json:"process,omitempty"`
|
||||||
|
|
||||||
|
// DurationMs 是 thinking 部件的思考时长(毫秒,从首个推理增量到最后一个)。
|
||||||
|
// 工具耗时在 ToolCallPart.Duration,不复用本字段。
|
||||||
|
DurationMs float64 `json:"durationMs,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ToolCallPart struct {
|
type ToolCallPart struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Arguments string `json:"arguments,omitempty"` // 原始 JSON
|
Arguments string `json:"arguments,omitempty"` // 原始 JSON
|
||||||
Duration float64 `json:"durationMs,omitempty"`
|
Duration float64 `json:"durationMs,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ToolResultPart struct {
|
type ToolResultPart struct {
|
||||||
CallID string `json:"callId"`
|
CallID string `json:"callId"`
|
||||||
Status string `json:"status"` // ok | error
|
Status string `json:"status"` // ok | error
|
||||||
Summary string `json:"summary,omitempty"`
|
Summary string `json:"summary,omitempty"`
|
||||||
Truncated bool `json:"truncated,omitempty"` // 超长截断并转工件引用(4.10)
|
Truncated bool `json:"truncated,omitempty"` // 超长截断并转工件引用(4.10)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ArtifactRef struct {
|
type ArtifactRef struct {
|
||||||
@@ -123,6 +138,12 @@ type ErrorPart struct {
|
|||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckPart 是终轮自动检查的结果。过程区显示一行,不进入给用户的回复。
|
||||||
|
type CheckPart struct {
|
||||||
|
Passed bool `json:"passed"`
|
||||||
|
Reason string `json:"reason,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// CardPart.Data 是 UI schema 节点(4.6 有限词汇)。
|
// CardPart.Data 是 UI schema 节点(4.6 有限词汇)。
|
||||||
// CardID 非空 = 收件箱 ActivityCard 的引用部件(如 submit_card 产卡:卡片为产物
|
// CardID 非空 = 收件箱 ActivityCard 的引用部件(如 submit_card 产卡:卡片为产物
|
||||||
// 权威载体,消息只存引用+锚点渲染);空 = 插件内嵌卡片部件(自包含 Data)。
|
// 权威载体,消息只存引用+锚点渲染);空 = 插件内嵌卡片部件(自包含 Data)。
|
||||||
@@ -145,39 +166,40 @@ const (
|
|||||||
type RunStatus string
|
type RunStatus string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
RunPending RunStatus = "pending"
|
RunPending RunStatus = "pending"
|
||||||
RunRunning RunStatus = "running"
|
RunRunning RunStatus = "running"
|
||||||
RunWaitingHITL RunStatus = "waiting_hitl"
|
RunWaitingHITL RunStatus = "waiting_hitl"
|
||||||
RunCompleted RunStatus = "completed"
|
RunCompleted RunStatus = "completed"
|
||||||
RunFailed RunStatus = "failed"
|
RunFailed RunStatus = "failed"
|
||||||
RunCancelled RunStatus = "cancelled"
|
RunCancelled RunStatus = "cancelled"
|
||||||
RunInterrupted RunStatus = "interrupted"
|
RunInterrupted RunStatus = "interrupted"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RunPhase string
|
type RunPhase string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
PhaseThinking RunPhase = "thinking"
|
PhaseThinking RunPhase = "thinking"
|
||||||
PhaseActing RunPhase = "acting"
|
PhaseActing RunPhase = "acting"
|
||||||
PhaseResponding RunPhase = "responding"
|
PhaseResponding RunPhase = "responding"
|
||||||
PhaseDone RunPhase = "done"
|
PhaseFinalizing RunPhase = "finalizing" // 工作循环已结束,正在整理给用户的最终回复
|
||||||
|
PhaseDone RunPhase = "done"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Run struct {
|
type Run struct {
|
||||||
ID ID `json:"id"`
|
ID ID `json:"id"`
|
||||||
ParentRunID ID `json:"parentRunId,omitempty"`
|
ParentRunID ID `json:"parentRunId,omitempty"`
|
||||||
ConversationID ID `json:"conversationId"`
|
ConversationID ID `json:"conversationId"`
|
||||||
ProjectID ID `json:"projectId"`
|
ProjectID ID `json:"projectId"`
|
||||||
Owner ID `json:"owner,omitempty"` // 冗余自会话
|
Owner ID `json:"owner,omitempty"` // 冗余自会话
|
||||||
Kind RunKind `json:"kind"`
|
Kind RunKind `json:"kind"`
|
||||||
ProfileID ID `json:"profileId,omitempty"`
|
ProfileID ID `json:"profileId,omitempty"`
|
||||||
Status RunStatus `json:"status"`
|
Status RunStatus `json:"status"`
|
||||||
Phase RunPhase `json:"phase,omitempty"`
|
Phase RunPhase `json:"phase,omitempty"`
|
||||||
AgentName string `json:"agentName,omitempty"` // subagent 显示名
|
AgentName string `json:"agentName,omitempty"` // subagent 显示名
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
EndedAt *time.Time `json:"endedAt,omitempty"`
|
EndedAt *time.Time `json:"endedAt,omitempty"`
|
||||||
Usage *Usage `json:"usage,omitempty"`
|
Usage *Usage `json:"usage,omitempty"`
|
||||||
Error *ErrorPart `json:"error,omitempty"`
|
Error *ErrorPart `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Usage struct {
|
type Usage struct {
|
||||||
@@ -186,6 +208,14 @@ type Usage struct {
|
|||||||
TotalTokens int `json:"totalTokens"`
|
TotalTokens int `json:"totalTokens"`
|
||||||
DurationMs float64 `json:"durationMs"`
|
DurationMs float64 `json:"durationMs"`
|
||||||
ToolCalls int `json:"toolCalls"`
|
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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 工件 ----
|
// ---- 工件 ----
|
||||||
@@ -206,20 +236,20 @@ type Artifact struct {
|
|||||||
// ---- 计划与待办(右栏 Plan / Todos)----
|
// ---- 计划与待办(右栏 Plan / Todos)----
|
||||||
|
|
||||||
type PlanSnapshot struct {
|
type PlanSnapshot struct {
|
||||||
PlanID ID `json:"planId"`
|
PlanID ID `json:"planId"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Steps []PlanStep `json:"steps"`
|
Steps []PlanStep `json:"steps"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PlanStep struct {
|
type PlanStep struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
Status string `json:"status"` // pending | in_progress | done | skipped
|
Status string `json:"status"` // pending | in_progress | done | skipped
|
||||||
AgentRun ID `json:"agentRun,omitempty"`
|
AgentRun ID `json:"agentRun,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TodoList struct {
|
type TodoList struct {
|
||||||
ConversationID ID `json:"conversationId"`
|
ConversationID ID `json:"conversationId"`
|
||||||
Items []TodoItem `json:"items"`
|
Items []TodoItem `json:"items"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,59 +8,79 @@ import "encoding/json"
|
|||||||
const PluginAPIVersion = "1.0"
|
const PluginAPIVersion = "1.0"
|
||||||
|
|
||||||
type PluginManifest struct {
|
type PluginManifest struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
APIVersion string `json:"apiVersion"`
|
APIVersion string `json:"apiVersion"`
|
||||||
Requires PluginRequires `json:"requires,omitempty"`
|
Requires PluginRequires `json:"requires,omitempty"`
|
||||||
Provides PluginProvides `json:"provides"`
|
Provides PluginProvides `json:"provides"`
|
||||||
|
Scope PluginScope `json:"scope,omitzero"`
|
||||||
|
Backend PluginBackend `json:"backend,omitzero"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PluginRequires struct {
|
type PluginRequires struct {
|
||||||
Network []string `json:"network,omitempty"` // 允许访问的域名(展示用,v1 自装自审)
|
Network []string `json:"network,omitempty"` // 出站域名白名单;v2 空名单拒绝出站,v1 空名单保持放行
|
||||||
}
|
}
|
||||||
|
|
||||||
type PluginProvides struct {
|
type PluginProvides struct {
|
||||||
Tools []string `json:"tools,omitempty"` // tools/ 目录下的资产名
|
Tools []string `json:"tools,omitempty"` // tools/ 目录下的资产名
|
||||||
Workflows []string `json:"workflows,omitempty"`
|
Workflows []string `json:"workflows,omitempty"`
|
||||||
Intents []string `json:"intents,omitempty"`
|
Intents []string `json:"intents,omitempty"`
|
||||||
Prompts []string `json:"prompts,omitempty"`
|
Prompts []string `json:"prompts,omitempty"`
|
||||||
Panels []string `json:"panels,omitempty"`
|
Panels []string `json:"panels,omitempty"`
|
||||||
Cards []string `json:"cards,omitempty"`
|
Cards []string `json:"cards,omitempty"`
|
||||||
Settings []string `json:"settings,omitempty"`
|
Settings []string `json:"settings,omitempty"`
|
||||||
Commands []string `json:"commands,omitempty"` // commands/ 资产名(spec 权威在 Agent-Command 库)
|
Commands []string `json:"commands,omitempty"` // commands/ 资产名(spec 权威在 Agent-Command 库)
|
||||||
Workspaces []string `json:"workspaces,omitempty"` // workspaces/ 资产名(工作区弹层)
|
Workspaces []string `json:"workspaces,omitempty"` // workspaces/ 资产名(工作区弹层)
|
||||||
|
Skills []string `json:"skills,omitempty"`
|
||||||
|
MCPServers []string `json:"mcpServers,omitempty"`
|
||||||
|
Knowledge []string `json:"knowledge,omitempty"`
|
||||||
|
Parsers []string `json:"parsers,omitempty"`
|
||||||
|
Agents []string `json:"agents,omitempty"`
|
||||||
|
Channels []string `json:"channels,omitempty"`
|
||||||
|
Pages []string `json:"pages,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PluginPanel:右栏面板声明(UI schema 静态骨架 + 可选数据提供者)。
|
// PluginPanel:右栏面板声明(UI schema 静态骨架 + 可选数据提供者)。
|
||||||
type PluginPanel struct {
|
type PluginPanel struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Icon string `json:"icon,omitempty"`
|
Icon string `json:"icon,omitempty"`
|
||||||
Body []UINode `json:"body"`
|
Body []UINode `json:"body"`
|
||||||
// Tool 绑定:面板挂载时调用该工具刷新 Body 中的动态区(v1 简化为整体刷新)。
|
// Tool 绑定:面板挂载时调用该工具刷新 Body 中的动态区(v1 简化为整体刷新)。
|
||||||
RefreshTool string `json:"refreshTool,omitempty"`
|
RefreshTool string `json:"refreshTool,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// WorkflowDef:工作流即数据(4.6)。Step 可产 HITL 卡片、调工具、产出工件。
|
// WorkflowDef:工作流即数据(4.6)。Step 可产 HITL 卡片、调工具、产出工件。
|
||||||
|
// 模板占位符:{{input}}、{{slots.<key>}}、{{steps.<id>.output}}。
|
||||||
type WorkflowDef struct {
|
type WorkflowDef struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Description string `json:"description,omitempty"`
|
Description string `json:"description,omitempty"`
|
||||||
IntentID string `json:"intentId,omitempty"` // 绑定意图(命中即启动)
|
IntentID string `json:"intentId,omitempty"` // 绑定意图(命中即启动)
|
||||||
Steps []WorkflowStep `json:"steps"`
|
MaxRetries int `json:"maxRetries,omitempty"` // 整条工作流的 critic 重试上限
|
||||||
|
Steps []WorkflowStep `json:"steps"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StepCritic:单步验收。无法解析的判定按不通过处理。
|
||||||
|
type StepCritic struct {
|
||||||
|
Criteria string `json:"criteria"`
|
||||||
|
MaxRetries int `json:"maxRetries,omitempty"` // 本步重试上限;0 表示缺省 1 次
|
||||||
}
|
}
|
||||||
|
|
||||||
type WorkflowStep struct {
|
type WorkflowStep struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Kind string `json:"kind"` // prompt | tool | hitl | artifact | card
|
Kind string `json:"kind"` // prompt | tool | hitl | artifact | card | script
|
||||||
Prompt string `json:"prompt,omitempty"` // prompt: 发给模型的指令模板
|
Prompt string `json:"prompt,omitempty"` // prompt: 发给模型的指令模板
|
||||||
Tool string `json:"tool,omitempty"` // tool: 工具名
|
Agent string `json:"agent,omitempty"` // prompt: 子代理 profile,缺省 general
|
||||||
Args json.RawMessage `json:"args,omitempty"` // tool: 参数模板
|
Tool string `json:"tool,omitempty"` // tool: 工具名
|
||||||
Hitl *HITLCardTemplate `json:"hitl,omitempty"` // hitl: 卡片模板
|
Args json.RawMessage `json:"args,omitempty"` // tool: 参数模板
|
||||||
Artifact string `json:"artifact,omitempty"` // artifact: 产出工件名模板
|
Hitl *HITLCardTemplate `json:"hitl,omitempty"` // hitl: 卡片模板
|
||||||
Card *ActivityCardTemplate `json:"card,omitempty"` // card: 产收件箱任务卡
|
Artifact string `json:"artifact,omitempty"` // artifact: 产出工件名模板
|
||||||
|
Card *ActivityCardTemplate `json:"card,omitempty"` // card: 产收件箱任务卡
|
||||||
|
Script string `json:"script,omitempty"` // script: 插件目录内的相对路径
|
||||||
|
Critic *StepCritic `json:"critic,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ActivityCardTemplate:工作流步骤产卡模板(kind=card)——步骤执行时生成
|
// ActivityCardTemplate:工作流步骤产卡模板(kind=card)——步骤执行时生成
|
||||||
@@ -76,20 +96,20 @@ type ActivityCardTemplate struct {
|
|||||||
// PluginWorkspace:工作区弹层声明(深读与治理视图;Esc/遮罩关闭回对话现场,
|
// PluginWorkspace:工作区弹层声明(深读与治理视图;Esc/遮罩关闭回对话现场,
|
||||||
// 不承载任何唯一步骤——每个操作必须另有命令路径,提案 D31)。
|
// 不承载任何唯一步骤——每个操作必须另有命令路径,提案 D31)。
|
||||||
type PluginWorkspace struct {
|
type PluginWorkspace struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Icon string `json:"icon,omitempty"`
|
Icon string `json:"icon,omitempty"`
|
||||||
Shortcut string `json:"shortcut,omitempty"` // "1"–"9"(⌘N / Ctrl+N 召出)
|
Shortcut string `json:"shortcut,omitempty"` // "1"–"9"(⌘N / Ctrl+N 召出)
|
||||||
BadgeTool string `json:"badgeTool,omitempty"` // 角标提供者工具:返回 {count,tone}
|
BadgeTool string `json:"badgeTool,omitempty"` // 角标提供者工具:返回 {count,tone}
|
||||||
Body []UINode `json:"body"`
|
Body []UINode `json:"body"`
|
||||||
// RefreshTool:挂载时调用刷新 Body 动态区(与 Panel 同机制,v1 整体刷新)。
|
// RefreshTool:挂载时调用刷新 Body 动态区(与 Panel 同机制,v1 整体刷新)。
|
||||||
RefreshTool string `json:"refreshTool,omitempty"`
|
RefreshTool string `json:"refreshTool,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HITLCardTemplate struct {
|
type HITLCardTemplate struct {
|
||||||
Question string `json:"question"`
|
Question string `json:"question"`
|
||||||
Kind HITLKind `json:"kind"`
|
Kind HITLKind `json:"kind"`
|
||||||
Choices []HITLChoice `json:"choices,omitempty"`
|
Choices []HITLChoice `json:"choices,omitempty"`
|
||||||
AllowFreeInput bool `json:"allowFreeInput"`
|
AllowFreeInput bool `json:"allowFreeInput"`
|
||||||
FreeInputHint string `json:"freeInputHint,omitempty"`
|
FreeInputHint string `json:"freeInputHint,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
+100
@@ -0,0 +1,100 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
// PluginAPIVersionExperimental 是插件契约 v2 的实验窗口。
|
||||||
|
// 1.0 清单继续有效;v2 只增加可选字段,不收紧 1.0 已有资产。
|
||||||
|
const PluginAPIVersionExperimental = "2.0-experimental"
|
||||||
|
|
||||||
|
// PluginAPIAccepted 报告宿主是否接受该 apiVersion。空值由宿主填成 PluginAPIVersion。
|
||||||
|
func PluginAPIAccepted(v string) bool {
|
||||||
|
switch v {
|
||||||
|
case PluginAPIVersion, PluginAPIVersionExperimental:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginScope 限制插件对哪些租户/项目可见。两者都空 = 全部可见。
|
||||||
|
type PluginScope struct {
|
||||||
|
Tenants []string `json:"tenants,omitempty"`
|
||||||
|
Projects []string `json:"projects,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allows 判断 owner/project 是否落在作用域内。
|
||||||
|
func (s PluginScope) Allows(owner, project string) bool {
|
||||||
|
if len(s.Tenants) > 0 && !scopeContains(s.Tenants, owner) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(s.Projects) > 0 && !scopeContains(s.Projects, project) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func scopeContains(xs []string, v string) bool {
|
||||||
|
for _, x := range xs {
|
||||||
|
if x == v {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginBackend 声明进程内或 sidecar 代码档。SchemaVersion 是插件自有数据的迁移版本。
|
||||||
|
type PluginBackend struct {
|
||||||
|
Mode string `json:"mode,omitempty"` // inproc | sidecar
|
||||||
|
Command string `json:"command,omitempty"`
|
||||||
|
Args []string `json:"args,omitempty"`
|
||||||
|
SchemaVersion int `json:"schemaVersion,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginSkill 是 skills/<name>.md 的装载结果(SKILL.md 正文)。
|
||||||
|
type PluginSkill struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginMCPServer 是 mcpServers/<name>.json,字段与 agent-native MCP ServerConfig 对齐。
|
||||||
|
type PluginMCPServer struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Command string `json:"command,omitempty"`
|
||||||
|
Args []string `json:"args,omitempty"`
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginKnowledgeSet 是知识数据集声明(数据在插件侧,检索引擎在基座)。
|
||||||
|
type PluginKnowledgeSet struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginParserDecl 是解析器声明。实现可以是内置名或 sidecar 工具名。
|
||||||
|
type PluginParserDecl struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
MIME string `json:"mime,omitempty"`
|
||||||
|
Tool string `json:"tool,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginAgentDecl 是画像/子代理声明。
|
||||||
|
type PluginAgentDecl struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Instructions string `json:"instructions,omitempty"`
|
||||||
|
Tools []string `json:"tools,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginChannelDecl 是渠道适配器声明(飞书等由下游实现,基座只登记入口)。
|
||||||
|
type PluginChannelDecl struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginPageDecl 是前端 ESM 页面。Module 相对插件目录或绝对 URL。
|
||||||
|
type PluginPageDecl struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Module string `json:"module"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestPluginAPIAccepted(t *testing.T) {
|
||||||
|
if !PluginAPIAccepted(PluginAPIVersion) || !PluginAPIAccepted(PluginAPIVersionExperimental) {
|
||||||
|
t.Fatal("current and experimental windows must be accepted")
|
||||||
|
}
|
||||||
|
if PluginAPIAccepted("0.9") || PluginAPIAccepted("") {
|
||||||
|
t.Fatal("unknown or empty apiVersion must be rejected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPluginScopeAllows(t *testing.T) {
|
||||||
|
open := PluginScope{}
|
||||||
|
if !open.Allows("a", "p") {
|
||||||
|
t.Fatal("empty scope is visible to everyone")
|
||||||
|
}
|
||||||
|
scoped := PluginScope{Tenants: []string{"acme"}, Projects: []string{"p1"}}
|
||||||
|
if !scoped.Allows("acme", "p1") {
|
||||||
|
t.Fatal("matching tenant and project should pass")
|
||||||
|
}
|
||||||
|
if scoped.Allows("other", "p1") || scoped.Allows("acme", "p2") {
|
||||||
|
t.Fatal("scope must reject mismatches")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,10 @@ type PromptModule struct {
|
|||||||
BasedOnVersion int `json:"basedOnVersion,omitempty"` // 用户副本基于的内置版本
|
BasedOnVersion int `json:"basedOnVersion,omitempty"` // 用户副本基于的内置版本
|
||||||
Drifted bool `json:"drifted,omitempty"` // 用户副本落后于当前内置版本(提示用,不阻断)
|
Drifted bool `json:"drifted,omitempty"` // 用户副本落后于当前内置版本(提示用,不阻断)
|
||||||
Disabled bool `json:"disabled,omitempty"` // 用户停用
|
Disabled bool `json:"disabled,omitempty"` // 用户停用
|
||||||
|
// 子代理 profile。只对 agent.sub.* 有意义;空字段表示沿用内置。
|
||||||
|
Tools []string `json:"tools,omitempty"`
|
||||||
|
Model string `json:"model,omitempty"` // lightweight | chat
|
||||||
|
MaxTurns int `json:"maxTurns,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PromptTrigger 触发条件;模块命中 = 任一 trigger 命中(再过 RequiresAttachments)。
|
// PromptTrigger 触发条件;模块命中 = 任一 trigger 命中(再过 RequiresAttachments)。
|
||||||
|
|||||||
+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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+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")
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"seq": 47,
|
||||||
|
"conversationId": "project:prj_abc123def456",
|
||||||
|
"projectID": "prj_abc123def456",
|
||||||
|
"ts": "2026-09-30T11:00:00Z",
|
||||||
|
"v": 1,
|
||||||
|
"type": "card.created",
|
||||||
|
"payload": {
|
||||||
|
"card": {
|
||||||
|
"id": "card_def456abc789",
|
||||||
|
"projectId": "prj_abc123def456",
|
||||||
|
"kind": "result",
|
||||||
|
"title": "项目级卡",
|
||||||
|
"status": "done",
|
||||||
|
"createdAt": "2026-09-30T11:00:00Z",
|
||||||
|
"updatedAt": "2026-09-30T11:00:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+2
-1
@@ -7,7 +7,8 @@
|
|||||||
{
|
{
|
||||||
"type": "thinking",
|
"type": "thinking",
|
||||||
"text": "考虑先检索…",
|
"text": "考虑先检索…",
|
||||||
"process": true
|
"process": true,
|
||||||
|
"durationMs": 1500
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "tool_call",
|
"type": "tool_call",
|
||||||
|
|||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"id": "agent.sub.explore",
|
||||||
|
"layer": "agent",
|
||||||
|
"version": 1,
|
||||||
|
"title": "探索子代理",
|
||||||
|
"description": "",
|
||||||
|
"body": "只读检索。",
|
||||||
|
"tools": [
|
||||||
|
"knowledge_search",
|
||||||
|
"web_search"
|
||||||
|
],
|
||||||
|
"model": "lightweight",
|
||||||
|
"maxTurns": 8
|
||||||
|
}
|
||||||
Vendored
+69
@@ -0,0 +1,69 @@
|
|||||||
|
{
|
||||||
|
"id": "demo.brief",
|
||||||
|
"title": "简报",
|
||||||
|
"description": "按主题写一份简报",
|
||||||
|
"intentId": "demo.brief",
|
||||||
|
"maxRetries": 6,
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"id": "length",
|
||||||
|
"title": "篇幅",
|
||||||
|
"kind": "hitl",
|
||||||
|
"hitl": {
|
||||||
|
"question": "篇幅?",
|
||||||
|
"kind": "choice",
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"id": "short",
|
||||||
|
"label": "短",
|
||||||
|
"description": "一页",
|
||||||
|
"impact": "更快"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"allowFreeInput": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "draft",
|
||||||
|
"title": "起草",
|
||||||
|
"kind": "prompt",
|
||||||
|
"prompt": "写一份关于 {{slots.topic}} 的简报。用户说:{{input}}。上一步:{{steps.length.output}}",
|
||||||
|
"agent": "research",
|
||||||
|
"critic": {
|
||||||
|
"criteria": "必须点明主题",
|
||||||
|
"maxRetries": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "lookup",
|
||||||
|
"title": "查时间",
|
||||||
|
"kind": "tool",
|
||||||
|
"tool": "time_now",
|
||||||
|
"args": {
|
||||||
|
"tz": "UTC"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "save",
|
||||||
|
"title": "存稿",
|
||||||
|
"kind": "artifact",
|
||||||
|
"prompt": "{{steps.draft.output}}",
|
||||||
|
"artifact": "brief.md"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "note",
|
||||||
|
"title": "通知",
|
||||||
|
"kind": "card",
|
||||||
|
"card": {
|
||||||
|
"kind": "result",
|
||||||
|
"title": "简报已写好"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "run",
|
||||||
|
"title": "脚本",
|
||||||
|
"kind": "script",
|
||||||
|
"script": "scripts/demo.py"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -20,16 +20,23 @@ type ToolDesc struct {
|
|||||||
Source SourceKind `json:"source"`
|
Source SourceKind `json:"source"`
|
||||||
PluginID string `json:"pluginId,omitempty"`
|
PluginID string `json:"pluginId,omitempty"`
|
||||||
HTTP *HTTPToolSpec `json:"http,omitempty"` // 声明式 HTTP 工具(无代码)
|
HTTP *HTTPToolSpec `json:"http,omitempty"` // 声明式 HTTP 工具(无代码)
|
||||||
|
// Hidden 预留能力形态:默认不对模型与 /api/tools 列表可见,须运行时开关
|
||||||
|
// (如 runtime.fsTools)或管理动作开启后可用(提案 2026-09-30-file-preview-and-reserved-capabilities)。
|
||||||
|
Hidden bool `json:"hidden,omitempty"`
|
||||||
|
// ReadOnly、Edits、Risk 是工具注册时的权限声明。Risk 取 low、medium、high。
|
||||||
|
ReadOnly bool `json:"readOnly,omitempty"`
|
||||||
|
Edits bool `json:"edits,omitempty"`
|
||||||
|
Risk string `json:"risk,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTPToolSpec:无代码自定义工具(4.8)。模板变量 {{param.x}} 从调用参数注入。
|
// HTTPToolSpec:无代码自定义工具(4.8)。模板变量 {{param.x}} 从调用参数注入。
|
||||||
type HTTPToolSpec struct {
|
type HTTPToolSpec struct {
|
||||||
Method string `json:"method"`
|
Method string `json:"method"`
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
Headers map[string]string `json:"headers,omitempty"`
|
Headers map[string]string `json:"headers,omitempty"`
|
||||||
BodyTemplate string `json:"bodyTemplate,omitempty"` // JSON 模板
|
BodyTemplate string `json:"bodyTemplate,omitempty"` // JSON 模板
|
||||||
AuthRef string `json:"authRef,omitempty"` // 设置中的密钥引用(settings key)
|
AuthRef string `json:"authRef,omitempty"` // 设置中的密钥引用(settings key)
|
||||||
TimeoutMs int `json:"timeoutMs,omitempty"`
|
TimeoutMs int `json:"timeoutMs,omitempty"`
|
||||||
// ResultPath 从响应 JSON 中裁剪(jq 点路径);空取整个响应。
|
// ResultPath 从响应 JSON 中裁剪(jq 点路径);空取整个响应。
|
||||||
ResultPath string `json:"resultPath,omitempty"`
|
ResultPath string `json:"resultPath,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -41,9 +48,9 @@ type ToolCallRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ToolCallResult struct {
|
type ToolCallResult struct {
|
||||||
CallID string `json:"callId"`
|
CallID string `json:"callId"`
|
||||||
Status string `json:"status"` // ok | error
|
Status string `json:"status"` // ok | error
|
||||||
Output string `json:"output,omitempty"`
|
Output string `json:"output,omitempty"`
|
||||||
Truncated bool `json:"truncated,omitempty"`
|
Truncated bool `json:"truncated,omitempty"`
|
||||||
Artifact *Artifact `json:"artifact,omitempty"` // 超长转工件
|
Artifact *Artifact `json:"artifact,omitempty"` // 超长转工件
|
||||||
}
|
}
|
||||||
|
|||||||
+118
-5
@@ -15,6 +15,8 @@ export interface Conversation {
|
|||||||
id: ID; projectId: ID; owner?: ID; title: string; profileId?: ID;
|
id: ID; projectId: ID; owner?: ID; title: string; profileId?: ID;
|
||||||
pinned?: boolean; archived?: boolean;
|
pinned?: boolean; archived?: boolean;
|
||||||
allowCrossRead?: boolean; crossPrivate?: boolean;
|
allowCrossRead?: boolean; crossPrivate?: boolean;
|
||||||
|
permissionMode?: 'default' | 'plan' | 'auto_edit' | '';
|
||||||
|
sessionNote?: string;
|
||||||
createdAt: string; updatedAt: string;
|
createdAt: string; updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,7 +24,7 @@ export type Role = 'user' | 'assistant' | 'system';
|
|||||||
|
|
||||||
export type PartType =
|
export type PartType =
|
||||||
| 'text' | 'thinking' | 'tool_call' | 'tool_result' | 'artifact_ref'
|
| 'text' | 'thinking' | 'tool_call' | 'tool_result' | 'artifact_ref'
|
||||||
| 'hitl' | 'plan' | 'error' | 'card';
|
| 'hitl' | 'plan' | 'error' | 'card' | 'check';
|
||||||
|
|
||||||
export interface Part {
|
export interface Part {
|
||||||
type: PartType;
|
type: PartType;
|
||||||
@@ -34,23 +36,35 @@ export interface Part {
|
|||||||
plan?: PlanSnapshot;
|
plan?: PlanSnapshot;
|
||||||
card?: { kind: string; data: UINode[]; cardId?: ID }; // cardId 非空 = 收件箱卡片引用部件(submit_card)
|
card?: { kind: string; data: UINode[]; cardId?: ID }; // cardId 非空 = 收件箱卡片引用部件(submit_card)
|
||||||
error?: { code: string; message: string };
|
error?: { code: string; message: string };
|
||||||
|
check?: { passed: boolean; reason?: string }; // 终轮自动检查,过程区一行
|
||||||
process?: boolean; // run 终态后折叠为摘要行
|
process?: boolean; // run 终态后折叠为摘要行
|
||||||
|
durationMs?: number; // thinking 部件的思考时长(毫秒);工具耗时在 toolCall.durationMs
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Message {
|
export interface Message {
|
||||||
id: ID; conversationId: ID; runId?: ID; role: Role;
|
id: ID; conversationId: ID; runId?: ID; role: Role;
|
||||||
parts: Part[]; createdAt: string;
|
parts: Part[]; createdAt: string;
|
||||||
|
excluded?: boolean;
|
||||||
|
variantGroup?: string;
|
||||||
|
variantIndex?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RunKind = 'conversation' | 'subagent' | 'tool';
|
export type RunKind = 'conversation' | 'subagent' | 'tool';
|
||||||
export type RunStatus =
|
export type RunStatus =
|
||||||
| 'pending' | 'running' | 'waiting_hitl' | 'completed'
|
| 'pending' | 'running' | 'waiting_hitl' | 'completed'
|
||||||
| 'failed' | 'cancelled' | 'interrupted';
|
| 'failed' | 'cancelled' | 'interrupted';
|
||||||
export type RunPhase = 'thinking' | 'acting' | 'responding' | 'done';
|
export type RunPhase = 'thinking' | 'acting' | 'responding' | 'finalizing' | 'done';
|
||||||
|
|
||||||
export interface Usage {
|
export interface Usage {
|
||||||
promptTokens: number; completionTokens: number; totalTokens: number;
|
promptTokens: number; completionTokens: number; totalTokens: number;
|
||||||
durationMs: number; toolCalls: number;
|
durationMs: number; toolCalls: number;
|
||||||
|
model?: string;
|
||||||
|
// N3 计费拆分(additive;旧记录缺省为 0):
|
||||||
|
// Last* 是最近一次请求的用量,Cache* 是缓存命中累计。
|
||||||
|
lastPromptTokens?: number;
|
||||||
|
lastCompletionTokens?: number;
|
||||||
|
cacheReadTokens?: number;
|
||||||
|
cacheWriteTokens?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Run {
|
export interface Run {
|
||||||
@@ -78,12 +92,14 @@ export interface TodoList {
|
|||||||
// ---- 流式事件 ----
|
// ---- 流式事件 ----
|
||||||
|
|
||||||
export type StreamEventType =
|
export type StreamEventType =
|
||||||
| 'message.delta' | 'thinking.delta' | 'tool.call' | 'tool.result'
|
| 'message.delta' | 'thinking.delta'
|
||||||
| 'run.phase' | 'hitl.request' | 'hitl.resolved' | 'plan.update' | 'todo.update'
|
| 'tool.call.delta' | 'tool.call' | 'tool.output' | 'tool.result'
|
||||||
|
| 'run.phase' | 'run.check' | 'hitl.request' | 'hitl.resolved' | 'plan.update' | 'todo.update'
|
||||||
| 'agent.spawn' | 'agent.progress' | 'agent.result'
|
| 'agent.spawn' | 'agent.progress' | 'agent.result'
|
||||||
| 'artifact.created' | 'artifact.updated'
|
| 'artifact.created' | 'artifact.updated'
|
||||||
| 'run.completed' | 'run.failed' | 'error'
|
| '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> {
|
export interface StreamEvent<P = unknown> {
|
||||||
seq: number; runId?: ID; conversationId: ID; ts: string; v: number;
|
seq: number; runId?: ID; conversationId: ID; ts: string; v: number;
|
||||||
@@ -92,6 +108,9 @@ export interface StreamEvent<P = unknown> {
|
|||||||
|
|
||||||
// payload 形状(按 type 判别;未知字段忽略)
|
// payload 形状(按 type 判别;未知字段忽略)
|
||||||
export interface MessageDeltaPayload { index: number; text: string }
|
export interface MessageDeltaPayload { index: number; text: string }
|
||||||
|
export interface ToolCallDeltaPayload { index: number; callId?: string; name?: string; delta: string }
|
||||||
|
export interface ToolOutputPayload { callId: string; stream: 'stdout' | 'stderr'; text: string }
|
||||||
|
export interface ToolResultPayload { callId: string; status: string; summary?: string; truncated?: boolean; durationMs?: number }
|
||||||
export interface AgentSpawnPayload { agentRun: ID; name: string; task: string }
|
export interface AgentSpawnPayload { agentRun: ID; name: string; task: string }
|
||||||
export interface AgentProgressPayload { agentRun: ID; note?: string; phase?: RunPhase }
|
export interface AgentProgressPayload { agentRun: ID; note?: string; phase?: RunPhase }
|
||||||
export interface AgentResultPayload { agentRun: ID; status: RunStatus; summary?: string }
|
export interface AgentResultPayload { agentRun: ID; status: RunStatus; summary?: string }
|
||||||
@@ -101,11 +120,19 @@ export interface RunCompletedPayload {
|
|||||||
context?: ContextObservation
|
context?: ContextObservation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ContextSection {
|
||||||
|
name: string
|
||||||
|
estTokens: number
|
||||||
|
}
|
||||||
|
|
||||||
export interface ContextObservation {
|
export interface ContextObservation {
|
||||||
items: string[]
|
items: string[]
|
||||||
truncations?: string[]
|
truncations?: string[]
|
||||||
estChars: number
|
estChars: number
|
||||||
|
sections?: ContextSection[]
|
||||||
|
prefixHash?: string
|
||||||
}
|
}
|
||||||
|
export interface RunCheckPayload { passed: boolean; reason?: string }
|
||||||
export interface RunFailedPayload { code: string; message: string }
|
export interface RunFailedPayload { code: string; message: string }
|
||||||
|
|
||||||
// ---- 收件箱卡片 ----
|
// ---- 收件箱卡片 ----
|
||||||
@@ -143,6 +170,7 @@ export interface HITLCard {
|
|||||||
choices?: HITLChoice[]; allowFreeInput: boolean; freeInputHint?: string;
|
choices?: HITLChoice[]; allowFreeInput: boolean; freeInputHint?: string;
|
||||||
timeoutSec?: number | null; form?: UINode[]; createdAt: string; resolved: boolean;
|
timeoutSec?: number | null; form?: UINode[]; createdAt: string; resolved: boolean;
|
||||||
anchorMessageId?: ID; blocked?: boolean; blockReason?: string;
|
anchorMessageId?: ID; blocked?: boolean; blockReason?: string;
|
||||||
|
detail?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HITLResponse {
|
export interface HITLResponse {
|
||||||
@@ -157,6 +185,9 @@ export type UINodeType =
|
|||||||
|
|
||||||
export interface UINode {
|
export interface UINode {
|
||||||
type: UINodeType;
|
type: UINodeType;
|
||||||
|
/** Surface 层节点级标注(Generative UI U2):整页唯一稳定 id 与事实来源标识,旧路径不填。 */
|
||||||
|
id?: string;
|
||||||
|
source?: string[];
|
||||||
title?: string;
|
title?: string;
|
||||||
items?: string[];
|
items?: string[];
|
||||||
kv?: [string, string][];
|
kv?: [string, string][];
|
||||||
@@ -208,6 +239,8 @@ export interface PromptModule {
|
|||||||
body: string; outputContract?: string;
|
body: string; outputContract?: string;
|
||||||
source?: 'builtin' | 'plugin' | 'user'; owner?: string;
|
source?: 'builtin' | 'plugin' | 'user'; owner?: string;
|
||||||
basedOnVersion?: number; drifted?: boolean; disabled?: boolean;
|
basedOnVersion?: number; drifted?: boolean; disabled?: boolean;
|
||||||
|
/** 子代理 profile。只对 agent.sub.* 有意义;空字段表示沿用内置。 */
|
||||||
|
tools?: string[]; model?: string; maxTurns?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PromptBlock { moduleId: string; version: number; layer: string; body: string; }
|
export interface PromptBlock { moduleId: string; version: number; layer: string; body: string; }
|
||||||
@@ -247,18 +280,56 @@ export interface PluginManifest {
|
|||||||
tools?: string[]; workflows?: string[]; intents?: string[];
|
tools?: string[]; workflows?: string[]; intents?: string[];
|
||||||
prompts?: string[]; panels?: string[]; cards?: string[]; settings?: string[];
|
prompts?: string[]; panels?: string[]; cards?: string[]; settings?: string[];
|
||||||
commands?: string[]; workspaces?: string[];
|
commands?: string[]; workspaces?: string[];
|
||||||
|
skills?: string[]; mcpServers?: string[]; knowledge?: string[];
|
||||||
|
parsers?: string[]; agents?: string[]; channels?: string[]; pages?: string[];
|
||||||
};
|
};
|
||||||
|
scope?: { tenants?: string[]; projects?: string[] };
|
||||||
|
backend?: { mode?: 'inproc' | 'sidecar'; command?: string; args?: string[]; schemaVersion?: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PluginPanel {
|
export interface PluginPanel {
|
||||||
id: string; title: string; icon?: string; body: UINode[]; refreshTool?: string;
|
id: string; title: string; icon?: string; body: UINode[]; refreshTool?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 模板占位符:{{input}}、{{slots.<key>}}、{{steps.<id>.output}}。 */
|
||||||
|
export interface WorkflowDef {
|
||||||
|
id: string; title: string; description?: string; intentId?: string;
|
||||||
|
/** 整条工作流的 critic 重试上限。 */
|
||||||
|
maxRetries?: number;
|
||||||
|
steps: WorkflowStep[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StepCritic {
|
||||||
|
criteria: string;
|
||||||
|
/** 本步重试上限;缺省 1 次。 */
|
||||||
|
maxRetries?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkflowStep {
|
||||||
|
id: string; title: string;
|
||||||
|
kind: 'prompt' | 'tool' | 'hitl' | 'artifact' | 'card' | 'script';
|
||||||
|
prompt?: string;
|
||||||
|
/** prompt 步骤的子代理 profile,缺省 general。 */
|
||||||
|
agent?: string;
|
||||||
|
tool?: string;
|
||||||
|
args?: unknown;
|
||||||
|
hitl?: { question: string; kind: HITLKind; choices?: HITLChoice[]; allowFreeInput: boolean; freeInputHint?: string };
|
||||||
|
artifact?: string;
|
||||||
|
card?: { kind: CardKind; title: string; tone?: string; body?: UINode[]; actions?: { id: string; label: string; style?: 'primary' | 'default' | 'danger'; command?: string }[] };
|
||||||
|
/** script 步骤在插件目录内的相对路径。 */
|
||||||
|
script?: string;
|
||||||
|
critic?: StepCritic;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PluginWorkspace {
|
export interface PluginWorkspace {
|
||||||
id: string; title: string; icon?: string; shortcut?: string;
|
id: string; title: string; icon?: string; shortcut?: string;
|
||||||
badgeTool?: string; body: UINode[]; refreshTool?: string;
|
badgeTool?: string; body: UINode[]; refreshTool?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PluginPageDecl {
|
||||||
|
id: string; title: string; module: string;
|
||||||
|
}
|
||||||
|
|
||||||
// ---- 摄取 / 检索 ----
|
// ---- 摄取 / 检索 ----
|
||||||
|
|
||||||
export interface ParsedDocument {
|
export interface ParsedDocument {
|
||||||
@@ -284,3 +355,45 @@ export interface UploadInfo {
|
|||||||
artifactId: ID; filename: string; mime?: string; size: number;
|
artifactId: ID; filename: string; mime?: string; size: number;
|
||||||
parsed?: ParsedDocument;
|
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 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"`
|
Title string `json:"title,omitempty"`
|
||||||
Items []string `json:"items,omitempty"` // list
|
Items []string `json:"items,omitempty"` // list
|
||||||
KV [][2]string `json:"kv,omitempty"` // kv
|
KV [][2]string `json:"kv,omitempty"` // kv
|
||||||
|
|||||||
Reference in New Issue
Block a user