From ae564eac6b3039b726355d2c9576086183fa0fab Mon Sep 17 00:00:00 2001 From: yibo Date: Sat, 28 Mar 2026 01:24:57 +0800 Subject: [PATCH] feat: template logs panel - show agent-creator/innovator execution history --- server/routes/company.js | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ web/src/views/TemplateAgents.vue | 90 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 138 insertions(+), 5 deletions(-) diff --git a/server/routes/company.js b/server/routes/company.js index b9a1f68..ad04b30 100644 --- a/server/routes/company.js +++ b/server/routes/company.js @@ -222,6 +222,59 @@ router.post('/templates/:templateId/train', async (req, res) => { } }); +// GET /api/company/templates/:templateId/logs — 获取模板相关的 agent-creator / innovator 执行日志 +router.get('/templates/:templateId/logs', (req, res) => { + try { + const path = require('path'); + const fs = require('fs'); + const templateId = req.params.templateId; + const AGENTS_DIR = path.resolve(__dirname, '../../data/system/agents'); + const logs = []; + + // 扫描 agent-creator 和 innovator 的对话,匹配 tpl-gen-{id} 和 tpl-train-{id} + const scanAgents = [ + { agentId: 'agent-creator', prefix: `tpl-gen-${templateId}`, type: 'generate', label: '🤖 Agent Creator' }, + { agentId: 'innovator', prefix: `tpl-train-${templateId}`, type: 'train', label: '🎓 Innovator' }, + ]; + + for (const scan of scanAgents) { + const convDir = path.join(AGENTS_DIR, scan.agentId, 'conversations'); + if (!fs.existsSync(convDir)) continue; + + const dirs = fs.readdirSync(convDir, { withFileTypes: true }) + .filter(d => d.isDirectory() && d.name.startsWith(scan.prefix)); + + for (const d of dirs) { + const metaPath = path.join(convDir, d.name, 'meta.json'); + const histPath = path.join(convDir, d.name, 'history.json'); + let meta = {}, history = []; + try { meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8')); } catch {} + try { history = JSON.parse(fs.readFileSync(histPath, 'utf-8')); } catch {} + + // 提取最后一条 assistant 消息作为摘要 + const lastAssistant = [...history].reverse().find(m => m.role === 'assistant'); + + logs.push({ + conversationId: d.name, + agentId: scan.agentId, + type: scan.type, + label: scan.label, + createdAt: meta.createdAt || null, + messageCount: history.length, + summary: lastAssistant ? (lastAssistant.content || '').slice(0, 500) : '(执行中...)', + status: lastAssistant ? 'done' : 'running', + }); + } + } + + // 按时间倒序 + logs.sort((a, b) => new Date(b.createdAt || 0).getTime() - new Date(a.createdAt || 0).getTime()); + res.json(logs); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + // GET /api/company/templates/:templateId/files router.get('/templates/:templateId/files', (req, res) => { try { diff --git a/web/src/views/TemplateAgents.vue b/web/src/views/TemplateAgents.vue index 0cfbe75..3fb7ae2 100644 --- a/web/src/views/TemplateAgents.vue +++ b/web/src/views/TemplateAgents.vue @@ -121,6 +121,26 @@ + +
+
+

📜 执行日志

+ +
+
+
+
+ {{ log.status === 'done' ? '✅' : '⏳' }} + {{ log.label }} + {{ formatLogTime(log.createdAt) }} + {{ log.messageCount }} 条 + {{ log._expanded ? '▼' : '▶' }} +
+
{{ log.summary }}
+
+
+
+
📦
@@ -160,6 +180,7 @@ const newTemplate = ref({ id: '', name: '', type: 'skill', description: '' }) // Action state const actionStatus = ref(null) const actionMessage = ref('') +const templateLogs = ref([]) const fileDirty = computed(() => editFileContent.value !== null && filePreviewContent.value !== null && editFileContent.value !== filePreviewContent.value) @@ -228,6 +249,7 @@ function selectTemplate(id) { editFileContent.value = null; filePreviewSize.value = 0; actionStatus.value = null; + templateLogs.value = []; router.push(`/templates/${id}`); } @@ -283,12 +305,16 @@ async function triggerGenerate() { description: selectedTemplate.value?.description || '', }) actionMessage.value = data.message - // Auto-refresh after a delay - setTimeout(async () => { + // Auto-refresh logs and files + const pollId = setInterval(async () => { + await loadTemplateLogs(selectedTemplateId.value) await loadTemplateFiles(selectedTemplateId.value) + }, 10000) + setTimeout(() => { + clearInterval(pollId) actionStatus.value = 'done' actionMessage.value = 'Agent Creator 已完成(请检查文件树)' - }, 30000) + }, 60000) } catch (e) { actionStatus.value = 'error' actionMessage.value = e.response?.data?.error || e.message @@ -301,18 +327,35 @@ async function triggerTrain() { try { const { data } = await axios.post(`/api/company/templates/${selectedTemplateId.value}/train`) actionMessage.value = data.message + const pollId = setInterval(async () => { + await loadTemplateLogs(selectedTemplateId.value) + await loadTemplateFiles(selectedTemplateId.value) + }, 15000) setTimeout(async () => { + clearInterval(pollId) await loadTemplates() - await loadTemplateFiles(selectedTemplateId.value) actionStatus.value = 'done' actionMessage.value = 'Innovator 训练已触发(请稍后查看分数更新)' - }, 60000) + }, 120000) } catch (e) { actionStatus.value = 'error' actionMessage.value = e.response?.data?.error || e.message } } +async function loadTemplateLogs(id) { + try { + const { data } = await axios.get(`/api/company/templates/${id}/logs`) + templateLogs.value = data.map(l => ({ ...l, _expanded: false })) + } catch (e) { console.error(e) } +} + +function formatLogTime(ts) { + if (!ts) return '' + const d = new Date(ts) + return d.toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) +} + watch(() => route.params.agentId, (newId) => { if (newId && newId !== selectedTemplateId.value) { selectedTemplateId.value = newId; @@ -323,6 +366,7 @@ watch(() => route.params.agentId, (newId) => { watch(selectedTemplateId, (newId) => { if (newId) { loadTemplateFiles(newId); + loadTemplateLogs(newId); } }) @@ -552,4 +596,40 @@ onMounted(() => { background: var(--purple-bg, rgba(139,92,246,0.1)); color: var(--purple-text, #a78bfa); flex-shrink: 0; font-weight: 600; } + +/* ═══ Logs Panel ═══ */ +.logs-section { + margin-top: 8px; border: 1px solid var(--border-primary); border-radius: 12px; + background: var(--bg-surface); overflow: hidden; +} +.logs-section .section-header { + display: flex; align-items: center; justify-content: space-between; + padding: 10px 16px; border-bottom: 1px solid var(--border-primary); +} +.logs-section .section-header h4 { + margin: 0; font-size: 14px; font-weight: 700; color: var(--text-primary); +} +.logs-list { max-height: 250px; overflow-y: auto; padding: 8px; display: flex; flex-direction: column; gap: 6px; } +.log-card { + border: 1px solid var(--border-primary); border-radius: 8px; overflow: hidden; + transition: border-color 0.2s; +} +.log-card.log-done { border-left: 3px solid rgba(16,185,129,0.4); } +.log-card.log-running { border-left: 3px solid rgba(59,130,246,0.4); } +.log-header { + display: flex; align-items: center; gap: 8px; padding: 8px 12px; + cursor: pointer; user-select: none; transition: background 0.15s; +} +.log-header:hover { background: var(--bg-surface-hover); } +.log-status-icon { font-size: 12px; } +.log-label { font-size: 12px; font-weight: 700; color: var(--text-secondary); flex: 1; } +.log-time { font-size: 10px; color: var(--text-muted); } +.log-msg-count { font-size: 10px; color: var(--text-faint); } +.log-toggle { font-size: 9px; color: var(--text-faint); } +.log-summary { + padding: 8px 12px; font-size: 12px; line-height: 1.6; color: var(--text-secondary); + background: var(--bg-input); border-top: 1px solid var(--border-primary); + white-space: pre-wrap; word-break: break-word; max-height: 200px; overflow-y: auto; + font-family: 'JetBrains Mono', monospace; +} -- libgit2 0.21.4