diff --git a/electron/main/ai/llm/index.ts b/electron/main/ai/llm/index.ts index 48c606e..29b8805 100644 --- a/electron/main/ai/llm/index.ts +++ b/electron/main/ai/llm/index.ts @@ -504,7 +504,6 @@ export async function* chatStream(messages: ChatMessage[], options?: ChatOptions messagesCount: messages.length, firstMessageRole: messages[0]?.role, firstMessageLength: messages[0]?.content?.length, - options, }) const service = getCurrentLLMService() diff --git a/electron/main/ai/logger.ts b/electron/main/ai/logger.ts index 1a1f534..c81a8a5 100644 --- a/electron/main/ai/logger.ts +++ b/electron/main/ai/logger.ts @@ -48,6 +48,16 @@ function getLogFilePath(): string { return LOG_FILE } +/** + * 获取已存在的日志文件路径(不会创建新文件) + */ +function getExistingLogPath(): string | null { + if (LOG_FILE && fs.existsSync(LOG_FILE)) { + return LOG_FILE + } + return null +} + /** * 获取日志写入流 */ @@ -151,6 +161,13 @@ export const aiLogger = { getLogPath(): string { return getLogFilePath() }, + + /** + * 获取已存在的日志文件路径(无日志时返回空) + */ + getExistingLogPath(): string | null { + return getExistingLogPath() + }, } // 导出便捷函数 diff --git a/electron/main/ipc/ai.ts b/electron/main/ipc/ai.ts index 5e765ca..fe3a87a 100644 --- a/electron/main/ipc/ai.ts +++ b/electron/main/ipc/ai.ts @@ -1,8 +1,11 @@ // electron/main/ipc/ai.ts -import { ipcMain, BrowserWindow } from 'electron' +import { ipcMain, BrowserWindow, shell } from 'electron' +import * as fs from 'fs' +import * as path from 'path' import * as aiConversations from '../ai/conversations' import * as llm from '../ai/llm' import { aiLogger } from '../ai/logger' +import { getLogsDir } from '../paths' import { Agent, type AgentStreamChunk, type PromptConfig } from '../ai/agent' import type { ToolContext } from '../ai/tools/types' import type { IpcContext } from './types' @@ -140,6 +143,48 @@ export function registerAIHandlers({ win }: IpcContext): void { } }) + /** + * 打开当前 AI 日志文件并定位到文件 + */ + ipcMain.handle('ai:showLogFile', async () => { + try { + // 优先使用当前已存在的日志文件,避免创建新的空日志 + const existingLogPath = aiLogger.getExistingLogPath() + if (existingLogPath) { + shell.showItemInFolder(existingLogPath) + return { success: true, path: existingLogPath } + } + + const logDir = path.join(getLogsDir(), 'ai') + if (!fs.existsSync(logDir)) { + return { success: false, error: '暂无 AI 日志文件' } + } + + const logFiles = fs + .readdirSync(logDir) + .filter((name) => name.startsWith('ai_') && name.endsWith('.log')) + + if (logFiles.length === 0) { + return { success: false, error: '暂无 AI 日志文件' } + } + + // 选择最近修改的日志文件 + const latestLog = logFiles + .map((name) => { + const filePath = path.join(logDir, name) + const stat = fs.statSync(filePath) + return { path: filePath, mtimeMs: stat.mtimeMs } + }) + .sort((a, b) => b.mtimeMs - a.mtimeMs)[0] + + shell.showItemInFolder(latestLog.path) + return { success: true, path: latestLog.path } + } catch (error) { + console.error('打开 AI 日志文件失败:', error) + return { success: false, error: String(error) } + } + }) + /** * 获取单个对话详情 */ diff --git a/electron/preload/index.d.ts b/electron/preload/index.d.ts index 240b3ea..03e6a80 100644 --- a/electron/preload/index.d.ts +++ b/electron/preload/index.d.ts @@ -292,6 +292,7 @@ interface AiApi { getMessages: (conversationId: string) => Promise getMessages: (conversationId: string) => Promise deleteMessage: (messageId: string) => Promise + showAiLogFile: () => Promise<{ success: boolean; path?: string; error?: string }> // 自定义筛选 filterMessagesWithContext: ( sessionId: string, diff --git a/electron/preload/index.ts b/electron/preload/index.ts index ef1e46b..bf7817a 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -743,6 +743,13 @@ const aiApi = { deleteMessage: (messageId: string): Promise => { return ipcRenderer.invoke('ai:deleteMessage', messageId) }, + + /** + * 打开当前 AI 日志文件并定位到文件 + */ + showAiLogFile: (): Promise<{ success: boolean; path?: string; error?: string }> => { + return ipcRenderer.invoke('ai:showLogFile') + }, } // LLM API - LLM 服务功能 diff --git a/src/components/analysis/AIChat/ChatStatusBar.vue b/src/components/analysis/AIChat/ChatStatusBar.vue index a6d095d..9e06ffc 100644 --- a/src/components/analysis/AIChat/ChatStatusBar.vue +++ b/src/components/analysis/AIChat/ChatStatusBar.vue @@ -2,10 +2,12 @@ import { ref, computed } from 'vue' import { storeToRefs } from 'pinia' import { useI18n } from 'vue-i18n' +import { useToast } from '@nuxt/ui/runtime/composables/useToast.js' import { usePromptStore } from '@/stores/prompt' import { useLayoutStore } from '@/stores/layout' const { t } = useI18n() +const toast = useToast() // Props const props = defineProps<{ @@ -34,6 +36,7 @@ const currentActivePreset = computed(() => { // 预设下拉菜单状态 const isPresetPopoverOpen = ref(false) +const isOpeningLog = ref(false) // 设置激活预设 function setActivePreset(presetId: string) { @@ -51,6 +54,35 @@ function openPresetSettings() { function openChatSettings() { layoutStore.openSettingAt('ai', 'chat') } + +// 打开当前 AI 日志文件并定位到文件 +async function openAiLogFile() { + if (isOpeningLog.value) return + isOpeningLog.value = true + try { + const result = await window.aiApi.showAiLogFile() + if (!result?.success) { + toast.add({ + title: t('log.openFailed'), + description: result?.error || t('log.openFailedDesc'), + icon: 'i-heroicons-x-circle', + color: 'error', + duration: 2000, + }) + } + } catch (error) { + console.error('打开 AI 日志失败:', error) + toast.add({ + title: t('log.openFailed'), + description: String(error), + icon: 'i-heroicons-x-circle', + color: 'error', + duration: 2000, + }) + } finally { + isOpeningLog.value = false + } +}