chore: 版本号 0.14.4 → 0.14.5

refactor: Hook 系统精简 — 7个→2个(SecurityCheck + FileWriteDedup)
delete: verification.ts 删除(DiffAnalyzer/FileChangeAudit 仅打日志无实际作用)
remove: ResultValidation/IterationMetrics/PlanAutoTrack Hook
merge: write_file 路径记录内联到 agent-engine
This commit is contained in:
thzxx
2026-06-28 20:49:34 +08:00
parent adff2f36eb
commit 2f37111074
9 changed files with 44 additions and 332 deletions
+3 -3
View File
@@ -14,7 +14,7 @@
</p>
<p align="center">
<img src="https://img.shields.io/badge/version-v0.14.4-E8734A?style=flat-square" alt="version">
<img src="https://img.shields.io/badge/version-v0.14.5-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.4.exe`
产出:`release/Metona Ollama Setup v0.14.5.exe`
## 🛠️ 常用命令
@@ -501,7 +501,7 @@ npm start
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm run dist
```
Output: `release/Metona Ollama Setup v0.14.4.exe`
Output: `release/Metona Ollama Setup v0.14.5.exe`
## 🛠️ Common Commands
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "metona-ollama-desktop",
"version": "0.14.4",
"version": "0.14.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "metona-ollama-desktop",
"version": "0.14.4",
"version": "0.14.5",
"license": "MIT",
"dependencies": {
"ffmpeg-static": "^5.2.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "metona-ollama-desktop",
"version": "0.14.4",
"version": "0.14.5",
"description": "Metona Ollama - TypeScript + Electron 桌面 AI 聊天客户端",
"main": "dist/main/main.js",
"author": "thzxx",
+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.14.4',
message: 'Metona Ollama Desktop v0.14.5',
detail: 'TypeScript + Electron Ollama AI 聊天客户端\n\nhttps://gitee.com/thzxx/metona-ollama',
icon: getIconPath()
});
+1 -1
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.14.4</span>
<span class="app-version">v0.14.5</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"/>
-3
View File
@@ -31,7 +31,6 @@ import { initLogPanel, addLog } from './services/log-service.js';
import { logInfo, logSuccess, logError, logDebug, logInit, logWarn } from './services/log-service.js';
import { initSearxngModal, closeSearxngModal, loadSearxngConfig } from './components/searxng-modal.js';
import { initHarnessHooks } from './services/hooks.js';
import { initVerificationSystem } from './services/verification.js';
import type { ChatSession } from './types.js';
// ─── v4.0 数据迁移:IndexedDB → SQLite ───
@@ -353,7 +352,6 @@ async function init(): Promise<void> {
// ── Harness Engineering 系统初始化 ──
initHarnessHooks();
initVerificationSystem();
// Agent Metrics 无需显式初始化(按需启动)
// Context Indexer 在 Agent INIT 状态按需构建
@@ -481,7 +479,6 @@ async function init(): Promise<void> {
initWorkspacePanel();
setupDesktopIntegration();
initHarnessHooks();
initVerificationSystem();
// ── MEMORY.md 初始化(fallback 路径)──
import('./services/memory-service.js').then(({ initMemoryFile }) => {
initMemoryFile().catch(() => {});
+5
View File
@@ -1484,6 +1484,11 @@ async function handleExecuting(
const result = await executeTool(call.function.name, call.function.arguments)
.catch(err => ({ success: false, error: err?.message || String(err) }) as ToolResult);
logToolResult(call.function.name, result.success, result.success ? undefined : result.error);
// 记录 write_file 成功路径,供 FileWriteDedup Hook 拦截重复写入
if (call.function.name === 'write_file' && result.success && call.function.arguments?.path) {
const { addWrittenFile } = await import('./hooks.js');
addWrittenFile(String(call.function.arguments.path));
}
return [{
name: call.function.name, arguments: call.function.arguments,
result, status: result.success ? 'success' as const : 'error' as const, timestamp: Date.now()
+31 -136
View File
@@ -1,18 +1,12 @@
/**
* Harness Hook 系统
* Harness Engineering: 可扩展的 Agent 执行钩子架构
* 可扩展的 Agent 执行钩子架构
*
* 钩子阶段:
* - pre_tool: 工具执行前(安全检查、缓存检查、权限验证
* - post_tool: 工具执行后(结果验证、自动 lint、自动测试)
* - post_iteration: 每轮迭代后(记忆提取、上下文裁剪)
* - pre_completion: 最终输出前(完成门控、质量检查)
*
* 设计原则:
* - 异步并行执行,失败不阻塞主流程
* - 优先级排序,高优先级先执行
* - 可动态注册/移除
* - MCP 工具可注册自定义 Hook
* - pre_tool: 工具执行前(安全检查、文件去重
* - post_tool: 工具执行后
* - post_iteration: 每轮迭代后
* - pre_completion: 最终输出前
*/
import { logInfo, logWarn, logDebug } from './log-service.js';
@@ -31,17 +25,14 @@ function ensurePhaseIndex(phase: HookPhase): void {
}
}
/** 注册 Hook */
export function registerHook(hook: HarnessHook): void {
hookRegistry.set(hook.name, hook);
ensurePhaseIndex(hook.phase);
phaseIndex.get(hook.phase)!.push(hook);
// 按优先级降序排列
phaseIndex.get(hook.phase)!.sort((a, b) => b.priority - a.priority);
logDebug(`Hook 已注册: "${hook.name}" phase=${hook.phase} priority=${hook.priority}`);
}
/** 移除 Hook */
export function unregisterHook(name: string): void {
const hook = hookRegistry.get(name);
if (!hook) return;
@@ -53,7 +44,6 @@ export function unregisterHook(name: string): void {
}
}
/** 启用/禁用 Hook */
export function setHookEnabled(name: string, enabled: boolean): void {
const hook = hookRegistry.get(name);
if (hook) {
@@ -62,12 +52,10 @@ export function setHookEnabled(name: string, enabled: boolean): void {
}
}
/** 获取所有已注册 Hook */
export function getRegisteredHooks(): HarnessHook[] {
return Array.from(hookRegistry.values());
}
/** 获取指定阶段已启用的 Hook */
export function getPhaseHooks(phase: HookPhase): HarnessHook[] {
return (phaseIndex.get(phase) || []).filter(h => h.enabled);
}
@@ -76,10 +64,6 @@ export function getPhaseHooks(phase: HookPhase): HarnessHook[] {
// Hook 执行引擎
// ═══════════════════════════════════════════════════════════════
/**
* 并行执行指定阶段的所有 Hook
* @returns 所有 Hook 的执行结果汇总
*/
export async function executeHooks(
phase: HookPhase,
ctx: LoopContext,
@@ -93,11 +77,7 @@ export async function executeHooks(
const results = await Promise.all(
hooks.map(async (hook) => {
try {
const result = await hook.handler(ctx, data);
if (!result.passed) {
logWarn(`Hook "${hook.name}" 未通过: ${result.message}`);
}
return result;
return await hook.handler(ctx, data);
} catch (err) {
logWarn(`Hook "${hook.name}" 执行异常: ${(err as Error).message}`);
return { passed: true, message: `Hook 异常(已忽略): ${(err as Error).message}` };
@@ -113,31 +93,46 @@ export async function executeHooks(
// 内置 Hook
// ═══════════════════════════════════════════════════════════════
/** 会话级已写入文件路径集合 */
const _writtenFilesThisSession = new Set<string>();
/** 暴露写入记录供 agent-engine 清理 */
export function clearWrittenFiles(): void {
_writtenFilesThisSession.clear();
}
export function hasWrittenFile(path: string): boolean {
return _writtenFilesThisSession.has(path);
}
export function addWrittenFile(path: string): void {
_writtenFilesThisSession.add(path);
}
/**
* SecurityCheckHook — 工具执行前安全检查
* 检查命令和路径是否在黑名单内
* SecurityCheckHook — 工具执行前安全检查pre_tool 阻断级)
*/
export const securityCheckHook: HarnessHook = {
name: 'SecurityCheck',
phase: 'pre_tool',
priority: 100, // 最高优先级
priority: 100,
enabled: true,
handler: async (_ctx: LoopContext, data: HookData) => {
const { toolName, toolArgs } = data;
// 危险命令黑名单
if (toolName === 'run_command' && typeof toolArgs?.command === 'string') {
const cmd = toolArgs.command as string;
// 快速黑名单检查
const dangerous = ['rm -rf /', 'mkfs', 'dd if=', 'shutdown', 'reboot', ':(){', 'chmod 777 /'];
for (const d of dangerous) {
if (cmd.toLowerCase().includes(d.toLowerCase())) {
return { passed: false, message: `危险命令被拦截: ${cmd.slice(0, 100)}` };
}
}
// 管道执行 shell 检测
if (/(\||>|<)\s*(sh|bash|zsh|powershell|cmd)/i.test(cmd)) {
return { passed: false, message: `禁止通过管道执行 shell: ${cmd.slice(0, 100)}` };
}
}
// 受保护路径不可删除
if (toolName === 'delete_file' && typeof toolArgs?.path === 'string') {
const p = (toolArgs.path as string).toLowerCase();
const protectedPaths = ['/etc', '/sys', '/proc', '/dev', '/boot', 'c:\\windows', '.ssh', '.gnupg'];
@@ -147,88 +142,15 @@ export const securityCheckHook: HarnessHook = {
}
}
}
return { passed: true, message: '' };
},
};
/**
* ResultValidationHook — 工具执行后结果验证
* 检查工具返回是否有效。注意:此 Hook 为咨询性(post_tool 阶段不阻断执行),
* 实际的截断由 agent-engine 的 formatToolResultForModel 和 40 条消息硬限制负责。
*/
export const resultValidationHook: HarnessHook = {
name: 'ResultValidation',
phase: 'post_tool',
priority: 90,
enabled: true,
handler: async (_ctx: LoopContext, data: HookData) => {
const { toolName, toolResult } = data;
if (!toolResult) return { passed: true, message: '' };
// 检查空结果
if (!toolResult.success && !toolResult.error) {
return { passed: false, message: `工具 ${toolName} 执行失败但未提供错误信息` };
}
// 检查超大结果。不同工具有不同阈值:
// web_search 包含自动抓取内容,允许较大结果
const SIZE_LIMITS: Record<string, number> = {
web_search: 1_000_000, // 1MB(含自动抓取的多页内容)
web_fetch: 500_000, // 500KB
read_file: 200_000, // 200KB
default: 500_000, // 默认 500KB
};
const limit = SIZE_LIMITS[toolName || ''] || SIZE_LIMITS.default;
if (toolResult.success && toolResult) {
const resultStr = JSON.stringify(toolResult);
if (resultStr.length > limit) {
// 仅警告,不阻断(实际截断由 agent-engine 负责)
return {
passed: true,
message: `工具 ${toolName} 返回结果较大 (${(resultStr.length / 1024).toFixed(0)}KB),将由上下文管理器自动截断`,
data: { oversized: true, size: resultStr.length },
};
}
}
return { passed: true, message: '' };
},
};
/**
* IterationMetricsHook — 迭代度量收集
* 每轮迭代后记录关键指标
*/
export const iterationMetricsHook: HarnessHook = {
name: 'IterationMetrics',
phase: 'post_iteration',
priority: 50,
enabled: true,
handler: async (ctx: LoopContext, _data: HookData) => {
// 收集本轮迭代统计
const totalMessages = ctx.messages.length;
const toolMessages = ctx.messages.filter(m => m.role === 'tool').length;
logDebug(
`迭代度量 [第 ${ctx.loopCount} 轮]: ` +
`${totalMessages} 消息, ${toolMessages} 工具消息, ` +
`${ctx.totalEvalCount} eval tokens, ${ctx.allToolRecords.length} 总工具调用`
);
return { passed: true, message: '' };
},
};
// ═══════════════════════════════════════════════════════════════
// Harness Hook: 文件写入去重
// ═══════════════════════════════════════════════════════════════
/** 会话级已写入文件路径集合 — 由 FileWriteDedupHook 和 PlanAutoTrackHook 共同维护 */
const _writtenFilesThisSession = new Set<string>();
/**
* FileWriteDedupHook — write_file 同路径去重(pre_tool 阻断级)
* 同一文件路径在本会话中已成功写入后,后续写入被拦截,
* 防止模型在设计迭代中反复覆盖同一文件。
* FileWriteDedupHook — 文件写入去重(pre_tool 阻断级)
* 同一会话中同一路径写入成功后,后续写入被拦截,防止 AI 反复覆盖同一文件。
* 同时记录 write_file 成功路径。
*/
export const fileWriteDedupHook: HarnessHook = {
name: 'FileWriteDedup',
@@ -240,38 +162,14 @@ export const fileWriteDedupHook: HarnessHook = {
const filePath = String(data.toolArgs?.path || '');
if (!filePath) return { passed: true, message: '' };
if (_writtenFilesThisSession.has(filePath)) {
return { passed: false, message: `文件 "${filePath}" 已在本会话中写入,无需重复覆盖。请直接给出最终回答。` };
return { passed: false, message: `文件 "${filePath}" 已在本会话中写入,无需重复覆盖。` };
}
return { passed: true, message: '' };
},
};
/**
* PlanAutoTrackHook — Plan Mode 步骤追踪(post_tool 非阻断级)
* 不自动标记步骤(避免模糊匹配误标),仅由 AI 通过 plan_track 工具精确标记。
* 此处只维护 write_file 路径记录供 FileWriteDedupHook 使用。
*/
export const planAutoTrackHook: HarnessHook = {
name: 'PlanAutoTrack',
phase: 'post_tool',
priority: 80,
enabled: true,
handler: async (ctx: LoopContext, data: HookData) => {
if (ctx.mode !== 'plan') return { passed: true, message: '' };
const toolName = data.toolName || '';
const toolResult = data.toolResult as Record<string, unknown> | undefined;
// write_file 成功后记录文件路径,供 FileWriteDedupHook 拦截
if (toolName === 'write_file' && toolResult?.success && data.toolArgs?.path) {
_writtenFilesThisSession.add(String(data.toolArgs.path));
}
return { passed: true, message: '' };
},
};
// ═══════════════════════════════════════════════════════════════
// 初始化:注册所有内置 Hook
// 初始化
// ═══════════════════════════════════════════════════════════════
let hooksInitialized = false;
@@ -279,10 +177,7 @@ let hooksInitialized = false;
export function initHarnessHooks(): void {
if (hooksInitialized) return;
registerHook(securityCheckHook);
registerHook(resultValidationHook);
registerHook(iterationMetricsHook);
registerHook(fileWriteDedupHook);
registerHook(planAutoTrackHook);
hooksInitialized = true;
logInfo('Harness Hook 系统已初始化', `${hookRegistry.size} 个 Hook 已注册`);
}
-185
View File
@@ -1,185 +0,0 @@
/**
* Verification System — 验证系统模块
* Harness Engineering: 计算性反馈管道
*
* 在 Agent 修改文件后自动运行:
* - Linter(代码风格检查)
* - Type Checker(类型检查)
* - Test Runner(单元测试)
* - Diff Analyzer(修改范围分析)
*
* 设计原则:
* - 所有验证利用现有 run_command 工具执行
* - 验证 Hook 注册到 hooks.ts 的 post_tool 阶段
* - 验证失败的结果作为 Observation 注入 Agent 上下文
* - 可配置开关(设置面板)
*/
import { registerHook } from './hooks.js';
import type { HarnessHook } from '../types.js';
import { logInfo, logDebug, logWarn } from './log-service.js';
import type { LoopContext, HookData, HookResult } from '../types.js';
// ═══════════════════════════════════════════════════════════════
// 验证配置
// ═══════════════════════════════════════════════════════════════
export interface VerificationConfig {
/** 是否启用自动 Lint */
autoLint: boolean;
/** 是否启用自动类型检查 */
autoTypeCheck: boolean;
/** 是否启用自动测试 */
autoTest: boolean;
/** Lint 命令(留空自动检测) */
lintCommand: string;
/** 类型检查命令 */
typeCheckCommand: string;
/** 测试命令 */
testCommand: string;
}
/** 默认配置 */
const defaultConfig: VerificationConfig = {
autoLint: false,
autoTypeCheck: false,
autoTest: false,
lintCommand: '',
typeCheckCommand: '',
testCommand: '',
};
/** 运行时配置 */
let config: VerificationConfig = { ...defaultConfig };
// ═══════════════════════════════════════════════════════════════
// 文件类型检测
// ═══════════════════════════════════════════════════════════════
/** 检测文件扩展名对应的语言 */
function detectFileType(filePath: string): string | null {
const ext = filePath.split('.').pop()?.toLowerCase() || '';
const map: Record<string, string> = {
ts: 'typescript', tsx: 'tsx', js: 'javascript', jsx: 'jsx',
py: 'python', rs: 'rust', go: 'go', java: 'java',
cpp: 'cpp', c: 'c', rb: 'ruby', css: 'css',
html: 'html', vue: 'vue', svelte: 'svelte',
};
return map[ext] || null;
}
/** 检测项目根目录(查找 package.json / pyproject.toml / Cargo.toml */
function detectProjectRoot(filePath: string): string | null {
// 简化实现:向上查找 package.json
const parts = filePath.replace(/\\/g, '/').split('/');
for (let i = parts.length - 1; i >= 0; i--) {
const dir = parts.slice(0, i + 1).join('/');
// 这里无法实际访问文件系统,返回文件所在目录的父级作为近似
}
// 返回文件所在目录
const lastSep = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\'));
return lastSep > 0 ? filePath.slice(0, lastSep) : null;
}
// ═══════════════════════════════════════════════════════════════
// 验证 Hook 实现
// ═══════════════════════════════════════════════════════════════
/**
* DiffAnalyzerHook — 修改范围分析
* 检测 Agent 的修改是否跨越了架构边界
*/
export const diffAnalyzerHook: HarnessHook = {
name: 'DiffAnalyzer',
phase: 'post_tool',
priority: 75,
enabled: true,
handler: async (_ctx: LoopContext, data: HookData): Promise<HookResult> => {
const { toolName, toolArgs } = data;
if (toolName !== 'write_file' && toolName !== 'edit_file') {
return { passed: true, message: '' };
}
const filePath = (toolArgs?.path as string) || '';
if (!filePath) return { passed: true, message: '' };
// 分析修改的文件类型
const fileType = detectFileType(filePath);
if (!fileType) return { passed: true, message: '' };
// 分析跨层调用风险
const warnings: string[] = [];
// 检测是否修改了核心基础模块
const criticalPaths = ['/core/', '/base/', '/common/', '/shared/', '/utils/', '/lib/'];
const filePathLower = filePath.toLowerCase();
for (const cp of criticalPaths) {
if (filePathLower.includes(cp)) {
warnings.push(`⚠️ 修改了核心基础模块 "${filePath}",可能影响多个依赖方`);
break;
}
}
if (warnings.length > 0) {
return {
passed: true, // 警告不阻止,但注入提醒
message: warnings.join('; '),
data: { warnings },
};
}
return { passed: true, message: '' };
},
};
/**
* FileChangeAuditHook — 文件变更审计
* 记录 Agent 的每一次文件修改
*/
export const fileChangeAuditHook: HarnessHook = {
name: 'FileChangeAudit',
phase: 'post_tool',
priority: 30,
enabled: true,
handler: async (ctx: LoopContext, data: HookData): Promise<HookResult> => {
const { toolName, toolArgs, toolResult } = data;
if (toolName !== 'write_file' && toolName !== 'edit_file' && toolName !== 'delete_file') {
return { passed: true, message: '' };
}
const filePath = (toolArgs?.path as string) || '';
const success = toolResult?.success || false;
logInfo(
`文件变更审计 [第 ${ctx.loopCount} 轮]: ` +
`${toolName} ${filePath}${success ? '✅' : '❌'}`
);
return { passed: true, message: '' };
},
};
// ═══════════════════════════════════════════════════════════════
// 初始化
// ═══════════════════════════════════════════════════════════════
let verificationInitialized = false;
export function initVerificationSystem(): void {
if (verificationInitialized) return;
registerHook(diffAnalyzerHook);
registerHook(fileChangeAuditHook);
verificationInitialized = true;
logInfo('验证系统已初始化', 'DiffAnalyzer + FileChangeAudit Hook 已注册');
}
/** 获取验证配置 */
export function getVerificationConfig(): VerificationConfig {
return { ...config };
}
/** 更新验证配置 */
export function updateVerificationConfig(updates: Partial<VerificationConfig>): void {
Object.assign(config, updates);
logInfo('验证配置已更新', JSON.stringify(config));
}