v0.10.1: 修复显存泄漏 + 删除普通聊天模式 + Tool Calling 始终开启

fix: ollama.ts - chat() 添加默认 keep_alive: '1h',chatStream done 时不再 reader.cancel()
refactor: 删除普通聊天模式,所有消息统一走 Agent Loop
refactor: Tool Calling 开关始终开启且不可关闭
chore: 版本号 0.10.0 → 0.10.1 (12 处)
docs: 更新帮助面板和 README 反映架构变更
This commit is contained in:
thzxx
2026-06-06 12:06:48 +08:00
parent c9adc764ae
commit 64264cc0f8
13 changed files with 47 additions and 379 deletions
+5 -5
View File
@@ -14,7 +14,7 @@
</p>
<p align="center">
<img src="https://img.shields.io/badge/version-v0.10.0-E8734A?style=flat-square" alt="version">
<img src="https://img.shields.io/badge/version-v0.10.1-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">
@@ -34,7 +34,7 @@
| | 功能 | 说明 |
|:---:|:---|:---|
| 🤖 | **ReAct Agent Loop** | Thought → Action → Observation → Reflection 完整循环,最大 85 轮(可配置),自动重试 2 次,工具去重,并行/链式执行 |
| 🤖 | **ReAct Agent Loop** | 始终开启的唯一对话模式。Thought → Action → Observation → Reflection 完整循环,最大 85 轮(可配置),自动重试 2 次,工具去重,并行/链式执行 |
| 🔧 | **38 个内置工具** | 文件系统 · 命令执行 · 联网搜索 · 浏览器控制 · Git · 记忆 · 技能 · 会话 · 子代理 |
| 🧠 | **智能记忆系统** | 三类记忆(fact / preference / rule),FTS5 全文搜索 + 向量语义搜索,写入前安全扫描,容量 500 条,90 天衰减 |
| 📋 | **自定义文件** | SOUL.md(人格,不可压缩)+ AGENT.md(行为准则)+ USER.md(用户画像),工作空间优先,内置默认 fallback |
@@ -231,7 +231,7 @@ npm start
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm run dist
```
产出:`release/Metona Ollama Setup v0.10.0.exe`
产出:`release/Metona Ollama Setup v0.10.1.exe`
## 🛠️ 常用命令
@@ -261,7 +261,7 @@ npm run dist # 构建 Windows 安装包
| | Feature | Description |
|:---:|:---|:---|
| 🤖 | **ReAct Agent Loop** | Full Thought → Action → Observation → Reflection cycle, up to 85 iterations (configurable), auto-retry 2 times, tool dedup, parallel/chain execution |
| 🤖 | **ReAct Agent Loop** | Always-on, only chat mode. Full Thought → Action → Observation → Reflection cycle, up to 85 iterations (configurable), auto-retry 2 times, tool dedup, parallel/chain execution |
| 🔧 | **38 Built-in Tools** | File system · Command · Web search · Browser · Git · Memory · Skills · Sessions · Sub-agent |
| 🧠 | **Smart Memory System** | Three types (fact / preference / rule), FTS5 + vector semantic search, pre-write security scan, 500 capacity, 90-day decay |
| 📋 | **Custom Files** | SOUL.md (persona, never compressed) + AGENT.md (behavior rules) + USER.md (user profile), workspace-first with built-in defaults |
@@ -454,7 +454,7 @@ npm start
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm run dist
```
Output: `release/Metona Ollama Setup v0.10.0.exe`
Output: `release/Metona Ollama Setup v0.10.1.exe`
## 🛠️ Common Commands
+3 -3
View File
@@ -91,7 +91,7 @@ npm run dist # NSIS 安装包
| 文件 | 说明 |
|------|------|
| `release/Metona Ollama Setup v0.10.0.exe` | NSIS 安装包(可选目录、创建快捷方式) |
| `release/Metona Ollama Setup v0.10.1.exe` | NSIS 安装包(可选目录、创建快捷方式) |
> 仅提供 NSIS 安装包。
@@ -103,7 +103,7 @@ npm run dist # NSIS 安装包
TOKEN="your_access_token"
curl -X POST "https://gitee.com/api/v5/repos/thzxx/metona-ollama-desktop/releases?access_token=$TOKEN" \
-H "Content-Type: application/json" \
-d '{"tag_name":"v0.10.0","name":"v0.10.0","body":"Release notes","target_commitish":"master"}'
-d '{"tag_name":"v0.10.1","name":"v0.10.1","body":"Release notes","target_commitish":"master"}'
```
### 上传附件
@@ -112,7 +112,7 @@ curl -X POST "https://gitee.com/api/v5/repos/thzxx/metona-ollama-desktop/release
TOKEN="your_access_token"
RELEASE_ID="<release_id>"
curl -X POST "https://gitee.com/api/v5/repos/thzxx/metona-ollama-desktop/releases/$RELEASE_ID/attach_files?access_token=$TOKEN" \
-H "Content-Type: multipart/form-data" -F "file=@release/Metona Ollama Setup v0.10.0.exe"
-H "Content-Type: multipart/form-data" -F "file=@release/Metona Ollama Setup v0.10.1.exe"
```
## 故障排查
+1 -1
View File
@@ -1,6 +1,6 @@
# Metona Ollama Desktop — 开发规范
> 版本: v0.10.0 | 更新: 2026-06-05 | 维护: 项目团队
> 版本: v0.10.1 | 更新: 2026-06-05 | 维护: 项目团队
---
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "metona-ollama-desktop",
"version": "0.10.0",
"version": "0.10.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "metona-ollama-desktop",
"version": "0.10.0",
"version": "0.10.1",
"license": "MIT",
"dependencies": {
"sql.js": "^1.11.0"
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "metona-ollama-desktop",
"version": "0.10.0",
"version": "0.10.1",
"description": "Metona Ollama - TypeScript + Electron 桌面 AI 聊天客户端",
"main": "dist/main/main.js",
"author": "thzxx",
@@ -48,7 +48,7 @@
"requestedExecutionLevel": "asInvoker"
},
"nsis": {
"artifactName": "Metona Ollama Setup v0.10.0.${ext}",
"artifactName": "Metona Ollama Setup v0.10.1.${ext}",
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true,
+1 -1
View File
@@ -101,7 +101,7 @@ export function createMenu(): void {
dialog.showMessageBox(mainWindow!, {
type: 'info',
title: '关于 Metona Ollama',
message: 'Metona Ollama Desktop v0.10.0',
message: 'Metona Ollama Desktop v0.10.1',
detail: 'TypeScript + Electron Ollama AI 聊天客户端\n\nhttps://gitee.com/thzxx/metona-ollama',
icon: getIconPath()
});
+2 -1
View File
@@ -56,6 +56,7 @@ export class OllamaAPI {
model: params.model,
messages: params.messages,
stream: false,
keep_alive: '1h',
};
if (params.think !== undefined) body.think = params.think;
if (params.system) body.system = params.system;
@@ -117,7 +118,7 @@ export class OllamaAPI {
const chunk: OllamaStreamChunk = JSON.parse(line);
if (onChunk) onChunk(chunk);
if (chunk.done) {
reader.cancel();
// 不调用 reader.cancel() — TCP RST 可能导致 Ollama 不执行 keep_alive
return;
}
} catch {
+10 -332
View File
@@ -327,22 +327,19 @@ async function handleRetry(): Promise<void> {
clearMessagesDOM();
renderMessages();
// 重新触发 Agent Loop / 普通聊天
const toolCallingEnabled = state.get<boolean>('toolCallingEnabled', false);
// 重新触发 Agent Loop
const model = getSelectedModel() || currentSession.model;
// 新一轮开始:清理上一轮的工作空间状态
if (toolCallingEnabled) {
clearToolCardsExternal();
clearTerminalExternal();
}
clearToolCardsExternal();
clearTerminalExternal();
appendAssistantPlaceholder();
state.set(KEYS.IS_STREAMING, true);
updateSendButton(true);
if (toolCallingEnabled) {
const historyMessages = currentSession.messages
// 始终走 Agent Loop
const historyMessages = currentSession.messages
.filter(m => m.role === 'user' || m.role === 'assistant')
.slice(0, -1) // 不包含刚保留的最后一条 user 消息
.slice(-20)
@@ -441,16 +438,8 @@ async function handleRetry(): Promise<void> {
}
});
} catch (err) {
logError('重试失败', (err as Error).message);
appendSystemMessage(`❌ 重试失败: ${(err as Error).message}`);
}
} else {
// 普通聊天模式重试 - 直接调用 sendMessage
chatInputEl.value = userMsg.content || '';
state.set(KEYS.IS_STREAMING, false);
updateSendButton(false);
await sendMessage();
return;
logError('重试失败', (err as Error).message);
appendSystemMessage(`❌ 重试失败: ${(err as Error).message}`);
}
state.set(KEYS.IS_STREAMING, false);
@@ -770,320 +759,9 @@ export async function sendMessage(): Promise<void> {
const currentSession = state.get<ChatSession | null>(KEYS.CURRENT_SESSION);
if (!currentSession) return;
// ── Tool Calling Agent Loop 分支 ──
const toolCallingEnabled = state.get<boolean>('toolCallingEnabled', false);
if (toolCallingEnabled) {
// 检查模型是否支持 Tool Calling
if (!isToolCallingSupported()) {
appendSystemMessage('⚠️ 当前模型未声明支持 Tool CallingAgent Loop 可能不稳定。建议使用 Qwen3、Llama 3.1+ 或 Mistral 等支持工具调用的模型。');
}
await sendMessageWithAgentLoop(text, currentSession, model);
return;
}
// ── 原有普通聊天流程 ──
const now = Date.now();
const msgsToAdd: ChatMessage[] = [];
const userFiles: ChatFile[] = pendingFiles.map(f => ({ name: f.name, language: f.language, size: f.size }));
if (pendingImages.length > 0) {
msgsToAdd.push({
role: 'user',
content: pendingImages.length === 1 ? `[上传了图片: ${pendingImages[0].name}]` : `[上传了 ${pendingImages.length} 张图片]`,
images: pendingImages.map(img => img.base64),
timestamp: now
});
}
if (text || pendingFiles.length > 0) {
const msg: ChatMessage = {
role: 'user',
content: text || '',
timestamp: now
};
if (userFiles.length > 0) {
msg.files = userFiles;
msg._fileContents = pendingFiles.map(f => ({ language: f.language, content: f.content }));
}
msgsToAdd.push(msg);
}
const isFirstMsg = currentSession.messages.length === 0;
state.update(KEYS.CURRENT_SESSION, (session: any) => ({
...session,
...(isFirstMsg && {
title: truncate(text || (pendingFiles.length > 0 ? `[文件: ${pendingFiles.map(f => f.name).join(', ')}]` : '[图片消息]'), 30),
model
}),
messages: [...session.messages, ...msgsToAdd],
updatedAt: Date.now()
}));
const freshSession = state.get<ChatSession>(KEYS.CURRENT_SESSION);
enableAutoScroll();
renderMessages();
await saveCurrentSession();
chatInputEl.value = '';
pendingImages = [];
pendingFiles = [];
imagePreviewEl.style.display = 'none';
imagePreviewEl.innerHTML = '';
filePreviewEl.style.display = 'none';
filePreviewEl.innerHTML = '';
autoResizeTextarea();
appendAssistantPlaceholder();
state.set(KEYS.IS_STREAMING, true);
updateSendButton(true);
const api = state.get<OllamaAPI>(KEYS.API)!;
const abortController = new AbortController();
state.set(KEYS.ABORT_CONTROLLER, abortController);
const thinkMode = isThinkEnabled();
const numCtx = state.get<number>(KEYS.NUM_CTX, 24576);
let assistantContent = '';
let thinkContent = '';
let finalStats: any = null;
let modelName = '';
try {
const apiMessages = buildApiMessages(freshSession.messages);
const temperature = state.get<number>('temperature', 0.7);
const chatParams: Record<string, unknown> = {
model,
messages: apiMessages,
stream: true,
think: thinkMode,
};
if (numCtx) chatParams.options = { num_ctx: numCtx, temperature };
// ── 扫描工作空间 SOUL.md ──
let soulContent = '';
let soulMdContent = '';
const workspaceDir = getWorkspaceDirPath();
if (workspaceDir) {
try {
const soulResult = await window.metonaDesktop?.workspace.readFile(
workspaceDir.replace(/\/+$/, '') + '/SOUL.md'
);
if (soulResult?.success && soulResult.content) {
soulMdContent = soulResult.content;
}
} catch { /* 工作空间 SOUL.md 不存在 */ }
}
// fallback:读取内置 SOUL.md
if (!soulMdContent) {
try {
const resp = await fetch('./SOUL.md');
if (resp.ok) {
soulMdContent = await resp.text();
}
} catch { /* 内置 SOUL.md 也不可用 */ }
}
if (soulMdContent) {
soulContent = `[SOUL.md] ${soulMdContent}`;
}
// ── 扫描工作空间 USER.md ──
let userMdContent = '';
if (workspaceDir) {
try {
const userResult = await window.metonaDesktop?.workspace.readFile(
workspaceDir.replace(/\/+$/, '') + '/USER.md'
);
if (userResult?.success && userResult.content) {
userMdContent = userResult.content;
}
} catch { /* 工作空间 USER.md 不存在 */ }
}
if (!userMdContent) {
try {
const resp = await fetch('./USER.md');
if (resp.ok) {
userMdContent = await resp.text();
}
} catch { /* 内置 USER.md 也不可用 */ }
}
// ── Agent 记忆注入 ──
if (isMemoryEnabled()) {
const userMessage = text || (msgsToAdd.find(m => m.role === 'user')?.content || '');
if (userMessage) {
const relevantMemories = searchMemories(userMessage, 6);
if (relevantMemories.length > 0) {
const memoryContext = buildMemoryContext(relevantMemories);
const parts = [soulContent, userMdContent ? `[USER.md] ${userMdContent}` : '', memoryContext].filter(Boolean);
chatParams.system = parts.join('\n\n');
for (const m of relevantMemories) {
await markMemoryUsed(m.id);
}
appendSystemMessage(`🧠 已注入 ${relevantMemories.length} 条相关记忆`, 'memory-status');
}
}
}
if (soulContent && !chatParams.system) {
const parts = [soulContent, userMdContent ? `[USER.md] ${userMdContent}` : ''].filter(Boolean);
chatParams.system = parts.join('\n\n');
}
// 存入 state 供 chat-area 渲染系统提示词卡片
state.set('_lastSystemPrompt', chatParams.system || '');
await api.chatStream(chatParams as any, (chunk: OllamaStreamChunk) => {
if (chunk.message) {
if (chunk.message.content) assistantContent += chunk.message.content;
const think = chunk.message.thinking || chunk.message.reasoning_content;
if (think && typeof think === 'string') thinkContent += think;
}
if (chunk.model) modelName = chunk.model;
// 捕获任何包含统计的 chunk(不限于 done)
if (chunk.eval_count || chunk.prompt_eval_count || chunk.total_duration) {
finalStats = {
eval_count: chunk.eval_count ?? finalStats?.eval_count,
prompt_eval_count: chunk.prompt_eval_count ?? finalStats?.prompt_eval_count,
total_duration: chunk.total_duration ?? finalStats?.total_duration,
};
}
updateLastAssistantMessage(assistantContent, thinkContent || null, null, modelName || undefined);
if (chunk.done) {
// done chunk 优先使用其统计,保留之前的兜底
finalStats = {
eval_count: chunk.eval_count ?? finalStats?.eval_count,
prompt_eval_count: chunk.prompt_eval_count ?? finalStats?.prompt_eval_count,
total_duration: chunk.total_duration ?? finalStats?.total_duration,
};
}
}, abortController);
if (!assistantContent && !thinkContent) {
appendSystemMessage('⚠️ 模型未返回任何内容,可能是上下文过长或模型不支持当前请求');
logWarn('模型未返回任何内容');
}
logStream(`流式完成: ${assistantContent.length} 字符${finalStats?.eval_count ? `, ${finalStats.eval_count} tokens` : ''}`);
const assistantMsg: ChatMessage = {
role: 'assistant',
content: assistantContent || '',
timestamp: Date.now(),
...(modelName && { model: modelName }),
...(thinkContent && { think: thinkContent }),
...((finalStats || {}) as any)
};
state.update(KEYS.CURRENT_SESSION, (session: any) => ({
...session,
messages: [...session.messages, assistantMsg],
updatedAt: Date.now()
}));
if (finalStats) {
updateLastAssistantMessage(assistantContent, thinkContent || null, finalStats, modelName || undefined, Date.now());
} else {
updateLastAssistantMessage(assistantContent, thinkContent || null, null, modelName || undefined, Date.now());
}
await saveCurrentSession();
// ── 自动提取记忆(非阻塞)──
if (isMemoryEnabled() && freshSession.messages.length >= 10) {
const currentSession2 = state.get<ChatSession | null>(KEYS.CURRENT_SESSION);
if (currentSession2) {
extractMemoriesFromConversation(
currentSession2.messages.map(m => ({ role: m.role, content: m.content })),
currentSession2.title
).then(count => {
if (count > 0) {
logInfo(`自动提取了 ${count} 条记忆`);
}
}).catch(err => logError('记忆提取失败', (err as Error).message));
}
}
} catch (err) {
if ((err as Error).name === 'AbortError') {
const placeholder = document.querySelector('#messagesContainer .message.assistant.loading') as HTMLElement;
const loadingDots = placeholder?.querySelector('.loading-dots');
const loadingText = placeholder?.querySelector('.loading-text');
if (loadingDots) loadingDots.remove();
if (loadingText) loadingText.remove();
placeholder?.classList.remove('loading');
const contentDiv = placeholder?.querySelector('.msg-content');
const partialMsg: ChatMessage = {
role: 'assistant',
content: assistantContent,
timestamp: Date.now(),
...(modelName && { model: modelName }),
...(thinkContent && { think: thinkContent }),
stopped: true
};
state.update(KEYS.CURRENT_SESSION, (session: any) => ({
...session,
messages: [...session.messages, partialMsg],
updatedAt: Date.now()
}));
if (contentDiv) {
let html = assistantContent ? safeMarkdown(assistantContent) : '';
html += '<p><code>[已停止]</code></p>';
contentDiv.innerHTML = html;
}
appendSystemMessage('⏹ 已停止生成');
await saveCurrentSession();
state.set(KEYS.IS_STREAMING, false);
updateSendButton(false);
state.set(KEYS.ABORT_CONTROLLER, null);
return;
}
const placeholder = document.querySelector('#messagesContainer .message.assistant.loading') as HTMLElement;
const loadingDots = placeholder?.querySelector('.loading-dots');
const loadingText = placeholder?.querySelector('.loading-text');
if (loadingDots) loadingDots.remove();
if (loadingText) loadingText.remove();
placeholder?.classList.remove('loading');
const contentDiv = placeholder?.querySelector('.msg-content');
if (assistantContent && contentDiv) {
state.update(KEYS.CURRENT_SESSION, (session: any) => ({
...session,
messages: [...session.messages, { role: 'assistant', content: assistantContent + '\n\n`[已中断]`', timestamp: Date.now() } as ChatMessage],
updatedAt: Date.now()
}));
contentDiv.innerHTML = safeMarkdown(assistantContent) + '<p><code>[已中断]</code></p>';
await saveCurrentSession();
} else {
if (placeholder) placeholder.remove();
}
let errMsg = `❌ 错误: ${(err as Error).message}`;
if ((err as Error).message.includes('Failed to fetch') || (err as Error).message.includes('NetworkError')) {
errMsg = '❌ 连接失败。请检查:\n1. Ollama 是否正在运行\n2. 地址是否正确\n3. 是否设置了 OLLAMA_ORIGINS="*"';
logError('连接失败', 'Ollama 服务不可达');
} else if ((err as Error).message.includes('400')) {
errMsg = '❌ 请求参数错误,可能是上下文过长';
logError('请求参数错误 (400)', (err as Error).message);
} else if ((err as Error).message.includes('500')) {
errMsg = '❌ Ollama 服务器错误,请检查 Ollama 日志';
logError('Ollama 服务器错误 (500)', (err as Error).message);
} else {
logError('对话错误', (err as Error).message);
}
appendSystemMessage(errMsg);
} finally {
state.set(KEYS.IS_STREAMING, false);
updateSendButton(false);
state.set(KEYS.ABORT_CONTROLLER, null);
}
// 所有消息统一走 Agent Loop
await sendMessageWithAgentLoop(text, currentSession, model);
return;
}
/** 更新当前会话消息中最近一个 'running' 状态的同名工具记录 */
+12 -23
View File
@@ -1,6 +1,6 @@
/**
* ToolsModal - 工具面板模态框
* 展示所有可用工具的详细信息,管理 Tool Calling 开关
* 展示所有可用工具的详细信息Tool Calling 始终开启,不可关闭。
*/
import { state, KEYS } from '../state/state.js';
@@ -21,27 +21,14 @@ export function initToolsModal(): void {
if (e.target === modalEl) closeToolsModal();
});
// Tool Calling 主开关
// Tool Calling 始终开启,不可关闭
const toggleToolCalling = modalEl.querySelector('#toggleToolCalling') as HTMLInputElement;
if (toggleToolCalling) {
toggleToolCalling.addEventListener('change', async () => {
const db = state.get<ChatDB | null>(KEYS.DB);
state.set('toolCallingEnabled', toggleToolCalling.checked);
if (db) await db.saveSetting('toolCallingEnabled', toggleToolCalling.checked);
if (toggleToolCalling.checked) {
const modelSupportsTools = state.get<boolean>('modelSupportsTools', false);
if (!modelSupportsTools) {
showToast('⚠️ 当前模型可能不支持 Tool Calling,建议使用 Qwen3、Llama 3.1+ 等模型', 'warning', 5000);
logWarn('Tool Calling 已开启(模型不支持)');
} else {
showToast('工具调用已开启', 'info');
logInfo('Tool Calling 已开启');
}
} else {
showToast('工具调用已关闭', 'info');
logInfo('Tool Calling 已关闭');
}
});
toggleToolCalling.checked = true;
toggleToolCalling.disabled = true;
state.set('toolCallingEnabled', true);
const db = state.get<ChatDB | null>(KEYS.DB);
if (db) db.saveSetting('toolCallingEnabled', true);
}
// run_command 模式下拉框
@@ -76,10 +63,12 @@ function updateRunCommandBadge(mode: string): void {
}
export function openToolsModal(): void {
// 同步开关状态
const toolCallingEnabled = state.get<boolean>('toolCallingEnabled', false);
// 同步开关状态 — 始终开启
const toggleTC = modalEl.querySelector('#toggleToolCalling') as HTMLInputElement;
if (toggleTC) toggleTC.checked = toolCallingEnabled;
if (toggleTC) {
toggleTC.checked = true;
toggleTC.disabled = true;
}
const runCommandMode = state.get<string>('runCommandMode', 'confirm');
const selectMode = modalEl.querySelector('#selectRunCommandMode') as HTMLSelectElement;
+5 -5
View File
@@ -28,7 +28,7 @@
<div class="header-left">
<span class="logo">🦙</span>
<span class="app-title">Metona Ollama</span>
<span class="app-version">v0.10.0</span>
<span class="app-version">v0.10.1</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"/>
@@ -394,9 +394,9 @@
<div class="modal-body">
<div class="help-section"><h4>🚀 快速开始</h4><ol><li>确保 Ollama 已启动(默认 <code>http://127.0.0.1:11434</code>,可在设置中修改)</li><li>顶部模型栏选择一个模型,右侧会显示模型能力徽章(🧠 Think / 👁️ Vision / 🔧 Tools</li><li>输入消息,按 <kbd>Enter</kbd> 发送,<kbd>Shift+Enter</kbd> 换行</li></ol></div>
<div class="help-section"><h4>💬 聊天功能</h4><ul><li><strong>流式回复</strong> — 实时打字效果,随时点 ■ 停止</li><li><strong>Think 推理</strong> — 下方 Think 按钮切换,让模型展示深度思考过程(需模型支持,如 Qwen3)</li><li><strong>上下文长度自动检测</strong> — 切换模型时自动从模型元数据获取实际支持的上下文长度(如 Qwen3.6 27B = 262,144 tokens),无需手动配置</li><li><strong>多模态</strong> — 上传图片,模型需支持 Vision 能力。图片自动压缩至合适分辨率,节省上下文</li><li><strong>文件分析</strong> — 支持 50+ 种文本/代码格式,单文件 ≤500KB,自动剥离注释并按上下文预算智能截断</li><li><strong>AI 回复顶部</strong> — 每条 AI 回复上方展示 📋 系统提示词折叠卡片,可点击查看实际发送给模型的完整上下文</li></ul></div>
<div class="help-section"><h4>🔧 Tool CallingAI 自主操作</h4><ul><li>设置中开启后,AI 可以在对话中<strong>自主调用本地工具</strong>,像一个本地 Agent</li><li><strong>38 个工具</strong>,分为 8 类:<ul><li><strong>文件系统</strong>17 个):read_file / write_file / list_directory / search_files / create_directory / delete_file / move_file / copy_file / append_file / edit_file / get_file_info / tree / download_file / diff_files / replace_in_files / read_multiple_files / compress</li><li><strong>命令执行</strong>1 个):run_command(实时流式输出,支持自动/需确认/禁用三种模式,可配置超时)</li><li><strong>联网搜索</strong>2 个):web_search4引擎并行:Bing+百度+DuckDuckGo+Google,结果缓存5分钟,URL可达性预检)/ web_fetch(自动重试+移动端UA切换+SPA页面自动升级到浏览器渲染)</li><li><strong>Git</strong>(1 个):git16 个子操作,push/pull/clone 内置60-120s超时保护)</li><li><strong>浏览器控制</strong>8 个):browser_open / browser_screenshot / browser_evaluate / browser_extract / browser_click / browser_type / browser_scroll / browser_close</li><li><strong>记忆 & 会话 & Skill</strong>8 个):memory_search / memory_add / memory_replace / memory_remove / session_list / session_read / skill_list / skill_view</li><li><strong>子代理</strong>1 个):spawn_task</li></ul></li><li>read_multiple_files 支持<strong>并行读取</strong>diff_files 优先调用系统 diff/git diff 命令</li><li>search_files 支持<strong>正则表达式搜索</strong>use_regex=true</li><li>list_directory 支持 <strong>offset 分页</strong>,大目录可分页读取</li><li><code>run_command</code>(命令执行)支持三模式切换:自动/需确认/禁用</li><li>危险命令(<code>rm -rf</code><code>mkfs</code>、反弹 shell 等)和系统路径(<code>/etc</code><code>~/.ssh</code> 等)被自动拦截</li><li>工具调用以<strong>可视化卡片</strong>展示状态(pending → running → success/error),在工作空间🔧工具页签中显示</li><li>默认最大 <strong>85 轮</strong>工具调用循环,上下文使用率>80%时自动缩减到3轮</li><li>独立工具自动<strong>并行执行</strong>(16个只读工具加入并行白名单),有依赖关系的工具串行执行</li><li>⚠️ 需要模型支持 Tool Calling(推荐 Qwen3、Llama 3.1+、Mistral</li></ul></div>
<div class="help-section"><h4>🔧 Tool Calling始终开启</h4><ul><li>所有消息均通过 <strong>Agent Loop 自主调用本地工具</strong>,像一个本地 Agent,无普通聊天模式</li><li><strong>38 个工具</strong>,分为 8 类:<ul><li><strong>文件系统</strong>17 个):read_file / write_file / list_directory / search_files / create_directory / delete_file / move_file / copy_file / append_file / edit_file / get_file_info / tree / download_file / diff_files / replace_in_files / read_multiple_files / compress</li><li><strong>命令执行</strong>1 个):run_command(实时流式输出,支持自动/需确认/禁用三种模式,可配置超时)</li><li><strong>联网搜索</strong>2 个):web_search4引擎并行:Bing+百度+DuckDuckGo+Google,结果缓存5分钟,URL可达性预检)/ web_fetch(自动重试+移动端UA切换+SPA页面自动升级到浏览器渲染)</li><li><strong>Git</strong>(1 个):git16 个子操作,push/pull/clone 内置60-120s超时保护)</li><li><strong>浏览器控制</strong>8 个):browser_open / browser_screenshot / browser_evaluate / browser_extract / browser_click / browser_type / browser_scroll / browser_close</li><li><strong>记忆 & 会话 & Skill</strong>8 个):memory_search / memory_add / memory_replace / memory_remove / session_list / session_read / skill_list / skill_view</li><li><strong>子代理</strong>1 个):spawn_task</li></ul></li><li>read_multiple_files 支持<strong>并行读取</strong>diff_files 优先调用系统 diff/git diff 命令</li><li>search_files 支持<strong>正则表达式搜索</strong>use_regex=true</li><li>list_directory 支持 <strong>offset 分页</strong>,大目录可分页读取</li><li><code>run_command</code>(命令执行)支持三模式切换:自动/需确认/禁用</li><li>危险命令(<code>rm -rf</code><code>mkfs</code>、反弹 shell 等)和系统路径(<code>/etc</code><code>~/.ssh</code> 等)被自动拦截</li><li>工具调用以<strong>可视化卡片</strong>展示状态(pending → running → success/error),在工作空间🔧工具页签中显示</li><li>默认最大 <strong>85 轮</strong>工具调用循环,上下文使用率>80%时自动缩减到3轮</li><li>独立工具自动<strong>并行执行</strong>(16个只读工具加入并行白名单),有依赖关系的工具串行执行</li><li>⚠️ 需要模型支持 Tool Calling(推荐 Qwen3、Llama 3.1+、Mistral</li></ul></div>
<div class="help-section"><h4>🧠 Agent 记忆系统</h4><ul><li>设置中开启后,AI 从对话中<strong>自动提取关键信息</strong>(事实/偏好/规则),跨会话持续积累</li><li><strong>增量提取</strong>:长对话中每 20 轮自动触发轻量级记忆提取,不等到对话结束</li><li>对话结束时自动触发完整记忆提取</li><li>新对话时自动检索相关记忆注入上下文,让 AI "记住"你</li><li>点击顶部 🧠 按钮打开记忆面板:搜索、筛选、编辑、删除</li><li><strong>记忆容量上限 500 条</strong>,超限时自动清理低价值条目(规则类型受保护)</li><li><strong>向量语义搜索</strong>:在设置中选择嵌入模型后,支持语义级别记忆检索(更精准)</li><li>未选择嵌入模型时,使用关键词匹配检索记忆</li><li>AI 可通过 memory 工具主动管理记忆</li><li>记忆写入前自动安全扫描(prompt injection / 敏感信息 / 不可见字符检测)</li><li>记忆存储在本地 SQLite,不会上传到任何服务器</li></ul></div>
<div class="help-section"><h4>🤖 Agent Loop v0.10.0 增强</h4><ul><li><strong>智能上下文压缩</strong>:滑动窗口 + LLM结构化JSON摘要,超过50%上下文窗口自动触发</li><li><strong>流式调用超时保护</strong>:可配置超时(默认180s),Ollama 卡死不再永久阻塞</li><li><strong>Final Answer 智能检测</strong>:多模式匹配(Final Answer/最终答案/最终回答/总结),避免漏检或误触</li><li><strong>Token 感知迭代预算</strong>:上下文使用率>80%时自动缩减剩余轮次到3轮</li><li><strong>工具缓存 TTL</strong>:搜索5分钟/网页10分钟/文件30分钟,过期自动刷新</li><li><strong>自动子任务拆解</strong>:检测"同时/分别/以及"等并行关键词,自动 spawn 子代理并行处理</li><li><strong>旧工具结果自动截断</strong>:超过10轮的工具结果自动截断到500字符,控制上下文膨胀</li></ul></div>
<div class="help-section"><h4>🤖 Agent Loop v0.10.1 增强</h4><ul><li><strong>智能上下文压缩</strong>:滑动窗口 + LLM结构化JSON摘要,超过50%上下文窗口自动触发</li><li><strong>流式调用超时保护</strong>:可配置超时(默认180s),Ollama 卡死不再永久阻塞</li><li><strong>Final Answer 智能检测</strong>:多模式匹配(Final Answer/最终答案/最终回答/总结),避免漏检或误触</li><li><strong>Token 感知迭代预算</strong>:上下文使用率>80%时自动缩减剩余轮次到3轮</li><li><strong>工具缓存 TTL</strong>:搜索5分钟/网页10分钟/文件30分钟,过期自动刷新</li><li><strong>自动子任务拆解</strong>:检测"同时/分别/以及"等并行关键词,自动 spawn 子代理并行处理</li><li><strong>旧工具结果自动截断</strong>:超过10轮的工具结果自动截断到500字符,控制上下文膨胀</li></ul></div>
<div class="help-section"><h4>🔌 MCPModel Context Protocol</h4><ul><li>支持连接外部 MCP Server,动态扩展工具能力</li><li>设置面板可添加/启用/禁用/删除 MCP 服务器</li><li>MCP 工具以 <code>mcp_{server}_{tool}</code> 前缀注册,与内置工具统一调度</li><li>启动时自动连接已启用的 MCP 服务器</li></ul></div>
<div class="help-section"><h4>📋 自定义文件(SOUL.md / AGENT.md / USER.md</h4><ul><li>在工作空间目录创建以下文件即可自定义 AI 行为,修改后下一轮对话立即生效</li><li><strong>SOUL.md</strong> — AI 身份、性格、行为准则(<strong>永远不可被压缩</strong>,注入为最高优先级系统提示词)</li><li><strong>AGENT.md</strong> — 工具调用规则、链式调用模式、核心约束(内置精简默认版,可通过工作空间覆盖)</li><li><strong>USER.md</strong> — 用户画像:技术栈、偏好、习惯等个人信息,AI 在对话中自动参考</li><li>可在 AI 回复顶部的 📋 系统提示词卡片中查看实际注入的完整上下文</li><li>删除工作空间中的文件即可恢复为内置默认版本</li></ul></div>
<div class="help-section"><h4>🎯 技能自动生成</h4><ul><li>AI 完成包含 <strong>2+ 工具调用</strong>的复杂任务后,自动从执行轨迹中提取可复用技能</li><li><strong>语义匹配</strong>:配置嵌入模型后,根据用户消息的语义自动匹配最相关技能</li><li><strong>参数自优化</strong>:同一技能链多次执行后,自动合并高频参数提示,越用越精准</li><li><strong>技能衰减</strong>:30 天未使用的技能自动降权,90 天几乎忽略</li><li><strong>链合并</strong>:相同前缀(前 2 步相同)的多个技能自动合并为抽象技能</li><li>AI 可通过 <code>skill_list</code>(渐进式摘要)和 <code>skill_view</code>(完整工具链)查看技能</li><li>技能存储在本地 SQLite,不会上传到任何服务器</li></ul></div>
@@ -421,12 +421,12 @@
</div>
<div class="modal-body">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px;padding:8px 12px;background:var(--bg-layer);border-radius:var(--radius-control);border:1px solid var(--border-subtle);">
<span style="font-size:13px;font-weight:600;">启用工具调用</span>
<span style="font-size:13px;font-weight:600;">工具调用(始终开启)</span>
<label class="toggle-label" style="margin:0;">
<input type="checkbox" id="toggleToolCalling">
<span class="toggle-slider"></span>
</label>
<span class="text-muted" style="font-size:11px;margin-left:auto;">需要模型支持 Tool Calling</span>
<span class="text-muted" style="font-size:11px;margin-left:auto;">始终开启,所有对话均通过 Agent Loop</span>
</div>
<p class="text-muted" style="font-size:12px;margin-bottom:12px;">启用后,AI 可在对话中自主调用以下工具。高风险操作需用户确认。系统关键路径自动屏蔽。</p>
<div class="tools-grid">
+2 -2
View File
@@ -312,7 +312,7 @@ async function init(): Promise<void> {
state.set(KEYS.NUM_CTX, 24576);
// ── 防御性重置:确保首次启动不会遗留脏状态 ──
state.set('toolCallingEnabled', false);
state.set('toolCallingEnabled', true);
state.set('runCommandMode', 'confirm'); // 默认需确认
state.set('memoryEnabled', true);
state.set('thinkEnabled', false);
@@ -363,7 +363,7 @@ async function init(): Promise<void> {
(document.querySelector('#inputMaxTurns') as HTMLInputElement).value = String(maxTurns);
// ── Tool Calling 设置 ──
const toolCallingEnabled = await db.getSetting('toolCallingEnabled', false);
const toolCallingEnabled = await db.getSetting('toolCallingEnabled', true);
const runCommandMode = await db.getSetting('runCommandMode', 'confirm') as 'auto' | 'confirm' | 'disabled';
state.set('toolCallingEnabled', toolCallingEnabled);
state.set('runCommandMode', runCommandMode);
+1 -1
View File
@@ -6,7 +6,7 @@
- **名称**: Metona
- **角色**: Metona Ollama 桌面 AI 助手
- **核心能力**: ReAct Agent Loop v0.10.0 + 38 个增强工具 + 记忆系统 + 技能自动生成 + 智能上下文压缩
- **核心能力**: ReAct Agent Loop v0.10.1 + 38 个增强工具 + 记忆系统 + 技能自动生成 + 智能上下文压缩
## 性格与语气
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* SubAgent - 子代理委派系统 (v0.10.0 增强版)
* SubAgent - 子代理委派系统 (v0.10.1 增强版)
* 子代理拥有受限工具集(只读),可独立完成调研/搜索/分析类任务
* 主 Agent 通过 spawn_task 工具并行委派多个子代理
*/