feat(engine): 添加MCP连接管理和工具集成功能
- 集成MCP连接管理器,支持MCP服务器连接 - 添加多种内置工具:ClarifyTool、CronTool、DelegateTool、ExecuteCodeTool、 PatchFileTool、ProcessTool、SendMessageTool、SpawnTool、TerminalTool、 TodoTool、WebFetchTool、WebSearchTool、WriteFileTool等 - 实现工具注册和装配功能 - 添加技能选择上下文参数 - 支持思考模式控制参数thinking_enabled feat(coordinator): 重构任务执行计划器参数命名 - 将learning_candidate_enabled重命名为allow_candidate_generation - 更新TeamGraphScheduler中的参数传递 - 修改LocalAgentRunner中的相关参数处理 - 更新README文档中的相应描述 refactor(context): 标准化工具调用参数格式 - 添加_json导入用于参数序列化 - 实现_provider_tool_calls方法标准化OpenAI兼容的工具调用载荷 - 修复工具调用中参数非字符串类型的序列化问题 refactor(session): 优化消息历史记录过滤逻辑 - 修改get_messages_as_conversation为基于运行状态过滤消息 - 排除未完成、失败或错误结束的运行记录 - 改进对话历史的可见性控制机制 fix(store): 修复FTS索引重建逻辑 - 添加异常处理防止FTS索引创建失败 - 实现_rebuild_fts_index方法重新构建全文搜索索引 - 优化索引触发器和表的维护流程
This commit is contained in:
205
app-instance/frontend/app/(app)/logs/page.tsx
Normal file
205
app-instance/frontend/app/(app)/logs/page.tsx
Normal file
@ -0,0 +1,205 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { AlertCircle, Bot, Braces, ChevronDown, Loader2, MessageSquare, RefreshCw, TerminalSquare } from 'lucide-react';
|
||||
|
||||
import { getChatLogs } from '@/lib/api';
|
||||
import { pickAppText } from '@/lib/i18n/core';
|
||||
import { useAppI18n } from '@/lib/i18n/provider';
|
||||
import type { ChatLogEvent, ChatLogSession } from '@/types';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
|
||||
function eventLabel(event: ChatLogEvent): string {
|
||||
return event.event_type || event.role || 'event';
|
||||
}
|
||||
|
||||
function eventIcon(event: ChatLogEvent) {
|
||||
if (event.event_type === 'llm_request_snapshotted') return Braces;
|
||||
if (event.role === 'assistant') return Bot;
|
||||
if (event.role === 'tool') return TerminalSquare;
|
||||
return MessageSquare;
|
||||
}
|
||||
|
||||
function formatPayload(value: unknown): string {
|
||||
if (value === null || value === undefined || value === '') return '';
|
||||
if (typeof value === 'string') return value;
|
||||
try {
|
||||
return JSON.stringify(value, null, 2);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
function eventBody(event: ChatLogEvent): string {
|
||||
const content = event.content?.trim();
|
||||
if (content) return content;
|
||||
return formatPayload(event.event_payload);
|
||||
}
|
||||
|
||||
function timestampLabel(value?: string | null): string {
|
||||
if (!value) return '';
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return value;
|
||||
return date.toLocaleString();
|
||||
}
|
||||
|
||||
export default function LogsPage() {
|
||||
const { locale } = useAppI18n();
|
||||
const [sessions, setSessions] = useState<ChatLogSession[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [expandedRuns, setExpandedRuns] = useState<Set<string>>(() => new Set());
|
||||
|
||||
const runs = useMemo(
|
||||
() =>
|
||||
sessions.flatMap((session) =>
|
||||
session.runs.map((run) => ({
|
||||
...run,
|
||||
sessionTitle: session.title || session.session_id,
|
||||
}))
|
||||
),
|
||||
[sessions]
|
||||
);
|
||||
|
||||
const loadLogs = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await getChatLogs(80);
|
||||
setSessions(data.sessions || []);
|
||||
const firstRun = data.sessions?.[0]?.runs?.[0]?.run_id;
|
||||
if (firstRun) {
|
||||
setExpandedRuns((current) => (current.size ? current : new Set([firstRun])));
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.message || pickAppText(locale, '读取日志失败', 'Failed to load logs'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadLogs();
|
||||
}, []);
|
||||
|
||||
const toggleRun = (runId: string) => {
|
||||
setExpandedRuns((current) => {
|
||||
const next = new Set(current);
|
||||
if (next.has(runId)) next.delete(runId);
|
||||
else next.add(runId);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-7xl p-6 space-y-6">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">{pickAppText(locale, '运行日志', 'Runtime Logs')}</h1>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{pickAppText(
|
||||
locale,
|
||||
'按每一次用户输入分组,查看 LLM 请求、回复、工具结果和隐藏运行快照。',
|
||||
'Grouped by each user input, with LLM requests, responses, tool results, and hidden runtime snapshots.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={loadLogs} variant="outline" size="sm" disabled={loading}>
|
||||
{loading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <RefreshCw className="mr-2 h-4 w-4" />}
|
||||
{pickAppText(locale, '刷新', 'Refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
<Card className="border-destructive">
|
||||
<CardContent className="flex items-start gap-3 pt-6 text-destructive">
|
||||
<AlertCircle className="mt-0.5 h-5 w-5" />
|
||||
<div>
|
||||
<p className="text-sm font-medium">{pickAppText(locale, '无法读取日志', 'Unable to load logs')}</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">{error}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
{loading && !runs.length ? (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!loading && !runs.length && !error ? (
|
||||
<Card>
|
||||
<CardContent className="py-12 text-center text-sm text-muted-foreground">
|
||||
{pickAppText(locale, '还没有可展示的运行日志。', 'No runtime logs are available yet.')}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
<div className="space-y-3">
|
||||
{runs.map((run) => {
|
||||
const expanded = expandedRuns.has(run.run_id);
|
||||
return (
|
||||
<Card key={`${run.session_id}:${run.run_id}`} className="overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleRun(run.run_id)}
|
||||
className="flex w-full items-start justify-between gap-4 border-b px-5 py-4 text-left transition-colors hover:bg-muted/40"
|
||||
>
|
||||
<span className="min-w-0 space-y-2">
|
||||
<span className="flex flex-wrap items-center gap-2">
|
||||
<Badge variant={run.task_mode ? 'default' : 'secondary'}>
|
||||
{run.task_mode ? 'Task' : 'Chat'}
|
||||
</Badge>
|
||||
{run.source ? <Badge variant="outline">{run.source}</Badge> : null}
|
||||
{run.attempt_index ? <Badge variant="outline">attempt {run.attempt_index}</Badge> : null}
|
||||
<span className="text-xs text-muted-foreground">{timestampLabel(run.started_at)}</span>
|
||||
</span>
|
||||
<span className="block truncate text-sm font-semibold text-foreground">
|
||||
{run.user_input || run.title || run.run_id}
|
||||
</span>
|
||||
<span className="block truncate text-xs text-muted-foreground">
|
||||
{run.sessionTitle} · {run.run_id}
|
||||
</span>
|
||||
</span>
|
||||
<ChevronDown className={`mt-1 h-4 w-4 shrink-0 text-muted-foreground transition-transform ${expanded ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
|
||||
{expanded ? (
|
||||
<CardContent className="space-y-3 p-5">
|
||||
{run.events.map((event, index) => {
|
||||
const Icon = eventIcon(event);
|
||||
const body = eventBody(event);
|
||||
return (
|
||||
<div
|
||||
key={`${event.message_id ?? index}:${event.event_type}`}
|
||||
className="rounded-lg border border-border bg-background"
|
||||
>
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 border-b px-3 py-2">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<Icon className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<span className="truncate text-sm font-medium">{eventLabel(event)}</span>
|
||||
<Badge variant={event.context_visible ? 'secondary' : 'outline'}>
|
||||
{event.context_visible ? 'visible' : 'hidden'}
|
||||
</Badge>
|
||||
{event.tool_name ? <Badge variant="outline">{event.tool_name}</Badge> : null}
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">{timestampLabel(event.timestamp)}</span>
|
||||
</div>
|
||||
<pre className="max-h-[520px] overflow-auto whitespace-pre-wrap break-words p-3 text-xs leading-5 text-foreground">
|
||||
{body || formatPayload(event)}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</CardContent>
|
||||
) : null}
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user