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:
kangfenmao 2025-06-15 11:39:21 +08:00
commit facf29e02b
47 changed files with 884 additions and 109 deletions

View File

@ -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

View File

@ -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)."
}, },

View File

@ -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'
}, },

View File

@ -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 })
}
} }
/** /**

View File

@ -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`
} }

View File

@ -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
} }

View File

@ -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

View File

@ -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

View File

@ -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. 处理用户消息

View File

@ -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

View File

@ -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 }

View File

@ -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 {

View File

@ -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 {

View File

@ -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

View 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')
})
})
})

View File

@ -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)
) )
} }

View File

@ -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.",

View File

@ -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": "デフォルト機能にリセット(カスタム機能は保持)",

View File

@ -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": "Сбросить стандартные действия. Пользовательские останутся.",

View File

@ -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": "重置为默认功能,自定义功能不会被删除",

View File

@ -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": "重設為預設功能,自訂功能不會被刪除",

View File

@ -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": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτόν τον ειδικό;",

View File

@ -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?",

View File

@ -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 ?",

View File

@ -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?",

View File

@ -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) {

View File

@ -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)

View File

@ -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;

View File

@ -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(() => {

View File

@ -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>

View File

@ -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) => ({

View File

@ -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)
} }
} }
}, },

View File

@ -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
} }
] ]

View 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
* keylang '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)
}
}
}

View File

@ -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'

View File

@ -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'

View File

@ -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')}

View File

@ -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'

View File

@ -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>
) )

View File

@ -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)

View File

@ -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 {

View File

@ -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', () => {

View File

@ -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', () => {
// 验证生成有效的十六进制颜色代码 // 验证生成有效的十六进制颜色代码

View File

@ -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'
/** /**
* *

View File

@ -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

View File

@ -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) {

View File

@ -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