chore: 版本号 0.14.6 → 0.14.7
fix: onNewIteration 只在 THINKING 状态调用,消除每状态切换的冗余触发 fix: renderMessages 全量清空重建,去掉增量 diff 的边界 bug fix: 空消息跳过增加 think 判断,纯思考卡片不再被吞掉 fix: updateMessageToolRecord 改为空函数,工具卡片统一由 onDone 挂载
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/version-v0.14.6-E8734A?style=flat-square" alt="version">
|
||||
<img src="https://img.shields.io/badge/version-v0.14.7-E8734A?style=flat-square" alt="version">
|
||||
<img src="https://img.shields.io/badge/electron-33+-47848F?style=flat-square&logo=electron" alt="electron">
|
||||
<img src="https://img.shields.io/badge/typescript-5.7+-3178C6?style=flat-square&logo=typescript" alt="typescript">
|
||||
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="license">
|
||||
@@ -253,7 +253,7 @@ npm start
|
||||
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm run dist
|
||||
```
|
||||
|
||||
产出:`release/Metona Ollama Setup v0.14.6.exe`
|
||||
产出:`release/Metona Ollama Setup v0.14.7.exe`
|
||||
|
||||
## 🛠️ 常用命令
|
||||
|
||||
@@ -501,7 +501,7 @@ npm start
|
||||
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm run dist
|
||||
```
|
||||
|
||||
Output: `release/Metona Ollama Setup v0.14.6.exe`
|
||||
Output: `release/Metona Ollama Setup v0.14.7.exe`
|
||||
|
||||
## 🛠️ Common Commands
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "metona-ollama-desktop",
|
||||
"version": "0.14.6",
|
||||
"version": "0.14.7",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "metona-ollama-desktop",
|
||||
"version": "0.14.6",
|
||||
"version": "0.14.7",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ffmpeg-static": "^5.2.0",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "metona-ollama-desktop",
|
||||
"version": "0.14.6",
|
||||
"version": "0.14.7",
|
||||
"description": "Metona Ollama - TypeScript + Electron 桌面 AI 聊天客户端",
|
||||
"main": "dist/main/main.js",
|
||||
"author": "thzxx",
|
||||
|
||||
+1
-1
@@ -101,7 +101,7 @@ export function createMenu(): void {
|
||||
dialog.showMessageBox(mainWindow!, {
|
||||
type: 'info',
|
||||
title: '关于 Metona Ollama',
|
||||
message: 'Metona Ollama Desktop v0.14.6',
|
||||
message: 'Metona Ollama Desktop v0.14.7',
|
||||
detail: 'TypeScript + Electron Ollama AI 聊天客户端\n\nhttps://gitee.com/thzxx/metona-ollama',
|
||||
icon: getIconPath()
|
||||
});
|
||||
|
||||
@@ -115,37 +115,29 @@ export function renderMessages(): void {
|
||||
const currentSession = state.get<ChatSession | null>(KEYS.CURRENT_SESSION);
|
||||
const msgs = currentSession ? currentSession.messages : [];
|
||||
|
||||
// 全量清空重建,避免增量 diff 的边界 bug
|
||||
messagesContainerEl.innerHTML = '';
|
||||
currentPlaceholder = null;
|
||||
_renderedMsgIndices.clear();
|
||||
|
||||
if (msgs.length === 0) {
|
||||
emptyStateEl.style.display = '';
|
||||
messagesContainerEl.style.display = 'none';
|
||||
_renderedMsgIndices.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
emptyStateEl.style.display = 'none';
|
||||
messagesContainerEl.style.display = '';
|
||||
|
||||
// P1-1: 基于 Set 的稳定增量 diff
|
||||
const wasExisting = _renderedMsgIndices.size > 0;
|
||||
let isNewMessage = false;
|
||||
for (let i = 0; i < msgs.length; i++) {
|
||||
if (!_renderedMsgIndices.has(i)) {
|
||||
const msg = msgs[i];
|
||||
// 只跳过既无内容也无 think 也无工具的空消息
|
||||
if (msg.role === 'assistant' && !msg.content && !msg.think && (!msg.toolCalls || msg.toolCalls.length === 0)) continue;
|
||||
appendMessageDOM(msgs[i], i);
|
||||
_renderedMsgIndices.add(i);
|
||||
isNewMessage = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 清理超出范围的索引(消息被 undo/retry 移除时)
|
||||
for (const idx of _renderedMsgIndices) {
|
||||
if (idx >= msgs.length) _renderedMsgIndices.delete(idx);
|
||||
}
|
||||
|
||||
if (isNewMessage) {
|
||||
scrollToBottom();
|
||||
} else if (msgs.length > 0 && !wasExisting) {
|
||||
chatAreaEl.scrollTop = chatAreaEl.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
export function appendMessageDOM(msg: ChatMessage, index: number): void {
|
||||
@@ -520,6 +512,14 @@ export function resetCurrentPlaceholder(): void {
|
||||
}
|
||||
}
|
||||
|
||||
/** 从 DOM 移除当前 placeholder 并清空引用 */
|
||||
export function removeCurrentPlaceholder(): void {
|
||||
if (currentPlaceholder) {
|
||||
currentPlaceholder.remove();
|
||||
currentPlaceholder = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function enableAutoScroll(): void {
|
||||
autoScroll = true;
|
||||
if (scrollBtnEl) scrollBtnEl.style.display = 'none';
|
||||
|
||||
@@ -9,7 +9,7 @@ import { getSelectedModel, isThinkEnabled, isVisionAvailable, isToolCallingSuppo
|
||||
import {
|
||||
renderMessages, clearMessagesDOM, appendAssistantPlaceholder, appendSystemMessage,
|
||||
updateLastAssistantMessage, clearMessages, safeMarkdown, enableAutoScroll, resetAutoScroll,
|
||||
appendToolCallCardToPlaceholder, updateToolCallCardInPlaceholder, resetCurrentPlaceholder
|
||||
appendToolCallCardToPlaceholder, updateToolCallCardInPlaceholder, resetCurrentPlaceholder, removeCurrentPlaceholder
|
||||
} from './chat-area.js';
|
||||
import { showToast } from './toast.js';
|
||||
import { addToolCard, startToolCard, updateToolCard, clearToolCardsExternal, clearTerminalExternal, getWorkspaceDirPath, hasActiveCards, showWorkingHint, clearWorkingHint } from './workspace-panel.js';
|
||||
@@ -529,17 +529,14 @@ async function handleRetry(): Promise<void> {
|
||||
});
|
||||
},
|
||||
onNewIteration: (toolCalls) => {
|
||||
const hasToolCalls = toolCalls && toolCalls.length > 0;
|
||||
if (retryContent || hasToolCalls) {
|
||||
removeCurrentPlaceholder();
|
||||
const hasContent = !!retryContent?.trim();
|
||||
if (hasContent || retryIterations === 0) {
|
||||
const now = Date.now();
|
||||
const toolCallRecords: ToolCallRecord[] | undefined = toolCalls?.map(tc => ({
|
||||
name: tc.function.name, arguments: tc.function.arguments,
|
||||
result: null, status: 'running' as const, timestamp: now
|
||||
}));
|
||||
const prevMsg: ChatMessage = {
|
||||
role: 'assistant', content: retryContent || '', timestamp: now,
|
||||
role: 'assistant', content: retryContent || '', model: getSelectedModel(),
|
||||
timestamp: now,
|
||||
...(retryThinkContent && { think: retryThinkContent }),
|
||||
...(toolCallRecords?.length && { toolCalls: toolCallRecords }),
|
||||
};
|
||||
state.update(KEYS.CURRENT_SESSION, (s: any) => ({
|
||||
...s, messages: [...s.messages, prevMsg], updatedAt: Date.now()
|
||||
@@ -548,7 +545,6 @@ async function handleRetry(): Promise<void> {
|
||||
}
|
||||
appendAssistantPlaceholder();
|
||||
retryContent = '';
|
||||
retryThinkContent = '';
|
||||
retryIterations++;
|
||||
},
|
||||
onThinking: (thinking) => { retryThinkContent = thinking; updateLastAssistantMessage(retryContent, thinking, null, getSelectedModel()); },
|
||||
@@ -1090,27 +1086,7 @@ export async function sendMessage(): Promise<void> {
|
||||
|
||||
/** 更新当前会话消息中最近一个 'running' 状态的同名工具记录 */
|
||||
function updateMessageToolRecord(toolName: string, status: 'success' | 'error', result: Record<string, unknown>): void {
|
||||
let updated = false;
|
||||
state.update(KEYS.CURRENT_SESSION, (session: any) => {
|
||||
if (!session) return session;
|
||||
const messages = [...session.messages];
|
||||
// 从后往前找最近一条包含该工具 running 状态记录的消息
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const msg = messages[i];
|
||||
if (!msg.toolCalls) continue;
|
||||
const idx = msg.toolCalls.findIndex((tc: any) => tc.name === toolName && tc.status === 'running');
|
||||
if (idx !== -1) {
|
||||
const updatedRecords = [...msg.toolCalls];
|
||||
updatedRecords[idx] = { ...updatedRecords[idx], status, result };
|
||||
messages[i] = { ...msg, toolCalls: updatedRecords };
|
||||
updated = true;
|
||||
return { ...session, messages, updatedAt: Date.now() };
|
||||
}
|
||||
}
|
||||
return session;
|
||||
});
|
||||
// 状态更新后重新渲染消息,使工具卡片状态即时生效
|
||||
if (updated) renderMessages();
|
||||
// 工具卡片统一由 onDone 挂载,不在执行中更新消息记录
|
||||
}
|
||||
|
||||
async function sendMessageWithAgentLoop(text: string, currentSession: ChatSession, model: string): Promise<void> {
|
||||
@@ -1255,10 +1231,7 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio
|
||||
|
||||
let assistantContent = '';
|
||||
let thinkContent = '';
|
||||
let agentModeIterations = 0; // 跟踪 onNewIteration 调用次数
|
||||
state.set('_currentEvalCount', 0); // 重置累计 token 计数,防止跨会话残留
|
||||
let lastIterationEvalCount = 0; // 上一轮迭代结束时的累计 token 数
|
||||
let lastIterationStartTime = Date.now(); // 当前迭代开始时间
|
||||
state.set('_currentEvalCount', 0);
|
||||
|
||||
// ── 监控定时器:流式输出期间持续显示工作提示 ──
|
||||
// 模型生成大参数 tool_call 可能需要几分钟,期间工作空间显示提示
|
||||
@@ -1278,36 +1251,13 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio
|
||||
});
|
||||
},
|
||||
onNewIteration: (toolCalls) => {
|
||||
// Agent Loop 新迭代开始:保存上一轮内容为独立消息,创建新 placeholder
|
||||
// 先移除旧 placeholder(防止被 renderMessages 当作已渲染消息计数)
|
||||
const oldPlaceholder = document.querySelector('#messagesContainer .message.assistant.loading') as HTMLElement;
|
||||
if (oldPlaceholder) oldPlaceholder.remove();
|
||||
|
||||
// 有文本内容 或 有工具调用 → 保存为独立消息
|
||||
const hasToolCalls = toolCalls && toolCalls.length > 0;
|
||||
if (assistantContent || hasToolCalls) {
|
||||
const now = Date.now();
|
||||
// 计算本轮迭代的 token 和时长
|
||||
const currentEvalCount = state.get<number>('_currentEvalCount', 0);
|
||||
const iterationEvalCount = Math.max(0, currentEvalCount - lastIterationEvalCount);
|
||||
const iterationDuration = (now - lastIterationStartTime) * 1e6;
|
||||
|
||||
const toolCallRecords: ToolCallRecord[] | undefined = toolCalls?.map(tc => ({
|
||||
name: tc.function.name,
|
||||
arguments: tc.function.arguments,
|
||||
result: null,
|
||||
status: 'running' as const,
|
||||
timestamp: now
|
||||
}));
|
||||
|
||||
// 保存上一轮的卡片(不含工具记录,工具统一在 onDone 挂载)
|
||||
const prevMsg: ChatMessage = {
|
||||
role: 'assistant',
|
||||
content: assistantContent || '',
|
||||
timestamp: now,
|
||||
model: getSelectedModel(),
|
||||
timestamp: Date.now(),
|
||||
...(thinkContent && { think: thinkContent }),
|
||||
...(toolCallRecords?.length && { toolCalls: toolCallRecords }),
|
||||
...(iterationEvalCount > 0 && { eval_count: iterationEvalCount }),
|
||||
...(iterationDuration > 0 && { total_duration: iterationDuration }),
|
||||
};
|
||||
state.update(KEYS.CURRENT_SESSION, (session: any) => ({
|
||||
...session,
|
||||
@@ -1316,14 +1266,10 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio
|
||||
}));
|
||||
renderMessages();
|
||||
|
||||
lastIterationEvalCount = currentEvalCount;
|
||||
lastIterationStartTime = Date.now();
|
||||
}
|
||||
// 创建新 placeholder,重置内容
|
||||
removeCurrentPlaceholder();
|
||||
appendAssistantPlaceholder();
|
||||
assistantContent = '';
|
||||
thinkContent = '';
|
||||
agentModeIterations++;
|
||||
},
|
||||
onThinking: (thinking) => {
|
||||
thinkContent = thinking;
|
||||
@@ -1372,34 +1318,9 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio
|
||||
}
|
||||
},
|
||||
onDone: async (finalContent, toolRecords, loopStats) => {
|
||||
assistantContent = finalContent;
|
||||
|
||||
// 如果有多个迭代,最后一条消息已由 onNewIteration 保存,这里创建新消息保存最终回复
|
||||
if (agentModeIterations > 0) {
|
||||
if (assistantContent) {
|
||||
const lastIterDuration = (Date.now() - lastIterationStartTime) * 1e6;
|
||||
const lastMsg: ChatMessage = {
|
||||
role: 'assistant',
|
||||
content: assistantContent,
|
||||
model: getSelectedModel(),
|
||||
timestamp: Date.now(),
|
||||
...(thinkContent && { think: thinkContent }),
|
||||
...(toolRecords?.length && { toolCalls: toolRecords }),
|
||||
...(loopStats?.eval_count && { eval_count: loopStats.eval_count }),
|
||||
...(loopStats?.prompt_eval_count && { prompt_eval_count: loopStats.prompt_eval_count }),
|
||||
...(loopStats?.total_duration && { total_duration: loopStats.total_duration }),
|
||||
};
|
||||
state.update(KEYS.CURRENT_SESSION, (session: any) => ({
|
||||
...session,
|
||||
messages: [...session.messages, lastMsg],
|
||||
updatedAt: Date.now()
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
// 单迭代模式:正常保存
|
||||
const assistantMsg: ChatMessage = {
|
||||
role: 'assistant',
|
||||
content: assistantContent || '',
|
||||
content: finalContent || '',
|
||||
model: getSelectedModel(),
|
||||
timestamp: Date.now(),
|
||||
...(thinkContent && { think: thinkContent }),
|
||||
@@ -1413,9 +1334,7 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio
|
||||
messages: [...session.messages, assistantMsg],
|
||||
updatedAt: Date.now()
|
||||
}));
|
||||
}
|
||||
// 清掉流式 placeholder,统一从 session 渲染(单条消息,Markdown 格式)
|
||||
clearMessagesDOM();
|
||||
|
||||
renderMessages();
|
||||
|
||||
// ── 保存 Plan Mode 最终进度到会话(供下一轮 Loop 注入)──
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<div class="header-left">
|
||||
<span class="logo">🦙</span>
|
||||
<span class="app-title">Metona Ollama</span>
|
||||
<span class="app-version">v0.14.6</span>
|
||||
<span class="app-version">v0.14.7</span>
|
||||
<button class="icon-btn help-btn" id="btnHelp" title="使用帮助">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
|
||||
|
||||
@@ -1946,8 +1946,8 @@ export async function runAgentLoop(
|
||||
ctx.maxLoops = newMax;
|
||||
}
|
||||
|
||||
// 非首轮迭代:通知 UI
|
||||
if (ctx.loopCount > 0 && callbacks.onNewIteration) {
|
||||
// 仅在 THINKING 阶段通知 UI(一次迭代只通知一次,不是每个状态切换都通知)
|
||||
if (ctx.loopCount > 0 && ctx.state === S.THINKING && callbacks.onNewIteration) {
|
||||
callbacks.onNewIteration(ctx.prevToolCalls.length > 0 ? ctx.prevToolCalls : undefined);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user