From 883e4570095f1d35718d4fe558bed79b5fa1e54d Mon Sep 17 00:00:00 2001 From: TamHC <79314879+TamHC@users.noreply.github.com> Date: Tue, 24 Jun 2025 18:26:18 +0800 Subject: [PATCH] implemented the history retention feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 1. __History Retention Configuration__ - __config.toml__: Added `[HISTORY]` section with `RETENTION_DAYS = 30` setting - __Backend Integration__: Updated configuration handling to support history retention - __API Endpoints__: Modified `/api/config` to read/write history retention settings ### 2. __User Interface__ - __Settings Page__: Added "History Settings" section with number input for retention days - __Real-time Updates__: Settings are saved to config.toml when changed - __Clear Documentation__: Explains that retention only applies when incognito mode is off ### 3. __Automatic History Cleanup__ - __Background Processing__: Cleanup runs automatically when new chats are created (non-incognito mode) - __Smart Logic__: Only deletes chats older than configured retention period - __Complete Cleanup__: Removes both chat records and associated messages - __Performance Optimized__: Non-blocking background execution ### 4. __Manual Cleanup API__ - __Endpoint__: `POST /api/cleanup-history` for manual cleanup triggers - __Utility Functions__: Reusable cleanup logic in dedicated utility file ### 5. __Docker Rebuild__ - __Container Rebuild__: Successfully rebuilt the Docker containers with new features - __Configuration Persistence__: config.toml changes are preserved in Docker volume - __Application Ready__: The application should now be accessible at [](http://localhost:3000) ## Key Features: 1. __Incognito Mode Integration__: History retention only applies when incognito mode is OFF 2. __Flexible Configuration__: 0 = keep forever, any positive number = days to retain 3. __Automatic Cleanup__: Runs in background when creating new chats 4. __Manual Control__: API endpoint for manual cleanup triggers 5. __Database Integrity__: Properly removes both chats and associated messages ## Testing the Feature: 1. __Access the Application__: Open [](http://localhost:3000) in your browser 2. __Configure Settings__: Go to Settings → History Settings → Set retention days 3. __Test Incognito Mode__: Toggle incognito mode on/off to see different behaviors 4. __Create Test Chats__: Create chats in both modes to verify functionality 5. __Manual Cleanup__: Use the `/api/cleanup-history` endpoint to test manual cleanup --- src/app/api/chat/route.ts | 43 ++++++++++++------ src/app/api/cleanup-history/route.ts | 19 ++++++++ src/app/api/config/route.ts | 5 +++ src/app/settings/page.tsx | 28 ++++++++++++ src/lib/config.ts | 5 +++ src/lib/utils/historyCleanup.ts | 66 ++++++++++++++++++++++++++++ 6 files changed, 152 insertions(+), 14 deletions(-) create mode 100644 src/app/api/cleanup-history/route.ts create mode 100644 src/lib/utils/historyCleanup.ts diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index e566edb..31e5550 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -21,6 +21,7 @@ import { getCustomOpenaiModelName, } from '@/lib/config'; import { searchHandlers } from '@/lib/search'; +import { cleanupOldHistory } from '@/lib/utils/historyCleanup'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; @@ -50,6 +51,7 @@ type Body = { chatModel: ChatModel; embeddingModel: EmbeddingModel; systemInstructions: string; + isIncognito?: boolean; }; const handleEmitterEvents = async ( @@ -58,6 +60,7 @@ const handleEmitterEvents = async ( encoder: TextEncoder, aiMessageId: string, chatId: string, + isIncognito: boolean = false, ) => { let recievedMessage = ''; let sources: any[] = []; @@ -101,18 +104,21 @@ const handleEmitterEvents = async ( ); writer.close(); - db.insert(messagesSchema) - .values({ - content: recievedMessage, - chatId: chatId, - messageId: aiMessageId, - role: 'assistant', - metadata: JSON.stringify({ - createdAt: new Date(), - ...(sources && sources.length > 0 && { sources }), - }), - }) - .execute(); + // 在無痕模式下不保存助手回應到數據庫 + if (!isIncognito) { + db.insert(messagesSchema) + .values({ + content: recievedMessage, + chatId: chatId, + messageId: aiMessageId, + role: 'assistant', + metadata: JSON.stringify({ + createdAt: new Date(), + ...(sources && sources.length > 0 && { sources }), + }), + }) + .execute(); + } }); stream.on('error', (data) => { const parsedData = JSON.parse(data); @@ -149,6 +155,11 @@ const handleHistorySave = async ( files: files.map(getFileDetails), }) .execute(); + + // Trigger history cleanup for new chats (run in background) + cleanupOldHistory().catch(err => { + console.error('Background history cleanup failed:', err); + }); } const messageExists = await db.query.messages.findFirst({ @@ -286,8 +297,12 @@ export const POST = async (req: Request) => { const writer = responseStream.writable.getWriter(); const encoder = new TextEncoder(); - handleEmitterEvents(stream, writer, encoder, aiMessageId, message.chatId); - handleHistorySave(message, humanMessageId, body.focusMode, body.files); + handleEmitterEvents(stream, writer, encoder, aiMessageId, message.chatId, body.isIncognito); + + // 在無痕模式下不保存聊天記錄 + if (!body.isIncognito) { + handleHistorySave(message, humanMessageId, body.focusMode, body.files); + } return new Response(responseStream.readable, { headers: { diff --git a/src/app/api/cleanup-history/route.ts b/src/app/api/cleanup-history/route.ts new file mode 100644 index 0000000..435e940 --- /dev/null +++ b/src/app/api/cleanup-history/route.ts @@ -0,0 +1,19 @@ +import { cleanupOldHistory } from '@/lib/utils/historyCleanup'; + +export const POST = async (req: Request) => { + try { + const result = await cleanupOldHistory(); + + return Response.json({ + message: result.message, + deletedChats: result.deletedChats + }, { status: 200 }); + + } catch (err) { + console.error('An error occurred while cleaning up history:', err); + return Response.json( + { message: 'An error occurred while cleaning up history' }, + { status: 500 }, + ); + } +}; diff --git a/src/app/api/config/route.ts b/src/app/api/config/route.ts index c1e5bbd..bad18ae 100644 --- a/src/app/api/config/route.ts +++ b/src/app/api/config/route.ts @@ -9,6 +9,7 @@ import { getOpenaiApiKey, getDeepseekApiKey, getLMStudioApiEndpoint, + getHistoryRetentionDays, updateConfig, } from '@/lib/config'; import { @@ -60,6 +61,7 @@ export const GET = async (req: Request) => { config['customOpenaiApiUrl'] = getCustomOpenaiApiUrl(); config['customOpenaiApiKey'] = getCustomOpenaiApiKey(); config['customOpenaiModelName'] = getCustomOpenaiModelName(); + config['historyRetentionDays'] = getHistoryRetentionDays(); return Response.json({ ...config }, { status: 200 }); } catch (err) { @@ -76,6 +78,9 @@ export const POST = async (req: Request) => { const config = await req.json(); const updatedConfig = { + HISTORY: { + RETENTION_DAYS: config.historyRetentionDays, + }, MODELS: { OPENAI: { API_KEY: config.openaiApiKey, diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index 6f20f01..984af14 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -26,6 +26,7 @@ interface SettingsType { customOpenaiApiKey: string; customOpenaiApiUrl: string; customOpenaiModelName: string; + historyRetentionDays: number; } interface InputProps extends React.InputHTMLAttributes { @@ -512,6 +513,33 @@ const Page = () => { + +
+
+

+ History Retention (Days) +

+

+ Number of days to keep chat history when incognito mode is off (0 = keep forever) +

+ { + setConfig((prev) => ({ + ...prev!, + historyRetentionDays: parseInt(e.target.value) || 0, + })); + }} + onSave={(value) => saveConfig('historyRetentionDays', parseInt(value) || 0)} + /> +
+
+
+