- 开发工具
- CLI
- 人工智能
- AI 应用
- 浏览器控制
- GUI 自动化
【免费下载链接】OpenCLI
Make Any Website into CLI & Use your logged-in browser by AI agent.
导读
本文围绕 OpenCLI 仓库中 Mercury 适配器的完整实现(docs/adapters/browser/mercury.md),讲解如何在没有公开 API 的前提下,通过 OpenCLI Browser Bridge 驱动app.mercury.com的可见报销页面完成"上传票据 → 等待 OCR → 纠正被覆盖字段 → 停在 Review 审核页"的安全写入链路。读完本文,你将掌握reimbursement-plan、check-login、reimbursement-draft三个命令的参数语义、输入校验规则、底层调用链与测试方法,并理解适配器刻意不点击最终Submit expense按钮的设计原因。
Mercury 报销在 OpenCLI 中属于浏览器 UI 流程(🔐 Browser 模式)。它没有公开 API,也没有稳定的 JSON 接口用于创建报销单,因此适配器直接驱动 Mercury 报销页面的可见元素:创建草稿、上传本地票据文件、等待 Mercury OCR、再重新应用 Agent 提供的字段——因为 OCR 可能会覆盖金额、币种、日期或商户名。写入命令被刻意设计为保守:它止步于 Mercury 的 Review 步骤,绝不点击最终的Submit expense按钮,任何最终提交都必须由人类或负责任的 Agent 在检查 Review 页面后完成。
Mercury 适配器概览与设计动机
为什么需要浏览器 UI 驱动
从仓库的适配器清单(cli-manifest.json)可以看出,Mercury 的三个命令被声明为Strategy.UI策略(reimbursement-plan为Strategy.LOCAL),其中reimbursement-draft声明了access: 'write'、browser: true、siteSession: 'persistent'、defaultWindowMode: 'foreground'。这些声明直接对应本文核心结论:
- 无公开 API 可调用:创建报销只能走页面 UI;
- 必须复用已登录的浏览器会话:适配器不做 OAuth 登录流,而是使用 OpenCLI 选中的浏览器 profile 里已有的登录态(
siteSession: 'persistent'); - 写入是前台可见的:
foreground窗口模式让人类可以随时看到适配器在页面上做了什么,这与"Review 不提交"的安全设计互相配合。
适配器实现的三个命令
原文档给出的命令总表如下,这是适配器对外暴露的全部能力:
| Command | Description |
|---|---|
opencli mercury reimbursement-plan | Validate a reimbursement payload locally without opening Mercury |
opencli mercury check-login | Open Mercury reimbursements and report whether the selected browser profile is logged in |
opencli mercury reimbursement-draft | Create a reimbursement draft, attach the receipt, correct OCR-overwritten fields, and stop at Review |
三个命令在源码中的注册位置分别为 clis/mercury/reimbursement-plan.js、clis/mercury/check-login.js 和 clis/mercury/reimbursement-draft.js,共享同一套输入归一化逻辑 clis/mercury/utils.js。
参数详解:reimbursement-plan与reimbursement-draft的共享负载
reimbursement-plan和reimbursement-draft接收完全相同的业务负载参数(reimbursement-plan是纯本地校验,不打开浏览器;reimbursement-draft才真正执行页面写入)。原文档参数表如下:
| Flag | Meaning |
|---|---|
--receipt | Absolute or relative path to a local receipt/proof file |
--amount | Original-currency positive amount, e.g.140.00 |
--currency | Original currency code, defaultCNY |
--date | Expense date asYYYY-MM-DD |
--merchant | Merchant name to show in Mercury |
--category | Mercury expense category, defaultMarketing & Advertising |
--notes | Business purpose / reimbursement notes |
--ocr-wait-seconds | Seconds to wait after receipt upload before reapplying fields, default8 |
--close-after-review | Close Review after verification; still never submits |
结合源码 clis/mercury/reimbursement-draft.js 中实际的参数声明,可以补充以下精确语义:
--receipt、--amount、--date、--merchant、--notes均为必填参数;--currency默认CNY,--category默认Marketing & Advertising,--ocr-wait-seconds默认8;--close-after-review是布尔开关,默认false;置为true时会在 Review 验证完成后点击页面上匹配close文本的按钮关闭审核对话框,但仍然绝不触发最终提交(源码在 clis/mercury/reimbursement-draft.js 中实现,点击目标限定为文本归一化后恰好等于close的元素)。
源码级的输入校验规则
所有参数都会先经过 clis/mercury/utils.js 的normalizeReimbursementInput归一化。这一步是"本地先行"策略的基石,让非法输入在浏览器被打开之前就以ArgumentError失败。逐项规则如下:
- 票据文件(
parseReceiptPath,utils.js):相对路径会被path.resolve解析为绝对路径,随后fs.statSync校验文件必须真实存在且是普通文件(stat.isFile()),否则抛出ArgumentError; - 金额(
parseAmount,utils.js):先移除千分位逗号,然后必须匹配^\d+(\.\d{1,2})?$且数值大于 0,即正数、最多两位小数,如140.00;0、1.999都会被拒绝; - 币种(
parseCurrency,utils.js):统一转为大写,必须匹配^[A-Z]{3}$的三字母 ISO 货币代码;US、cny(会自动大写为CNY后通过)等不合规输入会在本地被拦截; - 日期(
parseDate,utils.js):必须匹配YYYY-MM-DD,且经过Date.UTC往返校验是真实存在的日历日期——2026-02-30这类不存在的日期会被拒绝; - OCR 等待秒数(
parseOcrWaitSeconds,utils.js):必须是非负整数字符串,abc等非法输入直接失败; - 布尔开关(
optionalBoolean,utils.js):接受1/true/yes/y/on与0/false/no/n/off字符串,其他值抛错。
这些校验规则被测试用例 clis/mercury/mercury.test.js 逐一覆盖:amount: '0'、amount: '1.999'、currency: 'US'、date: '2026-02-30'、ocr-wait-seconds: 'abc'、不存在的票据路径都会抛出ArgumentError。
Agent 工作流:三步完成一次安全的报销写入
原文档给出了标准的 Agent 调用顺序。在真实落地时,建议严格按 1→2→3 的顺序执行,先确认登录态、再本地校验、最后才触碰页面:
# 1. Confirm the selected browser profile is logged into Mercury opencli --profile <profile> mercury check-login -f json # 2. Validate the payload locally first opencli mercury reimbursement-plan \ --receipt /absolute/path/to/receipt.png \ --amount 140.00 \ --currency CNY \ --date 2026-06-26 \ --merchant "Example Merchant" \ --category "Marketing & Advertising" \ --notes "Example business purpose." \ -f json # 3. Create the draft and stop at Review opencli --profile <profile> mercury reimbursement-draft \ --receipt /absolute/path/to/receipt.png \ --amount 140.00 \ --currency CNY \ --date 2026-06-26 \ --merchant "Example Merchant" \ --category "Marketing & Advertising" \ --notes "Example business purpose." \ -f json值得注意的细节:
check-login与reimbursement-draft是浏览器命令,需要--profile <profile>指定浏览器 profile;reimbursement-plan是纯本地命令,不需要浏览器(源码中声明为browser: false);- 三个命令都建议追加
-f json以 JSON 输出,方便 Agent 程序化解析结果行; - 传入的
--receipt必须是本机真实存在的文件绝对路径(reimbursement-plan阶段会做本地文件校验)。
执行链路:reimbursement-draft从登录检查到 Review 的七步调用
原文档描述了结果,源码则给出了精确的执行顺序。结合 clis/mercury/reimbursement-draft.js 与 clis/mercury/utils.js,完整链路如下:
- 登录态检查:
inspectMercury先goto到https://app.mercury.com/expenses/my-expenses(常量定义于 utils.js),再通过页面文本与 URL 判断loggedIn;未登录时assertLoggedIn抛出AuthRequiredError(utils.js); - 创建表面防护:
assertCreateExpenseSurface探测当前页面是否已存在"Review + Submit expense + 表单字段"的组合。若发现已有报销审核打开,命令直接抛出CommandExecutionError,拒绝点击任何可能是最终提交的按钮(reimbursement-draft.js); - 打开新建报销:
clickCreateExpenseButton在可见的button/a/[role=button]/[role=link]中查找文本为submit expense或new expense的元素;若候选元素位于对话框/表单内或上下文包含 Review、receipt、amount 等关键词,则判定为危险目标并拒绝点击(utils.js); - 上传票据并验证:要求 Browser Bridge 提供
uploadFiles能力(reimbursement-draft.js),以[data-testid="expense-attachment-upload"]为目标上传文件,并核验四点:uploaded === true、文件数恰好为 1、目标选择器精确匹配、文件名包含票据的 basename。任一点不符即抛错,绝不带着未确认的上传继续; - 等待 OCR 后纠正字段:按
--ocr-wait-seconds等待(默认 8 秒),然后fillReimbursementFields使用原生 setter 派发input/change/blur事件,重新填写 amount、currency、date、merchant、notes;category 字段额外派发Enter键事件以触发自定义下拉的确认(utils.js)。填写后校验六个字段的touched标记全部为真,缺失即抛错; - 点击 Review 并快照:在可见可交互元素中精确匹配文本
review并点击(reimbursement-draft.js),随后reviewSnapshot断言页面出现Review文本且Submit expense按钮可见(utils.js); - 可选的关闭动作:若传入
--close-after-review,才点击文本为close的元素关闭对话框,然后返回结果行。
这里体现出适配器的核心工程思想:每一步都带后置条件(postcondition)验证,任何一环失败都以类型化错误(ArgumentError/AuthRequiredError/CommandExecutionError)终止,而不是返回部分成功的假象。
Browser Bridge 的uploadFiles底层实现
适配器对票据上传的要求,依赖 Browser Bridge 的uploadFiles能力。该方法在浏览器内核中实现于 src/browser/base-page.ts,其工作方式与适配器的严格校验正好呼应:
- 解析目标选择器(此处为
[data-testid="expense-attachment-upload"]),先在页面里确认目标是input[type=file],并检查multiple属性与accept约束; - 打上一次性标记属性后,优先通过
setFileInput后端能力、否则通过 CDP 的DOM.setFileInputFiles注入本地文件(base-page.ts); - 上传完成后回读
el.files中的文件名做验证,这与reimbursement-draft中"上传必须确认恰一个文件且名字匹配"的断言相互印证。
因此,如果使用的浏览器后端既不支持setFileInput也不支持 CDP 文件注入,reimbursement-draft会在上传前直接抛出CommandExecutionError,提示需要 Browser BridgeuploadFiles支持(见 reimbursement-draft.js)。
预期结果:三个命令的返回语义
原文档按命令分别定义了成功/失败语义,结合源码列字段可以给出更完整的说明。
check-login的结果
status: "ready"表示 profile 到达了 Mercury 报销页且处于登录态;status: "needs_login"表示 Mercury 重定向到了登录页,需要在同一个 Chrome/OpenCLI profile 中登录后再重跑。
源码输出列还包括loggedIn、url、hasSubmitExpense、hasReimbursements、title(check-login.js),可供 Agent 进一步判断页面状态。
reimbursement-plan的结果
status: "ready"表示本地票据存在、金额/日期格式合法;- 票据文件缺失、非正数金额串、畸形币种代码、非法日历日期、非法的等待秒数或布尔参数,都会在打开 Mercury 之前失败(抛出
ArgumentError); - 输出使用票据的 basename(如
receipt.png),而不是绝对路径,避免向外部暴露本机目录结构。这一行为由receiptBasename实现(utils.js),并被测试用例断言为确定性输出(mercury.test.js)。
reimbursement-draft的结果
成功路径返回一行包含以下字段的摘要:
uploaded: truereviewReady: truesubmitBlocked: truewarnings包含final Submit expense was intentionally not clicked- Mercury 页面停留在 Review 步骤,且展示预期的 receipt、amount、currency、date、merchant、category、notes
若上传确认、必填字段纠正或 Review 后置条件失败,命令抛出类型化错误而非返回部分成功行;此时应保持浏览器打开,人工检查 Mercury 中的校验错误。
从测试用例看,成功路径还精确断言了副作用次数:uploadFiles必须以[data-testid="expense-attachment-upload"]为目标、只调用一次,page.evaluate恰好调用 6 次(mercury.test.js),说明适配器对页面操作的每一步都有严格的确定性约束。
测试与验证:从仓库级检查到真实 UI 冒烟
原文档给出三层测试策略,均可在仓库根目录直接运行。
仓库级检查
npm run dev -- validate mercury npm run typecheck npm run docs:buildvalidate mercury会校验该适配器的清单与实现是否一致(命令注册信息见 cli-manifest.json),typecheck与docs:build保证类型与文档体系完好。
本地输入冒烟(不打开浏览器)
npm run dev -- mercury reimbursement-plan \ --receipt /tmp/example-receipt.png \ --amount 1.00 \ --currency USD \ --date 2026-06-30 \ --merchant "OpenCLI Test Merchant" \ --category "Office Supplies & Equipment" \ --notes "OpenCLI adapter smoke test; do not submit." \ -f json这一步只做纯本地校验,不会打开 Mercury,适合 CI 或无头环境。
真实 UI 冒烟(仅在测试环境执行)
npm run dev -- --profile <profile> mercury reimbursement-draft \ --receipt /absolute/path/to/test-receipt.png \ --amount 1.00 \ --currency USD \ --date 2026-06-30 \ --merchant "OpenCLI Test Merchant" \ --category "Office Supplies & Equipment" \ --notes "OpenCLI adapter smoke test; do not submit." \ -f json通过条件:Mercury 停在 Review 且返回行中submitBlocked: true。冒烟测试期间绝不要点击最终的 Submit,也不要在生产工作区/真实票据上执行。
此外,仓库自带完整的 vitest 测试套件 clis/mercury/mercury.test.js,覆盖输入校验、reimbursement-plan确定性输出、reimbursement-draft的 12 类失败路径(未登录、畸形状态、已有审核面、上传漂移、字段缺失、未达 Review 等)与成功路径,是理解适配器行为边界的最佳参考。
设计约束与注意事项
原文档在 Notes 一节明确了五条不可忽略的设计约束,每条都可以在源码中找到对应实现:
- 登录是硬性前提:适配器复用 OpenCLI 选中浏览器 profile 的登录态,不存储 Mercury 凭据、不执行 OAuth/登录流程(未登录直接抛
AuthRequiredError); - 票据上传依赖固定选择器:
[data-testid="expense-attachment-upload"]是 Mercury 附件输入的唯一锚点。若 Mercury 修改该选择器,上传会失败,但表单其余部分仍然可见,因此故障表现是"上传失败而表单正常"; - OCR 先于纠正执行:Mercury OCR 可能误读币种(例如把
CNY读成JPY)或覆盖商户/金额。命令先上传票据、等待、再用 CLI 参数重新填充字段,这正是--ocr-wait-seconds存在的意义; - Review 不等于提交:
reimbursement-draft永不按下最终的Submit expense按钮,只准备一份待人工检查的草稿; - 使用原始币种:应录入票据上的原始币种(Mercury 支持时),然后在 Review 页核对 Mercury 换算后的报销金额;
- 输出刻意精简:返回行只是控制面(control-plane)状态摘要,最终视觉审核以 Mercury 页面为准。
故障排查指南
原文档给出的排查表,结合源码可以补充更精确的判定依据:
needs_login:在同一个 Chrome/OpenCLI profile 中打开 Mercury 完成登录,再重跑check-login。对应源码路径是assertLoggedIn抛出的AuthRequiredError;- 上传失败:先确认本机文件真实存在,再确认 Mercury 仍使用
[data-testid="expense-attachment-upload"]作为票据输入。该命令要求 Browser BridgeuploadFiles支持,以便验证目标文件输入;若后端无文件注入能力,会在上传前直接失败; - Review 失败:打开浏览器检查 Mercury 的校验错误——命令可能已上传票据并填写字段,但未能到达 Review 步骤。失败抛出的
CommandExecutionError提示"Mercury Review button was not clicked; inspect the page for validation errors"(reimbursement-draft.js); - 分类未生效(Category did not commit):Mercury 的自定义下拉对 UI 变化敏感,重试时使用 Mercury 页面中精确可见的分类标签文本。源码中 category 字段会额外派发
Enter键事件以触发下拉确认,UI 变化时这一路径最易受影响。
小结
OpenCLI 的 Mercury 适配器是一个"无 API 场景下安全写入"的典型范本:本地校验先行(reimbursement-plan)、登录态探测(check-login)、带后置条件的 UI 写入(reimbursement-draft),以及贯穿始终的"绝不点击最终提交"安全边界。对 Agent 而言,正确的使用姿势是让reimbursement-draft停在 Review 页,把最终提交决定权留给人类或负责任的监督者——这既是适配器的行为约定,也是其源码与测试共同强化的契约。
- 开发工具
- CLI
- 人工智能
- AI 应用
- 浏览器控制
- GUI 自动化
【免费下载链接】OpenCLI
Make Any Website into CLI & Use your logged-in browser by AI agent.
相关推荐
OpenCLI Bilibili 浏览器适配器实战指南:用登录态浏览器驱动 B 站数据读写
OpenCLI Bilibili 浏览器适配器实战指南:用登录态浏览器驱动 B 站数据读写 OpenCLI 的 Bilibili 适配器以“浏览器模式”(🔐
开发工具CLI人工智能AI 应用浏览器控制GUI 自动化OpenCLI Claude 适配器实战:用命令行驱动 claude.ai 浏览器会话
OpenCLI Claude 适配器实战:用命令行驱动 claude.ai 浏览器会话 本指南聚焦 OpenCLI 的 claude 浏览器适配器(Browse
开发工具CLI人工智能AI 应用浏览器控制GUI 自动化OpenCLI DeepSeek 适配器实战:用浏览器会话驱动 chat.deepseek.com 的 CLI 命令指南
OpenCLI DeepSeek 适配器实战:用浏览器会话驱动 chat.deepseek.com 的 CLI 命令指南 导读 本文讲解 OpenCLI 项目中
开发工具CLI人工智能AI 应用浏览器控制GUI 自动化
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考