You need to sign in before continuing.
Commit ed87748033f8e993da77f5a4a62f2ec62b6b1473
1 parent
c5693e36
feat: Agent action controls - chat, skills panel, train trigger
- Agent names are clickable links to chatbot page - Action button group per agent: heartbeat ▶/⏹, 💬 Chat, 📋 Skills, 🎓 Train - Expandable skills panel shows agent's loaded skills - Train button sends instruction to CEO to arrange Innovator training - Backend: GET /api/company/:id/agents/:agentId/skills API
Showing
2 changed files
with
169 additions
and
42 deletions
server/routes/company.js
| @@ -285,6 +285,32 @@ router.post('/:companyId/agents/:agentId/heartbeat/stop', (req, res) => { | @@ -285,6 +285,32 @@ router.post('/:companyId/agents/:agentId/heartbeat/stop', (req, res) => { | ||
| 285 | }); | 285 | }); |
| 286 | 286 | ||
| 287 | // ═══════════════════════════════════════ | 287 | // ═══════════════════════════════════════ |
| 288 | +// Agent 技能查询 | ||
| 289 | +// ═══════════════════════════════════════ | ||
| 290 | + | ||
| 291 | +// GET /api/company/:companyId/agents/:agentId/skills — 获取 Agent 技能列表 | ||
| 292 | +router.get('/:companyId/agents/:agentId/skills', (req, res) => { | ||
| 293 | + try { | ||
| 294 | + const path = require('path'); | ||
| 295 | + const fs = require('fs'); | ||
| 296 | + const DATA_DIR = path.resolve(__dirname, '../../data'); | ||
| 297 | + const skillsDir = path.join(DATA_DIR, req.params.companyId, 'agents', req.params.agentId, 'skills'); | ||
| 298 | + | ||
| 299 | + if (!fs.existsSync(skillsDir)) { | ||
| 300 | + return res.json([]); | ||
| 301 | + } | ||
| 302 | + | ||
| 303 | + const skills = fs.readdirSync(skillsDir, { withFileTypes: true }) | ||
| 304 | + .filter(d => d.isDirectory()) | ||
| 305 | + .map(d => d.name); | ||
| 306 | + | ||
| 307 | + res.json(skills); | ||
| 308 | + } catch (err) { | ||
| 309 | + res.status(500).json({ error: err.message }); | ||
| 310 | + } | ||
| 311 | +}); | ||
| 312 | + | ||
| 313 | +// ═══════════════════════════════════════ | ||
| 288 | // Agent 招聘 | 314 | // Agent 招聘 |
| 289 | // ═══════════════════════════════════════ | 315 | // ═══════════════════════════════════════ |
| 290 | 316 |
web/src/views/CompanyDashboard.vue
| @@ -123,48 +123,68 @@ | @@ -123,48 +123,68 @@ | ||
| 123 | <span class="col-heartbeat">心跳</span> | 123 | <span class="col-heartbeat">心跳</span> |
| 124 | <span class="col-actions">操作</span> | 124 | <span class="col-actions">操作</span> |
| 125 | </div> | 125 | </div> |
| 126 | - <div v-for="agent in companyStatus.agents" :key="agent.id" class="table-row"> | ||
| 127 | - <span class="col-name"> | ||
| 128 | - <span class="agent-dot" :class="agent.heartbeatActive ? 'active' : 'inactive'"></span> | ||
| 129 | - {{ agent.name }} | ||
| 130 | - </span> | ||
| 131 | - <span class="col-type"> | ||
| 132 | - <span class="type-badge">{{ agent.type }}</span> | ||
| 133 | - </span> | ||
| 134 | - <span class="col-status"> | ||
| 135 | - <span class="card-status" :class="agent.status">{{ agent.status }}</span> | ||
| 136 | - </span> | ||
| 137 | - <span class="col-interval"> | ||
| 138 | - <select | ||
| 139 | - class="interval-select" | ||
| 140 | - :value="agent.heartbeatInterval" | ||
| 141 | - @change="updateAgentInterval(agent.id, $event.target.value)" | ||
| 142 | - > | ||
| 143 | - <option :value="1">1 分钟</option> | ||
| 144 | - <option :value="5">5 分钟</option> | ||
| 145 | - <option :value="10">10 分钟</option> | ||
| 146 | - <option :value="15">15 分钟</option> | ||
| 147 | - <option :value="30">30 分钟</option> | ||
| 148 | - <option :value="60">60 分钟</option> | ||
| 149 | - </select> | ||
| 150 | - </span> | ||
| 151 | - <span class="col-heartbeat"> | ||
| 152 | - <span v-if="agent.heartbeatActive" class="hb-active">💓 运行中</span> | ||
| 153 | - <span v-else class="hb-inactive">⏸️ 停止</span> | ||
| 154 | - </span> | ||
| 155 | - <span class="col-actions"> | ||
| 156 | - <button | ||
| 157 | - v-if="!agent.heartbeatActive" | ||
| 158 | - class="btn-agent-start" | ||
| 159 | - @click="startAgentHeartbeat(agent.id)" | ||
| 160 | - >▶ 启动</button> | ||
| 161 | - <button | ||
| 162 | - v-else | ||
| 163 | - class="btn-agent-stop" | ||
| 164 | - @click="stopAgentHeartbeat(agent.id)" | ||
| 165 | - >⏹ 停止</button> | ||
| 166 | - </span> | ||
| 167 | - </div> | 126 | + <template v-for="agent in companyStatus.agents" :key="agent.id"> |
| 127 | + <div class="table-row"> | ||
| 128 | + <span class="col-name"> | ||
| 129 | + <span class="agent-dot" :class="agent.heartbeatActive ? 'active' : 'inactive'"></span> | ||
| 130 | + <a class="agent-name-link" @click="goToAgentChat(agent.id)">{{ agent.name }}</a> | ||
| 131 | + </span> | ||
| 132 | + <span class="col-type"> | ||
| 133 | + <span class="type-badge">{{ agent.type }}</span> | ||
| 134 | + </span> | ||
| 135 | + <span class="col-status"> | ||
| 136 | + <span class="card-status" :class="agent.status">{{ agent.status }}</span> | ||
| 137 | + </span> | ||
| 138 | + <span class="col-interval"> | ||
| 139 | + <select | ||
| 140 | + class="interval-select" | ||
| 141 | + :value="agent.heartbeatInterval" | ||
| 142 | + @change="updateAgentInterval(agent.id, $event.target.value)" | ||
| 143 | + > | ||
| 144 | + <option :value="1">1 分钟</option> | ||
| 145 | + <option :value="5">5 分钟</option> | ||
| 146 | + <option :value="10">10 分钟</option> | ||
| 147 | + <option :value="15">15 分钟</option> | ||
| 148 | + <option :value="30">30 分钟</option> | ||
| 149 | + <option :value="60">60 分钟</option> | ||
| 150 | + </select> | ||
| 151 | + </span> | ||
| 152 | + <span class="col-heartbeat"> | ||
| 153 | + <span v-if="agent.heartbeatActive" class="hb-active">💓 运行中</span> | ||
| 154 | + <span v-else class="hb-inactive">⏸️ 停止</span> | ||
| 155 | + </span> | ||
| 156 | + <span class="col-actions"> | ||
| 157 | + <div class="action-group"> | ||
| 158 | + <button | ||
| 159 | + v-if="!agent.heartbeatActive" | ||
| 160 | + class="btn-agent-start" | ||
| 161 | + @click="startAgentHeartbeat(agent.id)" | ||
| 162 | + title="启动心跳" | ||
| 163 | + >▶</button> | ||
| 164 | + <button | ||
| 165 | + v-else | ||
| 166 | + class="btn-agent-stop" | ||
| 167 | + @click="stopAgentHeartbeat(agent.id)" | ||
| 168 | + title="停止心跳" | ||
| 169 | + >⏹</button> | ||
| 170 | + <button class="btn-action-chat" @click="goToAgentChat(agent.id)" title="聊天">💬</button> | ||
| 171 | + <button class="btn-action-skills" @click="toggleAgentSkills(agent.id)" title="技能">📋</button> | ||
| 172 | + <button class="btn-action-train" @click="triggerTrain(agent.id)" title="训练">🎓</button> | ||
| 173 | + </div> | ||
| 174 | + </span> | ||
| 175 | + </div> | ||
| 176 | + <!-- Expandable Skills Panel --> | ||
| 177 | + <div v-if="expandedAgent === agent.id" class="agent-expand-panel"> | ||
| 178 | + <div class="expand-header"> | ||
| 179 | + <h4>📋 {{ agent.name }} 的技能列表</h4> | ||
| 180 | + <button class="btn-close-expand" @click="expandedAgent = null">✕</button> | ||
| 181 | + </div> | ||
| 182 | + <div v-if="agentSkills.length > 0" class="skills-grid"> | ||
| 183 | + <div v-for="skill in agentSkills" :key="skill" class="skill-chip">⚡ {{ skill }}</div> | ||
| 184 | + </div> | ||
| 185 | + <div v-else class="empty-mini" style="padding:12px;">暂无技能(可通过 Innovator 训练获得)</div> | ||
| 186 | + </div> | ||
| 187 | + </template> | ||
| 168 | </div> | 188 | </div> |
| 169 | <div v-else class="empty-mini">暂无 Agent</div> | 189 | <div v-else class="empty-mini">暂无 Agent</div> |
| 170 | </div> | 190 | </div> |
| @@ -312,6 +332,8 @@ const templates = ref([]) | @@ -312,6 +332,8 @@ const templates = ref([]) | ||
| 312 | const activeTab = ref('overview') | 332 | const activeTab = ref('overview') |
| 313 | const showTemplates = ref(false) | 333 | const showTemplates = ref(false) |
| 314 | const showDropdown = ref(false) | 334 | const showDropdown = ref(false) |
| 335 | +const expandedAgent = ref(null) | ||
| 336 | +const agentSkills = ref([]) | ||
| 315 | 337 | ||
| 316 | function statusLabel(status) { | 338 | function statusLabel(status) { |
| 317 | const map = { creating: '创建中', running: '运行中', paused: '已暂停', shutdown: '已关停' } | 339 | const map = { creating: '创建中', running: '运行中', paused: '已暂停', shutdown: '已关停' } |
| @@ -482,6 +504,41 @@ async function stopAgentHeartbeat(agentId) { | @@ -482,6 +504,41 @@ async function stopAgentHeartbeat(agentId) { | ||
| 482 | } catch (e) { console.error(e) } | 504 | } catch (e) { console.error(e) } |
| 483 | } | 505 | } |
| 484 | 506 | ||
| 507 | +function goToAgentChat(agentId) { | ||
| 508 | + if (!selectedCompany.value) return | ||
| 509 | + router.push(`/${selectedCompany.value.id}/agents/${agentId}`) | ||
| 510 | +} | ||
| 511 | + | ||
| 512 | +async function toggleAgentSkills(agentId) { | ||
| 513 | + if (expandedAgent.value === agentId) { | ||
| 514 | + expandedAgent.value = null | ||
| 515 | + return | ||
| 516 | + } | ||
| 517 | + // Fetch skills from backend | ||
| 518 | + try { | ||
| 519 | + const { data } = await axios.get(`/api/company/${selectedCompany.value.id}/agents/${agentId}/skills`) | ||
| 520 | + agentSkills.value = data | ||
| 521 | + } catch { | ||
| 522 | + agentSkills.value = [] | ||
| 523 | + } | ||
| 524 | + expandedAgent.value = agentId | ||
| 525 | +} | ||
| 526 | + | ||
| 527 | +async function triggerTrain(agentId) { | ||
| 528 | + if (!selectedCompany.value) return | ||
| 529 | + const namespacedId = `${selectedCompany.value.id}:${agentId}` | ||
| 530 | + if (!confirm(`确定要对 ${agentId} 发起 Innovator 训练?这将触发 agent-creator 重新定义并由 Innovator 训练该 Agent。`)) return | ||
| 531 | + try { | ||
| 532 | + await axios.post(`/api/agent/${selectedCompany.value.id}:ceo/chat`, { | ||
| 533 | + message: `请安排 Innovator 对 ${namespacedId} 进行一次完整的技能训练/再训练。要求:1)评估当前技能缺口 2)使用 Innovator 进行 FDE 训练 3)训练完成后汇报结果`, | ||
| 534 | + conversationId: `train-${agentId}-${Date.now().toString(36)}`, | ||
| 535 | + }) | ||
| 536 | + alert(`✅ 训练指令已发送给 CEO,CEO 将安排 Innovator 训练 ${agentId}`) | ||
| 537 | + } catch (e) { | ||
| 538 | + alert(`❌ 发送训练指令失败: ${e.message}`) | ||
| 539 | + } | ||
| 540 | +} | ||
| 541 | + | ||
| 485 | // switchTab removed, handled by goToTab and router watcher | 542 | // switchTab removed, handled by goToTab and router watcher |
| 486 | 543 | ||
| 487 | onMounted(() => { | 544 | onMounted(() => { |
| @@ -963,4 +1020,48 @@ function closeDropdown() { | @@ -963,4 +1020,48 @@ function closeDropdown() { | ||
| 963 | 1020 | ||
| 964 | .hb-active { color: var(--accent-light); font-size: 12px; } | 1021 | .hb-active { color: var(--accent-light); font-size: 12px; } |
| 965 | .hb-inactive { color: var(--text-muted); font-size: 12px; } | 1022 | .hb-inactive { color: var(--text-muted); font-size: 12px; } |
| 1023 | + | ||
| 1024 | +/* Agent Name Link */ | ||
| 1025 | +.agent-name-link { | ||
| 1026 | + color: var(--text-primary); cursor: pointer; text-decoration: none; | ||
| 1027 | + transition: color 0.15s; | ||
| 1028 | +} | ||
| 1029 | +.agent-name-link:hover { color: var(--accent-light); text-decoration: underline; } | ||
| 1030 | + | ||
| 1031 | +/* Action Button Group */ | ||
| 1032 | +.action-group { display: flex; gap: 4px; align-items: center; } | ||
| 1033 | +.action-group button { | ||
| 1034 | + width: 28px; height: 28px; border-radius: 6px; border: 1px solid var(--border-primary); | ||
| 1035 | + background: var(--bg-primary); cursor: pointer; font-size: 13px; | ||
| 1036 | + display: flex; align-items: center; justify-content: center; transition: all 0.15s; | ||
| 1037 | + padding: 0; | ||
| 1038 | +} | ||
| 1039 | +.action-group button:hover { background: var(--bg-surface-hover); border-color: var(--border-active); transform: scale(1.1); } | ||
| 1040 | + | ||
| 1041 | +.btn-action-chat { color: var(--accent-light); } | ||
| 1042 | +.btn-action-skills { color: var(--blue-text, #60a5fa); } | ||
| 1043 | +.btn-action-train { color: var(--purple-text, #a78bfa); } | ||
| 1044 | + | ||
| 1045 | +/* Expandable Skills Panel */ | ||
| 1046 | +.agent-expand-panel { | ||
| 1047 | + padding: 16px 20px; background: var(--bg-primary); | ||
| 1048 | + border-bottom: 1px solid var(--border-primary); | ||
| 1049 | + animation: fadeIn 0.2s ease; | ||
| 1050 | +} | ||
| 1051 | +.expand-header { | ||
| 1052 | + display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; | ||
| 1053 | +} | ||
| 1054 | +.expand-header h4 { font-size: 13px; font-weight: 700; margin: 0; } | ||
| 1055 | +.btn-close-expand { | ||
| 1056 | + background: none; border: none; color: var(--text-muted); cursor: pointer; font-size: 14px; | ||
| 1057 | + padding: 4px 8px; border-radius: 4px; transition: all 0.15s; | ||
| 1058 | +} | ||
| 1059 | +.btn-close-expand:hover { background: var(--bg-surface-hover); color: var(--text-primary); } | ||
| 1060 | + | ||
| 1061 | +.skills-grid { display: flex; flex-wrap: wrap; gap: 8px; } | ||
| 1062 | +.skill-chip { | ||
| 1063 | + padding: 4px 12px; border-radius: 8px; font-size: 12px; font-weight: 600; | ||
| 1064 | + background: rgba(139,92,246,0.08); color: var(--purple-text, #a78bfa); | ||
| 1065 | + border: 1px solid rgba(139,92,246,0.15); | ||
| 1066 | +} | ||
| 966 | </style> | 1067 | </style> |