// Package settings 实现分层设置(4.9):system 默认 → user → project → conversation, // schema 驱动设置页(生效时机 hot / new-run / restart 显式标注)。 package settings import ( core "agent-system-core" "agent-system-backend/internal/store" ) // Scope 层级键:user 与 project: 落库;conversation 经会话画像选择实现。 const ( ScopeUser = "user" ScopeProjectPrefix = "project:" ) // SystemDefaults 是系统级默认(代码内置,settings.system.json 语义)。 func SystemDefaults() map[string]any { return map[string]any{ "models": map[string]any{ "chat": map[string]any{ "baseUrl": "", // 例:https://api.openai.com/v1 (OpenAI 兼容) "apiKey": "", "model": "", }, "lightweight": map[string]any{"baseUrl": "", "apiKey": "", "model": ""}, "embedding": map[string]any{"baseUrl": "", "apiKey": "", "model": ""}, "rerank": map[string]any{"baseUrl": "", "apiKey": "", "model": ""}, }, "runtime": map[string]any{ "mock": false, // true = 全部会话走 mock runner(演示/测试) "squad": true, // 小队编排(Agent-Squad 引擎)可用 "rateLimitPerMin": 0, // 租户限流(次/分;0 = 不限) "redisAddr": "", // 限流共享后端(原生 Redis addr;空 = 进程内) }, "storage": map[string]any{ "dataDir": "./data", // 启动注入实际值(--data) }, "context": map[string]any{ "maxHistoryMessages": 40, "maxToolResultChars": 20000, }, "retrieval": map[string]any{"topK": 5}, "ui": map[string]any{"theme": "dark"}, "plugins": map[string]any{"dir": "./plugins"}, } } // Schema 返回设置描述清单(设置页渲染依据;插件设置项由 pluginhost 追加)。 func Schema() []core.SettingDesc { return []core.SettingDesc{ {Key: "models.chat.baseUrl", Group: "models", Label: "主模型 Base URL", Type: core.SettingString, Default: "", Effect: core.EffectHot, Description: "OpenAI 兼容端点,如 https://api.openai.com/v1"}, {Key: "models.chat.apiKey", Group: "models", Label: "主模型 API Key", Type: core.SettingSecret, Default: "", Effect: core.EffectHot}, {Key: "models.chat.model", Group: "models", Label: "主模型名称", Type: core.SettingString, Default: "", Effect: core.EffectNewRun}, {Key: "models.lightweight.baseUrl", Group: "models", Label: "轻量模型 Base URL", Type: core.SettingString, Default: "", Effect: core.EffectHot, Description: "标题生成/澄清等轻任务;空则回落主模型"}, {Key: "models.lightweight.apiKey", Group: "models", Label: "轻量模型 API Key", Type: core.SettingSecret, Default: "", Effect: core.EffectHot}, {Key: "models.lightweight.model", Group: "models", Label: "轻量模型名称", Type: core.SettingString, Default: "", Effect: core.EffectNewRun}, {Key: "models.embedding.baseUrl", Group: "models", Label: "Embedding Base URL", Type: core.SettingString, Default: "", Effect: core.EffectHot, Description: "RAG/记忆检索用(向量管线远期启用)"}, {Key: "models.embedding.apiKey", Group: "models", Label: "Embedding API Key", Type: core.SettingSecret, Default: "", Effect: core.EffectHot}, {Key: "models.embedding.model", Group: "models", Label: "Embedding 模型", Type: core.SettingString, Default: "", Effect: core.EffectHot}, {Key: "models.rerank.baseUrl", Group: "models", Label: "Reranker Base URL", Type: core.SettingString, Default: "", Effect: core.EffectHot}, {Key: "models.rerank.apiKey", Group: "models", Label: "Reranker API Key", Type: core.SettingSecret, Default: "", Effect: core.EffectHot}, {Key: "models.rerank.model", Group: "models", Label: "Reranker 模型", Type: core.SettingString, Default: "", Effect: core.EffectHot}, {Key: "runtime.mock", Group: "runtime", Label: "演示模式(mock runner)", Type: core.SettingBool, Default: false, Effect: core.EffectNewRun, Description: "不调用真实模型,按脚本回放事件流"}, {Key: "runtime.squad", Group: "runtime", Label: "小队编排(Agent-Squad)", Type: core.SettingBool, Default: true, Effect: core.EffectNewRun, Description: "启用 Agent-Squad 引擎编排(#squad 指令与 squad_orchestrate 工具)"}, {Key: "runtime.rateLimitPerMin", Group: "runtime", Label: "租户限流(次/分)", Type: core.SettingNumber, Default: 0, Effect: core.EffectHot, Description: "0 = 不限;多用户建议 300–1200"}, {Key: "runtime.redisAddr", Group: "runtime", Label: "Redis 地址(限流共享)", Type: core.SettingString, Default: "", Effect: core.EffectRestart, Description: "原生 Redis(如 127.0.0.1:6379);空 = 进程内限流(单实例)"}, {Key: "context.maxHistoryMessages", Group: "context", Label: "历史窗口条数", Type: core.SettingNumber, Default: 40, Effect: core.EffectNewRun}, {Key: "context.maxToolResultChars", Group: "context", Label: "工具结果上限(字符)", Type: core.SettingNumber, Default: 20000, Effect: core.EffectNewRun}, {Key: "retrieval.topK", Group: "retrieval", Label: "检索命中数", Type: core.SettingNumber, Default: 5, Effect: core.EffectHot}, {Key: "ui.theme", Group: "ui", Label: "主题", Type: core.SettingEnum, Default: "dark", Options: []string{"dark", "light"}, Effect: core.EffectHot}, {Key: "plugins.dir", Group: "plugins", Label: "插件目录", Type: core.SettingString, Default: "./plugins", Effect: core.EffectRestart}, } } // Service 提供有效设置计算与分层写入。 type Service struct { st *store.Store system map[string]any // 启动注入的系统层覆盖(如数据目录),位于 defaults 与 user 之间 } func NewService(st *store.Store) *Service { return &Service{st: st} } // SetSystemOverride 注入启动期系统层覆盖(组合根调用;不落库)。 func (s *Service) SetSystemOverride(values map[string]any) { s.system = values } // Effective 计算覆盖链结果:defaults → system(启动注入) → user(租户) → project。 // owner 是租户身份(认证中间件注入;未启用认证 = store.OwnerDefault)。 func (s *Service) Effective(owner, projectID string) core.EffectiveSettings { eff := core.MergeSettings(SystemDefaults(), map[string]any{}) if s.system != nil { eff = core.MergeSettings(eff, s.system) } if user, err := s.st.GetSettingsScope(owner, ScopeUser); err == nil { eff = core.MergeSettings(eff, user) } if projectID != "" { if proj, err := s.st.GetSettingsScope(owner, ScopeProjectPrefix+projectID); err == nil { eff = core.MergeSettings(eff, proj) } } return eff } func (s *Service) UserScope(owner string) (map[string]any, error) { return s.st.GetSettingsScope(owner, ScopeUser) } func (s *Service) ProjectScope(owner, projectID string) (map[string]any, error) { return s.st.GetSettingsScope(owner, ScopeProjectPrefix+projectID) } func (s *Service) MergeUser(owner string, values map[string]any) error { return s.st.MergeSettingsScope(owner, ScopeUser, values) } func (s *Service) MergeProject(owner, projectID string, values map[string]any) error { return s.st.MergeSettingsScope(owner, ScopeProjectPrefix+projectID, values) } // Masked 返回脱敏视图(secret 类 key 掩码),用于 API 响应。Effective 每次 // 深合并产出新 map,就地掩码不影响后续计算。 func Masked(eff core.EffectiveSettings) map[string]any { out := map[string]any(eff) maskSecrets(out) return out } func maskSecrets(m map[string]any) { for k, v := range m { if vm, ok := v.(map[string]any); ok { maskSecrets(vm) continue } if isSecretKey(k) { if s, ok := v.(string); ok && s != "" { m[k] = "••••••" } } } } func isSecretKey(k string) bool { return k == "apiKey" || k == "api_key" || k == "secret" || k == "token" }