mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-03 11:41:17 +08:00
feat: 新增定位日志功能
This commit is contained in:
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导出便捷函数
|
// 导出便捷函数
|
||||||
|
|||||||
@@ -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) }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取单个对话详情
|
* 获取单个对话详情
|
||||||
*/
|
*/
|
||||||
|
|||||||
1
electron/preload/index.d.ts
vendored
1
electron/preload/index.d.ts
vendored
@@ -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,
|
||||||
|
|||||||
@@ -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 服务功能
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user