- 只翻注释/文档字符串:剥离注释后代码行逐字节不变(机械校验只剩 4 条行尾注释的翻译,代码前缀全等) - 交叉引用(design/16 §4.6、D4/B3、0010 迁移文件名)逐字保留 - 保留未译技术词:DDL/rowid/LIKE/LEFT JOIN/DTO/rune/WS/actor 及全部标识符与表名 - 顺带副作用:store.go、session_persona_test.go 由注释文字改变而变得 gofmt 干净(未运行 gofmt -w)
118 lines
4.8 KiB
Go
118 lines
4.8 KiB
Go
package history
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"errors"
|
||
"fmt"
|
||
"strings"
|
||
)
|
||
|
||
// 本文件负责 Android 协同批次 ③a 背后的单条消息操作(D2 搜索、D3 编辑/删除),
|
||
// 以及 D 删除广播所需的按 id 读取。它们都严格限定在会话作用域或 id 作用域:
|
||
// 消息只能经由它所属的会话(Search)或它自己不透明的 id(GetMessage/
|
||
// UpdateMessage/DeleteMessage)到达,因此隔离的群聊/陌生人会话绝不浮现在
|
||
// 另一个会话的结果中(项目红线)。
|
||
|
||
// Search 按最新优先返回 sessionID 中内容包含 query 的至多 limit 条消息。匹配
|
||
// 是 ASCII 大小写不敏感的 LIKE(SQLite 对 LIKE 的默认行为),并把 query 中的
|
||
// %/_/\ 转义使它们按字面匹配;before 与 List 是同一个毫秒游标(严格早于
|
||
// before),limit 钳制到 [1, maxPageLimit]。rowid 打破 created_at 的并列,使
|
||
// 同一毫秒的消息保持稳定顺序。空 query 返回空(非 nil)切片 —— 调用方改用
|
||
// List 做普通翻页。Search 绝不跨越会话边界。
|
||
func (s *Store) Search(ctx context.Context, sessionID, query string, before int64, limit int) ([]Message, error) {
|
||
limit = clampLimit(limit)
|
||
if query == "" {
|
||
return make([]Message, 0), nil
|
||
}
|
||
sqlText := `SELECT id, session_id, role, content, mode, mood, image, thinking, platform, device_id, sender_name, adapter_id, persona_id, created_at
|
||
FROM chat_messages WHERE session_id = ? AND content LIKE ? ESCAPE '\'`
|
||
args := []any{sessionID, "%" + escapeLike(query) + "%"}
|
||
if before > 0 {
|
||
sqlText += ` AND created_at < ?`
|
||
args = append(args, before)
|
||
}
|
||
sqlText += ` ORDER BY created_at DESC, rowid DESC LIMIT ?`
|
||
args = append(args, limit)
|
||
|
||
rows, err := s.db.QueryContext(ctx, sqlText, args...)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("history: search %s: %w", sessionID, err)
|
||
}
|
||
defer func() { _ = rows.Close() }()
|
||
|
||
msgs := make([]Message, 0)
|
||
for rows.Next() {
|
||
m, err := scanMessage(rows)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("history: scan search %s: %w", sessionID, err)
|
||
}
|
||
msgs = append(msgs, m)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, fmt.Errorf("history: iterate search %s: %w", sessionID, err)
|
||
}
|
||
return msgs, nil
|
||
}
|
||
|
||
// UpdateMessage 替换 id 所指消息的内容并返回更新后的消息(与 AppendFull
|
||
// 一样,使调用方能无需二次读取就返回/广播它)。只有 content 变化;role、
|
||
// platform、时间戳及其余字段保持已存值。未知 id 返回 ErrMessageNotFound。
|
||
func (s *Store) UpdateMessage(ctx context.Context, id, content string) (Message, error) {
|
||
res, err := s.db.ExecContext(ctx, `UPDATE chat_messages SET content = ? WHERE id = ?`, content, id)
|
||
if err != nil {
|
||
return Message{}, fmt.Errorf("history: update message %s: %w", id, err)
|
||
}
|
||
n, err := res.RowsAffected()
|
||
if err != nil {
|
||
return Message{}, fmt.Errorf("history: update message %s rows: %w", id, err)
|
||
}
|
||
if n == 0 {
|
||
return Message{}, fmt.Errorf("history: update message %s: %w", id, ErrMessageNotFound)
|
||
}
|
||
return s.messageByID(ctx, id)
|
||
}
|
||
|
||
// DeleteMessage 删除单条消息。未知 id 返回 ErrMessageNotFound。
|
||
func (s *Store) DeleteMessage(ctx context.Context, id string) error {
|
||
res, err := s.db.ExecContext(ctx, `DELETE FROM chat_messages WHERE id = ?`, id)
|
||
if err != nil {
|
||
return fmt.Errorf("history: delete message %s: %w", id, err)
|
||
}
|
||
n, err := res.RowsAffected()
|
||
if err != nil {
|
||
return fmt.Errorf("history: delete message %s rows: %w", id, err)
|
||
}
|
||
if n == 0 {
|
||
return fmt.Errorf("history: delete message %s: %w", id, ErrMessageNotFound)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// GetMessage 读取 id 所指消息,没有该行时返回 ErrMessageNotFound。它是传输层
|
||
// 在移除消息之前使用的按 id 读取:DeleteMessage 之后已无从再问,而那条告知
|
||
// 其他客户端丢弃该行的广播是按会话划分的。
|
||
func (s *Store) GetMessage(ctx context.Context, id string) (Message, error) {
|
||
return s.messageByID(ctx, id)
|
||
}
|
||
|
||
// messageByID 按 id 读取一条消息,行不存在时返回 ErrMessageNotFound。
|
||
func (s *Store) messageByID(ctx context.Context, id string) (Message, error) {
|
||
row := s.db.QueryRowContext(ctx, `SELECT id, session_id, role, content, mode, mood, image, thinking, platform, device_id, sender_name, adapter_id, persona_id, created_at
|
||
FROM chat_messages WHERE id = ?`, id)
|
||
m, err := scanMessage(row)
|
||
if err != nil {
|
||
if errors.Is(err, sql.ErrNoRows) {
|
||
return Message{}, fmt.Errorf("history: read message %s: %w", id, ErrMessageNotFound)
|
||
}
|
||
return Message{}, fmt.Errorf("history: read message %s: %w", id, err)
|
||
}
|
||
return m, nil
|
||
}
|
||
|
||
// escapeLike 转义用户查询中的 LIKE 通配符,使 %、_ 与 \ 按字面匹配(调用方
|
||
// 配合 ESCAPE '\' 使用)。它在 %...% 包裹之前应用。
|
||
func escapeLike(query string) string {
|
||
return strings.NewReplacer(`\`, `\\`, `%`, `\%`, `_`, `\_`).Replace(query)
|
||
}
|