update
Browse files- Dockerfile +7 -1
- README_APP.md +57 -0
- app.py +0 -7
- docs/development-workflow.md +130 -0
- docs/research-log.md +214 -0
- requirements.txt +4 -2
- roles/requirements-analyst-role.md +19 -0
- roles/software-engineer-role.md +35 -0
- scripts/start_dev.py +21 -0
- scripts/start_prod.py +22 -0
- src/__init__.py +7 -0
- src/api/__init__.py +7 -0
- src/api/routes/__init__.py +7 -0
- src/api/routes/communication.py +130 -0
- src/config/__init__.py +7 -0
- src/config/settings.py +31 -0
- src/core/__init__.py +9 -0
- src/core/adapter.py +237 -0
- src/core/entities.py +25 -0
- src/core/messages.py +35 -0
- src/main.py +33 -0
- src/utils/__init__.py +7 -0
- src/utils/logger.py +30 -0
- tests/__init__.py +3 -0
- tests/fixtures/__init__.py +7 -0
- tests/fixtures/test_data.py +38 -0
- tests/integration/__init__.py +3 -0
- tests/integration/test_communication.py +85 -0
- tests/run_tests.py +53 -0
- tests/unit/__init__.py +3 -0
- tests/unit/test_entities.py +63 -0
- tests/unit/test_messages.py +69 -0
Dockerfile
CHANGED
|
@@ -6,6 +6,7 @@ FROM python:3.12.11
|
|
| 6 |
RUN useradd -m -u 1000 user
|
| 7 |
USER user
|
| 8 |
ENV PATH="/home/user/.local/bin:$PATH"
|
|
|
|
| 9 |
|
| 10 |
WORKDIR /app
|
| 11 |
|
|
@@ -13,4 +14,9 @@ COPY --chown=user ./requirements.txt requirements.txt
|
|
| 13 |
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
| 14 |
|
| 15 |
COPY --chown=user . /app
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
RUN useradd -m -u 1000 user
|
| 7 |
USER user
|
| 8 |
ENV PATH="/home/user/.local/bin:$PATH"
|
| 9 |
+
ENV PYTHONPATH="/app:$PYTHONPATH"
|
| 10 |
|
| 11 |
WORKDIR /app
|
| 12 |
|
|
|
|
| 14 |
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
| 15 |
|
| 16 |
COPY --chown=user . /app
|
| 17 |
+
|
| 18 |
+
# 设置环境变量
|
| 19 |
+
ENV API_HOST="0.0.0.0"
|
| 20 |
+
ENV API_PORT="7860"
|
| 21 |
+
|
| 22 |
+
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "7860"]
|
README_APP.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Human-Clone System
|
| 2 |
+
|
| 3 |
+
Human-Clone 系统是一个多主体通讯协作平台,支持 human 和 clone 之间的实时异步通讯。
|
| 4 |
+
|
| 5 |
+
## 快速开始
|
| 6 |
+
|
| 7 |
+
### 环境要求
|
| 8 |
+
- Python 3.9+
|
| 9 |
+
- Redis 服务器
|
| 10 |
+
|
| 11 |
+
### 安装依赖
|
| 12 |
+
```bash
|
| 13 |
+
pip install -r requirements.txt
|
| 14 |
+
```
|
| 15 |
+
|
| 16 |
+
### 启动方式
|
| 17 |
+
|
| 18 |
+
#### 1. 直接启动(推荐)
|
| 19 |
+
```bash
|
| 20 |
+
uvicorn src.main:app --reload --host 0.0.0.0 --port 7860
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
#### 2. 使用脚本启动
|
| 24 |
+
```bash
|
| 25 |
+
python scripts/start_dev.py
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
#### 3. 开发模式
|
| 29 |
+
```bash
|
| 30 |
+
uvicorn src.main:app --reload --debug --port 7860
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
## API 文档
|
| 34 |
+
启动后访问:
|
| 35 |
+
- Swagger UI: http://localhost:7860/docs
|
| 36 |
+
- ReDoc: http://localhost:7860/redoc
|
| 37 |
+
|
| 38 |
+
## 项目结构
|
| 39 |
+
```
|
| 40 |
+
├── src/ # 源代码目录
|
| 41 |
+
│ ├── core/ # 核心功能模块
|
| 42 |
+
│ ├── config/ # 配置管理
|
| 43 |
+
│ ├── api/ # API路由
|
| 44 |
+
│ └── utils/ # 工具函数
|
| 45 |
+
├── tests/ # 测试代码
|
| 46 |
+
├── scripts/ # 启动脚本
|
| 47 |
+
└── docs/ # 项目文档
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
## 核心功能
|
| 51 |
+
- 多主体(human/clone)实时通讯
|
| 52 |
+
- 基于 Redis 的异步消息传递
|
| 53 |
+
- 支持点对点消息发送
|
| 54 |
+
- 完全解耦的发送/接收队列机制
|
| 55 |
+
|
| 56 |
+
## 开发指南
|
| 57 |
+
详见 `docs/development-workflow.md`
|
app.py
DELETED
|
@@ -1,7 +0,0 @@
|
|
| 1 |
-
from fastapi import FastAPI
|
| 2 |
-
|
| 3 |
-
app = FastAPI()
|
| 4 |
-
|
| 5 |
-
@app.get("/")
|
| 6 |
-
def greet_json():
|
| 7 |
-
return {"Hello": "World!"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docs/development-workflow.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Human-Clone 系统开发流程
|
| 2 |
+
|
| 3 |
+
## 项目概述
|
| 4 |
+
基于需求调研结果的Human-Clone系统开发实施计划
|
| 5 |
+
|
| 6 |
+
## 开发阶段规划
|
| 7 |
+
|
| 8 |
+
### 第一阶段:基础设计
|
| 9 |
+
|
| 10 |
+
#### 1.1 数据结构设计
|
| 11 |
+
- **主体信息结构**
|
| 12 |
+
- UUID(唯一标识)
|
| 13 |
+
- name(显示名称)
|
| 14 |
+
- redis配置(服务器地址、端口、DB、channel)
|
| 15 |
+
|
| 16 |
+
- **消息结构**
|
| 17 |
+
- sender_id(发送者UUID)
|
| 18 |
+
- receiver_id(接收者UUID)
|
| 19 |
+
- timestamp(时间戳)
|
| 20 |
+
- content(消息内容)
|
| 21 |
+
|
| 22 |
+
#### 1.2 Redis-Adapter组件设计
|
| 23 |
+
- **发送模块**
|
| 24 |
+
- 发送队列(thread-safe queue)
|
| 25 |
+
- 发送线程(独立线程循环处理)
|
| 26 |
+
- Redis连接管理
|
| 27 |
+
|
| 28 |
+
- **接收模块**
|
| 29 |
+
- 接收队列(thread-safe queue)
|
| 30 |
+
- 接收线程(独立线程循环处理)
|
| 31 |
+
- 回调注册机制
|
| 32 |
+
|
| 33 |
+
- **配置管理**
|
| 34 |
+
- Redis连接参数
|
| 35 |
+
- 主体身份信息
|
| 36 |
+
|
| 37 |
+
#### 1.3 接口设计
|
| 38 |
+
- **主体注册接口**
|
| 39 |
+
- register_entity(entity_info)
|
| 40 |
+
- unregister_entity(entity_id)
|
| 41 |
+
|
| 42 |
+
- **消息发送接口**
|
| 43 |
+
- send_message(sender_id, receiver_id, content)
|
| 44 |
+
|
| 45 |
+
- **消息接收接口**
|
| 46 |
+
- register_callback(callback_function)
|
| 47 |
+
- unregister_callback()
|
| 48 |
+
|
| 49 |
+
### 第二阶段:核心实现
|
| 50 |
+
|
| 51 |
+
#### 2.1 消息发送流程实现
|
| 52 |
+
- 构造消息对象
|
| 53 |
+
- 放入发送队列
|
| 54 |
+
- 发送线程异步处理
|
| 55 |
+
- Redis连接和发布
|
| 56 |
+
|
| 57 |
+
#### 2.2 消息接收流程实现
|
| 58 |
+
- 监听Redis channel
|
| 59 |
+
- 推送到接收队列
|
| 60 |
+
- 接收线程异步处理
|
| 61 |
+
- 回调函数调用
|
| 62 |
+
|
| 63 |
+
#### 2.3 队列管理实现
|
| 64 |
+
- 线程安全队列设计
|
| 65 |
+
- 生产者-消费者模式
|
| 66 |
+
- 异常处理机制
|
| 67 |
+
|
| 68 |
+
### 第三阶段:测试验证
|
| 69 |
+
|
| 70 |
+
#### 3.1 单元测试
|
| 71 |
+
- 数据结构测试
|
| 72 |
+
- Redis-Adapter组件测试
|
| 73 |
+
- 队列操作测试
|
| 74 |
+
- 线程安全测试
|
| 75 |
+
|
| 76 |
+
#### 3.2 集成测试
|
| 77 |
+
- 发送流程集成测试
|
| 78 |
+
- 接收流程集成测试
|
| 79 |
+
- 端到端通讯测试
|
| 80 |
+
|
| 81 |
+
#### 3.3 性能测试
|
| 82 |
+
- 并发发送测试
|
| 83 |
+
- 大量消息处理测试
|
| 84 |
+
- 内存和性能监控
|
| 85 |
+
|
| 86 |
+
### 第四阶段:优化完善
|
| 87 |
+
|
| 88 |
+
#### 4.1 错误处理
|
| 89 |
+
- 连接失败处理
|
| 90 |
+
- 消息发送失败处理
|
| 91 |
+
- 线程异常处理
|
| 92 |
+
|
| 93 |
+
#### 4.2 监控日志
|
| 94 |
+
- 消息发送接收日志
|
| 95 |
+
- 性能指标监控
|
| 96 |
+
- 异常情况记录
|
| 97 |
+
|
| 98 |
+
## 技术栈选择
|
| 99 |
+
|
| 100 |
+
### 编程语言
|
| 101 |
+
- **推荐**: Python(丰富的Redis库支持,线程处理便利)
|
| 102 |
+
- **备选**: Java/Go(高性能要求场景)
|
| 103 |
+
|
| 104 |
+
### 核心依赖
|
| 105 |
+
- **Redis客户端**: redis-py (Python)
|
| 106 |
+
- **线程库**: threading/queue (Python原生)
|
| 107 |
+
- **JSON处理**: json (Python原生)
|
| 108 |
+
|
| 109 |
+
## 开发顺序
|
| 110 |
+
|
| 111 |
+
1. **数据结构定义** → 2. **Redis连接工具类** → 3. **发送队列模块** → 4. **接收队列模块** → 5. **主控制器** → 6. **测试用例**
|
| 112 |
+
|
| 113 |
+
## 里程碑
|
| 114 |
+
|
| 115 |
+
- **M1**: 完成数据结构和Redis连接(第1-2天)
|
| 116 |
+
- **M2**: 完成发送模块(第3-4天)
|
| 117 |
+
- **M3**: 完成接收模块(第5-6天)
|
| 118 |
+
- **M4**: 完成集成测试(第7-8天)
|
| 119 |
+
|
| 120 |
+
## 风险评估
|
| 121 |
+
|
| 122 |
+
### 技术风险
|
| 123 |
+
- Redis连接稳定性
|
| 124 |
+
- 线程安全问题
|
| 125 |
+
- 队列性能瓶颈
|
| 126 |
+
|
| 127 |
+
### 缓解措施
|
| 128 |
+
- 连接池设计
|
| 129 |
+
- 线程安全机制
|
| 130 |
+
- 队列大小监控
|
docs/research-log.md
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Human-Clone 系统需求调研记录
|
| 2 |
+
|
| 3 |
+
## 调研日期
|
| 4 |
+
2025-01-04
|
| 5 |
+
|
| 6 |
+
## 项目概述
|
| 7 |
+
Human-Clone 系统需求分析
|
| 8 |
+
|
| 9 |
+
## 第一轮调研
|
| 10 |
+
|
| 11 |
+
### 用户回答记录
|
| 12 |
+
**问题**: 这个human-clone系统的主要用途是什么?
|
| 13 |
+
**回答**: human 和 clone 之间相互协作,且可以和其他 clone、human 之间相互通讯的系统。现阶段首先解决的是通讯问题。
|
| 14 |
+
|
| 15 |
+
### 初步需求分析
|
| 16 |
+
- **核心功能**: 多主体间通讯协作系统
|
| 17 |
+
- **主体类型**: human, clone
|
| 18 |
+
- **通讯模式**:
|
| 19 |
+
- human ↔ clone
|
| 20 |
+
- clone ↔ clone
|
| 21 |
+
- human ↔ human (通过系统)
|
| 22 |
+
- **当前优先级**: 通讯功能
|
| 23 |
+
|
| 24 |
+
### 下一步调研方向
|
| 25 |
+
需要深入了解通讯的具体需求和技术细节。
|
| 26 |
+
|
| 27 |
+
### 第二轮调研记录
|
| 28 |
+
|
| 29 |
+
**问题**: 这些主体之间的通讯主要是通过什么形式?文字、语音、还是视频?
|
| 30 |
+
**回答**: 第一步先解决主体之间文字形式的通讯
|
| 31 |
+
|
| 32 |
+
### 需求更新
|
| 33 |
+
- **通讯方式**: 首先实现文字通讯
|
| 34 |
+
- **开发阶段**: 第一阶段专注文字通讯功能
|
| 35 |
+
- **通讯模式**: 实时异步通讯
|
| 36 |
+
|
| 37 |
+
### 第三轮调研记录
|
| 38 |
+
|
| 39 |
+
**问题**: 系统需要支持实时通讯(如聊天室形式)还是异步通讯(如邮件/留言形式)?
|
| 40 |
+
**回答**: 基本属于实施通讯,但是是异步的,计划采用 redis 订阅的方式来实现信息传递,每一个主体都会有一个 redis-adapter,负责发送和接收消息
|
| 41 |
+
|
| 42 |
+
### 技术架构初步确定
|
| 43 |
+
- **消息传递**: Redis 订阅/发布模式
|
| 44 |
+
- **组件设计**: 每个主体配备 redis-adapter
|
| 45 |
+
- **消息处理**: 发送/接收消息统一处理
|
| 46 |
+
|
| 47 |
+
### 第四轮调研记录
|
| 48 |
+
|
| 49 |
+
**问题**: 每个主体(human/clone)需要有自己的身份标识吗?比如用户名、ID之类的?
|
| 50 |
+
**回答**: 需要有 id —— uuid 格式、实体名称(name)、redis-adapter 里和 redis 相关订阅 channel 相关的配置信息
|
| 51 |
+
|
| 52 |
+
### 主体身份设计
|
| 53 |
+
- **唯一标识**: UUID格式ID
|
| 54 |
+
- **显示名称**: 实体名称(name)
|
| 55 |
+
- **配置信息**: redis-adapter配置(订阅channel等)
|
| 56 |
+
|
| 57 |
+
### 第五轮调研记录
|
| 58 |
+
|
| 59 |
+
**问题**: 消息需要有哪些基本字段?比如发送者、接收者、时间戳、消息内容?
|
| 60 |
+
**回答**: 需要,其中发送者、接收者都使用 id
|
| 61 |
+
|
| 62 |
+
### 消息结构设计
|
| 63 |
+
- **发送者**: UUID格式ID
|
| 64 |
+
- **接收者**: UUID格式ID
|
| 65 |
+
- **时间戳**: 消息发送时间
|
| 66 |
+
- **消息内容**: 文字内容
|
| 67 |
+
- **消息类型**: 点对点通讯
|
| 68 |
+
|
| 69 |
+
### 第六轮调研记录
|
| 70 |
+
|
| 71 |
+
**问题**: 是否需要支持群发消息(一个发送者同时发给多个接收者)?
|
| 72 |
+
**回答**: 此阶段不需要
|
| 73 |
+
|
| 74 |
+
### 功能范围明确
|
| 75 |
+
- **当前阶段**: 仅支持点对点消息
|
| 76 |
+
- **群发功能**: 后续版本考虑
|
| 77 |
+
|
| 78 |
+
### 第七轮调研记录
|
| 79 |
+
|
| 80 |
+
**问题**: 系统需要存储历史消息记录吗?
|
| 81 |
+
**回答**: 需要
|
| 82 |
+
|
| 83 |
+
### 数据持久化需求
|
| 84 |
+
- **消息存储**: 需要保存历史消息记录
|
| 85 |
+
- **存储方案**: 待确定(数据库选择)
|
| 86 |
+
|
| 87 |
+
### 第八轮调研记录
|
| 88 |
+
|
| 89 |
+
**问题**: 历史消息需要保存多长时间?
|
| 90 |
+
**回答**: 先不讨论历史记录的问题,先解决通信问题
|
| 91 |
+
|
| 92 |
+
### 开发优先级调整
|
| 93 |
+
- **当前重点**: 通讯功能实现
|
| 94 |
+
- **历史记录**: 后续阶段处理
|
| 95 |
+
|
| 96 |
+
### 第九轮调研记录
|
| 97 |
+
|
| 98 |
+
**问题**: redis-adapter 需要哪些具体的配置信息?比如 Redis 服务器地址、端口、认证信息等?
|
| 99 |
+
**回答**: redis 服务器地址、端口、db、channel
|
| 100 |
+
|
| 101 |
+
### redis-adapter 配置设计
|
| 102 |
+
- **服务器地址**: Redis host
|
| 103 |
+
- **端口**: Redis port
|
| 104 |
+
- **数据库**: Redis DB number
|
| 105 |
+
- **频道**: 订阅/发布的 channel
|
| 106 |
+
|
| 107 |
+
### 第十轮调研记录
|
| 108 |
+
|
| 109 |
+
**问题**: 每个主体是使用相同的 channel 还是各自有独立的 channel?如果是独立的,channel 的命名规则是什么?
|
| 110 |
+
**回答**: 每个主体都是独立的 channel,channel 的命名就是主体的 id
|
| 111 |
+
|
| 112 |
+
### Channel 设计方案
|
| 113 |
+
- **Channel 分配**: 每个主体独立 channel
|
| 114 |
+
- **命名规则**: 使用主体的 UUID 作为 channel 名称
|
| 115 |
+
- **通讯机制**: 目标主体监听自己的 channel 接收消息
|
| 116 |
+
|
| 117 |
+
### 第十一轮调研记录
|
| 118 |
+
|
| 119 |
+
**问题**: 当一个主体要发送消息给另一个主体时,是如何知道接收者的 channel 名称的?是否需要一个主体注册发现机制?
|
| 120 |
+
**回答**: 主体注册发现机制可以放到下一个迭代,此次只假设可以通过目标主体的 id 就可以获得目标主体的订阅 redis 参数,包括服务器、端口、db、channel
|
| 121 |
+
|
| 122 |
+
### 主体发现机制设计
|
| 123 |
+
- **当前版本**: 假设可通过 ID 获取目标的 Redis 连接参数
|
| 124 |
+
- **后续迭代**: 实现主体注册发现机制
|
| 125 |
+
- **参数获取**: 服务器地址、端口、DB、channel 通过 ID 直接获得
|
| 126 |
+
|
| 127 |
+
### 第十二轮调研记录
|
| 128 |
+
|
| 129 |
+
**问题**: redis-adapter 是否需要处理连接断开重连、消息发送失败重试等异常情况?
|
| 130 |
+
**回答**: 目前不需要
|
| 131 |
+
|
| 132 |
+
### 异常处理设计
|
| 133 |
+
- **当前版本**: 不处理连接重连和重试机制
|
| 134 |
+
- **设计原则**: 专注核心通讯功能,异常处理后续考虑
|
| 135 |
+
|
| 136 |
+
### 第十三轮调研记录
|
| 137 |
+
|
| 138 |
+
**问题**: 消息的发送和接收是否需要确认机制?比如接收者收到消息后需要回复确认?
|
| 139 |
+
**回答**: 不需要
|
| 140 |
+
|
| 141 |
+
### 消息确认机制
|
| 142 |
+
- **确认机制**: 不需要消息确认
|
| 143 |
+
- **通讯模式**: 单向发送,无回复确认
|
| 144 |
+
- **简化设计**: 降低复杂度,专注基础通讯
|
| 145 |
+
|
| 146 |
+
### 第十四轮调研记录
|
| 147 |
+
|
| 148 |
+
**问题**: 系统是否需要区分不同类型的消息?比如普通消息、系统消息、命令消息等?
|
| 149 |
+
**回答**: 不需要
|
| 150 |
+
|
| 151 |
+
### 消息类型设计
|
| 152 |
+
- **消息分类**: 不区分消息类型
|
| 153 |
+
- **统一处理**: 所有消息均为普通文字消息
|
| 154 |
+
- **简化架构**: 无需消息类型判断和处理逻辑
|
| 155 |
+
|
| 156 |
+
### 第十五轮调研记录
|
| 157 |
+
|
| 158 |
+
**问题**: 每个主体的 redis-adapter 是需要同时监听多个 channel(包括自己的和其他主体的)还是只监听自己的 channel?
|
| 159 |
+
**回答**: 只监听自己的
|
| 160 |
+
|
| 161 |
+
### 监听机制设计
|
| 162 |
+
- **监听范围**: 每个主体只监听自己的 channel
|
| 163 |
+
- **消息接收**: 通过订阅自己的 ID channel 接收消息
|
| 164 |
+
- **消息发送**: 向目标主体的 ID channel 发布消息
|
| 165 |
+
|
| 166 |
+
### 需求调研总结
|
| 167 |
+
|
| 168 |
+
基于15轮调研,Human-Clone 系统第一版本的核心需求已明确:
|
| 169 |
+
|
| 170 |
+
#### 系统架构
|
| 171 |
+
- **主体类型**: human、clone
|
| 172 |
+
- **通讯模式**: 点对点文字通讯
|
| 173 |
+
- **技术方案**: Redis 订阅/发布模式
|
| 174 |
+
- **身份标识**: UUID + name
|
| 175 |
+
|
| 176 |
+
#### 核心组件
|
| 177 |
+
- **redis-adapter**: 每个主体一个,负责发送/接收消息
|
| 178 |
+
- **配置参数**: 服务器地址、端口、DB、channel(=主体ID)
|
| 179 |
+
- **监听机制**: 只监听自己的 channel
|
| 180 |
+
|
| 181 |
+
#### 消息格式
|
| 182 |
+
- **发送者ID**: UUID格式
|
| 183 |
+
- **接收者ID**: UUID格式
|
| 184 |
+
- **时间戳**: 发送时间
|
| 185 |
+
- **内容**: 文字消息
|
| 186 |
+
|
| 187 |
+
#### 功能边界
|
| 188 |
+
- ✅ 点对点文字通讯
|
| 189 |
+
- ❌ 群发消息
|
| 190 |
+
- ❌ 历史消息存储
|
| 191 |
+
- ❌ 连接重连机制
|
| 192 |
+
- ❌ 消息确认机制
|
| 193 |
+
- ❌ 消息类型区分
|
| 194 |
+
- ❌ 主体注册发现
|
| 195 |
+
|
| 196 |
+
### 第十六轮调研记录
|
| 197 |
+
|
| 198 |
+
**问题**: 发送消息时是否需要引入发送缓存队列来解耦主体发送和Redis发送操作?
|
| 199 |
+
**回答**: 在发送主体不直接调用发送消息到 redis,而是在 redis-adapter 里设置一个发送缓存 queue,通过线程循环逐个 pop 后发送到 redis,这样主体发送和 redis 发送分开,且有缓存,解耦
|
| 200 |
+
|
| 201 |
+
### 第十七轮调研记录
|
| 202 |
+
|
| 203 |
+
**问题**: 消息接收部分是否也需要类似的队列解耦设计?
|
| 204 |
+
**回答**: 消息接收部分,目标主体 B 的redis-adapter 收到 channel 里的消息后,推送到接收队列 receiver-queue,在线程里循环读取 queue,并调用 callback,给到 B 的函数里进行处理
|
| 205 |
+
|
| 206 |
+
### 完整架构设计优化
|
| 207 |
+
- **发送缓存**: redis-adapter内部设置发送队列
|
| 208 |
+
- **接收缓存**: redis-adapter内部设置接收队列
|
| 209 |
+
- **双向异步**: 发送和接收均采用队列+线程机制
|
| 210 |
+
- **回调机制**: 接收线程通过callback将消息传递给主体
|
| 211 |
+
- **完全解耦**: Redis操作与主体处理完全分离
|
| 212 |
+
|
| 213 |
+
#### 下一步行动
|
| 214 |
+
基于明确的需求和完整的双向异步架构设计,可以开始进行技术设计和开发实现。
|
requirements.txt
CHANGED
|
@@ -1,2 +1,4 @@
|
|
| 1 |
-
|
| 2 |
-
|
|
|
|
|
|
|
|
|
| 1 |
+
redis>=5.0.0
|
| 2 |
+
fastapi>=0.104.0
|
| 3 |
+
uvicorn[standard]>=0.24.0
|
| 4 |
+
pydantic-settings>=2.0.0
|
roles/requirements-analyst-role.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 需求分析师角色设定
|
| 2 |
+
|
| 3 |
+
## 角色定义
|
| 4 |
+
软件系统需求分析师
|
| 5 |
+
|
| 6 |
+
## 职责
|
| 7 |
+
- 分析和梳理软件系统需求
|
| 8 |
+
- 进行需求调研和评估
|
| 9 |
+
- 编写需求规格文档
|
| 10 |
+
- 协助项目规划和设计
|
| 11 |
+
|
| 12 |
+
## 工作要求
|
| 13 |
+
1. 深入挖掘系统需求
|
| 14 |
+
2. 每次与用户提问时,一次只提出一个问题
|
| 15 |
+
3. 避免对用户造成不必要的干扰
|
| 16 |
+
4. 将调研过程及报告写入根目录下的 docs 文件夹
|
| 17 |
+
|
| 18 |
+
## 当前项目
|
| 19 |
+
Human-Clone 系统需求分析
|
roles/software-engineer-role.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 软件开发工程师角色设定
|
| 2 |
+
|
| 3 |
+
## 角色定义
|
| 4 |
+
软件开发工程师 - Human-Clone 系统实现
|
| 5 |
+
|
| 6 |
+
## 职责
|
| 7 |
+
- 根据需求分析和设计文档进行代码实现
|
| 8 |
+
- 编写高质量、可维护的代码
|
| 9 |
+
- 进行单元测试和集成测试
|
| 10 |
+
- 代码重构和性能优化
|
| 11 |
+
- 技术文档编写
|
| 12 |
+
|
| 13 |
+
## 技术要求
|
| 14 |
+
- 熟练掌握 Python 编程
|
| 15 |
+
- Redis 操作和 pub/sub 机制
|
| 16 |
+
- 多线程编程和并发处理
|
| 17 |
+
- 队列数据结构和线程安全
|
| 18 |
+
- 软件架构设计和模式应用
|
| 19 |
+
|
| 20 |
+
## 工作原则
|
| 21 |
+
1. 遵循 SOLID 设计原则
|
| 22 |
+
2. 编写可测试的代码
|
| 23 |
+
3. 关注代码可读性和可维护性
|
| 24 |
+
4. 进行充分的测试验证
|
| 25 |
+
5. 及时更新技术文档
|
| 26 |
+
|
| 27 |
+
## 当前项目
|
| 28 |
+
Human-Clone 系统核心通讯功能开发
|
| 29 |
+
|
| 30 |
+
## 开发任务
|
| 31 |
+
- 实现数据结构定义
|
| 32 |
+
- 开发 Redis-Adapter 组件
|
| 33 |
+
- 实现发送/接收队列机制
|
| 34 |
+
- 编写测试用例
|
| 35 |
+
- 性能优化和错误处理
|
scripts/start_dev.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
开发启动脚本
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import uvicorn
|
| 6 |
+
from src.config import settings
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def main():
|
| 10 |
+
"""开发环境启动"""
|
| 11 |
+
uvicorn.run(
|
| 12 |
+
"src.main:app",
|
| 13 |
+
host=settings.api_host,
|
| 14 |
+
port=settings.api_port,
|
| 15 |
+
reload=settings.debug,
|
| 16 |
+
log_level=settings.log_level.lower()
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
if __name__ == "__main__":
|
| 21 |
+
main()
|
scripts/start_prod.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
生产启动脚本
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import uvicorn
|
| 6 |
+
from src.config import settings
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def main():
|
| 10 |
+
"""生产环境启动"""
|
| 11 |
+
uvicorn.run(
|
| 12 |
+
"src.main:app",
|
| 13 |
+
host=settings.api_host,
|
| 14 |
+
port=settings.api_port,
|
| 15 |
+
reload=False,
|
| 16 |
+
log_level=settings.log_level.lower(),
|
| 17 |
+
workers=4
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
if __name__ == "__main__":
|
| 22 |
+
main()
|
src/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Human-Clone System
|
| 3 |
+
多主体通讯协作系统
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
__version__ = "1.0.0"
|
| 7 |
+
__author__ = "Human-Clone Team"
|
src/api/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
API模块初始化
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from .routes import communication
|
| 6 |
+
|
| 7 |
+
__all__ = ["communication"]
|
src/api/routes/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
路由模块初始化
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from .communication import router
|
| 6 |
+
|
| 7 |
+
__all__ = ["router"]
|
src/api/routes/communication.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
通讯API路由
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from fastapi import APIRouter, HTTPException
|
| 6 |
+
from pydantic import BaseModel
|
| 7 |
+
from typing import List
|
| 8 |
+
import uuid
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
|
| 11 |
+
from ...core import EntityInfo, Message, RedisAdapter
|
| 12 |
+
|
| 13 |
+
router = APIRouter()
|
| 14 |
+
|
| 15 |
+
# 全局存储(生产环境应使用数据库)
|
| 16 |
+
entities: dict[str, EntityInfo] = {}
|
| 17 |
+
adapters: dict[str, RedisAdapter] = {}
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class EntityCreate(BaseModel):
|
| 21 |
+
name: str
|
| 22 |
+
redis_host: str = "localhost"
|
| 23 |
+
redis_port: int = 6379
|
| 24 |
+
redis_db: int = 0
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class MessageSend(BaseModel):
|
| 28 |
+
sender_id: str
|
| 29 |
+
receiver_id: str
|
| 30 |
+
content: str
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class MessageResponse(BaseModel):
|
| 34 |
+
sender_id: str
|
| 35 |
+
receiver_id: str
|
| 36 |
+
timestamp: str
|
| 37 |
+
content: str
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
@router.post("/entities", response_model=dict)
|
| 41 |
+
async def create_entity(entity: EntityCreate):
|
| 42 |
+
"""创建新实体"""
|
| 43 |
+
entity_id = str(uuid.uuid4())
|
| 44 |
+
channel = entity_id
|
| 45 |
+
|
| 46 |
+
entity_info = EntityInfo(
|
| 47 |
+
id=entity_id,
|
| 48 |
+
name=entity.name,
|
| 49 |
+
redis_host=entity.redis_host,
|
| 50 |
+
redis_port=entity.redis_port,
|
| 51 |
+
redis_db=entity.redis_db,
|
| 52 |
+
channel=channel
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
entities[entity_id] = entity_info
|
| 56 |
+
|
| 57 |
+
return {
|
| 58 |
+
"id": entity_id,
|
| 59 |
+
"name": entity.name,
|
| 60 |
+
"status": "created"
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
@router.get("/entities", response_model=List[dict])
|
| 65 |
+
async def list_entities():
|
| 66 |
+
"""获取所有实体列表"""
|
| 67 |
+
return [
|
| 68 |
+
{
|
| 69 |
+
"id": entity_id,
|
| 70 |
+
"name": entity.name,
|
| 71 |
+
"redis_host": entity.redis_host,
|
| 72 |
+
"redis_port": entity.redis_port,
|
| 73 |
+
"redis_db": entity.redis_db
|
| 74 |
+
}
|
| 75 |
+
for entity_id, entity in entities.items()
|
| 76 |
+
]
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
@router.post("/entities/{entity_id}/start")
|
| 80 |
+
async def start_entity(entity_id: str):
|
| 81 |
+
"""启动实体通讯"""
|
| 82 |
+
if entity_id not in entities:
|
| 83 |
+
raise HTTPException(status_code=404, detail="Entity not found")
|
| 84 |
+
|
| 85 |
+
if entity_id in adapters:
|
| 86 |
+
raise HTTPException(status_code=400, detail="Entity already started")
|
| 87 |
+
|
| 88 |
+
adapter = RedisAdapter(entities[entity_id])
|
| 89 |
+
if adapter.start():
|
| 90 |
+
adapters[entity_id] = adapter
|
| 91 |
+
return {"status": "started"}
|
| 92 |
+
else:
|
| 93 |
+
raise HTTPException(status_code=500, detail="Failed to start entity")
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
@router.post("/entities/{entity_id}/stop")
|
| 97 |
+
async def stop_entity(entity_id: str):
|
| 98 |
+
"""停止实体通讯"""
|
| 99 |
+
if entity_id not in adapters:
|
| 100 |
+
raise HTTPException(status_code=404, detail="Entity not started")
|
| 101 |
+
|
| 102 |
+
adapters[entity_id].stop()
|
| 103 |
+
del adapters[entity_id]
|
| 104 |
+
|
| 105 |
+
return {"status": "stopped"}
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
@router.post("/messages/send")
|
| 109 |
+
async def send_message(message: MessageSend):
|
| 110 |
+
"""发送消息"""
|
| 111 |
+
if message.sender_id not in adapters:
|
| 112 |
+
raise HTTPException(status_code=404, detail="Sender not started")
|
| 113 |
+
|
| 114 |
+
if message.receiver_id not in entities:
|
| 115 |
+
raise HTTPException(status_code=404, detail="Receiver not found")
|
| 116 |
+
|
| 117 |
+
adapter = adapters[message.sender_id]
|
| 118 |
+
success = adapter.send_message(message.receiver_id, message.content)
|
| 119 |
+
|
| 120 |
+
if success:
|
| 121 |
+
return {"status": "sent"}
|
| 122 |
+
else:
|
| 123 |
+
raise HTTPException(status_code=500, detail="Failed to send message")
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
@router.get("/entities/{entity_id}/messages", response_model=List[MessageResponse])
|
| 127 |
+
async def get_entity_messages(entity_id: str):
|
| 128 |
+
"""获取实体的消息历史(临时存储,演示用)"""
|
| 129 |
+
# 这里只是示例,实际应该从持久化存储获取
|
| 130 |
+
return []
|
src/config/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
配置模块初始化
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from .settings import settings
|
| 6 |
+
|
| 7 |
+
__all__ = ["settings"]
|
src/config/settings.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
配置管理模块
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from pydantic_settings import BaseSettings
|
| 6 |
+
from typing import Optional
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class Settings(BaseSettings):
|
| 10 |
+
"""系统配置"""
|
| 11 |
+
|
| 12 |
+
# Redis配置
|
| 13 |
+
redis_host: str = "localhost"
|
| 14 |
+
redis_port: int = 6379
|
| 15 |
+
redis_db: int = 0
|
| 16 |
+
redis_password: Optional[str] = None
|
| 17 |
+
|
| 18 |
+
# API配置
|
| 19 |
+
api_host: str = "0.0.0.0"
|
| 20 |
+
api_port: int = 7860
|
| 21 |
+
debug: bool = True
|
| 22 |
+
|
| 23 |
+
# 日志配置
|
| 24 |
+
log_level: str = "INFO"
|
| 25 |
+
|
| 26 |
+
class Config:
|
| 27 |
+
env_file = ".env"
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# 全局配置实例
|
| 31 |
+
settings = Settings()
|
src/core/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
核心模块初始化
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from .entities import EntityInfo
|
| 6 |
+
from .messages import Message
|
| 7 |
+
from .adapter import RedisAdapter
|
| 8 |
+
|
| 9 |
+
__all__ = ["EntityInfo", "Message", "RedisAdapter"]
|
src/core/adapter.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Redis适配器 - 处理消息发送和接收
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import redis
|
| 6 |
+
import json
|
| 7 |
+
import threading
|
| 8 |
+
import queue
|
| 9 |
+
import time
|
| 10 |
+
import logging
|
| 11 |
+
from dataclasses import dataclass
|
| 12 |
+
from datetime import datetime
|
| 13 |
+
from typing import Optional, Callable, Dict, Any
|
| 14 |
+
|
| 15 |
+
from .entities import EntityInfo
|
| 16 |
+
from .messages import Message
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
# 配置日志
|
| 20 |
+
logging.basicConfig(level=logging.INFO)
|
| 21 |
+
logger = logging.getLogger(__name__)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
# 类型定义
|
| 25 |
+
MessageCallback = Callable[[Message], None]
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
@dataclass
|
| 29 |
+
class RedisConfig:
|
| 30 |
+
"""Redis连接配置"""
|
| 31 |
+
host: str
|
| 32 |
+
port: int
|
| 33 |
+
db: int
|
| 34 |
+
password: Optional[str] = None
|
| 35 |
+
|
| 36 |
+
def to_connection_params(self) -> Dict[str, Any]:
|
| 37 |
+
"""获取连接参数"""
|
| 38 |
+
params = {
|
| 39 |
+
'host': self.host,
|
| 40 |
+
'port': self.port,
|
| 41 |
+
'db': self.db
|
| 42 |
+
}
|
| 43 |
+
if self.password:
|
| 44 |
+
params['password'] = self.password
|
| 45 |
+
return params
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class RedisAdapter:
|
| 49 |
+
"""Redis适配器 - 处理消息发送和接收"""
|
| 50 |
+
|
| 51 |
+
def __init__(self, entity_info: EntityInfo):
|
| 52 |
+
self.entity_info = entity_info
|
| 53 |
+
self.redis_config = RedisConfig(
|
| 54 |
+
host=entity_info.redis_host,
|
| 55 |
+
port=entity_info.redis_port,
|
| 56 |
+
db=entity_info.redis_db
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
# Redis连接
|
| 60 |
+
self.redis_client: Optional[redis.Redis] = None
|
| 61 |
+
|
| 62 |
+
# 发送相关
|
| 63 |
+
self.send_queue = queue.Queue()
|
| 64 |
+
self.send_thread = None
|
| 65 |
+
self.send_running = False
|
| 66 |
+
|
| 67 |
+
# 接收相关
|
| 68 |
+
self.receive_queue = queue.Queue()
|
| 69 |
+
self.receive_thread = None
|
| 70 |
+
self.receive_running = False
|
| 71 |
+
self.message_callback: Optional[MessageCallback] = None
|
| 72 |
+
|
| 73 |
+
# 连接相关
|
| 74 |
+
self.pubsub: Optional[redis.client.PubSub] = None
|
| 75 |
+
self.connected = False
|
| 76 |
+
|
| 77 |
+
def connect(self) -> bool:
|
| 78 |
+
"""连接到Redis服务器"""
|
| 79 |
+
try:
|
| 80 |
+
self.redis_client = redis.Redis(**self.redis_config.to_connection_params())
|
| 81 |
+
self.redis_client.ping() # 测试连接
|
| 82 |
+
self.connected = True
|
| 83 |
+
logger.info(f"Entity {self.entity_info.id} connected to Redis")
|
| 84 |
+
return True
|
| 85 |
+
except Exception as e:
|
| 86 |
+
logger.error(f"Failed to connect to Redis: {e}")
|
| 87 |
+
return False
|
| 88 |
+
|
| 89 |
+
def disconnect(self):
|
| 90 |
+
"""断开Redis连接"""
|
| 91 |
+
self.stop()
|
| 92 |
+
if self.redis_client:
|
| 93 |
+
self.redis_client.close()
|
| 94 |
+
self.connected = False
|
| 95 |
+
logger.info(f"Entity {self.entity_info.id} disconnected from Redis")
|
| 96 |
+
|
| 97 |
+
def start(self) -> bool:
|
| 98 |
+
"""启动适配器(发送和接收线程)"""
|
| 99 |
+
if not self.connect():
|
| 100 |
+
return False
|
| 101 |
+
|
| 102 |
+
self.send_running = True
|
| 103 |
+
self.receive_running = True
|
| 104 |
+
|
| 105 |
+
# 启动发送线程
|
| 106 |
+
self.send_thread = threading.Thread(target=self._send_worker, daemon=True)
|
| 107 |
+
self.send_thread.start()
|
| 108 |
+
|
| 109 |
+
# 启动接收线程
|
| 110 |
+
self.receive_thread = threading.Thread(target=self._receive_worker, daemon=True)
|
| 111 |
+
self.receive_thread.start()
|
| 112 |
+
|
| 113 |
+
logger.info(f"RedisAdapter started for entity {self.entity_info.id}")
|
| 114 |
+
return True
|
| 115 |
+
|
| 116 |
+
def stop(self):
|
| 117 |
+
"""停止适配器"""
|
| 118 |
+
self.send_running = False
|
| 119 |
+
self.receive_running = False
|
| 120 |
+
|
| 121 |
+
# 等待线程结束
|
| 122 |
+
if self.send_thread and self.send_thread.is_alive():
|
| 123 |
+
self.send_thread.join(timeout=5)
|
| 124 |
+
if self.receive_thread and self.receive_thread.is_alive():
|
| 125 |
+
self.receive_thread.join(timeout=5)
|
| 126 |
+
|
| 127 |
+
logger.info(f"RedisAdapter stopped for entity {self.entity_info.id}")
|
| 128 |
+
|
| 129 |
+
def send_message(self, receiver_id: str, content: str) -> bool:
|
| 130 |
+
"""发送消息(异步)"""
|
| 131 |
+
try:
|
| 132 |
+
message = Message(
|
| 133 |
+
sender_id=self.entity_info.id,
|
| 134 |
+
receiver_id=receiver_id,
|
| 135 |
+
timestamp=datetime.now(),
|
| 136 |
+
content=content
|
| 137 |
+
)
|
| 138 |
+
self.send_queue.put(message)
|
| 139 |
+
logger.debug(f"Message queued for {receiver_id}")
|
| 140 |
+
return True
|
| 141 |
+
except Exception as e:
|
| 142 |
+
logger.error(f"Failed to queue message: {e}")
|
| 143 |
+
return False
|
| 144 |
+
|
| 145 |
+
def register_callback(self, callback: MessageCallback):
|
| 146 |
+
"""注册消息接收回调函数"""
|
| 147 |
+
self.message_callback = callback
|
| 148 |
+
|
| 149 |
+
def _send_worker(self):
|
| 150 |
+
"""发送工作线程"""
|
| 151 |
+
logger.info("Send worker thread started")
|
| 152 |
+
|
| 153 |
+
while self.send_running:
|
| 154 |
+
try:
|
| 155 |
+
# 从队列获取消息(超时1秒)
|
| 156 |
+
message = self.send_queue.get(timeout=1)
|
| 157 |
+
|
| 158 |
+
# 这里需要获取接收者的Redis连接信息
|
| 159 |
+
# 当前简化处理,使用同一个Redis实例
|
| 160 |
+
target_channel = message.receiver_id
|
| 161 |
+
|
| 162 |
+
# 发送到Redis
|
| 163 |
+
if self.redis_client:
|
| 164 |
+
self.redis_client.publish(
|
| 165 |
+
target_channel,
|
| 166 |
+
json.dumps(message.to_dict())
|
| 167 |
+
)
|
| 168 |
+
logger.debug(f"Message sent to channel {target_channel}")
|
| 169 |
+
|
| 170 |
+
self.send_queue.task_done()
|
| 171 |
+
|
| 172 |
+
except queue.Empty:
|
| 173 |
+
continue
|
| 174 |
+
except Exception as e:
|
| 175 |
+
logger.error(f"Error in send worker: {e}")
|
| 176 |
+
|
| 177 |
+
logger.info("Send worker thread stopped")
|
| 178 |
+
|
| 179 |
+
def _receive_worker(self):
|
| 180 |
+
"""接收工作线程"""
|
| 181 |
+
logger.info("Receive worker thread started")
|
| 182 |
+
|
| 183 |
+
if not self.redis_client:
|
| 184 |
+
logger.error("Redis client not available for receiving")
|
| 185 |
+
return
|
| 186 |
+
|
| 187 |
+
try:
|
| 188 |
+
# 创建pubsub对象
|
| 189 |
+
self.pubsub = self.redis_client.pubsub()
|
| 190 |
+
self.pubsub.subscribe(self.entity_info.channel)
|
| 191 |
+
|
| 192 |
+
while self.receive_running:
|
| 193 |
+
try:
|
| 194 |
+
# 获取消息(超时1秒)
|
| 195 |
+
message = self.pubsub.get_message(timeout=1)
|
| 196 |
+
|
| 197 |
+
if message and message['type'] == 'message':
|
| 198 |
+
# 解析消息数据
|
| 199 |
+
message_data = json.loads(message['data'].decode('utf-8'))
|
| 200 |
+
received_message = Message.from_dict(message_data)
|
| 201 |
+
|
| 202 |
+
# 放入接收队列
|
| 203 |
+
self.receive_queue.put(received_message)
|
| 204 |
+
|
| 205 |
+
# 处理接收队列中的消息
|
| 206 |
+
self._process_receive_queue()
|
| 207 |
+
|
| 208 |
+
except Exception as e:
|
| 209 |
+
logger.error(f"Error processing received message: {e}")
|
| 210 |
+
|
| 211 |
+
except Exception as e:
|
| 212 |
+
logger.error(f"Error in receive worker: {e}")
|
| 213 |
+
finally:
|
| 214 |
+
if self.pubsub:
|
| 215 |
+
self.pubsub.close()
|
| 216 |
+
|
| 217 |
+
logger.info("Receive worker thread stopped")
|
| 218 |
+
|
| 219 |
+
def _process_receive_queue(self):
|
| 220 |
+
"""处理接收队列中的消息"""
|
| 221 |
+
try:
|
| 222 |
+
while not self.receive_queue.empty():
|
| 223 |
+
message = self.receive_queue.get_nowait()
|
| 224 |
+
|
| 225 |
+
if self.message_callback:
|
| 226 |
+
try:
|
| 227 |
+
self.message_callback(message)
|
| 228 |
+
logger.debug(f"Message delivered to callback")
|
| 229 |
+
except Exception as e:
|
| 230 |
+
logger.error(f"Error in message callback: {e}")
|
| 231 |
+
|
| 232 |
+
self.receive_queue.task_done()
|
| 233 |
+
|
| 234 |
+
except queue.Empty:
|
| 235 |
+
pass
|
| 236 |
+
except Exception as e:
|
| 237 |
+
logger.error(f"Error processing receive queue: {e}")
|
src/core/entities.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
实体信息结构
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from dataclasses import dataclass
|
| 6 |
+
from typing import Optional
|
| 7 |
+
import uuid
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@dataclass
|
| 11 |
+
class EntityInfo:
|
| 12 |
+
"""主体信息结构"""
|
| 13 |
+
id: str # UUID格式唯一标识
|
| 14 |
+
name: str # 显示名称
|
| 15 |
+
redis_host: str # Redis服务器地址
|
| 16 |
+
redis_port: int # Redis端口
|
| 17 |
+
redis_db: int # Redis数据库编号
|
| 18 |
+
channel: str # 订阅channel名称
|
| 19 |
+
|
| 20 |
+
def __post_init__(self):
|
| 21 |
+
"""数据验证"""
|
| 22 |
+
if not self.id:
|
| 23 |
+
self.id = str(uuid.uuid4())
|
| 24 |
+
if not self.channel:
|
| 25 |
+
self.channel = self.id
|
src/core/messages.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
消息结构定义
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from dataclasses import dataclass
|
| 6 |
+
from typing import Dict, Any
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@dataclass
|
| 11 |
+
class Message:
|
| 12 |
+
"""消息结构"""
|
| 13 |
+
sender_id: str # 发送者UUID
|
| 14 |
+
receiver_id: str # 接收者UUID
|
| 15 |
+
timestamp: datetime # 时间戳
|
| 16 |
+
content: str # 消息内容
|
| 17 |
+
|
| 18 |
+
def to_dict(self) -> Dict[str, Any]:
|
| 19 |
+
"""转换为字典格式"""
|
| 20 |
+
return {
|
| 21 |
+
'sender_id': self.sender_id,
|
| 22 |
+
'receiver_id': self.receiver_id,
|
| 23 |
+
'timestamp': self.timestamp.isoformat(),
|
| 24 |
+
'content': self.content
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
@classmethod
|
| 28 |
+
def from_dict(cls, data: Dict[str, Any]) -> 'Message':
|
| 29 |
+
"""从字典创建消息对象"""
|
| 30 |
+
return cls(
|
| 31 |
+
sender_id=data['sender_id'],
|
| 32 |
+
receiver_id=data['receiver_id'],
|
| 33 |
+
timestamp=datetime.fromisoformat(data['timestamp']),
|
| 34 |
+
content=data['content']
|
| 35 |
+
)
|
src/main.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
FastAPI应用主入口
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from fastapi import FastAPI
|
| 6 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 7 |
+
from src.api.routes.communication import router as comm_router
|
| 8 |
+
|
| 9 |
+
app = FastAPI(
|
| 10 |
+
title="Human-Clone API",
|
| 11 |
+
description="Human-Clone系统通讯接口",
|
| 12 |
+
version="1.0.0"
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
# CORS中间件
|
| 16 |
+
app.add_middleware(
|
| 17 |
+
CORSMiddleware,
|
| 18 |
+
allow_origins=["*"],
|
| 19 |
+
allow_credentials=True,
|
| 20 |
+
allow_methods=["*"],
|
| 21 |
+
allow_headers=["*"],
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
# 注册路由
|
| 25 |
+
app.include_router(comm_router, prefix="/api/v1")
|
| 26 |
+
|
| 27 |
+
@app.get("/")
|
| 28 |
+
async def root():
|
| 29 |
+
return {"message": "Human-Clone API"}
|
| 30 |
+
|
| 31 |
+
@app.get("/health")
|
| 32 |
+
async def health_check():
|
| 33 |
+
return {"status": "healthy"}
|
src/utils/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
工具模块初始化
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from .logger import setup_logger
|
| 6 |
+
|
| 7 |
+
__all__ = ["setup_logger"]
|
src/utils/logger.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
日志工具
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import logging
|
| 6 |
+
import sys
|
| 7 |
+
from typing import Optional
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def setup_logger(
|
| 11 |
+
name: str,
|
| 12 |
+
level: str = "INFO",
|
| 13 |
+
format_string: Optional[str] = None
|
| 14 |
+
) -> logging.Logger:
|
| 15 |
+
"""设置日志器"""
|
| 16 |
+
|
| 17 |
+
if format_string is None:
|
| 18 |
+
format_string = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
| 19 |
+
|
| 20 |
+
logger = logging.getLogger(name)
|
| 21 |
+
logger.setLevel(getattr(logging, level.upper()))
|
| 22 |
+
|
| 23 |
+
# 避免重复添加handler
|
| 24 |
+
if not logger.handlers:
|
| 25 |
+
handler = logging.StreamHandler(sys.stdout)
|
| 26 |
+
formatter = logging.Formatter(format_string)
|
| 27 |
+
handler.setFormatter(formatter)
|
| 28 |
+
logger.addHandler(handler)
|
| 29 |
+
|
| 30 |
+
return logger
|
tests/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
测试模块初始化
|
| 3 |
+
"""
|
tests/fixtures/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
测试数据模块初始化
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from .test_data import create_test_entity_a, create_test_entity_b, get_test_messages
|
| 6 |
+
|
| 7 |
+
__all__ = ["create_test_entity_a", "create_test_entity_b", "get_test_messages"]
|
tests/fixtures/test_data.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
测试数据fixture
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from src.core import EntityInfo
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def create_test_entity_a():
|
| 9 |
+
"""创建测试实体A"""
|
| 10 |
+
return EntityInfo(
|
| 11 |
+
id="test-entity-a-001",
|
| 12 |
+
name="Test Entity A",
|
| 13 |
+
redis_host="localhost",
|
| 14 |
+
redis_port=6379,
|
| 15 |
+
redis_db=0,
|
| 16 |
+
channel="test-entity-a-001"
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def create_test_entity_b():
|
| 21 |
+
"""创建测试实体B"""
|
| 22 |
+
return EntityInfo(
|
| 23 |
+
id="test-entity-b-002",
|
| 24 |
+
name="Test Entity B",
|
| 25 |
+
redis_host="localhost",
|
| 26 |
+
redis_port=6379,
|
| 27 |
+
redis_db=0,
|
| 28 |
+
channel="test-entity-b-002"
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def get_test_messages():
|
| 33 |
+
"""获取测试消息列表"""
|
| 34 |
+
return [
|
| 35 |
+
"Hello from Test Entity A!",
|
| 36 |
+
"This is a test message",
|
| 37 |
+
"Human-Clone system working"
|
| 38 |
+
]
|
tests/integration/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
集成测试模块初始化
|
| 3 |
+
"""
|
tests/integration/test_communication.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Human-Clone 系统集成测试
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import sys
|
| 6 |
+
import os
|
| 7 |
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
| 8 |
+
|
| 9 |
+
import unittest
|
| 10 |
+
import time
|
| 11 |
+
import threading
|
| 12 |
+
from datetime import datetime
|
| 13 |
+
from src.core import EntityInfo, RedisAdapter
|
| 14 |
+
from tests.fixtures import create_test_entity_a, create_test_entity_b, get_test_messages
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class TestCommunication(unittest.TestCase):
|
| 18 |
+
"""通讯集成测试类"""
|
| 19 |
+
|
| 20 |
+
def test_basic_communication(self):
|
| 21 |
+
"""测试基础通讯功能"""
|
| 22 |
+
print("=== Human-Clone 系统通讯测试 ===")
|
| 23 |
+
|
| 24 |
+
# 创建测试主体
|
| 25 |
+
entity_a = create_test_entity_a()
|
| 26 |
+
entity_b = create_test_entity_b()
|
| 27 |
+
|
| 28 |
+
# 创建适配器
|
| 29 |
+
adapter_a = RedisAdapter(entity_a)
|
| 30 |
+
adapter_b = RedisAdapter(entity_b)
|
| 31 |
+
|
| 32 |
+
# 消息接收回调
|
| 33 |
+
received_messages = []
|
| 34 |
+
|
| 35 |
+
def message_callback(message):
|
| 36 |
+
received_messages.append(message)
|
| 37 |
+
print(f"[{message.receiver_id}] 收到来自 {message.sender_id} 的消息: {message.content}")
|
| 38 |
+
|
| 39 |
+
# 启动适配器
|
| 40 |
+
print("\n1. 启动适配器...")
|
| 41 |
+
if adapter_a.start() and adapter_b.start():
|
| 42 |
+
print("✓ 适配器启动成功")
|
| 43 |
+
|
| 44 |
+
# 注册回调
|
| 45 |
+
adapter_b.register_callback(message_callback)
|
| 46 |
+
|
| 47 |
+
# 等待连接建立
|
| 48 |
+
time.sleep(1)
|
| 49 |
+
|
| 50 |
+
# 发送测试消息
|
| 51 |
+
print("\n2. 发送测试消息...")
|
| 52 |
+
test_messages = get_test_messages()
|
| 53 |
+
|
| 54 |
+
for msg in test_messages:
|
| 55 |
+
if adapter_a.send_message(entity_b.id, msg):
|
| 56 |
+
print(f"✓ 消息已发送: {msg}")
|
| 57 |
+
time.sleep(0.5)
|
| 58 |
+
|
| 59 |
+
# 等待消息接收
|
| 60 |
+
print("\n3. 等待消息接收...")
|
| 61 |
+
time.sleep(2)
|
| 62 |
+
|
| 63 |
+
# 检查结果
|
| 64 |
+
print(f"\n4. 测试结果:")
|
| 65 |
+
print(f" 发送消息数: {len(test_messages)}")
|
| 66 |
+
print(f" 接收消息数: {len(received_messages)}")
|
| 67 |
+
|
| 68 |
+
if len(received_messages) == len(test_messages):
|
| 69 |
+
print("✓ 通讯测试通过!")
|
| 70 |
+
else:
|
| 71 |
+
print("✗ 通讯测试失败!")
|
| 72 |
+
|
| 73 |
+
else:
|
| 74 |
+
print("✗ 适配器启动失败")
|
| 75 |
+
|
| 76 |
+
# 清理资源
|
| 77 |
+
print("\n5. 清理资源...")
|
| 78 |
+
adapter_a.stop()
|
| 79 |
+
adapter_b.stop()
|
| 80 |
+
|
| 81 |
+
print("\n=== 测试完成 ===")
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
if __name__ == "__main__":
|
| 85 |
+
unittest.main()
|
tests/run_tests.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
批量运行所有测试
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import unittest
|
| 6 |
+
import sys
|
| 7 |
+
import os
|
| 8 |
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def run_unit_tests():
|
| 12 |
+
"""运行单元测试"""
|
| 13 |
+
print("=== 运行单元测试 ===")
|
| 14 |
+
loader = unittest.TestLoader()
|
| 15 |
+
suite = loader.discover('tests/unit', pattern='test_*.py')
|
| 16 |
+
runner = unittest.TextTestRunner(verbosity=2)
|
| 17 |
+
result = runner.run(suite)
|
| 18 |
+
return result.wasSuccessful()
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def run_integration_tests():
|
| 22 |
+
"""运行集成测试"""
|
| 23 |
+
print("\n=== 运行集成测试 ===")
|
| 24 |
+
loader = unittest.TestLoader()
|
| 25 |
+
suite = loader.discover('tests/integration', pattern='test_*.py')
|
| 26 |
+
runner = unittest.TextTestRunner(verbosity=2)
|
| 27 |
+
result = runner.run(suite)
|
| 28 |
+
return result.wasSuccessful()
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def run_all_tests():
|
| 32 |
+
"""运行所有测试"""
|
| 33 |
+
print("Human-Clone 系统测试套件")
|
| 34 |
+
print("=" * 50)
|
| 35 |
+
|
| 36 |
+
unit_success = run_unit_tests()
|
| 37 |
+
integration_success = run_integration_tests()
|
| 38 |
+
|
| 39 |
+
print("\n=== 测试结果汇总 ===")
|
| 40 |
+
print(f"单元测试: {'✓ 通过' if unit_success else '✗ 失败'}")
|
| 41 |
+
print(f"集成测试: {'✓ 通过' if integration_success else '✗ 失败'}")
|
| 42 |
+
|
| 43 |
+
if unit_success and integration_success:
|
| 44 |
+
print("🎉 所有测试通过!")
|
| 45 |
+
return True
|
| 46 |
+
else:
|
| 47 |
+
print("❌ 存在测试失败")
|
| 48 |
+
return False
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
if __name__ == "__main__":
|
| 52 |
+
success = run_all_tests()
|
| 53 |
+
sys.exit(0 if success else 1)
|
tests/unit/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
单元测试模块初始化
|
| 3 |
+
"""
|
tests/unit/test_entities.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
实体信息测试
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import sys
|
| 6 |
+
import os
|
| 7 |
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
| 8 |
+
|
| 9 |
+
import unittest
|
| 10 |
+
from src.core import EntityInfo
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class TestEntityInfo(unittest.TestCase):
|
| 14 |
+
"""测试EntityInfo类"""
|
| 15 |
+
|
| 16 |
+
def test_entity_creation(self):
|
| 17 |
+
"""测试实体创建"""
|
| 18 |
+
entity = EntityInfo(
|
| 19 |
+
id="test-001",
|
| 20 |
+
name="Test Entity",
|
| 21 |
+
redis_host="localhost",
|
| 22 |
+
redis_port=6379,
|
| 23 |
+
redis_db=0,
|
| 24 |
+
channel="test-channel"
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
self.assertEqual(entity.id, "test-001")
|
| 28 |
+
self.assertEqual(entity.name, "Test Entity")
|
| 29 |
+
self.assertEqual(entity.redis_host, "localhost")
|
| 30 |
+
self.assertEqual(entity.redis_port, 6379)
|
| 31 |
+
self.assertEqual(entity.redis_db, 0)
|
| 32 |
+
self.assertEqual(entity.channel, "test-channel")
|
| 33 |
+
|
| 34 |
+
def test_auto_uuid_generation(self):
|
| 35 |
+
"""测试UUID自动生成"""
|
| 36 |
+
entity = EntityInfo(
|
| 37 |
+
id="", # 空ID应该自动生成UUID
|
| 38 |
+
name="Test Entity",
|
| 39 |
+
redis_host="localhost",
|
| 40 |
+
redis_port=6379,
|
| 41 |
+
redis_db=0,
|
| 42 |
+
channel="test-channel"
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
self.assertIsNotNone(entity.id)
|
| 46 |
+
self.assertNotEqual(entity.id, "")
|
| 47 |
+
|
| 48 |
+
def test_auto_channel_assignment(self):
|
| 49 |
+
"""测试channel自动赋值"""
|
| 50 |
+
entity = EntityInfo(
|
| 51 |
+
id="test-001",
|
| 52 |
+
name="Test Entity",
|
| 53 |
+
redis_host="localhost",
|
| 54 |
+
redis_port=6379,
|
| 55 |
+
redis_db=0,
|
| 56 |
+
channel="" # 空channel应该自动使用ID
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
self.assertEqual(entity.channel, "test-001")
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
if __name__ == "__main__":
|
| 63 |
+
unittest.main()
|
tests/unit/test_messages.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
消息结构测试
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import sys
|
| 6 |
+
import os
|
| 7 |
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
| 8 |
+
|
| 9 |
+
import unittest
|
| 10 |
+
from datetime import datetime
|
| 11 |
+
from src.core import Message
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class TestMessage(unittest.TestCase):
|
| 15 |
+
"""测试Message类"""
|
| 16 |
+
|
| 17 |
+
def test_message_creation(self):
|
| 18 |
+
"""测试消息创建"""
|
| 19 |
+
timestamp = datetime.now()
|
| 20 |
+
message = Message(
|
| 21 |
+
sender_id="sender-001",
|
| 22 |
+
receiver_id="receiver-001",
|
| 23 |
+
timestamp=timestamp,
|
| 24 |
+
content="Test message content"
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
self.assertEqual(message.sender_id, "sender-001")
|
| 28 |
+
self.assertEqual(message.receiver_id, "receiver-001")
|
| 29 |
+
self.assertEqual(message.timestamp, timestamp)
|
| 30 |
+
self.assertEqual(message.content, "Test message content")
|
| 31 |
+
|
| 32 |
+
def test_message_to_dict(self):
|
| 33 |
+
"""测试消息转换为字典"""
|
| 34 |
+
timestamp = datetime.now()
|
| 35 |
+
message = Message(
|
| 36 |
+
sender_id="sender-001",
|
| 37 |
+
receiver_id="receiver-001",
|
| 38 |
+
timestamp=timestamp,
|
| 39 |
+
content="Test message"
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
message_dict = message.to_dict()
|
| 43 |
+
|
| 44 |
+
self.assertEqual(message_dict['sender_id'], "sender-001")
|
| 45 |
+
self.assertEqual(message_dict['receiver_id'], "receiver-001")
|
| 46 |
+
self.assertEqual(message_dict['timestamp'], timestamp.isoformat())
|
| 47 |
+
self.assertEqual(message_dict['content'], "Test message")
|
| 48 |
+
|
| 49 |
+
def test_message_from_dict(self):
|
| 50 |
+
"""测试从字典创建消息"""
|
| 51 |
+
timestamp = datetime.now()
|
| 52 |
+
message_dict = {
|
| 53 |
+
'sender_id': 'sender-001',
|
| 54 |
+
'receiver_id': 'receiver-001',
|
| 55 |
+
'timestamp': timestamp.isoformat(),
|
| 56 |
+
'content': 'Test message'
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
message = Message.from_dict(message_dict)
|
| 60 |
+
|
| 61 |
+
self.assertEqual(message.sender_id, "sender-001")
|
| 62 |
+
self.assertEqual(message.receiver_id, "receiver-001")
|
| 63 |
+
self.assertEqual(message.content, "Test message")
|
| 64 |
+
# 比较datetime对象可能会有毫秒级差异,所以转换为字符串比较
|
| 65 |
+
self.assertEqual(message.timestamp.isoformat(), timestamp.isoformat())
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
if __name__ == "__main__":
|
| 69 |
+
unittest.main()
|