diff --git a/Project.pro b/Project.pro index df171da..e056416 100644 --- a/Project.pro +++ b/Project.pro @@ -5,7 +5,7 @@ "ProInfo": { "CodeCustomCount": "0", "ProAbsolutePath": "D:/RPA/AutoRpa", - "ProChange": "2026/08/13 10:38:45", + "ProChange": "2026/08/13 11:38:45", "ProColorRef": "4", "ProCreate": "2026/08/12 10:13:31", "ProDesc": "", diff --git a/build_seq.py b/build_seq.py index 0097384..df30794 100644 --- a/build_seq.py +++ b/build_seq.py @@ -1,6 +1,6 @@ # coding=utf-8 """ -构建 Main.seq - 合同建单流程(Chrome浏览器版本) +构建 Main.seq - 合同建单流程(输入驱动 + 状态中心回调版本) """ import json import base64 @@ -11,150 +11,67 @@ def b64_apa(val): def b64_raw(val): return base64.b64encode(str(val).encode()).decode() -s01_body = """\ -import sys, os, json, time +# 单节点代码块:读取入参(input_arg / -c params / JSON 文件)→ 调用 run_contract_create +# run_contract_create 内部完成 start/complete/fail 回调、Selenium 起草与合同编号读取。 +body = """\ +import sys, os, json from pathlib import Path _CODES_DIR = Path(r'd:\\RPA\\AutoRpa\\codes') if str(_CODES_DIR) not in sys.path: sys.path.insert(0, str(_CODES_DIR)) -from rpa_yisaiqi.contract_flow_steps import ContractFlowContext, ContractGoodsLine - -print("=" * 60) -print("合同建单 CONTRACT_CREATE - 初始化") -print("=" * 60) - -self._ctx = ContractFlowContext( - contract_url="http://contract-test.gzmpc.com:8080/", - username="test002", password="1", - supplier_name="测试供应商", - order_date="2026-08-12", - source_type="YYC", template_type="NON_FORMAT", - template_name="非格式--采购协议", - customer_code="CUST-001", customer_name="测试供应商", - contract_category="采购协议", settlement_type="付款", - contract_background="自动化测试合同起草", contract_nature="主合同", - amount_currency="人民币", amount="50000.00", - contract_end_date="2026-12-31", - is_major_contract="否", is_prepayment="否", - belong_year="2026", payment_method="账期支付", - payment_days="30", contract_status="正式合同", - contains_blood_products="否", - submit_mode="dry_run", - goods_lines=[ - ContractGoodsLine( - operation_code="ADD", goods_code="YP-001", - goods_name="测试药品A", approval_no="国药准字H00000001", - spec="50mg*30片", unit="盒", quantity=10.0, - ), - ], - merged_pdf_path=None, +from rpa_yisaiqi.contract_create_node import ( + ContractCreateError, load_input_data, run_contract_create, ) -print(f"[OK] 上下文就绪: source={self._ctx.source_type}") -print(f"[OK] 模板: {self._ctx.template_name}") -print(f"[OK] 货品行数: {len(self._ctx.goods_lines)}") -""" +def _first(*values): + for value in values: + if value is not None and str(value).strip(): + return str(value).strip() + return "" -s02_body = """\ -from rpa_yisaiqi.apa_browser_adapter import ApaBrowserAdapter - -print("[S02] 启动 Chrome + 打开合同系统...") -print(f" URL: {self._ctx.contract_url}") -adapter = ApaBrowserAdapter(url=self._ctx.contract_url) -adapter.open_url(self._ctx.contract_url) -self._adapter = adapter -print("[OK] Chrome 已打开并导航到合同系统") -""" - -s03_body = """\ -from rpa_yisaiqi.contract_flow_steps import login_contract_system, enter_contract_create - -adapter = self._adapter -print("[S03] 登录合同系统...") -login_contract_system(adapter, self._ctx) -print("[OK] 登录完成") -print("[S04] 进入合同起草...") -enter_contract_create(adapter) -print("[OK] 已进入合同起草页面") -""" - -s04_body = """\ -from rpa_yisaiqi.contract_flow_steps import select_contract_template, fill_contract_signatory_info - -adapter = self._adapter - -print("[S05] 选择合同模板...") -select_contract_template(adapter, self._ctx) -print(f"[OK] 模板: {self._ctx.template_name}") -print("[S06] 填写签约方信息...") -fill_contract_signatory_info(adapter, self._ctx) -print(f"[OK] 签约方: {self._ctx.customer_name}") -""" - -s05_body = """\ -from rpa_yisaiqi.contract_flow_steps import fill_contract_basic_info, fill_contract_amount_and_dates - -adapter = self._adapter - -print("[S07] 填写基础表单...") -fill_contract_basic_info(adapter, self._ctx) -print(f"[OK] 类别={self._ctx.contract_category}, 结算={self._ctx.settlement_type}") -print("[S08] 填写金额与日期...") -fill_contract_amount_and_dates(adapter, self._ctx) -print(f"[OK] 金额={self._ctx.amount}, 结束日期={self._ctx.contract_end_date}") -""" - -s06_body = """\ -from rpa_yisaiqi.contract_flow_steps import ( - fill_contract_goods_lines, fill_contract_payment_info, - upload_contract_document_if_needed, submit_contract_for_approval, - read_contract_number -) - -adapter = self._adapter - -print("[S09] 填写货品明细...") -fill_contract_goods_lines(adapter, self._ctx) -print(f"[OK] 货品: {len(self._ctx.goods_lines)} 行") -print("[S10] 填写付款信息...") -fill_contract_payment_info(adapter, self._ctx) -print(f"[OK] 付款方式={self._ctx.payment_method}") - -if self._ctx.source_type == "YYC": - print("[S11] 合并PDF [YYC专属]... (单PDF, 跳过合并)") - print("[S12] 上传PDF附件 [YYC专属]... (dry_run 跳过)") - -print() -print("[S13] 提交审批...") -if not submit_contract_for_approval(adapter, self._ctx): - print("[DRY-RUN] 演练模式,跳过实际提交") - print("=" * 60) - print("合同建单流程演练完成") - print("=" * 60) +raw = (self.input_arg or "").strip() +if raw: + data = load_input_data(raw) +elif isinstance(params, dict) and params: + nested = params.get("inputJson") or params.get("inputArg") or params.get("input") + data = load_input_data(str(nested)) if nested else dict(params) else: - print("[S14] 读取合同编号...") - contract_no = read_contract_number(adapter) - print(f"[OK] 合同编号: {contract_no}") - print("=" * 60) - print(f"[COMPLETED] 合同创建成功: {contract_no}") - print("=" * 60) + raise ContractCreateError("缺少业务入参:请通过 -i/--input 传入 JSON 字符串或文件路径,或通过 -c/--params 传入参数") + +state_center_url = _first( + data.get("stateCenterUrl"), data.get("stateCenterURL"), + os.getenv("STATE_CENTER_URL"), params.get("stateCenterUrl"), + "http://127.0.0.1:8000", +) +agent_token = _first( + data.get("agentToken"), os.getenv("AGENT_TOKEN"), + params.get("agentToken"), "dev-agent-token", +) + +print("=" * 60) +print("合同建单 CONTRACT_CREATE - 开始") +print("jobId=%s sourceType=%s submitMode=%s" % ( + data.get("jobId"), data.get("sourceType"), data.get("submitMode"))) +print("=" * 60) + +result = run_contract_create( + data, + state_center_url=state_center_url, + agent_token=agent_token, +) +print(json.dumps(result, ensure_ascii=False)) """ steps = [ - ("S01 初始化 & 上下文构建", s01_body, "设置环境变量并构建YYC测试上下文"), - ("S02 启动Chrome", s02_body, "Selenium打开Chrome浏览器并导航到合同系统"), - ("S03-S04 登录 & 进入起草", s03_body, "登录系统并进入合同起草页面"), - ("S05-S06 模板选择 & 签约方", s04_body, "选择模板并填写签约方信息"), - ("S07-S08 基础表单 & 金额日期", s05_body, "填写合同类别、金额、日期"), - ("S09-S15 货品/付款/提交", s06_body, "填写货品明细、付款信息并提交"), + ("读取入参并执行合同建单", body, "读取艺赛旗入参并执行 CONTRACT_CREATE(含状态中心回调)"), ] nodes = [] lines = [] -begin_tag = "20260812100000" +begin_tag = "20260813100000" nodes.append({ "NodePanelData": { "ExpandedGroups": [], "InputProperty": {"mouse_x": 0, "mouse_y": 0}, @@ -174,12 +91,12 @@ start_x = 500 start_y = 200 space_y = 120 -for i, (title, body, note) in enumerate(steps): - tag = f"20260812{200000 + i:06d}" +for i, (title, body_text, note) in enumerate(steps): + tag = f"20260813{200000 + i:06d}" py = start_y + i * space_y pimport_b64 = b64_raw("") - pbody_b64 = b64_raw(body) + pbody_b64 = b64_raw(body_text) props = { "EP_CONTROL": { @@ -218,9 +135,9 @@ for i, (title, body, note) in enumerate(steps): "LineStart": { "Pidx": f"{prev_tag}/EPin_Flow_Out", "Px": str(start_x - 100) if i > 0 else "415", - "Py": str(start_y + 30) if i == 0 else str(start_y + (i-1) * space_y + 30) + "Py": str(start_y + 30) if i == 0 else str(start_y + (i - 1) * space_y + 30) }, - "LineTag": f"20260812{300000 + i:06d}" + "LineTag": f"20260813{300000 + i:06d}" }) prev_tag = tag diff --git a/codes/Main.py b/codes/Main.py index 46adf91..a50b3db 100644 --- a/codes/Main.py +++ b/codes/Main.py @@ -40,124 +40,66 @@ class AutoRpa: self.input_arg = iinput.load_init(__file__) if self.input_arg is None: sys.exit(0) - - def Main(self): - #插入代码块#1 - self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:20260812200000,Title:插入代码块#1,Note:设置环境变量并构建YYC测试上下文') - import sys, os, json, time + + def _resolve_input_data(self, params): + """优先 input_arg(JSON 字符串或文件路径),其次 -c 传入的 params 字典。""" + from rpa_yisaiqi.contract_create_node import ContractCreateError, load_input_data + raw = (self.input_arg or "").strip() + if raw: + return load_input_data(raw) + if isinstance(params, dict) and params: + nested = params.get("inputJson") or params.get("inputArg") or params.get("input") + if nested: + return load_input_data(str(nested)) + return dict(params) + raise ContractCreateError("缺少业务入参:请通过 -i/--input 传入 JSON 字符串或文件路径,或通过 -c/--params 传入参数") + + @staticmethod + def _first(*values): + for value in values: + if value is not None and str(value).strip(): + return str(value).strip() + return "" + + def Main(self, **params): + self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:20260813000000,Title:合同建单,Note:读取入参并执行 CONTRACT_CREATE') + import sys, os, json from pathlib import Path _CODES_DIR = Path(r'd:\RPA\AutoRpa\codes') if str(_CODES_DIR) not in sys.path: sys.path.insert(0, str(_CODES_DIR)) - from rpa_yisaiqi.contract_flow_steps import ContractFlowContext, ContractGoodsLine - print("=" * 60) - print("合同建单 CONTRACT_CREATE - 初始化") - print("=" * 60) - self._ctx = ContractFlowContext( - contract_url="http://contract-test.gzmpc.com:8080/", - username="test002", password="1", - supplier_name="测试供应商", - order_date="2026-08-12", - source_type="YYC", template_type="NON_FORMAT", - template_name="非格式--采购协议", - customer_code="CUST-001", customer_name="测试供应商", - contract_category="采购协议", settlement_type="付款", - contract_background="自动化测试合同起草", contract_nature="主合同", - amount_currency="人民币", amount="50000.00", - contract_end_date="2026-12-31", - is_major_contract="否", is_prepayment="否", - belong_year="2026", payment_method="账期支付", - payment_days="30", contract_status="正式合同", - contains_blood_products="否", - submit_mode="dry_run", - goods_lines=[ - ContractGoodsLine( - operation_code="ADD", goods_code="YP-001", - goods_name="测试药品A", approval_no="国药准字H00000001", - spec="50mg*30片", unit="盒", quantity=10.0, - ), - ], - merged_pdf_path=None, + + from rpa_yisaiqi.contract_create_node import run_contract_create + + data = self._resolve_input_data(params) + state_center_url = self._first( + data.get("stateCenterUrl"), + data.get("stateCenterURL"), + os.getenv("STATE_CENTER_URL"), + params.get("stateCenterUrl"), + "http://127.0.0.1:8000", ) - print(f"[OK] 上下文就绪: source={self._ctx.source_type}") - print(f"[OK] 模板: {self._ctx.template_name}") - print(f"[OK] 货品行数: {len(self._ctx.goods_lines)}") - - #插入代码块#1 - self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:20260812200001,Title:插入代码块#1,Note:Selenium打开Chrome浏览器并导航到合同系统') - from rpa_yisaiqi.apa_browser_adapter import ApaBrowserAdapter - print("[S02] 启动 Chrome + 打开合同系统...") - print(f" URL: {self._ctx.contract_url}") - adapter = ApaBrowserAdapter(url=self._ctx.contract_url) - adapter.open_url(self._ctx.contract_url) - self._adapter = adapter - print("[OK] Chrome 已打开并导航到合同系统") - - #插入代码块#1 - self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:20260812200002,Title:插入代码块#1,Note:登录系统并进入合同起草页面') - from rpa_yisaiqi.contract_flow_steps import login_contract_system, enter_contract_create - adapter = self._adapter - print("[S03] 登录合同系统...") - login_contract_system(adapter, self._ctx) - print("[OK] 登录完成") - print("[S04] 进入合同起草...") - enter_contract_create(adapter) - print("[OK] 已进入合同起草页面") - - #插入代码块#1 - self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:20260812200003,Title:插入代码块#1,Note:选择模板并填写签约方信息') - from rpa_yisaiqi.contract_flow_steps import select_contract_template, fill_contract_signatory_info - adapter = self._adapter - print("[S05] 选择合同模板...") - select_contract_template(adapter, self._ctx) - print(f"[OK] 模板: {self._ctx.template_name}") - print("[S06] 填写签约方信息...") - fill_contract_signatory_info(adapter, self._ctx) - print(f"[OK] 签约方: {self._ctx.customer_name}") - - #插入代码块#1 - self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:20260812200004,Title:插入代码块#1,Note:填写合同类别、金额、日期') - from rpa_yisaiqi.contract_flow_steps import fill_contract_basic_info, fill_contract_amount_and_dates - adapter = self._adapter - print("[S07] 填写基础表单...") - fill_contract_basic_info(adapter, self._ctx) - print(f"[OK] 类别={self._ctx.contract_category}, 结算={self._ctx.settlement_type}") - print("[S08] 填写金额与日期...") - fill_contract_amount_and_dates(adapter, self._ctx) - print(f"[OK] 金额={self._ctx.amount}, 结束日期={self._ctx.contract_end_date}") - - #插入代码块#1 - self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:20260812200005,Title:插入代码块#1,Note:填写货品明细、付款信息并提交') - from rpa_yisaiqi.contract_flow_steps import ( - fill_contract_goods_lines, fill_contract_payment_info, - upload_contract_document_if_needed, submit_contract_for_approval, - read_contract_number + agent_token = self._first( + data.get("agentToken"), + os.getenv("AGENT_TOKEN"), + params.get("agentToken"), + "dev-agent-token", ) - adapter = self._adapter - print("[S09] 填写货品明细...") - fill_contract_goods_lines(adapter, self._ctx) - print(f"[OK] 货品: {len(self._ctx.goods_lines)} 行") - print("[S10] 填写付款信息...") - fill_contract_payment_info(adapter, self._ctx) - print(f"[OK] 付款方式={self._ctx.payment_method}") - if self._ctx.source_type == "YYC": - print("[S11] 合并PDF [YYC专属]... (单PDF, 跳过合并)") - print("[S12] 上传PDF附件 [YYC专属]... (dry_run 跳过)") - print() - print("[S13] 提交审批...") - if not submit_contract_for_approval(adapter, self._ctx): - print("[DRY-RUN] 演练模式,跳过实际提交") - print("=" * 60) - print("合同建单流程演练完成") - print("=" * 60) - else: - print("[S14] 读取合同编号...") - contract_no = read_contract_number(adapter) - print(f"[OK] 合同编号: {contract_no}") - print("=" * 60) - print(f"[COMPLETED] 合同创建成功: {contract_no}") - print("=" * 60) - + + print("=" * 60) + print("合同建单 CONTRACT_CREATE - 开始") + print("jobId=%s sourceType=%s submitMode=%s" % ( + data.get("jobId"), data.get("sourceType"), data.get("submitMode"))) + print("=" * 60) + + result = run_contract_create( + data, + state_center_url=state_center_url, + agent_token=agent_token, + ) + print(json.dumps(result, ensure_ascii=False)) + return result + if __name__ == '__main__': ILog.begin_init() diff --git a/codes/__pycache__/GlobalFun.cpython-38.pyc b/codes/__pycache__/GlobalFun.cpython-38.pyc new file mode 100644 index 0000000..a7afa54 Binary files /dev/null and b/codes/__pycache__/GlobalFun.cpython-38.pyc differ diff --git a/codes/__pycache__/Main.cpython-38.pyc b/codes/__pycache__/Main.cpython-38.pyc new file mode 100644 index 0000000..de021ab Binary files /dev/null and b/codes/__pycache__/Main.cpython-38.pyc differ diff --git a/codes/rpa_yisaiqi/__pycache__/__init__.cpython-38.pyc b/codes/rpa_yisaiqi/__pycache__/__init__.cpython-38.pyc index 822817f..8bc8939 100644 Binary files a/codes/rpa_yisaiqi/__pycache__/__init__.cpython-38.pyc and b/codes/rpa_yisaiqi/__pycache__/__init__.cpython-38.pyc differ diff --git a/codes/rpa_yisaiqi/__pycache__/apa_browser_adapter.cpython-38.pyc b/codes/rpa_yisaiqi/__pycache__/apa_browser_adapter.cpython-38.pyc index 6b60130..e056e1b 100644 Binary files a/codes/rpa_yisaiqi/__pycache__/apa_browser_adapter.cpython-38.pyc and b/codes/rpa_yisaiqi/__pycache__/apa_browser_adapter.cpython-38.pyc differ diff --git a/codes/rpa_yisaiqi/__pycache__/callback_client.cpython-38.pyc b/codes/rpa_yisaiqi/__pycache__/callback_client.cpython-38.pyc index 303215a..9c039a6 100644 Binary files a/codes/rpa_yisaiqi/__pycache__/callback_client.cpython-38.pyc and b/codes/rpa_yisaiqi/__pycache__/callback_client.cpython-38.pyc differ diff --git a/codes/rpa_yisaiqi/__pycache__/contract_create_node.cpython-38.pyc b/codes/rpa_yisaiqi/__pycache__/contract_create_node.cpython-38.pyc index 2fd64e4..e421521 100644 Binary files a/codes/rpa_yisaiqi/__pycache__/contract_create_node.cpython-38.pyc and b/codes/rpa_yisaiqi/__pycache__/contract_create_node.cpython-38.pyc differ diff --git a/codes/rpa_yisaiqi/__pycache__/contract_draft.cpython-38.pyc b/codes/rpa_yisaiqi/__pycache__/contract_draft.cpython-38.pyc index 557d9ee..4282f76 100644 Binary files a/codes/rpa_yisaiqi/__pycache__/contract_draft.cpython-38.pyc and b/codes/rpa_yisaiqi/__pycache__/contract_draft.cpython-38.pyc differ diff --git a/codes/rpa_yisaiqi/__pycache__/contract_flow_steps.cpython-38.pyc b/codes/rpa_yisaiqi/__pycache__/contract_flow_steps.cpython-38.pyc index 33559a1..90e29db 100644 Binary files a/codes/rpa_yisaiqi/__pycache__/contract_flow_steps.cpython-38.pyc and b/codes/rpa_yisaiqi/__pycache__/contract_flow_steps.cpython-38.pyc differ diff --git a/codes/rpa_yisaiqi/__pycache__/diag_template_click.cpython-38.pyc b/codes/rpa_yisaiqi/__pycache__/diag_template_click.cpython-38.pyc new file mode 100644 index 0000000..52b109b Binary files /dev/null and b/codes/rpa_yisaiqi/__pycache__/diag_template_click.cpython-38.pyc differ diff --git a/codes/rpa_yisaiqi/__pycache__/errors.cpython-38.pyc b/codes/rpa_yisaiqi/__pycache__/errors.cpython-38.pyc new file mode 100644 index 0000000..9ae89f7 Binary files /dev/null and b/codes/rpa_yisaiqi/__pycache__/errors.cpython-38.pyc differ diff --git a/codes/rpa_yisaiqi/__pycache__/pdf_utils.cpython-38.pyc b/codes/rpa_yisaiqi/__pycache__/pdf_utils.cpython-38.pyc index ad75b51..e956af7 100644 Binary files a/codes/rpa_yisaiqi/__pycache__/pdf_utils.cpython-38.pyc and b/codes/rpa_yisaiqi/__pycache__/pdf_utils.cpython-38.pyc differ diff --git a/codes/rpa_yisaiqi/__pycache__/verify_selenium_fill.cpython-38.pyc b/codes/rpa_yisaiqi/__pycache__/verify_selenium_fill.cpython-38.pyc new file mode 100644 index 0000000..e523b55 Binary files /dev/null and b/codes/rpa_yisaiqi/__pycache__/verify_selenium_fill.cpython-38.pyc differ diff --git a/codes/rpa_yisaiqi/__pycache__/yisaiqi_adapter_template.cpython-38.pyc b/codes/rpa_yisaiqi/__pycache__/yisaiqi_adapter_template.cpython-38.pyc index e9a8ba2..e37f26c 100644 Binary files a/codes/rpa_yisaiqi/__pycache__/yisaiqi_adapter_template.cpython-38.pyc and b/codes/rpa_yisaiqi/__pycache__/yisaiqi_adapter_template.cpython-38.pyc differ diff --git a/codes/rpa_yisaiqi/apa_browser_adapter.py b/codes/rpa_yisaiqi/apa_browser_adapter.py index 23971a2..e58d940 100644 --- a/codes/rpa_yisaiqi/apa_browser_adapter.py +++ b/codes/rpa_yisaiqi/apa_browser_adapter.py @@ -71,10 +71,14 @@ class ApaBrowserAdapter: return self._driver.find_element(By.XPATH, xpath) except NoSuchElementException: # Fallback: find the label's "for" attribute - xpath2 = f'//label[contains(text(),"{text_val}")]/@for' - for_id = self._driver.find_element(By.XPATH, xpath2).get_attribute("for") if False else None - if for_id: - return self._driver.find_element(By.ID, for_id) + try: + for_id = self._driver.find_element( + By.XPATH, f'//label[contains(text(),"{text_val}")]' + ).get_attribute("for") + if for_id: + return self._driver.find_element(By.ID, for_id) + except NoSuchElementException: + pass raise # 3. aria=xxx → aria-label 属性 @@ -87,9 +91,9 @@ class ApaBrowserAdapter: text_val = selector[6:].strip('"').strip("'") return self._driver.find_element(By.CSS_SELECTOR, f'[title="{text_val}"]') - # 5. :contains() → 转 XPath + # 5. :contains() → 转 XPath(支持 button:contains(文本) / a:contains("文本")) if ":contains(" in selector: - m = re.match(r'(.+?):contains\("(.+?)"\)', selector) + m = re.match(r'(.+?):contains\(["\']?(.+?)["\']?\)', selector) if m: tag_part, text_val = m.groups() xpath = f'//{tag_part}[contains(.,"{text_val}")]' @@ -164,6 +168,21 @@ class ApaBrowserAdapter: continue raise RuntimeError(f"click_first failed: {selectors}, last: {last_error}") + def double_click_first(self, selectors: list) -> None: + self._ensure_open() + from selenium.webdriver.common.action_chains import ActionChains + last_error = None + for sel in selectors: + try: + el = self._find(sel) + ActionChains(self._driver).double_click(el).perform() + time.sleep(0.5) + return + except Exception as e: + last_error = e + continue + raise RuntimeError(f"double_click_first failed: {selectors}, last: {last_error}") + def wait_for_text(self, text: str, timeout_seconds: int = 15) -> None: self._ensure_open() from selenium.webdriver.support.ui import WebDriverWait @@ -176,6 +195,30 @@ class ApaBrowserAdapter: except Exception as e: raise TimeoutError(f"wait_for_text '{text}' timed out: {e}") + def wait_for_element(self, selectors: list, timeout_seconds: int = 15) -> None: + self._ensure_open() + deadline = time.time() + timeout_seconds + last_error = None + while time.time() < deadline: + for sel in selectors: + try: + self._find(sel, timeout=1) + return + except Exception as e: + last_error = e + time.sleep(0.5) + raise TimeoutError(f"wait_for_element timed out: {selectors}, last: {last_error}") + + def element_exists(self, selectors: list) -> bool: + self._ensure_open() + for sel in selectors: + try: + self._find(sel, timeout=1) + return True + except Exception: + continue + return False + def select_first(self, selectors: list, value: str) -> None: self._ensure_open() from selenium.webdriver.support.ui import Select @@ -183,14 +226,59 @@ class ApaBrowserAdapter: for sel in selectors: try: el = self._find(sel) - Select(el).select_by_visible_text(value) + # 原生