From ed87748033f8e993da77f5a4a62f2ec62b6b1473 Mon Sep 17 00:00:00 2001 From: Yi Bo Date: Thu, 26 Mar 2026 16:36:39 +0800 Subject: [PATCH] feat: Agent action controls - chat, skills panel, train trigger --- server/routes/company.js | 26 ++++++++++++++++++++++++++ web/src/views/CompanyDashboard.vue | 185 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------------ 2 files changed, 169 insertions(+), 42 deletions(-) diff --git a/server/routes/company.js b/server/routes/company.js index 2b495a9..90478c8 100644 --- a/server/routes/company.js +++ b/server/routes/company.js @@ -285,6 +285,32 @@ router.post('/:companyId/agents/:agentId/heartbeat/stop', (req, res) => { }); // ═══════════════════════════════════════ +// Agent 技能查询 +// ═══════════════════════════════════════ + +// GET /api/company/:companyId/agents/:agentId/skills — 获取 Agent 技能列表 +router.get('/:companyId/agents/:agentId/skills', (req, res) => { + try { + const path = require('path'); + const fs = require('fs'); + const DATA_DIR = path.resolve(__dirname, '../../data'); + const skillsDir = path.join(DATA_DIR, req.params.companyId, 'agents', req.params.agentId, 'skills'); + + if (!fs.existsSync(skillsDir)) { + return res.json([]); + } + + const skills = fs.readdirSync(skillsDir, { withFileTypes: true }) + .filter(d => d.isDirectory()) + .map(d => d.name); + + res.json(skills); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// ═══════════════════════════════════════ // Agent 招聘 // ═══════════════════════════════════════ diff --git a/web/src/views/CompanyDashboard.vue b/web/src/views/CompanyDashboard.vue index 0f4b413..e677a2a 100644 --- a/web/src/views/CompanyDashboard.vue +++ b/web/src/views/CompanyDashboard.vue @@ -123,48 +123,68 @@ 心跳 操作 -
- - - {{ agent.name }} - - - {{ agent.type }} - - - {{ agent.status }} - - - - - - 💓 运行中 - ⏸️ 停止 - - - - - -
+
暂无 Agent
@@ -312,6 +332,8 @@ const templates = ref([]) const activeTab = ref('overview') const showTemplates = ref(false) const showDropdown = ref(false) +const expandedAgent = ref(null) +const agentSkills = ref([]) function statusLabel(status) { const map = { creating: '创建中', running: '运行中', paused: '已暂停', shutdown: '已关停' } @@ -482,6 +504,41 @@ async function stopAgentHeartbeat(agentId) { } catch (e) { console.error(e) } } +function goToAgentChat(agentId) { + if (!selectedCompany.value) return + router.push(`/${selectedCompany.value.id}/agents/${agentId}`) +} + +async function toggleAgentSkills(agentId) { + if (expandedAgent.value === agentId) { + expandedAgent.value = null + return + } + // Fetch skills from backend + try { + const { data } = await axios.get(`/api/company/${selectedCompany.value.id}/agents/${agentId}/skills`) + agentSkills.value = data + } catch { + agentSkills.value = [] + } + expandedAgent.value = agentId +} + +async function triggerTrain(agentId) { + if (!selectedCompany.value) return + const namespacedId = `${selectedCompany.value.id}:${agentId}` + if (!confirm(`确定要对 ${agentId} 发起 Innovator 训练?这将触发 agent-creator 重新定义并由 Innovator 训练该 Agent。`)) return + try { + await axios.post(`/api/agent/${selectedCompany.value.id}:ceo/chat`, { + message: `请安排 Innovator 对 ${namespacedId} 进行一次完整的技能训练/再训练。要求:1)评估当前技能缺口 2)使用 Innovator 进行 FDE 训练 3)训练完成后汇报结果`, + conversationId: `train-${agentId}-${Date.now().toString(36)}`, + }) + alert(`✅ 训练指令已发送给 CEO,CEO 将安排 Innovator 训练 ${agentId}`) + } catch (e) { + alert(`❌ 发送训练指令失败: ${e.message}`) + } +} + // switchTab removed, handled by goToTab and router watcher onMounted(() => { @@ -963,4 +1020,48 @@ function closeDropdown() { .hb-active { color: var(--accent-light); font-size: 12px; } .hb-inactive { color: var(--text-muted); font-size: 12px; } + +/* Agent Name Link */ +.agent-name-link { + color: var(--text-primary); cursor: pointer; text-decoration: none; + transition: color 0.15s; +} +.agent-name-link:hover { color: var(--accent-light); text-decoration: underline; } + +/* Action Button Group */ +.action-group { display: flex; gap: 4px; align-items: center; } +.action-group button { + width: 28px; height: 28px; border-radius: 6px; border: 1px solid var(--border-primary); + background: var(--bg-primary); cursor: pointer; font-size: 13px; + display: flex; align-items: center; justify-content: center; transition: all 0.15s; + padding: 0; +} +.action-group button:hover { background: var(--bg-surface-hover); border-color: var(--border-active); transform: scale(1.1); } + +.btn-action-chat { color: var(--accent-light); } +.btn-action-skills { color: var(--blue-text, #60a5fa); } +.btn-action-train { color: var(--purple-text, #a78bfa); } + +/* Expandable Skills Panel */ +.agent-expand-panel { + padding: 16px 20px; background: var(--bg-primary); + border-bottom: 1px solid var(--border-primary); + animation: fadeIn 0.2s ease; +} +.expand-header { + display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; +} +.expand-header h4 { font-size: 13px; font-weight: 700; margin: 0; } +.btn-close-expand { + background: none; border: none; color: var(--text-muted); cursor: pointer; font-size: 14px; + padding: 4px 8px; border-radius: 4px; transition: all 0.15s; +} +.btn-close-expand:hover { background: var(--bg-surface-hover); color: var(--text-primary); } + +.skills-grid { display: flex; flex-wrap: wrap; gap: 8px; } +.skill-chip { + padding: 4px 12px; border-radius: 8px; font-size: 12px; font-weight: 600; + background: rgba(139,92,246,0.08); color: var(--purple-text, #a78bfa); + border: 1px solid rgba(139,92,246,0.15); +} -- libgit2 0.21.4