合并远程报价模板 AI 解析与超时设置:保留私聊图片和附件询价。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
"""
|
||||
报价模板 AI 映射入队路由。
|
||||
|
||||
本文件职责:校验 callback token,接收主账单元格清单,写入 recognition Stream 后快回。
|
||||
禁止:在本请求线程调千问 / 等完整 mapping;禁止直连 MySQL。
|
||||
Worker:recognition 槽认领 kind=quote_template_parse 后跑 mode_sheet_map。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, Optional
|
||||
|
||||
from fastapi import APIRouter, Header, HTTPException, Response
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from agent.config import get_settings
|
||||
from agent.jobs.recognition import enqueue_recognition
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/internal/quote-template", tags=["quote-template"])
|
||||
|
||||
|
||||
class QuoteTemplateParseBody(BaseModel):
|
||||
"""主账提交的模板映射解析任务。"""
|
||||
|
||||
version_id: str = Field(default="", alias="versionId")
|
||||
template_id: str = Field(default="", alias="templateId")
|
||||
template_name: str = Field(default="", alias="templateName")
|
||||
file_name: str = Field(default="", alias="fileName")
|
||||
biz_type: str = Field(default="", alias="bizType")
|
||||
transport_mode: str = Field(default="sea", alias="transportMode")
|
||||
sheet: str = ""
|
||||
cell_inventory: list[Any] = Field(default_factory=list, alias="cellInventory")
|
||||
cell_count: int = Field(default=0, alias="cellCount")
|
||||
yellow_count: int = Field(default=0, alias="yellowCount")
|
||||
truncated: bool = False
|
||||
request_id: str = Field(default="", alias="requestId")
|
||||
|
||||
model_config = {"populate_by_name": True}
|
||||
|
||||
|
||||
def _expected_token() -> str:
|
||||
s = get_settings()
|
||||
return (s.agent_callback_token or s.inquiry_agent_callback_token or "").strip()
|
||||
|
||||
|
||||
def _check_token(authorization: Optional[str], x_agent_token: Optional[str]) -> None:
|
||||
expected = _expected_token()
|
||||
if not expected:
|
||||
if (get_settings().ytd_env or "").lower() == "prod":
|
||||
raise HTTPException(status_code=503, detail="callback token 未配置")
|
||||
logger.warning("quote-template parse:未配置 callback token,test 允许空校验")
|
||||
return
|
||||
provided = (x_agent_token or "").strip()
|
||||
if not provided and authorization:
|
||||
auth = authorization.strip()
|
||||
if auth.lower().startswith("bearer "):
|
||||
provided = auth[7:].strip()
|
||||
else:
|
||||
provided = auth
|
||||
if provided != expected:
|
||||
raise HTTPException(status_code=401, detail="unauthorized")
|
||||
|
||||
|
||||
@router.get("/ping")
|
||||
def parse_ping() -> dict[str, str]:
|
||||
return {"status": "ok", "channel": "quote-template-parse"}
|
||||
|
||||
|
||||
@router.post("/parse")
|
||||
def enqueue_parse(
|
||||
body: QuoteTemplateParseBody,
|
||||
response: Response,
|
||||
authorization: Optional[str] = Header(default=None),
|
||||
x_agent_callback_token: Optional[str] = Header(
|
||||
default=None, alias="X-Agent-Callback-Token"
|
||||
),
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
接收主账解析任务:入 recognition 队列后立即 202。
|
||||
|
||||
不在此调用千问。
|
||||
"""
|
||||
_check_token(authorization, x_agent_callback_token)
|
||||
version_id = (body.version_id or "").strip()
|
||||
if not version_id:
|
||||
raise HTTPException(status_code=400, detail="versionId 必填")
|
||||
inventory = body.cell_inventory or []
|
||||
if not inventory:
|
||||
raise HTTPException(status_code=400, detail="cellInventory 不能为空")
|
||||
|
||||
payload = {
|
||||
"versionId": version_id,
|
||||
"templateId": body.template_id,
|
||||
"templateName": body.template_name,
|
||||
"fileName": body.file_name,
|
||||
"bizType": body.biz_type,
|
||||
"transportMode": (body.transport_mode or "sea").strip().lower() or "sea",
|
||||
"sheet": body.sheet or "",
|
||||
"cellInventory": inventory,
|
||||
"cellCount": body.cell_count or len(inventory),
|
||||
"yellowCount": body.yellow_count,
|
||||
"truncated": bool(body.truncated),
|
||||
}
|
||||
result = enqueue_recognition(
|
||||
kind="quote_template_parse",
|
||||
payload=payload,
|
||||
idempotency_key=(body.request_id or "").strip()
|
||||
or f"quote-tpl-parse:{version_id}:{len(inventory)}",
|
||||
)
|
||||
if not result.accepted:
|
||||
# 幂等重复:视为已接受,避免主账把「重复」当成失败
|
||||
if result.reason == "duplicate_idempotency_key":
|
||||
response.status_code = 202
|
||||
return {"accepted": True, "entry_id": "", "reason": "duplicate_idempotency_key"}
|
||||
response.status_code = 503
|
||||
return {"accepted": False, "reason": result.reason or "enqueue_failed"}
|
||||
|
||||
response.status_code = 202
|
||||
logger.info(
|
||||
"quote_template.parse.enqueued versionId=%s cells=%s entry=%s",
|
||||
version_id,
|
||||
len(inventory),
|
||||
result.entry_id,
|
||||
)
|
||||
return {"accepted": True, "entry_id": result.entry_id}
|
||||
Reference in New Issue
Block a user