Compare commits
5 Commits
v1.1.18
...
feat/varia
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
370cfd6e9f | ||
|
|
d213bc1024 | ||
|
|
1187a47698 | ||
|
|
83d0eb07aa | ||
|
|
8f6bf11320 |
@@ -83,8 +83,7 @@ afterPack: scripts/after-pack.js
|
||||
afterSign: scripts/notarize.js
|
||||
releaseInfo:
|
||||
releaseNotes: |
|
||||
新增助手级别的 MCP 能力支持
|
||||
改进 MCP 包的安装与管理
|
||||
导出单条消息时,增加生成消息标题的功能
|
||||
快捷助手窗口新增钉住和调整大小的功能
|
||||
小程序现在支持显示、复制当前实际 URL
|
||||
小程序支持多开
|
||||
支持 GPT-4o 图像生成
|
||||
修复 MCP 服务器无法使用问题
|
||||
修复升级导致旧版本数据丢失问题
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "CherryStudio",
|
||||
"version": "1.1.18",
|
||||
"version": "1.1.17",
|
||||
"private": true,
|
||||
"description": "A powerful AI assistant for producer.",
|
||||
"main": "./out/main/index.js",
|
||||
|
||||
@@ -366,7 +366,6 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
afterClose={onClose}
|
||||
width={600}
|
||||
transitionName="ant-move-down"
|
||||
styles={{
|
||||
content: {
|
||||
|
||||
103
src/renderer/src/components/VariableList.tsx
Normal file
103
src/renderer/src/components/VariableList.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import { DeleteOutlined, ImportOutlined } from '@ant-design/icons'
|
||||
import { VStack } from '@renderer/components/Layout'
|
||||
import { Variable } from '@renderer/types'
|
||||
import { Button, Input, Tooltip } from 'antd'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface VariableListProps {
|
||||
variables: Variable[]
|
||||
setVariables: (variables: Variable[]) => void
|
||||
onUpdate?: (variables: Variable[]) => void
|
||||
onInsertVariable?: (name: string) => void
|
||||
}
|
||||
|
||||
const VariableList: React.FC<VariableListProps> = ({ variables, setVariables, onUpdate, onInsertVariable }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const deleteVariable = (id: string) => {
|
||||
const updatedVariables = variables.filter((v) => v.id !== id)
|
||||
setVariables(updatedVariables)
|
||||
|
||||
if (onUpdate) {
|
||||
onUpdate(updatedVariables)
|
||||
}
|
||||
}
|
||||
|
||||
const updateVariable = (id: string, field: 'name' | 'value', value: string) => {
|
||||
// Only update the local state when typing, don't call the parent's onUpdate
|
||||
const updatedVariables = variables.map((v) => (v.id === id ? { ...v, [field]: value } : v))
|
||||
setVariables(updatedVariables)
|
||||
}
|
||||
|
||||
// This function will be called when input loses focus
|
||||
const handleInputBlur = () => {
|
||||
if (onUpdate) {
|
||||
onUpdate(variables)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<VariablesContainer>
|
||||
{variables.length === 0 ? (
|
||||
<EmptyText>{t('common.no_variables_added')}</EmptyText>
|
||||
) : (
|
||||
<VStack gap={8} width="100%">
|
||||
{variables.map((variable) => (
|
||||
<VariableItem key={variable.id}>
|
||||
<Input
|
||||
placeholder={t('common.variable_name')}
|
||||
value={variable.name}
|
||||
onChange={(e) => updateVariable(variable.id, 'name', e.target.value)}
|
||||
onBlur={handleInputBlur}
|
||||
style={{ width: '30%' }}
|
||||
/>
|
||||
<Input
|
||||
placeholder={t('common.value')}
|
||||
value={variable.value}
|
||||
onChange={(e) => updateVariable(variable.id, 'value', e.target.value)}
|
||||
onBlur={handleInputBlur}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
{onInsertVariable && (
|
||||
<Tooltip title={t('common.insert_variable_into_prompt')}>
|
||||
<Button type="text" onClick={() => onInsertVariable(variable.name)}>
|
||||
<ImportOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Button type="text" danger icon={<DeleteOutlined />} onClick={() => deleteVariable(variable.id)} />
|
||||
</VariableItem>
|
||||
))}
|
||||
</VStack>
|
||||
)}
|
||||
</VariablesContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const VariablesContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
max-height: 200px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
`
|
||||
|
||||
const VariableItem = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
const EmptyText = styled.div`
|
||||
color: var(--color-text-2);
|
||||
opacity: 0.6;
|
||||
font-style: italic;
|
||||
`
|
||||
|
||||
export default VariableList
|
||||
@@ -1,5 +1,6 @@
|
||||
import ThreeMinTopAppLogo from '@renderer/assets/images/apps/3mintop.png?url'
|
||||
import AbacusLogo from '@renderer/assets/images/apps/abacus.webp?url'
|
||||
import AIStudioLogo from '@renderer/assets/images/apps/aistudio.svg?url'
|
||||
import BaiduAiAppLogo from '@renderer/assets/images/apps/baidu-ai.png?url'
|
||||
import BaiduAiSearchLogo from '@renderer/assets/images/apps/baidu-ai-search.webp?url'
|
||||
import BaicuanAppLogo from '@renderer/assets/images/apps/baixiaoying.webp?url'
|
||||
@@ -307,6 +308,12 @@ export const DEFAULT_MIN_APPS: MinAppType[] = [
|
||||
url: 'https://3min.top',
|
||||
bodered: false
|
||||
},
|
||||
{
|
||||
id: 'aistudio',
|
||||
name: 'AI Studio',
|
||||
logo: AIStudioLogo,
|
||||
url: 'https://aistudio.google.com/'
|
||||
},
|
||||
{
|
||||
id: 'xiaoyi',
|
||||
name: '小艺',
|
||||
|
||||
@@ -286,7 +286,13 @@
|
||||
"select": "Select",
|
||||
"topics": "Topics",
|
||||
"warning": "Warning",
|
||||
"you": "You"
|
||||
"you": "You",
|
||||
"variable_name": "Variable Name",
|
||||
"value": "Value",
|
||||
"no_variables_added": "No variables added",
|
||||
"insert_variable_into_prompt": "Insert variable into prompt",
|
||||
"variables": "Variables",
|
||||
"variables_help": "Add variables that need to be replaced in the text, triggered by {{variable_name}} in the replacement document"
|
||||
},
|
||||
"docs": {
|
||||
"title": "Docs"
|
||||
|
||||
@@ -286,7 +286,13 @@
|
||||
"select": "選択",
|
||||
"topics": "トピック",
|
||||
"warning": "警告",
|
||||
"you": "あなた"
|
||||
"you": "あなた",
|
||||
"variable_name": "変数名",
|
||||
"value": "値",
|
||||
"no_variables_added": "変数がありません",
|
||||
"insert_variable_into_prompt": "プロンプトに変数を挿入",
|
||||
"variables": "変数",
|
||||
"variables_help": "テキスト内で置換が必要な変数を追加し、置換ドキュメント内で{{variable_name}}の形式でトリガーします"
|
||||
},
|
||||
"docs": {
|
||||
"title": "ドキュメント"
|
||||
|
||||
@@ -286,7 +286,13 @@
|
||||
"select": "Выбрать",
|
||||
"topics": "Топики",
|
||||
"warning": "Предупреждение",
|
||||
"you": "Вы"
|
||||
"you": "Вы",
|
||||
"variable_name": "Имя переменной",
|
||||
"value": "Значение",
|
||||
"no_variables_added": "Нет переменных",
|
||||
"insert_variable_into_prompt": "Вставить переменную в промпт",
|
||||
"variables": "Переменные",
|
||||
"variables_help": "Добавьте переменные, которые нужно заменить в тексте, замена срабатывает в формате {{variable_name}} в документе замены"
|
||||
},
|
||||
"docs": {
|
||||
"title": "Документация"
|
||||
|
||||
@@ -286,7 +286,13 @@
|
||||
"select": "选择",
|
||||
"topics": "话题",
|
||||
"warning": "警告",
|
||||
"you": "用户"
|
||||
"you": "用户",
|
||||
"variable_name": "变量名称",
|
||||
"value": "值",
|
||||
"no_variables_added": "没有添加变量",
|
||||
"insert_variable_into_prompt": "插入变量到提示词",
|
||||
"variables": "变量",
|
||||
"variables_help": "添加需要替换的变量名字和值即可"
|
||||
},
|
||||
"docs": {
|
||||
"title": "帮助文档"
|
||||
|
||||
@@ -286,7 +286,13 @@
|
||||
"select": "選擇",
|
||||
"topics": "話題",
|
||||
"warning": "警告",
|
||||
"you": "您"
|
||||
"you": "您",
|
||||
"variable_name": "變量名稱",
|
||||
"value": "值",
|
||||
"no_variables_added": "沒有添加變量",
|
||||
"insert_variable_into_prompt": "插入變量到提示詞",
|
||||
"variables": "變量",
|
||||
"variables_help": "添加需要替換的變量名字和值即可"
|
||||
},
|
||||
"docs": {
|
||||
"title": "說明文件"
|
||||
|
||||
@@ -182,8 +182,10 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
}
|
||||
|
||||
if (isFunctionCallingModel(model)) {
|
||||
if (!isEmpty(enabledMCPs) && !isEmpty(activedMcpServers)) {
|
||||
userMessage.enabledMCPs = activedMcpServers.filter((server) => enabledMCPs?.some((s) => s.id === server.id))
|
||||
if (!isEmpty(assistant.mcpServers) && !isEmpty(activedMcpServers)) {
|
||||
userMessage.enabledMCPs = activedMcpServers.filter((server) =>
|
||||
assistant.mcpServers?.some((s) => s.id === server.id)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,6 +208,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
console.error('Failed to send message:', error)
|
||||
}
|
||||
}, [
|
||||
activedMcpServers,
|
||||
assistant,
|
||||
dispatch,
|
||||
files,
|
||||
@@ -216,9 +219,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
resizeTextArea,
|
||||
selectedKnowledgeBases,
|
||||
text,
|
||||
topic,
|
||||
enabledMCPs,
|
||||
activedMcpServers
|
||||
topic
|
||||
])
|
||||
|
||||
const translate = async () => {
|
||||
|
||||
@@ -255,15 +255,19 @@ const ChatNavigation: FC<ChatNavigationProps> = ({ containerId }) => {
|
||||
if (now - lastMoveTime.current < 50) return
|
||||
lastMoveTime.current = now
|
||||
|
||||
const triggerWidth = 10
|
||||
let rightOffset = 5
|
||||
// Calculate if the mouse is in the trigger area
|
||||
const triggerWidth = 80 // Same as the width in styled component
|
||||
|
||||
// Safe way to calculate position when using calc expressions
|
||||
let rightOffset = 16 // Default right offset
|
||||
if (showRightTopics) {
|
||||
rightOffset = 5 + 300
|
||||
// When topics are shown on right, we need to account for topic list width
|
||||
rightOffset = 16 + 300 // Assuming topic list width is 300px, adjust if different
|
||||
}
|
||||
|
||||
const rightPosition = window.innerWidth - rightOffset - triggerWidth
|
||||
const topPosition = window.innerHeight * 0.35
|
||||
const height = window.innerHeight * 0.3
|
||||
const topPosition = window.innerHeight * 0.3 // 30% from top
|
||||
const height = window.innerHeight * 0.4 // 40% of window height
|
||||
|
||||
const isInTriggerArea =
|
||||
e.clientX > rightPosition &&
|
||||
@@ -403,32 +407,31 @@ const ButtonGroup = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-color);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
backdrop-filter: blur(12px);
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid var(--color-border);
|
||||
`
|
||||
|
||||
const NavigationButton = styled(Button)`
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
color: var(--color-text);
|
||||
transition: all 0.25s ease-in-out;
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-hover);
|
||||
color: var(--color-primary);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.anticon {
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SearchOutlined } from '@ant-design/icons'
|
||||
import { FormOutlined, SearchOutlined } from '@ant-design/icons'
|
||||
import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import MinAppsPopover from '@renderer/components/Popups/MinAppsPopover'
|
||||
@@ -64,10 +64,10 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
|
||||
<i className="iconfont icon-hide-sidebar" />
|
||||
</NavbarIcon>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('chat.assistant.search.placeholder')} mouseEnterDelay={0.8}>
|
||||
<NarrowIcon onClick={() => SearchPopup.show()}>
|
||||
<SearchOutlined />
|
||||
</NarrowIcon>
|
||||
<Tooltip title={t('settings.shortcuts.new_topic')} mouseEnterDelay={0.8}>
|
||||
<NavbarIcon onClick={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)}>
|
||||
<FormOutlined />
|
||||
</NavbarIcon>
|
||||
</Tooltip>
|
||||
</NavbarLeft>
|
||||
)}
|
||||
@@ -86,6 +86,11 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
|
||||
</HStack>
|
||||
<HStack alignItems="center" gap={8}>
|
||||
<UpdateAppButton />
|
||||
<Tooltip title={t('chat.assistant.search.placeholder')} mouseEnterDelay={0.8}>
|
||||
<NarrowIcon onClick={() => SearchPopup.show()}>
|
||||
<SearchOutlined />
|
||||
</NarrowIcon>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('navbar.expand')} mouseEnterDelay={0.8}>
|
||||
<NarrowIcon onClick={handleNarrowModeToggle}>
|
||||
<i className="iconfont icon-icon-adaptive-width"></i>
|
||||
|
||||
@@ -84,6 +84,9 @@ const AssistantItem: FC<AssistantItemProps> = ({ assistant, isActive, onSwitch,
|
||||
const agent = omit(assistant, ['model', 'emoji'])
|
||||
agent.id = uuid()
|
||||
agent.type = 'agent'
|
||||
if (assistant.promptVariables) {
|
||||
agent.promptVariables = [...assistant.promptVariables]
|
||||
}
|
||||
addAgent(agent)
|
||||
window.message.success({
|
||||
content: t('assistants.save.success'),
|
||||
|
||||
@@ -204,13 +204,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
||||
allowClear: true
|
||||
}
|
||||
})
|
||||
|
||||
prompt !== null &&
|
||||
(() => {
|
||||
const updatedTopic = { ...topic, prompt: prompt.trim() }
|
||||
updateTopic(updatedTopic)
|
||||
topic.id === activeTopic.id && setActiveTopic(updatedTopic)
|
||||
})()
|
||||
prompt !== null && updateTopic({ ...topic, prompt: prompt.trim() })
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -345,18 +339,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
||||
|
||||
return menus
|
||||
},
|
||||
[
|
||||
t,
|
||||
assistants,
|
||||
assistant,
|
||||
updateTopic,
|
||||
activeTopic.id,
|
||||
setActiveTopic,
|
||||
onPinTopic,
|
||||
onClearMessages,
|
||||
onMoveTopic,
|
||||
onDeleteTopic
|
||||
]
|
||||
[assistant, assistants, onClearMessages, onDeleteTopic, onPinTopic, onMoveTopic, t, updateTopic]
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,32 +1,35 @@
|
||||
import 'emoji-picker-element'
|
||||
|
||||
import { CloseCircleFilled } from '@ant-design/icons'
|
||||
import { CloseCircleFilled, PlusOutlined } from '@ant-design/icons'
|
||||
import EmojiPicker from '@renderer/components/EmojiPicker'
|
||||
import { Box, HSpaceBetweenStack, HStack } from '@renderer/components/Layout'
|
||||
import { Box, HStack } from '@renderer/components/Layout'
|
||||
import VariableList from '@renderer/components/VariableList'
|
||||
import { estimateTextTokens } from '@renderer/services/TokenService'
|
||||
import { Assistant, AssistantSettings } from '@renderer/types'
|
||||
import { Assistant, AssistantSettings, Variable } from '@renderer/types'
|
||||
import { getLeadingEmoji } from '@renderer/utils'
|
||||
import { Button, Input, Popover } from 'antd'
|
||||
import { Button, Input, Popover, Tooltip, Typography } from 'antd'
|
||||
import TextArea from 'antd/es/input/TextArea'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import styled from 'styled-components'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
interface Props {
|
||||
assistant: Assistant
|
||||
updateAssistant: (assistant: Assistant) => void
|
||||
updateAssistantSettings?: (settings: AssistantSettings) => void
|
||||
onOk?: () => void
|
||||
updateAssistantSettings: (settings: AssistantSettings) => void
|
||||
onOk: () => void
|
||||
}
|
||||
|
||||
const AssistantPromptSettings: React.FC<Props> = ({ assistant, updateAssistant }) => {
|
||||
const AssistantPromptSettings: React.FC<Props> = ({ assistant, updateAssistant, onOk }) => {
|
||||
const [emoji, setEmoji] = useState(getLeadingEmoji(assistant.name) || assistant.emoji)
|
||||
const [name, setName] = useState(assistant.name.replace(getLeadingEmoji(assistant.name) || '', '').trim())
|
||||
const [prompt, setPrompt] = useState(assistant.prompt)
|
||||
const [tokenCount, setTokenCount] = useState(0)
|
||||
const [variables, setVariables] = useState<Variable[]>(assistant.promptVariables || [])
|
||||
const [variableName, setVariableName] = useState('')
|
||||
const [variableValue, setVariableValue] = useState('')
|
||||
const { t } = useTranslation()
|
||||
const [showMarkdown, setShowMarkdown] = useState(prompt.length > 0)
|
||||
|
||||
useEffect(() => {
|
||||
const updateTokenCount = async () => {
|
||||
@@ -37,19 +40,77 @@ const AssistantPromptSettings: React.FC<Props> = ({ assistant, updateAssistant }
|
||||
}, [prompt])
|
||||
|
||||
const onUpdate = () => {
|
||||
const _assistant = { ...assistant, name: name.trim(), emoji, prompt }
|
||||
const _assistant = {
|
||||
...assistant,
|
||||
name: name.trim(),
|
||||
emoji,
|
||||
prompt,
|
||||
promptVariables: variables
|
||||
}
|
||||
updateAssistant(_assistant)
|
||||
}
|
||||
|
||||
const handleEmojiSelect = (selectedEmoji: string) => {
|
||||
setEmoji(selectedEmoji)
|
||||
const _assistant = { ...assistant, name: name.trim(), emoji: selectedEmoji, prompt }
|
||||
const _assistant = {
|
||||
...assistant,
|
||||
name: name.trim(),
|
||||
emoji: selectedEmoji,
|
||||
prompt,
|
||||
promptVariables: variables
|
||||
}
|
||||
updateAssistant(_assistant)
|
||||
}
|
||||
|
||||
const handleEmojiDelete = () => {
|
||||
setEmoji('')
|
||||
const _assistant = { ...assistant, name: name.trim(), prompt, emoji: '' }
|
||||
const _assistant = {
|
||||
...assistant,
|
||||
name: name.trim(),
|
||||
prompt,
|
||||
emoji: '',
|
||||
promptVariables: variables
|
||||
}
|
||||
updateAssistant(_assistant)
|
||||
}
|
||||
|
||||
const handleUpdateVariables = (updatedVariables: Variable[]) => {
|
||||
const _assistant = {
|
||||
...assistant,
|
||||
name: name.trim(),
|
||||
emoji,
|
||||
prompt,
|
||||
promptVariables: updatedVariables
|
||||
}
|
||||
updateAssistant(_assistant)
|
||||
}
|
||||
|
||||
const handleInsertVariable = (varName: string) => {
|
||||
const insertText = `{{${varName}}}`
|
||||
setPrompt((prev) => prev + insertText)
|
||||
}
|
||||
|
||||
const addVariable = () => {
|
||||
if (!variableName.trim()) return
|
||||
|
||||
const newVar: Variable = {
|
||||
id: uuidv4(),
|
||||
name: variableName.trim(),
|
||||
value: variableValue.trim()
|
||||
}
|
||||
|
||||
const updatedVariables = [...variables, newVar]
|
||||
setVariables(updatedVariables)
|
||||
setVariableName('')
|
||||
setVariableValue('')
|
||||
|
||||
const _assistant = {
|
||||
...assistant,
|
||||
name: name.trim(),
|
||||
emoji,
|
||||
prompt,
|
||||
promptVariables: updatedVariables
|
||||
}
|
||||
updateAssistant(_assistant)
|
||||
}
|
||||
|
||||
@@ -94,39 +155,61 @@ const AssistantPromptSettings: React.FC<Props> = ({ assistant, updateAssistant }
|
||||
{t('common.prompt')}
|
||||
</Box>
|
||||
<TextAreaContainer>
|
||||
{showMarkdown ? (
|
||||
<MarkdownContainer onClick={() => setShowMarkdown(false)}>
|
||||
<ReactMarkdown className="markdown">{prompt}</ReactMarkdown>
|
||||
<div style={{ height: '30px' }} />
|
||||
</MarkdownContainer>
|
||||
) : (
|
||||
<TextArea
|
||||
rows={10}
|
||||
placeholder={t('common.assistant') + t('common.prompt')}
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
onBlur={() => {
|
||||
onUpdate()
|
||||
}}
|
||||
autoFocus={true}
|
||||
spellCheck={false}
|
||||
style={{ minHeight: 'calc(80vh - 200px)', maxHeight: 'calc(80vh - 200px)', paddingBottom: '30px' }}
|
||||
/>
|
||||
)}
|
||||
</TextAreaContainer>
|
||||
<HSpaceBetweenStack width="100%" justifyContent="flex-end" mt="10px">
|
||||
<TextArea
|
||||
rows={10}
|
||||
placeholder={t('common.assistant') + t('common.prompt')}
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
onBlur={onUpdate}
|
||||
spellCheck={false}
|
||||
style={{ minHeight: 'calc(80vh - 320px)', maxHeight: 'calc(80vh - 270px)' }}
|
||||
/>
|
||||
<TokenCount>Tokens: {tokenCount}</TokenCount>
|
||||
</TextAreaContainer>
|
||||
|
||||
{showMarkdown ? (
|
||||
<Button type="primary" onClick={() => setShowMarkdown(false)}>
|
||||
{t('common.edit')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button type="primary" onClick={() => setShowMarkdown(true)}>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
)}
|
||||
</HSpaceBetweenStack>
|
||||
<Box mt={12} mb={8}>
|
||||
<HStack justifyContent="space-between" alignItems="center">
|
||||
<Typography.Title level={5} style={{ margin: 0 }}>
|
||||
{t('common.variables')}
|
||||
</Typography.Title>
|
||||
<Tooltip title={t('common.variables_help')}>
|
||||
<Typography.Text type="secondary" style={{ fontSize: '12px', cursor: 'help' }}>
|
||||
?
|
||||
</Typography.Text>
|
||||
</Tooltip>
|
||||
</HStack>
|
||||
</Box>
|
||||
|
||||
<VariableList
|
||||
variables={variables}
|
||||
setVariables={setVariables}
|
||||
onUpdate={handleUpdateVariables}
|
||||
onInsertVariable={handleInsertVariable}
|
||||
/>
|
||||
|
||||
<HStack gap={8} width="100%" mt={8} mb={8}>
|
||||
<Input
|
||||
placeholder={t('common.variable_name')}
|
||||
value={variableName}
|
||||
onChange={(e) => setVariableName(e.target.value)}
|
||||
style={{ width: '30%' }}
|
||||
/>
|
||||
<Input
|
||||
placeholder={t('common.value')}
|
||||
value={variableValue}
|
||||
onChange={(e) => setVariableValue(e.target.value)}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={addVariable}>
|
||||
{t('common.add')}
|
||||
</Button>
|
||||
</HStack>
|
||||
|
||||
<HStack width="100%" justifyContent="flex-end" mt="10px">
|
||||
<Button type="primary" onClick={onOk}>
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
</HStack>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -154,19 +237,15 @@ const TextAreaContainer = styled.div`
|
||||
`
|
||||
|
||||
const TokenCount = styled.div`
|
||||
padding: 2px 2px;
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 8px;
|
||||
background-color: var(--color-background-soft);
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-2);
|
||||
user-select: none;
|
||||
`
|
||||
|
||||
const MarkdownContainer = styled.div`
|
||||
min-height: calc(80vh - 200px);
|
||||
max-height: calc(80vh - 200px);
|
||||
padding-right: 2px;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
`
|
||||
|
||||
export default AssistantPromptSettings
|
||||
|
||||
@@ -90,7 +90,8 @@ const AssistantSettingPopupContainer: React.FC<Props> = ({ resolve, tab, ...prop
|
||||
content: {
|
||||
padding: 0,
|
||||
overflow: 'hidden',
|
||||
background: 'var(--color-background)'
|
||||
background: 'var(--color-background)',
|
||||
border: `1px solid var(--color-frame-border)`
|
||||
},
|
||||
header: { padding: '10px 15px', borderBottom: '0.5px solid var(--color-border)', margin: 0 }
|
||||
}}
|
||||
@@ -99,7 +100,8 @@ const AssistantSettingPopupContainer: React.FC<Props> = ({ resolve, tab, ...prop
|
||||
centered>
|
||||
<HStack>
|
||||
<LeftMenu>
|
||||
<StyledMenu
|
||||
<Menu
|
||||
style={{ width: 220, padding: 5, background: 'transparent' }}
|
||||
defaultSelectedKeys={[tab || 'prompt']}
|
||||
mode="vertical"
|
||||
items={items}
|
||||
@@ -112,6 +114,7 @@ const AssistantSettingPopupContainer: React.FC<Props> = ({ resolve, tab, ...prop
|
||||
assistant={assistant}
|
||||
updateAssistant={updateAssistant}
|
||||
updateAssistantSettings={updateAssistantSettings}
|
||||
onOk={onOk}
|
||||
/>
|
||||
)}
|
||||
{menu === 'model' && (
|
||||
@@ -193,16 +196,6 @@ const StyledModal = styled(Modal)`
|
||||
}
|
||||
`
|
||||
|
||||
const StyledMenu = styled(Menu)`
|
||||
width: 220px;
|
||||
padding: 5px;
|
||||
background: transparent;
|
||||
margin-top: 2px;
|
||||
.ant-menu-item {
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
`
|
||||
|
||||
export default class AssistantSettingsPopup {
|
||||
static show(props: AssistantSettingPopupShowParams) {
|
||||
return new Promise<Assistant>((resolve) => {
|
||||
|
||||
@@ -93,17 +93,22 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
||||
.join('\n')
|
||||
: ''
|
||||
})
|
||||
}, [server, form])
|
||||
|
||||
useEffect(() => {
|
||||
const currentServerType = form.getFieldValue('serverType')
|
||||
if (currentServerType) {
|
||||
setServerType(currentServerType)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [form.getFieldValue('serverType')])
|
||||
}, [server])
|
||||
|
||||
const fetchTools = useCallback(async () => {
|
||||
// Watch the serverType field to update the form layout dynamically
|
||||
useEffect(() => {
|
||||
const type = form.getFieldValue('serverType')
|
||||
type && setServerType(type)
|
||||
}, [form])
|
||||
|
||||
// Load tools on initial mount if server is active
|
||||
useEffect(() => {
|
||||
fetchTools()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const fetchTools = async () => {
|
||||
if (server.isActive) {
|
||||
try {
|
||||
setLoadingServer(server.id)
|
||||
@@ -119,15 +124,7 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
||||
setLoadingServer(null)
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [server.id])
|
||||
|
||||
useEffect(() => {
|
||||
console.log('Loading tools for server:', server.id, 'Active:', server.isActive)
|
||||
if (server.isActive) {
|
||||
fetchTools()
|
||||
}
|
||||
}, [server.id, server.isActive, fetchTools])
|
||||
}
|
||||
|
||||
// Save the form data
|
||||
const onSave = async () => {
|
||||
@@ -244,6 +241,10 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
||||
[server, t]
|
||||
)
|
||||
|
||||
const onFormValuesChange = () => {
|
||||
setIsFormChanged(true)
|
||||
}
|
||||
|
||||
const formatError = (error: any) => {
|
||||
if (error.message.includes('32000')) {
|
||||
return t('settings.mcp.errors.32000')
|
||||
@@ -277,35 +278,6 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle toggling a tool on/off
|
||||
const handleToggleTool = useCallback(
|
||||
async (tool: MCPTool, enabled: boolean) => {
|
||||
// Create a new disabledTools array or use the existing one
|
||||
let disabledTools = [...(server.disabledTools || [])]
|
||||
|
||||
if (enabled) {
|
||||
// Remove tool from disabledTools if it's being enabled
|
||||
disabledTools = disabledTools.filter((name) => name !== tool.name)
|
||||
} else {
|
||||
// Add tool to disabledTools if it's being disabled
|
||||
if (!disabledTools.includes(tool.name)) {
|
||||
disabledTools.push(tool.name)
|
||||
}
|
||||
}
|
||||
|
||||
// Update the server with new disabledTools
|
||||
const updatedServer = {
|
||||
...server,
|
||||
disabledTools
|
||||
}
|
||||
|
||||
// Save the updated server configuration
|
||||
// await window.api.mcp.updateServer(updatedServer)
|
||||
updateMCPServer(updatedServer)
|
||||
},
|
||||
[server, updateMCPServer]
|
||||
)
|
||||
|
||||
return (
|
||||
<SettingContainer>
|
||||
<SettingGroup style={{ marginBottom: 0 }}>
|
||||
@@ -330,7 +302,7 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onValuesChange={() => setIsFormChanged(true)}
|
||||
onValuesChange={onFormValuesChange}
|
||||
style={{
|
||||
// height: 'calc(100vh - var(--navbar-height) - 315px)',
|
||||
overflowY: 'auto',
|
||||
@@ -408,7 +380,7 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
{server.isActive && <MCPToolsSection tools={tools} server={server} onToggleTool={handleToggleTool} />}
|
||||
{server.isActive && <MCPToolsSection tools={tools} />}
|
||||
</SettingGroup>
|
||||
</SettingContainer>
|
||||
)
|
||||
|
||||
@@ -1,27 +1,11 @@
|
||||
import { MCPServer, MCPTool } from '@renderer/types'
|
||||
import { Badge, Collapse, Descriptions, Empty, Flex, Switch, Tag, Tooltip, Typography } from 'antd'
|
||||
import { MCPTool } from '@renderer/types'
|
||||
import { Badge, Collapse, Descriptions, Empty, Flex, Tag, Tooltip, Typography } from 'antd'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface MCPToolsSectionProps {
|
||||
tools: MCPTool[]
|
||||
server: MCPServer
|
||||
onToggleTool: (tool: MCPTool, enabled: boolean) => void
|
||||
}
|
||||
|
||||
const MCPToolsSection = ({ tools, server, onToggleTool }: MCPToolsSectionProps) => {
|
||||
const MCPToolsSection = ({ tools }: { tools: MCPTool[] }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
// Check if a tool is enabled (not in the disabledTools array)
|
||||
const isToolEnabled = (tool: MCPTool) => {
|
||||
return !server.disabledTools?.includes(tool.name)
|
||||
}
|
||||
|
||||
// Handle tool toggle
|
||||
const handleToggle = (tool: MCPTool, checked: boolean) => {
|
||||
onToggleTool(tool, checked)
|
||||
}
|
||||
|
||||
// Render tool properties from the input schema
|
||||
const renderToolProperties = (tool: MCPTool) => {
|
||||
if (!tool.inputSchema?.properties) return null
|
||||
@@ -102,21 +86,18 @@ const MCPToolsSection = ({ tools, server, onToggleTool }: MCPToolsSectionProps)
|
||||
<Collapse.Panel
|
||||
key={tool.id}
|
||||
header={
|
||||
<Flex justify="space-between" align="center" style={{ width: '100%' }}>
|
||||
<Flex vertical align="flex-start">
|
||||
<Flex align="center" style={{ width: '100%' }}>
|
||||
<Typography.Text strong>{tool.name}</Typography.Text>
|
||||
<Typography.Text type="secondary" style={{ marginLeft: 8, fontSize: '12px' }}>
|
||||
{tool.id}
|
||||
</Typography.Text>
|
||||
</Flex>
|
||||
{tool.description && (
|
||||
<Typography.Text type="secondary" style={{ fontSize: '13px', marginTop: 4 }}>
|
||||
{tool.description}
|
||||
</Typography.Text>
|
||||
)}
|
||||
<Flex vertical align="flex-start" style={{ width: '100%' }}>
|
||||
<Flex align="center" style={{ width: '100%' }}>
|
||||
<Typography.Text strong>{tool.name}</Typography.Text>
|
||||
<Typography.Text type="secondary" style={{ marginLeft: 8, fontSize: '12px' }}>
|
||||
{tool.id}
|
||||
</Typography.Text>
|
||||
</Flex>
|
||||
<Switch checked={isToolEnabled(tool)} onChange={(checked) => handleToggle(tool, checked)} />
|
||||
{tool.description && (
|
||||
<Typography.Text type="secondary" style={{ fontSize: '13px', marginTop: 4 }}>
|
||||
{tool.description}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</Flex>
|
||||
}>
|
||||
<SelectableContent>{renderToolProperties(tool)}</SelectableContent>
|
||||
|
||||
@@ -44,10 +44,7 @@ export default class AnthropicProvider extends BaseProvider {
|
||||
this.sdk = new Anthropic({
|
||||
apiKey: this.apiKey,
|
||||
baseURL: this.getBaseURL(),
|
||||
dangerouslyAllowBrowser: true,
|
||||
defaultHeaders: {
|
||||
'anthropic-beta': 'output-128k-2025-02-19'
|
||||
}
|
||||
dangerouslyAllowBrowser: true
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import i18n from '@renderer/i18n'
|
||||
import store from '@renderer/store'
|
||||
import { setGenerating } from '@renderer/store/runtime'
|
||||
import { Assistant, MCPTool, Message, Model, Provider, Suggestion } from '@renderer/types'
|
||||
import { processPromptVariables } from '@renderer/utils'
|
||||
import { formatMessageError, isAbortError } from '@renderer/utils/error'
|
||||
import { withGenerateImage } from '@renderer/utils/formats'
|
||||
import { cloneDeep, findLast, isEmpty } from 'lodash'
|
||||
@@ -42,6 +43,14 @@ export async function fetchChatCompletion({
|
||||
let isFirstChunk = true
|
||||
let query = ''
|
||||
|
||||
// Process variables in the prompt if they exist
|
||||
if (assistant.promptVariables && assistant.promptVariables.length > 0) {
|
||||
assistant = {
|
||||
...assistant,
|
||||
prompt: processPromptVariables(assistant.prompt, assistant.promptVariables)
|
||||
}
|
||||
}
|
||||
|
||||
// Search web
|
||||
if (WebSearchService.isWebSearchEnabled() && assistant.enableWebSearch && assistant.model) {
|
||||
const webSearchParams = getOpenAIWebSearchParams(assistant, assistant.model)
|
||||
@@ -106,8 +115,8 @@ export async function fetchChatCompletion({
|
||||
if (enabledMCPs && enabledMCPs.length > 0) {
|
||||
for (const mcpServer of enabledMCPs) {
|
||||
const tools = await window.api.mcp.listTools(mcpServer)
|
||||
const availableTools = tools.filter((tool: any) => !mcpServer.disabledTools?.includes(tool.name))
|
||||
mcpTools.push(...availableTools)
|
||||
console.debug('tools', tools)
|
||||
mcpTools.push(...tools)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,25 +60,14 @@ export async function reset() {
|
||||
}
|
||||
|
||||
// 备份到 webdav
|
||||
/**
|
||||
* @param autoBackupProcess
|
||||
* if call in auto backup process, not show any message, any error will be thrown
|
||||
*/
|
||||
export async function backupToWebdav({
|
||||
showMessage = false,
|
||||
customFileName = '',
|
||||
autoBackupProcess = false
|
||||
}: { showMessage?: boolean; customFileName?: string; autoBackupProcess?: boolean } = {}) {
|
||||
customFileName = ''
|
||||
}: { showMessage?: boolean; customFileName?: string } = {}) {
|
||||
if (isManualBackupRunning) {
|
||||
Logger.log('[Backup] Manual backup already in progress')
|
||||
return
|
||||
}
|
||||
// force set showMessage to false when auto backup process
|
||||
if (autoBackupProcess) {
|
||||
showMessage = false
|
||||
}
|
||||
|
||||
isManualBackupRunning = true
|
||||
|
||||
store.dispatch(setWebDAVSyncState({ syncing: true, lastSyncError: null }))
|
||||
|
||||
@@ -109,41 +98,25 @@ export async function backupToWebdav({
|
||||
lastSyncError: null
|
||||
})
|
||||
)
|
||||
if (showMessage && !autoBackupProcess) {
|
||||
window.message.success({ content: i18n.t('message.backup.success'), key: 'backup' })
|
||||
}
|
||||
showMessage && window.message.success({ content: i18n.t('message.backup.success'), key: 'backup' })
|
||||
} else {
|
||||
// if auto backup process, throw error
|
||||
if (autoBackupProcess) {
|
||||
throw new Error(i18n.t('message.backup.failed'))
|
||||
}
|
||||
|
||||
store.dispatch(setWebDAVSyncState({ lastSyncError: 'Backup failed' }))
|
||||
showMessage && window.message.error({ content: i18n.t('message.backup.failed'), key: 'backup' })
|
||||
window.message.error({ content: i18n.t('message.backup.failed'), key: 'backup' })
|
||||
}
|
||||
} catch (error: any) {
|
||||
// if auto backup process, throw error
|
||||
if (autoBackupProcess) {
|
||||
throw error
|
||||
}
|
||||
|
||||
store.dispatch(setWebDAVSyncState({ lastSyncError: error.message }))
|
||||
console.error('[Backup] backupToWebdav: Error uploading file to WebDAV:', error)
|
||||
showMessage &&
|
||||
window.modal.error({
|
||||
title: i18n.t('message.backup.failed'),
|
||||
content: error.message
|
||||
})
|
||||
throw error
|
||||
window.modal.error({
|
||||
title: i18n.t('message.backup.failed'),
|
||||
content: error.message
|
||||
})
|
||||
} finally {
|
||||
if (!autoBackupProcess) {
|
||||
store.dispatch(
|
||||
setWebDAVSyncState({
|
||||
lastSyncTime: Date.now(),
|
||||
syncing: false
|
||||
})
|
||||
)
|
||||
}
|
||||
store.dispatch(
|
||||
setWebDAVSyncState({
|
||||
lastSyncTime: Date.now(),
|
||||
syncing: false
|
||||
})
|
||||
)
|
||||
isManualBackupRunning = false
|
||||
}
|
||||
}
|
||||
@@ -176,7 +149,7 @@ let syncTimeout: NodeJS.Timeout | null = null
|
||||
let isAutoBackupRunning = false
|
||||
let isManualBackupRunning = false
|
||||
|
||||
export function startAutoSync(immediate = false) {
|
||||
export function startAutoSync() {
|
||||
if (autoSyncStarted) {
|
||||
return
|
||||
}
|
||||
@@ -192,15 +165,9 @@ export function startAutoSync(immediate = false) {
|
||||
|
||||
stopAutoSync()
|
||||
|
||||
scheduleNextBackup(immediate ? 'immediate' : 'fromLastSyncTime')
|
||||
scheduleNextBackup()
|
||||
|
||||
/**
|
||||
* @param type 'immediate' | 'fromLastSyncTime' | 'fromNow'
|
||||
* 'immediate', first backup right now
|
||||
* 'fromLastSyncTime', schedule next backup from last sync time
|
||||
* 'fromNow', schedule next backup from now
|
||||
*/
|
||||
function scheduleNextBackup(type: 'immediate' | 'fromLastSyncTime' | 'fromNow' = 'fromLastSyncTime') {
|
||||
function scheduleNextBackup() {
|
||||
if (syncTimeout) {
|
||||
clearTimeout(syncTimeout)
|
||||
syncTimeout = null
|
||||
@@ -218,15 +185,10 @@ export function startAutoSync(immediate = false) {
|
||||
// 用户指定的自动备份时间间隔(毫秒)
|
||||
const requiredInterval = webdavSyncInterval * 60 * 1000
|
||||
|
||||
let timeUntilNextSync = 1000 //also immediate
|
||||
switch (type) {
|
||||
case 'fromLastSyncTime': // 如果存在最后一次同步WebDAV的时间,以它为参考计算下一次同步的时间
|
||||
timeUntilNextSync = Math.max(1000, (webdavSync?.lastSyncTime || 0) + requiredInterval - Date.now())
|
||||
break
|
||||
case 'fromNow':
|
||||
timeUntilNextSync = requiredInterval
|
||||
break
|
||||
}
|
||||
// 如果存在最后一次同步WebDAV的时间,以它为参考计算下一次同步的时间
|
||||
const timeUntilNextSync = webdavSync?.lastSyncTime
|
||||
? Math.max(1000, webdavSync.lastSyncTime + requiredInterval - Date.now())
|
||||
: requiredInterval
|
||||
|
||||
syncTimeout = setTimeout(performAutoBackup, timeUntilNextSync)
|
||||
|
||||
@@ -245,62 +207,14 @@ export function startAutoSync(immediate = false) {
|
||||
}
|
||||
|
||||
isAutoBackupRunning = true
|
||||
const maxRetries = 4
|
||||
let retryCount = 0
|
||||
|
||||
while (retryCount < maxRetries) {
|
||||
try {
|
||||
console.log(`[AutoSync] Starting auto backup... (attempt ${retryCount + 1}/${maxRetries})`)
|
||||
|
||||
await backupToWebdav({ autoBackupProcess: true })
|
||||
|
||||
store.dispatch(
|
||||
setWebDAVSyncState({
|
||||
lastSyncError: null,
|
||||
lastSyncTime: Date.now(),
|
||||
syncing: false
|
||||
})
|
||||
)
|
||||
|
||||
isAutoBackupRunning = false
|
||||
scheduleNextBackup()
|
||||
|
||||
break
|
||||
} catch (error: any) {
|
||||
retryCount++
|
||||
if (retryCount === maxRetries) {
|
||||
console.error('[AutoSync] Auto backup failed after all retries:', error)
|
||||
|
||||
store.dispatch(
|
||||
setWebDAVSyncState({
|
||||
lastSyncError: 'Auto backup failed',
|
||||
lastSyncTime: Date.now(),
|
||||
syncing: false
|
||||
})
|
||||
)
|
||||
|
||||
//only show 1 time error modal, and autoback stopped until user click ok
|
||||
await window.modal.error({
|
||||
title: i18n.t('message.backup.failed'),
|
||||
content: `[WebDAV Auto Backup] ${new Date().toLocaleString()} ` + error.message
|
||||
})
|
||||
|
||||
scheduleNextBackup('fromNow')
|
||||
isAutoBackupRunning = false
|
||||
} else {
|
||||
//Exponential Backoff with Base 2: 7s、17s、37s
|
||||
const backoffDelay = Math.pow(2, retryCount - 1) * 10000 - 3000
|
||||
console.log(`[AutoSync] Failed, retry ${retryCount}/${maxRetries} after ${backoffDelay / 1000}s`)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, backoffDelay))
|
||||
|
||||
//in case auto backup is stopped by user
|
||||
if (!isAutoBackupRunning) {
|
||||
console.log('[AutoSync] retry cancelled by user, exit')
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
console.log('[AutoSync] Starting auto backup...')
|
||||
await backupToWebdav({ showMessage: false })
|
||||
} catch (error) {
|
||||
console.error('[AutoSync] Auto backup failed:', error)
|
||||
} finally {
|
||||
isAutoBackupRunning = false
|
||||
scheduleNextBackup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ export async function backupToNutstore({
|
||||
}
|
||||
}
|
||||
|
||||
export async function restoreFromNutstore(fileName?: string) {
|
||||
export async function restoreFromNutstore() {
|
||||
const nutstoreToken = getNutstoreToken()
|
||||
if (!nutstoreToken) {
|
||||
return
|
||||
@@ -137,7 +137,7 @@ export async function restoreFromNutstore(fileName?: string) {
|
||||
let data = ''
|
||||
|
||||
try {
|
||||
data = await window.api.backup.restoreFromWebdav({ ...config, fileName })
|
||||
data = await window.api.backup.restoreFromWebdav(config)
|
||||
} catch (error: any) {
|
||||
console.error('[backup] restoreFromWebdav: Error downloading file from WebDAV:', error)
|
||||
window.modal.error({
|
||||
|
||||
@@ -42,7 +42,7 @@ const persistedReducer = persistReducer(
|
||||
{
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 89,
|
||||
version: 88,
|
||||
blacklist: ['runtime', 'messages'],
|
||||
migrate
|
||||
},
|
||||
|
||||
@@ -11,7 +11,6 @@ const initialState: MCPConfig = {
|
||||
baseUrl: '',
|
||||
command: 'npx',
|
||||
args: ['-y', '@mcpmarket/mcp-auto-install', 'connect', '--json'],
|
||||
registryUrl: 'https://registry.npmmirror.com',
|
||||
env: {},
|
||||
isActive: false
|
||||
}
|
||||
|
||||
@@ -24,13 +24,6 @@ function removeMiniAppIconsFromState(state: RootState) {
|
||||
}
|
||||
}
|
||||
|
||||
function removeMiniAppFromState(state: RootState, id: string) {
|
||||
if (state.minapps) {
|
||||
state.minapps.enabled = state.minapps.enabled.filter((app) => app.id !== id)
|
||||
state.minapps.disabled = state.minapps.disabled.filter((app) => app.id !== id)
|
||||
}
|
||||
}
|
||||
|
||||
// add provider to state
|
||||
function addProvider(state: RootState, id: string) {
|
||||
if (!state.llm.providers.find((p) => p.id === id)) {
|
||||
@@ -1166,18 +1159,12 @@ const migrateConfig = {
|
||||
state.mcp.servers = [{ ...defaultServer, id: nanoid() }, ...state.mcp.servers]
|
||||
}
|
||||
}
|
||||
return state
|
||||
} catch (error) {
|
||||
return state
|
||||
}
|
||||
},
|
||||
'89': (state: RootState) => {
|
||||
try {
|
||||
removeMiniAppFromState(state, 'aistudio')
|
||||
return state
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return state
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ export type Assistant = {
|
||||
messages?: AssistantMessage[]
|
||||
enableWebSearch?: boolean
|
||||
enableGenerateImage?: boolean
|
||||
promptVariables?: Variable[]
|
||||
mcpServers?: MCPServer[]
|
||||
}
|
||||
|
||||
@@ -44,6 +45,12 @@ export type AssistantSettings = {
|
||||
reasoning_effort?: 'low' | 'medium' | 'high'
|
||||
}
|
||||
|
||||
export type Variable = {
|
||||
id: string
|
||||
name: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export type Agent = Omit<Assistant, 'model'>
|
||||
|
||||
export type Message = {
|
||||
@@ -372,7 +379,6 @@ export interface MCPServer {
|
||||
args?: string[]
|
||||
env?: Record<string, string>
|
||||
isActive: boolean
|
||||
disabledTools?: string[] // List of tool names that are disabled for this server
|
||||
}
|
||||
|
||||
export interface MCPToolInputSchema {
|
||||
|
||||
@@ -500,4 +500,23 @@ export function hasObjectKey(obj: any, key: string) {
|
||||
return Object.keys(obj).includes(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Process variables in a prompt string
|
||||
* @param prompt The prompt string containing variables in {{var_name}} format
|
||||
* @param variables Array of variables with name and value
|
||||
* @returns The prompt with variables replaced
|
||||
*/
|
||||
export function processPromptVariables(prompt: string, variables: Array<{ name: string; value: string }> = []) {
|
||||
if (!prompt || !variables || variables.length === 0) {
|
||||
return prompt
|
||||
}
|
||||
let processedPrompt = prompt
|
||||
variables.forEach((variable) => {
|
||||
const pattern = new RegExp(`{{${variable.name}}}`, 'g')
|
||||
processedPrompt = processedPrompt.replace(pattern, variable.value)
|
||||
})
|
||||
|
||||
return processedPrompt
|
||||
}
|
||||
|
||||
export { classNames }
|
||||
|
||||
@@ -245,7 +245,6 @@ export async function callMCPTool(tool: MCPTool): Promise<any> {
|
||||
command: resp.data.command,
|
||||
args: resp.data.args,
|
||||
env: resp.data.env,
|
||||
registryUrl: '',
|
||||
isActive: false
|
||||
}
|
||||
store.dispatch(addMCPServer(mcpServer))
|
||||
|
||||
Reference in New Issue
Block a user