Files
Cyrene_v2/internal/services/backthink/store.go
T
AskaEth 3dc1232bc2
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
docs(comments): 注释中文化 · internal/runtime/model + internal/services/backthink(54 文件,纯替换)
- 只翻注释/文档字符串:`go/scanner` 剥注释后按 token 序列+literal 逐 token 比对,**54/54 文件 SAME**;
  改动文件总行数与 HEAD 漂移 = 0(严格 1:1,无合并/拆行)
- 裁决编号与 design 节号逐字保留(03.4.1/03.4.2/03.4.3/14.4.1/14.4.5/07.4.2-4、design/04 §3.1/§3.2 等);
  `chat_template_kwargs` / `enable_thinking` / `reasoning_effort` / `thinking:{type}` 字段名与形状说明未动
- 存疑译法(已登记):`persona` 在叙事语境用「角色」、标识符语境保留 `persona`(仓库既有模块本身混用);
  `rung`→「级」;`provider` 存在「提供商/提供者」两种落笔(可按需一键统一)
- 复核:build/vet 绿 + 两包测试绿;gofmt 干净;残留英文注释 0 条
- 既有存量(未修,只报告):`model/anthropic_test.go` 612 行 >500;`applyAnthropicEvent` 115 行 >80
2026-10-04 16:27:54 +08:00

182 lines
5.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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{}
}