5D3420315FA54219AB2427B0C674AD66auto

This commit is contained in:
34BFF76918A9449ABAB323149DDCEA73 2026-08-13 11:38:46 +08:00
parent 0a8d4e8c75
commit 7c9217d197
24 changed files with 658 additions and 367 deletions

View File

@ -5,7 +5,7 @@
"ProInfo": {
"CodeCustomCount": "0",
"ProAbsolutePath": "D:/RPA/AutoRpa",
"ProChange": "2026/08/13 10:38:45",
"ProChange": "2026/08/13 11:38:45",
"ProColorRef": "4",
"ProCreate": "2026/08/12 10:13:31",
"ProDesc": "",

View File

@ -1,6 +1,6 @@
# coding=utf-8
"""
构建 Main.seq - 合同建单流程(Chrome浏览器版本)
构建 Main.seq - 合同建单流程(输入驱动 + 状态中心回调版本)
"""
import json
import base64
@ -11,150 +11,67 @@ def b64_apa(val):
def b64_raw(val):
return base64.b64encode(str(val).encode()).decode()
s01_body = """\
import sys, os, json, time
# 单节点代码块:读取入参(input_arg / -c params / JSON 文件)→ 调用 run_contract_create
# run_contract_create 内部完成 start/complete/fail 回调、Selenium 起草与合同编号读取。
body = """\
import sys, os, json
from pathlib import Path
_CODES_DIR = Path(r'd:\\RPA\\AutoRpa\\codes')
if str(_CODES_DIR) not in sys.path:
sys.path.insert(0, str(_CODES_DIR))
from rpa_yisaiqi.contract_flow_steps import ContractFlowContext, ContractGoodsLine
print("=" * 60)
print("合同建单 CONTRACT_CREATE - 初始化")
print("=" * 60)
self._ctx = ContractFlowContext(
contract_url="http://contract-test.gzmpc.com:8080/",
username="test002", password="1",
supplier_name="测试供应商",
order_date="2026-08-12",
source_type="YYC", template_type="NON_FORMAT",
template_name="非格式--采购协议",
customer_code="CUST-001", customer_name="测试供应商",
contract_category="采购协议", settlement_type="付款",
contract_background="自动化测试合同起草", contract_nature="主合同",
amount_currency="人民币", amount="50000.00",
contract_end_date="2026-12-31",
is_major_contract="否", is_prepayment="否",
belong_year="2026", payment_method="账期支付",
payment_days="30", contract_status="正式合同",
contains_blood_products="否",
submit_mode="dry_run",
goods_lines=[
ContractGoodsLine(
operation_code="ADD", goods_code="YP-001",
goods_name="测试药品A", approval_no="国药准字H00000001",
spec="50mg*30片", unit="盒", quantity=10.0,
),
],
merged_pdf_path=None,
from rpa_yisaiqi.contract_create_node import (
ContractCreateError, load_input_data, run_contract_create,
)
print(f"[OK] 上下文就绪: source={self._ctx.source_type}")
print(f"[OK] 模板: {self._ctx.template_name}")
print(f"[OK] 货品行数: {len(self._ctx.goods_lines)}")
"""
def _first(*values):
for value in values:
if value is not None and str(value).strip():
return str(value).strip()
return ""
s02_body = """\
from rpa_yisaiqi.apa_browser_adapter import ApaBrowserAdapter
print("[S02] 启动 Chrome + 打开合同系统...")
print(f" URL: {self._ctx.contract_url}")
adapter = ApaBrowserAdapter(url=self._ctx.contract_url)
adapter.open_url(self._ctx.contract_url)
self._adapter = adapter
print("[OK] Chrome 已打开并导航到合同系统")
"""
s03_body = """\
from rpa_yisaiqi.contract_flow_steps import login_contract_system, enter_contract_create
adapter = self._adapter
print("[S03] 登录合同系统...")
login_contract_system(adapter, self._ctx)
print("[OK] 登录完成")
print("[S04] 进入合同起草...")
enter_contract_create(adapter)
print("[OK] 已进入合同起草页面")
"""
s04_body = """\
from rpa_yisaiqi.contract_flow_steps import select_contract_template, fill_contract_signatory_info
adapter = self._adapter
print("[S05] 选择合同模板...")
select_contract_template(adapter, self._ctx)
print(f"[OK] 模板: {self._ctx.template_name}")
print("[S06] 填写签约方信息...")
fill_contract_signatory_info(adapter, self._ctx)
print(f"[OK] 签约方: {self._ctx.customer_name}")
"""
s05_body = """\
from rpa_yisaiqi.contract_flow_steps import fill_contract_basic_info, fill_contract_amount_and_dates
adapter = self._adapter
print("[S07] 填写基础表单...")
fill_contract_basic_info(adapter, self._ctx)
print(f"[OK] 类别={self._ctx.contract_category}, 结算={self._ctx.settlement_type}")
print("[S08] 填写金额与日期...")
fill_contract_amount_and_dates(adapter, self._ctx)
print(f"[OK] 金额={self._ctx.amount}, 结束日期={self._ctx.contract_end_date}")
"""
s06_body = """\
from rpa_yisaiqi.contract_flow_steps import (
fill_contract_goods_lines, fill_contract_payment_info,
upload_contract_document_if_needed, submit_contract_for_approval,
read_contract_number
)
adapter = self._adapter
print("[S09] 填写货品明细...")
fill_contract_goods_lines(adapter, self._ctx)
print(f"[OK] 货品: {len(self._ctx.goods_lines)} 行")
print("[S10] 填写付款信息...")
fill_contract_payment_info(adapter, self._ctx)
print(f"[OK] 付款方式={self._ctx.payment_method}")
if self._ctx.source_type == "YYC":
print("[S11] 合并PDF [YYC专属]... (单PDF, 跳过合并)")
print("[S12] 上传PDF附件 [YYC专属]... (dry_run 跳过)")
print()
print("[S13] 提交审批...")
if not submit_contract_for_approval(adapter, self._ctx):
print("[DRY-RUN] 演练模式,跳过实际提交")
print("=" * 60)
print("合同建单流程演练完成")
print("=" * 60)
raw = (self.input_arg or "").strip()
if raw:
data = load_input_data(raw)
elif isinstance(params, dict) and params:
nested = params.get("inputJson") or params.get("inputArg") or params.get("input")
data = load_input_data(str(nested)) if nested else dict(params)
else:
print("[S14] 读取合同编号...")
contract_no = read_contract_number(adapter)
print(f"[OK] 合同编号: {contract_no}")
print("=" * 60)
print(f"[COMPLETED] 合同创建成功: {contract_no}")
print("=" * 60)
raise ContractCreateError("缺少业务入参:请通过 -i/--input 传入 JSON 字符串或文件路径,或通过 -c/--params 传入参数")
state_center_url = _first(
data.get("stateCenterUrl"), data.get("stateCenterURL"),
os.getenv("STATE_CENTER_URL"), params.get("stateCenterUrl"),
"http://127.0.0.1:8000",
)
agent_token = _first(
data.get("agentToken"), os.getenv("AGENT_TOKEN"),
params.get("agentToken"), "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))
"""
steps = [
("S01 初始化 & 上下文构建", s01_body, "设置环境变量并构建YYC测试上下文"),
("S02 启动Chrome", s02_body, "Selenium打开Chrome浏览器并导航到合同系统"),
("S03-S04 登录 & 进入起草", s03_body, "登录系统并进入合同起草页面"),
("S05-S06 模板选择 & 签约方", s04_body, "选择模板并填写签约方信息"),
("S07-S08 基础表单 & 金额日期", s05_body, "填写合同类别、金额、日期"),
("S09-S15 货品/付款/提交", s06_body, "填写货品明细、付款信息并提交"),
("读取入参并执行合同建单", body, "读取艺赛旗入参并执行 CONTRACT_CREATE(含状态中心回调)"),
]
nodes = []
lines = []
begin_tag = "20260812100000"
begin_tag = "20260813100000"
nodes.append({
"NodePanelData": {
"ExpandedGroups": [], "InputProperty": {"mouse_x": 0, "mouse_y": 0},
@ -174,12 +91,12 @@ start_x = 500
start_y = 200
space_y = 120
for i, (title, body, note) in enumerate(steps):
tag = f"20260812{200000 + i:06d}"
for i, (title, body_text, note) in enumerate(steps):
tag = f"20260813{200000 + i:06d}"
py = start_y + i * space_y
pimport_b64 = b64_raw("")
pbody_b64 = b64_raw(body)
pbody_b64 = b64_raw(body_text)
props = {
"EP_CONTROL": {
@ -218,9 +135,9 @@ for i, (title, body, note) in enumerate(steps):
"LineStart": {
"Pidx": f"{prev_tag}/EPin_Flow_Out",
"Px": str(start_x - 100) if i > 0 else "415",
"Py": str(start_y + 30) if i == 0 else str(start_y + (i-1) * space_y + 30)
"Py": str(start_y + 30) if i == 0 else str(start_y + (i - 1) * space_y + 30)
},
"LineTag": f"20260812{300000 + i:06d}"
"LineTag": f"20260813{300000 + i:06d}"
})
prev_tag = tag

View File

@ -41,123 +41,65 @@ class AutoRpa:
if self.input_arg is None:
sys.exit(0)
def Main(self):
#插入代码块#1
self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:20260812200000,Title:插入代码块#1,Note:设置环境变量并构建YYC测试上下文')
import sys, os, json, time
def _resolve_input_data(self, params):
"""优先 input_arg(JSON 字符串或文件路径),其次 -c 传入的 params 字典。"""
from rpa_yisaiqi.contract_create_node import ContractCreateError, load_input_data
raw = (self.input_arg or "").strip()
if raw:
return load_input_data(raw)
if isinstance(params, dict) and params:
nested = params.get("inputJson") or params.get("inputArg") or params.get("input")
if nested:
return load_input_data(str(nested))
return dict(params)
raise ContractCreateError("缺少业务入参:请通过 -i/--input 传入 JSON 字符串或文件路径,或通过 -c/--params 传入参数")
@staticmethod
def _first(*values):
for value in values:
if value is not None and str(value).strip():
return str(value).strip()
return ""
def Main(self, **params):
self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:20260813000000,Title:合同建单,Note:读取入参并执行 CONTRACT_CREATE')
import sys, os, json
from pathlib import Path
_CODES_DIR = Path(r'd:\RPA\AutoRpa\codes')
if str(_CODES_DIR) not in sys.path:
sys.path.insert(0, str(_CODES_DIR))
from rpa_yisaiqi.contract_flow_steps import ContractFlowContext, ContractGoodsLine
print("=" * 60)
print("合同建单 CONTRACT_CREATE - 初始化")
print("=" * 60)
self._ctx = ContractFlowContext(
contract_url="http://contract-test.gzmpc.com:8080/",
username="test002", password="1",
supplier_name="测试供应商",
order_date="2026-08-12",
source_type="YYC", template_type="NON_FORMAT",
template_name="非格式--采购协议",
customer_code="CUST-001", customer_name="测试供应商",
contract_category="采购协议", settlement_type="付款",
contract_background="自动化测试合同起草", contract_nature="主合同",
amount_currency="人民币", amount="50000.00",
contract_end_date="2026-12-31",
is_major_contract="否", is_prepayment="否",
belong_year="2026", payment_method="账期支付",
payment_days="30", contract_status="正式合同",
contains_blood_products="否",
submit_mode="dry_run",
goods_lines=[
ContractGoodsLine(
operation_code="ADD", goods_code="YP-001",
goods_name="测试药品A", approval_no="国药准字H00000001",
spec="50mg*30片", unit="盒", quantity=10.0,
),
],
merged_pdf_path=None,
from rpa_yisaiqi.contract_create_node import run_contract_create
data = self._resolve_input_data(params)
state_center_url = self._first(
data.get("stateCenterUrl"),
data.get("stateCenterURL"),
os.getenv("STATE_CENTER_URL"),
params.get("stateCenterUrl"),
"http://127.0.0.1:8000",
)
print(f"[OK] 上下文就绪: source={self._ctx.source_type}")
print(f"[OK] 模板: {self._ctx.template_name}")
print(f"[OK] 货品行数: {len(self._ctx.goods_lines)}")
#插入代码块#1
self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:20260812200001,Title:插入代码块#1,Note:Selenium打开Chrome浏览器并导航到合同系统')
from rpa_yisaiqi.apa_browser_adapter import ApaBrowserAdapter
print("[S02] 启动 Chrome + 打开合同系统...")
print(f" URL: {self._ctx.contract_url}")
adapter = ApaBrowserAdapter(url=self._ctx.contract_url)
adapter.open_url(self._ctx.contract_url)
self._adapter = adapter
print("[OK] Chrome 已打开并导航到合同系统")
#插入代码块#1
self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:20260812200002,Title:插入代码块#1,Note:登录系统并进入合同起草页面')
from rpa_yisaiqi.contract_flow_steps import login_contract_system, enter_contract_create
adapter = self._adapter
print("[S03] 登录合同系统...")
login_contract_system(adapter, self._ctx)
print("[OK] 登录完成")
print("[S04] 进入合同起草...")
enter_contract_create(adapter)
print("[OK] 已进入合同起草页面")
#插入代码块#1
self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:20260812200003,Title:插入代码块#1,Note:选择模板并填写签约方信息')
from rpa_yisaiqi.contract_flow_steps import select_contract_template, fill_contract_signatory_info
adapter = self._adapter
print("[S05] 选择合同模板...")
select_contract_template(adapter, self._ctx)
print(f"[OK] 模板: {self._ctx.template_name}")
print("[S06] 填写签约方信息...")
fill_contract_signatory_info(adapter, self._ctx)
print(f"[OK] 签约方: {self._ctx.customer_name}")
#插入代码块#1
self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:20260812200004,Title:插入代码块#1,Note:填写合同类别、金额、日期')
from rpa_yisaiqi.contract_flow_steps import fill_contract_basic_info, fill_contract_amount_and_dates
adapter = self._adapter
print("[S07] 填写基础表单...")
fill_contract_basic_info(adapter, self._ctx)
print(f"[OK] 类别={self._ctx.contract_category}, 结算={self._ctx.settlement_type}")
print("[S08] 填写金额与日期...")
fill_contract_amount_and_dates(adapter, self._ctx)
print(f"[OK] 金额={self._ctx.amount}, 结束日期={self._ctx.contract_end_date}")
#插入代码块#1
self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:20260812200005,Title:插入代码块#1,Note:填写货品明细、付款信息并提交')
from rpa_yisaiqi.contract_flow_steps import (
fill_contract_goods_lines, fill_contract_payment_info,
upload_contract_document_if_needed, submit_contract_for_approval,
read_contract_number
agent_token = self._first(
data.get("agentToken"),
os.getenv("AGENT_TOKEN"),
params.get("agentToken"),
"dev-agent-token",
)
adapter = self._adapter
print("[S09] 填写货品明细...")
fill_contract_goods_lines(adapter, self._ctx)
print(f"[OK] 货品: {len(self._ctx.goods_lines)} 行")
print("[S10] 填写付款信息...")
fill_contract_payment_info(adapter, self._ctx)
print(f"[OK] 付款方式={self._ctx.payment_method}")
if self._ctx.source_type == "YYC":
print("[S11] 合并PDF [YYC专属]... (单PDF, 跳过合并)")
print("[S12] 上传PDF附件 [YYC专属]... (dry_run 跳过)")
print()
print("[S13] 提交审批...")
if not submit_contract_for_approval(adapter, self._ctx):
print("[DRY-RUN] 演练模式,跳过实际提交")
print("=" * 60)
print("合同建单流程演练完成")
print("=" * 60)
else:
print("[S14] 读取合同编号...")
contract_no = read_contract_number(adapter)
print(f"[OK] 合同编号: {contract_no}")
print("=" * 60)
print(f"[COMPLETED] 合同创建成功: {contract_no}")
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()

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -71,10 +71,14 @@ class ApaBrowserAdapter:
return self._driver.find_element(By.XPATH, xpath)
except NoSuchElementException:
# Fallback: find the label's "for" attribute
xpath2 = f'//label[contains(text(),"{text_val}")]/@for'
for_id = self._driver.find_element(By.XPATH, xpath2).get_attribute("for") if False else None
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 属性
@ -87,9 +91,9 @@ class ApaBrowserAdapter:
text_val = selector[6:].strip('"').strip("'")
return self._driver.find_element(By.CSS_SELECTOR, f'[title="{text_val}"]')
# 5. :contains() → 转 XPath
# 5. :contains() → 转 XPath(支持 button:contains(文本) / a:contains("文本"))
if ":contains(" in selector:
m = re.match(r'(.+?):contains\("(.+?)"\)', selector)
m = re.match(r'(.+?):contains\(["\']?(.+?)["\']?\)', selector)
if m:
tag_part, text_val = m.groups()
xpath = f'//{tag_part}[contains(.,"{text_val}")]'
@ -164,6 +168,21 @@ class ApaBrowserAdapter:
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
@ -176,6 +195,30 @@ class ApaBrowserAdapter:
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 select_first(self, selectors: list, value: str) -> None:
self._ensure_open()
from selenium.webdriver.support.ui import Select
@ -183,14 +226,59 @@ class ApaBrowserAdapter:
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

View File

@ -1,52 +1,31 @@
"""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 节点编排:入参构建 + 状态中心回调 + Selenium 起草。"""
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 .errors import (
ContractCreateError,
LoginFailed,
PdfMergeUnsupported,
SupplierContractDisabled,
UnknownRpaError,
)
from .pdf_utils import ensure_merged_pdf
from .yisaiqi_adapter_template import YiSaiQiBrowserAdapter
NODE_CODE = "CONTRACT_CREATE"
CONTRACT_URL = "http://contract-test.gzmpc.com:8080/"
class ContractCreateError(RuntimeError):
error_code = "UI_ELEMENT_NOT_FOUND"
retryable = False
route_role = "OPS"
class LoginFailed(ContractCreateError):
error_code = "SYS_LOGIN_FAIL"
retryable = True
route_role = "OPS"
class SupplierContractDisabled(ContractCreateError):
error_code = "BIZ_CONTRACT_DISABLED"
retryable = False
route_role = "PURCHASER"
class PdfMergeUnsupported(ContractCreateError):
error_code = "CAP_PDF_MERGE_UNSUPPORTED"
retryable = False
route_role = "OPS"
@dataclass(frozen=True)
class ContractCreateInput:
job_id: str
@ -58,9 +37,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]
@ -76,42 +58,58 @@ def execute_contract_create(
runner = contract_runner or create_contract_in_contract_system
try:
contract_no = runner(spec)
complete_payload = build_complete_payload(spec, contract_no)
return client.complete_node(spec.job_id, NODE_CODE, complete_payload)
if spec.submit_mode == "dry_run":
return {"status": "dry_run", "submitted": False}
if not contract_no:
raise ContractCreateError("submit 模式未读取到真实合同编号,无法 complete")
return client.complete_node(spec.job_id, NODE_CODE, build_complete_payload(spec, contract_no))
except ContractCreateError as exc:
fail_payload = build_fail_payload(spec, exc)
return client.fail_node(spec.job_id, NODE_CODE, fail_payload)
return client.fail_node(spec.job_id, NODE_CODE, build_fail_payload(spec, exc))
except Exception as exc:
return client.fail_node(spec.job_id, NODE_CODE, build_fail_payload(spec, UnknownRpaError(str(exc))))
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,
@ -140,9 +138,32 @@ 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 _resolve_merged_pdf(draft: ContractDraft, spec: ContractCreateInput) -> str | None:
if draft.source_type != "YYC":
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_complete_payload(spec: ContractCreateInput, contract_no: str) -> dict[str, Any]:
return {
"attemptNo": spec.attempt_no,
@ -153,8 +174,10 @@ def build_complete_payload(spec: ContractCreateInput, contract_no: str) -> dict[
"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",
"orderNoList": list(spec.order_no_list),
"templateName": spec.template_name,
"pdfMerged": spec.source_type == "YYC",
"submitted": True,
},
"artifacts": [],
}
@ -171,20 +194,108 @@ def build_fail_payload(spec: ContractCreateInput, exc: ContractCreateError) -> d
}
def _validate_data(data: dict[str, Any], draft: ContractDraft) -> None:
missing = []
for key, label in (
("jobId", "jobId"),
("supplierName", "supplierName"),
("orderDate", "orderDate"),
("sourceType", "sourceType"),
("customerCode", "customerCode"),
):
if not _str(data.get(key)):
missing.append(label)
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()
@ -192,6 +303,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,
@ -202,15 +320,35 @@ 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
# 保持旧导入名可用(errors 模块为新的统一异常来源)
__all__ = [
"ContractCreateError",
"LoginFailed",
"SupplierContractDisabled",
"PdfMergeUnsupported",
"UnknownRpaError",
"ContractCreateInput",
"execute_contract_create",
"create_contract_in_contract_system",
"build_spec_from_data",
"build_flow_context",
"build_complete_payload",
"build_fail_payload",
"run_contract_create",
"load_input_data",
"NODE_CODE",
"CONTRACT_URL",
]
if __name__ == "__main__":
raise SystemExit(main())

View File

@ -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", [])],
)

View File

@ -5,18 +5,30 @@ 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 .errors import (
ContractCreateError,
LoginFailed,
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 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 +41,8 @@ class ContractGoodsLine:
spec: str
unit: str
quantity: float
mdm_code: str = ""
unit_price: str = ""
@dataclass(frozen=True)
@ -60,6 +74,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:
@ -117,14 +132,54 @@ F = {
"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)"]',
"goods_code": 'input[name="extendDataFormInfo.value(fd_detail1.0.fd_opcode)"]',
"goods_name": 'input[name="extendDataFormInfo.value(fd_detail1.0.fd_goodsname)"]',
"goods_mdm": 'input[name="extendDataFormInfo.value(fd_detail1.0.fd_mdmCode)"]',
"goods_approval": 'input[name="extendDataFormInfo.value(fd_detail1.0.fd_registdocno)"]',
"goods_spec": 'input[name="extendDataFormInfo.value(fd_detail1.0.fd_goodstype)"]',
"goods_unit": 'input[name="extendDataFormInfo.value(fd_detail1.0.fd_goodsunit)"]',
"goods_qty": 'input[name="extendDataFormInfo.value(fd_detail1.0.fd_goodsqty)"]',
"goods_price": 'input[name="extendDataFormInfo.value(fd_detail1.0.fd_unitprice)"]',
}
# 货品明细行的字段后缀;行索引从 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 = {
"search_input": [
'input[placeholder*="客商"]',
'input[placeholder*="编码"]',
'input[placeholder*="搜索"]',
'input[name*="search"]',
'input[class*="search"]',
],
"search_button": [
"text=搜索",
"text=查询",
"button:contains(搜索)",
"a:contains(搜索)",
],
"result_rows": [
"//table//tr[contains(.,'{code}')]",
"//*[contains(@class,'list')]//*[contains(text(),'{code}')]",
"text={code}",
],
"confirm_button": [
"text=确定",
"text=确认",
"button:contains(确定)",
"button:contains(确认)",
],
}
@ -133,10 +188,14 @@ def open_contract_system(browser: BrowserAdapter, ctx: ContractFlowContext) -> N
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(['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:
@ -153,19 +212,63 @@ def select_contract_template(browser: BrowserAdapter, ctx: ContractFlowContext)
def fill_contract_signatory_info(browser: BrowserAdapter, ctx: ContractFlowContext) -> None:
customer_name = ctx.customer_name or ctx.supplier_name
if ctx.customer_code:
expected_name = (ctx.customer_name or ctx.supplier_name).strip()
if not ctx.customer_code:
raise SupplierContractDisabled("客商操作码(customerCode)为空,无法加载客商")
# 双击客户名称字段打开客商选择窗口,按编码搜索并带回客商
browser.double_click_first([F["customer_name"]])
_pick_customer_from_dialog(browser, ctx.customer_code)
loaded_name = _wait_customer_name(browser, timeout=8)
# 兜底:双击未弹出选择窗时,直接填客商操作码触发自动加载
if not loaded_name:
browser.fill_first([F["customer_code"]], ctx.customer_code)
if customer_name:
browser.fill_first([F["customer_name"]], customer_name)
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 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:
browser.wait_for_element(CUSTOMER_PICKER["search_input"], timeout_seconds=10)
browser.fill_first(CUSTOMER_PICKER["search_input"], code)
browser.click_first(CUSTOMER_PICKER["search_button"])
time.sleep(1)
rows = [sel.format(code=code) for sel in CUSTOMER_PICKER["result_rows"]]
browser.click_first(rows)
browser.click_first(CUSTOMER_PICKER["confirm_button"])
def fill_contract_basic_info(browser: BrowserAdapter, ctx: ContractFlowContext) -> None:
if ctx.contract_category:
browser.fill_first([F["contract_category"]], ctx.contract_category)
if ctx.settlement_type:
browser.select_first([F["settlement_type"]], ctx.settlement_type)
_select_settlement_type(browser, ctx.settlement_type)
if ctx.contract_background:
browser.fill_first([F["contract_background"]], ctx.contract_background)
# 合同性质默认主合同(页面已勾选),仅在需要从合同时切换
@ -174,6 +277,18 @@ def fill_contract_basic_info(browser: BrowserAdapter, ctx: ContractFlowContext)
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 ContractCreateError(f"结算方式下拉未选中,仍停留在「==请选择==」,期望「{value}」")
def fill_contract_amount_and_dates(browser: BrowserAdapter, ctx: ContractFlowContext) -> None:
if ctx.amount:
browser.fill_first([F["amount"]], ctx.amount)
@ -185,21 +300,33 @@ def fill_contract_amount_and_dates(browser: BrowserAdapter, ctx: ContractFlowCon
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=新增行"])
browser.fill_first([F["goods_code"]], line.goods_code)
browser.fill_first([F["goods_name"]], line.goods_name)
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([F["goods_approval"]], line.approval_no)
browser.fill_first([_goods_selector("goods_approval", row)], line.approval_no)
if line.spec:
browser.fill_first([F["goods_spec"]], line.spec)
browser.fill_first([_goods_selector("goods_spec", row)], line.spec)
if line.unit:
browser.fill_first([F["goods_unit"]], line.unit)
browser.fill_first([F["goods_qty"]], str(line.quantity))
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.fill_first([F["is_major_contract"]], ctx.is_major_contract)
browser.fill_first([F["is_prepayment"]], ctx.is_prepayment)
@ -218,9 +345,14 @@ def fill_contract_payment_info(browser: BrowserAdapter, ctx: ContractFlowContext
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")
@ -242,5 +374,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 "格式合同"

View File

@ -0,0 +1,42 @@
"""CONTRACT_CREATE 领域异常及状态中心错误码映射。"""
from __future__ import annotations
class ContractCreateError(RuntimeError):
"""页面元素找不到 / 页面改版(含入参缺失,均不可自动重试)。"""
error_code = "UI_ELEMENT_NOT_FOUND"
retryable = False
route_role = "OPS"
class LoginFailed(ContractCreateError):
"""合同系统登录失败。"""
error_code = "SYS_LOGIN_FAIL"
retryable = True
route_role = "OPS"
class SupplierContractDisabled(ContractCreateError):
"""供应商/客商被禁或失效(含客商加载失败)。"""
error_code = "BIZ_CONTRACT_DISABLED"
retryable = False
route_role = "PURCHASER"
class PdfMergeUnsupported(ContractCreateError):
"""PDF 合并失败或不受支持。"""
error_code = "CAP_PDF_MERGE_UNSUPPORTED"
retryable = False
route_role = "OPS"
class UnknownRpaError(ContractCreateError):
"""未知异常。"""
error_code = "RPA_UNKNOWN_ERROR"
retryable = True
route_role = "OPS"

View File

@ -1,11 +1,11 @@
{
"supplierName": "重庆亿昊药业",
"supplierName": "重庆亿昊药业有限公司",
"orderDate": "2026-08-11",
"sourceType": "YYC",
"templateType": "NON_FORMAT",
"templateName": "非格式--采购协议",
"customerCode": "CUST-YYC-001",
"customerName": "重庆亿昊药业",
"customerCode": "DW0100046361",
"customerName": "重庆亿昊药业有限公司",
"contractCategory": "采购协议",
"settlementType": "付款",
"contractBackground": "按医药城订单自动起草采购协议",

View File

@ -1 +1,4 @@
pypdf==5.1.0
selenium
webdriver-manager
requests

View File

@ -20,10 +20,26 @@ class YiSaiQiBrowserAdapter:
"""Flowchart node: 按候选定位器依次查找,找到后点击."""
raise NotImplementedError(f"Map to YiSaiQi click activity: {selectors=}")
def double_click_first(self, selectors: list[str]) -> None:
"""Flowchart node: 双击元素(客商选择窗口等)."""
raise NotImplementedError(f"Map to YiSaiQi double-click activity: {selectors=}")
def wait_for_text(self, text: str, timeout_seconds: int = 15) -> None:
"""Flowchart node: 等待页面出现指定文本."""
raise NotImplementedError(f"Map to YiSaiQi wait-text activity: {text=} {timeout_seconds=}")
def wait_for_element(self, selectors: list[str], timeout_seconds: int = 15) -> None:
"""Flowchart node: 等待元素出现."""
raise NotImplementedError(f"Map to YiSaiQi wait-element activity: {selectors=}")
def element_exists(self, selectors: list[str]) -> bool:
"""Flowchart node: 判断元素是否存在."""
raise NotImplementedError(f"Map to YiSaiQi element-exists activity: {selectors=}")
def read_body_text(self) -> str:
"""Flowchart node: 读取页面 body 文本."""
raise NotImplementedError("Map to YiSaiQi read-body-text activity")
def select_first(self, selectors: list[str], value: str) -> None:
"""Flowchart node: 下拉框选择."""
raise NotImplementedError(f"Map to YiSaiQi select activity: {selectors=} {value=}")