mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-08-17 03:27:02 +08:00
244 lines
7.2 KiB
Go
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 返回按时间倒序排列的请求摘要。
|