Files
Agent-System-Backend/internal/store/borrow.go
T
weichen adcd8fefd2 Fence U3 account writes and stabilize directory cursors
Why: Account disable could be followed by late Backend business writes, API key creation could implicitly create users, account bootstrap locks could invert, and directory cursors were process-local or leaked out-of-scope changes.

Evidence: Local go test ./... and isolated PostgreSQL package suites passed on the staged candidate; exact committed-head SELF replay and stage 6 human review remain pending.

Impact: Adds final account transactions and external-effect checks across HTTP, Host, Bus and run manager; tightens API key issuance and persists versioned cursor signing. Delegated RDM rename still needs the separately proposed real-operator contract, so A1 and stage 6 are not complete.

Refs: Documents U3 r1, approved A1-A4 amendment fb3a821, pending delegated-projection amendment 5783940, Dev feature/iam-u3-account-directory-recovery.
2026-10-14 07:33:54 +00:00

722 lines
23 KiB
Go

package store
import (
"database/sql"
"encoding/json"
"fmt"
"strings"
"time"
core "agent-system-core"
)
// Secret 是一把带标签的服务密钥。列表接口不返回 Value。
type Secret struct {
ID string `json:"id"`
Owner string `json:"owner,omitempty"`
Service string `json:"service"`
Label string `json:"label"`
Active bool `json:"active"`
Mask string `json:"mask"`
CreatedAt string `json:"createdAt"`
Value string `json:"-"`
}
// ContextEntry 是一条条件注入。常驻或关键词命中后附在上下文末尾。
type ContextEntry struct {
ID string `json:"id"`
Owner string `json:"owner,omitempty"`
ProjectID string `json:"projectId,omitempty"`
ConversationID string `json:"conversationId,omitempty"`
Keys []string `json:"keys"`
Secondary string `json:"secondary,omitempty"`
Logic string `json:"logic"`
Content string `json:"content"`
Constant bool `json:"constant"`
ScanDepth int `json:"scanDepth"`
SortOrder int `json:"sortOrder"`
BudgetChars int `json:"budgetChars"`
Enabled bool `json:"enabled"`
}
// UserCommand 是用户自定义的 / 命令,展开成提示词,不自动触发。
type UserCommand struct {
ID string `json:"id"`
Owner string `json:"owner,omitempty"`
Name string `json:"name"`
Title string `json:"title"`
Template string `json:"template"`
CreatedAt string `json:"createdAt"`
}
func maskSecret(v string) string {
v = strings.TrimSpace(v)
if len(v) <= 4 {
return "••••••"
}
return "••••" + v[len(v)-4:]
}
func (s *Store) ListSecrets(owner string) ([]Secret, error) {
rows, err := s.db.Query(s.q(`SELECT id,service,label,value,active,created_at FROM secret_keys WHERE owner=? ORDER BY service, created_at`), owner)
if err != nil {
return nil, err
}
defer rows.Close()
out := []Secret{}
for rows.Next() {
var sec Secret
var active int
if err := rows.Scan(&sec.ID, &sec.Service, &sec.Label, &sec.Value, &active, &sec.CreatedAt); err != nil {
return nil, err
}
sec.Active = active == 1
sec.Mask = maskSecret(sec.Value)
sec.Value = ""
out = append(out, sec)
}
return out, rows.Err()
}
// ActiveSecret 返回该服务当前生效的明文。没有记录时 ok 为 false。
func (s *Store) ActiveSecret(owner, service string) (string, bool) {
var v string
err := s.db.QueryRow(s.q(`SELECT value FROM secret_keys WHERE owner=? AND service=? AND active=1 ORDER BY created_at DESC LIMIT 1`), owner, service).Scan(&v)
if err != nil || strings.TrimSpace(v) == "" {
return "", false
}
return v, true
}
// AddSecret 追加一把密钥并把它设为该服务唯一生效项。
func (s *Store) AddSecret(owner, service, label, value string) (Secret, error) {
service = strings.TrimSpace(service)
value = strings.TrimSpace(value)
label = strings.TrimSpace(label)
if service == "" || value == "" {
return Secret{}, fmt.Errorf("store: secret service and value are required")
}
if label == "" {
label = "未命名"
}
tx, err := s.db.Begin()
if err != nil {
return Secret{}, err
}
defer tx.Rollback()
if _, err := tx.Exec(s.q(`UPDATE secret_keys SET active=0 WHERE owner=? AND service=?`), owner, service); err != nil {
return Secret{}, err
}
sec := Secret{ID: newID("key"), Service: service, Label: label, Active: true, CreatedAt: nowISO(), Mask: maskSecret(value)}
if _, err := tx.Exec(s.q(`INSERT INTO secret_keys(id,owner,service,label,value,active,created_at) VALUES(?,?,?,?,?,?,?)`),
sec.ID, owner, service, label, value, 1, sec.CreatedAt); err != nil {
return Secret{}, err
}
return sec, tx.Commit()
}
func (s *Store) ActivateSecret(owner, id string) error {
var service string
err := s.db.QueryRow(s.q(`SELECT service FROM secret_keys WHERE id=? AND owner=?`), id, owner).Scan(&service)
if err != nil {
return ErrNotFound
}
tx, err := s.db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if _, err := tx.Exec(s.q(`UPDATE secret_keys SET active=0 WHERE owner=? AND service=?`), owner, service); err != nil {
return err
}
if _, err := tx.Exec(s.q(`UPDATE secret_keys SET active=1 WHERE id=? AND owner=?`), id, owner); err != nil {
return err
}
return tx.Commit()
}
func (s *Store) RenameSecret(owner, id, label string) error {
label = strings.TrimSpace(label)
if label == "" {
return fmt.Errorf("store: label required")
}
res, err := s.db.Exec(s.q(`UPDATE secret_keys SET label=? WHERE id=? AND owner=?`), label, id, owner)
if err != nil {
return err
}
n, _ := res.RowsAffected()
if n == 0 {
return ErrNotFound
}
return nil
}
func (s *Store) DeleteSecret(owner, id string) error {
res, err := s.db.Exec(s.q(`DELETE FROM secret_keys WHERE id=? AND owner=?`), id, owner)
if err != nil {
return err
}
n, _ := res.RowsAffected()
if n == 0 {
return ErrNotFound
}
return nil
}
// AddSecretAccountChecked fences the final secret write against account disable.
func (s *Store) AddSecretAccountChecked(actor, service, label, value string, allowImplicitDefault bool) (Secret, error) {
service, label, value = strings.TrimSpace(service), strings.TrimSpace(label), strings.TrimSpace(value)
if service == "" || value == "" {
return Secret{}, fmt.Errorf("store: secret service and value are required")
}
if label == "" {
label = "未命名"
}
sec := Secret{ID: newID("key"), Service: service, Label: label, Active: true, CreatedAt: nowISO(), Mask: maskSecret(value)}
err := s.withEffectiveAccountWrite(actor, allowImplicitDefault, func(tx *sql.Tx, _ Account) error {
if _, err := tx.Exec(s.q(`UPDATE secret_keys SET active=0 WHERE owner=? AND service=?`), actor, service); err != nil {
return err
}
_, err := tx.Exec(s.q(`INSERT INTO secret_keys(id,owner,service,label,value,active,created_at) VALUES(?,?,?,?,?,?,?)`),
sec.ID, actor, service, label, value, 1, sec.CreatedAt)
return err
})
if err != nil {
return Secret{}, err
}
return sec, nil
}
func (s *Store) ActivateSecretAccountChecked(actor, id string, allowImplicitDefault bool) error {
return s.withEffectiveAccountWrite(actor, allowImplicitDefault, func(tx *sql.Tx, _ Account) error {
var service string
err := tx.QueryRow(s.q(`SELECT service FROM secret_keys WHERE id=? AND owner=?`), id, actor).Scan(&service)
if err == sql.ErrNoRows {
return ErrNotFound
}
if err != nil {
return err
}
if _, err := tx.Exec(s.q(`UPDATE secret_keys SET active=0 WHERE owner=? AND service=?`), actor, service); err != nil {
return err
}
res, err := tx.Exec(s.q(`UPDATE secret_keys SET active=1 WHERE id=? AND owner=?`), id, actor)
if err != nil {
return err
}
if n, err := res.RowsAffected(); err != nil {
return err
} else if n != 1 {
return ErrNotFound
}
return nil
})
}
func (s *Store) RenameSecretAccountChecked(actor, id, label string, allowImplicitDefault bool) error {
label = strings.TrimSpace(label)
if label == "" {
return fmt.Errorf("store: label required")
}
return s.withEffectiveAccountWrite(actor, allowImplicitDefault, func(tx *sql.Tx, _ Account) error {
res, err := tx.Exec(s.q(`UPDATE secret_keys SET label=? WHERE id=? AND owner=?`), label, id, actor)
if err != nil {
return err
}
if n, err := res.RowsAffected(); err != nil {
return err
} else if n != 1 {
return ErrNotFound
}
return nil
})
}
func (s *Store) DeleteSecretAccountChecked(actor, id string, allowImplicitDefault bool) error {
return s.withEffectiveAccountWrite(actor, allowImplicitDefault, func(tx *sql.Tx, _ Account) error {
res, err := tx.Exec(s.q(`DELETE FROM secret_keys WHERE id=? AND owner=?`), id, actor)
if err != nil {
return err
}
if n, err := res.RowsAffected(); err != nil {
return err
} else if n != 1 {
return ErrNotFound
}
return nil
})
}
func (s *Store) HasSecret(owner, service string) (bool, error) {
var n int
err := s.db.QueryRow(s.q(`SELECT COUNT(1) FROM secret_keys WHERE owner=? AND service=?`), owner, service).Scan(&n)
return n > 0, err
}
// SaveSnapshotIfChanged 在正文与最近一份不同时留档。相同则跳过。
func (s *Store) SaveSnapshotIfChanged(owner, scope, body, reason string) (bool, error) {
var prev string
err := s.db.QueryRow(s.q(`SELECT body FROM settings_snapshots WHERE owner=? AND scope=? ORDER BY created_at DESC LIMIT 1`), owner, scope).Scan(&prev)
if err == nil && prev == body {
return false, nil
}
if err != nil && err != sql.ErrNoRows {
return false, err
}
_, err = s.db.Exec(s.q(`INSERT INTO settings_snapshots(id,owner,scope,body,reason,created_at) VALUES(?,?,?,?,?,?)`),
newID("snap"), owner, scope, body, reason, nowISO())
return err == nil, err
}
// SaveSnapshotIfChangedAccountChecked fences the optional post-merge snapshot
// in its own final transaction. A disable between the settings commit and this
// transaction may suppress the snapshot, but cannot admit a late write.
func (s *Store) SaveSnapshotIfChangedAccountChecked(actor, owner, scope, body, reason string, allowImplicitDefault bool) (bool, error) {
var saved bool
err := s.withEffectiveAccountWrite(actor, allowImplicitDefault, func(tx *sql.Tx, account Account) error {
if err := s.dialect.lockScope(tx, owner, scope); err != nil {
return err
}
if err := s.checkSettingsScopeWriteActor(tx, actor, owner, scope, account, allowImplicitDefault); err != nil {
return err
}
var prev string
err := tx.QueryRow(s.q(`SELECT body FROM settings_snapshots WHERE owner=? AND scope=? ORDER BY created_at DESC LIMIT 1`), owner, scope).Scan(&prev)
if err == nil && prev == body {
return nil
}
if err != nil && err != sql.ErrNoRows {
return err
}
_, err = tx.Exec(s.q(`INSERT INTO settings_snapshots(id,owner,scope,body,reason,created_at) VALUES(?,?,?,?,?,?)`),
newID("snap"), owner, scope, body, reason, nowISO())
if err == nil {
saved = true
}
return err
})
return saved && err == nil, err
}
// LatestSnapshotAge 返回最近一份快照的时间。没有快照时 ok 为 false。
func (s *Store) LatestSnapshotAt(owner, scope string) (time.Time, bool) {
var raw string
err := s.db.QueryRow(s.q(`SELECT created_at FROM settings_snapshots WHERE owner=? AND scope=? ORDER BY created_at DESC LIMIT 1`), owner, scope).Scan(&raw)
if err != nil {
return time.Time{}, false
}
t, err := time.Parse(time.RFC3339Nano, raw)
if err != nil {
return time.Time{}, false
}
return t, true
}
func (s *Store) SetSessionNote(owner, id, note string) error {
res, err := s.db.Exec(s.q(`UPDATE conversations SET session_note=?, updated_at=? WHERE id=? AND owner=?`), note, nowISO(), id, owner)
if err != nil {
return err
}
n, _ := res.RowsAffected()
if n == 0 {
return ErrNotFound
}
return nil
}
func (s *Store) SetMessageFlags(id string, excluded bool, group string, index int) error {
ex := 0
if excluded {
ex = 1
}
_, err := s.db.Exec(s.q(`UPDATE messages SET excluded=?, variant_group=?, variant_index=? WHERE id=?`), ex, group, index, id)
return err
}
// ExcludeRunGroup 把这条消息以及同一 run 的消息一起标成排除或恢复。
// 工具调用和结果落在同一条助手消息里,同一 run 的其它落库消息一并处理。
func (s *Store) ExcludeRunGroup(id string, excluded bool) error {
var runID string
err := s.db.QueryRow(s.q(`SELECT run_id FROM messages WHERE id=?`), id).Scan(&runID)
if err != nil {
return ErrNotFound
}
ex := 0
if excluded {
ex = 1
}
if _, err := s.db.Exec(s.q(`UPDATE messages SET excluded=? WHERE id=?`), ex, id); err != nil {
return err
}
if runID == "" {
return nil
}
_, err = s.db.Exec(s.q(`UPDATE messages SET excluded=? WHERE run_id=?`), ex, runID)
return err
}
// SelectVariant 让同一组里只有这条候选进入上下文。
func (s *Store) SelectVariant(id string) error {
var group, conv string
err := s.db.QueryRow(s.q(`SELECT variant_group, conversation_id FROM messages WHERE id=?`), id).Scan(&group, &conv)
if err != nil {
return ErrNotFound
}
if group == "" {
return s.ExcludeRunGroup(id, false)
}
if _, err := s.db.Exec(s.q(`UPDATE messages SET excluded=1 WHERE conversation_id=? AND variant_group=?`), conv, group); err != nil {
return err
}
_, err = s.db.Exec(s.q(`UPDATE messages SET excluded=0 WHERE id=?`), id)
return err
}
func (s *Store) SetRegenPending(conversationID, group string, index int) error {
res, err := s.db.Exec(s.q(`UPDATE regen_pending SET variant_group=?, variant_index=? WHERE conversation_id=?`), group, index, conversationID)
if err != nil {
return err
}
n, _ := res.RowsAffected()
if n > 0 {
return nil
}
_, err = s.db.Exec(s.q(`INSERT INTO regen_pending(conversation_id, variant_group, variant_index) VALUES(?,?,?)`), conversationID, group, index)
return err
}
func (s *Store) TakeRegenPending(conversationID string) (group string, index int, ok bool) {
err := s.db.QueryRow(s.q(`SELECT variant_group, variant_index FROM regen_pending WHERE conversation_id=?`), conversationID).Scan(&group, &index)
if err != nil {
return "", 0, false
}
_, _ = s.db.Exec(s.q(`DELETE FROM regen_pending WHERE conversation_id=?`), conversationID)
return group, index, true
}
// PeekRegenPending leaves the marker in place until settlement commits its
// assistant message, avoiding a disable/fence between consumption and settle.
func (s *Store) PeekRegenPending(conversationID string) (group string, index int, ok bool) {
err := s.db.QueryRow(s.q(`SELECT variant_group, variant_index FROM regen_pending WHERE conversation_id=?`), conversationID).Scan(&group, &index)
return group, index, err == nil
}
func (s *Store) ListContextEntries(owner, projectID, conversationID string) ([]ContextEntry, error) {
rows, err := s.db.Query(s.q(`SELECT id,project_id,conversation_id,keys_json,secondary,logic,content,constant,scan_depth,sort_order,budget_chars,enabled
FROM context_entries WHERE owner=? ORDER BY sort_order, created_at`), owner)
if err != nil {
return nil, err
}
defer rows.Close()
out := []ContextEntry{}
for rows.Next() {
e, err := scanEntry(rows)
if err != nil {
return nil, err
}
if e.ProjectID != "" && projectID != "" && e.ProjectID != projectID {
continue
}
if e.ConversationID != "" && conversationID != "" && e.ConversationID != conversationID {
continue
}
out = append(out, e)
}
return out, rows.Err()
}
func scanEntry(rows interface{ Scan(...any) error }) (ContextEntry, error) {
var e ContextEntry
var keys, logic string
var constant, enabled int
err := rows.Scan(&e.ID, &e.ProjectID, &e.ConversationID, &keys, &e.Secondary, &logic, &e.Content, &constant, &e.ScanDepth, &e.SortOrder, &e.BudgetChars, &enabled)
if err != nil {
return e, err
}
_ = json.Unmarshal([]byte(keys), &e.Keys)
if e.Keys == nil {
e.Keys = []string{}
}
e.Logic = logic
if e.Logic == "" {
e.Logic = "and"
}
e.Constant = constant == 1
e.Enabled = enabled == 1
return e, nil
}
func (s *Store) SaveContextEntry(owner string, e ContextEntry) (ContextEntry, error) {
if strings.TrimSpace(e.Content) == "" {
return ContextEntry{}, fmt.Errorf("store: entry content required")
}
if e.Logic != "and" && e.Logic != "or" && e.Logic != "not" {
e.Logic = "and"
}
if e.ScanDepth <= 0 {
e.ScanDepth = 4
}
if e.BudgetChars <= 0 {
e.BudgetChars = 2000
}
raw, _ := json.Marshal(e.Keys)
constant, enabled := 0, 0
if e.Constant {
constant = 1
}
if e.Enabled || e.ID == "" {
enabled = 1
e.Enabled = true
}
if e.ID == "" {
e.ID = newID("ctx")
_, err := s.db.Exec(s.q(`INSERT INTO context_entries(id,owner,project_id,conversation_id,keys_json,secondary,logic,content,constant,scan_depth,sort_order,budget_chars,enabled,created_at)
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)`),
e.ID, owner, e.ProjectID, e.ConversationID, string(raw), e.Secondary, e.Logic, e.Content, constant, e.ScanDepth, e.SortOrder, e.BudgetChars, enabled, nowISO())
return e, err
}
res, err := s.db.Exec(s.q(`UPDATE context_entries SET project_id=?, conversation_id=?, keys_json=?, secondary=?, logic=?, content=?, constant=?, scan_depth=?, sort_order=?, budget_chars=?, enabled=? WHERE id=? AND owner=?`),
e.ProjectID, e.ConversationID, string(raw), e.Secondary, e.Logic, e.Content, constant, e.ScanDepth, e.SortOrder, e.BudgetChars, enabled, e.ID, owner)
if err != nil {
return e, err
}
n, _ := res.RowsAffected()
if n == 0 {
return e, ErrNotFound
}
return e, nil
}
func (s *Store) DeleteContextEntry(owner, id string) error {
res, err := s.db.Exec(s.q(`DELETE FROM context_entries WHERE id=? AND owner=?`), id, owner)
if err != nil {
return err
}
n, _ := res.RowsAffected()
if n == 0 {
return ErrNotFound
}
return nil
}
func (s *Store) SaveContextEntryAccountChecked(actor string, e ContextEntry, allowImplicitDefault bool) (ContextEntry, error) {
if strings.TrimSpace(e.Content) == "" {
return ContextEntry{}, fmt.Errorf("store: entry content required")
}
if e.Logic != "and" && e.Logic != "or" && e.Logic != "not" {
e.Logic = "and"
}
if e.ScanDepth <= 0 {
e.ScanDepth = 4
}
if e.BudgetChars <= 0 {
e.BudgetChars = 2000
}
raw, _ := json.Marshal(e.Keys)
constant, enabled := 0, 0
if e.Constant {
constant = 1
}
if e.Enabled || e.ID == "" {
enabled = 1
e.Enabled = true
}
creating := e.ID == ""
if creating {
e.ID = newID("ctx")
}
err := s.withEffectiveAccountWrite(actor, allowImplicitDefault, func(tx *sql.Tx, _ Account) error {
if creating {
_, err := tx.Exec(s.q(`INSERT INTO context_entries(id,owner,project_id,conversation_id,keys_json,secondary,logic,content,constant,scan_depth,sort_order,budget_chars,enabled,created_at)
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)`),
e.ID, actor, e.ProjectID, e.ConversationID, string(raw), e.Secondary, e.Logic, e.Content, constant, e.ScanDepth, e.SortOrder, e.BudgetChars, enabled, nowISO())
return err
}
res, err := tx.Exec(s.q(`UPDATE context_entries SET project_id=?, conversation_id=?, keys_json=?, secondary=?, logic=?, content=?, constant=?, scan_depth=?, sort_order=?, budget_chars=?, enabled=? WHERE id=? AND owner=?`),
e.ProjectID, e.ConversationID, string(raw), e.Secondary, e.Logic, e.Content, constant, e.ScanDepth, e.SortOrder, e.BudgetChars, enabled, e.ID, actor)
if err != nil {
return err
}
if n, err := res.RowsAffected(); err != nil {
return err
} else if n != 1 {
return ErrNotFound
}
return nil
})
if err != nil {
return ContextEntry{}, err
}
return e, nil
}
func (s *Store) DeleteContextEntryAccountChecked(actor, id string, allowImplicitDefault bool) error {
return s.withEffectiveAccountWrite(actor, allowImplicitDefault, func(tx *sql.Tx, _ Account) error {
res, err := tx.Exec(s.q(`DELETE FROM context_entries WHERE id=? AND owner=?`), id, actor)
if err != nil {
return err
}
if n, err := res.RowsAffected(); err != nil {
return err
} else if n != 1 {
return ErrNotFound
}
return nil
})
}
func (s *Store) ListUserCommands(owner string) ([]UserCommand, error) {
rows, err := s.db.Query(s.q(`SELECT id,name,title,template,created_at FROM user_commands WHERE owner=? ORDER BY name`), owner)
if err != nil {
return nil, err
}
defer rows.Close()
out := []UserCommand{}
for rows.Next() {
var c UserCommand
if err := rows.Scan(&c.ID, &c.Name, &c.Title, &c.Template, &c.CreatedAt); err != nil {
return nil, err
}
out = append(out, c)
}
return out, rows.Err()
}
func (s *Store) SaveUserCommand(owner string, c UserCommand) (UserCommand, error) {
c.Name = strings.TrimSpace(c.Name)
if !strings.HasPrefix(c.Name, "/") {
c.Name = "/" + c.Name
}
if c.Title == "" {
c.Title = c.Name
}
if strings.TrimSpace(c.Template) == "" {
return UserCommand{}, fmt.Errorf("store: command template required")
}
if c.ID == "" {
c.ID = newID("cmd")
c.CreatedAt = nowISO()
_, err := s.db.Exec(s.q(`INSERT INTO user_commands(id,owner,name,title,template,created_at) VALUES(?,?,?,?,?,?)`),
c.ID, owner, c.Name, c.Title, c.Template, c.CreatedAt)
return c, err
}
res, err := s.db.Exec(s.q(`UPDATE user_commands SET name=?, title=?, template=? WHERE id=? AND owner=?`), c.Name, c.Title, c.Template, c.ID, owner)
if err != nil {
return c, err
}
n, _ := res.RowsAffected()
if n == 0 {
return c, ErrNotFound
}
return c, nil
}
func (s *Store) DeleteUserCommand(owner, id string) error {
res, err := s.db.Exec(s.q(`DELETE FROM user_commands WHERE id=? AND owner=?`), id, owner)
if err != nil {
return err
}
n, _ := res.RowsAffected()
if n == 0 {
return ErrNotFound
}
return nil
}
func (s *Store) SaveUserCommandAccountChecked(actor string, c UserCommand, allowImplicitDefault bool) (UserCommand, error) {
c.Name = strings.TrimSpace(c.Name)
if !strings.HasPrefix(c.Name, "/") {
c.Name = "/" + c.Name
}
if c.Title == "" {
c.Title = c.Name
}
if strings.TrimSpace(c.Template) == "" {
return UserCommand{}, fmt.Errorf("store: command template required")
}
creating := c.ID == ""
if creating {
c.ID = newID("cmd")
c.CreatedAt = nowISO()
}
err := s.withEffectiveAccountWrite(actor, allowImplicitDefault, func(tx *sql.Tx, _ Account) error {
if creating {
_, err := tx.Exec(s.q(`INSERT INTO user_commands(id,owner,name,title,template,created_at) VALUES(?,?,?,?,?,?)`),
c.ID, actor, c.Name, c.Title, c.Template, c.CreatedAt)
return err
}
res, err := tx.Exec(s.q(`UPDATE user_commands SET name=?, title=?, template=? WHERE id=? AND owner=?`), c.Name, c.Title, c.Template, c.ID, actor)
if err != nil {
return err
}
if n, err := res.RowsAffected(); err != nil {
return err
} else if n != 1 {
return ErrNotFound
}
return nil
})
if err != nil {
return UserCommand{}, err
}
return c, nil
}
// DeleteUserCommandAccountChecked returns the deleted name from the guarded
// transaction so the caller can unregister that exact command from its Bus.
func (s *Store) DeleteUserCommandAccountChecked(actor, id string, allowImplicitDefault bool) (UserCommand, error) {
var deleted UserCommand
err := s.withEffectiveAccountWrite(actor, allowImplicitDefault, func(tx *sql.Tx, _ Account) error {
query := s.q(`SELECT id,name,title,template,created_at FROM user_commands WHERE id=? AND owner=?`)
if s.Dialect() == "postgres" {
query += ` FOR UPDATE`
}
err := tx.QueryRow(query, id, actor).Scan(&deleted.ID, &deleted.Name, &deleted.Title, &deleted.Template, &deleted.CreatedAt)
if err == sql.ErrNoRows {
return ErrNotFound
}
if err != nil {
return err
}
res, err := tx.Exec(s.q(`DELETE FROM user_commands WHERE id=? AND owner=?`), id, actor)
if err != nil {
return err
}
if n, err := res.RowsAffected(); err != nil {
return err
} else if n != 1 {
return ErrNotFound
}
return nil
})
if err != nil {
return UserCommand{}, err
}
return deleted, nil
}
// CheckEffectiveAccountForExternalEffect is a final state check immediately
// before a Bus mutation. The Bus is outside the account database transaction:
// disable can still commit between this check and the in-memory effect.
func (s *Store) CheckEffectiveAccountForExternalEffect(actor string, allowImplicitDefault bool) error {
return s.withEffectiveAccountWrite(actor, allowImplicitDefault, func(*sql.Tx, Account) error { return nil })
}
// MessageByID 读取一条消息。不存在时返回 ErrNotFound。
func (s *Store) MessageByID(id string) (core.Message, error) {
var m core.Message
var parts, ca string
var excluded, index int
err := s.db.QueryRow(s.q(`SELECT id,conversation_id,run_id,role,parts,created_at,excluded,variant_group,variant_index FROM messages WHERE id=?`), id).
Scan(&m.ID, &m.ConversationID, &m.RunID, &m.Role, &parts, &ca, &excluded, &m.VariantGroup, &index)
if err != nil {
return m, ErrNotFound
}
m.Role = core.Role(m.Role)
m.Parts, _ = unmarshalJSON[[]core.Part](parts)
m.CreatedAt, _ = parseTime(ca)
m.Excluded = excluded == 1
m.VariantIndex = index
return m, nil
}