落地报价模板 AI 解析与对象存储,并增加回复超时系统设置页。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
linmianxun
2026-09-18 17:42:46 +08:00
co-authored by Cursor
parent 982b3a2317
commit 527050bbf9
41 changed files with 4965 additions and 426 deletions
+5 -2
View File
@@ -19,7 +19,7 @@ from agent.jobs.archive import archive_stream, process_archive_stub
from agent.jobs.libreoffice import LibreOfficeConverter
from agent.jobs.pdf_job import pdf_stream, process_pdf_job
from agent.jobs.quote_fill import fill_quote_workbook
from agent.jobs.recognition import process_recognition_stub
from agent.jobs.recognition import process_recognition
from agent.jobs.wake_consumer import claim_and_apply_wake, wake_stream
from agent.llm import LlmGateway, LlmMode
from agent.redis_coord.runtime import RedisRuntime
@@ -211,7 +211,10 @@ class WorkerRuntime:
return
entry_id, data = claimed[0]
try:
process_recognition_stub(data)
process_recognition(
data,
allow_network=bool(self.settings.llm_allow_network),
)
finally:
stream.ack(entry_id)
+100 -5
View File
@@ -1,7 +1,7 @@
"""
识别任务壳:入队 Redis Stream,Worker HTTP 槽认领后跑占位处理。
识别任务:入队 Redis Stream,Worker HTTP 槽认领后处理。
本文件职责:enqueue / process_stub;正式识别(附件/OCR/字段)后续按 payload.kind 分支。
本文件职责:enqueue;process_recognition 按 kind 分支(含报价模板 AI 映射)。
禁止:回调线程同步等识别完成;禁止本模块写主账六态。
"""
@@ -41,7 +41,7 @@ def enqueue_recognition(
"""
识别任务入队(快回)。
kind 例:attachment_text / vision / sheet_map。
kind 例:attachment_text / vision / quote_template_parse。
"""
rt = redis or get_redis_runtime()
if idempotency_key:
@@ -66,14 +66,109 @@ def process_recognition_stub(data: dict[str, Any]) -> dict[str, Any]:
"""
识别占位:回显 kind,不读附件、不调模型。
Worker 槽调用;正式实现替换为本函数旁的真实处理器。
兼容旧调用名;正式路径请用 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 处理识别/映射任务。
quote_template_parse:调千问 sheet_map,再 PUT 主账 parseResult。
"""
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 not isinstance(payload, dict):
payload = {}
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)}