实现 MinIO 对象存储图片流水线(可迁移 OSS)

- 新增 S3 兼容 Storage 抽象与 MinIO 实现,compose 启动 MinIO
- 上传接口:校验 → 缩放 → WebP → 主图/缩略图入库
- 消息图片 content 改为对象 URL,拒绝 base64
- 工作台/访客端改为先上传再发消息
This commit is contained in:
yml2213
2026-07-15 12:21:31 +08:00
parent 85b407fddf
commit ec2f12c6ed
20 changed files with 907 additions and 108 deletions
+36 -14
View File
@@ -10,6 +10,7 @@ import { useAuth } from '@/stores/auth'
import {
addSessionNote, claimSession, endSession, getAvailableAgents, getCustomers, getKnowledgeEntries,
getSession, getSessions, markSessionRead, sendSessionMessage, transferSession, updateSessionPriority,
uploadImage,
type AvailableAgent, type Customer, type KnowledgeEntry, type Message, type Session, type SessionEvent,
} from '@/services/api'
@@ -111,7 +112,7 @@ const Dashboard = () => {
const [knowledgeKeyword, setKnowledgeKeyword] = useState('')
const [knowledgeEntries, setKnowledgeEntries] = useState<KnowledgeEntry[]>([])
const [knowledgeLoading, setKnowledgeLoading] = useState(false)
const [imagePreview, setImagePreview] = useState<string | null>(null)
const [pendingImage, setPendingImage] = useState<{ file: File; preview: string } | null>(null)
const [noteInput, setNoteInput] = useState('')
const [savingNote, setSavingNote] = useState(false)
const [customerHistory, setCustomerHistory] = useState<Session[]>([])
@@ -312,17 +313,34 @@ const Dashboard = () => {
const handleImage = (file?: File) => {
if (!file) return
if (!['image/jpeg', 'image/png', 'image/gif'].includes(file.type)) {
antMsg.error('仅支持 jpg、png、gif 图片')
const okTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']
if (!okTypes.includes(file.type)) {
antMsg.error('仅支持 jpg、png、gif、webp 图片')
return
}
if (file.size > 5 * 1024 * 1024) {
antMsg.error('图片不能超过 5 MB')
if (file.size > 10 * 1024 * 1024) {
antMsg.error('图片不能超过 10 MB')
return
}
const reader = new FileReader()
reader.onload = () => setImagePreview(String(reader.result))
reader.readAsDataURL(file)
if (pendingImage?.preview) URL.revokeObjectURL(pendingImage.preview)
setPendingImage({ file, preview: URL.createObjectURL(file) })
}
const confirmSendImage = async () => {
if (!pendingImage || !selectedId || !canOperate) return
setSending(true)
try {
const uploaded = await uploadImage(pendingImage.file)
await sendSessionMessage(selectedId, uploaded.data.url, 'image')
URL.revokeObjectURL(pendingImage.preview)
setPendingImage(null)
await loadDetail(selectedId)
await loadAll()
} catch (error) {
antMsg.error(error instanceof Error ? error.message : '图片发送失败')
} finally {
setSending(false)
}
}
const handleClaim = async (sessionID: number) => {
@@ -412,7 +430,7 @@ const Dashboard = () => {
<input
ref={fileInputRef}
type="file"
accept="image/jpeg,image/png,image/gif"
accept="image/jpeg,image/png,image/gif,image/webp"
className="hidden"
onChange={event => { handleImage(event.target.files?.[0]); event.currentTarget.value = '' }}
/>
@@ -1006,15 +1024,19 @@ const Dashboard = () => {
</Modal>
<Modal
title="图片预览"
open={Boolean(imagePreview)}
onCancel={() => setImagePreview(null)}
onOk={() => { if (imagePreview) sendMessage(imagePreview, 'image'); setImagePreview(null) }}
okText="发送"
open={Boolean(pendingImage)}
onCancel={() => {
if (pendingImage?.preview) URL.revokeObjectURL(pendingImage.preview)
setPendingImage(null)
}}
onOk={confirmSendImage}
okText="上传并发送"
okButtonProps={{ loading: sending }}
>
<div className="flex justify-center">
<img src={imagePreview || ''} alt="待发送图片预览" className="max-h-[420px] max-w-full rounded-lg" />
<img src={pendingImage?.preview || ''} alt="待发送图片预览" className="max-h-[420px] max-w-full rounded-lg" />
</div>
<p className="text-xs text-neutral-400 text-center mt-3 mb-0"> WebP </p>
</Modal>
<Modal title="知识库与快捷回复" open={knowledgeOpen} onCancel={() => setKnowledgeOpen(false)} footer={null} width={640}>
<Input