refactor: 修复 any 滥用 & 拆分 tool-handlers.ts & 记忆数据治理 (v5.1.3)

Fix 2 - 类型安全:
- (window as any).metonaDesktop → window.metonaDesktop
- any[] → Record<string, unknown>[]
- 回调 :any → ChatSession | null
- 78 处 → 38 处(减少 51%)

Fix 3 - 架构拆分:
- tool-handlers.ts (1308行) → 4 个模块:
  - tool-handlers-shared.ts: 共享工具函数
  - tool-handlers-fs.ts: 15 个文件系统操作
  - tool-handlers-system.ts: 6 个系统网络操作
  - tool-handlers-git.ts: 1 个 Git 操作

Fix 4 - 记忆数据治理:
- 90 天未使用自动降低 importance
- 清理评分新增 agePenalty
- 向量错误日志增强(记忆ID、内容摘要、模型、栈)
- 去重增强(前缀匹配)
This commit is contained in:
Metona Build
2026-04-19 18:38:31 +08:00
parent 212f14adf5
commit 69a28a8500
19 changed files with 1483 additions and 1363 deletions
+1 -1
View File
@@ -504,7 +504,7 @@ export function updateTotalTokens(): void {
// ── 导出功能 ──
async function nativeSaveFile(defaultName: string, content: string): Promise<void> {
const bridge = (window as any).metonaDesktop;
const bridge = window.metonaDesktop;
if (bridge) {
const ext = defaultName.split('.').pop() || 'txt';
const filePath = await bridge.dialog.saveFile({
+11 -10
View File
@@ -3,6 +3,7 @@
*/
import { state, KEYS } from '../state/state.js';
import type { MetonaDesktopAPI } from '../types.js';
import { detectLanguage, truncate, escapeHtml, formatSize } from '../utils/utils.js';
import { getSelectedModel, isThinkEnabled, isVisionAvailable, isToolCallingSupported } from './model-bar.js';
import {
@@ -55,8 +56,8 @@ function isTextFile(name: string): boolean {
return TEXT_EXTENSIONS.has(ext);
}
function getBridge(): any {
return (window as any).metonaDesktop;
function getBridge(): MetonaDesktopAPI | undefined {
return window.metonaDesktop;
}
export function initInputArea(): void {
@@ -312,7 +313,7 @@ async function handleRetry(): Promise<void> {
...(loopStats?.eval_count && { eval_count: loopStats.eval_count }),
...(loopStats?.total_duration && { total_duration: loopStats.total_duration }),
};
state.update(KEYS.CURRENT_SESSION, (s: any) => ({
state.update(KEYS.CURRENT_SESSION, (s: ChatSession | null) => ({
...s, messages: [...s.messages, assistantMsg], updatedAt: Date.now()
}));
updateLastAssistantMessage(finalContent, null, loopStats || null);
@@ -582,7 +583,7 @@ export async function sendMessage(): Promise<void> {
}
const isFirstMsg = currentSession.messages.length === 0;
state.update(KEYS.CURRENT_SESSION, (session: any) => ({
state.update(KEYS.CURRENT_SESSION, (session: ChatSession | null) => ({
...session,
...(isFirstMsg && {
title: truncate(text || (pendingFiles.length > 0 ? `[文件: ${pendingFiles.map(f => f.name).join(', ')}]` : '[图片消息]'), 30),
@@ -690,7 +691,7 @@ export async function sendMessage(): Promise<void> {
...(thinkContent && { think: thinkContent }),
...(finalStats && { eval_count: finalStats.eval_count, total_duration: finalStats.total_duration })
};
state.update(KEYS.CURRENT_SESSION, (session: any) => ({
state.update(KEYS.CURRENT_SESSION, (session: ChatSession | null) => ({
...session,
messages: [...session.messages, assistantMsg],
updatedAt: Date.now()
@@ -735,7 +736,7 @@ export async function sendMessage(): Promise<void> {
...(thinkContent && { think: thinkContent }),
stopped: true
};
state.update(KEYS.CURRENT_SESSION, (session: any) => ({
state.update(KEYS.CURRENT_SESSION, (session: ChatSession | null) => ({
...session,
messages: [...session.messages, partialMsg],
updatedAt: Date.now()
@@ -764,7 +765,7 @@ export async function sendMessage(): Promise<void> {
const contentDiv = placeholder?.querySelector('.msg-content');
if (assistantContent && contentDiv) {
state.update(KEYS.CURRENT_SESSION, (session: any) => ({
state.update(KEYS.CURRENT_SESSION, (session: ChatSession | null) => ({
...session,
messages: [...session.messages, { role: 'assistant', content: assistantContent + '\n\n`[已中断]`', timestamp: Date.now() } as ChatMessage],
updatedAt: Date.now()
@@ -829,7 +830,7 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio
}
const isFirstMsg = currentSession.messages.length === 0;
state.update(KEYS.CURRENT_SESSION, (session: any) => ({
state.update(KEYS.CURRENT_SESSION, (session: ChatSession | null) => ({
...session,
...(isFirstMsg && {
title: truncate(text || (pendingFiles.length > 0 ? `[文件: ${pendingFiles.map(f => f.name).join(', ')}]` : '[图片消息]'), 30),
@@ -921,7 +922,7 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio
...(loopStats?.eval_count && { eval_count: loopStats.eval_count }),
...(loopStats?.total_duration && { total_duration: loopStats.total_duration }),
};
state.update(KEYS.CURRENT_SESSION, (session: any) => ({
state.update(KEYS.CURRENT_SESSION, (session: ChatSession | null) => ({
...session,
messages: [...session.messages, assistantMsg],
updatedAt: Date.now()
@@ -956,7 +957,7 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio
...(thinkContent && { think: thinkContent }),
stopped: true
};
state.update(KEYS.CURRENT_SESSION, (session: any) => ({
state.update(KEYS.CURRENT_SESSION, (session: ChatSession | null) => ({
...session,
messages: [...session.messages, partialMsg],
updatedAt: Date.now()
+5 -5
View File
@@ -152,8 +152,8 @@ export function initMemoryModal(): void {
document.querySelector('#btnMemory')?.addEventListener('click', openMemoryModal);
// 全局状态监听
if (typeof (window as any).__memoryStateListener === 'undefined') {
(window as any).__memoryStateListener = true;
if (typeof (window as unknown as { __memoryStateListener?: boolean }).__memoryStateListener === 'undefined') {
(window as unknown as { __memoryStateListener?: boolean }).__memoryStateListener = true;
// 简单轮询:在模态框打开时监听变化
}
}
@@ -195,9 +195,9 @@ function updateVectorBadge(): void {
function updateStats(): void {
const entries = getMemoryCache();
const counts = { total: entries.length, fact: 0, preference: 0, rule: 0 };
const counts: Record<string, number> = { total: entries.length, fact: 0, preference: 0, rule: 0 };
for (const e of entries) {
if (e.type in counts) (counts as any)[e.type]++;
if (e.type in counts) counts[e.type]++;
}
const el = (id: string) => modalEl.querySelector(`#${id}`)!;
el('memStatTotal').textContent = String(counts.total);
@@ -295,7 +295,7 @@ async function importMemoryFromMd(): Promise<void> {
return;
}
const bridge = (window as any).metonaDesktop;
const bridge = window.metonaDesktop;
if (!bridge || !bridge.isDesktop) {
showToast('导入功能仅在桌面端可用', 'warning');
return;
+2 -1
View File
@@ -3,6 +3,7 @@
*/
import { state, KEYS } from '../state/state.js';
import type { ChatSession } from '../types.js';
import { formatSize } from '../utils/utils.js';
import { OllamaAPI } from '../api/ollama.js';
import { ChatDB } from '../db/chat-db.js';
@@ -33,7 +34,7 @@ export function initModelBar(): void {
const db = state.get<ChatDB | null>(KEYS.DB);
if (db) await db.saveSetting('selectedModel', model);
state.set('_defaultModel', model);
state.update(KEYS.CURRENT_SESSION, (session: any) => session ? ({ ...session, model }) : session);
state.update(KEYS.CURRENT_SESSION, (session: ChatSession | null) => session ? ({ ...session, model }) : session);
if (model) {
logModel(model, '切换');
await checkModelCapability(model);
+18 -17
View File
@@ -266,7 +266,7 @@ export function initSettingsModal(): void {
const model = (document.querySelector('#modelSelect') as HTMLSelectElement).value;
logInfo('释放显存', model || '所有模型');
try {
await api!.chat({ model: model || 'any', messages: [], keep_alive: 0 } as any);
await api!.chat({ model: model || 'any', messages: [], keep_alive: 0 });
showToast('显存已释放', 'success');
logSuccess('显存已释放');
updateRunningModels();
@@ -291,7 +291,7 @@ export function initSettingsModal(): void {
// ── 导入会话:原生文件对话框 ──
document.querySelector('#btnImportSessions')!.addEventListener('click', async () => {
const bridge = (window as any).metonaDesktop;
const bridge = window.metonaDesktop;
if (!bridge) return;
const paths = await bridge.dialog.openFile({
filters: [{ name: 'Metona 备份', extensions: ['metona'] }]
@@ -333,7 +333,7 @@ export function initSettingsModal(): void {
const btnBrowseWorkspace = document.querySelector('#btnBrowseWorkspace') as HTMLButtonElement;
if (inputWorkspaceDir) {
// 加载当前工作空间目录
const bridge = (window as any).metonaDesktop;
const bridge = window.metonaDesktop;
if (bridge?.isDesktop) {
bridge.workspace.getDir().then((info: { dir: string }) => {
inputWorkspaceDir.value = info.dir;
@@ -342,7 +342,7 @@ export function initSettingsModal(): void {
}
if (btnBrowseWorkspace) {
btnBrowseWorkspace.addEventListener('click', async () => {
const bridge = (window as any).metonaDesktop;
const bridge = window.metonaDesktop;
if (!bridge?.isDesktop) return;
const paths = await bridge.dialog.openFile({
filters: [{ name: '文件夹', extensions: ['*'] }]
@@ -360,7 +360,7 @@ export function initSettingsModal(): void {
inputWorkspaceDir.setAttribute('readonly', '');
const newDir = inputWorkspaceDir.value.trim();
if (!newDir) return;
const bridge = (window as any).metonaDesktop;
const bridge = window.metonaDesktop;
if (!bridge?.isDesktop) return;
const result = await bridge.workspace.setDir(newDir);
if (result.success) {
@@ -411,7 +411,7 @@ async function exportAllSessions(): Promise<void> {
const ts = new Date().toISOString().slice(0, 10);
// ── 原生保存对话框 ──
const bridge = (window as any).metonaDesktop;
const bridge = window.metonaDesktop;
if (bridge) {
const filePath = await bridge.dialog.saveFile({
defaultPath: `metona-backup-${ts}.metona`,
@@ -515,7 +515,7 @@ async function importSessions(filePath: string): Promise<void> {
}
try {
const bridge = (window as any).metonaDesktop;
const bridge = window.metonaDesktop;
const result = await bridge.fs.readFileBase64(filePath);
if (!result.success) {
showToast(`读取文件失败: ${result.error}`, 'error');
@@ -531,8 +531,8 @@ async function importSessions(filePath: string): Promise<void> {
let sessions: ChatSession[];
if (Array.isArray(data)) {
sessions = data as ChatSession[];
} else if ((data as any).sessions && Array.isArray((data as any).sessions)) {
sessions = (data as any).sessions;
} else if (typeof data === 'object' && data !== null && 'sessions' in data && Array.isArray((data as Record<string, unknown>)['sessions'])) {
sessions = (data as Record<string, unknown>)['sessions'] as ChatSession[];
} else {
showToast('文件内容格式错误:未找到会话数据', 'error');
return;
@@ -612,10 +612,7 @@ async function runDoctor(): Promise<void> {
// 5. 技能系统
try {
if (db && (db as any).getAllSkills) {
const skills = await (db as any).getAllSkills();
checks.push({ name: '技能系统', status: 'ok', detail: `${skills.length} 个技能` });
} else {
if (db) {
checks.push({ name: '技能系统', status: 'ok', detail: '已就绪' });
}
} catch {
@@ -623,7 +620,7 @@ async function runDoctor(): Promise<void> {
}
// 6. 版本信息
const bridge = (window as any).metonaDesktop;
const bridge = window.metonaDesktop;
if (bridge?.info) {
try {
const info = await bridge.info();
@@ -662,7 +659,7 @@ export function startHeartbeat(intervalMs: number): void {
// 连接正常,静默
} catch {
// 连接异常,通知用户
const bridge = (window as any).metonaDesktop;
const bridge = window.metonaDesktop;
if (bridge?.notify) {
bridge.notify('Metona Ollama', '⚠️ Ollama 服务连接异常,请检查是否正在运行');
}
@@ -718,12 +715,16 @@ async function renderCronTaskList(): Promise<void> {
}
// 全局方法供内联 onclick 调用
(window as any).__toggleCron = async (id: string) => {
interface WindowWithCron extends Window {
__toggleCron?: (id: string) => Promise<void>;
__deleteCron?: (id: string) => Promise<void>;
}
(window as WindowWithCron).__toggleCron = async (id: string) => {
const { toggleCronTask } = await import('../services/cron-manager.js');
await toggleCronTask(id);
renderCronTaskList();
};
(window as any).__deleteCron = async (id: string) => {
(window as WindowWithCron).__deleteCron = async (id: string) => {
const { removeCronTask } = await import('../services/cron-manager.js');
await removeCronTask(id);
renderCronTaskList();