feat: 添加左侧实时执行日志面板
功能: - 日志面板位于聊天区左侧,可折叠/展开 - 实时展示所有执行事件: - 🤔 模型思考过程 - 📡 流式响应状态 - 🔧 工具调用(参数+结果) - ⚠️ 错误和警告 - ℹ️ Agent Loop 迭代 - ✅ 操作完成 - 日志自动滚动、最大 500 条、可清空 - 等宽字体显示,按级别着色 - 头部新增日志开关按钮
This commit is contained in:
@@ -16,6 +16,7 @@ import { OllamaAPI } from '../api/ollama.js';
|
|||||||
import { runAgentLoop } from '../services/agent-engine.js';
|
import { runAgentLoop } from '../services/agent-engine.js';
|
||||||
import { searchMemories, buildMemoryContext, markMemoryUsed, extractMemoriesFromConversation, isMemoryEnabled } from '../services/memory-manager.js';
|
import { searchMemories, buildMemoryContext, markMemoryUsed, extractMemoriesFromConversation, isMemoryEnabled } from '../services/memory-manager.js';
|
||||||
import { showToolConfirm } from './tool-confirm-modal.js';
|
import { showToolConfirm } from './tool-confirm-modal.js';
|
||||||
|
import { logInfo, logStream, logError, logSuccess, logWarn } from '../services/log-service.js';
|
||||||
import type { ChatSession, ChatMessage, OllamaStreamChunk, RagSource, FileContent, ChatFile, ToolCallRecord } from '../types.js';
|
import type { ChatSession, ChatMessage, OllamaStreamChunk, RagSource, FileContent, ChatFile, ToolCallRecord } from '../types.js';
|
||||||
|
|
||||||
let chatInputEl: HTMLTextAreaElement;
|
let chatInputEl: HTMLTextAreaElement;
|
||||||
@@ -249,6 +250,8 @@ export async function sendMessage(): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logInfo(`发送消息: ${model}`, text ? text.slice(0, 60) + (text.length > 60 ? '...' : '') : '[附件消息]');
|
||||||
|
|
||||||
if (pendingImages.length > 0 && !isVisionAvailable()) {
|
if (pendingImages.length > 0 && !isVisionAvailable()) {
|
||||||
appendSystemMessage('⚠️ 当前模型不支持图片分析,请移除图片后重试');
|
appendSystemMessage('⚠️ 当前模型不支持图片分析,请移除图片后重试');
|
||||||
return;
|
return;
|
||||||
@@ -441,8 +444,11 @@ export async function sendMessage(): Promise<void> {
|
|||||||
|
|
||||||
if (!assistantContent && !thinkContent) {
|
if (!assistantContent && !thinkContent) {
|
||||||
appendSystemMessage('⚠️ 模型未返回任何内容,可能是上下文过长或模型不支持当前请求');
|
appendSystemMessage('⚠️ 模型未返回任何内容,可能是上下文过长或模型不支持当前请求');
|
||||||
|
logWarn('模型未返回任何内容');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logStream(`流式完成: ${assistantContent.length} 字符${finalStats?.eval_count ? `, ${finalStats.eval_count} tokens` : ''}`);
|
||||||
|
|
||||||
const assistantMsg: ChatMessage = {
|
const assistantMsg: ChatMessage = {
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: assistantContent || '',
|
content: assistantContent || '',
|
||||||
@@ -555,10 +561,15 @@ export async function sendMessage(): Promise<void> {
|
|||||||
let errMsg = `❌ 错误: ${(err as Error).message}`;
|
let errMsg = `❌ 错误: ${(err as Error).message}`;
|
||||||
if ((err as Error).message.includes('Failed to fetch') || (err as Error).message.includes('NetworkError')) {
|
if ((err as Error).message.includes('Failed to fetch') || (err as Error).message.includes('NetworkError')) {
|
||||||
errMsg = '❌ 连接失败。请检查:\n1. Ollama 是否正在运行\n2. 地址是否正确\n3. 是否设置了 OLLAMA_ORIGINS="*"';
|
errMsg = '❌ 连接失败。请检查:\n1. Ollama 是否正在运行\n2. 地址是否正确\n3. 是否设置了 OLLAMA_ORIGINS="*"';
|
||||||
|
logError('连接失败', 'Ollama 服务不可达');
|
||||||
} else if ((err as Error).message.includes('400')) {
|
} else if ((err as Error).message.includes('400')) {
|
||||||
errMsg = '❌ 请求参数错误,可能是知识库内容超出模型上下文限制';
|
errMsg = '❌ 请求参数错误,可能是知识库内容超出模型上下文限制';
|
||||||
|
logError('请求参数错误 (400)', (err as Error).message);
|
||||||
} else if ((err as Error).message.includes('500')) {
|
} else if ((err as Error).message.includes('500')) {
|
||||||
errMsg = '❌ Ollama 服务器错误,请检查 Ollama 日志';
|
errMsg = '❌ Ollama 服务器错误,请检查 Ollama 日志';
|
||||||
|
logError('Ollama 服务器错误 (500)', (err as Error).message);
|
||||||
|
} else {
|
||||||
|
logError('对话错误', (err as Error).message);
|
||||||
}
|
}
|
||||||
appendSystemMessage(errMsg);
|
appendSystemMessage(errMsg);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -691,6 +702,7 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio
|
|||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[AgentLoop] 错误:', err);
|
console.error('[AgentLoop] 错误:', err);
|
||||||
|
logError('Agent Loop 错误', (err as Error).message);
|
||||||
|
|
||||||
if ((err as Error).name === 'AbortError') {
|
if ((err as Error).name === 'AbortError') {
|
||||||
const placeholder = document.querySelector('#messagesContainer .message.assistant.loading') as HTMLElement;
|
const placeholder = document.querySelector('#messagesContainer .message.assistant.loading') as HTMLElement;
|
||||||
|
|||||||
+32
-1
@@ -11,9 +11,40 @@
|
|||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
|
|
||||||
|
<!-- ═══════════════ 日志面板(左侧)═══════════════ -->
|
||||||
|
<aside class="log-panel" id="logPanel" style="display:none;">
|
||||||
|
<div class="log-panel-header">
|
||||||
|
<span class="log-panel-title">📋 执行日志</span>
|
||||||
|
<div class="log-panel-actions">
|
||||||
|
<button class="icon-btn log-btn" id="btnClearLog" title="清空日志">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="icon-btn log-btn" id="btnCloseLog" title="关闭日志">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="log-panel-body" id="logPanelBody"></div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- ═══════════════ 主内容区 ═══════════════ -->
|
||||||
|
<div class="main-wrap" id="mainWrap">
|
||||||
|
|
||||||
<!-- ═══════════════ 顶部导航 ═══════════════ -->
|
<!-- ═══════════════ 顶部导航 ═══════════════ -->
|
||||||
<header class="app-header">
|
<header class="app-header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
|
<button class="icon-btn" id="btnToggleLog" title="执行日志">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||||
|
<polyline points="14 2 14 8 20 8"/>
|
||||||
|
<line x1="16" y1="13" x2="8" y2="13"/>
|
||||||
|
<line x1="16" y1="17" x2="8" y2="17"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
<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">v3.0.0</span>
|
<span class="app-version">v3.0.0</span>
|
||||||
@@ -149,7 +180,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div><!-- /.main-wrap -->
|
||||||
|
|
||||||
<!-- ═══════════════ 设置模态框 ═══════════════ -->
|
<!-- ═══════════════ 设置模态框 ═══════════════ -->
|
||||||
<div class="modal-overlay" id="settingsModal" style="display:none;">
|
<div class="modal-overlay" id="settingsModal" style="display:none;">
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { initMemoryManager } from './services/memory-manager.js';
|
|||||||
import { setToolEnabled } from './services/tool-registry.js';
|
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 type { ChatSession } from './types.js';
|
import type { ChatSession } from './types.js';
|
||||||
|
|
||||||
function initHelpModal(): void {
|
function initHelpModal(): void {
|
||||||
@@ -157,6 +158,7 @@ async function init(): Promise<void> {
|
|||||||
initHelpModal();
|
initHelpModal();
|
||||||
initToolConfirmModal();
|
initToolConfirmModal();
|
||||||
initMemoryPanel();
|
initMemoryPanel();
|
||||||
|
initLogPanel();
|
||||||
setupDesktopIntegration();
|
setupDesktopIntegration();
|
||||||
|
|
||||||
bindGlobalEvents();
|
bindGlobalEvents();
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
} from './tool-registry.js';
|
} from './tool-registry.js';
|
||||||
import { searchMemories, buildMemoryContext, markMemoryUsed, isMemoryEnabled } from './memory-manager.js';
|
import { searchMemories, buildMemoryContext, markMemoryUsed, isMemoryEnabled } from './memory-manager.js';
|
||||||
import { showToast } from '../components/toast.js';
|
import { showToast } from '../components/toast.js';
|
||||||
|
import { logInfo, logWarn, logError, logToolStart, logToolResult, logThink, logStream, logAgentLoop, logModelResponse } from './log-service.js';
|
||||||
import type {
|
import type {
|
||||||
OllamaMessage,
|
OllamaMessage,
|
||||||
OllamaStreamChunk,
|
OllamaStreamChunk,
|
||||||
@@ -53,6 +54,7 @@ export async function runAgentLoop(
|
|||||||
const modelSupportsTools = state.get<boolean>('modelSupportsTools', false);
|
const modelSupportsTools = state.get<boolean>('modelSupportsTools', false);
|
||||||
if (!modelSupportsTools) {
|
if (!modelSupportsTools) {
|
||||||
console.warn(`[AgentEngine] 模型 ${model} 未声明 tools 能力,Agent Loop 可能不稳定`);
|
console.warn(`[AgentEngine] 模型 ${model} 未声明 tools 能力,Agent Loop 可能不稳定`);
|
||||||
|
logWarn(`模型 ${model} 不支持 Tool Calling`, 'Agent Loop 可能不稳定');
|
||||||
}
|
}
|
||||||
|
|
||||||
const messages: OllamaMessage[] = [];
|
const messages: OllamaMessage[] = [];
|
||||||
@@ -94,6 +96,8 @@ export async function runAgentLoop(
|
|||||||
const tools = getEnabledToolDefinitions();
|
const tools = getEnabledToolDefinitions();
|
||||||
const useTools = tools.length > 0;
|
const useTools = tools.length > 0;
|
||||||
|
|
||||||
|
logInfo(`Agent Loop 启动: ${model}`, `工具: ${useTools ? '开启' : '关闭'}, 记忆: ${isMemoryEnabled() ? '开启' : '关闭'}`);
|
||||||
|
|
||||||
let loopCount = 0;
|
let loopCount = 0;
|
||||||
const allToolRecords: ToolCallRecord[] = [];
|
const allToolRecords: ToolCallRecord[] = [];
|
||||||
const loopStartTime = Date.now();
|
const loopStartTime = Date.now();
|
||||||
@@ -105,16 +109,20 @@ export async function runAgentLoop(
|
|||||||
// 全局超时检查
|
// 全局超时检查
|
||||||
if (Date.now() - loopStartTime > MAX_LOOP_TIME) {
|
if (Date.now() - loopStartTime > MAX_LOOP_TIME) {
|
||||||
console.warn('[AgentEngine] 达到最大运行时间(5分钟),自动停止');
|
console.warn('[AgentEngine] 达到最大运行时间(5分钟),自动停止');
|
||||||
|
logWarn('Agent Loop 达到最大运行时间(5分钟),自动停止');
|
||||||
callbacks.onDone(content || '(达到最大运行时间限制)', allToolRecords);
|
callbacks.onDone(content || '(达到最大运行时间限制)', allToolRecords);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否已中止
|
// 检查是否已中止
|
||||||
if (state.get<AbortController | null>(KEYS.ABORT_CONTROLLER)?.signal.aborted) {
|
if (state.get<AbortController | null>(KEYS.ABORT_CONTROLLER)?.signal.aborted) {
|
||||||
|
logInfo('Agent Loop 已中止');
|
||||||
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined);
|
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logAgentLoop(loopCount, MAX_LOOPS);
|
||||||
|
|
||||||
let thinking = '';
|
let thinking = '';
|
||||||
content = '';
|
content = '';
|
||||||
const toolCalls: ToolCall[] = [];
|
const toolCalls: ToolCall[] = [];
|
||||||
@@ -141,6 +149,10 @@ export async function runAgentLoop(
|
|||||||
if (chunk.message?.thinking) {
|
if (chunk.message?.thinking) {
|
||||||
thinking += chunk.message.thinking;
|
thinking += chunk.message.thinking;
|
||||||
callbacks.onThinking(thinking);
|
callbacks.onThinking(thinking);
|
||||||
|
// 每收到 200 字符记录一次思考日志
|
||||||
|
if (thinking.length % 200 < 10) {
|
||||||
|
logThink(thinking);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (chunk.message?.content) {
|
if (chunk.message?.content) {
|
||||||
content += chunk.message.content;
|
content += chunk.message.content;
|
||||||
@@ -173,6 +185,7 @@ export async function runAgentLoop(
|
|||||||
]);
|
]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (abortController.signal.aborted) {
|
if (abortController.signal.aborted) {
|
||||||
|
logInfo('流式调用已中止');
|
||||||
if (content || thinking) {
|
if (content || thinking) {
|
||||||
messages.push({
|
messages.push({
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
@@ -186,6 +199,7 @@ export async function runAgentLoop(
|
|||||||
// 超时错误 — 保留已生成的内容
|
// 超时错误 — 保留已生成的内容
|
||||||
if ((err as Error).message.includes('超时')) {
|
if ((err as Error).message.includes('超时')) {
|
||||||
console.warn('[AgentEngine] 流式调用超时:', (err as Error).message);
|
console.warn('[AgentEngine] 流式调用超时:', (err as Error).message);
|
||||||
|
logError('流式调用超时(2分钟无数据)', (err as Error).message);
|
||||||
if (content || thinking) {
|
if (content || thinking) {
|
||||||
messages.push({
|
messages.push({
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
@@ -209,6 +223,8 @@ export async function runAgentLoop(
|
|||||||
}
|
}
|
||||||
messages.push(assistantMsg);
|
messages.push(assistantMsg);
|
||||||
|
|
||||||
|
logModelResponse(content.length, toolCalls.length);
|
||||||
|
|
||||||
if (toolCalls.length === 0) {
|
if (toolCalls.length === 0) {
|
||||||
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined);
|
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined);
|
||||||
return;
|
return;
|
||||||
@@ -222,6 +238,7 @@ export async function runAgentLoop(
|
|||||||
}
|
}
|
||||||
|
|
||||||
callbacks.onToolCallStart(call);
|
callbacks.onToolCallStart(call);
|
||||||
|
logToolStart(call.function.name, JSON.stringify(call.function.arguments));
|
||||||
|
|
||||||
if (needsConfirmation(call.function.name)) {
|
if (needsConfirmation(call.function.name)) {
|
||||||
const confirmed = await callbacks.onConfirmTool(call);
|
const confirmed = await callbacks.onConfirmTool(call);
|
||||||
@@ -231,6 +248,7 @@ export async function runAgentLoop(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
|
logWarn(`工具取消: ${call.function.name}`, '用户取消了操作');
|
||||||
const record: ToolCallRecord = {
|
const record: ToolCallRecord = {
|
||||||
name: call.function.name,
|
name: call.function.name,
|
||||||
arguments: call.function.arguments,
|
arguments: call.function.arguments,
|
||||||
@@ -273,9 +291,11 @@ export async function runAgentLoop(
|
|||||||
content: JSON.stringify(result)
|
content: JSON.stringify(result)
|
||||||
});
|
});
|
||||||
callbacks.onToolCallResult(call.function.name, result, call);
|
callbacks.onToolCallResult(call.function.name, result, call);
|
||||||
|
logToolResult(call.function.name, result.success, result.success ? undefined : result.error);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errorMsg = (err as Error).message;
|
const errorMsg = (err as Error).message;
|
||||||
console.error(`[AgentEngine] 工具执行失败 (${call.function.name}):`, errorMsg);
|
console.error(`[AgentEngine] 工具执行失败 (${call.function.name}):`, errorMsg);
|
||||||
|
logError(`工具执行失败: ${call.function.name}`, errorMsg);
|
||||||
const record: ToolCallRecord = {
|
const record: ToolCallRecord = {
|
||||||
name: call.function.name,
|
name: call.function.name,
|
||||||
arguments: call.function.arguments,
|
arguments: call.function.arguments,
|
||||||
@@ -295,5 +315,6 @@ export async function runAgentLoop(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logWarn('Agent Loop 达到最大工具调用次数限制');
|
||||||
callbacks.onDone(content || '(达到最大工具调用次数限制)', allToolRecords);
|
callbacks.onDone(content || '(达到最大工具调用次数限制)', allToolRecords);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
/**
|
||||||
|
* LogService - 实时日志面板服务
|
||||||
|
* 收集应用内所有事件,渲染到左侧日志面板
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type LogLevel = 'info' | 'success' | 'warn' | 'error' | 'debug' | 'tool' | 'think' | 'stream';
|
||||||
|
|
||||||
|
export interface LogEntry {
|
||||||
|
id: string;
|
||||||
|
time: number;
|
||||||
|
level: LogLevel;
|
||||||
|
icon: string;
|
||||||
|
message: string;
|
||||||
|
detail?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let logBodyEl: HTMLElement | null = null;
|
||||||
|
let logPanelEl: HTMLElement | null = null;
|
||||||
|
const MAX_LOGS = 500;
|
||||||
|
let logs: LogEntry[] = [];
|
||||||
|
let autoScroll = true;
|
||||||
|
let idCounter = 0;
|
||||||
|
|
||||||
|
const LEVEL_ICONS: Record<LogLevel, string> = {
|
||||||
|
info: 'ℹ️',
|
||||||
|
success: '✅',
|
||||||
|
warn: '⚠️',
|
||||||
|
error: '❌',
|
||||||
|
debug: '🔍',
|
||||||
|
tool: '🔧',
|
||||||
|
think: '🤔',
|
||||||
|
stream: '📡',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function initLogPanel(): void {
|
||||||
|
logBodyEl = document.querySelector('#logPanelBody')!;
|
||||||
|
logPanelEl = document.querySelector('#logPanel')!;
|
||||||
|
|
||||||
|
const btnToggle = document.querySelector('#btnToggleLog')!;
|
||||||
|
const btnClose = document.querySelector('#btnCloseLog')!;
|
||||||
|
const btnClear = document.querySelector('#btnClearLog')!;
|
||||||
|
|
||||||
|
btnToggle.addEventListener('click', () => {
|
||||||
|
const visible = logPanelEl!.style.display !== 'none';
|
||||||
|
logPanelEl!.style.display = visible ? 'none' : '';
|
||||||
|
document.querySelector('#mainWrap')!.classList.toggle('log-open', !visible);
|
||||||
|
});
|
||||||
|
|
||||||
|
btnClose.addEventListener('click', () => {
|
||||||
|
logPanelEl!.style.display = 'none';
|
||||||
|
document.querySelector('#mainWrap')!.classList.remove('log-open');
|
||||||
|
});
|
||||||
|
|
||||||
|
btnClear.addEventListener('click', () => {
|
||||||
|
logs = [];
|
||||||
|
if (logBodyEl) logBodyEl.innerHTML = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
logBodyEl.addEventListener('scroll', () => {
|
||||||
|
const el = logBodyEl!;
|
||||||
|
autoScroll = el.scrollHeight - el.scrollTop - el.clientHeight < 40;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function genId(): string {
|
||||||
|
return `log_${++idCounter}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(ts: number): string {
|
||||||
|
const d = new Date(ts);
|
||||||
|
const h = String(d.getHours()).padStart(2, '0');
|
||||||
|
const m = String(d.getMinutes()).padStart(2, '0');
|
||||||
|
const s = String(d.getSeconds()).padStart(2, '0');
|
||||||
|
return `${h}:${m}:${s}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addLog(level: LogLevel, message: string, detail?: string, icon?: string): void {
|
||||||
|
const entry: LogEntry = {
|
||||||
|
id: genId(),
|
||||||
|
time: Date.now(),
|
||||||
|
level,
|
||||||
|
icon: icon || LEVEL_ICONS[level] || '•',
|
||||||
|
message,
|
||||||
|
detail,
|
||||||
|
};
|
||||||
|
|
||||||
|
logs.push(entry);
|
||||||
|
if (logs.length > MAX_LOGS) {
|
||||||
|
const removed = logs.splice(0, logs.length - MAX_LOGS);
|
||||||
|
for (const r of removed) {
|
||||||
|
const el = document.getElementById(r.id);
|
||||||
|
if (el) el.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appendLogDOM(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendLogDOM(entry: LogEntry): void {
|
||||||
|
if (!logBodyEl) return;
|
||||||
|
|
||||||
|
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;
|
||||||
|
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 {
|
||||||
|
addLog('tool', `工具调用: ${name}`, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logToolResult(name: string, success: boolean, summary?: string): void {
|
||||||
|
addLog(
|
||||||
|
success ? 'success' : 'error',
|
||||||
|
`工具完成: ${name}`,
|
||||||
|
summary,
|
||||||
|
success ? '✅' : '❌'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logThink(thinking: string): void {
|
||||||
|
const preview = thinking.length > 200 ? thinking.slice(-200) + '...' : thinking;
|
||||||
|
addLog('think', '模型思考中', preview);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logStream(msg: string): void {
|
||||||
|
addLog('stream', msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logAgentLoop(iteration: number, maxLoops: number): void {
|
||||||
|
addLog('info', `Agent Loop #${iteration}/${maxLoops}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logModelResponse(contentLen: number, toolCalls: number): void {
|
||||||
|
addLog('stream', `模型响应: ${contentLen} 字符, ${toolCalls} 个工具调用`);
|
||||||
|
}
|
||||||
@@ -78,11 +78,132 @@ html, body {
|
|||||||
|
|
||||||
#app {
|
#app {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ═══ 日志面板 ═══ */
|
||||||
|
.log-panel {
|
||||||
|
width: 300px;
|
||||||
|
min-width: 300px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border-right: 1px solid var(--border-subtle);
|
||||||
|
z-index: 5;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-panel-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-panel-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-panel-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-btn {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-btn svg {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-panel-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 6px 0;
|
||||||
|
font-family: 'Cascadia Code', 'Consolas', 'Menlo', monospace;
|
||||||
|
font-size: 11.5px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-panel-body::-webkit-scrollbar { width: 6px; }
|
||||||
|
.log-panel-body::-webkit-scrollbar-track { background: transparent; }
|
||||||
|
.log-panel-body::-webkit-scrollbar-thumb { background: var(--border-subtle); border-radius: 3px; }
|
||||||
|
|
||||||
|
.log-entry {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: baseline;
|
||||||
|
padding: 3px 10px;
|
||||||
|
gap: 6px;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry:hover {
|
||||||
|
background: rgba(255,255,255,0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-time {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-width: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-msg {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-detail {
|
||||||
|
width: 100%;
|
||||||
|
margin: 2px 0 0;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: rgba(0,0,0,0.25);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 10.5px;
|
||||||
|
line-height: 1.4;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
max-height: 120px;
|
||||||
|
overflow-y: auto;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 日志级别颜色 */
|
||||||
|
.log-info .log-msg { color: var(--text-secondary); }
|
||||||
|
.log-success .log-msg { color: var(--success); }
|
||||||
|
.log-warn .log-msg { color: #FFB74D; }
|
||||||
|
.log-error .log-msg { color: #EF5350; }
|
||||||
|
.log-debug .log-msg { color: var(--text-muted); }
|
||||||
|
.log-tool .log-msg { color: #FFB74D; }
|
||||||
|
.log-think .log-msg { color: var(--accent); }
|
||||||
|
.log-stream .log-msg { color: var(--accent); }
|
||||||
|
|
||||||
|
/* 主内容区 */
|
||||||
|
.main-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
/* ═══ 顶部导航 ═══ */
|
/* ═══ 顶部导航 ═══ */
|
||||||
.app-header {
|
.app-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Reference in New Issue
Block a user