From 0dfa783eeca04c4e41335a4dd8425657286d7f78 Mon Sep 17 00:00:00 2001 From: mlogclub Date: Fri, 17 Apr 2026 12:54:26 +0800 Subject: [PATCH] feat(asset): enhance asset handling by adding storageKey and updating upload functions --- .../conversations/_components/chat-panel.tsx | 4 +- dashboard/components/im-message-editor.tsx | 43 ++++++++++++++--- dashboard/lib/api/agent.ts | 1 + dashboard/lib/api/im.ts | 1 + widget/components/im/message-editor.tsx | 48 ++++++++++++++----- widget/lib/services/types.ts | 1 + widget/lib/store/chat-store.ts | 7 ++- 7 files changed, 81 insertions(+), 24 deletions(-) diff --git a/dashboard/app/(dashboard)/conversations/_components/chat-panel.tsx b/dashboard/app/(dashboard)/conversations/_components/chat-panel.tsx index 377ac4a..bad7ce6 100644 --- a/dashboard/app/(dashboard)/conversations/_components/chat-panel.tsx +++ b/dashboard/app/(dashboard)/conversations/_components/chat-panel.tsx @@ -417,9 +417,7 @@ export function ChatPanel() { onUploadImage={async (file) => { shouldStickToBottomRef.current = true; const uploaded = await uploadImage(file); - return uploaded - ? { url: uploaded.url, filename: uploaded.filename } - : null; + return uploaded; }} onSendAttachment={async (file) => { shouldStickToBottomRef.current = true; diff --git a/dashboard/components/im-message-editor.tsx b/dashboard/components/im-message-editor.tsx index 852e52e..21d9a14 100644 --- a/dashboard/components/im-message-editor.tsx +++ b/dashboard/components/im-message-editor.tsx @@ -22,10 +22,39 @@ import { fetchQuickReplyListAll, type AdminQuickReply } from "@/lib/api/admin" import { generateUUID } from "@/lib/utils" type UploadedImage = { + assetId: string + provider: string + storageKey: string url: string filename?: string } +const MessageImage = Image.extend({ + addAttributes() { + return { + ...this.parent?.(), + dataAssetId: { + default: null, + parseHTML: (element) => element.getAttribute("data-asset-id"), + renderHTML: (attributes) => + attributes.dataAssetId ? { "data-asset-id": attributes.dataAssetId } : {}, + }, + dataProvider: { + default: null, + parseHTML: (element) => element.getAttribute("data-provider"), + renderHTML: (attributes) => + attributes.dataProvider ? { "data-provider": attributes.dataProvider } : {}, + }, + dataStorageKey: { + default: null, + parseHTML: (element) => element.getAttribute("data-storage-key"), + renderHTML: (attributes) => + attributes.dataStorageKey ? { "data-storage-key": attributes.dataStorageKey } : {}, + }, + } + }, +}) + type ImMessageEditorProps = { disabled?: boolean uploadingAsset?: boolean @@ -98,7 +127,7 @@ export function ImMessageEditor({ orderedList: false, horizontalRule: false, }), - Image, + MessageImage, Placeholder.configure({ placeholder: "输入消息,Enter 发送,Shift + Enter 换行", }), @@ -200,7 +229,7 @@ export function ImMessageEditor({ removeImageByTitle(editor, placeholderId) return } - replaceImageSourceByTitle(editor, placeholderId, uploaded.url, uploaded.filename || "image") + replaceImageSourceByTitle(editor, placeholderId, uploaded) } finally { URL.revokeObjectURL(objectUrl) requestAnimationFrame(() => { @@ -387,8 +416,7 @@ function removeImageByTitle(editor: NonNullable>, t function replaceImageSourceByTitle( editor: NonNullable>, title: string, - src: string, - alt: string + uploaded: UploadedImage ) { const { state, view } = editor let targetPos: number | null = null @@ -404,8 +432,11 @@ function replaceImageSourceByTitle( } const transaction = view.state.tr.setNodeMarkup(targetPos, undefined, { ...view.state.doc.nodeAt(targetPos)?.attrs, - src, - alt, + src: uploaded.url, + alt: uploaded.filename || "image", + dataAssetId: uploaded.assetId, + dataProvider: uploaded.provider, + dataStorageKey: uploaded.storageKey, title: "", }) view.dispatch(transaction) diff --git a/dashboard/lib/api/agent.ts b/dashboard/lib/api/agent.ts index 2461c3a..8846696 100644 --- a/dashboard/lib/api/agent.ts +++ b/dashboard/lib/api/agent.ts @@ -96,6 +96,7 @@ export type AgentAsset = { id: number assetId: string provider: string + storageKey: string filename: string fileSize: number mimeType: string diff --git a/dashboard/lib/api/im.ts b/dashboard/lib/api/im.ts index bdb843c..fbf3d04 100644 --- a/dashboard/lib/api/im.ts +++ b/dashboard/lib/api/im.ts @@ -86,6 +86,7 @@ export type ImAsset = { id: number assetId: string provider: string + storageKey: string filename: string fileSize: number mimeType: string diff --git a/widget/components/im/message-editor.tsx b/widget/components/im/message-editor.tsx index 95e64cc..d0893e0 100644 --- a/widget/components/im/message-editor.tsx +++ b/widget/components/im/message-editor.tsx @@ -10,10 +10,39 @@ import { ImageIcon, PaperclipIcon, SendHorizonalIcon } from "lucide-react"; import { generateUUID } from "@/lib/utils"; type UploadedImage = { + assetId: string; + provider: string; + storageKey: string; url: string; filename?: string; }; +const MessageImage = Image.extend({ + addAttributes() { + return { + ...this.parent?.(), + dataAssetId: { + default: null, + parseHTML: (element) => element.getAttribute("data-asset-id"), + renderHTML: (attributes) => + attributes.dataAssetId ? { "data-asset-id": attributes.dataAssetId } : {}, + }, + dataProvider: { + default: null, + parseHTML: (element) => element.getAttribute("data-provider"), + renderHTML: (attributes) => + attributes.dataProvider ? { "data-provider": attributes.dataProvider } : {}, + }, + dataStorageKey: { + default: null, + parseHTML: (element) => element.getAttribute("data-storage-key"), + renderHTML: (attributes) => + attributes.dataStorageKey ? { "data-storage-key": attributes.dataStorageKey } : {}, + }, + }; + }, +}); + type MessageEditorProps = { disabled?: boolean; uploadingAsset?: boolean; @@ -61,7 +90,7 @@ export function MessageEditor({ orderedList: false, horizontalRule: false, }), - Image, + MessageImage, Placeholder.configure({ placeholder: "输入消息,Enter 发送,Shift + Enter 换行", }), @@ -161,12 +190,7 @@ export function MessageEditor({ removeImageByTitle(editor, placeholderId); return; } - replaceImageSourceByTitle( - editor, - placeholderId, - uploaded.url, - uploaded.filename || "image", - ); + replaceImageSourceByTitle(editor, placeholderId, uploaded); } finally { setLocalUploading(false); URL.revokeObjectURL(objectUrl); @@ -314,8 +338,7 @@ function removeImageByTitle(editor: NonNullable>, t function replaceImageSourceByTitle( editor: NonNullable>, title: string, - src: string, - alt: string, + uploaded: UploadedImage, ) { const { state, view } = editor; let targetPos: number | null = null; @@ -331,8 +354,11 @@ function replaceImageSourceByTitle( } const transaction = view.state.tr.setNodeMarkup(targetPos, undefined, { ...view.state.doc.nodeAt(targetPos)?.attrs, - src, - alt, + src: uploaded.url, + alt: uploaded.filename || "image", + dataAssetId: uploaded.assetId, + dataProvider: uploaded.provider, + dataStorageKey: uploaded.storageKey, title: "", }); view.dispatch(transaction); diff --git a/widget/lib/services/types.ts b/widget/lib/services/types.ts index 250ecde..d8b4281 100644 --- a/widget/lib/services/types.ts +++ b/widget/lib/services/types.ts @@ -60,6 +60,7 @@ export type WidgetAsset = { id: number; assetId: string; provider: string; + storageKey: string; filename: string; fileSize: number; mimeType: string; diff --git a/widget/lib/store/chat-store.ts b/widget/lib/store/chat-store.ts index b4e803d..5cb21ba 100644 --- a/widget/lib/store/chat-store.ts +++ b/widget/lib/store/chat-store.ts @@ -21,6 +21,7 @@ import { type RealtimeEnvelope, } from "@/lib/services/realtime"; import type { + WidgetAsset, WidgetConfigResponse, WidgetConversation, WidgetMessage, @@ -111,9 +112,7 @@ export interface ChatStore { setIsVisible: (isVisible: boolean) => void; bootstrap: () => void; handleSendMessage: (html: string) => Promise; - uploadMessageImage: ( - file: File, - ) => Promise<{ url: string; filename?: string } | null>; + uploadMessageImage: (file: File) => Promise; sendAttachment: (file: File) => Promise; retry: () => void; disconnectSocket: () => void; @@ -507,7 +506,7 @@ export const useChatStore = create((set, get) => { set({ error: "" }); try { const asset = await uploadImage(conversationId, file); - return { url: asset.url, filename: asset.filename }; + return asset; } catch (e) { set({ error: e instanceof Error ? e.message : "发送图片失败" }); return null;