import { Image as ImageIcon, Mic } from "lucide-react"; import { useEffect, useState, type ReactNode } from "react"; import { getResourceAccess, resolveResourceAccessURL, resourceStorageKey } from "@/services/api/resources"; import type { CanvasNodeData } from "@/types/canvas"; const RESOURCE_FILE_PATTERN = /\/resources\/([^/?#]+)\/file/; /** 角色卡封面存的是 `/resources/:id/file`,展示前换成授权访问地址;换取失败再回落原地址。 */ export function useCharacterCoverSrc(url?: string) { const [resolved, setResolved] = useState<{ url: string; src: string } | null>(null); const resourceId = url ? RESOURCE_FILE_PATTERN.exec(url)?.[1] : undefined; useEffect(() => { if (!url || !resourceId) return; let cancelled = false; getResourceAccess(resourceStorageKey(decodeURIComponent(resourceId)), "display") .then((access) => { if (!cancelled) setResolved({ url, src: resolveResourceAccessURL(access.url) || url }); }) .catch(() => { if (!cancelled) setResolved({ url, src: url }); }); return () => { cancelled = true; }; }, [resourceId, url]); if (!url) return ""; if (!resourceId) return url; return resolved?.url === url ? resolved.src : ""; } export function characterCoverResourceId(url?: string) { const id = url ? RESOURCE_FILE_PATTERN.exec(url)?.[1] : undefined; return id ? decodeURIComponent(id) : ""; } export function characterInitial(name: string) { return Array.from(name.trim())[0] || "角"; } /** 画布上的角色卡:海报式主视觉,底部压名字与形象/声音状态。 */ export function CanvasCharacterReferenceNodeContent({ node }: { node: CanvasNodeData }) { const metadata = node.metadata; const name = metadata?.characterName || node.title || "未命名角色"; const role = typeof metadata?.characterDefinition?.role === "string" ? metadata.characterDefinition.role.trim() : ""; const visualReady = metadata?.characterVisualStatus === "ready"; const voiceReady = metadata?.characterVoiceStatus === "ready"; const cover = useCharacterCoverSrc(metadata?.characterCoverUrl); return (