diff --git a/electron/main/ai/agent/index.ts b/electron/main/ai/agent/index.ts index 21ede61c..1f521872 100644 --- a/electron/main/ai/agent/index.ts +++ b/electron/main/ai/agent/index.ts @@ -6,7 +6,7 @@ import { getDefaultAssistantConfig, buildPiModel } from '../llm' import { getAllTools, createActivateSkillTool } from '../tools' import type { ToolContext } from '../tools/types' -import { getHistoryForAgent } from '../conversations' +import { getHistoryForAgent, setPendingDebugContext } from '../conversations' import { aiLogger, isDebugMode } from '../logger' import { t as i18nT } from '../../i18n' import { Agent as PiAgentCore } from '@mariozechner/pi-agent-core' @@ -23,7 +23,7 @@ import { buildSystemPrompt } from './prompt-builder' import { extractThinkingContent, stripToolCallTags } from './content-parser' import { AgentEventHandler } from './event-handler' -type SimpleHistoryMessage = { role: 'user' | 'assistant' | 'system'; content: string } +type SimpleHistoryMessage = { role: 'user' | 'assistant' | 'summary'; content: string } // Re-export types for external consumers export type { AgentConfig, AgentStreamChunk, AgentResult, TokenUsage, AgentRuntimeStatus, SkillContext } from './types' @@ -194,7 +194,22 @@ export class Agent { try { if (isDebugMode()) { - aiLogger.debug('Agent', `[DEBUG] System prompt`, systemPrompt) + // 捕获发给 LLM 的完整上下文(system prompt + history + user message),写入 DB + if (this.context.conversationId) { + try { + const debugMessages = [ + { role: 'system', content: systemPrompt }, + ...historyMessages.map((m) => ({ + role: m.role === 'summary' ? 'assistant' : m.role, + content: m.content, + })), + { role: 'user', content: userMessage }, + ] + setPendingDebugContext(this.context.conversationId, JSON.stringify(debugMessages, null, 2)) + } catch { + // 静默失败,不影响主流程 + } + } } await coreAgent.prompt(userMessage) diff --git a/electron/main/ai/compression/index.ts b/electron/main/ai/compression/index.ts index 4ea32233..6ee0bd6f 100644 --- a/electron/main/ai/compression/index.ts +++ b/electron/main/ai/compression/index.ts @@ -6,7 +6,7 @@ * 1. 计算当前上下文总 token → 未超阈值则跳过 * 2. 确定缓冲区:最近 bufferSizePercent% context window 的消息原文 * 3. 缓冲区之前的消息(含旧 system 摘要)→ LLM 压缩为新摘要 - * 4. 写入 ai_message(role='system'),替换旧摘要 + * 4. 写入 ai_message(role='summary'),替换旧摘要 * 5. Thrashing 检查 */ @@ -17,6 +17,7 @@ import { getAllUserAssistantMessages, addSummaryMessage, getMessageCountAfterSummary, + setDebugContext, type ContentBlock, type AIMessageRole, } from '../conversations' @@ -179,30 +180,6 @@ export async function checkAndCompress( inputTokensEstimate: countTokens(compressInput), }) - // DEBUG 模式下输出原始消息列表和完整压缩输入 - if (isDebugMode()) { - aiLogger.debug('Compression', 'Messages to compress (raw)', { - messages: messagesToCompress.map((m, i) => ({ - index: i, - role: m.role, - contentLength: m.content.length, - contentPreview: m.content.slice(0, 200), - })), - }) - aiLogger.debug('Compression', 'Buffer messages (kept as-is)', { - messages: bufferMessages.map((m, i) => ({ - index: i, - role: m.role, - contentLength: m.content.length, - contentPreview: m.content.slice(0, 200), - })), - }) - aiLogger.debug('Compression', 'Full compression input sent to LLM', compressInput) - if (summary) { - aiLogger.debug('Compression', 'Previous summary content', summary.content) - } - } - // 使用默认助手模型压缩,失败则强制截断 let summaryText: string | null = null @@ -219,9 +196,6 @@ export async function checkAndCompress( outputLength: summaryText.length, outputTokensEstimate: countTokens(summaryText), }) - if (isDebugMode()) { - aiLogger.debug('Compression', 'Generated summary content', summaryText) - } } // 写入 summary:时间戳 = NOW(UI 中显示在触发压缩的位置) @@ -242,7 +216,19 @@ export async function checkAndCompress( bufferCount: bufferMessages.length, }) - addSummaryMessage(conversationId, summaryText, summaryMeta) + const summaryMsg = addSummaryMessage(conversationId, summaryText, summaryMeta) + + // DEBUG 模式:记录发给压缩 LLM 的完整上下文(与 assistant 的 debug_context 格式一致) + if (isDebugMode()) { + try { + const template = isProgressive ? PROGRESSIVE_COMPRESSION_PROMPT : INITIAL_COMPRESSION_PROMPT + const fullPrompt = template.replace('{maxTokens}', String(targetTokens)).replace('{messages}', compressInput) + const debugMessages = [{ role: 'user', content: fullPrompt }] + setDebugContext(summaryMsg.id, JSON.stringify(debugMessages, null, 2)) + } catch { + // 静默失败,不影响主流程 + } + } // Thrashing 检查:压缩后重新计算 token(summary + buffer 消息) const afterTokenCount: Array<{ role: string; content: string }> = [ diff --git a/electron/main/ai/conversations.ts b/electron/main/ai/conversations.ts index 044c28f5..0372a89a 100644 --- a/electron/main/ai/conversations.ts +++ b/electron/main/ai/conversations.ts @@ -13,6 +13,9 @@ const DEFAULT_GENERAL_ID = 'general_cn' // AI 数据库实例 let AI_DB: Database.Database | null = null +// DEBUG 模式:暂存待写入的 debug context(key = conversationId) +const pendingDebugContextMap = new Map() + /** * 获取 AI 数据库目录 */ @@ -94,6 +97,12 @@ function migrateAiDatabase(db: Database.Database): void { console.log('[AI DB Migration] Adding token_usage column to ai_message') } + // 检查并添加 debug_context 列(仅 DEBUG 模式下写入,存储发给 LLM 的完整上下文) + if (!messageColumns.includes('debug_context')) { + db.exec('ALTER TABLE ai_message ADD COLUMN debug_context TEXT') + console.log('[AI DB Migration] Adding debug_context column to ai_message') + } + // 获取 ai_conversation 表的列信息 const convTableInfo = db.pragma('table_info(ai_conversation)') as Array<{ name: string }> const convColumns = convTableInfo.map((col) => col.name) @@ -230,7 +239,7 @@ export type ContentBlock = /** * AI 消息类型 */ -export type AIMessageRole = 'user' | 'assistant' | 'system' +export type AIMessageRole = 'user' | 'assistant' | 'summary' export interface TokenUsageData { promptTokens: number @@ -389,10 +398,16 @@ export function addMessage( const now = Math.floor(Date.now() / 1000) const id = `msg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}` + // 检查是否有暂存的 debug context 需要写入(仅 assistant 消息) + const pendingDebug = role === 'assistant' ? pendingDebugContextMap.get(conversationId) : undefined + if (pendingDebug) { + pendingDebugContextMap.delete(conversationId) + } + db.prepare( ` - INSERT INTO ai_message (id, conversation_id, role, content, timestamp, data_keywords, data_message_count, content_blocks, token_usage) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO ai_message (id, conversation_id, role, content, timestamp, data_keywords, data_message_count, content_blocks, token_usage, debug_context) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ` ).run( id, @@ -403,7 +418,8 @@ export function addMessage( dataKeywords ? JSON.stringify(dataKeywords) : null, dataMessageCount ?? null, contentBlocks ? JSON.stringify(contentBlocks) : null, - tokenUsage ? JSON.stringify(tokenUsage) : null + tokenUsage ? JSON.stringify(tokenUsage) : null, + pendingDebug ?? null ) // 更新对话的 updated_at @@ -480,6 +496,30 @@ export function deleteMessage(messageId: string): boolean { return result.changes > 0 } +/** + * 暂存 debug context,等待下一次 addMessage(assistant) 时自动写入 + */ +export function setPendingDebugContext(conversationId: string, debugContext: string): void { + pendingDebugContextMap.set(conversationId, debugContext) +} + +/** + * 更新消息的 debug_context 字段(仅 DEBUG 模式下调用) + */ +export function setDebugContext(messageId: string, debugContext: string): void { + const db = getAiDb() + db.prepare('UPDATE ai_message SET debug_context = ? WHERE id = ?').run(debugContext, messageId) +} + +/** + * 一键清除所有消息的 debug_context 数据 + */ +export function clearAllDebugContext(): number { + const db = getAiDb() + const result = db.prepare('UPDATE ai_message SET debug_context = NULL WHERE debug_context IS NOT NULL').run() + return result.changes +} + /** * 获取对话的累计 token 使用量(聚合所有 assistant 消息的 token_usage) */ @@ -520,44 +560,44 @@ export function getConversationTokenUsage(conversationId: string): TokenUsageDat export function getHistoryForAgent( conversationId: string, maxMessages?: number -): Array<{ role: 'user' | 'assistant' | 'system'; content: string }> { +): Array<{ role: 'user' | 'assistant' | 'summary'; content: string }> { const messages = getMessages(conversationId) const validMessages = messages.filter( - (m) => (m.role === 'user' || m.role === 'assistant' || m.role === 'system') && m.content?.trim() + (m) => (m.role === 'user' || m.role === 'assistant' || m.role === 'summary') && m.content?.trim() ) - // 查找最新的 system (summary) 消息 - let systemMsg: AIMessage | undefined + // 查找最新的 summary 消息 + let summaryMsg: AIMessage | undefined for (let i = validMessages.length - 1; i >= 0; i--) { - if (validMessages[i].role === 'system') { - systemMsg = validMessages[i] + if (validMessages[i].role === 'summary') { + summaryMsg = validMessages[i] break } } - let result: Array<{ role: 'user' | 'assistant' | 'system'; content: string }> + let result: Array<{ role: 'user' | 'assistant' | 'summary'; content: string }> - if (systemMsg) { + if (summaryMsg) { // 从 content_blocks 中解析 summary_meta 获取 buffer 边界 - const metaBlock = systemMsg.contentBlocks?.find( + const metaBlock = summaryMsg.contentBlocks?.find( (b): b is Extract => b.type === 'summary_meta' ) const bufferBoundary = metaBlock?.bufferBoundaryTimestamp if (!metaBlock) { - aiLogger.warn('Conversations', 'system message missing summary_meta; agent context will be summary-only', { + aiLogger.warn('Conversations', 'summary message missing summary_meta; agent context will be summary-only', { conversationId, - messageId: systemMsg.id, + messageId: summaryMsg.id, }) } // 取 timestamp >= boundary 的 user/assistant 消息(buffer + 新消息) const contextMessages = bufferBoundary - ? validMessages.filter((m) => m.role !== 'system' && m.timestamp >= bufferBoundary) + ? validMessages.filter((m) => m.role !== 'summary' && m.timestamp >= bufferBoundary) : [] result = [ - { role: 'system' as const, content: systemMsg.content }, + { role: 'summary' as const, content: summaryMsg.content }, ...contextMessages.map((m) => ({ role: m.role, content: m.content })), ] } else { @@ -565,7 +605,7 @@ export function getHistoryForAgent( } if (maxMessages && result.length > maxMessages) { - if (result.length > 0 && result[0].role === 'system') { + if (result.length > 0 && result[0].role === 'summary') { const rest = result.slice(1) const truncated = rest.slice(-(maxMessages - 1)) return [result[0], ...truncated] @@ -578,7 +618,7 @@ export function getHistoryForAgent( // ==================== Summary / 压缩专用 ==================== /** - * 添加 system 消息并替换旧的 system(每个对话只保留一条最新压缩摘要) + * 添加 summary 消息并替换旧的 summary(每个对话只保留一条最新压缩摘要) * * Summary 时间戳 = NOW(UI 中显示在触发压缩的位置)。 * Buffer 边界信息存入 content_blocks 的 summary_meta block 中,供 getHistoryForAgent 使用。 @@ -590,7 +630,7 @@ export function addSummaryMessage( ): AIMessage { const db = getAiDb() - db.prepare("DELETE FROM ai_message WHERE conversation_id = ? AND role = 'system'").run(conversationId) + db.prepare("DELETE FROM ai_message WHERE conversation_id = ? AND role = 'summary'").run(conversationId) const contentBlocks: ContentBlock[] = [ { @@ -600,7 +640,7 @@ export function addSummaryMessage( }, ] - return addMessage(conversationId, 'system', content, undefined, undefined, contentBlocks) + return addMessage(conversationId, 'summary', content, undefined, undefined, contentBlocks) } /** @@ -614,7 +654,7 @@ export function getLatestSummary(conversationId: string): AIMessage | null { SELECT id, conversation_id as conversationId, role, content, timestamp, data_keywords as dataKeywords, data_message_count as dataMessageCount, content_blocks as contentBlocks FROM ai_message - WHERE conversation_id = ? AND role = 'system' + WHERE conversation_id = ? AND role = 'summary' ORDER BY timestamp DESC LIMIT 1 ` diff --git a/electron/main/ipc/ai.ts b/electron/main/ipc/ai.ts index 73f48859..a3716527 100644 --- a/electron/main/ipc/ai.ts +++ b/electron/main/ipc/ai.ts @@ -200,6 +200,17 @@ export function registerAIHandlers({ win }: IpcContext): void { aiLogger.info('Config', `Debug mode ${enabled ? 'enabled' : 'disabled'}`) }) + ipcMain.handle('ai:clearDebugContext', async () => { + try { + const cleared = aiConversations.clearAllDebugContext() + aiLogger.info('Debug', `Cleared debug_context for ${cleared} messages`) + return { success: true, cleared } + } catch (error) { + console.error('Failed to clear debug context:', error) + throw error + } + }) + // ==================== AI 对话管理 ==================== /** diff --git a/electron/preload/apis/ai.ts b/electron/preload/apis/ai.ts index fc7487a0..3910b32f 100644 --- a/electron/preload/apis/ai.ts +++ b/electron/preload/apis/ai.ts @@ -41,7 +41,7 @@ export type ContentBlock = } | { type: 'skill'; skillId: string; skillName: string } -export type AIMessageRole = 'user' | 'assistant' | 'system' +export type AIMessageRole = 'user' | 'assistant' | 'summary' export interface TokenUsageData { promptTokens: number @@ -541,6 +541,13 @@ export const aiApi = { return ipcRenderer.invoke('ai:showLogFile') }, + /** + * 一键清除所有消息的 debug_context 数据 + */ + clearDebugContext: (): Promise<{ success: boolean; cleared: number }> => { + return ipcRenderer.invoke('ai:clearDebugContext') + }, + getDefaultDesensitizeRules: (locale: string): Promise => { return ipcRenderer.invoke('ai:getDefaultDesensitizeRules', locale) }, diff --git a/electron/preload/index.d.ts b/electron/preload/index.d.ts index 4f2bbc76..a523fb6b 100644 --- a/electron/preload/index.d.ts +++ b/electron/preload/index.d.ts @@ -310,7 +310,7 @@ type AIContentBlock = compressedMessageCount: number } -type AIMessageRole = 'user' | 'assistant' | 'system' +type AIMessageRole = 'user' | 'assistant' | 'summary' interface AITokenUsageData { promptTokens: number @@ -396,6 +396,7 @@ interface AiApi { getConversationTokenUsage: (conversationId: string) => Promise deleteMessage: (messageId: string) => Promise showAiLogFile: () => Promise<{ success: boolean; path?: string; error?: string }> + clearDebugContext: () => Promise<{ success: boolean; cleared: number }> getDefaultDesensitizeRules: (locale: string) => Promise mergeDesensitizeRules: (existingRules: DesensitizeRule[], locale: string) => Promise getToolCatalog: () => Promise diff --git a/src/components/AIChat/chat/ChatMessage.vue b/src/components/AIChat/chat/ChatMessage.vue index 63bdeb00..7783828d 100644 --- a/src/components/AIChat/chat/ChatMessage.vue +++ b/src/components/AIChat/chat/ChatMessage.vue @@ -13,7 +13,7 @@ const toast = useToast() // Props const props = defineProps<{ - role: 'user' | 'assistant' | 'system' + role: 'user' | 'assistant' | 'summary' content: string timestamp: number isStreaming?: boolean @@ -30,7 +30,7 @@ const formattedTime = computed(() => { // 是否是用户消息 const isUser = computed(() => props.role === 'user') -const isSystem = computed(() => props.role === 'system') +const isSummary = computed(() => props.role === 'summary') // 创建 markdown-it 实例 const md = new MarkdownIt({ @@ -302,11 +302,11 @@ async function handleCopyMarkdown() {