关于 fieldtwin
fieldtwin 在本机模拟 Jobber、Housecall Pro 和 QuickBooks Online API,供集成开发者、测试人员及构建业务 Agent 的团队使用。它提供可重复的模拟业务数据、可控故障,以及变更检查点和回滚。
主要功能
- 01模拟三种业务 API 的请求与响应
- 02生成跨系统一致的虚构业务数据
- 03按需触发限流、故障和重复 Webhook
- 04使用种子数据重复运行测试
- 05查看变更记录并回滚检查点
- 06通过时间跳转测试过期与重试
适用场景
- 01在不访问客户账户的情况下测试集成
- 02在 CI 中重现 API 故障并验证重试
- 03检查 Agent 对业务记录的实际更改
- 04测试同步逻辑处理系统数据偏差的能力
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
npx -y fieldtwinFIELDTWIN_PORTThe port the twin listens on. The MCP server starts a twin of its own, so pick another port if one already runs on 4010.FIELDTWIN_HOSTThe address the twin listens on. Keep the loopback address unless something else must reach the twin.FIELDTWIN_AUTH密钥any-token accepts any Bearer token; oauth accepts only tokens from the twin's OAuth endpoints.FIELDTWIN_SEEDWhich fake business to generate: the same seed builds the same world.本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。
工具
14收录的工具列表 · 更新于 1 天前
此工具无需参数。
详细介绍
fieldtwin
在你的笔记本电脑上运行 Jobber、Housecall Pro 和 QuickBooks Online:真实的 API 格式、可信的虚拟业务,以及可按需触发的各种故障。构建和测试集成与 AI Agent,无需触碰客户账户。
观看两分钟演示:仪表板可操控一个双生环境,列出并撤销 Agent 所做的更改,还可通过同步让发生偏差的账簿恢复一致。
将代码指向 http://localhost:4010,而不是 api.getjobber.com 或 quickbooks.api.intuit.com(或者指向 http://localhost:4010/housecallpro,而不是 api.housecallpro.com)。它会返回相同的路径、请求头和 JSON,数据来自一家虚拟的服务行业企业,其中包含客户、技术人员,以及数月的工作、报价、发票和付款记录,并且这些数据同时存在于每个系统中。然后让测试变得更难:限制 QuickBooks 的请求速率、丢弃写入操作的响应、将同一 Webhook 投递两次、让时钟跳过所有令牌的过期时间。之后,准确查看代码更改了什么,并撤销这些更改。
curl -X POST localhost:4010/_twin/checkpoints/before-agent # name this moment
# ...let your integration or agent loose on http://localhost:4010...
curl -s "localhost:4010/_twin/checkpoints/before-agent/changes?source=api" | jq -r '.[].summary'
2026-07-13T11:00:00.000Z quickbooks CustomerCreated Id=26 "Dana Whitfield" SyncToken=0 (api)
2026-07-13T11:00:00.000Z quickbooks InvoiceCreated Id=29 DocNumber=1001 "Dana Whitfield" TotalAmt=185.00 Balance=185.00 (api)
2026-07-13T11:00:00.000Z quickbooks CustomerUpdated Id=3 "Harper Alder Jr" SyncToken=1 (api)
curl -X POST localhost:4010/_twin/checkpoints/before-agent/rewind # and it never happened
每次运行都从种子数据开始,因此相同的请求会得到完全相同的结果,逐字节一致。遇到一次的 Bug,可以再次复现。
为什么选择 fieldtwin
如果你为现场服务和行业服务企业开发软件,就会了解现有的选择:在客户的真实账簿上测试并祈祷一切顺利;等待供应商开放沙盒权限,再手动填充空账户;或者编写只会返回你预期内容的模拟,因此永远无法发现你没想到的问题。这些选择都不适合 AI Agent,因为它需要一个可以反复犯错、并行犯错的环境。
| 客户的真实账户 | 供应商沙盒 | 手写模拟 | fieldtwin | |
|---|---|---|---|---|
| 可以安全地进行破坏性测试 | 否 | 是 | 是 | 是 |
| 开箱即用 | 等客户信任你之后 | 获得开发者账户、创建应用并完成设置之后 | 为每个端点编写代码之后 | npx fieldtwin |
| 数据真实可信 | 是 | 通用示例数据,或没有数据 | 你输入的数据 | 包含数月历史数据的种子业务 |
| Jobber、Housecall Pro 和 QuickBooks 描述的是同一家企业 | 是 | 否 | 否 | 是,也可以故意制造数据偏差 |
| 恢复到干净的初始状态 | 从不 | 手动 | 重启 | POST /_twin/reset |
| 按需触发 429、5xx、响应丢失和重复 Webhook | 否 | 否 | 仅限你想到的情况 | 是,使用供应商自身的错误格式 |
| 快进时间(令牌过期、重试、定时事件) | 否 | 否 | 很少 | 是,立即生效 |
| 查看代码更改并撤销 | 否 | 否 | 否 | 检查点、更改列表、回退 |
| 离线运行、用于 CI、并行运行多个副本 | 否 | 否 | 是 | 是 |
| 与生产环境完全一致 | 是 | 几乎一致 | 否 | 否。根据公开文档建模;FIDELITY.md 说明了其接近程度 |
fieldtwin 不会取代供应商的沙盒;它补足了沙盒无法覆盖的部分。在正式上线前,仍值得在供应商沙盒中做最后一次检查。如果你有沙盒可用,fieldtwin 的一致性测试套件可以记录真实 API 的行为,并据此评估双生环境。
适用对象
为行业和现场服务软件构建集成的开发者。 最让你头疼的 Bug,往往是那些你无法刻意复现的:被限流的 API、迟迟没有返回结果的写入操作、重复到达的 webhook、在同步过程中途过期的令牌。fieldtwin 可以按需触发这些情况,让它们发生在一个在所有系统中都看起来真实的业务里;而 twin time 可以跳过等待。集成示例在短短几秒内就经历了上百次重试和长达两小时的令牌过期。每次运行都从一个种子开始,因此无论在 CI 还是队友的笔记本电脑上,都能精确重放故障。你可以更早交付,并在测试中发现糟糕的情况,而不是等到客户的账目出问题。
构建会处理业务资金的 AI Agent 的团队。 Agent 需要一个可以反复犯错的地方,而你需要证明它实际做了什么。标记一个检查点,让 Agent 执行,然后根据它所做更改的确切列表来评估,而不是听它说自己做了什么。之后回滚并再次运行。加载 mismatched-books,看看它是否能发现系统间的数据不一致;或者加载 flaky-apis,看看它是否会放弃。仪表盘会实时展示所有情况。
向行业企业提供服务的代理机构和集成商。 “在任何操作触及你的账户之前,我们都会针对 Jobber 或 Housecall Pro 与 QuickBooks 的完整模拟环境进行测试。”这是你能够兑现的承诺,也是谨慎的业主愿意点头的理由。
企业本身,尽管它们从不运行 fieldtwin。它们获得的同步不会创建重复客户、漏记发票或重复记录付款;AI 助手也会在企业的副本中犯错,而不是在企业自己的系统里。
你将获得
| 你将获得 | 无需再做 |
|---|---|
| 通过一条命令,同时在 Jobber、Housecall Pro 和 QuickBooks 中创建可运行的模拟业务 | 手动注册开发者账户、设置应用并输入测试数据 |
| 按需触发限流、服务中断、响应丢失和重复 webhook | 首次在生产环境中遇到这些问题 |
| 在几秒内模拟数小时的重试等待和令牌过期 | 让测试休眠,或完全不覆盖这些情况 |
| 每次运行都能通过相同的种子获得相同结果 | “在我的机器上可以运行” |
| 精确列出代码或 Agent 所做的更改,并可撤销 | 根据日志猜测,且账户永远无法重置 |
| 每次提交都运行的测试,可离线运行,无需供应商账户 | 每次发布前都在沙盒中手动检查 |
fieldtwin 由 Nick George 开发。该项目页面(包含两段演示视频)见 nickgeorgeai.com/fieldtwin;他的更多作品和联系方式见 nickgeorgeai.com。
建模内容
| QuickBooks Online | Jobber | Housecall Pro | |
|---|---|---|---|
| 记录 | 客户、项目、发票、付款 | 客户、工作、上门服务、报价、发票、用户 | 客户及其地址、员工、包含预约和行项目的工作、估价单、发票 |
| 读取 | 按 ID;查询语言(WHERE 支持 =、<、>、LIKE、IN,用 AND 连接;ORDERBY;分页;COUNT(*));变更数据捕获 | GraphQL,支持双向 Relay 分页、编码后的 ID 和带日期的 API 版本 | REST 列表支持 page 和 page_size、排序、客户搜索,以及文档中列出的工作和发票筛选条件 |
| 写入 | 创建和更新客户及发票,使用 SyncTokens(过期时返回 5010),支持稀疏或完整更新;创建并应用付款;使用 requestid 安全重试 | clientCreate | 创建和更新客户、添加地址;创建、安排和派发工作 |
| 速率限制 | 每家公司每分钟 500 个请求:HTTP 429 | 查询成本预算:10,000 点,每秒补充 500 点;返回 THROTTLED | 未公布:故障规则会触发 429,并附带 RateLimit-Reset |
| Webhook | CloudEvents,经过签名,按 Intuit 的计划重试 | 经 HMAC 签名,至少投递一次,并会重试 | 整条记录经过 Api-Timestamp 和 Api-Signature 签名,只尝试一次 |
| OAuth 2.0 | 授权、令牌、刷新(滚动 100 天)、撤销 | 使用 PKCE 授权、令牌、会轮换的刷新令牌 | 授权、令牌(30 天)、会轮换的刷新令牌;API 密钥使用 Authorization: Token |
未列出的任何内容都会以供应商自己的错误格式明确失败,而不会返回虚构数据。FIDELITY.md 列出了所有行为、匹配程度,以及对应的文档页面。
快速开始
需要 Node 22+。
npx fieldtwin
fieldtwin: Rowntree Electric is open for business at http://localhost:4010
fieldtwin: auth is any-token: ANY Bearer token is accepted. Set FIELDTWIN_AUTH=oauth for real OAuth checks.
在浏览器中打开 http://localhost:4010,查看所有内容的位置。从 QuickBooks 模拟服务读取客户(默认情况下,任何 bearer token 都有效;参见身份验证):
curl -H "Authorization: Bearer dev" \
http://localhost:4010/v3/company/9341000000000001/customer/1
从 Jobber 模拟服务列出客户:
curl -X POST http://localhost:4010/api/graphql \
-H "Authorization: Bearer dev" \
-H "X-JOBBER-GRAPHQL-VERSION: 2025-04-16" \
-H "Content-Type: application/json" \
-d '{"query":"{ clients(first: 5) { nodes { id name } totalCount } }"}'
使用 API 密钥从 Housecall Pro 模拟服务列出已安排的工作(任何密钥都有效):
curl -H "Authorization: Token dev" \
"http://localhost:4010/housecallpro/jobs?work_status[]=scheduled&page_size=5"
也可以在预先配置好的场景中启动:npx fieldtwin flaky-apis(参见场景和故障)。npx fieldtwin --help 会列出场景和设置。
如需开发 fieldtwin 本身,或运行仪表板和完整示例:从 Jobber 到 QuickBooks,请改为克隆项目:git clone https://github.com/nickgeorgeseo/fieldtwin && cd fieldtwin && npm ci && npm start。
在代码中使用
只需更改基础 URL。路径、标头和请求体结构都与真实 API 相同。
| 供应商 | 真实基础 URL | 模拟服务基础 URL |
|---|---|---|
| QuickBooks Online | https://quickbooks.api.intuit.com | http://localhost:4010 |
| Jobber | https://api.getjobber.com | http://localhost:4010 |
| Housecall Pro | https://api.housecallpro.com | http://localhost:4010/housecallpro |
QuickBooks 模拟服务提供一个公司,realm 为 9341000000000001。Housecall Pro 模拟服务在 /housecallpro 下提供真实路径,因为其中一些路径(/api/invoices/{uuid})的开头与 Jobber 的路径相同。
QuickBooks 和 Jobber 模拟服务会按模拟时间执行供应商公布的速率限制:当查询成本超过剩余预算时,Jobber 会返回 THROTTLED;QuickBooks 在一分钟内收到 500 个请求后会返回 HTTP 429。推进时钟即可解除限制;重置后额度会恢复至满额。Housecall Pro 未公布其限制,因此其模拟服务仅在故障规则指定时才会限流。故障还可以在这些限制的基础上增加更多限流。
配置
| 变量 | 默认值 | 作用 |
|---|---|---|
FIELDTWIN_PORT | 4010 | 监听端口 |
FIELDTWIN_HOST | 127.0.0.1 | 监听地址。0.0.0.0 可让容器或其他机器访问它:仅在你信任的网络中使用,因为任何能访问该 twin 的人都能控制它(SECURITY.md) |
FIELDTWIN_AUTH | any-token | oauth 会让供应商 API 像真实 API 一样检查令牌 |
FIELDTWIN_SEED | 42 | 要生成哪个虚拟业务。相同的种子始终会生成相同的业务 |
在每次提交时测试
测试会启动一个专属 twin,并像你的应用一样访问它。每个测试都会加载一个全新的环境,针对它运行你的代码,并检查变更列表:查看代码实际做了什么,而不是记录了什么。Twin 会跳过等待,因此重试和令牌过期只需几秒;每个环境都由种子生成,因此在 CI 中失败的测试,在你的笔记本电脑上也会以同样的方式失败。
1. 编写测试。 这个测试会检查答案丢失导致的问题。QuickBooks 可以接受编号相同的两张发票,因此,如果代码在超时后重试创建操作,却没有提供 requestid,就会把同一项工作记入账目两次:
// test/sync.test.ts in your project, with fieldtwin installed: npm install --save-dev fieldtwin
import assert from "node:assert/strict";
import { after, beforeEach, test } from "node:test";
import { startTwin } from "fieldtwin";
import { syncPaidInvoices } from "../src/sync.ts"; // your code
const running = await startTwin(); // a twin of this file's own, on a free port
after(() => running.close());
/** Something the twin recorded: what changed, in which vendor, with the whole record as that vendor's API returns it. */
interface Change {
vendor: string;
type: string;
data: any;
}
/** A request to the twin's control plane, failing the test with the twin's reason if it refuses. */
async function twin(method: string, path: string) {
const response = await fetch(`${running.url}/_twin${path}`, { method });
if (!response.ok) throw new Error(`${method} /_twin${path}: ${await response.text()}`);
return response.json();
}
beforeEach(async () => {
await twin("POST", "/scenarios/flaky-apis"); // a fresh world for each test: 429s, 503s, and writes whose answer is lost
await twin("POST", "/checkpoints/before-sync");
});
test("books each invoice once, even when QuickBooks loses the answer to a write", async () => {
await syncPaidInvoices({ jobberUrl: running.url, quickbooksUrl: running.url });
const changes: Change[] = await twin("GET", "/checkpoints/before-sync/changes?source=api"); // what your code did
const booked = changes
.filter((change) => change.vendor === "quickbooks" && change.type === "InvoiceCreated")
.map((change) => change.data.invoice.DocNumber);
assert.ok(booked.length > 0, "the sync booked no invoices");
assert.equal(new Set(booked).size, booked.length, `an invoice was booked twice: ${booked.join(", ")}`);
});
它使用 Node 内置的测试运行器,Vitest 的用法也相同。Jest 需要使用其 ESM 模式,因为 fieldtwin 是一个 ES 模块。如果同步任务在没有 requestid 的情况下重试,它就会因发现重复项而失败:
✖ books each invoice once, even when QuickBooks loses the answer to a write
AssertionError [ERR_ASSERTION]: an invoice was booked twice: 41, 41
2. 运行测试。 使用 node --test(或 npm test)。无需启动其他任何内容:测试文件会启动 twin 并将其关闭。从 Node 22.18(以及 23.6)开始,Node 可以直接运行 .ts 测试文件;在较旧的 Node 版本上,请运行 node --test --experimental-strip-types,否则 node --test 将找不到测试。
3. 在每次提交时运行测试。 相同的命令也会在 CI 中运行,因为 fieldtwin 和其他依赖项一样,是开发依赖项。如果测试不在 Node 中运行,或者你希望让一个 twin 始终与测试一起运行,可以将其作为独立步骤启动,并等待它响应。在 GitHub Actions 中:
# .github/workflows/test.yml in your project
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
- name: Start the twin
run: |
nohup npx -y [email protected] > twin.log 2>&1 & # pin the version for repeatable runs
timeout 60 bash -c 'until curl -sf localhost:4010/_twin > /dev/null; do sleep 1; done'
- run: npm ci && npm test # your tests, with the twin at http://localhost:4010
- name: Keep the twin's event log
if: failure()
run: curl -s localhost:4010/_twin/log.jsonl > fieldtwin-log.jsonl
- if: failure()
uses: actions/upload-artifact@v7
with:
name: fieldtwin-log
path: fieldtwin-log.jsonl
运行失败时,其事件日志会一并保留:你的代码对 twin 执行的所有操作,既可供查看,也可供重放。
需要注意以下几点:
- 共享同一个 twin 的测试会共享它的环境。 每个调用
startTwin()的文件都会获得自己的 twin,因此文件可以并行运行;同一文件内的测试则会轮流运行,这是 Node 测试运行器和 Vitest 的默认行为。与测试并行启动的一个 twin 会由所有测试共享:请逐个运行文件(node --test --test-concurrency=1、Vitest 的--no-file-parallelism、Jest 的--runInBand)。 startTwin()接受与命令相同的设置:seed、auth("any-token"或"oauth")、scenario(预设场景的名称或 YAML 文件路径),以及port(除非你指定,否则会使用一个空闲端口)。它会返回url、port和close()。flaky-apis会像真实服务一样检查 OAuth 令牌,因此你的代码会通过 twin 的 OAuth 端点进行连接,就像在生产环境中一样。若要单独测试重试,请在场景后使用PUT /_twin/auth和{"mode": "any-token"}。- 如需更严格的检查,参考集成项目中的不稳定测试会通过
flaky-apis进行同步,并检查账目最终是否与干净运行后的结果完全一致。
一个实际示例:Jobber 到 QuickBooks
examples/sync-jobber-invoice-to-quickbooks 是一个按生产代码编写的参考集成,欢迎复制使用。它会按计划同步,并在客户付款时通过 Jobber 的 webhook,将每张已付款的 Jobber 发票录入 QuickBooks(客户、发票、付款)。它能应对双胞胎 API 带来的各种情况:带退避的 429 和 5xx、写入后丢失响应、Jobber 的查询成本限流、过期和轮换的 OAuth 令牌、过时的 SyncToken、两个系统中拼写不一致的客户、重复或延迟的 webhook,以及同时进行的两次同步。
npm start # terminal 1
npm run example:sync # terminal 2
它的测试展示了针对双胞胎进行测试的价值:同一项同步在 flaky-apis 下经过一百多次重试后,最终生成的账目与顺利运行时完全一致;等待令牌过期的两小时也不会占用实际时间。
测试 AI Agent
双胞胎为 Agent 提供完整的业务环境,也会为你精确记录它的操作。为每项测试设置一个检查点,让 Agent 执行任务,然后根据变更列表评估结果,而不是听它描述自己做了什么:
curl -X POST localhost:4010/_twin/checkpoints/before-agent
# ...run your agent against the twin...
curl "localhost:4010/_twin/checkpoints/before-agent/changes?source=api"
curl -X POST localhost:4010/_twin/checkpoints/before-agent/rewind
加载 flaky-apis,看看它如何应对 API 的故障;或加载 mismatched-books,看看它是否能发现现场服务系统与 QuickBooks 中的数据不一致。若要让编码 Agent 直接操作双胞胎,请使用 MCP 服务。
身份验证
默认情况下,双胞胎接受任何 bearer 令牌。 这样快速入门只需一条命令,但这 不适合测试你的 OAuth 代码。若要让双胞胎像真实 API 一样验证令牌,请使用
FIELDTWIN_AUTH=oauth npx fieldtwin启动,将auth: oauth放入场景,或切换运行中的双胞胎:curl -X PUT localhost:4010/_twin/auth -H "Content-Type: application/json" -d '{"mode":"oauth"}'。
每个供应商都在自己的路径下提供双胞胎 OAuth 2.0 服务端(Housecall Pro 的路径位于 /housecallpro 下),因此你的 OAuth 客户端只需更换 URL:
| 端点 | 真实 URL | 双胞胎 URL |
|---|---|---|
| QuickBooks 授权 | https://appcenter.intuit.com/connect/oauth2 | http://localhost:4010/connect/oauth2 |
| QuickBooks 令牌 | https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer | http://localhost:4010/oauth2/v1/tokens/bearer |
| QuickBooks 撤销 | https://developer.api.intuit.com/v2/oauth2/tokens/revoke | http://localhost:4010/v2/oauth2/tokens/revoke |
| Jobber 授权 | https://api.getjobber.com/api/oauth/authorize | http://localhost:4010/api/oauth/authorize |
| Jobber 令牌 | https://api.getjobber.com/api/oauth/token | http://localhost:4010/api/oauth/token |
| Housecall Pro 授权 | https://pro.housecallpro.com/oauth/authorize | http://localhost:4010/housecallpro/oauth/authorize |
| Housecall Pro 令牌 | https://api.housecallpro.com/oauth/token | http://localhost:4010/housecallpro/oauth/token |
双胞胎会立即批准每个授权请求(没有同意页面),并接受任意 client_id 和 client_secret。此后的行为均遵循供应商文档:授权码只能使用一次;访问令牌有效期为 60 分钟(Housecall Pro 为 30 天);Jobber 和 Housecall Pro 会轮换刷新令牌并拒绝旧令牌;QuickBooks 的刷新令牌则按滚动的 100 天有效期保留。令牌按双胞胎时间过期,因此 POST /_twin/clock/advance {"minutes": 60} 可以在一秒内测试令牌刷新流程。(Jobber 的访问令牌是 JWT,其 exp 也使用双胞胎时间,因此应与双胞胎时钟比较,而不是与自己的时钟比较。)
Pro 自己的 Housecall Pro 集成使用 API 密钥,并通过 Authorization: Token <key> 发送。双胞胎无法知道 Pro 创建了哪些密钥,因此在两种身份验证模式下都会接受任意密钥。
从命令行获取 Jobber 令牌:
CODE=$(curl -s -o /dev/null -w '%{redirect_url}' \
"http://localhost:4010/api/oauth/authorize?response_type=code&client_id=my-app&redirect_uri=http://localhost:3000/callback" \
| sed -E 's/.*code=([^&]+).*/\1/')
curl -s -X POST http://localhost:4010/api/oauth/token \
-d client_id=my-app -d client_secret=dev -d grant_type=authorization_code \
-d code=$CODE -d redirect_uri=http://localhost:3000/callback
Webhook
每个 Twin 都会按照对应供应商的方式发送 Webhook。每个供应商都会按应用配置 Webhook(在开发者门户中,或在 Housecall Pro 中通过 Pro 自己的设置配置);Twin 对应的配置位置是控制平面(或 twin_set_webhooks MCP 工具):
curl -X PUT localhost:4010/_twin/webhooks -H "Content-Type: application/json" -d '[
{"vendor":"jobber","url":"http://localhost:3000/webhooks/jobber","topics":["INVOICE_UPDATE"],"clientId":"my-app","clientSecret":"dev"},
{"vendor":"quickbooks","url":"http://localhost:3000/webhooks/qbo","topics":["qbo.payment.created.v1"],"clientId":"my-app","clientSecret":"my-verifier-token"},
{"vendor":"housecallpro","url":"http://localhost:3000/webhooks/hcp","topics":["invoice.paid"],"clientId":"my-app","clientSecret":"my-signing-secret"}
]'
curl -X POST localhost:4010/_twin/actions/jobber/payInvoices -H "Content-Type: application/json" -d '2'
curl -X POST localhost:4010/_twin/actions/housecallpro/payInvoices -H "Content-Type: application/json" -d '2'
Jobber 的投递包含 Jobber 文档中说明的负载({ "data": { "webHookEvent": { "topic", "appId", "accountId", "itemId", "occurredAt" } } }),以及使用订阅的客户端密钥生成的 X-Jobber-Hmac-SHA256 签名,因此你的验证代码无需修改即可运行。与 Jobber 一样,Twin 至少会投递一次:支付发票会触发两次 INVOICE_UPDATE;如果你的应用在一秒内未接受某次投递,该投递稍后会在 Twin 时间中再次发送。目前已建模 CLIENT_CREATE 和 INVOICE_UPDATE。
QuickBooks 的投递采用 CloudEvents 格式,即 Intuit 当前发送的格式:一个由事件组成的 JSON 数组,例如 { "specversion": "1.0", "type": "qbo.invoice.updated.v1", "intuitentityid": "130", "intuitaccountid": "9341000000000001", ... };intuit-signature 中的签名使用订阅的 clientSecret 代替应用的验证令牌。通过 API 创建或更新的客户、发票和付款都会触发通知(qbo.customer.created.v1、qbo.customer.updated.v1、qbo.invoice.created.v1、qbo.invoice.updated.v1、qbo.payment.created.v1),付款还会通知其支付的每张发票。你的应用有 3 秒时间返回 HTTP 200;否则 Twin 会按照 Intuit 的计划重试,间隔从 10 秒起,最长可达 Twin 时间中的每 6 小时一次。
Housecall Pro 的投递包含文档中说明的公共字段(event、company_id、event_occurred_at、event_created_at)和完整记录,内容与事件发出时一致:{ "event": "job.created", ..., "job": { ... } }。Api-Timestamp 和 Api-Signature 遵循文档中的验证步骤:以订阅的 clientSecret 作为签名密钥,对时间戳、一个点号和负载计算 HMAC-SHA256。当前已建模 customer.created、customer.updated、job.created、job.scheduled、job.completed、job.paid、invoice.created、invoice.sent、invoice.payment.succeeded 和 invoice.paid。作业通过 completeJobs、invoiceJobs 和 payInvoices 操作完成、开票和付款;这些操作可在场景的时间线中执行,也可通过控制平面执行:为作业开票会依次发送 invoice.created 和 invoice.sent;支付发票会依次发送 invoice.payment.succeeded、invoice.paid 和 job.paid。文档承诺不进行重试,因此 Twin 对每次投递只尝试一次。
每个 URL 都会按顺序逐个接收投递;某个应用响应缓慢不会拖延发往其他应用的投递。GET /_twin/webhooks 会显示每次近期投递及应用的响应。
带有 webhook 主题(INVOICE_UPDATE、qbo.invoice.updated.v1、job.created 或 "*")的故障规则会让投递而非请求出现异常:重复发送,或延迟到足以让后续投递先行。发生故障的投递会带有 X-Twin-Fault。
faults:
- name: jobber-webhooks-twice
vendor: jobber
webhook: INVOICE_UPDATE
rate: 0.3
effect: { duplicate: true }
- name: jobber-webhooks-late
vendor: jobber
webhook: "*"
rate: 0.2
effect: { delayMs: 300000 } # five minutes of twin time late
场景和故障
场景是一个 YAML 文件,包含种子值、开始时间、世界大小、故障以及随时间推移发生的事件。按名称使用现成场景启动孪生环境,也可以使用自己的文件:
npx fieldtwin heat-wave-monday
npx fieldtwin my-scenario.yaml
| 场景 | 测试内容 |
|---|---|
heat-wave-monday | 忙碌的周一 HVAC 场景:120 位客户、受限的 QuickBooks 写入、响应缓慢的 Jobber,以及整个上午不断打来的新客户电话 |
mismatched-books | 现场服务系统(Jobber 和 Housecall Pro)与 QuickBooks 数据不一致:客户姓名拼写不同(邮箱相同)、仅存在于现场系统且有未同步工作的客户、仅存在于 QuickBooks 的客户,以及 QuickBooks 中标记为非活跃、但此后已为新工作付款的客户 |
flaky-apis | 重试测试:真实 OAuth 检查以及分层故障(QuickBooks 429、503 和响应丢失的写入;Jobber 429、502,以及重复或延迟发送的 Webhook;Housecall Pro 429(带有 RateLimit-Reset)、502、响应丢失的写入,以及延迟发送的 Webhook),同时客户仍在持续付款 |
故障规则按供应商、方法和路径匹配请求(使用 pathIncludes,并可用 pathExcludes 排除某些路径,例如供应商的 OAuth 端点),并按一定概率触发:
faults:
- name: qbo-throttled-writes
vendor: quickbooks
method: POST
rate: 0.2 # one in five matching requests
effect: { status: 429, message: "ThrottleExceeded" }
注入的故障会以供应商自己的错误格式返回,并带上其发送的标头(Housecall Pro 的 429 响应包含以孪生时间表示的 RateLimit-Reset),同时带有 X-Twin-Fault 标头,以便你将其与真实错误区分开来。effect 还可以延迟请求(latencyMs),或丢弃响应:设置 lostResponse: true 时,孪生环境会先完成操作,然后仍以错误作答,就像网关超时掩盖了一次成功的写入一样。重试的写入必须能承受这种故障而不重复执行(对于 QuickBooks,请发送 requestid):
- name: qbo-lost-answers
vendor: quickbooks
method: POST
rate: 0.1
effect: { status: 504, lostResponse: true }
场景还可以安排事件,随着孪生时间推移而发生。推进时钟会按各条目的设定时间执行,并将来源记录为 scenario:
timeline:
- at: "08:00" # business time; also "+90m", "+2h", or "2026-07-13T15:00:00Z"
jobber: { newClients: 12 } # new clients call in
- at: "13:00"
jobber: { payInvoices: 4 } # the longest-open invoices get paid
- at: "16:00"
housecallpro: { completeJobs: 2, invoiceJobs: 2, payInvoices: 1 } # techs finish work, the Pro invoices it, a customer pays
Housecall Pro 也支持 newCustomers,类似于 Jobber 的 newClients。只有当一项工作的所有上门服务都已开始后,该工作才算完成,因此 completeJobs 不会在设定时间之前将工作标记为已完成。完成工作不会为其开具发票:invoiceJobs 会为已完成的工作开具发票,并优先处理完成时间最早的工作;之后,payInvoices 就可以为这些发票付款。
场景若存在拼写错误、未知键,或时间未指定 UTC 偏移量,加载时都会被拒绝并说明原因,孪生环境则保持原样。
控制平面
/_twin 下的所有内容都用于控制模拟世界,而不是模仿某个供应商。
| 请求 | 功能 |
|---|---|
GET /_twin | 公司、种子数据、已加载的场景、孪生环境时间、事件数量、认证模式、已启用的故障、Webhook 订阅、检查点、下一项计划 |
POST /_twin/reset | 回到起点:恢复已加载场景的种子世界(如果有),包括其时间线和故障 |
POST /_twin/clock/advance {"minutes": 60} | 推进时间(速率限制额度会恢复,令牌会过期,计划事件及其 Webhook 会触发) |
POST /_twin/actions/:vendor/:action(正文:params) | 立即触发某项操作,如同在供应商自己的应用中执行:jobber/payInvoices 参数为 2、jobber/newClients 参数为 3、housecallpro/invoiceJobs 参数为 1 |
POST /_twin/checkpoints/:name | 为当前时刻命名 |
GET /_twin/checkpoints/:name/changes?source=api | 查看自该时刻起你的代码所做的更改(也可使用 seed、scenario、control;省略 source 时显示全部):每个事件都附带一行 summary。与日志一样,可通过 after= 从之前读取的位置继续读取 |
POST /_twin/checkpoints/:name/rewind | 撤销检查点之后的所有操作。相同请求会再次遇到相同故障 |
PUT /_twin/faults | 替换当前启用的故障规则 |
PUT /_twin/auth {"mode": "oauth"} | 验证 bearer 令牌(oauth),或接受任何令牌(any-token) |
GET /_twin/scenarios | 查看现成场景及其各自测试的内容 |
POST /_twin/scenarios/:name | 加载现成场景 |
POST /_twin/scenario(YAML 正文) | 加载自定义场景 |
GET /_twin/log.jsonl?since=:checkpoint | 事件日志,每行一个事件(省略 since 时返回全部日志)。结合种子数据,它包含 bug 报告所需的所有信息。其 X-Twin-Log-Position 标头会指出日志的结束位置 |
GET /_twin/log.jsonl?after=:position | 仅返回之前读取时给出的位置(或某个事件的 seq)之后记录的内容:用于跟踪不断增长的日志。如果返回 409,表示期间日志已更改(发生了重置、回退、场景加载或孪生环境重启):请从头重新读取 |
PUT /_twin/webhooks | 替换 Webhook 订阅(参见 Webhooks) |
GET /_twin/webhooks | 查看订阅,以及每次近期投递的结果和应用的响应 |
仪表板
dashboard/ 是用于运行中孪生环境的 Web 仪表板:可在浏览器中查看控制平面的所有内容、不断增长的事件日志,并排查看 QuickBooks 与 Jobber 或 Housecall Pro,同时标记出每位客户和每张发票是否匹配或存在偏差。
npm start # terminal 1: the twin
cd dashboard && npm ci && npm run dev # terminal 2: http://localhost:4020
它是一个独立的 Next.js 软件包,也是孪生环境的普通客户端:它只使用 /_twin,因此查看代码运行情况不会消耗速率限制额度,也不会触发故障。
在编码 Agent 中使用(MCP)
npx fieldtwin mcp 会通过 stdio 启动 twin 和 MCP 服务(在克隆仓库中使用 npm run --silent mcp,这样 npm 自身的输出不会写入 stdout)。其工具与控制平面相对应:
| 工具 | 功能 |
|---|---|
twin_status | 公司、种子、场景、twin 时间、身份验证模式、活动故障、检查点以及接下来安排的事项 |
twin_reset | 返回已加载场景的起点(或默认世界) |
twin_advance_clock | 将时间向前推进;场景中安排的事件会在推进过程中发生 |
twin_checkpoint、twin_changes、twin_rewind | 标记一个时刻、列出此后发生的更改(可选仅查看你的 API 调用),并撤销更改 |
twin_list_scenarios、twin_load_scenario | 按名称选择预设场景,或加载自己的 YAML |
twin_set_faults | 故意让 API 出现异常 |
twin_set_auth | 接受任意令牌,或实际验证 OAuth 令牌 |
twin_export_log | 以 JSON Lines 格式导出事件日志,可导出全部日志或检查点之后的日志 |
twin_run_action | 立即触发事件:客户付款、客户端来电、技术人员完成工作、已完成的工作开具发票 |
twin_set_webhooks、twin_webhooks | 订阅应用的 Webhook;查看已送达的内容及其响应 |
每个工具都带有 MCP 工具注释,因此,使用会在运行工具前征求确认的客户端时,可以识别哪些工具会更改内容。仅用于查看的五个工具标记为只读。会替换或丢弃现有内容的工具标记为破坏性操作:重置、回退、加载场景、设置故障或 Webhook,以及重用检查点名称。只有 twin_advance_clock 和 twin_run_action 会访问 twin 之外的内容,通过向你订阅的 URL 发送 Webhook。
Claude Code:
claude mcp add fieldtwin -- npx -y fieldtwin mcp
其他 MCP 客户端(Cursor、Claude Desktop、VS Code)在其 JSON 配置中使用相同命令:
{ "mcpServers": { "fieldtwin": { "command": "npx", "args": ["-y", "fieldtwin", "mcp"] } } }
它已列入官方 MCP Registry,名称为 io.github.nickgeorgeseo/fieldtwin,因此,读取该 Registry 的客户端和目录可以使用相同命令及其设置进行安装。
在 mcp 后添加场景名称,即可从该场景启动。MCP 服务会在自己的端口启动 twin,默认端口为 4010,除非 FIELDTWIN_PORT 指定其他端口,因此请先停止该端口上的其他 twin(claude mcp add -e FIELDTWIN_PORT=4011 ... 会选择另一个端口)。Agent 关闭会话时,服务也会退出。
MCP 工具只控制测试世界。你的 Agent 仍会像生产代码那样,通过 HTTP 调用供应商 API。
遇到异常时
| 你看到的内容 | 含义 |
|---|---|
fieldtwin: port 4010 is already in use | 端口已被另一个 twin(或其他程序)占用。使用 FIELDTWIN_PORT=4011 npx fieldtwin |
| 任一供应商返回 HTTP 401 | twin 正在检查令牌(auth 为 oauth,由 FIELDTWIN_AUTH、场景或 PUT /_twin/auth 设置)。从 twin 的 OAuth 端点获取令牌,或切回 any-token。对于格式不完全符合 Token <key> 或 Bearer <token> 的 Authorization 标头,Housecall Pro 也会返回 401 |
Jobber 每次尝试都返回 THROTTLED | 查询所需的数据点超过了 10,000 点的总预算,因此永远无法执行。使用 first 或 last 减少请求的节点数 |
QuickBooks 返回 5010 Stale Object | 你的更新发送了旧的 SyncToken,真实 API 也会拒绝此类请求。重新读取记录并重试 |
出现 X-Twin-Fault 标头 | 故障规则被有意触发。GET /_twin 会列出活动故障 |
| 500 错误提到 fieldtwin 中的 bug | 请附上 twin 日志和 GET /_twin/log.jsonl,提交 issue |
项目结构
src/
core/ the twin runtime: event log, clock, seeded randomness, OAuth server, vendor contract
world/ the vendor-neutral fake business: customers, price list, months of work history, drift
vendors/
quickbooks/ one folder per vendor, all with the same shape
jobber/
housecallpro/
faults/ makes vendor APIs misbehave on purpose
control/ /_twin HTTP routes, MCP tools, scenarios (one shared operations.ts)
app.ts wires it together
start.ts startTwin(): a twin on its own port, from code
index.ts what `import { startTwin } from "fieldtwin"` gets
cli.ts the fieldtwin command
server.ts entry point for `npm start`
mcp.ts entry point for `npm run mcp` and `fieldtwin mcp`
scenarios/ ready-made situations (heat-wave-monday, mismatched-books, flaky-apis)
examples/ code that uses the twins the way yours will: over HTTP, never importing src/
sync-jobber-invoice-to-quickbooks/ the reference integration, its demo, and its tests
conformance/ the same scripted sessions against a real sandbox and the twin, compared and scored
dashboard/ a web dashboard for a running twin (Next.js, a package of its own), over HTTP like examples/
test/ tests by vendor and area, plus core and the architecture guards
ARCHITECTURE.md 介绍了各部分如何协同工作,以及让项目易于修改的三条规则。修改 core/ 中的内容之前,请先阅读。
贡献
欢迎贡献,尤其是能让孪生体更加忠实的贡献。CONTRIBUTING.md 介绍了如何添加端点,从厂商的文档页面到测试,再到 FIDELITY.md 中的条目;每个厂商文件夹的结构都相同,所以读过一个之后就能读懂其他文件夹。如果你有 QuickBooks 沙盒公司、Jobber 测试账户,或用于测试的 Housecall Pro MAX 或 XL 套餐账户,最有价值的贡献就是针对该账户录制一致性测试套件:npm run conformance:record 会将孪生体的猜测转化为证据。请先确认厂商的 API 条款允许这样做(各厂商条款链接)。仅依据公开文档构建;切勿粘贴厂商代码、非公开文档或真实客户数据。
未来计划:随着 Jobber 的输入参数进入公开文档,增加更多 Jobber 写入操作;录制真实沙盒中的数据;扩展 Housecall Pro 的支持;然后支持更多厂商(Samsara 或 Motive、EDI 交易伙伴),并为调度和运营 Agent 构建基准测试。ARCHITECTURE.md 中列出了路线图。
免责声明和许可
fieldtwin 是一个独立项目,与 Jobber、Intuit 或 Housecall Pro 无关,也未获得其认可或支持。Jobber、QuickBooks 和 Housecall Pro 是各自所有者的商标。
版权所有 2026 Nick George。根据 Apache License 2.0 授权。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。