feat: 全量日志接入 + 日志服务重构(非阻塞)
log-service 重构: - DOM 渲染改用 requestAnimationFrame 批量队列 - 全程 try-catch,任何异常都不阻塞主流程 - 新增 logConnection/logModel/logSession/logMemory/logRAG/logSetting/logIPC/logInit 便捷方法 全量接入(12 个文件): - header.ts: 连接状态变化(connected/disconnected/error) - model-bar.ts: 模型列表加载、模型切换、能力检测结果 - settings-modal.ts: 所有设置变更(系统提示词/上下文/温度/工具调用/命令执行/记忆)、显存释放、导出/导入 - tool-confirm-modal.ts: 工具确认弹窗、确认/取消操作 - memory-manager.ts: 记忆初始化、新增/删除、自动提取 - tool-registry.ts: 工具执行异常 - rag.ts: 文档分块、向量检索 - kb-modal.ts: 知识库创建、文档上传、批量上传 - history-modal.ts: 会话加载、会话删除 - main.ts: 应用初始化各阶段、桌面集成、菜单/托盘操作、新建会话 - input-area.ts: 消息发送、流式完成/错误、文件/图片添加、Agent Loop 错误
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
import { state, KEYS } from '../state/state.js';
|
import { state, KEYS } from '../state/state.js';
|
||||||
import { formatSize } from '../utils/utils.js';
|
import { formatSize } from '../utils/utils.js';
|
||||||
import { OllamaAPI } from '../api/ollama.js';
|
import { OllamaAPI } from '../api/ollama.js';
|
||||||
|
import { logConnection, logError, logDebug } from '../services/log-service.js';
|
||||||
|
|
||||||
let connStatusEl: HTMLElement;
|
let connStatusEl: HTMLElement;
|
||||||
|
|
||||||
@@ -24,11 +25,13 @@ export async function checkConnection(): Promise<{ ok: boolean; version?: { vers
|
|||||||
connStatusEl.className = 'conn-status connected';
|
connStatusEl.className = 'conn-status connected';
|
||||||
connStatusEl.title = `已连接 (v${version.version})`;
|
connStatusEl.title = `已连接 (v${version.version})`;
|
||||||
connStatusEl.querySelector('.status-label')!.textContent = '已连接';
|
connStatusEl.querySelector('.status-label')!.textContent = '已连接';
|
||||||
|
logConnection('connected', `Ollama v${version.version}`);
|
||||||
return { ok: true, version };
|
return { ok: true, version };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
connStatusEl.className = 'conn-status disconnected';
|
connStatusEl.className = 'conn-status disconnected';
|
||||||
connStatusEl.title = '未连接';
|
connStatusEl.title = '未连接';
|
||||||
connStatusEl.querySelector('.status-label')!.textContent = '未连接';
|
connStatusEl.querySelector('.status-label')!.textContent = '未连接';
|
||||||
|
logConnection('disconnected', (err as Error).message);
|
||||||
return { ok: false, error: err as Error };
|
return { ok: false, error: err as Error };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,10 +54,12 @@ export async function updateConnectionInfo(): Promise<void> {
|
|||||||
badge.className = 'status-badge success';
|
badge.className = 'status-badge success';
|
||||||
verEl.textContent = `v${version.version}`;
|
verEl.textContent = `v${version.version}`;
|
||||||
corsHint.style.display = 'none';
|
corsHint.style.display = 'none';
|
||||||
|
logConnection('info', `Ollama v${version.version}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
badge.textContent = '✗ 未连接';
|
badge.textContent = '✗ 未连接';
|
||||||
badge.className = 'status-badge error';
|
badge.className = 'status-badge error';
|
||||||
verEl.textContent = '';
|
verEl.textContent = '';
|
||||||
|
logError('连接检测失败', (err as Error).message);
|
||||||
|
|
||||||
if ((err as Error).message.includes('Failed to fetch') || (err as Error).message.includes('NetworkError') || (err as Error).message.includes('CORS')) {
|
if ((err as Error).message.includes('Failed to fetch') || (err as Error).message.includes('NetworkError') || (err as Error).message.includes('CORS')) {
|
||||||
corsHint.style.display = '';
|
corsHint.style.display = '';
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { state, KEYS } from '../state/state.js';
|
|||||||
import { debounce, escapeHtml, formatTime } from '../utils/utils.js';
|
import { debounce, escapeHtml, formatTime } from '../utils/utils.js';
|
||||||
import { exportAsMarkdown, exportAsHtml, exportAsTxt, renderMessages, clearMessages } from './chat-area.js';
|
import { exportAsMarkdown, exportAsHtml, exportAsTxt, renderMessages, clearMessages } from './chat-area.js';
|
||||||
import { ChatDB } from '../db/chat-db.js';
|
import { ChatDB } from '../db/chat-db.js';
|
||||||
|
import { logSession, logWarn } from '../services/log-service.js';
|
||||||
import type { ChatSession } from '../types.js';
|
import type { ChatSession } from '../types.js';
|
||||||
|
|
||||||
const HISTORY_PAGE_SIZE = 10;
|
const HISTORY_PAGE_SIZE = 10;
|
||||||
@@ -57,6 +58,7 @@ export function initHistoryModal(): void {
|
|||||||
if (target.classList.contains('btn-delete-session')) {
|
if (target.classList.contains('btn-delete-session')) {
|
||||||
if (confirm('确定删除此会话?')) {
|
if (confirm('确定删除此会话?')) {
|
||||||
await db!.deleteSession(sessionId);
|
await db!.deleteSession(sessionId);
|
||||||
|
logSession('删除', sessionId);
|
||||||
const currentSession = state.get<ChatSession | null>(KEYS.CURRENT_SESSION);
|
const currentSession = state.get<ChatSession | null>(KEYS.CURRENT_SESSION);
|
||||||
if (currentSession && currentSession.id === sessionId) {
|
if (currentSession && currentSession.id === sessionId) {
|
||||||
(document.querySelector('#btnNewChat') as HTMLElement).click();
|
(document.querySelector('#btnNewChat') as HTMLElement).click();
|
||||||
@@ -164,8 +166,12 @@ async function loadHistorySession(sessionId: string): Promise<void> {
|
|||||||
if (!db) return;
|
if (!db) return;
|
||||||
|
|
||||||
const session = await db.getSession(sessionId);
|
const session = await db.getSession(sessionId);
|
||||||
if (!session) return;
|
if (!session) {
|
||||||
|
logWarn('会话不存在', sessionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logSession('加载', `${session.title} (${session.messages.length}条消息)`);
|
||||||
state.set(KEYS.CURRENT_SESSION, session);
|
state.set(KEYS.CURRENT_SESSION, session);
|
||||||
state.set(KEYS.IS_HISTORY_VIEW, true);
|
state.set(KEYS.IS_HISTORY_VIEW, true);
|
||||||
|
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ async function handleImageSelect(files: FileList): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
const base64 = await fileToBase64(file);
|
const base64 = await fileToBase64(file);
|
||||||
pendingImages.push({ name: file.name, base64 });
|
pendingImages.push({ name: file.name, base64 });
|
||||||
|
logInfo(`图片已添加: ${file.name}`, formatSize(file.size));
|
||||||
renderImagePreviews();
|
renderImagePreviews();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[InputArea] 图片读取失败:', err);
|
console.error('[InputArea] 图片读取失败:', err);
|
||||||
@@ -172,6 +173,7 @@ async function handleTextFileSelect(files: FileList): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
const content = await fileToText(file);
|
const content = await fileToText(file);
|
||||||
pendingFiles.push({ name: file.name, content, language: detectLanguage(file.name), size: file.size });
|
pendingFiles.push({ name: file.name, content, language: detectLanguage(file.name), size: file.size });
|
||||||
|
logInfo(`文件已添加: ${file.name}`, formatSize(file.size));
|
||||||
renderFilePreviews();
|
renderFilePreviews();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[InputArea] 文件读取失败:', err);
|
console.error('[InputArea] 文件读取失败:', err);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
import { fileToText, formatSize, escapeHtml, truncate } from '../utils/utils.js';
|
import { fileToText, formatSize, escapeHtml, truncate } from '../utils/utils.js';
|
||||||
import { showToast } from './toast.js';
|
import { showToast } from './toast.js';
|
||||||
import { OllamaAPI } from '../api/ollama.js';
|
import { OllamaAPI } from '../api/ollama.js';
|
||||||
|
import { logRAG, logError } from '../services/log-service.js';
|
||||||
import type { VectorCollection, SearchResult } from '../types.js';
|
import type { VectorCollection, SearchResult } from '../types.js';
|
||||||
|
|
||||||
let kbModalEl: HTMLElement;
|
let kbModalEl: HTMLElement;
|
||||||
@@ -209,6 +210,7 @@ async function createCollection(): Promise<void> {
|
|||||||
currentColId = col.id;
|
currentColId = col.id;
|
||||||
nameInput.value = '';
|
nameInput.value = '';
|
||||||
showToast(`知识库「${name}」已创建`, 'success');
|
showToast(`知识库「${name}」已创建`, 'success');
|
||||||
|
logRAG(`创建集合: ${name}`);
|
||||||
await refreshCollections();
|
await refreshCollections();
|
||||||
await refreshDocList();
|
await refreshDocList();
|
||||||
}
|
}
|
||||||
@@ -248,10 +250,12 @@ async function handleDocUpload(e: Event): Promise<void> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
showToast(`${file.name} 已添加到知识库(${result.chunkCount} 个分块)`, 'success');
|
showToast(`${file.name} 已添加到知识库(${result.chunkCount} 个分块)`, 'success');
|
||||||
|
logRAG(`文档上传: ${file.name}`, `${result.chunkCount} 个分块`);
|
||||||
successCount++;
|
successCount++;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[KB] 文档处理失败:', err);
|
console.error('[KB] 文档处理失败:', err);
|
||||||
showToast(`${file.name} 处理失败: ${(err as Error).message}`, 'error');
|
showToast(`${file.name} 处理失败: ${(err as Error).message}`, 'error');
|
||||||
|
logError(`RAG 文档处理失败: ${file.name}`, (err as Error).message);
|
||||||
failCount++;
|
failCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -260,6 +264,7 @@ async function handleDocUpload(e: Event): Promise<void> {
|
|||||||
|
|
||||||
if (fileArr.length > 1) {
|
if (fileArr.length > 1) {
|
||||||
showToast(`上传完成:成功 ${successCount} 个,失败 ${failCount} 个`, failCount > 0 ? 'warning' : 'success');
|
showToast(`上传完成:成功 ${successCount} 个,失败 ${failCount} 个`, failCount > 0 ? 'warning' : 'success');
|
||||||
|
logRAG(`批量上传完成`, `成功 ${successCount}, 失败 ${failCount}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await refreshCollections();
|
await refreshCollections();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { state, KEYS } from '../state/state.js';
|
|||||||
import { formatSize } from '../utils/utils.js';
|
import { formatSize } from '../utils/utils.js';
|
||||||
import { OllamaAPI } from '../api/ollama.js';
|
import { OllamaAPI } from '../api/ollama.js';
|
||||||
import { ChatDB } from '../db/chat-db.js';
|
import { ChatDB } from '../db/chat-db.js';
|
||||||
|
import { logModel, logDebug, logError, logWarn } from '../services/log-service.js';
|
||||||
import type { OllamaModelDetail, ModelCaps } from '../types.js';
|
import type { OllamaModelDetail, ModelCaps } from '../types.js';
|
||||||
|
|
||||||
let modelSelectEl: HTMLSelectElement;
|
let modelSelectEl: HTMLSelectElement;
|
||||||
@@ -34,6 +35,7 @@ export function initModelBar(): void {
|
|||||||
state.set('_defaultModel', model);
|
state.set('_defaultModel', model);
|
||||||
state.update(KEYS.CURRENT_SESSION, (session: any) => session ? ({ ...session, model }) : session);
|
state.update(KEYS.CURRENT_SESSION, (session: any) => session ? ({ ...session, model }) : session);
|
||||||
if (model) {
|
if (model) {
|
||||||
|
logModel(model, '切换');
|
||||||
await checkModelCapability(model);
|
await checkModelCapability(model);
|
||||||
} else {
|
} else {
|
||||||
setThinkAvailable(false);
|
setThinkAvailable(false);
|
||||||
@@ -58,9 +60,12 @@ export async function loadModels(): Promise<void> {
|
|||||||
modelSelectEl.innerHTML = '<option value="">未安装任何模型</option>';
|
modelSelectEl.innerHTML = '<option value="">未安装任何模型</option>';
|
||||||
badgeThinkEl.style.display = 'none'; badgeVisionEl.style.display = 'none';
|
badgeThinkEl.style.display = 'none'; badgeVisionEl.style.display = 'none';
|
||||||
setThinkAvailable(false); setVisionAvailable(false);
|
setThinkAvailable(false); setVisionAvailable(false);
|
||||||
|
logWarn('未检测到已安装模型');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logModel(`${data.models.length} 个模型`, '加载列表');
|
||||||
|
|
||||||
data.models
|
data.models
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const nameA = a.name.toLowerCase(), nameB = b.name.toLowerCase();
|
const nameA = a.name.toLowerCase(), nameB = b.name.toLowerCase();
|
||||||
@@ -86,6 +91,7 @@ export async function loadModels(): Promise<void> {
|
|||||||
filterEmbedModels(data.models);
|
filterEmbedModels(data.models);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[ModelBar] 加载模型失败:', err);
|
console.error('[ModelBar] 加载模型失败:', err);
|
||||||
|
logError('加载模型列表失败', (err as Error).message);
|
||||||
modelSelectEl.innerHTML = '<option value="">加载失败 - 检查连接</option>';
|
modelSelectEl.innerHTML = '<option value="">加载失败 - 检查连接</option>';
|
||||||
badgeThinkEl.style.display = 'none'; badgeVisionEl.style.display = 'none';
|
badgeThinkEl.style.display = 'none'; badgeVisionEl.style.display = 'none';
|
||||||
setThinkAvailable(false); setVisionAvailable(false);
|
setThinkAvailable(false); setVisionAvailable(false);
|
||||||
@@ -125,6 +131,7 @@ async function filterEmbedModels(models: Array<{ name: string }>): Promise<void>
|
|||||||
if (!isCompletion) {
|
if (!isCompletion) {
|
||||||
const opt = modelSelectEl.querySelector(`option[value="${CSS.escape(m.name)}"]`);
|
const opt = modelSelectEl.querySelector(`option[value="${CSS.escape(m.name)}"]`);
|
||||||
if (opt) opt.remove();
|
if (opt) opt.remove();
|
||||||
|
logDebug(`模型 ${m.name} 不是 completion 模型,已移除`);
|
||||||
}
|
}
|
||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
}
|
}
|
||||||
@@ -159,9 +166,11 @@ async function checkModelCapability(modelName: string): Promise<void> {
|
|||||||
completion: capabilities.includes('completion'),
|
completion: capabilities.includes('completion'),
|
||||||
};
|
};
|
||||||
modelCapabilityCache.set(modelName, caps);
|
modelCapabilityCache.set(modelName, caps);
|
||||||
|
logDebug(`能力检测: ${modelName}`, capabilities.join(', '));
|
||||||
applyCapability(modelName, caps);
|
applyCapability(modelName, caps);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('[ModelBar] 获取模型能力失败:', err);
|
console.warn('[ModelBar] 获取模型能力失败:', err);
|
||||||
|
logWarn(`能力检测失败: ${modelName}`, (err as Error).message);
|
||||||
const fallback = { think: false, vision: false, tools: false, completion: true };
|
const fallback = { think: false, vision: false, tools: false, completion: true };
|
||||||
modelCapabilityCache.set(modelName, fallback);
|
modelCapabilityCache.set(modelName, fallback);
|
||||||
applyCapability(modelName, fallback);
|
applyCapability(modelName, fallback);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { debounce } from '../utils/utils.js';
|
|||||||
import { encryptData, decryptData, isMetonaFile } from '../services/crypto.js';
|
import { encryptData, decryptData, isMetonaFile } from '../services/crypto.js';
|
||||||
import { setMemoryEnabled } from '../services/memory-manager.js';
|
import { setMemoryEnabled } from '../services/memory-manager.js';
|
||||||
import { setToolEnabled } from '../services/tool-registry.js';
|
import { setToolEnabled } from '../services/tool-registry.js';
|
||||||
|
import { logSetting, logInfo, logWarn, logError, logSuccess } from '../services/log-service.js';
|
||||||
import { updateConnectionInfo, updateRunningModels } from './header.js';
|
import { updateConnectionInfo, updateRunningModels } from './header.js';
|
||||||
import { loadModels } from './model-bar.js';
|
import { loadModels } from './model-bar.js';
|
||||||
import { showToast } from './toast.js';
|
import { showToast } from './toast.js';
|
||||||
@@ -39,9 +40,11 @@ export function initSettingsModal(): void {
|
|||||||
|
|
||||||
document.querySelector('#toggleSystemPrompt')!.addEventListener('change', async (e) => {
|
document.querySelector('#toggleSystemPrompt')!.addEventListener('change', async (e) => {
|
||||||
const db = state.get<ChatDB | null>(KEYS.DB);
|
const db = state.get<ChatDB | null>(KEYS.DB);
|
||||||
state.set(KEYS.SYSTEM_PROMPT_ENABLED, (e.target as HTMLInputElement).checked);
|
const checked = (e.target as HTMLInputElement).checked;
|
||||||
(document.querySelector('#inputSystemPrompt') as HTMLTextAreaElement).disabled = !(e.target as HTMLInputElement).checked;
|
state.set(KEYS.SYSTEM_PROMPT_ENABLED, checked);
|
||||||
if (db) await db.saveSetting('systemPromptEnabled', (e.target as HTMLInputElement).checked);
|
(document.querySelector('#inputSystemPrompt') as HTMLTextAreaElement).disabled = !checked;
|
||||||
|
if (db) await db.saveSetting('systemPromptEnabled', checked);
|
||||||
|
logSetting('系统提示词', checked ? '开启' : '关闭');
|
||||||
});
|
});
|
||||||
|
|
||||||
const saveSystemPrompt = debounce(async () => {
|
const saveSystemPrompt = debounce(async () => {
|
||||||
@@ -58,6 +61,7 @@ export function initSettingsModal(): void {
|
|||||||
const numCtx = val ? parseInt(val) : 24576;
|
const numCtx = val ? parseInt(val) : 24576;
|
||||||
state.set(KEYS.NUM_CTX, numCtx);
|
state.set(KEYS.NUM_CTX, numCtx);
|
||||||
if (db) await db.saveSetting('numCtx', numCtx);
|
if (db) await db.saveSetting('numCtx', numCtx);
|
||||||
|
logSetting('上下文长度', `${numCtx} tokens`);
|
||||||
}, 500);
|
}, 500);
|
||||||
document.querySelector('#inputNumCtx')!.addEventListener('input', saveNumCtx);
|
document.querySelector('#inputNumCtx')!.addEventListener('input', saveNumCtx);
|
||||||
|
|
||||||
@@ -71,27 +75,33 @@ export function initSettingsModal(): void {
|
|||||||
const val = parseFloat(tempSlider.value);
|
const val = parseFloat(tempSlider.value);
|
||||||
state.set('temperature', val);
|
state.set('temperature', val);
|
||||||
if (db) await db.saveSetting('temperature', val);
|
if (db) await db.saveSetting('temperature', val);
|
||||||
|
logSetting('温度', val.toFixed(1));
|
||||||
}, 300);
|
}, 300);
|
||||||
tempSlider.addEventListener('change', saveTemperature);
|
tempSlider.addEventListener('change', saveTemperature);
|
||||||
|
|
||||||
document.querySelector('#btnReleaseVRAM')!.addEventListener('click', async () => {
|
document.querySelector('#btnReleaseVRAM')!.addEventListener('click', async () => {
|
||||||
const api = state.get<OllamaAPI>(KEYS.API);
|
const api = state.get<OllamaAPI>(KEYS.API);
|
||||||
const model = (document.querySelector('#modelSelect') as HTMLSelectElement).value;
|
const model = (document.querySelector('#modelSelect') as HTMLSelectElement).value;
|
||||||
|
logInfo('释放显存', model || '所有模型');
|
||||||
try {
|
try {
|
||||||
await api!.chat({ model: model || 'any', messages: [], keep_alive: 0 } as any);
|
await api!.chat({ model: model || 'any', messages: [], keep_alive: 0 } as any);
|
||||||
showToast('显存已释放', 'success');
|
showToast('显存已释放', 'success');
|
||||||
|
logSuccess('显存已释放');
|
||||||
updateRunningModels();
|
updateRunningModels();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(`释放失败: ${(err as Error).message}`, 'error');
|
showToast(`释放失败: ${(err as Error).message}`, 'error');
|
||||||
|
logError('释放显存失败', (err as Error).message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelector('#btnClearAllHistory')!.addEventListener('click', async () => {
|
document.querySelector('#btnClearAllHistory')!.addEventListener('click', async () => {
|
||||||
const db = state.get<ChatDB | null>(KEYS.DB);
|
const db = state.get<ChatDB | null>(KEYS.DB);
|
||||||
if (confirm('确定清空所有历史记录?此操作不可恢复!')) {
|
if (confirm('确定清空所有历史记录?此操作不可恢复!')) {
|
||||||
|
logWarn('清空所有历史记录');
|
||||||
await db!.clearAll();
|
await db!.clearAll();
|
||||||
(document.querySelector('#btnNewChat') as HTMLElement).click();
|
(document.querySelector('#btnNewChat') as HTMLElement).click();
|
||||||
showToast('已清空所有历史记录', 'success');
|
showToast('已清空所有历史记录', 'success');
|
||||||
|
logSuccess('历史记录已清空');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -117,11 +127,14 @@ export function initSettingsModal(): void {
|
|||||||
const modelSupportsTools = state.get<boolean>('modelSupportsTools', false);
|
const modelSupportsTools = state.get<boolean>('modelSupportsTools', false);
|
||||||
if (!modelSupportsTools) {
|
if (!modelSupportsTools) {
|
||||||
showToast('⚠️ 当前模型可能不支持 Tool Calling,建议使用 Qwen3、Llama 3.1+ 等模型', 'warning', 5000);
|
showToast('⚠️ 当前模型可能不支持 Tool Calling,建议使用 Qwen3、Llama 3.1+ 等模型', 'warning', 5000);
|
||||||
|
logWarn('Tool Calling 已开启(模型不支持)');
|
||||||
} else {
|
} else {
|
||||||
showToast('工具调用已开启', 'info');
|
showToast('工具调用已开启', 'info');
|
||||||
|
logInfo('Tool Calling 已开启');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
showToast('工具调用已关闭', 'info');
|
showToast('工具调用已关闭', 'info');
|
||||||
|
logInfo('Tool Calling 已关闭');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -135,6 +148,9 @@ export function initSettingsModal(): void {
|
|||||||
setToolEnabled('run_command', toggleRunCommand.checked);
|
setToolEnabled('run_command', toggleRunCommand.checked);
|
||||||
if (toggleRunCommand.checked) {
|
if (toggleRunCommand.checked) {
|
||||||
showToast('⚠️ 命令执行已开启,请谨慎使用', 'warning');
|
showToast('⚠️ 命令执行已开启,请谨慎使用', 'warning');
|
||||||
|
logWarn('命令执行已开启(高风险)');
|
||||||
|
} else {
|
||||||
|
logInfo('命令执行已关闭');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -145,6 +161,7 @@ export function initSettingsModal(): void {
|
|||||||
toggleMemory.addEventListener('change', () => {
|
toggleMemory.addEventListener('change', () => {
|
||||||
setMemoryEnabled(toggleMemory.checked);
|
setMemoryEnabled(toggleMemory.checked);
|
||||||
showToast(toggleMemory.checked ? 'Agent 记忆已开启' : 'Agent 记忆已关闭', 'info');
|
showToast(toggleMemory.checked ? 'Agent 记忆已开启' : 'Agent 记忆已关闭', 'info');
|
||||||
|
logSetting('Agent 记忆', toggleMemory.checked ? '开启' : '关闭');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -185,9 +202,11 @@ async function exportAllSessions(): Promise<void> {
|
|||||||
a.download = `metona-backup-${ts}.metona`;
|
a.download = `metona-backup-${ts}.metona`;
|
||||||
a.click();
|
a.click();
|
||||||
showToast(`已导出 ${sessions.length} 个会话(已加密)`, 'success');
|
showToast(`已导出 ${sessions.length} 个会话(已加密)`, 'success');
|
||||||
|
logSuccess(`导出完成: ${sessions.length} 个会话`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[Settings] 导出失败:', err);
|
console.error('[Settings] 导出失败:', err);
|
||||||
showToast(`导出失败: ${(err as Error).message}`, 'error');
|
showToast(`导出失败: ${(err as Error).message}`, 'error');
|
||||||
|
logError('导出失败', (err as Error).message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,6 +222,7 @@ async function importSessions(file: File): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
const buffer = await file.arrayBuffer();
|
const buffer = await file.arrayBuffer();
|
||||||
const data = await decryptData(buffer);
|
const data = await decryptData(buffer);
|
||||||
|
logInfo('导入会话', `文件: ${file.name}`);
|
||||||
|
|
||||||
let sessions: ChatSession[];
|
let sessions: ChatSession[];
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
@@ -216,8 +236,10 @@ async function importSessions(file: File): Promise<void> {
|
|||||||
|
|
||||||
const result = await db.importSessions(sessions);
|
const result = await db.importSessions(sessions);
|
||||||
showToast(`导入完成:${result.imported} 个会话${result.skipped > 0 ? `,跳过 ${result.skipped} 个` : ''}`, 'success', 4000);
|
showToast(`导入完成:${result.imported} 个会话${result.skipped > 0 ? `,跳过 ${result.skipped} 个` : ''}`, 'success', 4000);
|
||||||
|
logSuccess(`导入完成: ${result.imported} 个, 跳过 ${result.skipped} 个`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[Settings] 导入失败:', err);
|
console.error('[Settings] 导入失败:', err);
|
||||||
showToast(`导入失败: ${(err as Error).message}`, 'error');
|
showToast(`导入失败: ${(err as Error).message}`, 'error');
|
||||||
|
logError('导入失败', (err as Error).message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
import type { ToolCall } from '../types.js';
|
import type { ToolCall } from '../types.js';
|
||||||
import { getToolIcon, formatToolName } from '../services/tool-registry.js';
|
import { getToolIcon, formatToolName } from '../services/tool-registry.js';
|
||||||
|
import { logDebug, logInfo } from '../services/log-service.js';
|
||||||
|
|
||||||
let modalEl: HTMLElement | null = null;
|
let modalEl: HTMLElement | null = null;
|
||||||
let resolveConfirm: ((confirmed: boolean) => void) | null = null;
|
let resolveConfirm: ((confirmed: boolean) => void) | null = null;
|
||||||
@@ -45,6 +46,7 @@ export function showToolConfirm(call: ToolCall): Promise<boolean> {
|
|||||||
|
|
||||||
const icon = getToolIcon(call.function.name);
|
const icon = getToolIcon(call.function.name);
|
||||||
const name = formatToolName(call.function.name);
|
const name = formatToolName(call.function.name);
|
||||||
|
logInfo(`等待确认: ${call.function.name}`, JSON.stringify(call.function.arguments));
|
||||||
|
|
||||||
const titleEl = modalEl.querySelector('#toolConfirmTitle')!;
|
const titleEl = modalEl.querySelector('#toolConfirmTitle')!;
|
||||||
const bodyEl = modalEl.querySelector('#toolConfirmBody')!;
|
const bodyEl = modalEl.querySelector('#toolConfirmBody')!;
|
||||||
@@ -83,11 +85,13 @@ export function showToolConfirm(call: ToolCall): Promise<boolean> {
|
|||||||
const onConfirm = (e: Event) => {
|
const onConfirm = (e: Event) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
cleanup();
|
cleanup();
|
||||||
|
logInfo(`已确认: ${call.function.name}`);
|
||||||
resolveConfirm?.(true);
|
resolveConfirm?.(true);
|
||||||
};
|
};
|
||||||
const onCancel = (e: Event) => {
|
const onCancel = (e: Event) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
cleanup();
|
cleanup();
|
||||||
|
logInfo(`已取消: ${call.function.name}`);
|
||||||
resolveConfirm?.(false);
|
resolveConfirm?.(false);
|
||||||
};
|
};
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
|
|||||||
+19
-1
@@ -26,6 +26,7 @@ import { setToolEnabled } from './services/tool-registry.js';
|
|||||||
import { initToolConfirmModal } from './components/tool-confirm-modal.js';
|
import { initToolConfirmModal } from './components/tool-confirm-modal.js';
|
||||||
import { initMemoryPanel } from './components/memory-panel.js';
|
import { initMemoryPanel } from './components/memory-panel.js';
|
||||||
import { initLogPanel } from './services/log-service.js';
|
import { initLogPanel } from './services/log-service.js';
|
||||||
|
import { logInfo, logSuccess, logError, logDebug, logInit } from './services/log-service.js';
|
||||||
import type { ChatSession } from './types.js';
|
import type { ChatSession } from './types.js';
|
||||||
|
|
||||||
function initHelpModal(): void {
|
function initHelpModal(): void {
|
||||||
@@ -43,11 +44,17 @@ function initHelpModal(): void {
|
|||||||
|
|
||||||
function setupDesktopIntegration(): void {
|
function setupDesktopIntegration(): void {
|
||||||
const bridge = window.__metonaBridge;
|
const bridge = window.__metonaBridge;
|
||||||
if (!bridge || !bridge.isDesktop) return;
|
if (!bridge || !bridge.isDesktop) {
|
||||||
|
logDebug('非桌面环境,跳过桌面集成');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logInit('桌面集成...');
|
||||||
|
|
||||||
console.log('[Desktop Integration] 初始化桌面集成...');
|
console.log('[Desktop Integration] 初始化桌面集成...');
|
||||||
|
|
||||||
bridge.onMenuAction((action: string) => {
|
bridge.onMenuAction((action: string) => {
|
||||||
|
logInfo(`菜单操作: ${action}`);
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'new-chat': document.querySelector('#btnNewChat')?.dispatchEvent(new Event('click')); break;
|
case 'new-chat': document.querySelector('#btnNewChat')?.dispatchEvent(new Event('click')); break;
|
||||||
case 'import': document.querySelector('#btnImportSessions')?.dispatchEvent(new Event('click')); break;
|
case 'import': document.querySelector('#btnImportSessions')?.dispatchEvent(new Event('click')); break;
|
||||||
@@ -57,6 +64,7 @@ function setupDesktopIntegration(): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
bridge.onTrayAction((action: string) => {
|
bridge.onTrayAction((action: string) => {
|
||||||
|
logInfo(`托盘操作: ${action}`);
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'new-chat': document.querySelector('#btnNewChat')?.dispatchEvent(new Event('click')); break;
|
case 'new-chat': document.querySelector('#btnNewChat')?.dispatchEvent(new Event('click')); break;
|
||||||
}
|
}
|
||||||
@@ -80,6 +88,7 @@ function setupDesktopIntegration(): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
console.log('[Desktop Integration] 桌面集成完成 ✓');
|
console.log('[Desktop Integration] 桌面集成完成 ✓');
|
||||||
|
logSuccess('桌面集成完成');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[Metona] TypeScript + Electron v3.0.0 ' + new Date().toISOString());
|
console.log('[Metona] TypeScript + Electron v3.0.0 ' + new Date().toISOString());
|
||||||
@@ -110,6 +119,7 @@ async function startNewSession(): Promise<void> {
|
|||||||
if (api && model) {
|
if (api && model) {
|
||||||
try {
|
try {
|
||||||
await api.chat({ model, messages: [], keep_alive: 0 } as any);
|
await api.chat({ model, messages: [], keep_alive: 0 } as any);
|
||||||
|
logDebug('释放旧模型显存');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[App] 释放显存失败:', e);
|
console.warn('[App] 释放显存失败:', e);
|
||||||
}
|
}
|
||||||
@@ -117,6 +127,7 @@ async function startNewSession(): Promise<void> {
|
|||||||
|
|
||||||
const session = createNewSession();
|
const session = createNewSession();
|
||||||
state.set(KEYS.CURRENT_SESSION, session);
|
state.set(KEYS.CURRENT_SESSION, session);
|
||||||
|
logInfo('新建会话');
|
||||||
state.set(KEYS.IS_HISTORY_VIEW, false);
|
state.set(KEYS.IS_HISTORY_VIEW, false);
|
||||||
|
|
||||||
(document.querySelector('#historyBar') as HTMLElement).style.display = 'none';
|
(document.querySelector('#historyBar') as HTMLElement).style.display = 'none';
|
||||||
@@ -132,14 +143,18 @@ async function init(): Promise<void> {
|
|||||||
let db: ChatDB | undefined;
|
let db: ChatDB | undefined;
|
||||||
let api: OllamaAPI;
|
let api: OllamaAPI;
|
||||||
|
|
||||||
|
logInit('启动...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
db = new ChatDB();
|
db = new ChatDB();
|
||||||
await db.init();
|
await db.init();
|
||||||
state.set(KEYS.DB, db);
|
state.set(KEYS.DB, db);
|
||||||
|
logInit('IndexedDB 已连接');
|
||||||
|
|
||||||
const serverUrl = await db.getSetting('serverUrl', 'http://127.0.0.1:11434');
|
const serverUrl = await db.getSetting('serverUrl', 'http://127.0.0.1:11434');
|
||||||
api = new OllamaAPI(serverUrl);
|
api = new OllamaAPI(serverUrl);
|
||||||
state.set(KEYS.API, api);
|
state.set(KEYS.API, api);
|
||||||
|
logInit(`Ollama 地址: ${serverUrl}`);
|
||||||
|
|
||||||
state.set(KEYS.CURRENT_SESSION, createNewSession());
|
state.set(KEYS.CURRENT_SESSION, createNewSession());
|
||||||
state.set(KEYS.IS_STREAMING, false);
|
state.set(KEYS.IS_STREAMING, false);
|
||||||
@@ -167,6 +182,7 @@ async function init(): Promise<void> {
|
|||||||
await loadModels();
|
await loadModels();
|
||||||
await initVectorStore();
|
await initVectorStore();
|
||||||
await initMemoryManager();
|
await initMemoryManager();
|
||||||
|
logInit('所有组件已就绪');
|
||||||
|
|
||||||
const savedModel = await db.getSetting('selectedModel', '');
|
const savedModel = await db.getSetting('selectedModel', '');
|
||||||
if (savedModel) {
|
if (savedModel) {
|
||||||
@@ -210,8 +226,10 @@ async function init(): Promise<void> {
|
|||||||
(document.querySelector('#chatInput') as HTMLElement)?.focus();
|
(document.querySelector('#chatInput') as HTMLElement)?.focus();
|
||||||
|
|
||||||
console.log('[App] 初始化完成');
|
console.log('[App] 初始化完成');
|
||||||
|
logSuccess('应用初始化完成');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[App] 初始化失败:', err);
|
console.error('[App] 初始化失败:', err);
|
||||||
|
logError('初始化失败', (err as Error).message);
|
||||||
if (!api!) {
|
if (!api!) {
|
||||||
api = new OllamaAPI();
|
api = new OllamaAPI();
|
||||||
state.set(KEYS.API, api);
|
state.set(KEYS.API, api);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* LogService - 实时日志面板服务
|
* LogService - 实时日志面板服务
|
||||||
* 收集应用内所有事件,渲染到左侧日志面板
|
* 收集应用内所有事件,渲染到左侧日志面板
|
||||||
|
* 全程 try-catch,任何情况下不阻塞主流程
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type LogLevel = 'info' | 'success' | 'warn' | 'error' | 'debug' | 'tool' | 'think' | 'stream';
|
export type LogLevel = 'info' | 'success' | 'warn' | 'error' | 'debug' | 'tool' | 'think' | 'stream';
|
||||||
@@ -20,6 +21,8 @@ const MAX_LOGS = 500;
|
|||||||
let logs: LogEntry[] = [];
|
let logs: LogEntry[] = [];
|
||||||
let autoScroll = true;
|
let autoScroll = true;
|
||||||
let idCounter = 0;
|
let idCounter = 0;
|
||||||
|
let renderQueued = false;
|
||||||
|
const pendingEntries: LogEntry[] = [];
|
||||||
|
|
||||||
const LEVEL_ICONS: Record<LogLevel, string> = {
|
const LEVEL_ICONS: Record<LogLevel, string> = {
|
||||||
info: 'ℹ️',
|
info: 'ℹ️',
|
||||||
@@ -33,33 +36,40 @@ const LEVEL_ICONS: Record<LogLevel, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function initLogPanel(): void {
|
export function initLogPanel(): void {
|
||||||
|
try {
|
||||||
logBodyEl = document.querySelector('#logPanelBody')!;
|
logBodyEl = document.querySelector('#logPanelBody')!;
|
||||||
logPanelEl = document.querySelector('#logPanel')!;
|
logPanelEl = document.querySelector('#logPanel')!;
|
||||||
|
|
||||||
const btnToggle = document.querySelector('#btnToggleLog')!;
|
document.querySelector('#btnToggleLog')!.addEventListener('click', () => {
|
||||||
const btnClose = document.querySelector('#btnCloseLog')!;
|
try {
|
||||||
const btnClear = document.querySelector('#btnClearLog')!;
|
|
||||||
|
|
||||||
btnToggle.addEventListener('click', () => {
|
|
||||||
const visible = logPanelEl!.style.display !== 'none';
|
const visible = logPanelEl!.style.display !== 'none';
|
||||||
logPanelEl!.style.display = visible ? 'none' : '';
|
logPanelEl!.style.display = visible ? 'none' : '';
|
||||||
document.querySelector('#mainWrap')!.classList.toggle('log-open', !visible);
|
document.querySelector('#mainWrap')!.classList.toggle('log-open', !visible);
|
||||||
|
} catch { /* ignore */ }
|
||||||
});
|
});
|
||||||
|
|
||||||
btnClose.addEventListener('click', () => {
|
document.querySelector('#btnCloseLog')!.addEventListener('click', () => {
|
||||||
|
try {
|
||||||
logPanelEl!.style.display = 'none';
|
logPanelEl!.style.display = 'none';
|
||||||
document.querySelector('#mainWrap')!.classList.remove('log-open');
|
document.querySelector('#mainWrap')!.classList.remove('log-open');
|
||||||
|
} catch { /* ignore */ }
|
||||||
});
|
});
|
||||||
|
|
||||||
btnClear.addEventListener('click', () => {
|
document.querySelector('#btnClearLog')!.addEventListener('click', () => {
|
||||||
|
try {
|
||||||
logs = [];
|
logs = [];
|
||||||
|
pendingEntries.length = 0;
|
||||||
if (logBodyEl) logBodyEl.innerHTML = '';
|
if (logBodyEl) logBodyEl.innerHTML = '';
|
||||||
|
} catch { /* ignore */ }
|
||||||
});
|
});
|
||||||
|
|
||||||
logBodyEl.addEventListener('scroll', () => {
|
logBodyEl.addEventListener('scroll', () => {
|
||||||
|
try {
|
||||||
const el = logBodyEl!;
|
const el = logBodyEl!;
|
||||||
autoScroll = el.scrollHeight - el.scrollTop - el.clientHeight < 40;
|
autoScroll = el.scrollHeight - el.scrollTop - el.clientHeight < 40;
|
||||||
|
} catch { /* ignore */ }
|
||||||
});
|
});
|
||||||
|
} catch { /* ignore */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
function genId(): string {
|
function genId(): string {
|
||||||
@@ -68,13 +78,53 @@ function genId(): string {
|
|||||||
|
|
||||||
function formatTime(ts: number): string {
|
function formatTime(ts: number): string {
|
||||||
const d = new Date(ts);
|
const d = new Date(ts);
|
||||||
const h = String(d.getHours()).padStart(2, '0');
|
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`;
|
||||||
const m = String(d.getMinutes()).padStart(2, '0');
|
|
||||||
const s = String(d.getSeconds()).padStart(2, '0');
|
|
||||||
return `${h}:${m}:${s}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text: string): string {
|
||||||
|
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量渲染队列 — 使用 rAF 避免阻塞主线程 */
|
||||||
|
function flushPending(): void {
|
||||||
|
if (!logBodyEl || pendingEntries.length === 0) return;
|
||||||
|
renderQueued = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const frag = document.createDocumentFragment();
|
||||||
|
const toRender = pendingEntries.splice(0, pendingEntries.length);
|
||||||
|
|
||||||
|
for (const entry of toRender) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = `log-entry log-${entry.level}`;
|
||||||
|
div.id = entry.id;
|
||||||
|
|
||||||
|
let html = `<span class="log-time">${formatTime(entry.time)}</span>`;
|
||||||
|
html += `<span class="log-icon">${entry.icon}</span>`;
|
||||||
|
html += `<span class="log-msg">${escapeHtml(entry.message)}</span>`;
|
||||||
|
if (entry.detail) {
|
||||||
|
html += `<pre class="log-detail">${escapeHtml(entry.detail)}</pre>`;
|
||||||
|
}
|
||||||
|
div.innerHTML = html;
|
||||||
|
frag.appendChild(div);
|
||||||
|
}
|
||||||
|
|
||||||
|
logBodyEl.appendChild(frag);
|
||||||
|
|
||||||
|
// 清理超出的旧 DOM 节点
|
||||||
|
while (logBodyEl.children.length > MAX_LOGS) {
|
||||||
|
logBodyEl.removeChild(logBodyEl.firstChild!);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (autoScroll) {
|
||||||
|
logBodyEl.scrollTop = logBodyEl.scrollHeight;
|
||||||
|
}
|
||||||
|
} catch { /* 绝不抛出 */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 核心日志方法 — 永远不阻塞调用方 */
|
||||||
export function addLog(level: LogLevel, message: string, detail?: string, icon?: string): void {
|
export function addLog(level: LogLevel, message: string, detail?: string, icon?: string): void {
|
||||||
|
try {
|
||||||
const entry: LogEntry = {
|
const entry: LogEntry = {
|
||||||
id: genId(),
|
id: genId(),
|
||||||
time: Date.now(),
|
time: Date.now(),
|
||||||
@@ -85,96 +135,45 @@ export function addLog(level: LogLevel, message: string, detail?: string, icon?:
|
|||||||
};
|
};
|
||||||
|
|
||||||
logs.push(entry);
|
logs.push(entry);
|
||||||
if (logs.length > MAX_LOGS) {
|
if (logs.length > MAX_LOGS) logs.splice(0, logs.length - MAX_LOGS);
|
||||||
const removed = logs.splice(0, logs.length - MAX_LOGS);
|
|
||||||
for (const r of removed) {
|
pendingEntries.push(entry);
|
||||||
const el = document.getElementById(r.id);
|
if (!renderQueued) {
|
||||||
if (el) el.remove();
|
renderQueued = true;
|
||||||
|
requestAnimationFrame(flushPending);
|
||||||
}
|
}
|
||||||
}
|
} catch { /* 绝不抛出 */ }
|
||||||
|
|
||||||
appendLogDOM(entry);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendLogDOM(entry: LogEntry): void {
|
// ── 便捷方法(全部安全包装)──
|
||||||
if (!logBodyEl) return;
|
|
||||||
|
|
||||||
const div = document.createElement('div');
|
export function logInfo(msg: string, detail?: string): void { addLog('info', msg, detail); }
|
||||||
div.className = `log-entry log-${entry.level}`;
|
export function logSuccess(msg: string, detail?: string): void { addLog('success', msg, detail); }
|
||||||
div.id = entry.id;
|
export function logWarn(msg: string, detail?: string): void { addLog('warn', msg, detail); }
|
||||||
|
export function logError(msg: string, detail?: string): void { addLog('error', msg, detail); }
|
||||||
let html = `<span class="log-time">${formatTime(entry.time)}</span>`;
|
export function logDebug(msg: string, detail?: string): void { addLog('debug', msg, detail); }
|
||||||
html += `<span class="log-icon">${entry.icon}</span>`;
|
|
||||||
html += `<span class="log-msg">${escapeHtml(entry.message)}</span>`;
|
|
||||||
|
|
||||||
if (entry.detail) {
|
|
||||||
html += `<pre class="log-detail">${escapeHtml(entry.detail)}</pre>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.innerHTML = html;
|
|
||||||
logBodyEl.appendChild(div);
|
|
||||||
|
|
||||||
if (autoScroll) {
|
|
||||||
logBodyEl.scrollTop = logBodyEl.scrollHeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeHtml(text: string): string {
|
|
||||||
return text
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 便捷方法 ──
|
|
||||||
|
|
||||||
export function logInfo(msg: string, detail?: string): void {
|
|
||||||
addLog('info', msg, detail);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function logSuccess(msg: string, detail?: string): void {
|
|
||||||
addLog('success', msg, detail);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function logWarn(msg: string, detail?: string): void {
|
|
||||||
addLog('warn', msg, detail);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function logError(msg: string, detail?: string): void {
|
|
||||||
addLog('error', msg, detail);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function logDebug(msg: string, detail?: string): void {
|
|
||||||
addLog('debug', msg, detail);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function logToolStart(name: string, args: string): void {
|
export function logToolStart(name: string, args: string): void {
|
||||||
addLog('tool', `工具调用: ${name}`, args);
|
addLog('tool', `▶ ${name}`, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logToolResult(name: string, success: boolean, summary?: string): void {
|
export function logToolResult(name: string, success: boolean, summary?: string): void {
|
||||||
addLog(
|
addLog(success ? 'success' : 'error', `◀ ${name} ${success ? '✓' : '✗'}`, summary, success ? '✅' : '❌');
|
||||||
success ? 'success' : 'error',
|
|
||||||
`工具完成: ${name}`,
|
|
||||||
summary,
|
|
||||||
success ? '✅' : '❌'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logThink(thinking: string): void {
|
export function logThink(thinking: string): void {
|
||||||
const preview = thinking.length > 200 ? thinking.slice(-200) + '...' : thinking;
|
const preview = thinking.length > 200 ? thinking.slice(-200) + '...' : thinking;
|
||||||
addLog('think', '模型思考中', preview);
|
addLog('think', '思考中', preview);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logStream(msg: string): void {
|
export function logStream(msg: string): void { addLog('stream', msg); }
|
||||||
addLog('stream', msg);
|
export function logAgentLoop(iteration: number, maxLoops: number): void { addLog('info', `Loop #${iteration}/${maxLoops}`); }
|
||||||
}
|
export function logModelResponse(contentLen: number, toolCalls: number): void { addLog('stream', `响应: ${contentLen}字, ${toolCalls}工具`); }
|
||||||
|
export function logConnection(status: string, detail?: string): void { addLog(status === 'connected' ? 'success' : 'warn', `连接: ${status}`, detail); }
|
||||||
export function logAgentLoop(iteration: number, maxLoops: number): void {
|
export function logModel(name: string, action: string): void { addLog('info', `模型: ${action}`, name); }
|
||||||
addLog('info', `Agent Loop #${iteration}/${maxLoops}`);
|
export function logSession(action: string, detail?: string): void { addLog('info', `会话: ${action}`, detail); }
|
||||||
}
|
export function logMemory(action: string, detail?: string): void { addLog('debug', `记忆: ${action}`, detail); }
|
||||||
|
export function logRAG(action: string, detail?: string): void { addLog('info', `RAG: ${action}`, detail); }
|
||||||
export function logModelResponse(contentLen: number, toolCalls: number): void {
|
export function logSetting(key: string, value: string): void { addLog('debug', `设置: ${key}`, value); }
|
||||||
addLog('stream', `模型响应: ${contentLen} 字符, ${toolCalls} 个工具调用`);
|
export function logIPC(action: string, detail?: string): void { addLog('debug', `IPC: ${action}`, detail); }
|
||||||
}
|
export function logInit(msg: string): void { addLog('info', `初始化: ${msg}`); }
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
import { state, KEYS } from '../state/state.js';
|
import { state, KEYS } from '../state/state.js';
|
||||||
import { generateId } from '../utils/utils.js';
|
import { generateId } from '../utils/utils.js';
|
||||||
|
import { logMemory, logDebug, logWarn } from './log-service.js';
|
||||||
import type { MemoryEntry, MemorySearchResult, MemoryExtractionResult, ChatDB, OllamaAPI } from '../types.js';
|
import type { MemoryEntry, MemorySearchResult, MemoryExtractionResult, ChatDB, OllamaAPI } from '../types.js';
|
||||||
|
|
||||||
const TYPE_ICONS: Record<string, string> = {
|
const TYPE_ICONS: Record<string, string> = {
|
||||||
@@ -41,6 +42,7 @@ export async function initMemoryManager(): Promise<void> {
|
|||||||
state.set('memoryEntries', memoryCache);
|
state.set('memoryEntries', memoryCache);
|
||||||
|
|
||||||
console.log(`[Memory] 初始化完成,加载 ${memoryCache.length} 条记忆`);
|
console.log(`[Memory] 初始化完成,加载 ${memoryCache.length} 条记忆`);
|
||||||
|
logMemory(`初始化完成, 加载 ${memoryCache.length} 条`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isMemoryEnabled(): boolean {
|
export function isMemoryEnabled(): boolean {
|
||||||
@@ -152,6 +154,7 @@ export async function addMemory(data: {
|
|||||||
state.set('memoryEntries', [...memoryCache]);
|
state.set('memoryEntries', [...memoryCache]);
|
||||||
|
|
||||||
if (db) await db.saveMemory(entry);
|
if (db) await db.saveMemory(entry);
|
||||||
|
logMemory(`新增: ${data.type}`, entry.content.slice(0, 60));
|
||||||
|
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
@@ -177,6 +180,7 @@ export async function deleteMemory(id: string): Promise<void> {
|
|||||||
|
|
||||||
const db = state.get<ChatDB | null>(KEYS.DB);
|
const db = state.get<ChatDB | null>(KEYS.DB);
|
||||||
if (db) await db.deleteMemory(id);
|
if (db) await db.deleteMemory(id);
|
||||||
|
logMemory(`删除`, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 清空所有记忆 ──
|
// ── 清空所有记忆 ──
|
||||||
@@ -316,9 +320,11 @@ ${conversationText.slice(0, 4000)}
|
|||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (count > 0) logMemory(`自动提取 ${count} 条`);
|
||||||
return count;
|
return count;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('[Memory] 自动提取失败:', err);
|
console.warn('[Memory] 自动提取失败:', err);
|
||||||
|
logWarn('记忆自动提取失败', (err as Error).message);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import { state, KEYS } from '../state/state.js';
|
import { state, KEYS } from '../state/state.js';
|
||||||
import { VectorStore } from './vector-store.js';
|
import { VectorStore } from './vector-store.js';
|
||||||
import { chunkText, createChunkMetadata } from './document-processor.js';
|
import { chunkText, createChunkMetadata } from './document-processor.js';
|
||||||
|
import { logRAG, logDebug, logError } from './log-service.js';
|
||||||
import type { OllamaAPI, SearchResult, VectorItem } from '../types.js';
|
import type { OllamaAPI, SearchResult, VectorItem } from '../types.js';
|
||||||
|
|
||||||
let vectorStore: VectorStore | null = null;
|
let vectorStore: VectorStore | null = null;
|
||||||
@@ -50,6 +51,7 @@ export async function addDocumentToKB(
|
|||||||
|
|
||||||
const textChunks = chunkText(content);
|
const textChunks = chunkText(content);
|
||||||
if (textChunks.length === 0) throw new Error('文档内容为空,无法处理');
|
if (textChunks.length === 0) throw new Error('文档内容为空,无法处理');
|
||||||
|
logRAG(`分块完成: ${filename}`, `${textChunks.length} 个分块`);
|
||||||
|
|
||||||
const docId = `doc_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
|
const docId = `doc_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
|
||||||
|
|
||||||
@@ -120,6 +122,7 @@ export async function retrieveContext(query: string, colId: string, topK = 5): P
|
|||||||
const queryEmbedding = await embedText(query, embedModel);
|
const queryEmbedding = await embedText(query, embedModel);
|
||||||
const results = await vs.search(colId, queryEmbedding, topK);
|
const results = await vs.search(colId, queryEmbedding, topK);
|
||||||
if (results.length === 0) return { context: '', results: [] };
|
if (results.length === 0) return { context: '', results: [] };
|
||||||
|
logRAG(`检索完成`, `${results.length} 个相关片段`);
|
||||||
|
|
||||||
const contextParts = results.map((r, i) =>
|
const contextParts = results.map((r, i) =>
|
||||||
`[来源 ${i + 1}: ${r.filename}]\n${r.text}`
|
`[来源 ${i + 1}: ${r.filename}]\n${r.text}`
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ToolDefinition, ToolResult } from '../types.js';
|
import type { ToolDefinition, ToolResult } from '../types.js';
|
||||||
|
import { logToolResult, logError } from './log-service.js';
|
||||||
|
|
||||||
export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
||||||
{
|
{
|
||||||
@@ -147,11 +148,13 @@ export function getEnabledToolDefinitions(): ToolDefinition[] {
|
|||||||
|
|
||||||
export async function executeTool(toolName: string, args: Record<string, unknown>): Promise<ToolResult> {
|
export async function executeTool(toolName: string, args: Record<string, unknown>): Promise<ToolResult> {
|
||||||
if (!isToolEnabled(toolName)) {
|
if (!isToolEnabled(toolName)) {
|
||||||
|
logError(`工具未启用: ${toolName}`);
|
||||||
return { success: false, error: `工具 ${toolName} 未启用` };
|
return { success: false, error: `工具 ${toolName} 未启用` };
|
||||||
}
|
}
|
||||||
|
|
||||||
const bridge = window.metonaDesktop;
|
const bridge = window.metonaDesktop;
|
||||||
if (!bridge?.isDesktop) {
|
if (!bridge?.isDesktop) {
|
||||||
|
logError(`工具调用仅支持桌面版: ${toolName}`);
|
||||||
return { success: false, error: '工具调用仅支持桌面版' };
|
return { success: false, error: '工具调用仅支持桌面版' };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,6 +162,7 @@ export async function executeTool(toolName: string, args: Record<string, unknown
|
|||||||
const result = await bridge.tool.execute(toolName, args);
|
const result = await bridge.tool.execute(toolName, args);
|
||||||
return result;
|
return result;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
logError(`工具异常: ${toolName}`, (err as Error).message);
|
||||||
return { success: false, error: (err as Error).message };
|
return { success: false, error: (err as Error).message };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user