Files
Agent-System-Backend/internal/settings/secrets.go
T
weichen 42cca8fade Store secrets apart from settings and assemble context at the end
Why: Masks must not be the only copy of a key, and personal instructions, lore hits, and candidate replies have to reach the model without rewriting the system prefix or deleting history.
Evidence: go test ./internal/store ./internal/ctxasm ./internal/settings ./internal/runtime ./internal/server. Migration v16 upgrades and rolls back. Startup snapshots settings before moving raw keys.
Impact: One labeled key is active per service. Excluding a message hides its whole run from the model. Regenerating appends another candidate and rewinds files to the user turn. User slash commands expand to prompt text and do not fire on their own.
Refs: docs/records/2026-10-04-sillytavern-review.md
2026-10-04 18:20:10 +00:00

116 lines
2.6 KiB
Go

package settings
import (
"encoding/json"
"strings"
"time"
)
type secretSpec struct {
path string
service string
}
var secretSpecs = []secretSpec{
{"models.chat.apiKey", "models.chat"},
{"models.lightweight.apiKey", "models.lightweight"},
{"models.embedding.apiKey", "models.embedding"},
{"models.rerank.apiKey", "models.rerank"},
{"tools.tavily.apiKeys", "tools.tavily"},
}
// SnapshotAndMigrateSecrets 先为每个已存 scope 留快照,再把明文密钥迁入独立表并从表单设置里清掉。
// 该服务已经有密钥行时不再迁入,避免重复。掩码占位符不迁。
func (s *Service) SnapshotAndMigrateSecrets() error {
refs, err := s.st.ListSettingsScopes()
if err != nil {
return err
}
for _, ref := range refs {
cur, err := s.st.GetSettingsScope(ref.Owner, ref.Scope)
if err != nil {
return err
}
raw, err := json.Marshal(cur)
if err != nil {
return err
}
if _, err := s.st.SaveSnapshotIfChanged(ref.Owner, ref.Scope, string(raw), "startup"); err != nil {
return err
}
for _, spec := range secretSpecs {
val, ok := lookupString(cur, spec.path)
if !ok || val == MaskPlaceholder {
continue
}
has, err := s.st.HasSecret(ref.Owner, spec.service)
if err != nil {
return err
}
if !has {
if _, err := s.st.AddSecret(ref.Owner, spec.service, "迁入", val); err != nil {
return err
}
}
path := spec.path
if err := s.st.UpdateSettingsScope(ref.Owner, ref.Scope, func(cur map[string]any) (map[string]any, error) {
deletePath(cur, path)
return cur, nil
}); err != nil {
return err
}
}
}
return nil
}
func (s *Service) snapshotThrottled(owner, scope string) {
if at, ok := s.st.LatestSnapshotAt(owner, scope); ok && time.Since(at) < 10*time.Minute {
return
}
cur, err := s.st.GetSettingsScope(owner, scope)
if err != nil {
return
}
raw, err := json.Marshal(cur)
if err != nil {
return
}
_, _ = s.st.SaveSnapshotIfChanged(owner, scope, string(raw), "save")
}
func lookupString(m map[string]any, path string) (string, bool) {
var cur any = m
for _, p := range strings.Split(path, ".") {
obj, ok := cur.(map[string]any)
if !ok {
return "", false
}
cur, ok = obj[p]
if !ok {
return "", false
}
}
s, ok := cur.(string)
if !ok || strings.TrimSpace(s) == "" {
return "", false
}
return s, true
}
func deletePath(m map[string]any, path string) {
parts := strings.Split(path, ".")
cur := m
for i, p := range parts {
if i == len(parts)-1 {
delete(cur, p)
return
}
next, ok := cur[p].(map[string]any)
if !ok {
return
}
cur = next
}
}