feat: 预算警告 & Shadowing 防护 & 记忆容量管理 (v5.1.2)

- 预算警告临时层:Agent Loop 剩余 ≤5 轮注入 WARNING,≤2 轮注入 CRITICAL
- MCP Shadowing 防护:MCP 工具注册时检测重名,自动跳过并警告
- 记忆容量限制:上限 500 条,超限自动清理低价值条目(rule 受保护)
- 版本号更新至 5.1.2
This commit is contained in:
Metona Build
2026-04-19 18:10:26 +08:00
parent bea52bf1b7
commit 71bd6e19ea
10 changed files with 114 additions and 10 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
全离线运行 · 数据本地存储 · 零外部依赖 全离线运行 · 数据本地存储 · 零外部依赖
[![版本](https://img.shields.io/badge/version-5.1.1-brightgreen?style=flat-square)](https://gitee.com/thzxx/metona-ollama-desktop/releases) [![版本](https://img.shields.io/badge/version-5.1.2-brightgreen?style=flat-square)](https://gitee.com/thzxx/metona-ollama-desktop/releases)
[![平台](https://img.shields.io/badge/platform-Windows%20x64-blue?style=flat-square)](https://gitee.com/thzxx/metona-ollama-desktop/releases) [![平台](https://img.shields.io/badge/platform-Windows%20x64-blue?style=flat-square)](https://gitee.com/thzxx/metona-ollama-desktop/releases)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.7-3178c6?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org/) [![TypeScript](https://img.shields.io/badge/TypeScript-5.7-3178c6?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
[![Electron](https://img.shields.io/badge/Electron-33-47848f?style=flat-square&logo=electron&logoColor=white)](https://www.electronjs.org/) [![Electron](https://img.shields.io/badge/Electron-33-47848f?style=flat-square&logo=electron&logoColor=white)](https://www.electronjs.org/)
+7
View File
@@ -176,6 +176,13 @@ electron-builder 会自动处理所有依赖下载(Electron、NSIS、winCodeSi
## 构建日志 ## 构建日志
### v5.1.22026-04-19 — metona-ollama-desktop 仓库
- ✨ 预算警告临时层:Agent Loop 接近迭代上限时注入临时提示,≤5 轮时 warning,≤2 轮时 critical
- ✨ Shadowing 防护:MCP 工具注册时检测与内置工具重名,自动跳过并日志警告
- ✨ 记忆容量限制:上限 500 条,超限时自动清理低价值条目(按 importance + useCount + 近期使用综合评分,rule 类型受保护)
- 📝 版本号更新至 5.1.2
### v5.1.12026-04-19 — metona-ollama-desktop 仓库 ### v5.1.12026-04-19 — metona-ollama-desktop 仓库
- 🔧 搜索引擎:DuckDuckGo → Bing + 百度 + Google(三引擎),解决国内不可达问题 - 🔧 搜索引擎:DuckDuckGo → Bing + 百度 + Google(三引擎),解决国内不可达问题
+19 -1
View File
@@ -3,7 +3,7 @@
## 分支结构 ## 分支结构
**桌面版主线**`master`): **桌面版主线**`master`):
- `metona-ollama-desktop-v3.2.6``metona-ollama-desktop-v3.3.4``metona-ollama-desktop-v4.0.0``metona-ollama-desktop-v5.0.0``metona-ollama-desktop-v5.0.1``metona-ollama-desktop-v5.1.0``metona-ollama-desktop-v5.1.1`(当前发布分支) - `metona-ollama-desktop-v3.2.6``metona-ollama-desktop-v3.3.4``metona-ollama-desktop-v4.0.0``metona-ollama-desktop-v5.0.0``metona-ollama-desktop-v5.0.1``metona-ollama-desktop-v5.1.0``metona-ollama-desktop-v5.1.1``metona-ollama-desktop-v5.1.2`(当前发布分支)
**Web 版历史**已独立为 [metona-ollama-web](https://gitee.com/thzxx/metona-ollama-web) 仓库(v1.1.0 ~ v4.0.3)。 **Web 版历史**已独立为 [metona-ollama-web](https://gitee.com/thzxx/metona-ollama-web) 仓库(v1.1.0 ~ v4.0.3)。
@@ -11,6 +11,24 @@
## 桌面版(Desktop ## 桌面版(Desktop
### Desktop v5.1.2 — 预算警告 & Shadowing 防护 & 记忆容量管理
-**预算警告临时层**(借鉴 Hermes Agent
- Agent Loop 接近迭代上限时自动注入临时提示
- 剩余 ≤5 轮:WARNING 级别,提示模型准备收尾
- 剩余 ≤2 轮:CRITICAL 级别,要求模型立即给出最终答案
- 临时消息在 API 调用后自动清理,不影响对话历史
-**MCP Shadowing 防护**(借鉴 Hermes Agent
- MCP 工具注册时自动检测与内置工具重名
- 重名工具自动跳过,日志输出警告信息
- 防止外部 MCP Server 覆盖内置工具导致行为异常或绕过安全检查
-**记忆容量限制**(借鉴 Hermes Agent
- memories 表上限 500 条,超限时自动清理
- 清理策略:按综合评分排序,清理后 20%
- 评分公式:importance × 2 + useCount × 3 + recencyBonus7 天内使用过 +5
- rule 类型记忆受保护,不会被自动清理
- 📝 **版本号更新至 5.1.2**7 个文件同步更新)
### Desktop v5.1.1 — 搜索引擎替换 & 超时移除 & 日志导出 ### Desktop v5.1.1 — 搜索引擎替换 & 超时移除 & 日志导出
- 🔧 **搜索引擎:DuckDuckGo → Bing + 百度 + Google** - 🔧 **搜索引擎:DuckDuckGo → Bing + 百度 + Google**
+1 -1
View File
@@ -1,6 +1,6 @@
# Metona Ollama Desktop — 开发规范 # Metona Ollama Desktop — 开发规范
> 版本: 5.1.1 | 更新: 2026-04-19 | 维护: 项目团队 > 版本: 5.1.2 | 更新: 2026-04-19 | 维护: 项目团队
--- ---
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "metona-ollama-desktop", "name": "metona-ollama-desktop",
"version": "5.1.1", "version": "5.1.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "metona-ollama-desktop", "name": "metona-ollama-desktop",
"version": "5.1.1", "version": "5.1.2",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"sql.js": "^1.11.0" "sql.js": "^1.11.0"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "metona-ollama-desktop", "name": "metona-ollama-desktop",
"version": "5.1.1", "version": "5.1.2",
"description": "Metona Ollama - TypeScript + Electron 桌面 AI 聊天客户端", "description": "Metona Ollama - TypeScript + Electron 桌面 AI 聊天客户端",
"main": "dist/main/main.js", "main": "dist/main/main.js",
"author": "thzxx", "author": "thzxx",
+1 -1
View File
@@ -28,7 +28,7 @@
<div class="header-left"> <div class="header-left">
<span class="logo">🦙</span> <span class="logo">🦙</span>
<span class="app-title">Metona Ollama</span> <span class="app-title">Metona Ollama</span>
<span class="app-version">v5.1.1</span> <span class="app-version">v5.1.2</span>
<button class="icon-btn help-btn" id="btnHelp" title="使用帮助"> <button class="icon-btn help-btn" id="btnHelp" title="使用帮助">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <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"/> <circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
+20
View File
@@ -529,6 +529,17 @@ export async function runAgentLoop(
const toolCalls: ToolCall[] = []; const toolCalls: ToolCall[] = [];
allCallsThisLoop = []; allCallsThisLoop = [];
// v5.1.2 预算警告:接近迭代上限时注入临时提示
let budgetWarningIdx = -1;
const remaining = maxLoops - loopCount + 1;
if (remaining <= 5 && remaining > 0) {
const warning = remaining <= 2
? `\n⚠️ CRITICAL: You have only ${remaining} iteration(s) left. Stop using tools and provide your final answer NOW.`
: `\n⚠️ WARNING: You have approximately ${remaining} iterations remaining. Start wrapping up and prepare your final answer.`;
messages.push({ role: 'system', content: warning });
budgetWarningIdx = messages.length - 1;
}
const abortController = new AbortController(); const abortController = new AbortController();
state.set(KEYS.ABORT_CONTROLLER, abortController); state.set(KEYS.ABORT_CONTROLLER, abortController);
@@ -587,6 +598,10 @@ export async function runAgentLoop(
return; return;
} }
logError('流式调用异常', (err as Error).message); logError('流式调用异常', (err as Error).message);
// 清理预算警告临时消息
if (budgetWarningIdx >= 0) {
messages.splice(budgetWarningIdx, 1);
}
if (content || thinking) { if (content || thinking) {
messages.push({ role: 'assistant', content: content || '(模型响应异常)', ...(thinking && { thinking }) }); messages.push({ role: 'assistant', content: content || '(模型响应异常)', ...(thinking && { thinking }) });
} }
@@ -594,6 +609,11 @@ export async function runAgentLoop(
return; return;
} }
// 清理预算警告临时消息
if (budgetWarningIdx >= 0) {
messages.splice(budgetWarningIdx, 1);
}
// 提取 ReAct 思考过程 // 提取 ReAct 思考过程
const thoughtMatch = content.match(/\*\*Thought:\*\*\s*([\s\S]*?)(?=\*\*Action:\*\*|\*\*Final Answer:\*\*|$)/i); const thoughtMatch = content.match(/\*\*Thought:\*\*\s*([\s\S]*?)(?=\*\*Action:\*\*|\*\*Final Answer:\*\*|$)/i);
const thought = thoughtMatch ? thoughtMatch[1].trim() : ''; const thought = thoughtMatch ? thoughtMatch[1].trim() : '';
+48
View File
@@ -255,6 +255,15 @@ export async function addMemory(data: {
throw new Error(`记忆内容被安全规则拦截: ${securityCheck.reason}`); throw new Error(`记忆内容被安全规则拦截: ${securityCheck.reason}`);
} }
// v5.1.2 记忆容量限制:超过上限自动清理低价值条目
const MAX_MEMORIES = 500;
if (memoryCache.length >= MAX_MEMORIES) {
const evicted = autoCleanMemories();
if (evicted > 0) {
logInfo('记忆容量清理', `已清理 ${evicted} 条低价值记忆(上限 ${MAX_MEMORIES}`);
}
}
// 检查重复(内容相似度 > 80% 则跳过) // 检查重复(内容相似度 > 80% 则跳过)
const existing = memoryCache.find(e => { const existing = memoryCache.find(e => {
if (e.type !== data.type) return false; if (e.type !== data.type) return false;
@@ -300,6 +309,45 @@ export async function addMemory(data: {
return entry; return entry;
} }
// ── 容量管理 ──
/**
* 自动清理低价值记忆条目
* 策略:按综合评分排序,清理后 20% 的条目
* 评分 = importance * 2 + useCount * 3 + recencyBonus(最近 7 天使用过 +5
* rule 类型记忆受保护不被清理
* @returns 清理的条目数
*/
function autoCleanMemories(): number {
const db = state.get<ChatDB | null>(KEYS.DB);
const now = Date.now();
const SEVEN_DAYS = 7 * 24 * 3600 * 1000;
// 计算综合评分
const scored = memoryCache
.map((entry, idx) => {
const recencyBonus = (now - entry.lastUsedAt) < SEVEN_DAYS ? 5 : 0;
const score = entry.importance * 2 + entry.useCount * 3 + recencyBonus;
return { entry, idx, score };
})
// rule 类型受保护
.filter(s => s.entry.type !== 'rule')
.sort((a, b) => a.score - b.score);
const evictCount = Math.max(1, Math.floor(memoryCache.length * 0.2));
const toEvict = scored.slice(0, evictCount);
// 从缓存和数据库中移除
const evictIds = new Set(toEvict.map(s => s.entry.id));
for (const id of evictIds) {
if (db) db.deleteMemory(id);
}
memoryCache = memoryCache.filter(e => !evictIds.has(e.id));
state.set('memoryEntries', [...memoryCache]);
return evictIds.size;
}
// ── 安全扫描 ── // ── 安全扫描 ──
/** 记忆内容安全扫描 */ /** 记忆内容安全扫描 */
+14 -3
View File
@@ -641,9 +641,20 @@ let _mcpToolCache: ToolDefinition[] = [];
/** 刷新 MCP 工具缓存(在 MCP 服务器配置变更时调用) */ /** 刷新 MCP 工具缓存(在 MCP 服务器配置变更时调用) */
export async function refreshMCPTools(): Promise<void> { export async function refreshMCPTools(): Promise<void> {
try { try {
_mcpToolCache = await getMCPToolDefinitions(); const rawTools = await getMCPToolDefinitions();
if (_mcpToolCache.length > 0) { // v5.1.2 Shadowing 防护:MCP 工具不能覆盖内置工具
logInfo(`MCP 工具已注册: ${_mcpToolCache.length}`, _mcpToolCache.map(t => t.function.name).join(', ')); const builtInNames = new Set(TOOL_DEFINITIONS.map(d => d.function.name));
const filtered = rawTools.filter(t => {
if (builtInNames.has(t.function.name)) {
logWarn(`MCP 工具 "${t.function.name}" 与内置工具重名,已跳过`);
return false;
}
return true;
});
const skipped = rawTools.length - filtered.length;
_mcpToolCache = filtered;
if (filtered.length > 0) {
logInfo(`MCP 工具已注册: ${filtered.length}${skipped > 0 ? `${skipped} 个因重名被跳过)` : ''}`, filtered.map(t => t.function.name).join(', '));
} }
} catch (err) { } catch (err) {
logError('MCP 工具刷新失败', (err as Error).message); logError('MCP 工具刷新失败', (err as Error).message);