Files
Agent-System-Backend/internal/settings/settings.go
T
weichen 53c6630eca Add submit_card explicit routing with card-as-authority and context backfill (CF1)
Why: 卡流机制(D39–D43)——产物型长内容显式路由进收件箱:模型经 submit_card 提交 result 卡,全文权威在卡片,消息只存引用,上下文回填防失忆。
Evidence: go test ./internal/runtime/(门控/产卡/引用部件)、./internal/ctxasm/(回填/截断/占位)、./internal/server/ 全绿(新增 TestMockCardflowFullLoop:#cardflow → result 卡 + cardId 部件 + finishRun 锚点回填)。
Impact: 新内置工具 submit_card(ui.cardflow 默认关闭,未声明被幻觉调用时拒绝执行);settings 增 ui.cardflow;style-cardflow 模块 + ModuleBody 注入;ctxasm 增 CardResolver 与 card(n) 观测;runmgr 增 SubmitCard/Card 与锚点回填;mock 增 #cardflow。
Refs: Dev 任务卡 feature/cardflow-r1;依赖 core bcfacee(CardPart.CardID)。
2026-09-30 16:30:13 +00:00

155 lines
9.0 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 (
"agent-system-backend/internal/store"
core "agent-system-core"
)
// 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": ""},
},
"tools": map[string]any{
"tavily": map[string]any{"apiKeys": "", "endpoint": ""}, // 网络搜索(逗号分隔密钥池;空 = 不注册搜索工具)
},
"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,
},
"crossSession": map[string]any{
"read": false, // 跨会话读取项目闸门(提案 2026-09-30-cross-session-reading):关 = 全项目禁读
},
"retrieval": map[string]any{"topK": 5},
"ui": map[string]any{"theme": "dark", "cardflow": false},
"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: "crossSession.read", Group: "crossSession", Label: "跨会话读取", Type: core.SettingBool, Default: false, Effect: core.EffectNewRun, Description: "允许本项目会话读取同项目其他会话的对话记录(list_conversations / read_conversation 工具);会话可单独关闭读取或设为私有"},
{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: "ui.cardflow", Group: "ui", Label: "长结果进右侧卡片", Type: core.SettingBool, Default: false, Effect: core.EffectNewRun, Description: "开启后报告/纪要/方案等产物型长内容经 submit_card 进收件箱,对话只留结论摘要与锚点"},
{Key: "tools.tavily.apiKeys", Group: "tools", Label: "Tavily API Keys(池)", Type: core.SettingSecret, Default: "", Effect: core.EffectRestart, Description: "逗号分隔;配置后注册 tavily_search/web_search 工具(429/401/403 自动轮换下一把)"},
{Key: "tools.tavily.endpoint", Group: "tools", Label: "Tavily 端点", Type: core.SettingString, Default: "", Effect: core.EffectRestart, Description: "空 = 官方 https://api.tavily.com/search;可指向自建代理"},
{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"
}