refactor: purge Node.js, Rust is the one true backend

- Moved boosapi-rs/* to repo root
- Deleted all Node.js source (src/server/, src/web/, scripts/)
- Deleted node_modules, package.json, tsconfig, vite.config
- Updated .gitignore and AGENTS.md for Rust-only stack
- Fixed static file path for frontend dist/web/
- Dockerfile stays the same (already Rust-only)
- Kept dist/web/ for Rust static serving
This commit is contained in:
boos
2026-05-18 09:11:29 +08:00
parent 725f44699a
commit 5d29f03d50
1165 changed files with 271 additions and 323571 deletions
-428
View File
@@ -1,428 +0,0 @@
import { useState, useRef, useEffect } from 'react';
interface Message {
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
images?: Array<{ url: string; character?: string; angle?: string }>;
scenes?: Array<{ id: number; description: string; prompt: string }>;
tts?: Array<{ character: string; text: string; url: string }>;
workflowJson?: any;
}
export default function ComfyUIAgent() {
const [messages, setMessages] = useState<Message[]>([
{
id: 'welcome',
role: 'system',
content: '🎬 你好!我是 AI 视频生成助手。请告诉我你的视频脚本或创意,我可以帮你:\n\n1️⃣ 分析脚本结构(角色、场景、对话)\n2️⃣ 生成角色 4 角度图像(正面/背面/左侧/右侧)\n3️⃣ 生成场景背景图\n4️⃣ 生成角色语音(TTS)\n5️⃣ 导出 ComfyUI 工作流\n\n直接发脚本给我,我们开始吧!',
},
]);
const [input, setInput] = useState('');
const [loading, setLoading] = useState(false);
const [streamingContent, setStreamingContent] = useState('');
const chatEndRef = useRef<HTMLDivElement>(null);
const abortRef = useRef<AbortController | null>(null);
useEffect(() => {
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages, streamingContent]);
const sendMessage = async () => {
if (!input.trim() || loading) return;
const userMsg: Message = {
id: `user-${Date.now()}`,
role: 'user',
content: input.trim(),
};
setMessages((prev) => [...prev, userMsg]);
setInput('');
setLoading(true);
setStreamingContent('');
const abortController = new AbortController();
abortRef.current = abortController;
try {
const apiMessages = [...messages, userMsg].map((m) => ({
role: m.role === 'system' ? 'system' : m.role,
content: m.content,
}));
const resp = await fetch('/api/comfyui-agent/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('auth_token') || ''}`,
},
body: JSON.stringify({ messages: apiMessages }),
signal: abortController.signal,
});
if (!resp.ok) {
setStreamingContent('请求失败,请重试');
return;
}
const reader = resp.body?.getReader();
if (!reader) return;
const decoder = new TextDecoder();
let buffer = '';
let textBuffer = '';
const images: Message['images'] = [];
const scenes: Message['scenes'] = [];
const ttsList: Message['tts'] = [];
let msgId = `agent-${Date.now()}`;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const parts = buffer.split('\n');
buffer = parts.pop() || '';
for (const part of parts) {
if (!part.startsWith('data: ')) continue;
const data = part.slice(6).trim();
if (!data) continue;
try {
const event = JSON.parse(data);
switch (event.type) {
case 'text':
textBuffer += event.content || '';
setStreamingContent(textBuffer);
break;
case 'character_prompts':
textBuffer += `\n\n📐 **${event.character}** 角色提示词已生成`;
setStreamingContent(textBuffer);
break;
case 'image':
images.push({ url: event.url, character: event.character, angle: event.angle });
textBuffer += `\n🖼️ 生成 ${event.character} [${event.angle}]`;
setStreamingContent(textBuffer);
break;
case 'scene':
scenes.push({ id: event.id, description: event.description, prompt: event.prompt });
textBuffer += `\n🌄 场景 ${event.id}: ${event.description}`;
setStreamingContent(textBuffer);
break;
case 'tts':
ttsList.push({ character: event.character, text: event.text, url: event.url });
textBuffer += `\n🔊 ${event.character}: "${event.text.slice(0, 30)}..."`;
setStreamingContent(textBuffer);
break;
case 'comfyui_workflow':
textBuffer += '\n\n📦 ComfyUI 工作流已就绪,点击上方「导出」按钮下载';
setStreamingContent(textBuffer);
break;
case 'done':
break;
}
} catch {
// skip malformed JSON
}
}
}
const finalContent = textBuffer || (await resp.text()).slice(0, 500);
setMessages((prev) => [
...prev,
{
id: msgId,
role: 'assistant',
content: finalContent,
images: images.length > 0 ? images : undefined,
scenes: scenes.length > 0 ? scenes : undefined,
tts: ttsList.length > 0 ? ttsList : undefined,
},
]);
setStreamingContent('');
} catch (e: any) {
if (e.name !== 'AbortError') {
setStreamingContent(`错误: ${e.message}`);
}
} finally {
setLoading(false);
abortRef.current = null;
}
};
const stopGeneration = () => {
abortRef.current?.abort();
setLoading(false);
if (streamingContent) {
setMessages((prev) => [
...prev,
{ id: `agent-${Date.now()}`, role: 'assistant', content: streamingContent },
]);
setStreamingContent('');
}
};
const exportWorkflow = () => {
// Simple ComfyUI workflow JSON for video generation
const workflow = {
name: 'AI Video Project',
version: 1,
nodes: messages
.filter((m) => m.images && m.images.length > 0)
.flatMap((m) =>
(m.images || []).map((img, i) => ({
id: `img-${i}`,
type: 'LoadImage',
url: img.url,
character: img.character,
angle: img.angle,
}))
),
};
const blob = new Blob([JSON.stringify(workflow, null, 2)], {
type: 'application/json',
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'comfyui-workflow.json';
a.click();
URL.revokeObjectURL(url);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
};
return (
<div
style={{
width: '100%',
height: 'calc(100vh - 56px)',
display: 'flex',
flexDirection: 'column',
background: 'var(--color-bg, #0f0f1a)',
}}
>
{/* Header */}
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '12px 20px',
borderBottom: '1px solid var(--color-border, #2a2a3e)',
background: 'var(--color-surface, #1a1a2e)',
}}
>
<h1 style={{ fontSize: '16px', fontWeight: 600, margin: 0, color: 'var(--color-text, #e0e0f0)' }}>
🎬 AI 视频助手(对话)
</h1>
<button
onClick={exportWorkflow}
style={{
padding: '6px 16px',
borderRadius: '6px',
border: '1px solid var(--color-border, #2a2a3e)',
background: 'var(--color-surface-raised, #252540)',
color: 'var(--color-text, #e0e0f0)',
fontSize: '13px',
cursor: 'pointer',
}}
>
📦 导出 ComfyUI
</button>
</div>
{/* Messages */}
<div
style={{
flex: 1,
overflowY: 'auto',
padding: '16px 20px',
display: 'flex',
flexDirection: 'column',
gap: '12px',
}}
>
{messages.map((msg) => (
<div
key={msg.id}
style={{
display: 'flex',
flexDirection: 'column',
alignItems: msg.role === 'user' ? 'flex-end' : 'flex-start',
}}
>
<div
style={{
maxWidth: '85%',
padding: '10px 14px',
borderRadius: '12px',
background:
msg.role === 'user'
? 'var(--color-primary, #6366f1)'
: msg.role === 'system'
? 'transparent'
: 'var(--color-surface-raised, #252540)',
color:
msg.role === 'user'
? '#fff'
: 'var(--color-text, #e0e0f0)',
fontSize: '14px',
lineHeight: 1.6,
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
}}
>
{msg.content}
</div>
{/* Images */}
{msg.images && msg.images.length > 0 && (
<div
style={{
display: 'flex',
flexWrap: 'wrap',
gap: '8px',
marginTop: '8px',
}}
>
{msg.images.map((img, i) => (
<div key={i} style={{ textAlign: 'center' }}>
<img
src={img.url}
alt={`${img.character || ''} ${img.angle || ''}`}
style={{
width: '150px',
height: '150px',
objectFit: 'cover',
borderRadius: '8px',
border: '1px solid var(--color-border, #2a2a3e)',
}}
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
}}
/>
<div
style={{
fontSize: '11px',
color: 'var(--color-text-secondary, #888)',
marginTop: '2px',
}}
>
{img.character} {img.angle === 'front' ? '正面' : img.angle === 'back' ? '背面' : img.angle === 'left' ? '左侧' : img.angle === 'right' ? '右侧' : img.angle}
</div>
</div>
))}
</div>
)}
</div>
))}
{/* Streaming message */}
{streamingContent && (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
<div
style={{
maxWidth: '85%',
padding: '10px 14px',
borderRadius: '12px',
background: 'var(--color-surface-raised, #252540)',
color: 'var(--color-text, #e0e0f0)',
fontSize: '14px',
lineHeight: 1.6,
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
}}
>
{streamingContent}
<span style={{ animation: 'blink 1s step-end infinite', marginLeft: 2 }}>▌</span>
</div>
</div>
)}
<div ref={chatEndRef} />
</div>
{/* Input */}
<div
style={{
padding: '12px 20px',
borderTop: '1px solid var(--color-border, #2a2a3e)',
background: 'var(--color-surface, #1a1a2e)',
}}
>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="输入脚本内容或创作需求..."
rows={2}
disabled={loading}
style={{
flex: 1,
padding: '10px 14px',
borderRadius: '8px',
border: '1px solid var(--color-border, #2a2a3e)',
background: 'var(--color-bg, #0f0f1a)',
color: 'var(--color-text, #e0e0f0)',
fontSize: '14px',
resize: 'none',
outline: 'none',
}}
/>
{loading ? (
<button
onClick={stopGeneration}
style={{
padding: '10px 20px',
borderRadius: '8px',
border: 'none',
background: '#ef4444',
color: '#fff',
fontSize: '14px',
cursor: 'pointer',
whiteSpace: 'nowrap',
}}
>
■ 停止
</button>
) : (
<button
onClick={sendMessage}
disabled={!input.trim()}
style={{
padding: '10px 20px',
borderRadius: '8px',
border: 'none',
background: !input.trim() ? '#555' : '#6366f1',
color: '#fff',
fontSize: '14px',
cursor: !input.trim() ? 'not-allowed' : 'pointer',
whiteSpace: 'nowrap',
}}
>
发送
</button>
)}
</div>
</div>
<style>{`
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
`}</style>
</div>
);
}