feat: implement session deletion protection and enhance chat history management

This commit is contained in:
duanshuwen
2026-04-22 22:53:22 +08:00
parent d915fcd61a
commit bbe32f7954
7 changed files with 44 additions and 16 deletions

View File

@@ -1,6 +1,6 @@
"use strict"; "use strict";
require("electron"); require("electron");
require("./main-DfEXs_ww.js"); require("./main-CDQKuYIF.js");
require("electron-squirrel-startup"); require("electron-squirrel-startup");
require("electron-log"); require("electron-log");
require("bytenode"); require("bytenode");

View File

@@ -798,6 +798,10 @@ class GatewayManager {
} }
case 'session.delete': { case 'session.delete': {
const request = params as GatewayRpcParams['session.delete']; const request = params as GatewayRpcParams['session.delete'];
if (normalizeAgentSessionKey(request.sessionKey).endsWith(':main')) {
return { success: false };
}
await this.rpcGateway('sessions.delete', { await this.rpcGateway('sessions.delete', {
key: normalizeAgentSessionKey(request.sessionKey), key: normalizeAgentSessionKey(request.sessionKey),
deleteTranscript: true, deleteTranscript: true,

View File

@@ -206,6 +206,7 @@ function ChatHistoryPanel({
{bucket.sessions.map((session) => { {bucket.sessions.map((session) => {
const isActive = session.conversationId === selectedConversationId; const isActive = session.conversationId === selectedConversationId;
const isMenuOpen = menuState?.conversationId === session.conversationId; const isMenuOpen = menuState?.conversationId === session.conversationId;
const canDelete = Boolean(onDeleteConversation) && session.canDelete !== false;
return ( return (
<li key={session.conversationId}> <li key={session.conversationId}>
@@ -273,19 +274,20 @@ function ChatHistoryPanel({
<PencilLine className="h-4 w-4 text-[#94a3b8]" /> <PencilLine className="h-4 w-4 text-[#94a3b8]" />
{t('conversation.historyPanel.rename')} {t('conversation.historyPanel.rename')}
</button> </button>
<button {canDelete ? (
type="button" <button
className="flex w-full items-center gap-2 border-t border-[#eef3f9] px-3 py-2.5 text-left text-sm text-[#ef4444] transition-colors hover:bg-[#fff5f5] disabled:cursor-not-allowed disabled:text-[#f5a2a2] dark:border-[#2f3136] dark:hover:bg-[#2a2a2d]" type="button"
disabled={!onDeleteConversation} className="flex w-full items-center gap-2 border-t border-[#eef3f9] px-3 py-2.5 text-left text-sm text-[#ef4444] transition-colors hover:bg-[#fff5f5] disabled:cursor-not-allowed disabled:text-[#f5a2a2] dark:border-[#2f3136] dark:hover:bg-[#2a2a2d]"
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
setMenuState(null); setMenuState(null);
onDeleteConversation?.(session.conversationId); onDeleteConversation?.(session.conversationId);
}} }}
> >
<Trash2 className="h-4 w-4" /> <Trash2 className="h-4 w-4" />
{t('conversation.historyPanel.delete')} {t('conversation.historyPanel.delete')}
</button> </button>
) : null}
</div> </div>
) : null} ) : null}
</div> </div>

View File

@@ -101,8 +101,8 @@ function ChatMessageList({ messages, loading, showWelcomeState }: ChatMessageLis
}, [loading, messages]); }, [loading, messages]);
return ( return (
<div className="flex min-h-0 flex-1 flex-col overflow-hidden px-4 py-6 dark:bg-[#161618] sm:px-6"> <div className="flex min-h-0 flex-1 flex-col overflow-hidden p-4 dark:bg-[#161618] sm:px-6">
<div ref={containerRef} className="mx-auto flex min-h-0 w-full flex-1 flex-col gap-5 overflow-y-auto pr-1"> <div ref={containerRef} className="mx-auto flex min-h-0 w-full flex-1 flex-col gap-5 overflow-y-auto pr-2">
{loading ? ( {loading ? (
<div className="w-full rounded-[18px] border border-dashed border-[#DDD3C3] bg-[#EFE7D8] px-4 py-3 text-sm text-[#645C50] dark:border-white/10 dark:bg-white/5 dark:text-gray-400"> <div className="w-full rounded-[18px] border border-dashed border-[#DDD3C3] bg-[#EFE7D8] px-4 py-3 text-sm text-[#645C50] dark:border-white/10 dark:bg-white/5 dark:text-gray-400">
{t('conversation.messageList.loading')} {t('conversation.messageList.loading')}

View File

@@ -9,6 +9,7 @@ export type ChatHistoryBucket = {
conversationId: string; conversationId: string;
title: string; title: string;
updatedAt: string; updatedAt: string;
canDelete?: boolean;
}>; }>;
}; };

View File

@@ -120,6 +120,10 @@ function getTaskRoomTypeLabel(task: { roomType: string; roomList: Array<{ id?: s
return matchedRoom?.pmsName || task.roomType; return matchedRoom?.pmsName || task.roomType;
} }
function isProtectedMainSession(sessionKey: string): boolean {
return sessionKey.endsWith(':main');
}
function mapMessages( function mapMessages(
messages: RawMessage[], messages: RawMessage[],
streamingMessage: RawMessage | null, streamingMessage: RawMessage | null,
@@ -281,6 +285,7 @@ export default function HomePage() {
conversationId: session.key, conversationId: session.key,
title: chatSessionLabels[session.key] || session.displayName || session.key, title: chatSessionLabels[session.key] || session.displayName || session.key,
updatedAt: formatHistoryTime(locale, t, activityMs, currentMs), updatedAt: formatHistoryTime(locale, t, activityMs, currentMs),
canDelete: !isProtectedMainSession(session.key),
}); });
} }
@@ -421,6 +426,10 @@ export default function HomePage() {
} }
function handleDeleteConversation(conversationId: string): void { function handleDeleteConversation(conversationId: string): void {
if (isProtectedMainSession(conversationId)) {
return;
}
const nextTitle = chatSessionLabels[conversationId] const nextTitle = chatSessionLabels[conversationId]
|| chatSessions.find((session) => session.key === conversationId)?.displayName || chatSessions.find((session) => session.key === conversationId)?.displayName
|| t('conversation.untitledConversation'); || t('conversation.untitledConversation');
@@ -433,6 +442,10 @@ export default function HomePage() {
async function handleConfirmDeleteConversation(): Promise<void> { async function handleConfirmDeleteConversation(): Promise<void> {
if (!deleteConversationTarget) return; if (!deleteConversationTarget) return;
if (isProtectedMainSession(deleteConversationTarget.conversationId)) {
setDeleteConversationTarget(null);
return;
}
setDeletingConversation(true); setDeletingConversation(true);
try { try {

View File

@@ -197,6 +197,10 @@ function buildNewSessionKey(agentId: string | null | undefined): string {
return buildAgentSessionKey(agentId || getDefaultAgentId(), `session-${Date.now()}`); return buildAgentSessionKey(agentId || getDefaultAgentId(), `session-${Date.now()}`);
} }
function isProtectedMainSession(sessionKey: string): boolean {
return normalizeAgentSessionKey(sessionKey).endsWith(':main');
}
function clearSessionEntryFromMap<T extends Record<string, unknown>>(entries: T, sessionKey: string): T { function clearSessionEntryFromMap<T extends Record<string, unknown>>(entries: T, sessionKey: string): T {
return Object.fromEntries(Object.entries(entries).filter(([key]) => key !== sessionKey)) as T; return Object.fromEntries(Object.entries(entries).filter(([key]) => key !== sessionKey)) as T;
} }
@@ -671,6 +675,10 @@ function selectAgent(agentId: string): void {
} }
async function deleteSession(sessionKey: string): Promise<void> { async function deleteSession(sessionKey: string): Promise<void> {
if (isProtectedMainSession(sessionKey)) {
return;
}
const currentState = state; const currentState = state;
const isDeletingCurrentSession = sessionKey === currentState.currentSessionKey; const isDeletingCurrentSession = sessionKey === currentState.currentSessionKey;