feat: LLM 调用日志 + ModelSelector 优化 + devtools.bat 编码修复
- 新增 call_log.go: 全局环形缓冲区记录每次 LLM 调用(模型/Token/耗时/错误) - OpenAIProvider.doChat/ChatStreamWithTools 自动记录调用日志 - ai-core 暴露 GET /api/v1/llm-calls 端点, DevTools 代理 + UI 面板 - ModelSelector.envProvider 改为单例缓存, 避免重复创建 HTTP Client - 新增 PurposeToolCalling 适配器, 后台思考工具调用走专用路由 - envFallback 超时 120s→180s, 显式设置 MaxRetries - devtools.bat 全英文, 解决 Windows CMD GBK 编码乱码问题 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -696,6 +696,9 @@ input[type="range"] { accent-color: var(--accent); padding: 0; }
|
||||
<button class="nav-item" data-panel="modelConfig">
|
||||
<span class="nav-icon">🤖</span><span class="nav-label">模型配置</span>
|
||||
</button>
|
||||
<button class="nav-item" data-panel="llmCalls">
|
||||
<span class="nav-icon">📊</span><span class="nav-label">LLM 调用</span>
|
||||
</button>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<span id="ws-dot" class="disconnected"></span>
|
||||
@@ -737,6 +740,7 @@ input[type="range"] { accent-color: var(--accent); padding: 0; }
|
||||
<!-- 客户端管理 -->
|
||||
<div class="panel" id="panel-clients"></div>
|
||||
<div class="panel" id="panel-modelConfig"></div>
|
||||
<div class="panel" id="panel-llmCalls"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -801,7 +805,8 @@ const STATE = {
|
||||
modelConfigModels: [],
|
||||
modelConfigRouting: [],
|
||||
fetchedModels: [],
|
||||
expandedThinkingLogs: {},
|
||||
llmCallsData: [],
|
||||
expandedThinkingLogs: {},
|
||||
};
|
||||
|
||||
// ========== WebSocket ==========
|
||||
@@ -1015,6 +1020,7 @@ function switchPanel(name) {
|
||||
chatPlatforms: '💬 第三方聊天配置与消息日志',
|
||||
clients: '📱 客户端管理',
|
||||
modelConfig: '🤖 模型配置管理',
|
||||
llmCalls: '📊 LLM 调用日志',
|
||||
};
|
||||
document.getElementById('panel-title').textContent = titles[name] || name;
|
||||
|
||||
@@ -1041,6 +1047,7 @@ function switchPanel(name) {
|
||||
case 'chatPlatforms': renderChatPlatformsPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); startChatAutoRefresh(); break;
|
||||
case 'clients': renderClientsPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
|
||||
case 'modelConfig': renderModelConfigPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
|
||||
case 'llmCalls': renderLlmCallsPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4463,6 +4470,63 @@ async function editClientNote(clientID) {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== LLM Calls Panel ==========
|
||||
async function renderLlmCallsPanel() {
|
||||
var panel = document.getElementById('panel-llmCalls');
|
||||
panel.innerHTML = '<div class="loading">🔄 加载中...</div>';
|
||||
|
||||
try {
|
||||
var resp = await api('/api/llm-calls?limit=100');
|
||||
var calls = resp.calls || [];
|
||||
STATE.llmCallsData = calls;
|
||||
|
||||
if (calls.length === 0) {
|
||||
panel.innerHTML = '<div class="empty-state">暂无 LLM 调用记录<br><small>发送一条消息后刷新查看</small></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
var totalTokens = calls.reduce(function(s, c) { return s + (c.total_tokens || 0); }, 0);
|
||||
var successCount = calls.filter(function(c) { return c.success; }).length;
|
||||
|
||||
var html = '<div class="stats-row" style="margin-bottom:16px">' +
|
||||
'<div class="stat-card"><div class="stat-value">' + calls.length + '</div><div class="stat-label">总调用</div></div>' +
|
||||
'<div class="stat-card"><div class="stat-value">' + successCount + '/' + calls.length + '</div><div class="stat-label">成功</div></div>' +
|
||||
'<div class="stat-card"><div class="stat-value">' + formatTokens(totalTokens) + '</div><div class="stat-label">总 Token</div></div>' +
|
||||
'</div>';
|
||||
|
||||
html += '<div class="table-wrap"><table class="data-table">' +
|
||||
'<thead><tr>' +
|
||||
'<th>时间</th><th>模型</th><th>耗时</th><th>Prompt</th><th>Completion</th><th>Total</th><th>状态</th>' +
|
||||
'</tr></thead><tbody>';
|
||||
|
||||
calls.forEach(function(c) {
|
||||
var statusClass = c.success ? 'status-ok' : 'status-err';
|
||||
var statusText = c.success ? '✓' : '✗ ' + escHtml(c.error || '');
|
||||
var durMs = (c.duration_ms || 0) / 1000000;
|
||||
html += '<tr>' +
|
||||
'<td style="font-size:11px;white-space:nowrap">' + formatTime(c.time) + '</td>' +
|
||||
'<td style="font-size:12px;font-family:monospace">' + escHtml(c.model) + '</td>' +
|
||||
'<td style="font-size:11px">' + (durMs > 0 ? (durMs / 1000).toFixed(2) + 's' : '-') + '</td>' +
|
||||
'<td style="font-size:11px">' + (c.prompt_tokens || 0).toLocaleString() + '</td>' +
|
||||
'<td style="font-size:11px">' + (c.completion_tokens || 0).toLocaleString() + '</td>' +
|
||||
'<td style="font-size:11px;font-weight:600">' + (c.total_tokens || 0).toLocaleString() + '</td>' +
|
||||
'<td><span class="' + statusClass + '">' + statusText + '</span></td>' +
|
||||
'</tr>';
|
||||
});
|
||||
|
||||
html += '</tbody></table></div>';
|
||||
panel.innerHTML = html;
|
||||
} catch (err) {
|
||||
panel.innerHTML = '<div class="error-state">加载失败: ' + escHtml(err.message) + '<br><small>确认 AI-Core 服务已启动</small></div>';
|
||||
}
|
||||
}
|
||||
|
||||
function formatTokens(n) {
|
||||
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
|
||||
if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
|
||||
return String(n);
|
||||
}
|
||||
|
||||
</script>
|
||||
<script src="iot-panel.js"></script>
|
||||
<script>
|
||||
@@ -4471,7 +4535,7 @@ async function editClientNote(clientID) {
|
||||
// Listen for browser back/forward navigation.
|
||||
window.addEventListener('hashchange', function() {
|
||||
var hash = location.hash.replace('#', '');
|
||||
var validPanels = ['dashboard', 'memory', 'sessions', 'services', 'iot', 'performance', 'database', 'toolCalls', 'stt', 'thinking', 'timeline', 'chatPlatforms', 'clients', 'modelConfig'];
|
||||
var validPanels = ['dashboard', 'memory', 'sessions', 'services', 'iot', 'performance', 'database', 'toolCalls', 'stt', 'thinking', 'timeline', 'chatPlatforms', 'clients', 'modelConfig', 'llmCalls'];
|
||||
if (hash && validPanels.indexOf(hash) >= 0 && hash !== STATE.activePanel) {
|
||||
switchPanel(hash);
|
||||
}
|
||||
@@ -4482,7 +4546,7 @@ refreshStatus();
|
||||
|
||||
// Restore last panel from URL hash, or default to dashboard.
|
||||
var initHash = location.hash.replace('#', '');
|
||||
var validPanels = ['dashboard', 'memory', 'sessions', 'services', 'iot', 'performance', 'database', 'toolCalls', 'stt', 'thinking', 'timeline', 'chatPlatforms', 'clients', 'modelConfig'];
|
||||
var validPanels = ['dashboard', 'memory', 'sessions', 'services', 'iot', 'performance', 'database', 'toolCalls', 'stt', 'thinking', 'timeline', 'chatPlatforms', 'clients', 'modelConfig', 'llmCalls'];
|
||||
if (initHash && validPanels.indexOf(initHash) >= 0) {
|
||||
switchPanel(initHash);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user