From d5fbc490ea1383f85f5d15a326124eb7bd805cbc Mon Sep 17 00:00:00 2001 From: CrescentLeaf Date: Wed, 24 Sep 2025 21:33:16 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E7=99=BC=E9=80=81?= =?UTF-8?q?=E6=96=87=E4=BB=B6=20*=20=E7=9B=AE=E5=89=8D=E9=82=84=E5=8F=AA?= =?UTF-8?q?=E8=83=BD=E6=8B=96=E6=8B=BD=E5=88=B0=E8=BC=B8=E5=85=A5=E6=A1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/api/ApiDeclare.ts | 4 +- client/ui/chat/ChatFragment.tsx | 86 +++++++++++++++++++++++++-------- server/api/ChatApi.ts | 4 +- 3 files changed, 72 insertions(+), 22 deletions(-) diff --git a/client/api/ApiDeclare.ts b/client/api/ApiDeclare.ts index d503082..4edba17 100644 --- a/client/api/ApiDeclare.ts +++ b/client/api/ApiDeclare.ts @@ -15,7 +15,9 @@ export type CallMethod = "Chat.getInfo" | "Chat.sendMessage" | - "Chat.getMessageHistory" + "Chat.getMessageHistory" | + + "Chat.uploadFile" export type ClientEvent = "Client.onMessage" diff --git a/client/ui/chat/ChatFragment.tsx b/client/ui/chat/ChatFragment.tsx index cc00335..bcb4a7c 100644 --- a/client/ui/chat/ChatFragment.tsx +++ b/client/ui/chat/ChatFragment.tsx @@ -8,10 +8,11 @@ import Client from "../../api/Client.ts" import Message from "../../api/client_data/Message.ts" import Chat from "../../api/client_data/Chat.ts" import data from "../../Data.ts" -import { checkApiSuccessOrSncakbar } from "../snackbar.ts" +import { checkApiSuccessOrSncakbar, snackbar } from "../snackbar.ts" import useAsyncEffect from "../useAsyncEffect.ts" import * as marked from 'marked' import DOMPurify from 'dompurify' +import randomUUID from "../../randomUUID.ts" interface Args extends React.HTMLAttributes { target: string @@ -25,12 +26,14 @@ const markedInstance = new marked.Marked({ const text = this.parser.parseInline(tokens) return `${text}` }, - paragraph({ tokens, depth: _depth }) { + paragraph({ tokens }) { const text = this.parser.parseInline(tokens) return `${text}` }, - image({ title, href }) { - return `` + image({ text, href }) { + if (/uploaded_files\/[A-Za-z0-9]+$/.test(href)) + return `` + return `` } } }) @@ -85,12 +88,6 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC } setMessagesList(returnMsgs.concat(messagesList)) - if (page.current == 0) - setTimeout(() => chatPanelRef.current!.scrollTo({ - top: 10000000000, - behavior: "smooth", - }), 100) - page.current++ } @@ -119,8 +116,21 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC const [showLoadingMoreMessagesTip, setShowLoadingMoreMessagesTip] = React.useState(false) const [showNoMoreMessagesTip, setShowNoMoreMessagesTip] = React.useState(false) + const cachedFiles = React.useRef({} as { [fileName: string]: ArrayBuffer }) async function sendMessage() { - const text = inputRef.current!.value + let text = inputRef.current!.value + for (const fileName of Object.keys(cachedFiles.current)) { + if (text.indexOf(fileName) != -1) { + const re = await Client.invoke("Chat.uploadFile", { + token: data.access_token, + file_name: fileName, + target, + data: cachedFiles.current[fileName], + }, 5000) + if (checkApiSuccessOrSncakbar(re, `文件[${fileName}] 上傳失敗`)) return + text = text.replaceAll(fileName, re.data!.file_path as string) + } + } const re = await Client.invoke("Chat.sendMessage", { token: data.access_token, @@ -129,6 +139,7 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC }, 5000) if (checkApiSuccessOrSncakbar(re, "發送失敗")) return inputRef.current!.value = '' + cachedFiles.current = {} } return ( @@ -203,14 +214,14 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC -
) @@ -230,18 +241,55 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC paddingRight: '4px', backgroundColor: 'rgb(var(--mdui-color-surface))', }} onDrop={(e) => { - if (e.dataTransfer.files) { - const files = e.dataTransfer.files + const input = inputRef.current!.shadowRoot!.querySelector('[part=input]') as HTMLTextAreaElement + function insertText(text: string) { + inputRef.current!.value = input.value!.substring(0, input.selectionStart as number) + text + input.value!.substring(input.selectionEnd as number, input.value.length) + } + async function addFile(type: string, name: string, data: Blob | Response) { + cachedFiles.current![name] = await data.arrayBuffer() + if (type.startsWith('image/')) + insertText(`![圖片](${name})`) + else + insertText(`![File=${name}](${name})`) + } + function getFileNameOrRandom(urlString: string) { + const url = new URL(urlString) + let filename = url.pathname.substring(url.pathname.lastIndexOf('/') + 1).trim() + if (filename == '') + filename = 'file_' + randomUUID() + return filename + } + if (e.dataTransfer.items.length > 0) { // 基于当前的实现, 浏览器不会读取文件的字节流来确定其媒体类型, 其根据文件扩展名进行假设 // https://developer.mozilla.org/zh-CN/docs/Web/API/Blob/type - for (const file of files) { - if (file.type.startsWith("image/")) { - + for (const item of e.dataTransfer.items) { + if (item.type == 'text/uri-list') { + item.getAsString(async (url) => { + try { + // 即便是 no-cors 還是殘廢, 因此暫時沒有什麽想法 + const re = await fetch(url) + const type = re.headers.get("Content-Type") + if (type?.startsWith("image/")) + addFile(type as string, getFileNameOrRandom(url), re) + } catch (e) { + snackbar({ + message: '無法解析連結: ' + (e as Error).message, + placement: 'top', + }) + } + }) + } else if (item.kind == 'file') { + e.preventDefault() + const file = item.getAsFile() as File + addFile(item.type, file.name, file) } } } }}> - { + { + if (inputRef.current?.value.trim() == '') + cachedFiles.current = {} + }} onKeyDown={(event) => { if (event.ctrlKey && event.key == 'Enter') sendMessage() }} style={{ diff --git a/server/api/ChatApi.ts b/server/api/ChatApi.ts index dc298f5..7d00c55 100644 --- a/server/api/ChatApi.ts +++ b/server/api/ChatApi.ts @@ -182,13 +182,13 @@ export default class ChatApi extends BaseApi { msg: "用戶無權訪問該對話", } - const file = await FileManager.uploadFile(args.file_name as string, args.data as Buffer) + const file = await FileManager.uploadFile(args.file_name as string, args.data as Buffer, args.target as string) return { code: 200, msg: "成功", data: { - messages: MessagesManager.getInstanceForChat(chat).getMessagesWithPage(15, args.page as number), + file_path: 'uploaded_files/' + file.getHash() }, } })