Commit ae564eac6b3039b726355d2c9576086183fa0fab
1 parent
d4ed917e
feat: template logs panel - show agent-creator/innovator execution history
- GET /api/company/templates/:id/logs scans system agent conversations - Logs stored in system agents (agent-creator/innovator), NOT in template dir - Template directories stay clean for copying - Collapsible log cards with status, time, message count, and output summary - Auto-poll during generate/train actions (10s/15s intervals)
Showing
2 changed files
with
138 additions
and
5 deletions
server/routes/company.js
| @@ -222,6 +222,59 @@ router.post('/templates/:templateId/train', async (req, res) => { | @@ -222,6 +222,59 @@ router.post('/templates/:templateId/train', async (req, res) => { | ||
| 222 | } | 222 | } |
| 223 | }); | 223 | }); |
| 224 | 224 | ||
| 225 | +// GET /api/company/templates/:templateId/logs — 获取模板相关的 agent-creator / innovator 执行日志 | ||
| 226 | +router.get('/templates/:templateId/logs', (req, res) => { | ||
| 227 | + try { | ||
| 228 | + const path = require('path'); | ||
| 229 | + const fs = require('fs'); | ||
| 230 | + const templateId = req.params.templateId; | ||
| 231 | + const AGENTS_DIR = path.resolve(__dirname, '../../data/system/agents'); | ||
| 232 | + const logs = []; | ||
| 233 | + | ||
| 234 | + // 扫描 agent-creator 和 innovator 的对话,匹配 tpl-gen-{id} 和 tpl-train-{id} | ||
| 235 | + const scanAgents = [ | ||
| 236 | + { agentId: 'agent-creator', prefix: `tpl-gen-${templateId}`, type: 'generate', label: '🤖 Agent Creator' }, | ||
| 237 | + { agentId: 'innovator', prefix: `tpl-train-${templateId}`, type: 'train', label: '🎓 Innovator' }, | ||
| 238 | + ]; | ||
| 239 | + | ||
| 240 | + for (const scan of scanAgents) { | ||
| 241 | + const convDir = path.join(AGENTS_DIR, scan.agentId, 'conversations'); | ||
| 242 | + if (!fs.existsSync(convDir)) continue; | ||
| 243 | + | ||
| 244 | + const dirs = fs.readdirSync(convDir, { withFileTypes: true }) | ||
| 245 | + .filter(d => d.isDirectory() && d.name.startsWith(scan.prefix)); | ||
| 246 | + | ||
| 247 | + for (const d of dirs) { | ||
| 248 | + const metaPath = path.join(convDir, d.name, 'meta.json'); | ||
| 249 | + const histPath = path.join(convDir, d.name, 'history.json'); | ||
| 250 | + let meta = {}, history = []; | ||
| 251 | + try { meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8')); } catch {} | ||
| 252 | + try { history = JSON.parse(fs.readFileSync(histPath, 'utf-8')); } catch {} | ||
| 253 | + | ||
| 254 | + // 提取最后一条 assistant 消息作为摘要 | ||
| 255 | + const lastAssistant = [...history].reverse().find(m => m.role === 'assistant'); | ||
| 256 | + | ||
| 257 | + logs.push({ | ||
| 258 | + conversationId: d.name, | ||
| 259 | + agentId: scan.agentId, | ||
| 260 | + type: scan.type, | ||
| 261 | + label: scan.label, | ||
| 262 | + createdAt: meta.createdAt || null, | ||
| 263 | + messageCount: history.length, | ||
| 264 | + summary: lastAssistant ? (lastAssistant.content || '').slice(0, 500) : '(执行中...)', | ||
| 265 | + status: lastAssistant ? 'done' : 'running', | ||
| 266 | + }); | ||
| 267 | + } | ||
| 268 | + } | ||
| 269 | + | ||
| 270 | + // 按时间倒序 | ||
| 271 | + logs.sort((a, b) => new Date(b.createdAt || 0).getTime() - new Date(a.createdAt || 0).getTime()); | ||
| 272 | + res.json(logs); | ||
| 273 | + } catch (err) { | ||
| 274 | + res.status(500).json({ error: err.message }); | ||
| 275 | + } | ||
| 276 | +}); | ||
| 277 | + | ||
| 225 | // GET /api/company/templates/:templateId/files | 278 | // GET /api/company/templates/:templateId/files |
| 226 | router.get('/templates/:templateId/files', (req, res) => { | 279 | router.get('/templates/:templateId/files', (req, res) => { |
| 227 | try { | 280 | try { |
web/src/views/TemplateAgents.vue
| @@ -121,6 +121,26 @@ | @@ -121,6 +121,26 @@ | ||
| 121 | </div> | 121 | </div> |
| 122 | </div> | 122 | </div> |
| 123 | 123 | ||
| 124 | + <!-- Logs Panel --> | ||
| 125 | + <div v-if="templateLogs.length > 0" class="logs-section"> | ||
| 126 | + <div class="section-header"> | ||
| 127 | + <h4>📜 执行日志</h4> | ||
| 128 | + <button class="refresh-btn" @click="loadTemplateLogs(selectedTemplateId)" title="刷新">🔄</button> | ||
| 129 | + </div> | ||
| 130 | + <div class="logs-list"> | ||
| 131 | + <div v-for="log in templateLogs" :key="log.conversationId" class="log-card" :class="'log-' + log.status"> | ||
| 132 | + <div class="log-header" @click="log._expanded = !log._expanded"> | ||
| 133 | + <span class="log-status-icon">{{ log.status === 'done' ? '✅' : '⏳' }}</span> | ||
| 134 | + <span class="log-label">{{ log.label }}</span> | ||
| 135 | + <span class="log-time">{{ formatLogTime(log.createdAt) }}</span> | ||
| 136 | + <span class="log-msg-count">{{ log.messageCount }} 条</span> | ||
| 137 | + <span class="log-toggle">{{ log._expanded ? '▼' : '▶' }}</span> | ||
| 138 | + </div> | ||
| 139 | + <div v-if="log._expanded" class="log-summary">{{ log.summary }}</div> | ||
| 140 | + </div> | ||
| 141 | + </div> | ||
| 142 | + </div> | ||
| 143 | + | ||
| 124 | </div> | 144 | </div> |
| 125 | <div v-else class="empty-state"> | 145 | <div v-else class="empty-state"> |
| 126 | <div class="empty-icon">📦</div> | 146 | <div class="empty-icon">📦</div> |
| @@ -160,6 +180,7 @@ const newTemplate = ref({ id: '', name: '', type: 'skill', description: '' }) | @@ -160,6 +180,7 @@ const newTemplate = ref({ id: '', name: '', type: 'skill', description: '' }) | ||
| 160 | // Action state | 180 | // Action state |
| 161 | const actionStatus = ref(null) | 181 | const actionStatus = ref(null) |
| 162 | const actionMessage = ref('') | 182 | const actionMessage = ref('') |
| 183 | +const templateLogs = ref([]) | ||
| 163 | 184 | ||
| 164 | const fileDirty = computed(() => editFileContent.value !== null && filePreviewContent.value !== null && editFileContent.value !== filePreviewContent.value) | 185 | const fileDirty = computed(() => editFileContent.value !== null && filePreviewContent.value !== null && editFileContent.value !== filePreviewContent.value) |
| 165 | 186 | ||
| @@ -228,6 +249,7 @@ function selectTemplate(id) { | @@ -228,6 +249,7 @@ function selectTemplate(id) { | ||
| 228 | editFileContent.value = null; | 249 | editFileContent.value = null; |
| 229 | filePreviewSize.value = 0; | 250 | filePreviewSize.value = 0; |
| 230 | actionStatus.value = null; | 251 | actionStatus.value = null; |
| 252 | + templateLogs.value = []; | ||
| 231 | 253 | ||
| 232 | router.push(`/templates/${id}`); | 254 | router.push(`/templates/${id}`); |
| 233 | } | 255 | } |
| @@ -283,12 +305,16 @@ async function triggerGenerate() { | @@ -283,12 +305,16 @@ async function triggerGenerate() { | ||
| 283 | description: selectedTemplate.value?.description || '', | 305 | description: selectedTemplate.value?.description || '', |
| 284 | }) | 306 | }) |
| 285 | actionMessage.value = data.message | 307 | actionMessage.value = data.message |
| 286 | - // Auto-refresh after a delay | ||
| 287 | - setTimeout(async () => { | 308 | + // Auto-refresh logs and files |
| 309 | + const pollId = setInterval(async () => { | ||
| 310 | + await loadTemplateLogs(selectedTemplateId.value) | ||
| 288 | await loadTemplateFiles(selectedTemplateId.value) | 311 | await loadTemplateFiles(selectedTemplateId.value) |
| 312 | + }, 10000) | ||
| 313 | + setTimeout(() => { | ||
| 314 | + clearInterval(pollId) | ||
| 289 | actionStatus.value = 'done' | 315 | actionStatus.value = 'done' |
| 290 | actionMessage.value = 'Agent Creator 已完成(请检查文件树)' | 316 | actionMessage.value = 'Agent Creator 已完成(请检查文件树)' |
| 291 | - }, 30000) | 317 | + }, 60000) |
| 292 | } catch (e) { | 318 | } catch (e) { |
| 293 | actionStatus.value = 'error' | 319 | actionStatus.value = 'error' |
| 294 | actionMessage.value = e.response?.data?.error || e.message | 320 | actionMessage.value = e.response?.data?.error || e.message |
| @@ -301,18 +327,35 @@ async function triggerTrain() { | @@ -301,18 +327,35 @@ async function triggerTrain() { | ||
| 301 | try { | 327 | try { |
| 302 | const { data } = await axios.post(`/api/company/templates/${selectedTemplateId.value}/train`) | 328 | const { data } = await axios.post(`/api/company/templates/${selectedTemplateId.value}/train`) |
| 303 | actionMessage.value = data.message | 329 | actionMessage.value = data.message |
| 330 | + const pollId = setInterval(async () => { | ||
| 331 | + await loadTemplateLogs(selectedTemplateId.value) | ||
| 332 | + await loadTemplateFiles(selectedTemplateId.value) | ||
| 333 | + }, 15000) | ||
| 304 | setTimeout(async () => { | 334 | setTimeout(async () => { |
| 335 | + clearInterval(pollId) | ||
| 305 | await loadTemplates() | 336 | await loadTemplates() |
| 306 | - await loadTemplateFiles(selectedTemplateId.value) | ||
| 307 | actionStatus.value = 'done' | 337 | actionStatus.value = 'done' |
| 308 | actionMessage.value = 'Innovator 训练已触发(请稍后查看分数更新)' | 338 | actionMessage.value = 'Innovator 训练已触发(请稍后查看分数更新)' |
| 309 | - }, 60000) | 339 | + }, 120000) |
| 310 | } catch (e) { | 340 | } catch (e) { |
| 311 | actionStatus.value = 'error' | 341 | actionStatus.value = 'error' |
| 312 | actionMessage.value = e.response?.data?.error || e.message | 342 | actionMessage.value = e.response?.data?.error || e.message |
| 313 | } | 343 | } |
| 314 | } | 344 | } |
| 315 | 345 | ||
| 346 | +async function loadTemplateLogs(id) { | ||
| 347 | + try { | ||
| 348 | + const { data } = await axios.get(`/api/company/templates/${id}/logs`) | ||
| 349 | + templateLogs.value = data.map(l => ({ ...l, _expanded: false })) | ||
| 350 | + } catch (e) { console.error(e) } | ||
| 351 | +} | ||
| 352 | + | ||
| 353 | +function formatLogTime(ts) { | ||
| 354 | + if (!ts) return '' | ||
| 355 | + const d = new Date(ts) | ||
| 356 | + return d.toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) | ||
| 357 | +} | ||
| 358 | + | ||
| 316 | watch(() => route.params.agentId, (newId) => { | 359 | watch(() => route.params.agentId, (newId) => { |
| 317 | if (newId && newId !== selectedTemplateId.value) { | 360 | if (newId && newId !== selectedTemplateId.value) { |
| 318 | selectedTemplateId.value = newId; | 361 | selectedTemplateId.value = newId; |
| @@ -323,6 +366,7 @@ watch(() => route.params.agentId, (newId) => { | @@ -323,6 +366,7 @@ watch(() => route.params.agentId, (newId) => { | ||
| 323 | watch(selectedTemplateId, (newId) => { | 366 | watch(selectedTemplateId, (newId) => { |
| 324 | if (newId) { | 367 | if (newId) { |
| 325 | loadTemplateFiles(newId); | 368 | loadTemplateFiles(newId); |
| 369 | + loadTemplateLogs(newId); | ||
| 326 | } | 370 | } |
| 327 | }) | 371 | }) |
| 328 | 372 | ||
| @@ -552,4 +596,40 @@ onMounted(() => { | @@ -552,4 +596,40 @@ onMounted(() => { | ||
| 552 | background: var(--purple-bg, rgba(139,92,246,0.1)); color: var(--purple-text, #a78bfa); | 596 | background: var(--purple-bg, rgba(139,92,246,0.1)); color: var(--purple-text, #a78bfa); |
| 553 | flex-shrink: 0; font-weight: 600; | 597 | flex-shrink: 0; font-weight: 600; |
| 554 | } | 598 | } |
| 599 | + | ||
| 600 | +/* ═══ Logs Panel ═══ */ | ||
| 601 | +.logs-section { | ||
| 602 | + margin-top: 8px; border: 1px solid var(--border-primary); border-radius: 12px; | ||
| 603 | + background: var(--bg-surface); overflow: hidden; | ||
| 604 | +} | ||
| 605 | +.logs-section .section-header { | ||
| 606 | + display: flex; align-items: center; justify-content: space-between; | ||
| 607 | + padding: 10px 16px; border-bottom: 1px solid var(--border-primary); | ||
| 608 | +} | ||
| 609 | +.logs-section .section-header h4 { | ||
| 610 | + margin: 0; font-size: 14px; font-weight: 700; color: var(--text-primary); | ||
| 611 | +} | ||
| 612 | +.logs-list { max-height: 250px; overflow-y: auto; padding: 8px; display: flex; flex-direction: column; gap: 6px; } | ||
| 613 | +.log-card { | ||
| 614 | + border: 1px solid var(--border-primary); border-radius: 8px; overflow: hidden; | ||
| 615 | + transition: border-color 0.2s; | ||
| 616 | +} | ||
| 617 | +.log-card.log-done { border-left: 3px solid rgba(16,185,129,0.4); } | ||
| 618 | +.log-card.log-running { border-left: 3px solid rgba(59,130,246,0.4); } | ||
| 619 | +.log-header { | ||
| 620 | + display: flex; align-items: center; gap: 8px; padding: 8px 12px; | ||
| 621 | + cursor: pointer; user-select: none; transition: background 0.15s; | ||
| 622 | +} | ||
| 623 | +.log-header:hover { background: var(--bg-surface-hover); } | ||
| 624 | +.log-status-icon { font-size: 12px; } | ||
| 625 | +.log-label { font-size: 12px; font-weight: 700; color: var(--text-secondary); flex: 1; } | ||
| 626 | +.log-time { font-size: 10px; color: var(--text-muted); } | ||
| 627 | +.log-msg-count { font-size: 10px; color: var(--text-faint); } | ||
| 628 | +.log-toggle { font-size: 9px; color: var(--text-faint); } | ||
| 629 | +.log-summary { | ||
| 630 | + padding: 8px 12px; font-size: 12px; line-height: 1.6; color: var(--text-secondary); | ||
| 631 | + background: var(--bg-input); border-top: 1px solid var(--border-primary); | ||
| 632 | + white-space: pre-wrap; word-break: break-word; max-height: 200px; overflow-y: auto; | ||
| 633 | + font-family: 'JetBrains Mono', monospace; | ||
| 634 | +} | ||
| 555 | </style> | 635 | </style> |