Files
Cyrene_v2/internal/runtime/history/title.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

82 lines
3.3 KiB
Go

package history
import (
"context"
"errors"
"fmt"
"strings"
)
// 本文件负责两条会话标题路径:从第一条用户消息为会话自动命名,以及显式
// 重命名。标题只是展示元数据(design/16 §2.1),绝不用于标识会话、发送者
// 或权限 —— 身份永远来自渠道 actor 元数据 / JWT(项目红线)。
const (
// autoTitleMaxRunes 限制自动生成标题的长度,使会话列表行不会溢出;
// 更长的首条消息会被截断并加省略号。
autoTitleMaxRunes = 20
// titleMainFallback 与 titleSubFallback 用于给首条用户消息没有任何
// 文本的会话(例如纯图片回合)命名,使会话列表不会无名。
titleMainFallback = "主会话"
titleSubFallback = "次级会话"
)
// ErrSessionNotFound 在没有任何 chat_sessions 行携带所请求 id 时由
// RenameSession 返回。
var ErrSessionNotFound = errors.New("history: session not found")
// RenameSession 设置 sessionID 的显示标题。固定主会话(id "web")与子会话
// 都可重命名:与删除不同,重命名不是破坏性的身份变更。未知 id 返回
// ErrSessionNotFound。标题由调用方校验(非空、限长);存储原样持久化它。
func (s *Store) RenameSession(ctx context.Context, sessionID, title string) error {
res, err := s.db.ExecContext(ctx, `UPDATE chat_sessions SET title = ? WHERE id = ?`, title, sessionID)
if err != nil {
return fmt.Errorf("history: rename session %s: %w", sessionID, err)
}
n, err := res.RowsAffected()
if err != nil {
return fmt.Errorf("history: rename session %s rows: %w", sessionID, err)
}
if n == 0 {
return fmt.Errorf("history: rename session %s: %w", sessionID, ErrSessionNotFound)
}
return nil
}
// ensureTitle 用第一条用户消息为会话命名。除非该会话行已存在于
// chat_sessions、其标题仍为空、且尚未为它持久化任何用户消息(因此调用方
// 必须在插入消息之前调用它),否则它是空操作。纯空白的消息回退到会话类型
// 标签。标题只是展示元数据,因此失败只记日志并吞掉,而不是让它伴随的
// 消息写入失败。
func (s *Store) ensureTitle(ctx context.Context, sessionID, content string) {
if sessionID == "" {
return
}
title := deriveTitle(content)
_, err := s.db.ExecContext(ctx, `UPDATE chat_sessions
SET title = CASE WHEN ? = '' THEN CASE WHEN type = ? THEN ? ELSE ? END ELSE ? END
WHERE id = ? AND title = ''
AND NOT EXISTS (SELECT 1 FROM chat_messages WHERE session_id = ? AND role = ?)`,
title, SessionMain, titleMainFallback, titleSubFallback, title,
sessionID, sessionID, RoleUser)
if err != nil && s.log != nil {
s.log.Warn("auto title failed", "err", err, "session_id", sessionID)
}
}
// deriveTitle 把一条用户消息变成会话标题:空白串(含换行)折叠为单个空格,
// 再把结果截断到 autoTitleMaxRunes 个 rune 并加省略号。纯空白的消息得到
// 空串,使 ensureTitle 回退到会话类型标签。
func deriveTitle(content string) string {
fields := strings.Fields(content)
if len(fields) == 0 {
return ""
}
joined := strings.Join(fields, " ")
runes := []rune(joined)
if len(runes) <= autoTitleMaxRunes {
return joined
}
return string(runes[:autoTitleMaxRunes]) + "…"
}