From d5d392aca4bc24ea2f5d7450846eeb2f65572047 Mon Sep 17 00:00:00 2001 From: wanghanlin <1533525126@qq.com> Date: Tue, 25 Aug 2026 17:51:56 +0800 Subject: [PATCH] =?UTF-8?q?feat(sdk):=20=E6=94=AF=E6=8C=81=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E4=B8=8A=E4=BC=A0=E4=B8=8E=E5=A4=9A=E6=A8=A1=E6=80=81?= =?UTF-8?q?=E5=AF=B9=E8=AF=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SDK 前端新增图片附件上传与多模态对话能力,输入区新增附件按钮与图片预览,新增 allowImageUpload 配置项;/attachment/upload 鉴权改由 SdkAuthFilter 承担(移除方法级 @PreAuthorize),JwtAuthFilter/SecurityConfig 同步放行 --- client/src/api.ts | 68 ++++++++-- client/src/chat.ts | 120 +++++++++++++++--- client/src/config.ts | 1 + client/src/dom.ts | 28 +++- client/src/i18n.ts | 2 + client/src/index.ts | 1 + client/src/types.ts | 20 +++ .../controller/AttachmentController.java | 6 +- .../supportbot/security/JwtAuthFilter.java | 4 +- .../supportbot/security/SdkAuthFilter.java | 4 +- .../supportbot/security/SecurityConfig.java | 2 + src/main/resources/static/sdk/test.html | 2 +- 12 files changed, 221 insertions(+), 37 deletions(-) diff --git a/client/src/api.ts b/client/src/api.ts index 1232d60..6971906 100644 --- a/client/src/api.ts +++ b/client/src/api.ts @@ -6,7 +6,7 @@ * userId → accountId(客户账号 ID) * chatId → 自动管理的对话 ID(从 /ai/sdk/conversation/list 获取或自动生成) */ -import { ResolvedConfig, ApiResponse, CategoryNode } from './types'; +import { ResolvedConfig, ApiResponse, CategoryNode, ImageAttachment } from './types'; import { logger } from './logger'; import { t } from './i18n'; @@ -72,13 +72,22 @@ function setIfPresent(params: URLSearchParams, key: string, value: string | numb // ==================== 对话接口 URL 构建 ==================== +/** + * 把图片 URL 列表追加为逗号分隔、encodeURIComponent 编码的查询参数 + */ +function appendImageUrls(params: URLSearchParams, imageUrls?: string[]): void { + if (imageUrls && imageUrls.length > 0) { + params.set('imageUrls', imageUrls.map(u => encodeURIComponent(u)).join(',')); + } +} + /** * 构建同步对话请求 URL * - integrateId → roleId * - userId → accountId * - chatId → 自动管理的对话 ID */ -function buildChatUrl(message: string): string { +function buildChatUrl(message: string, imageUrls?: string[]): string { const params = new URLSearchParams(); params.set('message', message); params.set('chatId', currentConfig!.chatId); @@ -87,6 +96,7 @@ function buildChatUrl(message: string): string { setIfPresent(params, 'roleId', getActiveIntegrateId()); // userId 映射为 accountId setIfPresent(params, 'accountId', currentConfig!.userId); + appendImageUrls(params, imageUrls); return buildUrl(`/ai/chat?${params.toString()}`); } @@ -94,7 +104,7 @@ function buildChatUrl(message: string): string { /** * 构建 SSE 流式请求 URL */ -function buildChatSSEUrl(message: string, categoryId?: number, useRag?: boolean): string { +function buildChatSSEUrl(message: string, categoryId?: number, useRag?: boolean, imageUrls?: string[]): string { const params = new URLSearchParams(); params.set('message', message); params.set('chatId', currentConfig!.chatId); @@ -108,6 +118,7 @@ function buildChatSSEUrl(message: string, categoryId?: number, useRag?: boolean) params.set('enableRag', 'true'); params.set('rewriteStrategy', currentConfig!.rewriteStrategy || 'REWRITE'); } + appendImageUrls(params, imageUrls); return buildUrl(`/ai/chat/stream?${params.toString()}`); } @@ -161,8 +172,9 @@ async function safeFetch( Object.assign(headers, options.headers as Record); } } - // 为受 SdkAuthFilter 保护的路径自动注入 Bearer Token(/ai/** 和 /feedback) - if (currentConfig?.token && (url.includes('/ai/') || url.endsWith('/feedback'))) { + // 为受 SdkAuthFilter 保护的路径自动注入 Bearer Token(/ai/**、/feedback、/attachment/upload) + if (currentConfig?.token && + (url.includes('/ai/') || url.endsWith('/feedback') || url.includes('/attachment/upload'))) { headers['Authorization'] = `Bearer ${currentConfig.token}`; } @@ -223,8 +235,8 @@ function getHttpErrorMessage(status: number): string { /** * 同步对话请求 */ -export async function chatRequest(message: string): Promise { - const url = buildChatUrl(message); +export async function chatRequest(message: string, imageUrls?: string[]): Promise { + const url = buildChatUrl(message, imageUrls); logger.lifecycleSend(getActiveIntegrateId(), message.length); try { @@ -257,11 +269,12 @@ export async function chatSSERequest( onError: (error: CskError) => void, categoryId?: number, useRag?: boolean, + imageUrls?: string[], signal?: AbortSignal ): Promise { const url = useRag - ? buildChatSSEUrl(message, categoryId, true) - : buildChatSSEUrl(message, categoryId, false); + ? buildChatSSEUrl(message, categoryId, true, imageUrls) + : buildChatSSEUrl(message, categoryId, false, imageUrls); let totalText = ''; /** @@ -449,6 +462,43 @@ export async function chatSSERequest( } } +// ==================== 图片上传 ==================== + +/** 上传超时时间(图片体积较大,放宽到 120s) */ +const UPLOAD_TIMEOUT = 120000; + +/** + * 上传单个图片到 /attachment/upload,返回可公开访问的 URL 结构 + */ +export async function uploadAttachment(file: File): Promise { + const url = buildUrl('/attachment/upload'); + const fd = new FormData(); + fd.append('file', file); + + const response = await safeFetch(url, { method: 'POST', body: fd }, UPLOAD_TIMEOUT); + if (!response.ok) { + throw new CskError(getHttpErrorMessage(response.status), `http_${response.status}`); + } + + const json = await response.json() as ApiResponse<{ + url: string; + name?: string; + mimeType?: string; + size?: number; + }>; + + if (!json.success || !json.data?.url) { + throw new CskError(json.message || t('error_upload_failed'), 'upload_failed'); + } + + return { + name: json.data.name || file.name, + url: json.data.url, + mimeType: json.data.mimeType, + size: json.data.size, + }; +} + // ==================== P1: 知识库分类 ==================== /** diff --git a/client/src/chat.ts b/client/src/chat.ts index d6882d2..1e39fc7 100644 --- a/client/src/chat.ts +++ b/client/src/chat.ts @@ -6,7 +6,7 @@ * userId → accountId(客户账号 ID) * chatId → 自动管理(从 /conversation/list 获取或自动生成) */ -import { ResolvedConfig, ChatMessage, RagSource } from './types'; +import { ResolvedConfig, ChatMessage, RagSource, ImageAttachment } from './types'; import { chatRequest, chatSSERequest, @@ -26,6 +26,7 @@ import { getActiveIntegrateId, CskError, fetchSuggestions, + uploadAttachment, } from './api'; import { renderUserBubble, @@ -83,6 +84,9 @@ let currentCategoryId: number | undefined; /** 当前是否使用 RAG 对话 */ let useRag = false; +/** 待发送的图片列表(与 t-chat-sender 的 attachmentsProps.items 同步) */ +let pendingImages: ImageAttachment[] = []; + /** * 初始化对话模块 */ @@ -195,12 +199,20 @@ async function loadHistoryFromBackend(): Promise { function bindSendEvents(): void { if (!inputEl) return; - // t-chat-sender:send 事件(点击发送/回车)携带 value;stop 事件(loading 态点击)中断流式 + // t-chat-sender:send 事件(点击发送/回车)携带 value 与 attachments;stop 事件(loading 态点击)中断流式 inputEl.addEventListener('send', (e) => { - const detail = (e as CustomEvent).detail as { value?: string } | undefined; + const detail = (e as CustomEvent).detail as { + value?: string; + attachments?: Array<{ name?: string; url?: string; fileType?: string; size?: number }>; + } | undefined; const value = (detail?.value || '').trim(); - if (!value || isSending) return; - handleSend(value); + // 从附件中提取图片(本次仅图片参与多模态对话) + const images: ImageAttachment[] = (detail?.attachments || []) + .filter(a => a.fileType === 'image' && a.url) + .map(a => ({ name: a.name || '', url: a.url as string, size: a.size })); + + if ((!value && images.length === 0) || isSending) return; + handleSend(value, images); }); inputEl.addEventListener('stop', () => { @@ -209,11 +221,61 @@ function bindSendEvents(): void { } }); + // t-chat-sender:fileSelect 事件(选择文件后触发,携带原始 File[]) + inputEl.addEventListener('fileSelect', (e) => { + const files = (e as CustomEvent).detail; + if (!files || files.length === 0) return; + handleFileSelect(files); + }); + if (clearBtn) { clearBtn.addEventListener('click', () => handleClear()); } } +/** + * 处理文件选择:逐个上传到 /attachment/upload,成功后同步到输入区附件预览 + */ +async function handleFileSelect(files: File[]): Promise { + if (!config) return; + + for (const file of files) { + try { + const att = await uploadAttachment(file); + pendingImages.push(att); + } catch (err) { + const msg = err instanceof CskError ? err.message : t('error_upload_failed'); + if (messagesContainer) renderErrorBubble(messagesContainer, msg, now()); + logger.error('图片上传失败', err); + } + } + + syncSenderAttachments(); +} + +/** + * 把 pendingImages 同步到 t-chat-sender 的 attachmentsProps.items, + * 由 TDesign Chat 组件原生渲染附件预览。 + */ +function syncSenderAttachments(): void { + if (!inputEl) return; + const items = pendingImages.map(img => ({ + name: img.name, + url: img.url, + fileType: 'image' as const, + size: img.size, + })); + (inputEl as unknown as { + attachmentsProps: { items: typeof items; overflow: string }; + }).attachmentsProps = { items, overflow: 'scrollX' }; +} + +/** 清空待发送图片(发送成功后、清空会话、切换角色/会话时调用) */ +function clearPendingImages(): void { + pendingImages = []; + syncSenderAttachments(); +} + /** 绑定滚动监听:判断是否在底部,控制新消息提示按钮 */ function bindScrollEvents(): void { if (!messagesContainer) return; @@ -404,16 +466,20 @@ function updateEmptyState(): void { } /** 处理发送消息(text 来自 t-chat-sender 的 send 事件,或快捷问题/重试的显式调用) */ -async function handleSend(text?: string): Promise { +async function handleSend(text?: string, images?: ImageAttachment[]): Promise { if (!config || isSending) return; const input = (text ?? '').trim(); - if (input === '') return; + const atts = images && images.length > 0 ? images : [...pendingImages]; + if (input === '' && atts.length === 0) return; + + // 清空待发送图片(快照已保存到 atts,发送失败时恢复) + clearPendingImages(); // 1. 渲染用户气泡 const userTimestamp = now(); - const userMsg: ChatMessage = { id: uuid(), role: 'user', content: input, timestamp: userTimestamp }; - if (messagesContainer) renderUserBubble(messagesContainer, input, userTimestamp); + const userMsg: ChatMessage = { id: uuid(), role: 'user', content: input, timestamp: userTimestamp, images: atts }; + if (messagesContainer) renderUserBubble(messagesContainer, input, userTimestamp, atts); messages.push(userMsg); updateEmptyState(); @@ -421,15 +487,21 @@ async function handleSend(text?: string): Promise { if (messagesContainer) smartScrollToBottom(); // 2. 生成 AI 回复 - await produceAIReply(input); + const ok = await produceAIReply(input, atts); + // 发送失败时恢复待发送图片,方便用户重试 + if (!ok && atts.length > 0) { + pendingImages = atts; + syncSenderAttachments(); + } } /** * 生成 AI 回复(发送请求 + 渲染气泡 + 持久化) * 复用于:正常发送、重试。调用方负责先渲染用户气泡。 + * @returns 是否发送成功(false 时调用方可恢复待发送图片) */ -async function produceAIReply(userText: string): Promise { - if (!config || !messagesContainer) return; +async function produceAIReply(userText: string, images?: ImageAttachment[]): Promise { + if (!config || !messagesContainer) return false; isSending = true; setSendButtonMode('stop'); @@ -439,6 +511,9 @@ async function produceAIReply(userText: string): Promise { await initChatId(); } + // 提取图片 URL,仅图片参与多模态对话 + const imageUrls = (images || []).map(img => img.url); + const aiTimestamp = now(); // RAG 启用条件:由 enableRag 控制 const shouldUseRag = useRag; @@ -452,9 +527,9 @@ async function produceAIReply(userText: string): Promise { try { if (config.streaming) { - aiContent = await sendStreamMessage(userText, aiTimestamp, shouldUseRag, aiMsgId); + aiContent = await sendStreamMessage(userText, aiTimestamp, shouldUseRag, aiMsgId, imageUrls); } else { - aiContent = await chatRequest(userText); + aiContent = await chatRequest(userText, imageUrls); if (hideLoadingFn) hideLoadingFn(); if (messagesContainer) { renderAIBubble(messagesContainer, aiContent, aiTimestamp, aiMsgId); @@ -484,6 +559,7 @@ async function produceAIReply(userText: string): Promise { // 发送成功后清除离线横幅(网络已恢复) hideOfflineBanner(); + return true; } catch (err) { if (hideLoadingFn) hideLoadingFn(); @@ -492,6 +568,7 @@ async function produceAIReply(userText: string): Promise { renderErrorBubble(messagesContainer, errMsg, now()); } logger.error(`发送失败 integrateId=${config.integrateId}`, err); + return false; } finally { isSending = false; abortController = null; @@ -500,7 +577,7 @@ async function produceAIReply(userText: string): Promise { } /** 流式发送消息 */ -async function sendStreamMessage(text: string, aiTimestamp: number, shouldUseRag: boolean, aiMsgId: string): Promise { +async function sendStreamMessage(text: string, aiTimestamp: number, shouldUseRag: boolean, aiMsgId: string, imageUrls?: string[]): Promise { // 创建中断控制器,供"停止生成"使用 abortController = new AbortController(); const signal = abortController.signal; @@ -535,7 +612,7 @@ async function sendStreamMessage(text: string, aiTimestamp: number, shouldUseRag // 无流内容降级为同步请求(须在 wrapperEl/bubbleEl 判断之外: // 二者仅在 onChunk 收到首个 token 时才赋值,否则此分支不可达) if (!streamStarted && accumulated === '') { - chatRequest(text).then(resolve).catch(reject); + chatRequest(text, imageUrls).then(resolve).catch(reject); return; } if (wrapperEl && bubbleEl) { @@ -563,6 +640,7 @@ async function sendStreamMessage(text: string, aiTimestamp: number, shouldUseRag }, currentCategoryId, shouldUseRag, + imageUrls, signal ); }); @@ -597,7 +675,8 @@ export async function retryFromMessage(msgId: string): Promise { } if (userIndex < 0) return; const userText = messages[userIndex].content; - if (!userText) return; + const userImages = messages[userIndex].images || []; + if (!userText && userImages.length === 0) return; // 计算 userTurn(1-based,仅统计 USER 消息):该轮提问是第几条用户消息 let userTurn = 0; @@ -623,7 +702,7 @@ export async function retryFromMessage(msgId: string): Promise { renderHistory(); // 4. 重新发送该轮提问(重新渲染用户气泡 + 生成新回复) - await handleSend(userText); + await handleSend(userText, userImages); } finally { isRetrying = false; } @@ -694,7 +773,7 @@ function renderHistory(): void { for (const msg of messages) { if (msg.role === 'user') { - renderUserBubble(messagesContainer, msg.content, msg.timestamp); + renderUserBubble(messagesContainer, msg.content, msg.timestamp, msg.images); } else { const wrapper = renderAIBubble(messagesContainer, msg.content, msg.timestamp, msg.id, msg.feedback); if (msg.sources && msg.sources.length > 0) renderSources(wrapper, msg.sources); @@ -731,6 +810,7 @@ function handleClear(): void { if (clearBtn) clearBtn.style.display = 'none'; updateEmptyState(); clearMessages(config.integrateId); + clearPendingImages(); // 生成新的 chatId,开始新会话 const newId = generateNewChatId(); @@ -789,6 +869,7 @@ export async function switchRole(newRoleId: string): Promise { messages = []; const msgNodes = messagesContainer.querySelectorAll('.csk-msg, .csk-loading'); msgNodes.forEach(el => el.remove()); + clearPendingImages(); // 5. 清空新旧角色的 localStorage 缓存(消息 + chatId),必须在 setActiveRoleId 之前 clearMessages(oldRoleId); @@ -928,6 +1009,7 @@ async function switchToConversation(conversationId: string): Promise { messages = []; const msgs = messagesContainer.querySelectorAll('.csk-msg, .csk-loading'); msgs.forEach(el => el.remove()); + clearPendingImages(); // 4. 从后端加载该会话的消息 try { diff --git a/client/src/config.ts b/client/src/config.ts index 76993d6..bdbfbd2 100644 --- a/client/src/config.ts +++ b/client/src/config.ts @@ -87,6 +87,7 @@ export function parseConfig(raw: SDKConfig): ResolvedConfig | null { rewriteStrategy: raw.rewriteStrategy || 'REWRITE', locale: raw.locale || 'zh-CN', debug: raw.debug ?? true, + allowImageUpload: raw.allowImageUpload ?? true, sound: raw.sound ?? false, notification: raw.notification ?? false, onError: typeof raw.onError === 'function' ? raw.onError : undefined, diff --git a/client/src/dom.ts b/client/src/dom.ts index 5a4f710..5a73cc2 100644 --- a/client/src/dom.ts +++ b/client/src/dom.ts @@ -7,7 +7,7 @@ * - 无消息时显示欢迎空状态 * - 输入区采用圆角容器包裹文本框与发送按钮 */ -import { ResolvedConfig, RagSource } from './types'; +import { ResolvedConfig, RagSource, ImageAttachment } from './types'; import { debounce, formatTime, formatHistoryTime } from './utils'; import { t } from './i18n'; @@ -352,6 +352,8 @@ export function createChatWindow(config: ResolvedConfig): { inputEl.setAttribute('placeholder', t('placeholder')); // 压缩输入框:autosize 为 Object 复杂类型,走 property 赋值(同 dom.ts createChatItemMsg 的 content 处理) (inputEl as unknown as { autosize: { minRows: number } }).autosize = { minRows: 1 }; + // 启用附件按钮 + 发送按钮(actions 为 Array 复杂类型,走 property 赋值) + (inputEl as unknown as { actions: string[] }).actions = config.allowImageUpload ? ['attachment', 'send'] : ['send']; inputArea.appendChild(inputEl); // === 保密声明脚注(默认折叠为单行,点击展开完整条文) === @@ -823,11 +825,31 @@ function createChatItemMsg(role: 'user' | 'assistant', content: unknown[], times return msg; } -export function renderUserBubble(container: HTMLElement, text: string, timestamp: number): HTMLElement { +export function renderUserBubble( + container: HTMLElement, + text: string, + timestamp: number, + images?: ImageAttachment[] +): HTMLElement { const wrapper = document.createElement('div'); wrapper.className = 'csk-msg csk-msg--user'; - wrapper.appendChild(createChatItemMsg('user', [{ type: 'text', data: text }], timestamp)); + // 组装 content:文本 + 附件(TDesign Chat 的 attachment content 类型,复用组件原生渲染) + const content: Array<{ type: string; data: unknown }> = []; + if (text) content.push({ type: 'text', data: text }); + if (images && images.length > 0) { + content.push({ + type: 'attachment', + data: images.map(img => ({ + name: img.name, + url: img.url, + fileType: 'image', + size: img.size, + })), + }); + } + + wrapper.appendChild(createChatItemMsg('user', content, timestamp)); container.appendChild(wrapper); diff --git a/client/src/i18n.ts b/client/src/i18n.ts index 8145b35..682c8cd 100644 --- a/client/src/i18n.ts +++ b/client/src/i18n.ts @@ -74,6 +74,7 @@ const dictionaries: Record> = { error_unavailable: '服务暂不可用,请稍后重试', error_unknown: '请求发生未知错误', error_send: '发送失败,请稍后重试', + error_upload_failed: '图片上传失败,请稍后重试', error_stream_unsupported: '浏览器不支持流式读取', }, @@ -147,6 +148,7 @@ const dictionaries: Record> = { error_unavailable: 'Service temporarily unavailable', error_unknown: 'Unknown request error', error_send: 'Failed to send, please try again', + error_upload_failed: 'Image upload failed, please try again', error_stream_unsupported: 'Browser does not support streaming', }, }; diff --git a/client/src/index.ts b/client/src/index.ts index 2116a4d..7cb9732 100644 --- a/client/src/index.ts +++ b/client/src/index.ts @@ -28,6 +28,7 @@ export type { SDKConfig, ResolvedConfig, ChatMessage, + ImageAttachment, RagSource, CategoryNode, ChatbotSDKInstance, diff --git a/client/src/types.ts b/client/src/types.ts index 43cc94c..6fc6d1f 100644 --- a/client/src/types.ts +++ b/client/src/types.ts @@ -83,6 +83,10 @@ export interface SDKConfig { /** 是否输出调试日志,默认 true */ debug?: boolean; + // === 图片上传配置 === + /** 是否允许上传图片参与对话,默认 true */ + allowImageUpload?: boolean; + // === 通知配置 === /** 弹窗关闭时收到新消息是否播放提示音,默认 false */ sound?: boolean; @@ -168,6 +172,20 @@ export interface ResolvedConfig { onMessage?: (msg: ChatMessage) => void; /** 当前对话 ID(自动管理,从 /conversation/list 获取或自动生成) */ chatId: string; + /** 是否允许上传图片参与对话 */ + allowImageUpload: boolean; +} + +/** 图片附件 */ +export interface ImageAttachment { + /** 文件名 */ + name: string; + /** 可访问的 URL */ + url: string; + /** MIME 类型 */ + mimeType?: string; + /** 文件大小(字节) */ + size?: number; } /** 聊天消息 */ @@ -180,6 +198,8 @@ export interface ChatMessage { content: string; /** 时间戳(毫秒) */ timestamp: number; + /** 可选:用户消息携带的图片 */ + images?: ImageAttachment[]; /** 可选:RAG 引用来源 */ sources?: RagSource[]; /** 可选:用户反馈 'up' | 'down',预留后端对接位 */ diff --git a/src/main/java/com/wok/supportbot/controller/AttachmentController.java b/src/main/java/com/wok/supportbot/controller/AttachmentController.java index 2b09ce1..fa8a21c 100644 --- a/src/main/java/com/wok/supportbot/controller/AttachmentController.java +++ b/src/main/java/com/wok/supportbot/controller/AttachmentController.java @@ -3,7 +3,6 @@ package com.wok.supportbot.controller; import com.wok.supportbot.config.StorageProperties; import com.wok.supportbot.service.SftpStorageService; import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -35,11 +34,14 @@ public class AttachmentController { /** * 上传图片/附件 * + *

鉴权由 {@code SdkAuthFilter} 承担(拦截 /attachment/upload,验证 SDK JWT 或管理后台 JWT), + * 与 /ai/** 接口一致,方法层不再叠加 @PreAuthorize(否则 SDK Token 会被 JwtAuthFilter 用管理后台 + * 密钥重复验证失败,导致 isAuthenticated() 判定为未认证)。 + * * @param file 上传的文件 * @return 上传结果:data 内含 url / name / type / mimeType / size */ @PostMapping("/upload") - @PreAuthorize("isAuthenticated()") public ResponseEntity> upload(@RequestParam("file") MultipartFile file) { try { validateFile(file); diff --git a/src/main/java/com/wok/supportbot/security/JwtAuthFilter.java b/src/main/java/com/wok/supportbot/security/JwtAuthFilter.java index e97007c..d9b2f0a 100644 --- a/src/main/java/com/wok/supportbot/security/JwtAuthFilter.java +++ b/src/main/java/com/wok/supportbot/security/JwtAuthFilter.java @@ -90,7 +90,9 @@ public class JwtAuthFilter extends OncePerRequestFilter { || path.startsWith("/assets/") || path.startsWith("/sdk/") || path.equals("/favicon.ico") - || path.equals("/favicon.svg"); + || path.equals("/favicon.svg") + || path.equals("/feedback") + || path.equals("/attachment/upload"); } /** diff --git a/src/main/java/com/wok/supportbot/security/SdkAuthFilter.java b/src/main/java/com/wok/supportbot/security/SdkAuthFilter.java index e25a934..ed85192 100644 --- a/src/main/java/com/wok/supportbot/security/SdkAuthFilter.java +++ b/src/main/java/com/wok/supportbot/security/SdkAuthFilter.java @@ -71,8 +71,8 @@ public class SdkAuthFilter extends OncePerRequestFilter { if (path.startsWith("/ai/system-config/")) { return true; } - // 拦截 /ai/ 路径和 /feedback 端点,排除静态资源和 swagger - return !path.startsWith("/ai/") && !path.equals("/feedback"); + // 拦截 /ai/ 路径、/feedback 端点,以及 SDK 上传接口 /attachment/upload,排除静态资源和 swagger + return !path.startsWith("/ai/") && !path.equals("/feedback") && !path.equals("/attachment/upload"); } @Override diff --git a/src/main/java/com/wok/supportbot/security/SecurityConfig.java b/src/main/java/com/wok/supportbot/security/SecurityConfig.java index ee8435e..eb018cb 100644 --- a/src/main/java/com/wok/supportbot/security/SecurityConfig.java +++ b/src/main/java/com/wok/supportbot/security/SecurityConfig.java @@ -67,6 +67,8 @@ public class SecurityConfig { .requestMatchers("/auth/login", "/auth/refresh", "/auth/logout").permitAll() // SDK 需要的接口(feedback、分类、会话查询等) .requestMatchers("/feedback").permitAll() + // 图片/附件上传:实际鉴权由 SdkAuthFilter 承担(与 /ai/** 一致) + .requestMatchers("/attachment/upload").permitAll() .requestMatchers("/category/tree", "/category/list").permitAll() // 系统配置(SDK 拉取声明内容) .requestMatchers("/ai/system-config/**").permitAll() diff --git a/src/main/resources/static/sdk/test.html b/src/main/resources/static/sdk/test.html index 3c5a2ea..e590a94 100644 --- a/src/main/resources/static/sdk/test.html +++ b/src/main/resources/static/sdk/test.html @@ -11,8 +11,8 @@ - +