news 2026/9/28 2:23:53

open-codesign 会话历史持久化恢复:从 v0.2 TODO stub 到 JSONL 聊天存储的 IPC 链路重建

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
open-codesign 会话历史持久化恢复:从 v0.2 TODO stub 到 JSONL 聊天存储的 IPC 链路重建
  • 人工智能
  • AI 应用
  • 桌面应用

【免费下载链接】open-codesign

Open-source Claude Design alternative. One-click import your Claude Code / Codex API key. Prompt → prototype / slides / PDF. Multi-model (Claude, GPT, Gemini, Kimi, GLM, Ollama). BYOK, local-first, MIT.

项目地址:https://gitcode.com/gh_mirrors/op/open-codesign
点击查看免费下载

导读

本文基于仓库内的 session_history_restore_plan.md 计划文档,完整还原 open-codesign 桌面端(Electron)一次会话历史持久化链路修复的根因分析与四步实施方案。该计划针对window.codesign.chat.*在 v0.2 阶段遗留的 TODO stub——聊天记录既不落盘也无法重载的问题,通过主进程 IPC 通道恢复、preload 桥接、快照种子与工具状态更新回归覆盖四个步骤完成重建。读完本文,你将掌握 open-codesign 主进程 / preload / 渲染进程三层之间聊天数据的完整调用链、JSONL 会话文件的存储与回放机制,以及如何验证这套恢复方案的正确性。

一、问题根因:v0.2 遗留的 TODO stub

计划文档开门见山地给出了问题的根因:

window.codesign.chat.*在apps/desktop/src/preload/index.ts中是 v0.2 的 TODO stub。它返回空列表、在内存中完成 append,因此渲染器(renderer)的聊天记录从未被持久化,也无法在重启后重新加载。

这段话描述了三个具体症状,可以对照源码逐一理解:

  1. chat.list返回空列表:界面每次启动读取聊天历史时得到的都是[],历史对话"凭空消失";
  2. chat.append只在内存中追加:消息仅在当前进程存活期间可见,进程退出即丢失;
  3. 聊天记录既不持久化也不重载:没有落盘通道,自然也没有恢复路径,用户无法接续上一次会话。

修复前,preload 层chat对象是一段"占位实现";修复后,它变成了一组真正指向主进程 IPC 通道的桥接方法(详见下文第四节)。

二、恢复方案总览:四步重建链路

计划文档给出了完整的实施清单(当前状态均为已完成):

步骤内容状态
1在主进程中使用现有的聊天消息辅助函数(chat message helpers)恢复持久化的聊天 IPC 通道✅ 已完成
2将 preload 中的聊天方法指向这些 IPC 通道✅ 已完成
3为 append/list、快照种子(snapshot seeding)与工具状态更新(tool status updates)补充 IPC 回归测试覆盖✅ 已完成
4运行聚焦的桌面端主进程测试✅ 已完成

这套方案的核心思想是不新造轮子:主进程侧本就存在一套围绕SessionManager的会话聊天辅助函数(位于 apps/desktop/src/main/session-chat.ts),只是缺少暴露给渲染层的 IPC 通道。恢复工作因此变成"接线"而非"重写"——把已有能力通过 IPC 暴露出去,再让 preload 指向它。

三、第一步:主进程恢复持久化聊天 IPC 通道

恢复后的 IPC 通道注册在 apps/desktop/src/main/snapshots-ipc.ts(第 1601–1625 行),共四条:

ipcMain.handle('chat:v1:list', (_e, raw): ChatMessageRow[] => { const designId = parseDesignIdPayload(raw, 'chat:v1:list'); return runDb('chat:list', () => listSessionChatMessages(chatStoreOptions(db), designId)); }); ipcMain.handle('chat:v1:append', (_e, raw): ChatMessageRow => { const input = parseChatAppendInput(raw); return runDb('chat:append', () => appendSessionChatMessage(chatStoreOptions(db), input)); }); ipcMain.handle('chat:v1:seed-from-snapshots', (_e, raw): { inserted: number } => { const designId = parseDesignIdPayload(raw, 'chat:v1:seed-from-snapshots'); return runDb('chat:seed-from-snapshots', () => ({ inserted: seedSessionChatFromSnapshots(chatStoreOptions(db), designId), })); }); ipcMain.handle('chat:v1:update-tool-status', (_e, raw): { ok: true } => { const input = parseToolStatusInput(raw); runDb('chat:update-tool-status', () => appendSessionToolStatus(chatStoreOptions(db), input)); return { ok: true }; });

四条通道的职责划分非常清晰:

  • chat:v1:list:读取某个 design 的全部聊天记录,返回ChatMessageRow[];
  • chat:v1:append:追加一条聊天消息,返回持久化后的完整行(含id、seq、createdAt);
  • chat:v1:seed-from-snapshots:首次打开既有 design 时,用快照中的 prompt 反向填充聊天历史,返回插入条数;
  • chat:v1:update-tool-status:更新某条工具调用(tool_call)消息的执行状态,返回{ ok: true }。

底层真正干活的是 session-chat.ts 中导出的辅助函数——listSessionChatMessages、appendSessionChatMessage、seedSessionChatFromSnapshots、appendSessionToolStatus。每个 handler 都经过runDb包裹,配合CodesignError与IPC_BAD_INPUT/IPC_NOT_FOUND等错误码(见 packages/shared/src/error-codes.ts),保证异常路径可控。

3.1 存储层:JSONL 会话文件

session-chat.ts的存储模型是每个 design 一个 JSONL 文件:

function sessionFileForDesign(sessionDir: string, designId: string): string { const safeId = designId.replace(/[^A-Za-z0-9_-]/g, '_'); return path.join(sessionDir, `${safeId}.jsonl`); }
  • designId 会被清洗为只含A-Za-z0-9_-的安全文件名,避免路径注入;
  • 会话文件放在db.sessionDir下,主进程通过SessionChatStoreOptions({ db, sessionDir })统一传入;
  • 写入前mkdirSync(..., { recursive: true })确保目录存在,文件为JSON.stringify的逐行追加(writeFileSync全量重写)。

3.2 消息条目类型与 schema 版本

每条写入会话文件的自定义条目(custom entry)都带type: 'custom'与customType,由常量标识:

常量customType 值用途
CHAT_MESSAGE_CUSTOM_TYPEopen-codesign.chat.message聊天消息本体
CHAT_TOOL_STATUS_CUSTOM_TYPEopen-codesign.chat.tool_status工具调用状态更新
COMMENT_CUSTOM_TYPEopen-codesign.comment.v1评论事件(add/update/remove/mark-applied)
CONTEXT_BRIEF_CUSTOM_TYPEopen-codesign.context.brief.v1设计简报
RUN_PREFERENCES_CUSTOM_TYPEopen-codesign.context.run_preferences.v1运行偏好
ACTIVE_MESSAGE_CUSTOM_TYPEopen-codesign.active-message.v1运行中的活跃消息

所有持久化结构均携带schemaVersion: 1,例如存储的聊天消息:

interface StoredChatMessage { schemaVersion: 1; id: number; seq: number; kind: ChatMessageKind; payload: unknown; snapshotId: string | null; }

回放时通过parseStoredMessage、parseStatusUpdate、parseCommentEvent等解析函数做严格校验(schemaVersion必须为 1、字段类型逐一检查),遇到畸形条目会抛出IPC_DB_ERROR,而不是静默吞掉数据。

3.3 追加消息:seq 与活动时间戳

appendSessionChatMessage的实现要点:

const seq = listSessionChatMessages(opts, input.designId).length; const stored: StoredChatMessage = { schemaVersion: 1, id: seq, seq, kind: input.kind, payload: input.payload ?? {}, snapshotId: input.snapshotId ?? null, };
  • seq取自当前消息总数,保证同一会话内消息序号单调递增且稳定,这是后面工具状态按seq回写的前提;
  • 默认会调用touchDesignActivity(db, designId, createdAt)更新设计活跃时间(快照种子等批量回填场景通过{ touchActivity: false }关闭,避免污染排序)。

四、第二步:preload 聊天方法指向 IPC 通道

恢复后的 preload 桥接位于 apps/desktop/src/preload/index.ts(第 865–898 行),window.codesign.chat从 TODO stub 变成了四个真实调用ipcRenderer.invoke的方法:

chat: { list: (designId: string) => ipcRenderer.invoke('chat:v1:list', { schemaVersion: 1, designId }) as Promise<ChatMessageRow[]>, append: (input: ChatAppendInput) => ipcRenderer.invoke('chat:v1:append', { schemaVersion: 1, ...input }) as Promise<ChatMessageRow>, seedFromSnapshots: (designId: string) => ipcRenderer.invoke('chat:v1:seed-from-snapshots', { schemaVersion: 1, designId }) as Promise<{ inserted: number }>, updateToolStatus: (input: { designId; seq; status: 'done' | 'error'; result?; durationMs?; errorMessage? }) => ipcRenderer.invoke('chat:v1:update-tool-status', { schemaVersion: 1, ...input }) as Promise<{ ok: true }>, onAgentEvent: (cb: (event: AgentStreamEvent) => void) => { ... }, }

注意所有 IPC 载荷都携带schemaVersion: 1,与主进程 handler 中的parseDesignIdPayload/requireSchemaV1校验对应;同时onAgentEvent订阅了agent:event:v1通道,用于接收主进程推送的实时 Agent 事件(见第五节)。

渲染进程侧的消费端同样印证了这条链路:在 apps/desktop/src/renderer/src/store/slices/chat.ts 中,打开设计时会先window.codesign.chat.seedFromSnapshots(designId)再window.codesign.chat.list(designId)加载历史,追加消息则走window.codesign.chat.append(input)——这正是计划中"渲染器聊天行从未被持久化或重载"问题被修复后的实际调用形态。

五、快照种子:让既有设计的历史"复活"

对于 v0.2 之前创建、从未写入过会话文件的既有设计,直接chat:list只能得到空数组。seedSessionChatFromSnapshots(session-chat.ts 第 690–724 行)解决这个问题:

export function seedSessionChatFromSnapshots(opts, designId): number { if (listSessionChatMessages(opts, designId).length > 0) return 0; // 已有历史则跳过 const snapshots = listSnapshots(opts.db, designId).slice().reverse(); for (const snapshot of snapshots) { if (snapshot.prompt 非空) { appendSessionChatMessage(opts, { designId, kind: 'user', payload: { text: snapshot.prompt } }, { touchActivity: false }); } appendSessionChatMessage(opts, { designId, kind: 'artifact_delivered', payload: { createdAt: snapshot.createdAt }, snapshotId: snapshot.id, }, { touchActivity: false }); } return inserted; }

关键行为:

  • 幂等:会话文件已存在聊天记录时直接返回 0,绝不重复播种;
  • 从快照反推历史:每个快照的prompt回填为一条user消息,并追加一条带snapshotId的artifact_delivered消息(标记交付了哪个快照产物);
  • 时间顺序:快照按时间倒序取、再reverse()成正序写入,保证聊天记录的自然顺序;
  • 不触碰活动时间:批量回填使用touchActivity: false,避免把所有设计"顶"到活跃列表最前。

六、实时事件投影:run 事件到聊天行的持续写入

持久化不只发生在用户手动 append 时。Agent 运行过程中的流式事件同样需要落到会话文件中,这一职责由 apps/desktop/src/main/run-event-chat.ts 的projectRunEventToChat承担,其注释点明设计原则:"日志(journal)是权威来源;回放用于修复日志与聊天写入之间的崩溃间隙。"

事件 → 聊天行的映射规则:

Agent 事件生成的聊天行说明
turn_endassistant_text写入finalText,携带runId与runEventKey(generationId:seq)
tool_call_starttool_call记录toolName、args、toolCallId、command、status
tool_call_resulttool_call状态更新按runId + toolCallId定位原行,appendSessionToolStatus回写done/error、result、durationMs
errorerror消息记录message与code
run_settled结算所有running工具 + 补写assistant_text/artifact_deliveredcompleted→done,否则 →error

去重与幂等是这套投影机制的关键:每条追加都带唯一runEventKey,append前先检查rows.some((row) => payload(row)['runEventKey'] === eventKey),重复事件不会产生重复行;工具结果按seq应用,且通过runResultEventKey保证"已提交的结果不会被运行结算推断的错误覆盖"(见applyStatusUpdate中删除error/errorMessage的逻辑)。

主进程在 apps/desktop/src/main/ipc/generate.ts(第 333–381 行)的publishEvent中完成"日志先写、投影随后、事件再推送给渲染器"的编排:事件先journal.append持久化,再projectRunEventToChat投影到聊天存储,最后target.send('agent:event:v1', durable)推送给窗口。此外codesign:v1:recover-runshandler(同文件第 463–511 行)会读取 journal 中全部历史事件并重放投影,实现启动时的崩溃恢复。

七、工具状态更新的落地细节

chat:v1:update-tool-status对应的appendSessionToolStatus(session-chat.ts 第 599–629 行)有几个值得注意的实现细节:

  1. 先定位再更新:按seq找到原始消息,若其kind !== 'tool_call'直接返回,绝不写入孤儿状态;
  2. 结果压缩:工具结果先经compactToolResultForHistory(toolName, result)压缩(见 apps/desktop/src/main/ipc/tool-log.ts),避免把超大结果原样塞进 JSONL;
  3. 回放时合并:replayEntries在重建聊天行时按seq找到对应tool_call行并applyStatusUpdate合并状态字段,因此读取出来的行始终是"已应用最新状态"的最终形态;
  4. 错误字段规范化:errorMessage会被展开为{ error: { message } }与平铺errorMessage两种形态,供渲染层不同组件消费。

八、回归测试覆盖与验证

计划第三步要求为 append/list、快照种子、工具状态更新补充 IPC 回归测试,仓库中的测试文件可以逐一对应:

  • apps/desktop/src/main/session-chat.test.ts:直接覆盖appendSessionChatMessage、listSessionChatMessages、seedSessionChatFromSnapshots等存储函数——例如种子测试断言插入数为 2(1 条 user prompt + 1 条 artifact_delivered),且getDesign(...).updatedAt在touchActivity: false下保持不变;
  • apps/desktop/src/main/run-event-chat.test.ts:覆盖projectRunEventToChat的事件投影规则,包括"忽略瞬态与未入日志的事件"等边界;
  • apps/desktop/src/renderer/src/store.chat-continuity.test.ts:以api.chat.seedFromSnapshots/api.chat.list的 mock 验证渲染器在重载/聚焦时的续聊行为;
  • apps/desktop/src/renderer/src/hooks/useAgentStream.completion.test.ts:验证chat.append/chat.list在完成事件下的调用次数与窗口聚焦后的重新拉取。

验证方式(第四步)即运行桌面端主进程的聚焦测试。仓库使用 pnpm + vitest(配置见 apps/desktop/vitest.config.ts),典型命令为:

# 仓库根目录(pnpm workspace) pnpm --filter @open-codesign/desktop test -- session-chat run-event-chat

或单独跑某个文件:

pnpm --filter @open-codesign/desktop vitest run apps/desktop/src/main/session-chat.test.ts

若要在本地复现整个链路,可先pnpm install,再运行pnpm --filter @open-codesign/desktop dev(dev 入口见 apps/desktop/scripts/dev.cjs)启动 Electron 应用,创建/打开一个 design 后重启应用,即可验证聊天历史被从<sessionDir>/<safeId>.jsonl中恢复。

九、小结

从这份计划文档可以提炼出 open-codesign 桌面端会话历史持久化的完整链路:

渲染进程 store/slices/chat.ts │ window.codesign.chat.{list,append,seedFromSnapshots,updateToolStatus} ▼ preload 层 index.ts(IPC 桥接,携带 schemaVersion: 1) │ ipcRenderer.invoke('chat:v1:*') ▼ 主进程 snapshots-ipc.ts(IPC handler + 输入校验 + runDb) │ ▼ session-chat.ts(SessionManager + JSONL 文件 + 回放/校验) ▲ │ projectRunEventToChat(run-event-chat.ts) │ RunJournal 事件流(ipc/generate.ts publishEvent / recover-runs)

这条链路的修复价值在于:聊天记录从"进程内存中的易失数据"升级为"以 JSONL 落盘、可由快照种子恢复、可被运行日志回放修复的持久数据",同时通过schemaVersion版本化、seq序号、runEventKey去重等手段保证读取结果的确定性与幂等性。对于需要在 Electron 桌面应用中实现"会话级聊天持久化 + 崩溃恢复 + 历史回填"的开发者,这份计划与实现提供了一个结构清晰、可逐层验证的参考范式。

  • 人工智能
  • AI 应用
  • 桌面应用

【免费下载链接】open-codesign

Open-source Claude Design alternative. One-click import your Claude Code / Codex API key. Prompt → prototype / slides / PDF. Multi-model (Claude, GPT, Gemini, Kimi, GLM, Ollama). BYOK, local-first, MIT.

项目地址:https://gitcode.com/gh_mirrors/op/open-codesign
点击查看免费下载

相关推荐

上一篇:如何快速掌握REFramework:游戏模组开发的终极指南
下一篇:Proxyee 项目常见问题解决方案

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 2:23:53

goim v2.0:基于 Golang 的高性能 IM 与实时推送服务集群实战指南

后端即时通讯微服务 【免费下载链接】goim goim 项目地址&#xff1a; https://gitcode.com/gh_mirrors/go/goim 点击查看 免费下载 goim 是一个用纯 Golang 编写的即时通讯&#xff08;IM&#xff09;服务端及实时推送集群&#xff0c;支持单推、多推、房间推送与全量广播&am…

作者头像 李华
网站建设 2026/9/28 2:23:05

大麦抢票脚本快速上手:3步配置参数,开售即自动下单

大麦抢票脚本快速上手&#xff1a;3步配置参数&#xff0c;开售即自动下单 【免费下载链接】Automatic_ticket_purchase 大麦网抢票脚本 项目地址: https://gitcode.com/GitHub_Trending/au/Automatic_ticket_purchase 热门演出中午开售&#xff0c;页面瞬间挤满&#x…

作者头像 李华