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
334 lines
9.6 KiB
Go
334 lines
9.6 KiB
Go
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 一致性)
|