Merge branch 'feat/contract-create-ui'
YiSaiQi CONTRACT_CREATE studio project, Selenium flow, and canvas artifacts.
This commit is contained in:
commit
63eec40334
|
|
@ -21,6 +21,12 @@ state_center_web/dist/
|
|||
|
||||
# Local runtime / agent
|
||||
.runtime/
|
||||
rpa_yisaiqi/screenshots/
|
||||
rpa-chrome-profile/
|
||||
**/chrome-profile/
|
||||
.tmp/
|
||||
.grok/
|
||||
.grokignore
|
||||
.claude/
|
||||
.superpowers/
|
||||
|
||||
|
|
|
|||
|
|
@ -57,10 +57,20 @@ python -m rpa_yisaiqi.contract_create_node `
|
|||
|
||||
## 真实合同起草运行
|
||||
|
||||
先准备合同起草输入 JSON,参考:
|
||||
客商必须用合同测试系统里**真实存在**的操作码。`CUST-YYC-001` 这类占位符会在选客商时搜不到,入参校验会直接拒绝。
|
||||
|
||||
- `examples/contract_draft_yyc.json`
|
||||
- `examples/contract_draft_xyy.json`
|
||||
测试环境当前可用:
|
||||
|
||||
| 字段 | 值 |
|
||||
| --- | --- |
|
||||
| customerCode | `DW0100046361` |
|
||||
| customerName | `重庆亿昊药业有限公司` |
|
||||
|
||||
联调入参用:
|
||||
|
||||
- `examples/contract_create_input_dry_run.json`:真客商 + `dry_run`,先校准页面
|
||||
- `examples/contract_draft_yyc.json`:医药城,同上客商
|
||||
- `examples/contract_draft_xyy.json`:小药药分支(不上传 PDF),客商仍用合同系统里能搜到的同一条
|
||||
|
||||
YYC 有多份平台 PDF 时需要合并,先安装:
|
||||
|
||||
|
|
@ -75,7 +85,7 @@ python -m rpa_yisaiqi.contract_create_node `
|
|||
--state-center-url http://192.168.x.x:8000 `
|
||||
--token dev-agent-token `
|
||||
--job-id <状态中心job_id> `
|
||||
--supplier-name 重庆亿昊药业 `
|
||||
--supplier-name 重庆亿昊药业有限公司 `
|
||||
--order-date 2026-08-11 `
|
||||
--source-type YYC `
|
||||
--draft-path rpa_yisaiqi/examples/contract_draft_yyc.json `
|
||||
|
|
@ -111,6 +121,27 @@ python -m rpa_yisaiqi.contract_create_node `
|
|||
--state-center-url http://192.168.x.x:8000
|
||||
```
|
||||
|
||||
## 艺赛旗画布怎么看
|
||||
|
||||
Studio 里「插入代码块」标题改不了,会一直显示「插入代码块#1」。步骤名写在节点**备注**里,画布上出现在代码块下方,例如 `S06 填写签约方`。
|
||||
|
||||
流程按四段换行,不要再排成一条长龙:
|
||||
|
||||
```
|
||||
准备 开始 → S01 初始化 → S02 打开系统 → S03 登录
|
||||
进入起草 S04 进入起草 → S05 选择模板
|
||||
填写表单 S06 签约方 → S07 基础信息 → S08 金额日期 → S09 货品 → S10 付款
|
||||
提交回写 S11 上传PDF → S12 提交 → S13 读合同号 → S14 回调
|
||||
```
|
||||
|
||||
改步骤后先关掉 Studio,再生成画布:
|
||||
|
||||
```powershell
|
||||
python rpa_yisaiqi/build_multinode_seq.py
|
||||
```
|
||||
|
||||
然后重新打开 `rpa_yisaiqi/studio` 项目并编译。
|
||||
|
||||
## 接入真实页面自动化
|
||||
|
||||
真实艺赛旗页面操作按流程图拆在:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
# coding=utf-8
|
||||
"""多节点流程图(Main.seq)的跨节点共享状态容器。
|
||||
|
||||
艺赛旗编译 Main.seq 后,各代码节点的 PBCode 会在同一 Python 进程中执行。
|
||||
本模块以「模块级全局变量」承载 browser / ctx / client / spec 等对象,
|
||||
避免依赖艺赛旗对局部变量的拼接方式,让每个节点都能可靠地取到上一步的产物。
|
||||
"""
|
||||
|
||||
spec = None
|
||||
client = None
|
||||
ctx = None
|
||||
browser = None
|
||||
submitted = False
|
||||
contract_no = ""
|
||||
fd_id = ""
|
||||
is_dry_run = False
|
||||
completed = False
|
||||
|
|
@ -0,0 +1,505 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
APA (艺赛旗) Selenium Chrome BrowserAdapter 实现
|
||||
通过 Selenium WebDriver 直接操控 Chrome,不依赖 APA 浏览器模块
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def _is_inside_repo(path: Path, repo_root: Path) -> bool:
|
||||
try:
|
||||
path.resolve().relative_to(repo_root.resolve())
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def resolve_chrome_profile_base(requested: str | None = None, *, repo_root: Path | None = None) -> Path:
|
||||
"""Chrome user-data dir must stay outside the repo so list_dir never walks 3GB."""
|
||||
root = (repo_root or REPO_ROOT).resolve()
|
||||
outside = (root.parent / "rpa-chrome-profile").resolve()
|
||||
raw = os.getenv("RPA_CHROME_PROFILE") if requested is None else requested
|
||||
if raw:
|
||||
candidate = Path(raw).expanduser().resolve()
|
||||
if _is_inside_repo(candidate, root):
|
||||
print(f"RPA_CHROME_PROFILE 指向仓库内 {candidate},已改用 {outside}", flush=True)
|
||||
return outside
|
||||
return candidate
|
||||
return outside
|
||||
|
||||
|
||||
class ApaBrowserAdapter:
|
||||
"""Selenium Chrome 浏览器适配器,实现 BrowserAdapter 协议"""
|
||||
|
||||
def __init__(self, url: str = "", *, flow: str = "default"):
|
||||
self._url = url
|
||||
self._flow = flow
|
||||
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
|
||||
|
||||
profile_base = resolve_chrome_profile_base()
|
||||
if _is_inside_repo(profile_base, REPO_ROOT):
|
||||
raise RuntimeError(f"拒绝在仓库内创建 Chrome 用户目录: {profile_base}")
|
||||
# 每次进程单独目录:上次 detach 留下的 Chrome 会锁住同一 profile
|
||||
profile_dir = profile_base / str(os.getpid())
|
||||
os.makedirs(profile_dir, exist_ok=True)
|
||||
profile_dir = str(profile_dir)
|
||||
|
||||
options = Options()
|
||||
options.page_load_strategy = "eager"
|
||||
# 不自动关闭浏览器,方便调试
|
||||
options.add_experimental_option("detach", True)
|
||||
# 避免被检测为自动化
|
||||
options.add_experimental_option("excludeSwitches", ["enable-automation", "enable-logging"])
|
||||
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")
|
||||
options.add_argument("--disable-gpu")
|
||||
options.add_argument("--disable-extensions")
|
||||
options.add_argument("--disable-popup-blocking")
|
||||
options.add_argument("--start-maximized")
|
||||
# 独立目录,避免和日常 Chrome 抢用户数据目录导致卡住
|
||||
options.add_argument("--user-data-dir=" + profile_dir)
|
||||
|
||||
chromedriver = os.getenv("CHROMEDRIVER_PATH")
|
||||
if chromedriver and os.path.exists(chromedriver):
|
||||
service = Service(chromedriver)
|
||||
else:
|
||||
from webdriver_manager.chrome import ChromeDriverManager
|
||||
service = Service(ChromeDriverManager().install())
|
||||
self._driver = webdriver.Chrome(service=service, options=options)
|
||||
self._driver.set_page_load_timeout(45)
|
||||
self._driver.set_script_timeout(30)
|
||||
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
|
||||
driver = self._get_driver()
|
||||
try:
|
||||
driver.get(url)
|
||||
except Exception as exc:
|
||||
# eager 仍可能因长轮询资源超时;页面文档已出来就继续
|
||||
current = ""
|
||||
try:
|
||||
current = driver.current_url or ""
|
||||
except Exception:
|
||||
current = ""
|
||||
if not current or current in ("data:,", "about:blank"):
|
||||
raise RuntimeError(f"打开页面失败:{url} ({exc})") from exc
|
||||
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_native(self, selectors: list) -> None:
|
||||
"""真实鼠标点击,给只监听 mousedown 的 LUI 页签/工具栏用。"""
|
||||
self._ensure_open()
|
||||
from selenium.webdriver.common.action_chains import ActionChains
|
||||
last_error = None
|
||||
for sel in selectors:
|
||||
try:
|
||||
el = self._find(sel)
|
||||
self._driver.execute_script(
|
||||
"arguments[0].scrollIntoView({block:'center', inline:'center'});",
|
||||
el,
|
||||
)
|
||||
time.sleep(0.2)
|
||||
ActionChains(self._driver).move_to_element(el).pause(0.15).click().perform()
|
||||
time.sleep(0.4)
|
||||
return
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
continue
|
||||
raise RuntimeError(f"click_native 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。优先可见弹窗,避免切到上次留下的隐藏 iframe。"""
|
||||
self._ensure_open()
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
last_error = None
|
||||
found = []
|
||||
for sel in selectors:
|
||||
try:
|
||||
if sel.startswith("//") or sel.startswith("/"):
|
||||
found.extend(self._driver.find_elements(By.XPATH, sel))
|
||||
else:
|
||||
found.extend(self._driver.find_elements(By.CSS_SELECTOR, sel))
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
visible = []
|
||||
hidden = []
|
||||
for el in found:
|
||||
try:
|
||||
(visible if el.is_displayed() else hidden).append(el)
|
||||
except Exception:
|
||||
hidden.append(el)
|
||||
for el in visible + hidden:
|
||||
try:
|
||||
self._driver.switch_to.default_content()
|
||||
self._driver.switch_to.frame(el)
|
||||
return
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
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 execute_script(self, script: str, *args):
|
||||
self._ensure_open()
|
||||
return self._driver.execute_script(script, *args)
|
||||
|
||||
def select_xform(self, name: str, value: str) -> None:
|
||||
"""选中 Landray xform fancy-select(.fs-wrap / .fs-option)。"""
|
||||
self._ensure_open()
|
||||
result = self._driver.execute_script(
|
||||
"""
|
||||
const name = arguments[0];
|
||||
const wanted = String(arguments[1] || '').trim();
|
||||
const flag = document.querySelector('xformflag[property="' + name + '"]');
|
||||
if (!flag) {
|
||||
return {ok: false, error: 'flag not found: ' + name};
|
||||
}
|
||||
const norm = (s) => String(s || '').replace(/\\s+/g, '').trim();
|
||||
const wantedN = norm(wanted);
|
||||
const opts = Array.from(flag.querySelectorAll('.fs-option'));
|
||||
let target = opts.find((o) => {
|
||||
const text = norm((o.querySelector('.fs-option-label') || o).textContent);
|
||||
const val = norm(o.getAttribute('data-value'));
|
||||
return text === wantedN || val === wantedN;
|
||||
});
|
||||
if (!target && wantedN.length >= 2) {
|
||||
target = opts.find((o) => {
|
||||
const text = norm((o.querySelector('.fs-option-label') || o).textContent);
|
||||
return text.indexOf(wantedN) >= 0;
|
||||
});
|
||||
}
|
||||
if (!target) {
|
||||
return {
|
||||
ok: false,
|
||||
error: 'option not found: ' + wanted,
|
||||
available: opts.map((o) => ((o.querySelector('.fs-option-label') || o).textContent || '').trim()),
|
||||
};
|
||||
}
|
||||
const labelWrap = flag.querySelector('.fs-label-wrap');
|
||||
if (labelWrap) { labelWrap.click(); }
|
||||
target.click();
|
||||
const input = flag.querySelector('input[name="' + name + '"]');
|
||||
const selected = target.getAttribute('data-value') || '';
|
||||
if (input) {
|
||||
const proto = window.HTMLInputElement.prototype;
|
||||
const setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
|
||||
setter.call(input, selected);
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
if (typeof __xformDispatch === 'function') {
|
||||
try { __xformDispatch(selected, input); } catch (e) {}
|
||||
}
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
value: selected,
|
||||
label: ((flag.querySelector('.fs-label') || {}).textContent || '').trim(),
|
||||
};
|
||||
""",
|
||||
name,
|
||||
str(value),
|
||||
)
|
||||
if not result or not result.get("ok"):
|
||||
raise RuntimeError(f"select_xform failed: {name}={value} {result}")
|
||||
time.sleep(0.3)
|
||||
|
||||
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:
|
||||
from .runtime.accept import artifact_dir
|
||||
|
||||
save_dir = artifact_dir(self._flow)
|
||||
save_dir.mkdir(parents=True, exist_ok=True)
|
||||
path = save_dir / f"{name}.png"
|
||||
if self._driver:
|
||||
self._driver.save_screenshot(str(path))
|
||||
return str(path)
|
||||
|
|
@ -0,0 +1,358 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
构建合同建单 Main.seq —— 按业务段换行,画布上能直接读出步骤。
|
||||
|
||||
艺赛旗「插入代码块」的标题固定为「插入代码块#1」,「业务流程描述」节点
|
||||
打开后再保存标题会被清空。画布上真正能看见的是代码块的备注(NOTE)。
|
||||
因此本脚本:
|
||||
1. 不再插入空的业务流程描述节点
|
||||
2. 把「S06 填写签约方」写进 NOTE,作为画布副标题
|
||||
3. 按 准备 / 进入起草 / 填写表单 / 提交回写 四段换行
|
||||
|
||||
关闭 Studio 后运行本脚本,再重新打开项目编译。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
OUT = HERE / "studio" / "Main.seq"
|
||||
|
||||
COL_W = 280
|
||||
ROW_H = 200
|
||||
ORIGIN_X = 360
|
||||
ORIGIN_Y = 220
|
||||
BEGIN_TAG = "202608141000000001"
|
||||
|
||||
|
||||
def b64_apa(val):
|
||||
# 艺赛旗 EP_CONTROL / EP_Gen 字段值末尾带换行,缺失会导致字段解析失败
|
||||
return json.dumps({"val": base64.b64encode(str(val).encode()).decode()}, indent=4) + "\n"
|
||||
|
||||
|
||||
def b64_raw(val):
|
||||
return base64.b64encode(str(val).encode()).decode()
|
||||
|
||||
|
||||
STEP_INIT = '''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))
|
||||
|
||||
import rpa_yisaiqi._flow_state as _st
|
||||
from rpa_yisaiqi.contract_create_node import ContractCreateError, load_input_data, build_spec_from_data
|
||||
from rpa_yisaiqi.callback_client import StateCenterClient
|
||||
from rpa_yisaiqi.apa_browser_adapter import ApaBrowserAdapter
|
||||
|
||||
|
||||
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:
|
||||
raw = os.getenv("CONTRACT_INPUT_JSON", "").strip()
|
||||
if not raw:
|
||||
raw = str(_REPO_ROOT / "rpa_yisaiqi" / "examples" / "contract_create_input_dry_run.json")
|
||||
print("未传入 -i/--input,使用默认联调入参:", raw)
|
||||
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://192.168.105.69:8000",
|
||||
)
|
||||
agent_token = _first(data.get("agentToken"), os.getenv("AGENT_TOKEN"), "dev-agent-token")
|
||||
|
||||
_st.spec = build_spec_from_data(data)
|
||||
_st.client = StateCenterClient(state_center_url, agent_token)
|
||||
_st.ctx = _st.spec.flow_context
|
||||
_st.browser = ApaBrowserAdapter(url=_st.ctx.contract_url, flow="CONTRACT_CREATE")
|
||||
_st.is_dry_run = (_st.spec.submit_mode == "dry_run")
|
||||
|
||||
if not _st.is_dry_run:
|
||||
print("回调状态中心 start", _st.spec.job_id)
|
||||
_st.client.start_node(_st.spec.job_id, "CONTRACT_CREATE")'''
|
||||
|
||||
STEP_OPEN = '''import rpa_yisaiqi._flow_state as _st
|
||||
from rpa_yisaiqi.contract_flow_steps import open_contract_system
|
||||
|
||||
open_contract_system(_st.browser, _st.ctx)'''
|
||||
|
||||
STEP_LOGIN = '''import rpa_yisaiqi._flow_state as _st
|
||||
from rpa_yisaiqi.contract_flow_steps import login_contract_system
|
||||
|
||||
login_contract_system(_st.browser, _st.ctx)'''
|
||||
|
||||
STEP_ENTER = '''import rpa_yisaiqi._flow_state as _st
|
||||
from rpa_yisaiqi.contract_flow_steps import enter_contract_create
|
||||
|
||||
enter_contract_create(_st.browser)'''
|
||||
|
||||
STEP_TEMPLATE = '''import rpa_yisaiqi._flow_state as _st
|
||||
from rpa_yisaiqi.contract_flow_steps import select_contract_template
|
||||
|
||||
select_contract_template(_st.browser, _st.ctx)'''
|
||||
|
||||
STEP_SIGNATORY = '''import rpa_yisaiqi._flow_state as _st
|
||||
from rpa_yisaiqi.contract_flow_steps import fill_contract_signatory_info
|
||||
|
||||
fill_contract_signatory_info(_st.browser, _st.ctx)'''
|
||||
|
||||
STEP_BASIC = '''import rpa_yisaiqi._flow_state as _st
|
||||
from rpa_yisaiqi.contract_flow_steps import fill_contract_basic_info
|
||||
|
||||
fill_contract_basic_info(_st.browser, _st.ctx)'''
|
||||
|
||||
STEP_AMOUNT = '''import rpa_yisaiqi._flow_state as _st
|
||||
from rpa_yisaiqi.contract_flow_steps import fill_contract_amount_and_dates
|
||||
|
||||
fill_contract_amount_and_dates(_st.browser, _st.ctx)'''
|
||||
|
||||
STEP_GOODS = '''import rpa_yisaiqi._flow_state as _st
|
||||
from rpa_yisaiqi.contract_flow_steps import fill_contract_goods_lines
|
||||
|
||||
fill_contract_goods_lines(_st.browser, _st.ctx)'''
|
||||
|
||||
STEP_PAYMENT = '''import rpa_yisaiqi._flow_state as _st
|
||||
from rpa_yisaiqi.contract_flow_steps import fill_contract_payment_info
|
||||
|
||||
fill_contract_payment_info(_st.browser, _st.ctx)'''
|
||||
|
||||
STEP_UPLOAD = '''import rpa_yisaiqi._flow_state as _st
|
||||
from rpa_yisaiqi.contract_flow_steps import upload_contract_document_if_needed
|
||||
|
||||
upload_contract_document_if_needed(_st.browser, _st.ctx)'''
|
||||
|
||||
STEP_SUBMIT = '''import rpa_yisaiqi._flow_state as _st
|
||||
from rpa_yisaiqi.contract_flow_steps import submit_contract_for_approval
|
||||
|
||||
_st.submitted = submit_contract_for_approval(_st.browser, _st.ctx)'''
|
||||
|
||||
STEP_READ_NO = '''import rpa_yisaiqi._flow_state as _st
|
||||
from rpa_yisaiqi.contract_flow_steps import read_contract_number
|
||||
|
||||
if _st.submitted:
|
||||
_st.contract_no = read_contract_number(_st.browser, _st.ctx)'''
|
||||
|
||||
STEP_COMPLETE = '''import json
|
||||
import rpa_yisaiqi._flow_state as _st
|
||||
|
||||
if not _st.is_dry_run and _st.contract_no:
|
||||
print("回调状态中心 complete", _st.spec.job_id, _st.contract_no)
|
||||
_st.client.complete_node(_st.spec.job_id, "CONTRACT_CREATE", {
|
||||
"businessRefs": {"contractSystemNo": _st.contract_no},
|
||||
})
|
||||
print(json.dumps({"status": "done", "contractSystemNo": _st.contract_no}, ensure_ascii=False))'''
|
||||
|
||||
|
||||
# 每段一行。NOTE 会显示在画布「插入代码块#1」下方,务必写短步骤名。
|
||||
PHASES = [
|
||||
(
|
||||
"准备",
|
||||
[
|
||||
("S01 初始化并回调 start", STEP_INIT),
|
||||
("S02 打开合同系统", STEP_OPEN),
|
||||
("S03 登录合同系统", STEP_LOGIN),
|
||||
],
|
||||
),
|
||||
(
|
||||
"进入起草",
|
||||
[
|
||||
("S04 进入合同起草", STEP_ENTER),
|
||||
("S05 选择模板", STEP_TEMPLATE),
|
||||
],
|
||||
),
|
||||
(
|
||||
"填写表单",
|
||||
[
|
||||
("S06 填写签约方", STEP_SIGNATORY),
|
||||
("S07 填写基础信息", STEP_BASIC),
|
||||
("S08 填写金额日期", STEP_AMOUNT),
|
||||
("S09 填写货品明细", STEP_GOODS),
|
||||
("S10 填写付款信息", STEP_PAYMENT),
|
||||
],
|
||||
),
|
||||
(
|
||||
"提交回写",
|
||||
[
|
||||
("S11 上传合同PDF", STEP_UPLOAD),
|
||||
("S12 提交送审", STEP_SUBMIT),
|
||||
("S13 读取合同编号", STEP_READ_NO),
|
||||
("S14 回调 complete", STEP_COMPLETE),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def code_tag(i: int) -> str:
|
||||
return f"202608143{i:02d}0000000"
|
||||
|
||||
|
||||
def line_tag(i: int) -> str:
|
||||
return f"202608149{i:02d}0000000"
|
||||
|
||||
|
||||
def _iter_steps():
|
||||
index = 1
|
||||
for phase_name, items in PHASES:
|
||||
for title, code in items:
|
||||
yield index, phase_name, title, code
|
||||
index += 1
|
||||
|
||||
|
||||
def build_begin_node(px: int, py: int) -> dict:
|
||||
return {
|
||||
"NodePanelData": {
|
||||
"ExpandedGroups": [],
|
||||
"InputProperty": {"mouse_x": 0, "mouse_y": 0},
|
||||
"PubProperty": "",
|
||||
"RetPins": [],
|
||||
},
|
||||
"NodeProperty": "",
|
||||
"NodeUI": {
|
||||
"NodeElementID": "",
|
||||
"NodeModel": "2",
|
||||
"NodePropErr": False,
|
||||
"NodePx": str(px),
|
||||
"NodePy": str(py),
|
||||
"NodeSubType": "EnumWidget_Begin",
|
||||
"NodeTag": BEGIN_TAG,
|
||||
"NodeTitle": "开始",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def build_code_node(tag: str, title: str, code: str, px: int, py: int) -> dict:
|
||||
body = f'print("[{title}]")\n' + code
|
||||
return {
|
||||
"NodePanelData": {
|
||||
"ExpandedGroups": [],
|
||||
"InputProperty": {"mouse_x": 0, "mouse_y": 0},
|
||||
"PubProperty": "",
|
||||
"RetPins": [],
|
||||
},
|
||||
"NodeProperty": {
|
||||
"EP_CONTROL": {
|
||||
"EP_CONTROL_IMPORT": json.dumps(
|
||||
{"PImport": "", "PBCode": b64_raw(body)}, indent=4
|
||||
) + "\n"
|
||||
},
|
||||
"EP_Gen": {
|
||||
"EP_Gen_AFTERDELAY": b64_apa("0"),
|
||||
"EP_Gen_BEFOREDELAY": b64_apa("0"),
|
||||
"EP_Gen_NOTE": b64_apa(title),
|
||||
},
|
||||
},
|
||||
"NodeUI": {
|
||||
"NodeElementID": "",
|
||||
"NodeModel": "2",
|
||||
"NodePropErr": False,
|
||||
"NodePx": str(px),
|
||||
"NodePy": str(py),
|
||||
"NodeSubType": "EnumWidget_Code_Custom",
|
||||
"NodeTag": tag,
|
||||
"NodeTitle": title,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def build_nodes():
|
||||
nodes = []
|
||||
placed = [] # (tag, x, y)
|
||||
|
||||
begin_x, begin_y = ORIGIN_X, ORIGIN_Y
|
||||
nodes.append(build_begin_node(begin_x, begin_y))
|
||||
placed.append((BEGIN_TAG, begin_x, begin_y))
|
||||
|
||||
# 第一段从「开始」右侧接着排;后续每段单独一行
|
||||
col = 1
|
||||
row = 0
|
||||
current_phase = None
|
||||
for index, phase_name, title, code in _iter_steps():
|
||||
if current_phase is None:
|
||||
current_phase = phase_name
|
||||
elif phase_name != current_phase:
|
||||
current_phase = phase_name
|
||||
row += 1
|
||||
col = 0
|
||||
x = ORIGIN_X + col * COL_W
|
||||
y = ORIGIN_Y + row * ROW_H
|
||||
tag = code_tag(index)
|
||||
nodes.append(build_code_node(tag, title, code, x, y))
|
||||
placed.append((tag, x, y))
|
||||
col += 1
|
||||
|
||||
return nodes, placed
|
||||
|
||||
|
||||
def build_lines(placed: list) -> list:
|
||||
lines = []
|
||||
for k in range(len(placed) - 1):
|
||||
prev_tag, prev_x, prev_y = placed[k]
|
||||
next_tag, next_x, next_y = placed[k + 1]
|
||||
same_row = prev_y == next_y
|
||||
if same_row:
|
||||
start_x, start_y = prev_x + 150, prev_y + 40
|
||||
end_x, end_y = next_x - 4, next_y + 40
|
||||
else:
|
||||
start_x, start_y = prev_x + 70, prev_y + 90
|
||||
end_x, end_y = next_x + 70, next_y - 8
|
||||
lines.append({
|
||||
"LineColor": "255, 255, 255",
|
||||
"LineCommented": 0,
|
||||
"LineEnd": {
|
||||
"Pidx": f"{next_tag}/EPin_Flow_In",
|
||||
"Px": str(end_x),
|
||||
"Py": str(end_y),
|
||||
},
|
||||
"LineNote": "",
|
||||
"LineSplit": {"Px": "", "Py": ""},
|
||||
"LineStart": {
|
||||
"Pidx": f"{prev_tag}/EPin_Flow_Out",
|
||||
"Px": str(start_x),
|
||||
"Py": str(start_y),
|
||||
},
|
||||
"LineTag": line_tag(k + 1),
|
||||
"LineType": 0,
|
||||
})
|
||||
return lines
|
||||
|
||||
|
||||
def main() -> None:
|
||||
nodes, placed = build_nodes()
|
||||
xs = [x for _, x, _ in placed]
|
||||
ys = [y for _, _, y in placed]
|
||||
main_seq = {
|
||||
"LineList": build_lines(placed),
|
||||
"NodeList": nodes,
|
||||
"SeqTag": "Main",
|
||||
"ViewCenterPx": str((min(xs) + max(xs)) // 2),
|
||||
"ViewCenterPy": str((min(ys) + max(ys)) // 2),
|
||||
"ViewScaling": "0.7",
|
||||
}
|
||||
OUT.write_text(json.dumps(main_seq, ensure_ascii=False, indent=4), encoding="utf-8")
|
||||
print(f"多节点 Main.seq 已生成:{OUT}")
|
||||
print(f"节点数:{len(nodes)}(1 开始 + {len(placed) - 1} 代码块,已去掉空业务标识)")
|
||||
print(f"连线数:{len(main_seq['LineList'])}")
|
||||
current_phase = None
|
||||
for index, phase_name, title, _ in _iter_steps():
|
||||
if phase_name != current_phase:
|
||||
current_phase = phase_name
|
||||
print(f" [{phase_name}]")
|
||||
print(f" {title}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -2,12 +2,13 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any, Callable
|
||||
from typing import Any, Callable, Dict
|
||||
|
||||
from urllib.error import HTTPError
|
||||
from urllib.parse import quote
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
JsonDict = dict[str, Any]
|
||||
JsonDict = Dict[str, Any]
|
||||
Opener = Callable[[Request, float], Any]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,47 +1,66 @@
|
|||
"""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
|
||||
import sys
|
||||
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,
|
||||
is_placeholder_customer_code,
|
||||
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
|
||||
"""入参缺失 / 页面元素找不到 / 页面改版,均映射 UI_ELEMENT_NOT_FOUND。"""
|
||||
|
||||
def __init__(self, message: str = "") -> None:
|
||||
# Studio 编译出来的旧 S01 在 input_arg 为空时会直接 raise。
|
||||
# 这里拦下来改走默认联调 JSON,避免每次改 Main.py 都被覆盖。
|
||||
if _should_bootstrap_default_input(message):
|
||||
_bootstrap_default_input_and_exit()
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class LoginFailed(RuntimeLoginFailed, ContractCreateError):
|
||||
pass
|
||||
def default_contract_input_path() -> str:
|
||||
return str(
|
||||
Path(__file__).resolve().parent / "examples" / "contract_create_input_dry_run.json"
|
||||
)
|
||||
|
||||
|
||||
class SupplierContractDisabled(RuntimeSupplierContractDisabled, ContractCreateError):
|
||||
pass
|
||||
def _should_bootstrap_default_input(message: str) -> bool:
|
||||
if "缺少业务入参" not in str(message):
|
||||
return False
|
||||
script = os.path.basename(str(sys.argv[0] if sys.argv else "")).lower()
|
||||
return script == "main.py"
|
||||
|
||||
|
||||
class PdfMergeUnsupported(RuntimePdfMergeUnsupported, ContractCreateError):
|
||||
pass
|
||||
def _bootstrap_default_input_and_exit() -> None:
|
||||
path = default_contract_input_path()
|
||||
if not Path(path).is_file():
|
||||
return
|
||||
print("未传入 -i/--input,使用默认联调入参:", path, flush=True)
|
||||
codes_dir = Path(__file__).resolve().parent / "studio" / "codes"
|
||||
if str(codes_dir) not in sys.path:
|
||||
sys.path.insert(0, str(codes_dir))
|
||||
from run_contract import main
|
||||
|
||||
raise SystemExit(main())
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
@ -55,9 +74,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 +93,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 +124,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, flow="CONTRACT_CREATE")
|
||||
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,
|
||||
|
|
@ -135,7 +180,9 @@ def build_flow_context(spec: ContractCreateInput, draft: ContractDraft) -> Contr
|
|||
contract_background=draft.contract_background,
|
||||
contract_nature=draft.contract_nature,
|
||||
amount_currency=draft.amount_currency,
|
||||
amount_type=draft.amount_type,
|
||||
amount=draft.amount,
|
||||
contract_title=draft.contract_title,
|
||||
contract_end_date=draft.contract_end_date,
|
||||
is_major_contract=draft.is_major_contract,
|
||||
is_prepayment=draft.is_prepayment,
|
||||
|
|
@ -147,51 +194,158 @@ 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,
|
||||
contract_document_path=draft.contract_document_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:
|
||||
if draft.contract_document_path:
|
||||
return None
|
||||
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)
|
||||
|
||||
customer_code = draft.customer_code or _str(data.get("customerCode"))
|
||||
if customer_code and is_placeholder_customer_code(customer_code):
|
||||
raise ContractCreateError(
|
||||
f"customerCode「{customer_code}」是占位符,合同系统客商选择窗搜不到。"
|
||||
"请换成测试环境真实存在的客商操作码,例如 DW0100046361(重庆亿昊药业有限公司)"
|
||||
)
|
||||
|
||||
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:
|
||||
fallback = default_contract_input_path()
|
||||
if Path(fallback).is_file():
|
||||
print("未传入 -i/--input,使用默认联调入参:", fallback, flush=True)
|
||||
raw = fallback
|
||||
else:
|
||||
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 +353,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 +370,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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,14 @@ from typing import Any
|
|||
|
||||
from .contract_flow_steps import ContractGoodsLine
|
||||
|
||||
_PLACEHOLDER_CUSTOMER_PREFIXES = ("CUST-", "TEST-", "MOCK-")
|
||||
|
||||
|
||||
def is_placeholder_customer_code(code: str) -> bool:
|
||||
"""CUST-001 / TEST-xxx 这类占位符在合同系统里搜不到,不能拿去点真页面。"""
|
||||
value = (code or "").strip().upper()
|
||||
return value.startswith(_PLACEHOLDER_CUSTOMER_PREFIXES)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ContractDraft:
|
||||
|
|
@ -23,7 +31,9 @@ class ContractDraft:
|
|||
contract_background: str = ""
|
||||
contract_nature: str = "主合同"
|
||||
amount_currency: str = "人民币"
|
||||
amount_type: str = "合同标的金额"
|
||||
amount: str = ""
|
||||
contract_title: str = ""
|
||||
contract_end_date: str = ""
|
||||
is_major_contract: str = "否"
|
||||
is_prepayment: str = "否"
|
||||
|
|
@ -36,6 +46,8 @@ 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
|
||||
contract_document_path: str = ""
|
||||
order_no_list: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
def load_contract_draft(path: str | Path) -> ContractDraft:
|
||||
|
|
@ -52,7 +64,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", [])
|
||||
]
|
||||
|
|
@ -69,7 +83,9 @@ def contract_draft_from_dict(data: dict[str, Any]) -> ContractDraft:
|
|||
contract_background=str(data.get("contractBackground", "")),
|
||||
contract_nature=str(data.get("contractNature", "主合同")),
|
||||
amount_currency=str(data.get("amountCurrency", "人民币")),
|
||||
amount_type=str(data.get("amountType", "合同标的金额")),
|
||||
amount=str(data.get("amount", "")),
|
||||
contract_title=str(data.get("contractTitle") or data.get("docSubject") or ""),
|
||||
contract_end_date=str(data.get("contractEndDate", "")),
|
||||
is_major_contract=str(data.get("isMajorContract", "否")),
|
||||
is_prepayment=str(data.get("isPrepayment", "否")),
|
||||
|
|
@ -82,4 +98,8 @@ 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"),
|
||||
contract_document_path=str(
|
||||
data.get("contractDocumentPath") or data.get("contractBodyPath") or ""
|
||||
),
|
||||
order_no_list=[str(x) for x in data.get("orderNoList", [])],
|
||||
)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"jobId": "YYC-重庆亿昊药业-2fa0-20260812-16abff",
|
||||
"supplierName": "重庆亿昊药业有限公司",
|
||||
"orderDate": "2026-08-12",
|
||||
"sourceType": "YYC",
|
||||
"templateType": "NON_FORMAT",
|
||||
"templateName": "非格式--采购协议",
|
||||
"customerCode": "DW0100046361",
|
||||
"customerName": "重庆亿昊药业有限公司",
|
||||
"contractCategory": "其他协议(采购)",
|
||||
"settlementType": "付款",
|
||||
"contractBackground": "自动化测试合同起草",
|
||||
"contractNature": "主合同",
|
||||
"contractTitle": "test",
|
||||
"amountCurrency": "人民币",
|
||||
"amountType": "合同标的金额",
|
||||
"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": "FQX0100118047",
|
||||
"goodsName": "盐酸托烷司琼注射液",
|
||||
"approvalNo": "苏20160372",
|
||||
"spec": "5ml:5mg*5支",
|
||||
"unit": "盒",
|
||||
"quantity": 10,
|
||||
"mdmCode": "",
|
||||
"unitPrice": "50.00"
|
||||
}
|
||||
],
|
||||
"stateCenterUrl": "http://127.0.0.1:8000",
|
||||
"agentToken": "dev-agent-token"
|
||||
}
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"supplierName": "小药药供应商",
|
||||
"supplierName": "重庆亿昊药业有限公司",
|
||||
"orderDate": "2026-08-11",
|
||||
"sourceType": "XYY",
|
||||
"templateType": "FORMAT",
|
||||
"templateName": "购销协议书(2025)",
|
||||
"customerCode": "CUST-XYY-001",
|
||||
"customerName": "小药药供应商",
|
||||
"customerCode": "DW0100046361",
|
||||
"customerName": "重庆亿昊药业有限公司",
|
||||
"contractCategory": "采购协议",
|
||||
"settlementType": "付款",
|
||||
"contractBackground": "按小药药订单自动起草采购协议",
|
||||
|
|
|
|||
|
|
@ -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": "按医药城订单自动起草采购协议",
|
||||
|
|
|
|||
|
|
@ -1 +1,4 @@
|
|||
pypdf==5.1.0
|
||||
selenium>=4.15.0
|
||||
webdriver-manager>=4.0.0
|
||||
requests>=2.31.0
|
||||
|
|
|
|||
|
|
@ -0,0 +1,153 @@
|
|||
"""Shared ACCEPT report for every RPA flow (any platform / page).
|
||||
|
||||
New flows (ORDER_FETCH, REBATE_*, M5, ...) must use this instead of
|
||||
reading screenshots. Screenshots stay on disk for humans; this text is
|
||||
the agent oracle.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Protocol
|
||||
|
||||
from .errors import NodeError
|
||||
|
||||
PACKAGE_ROOT = Path(__file__).resolve().parents[1]
|
||||
SCREENSHOT_ROOT = PACKAGE_ROOT / "screenshots"
|
||||
|
||||
|
||||
class HasScreenshot(Protocol):
|
||||
def screenshot(self, name: str) -> str: ...
|
||||
|
||||
|
||||
@dataclass
|
||||
class AcceptLine:
|
||||
step: str
|
||||
title: str
|
||||
ok: bool
|
||||
facts: dict[str, str]
|
||||
mismatches: list[str]
|
||||
|
||||
|
||||
def safe_flow_name(flow: str) -> str:
|
||||
text = (flow or "default").strip() or "default"
|
||||
return "".join(ch if ch.isalnum() or ch in "-_" else "_" for ch in text)
|
||||
|
||||
|
||||
def artifact_dir(flow: str) -> Path:
|
||||
return SCREENSHOT_ROOT / safe_flow_name(flow)
|
||||
|
||||
|
||||
def evidence_shot(browser: HasScreenshot, name: str, *, failed: bool = False) -> str:
|
||||
"""Save a screenshot only on failure. Set RPA_SCREENSHOT=always to keep every frame."""
|
||||
mode = (os.environ.get("RPA_SCREENSHOT") or "fail").strip().lower()
|
||||
if not failed and mode != "always":
|
||||
return ""
|
||||
return browser.screenshot(name)
|
||||
|
||||
|
||||
def accept_log_path(flow: str = "default") -> Path:
|
||||
override = os.environ.get("RPA_ACCEPT_LOG")
|
||||
if override:
|
||||
return Path(override)
|
||||
return artifact_dir(flow) / "ACCEPT.txt"
|
||||
|
||||
|
||||
def field_match(actual: str, expected: str) -> bool:
|
||||
actual_text = (actual or "").strip()
|
||||
expected_text = (expected or "").strip()
|
||||
if not expected_text:
|
||||
return True
|
||||
if actual_text == expected_text:
|
||||
return True
|
||||
compact_a = actual_text.replace(" ", "").replace(",", "")
|
||||
compact_e = expected_text.replace(" ", "").replace(",", "")
|
||||
if compact_a == compact_e or compact_a.replace("-", "") == compact_e.replace("-", ""):
|
||||
return True
|
||||
try:
|
||||
return float(compact_a) == float(compact_e)
|
||||
except ValueError:
|
||||
pass
|
||||
yes = {"是", "1", "Y", "YES", "TRUE"}
|
||||
no = {"否", "2", "0", "N", "NO", "FALSE"}
|
||||
if compact_a.upper() in yes and compact_e.upper() in yes:
|
||||
return True
|
||||
if compact_a.upper() in no and compact_e.upper() in no:
|
||||
return True
|
||||
return compact_e in compact_a or compact_a in compact_e
|
||||
|
||||
|
||||
def format_accept(line: AcceptLine) -> str:
|
||||
status = "ok" if line.ok else "FAIL"
|
||||
facts = " ".join(f"{key}={value}" for key, value in line.facts.items())
|
||||
extra = f" {' | '.join(line.mismatches)}" if line.mismatches else ""
|
||||
return f"ACCEPT {line.step} {status} {line.title} {facts}{extra}".rstrip()
|
||||
|
||||
|
||||
@dataclass
|
||||
class AcceptanceReport:
|
||||
"""DOM / page-state checklist. One instance per flow run."""
|
||||
|
||||
flow: str = "default"
|
||||
lines: list[AcceptLine] = field(default_factory=list)
|
||||
|
||||
def reset(self) -> None:
|
||||
self.lines = []
|
||||
|
||||
def check(
|
||||
self,
|
||||
step: str,
|
||||
title: str,
|
||||
pairs: list[tuple[str, object, object]],
|
||||
*,
|
||||
browser: HasScreenshot | None = None,
|
||||
screenshot_on_fail: str = "",
|
||||
) -> None:
|
||||
facts: dict[str, str] = {}
|
||||
mismatches: list[str] = []
|
||||
for name, actual, expected in pairs:
|
||||
actual_text = "" if actual is None else str(actual).strip()
|
||||
expected_text = "" if expected is None else str(expected).strip()
|
||||
facts[name] = actual_text or "(empty)"
|
||||
if expected_text and not field_match(actual_text, expected_text):
|
||||
mismatches.append(f"{name} expected={expected_text!r} actual={actual_text!r}")
|
||||
ok = not mismatches
|
||||
self.lines.append(AcceptLine(step, title, ok, facts, mismatches))
|
||||
print(format_accept(self.lines[-1]), flush=True)
|
||||
self.dump()
|
||||
if ok:
|
||||
return
|
||||
if browser is not None and screenshot_on_fail:
|
||||
evidence_shot(browser, screenshot_on_fail, failed=True)
|
||||
raise NodeError(f"ACCEPT {step} FAIL " + "; ".join(mismatches))
|
||||
|
||||
def note(
|
||||
self,
|
||||
step: str,
|
||||
title: str,
|
||||
facts: dict[str, object],
|
||||
*,
|
||||
ok: bool = True,
|
||||
raise_error: bool = True,
|
||||
) -> None:
|
||||
rendered = {key: ("" if value is None else str(value).strip()) or "(empty)" for key, value in facts.items()}
|
||||
self.lines.append(AcceptLine(step, title, ok, rendered, [] if ok else ["failed"]))
|
||||
print(format_accept(self.lines[-1]), flush=True)
|
||||
self.dump()
|
||||
if not ok and raise_error:
|
||||
raise NodeError(f"ACCEPT {step} FAIL {title} " + " ".join(f"{k}={v}" for k, v in rendered.items()))
|
||||
|
||||
def dump(self) -> None:
|
||||
path = accept_log_path(self.flow)
|
||||
try:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(self.summary_text(), encoding="utf-8")
|
||||
except OSError as exc:
|
||||
print(f"ACCEPT log write skipped: {exc}", flush=True)
|
||||
|
||||
def summary_text(self) -> str:
|
||||
passed = sum(1 for line in self.lines if line.ok)
|
||||
rows = [f"ACCEPT SUMMARY {self.flow} {passed}/{len(self.lines)} ok"]
|
||||
rows.extend(format_accept(line) for line in self.lines)
|
||||
return "\n".join(rows) + "\n"
|
||||
|
|
@ -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,14 +1,13 @@
|
|||
"""Generic start → runner → complete/fail shell."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
from typing import Any, Callable, Dict, Optional
|
||||
|
||||
from ..callback_client import StateCenterClient
|
||||
from .errors import NodeError
|
||||
from .payloads import build_complete_payload, build_fail_payload
|
||||
|
||||
Runner = Callable[[], dict[str, Any] | None]
|
||||
Runner = Callable[[], Optional[Dict[str, Any]]]
|
||||
|
||||
|
||||
def execute_node(
|
||||
|
|
|
|||
|
|
@ -1,116 +1,47 @@
|
|||
{
|
||||
"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": "xform:extendDataFormInfo.value(fd_contract_cate_type)",
|
||||
"coverTemplate": "xform:extendDataFormInfo.value(fd_contract_cover_type)",
|
||||
"settlementType": "select#fdSettype",
|
||||
"contractBackground": "textarea[name='extendDataFormInfo.value(fd_desc_info)']",
|
||||
"contractNatureMain": "input[name='fdType'][value='00']",
|
||||
"contractNatureSub": "input[name='fdType'][value='01']",
|
||||
"docSubject": "input[name='docSubject']",
|
||||
"amountType": "xform:extendDataFormInfo.value(fd_all_amount_type)",
|
||||
"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)']",
|
||||
"selectGoodsButton": "div[title='选择商品']",
|
||||
"goodsDialogFrame": "iframe[src*='EopBasedataMaterial']",
|
||||
"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,790 @@
|
|||
{
|
||||
"LineList": [
|
||||
{
|
||||
"LineColor": "255, 255, 255",
|
||||
"LineCommented": 0,
|
||||
"LineEnd": {
|
||||
"Pidx": "202608143010000000/EPin_Flow_In",
|
||||
"Px": "697",
|
||||
"Py": "314.931"
|
||||
},
|
||||
"LineNote": "",
|
||||
"LineSplit": {
|
||||
"Px": "",
|
||||
"Py": ""
|
||||
},
|
||||
"LineStart": {
|
||||
"Pidx": "202608141000000001/EPin_Flow_Out",
|
||||
"Px": "547",
|
||||
"Py": "296"
|
||||
},
|
||||
"LineTag": "202608149010000000",
|
||||
"LineType": 0
|
||||
},
|
||||
{
|
||||
"LineColor": "255, 255, 255",
|
||||
"LineCommented": 0,
|
||||
"LineEnd": {
|
||||
"Pidx": "202608143020000000/EPin_Flow_In",
|
||||
"Px": "977",
|
||||
"Py": "314.931"
|
||||
},
|
||||
"LineNote": "",
|
||||
"LineSplit": {
|
||||
"Px": "",
|
||||
"Py": ""
|
||||
},
|
||||
"LineStart": {
|
||||
"Pidx": "202608143010000000/EPin_Flow_Out",
|
||||
"Px": "827",
|
||||
"Py": "314.931"
|
||||
},
|
||||
"LineTag": "202608149020000000",
|
||||
"LineType": 0
|
||||
},
|
||||
{
|
||||
"LineColor": "255, 255, 255",
|
||||
"LineCommented": 0,
|
||||
"LineEnd": {
|
||||
"Pidx": "202608143030000000/EPin_Flow_In",
|
||||
"Px": "1257",
|
||||
"Py": "320"
|
||||
},
|
||||
"LineNote": "",
|
||||
"LineSplit": {
|
||||
"Px": "",
|
||||
"Py": ""
|
||||
},
|
||||
"LineStart": {
|
||||
"Pidx": "202608143020000000/EPin_Flow_Out",
|
||||
"Px": "1107",
|
||||
"Py": "314.931"
|
||||
},
|
||||
"LineTag": "202608149030000000",
|
||||
"LineType": 0
|
||||
},
|
||||
{
|
||||
"LineColor": "255, 255, 255",
|
||||
"LineCommented": 0,
|
||||
"LineEnd": {
|
||||
"Pidx": "202608143040000000/EPin_Flow_In",
|
||||
"Px": "417",
|
||||
"Py": "520"
|
||||
},
|
||||
"LineNote": "",
|
||||
"LineSplit": {
|
||||
"Px": "",
|
||||
"Py": ""
|
||||
},
|
||||
"LineStart": {
|
||||
"Pidx": "202608143030000000/EPin_Flow_Out",
|
||||
"Px": "1387",
|
||||
"Py": "320"
|
||||
},
|
||||
"LineTag": "202608149040000000",
|
||||
"LineType": 0
|
||||
},
|
||||
{
|
||||
"LineColor": "255, 255, 255",
|
||||
"LineCommented": 0,
|
||||
"LineEnd": {
|
||||
"Pidx": "202608143050000000/EPin_Flow_In",
|
||||
"Px": "697",
|
||||
"Py": "520"
|
||||
},
|
||||
"LineNote": "",
|
||||
"LineSplit": {
|
||||
"Px": "",
|
||||
"Py": ""
|
||||
},
|
||||
"LineStart": {
|
||||
"Pidx": "202608143040000000/EPin_Flow_Out",
|
||||
"Px": "547",
|
||||
"Py": "520"
|
||||
},
|
||||
"LineTag": "202608149050000000",
|
||||
"LineType": 0
|
||||
},
|
||||
{
|
||||
"LineColor": "255, 255, 255",
|
||||
"LineCommented": 0,
|
||||
"LineEnd": {
|
||||
"Pidx": "202608143060000000/EPin_Flow_In",
|
||||
"Px": "417",
|
||||
"Py": "720"
|
||||
},
|
||||
"LineNote": "",
|
||||
"LineSplit": {
|
||||
"Px": "",
|
||||
"Py": ""
|
||||
},
|
||||
"LineStart": {
|
||||
"Pidx": "202608143050000000/EPin_Flow_Out",
|
||||
"Px": "827",
|
||||
"Py": "520"
|
||||
},
|
||||
"LineTag": "202608149060000000",
|
||||
"LineType": 0
|
||||
},
|
||||
{
|
||||
"LineColor": "255, 255, 255",
|
||||
"LineCommented": 0,
|
||||
"LineEnd": {
|
||||
"Pidx": "202608143070000000/EPin_Flow_In",
|
||||
"Px": "697",
|
||||
"Py": "720"
|
||||
},
|
||||
"LineNote": "",
|
||||
"LineSplit": {
|
||||
"Px": "",
|
||||
"Py": ""
|
||||
},
|
||||
"LineStart": {
|
||||
"Pidx": "202608143060000000/EPin_Flow_Out",
|
||||
"Px": "547",
|
||||
"Py": "720"
|
||||
},
|
||||
"LineTag": "202608149070000000",
|
||||
"LineType": 0
|
||||
},
|
||||
{
|
||||
"LineColor": "255, 255, 255",
|
||||
"LineCommented": 0,
|
||||
"LineEnd": {
|
||||
"Pidx": "202608143080000000/EPin_Flow_In",
|
||||
"Px": "977",
|
||||
"Py": "720"
|
||||
},
|
||||
"LineNote": "",
|
||||
"LineSplit": {
|
||||
"Px": "",
|
||||
"Py": ""
|
||||
},
|
||||
"LineStart": {
|
||||
"Pidx": "202608143070000000/EPin_Flow_Out",
|
||||
"Px": "827",
|
||||
"Py": "720"
|
||||
},
|
||||
"LineTag": "202608149080000000",
|
||||
"LineType": 0
|
||||
},
|
||||
{
|
||||
"LineColor": "255, 255, 255",
|
||||
"LineCommented": 0,
|
||||
"LineEnd": {
|
||||
"Pidx": "202608143090000000/EPin_Flow_In",
|
||||
"Px": "1257",
|
||||
"Py": "720"
|
||||
},
|
||||
"LineNote": "",
|
||||
"LineSplit": {
|
||||
"Px": "",
|
||||
"Py": ""
|
||||
},
|
||||
"LineStart": {
|
||||
"Pidx": "202608143080000000/EPin_Flow_Out",
|
||||
"Px": "1107",
|
||||
"Py": "720"
|
||||
},
|
||||
"LineTag": "202608149090000000",
|
||||
"LineType": 0
|
||||
},
|
||||
{
|
||||
"LineColor": "255, 255, 255",
|
||||
"LineCommented": 0,
|
||||
"LineEnd": {
|
||||
"Pidx": "202608143100000000/EPin_Flow_In",
|
||||
"Px": "1537",
|
||||
"Py": "720"
|
||||
},
|
||||
"LineNote": "",
|
||||
"LineSplit": {
|
||||
"Px": "",
|
||||
"Py": ""
|
||||
},
|
||||
"LineStart": {
|
||||
"Pidx": "202608143090000000/EPin_Flow_Out",
|
||||
"Px": "1387",
|
||||
"Py": "720"
|
||||
},
|
||||
"LineTag": "202608149100000000",
|
||||
"LineType": 0
|
||||
},
|
||||
{
|
||||
"LineColor": "255, 255, 255",
|
||||
"LineCommented": 0,
|
||||
"LineEnd": {
|
||||
"Pidx": "202608143110000000/EPin_Flow_In",
|
||||
"Px": "417",
|
||||
"Py": "920"
|
||||
},
|
||||
"LineNote": "",
|
||||
"LineSplit": {
|
||||
"Px": "",
|
||||
"Py": ""
|
||||
},
|
||||
"LineStart": {
|
||||
"Pidx": "202608143100000000/EPin_Flow_Out",
|
||||
"Px": "1667",
|
||||
"Py": "720"
|
||||
},
|
||||
"LineTag": "202608149110000000",
|
||||
"LineType": 0
|
||||
},
|
||||
{
|
||||
"LineColor": "255, 255, 255",
|
||||
"LineCommented": 0,
|
||||
"LineEnd": {
|
||||
"Pidx": "202608143120000000/EPin_Flow_In",
|
||||
"Px": "697",
|
||||
"Py": "920"
|
||||
},
|
||||
"LineNote": "",
|
||||
"LineSplit": {
|
||||
"Px": "",
|
||||
"Py": ""
|
||||
},
|
||||
"LineStart": {
|
||||
"Pidx": "202608143110000000/EPin_Flow_Out",
|
||||
"Px": "547",
|
||||
"Py": "920"
|
||||
},
|
||||
"LineTag": "202608149120000000",
|
||||
"LineType": 0
|
||||
},
|
||||
{
|
||||
"LineColor": "255, 255, 255",
|
||||
"LineCommented": 0,
|
||||
"LineEnd": {
|
||||
"Pidx": "202608143130000000/EPin_Flow_In",
|
||||
"Px": "977",
|
||||
"Py": "920"
|
||||
},
|
||||
"LineNote": "",
|
||||
"LineSplit": {
|
||||
"Px": "",
|
||||
"Py": ""
|
||||
},
|
||||
"LineStart": {
|
||||
"Pidx": "202608143120000000/EPin_Flow_Out",
|
||||
"Px": "827",
|
||||
"Py": "920"
|
||||
},
|
||||
"LineTag": "202608149130000000",
|
||||
"LineType": 0
|
||||
},
|
||||
{
|
||||
"LineColor": "255, 255, 255",
|
||||
"LineCommented": 0,
|
||||
"LineEnd": {
|
||||
"Pidx": "202608143140000000/EPin_Flow_In",
|
||||
"Px": "1257",
|
||||
"Py": "920"
|
||||
},
|
||||
"LineNote": "",
|
||||
"LineSplit": {
|
||||
"Px": "",
|
||||
"Py": ""
|
||||
},
|
||||
"LineStart": {
|
||||
"Pidx": "202608143130000000/EPin_Flow_Out",
|
||||
"Px": "1107",
|
||||
"Py": "920"
|
||||
},
|
||||
"LineTag": "202608149140000000",
|
||||
"LineType": 0
|
||||
}
|
||||
],
|
||||
"NodeList": [
|
||||
{
|
||||
"NodePanelData": {
|
||||
"ExpandedGroups": [
|
||||
],
|
||||
"InputProperty": {
|
||||
"mouse_x": 0,
|
||||
"mouse_y": 0
|
||||
},
|
||||
"PubProperty": "",
|
||||
"RetPins": [
|
||||
]
|
||||
},
|
||||
"NodeProperty": "",
|
||||
"NodeUI": {
|
||||
"NodeElementID": "",
|
||||
"NodeModel": "2",
|
||||
"NodePropErr": false,
|
||||
"NodePx": "420",
|
||||
"NodePy": "280",
|
||||
"NodeSubType": "EnumWidget_Begin",
|
||||
"NodeTag": "202608141000000001",
|
||||
"NodeTitle": "开始"
|
||||
}
|
||||
},
|
||||
{
|
||||
"NodePanelData": {
|
||||
"ExpandedGroups": [
|
||||
],
|
||||
"InputProperty": {
|
||||
"mouse_x": 0,
|
||||
"mouse_y": 0
|
||||
},
|
||||
"PubProperty": "",
|
||||
"RetPins": [
|
||||
]
|
||||
},
|
||||
"NodeProperty": {
|
||||
"EP_CONTROL": {
|
||||
"EP_CONTROL_IMPORT": "{\n \"PBCode\": \"cHJpbnQoIltTMDEg5Yid5aeL5YyW5bm25Zue6LCDIHN0YXJ0XSIpCmltcG9ydCBzeXMsIG9zLCBqc29uCmZyb20gcGF0aGxpYiBpbXBvcnQgUGF0aAoKX1JFUE9fUk9PVCA9IG5leHQoCiAgICBwIGZvciBwIGluIFBhdGgoX19maWxlX18pLnJlc29sdmUoKS5wYXJlbnRzCiAgICBpZiAocCAvICJycGFfeWlzYWlxaSIgLyAiX19pbml0X18ucHkiKS5leGlzdHMoKQopCmlmIHN0cihfUkVQT19ST09UKSBub3QgaW4gc3lzLnBhdGg6CiAgICBzeXMucGF0aC5pbnNlcnQoMCwgc3RyKF9SRVBPX1JPT1QpKQoKaW1wb3J0IHJwYV95aXNhaXFpLl9mbG93X3N0YXRlIGFzIF9zdApmcm9tIHJwYV95aXNhaXFpLmNvbnRyYWN0X2NyZWF0ZV9ub2RlIGltcG9ydCBDb250cmFjdENyZWF0ZUVycm9yLCBsb2FkX2lucHV0X2RhdGEsIGJ1aWxkX3NwZWNfZnJvbV9kYXRhCmZyb20gcnBhX3lpc2FpcWkuY2FsbGJhY2tfY2xpZW50IGltcG9ydCBTdGF0ZUNlbnRlckNsaWVudApmcm9tIHJwYV95aXNhaXFpLmFwYV9icm93c2VyX2FkYXB0ZXIgaW1wb3J0IEFwYUJyb3dzZXJBZGFwdGVyCgoKZGVmIF9maXJzdCgqdmFsdWVzKToKICAgIGZvciB2YWx1ZSBpbiB2YWx1ZXM6CiAgICAgICAgaWYgdmFsdWUgaXMgbm90IE5vbmUgYW5kIHN0cih2YWx1ZSkuc3RyaXAoKToKICAgICAgICAgICAgcmV0dXJuIHN0cih2YWx1ZSkuc3RyaXAoKQogICAgcmV0dXJuICIiCgoKcmF3ID0gKHNlbGYuaW5wdXRfYXJnIG9yICIiKS5zdHJpcCgpCmlmIG5vdCByYXc6CiAgICByYXcgPSBvcy5nZXRlbnYoIkNPTlRSQUNUX0lOUFVUX0pTT04iLCAiIikuc3RyaXAoKQppZiBub3QgcmF3OgogICAgcmF3ID0gc3RyKF9SRVBPX1JPT1QgLyAicnBhX3lpc2FpcWkiIC8gImV4YW1wbGVzIiAvICJjb250cmFjdF9jcmVhdGVfaW5wdXRfZHJ5X3J1bi5qc29uIikKICAgIHByaW50KCLmnKrkvKDlhaUgLWkvLS1pbnB1dO+8jOS9v+eUqOm7mOiupOiBlOiwg+WFpeWPgu+8miIsIHJhdykKaWYgbm90IHJhdzoKICAgIHJhaXNlIENvbnRyYWN0Q3JlYXRlRXJyb3IoIue8uuWwkeS4muWKoeWFpeWPgu+8muivt+mAmui/hyAtaS8tLWlucHV0IOS8oOWFpSBKU09OIOWtl+espuS4suaIluaWh+S7tui3r+W+hCIpCgpkYXRhID0gbG9hZF9pbnB1dF9kYXRhKHJhdykKc3RhdGVfY2VudGVyX3VybCA9IF9maXJzdCgKICAgIGRhdGEuZ2V0KCJzdGF0ZUNlbnRlclVybCIpLCBkYXRhLmdldCgic3RhdGVDZW50ZXJVUkwiKSwKICAgIG9zLmdldGVudigiU1RBVEVfQ0VOVEVSX1VSTCIpLCAiaHR0cDovLzEyNy4wLjAuMTo4MDAwIiwKKQphZ2VudF90b2tlbiA9IF9maXJzdChkYXRhLmdldCgiYWdlbnRUb2tlbiIpLCBvcy5nZXRlbnYoIkFHRU5UX1RPS0VOIiksICJkZXYtYWdlbnQtdG9rZW4iKQoKX3N0LnNwZWMgPSBidWlsZF9zcGVjX2Zyb21fZGF0YShkYXRhKQpfc3QuY2xpZW50ID0gU3RhdGVDZW50ZXJDbGllbnQoc3RhdGVfY2VudGVyX3VybCwgYWdlbnRfdG9rZW4pCl9zdC5jdHggPSBfc3Quc3BlYy5mbG93X2NvbnRleHQKX3N0LmJyb3dzZXIgPSBBcGFCcm93c2VyQWRhcHRlcih1cmw9X3N0LmN0eC5jb250cmFjdF91cmwpCl9zdC5pc19kcnlfcnVuID0gKF9zdC5zcGVjLnN1Ym1pdF9tb2RlID09ICJkcnlfcnVuIikKCmlmIG5vdCBfc3QuaXNfZHJ5X3J1biBhbmQgb3MuZ2V0ZW52KCJSUEFfQ0FMTEJBQ0tfU1RBVEVfQ0VOVEVSIikgPT0gIjEiOgogICAgX3N0LmNsaWVudC5zdGFydF9ub2RlKF9zdC5zcGVjLmpvYl9pZCwgIkNPTlRSQUNUX0NSRUFURSIp\",\n \"PImport\": \"\"\n}\n"
|
||||
},
|
||||
"EP_Gen": {
|
||||
"EP_Gen_AFTERDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
||||
"EP_Gen_BEFOREDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
||||
"EP_Gen_NOTE": "{\n \"val\": \"UzAxIOWIneWni+WMluW5tuWbnuiwgyBzdGFydA==\"\n}\n"
|
||||
}
|
||||
},
|
||||
"NodeUI": {
|
||||
"NodeElementID": "",
|
||||
"NodeModel": "2",
|
||||
"NodePropErr": false,
|
||||
"NodePx": "700",
|
||||
"NodePy": "280",
|
||||
"NodeSubType": "EnumWidget_Code_Custom",
|
||||
"NodeTag": "202608143010000000",
|
||||
"NodeTitle": "插入代码块#1-13"
|
||||
}
|
||||
},
|
||||
{
|
||||
"NodePanelData": {
|
||||
"ExpandedGroups": [
|
||||
],
|
||||
"InputProperty": {
|
||||
"mouse_x": 0,
|
||||
"mouse_y": 0
|
||||
},
|
||||
"PubProperty": "",
|
||||
"RetPins": [
|
||||
]
|
||||
},
|
||||
"NodeProperty": {
|
||||
"EP_CONTROL": {
|
||||
"EP_CONTROL_IMPORT": "{\n \"PBCode\": \"cHJpbnQoIltTMDIg5omT5byA5ZCI5ZCM57O757ufXSIpCmltcG9ydCBycGFfeWlzYWlxaS5fZmxvd19zdGF0ZSBhcyBfc3QKZnJvbSBycGFfeWlzYWlxaS5jb250cmFjdF9mbG93X3N0ZXBzIGltcG9ydCBvcGVuX2NvbnRyYWN0X3N5c3RlbQoKb3Blbl9jb250cmFjdF9zeXN0ZW0oX3N0LmJyb3dzZXIsIF9zdC5jdHgp\",\n \"PImport\": \"\"\n}\n"
|
||||
},
|
||||
"EP_Gen": {
|
||||
"EP_Gen_AFTERDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
||||
"EP_Gen_BEFOREDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
||||
"EP_Gen_NOTE": "{\n \"val\": \"UzAyIOaJk+W8gOWQiOWQjOezu+e7nw==\"\n}\n"
|
||||
}
|
||||
},
|
||||
"NodeUI": {
|
||||
"NodeElementID": "",
|
||||
"NodeModel": "2",
|
||||
"NodePropErr": false,
|
||||
"NodePx": "980",
|
||||
"NodePy": "280",
|
||||
"NodeSubType": "EnumWidget_Code_Custom",
|
||||
"NodeTag": "202608143020000000",
|
||||
"NodeTitle": "插入代码块#1-12"
|
||||
}
|
||||
},
|
||||
{
|
||||
"NodePanelData": {
|
||||
"ExpandedGroups": [
|
||||
],
|
||||
"InputProperty": {
|
||||
"mouse_x": 0,
|
||||
"mouse_y": 0
|
||||
},
|
||||
"PubProperty": "",
|
||||
"RetPins": [
|
||||
]
|
||||
},
|
||||
"NodeProperty": {
|
||||
"EP_CONTROL": {
|
||||
"EP_CONTROL_IMPORT": "{\n \"PImport\": \"\",\n \"PBCode\": \"cHJpbnQoIltTMDMg55m75b2V5ZCI5ZCM57O757ufXSIpCmltcG9ydCBycGFfeWlzYWlxaS5fZmxvd19zdGF0ZSBhcyBfc3QKZnJvbSBycGFfeWlzYWlxaS5jb250cmFjdF9mbG93X3N0ZXBzIGltcG9ydCBsb2dpbl9jb250cmFjdF9zeXN0ZW0KCmxvZ2luX2NvbnRyYWN0X3N5c3RlbShfc3QuYnJvd3NlciwgX3N0LmN0eCk=\"\n}\n"
|
||||
},
|
||||
"EP_Gen": {
|
||||
"EP_Gen_AFTERDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
||||
"EP_Gen_BEFOREDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
||||
"EP_Gen_NOTE": "{\n \"val\": \"UzAzIOeZu+W9leWQiOWQjOezu+e7nw==\"\n}\n"
|
||||
}
|
||||
},
|
||||
"NodeUI": {
|
||||
"NodeElementID": "",
|
||||
"NodeModel": "2",
|
||||
"NodePropErr": false,
|
||||
"NodePx": "1260",
|
||||
"NodePy": "280",
|
||||
"NodeSubType": "EnumWidget_Code_Custom",
|
||||
"NodeTag": "202608143030000000",
|
||||
"NodeTitle": "插入代码块#1-11"
|
||||
}
|
||||
},
|
||||
{
|
||||
"NodePanelData": {
|
||||
"ExpandedGroups": [
|
||||
],
|
||||
"InputProperty": {
|
||||
"mouse_x": 0,
|
||||
"mouse_y": 0
|
||||
},
|
||||
"PubProperty": "",
|
||||
"RetPins": [
|
||||
]
|
||||
},
|
||||
"NodeProperty": {
|
||||
"EP_CONTROL": {
|
||||
"EP_CONTROL_IMPORT": "{\n \"PImport\": \"\",\n \"PBCode\": \"cHJpbnQoIltTMDQg6L+b5YWl5ZCI5ZCM6LW36I2JXSIpCmltcG9ydCBycGFfeWlzYWlxaS5fZmxvd19zdGF0ZSBhcyBfc3QKZnJvbSBycGFfeWlzYWlxaS5jb250cmFjdF9mbG93X3N0ZXBzIGltcG9ydCBlbnRlcl9jb250cmFjdF9jcmVhdGUKCmVudGVyX2NvbnRyYWN0X2NyZWF0ZShfc3QuYnJvd3Nlcik=\"\n}\n"
|
||||
},
|
||||
"EP_Gen": {
|
||||
"EP_Gen_AFTERDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
||||
"EP_Gen_BEFOREDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
||||
"EP_Gen_NOTE": "{\n \"val\": \"UzA0IOi/m+WFpeWQiOWQjOi1t+iNiQ==\"\n}\n"
|
||||
}
|
||||
},
|
||||
"NodeUI": {
|
||||
"NodeElementID": "",
|
||||
"NodeModel": "2",
|
||||
"NodePropErr": false,
|
||||
"NodePx": "420",
|
||||
"NodePy": "480",
|
||||
"NodeSubType": "EnumWidget_Code_Custom",
|
||||
"NodeTag": "202608143040000000",
|
||||
"NodeTitle": "插入代码块#1-10"
|
||||
}
|
||||
},
|
||||
{
|
||||
"NodePanelData": {
|
||||
"ExpandedGroups": [
|
||||
],
|
||||
"InputProperty": {
|
||||
"mouse_x": 0,
|
||||
"mouse_y": 0
|
||||
},
|
||||
"PubProperty": "",
|
||||
"RetPins": [
|
||||
]
|
||||
},
|
||||
"NodeProperty": {
|
||||
"EP_CONTROL": {
|
||||
"EP_CONTROL_IMPORT": "{\n \"PImport\": \"\",\n \"PBCode\": \"cHJpbnQoIltTMDUg6YCJ5oup5qih5p2/XSIpCmltcG9ydCBycGFfeWlzYWlxaS5fZmxvd19zdGF0ZSBhcyBfc3QKZnJvbSBycGFfeWlzYWlxaS5jb250cmFjdF9mbG93X3N0ZXBzIGltcG9ydCBzZWxlY3RfY29udHJhY3RfdGVtcGxhdGUKCnNlbGVjdF9jb250cmFjdF90ZW1wbGF0ZShfc3QuYnJvd3NlciwgX3N0LmN0eCk=\"\n}\n"
|
||||
},
|
||||
"EP_Gen": {
|
||||
"EP_Gen_AFTERDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
||||
"EP_Gen_BEFOREDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
||||
"EP_Gen_NOTE": "{\n \"val\": \"UzA1IOmAieaLqeaooeadvw==\"\n}\n"
|
||||
}
|
||||
},
|
||||
"NodeUI": {
|
||||
"NodeElementID": "",
|
||||
"NodeModel": "2",
|
||||
"NodePropErr": false,
|
||||
"NodePx": "700",
|
||||
"NodePy": "480",
|
||||
"NodeSubType": "EnumWidget_Code_Custom",
|
||||
"NodeTag": "202608143050000000",
|
||||
"NodeTitle": "插入代码块#1-9"
|
||||
}
|
||||
},
|
||||
{
|
||||
"NodePanelData": {
|
||||
"ExpandedGroups": [
|
||||
],
|
||||
"InputProperty": {
|
||||
"mouse_x": 0,
|
||||
"mouse_y": 0
|
||||
},
|
||||
"PubProperty": "",
|
||||
"RetPins": [
|
||||
]
|
||||
},
|
||||
"NodeProperty": {
|
||||
"EP_CONTROL": {
|
||||
"EP_CONTROL_IMPORT": "{\n \"PImport\": \"\",\n \"PBCode\": \"cHJpbnQoIltTMDYg5aGr5YaZ562+57qm5pa5XSIpCmltcG9ydCBycGFfeWlzYWlxaS5fZmxvd19zdGF0ZSBhcyBfc3QKZnJvbSBycGFfeWlzYWlxaS5jb250cmFjdF9mbG93X3N0ZXBzIGltcG9ydCBmaWxsX2NvbnRyYWN0X3NpZ25hdG9yeV9pbmZvCgpmaWxsX2NvbnRyYWN0X3NpZ25hdG9yeV9pbmZvKF9zdC5icm93c2VyLCBfc3QuY3R4KQ==\"\n}\n"
|
||||
},
|
||||
"EP_Gen": {
|
||||
"EP_Gen_AFTERDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
||||
"EP_Gen_BEFOREDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
||||
"EP_Gen_NOTE": "{\n \"val\": \"UzA2IOWhq+WGmeetvue6puaWuQ==\"\n}\n"
|
||||
}
|
||||
},
|
||||
"NodeUI": {
|
||||
"NodeElementID": "",
|
||||
"NodeModel": "2",
|
||||
"NodePropErr": false,
|
||||
"NodePx": "420",
|
||||
"NodePy": "680",
|
||||
"NodeSubType": "EnumWidget_Code_Custom",
|
||||
"NodeTag": "202608143060000000",
|
||||
"NodeTitle": "插入代码块#1-8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"NodePanelData": {
|
||||
"ExpandedGroups": [
|
||||
],
|
||||
"InputProperty": {
|
||||
"mouse_x": 0,
|
||||
"mouse_y": 0
|
||||
},
|
||||
"PubProperty": "",
|
||||
"RetPins": [
|
||||
]
|
||||
},
|
||||
"NodeProperty": {
|
||||
"EP_CONTROL": {
|
||||
"EP_CONTROL_IMPORT": "{\n \"PImport\": \"\",\n \"PBCode\": \"cHJpbnQoIltTMDcg5aGr5YaZ5Z+656GA5L+h5oGvXSIpCmltcG9ydCBycGFfeWlzYWlxaS5fZmxvd19zdGF0ZSBhcyBfc3QKZnJvbSBycGFfeWlzYWlxaS5jb250cmFjdF9mbG93X3N0ZXBzIGltcG9ydCBmaWxsX2NvbnRyYWN0X2Jhc2ljX2luZm8KCmZpbGxfY29udHJhY3RfYmFzaWNfaW5mbyhfc3QuYnJvd3NlciwgX3N0LmN0eCk=\"\n}\n"
|
||||
},
|
||||
"EP_Gen": {
|
||||
"EP_Gen_AFTERDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
||||
"EP_Gen_BEFOREDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
||||
"EP_Gen_NOTE": "{\n \"val\": \"UzA3IOWhq+WGmeWfuuehgOS/oeaBrw==\"\n}\n"
|
||||
}
|
||||
},
|
||||
"NodeUI": {
|
||||
"NodeElementID": "",
|
||||
"NodeModel": "2",
|
||||
"NodePropErr": false,
|
||||
"NodePx": "700",
|
||||
"NodePy": "680",
|
||||
"NodeSubType": "EnumWidget_Code_Custom",
|
||||
"NodeTag": "202608143070000000",
|
||||
"NodeTitle": "插入代码块#1-7"
|
||||
}
|
||||
},
|
||||
{
|
||||
"NodePanelData": {
|
||||
"ExpandedGroups": [
|
||||
],
|
||||
"InputProperty": {
|
||||
"mouse_x": 0,
|
||||
"mouse_y": 0
|
||||
},
|
||||
"PubProperty": "",
|
||||
"RetPins": [
|
||||
]
|
||||
},
|
||||
"NodeProperty": {
|
||||
"EP_CONTROL": {
|
||||
"EP_CONTROL_IMPORT": "{\n \"PImport\": \"\",\n \"PBCode\": \"cHJpbnQoIltTMDgg5aGr5YaZ6YeR6aKd5pel5pyfXSIpCmltcG9ydCBycGFfeWlzYWlxaS5fZmxvd19zdGF0ZSBhcyBfc3QKZnJvbSBycGFfeWlzYWlxaS5jb250cmFjdF9mbG93X3N0ZXBzIGltcG9ydCBmaWxsX2NvbnRyYWN0X2Ftb3VudF9hbmRfZGF0ZXMKCmZpbGxfY29udHJhY3RfYW1vdW50X2FuZF9kYXRlcyhfc3QuYnJvd3NlciwgX3N0LmN0eCk=\"\n}\n"
|
||||
},
|
||||
"EP_Gen": {
|
||||
"EP_Gen_AFTERDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
||||
"EP_Gen_BEFOREDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
||||
"EP_Gen_NOTE": "{\n \"val\": \"UzA4IOWhq+WGmemHkemineaXpeacnw==\"\n}\n"
|
||||
}
|
||||
},
|
||||
"NodeUI": {
|
||||
"NodeElementID": "",
|
||||
"NodeModel": "2",
|
||||
"NodePropErr": false,
|
||||
"NodePx": "980",
|
||||
"NodePy": "680",
|
||||
"NodeSubType": "EnumWidget_Code_Custom",
|
||||
"NodeTag": "202608143080000000",
|
||||
"NodeTitle": "插入代码块#1-6"
|
||||
}
|
||||
},
|
||||
{
|
||||
"NodePanelData": {
|
||||
"ExpandedGroups": [
|
||||
],
|
||||
"InputProperty": {
|
||||
"mouse_x": 0,
|
||||
"mouse_y": 0
|
||||
},
|
||||
"PubProperty": "",
|
||||
"RetPins": [
|
||||
]
|
||||
},
|
||||
"NodeProperty": {
|
||||
"EP_CONTROL": {
|
||||
"EP_CONTROL_IMPORT": "{\n \"PImport\": \"\",\n \"PBCode\": \"cHJpbnQoIltTMDkg5aGr5YaZ6LSn5ZOB5piO57uGXSIpCmltcG9ydCBycGFfeWlzYWlxaS5fZmxvd19zdGF0ZSBhcyBfc3QKZnJvbSBycGFfeWlzYWlxaS5jb250cmFjdF9mbG93X3N0ZXBzIGltcG9ydCBmaWxsX2NvbnRyYWN0X2dvb2RzX2xpbmVzCgpmaWxsX2NvbnRyYWN0X2dvb2RzX2xpbmVzKF9zdC5icm93c2VyLCBfc3QuY3R4KQ==\"\n}\n"
|
||||
},
|
||||
"EP_Gen": {
|
||||
"EP_Gen_AFTERDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
||||
"EP_Gen_BEFOREDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
||||
"EP_Gen_NOTE": "{\n \"val\": \"UzA5IOWhq+WGmei0p+WTgeaYjue7hg==\"\n}\n"
|
||||
}
|
||||
},
|
||||
"NodeUI": {
|
||||
"NodeElementID": "",
|
||||
"NodeModel": "2",
|
||||
"NodePropErr": false,
|
||||
"NodePx": "1260",
|
||||
"NodePy": "680",
|
||||
"NodeSubType": "EnumWidget_Code_Custom",
|
||||
"NodeTag": "202608143090000000",
|
||||
"NodeTitle": "插入代码块#1-5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"NodePanelData": {
|
||||
"ExpandedGroups": [
|
||||
],
|
||||
"InputProperty": {
|
||||
"mouse_x": 0,
|
||||
"mouse_y": 0
|
||||
},
|
||||
"PubProperty": "",
|
||||
"RetPins": [
|
||||
]
|
||||
},
|
||||
"NodeProperty": {
|
||||
"EP_CONTROL": {
|
||||
"EP_CONTROL_IMPORT": "{\n \"PImport\": \"\",\n \"PBCode\": \"cHJpbnQoIltTMTAg5aGr5YaZ5LuY5qy+5L+h5oGvXSIpCmltcG9ydCBycGFfeWlzYWlxaS5fZmxvd19zdGF0ZSBhcyBfc3QKZnJvbSBycGFfeWlzYWlxaS5jb250cmFjdF9mbG93X3N0ZXBzIGltcG9ydCBmaWxsX2NvbnRyYWN0X3BheW1lbnRfaW5mbwoKZmlsbF9jb250cmFjdF9wYXltZW50X2luZm8oX3N0LmJyb3dzZXIsIF9zdC5jdHgp\"\n}\n"
|
||||
},
|
||||
"EP_Gen": {
|
||||
"EP_Gen_AFTERDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
||||
"EP_Gen_BEFOREDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
||||
"EP_Gen_NOTE": "{\n \"val\": \"UzEwIOWhq+WGmeS7mOasvuS/oeaBrw==\"\n}\n"
|
||||
}
|
||||
},
|
||||
"NodeUI": {
|
||||
"NodeElementID": "",
|
||||
"NodeModel": "2",
|
||||
"NodePropErr": false,
|
||||
"NodePx": "1540",
|
||||
"NodePy": "680",
|
||||
"NodeSubType": "EnumWidget_Code_Custom",
|
||||
"NodeTag": "202608143100000000",
|
||||
"NodeTitle": "插入代码块#1-4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"NodePanelData": {
|
||||
"ExpandedGroups": [
|
||||
],
|
||||
"InputProperty": {
|
||||
"mouse_x": 0,
|
||||
"mouse_y": 0
|
||||
},
|
||||
"PubProperty": "",
|
||||
"RetPins": [
|
||||
]
|
||||
},
|
||||
"NodeProperty": {
|
||||
"EP_CONTROL": {
|
||||
"EP_CONTROL_IMPORT": "{\n \"PImport\": \"\",\n \"PBCode\": \"cHJpbnQoIltTMTEg5LiK5Lyg5ZCI5ZCMUERGXSIpCmltcG9ydCBycGFfeWlzYWlxaS5fZmxvd19zdGF0ZSBhcyBfc3QKZnJvbSBycGFfeWlzYWlxaS5jb250cmFjdF9mbG93X3N0ZXBzIGltcG9ydCB1cGxvYWRfY29udHJhY3RfZG9jdW1lbnRfaWZfbmVlZGVkCgp1cGxvYWRfY29udHJhY3RfZG9jdW1lbnRfaWZfbmVlZGVkKF9zdC5icm93c2VyLCBfc3QuY3R4KQ==\"\n}\n"
|
||||
},
|
||||
"EP_Gen": {
|
||||
"EP_Gen_AFTERDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
||||
"EP_Gen_BEFOREDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
||||
"EP_Gen_NOTE": "{\n \"val\": \"UzExIOS4iuS8oOWQiOWQjFBERg==\"\n}\n"
|
||||
}
|
||||
},
|
||||
"NodeUI": {
|
||||
"NodeElementID": "",
|
||||
"NodeModel": "2",
|
||||
"NodePropErr": false,
|
||||
"NodePx": "420",
|
||||
"NodePy": "880",
|
||||
"NodeSubType": "EnumWidget_Code_Custom",
|
||||
"NodeTag": "202608143110000000",
|
||||
"NodeTitle": "插入代码块#1-3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"NodePanelData": {
|
||||
"ExpandedGroups": [
|
||||
],
|
||||
"InputProperty": {
|
||||
"mouse_x": 0,
|
||||
"mouse_y": 0
|
||||
},
|
||||
"PubProperty": "",
|
||||
"RetPins": [
|
||||
]
|
||||
},
|
||||
"NodeProperty": {
|
||||
"EP_CONTROL": {
|
||||
"EP_CONTROL_IMPORT": "{\n \"PImport\": \"\",\n \"PBCode\": \"cHJpbnQoIltTMTIg5o+Q5Lqk6YCB5a6hXSIpCmltcG9ydCBycGFfeWlzYWlxaS5fZmxvd19zdGF0ZSBhcyBfc3QKZnJvbSBycGFfeWlzYWlxaS5jb250cmFjdF9mbG93X3N0ZXBzIGltcG9ydCBzdWJtaXRfY29udHJhY3RfZm9yX2FwcHJvdmFsCgpfc3Quc3VibWl0dGVkID0gc3VibWl0X2NvbnRyYWN0X2Zvcl9hcHByb3ZhbChfc3QuYnJvd3NlciwgX3N0LmN0eCk=\"\n}\n"
|
||||
},
|
||||
"EP_Gen": {
|
||||
"EP_Gen_AFTERDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
||||
"EP_Gen_BEFOREDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
||||
"EP_Gen_NOTE": "{\n \"val\": \"UzEyIOaPkOS6pOmAgeWuoQ==\"\n}\n"
|
||||
}
|
||||
},
|
||||
"NodeUI": {
|
||||
"NodeElementID": "",
|
||||
"NodeModel": "2",
|
||||
"NodePropErr": false,
|
||||
"NodePx": "700",
|
||||
"NodePy": "880",
|
||||
"NodeSubType": "EnumWidget_Code_Custom",
|
||||
"NodeTag": "202608143120000000",
|
||||
"NodeTitle": "插入代码块#1-2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"NodePanelData": {
|
||||
"ExpandedGroups": [
|
||||
],
|
||||
"InputProperty": {
|
||||
"mouse_x": 0,
|
||||
"mouse_y": 0
|
||||
},
|
||||
"PubProperty": "",
|
||||
"RetPins": [
|
||||
]
|
||||
},
|
||||
"NodeProperty": {
|
||||
"EP_CONTROL": {
|
||||
"EP_CONTROL_IMPORT": "{\n \"PImport\": \"\",\n \"PBCode\": \"cHJpbnQoIltTMTMg6K+75Y+W5ZCI5ZCM57yW5Y+3XSIpCmltcG9ydCBycGFfeWlzYWlxaS5fZmxvd19zdGF0ZSBhcyBfc3QKZnJvbSBycGFfeWlzYWlxaS5jb250cmFjdF9mbG93X3N0ZXBzIGltcG9ydCByZWFkX2NvbnRyYWN0X251bWJlcgoKaWYgX3N0LnN1Ym1pdHRlZDoKICAgIF9zdC5jb250cmFjdF9ubyA9IHJlYWRfY29udHJhY3RfbnVtYmVyKF9zdC5icm93c2VyKQ==\"\n}\n"
|
||||
},
|
||||
"EP_Gen": {
|
||||
"EP_Gen_AFTERDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
||||
"EP_Gen_BEFOREDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
||||
"EP_Gen_NOTE": "{\n \"val\": \"UzEzIOivu+WPluWQiOWQjOe8luWPtw==\"\n}\n"
|
||||
}
|
||||
},
|
||||
"NodeUI": {
|
||||
"NodeElementID": "",
|
||||
"NodeModel": "2",
|
||||
"NodePropErr": false,
|
||||
"NodePx": "980",
|
||||
"NodePy": "880",
|
||||
"NodeSubType": "EnumWidget_Code_Custom",
|
||||
"NodeTag": "202608143130000000",
|
||||
"NodeTitle": "插入代码块#1-1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"NodePanelData": {
|
||||
"ExpandedGroups": [
|
||||
],
|
||||
"InputProperty": {
|
||||
"mouse_x": 0,
|
||||
"mouse_y": 0
|
||||
},
|
||||
"PubProperty": "",
|
||||
"RetPins": [
|
||||
]
|
||||
},
|
||||
"NodeProperty": {
|
||||
"EP_CONTROL": {
|
||||
"EP_CONTROL_IMPORT": "{\n \"PImport\": \"\",\n \"PBCode\": \"cHJpbnQoIltTMTQg5Zue6LCDIGNvbXBsZXRlXSIpCmltcG9ydCBqc29uCmltcG9ydCBycGFfeWlzYWlxaS5fZmxvd19zdGF0ZSBhcyBfc3QKCmlmIG5vdCBfc3QuaXNfZHJ5X3J1biBhbmQgX3N0LmNvbnRyYWN0X25vIGFuZCBvcy5nZXRlbnYoIlJQQV9DQUxMQkFDS19TVEFURV9DRU5URVIiKSA9PSAiMSI6CiAgICBfc3QuY2xpZW50LmNvbXBsZXRlX25vZGUoX3N0LnNwZWMuam9iX2lkLCAiQ09OVFJBQ1RfQ1JFQVRFIiwgewogICAgICAgICJidXNpbmVzc1JlZnMiOiB7ImNvbnRyYWN0U3lzdGVtTm8iOiBfc3QuY29udHJhY3Rfbm99LAogICAgfSkKcHJpbnQoanNvbi5kdW1wcyh7InN0YXR1cyI6ICJkb25lIiwgImNvbnRyYWN0U3lzdGVtTm8iOiBfc3QuY29udHJhY3Rfbm99LCBlbnN1cmVfYXNjaWk9RmFsc2UpKQ==\"\n}\n"
|
||||
},
|
||||
"EP_Gen": {
|
||||
"EP_Gen_AFTERDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
||||
"EP_Gen_BEFOREDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
||||
"EP_Gen_NOTE": "{\n \"val\": \"UzE0IOWbnuiwgyBjb21wbGV0ZQ==\"\n}\n"
|
||||
}
|
||||
},
|
||||
"NodeUI": {
|
||||
"NodeElementID": "",
|
||||
"NodeModel": "2",
|
||||
"NodePropErr": false,
|
||||
"NodePx": "1260",
|
||||
"NodePy": "880",
|
||||
"NodeSubType": "EnumWidget_Code_Custom",
|
||||
"NodeTag": "202608143140000000",
|
||||
"NodeTitle": "插入代码块#1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"SeqTag": "Main",
|
||||
"ViewCenterPx": "1357.02",
|
||||
"ViewCenterPy": "547.412",
|
||||
"ViewScaling": "0.564474"
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"GlobalVar": [
|
||||
],
|
||||
"OpenFlow": "[\n {\n \"current-tab\": \"true\",\n \"seq\": \"Main\",\n \"tag\": \"2026081210133309518\",\n \"title\": \"Main\",\n \"type\": \"bp\"\n }\n]\n",
|
||||
"ProInfo": {
|
||||
"CodeCustomCount": "0",
|
||||
"ProAbsolutePath": "D:/work/AutoRpa/rpa_yisaiqi/studio",
|
||||
"ProChange": "2026/08/18 13:12:33",
|
||||
"ProColorRef": "4",
|
||||
"ProCreate": "2026/08/12 10:13:31",
|
||||
"ProDesc": "",
|
||||
"ProDocVersion": "1.0.0.4",
|
||||
"ProImports": "",
|
||||
"ProIsAdminRun": "0",
|
||||
"ProIsNewReturn": "1",
|
||||
"ProName": "AutoRpa",
|
||||
"ProStudio": "2026.1.0.36",
|
||||
"ProTag": "2026081210133159316",
|
||||
"ProTheme": "light",
|
||||
"ProUIADeepMatch": "1",
|
||||
"ProUserID": "34BFF76918A9449ABAB323149DDCEA73",
|
||||
"ProUserName": "Administrator",
|
||||
"ProVersion": "1.0.0.0",
|
||||
"operatingSystem": 0,
|
||||
"releaseUserName": "qm01"
|
||||
},
|
||||
"SeqGroupList": [
|
||||
],
|
||||
"SeqPythonModuleList": [
|
||||
],
|
||||
"SeqTextFileList": [
|
||||
],
|
||||
"SequenceList": [
|
||||
{
|
||||
"sdc": "",
|
||||
"seq": "Main",
|
||||
"seqptag": "",
|
||||
"seqret": "1",
|
||||
"seqtag": "2026081210133309518",
|
||||
"spr": [
|
||||
],
|
||||
"spv": [
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
# coding=utf-8
|
||||
# 编译日期:2026-08-12 10:13:31
|
||||
# 版权所有:www.i-search.com.cn
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# 测试运行或调试以上全局函数入口
|
||||
if __name__ == '__main__':
|
||||
pass
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
# coding=utf-8
|
||||
# 编译日期:2026-08-17 16:28:48
|
||||
# 版权所有: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):
|
||||
#插入代码块#1-13
|
||||
self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:202608143010000000,Title:插入代码块#1-13,Note:S01 初始化并回调 start')
|
||||
print("[S01 初始化并回调 start]")
|
||||
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))
|
||||
import rpa_yisaiqi._flow_state as _st
|
||||
from rpa_yisaiqi.contract_create_node import ContractCreateError, load_input_data, build_spec_from_data
|
||||
from rpa_yisaiqi.callback_client import StateCenterClient
|
||||
from rpa_yisaiqi.apa_browser_adapter import ApaBrowserAdapter
|
||||
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:
|
||||
raw = os.getenv("CONTRACT_INPUT_JSON", "").strip()
|
||||
if not raw:
|
||||
raw = str(_REPO_ROOT / "rpa_yisaiqi" / "examples" / "contract_create_input_dry_run.json")
|
||||
print("未传入 -i/--input,使用默认联调入参:", raw)
|
||||
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")
|
||||
_st.spec = build_spec_from_data(data)
|
||||
_st.client = StateCenterClient(state_center_url, agent_token)
|
||||
_st.ctx = _st.spec.flow_context
|
||||
_st.browser = ApaBrowserAdapter(url=_st.ctx.contract_url)
|
||||
_st.is_dry_run = (_st.spec.submit_mode == "dry_run")
|
||||
if not _st.is_dry_run and os.getenv("RPA_CALLBACK_STATE_CENTER") == "1":
|
||||
_st.client.start_node(_st.spec.job_id, "CONTRACT_CREATE")
|
||||
#插入代码块#1-12
|
||||
self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:202608143020000000,Title:插入代码块#1-12,Note:S02 打开合同系统')
|
||||
print("[S02 打开合同系统]")
|
||||
import rpa_yisaiqi._flow_state as _st
|
||||
from rpa_yisaiqi.contract_flow_steps import open_contract_system
|
||||
open_contract_system(_st.browser, _st.ctx)
|
||||
#插入代码块#1-11
|
||||
self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:202608143030000000,Title:插入代码块#1-11,Note:S03 登录合同系统')
|
||||
print("[S03 登录合同系统]")
|
||||
import rpa_yisaiqi._flow_state as _st
|
||||
from rpa_yisaiqi.contract_flow_steps import login_contract_system
|
||||
login_contract_system(_st.browser, _st.ctx)
|
||||
#插入代码块#1-10
|
||||
self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:202608143040000000,Title:插入代码块#1-10,Note:S04 进入合同起草')
|
||||
print("[S04 进入合同起草]")
|
||||
import rpa_yisaiqi._flow_state as _st
|
||||
from rpa_yisaiqi.contract_flow_steps import enter_contract_create
|
||||
enter_contract_create(_st.browser)
|
||||
#插入代码块#1-9
|
||||
self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:202608143050000000,Title:插入代码块#1-9,Note:S05 选择模板')
|
||||
print("[S05 选择模板]")
|
||||
import rpa_yisaiqi._flow_state as _st
|
||||
from rpa_yisaiqi.contract_flow_steps import select_contract_template
|
||||
select_contract_template(_st.browser, _st.ctx)
|
||||
#插入代码块#1-8
|
||||
self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:202608143060000000,Title:插入代码块#1-8,Note:S06 填写签约方')
|
||||
print("[S06 填写签约方]")
|
||||
import rpa_yisaiqi._flow_state as _st
|
||||
from rpa_yisaiqi.contract_flow_steps import fill_contract_signatory_info
|
||||
fill_contract_signatory_info(_st.browser, _st.ctx)
|
||||
#插入代码块#1-7
|
||||
self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:202608143070000000,Title:插入代码块#1-7,Note:S07 填写基础信息')
|
||||
print("[S07 填写基础信息]")
|
||||
import rpa_yisaiqi._flow_state as _st
|
||||
from rpa_yisaiqi.contract_flow_steps import fill_contract_basic_info
|
||||
fill_contract_basic_info(_st.browser, _st.ctx)
|
||||
#插入代码块#1-6
|
||||
self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:202608143080000000,Title:插入代码块#1-6,Note:S08 填写金额日期')
|
||||
print("[S08 填写金额日期]")
|
||||
import rpa_yisaiqi._flow_state as _st
|
||||
from rpa_yisaiqi.contract_flow_steps import fill_contract_amount_and_dates
|
||||
fill_contract_amount_and_dates(_st.browser, _st.ctx)
|
||||
#插入代码块#1-5
|
||||
self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:202608143090000000,Title:插入代码块#1-5,Note:S09 填写货品明细')
|
||||
print("[S09 填写货品明细]")
|
||||
import rpa_yisaiqi._flow_state as _st
|
||||
from rpa_yisaiqi.contract_flow_steps import fill_contract_goods_lines
|
||||
fill_contract_goods_lines(_st.browser, _st.ctx)
|
||||
#插入代码块#1-4
|
||||
self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:202608143100000000,Title:插入代码块#1-4,Note:S10 填写付款信息')
|
||||
print("[S10 填写付款信息]")
|
||||
import rpa_yisaiqi._flow_state as _st
|
||||
from rpa_yisaiqi.contract_flow_steps import fill_contract_payment_info
|
||||
fill_contract_payment_info(_st.browser, _st.ctx)
|
||||
#插入代码块#1-3
|
||||
self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:202608143110000000,Title:插入代码块#1-3,Note:S11 上传合同PDF')
|
||||
print("[S11 上传合同PDF]")
|
||||
import rpa_yisaiqi._flow_state as _st
|
||||
from rpa_yisaiqi.contract_flow_steps import upload_contract_document_if_needed
|
||||
upload_contract_document_if_needed(_st.browser, _st.ctx)
|
||||
#插入代码块#1-2
|
||||
self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:202608143120000000,Title:插入代码块#1-2,Note:S12 提交送审')
|
||||
print("[S12 提交送审]")
|
||||
import rpa_yisaiqi._flow_state as _st
|
||||
from rpa_yisaiqi.contract_flow_steps import submit_contract_for_approval
|
||||
_st.submitted = submit_contract_for_approval(_st.browser, _st.ctx)
|
||||
#插入代码块#1-1
|
||||
self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:202608143130000000,Title:插入代码块#1-1,Note:S13 读取合同编号')
|
||||
print("[S13 读取合同编号]")
|
||||
import rpa_yisaiqi._flow_state as _st
|
||||
from rpa_yisaiqi.contract_flow_steps import read_contract_number
|
||||
if _st.submitted:
|
||||
_st.contract_no = read_contract_number(_st.browser)
|
||||
#插入代码块#1
|
||||
self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:202608143140000000,Title:插入代码块#1,Note:S14 回调 complete')
|
||||
print("[S14 回调 complete]")
|
||||
import json
|
||||
import rpa_yisaiqi._flow_state as _st
|
||||
if not _st.is_dry_run and _st.contract_no and os.getenv("RPA_CALLBACK_STATE_CENTER") == "1":
|
||||
_st.client.complete_node(_st.spec.job_id, "CONTRACT_CREATE", {
|
||||
"businessRefs": {"contractSystemNo": _st.contract_no},
|
||||
})
|
||||
print(json.dumps({"status": "done", "contractSystemNo": _st.contract_no}, ensure_ascii=False))
|
||||
|
||||
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,[]))
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
# coding: utf8
|
||||
"""
|
||||
Python模块脚本
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
# 项目路径、资源库元素文件(建议其他元素库文件路径在此处声明)
|
||||
ProjectPath = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
CodePath = os.path.join(ProjectPath, "codes")
|
||||
ModulePath = os.path.join(CodePath, "modules")
|
||||
ElementFile = os.path.join(ModulePath, "resources", "elements.json")
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
|
||||
# 更多示例代码 http://support.i-search.com.cn:8088/showdoc/web/#/page/11923
|
||||
|
||||
## 导入资源库
|
||||
|
||||
'''
|
||||
from iresource import iSResource
|
||||
|
||||
is_resource: iSResource = iSResource(__file__)
|
||||
|
||||
或者手动指定其他工程元素库文件(当前项目或其他项目)
|
||||
|
||||
ElementFile = r"~/NewProject1/codes/modules/resources/elements.json"
|
||||
is_resource: iSResource = iSResource(json_file=ElementFile)
|
||||
'''
|
||||
|
||||
|
||||
## 全部资源
|
||||
|
||||
'''
|
||||
all_resource = is_resource.all()
|
||||
'''
|
||||
|
||||
|
||||
## 获取全局变量、流程变量(参数)
|
||||
|
||||
'''
|
||||
gv1 = is_resource.get_global_var("gv_1")
|
||||
lv1 = is_resource.get_flow_var("lv_1")
|
||||
'''
|
||||
|
||||
|
||||
## 设置全局变量、流程变量(参数)
|
||||
|
||||
'''
|
||||
gv1 = is_resource.set_global_var("gv_1", "gv_1新值")
|
||||
lv1 = is_resource.set_flow_var("lv_1", "lv_1新值")
|
||||
'''
|
||||
|
||||
|
||||
## 获取元素对象(这段代码可从【元素库】中右击元素,点击【复制代码】直接获得)
|
||||
|
||||
'''
|
||||
element1 = is_resource.get_element("元素ID")
|
||||
element1.show() # 打印显示元素信息
|
||||
all_element = is_resource.elements() # 全部元素
|
||||
'''
|
||||
|
||||
|
||||
## 支持 By.ID,By.CODE,By.NAME 获取元素对象
|
||||
|
||||
'''
|
||||
is_resource.get_element(By.ID, "元素ID")
|
||||
is_resource.get_element(By.CODE, "元素Code")
|
||||
is_resource.get_element(By.NAME, "元素Name")
|
||||
|
||||
|
||||
## 也支持具体方法获取元素对象
|
||||
|
||||
'''
|
||||
is_resource.get_element_by_id("元素id")
|
||||
is_resource.get_element_by_id("元素Code")
|
||||
is_resource.get_element_by_id("元素Name")
|
||||
'''
|
||||
|
||||
|
||||
## 元素对象的 方法 和 属性
|
||||
|
||||
'''
|
||||
element1.actions() # 所有方法
|
||||
element1.properties() # 所有属性
|
||||
element1.path # 元素路径
|
||||
element1.snapshot # 元素快照
|
||||
element1.help("click") # 查看方法使用文档
|
||||
element1.click(wait_seconds=5)
|
||||
is_resource.set_action_wait_seconds(5) # 全局调整动作方法执行的等待时间秒数
|
||||
element1.set_text(value="测试一下")
|
||||
'''
|
||||
|
||||
## 对象化使用
|
||||
|
||||
'''
|
||||
# 使用对象库,通过iSObject对象可以调用子对象(如:键盘、窗口、Excel等)
|
||||
iSObject.Keyboard(element1.window_title).keypress_input("abc") # 模拟按键
|
||||
iSObject.Server().set_var("abc", "123") # 设置服务器变量
|
||||
'''
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"version": "2.0"
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
# coding=utf-8
|
||||
"""合同起草真实入口。Studio 编译不会改这个文件。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _repo_root() -> Path:
|
||||
for parent in Path(__file__).resolve().parents:
|
||||
if (parent / "rpa_yisaiqi" / "__init__.py").exists():
|
||||
return parent
|
||||
raise RuntimeError("找不到仓库根目录(缺少 rpa_yisaiqi/__init__.py)")
|
||||
|
||||
|
||||
_REPO_ROOT = _repo_root()
|
||||
if str(_REPO_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(_REPO_ROOT))
|
||||
|
||||
from rpa_yisaiqi.apa_browser_adapter import ApaBrowserAdapter # noqa: E402
|
||||
from rpa_yisaiqi.callback_client import StateCenterClient # noqa: E402
|
||||
from rpa_yisaiqi.contract_create_node import ( # noqa: E402
|
||||
ContractCreateError,
|
||||
build_spec_from_data,
|
||||
load_input_data,
|
||||
)
|
||||
from rpa_yisaiqi.contract_flow_steps import ( # noqa: E402
|
||||
enter_contract_create,
|
||||
fill_contract_amount_and_dates,
|
||||
fill_contract_basic_info,
|
||||
fill_contract_goods_lines,
|
||||
fill_contract_payment_info,
|
||||
fill_contract_signatory_info,
|
||||
login_contract_system,
|
||||
open_contract_system,
|
||||
read_contract_number,
|
||||
select_contract_template,
|
||||
submit_contract_for_approval,
|
||||
upload_contract_document_if_needed,
|
||||
)
|
||||
|
||||
|
||||
def _first(*values) -> str:
|
||||
for value in values:
|
||||
if value is not None and str(value).strip():
|
||||
return str(value).strip()
|
||||
return ""
|
||||
|
||||
|
||||
def _parse_input_arg(argv: list[str]) -> str:
|
||||
i = 0
|
||||
while i < len(argv):
|
||||
arg = argv[i]
|
||||
if arg in ("-i", "--input", "--input-json") and i + 1 < len(argv):
|
||||
return argv[i + 1]
|
||||
if arg.startswith("--input=") or arg.startswith("--input-json="):
|
||||
return arg.split("=", 1)[1]
|
||||
i += 1
|
||||
return ""
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print("合同起草流程启动", flush=True)
|
||||
|
||||
raw = _parse_input_arg(sys.argv[1:])
|
||||
if not raw:
|
||||
raw = os.getenv("CONTRACT_INPUT_JSON", "").strip()
|
||||
if not raw:
|
||||
raw = str(_REPO_ROOT / "rpa_yisaiqi" / "examples" / "contract_create_input_dry_run.json")
|
||||
print("未传入 -i/--input,使用默认联调入参:", raw, flush=True)
|
||||
|
||||
print("[S01 初始化]", flush=True)
|
||||
data = load_input_data(raw)
|
||||
state_center_url = _first(
|
||||
data.get("stateCenterUrl"),
|
||||
data.get("stateCenterURL"),
|
||||
os.getenv("STATE_CENTER_URL"),
|
||||
"http://192.168.105.69:8000",
|
||||
)
|
||||
agent_token = _first(data.get("agentToken"), os.getenv("AGENT_TOKEN"), "dev-agent-token")
|
||||
spec = build_spec_from_data(data)
|
||||
ctx = spec.flow_context
|
||||
if ctx is None:
|
||||
raise ContractCreateError("初始化失败:flow_context 为空")
|
||||
client = StateCenterClient(state_center_url, agent_token)
|
||||
browser = ApaBrowserAdapter(url=ctx.contract_url, flow="CONTRACT_CREATE")
|
||||
is_dry_run = spec.submit_mode == "dry_run"
|
||||
print(" jobId=", spec.job_id, " submitMode=", spec.submit_mode, flush=True)
|
||||
print(" contractDocumentPath=", ctx.contract_document_path or ctx.merged_pdf_path, flush=True)
|
||||
|
||||
if not is_dry_run:
|
||||
print("回调状态中心 start", spec.job_id, flush=True)
|
||||
client.start_node(spec.job_id, "CONTRACT_CREATE")
|
||||
|
||||
print("[S02 打开合同系统]", flush=True)
|
||||
open_contract_system(browser, ctx)
|
||||
|
||||
print("[S03 登录合同系统]", flush=True)
|
||||
login_contract_system(browser, ctx)
|
||||
|
||||
print("[S04 进入合同起草]", flush=True)
|
||||
enter_contract_create(browser)
|
||||
|
||||
print("[S05 选择模板]", flush=True)
|
||||
select_contract_template(browser, ctx)
|
||||
|
||||
print("[S06 填写签约方]", flush=True)
|
||||
fill_contract_signatory_info(browser, ctx)
|
||||
|
||||
print("[S07 填写基础信息]", flush=True)
|
||||
fill_contract_basic_info(browser, ctx)
|
||||
|
||||
print("[S08 填写金额日期]", flush=True)
|
||||
fill_contract_amount_and_dates(browser, ctx)
|
||||
|
||||
print("[S09 填写货品明细]", flush=True)
|
||||
fill_contract_goods_lines(browser, ctx)
|
||||
|
||||
print("[S10 填写付款信息]", flush=True)
|
||||
fill_contract_payment_info(browser, ctx)
|
||||
|
||||
print("[S11 上传合同PDF]", flush=True)
|
||||
upload_contract_document_if_needed(browser, ctx)
|
||||
|
||||
print("[S12 提交送审]", flush=True)
|
||||
submitted = submit_contract_for_approval(browser, ctx)
|
||||
|
||||
contract_no = ""
|
||||
if submitted:
|
||||
print("[S13 读取合同编号]", flush=True)
|
||||
contract_no = read_contract_number(browser, ctx)
|
||||
|
||||
print("[S14 结束]", flush=True)
|
||||
if not is_dry_run and contract_no:
|
||||
print("回调状态中心 complete", spec.job_id, contract_no, flush=True)
|
||||
client.complete_node(
|
||||
spec.job_id,
|
||||
"CONTRACT_CREATE",
|
||||
{"businessRefs": {"contractSystemNo": contract_no}},
|
||||
)
|
||||
print(json.dumps({"status": "done", "contractSystemNo": contract_no}, ensure_ascii=False), flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
# coding=utf-8
|
||||
"""APA 启动 Main.py 前会加载本文件。Studio 重新编译不会改这里。
|
||||
|
||||
Studio 点运行只带 -p,不带 -i。这里自动补上默认联调 JSON,
|
||||
避免 S01 因 input_arg 为空直接报「缺少业务入参」。
|
||||
账号过期把 Main.py 覆盖成空注释时,改走 run_contract.py。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def _codes_dir() -> str:
|
||||
return os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
def _default_input_path() -> str:
|
||||
repo = os.path.abspath(os.path.join(_codes_dir(), "..", "..", ".."))
|
||||
return os.path.join(repo, "rpa_yisaiqi", "examples", "contract_create_input_dry_run.json")
|
||||
|
||||
|
||||
def _is_apa_main() -> bool:
|
||||
if not sys.argv:
|
||||
return False
|
||||
return os.path.basename(str(sys.argv[0])).lower() == "main.py"
|
||||
|
||||
|
||||
def _argv_has_input(argv: list) -> bool:
|
||||
for i, arg in enumerate(argv):
|
||||
if arg in ("-i", "--input") and i + 1 < len(argv) and str(argv[i + 1]).strip():
|
||||
return True
|
||||
if str(arg).startswith("--input=") and str(arg).split("=", 1)[1].strip():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _main_is_expired_stub(path: str) -> bool:
|
||||
try:
|
||||
text = open(path, encoding="utf-8").read()
|
||||
except Exception:
|
||||
return False
|
||||
return "账号过期" in text
|
||||
|
||||
|
||||
# Windows 虚拟机只回调 Mac 状态中心,不在本机起库。
|
||||
os.environ.setdefault("STATE_CENTER_URL", "http://192.168.105.69:8000")
|
||||
os.environ.setdefault("AGENT_TOKEN", "dev-agent-token")
|
||||
os.environ.setdefault("RPA_CALLBACK_STATE_CENTER", "1")
|
||||
|
||||
if _is_apa_main():
|
||||
if _main_is_expired_stub(sys.argv[0]):
|
||||
print("检测到 Main.py 被艺赛旗覆盖(账号过期),改走独立入口", flush=True)
|
||||
if _codes_dir() not in sys.path:
|
||||
sys.path.insert(0, _codes_dir())
|
||||
from run_contract import main
|
||||
|
||||
raise SystemExit(main())
|
||||
|
||||
if not _argv_has_input(sys.argv):
|
||||
default_input = _default_input_path()
|
||||
if os.path.isfile(default_input):
|
||||
sys.argv.extend(["-i", default_input])
|
||||
print("未传入 -i,自动使用默认联调入参:", default_input, flush=True)
|
||||
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,33 @@
|
|||
from pathlib import Path
|
||||
|
||||
from rpa_yisaiqi.apa_browser_adapter import REPO_ROOT, resolve_chrome_profile_base
|
||||
|
||||
|
||||
def test_default_chrome_profile_is_outside_repo():
|
||||
profile = resolve_chrome_profile_base(requested="")
|
||||
assert profile == (REPO_ROOT.parent / "rpa-chrome-profile").resolve()
|
||||
assert REPO_ROOT.resolve() not in profile.parents
|
||||
assert profile != REPO_ROOT.resolve()
|
||||
|
||||
|
||||
def test_env_path_inside_repo_is_rejected(tmp_path, monkeypatch):
|
||||
inside = REPO_ROOT / "rpa_yisaiqi" / "runtime" / "chrome-profile"
|
||||
monkeypatch.delenv("RPA_CHROME_PROFILE", raising=False)
|
||||
profile = resolve_chrome_profile_base(requested=str(inside))
|
||||
assert profile == (REPO_ROOT.parent / "rpa-chrome-profile").resolve()
|
||||
assert not _is_relative_to(profile, REPO_ROOT)
|
||||
|
||||
|
||||
def test_env_path_outside_repo_is_kept(tmp_path):
|
||||
outside = tmp_path / "custom-chrome"
|
||||
outside.mkdir()
|
||||
profile = resolve_chrome_profile_base(requested=str(outside))
|
||||
assert profile == outside.resolve()
|
||||
|
||||
|
||||
def _is_relative_to(path: Path, root: Path) -> bool:
|
||||
try:
|
||||
path.resolve().relative_to(root.resolve())
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
from rpa_yisaiqi.runtime.accept import (
|
||||
AcceptanceReport,
|
||||
accept_log_path,
|
||||
artifact_dir,
|
||||
evidence_shot,
|
||||
field_match,
|
||||
)
|
||||
from rpa_yisaiqi.runtime.errors import NodeError
|
||||
import pytest
|
||||
|
||||
|
||||
def test_each_flow_writes_its_own_accept_file(tmp_path, monkeypatch):
|
||||
monkeypatch.delenv("RPA_ACCEPT_LOG", raising=False)
|
||||
monkeypatch.setattr(
|
||||
"rpa_yisaiqi.runtime.accept.SCREENSHOT_ROOT",
|
||||
tmp_path,
|
||||
)
|
||||
report = AcceptanceReport(flow="ORDER_FETCH")
|
||||
report.note("S01", "登录", {"user": "u1"}, ok=True)
|
||||
path = accept_log_path("ORDER_FETCH")
|
||||
assert path == tmp_path / "ORDER_FETCH" / "ACCEPT.txt"
|
||||
text = path.read_text(encoding="utf-8")
|
||||
assert "ACCEPT SUMMARY ORDER_FETCH 1/1 ok" in text
|
||||
assert "ACCEPT S01 ok 登录 user=u1" in text
|
||||
|
||||
|
||||
def test_artifact_dir_is_per_flow():
|
||||
assert artifact_dir("REBATE_HEADER").name == "REBATE_HEADER"
|
||||
assert artifact_dir("M5_CREATE_ORDER").name == "M5_CREATE_ORDER"
|
||||
assert artifact_dir("CONTRACT_CREATE") != artifact_dir("ORDER_FETCH")
|
||||
|
||||
|
||||
def test_check_fails_on_mismatch_without_reading_images():
|
||||
report = AcceptanceReport(flow="REBATE_HEADER")
|
||||
with pytest.raises(NodeError, match="ACCEPT S02 FAIL"):
|
||||
report.check("S02", "总单号", [("rebateOrderNo", "RB-1", "RB-2")])
|
||||
|
||||
|
||||
def test_evidence_shot_skips_success_and_keeps_failure():
|
||||
class Fake:
|
||||
def __init__(self):
|
||||
self.names = []
|
||||
|
||||
def screenshot(self, name):
|
||||
self.names.append(name)
|
||||
return name
|
||||
|
||||
browser = Fake()
|
||||
assert evidence_shot(browser, "ok") == ""
|
||||
assert browser.names == []
|
||||
assert evidence_shot(browser, "bad", failed=True) == "bad"
|
||||
assert browser.names == ["bad"]
|
||||
|
||||
|
||||
def test_field_match_accepts_numeric_and_yes_no():
|
||||
assert field_match("12.0", "12")
|
||||
assert field_match("是", "1")
|
||||
assert field_match("2026-08-11", "20260811")
|
||||
assert not field_match("重庆其他", "重庆亿昊药业有限公司")
|
||||
|
|
@ -37,6 +37,6 @@ class YiSaiQiBrowserAdapter:
|
|||
raise NotImplementedError(f"Map to YiSaiQi read-text activity: {selectors=} {value_name=}")
|
||||
|
||||
def screenshot(self, name: str) -> str:
|
||||
"""Flowchart node: 截图存证,返回文件路径."""
|
||||
"""Human evidence only. Acceptance is ACCEPT text from runtime.accept, not this image."""
|
||||
raise NotImplementedError(f"Map to YiSaiQi screenshot activity: {name=}")
|
||||
|
||||
|
|
|
|||
|
|
@ -3,13 +3,15 @@ from pathlib import Path
|
|||
|
||||
import pytest
|
||||
|
||||
from rpa_yisaiqi.apa_browser_adapter import ApaBrowserAdapter
|
||||
from rpa_yisaiqi.contract_create_node import (
|
||||
ContractCreateError,
|
||||
ContractCreateInput,
|
||||
PdfMergeUnsupported,
|
||||
build_spec_from_data,
|
||||
create_contract_in_contract_system,
|
||||
)
|
||||
from rpa_yisaiqi.contract_draft import load_contract_draft
|
||||
from rpa_yisaiqi.contract_draft import is_placeholder_customer_code, load_contract_draft
|
||||
from rpa_yisaiqi.pdf_utils import ensure_merged_pdf
|
||||
|
||||
|
||||
|
|
@ -77,6 +79,33 @@ def test_load_contract_draft_maps_full_contract_inputs(tmp_path):
|
|||
assert draft.goods_lines[0].goods_code == "YP-001"
|
||||
|
||||
|
||||
def test_placeholder_customer_code_is_rejected_before_opening_browser():
|
||||
assert is_placeholder_customer_code("CUST-YYC-001")
|
||||
assert is_placeholder_customer_code("test-001")
|
||||
assert not is_placeholder_customer_code("DW0100046361")
|
||||
|
||||
with pytest.raises(ContractCreateError, match="占位符"):
|
||||
build_spec_from_data(
|
||||
{
|
||||
"jobId": "JOB-1",
|
||||
"supplierName": "重庆亿昊药业有限公司",
|
||||
"orderDate": "2026-08-11",
|
||||
"sourceType": "XYY",
|
||||
"customerCode": "CUST-YYC-001",
|
||||
"customerName": "重庆亿昊药业有限公司",
|
||||
"amount": "100",
|
||||
"submitMode": "dry_run",
|
||||
"goodsLines": [
|
||||
{
|
||||
"goodsCode": "YP-001",
|
||||
"goodsName": "阿卡波糖片",
|
||||
"quantity": 1,
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_yyc_requires_pdf_inputs_before_contract_drafting(tmp_path):
|
||||
draft_path = tmp_path / "draft.json"
|
||||
draft_path.write_text(
|
||||
|
|
@ -111,14 +140,16 @@ def test_ensure_merged_pdf_uses_single_pdf_without_merge(tmp_path):
|
|||
assert ensure_merged_pdf([str(source)], str(tmp_path / "merged.pdf")) == str(source)
|
||||
|
||||
|
||||
def test_real_drafting_requires_yisaiqi_adapter_mapping(tmp_path):
|
||||
def test_create_contract_uses_apa_browser_adapter(tmp_path, monkeypatch):
|
||||
draft_path = tmp_path / "draft.json"
|
||||
draft_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"supplierName": "小药药供应商",
|
||||
"supplierName": "重庆亿昊药业有限公司",
|
||||
"orderDate": "2026-08-11",
|
||||
"sourceType": "XYY",
|
||||
"customerCode": "DW0100046361",
|
||||
"customerName": "重庆亿昊药业有限公司",
|
||||
"goodsLines": [],
|
||||
"platformPdfPaths": [],
|
||||
},
|
||||
|
|
@ -128,11 +159,22 @@ def test_real_drafting_requires_yisaiqi_adapter_mapping(tmp_path):
|
|||
)
|
||||
spec = ContractCreateInput(
|
||||
job_id="JOB-1",
|
||||
supplier_name="小药药供应商",
|
||||
supplier_name="重庆亿昊药业有限公司",
|
||||
order_date="2026-08-11",
|
||||
source_type="XYY",
|
||||
draft_path=str(draft_path),
|
||||
)
|
||||
captured = {}
|
||||
|
||||
with pytest.raises(ContractCreateError, match="YiSaiQiBrowserAdapter"):
|
||||
create_contract_in_contract_system(spec)
|
||||
def fake_run(browser, ctx):
|
||||
captured["browser"] = browser
|
||||
captured["ctx"] = ctx
|
||||
return "HT-1"
|
||||
|
||||
monkeypatch.setattr(
|
||||
"rpa_yisaiqi.contract_create_node.run_contract_create_flow",
|
||||
fake_run,
|
||||
)
|
||||
|
||||
assert create_contract_in_contract_system(spec) == "HT-1"
|
||||
assert isinstance(captured["browser"], ApaBrowserAdapter)
|
||||
|
|
|
|||
|
|
@ -1,110 +1,269 @@
|
|||
import pytest
|
||||
|
||||
from rpa_yisaiqi.contract_flow_steps import (
|
||||
ContractFlowContext,
|
||||
ContractGoodsLine,
|
||||
fill_contract_signatory_info,
|
||||
run_contract_create_flow,
|
||||
)
|
||||
from rpa_yisaiqi.runtime.errors import SupplierContractDisabled
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def no_sleep(monkeypatch):
|
||||
monkeypatch.setattr("rpa_yisaiqi.contract_flow_steps.time.sleep", lambda _s: None)
|
||||
|
||||
|
||||
class FakeBrowser:
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
self.body_text = ""
|
||||
self.xform_labels = {}
|
||||
self.filled = {}
|
||||
self.read_values = {
|
||||
"contract_number": "HT-20260811-REAL-001",
|
||||
}
|
||||
|
||||
def open_url(self, url):
|
||||
self.calls.append(("open_url", url))
|
||||
if "kmAgreementApply" in url or "draft.jsp" in url:
|
||||
self.body_text = "客户编码 客户名称 合同类别"
|
||||
|
||||
def fill_first(self, selectors, value):
|
||||
self.calls.append(("fill_first", tuple(selectors), value))
|
||||
for selector in selectors:
|
||||
self.filled[selector] = value
|
||||
|
||||
def click_native(self, selectors):
|
||||
self.calls.append(("click_native", tuple(selectors)))
|
||||
|
||||
def click_first(self, selectors):
|
||||
self.calls.append(("click_first", tuple(selectors)))
|
||||
joined = " ".join(str(item) for item in selectors)
|
||||
if "login" in joined or "登录" in joined:
|
||||
self.body_text = "切换门户 待办 待阅 合同管理 合同起草"
|
||||
|
||||
def wait_for_text(self, text, timeout_seconds=15):
|
||||
self.calls.append(("wait_for_text", text, timeout_seconds))
|
||||
if text and text not in self.body_text:
|
||||
self.body_text = f"{self.body_text} {text}".strip()
|
||||
|
||||
def wait_for_element(self, selectors, timeout_seconds=15):
|
||||
self.calls.append(("wait_for_element", tuple(selectors), timeout_seconds))
|
||||
|
||||
def element_exists(self, selectors):
|
||||
self.calls.append(("element_exists", tuple(selectors)))
|
||||
return True
|
||||
|
||||
def switch_to_frame(self, selectors):
|
||||
self.calls.append(("switch_to_frame", tuple(selectors)))
|
||||
|
||||
def switch_to_default_content(self):
|
||||
self.calls.append(("switch_to_default_content",))
|
||||
|
||||
def select_first(self, selectors, value):
|
||||
self.calls.append(("select_first", tuple(selectors), value))
|
||||
for selector in selectors:
|
||||
self.filled[selector] = value
|
||||
|
||||
def select_xform(self, name, value):
|
||||
self.calls.append(("select_xform", name, value))
|
||||
self.xform_labels[name] = value
|
||||
|
||||
def execute_script(self, script, *args):
|
||||
self.calls.append(("execute_script", script[:48], args))
|
||||
if "hasTextInFrames" in script:
|
||||
return True
|
||||
if "latestByApplyTime" in script and "headerIdx" in script:
|
||||
return "001132026081734940"
|
||||
if "wantedTitle" in script and "headerIdx" in script:
|
||||
return "001132026081734940"
|
||||
if "fd_opcode" in script and "return i" in script:
|
||||
return 0
|
||||
if "List_Selected" in script and "innerText" in script:
|
||||
token = str(args[0]) if args else ""
|
||||
if token.startswith("DW"):
|
||||
self.filled['input[name="extendDataFormInfo.value(fd_customeropcode)"]'] = token
|
||||
elif token:
|
||||
self.filled['input[name="extendDataFormInfo.value(fd_detail1.0.fd_opcode)"]'] = token
|
||||
if "box.click" in script:
|
||||
return {"ok": True}
|
||||
return True
|
||||
if "marked" in script and "fd_detail1" in script:
|
||||
return []
|
||||
if "return location.href" in script:
|
||||
return "http://contract-test.gzmpc.com:8080/km/agreement/km_agreement_apply/kmAgreementApply.do?method=view"
|
||||
if "Com_Parameter" in script and "fdId" in script:
|
||||
return "FD-TEST-001"
|
||||
if "添加合同" in script and ("pdf" in script or "wantedStem" in script):
|
||||
if any(call[0] == "upload_file_first" for call in self.calls):
|
||||
return {"ok": True, "via": "name"}
|
||||
return {"ok": False, "via": "empty"}
|
||||
if "提交" in script or "合同正文" in script or "确定" in script:
|
||||
return {"ok": True}
|
||||
if "fs-label" in script and args:
|
||||
return self.xform_labels.get(args[0], "")
|
||||
if "data-value" in script:
|
||||
return "模板A"
|
||||
return []
|
||||
|
||||
def upload_file_first(self, selectors, file_path):
|
||||
self.calls.append(("upload_file_first", tuple(selectors), file_path))
|
||||
|
||||
def read_first(self, selectors, value_name):
|
||||
self.calls.append(("read_first", tuple(selectors), value_name))
|
||||
if value_name in self.read_values:
|
||||
return self.read_values[value_name]
|
||||
for selector in selectors:
|
||||
if selector in self.filled:
|
||||
return self.filled[selector]
|
||||
joined = " ".join(str(item) for item in selectors)
|
||||
if "fd_customername" in joined:
|
||||
return self.read_values.get("customer_name", "重庆亿昊药业有限公司")
|
||||
if "fd_customeropcode" in joined:
|
||||
return self.read_values.get("customer_code", self.filled.get(
|
||||
'input[name="extendDataFormInfo.value(fd_customeropcode)"]', ""
|
||||
))
|
||||
if "fdSettype" in joined:
|
||||
return self.read_values.get("settlement_type", "付款")
|
||||
return ""
|
||||
|
||||
def read_body_text(self):
|
||||
self.calls.append(("read_body_text",))
|
||||
return self.body_text
|
||||
|
||||
def screenshot(self, name):
|
||||
self.calls.append(("screenshot", name))
|
||||
return f"/tmp/{name}.png"
|
||||
|
||||
|
||||
def test_signatory_rejects_placeholder_customer_before_opening_picker():
|
||||
browser = FakeBrowser()
|
||||
ctx = ContractFlowContext(
|
||||
contract_url="http://contract-test.gzmpc.com:8080/",
|
||||
username="test002",
|
||||
password="1",
|
||||
supplier_name="重庆亿昊药业有限公司",
|
||||
order_date="2026-08-11",
|
||||
source_type="YYC",
|
||||
customer_code="CUST-YYC-001",
|
||||
customer_name="重庆亿昊药业有限公司",
|
||||
)
|
||||
|
||||
with pytest.raises(SupplierContractDisabled, match="占位符"):
|
||||
fill_contract_signatory_info(browser, ctx)
|
||||
|
||||
assert browser.calls == []
|
||||
|
||||
|
||||
def test_contract_flow_logs_in_with_real_contract_login_selectors():
|
||||
browser = FakeBrowser()
|
||||
ctx = ContractFlowContext(
|
||||
contract_url="http://contract-test.gzmpc.com:8080/",
|
||||
username="test002",
|
||||
password="1",
|
||||
supplier_name="合同节点联调供应商",
|
||||
supplier_name="重庆亿昊药业有限公司",
|
||||
order_date="2026-08-11",
|
||||
source_type="XYY",
|
||||
template_type="FORMAT",
|
||||
customer_code="DW0100046361",
|
||||
customer_name="重庆亿昊药业有限公司",
|
||||
goods_lines=[],
|
||||
)
|
||||
|
||||
run_contract_create_flow(browser, ctx)
|
||||
|
||||
assert browser.calls[:5] == [
|
||||
("open_url", "http://contract-test.gzmpc.com:8080/"),
|
||||
("fill_first", ('input[name="j_username"]', 'input[placeholder="用户名"]'), "test002"),
|
||||
("fill_first", ('input[name="j_password"]', 'input[placeholder="密码"]'), "1"),
|
||||
("click_first", ('input[name="btn_submit"]', 'a[href*="btn_submit"]', 'text=登录')),
|
||||
("wait_for_text", "合同", 20),
|
||||
]
|
||||
assert ("open_url", "http://contract-test.gzmpc.com:8080/") in browser.calls
|
||||
assert ("fill_first", ('input[name="j_username"]', 'input[placeholder="用户名"]'), "test002") in browser.calls
|
||||
assert ("fill_first", ('input[name="j_password"]', 'input[placeholder="密码"]'), "1") in browser.calls
|
||||
assert (
|
||||
"click_first",
|
||||
('a[class*="login_button"]', 'a:contains("登录")', 'input[name="btn_submit"]'),
|
||||
) in browser.calls
|
||||
assert not [call for call in browser.calls if call[0] == "screenshot"]
|
||||
|
||||
|
||||
def test_contract_flow_uses_order_date_and_uploads_yyc_merged_pdf():
|
||||
def _touch_pdf(path):
|
||||
path.write_bytes(b"%PDF-1.4\n1 0 obj<<>>endobj\ntrailer<<>>\n%%EOF\n")
|
||||
return str(path)
|
||||
|
||||
|
||||
def test_contract_flow_uses_order_date_and_uploads_yyc_merged_pdf(tmp_path):
|
||||
browser = FakeBrowser()
|
||||
browser.read_values["customer_name"] = "重庆亿昊药业有限公司"
|
||||
pdf_path = _touch_pdf(tmp_path / "contract-merged.pdf")
|
||||
ctx = ContractFlowContext(
|
||||
contract_url="http://contract-test.gzmpc.com:8080/",
|
||||
username="test002",
|
||||
password="1",
|
||||
supplier_name="合同节点联调供应商",
|
||||
supplier_name="重庆亿昊药业有限公司",
|
||||
order_date="2026-08-11",
|
||||
source_type="YYC",
|
||||
template_type="FORMAT",
|
||||
customer_code="DW0100046361",
|
||||
customer_name="重庆亿昊药业有限公司",
|
||||
goods_lines=[
|
||||
ContractGoodsLine(
|
||||
operation_code="ADD",
|
||||
goods_code="YP-001",
|
||||
goods_name="阿卡波糖片",
|
||||
approval_no="国药准字H00000001",
|
||||
spec="50mg*30片",
|
||||
goods_code="FQX0100118047",
|
||||
goods_name="盐酸托烷司琼注射液",
|
||||
approval_no="苏20160372",
|
||||
spec="5ml:5mg*5支",
|
||||
unit="盒",
|
||||
quantity=12,
|
||||
unit_price="50.00",
|
||||
)
|
||||
],
|
||||
merged_pdf_path="C:/rpa/jobs/JOB-1/contract-merged.pdf",
|
||||
merged_pdf_path=pdf_path,
|
||||
)
|
||||
|
||||
contract_no = run_contract_create_flow(browser, ctx)
|
||||
|
||||
assert contract_no == "HT-20260811-REAL-001"
|
||||
assert ("fill_first", ('label=合同签订日期', 'input[name="fdSignDate"]', 'input[placeholder*="合同签订"]'), "2026-08-11") in browser.calls
|
||||
assert ("fill_first", ('label=合同开始日期', 'input[name="fdStartDate"]', 'input[placeholder*="合同开始"]'), "2026-08-11") in browser.calls
|
||||
assert ("upload_file_first", ('input[type="file"]', 'label=合同证文上传'), "C:/rpa/jobs/JOB-1/contract-merged.pdf") in browser.calls
|
||||
assert contract_no == "001132026081734940"
|
||||
assert ("fill_first", ('input[name="fdSigndate"]',), "2026-08-11") in browser.calls
|
||||
assert ("fill_first", ('input[name="fdEffedate"]',), "2026-08-11") in browser.calls
|
||||
assert (
|
||||
"click_first",
|
||||
('div[title="选择商品"]', "div.lui-component.lui_widget_btn_txt", "text=选择商品"),
|
||||
) in browser.calls
|
||||
assert (
|
||||
"switch_to_frame",
|
||||
(
|
||||
'iframe[src*="EopBasedataMaterial"]',
|
||||
'iframe[src*="dialog_fdMaterial"]',
|
||||
'iframe[src*="common-template"]',
|
||||
),
|
||||
) in browser.calls
|
||||
assert ("fill_first", ("input.search-box-text", "input[placeholder*='关键字']"), "FQX0100118047") in browser.calls
|
||||
assert (
|
||||
"fill_first",
|
||||
('input[name="extendDataFormInfo.value(fd_detail1.0.fd_goodsqty)"]',),
|
||||
"12",
|
||||
) in browser.calls
|
||||
assert (
|
||||
"fill_first",
|
||||
('input[name="extendDataFormInfo.value(fd_detail1.0.fd_unitprice)"]',),
|
||||
"50.00",
|
||||
) in browser.calls
|
||||
assert any(
|
||||
call[0] == "upload_file_first" and call[-1] == pdf_path
|
||||
for call in browser.calls
|
||||
)
|
||||
|
||||
|
||||
def test_contract_flow_skips_pdf_upload_for_xyy():
|
||||
browser = FakeBrowser()
|
||||
browser.read_values["customer_name"] = "重庆亿昊药业有限公司"
|
||||
ctx = ContractFlowContext(
|
||||
contract_url="http://contract-test.gzmpc.com:8080/",
|
||||
username="test002",
|
||||
password="1",
|
||||
supplier_name="小药药供应商",
|
||||
supplier_name="重庆亿昊药业有限公司",
|
||||
order_date="2026-08-11",
|
||||
source_type="XYY",
|
||||
template_type="FORMAT",
|
||||
customer_code="DW0100046361",
|
||||
customer_name="重庆亿昊药业有限公司",
|
||||
goods_lines=[],
|
||||
merged_pdf_path="C:/should-not-upload.pdf",
|
||||
)
|
||||
|
|
@ -114,45 +273,46 @@ def test_contract_flow_skips_pdf_upload_for_xyy():
|
|||
assert not [call for call in browser.calls if call[0] == "upload_file_first"]
|
||||
|
||||
|
||||
def test_contract_flow_enters_real_draft_menu_and_recent_template():
|
||||
def test_contract_flow_enters_draft_form_by_template_url(tmp_path):
|
||||
from rpa_yisaiqi.contract_flow_steps import FORM_PAGE_URL
|
||||
|
||||
browser = FakeBrowser()
|
||||
browser.read_values["customer_name"] = "重庆亿昊药业有限公司"
|
||||
ctx = ContractFlowContext(
|
||||
contract_url="http://contract-test.gzmpc.com:8080/",
|
||||
username="test002",
|
||||
password="1",
|
||||
supplier_name="重庆亿昊药业",
|
||||
supplier_name="重庆亿昊药业有限公司",
|
||||
order_date="2026-08-11",
|
||||
source_type="YYC",
|
||||
template_type="NON_FORMAT",
|
||||
template_name="非格式--采购协议",
|
||||
customer_code="DW0100046361",
|
||||
customer_name="重庆亿昊药业有限公司",
|
||||
goods_lines=[],
|
||||
merged_pdf_path="C:/rpa/jobs/JOB-1/contract-merged.pdf",
|
||||
merged_pdf_path=_touch_pdf(tmp_path / "contract-merged.pdf"),
|
||||
)
|
||||
|
||||
run_contract_create_flow(browser, ctx)
|
||||
|
||||
assert ("click_first", ("text=合同管理", "aria=合同管理")) in browser.calls
|
||||
assert ("click_first", ("text=合同起草", "aria=合同起草")) in browser.calls
|
||||
assert ("wait_for_text", "最近使用模板", 20) in browser.calls
|
||||
assert (
|
||||
"click_first",
|
||||
("text=非格式--采购协议", "text=非格式--采购协议...", "title=非格式--采购协议"),
|
||||
) in browser.calls
|
||||
assert ("open_url", FORM_PAGE_URL) in browser.calls
|
||||
assert ("wait_for_text", "客户编码", 20) in browser.calls
|
||||
|
||||
|
||||
def test_contract_flow_fills_real_contract_draft_form_fields_from_screenshot():
|
||||
browser = FakeBrowser()
|
||||
browser.read_values["customer_name"] = "重庆亿昊药业有限公司"
|
||||
ctx = ContractFlowContext(
|
||||
contract_url="http://contract-test.gzmpc.com:8080/",
|
||||
username="test002",
|
||||
password="1",
|
||||
supplier_name="重庆亿昊药业",
|
||||
supplier_name="重庆亿昊药业有限公司",
|
||||
order_date="2026-08-11",
|
||||
source_type="XYY",
|
||||
template_type="NON_FORMAT",
|
||||
template_name="非格式--采购协议",
|
||||
customer_code="CUST-001",
|
||||
customer_name="重庆亿昊药业",
|
||||
customer_code="DW0100046361",
|
||||
customer_name="重庆亿昊药业有限公司",
|
||||
contract_category="采购协议",
|
||||
settlement_type="付款",
|
||||
contract_background="按订单自动起草采购协议",
|
||||
|
|
@ -172,37 +332,53 @@ def test_contract_flow_fills_real_contract_draft_form_fields_from_screenshot():
|
|||
|
||||
run_contract_create_flow(browser, ctx)
|
||||
|
||||
assert ("fill_first", ('label=客户编码', 'input[name="fdCustomerCode"]', 'input[placeholder*="客户编码"]'), "CUST-001") in browser.calls
|
||||
assert ("fill_first", ('label=客户名称', 'input[name="fdCustomerName"]', 'input[placeholder*="客户名称"]'), "重庆亿昊药业") in browser.calls
|
||||
assert ("select_first", ('label=合同类别', 'select[name="fdContractCategory"]'), "采购协议") in browser.calls
|
||||
assert ("select_first", ('label=结算类型', 'select[name="fdSettlementType"]'), "付款") in browser.calls
|
||||
assert ("fill_first", ('label=合同背景概述', 'textarea[name="fdContractBackground"]'), "按订单自动起草采购协议") in browser.calls
|
||||
assert ("click_first", ("label=主合同", "text=主合同")) in browser.calls
|
||||
assert ("select_first", ('label=合同金额', 'select[name="fdCurrency"]'), "人民币") in browser.calls
|
||||
assert ("fill_first", ('label=金额(元)', 'input[name="fdAmount"]'), "12000.50") in browser.calls
|
||||
assert ("fill_first", ('label=合同签订日期', 'input[name="fdSignDate"]', 'input[placeholder*="合同签订"]'), "2026-08-11") in browser.calls
|
||||
assert ("fill_first", ('label=合同开始日期', 'input[name="fdStartDate"]', 'input[placeholder*="合同开始"]'), "2026-08-11") in browser.calls
|
||||
assert ("fill_first", ('label=合同结束日期', 'input[name="fdEndDate"]', 'input[placeholder*="合同结束"]'), "2026-12-31") in browser.calls
|
||||
assert ("select_first", ('label=是否重大合同', 'select[name="fdIsMajorContract"]'), "否") in browser.calls
|
||||
assert ("select_first", ('label=是否预付款', 'select[name="fdIsPrepayment"]'), "否") in browser.calls
|
||||
assert ("fill_first", ('label=合同归属年份', 'input[name="fdBelongYear"]'), "2026") in browser.calls
|
||||
assert ("select_first", ('label=付款方式', 'select[name="fdPaymentMethod"]', 'select[name="paymentMethod"]'), "账期支付") in browser.calls
|
||||
assert ("fill_first", ('label=付款期限', 'input[name="fdPaymentDays"]', 'input[name="paymentDays"]'), "30") in browser.calls
|
||||
assert ("select_first", ('label=合同状态', 'select[name="fdContractStatus"]'), "正式合同") in browser.calls
|
||||
assert ("select_first", ('label=是否包含血液制品', 'select[name="fdContainsBloodProducts"]'), "否") in browser.calls
|
||||
assert (
|
||||
"select_xform",
|
||||
"extendDataFormInfo.value(fd_contract_cate_type)",
|
||||
"其他协议(采购)",
|
||||
) in browser.calls
|
||||
assert ("select_first", ('select#fdSettype',), "付款") in browser.calls
|
||||
assert (
|
||||
"fill_first",
|
||||
('textarea[name="extendDataFormInfo.value(fd_desc_info)"]',),
|
||||
"按订单自动起草采购协议",
|
||||
) in browser.calls
|
||||
assert ("select_xform", "extendDataFormInfo.value(fd_all_amount_type)", "合同标的金额") in browser.calls
|
||||
assert ("fill_first", ('input[name="fdAccount"]',), "12000.50") in browser.calls
|
||||
assert ("fill_first", ('input[name="fdSigndate"]',), "2026-08-11") in browser.calls
|
||||
assert ("fill_first", ('input[name="fdEffedate"]',), "2026-08-11") in browser.calls
|
||||
assert ("fill_first", ('input[name="fdEnddate"]',), "2026-12-31") in browser.calls
|
||||
assert ("select_xform", "extendDataFormInfo.value(fd_is_major_contract)", "否") in browser.calls
|
||||
assert ("select_xform", "extendDataFormInfo.value(fd_advance_amout)", "否") in browser.calls
|
||||
assert (
|
||||
"fill_first",
|
||||
('input[name="extendDataFormInfo.value(fd_contract_year_text)"]',),
|
||||
"2026",
|
||||
) in browser.calls
|
||||
assert ("select_xform", "extendDataFormInfo.value(fd_paytype)", "电汇") in browser.calls
|
||||
assert (
|
||||
"fill_first",
|
||||
('input[name="extendDataFormInfo.value(fd_pay_time_limit)"]',),
|
||||
"30",
|
||||
) in browser.calls
|
||||
assert ("select_xform", "extendDataFormInfo.value(fd_suconstatus)", "正式合同") in browser.calls
|
||||
assert ("select_xform", "extendDataFormInfo.value(fd_3d1ce3953f1162)", "否") in browser.calls
|
||||
|
||||
|
||||
def test_contract_flow_dry_run_skips_submit_but_still_reads_page_state():
|
||||
browser = FakeBrowser()
|
||||
browser.read_values["customer_name"] = "重庆亿昊药业有限公司"
|
||||
ctx = ContractFlowContext(
|
||||
contract_url="http://contract-test.gzmpc.com:8080/",
|
||||
username="test002",
|
||||
password="1",
|
||||
supplier_name="重庆亿昊药业",
|
||||
supplier_name="重庆亿昊药业有限公司",
|
||||
order_date="2026-08-11",
|
||||
source_type="XYY",
|
||||
template_type="NON_FORMAT",
|
||||
template_name="非格式--采购协议",
|
||||
customer_code="DW0100046361",
|
||||
customer_name="重庆亿昊药业有限公司",
|
||||
submit_mode="dry_run",
|
||||
goods_lines=[],
|
||||
)
|
||||
|
|
@ -210,6 +386,105 @@ def test_contract_flow_dry_run_skips_submit_but_still_reads_page_state():
|
|||
contract_no = run_contract_create_flow(browser, ctx)
|
||||
|
||||
assert not [call for call in browser.calls if call[0] == "click_first" and "text=提交" in call[1]]
|
||||
assert ("screenshot", "contract-dry-run-ready") in browser.calls
|
||||
assert not [call for call in browser.calls if call[0] == "screenshot"]
|
||||
assert contract_no == ""
|
||||
assert not [call for call in browser.calls if call[0] == "read_first"]
|
||||
|
||||
|
||||
def test_contract_flow_writes_text_accept_report_instead_of_requiring_screenshots(tmp_path, monkeypatch):
|
||||
from rpa_yisaiqi.contract_flow_steps import _ACCEPT, accept_log_path
|
||||
|
||||
monkeypatch.setenv("RPA_ACCEPT_LOG", str(tmp_path / "ACCEPT.txt"))
|
||||
browser = FakeBrowser()
|
||||
browser.read_values["customer_name"] = "重庆亿昊药业有限公司"
|
||||
ctx = ContractFlowContext(
|
||||
contract_url="http://contract-test.gzmpc.com:8080/",
|
||||
username="test002",
|
||||
password="1",
|
||||
supplier_name="重庆亿昊药业有限公司",
|
||||
order_date="2026-08-11",
|
||||
source_type="XYY",
|
||||
template_type="NON_FORMAT",
|
||||
customer_code="DW0100046361",
|
||||
customer_name="重庆亿昊药业有限公司",
|
||||
submit_mode="dry_run",
|
||||
goods_lines=[],
|
||||
)
|
||||
|
||||
run_contract_create_flow(browser, ctx)
|
||||
|
||||
text = accept_log_path("CONTRACT_CREATE").read_text(encoding="utf-8")
|
||||
assert "ACCEPT SUMMARY" in text
|
||||
assert "ACCEPT S03 ok" in text
|
||||
assert "ACCEPT S06 ok" in text
|
||||
assert "customerName=重庆亿昊药业有限公司" in text
|
||||
assert "ACCEPT S12 ok" in text
|
||||
assert all(line.ok for line in _ACCEPT.lines)
|
||||
|
||||
|
||||
def test_read_contract_number_falls_back_to_my_contracts_list():
|
||||
from rpa_yisaiqi.contract_flow_steps import read_contract_number
|
||||
|
||||
browser = FakeBrowser()
|
||||
browser.read_values.pop("contract_number", None)
|
||||
ctx = ContractFlowContext(
|
||||
contract_url="http://contract-test.gzmpc.com:8080/",
|
||||
username="test002",
|
||||
password="1",
|
||||
supplier_name="重庆亿昊药业有限公司",
|
||||
order_date="2026-08-11",
|
||||
source_type="XYY",
|
||||
customer_code="DW0100046361",
|
||||
customer_name="重庆亿昊药业有限公司",
|
||||
contract_title="test1",
|
||||
goods_lines=[],
|
||||
)
|
||||
|
||||
contract_no = read_contract_number(browser, ctx)
|
||||
|
||||
assert contract_no == "001132026081734940"
|
||||
assert ("open_url", "http://contract-test.gzmpc.com:8080/") in browser.calls
|
||||
assert ("click_first", ("text=我的合同",)) in browser.calls
|
||||
assert ("click_first", ("text=我起草的",)) in browser.calls
|
||||
assert ("click_first", ("text=申请时间",)) in browser.calls
|
||||
|
||||
|
||||
def test_read_contract_number_goes_to_my_list_even_without_ctx_arg():
|
||||
from rpa_yisaiqi import _flow_state as state
|
||||
from rpa_yisaiqi.contract_flow_steps import read_contract_number
|
||||
|
||||
browser = FakeBrowser()
|
||||
browser.read_values.pop("contract_number", None)
|
||||
old_ctx = state.ctx
|
||||
state.ctx = ContractFlowContext(
|
||||
contract_url="http://contract-test.gzmpc.com:8080/",
|
||||
username="test002",
|
||||
password="1",
|
||||
supplier_name="重庆亿昊药业有限公司",
|
||||
order_date="2026-08-11",
|
||||
source_type="XYY",
|
||||
customer_code="DW0100046361",
|
||||
customer_name="重庆亿昊药业有限公司",
|
||||
contract_title="test1",
|
||||
goods_lines=[],
|
||||
)
|
||||
try:
|
||||
contract_no = read_contract_number(browser)
|
||||
finally:
|
||||
state.ctx = old_ctx
|
||||
|
||||
assert contract_no == "001132026081734940"
|
||||
assert ("click_first", ("text=我的合同",)) in browser.calls
|
||||
assert ("click_first", ("text=申请时间",)) in browser.calls
|
||||
|
||||
|
||||
def test_accept_check_fails_when_page_value_mismatches():
|
||||
from rpa_yisaiqi.contract_flow_steps import AcceptanceReport
|
||||
from rpa_yisaiqi.runtime.errors import NodeError
|
||||
|
||||
report = AcceptanceReport()
|
||||
with pytest.raises(NodeError, match="ACCEPT S06 FAIL"):
|
||||
report.check(
|
||||
"S06",
|
||||
"签约方",
|
||||
[("customerName", "其他公司", "重庆亿昊药业有限公司")],
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in New Issue