405 lines
11 KiB
Markdown
405 lines
11 KiB
Markdown
# 02_generate_alpha_idea.py 微服务迁移方案
|
||
|
||
## 一、功能总结
|
||
|
||
### 1.1 核心功能
|
||
`02_generate_alpha_idea.py` 是一个 Odoo 工作流自动化脚本,主要功能:
|
||
|
||
1. **搜索数据**:在 `alpha.idea` 模型中查找当天创建且状态为 `generated_prompt` 的记录
|
||
2. **遍历处理**:单线程依次处理每条记录
|
||
3. **调用 post_to_ms**:触发 `alpha.idea` 模型的 `post_to_ms()` 方法,将数据发送到微服务
|
||
4. **等待 LLM 返回**:死循环轮询状态,每5秒检查一次
|
||
5. **超时处理**:10分钟超时,超时后在 `result_message` 字段写入 "timeout"
|
||
6. **调用 decode_template**:状态变为 `llm_received` 后,调用 `decode_template()` 方法解码模板
|
||
|
||
### 1.2 关键特性
|
||
- **单线程处理**:不并发,避免 LLM 压力过大
|
||
- **10分钟硬编码超时**:正常2-3分钟,超时即放弃
|
||
- **状态机驱动**:依赖 Odoo 模型的状态字段流转
|
||
|
||
---
|
||
|
||
## 二、微服务架构设计
|
||
|
||
### 2.1 接口设计
|
||
|
||
```
|
||
POST /api/alpha-idea/process
|
||
```
|
||
|
||
**请求参数**:
|
||
```json
|
||
{
|
||
"idea_id": 123 // alpha.idea 记录的 ID
|
||
}
|
||
```
|
||
|
||
**响应**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "处理完成",
|
||
"idea_id": 123,
|
||
"wait_time": 45, // 等待秒数
|
||
"status": "llm_received"
|
||
}
|
||
```
|
||
|
||
### 2.2 处理流程
|
||
|
||
```
|
||
Odoo 按钮点击
|
||
↓
|
||
httpx 请求 FastAPI 微服务
|
||
↓
|
||
POST /api/alpha-idea/process (携带 idea_id)
|
||
↓
|
||
微服务调用 Odoo XML-RPC
|
||
↓
|
||
1. 调用 post_to_ms()
|
||
2. 轮询等待状态 (每5秒)
|
||
3. 超时10分钟或成功
|
||
4. 调用 decode_template()
|
||
↓
|
||
返回结果给 Odoo
|
||
```
|
||
|
||
---
|
||
|
||
## 三、代码实现
|
||
|
||
### 3.1 目录结构
|
||
|
||
```
|
||
rpc_alpha_workflow/
|
||
├── 01_generate_direction.py
|
||
├── 02_generate_alpha_idea.py # 原脚本(保留备份)
|
||
├── reference_01.py
|
||
├── reference_02.py
|
||
├── migration_plan.md # 本文档
|
||
└── alpha_idea_service/ # 微服务目录
|
||
├── main.py # FastAPI 入口
|
||
├── config.py # 配置
|
||
├── odoo_client.py # Odoo XML-RPC 客户端
|
||
└── requirements.txt # 依赖
|
||
```
|
||
|
||
### 3.2 核心代码
|
||
|
||
#### config.py
|
||
```python
|
||
# -*- coding: utf-8 -*-
|
||
"""配置文件"""
|
||
|
||
# Odoo 连接配置
|
||
ODOO_URL = "http://192.168.31.41:32000"
|
||
ODOO_DB = "quantify"
|
||
ODOO_USERNAME = "rpc"
|
||
ODOO_PASSWORD = "aaaAAA111"
|
||
|
||
# 超时配置(硬编码,不对外暴露)
|
||
TIMEOUT_SECONDS = 10 * 60 # 10分钟
|
||
POLL_INTERVAL = 5 # 轮询间隔秒数
|
||
```
|
||
|
||
#### odoo_client.py
|
||
```python
|
||
# -*- coding: utf-8 -*-
|
||
"""Odoo XML-RPC 客户端"""
|
||
from xmlrpc.client import ServerProxy, Fault
|
||
from . import config
|
||
|
||
|
||
class OdooClient:
|
||
def __init__(self):
|
||
self.url = config.ODOO_URL
|
||
self.db = config.ODOO_DB
|
||
self.username = config.ODOO_USERNAME
|
||
self.password = config.ODOO_PASSWORD
|
||
self.uid = None
|
||
self.models = None
|
||
self._authenticate()
|
||
|
||
def _authenticate(self):
|
||
"""认证并获取 uid"""
|
||
common = ServerProxy(f"{self.url}/xmlrpc/2/common")
|
||
self.uid = common.authenticate(self.db, self.username, self.password, {})
|
||
self.models = ServerProxy(f"{self.url}/xmlrpc/2/object")
|
||
|
||
def call(self, model, method, args=None, kwargs=None):
|
||
"""调用 Odoo 方法"""
|
||
args = args or []
|
||
kwargs = kwargs or {}
|
||
return self.models.execute_kw(self.db, self.uid, self.password, model, method, args, kwargs)
|
||
|
||
def post_to_ms(self, idea_id: int):
|
||
"""调用 post_to_ms 方法"""
|
||
try:
|
||
result = self.call('alpha.idea', 'post_to_ms', [[idea_id]])
|
||
return {"success": True, "result": result}
|
||
except Fault as e:
|
||
if "cannot marshal None" in str(e):
|
||
return {"success": True, "result": None}
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def read_record(self, idea_id: int, fields: list):
|
||
"""读取记录"""
|
||
result = self.call('alpha.idea', 'read', [[idea_id], fields])
|
||
return result[0] if result else None
|
||
|
||
def write_record(self, idea_id: int, values: dict):
|
||
"""写入记录"""
|
||
return self.call('alpha.idea', 'write', [[idea_id], values])
|
||
|
||
def decode_template(self, idea_id: int):
|
||
"""调用 decode_template 方法"""
|
||
try:
|
||
result = self.call('alpha.idea', 'decode_template', [[idea_id]])
|
||
return {"success": True, "result": result}
|
||
except Fault as e:
|
||
if "cannot marshal None" in str(e):
|
||
return {"success": True, "result": None}
|
||
return {"success": False, "error": str(e)}
|
||
```
|
||
|
||
#### main.py
|
||
```python
|
||
# -*- coding: utf-8 -*-
|
||
"""FastAPI 微服务入口"""
|
||
import time
|
||
from fastapi import FastAPI
|
||
from pydantic import BaseModel
|
||
from .odoo_client import OdooClient
|
||
from . import config
|
||
|
||
app = FastAPI(title="Alpha Idea Service")
|
||
|
||
|
||
class ProcessRequest(BaseModel):
|
||
idea_id: int
|
||
|
||
|
||
class ProcessResponse(BaseModel):
|
||
success: bool
|
||
message: str
|
||
idea_id: int
|
||
wait_time: float = 0
|
||
status: str = ""
|
||
|
||
|
||
@app.post("/api/alpha-idea/process", response_model=ProcessResponse)
|
||
async def process_idea(request: ProcessRequest):
|
||
"""
|
||
处理单个 alpha.idea 记录
|
||
1. 调用 post_to_ms
|
||
2. 等待状态变为 llm_received(10分钟超时)
|
||
3. 调用 decode_template
|
||
"""
|
||
idea_id = request.idea_id
|
||
client = OdooClient()
|
||
|
||
# 1. 调用 post_to_ms
|
||
post_result = client.post_to_ms(idea_id)
|
||
if not post_result["success"]:
|
||
return ProcessResponse(
|
||
success=False,
|
||
message=f"post_to_ms 失败: {post_result.get('error')}",
|
||
idea_id=idea_id,
|
||
status="failed"
|
||
)
|
||
|
||
# 2. 等待状态变为 llm_received
|
||
start_time = time.time()
|
||
current_status = None
|
||
|
||
while True:
|
||
elapsed_time = time.time() - start_time
|
||
|
||
# 检查超时
|
||
if elapsed_time > config.TIMEOUT_SECONDS:
|
||
# 写入 timeout
|
||
try:
|
||
client.write_record(idea_id, {"result_message": "timeout"})
|
||
except Exception:
|
||
pass
|
||
return ProcessResponse(
|
||
success=False,
|
||
message=f"超时!已等待 {elapsed_time/60:.1f} 分钟",
|
||
idea_id=idea_id,
|
||
wait_time=elapsed_time,
|
||
status="timeout"
|
||
)
|
||
|
||
time.sleep(config.POLL_INTERVAL)
|
||
|
||
# 读取状态
|
||
record = client.read_record(idea_id, ["status", "name"])
|
||
if not record:
|
||
return ProcessResponse(
|
||
success=False,
|
||
message="记录不存在",
|
||
idea_id=idea_id,
|
||
wait_time=elapsed_time,
|
||
status="not_found"
|
||
)
|
||
|
||
current_status = record.get("status")
|
||
|
||
if current_status == "llm_received":
|
||
break
|
||
elif current_status == "failed":
|
||
return ProcessResponse(
|
||
success=False,
|
||
message="处理失败",
|
||
idea_id=idea_id,
|
||
wait_time=elapsed_time,
|
||
status="failed"
|
||
)
|
||
|
||
# 3. 调用 decode_template
|
||
decode_result = client.decode_template(idea_id)
|
||
|
||
return ProcessResponse(
|
||
success=decode_result["success"],
|
||
message="处理完成" if decode_result["success"] else f"decode_template 失败: {decode_result.get('error')}",
|
||
idea_id=idea_id,
|
||
wait_time=time.time() - start_time,
|
||
status=current_status
|
||
)
|
||
|
||
|
||
@app.get("/health")
|
||
async def health_check():
|
||
"""健康检查"""
|
||
return {"status": "ok"}
|
||
```
|
||
|
||
#### requirements.txt
|
||
```
|
||
fastapi
|
||
uvicorn
|
||
pydantic
|
||
```
|
||
|
||
---
|
||
|
||
## 四、Odoo 端改造
|
||
|
||
### 4.1 修改 post_to_ms 方法
|
||
|
||
在 `reference_02.py` 的 `post_to_ms` 方法中,移除直接调用 httpx 发送请求到微服务的逻辑,改为:
|
||
|
||
```python
|
||
def post_to_ms(self):
|
||
"""
|
||
修改为:只准备数据,不直接发送请求
|
||
真正的发送由外部微服务触发
|
||
"""
|
||
# ... 原有校验逻辑不变 ...
|
||
|
||
# 组装请求数据
|
||
payload = {
|
||
'record_id': self.id,
|
||
'prompt': full_prompt,
|
||
'model_name': model_name,
|
||
'base_url': base_url,
|
||
'api_key': api_key,
|
||
'callback_url': callback_url,
|
||
'system_prompt': self.system_prompt,
|
||
'user_prompt': self.user_prompt,
|
||
'final_prompt': self.final_prompt,
|
||
}
|
||
|
||
# 将 payload 保存到某个字段,或者返回给调用方
|
||
# 这里我们返回 payload,让调用方(微服务)来处理
|
||
return payload
|
||
```
|
||
|
||
**或者更简单的方式**:Odoo 按钮点击时,直接 httpx 请求微服务:
|
||
|
||
```python
|
||
def btn_trigger_alpha_idea_service(self):
|
||
"""按钮方法:触发微服务处理"""
|
||
import httpx
|
||
|
||
# 获取微服务地址(从配置读取)
|
||
ms_url = self.env['ir.config_parameter'].sudo().get_param('alpha.idea.service.url')
|
||
|
||
try:
|
||
response = httpx.post(
|
||
f"{ms_url}/api/alpha-idea/process",
|
||
json={"idea_id": self.id},
|
||
timeout=600 # 10分钟超时
|
||
)
|
||
result = response.json()
|
||
|
||
if result["success"]:
|
||
return {
|
||
'type': 'ir.actions.client',
|
||
'tag': 'display_notification',
|
||
'params': {
|
||
'title': '成功',
|
||
'message': f'处理完成,等待时间: {result["wait_time"]:.0f}秒',
|
||
'type': 'success',
|
||
'sticky': False,
|
||
}
|
||
}
|
||
else:
|
||
return {
|
||
'type': 'ir.actions.client',
|
||
'tag': 'display_notification',
|
||
'params': {
|
||
'title': '失败',
|
||
'message': result["message"],
|
||
'type': 'danger',
|
||
'sticky': False,
|
||
}
|
||
}
|
||
except Exception as e:
|
||
return {
|
||
'type': 'ir.actions.client',
|
||
'tag': 'display_notification',
|
||
'params': {
|
||
'title': '错误',
|
||
'message': str(e),
|
||
'type': 'danger',
|
||
'sticky': False,
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 五、部署步骤
|
||
|
||
### 5.1 微服务部署
|
||
|
||
1. 创建目录结构
|
||
2. 复制代码文件
|
||
3. 安装依赖:`pip install -r requirements.txt`
|
||
4. 启动服务:`uvicorn alpha_idea_service.main:app --host 0.0.0.0 --port 32005`
|
||
|
||
### 5.2 Odoo 配置
|
||
|
||
1. 添加系统参数:`alpha.idea.service.url` = `http://微服务地址:32005`
|
||
2. 在 `alpha.idea` 模型添加按钮方法 `btn_trigger_alpha_idea_service`
|
||
3. 在视图添加按钮
|
||
|
||
---
|
||
|
||
## 六、注意事项
|
||
|
||
1. **单线程保证**:微服务内部不开启多线程/异步并发处理单个请求
|
||
2. **超时处理**:HTTP 客户端(Odoo 端)和微服务端都要设置 10 分钟超时
|
||
3. **状态回写**:超时后必须回写 `result_message = 'timeout'`
|
||
4. **错误处理**:任何异常都不能阻塞流程,要返回给调用方
|
||
|
||
---
|
||
|
||
## 七、待确认事项
|
||
|
||
1. 微服务端口是否固定为 32005?
|
||
2. Odoo 端是否需要批量处理接口(一次性传入多个 idea_id)?
|
||
3. 是否需要添加日志记录(loguru 等)?
|
||
4. 是否需要限制同时处理的请求数(比如只处理一个,其他排队)?
|