合并远程报价模板 AI 解析与超时设置:保留私聊图片和附件询价。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-18 17:47:48 +08:00
co-authored by Cursor
41 changed files with 4972 additions and 438 deletions
+4 -1
View File
@@ -211,7 +211,10 @@ class WorkerRuntime:
return
entry_id, data = claimed[0]
try:
process_recognition(data)
process_recognition(
data,
allow_network=bool(self.settings.llm_allow_network),
)
finally:
stream.ack(entry_id)
+108 -18
View File
@@ -1,7 +1,9 @@
"""
识别任务壳:入队 Redis Stream,Worker HTTP 槽认领后跑占位处理。
识别任务:入队 Redis Stream,Worker HTTP 槽认领后处理。
本文件职责:识别入队;Worker 槽消费。kind=vision 跑私聊图片波次,kind=attachment 跑附件材料,其它 kind 仍占位。
本文件职责:enqueue;process_recognition 按 kind 分支。
kind=vision 跑私聊图片波次,kind=attachment 跑附件材料,
kind=quote_template_parse 跑报价模板 AI 映射;其它 kind 仍占位。
禁止:回调线程同步等识别完成;禁止本模块写主账六态。
"""
@@ -41,7 +43,7 @@ def enqueue_recognition(
"""
识别任务入队(快回)。
kind 例:attachment_text / vision / sheet_map。
kind 例:attachment / vision / quote_template_parse。
"""
rt = redis or get_redis_runtime()
if idempotency_key:
@@ -62,14 +64,29 @@ def enqueue_recognition(
return RecognitionEnqueueResult(accepted=True, entry_id=entry_id)
def process_recognition(data: dict[str, Any]) -> dict[str, Any]:
def process_recognition_stub(data: dict[str, Any]) -> dict[str, Any]:
"""
Worker 槽:vision 跑图片波次,其它 kind 仍走占位壳。
识别占位:回显 kind,不读附件、不调模型。
兼容旧调用名;正式路径请用 process_recognition。
禁止再调本函数以免与 process_recognition 互相递归。
"""
return process_recognition(data, allow_network=False)
def process_recognition(data: dict[str, Any], *, allow_network: bool = True) -> dict[str, Any]:
"""
Worker 槽:按 kind 处理识别/映射任务。
vision:私聊图片波次收口。
attachment:H5 附件材料收口。
quote_template_parse:调千问 sheet_map,再 PUT 主账 parseResult。
禁止在本函数写六态。
"""
kind = str(data.get("kind") or "")
kind = str(data.get("kind") or "unknown")
payload = data.get("payload") if isinstance(data.get("payload"), dict) else {}
if not isinstance(payload, dict):
payload = {}
if kind == "vision":
from agent.handlers.image_inquiry import finish_queued_image
@@ -78,21 +95,94 @@ def process_recognition(data: dict[str, Any]) -> dict[str, Any]:
from agent.handlers.attachment_inquiry import finish_queued_attachment
return finish_queued_attachment(payload)
return process_recognition_stub(data)
def process_recognition_stub(data: dict[str, Any]) -> dict[str, Any]:
"""
识别占位:回显 kind,不读附件、不调模型。
Worker 槽调用;正式实现替换为本函数旁的真实处理器。
"""
kind = str(data.get("kind") or "unknown")
payload = data.get("payload") or {}
logger.info("recognition.stub kind=%s keys=%s", kind, sorted(payload.keys()) if isinstance(payload, dict) else [])
if kind == "quote_template_parse":
return _process_quote_template_parse(payload, allow_network=allow_network)
logger.info(
"recognition.stub kind=%s keys=%s",
kind,
sorted(payload.keys()),
)
return {
"ok": True,
"stub": True,
"kind": kind,
"note": "识别壳:未跑真实抽取",
}
def _process_quote_template_parse(payload: dict[str, Any], *, allow_network: bool) -> dict[str, Any]:
"""跑 sheet_map 并回写主账;失败也必须回写 failed+原因。"""
from agent.config import get_settings
from agent.llm.mode_sheet_map import invoke_sheet_map
version_id = str(payload.get("versionId") or "").strip()
settings = get_settings()
net = bool(allow_network and settings.llm_allow_network)
result = invoke_sheet_map(payload, allow_network=net)
body: dict[str, Any]
if result.get("ok") and isinstance(result.get("mapping"), dict):
mapping = dict(result["mapping"])
gaps = result.get("gaps_after")
if not isinstance(gaps, list):
gaps = mapping.get("mappingGaps") if isinstance(mapping.get("mappingGaps"), list) else []
mapping["mappingGaps"] = [str(g) for g in gaps if g]
# 运营可读摘要:点数之外固定带上未解析项
n_fields = len(mapping.get("fields") or {})
n_fee = len(mapping.get("feeRegions") or []) if isinstance(mapping.get("feeRegions"), list) else 0
n_route = len(mapping.get("routeRegions") or []) if isinstance(mapping.get("routeRegions"), list) else 0
points = n_fields + n_fee + n_route
parse_message = f"已解析 {points} 个映射点"
if mapping["mappingGaps"]:
preview = ";".join(mapping["mappingGaps"][:8])
more = len(mapping["mappingGaps"]) - 8
parse_message += f";未解析到 {len(mapping['mappingGaps'])} 项:{preview}"
if more > 0:
parse_message += f" 等共{len(mapping['mappingGaps'])}项"
body = {
"versionId": version_id,
"ok": True,
"mappingJson": mapping,
"parseStatus": "parsed",
"parseMessage": parse_message,
}
else:
err = str(result.get("error") or "AI 解析失败")
body = {
"versionId": version_id,
"ok": False,
"parseStatus": "failed",
"parseMessage": err,
}
posted = _post_parse_result(body)
logger.info(
"quote_template_parse done versionId=%s ok=%s posted=%s",
version_id,
body.get("ok"),
posted.get("ok"),
)
return {"ok": bool(body.get("ok")), "posted": posted, "kind": "quote_template_parse"}
def _post_parse_result(body: dict[str, Any]) -> dict[str, Any]:
"""PUT /inquiry/agent/quoteTemplate/parseResult。"""
try:
from agent.ledger import LedgerClient
client = LedgerClient.from_settings()
resp = client.request_json(
"PUT",
"/inquiry/agent/quoteTemplate/parseResult",
json_body=body,
idempotency_key=f"parseResult:{body.get('versionId')}",
)
if not resp.ok:
logger.warning(
"parseResult 回写失败 status=%s err=%s",
resp.status_code,
resp.error,
)
return {"ok": False, "error": resp.error or f"http_{resp.status_code}"}
return {"ok": True, "data": resp.data}
except Exception as exc:
logger.exception("parseResult 回写异常")
return {"ok": False, "error": str(exc)}