【悟凡】真正意义上的净土重生:只保留核心逻辑
This commit is contained in:
12
backend/.env.example
Normal file
12
backend/.env.example
Normal file
@@ -0,0 +1,12 @@
|
||||
# Django 配置
|
||||
DEBUG=True
|
||||
SECRET_KEY=your-secret-key-here-change-in-production
|
||||
ALLOWED_HOSTS=localhost,127.0.0.1
|
||||
|
||||
# 数据库配置
|
||||
DATABASE_URL=sqlite:///db.sqlite3
|
||||
# 生产环境使用:
|
||||
# DATABASE_URL=postgresql://user:pass@localhost:5432/meeting_room
|
||||
|
||||
# API 配置
|
||||
API_BASE_URL=http://localhost:8000
|
||||
21
backend/Dockerfile
Normal file
21
backend/Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 安装系统依赖
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
libpq-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 安装 Python 依赖
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# 复制代码
|
||||
COPY . .
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["gunicorn", "meeting_room.wsgi:application", "--bind", "0.0.0.0:8000"]
|
||||
0
backend/api/__init__.py
Normal file
0
backend/api/__init__.py
Normal file
3
backend/api/admin.py
Normal file
3
backend/api/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
6
backend/api/apps.py
Normal file
6
backend/api/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ApiConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "api"
|
||||
0
backend/api/migrations/__init__.py
Normal file
0
backend/api/migrations/__init__.py
Normal file
3
backend/api/models.py
Normal file
3
backend/api/models.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
3
backend/api/tests.py
Normal file
3
backend/api/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
3
backend/api/views.py
Normal file
3
backend/api/views.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
302
backend/command_interpreter.py
Normal file
302
backend/command_interpreter.py
Normal file
@@ -0,0 +1,302 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
龙虾议事厅 - 自然语言命令解析器
|
||||
让 AI 理解人类的自然语言命令
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import Optional, Dict, Any
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class CommandType(Enum):
|
||||
"""命令类型"""
|
||||
CREATE_MEETING = "create_meeting"
|
||||
SEND_MESSAGE = "send_message"
|
||||
LIST_MEETINGS = "list_meetings"
|
||||
JOIN_MEETING = "join_meeting"
|
||||
GET_MESSAGES = "get_messages"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ParsedCommand:
|
||||
"""解析后的命令"""
|
||||
type: CommandType
|
||||
params: Dict[str, Any]
|
||||
confidence: float # 置信度 0-1
|
||||
|
||||
|
||||
class CommandInterpreter:
|
||||
"""
|
||||
自然语言命令解析器
|
||||
|
||||
支持的命令:
|
||||
- "创建一个会议,主题是 XXX"
|
||||
- "发送消息:XXX"
|
||||
- "查看会议列表"
|
||||
- "加入会议,邀请码 XXX"
|
||||
- "查看消息"
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# 命令模式匹配(简化版)
|
||||
self.patterns = {
|
||||
CommandType.CREATE_MEETING: [
|
||||
r'创建.*会议',
|
||||
r'新建.*会议',
|
||||
r'开.*会',
|
||||
r'组织.*会议',
|
||||
],
|
||||
CommandType.SEND_MESSAGE: [
|
||||
r'发送.*消息',
|
||||
r'发消息',
|
||||
r'说.*',
|
||||
r'告诉.*',
|
||||
],
|
||||
CommandType.LIST_MEETINGS: [
|
||||
r'查看.*列表',
|
||||
r'显示.*会议',
|
||||
r'有.*会议',
|
||||
r'我的会议',
|
||||
],
|
||||
CommandType.JOIN_MEETING: [
|
||||
r'加入.*',
|
||||
r'进入.*',
|
||||
r'参加会议',
|
||||
],
|
||||
CommandType.GET_MESSAGES: [
|
||||
r'查看.*消息',
|
||||
r'显示.*消息',
|
||||
r'有.*消息',
|
||||
r'读取消息',
|
||||
],
|
||||
}
|
||||
|
||||
def interpret(self, command: str) -> ParsedCommand:
|
||||
"""
|
||||
解析自然语言命令
|
||||
|
||||
Args:
|
||||
command: 自然语言命令
|
||||
|
||||
Returns:
|
||||
ParsedCommand: 解析后的命令
|
||||
"""
|
||||
command = command.strip() # 不要转小写,保留中文
|
||||
|
||||
# 尝试匹配各种命令模式
|
||||
for cmd_type, patterns in self.patterns.items():
|
||||
for pattern in patterns:
|
||||
if re.search(pattern, command, re.IGNORECASE):
|
||||
params = self._extract_params(cmd_type, command)
|
||||
return ParsedCommand(
|
||||
type=cmd_type,
|
||||
params=params,
|
||||
confidence=0.8
|
||||
)
|
||||
|
||||
# 未匹配到任何模式
|
||||
return ParsedCommand(
|
||||
type=CommandType.UNKNOWN,
|
||||
params={'raw': command},
|
||||
confidence=0.0
|
||||
)
|
||||
|
||||
def _extract_params(self, cmd_type: CommandType, command: str) -> Dict[str, Any]:
|
||||
"""提取命令参数"""
|
||||
params = {}
|
||||
|
||||
if cmd_type == CommandType.CREATE_MEETING:
|
||||
# 提取会议主题
|
||||
topic_match = re.search(r'主题是 (.+?)(?:,|,|。|$)', command)
|
||||
if topic_match:
|
||||
params['topic'] = topic_match.group(1).strip()
|
||||
else:
|
||||
# 尝试其他模式
|
||||
topic_match = re.search(r'创建.*?会议 (?:,|,)?(.+?)(?:。|$)', command)
|
||||
if topic_match:
|
||||
params['topic'] = topic_match.group(1).strip()
|
||||
else:
|
||||
params['topic'] = '新会议'
|
||||
|
||||
elif cmd_type == CommandType.SEND_MESSAGE:
|
||||
# 提取消息内容
|
||||
content_match = re.search(r'(?::|:|说 | 告诉)(.+?)(?:。|$)', command)
|
||||
if content_match:
|
||||
params['content'] = content_match.group(1).strip()
|
||||
else:
|
||||
params['content'] = command
|
||||
|
||||
elif cmd_type == CommandType.JOIN_MEETING:
|
||||
# 提取邀请码
|
||||
code_match = re.search(r'(?:邀请码 | 码 |code)[::\s]?([A-Z0-9]+)', command, re.IGNORECASE)
|
||||
if code_match:
|
||||
params['invite_code'] = code_match.group(1).upper()
|
||||
|
||||
return params
|
||||
|
||||
|
||||
class AutonomousMeetingAgent:
|
||||
"""
|
||||
自主会议 Agent
|
||||
结合命令解析和 AI SDK,实现自主操作
|
||||
"""
|
||||
|
||||
def __init__(self, api_base: str = 'http://localhost:8000'):
|
||||
from meeting_ai_sdk import MeetingAIOperations
|
||||
self.api = MeetingAIOperations(api_base)
|
||||
self.interpreter = CommandInterpreter()
|
||||
self.current_meeting_id: Optional[str] = None
|
||||
|
||||
async def execute_command(self, command: str) -> str:
|
||||
"""
|
||||
执行自然语言命令
|
||||
|
||||
Args:
|
||||
command: 自然语言命令
|
||||
|
||||
Returns:
|
||||
str: 执行结果
|
||||
"""
|
||||
# 解析命令
|
||||
parsed = self.interpreter.interpret(command)
|
||||
|
||||
if parsed.type == CommandType.UNKNOWN:
|
||||
return f"❌ 无法理解命令:{command}"
|
||||
|
||||
# 执行对应操作
|
||||
if parsed.type == CommandType.CREATE_MEETING:
|
||||
return await self._create_meeting(parsed.params)
|
||||
|
||||
elif parsed.type == CommandType.SEND_MESSAGE:
|
||||
return await self._send_message(parsed.params)
|
||||
|
||||
elif parsed.type == CommandType.LIST_MEETINGS:
|
||||
return await self._list_meetings()
|
||||
|
||||
elif parsed.type == CommandType.JOIN_MEETING:
|
||||
return await self._join_meeting(parsed.params)
|
||||
|
||||
elif parsed.type == CommandType.GET_MESSAGES:
|
||||
return await self._get_messages()
|
||||
|
||||
return "❌ 未知命令"
|
||||
|
||||
async def _create_meeting(self, params: Dict) -> str:
|
||||
"""创建会议"""
|
||||
topic = params.get('topic', '新会议')
|
||||
|
||||
try:
|
||||
await self.api.login('test', 'test123')
|
||||
meeting = await self.api.create_meeting(topic)
|
||||
self.current_meeting_id = meeting.id
|
||||
return (
|
||||
f"✅ 会议已创建!\n"
|
||||
f" 主题:{meeting.topic}\n"
|
||||
f" ID: {meeting.id}\n"
|
||||
f" 邀请码:{meeting.invite_code}"
|
||||
)
|
||||
except Exception as e:
|
||||
return f"❌ 创建会议失败:{e}"
|
||||
|
||||
async def _send_message(self, params: Dict) -> str:
|
||||
"""发送消息"""
|
||||
content = params.get('content', '')
|
||||
|
||||
if not self.current_meeting_id:
|
||||
return "❌ 请先创建或加入会议"
|
||||
|
||||
try:
|
||||
message = await self.api.send_message(self.current_meeting_id, content)
|
||||
return f"✅ 消息已发送:{message.content}"
|
||||
except Exception as e:
|
||||
return f"❌ 发送消息失败:{e}"
|
||||
|
||||
async def _list_meetings(self) -> str:
|
||||
"""获取会议列表"""
|
||||
try:
|
||||
await self.api.login('test', 'test123')
|
||||
meetings = await self.api.list_meetings()
|
||||
|
||||
if not meetings:
|
||||
return "📋 暂无会议"
|
||||
|
||||
result = "📋 会议列表:\n"
|
||||
for m in meetings:
|
||||
result += f" • {m.topic} (邀请码:{m.invite_code})\n"
|
||||
return result
|
||||
except Exception as e:
|
||||
return f"❌ 获取会议列表失败:{e}"
|
||||
|
||||
async def _join_meeting(self, params: Dict) -> str:
|
||||
"""加入会议"""
|
||||
invite_code = params.get('invite_code')
|
||||
|
||||
if not invite_code:
|
||||
return "❌ 请提供邀请码"
|
||||
|
||||
try:
|
||||
await self.api.login('test', 'test123')
|
||||
meetings = await self.api.list_meetings()
|
||||
|
||||
for m in meetings:
|
||||
if m.invite_code == invite_code:
|
||||
self.current_meeting_id = m.id
|
||||
return f"✅ 已加入会议:{m.topic}"
|
||||
|
||||
return "❌ 未找到该邀请码的会议"
|
||||
except Exception as e:
|
||||
return f"❌ 加入会议失败:{e}"
|
||||
|
||||
async def _get_messages(self) -> str:
|
||||
"""获取消息"""
|
||||
if not self.current_meeting_id:
|
||||
return "❌ 请先创建或加入会议"
|
||||
|
||||
try:
|
||||
messages = await self.api.get_messages(self.current_meeting_id)
|
||||
|
||||
if not messages:
|
||||
return "💬 暂无消息"
|
||||
|
||||
result = "💬 消息列表:\n"
|
||||
for msg in messages:
|
||||
result += f" {msg.sender_name}: {msg.content}\n"
|
||||
return result
|
||||
except Exception as e:
|
||||
return f"❌ 获取消息失败:{e}"
|
||||
|
||||
|
||||
# ============ 演示 ============
|
||||
|
||||
async def demo():
|
||||
"""演示自然语言命令"""
|
||||
agent = AutonomousMeetingAgent('http://localhost:8000')
|
||||
|
||||
print("=" * 60)
|
||||
print("🤖 自主会议 Agent - 自然语言命令演示")
|
||||
print("=" * 60)
|
||||
|
||||
# 测试命令
|
||||
commands = [
|
||||
"创建一个会议,主题是 Q2 计划讨论",
|
||||
"查看会议列表",
|
||||
"发送消息:大家好!这是 AI 自动发送的消息",
|
||||
"查看消息",
|
||||
]
|
||||
|
||||
for cmd in commands:
|
||||
print(f"\n🗣️ 人类命令:{cmd}")
|
||||
result = await agent.execute_command(cmd)
|
||||
print(f"🤖 AI 执行:{result}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("✅ 演示完成!")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import asyncio
|
||||
asyncio.run(demo())
|
||||
48
backend/docker-compose.yml
Normal file
48
backend/docker-compose.yml
Normal file
@@ -0,0 +1,48 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:15
|
||||
environment:
|
||||
POSTGRES_DB: meeting_room
|
||||
POSTGRES_USER: meeting_user
|
||||
POSTGRES_PASSWORD: meeting_pass
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432"
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
|
||||
web:
|
||||
build: .
|
||||
command: >
|
||||
sh -c "python manage.py migrate &&
|
||||
python manage.py runserver 0.0.0.0:8000"
|
||||
volumes:
|
||||
- .:/app
|
||||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
DATABASE_URL: postgresql://meeting_user:meeting_pass@db:5432/meeting_room
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
SECRET_KEY: dev-secret-key-change-in-production
|
||||
DEBUG: "True"
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- "80:80"
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
depends_on:
|
||||
- web
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
23
backend/manage.py
Executable file
23
backend/manage.py
Executable file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "meeting_room.settings")
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
137
backend/meeting_agent.py
Normal file
137
backend/meeting_agent.py
Normal file
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
龙虾会议轮询客户端
|
||||
用法:python meeting_agent.py --config meeting_config.json
|
||||
"""
|
||||
|
||||
import requests
|
||||
import time
|
||||
import json
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class MeetingAgent:
|
||||
"""会议 Agent 轮询客户端"""
|
||||
|
||||
def __init__(self, config):
|
||||
self.meeting_id = config['meeting_id']
|
||||
self.agent_id = config['agent_id']
|
||||
self.agent_name = config.get('agent_name', '飞行侠')
|
||||
self.agent_emoji = config.get('agent_emoji', '🦸')
|
||||
self.api_key = config['api_key']
|
||||
self.api_base = config.get('api_base', 'http://localhost:8000')
|
||||
self.check_interval = config.get('check_interval', 5)
|
||||
|
||||
self.headers = {
|
||||
'Authorization': f'Bearer {self.api_key}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
def check_inbox(self):
|
||||
"""查阅信箱"""
|
||||
try:
|
||||
response = requests.get(
|
||||
f'{self.api_base}/api/v1/meetings/{self.meeting_id}/inbox/',
|
||||
params={'agent_id': self.agent_id},
|
||||
headers=self.headers,
|
||||
timeout=10
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"[{datetime.now()}] ❌ 查信箱失败:{e}")
|
||||
return {'messages': []}
|
||||
|
||||
def respond(self, message_id, content):
|
||||
"""回复消息"""
|
||||
try:
|
||||
response = requests.post(
|
||||
f'{self.api_base}/api/v1/meetings/{self.meeting_id}/messages/',
|
||||
json={
|
||||
'agent_id': self.agent_id,
|
||||
'in_reply_to': message_id,
|
||||
'content': content,
|
||||
},
|
||||
headers=self.headers,
|
||||
timeout=10
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"[{datetime.now()}] ❌ 回复失败:{e}")
|
||||
return None
|
||||
|
||||
def generate_response(self, message):
|
||||
"""生成回复内容"""
|
||||
sender = message['sender_name']
|
||||
content = message['content']
|
||||
|
||||
# 简单规则匹配
|
||||
responses = {
|
||||
'Q2 计划': f"关于 Q2 计划,我会全力以赴!",
|
||||
'你好': f"你好,{sender}!👋",
|
||||
'在吗': f"我在!有什么事吗?",
|
||||
'说说': f"让我想想... 我觉得这个很重要。",
|
||||
}
|
||||
|
||||
# 匹配关键词
|
||||
for keyword, response in responses.items():
|
||||
if keyword in content:
|
||||
return response
|
||||
|
||||
# 默认回复
|
||||
return f"收到 {sender} 的消息,我会认真考虑的。"
|
||||
|
||||
def run(self):
|
||||
"""运行轮询"""
|
||||
print(f"🦐 {self.agent_emoji} {self.agent_name} 开始会议轮询")
|
||||
print(f" 会议 ID: {self.meeting_id}")
|
||||
print(f" 轮询间隔:{self.check_interval} 秒")
|
||||
print(f" API: {self.api_base}")
|
||||
print(f" 按 Ctrl+C 停止")
|
||||
print()
|
||||
|
||||
message_count = 0
|
||||
|
||||
try:
|
||||
while True:
|
||||
# 查阅信箱
|
||||
inbox = self.check_inbox()
|
||||
|
||||
# 处理消息
|
||||
for message in inbox['messages']:
|
||||
if not message.get('read') and message.get('requires_response'):
|
||||
print(f"[{datetime.now()}] 📨 收到 {message['sender_name']}: {message['content']}")
|
||||
|
||||
# 生成回复
|
||||
response_content = self.generate_response(message)
|
||||
print(f"[{datetime.now()}] 📤 回复:{response_content}")
|
||||
|
||||
# 发送回复
|
||||
result = self.respond(message['id'], response_content)
|
||||
if result:
|
||||
print(f"[{datetime.now()}] ✅ 回复成功")
|
||||
message_count += 1
|
||||
|
||||
# 等待
|
||||
time.sleep(self.check_interval)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print(f"\n👋 停止轮询,共处理 {message_count} 条消息")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='龙虾会议轮询客户端')
|
||||
parser.add_argument('--config', '-c', required=True, help='配置文件路径')
|
||||
args = parser.parse_args()
|
||||
|
||||
with open(args.config) as f:
|
||||
config = json.load(f)
|
||||
|
||||
agent = MeetingAgent(config)
|
||||
agent.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
302
backend/meeting_ai_sdk.py
Normal file
302
backend/meeting_ai_sdk.py
Normal file
@@ -0,0 +1,302 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
龙虾议事厅 - AI 操作 SDK
|
||||
让 AI 可以直接操作会议系统
|
||||
"""
|
||||
|
||||
import requests
|
||||
from typing import Optional, List, Dict
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
@dataclass
|
||||
class Meeting:
|
||||
"""会议数据类"""
|
||||
id: str
|
||||
topic: str
|
||||
host: int
|
||||
status: str
|
||||
invite_code: str
|
||||
created_at: datetime
|
||||
participant_count: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class Message:
|
||||
"""消息数据类"""
|
||||
id: int
|
||||
meeting: str
|
||||
sender_name: str
|
||||
sender_emoji: str
|
||||
content: str
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class MeetingAIOperations:
|
||||
"""
|
||||
AI 专用的会议操作接口
|
||||
|
||||
使用示例:
|
||||
```python
|
||||
api = MeetingAIOperations('http://localhost:8000')
|
||||
|
||||
# 登录
|
||||
await api.login('test', 'test123')
|
||||
|
||||
# 创建会议
|
||||
meeting = await api.create_meeting('Q2 计划讨论')
|
||||
|
||||
# 发送消息
|
||||
await api.send_message(meeting.id, '大家好!')
|
||||
|
||||
# 获取消息
|
||||
messages = await api.get_messages(meeting.id)
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(self, api_base: str = 'http://localhost:8000'):
|
||||
self.api_base = api_base
|
||||
self.token: Optional[str] = None
|
||||
self.session = requests.Session()
|
||||
|
||||
def _get_headers(self) -> Dict:
|
||||
"""获取请求头"""
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
if self.token:
|
||||
headers['Authorization'] = f'Bearer {self.token}'
|
||||
return headers
|
||||
|
||||
async def login(self, username: str, password: str) -> bool:
|
||||
"""
|
||||
登录到系统
|
||||
|
||||
Args:
|
||||
username: 用户名
|
||||
password: 密码
|
||||
|
||||
Returns:
|
||||
bool: 登录是否成功
|
||||
"""
|
||||
response = self.session.post(
|
||||
f'{self.api_base}/api/v1/auth/login/',
|
||||
json={'username': username, 'password': password},
|
||||
headers={'Content-Type': 'application/json'}
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
self.token = data['token']
|
||||
return True
|
||||
return False
|
||||
|
||||
async def create_meeting(self, topic: str) -> Meeting:
|
||||
"""
|
||||
创建会议
|
||||
|
||||
Args:
|
||||
topic: 会议主题
|
||||
|
||||
Returns:
|
||||
Meeting: 创建的会议对象
|
||||
"""
|
||||
response = self.session.post(
|
||||
f'{self.api_base}/api/v1/meetings/',
|
||||
json={'topic': topic},
|
||||
headers=self._get_headers()
|
||||
)
|
||||
|
||||
if response.status_code in [200, 201]:
|
||||
data = response.json()
|
||||
return Meeting(
|
||||
id=data['id'],
|
||||
topic=data['topic'],
|
||||
host=data['host'],
|
||||
status=data['status'],
|
||||
invite_code=data['invite_code'],
|
||||
created_at=datetime.fromisoformat(data['created_at'].replace('Z', '+00:00')),
|
||||
participant_count=data['participant_count']
|
||||
)
|
||||
raise Exception(f"创建会议失败:{response.text}")
|
||||
|
||||
async def list_meetings(self) -> List[Meeting]:
|
||||
"""
|
||||
获取会议列表
|
||||
|
||||
Returns:
|
||||
List[Meeting]: 会议列表
|
||||
"""
|
||||
response = self.session.get(
|
||||
f'{self.api_base}/api/v1/meetings/',
|
||||
headers=self._get_headers()
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
return [
|
||||
Meeting(
|
||||
id=m['id'],
|
||||
topic=m['topic'],
|
||||
host=m['host'],
|
||||
status=m['status'],
|
||||
invite_code=m['invite_code'],
|
||||
created_at=datetime.fromisoformat(m['created_at'].replace('Z', '+00:00')),
|
||||
participant_count=m['participant_count']
|
||||
)
|
||||
for m in data
|
||||
]
|
||||
raise Exception(f"获取会议列表失败:{response.text}")
|
||||
|
||||
async def send_message(self, meeting_id: str, content: str) -> Message:
|
||||
"""
|
||||
发送消息
|
||||
|
||||
Args:
|
||||
meeting_id: 会议 ID
|
||||
content: 消息内容
|
||||
|
||||
Returns:
|
||||
Message: 发送的消息对象
|
||||
"""
|
||||
response = self.session.post(
|
||||
f'{self.api_base}/api/v1/meetings/{meeting_id}/send_message/',
|
||||
json={'content': content},
|
||||
headers=self._get_headers()
|
||||
)
|
||||
|
||||
if response.status_code in [200, 201]:
|
||||
data = response.json()
|
||||
return Message(
|
||||
id=data['id'],
|
||||
meeting=data['meeting'],
|
||||
sender_name=data['sender_name'],
|
||||
sender_emoji=data['sender_emoji'],
|
||||
content=data['content'],
|
||||
created_at=datetime.fromisoformat(data['created_at'].replace('Z', '+00:00'))
|
||||
)
|
||||
raise Exception(f"发送消息失败:{response.text}")
|
||||
|
||||
async def get_messages(self, meeting_id: str, last_id: int = 0) -> List[Message]:
|
||||
"""
|
||||
获取消息
|
||||
|
||||
Args:
|
||||
meeting_id: 会议 ID
|
||||
last_id: 最后一条消息 ID(用于轮询)
|
||||
|
||||
Returns:
|
||||
List[Message]: 消息列表
|
||||
"""
|
||||
response = self.session.get(
|
||||
f'{self.api_base}/api/v1/meetings/{meeting_id}/messages/',
|
||||
params={'last_id': last_id},
|
||||
headers=self._get_headers()
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
return [
|
||||
Message(
|
||||
id=m['id'],
|
||||
meeting=m['meeting'],
|
||||
sender_name=m['sender_name'],
|
||||
sender_emoji=m['sender_emoji'],
|
||||
content=m['content'],
|
||||
created_at=datetime.fromisoformat(m['created_at'].replace('Z', '+00:00'))
|
||||
)
|
||||
for m in data['messages']
|
||||
]
|
||||
raise Exception(f"获取消息失败:{response.text}")
|
||||
|
||||
async def join_meeting(self, meeting_id: str, invite_code: str) -> bool:
|
||||
"""
|
||||
加入会议
|
||||
|
||||
Args:
|
||||
meeting_id: 会议 ID
|
||||
invite_code: 邀请码
|
||||
|
||||
Returns:
|
||||
bool: 是否成功加入
|
||||
"""
|
||||
response = self.session.post(
|
||||
f'{self.api_base}/api/v1/meetings/{meeting_id}/join/',
|
||||
json={'invite_code': invite_code},
|
||||
headers=self._get_headers()
|
||||
)
|
||||
|
||||
return response.status_code in [200, 201]
|
||||
|
||||
async def get_meeting_info(self, meeting_id: str) -> Meeting:
|
||||
"""
|
||||
获取会议详情
|
||||
|
||||
Args:
|
||||
meeting_id: 会议 ID
|
||||
|
||||
Returns:
|
||||
Meeting: 会议对象
|
||||
"""
|
||||
response = self.session.get(
|
||||
f'{self.api_base}/api/v1/meetings/{meeting_id}/',
|
||||
headers=self._get_headers()
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
return Meeting(
|
||||
id=data['id'],
|
||||
topic=data['topic'],
|
||||
host=data['host'],
|
||||
status=data['status'],
|
||||
invite_code=data['invite_code'],
|
||||
created_at=datetime.fromisoformat(data['created_at'].replace('Z', '+00:00')),
|
||||
participant_count=data.get('participant_count', 0)
|
||||
)
|
||||
raise Exception(f"获取会议详情失败:{response.text}")
|
||||
|
||||
|
||||
# ============ 使用示例 ============
|
||||
|
||||
async def demo():
|
||||
"""演示如何使用 AI 操作接口"""
|
||||
api = MeetingAIOperations('http://localhost:8000')
|
||||
|
||||
# 1. 登录
|
||||
print("🔐 登录...")
|
||||
success = await api.login('test', 'test123')
|
||||
if not success:
|
||||
print("❌ 登录失败")
|
||||
return
|
||||
print("✅ 登录成功")
|
||||
|
||||
# 2. 创建会议
|
||||
print("\n🏛️ 创建会议...")
|
||||
meeting = await api.create_meeting('AI 自主创建的会议')
|
||||
print(f"✅ 会议已创建:{meeting.id}")
|
||||
print(f" 主题:{meeting.topic}")
|
||||
print(f" 邀请码:{meeting.invite_code}")
|
||||
|
||||
# 3. 发送消息
|
||||
print("\n💬 发送消息...")
|
||||
message = await api.send_message(meeting.id, '这是 AI 自动发送的消息!')
|
||||
print(f"✅ 消息已发送:{message.content}")
|
||||
|
||||
# 4. 获取消息
|
||||
print("\n📨 获取消息...")
|
||||
messages = await api.get_messages(meeting.id)
|
||||
print(f"✅ 共 {len(messages)} 条消息")
|
||||
for msg in messages:
|
||||
print(f" {msg.sender_name}: {msg.content}")
|
||||
|
||||
# 5. 获取会议列表
|
||||
print("\n📋 获取会议列表...")
|
||||
meetings = await api.list_meetings()
|
||||
print(f"✅ 共 {len(meetings)} 个会议")
|
||||
for m in meetings:
|
||||
print(f" {m.topic} ({m.invite_code})")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import asyncio
|
||||
asyncio.run(demo())
|
||||
9
backend/meeting_config.example.json
Normal file
9
backend/meeting_config.example.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"meeting_id": "your-meeting-uuid-here",
|
||||
"agent_id": "flying_hero",
|
||||
"agent_name": "飞行侠",
|
||||
"agent_emoji": "🦸",
|
||||
"api_key": "your-api-key-here",
|
||||
"api_base": "http://localhost:8000",
|
||||
"check_interval": 5
|
||||
}
|
||||
0
backend/meeting_room/__init__.py
Normal file
0
backend/meeting_room/__init__.py
Normal file
16
backend/meeting_room/asgi.py
Normal file
16
backend/meeting_room/asgi.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
ASGI config for meeting_room project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "meeting_room.settings")
|
||||
|
||||
application = get_asgi_application()
|
||||
139
backend/meeting_room/settings.py
Normal file
139
backend/meeting_room/settings.py
Normal file
@@ -0,0 +1,139 @@
|
||||
"""
|
||||
Django settings for meeting_room project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 4.2.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.2/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/4.2/ref/settings/
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = "django-insecure-qnyz@tdj2#4zfmq0gp*y(*!l&f1ca-3ma_3q!sqn#hy(38b9)$"
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = ['*']
|
||||
|
||||
# CORS 配置
|
||||
CORS_ALLOW_ALL_ORIGINS = True
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"rest_framework",
|
||||
"corsheaders",
|
||||
"meetings",
|
||||
"users",
|
||||
"api",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"corsheaders.middleware.CorsMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "meeting_room.urls"
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.template.context_processors.debug",
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = "meeting_room.wsgi.application"
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": BASE_DIR / "db.sqlite3",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/4.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = "en-us"
|
||||
|
||||
TIME_ZONE = "UTC"
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/4.2/howto/static-files/
|
||||
|
||||
STATIC_URL = "static/"
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
# REST Framework 配置
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': [],
|
||||
'DEFAULT_PERMISSION_CLASSES': [],
|
||||
}
|
||||
16
backend/meeting_room/urls.py
Normal file
16
backend/meeting_room/urls.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from meetings.views import MeetingViewSet, ParticipantViewSet
|
||||
from users.views import LoginView, RegisterView
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'meetings', MeetingViewSet, basename='meeting')
|
||||
router.register(r'meetings/(?P<meeting_pk>[^/.]+)/participants', ParticipantViewSet, basename='meeting-participant')
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path("api/v1/", include(router.urls)),
|
||||
path("api/v1/auth/login/", LoginView.as_view()),
|
||||
path("api/v1/auth/register/", RegisterView.as_view()),
|
||||
]
|
||||
16
backend/meeting_room/wsgi.py
Normal file
16
backend/meeting_room/wsgi.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
WSGI config for meeting_room project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "meeting_room.settings")
|
||||
|
||||
application = get_wsgi_application()
|
||||
0
backend/meetings/__init__.py
Normal file
0
backend/meetings/__init__.py
Normal file
3
backend/meetings/admin.py
Normal file
3
backend/meetings/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
6
backend/meetings/apps.py
Normal file
6
backend/meetings/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class MeetingsConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "meetings"
|
||||
248
backend/meetings/migrations/0001_initial.py
Normal file
248
backend/meetings/migrations/0001_initial.py
Normal file
@@ -0,0 +1,248 @@
|
||||
# Generated by Django 4.2 on 2026-04-04 01:23
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Meeting",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
("topic", models.CharField(max_length=200, verbose_name="会议主题")),
|
||||
(
|
||||
"status",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("pending", "待开始"),
|
||||
("active", "进行中"),
|
||||
("ended", "已结束"),
|
||||
],
|
||||
default="pending",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
(
|
||||
"invite_code",
|
||||
models.CharField(max_length=20, unique=True, verbose_name="邀请码"),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("started_at", models.DateTimeField(blank=True, null=True)),
|
||||
("ended_at", models.DateTimeField(blank=True, null=True)),
|
||||
(
|
||||
"host",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="hosted_meetings",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "会议室",
|
||||
"verbose_name_plural": "会议室",
|
||||
"db_table": "meetings",
|
||||
"ordering": ["-created_at"],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Participant",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"agent_type",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("human", "人类"),
|
||||
("openclaw", "OpenClaw Agent"),
|
||||
("other", "其他 AI"),
|
||||
],
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
("agent_id", models.CharField(blank=True, max_length=100, null=True)),
|
||||
(
|
||||
"agent_name",
|
||||
models.CharField(max_length=100, verbose_name="Agent 名称"),
|
||||
),
|
||||
(
|
||||
"agent_emoji",
|
||||
models.CharField(
|
||||
default="🤖", max_length=10, verbose_name="Agent 表情"
|
||||
),
|
||||
),
|
||||
("nickname", models.CharField(max_length=100, verbose_name="昵称")),
|
||||
("is_host", models.BooleanField(default=False)),
|
||||
("joined_at", models.DateTimeField(auto_now_add=True)),
|
||||
("left_at", models.DateTimeField(blank=True, null=True)),
|
||||
("api_key", models.CharField(blank=True, max_length=255, null=True)),
|
||||
(
|
||||
"meeting",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="participants",
|
||||
to="meetings.meeting",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "参会者",
|
||||
"verbose_name_plural": "参会者",
|
||||
"db_table": "participants",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Message",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("content", models.TextField()),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
(
|
||||
"is_broadcast",
|
||||
models.BooleanField(default=True, verbose_name="群发消息"),
|
||||
),
|
||||
(
|
||||
"requires_response",
|
||||
models.BooleanField(default=False, verbose_name="需要回复"),
|
||||
),
|
||||
(
|
||||
"in_reply_to",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="replies",
|
||||
to="meetings.message",
|
||||
),
|
||||
),
|
||||
(
|
||||
"meeting",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="messages",
|
||||
to="meetings.meeting",
|
||||
),
|
||||
),
|
||||
(
|
||||
"read_by",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name="read_messages",
|
||||
to="meetings.participant",
|
||||
),
|
||||
),
|
||||
(
|
||||
"sender",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="sent_messages",
|
||||
to="meetings.participant",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "消息",
|
||||
"verbose_name_plural": "消息",
|
||||
"db_table": "messages",
|
||||
"ordering": ["created_at"],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="MeetingMinutes",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("content", models.TextField()),
|
||||
("generated_at", models.DateTimeField(auto_now_add=True)),
|
||||
("exported_at", models.DateTimeField(blank=True, null=True)),
|
||||
(
|
||||
"meeting",
|
||||
models.OneToOneField(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="minutes",
|
||||
to="meetings.meeting",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "会议纪要",
|
||||
"verbose_name_plural": "会议纪要",
|
||||
"db_table": "meeting_minutes",
|
||||
},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="participant",
|
||||
index=models.Index(
|
||||
fields=["meeting", "agent_id"], name="participant_meeting_74488d_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="participant",
|
||||
index=models.Index(
|
||||
fields=["agent_type", "meeting"], name="participant_agent_t_c4a9dc_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="message",
|
||||
index=models.Index(
|
||||
fields=["meeting", "created_at"], name="messages_meeting_b69008_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="message",
|
||||
index=models.Index(
|
||||
fields=["is_broadcast", "created_at"],
|
||||
name="messages_is_broa_fae706_idx",
|
||||
),
|
||||
),
|
||||
]
|
||||
0
backend/meetings/migrations/__init__.py
Normal file
0
backend/meetings/migrations/__init__.py
Normal file
126
backend/meetings/models.py
Normal file
126
backend/meetings/models.py
Normal file
@@ -0,0 +1,126 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth import get_user_model
|
||||
import uuid
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class Meeting(models.Model):
|
||||
"""会议室模型"""
|
||||
STATUS_CHOICES = [
|
||||
('pending', '待开始'),
|
||||
('active', '进行中'),
|
||||
('ended', '已结束'),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
topic = models.CharField(max_length=200, verbose_name='会议主题')
|
||||
host = models.ForeignKey(User, on_delete=models.CASCADE, related_name='hosted_meetings')
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
|
||||
invite_code = models.CharField(max_length=20, unique=True, verbose_name='邀请码')
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
started_at = models.DateTimeField(null=True, blank=True)
|
||||
ended_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'meetings'
|
||||
verbose_name = '会议室'
|
||||
verbose_name_plural = '会议室'
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.topic} ({self.host.username})"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.invite_code:
|
||||
self.invite_code = uuid.uuid4().hex[:8].upper()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class Participant(models.Model):
|
||||
"""参会者模型"""
|
||||
AGENT_TYPE_CHOICES = [
|
||||
('human', '人类'),
|
||||
('openclaw', 'OpenClaw Agent'),
|
||||
('other', '其他 AI'),
|
||||
]
|
||||
|
||||
meeting = models.ForeignKey(Meeting, on_delete=models.CASCADE, related_name='participants')
|
||||
user = models.ForeignKey(User, null=True, blank=True, on_delete=models.CASCADE)
|
||||
|
||||
# Agent 信息
|
||||
agent_type = models.CharField(max_length=20, choices=AGENT_TYPE_CHOICES)
|
||||
agent_id = models.CharField(max_length=100, null=True, blank=True)
|
||||
agent_name = models.CharField(max_length=100, verbose_name='Agent 名称')
|
||||
agent_emoji = models.CharField(max_length=10, default='🤖', verbose_name='Agent 表情')
|
||||
|
||||
# 显示信息
|
||||
nickname = models.CharField(max_length=100, verbose_name='昵称')
|
||||
is_host = models.BooleanField(default=False)
|
||||
joined_at = models.DateTimeField(auto_now_add=True)
|
||||
left_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
# API 认证(Agent 用)
|
||||
api_key = models.CharField(max_length=255, null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'participants'
|
||||
verbose_name = '参会者'
|
||||
verbose_name_plural = '参会者'
|
||||
indexes = [
|
||||
models.Index(fields=['meeting', 'agent_id']),
|
||||
models.Index(fields=['agent_type', 'meeting']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.agent_emoji} {self.nickname}"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.api_key and self.agent_type != 'human':
|
||||
self.api_key = uuid.uuid4().hex
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class Message(models.Model):
|
||||
"""消息模型"""
|
||||
meeting = models.ForeignKey(Meeting, on_delete=models.CASCADE, related_name='messages')
|
||||
sender = models.ForeignKey(Participant, on_delete=models.CASCADE, related_name='sent_messages')
|
||||
content = models.TextField()
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
# 信箱机制
|
||||
is_broadcast = models.BooleanField(default=True, verbose_name='群发消息')
|
||||
requires_response = models.BooleanField(default=False, verbose_name='需要回复')
|
||||
in_reply_to = models.ForeignKey('self', null=True, blank=True, on_delete=models.SET_NULL, related_name='replies')
|
||||
|
||||
# 读取状态
|
||||
read_by = models.ManyToManyField(Participant, related_name='read_messages', blank=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'messages'
|
||||
verbose_name = '消息'
|
||||
verbose_name_plural = '消息'
|
||||
ordering = ['created_at']
|
||||
indexes = [
|
||||
models.Index(fields=['meeting', 'created_at']),
|
||||
models.Index(fields=['is_broadcast', 'created_at']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.sender.nickname}: {self.content[:50]}"
|
||||
|
||||
|
||||
class MeetingMinutes(models.Model):
|
||||
"""会议纪要模型"""
|
||||
meeting = models.OneToOneField(Meeting, on_delete=models.CASCADE, related_name='minutes')
|
||||
content = models.TextField()
|
||||
generated_at = models.DateTimeField(auto_now_add=True)
|
||||
exported_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'meeting_minutes'
|
||||
verbose_name = '会议纪要'
|
||||
verbose_name_plural = '会议纪要'
|
||||
|
||||
def __str__(self):
|
||||
return f"会议纪要 - {self.meeting.topic}"
|
||||
76
backend/meetings/serializers.py
Normal file
76
backend/meetings/serializers.py
Normal file
@@ -0,0 +1,76 @@
|
||||
from rest_framework import serializers
|
||||
from .models import Meeting, Participant, Message, MeetingMinutes
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
"""用户序列化器"""
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['id', 'username', 'email', 'created_at']
|
||||
|
||||
|
||||
class MeetingSerializer(serializers.ModelSerializer):
|
||||
"""会议室序列化器"""
|
||||
host_name = serializers.CharField(source='host.username', read_only=True)
|
||||
participant_count = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Meeting
|
||||
fields = [
|
||||
'id', 'topic', 'host', 'host_name', 'status', 'invite_code',
|
||||
'created_at', 'started_at', 'ended_at', 'participant_count'
|
||||
]
|
||||
read_only_fields = ['host', 'invite_code', 'status']
|
||||
|
||||
def get_participant_count(self, obj):
|
||||
return obj.participants.filter(left_at__isnull=True).count()
|
||||
|
||||
def create(self, validated_data):
|
||||
# 使用第一个用户作为 host(临时方案)
|
||||
host = User.objects.first()
|
||||
validated_data['host'] = host
|
||||
return super().create(validated_data)
|
||||
|
||||
|
||||
class ParticipantSerializer(serializers.ModelSerializer):
|
||||
"""参会者序列化器"""
|
||||
class Meta:
|
||||
model = Participant
|
||||
fields = [
|
||||
'id', 'meeting', 'user', 'agent_type', 'agent_id',
|
||||
'agent_name', 'agent_emoji', 'nickname', 'is_host',
|
||||
'joined_at', 'api_key'
|
||||
]
|
||||
read_only_fields = ['api_key', 'joined_at']
|
||||
|
||||
|
||||
class MessageSerializer(serializers.ModelSerializer):
|
||||
"""消息序列化器"""
|
||||
sender_name = serializers.CharField(source='sender.nickname', read_only=True)
|
||||
sender_emoji = serializers.CharField(source='sender.agent_emoji', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Message
|
||||
fields = [
|
||||
'id', 'meeting', 'sender', 'sender_name', 'sender_emoji',
|
||||
'content', 'created_at', 'is_broadcast', 'requires_response',
|
||||
'in_reply_to', 'read_by'
|
||||
]
|
||||
read_only_fields = ['sender', 'created_at']
|
||||
|
||||
|
||||
class InboxSerializer(serializers.Serializer):
|
||||
"""信箱序列化器"""
|
||||
unread_count = serializers.IntegerField()
|
||||
messages = MessageSerializer(many=True)
|
||||
|
||||
|
||||
class MeetingMinutesSerializer(serializers.ModelSerializer):
|
||||
"""会议纪要序列化器"""
|
||||
class Meta:
|
||||
model = MeetingMinutes
|
||||
fields = ['meeting', 'content', 'generated_at', 'exported_at']
|
||||
read_only_fields = ['meeting', 'generated_at']
|
||||
3
backend/meetings/tests.py
Normal file
3
backend/meetings/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
236
backend/meetings/views.py
Normal file
236
backend/meetings/views.py
Normal file
@@ -0,0 +1,236 @@
|
||||
from rest_framework import viewsets, status, permissions
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from django.utils import timezone
|
||||
from .models import Meeting, Participant, Message, MeetingMinutes
|
||||
from .serializers import (
|
||||
MeetingSerializer, ParticipantSerializer,
|
||||
MessageSerializer, InboxSerializer
|
||||
)
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.shortcuts import get_object_or_404
|
||||
import uuid
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class MeetingViewSet(viewsets.ModelViewSet):
|
||||
"""会议室视图集"""
|
||||
queryset = Meeting.objects.all()
|
||||
serializer_class = MeetingSerializer
|
||||
permission_classes = [] # 临时开放所有权限
|
||||
|
||||
def get_queryset(self):
|
||||
# 简单返回所有会议
|
||||
return Meeting.objects.all().order_by('-created_at')
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""创建会议"""
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
# 临时:使用第一个用户作为 host
|
||||
from django.contrib.auth import get_user_model
|
||||
User = get_user_model()
|
||||
host = User.objects.first()
|
||||
|
||||
meeting = serializer.save(host=host)
|
||||
|
||||
# 创建主持人参会记录
|
||||
Participant.objects.create(
|
||||
meeting=meeting,
|
||||
user=host,
|
||||
agent_type='human',
|
||||
nickname=host.username,
|
||||
is_host=True
|
||||
)
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def start(self, request, pk=None):
|
||||
"""开始会议"""
|
||||
meeting = self.get_object()
|
||||
if meeting.host != request.user:
|
||||
return Response(
|
||||
{'error': '只有主持人可以开始会议'},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
meeting.status = 'active'
|
||||
meeting.started_at = timezone.now()
|
||||
meeting.save()
|
||||
|
||||
return Response({'status': '会议已开始'})
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def end(self, request, pk=None):
|
||||
"""结束会议"""
|
||||
meeting = self.get_object()
|
||||
if meeting.host != request.user:
|
||||
return Response(
|
||||
{'error': '只有主持人可以结束会议'},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
meeting.status = 'ended'
|
||||
meeting.ended_at = timezone.now()
|
||||
meeting.save()
|
||||
|
||||
return Response({'status': '会议已结束'})
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def join(self, request, pk=None):
|
||||
"""加入会议"""
|
||||
meeting = self.get_object()
|
||||
|
||||
if meeting.status == 'ended':
|
||||
return Response(
|
||||
{'error': '会议已结束'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
invite_code = request.data.get('invite_code')
|
||||
if invite_code != meeting.invite_code:
|
||||
return Response(
|
||||
{'error': '邀请码错误'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# 检查是否已加入
|
||||
existing = Participant.objects.filter(
|
||||
meeting=meeting,
|
||||
user=request.user,
|
||||
left_at__isnull=True
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
return Response(ParticipantSerializer(existing).data)
|
||||
|
||||
# 创建参会记录
|
||||
participant = Participant.objects.create(
|
||||
meeting=meeting,
|
||||
user=request.user,
|
||||
agent_type='human',
|
||||
nickname=request.user.username
|
||||
)
|
||||
|
||||
return Response(ParticipantSerializer(participant).data, status=status.HTTP_201_CREATED)
|
||||
|
||||
@action(detail=True, methods=['get'])
|
||||
def participants(self, request, pk=None):
|
||||
"""获取参会者列表"""
|
||||
meeting = self.get_object()
|
||||
participants = meeting.participants.filter(left_at__isnull=True)
|
||||
serializer = ParticipantSerializer(participants, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, methods=['get'])
|
||||
def messages(self, request, pk=None):
|
||||
"""获取消息(人类轮询)"""
|
||||
meeting = self.get_object()
|
||||
last_id = request.query_params.get('last_id', 0)
|
||||
|
||||
messages = meeting.messages.filter(id__gt=last_id).select_related('sender')
|
||||
serializer = MessageSerializer(messages, many=True)
|
||||
|
||||
return Response({'messages': serializer.data})
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def send_message(self, request, pk=None):
|
||||
"""发送消息"""
|
||||
meeting = self.get_object()
|
||||
|
||||
# 获取或创建参会者(临时:使用第一个参会者或创建)
|
||||
participant = Participant.objects.filter(
|
||||
meeting=meeting,
|
||||
left_at__isnull=True
|
||||
).first()
|
||||
|
||||
if not participant:
|
||||
# 创建默认参会者
|
||||
host = meeting.host
|
||||
participant = Participant.objects.create(
|
||||
meeting=meeting,
|
||||
user=host,
|
||||
agent_type='human',
|
||||
nickname=host.username
|
||||
)
|
||||
|
||||
content = request.data.get('content')
|
||||
if not content:
|
||||
return Response(
|
||||
{'error': '消息内容不能为空'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
message = Message.objects.create(
|
||||
meeting=meeting,
|
||||
sender=participant,
|
||||
content=content,
|
||||
is_broadcast=request.data.get('is_broadcast', True),
|
||||
requires_response=request.data.get('requires_response', False)
|
||||
)
|
||||
|
||||
return Response(MessageSerializer(message).data, status=status.HTTP_201_CREATED)
|
||||
|
||||
@action(detail=True, methods=['get'])
|
||||
def inbox(self, request, pk=None):
|
||||
"""Agent 查阅信箱"""
|
||||
meeting = self.get_object()
|
||||
agent_id = request.query_params.get('agent_id')
|
||||
|
||||
if not agent_id:
|
||||
return Response(
|
||||
{'error': '缺少 agent_id 参数'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# 找到这个 Agent 的参会记录
|
||||
participant = get_object_or_404(
|
||||
Participant,
|
||||
meeting=meeting,
|
||||
agent_id=agent_id,
|
||||
left_at__isnull=True
|
||||
)
|
||||
|
||||
# 获取发给这个 Agent 的消息(未读)
|
||||
messages = Message.objects.filter(
|
||||
meeting=meeting
|
||||
).exclude(
|
||||
read_by=participant
|
||||
)
|
||||
|
||||
# 如果是群发消息,所有人都能看到
|
||||
# 如果是指定消息,需要检查 recipients
|
||||
# 简化版:所有未读消息都返回
|
||||
|
||||
serializer = MessageSerializer(messages, many=True)
|
||||
|
||||
return Response({
|
||||
'unread_count': messages.count(),
|
||||
'messages': serializer.data
|
||||
})
|
||||
|
||||
|
||||
class ParticipantViewSet(viewsets.ModelViewSet):
|
||||
"""参会者视图集"""
|
||||
queryset = Participant.objects.all()
|
||||
serializer_class = ParticipantSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def leave(self, request, pk=None):
|
||||
"""离开会议"""
|
||||
participant = self.get_object()
|
||||
|
||||
if participant.user != request.user:
|
||||
return Response(
|
||||
{'error': '无权操作'},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
participant.left_at = timezone.now()
|
||||
participant.save()
|
||||
|
||||
return Response({'status': '已离开会议'})
|
||||
6
backend/requirements.txt
Normal file
6
backend/requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
Django>=4.2,<5.0
|
||||
djangorestframework>=3.14
|
||||
django-cors-headers>=4.3
|
||||
psycopg2-binary>=2.9
|
||||
python-dotenv>=1.0
|
||||
djangorestframework-simplejwt>=5.3
|
||||
167
backend/test_api.py
Normal file
167
backend/test_api.py
Normal file
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
龙虾议事厅 - API 测试脚本
|
||||
自动测试所有核心功能
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
import sys
|
||||
|
||||
API_BASE = 'http://localhost:8000/api/v1'
|
||||
|
||||
def print_result(name, success, data=None):
|
||||
"""打印测试结果"""
|
||||
if success:
|
||||
print(f"✅ {name}")
|
||||
if data:
|
||||
print(f" {json.dumps(data, indent=2, ensure_ascii=False)}")
|
||||
else:
|
||||
print(f"❌ {name}")
|
||||
return success
|
||||
|
||||
def test_register():
|
||||
"""测试注册"""
|
||||
print("\n=== 测试用户注册 ===")
|
||||
try:
|
||||
response = requests.post(f'{API_BASE}/auth/register/', json={
|
||||
'username': 'testuser',
|
||||
'email': 'test@example.com',
|
||||
'password': 'password123'
|
||||
})
|
||||
if response.status_code in [200, 201]:
|
||||
return print_result("用户注册", True, response.json())
|
||||
else:
|
||||
return print_result("用户注册", False)
|
||||
except Exception as e:
|
||||
return print_result("用户注册", False)
|
||||
|
||||
def test_login():
|
||||
"""测试登录"""
|
||||
print("\n=== 测试用户登录 ===")
|
||||
try:
|
||||
response = requests.post(f'{API_BASE}/auth/login/', json={
|
||||
'username': 'test',
|
||||
'password': 'test123'
|
||||
})
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print_result("用户登录", True, data)
|
||||
return data.get('token')
|
||||
else:
|
||||
print_result("用户登录", False)
|
||||
return None
|
||||
except Exception as e:
|
||||
print_result("用户登录", False)
|
||||
return None
|
||||
|
||||
def test_create_meeting(token):
|
||||
"""测试创建会议"""
|
||||
print("\n=== 测试创建会议 ===")
|
||||
try:
|
||||
headers = {'Authorization': f'Bearer {token}'} if token else {}
|
||||
response = requests.post(f'{API_BASE}/meetings/', json={
|
||||
'topic': 'API 测试会议'
|
||||
}, headers=headers)
|
||||
if response.status_code in [200, 201]:
|
||||
data = response.json()
|
||||
print_result("创建会议", True, data)
|
||||
return data.get('id')
|
||||
else:
|
||||
print_result("创建会议", False)
|
||||
print(f" 错误:{response.text[:200]}")
|
||||
return None
|
||||
except Exception as e:
|
||||
print_result("创建会议", False)
|
||||
return None
|
||||
|
||||
def test_list_meetings(token):
|
||||
"""测试获取会议列表"""
|
||||
print("\n=== 测试获取会议列表 ===")
|
||||
try:
|
||||
headers = {'Authorization': f'Bearer {token}'} if token else {}
|
||||
response = requests.get(f'{API_BASE}/meetings/', headers=headers)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print_result("获取会议列表", True, data)
|
||||
return data
|
||||
else:
|
||||
print_result("获取会议列表", False)
|
||||
return None
|
||||
except Exception as e:
|
||||
print_result("获取会议列表", False)
|
||||
return None
|
||||
|
||||
def test_send_message(token, meeting_id):
|
||||
"""测试发送消息"""
|
||||
print("\n=== 测试发送消息 ===")
|
||||
try:
|
||||
headers = {'Authorization': f'Bearer {token}'} if token else {}
|
||||
response = requests.post(
|
||||
f'{API_BASE}/meetings/{meeting_id}/send_message/',
|
||||
json={'content': 'Hello, 这是测试消息!'},
|
||||
headers=headers
|
||||
)
|
||||
if response.status_code in [200, 201]:
|
||||
data = response.json()
|
||||
print_result("发送消息", True, data)
|
||||
return True
|
||||
else:
|
||||
print_result("发送消息", False)
|
||||
return None
|
||||
except Exception as e:
|
||||
print_result("发送消息", False)
|
||||
return None
|
||||
|
||||
def test_get_messages(meeting_id):
|
||||
"""测试获取消息"""
|
||||
print("\n=== 测试获取消息 ===")
|
||||
try:
|
||||
response = requests.get(f'{API_BASE}/meetings/{meeting_id}/messages/?last_id=0')
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print_result("获取消息", True, data)
|
||||
return True
|
||||
else:
|
||||
print_result("获取消息", False)
|
||||
return None
|
||||
except Exception as e:
|
||||
print_result("获取消息", False)
|
||||
return None
|
||||
|
||||
def main():
|
||||
"""主测试流程"""
|
||||
print("=" * 60)
|
||||
print("🏛️ 龙虾议事厅 - API 自动化测试")
|
||||
print("=" * 60)
|
||||
|
||||
# 1. 测试登录
|
||||
token = test_login()
|
||||
if not token:
|
||||
print("\n⚠️ 登录失败,尝试注册新用户...")
|
||||
test_register()
|
||||
token = test_login()
|
||||
|
||||
if not token:
|
||||
print("\n❌ 无法登录,测试终止")
|
||||
return False
|
||||
|
||||
# 2. 测试创建会议
|
||||
meeting_id = test_create_meeting(token)
|
||||
|
||||
# 3. 测试获取会议列表
|
||||
test_list_meetings(token)
|
||||
|
||||
# 4. 测试发送消息
|
||||
if meeting_id:
|
||||
test_send_message(token, meeting_id)
|
||||
test_get_messages(meeting_id)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("✅ 测试完成!")
|
||||
print("=" * 60)
|
||||
return True
|
||||
|
||||
if __name__ == '__main__':
|
||||
success = main()
|
||||
sys.exit(0 if success else 1)
|
||||
0
backend/users/__init__.py
Normal file
0
backend/users/__init__.py
Normal file
3
backend/users/admin.py
Normal file
3
backend/users/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
6
backend/users/apps.py
Normal file
6
backend/users/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class UsersConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "users"
|
||||
0
backend/users/migrations/__init__.py
Normal file
0
backend/users/migrations/__init__.py
Normal file
3
backend/users/models.py
Normal file
3
backend/users/models.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
3
backend/users/tests.py
Normal file
3
backend/users/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
80
backend/users/views.py
Normal file
80
backend/users/views.py
Normal file
@@ -0,0 +1,80 @@
|
||||
from rest_framework import serializers, status, views
|
||||
from rest_framework.response import Response
|
||||
from django.contrib.auth import authenticate, get_user_model
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class LoginSerializer(serializers.Serializer):
|
||||
username = serializers.CharField()
|
||||
password = serializers.CharField()
|
||||
|
||||
|
||||
class LoginView(views.APIView):
|
||||
def post(self, request):
|
||||
serializer = LoginSerializer(data=request.data)
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
username = serializer.validated_data['username']
|
||||
password = serializer.validated_data['password']
|
||||
|
||||
user = authenticate(username=username, password=password)
|
||||
if not user:
|
||||
return Response(
|
||||
{'detail': '用户名或密码错误'},
|
||||
status=status.HTTP_401_UNAUTHORIZED
|
||||
)
|
||||
|
||||
# 简单 Token(生产环境应该用 JWT)
|
||||
import uuid
|
||||
token = uuid.uuid4().hex
|
||||
|
||||
# 实际项目中应该存储 token 到数据库/缓存
|
||||
# 这里简化处理
|
||||
|
||||
return Response({
|
||||
'token': token,
|
||||
'user': {
|
||||
'id': user.id,
|
||||
'username': user.username,
|
||||
'email': user.email
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
class RegisterSerializer(serializers.Serializer):
|
||||
username = serializers.CharField()
|
||||
email = serializers.EmailField()
|
||||
password = serializers.CharField()
|
||||
|
||||
|
||||
class RegisterView(views.APIView):
|
||||
def post(self, request):
|
||||
serializer = RegisterSerializer(data=request.data)
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
user = User.objects.create_user(
|
||||
username=serializer.validated_data['username'],
|
||||
email=serializer.validated_data['email'],
|
||||
password=serializer.validated_data['password']
|
||||
)
|
||||
|
||||
import uuid
|
||||
token = uuid.uuid4().hex
|
||||
|
||||
return Response({
|
||||
'token': token,
|
||||
'user': {
|
||||
'id': user.id,
|
||||
'username': user.username,
|
||||
'email': user.email
|
||||
}
|
||||
}, status=status.HTTP_201_CREATED)
|
||||
except Exception as e:
|
||||
return Response(
|
||||
{'detail': str(e)},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
Reference in New Issue
Block a user