diff --git a/TeXmacs/progs/dynamic/chat-session-persist.scm b/TeXmacs/progs/dynamic/chat-session-persist.scm index 237195509a..b53dbf9cd3 100644 --- a/TeXmacs/progs/dynamic/chat-session-persist.scm +++ b/TeXmacs/progs/dynamic/chat-session-persist.scm @@ -84,18 +84,9 @@ (title (cdr (assoc "title" entry))) (model (cdr (assoc "model" entry))) (archived-str (cdr (assoc "archived" entry))) - (msg-path (chat-persist-message-path sid)) - (msg-buf (chat-tab-session->message-buffer sid)) ) ; - ;; 加载消息内容到 buffer - (when (file-exists? msg-path) - (let ((file-url (system->url msg-path))) - (buffer-load file-url) - (buffer-set-body msg-buf (buffer-get-body file-url)) - (buffer-pretend-saved msg-buf) - ) ;let - ) ;when ;; 直接调用 C++ 回调创建 panel + ;; C++ restore_conversation 会从文件加载消息内容 (qt-chat-tab-restore-session sid title model archived-str) ) ;let* ) ;lambda @@ -110,12 +101,10 @@ ;;; ---------- 增量保存 ---------- (tm-define (chat-persist-save-one session-id title model archived) - (let ((msg-path (chat-persist-message-path session-id)) - (msg-buf (chat-tab-session->message-buffer session-id)) + (let ((msg-buf (chat-tab-session->message-buffer session-id)) ) ; - ;; 1. 导出 message buffer - (chat-persist-ensure-dir! (chat-persist-parent-dir msg-path)) - (buffer-export msg-buf (system->url msg-path) "tmu") + ;; 1. 保存 message buffer 到磁盘(buffer 已绑定文件 URL,直接 save) + (buffer-save msg-buf) ;; 2. 增量更新 manifest (let ((manifest-path (chat-persist-manifest-path)) (entry (chat-persist-make-entry session-id title model archived)) diff --git a/TeXmacs/progs/dynamic/chat-tab-session.scm b/TeXmacs/progs/dynamic/chat-tab-session.scm index f5a4673881..2029fd55cb 100644 --- a/TeXmacs/progs/dynamic/chat-tab-session.scm +++ b/TeXmacs/progs/dynamic/chat-tab-session.scm @@ -29,11 +29,17 @@ ;;; ---------- Buffer URL 推导函数 ---------- (tm-define (chat-tab-session->message-buffer session-id) - (string->url (string-append "tmfs://chat-message-" session-id)) + (let ((base-dir (string-append (url->system (get-texmacs-home-path)) + "/system/ai-chat-sessions"))) + (string->url (string-append base-dir "/" session-id "/message.tmu")) + ) ;let ) ;tm-define (define (chat-tab-session->input-buffer session-id) - (string->url (string-append "tmfs://chat-input-" session-id)) + (let ((base-dir (string-append (url->system (get-texmacs-home-path)) + "/system/ai-chat-sessions"))) + (string->url (string-append base-dir "/" session-id "/input.tmu")) + ) ;let ) ;define ;;; ---------- State 构造器和访问器 ---------- diff --git a/devel/0210.md b/devel/0210.md new file mode 100644 index 0000000000..eb19254bff --- /dev/null +++ b/devel/0210.md @@ -0,0 +1,99 @@ +# [0210] Chat Tab Message Buffer 绑定硬盘存储 + +## 状态:可行性分析完成 + +## 背景 + +当前 chat tab 的 message buffer 使用 `tmfs://chat-message-{sessionId}` 虚拟 URL,内容仅存在于内存中。持久化需要显式调用 `buffer-export` 写入磁盘。用户希望将 message buffer 直接绑定到磁盘存储位置,简化持久化逻辑。 + +## 当前架构 + +``` +新建会话: + C++ create_conversation() → texmacs_input_widget("", style, tmfs://chat-message-{sid}) + → 内存中的虚拟 buffer + +保存: + C++ saveOneSession() → Scheme chat-persist-save-one() + → buffer-export(tmfs buffer → $TEXMACS_HOME/system/ai-chat-sessions/{sid}/message.tmu) + +加载: + Scheme chat-persist-load-all() + → buffer-load(file.tmu) + buffer-set-body(tmfs buffer, file content) + → C++ qt-chat-tab-restore-session → get_buffer_body(tmfs) → texmacs_input_widget(body, style, tmfs) +``` + +## 目标架构 + +``` +新建会话: + 1. 创建目录和空 TMU 文件: $TEXMACS_HOME/system/ai-chat-sessions/{sid}/message.tmu + 2. C++ texmacs_input_widget("", style, file://...message.tmu) + → buffer 直接关联磁盘文件 + +保存: + buffer-save(file buffer) → 直接写回磁盘文件 + 无需 buffer-export + +加载: + C++ qt-chat-tab-restore-session → texmacs_input_widget("", style, file://...message.tmu) + → buffer 从文件自动加载,无需 buffer-set-body +``` + +## 关键确认 + +1. **C++ 路径获取**: `get_texmacs_home_path()` 返回 `url` 类型,跨平台可用 +2. **texmacs_input_widget 支持 file URL**: TeXmacs URL 系统统一处理 file:// 和 tmfs:// +3. **空文件初始化**: 传 `tree(DOCUMENT, "")` 给 `texmacs_input_widget`,内部自动补全 TMU 头部 +4. **Input buffer 保持 tmfs 不变**: input 是瞬态数据,无需持久化 + +## 修改位置 + +### C++ 侧 (`qt_chat_tab_widget.cpp/hpp`) +1. `ChatSessionManager::messageBufferUrl()` — 从 `tmfs://` 改为 file URL +2. `create_conversation()` — 创建 widget 前先创建空文件和目录 +3. `restore_conversation()` — widget 直接从文件加载,无需 get_buffer_body 中转 + +### Scheme 侧 +1. `chat-tab-session->message-buffer` — 从 `tmfs://` 改为 file URL +2. `chat-persist-save-one` — 简化为 `buffer-save` +3. `chat-persist-load-all` — 移除 `buffer-load` + `buffer-set-body` 拷贝 +4. `chat-tab-ensure-session!` — `with-buffer` 改为操作 file URL(自动生效) + +## 测试说明 + +### 1. 编译验证 +```bash +xmake b stem +``` +预期:编译通过,无新增 warning。 + +### 2. 新建会话文件创建验证 +1. 启动 Mogan,切换到 Chat Tab +2. 新建一个会话 +3. 检查磁盘目录: + $TEXMACS_HOME_PATH/system/ai-chat-sessions/{sid}/ +4. 预期:`message.tmu` 和 `input.tmu` 文件存在(`input.tmu` 可能为空或很小) + +### 3. 消息持久化验证 +1. 在输入框输入内容,点击发送 +2. 等待 LLM 回复完成后,检查 `message.tmu` 文件大小 +3. 预期:`message.tmu` 文件大小 > 0,且内容包含发送的消息和 LLM 的回复 +4. 直接用 TeXmacs 打开 `message.tmu`,检查内容格式是否正确 + +### 4. 重启恢复验证 +1. 完全退出 Mogan +2. 重新启动 Mogan +3. 检查 Chat Tab 中的会话列表 +4. 预期:之前的会话正确恢复,消息内容完整显示 +5. 检查 `message.tmu` 的修改时间,确认没有被异常覆盖 + +### 5. 删除会话验证 +1. 删除一个会话 +2. 检查磁盘:对应的 `{sid}/` 目录应该被清理 +3. 检查 manifest.json:该会话条目已移除 + +### 6. 边界情况 +- 空会话(未发送任何消息):重启后应正确显示为空会话 +- 多会话切换:切换会话时文件内容应保持独立 +- 归档/恢复:归档后重启,会话状态正确保持 diff --git a/src/Plugins/Qt/qt_chat_tab_widget.cpp b/src/Plugins/Qt/qt_chat_tab_widget.cpp index 63380911cd..1487d2426d 100644 --- a/src/Plugins/Qt/qt_chat_tab_widget.cpp +++ b/src/Plugins/Qt/qt_chat_tab_widget.cpp @@ -20,6 +20,8 @@ #include "qt_utilities.hpp" #include "qt_widget.hpp" #include "s7_tm.hpp" +#include "tm_file.hpp" +#include "tm_sys_utils.hpp" #include "tm_window.hpp" #include @@ -572,6 +574,11 @@ QTChatTabWidget::create_conversation (const QString& title) { url msgBufUrl= ChatSessionManager::messageBufferUrl (sessionId); url inBufUrl = ChatSessionManager::inputBufferUrl (sessionId); + // 确保消息 buffer 的父目录存在,以便后续 buffer-save 可写入 + url msgDir= get_texmacs_home_path () * url ("system/ai-chat-sessions") * + url (sessionId); + if (!is_directory (msgDir)) mkdir (msgDir); + QWidget* page= new QWidget (conversationStack_); page->setObjectName ("chat-tab-conversation-page"); panel->pageWidget= page; @@ -1656,12 +1663,14 @@ ChatSessionManager::setPanel (const string& sessionId, void* panel) { url ChatSessionManager::messageBufferUrl (const string& sessionId) { - return url ("tmfs://chat-message-" * sessionId); + return get_texmacs_home_path () * url ("system/ai-chat-sessions") * + url (sessionId) * url ("message.tmu"); } url ChatSessionManager::inputBufferUrl (const string& sessionId) { - return url ("tmfs://chat-input-" * sessionId); + return get_texmacs_home_path () * url ("system/ai-chat-sessions") * + url (sessionId) * url ("input.tmu"); } void @@ -1783,12 +1792,13 @@ QTChatTabWidget::restore_conversation (const string& sessionId, panel->modelLabel->setMinimumHeight (DpiUtils::scaled (20)); topLayout->addWidget (panel->modelLabel, 0, Qt::AlignHCenter); - // 恢复会话时,buffer 中已有 Scheme 加载的消息内容,需使用 buffer 内容而非空 - // tree - tree msgBody= get_buffer_body (msgBufUrl); - if (is_empty_document_body (msgBody)) msgBody= tree (DOCUMENT, ""); - panel->messageWidget= - texmacs_input_widget (msgBody, make_chat_embedded_style (), msgBufUrl); + // 从磁盘文件加载消息内容到 buffer + if (exists (msgBufUrl)) { + buffer_load (msgBufUrl); + } + + panel->messageWidget= texmacs_input_widget ( + tree (DOCUMENT, ""), make_chat_embedded_style (), msgBufUrl); QWidget* messageQWidget= concrete (panel->messageWidget)->as_qwidget (); panel->messageFrame = new QWidget (topPanel); @@ -1941,7 +1951,8 @@ QTChatTabWidget::restore_conversation (const string& sessionId, conversationListLayout_->addWidget (panel->itemWidget); // 如果消息 buffer 非空,进入会话模式 - if (!is_empty_document_body (msgBody)) { + tree restoredBody= get_buffer_body (msgBufUrl); + if (!is_empty_document_body (restoredBody)) { enter_conversation_mode (panel); }