Civitai 的 `@civitai/buzz` 包:Buzz 服务类型化 HTTP 客户端与共享货币化规则解析
Civitai 的civitai/buzz包Buzz 服务类型化 HTTP 客户端与共享货币化规则解析【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai导读civitai/buzz是 Civitai 仓库packages/civitai-buzz中面向服务端的 Buzz 服务客户端统一封装了 buzz 服务的fetch 重试 状态码→异常的 HTTP 传输层并为每个端点提供类型化方法同时以纯函数、浏览器安全的方式承载账户类型映射与货币化规则被 Next.js 主应用与 SvelteKit 各 spoke 共同复用。读完本文你将掌握该包的边界划分、客户端初始化与全部端点方法、BUZZ_ENDPOINT环境变量契约、错误映射与可安全重试语义以及配套的授权费、付费访问、定价额度等纯规则模块的设计意图与调用方式。包的定位与设计边界一个被两个应用共享的单一 buzz 传输层仓库中同时存在多个消费方Next.js 主应用src/server/services/buzz.service.ts中的buzzService与 SvelteKit 的 creator-studio spokeapps/creator-studio/src/lib/server/buzz.ts。在引入本包之前各应用各自手写 buzz HTTP 调用容易产生重复与漂移。本包的 README 明确其设计意图Shared by the main app and the SvelteKit spokes so nobody hand-rolls the buzz HTTP transport.即让任何人都不再手写 buzz 的 HTTP 传输层。包内client.ts头部注释进一步界定了职责Owns the HTTP transport (fetch retry status→error) and typed per-endpoint methods. Domain concerns (balance pre-checks, entity lookups, DB co-writes) stay in the consuming app.也就是说客户端只负责传输与端点方法而余额预检、实体归属查询、数据库协同写入等更上层的buzz 编排逻辑保留在消费应用内或未来建立在它之上的civitai/monetization中。服务端专属浏览器中不可调用Buzz 服务是内部服务BUZZ_ENDPOINT指向的 .NET/Postgres API永不对外暴露因此clientcreateBuzzClient是 server-only禁止在浏览器代码中调用account-types 辅助模块toApiType、BuzzAccountType等是纯函数、浏览器安全可安全地在客户端代码中使用。这一约束也体现在消费端creator-studio 的apps/creator-studio/src/lib/server/buzz.ts将客户端惰性构造并缓存在globalThis上vite build阶段不解析端点dev HMR 复用同一实例import { createBuzzClient } from civitai/buzz; type BuzzClient ReturnTypetypeof createBuzzClient; const g globalThis as unknown as { buzzClient?: BuzzClient }; export function getBuzz(): BuzzClient { if (!g.buzzClient) g.buzzClient createBuzzClient(); return g.buzzClient; }以raw方式发布消费方自行转译package.json中main: ./src/index.ts、types: ./src/index.ts即包以原始 TypeScript 发布与其他civitai/*包保持一致由消费方转译Next 主应用在 next.config.mjs 的transpilePackages中列出civitai/buzzcreator-studio 在 apps/creator-studio/vite.config.ts 的ssr.noExternal中列出civitai/buzz源码注释civitai/* packages ship raw TS (main: ./src/index.ts) — let Vite transpile them。其运行依赖极简仅civitai/sharedworkspace与zod测试通过vitestvitest.config.ts限定src/**/*.test.tsNode 环境。快速上手初始化客户端基础用法import { createBuzzClient } from civitai/buzz; const buzzService createBuzzClient({ // endpoint 可选缺省时读取 BUZZ_ENDPOINT 环境变量 mapError: (e) { /* 可选把 BuzzApiError.status 转成你的框架错误类型 */ }, }); const account await buzzService.getUserBuzzByAccountType(userId, yellow); const report await buzzService.getUserTransactionsReport(userId, query); await buzzService.createTransaction({ fromAccountId, toAccountId, amount, type, /* … */ });createBuzzClient(options)接受CreateBuzzClientOptions定义于 client.ts关键选项如下选项类型默认值说明endpointstringBUZZ_ENDPOINT覆盖环境变量指定的服务基地址retriesnumber3失败重试次数与主应用此前行为一致transactionTimeoutMsnumber无不超时写调用的默认请求超时缺省保持历史的无界行为logBuzzLogFnno-op调试日志函数应用自定义mapError(error: BuzzApiError) unknown直接抛BuzzApiError重试耗尽后将错误映射为消费方自己的错误如 tRPC/HTTP端点方法清单客户端返回带类型的端点方法全部在 client.ts 中返回读getAccount、getUserBuzzByAccountType、getUserAccounts、getAccountTransactions、getUserTransactionsReport、getAccountBalances、getAccountSummary、getContributors、getCounterparties、previewMultiTransaction、listMultiTransactions另有getTransactionByExternalId对 404 返回null。写createTransaction、createTransactions、refundTransaction、createMultiTransaction、refundMultiTransaction。逃生舱requestT(urlPart, init?)可调用任何尚未封装成方法的端点此外还有ping(timeoutMs 1000)用于轻量存活探测永不抛错任何异常均返回false绕过重试与mapError。非 2xx 状态码时客户端抛出BuzzApiError携带status若提供了mapError则抛出其返回值。账户类型友好名 ↔ API 名的唯一契约账户类型映射是civitai/buzz承担的最重要的单一事实来源。应用侧使用友好账户类型yellow/blue/green/red、creatorProgramBank、cashPending等而 buzz 服务端使用 PascalCase 的 API 账户类型User/Generation/Blue/FakeRed等。映射表clientToApiAccountType定义于 account-types.tsexport const clientToApiAccountType: RecordBuzzAccountType, BuzzApiAccountType { blue: Generation, green: Green, yellow: User, red: FakeRed, creatorProgramBank: CreatorProgramBank, creatorProgramBankGreen: CreatorProgramBankGreen, cashPending: CashPending, cashSettled: CashSettled, club: Club, };toApiType(type)负责友好→API 映射toClientType(value)反向解析同时容忍 API 名、小写 API 名与友好名三种写法见apiTypesMap的构造toApiTransaction(transaction)将事务负载中的fromAccountType/toAccountType一并转换。Buzz API 侧完整账户类型枚举buzzApiAccountTypes也在此定义并在主应用 src/shared/constants/buzz.constants.ts 中重新导出BuzzTypes类将toApiType委托给包内实现确保映射只有一个来源、不会漂移而 UX 配置buzzTypeConfig的nsfw/purchasable/bankable/disabled等标志保留在应用侧。环境变量BUZZ_ENDPOINT 契约包内 env.ts 用 zod 定义了包自有环境变量模式const isProd process.env.NODE_ENV production; const schema z.object({ BUZZ_ENDPOINT: isProd ? z.url() : z.url().optional(), });变量是否必需说明BUZZ_ENDPOINT仅生产环境buzz 服务的基地址 URL开发环境可选缺省时客户端按需抛错关键实现细节惰性且记忆化loadBuzzEnv()仅在客户端首次解析端点时才执行 zod 校验并缓存结果裸import永不触碰process.env因此构建、测试、脚本阶段不会抛错每次请求惰性解析createBuzzClient中的endpoint()每次请求时解析envOverrides.endpoint ?? loadBuzzEnv().endpoint缺省且未提供endpoint时抛出Missing BUZZ_ENDPOINTclient.ts。传输层原理查询序列化、重试与错误映射查询参数序列化toQueryStringclient.ts将结构化查询对象序列化为查询串丢弃undefined/null/数组展开为重复键其余值字符串化无参数时返回空串不带?。日期序列化分两种isoDateDate→ 完整 ISO 字符串用于cursor/start/end等dateOnlyDate→YYYY-MM-DDUTC匹配 report/summary 端点的日期语义。各端点查询类型集中在 queries.ts例如GetAccountTransactionsQuerytype、cursor、start、end、limit、descending与GetTransactionsReportQueryaccountType、window、start、end。window取值hour | day | week | monthgetAccountSummary额外支持year。写操作的重试语义只有确定未发生才重试包内对非幂等写操作的重试做了精心设计。withRetriesclient.ts在缺省谓词时保留历史行为全部重试但一旦提供了shouldRetry谓词谓词不确定返回非 true 或抛错时绝不重试——对非幂等写而言我不知道必须等于不要重发。导出的isSafeToRetryclient.ts是一个刻意收窄的允许清单export const isSafeToRetry (error: unknown) { const code (error as { cause?: { code?: string } } | null)?.cause?.code; return ( error instanceof TypeError (code ECONNREFUSED || code ENOTFOUND || code EAI_AGAIN) ); };其设计理由源码注释明确阐述判断准则是服务器可能已经执行过这个请求吗——只有连接被拒ECONNREFUSED、域名不存在ENOTFOUND、DNS 临时失败EAI_AGAIN这些 Node fetch 报出的TypeError携带 syscall 错误码才能确定什么都没发生反例超时绝不应重试——客户端 abort 不会取消服务端执行重试超时的非幂等写会把第二个相同 external id 的请求送上线路504只说明网关放弃不说明写是否落库收益滚动重启导致的几秒拒连可以被客户端毫秒级吸收而调用方自己的重试循环通常粒度更粗。BuzzWriteOptionsclient.ts支持按调用覆盖timeoutMsopt-in缺省保持无界——持有锁的调用方需要请求不再在途这一事实最终成立无超时则不存在该时点锁 TTL 无从设定、retries0完全禁用内部重试、shouldRetry。所有写调用统一经过post()携带JSON_HEADERS与可配置的timeoutMs/retries/shouldRetry读调用保持原状。错误映射主应用的真实案例主应用 src/server/services/buzz.service.ts 集中演示了mapError的用法——把 buzz 状态码映射为 tRPC 错误export const buzzService createBuzzClient({ endpoint: env.BUZZ_ENDPOINT, log: isDev ? (message, ...args) console.log(message, ...args) : undefined, mapError: (error) { switch (error.status) { case 400: throw throwBadRequestError(null, error); case 404: throw new TRPCError({ code: NOT_FOUND, message: Not found, cause: error }); case 409: throw throwBadRequestError(There is a conflict with the transaction, error); default: throw new TRPCError({ code: INTERNAL_SERVER_ERROR, message: An unexpected error ocurred, please try again later, cause: error, }); } }, });由于该映射是有损的400 与 409 共享同一 code 与 message每个分支都把BuzzApiError作为cause保留调用方需要区分状态码时通过 src/server/utils/buzz-error.ts 的getBuzzApiStatus(error)读取原始 status兼容BuzzApiError直抛与包在TRPCError内的两种情况。响应模型线上数据的原始形状responses.ts 定义各端点的线上响应类型消费方转换前的原始形状账户类型字段一律为 API 值PascalCaseBuzzApiAccountType。要点BuzzTransactionResponse单笔事务date、type、from/toAccountId、from/toAccountType、amount、可选description/detailsGetAccountTransactionsResponse{ cursor?, transactions[] }游标分页GetTransactionsReportResponseBuzzTransactionsReportEntry[]按天聚合accounts[{accountType, spent, gained}]GetAccountBalancesResponseBuzzAccountBalance[]accountId/accountType/balanceGetAccountSummaryResponse按账户 id 分组的{ data: BuzzAccountSummaryRecord[], cursor }记录GetContributorsResponse按账户分组的BuzzContributor[]GetCounterpartiesResponse对端聚合inwardsBalance/outwardsBalance/totalBalancePreviewMultiTransactionResponse预演多账户事务isPossible分支携带remainingAmount余额不足分支携带shortfall两者在线上互斥CreateMultiTransactionResponse/RefundMultiTransactionResponse返回transactionIds[]含duplicate标记与退款明细CreateTransactionResponse{ transactionId: string | null, remainingBalance: number | null }——当 from 账户未在本地追踪如 bank/0时remainingBalance为 null。共享货币化规则纯函数模块civitai/buzz的价值不止于传输层。仓库将主应用与 creator-studio 必须一致执行的货币化规则下沉为纯函数模块浏览器安全、无框架依赖每个模块都配套 vitest 测试。licensing-fee授权费的每张图换算licensing-fee.ts 是授权费的唯一换算来源创作者以整数比N ⚡ / M 张图表达授权费绝不用小数存储/收费值为每张图费用ModelVersion.licensingFee DECIMAL(10,2)的 0.01 精度。核心常量与函数MAX_LICENSING_FEE 100单图授权费上限BuzzVIDEO_CAP_MULTIPLIER 5视频生成成本远高于图片故视频模型的费用上限是图片的 5 倍不作用于月度定价额度因为额度计数而非量FEE_IMAGE_OPTIONS [1, 10, 20, 50, 100]创作者 UI 提供的分母下拉选择而非自由输入每个已存储费用都能映射到其中之一保持升序且必须保留 1000.01 列精度下的最细粒度保证每个费用精确可表示DEFAULT_FEE_IMAGES 10feeToRatio(perImage)单图费用 → 整数比用整数百分位运算保持浮点安全如1 → {1,1}、0.1 → {1,10}、0.5 → {5,10}、0.05 → {1,20}、0.01 → {1,100}null/0 → offratioToFee(buzz, images)其逆运算结果始终是 0.01 的整数倍满足列与 schema 的multipleOf(0.01)suggestedFeePerImage(modelType, mediaType?)按模型类型给出建议值——Checkpoint 为1SUGGESTED_FEE_PER_IMAGE { Checkpoint: 1 }其余默认0.1即 1 ⚡/10 张视频乘 5maxFeeBuzzForRatio(images, mediaType?)floor(上限 × images)供编辑器在整数域显示N buzz per M generations的最大值展示辅助formatFeeCadence(count)per generation/per 10 generations与formatFeeRatio(perImage)1 ⚡ / generation、5 ⚡ / 10 generations或 Off。media-type上限的媒体轴media-type.ts 的capMediaType(baseModel)决定费用上限沿哪条轴解析基于civitai/shared/basemodel.constants的getBaseModelMediaType命中video则按视频其余含未知 base model、Other、音频、3D、双类型生态一律按图片——图片是两者中更严格的匹配失败只会少收、绝不会让创作者超出其未挣得的上限。monetization-limits一个调用拿到全部上限monetization-limits.ts 将各上限组装为monetizationLimits({ tier, baseModel })返回的MonetizationLimitstype MonetizationLimits { fee: { maxPerGeneration: number; // 每次生成的费用上限对所有创作者相同仅媒体轴改变它 denominators: number[]; // 编辑器可提供的分母 }; allowance: { monthlyPrices: number | null }; // 本月可新增价格数null 无限 };配套规则全部有测试佐证见 monetization-limits.test.tsresolveCapTier(membership)唯一的层级归一规则——非会员回退free而非无访问founder按bronze计永远返回真实层级非 null调用方不再需要到处写?? free会员层级只决定月度额度费用上限对所有人相同free与gold的maxPerGeneration相等suggestedFee刻意不纳入MonetizationLimits它随模型类型与媒体变化、与层级无关接收baseModel而非媒体类型避免调用方命名错误的轴seedFeeRatio决定编辑器打开时的初值已有费用优先否则按类型建议只有建议分支被denominators钳制已有费用永不被钳制创作者始终以其实际存储的分母打开feeMaxFor(limits, images)在整数域给出编辑器上限测试验证与maxFeeBuzzForRatio在 [1,2,10,20,50,100] 全部分母一致测试还固化了一条容易被忽视的规则版主不豁免额度——他们豁免的是费用上限在写路径应用但从不豁免资格下限与额度否则 UI 上报的无限额度会被服务端拒绝。paid-access付费访问门槛与定时销售paid-access.ts 定义PaidAccessEntityType ModelVersion | ComicChapter的付费访问模型ModelVersionTermsdownload为全访问档购买即含生成generation描述非买家如何生成{ free: true }免费 / 带price?、trialLimit?的付费纯生成档acceptsBlueBuzz表示创作者同意同时接受 Blue Buzz 付款门槛判活isPaidAccessActiveendsAt null永久 或endsAt now、isTimedGateActive排除永久门槛回答是否存在可能提前结束的限时窗口buildModelVersionTerms把编辑器定价accessPrice、generationPrice、freePreviewGenerations、genOnly、freeGeneration、acceptsBlueBuzz统一构造成 terms供站内表单与 Creator Studio 使用避免两处漂移政策门槛paidAccessBlockedForPOI 模型或Private模型禁止付费门槛、licensingFeeBlockedForPOI 模型禁止授权费私有模型保留定时销售CU 868ktk1kuModelVersionSaleWindowFixed/Percent折扣、startsAt/endsAt/canceledAt配套SALE_DAYS_BY_TIERfree 3 / founder 7 / bronze 7 / silver 14 / gold 30、MAX_SALE_LEAD_DAYS 14、MIN_SALE_PRICE 1零 Buzz 购买不写账本行且 30 天退款路径从账本读回金额免费购买将无法退款且对报表不可见saleDiscountFor的百分比向下取整33% 的 33 取 10 而非 11源码注释明确警告不要好心改成Math.rounddiscountedPrice/discountedTerms是唯一的折后价计算入口仓库曾因按钮减了服务端从未应用的折扣而付出代价bestSaleFor在重叠销售中取减得最多者saleDaysCharged计算已取消销售返还尾段天数saleDaysUsed按开始月份计跨月销售不重复计费。pricing-allowance月度定价额度pricing-allowance.ts 定义会员实际治理的唯一货币化维度export const MONTHLY_PRICING_ALLOWANCE_BY_TIER: Recordstring, number { free: 3, founder: 10, // 遗留付费层级额度对齐 bronze bronze: 10, silver: 25, gold: Infinity, };一个价格 授权费或永久付费门槛定时 Early Access 窗口不占额度窗口关闭即自我出价淘汰编辑已设价格也不占额度按实体首次定价计数每个实体无论带几种价格只占一格未知或失效层级回退free 额度而非 0失去会员绝不能剥夺定价能力也绝不能影响已设价格资格下限MONETIZATION_MIN_CREATOR_SCORE 10_000paid-access.tspricingEligibility(score)对缺失/异常分数关闭失败按 0 处理因为这决定谁能开始收费pricingFloorMessage提供带个人进度的拒绝文案状态工具pricingAllowanceStateunlimited/remaining/atLimitexempt用于正在编辑的项已定价时避免误报已用完、formatPricingAllowance统一计数文案、shouldUpsellAllowance达到CAP_UPSELL_THRESHOLD 0.8且存在更高层级时提示升级、tierAllowanceRows展示用Infinity以null序列化。rights-affirmation货币化的权利声明rights-affirmation.ts 承载创作者在版本被货币化付费访问或授权费前必须接受的权利声明MONETIZATION_RIGHTS_AFFIRMATION_VERSION 1与MONETIZATION_RIGHTS_AFFIRMATION_STATEMENT声明文本原样存储在版本上改词即升版旧记录保留其实际同意的文本buildRightsAffirmation(userId)构造记录readRightsAffirmation(meta)逐字段校验这整条记录的证明价值半成品绝不能通过门槛读取失败则视为缺失、要求重新确认hasCurrentRightsAffirmation(meta, ownerId?)校验当前措辞版本并可要求确认者仍是当前所有者声明是具名者承担责任不随模型转移paidAccessCharges(input)判定输入是否真的收费无永久标记且无时限、或{ free: true }的生成授权都不算收费因此无需声明。creator-program补偿池价值换算creator-program.ts 是 Creator Program 补偿池的纯价值换算主应用src/server/utils/creator-program.utils.ts重新导出creator-studio 的apps/creator-studio/src/lib/server/creator-program.ts同样复用getForecastedValue(toBank, pool)按预测池规模折算你的 Buzz 可能值 $X的入池/赚取预估getCurrentValue(toBank, pool)按**当前实时**池规模折算已入池金额实时池为空时返回 0两者都硬性封顶$1 / 1000 buzzMath.min(比例折算, toBank / 1000)池规模为零/缺失时按上限处理÷∞。测试与消费方式包的 vitest 配置vitest.config.ts限定 Node 环境、include: [src/**/*.test.ts]运行pnpm --filter civitai/buzz test即可执行。仓库内的测试覆盖monetization-limits.test.ts层级归一founder→bronze、失效会员→free、永远非 null、视频 5 倍上限、各层级月度额度3/10/25/∞、版主不豁免额度、编辑器建议可保存性等paid-access.test.ts、pricing-allowance.test.ts、rights-affirmation.test.ts分别覆盖门槛/销售、额度计数、声明校验。主应用在 src/server/services/tests/ 下的多个测试如buzz.service.transactions-report.test.ts、buzz.service.multi-account-destination.test.ts、buzz.service.multiplier-floor.test.ts以vi.mock(civitai/buzz)的方式 mockcreateBuzzClient从中可以直观看到消费方与包的契约mock 只 stub 被测试路径实际用到的端点方法getUserTransactionsReport、createMultiTransaction等。creator-studio 侧除服务端 apps/creator-studio/src/lib/server/buzz.ts 外apps/creator-studio/src/lib/components/下大量 Svelte 组件BulkActionDialog.svelte、PaidAccessEditor.svelte、LicensingFeeFields.svelte等与apps/creator-studio/src/lib/monetization/的测试如gate-eligibility.test.ts、sales.test.ts直接消费本包的纯规则模块。小结civitai/buzz的架构可以概括为传输层收拢、规则层下沉HTTP 传输fetch 重试 状态→异常与类型化端点方法收拢到一个 server-only 客户端让主应用与 SvelteKit spokes 不再各写各的账户类型映射、授权费换算、付费门槛、定价额度、权利声明等跨应用必须一致的规则则以纯函数形式下沉到浏览器安全的共享模块并配以 vitest 测试固化。这种共享规则 应用各自编排的边界划分正是多应用 monorepo 中避免业务逻辑漂移的实践范本——在civitai/buzz之上未来的civitai/monetization可以进一步承接更上层的货币化编排。【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考