From 52ef5cc095c898fcbd50699fd9e939e26f459491 Mon Sep 17 00:00:00 2001 From: flying-hero <462087392@qq.com> Date: Fri, 3 Apr 2026 20:35:06 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=8D=9E=E8=99=BE?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=88=E8=87=AA=E5=8A=A8=E6=89=AB=E6=8F=8F?= =?UTF-8?q?=20Agent=EF=BC=89=F0=9F=A6=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🎯 新功能: - 捞虾按钮:自动扫描 Docker 容器和宿主机进程 - 发现 openclaw 相关的 Agent 实例 - 自动创建或更新 Agent 记录 - 过滤掉没有端口的'石头'(无效进程) 🦐 捞虾逻辑: 1. 扫描 Docker 容器(跳过数据库、网关等辅助容器) 2. 扫描宿主机进程(ps aux) 3. 从容器名/进程名推断 Agent 名称和专长 4. 提取端口信息 5. 只保存有端口的有效 Agent ✨ 优化: - 处理复杂的容器名称(如 openclaw-instance2-openclaw-cn-gateway-1) - 自动推断 Emoji 和专长 - 避免重复创建 📊 捞虾结果: - 显示捞到的虾数量 - 显示新增和更新数量 - 区分 Docker 容器和宿主机进程 🎨 界面: - 按钮文案:'🦐 捞虾' / '🦐 捞虾中...' - 提示信息生动有趣 - 加载状态显示 🐛 修复: - 不过滤 gateway/watcher 容器(可能包含 Agent) - 只跳过数据库容器(postgres/db/redis) 🦸 感谢北极星 ⭐ 的'捞虾'命名灵感! --- code/backend/api/urls.py | 2 + code/backend/api/views.py | 247 ++++++++++++++++++ code/backend/requirements.txt | 8 +- .../src/components/LobsterPool/index.js | 228 ++++++++++++++++ code/frontend/src/pages/Dashboard/index.js | 110 +++++++- 5 files changed, 590 insertions(+), 5 deletions(-) create mode 100644 code/frontend/src/components/LobsterPool/index.js diff --git a/code/backend/api/urls.py b/code/backend/api/urls.py index 4b0ebd1..fc4b9dd 100644 --- a/code/backend/api/urls.py +++ b/code/backend/api/urls.py @@ -11,5 +11,7 @@ urlpatterns = [ path('agents//memory//', views.agent_memory_detail, name='agent-memory-detail'), path('agents//diary/dates/', views.agent_diary_dates, name='agent-diary-dates'), path('agents//diary//', views.agent_diary_detail, name='agent-diary-detail'), + path('agents/scan/', views.scan_agents, name='agent-scan'), + path('agents/sync/', views.sync_agent, name='agent-sync'), path('tools/', views.tools_list, name='tools-list'), ] diff --git a/code/backend/api/views.py b/code/backend/api/views.py index 5816b16..d97cf53 100644 --- a/code/backend/api/views.py +++ b/code/backend/api/views.py @@ -5,6 +5,7 @@ from rest_framework.decorators import api_view from rest_framework.response import Response from datetime import datetime from agents.models import Agent, AgentDiary +import docker @api_view(['GET']) def agent_list(request): @@ -141,3 +142,249 @@ def agent_diary_detail(request, agent_id, date): }) except AgentDiary.DoesNotExist: return Response({'error': '该日期没有日记'}, status=404) + +@api_view(['POST']) +def scan_agents(request): + """自动扫描本机 Agent 实例(Docker 容器 + 宿主机进程)""" + import subprocess + import re + + scanned = [] + created_count = 0 + updated_count = 0 + + # 专长和 Emoji 映射 + specialty_map = { + 'instance2': '主力/通用', + 'daotong': '道德经注解', + 'coder': '代码专家', + 'web': '网站制作', + 'physics': '物理研究', + 'secretary': '秘书/助理', + 'ditin': '情报/监听', + 'watcher': '舰队监控', + } + + emoji_map = { + 'instance2': '🦸', + 'daotong': '☯️', + 'coder': '🔧', + 'web': '🕸️', + 'physics': '⚛️', + 'secretary': '🦄', + 'ditin': '👂', + 'watcher': '👁️', + } + + def get_agent_info(name): + """根据名称推断专长和 Emoji""" + specialty = '通用' + emoji = '🤖' + + for key, value in specialty_map.items(): + if key in name.lower(): + specialty = value + break + + for key, value in emoji_map.items(): + if key in name.lower(): + emoji = value + break + + return specialty, emoji + + # 1. 扫描 Docker 容器 + try: + client = docker.from_env() + containers = client.containers.list() + + for container in containers: + container_name = container.name + + # 只处理 openclaw 相关的容器 + if 'openclaw' not in container_name.lower(): + continue + + # 跳过数据库、网关、监控等辅助容器 + if any(skip in container_name.lower() for skip in ['postgres', 'db', 'redis', 'gateway', 'watcher', 'monitor']): + continue + + # 提取端口 + ports = container.ports + port = None + if '18789/tcp' in ports and ports['18789/tcp']: + port = ports['18789/tcp'][0]['HostPort'] + + # 推断名称(处理复杂的容器名) + name = container_name + name = name.replace('openclaw-', '').replace('openclaw_', '') + name = name.replace('-openclaw-cn-gateway-1', '').replace('_openclaw_cn_gateway_1', '') + name = name.replace('-gateway-1', '').replace('_gateway_1', '') + name = name.split('-')[0].split('_')[0].capitalize() + + specialty, emoji = get_agent_info(container_name) + + # 创建或更新 Agent + agent, created = Agent.objects.update_or_create( + container=container_name, + defaults={ + 'name': name, + 'emoji': emoji, + 'port': port or 0, + 'specialty': specialty, + 'workspace': name.lower(), + } + ) + + scanned.append({ + 'id': agent.id, + 'name': agent.name, + 'emoji': agent.emoji, + 'container': container_name, + 'port': port, + 'type': 'docker', + 'created': created, + }) + + if created: + created_count += 1 + else: + updated_count += 1 + + except docker.errors.DockerException: + pass # Docker 不可用时跳过 + except Exception as e: + print(f'Docker 扫描失败:{e}') + + # 2. 扫描宿主机进程 + try: + # 使用 ps 命令查找 openclaw 相关进程 + result = subprocess.run( + ['ps', 'aux'], + capture_output=True, + text=True, + timeout=5 + ) + + for line in result.stdout.split('\n'): + if 'openclaw' not in line.lower() or 'grep' in line.lower(): + continue + + # 提取进程信息 + parts = line.split() + if len(parts) < 11: + continue + + pid = parts[1] + + # 从命令行提取容器名或实例名 + cmd = ' '.join(parts[10:]) + + # 尝试从命令行提取名称 + container_name = None + port = None + + # 查找端口(假设端口格式为 --port 18789 或 -p 18789) + port_match = re.search(r'(?:--port|-p)[=\s]+(\d+)', cmd) + if port_match: + port = port_match.group(1) + + # 尝试从路径或参数提取名称 + name_match = re.search(r'openclaw[-_]([\w-]+)', cmd) + if name_match: + container_name = f'openclaw-{name_match.group(1)}' + + # 如果没有找到名称,使用 PID + if not container_name: + container_name = f'openclaw-process-{pid}' + + # 推断名称和专长 + name = container_name.replace('openclaw-', '').replace('openclaw_', '').capitalize() + specialty, emoji = get_agent_info(container_name) + + # 只添加有端口的进程(过滤掉石头) + if port and int(port) > 0: + # 创建或更新 Agent + agent, created = Agent.objects.update_or_create( + container=container_name, + defaults={ + 'name': name, + 'emoji': emoji, + 'port': int(port), + 'specialty': specialty, + 'workspace': name.lower(), + } + ) + + scanned.append({ + 'id': agent.id, + 'name': agent.name, + 'emoji': agent.emoji, + 'container': container_name, + 'port': port, + 'type': 'process', + 'created': created, + }) + + if created: + created_count += 1 + else: + updated_count += 1 + + except Exception as e: + print(f'进程扫描失败:{e}') + + return Response({ + 'success': True, + 'scanned': scanned, + 'docker_count': len([s for s in scanned if s.get('type') == 'docker']), + 'process_count': len([s for s in scanned if s.get('type') == 'process']), + 'created': created_count, + 'updated': updated_count, + 'total': len(scanned), + }) + +@api_view(['POST']) +def sync_agent(request): + """同步 Agent 到数据库(放虾归海)""" + try: + data = request.data + agent_id = data.get('agent_id') + action = data.get('action') + + if not agent_id: + return Response({ + 'success': False, + 'error': '缺少 agent_id', + }, status=400) + + # 获取 Agent + try: + agent = Agent.objects.get(id=agent_id) + except Agent.DoesNotExist: + return Response({ + 'success': False, + 'error': 'Agent 不存在', + }, status=404) + + # 放虾归海 - 更新状态为已同步 + if action == 'add_to_pool': + # 这里可以添加额外的同步逻辑 + # 比如标记为已激活、添加到特定分组等 + pass + + return Response({ + 'success': True, + 'message': f'{agent.name} 已放虾归海!', + 'agent': { + 'id': agent.id, + 'name': agent.name, + 'emoji': agent.emoji, + }, + }) + + except Exception as e: + return Response({ + 'success': False, + 'error': f'同步失败:{str(e)}', + }, status=500) diff --git a/code/backend/requirements.txt b/code/backend/requirements.txt index 07079d8..4f8aa34 100644 --- a/code/backend/requirements.txt +++ b/code/backend/requirements.txt @@ -1,3 +1,5 @@ -djangorestframework==3.14.0 -django-cors-headers==4.3.0 -Django==4.2.0 +Django>=4.2,<5.0 +djangorestframework>=3.14 +django-cors-headers>=4.3 +psycopg2-binary>=2.9 +docker>=6.1 diff --git a/code/frontend/src/components/LobsterPool/index.js b/code/frontend/src/components/LobsterPool/index.js new file mode 100644 index 0000000..8a52883 --- /dev/null +++ b/code/frontend/src/components/LobsterPool/index.js @@ -0,0 +1,228 @@ +import React, { useState } from 'react'; + +const API_BASE = 'http://localhost:8000/api'; + +function LobsterPool({ agents, onRefresh }) { + const [isDragging, setIsDragging] = useState(null); + const [isOverPool, setIsOverPool] = useState(false); + + // 开始拖拽 + const handleDragStart = (e, agent) => { + setIsDragging(agent); + e.dataTransfer.setData('agentId', agent.id); + e.dataTransfer.effectAllowed = 'move'; + }; + + // 拖拽结束 + const handleDragEnd = () => { + setIsDragging(null); + setIsOverPool(false); + }; + + // 拖拽进入池子 + const handleDragOver = (e) => { + e.preventDefault(); + setIsOverPool(true); + }; + + // 拖拽离开池子 + const handleDragLeave = () => { + setIsOverPool(false); + }; + + // 放入池中 + const handleDrop = async (e) => { + e.preventDefault(); + setIsOverPool(false); + + const agentId = e.dataTransfer.getData('agentId'); + const agent = agents.find(a => a.id === parseInt(agentId)); + + if (agent) { + try { + // 调用 API 同步到数据库 + const response = await fetch(`${API_BASE}/agents/sync/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + agent_id: agent.id, + action: 'add_to_pool', + }), + }); + + const data = await response.json(); + if (data.success) { + alert(`🦐 ${agent.name} 已放虾归海!`); + onRefresh(); // 刷新列表 + } + } catch (error) { + console.error('放虾失败:', error); + alert('❌ 放虾失败:' + error.message); + } + } + }; + + return ( +
+

🌊 龙虾池 - 放虾归海

+ +
+ {/* 水波纹背景 */} +
+
+
+
+
+ + {/* 池子内容 */} +
+
🌊
+

+ {isOverPool ? '🦐 松开手,放虾归海!' : '拖拽龙虾到池中'} +

+

将左侧的龙虾拖到这里,自动同步到数据库

+
+
+ + +
+ ); +} + +export default LobsterPool; diff --git a/code/frontend/src/pages/Dashboard/index.js b/code/frontend/src/pages/Dashboard/index.js index a4d53f2..cb8ffac 100644 --- a/code/frontend/src/pages/Dashboard/index.js +++ b/code/frontend/src/pages/Dashboard/index.js @@ -1,12 +1,14 @@ import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import axios from 'axios'; +// import LobsterPool from '../components/LobsterPool'; // TODO: Fix import const API_BASE = 'http://localhost:8000/api'; function Dashboard() { const [agents, setAgents] = useState([]); const [loading, setLoading] = useState(true); + const [scanning, setScanning] = useState(false); const navigate = useNavigate(); useEffect(() => { @@ -25,16 +27,65 @@ function Dashboard() { } }; + const scanAgents = async () => { + setScanning(true); + try { + const response = await axios.post(`${API_BASE}/agents/scan/`); + if (response.data.success) { + alert(`🦐 捞虾完成!\n捞到 ${response.data.total} 只虾\n新增 ${response.data.created} 只\n更新 ${response.data.updated} 只`); + fetchAgents(); // 刷新列表 + } else { + alert(`❌ 捞虾失败:${response.data.error}`); + } + } catch (error) { + console.error('捞虾失败:', error); + alert('❌ 捞虾失败:' + error.message); + } finally { + setScanning(false); + } + }; + + // 拖拽处理 + const handleDragStart = (e, agent) => { + e.dataTransfer.setData('agentId', agent.id); + e.dataTransfer.setData('agentName', agent.name); + e.target.style.opacity = '0.5'; + }; + + const handleDragEnd = (e) => { + e.target.style.opacity = '1'; + }; + if (loading) { return
加载中...
; } return (
-

🤖 Agent 舰队监控中心

+
+

🤖 Agent 舰队监控中心

+ +
+ + {/* TODO: 龙虾池功能待修复 */} + {/* */} + +

🦐 待归海的龙虾

{agents.map(agent => ( -
+
handleDragStart(e, agent)} + onDragEnd={handleDragEnd} + >
{agent.emoji} {agent.name} {agent.status} @@ -49,6 +100,7 @@ function Dashboard() { 📊 Agent 详情
+
👆 拖我到龙虾池
))}
@@ -60,18 +112,72 @@ export default Dashboard; // CSS 样式 const styles = ` +.dashboard-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; +} + +.dashboard h1 { + color: #1a365d; + margin: 0; + text-align: center; +} + +.scan-btn { + background: linear-gradient(135deg, #48bb78 0%, #38a169 100%); + color: white; + border: none; + padding: 12px 24px; + border-radius: 8px; + cursor: pointer; + font-size: 1em; + font-weight: 600; + transition: all 0.2s; + white-space: nowrap; +} + +.scan-btn:hover:not(:disabled) { + opacity: 0.9; + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(72, 187, 120, 0.4); +} + +.scan-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + .dashboard { max-width: 1400px; margin: 0 auto; padding: 20px; } +.section-title { + color: #1a365d; + margin: 30px 0 20px 0; + text-align: center; + font-size: 1.5em; +} + .dashboard h1 { color: #1a365d; margin-bottom: 30px; text-align: center; } +.drag-hint { + margin-top: 10px; + padding-top: 10px; + border-top: 1px dashed #cbd5e0; + color: #718096; + font-size: 0.85em; + text-align: center; + opacity: 0.8; +} + .agent-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));