- 只翻注释/文档字符串:`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
182 lines
5.2 KiB
Go
182 lines
5.2 KiB
Go
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{}
|
||
}
|