Files
Agent-System-Backend/internal/settings/settings.go
T
weichen 29f14ac5f2 Add multi-instance groundwork: blobstore seam for artifacts
Details (original message):
Add multi-instance groundwork: blobstore seam for artifacts, PG LISTEN/NOTIFY event fanout, and per-tenant rate limiting with native Redis backend
2026-09-30 02:39:50 +00:00

145 lines
7.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 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 引擎)可用
"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"
}