
Midday 会计集成包深度解析Xero / QuickBooks / Fortnox 的手动导出、OAuth 令牌管理与限流设计【免费下载链接】middayInvoicing, Time tracking, File reconciliation, Storage, Financial Overview your own Assistant made for Freelancers项目地址: https://gitcode.com/GitHub_Trending/mi/middayMidday 的midday/accounting包实现了将交易流水与附件收据、发票手动导出到 Xero、QuickBooks、Fortnox 三家会计软件的完整能力。本文基于仓库内的 packages/accounting/README.md 与配套实现源码系统讲解其 Provider 抽象层、手动导出的数据流、accounting_sync_records数据库模型、BullMQ 队列与令牌刷新机制、各 Provider 的限流参数与错误处理策略帮助读者掌握一套多 Provider 会计集成系统的落地设计。一、包定位与支持的 Provider会计集成让用户把 Midday 中已经整理好的财务交易和附件导出到外部会计软件且仅支持手动导出——由用户完全控制数据何时发送到会计提供商。三个 Provider 的当前能力如下Provider状态认证导出附件XeroActiveOAuth 2.0YesYesQuickBooksActiveOAuth 2.0YesYesFortnoxActiveOAuth 2.0Yes凭证 VoucherYes核心特性包括OAuth 2.0 自动令牌刷新、手动选择交易导出、附件去重上传、每个 Team 支持多个 Provider、批次处理与进度跟踪、指数退避重试、重新导出支持以及针对各 Provider 的差异化限流如 Xero 基于 API 配额跟踪的自适应限流与按日期排序导出以保证会计软件中的时间顺序。Provider 枚举与运行时支持判断定义在 packages/accounting/src/types.ts 中通过 Zodz.enum([xero, quickbooks, fortnox])派生出AccountingProviderId类型并附带isAccountingProviderId类型守卫packages/accounting/src/index.ts 中的getSupportedProviders()与isProviderSupported()与之保持一致。1.1 包结构与 Worker 侧代码分布packages/accounting/ ├── src/ │ ├── index.ts # 工厂函数与导出 │ ├── provider.ts # AccountingProvider 接口 BaseAccountingProvider │ ├── types.ts # 共享类型、错误码、限流配置 │ ├── utils.ts # OAuth state 加密、幂等键、MIME 解析、并发工具 │ └── providers/ │ ├── xero.ts # Xero 实现 │ ├── quickbooks.ts # QuickBooks 实现 │ └── fortnox.ts # Fortnox 实现 ├── package.json └── tsconfig.json apps/worker/src/ ├── processors/accounting/ │ ├── base.ts # 共享处理器逻辑Provider 初始化、交易映射 │ ├── export-transactions.ts# 手动导出处理器 │ ├── sync-attachments.ts # 附件上传处理器 │ └── index.ts # 处理器导出 ├── queues/ │ └── accounting.config.ts # BullMQ 队列配置 ├── schemas/ │ └── accounting.ts # 任务载荷的 Zod schema └── utils/ └── accounting-auth.ts # 令牌刷新工具从源码结构看包内三个 Provider 均配有独立的测试文件xero.test.ts、quickbooks.test.ts、fortnox.test.tsapps/worker/src/processors/accounting/ 目录下同样有 export-transactions.test.ts 等测试覆盖导出流程。二、整体架构与数据流2.1 分层架构系统由四层组成DashboardReact 组件→ API 层tRPC 路由 OAuth REST 接口→ WorkerBullMQ 队列含ExportTransactionsProcessor与SyncAttachmentsProcessor→midday/accounting包中的AccountingProvider接口及三个实现最终调用 Xero / QuickBooks / Fortnox 的外部 API。数据库midday/db承载transactions、apps、accounting_sync_records、transaction_attachments四张核心表Worker 直接读写这些表。2.2 手动导出时序User - Dashboard: 选择交易并点击 Export to Accounting Dashboard - API: 触发导出 API - ExportProcessor: 入队 export-to-accounting 任务 ExportProcessor - Database: 加载交易数据getTransactionsForAccountingSync loop 每批 50 条: ExportProcessor - Provider: syncTransactions() Provider -- ExportProcessor: 返回带 Provider 侧 ID 的结果 ExportProcessor - Database: upsert sync records alt 有附件: ExportProcessor - AttachmentProcessor: 触发附件任务 ExportProcessor -- Dashboard: 导出完成实际处理器 apps/worker/src/processors/accounting/export-transactions.ts 实现了文档中的“智能幂等导出”Smart idempotent export新交易创建凭证 上传附件已同步但附件有变化只同步附件不重复创建凭证已同步且无变化跳过。处理器先将交易通过getAccountingSyncStatus查询已有同步记录再用categorizeTransactions分为toExport/toSyncAttachments/alreadyComplete三类然后以BATCH_SIZE 50分批调用provider.syncTransactions()逐笔upsertAccountingSyncRecord落库并通过updateProgress(job, 2..100)上报进度10–70% 为导出阶段70–90% 为触发附件任务阶段。2.3 OAuth 认证时序User - Dashboard: 点击 Connect Provider Dashboard - API: GET /apps/{provider}/install-url API - API: 生成加密 state含 teamId API -- Dashboard: 返回 Consent URL Dashboard - Provider: 跳转授权页 User - Provider: 授权 Provider - API: callback 携带 code state API - API: 解密并校验 state API - Provider: 用 code 换取令牌 Provider -- API: access refresh tokens API - Database: 令牌存入 apps.config API -- Dashboard: 重定向成功页State 载荷在 packages/accounting/src/utils.ts 中被定义为export interface AccountingOAuthStatePayload { teamId: string; userId: string; provider: AccountingProviderId; source: apps | settings; }encryptAccountingOAuthState/decryptAccountingOAuthState基于midday/encryption完成加密与校验解密失败或载荷被篡改时返回null从源头防住 CSRF/重放。三、数据库模型accounting_sync_records 与 apps.config3.1 accounting_sync_records该表按“交易 × Provider”维度跟踪导出状态是重导出、附件去重与错误展示的依据transactions ||--o{ accounting_sync_records : has sync status transactions ||--o{ transaction_attachments : has attachments teams ||--o{ accounting_sync_records : owns teams ||--o{ apps : has integrations accounting_sync_records { uuid id PK uuid transaction_id FK uuid team_id FK enum provider text provider_tenant_id text provider_transaction_id text provider_entity_type jsonb synced_attachment_mapping timestamp synced_at timestamp created_at enum sync_type enum status text error_message }其中synced_attachment_mappingMidday 附件 ID → Provider 附件 ID 的映射支撑附件去重provider_transaction_id与provider_entity_type记录 Provider 侧创建的对象供后续附件上传时定位挂载点。3.2 apps.configOAuth 令牌存储令牌与租户信息以 JSONB 形式存放在apps.config字段。packages/accounting/src/types.ts 用 Zod 定义了基类配置accessToken、refreshToken、expiresAt、可选scope与用户选择的defaultBankAccountId再按 Provider 分派补充租户字段Xero 的tenantId、QuickBooks 的realmIdFortnox 无需租户 ID公司上下文来自令牌本身。apps/worker/src/processors/accounting/base.ts 的hasRequiredConfigFields用 provider 判别字段做类型收窄校验Xero 必须有tenantId、QuickBooks 必须有realmId、Fortnox 恒为真。四、导出逻辑筛选规则、重导出语义与 Provider 差异4.1 交易筛选用户手动选择要导出的交易系统校验其状态是否可导出条件可导出原因status pending是用户可随时导出status completed是用户已标记完成status excluded否用户将其排除在账外status archived否历史交易处理器查询交易时使用sinceDaysAgo: 365手动导出回看一整年与limit: transactionIds.length见 export-transactions.ts 中getTransactionsForAccountingSync的调用。4.2 重导出Re-export行为总是创建新条目重导出会在会计 Provider 中创建新的交易/凭证不更新旧条目会计软件普遍不支持或有限支持更新Fortnox 凭证不可变同步记录会被更新保存最新的provider_transaction_id用户责任如需清理用户应自行在会计软件中删除旧条目。各 Provider 的实体类型与幂等策略Provider实体类型幂等机制备注XeroBankTransaction确定性幂等键SPEND/RECEIVE按金额符号QuickBooksPurchase/DepositRequest-Id头按金额符号选择实体FortnoxVoucher无不可变已过账凭证复式记账4.3 幂等键的生成策略packages/accounting/src/utils.ts 提供了两类幂等键// 交易同步同一 job 重试 同一 key 不产生重复 export function generateTransactionIdempotencyKey( transactionId: string, jobId: string, ): string { return midday-tx-${transactionId}-${jobId}; } // 附件上传防止重试期间重复上传同一文件 export function generateAttachmentIdempotencyKey( providerTransactionId: string, fileName: string, ): string { return midday-attachment-${providerTransactionId}-${fileName}; }设计意图在注释中写明jobId参与键值因此同一任务的 BullMQ 重试会命中相同键、不产生重复条目而一次新的导出任务则生成新键允许用户在删除旧条目后重新导出。导出处理器调用syncTransactions时传入jobId: job.id ?? \fallback-${Date.now()} 与之呼应。五、认证令牌生命周期与刷新5.1 ensureValidToken 流程任务开始 - 从 DB 加载 config - token 过期 否 - 直接使用当前令牌 是 - provider.refreshTokens(refreshToken) - 原子化 DB 更新JSONB merge保留其他字段 - 返回合并后的 config - 继续 API 调用README 给出的工具函数签名export const ensureValidToken async ( db: Database, provider: AccountingProvider, config: AccountingProviderConfig, teamId: string, providerId: string, ): PromiseAccountingProviderConfig { if (!provider.isTokenExpired(new Date(config.expiresAt))) { return config; } const newTokens await provider.refreshTokens(config.refreshToken); await updateAppTokens(db, { teamId, appId: providerId, ...newTokens, }); return { ...config, ...newTokens }; };该工具位于 apps/worker/src/utils/accounting-auth.ts被AccountingProcessorBase.initializeProviderbase.ts在任务开始时调用从 DB 拉取 app 配置 → 校验必填字段 → 从环境变量取 OAuth 凭据 → 构造 Provider 实例 → 必要时刷新令牌。5.2 Provider 基类的令牌与重试基础设施packages/accounting/src/provider.ts 的BaseAccountingProvider提供了三个跨 Provider 的公共机制过期缓冲判断isTokenExpired(expiresAt, bufferSeconds 60)默认提前 60 秒视为过期避免调用中途令牌失效进程内令牌更新getValidAccessToken()在过期时调用refreshTokens并把新令牌写回config.config可重试错误封装withRetry按rateLimitConfig.maxRetries默认 3重试限流错误使用固定retryDelayMs其他可重试错误使用指数退避retryDelayMs * 2^(attempt-1)上限 300 秒最终抛出自带code/retryable/metadata的AccountingOperationError。错误解析parseError将 Provider 原始错误归一为rate_limit/auth_expired/validation/not_found/server_error/unknown并能从 Xero 风格的Elements[0].ValidationErrors[0].Message、通用Detail/message/error_description等字段中提取可读消息。六、Worker 作业队列配置、重试序列与任务类型6.1 BullMQ 队列配置apps/worker/src/queues/accounting.config.ts 的实际配置const accountingQueueOptions: QueueOptions { connection: getRedisConnection(), defaultJobOptions: { attempts: 4, backoff: { type: exponential, delay: 5 * 60 * 1000, // 5 分钟初始延迟给 API 恢复窗口 }, removeOnComplete: { age: 24 * 3600, // 完成任务保留 24 小时 count: 100, // 最多保留 100 个 }, removeOnFail: { age: 7 * 24 * 3600, // 失败任务保留 7 天用于排查 count: 500, }, }, }; const accountingWorkerOptions: WorkerOptions { connection: getRedisConnection(), concurrency: 10, // 高并行度——延迟由入队时预计算避免限流冲突 lockDuration: 600000, // 10 分钟锁容纳约 2000 笔交易的节流上传 stalledInterval: 10 * 60 * 1000, maxStalledCount: 1, };队列名为accounting承载sync-accounting-transactions、sync-accounting-attachments、export-to-accounting三类任务事件处理器onCompleted/onFailed输出带jobName/jobId的结构化日志。6.2 重试序列指数退避attempts: 4、初始延迟 5 分钟即第 1 次立即执行 → 失败等 5 分钟 → 第 2 次 → 等 10 分钟 → 第 3 次 → 等 20 分钟 → 第 4 次 → 仍失败则进入永久失败保留 7 天可供调试。6.3 任务类型任务名处理器触发方用途export-to-accountingExportTransactionsProcessor用户操作导出选中的交易sync-accounting-attachmentsSyncAttachmentsProcessor导出任务向 Provider 上传附件附件任务由导出任务在处理每个交易成功后按延迟入队见第七节的延迟计算实现“交易先落、附件随后”的两级流水。七、限流与可靠性三层设计7.1 Provider 侧限额2025 年核实Provider调用/分钟并发每日备注Xero6055,000每租户QuickBooks50010无每 realmFortnox~3003无约 25 次/5 秒这些数值在 packages/accounting/src/types.ts 的RATE_LIMITS常量中落地为运行参数export const RATE_LIMITS { xero: { callsPerMinute: 60, maxConcurrent: 2, callDelayMs: 1000, retryDelayMs: 60000, maxRetries: 3 }, quickbooks: { callsPerMinute: 500, maxConcurrent: 10, callDelayMs: 200, retryDelayMs: 60000, maxRetries: 3 }, fortnox: { callsPerMinute: 300, maxConcurrent: 2, callDelayMs: 1000, retryDelayMs: 5000, maxRetries: 3 }, } as const satisfies Recordstring, RateLimitConfig;注意源码注释中的两处保守化调整Xero 的maxConcurrent从官方 5 降到 2防止任务间上传重叠Fortnox 从 3 降到 2安全边际。7.2 入队时预计算附件任务延迟附件任务按 Provider 限额计算入队延迟使任务处于 delayed 状态排队而不占用 worker// apps/worker/src/processors/accounting/export-transactions.ts function calculateAttachmentJobDelay(providerId: string, jobIndex: number): number { const config RATE_LIMITS[providerId as keyof typeof RATE_LIMITS]; const callsPerMinute config?.callsPerMinute ?? 60; // 基础延迟 60000/callsPerMinute再乘 2 倍缓冲 // 为上传耗时留空间防止相邻任务上传窗口重叠 const msPerJob Math.ceil((60000 / callsPerMinute) * 2); return jobIndex * msPerJob; }以 Xero60 次/分钟为例Job 0 在 0ms、Job 1 在 2000ms、Job 2 在 4000ms……注README 早期示例使用 1.1 倍缓冲当前仓库实现已改为 2 倍缓冲以源码为准。该策略的收益是任务不阻塞 worker、不同 Team 可并行处理、无需运行时限流检查。7.3 任务内并发与节流对单个交易的多附件上传包内提供 utils.ts 的throttledConcurrent(items, processor, maxConcurrent, callDelayMs, onProgress)按maxConcurrent分批并发批间休眠callDelayMs最后一批不休眠并收集results与带下标的errors配合onProgress回调更新任务进度。7.4 按日期排序导出与耗时估算所有 Provider 导出前按交易日期排序保证会计软件中的时间顺序Fortnox 的凭证号按创建顺序分配Xero/QuickBooks 的交易列表更整洁。基于限流参数的导出耗时估算交易 附件数XeroQuickBooksFortnox200~4 分钟~30 秒~1 分钟1000~18 分钟~2 分钟~4 分钟2000~37 分钟~4 分钟~8 分钟Xero 有 5000 次/天的配额上限超过约 4500 个附件的导出可能跨越多天完成。八、API 参考AccountingProvider 接口packages/accounting/src/provider.ts 中定义的接口含 JSDoc 注释interface AccountingProvider { readonly id: AccountingProviderId; readonly name: string; // OAuth buildConsentUrl(state: string): Promisestring; exchangeCodeForTokens(code: string): PromiseTokenSet; refreshTokens(refreshToken: string): PromiseTokenSet; isTokenExpired(expiresAt: Date, bufferSeconds?: number): boolean; // 默认提前 60 秒 // 租户 / 账户 getTenantInfo(tenantId: string): Promise{ id: string; name: string; currency?: string }; getTenants(): PromiseArray{ tenantId: string; tenantName: string; tenantType: string }; getAccounts(tenantId: string): PromiseAccountingAccount[]; // 交易与附件 syncTransactions(params: SyncTransactionsParams): PromiseSyncResult; uploadAttachment(params: UploadAttachmentParams): PromiseAttachmentResult; deleteAttachment(params: DeleteAttachmentParams): PromiseDeleteAttachmentResult; // 健康检查与断开 checkConnection(): Promise{ connected: boolean; error?: string }; disconnect?(): Promisevoid; // 可选扩展Xero 专属的交易历史备注 addTransactionHistoryNote?(params: { tenantId: string; transactionId: string; taxAmount?: number; taxRate?: number; taxType?: string; note?: string; }): Promisevoid; }相比 README 的简化版接口源码中额外暴露了id/name只读属性与addTransactionHistoryNote?可选方法仅 Xero 实现用于在 Xero 交易上追加税额备注。工厂函数getAccountingProvider(providerId, config?)index.ts内部先调用getProviderOAuthCredentials从环境变量读取凭据任一缺失即抛出OAuth configuration missing for ${providerId}。8.1 数据库查询 API// 获取待导出的交易手动导出必须传 transactionIds getTransactionsForAccountingSync(db, { teamId: string, provider: ProviderType, transactionIds: string[], sinceDaysAgo?: number, // 手动导出为 365 limit?: number, }): PromiseTransactionForSync[] // Upsert 同步记录 upsertAccountingSyncRecord(db, { transactionId: string, teamId: string, provider: ProviderType, providerTenantId: string, providerTransactionId?: string, providerEntityType?: string, // Midday 附件 ID - Provider 附件 ID 的映射 syncedAttachmentMapping?: Recordstring, string | null, syncType: manual, status: synced | failed | pending, errorMessage?: string, errorCode?: string, // 前端按此展示具体错误提示 }): PromiseAccountingSyncRecord // 同步完成后更新附件映射 updateSyncedAttachmentMapping(db, { syncRecordId: string, syncedAttachmentMapping: Recordstring, string | null, }): PromiseAccountingSyncRecord这些查询位于midday/db/queriespackages/db/src/queries/导出处理器与附件处理器共享同一套读写路径。九、附件处理类型解析、扩展名修复与 Provider 上限附件上传链路中有若干容易被忽视的工程细节全部集中在 packages/accounting/src/utils.tsProvider 附件上限表PROVIDER_ATTACHMENT_CONFIG定义各 Provider 支持的文件类型与大小上限——QuickBooks 支持 PDF/JPEG/PNG/GIF/TIFF/BMP 且最大 20 MBXero 支持 PDF/JPEG/PNG/GIF 且最大 3 MBFortnox 支持 PDF/JPEG/PNG 且最大 10 MBMIME 三层解析resolveMimeType依次尝试“存储值是否受 Provider 支持 → 按文件扩展名推断 → 按魔数magic bytes检测”魔数检测覆盖 PDF/JPEG/PNG/GIF/TIFF/BMP/WebP无法解析或类型不受支持时返回带明确error的failed结果如File type application/tiff is not supported by xero扩展名修复ensureFileExtension对缺少扩展名的文件名按 MIME 补全invoiceapplication/pdf→invoice.pdf因为三家会计软件都要求合法扩展名流式读取streamToBuffer将 ReadableStream 或 Buffer 统一转为 Buffer 供上传使用。十、配置环境变量各 Provider 的 OAuth 凭据通过环境变量注入映射关系定义在 packages/accounting/src/index.ts 的PROVIDER_ENV_KEYS# Xero XERO_CLIENT_IDyour_client_id XERO_CLIENT_SECRETyour_client_secret XERO_OAUTH_REDIRECT_URLhttps://api.midday.ai/v1/apps/xero/oauth-callback # QuickBooks QUICKBOOKS_CLIENT_IDyour_client_id QUICKBOOKS_CLIENT_SECRETyour_client_secret QUICKBOOKS_OAUTH_REDIRECT_URLhttps://api.midday.ai/v1/apps/quickbooks/oauth-callback # Fortnox FORTNOX_CLIENT_IDyour_client_id FORTNOX_CLIENT_SECRETyour_client_secret FORTNOX_OAUTH_REDIRECT_URLhttps://api.midday.ai/v1/apps/fortnox/oauth-callback # OAuth state 加密密钥 ACCOUNTING_OAUTH_SECRET32_byte_encryption_keygetProviderOAuthCredentials会对clientId/clientSecret/redirectUri做缺失检查任何一项为空都会直接抛错使配置问题在任务启动阶段就暴露而不是在 API 调用时以晦涩形式出现。十一、错误处理错误码、重试策略与落库11.1 重试策略总览错误类型是否重试说明网络超时是BullMQ 指数退避限流 (429)是退避窗口允许恢复认证失败 (401)是先尝试刷新令牌数据非法 (400)否记录日志并标记 failed服务端错误 (5xx)是Provider 侧可能自行恢复11.2 面向前端的错误码体系packages/accounting/src/types.ts 定义了ACCOUNTING_ERROR_CODES与对应的用户可读消息ACCOUNTING_ERROR_MESSAGES覆盖会计年度缺失FINANCIAL_YEAR_MISSING/FINANCIAL_YEAR_SETUP_REQUIRED、认证过期AUTH_EXPIRED、限流RATE_LIMIT、校验类VALIDATION/NOT_FOUND/INVALID_ACCOUNT、服务端错误SERVER_ERROR、附件专用错误ATTACHMENT_UNSUPPORTED_TYPE/ATTACHMENT_TOO_LARGE/ATTACHMENT_TIMEOUT/ATTACHMENT_UPLOAD_FAILED/ATTACHMENT_NOT_FOUND以及UNKNOWN。getErrorMessage(code)为未知代码兜底返回通用消息。导出处理器中的deriveErrorCodeFromMessage进一步把各 Provider 的原始错误文本归一为错误码其识别规则体现了多 Provider 适配经验含 rate limit →RATE_LIMIT含 401/unauthorized/authentication failed →AUTH_EXPIRED含 financial year/fiscal year/瑞典语 bokföringsår →FINANCIAL_YEAR_MISSINGXero 的 Account code ... is not valid、Fortnox 的 konto 与错误码 2000106、QuickBooks 账户校验错误 →INVALID_ACCOUNT5xx 正则匹配 →SERVER_ERROR。错误落库示例await upsertAccountingSyncRecord(db, { transactionId: tx.id, teamId, provider: providerId, status: failed, errorMessage: error.message, errorCode: deriveErrorCodeFromMessage(error.message), });处理器最终返回结构化结果{ teamId, providerId, exportedCount, attachmentsSyncedCount, skippedCount, failedCount, exportedAt, errors: [{ code, message, transactionId }] }错误数组按 code/message 去重供前端按错误码展示针对性 toast。十二、安全考虑令牌存储OAuth 令牌加密后存入数据库apps.configState 参数OAuth state 使用加密HMAC 语义防篡改载荷经类型守卫校验非法载荷直接拒绝RLS 策略数据库按team_id强制团队级访问控制覆盖accounting_sync_records、transactions、appsWorker 使用 Service Role Key 绕过 RLS 执行后台任务API 密钥Provider 凭据仅存放于环境变量只被 Worker/API 进程读取审计追踪accounting_sync_records保留完整导出历史含失败原因与错误码失败任务在 Redis 中保留 7 天供排查。十三、当前局限Limitations无更新语义重导出总是创建新条目已存在条目无法原地更新附件删除部分支持QuickBooks 与 Fortnox 支持删除Xero 不支持附件会残留在 Xero银行账户映射当前取第一个活跃账户多账户映射尚在规划中apps.config中已预留defaultBankAccountId字段限流约束受各 Provider API 限额约束由入队延迟 批内节流自动消化Fortnox 凭证创建为已过账凭证Fortnox API 不支持草稿凭证。十四、延伸阅读包级总览文档packages/accounting/README.md架构深潜同步算法三阶段、令牌生命周期状态机、数据映射packages/accounting/ARCHITECTURE.mdProvider 接口与基类packages/accounting/src/provider.ts、packages/accounting/src/index.ts限流配置与错误码packages/accounting/src/types.ts队列与 Worker 选项apps/worker/src/queues/accounting.config.ts处理器实现apps/worker/src/processors/accounting/base.ts、apps/worker/src/processors/accounting/export-transactions.ts、apps/worker/src/processors/accounting/sync-attachments.ts整套设计可以概括为三条主线用 Provider 接口抽象屏蔽三家会计软件的 OAuth、实体模型与幂等差异用 sync_records 附件映射表把“导出状态”做成可查询、可恢复的数据用“入队预计算延迟 批内节流 指数退避”三层限流把 Provider 限额从运行时问题转化为调度问题。若你正在为财务类产品集成外部会计系统这套模式确定性幂等键、按任务维度而非全局维度调度、错误码归一后落库具有很强的可复用性。【免费下载链接】middayInvoicing, Time tracking, File reconciliation, Storage, Financial Overview your own Assistant made for Freelancers项目地址: https://gitcode.com/GitHub_Trending/mi/midday创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考