feat: 新增定位日志功能

This commit is contained in:
digua
2026-01-21 00:40:40 +08:00
parent 8f3f3e62f2
commit a669306bbd
6 changed files with 126 additions and 3 deletions

View File

@@ -504,7 +504,6 @@ export async function* chatStream(messages: ChatMessage[], options?: ChatOptions
messagesCount: messages.length, messagesCount: messages.length,
firstMessageRole: messages[0]?.role, firstMessageRole: messages[0]?.role,
firstMessageLength: messages[0]?.content?.length, firstMessageLength: messages[0]?.content?.length,
options,
}) })
const service = getCurrentLLMService() const service = getCurrentLLMService()

View File

@@ -48,6 +48,16 @@ function getLogFilePath(): string {
return LOG_FILE 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 { getLogPath(): string {
return getLogFilePath() return getLogFilePath()
}, },
/**
* 获取已存在的日志文件路径(无日志时返回空)
*/
getExistingLogPath(): string | null {
return getExistingLogPath()
},
} }
// 导出便捷函数 // 导出便捷函数

View File

@@ -1,8 +1,11 @@
// electron/main/ipc/ai.ts // 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 aiConversations from '../ai/conversations'
import * as llm from '../ai/llm' import * as llm from '../ai/llm'
import { aiLogger } from '../ai/logger' import { aiLogger } from '../ai/logger'
import { getLogsDir } from '../paths'
import { Agent, type AgentStreamChunk, type PromptConfig } from '../ai/agent' import { Agent, type AgentStreamChunk, type PromptConfig } from '../ai/agent'
import type { ToolContext } from '../ai/tools/types' import type { ToolContext } from '../ai/tools/types'
import type { IpcContext } from './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) }
}
})
/** /**
* 获取单个对话详情 * 获取单个对话详情
*/ */

View File

@@ -292,6 +292,7 @@ interface AiApi {
getMessages: (conversationId: string) => Promise<AIMessage[]> getMessages: (conversationId: string) => Promise<AIMessage[]>
getMessages: (conversationId: string) => Promise<AIMessage[]> getMessages: (conversationId: string) => Promise<AIMessage[]>
deleteMessage: (messageId: string) => Promise<boolean> deleteMessage: (messageId: string) => Promise<boolean>
showAiLogFile: () => Promise<{ success: boolean; path?: string; error?: string }>
// 自定义筛选 // 自定义筛选
filterMessagesWithContext: ( filterMessagesWithContext: (
sessionId: string, sessionId: string,

View File

@@ -743,6 +743,13 @@ const aiApi = {
deleteMessage: (messageId: string): Promise<boolean> => { deleteMessage: (messageId: string): Promise<boolean> => {
return ipcRenderer.invoke('ai:deleteMessage', messageId) return ipcRenderer.invoke('ai:deleteMessage', messageId)
}, },
/**
* 打开当前 AI 日志文件并定位到文件
*/
showAiLogFile: (): Promise<{ success: boolean; path?: string; error?: string }> => {
return ipcRenderer.invoke('ai:showLogFile')
},
} }
// LLM API - LLM 服务功能 // LLM API - LLM 服务功能

View File

@@ -2,10 +2,12 @@
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useToast } from '@nuxt/ui/runtime/composables/useToast.js'
import { usePromptStore } from '@/stores/prompt' import { usePromptStore } from '@/stores/prompt'
import { useLayoutStore } from '@/stores/layout' import { useLayoutStore } from '@/stores/layout'
const { t } = useI18n() const { t } = useI18n()
const toast = useToast()
// Props // Props
const props = defineProps<{ const props = defineProps<{
@@ -34,6 +36,7 @@ const currentActivePreset = computed(() => {
// 预设下拉菜单状态 // 预设下拉菜单状态
const isPresetPopoverOpen = ref(false) const isPresetPopoverOpen = ref(false)
const isOpeningLog = ref(false)
// 设置激活预设 // 设置激活预设
function setActivePreset(presetId: string) { function setActivePreset(presetId: string) {
@@ -51,6 +54,35 @@ function openPresetSettings() {
function openChatSettings() { function openChatSettings() {
layoutStore.openSettingAt('ai', 'chat') 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
}
}
</script> </script>
<template> <template>
@@ -106,7 +138,7 @@ function openChatSettings() {
</UPopover> </UPopover>
<!-- 右侧配置状态指示 --> <!-- 右侧配置状态指示 -->
<div class="flex items-center gap-3"> <div class="flex items-center gap-1">
<!-- 消息条数限制点击跳转设置 --> <!-- 消息条数限制点击跳转设置 -->
<button <button
class="flex items-center gap-1 rounded-md px-2 py-1 text-xs text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-gray-800 dark:hover:text-gray-300" class="flex items-center gap-1 rounded-md px-2 py-1 text-xs text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-gray-800 dark:hover:text-gray-300"
@@ -115,6 +147,16 @@ function openChatSettings() {
> >
<span>{{ t('messageLimit.label') }}{{ aiGlobalSettings.maxMessagesPerRequest }}</span> <span>{{ t('messageLimit.label') }}{{ aiGlobalSettings.maxMessagesPerRequest }}</span>
</button> </button>
<!-- 日志按钮 -->
<button
class="flex items-center gap-1 rounded-md px-2 py-1 text-xs text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600 disabled:cursor-not-allowed disabled:opacity-60 dark:hover:bg-gray-800 dark:hover:text-gray-300"
:title="t('log.title')"
:disabled="isOpeningLog"
@click="openAiLogFile"
>
<UIcon name="i-heroicons-folder-open" class="h-3.5 w-3.5" />
<span>{{ t('log.label') }}</span>
</button>
<!-- Token 使用量 --> <!-- Token 使用量 -->
<div <div
v-if="sessionTokenUsage.totalTokens > 0" v-if="sessionTokenUsage.totalTokens > 0"
@@ -152,6 +194,12 @@ function openChatSettings() {
"title": "每次发送的最大消息条数,点击配置" "title": "每次发送的最大消息条数,点击配置"
}, },
"tokenUsageTitle": "本次会话累计 Token 使用量", "tokenUsageTitle": "本次会话累计 Token 使用量",
"log": {
"label": "日志",
"title": "打开当前 AI 日志文件",
"openFailed": "打开日志失败",
"openFailedDesc": "请稍后重试"
},
"status": { "status": {
"connected": "AI 已连接", "connected": "AI 已连接",
"notConfigured": "请在全局设置中配置 AI 服务" "notConfigured": "请在全局设置中配置 AI 服务"
@@ -169,6 +217,12 @@ function openChatSettings() {
"title": "Max messages per request, click to configure" "title": "Max messages per request, click to configure"
}, },
"tokenUsageTitle": "Total token usage in this session", "tokenUsageTitle": "Total token usage in this session",
"log": {
"label": "Logs",
"title": "Open current AI log file",
"openFailed": "Failed to open log",
"openFailedDesc": "Please try again later"
},
"status": { "status": {
"connected": "AI Connected", "connected": "AI Connected",
"notConfigured": "Please configure AI service in Settings" "notConfigured": "Please configure AI service in Settings"