Pārlūkot izejas kodu

feat(会话管理): 增强会话删除功能,添加删除状态管理与安全性检查

Zhang Yice 1 mēnesi atpakaļ
vecāks
revīzija
f23f629b0d

+ 23 - 1
src/components/SessionList/SessionList.tsx

@@ -159,6 +159,7 @@ const SessionList: React.FC<SessionListProps> = ({ onSessionLoad }) => {
159 159
   // Batch selection state
160 160
   const [isSelectionMode, setIsSelectionMode] = useState(false);
161 161
   const [selectedSessionIds, setSelectedSessionIds] = useState<Set<string>>(new Set());
162
+  const [deletingSessionIds, setDeletingSessionIds] = useState<Set<string>>(new Set());
162 163
 
163 164
   /**
164 165
    * Handle creating a new session
@@ -187,6 +188,9 @@ const SessionList: React.FC<SessionListProps> = ({ onSessionLoad }) => {
187 188
   const handleDeleteSession = async (sessionId: string, e: React.MouseEvent) => {
188 189
     // Stop propagation to prevent loading the session
189 190
     e.stopPropagation();
191
+    if (deletingSessionIds.has(sessionId)) return;
192
+
193
+    setDeletingSessionIds((current) => new Set(current).add(sessionId));
190 194
 
191 195
     try {
192 196
       // 调用 chatStore 的 deleteSession (内部会调用后端 API)
@@ -195,6 +199,12 @@ const SessionList: React.FC<SessionListProps> = ({ onSessionLoad }) => {
195 199
       antdMessage.success('成功删除会话及其关联的所有文档');
196 200
     } catch {
197 201
       antdMessage.error('删除会话失败,请重试');
202
+    } finally {
203
+      setDeletingSessionIds((current) => {
204
+        const next = new Set(current);
205
+        next.delete(sessionId);
206
+        return next;
207
+      });
198 208
     }
199 209
   };
200 210
 
@@ -239,12 +249,15 @@ const SessionList: React.FC<SessionListProps> = ({ onSessionLoad }) => {
239 249
       return;
240 250
     }
241 251
 
252
+    const sessionIds = Array.from(selectedSessionIds);
253
+    setDeletingSessionIds(new Set(sessionIds));
254
+
242 255
     try {
243 256
       let successCount = 0;
244 257
       let failedCount = 0;
245 258
 
246 259
       // 逐个调用 deleteSession (内部会调用后端 API)
247
-      for (const sessionId of Array.from(selectedSessionIds)) {
260
+      for (const sessionId of sessionIds) {
248 261
         try {
249 262
           await deleteSession(sessionId);
250 263
           successCount++;
@@ -264,6 +277,12 @@ const SessionList: React.FC<SessionListProps> = ({ onSessionLoad }) => {
264 277
       }
265 278
     } catch {
266 279
       antdMessage.error('批量删除失败,请重试');
280
+    } finally {
281
+      setDeletingSessionIds((current) => {
282
+        const next = new Set(current);
283
+        sessionIds.forEach((sessionId) => next.delete(sessionId));
284
+        return next;
285
+      });
267 286
     }
268 287
   };
269 288
 
@@ -275,6 +294,7 @@ const SessionList: React.FC<SessionListProps> = ({ onSessionLoad }) => {
275 294
     const messageCount = session.messages.length;
276 295
     const exportCount = session.exportRecords.length;
277 296
     const isSelected = selectedSessionIds.has(session.id);
297
+    const isDeleting = deletingSessionIds.has(session.id);
278 298
 
279 299
     return (
280 300
       <List.Item
@@ -329,6 +349,8 @@ const SessionList: React.FC<SessionListProps> = ({ onSessionLoad }) => {
329 349
               onClick={(e) => e.stopPropagation()}
330 350
               style={{ marginRight: '8px' }}
331 351
               aria-label="删除会话"
352
+              loading={isDeleting}
353
+              disabled={isDeleting}
332 354
             />
333 355
           </Popconfirm>
334 356
         )}

+ 12 - 1
src/services/sessionService.ts

@@ -54,7 +54,18 @@ export interface ListDocumentsResponse {
54 54
  * @returns 删除结果
55 55
  */
56 56
 export async function deleteSessionHistory(sessionId: string): Promise<DeleteSessionResponse> {
57
-  const response = await apiClient.delete<DeleteSessionResponse>(`/api/v1/documents/${sessionId}`);
57
+  const hasUnsafeCharacter = Array.from(sessionId).some((character) => {
58
+    const codePoint = character.codePointAt(0) ?? 0;
59
+    return codePoint < 32 || codePoint === 127 || character === '/' || character === '\\';
60
+  });
61
+
62
+  if (!sessionId || sessionId.length > 128 || hasUnsafeCharacter) {
63
+    throw new Error('无效的会话 ID');
64
+  }
65
+
66
+  const response = await apiClient.delete<DeleteSessionResponse>(
67
+    `/api/v1/documents/${encodeURIComponent(sessionId)}`
68
+  );
58 69
   return response.data;
59 70
 }
60 71
 

+ 25 - 86
src/stores/chatStore.ts

@@ -291,96 +291,35 @@ export const useChatStore = create<ChatStoreState>((set, get) => ({
291 291
    * @param sessionId - Session ID to delete
292 292
    */
293 293
   deleteSession: async (sessionId: string) => {
294
-    try {
295
-      // ⭐ 在删除前先获取会话信息,用于后续判断是否需要关闭编辑器
296
-      const sessionToDelete = get().sessions.find((s) => s.id === sessionId);
297
-
298
-      // 调用后端 API 删除会话的所有文档
299
-      const { deleteSessionHistory } = await import('../services/sessionService');
300
-      await deleteSessionHistory(sessionId);
301
-
302
-      // 删除成功后,从本地状态中移除
303
-      set((state) => {
304
-        const updatedSessions = state.sessions.filter((s) => s.id !== sessionId);
305
-        saveSessionsToStorage(updatedSessions);
306
-
307
-        // If deleting current session, clear current session
308
-        const newCurrentSessionId =
309
-          state.currentSessionId === sessionId ? null : state.currentSessionId;
310
-
311
-        return {
312
-          currentSessionId: newCurrentSessionId,
313
-          sessions: updatedSessions,
314
-          messages: newCurrentSessionId === null ? [] : state.messages,
315
-        };
316
-      });
317
-
318
-      // ⭐ 新增逻辑: 检查编辑器中是否打开了该会话的文档
319
-      // 如果是,关闭编辑器并返回导出记录面板
320
-      if (sessionToDelete) {
321
-        const { useEditorStore } = await import('./editorStore');
322
-        const { useUIStore } = await import('./uiStore');
323
-        const editorState = useEditorStore.getState();
324
-        const uiState = useUIStore.getState();
325
-
326
-        // 检查当前打开的文档是否属于被删除的会话
327
-        // 通过检查 documentId 是否在被删除会话的 exportRecords 中
328
-        if (editorState.documentId) {
329
-          const isDocumentFromSession = sessionToDelete.exportRecords.some(
330
-            (record) => record.documentId === editorState.documentId
331
-          );
332
-
333
-          if (isDocumentFromSession) {
334
-            // 1. 关闭编辑器状态
335
-            editorState.reset();
336
-            // 2. 关闭文档预览,返回导出记录面板
337
-            uiState.closeDocumentPreview();
338
-            message.info('已关闭编辑器中的文档');
339
-          }
340
-        }
341
-      }
342
-    } catch (error) {
343
-      // ⭐ 在删除前先获取会话信息
344
-      const sessionToDelete = get().sessions.find((s) => s.id === sessionId);
294
+    const sessionToDelete = get().sessions.find((s) => s.id === sessionId);
295
+    if (!sessionToDelete) return;
345 296
 
346
-      // 即使后端删除失败,也从本地状态中移除(因为可能是网络问题)
347
-      set((state) => {
348
-        const updatedSessions = state.sessions.filter((s) => s.id !== sessionId);
349
-        saveSessionsToStorage(updatedSessions);
297
+    const { deleteSessionHistory } = await import('../services/sessionService');
298
+    await deleteSessionHistory(sessionId);
350 299
 
351
-        const newCurrentSessionId =
352
-          state.currentSessionId === sessionId ? null : state.currentSessionId;
353
-
354
-        return {
355
-          currentSessionId: newCurrentSessionId,
356
-          sessions: updatedSessions,
357
-          messages: newCurrentSessionId === null ? [] : state.messages,
358
-        };
359
-      });
360
-
361
-      // ⭐ 错误情况下也需要关闭编辑器并返回导出记录面板
362
-      if (sessionToDelete) {
363
-        const { useEditorStore } = await import('./editorStore');
364
-        const { useUIStore } = await import('./uiStore');
365
-        const editorState = useEditorStore.getState();
366
-        const uiState = useUIStore.getState();
367
-
368
-        if (editorState.documentId) {
369
-          const isDocumentFromSession = sessionToDelete.exportRecords.some(
370
-            (record) => record.documentId === editorState.documentId
371
-          );
300
+    set((state) => {
301
+      const updatedSessions = state.sessions.filter((s) => s.id !== sessionId);
302
+      saveSessionsToStorage(updatedSessions);
303
+      const newCurrentSessionId =
304
+        state.currentSessionId === sessionId ? null : state.currentSessionId;
372 305
 
373
-          if (isDocumentFromSession) {
374
-            // 1. 关闭编辑器状态
375
-            editorState.reset();
376
-            // 2. 关闭文档预览,返回导出记录面板
377
-            uiState.closeDocumentPreview();
378
-            message.info('已关闭编辑器中的文档');
379
-          }
380
-        }
381
-      }
306
+      return {
307
+        currentSessionId: newCurrentSessionId,
308
+        sessions: updatedSessions,
309
+        messages: newCurrentSessionId === null ? [] : state.messages,
310
+      };
311
+    });
382 312
 
383
-      throw error; // 重新抛出错误供调用者处理
313
+    const { useEditorStore } = await import('./editorStore');
314
+    const { useUIStore } = await import('./uiStore');
315
+    const editorState = useEditorStore.getState();
316
+    if (
317
+      editorState.documentId &&
318
+      sessionToDelete.exportRecords.some((record) => record.documentId === editorState.documentId)
319
+    ) {
320
+      editorState.reset();
321
+      useUIStore.getState().closeDocumentPreview();
322
+      message.info('已关闭编辑器中的文档');
384 323
     }
385 324
   },
386 325