Files
alpha_tools/rpc_alpha_workflow/migration_plan.md
T
2026-03-26 18:12:48 +08:00

405 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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. 是否需要限制同时处理的请求数(比如只处理一个,其他排队)?