ComfyUI 授权服务 API 文档
前端客户端 / 启动器对接指南 v3.0 — 激活码升级 + 有效期
概述
| Base URL | https://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_code | string | 是 | 由硬件信息生成的设备唯一标识 |
| license_key | string | 是 | 管理员提供的授权码(大小写不敏感) |
升级场景:设备已有有效授权,用户输入新的更高等级授权码即可升级。新授权码会绑定到此设备,客户端应保存返回的新 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_code | HTTP | 说明 | 客户端建议 |
|---|---|---|---|
| MISSING_PARAMS | 400 | 缺少参数 | 检查请求参数 |
| INVALID_LICENSE | 400 | 授权码无效 | 提示用户核对授权码 |
| LICENSE_USED | 400 | 授权码已绑定其他设备 | 提示联系管理员解绑 |
| LICENSE_EXPIRED | 400 | 授权码已过期 | 提示联系管理员续期 |
| TOKEN_INVALID | 401 | 令牌无效 | 清除缓存,引导重新激活 |
| TOKEN_EXPIRED | 401 | 令牌已过期 | 提示授权过期 |
| ACCOUNT_DISABLED | 403 | 帐号已停用 | 提示联系管理员 |
代码示例
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