Commit ae564eac6b3039b726355d2c9576086183fa0fab

Authored by yibo
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)
server/routes/company.js
... ... @@ -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 278 // GET /api/company/templates/:templateId/files
226 279 router.get('/templates/:templateId/files', (req, res) => {
227 280 try {
... ...
web/src/views/TemplateAgents.vue
... ... @@ -121,6 +121,26 @@
121 121 </div>
122 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 144 </div>
125 145 <div v-else class="empty-state">
126 146 <div class="empty-icon">📦</div>
... ... @@ -160,6 +180,7 @@ const newTemplate = ref({ id: &#39;&#39;, name: &#39;&#39;, type: &#39;skill&#39;, description: &#39;&#39; })
160 180 // Action state
161 181 const actionStatus = ref(null)
162 182 const actionMessage = ref('')
  183 +const templateLogs = ref([])
163 184  
164 185 const fileDirty = computed(() => editFileContent.value !== null && filePreviewContent.value !== null && editFileContent.value !== filePreviewContent.value)
165 186  
... ... @@ -228,6 +249,7 @@ function selectTemplate(id) {
228 249 editFileContent.value = null;
229 250 filePreviewSize.value = 0;
230 251 actionStatus.value = null;
  252 + templateLogs.value = [];
231 253  
232 254 router.push(`/templates/${id}`);
233 255 }
... ... @@ -283,12 +305,16 @@ async function triggerGenerate() {
283 305 description: selectedTemplate.value?.description || '',
284 306 })
285 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 311 await loadTemplateFiles(selectedTemplateId.value)
  312 + }, 10000)
  313 + setTimeout(() => {
  314 + clearInterval(pollId)
289 315 actionStatus.value = 'done'
290 316 actionMessage.value = 'Agent Creator 已完成(请检查文件树)'
291   - }, 30000)
  317 + }, 60000)
292 318 } catch (e) {
293 319 actionStatus.value = 'error'
294 320 actionMessage.value = e.response?.data?.error || e.message
... ... @@ -301,18 +327,35 @@ async function triggerTrain() {
301 327 try {
302 328 const { data } = await axios.post(`/api/company/templates/${selectedTemplateId.value}/train`)
303 329 actionMessage.value = data.message
  330 + const pollId = setInterval(async () => {
  331 + await loadTemplateLogs(selectedTemplateId.value)
  332 + await loadTemplateFiles(selectedTemplateId.value)
  333 + }, 15000)
304 334 setTimeout(async () => {
  335 + clearInterval(pollId)
305 336 await loadTemplates()
306   - await loadTemplateFiles(selectedTemplateId.value)
307 337 actionStatus.value = 'done'
308 338 actionMessage.value = 'Innovator 训练已触发(请稍后查看分数更新)'
309   - }, 60000)
  339 + }, 120000)
310 340 } catch (e) {
311 341 actionStatus.value = 'error'
312 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 359 watch(() => route.params.agentId, (newId) => {
317 360 if (newId && newId !== selectedTemplateId.value) {
318 361 selectedTemplateId.value = newId;
... ... @@ -323,6 +366,7 @@ watch(() =&gt; route.params.agentId, (newId) =&gt; {
323 366 watch(selectedTemplateId, (newId) => {
324 367 if (newId) {
325 368 loadTemplateFiles(newId);
  369 + loadTemplateLogs(newId);
326 370 }
327 371 })
328 372  
... ... @@ -552,4 +596,40 @@ onMounted(() =&gt; {
552 596 background: var(--purple-bg, rgba(139,92,246,0.1)); color: var(--purple-text, #a78bfa);
553 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 635 </style>
... ...