diff --git a/CHANGELOG.md b/CHANGELOG.md index 67ab396..bb0644b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,11 @@ - 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节 - 打 tag 前**必须**为即将发布的版本新增一节;找不到匹配小节时,本次发布不会更新 changelog 字段 +## 1.0.109 + +- 沉淀任务中断与账号租约契约:禁止用裸 `except Exception` 吞掉 `JobStopped`;`finally` 先释租约再关浏览器;宿主只通知不代收租约 +- 补充 CHECKLIST A5-13(中断后租约须释放)及 ACCOUNT_MANAGER / LOGGING / ADAPTER / 样例 README + ## 1.0.108 - 沉淀数据管理「用户可见文案」契约:弹窗 description / 确认黄条 / 参数说明 / 任务失败 message 须全中文产品用语 diff --git a/SKILL.md b/SKILL.md index efc36b8..7bc42d4 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,7 +1,7 @@ --- name: 技能开发模板(通用业务版) description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。" -version: 1.0.108 +version: 1.0.109 author: 深圳匠厂科技有限公司 metadata: openclaw: diff --git a/assets/naming/platforms.txt b/assets/naming/platforms.txt index db8a041..80bc1a3 100644 --- a/assets/naming/platforms.txt +++ b/assets/naming/platforms.txt @@ -11,3 +11,4 @@ lianlian single-window export-rebate etax +colipu diff --git a/development/ACCOUNT_MANAGER.md b/development/ACCOUNT_MANAGER.md index 267fda5..1824e15 100644 --- a/development/ACCOUNT_MANAGER.md +++ b/development/ACCOUNT_MANAGER.md @@ -173,7 +173,9 @@ python scripts/main.py account open - 租约持有者应使用技能 slug 或可追踪任务标识,`purpose` 写明用途。 - 租约 TTL 应覆盖正常任务时长;长任务需要平台支持的续租策略,不能依赖永不过期。 - `finally release_lease(...)` 是最低要求,浏览器关闭失败也不能跳过释放尝试。 -- 进程被强制结束后可能留下租约;先用 `lease list --active` 核对,确认没有真实任务仍在使用后再释放或清理。 +- **清理顺序**:`finally` 内**先 `release_lease`,再关浏览器**。关浏览器(含 CDP / 强杀 Profile 进程)可能耗时或卡住;先释租约可避免「窗还没关完、账号已被下一任务抢走失败」的空窗拉长。 +- **任务中心中断**:宿主只下发 `stop` 并等待技能退出;技能须在闸门抛出 `JobStopped` 后进入 `finally` 自清。禁止用 `except Exception` 吞掉 `JobStopped`(详见 [`LOGGING.md`](LOGGING.md) §2.5)。不要指望宿主代收租约。 +- 进程被强制结束后可能留下租约;先用 `lease list --active` 核对,确认没有真实任务仍在使用后再释放或清理;亦可等待 TTL 过期。 - 禁止看到租约冲突就直接删数据库记录、删 Profile、换账号或新建重复账号。 ## 9. 工业级测试 @@ -184,6 +186,7 @@ python scripts/main.py account open - 账号不存在、平台不匹配、Profile 缺失、返回格式异常; - 同一账号并发抢占和 `LEASE_CONFLICT`; - 业务成功、业务失败、用户取消、浏览器崩溃后的租约释放; +- **NEED_MANUAL** 任务中心运行中点「中断」:租约变为 `released`(或无 active),马上再采不因残留租约 `LEASE_CONFLICT`; - 登录过期、验证码 / 二次验证、账号风险和权限不足; - 两个以上账号轮换且 Profile、登录态、结果互不污染; - 关闭重开后登录保持; diff --git a/development/ADAPTER.md b/development/ADAPTER.md index 3d6b019..6da3af1 100644 --- a/development/ADAPTER.md +++ b/development/ADAPTER.md @@ -102,7 +102,7 @@ def get_adapter(): 2. **普通兄弟技能调用**,优先走统一 **`service.sibling_bridge`**(`call_sibling_json`),**不要**在 `task_service.py`、`task_rpa.py` 等业务流程文件中到处散落 `subprocess.run`。 3. **account-manager 账号/租约能力**是例外:可参考 `examples/real_browser_rpa/scripts/service/account_client.py` 与 `examples/simulator_browser_rpa/scripts/service/account_client.py`,封装为**单一** `account_client.py`;允许在该文件内部集中通过 subprocess 调 account-manager CLI。**禁止** import `rpa_helpers` 等 account-manager 内部模块;`simulator_rpa` 与 `real_rpa` 均走同一 `pick_web_account` / `release_lease` 模式。 4. **不允许**直接 `import account-manager` 的内部 Python 模块(如 `service/`、`util/`、`db/`)。 -5. **所有选择路径都必须取得 lease**:包括纯数字 `DEFAULT_ACCOUNT_ID`。`account get ` 只能读取账号,不能单独作为运行结果;必须再通过可定位该账号的 pick 流程取得非空 `lease_token`。随后必须 `finally release lease`;进程被 kill 后可能残留 lease,需按 [`ACCOUNT_MANAGER.md`](ACCOUNT_MANAGER.md) 排查。 +5. **所有选择路径都必须取得 lease**:包括纯数字 `DEFAULT_ACCOUNT_ID`。`account get ` 只能读取账号,不能单独作为运行结果;必须再通过可定位该账号的 pick 流程取得非空 `lease_token`。随后必须 `finally` **先 `release_lease`、再关浏览器**;进程被 kill 后可能残留 lease,需按 [`ACCOUNT_MANAGER.md`](ACCOUNT_MANAGER.md) 排查。宿主中断只通知技能,**不**代收租约;禁止 `except Exception` 吞掉 `JobStopped`(见 [`LOGGING.md`](LOGGING.md) §2.5)。 6. **网页 RPA 硬规则**(详见 [`RPA.md`](RPA.md) §0.2):必须先经 `pick_web_account`(底层 **`ensure-web` / `pick-web --ensure`**)拿到 `profile_dir` 再开浏览器;拿不到则失败退出;生产有头;**是否站点登录**按平台约定,但**不等于**可以跳过 account-manager。登记账号 ≠ 站点已登录。 7. **账号选择优先级**:纯数字 `DEFAULT_ACCOUNT_ID` → 校验指定账号后带租约 pick;`DEFAULT_LOGIN_ID` / 非数字标识 → `ensure-web --login-id --lease`;皆空 → 按平台 `ensure-web --lease`。人读展示优先 `login_id` / `account_label`,不要只用数字 id。多条匹配或无法确保拿到指定 ID → `ERROR:AMBIGUOUS_ACCOUNT`。 8. **运行前校验**:`profile_dir` 和 `lease_token` 均须非空;账号平台必须匹配;缺任一字段立即失败,不打开浏览器。 @@ -119,10 +119,12 @@ result = call_sibling_json("account-manager", ["list", "--limit", "10"]) from service.account_client import pick_web_account, release_lease account = pick_web_account(platform="target_platform", login_id=None) +lease_token = account.get("lease_token") try: ... finally: - release_lease(account.get("lease_token")) + release_lease(lease_token) # 先释租约 + # 再关浏览器 / Playwright ``` ## 相关文档 diff --git a/development/CHECKLIST.md b/development/CHECKLIST.md index e74b386..314a8e0 100644 --- a/development/CHECKLIST.md +++ b/development/CHECKLIST.md @@ -125,10 +125,11 @@ checklist_version: 1.0 - [ ] A5-6 **P0** **NEED_MANUAL** 真实站或约定仿真站主路径至少完整跑通 1 次;仅模拟数据通过不算交活 - [ ] A5-7 **P0** 账号创建后可在宿主数据管理和 `account list/get` 查到,返回的 `profile_dir` 在当前用户数据目录真实存在 - [ ] A5-8 **P0** 每个账号使用独立且稳定的 Profile;不同账号不共用,本机日常浏览器目录不参与运行 -- [ ] A5-9 **P0** 指定账号 ID、指定登录标识和自动选择均取得非空租约;成功、失败、取消和异常路径最终释放 +- [ ] A5-9 **P0** 指定账号 ID、指定登录标识和自动选择均取得非空租约;成功、失败、取消和异常路径最终释放;`finally` 先释租约再关浏览器 - [ ] A5-10 **P0** **NEED_MANUAL** 登录后关闭浏览器并释放租约,再取同一账号重开,登录状态仍有效;失效时明确提示重新登录 - [ ] A5-11 **P1** 至少两个账号连续或轮换测试通过,Profile、登录态、结果和失败互不污染 - [ ] A5-12 **P1** 租约冲突、残留租约和账号登录失效能按文档自助定位,不通过删库、删 Profile 或自动换号处理 +- [ ] A5-13 **P0** **NEED_MANUAL** 任务中心运行中点「中断」:技能响应 `JobStopped` 并释放租约(数据管理「账号租约」为 `released` / 无 active);立即再跑不因残留租约失败;进度 `emit` 不得用裸 `except Exception` 吞掉停止 --- diff --git a/development/LOGGING.md b/development/LOGGING.md index 8c208ff..4cd4183 100644 --- a/development/LOGGING.md +++ b/development/LOGGING.md @@ -122,6 +122,26 @@ RPA 等待用 `interruptible_sleep`,**不要**裸 `asyncio.sleep`(否则暂 Journal 会写入 `type: lifecycle` 事件(`paused` / `resumed` / `stopping`),供宿主任务中心显示状态。 +**宿主中断(`stop`)与 `JobStopped`(必读):** + +- 用户在任务中心点「中断」时,宿主写入 `control.json` `command=stop`,并**等待技能进程协作退出**(超时后才可能强杀)。 +- 技能在 `emit` / `checkpoint` / `interruptible_sleep` / `rpa_step` 等闸门处会抛出 `JobStopped`;业务代码须让其冒泡到 `job_context` / 外层 `finally`,由技能**自己**释放租约、关闭浏览器等资源。 +- **禁止**用裸 `except Exception:` 包住上述闸门调用。`JobStopped` 是 `Exception` 子类,会被吞掉,导致 UI 已停、进程仍占账号租约,下次采集必 `LEASE_CONFLICT`。 +- 若必须兜底「emit 写 Journal 失败」,正确写法: + +```python +from jiangchang_skill_core.activity import JobStopped, emit + +try: + emit("正在处理…", skill=SKILL_SLUG, stage="fetch") +except JobStopped: + raise +except Exception: + log.debug("emit_failed", exc_info=True) +``` + +- 宿主**不会**按技能类型统一代收 account-manager 租约(许多技能根本不用账号管理)。强杀后的残留靠租约 TTL / 数据管理手工 force-release;主路径必须是技能自清。 + ### 2.3 可观测性三分法(禁止 task_logs) | 信息类型 | 去哪里 | 技能怎么做 | diff --git a/examples/real_browser_rpa/README.md b/examples/real_browser_rpa/README.md index bc01fa3..e7debee 100644 --- a/examples/real_browser_rpa/README.md +++ b/examples/real_browser_rpa/README.md @@ -52,7 +52,7 @@ real_browser_rpa/ 8. **等待结果容器** — `#search-result-container`(示例 selector) 9. **滚动采集循环** — 采集可见条目 → 判断 stop_reason → 滚动加载 10. **返回结果** — `ScrapeRunResult` + `format_stop_reason_for_user()` -11. **释放租约** — `finally` 中 `release_lease()` +11. **释放租约** — `finally` 中**先** `release_lease()`,**再**关浏览器;进度 `emit` 不得用裸 `except Exception` 吞掉 `JobStopped` ## 开发新 skill 时如何复制 @@ -150,7 +150,7 @@ real_browser_rpa/ - **不自动破解验证码** — 滑块/短信只检测 + 等待人工完成 - **不对抗平台风控** — 频控、账号风险、封禁或权限不足时停止并交由人工处理 - **日志脱敏** — 不输出完整手机号/账号 -- **租约释放** — `ensure-web` / `pick-web --lease` 后必须在 `finally` 释放 +- **租约释放** — `ensure-web` / `pick-web --lease` 后必须在 `finally` **先**释放租约再关浏览器;任务中心中断靠 `JobStopped` 进 `finally`,禁止吞掉停止信号 - **失败留痕** — 真实 skill 应在关键失败点截图(示例中已留注释位) ## 常见坑 diff --git a/examples/simulator_browser_rpa/README.md b/examples/simulator_browser_rpa/README.md index 22951ed..d94a1b3 100644 --- a/examples/simulator_browser_rpa/README.md +++ b/examples/simulator_browser_rpa/README.md @@ -55,7 +55,7 @@ simulator_browser_rpa/ | `browser_session.py` | async 系统 Chrome/Edge + launch args;**不在此 goto** | | `account_client.py` | **唯一** account-manager subprocess 封装(`pick_web_account`→ensure-web / `release_lease`) | | `simulator_playwright.py` | 门户轮询、登录、批量表单、PIN、解析 `batch_id`、截图 | -| `adapter/simulator_rpa.py` | **薄** adapter:pick 账号、lease、委托 RPA、`finally release_lease` | +| `adapter/simulator_rpa.py` | **薄** adapter:pick 账号、lease、委托 RPA、`finally` 先 `release_lease` 再关浏览器 | | `adapter/mock.py` | 不启浏览器,供 unit/mock/CI | | `task_service.py` | async 校验输入 → 选 adapter → `await submit_batch` | | `sandbox/demo_app.html` | 可控 DOM;含可选门户门闩 + 原批量提交流程 | diff --git a/scripts/util/constants.py b/scripts/util/constants.py index 2b247b6..6f9126a 100644 --- a/scripts/util/constants.py +++ b/scripts/util/constants.py @@ -1,6 +1,6 @@ """技能标识、版本与平台公共库约束(复制后请修改 slug/version/logger)。""" SKILL_SLUG = "your-skill-slug" -SKILL_VERSION = "1.0.108" +SKILL_VERSION = "1.0.109" LOG_LOGGER_NAME = "openclaw.skill.your_skill_slug" PLATFORM_KIT_MIN_VERSION = "1.2.11"