Files
Agent-System-Core/profile.go
T
weichen 9150e6f90b Add system-managed setting scope contract to SettingDesc
Why: P2 受管设置机制需要 schema 声明写入归属——system 受管项仅管理员可写,user/project 层拒绝写入且生效链忽略存量覆盖。
Evidence: go test ./... 全绿(ManagedPaths/Touches/Strip 含祖先标量覆盖与整棵子树剥离用例)。
Impact: 新增 SettingScope 常量、SettingDesc.Scope 字段与三个纯函数;空 Scope 保持既有行为,向后兼容。
Refs: modules/documents/docs/proposals/2026-10-13-rdm-settings-members-duties-perms-increment.md P2
2026-10-13 03:40:25 +00:00

334 lines
9.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package core
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"`
// 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"`
}
// ModelRoute 按用途选择模型(4.7 kind 路由):空串回落到 chat 默认。
type ModelRoute struct {
Chat string `json:"chat,omitempty"`
Lightweight string `json:"lightweight,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"` // ["*"] 全部
Deny []string `json:"deny,omitempty"`
Confirm []string `json:"confirm,omitempty"`
}
// ContextPolicy(4.10):ctxasm 的输入;逐版本增加策略项不动组装器接口。
type ContextPolicy struct {
MaxHistoryMessages int `json:"maxHistoryMessages"` // 0 = 默认 40
SummarizeBeyond bool `json:"summarizeBeyond,omitempty"` // 远期;先存标志
MemoryBudgetPct int `json:"memoryBudgetPct,omitempty"` // 注入槽份额上限
KnowledgeBudgetPct int `json:"knowledgeBudgetPct,omitempty"`
MaxToolResultChars int `json:"maxToolResultChars,omitempty"` // 0 = 默认 20000
}
type HITLPolicy struct {
AskOnAmbiguity bool `json:"askOnAmbiguity"`
TimeoutSec *int `json:"timeoutSec,omitempty"`
}
type RunBudget struct {
MaxTokens *int `json:"maxTokens,omitempty"`
MaxToolCalls *int `json:"maxToolCalls,omitempty"`
MaxTurns *int `json:"maxTurns,omitempty"` // LLM 循环轮上限
}
// ToolAllows 判定:deny 优先;allow 含 "*" 或精确/前缀命中。
func (p ToolPolicy) ToolAllowed(name string) bool {
if matchAny(p.Deny, name) {
return false
}
if len(p.Allow) == 0 {
return true
}
return matchAny(p.Allow, name)
}
func (p ToolPolicy) NeedsConfirm(name string) bool {
return matchAny(p.Confirm, name)
}
// matchAny 语义:pattern 精确等于 name,或以 "xxx." 开头表示前缀族(http. → http.*)。
func matchAny(patterns []string, name string) bool {
for _, p := range patterns {
if p == "*" || p == name {
return true
}
if n := len(p); n > 1 && p[n-1] == '.' {
if len(name) > n-1 && name[:n-1] == p[:n-1] {
return true
}
}
}
return false
}
// ---- 设置(4.9)----
type SettingType string
const (
SettingString SettingType = "string"
SettingNumber SettingType = "number"
SettingBool SettingType = "boolean"
SettingEnum SettingType = "enum"
SettingSecret SettingType = "secret"
)
type SettingEffect string
const (
EffectHot SettingEffect = "hot" // 立即生效
EffectNewRun SettingEffect = "new-run" // 仅影响新 run
EffectRestart SettingEffect = "restart" // 需重启
)
// SettingScope 声明设置项的写入归属(提案 2026-10-13-rdm-settings-members-duties-perms-increment P2)。
// 空串 = 默认:user 与 project 层可写(向后兼容既有 schema)。
type SettingScope string
const (
// SettingScopeSystem 受管项:仅系统管理面(admin)可写;user/project 写入被拒,
// 生效链忽略这两层的存量覆盖值。
SettingScopeSystem SettingScope = "system"
)
// SettingDesc 描述一个设置项;设置页按此渲染并标注生效时机(schema 驱动)。
type SettingDesc struct {
Key string `json:"key"`
Group string `json:"group"` // 分区:models | profiles | tools | knowledge | ui | plugin:<id>
Label string `json:"label"`
Description string `json:"description,omitempty"`
Type SettingType `json:"type"`
Default any `json:"default,omitempty"`
Options []string `json:"options,omitempty"` // enum
Effect SettingEffect `json:"effect"`
Secret bool `json:"secret,omitempty"`
Scope SettingScope `json:"scope,omitempty"` // 空 = user/project 可写;system = 受管
}
// 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)深合并结果。
type EffectiveSettings map[string]any
// MergeSettings 深合并:后者覆盖前者;map 递归合并,其它类型直接覆盖(4.9 数组显式覆盖)。
func MergeSettings(base, override map[string]any) map[string]any {
out := make(map[string]any, len(base)+len(override))
for k, v := range base {
out[k] = v
}
for k, v := range override {
if bm, ok := base[k].(map[string]any); ok {
if vm, ok2 := v.(map[string]any); ok2 {
out[k] = MergeSettings(bm, vm)
continue
}
}
out[k] = v
}
return out
}
// GetString/GetBool/GetNumber 读取点路径("models.chat");缺省返回 def。
func (s EffectiveSettings) GetString(path, def string) string {
v, ok := lookupPath(s, path)
if !ok {
return def
}
if str, ok2 := v.(string); ok2 {
return str
}
return def
}
func (s EffectiveSettings) GetBool(path string, def bool) bool {
v, ok := lookupPath(s, path)
if !ok {
return def
}
if b, ok2 := v.(bool); ok2 {
return b
}
return def
}
func (s EffectiveSettings) GetNumber(path string, def float64) float64 {
v, ok := lookupPath(s, path)
if !ok {
return def
}
if f, ok2 := v.(float64); ok2 {
return f
}
return def
}
func lookupPath(m map[string]any, path string) (any, bool) {
parts := splitPath(path)
var cur any = m
for _, p := range parts {
cm, ok := cur.(map[string]any)
if !ok {
return nil, false
}
cur, ok = cm[p]
if !ok {
return nil, false
}
}
return cur, true
}
func splitPath(path string) []string {
var out []string
cur := ""
for i := 0; i < len(path); i++ {
switch path[i] {
case '.':
out = append(out, cur)
cur = ""
default:
cur += string(path[i])
}
}
return append(out, cur)
}
var _ = json.Marshal // 保持 encoding/json 引用(RawMessage 一致性)