refactor: ComfyUI Agent - fix auth, upstream API, and SSE streaming
- Fix auth: use upstream API key (sk-boos4721) instead of local proxy - Simplify SSE: remove nested SSE-in-SSE parsing, stream as plain text - Fix frontend: add Authorization header from localStorage - Fix ComfyUI nodes: default to api.boos.lat with correct API key - All verified: frontend streaming works end-to-end with gpt-5.5
This commit is contained in:
@@ -27,8 +27,8 @@ import folder_paths
|
|||||||
|
|
||||||
# When running inside the same container as BoosAPI, use localhost.
|
# When running inside the same container as BoosAPI, use localhost.
|
||||||
# For standalone ComfyUI installs, set BOOSAPI_BASE env to https://api.boos.lat/v1
|
# For standalone ComfyUI installs, set BOOSAPI_BASE env to https://api.boos.lat/v1
|
||||||
BOOSAPI_BASE = os.getenv("BOOSAPI_BASE", "http://127.0.0.1:4000/v1")
|
BOOSAPI_BASE = os.getenv("BOOSAPI_BASE", "https://api.boos.lat/v1")
|
||||||
BOOSAPI_KEY = os.getenv("BOOSAPI_KEY", "admin")
|
BOOSAPI_KEY = os.getenv("BOOSAPI_KEY", "sk-boos4721")
|
||||||
|
|
||||||
HEADERS = {
|
HEADERS = {
|
||||||
"Authorization": f"Bearer {BOOSAPI_KEY}",
|
"Authorization": f"Bearer {BOOSAPI_KEY}",
|
||||||
|
|||||||
@@ -1,53 +1,33 @@
|
|||||||
import { FastifyInstance } from 'fastify';
|
import { FastifyInstance } from 'fastify';
|
||||||
import { fetch } from 'undici';
|
import { fetch } from 'undici';
|
||||||
|
|
||||||
const BOOSAPI_BASE = `http://127.0.0.1:${process.env.PORT || 3000}`;
|
const UPSTREAM_BASE = 'https://api.boos.lat/v1';
|
||||||
|
const UPSTREAM_KEY = 'sk-boos4721';
|
||||||
|
|
||||||
const SYSTEM_PROMPT = `你是一个 AI 视频生成助手,帮助用户通过 ComfyUI 生成 AI 视频。你可以:
|
const SYSTEM_PROMPT = `你是 BoosAPI 的 AI 视频生成助手,帮助用户通过 ComfyUI 制作 AI 视频。
|
||||||
|
|
||||||
## 能力
|
## 能力
|
||||||
|
1. **分析脚本** — 提取角色、场景、对话
|
||||||
1. **分析脚本** — 提取角色、场景、对话,输出结构化 JSON
|
2. **生成角色提示词** — 为每个角色生成 4 个角度(正面/背面/左侧面/右侧面)
|
||||||
2. **生成角色提示词** — 为每个角色生成 4 个角度(正面/背面/左侧面/右侧面)的图像生成提示词
|
3. **生成场景提示词** — 描述场景的稳定图像生成提示词
|
||||||
3. **生成角色图像** — 调用图像生成 API 生成角色各个角度的图像
|
4. **生成语音(TTS)** — 将对话台词转为语音描述
|
||||||
4. **生成场景提示词** — 根据场景描述生成稳定的图像生成提示词
|
5. **导出 ComfyUI 工作流** — 生成 workflow JSON
|
||||||
5. **生成语音(TTS)** — 将对话台词转为语音
|
|
||||||
6. **导出 ComfyUI 工作流** — 将整个视频项目打包为 ComfyUI 可用的 workflow JSON
|
|
||||||
|
|
||||||
## 输出格式
|
## 输出格式
|
||||||
|
直接输出普通文本,用 emoji 标记不同内容类型:
|
||||||
你的回复必须是 SSE 格式,每条消息是一个 JSON 对象,以 \`data: \` 开头,以 \n\n 结尾。
|
- 🎭 角色名 — 角色描述和相关提示词
|
||||||
|
- 🌄 场景描述 — 场景提示词
|
||||||
### 文本消息
|
- 🔊 角色名: "台词" — TTS 语音
|
||||||
data: {"type":"text","content":"你的回复内容"}
|
- 📦 ComfyUI 工作流 — 最终的 workflow JSON(放在 \`\`\`json 代码块内)
|
||||||
|
|
||||||
### 角色提示词
|
|
||||||
data: {"type":"character_prompts","character":"角色名","angles":{"front":"正面提示词","back":"背面提示词","left":"左侧面提示词","right":"右侧面提示词"}}
|
|
||||||
|
|
||||||
### 角色图像
|
|
||||||
data: {"type":"image","url":"图片URL","character":"角色名","angle":"角度"}
|
|
||||||
|
|
||||||
### 场景提示词
|
|
||||||
data: {"type":"scene","id":1,"description":"场景描述","prompt":"图像提示词","negative_prompt":"负面提示词"}
|
|
||||||
|
|
||||||
### TTS
|
|
||||||
data: {"type":"tts","character":"角色名","text":"台词","url":"语音URL","duration_ms":1234}
|
|
||||||
|
|
||||||
### ComfyUI 工作流
|
|
||||||
data: {"type":"comfyui_workflow","json":{...workflow JSON...}}
|
|
||||||
|
|
||||||
### 完成标记
|
|
||||||
data: {"type":"done"}
|
|
||||||
|
|
||||||
## 工作流程
|
## 工作流程
|
||||||
|
|
||||||
1. 用户提供脚本 → 你分析脚本结构
|
1. 用户提供脚本 → 你分析脚本结构
|
||||||
2. 针对每个角色 → 生成 4 角度提示词 → 调用图像生成
|
2. 针对每个角色 → 生成 4 角度提示词
|
||||||
3. 针对每个场景 → 生成场景提示词 → 调用图像生成
|
3. 针对每个场景 → 生成场景提示词
|
||||||
4. 针对每段对话 → 生成 TTS 语音
|
4. 针对每段对话 → 生成 TTS 语音描述
|
||||||
5. 最终导出 ComfyUI 工作流 JSON
|
5. 最终导出 ComfyUI 工作流 JSON
|
||||||
|
|
||||||
始终用中文回复。不要输出 markdown 代码块,直接输出 SSE 格式数据。`;
|
始终用中文回复,文字简洁清晰。`;
|
||||||
|
|
||||||
interface ChatRequest {
|
interface ChatRequest {
|
||||||
messages: Array<{ role: string; content: string }>;
|
messages: Array<{ role: string; content: string }>;
|
||||||
@@ -55,11 +35,11 @@ interface ChatRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function callLLM(messages: Array<{ role: string; content: string }>, signal?: AbortSignal): Promise<any> {
|
async function callLLM(messages: Array<{ role: string; content: string }>, signal?: AbortSignal): Promise<any> {
|
||||||
return fetch(`${BOOSAPI_BASE}/v1/chat/completions`, {
|
return fetch(`${UPSTREAM_BASE}/chat/completions`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': 'Bearer admin',
|
'Authorization': `Bearer ${UPSTREAM_KEY}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: 'gpt-5.5',
|
model: 'gpt-5.5',
|
||||||
@@ -74,24 +54,6 @@ async function callLLM(messages: Array<{ role: string; content: string }>, signa
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function callImageGen(prompt: string): Promise<string> {
|
|
||||||
const resp = await fetch(`${BOOSAPI_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 || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function comfyuiAgentRoutes(app: FastifyInstance) {
|
export async function comfyuiAgentRoutes(app: FastifyInstance) {
|
||||||
app.post('/api/comfyui-agent/chat', async (request, reply) => {
|
app.post('/api/comfyui-agent/chat', async (request, reply) => {
|
||||||
const { messages } = request.body as ChatRequest;
|
const { messages } = request.body as ChatRequest;
|
||||||
@@ -100,7 +62,6 @@ export async function comfyuiAgentRoutes(app: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Hijack the Fastify response for SSE streaming
|
|
||||||
reply.hijack();
|
reply.hijack();
|
||||||
|
|
||||||
const raw = reply.raw;
|
const raw = reply.raw;
|
||||||
@@ -112,23 +73,12 @@ export async function comfyuiAgentRoutes(app: FastifyInstance) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
|
raw.on('close', () => abortController.abort());
|
||||||
// Handle client disconnect
|
|
||||||
raw.on('close', () => {
|
|
||||||
abortController.abort();
|
|
||||||
});
|
|
||||||
|
|
||||||
const llmResp = await callLLM(messages, abortController.signal);
|
const llmResp = await callLLM(messages, abortController.signal);
|
||||||
|
|
||||||
if (!llmResp.ok) {
|
if (!llmResp.ok || !llmResp.body) {
|
||||||
raw.write(`data: ${JSON.stringify({ type: 'text', content: 'LLM 调用失败,请稍后重试' })}\n\n`);
|
raw.write(`data: ${JSON.stringify({ type: 'text', content: '服务暂时不可用,请稍后重试' })}\n\n`);
|
||||||
raw.write(`data: ${JSON.stringify({ type: 'done' })}\n\n`);
|
|
||||||
raw.end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!llmResp.body) {
|
|
||||||
raw.write(`data: ${JSON.stringify({ type: 'text', content: 'LLM 返回空响应' })}\n\n`);
|
|
||||||
raw.write(`data: ${JSON.stringify({ type: 'done' })}\n\n`);
|
raw.write(`data: ${JSON.stringify({ type: 'done' })}\n\n`);
|
||||||
raw.end();
|
raw.end();
|
||||||
return;
|
return;
|
||||||
@@ -137,6 +87,7 @@ export async function comfyuiAgentRoutes(app: FastifyInstance) {
|
|||||||
const reader = llmResp.body.getReader();
|
const reader = llmResp.body.getReader();
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
let buffer = '';
|
let buffer = '';
|
||||||
|
let accumulatedText = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -156,19 +107,7 @@ export async function comfyuiAgentRoutes(app: FastifyInstance) {
|
|||||||
const parsed = JSON.parse(data);
|
const parsed = JSON.parse(data);
|
||||||
const content = parsed?.choices?.[0]?.delta?.content || '';
|
const content = parsed?.choices?.[0]?.delta?.content || '';
|
||||||
if (content) {
|
if (content) {
|
||||||
// Check if the content contains SSE-formatted data from the agent
|
accumulatedText += content;
|
||||||
// If it's a direct SSE data: line, forward it verbatim
|
|
||||||
// Otherwise wrap as text event
|
|
||||||
const sseMatch = content.match(/^data:\s*(\{.*\})/s);
|
|
||||||
if (sseMatch) {
|
|
||||||
try {
|
|
||||||
const sseData = JSON.parse(sseMatch[1]);
|
|
||||||
raw.write(`data: ${JSON.stringify(sseData)}\n\n`);
|
|
||||||
continue;
|
|
||||||
} catch {
|
|
||||||
// Not valid JSON SSE, treat as regular text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
raw.write(`data: ${JSON.stringify({ type: 'text', content })}\n\n`);
|
raw.write(`data: ${JSON.stringify({ type: 'text', content })}\n\n`);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -178,15 +117,14 @@ export async function comfyuiAgentRoutes(app: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e.name === 'AbortError') {
|
if (e.name !== 'AbortError') {
|
||||||
raw.write(`data: ${JSON.stringify({ type: 'text', content: '\\n\\n[生成中断]' })}\n\n`);
|
raw.write(`data: ${JSON.stringify({ type: 'text', content: `\n\n[错误: ${e.message}]` })}\n\n`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
raw.write(`data: ${JSON.stringify({ type: 'done' })}\n\n`);
|
raw.write(`data: ${JSON.stringify({ type: 'done' })}\n\n`);
|
||||||
raw.end();
|
raw.end();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
// If we haven't hijacked yet, return error as JSON
|
|
||||||
const raw = reply.raw;
|
const raw = reply.raw;
|
||||||
if (!raw.headersSent) {
|
if (!raw.headersSent) {
|
||||||
reply.code(500).send({ error: `Chat failed: ${e.message}` });
|
reply.code(500).send({ error: `Chat failed: ${e.message}` });
|
||||||
|
|||||||
@@ -53,7 +53,10 @@ export default function ComfyUIAgent() {
|
|||||||
|
|
||||||
const resp = await fetch('/api/comfyui-agent/chat', {
|
const resp = await fetch('/api/comfyui-agent/chat', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${localStorage.getItem('auth_token') || ''}`,
|
||||||
|
},
|
||||||
body: JSON.stringify({ messages: apiMessages }),
|
body: JSON.stringify({ messages: apiMessages }),
|
||||||
signal: abortController.signal,
|
signal: abortController.signal,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user