fix: regulatory 对话模式改用 event_stream_handler 修复工具调用截断,优化节点 prompt 和日志展示

- regulatory_node: stream_working 从 run_stream 改为 agent.run + event_stream_handler,
  解决工具调用后文本被截断的问题;添加 PartStartEvent 处理修复首字丢失
- consciousness_node: prompt 重写为三模式(生成/执行/报告),强调禁止编造 agent_id
- workflow API: _merge_runtime_status 暴露步骤输出内容(workflow_log 第三元素)
- 前端日志: 系统日志改为终端滚动样式,工作流步骤可展开查看输出

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-05 13:19:52 +00:00
parent ad5da2a118
commit d39c80743d
4 changed files with 153 additions and 107 deletions
@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { RefreshCw, Search, Server, GitBranch } from 'lucide-react';
import { RefreshCw, Server, GitBranch, ChevronDown, ChevronRight } from 'lucide-react';
import apiClient from '../../api/client';
interface EventLog {
@@ -28,12 +28,13 @@ interface WorkflowStep {
action: string;
status: string;
agent_id: string | null;
output?: string;
}
const LEVEL_STYLES: Record<string, { bg: string; text: string; label: string }> = {
error: { bg: 'bg-[rgba(196,145,122,0.12)]', text: 'text-[#a0705a]', label: 'ERROR' },
warn: { bg: 'bg-[rgba(196,168,130,0.15)]', text: 'text-[#9a7d5e]', label: 'WARN' },
info: { bg: 'bg-[rgba(156,175,136,0.12)]', text: 'text-[#7a8e6a]', label: 'INFO' },
const LEVEL_COLORS: Record<string, string> = {
error: 'text-red-400',
warn: 'text-yellow-400',
info: 'text-green-400',
};
const STATUS_STYLES: Record<string, { bg: string; text: string }> = {
@@ -53,12 +54,14 @@ export function SystemLogsView() {
const [traceFilter, setTraceFilter] = useState('');
const [typeFilter, setTypeFilter] = useState('');
const [levelFilter, setLevelFilter] = useState('');
const terminalRef = useRef<HTMLDivElement>(null);
// Workflow logs state
const [workflows, setWorkflows] = useState<WorkflowSummary[]>([]);
const [selectedTrace, setSelectedTrace] = useState<string | null>(null);
const [workflowSteps, setWorkflowSteps] = useState<WorkflowStep[]>([]);
const [wfLoading, setWfLoading] = useState(false);
const [expandedStep, setExpandedStep] = useState<number | null>(null);
const fetchLogs = async () => {
setLoading(true);
@@ -88,6 +91,7 @@ export function SystemLogsView() {
const fetchWorkflowDetail = async (traceId: string) => {
setWfLoading(true);
setExpandedStep(null);
try {
const resp = await apiClient.get(`/api/v1/workflow/${traceId}`);
setWorkflowSteps(resp.data.steps || []);
@@ -108,9 +112,16 @@ export function SystemLogsView() {
if (selectedTrace) fetchWorkflowDetail(selectedTrace);
}, [selectedTrace]);
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
fetchLogs();
useEffect(() => {
if (terminalRef.current) {
terminalRef.current.scrollTop = terminalRef.current.scrollHeight;
}
}, [logs]);
const formatTime = (ts: string | null) => {
if (!ts) return '--:--:--';
const d = new Date(ts);
return d.toLocaleTimeString('en-GB', { hour12: false });
};
return (
@@ -143,18 +154,19 @@ export function SystemLogsView() {
{tab === 'system' ? (
<>
<form onSubmit={handleSearch} className="grid grid-cols-4 gap-3 mb-5">
{/* Filter bar */}
<div className="flex gap-3 mb-4">
<input
type="text"
value={traceFilter}
onChange={(e) => setTraceFilter(e.target.value)}
placeholder={t('agent.logFilterTraceId')}
className="px-3 py-2 bg-bg-card border border-border-primary rounded-lg text-sm text-text-primary placeholder:text-text-muted/50 focus:outline-none focus:ring-2 focus:ring-accent/15"
placeholder="Trace ID"
className="px-3 py-1.5 bg-bg-card border border-border-primary rounded-lg text-xs text-text-primary placeholder:text-text-muted/50 focus:outline-none focus:ring-2 focus:ring-accent/15 w-40"
/>
<select
value={typeFilter}
onChange={(e) => setTypeFilter(e.target.value)}
className="px-3 py-2 bg-bg-card border border-border-primary rounded-lg text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/15"
className="px-3 py-1.5 bg-bg-card border border-border-primary rounded-lg text-xs text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/15"
>
<option value="">{t('agent.logFilterAllTypes')}</option>
<option value="workflow_start">workflow_start</option>
@@ -168,54 +180,42 @@ export function SystemLogsView() {
<select
value={levelFilter}
onChange={(e) => setLevelFilter(e.target.value)}
className="px-3 py-2 bg-bg-card border border-border-primary rounded-lg text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/15"
className="px-3 py-1.5 bg-bg-card border border-border-primary rounded-lg text-xs text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/15"
>
<option value="">{t('agent.logFilterAllLevels')}</option>
<option value="info">INFO</option>
<option value="warn">WARN</option>
<option value="error">ERROR</option>
</select>
<button type="submit" className="flex items-center justify-center gap-2 px-4 py-2 bg-accent text-white text-sm font-medium rounded-lg hover:bg-accent-hover transition-colors">
<Search size={14} /> {t('agent.logSearch')}
<button
onClick={fetchLogs}
className="px-3 py-1.5 bg-accent text-white text-xs font-medium rounded-lg hover:bg-accent-hover transition-colors"
>
{t('agent.logSearch')}
</button>
</form>
</div>
<div className="bg-bg-card border border-border-primary rounded-xl overflow-hidden">
<div className="overflow-x-auto max-h-[60vh] overflow-y-auto">
<table className="w-full text-xs">
<thead className="bg-bg-secondary sticky top-0">
<tr>
<th className="px-4 py-3 text-left font-semibold text-text-muted uppercase tracking-wider">{t('agent.logLevel')}</th>
<th className="px-4 py-3 text-left font-semibold text-text-muted uppercase tracking-wider">{t('agent.logType')}</th>
<th className="px-4 py-3 text-left font-semibold text-text-muted uppercase tracking-wider">Trace ID</th>
<th className="px-4 py-3 text-left font-semibold text-text-muted uppercase tracking-wider">{t('agent.logNode')}</th>
<th className="px-4 py-3 text-left font-semibold text-text-muted uppercase tracking-wider">{t('agent.logMessage')}</th>
<th className="px-4 py-3 text-left font-semibold text-text-muted uppercase tracking-wider">{t('agent.logTime')}</th>
</tr>
</thead>
<tbody className="divide-y divide-border-primary">
{logs.length === 0 ? (
<tr><td colSpan={6} className="px-4 py-12 text-center text-text-muted">{t('agent.noLogs')}</td></tr>
) : (
logs.map((log) => {
const style = LEVEL_STYLES[log.level] || LEVEL_STYLES.info;
return (
<tr key={log.id} className="hover:bg-bg-secondary/50 transition-colors">
<td className="px-4 py-2.5">
<span className={`px-2 py-0.5 rounded text-[10px] font-bold ${style.bg} ${style.text}`}>{style.label}</span>
</td>
<td className="px-4 py-2.5 text-text-secondary font-mono">{log.event_type}</td>
<td className="px-4 py-2.5 text-text-muted font-mono">{log.trace_id.slice(-8)}</td>
<td className="px-4 py-2.5 text-text-secondary">{log.node_name || '-'}</td>
<td className="px-4 py-2.5 text-text-primary max-w-xs truncate" title={log.message}>{log.message}</td>
<td className="px-4 py-2.5 text-text-muted whitespace-nowrap">{log.created_at ? new Date(log.created_at).toLocaleString() : '-'}</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
{/* Terminal-style log display */}
<div
ref={terminalRef}
className="bg-[#1a1b1e] border border-border-primary rounded-xl p-4 h-[60vh] overflow-y-auto font-mono text-xs leading-relaxed"
>
{logs.length === 0 ? (
<div className="text-gray-500 text-center py-8">{t('agent.noLogs')}</div>
) : (
logs.map((log) => {
const levelColor = LEVEL_COLORS[log.level] || 'text-gray-400';
return (
<div key={log.id} className="py-0.5 hover:bg-white/5 px-1 rounded">
<span className="text-gray-500">[{formatTime(log.created_at)}]</span>{' '}
<span className={`font-bold ${levelColor}`}>{log.level.toUpperCase().padEnd(5)}</span>{' '}
<span className="text-blue-300">{log.node_name || 'system'}</span>{' '}
<span className="text-gray-200">{log.message}</span>
{log.trace_id && <span className="text-gray-600 ml-2">#{log.trace_id.slice(-6)}</span>}
</div>
);
})
)}
</div>
</>
) : (
@@ -259,38 +259,40 @@ export function SystemLogsView() {
{t('common.loading')}
</div>
) : (
<div className="flex-1 overflow-y-auto">
<table className="w-full text-xs">
<thead className="bg-bg-secondary sticky top-0">
<tr>
<th className="px-4 py-3 text-left font-semibold text-text-muted uppercase tracking-wider">#</th>
<th className="px-4 py-3 text-left font-semibold text-text-muted uppercase tracking-wider">{t('agent.name')}</th>
<th className="px-4 py-3 text-left font-semibold text-text-muted uppercase tracking-wider">{t('agent.logNode')}</th>
<th className="px-4 py-3 text-left font-semibold text-text-muted uppercase tracking-wider">{t('common.status')}</th>
<th className="px-4 py-3 text-left font-semibold text-text-muted uppercase tracking-wider">{t('agent.workflowAction')}</th>
</tr>
</thead>
<tbody className="divide-y divide-border-primary">
{workflowSteps.length === 0 ? (
<tr><td colSpan={5} className="px-4 py-12 text-center text-text-muted">{t('workflow.noStepsYet')}</td></tr>
) : (
workflowSteps.map((step, idx) => {
const ss = STATUS_STYLES[step.status] || STATUS_STYLES.pending;
return (
<tr key={idx} className="hover:bg-bg-secondary/50 transition-colors">
<td className="px-4 py-2.5 text-text-muted">{step.step || idx + 1}</td>
<td className="px-4 py-2.5 text-text-primary font-medium">{step.name}</td>
<td className="px-4 py-2.5 text-text-secondary">{step.node}</td>
<td className="px-4 py-2.5">
<span className={`px-2 py-0.5 rounded text-[10px] font-bold ${ss.bg} ${ss.text}`}>{step.status}</span>
</td>
<td className="px-4 py-2.5 text-text-secondary max-w-sm truncate" title={step.action}>{step.action}</td>
</tr>
);
})
)}
</tbody>
</table>
<div className="flex-1 overflow-y-auto p-4 space-y-2">
{workflowSteps.length === 0 ? (
<div className="text-center text-text-muted text-xs py-8">{t('workflow.noStepsYet')}</div>
) : (
workflowSteps.map((step, idx) => {
const ss = STATUS_STYLES[step.status] || STATUS_STYLES.pending;
const isExpanded = expandedStep === idx;
return (
<div key={idx} className="border border-border-primary rounded-lg overflow-hidden">
<button
onClick={() => setExpandedStep(isExpanded ? null : idx)}
className="w-full flex items-center gap-3 px-4 py-3 hover:bg-bg-secondary/50 transition-colors text-left"
>
{step.output ? (
isExpanded ? <ChevronDown size={14} className="text-text-muted shrink-0" /> : <ChevronRight size={14} className="text-text-muted shrink-0" />
) : (
<div className="w-3.5 shrink-0" />
)}
<span className="text-xs text-text-muted w-6">{step.step || idx + 1}</span>
<span className="text-xs font-medium text-text-primary flex-1 truncate">{step.name}</span>
<span className="text-xs text-text-secondary mr-2">{step.node}</span>
<span className={`px-2 py-0.5 rounded text-[10px] font-bold ${ss.bg} ${ss.text}`}>{step.status}</span>
</button>
{isExpanded && step.output && (
<div className="px-4 pb-3 pt-1 border-t border-border-secondary">
<div className="bg-[#1a1b1e] rounded-lg p-3 text-xs text-gray-200 font-mono whitespace-pre-wrap leading-relaxed max-h-48 overflow-y-auto">
{step.output}
</div>
</div>
)}
</div>
);
})
)}
</div>
)}
</div>