feat: add Windows Selenium runner for CONTRACT_CREATE
This commit is contained in:
parent
4060d7b45d
commit
ca79659e3e
|
|
@ -0,0 +1,343 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
APA (艺赛旗) Selenium Chrome BrowserAdapter 实现
|
||||
通过 Selenium WebDriver 直接操控 Chrome,不依赖 APA 浏览器模块
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
|
||||
class ApaBrowserAdapter:
|
||||
"""Selenium Chrome 浏览器适配器,实现 BrowserAdapter 协议"""
|
||||
|
||||
def __init__(self, url: str = ""):
|
||||
self._url = url
|
||||
self._driver = None
|
||||
self._is_open = False
|
||||
|
||||
def _get_driver(self):
|
||||
"""创建或获取 Chrome WebDriver"""
|
||||
if self._driver is not None:
|
||||
return self._driver
|
||||
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.service import Service
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
from webdriver_manager.chrome import ChromeDriverManager
|
||||
|
||||
options = Options()
|
||||
# 不自动关闭浏览器,方便调试
|
||||
options.add_experimental_option("detach", True)
|
||||
# 避免被检测为自动化
|
||||
options.add_experimental_option("excludeSwitches", ["enable-automation"])
|
||||
options.add_experimental_option("useAutomationExtension", False)
|
||||
# 基本参数
|
||||
options.add_argument("--disable-blink-features=AutomationControlled")
|
||||
options.add_argument("--no-first-run")
|
||||
options.add_argument("--no-default-browser-check")
|
||||
|
||||
service = Service(ChromeDriverManager().install())
|
||||
self._driver = webdriver.Chrome(service=service, options=options)
|
||||
return self._driver
|
||||
|
||||
def _ensure_open(self):
|
||||
if self._driver is None:
|
||||
driver = self._get_driver()
|
||||
if self._url:
|
||||
driver.get(self._url)
|
||||
self._is_open = True
|
||||
|
||||
# ---- 内部查找方法 ----
|
||||
|
||||
def _find(self, selector: str, timeout: int = 5):
|
||||
"""
|
||||
智能查找元素:支持 CSS、XPath、text=、label=、aria=、title=、:contains()
|
||||
"""
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.common.exceptions import NoSuchElementException
|
||||
import re
|
||||
|
||||
# 1. text=xxx → 查找包含对应文本的元素
|
||||
if selector.startswith("text="):
|
||||
text_val = selector[5:].strip('"').strip("'")
|
||||
xpath = f'//*[contains(text(),"{text_val}")]'
|
||||
return self._driver.find_element(By.XPATH, xpath)
|
||||
|
||||
# 2. label=xxx → 找到对应标签的输入框
|
||||
if selector.startswith("label="):
|
||||
text_val = selector[6:].strip('"').strip("'")
|
||||
xpath = f'//label[contains(text(),"{text_val}")]/following-sibling::*[1][self::input or self::select or self::textarea]'
|
||||
try:
|
||||
return self._driver.find_element(By.XPATH, xpath)
|
||||
except NoSuchElementException:
|
||||
# Fallback: find the label's "for" attribute
|
||||
try:
|
||||
for_id = self._driver.find_element(
|
||||
By.XPATH, f'//label[contains(text(),"{text_val}")]'
|
||||
).get_attribute("for")
|
||||
if for_id:
|
||||
return self._driver.find_element(By.ID, for_id)
|
||||
except NoSuchElementException:
|
||||
pass
|
||||
raise
|
||||
|
||||
# 3. aria=xxx → aria-label 属性
|
||||
if selector.startswith("aria="):
|
||||
text_val = selector[5:].strip('"').strip("'")
|
||||
return self._driver.find_element(By.CSS_SELECTOR, f'[aria-label="{text_val}"]')
|
||||
|
||||
# 4. title=xxx → title 属性
|
||||
if selector.startswith("title="):
|
||||
text_val = selector[6:].strip('"').strip("'")
|
||||
return self._driver.find_element(By.CSS_SELECTOR, f'[title="{text_val}"]')
|
||||
|
||||
# 5. :contains() → 转 XPath(支持 button:contains(文本) / a:contains("文本"))
|
||||
if ":contains(" in selector:
|
||||
m = re.match(r'(.+?):contains\(["\']?(.+?)["\']?\)', selector)
|
||||
if m:
|
||||
tag_part, text_val = m.groups()
|
||||
xpath = f'//{tag_part}[contains(.,"{text_val}")]'
|
||||
return self._driver.find_element(By.XPATH, xpath)
|
||||
|
||||
# 6. 显式 XPath(以 / 或 // 开头)优先按 XPath 查找
|
||||
if selector.startswith("//") or selector.startswith("/"):
|
||||
return self._driver.find_element(By.XPATH, selector)
|
||||
|
||||
# 7. 标准 CSS selector
|
||||
return self._driver.find_element(By.CSS_SELECTOR, selector)
|
||||
|
||||
# ---- BrowserAdapter 协议实现 ----
|
||||
|
||||
def open_url(self, url: str) -> None:
|
||||
self._url = url
|
||||
self._get_driver().get(url)
|
||||
self._is_open = True
|
||||
|
||||
def _set_value_via_js(self, el, value: str) -> None:
|
||||
"""通过原生 value setter + 事件注入设置值,兼容 LUI/React 受控组件"""
|
||||
self._driver.execute_script(
|
||||
"""
|
||||
var el = arguments[0];
|
||||
var value = arguments[1];
|
||||
var proto = el.tagName === 'TEXTAREA'
|
||||
? window.HTMLTextAreaElement.prototype
|
||||
: window.HTMLInputElement.prototype;
|
||||
var setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
|
||||
setter.call(el, value);
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
el.dispatchEvent(new Event('blur', { bubbles: true }));
|
||||
""",
|
||||
el,
|
||||
str(value),
|
||||
)
|
||||
|
||||
def fill_first(self, selectors: list, value: str) -> None:
|
||||
self._ensure_open()
|
||||
last_error = None
|
||||
for sel in selectors:
|
||||
try:
|
||||
el = self._find(sel)
|
||||
# 先尝试原生输入;LUI 受控组件用 JS 注入兜底
|
||||
try:
|
||||
el.clear()
|
||||
el.send_keys(str(value))
|
||||
except Exception:
|
||||
pass
|
||||
if el.get_attribute("value") != str(value):
|
||||
self._set_value_via_js(el, str(value))
|
||||
time.sleep(0.3)
|
||||
return
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
continue
|
||||
raise RuntimeError(f"fill_first failed: {selectors}, last: {last_error}")
|
||||
|
||||
def click_first(self, selectors: list) -> None:
|
||||
self._ensure_open()
|
||||
last_error = None
|
||||
for sel in selectors:
|
||||
try:
|
||||
el = self._find(sel)
|
||||
# LUI/委托事件用 JS 点击最可靠
|
||||
self._driver.execute_script("arguments[0].click()", el)
|
||||
time.sleep(0.5)
|
||||
return
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
continue
|
||||
raise RuntimeError(f"click_first failed: {selectors}, last: {last_error}")
|
||||
|
||||
def double_click_first(self, selectors: list) -> None:
|
||||
self._ensure_open()
|
||||
from selenium.webdriver.common.action_chains import ActionChains
|
||||
last_error = None
|
||||
for sel in selectors:
|
||||
try:
|
||||
el = self._find(sel)
|
||||
ActionChains(self._driver).double_click(el).perform()
|
||||
time.sleep(0.5)
|
||||
return
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
continue
|
||||
raise RuntimeError(f"double_click_first failed: {selectors}, last: {last_error}")
|
||||
|
||||
def wait_for_text(self, text: str, timeout_seconds: int = 15) -> None:
|
||||
self._ensure_open()
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.webdriver.common.by import By
|
||||
try:
|
||||
WebDriverWait(self._driver, timeout_seconds).until(
|
||||
EC.text_to_be_present_in_element((By.TAG_NAME, "body"), text)
|
||||
)
|
||||
except Exception as e:
|
||||
raise TimeoutError(f"wait_for_text '{text}' timed out: {e}")
|
||||
|
||||
def wait_for_element(self, selectors: list, timeout_seconds: int = 15) -> None:
|
||||
self._ensure_open()
|
||||
deadline = time.time() + timeout_seconds
|
||||
last_error = None
|
||||
while time.time() < deadline:
|
||||
for sel in selectors:
|
||||
try:
|
||||
self._find(sel, timeout=1)
|
||||
return
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
time.sleep(0.5)
|
||||
raise TimeoutError(f"wait_for_element timed out: {selectors}, last: {last_error}")
|
||||
|
||||
def element_exists(self, selectors: list) -> bool:
|
||||
self._ensure_open()
|
||||
for sel in selectors:
|
||||
try:
|
||||
self._find(sel, timeout=1)
|
||||
return True
|
||||
except Exception:
|
||||
continue
|
||||
return False
|
||||
|
||||
def switch_to_frame(self, selectors: list) -> None:
|
||||
"""切换到匹配到的 iframe(客商选择弹窗等)。"""
|
||||
self._ensure_open()
|
||||
last_error = None
|
||||
for sel in selectors:
|
||||
try:
|
||||
el = self._find(sel, timeout=1)
|
||||
self._driver.switch_to.frame(el)
|
||||
return
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
continue
|
||||
raise RuntimeError(f"switch_to_frame failed: {selectors}, last: {last_error}")
|
||||
|
||||
def switch_to_default_content(self) -> None:
|
||||
"""切回主文档(顶层 frame)。"""
|
||||
self._ensure_open()
|
||||
self._driver.switch_to.default_content()
|
||||
|
||||
def select_first(self, selectors: list, value: str) -> None:
|
||||
self._ensure_open()
|
||||
from selenium.webdriver.support.ui import Select
|
||||
last_error = None
|
||||
for sel in selectors:
|
||||
try:
|
||||
el = self._find(sel)
|
||||
# 原生 <select>
|
||||
try:
|
||||
Select(el).select_by_visible_text(value)
|
||||
time.sleep(0.3)
|
||||
if self._selected_text_matches(el, value):
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
# 兜底:JS 定位 option 并设置选中
|
||||
self._select_via_js(el, value)
|
||||
time.sleep(0.3)
|
||||
if self._selected_text_matches(el, value):
|
||||
return
|
||||
raise RuntimeError(f"select_first 未选中目标项: {value}")
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
continue
|
||||
raise RuntimeError(f"select_first failed: {selectors}, last: {last_error}")
|
||||
|
||||
def _selected_text_matches(self, el, value: str) -> bool:
|
||||
try:
|
||||
script = (
|
||||
"var el = arguments[0];"
|
||||
"if (el.selectedIndex != null) {"
|
||||
" var o = el.options[el.selectedIndex];"
|
||||
" return o ? (o.textContent || o.text || '').trim() : '';"
|
||||
"}"
|
||||
"return (el.value || '').trim();"
|
||||
)
|
||||
selected = self._driver.execute_script(script, el)
|
||||
return (selected or "").strip() == str(value).strip()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _select_via_js(self, el, value: str) -> None:
|
||||
self._driver.execute_script(
|
||||
"""
|
||||
var el = arguments[0];
|
||||
var value = arguments[1];
|
||||
var options = el.options || [];
|
||||
for (var i = 0; i < options.length; i++) {
|
||||
var t = (options[i].textContent || options[i].text || '').trim();
|
||||
if (t === value || options[i].value === value) {
|
||||
el.selectedIndex = i;
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
return;
|
||||
}
|
||||
}
|
||||
""",
|
||||
el,
|
||||
str(value),
|
||||
)
|
||||
|
||||
def upload_file_first(self, selectors: list, file_path: str) -> None:
|
||||
self._ensure_open()
|
||||
last_error = None
|
||||
for sel in selectors:
|
||||
try:
|
||||
el = self._find(sel)
|
||||
el.send_keys(file_path)
|
||||
time.sleep(1)
|
||||
return
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
continue
|
||||
raise RuntimeError(f"upload_file_first failed: {selectors}, last: {last_error}")
|
||||
|
||||
def read_first(self, selectors: list, value_name: str = "") -> str:
|
||||
self._ensure_open()
|
||||
last_error = None
|
||||
for sel in selectors:
|
||||
try:
|
||||
el = self._find(sel)
|
||||
result = el.text or el.get_attribute("value") or ""
|
||||
if result:
|
||||
return result
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
continue
|
||||
raise RuntimeError(f"read_first failed: {selectors}, last: {last_error}")
|
||||
|
||||
def read_body_text(self) -> str:
|
||||
self._ensure_open()
|
||||
try:
|
||||
return self._driver.find_element("tag name", "body").text
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def screenshot(self, name: str) -> str:
|
||||
import os
|
||||
save_dir = os.path.join(os.path.dirname(__file__), "screenshots")
|
||||
os.makedirs(save_dir, exist_ok=True)
|
||||
path = os.path.join(save_dir, f"{name}.png")
|
||||
if self._driver:
|
||||
self._driver.save_screenshot(path)
|
||||
return path
|
||||
|
|
@ -1,47 +1,27 @@
|
|||
"""CONTRACT_CREATE node skeleton for YiSaiQi Windows integration.
|
||||
|
||||
This first version proves the state-center callback path and leaves the
|
||||
contract-system page automation behind a single replaceable runner function.
|
||||
"""
|
||||
"""CONTRACT_CREATE node: input building + state-center callback (runtime.shell) + Selenium drafting."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import uuid
|
||||
import webbrowser
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, replace
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable
|
||||
|
||||
from .apa_browser_adapter import ApaBrowserAdapter
|
||||
from .callback_client import StateCenterClient
|
||||
from .contract_draft import ContractDraft, load_contract_draft
|
||||
from .contract_draft import ContractDraft, contract_draft_from_dict, load_contract_draft
|
||||
from .contract_flow_steps import ContractFlowContext, run_contract_create_flow
|
||||
from .pdf_utils import ensure_merged_pdf
|
||||
from .runtime.errors import LoginFailed as RuntimeLoginFailed
|
||||
from .runtime.errors import NodeError
|
||||
from .runtime.errors import PdfMergeUnsupported as RuntimePdfMergeUnsupported
|
||||
from .runtime.errors import SupplierContractDisabled as RuntimeSupplierContractDisabled
|
||||
from .runtime.errors import NodeError, PdfMergeUnsupported, UnknownRpaError
|
||||
from .runtime.shell import execute_node
|
||||
from .yisaiqi_adapter_template import YiSaiQiBrowserAdapter
|
||||
|
||||
NODE_CODE = "CONTRACT_CREATE"
|
||||
CONTRACT_URL = "http://contract-test.gzmpc.com:8080/"
|
||||
|
||||
|
||||
class ContractCreateError(NodeError):
|
||||
pass
|
||||
|
||||
|
||||
class LoginFailed(RuntimeLoginFailed, ContractCreateError):
|
||||
pass
|
||||
|
||||
|
||||
class SupplierContractDisabled(RuntimeSupplierContractDisabled, ContractCreateError):
|
||||
pass
|
||||
|
||||
|
||||
class PdfMergeUnsupported(RuntimePdfMergeUnsupported, ContractCreateError):
|
||||
pass
|
||||
"""入参缺失 / 页面元素找不到 / 页面改版,均映射 UI_ELEMENT_NOT_FOUND。"""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
@ -55,9 +35,12 @@ class ContractCreateInput:
|
|||
contract_url: str = CONTRACT_URL
|
||||
username: str = "test002"
|
||||
password: str = "1"
|
||||
open_browser: bool = False
|
||||
submit_mode: str = "submit"
|
||||
order_no_list: tuple[str, ...] = ()
|
||||
template_name: str = ""
|
||||
draft_path: str | None = None
|
||||
merged_pdf_output: str | None = None
|
||||
flow_context: ContractFlowContext | None = None
|
||||
|
||||
|
||||
ContractRunner = Callable[[ContractCreateInput], str]
|
||||
|
|
@ -71,13 +54,24 @@ def execute_contract_create(
|
|||
) -> dict[str, Any]:
|
||||
runner = contract_runner or create_contract_in_contract_system
|
||||
|
||||
# dry_run:只跑到提交前,不回调状态中心(不 start/complete/fail)
|
||||
if spec.submit_mode == "dry_run":
|
||||
runner(spec)
|
||||
return {"status": "dry_run", "submitted": False}
|
||||
|
||||
def _run() -> dict[str, Any]:
|
||||
try:
|
||||
contract_no = runner(spec)
|
||||
payload = build_complete_payload(spec, contract_no)
|
||||
except NodeError:
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise UnknownRpaError(str(exc)) from exc
|
||||
if not contract_no:
|
||||
raise ContractCreateError("submit 模式未读取到真实合同编号,无法 complete")
|
||||
return {
|
||||
"businessRefs": payload["businessRefs"],
|
||||
"summary": payload["summary"],
|
||||
"artifacts": payload["artifacts"],
|
||||
"businessRefs": {"contractSystemNo": contract_no},
|
||||
"summary": _build_summary(spec),
|
||||
"artifacts": [],
|
||||
}
|
||||
|
||||
return execute_node(
|
||||
|
|
@ -91,34 +85,46 @@ def execute_contract_create(
|
|||
|
||||
|
||||
def create_contract_in_contract_system(spec: ContractCreateInput) -> str:
|
||||
"""Draft the contract in the contract system and return contract number."""
|
||||
if spec.open_browser:
|
||||
webbrowser.open(spec.contract_url)
|
||||
"""在合同系统中起草合同并返回合同编号。"""
|
||||
if spec.contract_system_no:
|
||||
return spec.contract_system_no
|
||||
|
||||
ctx = spec.flow_context
|
||||
if ctx is None:
|
||||
if not spec.draft_path:
|
||||
raise ContractCreateError("draft_path is required for real contract drafting")
|
||||
draft = load_contract_draft(spec.draft_path)
|
||||
ctx = build_flow_context(spec, draft)
|
||||
browser = YiSaiQiBrowserAdapter()
|
||||
try:
|
||||
raise ContractCreateError("缺少 flow_context 或 draft_path,无法起草合同")
|
||||
ctx = build_flow_context(spec, load_contract_draft(spec.draft_path))
|
||||
|
||||
browser = ApaBrowserAdapter(url=ctx.contract_url)
|
||||
return run_contract_create_flow(browser, ctx)
|
||||
except NotImplementedError as exc:
|
||||
raise ContractCreateError(
|
||||
"YiSaiQiBrowserAdapter still needs to be mapped to YiSaiQi designer activities"
|
||||
) from exc
|
||||
|
||||
|
||||
def build_spec_from_data(data: dict[str, Any]) -> ContractCreateInput:
|
||||
draft = contract_draft_from_dict(data)
|
||||
_validate_data(data, draft)
|
||||
|
||||
spec = ContractCreateInput(
|
||||
job_id=_require(data, "jobId"),
|
||||
supplier_name=draft.supplier_name or _require(data, "supplierName"),
|
||||
order_date=draft.order_date or _require(data, "orderDate"),
|
||||
source_type=draft.source_type or _require(data, "sourceType"),
|
||||
contract_system_no=_opt(data.get("contractSystemNo")),
|
||||
attempt_no=int(data.get("attemptNo", 1) or 1),
|
||||
contract_url=_clean(data.get("contractUrl")) or os.getenv("CONTRACT_URL", CONTRACT_URL),
|
||||
username=_clean(data.get("username")) or os.getenv("CONTRACT_USERNAME", "test002"),
|
||||
password=str(data.get("password") or os.getenv("CONTRACT_PASSWORD", "1")),
|
||||
submit_mode=draft.submit_mode,
|
||||
order_no_list=tuple(draft.order_no_list),
|
||||
template_name=draft.template_name,
|
||||
merged_pdf_output=_opt(data.get("mergedPdfOutput")) or os.getenv("MERGED_PDF_OUTPUT"),
|
||||
)
|
||||
|
||||
ctx = build_flow_context(spec, draft)
|
||||
return replace(spec, flow_context=ctx)
|
||||
|
||||
|
||||
def build_flow_context(spec: ContractCreateInput, draft: ContractDraft) -> ContractFlowContext:
|
||||
merged_pdf_path = draft.merged_pdf_path
|
||||
if draft.source_type == "YYC":
|
||||
output_path = spec.merged_pdf_output or str(
|
||||
Path("runtime") / "contracts" / spec.job_id / "yyc-contract-merged.pdf"
|
||||
)
|
||||
try:
|
||||
merged_pdf_path = ensure_merged_pdf(draft.platform_pdf_paths, output_path)
|
||||
except (RuntimeError, ValueError) as exc:
|
||||
raise PdfMergeUnsupported(str(exc)) from exc
|
||||
merged_pdf_path = _resolve_merged_pdf(draft, spec)
|
||||
return ContractFlowContext(
|
||||
contract_url=spec.contract_url,
|
||||
username=spec.username,
|
||||
|
|
@ -147,51 +153,143 @@ def build_flow_context(spec: ContractCreateInput, draft: ContractDraft) -> Contr
|
|||
submit_mode=draft.submit_mode,
|
||||
goods_lines=draft.goods_lines,
|
||||
merged_pdf_path=merged_pdf_path,
|
||||
order_no_list=draft.order_no_list,
|
||||
)
|
||||
|
||||
|
||||
def build_complete_payload(spec: ContractCreateInput, contract_no: str) -> dict[str, Any]:
|
||||
def _resolve_merged_pdf(draft: ContractDraft, spec: ContractCreateInput) -> str | None:
|
||||
if draft.source_type != "YYC":
|
||||
return None
|
||||
if spec.submit_mode == "dry_run":
|
||||
# dry_run 只跑到提交前,不校验也不上传 PDF
|
||||
return None
|
||||
|
||||
paths = [str(p).strip() for p in draft.platform_pdf_paths if str(p).strip()]
|
||||
if not paths:
|
||||
raise PdfMergeUnsupported("YYC 缺少平台合同 PDF(platformPdfPaths 为空)")
|
||||
for path in paths:
|
||||
if not Path(path).exists():
|
||||
raise PdfMergeUnsupported(f"平台合同 PDF 不存在:{path}")
|
||||
if len(paths) == 1:
|
||||
return paths[0]
|
||||
|
||||
output_path = spec.merged_pdf_output or str(
|
||||
Path("runtime") / "contracts" / spec.job_id / "yyc-contract-merged.pdf"
|
||||
)
|
||||
try:
|
||||
return ensure_merged_pdf(paths, output_path)
|
||||
except Exception as exc:
|
||||
raise PdfMergeUnsupported(f"YYC 平台合同 PDF 合并失败:{exc}") from exc
|
||||
|
||||
|
||||
def _build_summary(spec: ContractCreateInput) -> dict[str, Any]:
|
||||
return {
|
||||
"attemptNo": spec.attempt_no,
|
||||
"executorRunId": f"YSQ-CONTRACT-{uuid.uuid4().hex[:12]}",
|
||||
"idempotencyKey": f"{spec.job_id}:{NODE_CODE}:{spec.attempt_no}:{contract_no}",
|
||||
"businessRefs": {"contractSystemNo": contract_no},
|
||||
"summary": {
|
||||
"supplierName": spec.supplier_name,
|
||||
"contractDate": spec.order_date,
|
||||
"sourceType": spec.source_type,
|
||||
"contractUrl": spec.contract_url,
|
||||
"uploadRule": "merge_pdf_before_upload" if spec.source_type == "YYC" else "skip_contract_upload",
|
||||
},
|
||||
"artifacts": [],
|
||||
"orderNoList": list(spec.order_no_list),
|
||||
"templateName": spec.template_name,
|
||||
"pdfMerged": spec.source_type == "YYC",
|
||||
"submitted": True,
|
||||
}
|
||||
|
||||
|
||||
def build_fail_payload(spec: ContractCreateInput, exc: ContractCreateError) -> dict[str, Any]:
|
||||
return {
|
||||
"attemptNo": spec.attempt_no,
|
||||
"errorCode": exc.error_code,
|
||||
"errorSummary": str(exc),
|
||||
"retryable": exc.retryable,
|
||||
"routeRole": exc.route_role,
|
||||
"artifacts": [],
|
||||
}
|
||||
def _validate_data(data: dict[str, Any], draft: ContractDraft) -> None:
|
||||
missing = []
|
||||
for key in ("jobId", "supplierName", "orderDate", "sourceType", "customerCode"):
|
||||
if not _str(data.get(key)):
|
||||
missing.append(key)
|
||||
|
||||
if draft.submit_mode not in ("dry_run", "submit"):
|
||||
raise ContractCreateError(f"submitMode 非法:{draft.submit_mode}(仅支持 dry_run / submit)")
|
||||
|
||||
if not _str(draft.amount):
|
||||
missing.append("amount")
|
||||
else:
|
||||
try:
|
||||
float(draft.amount)
|
||||
except (TypeError, ValueError):
|
||||
raise ContractCreateError(f"amount 不是合法数字:{draft.amount}")
|
||||
|
||||
if not draft.goods_lines:
|
||||
raise ContractCreateError("goodsLines 为空,至少需要一行货品明细")
|
||||
for index, line in enumerate(draft.goods_lines, start=1):
|
||||
if not _str(line.goods_code) or not _str(line.goods_name) or line.quantity <= 0:
|
||||
raise ContractCreateError(
|
||||
f"goodsLines[{index}] 缺少 goodsCode/goodsName,或 quantity<=0"
|
||||
)
|
||||
|
||||
if missing:
|
||||
raise ContractCreateError("缺少必填字段:" + ", ".join(missing))
|
||||
|
||||
|
||||
def _require(data: dict[str, Any], key: str) -> str:
|
||||
value = _str(data.get(key))
|
||||
if not value:
|
||||
raise ContractCreateError(f"缺少必填字段:{key}")
|
||||
return value
|
||||
|
||||
|
||||
def _opt(value: Any) -> str | None:
|
||||
value = _str(value)
|
||||
return value or None
|
||||
|
||||
|
||||
def _str(value: Any) -> str:
|
||||
if value is None:
|
||||
return ""
|
||||
return str(value).strip()
|
||||
|
||||
|
||||
def _clean(value: Any) -> str:
|
||||
value = _str(value)
|
||||
if len(value) >= 2 and value[0] == "`" and value[-1] == "`":
|
||||
value = value[1:-1].strip()
|
||||
return value
|
||||
|
||||
|
||||
def load_input_data(raw: str) -> dict[str, Any]:
|
||||
"""解析入参:优先按 JSON 字符串,其次按 JSON 文件路径。"""
|
||||
raw = (raw or "").strip()
|
||||
if not raw:
|
||||
return {}
|
||||
try:
|
||||
obj = json.loads(raw)
|
||||
if isinstance(obj, dict):
|
||||
return obj
|
||||
except Exception:
|
||||
pass
|
||||
if os.path.isfile(raw):
|
||||
with open(raw, "r", encoding="utf-8") as file:
|
||||
return json.load(file)
|
||||
raise ContractCreateError(f"无法解析入参:既不是合法 JSON,也不是存在的文件路径:{raw}")
|
||||
|
||||
|
||||
def run_contract_create(
|
||||
data: dict[str, Any],
|
||||
*,
|
||||
state_center_url: str,
|
||||
agent_token: str,
|
||||
) -> dict[str, Any]:
|
||||
spec = build_spec_from_data(data)
|
||||
client = StateCenterClient(state_center_url, agent_token)
|
||||
return execute_contract_create(spec, client)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Run YiSaiQi CONTRACT_CREATE callback skeleton.")
|
||||
parser = argparse.ArgumentParser(description="Run YiSaiQi CONTRACT_CREATE node.")
|
||||
parser.add_argument("--state-center-url", default=os.getenv("STATE_CENTER_URL", "http://127.0.0.1:8000"))
|
||||
parser.add_argument("--token", default=os.getenv("AGENT_TOKEN", "dev-agent-token"))
|
||||
parser.add_argument("--job-id", required=True)
|
||||
parser.add_argument("--supplier-name", required=True)
|
||||
parser.add_argument("--order-date", required=True)
|
||||
parser.add_argument("--input-json", default=os.getenv("CONTRACT_INPUT_JSON"))
|
||||
parser.add_argument("--job-id")
|
||||
parser.add_argument("--supplier-name")
|
||||
parser.add_argument("--order-date")
|
||||
parser.add_argument("--source-type", choices=("YYC", "XYY", "OFFLINE"), default="YYC")
|
||||
parser.add_argument("--contract-system-no", default=os.getenv("CONTRACT_SYSTEM_NO"))
|
||||
parser.add_argument("--attempt-no", type=int, default=1)
|
||||
parser.add_argument("--contract-url", default=os.getenv("CONTRACT_URL", CONTRACT_URL))
|
||||
parser.add_argument("--username", default=os.getenv("CONTRACT_USERNAME", "test002"))
|
||||
parser.add_argument("--password", default=os.getenv("CONTRACT_PASSWORD", "1"))
|
||||
parser.add_argument("--open-browser", action="store_true")
|
||||
parser.add_argument("--draft-path", default=os.getenv("CONTRACT_DRAFT_PATH"))
|
||||
parser.add_argument("--merged-pdf-output", default=os.getenv("MERGED_PDF_OUTPUT"))
|
||||
return parser.parse_args()
|
||||
|
|
@ -199,6 +297,13 @@ def parse_args() -> argparse.Namespace:
|
|||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
client = StateCenterClient(args.state_center_url, args.token)
|
||||
|
||||
if args.input_json:
|
||||
spec = build_spec_from_data(load_input_data(args.input_json))
|
||||
else:
|
||||
if not (args.job_id and args.supplier_name and args.order_date):
|
||||
raise SystemExit("缺少 --job-id/--supplier-name/--order-date 或 --input-json")
|
||||
spec = ContractCreateInput(
|
||||
job_id=args.job_id,
|
||||
supplier_name=args.supplier_name,
|
||||
|
|
@ -209,13 +314,12 @@ def main() -> int:
|
|||
contract_url=args.contract_url,
|
||||
username=args.username,
|
||||
password=args.password,
|
||||
open_browser=args.open_browser,
|
||||
draft_path=args.draft_path,
|
||||
merged_pdf_output=args.merged_pdf_output,
|
||||
)
|
||||
client = StateCenterClient(args.state_center_url, args.token)
|
||||
|
||||
result = execute_contract_create(spec, client)
|
||||
print(result)
|
||||
print(json.dumps(result, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ class ContractDraft:
|
|||
goods_lines: list[ContractGoodsLine] = field(default_factory=list)
|
||||
platform_pdf_paths: list[str] = field(default_factory=list)
|
||||
merged_pdf_path: str | None = None
|
||||
order_no_list: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
def load_contract_draft(path: str | Path) -> ContractDraft:
|
||||
|
|
@ -52,7 +53,9 @@ def contract_draft_from_dict(data: dict[str, Any]) -> ContractDraft:
|
|||
approval_no=str(item.get("approvalNo", "")),
|
||||
spec=str(item.get("spec", "")),
|
||||
unit=str(item.get("unit", "")),
|
||||
quantity=float(item.get("quantity", 0)),
|
||||
quantity=float(item.get("quantity", 0) or 0),
|
||||
mdm_code=str(item.get("mdmCode", "")),
|
||||
unit_price=str(item.get("unitPrice", "")),
|
||||
)
|
||||
for item in data.get("goodsLines", [])
|
||||
]
|
||||
|
|
@ -82,4 +85,5 @@ def contract_draft_from_dict(data: dict[str, Any]) -> ContractDraft:
|
|||
goods_lines=goods_lines,
|
||||
platform_pdf_paths=[str(path) for path in data.get("platformPdfPaths", [])],
|
||||
merged_pdf_path=data.get("mergedPdfPath"),
|
||||
order_no_list=[str(x) for x in data.get("orderNoList", [])],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,18 +5,32 @@ node or one script block behind a flowchart node.
|
|||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Protocol
|
||||
|
||||
from .runtime.errors import (
|
||||
LoginFailed,
|
||||
NodeError,
|
||||
PdfMergeUnsupported,
|
||||
SupplierContractDisabled,
|
||||
)
|
||||
|
||||
|
||||
class BrowserAdapter(Protocol):
|
||||
def open_url(self, url: str) -> None: ...
|
||||
def fill_first(self, selectors: list[str], value: str) -> None: ...
|
||||
def click_first(self, selectors: list[str]) -> None: ...
|
||||
def double_click_first(self, selectors: list[str]) -> None: ...
|
||||
def wait_for_text(self, text: str, timeout_seconds: int = 15) -> None: ...
|
||||
def wait_for_element(self, selectors: list[str], timeout_seconds: int = 15) -> None: ...
|
||||
def element_exists(self, selectors: list[str]) -> bool: ...
|
||||
def switch_to_frame(self, selectors: list[str]) -> None: ...
|
||||
def switch_to_default_content(self) -> None: ...
|
||||
def select_first(self, selectors: list[str], value: str) -> None: ...
|
||||
def upload_file_first(self, selectors: list[str], file_path: str) -> None: ...
|
||||
def read_first(self, selectors: list[str], value_name: str) -> str: ...
|
||||
def read_first(self, selectors: list[str], value_name: str = "") -> str: ...
|
||||
def read_body_text(self) -> str: ...
|
||||
def screenshot(self, name: str) -> str: ...
|
||||
|
||||
|
||||
|
|
@ -29,6 +43,8 @@ class ContractGoodsLine:
|
|||
spec: str
|
||||
unit: str
|
||||
quantity: float
|
||||
mdm_code: str = ""
|
||||
unit_price: str = ""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
@ -60,6 +76,7 @@ class ContractFlowContext:
|
|||
submit_mode: str = "submit"
|
||||
goods_lines: list[ContractGoodsLine] = field(default_factory=list)
|
||||
merged_pdf_path: str | None = None
|
||||
order_no_list: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
def run_contract_create_flow(browser: BrowserAdapter, ctx: ContractFlowContext) -> str:
|
||||
|
|
@ -78,115 +95,256 @@ def run_contract_create_flow(browser: BrowserAdapter, ctx: ContractFlowContext)
|
|||
return read_contract_number(browser)
|
||||
|
||||
|
||||
# 合同起草页(模板选择)直达 URL,绕过 SPA 菜单/iframe 复杂性
|
||||
DRAFT_PAGE_URL = (
|
||||
"http://contract-test.gzmpc.com:8080/km/agreement/km_agreement_apply/draft/"
|
||||
"draft.jsp?isSimpleCategory=false"
|
||||
"&mainModelName=com.landray.kmss.km.agreement.model.KmAgreementApply"
|
||||
"&modelName=com.landray.kmss.km.agreement.model.KmAgreementTmpl"
|
||||
)
|
||||
|
||||
# 非格式-采购协议(2025集团版)模板 ID(测试环境固定值)
|
||||
# 直接跳转表单页,绕过「点模板」这一依赖前端委托事件、Selenium 点击无效的步骤
|
||||
TEMPLATE_ID = "198d10b04325279b8ffeb8b4c6493b5a"
|
||||
FORM_PAGE_URL = (
|
||||
"http://contract-test.gzmpc.com:8080/km/agreement/km_agreement_apply/"
|
||||
"kmAgreementApply.do?method=add"
|
||||
f"&fdTemplateId={TEMPLATE_ID}"
|
||||
f"&.fdTemplate={TEMPLATE_ID}"
|
||||
f"&i.docTemplate={TEMPLATE_ID}"
|
||||
"&s_css=default"
|
||||
)
|
||||
|
||||
# Landray/LUI 表单字段的真实 name
|
||||
F = {
|
||||
"customer_code": 'input[name="extendDataFormInfo.value(fd_customeropcode)"]',
|
||||
"customer_name": 'input[name="extendDataFormInfo.value(fd_customername)"]',
|
||||
"contract_category": 'input[name="extendDataFormInfo.value(fd_contract_cate_type)"]',
|
||||
"settlement_type": 'select#fdSettype',
|
||||
"contract_background": 'textarea[name="extendDataFormInfo.value(fd_desc_info)"]',
|
||||
"nature_main": 'input[name="fdType"][value="00"]',
|
||||
"amount": 'input[name="fdAccount"]',
|
||||
"sign_date": 'input[name="fdSigndate"]',
|
||||
"begin_date": 'input[name="fdEffedate"]',
|
||||
"end_date": 'input[name="fdEnddate"]',
|
||||
"is_major_contract": 'input[name="extendDataFormInfo.value(fd_is_major_contract)"]',
|
||||
"is_prepayment": 'input[name="extendDataFormInfo.value(fd_advance_amout)"]',
|
||||
"belong_year": 'input[name="extendDataFormInfo.value(fd_contract_year_text)"]',
|
||||
"payment_method": 'input[name="extendDataFormInfo.value(fd_paytype)"]',
|
||||
"payment_days": 'input[name="extendDataFormInfo.value(fd_pay_time_limit)"]',
|
||||
"contract_status": 'input[name="extendDataFormInfo.value(fd_suconstatus)"]',
|
||||
"contains_blood": 'input[name="extendDataFormInfo.value(fd_3d1ce3953f1162)"]',
|
||||
}
|
||||
|
||||
# 货品明细行的字段后缀;行索引从 0 开始(fd_detail1.0.* 为第 1 行)
|
||||
GOODS_FIELD_SUFFIXES = {
|
||||
"goods_code": "fd_opcode",
|
||||
"goods_name": "fd_goodsname",
|
||||
"goods_mdm": "fd_mdmCode",
|
||||
"goods_approval": "fd_registdocno",
|
||||
"goods_spec": "fd_goodstype",
|
||||
"goods_unit": "fd_goodsunit",
|
||||
"goods_qty": "fd_goodsqty",
|
||||
"goods_price": "fd_unitprice",
|
||||
}
|
||||
|
||||
ADD_ROW_SELECTORS = [
|
||||
"text=添加行",
|
||||
"text=新增明细",
|
||||
"text=新增行",
|
||||
"text=新增",
|
||||
"button:contains(添加行)",
|
||||
]
|
||||
|
||||
# 客商选择窗口(点击客户名称旁「选择」按钮弹出)。若真实页面改版,仅需调整此处。
|
||||
CUSTOMER_PICKER = {
|
||||
"trigger": [
|
||||
'div.inputselectsgl[onclick*="fdCustomerEdit"]',
|
||||
'div[class*="inputselect"][onclick*="fdCustomer"]',
|
||||
],
|
||||
"frame": [
|
||||
'iframe[src*="common-template"]',
|
||||
'iframe[src*="KmRelativeMain"]',
|
||||
'iframe[src*="relative"]',
|
||||
],
|
||||
"search_input": ["input.search-box-text"],
|
||||
"search_button": ["input.search-box-btn"],
|
||||
"result_checkbox": ['input[name="List_Selected"]'],
|
||||
"confirm_button": ["div.btn.ok", "div.lui_toolbar_btn_def"],
|
||||
}
|
||||
|
||||
|
||||
def open_contract_system(browser: BrowserAdapter, ctx: ContractFlowContext) -> None:
|
||||
browser.open_url(ctx.contract_url)
|
||||
|
||||
|
||||
def login_contract_system(browser: BrowserAdapter, ctx: ContractFlowContext) -> None:
|
||||
try:
|
||||
browser.fill_first(['input[name="j_username"]', 'input[placeholder="用户名"]'], ctx.username)
|
||||
browser.fill_first(['input[name="j_password"]', 'input[placeholder="密码"]'], ctx.password)
|
||||
browser.click_first(['input[name="btn_submit"]', 'a[href*="btn_submit"]', 'text=登录'])
|
||||
browser.click_first(['a[class*="login_button"]', 'a:contains("登录")', 'input[name="btn_submit"]'])
|
||||
browser.wait_for_text("合同", timeout_seconds=20)
|
||||
except Exception as exc:
|
||||
browser.screenshot("contract-login-failed")
|
||||
raise LoginFailed(f"合同系统登录失败:{exc}") from exc
|
||||
|
||||
|
||||
def enter_contract_create(browser: BrowserAdapter) -> None:
|
||||
browser.click_first(["text=合同管理", "aria=合同管理"])
|
||||
browser.click_first(["text=合同起草", "aria=合同起草"])
|
||||
browser.wait_for_text("最近使用模板", timeout_seconds=20)
|
||||
# 登录后直接导航到起草表单页,跳过模板选择(前端委托事件 Selenium 点击无效)
|
||||
browser.open_url(FORM_PAGE_URL)
|
||||
browser.wait_for_text("客户编码", timeout_seconds=20)
|
||||
|
||||
|
||||
def select_contract_template(browser: BrowserAdapter, ctx: ContractFlowContext) -> None:
|
||||
template_text = ctx.template_name or (
|
||||
"非格式--采购协议" if ctx.template_type == "NON_FORMAT" else "购销协议书(2025)"
|
||||
)
|
||||
browser.click_first([f"text={template_text}", f"text={template_text}...", f"title={template_text}"])
|
||||
browser.wait_for_text("合同起草", timeout_seconds=20)
|
||||
# 已通过 URL 直接指定模板,本步骤仅兜底:若尚未进入表单则再次导航
|
||||
if "客户编码" not in browser.read_body_text():
|
||||
browser.open_url(FORM_PAGE_URL)
|
||||
browser.wait_for_text("客户编码", timeout_seconds=20)
|
||||
|
||||
|
||||
def fill_contract_signatory_info(browser: BrowserAdapter, ctx: ContractFlowContext) -> None:
|
||||
customer_name = ctx.customer_name or ctx.supplier_name
|
||||
if ctx.customer_code:
|
||||
browser.fill_first(
|
||||
['label=客户编码', 'input[name="fdCustomerCode"]', 'input[placeholder*="客户编码"]'],
|
||||
ctx.customer_code,
|
||||
expected_name = (ctx.customer_name or ctx.supplier_name).strip()
|
||||
if not ctx.customer_code:
|
||||
raise SupplierContractDisabled("客商操作码(customerCode)为空,无法加载客商")
|
||||
|
||||
# 点击客户名称字段旁的「选择」按钮打开客商选择窗口,按编码搜索并带回客商
|
||||
browser.click_first(CUSTOMER_PICKER["trigger"])
|
||||
_pick_customer_from_dialog(browser, ctx.customer_code)
|
||||
loaded_name = _wait_customer_name(browser, timeout=8)
|
||||
|
||||
if not loaded_name:
|
||||
browser.screenshot("contract-customer-load-failed")
|
||||
raise SupplierContractDisabled(
|
||||
f"客商加载失败,请确认输入的客商操作码是否有误:{ctx.customer_code}"
|
||||
)
|
||||
if customer_name:
|
||||
browser.fill_first(
|
||||
['label=客户名称', 'input[name="fdCustomerName"]', 'input[placeholder*="客户名称"]'],
|
||||
customer_name,
|
||||
|
||||
if expected_name and loaded_name != expected_name:
|
||||
browser.screenshot("contract-customer-name-mismatch")
|
||||
raise SupplierContractDisabled(
|
||||
f"客商名称校验不通过:期望「{expected_name}」,实际「{loaded_name}」"
|
||||
)
|
||||
|
||||
browser.screenshot("contract-signatory-info-filled")
|
||||
|
||||
|
||||
def _wait_customer_name(browser: BrowserAdapter, timeout: int) -> str:
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
body = browser.read_body_text()
|
||||
if "自动加载客商失败" in body or "客商操作码是否有误" in body:
|
||||
return ""
|
||||
name = _try_read(browser, [F["customer_name"]])
|
||||
if name and name not in ("==请选择==", "请选择"):
|
||||
return name
|
||||
time.sleep(0.5)
|
||||
return ""
|
||||
|
||||
|
||||
def _pick_customer_from_dialog(browser: BrowserAdapter, code: str) -> None:
|
||||
# 客商选择窗口是一个 iframe 弹窗:切入后搜索编码 → 选中结果复选框 → 切回主文档。
|
||||
# 单选模式下点击复选框即触发回填并关闭弹窗;若未自动关闭再点确认按钮兜底。
|
||||
browser.wait_for_element(CUSTOMER_PICKER["frame"], timeout_seconds=10)
|
||||
browser.switch_to_frame(CUSTOMER_PICKER["frame"])
|
||||
try:
|
||||
browser.fill_first(CUSTOMER_PICKER["search_input"], code)
|
||||
browser.click_first(CUSTOMER_PICKER["search_button"])
|
||||
time.sleep(1)
|
||||
browser.wait_for_element(CUSTOMER_PICKER["result_checkbox"], timeout_seconds=10)
|
||||
browser.click_first(CUSTOMER_PICKER["result_checkbox"])
|
||||
time.sleep(1)
|
||||
finally:
|
||||
browser.switch_to_default_content()
|
||||
|
||||
|
||||
def fill_contract_basic_info(browser: BrowserAdapter, ctx: ContractFlowContext) -> None:
|
||||
browser.select_first(['label=合同版本类型', 'select[name="fdTemplateType"]'], contract_version_label(ctx))
|
||||
if ctx.contract_category:
|
||||
browser.select_first(['label=合同类别', 'select[name="fdContractCategory"]'], ctx.contract_category)
|
||||
browser.select_first(['label=结算类型', 'select[name="fdSettlementType"]'], ctx.settlement_type)
|
||||
browser.fill_first([F["contract_category"]], ctx.contract_category)
|
||||
if ctx.settlement_type:
|
||||
_select_settlement_type(browser, ctx.settlement_type)
|
||||
if ctx.contract_background:
|
||||
browser.fill_first(
|
||||
['label=合同背景概述', 'textarea[name="fdContractBackground"]'],
|
||||
ctx.contract_background,
|
||||
)
|
||||
browser.click_first([f"label={ctx.contract_nature}", f"text={ctx.contract_nature}"])
|
||||
browser.fill_first([F["contract_background"]], ctx.contract_background)
|
||||
# 合同性质默认主合同(页面已勾选),仅在需要从合同时切换
|
||||
if ctx.contract_nature and ctx.contract_nature != "主合同":
|
||||
browser.click_first(['input[name="fdType"][value="01"]'])
|
||||
browser.screenshot("contract-basic-info-filled")
|
||||
|
||||
|
||||
def _select_settlement_type(browser: BrowserAdapter, value: str) -> None:
|
||||
browser.select_first([F["settlement_type"]], value)
|
||||
selected = _try_read(browser, [F["settlement_type"]])
|
||||
if not selected or selected in ("==请选择==", "请选择"):
|
||||
# LUI 自定义下拉兜底:点击展开后按可见文本选择
|
||||
browser.click_first([F["settlement_type"]])
|
||||
browser.click_first([f"//option[contains(text(),'{value}')]", f"text={value}"])
|
||||
selected = _try_read(browser, [F["settlement_type"]])
|
||||
if not selected or selected in ("==请选择==", "请选择"):
|
||||
raise NodeError(f"结算方式下拉未选中,仍停留在「==请选择==」,期望「{value}」")
|
||||
|
||||
|
||||
def fill_contract_amount_and_dates(browser: BrowserAdapter, ctx: ContractFlowContext) -> None:
|
||||
browser.select_first(['label=合同金额', 'select[name="fdCurrency"]'], ctx.amount_currency)
|
||||
if ctx.amount:
|
||||
browser.fill_first(['label=金额(元)', 'input[name="fdAmount"]'], ctx.amount)
|
||||
browser.fill_first(
|
||||
['label=合同签订日期', 'input[name="fdSignDate"]', 'input[placeholder*="合同签订"]'],
|
||||
ctx.order_date,
|
||||
)
|
||||
browser.fill_first(
|
||||
['label=合同开始日期', 'input[name="fdStartDate"]', 'input[placeholder*="合同开始"]'],
|
||||
ctx.order_date,
|
||||
)
|
||||
browser.fill_first([F["amount"]], ctx.amount)
|
||||
browser.fill_first([F["sign_date"]], ctx.order_date)
|
||||
browser.fill_first([F["begin_date"]], ctx.order_date)
|
||||
if ctx.contract_end_date:
|
||||
browser.fill_first(
|
||||
['label=合同结束日期', 'input[name="fdEndDate"]', 'input[placeholder*="合同结束"]'],
|
||||
ctx.contract_end_date,
|
||||
)
|
||||
browser.fill_first([F["end_date"]], ctx.contract_end_date)
|
||||
browser.screenshot("contract-amount-and-dates-filled")
|
||||
|
||||
|
||||
def fill_contract_goods_lines(browser: BrowserAdapter, ctx: ContractFlowContext) -> None:
|
||||
for index, line in enumerate(ctx.goods_lines, start=1):
|
||||
if index > 1:
|
||||
browser.click_first(["text=新增明细", "text=添加货品", "text=新增行"])
|
||||
prefix = f"明细{index}"
|
||||
browser.fill_first([f'label={prefix}操作码', 'input[name="operationCode"]'], line.operation_code)
|
||||
browser.fill_first([f'label={prefix}货品编码', 'input[name="goodsCode"]'], line.goods_code)
|
||||
browser.fill_first([f'label={prefix}名称', 'input[name="goodsName"]'], line.goods_name)
|
||||
browser.fill_first([f'label={prefix}国药准字', 'input[name="approvalNo"]'], line.approval_no)
|
||||
browser.fill_first([f'label={prefix}规格', 'input[name="spec"]'], line.spec)
|
||||
browser.fill_first([f'label={prefix}单位', 'input[name="unit"]'], line.unit)
|
||||
browser.fill_first([f'label={prefix}数量', 'input[name="quantity"]'], str(line.quantity))
|
||||
for index, line in enumerate(ctx.goods_lines):
|
||||
row = index # 第 1 行 row=0,第 2 行 row=1,依此类推
|
||||
if index > 0:
|
||||
browser.click_first(ADD_ROW_SELECTORS)
|
||||
browser.wait_for_element([_goods_selector("goods_code", row)], timeout_seconds=15)
|
||||
|
||||
browser.fill_first([_goods_selector("goods_code", row)], line.goods_code)
|
||||
browser.fill_first([_goods_selector("goods_name", row)], line.goods_name)
|
||||
if line.mdm_code:
|
||||
browser.fill_first([_goods_selector("goods_mdm", row)], line.mdm_code)
|
||||
if line.approval_no:
|
||||
browser.fill_first([_goods_selector("goods_approval", row)], line.approval_no)
|
||||
if line.spec:
|
||||
browser.fill_first([_goods_selector("goods_spec", row)], line.spec)
|
||||
if line.unit:
|
||||
browser.fill_first([_goods_selector("goods_unit", row)], line.unit)
|
||||
browser.fill_first([_goods_selector("goods_qty", row)], str(line.quantity))
|
||||
if line.unit_price:
|
||||
browser.fill_first([_goods_selector("goods_price", row)], str(line.unit_price))
|
||||
browser.screenshot("contract-goods-lines-filled")
|
||||
|
||||
|
||||
def _goods_selector(field_key: str, row_index: int) -> str:
|
||||
suffix = GOODS_FIELD_SUFFIXES[field_key]
|
||||
return f'input[name="extendDataFormInfo.value(fd_detail1.{row_index}.{suffix})"]'
|
||||
|
||||
|
||||
def fill_contract_payment_info(browser: BrowserAdapter, ctx: ContractFlowContext) -> None:
|
||||
browser.select_first(['label=是否重大合同', 'select[name="fdIsMajorContract"]'], ctx.is_major_contract)
|
||||
browser.select_first(['label=是否预付款', 'select[name="fdIsPrepayment"]'], ctx.is_prepayment)
|
||||
browser.fill_first([F["is_major_contract"]], ctx.is_major_contract)
|
||||
browser.fill_first([F["is_prepayment"]], ctx.is_prepayment)
|
||||
if ctx.belong_year:
|
||||
browser.fill_first(['label=合同归属年份', 'input[name="fdBelongYear"]'], ctx.belong_year)
|
||||
browser.select_first(['label=付款方式', 'select[name="fdPaymentMethod"]', 'select[name="paymentMethod"]'], ctx.payment_method)
|
||||
browser.fill_first(['label=付款期限', 'input[name="fdPaymentDays"]', 'input[name="paymentDays"]'], ctx.payment_days)
|
||||
browser.select_first(['label=合同状态', 'select[name="fdContractStatus"]'], ctx.contract_status)
|
||||
browser.select_first(
|
||||
['label=是否包含血液制品', 'select[name="fdContainsBloodProducts"]'],
|
||||
ctx.contains_blood_products,
|
||||
)
|
||||
browser.fill_first([F["belong_year"]], ctx.belong_year)
|
||||
if ctx.payment_method:
|
||||
browser.fill_first([F["payment_method"]], ctx.payment_method)
|
||||
if ctx.payment_days:
|
||||
browser.fill_first([F["payment_days"]], ctx.payment_days)
|
||||
if ctx.contract_status:
|
||||
browser.fill_first([F["contract_status"]], ctx.contract_status)
|
||||
if ctx.contains_blood_products:
|
||||
browser.fill_first([F["contains_blood"]], ctx.contains_blood_products)
|
||||
browser.screenshot("contract-payment-filled")
|
||||
|
||||
|
||||
def upload_contract_document_if_needed(browser: BrowserAdapter, ctx: ContractFlowContext) -> None:
|
||||
if ctx.source_type != "YYC":
|
||||
# XYY / OFFLINE:跳过合同证文上传
|
||||
return
|
||||
if ctx.submit_mode == "dry_run":
|
||||
# dry_run 只跑到提交前,不真正上传附件
|
||||
browser.screenshot("contract-pdf-upload-skipped")
|
||||
return
|
||||
if not ctx.merged_pdf_path:
|
||||
raise ValueError("YYC contract creation requires merged_pdf_path before upload")
|
||||
raise PdfMergeUnsupported("YYC 合同起草缺少合并后的 PDF 路径,无法上传")
|
||||
browser.upload_file_first(['input[type="file"]', "label=合同证文上传"], ctx.merged_pdf_path)
|
||||
browser.screenshot("contract-pdf-uploaded")
|
||||
|
||||
|
|
@ -208,5 +366,14 @@ def read_contract_number(browser: BrowserAdapter) -> str:
|
|||
)
|
||||
|
||||
|
||||
def _try_read(browser: BrowserAdapter, selectors: list[str]) -> str:
|
||||
if not browser.element_exists(selectors):
|
||||
return ""
|
||||
try:
|
||||
return (browser.read_first(selectors, "") or "").strip()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def contract_version_label(ctx: ContractFlowContext) -> str:
|
||||
return "非格式合同" if ctx.template_type == "NON_FORMAT" else "格式合同"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"jobId": "TEST-CONTRACT-20260813-001",
|
||||
"supplierName": "重庆亿昊药业有限公司",
|
||||
"orderDate": "2026-08-12",
|
||||
"sourceType": "YYC",
|
||||
"templateType": "NON_FORMAT",
|
||||
"templateName": "非格式--采购协议",
|
||||
"customerCode": "DW0100046361",
|
||||
"customerName": "重庆亿昊药业有限公司",
|
||||
"contractCategory": "采购协议",
|
||||
"settlementType": "付款",
|
||||
"contractBackground": "自动化测试合同起草",
|
||||
"contractNature": "主合同",
|
||||
"amountCurrency": "人民币",
|
||||
"amount": "50000.00",
|
||||
"contractEndDate": "2026-12-31",
|
||||
"isMajorContract": "否",
|
||||
"isPrepayment": "否",
|
||||
"belongYear": "2026",
|
||||
"paymentMethod": "账期支付",
|
||||
"paymentDays": "30",
|
||||
"contractStatus": "正式合同",
|
||||
"containsBloodProducts": "否",
|
||||
"submitMode": "dry_run",
|
||||
"orderNoList": [],
|
||||
"platformPdfPaths": [],
|
||||
"goodsLines": [
|
||||
{
|
||||
"operationCode": "ADD",
|
||||
"goodsCode": "YP-001",
|
||||
"goodsName": "测试药品A",
|
||||
"approvalNo": "国药准字H00000001",
|
||||
"spec": "50mg*30片",
|
||||
"unit": "盒",
|
||||
"quantity": 10,
|
||||
"mdmCode": "",
|
||||
"unitPrice": ""
|
||||
}
|
||||
],
|
||||
"stateCenterUrl": "http://127.0.0.1:8000",
|
||||
"agentToken": "dev-agent-token"
|
||||
}
|
||||
|
|
@ -1 +1,4 @@
|
|||
pypdf==5.1.0
|
||||
selenium>=4.15.0
|
||||
webdriver-manager>=4.0.0
|
||||
requests>=2.31.0
|
||||
|
|
|
|||
|
|
@ -23,3 +23,9 @@ class PdfMergeUnsupported(NodeError):
|
|||
error_code = "CAP_PDF_MERGE_UNSUPPORTED"
|
||||
retryable = False
|
||||
route_role = "OPS"
|
||||
|
||||
|
||||
class UnknownRpaError(NodeError):
|
||||
error_code = "RPA_UNKNOWN_ERROR"
|
||||
retryable = True
|
||||
route_role = "OPS"
|
||||
|
|
|
|||
|
|
@ -1,116 +1,42 @@
|
|||
{
|
||||
"knownLoginSelectors": {
|
||||
"login": {
|
||||
"username": "input[name='j_username']",
|
||||
"password": "input[name='j_password']",
|
||||
"submit": "input[name='btn_submit']"
|
||||
"submit": "input[name='btn_submit']",
|
||||
"submitLink": "text=登录"
|
||||
},
|
||||
"candidateSelectors": {
|
||||
"contractManagementMenu": [
|
||||
"text=合同管理",
|
||||
"aria=合同管理"
|
||||
],
|
||||
"contractDraftMenu": [
|
||||
"text=合同起草",
|
||||
"aria=合同起草"
|
||||
],
|
||||
"recentNonFormatPurchaseTemplate": [
|
||||
"text=非格式--采购协议",
|
||||
"text=非格式--采购协议...",
|
||||
"title=非格式--采购协议"
|
||||
],
|
||||
"customerCode": [
|
||||
"label=客户编码",
|
||||
"input[name='fdCustomerCode']",
|
||||
"input[placeholder*='客户编码']"
|
||||
],
|
||||
"customerName": [
|
||||
"label=客户名称",
|
||||
"input[name='fdCustomerName']",
|
||||
"input[placeholder*='客户名称']"
|
||||
],
|
||||
"contractVersionType": [
|
||||
"label=合同版本类型",
|
||||
"select[name='fdTemplateType']"
|
||||
],
|
||||
"contractCategory": [
|
||||
"label=合同类别",
|
||||
"select[name='fdContractCategory']"
|
||||
],
|
||||
"settlementType": [
|
||||
"label=结算类型",
|
||||
"select[name='fdSettlementType']"
|
||||
],
|
||||
"contractBackground": [
|
||||
"label=合同背景概述",
|
||||
"textarea[name='fdContractBackground']"
|
||||
],
|
||||
"contractNatureMain": [
|
||||
"label=主合同",
|
||||
"text=主合同"
|
||||
],
|
||||
"amountCurrency": [
|
||||
"label=合同金额",
|
||||
"select[name='fdCurrency']"
|
||||
],
|
||||
"amount": [
|
||||
"label=金额(元)",
|
||||
"input[name='fdAmount']"
|
||||
],
|
||||
"signDate": [
|
||||
"label=合同签订日期",
|
||||
"input[name='fdSignDate']",
|
||||
"input[placeholder*='合同签订']"
|
||||
],
|
||||
"beginDate": [
|
||||
"label=合同开始日期",
|
||||
"input[name='fdStartDate']",
|
||||
"input[placeholder*='合同开始']"
|
||||
],
|
||||
"endDate": [
|
||||
"label=合同结束日期",
|
||||
"input[name='fdEndDate']",
|
||||
"input[placeholder*='合同结束']"
|
||||
],
|
||||
"isMajorContract": [
|
||||
"label=是否重大合同",
|
||||
"select[name='fdIsMajorContract']"
|
||||
],
|
||||
"isPrepayment": [
|
||||
"label=是否预付款",
|
||||
"select[name='fdIsPrepayment']"
|
||||
],
|
||||
"belongYear": [
|
||||
"label=合同归属年份",
|
||||
"input[name='fdBelongYear']"
|
||||
],
|
||||
"paymentMethod": [
|
||||
"label=付款方式",
|
||||
"select[name='fdPaymentMethod']",
|
||||
"select[name='paymentMethod']"
|
||||
],
|
||||
"paymentDays": [
|
||||
"label=付款期限",
|
||||
"input[name='fdPaymentDays']",
|
||||
"input[name='paymentDays']"
|
||||
],
|
||||
"contractStatus": [
|
||||
"label=合同状态",
|
||||
"select[name='fdContractStatus']"
|
||||
],
|
||||
"containsBloodProducts": [
|
||||
"label=是否包含血液制品",
|
||||
"select[name='fdContainsBloodProducts']"
|
||||
],
|
||||
"submit": [
|
||||
"text=提交",
|
||||
"text=提交送审",
|
||||
"text=送审"
|
||||
],
|
||||
"contractNumber": [
|
||||
"input[name='contractNo']",
|
||||
"input[name='contractSystemNo']",
|
||||
"label=合同编号",
|
||||
"text=合同编号"
|
||||
]
|
||||
"navigation": {
|
||||
"draftPage": "http://contract-test.gzmpc.com:8080/km/agreement/km_agreement_apply/draft/draft.jsp?isSimpleCategory=false&mainModelName=com.landray.kmss.km.agreement.model.KmAgreementApply&modelName=com.landray.kmss.km.agreement.model.KmAgreementTmpl",
|
||||
"templatePrefix": "非格式-采购协议"
|
||||
},
|
||||
"form": {
|
||||
"customerCode": "input[name='extendDataFormInfo.value(fd_customeropcode)']",
|
||||
"customerName": "input[name='extendDataFormInfo.value(fd_customername)']",
|
||||
"contractCategory": "input[name='extendDataFormInfo.value(fd_contract_cate_type)']",
|
||||
"settlementType": "select#fdSettype",
|
||||
"contractBackground": "textarea[name='extendDataFormInfo.value(fd_desc_info)']",
|
||||
"contractNatureMain": "input[name='fdType'][value='00']",
|
||||
"contractNatureSub": "input[name='fdType'][value='01']",
|
||||
"amount": "input[name='fdAccount']",
|
||||
"signDate": "input[name='fdSigndate']",
|
||||
"beginDate": "input[name='fdEffedate']",
|
||||
"endDate": "input[name='fdEnddate']",
|
||||
"isMajorContract": "input[name='extendDataFormInfo.value(fd_is_major_contract)']",
|
||||
"isPrepayment": "input[name='extendDataFormInfo.value(fd_advance_amout)']",
|
||||
"belongYear": "input[name='extendDataFormInfo.value(fd_contract_year_text)']",
|
||||
"paymentMethod": "input[name='extendDataFormInfo.value(fd_paytype)']",
|
||||
"paymentDays": "input[name='extendDataFormInfo.value(fd_pay_time_limit)']",
|
||||
"contractStatus": "input[name='extendDataFormInfo.value(fd_suconstatus)']",
|
||||
"containsBloodProducts": "input[name='extendDataFormInfo.value(fd_3d1ce3953f1162)']",
|
||||
"goodsOperationCode": "input[name='extendDataFormInfo.value(fd_detail1.0.fd_opcode)']",
|
||||
"goodsCode": "input[name='extendDataFormInfo.value(fd_detail1.0.fd_opcode)']",
|
||||
"goodsName": "input[name='extendDataFormInfo.value(fd_detail1.0.fd_goodsname)']",
|
||||
"goodsMdmCode": "input[name='extendDataFormInfo.value(fd_detail1.0.fd_mdmCode)']",
|
||||
"goodsApprovalNo": "input[name='extendDataFormInfo.value(fd_detail1.0.fd_registdocno)']",
|
||||
"goodsSpec": "input[name='extendDataFormInfo.value(fd_detail1.0.fd_goodstype)']",
|
||||
"goodsUnit": "input[name='extendDataFormInfo.value(fd_detail1.0.fd_goodsunit)']",
|
||||
"goodsQuantity": "input[name='extendDataFormInfo.value(fd_detail1.0.fd_goodsqty)']",
|
||||
"goodsUnitPrice": "input[name='extendDataFormInfo.value(fd_detail1.0.fd_unitprice)']",
|
||||
"submit": "text=提交"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
{
|
||||
"LineList": [
|
||||
{
|
||||
"LineColor": "255, 255, 255",
|
||||
"LineCommented": 0,
|
||||
"LineEnd": {
|
||||
"Pidx": "20260813200000/EPin_Flow_In",
|
||||
"Px": "512",
|
||||
"Py": "300"
|
||||
},
|
||||
"LineNote": "",
|
||||
"LineSplit": {
|
||||
"Px": "",
|
||||
"Py": ""
|
||||
},
|
||||
"LineStart": {
|
||||
"Pidx": "20260813100000/EPin_Flow_Out",
|
||||
"Px": "457",
|
||||
"Py": "246"
|
||||
},
|
||||
"LineTag": "20260813300000",
|
||||
"LineType": 0
|
||||
}
|
||||
],
|
||||
"NodeList": [
|
||||
{
|
||||
"NodePanelData": {
|
||||
"ExpandedGroups": [],
|
||||
"InputProperty": {
|
||||
"mouse_x": 0,
|
||||
"mouse_y": 0
|
||||
},
|
||||
"PubProperty": "",
|
||||
"RetPins": []
|
||||
},
|
||||
"NodeProperty": {
|
||||
"EP_CONTROL": {
|
||||
"EP_CONTROL_IMPORT": "{\n \"PImport\": \"\",\n \"PBCode\": \"aW1wb3J0IHN5cywgb3MsIGpzb24KZnJvbSBwYXRobGliIGltcG9ydCBQYXRoCgpfUkVQT19ST09UID0gbmV4dCgKICAgIHAgZm9yIHAgaW4gUGF0aChfX2ZpbGVfXykucmVzb2x2ZSgpLnBhcmVudHMKICAgIGlmIChwIC8gInJwYV95aXNhaXFpIiAvICJfX2luaXRfXy5weSIpLmV4aXN0cygpCikKaWYgc3RyKF9SRVBPX1JPT1QpIG5vdCBpbiBzeXMucGF0aDoKICAgIHN5cy5wYXRoLmluc2VydCgwLCBzdHIoX1JFUE9fUk9PVCkpCgpmcm9tIHJwYV95aXNhaXFpLmNvbnRyYWN0X2NyZWF0ZV9ub2RlIGltcG9ydCAoCiAgICBDb250cmFjdENyZWF0ZUVycm9yLCBsb2FkX2lucHV0X2RhdGEsIHJ1bl9jb250cmFjdF9jcmVhdGUsCikKCmRlZiBfZmlyc3QoKnZhbHVlcyk6CiAgICBmb3IgdmFsdWUgaW4gdmFsdWVzOgogICAgICAgIGlmIHZhbHVlIGlzIG5vdCBOb25lIGFuZCBzdHIodmFsdWUpLnN0cmlwKCk6CiAgICAgICAgICAgIHJldHVybiBzdHIodmFsdWUpLnN0cmlwKCkKICAgIHJldHVybiAiIgoKcmF3ID0gKHNlbGYuaW5wdXRfYXJnIG9yICIiKS5zdHJpcCgpCmlmIG5vdCByYXc6CiAgICByYWlzZSBDb250cmFjdENyZWF0ZUVycm9yKCLnvLrlsJHkuJrliqHlhaXlj4LvvJror7fpgJrov4cgLWkvLS1pbnB1dCDkvKDlhaUgSlNPTiDlrZfnrKbkuLLmiJbmlofku7bot6/lvoQiKQoKZGF0YSA9IGxvYWRfaW5wdXRfZGF0YShyYXcpCgpzdGF0ZV9jZW50ZXJfdXJsID0gX2ZpcnN0KAogICAgZGF0YS5nZXQoInN0YXRlQ2VudGVyVXJsIiksIGRhdGEuZ2V0KCJzdGF0ZUNlbnRlclVSTCIpLAogICAgb3MuZ2V0ZW52KCJTVEFURV9DRU5URVJfVVJMIiksICJodHRwOi8vMTI3LjAuMC4xOjgwMDAiLAopCmFnZW50X3Rva2VuID0gX2ZpcnN0KAogICAgZGF0YS5nZXQoImFnZW50VG9rZW4iKSwgb3MuZ2V0ZW52KCJBR0VOVF9UT0tFTiIpLCAiZGV2LWFnZW50LXRva2VuIiwKKQoKcHJpbnQoIj0iICogNjApCnByaW50KCLlkIjlkIzlu7rljZUgQ09OVFJBQ1RfQ1JFQVRFIC0g5byA5aeLIikKcHJpbnQoImpvYklkPSVzIHNvdXJjZVR5cGU9JXMgc3VibWl0TW9kZT0lcyIgJSAoCiAgICBkYXRhLmdldCgiam9iSWQiKSwgZGF0YS5nZXQoInNvdXJjZVR5cGUiKSwgZGF0YS5nZXQoInN1Ym1pdE1vZGUiKSkpCnByaW50KCI9IiAqIDYwKQoKcmVzdWx0ID0gcnVuX2NvbnRyYWN0X2NyZWF0ZSgKICAgIGRhdGEsCiAgICBzdGF0ZV9jZW50ZXJfdXJsPXN0YXRlX2NlbnRlcl91cmwsCiAgICBhZ2VudF90b2tlbj1hZ2VudF90b2tlbiwKKQpwcmludChqc29uLmR1bXBzKHJlc3VsdCwgZW5zdXJlX2FzY2lpPUZhbHNlKSkK\"\n}"
|
||||
},
|
||||
"EP_Gen": {
|
||||
"EP_Gen_AFTERDELAY": "{\n \"val\": \"MA==\"\n}",
|
||||
"EP_Gen_BEFOREDELAY": "{\n \"val\": \"MA==\"\n}",
|
||||
"EP_Gen_NOTE": "{\n \"val\": \"6K+75Y+W5YWl5Y+C5bm25omn6KGMIENPTlRSQUNUX0NSRUFURe+8iOWQq+eKtuaAgeS4reW/g+Wbnuiwg++8iQ==\"\n}"
|
||||
}
|
||||
},
|
||||
"NodeUI": {
|
||||
"NodeElementID": "",
|
||||
"NodeModel": "2",
|
||||
"NodePropErr": false,
|
||||
"NodePx": "515",
|
||||
"NodePy": "270",
|
||||
"NodeSubType": "EnumWidget_Code_Custom",
|
||||
"NodeTag": "20260813200000",
|
||||
"NodeTitle": "读取入参并执行合同建单"
|
||||
}
|
||||
},
|
||||
{
|
||||
"NodePanelData": {
|
||||
"ExpandedGroups": [],
|
||||
"InputProperty": {
|
||||
"mouse_x": 0,
|
||||
"mouse_y": 0
|
||||
},
|
||||
"PubProperty": "",
|
||||
"RetPins": []
|
||||
},
|
||||
"NodeProperty": "",
|
||||
"NodeUI": {
|
||||
"NodeElementID": "",
|
||||
"NodeModel": "2",
|
||||
"NodePropErr": false,
|
||||
"NodePx": "330",
|
||||
"NodePy": "230",
|
||||
"NodeSubType": "EnumWidget_Begin",
|
||||
"NodeTag": "20260813100000",
|
||||
"NodeTitle": "开始"
|
||||
}
|
||||
}
|
||||
],
|
||||
"SeqTag": "Main",
|
||||
"ViewCenterPx": "600",
|
||||
"ViewCenterPy": "300",
|
||||
"ViewScaling": "1"
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
# coding=utf-8
|
||||
# 编译日期:2026-08-13 14:30:00
|
||||
# 版权所有:www.i-search.com.cn
|
||||
import ubpa.init_input as iinput
|
||||
from ubpa.base_util import StdOutHook, ExceptionHandler
|
||||
import time
|
||||
import pdb
|
||||
from ubpa.ilog import ILog
|
||||
import getopt
|
||||
from sys import argv
|
||||
import sys
|
||||
import os
|
||||
import datetime as is_datetime
|
||||
import pandas
|
||||
from ubpa.base_img import *
|
||||
import ubpa.iplatform as iplatform
|
||||
|
||||
class AutoRpa:
|
||||
|
||||
def __init__(self,**kwargs):
|
||||
self.__logger = ILog(__file__)
|
||||
self.path = set_img_res_path(__file__)
|
||||
self.robot_no = ''
|
||||
self.proc_no = ''
|
||||
self.job_no = ''
|
||||
self.input_arg = ''
|
||||
if('robot_no' in kwargs.keys()):
|
||||
self.robot_no = kwargs['robot_no']
|
||||
if('proc_no' in kwargs.keys()):
|
||||
self.proc_no = kwargs['proc_no']
|
||||
if('job_no' in kwargs.keys()):
|
||||
self.job_no = kwargs['job_no']
|
||||
ILog.JOB_NO, ILog.OLD_STDOUT = self.job_no, sys.stdout
|
||||
sys.stdout = StdOutHook(self.job_no, sys.stdout)
|
||||
ExceptionHandler.JOB_NO, ExceptionHandler.OLD_STDERR = self.job_no, sys.stderr
|
||||
sys.excepthook = ExceptionHandler.handle_exception
|
||||
if('input_arg' in kwargs.keys()):
|
||||
self.input_arg = kwargs['input_arg']
|
||||
if(len(self.input_arg) <= 0):
|
||||
self.input_arg = iinput.load_init(__file__)
|
||||
if self.input_arg is None:
|
||||
sys.exit(0)
|
||||
|
||||
def Main(self):
|
||||
import sys, os, json
|
||||
from pathlib import Path
|
||||
|
||||
_REPO_ROOT = next(
|
||||
p for p in Path(__file__).resolve().parents
|
||||
if (p / "rpa_yisaiqi" / "__init__.py").exists()
|
||||
)
|
||||
if str(_REPO_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(_REPO_ROOT))
|
||||
|
||||
from rpa_yisaiqi.contract_create_node import (
|
||||
ContractCreateError, load_input_data, run_contract_create,
|
||||
)
|
||||
|
||||
def _first(*values):
|
||||
for value in values:
|
||||
if value is not None and str(value).strip():
|
||||
return str(value).strip()
|
||||
return ""
|
||||
|
||||
raw = (self.input_arg or "").strip()
|
||||
if not raw:
|
||||
raise ContractCreateError("缺少业务入参:请通过 -i/--input 传入 JSON 字符串或文件路径")
|
||||
|
||||
data = load_input_data(raw)
|
||||
|
||||
state_center_url = _first(
|
||||
data.get("stateCenterUrl"), data.get("stateCenterURL"),
|
||||
os.getenv("STATE_CENTER_URL"), "http://127.0.0.1:8000",
|
||||
)
|
||||
agent_token = _first(
|
||||
data.get("agentToken"), os.getenv("AGENT_TOKEN"), "dev-agent-token",
|
||||
)
|
||||
|
||||
print("=" * 60)
|
||||
print("合同建单 CONTRACT_CREATE - 开始")
|
||||
print("jobId=%s sourceType=%s submitMode=%s" % (
|
||||
data.get("jobId"), data.get("sourceType"), data.get("submitMode")))
|
||||
print("=" * 60)
|
||||
|
||||
result = run_contract_create(
|
||||
data,
|
||||
state_center_url=state_center_url,
|
||||
agent_token=agent_token,
|
||||
)
|
||||
print(json.dumps(result, ensure_ascii=False))
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
ILog.begin_init()
|
||||
robot_no = ''
|
||||
proc_no = ''
|
||||
job_no = ''
|
||||
input_arg = ''
|
||||
params = {}
|
||||
fn = "Main"
|
||||
flow_params={'Main':[]}
|
||||
try:
|
||||
argv = sys.argv[1:]
|
||||
opts, args = getopt.getopt(argv,"hr:p:j:i:c:",["robot = ","proc = ","job = ","input = ","params = "])
|
||||
except getopt.GetoptError:
|
||||
print ('robot.py -r <robot> -p <proc> -j <job>')
|
||||
for opt, arg in opts:
|
||||
if opt == '-h':
|
||||
print ('robot.py -r <robot> -p <proc> -j <job>')
|
||||
elif opt in ("-r", "--robot"):
|
||||
robot_no = arg
|
||||
elif opt in ("-p", "--proc"):
|
||||
proc_no = arg
|
||||
elif opt in ("-j", "--job"):
|
||||
job_no = arg
|
||||
elif opt in ("-i", "--input"):
|
||||
input_arg = arg
|
||||
elif opt in ("-c", "--params"):
|
||||
cParam: dict = iinput.parseConnectorParam(arg)
|
||||
params = cParam.pop("Params", params)
|
||||
fn = cParam.pop("FlowName", fn)
|
||||
pro = AutoRpa(robot_no=robot_no,proc_no=proc_no,job_no=job_no,input_arg=input_arg)
|
||||
iplatform.Return(MainResult=getattr(pro, fn)(**params), job_no=job_no, fn=fn, flow_return_params=flow_params.get(fn,[]))
|
||||
Loading…
Reference in New Issue