Claude Code 会话攒太多,我用 Go 写了个本地管理台

Claude Code 会话攒太多,我用 Go 写了个本地管理台

Claude Code 的会话都堆在 ~/.claude/projects 下面,一个会话一个 uuid.jsonl,父目录还是 cwd 编码过的名字。会话攒多之后,想找回某次对话只能翻目录,或者先 grep 一遍再打开文件。自带的 resume 列表解决不了跨项目查找。

所以我写了个本地工具:Go + SQLite + 原生前端,单二进制,只监听 127.0.0.1:7788。它能按项目浏览会话、全文搜索、做多层归类、看 fork 谱系,还能调本机 Claude Code 给会话写摘要。

项目叫 session-deck,代码在 github.com/zhangweiwei...,Apache-2.0。

图 1:三栏布局。左边项目/归类,中间会话列表,右边详情。

先看它做了什么

图 2:搜索、切换项目、展开消息的操作过程。

能力 实现方式
项目归并 读 jsonl 里的 cwd,把会话归到对应项目,不移动文件
全文搜索 SQLite FTS5,Ctrl+K 唤起
逻辑归类 多层文件夹,只写 SQLite
会话谱系 解析 forkedFrom,画家族树
恢复会话 Windows Terminal + PowerShell,降级到 cmd
AI 摘要 调本机 claude CLI,不另配 API key
实时刷新 hooks 触发增量扫描,SSE 推给前端

一条原则:jsonl 是唯一真值

Claude Code 自己写 ~/.claude/projects/<cwd>/<uuid>.jsonl。这个目录我不重构,SQLite 只存索引、归类、AI 摘要这些本地元数据。删掉 data/sessions.db 不会影响任何会话,下次启动重新扫一遍就回来了。

图 3:数据流。SQLite 是可丢的索引层。

归类是纯逻辑的。你在界面里建多级文件夹、把会话拖进去,改的是 SQLite 里的 folders 表,~/.claude/projects 的目录结构一个字节都不动。这样做的好处是,Claude Code 升级、/cd 切目录、手动整理文件夹,都不会让工具里的归类错乱。

写操作只有四类:重命名(追加一行 custom-title)、分支/副本(逐行复制)、新建会话(写新的 jsonl)、删除(移到回收站)。forkedFrom 只读不写。

增量扫描:改了解析逻辑得主动失效

每 30 秒扫一次,加上 hooks 和手动同步。如果每轮都重读几百个 jsonl,磁盘和 CPU 都受不了,所以扫描器会先 stat 文件:size 和 mtime 都和索引里那行一致、状态还是 ok,直接跳过。

图 4:判定顺序。绝大多数会话停在上面的分支。

这里有个容易踩的坑。改了解析方式之后,重启服务可能发现数据还是旧的。原因很直白:文件没变,size 和 mtime 都没变,扫描器按契约跳过了。

解决办法是强制失效。凡是改动了解析逻辑的迁移,都在迁移里加一句:

sql 复制代码
UPDATE sessions SET file_mtime = -1;

-1 跟任何真实 mtime 都对不上,下一轮必然重读。新增解析字段的迁移也走这个路子。同样的机制还处理了另一种情况:首次启动时 FTS 表是空的,但 sessions 已经有数据,这时也会把 file_mtime 重置,让所有行重新进索引。

同一个会话 ID 可能出现在多个项目目录里(比如会话中途 /cd)。扫描器取 mtime 最新的那份,旧的索引行会被覆盖。

中文搜索:trigram 和「至少三个字」

SQLite 的 FTS5 默认分词器对中文不友好,一整句会被当成一个 token。我选了 trigram 分词器,它按三字符滑窗切分,中文子串能直接命中,比如搜「线程池配置」不用先分词。

trigram 的代价是查询长度至少 3 个字符。少于 3 个字的查询,FTS 直接返回空,所以这部分走 LIKE:

go 复制代码
// trigram 检索要求至少 3 个字符,短查询继续走 LIKE。
if utf8.RuneCountInString(q) >= 3 {
    return s.searchSessions(ctx, f, where, args)
}
where = append(where, "(s.title LIKE ? OR s.last_prompt LIKE ? OR s.id LIKE ?)")

另一个限制是索引体积。jsonl 里经常有超大的工具输出,一条消息几 MB 不奇怪。扫描器给正文设了 200KB 上限,只索引前 200KB,超出的部分不入库。搜索命中后的高亮用 FTS5 的 snippet() 生成。

AI 摘要生成之后,会单独回填到 FTS 的 summary 列。这样搜「鉴权」时,即使会话正文里没出现这个词,摘要里写到了也能命中,列表里还会高亮摘要片段。

两个 project 维度和 forkedFrom

一个会话有两个「项目」属性:

  • project_id:从 jsonl 的 cwd 得出,是事实来源。
  • display_project_id:会话实际展示在哪个项目下。

平时两者相同。如果文件里最新的 cwd 变了,扫描器会自动把展示位置改到新项目。一旦你手动移动过会话,display_project_source 记成 manual,自动归属就停了,手动结果优先。

分支和副本会写 forkedFrom,扫描器把它解析成 parent_id 和 fork_point_uuid。详情页能顺着血缘链往上翻,也能看到从第几条消息分出去的。父会话被删掉后关系还在,界面标成「缺失」,不会强行清理。

AI 摘要:把会话重新喂给本机 Claude Code

会话列表里有一行摘要,比「最后一条 prompt」有用得多。摘要走的是本机的 claude 可执行文件,没有另外配 API key,登录态直接复用现成的。

钱还是要花的,所以输入要省。一个几千行的会话不会整个塞进去,采样规则是:前 8 条用户提问 + 后 4 条用户提问 + 最后一条回答的结论段(最多 400 字符),默认 1500 字符预算。

图 5:采样、过滤、脱敏、调用的顺序。

读取时顺手做了几件事:跳过 isSidechain 的子代理消息;工具结果也记成 user 消息,得剔掉,否则输入全是命令输出;「继续」「好的」「可以」这类确认短句不计入。最后过一遍脱敏规则,sk-、ghp_、AKIA、Bearer、password= 这些串在进模型之前就被替换掉。

调用命令长这样:

bash 复制代码
claude -p --output-format json --no-session-persistence --tools "" \
  --json-schema '<压成单行的 JSON Schema>'

--tools "" 关掉工具,--no-session-persistence 不留会话文件,避免摘要任务自己也被索引进去。输出由 JSON Schema 约束成固定字段:目标、关键动作、结论、话题标签,解析时不用正则在自由文本里捞。

不同版本的 CLI 对参数支持不一样,所以启动时会依次试 bare、lean、plain 三档,用最小的请求探一下,取第一个能跑通的。批量摘要默认 10 条一起发,某条返回的 JSON 不合法就拆成单条重试。token 用量和累计成本在设置页能看到。

派生会话有单独处理:分叉之后新增不到 3 条消息,直接继承父摘要;否则只采样分叉点之后的内容。不然每个分支都要为同一段历史付一次钱。

Windows 上绕不开的几处

工具目前只支持 Windows,原因主要在恢复会话。

恢复会话要复用最近的 Windows Terminal 窗口,并在项目目录里执行 claude --resume <id>。命令是这样拼的:

xml 复制代码
wt.exe -w 0 nt -d <项目路径> pwsh.exe -NoExit -EncodedCommand <base64>

-w 0 表示复用当前窗口,nt 是新标签页。PowerShell 脚本先清掉 Claude Code 相关的环境变量,再进目录执行 resume。降级顺序是:pwsh + Terminal → powershell + Terminal → cmd /c start 新开窗口 → 都不行就返回错误,同时把可直接复制的命令给出来。

安全上有三道限制:会话 ID 必须是 UUID;项目路径必须已经出现在索引里;命令用参数数组传递,不拼 shell 字符串。另外,重命名和删除之前会检查 ~/.claude/sessions/*.json,会话正在运行就返回 409,不让改。

删除不是真删。文件先移到 ~/.claude-session/trash/<session-id>/,索引行标成 deleted,从回收站可以还原或彻底清掉。

还有个 Windows 特有的小问题:发布版用 -H windowsgui 编译,程序自己没控制台,但它调用的 claude 子进程是控制台程序,Windows 会给它新开一个黑窗口,每跑一次摘要闪一下。解决办法是给子进程加 CREATE_NO_WINDOW:

go 复制代码
cmd.SysProcAttr = &syscall.SysProcAttr{
    HideWindow:    true,
    CreationFlags: 0x08000000, // CREATE_NO_WINDOW
}

跑起来

从源码跑:

bash 复制代码
go run ./cmd/server

构建发布版:

bash 复制代码
go build -trimpath -ldflags "-H windowsgui -s -w" -o bin/session-deck.exe ./cmd/server

双击 exe 或 start.bat,浏览器打开 http://127.0.0.1:7788。默认读 ~/.claude/projects,数据库在 data/sessions.db,日志在 data/session-deck.log。

想让它更实时,可以把 hooks 片段加到 ~/.claude/settings.json,GET /api/hooks/config 能拿到现成的 JSON。配置之后,每次 Claude Code 回复结束都会触发一次增量扫描,页面通过 SSE 自动刷新;不配也有 30 秒定时扫描兜底。

限制和后续

  • 只支持 Windows。macOS / Linux 上的终端恢复要重写,暂时没做。
  • 本机得装好并登录 Claude Code,AI 摘要才有用。
  • AI 摘要会消耗 token,批量跑之前先看设置页的成本估算。
  • 不是云端服务,换台机器数据不跟着走。

路线图上是会话内容编辑、标签与批量操作、多实例访问。代码总共约 9000 行 Go 加 5000 行原生前端,直接依赖只有一个 modernc.org/sqlite,用来免掉 CGO。

仓库在 github.com/zhangweiwei...,README 里有完整的 API 路由表。如果你也在 ~/.claude/projects 里翻过会话,可以拉下来试试。

相关推荐
孔令飞6 小时前
企业智能化升级:智能体平台设计和实现
云原生·go·ai编程
vipxieliang8 小时前
Go GC 性能优化实践:从 GOGC、GOMEMLIMIT 到分配优化
后端·go
小满zs1 天前
Go语言第十八章(架构设计)
后端·go
FungLeo2 天前
成为全栈·Go 后端篇·三轮代码审阅之后:把可读性和证据做成工程规则
golang·go·ast·静态检查·成为全栈·代码审阅
我不会起名字3222 天前
Redis 从 0 到 1(二十八):Redis 抖一下,重试把它打挂了——超时/重试/熔断的 Go 封装
redis·go·超时·熔断·重试
xyzzklk2 天前
变相解决华为Mate 8锁屏后无法建立入站网络连接
linux·运维·服务器·网络·华为·go·安卓
vipxieliang3 天前
语言对比卡:PHP vs Go vs Java 的优缺点、适用场景与「为什么选 PHP」
java·go·php
我不会起名字3224 天前
Redis 从 0 到 1(二十七):JSON 体积是 Protobuf 的 3.7 倍?3 种序列化的 Go 基准测试
redis·go·序列化·protobuf·messagepack
我不会起名字3224 天前
Redis 从 0 到 1(二十三):一个热 key 打到 10 万 QPS——hotkey 与本地缓存的 Go 实现
redis·缓存·go·qps·hotkey