# Orchestra 首版积分计费需求 状态:2026-10-20 需求与架构提案;已按现有人工任务和 Debug 代码核对职责,默认单价与有效产出标准待补齐。 图解入口:[先把一次收费做好](/)。[首版收费表](/#decisions)、[DDD 与最小架构](/#architecture)、[两表 Schema 与事务](/#arch-schema)在同一页面。 ## 1. 首版只完成一次收费 租户共享一池积分:Orchestra 在最后确认计价参数、放行生成前预留积分,执行后按有效产出结算,释放未使用的冻结额。系统失败成本由平台承担,部分成功只收有效部分。详细用量继续记录,客户只取得公开的计费字段。 本轮已确认的简化: - 只检查租户可用余额,不设置成员累计总额度、周额度;每笔消费仍记录归属用户以供统计。 - 充值先人工改库,不建设充值、发放、到账确认或管理接口。 - 每类节点先用代码中的静态默认价,不建设调价发布和价格版本管理。 - 已扣积分纠错返还不在首版范围;执行失败或少收费产出时,仍应释放尚未消费的预留。 - 不设计老用户、旧运行迁移或租户启用切换。 首版仍使用现有 Nest 后端和 PostgreSQL,不引入 Redis。外部 worker 继续与 Conductor 通讯,不新增逐次直连 Orchestra 的扣费许可请求。 ## 2. 主流程与积分含义 ### 2.1 预留接在已有放行点 | 入口 | 首版接入顺序 | | ------------ | ------------------------------------------------------------------------------------------- | | 正式流程 | `OperateHumanTaskUseCase` 校验并规范化人工配置 → 预留成功 → `completeTask` 放行实际生成 | | Canvas Debug | `AdvanceDebugCanvasRunUseCase` 解析并校验实际输入 → 预留成功 → `runtime.start` 启动机器步骤 | 预留需要确定数量、模型等计价参数或可信费用上限,不要求歌词正文、生成音频等内容提前产生。省略值在 Orchestra 明确规范化,并用于真实提交,避免报价和 worker 采用不同默认值。 当前生成参数仍可在运行中的人工配置中确定,因此首版逐次预留,不建设整条工作流/批次集中预算。`productionQuantity` 是实例数,不是每个节点生成首数。配置与人工选择不因属于生成胖节点就重复收费。 ### 2.2 只保留余额、冻结与实际扣费 ```text 可用积分 = 租户余额 − 冻结额 ``` | 动作 | 租户余额变化 | 冻结变化 | | ---------------------------------- | ------------ | -------- | | 预留 H | 0 | +H | | 有效产出结算 C | −C | −H | | 确认无有效产出/确认未执行且已关闭 | 0 | −H | 始终满足 `余额 ≥ 冻结 ≥ 0`,且 `0 ≤ C ≤ H`。结算后的差额释放已经包含在冻结清零中,不能再增加一次余额。人工充值以余额增量写入,不覆盖并发消费,也不修改冻结。 预留、计费记录一起提交;结算状态、实际扣费、余额和冻结一起提交。余额不足或任一步失败全部回滚,不放行收费执行。 ### 2.3 按租户内的用户统计消费 每笔 BillingOperation 明确保存 consumerUserId,在预留时固定,不从当前负责人、回调调用者或可变 updatedBy 字段反推。人工触发必须有消费用户;仅确无用户的系统任务允许为空,并单列系统消费。服务端确定归属并校验租户关系;技术重试、回调、转派和移除成员均不改变历史归属。 按 tenantId + consumerUserId 分组,仅汇总 SETTLED 的 settledAmount,按 settledAt 筛选统计周期。HELD 的 reservedAmount 是待结算占用,单独统计;RELEASED 不计消费。用户更名或移除后保留用户 ID,用户关系不级联删除消费记录。只做查询汇总,不增加用户余额表或额外累计计数器。 归属默认建议:人工配置取实际提交人;Debug 取运行发起人;自动生成沿来源记录的业务发起用户,确无用户的系统任务单列,不能随意归给管理员。自动任务归属规则仍为建议。 ## 3. 默认价格与详细用量 实际默认单价尚未提供,HTML 的每首 10 积分只是演示。不同节点可以采用不同单位,但首版只使用明确的静态价格映射和简单计算函数。[待填写收费表](/#p01)只保留单位、默认价与有效产出标准。 | 节点 | 首版应补齐的收费规则 | 必须核实的事实 | | ---------------------- | -------------------------------------- | ------------------------------------------------------------------------------- | | AI/参考歌词 | 默认单价、按次/有效方案/token 等单位 | 有效歌词数;按 token 收费时的真实用量口径 | | Suno 各模式 | 默认单价、单位与部分成功计算 | 请求单位与有效歌曲数;`numGeneration=1` 表单显示 2 首,不能直接把请求值当歌曲数 | | RVC/SVC、声伴分离 | 首版收费或免费;收费时填默认价与单位 | 有效音频、请求音轨、可信时长;SVC 切片参数不是总时长 | | 配置、选择、上传和保存 | 建议免费 | 生成步骤与父节点不重复收费 | 价格、计费单位及计算结果随本次操作保存,足以解释这笔消费。首版没有价格版本表、租户特价、折扣、报价有效期或动态调价入口。 用量保存在计费操作的 JSON 中,按节点保留请求量、有效产出和实际执行尝试;歌词可包含输入/输出 token、缓存等真实明细。供应商成本未知不伪造为 0,也不为首版建设毛利核算。客户只返回数量、单价、实际费用和状态等计费字段,内部明细不整包公开。 技术尝试的完整用量用于内部记录,收费依据取业务最终确认的有效产出或计费用量,并受原授权上限约束;不能把每次尝试简单累加成额外客户费用。 Conductor 的 `taskId + scheduledTime` 用于识别执行尝试;业务上的一次消费和技术重试分别表达。采集事实不等于都参与收费。计价所需用量缺失时保留待核查,不能按 0 或猜测值结算。 ## 4. 进一步采用的首版建议 以下是为减少机制而提出的默认处理,尚未把建议改标为产品确认: | 场景 | 简单处理 | | -------------------------- | -------------------------------------------------------------------------------------------- | | 余额不足 | 拒绝本次提交;人工配置保持待提交,Debug 不启动。人工充值后用户重新提交,不新增自动等待和续跑 | | 成功与部分成功 | 按实际生成步骤可靠交付结算;未选用、下游失败不自动改变前置有效产出。具体有效标准随收费表确定 | | 单次费用上限 | 预留可执行的最大费用;不做执行中预算补额和贵模型切换交互。意外超出原授权部分由平台核查承担 | | 结果未知或取消尚未确认停止 | 先保留冻结并补查;无法确认时人工核对,不建设自动判责、审批和超时补偿平台 | | 技术重试 | 沿同一业务操作,失败与重复执行成本由平台承担,不形成额外客户账单 | 需要产品补齐的是首版收费表,包括默认价的小数精度与统一舍入。参数和用量是否真实可得、并发和恢复能否成立,分别由研发验证,不拆成大量产品待定卡。 ## 5. 最小架构与必要正确性 DDD、字段和事务统一见 [HTML 最小架构](/#architecture)。提案包含两张表: | 记录 | 职责 | | ---------------- | -------------------------------------------------------------------------------- | | BillingAccount | 每租户余额和冻结 | | BillingOperation | 一次生成的租户与消费用户、稳定来源、收费项目代码、当次价格、预留/扣费及用量依据 | 一个 BillingRepository 负责同一次数据库事务。静态价格映射、精确十进制计算保留在模块内。先不增加独立账本、执行尝试表、价格仓库和通用恢复平台;消费记录即可满足本轮明细查询,充值历史由人工操作自行留存。 ### 收费分类与执行来源 chargeCode 记录业务收费项目,例如 lyrics.generate(作词)、music.generate(作曲)、audio.convert(音频转换)。类型使用稳定字符串,后端从静态价格映射选择合法代码,客户端不能自由填写。不用数据库枚举;新增项目随对应实现和静态价格一同登记。显示名称保存在本次价格快照,改名称不改历史代码。 sourceType / sourceId 记录调用来源,例如正式工作流步骤或 Canvas Debug;nodeType / nodeVersion 只在节点来源存在时记录。收费项目与来源分别表达,未来非节点消费沿同一 chargeCode 规则记录。查询可按租户、用户、chargeCode 和结算时间组合统计,不新增分类表或可视化配置系统。 ### 5.1 业务提交与财务幂等分别负责 [代码与职责图解](/#identity-story)按真实人工任务、Debug 路径说明来源身份、消费用户与 key / hash / ref。 业务用例负责权限、参数校验、是否允许修改或重跑,并在调用计费前提供稳定 operationKey。正式流程可使用Orchestra 业务实例+配置 runtimeTaskId+生成 stepKey;Debug 可使用 runId+机器 stepKey,来源类型应有命名空间以防碰撞。BillingModule 不解释 key 中的业务结构,只以 tenantId + operationKey 唯一约束保证同一次消费不重复占额和建单。用户归属由业务入口确定,计费记录固定保存。 目前人工任务先检查负责人、最新任务身份及状态;pending 与 completed 分支不同,completed 进入重跑路径而生产适配器直接报 WORKFLOW_RERUN_DEFERRED。Debug 相同 sessionId 被拒绝,新 session 才创建新 Run;同一 Run 从 graphSnapshot 恢复并在步骤内部重试。业务模块按各自生命周期处理提交与恢复,计费模块不改变这些业务接口的语义。 BillingModule 不保存 inputHash、不判断生成参数冲突。重复财务调用返回原计费记录;业务用例根据任务生命周期决定请求响应与后续执行。 首版拟用 READ COMMITTED + 原子条件 UPDATE + 短事务。操作创建和账户变化同事务;结算/释放以 HELD 条件收尾原操作,再更新账户,只有一次生效。UPDATE 使用数据库行级写锁,并发更新同账户可能等待,等待后按新行重新检查余额条件。应用层不采用 version 式乐观锁。 ### 5.2 预留后的交接与收尾 来源校验、唯一键创建操作与冻结在同一个准备事务内完成。计费重复调用读取原单,不再次占额。来源业务用例负责在有效预留下启动、重试或核查原执行,不由 BillingModule 决定 HTTP 请求和生成参数的处理。 确定回执及执行绑定由现有来源模块保存,不在 BillingOperation 维护 dispatchState 或完整 dispatchPayload。预留已提交但执行结果未知时,计费保留原单与冻结;来源执行模块负责按原任务或 correlationId 核查,并按自己的协议处理技术重试。来源失效与预留共用源记录保护,顺序为源记录 → 操作 → 账户;已提交预留不能因源 Run 终态直接释放。 复用现有定时机制补查 HELD 操作,包括源运行已终态的操作。已接受的完整用量与必要产出标识保存到 usage;结算失败只重试原单,不重新生成。资金状态为 HELD / SETTLED / RELEASED,终态不重开。 ### 5.3 各阶段的数据库读写 [数据库图解](/#db-operations)只有“发起生成”和“结果结算”两个页签。初始 100、预留 20、每首 10 均为演示数值。 | 页签 | 数据库操作与事务边界 | | -------- | ---------------------------------------------------------------------------------------------------------------- | | 发起生成 | 一个预留事务固定消费用户、创建操作并按余额条件增加 held;提交后事务外调用 Conductor,确定回执由来源模块写回 | | 结果结算 | 事务外读取可信结果,将完整计费依据固定到 usage;另一短事务以 HELD 条件收尾原单,扣余额、清冻结,任一失败全部回滚 | ### 技术重试与一次收费的接入 现有 Debug startMachine 捕获 runtime.start 异常后会按配置重试。同一 Run / 步骤的技术尝试必须复用同一个 operationKey 和 BillingOperation,不增加冻结、不形成新客户账单;重试增加的成本由平台承担。最终只对原授权范围内的有效产出结算一次,不能按启动尝试次数收费。 人工任务的并发提交、Debug 的 session 管理及执行恢复由各自业务模块负责。计费接入需要保证稳定消费键和原单复用;不以“技术调用只能发生一次”作为一次收费的前提。回包未知与执行结果未知时保留原冻结,按可信结果收尾,不因单次启动异常直接释放。 ### 5.4 字段取舍 | 字段 | 首版设计 | | --------------- | -------------------------------------------------------------------------------------------------- | | consumerUserId | 增加明确字段,预留时固定,用于租户内的用户消费统计 | | chargeCode | 必填稳定字符串,表示收费项目;在后端静态价格映射中登记并校验,不采用数据库枚举,也不限定为节点类型 | | inputHash | 不设。参数一致性和提交冲突由业务模块处理,计费只按消费键保证资金操作幂等 | | outputRefs | 不独立设列,计费用到的产出标识合并到 usage,不复制作品内容 | | dispatchPayload | 不复制完整执行请求;参数、启动和技术重试由来源执行模块负责,计费模块不重放执行请求 | | dispatchState | 不重复维护执行交接状态;来源模块保存原任务状态与执行绑定,计费模块据此查询和收尾 | ## 6. 后端 API 规划 [API 图解与完整请求/响应示例](/#backend-api)是首版接口提案,尚未实现;本节权限安排也是建议。基础约定沿用 [REST API 唯一规范](https://github.com/Qiyin-Tech/orchestra-backend/blob/40d5ebf061c3affe525d7295704c6efbc9e7431b/docs/rest-api-design.md):`/v1`、Bearer Token、`X-Tenant-Id`、snake_case、成功 envelope 与既有错误信封。控制器返回 payload,全局拦截器包装一次。 ### 6.1 四个前端查询接口 | 方法与路径 | 读取内容 | 建议可见范围 | | ----------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------ | | `GET /v1/billing/accounts/current` | 当前租户 balance、held、available,同一次查询读取;金额为十进制字符串 | 所有有效租户成员 | | `GET /v1/billing/operations` | 收费项目、消费用户、状态、预留额、实际扣费及时间,按创建时间倒序分页 | admin/Owner 读本租户,普通成员仅读本人 consumerUserId | | `GET /v1/billing/operations/:id` | 列表字段加公开计费单位、数量、单价与业务来源引用 | 与列表相同;跨租户或不在可见范围统一 404 | | `GET /v1/billing/user-consumptions` | 指定结算周期内按消费用户分组的已扣金额与笔数 | 当前租户有效 admin/Owner | 无账户时余额接口返回三个 `"0"`,不在 GET 建行。金额、价格和计费数量均以字符串传输;HELD 的 settled_amount 和未确定计费数量为 null,RELEASED 已扣金额为 `"0"`。详情通过白名单 DTO 返回计费依据,不返回完整 usage、供应商成本、operationKey 或执行请求。来源引用不授予原任务的额外访问权;ref 只提供可公开的用户摘要。 列表与汇总沿用 page_index 默认 1、page_size 默认 20/最大 100,返回 data.list、page_index、page_size、total。列表筛选 state、consumer_user_id、charge_code;source_type / source_id 成对可选,created_from / created_to 成对可选,按创建时间过滤,排序 created_at DESC、id DESC。普通成员指定其他用户返回 403;列表和 total 使用相同范围。 用户汇总必填 settled_from / settled_to,采用带时区时间戳、左闭右开区间,最长 366 天,按 settledAt 过滤后仅汇总 SETTLED;可选 consumer_user_id、charge_code。排序 settled_amount DESC、consumer_user_id ASC,金额相同时系统消费在后;total 是用户分组数,summary 汇总全部匹配记录,不只当前页。移除用户仍保留历史 ID,摘要可为空,系统消费单列。HELD 当前占用到余额及对应列表查看,不混入周期消费。 同一分页响应的 list、total、summary 使用一个数据库快照;多次请求间可发生新消费。使用 JwtAuthGuard、TenantContextGuard,统计再使用 TenantRolesGuard;每次请求重新验证有效身份、租户成员和角色,平台管理员不绕过租户权限。 ### 6.2 沿现有生成入口接入 前端继续调用 `POST /v1/human-tasks/operations` 与 `POST /v1/canvas-debug-run`,不提供 operationKey、价格或扣费额。人工配置完成校验后预留,再 completeTask;不足建议返回 HTTP 409/code 40900,保持待提交并不放行。 Debug 仍先创建 Run,再解析输入、预留和启动机器步骤。若 Run 已创建后推进因余额不足失败,沿现有 201 + Run.failure 表达,failure.code 建议为 `billing_insufficient_balance`;不能描述为整个 Run 创建已回滚。余额不足属于业务失败,不按启动瞬时异常自动重试;充值后的再次提交沿用既有 Debug 会话规则。 现有成功响应建议增加可选 billing_operation_ids 数组,非收费步骤为空。前端按这些 ID 查询消费详情,并在既有运行状态刷新时更新余额和账单。HTTP 成功不代表已结算,余额快照不作为执行放行凭证。网络错误后查询原任务/Run 和原消费,再按来源业务规则恢复;技术重试继续复用同一消费。 ### 6.3 两个进程内资金用例 - `prepareOperation`:接收业务确定的租户、消费用户、稳定 operationKey、合法 chargeCode、来源和规范化计价输入;按静态价格计算预留,创建或返回原操作,在短事务内占额。返回原操作 ID、状态和预留额;不比较生成参数。 - `syncAndSettleOperation`:业务同步或现有定时任务传租户与原操作 ID,经可信来源读取结果与用量,固定计费依据后按原价计算费用,再以短事务结算或释放。结果未知保留 HELD,终态重复调用返回原值。调用方不传扣费金额,不自行配对保存用量和更新账户。 资金用例仅在 Nest 进程内调用。首版不新增浏览器预留/扣费/释放接口、worker 扣费许可接口、充值或调价写接口。 查询错误沿现有 HTTP / 业务码:400/40000 参数或租户头缺失,401/40100 未认证,403/40300 越权,404/40400 明细不可见,500/50000 查询或存储失败;错误 data 为 null。生成入口的余额不足按上述两条业务路径表达,不混为查询错误。结果未知正常返回 HELD,不伪装成零消费或释放成功。 ## 7. 首版验证范围 | 主题 | 关键场景 | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | | API 查询 | 鉴权、跨租户与成员越权、角色降级;列表筛选与统计周期边界、历史用户、分页一致性;金额字符串与 null,不泄漏内部用量 | | 正常收费 | 按租户和消费用户准确汇总,转派、移除和回调不改变历史归属;全部成功、部分成功、无有效产出;每笔收费有可解释的数量与默认价;内部用量不泄漏 | | 并发与重复 | 100 同时预留两个 80,最多一个成功;重复提交只占额一次;结算/释放竞争只收尾一次;人工增量充值不覆盖消费 | | 中断与源状态 | 预留后未交接、交接回包丢失、旧 Debug 替换、结果已到但结算中断;查原操作,保留产物,不因重试结账重新生成 | ## 8. 代码依据与证据边界 核对基线:`dev` / `40d5ebf061c3affe525d7295704c6efbc9e7431b`。本轮仍是设计,未修改业务代码、实际 Prisma schema 或执行迁移。已运行人工任务操作、重跑适配器、Debug 推进、Debug 仓库四组现有单元测试,52 个测试通过。测试确认既有业务行为,不代表计费功能或真实引擎行为已实测。 | 已核实的事实 | 对首版的作用 | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | | [人工提交](https://github.com/Qiyin-Tech/orchestra-backend/blob/40d5ebf061c3affe525d7295704c6efbc9e7431b/src/modules/human-tasks/application/task/operate-human-task.use-case.ts)先验证输出,再 completeTask;Conductor 完成与后端数据库保存没有共同事务 | 在放行前预留,固定当次价格和消费归属,回执与执行绑定由来源模块负责 | | [Debug 推进](https://github.com/Qiyin-Tech/orchestra-backend/blob/40d5ebf061c3affe525d7295704c6efbc9e7431b/src/modules/canvas-run/application/use-cases/advance-debug-canvas-run.use-case.ts)先 resolveInput 再 runtime.start;[新会话替换](https://github.com/Qiyin-Tech/orchestra-backend/blob/40d5ebf061c3affe525d7295704c6efbc9e7431b/src/modules/canvas-run/infrastructure/repositories/prisma-canvas-run.repository.ts)废止旧 Run 推进租约 | 在机器启动前预留;同一 Run / 步骤的技术重试复用原计费记录,未知结果保持原冻结 | | [发布步骤](https://github.com/Qiyin-Tech/orchestra-backend/blob/40d5ebf061c3affe525d7295704c6efbc9e7431b/prisma/schema/workflows.prisma)冻结节点与 stepKey;[NodeRunRecords](https://github.com/Qiyin-Tech/orchestra-backend/blob/40d5ebf061c3affe525d7295704c6efbc9e7431b/prisma/schema/workflow-instances.prisma)按逻辑节点汇总 | 复用执行身份,不把逻辑节点 success、当前行 ID 或行数当唯一消费和有效产出 | | [运行注册](https://github.com/Qiyin-Tech/orchestra-backend/blob/40d5ebf061c3affe525d7295704c6efbc9e7431b/src/modules/workflows/workflow-node-runtime-bootstrap.service.ts)只在本后端注册 human/internal-worker;生成 worker 沿 Conductor 通道 | 用量从可信 Conductor 输出读取,缺必要字段时补输出,不要求 worker 直连计费许可接口 | 外部 worker 源码、真实 Conductor 行为和 PostgreSQL 并发结果尚未实测;首版接入时按第 7 节验证。