
Corsair Harvest 插件实战用 57 个类型化 API 操作接入 Harvest 工时与账单数据【免费下载链接】corsairConnect your users to their apps项目地址: https://gitcode.com/GitHub_Trending/corsa/corsair本篇技术指南聚焦于 Corsair 开源仓库中的corsair-dev/harvest插件packages/harvest/README.md系统讲解如何通过 Corsair 把 Harvest面向代理机构、自由职业者与项目计费的工时追踪与账单软件接入你的应用覆盖安装配置、57 个类型化 API 操作、OAuth 2.0 授权与多账户解析、本地数据库同步、限流与错误处理策略等。读完本文你将掌握在 Corsair 中以统一租户模型调用 Harvest 全部核心资源客户、联系人、项目、任务、工时、费用、发票、估价、团队成员与公司设置并基于本地同步实体做快速查询的完整方案。插件定位与包结构corsair-dev/harvest是 Corsair 生态中的官方插件其插件元数据将其描述为 Time tracking and invoicing software for agencies, freelancers, and project billing面向代理机构、自由职业者和项目计费的工时追踪与发票软件。从包清单可以看到包名corsair-dev/harvest当前版本0.1.1许可证 Apache-2.0以 ESM 发布type: module主入口./dist/index.js类型声明./dist/index.d.ts声明了corsair0.1.0与zod^4.1.13作为 peerDependencies说明插件强依赖 Corsair 核心运行时与 Zod 校验源码目录里同时存在endpoints/10 组操作封装、schema/数据库实体 Schema、client.tsHTTP 客户端、error-handlers.ts错误处理器以及client.test.ts、endpoints.test.ts、schema.test.ts、integration.test.ts四类测试文件。仓库文档 docs/plugins/harvest/overview.mdx 概括了插件的两大核心价值57 个类型化 API 操作与10 个本地同步实体clients、company、contacts、estimates、expenseCategories、invoiceItemCategories、invoices、projects、tasks、users后者支持快速的.search()/.list()查询。安装与接入 CorsairREADME 给出了最简安装方式pnpm add corsair-dev/harvest完整接入 Corsair 时需要同时安装corsair核心包官方文档 docs/plugins/harvest/overview.mdx 提供了四种包管理器写法npm install corsair corsair-dev/harvest yarn add corsair corsair-dev/harvest pnpm install corsair corsair-dev/harvest bun add corsair corsair-dev/harvest安装后在corsair.ts中创建 Corsair 实例并注册插件import Database from better-sqlite3; import { createCorsair } from corsair; import { harvest } from corsair-dev/harvest; export const corsair createCorsair({ plugins: [ harvest(), ], database: new Database(corsair.db), kek: process.env.CORSAIR_KEK!, hub: { projectApiKey: process.env.CORSAIR_API_KEY!, signingSecret: process.env.CORSAIR_SIGNING_SECRET!, }, });Corsair 默认启用多租户调用时用corsair.withTenant(id)划定租户范围从而实现账户级数据隔离可参考仓库文档 docs/concepts/multi-tenancy.mdx。租户接入采用 Connect 链接流程向 Hub 申请一个链接并把租户重定向过去即可const { connectUrl } await corsair.manage.connect.createLink({ plugin: harvest, tenantId: acme, }); // redirect the users browser to connectUrl57 个类型化 API 操作总览README 的核心是一张完整的端点清单Operation / Operation ID / Risk / Description。这些操作按资源域分为 10 组与端点实现目录packages/harvest/endpoints/下的 10 个文件一一对应。完整清单如下OperationOperation IDRiskDescriptionclients.createharvest.api.clients.createwriteCreate a clientclients.deleteharvest.api.clients.deletedestructiveDelete a clientclients.getharvest.api.clients.getreadGet a client by idclients.listharvest.api.clients.listreadList clientsclients.updateharvest.api.clients.updatewriteUpdate a clientcompany.getharvest.api.company.getreadGet company settingscompany.updateharvest.api.company.updatewriteUpdate the writable company settingscontacts.createharvest.api.contacts.createwriteCreate a client contactcontacts.deleteharvest.api.contacts.deletedestructiveDelete a client contactcontacts.listharvest.api.contacts.listreadList client contactscontacts.updateharvest.api.contacts.updatewriteUpdate a client contactestimates.createharvest.api.estimates.createwriteCreate a draft estimateestimates.createItemCategoryharvest.api.estimates.createItemCategorywriteCreate an estimate item categoryestimates.createMessageharvest.api.estimates.createMessagewriteCreate an estimate message; event_type send emails the clientestimates.deleteharvest.api.estimates.deletedestructiveDelete an estimateestimates.deleteMessageharvest.api.estimates.deleteMessagedestructiveDelete an estimate messageestimates.getharvest.api.estimates.getreadGet an estimate by idestimates.listMessagesharvest.api.estimates.listMessagesreadList the messages recorded against an estimateestimates.updateharvest.api.estimates.updatewriteUpdate an estimateestimates.updateItemCategoryharvest.api.estimates.updateItemCategorywriteRename an estimate item categoryexpenses.createharvest.api.expenses.createwriteRecord an expense against a projectexpenses.listCategoriesharvest.api.expenses.listCategoriesreadList expense categoriesexpenses.updateharvest.api.expenses.updatewriteUpdate an expenseinvoices.createharvest.api.invoices.createwriteCreate a draft invoiceinvoices.createItemCategoryharvest.api.invoices.createItemCategorywriteCreate an invoice item categoryinvoices.createMessageharvest.api.invoices.createMessagewriteCreate an invoice message; event_type send emails the clientinvoices.createPaymentharvest.api.invoices.createPaymentwriteRecord a payment against an invoiceinvoices.deleteharvest.api.invoices.deletedestructiveDelete an invoiceinvoices.deleteItemCategoryharvest.api.invoices.deleteItemCategorydestructiveDelete an unused invoice item categoryinvoices.deleteMessageharvest.api.invoices.deleteMessagedestructiveDelete an invoice messageinvoices.deletePaymentharvest.api.invoices.deletePaymentdestructiveDelete a recorded paymentinvoices.getharvest.api.invoices.getreadGet an invoice by idinvoices.listharvest.api.invoices.listreadList invoicesinvoices.listItemCategoriesharvest.api.invoices.listItemCategoriesreadList invoice item categoriesinvoices.listMessagesharvest.api.invoices.listMessagesreadList the messages recorded against an invoiceinvoices.listPaymentsharvest.api.invoices.listPaymentsreadList payments recorded against an invoiceinvoices.updateharvest.api.invoices.updatewriteUpdate an invoiceprojects.createharvest.api.projects.createwriteCreate a projectprojects.deleteharvest.api.projects.deletedestructiveDelete a project and its time entries and expensesprojects.getharvest.api.projects.getreadGet a project by idprojects.listharvest.api.projects.listreadList projectsprojects.updateharvest.api.projects.updatewriteUpdate a projecttasks.createharvest.api.tasks.createwriteCreate a tasktasks.deleteharvest.api.tasks.deletedestructiveDelete a tasktasks.getharvest.api.tasks.getreadGet a task by idtasks.listharvest.api.tasks.listreadList taskstasks.updateharvest.api.tasks.updatewriteUpdate a tasktimeEntries.createharvest.api.timeEntries.createwriteLog time against a project and tasktimeEntries.deleteharvest.api.timeEntries.deletedestructiveDelete a time entrytimeEntries.getharvest.api.timeEntries.getreadGet a time entry by idtimeEntries.listharvest.api.timeEntries.listreadList time entriestimeEntries.updateharvest.api.timeEntries.updatewriteUpdate a time entryusers.createharvest.api.users.createwriteCreate a team member and email them an invitationusers.deleteharvest.api.users.deletedestructiveDelete a team memberusers.getharvest.api.users.getreadGet a team member by idusers.listharvest.api.users.listreadList team membersusers.updateharvest.api.users.updatewriteUpdate a team member合计 57 个操作与 packages/harvest/endpoints.test.ts 头部注释 Exercises all 57 endpoint wrappers 相互印证。风险等级Risk分三档read读取、write写入、destructive破坏性通常是删除或会产生不可逆副作用的高危操作该分级在 packages/harvest/index.ts 的harvestEndpointMeta中逐一声明是 Corsair 权限与审计机制的基础数据。端点实现与调用形态从源码结构看插件把 57 个操作按资源域组织为嵌套路由表harvestEndpointsNested见 packages/harvest/index.ts例如clients.list映射到Clients.list、invoices.createPayment映射到Invoices.createPayment。上层通过harvest()工厂函数返回符合CorsairPlugin约定的插件对象id: harvest并内置 Zod 输入/输出 SchemaharvestEndpointSchemas与事件元数据因此调用方获得的是完全类型化的 API。实际调用统一形如const tenant corsair.withTenant(acme); // 读取获取客户列表 await tenant.harvest.api.clients.list({}); // 读取按 id 获取客户 await tenant.harvest.api.clients.get({ client_id: 123 }); // 写入创建客户 await tenant.harvest.api.clients.create({ name: Acme Corp, currency: USD }); // 破坏性删除客户 await tenant.harvest.api.clients.delete({ client_id: 123 });以clients为例看一个端点的完整实现packages/harvest/endpoints/clients.tslist会把is_active、updated_since、page、per_page等查询参数通过compactQuery清洗后发给 Harvestcreate/update通过compactBody只序列化非undefined字段——因为 Harvest 的 PATCH 语义是省略字段保持不变、显式传null才清空未设置的字段绝不能出现在请求体里见 packages/harvest/endpoints/shared.ts。每个端点成功后还会执行两步收尾本地缓存cacheEntity/cacheEntities把 Harvest 返回的记录校验后写入本地库ctx.db.clients等事件审计通过logEventFromContext记录harvest.clients.list等操作事件。delete返回{ success: true, id }并调用evictEntity把本地镜像中的对应记录删除。认证OAuth 2.0 与 Harvest 多账户解析README 明确Auth: OAuth 2.0——Corsair 会在租户首次使用时引导其授权凭据Corsair prompts your tenant for credentials on first use。接入时在 Corsair Hub 的项目配置里填入 Harvest OAuth 应用的client ID与client secret即可插件默认authType就是oauth_2见 packages/harvest/index.ts。Harvest 的认证有一个关键特性一个访问令牌可以访问多个 Harvest 账户因此账户 IDaccount_id是独立于令牌的第二份凭据。插件在 packages/harvest/client.ts 中处理了完整的账户解析链由resolveAccountIdpackages/harvest/endpoints/shared.ts按优先级逐级回退配置优先插件选项中的accountIdharvest({ accountId: 1234567 })存储键回退已存储的account_id键OAuth 配置中声明了account: [account_id]见 packages/harvest/index.tsHarvest ID 发现兜底调用discoverHarvestAccountId通过 Harvest ID 的accounts接口https://id.getharvest.com/api/v2枚举令牌可达账户并过滤掉product非harvest的账户Harvest ID 会把 Forecast 账户一并列出而 Forecast 账户 ID 会被 Harvest API 拒绝。只有当恰好能解析出唯一一个Harvest 账户时才成功多账户场景必须显式指定否则抛出HarvestAccountIdMissingError其错误信息会提示在插件选项中设置accountId或存储account_id键。每次实际请求都会携带三个关键头见makeHarvestRequestpackages/harvest/client.tsAuthorization: Bearer accessTokenHarvest-Account-Id: accountIdUser-Agent: Corsair (...)—— Harvest 会直接拒绝不带User-Agent的请求400因此插件统一设置而非交给调用方此外请求通过 Corsair 共享的request辅助函数发出从而自动继承超时与限流重试逻辑令牌失效会以 401 呈现为ApiError而HarvestAccountIdMissingError只保留给列表可达但无法唯一确定账户的场景语义划分清晰。本地数据库同步10 个搜索实体README 指出插件没有 Webhooks同时仓库文档 docs/plugins/harvest/overview.mdx 强调本地同步能力每次 API 调用返回的数据都会镜像到本地 SQLite由createCorsair注入的 database 承载供tenant.harvest.db.entity.search()/.list()快速查询。10 个同步实体为clients、company、contacts、estimates、expenseCategories、invoiceItemCategories、invoices、projects、tasks、users。其字段 Schema 定义在 packages/harvest/schema/database.ts字段名与 Harvest 官方 JSON 键保持一致且大多数字段采用nullable().optional()——因为 Harvest 会依据套餐、权限与启用的功能对字段做置空处理仅主键id必填。例如HarvestClientEntity包含id、name、is_active、address、statement_key、currency、created_at、updated_at。镜像写入的工程细节值得留意packages/harvest/endpoints/persist.ts批量并发上限 16一次 list 调用单页最多可返回 2000 行逐个 await 会拖慢整个调用按 16 条一批并发写入既提速又避免单次调用向数据库灌入上千并发写Schema 校验先行记录先经safeParse校验不匹配的行被跳过并告警而不是写入插件自己都读不回来的脏数据读取不驱逐Harvest 对停用数据采用归档is_active: false而非删除保留旧行有助于解析历史引用只有显式 DELETE 操作才调用evictEntity真正删除镜像行镜像尽力而为写入/删除失败只console.warn绝不让本地缓存问题导致 API 调用本身失败。查询形态参考 docs/plugins/harvest/database.mdx以clients为例const rows await tenant.harvest.db.clients.search({ data: { is_active: true, name: { contains: Acme } }, limit: 100, offset: 0, });字符串字段支持equals、contains、startsWith、endsWith、in数值字段支持equals、gt、gte、lt、lte、in布尔字段支持equals每次.search()都接受limit/offset分页。限流与错误处理源码级策略Harvest 的限流规则是每 15 秒 100 次请求超限返回 429 并附Retry-After头成功响应不携带任何限流余量头没有X-RateLimit-Remaining可预判所以客户端无法主动节流只能收到 429 后被动退避。插件据此配置了统一限流参数packages/harvest/client.tsconst HARVEST_RATE_LIMIT_CONFIG: RateLimitConfig { enabled: true, maxRetries: 3, initialRetryDelay: 1000, backoffMultiplier: 2, headerNames: { retryAfter: Retry-After, }, };错误处理由 packages/harvest/error-handlers.ts 的errorHandlers提供覆盖八类场景核心原则是能重试的重试、不能重试的坚决不重试处理器匹配条件策略RATE_LIMIT_ERROR429 或 too many requests / throttle最多 3 次重试尊重Retry-AfterAUTH_ERRORHarvestAccountIdMissingError、401、含 authentication0 次重试配置错误重试无意义PERMISSION_ERROR403、含 forbidden / insufficient0 次重试如创建用户、更新公司设置这类仅管理员可用的操作NOT_FOUND_ERROR404、含 not found / does not exist0 次重试VALIDATION_ERROR422 / 4000 次重试如删除被依赖记录阻塞、对已归档项目记工时SERVER_ERROR状态码 ≥ 500重试非幂等操作 0 次幂等操作最多 2 次指数退避NETWORK_ERROR网络/连接/ECONNREFUSED 等重试非幂等操作 0 次幂等操作最多 3 次DEFAULT兜底0 次重试其中最重要的设计是isNonIdempotent谓词packages/harvest/error-handlers.ts所有命名含create的 POST 操作创建发票、付款、费用等被视为非幂等绝不自动重试——因为 Corsair 重试会整体重放端点若网络失败发生在 Harvest 已提交 POST 之后重放将产生重复记录而 Harvest 不接受幂等键重复发票无法撤回。为防该谓词与端点清单漂移packages/harvest/endpoints.test.ts 会对路由表逐一断言每个 POST 操作都命名为 create…。事件审计只记标识、不落敏感内容插件在每次操作成功后都会写入事件日志corsair_events。packages/harvest/endpoints/logging.ts 的auditPayload遵循明确的隐私边界只记录显式列出的标识字段如client_id其余传入字段仅保留字段名列表fields而不落值——避免联系邮箱、发票备注、工时描述等客户内容被原样持久化到日志。例如clients.create只记录 Harvest 分配的client_id因为客户名称与地址是调用方自填内容见 packages/harvest/endpoints/clients.ts 的注释说明。Webhooks 与触发README 明确声明No webhooks.Harvest 不提供 webhook、回调或流式推送机制因此插件没有可注册的触发器。这一点在实现层面同样清晰packages/harvest/index.ts 中HarvestWebhooks类型为Recordstring, neverharvestWebhooksNested为空对象pluginWebhookMatcher恒返回falseharvestWebhookSchemas为空。若需要事件驱动应从 Corsair 的常规集成渠道如定时轮询上述 list 类接口补足而非依赖该插件的推送能力。测试与验证插件的四个测试文件覆盖了从单元到集成的完整链路packages/harvest/endpoints.test.ts模拟网络逐一验证全部 57 个端点封装的 HTTP 方法、路径、缓存写入与事件日志载荷CI 可运行packages/harvest/client.test.ts验证账户发现与请求构造packages/harvest/schema.test.ts验证实体 Schema 的字段与宽松解析packages/harvest/integration.test.ts端到端集成验证。本地运行测试与类型检查的命令packages/harvest/package.jsonpnpm --filter corsair-dev/harvest test # jest 测试 pnpm --filter corsair-dev/harvest typecheck # tsc --noEmit参考与延伸阅读端点级 API 参考输入/输出类型、风险等级docs/plugins/harvest/api.mdx数据库同步实体与搜索过滤器/docs/plugins/harvest/database.mdx插件总览与接入步骤docs/plugins/harvest/overview.mdx核心实现客户端与账户发现 packages/harvest/client.ts、插件工厂与端点元数据 packages/harvest/index.ts、错误处理 packages/harvest/error-handlers.ts【免费下载链接】corsairConnect your users to their apps项目地址: https://gitcode.com/GitHub_Trending/corsa/corsair创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考