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:
@@ -28,3 +28,6 @@ DATA_DIR=./data
|
||||
NOTIFY_COOLDOWN_SEC=300
|
||||
ADMIN_IP_ALLOWLIST=
|
||||
TZ=Asia/Shanghai
|
||||
|
||||
# ComfyUI — 本地工作流引擎地址(默认 http://127.0.0.1:8188)
|
||||
COMFYUI_BASE=http://127.0.0.1:8188
|
||||
|
||||
@@ -26,3 +26,10 @@ docker/.env
|
||||
docs/plan/
|
||||
docs/plans/
|
||||
.ace-tool/
|
||||
|
||||
# Local ComfyUI development install
|
||||
.comfyui/
|
||||
|
||||
# Rust binary
|
||||
boosapi
|
||||
boosapi-rs/target/
|
||||
|
||||
Submodule
+1
Submodule boosapi-rs added at 02a92e537f
@@ -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"]
|
||||
@@ -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:
|
||||
@@ -49,6 +49,9 @@
|
||||
"start": "node dist/server/index.js",
|
||||
"start:deploy-helper": "node dist/server/update-helper/index.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:migrate": "tsx src/server/db/migrate.ts",
|
||||
"schema:contract": "tsx scripts/dev/generate-schema-contract.ts",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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');
|
||||
}
|
||||
@@ -5,7 +5,7 @@ const DESKTOP_HEALTH_ROUTE = '/api/desktop/health';
|
||||
export function isPublicApiRoute(url: string): boolean {
|
||||
return url === DESKTOP_HEALTH_ROUTE
|
||||
|| url.startsWith('/api/oauth/callback/')
|
||||
|| url.startsWith('/api/comfyui/proxy/')
|
||||
|| url.startsWith('/api/comfyui/')
|
||||
// User SaaS routes (JWT auth, not admin token)
|
||||
|| url === '/api/users/register'
|
||||
|| url === '/api/users/login'
|
||||
|
||||
@@ -1,7 +1,34 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
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 */
|
||||
function rebrandHtml(html: string): string {
|
||||
@@ -17,8 +44,16 @@ function rewriteProxyUrls(html: string): string {
|
||||
}
|
||||
|
||||
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
|
||||
app.post('/api/comfyui/prompt', async (request, reply) => {
|
||||
app.post('/api/comfyui/prompt', { preHandler: requireAuth }, async (request, reply) => {
|
||||
try {
|
||||
const body = typeof request.body === 'object' ? JSON.stringify(request.body) : '{}';
|
||||
const resp = await fetch(`${COMFYUI_BASE}/prompt`, {
|
||||
@@ -34,7 +69,7 @@ export async function comfyuiRoutes(app: FastifyInstance) {
|
||||
});
|
||||
|
||||
// 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 {
|
||||
const resp = await fetch(`${COMFYUI_BASE}/history`);
|
||||
const text = await resp.text();
|
||||
@@ -45,7 +80,7 @@ export async function comfyuiRoutes(app: FastifyInstance) {
|
||||
});
|
||||
|
||||
// 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 {
|
||||
const resp = await fetch(`${COMFYUI_BASE}/history/${encodeURIComponent(request.params.id)}`);
|
||||
const text = await resp.text();
|
||||
@@ -56,7 +91,7 @@ export async function comfyuiRoutes(app: FastifyInstance) {
|
||||
});
|
||||
|
||||
// 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 {
|
||||
const resp = await fetch(`${COMFYUI_BASE}/queue`);
|
||||
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)
|
||||
app.get('/api/comfyui/view', async (request, reply) => {
|
||||
app.get('/api/comfyui/view', { preHandler: requireAuth }, async (request, reply) => {
|
||||
try {
|
||||
const qs = request.url.includes('?') ? request.url.substring(request.url.indexOf('?')) : '';
|
||||
const resp = await fetch(`${COMFYUI_BASE}/view${qs}`);
|
||||
|
||||
Reference in New Issue
Block a user