fix(open-webui): drop MCP exposure and address remaining review comments
Per ajnart's review (and CodeRabbit withdrawing its own MCP comment on the
predecessor PR), the Open WebUI chat isn't a meaningful MCP surface:
- remove the .meta({ mcp }) metadata from every openWebUi procedure
Remaining CodeRabbit comments from the predecessor PR that weren't carried over:
- processWeb: constrain the URL to http(s) and reject loopback/metadata hosts
(private LAN ranges stay allowed for self-hosters); it is also creator-gated
- uploadFile/transcribe: bound the base64 payload and validate filename/type
- docs: align the example URL with the integration default port (8080)
- new chat: clear the composer draft so it doesn't leak into the fresh chat
- web ingestion: keep the typed URL when ingestion fails
- persistence: fall back to the default title for image-only opening turns, and
don't persist a partial reply from a failed turn as finished history
- multimodal round-trip: make the native chat/history contracts accept the same
text-or-parts content union as completions, and flatten (not drop) non-text
nodes when rebuilding transcripts so attachment-backed chats aren't lost.
Image data URLs are still kept out of persisted history by design.
This commit is contained in:
7 files changed
+107
-78
No files matched your search
@@ -42,17 +42,34 @@ export const toCompletionMessages = (messages: ChatMessage[]): CompletionMessage
|
||||
: { role: message.role, content: message.content },
|
||||
);
|
||||
|
||||
type RawMessage = { role?: string | null; content?: string | null };
|
||||
type ContentPart = { type?: string | null; text?: string | null };
|
||||
type MessageContent = string | ContentPart[] | null | undefined;
|
||||
type RawMessage = { role?: string | null; content?: MessageContent };
|
||||
type HistoryMessage = RawMessage & { parentId?: string | null };
|
||||
interface RawChat {
|
||||
messages?: RawMessage[] | null;
|
||||
history?: { currentId?: string | null; messages?: Record<string, HistoryMessage> | null } | null;
|
||||
}
|
||||
|
||||
const toChatMessage = (role: string | null | undefined, content: string | null | undefined): ChatMessage | null =>
|
||||
(role === "system" || role === "user" || role === "assistant") && typeof content === "string"
|
||||
? { role, content }
|
||||
: null;
|
||||
// Open WebUI stores message content as either a plain string or multimodal
|
||||
// parts. Flatten to display text (keeping text parts, dropping image nodes)
|
||||
// rather than discarding the whole message when it isn't a bare string.
|
||||
const flattenContent = (content: MessageContent): string | null => {
|
||||
if (typeof content === "string") return content;
|
||||
if (Array.isArray(content)) {
|
||||
return content
|
||||
.filter((part): part is ContentPart & { text: string } => typeof part?.text === "string")
|
||||
.map((part) => part.text)
|
||||
.join("");
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const toChatMessage = (role: string | null | undefined, content: MessageContent): ChatMessage | null => {
|
||||
if (role !== "system" && role !== "user" && role !== "assistant") return null;
|
||||
const text = flattenContent(content);
|
||||
return text === null ? null : { role, content: text };
|
||||
};
|
||||
|
||||
/**
|
||||
* Reconstructs the ordered conversation from an Open WebUI chat. Chats created in
|
||||
|
||||
@@ -170,6 +170,7 @@ export function Chat({ options, integrationIds, isEditMode }: WidgetComponentPro
|
||||
const handleNewChat = () => {
|
||||
stream.reset();
|
||||
attachments.reset();
|
||||
setInput("");
|
||||
setError(null);
|
||||
setHistoryOpened(false);
|
||||
};
|
||||
|
||||
@@ -98,9 +98,11 @@ export function useAttachments({ integrationId, onError, errorMessages }: UseAtt
|
||||
const raw = webInput.trim();
|
||||
if (!raw) return;
|
||||
const url = /^https?:\/\//i.test(raw) ? raw : `https://${raw}`;
|
||||
setWebInput("");
|
||||
try {
|
||||
const document = await processWeb.mutateAsync({ integrationId, url });
|
||||
// Only clear the field once ingestion succeeds so a transient error
|
||||
// doesn't force the user to retype the URL.
|
||||
setWebInput("");
|
||||
setWebItems((previous) =>
|
||||
previous.some((item) => item.collectionName === document.collectionName)
|
||||
? previous
|
||||
|
||||
@@ -77,7 +77,12 @@ export function useChatStream({
|
||||
const runPersist = async (finalMessages: ChatMessage[], session: number) => {
|
||||
if (!integrationId || !model) return;
|
||||
// Native history stores plain text; image data URLs are dropped on persist.
|
||||
const title = finalMessages.find((message) => message.role === "user")?.content.slice(0, 80) ?? newChatTitle;
|
||||
// Derive the title from the first user message that actually has text so an
|
||||
// image-only opening turn still gets the fallback title instead of a blank.
|
||||
const firstUserText = finalMessages
|
||||
.find((message) => message.role === "user" && message.content.trim() !== "")
|
||||
?.content.trim();
|
||||
const title = firstUserText ? firstUserText.slice(0, 80) : newChatTitle;
|
||||
const payload = {
|
||||
title,
|
||||
models: [model],
|
||||
@@ -131,15 +136,15 @@ export function useChatStream({
|
||||
setPendingContextTexts([]);
|
||||
};
|
||||
|
||||
const finalizeAssistantMessage = (stopped = false) => {
|
||||
const finalizeAssistantMessage = (skipPersist = false) => {
|
||||
const finalText = streamingTextRef.current;
|
||||
if (finalText.length > 0) {
|
||||
const finalMessages = [...messagesRef.current, { role: "assistant" as const, content: finalText }];
|
||||
setMessages(finalMessages);
|
||||
messagesRef.current = finalMessages;
|
||||
// A user-stopped reply is kept on screen but not written to history as a
|
||||
// finished turn; a later real turn will persist the whole conversation.
|
||||
if (!stopped) void persistChat(finalMessages);
|
||||
// A stopped or failed reply is kept on screen but not written to history as
|
||||
// a finished turn; a later real turn will persist the whole conversation.
|
||||
if (!skipPersist) void persistChat(finalMessages);
|
||||
}
|
||||
streamingTextRef.current = "";
|
||||
setStreamingText("");
|
||||
@@ -165,12 +170,14 @@ export function useChatStream({
|
||||
finalizeAssistantMessage();
|
||||
} else if (event.type === "error") {
|
||||
setError(event.message);
|
||||
finalizeAssistantMessage();
|
||||
// Don't persist a partial reply from a failed turn as finished history.
|
||||
finalizeAssistantMessage(true);
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
setError(err.message);
|
||||
finalizeAssistantMessage();
|
||||
// Don't persist a partial reply from a failed turn as finished history.
|
||||
finalizeAssistantMessage(true);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user