Details (original message): Implement the backend: store/bus/runmgr core, mock and LLM runners (agent-native client), HITL three-branch routing, subagent runs, builtin and declarative HTTP tools, ingest parsers (md/txt/docx/pdf-text), knowledge fulltext, layered settings, profiles, plugin host, REST+SSE API, single-binary entrypoint; e2e and unit tests.
122 lines
6.1 KiB
Go
122 lines
6.1 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(演示/测试)
|
||
},
|
||
"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: "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 }
|
||
|
||
func NewService(st *store.Store) *Service { return &Service{st: st} }
|
||
|
||
// Effective 计算覆盖链结果:system → user → project(conversation 层经画像实现)。
|
||
func (s *Service) Effective(projectID string) core.EffectiveSettings {
|
||
eff := core.MergeSettings(SystemDefaults(), map[string]any{})
|
||
if user, err := s.st.GetSettingsScope(ScopeUser); err == nil {
|
||
eff = core.MergeSettings(eff, user)
|
||
}
|
||
if projectID != "" {
|
||
if proj, err := s.st.GetSettingsScope(ScopeProjectPrefix + projectID); err == nil {
|
||
eff = core.MergeSettings(eff, proj)
|
||
}
|
||
}
|
||
return eff
|
||
}
|
||
|
||
func (s *Service) UserScope() (map[string]any, error) { return s.st.GetSettingsScope(ScopeUser) }
|
||
|
||
func (s *Service) ProjectScope(projectID string) (map[string]any, error) {
|
||
return s.st.GetSettingsScope(ScopeProjectPrefix + projectID)
|
||
}
|
||
|
||
func (s *Service) MergeUser(values map[string]any) error { return s.st.MergeSettingsScope(ScopeUser, values) }
|
||
|
||
func (s *Service) MergeProject(projectID string, values map[string]any) error {
|
||
return s.st.MergeSettingsScope(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"
|
||
}
|