feat: opencode 式会话持久化 + /resume 一键恢复

此前只有手动的 /chat save|list|open,关终端就丢上下文。现升级为
opencode 式体验:每轮对话自动落盘、启动提示历史会话、/resume 一键恢复。

- 新增 xcli_core/session.py:Session 数据类(id/title/created_at/updated_at/
  engine/model/messages)+ SessionManager(new/save/list/load/resolve,存
  chat_history/<id>.json)。向后兼容旧 /chat 格式(无 id、用 conversation 键,
  读取时回退用文件名作 id)。标题自动取首条 user 消息前若干字。
- HistoryMixin 升级(cli_history.py):新增 init_session_manager /
  _autosave_session / handle_resume_command。自动保存首轮惰性建会话、
  更新标题与 updated_at;/resume 列出(序号|ID)或按序号|ID|前缀恢复,
  恢复时把 messages 载入 shared_conversation_history 并同步引擎。
- 集成:cli_base.__init__ 在 chat_history_dir 就绪后 init_session_manager;
  cli_core 主循环 1395 行 process_conversation 后调用 _autosave_session;
  新增 /resume|/sessions 路由(/sessions 等同 /resume)。
- 体验:启动时若有历史会话提示 /resume(含最近会话标题);run() 紧凑提示
  与 /help 速查表均补 /resume(列出/恢复/开新会话)。
- 修复 cli_history.py 一处历史 typo:{Style.RESET型} -> {Style.RESET_ALL}
  (旧 load_chat_history 在含消息时会抛 AttributeError)。
- 测试:新增 tests/test_session.py(12 项,覆盖兼容/新建/落盘/排序/解析/
  自动保存/恢复/列表/未知token,全程临时目录不污染真实 config/chat_history)。
  全量 242 passed(原 230)。

真实 CLI 冒烟:启动提示识别到历史会话、/resume 正确列出(含标题/时间/引擎/
条数)、/resume <id> 正确恢复并回显;已清理测试种入文件,chat_history 复原为空。
This commit is contained in:
shuiyu67 committed 2026-08-05 17:16:49 +08:00
1 parent 1383d32f8b
commit 07490c4d38
6 files changed
+480 -2

No files matched your search

+26 -1
View File
@@ -1040,7 +1040,17 @@ multi 操作支持一次修改多处:
# 一行速览,完整命令清单收进 /help
print(f"{Fore.CYAN}/help 全部命令 · /quit 退出 · /tui 图形界面 · "
f"/model 换模型 · @文件路径 读文件给 AI{Style.RESET_ALL}")
f"/model 换模型 · /resume 恢复会话 · @文件路径 读文件给 AI{Style.RESET_ALL}")
# 历史会话提示(opencode 式 /resume)
try:
sessions = self.session_manager.list_sessions()
if sessions:
latest = sessions[0]
print(f"{Fore.YELLOW}有 {len(sessions)} 个历史会话,/resume 可恢复"
f"(最近: {latest.id} · {latest.title}){Style.RESET_ALL}")
except Exception:
pass
extras = []
loaded_engines = list(self.engines.keys())
@@ -1146,6 +1156,19 @@ multi 操作支持一次修改多处:
self.handle_model_command(user_input[7:].strip())
continue
# /resume 会话恢复(opencode 式自动持久化 + 一键恢复)
if user_input in ('/resume', '/sessions') or \
user_input.startswith('/resume ') or user_input.startswith('/sessions ') or \
user_input.startswith('/session '):
if user_input.startswith('/resume'):
self.handle_resume_command(user_input[7:].strip())
elif user_input.startswith('/sessions'):
self.handle_resume_command(user_input[9:].strip())
else: # /session <子命令>
sub = user_input[8:].strip()
self.handle_resume_command(sub if sub != 'new' else 'new')
continue
# /manual 快捷命令 — 切换到 manual 引擎或执行 manual 子命令
if user_input == '/manual' or user_input.startswith('/manual '):
args = user_input[8:].strip() if user_input.startswith('/manual ') else ''
@@ -1393,6 +1416,8 @@ multi 操作支持一次修改多处:
print(f"{Fore.CYAN}{'='*50}{Style.RESET_ALL}")
self.process_conversation(user_input)
# 自动持久化当前会话(opencode 式 /resume 落盘)
self._autosave_session()
except KeyboardInterrupt:
user_id_display = f"[用户ID: {self.user_id}]"
print(f"\n{Fore.GREEN}再见! {user_id_display}{Style.RESET_ALL}")