mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-26 03:31:24 +08:00
fix(prompt): resolve variable replacement in function mode and add UI features (#6581)
* 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 <wangan.cs@gmail.com> Co-authored-by: icarus <eurfelux@gmail.com>
This commit is contained in:
parent
e634279481
commit
10b7c70a59
34
src/renderer/src/hooks/usePromptProcessor.ts
Normal file
34
src/renderer/src/hooks/usePromptProcessor.ts
Normal file
@ -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
|
||||
}
|
||||
@ -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<Props> = ({ 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 (
|
||||
<Container className="system-prompt" onClick={() => AssistantSettingsPopup.show({ assistant })} $isDark={isDark}>
|
||||
<Text>{prompt}</Text>
|
||||
<Text $isVisible={isVisible}>{displayText}</Text>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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<Props> = ({ 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<Props> = ({ assistant, updateAssistant }
|
||||
<TextAreaContainer>
|
||||
{showMarkdown ? (
|
||||
<MarkdownContainer className="markdown" onClick={() => setShowMarkdown(false)}>
|
||||
<ReactMarkdown>{prompt}</ReactMarkdown>
|
||||
<ReactMarkdown>{processedPrompt || prompt}</ReactMarkdown>
|
||||
<div style={{ height: '30px' }} />
|
||||
</MarkdownContainer>
|
||||
) : (
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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()}
|
||||
|
||||
@ -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}
|
||||
</tools>`
|
||||
}
|
||||
|
||||
export const buildSystemPrompt = async (userSystemPrompt: string, assistant?: Assistant): Promise<string> => {
|
||||
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<string> => {
|
||||
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')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user