# 合同 S11:挂合同正文。仅 YYC 传合并 PDF;提交前无文件则用占位 PDF。 # # dry_run 且没有文件时跳过。 from __future__ import annotations import time from pathlib import Path from accept import evidence_shot from errors import NodeError from contract_common import ( BrowserAdapter, ContractFlowContext, _ACCEPT, _close_blocking_dialogs, ) # 决定要挂的合同正文路径。提交且无文件时用占位 PDF。 def _resolve_contract_body_path(ctx: ContractFlowContext) -> str: path = (ctx.contract_document_path or "").strip() if not path and ctx.source_type == "YYC": path = (ctx.merged_pdf_path or "").strip() if path: return path if ctx.submit_mode == "dry_run": return "" from pdf_utils import ensure_placeholder_contract_pdf return ensure_placeholder_contract_pdf() # 挂合同正文(S11)。dry_run 且没有文件则跳过。 def upload_contract_document_if_needed(browser: BrowserAdapter, ctx: ContractFlowContext) -> None: path = _resolve_contract_body_path(ctx) if not path: evidence_shot(browser, "contract-pdf-upload-skipped") _ACCEPT.note("S11", "上传正文", {"skipped": "dry_run_no_file"}, ok=True) return try: _upload_contract_body(browser, path) except Exception as exc: _ACCEPT.note("S11", "上传正文", {"attached": "no", "error": exc}, ok=False, raise_error=False) raise evidence_shot(browser, "contract-body-uploaded") attached = _contract_body_attached(browser, path) _ACCEPT.check( "S11", "上传正文", [("attached", "yes" if attached else "no", "yes"), ("file", Path(path).name, Path(path).name)], browser=browser, screenshot_on_fail="contract-body-uploaded", ) # 打开合同正文区,点添加合同,把本地文件塞进 file input。 def _upload_contract_body(browser: BrowserAdapter, file_path: str) -> None: from pathlib import Path if not Path(file_path).is_file(): raise NodeError(f"合同正文文件不存在:{file_path}") _close_blocking_dialogs(browser) _reveal_contract_body_section(browser) evidence_shot(browser, "contract-body-tab") if _contract_body_attached(browser, file_path): evidence_shot(browser, "contract-body-uploaded") return _click_add_contract_body(browser) time.sleep(0.8) try: browser.click_native(["text=打开本地初稿", "text=本地初稿", "text=上传本地文件"]) time.sleep(0.8) except Exception: pass browser.upload_file_first( [ 'input[name="_apply_filemainOnline"]', 'input[name="__landray_filemainOnline"]', 'input[name*="filemainOnline"]', 'input[name*="fdMainOnline"]', '.lui_dialog input[type="file"]', 'input[type="file"]', ], file_path, ) deadline = time.time() + 12 while time.time() < deadline: if _contract_body_attached(browser, file_path) or _on_body_editor_tab(browser): evidence_shot(browser, "contract-body-uploaded") return time.sleep(0.6) evidence_shot(browser, "contract-body-uploaded", failed=True) raise NodeError("合同正文未挂上:页面仍是「+添加合同」,提交会被系统拒绝") # 滚到或点开「合同正文」区域,露出「+添加合同」。 def _reveal_contract_body_section(browser: BrowserAdapter) -> None: browser.execute_script( """ const labelOf = (el) => (el.textContent || '').replace(/\\s+/g, '').trim(); const addBtn = Array.from(document.querySelectorAll('a, span, div, td')).find((el) => { const text = labelOf(el); return text === '+添加合同' || text === '添加合同'; }); if (addBtn) { addBtn.scrollIntoView({block: 'center'}); return {ok: true, via: 'add'}; } const hits = Array.from(document.querySelectorAll('div, li, span, a')).filter((el) => labelOf(el) === '合同正文'); for (const el of hits) { const group = (el.parentElement && el.parentElement.parentElement) || el; if (labelOf(group).indexOf('签约方信息') >= 0) { (el.closest('li') || el).click(); return {ok: true, via: 'tabbar'}; } } return {ok: false}; """ ) time.sleep(0.6) # 点「+添加合同」打开本地初稿入口。 def _click_add_contract_body(browser: BrowserAdapter) -> None: clicked = browser.execute_script( """ const labelOf = (el) => (el.textContent || '').replace(/\\s+/g, '').trim(); const hit = Array.from(document.querySelectorAll('a, span, div, td, button')).find((el) => { const text = labelOf(el); return (text === '+添加合同' || text === '添加合同') && el.children.length <= 1; }); if (!hit) return {ok: false}; hit.scrollIntoView({block: 'center'}); hit.click(); return {ok: true}; """ ) if isinstance(clicked, dict) and clicked.get("ok"): return browser.click_native(["text=+添加合同", "text=添加合同"]) # 看合同正文是否已经挂上:文件名、office iframe 或 file input 有文件。 def _contract_body_attached(browser: BrowserAdapter, file_path: str) -> bool: from pathlib import Path stem = Path(file_path).name result = browser.execute_script( """ const wanted = String(arguments[0] || ''); const wantedStem = wanted.replace(/\\.[^.]+$/, ''); const fileInputs = Array.from(document.querySelectorAll('input[type="file"]')); if (fileInputs.some((el) => el.files && el.files.length > 0)) { return {ok: true, via: 'input'}; } const frames = Array.from(document.querySelectorAll('iframe, object, embed')); const office = frames.find((el) => { const hint = ((el.src || '') + ' ' + (el.id || '') + ' ' + (el.className || '')).toLowerCase(); const box = el.getBoundingClientRect(); return box.width > 80 && box.height > 80 && /office|ntko|wps|kingsoft|weboffice|filemain|online|pdf/.test(hint); }); if (office) return {ok: true, via: 'iframe'}; const rows = Array.from(document.querySelectorAll('tr')); for (const row of rows) { const cells = Array.from(row.querySelectorAll('td, th')); if (!cells.length) continue; const head = (cells[0].innerText || '').replace(/\\s+/g, '').trim(); if (head !== '合同正文') continue; const body = (row.innerText || '').replace(/\\s+/g, ' ').trim(); if (wanted && body.indexOf(wanted) >= 0) return {ok: true, via: 'name'}; if (wantedStem && body.indexOf(wantedStem) >= 0) return {ok: true, via: 'stem'}; if (/\\.(pdf|doc|docx|wps)/i.test(body)) return {ok: true, via: 'ext'}; if (body.indexOf('添加合同') >= 0 && body.indexOf('打开本地初稿') < 0) return {ok: false, via: 'empty'}; } const page = (document.body.innerText || ''); if (wanted && page.indexOf(wanted) >= 0) return {ok: true, via: 'page'}; return {ok: false, via: 'missing'}; """, stem, ) return isinstance(result, dict) and bool(result.get("ok")) # 底部「合同正文」在线编辑页:文件进编辑器后不一定回显文件名。 def _on_body_editor_tab(browser: BrowserAdapter) -> bool: result = browser.execute_script( """ const page = (document.body.innerText || '').replace(/\\s+/g, ''); return page.indexOf('打开本地初稿') >= 0 && page.indexOf('添加合同') < 0; """ ) return result is True