mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-05 04:19:02 +08:00
Merge branch 'main' into feat/sidebar-ui
# Conflicts: # src/renderer/src/assets/styles/index.scss # src/renderer/src/hooks/useTopic.ts # src/renderer/src/pages/home/Messages/Blocks/ImageBlock.tsx # src/renderer/src/pages/home/Messages/Blocks/index.tsx # src/renderer/src/pages/mcp-servers/providers/lanyun.ts # src/renderer/src/pages/settings/ModelSettings/TopicNamingModalPopup.tsx
This commit is contained in:
commit
facf29e02b
2
.github/workflows/pr-ci.yml
vendored
2
.github/workflows/pr-ci.yml
vendored
@ -44,4 +44,4 @@ jobs:
|
|||||||
run: yarn build:check
|
run: yarn build:check
|
||||||
|
|
||||||
- name: Lint Check
|
- name: Lint Check
|
||||||
run: yarn lint
|
run: yarn test:lint
|
||||||
|
|||||||
@ -65,11 +65,44 @@ index e8bd7bb46c8a54b3f55cf3a853ef924195271e01..f956e9f3fe9eb903c78aef3502553b01
|
|||||||
await packager.info.emitArtifactBuildCompleted({
|
await packager.info.emitArtifactBuildCompleted({
|
||||||
file: installerPath,
|
file: installerPath,
|
||||||
updateInfo,
|
updateInfo,
|
||||||
|
diff --git a/out/util/yarn.js b/out/util/yarn.js
|
||||||
|
index 1ee20f8b252a8f28d0c7b103789cf0a9a427aec1..c2878ec54d57da50bf14225e0c70c9c88664eb8a 100644
|
||||||
|
--- a/out/util/yarn.js
|
||||||
|
+++ b/out/util/yarn.js
|
||||||
|
@@ -140,6 +140,7 @@ async function rebuild(config, { appDir, projectDir }, options) {
|
||||||
|
arch,
|
||||||
|
platform,
|
||||||
|
buildFromSource,
|
||||||
|
+ ignoreModules: config.excludeReBuildModules || undefined,
|
||||||
|
projectRootPath: projectDir,
|
||||||
|
mode: config.nativeRebuilder || "sequential",
|
||||||
|
disablePreGypCopy: true,
|
||||||
diff --git a/scheme.json b/scheme.json
|
diff --git a/scheme.json b/scheme.json
|
||||||
index 433e2efc9cef156ff5444f0c4520362ed2ef9ea7..a89c7a9b0b608fef67902c49106a43ebd0fa8b61 100644
|
index 433e2efc9cef156ff5444f0c4520362ed2ef9ea7..0167441bf928a92f59b5dbe70b2317a74dda74c9 100644
|
||||||
--- a/scheme.json
|
--- a/scheme.json
|
||||||
+++ b/scheme.json
|
+++ b/scheme.json
|
||||||
@@ -1975,6 +1975,13 @@
|
@@ -1825,6 +1825,20 @@
|
||||||
|
"string"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
+ "excludeReBuildModules": {
|
||||||
|
+ "anyOf": [
|
||||||
|
+ {
|
||||||
|
+ "items": {
|
||||||
|
+ "type": "string"
|
||||||
|
+ },
|
||||||
|
+ "type": "array"
|
||||||
|
+ },
|
||||||
|
+ {
|
||||||
|
+ "type": "null"
|
||||||
|
+ }
|
||||||
|
+ ],
|
||||||
|
+ "description": "The modules to exclude from the rebuild."
|
||||||
|
+ },
|
||||||
|
"executableArgs": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
@@ -1975,6 +1989,13 @@
|
||||||
],
|
],
|
||||||
"description": "The mime types in addition to specified in the file associations. Use it if you don't want to register a new mime type, but reuse existing."
|
"description": "The mime types in addition to specified in the file associations. Use it if you don't want to register a new mime type, but reuse existing."
|
||||||
},
|
},
|
||||||
@ -83,7 +116,7 @@ index 433e2efc9cef156ff5444f0c4520362ed2ef9ea7..a89c7a9b0b608fef67902c49106a43eb
|
|||||||
"packageCategory": {
|
"packageCategory": {
|
||||||
"description": "backward compatibility + to allow specify fpm-only category for all possible fpm targets in one place",
|
"description": "backward compatibility + to allow specify fpm-only category for all possible fpm targets in one place",
|
||||||
"type": [
|
"type": [
|
||||||
@@ -2327,6 +2334,13 @@
|
@@ -2327,6 +2348,13 @@
|
||||||
"MacConfiguration": {
|
"MacConfiguration": {
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -97,7 +130,28 @@ index 433e2efc9cef156ff5444f0c4520362ed2ef9ea7..a89c7a9b0b608fef67902c49106a43eb
|
|||||||
"additionalArguments": {
|
"additionalArguments": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -2737,7 +2751,7 @@
|
@@ -2527,6 +2555,20 @@
|
||||||
|
"string"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
+ "excludeReBuildModules": {
|
||||||
|
+ "anyOf": [
|
||||||
|
+ {
|
||||||
|
+ "items": {
|
||||||
|
+ "type": "string"
|
||||||
|
+ },
|
||||||
|
+ "type": "array"
|
||||||
|
+ },
|
||||||
|
+ {
|
||||||
|
+ "type": "null"
|
||||||
|
+ }
|
||||||
|
+ ],
|
||||||
|
+ "description": "The modules to exclude from the rebuild."
|
||||||
|
+ },
|
||||||
|
"executableName": {
|
||||||
|
"description": "The executable name. Defaults to `productName`.",
|
||||||
|
"type": [
|
||||||
|
@@ -2737,7 +2779,7 @@
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"minimumSystemVersion": {
|
"minimumSystemVersion": {
|
||||||
@ -106,7 +160,7 @@ index 433e2efc9cef156ff5444f0c4520362ed2ef9ea7..a89c7a9b0b608fef67902c49106a43eb
|
|||||||
"type": [
|
"type": [
|
||||||
"null",
|
"null",
|
||||||
"string"
|
"string"
|
||||||
@@ -2959,6 +2973,13 @@
|
@@ -2959,6 +3001,13 @@
|
||||||
"MasConfiguration": {
|
"MasConfiguration": {
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -120,7 +174,28 @@ index 433e2efc9cef156ff5444f0c4520362ed2ef9ea7..a89c7a9b0b608fef67902c49106a43eb
|
|||||||
"additionalArguments": {
|
"additionalArguments": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -3369,7 +3390,7 @@
|
@@ -3159,6 +3208,20 @@
|
||||||
|
"string"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
+ "excludeReBuildModules": {
|
||||||
|
+ "anyOf": [
|
||||||
|
+ {
|
||||||
|
+ "items": {
|
||||||
|
+ "type": "string"
|
||||||
|
+ },
|
||||||
|
+ "type": "array"
|
||||||
|
+ },
|
||||||
|
+ {
|
||||||
|
+ "type": "null"
|
||||||
|
+ }
|
||||||
|
+ ],
|
||||||
|
+ "description": "The modules to exclude from the rebuild."
|
||||||
|
+ },
|
||||||
|
"executableName": {
|
||||||
|
"description": "The executable name. Defaults to `productName`.",
|
||||||
|
"type": [
|
||||||
|
@@ -3369,7 +3432,7 @@
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"minimumSystemVersion": {
|
"minimumSystemVersion": {
|
||||||
@ -129,7 +204,28 @@ index 433e2efc9cef156ff5444f0c4520362ed2ef9ea7..a89c7a9b0b608fef67902c49106a43eb
|
|||||||
"type": [
|
"type": [
|
||||||
"null",
|
"null",
|
||||||
"string"
|
"string"
|
||||||
@@ -6507,6 +6528,13 @@
|
@@ -6381,6 +6444,20 @@
|
||||||
|
"string"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
+ "excludeReBuildModules": {
|
||||||
|
+ "anyOf": [
|
||||||
|
+ {
|
||||||
|
+ "items": {
|
||||||
|
+ "type": "string"
|
||||||
|
+ },
|
||||||
|
+ "type": "array"
|
||||||
|
+ },
|
||||||
|
+ {
|
||||||
|
+ "type": "null"
|
||||||
|
+ }
|
||||||
|
+ ],
|
||||||
|
+ "description": "The modules to exclude from the rebuild."
|
||||||
|
+ },
|
||||||
|
"executableName": {
|
||||||
|
"description": "The executable name. Defaults to `productName`.",
|
||||||
|
"type": [
|
||||||
|
@@ -6507,6 +6584,13 @@
|
||||||
"string"
|
"string"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -143,7 +239,28 @@ index 433e2efc9cef156ff5444f0c4520362ed2ef9ea7..a89c7a9b0b608fef67902c49106a43eb
|
|||||||
"protocols": {
|
"protocols": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -7376,6 +7404,13 @@
|
@@ -7153,6 +7237,20 @@
|
||||||
|
"string"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
+ "excludeReBuildModules": {
|
||||||
|
+ "anyOf": [
|
||||||
|
+ {
|
||||||
|
+ "items": {
|
||||||
|
+ "type": "string"
|
||||||
|
+ },
|
||||||
|
+ "type": "array"
|
||||||
|
+ },
|
||||||
|
+ {
|
||||||
|
+ "type": "null"
|
||||||
|
+ }
|
||||||
|
+ ],
|
||||||
|
+ "description": "The modules to exclude from the rebuild."
|
||||||
|
+ },
|
||||||
|
"executableName": {
|
||||||
|
"description": "The executable name. Defaults to `productName`.",
|
||||||
|
"type": [
|
||||||
|
@@ -7376,6 +7474,13 @@
|
||||||
],
|
],
|
||||||
"description": "MAS (Mac Application Store) development options (`mas-dev` target)."
|
"description": "MAS (Mac Application Store) development options (`mas-dev` target)."
|
||||||
},
|
},
|
||||||
|
|||||||
@ -19,7 +19,13 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
external: ['@libsql/client', 'bufferutil', 'utf-8-validate']
|
external: ['@libsql/client', 'bufferutil', 'utf-8-validate'],
|
||||||
|
output: {
|
||||||
|
// 彻底禁用代码分割 - 返回 null 强制单文件打包
|
||||||
|
manualChunks: undefined,
|
||||||
|
// 内联所有动态导入,这是关键配置
|
||||||
|
inlineDynamicImports: true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
sourcemap: process.env.NODE_ENV === 'development'
|
sourcemap: process.env.NODE_ENV === 'development'
|
||||||
},
|
},
|
||||||
|
|||||||
@ -36,6 +36,11 @@ exports.default = async function (context) {
|
|||||||
keepPackageNodeFiles(node_modules_path, '@libsql', ['win32-x64-msvc'])
|
keepPackageNodeFiles(node_modules_path, '@libsql', ['win32-x64-msvc'])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (platform === 'windows') {
|
||||||
|
fs.rmSync(path.join(context.appOutDir, 'LICENSE.electron.txt'), { force: true })
|
||||||
|
fs.rmSync(path.join(context.appOutDir, 'LICENSES.chromium.html'), { force: true })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -21,10 +21,13 @@ export default abstract class BaseReranker {
|
|||||||
return 'https://dashscope.aliyuncs.com/api/v1/services/rerank/text-rerank/text-rerank'
|
return 'https://dashscope.aliyuncs.com/api/v1/services/rerank/text-rerank/text-rerank'
|
||||||
}
|
}
|
||||||
|
|
||||||
let baseURL = this.base?.rerankBaseURL?.endsWith('/')
|
let baseURL = this.base.rerankBaseURL
|
||||||
? this.base.rerankBaseURL.slice(0, -1)
|
|
||||||
: this.base.rerankBaseURL
|
if (baseURL && baseURL.endsWith('/')) {
|
||||||
// 必须携带/v1,否则会404
|
// `/` 结尾强制使用rerankBaseURL
|
||||||
|
return `${baseURL}rerank`
|
||||||
|
}
|
||||||
|
|
||||||
if (baseURL && !baseURL.endsWith('/v1')) {
|
if (baseURL && !baseURL.endsWith('/v1')) {
|
||||||
baseURL = `${baseURL}/v1`
|
baseURL = `${baseURL}/v1`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
import { isWin } from '@main/constant'
|
import { isWin } from '@main/constant'
|
||||||
import { locales } from '@main/utils/locales'
|
import { locales } from '@main/utils/locales'
|
||||||
import { IpcChannel } from '@shared/IpcChannel'
|
|
||||||
import { FeedUrl } from '@shared/config/constant'
|
import { FeedUrl } from '@shared/config/constant'
|
||||||
|
import { IpcChannel } from '@shared/IpcChannel'
|
||||||
import { UpdateInfo } from 'builder-util-runtime'
|
import { UpdateInfo } from 'builder-util-runtime'
|
||||||
import { app, BrowserWindow, dialog } from 'electron'
|
import { app, BrowserWindow, dialog } from 'electron'
|
||||||
import logger from 'electron-log'
|
import logger from 'electron-log'
|
||||||
import { AppUpdater as _AppUpdater, autoUpdater } from 'electron-updater'
|
import { AppUpdater as _AppUpdater, autoUpdater, NsisUpdater } from 'electron-updater'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
import icon from '../../../build/icon.png?asset'
|
import icon from '../../../build/icon.png?asset'
|
||||||
import { configManager } from './ConfigManager'
|
import { configManager } from './ConfigManager'
|
||||||
@ -56,6 +57,10 @@ export default class AppUpdater {
|
|||||||
logger.info('下载完成', releaseInfo)
|
logger.info('下载完成', releaseInfo)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (isWin) {
|
||||||
|
;(autoUpdater as NsisUpdater).installDirectory = path.dirname(app.getPath('exe'))
|
||||||
|
}
|
||||||
|
|
||||||
this.autoUpdater = autoUpdater
|
this.autoUpdater = autoUpdater
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -453,7 +453,7 @@ export class AnthropicAPIClient extends BaseApiClient<
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (this.useSystemPromptForTools) {
|
if (this.useSystemPromptForTools) {
|
||||||
systemPrompt = await buildSystemPrompt(systemPrompt, mcpTools)
|
systemPrompt = await buildSystemPrompt(systemPrompt, mcpTools, assistant)
|
||||||
}
|
}
|
||||||
|
|
||||||
const systemMessage: TextBlockParam | undefined = systemPrompt
|
const systemMessage: TextBlockParam | undefined = systemPrompt
|
||||||
|
|||||||
@ -452,7 +452,7 @@ export class GeminiAPIClient extends BaseApiClient<
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (this.useSystemPromptForTools) {
|
if (this.useSystemPromptForTools) {
|
||||||
systemInstruction = await buildSystemPrompt(assistant.prompt || '', mcpTools)
|
systemInstruction = await buildSystemPrompt(assistant.prompt || '', mcpTools, assistant)
|
||||||
}
|
}
|
||||||
|
|
||||||
let messageContents: Content
|
let messageContents: Content
|
||||||
|
|||||||
@ -420,7 +420,7 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (this.useSystemPromptForTools) {
|
if (this.useSystemPromptForTools) {
|
||||||
systemMessage.content = await buildSystemPrompt(systemMessage.content || '', mcpTools)
|
systemMessage.content = await buildSystemPrompt(systemMessage.content || '', mcpTools, assistant)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 处理用户消息
|
// 3. 处理用户消息
|
||||||
|
|||||||
@ -290,7 +290,7 @@ export class OpenAIResponseAPIClient extends OpenAIBaseClient<
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (this.useSystemPromptForTools) {
|
if (this.useSystemPromptForTools) {
|
||||||
systemMessageInput.text = await buildSystemPrompt(systemMessageInput.text || '', mcpTools)
|
systemMessageInput.text = await buildSystemPrompt(systemMessageInput.text || '', mcpTools, assistant)
|
||||||
}
|
}
|
||||||
systemMessageContent.push(systemMessageInput)
|
systemMessageContent.push(systemMessageInput)
|
||||||
systemMessage.content = systemMessageContent
|
systemMessage.content = systemMessageContent
|
||||||
|
|||||||
@ -97,11 +97,21 @@ export const ImageGenerationMiddleware: CompletionsMiddleware =
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const b64_json_array = response.data?.map((item) => `data:image/png;base64,${item.b64_json}`) || []
|
let imageType: 'url' | 'base64' = 'base64'
|
||||||
|
const imageList =
|
||||||
|
response.data?.reduce((acc: string[], image) => {
|
||||||
|
if (image.url) {
|
||||||
|
acc.push(image.url)
|
||||||
|
imageType = 'url'
|
||||||
|
} else if (image.b64_json) {
|
||||||
|
acc.push(`data:image/png;base64,${image.b64_json}`)
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, []) || []
|
||||||
|
|
||||||
enqueue({
|
enqueue({
|
||||||
type: ChunkType.IMAGE_COMPLETE,
|
type: ChunkType.IMAGE_COMPLETE,
|
||||||
image: { type: 'base64', images: b64_json_array }
|
image: { type: imageType, images: imageList }
|
||||||
})
|
})
|
||||||
|
|
||||||
const usage = (response as any).usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
|
const usage = (response as any).usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
|
||||||
|
|||||||
@ -112,21 +112,50 @@ ul {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bubble {
|
.bubble {
|
||||||
|
background-color: var(--chat-background);
|
||||||
|
#chat-main {
|
||||||
|
background-color: var(--chat-background);
|
||||||
|
}
|
||||||
|
#messages {
|
||||||
|
background-color: var(--chat-background);
|
||||||
|
}
|
||||||
|
#inputbar {
|
||||||
|
margin: -5px 15px 15px 15px;
|
||||||
|
background: var(--color-background);
|
||||||
|
}
|
||||||
.system-prompt {
|
.system-prompt {
|
||||||
background-color: var(--chat-background-assistant);
|
background-color: var(--chat-background-assistant);
|
||||||
}
|
}
|
||||||
.message-content-container {
|
.message-content-container {
|
||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.block-wrapper {
|
||||||
|
display: flow-root;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-wrapper:last-child > *:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content-container > *:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.message-thought-container {
|
.message-thought-container {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-user {
|
.message-user {
|
||||||
.message-content-container {
|
color: var(--chat-text-user);
|
||||||
margin: 5px 0;
|
.message-content-container-user .anticon {
|
||||||
border-radius: 8px 0 8px 8px;
|
color: var(--chat-text-user) !important;
|
||||||
padding: 10px 15px 0 15px;
|
}
|
||||||
|
|
||||||
|
.markdown {
|
||||||
|
color: var(--chat-text-user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.group-grid-container.horizontal,
|
.group-grid-container.horizontal,
|
||||||
@ -147,6 +176,12 @@ ul {
|
|||||||
code {
|
code {
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
.markdown {
|
||||||
|
display: flow-root;
|
||||||
|
*:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.lucide {
|
.lucide {
|
||||||
|
|||||||
@ -334,6 +334,7 @@ mjx-container {
|
|||||||
|
|
||||||
.cm-gutters {
|
.cm-gutters {
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
|
border-right: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-content {
|
.cm-content {
|
||||||
|
|||||||
@ -22,6 +22,7 @@ const MermaidPreview: React.FC<Props> = ({ children, setTools }) => {
|
|||||||
const diagramId = useRef<string>(`mermaid-${nanoid(6)}`).current
|
const diagramId = useRef<string>(`mermaid-${nanoid(6)}`).current
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [isRendering, setIsRendering] = useState(false)
|
const [isRendering, setIsRendering] = useState(false)
|
||||||
|
const [isVisible, setIsVisible] = useState(true)
|
||||||
|
|
||||||
// 使用通用图像工具
|
// 使用通用图像工具
|
||||||
const { handleZoom, handleCopyImage, handleDownload } = usePreviewToolHandlers(mermaidRef, {
|
const { handleZoom, handleCopyImage, handleDownload } = usePreviewToolHandlers(mermaidRef, {
|
||||||
@ -75,10 +76,55 @@ const MermaidPreview: React.FC<Props> = ({ children, setTools }) => {
|
|||||||
[renderMermaid]
|
[renderMermaid]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听可见性变化,用于触发重新渲染。
|
||||||
|
* 这是为了解决 `MessageGroup` 组件的 `fold` 布局中被 `display: none` 隐藏的图标无法正确渲染的问题。
|
||||||
|
* 监听时向上遍历到第一个有 `fold` className 的父节点为止(也就是目前的 `MessageWrapper`)。
|
||||||
|
* FIXME: 将来 mermaid-js 修复此问题后可以移除这里的相关逻辑。
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mermaidRef.current) return
|
||||||
|
|
||||||
|
const checkVisibility = () => {
|
||||||
|
const element = mermaidRef.current
|
||||||
|
if (!element) return
|
||||||
|
|
||||||
|
const currentlyVisible = element.offsetParent !== null
|
||||||
|
setIsVisible(currentlyVisible)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始检查
|
||||||
|
checkVisibility()
|
||||||
|
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
checkVisibility()
|
||||||
|
})
|
||||||
|
|
||||||
|
let targetElement = mermaidRef.current.parentElement
|
||||||
|
while (targetElement) {
|
||||||
|
observer.observe(targetElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['class', 'style']
|
||||||
|
})
|
||||||
|
|
||||||
|
if (targetElement.className?.includes('fold')) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
targetElement = targetElement.parentElement
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
// 触发渲染
|
// 触发渲染
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLoadingMermaid) return
|
if (isLoadingMermaid) return
|
||||||
|
|
||||||
|
if (mermaidRef.current?.offsetParent === null) return
|
||||||
|
|
||||||
if (children) {
|
if (children) {
|
||||||
setIsRendering(true)
|
setIsRendering(true)
|
||||||
debouncedRender(children)
|
debouncedRender(children)
|
||||||
@ -90,7 +136,7 @@ const MermaidPreview: React.FC<Props> = ({ children, setTools }) => {
|
|||||||
return () => {
|
return () => {
|
||||||
debouncedRender.cancel()
|
debouncedRender.cancel()
|
||||||
}
|
}
|
||||||
}, [children, isLoadingMermaid, debouncedRender])
|
}, [children, isLoadingMermaid, debouncedRender, isVisible])
|
||||||
|
|
||||||
const isLoading = isLoadingMermaid || isRendering
|
const isLoading = isLoadingMermaid || isRendering
|
||||||
|
|
||||||
|
|||||||
221
src/renderer/src/components/__tests__/MermaidPreview.test.tsx
Normal file
221
src/renderer/src/components/__tests__/MermaidPreview.test.tsx
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
import { render, screen, waitFor } from '@testing-library/react'
|
||||||
|
import { act } from 'react'
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, type Mock, vi } from 'vitest'
|
||||||
|
|
||||||
|
import MermaidPreview from '../CodeBlockView/MermaidPreview'
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => ({
|
||||||
|
useMermaid: vi.fn(),
|
||||||
|
usePreviewToolHandlers: vi.fn(),
|
||||||
|
usePreviewTools: vi.fn()
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock hooks
|
||||||
|
vi.mock('@renderer/hooks/useMermaid', () => ({
|
||||||
|
useMermaid: () => mocks.useMermaid()
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@renderer/components/CodeToolbar', () => ({
|
||||||
|
usePreviewToolHandlers: () => mocks.usePreviewToolHandlers(),
|
||||||
|
usePreviewTools: () => mocks.usePreviewTools()
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock nanoid
|
||||||
|
vi.mock('@reduxjs/toolkit', () => ({
|
||||||
|
nanoid: () => 'test-id-123456'
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock lodash debounce
|
||||||
|
vi.mock('lodash', async () => {
|
||||||
|
const actual = await import('lodash')
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
debounce: vi.fn((fn) => {
|
||||||
|
const debounced = (...args: any[]) => fn(...args)
|
||||||
|
debounced.cancel = vi.fn()
|
||||||
|
return debounced
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock antd components
|
||||||
|
vi.mock('antd', () => ({
|
||||||
|
Flex: ({ children, vertical, ...props }: any) => (
|
||||||
|
<div data-testid="flex" data-vertical={vertical} {...props}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
Spin: ({ children, spinning, indicator }: any) => (
|
||||||
|
<div data-testid="spin" data-spinning={spinning}>
|
||||||
|
{spinning && indicator}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('MermaidPreview', () => {
|
||||||
|
const mockMermaid = {
|
||||||
|
parse: vi.fn(),
|
||||||
|
render: vi.fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
|
||||||
|
mocks.useMermaid.mockReturnValue({
|
||||||
|
mermaid: mockMermaid,
|
||||||
|
isLoading: false,
|
||||||
|
error: null
|
||||||
|
})
|
||||||
|
|
||||||
|
mocks.usePreviewToolHandlers.mockReturnValue({
|
||||||
|
handleZoom: vi.fn(),
|
||||||
|
handleCopyImage: vi.fn(),
|
||||||
|
handleDownload: vi.fn()
|
||||||
|
})
|
||||||
|
|
||||||
|
mocks.usePreviewTools.mockReturnValue({})
|
||||||
|
|
||||||
|
mockMermaid.parse.mockResolvedValue(true)
|
||||||
|
mockMermaid.render.mockResolvedValue({
|
||||||
|
svg: '<svg class="flowchart" viewBox="0 0 100 100"><g>test diagram</g></svg>'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock MutationObserver
|
||||||
|
global.MutationObserver = vi.fn().mockImplementation(() => ({
|
||||||
|
observe: vi.fn(),
|
||||||
|
disconnect: vi.fn(),
|
||||||
|
takeRecords: vi.fn()
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('visibility detection', () => {
|
||||||
|
it('should not render mermaid when element has display: none', async () => {
|
||||||
|
const mermaidCode = 'graph TD\nA-->B'
|
||||||
|
|
||||||
|
const { container } = render(<MermaidPreview>{mermaidCode}</MermaidPreview>)
|
||||||
|
|
||||||
|
// Mock offsetParent to be null (simulating display: none)
|
||||||
|
const mermaidElement = container.querySelector('.mermaid')
|
||||||
|
if (mermaidElement) {
|
||||||
|
Object.defineProperty(mermaidElement, 'offsetParent', {
|
||||||
|
get: () => null,
|
||||||
|
configurable: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-render to trigger the effect
|
||||||
|
render(<MermaidPreview>{mermaidCode}</MermaidPreview>)
|
||||||
|
|
||||||
|
// Should not call mermaid render when offsetParent is null
|
||||||
|
expect(mockMermaid.render).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
const svgElement = mermaidElement?.querySelector('svg.flowchart')
|
||||||
|
expect(svgElement).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should setup MutationObserver to monitor parent elements', () => {
|
||||||
|
const mermaidCode = 'graph TD\nA-->B'
|
||||||
|
|
||||||
|
render(<MermaidPreview>{mermaidCode}</MermaidPreview>)
|
||||||
|
|
||||||
|
expect(global.MutationObserver).toHaveBeenCalledWith(expect.any(Function))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should observe parent elements up to fold className', () => {
|
||||||
|
const mermaidCode = 'graph TD\nA-->B'
|
||||||
|
|
||||||
|
// Create a DOM structure that simulates MessageGroup fold layout
|
||||||
|
const foldContainer = document.createElement('div')
|
||||||
|
foldContainer.className = 'fold selected'
|
||||||
|
|
||||||
|
const messageWrapper = document.createElement('div')
|
||||||
|
messageWrapper.className = 'message-wrapper'
|
||||||
|
|
||||||
|
const codeBlock = document.createElement('div')
|
||||||
|
codeBlock.className = 'code-block'
|
||||||
|
|
||||||
|
foldContainer.appendChild(messageWrapper)
|
||||||
|
messageWrapper.appendChild(codeBlock)
|
||||||
|
document.body.appendChild(foldContainer)
|
||||||
|
|
||||||
|
render(<MermaidPreview>{mermaidCode}</MermaidPreview>, {
|
||||||
|
container: codeBlock
|
||||||
|
})
|
||||||
|
|
||||||
|
const observerInstance = (global.MutationObserver as Mock).mock.results[0]?.value
|
||||||
|
expect(observerInstance.observe).toHaveBeenCalled()
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
document.body.removeChild(foldContainer)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should trigger re-render when visibility changes from hidden to visible', async () => {
|
||||||
|
const mermaidCode = 'graph TD\nA-->B'
|
||||||
|
|
||||||
|
const { container, rerender } = render(<MermaidPreview>{mermaidCode}</MermaidPreview>)
|
||||||
|
|
||||||
|
const mermaidElement = container.querySelector('.mermaid')
|
||||||
|
|
||||||
|
// Initially hidden (offsetParent is null)
|
||||||
|
Object.defineProperty(mermaidElement, 'offsetParent', {
|
||||||
|
get: () => null,
|
||||||
|
configurable: true
|
||||||
|
})
|
||||||
|
|
||||||
|
// Clear previous calls
|
||||||
|
mockMermaid.render.mockClear()
|
||||||
|
|
||||||
|
// Re-render with hidden state
|
||||||
|
rerender(<MermaidPreview>{mermaidCode}</MermaidPreview>)
|
||||||
|
|
||||||
|
// Should not render when hidden
|
||||||
|
expect(mockMermaid.render).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
// Now make it visible
|
||||||
|
Object.defineProperty(mermaidElement, 'offsetParent', {
|
||||||
|
get: () => document.body,
|
||||||
|
configurable: true
|
||||||
|
})
|
||||||
|
|
||||||
|
// Simulate MutationObserver callback
|
||||||
|
const observerCallback = (global.MutationObserver as Mock).mock.calls[0][0]
|
||||||
|
act(() => {
|
||||||
|
observerCallback([])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Re-render to trigger visibility change effect
|
||||||
|
rerender(<MermaidPreview>{mermaidCode}</MermaidPreview>)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockMermaid.render).toHaveBeenCalledWith('mermaid-test-id-123456', mermaidCode, expect.any(Object))
|
||||||
|
|
||||||
|
const svgElement = mermaidElement?.querySelector('svg.flowchart')
|
||||||
|
expect(svgElement).toBeInTheDocument()
|
||||||
|
expect(svgElement).toHaveClass('flowchart')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle mermaid loading state', () => {
|
||||||
|
mocks.useMermaid.mockReturnValue({
|
||||||
|
mermaid: mockMermaid,
|
||||||
|
isLoading: true,
|
||||||
|
error: null
|
||||||
|
})
|
||||||
|
|
||||||
|
const mermaidCode = 'graph TD\nA-->B'
|
||||||
|
|
||||||
|
render(<MermaidPreview>{mermaidCode}</MermaidPreview>)
|
||||||
|
|
||||||
|
// Should not render when mermaid is loading
|
||||||
|
expect(mockMermaid.render).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
// Should show loading state
|
||||||
|
expect(screen.getByTestId('spin')).toHaveAttribute('data-spinning', 'true')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -145,6 +145,7 @@ import YoudaoLogo from '@renderer/assets/images/providers/netease-youdao.svg'
|
|||||||
import NomicLogo from '@renderer/assets/images/providers/nomic.png'
|
import NomicLogo from '@renderer/assets/images/providers/nomic.png'
|
||||||
import { getProviderByModel } from '@renderer/services/AssistantService'
|
import { getProviderByModel } from '@renderer/services/AssistantService'
|
||||||
import { Model } from '@renderer/types'
|
import { Model } from '@renderer/types'
|
||||||
|
import { getBaseModelName } from '@renderer/utils'
|
||||||
import OpenAI from 'openai'
|
import OpenAI from 'openai'
|
||||||
|
|
||||||
import { WEB_SEARCH_PROMPT_FOR_OPENROUTER } from './prompts'
|
import { WEB_SEARCH_PROMPT_FOR_OPENROUTER } from './prompts'
|
||||||
@ -2484,9 +2485,10 @@ export function isSupportedThinkingTokenQwenModel(model?: Model): boolean {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const baseName = getBaseModelName(model.id, '/').toLowerCase()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
model.id.toLowerCase().startsWith('qwen3') ||
|
baseName.startsWith('qwen3') ||
|
||||||
model.id.toLowerCase().startsWith('qwen/qwen3') ||
|
|
||||||
[
|
[
|
||||||
'qwen-plus-latest',
|
'qwen-plus-latest',
|
||||||
'qwen-plus-0428',
|
'qwen-plus-0428',
|
||||||
@ -2494,7 +2496,7 @@ export function isSupportedThinkingTokenQwenModel(model?: Model): boolean {
|
|||||||
'qwen-turbo-latest',
|
'qwen-turbo-latest',
|
||||||
'qwen-turbo-0428',
|
'qwen-turbo-0428',
|
||||||
'qwen-turbo-2025-04-28'
|
'qwen-turbo-2025-04-28'
|
||||||
].includes(model.id.toLowerCase())
|
].includes(baseName)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
"add.prompt.placeholder": "Enter prompt",
|
"add.prompt.placeholder": "Enter prompt",
|
||||||
"add.prompt.variables.tip": {
|
"add.prompt.variables.tip": {
|
||||||
"title": "Available variables",
|
"title": "Available variables",
|
||||||
"content": "{{date}}:\tDate\n{{time}}:\tTime\n{{datetime}}:\tDate and time\n{{system}}:\tOperating system\n{{arch}}:\tCPU architecture\n{{language}}:\tLanguage\n{{model_name}}:\tModel name"
|
"content": "{{date}}:\tDate\n{{time}}:\tTime\n{{datetime}}:\tDate and time\n{{system}}:\tOperating system\n{{arch}}:\tCPU architecture\n{{language}}:\tLanguage\n{{model_name}}:\tModel name\n{{username}}:\tUsername"
|
||||||
},
|
},
|
||||||
"add.title": "Create Agent",
|
"add.title": "Create Agent",
|
||||||
"import": {
|
"import": {
|
||||||
@ -1966,6 +1966,7 @@
|
|||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"title": "Actions",
|
"title": "Actions",
|
||||||
|
"custom": "Custom Action",
|
||||||
"reset": {
|
"reset": {
|
||||||
"button": "Reset",
|
"button": "Reset",
|
||||||
"tooltip": "Reset to default actions. Custom actions will not be deleted.",
|
"tooltip": "Reset to default actions. Custom actions will not be deleted.",
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
"add.prompt.placeholder": "プロンプトを入力",
|
"add.prompt.placeholder": "プロンプトを入力",
|
||||||
"add.prompt.variables.tip": {
|
"add.prompt.variables.tip": {
|
||||||
"title": "利用可能な変数",
|
"title": "利用可能な変数",
|
||||||
"content": "{{date}}:\t日付\n{{time}}:\t時間\n{{datetime}}:\t日付と時間\n{{system}}:\tオペレーティングシステム\n{{arch}}:\tCPUアーキテクチャ\n{{language}}:\t言語\n{{model_name}}:\tモデル名"
|
"content": "{{date}}:\t日付\n{{time}}:\t時間\n{{datetime}}:\t日付と時間\n{{system}}:\tオペレーティングシステム\n{{arch}}:\tCPUアーキテクチャ\n{{language}}:\t言語\n{{model_name}}:\tモデル名\n{{username}}:\tユーザー名"
|
||||||
},
|
},
|
||||||
"add.title": "エージェントを作成",
|
"add.title": "エージェントを作成",
|
||||||
"import": {
|
"import": {
|
||||||
@ -1966,6 +1966,7 @@
|
|||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"title": "機能設定",
|
"title": "機能設定",
|
||||||
|
"custom": "カスタム機能",
|
||||||
"reset": {
|
"reset": {
|
||||||
"button": "リセット",
|
"button": "リセット",
|
||||||
"tooltip": "デフォルト機能にリセット(カスタム機能は保持)",
|
"tooltip": "デフォルト機能にリセット(カスタム機能は保持)",
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
"add.prompt.placeholder": "Введите промпт",
|
"add.prompt.placeholder": "Введите промпт",
|
||||||
"add.prompt.variables.tip": {
|
"add.prompt.variables.tip": {
|
||||||
"title": "Доступные переменные",
|
"title": "Доступные переменные",
|
||||||
"content": "{{date}}:\tДата\n{{time}}:\tВремя\n{{datetime}}:\tДата и время\n{{system}}:\tОперационная система\n{{arch}}:\tАрхитектура процессора\n{{language}}:\tЯзык\n{{model_name}}:\tНазвание модели"
|
"content": "{{date}}:\tДата\n{{time}}:\tВремя\n{{datetime}}:\tДата и время\n{{system}}:\tОперационная система\n{{arch}}:\tАрхитектура процессора\n{{language}}:\tЯзык\n{{model_name}}:\tНазвание модели\n{{username}}:\tИмя пользователя"
|
||||||
},
|
},
|
||||||
"add.title": "Создать агента",
|
"add.title": "Создать агента",
|
||||||
"delete.popup.content": "Вы уверены, что хотите удалить этого агента?",
|
"delete.popup.content": "Вы уверены, что хотите удалить этого агента?",
|
||||||
@ -1966,6 +1966,7 @@
|
|||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"title": "Действия",
|
"title": "Действия",
|
||||||
|
"custom": "Пользовательское действие",
|
||||||
"reset": {
|
"reset": {
|
||||||
"button": "Сбросить",
|
"button": "Сбросить",
|
||||||
"tooltip": "Сбросить стандартные действия. Пользовательские останутся.",
|
"tooltip": "Сбросить стандартные действия. Пользовательские останутся.",
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
"add.prompt.placeholder": "输入提示词",
|
"add.prompt.placeholder": "输入提示词",
|
||||||
"add.prompt.variables.tip": {
|
"add.prompt.variables.tip": {
|
||||||
"title": "可用的变量",
|
"title": "可用的变量",
|
||||||
"content": "{{date}}:\t日期\n{{time}}:\t时间\n{{datetime}}:\t日期和时间\n{{system}}:\t操作系统\n{{arch}}:\tCPU架构\n{{language}}:\t语言\n{{model_name}}:\t模型名称"
|
"content": "{{date}}:\t日期\n{{time}}:\t时间\n{{datetime}}:\t日期和时间\n{{system}}:\t操作系统\n{{arch}}:\tCPU架构\n{{language}}:\t语言\n{{model_name}}:\t模型名称\n{{username}}:\t用户名"
|
||||||
},
|
},
|
||||||
"add.title": "创建智能体",
|
"add.title": "创建智能体",
|
||||||
"import": {
|
"import": {
|
||||||
@ -1931,7 +1931,7 @@
|
|||||||
"selected": "划词",
|
"selected": "划词",
|
||||||
"selected_note": "划词后立即显示工具栏",
|
"selected_note": "划词后立即显示工具栏",
|
||||||
"ctrlkey": "Ctrl 键",
|
"ctrlkey": "Ctrl 键",
|
||||||
"ctrlkey_note": "划词后,再 按住 Ctrl键,才显示工具栏",
|
"ctrlkey_note": "划词后,再 长按 Ctrl键,才显示工具栏",
|
||||||
"shortcut": "快捷键",
|
"shortcut": "快捷键",
|
||||||
"shortcut_note": "划词后,使用快捷键显示工具栏。请在快捷键设置页面中设置取词快捷键并启用。",
|
"shortcut_note": "划词后,使用快捷键显示工具栏。请在快捷键设置页面中设置取词快捷键并启用。",
|
||||||
"shortcut_link": "前往快捷键设置"
|
"shortcut_link": "前往快捷键设置"
|
||||||
@ -1966,6 +1966,7 @@
|
|||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"title": "功能",
|
"title": "功能",
|
||||||
|
"custom": "自定义功能",
|
||||||
"reset": {
|
"reset": {
|
||||||
"button": "重置",
|
"button": "重置",
|
||||||
"tooltip": "重置为默认功能,自定义功能不会被删除",
|
"tooltip": "重置为默认功能,自定义功能不会被删除",
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
"add.prompt.placeholder": "輸入提示詞",
|
"add.prompt.placeholder": "輸入提示詞",
|
||||||
"add.prompt.variables.tip": {
|
"add.prompt.variables.tip": {
|
||||||
"title": "可用的變數",
|
"title": "可用的變數",
|
||||||
"content": "{{date}}:\t日期\n{{time}}:\t時間\n{{datetime}}:\t日期和時間\n{{system}}:\t作業系統\n{{arch}}:\tCPU架構\n{{language}}:\t語言\n{{model_name}}:\t模型名稱"
|
"content": "{{date}}:\t日期\n{{time}}:\t時間\n{{datetime}}:\t日期和時間\n{{system}}:\t作業系統\n{{arch}}:\tCPU架構\n{{language}}:\t語言\n{{model_name}}:\t模型名稱\n{{username}}:\t使用者名稱"
|
||||||
},
|
},
|
||||||
"add.title": "建立智慧代理人",
|
"add.title": "建立智慧代理人",
|
||||||
"import": {
|
"import": {
|
||||||
@ -1965,6 +1965,7 @@
|
|||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"title": "功能",
|
"title": "功能",
|
||||||
|
"custom": "自訂功能",
|
||||||
"reset": {
|
"reset": {
|
||||||
"button": "重設",
|
"button": "重設",
|
||||||
"tooltip": "重設為預設功能,自訂功能不會被刪除",
|
"tooltip": "重設為預設功能,自訂功能不會被刪除",
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
"add.prompt.placeholder": "Εισαγάγετε φράση προκαλέσεως",
|
"add.prompt.placeholder": "Εισαγάγετε φράση προκαλέσεως",
|
||||||
"add.prompt.variables.tip": {
|
"add.prompt.variables.tip": {
|
||||||
"title": "Διαθέσιμες μεταβλητές",
|
"title": "Διαθέσιμες μεταβλητές",
|
||||||
"content": "{{date}}:\tΗμερομηνία\n{{time}}:\tΏρα\n{{datetime}}:\tΗμερομηνία και ώρα\n{{system}}:\tΛειτουργικό σύστημα\n{{arch}}:\tΑρχιτεκτονική CPU\n{{language}}:\tΓλώσσα\n{{model_name}}:\tΌνομα μοντέλου"
|
"content": "{{date}}:\tΗμερομηνία\n{{time}}:\tΏρα\n{{datetime}}:\tΗμερομηνία και ώρα\n{{system}}:\tΛειτουργικό σύστημα\n{{arch}}:\tΑρχιτεκτονική CPU\n{{language}}:\tΓλώσσα\n{{model_name}}:\tΌνομα μοντέλου\n{{username}}:\tΌνομα χρήστη"
|
||||||
},
|
},
|
||||||
"add.title": "Δημιουργία νέου ειδικού",
|
"add.title": "Δημιουργία νέου ειδικού",
|
||||||
"delete.popup.content": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτόν τον ειδικό;",
|
"delete.popup.content": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτόν τον ειδικό;",
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
"add.prompt.placeholder": "Ingrese la palabra clave",
|
"add.prompt.placeholder": "Ingrese la palabra clave",
|
||||||
"add.prompt.variables.tip": {
|
"add.prompt.variables.tip": {
|
||||||
"title": "Variables disponibles",
|
"title": "Variables disponibles",
|
||||||
"content": "{{date}}:\tFecha\n{{time}}:\tHora\n{{datetime}}:\tFecha y hora\n{{system}}:\tSistema operativo\n{{arch}}:\tArquitectura de CPU\n{{language}}:\tIdioma\n{{model_name}}:\tNombre del modelo"
|
"content": "{{date}}:\tFecha\n{{time}}:\tHora\n{{datetime}}:\tFecha y hora\n{{system}}:\tSistema operativo\n{{arch}}:\tArquitectura de CPU\n{{language}}:\tIdioma\n{{model_name}}:\tNombre del modelo\n{{username}}:\tNombre de usuario"
|
||||||
},
|
},
|
||||||
"add.title": "Crear agente inteligente",
|
"add.title": "Crear agente inteligente",
|
||||||
"delete.popup.content": "¿Está seguro de que desea eliminar este agente inteligente?",
|
"delete.popup.content": "¿Está seguro de que desea eliminar este agente inteligente?",
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
"add.prompt.placeholder": "Entrer le mot-clé",
|
"add.prompt.placeholder": "Entrer le mot-clé",
|
||||||
"add.prompt.variables.tip": {
|
"add.prompt.variables.tip": {
|
||||||
"title": "Variables disponibles",
|
"title": "Variables disponibles",
|
||||||
"content": "{{date}}:\tDate\n{{time}}:\tHeure\n{{datetime}}:\tDate et heure\n{{system}}:\tSystème d'exploitation\n{{arch}}:\tArchitecture du processeur\n{{language}}:\tLangue\n{{model_name}}:\tNom du modèle"
|
"content": "{{date}}:\tDate\n{{time}}:\tHeure\n{{datetime}}:\tDate et heure\n{{system}}:\tSystème d'exploitation\n{{arch}}:\tArchitecture du processeur\n{{language}}:\tLangue\n{{model_name}}:\tNom du modèle\n{{username}}:\tNom d'utilisateur"
|
||||||
},
|
},
|
||||||
"add.title": "Créer un agent intelligent",
|
"add.title": "Créer un agent intelligent",
|
||||||
"delete.popup.content": "Êtes-vous sûr de vouloir supprimer cet agent intelligent ?",
|
"delete.popup.content": "Êtes-vous sûr de vouloir supprimer cet agent intelligent ?",
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
"add.prompt.placeholder": "Digite o Prompt",
|
"add.prompt.placeholder": "Digite o Prompt",
|
||||||
"add.prompt.variables.tip": {
|
"add.prompt.variables.tip": {
|
||||||
"title": "Variáveis disponíveis",
|
"title": "Variáveis disponíveis",
|
||||||
"content": "{{date}}:\tData\n{{time}}:\tHora\n{{datetime}}:\tData e hora\n{{system}}:\tSistema operativo\n{{arch}}:\tArquitetura da CPU\n{{language}}:\tIdioma\n{{model_name}}:\tNome do modelo"
|
"content": "{{date}}:\tData\n{{time}}:\tHora\n{{datetime}}:\tData e hora\n{{system}}:\tSistema operativo\n{{arch}}:\tArquitetura da CPU\n{{language}}:\tIdioma\n{{model_name}}:\tNome do modelo\n{{username}}:\tNome de utilizador"
|
||||||
},
|
},
|
||||||
"add.title": "Criar Agente Inteligente",
|
"add.title": "Criar Agente Inteligente",
|
||||||
"delete.popup.content": "Tem certeza de que deseja excluir este agente inteligente?",
|
"delete.popup.content": "Tem certeza de que deseja excluir este agente inteligente?",
|
||||||
|
|||||||
@ -191,16 +191,16 @@ const Inputbar: FC = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (topic.prompt) {
|
const assistantWithTopicPrompt = topic.prompt
|
||||||
assistant.prompt = assistant.prompt ? `${assistant.prompt}\n${topic.prompt}` : topic.prompt
|
? { ...assistant, prompt: `${assistant.prompt}\n${topic.prompt}` }
|
||||||
}
|
: assistant
|
||||||
|
|
||||||
baseUserMessage.usage = await estimateUserPromptUsage(baseUserMessage)
|
baseUserMessage.usage = await estimateUserPromptUsage(baseUserMessage)
|
||||||
|
|
||||||
const { message, blocks } = getUserMessage(baseUserMessage)
|
const { message, blocks } = getUserMessage(baseUserMessage)
|
||||||
|
|
||||||
currentMessageId.current = message.id
|
currentMessageId.current = message.id
|
||||||
dispatch(_sendMessage(message, blocks, assistant, topic.id))
|
dispatch(_sendMessage(message, blocks, assistantWithTopicPrompt, topic.id))
|
||||||
|
|
||||||
// Clear input
|
// Clear input
|
||||||
setText('')
|
setText('')
|
||||||
@ -309,7 +309,7 @@ const Inputbar: FC = () => {
|
|||||||
}, [knowledgeBases, openKnowledgeFileList, quickPanel, t, inputbarToolsRef])
|
}, [knowledgeBases, openKnowledgeFileList, quickPanel, t, inputbarToolsRef])
|
||||||
|
|
||||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
const isEnterPressed = event.keyCode == 13
|
const isEnterPressed = event.key === 'Enter'
|
||||||
|
|
||||||
// 按下Tab键,自动选中${xxx}
|
// 按下Tab键,自动选中${xxx}
|
||||||
if (event.key === 'Tab' && inputFocus) {
|
if (event.key === 'Tab' && inputFocus) {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import SvgSpinners180Ring from '@renderer/components/Icons/SvgSpinners180Ring'
|
|
||||||
import ImageViewer from '@renderer/components/ImageViewer'
|
import ImageViewer from '@renderer/components/ImageViewer'
|
||||||
import { type ImageMessageBlock } from '@renderer/types/newMessage'
|
import { type ImageMessageBlock, MessageBlockStatus } from '@renderer/types/newMessage'
|
||||||
|
import { Skeleton } from 'antd'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
@ -9,23 +9,26 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ImageBlock: React.FC<Props> = ({ block }) => {
|
const ImageBlock: React.FC<Props> = ({ block }) => {
|
||||||
if (block.status !== 'success') return <SvgSpinners180Ring />
|
if (block.status === MessageBlockStatus.STREAMING || block.status === MessageBlockStatus.PROCESSING)
|
||||||
const images = block.metadata?.generateImageResponse?.images?.length
|
return <Skeleton.Image active style={{ width: 200, height: 200 }} />
|
||||||
? block.metadata?.generateImageResponse?.images
|
if (block.status === MessageBlockStatus.SUCCESS) {
|
||||||
: block?.file?.path
|
const images = block.metadata?.generateImageResponse?.images?.length
|
||||||
? [`file://${block?.file?.path}`]
|
? block.metadata?.generateImageResponse?.images
|
||||||
: []
|
: block?.file?.path
|
||||||
return (
|
? [`file://${block?.file?.path}`]
|
||||||
<Container style={{ marginBottom: 8 }}>
|
: []
|
||||||
{images.map((src, index) => (
|
return (
|
||||||
<ImageViewer
|
<Container style={{ marginBottom: 8 }}>
|
||||||
src={src}
|
{images.map((src, index) => (
|
||||||
key={`image-${index}`}
|
<ImageViewer
|
||||||
style={{ maxWidth: 500, maxHeight: 'min(500px, 55vh)', borderRadius: 8 }}
|
src={src}
|
||||||
/>
|
key={`image-${index}`}
|
||||||
))}
|
style={{ maxWidth: 500, maxHeight: 500, padding: 5, borderRadius: 8 }}
|
||||||
</Container>
|
/>
|
||||||
)
|
))}
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
} else return null
|
||||||
}
|
}
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -33,5 +36,4 @@ const Container = styled.div`
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
`
|
`
|
||||||
|
|
||||||
export default React.memo(ImageBlock)
|
export default React.memo(ImageBlock)
|
||||||
|
|||||||
@ -42,6 +42,7 @@ const blockWrapperVariants = {
|
|||||||
const AnimatedBlockWrapper: React.FC<AnimatedBlockWrapperProps> = ({ children, enableAnimation }) => {
|
const AnimatedBlockWrapper: React.FC<AnimatedBlockWrapperProps> = ({ children, enableAnimation }) => {
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
className="block-wrapper"
|
||||||
variants={blockWrapperVariants}
|
variants={blockWrapperVariants}
|
||||||
initial={enableAnimation ? 'hidden' : 'static'}
|
initial={enableAnimation ? 'hidden' : 'static'}
|
||||||
animate={enableAnimation ? 'visible' : 'static'}>
|
animate={enableAnimation ? 'visible' : 'static'}>
|
||||||
@ -85,7 +86,7 @@ const MessageBlockRenderer: React.FC<Props> = ({ blocks, message }) => {
|
|||||||
const groupKey = block.map((imageBlock) => imageBlock.id).join('-')
|
const groupKey = block.map((imageBlock) => imageBlock.id).join('-')
|
||||||
return (
|
return (
|
||||||
<AnimatedBlockWrapper key={groupKey} enableAnimation={message.status.includes('ing')}>
|
<AnimatedBlockWrapper key={groupKey} enableAnimation={message.status.includes('ing')}>
|
||||||
<ImageBlockGroup $columns={block.length}>
|
<ImageBlockGroup>
|
||||||
{block.map((imageBlock) => (
|
{block.map((imageBlock) => (
|
||||||
<ImageBlock key={imageBlock.id} block={imageBlock as ImageMessageBlock} />
|
<ImageBlock key={imageBlock.id} block={imageBlock as ImageMessageBlock} />
|
||||||
))}
|
))}
|
||||||
@ -161,17 +162,16 @@ const MessageBlockRenderer: React.FC<Props> = ({ blocks, message }) => {
|
|||||||
|
|
||||||
export default React.memo(MessageBlockRenderer)
|
export default React.memo(MessageBlockRenderer)
|
||||||
|
|
||||||
const ImageBlockGroup = styled.div<{ $columns: number }>`
|
const ImageBlockGroup = styled.div`
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(${({ $columns }) => Math.min(3, $columns)}, minmax(200px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
width: 100%;
|
|
||||||
max-width: 960px;
|
max-width: 960px;
|
||||||
> * {
|
/* > * {
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
}
|
} */
|
||||||
@media (min-width: 1536px) {
|
@media (min-width: 1536px) {
|
||||||
grid-template-columns: repeat(4, minmax(250px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
max-width: 1280px;
|
max-width: 1280px;
|
||||||
> * {
|
> * {
|
||||||
min-width: 250px;
|
min-width: 250px;
|
||||||
|
|||||||
@ -81,14 +81,17 @@ const MessageItem: FC<Props> = ({
|
|||||||
|
|
||||||
const handleEditResend = useCallback(
|
const handleEditResend = useCallback(
|
||||||
async (blocks: MessageBlock[]) => {
|
async (blocks: MessageBlock[]) => {
|
||||||
|
const assistantWithTopicPrompt = topic.prompt
|
||||||
|
? { ...assistant, prompt: `${assistant.prompt}\n${topic.prompt}` }
|
||||||
|
: assistant
|
||||||
try {
|
try {
|
||||||
await resendUserMessageWithEdit(message, blocks, assistant)
|
await resendUserMessageWithEdit(message, blocks, assistantWithTopicPrompt)
|
||||||
stopEditing()
|
stopEditing()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to resend message:', error)
|
console.error('Failed to resend message:', error)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[message, resendUserMessageWithEdit, assistant, stopEditing]
|
[message, resendUserMessageWithEdit, assistant, stopEditing, topic.prompt]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleEditCancel = useCallback(() => {
|
const handleEditCancel = useCallback(() => {
|
||||||
|
|||||||
@ -40,7 +40,7 @@ const MessageBlockEditor: FC<Props> = ({ message, onSave, onResend, onCancel })
|
|||||||
const model = assistant.model || assistant.defaultModel
|
const model = assistant.model || assistant.defaultModel
|
||||||
const isVision = useMemo(() => isVisionModel(model), [model])
|
const isVision = useMemo(() => isVisionModel(model), [model])
|
||||||
const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision])
|
const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision])
|
||||||
const { pasteLongTextAsFile, pasteLongTextThreshold, fontSize } = useSettings()
|
const { pasteLongTextAsFile, pasteLongTextThreshold, fontSize, sendMessageShortcut } = useSettings()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const textareaRef = useRef<TextAreaRef>(null)
|
const textareaRef = useRef<TextAreaRef>(null)
|
||||||
const attachmentButtonRef = useRef<AttachmentButtonRef>(null)
|
const attachmentButtonRef = useRef<AttachmentButtonRef>(null)
|
||||||
@ -137,9 +137,8 @@ const MessageBlockEditor: FC<Props> = ({ message, onSave, onResend, onCancel })
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleClick = async (withResend?: boolean) => {
|
// 处理编辑区块并上传文件
|
||||||
if (isProcessing) return
|
const processEditedBlocks = async () => {
|
||||||
setIsProcessing(true)
|
|
||||||
const updatedBlocks = [...editedBlocks]
|
const updatedBlocks = [...editedBlocks]
|
||||||
if (files && files.length) {
|
if (files && files.length) {
|
||||||
const uploadedFiles = await FileManager.uploadFiles(files)
|
const uploadedFiles = await FileManager.uploadFiles(files)
|
||||||
@ -153,10 +152,48 @@ const MessageBlockEditor: FC<Props> = ({ message, onSave, onResend, onCancel })
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (withResend) {
|
return updatedBlocks
|
||||||
onResend(updatedBlocks)
|
}
|
||||||
} else {
|
|
||||||
onSave(updatedBlocks)
|
const handleSave = async () => {
|
||||||
|
if (isProcessing) return
|
||||||
|
setIsProcessing(true)
|
||||||
|
const updatedBlocks = await processEditedBlocks()
|
||||||
|
onSave(updatedBlocks)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleResend = async () => {
|
||||||
|
if (isProcessing) return
|
||||||
|
setIsProcessing(true)
|
||||||
|
const updatedBlocks = await processEditedBlocks()
|
||||||
|
onResend(updatedBlocks)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
if (message.role !== 'user') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const isEnterPressed = event.key === 'Enter'
|
||||||
|
|
||||||
|
if (isEnterPressed && !event.shiftKey && sendMessageShortcut === 'Enter') {
|
||||||
|
handleResend()
|
||||||
|
return event.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sendMessageShortcut === 'Shift+Enter' && isEnterPressed && event.shiftKey) {
|
||||||
|
handleResend()
|
||||||
|
return event.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sendMessageShortcut === 'Ctrl+Enter' && isEnterPressed && event.ctrlKey) {
|
||||||
|
handleResend()
|
||||||
|
return event.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sendMessageShortcut === 'Command+Enter' && isEnterPressed && event.metaKey) {
|
||||||
|
handleResend()
|
||||||
|
return event.preventDefault()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,6 +212,7 @@ const MessageBlockEditor: FC<Props> = ({ message, onSave, onResend, onCancel })
|
|||||||
handleTextChange(block.id, e.target.value)
|
handleTextChange(block.id, e.target.value)
|
||||||
resizeTextArea()
|
resizeTextArea()
|
||||||
}}
|
}}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
autoFocus
|
autoFocus
|
||||||
contextMenu="true"
|
contextMenu="true"
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
@ -240,13 +278,13 @@ const MessageBlockEditor: FC<Props> = ({ message, onSave, onResend, onCancel })
|
|||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title={t('common.save')}>
|
<Tooltip title={t('common.save')}>
|
||||||
<ToolbarButton type="text" onClick={() => handleClick()}>
|
<ToolbarButton type="text" onClick={handleSave}>
|
||||||
<Save size={16} />
|
<Save size={16} />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{message.role === 'user' && (
|
{message.role === 'user' && (
|
||||||
<Tooltip title={t('chat.resend')}>
|
<Tooltip title={t('chat.resend')}>
|
||||||
<ToolbarButton type="text" onClick={() => handleClick(true)}>
|
<ToolbarButton type="text" onClick={handleResend}>
|
||||||
<Send size={16} />
|
<Send size={16} />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@ -121,10 +121,13 @@ const MessageMenubar: FC<Props> = (props) => {
|
|||||||
const handleResendUserMessage = useCallback(
|
const handleResendUserMessage = useCallback(
|
||||||
async (messageUpdate?: Message) => {
|
async (messageUpdate?: Message) => {
|
||||||
if (!loading) {
|
if (!loading) {
|
||||||
await resendMessage(messageUpdate ?? message, assistant)
|
const assistantWithTopicPrompt = topic.prompt
|
||||||
|
? { ...assistant, prompt: `${assistant.prompt}\n${topic.prompt}` }
|
||||||
|
: assistant
|
||||||
|
await resendMessage(messageUpdate ?? message, assistantWithTopicPrompt)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[assistant, loading, message, resendMessage]
|
[assistant, loading, message, resendMessage, topic.prompt]
|
||||||
)
|
)
|
||||||
|
|
||||||
const { startEditing } = useMessageEditing()
|
const { startEditing } = useMessageEditing()
|
||||||
@ -319,8 +322,12 @@ const MessageMenubar: FC<Props> = (props) => {
|
|||||||
// const _message = resetAssistantMessage(message, selectedModel)
|
// const _message = resetAssistantMessage(message, selectedModel)
|
||||||
// editMessage(message.id, { ..._message }) // REMOVED
|
// editMessage(message.id, { ..._message }) // REMOVED
|
||||||
|
|
||||||
|
const assistantWithTopicPrompt = topic.prompt
|
||||||
|
? { ...assistant, prompt: `${assistant.prompt}\n${topic.prompt}` }
|
||||||
|
: assistant
|
||||||
|
|
||||||
// Call the function from the hook
|
// Call the function from the hook
|
||||||
regenerateAssistantMessage(message, assistant)
|
regenerateAssistantMessage(message, assistantWithTopicPrompt)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onMentionModel = async (e: React.MouseEvent) => {
|
const onMentionModel = async (e: React.MouseEvent) => {
|
||||||
@ -397,7 +404,8 @@ const MessageMenubar: FC<Props> = (props) => {
|
|||||||
menu={{
|
menu={{
|
||||||
style: {
|
style: {
|
||||||
maxHeight: 250,
|
maxHeight: 250,
|
||||||
overflowY: 'auto'
|
overflowY: 'auto',
|
||||||
|
backgroundClip: 'border-box'
|
||||||
},
|
},
|
||||||
items: [
|
items: [
|
||||||
...TranslateLanguageOptions.map((item) => ({
|
...TranslateLanguageOptions.map((item) => ({
|
||||||
|
|||||||
@ -199,7 +199,6 @@ const Topics: FC<TopicsTabProps> = ({ style }) => {
|
|||||||
if (summaryText) {
|
if (summaryText) {
|
||||||
const updatedTopic = { ...topic, name: summaryText, isNameManuallyEdited: false }
|
const updatedTopic = { ...topic, name: summaryText, isNameManuallyEdited: false }
|
||||||
updateTopic(updatedTopic)
|
updateTopic(updatedTopic)
|
||||||
topic.id === activeTopic.id && setActiveTopic(updatedTopic)
|
|
||||||
} else {
|
} else {
|
||||||
window.message?.error(t('message.error.fetchTopicName'))
|
window.message?.error(t('message.error.fetchTopicName'))
|
||||||
}
|
}
|
||||||
@ -223,7 +222,6 @@ const Topics: FC<TopicsTabProps> = ({ style }) => {
|
|||||||
if (name && topic?.name !== name) {
|
if (name && topic?.name !== name) {
|
||||||
const updatedTopic = { ...topic, name, isNameManuallyEdited: true }
|
const updatedTopic = { ...topic, name, isNameManuallyEdited: true }
|
||||||
updateTopic(updatedTopic)
|
updateTopic(updatedTopic)
|
||||||
topic.id === activeTopic.id && setActiveTopic(updatedTopic)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import styled from 'styled-components'
|
|||||||
|
|
||||||
import { getModelScopeToken, saveModelScopeToken, syncModelScopeServers } from './modelscopeSyncUtils'
|
import { getModelScopeToken, saveModelScopeToken, syncModelScopeServers } from './modelscopeSyncUtils'
|
||||||
import { getTokenFluxToken, saveTokenFluxToken, syncTokenFluxServers, TOKENFLUX_HOST } from './providers/tokenflux'
|
import { getTokenFluxToken, saveTokenFluxToken, syncTokenFluxServers, TOKENFLUX_HOST } from './providers/tokenflux'
|
||||||
|
import { getTokenLanYunToken, LANYUN_KEY_HOST, saveTokenLanYunToken, syncTokenLanYunServers } from './providers/lanyun'
|
||||||
|
|
||||||
// Provider configuration interface
|
// Provider configuration interface
|
||||||
interface ProviderConfig {
|
interface ProviderConfig {
|
||||||
@ -45,6 +46,17 @@ const providers: ProviderConfig[] = [
|
|||||||
getToken: getTokenFluxToken,
|
getToken: getTokenFluxToken,
|
||||||
saveToken: saveTokenFluxToken,
|
saveToken: saveTokenFluxToken,
|
||||||
syncServers: syncTokenFluxServers
|
syncServers: syncTokenFluxServers
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'lanyun',
|
||||||
|
name: '蓝耘科技',
|
||||||
|
description: '蓝耘科技云平台 MCP 服务',
|
||||||
|
discoverUrl: 'https://mcp.lanyun.net',
|
||||||
|
apiKeyUrl: LANYUN_KEY_HOST,
|
||||||
|
tokenFieldName: 'tokenLanyunToken',
|
||||||
|
getToken: getTokenLanYunToken,
|
||||||
|
saveToken: saveTokenLanYunToken,
|
||||||
|
syncServers: syncTokenLanYunServers
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
178
src/renderer/src/pages/mcp-servers/providers/lanyun.ts
Normal file
178
src/renderer/src/pages/mcp-servers/providers/lanyun.ts
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
import type { MCPServer } from '@renderer/types'
|
||||||
|
import i18next from 'i18next'
|
||||||
|
|
||||||
|
// Token storage constants and utilities
|
||||||
|
const TOKEN_STORAGE_KEY = 'tokenLanyunToken'
|
||||||
|
export const TOKENLANYUN_HOST = 'https://mcp.lanyun.net'
|
||||||
|
export const LANYUN_MCP_HOST = TOKENLANYUN_HOST + '/mcp/manager/selectListByApiKey'
|
||||||
|
export const LANYUN_KEY_HOST = TOKENLANYUN_HOST + '/#/manage/apiKey'
|
||||||
|
|
||||||
|
export const saveTokenLanYunToken = (token: string): void => {
|
||||||
|
localStorage.setItem(TOKEN_STORAGE_KEY, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTokenLanYunToken = (): string | null => {
|
||||||
|
return localStorage.getItem(TOKEN_STORAGE_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const clearTokenLanYunToken = (): void => {
|
||||||
|
localStorage.removeItem(TOKEN_STORAGE_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hasTokenLanYunToken = (): boolean => {
|
||||||
|
return !!getTokenLanYunToken()
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TokenLanYunServer {
|
||||||
|
id: string
|
||||||
|
/**
|
||||||
|
* locales 字段用于存储多语言信息。
|
||||||
|
* 其中 key(lang)为语言代码(如 'zh', 'en'),
|
||||||
|
* value 为该语言下的 name 和 description。
|
||||||
|
* 例如:
|
||||||
|
* {
|
||||||
|
* "zh": { name: "文档处理工具", description: "..." },
|
||||||
|
* "en": { name: "Document Processor", description: "..." }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
locales?: {
|
||||||
|
[lang: string]: {
|
||||||
|
description?: string
|
||||||
|
name?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chineseName?: string
|
||||||
|
description?: string
|
||||||
|
operationalUrls?: { url: string }[]
|
||||||
|
tags?: string[]
|
||||||
|
logoUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TokenLanYunSyncResult {
|
||||||
|
success: boolean
|
||||||
|
message: string
|
||||||
|
addedServers: MCPServer[]
|
||||||
|
errorDetails?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to fetch and process TokenLanYun servers
|
||||||
|
export const syncTokenLanYunServers = async (
|
||||||
|
token: string,
|
||||||
|
existingServers: MCPServer[]
|
||||||
|
): Promise<TokenLanYunSyncResult> => {
|
||||||
|
const t = i18next.t
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(LANYUN_MCP_HOST, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle authentication errors
|
||||||
|
if (response.status === 401 || response.status === 403) {
|
||||||
|
clearTokenLanYunToken()
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: t('settings.mcp.sync.unauthorized', 'Sync Unauthorized'),
|
||||||
|
addedServers: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle server errors
|
||||||
|
if (response.status === 500 || !response.ok) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: t('settings.mcp.sync.error'),
|
||||||
|
addedServers: [],
|
||||||
|
errorDetails: `Status: ${response.status}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process successful response
|
||||||
|
const data = await response.json()
|
||||||
|
if (data.code === 401) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: t('settings.mcp.sync.unauthorized', 'Sync Unauthorized'),
|
||||||
|
addedServers: [],
|
||||||
|
errorDetails: `Status: ${response.status}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (data.code === 500) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: t('settings.mcp.sync.error'),
|
||||||
|
addedServers: [],
|
||||||
|
errorDetails: `Status: ${response.status}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const servers: TokenLanYunServer[] = data.data || []
|
||||||
|
|
||||||
|
if (servers.length === 0) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: t('settings.mcp.sync.noServersAvailable', 'No MCP servers available'),
|
||||||
|
addedServers: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform Token servers to MCP servers format
|
||||||
|
const addedServers: MCPServer[] = []
|
||||||
|
console.log('TokenLanYun servers:', servers)
|
||||||
|
for (const server of servers) {
|
||||||
|
try {
|
||||||
|
if (!server.operationalUrls?.[0]?.url) continue
|
||||||
|
|
||||||
|
// If any existing server id contains '@lanyun', clear them before adding new ones
|
||||||
|
// if (existingServers.some((s) => s.id.startsWith('@lanyun'))) {
|
||||||
|
// for (let i = existingServers.length - 1; i >= 0; i--) {
|
||||||
|
// if (existingServers[i].id.startsWith('@lanyun')) {
|
||||||
|
// existingServers.splice(i, 1)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// Skip if server already exists after clearing
|
||||||
|
if (existingServers.some((s) => s.id === `@lanyun/${server.id}`)) continue
|
||||||
|
|
||||||
|
const mcpServer: MCPServer = {
|
||||||
|
id: `@lanyun/${server.id}`,
|
||||||
|
name:
|
||||||
|
server.chineseName || server.locales?.zh?.name || server.locales?.en?.name || `LanYun Server ${server.id}`,
|
||||||
|
description: server.description || '',
|
||||||
|
type: 'sse',
|
||||||
|
baseUrl: server.operationalUrls[0].url,
|
||||||
|
command: '',
|
||||||
|
args: [],
|
||||||
|
env: {},
|
||||||
|
isActive: true,
|
||||||
|
provider: '蓝耘科技',
|
||||||
|
providerUrl: server.operationalUrls[0].url,
|
||||||
|
logoUrl: server.logoUrl || '',
|
||||||
|
tags: server.tags ?? (server.chineseName ? [server.chineseName] : [])
|
||||||
|
}
|
||||||
|
|
||||||
|
addedServers.push(mcpServer)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error processing LanYun server:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: t('settings.mcp.sync.success', { count: addedServers.length }),
|
||||||
|
addedServers
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('TokenLanyun sync error:', error)
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: t('settings.mcp.sync.error'),
|
||||||
|
addedServers: [],
|
||||||
|
errorDetails: String(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import { PlusOutlined, RedoOutlined } from '@ant-design/icons'
|
import { PlusOutlined, RedoOutlined } from '@ant-design/icons'
|
||||||
|
import AiProvider from '@renderer/aiCore'
|
||||||
import IcImageUp from '@renderer/assets/images/paintings/ic_ImageUp.svg'
|
import IcImageUp from '@renderer/assets/images/paintings/ic_ImageUp.svg'
|
||||||
import { NavbarCenter, NavbarMain, NavbarRight } from '@renderer/components/app/Navbar'
|
import { NavbarCenter, NavbarMain, NavbarRight } from '@renderer/components/app/Navbar'
|
||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
@ -11,7 +12,6 @@ import { usePaintings } from '@renderer/hooks/usePaintings'
|
|||||||
import { useAllProviders } from '@renderer/hooks/useProvider'
|
import { useAllProviders } from '@renderer/hooks/useProvider'
|
||||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import AiProvider from '@renderer/aiCore'
|
|
||||||
import FileManager from '@renderer/services/FileManager'
|
import FileManager from '@renderer/services/FileManager'
|
||||||
import { translateText } from '@renderer/services/TranslateService'
|
import { translateText } from '@renderer/services/TranslateService'
|
||||||
import { useAppDispatch } from '@renderer/store'
|
import { useAppDispatch } from '@renderer/store'
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { PlusOutlined, RedoOutlined } from '@ant-design/icons'
|
import { PlusOutlined, RedoOutlined } from '@ant-design/icons'
|
||||||
|
import AiProvider from '@renderer/aiCore'
|
||||||
import ImageSize1_1 from '@renderer/assets/images/paintings/image-size-1-1.svg'
|
import ImageSize1_1 from '@renderer/assets/images/paintings/image-size-1-1.svg'
|
||||||
import ImageSize1_2 from '@renderer/assets/images/paintings/image-size-1-2.svg'
|
import ImageSize1_2 from '@renderer/assets/images/paintings/image-size-1-2.svg'
|
||||||
import ImageSize3_2 from '@renderer/assets/images/paintings/image-size-3-2.svg'
|
import ImageSize3_2 from '@renderer/assets/images/paintings/image-size-3-2.svg'
|
||||||
@ -16,7 +17,6 @@ import { usePaintings } from '@renderer/hooks/usePaintings'
|
|||||||
import { useAllProviders } from '@renderer/hooks/useProvider'
|
import { useAllProviders } from '@renderer/hooks/useProvider'
|
||||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import AiProvider from '@renderer/aiCore'
|
|
||||||
import { getProviderByModel } from '@renderer/services/AssistantService'
|
import { getProviderByModel } from '@renderer/services/AssistantService'
|
||||||
import FileManager from '@renderer/services/FileManager'
|
import FileManager from '@renderer/services/FileManager'
|
||||||
import { translateText } from '@renderer/services/TranslateService'
|
import { translateText } from '@renderer/services/TranslateService'
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
|
import { QuestionCircleOutlined } from '@ant-design/icons'
|
||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { useAppDispatch } from '@renderer/store'
|
import { useAppDispatch } from '@renderer/store'
|
||||||
import { setEnableTopicNaming, setTopicNamingPrompt } from '@renderer/store/settings'
|
import { setEnableTopicNaming, setTopicNamingPrompt } from '@renderer/store/settings'
|
||||||
import { Button, Input, Modal, Switch } from 'antd'
|
import { Button, Divider, Flex, Input, Modal, Popover, Switch } from 'antd'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
@ -36,6 +37,8 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
|||||||
|
|
||||||
TopicNamingModalPopup.hide = onCancel
|
TopicNamingModalPopup.hide = onCancel
|
||||||
|
|
||||||
|
const promptVarsContent = <pre>{t('agents.add.prompt.variables.tip.content')}</pre>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={t('settings.models.topic_naming_model_setting_title')}
|
title={t('settings.models.topic_naming_model_setting_title')}
|
||||||
@ -45,14 +48,20 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
|||||||
afterClose={onClose}
|
afterClose={onClose}
|
||||||
transitionName="animation-move-down"
|
transitionName="animation-move-down"
|
||||||
footer={null}
|
footer={null}
|
||||||
width={500}
|
|
||||||
centered>
|
centered>
|
||||||
|
<Divider style={{ margin: '10px 0' }} />
|
||||||
<HStack style={{ gap: 10, marginBottom: 20, marginTop: 20 }} alignItems="center">
|
<HStack style={{ gap: 10, marginBottom: 20, marginTop: 20 }} alignItems="center">
|
||||||
<div>{t('settings.models.enable_topic_naming')}</div>
|
<div>{t('settings.models.enable_topic_naming')}</div>
|
||||||
<Switch checked={enableTopicNaming} onChange={(v) => dispatch(setEnableTopicNaming(v))} />
|
<Switch checked={enableTopicNaming} onChange={(v) => dispatch(setEnableTopicNaming(v))} />
|
||||||
</HStack>
|
</HStack>
|
||||||
<div style={{ marginBottom: 8 }}>
|
<Divider style={{ margin: '10px 0' }} />
|
||||||
<div style={{ marginBottom: 10 }}>{t('settings.models.topic_naming_prompt')}</div>
|
<div style={{ marginBottom: 20 }}>
|
||||||
|
<Flex align="center" style={{ marginBottom: 10, gap: 5 }}>
|
||||||
|
<div>{t('settings.models.topic_naming_prompt')}</div>
|
||||||
|
<Popover title={t('agents.add.prompt.variables.tip.title')} content={promptVarsContent}>
|
||||||
|
<QuestionCircleOutlined size={14} style={{ color: 'var(--color-text-2)' }} />
|
||||||
|
</Popover>
|
||||||
|
</Flex>
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
rows={4}
|
rows={4}
|
||||||
value={topicNamingPrompt || t('prompts.title')}
|
value={topicNamingPrompt || t('prompts.title')}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { CheckOutlined, LoadingOutlined } from '@ant-design/icons'
|
import { CheckOutlined, LoadingOutlined } from '@ant-design/icons'
|
||||||
|
import { isOpenAIProvider } from '@renderer/aiCore/clients/ApiClientFactory'
|
||||||
import OpenAIAlert from '@renderer/components/Alert/OpenAIAlert'
|
import OpenAIAlert from '@renderer/components/Alert/OpenAIAlert'
|
||||||
import { StreamlineGoodHealthAndWellBeing } from '@renderer/components/Icons/SVGIcon'
|
import { StreamlineGoodHealthAndWellBeing } from '@renderer/components/Icons/SVGIcon'
|
||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
@ -7,7 +8,6 @@ import { PROVIDER_CONFIG } from '@renderer/config/providers'
|
|||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
import { useAllProviders, useProvider, useProviders } from '@renderer/hooks/useProvider'
|
import { useAllProviders, useProvider, useProviders } from '@renderer/hooks/useProvider'
|
||||||
import i18n from '@renderer/i18n'
|
import i18n from '@renderer/i18n'
|
||||||
import { isOpenAIProvider } from '@renderer/aiCore/clients/ApiClientFactory'
|
|
||||||
import { checkApi, formatApiKeys } from '@renderer/services/ApiService'
|
import { checkApi, formatApiKeys } from '@renderer/services/ApiService'
|
||||||
import { checkModelsHealth, getModelCheckSummary } from '@renderer/services/HealthCheckService'
|
import { checkModelsHealth, getModelCheckSummary } from '@renderer/services/HealthCheckService'
|
||||||
import { isProviderSupportAuth } from '@renderer/services/ProviderService'
|
import { isProviderSupportAuth } from '@renderer/services/ProviderService'
|
||||||
|
|||||||
@ -32,7 +32,14 @@ const SettingsActionsListHeader = memo(({ customItemsCount, maxCustomItems, onRe
|
|||||||
? t('selection.settings.actions.add_tooltip.disabled', { max: maxCustomItems })
|
? t('selection.settings.actions.add_tooltip.disabled', { max: maxCustomItems })
|
||||||
: t('selection.settings.actions.add_tooltip.enabled')
|
: t('selection.settings.actions.add_tooltip.enabled')
|
||||||
}>
|
}>
|
||||||
<Button type="primary" icon={<Plus size={16} />} onClick={onAdd} disabled={isCustomItemLimitReached} />
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<Plus size={16} />}
|
||||||
|
onClick={onAdd}
|
||||||
|
disabled={isCustomItemLimitReached}
|
||||||
|
style={{ paddingInline: '8px' }}>
|
||||||
|
{t('selection.settings.actions.custom')}
|
||||||
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Row>
|
</Row>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -416,7 +416,10 @@ export async function fetchTranslate({ content, assistant, onResponse }: FetchTr
|
|||||||
export async function fetchMessagesSummary({ messages, assistant }: { messages: Message[]; assistant: Assistant }) {
|
export async function fetchMessagesSummary({ messages, assistant }: { messages: Message[]; assistant: Assistant }) {
|
||||||
const prompt = (getStoreSetting('topicNamingPrompt') as string) || i18n.t('prompts.title')
|
const prompt = (getStoreSetting('topicNamingPrompt') as string) || i18n.t('prompts.title')
|
||||||
const model = getTopNamingModel() || assistant.model || getDefaultModel()
|
const model = getTopNamingModel() || assistant.model || getDefaultModel()
|
||||||
const userMessages = takeRight(messages, 5)
|
const userMessages = takeRight(messages, 5).map((message) => ({
|
||||||
|
...message,
|
||||||
|
content: getMainTextContent(message)
|
||||||
|
}))
|
||||||
|
|
||||||
const provider = getProviderByModel(model)
|
const provider = getProviderByModel(model)
|
||||||
|
|
||||||
|
|||||||
@ -137,7 +137,7 @@ export interface ImageCompleteChunk {
|
|||||||
/**
|
/**
|
||||||
* The image content of the chunk
|
* The image content of the chunk
|
||||||
*/
|
*/
|
||||||
image?: { type: 'base64'; images: string[] }
|
image?: { type: 'url' | 'base64'; images: string[] }
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ThinkingDeltaChunk {
|
export interface ThinkingDeltaChunk {
|
||||||
|
|||||||
@ -7,9 +7,9 @@ import {
|
|||||||
convertMathFormula,
|
convertMathFormula,
|
||||||
findCitationInChildren,
|
findCitationInChildren,
|
||||||
getCodeBlockId,
|
getCodeBlockId,
|
||||||
|
markdownToPlainText,
|
||||||
removeTrailingDoubleSpaces,
|
removeTrailingDoubleSpaces,
|
||||||
updateCodeBlock,
|
updateCodeBlock
|
||||||
markdownToPlainText
|
|
||||||
} from '../markdown'
|
} from '../markdown'
|
||||||
|
|
||||||
describe('markdown', () => {
|
describe('markdown', () => {
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest'
|
|||||||
import {
|
import {
|
||||||
firstLetter,
|
firstLetter,
|
||||||
generateColorFromChar,
|
generateColorFromChar,
|
||||||
|
getBaseModelName,
|
||||||
getBriefInfo,
|
getBriefInfo,
|
||||||
getDefaultGroupName,
|
getDefaultGroupName,
|
||||||
getFirstCharacter,
|
getFirstCharacter,
|
||||||
@ -157,6 +158,38 @@ describe('naming', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('getBaseModelName', () => {
|
||||||
|
it('should extract base model name with single delimiter', () => {
|
||||||
|
expect(getBaseModelName('DeepSeek/DeepSeek-R1')).toBe('DeepSeek-R1')
|
||||||
|
expect(getBaseModelName('openai/gpt-4.1')).toBe('gpt-4.1')
|
||||||
|
expect(getBaseModelName('anthropic/claude-3.5-sonnet')).toBe('claude-3.5-sonnet')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should extract base model name with multiple levels', () => {
|
||||||
|
expect(getBaseModelName('Pro/deepseek-ai/DeepSeek-R1')).toBe('DeepSeek-R1')
|
||||||
|
expect(getBaseModelName('org/team/group/model')).toBe('model')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return original id if no delimiter found', () => {
|
||||||
|
expect(getBaseModelName('deepseek-r1')).toBe('deepseek-r1')
|
||||||
|
expect(getBaseModelName('deepseek-r1:free')).toBe('deepseek-r1:free')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle edge cases', () => {
|
||||||
|
// 验证空字符串的情况
|
||||||
|
expect(getBaseModelName('')).toBe('')
|
||||||
|
// 验证以分隔符结尾的字符串
|
||||||
|
expect(getBaseModelName('model/')).toBe('')
|
||||||
|
expect(getBaseModelName('model/name/')).toBe('')
|
||||||
|
// 验证以分隔符开头的字符串
|
||||||
|
expect(getBaseModelName('/model')).toBe('model')
|
||||||
|
expect(getBaseModelName('/path/to/model')).toBe('model')
|
||||||
|
// 验证连续分隔符的情况
|
||||||
|
expect(getBaseModelName('model//name')).toBe('name')
|
||||||
|
expect(getBaseModelName('model///name')).toBe('name')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('generateColorFromChar', () => {
|
describe('generateColorFromChar', () => {
|
||||||
it('should generate a valid hex color code', () => {
|
it('should generate a valid hex color code', () => {
|
||||||
// 验证生成有效的十六进制颜色代码
|
// 验证生成有效的十六进制颜色代码
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import remarkParse from 'remark-parse'
|
import remarkParse from 'remark-parse'
|
||||||
import remarkStringify from 'remark-stringify'
|
import remarkStringify from 'remark-stringify'
|
||||||
|
import removeMarkdown from 'remove-markdown'
|
||||||
import { unified } from 'unified'
|
import { unified } from 'unified'
|
||||||
import { visit } from 'unist-util-visit'
|
import { visit } from 'unist-util-visit'
|
||||||
import removeMarkdown from 'remove-markdown'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更彻底的查找方法,递归搜索所有子元素
|
* 更彻底的查找方法,递归搜索所有子元素
|
||||||
|
|||||||
@ -46,6 +46,20 @@ export const getDefaultGroupName = (id: string, provider?: string): string => {
|
|||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从模型 ID 中提取基础名称。
|
||||||
|
* 例如:
|
||||||
|
* - 'deepseek/deepseek-r1' => 'deepseek-r1'
|
||||||
|
* - 'deepseek-ai/deepseek/deepseek-r1' => 'deepseek-r1'
|
||||||
|
* @param {string} id 模型 ID
|
||||||
|
* @param {string} [delimiter='/'] 分隔符,默认为 '/'
|
||||||
|
* @returns {string} 基础名称
|
||||||
|
*/
|
||||||
|
export const getBaseModelName = (id: string, delimiter: string = '/'): string => {
|
||||||
|
const parts = id.split(delimiter)
|
||||||
|
return parts[parts.length - 1]
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用于获取 avatar 名字的辅助函数,会取出字符串的第一个字符,支持表情符号。
|
* 用于获取 avatar 名字的辅助函数,会取出字符串的第一个字符,支持表情符号。
|
||||||
* @param {string} str 输入字符串
|
* @param {string} str 输入字符串
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import store from '@renderer/store'
|
import store from '@renderer/store'
|
||||||
import { MCPTool } from '@renderer/types'
|
import { Assistant, MCPTool } from '@renderer/types'
|
||||||
|
|
||||||
export const SYSTEM_PROMPT = `In this environment you have access to a set of tools you can use to answer the user's question. \
|
export const SYSTEM_PROMPT = `In this environment you have access to a set of tools you can use to answer the user's question. \
|
||||||
You can use one tool per message, and will receive the result of that tool use in the user's response. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use.
|
You can use one tool per message, and will receive the result of that tool use in the user's response. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use.
|
||||||
|
|
||||||
@ -147,7 +148,11 @@ ${availableTools}
|
|||||||
</tools>`
|
</tools>`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const buildSystemPrompt = async (userSystemPrompt: string, tools?: MCPTool[]): Promise<string> => {
|
export const buildSystemPrompt = async (
|
||||||
|
userSystemPrompt: string,
|
||||||
|
tools?: MCPTool[],
|
||||||
|
assistant?: Assistant
|
||||||
|
): Promise<string> => {
|
||||||
if (typeof userSystemPrompt === 'string') {
|
if (typeof userSystemPrompt === 'string') {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
if (userSystemPrompt.includes('{{date}}')) {
|
if (userSystemPrompt.includes('{{date}}')) {
|
||||||
@ -197,13 +202,22 @@ export const buildSystemPrompt = async (userSystemPrompt: string, tools?: MCPToo
|
|||||||
|
|
||||||
if (userSystemPrompt.includes('{{model_name}}')) {
|
if (userSystemPrompt.includes('{{model_name}}')) {
|
||||||
try {
|
try {
|
||||||
const modelName = store.getState().llm.defaultModel.name
|
userSystemPrompt = userSystemPrompt.replace(/{{model_name}}/g, assistant?.model?.name || 'Unknown Model')
|
||||||
userSystemPrompt = userSystemPrompt.replace(/{{model_name}}/g, modelName)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to get model name:', error)
|
console.error('Failed to get model name:', error)
|
||||||
userSystemPrompt = userSystemPrompt.replace(/{{model_name}}/g, 'Unknown Model')
|
userSystemPrompt = userSystemPrompt.replace(/{{model_name}}/g, 'Unknown Model')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (userSystemPrompt.includes('{{username}}')) {
|
||||||
|
try {
|
||||||
|
const username = store.getState().settings.userName || 'Unknown Username'
|
||||||
|
userSystemPrompt = userSystemPrompt.replace(/{{username}}/g, username)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get username:', error)
|
||||||
|
userSystemPrompt = userSystemPrompt.replace(/{{username}}/g, 'Unknown Username')
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tools && tools.length > 0) {
|
if (tools && tools.length > 0) {
|
||||||
|
|||||||
@ -6030,7 +6030,7 @@ __metadata:
|
|||||||
|
|
||||||
"app-builder-lib@patch:app-builder-lib@npm%3A26.0.15#~/.yarn/patches/app-builder-lib-npm-26.0.15-360e5b0476.patch":
|
"app-builder-lib@patch:app-builder-lib@npm%3A26.0.15#~/.yarn/patches/app-builder-lib-npm-26.0.15-360e5b0476.patch":
|
||||||
version: 26.0.15
|
version: 26.0.15
|
||||||
resolution: "app-builder-lib@patch:app-builder-lib@npm%3A26.0.15#~/.yarn/patches/app-builder-lib-npm-26.0.15-360e5b0476.patch::version=26.0.15&hash=b02ae9"
|
resolution: "app-builder-lib@patch:app-builder-lib@npm%3A26.0.15#~/.yarn/patches/app-builder-lib-npm-26.0.15-360e5b0476.patch::version=26.0.15&hash=1f4887"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@develar/schema-utils": "npm:~2.6.5"
|
"@develar/schema-utils": "npm:~2.6.5"
|
||||||
"@electron/asar": "npm:3.4.1"
|
"@electron/asar": "npm:3.4.1"
|
||||||
@ -6068,7 +6068,7 @@ __metadata:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
dmg-builder: 26.0.15
|
dmg-builder: 26.0.15
|
||||||
electron-builder-squirrel-windows: 26.0.15
|
electron-builder-squirrel-windows: 26.0.15
|
||||||
checksum: 10c0/616072842c01f9f65283c95bf5642106c32bc3c6679672955f57b48bae9c28de10e18f2005d0e6e46cb2cb560dda3869ebf1412d3db50b7872c5f660581ad6db
|
checksum: 10c0/5de2bd593b21e464585ffa3424e053d41f8569b14ba2a00f29f84cb0b83347a7da3653587f9ef8b5d2f6d1e5bfc4081956b9d72f180d65960db49b5ac84b73d4
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user