# 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. 是否需要限制同时处理的请求数(比如只处理一个,其他排队)?