Civitai Orchestrator 工作流查询统一化:从双端点走向类型感知的单一路由
【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai
导读
本文围绕 Civitai 主站(当前仓库src/目录下的 Next.js 应用)中的一份架构演进文档 docs/unified-workflow-query.md 展开,剖析其提出的"统一工作流查询端点"(Unified Workflow Query Endpoint)方案:将图片生成查询与提示词增强查询合并为单一 tRPC 端点,并让服务端格式化层具备类型感知能力。读完本文,你将理解当前双端点架构的分歧点、统一方案的四个实施步骤、其中涉及的四个关键挑战,以及源码中已有的部分统一化进展(queryWorkflowsByTags),可以直接将该方案应用于自己的 orchestrator 集成或同类多类型查询场景。
一、现状:两条并行的查询路径
文档首先指出,当前存在两个彼此独立的查询端点,它们调用的是同一个底层 orchestrator API,分歧完全发生在服务端格式化层:
| 端点 | Tag 过滤 | 服务端处理 | 客户端消费方 |
|---|---|---|---|
queryGeneratedImages | generation | formatGenerationResponse2(约 150~200 行:资源富化、遗留元数据归一化、图片 blob 格式化) | Queue.tsx、Feed.tsx,经useGetTextToImageRequests |
queryPromptEnhancements | prompt-enhancement | 无(queryWorkflows原始透传) | HistoryTab.tsx,经useGetPromptEnhancementHistory |
在源码中可以完整印证这张表。路由定义位于 src/server/routers/orchestrator.router.ts:
queryGeneratedImages(第 331 行起)通过queryGeneratedImageWorkflows2处理,并在 green 域名下追加'green'tag、开启cache: true;- 通用按 tag 查询
queryWorkflowsByTags(第 211 行起)是提示词增强历史的实际落点,其注释明确写着"Generic workflow query by tags — used for prompt enhancement history, future text workflows, etc.",即该端点从设计上就是类型无关的。
客户端两侧的消费方式也各不相同:
- 图片侧
useGetTextToImageRequests(src/components/ImageGeneration/utils/generationRequestHooks.ts)会registerSignalGroup('generation'),把WORKFLOW_TAGS.GENERATION(实际常量值为'gen',见 src/shared/constants/generation.constants.ts)与筛选器 tag 拼接后调用useInfiniteQuery,随后把每条 workflow 包成new WorkflowData(workflow, { domain, nsfwEnabled }); - 提示词侧
useGetPromptEnhancementHistory(src/components/Generation/PromptEnhance/promptEnhanceHooks.ts)直接以{ tags: ['prompt-enhancement'] }调用queryWorkflowsByTags.useInfiniteQuery,再经mapWorkflowToRecord映射为PromptEnhancementRecord(包含 originalPrompt、enhancedPrompt、issues、recommendations、temperature 等字段),供 HistoryTab.tsx 渲染。
二、共享底座:类型无关的queryWorkflows
两条路径之所以能统一,关键在于它们最终都汇入同一个底层函数queryWorkflows。该函数位于 src/server/services/orchestrator/workflows.ts,本身不关心 workflow 的具体类型,它承担了三件公共职责:
- 强制注入
'civitai'tag:tags: ['civitai', ...(query.tags ?? [])],确保查询始终限定在 civitai 域内; - 读取超时兜底:
AbortSignal.timeout(ORCHESTRATOR_QUERY_TIMEOUT_MS),常量值为 20 秒(workflows.ts),防止 orchestrator 挂起时无限阻塞并占死 api 连接池;触发后会被识别为可重试的 503(统一文案ORCHESTRATOR_UNAVAILABLE_MESSAGE); - 错误分类映射:400 → BadRequest、401 → Authorization、403 → InsufficientFunds、429 → RateLimit、404 → NotFound,其余 4xx 归为 BadRequest,上游 5xx/网络故障归为可重试 503,未知错误原样抛出(保持真实 bug 可见)。
同时,单条读取getWorkflow也有独立的 20 秒兜底(ORCHESTRATOR_GET_TIMEOUT_MS),服务于statusUpdate轮询热路径。可以推断:既然底层查询天然类型无关,统一化的工作重心确实如文档所说,全部集中在"服务端格式化层 + 客户端消费层"。
三、分歧点:formatGenerationResponse2的图片假设
queryGeneratedImages之所以需要专门的服务端处理,是因为formatGenerationResponse2(src/server/services/orchestrator/orchestration-new.service.ts)假定每个 workflow 都包含图片/视频步骤。它的处理流水线包括:
- 资源富化:从
workflow.metadata.resources和每个 step 的getResourceRefsFromStep中收集资源 ID,按(id, epoch)去重(保证同一版本的不同 epoch 不被合并),然后批量调用getResourceData命中数据库,把模型名、版本、epoch 号等信息补全到响应中(Raw-AIR 负 ID 资源跳过富化); - 新格式归一化:
workflow.metadata.params+workflow.metadata.resources,其中normalizedWfMeta是显式白名单而非展开——每个暴露给调用方的 key 都必须被命名,例如remixOfId、isPrivateGeneration、modelSubstitutions都是逐项挑选的; - 遗留格式回退:无 workflow 级 metadata 时回退到
steps[0].metadata(标准生成),或step.metadata.transformations[last](遗留增强格式); - 步骤格式化:
formatStep再对每个 step 做输出归一化。formatStepOutputs内部已经是一个按step.$type分发的 switch(orchestration-new.service.ts),覆盖comfy、imageGen/textToImage、model3DPreview、imageUpscaler、preprocessImage、videoGen(含 LTX 2.3 的additionalVideos多视频批处理)、videoUpscaler/videoEnhancement/videoInterpolation、preprocessVideo、aceStepAudio、miniMaxMusic3、polyGen(合并 rigged/animated 变体)等类型,default返回空数组。
问题在于:提示词增强 workflow 的 input/output 是自包含的,没有资源、没有图片 blob,这套流水线对它要么空转、要么产生垃圾数据。这正是文档强调"必须让formatGenerationResponse2类型感知"的根本原因。
四、统一方案:四个实施步骤
文档给出的方案分四步,以下是完整设计及与源码的对应关系。
1. 单一 tRPC 端点
将queryGeneratedImages重命名/替换为统一的queryWorkflows端点,去掉硬编码的generationtag 过滤,让客户端按需传 tag。从源码看,这一思路的雏形其实已经落地为queryWorkflowsByTags——它接收的正是workflowQuerySchema,且没有任何类型假设。
2. 类型感知的服务端格式化器
把formatGenerationResponse2从"假定全图/视频"改为按步骤类型分发:
switch (step.$type) { case 'textToImage': case 'videoGen': // 现有 image/video 格式化(资源富化、blob 处理、遗留兼容) break; case 'promptEnhancement': // 透传或轻量归一化(input/output 自包含) break; default: // 未知类型的通用透传 break; }这里的 switch 结构与源码中formatStepOutputs的switch (step.$type)完全同构,差异在于前者目前是"输出归一化"层,而统一方案要求把"资源富化 + 元数据归一化"也纳入类型分派。
3. 判别联合(Discriminated Union)响应
每个 workflow 携带一个type判别字段,让客户端无需猜测 payload 形状:
type NormalizedWorkflow = | { type: 'generation'; /* 现有 image/video 字段 */ } | { type: 'promptEnhancement'; /* input/output/issues/recommendations */ } | { type: 'unknown'; /* 原始 step 数据 */ };可以推断,promptEnhancement分支的字段应与PromptEnhancementRecord(promptEnhanceHooks.ts)对齐,即 workflowId、createdAt、originalPrompt、enhancedPrompt、issues、recommendations、instruction、preserveTriggerWords、temperature、status。
4. 客户端过滤
- Queue/Feed:过滤
type === 'generation'; - 历史页:过滤
type === 'promptEnhancement'; - 或者干脆渲染混合内容,为不同类型配各自的卡片组件。
对应的客户端改造面包括:useGetTextToImageRequests目前直接对全部条目执行new WorkflowData(...)(generationRequestHooks.ts),统一后需要先按type分流;useGetPromptEnhancementHistory的mapWorkflowToRecord(按$type === 'promptEnhancement'或name === 'prompt-enhancement'查找 step)则可以退化为简单的类型断言。
五、关键挑战详解
5.1 遗留格式处理
formatGenerationResponse2同时处理三种元数据格式,统一时必须保证:
- 新格式:
workflow.metadata.params+workflow.metadata.resources(orchestration-new.service.ts); - 遗留格式:
step.metadata.params+step.metadata.resources(第 3115-3124 行); - 遗留增强格式:
step.metadata.transformations[last](第 3098-3114 行)。
这些逻辑必须对图片 workflow 保留,但绝不能对提示词增强 workflow 执行——否则会失败或产生垃圾数据。文档给出的约束是:类型分发发生在格式化入口,遗留兼容逻辑只挂在generation分支下。
5.2 资源富化的条件化
图片 workflow 需要把资源 ID 富化为模型名、版本、epoch 号,这一步要命中数据库(getResourceData)。提示词增强 workflow 没有资源,富化步骤必须条件化,否则每次历史查询都会产生无谓的数据库开销。文档同时点出一个细节:formatGenerationResponse2的资源收集横跨 workflow 级与 step 级两处(第 3010-3026 行),条件化时要同时覆盖这两处入口。
5.3 Signal 更新通道
useTextToImageSignalUpdate(src/components/ImageGeneration/utils/useGenerationSignalUpdate.ts)订阅SignalMessages.TextToImageUpdate(常量值'orchestrator:text-to-image-update',见 src/server/common/enums.ts),经 100ms 防抖后把 statusUpdate 结果 patch 进trpc.orchestrator.queryGeneratedImages的 React Query 缓存(updateWorkflowsStatus用getQueryKey(trpc.orchestrator.queryGeneratedImages)定位缓存)。而提示词侧走的是另一条通道:useWorkflowUpdateSignal订阅SignalMessages.WorkflowUpdate('orchestrator:workflow-update'),通过onWorkflowSignal监听器注册表把事件分发给useGetPromptEnhancementHistory,后者再以updateWorkflowInTagCache(workflowItem, PROMPT_ENHANCEMENT_TAGS)更新queryWorkflowsByTags的缓存(src/components/Orchestrator/workflowHooks.ts)。
统一后需要二选一:要么建立统一的 Signal 通道,要么维护多个订阅共同更新同一个缓存。注意updateWorkflowInTagCache/addWorkflowToTagCache已经是按 tag 匹配查询键的通用实现(matchesTagQuery检查 query key 的 input.tags 是否包含目标 tag),可以推断这套机制天然适合迁移到统一端点。
5.4WorkflowData/BlobData包装器
图片侧把 workflow 包进WorkflowData(src/shared/orchestrator/workflow-data.ts),它会把原始输出包装成BlobData子类(ImageBlob、VideoBlob、AudioBlob、Model3DBlob,抽象基类见 workflow-data.ts),承担 NSFW 拦截与父引用(BlobData.step、StepData.workflow)接线。这套机制与图片输出强耦合:统一查询需要多态包装器,或对非图片类型跳过包装。从源码看,WorkflowData的构造选项(domain、nsfwEnabled、allowMatureContent)都与图片的 NSFW 策略相关,提示词增强记录不需要也不应走这套包装。
六、统一化不可忽视的缓存维度
文档未展开、但源码中必须纳入统一化评估的是queryGeneratedImages专属的短 TTL 缓存体系(orchestration-new.service.ts):
QUERIED_WORKFLOWS_CACHE_TTL = 3秒,用于折叠 SignalR 重连风暴——每次重连所有标签页会同时 invalidatequeryGeneratedImages,导致毫秒级的大量相同首页查询;- 缓存键
getQueriedWorkflowsCacheKey必须覆盖所有影响响应的参数(take、cursor、tags、ascending、fromDate、toDate、excludeFailed、hideMatureContent),并刻意排除 token以允许同一用户的多标签页共享条目; - 失效采用每用户索引集合(
SMEMBERS→DEL),deleteWorkflow、cancelWorkflow、updateWorkflow、patch四个变更端点都会调用bustQueriedWorkflowsCache主动清缓存(orchestrator.router.ts),且只缓存第一页(无 cursor),深翻页直接穿透。
而queryWorkflowsByTags(提示词增强历史)目前没有这层缓存。统一端点后,"3 秒短 TTL + 变更即失效"的语义是否要覆盖全部类型,需要单独决策:如果对提示词增强历史也启用,会让历史记录的增删改(如删除一条增强记录)同样依赖失效链路;如果维持现状,则统一端点内部将出现"同端点不同缓存策略"的分叉,这本身也是一种复杂度。
七、工作量评估与落地建议
文档给出的评估为2~3 天,拆解如下:
- 让
formatGenerationResponse2类型感知且不破坏现有图片 workflow(约 1 天); - 更新客户端 hooks 与组件以处理判别联合(约 1 天);
- 回归验证遗留 workflow 格式仍能正确渲染(约 0.5 天)。
建议是作为后续项推进,而不是提示词增强功能的一部分——当前双端点方案已可用且足够清晰;当出现第三种 orchestrator workflow 类型时,统一化才真正值得做,因为届时"每新增一种类型就新增一个端点"的模式会开始显得冗余。
从当前仓库的实际状态可以印证这一演进方向已经部分发生:queryWorkflowsByTags的存在本身就是"通用端点先行"的落地(orchestrator.router.ts),提示词增强已不再需要专属端点;而图片侧的queryGeneratedImages仍保留着强图片假设与专属缓存。可以推断,后续真正需要做的,是让formatGenerationResponse2的类型分发与queryWorkflowsByTags的通用查询能力合流,再统一客户端判别与 Signal 通道,即可完成文档描述的整体收敛。相关查询类型定义可在 src/types/router.ts 的QueryGeneratedImages中追根溯源。
参考路径速查
- 设计文档:docs/unified-workflow-query.md
- 路由层:src/server/routers/orchestrator.router.ts(
queryGeneratedImagesL331、queryWorkflowsByTagsL211) - 底层查询:src/server/services/orchestrator/workflows.ts(
queryWorkflowsL127、超时兜底 L64-L88) - 格式化与缓存:src/server/services/orchestrator/orchestration-new.service.ts(
formatGenerationResponse2L3003、queryGeneratedImageWorkflows2L3277、缓存 TTL L3177) - 查询入参 schema:src/server/schema/orchestrator/workflows.schema.ts
- 图片侧 hooks:src/components/ImageGeneration/utils/generationRequestHooks.ts、Signal 更新:useGenerationSignalUpdate.ts
- 提示词侧 hooks:src/components/Generation/PromptEnhance/promptEnhanceHooks.ts
- 通用缓存辅助:src/components/Orchestrator/workflowHooks.ts
- 客户端包装器:src/shared/orchestrator/workflow-data.ts
- Tag 与 Signal 常量:src/shared/constants/generation.constants.ts、src/server/common/enums.ts
【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考