ComfyUI 授权服务 API 文档

前端客户端 / 启动器对接指南 v3.0 — 激活码升级 + 有效期

概述

Base URLhttps://app.goodsinventory.top/api/v1
通讯协议HTTPS(强制)
数据格式JSON(Content-Type: application/json)
认证方式机器码(machine_code)+ 授权令牌(token)
时区所有时间字段使用 +08:00(CST)
本系统通过工作流权限控制不同会员等级可以使用的工作流列表。激活码可用于首次激活升级会员等级,有效期在激活时开始计算。

客户端流程

启动客户端 ↓ 读取本地缓存(token + machine_code) ↓ 有缓存? ↓ ├─ 有 → 调用 POST /auth/verify │ ↓ │ ├─ 成功 → 获取 allowed_workflows → 按权限过滤工作流 │ │ → 显示剩余时间(用 expires_at 计算) │ │ → 进入主界面(启动10分钟心跳) │ └─ 失败 → 清除缓存 → 显示授权页面 │ └─ 无 → 显示授权页面 ↓ 用户输入授权码(首次激活 / 升级) ↓ 调用 POST /auth/activate ↓ ├─ 成功 → 保存新 token → 获取 allowed_workflows │ → 显示剩余时间 → 进入主界面 └─ 失败 → 显示错误信息

1. 设备激活 / 升级

POST 首次激活和升级共用此接口

/api/v1/auth/activate

请求参数

{
  "machine_code": "A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6",
  "license_key": "A1B2-C3D4-E5F6-G7H8-I9J0-K1L2"
}
字段类型必填说明
machine_codestring由硬件信息生成的设备唯一标识
license_keystring管理员提供的授权码(大小写不敏感)
升级场景:设备已有有效授权,用户输入新的更高等级授权码即可升级。新授权码会绑定到此设备,客户端应保存返回的新 token 替换旧 token。

成功响应

{
  "ok": true,
  "message": "启用成功",
  "data": {
    "token": "abc123...",
    "member_level": 2,
    "member_name": "專業版",
    "expires_at": "2026-07-16T18:23:00+08:00",
    "allowed_workflows": {
      "FLUX KONTEXT 工作流合集": {
        "folder": "FLUX KONTEXT工作流合集",
        "workflows": [
          {"file": "FLUX KONTEXT工作流合集/Kontext 一句話改變人物角度.json", "name": "Kontext 一句話改變人物角度"},
          {"file": "FLUX KONTEXT工作流合集/另一個工作流.json", "name": "另一個工作流"}
        ]
      },
      "圖片工作流": {
        "folder": "图片工作流",
        "workflows": [
          {"file": "图片工作流/FLUX-標準文生圖.json", "name": "FLUX 標準文生圖"}
        ]
      }
    }
  }
}
message 字段值:
  • "启用成功" — 首次激活
  • "升级成功" — 设备已有授权,使用新码升级
  • "授权有效(已激活)" — 重复使用同一授权码(不重置有效期)

allowed_workflows 结构说明

字段说明
key(分类名)工作流分类的显示名称,如 "FLUX KONTEXT 工作流合集"
folder对应的本地文件夹名,可用于前端按目录组织
workflows该分类下允许使用的工作流数组
workflows[].file工作流 JSON 文件的相对路径
workflows[].name工作流的中文显示名称
客户端应根据 allowed_workflows 过滤本地工作流列表,只显示用户有权限使用的工作流。

2. 验证授权状态

POST 每次启动时调用

/api/v1/auth/verify

请求参数

{
  "machine_code": "A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6",
  "token": "7a8b9c0d1e2f..."
}

成功响应

{
  "ok": true,
  "message": "授权有效",
  "data": {
    "member_level": 2,
    "member_name": "專業版",
    "expires_at": "2026-07-16T18:23:00+08:00",
    "allowed_workflows": { ... }
  }
}

3. 获取工作流权限

POST

/api/v1/member/workflows

单独获取当前会员的工作流权限。也可从 activate / verify 响应中直接获取。

请求参数

{
  "machine_code": "...",
  "token": "..."
}

成功响应

{
  "ok": true,
  "data": {
    "member_level": 2,
    "member_name": "专业版",
    "allowed_workflows": {
      "分类名": {
        "folder": "文件夹名",
        "workflows": [
          {"file": "相对路径.json", "name": "中文名称"}
        ]
      }
    }
  }
}

4. 心跳回报

POST 建议每 10 分钟调用一次

/api/v1/auth/heartbeat
请求: { "machine_code": "...", "token": "..." }
响应: { "ok": true, "message": "heartbeat received" }

5. 健康检查

GET 无需参数

/api/v1/health
{ "ok": true, "message": "服务器运作正常", "time": "2026-06-16T19:00:00+08:00" }

expires_at 格式说明

必须严格按照此格式处理 expires_at 字段。
场景expires_at 值说明
30 天有效期"2026-07-16T18:23:00+08:00"ISO 8601 带时区
10 小时有效期"2026-06-17T04:23:00+08:00"ISO 8601 带时区
永久有效""null空字符串表示永久

客户端剩余时间计算

// JavaScript 示例
function calcRemaining(expiresAt) {
  if (!expiresAt) return '永久有效';

  const exp = new Date(expiresAt);
  const now = new Date();
  const diff = exp - now;

  if (diff <= 0) return '已过期';

  const days = Math.floor(diff / 86400000);
  const hours = Math.floor((diff % 86400000) / 3600000);
  const mins = Math.floor((diff % 3600000) / 60000);

  return `剩餘:${days} 天 ${hours} 小時 ${mins} 分鐘`;
}
有效期机制说明:
  • 有效期在激活时开始计算,未激活的授权码不会过期
  • 管理员配置有效期为"天/时/分"(如 30天10小时15分)
  • 全为 0 表示永久有效,expires_at 返回空字符串
  • 重复使用同一授权码不会重置有效期

会员等级定义

level名称说明
0试用版仅可使用少量基础工作流
1入门版可使用图片类工作流
2专业版可使用全部工作流
3旗舰版可使用全部工作流,无限制

每个等级可用的具体工作流由管理员在后台配置。

通用响应结构

// 成功
{ "ok": true, "message": "操作说明", "data": { ... } }

// 失败
{ "ok": false, "message": "错误描述", "error_code": "ERROR_CODE" }

错误码速查表

error_codeHTTP说明客户端建议
MISSING_PARAMS400缺少参数检查请求参数
INVALID_LICENSE400授权码无效提示用户核对授权码
LICENSE_USED400授权码已绑定其他设备提示联系管理员解绑
LICENSE_EXPIRED400授权码已过期提示联系管理员续期
TOKEN_INVALID401令牌无效清除缓存,引导重新激活
TOKEN_EXPIRED401令牌已过期提示授权过期
ACCOUNT_DISABLED403帐号已停用提示联系管理员

代码示例

Python 客户端示例

import requests, hashlib, uuid, json
from datetime import datetime

BASE = "https://app.goodsinventory.top/api/v1"
CACHE = "auth_cache.json"

def machine_code():
    return hashlib.sha256(str(uuid.getnode()).encode()).hexdigest()[:32].upper()

def activate(license_key):
    """激活或升级"""
    r = requests.post(f"{BASE}/auth/activate", json={
        "machine_code": machine_code(), "license_key": license_key
    }).json()
    if r["ok"]:
        with open(CACHE, "w") as f:
            json.dump({"token": r["data"]["token"], "mc": machine_code()}, f)

        print(f"[{r['message']}] {r['data']['member_name']}")

        # 显示剩余时间
        exp = r["data"]["expires_at"]
        if exp:
            remaining = datetime.fromisoformat(exp) - datetime.now(
                datetime.fromisoformat(exp).tzinfo)
            print(f"剩余: {remaining.days}天 {remaining.seconds//3600}小时")
        else:
            print("永久有效")

        # 工作流列表
        for cat, info in r["data"]["allowed_workflows"].items():
            print(f"\n[{cat}] (folder: {info['folder']})")
            for wf in info["workflows"]:
                print(f"  - {wf['name']}  →  {wf['file']}")
    return r

def verify():
    try:
        c = json.load(open(CACHE))
    except FileNotFoundError:
        return None
    r = requests.post(f"{BASE}/auth/verify", json={
        "machine_code": c["mc"], "token": c["token"]
    }).json()
    return r["data"] if r["ok"] else None

def get_allowed_files(data):
    """从 allowed_workflows 提取所有允许的文件路径集合"""
    files = set()
    for cat_info in data["allowed_workflows"].values():
        for wf in cat_info["workflows"]:
            files.add(wf["file"])
    return files

JavaScript / Electron 示例

const BASE = 'https://app.goodsinventory.top/api/v1';

async function activate(machineCode, licenseKey) {
  const r = await fetch(`${BASE}/auth/activate`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ machine_code: machineCode, license_key: licenseKey }),
  }).then(r => r.json());

  if (r.ok) {
    // 保存新 token(升级时替换旧 token)
    localStorage.setItem('auth_token', r.data.token);
    localStorage.setItem('machine_code', machineCode);

    // 显示剩余时间
    showRemaining(r.data.expires_at);

    // 过滤工作流
    filterWorkflowList(r.data.allowed_workflows);
  }
  return r;
}

function showRemaining(expiresAt) {
  if (!expiresAt) {
    console.log('永久有效');
    return;
  }
  const exp = new Date(expiresAt);
  const diff = exp - new Date();
  if (diff <= 0) return console.log('已过期');

  const d = Math.floor(diff / 86400000);
  const h = Math.floor((diff % 86400000) / 3600000);
  const m = Math.floor((diff % 3600000) / 60000);
  console.log(`剩餘:${d} 天 ${h} 小時 ${m} 分鐘`);
}

function filterWorkflowList(allowedWorkflows) {
  const allowedFiles = new Set();
  for (const catInfo of Object.values(allowedWorkflows)) {
    for (const wf of catInfo.workflows) {
      allowedFiles.add(wf.file);
    }
  }
  return allowedFiles;
}

// 心跳(每10分钟)
setInterval(async () => {
  const token = localStorage.getItem('auth_token');
  const mc = localStorage.getItem('machine_code');
  if (token && mc) {
    await fetch(`${BASE}/auth/heartbeat`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ machine_code: mc, token }),
    });
  }
}, 600000);
前端对接要点:
  • allowed_workflows 提取所有 file 字段,构建允许的文件路径集合
  • 扫描本地工作流目录时,只显示在允许集合中的 JSON 文件
  • 工作流的 name 字段可作为 UI 显示名称
  • 工作流的 folder 字段可用于前端按文件夹分组展示
  • expires_at 为空字符串或 null 时表示永久有效
  • 升级后必须用返回的新 token 替换旧 token
  • Token 应安全存储,所有请求使用 HTTPS