此前只有手动的 /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 复原为空。
121 lines
5.8 KiB
Python
121 lines
5.8 KiB
Python
import os
|
||
import time
|
||
import json
|
||
from colorama import Fore, Style
|
||
from .session import SessionManager
|
||
|
||
|
||
class HistoryMixin:
|
||
"""聊天记录管理相关方法(含 opencode 式会话持久化 /resume)"""
|
||
|
||
def save_chat_history(self, name):
|
||
"""保存当前聊天记录到文件"""
|
||
try:
|
||
chat_data = {
|
||
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
|
||
"conversation": self.shared_conversation_history.copy()
|
||
}
|
||
file_path = os.path.join(self.chat_history_dir, f"{name}.json")
|
||
with open(file_path, 'w', encoding='utf-8') as f:
|
||
json.dump(chat_data, f, ensure_ascii=False, indent=2, default=str)
|
||
print(f"{Fore.GREEN}聊天记录已保存为: {name}{Style.RESET_ALL}")
|
||
return True
|
||
except Exception as e:
|
||
print(f"{Fore.RED}保存聊天记录失败: {e}{Style.RESET_ALL}")
|
||
return False
|
||
|
||
def list_chat_history(self):
|
||
"""列出所有已保存的聊天记录"""
|
||
try:
|
||
chat_files = [f for f in os.listdir(self.chat_history_dir) if f.endswith('.json')]
|
||
if not chat_files:
|
||
print(f"{Fore.YELLOW}没有已保存的聊天记录{Style.RESET_ALL}")
|
||
return
|
||
print(f"{Fore.GREEN}已保存的聊天记录:{Style.RESET_ALL}")
|
||
for i, file in enumerate(chat_files, 1):
|
||
file_path = os.path.join(self.chat_history_dir, file)
|
||
mod_time = os.path.getmtime(file_path)
|
||
mod_time_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(mod_time))
|
||
chat_name = file[:-5]
|
||
print(f"{Fore.GREEN}{i}. {chat_name} (保存时间: {mod_time_str}){Style.RESET_ALL}")
|
||
except Exception as e:
|
||
print(f"{Fore.RED}列出聊天记录失败: {e}{Style.RESET_ALL}")
|
||
|
||
def load_chat_history(self, name):
|
||
"""加载指定的聊天记录"""
|
||
try:
|
||
file_path = os.path.join(self.chat_history_dir, f"{name}.json")
|
||
if not os.path.exists(file_path):
|
||
print(f"{Fore.RED}聊天记录 '{name}' 不存在{Style.RESET_ALL}")
|
||
return False
|
||
with open(file_path, 'r', encoding='utf-8') as f:
|
||
chat_data = json.load(f)
|
||
self.shared_conversation_history = chat_data.get("conversation", [])
|
||
self._set_shared_conversation_history()
|
||
print(f"{Fore.GREEN}已加载聊天记录: {name}{Style.RESET_ALL}")
|
||
if self.shared_conversation_history:
|
||
print(f"{Fore.CYAN}加载了 {len(self.shared_conversation_history)} 条对话记录{Style.RESET_ALL}")
|
||
return True
|
||
except Exception as e:
|
||
print(f"{Fore.RED}加载聊天记录失败: {e}{Style.RESET_ALL}")
|
||
return False
|
||
|
||
# ── opencode 式会话(自动持久化 + /resume) ──
|
||
|
||
def init_session_manager(self):
|
||
"""初始化会话管理器与当前会话状态。在 chat_history_dir 就绪后调用。"""
|
||
self.session_manager = SessionManager(self.chat_history_dir)
|
||
self.current_session = None # 首轮对话时惰性创建
|
||
|
||
def _autosave_session(self):
|
||
"""每轮对话后调用:把当前会话落盘(首轮自动建会话)。"""
|
||
if not getattr(self, 'shared_conversation_history', None):
|
||
return
|
||
if not hasattr(self, 'session_manager'):
|
||
return
|
||
engine = getattr(self.current_engine, 'name', None) if self.current_engine else None
|
||
model = getattr(self, 'current_model', None)
|
||
try:
|
||
if self.current_session is None:
|
||
self.current_session = self.session_manager.new_session(engine=engine, model=model)
|
||
self.session_manager.save(
|
||
self.current_session, self.shared_conversation_history,
|
||
engine=engine, model=model,
|
||
)
|
||
except Exception:
|
||
pass # 自动保存失败不应中断对话
|
||
|
||
def handle_resume_command(self, args):
|
||
"""/resume 命令:列出或恢复历史会话。"""
|
||
args = (args or "").strip()
|
||
if not hasattr(self, 'session_manager'):
|
||
self.init_session_manager()
|
||
if args == 'new':
|
||
self.current_session = None
|
||
print(f"{Fore.GREEN}已开始新会话(下一轮对话自动创建){Style.RESET_ALL}")
|
||
return
|
||
sessions = self.session_manager.list_sessions()
|
||
if not sessions:
|
||
print(f"{Fore.YELLOW}没有可恢复的会话{Style.RESET_ALL}")
|
||
return
|
||
if not args:
|
||
print(f"{Fore.GREEN}已保存的会话 (按时间倒序):{Style.RESET_ALL}")
|
||
for i, s in enumerate(sessions, 1):
|
||
line = f"{Fore.GREEN}{i}. {s.id}{Style.RESET_ALL} {s.title}"
|
||
meta = f" {s.updated_at} {s.engine or '-'}/{s.model or '-'} {s.msg_count} 条"
|
||
print(line + f"{Fore.CYAN}{meta}{Style.RESET_ALL}")
|
||
print(f"{Fore.CYAN}用法: /resume <序号或ID> 恢复 · /resume new 开新会话{Style.RESET_ALL}")
|
||
return
|
||
s = self.session_manager.load(args)
|
||
if s is None:
|
||
print(f"{Fore.RED}未找到会话: {args}{Style.RESET_ALL}")
|
||
return
|
||
self.shared_conversation_history = list(s.messages)
|
||
self.current_session = s
|
||
self._set_shared_conversation_history()
|
||
print(f"{Fore.GREEN}已恢复会话: {s.id}{Style.RESET_ALL}")
|
||
print(f"{Fore.CYAN}标题: {s.title} · {s.msg_count} 条记录 · 引擎 {s.engine or '-'}/{s.model or '-'}{Style.RESET_ALL}")
|
||
cur = getattr(self.current_engine, 'name', None) if self.current_engine else None
|
||
if s.engine and cur and cur != s.engine:
|
||
print(f"{Fore.YELLOW}提示: 当前引擎为 {cur},可用 /engine switch {s.engine} 切回该会话的引擎{Style.RESET_ALL}")
|