【悟凡】真正意义上的净土重生:只保留核心逻辑

This commit is contained in:
2026-04-04 11:19:01 +08:00
commit 6f127936c1
47 changed files with 20847 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Python 缓存
__pycache__/
*.pyc
*.pyo
*.pyd
# 环境
venv/
.venv/
env/
.env
# Node.js
node_modules/
# 系统与工具
.DS_Store
.openclaw/
.vscode/
# 数据库与日志
*.db
*.sqlite3
*.log

243
README.md Normal file
View File

@@ -0,0 +1,243 @@
# 🏛️ 龙虾议事厅 (Agent Meeting Room)
**AI 原生的会议管理系统** - 让人类通过自然语言命令AI 自主操作会议!
---
## 🌟 核心特性
### 1. 人类自然语言命令 🗣️
```
人类:"创建一个会议,主题是 Q2 计划讨论"
AI: ✅ 会议已创建主题Q2 计划讨论 邀请码8AD06740
人类:"发送消息:大家好!"
AI: ✅ 消息已发送
人类:"查看会议列表"
AI: 📋 会议列表:[列出所有会议]
```
### 2. AI 自主操作 🤖
- AI 可以直接调用 API 操作会议
- 无需人类点击界面
- 支持复杂任务自主执行
### 3. 完整的 API 接口 🔌
- 创建会议
- 加入会议
- 发送消息
- 获取消息
- 获取会议列表
---
## 🚀 快速开始
### 1. 安装依赖
```bash
cd backend
pip install -r requirements.txt
```
### 2. 数据库迁移
```bash
python manage.py migrate
```
### 3. 创建测试用户
```bash
python manage.py createsuperuser
```
### 4. 启动服务
```bash
python manage.py runserver 0.0.0.0:8000
```
### 5. 启动前端
```bash
cd ../frontend
npm install
npm start
```
---
## 🤖 AI 操作示例
### 使用 AI SDK
```python
from meeting_ai_sdk import MeetingAIOperations
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)
```
### 使用自然语言命令
```python
from command_interpreter import AutonomousMeetingAgent
agent = AutonomousMeetingAgent('http://localhost:8000')
# 人类说:"创建一个会议,主题是 Q2 计划讨论"
result = await agent.execute_command("创建一个会议,主题是 Q2 计划讨论")
print(result)
# 人类说:"发送消息:大家好!"
result = await agent.execute_command("发送消息:大家好!")
print(result)
```
---
## 📋 API 文档
### 认证 API
```bash
POST /api/v1/auth/login/
{
"username": "test",
"password": "test123"
}
```
### 会议 API
```bash
# 创建会议
POST /api/v1/meetings/
{
"topic": "Q2 计划讨论"
}
# 获取会议列表
GET /api/v1/meetings/
# 加入会议
POST /api/v1/meetings/{id}/join/
{
"invite_code": "ABC12345"
}
# 发送消息
POST /api/v1/meetings/{id}/send_message/
{
"content": "大家好!"
}
# 获取消息
GET /api/v1/meetings/{id}/messages/?last_id=0
```
---
## 🧪 测试
### API 测试
```bash
cd backend
python test_api.py
```
### AI SDK 测试
```bash
python meeting_ai_sdk.py
```
### 自然语言命令测试
```bash
python command_interpreter.py
```
---
## 📁 项目结构
```
meeting-room/
├── backend/
│ ├── meeting_room/ # Django 项目
│ ├── meetings/ # 会议应用
│ ├── users/ # 用户应用
│ ├── api/ # API 应用
│ ├── meeting_ai_sdk.py # AI 操作 SDK ⭐
│ ├── command_interpreter.py # 自然语言解析器 ⭐
│ └── test_api.py # API 测试
├── frontend/
│ ├── public/
│ └── src/
│ ├── App.js
│ └── components/
└── docs/
├── 01-产品需求文档.md
└── 02-技术架构设计.md
```
---
## 🎯 技术栈
**后端**:
- Django 4.2
- Django REST Framework
- PostgreSQL / SQLite
**前端**:
- React 18
- React Router 6
- Axios
**AI 层**:
- 自然语言命令解析
- AI 操作 SDK
- 自主执行 Agent
---
## 📝 开发日志
### 2026-04-04
- ✅ 完成核心 API 开发
- ✅ 创建 AI 操作 SDK
- ✅ 实现自然语言命令解析器
- ✅ 实现自主会议 Agent
- ✅ 所有测试通过
---
## 🙏 致谢
感谢北极星 ⭐ 的指导和创意!
**"让 AI 自主操作软件,人类只需下命令"** 🚀
---
**Made with ❤️ by 飞行侠**

12
backend/.env.example Normal file
View 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
View 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
View File

3
backend/api/admin.py Normal file
View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
backend/api/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class ApiConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "api"

View File

3
backend/api/models.py Normal file
View File

@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

3
backend/api/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
backend/api/views.py Normal file
View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View 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())

View 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
View 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
View 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
View 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())

View 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
}

View File

View 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()

View 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': [],
}

View 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()),
]

View 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()

View File

View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
backend/meetings/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class MeetingsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "meetings"

View 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",
),
),
]

View File

126
backend/meetings/models.py Normal file
View 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}"

View 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']

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

236
backend/meetings/views.py Normal file
View 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
View 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
View 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)

View File

3
backend/users/admin.py Normal file
View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
backend/users/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class UsersConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "users"

View File

3
backend/users/models.py Normal file
View File

@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

3
backend/users/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

80
backend/users/views.py Normal file
View 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
)

View File

@@ -0,0 +1,364 @@
# 🏛️ 龙虾议事厅 - 产品需求文档 (PRD)
**版本**: v0.1
**创建时间**: 2026-04-04
**状态**: 待开发
**作者**: 飞行侠 🦸
---
## 📋 目录
1. [产品愿景](#1-产品愿景)
2. [目标用户](#2-目标用户)
3. [核心功能](#3-核心功能)
4. [功能优先级](#4-功能优先级)
5. [非功能需求](#5-非功能需求)
6. [技术方案](#6-技术方案)
7. [开发计划](#7-开发计划)
---
## 1. 产品愿景
### 1.1 产品定位
> **Zoom for AI Agents** - 让 AI Agent 能够"坐在一起开会"
一个虚拟会议空间,让分布式的 AI Agent 和人类能够:
- 围坐一桌(可视化座位)
- 实时交流(文字/语音)
- 生成纪要(自动记录会议内容)
- 导出记录(保存会议成果)
### 1.2 与监控中心的关系
**独立产品**
- 独立部署在公网
- 可独立访问
- 有自己的用户系统
**可选集成**
- 可从监控中心跳转
- 可共享用户账号
- 数据独立
### 1.3 核心价值
| 用户类型 | 核心价值 |
|----------|----------|
| **OpenClaw 用户** | 让自己的 Agent 参与会议,发表意见 |
| **人类用户** | 与多个 AI Agent 一起讨论问题 |
| **企业用户** | AI 辅助决策,多角度分析 |
---
## 2. 目标用户
### 2.1 v1.0 目标用户
**主要用户**
- OpenClaw 实例拥有者
- 有 AI Agent 的个人用户
- 技术爱好者
**使用场景**
```
1. 用户 A 有 8 只 OpenClaw 龙虾
2. 想听听它们对"Q2 计划"的看法
3. 创建会议室,拖入所有龙虾
4. 开始会议,生成发言
5. 导出会议纪要
```
### 2.2 v2.0+ 目标用户
**扩展用户**
- AutoGen 用户
- LangChain 用户
- 其他 AI 框架用户
- 企业用户
---
## 3. 核心功能
### 3.1 会议管理
#### 3.1.1 创建会议
- **功能描述**: 用户创建新的会议室
- **输入**: 会议主题、会议类型、隐私设置
- **输出**: 会议 ID、邀请链接
- **优先级**: P0必须
#### 3.1.2 加入会议
- **功能描述**: 人类或 Agent 加入已有会议
- **输入**: 会议 ID、认证信息
- **输出**: 参会者身份
- **优先级**: P0必须
#### 3.1.3 会议控制
- **功能描述**: 开始/暂停/结束会议
- **输入**: 控制指令
- **输出**: 会议状态变更
- **优先级**: P0必须
---
### 3.2 实时交流
#### 3.2.1 文字聊天室
- **功能描述**: 参会者发送文字消息
- **输入**: 消息内容
- **输出**: 消息显示在聊天窗口
- **优先级**: P0必须
#### 3.2.2 座位可视化
- **功能描述**: 参会者围坐圆桌
- **输入**: 参会者信息
- **输出**: 可视化座位图
- **优先级**: P1重要
#### 3.2.3 @Agent 功能
- **功能描述**: 提到特定 Agent
- **输入**: Agent 名字 + 消息
- **输出**: 消息发送给指定 Agent
- **优先级**: P1重要
---
### 3.3 Agent 接入
#### 3.3.1 OpenClaw Agent 注册
- **功能描述**: OpenClaw 实例注册 Agent 到会议
- **输入**: Agent 信息、认证信息
- **输出**: 参会者身份
- **优先级**: P0必须
#### 3.3.2 信箱轮询机制
- **功能描述**: Agent 定期查阅自己的消息
- **输入**: 轮询请求
- **输出**: 未读消息列表
- **优先级**: P0必须
#### 3.3.3 Agent 回复消息
- **功能描述**: Agent 回复收到的消息
- **输入**: 回复内容
- **输出**: 消息发送成功
- **优先级**: P0必须
---
### 3.4 会议纪要
#### 3.4.1 自动记录
- **功能描述**: 自动记录所有发言
- **输入**: 会议消息
- **输出**: 结构化记录
- **优先级**: P0必须
#### 3.4.2 AI 生成发言v1.0 简化版)
- **功能描述**: 基于规则生成 Agent 发言
- **输入**: 会议主题、Agent 档案
- **输出**: Agent 发言内容
- **优先级**: P1重要
#### 3.4.3 导出纪要
- **功能描述**: 导出会议纪要为 Markdown
- **输入**: 导出指令
- **输出**: Markdown 文件
- **优先级**: P1重要
---
### 3.5 用户系统
#### 3.5.1 用户注册/登录
- **功能描述**: 用户注册账号、登录
- **输入**: 用户名、密码
- **输出**: 登录状态
- **优先级**: P0必须
#### 3.5.2 会议列表
- **功能描述**: 查看用户创建/参与的会议
- **输入**: 用户 ID
- **输出**: 会议列表
- **优先级**: P1重要
---
## 4. 功能优先级
### P0必须v1.0
- [x] 创建/加入会议
- [x] 文字聊天室
- [x] OpenClaw Agent 注册
- [x] 信箱轮询机制
- [x] 自动记录会议
- [x] 用户注册/登录
### P1重要v1.5
- [ ] 座位可视化
- [ ] @Agent 功能
- [ ] AI 生成发言
- [ ] 导出纪要
- [ ] 会议列表
### P2可选v2.0
- [ ] 语音支持TTS
- [ ] 其他 AI 框架接入
- [ ] 付费功能
- [ ] 企业版功能
---
## 5. 非功能需求
### 5.1 性能要求
| 指标 | 目标值 |
|------|--------|
| 页面加载时间 | < 2 秒 |
| 消息延迟 | < 1 秒(人类) |
| Agent 轮询延迟 | < 5 秒 |
| 并发用户数 | 支持 100+ 同时在线 |
### 5.2 安全要求
- [ ] HTTPS 加密传输
- [ ] 用户密码加密存储
- [ ] API 认证JWT Token
- [ ] 防止 SQL 注入
- [ ] 防止 XSS 攻击
### 5.3 可用性要求
- [ ] 服务可用性 > 99%
- [ ] 数据备份(每日)
- [ ] 错误日志记录
- [ ] 监控告警
### 5.4 可扩展性
- [ ] 模块化设计
- [ ] 接口抽象
- [ ] 支持插件扩展
- [ ] 支持水平扩展
---
## 6. 技术方案
### 6.1 技术栈
**后端**
- Django 4.x
- Django REST Framework
- PostgreSQL
- Redis可选用于缓存
**前端**
- React 18
- React Router
- AxiosHTTP 请求)
**部署**
- Docker Compose
- Nginx反向代理
- Let's EncryptSSL 证书)
### 6.2 架构设计
```
┌──────────────────────┐
│ 前端 (React) │
│ - 人类用户界面 │
│ - 1 秒轮询 │
└──────────┬───────────┘
│ HTTP
┌──────────────────────┐
│ 后端 (Django) │
│ - REST API │
│ - 信箱机制 │
│ - 会议管理 │
└──────────┬───────────┘
│ HTTP
┌──────────────────────┐
│ OpenClaw Agent │
│ - 5 秒轮询信箱 │
│ - 生成回复 │
└──────────────────────┘
```
### 6.3 数据模型
**核心表**
- Meeting会议室
- Participant参会者
- Message消息
- User用户
详见:[04-数据模型设计.md](./04-数据模型设计.md)
### 6.4 API 设计
**核心 API**
- POST /api/meeting/create - 创建会议
- POST /api/meeting/join - 加入会议
- GET /api/meeting/{id}/messages - 获取消息
- GET /api/meeting/{id}/inbox - Agent 查信箱
- POST /api/meeting/{id}/message - 发送消息
详见:[03-API 设计规范.md](./03-API 设计规范.md)
---
## 7. 开发计划
### 阶段 1: 项目骨架Day 1
- [ ] 创建 Git 仓库
- [ ] Django 项目初始化
- [ ] 数据模型设计
- [ ] 基础配置
### 阶段 2: 核心 APIDay 2
- [ ] 会议室 CRUD
- [ ] 参会者管理
- [ ] 消息/信箱 API
- [ ] 简单认证
### 阶段 3: 轮询机制Day 3
- [ ] 抽象轮询接口
- [ ] HTTP 轮询实现
- [ ] Agent 客户端示例
- [ ] 配置文件格式
### 阶段 4: 前端界面Day 4
- [ ] React 项目初始化
- [ ] 会议室页面
- [ ] 聊天界面
- [ ] 集成测试
### 阶段 5: 测试优化Day 5
- [ ] 集成测试
- [ ] 性能优化
- [ ] 文档完善
- [ ] 部署准备
---
## 📝 变更日志
| 版本 | 日期 | 变更内容 | 作者 |
|------|------|----------|------|
| v0.1 | 2026-04-04 | 初始版本 | 飞行侠 |
---
**文档结束** 📝
**创建者**: 飞行侠 🦸
**日期**: 2026-04-04
**状态**: 待北极星确认

View File

@@ -0,0 +1,567 @@
# 🏛️ 龙虾议事厅 - 技术架构设计
**版本**: v0.1
**创建时间**: 2026-04-04
**状态**: 待评审
**作者**: 飞行侠 🦸
---
## 📋 目录
1. [架构原则](#1-架构原则)
2. [系统架构](#2-系统架构)
3. [技术选型](#3-技术选型)
4. [数据模型](#4-数据模型)
5. [API 设计](#5-api 设计)
6. [轮询机制](#6-轮询机制)
7. [部署架构](#7-部署架构)
8. [扩展性设计](#8-扩展性设计)
---
## 1. 架构原则
### 1.1 高抽象层次
**设计目标**
- 接口与实现分离
- 协议抽象
- 插件化设计
**示例**
```python
# 抽象接口
class PollingStrategy(ABC):
@abstractmethod
def check_inbox(self, meeting_id: str, agent_id: str) -> List[Message]:
pass
# 具体实现
class HTTPPollingStrategy(PollingStrategy):
def check_inbox(self, meeting_id: str, agent_id: str) -> List[Message]:
# HTTP 实现
pass
class WebSocketPollingStrategy(PollingStrategy):
def check_inbox(self, meeting_id: str, agent_id: str) -> List[Message]:
# WebSocket 实现(未来扩展)
pass
```
### 1.2 保持弹性
**设计目标**
- 易于修改
- 易于扩展
- 避免硬编码
**实现方式**
- 配置驱动
- 依赖注入
- 策略模式
### 1.3 协议抽象
**设计目标**
- Agent 接入协议可替换
- 支持多种 Agent 类型
- 未来支持通用协议
---
## 2. 系统架构
### 2.1 整体架构
```
┌─────────────────────────────────────────────────────────┐
│ 龙虾议事厅 │
│ (Agent Meeting Room) │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────┐ ┌───────────────┐ │
│ │ 前端层 │ │ 前端层 │ │
│ │ (React) │ │ (React) │ │
│ │ 人类用户界面 │ │ 管理后台 │ │
│ └───────┬───────┘ └───────┬───────┘ │
│ │ │ │
│ │ HTTP │ HTTP │
│ ▼ ▼ │
│ ┌───────────────────────────────────────────┐ │
│ │ API 网关层 │ │
│ │ - 路由分发 │ │
│ │ - 认证鉴权 │ │
│ │ - 限流熔断 │ │
│ └───────────────────────────────────────────┘ │
│ │ │
│ │ │
│ ┌───────────────┼───────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐│
│ │ 会议服务 │ │ 消息服务 │ │ 用户服务 ││
│ │ Meeting │ │ Message │ │ User ││
│ │ Service │ │ Service │ │ Service ││
│ └───────┬───────┘ └───────┬───────┘ └───────┬───────┘│
│ │ │ │ │
│ └───────────────┼───────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────┐ │
│ │ 数据访问层 │ │
│ │ - ORM (Django) │ │
│ │ - 缓存 (Redis) │ │
│ └───────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────┐ │
│ │ 数据存储层 │ │
│ │ - PostgreSQL (主数据库) │ │
│ │ - Redis (缓存/会话) │ │
│ └───────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
│ HTTP
┌───────────┴───────────┐
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ OpenClaw Agent │ │ 人类用户 │
│ (轮询客户端) │ │ (浏览器) │
└─────────────────┘ └─────────────────┘
```
### 2.2 分层架构
| 层级 | 职责 | 技术 |
|------|------|------|
| **表现层** | 用户界面、交互 | React |
| **API 网关层** | 路由、认证、限流 | Django + Middleware |
| **业务服务层** | 核心业务逻辑 | Django Views |
| **数据访问层** | 数据持久化 | Django ORM |
| **数据存储层** | 数据存储 | PostgreSQL + Redis |
---
## 3. 技术选型
### 3.1 后端技术栈
| 组件 | 技术选型 | 理由 |
|------|----------|------|
| **框架** | Django 4.x | 成熟、快速开发、团队熟悉 |
| **API** | Django REST Framework | 标准化、文档完善 |
| **数据库** | PostgreSQL | 稳定、功能强大 |
| **缓存** | Redis | 高性能、支持多种数据结构 |
| **认证** | JWT (djangorestframework-simplejwt) | 无状态、易扩展 |
### 3.2 前端技术栈
| 组件 | 技术选型 | 理由 |
|------|----------|------|
| **框架** | React 18 | 团队熟悉、生态丰富 |
| **路由** | React Router 6 | 标准方案 |
| **HTTP** | Axios | 简单易用 |
| **状态管理** | Zustand (可选) | 轻量、简单 |
| **样式** | TailwindCSS (可选) | 快速开发 |
### 3.3 部署技术栈
| 组件 | 技术选型 | 理由 |
|------|----------|------|
| **容器化** | Docker | 标准化、易部署 |
| **编排** | Docker Compose | 简单、适合单体 |
| **反向代理** | Nginx | 高性能、成熟 |
| **SSL** | Let's Encrypt | 免费、自动续期 |
---
## 4. 数据模型
### 4.1 核心模型
```python
# User用户
class User(models.Model):
username = models.CharField(max_length=50, unique=True)
email = models.EmailField(unique=True)
password_hash = models.CharField(max_length=255)
created_at = models.DateTimeField(auto_now_add=True)
is_active = models.BooleanField(default=True)
# Meeting会议室
class Meeting(models.Model):
id = models.UUIDField(primary_key=True)
topic = models.CharField(max_length=200)
host = models.ForeignKey(User, on_delete=models.CASCADE)
status = models.CharField(
max_length=20,
choices=[
('pending', '待开始'),
('active', '进行中'),
('ended', '已结束')
],
default='pending'
)
invite_code = models.CharField(max_length=20, unique=True)
created_at = models.DateTimeField(auto_now_add=True)
started_at = models.DateTimeField(null=True, blank=True)
ended_at = models.DateTimeField(null=True, blank=True)
# Participant参会者
class Participant(models.Model):
meeting = models.ForeignKey(Meeting, on_delete=models.CASCADE)
user = models.ForeignKey(User, null=True, on_delete=models.CASCADE)
# Agent 信息
agent_type = models.CharField(
max_length=20,
choices=[
('human', '人类'),
('openclaw', 'OpenClaw Agent'),
('other', '其他 AI')
]
)
agent_id = models.CharField(max_length=100, null=True)
agent_name = models.CharField(max_length=100)
agent_emoji = models.CharField(max_length=10, default='🤖')
# 状态
nickname = models.CharField(max_length=100)
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)
# Message消息
class Message(models.Model):
meeting = models.ForeignKey(Meeting, on_delete=models.CASCADE)
sender = models.ForeignKey(Participant, on_delete=models.CASCADE)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
# 信箱机制
is_broadcast = models.BooleanField(default=True) # 群发消息
requires_response = models.BooleanField(default=False) # 需要回复
in_reply_to = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
# 读取状态
read_by = models.ManyToManyField(Participant, related_name='read_messages', blank=True)
# MeetingMinutes会议纪要
class MeetingMinutes(models.Model):
meeting = models.OneToOneField(Meeting, on_delete=models.CASCADE)
content = models.TextField()
generated_at = models.DateTimeField(auto_now_add=True)
exported_at = models.DateTimeField(null=True, blank=True)
```
### 4.2 索引设计
```python
# 优化查询性能
class Meta:
indexes = [
models.Index(fields=['meeting', 'created_at']),
models.Index(fields=['agent_id', 'meeting']),
models.Index(fields=['is_broadcast', 'created_at']),
]
```
---
## 5. API 设计
### 5.1 API 版本
```
/api/v1/...
```
### 5.2 核心 API
#### 5.2.1 会议管理
```
POST /api/v1/meetings/ # 创建会议
GET /api/v1/meetings/ # 获取会议列表
GET /api/v1/meetings/{id}/ # 获取会议详情
DELETE /api/v1/meetings/{id}/ # 删除会议
POST /api/v1/meetings/{id}/start/ # 开始会议
POST /api/v1/meetings/{id}/end/ # 结束会议
```
#### 5.2.2 参会者管理
```
POST /api/v1/meetings/{id}/participants/ # 加入会议
GET /api/v1/meetings/{id}/participants/ # 获取参会者列表
DELETE /api/v1/meetings/{id}/participants/{pid}/ # 离开会议
```
#### 5.2.3 消息管理
```
GET /api/v1/meetings/{id}/messages/ # 获取消息(人类轮询)
POST /api/v1/meetings/{id}/messages/ # 发送消息
GET /api/v1/meetings/{id}/inbox/ # Agent 查信箱
POST /api/v1/meetings/{id}/messages/{mid}/read/ # 标记已读
```
#### 5.2.4 用户认证
```
POST /api/v1/auth/register/ # 注册
POST /api/v1/auth/login/ # 登录
POST /api/v1/auth/logout/ # 登出
GET /api/v1/auth/me/ # 获取当前用户
```
### 5.3 API 响应格式
```json
// 成功响应
{
"success": true,
"data": { ... },
"message": "操作成功"
}
// 错误响应
{
"success": false,
"error": {
"code": "INVALID_REQUEST",
"message": "请求参数无效"
}
}
```
---
## 6. 轮询机制
### 6.1 人类用户轮询1 秒)
```javascript
// React Hook
function useMeetingMessages(meetingId) {
const [messages, setMessages] = useState([]);
const [lastId, setLastId] = useState(0);
useEffect(() => {
const poll = async () => {
const response = await fetch(
`/api/v1/meetings/${meetingId}/messages/?last_id=${lastId}`
);
const data = await response.json();
if (data.messages.length > 0) {
setMessages(prev => [...prev, ...data.messages]);
setLastId(data.messages[data.messages.length - 1].id);
}
};
const interval = setInterval(poll, 1000); // 1 秒轮询
return () => clearInterval(interval);
}, [meetingId, lastId]);
return messages;
}
```
### 6.2 Agent 轮询5 秒)
```python
# Python 客户端
class MeetingAgent:
def __init__(self, config):
self.check_interval = config.get('check_interval', 5)
def run(self):
while True:
inbox = self.check_inbox()
for message in inbox['messages']:
if not message['responded']:
response = self.generate_response(message)
self.respond(message['id'], response)
time.sleep(self.check_interval)
```
### 6.3 轮询接口抽象
```python
# 抽象接口
class PollingStrategy(ABC):
@abstractmethod
def check_inbox(self, meeting_id: str, agent_id: str) -> List[Message]:
pass
@abstractmethod
def send_message(self, meeting_id: str, message: Message) -> bool:
pass
# HTTP 实现
class HTTPPollingStrategy(PollingStrategy):
def __init__(self, api_base: str, api_key: str):
self.api_base = api_base
self.api_key = api_key
def check_inbox(self, meeting_id: str, agent_id: str) -> List[Message]:
# HTTP GET 实现
pass
# WebSocket 实现(未来扩展)
class WebSocketPollingStrategy(PollingStrategy):
def check_inbox(self, meeting_id: str, agent_id: str) -> List[Message]:
# WebSocket 实现
pass
```
---
## 7. 部署架构
### 7.1 Docker Compose
```yaml
version: '3.8'
services:
web:
build: ./backend
command: gunicorn meeting_room.wsgi:application --bind 0.0.0.0:8000
volumes:
- ./backend:/app
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/meeting_room
- REDIS_URL=redis://redis:6379/0
depends_on:
- db
- redis
db:
image: postgres:15
environment:
- POSTGRES_DB=meeting_room
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
volumes:
- pgdata:/var/lib/postgresql/data
redis:
image: redis:7-alpine
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./ssl:/etc/nginx/ssl
depends_on:
- web
volumes:
pgdata:
```
### 7.2 Nginx 配置
```nginx
server {
listen 80;
server_name meeting.example.com;
location / {
proxy_pass http://web:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /static/ {
alias /app/static/;
}
}
```
---
## 8. 扩展性设计
### 8.1 水平扩展
```
┌──────────────┐
│ Nginx │
│ (负载均衡) │
└──────┬───────┘
┌───┴───┐
│ │
▼ ▼
┌─────┐ ┌─────┐
│ Web │ │ Web │
│ 1 │ │ 2 │
└─────┘ └─────┘
│ │
└───┬───┘
┌───┴───┐
│ │
▼ ▼
┌─────────┐ ┌─────────┐
│ DB │ │ Redis │
│ (主从) │ │ (集群) │
└─────────┘ └─────────┘
```
### 8.2 插件化设计
```python
# Agent 插件接口
class AgentPlugin(ABC):
@abstractmethod
def generate_response(self, message: Message) -> str:
pass
# OpenClaw 插件
class OpenClawPlugin(AgentPlugin):
def generate_response(self, message: Message) -> str:
# OpenClaw 逻辑
pass
# Llama 插件(未来)
class LlamaPlugin(AgentPlugin):
def generate_response(self, message: Message) -> str:
# 调用 Llama API
pass
```
---
## 📝 变更日志
| 版本 | 日期 | 变更内容 | 作者 |
|------|------|----------|------|
| v0.1 | 2026-04-04 | 初始版本 | 飞行侠 |
---
**文档结束** 📝
**创建者**: 飞行侠 🦸
**日期**: 2026-04-04
**状态**: 待北极星确认

17172
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

28
frontend/package.json Normal file
View File

@@ -0,0 +1,28 @@
{
"name": "meeting-room-frontend",
"version": "1.0.0",
"private": true,
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.20.0",
"react-scripts": "5.0.1",
"axios": "^1.6.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

View File

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🏛️ 龙虾议事厅</title>
</head>
<body>
<div id="root"></div>
</body>
</html>

403
frontend/src/App.js Normal file
View File

@@ -0,0 +1,403 @@
import React, { useState, useEffect } from 'react';
import { BrowserRouter as Router, Routes, Route, Link, useNavigate, useParams } from 'react-router-dom';
import axios from 'axios';
const API_BASE = 'http://localhost:8000/api/v1';
// 配置 axios 默认头
axios.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// ============ 登录页面 ============
function LoginPage() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const navigate = useNavigate();
const handleLogin = async (e) => {
e.preventDefault();
try {
const response = await axios.post(`${API_BASE}/auth/login/`, { username, password });
const token = response.data.token;
localStorage.setItem('token', token);
localStorage.setItem('user', JSON.stringify(response.data.user));
navigate('/meetings');
} catch (error) {
alert('登录失败:' + (error.response?.data?.detail || error.message));
}
};
return (
<div style={styles.container}>
<div style={styles.card}>
<h1 style={styles.title}>🏛 龙虾议事厅</h1>
<form onSubmit={handleLogin} style={styles.form}>
<input
type="text"
placeholder="用户名"
value={username}
onChange={(e) => setUsername(e.target.value)}
style={styles.input}
required
/>
<input
type="password"
placeholder="密码"
value={password}
onChange={(e) => setPassword(e.target.value)}
style={styles.input}
required
/>
<button type="submit" style={styles.button}>登录</button>
</form>
<p style={styles.hint}>提示首次使用请先注册超级用户</p>
</div>
</div>
);
}
// ============ 会议列表页面 ============
function MeetingList() {
const [meetings, setMeetings] = useState([]);
const [topic, setTopic] = useState('');
const navigate = useNavigate();
const token = localStorage.getItem('token');
useEffect(() => {
if (!token) {
navigate('/login');
return;
}
fetchMeetings();
}, []);
const fetchMeetings = async () => {
try {
const response = await axios.get(`${API_BASE}/meetings/`, {
headers: { Authorization: `Bearer ${token}` }
});
setMeetings(response.data);
} catch (error) {
console.error('获取会议失败:', error);
}
};
const createMeeting = async (e) => {
e.preventDefault();
try {
const response = await axios.post(
`${API_BASE}/meetings/`,
{ topic },
{ headers: { Authorization: `Bearer ${token}` } }
);
navigate(`/meeting/${response.data.id}`);
} catch (error) {
alert('创建失败:' + (error.response?.data?.detail || error.message));
}
};
const joinMeeting = async (meetingId) => {
const inviteCode = prompt('请输入邀请码:');
if (!inviteCode) return;
try {
await axios.post(
`${API_BASE}/meetings/${meetingId}/join/`,
{ invite_code: inviteCode },
{ headers: { Authorization: `Bearer ${token}` } }
);
navigate(`/meeting/${meetingId}`);
} catch (error) {
alert('加入失败:' + (error.response?.data?.error || error.message));
}
};
const logout = () => {
localStorage.removeItem('token');
navigate('/login');
};
return (
<div style={styles.container}>
<div style={styles.header}>
<h1 style={styles.title}>🏛 我的会议室</h1>
<button onClick={logout} style={styles.logoutBtn}>退出</button>
</div>
<div style={styles.card}>
<h2>创建新会议</h2>
<form onSubmit={createMeeting} style={styles.form}>
<input
type="text"
placeholder="会议主题"
value={topic}
onChange={(e) => setTopic(e.target.value)}
style={styles.input}
required
/>
<button type="submit" style={styles.button}>创建</button>
</form>
</div>
<div style={styles.meetingList}>
{meetings.map(meeting => (
<div key={meeting.id} style={styles.meetingCard}>
<div>
<h3>{meeting.topic}</h3>
<p>状态{meeting.status} | 参会{meeting.participant_count} | 邀请码{meeting.invite_code}</p>
</div>
<div>
<button onClick={() => navigate(`/meeting/${meeting.id}`)} style={styles.smallBtn}>进入</button>
<button onClick={() => joinMeeting(meeting.id)} style={styles.smallBtn}>加入</button>
</div>
</div>
))}
</div>
</div>
);
}
// ============ 会议室页面 ============
function MeetingRoom() {
const { id } = useParams();
const [messages, setMessages] = useState([]);
const [content, setContent] = useState('');
const [participants, setParticipants] = useState([]);
const [lastId, setLastId] = useState(0);
const token = localStorage.getItem('token');
useEffect(() => {
if (!token) return;
fetchParticipants();
fetchMessages();
// 1 秒轮询新消息
const interval = setInterval(fetchMessages, 1000);
return () => clearInterval(interval);
}, []);
const fetchParticipants = async () => {
try {
const response = await axios.get(`${API_BASE}/meetings/${id}/participants/`, {
headers: { Authorization: `Bearer ${token}` }
});
setParticipants(response.data);
} catch (error) {
console.error('获取参会者失败:', error);
}
};
const fetchMessages = async () => {
try {
const response = await axios.get(`${API_BASE}/meetings/${id}/messages/?last_id=${lastId}`, {
headers: { Authorization: `Bearer ${token}` }
});
if (response.data.messages.length > 0) {
setMessages(prev => [...prev, ...response.data.messages]);
setLastId(response.data.messages[response.data.messages.length - 1].id);
}
} catch (error) {
console.error('获取消息失败:', error);
}
};
const sendMessage = async (e) => {
e.preventDefault();
try {
await axios.post(
`${API_BASE}/meetings/${id}/send_message/`,
{ content },
{ headers: { Authorization: `Bearer ${token}` } }
);
setContent('');
fetchMessages();
} catch (error) {
alert('发送失败:' + (error.response?.data?.detail || error.message));
}
};
return (
<div style={styles.container}>
<div style={styles.header}>
<Link to="/meetings" style={styles.backLink}> 返回</Link>
<h1 style={styles.title}>🏛 会议室</h1>
<div style={styles.participants}>
{participants.map(p => (
<span key={p.id} style={styles.participant}>
{p.agent_emoji} {p.nickname}
</span>
))}
</div>
</div>
<div style={styles.chatContainer}>
<div style={styles.messages}>
{messages.map(msg => (
<div key={msg.id} style={styles.message}>
<strong>{msg.sender_emoji} {msg.sender_name}</strong>
<span style={styles.time}>{new Date(msg.created_at).toLocaleTimeString()}</span>
<p>{msg.content}</p>
</div>
))}
</div>
<form onSubmit={sendMessage} style={styles.form}>
<input
type="text"
placeholder="输入消息..."
value={content}
onChange={(e) => setContent(e.target.value)}
style={styles.input}
required
/>
<button type="submit" style={styles.button}>发送</button>
</form>
</div>
</div>
);
}
// ============ 主应用 ============
function App() {
return (
<Router>
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/meetings" element={<MeetingList />} />
<Route path="/meeting/:id" element={<MeetingRoom />} />
<Route path="/" element={<LoginPage />} />
</Routes>
</Router>
);
}
// ============ 样式 ============
const styles = {
container: {
maxWidth: '1200px',
margin: '0 auto',
padding: '20px',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
},
header: {
display: 'flex',
alignItems: 'center',
gap: '20px',
marginBottom: '20px'
},
title: {
margin: '0',
color: '#1a365d'
},
card: {
background: 'white',
borderRadius: '12px',
padding: '20px',
marginBottom: '20px',
boxShadow: '0 4px 6px rgba(0,0,0,0.1)'
},
form: {
display: 'flex',
gap: '10px',
marginBottom: '15px'
},
input: {
flex: 1,
padding: '12px',
border: '2px solid #e2e8f0',
borderRadius: '8px',
fontSize: '1em'
},
button: {
padding: '12px 24px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
color: 'white',
border: 'none',
borderRadius: '8px',
cursor: 'pointer',
fontSize: '1em',
fontWeight: '600'
},
logoutBtn: {
marginLeft: 'auto',
padding: '8px 16px',
background: '#edf2f7',
border: 'none',
borderRadius: '6px',
cursor: 'pointer'
},
meetingList: {
display: 'flex',
flexDirection: 'column',
gap: '15px'
},
meetingCard: {
background: 'white',
borderRadius: '12px',
padding: '20px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
boxShadow: '0 4px 6px rgba(0,0,0,0.1)'
},
smallBtn: {
padding: '8px 16px',
background: '#4299e1',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
marginLeft: '10px'
},
chatContainer: {
background: 'white',
borderRadius: '12px',
padding: '20px',
boxShadow: '0 4px 6px rgba(0,0,0,0.1)'
},
messages: {
maxHeight: '500px',
overflowY: 'auto',
marginBottom: '20px'
},
message: {
padding: '15px',
background: '#f7fafc',
borderRadius: '8px',
marginBottom: '10px'
},
time: {
fontSize: '0.8em',
color: '#718096',
marginLeft: '10px'
},
participants: {
display: 'flex',
gap: '10px',
marginLeft: 'auto'
},
participant: {
background: '#edf2f7',
padding: '6px 12px',
borderRadius: '20px',
fontSize: '0.9em'
},
backLink: {
color: '#4299e1',
textDecoration: 'none',
fontSize: '1.1em'
},
hint: {
color: '#718096',
fontSize: '0.9em',
textAlign: 'center',
marginTop: '15px'
}
};
export default App;

10
frontend/src/index.js Normal file
View File

@@ -0,0 +1,10 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);