Fix image proxy public access and model avatar fallback

This commit is contained in:
134 2026-08-19 09:27:10 +08:00
parent c27a707776
commit 15c3b519a7
3 changed files with 108 additions and 19 deletions

View File

@ -1437,6 +1437,34 @@ function buildPreviewImageUrl(url: string, width: number): string {
});
}
function ModelAvatarImage({
src,
alt,
className,
fallback,
}: {
src: string;
alt: string;
className: string;
fallback: JSX.Element;
}) {
const [loadFailed, setLoadFailed] = useState(false);
if (!src || loadFailed) {
return fallback;
}
return (
<img
src={src}
alt={alt}
className={className}
loading="lazy"
decoding="async"
onError={() => setLoadFailed(true)}
/>
);
}
const CANVAS_IMAGE_ANALYSIS_TITLE = "图片分析";
const CANVAS_IMAGE_DONE_SUMMARY = "已完成本次生成,如需微调可继续告诉我。";
const CANVAS_AGENT_SECTION_TITLES = new Set([
@ -15115,17 +15143,22 @@ export function CanvasChatPanel({
)}
>
{model.avatarUrl ? (
<img
<ModelAvatarImage
src={buildPreviewImageUrl(model.avatarUrl, 40)}
alt={model.displayName}
className="block h-full w-full object-contain"
loading="lazy"
decoding="async"
onError={(event) => {
if (event.currentTarget.src !== model.avatarUrl) {
event.currentTarget.src = model.avatarUrl || "";
}
}}
fallback={
<span
className={cn(
"flex h-full w-full items-center justify-center text-[10px] font-medium leading-none",
isVideoWorkspace
? "text-muted-foreground"
: "text-[#6b7280]"
)}
>
{model.id === "auto" ? "A" : model.displayName.slice(0, 1)}
</span>
}
/>
) : (
<span
@ -18648,17 +18681,20 @@ export function CanvasChatPanel({
)}
>
{selectDisplayModel?.avatarUrl ? (
<img
<ModelAvatarImage
src={buildPreviewImageUrl(selectDisplayModel.avatarUrl, 64)}
alt={selectDisplayModel.displayName || "模型头像"}
className="block h-full w-full object-contain"
loading="lazy"
decoding="async"
onError={(event) => {
if (event.currentTarget.src !== selectDisplayModel.avatarUrl) {
event.currentTarget.src = selectDisplayModel.avatarUrl || "";
}
}}
fallback={
<span
className={cn(
"flex h-full w-full items-center justify-center text-[10px] font-medium leading-none",
isVideoToolbarDark ? "text-zinc-100" : "text-[#4b5563] dark:text-zinc-900"
)}
>
{(selectDisplayModel?.displayName || "A").slice(0, 1)}
</span>
}
/>
) : getToolbarItem("chatModelSelector").iconUrl ? (
<CanvasToolbarIcon

View File

@ -151,6 +151,36 @@ function isNanoBananaProModel(model: { displayName?: string | null; modelId?: st
modelId.includes("nano-banana")
);
}
function ModelAvatarImage({
src,
alt,
className,
fallback,
}: {
src: string;
alt: string;
className: string;
fallback: JSX.Element;
}) {
const [loadFailed, setLoadFailed] = useState<boolean>(false);
if (!src || loadFailed) {
return fallback;
}
return (
<img
src={src}
alt={alt}
className={className}
loading="lazy"
decoding="async"
onError={() => setLoadFailed(true)}
/>
);
}
import { Trigger } from "@radix-ui/react-select";
import type { UIMessage } from "ai";
import equal from "fast-deep-equal";
@ -2132,10 +2162,11 @@ function PureModelSelectorCompact({
title={readOnlyName}
>
{readOnlyAvatarUrl ? (
<img
<ModelAvatarImage
src={readOnlyAvatarUrl}
alt={readOnlyName}
className="h-7 w-7 rounded-full object-cover"
fallback={<Bot className="h-4 w-4 text-[#8a919f]" />}
/>
) : (
<Bot className="h-4 w-4" />
@ -2189,10 +2220,11 @@ function PureModelSelectorCompact({
variant="ghost"
>
{selectedModel?.avatarUrl ? (
<img
<ModelAvatarImage
src={selectedModel.avatarUrl}
alt={selectedModel.name}
className="h-7 w-7 rounded-full object-cover"
fallback={<Bot className="h-4 w-4 text-[#8a919f]" />}
/>
) : (
<Bot className="h-4 w-4" />
@ -2227,10 +2259,26 @@ function PureModelSelectorCompact({
<span className="flex min-w-0 flex-1 items-center gap-3 overflow-visible">
<span className="flex h-6 w-6 shrink-0 items-center justify-center text-[#2f3640]">
{model.avatarUrl ? (
<img
<ModelAvatarImage
src={model.avatarUrl}
alt={model.displayName}
className="h-6 w-6 rounded-full object-cover"
fallback={
<span className="flex h-6 w-6 items-center justify-center text-[#2f3640]">
{(() => {
if (isVideoProviderType(model)) {
return <Video className="h-[19px] w-[19px]" strokeWidth={2.2} />;
}
if (isImageGenerationModel(model)) {
return <ImageIcon className="h-[19px] w-[19px]" strokeWidth={2.2} />;
}
if ((model.providerType || "") === "system") {
return <Sparkles className="h-[19px] w-[19px]" strokeWidth={2.2} />;
}
return <Bot className="h-[19px] w-[19px]" strokeWidth={2.2} />;
})()}
</span>
}
/>
) : (
<span className="flex h-6 w-6 items-center justify-center">

View File

@ -778,6 +778,11 @@ export async function proxy(request: NextRequest, event: NextFetchEvent) {
return NextResponse.next();
}
// 图片代理用于站内图片优化/转码,需支持未登录用户下的公开图片链接渲染。
if (pathname === "/api/image-proxy" && request.method === "GET") {
return NextResponse.next();
}
// 允许匿名访问新版首页灵感数据
if (pathname === "/api/featured" || pathname.startsWith("/api/featured/")) {
return NextResponse.next();