From 6bcd941cc618aaa7efeac930c9db1553b96f23db Mon Sep 17 00:00:00 2001 From: Vaayne Date: Tue, 23 Sep 2025 16:08:32 +0800 Subject: [PATCH] feat: Implement delete message functionality and validation in session messages --- .../routes/agents/handlers/messages.ts | 50 ++++++++++++++ src/main/apiServer/routes/agents/index.ts | 66 +++++++++++++++---- .../routes/agents/validators/messages.ts | 6 +- src/main/ipc.ts | 31 ++++++--- .../agents/services/SessionMessageService.ts | 12 +++- src/renderer/src/api/agent.ts | 10 +++ .../src/config/registry/messageMenubar.ts | 8 ++- src/renderer/src/types/agent.ts | 4 ++ 8 files changed, 162 insertions(+), 25 deletions(-) diff --git a/src/main/apiServer/routes/agents/handlers/messages.ts b/src/main/apiServer/routes/agents/handlers/messages.ts index 8f05bf8c0c..5f5bfc1466 100644 --- a/src/main/apiServer/routes/agents/handlers/messages.ts +++ b/src/main/apiServer/routes/agents/handlers/messages.ts @@ -228,3 +228,53 @@ export const createMessage = async (req: Request, res: Response): Promise res.end() } } + +export const deleteMessage = async (req: Request, res: Response): Promise => { + try { + const { agentId, sessionId, messageId: messageIdParam } = req.params + const messageId = Number(messageIdParam) + + await verifyAgentAndSession(agentId, sessionId) + + const deleted = await sessionMessageService.deleteSessionMessage(sessionId, messageId) + + if (!deleted) { + logger.warn(`Message ${messageId} not found for session ${sessionId}`) + return res.status(404).json({ + error: { + message: 'Message not found for this session', + type: 'not_found', + code: 'session_message_not_found' + } + }) + } + + logger.info(`Message ${messageId} deleted successfully for session ${sessionId}`) + return res.status(204).send() + } catch (error: any) { + if (error?.status === 404) { + logger.warn('Delete message failed - missing resource', { + agentId: req.params.agentId, + sessionId: req.params.sessionId, + messageId: req.params.messageId, + error + }) + return res.status(404).json({ + error: { + message: error.message, + type: 'not_found', + code: error.code ?? 'session_message_not_found' + } + }) + } + + logger.error('Error deleting session message:', error) + return res.status(500).json({ + error: { + message: 'Failed to delete session message', + type: 'internal_error', + code: 'session_message_delete_failed' + } + }) + } +} diff --git a/src/main/apiServer/routes/agents/index.ts b/src/main/apiServer/routes/agents/index.ts index 5d3393dc12..7a54f8acc6 100644 --- a/src/main/apiServer/routes/agents/index.ts +++ b/src/main/apiServer/routes/agents/index.ts @@ -11,6 +11,7 @@ import { validateSession, validateSessionId, validateSessionMessage, + validateSessionMessageId, validateSessionReplace, validateSessionUpdate } from './validators' @@ -362,7 +363,7 @@ const agentsRouter = express.Router() /** * @swagger - * /api/agents: + * /agents: * post: * summary: Create a new agent * tags: [Agents] @@ -391,7 +392,7 @@ agentsRouter.post('/', validateAgent, handleValidationErrors, agentHandlers.crea /** * @swagger - * /api/agents: + * /agents: * get: * summary: List all agents with pagination * tags: [Agents] @@ -429,7 +430,7 @@ agentsRouter.get('/', validatePagination, handleValidationErrors, agentHandlers. /** * @swagger - * /api/agents/{agentId}: + * /agents/{agentId}: * get: * summary: Get agent by ID * tags: [Agents] @@ -457,7 +458,7 @@ agentsRouter.get('/', validatePagination, handleValidationErrors, agentHandlers. agentsRouter.get('/:agentId', validateAgentId, handleValidationErrors, agentHandlers.getAgent) /** * @swagger - * /api/agents/{agentId}: + * /agents/{agentId}: * put: * summary: Replace agent (full update) * tags: [Agents] @@ -497,7 +498,7 @@ agentsRouter.get('/:agentId', validateAgentId, handleValidationErrors, agentHand agentsRouter.put('/:agentId', validateAgentId, validateAgentReplace, handleValidationErrors, agentHandlers.updateAgent) /** * @swagger - * /api/agents/{agentId}: + * /agents/{agentId}: * patch: * summary: Update agent (partial update) * tags: [Agents] @@ -537,7 +538,7 @@ agentsRouter.put('/:agentId', validateAgentId, validateAgentReplace, handleValid agentsRouter.patch('/:agentId', validateAgentId, validateAgentUpdate, handleValidationErrors, agentHandlers.patchAgent) /** * @swagger - * /api/agents/{agentId}: + * /agents/{agentId}: * delete: * summary: Delete agent * tags: [Agents] @@ -567,7 +568,7 @@ const createSessionsRouter = (): express.Router => { // Session CRUD routes (nested under agent) /** * @swagger - * /api/agents/{agentId}/sessions: + * /agents/{agentId}/sessions: * post: * summary: Create a new session for an agent * tags: [Sessions] @@ -608,7 +609,7 @@ const createSessionsRouter = (): express.Router => { /** * @swagger - * /api/agents/{agentId}/sessions: + * /agents/{agentId}/sessions: * get: * summary: List sessions for an agent * tags: [Sessions] @@ -657,7 +658,7 @@ const createSessionsRouter = (): express.Router => { sessionsRouter.get('/', validatePagination, handleValidationErrors, sessionHandlers.listSessions) /** * @swagger - * /api/agents/{agentId}/sessions/{sessionId}: + * /agents/{agentId}/sessions/{sessionId}: * get: * summary: Get session by ID * tags: [Sessions] @@ -691,7 +692,7 @@ const createSessionsRouter = (): express.Router => { sessionsRouter.get('/:sessionId', validateSessionId, handleValidationErrors, sessionHandlers.getSession) /** * @swagger - * /api/agents/{agentId}/sessions/{sessionId}: + * /agents/{agentId}/sessions/{sessionId}: * put: * summary: Replace session (full update) * tags: [Sessions] @@ -743,7 +744,7 @@ const createSessionsRouter = (): express.Router => { ) /** * @swagger - * /api/agents/{agentId}/sessions/{sessionId}: + * /agents/{agentId}/sessions/{sessionId}: * patch: * summary: Update session (partial update) * tags: [Sessions] @@ -795,7 +796,7 @@ const createSessionsRouter = (): express.Router => { ) /** * @swagger - * /api/agents/{agentId}/sessions/{sessionId}: + * /agents/{agentId}/sessions/{sessionId}: * delete: * summary: Delete session * tags: [Sessions] @@ -834,7 +835,7 @@ const createMessagesRouter = (): express.Router => { // Message CRUD routes (nested under agent/session) /** * @swagger - * /api/agents/{agentId}/sessions/{sessionId}/messages: + * /agents/{agentId}/sessions/{sessionId}/messages: * post: * summary: Create a new message in a session * tags: [Messages] @@ -902,8 +903,45 @@ const createMessagesRouter = (): express.Router => { * application/json: * schema: * $ref: '#/components/schemas/ErrorResponse' - */ + */ messagesRouter.post('/', validateSessionMessage, handleValidationErrors, messageHandlers.createMessage) + + /** + * @swagger + * /agents/{agentId}/sessions/{sessionId}/messages/{messageId}: + * delete: + * summary: Delete a message from a session + * tags: [Messages] + * parameters: + * - in: path + * name: agentId + * required: true + * schema: + * type: string + * description: Agent ID + * - in: path + * name: sessionId + * required: true + * schema: + * type: string + * description: Session ID + * - in: path + * name: messageId + * required: true + * schema: + * type: integer + * description: Message ID + * responses: + * 204: + * description: Message deleted successfully + * 404: + * description: Agent, session, or message not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ + messagesRouter.delete('/:messageId', validateSessionMessageId, handleValidationErrors, messageHandlers.deleteMessage) return messagesRouter } diff --git a/src/main/apiServer/routes/agents/validators/messages.ts b/src/main/apiServer/routes/agents/validators/messages.ts index 9a4f7dbbf0..8d7cddfa7b 100644 --- a/src/main/apiServer/routes/agents/validators/messages.ts +++ b/src/main/apiServer/routes/agents/validators/messages.ts @@ -1,7 +1,11 @@ -import { CreateSessionMessageRequestSchema } from '@types' +import { CreateSessionMessageRequestSchema, SessionMessageIdParamSchema } from '@types' import { createZodValidator } from './zodValidator' export const validateSessionMessage = createZodValidator({ body: CreateSessionMessageRequestSchema }) + +export const validateSessionMessageId = createZodValidator({ + params: SessionMessageIdParamSchema +}) diff --git a/src/main/ipc.ts b/src/main/ipc.ts index c375d86834..28097ad882 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -11,7 +11,16 @@ import { handleZoomFactor } from '@main/utils/zoom' import { SpanEntity, TokenUsage } from '@mcp-trace/trace-core' import { MIN_WINDOW_HEIGHT, MIN_WINDOW_WIDTH, UpgradeChannel } from '@shared/config/constant' import { IpcChannel } from '@shared/IpcChannel' -import { FileMetadata, Notification, OcrProvider, Provider, Shortcut, SupportedOcrFile, ThemeMode } from '@types' +import { + AgentPersistedMessage, + FileMetadata, + Notification, + OcrProvider, + Provider, + Shortcut, + SupportedOcrFile, + ThemeMode, +} from "@types"; import checkDiskSpace from 'check-disk-space' import { BrowserWindow, dialog, ipcMain, ProxyConfig, session, shell, systemPreferences, webContents } from 'electron' import fontList from 'font-list' @@ -209,14 +218,20 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { } }) - ipcMain.handle(IpcChannel.AgentMessage_GetHistory, async (_event, { sessionId }: { sessionId: string }) => { - try { - return await agentMessageRepository.getSessionHistory(sessionId) - } catch (error) { - logger.error('Failed to get agent session history', error as Error) - throw error + ipcMain.handle( + IpcChannel.AgentMessage_GetHistory, + async ( + _event, + { sessionId }: { sessionId: string } + ): Promise => { + try { + return await agentMessageRepository.getSessionHistory(sessionId); + } catch (error) { + logger.error("Failed to get agent session history", error as Error); + throw error; + } } - }) + ); //only for mac if (isMac) { diff --git a/src/main/services/agents/services/SessionMessageService.ts b/src/main/services/agents/services/SessionMessageService.ts index 9e0f3a73cb..0aa17425f0 100644 --- a/src/main/services/agents/services/SessionMessageService.ts +++ b/src/main/services/agents/services/SessionMessageService.ts @@ -6,7 +6,7 @@ import type { ListOptions } from '@types' import { TextStreamPart } from 'ai' -import { desc, eq } from 'drizzle-orm' +import { and, desc, eq } from 'drizzle-orm' import { BaseService } from '../BaseService' import { sessionMessagesTable } from '../database/schema' @@ -145,6 +145,16 @@ export class SessionMessageService extends BaseService { return { messages } } + async deleteSessionMessage(sessionId: string, messageId: number): Promise { + this.ensureInitialized() + + const result = await this.database + .delete(sessionMessagesTable) + .where(and(eq(sessionMessagesTable.id, messageId), eq(sessionMessagesTable.session_id, sessionId))) + + return result.rowsAffected > 0 + } + async createSessionMessage( session: GetAgentSessionResponse, messageData: CreateSessionMessageRequest, diff --git a/src/renderer/src/api/agent.ts b/src/renderer/src/api/agent.ts index b453f116cd..4d22b5a727 100644 --- a/src/renderer/src/api/agent.ts +++ b/src/renderer/src/api/agent.ts @@ -206,6 +206,16 @@ export class AgentApiClient { } } + public async deleteSessionMessage(agentId: string, sessionId: string, messageId: number): Promise { + const base = this.getSessionMessagesPath(agentId, sessionId) + const url = `${base}/${messageId}` + try { + await this.axios.delete(url) + } catch (error) { + throw processError(error, 'Failed to delete session message.') + } + } + public async updateSession(agentId: string, session: UpdateSessionForm): Promise { const url = this.getSessionPaths(agentId).withId(session.id) try { diff --git a/src/renderer/src/config/registry/messageMenubar.ts b/src/renderer/src/config/registry/messageMenubar.ts index 03fe0d9734..5f9f06802e 100644 --- a/src/renderer/src/config/registry/messageMenubar.ts +++ b/src/renderer/src/config/registry/messageMenubar.ts @@ -36,7 +36,13 @@ export const DEFAULT_MESSAGE_MENUBAR_BUTTON_IDS: MessageMenubarButtonId[] = [ 'more-menu' ] -export const SESSION_MESSAGE_MENUBAR_BUTTON_IDS: MessageMenubarButtonId[] = ['copy', 'translate', 'notes', 'more-menu'] +export const SESSION_MESSAGE_MENUBAR_BUTTON_IDS: MessageMenubarButtonId[] = [ + 'copy', + 'translate', + 'notes', + 'delete', + 'more-menu' +] const messageMenubarRegistry = new Map([ [DEFAULT_MESSAGE_MENUBAR_SCOPE, { buttonIds: [...DEFAULT_MESSAGE_MENUBAR_BUTTON_IDS] }], diff --git a/src/renderer/src/types/agent.ts b/src/renderer/src/types/agent.ts index af6350d488..09a237a373 100644 --- a/src/renderer/src/types/agent.ts +++ b/src/renderer/src/types/agent.ts @@ -267,6 +267,10 @@ export const SessionIdParamSchema = z.object({ sessionId: z.string().min(1, 'Session ID is required') }) +export const SessionMessageIdParamSchema = z.object({ + messageId: z.coerce.number().int().positive('Message ID must be a positive integer') +}) + // Query validation schemas export const PaginationQuerySchema = z.object({ limit: z.coerce.number().int().min(1).max(100).optional().default(20),