feat: ComfyUI embedded dev setup + proxy auth with downstream keys

- COMFYUI_BASE configurable via env var (default 127.0.0.1:8188)
- All /api/comfyui/ routes protected by downstream API Key or admin token
- docker/Dockerfile.comfyui + docker-compose.comfyui.yml for dev
- npm scripts: comfyui:install, comfyui:start, comfyui:stop
- .gitignore / .env.example updated
This commit is contained in:
boos
2026-05-17 13:04:46 +08:00
parent 2fe8d8528a
commit ad0bb86e31
11 changed files with 308 additions and 7 deletions
+3
View File
@@ -28,3 +28,6 @@ DATA_DIR=./data
NOTIFY_COOLDOWN_SEC=300 NOTIFY_COOLDOWN_SEC=300
ADMIN_IP_ALLOWLIST= ADMIN_IP_ALLOWLIST=
TZ=Asia/Shanghai TZ=Asia/Shanghai
# ComfyUI — 本地工作流引擎地址(默认 http://127.0.0.1:8188)
COMFYUI_BASE=http://127.0.0.1:8188
+7
View File
@@ -26,3 +26,10 @@ docker/.env
docs/plan/ docs/plan/
docs/plans/ docs/plans/
.ace-tool/ .ace-tool/
# Local ComfyUI development install
.comfyui/
# Rust binary
boosapi
boosapi-rs/target/
Submodule
+1
Submodule boosapi-rs added at 02a92e537f
+27
View File
@@ -0,0 +1,27 @@
FROM python:3.11-slim
WORKDIR /workspace
# Install system deps
RUN apt-get update && apt-get install -y --no-install-recommends \
git curl \
&& rm -rf /var/lib/apt/lists/*
# Clone ComfyUI
RUN git clone https://github.com/comfyanonymous/ComfyUI.git /workspace/ComfyUI
WORKDIR /workspace/ComfyUI
# Python venv + deps (CPU-only, generation goes through BoosAPI)
RUN python3 -m venv venv && \
. venv/bin/activate && \
pip install --upgrade pip && \
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu && \
grep -v kornia_rs requirements.txt | pip install -r /dev/stdin && \
rm -rf ~/.cache/pip
ENV PATH="/workspace/ComfyUI/venv/bin:${PATH}"
EXPOSE 8188
CMD ["python3", "main.py", "--listen", "0.0.0.0", "--port", "8188", "--cpu"]
+22
View File
@@ -0,0 +1,22 @@
# docker-compose.comfyui.yml
# 开发环境:在 BoosAPI 旁边运行 ComfyUI
# 使用: docker compose -f docker/docker-compose.yml -f docker/docker-compose.comfyui.yml up -d
services:
comfyui:
build:
context: ..
dockerfile: docker/Dockerfile.comfyui
ports:
- "127.0.0.1:8188:8188"
volumes:
- comfyui-data:/workspace
- ../boosapi_comfy_nodes.py:/workspace/ComfyUI/custom_nodes/boosapi_comfy_nodes.py
environment:
- BOOSAPI_BASE=http://host.docker.internal:4000/v1
- BOOSAPI_KEY=${BOOSAPI_KEY:-sk-demo-user}
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
comfyui-data:
+3
View File
@@ -49,6 +49,9 @@
"start": "node dist/server/index.js", "start": "node dist/server/index.js",
"start:deploy-helper": "node dist/server/update-helper/index.js", "start:deploy-helper": "node dist/server/update-helper/index.js",
"start:desktop": "electron dist/desktop/main.js", "start:desktop": "electron dist/desktop/main.js",
"comfyui:install": "node scripts/comfyui/install.mjs",
"comfyui:start": "node scripts/comfyui/start.mjs",
"comfyui:stop": "node scripts/comfyui/stop.mjs",
"db:generate": "drizzle-kit generate", "db:generate": "drizzle-kit generate",
"db:migrate": "tsx src/server/db/migrate.ts", "db:migrate": "tsx src/server/db/migrate.ts",
"schema:contract": "tsx scripts/dev/generate-schema-contract.ts", "schema:contract": "tsx scripts/dev/generate-schema-contract.ts",
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env node
/**
* Install ComfyUI locally for development
* Clones repo + sets up Python venv + installs deps
*/
import { execSync } from 'child_process';
import { existsSync, mkdirSync, copyFileSync } from 'fs';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const PROJECT_ROOT = resolve(__dirname, '../..');
const COMFYUI_DIR = resolve(PROJECT_ROOT, '.comfyui');
async function run() {
console.log('[comfyui] Installing ComfyUI for local development...');
console.log(`[comfyui] Target: ${COMFYUI_DIR}`);
if (existsSync(COMFYUI_DIR)) {
console.log('[comfyui] ComfyUI already installed. Pulling latest...');
execSync('git pull', { cwd: COMFYUI_DIR, stdio: 'inherit' });
} else {
console.log('[comfyui] Cloning ComfyUI...');
execSync(
'git clone https://github.com/comfyanonymous/ComfyUI.git .comfyui',
{ cwd: PROJECT_ROOT, stdio: 'inherit' },
);
}
// Check Python
try {
execSync('python3 --version', { stdio: 'pipe' });
} catch {
console.error('[comfyui] ❌ python3 not found. Please install Python 3.10+.');
process.exit(1);
}
// Setup venv
const venvDir = resolve(COMFYUI_DIR, 'venv');
if (!existsSync(venvDir)) {
console.log('[comfyui] Creating Python venv...');
execSync('python3 -m venv venv', { cwd: COMFYUI_DIR, stdio: 'inherit' });
}
// Install deps (CPU-only — all generation goes through BoosAPI)
const pip = process.platform === 'win32'
? resolve(venvDir, 'Scripts', 'pip')
: resolve(venvDir, 'bin', 'pip');
const python = process.platform === 'win32'
? resolve(venvDir, 'Scripts', 'python')
: resolve(venvDir, 'bin', 'python');
console.log('[comfyui] Upgrading pip...');
execSync(`"${pip}" install --upgrade pip`, { cwd: COMFYUI_DIR, stdio: 'inherit' });
console.log('[comfyui] Installing PyTorch (CPU)...');
execSync(
`"${pip}" install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu`,
{ cwd: COMFYUI_DIR, stdio: 'inherit' },
);
console.log('[comfyui] Installing ComfyUI dependencies...');
execSync(
`grep -v kornia_rs requirements.txt | "${pip}" install -r /dev/stdin`,
{ cwd: COMFYUI_DIR, stdio: 'inherit' },
);
// Install BoosAPI custom nodes
const customNodesDir = resolve(COMFYUI_DIR, 'custom_nodes');
mkdirSync(customNodesDir, { recursive: true });
const customNodeSrc = resolve(PROJECT_ROOT, 'boosapi_comfy_nodes.py');
const customNodeDst = resolve(customNodesDir, 'boosapi_comfy_nodes.py');
if (existsSync(customNodeSrc)) {
copyFileSync(customNodeSrc, customNodeDst);
console.log('[comfyui] ✅ BoosAPI custom nodes installed');
}
console.log('');
console.log('[comfyui] ✅ ComfyUI installed!');
console.log(`[comfyui] Directory: ${COMFYUI_DIR}`);
console.log('[comfyui] Start with: npm run comfyui:start');
console.log('[comfyui] Or set COMFYUI_BASE env to point elsewhere');
}
run().catch((err) => {
console.error('[comfyui] ❌ Install failed:', err.message);
process.exit(1);
});
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env node
/**
* Start local ComfyUI (managed subprocess)
*/
import { spawn } from 'child_process';
import { existsSync } from 'fs';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const PROJECT_ROOT = resolve(__dirname, '../..');
const COMFYUI_DIR = resolve(PROJECT_ROOT, '.comfyui');
function getPythonPath() {
const venvBin = resolve(COMFYUI_DIR, 'venv', 'bin');
const venvScripts = resolve(COMFYUI_DIR, 'venv', 'Scripts');
if (existsSync(resolve(venvBin, 'python3'))) return resolve(venvBin, 'python3');
if (existsSync(resolve(venvBin, 'python'))) return resolve(venvBin, 'python');
if (existsSync(resolve(venvScripts, 'python.exe'))) return resolve(venvScripts, 'python.exe');
return 'python3';
}
async function run() {
if (!existsSync(COMFYUI_DIR)) {
console.error('[comfyui] ❌ ComfyUI not installed. Run: npm run comfyui:install');
process.exit(1);
}
const python = getPythonPath();
const port = process.env.COMFYUI_PORT || '8188';
// Check if already running
try {
const resp = await fetch(`http://127.0.0.1:${port}/`);
if (resp.ok) {
console.log(`[comfyui] ✅ ComfyUI already running on port ${port}`);
process.exit(0);
}
} catch {
// Not running, start it
}
console.log(`[comfyui] Starting ComfyUI on port ${port}...`);
console.log(`[comfyui] Python: ${python}`);
console.log(`[comfyui] Dir: ${COMFYUI_DIR}`);
const proc = spawn(python, ['main.py', '--listen', '127.0.0.1', '--port', port, '--cpu'], {
cwd: COMFYUI_DIR,
stdio: 'inherit',
env: {
...process.env,
BOOSAPI_BASE: process.env.BOOSAPI_BASE || 'http://127.0.0.1:4000/v1',
BOOSAPI_KEY: process.env.BOOSAPI_KEY || '',
},
});
proc.on('exit', (code) => {
if (code !== 0 && code !== null) {
console.error(`[comfyui] ❌ Exited with code ${code}`);
}
});
// Graceful shutdown
process.on('SIGINT', () => { proc.kill(); process.exit(); });
process.on('SIGTERM', () => { proc.kill(); process.exit(); });
}
run().catch((err) => {
console.error('[comfyui] ❌ Start failed:', err.message);
process.exit(1);
});
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env node
/**
* Stop local ComfyUI by PID
*/
import { readFileSync, existsSync, unlinkSync } from 'fs';
import { execSync } from 'child_process';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const COMFYUI_DIR = resolve(__dirname, '../..', '.comfyui');
const PID_FILE = resolve(COMFYUI_DIR, 'comfyui.pid');
// Try PID file first
if (existsSync(PID_FILE)) {
try {
const pid = parseInt(readFileSync(PID_FILE, 'utf-8').trim(), 10);
process.kill(pid, 'SIGTERM');
console.log(`[comfyui] Stopped PID ${pid}`);
unlinkSync(PID_FILE);
process.exit(0);
} catch (e) {
if (e.code === 'ESRCH') {
console.log('[comfyui] Process already stopped');
unlinkSync(PID_FILE);
process.exit(0);
}
// Fall through to pkill
}
}
// Fallback: kill by process name
try {
execSync('pkill -f "python.*main.py.*--listen.*8188"', { stdio: 'pipe' });
console.log('[comfyui] Stopped ComfyUI process');
} catch {
console.log('[comfyui] No running ComfyUI process found');
}
+1 -1
View File
@@ -5,7 +5,7 @@ const DESKTOP_HEALTH_ROUTE = '/api/desktop/health';
export function isPublicApiRoute(url: string): boolean { export function isPublicApiRoute(url: string): boolean {
return url === DESKTOP_HEALTH_ROUTE return url === DESKTOP_HEALTH_ROUTE
|| url.startsWith('/api/oauth/callback/') || url.startsWith('/api/oauth/callback/')
|| url.startsWith('/api/comfyui/proxy/') || url.startsWith('/api/comfyui/')
// User SaaS routes (JWT auth, not admin token) // User SaaS routes (JWT auth, not admin token)
|| url === '/api/users/register' || url === '/api/users/register'
|| url === '/api/users/login' || url === '/api/users/login'
+41 -6
View File
@@ -1,7 +1,34 @@
import { FastifyInstance } from 'fastify'; import { FastifyInstance } from 'fastify';
import { fetch } from 'undici'; import { fetch } from 'undici';
import { authorizeDownstreamToken } from '../../services/downstreamApiKeyService.js';
import { config } from '../../config.js';
const COMFYUI_BASE = 'http://127.0.0.1:8188'; const COMFYUI_BASE = process.env.COMFYUI_BASE || 'http://127.0.0.1:8188';
/** 同时支持管理员 token 和下游 API Key 认证 */
async function comfyuiAuth(request: { headers: { authorization?: string } }): Promise<{ ok: boolean; error?: string; code?: number }> {
const auth = (request.headers.authorization || '').trim();
if (!auth) {
return { ok: false, error: 'Missing Authorization header', code: 401 };
}
const token = auth.startsWith('Bearer ') ? auth.slice(7).trim() : auth;
// 1. 管理员 token
if (token === config.authToken) {
return { ok: true };
}
// 2. 下游 API Key (sk-xxx)
if (token.startsWith('sk-')) {
const result = await authorizeDownstreamToken(token);
if (result.ok) {
return { ok: true };
}
return { ok: false, error: result.error, code: result.statusCode };
}
return { ok: false, error: 'Invalid token', code: 403 };
}
/** Rebrand ComfyUI HTML to show BoosAPI instead */ /** Rebrand ComfyUI HTML to show BoosAPI instead */
function rebrandHtml(html: string): string { function rebrandHtml(html: string): string {
@@ -17,8 +44,16 @@ function rewriteProxyUrls(html: string): string {
} }
export async function comfyuiRoutes(app: FastifyInstance) { export async function comfyuiRoutes(app: FastifyInstance) {
// Auth preHandler: prompt, history, queue, view 需要认证
const requireAuth = async (request: { headers: { authorization?: string } }, reply: any) => {
const result = await comfyuiAuth(request);
if (!result.ok) {
reply.code(result.code || 403).send({ error: result.error });
}
};
// Proxy: POST /api/comfyui/prompt -> ComfyUI /prompt // Proxy: POST /api/comfyui/prompt -> ComfyUI /prompt
app.post('/api/comfyui/prompt', async (request, reply) => { app.post('/api/comfyui/prompt', { preHandler: requireAuth }, async (request, reply) => {
try { try {
const body = typeof request.body === 'object' ? JSON.stringify(request.body) : '{}'; const body = typeof request.body === 'object' ? JSON.stringify(request.body) : '{}';
const resp = await fetch(`${COMFYUI_BASE}/prompt`, { const resp = await fetch(`${COMFYUI_BASE}/prompt`, {
@@ -34,7 +69,7 @@ export async function comfyuiRoutes(app: FastifyInstance) {
}); });
// Proxy: GET /api/comfyui/history -> ComfyUI /history // Proxy: GET /api/comfyui/history -> ComfyUI /history
app.get('/api/comfyui/history', async (_request, reply) => { app.get('/api/comfyui/history', { preHandler: requireAuth }, async (_request, reply) => {
try { try {
const resp = await fetch(`${COMFYUI_BASE}/history`); const resp = await fetch(`${COMFYUI_BASE}/history`);
const text = await resp.text(); const text = await resp.text();
@@ -45,7 +80,7 @@ export async function comfyuiRoutes(app: FastifyInstance) {
}); });
// Proxy: GET /api/comfyui/history/:id -> ComfyUI /history/{id} // Proxy: GET /api/comfyui/history/:id -> ComfyUI /history/{id}
app.get<{ Params: { id: string } }>('/api/comfyui/history/:id', async (request, reply) => { app.get<{ Params: { id: string } }>('/api/comfyui/history/:id', { preHandler: requireAuth }, async (request, reply) => {
try { try {
const resp = await fetch(`${COMFYUI_BASE}/history/${encodeURIComponent(request.params.id)}`); const resp = await fetch(`${COMFYUI_BASE}/history/${encodeURIComponent(request.params.id)}`);
const text = await resp.text(); const text = await resp.text();
@@ -56,7 +91,7 @@ export async function comfyuiRoutes(app: FastifyInstance) {
}); });
// Proxy: GET /api/comfyui/queue -> ComfyUI /queue // Proxy: GET /api/comfyui/queue -> ComfyUI /queue
app.get('/api/comfyui/queue', async (_request, reply) => { app.get('/api/comfyui/queue', { preHandler: requireAuth }, async (_request, reply) => {
try { try {
const resp = await fetch(`${COMFYUI_BASE}/queue`); const resp = await fetch(`${COMFYUI_BASE}/queue`);
const text = await resp.text(); const text = await resp.text();
@@ -67,7 +102,7 @@ export async function comfyuiRoutes(app: FastifyInstance) {
}); });
// Proxy: GET /api/comfyui/view -> ComfyUI /view (image output, preserve query params) // Proxy: GET /api/comfyui/view -> ComfyUI /view (image output, preserve query params)
app.get('/api/comfyui/view', async (request, reply) => { app.get('/api/comfyui/view', { preHandler: requireAuth }, async (request, reply) => {
try { try {
const qs = request.url.includes('?') ? request.url.substring(request.url.indexOf('?')) : ''; const qs = request.url.includes('?') ? request.url.substring(request.url.indexOf('?')) : '';
const resp = await fetch(`${COMFYUI_BASE}/view${qs}`); const resp = await fetch(`${COMFYUI_BASE}/view${qs}`);