package backthink import ( "context" "database/sql" "fmt" "log/slog" "time" "git.yeij.top/AskaEth/Cyrene_v2/internal/logger" ) // defaultListLimit 是调用方传入非正 limit 时,List 返回的 // 最近记录条数。 const defaultListLimit = 50 // 思考记录存储的 DDL。at/created_at 遵循项目时间戳 // 约定(带时区的 RFC3339);trigger 与 conclusion 是面板渲染的 // 自由文本字段;push_* 列携带分解后的 PushResult, // 使 List 不必解析 blob。主键是项目标准的自增 id。 const createRecordsSQL = ` CREATE TABLE IF NOT EXISTS backthink_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, trigger TEXT NOT NULL, at TEXT NOT NULL, conclusion TEXT NOT NULL, should_push INTEGER NOT NULL, push_pushed INTEGER NOT NULL DEFAULT 0, push_route TEXT NOT NULL, push_detail TEXT NOT NULL DEFAULT '', push_error TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) )` const createRecordsIndexSQL = ` CREATE INDEX IF NOT EXISTS idx_backthink_records_at ON backthink_records(at)` const insertRecordSQL = ` INSERT INTO backthink_records (trigger, at, conclusion, should_push, push_pushed, push_route, push_detail, push_error) VALUES (?, ?, ?, ?, ?, ?, ?, ?)` const selectRecordsSQL = ` SELECT id, trigger, at, conclusion, should_push, push_pushed, push_route, push_detail, push_error FROM backthink_records ORDER BY id DESC LIMIT ?` // Record 是一轮已持久化的思考(design/12 §4),由 Store.List // 与服务的 List 返回。Trigger 是来源子系统标签,At 是决策 // 时间戳,Conclusion 是运行时的输出,ShouldPush 是打扰决策, // PushResult 是推送如何(或是否)发出。 type Record struct { ID int64 Trigger string At time.Time Conclusion string ShouldPush bool PushResult PushResult } // Store 把思考记录持久化到 SQLite。它在 // NewStore 中建表与索引,因此本模块自包含(无需迁移记账), // 与 channel/session 自建表的做法一致。它凭借底层的 *sql.DB // 而并发安全。 type Store struct { db *sql.DB log *slog.Logger } // NewStore 构建 Store,确保 backthink_records 表与索引存在。 // db 为 nil 时返回 ErrNilDB。ctx 用于 DDL。 func NewStore(ctx context.Context, db *sql.DB, log *slog.Logger) (*Store, error) { if db == nil { return nil, ErrNilDB } log = logger.WithSource(baseLog(log), "services/backthink") if _, err := db.ExecContext(ctx, createRecordsSQL); err != nil { return nil, fmt.Errorf("backthink: ensure backthink_records: %w", err) } if _, err := db.ExecContext(ctx, createRecordsIndexSQL); err != nil { return nil, fmt.Errorf("backthink: ensure backthink_records index: %w", err) } return &Store{db: db, log: log}, nil } // Append 持久化 r 并返回新记录 id。 func (s *Store) Append(ctx context.Context, r Record) (int64, error) { res, err := s.db.ExecContext(ctx, insertRecordSQL, r.Trigger, r.At.UTC().Format(time.RFC3339Nano), r.Conclusion, boolInt(r.ShouldPush), boolInt(r.PushResult.Pushed), string(r.PushResult.Route), r.PushResult.Detail, r.PushResult.Err, ) if err != nil { return 0, fmt.Errorf("backthink: insert record: %w", err) } id, err := res.LastInsertId() if err != nil { return 0, fmt.Errorf("backthink: record id: %w", err) } if s.log != nil { s.log.Debug("backthink record appended", "id", id, "trigger", r.Trigger, "should_push", r.ShouldPush, "route", r.PushResult.Route) } return id, nil } // List 返回最近的记录,最新的在前。非正的 limit 选用 // defaultListLimit。 func (s *Store) List(ctx context.Context, limit int) ([]Record, error) { if limit <= 0 { limit = defaultListLimit } if err := ctx.Err(); err != nil { return nil, err } rows, err := s.db.QueryContext(ctx, selectRecordsSQL, limit) if err != nil { return nil, fmt.Errorf("backthink: list records: %w", err) } defer rows.Close() records := make([]Record, 0, limit) for rows.Next() { var ( r Record at string route string pushed int shouldPush int ) if err := rows.Scan( &r.ID, &r.Trigger, &at, &r.Conclusion, &shouldPush, &pushed, &route, &r.PushResult.Detail, &r.PushResult.Err, ); err != nil { return nil, fmt.Errorf("backthink: scan record: %w", err) } r.ShouldPush = intBool(shouldPush) r.PushResult.Pushed = intBool(pushed) r.PushResult.Route = Route(route) r.At = parseTime(at) records = append(records, r) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("backthink: iterate records: %w", err) } return records, nil } // boolInt 把 bool 映射为 SQLite 的 INTEGER 1/0 表示。 func boolInt(b bool) int { if b { return 1 } return 0 } // intBool 把 SQLite 的 INTEGER 映射为 bool。 func intBool(n int) bool { return n != 0 } // parseTime 解析存储的 RFC3339(带纳秒/时区)时间戳,失败时回退到 // 零值时间,因此格式错乱的行绝不会破坏列表查询。 func parseTime(s string) time.Time { for _, layout := range []string{time.RFC3339Nano, time.RFC3339} { if t, err := time.Parse(layout, s); err == nil { return t } } return time.Time{} }