# 合同 S12:暂存后提交。成功后不要等详情页。 # # 测试环境直接提交会落到「该文档不存在」,所以先点暂存再点提交。 # dry_run 不点提交,返回 False。 from __future__ import annotations import time from accept import evidence_shot from contract_common import ( EDIT_PAGE_URL, BrowserAdapter, ContractFlowContext, _ACCEPT, _click_toolbar_button, _close_blocking_dialogs, _current_url, _lui_toolbar_button_exists, _read_document_id, _wait_lui_toolbar_button, ) _ERROR_MARKS = ( "不能为空", "请填写", "校验失败", "提交失败", "必填项未填", "该文档不存在", "操作失败", ) _SUCCESS_MARKS = ("您的操作已成功", "操作已成功", "提交成功", "流程已提交", "成功提交") _CONFIRM_MARKS = ("确定提交", "是否提交", "确认提交", "是否确定", "确定要提交") # 暂存后提交(S12)。dry_run 不点提交。成功后不要等详情页。 def submit_contract_for_approval( browser: BrowserAdapter, ctx: ContractFlowContext, *, fd_id: str = "" ) -> bool: if ctx.submit_mode == "dry_run": evidence_shot(browser, "contract-dry-run-ready") _ACCEPT.note("S12", "提交", {"mode": "dry_run", "submitted": "no"}, ok=True) return False _close_blocking_dialogs(browser) fd_id = (fd_id or "").strip() or _read_document_id(browser) print(f"S12 fdId={fd_id or '-'}", flush=True) # 测试环境直接提交会落到「该文档不存在」:先暂存落库,再提交。合同号从详情 #_xform_docNumber 读。 try: saved_click = _click_toolbar_button(browser, "暂存") except Exception as exc: print(f"暂存未点到:{exc}", flush=True) saved_click = False save_kind, save_text = _wait_submit_outcome(browser, timeout_seconds=15) if save_kind == "error": evidence_shot(browser, "contract-submitted", failed=True) _ACCEPT.note( "S12", "提交", {"submitted": "no", "error": save_text, "at": "暂存", "clickedSave": "yes" if saved_click else "no"}, ok=False, raise_error=False, ) return False if save_kind != "success": print("暂存后仍在表单页,继续点提交。", flush=True) fd_id = fd_id or _read_document_id(browser) if not _lui_toolbar_button_exists(browser, "提交"): if not fd_id: evidence_shot(browser, "contract-submitted", failed=True) _ACCEPT.note( "S12", "提交", {"submitted": "no", "error": "暂存后离开表单且无 fdId,无法再点提交", "clicked": "no"}, ok=False, raise_error=False, ) return False print(f"暂存后打开编辑页 fdId={fd_id}", flush=True) browser.open_url(EDIT_PAGE_URL.format(fd_id=fd_id)) if not _wait_lui_toolbar_button(browser, "提交", timeout_seconds=12): evidence_shot(browser, "contract-submitted", failed=True) _ACCEPT.note( "S12", "提交", {"submitted": "no", "error": "编辑页未见提交按钮 lui_widget_btn_txt", "clicked": "no", "fdId": fd_id}, ok=False, raise_error=False, ) return False try: clicked = _click_toolbar_button(browser, "提交") except Exception as exc: evidence_shot(browser, "contract-submitted", failed=True) _ACCEPT.note("S12", "提交", {"submitted": "no", "error": str(exc)[:120], "clicked": "no"}, ok=False, raise_error=False) return False if not clicked: evidence_shot(browser, "contract-submitted", failed=True) _ACCEPT.note("S12", "提交", {"submitted": "no", "error": "未点到提交按钮 lui_widget_btn_txt", "clicked": "no"}, ok=False, raise_error=False) return False evidence_shot(browser, "contract-after-submit-click") kind, dialog = _wait_submit_outcome(browser, timeout_seconds=20, trust_body_success=False) if kind == "error": evidence_shot(browser, "contract-submitted", failed=True) _ACCEPT.note( "S12", "提交", {"submitted": "no", "clicked": "yes", "error": dialog}, ok=False, raise_error=False, ) return False print(f"提交已点 kind={kind} dialog={dialog[:80]!r},S13 从详情读合同号。", flush=True) _ACCEPT.note( "S12", "提交", { "submitted": "yes", "clicked": "yes", "via": "暂存后提交", "signal": kind, "dialog": dialog[:80] or "(none)", }, ok=True, ) return True # 从页面正文抽出提交失败原因。没有则返回空字符串。 def _submit_error_from_body(body: str) -> str: text = body or "" if "该文档不存在" in text: return "该文档不存在" if "操作失败" in text and "您的请求已提交" in text: return "操作失败" return "" # 把弹窗文案收成 error / success / confirm / none。 def _classify_dialog_text(text: str) -> str: blob = text or "" for mark in _ERROR_MARKS: if mark in blob: return "error" for mark in _SUCCESS_MARKS: if mark in blob: return "success" for mark in _CONFIRM_MARKS: if mark in blob: return "confirm" return "none" # 等到提交弹窗给出成功或失败信号。超时返回 none。 def _wait_submit_outcome( browser: BrowserAdapter, timeout_seconds: float, *, trust_body_success: bool = True, ) -> tuple[str, str]: deadline = time.time() + timeout_seconds last_dialog = "" saw_confirm = False while time.time() < deadline: kind, text, confirm = _read_submit_signal( browser, trust_body_success=trust_body_success or saw_confirm, ) if confirm: saw_confirm = True if text: last_dialog = text if kind == "error": return "error", text if kind == "success": return "success", text time.sleep(0.4) kind, text, _confirm = _read_submit_signal( browser, trust_body_success=trust_body_success or saw_confirm, ) if kind in ("error", "success"): return kind, text return "none", text or last_dialog # 读一次原生 alert 和 LUI 弹窗,返回 kind、文案、是否点过确认。 def _read_submit_signal( browser: BrowserAdapter, *, trust_body_success: bool = True, ) -> tuple[str, str, bool]: alert_text = _accept_native_alert(browser) if alert_text: kind = _classify_dialog_text(alert_text) if kind == "none": kind = "confirm" print(f"S12 native alert kind={kind} text={alert_text[:80]!r}", flush=True) if kind == "error": return "error", alert_text, False if kind == "success": return "success", alert_text, False if kind == "confirm": return _finish_submit_signal(browser, alert_text, True, trust_body_success) snapshot = _handle_submit_dialogs(browser) dialog_text = str(snapshot.get("text") or "").strip() kind = _classify_dialog_text(dialog_text) if dialog_text else "none" if kind == "none": kind = str(snapshot.get("kind") or "none") if kind == "error": return "error", dialog_text or "弹窗报错", False if kind == "success": return "success", dialog_text or "ok", False return _finish_submit_signal( browser, dialog_text, kind == "confirm" or bool(snapshot.get("clicked")), trust_body_success, ) # 弹窗之后再看正文和 URL,把提交结果收成 error / success / none。 def _finish_submit_signal( browser: BrowserAdapter, dialog_text: str, saw_confirm: bool, trust_body_success: bool, ) -> tuple[str, str, bool]: body_error = _submit_error_from_body(browser.read_body_text() or "") if body_error: return "error", body_error, saw_confirm if _submit_view_page(browser): return "success", dialog_text or "view", saw_confirm if (trust_body_success or saw_confirm) and _submit_success_text(browser): return "success", dialog_text or "ok", saw_confirm return "none", dialog_text, saw_confirm # 有原生 alert 就点确定并返回文案。没有则返回空字符串。 def _accept_native_alert(browser: BrowserAdapter) -> str: accept = getattr(browser, "accept_alert", None) if not callable(accept): return "" try: return str(accept() or "").strip() except Exception: return "" # 当前 URL 是否已经落到合同详情 view 页。 def _submit_view_page(browser: BrowserAdapter) -> bool: url = _current_url(browser) return "method=view" in url and "kmAgreementApply" in url # 页面正文是否出现提交成功标记,且没有失败文案。 def _submit_success_text(browser: BrowserAdapter) -> bool: body = browser.read_body_text() or "" if _submit_error_from_body(body): return False return any(mark in body for mark in _SUCCESS_MARKS) # 提交是否已经成功:成功文案或已到详情页,且正文没有失败。 def _submit_succeeded(browser: BrowserAdapter) -> bool: if _submit_error_from_body(browser.read_body_text() or ""): return False return _submit_success_text(browser) or _submit_view_page(browser) # 读当前弹窗/正文里的校验失败文案。没有则返回空字符串。 def _submit_validation_error(browser: BrowserAdapter) -> str: kind, text, _confirm = _read_submit_signal(browser, trust_body_success=True) if kind == "error": return text return "" # 扫描 LUI 弹窗:报错原样返回,确认/成功则点确定。 def _handle_submit_dialogs(browser: BrowserAdapter) -> dict: raw = browser.execute_script( """ const errorMarks = ['不能为空','请填写','校验失败','提交失败','必填项未填','该文档不存在','操作失败']; const successMarks = ['您的操作已成功','操作已成功','提交成功','流程已提交','成功提交']; const confirmMarks = ['确定提交','是否提交','确认提交','是否确定','确定要提交']; function classify(text) { if (errorMarks.some((m) => text.indexOf(m) >= 0)) return 'error'; if (successMarks.some((m) => text.indexOf(m) >= 0)) return 'success'; if (confirmMarks.some((m) => text.indexOf(m) >= 0)) return 'confirm'; return 'none'; } function dialogText(doc) { const roots = Array.from(doc.querySelectorAll( '.lui_dialog, .lui_message, .lui_alert, .lui_dialog_content, .prompt_frame, .lui_dialog_common' )); return roots.map((el) => (el.innerText || '').trim()).filter(Boolean).join('\\n'); } function clickLabel(doc, labels) { const nodes = Array.from(doc.querySelectorAll('div, a, button, span, input')); for (const label of labels) { const hit = nodes.find((el) => { const t = (el.textContent || el.value || '').trim(); return t === label && (!el.children || el.children.length === 0); }); if (hit) { hit.click(); return label; } } return ''; } function walk(win) { let found = {kind: 'none', text: '', clicked: ''}; try { const text = dialogText(win.document); const kind = classify(text); if (kind === 'error') return {kind: kind, text: text.slice(0, 200), clicked: ''}; if (kind === 'confirm') { const clicked = clickLabel(win.document, ['确定', '确认', '是', 'OK']); return {kind: kind, text: text.slice(0, 200), clicked: clicked}; } if (kind === 'success') { const clicked = clickLabel(win.document, ['确定', '关闭', '确认']); return {kind: kind, text: text.slice(0, 200), clicked: clicked}; } if (text) { const clicked = clickLabel(win.document, ['确定', '确认']); if (clicked) return {kind: 'confirm', text: text.slice(0, 200), clicked: clicked}; } } catch (e) {} for (let i = 0; i < win.frames.length; i++) { try { const inner = walk(win.frames[i]); if (inner.kind !== 'none' || inner.clicked) return inner; } catch (e) {} } return found; } return walk(window); """ ) if not isinstance(raw, dict): return {"kind": "none", "text": "", "clicked": ""} return { "kind": str(raw.get("kind") or "none"), "text": str(raw.get("text") or "").strip(), "clicked": str(raw.get("clicked") or ""), } # 等到提交确认弹窗消失或被点掉。 def _wait_lui_dialog_gone(browser: BrowserAdapter, timeout_seconds: float = 8) -> None: deadline = time.time() + timeout_seconds while time.time() < deadline: snapshot = _handle_submit_dialogs(browser) kind = str(snapshot.get("kind") or "none") text = str(snapshot.get("text") or "").strip() if kind == "confirm": return if kind in ("none", "") and not text: return _close_blocking_dialogs(browser) time.sleep(0.4) _close_blocking_dialogs(browser)