修复附件询价:公网打开上传页不再出骨架 JSON,未上传时打字能正常回话。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
H5 补问 / 附件上传 / 复制工单号通道。
|
||||
|
||||
本文件职责:挂载 /inquiry-form;用 token 绑定身份或工单号(URL 禁止 userid)。
|
||||
公网 Nginx 会把 /inquiry-form/upload/{token} 改写成 /inquiry-form/{token},
|
||||
附件票必须在这两条路径上出同一张上传页、收同一套文件。
|
||||
禁止:从 URL 读 userid;禁止在本包改六态或直连 TMS;禁止同步 invoke Graph。
|
||||
"""
|
||||
|
||||
@@ -73,11 +75,19 @@ def h5_jsapi_signature(
|
||||
raise HTTPException(status_code=503, detail="jsapi_unavailable") from exc
|
||||
|
||||
|
||||
@router.get("/upload/{token}", response_class=HTMLResponse)
|
||||
def h5_upload_open(token: str) -> HTMLResponse:
|
||||
def _is_upload_token(token: str) -> bool:
|
||||
"""
|
||||
打开附件上传页。身份只认 token;过期给 410,不吐内部栈。
|
||||
附件上传票:allowed_actions 含 upload_attachment。
|
||||
|
||||
公网 Nginx 会把 /inquiry-form/upload/{token} 改写成 /inquiry-form/{token},
|
||||
不能只认 /upload/ 这一条路径。
|
||||
"""
|
||||
rec = validate_token(token)
|
||||
return rec is not None and "upload_attachment" in (rec.allowed_actions or [])
|
||||
|
||||
|
||||
def _upload_page(token: str) -> HTMLResponse:
|
||||
"""打开附件上传页。身份只认 token;过期给 410,不吐内部栈。"""
|
||||
from agent.handlers.attachment_inquiry import lookup_upload_sender
|
||||
|
||||
if lookup_upload_sender(token) is None:
|
||||
@@ -85,10 +95,9 @@ def h5_upload_open(token: str) -> HTMLResponse:
|
||||
return HTMLResponse(render_upload_page())
|
||||
|
||||
|
||||
@router.post("/upload/{token}", response_class=HTMLResponse)
|
||||
async def h5_upload_submit(
|
||||
async def _accept_upload_files(
|
||||
token: str,
|
||||
files: Optional[list[UploadFile]] = File(default=None),
|
||||
files: Optional[list[UploadFile]],
|
||||
) -> HTMLResponse:
|
||||
"""
|
||||
接收多个 Excel/PDF。请求线程只验 token、读字节、入队,不等千问。
|
||||
@@ -112,6 +121,21 @@ async def h5_upload_submit(
|
||||
return HTMLResponse(render_upload_message("上传成功", copy.ATTACH_H5_OK))
|
||||
|
||||
|
||||
@router.get("/upload/{token}", response_class=HTMLResponse)
|
||||
def h5_upload_open(token: str) -> HTMLResponse:
|
||||
"""显式上传路径。公网可能被 Nginx 改写到 /{token},两路必须同一页。"""
|
||||
return _upload_page(token)
|
||||
|
||||
|
||||
@router.post("/upload/{token}", response_class=HTMLResponse)
|
||||
async def h5_upload_submit(
|
||||
token: str,
|
||||
files: Optional[list[UploadFile]] = File(default=None),
|
||||
) -> HTMLResponse:
|
||||
"""显式上传提交。与 POST /{token} 同一套入队逻辑。"""
|
||||
return await _accept_upload_files(token, files)
|
||||
|
||||
|
||||
@router.get("/air-routes/{token}", response_class=HTMLResponse)
|
||||
def h5_air_route_open(token: str) -> HTMLResponse:
|
||||
"""打开空运选线路页。身份只认 token。"""
|
||||
@@ -205,15 +229,19 @@ def h5_land_route_pick(token: str, index: int = Form(default=-1)) -> HTMLRespons
|
||||
|
||||
|
||||
@router.get("/{token}")
|
||||
def h5_open(token: str) -> dict[str, Any]:
|
||||
def h5_open(token: str):
|
||||
"""
|
||||
打开补问页:校验 token 未过期。
|
||||
合同入口:/inquiry-form/{token}。
|
||||
|
||||
返回允许动作与 wait_version;不返回内部异常栈。
|
||||
附件票出上传页(公网 Nginx 也会把 /upload/{token} 改写到这里)。
|
||||
其它票仍回补问骨架,不吐内部栈。
|
||||
"""
|
||||
rec = validate_token(token)
|
||||
if rec is None:
|
||||
raise HTTPException(status_code=410, detail="表单已过期或无效,请回企微重新打开")
|
||||
if "upload_attachment" in (rec.allowed_actions or []):
|
||||
logger.info("h5.open upload thread=%s", rec.thread_id)
|
||||
return _upload_page(token)
|
||||
return {
|
||||
"status": "ok",
|
||||
"thread_id": rec.thread_id,
|
||||
@@ -223,6 +251,22 @@ def h5_open(token: str) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
@router.post("/{token}", response_class=HTMLResponse)
|
||||
async def h5_post_by_token(
|
||||
token: str,
|
||||
files: Optional[list[UploadFile]] = File(default=None),
|
||||
) -> HTMLResponse:
|
||||
"""
|
||||
附件票在 /{token} 上收文件。
|
||||
|
||||
浏览器地址仍是 /upload/{token},表单相对提交后被 Nginx 改写到这里。
|
||||
非附件票不收文件,避免冒充补问提交。
|
||||
"""
|
||||
if not _is_upload_token(token):
|
||||
raise HTTPException(status_code=405, detail="该页不接收文件上传")
|
||||
return await _accept_upload_files(token, files)
|
||||
|
||||
|
||||
@router.post("/{token}/submit")
|
||||
def h5_submit(token: str, body: H5SubmitBody) -> dict[str, Any]:
|
||||
"""
|
||||
|
||||
@@ -79,6 +79,21 @@ def is_active_attachment_wait(sender_id: str, *, now: Optional[float] = None) ->
|
||||
return get_attach_wave_book().active(sender_id, now=now) is not None
|
||||
|
||||
|
||||
def is_idle_attachment_wait(sender_id: str, *, now: Optional[float] = None) -> bool:
|
||||
"""已发卡但还没传 Excel/PDF/图。此时销售打字应走文字询价。"""
|
||||
wave = get_attach_wave_book().active(sender_id, now=now)
|
||||
return wave is not None and not wave.started
|
||||
|
||||
|
||||
def abandon_idle_attach_wait(sender_id: str) -> bool:
|
||||
"""
|
||||
作废未开始认的等待票。已开始认返回 False。
|
||||
|
||||
副作用:旧上传卡随后按过期拒绝。
|
||||
"""
|
||||
return get_attach_wave_book().abandon_if_idle(sender_id)
|
||||
|
||||
|
||||
def append_attach_text(sender_id: str, text: str) -> bool:
|
||||
"""认完之前的字并进本票,不走文字询价。"""
|
||||
return get_attach_wave_book().append_follow_text(sender_id, text)
|
||||
|
||||
@@ -287,6 +287,24 @@ class AttachWaveBook:
|
||||
parts.append(wave.follow_text.strip())
|
||||
return "\n".join(parts)
|
||||
|
||||
def abandon_if_idle(self, sender_id: str) -> bool:
|
||||
"""
|
||||
还没开始认材料:作废等待票。已开始认或无票返回 False。
|
||||
|
||||
销售发卡后改口打字询价时用。旧 H5 随后按过期拒绝。
|
||||
"""
|
||||
sid = (sender_id or "").strip()
|
||||
if not sid:
|
||||
return False
|
||||
with self._lock:
|
||||
self._hydrate(sid)
|
||||
wave = self._waves.get(sid)
|
||||
if wave is None or wave.business_done or wave.started:
|
||||
return False
|
||||
wave.business_done = True
|
||||
self._persist(sid)
|
||||
return True
|
||||
|
||||
def mark_business_done(self, sender_id: str) -> None:
|
||||
"""业务补问/卡片已发出。之后旧 H5 与并入图都必须拒绝。"""
|
||||
sid = (sender_id or "").strip()
|
||||
|
||||
@@ -262,16 +262,29 @@ def dispatch_inbound(message: InboundMessage, decision: RouteDecision) -> str:
|
||||
handle_image_inquiry(message)
|
||||
return "image_inquiry"
|
||||
if kind in {"text", ""}:
|
||||
from agent.handlers.attachment_inquiry import append_attach_text, is_active_attachment_wait
|
||||
from agent.handlers.attachment_inquiry import (
|
||||
abandon_idle_attach_wait,
|
||||
append_attach_text,
|
||||
is_active_attachment_wait,
|
||||
is_idle_attachment_wait,
|
||||
)
|
||||
from agent.handlers.image_inquiry import append_wave_text, is_active_image_wave
|
||||
|
||||
if is_active_attachment_wait(message.sender_id) and (message.content or "").strip():
|
||||
if decision.intent == "attachment_inquiry":
|
||||
handle_attachment_inquiry(message)
|
||||
return "attachment_inquiry"
|
||||
append_attach_text(message.sender_id, message.content or "")
|
||||
logger.info("dispatch 附件波次收字 sender=%s", message.sender_id)
|
||||
return "attach_wave_follow_text"
|
||||
if is_idle_attachment_wait(message.sender_id):
|
||||
abandon_idle_attach_wait(message.sender_id)
|
||||
logger.info(
|
||||
"dispatch 附件未传材料,改走文字 sender=%s intent=%s",
|
||||
message.sender_id,
|
||||
decision.intent,
|
||||
)
|
||||
else:
|
||||
append_attach_text(message.sender_id, message.content or "")
|
||||
logger.info("dispatch 附件波次收字 sender=%s", message.sender_id)
|
||||
return "attach_wave_follow_text"
|
||||
if is_active_image_wave(message.sender_id) and (message.content or "").strip():
|
||||
append_wave_text(message.sender_id, message.content or "")
|
||||
logger.info("dispatch 图片波次收字 sender=%s", message.sender_id)
|
||||
|
||||
@@ -239,13 +239,34 @@ class AttachmentInquiryTests(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(name, "attachment_inquiry")
|
||||
|
||||
def test_follow_text_not_create_ticket_before_upload(self) -> None:
|
||||
def test_text_inquiry_before_upload_gets_reply(self) -> None:
|
||||
"""发卡后还没传材料:销售改口打字询价必须有回话,不能默收。"""
|
||||
handle_attachment_inquiry(
|
||||
InboundMessage(sender_id="s1", message_id="m1", content="附件询价"),
|
||||
store=self.store,
|
||||
)
|
||||
name = dispatch_inbound(
|
||||
InboundMessage(sender_id="s1", message_id="m2", content="海运上海到纽约 2个40尺"),
|
||||
InboundMessage(sender_id="s1", message_id="m2", content=SEA_ORAL),
|
||||
RouteDecision(intent="ordinary_text_inquiry"),
|
||||
)
|
||||
self.assertEqual(name, "ordinary_text_inquiry")
|
||||
self.assertIsNone(get_attach_wave_book().active("s1"))
|
||||
self.assertIsNotNone(get_sea_text_flow().session_of("s1"))
|
||||
|
||||
def test_follow_text_while_recognizing_not_new_ticket(self) -> None:
|
||||
"""已经开始认附件:期间的字并进口播,不另开文字询价。"""
|
||||
handle_attachment_inquiry(
|
||||
InboundMessage(sender_id="s1", message_id="m1", content="附件询价"),
|
||||
store=self.store,
|
||||
)
|
||||
accept_h5_uploads(
|
||||
sender_id="s1",
|
||||
files=[("sea.xlsx", b"fake")],
|
||||
store=self.store,
|
||||
process_now=False,
|
||||
)
|
||||
name = dispatch_inbound(
|
||||
InboundMessage(sender_id="s1", message_id="m2", content="只要整柜"),
|
||||
RouteDecision(intent="ordinary_text_inquiry"),
|
||||
)
|
||||
self.assertEqual(name, "attach_wave_follow_text")
|
||||
|
||||
@@ -66,6 +66,30 @@ class AttachmentUploadH5Tests(unittest.TestCase):
|
||||
self.assertEqual(miss.status_code, 410)
|
||||
self.assertIn("过期", miss.text)
|
||||
|
||||
def test_bare_token_opens_upload_page_not_json(self) -> None:
|
||||
"""
|
||||
公网 Nginx 会把 /inquiry-form/upload/{token} 改写成 /inquiry-form/{token}。
|
||||
附件票必须出上传页,不能把补问骨架 JSON 直接给销售。
|
||||
"""
|
||||
token = self._open_card()
|
||||
hit = self.client.get(f"/inquiry-form/{token}")
|
||||
self.assertEqual(hit.status_code, 200)
|
||||
self.assertIn("text/html", hit.headers.get("content-type", ""))
|
||||
self.assertIn("Excel", hit.text)
|
||||
self.assertNotIn("骨架", hit.text)
|
||||
self.assertNotIn("s1", hit.text)
|
||||
miss = self.client.get("/inquiry-form/not-a-real-token")
|
||||
self.assertIn(miss.status_code, (410, 422))
|
||||
|
||||
def test_bare_token_post_png_rejected(self) -> None:
|
||||
token = self._open_card()
|
||||
hit = self.client.post(
|
||||
f"/inquiry-form/{token}",
|
||||
files=[("files", ("shot.png", b"\x89PNG", "image/png"))],
|
||||
)
|
||||
self.assertEqual(hit.status_code, 400)
|
||||
self.assertIn("Excel", hit.text)
|
||||
|
||||
def test_upload_page_uses_card_chrome(self) -> None:
|
||||
from agent.channel.h5.upload_page import expired_upload_page, render_upload_message, render_upload_page
|
||||
|
||||
|
||||
Reference in New Issue
Block a user