feat: Phase5B 对话历史持久化

新增 ConversationPersistence:
- 退出REPL时自动保存对话到 ~/.claude-code-java/conversations/
- JSON格式存储:系统消息、用户消息、助手消息(含工具调用)、工具响应
- 支持保存、加载最近对话、列出所有对话
- 文件名格式: yyyyMMdd_HHmmss_摘要.json

新增 /history 命令:
- 列出最近10条保存的对话记录
- 显示时间、摘要、消息数、工作目录

ReplSession 更新:
- 自动记录对话摘要(第一次用户输入前40字)
- JLine模式和Scanner模式退出时都保存对话

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
liuzh
2026-04-01 21:35:48 +08:00
co-authored by Copilot
parent fd262bf98d
commit 399cea4478
4 changed files with 332 additions and 2 deletions
@@ -0,0 +1,56 @@
package com.claudecode.command.impl;
import com.claudecode.command.CommandContext;
import com.claudecode.command.SlashCommand;
import com.claudecode.console.AnsiStyle;
import com.claudecode.core.ConversationPersistence;
/**
* /history 命令 —— 列出保存的对话历史。
* <p>
* 显示最近的对话记录,包括时间、摘要和消息数量。
*/
public class HistoryCommand implements SlashCommand {
@Override
public String name() {
return "/history";
}
@Override
public String description() {
return "列出保存的对话历史";
}
@Override
public String execute(String args, CommandContext context) {
ConversationPersistence persistence = new ConversationPersistence();
var conversations = persistence.listConversations();
if (conversations.isEmpty()) {
return AnsiStyle.dim(" 📂 暂无保存的对话历史");
}
StringBuilder sb = new StringBuilder();
sb.append(AnsiStyle.bold(" 📂 对话历史") + AnsiStyle.dim(" (")
+ conversations.size() + AnsiStyle.dim(" 条记录)\n"));
sb.append(AnsiStyle.dim(" " + "".repeat(50)) + "\n");
int shown = Math.min(conversations.size(), 10);
for (int i = 0; i < shown; i++) {
var conv = conversations.get(i);
sb.append(" " + AnsiStyle.cyan((i + 1) + ".") + " ");
sb.append(AnsiStyle.bold(conv.summary()));
sb.append(AnsiStyle.dim(" (" + conv.messageCount() + " messages)") + "\n");
sb.append(" " + AnsiStyle.dim(conv.savedAt() + " | " + conv.workingDir()) + "\n");
}
if (conversations.size() > 10) {
sb.append(AnsiStyle.dim(" ... 还有 " + (conversations.size() - 10) + " 条更早的记录\n"));
}
sb.append(AnsiStyle.dim("\n 对话存储位置: " + persistence.getConversationsDir()));
return sb.toString();
}
}