chore: auto release commit (2026-09-15 10:19:55)
All checks were successful
技能自动化发布 / release (push) Successful in 18s
All checks were successful
技能自动化发布 / release (push) Successful in 18s
This commit is contained in:
@@ -4,12 +4,6 @@ OPENCLAW_TEST_TARGET=real_rpa # 可选:mock=本地模拟,simulator_
|
|||||||
# 目标网站地址:技能要访问的网站或服务地址
|
# 目标网站地址:技能要访问的网站或服务地址
|
||||||
TARGET_BASE_URL=https://sandbox.jc2009.com # 填写完整网址(含 https://);换环境时再改,一般保持默认即可
|
TARGET_BASE_URL=https://sandbox.jc2009.com # 填写完整网址(含 https://);换环境时再改,一般保持默认即可
|
||||||
|
|
||||||
# 默认登录账号:仅填写非敏感的账号标识(如工号/登录名/手机号)
|
|
||||||
DEFAULT_LOGIN_ID= # 留空=按平台自动选用或登记;有值则按该标识筛选;不要填密码
|
|
||||||
|
|
||||||
# 默认账号编号:仅填账号管理里的纯数字主键(一般留空)
|
|
||||||
DEFAULT_ACCOUNT_ID= # 留空=自动;只填纯数字;手机号请填上方登录账号
|
|
||||||
|
|
||||||
# 是否显示浏览器窗口:网页自动化时是否弹出可见浏览器
|
# 是否显示浏览器窗口:网页自动化时是否弹出可见浏览器
|
||||||
OPENCLAW_BROWSER_HEADLESS=0 # 0=显示窗口(必须,便于介入与排查);1=后台静默(勿作日常用法)
|
OPENCLAW_BROWSER_HEADLESS=0 # 0=显示窗口(必须,便于介入与排查);1=后台静默(勿作日常用法)
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,12 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 1.0.5
|
||||||
|
|
||||||
|
- sandbox 登录改为人工输入模式:不再通过 CLI 传入用户名密码,由用户在浏览器中手动完成登录
|
||||||
|
- 移除所有硬编码凭据(手机号、密码),从 constants.py 和 .env.example 中清除
|
||||||
|
- CLI 命令精简:extract-invoices / test-rpa-connection 只需 --profile-dir 参数
|
||||||
|
|
||||||
## 1.0.4
|
## 1.0.4
|
||||||
|
|
||||||
- RPA 流程优化:轮询间隔 5s→1s,填表改用 fill(),去掉冗余等待,整体提速约 3-5 倍
|
- RPA 流程优化:轮询间隔 5s→1s,填表改用 fill(),去掉冗余等待,整体提速约 3-5 倍
|
||||||
|
|||||||
2
DEMO.md
2
DEMO.md
@@ -19,7 +19,7 @@ demo_video_url: ""
|
|||||||
```powershell
|
```powershell
|
||||||
$env:PYTHONPATH="D:\jiangchang\huashang\jiangchang-platform-kit\jiangchang-platform-kit\src"
|
$env:PYTHONPATH="D:\jiangchang\huashang\jiangchang-platform-kit\jiangchang-platform-kit\src"
|
||||||
cd D:\jiangchang\huashang\verify-invoice-default\verify-invoice-default
|
cd D:\jiangchang\huashang\verify-invoice-default\verify-invoice-default
|
||||||
python scripts\main.py extract-invoices --username 15527406659 --password qwer1234 --profile-dir "$env:TEMP\verify-invoice-edge-profile"
|
python scripts\main.py extract-invoices --profile-dir "$env:TEMP\verify-invoice-edge-profile"
|
||||||
```
|
```
|
||||||
|
|
||||||
## 没有视频时
|
## 没有视频时
|
||||||
|
|||||||
2
SKILL.md
2
SKILL.md
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: 票据合规预审助手
|
name: 票据合规预审助手
|
||||||
description: "高校报销场景的发票合规性秒级预审:核对字段完整性、购买方税号白名单、开票日期与出差行程逻辑、住宿交通经费标准,对连号、同商家集中开票、节假日开票等风险点红黄灯预警,生成《初审意见单》。"
|
description: "高校报销场景的发票合规性秒级预审:核对字段完整性、购买方税号白名单、开票日期与出差行程逻辑、住宿交通经费标准,对连号、同商家集中开票、节假日开票等风险点红黄灯预警,生成《初审意见单》。"
|
||||||
version: 1.0.4
|
version: 1.0.5
|
||||||
author: 深圳匠厂科技有限公司
|
author: 深圳匠厂科技有限公司
|
||||||
metadata:
|
metadata:
|
||||||
openclaw:
|
openclaw:
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
$env:PYTHONPATH="D:\jiangchang\huashang\jiangchang-platform-kit\jiangchang-platform-kit\src"
|
$env:PYTHONPATH="D:\jiangchang\huashang\jiangchang-platform-kit\jiangchang-platform-kit\src"
|
||||||
cd D:\jiangchang\huashang\verify-invoice-default\verify-invoice-default
|
cd D:\jiangchang\huashang\verify-invoice-default\verify-invoice-default
|
||||||
|
|
||||||
python scripts\main.py extract-invoices --username 15527406659 --password qwer1234 --profile-dir "$env:TEMP\verify-invoice-edge-profile"
|
python scripts\main.py extract-invoices --profile-dir "$env:TEMP\verify-invoice-edge-profile"
|
||||||
```
|
```
|
||||||
|
|
||||||
参数说明:
|
参数说明:
|
||||||
@@ -41,7 +41,7 @@ python scripts\main.py extract-invoices --username 15527406659 --password qwer12
|
|||||||
### 测试连接
|
### 测试连接
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python scripts\main.py test-rpa-connection --username 15527406659 --password qwer1234 --profile-dir "$env:TEMP\verify-invoice-edge-profile"
|
python scripts\main.py test-rpa-connection --profile-dir "$env:TEMP\verify-invoice-edge-profile"
|
||||||
```
|
```
|
||||||
|
|
||||||
### 票据合规预审
|
### 票据合规预审
|
||||||
|
|||||||
@@ -141,8 +141,8 @@ def _handle_list_holidays(args: argparse.Namespace) -> int:
|
|||||||
|
|
||||||
def _handle_extract_invoices(args: argparse.Namespace) -> int:
|
def _handle_extract_invoices(args: argparse.Namespace) -> int:
|
||||||
return cmd_extract_invoices(
|
return cmd_extract_invoices(
|
||||||
username=str(args.username or "").strip(),
|
username="",
|
||||||
password=str(args.password or "").strip(),
|
password="",
|
||||||
profile_dir=str(args.profile_dir or "").strip(),
|
profile_dir=str(args.profile_dir or "").strip(),
|
||||||
headless=bool(args.headless),
|
headless=bool(args.headless),
|
||||||
detail_mode=bool(args.detail_mode),
|
detail_mode=bool(args.detail_mode),
|
||||||
@@ -151,8 +151,8 @@ def _handle_extract_invoices(args: argparse.Namespace) -> int:
|
|||||||
|
|
||||||
def _handle_test_rpa_connection(args: argparse.Namespace) -> int:
|
def _handle_test_rpa_connection(args: argparse.Namespace) -> int:
|
||||||
return cmd_test_rpa_connection(
|
return cmd_test_rpa_connection(
|
||||||
username=str(args.username or "").strip(),
|
username="",
|
||||||
password=str(args.password or "").strip(),
|
password="",
|
||||||
profile_dir=str(args.profile_dir or "").strip(),
|
profile_dir=str(args.profile_dir or "").strip(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -192,9 +192,9 @@ def _print_full_usage() -> None:
|
|||||||
print(" python main.py list-holidays [--year YYYY]")
|
print(" python main.py list-holidays [--year YYYY]")
|
||||||
print(" python main.py stats")
|
print(" python main.py stats")
|
||||||
print(" python main.py run [target [input_id]] [-t target] [-i input_id]")
|
print(" python main.py run [target [input_id]] [-t target] [-i input_id]")
|
||||||
print(" python main.py extract-invoices --username U --password P --profile-dir DIR")
|
print(" python main.py extract-invoices --profile-dir DIR")
|
||||||
print(" [--headless] [--detail-mode]")
|
print(" [--headless] [--detail-mode]")
|
||||||
print(" python main.py test-rpa-connection --username U --password P --profile-dir DIR")
|
print(" python main.py test-rpa-connection --profile-dir DIR")
|
||||||
print(" python main.py health")
|
print(" python main.py health")
|
||||||
print(" python main.py config-path")
|
print(" python main.py config-path")
|
||||||
print(" python main.py version")
|
print(" python main.py version")
|
||||||
@@ -288,17 +288,13 @@ def build_parser() -> ZhArgumentParser:
|
|||||||
sp.set_defaults(handler=lambda _a: cmd_stats())
|
sp.set_defaults(handler=lambda _a: cmd_stats())
|
||||||
|
|
||||||
# RPA commands
|
# RPA commands
|
||||||
sp = sub.add_parser("extract-invoices", help="从网站提取发票信息")
|
sp = sub.add_parser("extract-invoices", help="从网站提取发票信息(sandbox 需手动登录)")
|
||||||
sp.add_argument("--username", required=True, metavar="用户名")
|
|
||||||
sp.add_argument("--password", required=True, metavar="密码")
|
|
||||||
sp.add_argument("--profile-dir", required=True, metavar="浏览器配置目录", dest="profile_dir")
|
sp.add_argument("--profile-dir", required=True, metavar="浏览器配置目录", dest="profile_dir")
|
||||||
sp.add_argument("--headless", action="store_true", help="无头模式(生产不建议)")
|
sp.add_argument("--headless", action="store_true", help="无头模式(生产不建议)")
|
||||||
sp.add_argument("--detail-mode", action="store_true", help="提取详细信息", dest="detail_mode")
|
sp.add_argument("--detail-mode", action="store_true", help="提取详细信息", dest="detail_mode")
|
||||||
sp.set_defaults(handler=_handle_extract_invoices)
|
sp.set_defaults(handler=_handle_extract_invoices)
|
||||||
|
|
||||||
sp = sub.add_parser("test-rpa-connection", help="测试RPA连接和登录")
|
sp = sub.add_parser("test-rpa-connection", help="测试RPA连接(sandbox 需手动登录)")
|
||||||
sp.add_argument("--username", required=True, metavar="用户名")
|
|
||||||
sp.add_argument("--password", required=True, metavar="密码")
|
|
||||||
sp.add_argument("--profile-dir", required=True, metavar="浏览器配置目录", dest="profile_dir")
|
sp.add_argument("--profile-dir", required=True, metavar="浏览器配置目录", dest="profile_dir")
|
||||||
sp.set_defaults(handler=_handle_test_rpa_connection)
|
sp.set_defaults(handler=_handle_test_rpa_connection)
|
||||||
|
|
||||||
|
|||||||
@@ -70,9 +70,9 @@ async def _start_browser(profile_dir: str, headless: bool = False) -> tuple[Any,
|
|||||||
|
|
||||||
|
|
||||||
@rpa_step("登录 sandbox 门户")
|
@rpa_step("登录 sandbox 门户")
|
||||||
async def _login_sandbox_portal(page: Any, phone: str, password: str) -> bool:
|
async def _login_sandbox_portal(page: Any) -> bool:
|
||||||
"""登录 sandbox.jc2009.com 门户。"""
|
"""登录 sandbox.jc2009.com 门户(人工输入)。"""
|
||||||
return await login_sandbox(page, phone, password)
|
return await login_sandbox(page)
|
||||||
|
|
||||||
|
|
||||||
@rpa_step("进入智慧校园")
|
@rpa_step("进入智慧校园")
|
||||||
@@ -152,7 +152,7 @@ async def run_invoice_rpa(
|
|||||||
pw, context, page = await _start_browser(profile_dir, headless)
|
pw, context, page = await _start_browser(profile_dir, headless)
|
||||||
|
|
||||||
# 步骤 2:登录 sandbox 门户
|
# 步骤 2:登录 sandbox 门户
|
||||||
sandbox_ok = await _login_sandbox_portal(page, username, password)
|
sandbox_ok = await _login_sandbox_portal(page)
|
||||||
if not sandbox_ok:
|
if not sandbox_ok:
|
||||||
return InvoiceRpaResult(
|
return InvoiceRpaResult(
|
||||||
success=False,
|
success=False,
|
||||||
|
|||||||
@@ -18,8 +18,7 @@ from jiangchang_skill_core.activity import interruptible_sleep
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# ── Sandbox 门户登录配置 ──
|
# ── Sandbox 门户登录配置 ──
|
||||||
SANDBOX_LOGIN_PHONE = "15527406659"
|
SANDBOX_WAIT_TIMEOUT_SEC = 180 # 等待人工完成 sandbox 登录的最长时间
|
||||||
SANDBOX_LOGIN_PASSWORD = "qwer1234"
|
|
||||||
|
|
||||||
# ── 智慧校园默认凭据 ──
|
# ── 智慧校园默认凭据 ──
|
||||||
SMART_CAMPUS_DEFAULT_USER = "demo"
|
SMART_CAMPUS_DEFAULT_USER = "demo"
|
||||||
@@ -35,13 +34,10 @@ async def login_if_needed(
|
|||||||
page: Any,
|
page: Any,
|
||||||
username: str,
|
username: str,
|
||||||
password: str,
|
password: str,
|
||||||
wait_sec: int = 120,
|
wait_sec: int = 180,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""检查登录状态,如需要则执行登录。"""
|
"""检查登录状态,如需要则执行登录。"""
|
||||||
phone = username or SANDBOX_LOGIN_PHONE
|
sandbox_ok = await login_sandbox(page, wait_sec=wait_sec)
|
||||||
pwd = password or SANDBOX_LOGIN_PASSWORD
|
|
||||||
|
|
||||||
sandbox_ok = await login_sandbox(page, phone, pwd)
|
|
||||||
if not sandbox_ok:
|
if not sandbox_ok:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -80,60 +76,46 @@ async def _poll_until(check_fn, timeout_sec: float = 30.0) -> bool:
|
|||||||
# ── 阶段一:登录 sandbox.jc2009.com ──
|
# ── 阶段一:登录 sandbox.jc2009.com ──
|
||||||
|
|
||||||
|
|
||||||
async def login_sandbox(page: Any, phone: str, password: str) -> bool:
|
async def login_sandbox(page: Any, wait_sec: int = SANDBOX_WAIT_TIMEOUT_SEC) -> bool:
|
||||||
"""登录 sandbox.jc2009.com 门户。已登录则直接返回 True。"""
|
"""登录 sandbox.jc2009.com 门户。
|
||||||
logger.info("sandbox_login_start phone=%s***", phone[:3])
|
|
||||||
|
- 已登录(有智慧校园/Sign out/首页 URL)→ 直接返回 True
|
||||||
|
- 未登录 → 打开登录页,提示用户在浏览器中手动完成登录,轮询等待
|
||||||
|
"""
|
||||||
|
logger.info("sandbox_login_start")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if _is_sandbox_home(page.url) or await page.get_by_role("button", name=re.compile(r"Sign out|退出")).count() > 0 or await page.get_by_text("智慧校园", exact=False).count() > 0:
|
# 1. 检查是否已登录
|
||||||
|
if await _is_sandbox_logged_in(page):
|
||||||
logger.info("sandbox_already_logged_in url=%s", page.url)
|
logger.info("sandbox_already_logged_in url=%s", page.url)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# 找登录表单
|
# 2. 打开 sandbox 首页
|
||||||
phone_input = page.get_by_role("textbox", name=re.compile(r"Phone|phone|手机"))
|
if await page.get_by_role("textbox", name=re.compile(r"Phone|phone|手机")).count() == 0:
|
||||||
if await phone_input.count() == 0:
|
|
||||||
await page.goto("https://sandbox.jc2009.com/", wait_until="domcontentloaded", timeout=NAVIGATION_TIMEOUT_MS)
|
await page.goto("https://sandbox.jc2009.com/", wait_until="domcontentloaded", timeout=NAVIGATION_TIMEOUT_MS)
|
||||||
# 用轮询替代固定等待
|
await asyncio.sleep(2.0)
|
||||||
async def _home_check():
|
if await _is_sandbox_logged_in(page):
|
||||||
return _is_sandbox_home(page.url) or await page.get_by_text("智慧校园", exact=False).count() > 0
|
|
||||||
await _poll_until(_home_check, timeout_sec=10.0)
|
|
||||||
if _is_sandbox_home(page.url) or await page.get_by_text("智慧校园", exact=False).count() > 0:
|
|
||||||
logger.info("sandbox_already_logged_in_after_redirect")
|
logger.info("sandbox_already_logged_in_after_redirect")
|
||||||
return True
|
return True
|
||||||
phone_input = page.get_by_role("textbox", name=re.compile(r"Phone|phone|手机"))
|
|
||||||
|
|
||||||
if await phone_input.count() == 0:
|
# 3. 提示用户手动登录
|
||||||
logger.warning("sandbox_login_page_not_found url=%s", page.url)
|
print("\n" + "=" * 50)
|
||||||
return False
|
print("请在浏览器中手动完成 sandbox 登录")
|
||||||
|
print(f"登录地址:{page.url}")
|
||||||
|
print(f"最多等待 {wait_sec} 秒...")
|
||||||
|
print("=" * 50 + "\n")
|
||||||
|
logger.info("sandbox_waiting_for_human_login url=%s timeout=%ss", page.url, wait_sec)
|
||||||
|
|
||||||
logger.info("sandbox_login_page_detected")
|
# 4. 轮询等待登录完成
|
||||||
|
|
||||||
# 输入手机号(fill 直接设值,比 type 快)
|
|
||||||
await _type_field(phone_input, phone)
|
|
||||||
# 输入密码
|
|
||||||
pwd_input = page.get_by_role("textbox", name=re.compile(r"Password|password|密码"))
|
|
||||||
if await pwd_input.count() == 0:
|
|
||||||
pwd_input = page.get_by_label("Password")
|
|
||||||
await _type_field(pwd_input, password)
|
|
||||||
|
|
||||||
# 点击 Sign in
|
|
||||||
sign_in_btn = page.get_by_role("button", name=re.compile(r"Sign in|登录"))
|
|
||||||
await sign_in_btn.click()
|
|
||||||
logger.info("sandbox_login_click_sign_in")
|
|
||||||
|
|
||||||
# 快速轮询等待登录成功
|
|
||||||
async def _login_done():
|
async def _login_done():
|
||||||
return (
|
return await _is_sandbox_logged_in(page)
|
||||||
_is_sandbox_home(page.url)
|
|
||||||
or await page.get_by_role("button", name=re.compile(r"Sign out|退出")).count() > 0
|
|
||||||
or await page.get_by_text("智慧校园", exact=False).count() > 0
|
|
||||||
)
|
|
||||||
|
|
||||||
if await _poll_until(_login_done, timeout_sec=30.0):
|
if await _poll_until(_login_done, timeout_sec=float(wait_sec)):
|
||||||
logger.info("sandbox_login_success url=%s", page.url)
|
logger.info("sandbox_login_success url=%s", page.url)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
logger.warning("sandbox_login_timeout")
|
logger.warning("sandbox_login_timeout")
|
||||||
|
print("❌ sandbox 登录超时,请重新运行。")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -141,6 +123,17 @@ async def login_sandbox(page: Any, phone: str, password: str) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def _is_sandbox_logged_in(page: Any) -> bool:
|
||||||
|
"""快速检查是否已在 sandbox 首页(已登录)。"""
|
||||||
|
if _is_sandbox_home(page.url):
|
||||||
|
return True
|
||||||
|
if await page.get_by_role("button", name=re.compile(r"Sign out|退出")).count() > 0:
|
||||||
|
return True
|
||||||
|
if await page.get_by_text("智慧校园", exact=False).count() > 0:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
# ── 阶段二:进入智慧校园 ──
|
# ── 阶段二:进入智慧校园 ──
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from jiangchang_skill_core import config
|
|||||||
|
|
||||||
# ── 技能标识 ──
|
# ── 技能标识 ──
|
||||||
SKILL_SLUG = "verify-invoice-default"
|
SKILL_SLUG = "verify-invoice-default"
|
||||||
SKILL_VERSION = "1.0.4"
|
SKILL_VERSION = "1.0.5"
|
||||||
LOG_LOGGER_NAME = "openclaw.skill.verify_invoice_default"
|
LOG_LOGGER_NAME = "openclaw.skill.verify_invoice_default"
|
||||||
PLATFORM_KIT_MIN_VERSION = "1.2.11"
|
PLATFORM_KIT_MIN_VERSION = "1.2.11"
|
||||||
|
|
||||||
@@ -17,11 +17,7 @@ TARGET_PLATFORM = "jc2009"
|
|||||||
LEASE_HOLDER = "verify-invoice-default"
|
LEASE_HOLDER = "verify-invoice-default"
|
||||||
LEASE_TTL_SEC = "1800"
|
LEASE_TTL_SEC = "1800"
|
||||||
|
|
||||||
# ── Sandbox 登录凭据 ──
|
# ── 智慧校园默认凭据(sandbox 平台内置 demo 账号) ──
|
||||||
SANDBOX_LOGIN_PHONE = "15527406659"
|
|
||||||
SANDBOX_LOGIN_PASSWORD = "qwer1234"
|
|
||||||
|
|
||||||
# ── 智慧校园默认凭据 ──
|
|
||||||
SMART_CAMPUS_DEFAULT_USER = "demo"
|
SMART_CAMPUS_DEFAULT_USER = "demo"
|
||||||
SMART_CAMPUS_DEFAULT_PASSWORD = "demo123"
|
SMART_CAMPUS_DEFAULT_PASSWORD = "demo123"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user