fix: restore ComfyUI as workflow frontend with BoosAPI backend integration

ComfyUI is kept as a visual workflow editor (node-based). All actual
generation goes through BoosAPI's own API via boosapi_comfy_nodes.py
custom nodes (now defaulting to localhost:4000/v1).

Architecture:
  BoosAPI (Node.js, port 4000)
    ├── /video-agent → iframe → ComfyUI (Python, port 8188, CPU)
    │                              └── custom_nodes/boosapi_comfy_nodes.py
    │                                   └── calls BoosAPI /v1/... API
    ├── /comfyui-agent → chat interface (direct API calls)
    └── /v1/... → API endpoints (LLM, image, TTS, video)

Signed-off-by: boos <boos@boos.lat>
This commit is contained in:
boos
2026-05-16 00:26:07 +08:00
parent bd06698c8e
commit 0f3ff82147
8 changed files with 453 additions and 14 deletions
+5 -4
View File
@@ -1,15 +1,16 @@
""" """
BoosAPI Custom Nodes for ComfyUI BoosAPI Custom Nodes for ComfyUI
================================== ==================================
Call BoosAPI/metapi endpoints directly from ComfyUI workflows: Call local BoosAPI endpoints directly from ComfyUI workflows:
- Text generation (gpt-5.5) - Text generation (gpt-5.5)
- Image generation (gpt-image-2) - Image generation (gpt-image-2)
- Text-to-Speech - Text-to-Speech
- Video generation (async) - Video generation (async)
Configuration (in ComfyUI settings or env vars): Default: http://127.0.0.1:4000/v1 (same container)
BOOSAPI_BASE = https://api.boos.lat/v1 Override via env:
BOOSAPI_KEY = sk-boos4721 BOOSAPI_BASE = https://api.boos.lat/v1 (standalone ComfyUI)
BOOSAPI_KEY=***
""" """
import os import os
+27 -3
View File
@@ -23,15 +23,17 @@ ARG NO_PROXY
ENV DEBIAN_FRONTEND=noninteractive ENV DEBIAN_FRONTEND=noninteractive
ENV NODE_ENV=production ENV NODE_ENV=production
ENV COMFYUI_BASE=http://127.0.0.1:8188
ENV HTTP_PROXY=${HTTP_PROXY} ENV HTTP_PROXY=${HTTP_PROXY}
ENV HTTPS_PROXY=${HTTPS_PROXY} ENV HTTPS_PROXY=${HTTPS_PROXY}
ENV ALL_PROXY=${ALL_PROXY} ENV ALL_PROXY=${ALL_PROXY}
ENV NO_PROXY=${NO_PROXY} ENV NO_PROXY=${NO_PROXY}
# Install system deps: Node.js 22, supervisor # Install system deps: Node.js 22, Python 3, supervisor
RUN echo 'Acquire::http::Proxy "false";' > /etc/apt/apt.conf.d/99no-proxy && apt-get update && apt-get install -y --no-install-recommends \ RUN echo 'Acquire::http::Proxy "false";' > /etc/apt/apt.conf.d/99no-proxy && apt-get update && apt-get install -y --no-install-recommends \
curl ca-certificates gnupg \ curl ca-certificates gnupg \
supervisor \ python3 python3-pip python3-venv \
supervisor git \
&& curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ && curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \ && apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
@@ -40,6 +42,18 @@ RUN echo 'Acquire::http::Proxy "false";' > /etc/apt/apt.conf.d/99no-proxy && apt
COPY rtk /usr/local/bin/rtk COPY rtk /usr/local/bin/rtk
RUN chmod +x /usr/local/bin/rtk RUN chmod +x /usr/local/bin/rtk
# Set up ComfyUI (workflow editor frontend, calls BoosAPI API via custom nodes)
# CPU PyTorch is fine — all generation goes through BoosAPI's API
WORKDIR /app/comfy
RUN git clone https://github.com/comfyanonymous/ComfyUI.git . \
&& 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="/app/comfy/venv/bin:${PATH}"
# Copy metapi (built) # Copy metapi (built)
WORKDIR /app/metapi WORKDIR /app/metapi
COPY --from=metapi-builder /app/dist ./dist COPY --from=metapi-builder /app/dist ./dist
@@ -48,6 +62,9 @@ COPY --from=metapi-builder /app/package.json ./
COPY --from=metapi-builder /app/drizzle ./drizzle COPY --from=metapi-builder /app/drizzle ./drizzle
RUN mkdir -p data RUN mkdir -p data
# Copy BoosAPI custom nodes for ComfyUI (call local BoosAPI API)
COPY --from=metapi-builder /app/boosapi_comfy_nodes.py /app/comfy/custom_nodes/boosapi_comfy_nodes.py
# Supervisor config # Supervisor config
RUN mkdir -p /var/log/supervisor RUN mkdir -p /var/log/supervisor
COPY <<'SUPERVISOR' /etc/supervisor/conf.d/all.conf COPY <<'SUPERVISOR' /etc/supervisor/conf.d/all.conf
@@ -64,8 +81,15 @@ autorestart=true
stdout_logfile=/var/log/supervisor/metapi.log stdout_logfile=/var/log/supervisor/metapi.log
stderr_logfile=/var/log/supervisor/metapi.err stderr_logfile=/var/log/supervisor/metapi.err
[program:comfyui]
command=python3 main.py --listen 0.0.0.0 --port 8188 --cpu
directory=/app/comfy
autorestart=true
startretries=3
stdout_logfile=/var/log/supervisor/comfyui.log
stderr_logfile=/var/log/supervisor/comfyui.err
SUPERVISOR SUPERVISOR
EXPOSE 4000 EXPOSE 4000 8188
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/all.conf"] CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/all.conf"]
+1
View File
@@ -5,6 +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/')
// 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'
+4
View File
@@ -27,6 +27,8 @@ import { invitationRoutes } from './routes/api/invitations.js';
import { oauthRoutes } from './routes/api/oauth.js'; import { oauthRoutes } from './routes/api/oauth.js';
import { siteAnnouncementsRoutes } from './routes/api/siteAnnouncements.js'; import { siteAnnouncementsRoutes } from './routes/api/siteAnnouncements.js';
import { updateCenterRoutes } from './routes/api/updateCenter.js'; import { updateCenterRoutes } from './routes/api/updateCenter.js';
import { comfyuiRoutes } from './routes/api/comfyui.js';
import { videoAgentRoutes } from './routes/api/videoAgent.js';
import { adminUsersRoutes } from './routes/api/adminUsers.js'; import { adminUsersRoutes } from './routes/api/adminUsers.js';
import { comfyuiAgentRoutes } from './routes/api/comfyuiAgent.js'; import { comfyuiAgentRoutes } from './routes/api/comfyuiAgent.js';
import { proxyRoutes } from './routes/proxy/router.js'; import { proxyRoutes } from './routes/proxy/router.js';
@@ -239,6 +241,8 @@ await app.register(usersRoutes);
await app.register(userApiKeysRoutes); await app.register(userApiKeysRoutes);
await app.register(invitationRoutes); await app.register(invitationRoutes);
await app.register(organizationsRoutes); await app.register(organizationsRoutes);
await app.register(comfyuiRoutes);
await app.register(videoAgentRoutes);
await app.register(comfyuiAgentRoutes); await app.register(comfyuiAgentRoutes);
await app.register(oauthRoutes); await app.register(oauthRoutes);
+124
View File
@@ -0,0 +1,124 @@
import { FastifyInstance } from 'fastify';
import { fetch } from 'undici';
const COMFYUI_BASE = 'http://127.0.0.1:8188';
/** Rebrand ComfyUI HTML to show BoosAPI instead */
function rebrandHtml(html: string): string {
return html
.replace(/ComfyUI/g, 'BoosAPI')
.replace(/comfyui/gi, 'BoosAPI');
}
function rewriteProxyUrls(html: string): string {
return html.replace(/(src|href|action)=["']\//g, (m) =>
m.replace(/["']/, '/api/comfyui/proxy/')
);
}
export async function comfyuiRoutes(app: FastifyInstance) {
// Proxy: POST /api/comfyui/prompt -> ComfyUI /prompt
app.post('/api/comfyui/prompt', async (request, reply) => {
try {
const body = typeof request.body === 'object' ? JSON.stringify(request.body) : '{}';
const resp = await fetch(`${COMFYUI_BASE}/prompt`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body,
});
const text = await resp.text();
return reply.code(resp.status).type('application/json').send(text);
} catch (e: any) {
return reply.code(502).send({ error: `ComfyUI unreachable: ${e.message}` });
}
});
// Proxy: GET /api/comfyui/history -> ComfyUI /history
app.get('/api/comfyui/history', async (_request, reply) => {
try {
const resp = await fetch(`${COMFYUI_BASE}/history`);
const text = await resp.text();
return reply.code(resp.status).type('application/json').send(text);
} catch (e: any) {
return reply.code(502).send({ error: `ComfyUI unreachable: ${e.message}` });
}
});
// Proxy: GET /api/comfyui/history/:id -> ComfyUI /history/{id}
app.get<{ Params: { id: string } }>('/api/comfyui/history/:id', async (request, reply) => {
try {
const resp = await fetch(`${COMFYUI_BASE}/history/${encodeURIComponent(request.params.id)}`);
const text = await resp.text();
return reply.code(resp.status).type('application/json').send(text);
} catch (e: any) {
return reply.code(502).send({ error: `ComfyUI unreachable: ${e.message}` });
}
});
// Proxy: GET /api/comfyui/queue -> ComfyUI /queue
app.get('/api/comfyui/queue', async (_request, reply) => {
try {
const resp = await fetch(`${COMFYUI_BASE}/queue`);
const text = await resp.text();
return reply.code(resp.status).type('application/json').send(text);
} catch (e: any) {
return reply.code(502).send({ error: `ComfyUI unreachable: ${e.message}` });
}
});
// Proxy: GET /api/comfyui/view -> ComfyUI /view (image output, preserve query params)
app.get('/api/comfyui/view', async (request, reply) => {
try {
const qs = request.url.includes('?') ? request.url.substring(request.url.indexOf('?')) : '';
const resp = await fetch(`${COMFYUI_BASE}/view${qs}`);
const buffer = await resp.arrayBuffer();
return reply.code(resp.status).type(resp.headers.get('content-type') || 'application/octet-stream').send(Buffer.from(buffer));
} catch (e: any) {
return reply.code(502).send({ error: `ComfyUI unreachable: ${e.message}` });
}
});
// Proxy: GET /api/comfyui/proxy/* -> proxy everything to ComfyUI (HTML, assets, JS, CSS, etc.)
app.get('/api/comfyui/proxy/*', async (request, reply) => {
try {
const path = request.url.replace('/api/comfyui/proxy', '');
const targetUrl = `${COMFYUI_BASE}${path}`;
const resp = await fetch(targetUrl);
const contentType = resp.headers.get('content-type') || 'application/octet-stream';
if (contentType.includes('html')) {
const text = await resp.text();
const rebranded = rebrandHtml(text);
const rewritten = rewriteProxyUrls(rebranded);
return reply.code(resp.status).type(contentType).send(rewritten);
}
if (contentType.includes('text') || contentType.includes('json')) {
const text = await resp.text();
return reply.code(resp.status).type(contentType).send(text);
}
const buffer = await resp.arrayBuffer();
return reply.code(resp.status).type(contentType).send(Buffer.from(buffer));
} catch (e: any) {
return reply.code(502).send({ error: `ComfyUI unreachable: ${e.message}` });
}
});
// Serve ComfyUI HTML page at /comfyui with iframe to proxied ComfyUI
app.get('/comfyui', async (_request, reply) => {
return reply.type('text/html').send(`<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BoosAPI - ComfyUI</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #1a1a2e; }
iframe { width: 100%; height: 100%; border: none; }
</style>
</head>
<body>
<iframe src="/api/comfyui/proxy/" title="BoosAPI ComfyUI"></iframe>
</body>
</html>`);
});
}
+269
View File
@@ -0,0 +1,269 @@
import { FastifyInstance } from 'fastify';
import { fetch } from 'undici';
const METAPI_BASE = `http://127.0.0.1:${process.env.PORT || 3000}`;
interface AgentRequest {
script: string;
style?: string;
}
/**
* AI Video Generation Agent
*
* Workflow:
* 1. User provides script
* 2. Agent parses script → characters, scenes, dialogue
* 3. For characters: generate front/back/left/right concept art
* 4. For scenes: generate background/environment prompts
* 5. For dialogue: generate TTS audio
* 6. Package everything for ComfyUI workflow
*/
// Default system prompt for the script analysis agent
const SCRIPT_ANALYSIS_PROMPT = `你是一个 AI 视频生成助手。分析用户提供的脚本,提取以下结构化信息,以 JSON 格式返回:
{
"title": "视频标题",
"characters": [
{
"name": "角色名",
"gender": "男/女",
"age": "年龄描述",
"appearance": "外貌详细描述(发型、脸型、服装、体型等)",
"personality": "性格特征"
}
],
"scenes": [
{
"id": 1,
"description": "场景描述",
"environment": "环境详细描述(时间、地点、天气、光线等)",
"characters": ["出现的角色名"],
"action": "场景中发生的动作",
"dialogue": [
{"character": "角色名", "line": "台词", "emotion": "情感语气"}
]
}
],
"style": "整体视觉风格(动画/写实/像素等)"
}
只返回 JSON,不要其他文字。`;
const CHARACTER_PROMPT_GEN = `根据角色描述生成 4 组稳定的图像生成提示词(正面、背面、左侧面、右侧面),用于 AI 图像生成模型。
每组提示词应包含:人物姿态、服装、光照、背景。
返回 JSON 格式:
{
"front": "正面提示词",
"back": "背面提示词",
"left": "左侧面提示词",
"right": "右侧面提示词"
}
只返回 JSON。`;
const SCENE_PROMPT_GEN = `根据场景描述生成图像生成提示词,用于 AI 图像生成模型创建场景背景。
包含:环境、光照、色调、氛围。
返回 JSON:
{
"prompt": "场景生成提示词",
"negative_prompt": "负面提示词"
}
只返回 JSON。`;
async function callLLM(prompt: string, system: string): Promise<string> {
const resp = await fetch(`${METAPI_BASE}/v1/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer admin`,
},
body: JSON.stringify({
model: 'gpt-5.5',
messages: [
{ role: 'system', content: system },
{ role: 'user', content: prompt },
],
temperature: 0.7,
}),
});
const data: any = await resp.json();
return data?.choices?.[0]?.message?.content || '';
}
async function callImageGen(prompt: string): Promise<string> {
const resp = await fetch(`${METAPI_BASE}/v1/images/generations`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer admin`,
},
body: JSON.stringify({
model: 'dall-e-3',
prompt,
n: 1,
size: '1024x1024',
}),
});
const data: any = await resp.json();
return data?.data?.[0]?.url || '';
}
async function callTTS(text: string, voice = 'alloy'): Promise<Buffer | null> {
const resp = await fetch(`${METAPI_BASE}/v1/audio/speech`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer admin`,
},
body: JSON.stringify({
model: 'tts-1',
input: text,
voice,
}),
});
if (!resp.ok) return null;
const buffer = await resp.arrayBuffer();
return Buffer.from(buffer);
}
export async function videoAgentRoutes(app: FastifyInstance) {
// POST /api/video-agent/analyze — analyze script and extract structure
app.post('/api/video-agent/analyze', async (request, reply) => {
const { script } = request.body as AgentRequest;
if (!script) {
return reply.code(400).send({ error: 'script is required' });
}
try {
const result = await callLLM(script, SCRIPT_ANALYSIS_PROMPT);
const parsed = JSON.parse(result.replace(/```json|```/g, '').trim());
return { success: true, data: parsed };
} catch (e: any) {
return reply.code(500).send({ error: `Analysis failed: ${e.message}` });
}
});
// POST /api/video-agent/character-prompts — generate character image prompts
app.post('/api/video-agent/character-prompts', async (request, reply) => {
const { character } = request.body as any;
if (!character) {
return reply.code(400).send({ error: 'character is required' });
}
try {
const desc = `${character.name}: ${character.appearance}, ${character.gender}, ${character.age}`;
const result = await callLLM(desc, CHARACTER_PROMPT_GEN);
const prompts = JSON.parse(result.replace(/```json|```/g, '').trim());
return { success: true, data: prompts };
} catch (e: any) {
return reply.code(500).send({ error: `Prompt generation failed: ${e.message}` });
}
});
// POST /api/video-agent/generate-character — generate character images (front/back/left/right)
app.post('/api/video-agent/generate-character', async (request, reply) => {
const { prompts } = request.body as any;
if (!prompts) {
return reply.code(400).send({ error: 'prompts is required' });
}
try {
const results: Record<string, string> = {};
for (const [angle, prompt] of Object.entries(prompts)) {
const url = await callImageGen(prompt as string);
results[angle] = url;
}
return { success: true, data: results };
} catch (e: any) {
return reply.code(500).send({ error: `Image generation failed: ${e.message}` });
}
});
// POST /api/video-agent/scene-prompt — generate scene image prompt
app.post('/api/video-agent/scene-prompt', async (request, reply) => {
const { scene } = request.body as any;
if (!scene) {
return reply.code(400).send({ error: 'scene is required' });
}
try {
const desc = `${scene.environment}. ${scene.description}`;
const result = await callLLM(desc, SCENE_PROMPT_GEN);
const prompt = JSON.parse(result.replace(/```json|```/g, '').trim());
return { success: true, data: prompt };
} catch (e: any) {
return reply.code(500).send({ error: `Scene prompt failed: ${e.message}` });
}
});
// POST /api/video-agent/generate-scene — generate scene image
app.post('/api/video-agent/generate-scene', async (request, reply) => {
const { prompt } = request.body as any;
if (!prompt) {
return reply.code(400).send({ error: 'prompt is required' });
}
try {
const url = await callImageGen(prompt);
return { success: true, data: { url } };
} catch (e: any) {
return reply.code(500).send({ error: `Scene generation failed: ${e.message}` });
}
});
// POST /api/video-agent/generate-tts — generate TTS audio for a line
app.post('/api/video-agent/generate-tts', async (request, reply) => {
const { text, voice } = request.body as any;
if (!text) {
return reply.code(400).send({ error: 'text is required' });
}
try {
const audio = await callTTS(text, voice || 'alloy');
if (!audio) {
return reply.code(502).send({ error: 'TTS generation failed' });
}
return reply.type('audio/mpeg').send(audio);
} catch (e: any) {
return reply.code(500).send({ error: `TTS failed: ${e.message}` });
}
});
// POST /api/video-agent/full-workflow — run complete workflow: analyze + generate all assets
app.post('/api/video-agent/full-workflow', async (request, reply) => {
const { script, style } = request.body as AgentRequest;
if (!script) {
return reply.code(400).send({ error: 'script is required' });
}
try {
// Phase 1: Analyze script
const analysisRaw = await callLLM(script, SCRIPT_ANALYSIS_PROMPT);
const analysis = JSON.parse(analysisRaw.replace(/```json|```/g, '').trim());
// Phase 2: Generate character prompts and images
const characters: any[] = [];
for (const char of (analysis.characters || [])) {
const desc = `${char.name}: ${char.appearance}, ${char.gender}, ${char.age}`;
const promptsRaw = await callLLM(desc, CHARACTER_PROMPT_GEN);
const prompts = JSON.parse(promptsRaw.replace(/```json|```/g, '').trim());
characters.push({ ...char, imagePrompts: prompts });
}
// Phase 3: Generate scene prompts
const scenes: any[] = [];
for (const scene of (analysis.scenes || [])) {
const desc = `${scene.environment}. ${scene.description}`;
const scenePromptRaw = await callLLM(desc, SCENE_PROMPT_GEN);
const scenePrompt = JSON.parse(scenePromptRaw.replace(/```json|```/g, '').trim());
scenes.push({ ...scene, imagePrompt: scenePrompt });
}
return {
success: true,
data: {
title: analysis.title,
style: analysis.style || style,
characters,
scenes,
},
};
} catch (e: any) {
return reply.code(500).send({ error: `Workflow failed: ${e.message}` });
}
});
}
+3
View File
@@ -34,6 +34,7 @@ const ProgramLogs = lazy(() => import('./pages/ProgramLogs.js'));
const Models = lazy(() => import('./pages/Models.js')); const Models = lazy(() => import('./pages/Models.js'));
const About = lazy(() => import('./pages/About.js')); const About = lazy(() => import('./pages/About.js'));
const ModelTester = lazy(() => import('./pages/ModelTester.js')); const ModelTester = lazy(() => import('./pages/ModelTester.js'));
const ComfyUI = lazy(() => import('./pages/ComfyUI.js'));
const ComfyUIAgent = lazy(() => import('./pages/ComfyUIAgent.js')); const ComfyUIAgent = lazy(() => import('./pages/ComfyUIAgent.js'));
const Monitors = lazy(() => import('./pages/Monitors.js')); const Monitors = lazy(() => import('./pages/Monitors.js'));
const OAuthManagement = lazy(() => import('./pages/OAuthManagement.js')); const OAuthManagement = lazy(() => import('./pages/OAuthManagement.js'));
@@ -397,6 +398,7 @@ export const sidebarGroups = [
{ to: '/checkin', label: '签到记录', icon: <svg className="sidebar-item-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.75} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg> }, { to: '/checkin', label: '签到记录', icon: <svg className="sidebar-item-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.75} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg> },
{ to: '/routes', label: '路由', icon: <svg className="sidebar-item-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.75} d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" /></svg> }, { to: '/routes', label: '路由', icon: <svg className="sidebar-item-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.75} d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" /></svg> },
{ to: '/logs', label: '使用日志', icon: <svg className="sidebar-item-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.75} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" /></svg> }, { to: '/logs', label: '使用日志', icon: <svg className="sidebar-item-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.75} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" /></svg> },
{ to: '/video-agent', label: '视频工作台', icon: <svg className="sidebar-item-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" /></svg> },
{ to: '/comfyui-agent', label: '视频助手', icon: <svg className="sidebar-item-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" /><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg> }, { to: '/comfyui-agent', label: '视频助手', icon: <svg className="sidebar-item-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" /><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg> },
], ],
}, },
@@ -844,6 +846,7 @@ function AppShell() {
<Route path="/settings/import-export" element={<ImportExport />} /> <Route path="/settings/import-export" element={<ImportExport />} />
<Route path="/settings/notify" element={<NotificationSettings />} /> <Route path="/settings/notify" element={<NotificationSettings />} />
<Route path="/models" element={<Models />} /> <Route path="/models" element={<Models />} />
<Route path="/video-agent" element={<ComfyUI />} />
<Route path="/comfyui-agent" element={<ComfyUIAgent />} /> <Route path="/comfyui-agent" element={<ComfyUIAgent />} />
<Route path="/about" element={<About />} /> <Route path="/about" element={<About />} />
<Route path="/users" element={<UserManagement />} /> <Route path="/users" element={<UserManagement />} />
+20 -7
View File
@@ -1,8 +1,15 @@
import React from 'react'; import React from 'react';
export default function ComfyUI() { export default function ComfyUI() {
const [isDark, setIsDark] = React.useState(true);
React.useEffect(() => { React.useEffect(() => {
window.location.href = '/comfyui-agent'; const el = document.documentElement;
const check = () => setIsDark(el.getAttribute('data-theme') !== 'light');
check();
const mo = new MutationObserver(check);
mo.observe(el, { attributes: true, attributeFilter: ['data-theme'] });
return () => mo.disconnect();
}, []); }, []);
return ( return (
@@ -11,14 +18,20 @@ export default function ComfyUI() {
width: '100%', width: '100%',
height: 'calc(100vh - 56px)', height: 'calc(100vh - 56px)',
display: 'flex', display: 'flex',
alignItems: 'center', flexDirection: 'column',
justifyContent: 'center', background: isDark ? '#1a1a2e' : '#f5f5f5',
background: 'var(--color-bg, #0f0f1a)',
color: 'var(--color-text-secondary, #888)',
fontSize: '16px',
}} }}
> >
🔄 正在跳转至 AI 视频助手... <iframe
src="/comfyui"
style={{
width: '100%',
height: '100%',
border: 'none',
}}
title="ComfyUI"
allow="clipboard-read; clipboard-write"
/>
</div> </div>
); );
} }