Files
Cyrene_v2/internal/runtime/history/message_ops.go
T
AskaEth 09fba77f87
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
docs(comments): 注释中文化 · internal/runtime/history(15 文件,纯替换)
- 只翻注释/文档字符串:剥离注释后代码行逐字节不变(机械校验只剩 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)
2026-10-04 12:28:48 +08:00

118 lines
4.8 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 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)
}