Compare commits

...

25 Commits

Author SHA1 Message Date
kangfenmao
0b0f0b6416 chore(version): 1.4.3 2025-06-16 16:21:06 +08:00
LANYUN
0e0c37dc17 feat: Add new provider Lanyun Cloud MaaS (#7033)
* Add files via upload

添加蓝耘logo图片

* 添加lanyun api及站点信息

* fix:修改引号

---------

Co-authored-by: 亢奋猫 <kangfenmao@qq.com>
2025-06-16 10:22:30 +08:00
fullex
9114bffb80 fix(SelectionService): Win10 showing problem & AlwaysOnTop level (#7215)
refactor(SelectionService): enhance logging and adjust window behavior for Windows compatibility

- Updated logInfo method to include a forceShow parameter for improved logging control.
- Ensured toolbar window is set to always on top when shown.
- Commented out setOpacity calls to prevent transparency issues on Windows 10.
2025-06-16 10:22:30 +08:00
自由的世界人
30d3afbf9b fix: prevent update button from rendering when auto-check for updates… (#7212)
fix: prevent update button from rendering when auto-check for updates is disabled
2025-06-16 10:22:30 +08:00
kangfenmao
f62101e4c1 lint(SyncServersPopup): fix SyncServersPopup lint error 2025-06-16 10:22:30 +08:00
Aichaellee
1223c39250 feat:add lanyun mcp server 2025-06-15 11:18:15 +08:00
Chen Tao
e8c14a0e4d fix: 7127 (#7196) 2025-06-15 10:13:36 +08:00
Wang Jiyuan
6eb501a370 feat: add prompt variables docs on topic naming modal popup (#7175) 2025-06-15 10:11:57 +08:00
Wang Jiyuan
f0f0542a09 fix: remove margin-bottom for loading animation (#7191)
* fix: remove margin-bottom for loading animation

* fix: just need to remove the margin-bottom of the last block
2025-06-15 10:11:37 +08:00
Wang Jiyuan
37c9829f43 fix: transparent background on translate dropdown (#7189) 2025-06-15 10:11:28 +08:00
Wang Jiyuan
b68a8d9dfc fix: missing topic prompt on resend/regenerate and duplicate prevention (#7173)
* fix: completion doesn't include topic prompt

* fix: Multiple additions of topic prompts

* fix: improve logic

* fix: improve logic
2025-06-15 10:11:16 +08:00
kangfenmao
59a1506689 chore(release): update fetch depth in GitHub Actions workflow
- Changed the fetch depth to 0 in the release workflow to ensure all history is available for tagging. This adjustment improves the accuracy of the release process.
2025-06-15 10:10:27 +08:00
Wang Jiyuan
699eb2b384 feat: add prompt variable "username" (#7174) 2025-06-15 10:10:19 +08:00
fullex
470ce44316 fix(SelectionAssistant): make add custom action button bigger (#7185)
fix: make add custom action button bigger
2025-06-15 10:09:59 +08:00
beyondkmp
33f636f7d8 feat: clean up Windows license files (#7133)
* feat: enable minification in build configurations and clean up Windows license files

- Added minification option to the build configurations in electron.vite.config.ts to optimize output size.
- Updated after-pack.js to remove unnecessary license files on Windows, improving the packaging process.

* refactor: remove minification from build configurations in electron.vite.config.ts

- Eliminated the minification option from the build settings in electron.vite.config.ts to streamline the build process.
- This change may improve build times and simplify configuration management.

---------

Co-authored-by: beyondkmp <beyondkmkp@gmail.com>
2025-06-15 10:09:05 +08:00
one
a34955c4b6 fix(MermaidPreview): re-render mermaid on display change (#7058)
* fix(MermaidPreview): re-render mermaid on display change

* test: add tests for MermaidPreview
2025-06-15 10:08:16 +08:00
one
267b1a92e8 refactor(CodeEditor): remove the right border of gutters (#7137)
refactor: remove the right border of gutters
2025-06-15 10:08:03 +08:00
beyondkmp
f26bc19e34 feat: Enhance AppUpdater for Windows installation directory support (#7135)
- Added support for setting the installation directory for the autoUpdater on Windows using NsisUpdater.
- Imported the 'path' module to dynamically determine the installation path based on the executable location.
- This change improves the updater's functionality and ensures a smoother installation experience for Windows users.

Co-authored-by: beyondkmp <beyondkmkp@gmail.com>
2025-06-15 10:07:53 +08:00
Wang Jiyuan
ea605709e5 fix: token usage always display when assistant msg generation aborted (#7121)
* fix: token usage always display when assistant msg generation aborted

* remove console.log
2025-06-15 10:07:10 +08:00
one
d606d0077d fix: start animation only if the topic should be renamed (#7125) 2025-06-15 10:06:24 +08:00
one
22c85781e5 feat: animate topic renaming (#6794)
* feat: animate topic renaming

* fix: load messages before renaming a topic

* refactor: better error handling

* refactor: make function names more reasonable

* refactor: update shimmer colors

* refactor: use typing effect
2025-06-15 10:06:24 +08:00
kangfenmao
f42fbb511e refactor: replace 302ai PNG with WEBP format and update provider configurations
- Deleted the old PNG logo for 302ai and added a new WEBP version.
- Updated the provider configuration to use the new WEBP logo.
- Added translations for the new Cephalon provider in Japanese and Russian.
- Disabled the 302ai and Cephalon providers in the initial state of the store.
- Adjusted migration logic to accommodate the new provider setup.
2025-06-15 10:05:21 +08:00
JI4JUN
5a257cade2 feat: support 302ai provider (#7044)
* feat(porvider): add provider 302ai

* style(provider): change provider name 302AI to 302.AI

* style(provider): system models replacement of 302.AI provider

---------

Co-authored-by: 亢奋猫 <kangfenmao@qq.com>
2025-06-15 10:05:17 +08:00
HzTTT
e8f819fb46 feat:add cephalon provider (#7050)
* feat: add Cephalon provider and related assets

* add Cephalon logo image
* update models to include Cephalon's DeepSeek-R1
* add Cephalon provider configuration and API details
* include Cephalon translations in multiple languages
* update store to initialize Cephalon as a provider
* increment version for migration

* feat: update Cephalon provider configuration and assets

* add Cephalon logo image
* enable Cephalon provider in the store
* remove previous disabled configuration for Cephalon

* fix: update Cephalon provider model URL

* fix: update official website URL for Cephalon provider
2025-06-15 10:05:13 +08:00
one
7088a94489 fix(Markdown): inline math overflow (#7095) 2025-06-15 10:05:08 +08:00
41 changed files with 954 additions and 104 deletions

View File

@@ -27,7 +27,7 @@ jobs:
- name: Check out Git repository - name: Check out Git repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
ref: main fetch-depth: 0
- name: Get release tag - name: Get release tag
id: get-tag id: get-tag
@@ -149,4 +149,4 @@ jobs:
token: ${{ secrets.REPO_DISPATCH_TOKEN }} token: ${{ secrets.REPO_DISPATCH_TOKEN }}
repository: CherryHQ/cherry-studio-docs repository: CherryHQ/cherry-studio-docs
event-type: update-download-version event-type: update-download-version
client-payload: '{"version": "${{ steps.get-tag.outputs.tag }}"}' client-payload: '{"version": "${{ steps.get-tag.outputs.tag }}"}'

View File

@@ -107,11 +107,7 @@ afterSign: scripts/notarize.js
artifactBuildCompleted: scripts/artifact-build-completed.js artifactBuildCompleted: scripts/artifact-build-completed.js
releaseInfo: releaseInfo:
releaseNotes: | releaseNotes: |
划词助手:支持文本选择快捷键、开关快捷键、思考块支持和引用功能 服务商新增端脑云、302.AI、蓝耘服务商
复制功能新增纯文本复制去除Markdown格式符号 MCP: 新增蓝耘 MCP 服务器
知识库支持设置向量维度修复Ollama分数错误和维度编辑问题 实现话题重命名动画效果
多语言:增加模型名称多语言提示和翻译源语言手动选择 错误修复
文件管理:修复主题/消息删除时文件未清理问题,优化文件选择流程
模型修复Gemini模型推理预算、Voyage AI嵌入问题和DeepSeek翻译模型更新
图像功能统一图片查看器支持Base64图片渲染修复图片预览相关问题
UI实现标签折叠/拖拽排序,修复气泡溢出,增加引文索引显示

View File

@@ -1,6 +1,6 @@
{ {
"name": "CherryStudio", "name": "CherryStudio",
"version": "1.4.2", "version": "1.4.3",
"private": true, "private": true,
"description": "A powerful AI assistant for producer.", "description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js", "main": "./out/main/index.js",

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

@@ -5,7 +5,8 @@ import { FeedUrl } from '@shared/config/constant'
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

@@ -285,7 +285,7 @@ export class SelectionService {
this.processTriggerMode() this.processTriggerMode()
this.started = true this.started = true
this.logInfo('SelectionService Started') this.logInfo('SelectionService Started', true)
return true return true
} }
@@ -319,7 +319,7 @@ export class SelectionService {
this.closePreloadedActionWindows() this.closePreloadedActionWindows()
this.started = false this.started = false
this.logInfo('SelectionService Stopped') this.logInfo('SelectionService Stopped', true)
return true return true
} }
@@ -335,7 +335,7 @@ export class SelectionService {
this.selectionHook = null this.selectionHook = null
this.initStatus = false this.initStatus = false
SelectionService.instance = null SelectionService.instance = null
this.logInfo('SelectionService Quitted') this.logInfo('SelectionService Quitted', true)
} }
/** /**
@@ -456,8 +456,18 @@ export class SelectionService {
x: posX, x: posX,
y: posY y: posY
}) })
//set the window to always on top (highest level)
//should set every time the window is shown
this.toolbarWindow!.setAlwaysOnTop(true, 'screen-saver')
this.toolbarWindow!.show() this.toolbarWindow!.show()
this.toolbarWindow!.setOpacity(1)
/**
* In Windows 10, setOpacity(1) will make the window completely transparent
* It's a strange behavior, so we don't use it for compatibility
*/
// this.toolbarWindow!.setOpacity(1)
this.startHideByMouseKeyListener() this.startHideByMouseKeyListener()
} }
@@ -467,7 +477,7 @@ export class SelectionService {
public hideToolbar(): void { public hideToolbar(): void {
if (!this.isToolbarAlive()) return if (!this.isToolbarAlive()) return
this.toolbarWindow!.setOpacity(0) // this.toolbarWindow!.setOpacity(0)
this.toolbarWindow!.hide() this.toolbarWindow!.hide()
this.stopHideByMouseKeyListener() this.stopHideByMouseKeyListener()
@@ -1264,8 +1274,10 @@ export class SelectionService {
this.isIpcHandlerRegistered = true this.isIpcHandlerRegistered = true
} }
private logInfo(message: string) { private logInfo(message: string, forceShow: boolean = false) {
isDev && Logger.info('[SelectionService] Info: ', message) if (isDev || forceShow) {
Logger.info('[SelectionService] Info: ', message)
}
} }
private logError(...args: [...string[], Error]) { private logError(...args: [...string[], Error]) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -136,6 +136,10 @@ ul {
display: flow-root; display: flow-root;
} }
.block-wrapper:last-child > *:last-child {
margin-bottom: 0;
}
.message-content-container > *:last-child { .message-content-container > *:last-child {
margin-bottom: 0; margin-bottom: 0;
} }

View File

@@ -295,13 +295,16 @@ emoji-picker {
--border-size: 0; --border-size: 0;
} }
.katex-display { .katex,
mjx-container {
display: inline-block;
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
} overflow-wrap: break-word;
vertical-align: middle;
mjx-container { max-width: 100%;
overflow-x: auto; padding: 1px 2px;
margin-top: -2px;
} }
/* CodeMirror 相关样式 */ /* CodeMirror 相关样式 */
@@ -318,6 +321,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

@@ -429,7 +429,86 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
group: 'deepseek-ai' group: 'deepseek-ai'
} }
], ],
'302ai': [
{
id: 'deepseek-chat',
name: 'deepseek-chat',
provider: '302ai',
group: 'DeepSeek'
},
{
id: 'deepseek-reasoner',
name: 'deepseek-reasoner',
provider: '302ai',
group: 'DeepSeek'
},
{
id: 'chatgpt-4o-latest',
name: 'chatgpt-4o-latest',
provider: '302ai',
group: 'OpenAI'
},
{
id: 'gpt-4.1',
name: 'gpt-4.1',
provider: '302ai',
group: 'OpenAI'
},
{
id: 'o3',
name: 'o3',
provider: '302ai',
group: 'OpenAI'
},
{
id: 'o4-mini',
name: 'o4-mini',
provider: '302ai',
group: 'OpenAI'
},
{
id: 'qwen3-235b-a22b',
name: 'qwen3-235b-a22b',
provider: '302ai',
group: 'Qwen'
},
{
id: 'gemini-2.5-flash-preview-05-20',
name: 'gemini-2.5-flash-preview-05-20',
provider: '302ai',
group: 'Gemini'
},
{
id: 'gemini-2.5-pro-preview-06-05',
name: 'gemini-2.5-pro-preview-06-05',
provider: '302ai',
group: 'Gemini'
},
{
id: 'claude-sonnet-4-20250514',
provider: '302ai',
name: 'claude-sonnet-4-20250514',
group: 'Anthropic'
},
{
id: 'claude-opus-4-20250514',
provider: '302ai',
name: 'claude-opus-4-20250514',
group: 'Anthropic'
},
{
id: 'jina-clip-v2',
name: 'jina-clip-v2',
provider: '302ai',
group: 'Jina AI'
},
{
id: 'jina-reranker-m0',
name: 'jina-reranker-m0',
provider: '302ai',
group: 'Jina AI'
}
],
aihubmix: [ aihubmix: [
{ {
id: 'gpt-4o', id: 'gpt-4o',
@@ -2082,7 +2161,16 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
name: 'Qwen Plus', name: 'Qwen Plus',
group: 'Qwen' group: 'Qwen'
} }
] ],
cephalon: [
{
id: 'DeepSeek-R1',
provider: 'cephalon',
name: 'DeepSeek-R1满血版',
group: 'DeepSeek'
}
],
lanyun: []
} }
export const TEXT_TO_IMAGES_MODELS = [ export const TEXT_TO_IMAGES_MODELS = [

View File

@@ -1,6 +1,7 @@
import ZhinaoProviderLogo from '@renderer/assets/images/models/360.png' import ZhinaoProviderLogo from '@renderer/assets/images/models/360.png'
import HunyuanProviderLogo from '@renderer/assets/images/models/hunyuan.png' import HunyuanProviderLogo from '@renderer/assets/images/models/hunyuan.png'
import AzureProviderLogo from '@renderer/assets/images/models/microsoft.png' import AzureProviderLogo from '@renderer/assets/images/models/microsoft.png'
import Ai302ProviderLogo from '@renderer/assets/images/providers/302ai.webp'
import AiHubMixProviderLogo from '@renderer/assets/images/providers/aihubmix.webp' import AiHubMixProviderLogo from '@renderer/assets/images/providers/aihubmix.webp'
import AlayaNewProviderLogo from '@renderer/assets/images/providers/alayanew.webp' import AlayaNewProviderLogo from '@renderer/assets/images/providers/alayanew.webp'
import AnthropicProviderLogo from '@renderer/assets/images/providers/anthropic.png' import AnthropicProviderLogo from '@renderer/assets/images/providers/anthropic.png'
@@ -8,6 +9,7 @@ import BaichuanProviderLogo from '@renderer/assets/images/providers/baichuan.png
import BaiduCloudProviderLogo from '@renderer/assets/images/providers/baidu-cloud.svg' import BaiduCloudProviderLogo from '@renderer/assets/images/providers/baidu-cloud.svg'
import BailianProviderLogo from '@renderer/assets/images/providers/bailian.png' import BailianProviderLogo from '@renderer/assets/images/providers/bailian.png'
import BurnCloudProviderLogo from '@renderer/assets/images/providers/burncloud.png' import BurnCloudProviderLogo from '@renderer/assets/images/providers/burncloud.png'
import CephalonProviderLogo from '@renderer/assets/images/providers/cephalon.jpeg'
import DeepSeekProviderLogo from '@renderer/assets/images/providers/deepseek.png' import DeepSeekProviderLogo from '@renderer/assets/images/providers/deepseek.png'
import DmxapiProviderLogo from '@renderer/assets/images/providers/DMXAPI.png' import DmxapiProviderLogo from '@renderer/assets/images/providers/DMXAPI.png'
import FireworksProviderLogo from '@renderer/assets/images/providers/fireworks.png' import FireworksProviderLogo from '@renderer/assets/images/providers/fireworks.png'
@@ -20,6 +22,7 @@ import GroqProviderLogo from '@renderer/assets/images/providers/groq.png'
import HyperbolicProviderLogo from '@renderer/assets/images/providers/hyperbolic.png' import HyperbolicProviderLogo from '@renderer/assets/images/providers/hyperbolic.png'
import InfiniProviderLogo from '@renderer/assets/images/providers/infini.png' import InfiniProviderLogo from '@renderer/assets/images/providers/infini.png'
import JinaProviderLogo from '@renderer/assets/images/providers/jina.png' import JinaProviderLogo from '@renderer/assets/images/providers/jina.png'
import LanyunProviderLogo from '@renderer/assets/images/providers/lanyun.png'
import LMStudioProviderLogo from '@renderer/assets/images/providers/lmstudio.png' import LMStudioProviderLogo from '@renderer/assets/images/providers/lmstudio.png'
import MinimaxProviderLogo from '@renderer/assets/images/providers/minimax.png' import MinimaxProviderLogo from '@renderer/assets/images/providers/minimax.png'
import MistralProviderLogo from '@renderer/assets/images/providers/mistral.png' import MistralProviderLogo from '@renderer/assets/images/providers/mistral.png'
@@ -48,6 +51,7 @@ import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png'
import { TOKENFLUX_HOST } from './constant' import { TOKENFLUX_HOST } from './constant'
const PROVIDER_LOGO_MAP = { const PROVIDER_LOGO_MAP = {
'302ai': Ai302ProviderLogo,
openai: OpenAiProviderLogo, openai: OpenAiProviderLogo,
silicon: SiliconFlowProviderLogo, silicon: SiliconFlowProviderLogo,
deepseek: DeepSeekProviderLogo, deepseek: DeepSeekProviderLogo,
@@ -94,7 +98,9 @@ const PROVIDER_LOGO_MAP = {
alayanew: AlayaNewProviderLogo, alayanew: AlayaNewProviderLogo,
voyageai: VoyageAIProviderLogo, voyageai: VoyageAIProviderLogo,
qiniu: QiniuProviderLogo, qiniu: QiniuProviderLogo,
tokenflux: TokenFluxProviderLogo tokenflux: TokenFluxProviderLogo,
cephalon: CephalonProviderLogo,
lanyun: LanyunProviderLogo
} as const } as const
export function getProviderLogo(providerId: string) { export function getProviderLogo(providerId: string) {
@@ -106,6 +112,17 @@ export const NOT_SUPPORTED_REANK_PROVIDERS = ['ollama']
export const ONLY_SUPPORTED_DIMENSION_PROVIDERS = ['ollama', 'infini'] export const ONLY_SUPPORTED_DIMENSION_PROVIDERS = ['ollama', 'infini']
export const PROVIDER_CONFIG = { export const PROVIDER_CONFIG = {
'302ai': {
api: {
url: 'https://api.302.ai'
},
websites: {
official: 'https://302.ai',
apiKey: 'https://share.302.ai/F1B71g',
docs: 'https://302ai.apifox.cn/api-147522039',
models: 'https://302.ai/pricing/'
}
},
openai: { openai: {
api: { api: {
url: 'https://api.openai.com' url: 'https://api.openai.com'
@@ -612,5 +629,27 @@ export const PROVIDER_CONFIG = {
docs: `${TOKENFLUX_HOST}/docs`, docs: `${TOKENFLUX_HOST}/docs`,
models: `${TOKENFLUX_HOST}/models` models: `${TOKENFLUX_HOST}/models`
} }
},
cephalon: {
api: {
url: 'https://cephalon.cloud/user-center/v1/model'
},
websites: {
official: 'https://cephalon.cloud/share/register-landing?invite_id=jSdOYA',
apiKey: 'https://cephalon.cloud/api',
docs: 'https://cephalon.cloud/apitoken/1864244127731589124',
models: 'https://cephalon.cloud/model'
}
},
lanyun: {
api: {
url: 'https://maas-api.lanyun.net'
},
websites: {
official: 'https://lanyun.net',
apiKey: 'https://maas.lanyun.net/api/#/system/apiKey',
docs: 'https://archive.lanyun.net/maas/doc/',
models: 'https://maas.lanyun.net/api/#/model/modelSquare'
}
} }
} }

View File

@@ -4,6 +4,7 @@ import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { deleteMessageFiles } from '@renderer/services/MessagesService' import { deleteMessageFiles } from '@renderer/services/MessagesService'
import store from '@renderer/store' import store from '@renderer/store'
import { updateTopic } from '@renderer/store/assistants' import { updateTopic } from '@renderer/store/assistants'
import { setNewlyRenamedTopics, setRenamingTopics } from '@renderer/store/runtime'
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk' import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
import { Assistant, Topic } from '@renderer/types' import { Assistant, Topic } from '@renderer/types'
import { findMainTextBlocks } from '@renderer/utils/messageUtils/find' import { findMainTextBlocks } from '@renderer/utils/messageUtils/find'
@@ -13,8 +14,6 @@ import { useEffect, useState } from 'react'
import { useAssistant } from './useAssistant' import { useAssistant } from './useAssistant'
import { getStoreSetting } from './useSettings' import { getStoreSetting } from './useSettings'
const renamingTopics = new Set<string>()
let _activeTopic: Topic let _activeTopic: Topic
let _setActiveTopic: (topic: Topic) => void let _setActiveTopic: (topic: Topic) => void
@@ -58,13 +57,46 @@ export async function getTopicById(topicId: string) {
return { ...topic, messages } as Topic return { ...topic, messages } as Topic
} }
/**
* 开始重命名指定话题
*/
export const startTopicRenaming = (topicId: string) => {
const currentIds = store.getState().runtime.chat.renamingTopics
if (!currentIds.includes(topicId)) {
store.dispatch(setRenamingTopics([...currentIds, topicId]))
}
}
/**
* 完成重命名指定话题
*/
export const finishTopicRenaming = (topicId: string) => {
const state = store.getState()
// 1. 立即从 renamingTopics 移除
const currentRenaming = state.runtime.chat.renamingTopics
store.dispatch(setRenamingTopics(currentRenaming.filter((id) => id !== topicId)))
// 2. 立即添加到 newlyRenamedTopics
const currentNewlyRenamed = state.runtime.chat.newlyRenamedTopics
store.dispatch(setNewlyRenamedTopics([...currentNewlyRenamed, topicId]))
// 3. 延迟从 newlyRenamedTopics 移除
setTimeout(() => {
const current = store.getState().runtime.chat.newlyRenamedTopics
store.dispatch(setNewlyRenamedTopics(current.filter((id) => id !== topicId)))
}, 700)
}
const topicRenamingLocks = new Set<string>()
export const autoRenameTopic = async (assistant: Assistant, topicId: string) => { export const autoRenameTopic = async (assistant: Assistant, topicId: string) => {
if (renamingTopics.has(topicId)) { if (topicRenamingLocks.has(topicId)) {
return return
} }
try { try {
renamingTopics.add(topicId) topicRenamingLocks.add(topicId)
const topic = await getTopicById(topicId) const topic = await getTopicById(topicId)
const enableTopicNaming = getStoreSetting('enableTopicNaming') const enableTopicNaming = getStoreSetting('enableTopicNaming')
@@ -85,24 +117,36 @@ export const autoRenameTopic = async (assistant: Assistant, topicId: string) =>
.join('\n\n') .join('\n\n')
.substring(0, 50) .substring(0, 50)
if (topicName) { if (topicName) {
const data = { ...topic, name: topicName } as Topic try {
_setActiveTopic(data) startTopicRenaming(topicId)
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
const data = { ...topic, name: topicName } as Topic
_setActiveTopic(data)
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
} finally {
finishTopicRenaming(topicId)
}
} }
return return
} }
if (topic && topic.name === i18n.t('chat.default.topic.name') && topic.messages.length >= 2) { if (topic && topic.name === i18n.t('chat.default.topic.name') && topic.messages.length >= 2) {
const { fetchMessagesSummary } = await import('@renderer/services/ApiService') try {
const summaryText = await fetchMessagesSummary({ messages: topic.messages, assistant }) startTopicRenaming(topicId)
if (summaryText) {
const data = { ...topic, name: summaryText } const { fetchMessagesSummary } = await import('@renderer/services/ApiService')
_setActiveTopic(data) const summaryText = await fetchMessagesSummary({ messages: topic.messages, assistant })
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data })) if (summaryText) {
const data = { ...topic, name: summaryText }
_setActiveTopic(data)
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
}
} finally {
finishTopicRenaming(topicId)
} }
} }
} finally { } finally {
renamingTopics.delete(topicId) topicRenamingLocks.delete(topicId)
} }
} }
@@ -117,9 +161,18 @@ export const TopicManager = {
return await db.topics.toArray() return await db.topics.toArray()
}, },
/**
* 加载并返回指定话题的消息
*/
async getTopicMessages(id: string) { async getTopicMessages(id: string) {
const topic = await TopicManager.getTopic(id) const topic = await TopicManager.getTopic(id)
return topic ? topic.messages : [] if (!topic) return []
await store.dispatch(loadTopicMessagesThunk(id))
// 获取更新后的话题
const updatedTopic = await TopicManager.getTopic(id)
return updatedTopic?.messages || []
}, },
async removeTopic(id: string) { async removeTopic(id: string) {

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": {
@@ -978,6 +978,7 @@
"azure-openai": "Azure OpenAI", "azure-openai": "Azure OpenAI",
"baichuan": "Baichuan", "baichuan": "Baichuan",
"baidu-cloud": "Baidu Cloud", "baidu-cloud": "Baidu Cloud",
"cephalon": "Cephalon",
"copilot": "GitHub Copilot", "copilot": "GitHub Copilot",
"dashscope": "Alibaba Cloud", "dashscope": "Alibaba Cloud",
"deepseek": "DeepSeek", "deepseek": "DeepSeek",
@@ -1018,7 +1019,9 @@
"zhipu": "ZHIPU AI", "zhipu": "ZHIPU AI",
"voyageai": "Voyage AI", "voyageai": "Voyage AI",
"qiniu": "Qiniu AI", "qiniu": "Qiniu AI",
"tokenflux": "TokenFlux" "tokenflux": "TokenFlux",
"302ai": "302.AI",
"lanyun": "LANYUN"
}, },
"restore": { "restore": {
"confirm": "Are you sure you want to restore data?", "confirm": "Are you sure you want to restore data?",
@@ -1960,6 +1963,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": {
@@ -703,7 +703,8 @@
"warn.siyuan.exporting": "思源ノートにエクスポート中です。重複してエクスポートしないでください!", "warn.siyuan.exporting": "思源ノートにエクスポート中です。重複してエクスポートしないでください!",
"error.yuque.no_config": "語雀のAPIアドレスまたはトークンが設定されていません", "error.yuque.no_config": "語雀のAPIアドレスまたはトークンが設定されていません",
"download.success": "ダウンロードに成功しました", "download.success": "ダウンロードに成功しました",
"download.failed": "ダウンロードに失敗しました" "download.failed": "ダウンロードに失敗しました",
"error.fetchTopicName": "トピック名の取得に失敗しました"
}, },
"minapp": { "minapp": {
"popup": { "popup": {
@@ -1017,7 +1018,10 @@
"zhipu": "智譜AI", "zhipu": "智譜AI",
"voyageai": "Voyage AI", "voyageai": "Voyage AI",
"qiniu": "七牛云 AI 推理", "qiniu": "七牛云 AI 推理",
"tokenflux": "TokenFlux" "tokenflux": "TokenFlux",
"302ai": "302.AI",
"cephalon": "Cephalon",
"lanyun": "LANYUN"
}, },
"restore": { "restore": {
"confirm": "データを復元しますか?", "confirm": "データを復元しますか?",
@@ -1959,6 +1963,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": "Вы уверены, что хотите удалить этого агента?",
@@ -703,7 +703,8 @@
"warn.yuque.exporting": "Экспортируется в Yuque, пожалуйста, не отправляйте повторные запросы!", "warn.yuque.exporting": "Экспортируется в Yuque, пожалуйста, не отправляйте повторные запросы!",
"warn.siyuan.exporting": "Экспортируется в Siyuan, пожалуйста, не отправляйте повторные запросы!", "warn.siyuan.exporting": "Экспортируется в Siyuan, пожалуйста, не отправляйте повторные запросы!",
"download.success": "Скачано успешно", "download.success": "Скачано успешно",
"download.failed": "Скачивание не удалось" "download.failed": "Скачивание не удалось",
"error.fetchTopicName": "Не удалось назвать топик"
}, },
"minapp": { "minapp": {
"popup": { "popup": {
@@ -977,6 +978,7 @@
"azure-openai": "Azure OpenAI", "azure-openai": "Azure OpenAI",
"baichuan": "Baichuan", "baichuan": "Baichuan",
"baidu-cloud": "Baidu Cloud", "baidu-cloud": "Baidu Cloud",
"cephalon": "Cephalon",
"copilot": "GitHub Copilot", "copilot": "GitHub Copilot",
"dashscope": "Alibaba Cloud", "dashscope": "Alibaba Cloud",
"deepseek": "DeepSeek", "deepseek": "DeepSeek",
@@ -1017,7 +1019,9 @@
"zhipu": "ZHIPU AI", "zhipu": "ZHIPU AI",
"voyageai": "Voyage AI", "voyageai": "Voyage AI",
"qiniu": "Qiniu AI", "qiniu": "Qiniu AI",
"tokenflux": "TokenFlux" "tokenflux": "TokenFlux",
"302ai": "302.AI",
"lanyun": "LANYUN"
}, },
"restore": { "restore": {
"confirm": "Вы уверены, что хотите восстановить данные?", "confirm": "Вы уверены, что хотите восстановить данные?",
@@ -1959,6 +1963,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": {
@@ -978,6 +978,7 @@
"azure-openai": "Azure OpenAI", "azure-openai": "Azure OpenAI",
"baichuan": "百川", "baichuan": "百川",
"baidu-cloud": "百度云千帆", "baidu-cloud": "百度云千帆",
"cephalon": "Cephalon",
"copilot": "GitHub Copilot", "copilot": "GitHub Copilot",
"dashscope": "阿里云百炼", "dashscope": "阿里云百炼",
"deepseek": "深度求索", "deepseek": "深度求索",
@@ -1018,7 +1019,9 @@
"zhipu": "智谱AI", "zhipu": "智谱AI",
"voyageai": "Voyage AI", "voyageai": "Voyage AI",
"qiniu": "七牛云 AI 推理", "qiniu": "七牛云 AI 推理",
"tokenflux": "TokenFlux" "tokenflux": "TokenFlux",
"302ai": "302.AI",
"lanyun": "蓝耘科技"
}, },
"restore": { "restore": {
"confirm": "确定要恢复数据吗?", "confirm": "确定要恢复数据吗?",
@@ -1925,7 +1928,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": "前往快捷键设置"
@@ -1960,6 +1963,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": {
@@ -978,6 +978,7 @@
"azure-openai": "Azure OpenAI", "azure-openai": "Azure OpenAI",
"baichuan": "百川", "baichuan": "百川",
"baidu-cloud": "百度雲千帆", "baidu-cloud": "百度雲千帆",
"cephalon": "Cephalon",
"copilot": "GitHub Copilot", "copilot": "GitHub Copilot",
"dashscope": "阿里雲百鍊", "dashscope": "阿里雲百鍊",
"deepseek": "深度求索", "deepseek": "深度求索",
@@ -1018,7 +1019,9 @@
"zhipu": "智譜 AI", "zhipu": "智譜 AI",
"voyageai": "Voyage AI", "voyageai": "Voyage AI",
"qiniu": "七牛雲 AI 推理", "qiniu": "七牛雲 AI 推理",
"tokenflux": "TokenFlux" "tokenflux": "TokenFlux",
"302ai": "302.AI",
"lanyun": "藍耘"
}, },
"restore": { "restore": {
"confirm": "確定要復原資料嗎?", "confirm": "確定要復原資料嗎?",
@@ -1960,6 +1963,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": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτόν τον ειδικό;",
@@ -840,6 +840,7 @@
"azure-openai": "Azure OpenAI", "azure-openai": "Azure OpenAI",
"baichuan": "Παράκειμαι", "baichuan": "Παράκειμαι",
"baidu-cloud": "Baidu Cloud Qianfan", "baidu-cloud": "Baidu Cloud Qianfan",
"cephalon": "Cephalon",
"copilot": "GitHub Copilot", "copilot": "GitHub Copilot",
"dashscope": "AliCloud Bailian", "dashscope": "AliCloud Bailian",
"deepseek": "Βαθιά Αναζήτηση", "deepseek": "Βαθιά Αναζήτηση",

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?",
@@ -841,6 +841,7 @@
"azure-openai": "Azure OpenAI", "azure-openai": "Azure OpenAI",
"baichuan": "BaiChuan", "baichuan": "BaiChuan",
"baidu-cloud": "Baidu Nube Qiánfān", "baidu-cloud": "Baidu Nube Qiánfān",
"cephalon": "Cephalon",
"copilot": "GitHub Copiloto", "copilot": "GitHub Copiloto",
"dashscope": "Álibaba Nube BaiLiàn", "dashscope": "Álibaba Nube BaiLiàn",
"deepseek": "Profundo Buscar", "deepseek": "Profundo Buscar",

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 ?",
@@ -840,6 +840,7 @@
"azure-openai": "Azure OpenAI", "azure-openai": "Azure OpenAI",
"baichuan": "BaiChuan", "baichuan": "BaiChuan",
"baidu-cloud": "Baidu Cloud Qianfan", "baidu-cloud": "Baidu Cloud Qianfan",
"cephalon": "Cephalon",
"copilot": "GitHub Copilote", "copilot": "GitHub Copilote",
"dashscope": "AliCloud BaiLian", "dashscope": "AliCloud BaiLian",
"deepseek": "DeepSeek", "deepseek": "DeepSeek",

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

@@ -190,16 +190,16 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
) )
} }
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('')

View File

@@ -80,14 +80,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

@@ -15,6 +15,7 @@ import type { Model } from '@renderer/types'
import type { Assistant, Topic } from '@renderer/types' import type { Assistant, Topic } from '@renderer/types'
import type { Message } from '@renderer/types/newMessage' import type { Message } from '@renderer/types/newMessage'
import { captureScrollableDivAsBlob, captureScrollableDivAsDataURL } from '@renderer/utils' import { captureScrollableDivAsBlob, captureScrollableDivAsDataURL } from '@renderer/utils'
import { copyMessageAsPlainText } from '@renderer/utils/copy'
import { import {
exportMarkdownToJoplin, exportMarkdownToJoplin,
exportMarkdownToSiyuan, exportMarkdownToSiyuan,
@@ -23,7 +24,6 @@ import {
exportMessageToNotion, exportMessageToNotion,
messageToMarkdown messageToMarkdown
} from '@renderer/utils/export' } from '@renderer/utils/export'
import { copyMessageAsPlainText } from '@renderer/utils/copy'
// import { withMessageThought } from '@renderer/utils/formats' // import { withMessageThought } from '@renderer/utils/formats'
import { removeTrailingDoubleSpaces } from '@renderer/utils/markdown' import { removeTrailingDoubleSpaces } from '@renderer/utils/markdown'
import { findMainTextBlocks, findTranslationBlocks, getMainTextContent } from '@renderer/utils/messageUtils/find' import { findMainTextBlocks, findTranslationBlocks, getMainTextContent } from '@renderer/utils/messageUtils/find'
@@ -124,10 +124,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()
@@ -316,8 +319,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) => {
@@ -399,7 +406,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

@@ -53,15 +53,17 @@ const MessgeTokens: React.FC<MessageTokensProps> = ({ message }) => {
) )
return ( return (
<MessageMetadata className="message-tokens" onClick={locateMessage}> showTokens && (
{hasMetrics ? ( <MessageMetadata className="message-tokens" onClick={locateMessage}>
<Popover content={metrixs} placement="top" trigger="hover" styles={{ root: { fontSize: 11 } }}> {hasMetrics ? (
{showTokens && tokensInfo} <Popover content={metrixs} placement="top" trigger="hover" styles={{ root: { fontSize: 11 } }}>
</Popover> {tokensInfo}
) : ( </Popover>
tokensInfo ) : (
)} tokensInfo
</MessageMetadata> )}
</MessageMetadata>
)
) )
} }

View File

@@ -18,7 +18,7 @@ import { isMac } from '@renderer/config/constant'
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant' import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
import { modelGenerating } from '@renderer/hooks/useRuntime' import { modelGenerating } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { TopicManager } from '@renderer/hooks/useTopic' import { finishTopicRenaming, startTopicRenaming, TopicManager } from '@renderer/hooks/useTopic'
import { fetchMessagesSummary } from '@renderer/services/ApiService' import { fetchMessagesSummary } from '@renderer/services/ApiService'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import store from '@renderer/store' import store from '@renderer/store'
@@ -57,6 +57,9 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
const { t } = useTranslation() const { t } = useTranslation()
const { showTopicTime, pinTopicsToTop, setTopicPosition } = useSettings() const { showTopicTime, pinTopicsToTop, setTopicPosition } = useSettings()
const renamingTopics = useSelector((state: RootState) => state.runtime.chat.renamingTopics)
const newlyRenamedTopics = useSelector((state: RootState) => state.runtime.chat.newlyRenamedTopics)
const borderRadius = showTopicTime ? 12 : 'var(--list-item-border-radius)' const borderRadius = showTopicTime ? 12 : 'var(--list-item-border-radius)'
const [deletingTopicId, setDeletingTopicId] = useState<string | null>(null) const [deletingTopicId, setDeletingTopicId] = useState<string | null>(null)
@@ -84,6 +87,20 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
[activeTopic.id, pendingTopics] [activeTopic.id, pendingTopics]
) )
const isRenaming = useCallback(
(topicId: string) => {
return renamingTopics.includes(topicId)
},
[renamingTopics]
)
const isNewlyRenamed = useCallback(
(topicId: string) => {
return newlyRenamedTopics.includes(topicId)
},
[newlyRenamedTopics]
)
const handleDeleteClick = useCallback((topicId: string, e: React.MouseEvent) => { const handleDeleteClick = useCallback((topicId: string, e: React.MouseEvent) => {
e.stopPropagation() e.stopPropagation()
@@ -170,16 +187,22 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
label: t('chat.topics.auto_rename'), label: t('chat.topics.auto_rename'),
key: 'auto-rename', key: 'auto-rename',
icon: <i className="iconfont icon-business-smart-assistant" style={{ fontSize: '14px' }} />, icon: <i className="iconfont icon-business-smart-assistant" style={{ fontSize: '14px' }} />,
disabled: isRenaming(topic.id),
async onClick() { async onClick() {
const messages = await TopicManager.getTopicMessages(topic.id) const messages = await TopicManager.getTopicMessages(topic.id)
if (messages.length >= 2) { if (messages.length >= 2) {
const summaryText = await fetchMessagesSummary({ messages, assistant }) startTopicRenaming(topic.id)
if (summaryText) { try {
const updatedTopic = { ...topic, name: summaryText, isNameManuallyEdited: false } const summaryText = await fetchMessagesSummary({ messages, assistant })
updateTopic(updatedTopic) if (summaryText) {
topic.id === activeTopic.id && setActiveTopic(updatedTopic) const updatedTopic = { ...topic, name: summaryText, isNameManuallyEdited: false }
} else { updateTopic(updatedTopic)
window.message?.error(t('message.error.fetchTopicName')) topic.id === activeTopic.id && setActiveTopic(updatedTopic)
} else {
window.message?.error(t('message.error.fetchTopicName'))
}
} finally {
finishTopicRenaming(topic.id)
} }
} }
} }
@@ -188,6 +211,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
label: t('chat.topics.edit.title'), label: t('chat.topics.edit.title'),
key: 'rename', key: 'rename',
icon: <EditOutlined />, icon: <EditOutlined />,
disabled: isRenaming(topic.id),
async onClick() { async onClick() {
const name = await PromptPopup.show({ const name = await PromptPopup.show({
title: t('chat.topics.edit.title'), title: t('chat.topics.edit.title'),
@@ -388,6 +412,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
}, [ }, [
targetTopic, targetTopic,
t, t,
isRenaming,
exportMenuOptions.image, exportMenuOptions.image,
exportMenuOptions.markdown, exportMenuOptions.markdown,
exportMenuOptions.markdown_reason, exportMenuOptions.markdown_reason,
@@ -430,6 +455,13 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
const topicName = topic.name.replace('`', '') const topicName = topic.name.replace('`', '')
const topicPrompt = topic.prompt const topicPrompt = topic.prompt
const fullTopicPrompt = t('common.prompt') + ': ' + topicPrompt const fullTopicPrompt = t('common.prompt') + ': ' + topicPrompt
const getTopicNameClassName = () => {
if (isRenaming(topic.id)) return 'shimmer'
if (isNewlyRenamed(topic.id)) return 'typing'
return ''
}
return ( return (
<TopicListItem <TopicListItem
onContextMenu={() => setTargetTopic(topic)} onContextMenu={() => setTargetTopic(topic)}
@@ -438,7 +470,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
style={{ borderRadius }}> style={{ borderRadius }}>
{isPending(topic.id) && !isActive && <PendingIndicator />} {isPending(topic.id) && !isActive && <PendingIndicator />}
<TopicNameContainer> <TopicNameContainer>
<TopicName className="name" title={topicName}> <TopicName className={getTopicNameClassName()} title={topicName}>
{topicName} {topicName}
</TopicName> </TopicName>
{isActive && !topic.pinned && ( {isActive && !topic.pinned && (
@@ -544,6 +576,46 @@ const TopicName = styled.div`
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
font-size: 13px; font-size: 13px;
position: relative;
will-change: background-position, width;
--color-shimmer-mid: var(--color-text-1);
--color-shimmer-end: color-mix(in srgb, var(--color-text-1) 25%, transparent);
&.shimmer {
background: linear-gradient(to left, var(--color-shimmer-end), var(--color-shimmer-mid), var(--color-shimmer-end));
background-size: 200% 100%;
background-clip: text;
color: transparent;
animation: shimmer 3s linear infinite;
}
&.typing {
display: block;
-webkit-line-clamp: unset;
-webkit-box-orient: unset;
white-space: nowrap;
overflow: hidden;
animation: typewriter 0.5s steps(40, end);
}
@keyframes shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
@keyframes typewriter {
from {
width: 0;
}
to {
width: 100%;
}
}
` `
const PendingIndicator = styled.div.attrs({ const PendingIndicator = styled.div.attrs({

View File

@@ -1,5 +1,6 @@
import { SyncOutlined } from '@ant-design/icons' import { SyncOutlined } from '@ant-design/icons'
import { useRuntime } from '@renderer/hooks/useRuntime' import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { Button } from 'antd' import { Button } from 'antd'
import { FC } from 'react' import { FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@@ -7,13 +8,14 @@ import styled from 'styled-components'
const UpdateAppButton: FC = () => { const UpdateAppButton: FC = () => {
const { update } = useRuntime() const { update } = useRuntime()
const { autoCheckUpdate } = useSettings()
const { t } = useTranslation() const { t } = useTranslation()
if (!update) { if (!update) {
return null return null
} }
if (!update.downloaded) { if (!update.downloaded || !autoCheckUpdate) {
return null return null
} }

View File

@@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import { getModelScopeToken, saveModelScopeToken, syncModelScopeServers } from './modelscopeSyncUtils' import { getModelScopeToken, saveModelScopeToken, syncModelScopeServers } from './modelscopeSyncUtils'
import { getTokenLanYunToken, LANYUN_KEY_HOST, saveTokenLanYunToken, syncTokenLanYunServers } from './providers/lanyun'
import { getTokenFluxToken, saveTokenFluxToken, syncTokenFluxServers, TOKENFLUX_HOST } from './providers/tokenflux' import { getTokenFluxToken, saveTokenFluxToken, syncTokenFluxServers, TOKENFLUX_HOST } from './providers/tokenflux'
// Provider configuration interface // Provider configuration interface
@@ -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,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, Divider, 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')}
@@ -53,7 +56,12 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
</HStack> </HStack>
<Divider style={{ margin: '10px 0' }} /> <Divider style={{ margin: '10px 0' }} />
<div style={{ marginBottom: 20 }}> <div style={{ marginBottom: 20 }}>
<div style={{ marginBottom: 10 }}>{t('settings.models.topic_naming_prompt')}</div> <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

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

@@ -50,7 +50,7 @@ const persistedReducer = persistReducer(
{ {
key: 'cherry-studio', key: 'cherry-studio',
storage, storage,
version: 111, version: 112,
blacklist: ['runtime', 'messages', 'messageBlocks'], blacklist: ['runtime', 'messages', 'messageBlocks'],
migrate migrate
}, },

View File

@@ -127,12 +127,32 @@ export const INITIAL_PROVIDERS: Provider[] = [
enabled: false enabled: false
}, },
{ {
id: 'o3', id: '302ai',
name: 'O3', name: '302.AI',
type: 'openai', type: 'openai',
apiKey: '', apiKey: '',
apiHost: 'https://api.o3.fan', apiHost: 'https://api.302.ai',
models: SYSTEM_MODELS.o3, models: SYSTEM_MODELS['302ai'],
isSystem: true,
enabled: false
},
{
id: 'cephalon',
name: 'Cephalon',
type: 'openai',
apiKey: '',
apiHost: 'https://cephalon.cloud/user-center/v1/model',
models: SYSTEM_MODELS.cephalon,
isSystem: true,
enabled: false
},
{
id: 'lanyun',
name: 'LANYUN',
type: 'openai',
apiKey: '',
apiHost: 'https://maas-api.lanyun.net',
models: SYSTEM_MODELS.lanyun,
isSystem: true, isSystem: true,
enabled: false enabled: false
}, },

View File

@@ -1554,6 +1554,19 @@ const migrateConfig = {
// add selection_assistant_toggle and selection_assistant_select_text shortcuts after mini_window // add selection_assistant_toggle and selection_assistant_select_text shortcuts after mini_window
addShortcuts(state, ['selection_assistant_toggle', 'selection_assistant_select_text'], 'mini_window') addShortcuts(state, ['selection_assistant_toggle', 'selection_assistant_select_text'], 'mini_window')
return state
} catch (error) {
return state
}
},
'112': (state: RootState) => {
try {
addProvider(state, 'cephalon')
addProvider(state, '302ai')
addProvider(state, 'lanyun')
state.llm.providers = moveProvider(state.llm.providers, 'cephalon', 13)
state.llm.providers = moveProvider(state.llm.providers, '302ai', 14)
state.llm.providers = moveProvider(state.llm.providers, 'lanyun', 15)
return state return state
} catch (error) { } catch (error) {
return state return state

View File

@@ -7,6 +7,10 @@ export interface ChatState {
isMultiSelectMode: boolean isMultiSelectMode: boolean
selectedMessageIds: string[] selectedMessageIds: string[]
activeTopic: Topic | null activeTopic: Topic | null
/** topic ids that are currently being renamed */
renamingTopics: string[]
/** topic ids that are newly renamed */
newlyRenamedTopics: string[]
} }
export interface UpdateState { export interface UpdateState {
@@ -65,7 +69,9 @@ const initialState: RuntimeState = {
chat: { chat: {
isMultiSelectMode: false, isMultiSelectMode: false,
selectedMessageIds: [], selectedMessageIds: [],
activeTopic: null activeTopic: null,
renamingTopics: [],
newlyRenamedTopics: []
} }
} }
@@ -118,6 +124,12 @@ const runtimeSlice = createSlice({
}, },
setActiveTopic: (state, action: PayloadAction<Topic>) => { setActiveTopic: (state, action: PayloadAction<Topic>) => {
state.chat.activeTopic = action.payload state.chat.activeTopic = action.payload
},
setRenamingTopics: (state, action: PayloadAction<string[]>) => {
state.chat.renamingTopics = action.payload
},
setNewlyRenamedTopics: (state, action: PayloadAction<string[]>) => {
state.chat.newlyRenamedTopics = action.payload
} }
} }
}) })
@@ -137,7 +149,9 @@ export const {
// Chat related actions // Chat related actions
toggleMultiSelectMode, toggleMultiSelectMode,
setSelectedMessageIds, setSelectedMessageIds,
setActiveTopic setActiveTopic,
setRenamingTopics,
setNewlyRenamedTopics
} = runtimeSlice.actions } = runtimeSlice.actions
export default runtimeSlice.reducer export default runtimeSlice.reducer

View File

@@ -204,6 +204,16 @@ export const buildSystemPrompt = async (userSystemPrompt: string, tools?: MCPToo
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) {