diff --git a/package.json b/package.json index c8148f69a..8f58d4e7e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "DeepChat", - "version": "0.3.3", + "version": "0.3.4", "description": "DeepChat,一个简单易用的AI客户端", "main": "./out/main/index.js", "author": "ThinkInAIXYZ", diff --git a/src/main/presenter/configPresenter/index.ts b/src/main/presenter/configPresenter/index.ts index fb593d07d..37154c005 100644 --- a/src/main/presenter/configPresenter/index.ts +++ b/src/main/presenter/configPresenter/index.ts @@ -30,6 +30,12 @@ import { defaultShortcutKey, ShortcutKeySetting } from './shortcutKeySettings' import { ModelConfigHelper } from './modelConfig' import { KnowledgeConfHelper } from './knowledgeConfHelper' +// 默认系统提示词常量 +const DEFAULT_SYSTEM_PROMPT = `You are DeepChat, a highly capable AI assistant. Your goal is to fully complete the user’s requested task before handing the conversation back to them. Keep working autonomously until the task is fully resolved. +Be thorough in gathering information. Before replying, make sure you have all the details necessary to provide a complete solution. Use additional tools or ask clarifying questions when needed, but if you can find the answer on your own, avoid asking the user for help. +When using tools, briefly describe your intended steps first—for example, which tool you’ll use and for what purpose. +Adhere to this in all languages.Always respond in the same language as the user's query.` + // 定义应用设置的接口 interface IAppSettings { // 在这里定义你的配置项,例如: @@ -147,7 +153,7 @@ export class ConfigPresenter implements IConfigPresenter { const oldVersion = this.store.get('appVersion') this.store.set('appVersion', this.currentAppVersion) // 迁移数据 - this.migrateModelData(oldVersion) + this.migrateConfigData(oldVersion) this.mcpConfHelper.onUpgrade(oldVersion) } @@ -184,7 +190,7 @@ export class ConfigPresenter implements IConfigPresenter { return this.providersModelStores.get(providerId)! } - private migrateModelData(oldVersion: string | undefined): void { + private migrateConfigData(oldVersion: string | undefined): void { // 0.2.4 版本之前,minimax 的 baseUrl 是错误的,需要修正 if (oldVersion && compare(oldVersion, '0.2.4', '<')) { const providers = this.getProviders() @@ -267,6 +273,14 @@ export class ConfigPresenter implements IConfigPresenter { this.setProviders(filteredProviders) } } + + // 0.3.4 版本之前,如果默认系统提示词为空,则设置为内置的默认提示词 + if (oldVersion && compare(oldVersion, '0.3.4', '<')) { + const currentPrompt = this.getSetting('default_system_prompt') + if (!currentPrompt || currentPrompt.trim() === '') { + this.setSetting('default_system_prompt', DEFAULT_SYSTEM_PROMPT) + } + } } getSetting(key: string): T | undefined { @@ -1190,6 +1204,16 @@ export class ConfigPresenter implements IConfigPresenter { this.setSetting('default_system_prompt', prompt) } + // 重置为默认系统提示词 + async resetToDefaultPrompt(): Promise { + this.setSetting('default_system_prompt', DEFAULT_SYSTEM_PROMPT) + } + + // 清空系统提示词 + async clearSystemPrompt(): Promise { + this.setSetting('default_system_prompt', '') + } + // 获取默认快捷键 getDefaultShortcutKey(): ShortcutKeySetting { return { diff --git a/src/main/presenter/sqlitePresenter/importData.ts b/src/main/presenter/sqlitePresenter/importData.ts index 8fec3ccaa..7cb13418b 100644 --- a/src/main/presenter/sqlitePresenter/importData.ts +++ b/src/main/presenter/sqlitePresenter/importData.ts @@ -67,16 +67,57 @@ export class DataImporter { * @returns 导入的会话数量 */ public async importData(): Promise { - // 获取所有会话 - const conversations = this.sourceDb - .prepare( - `SELECT - conv_id, title, created_at, updated_at, system_prompt, - temperature, context_length, max_tokens, provider_id, - model_id, is_pinned, is_new, artifacts - FROM conversations` - ) - .all() as any[] + // 获取所有会话 - 兼容不同版本的数据库schema + let conversations: any[] + + try { + // 尝试使用包含所有新字段的查询 + conversations = this.sourceDb + .prepare( + `SELECT + conv_id, title, created_at, updated_at, system_prompt, + temperature, context_length, max_tokens, provider_id, + model_id, + COALESCE(is_pinned, 0) as is_pinned, + COALESCE(is_new, 0) as is_new, + COALESCE(artifacts, 0) as artifacts, + enabled_mcp_tools, + thinking_budget, + reasoning_effort, + verbosity + FROM conversations` + ) + .all() as any[] + } catch { + // 如果失败,使用基础字段查询(兼容旧版本数据库) + try { + conversations = this.sourceDb + .prepare( + `SELECT + conv_id, title, created_at, updated_at, system_prompt, + temperature, context_length, max_tokens, provider_id, + model_id, + COALESCE(is_pinned, 0) as is_pinned, + COALESCE(is_new, 0) as is_new, + COALESCE(artifacts, 0) as artifacts + FROM conversations` + ) + .all() as any[] + + // 为缺失的字段设置默认值 + conversations = conversations.map((conv) => ({ + ...conv, + enabled_mcp_tools: null, + thinking_budget: null, + reasoning_effort: null, + verbosity: null + })) + } catch (fallbackError) { + throw new Error( + `Failed to query conversations: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}` + ) + } + } // 使用better-sqlite3的transaction API来处理事务 const importTransaction = this.targetDb.transaction(() => { @@ -99,9 +140,11 @@ export class DataImporter { try { // 执行事务并返回导入的会话数量 return importTransaction() - } catch { - // 事务会自动回滚,直接抛出错误 - throw new Error('Failed to import data') + } catch (transactionError) { + // 事务会自动回滚,抛出详细错误 + throw new Error( + `Failed to import data: ${transactionError instanceof Error ? transactionError.message : String(transactionError)}` + ) } } @@ -114,34 +157,77 @@ export class DataImporter { // const newConvId = nanoid() // this.idMappings.conversations.set(conv.conv_id, newConvId) - // 插入会话 - this.targetDb - .prepare( - `INSERT INTO conversations ( - conv_id, title, created_at, updated_at, system_prompt, - temperature, context_length, max_tokens, provider_id, - model_id, is_pinned, is_new, artifacts, thinking_budget - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` - ) - .run( - conv.conv_id, - conv.title, - conv.created_at, - conv.updated_at, - conv.system_prompt, - conv.temperature, - conv.context_length, - conv.max_tokens, - conv.provider_id, - conv.model_id, - conv.is_pinned || 0, - conv.is_new || 0, - conv.artifacts || 0, - conv.thinking_budget !== undefined ? conv.thinking_budget : null - ) + try { + // 首先尝试使用包含所有新字段的INSERT语句 + this.targetDb + .prepare( + `INSERT INTO conversations ( + conv_id, title, created_at, updated_at, system_prompt, + temperature, context_length, max_tokens, provider_id, + model_id, is_pinned, is_new, artifacts, enabled_mcp_tools, + thinking_budget, reasoning_effort, verbosity + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + ) + .run( + conv.conv_id, + conv.title, + conv.created_at, + conv.updated_at, + conv.system_prompt, + conv.temperature, + conv.context_length, + conv.max_tokens, + conv.provider_id, + conv.model_id, + conv.is_pinned || 0, + conv.is_new || 0, + conv.artifacts || 0, + conv.enabled_mcp_tools || null, + conv.thinking_budget || null, + conv.reasoning_effort || null, + conv.verbosity || null + ) + } catch { + // 如果失败,使用基础字段的INSERT语句(兼容旧版本目标数据库) + try { + this.targetDb + .prepare( + `INSERT INTO conversations ( + conv_id, title, created_at, updated_at, system_prompt, + temperature, context_length, max_tokens, provider_id, + model_id, is_pinned, is_new, artifacts + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + ) + .run( + conv.conv_id, + conv.title, + conv.created_at, + conv.updated_at, + conv.system_prompt, + conv.temperature, + conv.context_length, + conv.max_tokens, + conv.provider_id, + conv.model_id, + conv.is_pinned || 0, + conv.is_new || 0, + conv.artifacts || 0 + ) + } catch (fallbackError) { + throw new Error( + `Failed to insert conversation ${conv.conv_id}: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}` + ) + } + } // 导入该会话的所有消息 - this.importMessages(conv.conv_id) + try { + this.importMessages(conv.conv_id) + } catch (messageError) { + throw new Error( + `Failed to import messages for conversation ${conv.conv_id}: ${messageError instanceof Error ? messageError.message : String(messageError)}` + ) + } } /** @@ -173,33 +259,39 @@ export class DataImporter { newParentId = this.idMappings.messages.get(msg.parent_id) || '' } - // 插入消息 - this.targetDb - .prepare( - `INSERT INTO messages ( - msg_id, conversation_id, parent_id, role, content, - created_at, order_seq, token_count, status, metadata, - is_context_edge, is_variant - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` - ) - .run( - newMsgId, - oldConvId, - newParentId, - msg.role, - msg.content, - msg.created_at, - msg.order_seq, - msg.token_count || 0, - msg.status || 'sent', - msg.metadata || null, - msg.is_context_edge || 0, - msg.is_variant || 0 - ) + try { + // 插入消息 + this.targetDb + .prepare( + `INSERT INTO messages ( + msg_id, conversation_id, parent_id, role, content, + created_at, order_seq, token_count, status, metadata, + is_context_edge, is_variant + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + ) + .run( + newMsgId, + oldConvId, + newParentId, + msg.role, + msg.content, + msg.created_at, + msg.order_seq, + msg.token_count || 0, + msg.status || 'sent', + msg.metadata || null, + msg.is_context_edge || 0, + msg.is_variant || 0 + ) - // 导入消息的附件 - this.importAttachments(msg.msg_id, newMsgId) - this.importMessageAttachments(msg.msg_id, newMsgId) + // 导入消息的附件 + this.importAttachments(msg.msg_id, newMsgId) + this.importMessageAttachments(msg.msg_id, newMsgId) + } catch (msgError) { + throw new Error( + `Failed to insert message ${msg.msg_id}: ${msgError instanceof Error ? msgError.message : String(msgError)}` + ) + } } } @@ -266,35 +358,73 @@ export class DataImporter { * @param newMsgId 新消息ID */ private importMessageAttachments(oldMsgId: string, newMsgId: string): void { - // 获取消息的所有message_attachments - const messageAttachments = this.sourceDb - .prepare( - `SELECT - attachment_id, type, content, created_at, metadata - FROM message_attachments - WHERE message_id = ?` - ) - .all(oldMsgId) as any[] + // 获取消息的所有message_attachments - 兼容不同的schema版本 + let messageAttachments: any[] + + try { + // 首先尝试包含metadata字段的查询 + messageAttachments = this.sourceDb + .prepare( + `SELECT + attachment_id, type, content, created_at, metadata + FROM message_attachments + WHERE message_id = ?` + ) + .all(oldMsgId) as any[] + } catch { + // 如果失败,使用不包含metadata的查询(兼容新版本schema) + messageAttachments = this.sourceDb + .prepare( + `SELECT + attachment_id, type, content, created_at + FROM message_attachments + WHERE message_id = ?` + ) + .all(oldMsgId) as any[] + + // 为缺失的字段设置默认值 + messageAttachments = messageAttachments.map((attachment) => ({ + ...attachment, + metadata: null + })) + } // 逐个导入message_attachments for (const attachment of messageAttachments) { const newAttachmentId = nanoid() - // 插入message_attachment - this.targetDb - .prepare( - `INSERT INTO message_attachments ( - attachment_id, message_id, type, content, created_at, metadata - ) VALUES (?, ?, ?, ?, ?, ?)` - ) - .run( - newAttachmentId, - newMsgId, - attachment.type, - attachment.content, - attachment.created_at, - attachment.metadata - ) + try { + // 首先尝试包含metadata字段的INSERT + this.targetDb + .prepare( + `INSERT INTO message_attachments ( + attachment_id, message_id, type, content, created_at, metadata + ) VALUES (?, ?, ?, ?, ?, ?)` + ) + .run( + newAttachmentId, + newMsgId, + attachment.type, + attachment.content, + attachment.created_at, + attachment.metadata + ) + } catch { + // 如果失败,使用不包含metadata的INSERT(兼容新版本schema) + this.targetDb + .prepare( + `INSERT INTO message_attachments ( + attachment_id, message_id, type, content, created_at + ) VALUES (?, ?, ?, ?, ?)` + ) + .run( + newAttachmentId, + newMsgId, + attachment.type, + attachment.content, + attachment.created_at + ) + } } } diff --git a/src/main/presenter/threadPresenter/index.ts b/src/main/presenter/threadPresenter/index.ts index 7aee5f262..a91eef570 100644 --- a/src/main/presenter/threadPresenter/index.ts +++ b/src/main/presenter/threadPresenter/index.ts @@ -30,6 +30,7 @@ import { UserMessageMentionBlock, UserMessageCodeBlock } from '@shared/chat' +import { ModelType } from '@shared/model' import { approximateTokenSize } from 'tokenx' import { generateSearchPrompt, SearchManager } from './searchManager' import { getFileContext } from './fileContext' @@ -2094,24 +2095,34 @@ export class ThreadPresenter implements IThreadPresenter { userMessage: Message, vision: boolean, imageFiles: MessageFile[], - supportsFunctionCall: boolean + supportsFunctionCall: boolean, + modelType?: ModelType ): Promise<{ finalContent: ChatMessage[] promptTokens: number }> { const { systemPrompt, contextLength, artifacts, enabledMcpTools } = conversation.settings - const searchPrompt = searchResults ? generateSearchPrompt(userContent, searchResults) : '' + // 判断是否为图片生成模型 + const isImageGeneration = modelType === ModelType.ImageGeneration + + // 图片生成模型不使用搜索、系统提示词和MCP工具 + const searchPrompt = + !isImageGeneration && searchResults ? generateSearchPrompt(userContent, searchResults) : '' const enrichedUserMessage = - urlResults.length > 0 + !isImageGeneration && urlResults.length > 0 ? '\n\n' + ContentEnricher.enrichUserMessageWithUrlContent(userContent, urlResults) : '' // 计算token数量 const searchPromptTokens = searchPrompt ? approximateTokenSize(searchPrompt ?? '') : 0 - const systemPromptTokens = systemPrompt ? approximateTokenSize(systemPrompt ?? '') : 0 + const systemPromptTokens = + !isImageGeneration && systemPrompt ? approximateTokenSize(systemPrompt ?? '') : 0 const userMessageTokens = approximateTokenSize(userContent + enrichedUserMessage) - const mcpTools = await presenter.mcpPresenter.getAllToolDefinitions(enabledMcpTools) + // 图片生成模型不使用MCP工具 + const mcpTools = !isImageGeneration + ? await presenter.mcpPresenter.getAllToolDefinitions(enabledMcpTools) + : [] const mcpToolsTokens = mcpTools.reduce( (acc, tool) => acc + approximateTokenSize(JSON.stringify(tool)), 0 @@ -2131,7 +2142,7 @@ export class ThreadPresenter implements IThreadPresenter { // 格式化消息 const formattedMessages = this.formatMessagesForCompletion( selectedContextMessages, - systemPrompt, + isImageGeneration ? '' : systemPrompt, // 图片生成模型不使用系统提示词 artifacts, searchPrompt, userContent, diff --git a/src/main/presenter/windowPresenter/index.ts b/src/main/presenter/windowPresenter/index.ts index 24e98ecca..83c091f3a 100644 --- a/src/main/presenter/windowPresenter/index.ts +++ b/src/main/presenter/windowPresenter/index.ts @@ -1,5 +1,5 @@ // src\main\presenter\windowPresenter\index.ts -import { BrowserWindow, shell, nativeImage, ipcMain } from 'electron' +import { BrowserWindow, shell, nativeImage, ipcMain, screen } from 'electron' import { join } from 'path' import icon from '../../../../resources/icon.png?asset' // 应用图标 (macOS/Linux) import iconWin from '../../../../resources/icon.ico?asset' // 应用图标 (Windows) @@ -575,10 +575,25 @@ export class WindowPresenter implements IWindowPresenter { defaultHeight: 620 }) - // 计算初始位置,确保 Y 坐标不为负数 - const initialX = options?.x !== undefined ? options.x : shellWindowState.x - let initialY = options?.y !== undefined ? options?.y : shellWindowState.y - initialY = Math.max(0, initialY || 0) + // 计算初始位置,确保窗口完全在屏幕范围内 + const initialX = + options?.x !== undefined + ? options.x + : this.validateWindowPosition( + shellWindowState.x, + shellWindowState.width, + shellWindowState.y, + shellWindowState.height + ).x + let initialY = + options?.y !== undefined + ? options?.y + : this.validateWindowPosition( + shellWindowState.x, + shellWindowState.width, + shellWindowState.y, + shellWindowState.height + ).y const shellWindow = new BrowserWindow({ width: shellWindowState.width, @@ -1179,4 +1194,26 @@ export class WindowPresenter implements IWindowPresenter { public setApplicationQuitting(isQuitting: boolean): void { this.isQuitting = isQuitting } + + private validateWindowPosition( + x: number, + width: number, + y: number, + height: number + ): { x: number; y: number } { + const primaryDisplay = screen.getPrimaryDisplay() + const { workArea } = primaryDisplay + const isXValid = x >= workArea.x && x + width <= workArea.x + workArea.width + const isYValid = y >= workArea.y && y + height <= workArea.y + workArea.height + if (!isXValid || !isYValid) { + console.log( + `Window position out of bounds (x: ${x}, y: ${y}, width: ${width}, height: ${height}), centering window` + ) + return { + x: workArea.x + Math.max(0, (workArea.width - width) / 2), + y: workArea.y + Math.max(0, (workArea.height - height) / 2) + } + } + return { x, y } + } } diff --git a/src/renderer/src/components/ChatConfig.vue b/src/renderer/src/components/ChatConfig.vue index 57f702230..3c9fbfa82 100644 --- a/src/renderer/src/components/ChatConfig.vue +++ b/src/renderer/src/components/ChatConfig.vue @@ -1,6 +1,6 @@