AutoRpa/rpa_yisaiqi/build_multinode_seq.py

359 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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()