From 10b7c70a59ee4722c38646c998e56e5e2bbd5f4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=86=8A=E5=8F=AF=E7=8B=B8?= Date: Thu, 31 Jul 2025 19:11:31 +0800 Subject: [PATCH] fix(prompt): resolve variable replacement in function mode and add UI features (#6581) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(prompt): fix variable replacement in function mode * fix(i18n): update available variables in prompt tip * feat(prompt): replace prompt variable in Prompt and AssistantPromptSettings components * fix(prompt): add fallback value if replace failed * feat(prompt): add hook and settings for automatic prompt replacement * feat(prompt): add supported variables and utility function to check if they exist * feat(prompt): enhance variable handling in prompt settings and tooltips * feat(i18n): add prompt settings translations for multiple languages * refactor(prompt): remove debug log from prompt processing * fix(prompt): handle model name variables and improve prompt processing * fix: correct variable replacement setting and update migration defaults * remove prompt settings * refactor: simplify model name replacement logic - Remove unnecessary assistant parameter from buildSystemPrompt function - Update all API clients to use the simplified function signature - Centralize model name replacement logic in promptVariableReplacer - Improve code maintainability by reducing parameter coupling * fix: eslint error * refactor: remove unused interval handling in usePromptProcessor * test: add tests, remove redundant replacing * feat: animate prompt substitution * chore: prepare for merge * refactor: update utils * refactor: remove getStoreSettings * refactor: update utils * style(Message/Prompt): 禁止文本选中以提升用户体验 * fix(Prompt): 修复内存泄漏问题,清除内部定时器 * refactor: move prompt replacement to api service --------- Co-authored-by: one Co-authored-by: icarus --- src/renderer/src/hooks/usePromptProcessor.ts | 34 +++++ .../src/pages/home/Messages/Prompt.tsx | 49 ++++++- .../AssistantPromptSettings.tsx | 8 +- src/renderer/src/services/ApiService.ts | 17 ++- src/renderer/src/store/thunk/messageThunk.ts | 3 - .../src/utils/__tests__/prompt.test.ts | 14 +- src/renderer/src/utils/prompt.ts | 135 ++++++++++-------- 7 files changed, 186 insertions(+), 74 deletions(-) create mode 100644 src/renderer/src/hooks/usePromptProcessor.ts diff --git a/src/renderer/src/hooks/usePromptProcessor.ts b/src/renderer/src/hooks/usePromptProcessor.ts new file mode 100644 index 0000000000..23f341a12f --- /dev/null +++ b/src/renderer/src/hooks/usePromptProcessor.ts @@ -0,0 +1,34 @@ +import { loggerService } from '@logger' +import { containsSupportedVariables, replacePromptVariables } from '@renderer/utils/prompt' +import { useEffect, useState } from 'react' + +const logger = loggerService.withContext('usePromptProcessor') + +interface PromptProcessor { + prompt: string + modelName?: string +} + +export function usePromptProcessor({ prompt, modelName }: PromptProcessor): string { + const [processedPrompt, setProcessedPrompt] = useState(prompt) + + useEffect(() => { + const processPrompt = async () => { + try { + if (containsSupportedVariables(prompt)) { + const result = await replacePromptVariables(prompt, modelName) + setProcessedPrompt(result) + } else { + setProcessedPrompt(prompt) + } + } catch (error) { + logger.error('Failed to process prompt variables, falling back:', error as Error) + setProcessedPrompt(prompt) + } + } + + processPrompt() + }, [prompt, modelName]) + + return processedPrompt +} diff --git a/src/renderer/src/pages/home/Messages/Prompt.tsx b/src/renderer/src/pages/home/Messages/Prompt.tsx index 7b8565f3c3..97fd5adfe5 100644 --- a/src/renderer/src/pages/home/Messages/Prompt.tsx +++ b/src/renderer/src/pages/home/Messages/Prompt.tsx @@ -1,7 +1,9 @@ import { useTheme } from '@renderer/context/ThemeProvider' +import { usePromptProcessor } from '@renderer/hooks/usePromptProcessor' import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings' import { Assistant, Topic } from '@renderer/types' -import { FC } from 'react' +import { containsSupportedVariables } from '@renderer/utils/prompt' +import { FC, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' @@ -18,13 +20,50 @@ const Prompt: FC = ({ assistant, topic }) => { const topicPrompt = topic?.prompt || '' const isDark = theme === 'dark' + const processedPrompt = usePromptProcessor({ prompt, modelName: assistant.model?.name }) + + // 用于控制显示的状态 + const [displayText, setDisplayText] = useState(prompt) + const [isVisible, setIsVisible] = useState(true) + + useEffect(() => { + // 如果没有变量需要替换,直接显示处理后的内容 + if (!containsSupportedVariables(prompt)) { + setDisplayText(processedPrompt) + setIsVisible(true) + return + } + + // 如果有变量需要替换,先显示原始prompt + setDisplayText(prompt) + setIsVisible(true) + + // 延迟过渡 + let innerTimer: NodeJS.Timeout + const outerTimer = setTimeout(() => { + // 先淡出 + setIsVisible(false) + + // 切换内容并淡入 + innerTimer = setTimeout(() => { + setDisplayText(processedPrompt) + setIsVisible(true) + }, 300) + }, 300) + + return () => { + clearTimeout(outerTimer) + clearTimeout(innerTimer) + } + }, [prompt, processedPrompt]) + if (!prompt && !topicPrompt) { return null } return ( AssistantSettingsPopup.show({ assistant })} $isDark={isDark}> - {prompt} + {displayText} ) } @@ -38,13 +77,17 @@ const Container = styled.div<{ $isDark: boolean }>` margin-bottom: 0; ` -const Text = styled.div` +const Text = styled.div<{ $isVisible: boolean }>` color: var(--color-text-2); font-size: 12px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; + user-select: none; + + opacity: ${(props) => (props.$isVisible ? 1 : 0)}; + transition: opacity 0.3s ease-in-out; ` export default Prompt diff --git a/src/renderer/src/pages/settings/AssistantSettings/AssistantPromptSettings.tsx b/src/renderer/src/pages/settings/AssistantSettings/AssistantPromptSettings.tsx index 54ade12315..643f386f32 100644 --- a/src/renderer/src/pages/settings/AssistantSettings/AssistantPromptSettings.tsx +++ b/src/renderer/src/pages/settings/AssistantSettings/AssistantPromptSettings.tsx @@ -4,6 +4,7 @@ import { CloseCircleFilled, QuestionCircleOutlined } from '@ant-design/icons' import CodeEditor from '@renderer/components/CodeEditor' import EmojiPicker from '@renderer/components/EmojiPicker' import { Box, HSpaceBetweenStack, HStack } from '@renderer/components/Layout' +import { usePromptProcessor } from '@renderer/hooks/usePromptProcessor' import { estimateTextTokens } from '@renderer/services/TokenService' import { Assistant, AssistantSettings } from '@renderer/types' import { getLeadingEmoji } from '@renderer/utils' @@ -38,6 +39,11 @@ const AssistantPromptSettings: React.FC = ({ assistant, updateAssistant } updateTokenCount() }, [prompt]) + const processedPrompt = usePromptProcessor({ + prompt, + modelName: assistant.model?.name + }) + const onUpdate = () => { const _assistant = { ...assistant, name: name.trim(), emoji, prompt } updateAssistant(_assistant) @@ -112,7 +118,7 @@ const AssistantPromptSettings: React.FC = ({ assistant, updateAssistant } {showMarkdown ? ( setShowMarkdown(false)}> - {prompt} + {processedPrompt || prompt}
) : ( diff --git a/src/renderer/src/services/ApiService.ts b/src/renderer/src/services/ApiService.ts index 8976780449..c2aa95d220 100644 --- a/src/renderer/src/services/ApiService.ts +++ b/src/renderer/src/services/ApiService.ts @@ -41,7 +41,12 @@ import { removeSpecialCharactersForTopicName } from '@renderer/utils' import { isAbortError } from '@renderer/utils/error' import { extractInfoFromXML, ExtractResults } from '@renderer/utils/extract' import { findFileBlocks, getMainTextContent } from '@renderer/utils/messageUtils/find' -import { buildSystemPromptWithThinkTool, buildSystemPromptWithTools } from '@renderer/utils/prompt' +import { + buildSystemPromptWithThinkTool, + buildSystemPromptWithTools, + containsSupportedVariables, + replacePromptVariables +} from '@renderer/utils/prompt' import { findLast, isEmpty, takeRight } from 'lodash' import AiProvider from '../aiCore' @@ -426,6 +431,10 @@ export async function fetchChatCompletion({ }) { logger.debug('fetchChatCompletion', messages, assistant) + if (assistant.prompt && containsSupportedVariables(assistant.prompt)) { + assistant.prompt = await replacePromptVariables(assistant.prompt, assistant.model?.name) + } + const provider = getAssistantProvider(assistant) const AI = new AiProvider(provider) @@ -643,9 +652,13 @@ export async function fetchTranslate({ content, assistant, onResponse }: FetchTr } export async function fetchMessagesSummary({ messages, assistant }: { messages: Message[]; assistant: Assistant }) { - const prompt = (getStoreSetting('topicNamingPrompt') as string) || i18n.t('prompts.title') + let prompt = (getStoreSetting('topicNamingPrompt') as string) || i18n.t('prompts.title') const model = getTopNamingModel() || assistant.model || getDefaultModel() + if (prompt && containsSupportedVariables(prompt)) { + prompt = await replacePromptVariables(prompt, model.name) + } + // 总结上下文总是取最后5条消息 const contextMessages = takeRight(messages, 5) diff --git a/src/renderer/src/store/thunk/messageThunk.ts b/src/renderer/src/store/thunk/messageThunk.ts index b40d3b555d..1a55e5f2c9 100644 --- a/src/renderer/src/store/thunk/messageThunk.ts +++ b/src/renderer/src/store/thunk/messageThunk.ts @@ -17,7 +17,6 @@ import { createTranslationBlock, resetAssistantMessage } from '@renderer/utils/messageUtils/create' -import { buildSystemPrompt } from '@renderer/utils/prompt' import { getTopicQueue } from '@renderer/utils/queue' import { waitForTopicQueue } from '@renderer/utils/queue' import { t } from 'i18next' @@ -878,8 +877,6 @@ const fetchAndProcessAssistantResponseImpl = async ( // } // } - assistant.prompt = await buildSystemPrompt(assistant.prompt || '', assistant) - callbacks = createCallbacks({ blockManager, dispatch, diff --git a/src/renderer/src/utils/__tests__/prompt.test.ts b/src/renderer/src/utils/__tests__/prompt.test.ts index 3ae21dcaa4..898f3fc691 100644 --- a/src/renderer/src/utils/__tests__/prompt.test.ts +++ b/src/renderer/src/utils/__tests__/prompt.test.ts @@ -4,9 +4,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { AvailableTools, - buildSystemPrompt, buildSystemPromptWithThinkTool, buildSystemPromptWithTools, + replacePromptVariables, SYSTEM_PROMPT, THINK_TOOL_PROMPT, ToolUseExamples @@ -130,7 +130,7 @@ describe('prompt', () => { - 用户名称: {{username}}; ` const assistant = createMockAssistant('MyAssistant', 'Super-Model-X') - const result = await buildSystemPrompt(userPrompt, assistant) + const result = await replacePromptVariables(userPrompt, assistant.model?.name) const expectedPrompt = ` 以下是一些辅助信息: - 日期和时间: ${mockDate.toLocaleString()}; @@ -148,13 +148,13 @@ describe('prompt', () => { mockApi.getAppInfo.mockRejectedValue(new Error('API Error')) const userPrompt = 'System: {{system}}, Architecture: {{arch}}' - const result = await buildSystemPrompt(userPrompt) + const result = await replacePromptVariables(userPrompt) const expectedPrompt = 'System: Unknown System, Architecture: Unknown Architecture' expect(result).toEqual(expectedPrompt) }) it('should handle non-string input gracefully', async () => { - const result = await buildSystemPrompt(null as any) + const result = await replacePromptVariables(null as any) expect(result).toBe(null) }) }) @@ -173,7 +173,7 @@ describe('prompt', () => { Instructions: Be helpful. ` const assistant = createMockAssistant('Test Assistant', 'Advanced-AI-Model') - basePrompt = await buildSystemPrompt(initialPrompt, assistant) + basePrompt = await replacePromptVariables(initialPrompt, assistant.model?.name) expectedBasePrompt = ` System Information: - Date: ${mockDate.toLocaleDateString()} @@ -212,7 +212,7 @@ describe('prompt', () => { describe('buildSystemPromptWithTools', () => { it('should build a full prompt for "prompt" toolUseMode', async () => { const assistant = createMockAssistant('Test Assistant', 'Advanced-AI-Model') - const basePrompt = await buildSystemPrompt('Be helpful.', assistant) + const basePrompt = await replacePromptVariables('Be helpful.', assistant.model?.name) const tools = [createMockTool('web_search', 'Search the web')] const finalPrompt = buildSystemPromptWithTools(basePrompt, tools) @@ -236,7 +236,7 @@ describe('prompt', () => { Instructions: Be helpful. ` const assistant = createMockAssistant('Test Assistant', 'Advanced-AI-Model') - const basePrompt = await buildSystemPrompt(initialPrompt, assistant) + const basePrompt = await replacePromptVariables(initialPrompt, assistant.model?.name) const expectedBasePrompt = ` System Information: - Date: ${mockDate.toLocaleDateString()} diff --git a/src/renderer/src/utils/prompt.ts b/src/renderer/src/utils/prompt.ts index 0d17eb0e52..fe5d9b462b 100644 --- a/src/renderer/src/utils/prompt.ts +++ b/src/renderer/src/utils/prompt.ts @@ -1,6 +1,6 @@ import { loggerService } from '@logger' import store from '@renderer/store' -import { Assistant, MCPTool } from '@renderer/types' +import { MCPTool } from '@renderer/types' const logger = loggerService.withContext('Utils:Prompt') @@ -196,71 +196,90 @@ ${availableTools} ` } -export const buildSystemPrompt = async (userSystemPrompt: string, assistant?: Assistant): Promise => { - if (typeof userSystemPrompt === 'string') { - const now = new Date() - if (userSystemPrompt.includes('{{date}}')) { - const date = now.toLocaleDateString() - userSystemPrompt = userSystemPrompt.replace(/{{date}}/g, date) - } +const supportedVariables = [ + '{{username}}', + '{{date}}', + '{{time}}', + '{{datetime}}', + '{{system}}', + '{{language}}', + '{{arch}}', + '{{model_name}}' +] - if (userSystemPrompt.includes('{{time}}')) { - const time = now.toLocaleTimeString() - userSystemPrompt = userSystemPrompt.replace(/{{time}}/g, time) - } +export const containsSupportedVariables = (userSystemPrompt: string): boolean => { + return supportedVariables.some((variable) => userSystemPrompt.includes(variable)) +} - if (userSystemPrompt.includes('{{datetime}}')) { - const datetime = now.toLocaleString() - userSystemPrompt = userSystemPrompt.replace(/{{datetime}}/g, datetime) - } +export const replacePromptVariables = async (userSystemPrompt: string, modelName?: string): Promise => { + if (typeof userSystemPrompt !== 'string') { + logger.warn('User system prompt is not a string:', userSystemPrompt) + return userSystemPrompt + } - if (userSystemPrompt.includes('{{system}}')) { - try { - const systemType = await window.api.system.getDeviceType() - userSystemPrompt = userSystemPrompt.replace(/{{system}}/g, systemType) - } catch (error) { - logger.error('Failed to get system type:', error as Error) - userSystemPrompt = userSystemPrompt.replace(/{{system}}/g, 'Unknown System') - } - } + const now = new Date() + if (userSystemPrompt.includes('{{date}}')) { + const date = now.toLocaleDateString() + userSystemPrompt = userSystemPrompt.replace(/{{date}}/g, date) + } - if (userSystemPrompt.includes('{{language}}')) { - try { - const language = store.getState().settings.language - userSystemPrompt = userSystemPrompt.replace(/{{language}}/g, language) - } catch (error) { - logger.error('Failed to get language:', error as Error) - userSystemPrompt = userSystemPrompt.replace(/{{language}}/g, 'Unknown System Language') - } - } + if (userSystemPrompt.includes('{{time}}')) { + const time = now.toLocaleTimeString() + userSystemPrompt = userSystemPrompt.replace(/{{time}}/g, time) + } - if (userSystemPrompt.includes('{{arch}}')) { - try { - const appInfo = await window.api.getAppInfo() - userSystemPrompt = userSystemPrompt.replace(/{{arch}}/g, appInfo.arch) - } catch (error) { - logger.error('Failed to get architecture:', error as Error) - userSystemPrompt = userSystemPrompt.replace(/{{arch}}/g, 'Unknown Architecture') - } - } + if (userSystemPrompt.includes('{{datetime}}')) { + const datetime = now.toLocaleString() + userSystemPrompt = userSystemPrompt.replace(/{{datetime}}/g, datetime) + } - if (userSystemPrompt.includes('{{model_name}}')) { - try { - userSystemPrompt = userSystemPrompt.replace(/{{model_name}}/g, assistant?.model?.name || 'Unknown Model') - } catch (error) { - logger.error('Failed to get model name:', error as Error) - userSystemPrompt = userSystemPrompt.replace(/{{model_name}}/g, 'Unknown Model') - } + if (userSystemPrompt.includes('{{username}}')) { + try { + const userName = store.getState().settings.userName || 'Unknown Username' + userSystemPrompt = userSystemPrompt.replace(/{{username}}/g, userName) + } catch (error) { + logger.error('Failed to get username:', error as Error) + userSystemPrompt = userSystemPrompt.replace(/{{username}}/g, 'Unknown Username') } + } - if (userSystemPrompt.includes('{{username}}')) { - try { - const username = store.getState().settings.userName || 'Unknown Username' - userSystemPrompt = userSystemPrompt.replace(/{{username}}/g, username) - } catch (error) { - logger.error('Failed to get username:', error as Error) - userSystemPrompt = userSystemPrompt.replace(/{{username}}/g, 'Unknown Username') - } + if (userSystemPrompt.includes('{{system}}')) { + try { + const systemType = await window.api.system.getDeviceType() + userSystemPrompt = userSystemPrompt.replace(/{{system}}/g, systemType) + } catch (error) { + logger.error('Failed to get system type:', error as Error) + userSystemPrompt = userSystemPrompt.replace(/{{system}}/g, 'Unknown System') + } + } + + if (userSystemPrompt.includes('{{language}}')) { + try { + const language = store.getState().settings.language + userSystemPrompt = userSystemPrompt.replace(/{{language}}/g, language) + } catch (error) { + logger.error('Failed to get language:', error as Error) + userSystemPrompt = userSystemPrompt.replace(/{{language}}/g, 'Unknown System Language') + } + } + + if (userSystemPrompt.includes('{{arch}}')) { + try { + const appInfo = await window.api.getAppInfo() + userSystemPrompt = userSystemPrompt.replace(/{{arch}}/g, appInfo.arch) + } catch (error) { + logger.error('Failed to get architecture:', error as Error) + userSystemPrompt = userSystemPrompt.replace(/{{arch}}/g, 'Unknown Architecture') + } + } + + if (userSystemPrompt.includes('{{model_name}}')) { + try { + const name = modelName || store.getState().llm.defaultModel?.name + userSystemPrompt = userSystemPrompt.replace(/{{model_name}}/g, name) + } catch (error) { + logger.error('Failed to get model name:', error as Error) + userSystemPrompt = userSystemPrompt.replace(/{{model_name}}/g, 'Unknown Model') } }