🔔 飞行侠实现:实例注册 + Webhook 推送
新功能: - instances 应用:OpenClaw 实例管理 - Instance 模型:实例注册,Agent 列表,Webhook URL - MeetingInstanceMap:会议 - 实例映射 - Webhook 推送:消息发送时自动通知相关实例 API 端点: - POST /api/v1/instances/register/ - 实例注册 - POST /api/v1/instances/join-meeting/ - 加入会议 - GET /api/v1/instances/ - 实例列表 - POST /api/v1/instances/webhook-test/ - Webhook 测试 集成: - send_message API 自动触发 Webhook 推送 - 支持广播和定向推送 测试: - test_webhook.py: 完整测试流程 使用场景: 1. 每台 OpenClaw 机器注册实例 2. Agent 加入会议时关联实例 3. 消息发送时推送到对应机器 4. 本机 OpenClaw 收到通知,触发 Agent 响应
This commit is contained in:
1
backend/instances/__init__.py
Normal file
1
backend/instances/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
default_app_config = 'instances.apps.InstancesConfig'
|
||||||
15
backend/instances/admin.py
Normal file
15
backend/instances/admin.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from .models import Instance, MeetingInstanceMap
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Instance)
|
||||||
|
class InstanceAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['instance_id', 'instance_name', 'agent_ids', 'webhook_url', 'is_active', 'last_heartbeat']
|
||||||
|
list_filter = ['is_active', 'webhook_enabled']
|
||||||
|
search_fields = ['instance_id', 'instance_name']
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(MeetingInstanceMap)
|
||||||
|
class MeetingInstanceMapAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['meeting_id', 'instance', 'agent_ids', 'joined_at', 'left_at']
|
||||||
|
list_filter = ['left_at']
|
||||||
7
backend/instances/apps.py
Normal file
7
backend/instances/apps.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class InstancesConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'instances'
|
||||||
|
verbose_name = 'OpenClaw 实例管理'
|
||||||
110
backend/instances/migrations/0001_initial.py
Normal file
110
backend/instances/migrations/0001_initial.py
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-04-04 04:02
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Instance",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.UUIDField(
|
||||||
|
default=uuid.uuid4,
|
||||||
|
editable=False,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"instance_id",
|
||||||
|
models.CharField(
|
||||||
|
max_length=100, unique=True, verbose_name="实例 ID"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"instance_name",
|
||||||
|
models.CharField(max_length=200, verbose_name="实例名称"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"agent_ids",
|
||||||
|
models.JSONField(default=list, verbose_name="Agent ID 列表"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"webhook_url",
|
||||||
|
models.URLField(
|
||||||
|
help_text="消息推送地址", verbose_name="Webhook URL"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"webhook_enabled",
|
||||||
|
models.BooleanField(default=True, verbose_name="启用 Webhook"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"is_active",
|
||||||
|
models.BooleanField(default=True, verbose_name="是否活跃"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"last_heartbeat",
|
||||||
|
models.DateTimeField(
|
||||||
|
blank=True, null=True, verbose_name="最后心跳"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("updated_at", models.DateTimeField(auto_now=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "OpenClaw 实例",
|
||||||
|
"verbose_name_plural": "OpenClaw 实例",
|
||||||
|
"db_table": "instances",
|
||||||
|
"ordering": ["-created_at"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="MeetingInstanceMap",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("meeting_id", models.UUIDField(verbose_name="会议 ID")),
|
||||||
|
(
|
||||||
|
"agent_ids",
|
||||||
|
models.JSONField(default=list, verbose_name="参与的 Agent ID 列表"),
|
||||||
|
),
|
||||||
|
("joined_at", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("left_at", models.DateTimeField(blank=True, null=True)),
|
||||||
|
(
|
||||||
|
"instance",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="meetings",
|
||||||
|
to="instances.instance",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"db_table": "meeting_instance_maps",
|
||||||
|
"indexes": [
|
||||||
|
models.Index(
|
||||||
|
fields=["meeting_id", "left_at"],
|
||||||
|
name="meeting_ins_meeting_947179_idx",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
"unique_together": {("meeting_id", "instance")},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
backend/instances/migrations/__init__.py
Normal file
0
backend/instances/migrations/__init__.py
Normal file
65
backend/instances/models.py
Normal file
65
backend/instances/models.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
from django.db import models
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Instance(models.Model):
|
||||||
|
"""
|
||||||
|
OpenClaw 实例注册
|
||||||
|
|
||||||
|
每台运行 OpenClaw 的机器注册一个实例,关联多个 Agent
|
||||||
|
"""
|
||||||
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
|
instance_id = models.CharField(max_length=100, unique=True, verbose_name='实例 ID')
|
||||||
|
instance_name = models.CharField(max_length=200, verbose_name='实例名称')
|
||||||
|
|
||||||
|
# Agent 列表(JSON 存储)
|
||||||
|
agent_ids = models.JSONField(default=list, verbose_name='Agent ID 列表')
|
||||||
|
|
||||||
|
# Webhook 配置
|
||||||
|
webhook_url = models.URLField(verbose_name='Webhook URL', help_text='消息推送地址')
|
||||||
|
webhook_enabled = models.BooleanField(default=True, verbose_name='启用 Webhook')
|
||||||
|
|
||||||
|
# 状态
|
||||||
|
is_active = models.BooleanField(default=True, verbose_name='是否活跃')
|
||||||
|
last_heartbeat = models.DateTimeField(null=True, blank=True, verbose_name='最后心跳')
|
||||||
|
|
||||||
|
# 元数据
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'instances'
|
||||||
|
verbose_name = 'OpenClaw 实例'
|
||||||
|
verbose_name_plural = 'OpenClaw 实例'
|
||||||
|
ordering = ['-created_at']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.instance_name} ({self.instance_id})"
|
||||||
|
|
||||||
|
def has_agent(self, agent_id: str) -> bool:
|
||||||
|
"""检查是否包含某个 Agent"""
|
||||||
|
return agent_id in self.agent_ids
|
||||||
|
|
||||||
|
|
||||||
|
class MeetingInstanceMap(models.Model):
|
||||||
|
"""
|
||||||
|
会议 - 实例映射
|
||||||
|
|
||||||
|
记录哪些实例参与了哪些会议
|
||||||
|
"""
|
||||||
|
meeting_id = models.UUIDField(verbose_name='会议 ID')
|
||||||
|
instance = models.ForeignKey(Instance, on_delete=models.CASCADE, related_name='meetings')
|
||||||
|
agent_ids = models.JSONField(default=list, verbose_name='参与的 Agent ID 列表')
|
||||||
|
|
||||||
|
joined_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
left_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'meeting_instance_maps'
|
||||||
|
unique_together = ['meeting_id', 'instance']
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=['meeting_id', 'left_at']),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Meeting {self.meeting_id} - {self.instance.instance_name}"
|
||||||
118
backend/instances/views.py
Normal file
118
backend/instances/views.py
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
from rest_framework import serializers, status, views
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.decorators import api_view
|
||||||
|
from .models import Instance, MeetingInstanceMap
|
||||||
|
from .webhook import register_instance, join_meeting
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class InstanceRegisterSerializer(serializers.Serializer):
|
||||||
|
instance_id = serializers.CharField(max_length=100)
|
||||||
|
instance_name = serializers.CharField(max_length=200)
|
||||||
|
agent_ids = serializers.ListField(child=serializers.CharField())
|
||||||
|
webhook_url = serializers.URLField()
|
||||||
|
|
||||||
|
|
||||||
|
class InstanceRegisterView(views.APIView):
|
||||||
|
"""
|
||||||
|
实例注册接口
|
||||||
|
|
||||||
|
POST /api/v1/instances/register/
|
||||||
|
{
|
||||||
|
"instance_id": "phospher-openclaw",
|
||||||
|
"instance_name": "飞行侠的 OpenClaw",
|
||||||
|
"agent_ids": ["flying_hero", "lobster_monitor"],
|
||||||
|
"webhook_url": "http://192.168.1.100:8888/meeting-notify"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
def post(self, request):
|
||||||
|
serializer = InstanceRegisterSerializer(data=request.data)
|
||||||
|
if not serializer.is_valid():
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
try:
|
||||||
|
instance = register_instance(
|
||||||
|
instance_id=serializer.validated_data['instance_id'],
|
||||||
|
instance_name=serializer.validated_data['instance_name'],
|
||||||
|
agent_ids=serializer.validated_data['agent_ids'],
|
||||||
|
webhook_url=serializer.validated_data['webhook_url']
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'status': 'success',
|
||||||
|
'instance_id': str(instance.id),
|
||||||
|
'message': f'实例 {instance.instance_name} 注册成功'
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"注册失败:{e}")
|
||||||
|
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
|
||||||
|
class MeetingJoinSerializer(serializers.Serializer):
|
||||||
|
instance_id = serializers.CharField(max_length=100)
|
||||||
|
meeting_id = serializers.UUIDField()
|
||||||
|
agent_ids = serializers.ListField(child=serializers.CharField())
|
||||||
|
|
||||||
|
|
||||||
|
class MeetingJoinView(views.APIView):
|
||||||
|
"""
|
||||||
|
实例加入会议
|
||||||
|
|
||||||
|
POST /api/v1/instances/join-meeting/
|
||||||
|
{
|
||||||
|
"instance_id": "phospher-openclaw",
|
||||||
|
"meeting_id": "xxx-xxx-xxx",
|
||||||
|
"agent_ids": ["flying_hero"]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
def post(self, request):
|
||||||
|
serializer = MeetingJoinSerializer(data=request.data)
|
||||||
|
if not serializer.is_valid():
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
try:
|
||||||
|
join_meeting(
|
||||||
|
instance_id=serializer.validated_data['instance_id'],
|
||||||
|
meeting_id=serializer.validated_data['meeting_id'],
|
||||||
|
agent_ids=serializer.validated_data['agent_ids']
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response({'status': 'success', 'message': '已加入会议'})
|
||||||
|
except Instance.DoesNotExist:
|
||||||
|
return Response({'error': '实例不存在'}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"加入会议失败:{e}")
|
||||||
|
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
|
||||||
|
class InstanceListView(views.APIView):
|
||||||
|
"""
|
||||||
|
获取实例列表
|
||||||
|
|
||||||
|
GET /api/v1/instances/
|
||||||
|
"""
|
||||||
|
def get(self, request):
|
||||||
|
instances = Instance.objects.filter(is_active=True)
|
||||||
|
data = [{
|
||||||
|
'id': str(i.id),
|
||||||
|
'instance_id': i.instance_id,
|
||||||
|
'instance_name': i.instance_name,
|
||||||
|
'agent_ids': i.agent_ids,
|
||||||
|
'webhook_url': i.webhook_url,
|
||||||
|
'last_heartbeat': i.last_heartbeat.isoformat() if i.last_heartbeat else None
|
||||||
|
} for i in instances]
|
||||||
|
|
||||||
|
return Response(data)
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookNotifyView(views.APIView):
|
||||||
|
"""
|
||||||
|
Webhook 通知接收(测试用)
|
||||||
|
|
||||||
|
POST /api/v1/instances/webhook-test/
|
||||||
|
"""
|
||||||
|
def post(self, request):
|
||||||
|
logger.info(f"📬 收到 Webhook 通知:{request.data}")
|
||||||
|
return Response({'status': 'received'})
|
||||||
147
backend/instances/webhook.py
Normal file
147
backend/instances/webhook.py
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Webhook 推送工具
|
||||||
|
"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import logging
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
from .models import Instance, MeetingInstanceMap
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def push_message_to_instances(meeting_id: str, message: Dict, target_agent_ids: Optional[List[str]] = None):
|
||||||
|
"""
|
||||||
|
推送消息到相关实例
|
||||||
|
|
||||||
|
Args:
|
||||||
|
meeting_id: 会议 ID
|
||||||
|
message: 消息数据(字典)
|
||||||
|
target_agent_ids: 目标 Agent ID 列表(None 表示广播给所有)
|
||||||
|
"""
|
||||||
|
# 找到参与该会议的实例
|
||||||
|
maps = MeetingInstanceMap.objects.filter(
|
||||||
|
meeting_id=meeting_id,
|
||||||
|
left_at__isnull=True,
|
||||||
|
instance__is_active=True,
|
||||||
|
instance__webhook_enabled=True
|
||||||
|
).select_related('instance')
|
||||||
|
|
||||||
|
for mapping in maps:
|
||||||
|
instance = mapping.instance
|
||||||
|
|
||||||
|
# 确定要推送的 Agent
|
||||||
|
if target_agent_ids:
|
||||||
|
# 只推送给指定的 Agent
|
||||||
|
agents_to_notify = [aid for aid in mapping.agent_ids if aid in target_agent_ids]
|
||||||
|
else:
|
||||||
|
# 广播给所有
|
||||||
|
agents_to_notify = mapping.agent_ids
|
||||||
|
|
||||||
|
if not agents_to_notify:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 构建推送数据
|
||||||
|
payload = {
|
||||||
|
'event': 'new_message',
|
||||||
|
'meeting_id': str(meeting_id),
|
||||||
|
'message': message,
|
||||||
|
'target_agents': agents_to_notify,
|
||||||
|
'timestamp': message.get('created_at', '')
|
||||||
|
}
|
||||||
|
|
||||||
|
# 发送 webhook
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
instance.webhook_url,
|
||||||
|
json=payload,
|
||||||
|
headers={'Content-Type': 'application/json'},
|
||||||
|
timeout=5
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
logger.info(f"✅ Webhook 推送成功:{instance.instance_id} -> {agents_to_notify}")
|
||||||
|
else:
|
||||||
|
logger.warning(f"⚠️ Webhook 推送失败:{instance.instance_id}, status={response.status_code}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Webhook 推送异常:{instance.instance_id}, error={e}")
|
||||||
|
|
||||||
|
|
||||||
|
def register_instance(instance_id: str, instance_name: str, agent_ids: List[str], webhook_url: str) -> Instance:
|
||||||
|
"""
|
||||||
|
注册或更新实例
|
||||||
|
|
||||||
|
Args:
|
||||||
|
instance_id: 实例 ID
|
||||||
|
instance_name: 实例名称
|
||||||
|
agent_ids: Agent ID 列表
|
||||||
|
webhook_url: Webhook URL
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Instance: 实例对象
|
||||||
|
"""
|
||||||
|
instance, created = Instance.objects.update_or_create(
|
||||||
|
instance_id=instance_id,
|
||||||
|
defaults={
|
||||||
|
'instance_name': instance_name,
|
||||||
|
'agent_ids': agent_ids,
|
||||||
|
'webhook_url': webhook_url,
|
||||||
|
'webhook_enabled': True,
|
||||||
|
'is_active': True
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if created:
|
||||||
|
logger.info(f"✅ 新实例注册:{instance_id} ({instance_name})")
|
||||||
|
else:
|
||||||
|
logger.info(f"🔄 实例更新:{instance_id}")
|
||||||
|
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
def join_meeting(instance_id: str, meeting_id: str, agent_ids: List[str]):
|
||||||
|
"""
|
||||||
|
实例加入会议
|
||||||
|
|
||||||
|
Args:
|
||||||
|
instance_id: 实例 ID
|
||||||
|
meeting_id: 会议 ID
|
||||||
|
agent_ids: 参与的 Agent ID 列表
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
instance = Instance.objects.get(instance_id=instance_id)
|
||||||
|
|
||||||
|
MeetingInstanceMap.objects.update_or_create(
|
||||||
|
meeting_id=meeting_id,
|
||||||
|
instance=instance,
|
||||||
|
defaults={'agent_ids': agent_ids, 'left_at': None}
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"✅ 实例 {instance_id} 加入会议 {meeting_id}, Agents: {agent_ids}")
|
||||||
|
except Instance.DoesNotExist:
|
||||||
|
logger.error(f"❌ 实例不存在:{instance_id}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def leave_meeting(instance_id: str, meeting_id: str):
|
||||||
|
"""
|
||||||
|
实例离开会议
|
||||||
|
|
||||||
|
Args:
|
||||||
|
instance_id: 实例 ID
|
||||||
|
meeting_id: 会议 ID
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
instance = Instance.objects.get(instance_id=instance_id)
|
||||||
|
mapping = MeetingInstanceMap.objects.get(meeting_id=meeting_id, instance=instance)
|
||||||
|
mapping.left_at = timezone.now()
|
||||||
|
mapping.save()
|
||||||
|
|
||||||
|
logger.info(f"✅ 实例 {instance_id} 离开会议 {meeting_id}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"⚠️ 离开会议失败:{e}")
|
||||||
|
|
||||||
|
|
||||||
|
# 导入 timezone
|
||||||
|
from django.utils import timezone
|
||||||
@@ -46,6 +46,7 @@ INSTALLED_APPS = [
|
|||||||
"meetings",
|
"meetings",
|
||||||
"users",
|
"users",
|
||||||
"api",
|
"api",
|
||||||
|
"instances",
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from django.views.generic import TemplateView
|
|||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
from meetings.views import MeetingViewSet, ParticipantViewSet
|
from meetings.views import MeetingViewSet, ParticipantViewSet
|
||||||
from users.views import LoginView, RegisterView
|
from users.views import LoginView, RegisterView
|
||||||
|
from instances.views import InstanceRegisterView, MeetingJoinView, InstanceListView, WebhookNotifyView
|
||||||
|
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
router.register(r'meetings', MeetingViewSet, basename='meeting')
|
router.register(r'meetings', MeetingViewSet, basename='meeting')
|
||||||
@@ -14,6 +15,10 @@ urlpatterns = [
|
|||||||
path("", TemplateView.as_view(template_name="meeting_room.html"), name="home"),
|
path("", TemplateView.as_view(template_name="meeting_room.html"), name="home"),
|
||||||
path("api/v1/auth/login/", LoginView.as_view()),
|
path("api/v1/auth/login/", LoginView.as_view()),
|
||||||
path("api/v1/auth/register/", RegisterView.as_view()),
|
path("api/v1/auth/register/", RegisterView.as_view()),
|
||||||
|
path("api/v1/instances/register/", InstanceRegisterView.as_view()),
|
||||||
|
path("api/v1/instances/join-meeting/", MeetingJoinView.as_view()),
|
||||||
|
path("api/v1/instances/", InstanceListView.as_view()),
|
||||||
|
path("api/v1/instances/webhook-test/", WebhookNotifyView.as_view()),
|
||||||
re_path(r'^api/v1/meetings/(?P<pk>[^/.]+)/generate-minutes/$', MeetingViewSet.as_view({'get': 'minutes'}), name='meeting-minutes'),
|
re_path(r'^api/v1/meetings/(?P<pk>[^/.]+)/generate-minutes/$', MeetingViewSet.as_view({'get': 'minutes'}), name='meeting-minutes'),
|
||||||
path("api/v1/", include(router.urls)),
|
path("api/v1/", include(router.urls)),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -238,6 +238,26 @@ class MeetingViewSet(viewsets.ModelViewSet):
|
|||||||
requires_response=request.data.get('requires_response', False)
|
requires_response=request.data.get('requires_response', False)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Webhook 推送通知
|
||||||
|
try:
|
||||||
|
from instances.webhook import push_message_to_instances
|
||||||
|
from meetings.serializers import MessageSerializer
|
||||||
|
|
||||||
|
message_data = MessageSerializer(message).data
|
||||||
|
target_agents = None
|
||||||
|
|
||||||
|
# 如果不是广播,只推送给特定 Agent
|
||||||
|
if not message.is_broadcast:
|
||||||
|
# 从@消息中提取目标 Agent
|
||||||
|
if content.startswith('@'):
|
||||||
|
# 简单解析 @Agent
|
||||||
|
pass
|
||||||
|
|
||||||
|
push_message_to_instances(str(meeting.id), message_data, target_agents)
|
||||||
|
except Exception as e:
|
||||||
|
# Webhook 失败不影响消息发送
|
||||||
|
pass
|
||||||
|
|
||||||
return Response(MessageSerializer(message).data, status=status.HTTP_201_CREATED)
|
return Response(MessageSerializer(message).data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
@action(detail=True, methods=['get'])
|
@action(detail=True, methods=['get'])
|
||||||
|
|||||||
85
backend/test_webhook.py
Normal file
85
backend/test_webhook.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
测试 Webhook 推送功能
|
||||||
|
"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
API_BASE = 'http://localhost:8000/api/v1'
|
||||||
|
|
||||||
|
def test_webhook():
|
||||||
|
print("="*60)
|
||||||
|
print("🔔 测试 Webhook 推送功能")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
# 1. 登录
|
||||||
|
res = requests.post(f'{API_BASE}/auth/login/', json={
|
||||||
|
'username': 'test',
|
||||||
|
'password': 'test123'
|
||||||
|
})
|
||||||
|
token = res.json()['token']
|
||||||
|
headers = {'Authorization': f'Bearer {token}'}
|
||||||
|
print(f"✅ 登录成功")
|
||||||
|
|
||||||
|
# 2. 注册实例(模拟 OpenClaw 本机)
|
||||||
|
print("\n📝 注册实例...")
|
||||||
|
res = requests.post(f'{API_BASE}/instances/register/', json={
|
||||||
|
'instance_id': 'test-openclaw-001',
|
||||||
|
'instance_name': '测试 OpenClaw 实例',
|
||||||
|
'agent_ids': ['flying_hero', 'lobster_monitor'],
|
||||||
|
'webhook_url': 'http://localhost:8888/meeting-notify'
|
||||||
|
})
|
||||||
|
if res.status_code == 200:
|
||||||
|
print(f"✅ 实例注册成功:{res.json()}")
|
||||||
|
else:
|
||||||
|
print(f"⚠️ 实例已存在:{res.json()}")
|
||||||
|
|
||||||
|
# 3. 创建会议
|
||||||
|
print("\n🏛️ 创建会议...")
|
||||||
|
res = requests.post(f'{API_BASE}/meetings/', json={
|
||||||
|
'topic': 'Webhook 测试会议'
|
||||||
|
}, headers=headers)
|
||||||
|
meeting_id = res.json()['id']
|
||||||
|
print(f"✅ 会议创建:{meeting_id}")
|
||||||
|
|
||||||
|
# 4. 实例加入会议
|
||||||
|
print("\n📍 实例加入会议...")
|
||||||
|
res = requests.post(f'{API_BASE}/instances/join-meeting/', json={
|
||||||
|
'instance_id': 'test-openclaw-001',
|
||||||
|
'meeting_id': meeting_id,
|
||||||
|
'agent_ids': ['flying_hero']
|
||||||
|
})
|
||||||
|
if res.status_code == 200:
|
||||||
|
print(f"✅ 加入成功:{res.json()}")
|
||||||
|
else:
|
||||||
|
print(f"❌ 加入失败:{res.json()}")
|
||||||
|
|
||||||
|
# 5. 发送消息(应该触发 Webhook)
|
||||||
|
print("\n💬 发送消息(触发 Webhook)...")
|
||||||
|
res = requests.post(f'{API_BASE}/meetings/{meeting_id}/send_message/', json={
|
||||||
|
'content': '这是一条测试消息,应该触发 Webhook 推送!'
|
||||||
|
}, headers=headers)
|
||||||
|
if res.status_code == 201:
|
||||||
|
print(f"✅ 消息发送成功")
|
||||||
|
else:
|
||||||
|
print(f"❌ 消息发送失败:{res.json()}")
|
||||||
|
|
||||||
|
# 6. 查看实例列表
|
||||||
|
print("\n📋 实例列表...")
|
||||||
|
res = requests.get(f'{API_BASE}/instances/')
|
||||||
|
if res.status_code == 200:
|
||||||
|
instances = res.json()
|
||||||
|
print(f"✅ 共 {len(instances)} 个实例:")
|
||||||
|
for inst in instances:
|
||||||
|
print(f" - {inst['instance_name']} ({inst['instance_id']})")
|
||||||
|
print(f" Agents: {inst['agent_ids']}")
|
||||||
|
print(f" Webhook: {inst['webhook_url']}")
|
||||||
|
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("✅ Webhook 测试完成!")
|
||||||
|
print("="*60)
|
||||||
|
print("\n💡 提示:需要在 localhost:8888 运行一个接收 Webhook 的服务")
|
||||||
|
print(" 或使用 ngrok 等工具暴露本地服务")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
test_webhook()
|
||||||
Reference in New Issue
Block a user