Files
sanguo_moziplus_v2/src/frontend/src/components/ToolchainPanel.tsx
T
cfdaily 91685ebfdd
CI / lint (pull_request) Successful in 7s
CI / test (pull_request) Successful in 29s
CI / frontend (pull_request) Successful in 11s
CI / notify-on-failure (pull_request) Successful in 0s
[moz] feat(frontend): 工具链面板加 from/to 显示 + 筛选 + 修复事件类型未知
- 前端:列表项加 from → to 标签(Agent 中文名)
- 前端:加「全部 / 未处理」筛选按钮
- 前端:详情区也显示 from → to
- 后端:ToolchainContextSection 修复事件类型 fallback 为中文标签
- 后端:加来源/指派信息到 prompt 消息体
2026-06-14 17:12:11 +08:00

251 lines
10 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* ToolchainPanel — 工具链事件(系统级)
* 展示 _toolchain 项目的 tasksCI/PR/部署/Review 通知
*/
import { useEffect, useState } from 'react';
const AGENT_NAMES: Record<string, string> = {
'pangtong-fujunshi': '庞统',
'simayi-challenger': '司马懿',
'zhangfei-dev': '张飞',
'guanyu-dev': '关羽',
'zhaoyun-data': '赵云',
'jiangwei-infra': '姜维',
'system': '系统',
};
const EVENT_LABELS: Record<string, string> = {
'review_request': 'Review 请求',
'review_result': 'Review 结果',
'review_merged': 'PR 合并',
'review_comment': 'Review 评论',
'review_updated': 'Review 更新',
'ci_failure': 'CI 失败',
'deploy_failure': '部署失败',
'issue_assigned': 'Issue 指派',
'mention': '@提及',
};
const STATUS_COLORS: Record<string, string> = {
pending: '#f59e0b22', claimed: '#6a9eff22', working: '#6a9eff22',
review: '#818cf822', done: '#2ecc8a22', failed: '#ef444422',
cancelled: '#6b728022', blocked: '#ef444422',
};
const STATUS_LABELS: Record<string, string> = {
pending: '待处理', claimed: '已认领', working: '处理中',
review: '审查中', done: '已完成', failed: '失败',
cancelled: '已取消', blocked: '已拦截',
};
function fmtTime(iso: string): string {
try {
const d = new Date(iso.includes('T') ? iso : iso.replace(' ', 'T') + 'Z');
const now = Date.now();
const diff = now - d.getTime();
const mins = Math.floor(diff / 60000);
if (mins < 1) return '刚刚';
if (mins < 60) return `${mins}分钟前`;
const hrs = Math.floor(mins / 60);
if (hrs < 24) return `${hrs}小时前`;
return `${d.getMonth() + 1}/${d.getDate()} ${d.getHours()}:${String(d.getMinutes()).padStart(2, '0')}`;
} catch { return iso; }
}
export default function ToolchainPanel() {
const [tasks, setTasks] = useState<any[]>([]);
const [selectedId, setSelectedId] = useState<string | null>(null);
const [detail, setDetail] = useState<any>(null);
const [searchQuery, setSearchQuery] = useState('');
const [loading, setLoading] = useState(false);
const [filterMode, setFilterMode] = useState<'all' | 'pending'>('all');
const loadTasks = async (q?: string) => {
setLoading(true);
try {
const url = q
? `/api/projects/_toolchain/tasks?q=${encodeURIComponent(q)}`
: `/api/projects/_toolchain/tasks`;
const res = await fetch(url);
if (res.ok) {
const data = await res.json();
setTasks(data.tasks || []);
}
} catch { /* */ }
setLoading(false);
};
const displayed = filterMode === 'pending'
? tasks.filter(t => !['done', 'failed', 'cancelled'].includes(t.status))
: tasks;
useEffect(() => { loadTasks(); }, []);
// 搜索防抖 300ms
useEffect(() => {
const timer = setTimeout(() => {
if (searchQuery !== undefined) loadTasks(searchQuery || undefined);
}, 300);
return () => clearTimeout(timer);
}, [searchQuery]);
useEffect(() => {
if (!selectedId) { setDetail(null); return; }
(async () => {
try {
const res = await fetch(
`/api/projects/_toolchain/tasks/${selectedId}?expand=comments`
);
if (res.ok) setDetail(await res.json());
} catch { /* */ }
})();
}, [selectedId]);
// 渲染评论列表(兼容 expand 和裸 list 格式)
const renderComments = (comments: any[]) => {
if (!comments || comments.length === 0) return null;
return (
<div style={{ marginTop: 16 }}>
<div style={{ fontSize: 11, color: 'var(--muted)', marginBottom: 8, fontWeight: 600 }}>
📋 ({comments.length})
</div>
{comments.map((c: any, i: number) => (
<div key={c.id || i} style={{
padding: '8px 12px', background: 'var(--panel2)', borderRadius: 6, marginBottom: 6,
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
<span style={{ fontSize: 10, color: 'var(--acc)', fontWeight: 600 }}>
{c.author || 'system'}
</span>
<span style={{ fontSize: 9, color: 'var(--muted)' }}>{fmtTime(c.created_at)}</span>
</div>
<div style={{ fontSize: 12, color: '#a0aec0', lineHeight: 1.5 }}>{c.body}</div>
</div>
))}
</div>
);
};
return (
<div style={{ display: 'flex', gap: 0, height: '100%', minHeight: 500 }}>
{/* 左侧列表 */}
<div style={{ width: 380, borderRight: '1px solid var(--line)', display: 'flex', flexDirection: 'column', flexShrink: 0 }}>
{/* 搜索栏 + 刷新 */}
<div style={{ padding: '10px 14px', borderBottom: '1px solid var(--line)', display: 'flex', gap: 6, alignItems: 'center' }}>
<input
type="text"
placeholder="搜索工具链事件..."
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
style={{
flex: 1, padding: '4px 8px', borderRadius: 4, fontSize: 11,
border: '1px solid #2a3550', background: '#161b2e', color: '#dde4f8',
outline: 'none',
}}
/>
<button onClick={() => loadTasks(searchQuery || undefined)} style={{
padding: '3px 8px', borderRadius: 4, fontSize: 10,
border: '1px solid #2a3550', background: '#161b2e', color: '#8899aa', cursor: 'pointer',
}}>🔄</button>
<button onClick={() => setFilterMode('all')} style={{
padding: '3px 8px', borderRadius: 4, fontSize: 10,
border: `1px solid ${filterMode === 'all' ? 'var(--acc)' : '#2a3550'}`,
background: filterMode === 'all' ? 'var(--acc)22' : '#161b2e',
color: filterMode === 'all' ? 'var(--acc)' : '#8899aa', cursor: 'pointer',
}}></button>
<button onClick={() => setFilterMode('pending')} style={{
padding: '3px 8px', borderRadius: 4, fontSize: 10,
border: `1px solid ${filterMode === 'pending' ? 'var(--acc)' : '#2a3550'}`,
background: filterMode === 'pending' ? 'var(--acc)22' : '#161b2e',
color: filterMode === 'pending' ? 'var(--acc)' : '#8899aa', cursor: 'pointer',
}}></button>
<span style={{ fontSize: 10, color: 'var(--muted)' }}>{filterMode === 'pending' ? displayed.length : tasks.length} </span>
</div>
{/* 事件列表 */}
<div style={{ flex: 1, overflowY: 'auto' }}>
{tasks.length === 0 && (
<div style={{ textAlign: 'center', padding: 40, color: 'var(--muted)', fontSize: 12 }}>
{loading ? '加载中...' : '暂无工具链事件'}
</div>
)}
{displayed.map((t: any) => (
<div key={t.id} onClick={() => setSelectedId(t.id)} style={{
padding: '10px 14px', borderBottom: '1px solid var(--line)',
cursor: 'pointer', transition: 'background .15s',
background: selectedId === t.id ? 'var(--panel2)' : 'transparent',
}}
onMouseEnter={e => e.currentTarget.style.background = 'var(--panel2)'}
onMouseLeave={e => e.currentTarget.style.background = selectedId === t.id ? 'var(--panel2)' : 'transparent'}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }}>
<span style={{
fontSize: 9, padding: '1px 5px', borderRadius: 3,
background: STATUS_COLORS[t.status] || '#2a3550',
color: '#dde4f8',
}}>{STATUS_LABELS[t.status] || t.status}</span>
<span style={{ fontSize: 9, color: 'var(--muted)' }}>{fmtTime(t.created_at)}</span>
</div>
<div style={{
fontSize: 12, fontWeight: 500, color: '#dde4f8',
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>{t.title}</div>
<div style={{ fontSize: 10, color: 'var(--muted)', marginTop: 2 }}>
{AGENT_NAMES['system'] || '系统'} {AGENT_NAMES[t.assignee] || t.assignee || '?'}
</div>
</div>
))}
</div>
</div>
{/* 右侧详情 */}
<div style={{ flex: 1, padding: '16px 20px', overflowY: 'auto' }}>
{!detail ? (
<div style={{ textAlign: 'center', padding: 60, color: 'var(--muted)' }}>
<div style={{ fontSize: 36, marginBottom: 12 }}></div>
<div style={{ fontSize: 13 }}></div>
</div>
) : (
<>
{/* 头部 */}
<div style={{ marginBottom: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
<span style={{ fontSize: 10, padding: '2px 6px', borderRadius: 4, background: STATUS_COLORS[detail.status] || '#2a3550', color: '#dde4f8' }}>
{STATUS_LABELS[detail.status] || detail.status}
</span>
<span style={{ fontSize: 10, color: 'var(--muted)' }}>{detail.id}</span>
</div>
<div style={{ fontSize: 18, fontWeight: 700, lineHeight: 1.3 }}>{detail.title}</div>
<div style={{ fontSize: 11, color: 'var(--muted)', marginTop: 4 }}>
{AGENT_NAMES['system'] || '系统'} {AGENT_NAMES[detail.assignee] || detail.assignee || '?'}
</div>
<div style={{ fontSize: 12, color: 'var(--muted)', marginTop: 6 }}>
{fmtTime(detail.created_at)}
</div>
</div>
{/* 正文 */}
{detail.description && (
<div style={{
padding: '14px 16px', background: 'var(--panel2)', borderRadius: 10,
fontSize: 13, color: '#a0aec0', lineHeight: 1.7, whiteSpace: 'pre-wrap',
}}>
{detail.description}
</div>
)}
{/* action_report 评论 — expand 格式 {items, total_count} */}
{detail.comments && detail.comments.items && detail.comments.items.length > 0 &&
renderComments(detail.comments.items)
}
{/* 兼容裸 list 格式 */}
{detail.comments && Array.isArray(detail.comments) && detail.comments.length > 0 &&
renderComments(detail.comments)
}
</>
)}
</div>
</div>
);
}