fix: route video agent to comfyui and ignore openspec
This commit is contained in:
@@ -4,10 +4,11 @@ import { getSessionToken } from '../authSession.js';
|
||||
|
||||
export default function ComfyUI() {
|
||||
const [isDark, setIsDark] = React.useState(true);
|
||||
const [iframeSrc, setIframeSrc] = React.useState('');
|
||||
const [starting, setStarting] = React.useState(true);
|
||||
const location = useLocation();
|
||||
const audience = location.pathname.startsWith('/user/') ? 'user' : 'admin';
|
||||
const token = getSessionToken(audience, localStorage);
|
||||
const iframeSrc = token ? `/comfy/?token=${encodeURIComponent(token)}` : '';
|
||||
|
||||
React.useEffect(() => {
|
||||
const el = document.documentElement;
|
||||
@@ -18,6 +19,49 @@ export default function ComfyUI() {
|
||||
return () => mo.disconnect();
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
const encodedToken = token ? encodeURIComponent(token) : '';
|
||||
|
||||
if (!token) {
|
||||
setIframeSrc('');
|
||||
setStarting(false);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}
|
||||
|
||||
setStarting(true);
|
||||
setIframeSrc('');
|
||||
|
||||
const start = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/video-agent/start', {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!cancelled && response.ok) {
|
||||
setIframeSrc(`/comfy/?token=${encodedToken}`);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// Fall through to the legacy embedded ComfyUI proxy.
|
||||
}
|
||||
|
||||
if (!cancelled) {
|
||||
setIframeSrc('/api/comfyui/proxy/');
|
||||
}
|
||||
};
|
||||
|
||||
void start().finally(() => {
|
||||
if (!cancelled) setStarting(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [token]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -47,7 +91,7 @@ export default function ComfyUI() {
|
||||
color: 'var(--color-text-muted)',
|
||||
fontSize: 14,
|
||||
}}>
|
||||
登录状态已过期,请重新登录。
|
||||
{starting ? '正在启动 ComfyUI...' : '登录状态已过期,请重新登录。'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+303
-10
@@ -57,10 +57,20 @@ type StreamAccumulator = {
|
||||
};
|
||||
|
||||
type StoredAgentSession = {
|
||||
activeConversationId?: string;
|
||||
conversations?: StoredAgentConversation[];
|
||||
messages?: AgentMessage[];
|
||||
selectedModel?: string;
|
||||
};
|
||||
|
||||
type StoredAgentConversation = {
|
||||
id: string;
|
||||
title: string;
|
||||
updatedAt: number;
|
||||
messages: AgentMessage[];
|
||||
selectedModel: string;
|
||||
};
|
||||
|
||||
const WELCOME_MESSAGE: AgentMessage = {
|
||||
id: 'welcome',
|
||||
role: 'system',
|
||||
@@ -82,6 +92,25 @@ function sessionStorageKey(audience: 'admin' | 'user') {
|
||||
return `boosapi.comfyuiAgent.${audience}.session`;
|
||||
}
|
||||
|
||||
function buildConversationTitle(messages: AgentMessage[]) {
|
||||
const firstUser = messages.find((message) => message.role === 'user')?.content.trim();
|
||||
if (!firstUser) return '新对话';
|
||||
return firstUser.length > 24 ? `${firstUser.slice(0, 24)}...` : firstUser;
|
||||
}
|
||||
|
||||
function createConversation(
|
||||
messages: AgentMessage[] = [WELCOME_MESSAGE],
|
||||
selectedModel = '',
|
||||
): StoredAgentConversation {
|
||||
return {
|
||||
id: createId('conversation'),
|
||||
title: buildConversationTitle(messages),
|
||||
updatedAt: Date.now(),
|
||||
messages,
|
||||
selectedModel,
|
||||
};
|
||||
}
|
||||
|
||||
function isValidMessage(value: unknown): value is AgentMessage {
|
||||
if (!value || typeof value !== 'object') return false;
|
||||
const item = value as Partial<AgentMessage>;
|
||||
@@ -97,6 +126,41 @@ function readStoredSession(audience: 'admin' | 'user'): StoredAgentSession {
|
||||
const raw = localStorage.getItem(sessionStorageKey(audience));
|
||||
if (!raw) return {};
|
||||
const parsed = JSON.parse(raw) as StoredAgentSession;
|
||||
const conversations: StoredAgentConversation[] = Array.isArray(parsed.conversations)
|
||||
? parsed.conversations
|
||||
.map((conversation): StoredAgentConversation | null => {
|
||||
const messages = Array.isArray(conversation?.messages)
|
||||
? conversation.messages.filter(isValidMessage)
|
||||
: [];
|
||||
if (!conversation?.id || !messages.length) return null;
|
||||
return {
|
||||
id: String(conversation.id),
|
||||
title:
|
||||
typeof conversation.title === 'string' && conversation.title.trim()
|
||||
? conversation.title.trim()
|
||||
: buildConversationTitle(messages),
|
||||
updatedAt:
|
||||
typeof conversation.updatedAt === 'number'
|
||||
? conversation.updatedAt
|
||||
: Date.now(),
|
||||
messages,
|
||||
selectedModel:
|
||||
typeof conversation.selectedModel === 'string'
|
||||
? conversation.selectedModel
|
||||
: '',
|
||||
};
|
||||
})
|
||||
.filter((conversation): conversation is StoredAgentConversation => !!conversation)
|
||||
: [];
|
||||
if (conversations.length) {
|
||||
const activeConversationId = conversations.some(
|
||||
(conversation) => conversation.id === parsed.activeConversationId,
|
||||
)
|
||||
? parsed.activeConversationId
|
||||
: conversations[0].id;
|
||||
return { conversations, activeConversationId };
|
||||
}
|
||||
|
||||
const messages = Array.isArray(parsed.messages)
|
||||
? parsed.messages.filter(isValidMessage)
|
||||
: undefined;
|
||||
@@ -112,13 +176,13 @@ function readStoredSession(audience: 'admin' | 'user'): StoredAgentSession {
|
||||
|
||||
function persistStoredSession(
|
||||
audience: 'admin' | 'user',
|
||||
messages: AgentMessage[],
|
||||
selectedModel: string,
|
||||
conversations: StoredAgentConversation[],
|
||||
activeConversationId: string,
|
||||
) {
|
||||
try {
|
||||
localStorage.setItem(
|
||||
sessionStorageKey(audience),
|
||||
JSON.stringify({ messages, selectedModel }),
|
||||
JSON.stringify({ conversations, activeConversationId }),
|
||||
);
|
||||
} catch {
|
||||
// Ignore quota/private-mode failures; chat still works for the current page.
|
||||
@@ -184,8 +248,27 @@ export default function ComfyUIAgent() {
|
||||
() => readStoredSession(sessionAudience),
|
||||
[sessionAudience],
|
||||
);
|
||||
const initialConversations = useMemo(() => {
|
||||
if (initialStoredSession.conversations?.length) return initialStoredSession.conversations;
|
||||
return [
|
||||
createConversation(
|
||||
initialStoredSession.messages || [WELCOME_MESSAGE],
|
||||
initialStoredSession.selectedModel || '',
|
||||
),
|
||||
];
|
||||
}, [initialStoredSession]);
|
||||
const initialActiveConversation =
|
||||
initialConversations.find(
|
||||
(conversation) => conversation.id === initialStoredSession.activeConversationId,
|
||||
) || initialConversations[0];
|
||||
const [conversations, setConversations] = useState<StoredAgentConversation[]>(
|
||||
() => initialConversations,
|
||||
);
|
||||
const [activeConversationId, setActiveConversationId] = useState(
|
||||
() => initialActiveConversation.id,
|
||||
);
|
||||
const [messages, setMessages] = useState<AgentMessage[]>(
|
||||
() => initialStoredSession.messages || [WELCOME_MESSAGE],
|
||||
() => initialActiveConversation.messages,
|
||||
);
|
||||
const [input, setInput] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -193,7 +276,7 @@ export default function ComfyUIAgent() {
|
||||
const [status, setStatus] = useState<AgentStatus | null>(null);
|
||||
const [modelOptions, setModelOptions] = useState<string[]>([]);
|
||||
const [selectedModel, setSelectedModel] = useState(
|
||||
() => initialStoredSession.selectedModel || '',
|
||||
() => initialActiveConversation.selectedModel || '',
|
||||
);
|
||||
const [modelSearch, setModelSearch] = useState('');
|
||||
const [loadingModels, setLoadingModels] = useState(false);
|
||||
@@ -224,8 +307,17 @@ export default function ComfyUIAgent() {
|
||||
|
||||
useEffect(() => {
|
||||
const stored = readStoredSession(sessionAudience);
|
||||
setMessages(stored.messages || [WELCOME_MESSAGE]);
|
||||
setSelectedModel(stored.selectedModel || '');
|
||||
const nextConversations = stored.conversations?.length
|
||||
? stored.conversations
|
||||
: [createConversation(stored.messages || [WELCOME_MESSAGE], stored.selectedModel || '')];
|
||||
const nextActive =
|
||||
nextConversations.find(
|
||||
(conversation) => conversation.id === stored.activeConversationId,
|
||||
) || nextConversations[0];
|
||||
setConversations(nextConversations);
|
||||
setActiveConversationId(nextActive.id);
|
||||
setMessages(nextActive.messages);
|
||||
setSelectedModel(nextActive.selectedModel || '');
|
||||
setModelSearch('');
|
||||
setStreaming(null);
|
||||
setLoading(false);
|
||||
@@ -234,8 +326,30 @@ export default function ComfyUIAgent() {
|
||||
}, [sessionAudience]);
|
||||
|
||||
useEffect(() => {
|
||||
persistStoredSession(sessionAudience, messages, selectedModel);
|
||||
}, [messages, selectedModel, sessionAudience]);
|
||||
setConversations((current) => {
|
||||
const now = Date.now();
|
||||
const next = current.length
|
||||
? current.map((conversation) =>
|
||||
conversation.id === activeConversationId
|
||||
? {
|
||||
...conversation,
|
||||
title: buildConversationTitle(messages),
|
||||
updatedAt: now,
|
||||
messages,
|
||||
selectedModel,
|
||||
}
|
||||
: conversation,
|
||||
)
|
||||
: [createConversation(messages, selectedModel)];
|
||||
const ordered = [...next].sort((a, b) => {
|
||||
if (a.id === activeConversationId) return -1;
|
||||
if (b.id === activeConversationId) return 1;
|
||||
return b.updatedAt - a.updatedAt;
|
||||
});
|
||||
persistStoredSession(sessionAudience, ordered, activeConversationId);
|
||||
return ordered;
|
||||
});
|
||||
}, [activeConversationId, messages, selectedModel, sessionAudience]);
|
||||
|
||||
useEffect(() => {
|
||||
const token = getSessionToken(sessionAudience, localStorage);
|
||||
@@ -479,6 +593,52 @@ export default function ComfyUIAgent() {
|
||||
abortRef.current?.abort();
|
||||
};
|
||||
|
||||
const startNewConversation = () => {
|
||||
if (loading) return;
|
||||
const conversation = createConversation([WELCOME_MESSAGE], selectedModel || status?.model || '');
|
||||
setConversations((current) => {
|
||||
const next = [conversation, ...current];
|
||||
persistStoredSession(sessionAudience, next, conversation.id);
|
||||
return next;
|
||||
});
|
||||
setActiveConversationId(conversation.id);
|
||||
setMessages(conversation.messages);
|
||||
setSelectedModel(conversation.selectedModel || '');
|
||||
setInput('');
|
||||
setStreaming(null);
|
||||
setModelSearch('');
|
||||
};
|
||||
|
||||
const selectConversation = (conversation: StoredAgentConversation) => {
|
||||
if (loading || conversation.id === activeConversationId) return;
|
||||
abortRef.current?.abort();
|
||||
setActiveConversationId(conversation.id);
|
||||
setMessages(conversation.messages);
|
||||
setSelectedModel(conversation.selectedModel || '');
|
||||
setInput('');
|
||||
setStreaming(null);
|
||||
setModelSearch('');
|
||||
};
|
||||
|
||||
const deleteConversation = (conversationId: string) => {
|
||||
if (loading) return;
|
||||
setConversations((current) => {
|
||||
const remaining = current.filter((conversation) => conversation.id !== conversationId);
|
||||
const next = remaining.length ? remaining : [createConversation([WELCOME_MESSAGE], selectedModel)];
|
||||
const nextActive =
|
||||
conversationId === activeConversationId
|
||||
? next[0]
|
||||
: next.find((conversation) => conversation.id === activeConversationId) || next[0];
|
||||
setActiveConversationId(nextActive.id);
|
||||
setMessages(nextActive.messages);
|
||||
setSelectedModel(nextActive.selectedModel || '');
|
||||
setInput('');
|
||||
setStreaming(null);
|
||||
persistStoredSession(sessionAudience, next, nextActive.id);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const renderAssets = (message: AgentMessage | StreamAccumulator) => (
|
||||
<>
|
||||
{!!message.images?.length && (
|
||||
@@ -520,6 +680,70 @@ export default function ComfyUIAgent() {
|
||||
|
||||
return (
|
||||
<div style={pageStyle}>
|
||||
<aside style={conversationSidebarStyle}>
|
||||
<div style={conversationSidebarHeaderStyle}>
|
||||
<strong>对话</strong>
|
||||
<button
|
||||
type="button"
|
||||
onClick={startNewConversation}
|
||||
disabled={loading}
|
||||
style={{
|
||||
...secondaryButtonStyle,
|
||||
padding: '6px 10px',
|
||||
opacity: loading ? 0.6 : 1,
|
||||
}}
|
||||
>
|
||||
新建
|
||||
</button>
|
||||
</div>
|
||||
<div style={conversationListStyle}>
|
||||
{conversations.map((conversation) => (
|
||||
<button
|
||||
key={conversation.id}
|
||||
type="button"
|
||||
onClick={() => selectConversation(conversation)}
|
||||
disabled={loading}
|
||||
style={{
|
||||
...conversationItemStyle,
|
||||
borderColor:
|
||||
conversation.id === activeConversationId
|
||||
? 'var(--color-primary)'
|
||||
: 'var(--color-border-light)',
|
||||
background:
|
||||
conversation.id === activeConversationId
|
||||
? 'var(--color-primary-soft, rgba(79, 70, 229, 0.12))'
|
||||
: 'var(--color-surface)',
|
||||
}}
|
||||
>
|
||||
<span style={conversationTitleStyle}>{conversation.title}</span>
|
||||
<span style={conversationMetaStyle}>
|
||||
{Math.max(0, conversation.messages.filter((message) => message.role !== 'system').length)} 条消息
|
||||
</span>
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
deleteConversation(conversation.id);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
deleteConversation(conversation.id);
|
||||
}
|
||||
}}
|
||||
style={conversationDeleteStyle}
|
||||
aria-label="删除对话"
|
||||
>
|
||||
删除
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section style={chatPanelStyle}>
|
||||
<header style={headerStyle}>
|
||||
<div>
|
||||
<h1 style={titleStyle}>视频助手</h1>
|
||||
@@ -645,6 +869,7 @@ export default function ComfyUIAgent() {
|
||||
</button>
|
||||
)}
|
||||
</footer>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -652,11 +877,79 @@ export default function ComfyUIAgent() {
|
||||
const pageStyle: CSSProperties = {
|
||||
height: 'calc(100vh - 56px)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexDirection: 'row',
|
||||
background: 'var(--color-bg)',
|
||||
color: 'var(--color-text-primary)',
|
||||
};
|
||||
|
||||
const chatPanelStyle: CSSProperties = {
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
};
|
||||
|
||||
const conversationSidebarStyle: CSSProperties = {
|
||||
width: 260,
|
||||
minWidth: 220,
|
||||
borderRight: '1px solid var(--color-border)',
|
||||
background: 'var(--color-surface)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
};
|
||||
|
||||
const conversationSidebarHeaderStyle: CSSProperties = {
|
||||
minHeight: 72,
|
||||
padding: '14px 14px',
|
||||
borderBottom: '1px solid var(--color-border)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 10,
|
||||
};
|
||||
|
||||
const conversationListStyle: CSSProperties = {
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
overflowY: 'auto',
|
||||
display: 'grid',
|
||||
alignContent: 'start',
|
||||
gap: 8,
|
||||
padding: 12,
|
||||
};
|
||||
|
||||
const conversationItemStyle: CSSProperties = {
|
||||
width: '100%',
|
||||
border: '1px solid var(--color-border-light)',
|
||||
borderRadius: 8,
|
||||
padding: 10,
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'minmax(0, 1fr) auto',
|
||||
gap: '4px 8px',
|
||||
textAlign: 'left',
|
||||
color: 'var(--color-text-primary)',
|
||||
cursor: 'pointer',
|
||||
};
|
||||
|
||||
const conversationTitleStyle: CSSProperties = {
|
||||
gridColumn: '1 / -1',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
fontSize: 13,
|
||||
fontWeight: 700,
|
||||
};
|
||||
|
||||
const conversationMetaStyle: CSSProperties = {
|
||||
fontSize: 12,
|
||||
color: 'var(--color-text-muted)',
|
||||
};
|
||||
|
||||
const conversationDeleteStyle: CSSProperties = {
|
||||
fontSize: 12,
|
||||
color: 'var(--color-danger, #dc2626)',
|
||||
};
|
||||
|
||||
const headerStyle: CSSProperties = {
|
||||
minHeight: 72,
|
||||
padding: '14px 20px',
|
||||
|
||||
Reference in New Issue
Block a user