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:
boos
2026-05-17 04:17:12 +08:00
parent b78b622f0c
commit 5e3d4ee7d4
3 changed files with 32 additions and 91 deletions
+26 -88
View File
@@ -1,53 +1,33 @@
import { FastifyInstance } from 'fastify';
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. **分析脚本** — 提取角色、场景、对话,输出结构化 JSON
2. **生成角色提示词** — 为每个角色生成 4 个角度(正面/背面/左侧面/右侧面)的图像生成提示词
3. **生成角色图像** — 调用图像生成 API 生成角色各个角度的图像
4. **生成场景提示词** — 根据场景描述生成稳定的图像生成提示词
5. **生成语音(TTS)** — 将对话台词转为语音
6. **导出 ComfyUI 工作流** — 将整个视频项目打包为 ComfyUI 可用的 workflow JSON
1. **分析脚本** — 提取角色、场景、对话
2. **生成角色提示词** — 为每个角色生成 4 个角度(正面/背面/左侧面/右侧面)
3. **生成场景提示词** — 描述场景的稳定图像生成提示词
4. **生成语音(TTS)** — 将对话台词转为语音描述
5. **导出 ComfyUI 工作流** — 生成 workflow JSON
## 输出格式
你的回复必须是 SSE 格式,每条消息是一个 JSON 对象,以 \`data: \` 开头,以 \n\n 结尾。
### 文本消息
data: {"type":"text","content":"你的回复内容"}
### 角色提示词
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"}
直接输出普通文本,用 emoji 标记不同内容类型:
- 🎭 角色名 — 角色描述和相关提示词
- 🌄 场景描述 — 场景提示词
- 🔊 角色名: "台词" — TTS 语音
- 📦 ComfyUI 工作流 — 最终的 workflow JSON(放在 \`\`\`json 代码块内)
## 工作流程
1. 用户提供脚本 → 你分析脚本结构
2. 针对每个角色 → 生成 4 角度提示词 → 调用图像生成
3. 针对每个场景 → 生成场景提示词 → 调用图像生成
4. 针对每段对话 → 生成 TTS 语音
2. 针对每个角色 → 生成 4 角度提示词
3. 针对每个场景 → 生成场景提示词
4. 针对每段对话 → 生成 TTS 语音描述
5. 最终导出 ComfyUI 工作流 JSON
始终用中文回复。不要输出 markdown 代码块,直接输出 SSE 格式数据。`;
始终用中文回复,文字简洁清晰。`;
interface ChatRequest {
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> {
return fetch(`${BOOSAPI_BASE}/v1/chat/completions`, {
return fetch(`${UPSTREAM_BASE}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer admin',
'Authorization': `Bearer ${UPSTREAM_KEY}`,
},
body: JSON.stringify({
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) {
app.post('/api/comfyui-agent/chat', async (request, reply) => {
const { messages } = request.body as ChatRequest;
@@ -100,7 +62,6 @@ export async function comfyuiAgentRoutes(app: FastifyInstance) {
}
try {
// Hijack the Fastify response for SSE streaming
reply.hijack();
const raw = reply.raw;
@@ -112,23 +73,12 @@ export async function comfyuiAgentRoutes(app: FastifyInstance) {
});
const abortController = new AbortController();
// Handle client disconnect
raw.on('close', () => {
abortController.abort();
});
raw.on('close', () => abortController.abort());
const llmResp = await callLLM(messages, abortController.signal);
if (!llmResp.ok) {
raw.write(`data: ${JSON.stringify({ type: 'text', content: 'LLM 调用失败,请稍后重试' })}\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`);
if (!llmResp.ok || !llmResp.body) {
raw.write(`data: ${JSON.stringify({ type: 'text', content: '服务暂时不可用,请稍后重试' })}\n\n`);
raw.write(`data: ${JSON.stringify({ type: 'done' })}\n\n`);
raw.end();
return;
@@ -137,6 +87,7 @@ export async function comfyuiAgentRoutes(app: FastifyInstance) {
const reader = llmResp.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let accumulatedText = '';
try {
while (true) {
@@ -156,19 +107,7 @@ export async function comfyuiAgentRoutes(app: FastifyInstance) {
const parsed = JSON.parse(data);
const content = parsed?.choices?.[0]?.delta?.content || '';
if (content) {
// Check if the content contains SSE-formatted data from the agent
// 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
}
}
accumulatedText += content;
raw.write(`data: ${JSON.stringify({ type: 'text', content })}\n\n`);
}
} catch {
@@ -178,15 +117,14 @@ export async function comfyuiAgentRoutes(app: FastifyInstance) {
}
}
} catch (e: any) {
if (e.name === 'AbortError') {
raw.write(`data: ${JSON.stringify({ type: 'text', content: '\\n\\n[生成中断]' })}\n\n`);
if (e.name !== 'AbortError') {
raw.write(`data: ${JSON.stringify({ type: 'text', content: `\n\n[错误: ${e.message}]` })}\n\n`);
}
}
raw.write(`data: ${JSON.stringify({ type: 'done' })}\n\n`);
raw.end();
} catch (e: any) {
// If we haven't hijacked yet, return error as JSON
const raw = reply.raw;
if (!raw.headersSent) {
reply.code(500).send({ error: `Chat failed: ${e.message}` });
+4 -1
View File
@@ -53,7 +53,10 @@ export default function ComfyUIAgent() {
const resp = await fetch('/api/comfyui-agent/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('auth_token') || ''}`,
},
body: JSON.stringify({ messages: apiMessages }),
signal: abortController.signal,
});