Files
Agent-System-Backend/internal/server/borrow.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

288 lines
7.7 KiB
Go

package server
import (
"encoding/json"
"net/http"
"regexp"
"strings"
agentcommand "agent-command"
"agent-system-backend/internal/store"
core "agent-system-core"
)
var userCommandSlug = regexp.MustCompile(`^[a-z][a-z0-9-]*$`)
func (s *Server) listSecrets(w http.ResponseWriter, r *http.Request) {
list, err := s.st.ListSecrets(s.owner(r))
writeJSONOr(w, list, err)
}
func (s *Server) createSecret(w http.ResponseWriter, r *http.Request) {
var req struct {
Service string `json:"service"`
Label string `json:"label"`
Value string `json:"value"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
sec, err := s.st.AddSecret(s.owner(r), req.Service, req.Label, req.Value)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
writeJSON(w, http.StatusOK, sec)
}
func (s *Server) activateSecret(w http.ResponseWriter, r *http.Request) {
writeOKOr(w, s.st.ActivateSecret(s.owner(r), r.PathValue("id")))
}
func (s *Server) renameSecret(w http.ResponseWriter, r *http.Request) {
var req struct {
Label string `json:"label"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
writeOKOr(w, s.st.RenameSecret(s.owner(r), r.PathValue("id"), req.Label))
}
func (s *Server) deleteSecret(w http.ResponseWriter, r *http.Request) {
writeOKOr(w, s.st.DeleteSecret(s.owner(r), r.PathValue("id")))
}
func (s *Server) listContextEntries(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
list, err := s.st.ListContextEntries(s.owner(r), q.Get("projectId"), q.Get("conversationId"))
writeJSONOr(w, list, err)
}
func (s *Server) saveContextEntry(w http.ResponseWriter, r *http.Request) {
var e store.ContextEntry
if err := json.NewDecoder(r.Body).Decode(&e); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
saved, err := s.st.SaveContextEntry(s.owner(r), e)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
writeJSON(w, http.StatusOK, saved)
}
func (s *Server) deleteContextEntry(w http.ResponseWriter, r *http.Request) {
writeOKOr(w, s.st.DeleteContextEntry(s.owner(r), r.PathValue("id")))
}
func (s *Server) listUserPromptCommands(w http.ResponseWriter, r *http.Request) {
list, err := s.st.ListUserCommands(s.owner(r))
writeJSONOr(w, list, err)
}
func (s *Server) saveUserPromptCommand(w http.ResponseWriter, r *http.Request) {
var c store.UserCommand
if err := json.NewDecoder(r.Body).Decode(&c); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
saved, err := s.st.SaveUserCommand(s.owner(r), c)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
s.registerUserPrompt(saved)
writeJSON(w, http.StatusOK, saved)
}
func (s *Server) deleteUserPromptCommand(w http.ResponseWriter, r *http.Request) {
owner := s.owner(r)
id := r.PathValue("id")
list, _ := s.st.ListUserCommands(owner)
var name string
for _, c := range list {
if c.ID == id {
name = c.Name
}
}
if err := s.st.DeleteUserCommand(owner, id); err != nil {
writeStoreErr(w, err)
return
}
if s.Bus != nil && name != "" {
s.Bus.UnregisterUser(userCommandID(name), name)
}
writeJSON(w, http.StatusOK, map[string]any{"deleted": id})
}
func (s *Server) excludeMessage(w http.ResponseWriter, r *http.Request) {
var req struct {
Excluded *bool `json:"excluded"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
excluded := true
if req.Excluded != nil {
excluded = *req.Excluded
}
msg, err := s.st.MessageByID(r.PathValue("id"))
if err != nil {
writeStoreErr(w, err)
return
}
if _, err := s.st.GetConversation(s.owner(r), msg.ConversationID); err != nil {
writeStoreErr(w, err)
return
}
writeOKOr(w, s.st.ExcludeRunGroup(msg.ID, excluded))
}
func (s *Server) selectVariant(w http.ResponseWriter, r *http.Request) {
msg, err := s.st.MessageByID(r.PathValue("id"))
if err != nil {
writeStoreErr(w, err)
return
}
if _, err := s.st.GetConversation(s.owner(r), msg.ConversationID); err != nil {
writeStoreErr(w, err)
return
}
writeOKOr(w, s.st.SelectVariant(msg.ID))
}
func (s *Server) regenerateMessage(w http.ResponseWriter, r *http.Request) {
owner := s.owner(r)
convID := r.PathValue("id")
conv, err := s.st.GetConversation(owner, convID)
if err != nil {
writeStoreErr(w, err)
return
}
var req struct {
MessageID string `json:"messageId"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.MessageID == "" {
http.Error(w, "messageId required", http.StatusBadRequest)
return
}
target, err := s.st.MessageByID(req.MessageID)
if err != nil || target.ConversationID != convID || target.Role != core.RoleAssistant {
http.Error(w, "assistant message required", http.StatusBadRequest)
return
}
hist, err := s.st.ListMessages(convID)
if err != nil {
writeStoreErr(w, err)
return
}
var user core.Message
found := false
maxIndex := target.VariantIndex
for _, m := range hist {
if m.VariantGroup != "" && m.VariantGroup == target.VariantGroup && m.VariantIndex > maxIndex {
maxIndex = m.VariantIndex
}
if m.ID == target.ID {
found = true
}
if !found && m.Role == core.RoleUser {
user = m
}
}
if user.ID == "" {
http.Error(w, "no user turn to regenerate", http.StatusBadRequest)
return
}
group := target.VariantGroup
if group == "" {
group = "var_" + target.ID
}
if err := s.st.SetMessageFlags(target.ID, true, group, target.VariantIndex); err != nil {
writeStoreErr(w, err)
return
}
if err := s.st.ExcludeRunGroup(target.ID, true); err != nil {
writeStoreErr(w, err)
return
}
if err := s.st.SetRegenPending(convID, group, maxIndex+1); err != nil {
writeStoreErr(w, err)
return
}
if _, err := s.mgr.RewindFiles(owner, convID, user.ID); err != nil {
// 没有文件检查点时仍然重新生成回复。
}
run, err := s.mgr.StartConversationRun(conv, user, nil, nil)
if err != nil {
_, _, _ = s.st.TakeRegenPending(convID)
writeStoreErr(w, err)
return
}
writeJSON(w, http.StatusOK, map[string]any{"run": run, "variantGroup": group})
}
func (s *Server) expandUserCommand(owner, text string) (string, bool) {
fields := strings.Fields(text)
if len(fields) == 0 || !strings.HasPrefix(fields[0], "/") {
return "", false
}
list, err := s.st.ListUserCommands(owner)
if err != nil {
return "", false
}
name := strings.ToLower(fields[0])
rest := strings.TrimSpace(strings.TrimPrefix(text, fields[0]))
for _, c := range list {
if strings.ToLower(c.Name) != name {
continue
}
body := c.Template
if strings.Contains(body, "{{input}}") {
body = strings.ReplaceAll(body, "{{input}}", rest)
} else if rest != "" {
body = strings.TrimSpace(body + "\n" + rest)
}
return body, true
}
return "", false
}
func userCommandID(name string) string {
slug := strings.TrimPrefix(strings.ToLower(name), "/")
return "user." + slug
}
func (s *Server) registerUserPrompt(c store.UserCommand) {
if s.Bus == nil {
return
}
slug := strings.TrimPrefix(strings.ToLower(c.Name), "/")
if !userCommandSlug.MatchString(slug) {
return
}
_ = s.Bus.RegisterUser(agentcommand.CommandSpec{
ID: userCommandID(c.Name), Title: c.Title, Description: "展开为提示词,不自动执行",
}, c.Name)
}
func (s *Server) appendUserPromptCommands(owner string, out []map[string]any) []map[string]any {
list, err := s.st.ListUserCommands(owner)
if err != nil {
return out
}
for _, c := range list {
s.registerUserPrompt(c)
out = append(out, map[string]any{
"id": userCommandID(c.Name), "name": c.Name, "title": c.Title,
"description": "展开为提示词", "source": "user", "enabled": true,
})
}
return out
}