Files
cursor-byok/cursor-backend/store.go
T
2026-08-13 22:01:18 +08:00

244 lines
7.2 KiB
Go

// store.go 管理调试捕获的内存索引、SQLite 持久化和订阅通知。
package main
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
_ "modernc.org/sqlite"
)
// exchangeStore 保存有限内存窗口以及可选的持久化数据库连接。
type exchangeStore struct {
mu sync.RWMutex
max int
order []string
exchanges map[string]*Exchange
subscribers map[chan storeEvent]struct{}
db *sql.DB
databasePath string
lastError string
}
// newExchangeStore 创建仅使用内存的捕获存储。
func newExchangeStore(max int) *exchangeStore {
return &exchangeStore{
max: max,
exchanges: make(map[string]*Exchange),
subscribers: make(map[chan storeEvent]struct{}),
}
}
// newPersistentExchangeStore 创建 SQLite 持久化捕获存储并恢复最近记录。
func newPersistentExchangeStore(path string, max int) (*exchangeStore, error) {
path = strings.TrimSpace(path)
if path == "" {
return nil, fmt.Errorf("SQLite 数据库路径不能为空")
}
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return nil, fmt.Errorf("创建 SQLite 数据目录失败: %w", err)
}
db, err := sql.Open("sqlite", path)
if err != nil {
return nil, fmt.Errorf("打开 SQLite 数据库失败: %w", err)
}
db.SetMaxOpenConns(1)
db.SetMaxIdleConns(1)
store := newExchangeStore(max)
store.db = db
store.databasePath = path
if err := store.initializeDatabase(context.Background()); err != nil {
_ = db.Close()
return nil, err
}
if err := store.backfillDecodedExchanges(context.Background()); err != nil {
_ = db.Close()
return nil, err
}
if err := store.loadRecent(context.Background()); err != nil {
_ = db.Close()
return nil, err
}
return store, nil
}
// backfillDecodedExchanges 为旧记录补齐解码视图并写回数据库。
func (store *exchangeStore) backfillDecodedExchanges(ctx context.Context) error {
rows, err := store.db.QueryContext(ctx, "SELECT payload_json FROM exchanges")
if err != nil {
return fmt.Errorf("读取待回填的 SQLite 抓包记录失败: %w", err)
}
var exchanges []Exchange
for rows.Next() {
var payload []byte
if err := rows.Scan(&payload); err != nil {
_ = rows.Close()
return err
}
var exchange Exchange
if err := json.Unmarshal(payload, &exchange); err != nil {
_ = rows.Close()
return fmt.Errorf("解析待回填的 SQLite 抓包记录失败: %w", err)
}
if hydrateStoredExchange(&exchange) {
exchanges = append(exchanges, exchange)
}
}
if err := rows.Close(); err != nil {
return err
}
if err := rows.Err(); err != nil {
return err
}
if len(exchanges) == 0 {
return nil
}
tx, err := store.db.BeginTx(ctx, nil)
if err != nil {
return err
}
committed := false
defer func() {
if !committed {
_ = tx.Rollback()
}
}()
for index := range exchanges {
payload, marshalErr := json.Marshal(&exchanges[index])
if marshalErr != nil {
return marshalErr
}
if _, err := tx.ExecContext(ctx, `UPDATE exchanges SET payload_json = ?, conversation_id = ?,
request_id = ?, updated_at_ms = ? WHERE id = ?`, payload, exchanges[index].ConversationID,
exchanges[index].RequestID, time.Now().UnixMilli(), exchanges[index].ID); err != nil {
return err
}
}
if err := tx.Commit(); err != nil {
return err
}
committed = true
return nil
}
// initializeDatabase 创建调试器使用的 SQLite 表结构。
func (store *exchangeStore) initializeDatabase(ctx context.Context) error {
for _, statement := range []string{
"PRAGMA journal_mode = WAL",
"PRAGMA busy_timeout = 5000",
"PRAGMA secure_delete = ON",
`CREATE TABLE IF NOT EXISTS exchanges (
id TEXT PRIMARY KEY,
started_at_ms INTEGER NOT NULL,
conversation_id TEXT NOT NULL DEFAULT '',
request_id TEXT NOT NULL DEFAULT '',
state TEXT NOT NULL DEFAULT '',
request_bytes INTEGER NOT NULL DEFAULT 0,
response_bytes INTEGER NOT NULL DEFAULT 0,
payload_json BLOB NOT NULL,
updated_at_ms INTEGER NOT NULL
)`,
"CREATE INDEX IF NOT EXISTS exchanges_conversation_started_idx ON exchanges(conversation_id, started_at_ms DESC)",
"CREATE INDEX IF NOT EXISTS exchanges_request_idx ON exchanges(request_id)",
} {
if _, err := store.db.ExecContext(ctx, statement); err != nil {
return fmt.Errorf("初始化 SQLite 数据库失败: %w", err)
}
}
return nil
}
// loadRecent 从数据库恢复内存窗口中的最新捕获。
func (store *exchangeStore) loadRecent(ctx context.Context) error {
rows, err := store.db.QueryContext(ctx, `SELECT payload_json, conversation_id
FROM exchanges ORDER BY started_at_ms DESC, id DESC LIMIT ?`, store.max)
if err != nil {
return fmt.Errorf("读取 SQLite 抓包记录失败: %w", err)
}
defer rows.Close()
for rows.Next() {
var payload []byte
var conversationID string
if err := rows.Scan(&payload, &conversationID); err != nil {
return err
}
var exchange Exchange
if err := json.Unmarshal(payload, &exchange); err != nil {
return fmt.Errorf("解析 SQLite 抓包记录失败: %w", err)
}
exchange.ConversationID = conversationID
store.exchanges[exchange.ID] = &exchange
store.order = append(store.order, exchange.ID)
}
return rows.Err()
}
// create 添加一条新的捕获并通知订阅者。
func (store *exchangeStore) create(exchange *Exchange) {
store.mu.Lock()
store.exchanges[exchange.ID] = exchange
store.order = append([]string{exchange.ID}, store.order...)
for len(store.order) > store.max {
oldest := store.order[len(store.order)-1]
store.order = store.order[:len(store.order)-1]
delete(store.exchanges, oldest)
}
store.persistLocked(exchange)
store.mu.Unlock()
store.publish(storeEvent{Type: "created", ID: exchange.ID})
}
// update 持久化修改并发布最终捕获快照。
func (store *exchangeStore) update(id string, apply func(*Exchange)) {
store.updateWithPersistence(id, apply, true)
}
// updateTransient 只更新内存并发布流式过程快照。
func (store *exchangeStore) updateTransient(id string, apply func(*Exchange)) {
store.updateWithPersistence(id, apply, false)
}
// updateWithPersistence 在统一锁内完成修改、关联和可选持久化。
func (store *exchangeStore) updateWithPersistence(id string, apply func(*Exchange), persist bool) {
store.mu.Lock()
exchange := store.exchanges[id]
if exchange == nil && store.db != nil {
var err error
exchange, err = store.loadPersistedLocked(id)
if err != nil {
store.lastError = err.Error()
}
if exchange != nil {
store.exchanges[id] = exchange
store.order = append([]string{id}, store.order...)
for len(store.order) > store.max {
oldest := store.order[len(store.order)-1]
store.order = store.order[:len(store.order)-1]
delete(store.exchanges, oldest)
}
}
}
if exchange != nil {
previousRequestID := exchange.RequestID
previousConversationID := exchange.ConversationID
apply(exchange)
if exchange.RequestID != previousRequestID || exchange.ConversationID != previousConversationID {
store.associateConversationLocked(exchange)
}
if persist {
store.persistLocked(exchange)
}
}
store.mu.Unlock()
store.publish(storeEvent{Type: "updated", ID: id})
}
// summaries 返回按时间倒序排列的请求摘要。