新增: 历史会话标题自定义编辑与摘要质量修复

- 标题自定义编辑:HISTORY 详情按 e 编辑标题,UserTitle 持久化到缓存且最高优先级
- AI 摘要质量修复:困惑回复检测词表收紧,消息提取过滤噪音,修复 JSON 包裹困惑回复漏网
- 数据维护命令:--fix-titles 清理低质量摘要,--regen-summaries 并发生成
- 修复代码审查 9 个问题:退格 UTF-8 损坏、中文输入、flush 调度、误杀合法摘要、
  按键全局拦截、UserTitle 未同步窗口标题、wt 启动回落、cmd 注入防护、前缀误隐藏会话
- 启动链 wt.exe 回退与 cmd start 标题转义
- gofmt 规范化,新增 launch_test.go
This commit is contained in:
lxy
2026-07-31 21:22:17 +08:00
parent 043a7d9227
commit 95f349bcd7
10 changed files with 1465 additions and 126 deletions
+60 -23
View File
@@ -3,6 +3,7 @@ package internal
import (
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"strconv"
@@ -114,42 +115,82 @@ func powershellExe() string {
return "powershell"
}
// wtExe 返回 Windows Terminal 可执行文件路径;未安装/找不到时返回空串。
// 查找顺序:PATH 中的 wt.exe → 应用执行别名标准路径
// %LOCALAPPDATA%\Microsoft\WindowsApps\wt.exe(WindowsApps 常不在 PATH)。
// 找不到的常见原因:未安装 Windows Terminal(Win10 需 Store 安装)、
// 或 WindowsApps 执行别名被禁用。调用方据此回退到独立窗口启动。
func wtExe() string {
if p, err := exec.LookPath("wt.exe"); err == nil {
return p
}
if local := os.Getenv("LOCALAPPDATA"); local != "" {
p := filepath.Join(local, "Microsoft", "WindowsApps", "wt.exe")
if _, err := os.Stat(p); err == nil {
return p
}
}
return ""
}
func launchInWtWithTitle(dir, tabTitle, script string) {
encoded := encodePSCommand(script)
color := fmt.Sprintf("#%02X%02X%02X", randRange(80, 255), randRange(80, 255), randRange(80, 255))
args := []string{"-w", "0", "-d", dir, "--tabColor", color}
if tabTitle != "" {
args = append(args, "--title", tabTitle)
ps := powershellExe()
// 优先 Windows Terminal:在已有窗口(-w 0)里开新标签页
if wt := wtExe(); wt != "" {
color := fmt.Sprintf("#%02X%02X%02X", randRange(80, 255), randRange(80, 255), randRange(80, 255))
args := []string{"-w", "0", "-d", dir, "--tabColor", color}
if tabTitle != "" {
args = append(args, "--title", tabTitle)
}
args = append(args, ps, "-NoExit", "-EncodedCommand", encoded)
if err := exec.Command(wt, args...).Start(); err == nil {
return
} else {
log.Printf("[u-tabs] launch fail (wt): %v, fallback to cmd start", err)
}
}
args = append(args, powershellExe(), "-NoExit", "-EncodedCommand", encoded)
cmd := exec.Command("wt.exe", args...)
// 回退:未安装 Windows Terminal → cmd start 开独立 PowerShell 窗口
// start 语法:START ["title"] [options] command [args]
// 首个参数被引号包裹时才视为窗口标题,否则视为命令。
// Go 的 exec.Command 只在参数含空格时自动添加引号,
// 所以给 tabTitle 追加空格(如尚不含空格)来确保被引号包裹。
titleArg := tabTitle
// 转义标题中的 cmd 特殊字符,防止命令注入:
// 双引号全部移除;& | < > ^ 前加 ^(顺序:先 ^ 再其余)
titleArg = strings.ReplaceAll(titleArg, `"`, "")
titleArg = strings.ReplaceAll(titleArg, "^", "^^")
titleArg = strings.ReplaceAll(titleArg, "&", "^&")
titleArg = strings.ReplaceAll(titleArg, "|", "^|")
titleArg = strings.ReplaceAll(titleArg, "<", "^<")
titleArg = strings.ReplaceAll(titleArg, ">", "^>")
if titleArg != "" && !strings.ContainsAny(titleArg, " \t") {
titleArg += " " // 迫使 Go 自动加引号,窗口标题尾部空格不影响使用
}
args := []string{"/c", "start", titleArg, ps, "-NoExit", "-EncodedCommand", encoded}
cmd := exec.Command("cmd.exe", args...)
cmd.Dir = dir
if err := cmd.Start(); err != nil {
log.Printf("[u-tabs] launch fail: %v", err)
log.Printf("[u-tabs] launch fail (cmd): %v", err)
}
}
// buildResumeAction 构建恢复会话动作
func buildResumeAction(s *Session) SessionAction {
title := s.CustomTitle
if title == "" {
title = s.ID[:8]
}
return SessionAction{
Cwd: s.Cwd,
Title: "resume: " + title,
Title: "resume: " + s.DisplayTitle(),
SessionID: s.ID,
}
}
// buildForkAction 构建分叉会话动作
func buildForkAction(s *Session, prompt string) SessionAction {
title := s.CustomTitle
if title == "" {
title = s.ID[:8]
}
return SessionAction{
Cwd: s.Cwd,
Title: "fork: " + title,
Title: "fork: " + s.DisplayTitle(),
SessionID: s.ID,
Fork: true,
Prompt: prompt,
@@ -184,7 +225,7 @@ func (m *Model) resumeSelected() (*Model, tea.Cmd) {
}
action := buildResumeAction(s)
action.Execute(m)
m.launched = "resume: " + s.CustomTitle
m.launched = "resume: " + s.DisplayTitle()
return m, nil
}
@@ -225,11 +266,7 @@ func (m *Model) executeFork() (*Model, tea.Cmd) {
At: time.Now(),
})
title := s.CustomTitle
if title == "" {
title = s.ID[:8]
}
m.launched = "fork: " + title
m.launched = "fork: " + s.DisplayTitle()
return m, nil
}