Browse Source

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

Zhang Yice 1 month ago
parent
commit
f23f629b0d
3 changed files with 60 additions and 88 deletions
  1. 23 1
      src/components/SessionList/SessionList.tsx
  2. 12 1
      src/services/sessionService.ts
  3. 25 86
      src/stores/chatStore.ts

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

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

+ 12 - 1
src/services/sessionService.ts

@@ -54,7 +54,18 @@ export interface ListDocumentsResponse {
54
  * @returns 删除结果
54
  * @returns 删除结果
55
  */
55
  */
56
 export async function deleteSessionHistory(sessionId: string): Promise<DeleteSessionResponse> {
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
   return response.data;
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
    * @param sessionId - Session ID to delete
291
    * @param sessionId - Session ID to delete
292
    */
292
    */
293
   deleteSession: async (sessionId: string) => {
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