OpenCLI Mercury 适配器实战:用浏览器桥接驱动 Mercury 报销草稿的安全写入
2026/9/19 11:48:11 网站建设 项目流程
  • 开发工具
  • CLI
  • 人工智能
  • AI 应用
  • 浏览器控制
  • GUI 自动化

【免费下载链接】OpenCLI

Make Any Website into CLI & Use your logged-in browser by AI agent.

项目地址:https://gitcode.com/gh_mirrors/ope/OpenCLI
点击查看免费下载

导读

本文围绕 OpenCLI 仓库中 Mercury 适配器的完整实现(docs/adapters/browser/mercury.md),讲解如何在没有公开 API 的前提下,通过 OpenCLI Browser Bridge 驱动app.mercury.com的可见报销页面完成"上传票据 → 等待 OCR → 纠正被覆盖字段 → 停在 Review 审核页"的安全写入链路。读完本文,你将掌握reimbursement-plancheck-loginreimbursement-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-planStrategy.LOCAL),其中reimbursement-draft声明了access: 'write'browser: truesiteSession: 'persistent'defaultWindowMode: 'foreground'。这些声明直接对应本文核心结论:

  • 无公开 API 可调用:创建报销只能走页面 UI;
  • 必须复用已登录的浏览器会话:适配器不做 OAuth 登录流,而是使用 OpenCLI 选中的浏览器 profile 里已有的登录态(siteSession: 'persistent');
  • 写入是前台可见的foreground窗口模式让人类可以随时看到适配器在页面上做了什么,这与"Review 不提交"的安全设计互相配合。

适配器实现的三个命令

原文档给出的命令总表如下,这是适配器对外暴露的全部能力:

CommandDescription
opencli mercury reimbursement-planValidate a reimbursement payload locally without opening Mercury
opencli mercury check-loginOpen Mercury reimbursements and report whether the selected browser profile is logged in
opencli mercury reimbursement-draftCreate 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-planreimbursement-draft的共享负载

reimbursement-planreimbursement-draft接收完全相同的业务负载参数(reimbursement-plan是纯本地校验,不打开浏览器;reimbursement-draft才真正执行页面写入)。原文档参数表如下:

FlagMeaning
--receiptAbsolute or relative path to a local receipt/proof file
--amountOriginal-currency positive amount, e.g.140.00
--currencyOriginal currency code, defaultCNY
--dateExpense date asYYYY-MM-DD
--merchantMerchant name to show in Mercury
--categoryMercury expense category, defaultMarketing & Advertising
--notesBusiness purpose / reimbursement notes
--ocr-wait-secondsSeconds to wait after receipt upload before reapplying fields, default8
--close-after-reviewClose 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.0001.999都会被拒绝;
  • 币种parseCurrency,utils.js):统一转为大写,必须匹配^[A-Z]{3}$的三字母 ISO 货币代码;UScny(会自动大写为CNY后通过)等不合规输入会在本地被拦截;
  • 日期parseDate,utils.js):必须匹配YYYY-MM-DD,且经过Date.UTC往返校验是真实存在的日历日期——2026-02-30这类不存在的日期会被拒绝;
  • OCR 等待秒数parseOcrWaitSeconds,utils.js):必须是非负整数字符串,abc等非法输入直接失败;
  • 布尔开关optionalBoolean,utils.js):接受1/true/yes/y/on0/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-loginreimbursement-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,完整链路如下:

  1. 登录态检查inspectMercurygotohttps://app.mercury.com/expenses/my-expenses(常量定义于 utils.js),再通过页面文本与 URL 判断loggedIn;未登录时assertLoggedIn抛出AuthRequiredError(utils.js);
  2. 创建表面防护assertCreateExpenseSurface探测当前页面是否已存在"Review + Submit expense + 表单字段"的组合。若发现已有报销审核打开,命令直接抛出CommandExecutionError拒绝点击任何可能是最终提交的按钮(reimbursement-draft.js);
  3. 打开新建报销clickCreateExpenseButton在可见的button/a/[role=button]/[role=link]中查找文本为submit expensenew expense的元素;若候选元素位于对话框/表单内或上下文包含 Review、receipt、amount 等关键词,则判定为危险目标并拒绝点击(utils.js);
  4. 上传票据并验证:要求 Browser Bridge 提供uploadFiles能力(reimbursement-draft.js),以[data-testid="expense-attachment-upload"]为目标上传文件,并核验四点:uploaded === true、文件数恰好为 1、目标选择器精确匹配、文件名包含票据的 basename。任一点不符即抛错,绝不带着未确认的上传继续;
  5. 等待 OCR 后纠正字段:按--ocr-wait-seconds等待(默认 8 秒),然后fillReimbursementFields使用原生 setter 派发input/change/blur事件,重新填写 amount、currency、date、merchant、notes;category 字段额外派发Enter键事件以触发自定义下拉的确认(utils.js)。填写后校验六个字段的touched标记全部为真,缺失即抛错;
  6. 点击 Review 并快照:在可见可交互元素中精确匹配文本review并点击(reimbursement-draft.js),随后reviewSnapshot断言页面出现Review文本且Submit expense按钮可见(utils.js);
  7. 可选的关闭动作:若传入--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 中登录后再重跑。

源码输出列还包括loggedInurlhasSubmitExpensehasReimbursementstitle(check-login.js),可供 Agent 进一步判断页面状态。

reimbursement-plan的结果

  • status: "ready"表示本地票据存在、金额/日期格式合法;
  • 票据文件缺失、非正数金额串、畸形币种代码、非法日历日期、非法的等待秒数或布尔参数,都会在打开 Mercury 之前失败(抛出ArgumentError);
  • 输出使用票据的 basename(如receipt.png),而不是绝对路径,避免向外部暴露本机目录结构。这一行为由receiptBasename实现(utils.js),并被测试用例断言为确定性输出(mercury.test.js)。

reimbursement-draft的结果

成功路径返回一行包含以下字段的摘要:

  • uploaded: true
  • reviewReady: true
  • submitBlocked: true
  • warnings包含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:build

validate mercury会校验该适配器的清单与实现是否一致(命令注册信息见 cli-manifest.json),typecheckdocs: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.

项目地址:https://gitcode.com/gh_mirrors/ope/OpenCLI
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询