ComfyUI 平台化:能力契约、节点白名单和积分预扣怎么串起来

ComfyUI 平台化:能力契约、节点白名单和积分预扣怎么串起来

这个项目是我和朋友一起做的内容生产 SaaS 后端,底层接 ComfyUI,但目标不是再套一层节点图 UI。

ComfyUI 对工程师很好用:节点怎么连、模型怎么换、输出怎么取,都能摊开调。但换到多租户内容团队里,问题会变得很别扭。创作者不应该理解一整张 workflow JSON,管理员也不能把 /prompt 原样开放出去,然后祈祷每张图都安全、稳定、可计费。

我负责的部分,是把 ComfyUI 收到一个后端治理层里:工作流先封成"能力",能力再进入任务和生产流。项目刚开发完后端核心链路,还没有进入生产环境,所以这篇只写工程实现和边界,不写任何生产效果。

我一开始想错的地方

最顺手的方案,其实是薄皮 UI:前端保存 ComfyUI workflow JSON,后端拿到后直接调 /prompt。开发快,演示也容易。

但这个方案很快露出问题:它没有能力边界。一个 workflow 可以悄悄引入新节点,输出类型可能变,资源消耗也不好估。更麻烦的是,多租户平台里还有组织、角色、订阅计划、积分余额和预算限制。ComfyUI 的接口只知道"提交一张图",不知道"谁可以跑、花多少钱、产物该归到哪里"。

所以我把入口从 /prompt 往上抬了一层。用户提交的不是节点图,而是某个已经被管理员封装过的能力。

flowchart TD User[创作者 / 内容团队] --> API[FastAPI API] API --> Auth[组织 / 角色 / 订阅计划] API --> Capability[WorkflowTemplate 能力契约] Capability --> CapGuard[能力级节点白名单] Auth --> RoleGuard[角色级节点白名单] API --> Billing[积分事件账本] Billing --> Task[任务创建] Task --> Inject[参数注入:8 种类型] Inject --> Client[ComfyClient:薄 HTTP 客户端] Client --> Runtime[ComfyUI runtime] Runtime --> History[history 产物引用] History --> PathGuard[_resolve_output_path] PathGuard --> ObjectStore[MinIO 对象存储] Flow[Production Flow DAG v2] --> Task Flow --> Transform[transform] Flow --> Review[manual_review]

这张图里的关键点不是组件数量,而是 ComfyUI 被压到了 runtime 层:业务规则、权限、计费和对象存储都不让它直接承担。

能力契约:把自由节点图收窄成平台能力

能力层以 WorkflowTemplate 为核心。一个能力至少要说清几件事:用户可以填什么参数、会产出什么类型、资源画像是什么、依赖哪些节点类型。

下面是脱敏简化后的 schema,不对应真实字段名,只保留机制:

json 复制代码
{
  "parameter_schema": {
    "parameters": [
      {
        "name": "prompt",
        "type": "string",
        "required": true,
        "inject_to": "workflow.node.text"
      },
      {
        "name": "reference_image",
        "type": "image",
        "required": false,
        "inject_to": "workflow.node.image"
      }
    ]
  },
  "output_schema": {
    "outputs": [
      {
        "name": "result",
        "type": "images"
      }
    ]
  },
  "resource_profile": {
    "recommended_concurrency": "positive_integer",
    "timeout_seconds": "positive_integer"
  },
  "node_dependencies": [
    "allowed_class_type_a",
    "allowed_class_type_b"
  ]
}

这段 schema 解决的是"平台能不能理解这张图"的问题:参数必须是数组,输出不能为空,资源配置要是正整数,工作流里的 class_type 必须都出现在 node_dependencies 里。

参数注入支持 string / integer / number / boolean / image / object / array / json 8 种类型。这里我没有把所有业务都塞进 ComfyUI 图里,原因很简单:图越自由,平台越难知道自己在执行什么。

两层节点白名单:能力能用,不代表角色能跑

节点白名单我做了两层。

能力级白名单回答的是:这个能力声明自己依赖哪些节点。它防止 workflow JSON 在配置后混入未声明节点。

角色级白名单回答的是:当前角色是否允许使用这些节点。比如管理员可以维护一类高成本能力,但普通成员不一定能跑。

这个设计会增加维护成本。新增节点时要更新能力依赖,也要检查角色权限。可如果省掉这层,平台就很难解释"为什么这个用户能跑一个成本更高、风险更大的图"。在多租户后端里,可解释性比省几行配置更重要。

任务链路:先扣积分,再提交 ComfyUI

任务创建时,我采用先扣后跑。后扣费对用户感觉更自然,但平台会承担算力已经消耗、余额却不足的风险。这个项目更偏后端治理层,所以我选择在提交 ComfyUI 前把约束先检查完。

核心流程大概是这样:

python 复制代码
def create_task(user, capability, params):
    assert_user_can_access(capability)
    assert_nodes_allowed_by_capability(capability.workflow)
    assert_nodes_allowed_by_role(user.role, capability.node_dependencies)
    assert_active_tasks_not_exceed(["created", "queued", "running"])

    cost = max(1, round(
        capability.default_cost_credits * user.plan.credits_multiplier
    ))
    ledger.reserve_once(user.org_id, cost)

    try:
        workflow = inject_params(capability.workflow, params)
        prompt_id = comfy_client.submit_prompt(workflow, timeout=30.0)
    except ComfyClientError:
        ledger.refund_once(user.org_id, cost)
        raise

    return Task(prompt_id=prompt_id, cost_credits=cost)

这段伪代码只保留任务创建主干:权限、节点白名单、活跃任务、积分预扣和提交失败退款;真实实现还要处理状态刷新和幂等。

余额没有直接存在用户表上,而是从积分事件账本里 sum(delta_credits) 算出来。发放、预扣、退款都走事件,这样后面追账会清楚很多。ComfyClient 也刻意做薄,只保留提交 prompt、查 history、抽取 images / gifs / videos 产物引用这些最小接口,默认 HTTP 超时是 30.0s

我挺喜欢这个边界。ComfyUI 只负责生成,平台负责判断这次生成是否应该发生。

生产流 DAG:不要把业务流程塞回节点图

内容生产不是一次生成就结束。会有生成、文本变换、人工审核、再进入下一步的情况。

我没有把这些动作继续塞进 ComfyUI workflow,而是单独做了 Production Flow,graph schema 版本是 2。节点类型可以是 capabilitytransformmanual_review 等。节点之间通过 input_mapping 传数据,来源包括 staticrun_inputpreferencenode_output

这里有一个容易忽略的校验:node_output 只能引用上游节点。运行时用 Kahn 拓扑排序推进就绪节点。如果允许引用下游节点,流程图看起来能存,跑起来就会变成很难查的循环依赖。

我把 transform 做得很克制,只支持有限文本格式化;manual_review 也只是流程卡点,不伪装成完整工作流引擎。这个阶段更重要的是把生成能力、参数传递、权限和计费跑顺。

对象存储迁移:history 里的路径不能直接信

踩得最实的一处坑,是 ComfyUI history 返回的 filenamesubfolder。如果平台直接拼路径去读文件,再上传 MinIO,就会留下路径逃逸风险。

测试里覆盖了几类脱敏输入:{"filename": "../secret.png"}{"filename": "/tmp/secret.png"}{"filename": "secret.png", "subfolder": "../"}。这些不需要真实生产事故也能判断出风险:多租户平台不能让 runtime 返回值决定后端读哪个文件。

python 复制代码
def resolve_output_path(output_dir, filename, subfolder=""):
    if Path(filename).is_absolute() or Path(subfolder).is_absolute():
        raise OutputPathForbidden()

    base = Path(output_dir).resolve()
    target = (base / subfolder / filename).resolve()

    if base != target and base not in target.parents:
        raise OutputPathForbidden()

    return target

这段伪代码解决的是对象迁移前的路径边界:先拒绝绝对路径,再确认 resolve() 后的真实路径仍在 ComfyUI 输出目录内,失败时在调用 MinIO 前就拦掉。

失败样例表

失败类型 脱敏简化样例 为什么错 处理策略
history 文件名路径逃逸 filename: "../secret.png" 可能读到输出目录之外的文件 _resolve_output_path 在上传对象存储前拒绝
history 子目录路径逃逸 subfolder: "../" 子目录参与拼接后仍可能越界 resolve() 后校验真实路径必须在输出目录内
参数 schema 形状不对 parameter_schema.parameters 不是数组 平台无法稳定做参数注入 能力变成可用状态前直接拒绝
输出契约为空 output_schema.outputs 为空 下游不知道该拿 images / gifs / videos 里的哪类产物 要求输出 schema 非空
节点依赖未声明 workflow 里出现未列入 node_dependenciesclass_type 能力实际执行内容和声明不一致 能力级白名单拦截
角色节点权限不足 角色不允许某个依赖节点 能力可用不代表当前角色可运行 角色级白名单再次校验
DAG 引用下游输出 node_output 指向尚未执行的节点 运行顺序不可确定,可能形成循环 校验只能引用上游节点,再做拓扑排序

表里的样例都是模式级示例,不含真实客户、文件名或生产数据。

什么时候不建议这么做

如果只是个人使用 ComfyUI,或者团队里人人都能理解节点图,这套治理层会显得重。直接用 ComfyUI 原生界面,反而更省事。

但一旦要做多用户、权限、预算、积分、对象存储和可复用流程,后端就不能只当转发器。/prompt 很好,但它不应该成为 SaaS 用户的业务入口。

我现在对这类项目的判断是:先把 ComfyUI 当 runtime,再在外层定义能力契约。节点图仍然保留它的表达力,平台也能知道自己到底在允许谁、以什么成本、执行什么能力。

如果这篇对你有帮助,欢迎点赞收藏。

相关推荐
用户7827110810631 小时前
Spring MVC RESTful 员工管理实战 ——踩坑全记录
后端
SimonKing1 小时前
Spring I18N 底层源码大揭秘:你的 MessageSource 到底是怎么找到配置的?
java·后端·程序员
IT_陈寒1 小时前
Vue的响应式竟然在这常见操作下失效了
前端·人工智能·后端
她说彩礼65万2 小时前
Asp.net core ContenResult
后端·asp.net
枕星而眠2 小时前
Git仓库基础用法
大数据·git·后端·elasticsearch·全文检索
ayqy贾杰2 小时前
Claude Fable 5 提示词泄漏,抓紧学习下
前端·后端·面试
智码看视界3 小时前
Day18 SpringBoot自动配置原理:从@SpringBootApplication开始
java·spring boot·后端·自动装配
程序员爱钓鱼3 小时前
Rust 常量与静态变量详解:const 与 static 的区别
后端
戮漠summer14 小时前
Missing Semester 计算机教育中缺失的一课 Lecture 01 Shell
开发语言·后端·scala