feat: 支持传图

This commit is contained in:
ZyGuo committed 2026-06-03 10:51:42 +08:00
1 parent 34a9f15cb4
commit 5d39aa55bd
3 files changed
+331 -86

No files matched your search

@@ -0,0 +1,247 @@
type BrainyPageContext = {
title?: string;
content?: string;
url?: string;
};
type BrainyScreenshotPath = {
image_url?: string;
};
type BrainyScreenshotImage = {
filename?: string;
url?: string;
size?: number;
};
type BrainyScreenshot = {
dataUrl?: string;
fileName?: string;
filePath?: BrainyScreenshotPath[];
images?: BrainyScreenshotImage[];
};
export type BrainyChatSubmitMessage = {
source?: string;
type?: string;
payload?: {
text?: string;
prompt?: string;
pageContext?: BrainyPageContext;
screenshot?: BrainyScreenshot;
};
requestId?: string;
};
export type IframePromptInputFilePart = {
type: "file";
url: string;
mediaType: string;
filename: string;
file?: File;
};
export type ParsedBrainyChatSubmitMessage = {
submitText: string;
files: IframePromptInputFilePart[];
iframeContext?: Record<string, unknown>;
};
function trimToUndefined(value: string | undefined): string | undefined {
const trimmed = value?.trim();
return trimmed ? trimmed : undefined;
}
function fileNameFromPath(path: string | undefined): string | undefined {
if (!path) return undefined;
const parts = path.split(/[\\/]/).filter(Boolean);
return parts.at(-1);
}
function isLikelyLocalPath(path: string): boolean {
return /^[a-zA-Z]:[\\/]/.test(path) || path.startsWith("\\\\");
}
function mimeTypeFromFilename(filename: string | undefined): string | undefined {
const lower = filename?.toLowerCase() ?? "";
if (lower.endsWith(".png")) return "image/png";
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
if (lower.endsWith(".webp")) return "image/webp";
if (lower.endsWith(".gif")) return "image/gif";
return undefined;
}
function parseDataUrlMeta(dataUrl: string): { mimeType: string; payload: string } | null {
const match = /^data:([^;,]+)?(?:;base64)?,(.*)$/i.exec(dataUrl);
if (!match) return null;
return {
mimeType: match[1] || "application/octet-stream",
payload: match[2] || "",
};
}
function decodeBase64(base64: string): Uint8Array {
if (typeof Buffer !== "undefined") {
return Uint8Array.from(Buffer.from(base64, "base64"));
}
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i += 1) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
function fileFromDataUrl(
dataUrl: string,
filename: string,
mediaType: string,
): File | undefined {
const meta = parseDataUrlMeta(dataUrl);
if (!meta) return undefined;
const bytes = decodeBase64(meta.payload);
const arrayBuffer = bytes.buffer.slice(
bytes.byteOffset,
bytes.byteOffset + bytes.byteLength,
);
return new File([arrayBuffer], filename, {
type: mediaType || meta.mimeType,
});
}
function normalizePageContext(
pageContext: BrainyPageContext | undefined,
): BrainyPageContext | undefined {
if (!pageContext) return undefined;
const normalized = {
title: trimToUndefined(pageContext.title),
content: trimToUndefined(pageContext.content),
url: trimToUndefined(pageContext.url),
};
return normalized.title || normalized.content || normalized.url
? normalized
: undefined;
}
export function buildIframeMessageText({
text,
prompt,
pageContext,
}: {
text?: string;
prompt?: string;
pageContext?: BrainyPageContext;
}) {
const primaryText = trimToUndefined(text) || trimToUndefined(prompt) || "";
const sections: string[] = [];
if (primaryText) {
sections.push(primaryText);
}
const normalizedPrompt = trimToUndefined(prompt);
if (normalizedPrompt && normalizedPrompt !== primaryText) {
sections.push(
`<extension_prompt>\n${normalizedPrompt}\n</extension_prompt>`,
);
}
const normalizedPageContext = normalizePageContext(pageContext);
if (normalizedPageContext) {
const pageContextParts = [
normalizedPageContext.title
? `<title>\n${normalizedPageContext.title}\n</title>`
: null,
normalizedPageContext.url
? `<url>\n${normalizedPageContext.url}\n</url>`
: null,
normalizedPageContext.content
? `<content>\n${normalizedPageContext.content}\n</content>`
: null,
].filter(Boolean);
if (pageContextParts.length > 0) {
sections.push(
`<page_context>\n${pageContextParts.join("\n\n")}\n</page_context>`,
);
}
}
return sections.join("\n\n");
}
function buildScreenshotFileParts(
screenshot: BrainyScreenshot | undefined,
): IframePromptInputFilePart[] {
if (!screenshot) return [];
const fallbackPath =
screenshot.filePath?.map((item) => item.image_url).find(Boolean) ??
screenshot.images?.map((item) => item.url).find(Boolean);
const fallbackFilename =
trimToUndefined(screenshot.fileName) ??
screenshot.images?.map((item) => trimToUndefined(item.filename)).find(Boolean) ??
fileNameFromPath(fallbackPath) ??
"screenshot.png";
const dataUrl = trimToUndefined(screenshot.dataUrl);
if (dataUrl) {
const parsed = parseDataUrlMeta(dataUrl);
const mediaType =
parsed?.mimeType || mimeTypeFromFilename(fallbackFilename) || "image/png";
const file = fileFromDataUrl(dataUrl, fallbackFilename, mediaType);
return [
{
type: "file",
url: dataUrl,
mediaType,
filename: fallbackFilename,
...(file ? { file } : {}),
},
];
}
const remoteImage = screenshot.images?.find((item) => {
const url = trimToUndefined(item.url);
return !!url && !isLikelyLocalPath(url);
});
if (remoteImage?.url && remoteImage.filename) {
return [
{
type: "file",
url: remoteImage.url,
mediaType:
mimeTypeFromFilename(remoteImage.filename) || "application/octet-stream",
filename: remoteImage.filename,
},
];
}
return [];
}
export function parseBrainyChatSubmitMessage(
data: BrainyChatSubmitMessage,
): ParsedBrainyChatSubmitMessage {
const text = trimToUndefined(data.payload?.text);
const prompt = trimToUndefined(data.payload?.prompt);
const pageContext = normalizePageContext(data.payload?.pageContext);
const submitText = buildIframeMessageText({ text, prompt, pageContext });
const files = buildScreenshotFileParts(data.payload?.screenshot);
const requestId = trimToUndefined(data.requestId);
return {
submitText,
files,
...(requestId
? { iframeContext: { brainy_request_id: requestId } }
: {}),
};
}