diff --git a/README.md b/README.md
index 8bdb574..eb9fe06 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@
-
+
@@ -252,7 +252,7 @@ npm start
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm run dist
```
-产出:`release/Metona Ollama Setup v0.13.1.exe`
+产出:`release/Metona Ollama Setup v0.13.2.exe`
## 🛠️ 常用命令
@@ -499,7 +499,7 @@ npm start
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm run dist
```
-Output: `release/Metona Ollama Setup v0.13.1.exe`
+Output: `release/Metona Ollama Setup v0.13.2.exe`
## 🛠️ Common Commands
diff --git a/package-lock.json b/package-lock.json
index b9c6329..1f8ea30 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "metona-ollama-desktop",
- "version": "0.13.1",
+ "version": "0.13.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "metona-ollama-desktop",
- "version": "0.13.1",
+ "version": "0.13.2",
"license": "MIT",
"dependencies": {
"ffmpeg-static": "^5.2.0",
diff --git a/package.json b/package.json
index d198323..c2bcd7c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "metona-ollama-desktop",
- "version": "0.13.1",
+ "version": "0.13.2",
"description": "Metona Ollama - TypeScript + Electron 桌面 AI 聊天客户端",
"main": "dist/main/main.js",
"author": "thzxx",
diff --git a/src/main/menu.ts b/src/main/menu.ts
index 6c2e3c5..b779b3c 100644
--- a/src/main/menu.ts
+++ b/src/main/menu.ts
@@ -101,7 +101,7 @@ export function createMenu(): void {
dialog.showMessageBox(mainWindow!, {
type: 'info',
title: '关于 Metona Ollama',
- message: 'Metona Ollama Desktop v0.13.1',
+ message: 'Metona Ollama Desktop v0.13.2',
detail: 'TypeScript + Electron Ollama AI 聊天客户端\n\nhttps://gitee.com/thzxx/metona-ollama',
icon: getIconPath()
});
diff --git a/src/renderer/components/input-area.ts b/src/renderer/components/input-area.ts
index 7dd2e67..f67d20e 100644
--- a/src/renderer/components/input-area.ts
+++ b/src/renderer/components/input-area.ts
@@ -19,7 +19,7 @@ import { runAgentLoop } from '../services/agent-engine.js';
import { estimateTokens } from '../services/context-manager.js';
import { showToolConfirm } from './tool-confirm-modal.js';
import { logInfo, logStream, logError, logSuccess, logWarn, resetVideoProgress, updateVideoProgress } from '../services/log-service.js';
-import type { ChatSession, ChatMessage, OllamaStreamChunk, FileContent, ChatFile, ToolCallRecord, AgentMode } from '../types.js';
+import type { ChatSession, ChatMessage, OllamaStreamChunk, OllamaMessage, FileContent, ChatFile, ToolCallRecord, AgentMode } from '../types.js';
let chatInputEl: HTMLTextAreaElement;
let btnSendEl: HTMLButtonElement;
@@ -489,18 +489,13 @@ async function handleRetry(): Promise {
updateSendButton(true);
// 始终走 Agent Loop
- const historyMessages = currentSession.messages
+ // ── 构建历史消息(不含最后一条用户消息,因为它是重试目标)──
+ const historyMessages = buildHistoryMessages(
+ currentSession.messages
.filter(m => m.role === 'user' || m.role === 'assistant')
- .slice(0, -1) // 不包含刚保留的最后一条 user 消息
- .slice(-20)
- .map(m => {
- let content = m.content || '';
- if (m._fileContents?.length) {
- const fileParts = buildFileContentParts(m._fileContents);
- content = content ? content + '\n\n---\n' + fileParts.join('\n\n---\n') : fileParts.join('\n\n---\n');
- }
- return { role: m.role, content, ...(m.images?.length && { images: m.images }), ...((m as any)._videoFrames?.length && { images: [...(m.images || []), ...(m as any)._videoFrames.map((f: any) => f.base64)] }) };
- });
+ .slice(0, -1), // 不包含刚保留的最后一条 user 消息
+ 30
+ );
let retryMonitor: ReturnType | null = null;
@@ -620,6 +615,16 @@ async function handleRetry(): Promise {
updateLastAssistantMessage(finalContent, retryThinkContent || null, loopStats || null, getSelectedModel(), Date.now(), toolRecords?.length ? toolRecords : undefined);
}
renderMessages();
+ // ── 保存 Plan Mode 最终进度到会话(供下一轮 Loop 注入)──
+ const lastPlanStatus = state.get('_lastPlanStatus', '');
+ if (lastPlanStatus) {
+ state.set('_lastPlanStatus', '');
+ state.update(KEYS.CURRENT_SESSION, (s: any) => ({
+ ...s,
+ messages: [...s.messages, { role: 'system', content: lastPlanStatus, timestamp: Date.now() }],
+ updatedAt: Date.now()
+ }));
+ }
await saveCurrentSession();
if (loopStats?.prompt_eval_count) updateCtxRemain(loopStats.prompt_eval_count);
}
@@ -974,6 +979,86 @@ function buildApiMessages(messages: ChatMessage[]): Array<{ role: string; conten
});
}
+/**
+ * 从会话消息构建 Ollama 格式的历史消息列表。
+ * 正确注入 tool_calls 和 role:'tool' 结果消息,
+ * 确保 AI 在新 Agent Loop 中能看到上一轮已执行的工具及其结果。
+ */
+function buildHistoryMessages(msgs: ChatMessage[], maxCount = 20): OllamaMessage[] {
+ const result: OllamaMessage[] = [];
+
+ for (const msg of msgs) {
+ if (msg.role !== 'user' && msg.role !== 'assistant' && msg.role !== 'system') continue;
+ if (result.length >= maxCount) break;
+
+ let content = msg.content || '';
+
+ if (msg.role === 'system') {
+ // 系统消息(Plan Mode 进度状态等)
+ result.push({ role: 'system', content });
+ continue;
+ }
+
+ if (msg.role === 'user') {
+ // 用户消息:注入文件内容
+ if (msg._fileContents?.length) {
+ const fileParts = buildFileContentParts(msg._fileContents);
+ content = content ? content + '\n\n---\n' + fileParts.join('\n\n---\n') : fileParts.join('\n\n---\n');
+ }
+ result.push({
+ role: 'user',
+ content,
+ ...(msg.images?.length && { images: msg.images }),
+ ...((msg as any)._videoFrames?.length && { images: [...(msg.images || []), ...(msg as any)._videoFrames.map((f: any) => f.base64)] }),
+ });
+ } else if (msg.role === 'assistant') {
+ // 构建 assistant 消息
+ const assistantMsg: OllamaMessage = {
+ role: 'assistant',
+ content,
+ ...(msg.think && { thinking: msg.think }),
+ ...(msg.images?.length && { images: msg.images }),
+ };
+
+ // ── 注入 tool_calls(Ollama 格式)──
+ if (msg.toolCalls?.length) {
+ assistantMsg.tool_calls = msg.toolCalls.map(tc => ({
+ type: 'function' as const,
+ function: {
+ name: tc.name,
+ arguments: tc.arguments,
+ },
+ }));
+ }
+
+ result.push(assistantMsg);
+
+ // ── 注入 tool 结果消息(role: 'tool')──
+ if (msg.toolCalls?.length) {
+ for (const tc of msg.toolCalls) {
+ if (!tc.result) continue;
+ if (result.length >= maxCount) break; // 避免截断处产生孤立 tool 消息
+ const resultContent = tc.status === 'success'
+ ? JSON.stringify(tc.result)
+ : JSON.stringify({ success: false, error: tc.result.error || '工具执行失败' });
+ result.push({
+ role: 'tool',
+ tool_name: tc.name,
+ content: resultContent,
+ });
+ }
+ }
+ }
+ }
+
+ // 如果超过 maxCount,从尾部截取(保留最新消息)
+ if (result.length > maxCount) {
+ return result.slice(-maxCount);
+ }
+
+ return result;
+}
+
export async function sendMessage(): Promise {
const text = chatInputEl.value.trim();
@@ -1182,22 +1267,11 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio
updateSendButton(true);
// 构建历史消息
- const historyMessages = freshSession.messages
- .filter(m => m.role === 'user' || m.role === 'assistant')
- .slice(-20)
- .map(m => {
- let content = m.content || '';
- if (m._fileContents && m._fileContents.length > 0) {
- const fileParts = buildFileContentParts(m._fileContents);
- content = content ? content + '\n\n---\n' + fileParts.join('\n\n---\n') : fileParts.join('\n\n---\n');
- }
- return {
- role: m.role,
- content,
- ...(m.images?.length && { images: m.images }),
- ...((m as any)._videoFrames?.length && { images: [...(m.images || []), ...(m as any)._videoFrames.map((f: any) => f.base64)] })
- };
- });
+ // ── 构建历史消息(含工具调用和结果,消除跨 Loop 上下文断裂)──
+ const historyMessages = buildHistoryMessages(
+ freshSession.messages.filter(m => m.role === 'user' || m.role === 'assistant'),
+ 30 // 预留空间给 tool 结果消息,实际有效轮次仍约 20 轮
+ );
let assistantContent = '';
let thinkContent = '';
@@ -1362,6 +1436,16 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio
updateLastAssistantMessage(assistantContent, thinkContent || null, loopStats || null, getSelectedModel(), Date.now(), toolRecords?.length ? toolRecords : undefined);
}
renderMessages();
+ // ── 保存 Plan Mode 最终进度到会话(供下一轮 Loop 注入)──
+ const lastPlanStatus2 = state.get('_lastPlanStatus', '');
+ if (lastPlanStatus2) {
+ state.set('_lastPlanStatus', '');
+ state.update(KEYS.CURRENT_SESSION, (session: any) => ({
+ ...session,
+ messages: [...session.messages, { role: 'system', content: lastPlanStatus2, timestamp: Date.now() }],
+ updatedAt: Date.now()
+ }));
+ }
await saveCurrentSession();
// 更新剩余上下文
if (loopStats?.prompt_eval_count) updateCtxRemain(loopStats.prompt_eval_count);
diff --git a/src/renderer/index.html b/src/renderer/index.html
index 5e4e374..dd2db06 100644
--- a/src/renderer/index.html
+++ b/src/renderer/index.html
@@ -28,7 +28,7 @@