feat: 升级至 v0.3.7 — 前后端状态同步与错误处理全量修复

核心引擎修复:
- CE-1: 上下文压缩摘要 role 从 system 改为 user,避免被 adapter 过滤
- CE-2: 工具失败时优先使用 error 字段(engine/openai-format/ollama 三处)
- P0-1: DeadLoopError 终止时正确传 error 参数,前端可见 ERROR 事件
- MT-1: 新增 waitForAbort 方法,abortSession 等待 run 结束再返回
- MT-2: TERMINATED 状态到达时标记步骤完成,避免 Trace Viewer 转圈
- MT-3: 压缩边界检测孤立 tool 消息,避免 API 400 错误
- isRetryableError 与 catch 分支统一 toLowerCase

IPC 与主进程修复:
- P0-2: createAdapter 配置缺失返回 null,FALLBACK_ADAPTER 兜底
- P0-3: reloadAdapter 失败返回 success:false 通知前端
- P1-5: 校验失败发 ERROR+DONE 流事件,防止 isStreaming 卡死
- P1-6: configLoaded 标志,配置加载前禁用发送按钮
- P1-7: MCP initialize 移到 agentLoop 后,完成后同步工具
- P2-11: beforeLoad 在 loadURL 前注册 IPC handler
- P2-12: provider 切换竞态保护

前端状态同步修复:
- clearSessions 后同步清空前端会话与消息状态
- clearMemories 通过 memoryVersion 触发 MemoryViewer 重新加载
- ContextMenu 4 个 session 操作补全 IPC 调用与 try/catch
- useConfig 配置保存失败回滚 UI 并提示
- handleToggle 工具切换失败回滚单个工具状态

错误处理全量补全:
- 所有 await window.metona 调用补全 try/catch 与 toast 反馈
- MCP addServer/toggleServer/removeServer 检查返回值
- showItemInFolder 检查返回值(handleOpen/handleOpenInFolder)
- sse-stream/ollama NDJSON 解析失败改为 log.warn
- adapter throwHttpError 带 status 属性供 isRetryableError 判断
This commit is contained in:
thzxx
2026-07-16 22:40:32 +08:00
parent 656c6b7af1
commit ceb8ee644d
28 changed files with 617 additions and 100 deletions
+72 -8
View File
@@ -75,24 +75,88 @@ export function createContextMenuItems(type: ContextMenuType, data?: unknown): C
case 'session': {
const sid = (data as { sessionId?: string })?.sessionId;
// 修复: 4 个 session 操作统一改为 await IPC + 失败回滚,避免 UI 与 DB 状态不一致
// 之前 archive 仅调用前端 store 未调用 IPCrename/pin/delete 未等待 IPC 结果即更新 UI
const showError = (msg: string) => {
import('metona-toast').then((mod) => mod.default.error(msg)).catch(() => {});
};
return [
{ id: 'rename', icon: Edit, label: '重命名', action: () => {
{ id: 'rename', icon: Edit, label: '重命名', action: async () => {
if (!sid) return;
// window.prompt may be unreliable in Electron; fall back to sidebar double-click rename
if (sid) { try { const t = prompt('新会话名称:'); if (t?.trim()) { window.metona?.sessions.rename(sid, t.trim()); useSessionStore.getState().updateSession(sid, { title: t.trim() }); } } catch { /* prompt unavailable, user can rename via sidebar double-click */ } }
let t: string | null = null;
try { t = prompt('新会话名称:'); } catch { /* prompt unavailable, user can rename via sidebar double-click */ return; }
if (!t?.trim()) return;
try {
const r = await window.metona?.sessions?.rename(sid, t.trim());
if (r?.success) {
useSessionStore.getState().updateSession(sid, { title: t.trim() });
} else {
showError(r?.error ?? '重命名失败');
}
} catch (err) {
console.error('[ContextMenu]', err);
showError('重命名失败');
}
}},
{ id: 'pin', icon: Pin, label: '置顶', action: () => {
if (sid) { const s = useSessionStore.getState().sessions.find((x) => x.id === sid); if (s) { window.metona?.sessions.pin(sid, !s.pinned); useSessionStore.getState().pinSession(sid, !s.pinned); } }
{ id: 'pin', icon: Pin, label: '置顶', action: async () => {
if (!sid) return;
const s = useSessionStore.getState().sessions.find((x) => x.id === sid);
if (!s) return;
const newPinned = !s.pinned;
try {
const r = await window.metona?.sessions?.pin(sid, newPinned);
if (r?.success) {
useSessionStore.getState().pinSession(sid, newPinned);
} else {
showError(r?.error ?? '置顶失败');
}
} catch (err) {
console.error('[ContextMenu]', err);
showError('置顶失败');
}
}},
// 修复: archive 之前完全未调用 IPC,UI 改了但 DB 没改,重启后状态丢失
{ id: 'archive', icon: Archive, label: '归档', action: async () => {
if (!sid) return;
const s = useSessionStore.getState().sessions.find((x) => x.id === sid);
const newArchived = s ? !s.archived : true;
try {
const r = await window.metona?.sessions?.archive(sid, newArchived);
if (r?.success) {
useSessionStore.getState().archiveSession(sid, newArchived);
} else {
showError(r?.error ?? '归档失败');
}
} catch (err) {
console.error('[ContextMenu]', err);
showError('归档失败');
}
}},
{ id: 'archive', icon: Archive, label: '归档', action: () => { if (sid) useSessionStore.getState().archiveSession(sid, true); }},
{ id: 'export', icon: FileDown, label: '导出', action: () => {
if (sid) window.metona?.sessions.getMessages(sid).then((msgs) => {
const b = new Blob([JSON.stringify(msgs, null, 2)], { type: 'application/json' });
const a = document.createElement('a'); a.href = URL.createObjectURL(b); a.download = `session-${sid}.json`; a.click();
}).catch((err) => console.error('[ContextMenu]', err));
}).catch((err) => {
console.error('[ContextMenu]', err);
showError('导出失败');
});
}},
{ id: 'delete', icon: Trash2, label: '删除', action: () => {
{ id: 'delete', icon: Trash2, label: '删除', action: async () => {
if (!sid) return;
// window.confirm may be unreliable in Electron
if (sid) { try { if (confirm('确定删除此会话?')) { window.metona?.sessions.delete(sid); useSessionStore.getState().removeSession(sid); } } catch { /* confirm unavailable */ } }
try { if (!confirm('确定删除此会话?')) return; } catch { /* confirm unavailable */ return; }
try {
const r = await window.metona?.sessions?.delete(sid);
if (r?.success) {
useSessionStore.getState().removeSession(sid);
} else {
showError(r?.error ?? '删除失败');
}
} catch (err) {
console.error('[ContextMenu]', err);
showError('删除失败');
}
}},
];
}