Files
flying-hero 52ef5cc095 feat: 添加捞虾功能(自动扫描 Agent)🦐
🎯 新功能:
- 捞虾按钮:自动扫描 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)

🦸 感谢北极星  的'捞虾'命名灵感!
2026-04-03 20:35:06 +08:00

391 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
API views for Agent Diary monitoring.
"""
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):
"""获取所有 Agent 状态"""
agents = Agent.objects.all()
result = []
for agent in agents:
result.append({
'id': agent.id,
'name': agent.name,
'emoji': agent.emoji,
'port': agent.port,
'specialty': agent.specialty,
'container': agent.container,
'app_name': agent.app_name,
'app_id': agent.app_id,
'status': 'healthy',
'last_check': datetime.now().isoformat()
})
return Response(result)
@api_view(['GET'])
def agent_detail(request, agent_id):
"""获取单个 Agent 详情"""
try:
agent = Agent.objects.get(id=agent_id)
return Response({
'id': agent.id,
'name': agent.name,
'emoji': agent.emoji,
'port': agent.port,
'specialty': agent.specialty,
'container': agent.container,
'app_name': agent.app_name,
'app_id': agent.app_id,
'status': 'healthy',
'workspace': f'/home/node/.openclaw/workspace/{agent.workspace}',
'last_check': datetime.now().isoformat()
})
except Agent.DoesNotExist:
return Response({'error': 'Agent 不存在'}, status=404)
@api_view(['GET'])
def tools_list(request):
"""获取工具列表"""
tools = [
{
'name': 'Git 版本控制',
'status': 'running',
'description': '代码版本管理服务',
'url': 'https://xjp.datalibstar.com/flying-hero/openclaw-monitor.git'
}
]
return Response(tools)
@api_view(['GET'])
def agent_memory_dates(request, agent_id):
"""获取 Agent 有工作记忆的日期列表 - 从数据库读取"""
try:
agent = Agent.objects.get(id=agent_id)
except Agent.DoesNotExist:
return Response({'error': 'Agent 不存在'}, status=404)
# 从数据库查询工作记忆
diaries = AgentDiary.objects.filter(
agent=agent,
category='memory'
).values_list('date', flat=True).distinct().order_by('-date')
dates = [str(date) for date in sorted(diaries, reverse=True)]
return Response({'dates': dates})
@api_view(['GET'])
def agent_memory_detail(request, agent_id, date):
"""获取指定日期的工作记忆内容 - 从数据库读取"""
try:
agent = Agent.objects.get(id=agent_id)
except Agent.DoesNotExist:
return Response({'error': 'Agent 不存在'}, status=404)
# 从数据库查询工作记忆
try:
diary = AgentDiary.objects.get(
agent=agent,
date=date,
category='memory'
)
return Response({
'date': str(diary.date),
'content': diary.content,
'title': diary.title,
'tags': diary.tags,
})
except AgentDiary.DoesNotExist:
return Response({'error': '该日期没有工作记忆'}, status=404)
@api_view(['GET'])
def agent_diary_dates(request, agent_id):
"""获取 Agent 有日记(成长之路)的日期列表 - 从数据库读取"""
try:
agent = Agent.objects.get(id=agent_id)
except Agent.DoesNotExist:
return Response({'error': 'Agent 不存在'}, status=404)
# 从数据库查询日记日期
diaries = AgentDiary.objects.filter(
agent=agent,
category='chengcai'
).values_list('date', flat=True).distinct().order_by('-date')
dates = [str(date) for date in sorted(diaries, reverse=True)]
return Response({'dates': dates})
@api_view(['GET'])
def agent_diary_detail(request, agent_id, date):
"""获取指定日期的日记内容(成长之路) - 从数据库读取"""
try:
agent = Agent.objects.get(id=agent_id)
except Agent.DoesNotExist:
return Response({'error': 'Agent 不存在'}, status=404)
# 从数据库查询日记
try:
diary = AgentDiary.objects.get(
agent=agent,
date=date,
category='chengcai'
)
return Response({
'date': str(diary.date),
'content': diary.content,
'title': diary.title,
'tags': diary.tags,
})
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)