Details (original message): Add dual-tier storage (embedded SQLite + optional PostgreSQL), tenant owner scoping, API key auth, versioned dual-dialect migrations, integrity hardening, and batched event persistence
141 lines
7.0 KiB
Go
141 lines
7.0 KiB
Go
// 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:<id> 落库;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 引擎)可用
|
||
},
|
||
"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: "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"
|
||
}
|