Compare commits
9 Commits
v0.9.2
...
feat/plugi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebf61b1ce9 | ||
|
|
1a68587684 | ||
|
|
47c455b125 | ||
|
|
96124cf58e | ||
|
|
ef975add01 | ||
|
|
ed49066bab | ||
|
|
e7545c5a94 | ||
|
|
fc35df65b8 | ||
|
|
56ca81d245 |
47
build/nsis-installer.nsh
Normal file
47
build/nsis-installer.nsh
Normal file
@@ -0,0 +1,47 @@
|
||||
;Inspired by:
|
||||
; https://gist.github.com/bogdibota/062919938e1ed388b3db5ea31f52955c
|
||||
; https://stackoverflow.com/questions/34177547/detect-if-visual-c-redistributable-for-visual-studio-2013-is-installed
|
||||
; https://stackoverflow.com/a/54391388
|
||||
; https://github.com/GitCommons/cpp-redist-nsis/blob/main/installer.nsh
|
||||
|
||||
;Find latests downloads here:
|
||||
; https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist
|
||||
|
||||
!include LogicLib.nsh
|
||||
|
||||
; https://github.com/electron-userland/electron-builder/issues/1122
|
||||
!ifndef BUILD_UNINSTALLER
|
||||
Function checkVCRedist
|
||||
ReadRegDWORD $0 HKLM "SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64" "Installed"
|
||||
FunctionEnd
|
||||
!endif
|
||||
|
||||
!macro customInit
|
||||
Push $0
|
||||
Call checkVCRedist
|
||||
${If} $0 != "1"
|
||||
MessageBox MB_YESNO "\
|
||||
NOTE: ${PRODUCT_NAME} requires $\r$\n\
|
||||
'Microsoft Visual C++ Redistributable'$\r$\n\
|
||||
to function properly.$\r$\n$\r$\n\
|
||||
Download and install now?" /SD IDYES IDYES InstallVCRedist IDNO DontInstall
|
||||
InstallVCRedist:
|
||||
inetc::get /CAPTION " " /BANNER "Downloading Microsoft Visual C++ Redistributable..." "https://aka.ms/vs/17/release/vc_redist.x64.exe" "$TEMP\vc_redist.x64.exe"
|
||||
ExecWait "$TEMP\vc_redist.x64.exe /install /norestart"
|
||||
;IfErrors InstallError ContinueInstall ; vc_redist exit code is unreliable :(
|
||||
Call checkVCRedist
|
||||
${If} $0 == "1"
|
||||
Goto ContinueInstall
|
||||
${EndIf}
|
||||
|
||||
;InstallError:
|
||||
MessageBox MB_ICONSTOP "\
|
||||
There was an unexpected error installing$\r$\n\
|
||||
Microsoft Visual C++ Redistributable.$\r$\n\
|
||||
The installation of ${PRODUCT_NAME} cannot continue."
|
||||
DontInstall:
|
||||
Abort
|
||||
${EndIf}
|
||||
ContinueInstall:
|
||||
Pop $0
|
||||
!macroend
|
||||
@@ -39,6 +39,7 @@ nsis:
|
||||
createDesktopShortcut: always
|
||||
allowToChangeInstallationDirectory: true
|
||||
oneClick: false
|
||||
include: build/nsis-installer.nsh
|
||||
mac:
|
||||
entitlementsInherit: build/entitlements.mac.plist
|
||||
notarize: false
|
||||
@@ -77,11 +78,4 @@ afterPack: scripts/after-pack.js
|
||||
afterSign: scripts/notarize.js
|
||||
releaseInfo:
|
||||
releaseNotes: |
|
||||
Azure OpenAI 嵌入模型支持
|
||||
增加嵌入模型专用服务商 JINA
|
||||
支持选择图片和文件后不输入内容即可发送
|
||||
增加 GitHub Copilot 小程序
|
||||
模型编辑支持显示模型 ID
|
||||
模型选择按钮增加服务商显示
|
||||
修复话题移动到不同的助手后搜索结果不能定位问题
|
||||
修复输出包含 sub 无法正常显示问题
|
||||
增加 Genspark 小程序
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
"@types/tinycolor2": "^1",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"antd": "^5.22.5",
|
||||
"axios": "^1.7.3",
|
||||
"axios": "^1.7.9",
|
||||
"browser-image-compression": "^2.0.2",
|
||||
"dayjs": "^1.11.11",
|
||||
"dexie": "^4.0.8",
|
||||
|
||||
202
resources/cherry-studio/releases.html
Normal file
202
resources/cherry-studio/releases.html
Normal file
@@ -0,0 +1,202 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Github Releases Timeline</title>
|
||||
<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet" />
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/markdown-it@13.0.1/dist/markdown-it.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/typography@0.5.10/dist/typography.min.css"></script>
|
||||
</head>
|
||||
|
||||
<body id="app">
|
||||
<div :class="isDark ? 'dark-bg' : 'bg'" class="min-h-screen">
|
||||
<div class="max-w-3xl mx-auto py-12 px-4">
|
||||
<h1 class="text-3xl font-bold mb-8" :class="isDark ? 'text-white' : 'text-gray-900'">Release Timeline</h1>
|
||||
|
||||
<!-- Loading状态 -->
|
||||
<div v-if="loading" class="text-center py-8">
|
||||
<div class="inline-block animate-spin rounded-full h-8 w-8 border-4"
|
||||
:class="isDark ? 'border-gray-700 border-t-blue-500' : 'border-gray-300 border-t-blue-500'"></div>
|
||||
</div>
|
||||
|
||||
<!-- Error 状态 -->
|
||||
<div v-else-if="error" class="text-red-500 text-center py-8">{{ error }}</div>
|
||||
|
||||
<!-- Release 列表 -->
|
||||
<div v-else class="space-y-8">
|
||||
<div v-for="release in releases" :key="release.id" class="relative pl-8"
|
||||
:class="isDark ? 'border-l-2 border-gray-700' : 'border-l-2 border-gray-200'">
|
||||
<div class="absolute -left-2 top-0 w-4 h-4 rounded-full bg-green-500"></div>
|
||||
<div class="rounded-lg shadow-sm p-6 transition-shadow"
|
||||
:class="isDark ? 'bg-black hover:shadow-md hover:shadow-black' : 'bg-white hover:shadow-md'">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold" :class="isDark ? 'text-white' : 'text-gray-900'">
|
||||
{{ release.name || release.tag_name }}
|
||||
</h2>
|
||||
<p class="text-sm mt-1" :class="isDark ? 'text-gray-400' : 'text-gray-500'">
|
||||
{{ formatDate(release.published_at) }}
|
||||
</p>
|
||||
</div>
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium"
|
||||
:class="isDark ? 'bg-green-900 text-green-200' : 'bg-green-100 text-green-800'">
|
||||
{{ release.tag_name }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="prose" :class="isDark ? 'text-gray-300 dark-prose' : 'text-gray-600'"
|
||||
v-html="renderMarkdown(release.body)"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const md = window.markdownit({
|
||||
breaks: true,
|
||||
linkify: true
|
||||
})
|
||||
|
||||
const { createApp } = Vue
|
||||
|
||||
createApp({
|
||||
data() {
|
||||
return {
|
||||
releases: [],
|
||||
loading: true,
|
||||
error: null,
|
||||
isDark: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async fetchReleases() {
|
||||
try {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
const response = await fetch('https://api.github.com/repos/kangfenmao/cherry-studio/releases')
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch releases')
|
||||
}
|
||||
this.releases = await response.json()
|
||||
} catch (err) {
|
||||
this.error = 'Error loading releases: ' + err.message
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
formatDate(dateString) {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})
|
||||
},
|
||||
renderMarkdown(content) {
|
||||
if (!content) return ''
|
||||
return md.render(content)
|
||||
},
|
||||
initTheme() {
|
||||
// 从 URL 参数获取主题设置
|
||||
const url = new URL(window.location.href)
|
||||
const theme = url.searchParams.get('theme')
|
||||
this.isDark = theme === 'dark'
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initTheme()
|
||||
this.fetchReleases()
|
||||
}
|
||||
}).mount('#app')
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 基础的 Markdown 样式 */
|
||||
.prose {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.prose h1 {
|
||||
font-size: 1.5em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.prose h2 {
|
||||
font-size: 1.3em;
|
||||
margin: 0.8em 0;
|
||||
}
|
||||
|
||||
.prose h3 {
|
||||
font-size: 1.1em;
|
||||
margin: 0.6em 0;
|
||||
}
|
||||
|
||||
.prose ul {
|
||||
list-style-type: disc;
|
||||
margin-left: 1.5em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.prose ol {
|
||||
list-style-type: decimal;
|
||||
margin-left: 1.5em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.prose code {
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: 0.2em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.dark .prose code {
|
||||
background-color: #1f2937;
|
||||
}
|
||||
|
||||
.prose code {
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
|
||||
.prose pre code {
|
||||
display: block;
|
||||
padding: 1em;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.prose a {
|
||||
color: #3b82f6;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.dark .prose a {
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.prose blockquote {
|
||||
border-left: 4px solid #e5e7eb;
|
||||
padding-left: 1em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.dark .prose blockquote {
|
||||
border-left-color: #374151;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.dark .prose {
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
.dark-bg {
|
||||
background-color: #151515;
|
||||
}
|
||||
|
||||
.bg {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,7 +1,9 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import vm from 'node:vm'
|
||||
|
||||
import { Shortcut, ThemeMode } from '@types'
|
||||
import axios from 'axios'
|
||||
import { BrowserWindow, ipcMain, ProxyConfig, session, shell } from 'electron'
|
||||
import log from 'electron-log'
|
||||
|
||||
@@ -154,4 +156,10 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
ipcMain.handle('knowledge-base:add', KnowledgeService.add)
|
||||
ipcMain.handle('knowledge-base:remove', KnowledgeService.remove)
|
||||
ipcMain.handle('knowledge-base:search', KnowledgeService.search)
|
||||
|
||||
// vm
|
||||
ipcMain.handle('run-js', (_, code: string) => {
|
||||
const context = vm.createContext(Object.assign({ fetch: fetch, URL: URL, axios: axios }, global))
|
||||
return vm.runInContext(code, context)
|
||||
})
|
||||
}
|
||||
|
||||
3
src/preload/index.d.ts
vendored
3
src/preload/index.d.ts
vendored
@@ -76,6 +76,9 @@ declare global {
|
||||
remove: ({ uniqueId, base }: { uniqueId: string; base: KnowledgeBaseParams }) => Promise<void>
|
||||
search: ({ search, base }: { search: string; base: KnowledgeBaseParams }) => Promise<ExtractChunkData[]>
|
||||
}
|
||||
vm: {
|
||||
run: (code: string) => Promise<any>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,9 @@ const api = {
|
||||
ipcRenderer.invoke('knowledge-base:remove', { uniqueId, base }),
|
||||
search: ({ search, base }: { search: string; base: KnowledgeBaseParams }) =>
|
||||
ipcRenderer.invoke('knowledge-base:search', { search, base })
|
||||
},
|
||||
vm: {
|
||||
run: (code: string) => ipcRenderer.invoke('run-js', code)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BIN
src/renderer/src/assets/images/apps/genspark.jpg
Normal file
BIN
src/renderer/src/assets/images/apps/genspark.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.0 KiB |
@@ -4,6 +4,7 @@ import { TextAreaProps } from 'antd/lib/input'
|
||||
import { TextAreaRef } from 'antd/lib/input/TextArea'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { TopView } from '../TopView'
|
||||
|
||||
@@ -11,13 +12,14 @@ interface ShowParams {
|
||||
text: string
|
||||
textareaProps?: TextAreaProps
|
||||
modalProps?: ModalProps
|
||||
children?: (props: { onOk?: () => void; onCancel?: () => void }) => React.ReactNode
|
||||
}
|
||||
|
||||
interface Props extends ShowParams {
|
||||
resolve: (data: any) => void
|
||||
}
|
||||
|
||||
const PopupContainer: React.FC<Props> = ({ text, textareaProps, modalProps, resolve }) => {
|
||||
const PopupContainer: React.FC<Props> = ({ text, textareaProps, modalProps, resolve, children }) => {
|
||||
const [open, setOpen] = useState(true)
|
||||
const { t } = useTranslation()
|
||||
const [textValue, setTextValue] = useState(text)
|
||||
@@ -73,12 +75,17 @@ const PopupContainer: React.FC<Props> = ({ text, textareaProps, modalProps, reso
|
||||
onInput={resizeTextArea}
|
||||
onChange={(e) => setTextValue(e.target.value)}
|
||||
/>
|
||||
<ChildrenContainer>{children && children({ onOk, onCancel })}</ChildrenContainer>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const TopViewKey = 'TextEditPopup'
|
||||
|
||||
const ChildrenContainer = styled.div`
|
||||
position: relative;
|
||||
`
|
||||
|
||||
export default class TextEditPopup {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
|
||||
@@ -6,6 +6,7 @@ import DoubaoAppLogo from '@renderer/assets/images/apps/doubao.png'
|
||||
import DuckDuckGoAppLogo from '@renderer/assets/images/apps/duckduckgo.webp'
|
||||
import FeloAppLogo from '@renderer/assets/images/apps/felo.png'
|
||||
import GeminiAppLogo from '@renderer/assets/images/apps/gemini.png'
|
||||
import GensparkLogo from '@renderer/assets/images/apps/genspark.jpg'
|
||||
import GithubCopilotLogo from '@renderer/assets/images/apps/github-copilot.webp'
|
||||
import HuggingChatLogo from '@renderer/assets/images/apps/huggingchat.svg'
|
||||
import KimiAppLogo from '@renderer/assets/images/apps/kimi.jpg'
|
||||
@@ -230,6 +231,12 @@ const _apps: MinAppType[] = [
|
||||
name: 'GitHub Copilot',
|
||||
logo: GithubCopilotLogo,
|
||||
url: 'https://github.com/copilot'
|
||||
},
|
||||
{
|
||||
id: 'genspark',
|
||||
name: 'Genspark',
|
||||
logo: GensparkLogo,
|
||||
url: 'https://www.genspark.ai/'
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ export const AGENT_PROMPT = `
|
||||
`
|
||||
|
||||
export const SUMMARIZE_PROMPT =
|
||||
'你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,不要使用标点符号和其他特殊符号。'
|
||||
'你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,标题语言与用户的首要语言一致,不要使用标点符号和其他特殊符号'
|
||||
|
||||
export const TRANSLATE_PROMPT =
|
||||
'You are a translation expert. Translate from input language to {{target_language}}, provide the translation result directly without any explanation and keep original format. Do not translate if the target language is the same as the source language.'
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import store, { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import {
|
||||
SendMessageShortcut,
|
||||
setSendMessageShortcut as _setSendMessageShortcut,
|
||||
setTheme,
|
||||
SettingsState,
|
||||
setTopicPosition,
|
||||
setTray,
|
||||
setWindowStyle
|
||||
@@ -41,3 +42,7 @@ export function useMessageStyle() {
|
||||
isBubbleStyle
|
||||
}
|
||||
}
|
||||
|
||||
export const getStoreSetting = (key: keyof SettingsState) => {
|
||||
return store.getState().settings[key]
|
||||
}
|
||||
|
||||
@@ -112,7 +112,8 @@
|
||||
"topics.list": "Topic List",
|
||||
"topics.move_to": "Move to",
|
||||
"topics.title": "Topics",
|
||||
"translate": "Translate"
|
||||
"translate": "Translate",
|
||||
"resend": "Resend"
|
||||
},
|
||||
"common": {
|
||||
"and": "and",
|
||||
@@ -417,6 +418,7 @@
|
||||
"models.translate_model_prompt_title": "Translate Model Prompt",
|
||||
"models.topic_naming_model_setting_title": "Topic Naming Model Settings",
|
||||
"models.enable_topic_naming": "Topic Auto Naming",
|
||||
"models.topic_naming_prompt": "Topic Naming Prompt",
|
||||
"provider": {
|
||||
"add.name": "Provider Name",
|
||||
"add.name.placeholder": "Example: OpenAI",
|
||||
@@ -585,7 +587,18 @@
|
||||
"embedding": "Embedding",
|
||||
"embedding_model": "Embedding Model",
|
||||
"embedding_model_tooltip": "Add in Settings->Model Provider->Manage",
|
||||
"dimensions": "Dimensions {{dimensions}}"
|
||||
"dimensions": "Dimensions {{dimensions}}",
|
||||
"custom_parameters": "Custom Parameters",
|
||||
"add_parameter": "Add Parameter",
|
||||
"parameter_name": "Parameter Name",
|
||||
"parameter_type": {
|
||||
"string": "Text",
|
||||
"number": "Number",
|
||||
"boolean": "Boolean"
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"summarize": "You are an assistant who is good at conversation. You need to summarize the user's conversation into a title of 10 characters or less, ensuring it matches the user's primary language without using punctuation or other special symbols."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,8 @@
|
||||
"topics.list": "トピックリスト",
|
||||
"topics.move_to": "移動先",
|
||||
"topics.title": "トピック",
|
||||
"translate": "翻訳"
|
||||
"translate": "翻訳",
|
||||
"resend": "再送信"
|
||||
},
|
||||
"common": {
|
||||
"and": "と",
|
||||
@@ -415,6 +416,7 @@
|
||||
"models.translate_model_prompt_title": "翻訳モデルのプロンプト",
|
||||
"models.topic_naming_model_setting_title": "トピック命名モデルの設定",
|
||||
"models.enable_topic_naming": "トピックの自動命名",
|
||||
"models.topic_naming_prompt": "トピック命名プロンプト",
|
||||
"provider": {
|
||||
"add.name": "プロバイダー名",
|
||||
"add.name.placeholder": "例:OpenAI",
|
||||
@@ -567,7 +569,19 @@
|
||||
"free": "無料モデル",
|
||||
"embedding": "埋め込みモデル",
|
||||
"embedding_model": "埋め込みモデル",
|
||||
"embedding_model_tooltip": "設定->モデルサービス->管理で追加"
|
||||
"embedding_model_tooltip": "設定->モデルサービス->管理で追加",
|
||||
"dimensions": "{{dimensions}} 次元",
|
||||
"custom_parameters": "カスタムパラメータ",
|
||||
"add_parameter": "パラメータを追加",
|
||||
"parameter_name": "パラメータ名",
|
||||
"parameter_type": {
|
||||
"string": "テキスト",
|
||||
"number": "数値",
|
||||
"boolean": "真偽値"
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"summarize": "あなたは会話を得意とするアシスタントです。ユーザーの会話を10文字以内のタイトルに要約し、ユーザーの主言語と一致していることを確認してください。句読点や特殊記号は使用しないでください。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,8 @@
|
||||
"topics.list": "Список топиков",
|
||||
"topics.move_to": "Переместить в",
|
||||
"topics.title": "Топики",
|
||||
"translate": "Перевести"
|
||||
"translate": "Перевести",
|
||||
"resend": "Переотправить"
|
||||
},
|
||||
"common": {
|
||||
"and": "и",
|
||||
@@ -417,6 +418,7 @@
|
||||
"models.translate_model_prompt_title": "Модель перевода",
|
||||
"models.topic_naming_model_setting_title": "Настройки модели именования топика",
|
||||
"models.enable_topic_naming": "Автоматическое переименование топика",
|
||||
"models.topic_naming_prompt": "Подсказка для именования топика",
|
||||
"provider": {
|
||||
"add.name": "Имя провайдера",
|
||||
"add.name.placeholder": "Пример: OpenAI",
|
||||
@@ -585,7 +587,18 @@
|
||||
"embedding": "Встраиваемые модели",
|
||||
"embedding_model": "Встраиваемые модели",
|
||||
"embedding_model_tooltip": "Добавьте в настройки->модель сервиса->управление",
|
||||
"dimensions": "{{dimensions}} мер"
|
||||
"dimensions": "{{dimensions}} мер",
|
||||
"custom_parameters": "Пользовательские параметры",
|
||||
"add_parameter": "Добавить параметр",
|
||||
"parameter_name": "Имя параметра",
|
||||
"parameter_type": {
|
||||
"string": "Текст",
|
||||
"number": "Число",
|
||||
"boolean": "Логическое"
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"summarize": "Вы - эксперт в общении, который суммирует разговоры пользователя в 10-символьном заголовке, совпадающем с языком пользователя, без использования знаков препинания и других специальных символов"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,8 @@
|
||||
"topics.list": "话题列表",
|
||||
"topics.move_to": "移动到",
|
||||
"topics.title": "话题",
|
||||
"translate": "翻译"
|
||||
"translate": "翻译",
|
||||
"resend": "重新发送"
|
||||
},
|
||||
"common": {
|
||||
"and": "和",
|
||||
@@ -418,6 +419,7 @@
|
||||
"models.translate_model_prompt_title": "翻译模型提示词",
|
||||
"models.topic_naming_model_setting_title": "话题命名模型设置",
|
||||
"models.enable_topic_naming": "话题自动重命名",
|
||||
"models.topic_naming_prompt": "话题命名提示词",
|
||||
"provider": {
|
||||
"add.name": "提供商名称",
|
||||
"add.name.placeholder": "例如 OpenAI",
|
||||
@@ -574,7 +576,18 @@
|
||||
"embedding": "嵌入模型",
|
||||
"embedding_model": "嵌入模型",
|
||||
"embedding_model_tooltip": "在设置->模型服务中点击管理按钮添加",
|
||||
"dimensions": "{{dimensions}} 维"
|
||||
"dimensions": "{{dimensions}} 维",
|
||||
"custom_parameters": "自定义参数",
|
||||
"add_parameter": "添加参数",
|
||||
"parameter_name": "参数名称",
|
||||
"parameter_type": {
|
||||
"string": "文本",
|
||||
"number": "数字",
|
||||
"boolean": "布尔值"
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"summarize": "你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,标题语言与用户的首要语言一致,不要使用标点符号和其他特殊符号"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,8 @@
|
||||
"topics.list": "話題列表",
|
||||
"topics.move_to": "移動到",
|
||||
"topics.title": "話題",
|
||||
"translate": "翻譯"
|
||||
"translate": "翻譯",
|
||||
"resend": "重新發送"
|
||||
},
|
||||
"common": {
|
||||
"and": "與",
|
||||
@@ -167,7 +168,7 @@
|
||||
"conversation_details": "會話詳情",
|
||||
"conversation_history": "會話歷史",
|
||||
"created": "創建時間",
|
||||
"last_updated": "最後<EFBFBD><EFBFBD>新",
|
||||
"last_updated": "最後更新",
|
||||
"messages": "訊息數",
|
||||
"user": "用戶"
|
||||
},
|
||||
@@ -417,6 +418,7 @@
|
||||
"models.translate_model_prompt_title": "翻譯模型提示詞",
|
||||
"models.topic_naming_model_setting_title": "話題命名模型設定",
|
||||
"models.enable_topic_naming": "話題自動重命名",
|
||||
"models.topic_naming_prompt": "話題命名提示詞",
|
||||
"provider": {
|
||||
"add.name": "提供者名稱",
|
||||
"add.name.placeholder": "例如:OpenAI",
|
||||
@@ -573,7 +575,18 @@
|
||||
"embedding": "嵌入模型",
|
||||
"embedding_model": "嵌入模型",
|
||||
"embedding_model_tooltip": "在设置->模型服务中点击管理按钮添加",
|
||||
"dimensions": "{{dimensions}} 維"
|
||||
"dimensions": "{{dimensions}} 維",
|
||||
"custom_parameters": "自定義參數",
|
||||
"add_parameter": "添加參數",
|
||||
"parameter_name": "參數名稱",
|
||||
"parameter_type": {
|
||||
"string": "文字",
|
||||
"number": "數字",
|
||||
"boolean": "布林值"
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"summarize": "你是一名擅長會話的助理,你需要將用戶的會話總結為 10 個字以內的標題,標題語言與用戶的首要語言一致,不要使用標點符號和其他特殊符號"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,9 +86,12 @@ const MessageItem: FC<Props> = ({
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribes = [EventEmitter.on(EVENT_NAMES.LOCATE_MESSAGE + ':' + message.id, messageHighlightHandler)]
|
||||
const unsubscribes = [
|
||||
EventEmitter.on(EVENT_NAMES.LOCATE_MESSAGE + ':' + message.id, messageHighlightHandler),
|
||||
EventEmitter.on(EVENT_NAMES.RESEND_MESSAGE + ':' + message.id, onEditMessage)
|
||||
]
|
||||
return () => unsubscribes.forEach((unsub) => unsub())
|
||||
}, [message])
|
||||
}, [message, onEditMessage])
|
||||
|
||||
useEffect(() => {
|
||||
if (message.role === 'user' && !message.usage) {
|
||||
@@ -178,6 +181,7 @@ const MessageItem: FC<Props> = ({
|
||||
setModel={setModel}
|
||||
onEditMessage={onEditMessage}
|
||||
onDeleteMessage={onDeleteMessage}
|
||||
onGetMessages={onGetMessages}
|
||||
/>
|
||||
</MessageFooter>
|
||||
)}
|
||||
|
||||
@@ -15,7 +15,7 @@ import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { translateText } from '@renderer/services/TranslateService'
|
||||
import { Message, Model } from '@renderer/types'
|
||||
import { removeTrailingDoubleSpaces } from '@renderer/utils'
|
||||
import { Dropdown, Popconfirm, Tooltip } from 'antd'
|
||||
import { Button, Dropdown, Popconfirm, Tooltip } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { FC, useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -31,6 +31,7 @@ interface Props {
|
||||
setModel: (model: Model) => void
|
||||
onEditMessage?: (message: Message) => void
|
||||
onDeleteMessage?: (message: Message) => void
|
||||
onGetMessages?: () => Message[]
|
||||
}
|
||||
|
||||
const MessageMenubar: FC<Props> = (props) => {
|
||||
@@ -43,7 +44,8 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
assistantModel,
|
||||
setModel,
|
||||
onEditMessage,
|
||||
onDeleteMessage
|
||||
onDeleteMessage,
|
||||
onGetMessages
|
||||
} = props
|
||||
const { t } = useTranslation()
|
||||
const [copied, setCopied] = useState(false)
|
||||
@@ -75,10 +77,43 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
})
|
||||
}, [index, t])
|
||||
|
||||
const onResend = useCallback(() => {
|
||||
const _messages = onGetMessages?.() || []
|
||||
const index = _messages.findIndex((m) => m.id === message.id)
|
||||
const nextIndex = index + 1
|
||||
const nextMessage = _messages[nextIndex]
|
||||
|
||||
if (nextMessage && nextMessage.role === 'assistant') {
|
||||
EventEmitter.emit(EVENT_NAMES.RESEND_MESSAGE + ':' + nextMessage.id, {
|
||||
...nextMessage,
|
||||
content: '',
|
||||
status: 'sending',
|
||||
modelId: assistantModel?.id || model?.id,
|
||||
translatedContent: undefined
|
||||
})
|
||||
}
|
||||
}, [assistantModel?.id, message.id, model?.id, onGetMessages])
|
||||
|
||||
const onEdit = useCallback(async () => {
|
||||
const editedText = await TextEditPopup.show({ text: message.content })
|
||||
let resendMessage = false
|
||||
|
||||
const editedText = await TextEditPopup.show({
|
||||
text: message.content,
|
||||
children: (props) => (
|
||||
<ReSendButton
|
||||
icon={<i className="iconfont icon-ic_send" style={{ color: 'var(--color-primary)' }} />}
|
||||
onClick={() => {
|
||||
props.onOk?.()
|
||||
resendMessage = true
|
||||
}}>
|
||||
{t('chat.resend')}
|
||||
</ReSendButton>
|
||||
)
|
||||
})
|
||||
|
||||
editedText && onEditMessage?.({ ...message, content: editedText })
|
||||
}, [message, onEditMessage])
|
||||
resendMessage && onResend()
|
||||
}, [message, onEditMessage, onResend, t])
|
||||
|
||||
const handleTranslate = useCallback(
|
||||
async (language: string) => {
|
||||
@@ -287,4 +322,10 @@ const ActionButton = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const ReSendButton = styled(Button)`
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 0;
|
||||
`
|
||||
|
||||
export default MessageMenubar
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CheckOutlined, QuestionCircleOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
import { CheckOutlined, DeleteOutlined, PlusOutlined, QuestionCircleOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import {
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
setShowMessageDivider
|
||||
} from '@renderer/store/settings'
|
||||
import { Assistant, AssistantSettings, ThemeMode } from '@renderer/types'
|
||||
import { Col, InputNumber, Row, Select, Slider, Switch, Tooltip } from 'antd'
|
||||
import { Button, Col, Input, InputNumber, Row, Select, Slider, Switch, Tooltip } from 'antd'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@@ -102,7 +102,8 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
maxTokens: DEFAULT_MAX_TOKENS,
|
||||
streamOutput: true,
|
||||
hideMessages: false,
|
||||
autoResetModel: false
|
||||
autoResetModel: false,
|
||||
customParameters: []
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -202,6 +203,106 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
{assistant?.settings?.customParameters?.map((param, index) => (
|
||||
<ParameterCard key={index}>
|
||||
<Row align="middle" gutter={8} style={{ marginBottom: 8 }}>
|
||||
<Col span={14}>
|
||||
<Input
|
||||
placeholder={t('models.parameter_name')}
|
||||
value={param.name}
|
||||
onChange={(e) => {
|
||||
const newParams = [...(assistant?.settings?.customParameters || [])]
|
||||
newParams[index] = { ...param, name: e.target.value }
|
||||
onUpdateAssistantSettings({ customParameters: newParams })
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={10}>
|
||||
<Select
|
||||
value={param.type}
|
||||
onChange={(value: 'string' | 'number' | 'boolean') => {
|
||||
const newParams = [...(assistant?.settings?.customParameters || [])]
|
||||
let defaultValue: any = ''
|
||||
switch (value) {
|
||||
case 'number':
|
||||
defaultValue = 0
|
||||
break
|
||||
case 'boolean':
|
||||
defaultValue = false
|
||||
break
|
||||
default:
|
||||
defaultValue = ''
|
||||
}
|
||||
newParams[index] = { ...param, type: value, value: defaultValue }
|
||||
onUpdateAssistantSettings({ customParameters: newParams })
|
||||
}}
|
||||
style={{ width: '100%' }}>
|
||||
<Select.Option value="string">{t('models.parameter_type.string')}</Select.Option>
|
||||
<Select.Option value="number">{t('models.parameter_type.number')}</Select.Option>
|
||||
<Select.Option value="boolean">{t('models.parameter_type.boolean')}</Select.Option>
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row align="middle" gutter={10}>
|
||||
<Col span={20}>
|
||||
{param.type === 'boolean' ? (
|
||||
<Switch
|
||||
checked={param.value as boolean}
|
||||
onChange={(checked) => {
|
||||
const newParams = [...(assistant?.settings?.customParameters || [])]
|
||||
newParams[index] = { ...param, value: checked }
|
||||
onUpdateAssistantSettings({ customParameters: newParams })
|
||||
}}
|
||||
/>
|
||||
) : param.type === 'number' ? (
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
value={param.value as number}
|
||||
onChange={(value) => {
|
||||
const newParams = [...(assistant?.settings?.customParameters || [])]
|
||||
newParams[index] = { ...param, value: value || 0 }
|
||||
onUpdateAssistantSettings({ customParameters: newParams })
|
||||
}}
|
||||
step={0.01}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
value={typeof param.value === 'string' ? param.value : JSON.stringify(param.value)}
|
||||
onChange={(e) => {
|
||||
const newParams = [...(assistant?.settings?.customParameters || [])]
|
||||
newParams[index] = { ...param, value: e.target.value }
|
||||
onUpdateAssistantSettings({ customParameters: newParams })
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Button
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => {
|
||||
const newParams = [...(assistant?.settings?.customParameters || [])]
|
||||
newParams.splice(index, 1)
|
||||
onUpdateAssistantSettings({ customParameters: newParams })
|
||||
}}
|
||||
danger
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</ParameterCard>
|
||||
))}
|
||||
<Button
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
const newParams = [
|
||||
...(assistant?.settings?.customParameters || []),
|
||||
{ name: '', value: '', type: 'string' as const }
|
||||
]
|
||||
onUpdateAssistantSettings({ customParameters: newParams })
|
||||
}}
|
||||
style={{ marginBottom: 0, width: '100%', borderStyle: 'dashed' }}>
|
||||
{t('models.add_parameter')}
|
||||
</Button>
|
||||
</SettingGroup>
|
||||
<SettingGroup>
|
||||
<SettingSubtitle style={{ marginTop: 0 }}>{t('settings.messages.title')}</SettingSubtitle>
|
||||
@@ -418,4 +519,15 @@ export const SettingGroup = styled.div<{ theme?: ThemeMode }>`
|
||||
background: var(--color-group-background);
|
||||
`
|
||||
|
||||
const ParameterCard = styled.div`
|
||||
margin-bottom: 8px;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
background: var(--color-background);
|
||||
&:last-child {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
`
|
||||
|
||||
export default SettingsTab
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setUpdateState } from '@renderer/store/runtime'
|
||||
import { setManualUpdateCheck } from '@renderer/store/settings'
|
||||
import { ThemeMode } from '@renderer/types'
|
||||
import { compareVersions, runAsyncFunction } from '@renderer/utils'
|
||||
import { Avatar, Button, Progress, Row, Switch, Tag } from 'antd'
|
||||
import { debounce } from 'lodash'
|
||||
@@ -71,6 +72,15 @@ const AboutSettings: FC = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const showReleases = async () => {
|
||||
const { appPath } = await window.api.getAppInfo()
|
||||
MinApp.start({
|
||||
name: t('settings.about.releases.title'),
|
||||
url: `file://${appPath}/resources/cherry-studio/releases.html?theme=${theme === ThemeMode.dark ? 'dark' : 'light'}`,
|
||||
logo: AppLogo
|
||||
})
|
||||
}
|
||||
|
||||
const hasNewVersion = update?.info?.version && version ? compareVersions(update.info.version, version) > 0 : false
|
||||
|
||||
useEffect(() => {
|
||||
@@ -158,16 +168,7 @@ const AboutSettings: FC = () => {
|
||||
<SoundOutlined />
|
||||
{t('settings.about.releases.title')}
|
||||
</SettingRowTitle>
|
||||
<Button
|
||||
onClick={() =>
|
||||
MinApp.start({
|
||||
name: t('settings.about.releases.title'),
|
||||
url: 'https://github.com/kangfenmao/cherry-studio/releases',
|
||||
logo: AppLogo
|
||||
})
|
||||
}>
|
||||
{t('settings.about.releases.button')}
|
||||
</Button>
|
||||
<Button onClick={showReleases}>{t('settings.about.releases.button')}</Button>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons'
|
||||
import { DeleteOutlined, PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons'
|
||||
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
|
||||
import { DEFAULT_CONTEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
|
||||
import { SettingRow } from '@renderer/pages/settings'
|
||||
import { Assistant, AssistantSettings } from '@renderer/types'
|
||||
import { Button, Col, Divider, InputNumber, Row, Slider, Switch, Tooltip } from 'antd'
|
||||
import { Button, Col, Divider, Input, InputNumber, Row, Select, Slider, Switch, Tooltip } from 'antd'
|
||||
import { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@@ -25,6 +25,13 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
const [streamOutput, setStreamOutput] = useState(assistant?.settings?.streamOutput ?? true)
|
||||
const [defaultModel, setDefaultModel] = useState(assistant?.defaultModel)
|
||||
const [topP, setTopP] = useState(assistant?.settings?.topP ?? 1)
|
||||
const [customParameters, setCustomParameters] = useState<
|
||||
Array<{
|
||||
name: string
|
||||
value: string | number | boolean
|
||||
type: 'string' | 'number' | 'boolean'
|
||||
}>
|
||||
>(assistant?.settings?.customParameters ?? [])
|
||||
const { t } = useTranslation()
|
||||
|
||||
const onTemperatureChange = (value) => {
|
||||
@@ -51,6 +58,77 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
}
|
||||
}
|
||||
|
||||
const onAddCustomParameter = () => {
|
||||
const newParam = { name: '', value: '', type: 'string' as const }
|
||||
const newParams = [...customParameters, newParam]
|
||||
setCustomParameters(newParams)
|
||||
updateAssistantSettings({ customParameters: newParams })
|
||||
}
|
||||
|
||||
const onUpdateCustomParameter = (
|
||||
index: number,
|
||||
field: 'name' | 'value' | 'type',
|
||||
value: string | number | boolean
|
||||
) => {
|
||||
const newParams = [...customParameters]
|
||||
if (field === 'type') {
|
||||
let defaultValue: any = ''
|
||||
switch (value) {
|
||||
case 'number':
|
||||
defaultValue = 0
|
||||
break
|
||||
case 'boolean':
|
||||
defaultValue = false
|
||||
break
|
||||
default:
|
||||
defaultValue = ''
|
||||
}
|
||||
newParams[index] = {
|
||||
...newParams[index],
|
||||
type: value as any,
|
||||
value: defaultValue
|
||||
}
|
||||
} else {
|
||||
newParams[index] = { ...newParams[index], [field]: value }
|
||||
}
|
||||
setCustomParameters(newParams)
|
||||
updateAssistantSettings({ customParameters: newParams })
|
||||
}
|
||||
|
||||
const renderParameterValueInput = (param: (typeof customParameters)[0], index: number) => {
|
||||
switch (param.type) {
|
||||
case 'number':
|
||||
return (
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
value={param.value as number}
|
||||
onChange={(value) => onUpdateCustomParameter(index, 'value', value || 0)}
|
||||
step={0.01}
|
||||
/>
|
||||
)
|
||||
case 'boolean':
|
||||
return (
|
||||
<Switch
|
||||
checked={param.value as boolean}
|
||||
onChange={(checked) => onUpdateCustomParameter(index, 'value', checked)}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<Input
|
||||
value={param.value as string}
|
||||
onChange={(e) => onUpdateCustomParameter(index, 'value', e.target.value)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const onDeleteCustomParameter = (index: number) => {
|
||||
const newParams = customParameters.filter((_, i) => i !== index)
|
||||
setCustomParameters(newParams)
|
||||
updateAssistantSettings({ customParameters: newParams })
|
||||
}
|
||||
|
||||
const onReset = () => {
|
||||
setTemperature(DEFAULT_TEMPERATURE)
|
||||
setContextCount(DEFAULT_CONTEXTCOUNT)
|
||||
@@ -58,13 +136,15 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
setMaxTokens(0)
|
||||
setStreamOutput(true)
|
||||
setTopP(1)
|
||||
setCustomParameters([])
|
||||
updateAssistantSettings({
|
||||
temperature: DEFAULT_TEMPERATURE,
|
||||
contextCount: DEFAULT_CONTEXTCOUNT,
|
||||
enableMaxTokens: false,
|
||||
maxTokens: 0,
|
||||
streamOutput: true,
|
||||
topP: 1
|
||||
topP: 1,
|
||||
customParameters: []
|
||||
})
|
||||
}
|
||||
|
||||
@@ -249,6 +329,38 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
}}
|
||||
/>
|
||||
</SettingRow>
|
||||
<Divider style={{ margin: '10px 0' }} />
|
||||
<SettingRow style={{ minHeight: 30 }}>
|
||||
<Label>{t('models.custom_parameters')}</Label>
|
||||
<Button icon={<PlusOutlined />} onClick={onAddCustomParameter}>
|
||||
{t('models.add_parameter')}
|
||||
</Button>
|
||||
</SettingRow>
|
||||
{customParameters.map((param, index) => (
|
||||
<Row key={index} align="middle" gutter={10} style={{ marginTop: 10 }}>
|
||||
<Col span={6}>
|
||||
<Input
|
||||
placeholder={t('models.parameter_name')}
|
||||
value={param.name}
|
||||
onChange={(e) => onUpdateCustomParameter(index, 'name', e.target.value)}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Select
|
||||
value={param.type}
|
||||
onChange={(value) => onUpdateCustomParameter(index, 'type', value)}
|
||||
style={{ width: '100%' }}>
|
||||
<Select.Option value="string">{t('models.parameter_type.string')}</Select.Option>
|
||||
<Select.Option value="number">{t('models.parameter_type.number')}</Select.Option>
|
||||
<Select.Option value="boolean">{t('models.parameter_type.boolean')}</Select.Option>
|
||||
</Select>
|
||||
</Col>
|
||||
<Col span={11}>{renderParameterValueInput(param, index)}</Col>
|
||||
<Col span={3}>
|
||||
<Button icon={<DeleteOutlined />} onClick={() => onDeleteCustomParameter(index)} danger />
|
||||
</Col>
|
||||
</Row>
|
||||
))}
|
||||
<Divider style={{ margin: '15px 0' }} />
|
||||
<HStack justifyContent="flex-end">
|
||||
<Button onClick={onReset} style={{ width: 80 }} danger type="primary">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FolderOpenOutlined, SaveOutlined } from '@ant-design/icons'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { backupToWebdav, restoreFromWebdav } from '@renderer/services/BackupService'
|
||||
import { backupToWebdav, restoreFromWebdav, startAutoSync, stopAutoSync } from '@renderer/services/BackupService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import {
|
||||
setWebdavAutoSync,
|
||||
@@ -66,6 +66,11 @@ const WebDavSettings: FC = () => {
|
||||
|
||||
const onToggleAutoSync = (checked: boolean) => {
|
||||
dispatch(setWebdavAutoSync(checked))
|
||||
if (checked) {
|
||||
startAutoSync()
|
||||
} else {
|
||||
stopAutoSync()
|
||||
}
|
||||
}
|
||||
|
||||
const onSyncIntervalChange = (value: number) => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setEnableTopicNaming } from '@renderer/store/settings'
|
||||
import { Divider, Modal, Switch } from 'antd'
|
||||
import { setEnableTopicNaming, setTopicNamingPrompt } from '@renderer/store/settings'
|
||||
import { Button, Divider, Input, Modal, Switch } from 'antd'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@@ -15,7 +15,7 @@ interface Props {
|
||||
const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
const [open, setOpen] = useState(true)
|
||||
const { t } = useTranslation()
|
||||
const { enableTopicNaming } = useSettings()
|
||||
const { enableTopicNaming, topicNamingPrompt } = useSettings()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const onOk = () => {
|
||||
@@ -30,6 +30,10 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
resolve({})
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
dispatch(setTopicNamingPrompt(''))
|
||||
}
|
||||
|
||||
TopicNamingModalPopup.hide = onCancel
|
||||
|
||||
return (
|
||||
@@ -47,6 +51,21 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
<div>{t('settings.models.enable_topic_naming')}</div>
|
||||
<Switch checked={enableTopicNaming} onChange={(v) => dispatch(setEnableTopicNaming(v))} />
|
||||
</HStack>
|
||||
<Divider style={{ margin: '10px 0' }} />
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<div style={{ marginBottom: 10 }}>{t('settings.models.topic_naming_prompt')}</div>
|
||||
<Input.TextArea
|
||||
rows={4}
|
||||
value={topicNamingPrompt || t('prompts.summarize')}
|
||||
onChange={(e) => dispatch(setTopicNamingPrompt(e.target.value.trim()))}
|
||||
placeholder={t('prompts.summarize')}
|
||||
/>
|
||||
{topicNamingPrompt && (
|
||||
<Button style={{ marginTop: 10 }} onClick={handleReset}>
|
||||
{t('common.reset')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@ import Anthropic from '@anthropic-ai/sdk'
|
||||
import { MessageCreateParamsNonStreaming, MessageParam } from '@anthropic-ai/sdk/resources'
|
||||
import { DEFAULT_MAX_TOKENS } from '@renderer/config/constant'
|
||||
import { isEmbeddingModel } from '@renderer/config/models'
|
||||
import { SUMMARIZE_PROMPT } from '@renderer/config/prompts'
|
||||
import { getStoreSetting } from '@renderer/hooks/useSettings'
|
||||
import i18n from '@renderer/i18n'
|
||||
import { getAssistantSettings, getDefaultModel, getTopNamingModel } from '@renderer/services/AssistantService'
|
||||
import { EVENT_NAMES } from '@renderer/services/EventService'
|
||||
import { filterContextMessages } from '@renderer/services/MessagesService'
|
||||
@@ -86,7 +87,8 @@ export default class AnthropicProvider extends BaseProvider {
|
||||
max_tokens: maxTokens || DEFAULT_MAX_TOKENS,
|
||||
temperature: assistant?.settings?.temperature,
|
||||
top_p: assistant?.settings?.topP,
|
||||
system: assistant.prompt
|
||||
system: assistant.prompt,
|
||||
...this.getCustomParameters(assistant)
|
||||
}
|
||||
|
||||
let time_first_token_millsec = 0
|
||||
@@ -188,7 +190,7 @@ export default class AnthropicProvider extends BaseProvider {
|
||||
|
||||
const systemMessage = {
|
||||
role: 'system',
|
||||
content: SUMMARIZE_PROMPT
|
||||
content: (getStoreSetting('topicNamingPrompt') as string) || i18n.t('prompts.summarize')
|
||||
}
|
||||
|
||||
const userMessage = {
|
||||
|
||||
@@ -98,4 +98,16 @@ export default abstract class BaseProvider {
|
||||
|
||||
return REFERENCE_PROMPT.replace('{question}', message.content).replace('{references}', references)
|
||||
}
|
||||
|
||||
protected getCustomParameters(assistant: Assistant) {
|
||||
return (
|
||||
assistant?.settings?.customParameters?.reduce(
|
||||
(acc, param) => ({
|
||||
...acc,
|
||||
[param.name]: param.value
|
||||
}),
|
||||
{}
|
||||
) || {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
TextPart
|
||||
} from '@google/generative-ai'
|
||||
import { isEmbeddingModel, isWebSearchModel } from '@renderer/config/models'
|
||||
import { SUMMARIZE_PROMPT } from '@renderer/config/prompts'
|
||||
import { getStoreSetting } from '@renderer/hooks/useSettings'
|
||||
import i18n from '@renderer/i18n'
|
||||
import { getAssistantSettings, getDefaultModel, getTopNamingModel } from '@renderer/services/AssistantService'
|
||||
import { EVENT_NAMES } from '@renderer/services/EventService'
|
||||
import { filterContextMessages } from '@renderer/services/MessagesService'
|
||||
@@ -95,7 +96,8 @@ export default class GeminiProvider extends BaseProvider {
|
||||
generationConfig: {
|
||||
maxOutputTokens: maxTokens,
|
||||
temperature: assistant?.settings?.temperature,
|
||||
topP: assistant?.settings?.topP
|
||||
topP: assistant?.settings?.topP,
|
||||
...this.getCustomParameters(assistant)
|
||||
},
|
||||
safetySettings: [
|
||||
{ category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold: HarmBlockThreshold.BLOCK_NONE },
|
||||
@@ -198,7 +200,7 @@ export default class GeminiProvider extends BaseProvider {
|
||||
|
||||
const systemMessage = {
|
||||
role: 'system',
|
||||
content: SUMMARIZE_PROMPT
|
||||
content: (getStoreSetting('topicNamingPrompt') as string) || i18n.t('prompts.summarize')
|
||||
}
|
||||
|
||||
const userMessage = {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { isEmbeddingModel, isSupportedModel, isVisionModel } from '@renderer/config/models'
|
||||
import { SUMMARIZE_PROMPT } from '@renderer/config/prompts'
|
||||
import { getStoreSetting } from '@renderer/hooks/useSettings'
|
||||
import i18n from '@renderer/i18n'
|
||||
import { getAssistantSettings, getDefaultModel, getTopNamingModel } from '@renderer/services/AssistantService'
|
||||
import { EVENT_NAMES } from '@renderer/services/EventService'
|
||||
import { filterContextMessages } from '@renderer/services/MessagesService'
|
||||
import DuckDuckGoLiteSearch from '@renderer/tools/DuckDuckGoLiteSearch/function.json'
|
||||
import DuckDuckGoLiteSearchCode from '@renderer/tools/DuckDuckGoLiteSearch/index.js?raw'
|
||||
import { Assistant, FileTypes, Message, Model, Provider, Suggestion } from '@renderer/types'
|
||||
import { removeQuotes } from '@renderer/utils'
|
||||
import { last, takeRight } from 'lodash'
|
||||
@@ -10,7 +13,8 @@ import OpenAI, { AzureOpenAI } from 'openai'
|
||||
import {
|
||||
ChatCompletionContentPart,
|
||||
ChatCompletionCreateParamsNonStreaming,
|
||||
ChatCompletionMessageParam
|
||||
ChatCompletionMessageParam,
|
||||
ChatCompletionTool
|
||||
} from 'openai/resources'
|
||||
|
||||
import { CompletionsParams } from '.'
|
||||
@@ -132,7 +136,6 @@ export default class OpenAIProvider extends BaseProvider {
|
||||
}
|
||||
|
||||
const isOpenAIo1 = model.id.includes('o1-')
|
||||
const isSupportStreamOutput = streamOutput
|
||||
|
||||
let time_first_token_millsec = 0
|
||||
const start_time_millsec = new Date().getTime()
|
||||
@@ -147,11 +150,28 @@ export default class OpenAIProvider extends BaseProvider {
|
||||
top_p: assistant?.settings?.topP,
|
||||
max_tokens: maxTokens,
|
||||
keep_alive: this.keepAliveTime,
|
||||
stream: isSupportStreamOutput
|
||||
stream: streamOutput,
|
||||
tools: [DuckDuckGoLiteSearch as ChatCompletionTool],
|
||||
...this.getCustomParameters(assistant)
|
||||
})
|
||||
|
||||
if (!isSupportStreamOutput) {
|
||||
if (!streamOutput) {
|
||||
const time_completion_millsec = new Date().getTime() - start_time_millsec
|
||||
|
||||
stream.choices[0].message?.tool_calls?.forEach(async (toolCall) => {
|
||||
const functionName = toolCall.function.name
|
||||
const params = toolCall.function.arguments
|
||||
|
||||
console.log(functionName, DuckDuckGoLiteSearchCode)
|
||||
|
||||
const result = await window.api.vm.run(`
|
||||
var params = ${params};
|
||||
${DuckDuckGoLiteSearchCode}
|
||||
`)
|
||||
|
||||
console.log(result)
|
||||
})
|
||||
|
||||
return onChunk({
|
||||
text: stream.choices[0].message?.content || '',
|
||||
usage: stream.usage,
|
||||
@@ -219,7 +239,7 @@ export default class OpenAIProvider extends BaseProvider {
|
||||
|
||||
const systemMessage = {
|
||||
role: 'system',
|
||||
content: SUMMARIZE_PROMPT
|
||||
content: getStoreSetting('topicNamingPrompt') || i18n.t('prompts.summarize')
|
||||
}
|
||||
|
||||
const userMessage = {
|
||||
|
||||
@@ -100,7 +100,8 @@ export const getAssistantSettings = (assistant: Assistant): AssistantSettings =>
|
||||
maxTokens: getAssistantMaxTokens(),
|
||||
streamOutput: assistant?.settings?.streamOutput ?? true,
|
||||
hideMessages: assistant?.settings?.hideMessages ?? false,
|
||||
autoResetModel: assistant?.settings?.autoResetModel ?? false
|
||||
autoResetModel: assistant?.settings?.autoResetModel ?? false,
|
||||
customParameters: assistant?.settings?.customParameters ?? []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,10 @@ export async function reset() {
|
||||
|
||||
// 备份到 webdav
|
||||
export async function backupToWebdav({ showMessage = true }: { showMessage?: boolean } = {}) {
|
||||
if (isManualBackupRunning) {
|
||||
console.log('[Backup] Manual backup already in progress')
|
||||
return
|
||||
}
|
||||
const { webdavHost, webdavUser, webdavPass, webdavPath } = store.getState().settings
|
||||
|
||||
const backupData = await getBackupData()
|
||||
@@ -83,6 +87,8 @@ export async function backupToWebdav({ showMessage = true }: { showMessage?: boo
|
||||
title: i18n.t('message.backup.failed'),
|
||||
content: error.message
|
||||
})
|
||||
} finally {
|
||||
isManualBackupRunning = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,40 +115,69 @@ export async function restoreFromWebdav() {
|
||||
}
|
||||
}
|
||||
|
||||
let syncInterval: NodeJS.Timeout | null = null
|
||||
export function startAutoSync() {
|
||||
const { webdavAutoSync, webdavHost, webdavSyncInterval } = store.getState().settings
|
||||
let autoSyncStarted = false
|
||||
let syncTimeout: NodeJS.Timeout | null = null
|
||||
let isAutoBackupRunning = false
|
||||
let isManualBackupRunning = false
|
||||
|
||||
if (syncInterval) {
|
||||
stopAutoSync()
|
||||
export function startAutoSync() {
|
||||
if (autoSyncStarted) {
|
||||
return
|
||||
}
|
||||
|
||||
if (webdavAutoSync && webdavHost) {
|
||||
console.log('[AutoSync] Starting auto sync with interval:', webdavSyncInterval, 'minutes')
|
||||
const { webdavAutoSync, webdavHost, webdavSyncInterval } = store.getState().settings
|
||||
|
||||
const performBackup = async () => {
|
||||
try {
|
||||
console.log('[AutoSync] Performing backup...')
|
||||
await backupToWebdav({ showMessage: false })
|
||||
window.message.success({ content: i18n.t('message.backup.success'), key: 'webdav-sync' })
|
||||
} catch (error) {
|
||||
console.error('[AutoSync] Backup failed:', error)
|
||||
window.message.error({ content: i18n.t('message.backup.failed'), key: 'webdav-sync' })
|
||||
}
|
||||
if (!webdavAutoSync || !webdavHost || webdavSyncInterval <= 0) {
|
||||
console.log('[AutoSync] Invalid sync settings, auto sync disabled')
|
||||
return
|
||||
}
|
||||
|
||||
autoSyncStarted = true
|
||||
|
||||
stopAutoSync()
|
||||
|
||||
scheduleNextBackup()
|
||||
|
||||
function scheduleNextBackup() {
|
||||
if (syncTimeout) {
|
||||
clearTimeout(syncTimeout)
|
||||
syncTimeout = null
|
||||
}
|
||||
|
||||
syncInterval = setInterval(performBackup, webdavSyncInterval * 60 * 1000)
|
||||
syncTimeout = setTimeout(performAutoBackup, webdavSyncInterval * 60 * 1000)
|
||||
console.log(`[AutoSync] Next sync scheduled in ${webdavSyncInterval} minutes`)
|
||||
}
|
||||
|
||||
console.log(`[AutoSync] Sync interval set up: ${webdavSyncInterval} minutes`)
|
||||
async function performAutoBackup() {
|
||||
if (isAutoBackupRunning || isManualBackupRunning) {
|
||||
console.log('[AutoSync] Backup already in progress, rescheduling')
|
||||
scheduleNextBackup()
|
||||
return
|
||||
}
|
||||
|
||||
isAutoBackupRunning = true
|
||||
try {
|
||||
console.log('[AutoSync] Performing auto backup...')
|
||||
await backupToWebdav({ showMessage: false })
|
||||
window.message.success({ content: i18n.t('message.backup.success'), key: 'webdav-auto-sync' })
|
||||
} catch (error) {
|
||||
console.error('[AutoSync] Auto backup failed:', error)
|
||||
window.message.error({ content: i18n.t('message.backup.failed'), key: 'webdav-auto-sync' })
|
||||
} finally {
|
||||
isAutoBackupRunning = false
|
||||
scheduleNextBackup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function stopAutoSync() {
|
||||
if (syncInterval) {
|
||||
if (syncTimeout) {
|
||||
console.log('[AutoSync] Stopping auto sync')
|
||||
clearInterval(syncInterval)
|
||||
syncInterval = null
|
||||
clearTimeout(syncTimeout)
|
||||
syncTimeout = null
|
||||
}
|
||||
isAutoBackupRunning = false
|
||||
autoSyncStarted = false
|
||||
}
|
||||
|
||||
async function getBackupData() {
|
||||
|
||||
@@ -20,5 +20,6 @@ export const EVENT_NAMES = {
|
||||
NEW_BRANCH: 'NEW_BRANCH',
|
||||
EXPORT_TOPIC_IMAGE: 'EXPORT_TOPIC_IMAGE',
|
||||
LOCATE_MESSAGE: 'LOCATE_MESSAGE',
|
||||
ADD_NEW_TOPIC: 'ADD_NEW_TOPIC'
|
||||
ADD_NEW_TOPIC: 'ADD_NEW_TOPIC',
|
||||
RESEND_MESSAGE: 'RESEND_MESSAGE'
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ const persistedReducer = persistReducer(
|
||||
{
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 50,
|
||||
version: 51,
|
||||
blacklist: ['runtime'],
|
||||
migrate
|
||||
},
|
||||
|
||||
@@ -771,6 +771,10 @@ const migrateConfig = {
|
||||
enabled: false
|
||||
})
|
||||
return state
|
||||
},
|
||||
'51': (state: RootState) => {
|
||||
state.settings.topicNamingPrompt = ''
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ export interface SettingsState {
|
||||
showMinappIcon: boolean
|
||||
showFilesIcon: boolean
|
||||
customCss: string
|
||||
topicNamingPrompt: string
|
||||
}
|
||||
|
||||
const initialState: SettingsState = {
|
||||
@@ -85,7 +86,8 @@ const initialState: SettingsState = {
|
||||
enableTopicNaming: true,
|
||||
showMinappIcon: true,
|
||||
showFilesIcon: true,
|
||||
customCss: ''
|
||||
customCss: '',
|
||||
topicNamingPrompt: ''
|
||||
}
|
||||
|
||||
const settingsSlice = createSlice({
|
||||
@@ -212,6 +214,9 @@ const settingsSlice = createSlice({
|
||||
},
|
||||
setCustomCss: (state, action: PayloadAction<string>) => {
|
||||
state.customCss = action.payload
|
||||
},
|
||||
setTopicNamingPrompt: (state, action: PayloadAction<string>) => {
|
||||
state.topicNamingPrompt = action.payload
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -256,7 +261,8 @@ export const {
|
||||
setShowMinappIcon,
|
||||
setShowFilesIcon,
|
||||
setPasteLongTextThreshold,
|
||||
setCustomCss
|
||||
setCustomCss,
|
||||
setTopicNamingPrompt
|
||||
} = settingsSlice.actions
|
||||
|
||||
export default settingsSlice.reducer
|
||||
|
||||
22
src/renderer/src/tools/DuckDuckGoLiteSearch/function.json
Normal file
22
src/renderer/src/tools/DuckDuckGoLiteSearch/function.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "DuckDuckGoLiteSearch",
|
||||
"description": "A search engine useful for answering questions about current events.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"q": {
|
||||
"type": "string",
|
||||
"description": "Keywords for query"
|
||||
},
|
||||
"kl": {
|
||||
"type": "string",
|
||||
"description": "Language/region code (e.g., wt-wt, us-en, uk-en)",
|
||||
"default": "wt-wt"
|
||||
}
|
||||
},
|
||||
"required": ["q"]
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/renderer/src/tools/DuckDuckGoLiteSearch/index.js
Normal file
23
src/renderer/src/tools/DuckDuckGoLiteSearch/index.js
Normal file
@@ -0,0 +1,23 @@
|
||||
new Promise((resolve, reject) => {
|
||||
async function makeRequest() {
|
||||
try {
|
||||
const response = await axios.request({
|
||||
method: 'post',
|
||||
maxBodyLength: Infinity,
|
||||
url: 'https://google.serper.dev/search',
|
||||
headers: {
|
||||
'X-API-KEY': 'fa70255d0ab3402ee2ddb6455f6b317e73588fc7',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: params
|
||||
})
|
||||
console.log(JSON.stringify(response.data))
|
||||
resolve(response.data)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
reject(error.toString())
|
||||
}
|
||||
}
|
||||
|
||||
makeRequest()
|
||||
})
|
||||
@@ -30,6 +30,11 @@ export type AssistantSettings = {
|
||||
streamOutput: boolean
|
||||
hideMessages: boolean
|
||||
autoResetModel: boolean
|
||||
customParameters?: {
|
||||
name: string
|
||||
value: string | number | boolean
|
||||
type: 'string' | 'number' | 'boolean'
|
||||
}[]
|
||||
}
|
||||
|
||||
export type Agent = Omit<Assistant, 'model'>
|
||||
|
||||
10
yarn.lock
10
yarn.lock
@@ -2867,7 +2867,7 @@ __metadata:
|
||||
adm-zip: "npm:^0.5.16"
|
||||
antd: "npm:^5.22.5"
|
||||
apache-arrow: "npm:^18.1.0"
|
||||
axios: "npm:^1.7.3"
|
||||
axios: "npm:^1.7.9"
|
||||
browser-image-compression: "npm:^2.0.2"
|
||||
dayjs: "npm:^1.11.11"
|
||||
dexie: "npm:^4.0.8"
|
||||
@@ -3506,14 +3506,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"axios@npm:^1.7.3":
|
||||
version: 1.7.7
|
||||
resolution: "axios@npm:1.7.7"
|
||||
"axios@npm:^1.7.9":
|
||||
version: 1.7.9
|
||||
resolution: "axios@npm:1.7.9"
|
||||
dependencies:
|
||||
follow-redirects: "npm:^1.15.6"
|
||||
form-data: "npm:^4.0.0"
|
||||
proxy-from-env: "npm:^1.1.0"
|
||||
checksum: 10c0/4499efc89e86b0b49ffddc018798de05fab26e3bf57913818266be73279a6418c3ce8f9e934c7d2d707ab8c095e837fc6c90608fb7715b94d357720b5f568af7
|
||||
checksum: 10c0/b7a41e24b59fee5f0f26c1fc844b45b17442832eb3a0fb42dd4f1430eb4abc571fe168e67913e8a1d91c993232bd1d1ab03e20e4d1fee8c6147649b576fc1b0b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user