llm(三协议 Provider)/ session(JSONL 持久化)/ tools(Registry、围栏、 builtin、computer)/ cdp(browser)/ skills / mcp / agent(循环、两级压缩、 编排、审批门控)+ 底座 jsonl/textx/execx/diag/scratch/spill/imagecache。 产品装配代码(config 桥接、schedule 工具、memory/plan/retro/prompt) 留在 AutoGo,宿主接入方式见 MIGRATION.md。
90 lines
2.5 KiB
Go
90 lines
2.5 KiB
Go
package session
|
|
|
|
// 会话标题:sidecar 文件 <id>.title(与会话 jsonl 同目录)。
|
|
// 标题本地派生自用户首条消息,零请求、零延迟;TUI 与 WebUI 共用本文件。
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// SafeID 拒绝路径穿越,标题文件只允许落在会话目录内。
|
|
func SafeID(id string) (string, bool) {
|
|
if id == "" || strings.ContainsAny(id, `/\`) || strings.Contains(id, "..") {
|
|
return "", false
|
|
}
|
|
return id, true
|
|
}
|
|
|
|
// TitlePath 标题 sidecar 文件路径。
|
|
func TitlePath(id string) string {
|
|
return filepath.Join(Dir(), id+".title")
|
|
}
|
|
|
|
// LoadTitle 读取标题;无标题返回 ""。
|
|
func LoadTitle(id string) string {
|
|
safe, ok := SafeID(id)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
b, err := os.ReadFile(TitlePath(safe))
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(string(b))
|
|
}
|
|
|
|
// SaveTitle 写入标题(去首尾空白,上限 60 字)。
|
|
func SaveTitle(id, title string) error {
|
|
safe, ok := SafeID(id)
|
|
if !ok {
|
|
return os.ErrInvalid
|
|
}
|
|
title = TruncateRunes(strings.TrimSpace(title), 60)
|
|
if title == "" {
|
|
return os.ErrInvalid
|
|
}
|
|
if err := os.MkdirAll(Dir(), 0o755); err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(TitlePath(safe), []byte(title), 0o644)
|
|
}
|
|
|
|
// TruncateRunes 按 rune 截断。
|
|
func TruncateRunes(s string, n int) string {
|
|
r := []rune(s)
|
|
if len(r) <= n {
|
|
return s
|
|
}
|
|
return string(r[:n])
|
|
}
|
|
|
|
// SanitizeTitle 清洗标题:去空白、去包围引号,折叠空白,截断 24 字。
|
|
func SanitizeTitle(s string) string {
|
|
s = strings.TrimSpace(s)
|
|
s = strings.Trim(s, `"'「」『』【】<>`)
|
|
s = strings.TrimSpace(s)
|
|
s = strings.Join(strings.Fields(s), " ")
|
|
return TruncateRunes(s, 24)
|
|
}
|
|
|
|
// DeriveTitle 从用户首条消息本地派生会话标题:不发请求、零延迟、无失败路径。
|
|
//
|
|
// 此前为标题单独发一次模型调用,代价远大于收益:该模型上常达 ~27s,与主回合并发时
|
|
// 极易超时(30s 上限),导致长回合的会话静默丢标题。而标题只是会话列表里的速记,
|
|
// 用「用户诉求的首行」就足够辨认,没必要为一个标签花掉一次完整往返。
|
|
func DeriveTitle(userMsg string) string {
|
|
s := strings.TrimSpace(userMsg)
|
|
if s == "" {
|
|
return ""
|
|
}
|
|
// 多行输入取首行——那通常是主诉求,后续多为补充细节或粘贴内容。
|
|
if i := strings.IndexByte(s, '\n'); i >= 0 {
|
|
s = s[:i]
|
|
}
|
|
// 去掉 markdown 装饰:标题符号、引用、列表符、代码块围栏。
|
|
s = strings.TrimLeft(s, "#>*-`~ ")
|
|
return SanitizeTitle(s)
|
|
}
|