feat(asset): enhance asset handling by adding storageKey and updating upload functions

This commit is contained in:
mlogclub
2026-04-17 12:54:26 +08:00
parent 2631689958
commit 0dfa783eec
7 changed files with 81 additions and 24 deletions
@@ -417,9 +417,7 @@ export function ChatPanel() {
onUploadImage={async (file) => { onUploadImage={async (file) => {
shouldStickToBottomRef.current = true; shouldStickToBottomRef.current = true;
const uploaded = await uploadImage(file); const uploaded = await uploadImage(file);
return uploaded return uploaded;
? { url: uploaded.url, filename: uploaded.filename }
: null;
}} }}
onSendAttachment={async (file) => { onSendAttachment={async (file) => {
shouldStickToBottomRef.current = true; shouldStickToBottomRef.current = true;
+37 -6
View File
@@ -22,10 +22,39 @@ import { fetchQuickReplyListAll, type AdminQuickReply } from "@/lib/api/admin"
import { generateUUID } from "@/lib/utils" import { generateUUID } from "@/lib/utils"
type UploadedImage = { type UploadedImage = {
assetId: string
provider: string
storageKey: string
url: string url: string
filename?: 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 = { type ImMessageEditorProps = {
disabled?: boolean disabled?: boolean
uploadingAsset?: boolean uploadingAsset?: boolean
@@ -98,7 +127,7 @@ export function ImMessageEditor({
orderedList: false, orderedList: false,
horizontalRule: false, horizontalRule: false,
}), }),
Image, MessageImage,
Placeholder.configure({ Placeholder.configure({
placeholder: "输入消息,Enter 发送,Shift + Enter 换行", placeholder: "输入消息,Enter 发送,Shift + Enter 换行",
}), }),
@@ -200,7 +229,7 @@ export function ImMessageEditor({
removeImageByTitle(editor, placeholderId) removeImageByTitle(editor, placeholderId)
return return
} }
replaceImageSourceByTitle(editor, placeholderId, uploaded.url, uploaded.filename || "image") replaceImageSourceByTitle(editor, placeholderId, uploaded)
} finally { } finally {
URL.revokeObjectURL(objectUrl) URL.revokeObjectURL(objectUrl)
requestAnimationFrame(() => { requestAnimationFrame(() => {
@@ -387,8 +416,7 @@ function removeImageByTitle(editor: NonNullable<ReturnType<typeof useEditor>>, t
function replaceImageSourceByTitle( function replaceImageSourceByTitle(
editor: NonNullable<ReturnType<typeof useEditor>>, editor: NonNullable<ReturnType<typeof useEditor>>,
title: string, title: string,
src: string, uploaded: UploadedImage
alt: string
) { ) {
const { state, view } = editor const { state, view } = editor
let targetPos: number | null = null let targetPos: number | null = null
@@ -404,8 +432,11 @@ function replaceImageSourceByTitle(
} }
const transaction = view.state.tr.setNodeMarkup(targetPos, undefined, { const transaction = view.state.tr.setNodeMarkup(targetPos, undefined, {
...view.state.doc.nodeAt(targetPos)?.attrs, ...view.state.doc.nodeAt(targetPos)?.attrs,
src, src: uploaded.url,
alt, alt: uploaded.filename || "image",
dataAssetId: uploaded.assetId,
dataProvider: uploaded.provider,
dataStorageKey: uploaded.storageKey,
title: "", title: "",
}) })
view.dispatch(transaction) view.dispatch(transaction)
+1
View File
@@ -96,6 +96,7 @@ export type AgentAsset = {
id: number id: number
assetId: string assetId: string
provider: string provider: string
storageKey: string
filename: string filename: string
fileSize: number fileSize: number
mimeType: string mimeType: string
+1
View File
@@ -86,6 +86,7 @@ export type ImAsset = {
id: number id: number
assetId: string assetId: string
provider: string provider: string
storageKey: string
filename: string filename: string
fileSize: number fileSize: number
mimeType: string mimeType: string
+37 -11
View File
@@ -10,10 +10,39 @@ import { ImageIcon, PaperclipIcon, SendHorizonalIcon } from "lucide-react";
import { generateUUID } from "@/lib/utils"; import { generateUUID } from "@/lib/utils";
type UploadedImage = { type UploadedImage = {
assetId: string;
provider: string;
storageKey: string;
url: string; url: string;
filename?: 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 = { type MessageEditorProps = {
disabled?: boolean; disabled?: boolean;
uploadingAsset?: boolean; uploadingAsset?: boolean;
@@ -61,7 +90,7 @@ export function MessageEditor({
orderedList: false, orderedList: false,
horizontalRule: false, horizontalRule: false,
}), }),
Image, MessageImage,
Placeholder.configure({ Placeholder.configure({
placeholder: "输入消息,Enter 发送,Shift + Enter 换行", placeholder: "输入消息,Enter 发送,Shift + Enter 换行",
}), }),
@@ -161,12 +190,7 @@ export function MessageEditor({
removeImageByTitle(editor, placeholderId); removeImageByTitle(editor, placeholderId);
return; return;
} }
replaceImageSourceByTitle( replaceImageSourceByTitle(editor, placeholderId, uploaded);
editor,
placeholderId,
uploaded.url,
uploaded.filename || "image",
);
} finally { } finally {
setLocalUploading(false); setLocalUploading(false);
URL.revokeObjectURL(objectUrl); URL.revokeObjectURL(objectUrl);
@@ -314,8 +338,7 @@ function removeImageByTitle(editor: NonNullable<ReturnType<typeof useEditor>>, t
function replaceImageSourceByTitle( function replaceImageSourceByTitle(
editor: NonNullable<ReturnType<typeof useEditor>>, editor: NonNullable<ReturnType<typeof useEditor>>,
title: string, title: string,
src: string, uploaded: UploadedImage,
alt: string,
) { ) {
const { state, view } = editor; const { state, view } = editor;
let targetPos: number | null = null; let targetPos: number | null = null;
@@ -331,8 +354,11 @@ function replaceImageSourceByTitle(
} }
const transaction = view.state.tr.setNodeMarkup(targetPos, undefined, { const transaction = view.state.tr.setNodeMarkup(targetPos, undefined, {
...view.state.doc.nodeAt(targetPos)?.attrs, ...view.state.doc.nodeAt(targetPos)?.attrs,
src, src: uploaded.url,
alt, alt: uploaded.filename || "image",
dataAssetId: uploaded.assetId,
dataProvider: uploaded.provider,
dataStorageKey: uploaded.storageKey,
title: "", title: "",
}); });
view.dispatch(transaction); view.dispatch(transaction);
+1
View File
@@ -60,6 +60,7 @@ export type WidgetAsset = {
id: number; id: number;
assetId: string; assetId: string;
provider: string; provider: string;
storageKey: string;
filename: string; filename: string;
fileSize: number; fileSize: number;
mimeType: string; mimeType: string;
+3 -4
View File
@@ -21,6 +21,7 @@ import {
type RealtimeEnvelope, type RealtimeEnvelope,
} from "@/lib/services/realtime"; } from "@/lib/services/realtime";
import type { import type {
WidgetAsset,
WidgetConfigResponse, WidgetConfigResponse,
WidgetConversation, WidgetConversation,
WidgetMessage, WidgetMessage,
@@ -111,9 +112,7 @@ export interface ChatStore {
setIsVisible: (isVisible: boolean) => void; setIsVisible: (isVisible: boolean) => void;
bootstrap: () => void; bootstrap: () => void;
handleSendMessage: (html: string) => Promise<void>; handleSendMessage: (html: string) => Promise<void>;
uploadMessageImage: ( uploadMessageImage: (file: File) => Promise<WidgetAsset | null>;
file: File,
) => Promise<{ url: string; filename?: string } | null>;
sendAttachment: (file: File) => Promise<void>; sendAttachment: (file: File) => Promise<void>;
retry: () => void; retry: () => void;
disconnectSocket: () => void; disconnectSocket: () => void;
@@ -507,7 +506,7 @@ export const useChatStore = create<ChatStore>((set, get) => {
set({ error: "" }); set({ error: "" });
try { try {
const asset = await uploadImage(conversationId, file); const asset = await uploadImage(conversationId, file);
return { url: asset.url, filename: asset.filename }; return asset;
} catch (e) { } catch (e) {
set({ error: e instanceof Error ? e.message : "发送图片失败" }); set({ error: e instanceof Error ? e.message : "发送图片失败" });
return null; return null;