Commit ed87748033f8e993da77f5a4a62f2ec62b6b1473

Authored by Yi Bo
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
server/routes/company.js
... ... @@ -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 314 // Agent 招聘
289 315 // ═══════════════════════════════════════
290 316  
... ...
web/src/views/CompanyDashboard.vue
... ... @@ -123,48 +123,68 @@
123 123 <span class="col-heartbeat">心跳</span>
124 124 <span class="col-actions">操作</span>
125 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 188 </div>
169 189 <div v-else class="empty-mini">暂无 Agent</div>
170 190 </div>
... ... @@ -312,6 +332,8 @@ const templates = ref([])
312 332 const activeTab = ref('overview')
313 333 const showTemplates = ref(false)
314 334 const showDropdown = ref(false)
  335 +const expandedAgent = ref(null)
  336 +const agentSkills = ref([])
315 337  
316 338 function statusLabel(status) {
317 339 const map = { creating: '创建中', running: '运行中', paused: '已暂停', shutdown: '已关停' }
... ... @@ -482,6 +504,41 @@ async function stopAgentHeartbeat(agentId) {
482 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 542 // switchTab removed, handled by goToTab and router watcher
486 543  
487 544 onMounted(() => {
... ... @@ -963,4 +1020,48 @@ function closeDropdown() {
963 1020  
964 1021 .hb-active { color: var(--accent-light); font-size: 12px; }
965 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 1067 </style>
... ...