diff --git a/.github/workflows/issue-management.yml b/.github/workflows/issue-management.yml
index f79b31fd92..59faedc04e 100644
--- a/.github/workflows/issue-management.yml
+++ b/.github/workflows/issue-management.yml
@@ -54,5 +54,5 @@ jobs:
days-before-pr-close: -1 # Completely disable closing for PRs
# Temporary to reduce the huge issues number
- operations-per-run: 100
+ operations-per-run: 1000
debug-only: false
diff --git a/.gitignore b/.gitignore
index 459dc6201c..68ea0f203f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -51,3 +51,4 @@ local
coverage
.vitest-cache
vitest.config.*.timestamp-*
+YOUR_MEMORY_FILE_PATH
diff --git a/.yarn/releases/yarn-4.6.0.cjs b/.yarn/releases/yarn-4.6.0.cjs
deleted file mode 100755
index 3e7773b1ed..0000000000
Binary files a/.yarn/releases/yarn-4.6.0.cjs and /dev/null differ
diff --git a/.yarn/releases/yarn-4.9.1.cjs b/.yarn/releases/yarn-4.9.1.cjs
new file mode 100755
index 0000000000..657026d5c6
Binary files /dev/null and b/.yarn/releases/yarn-4.9.1.cjs differ
diff --git a/.yarnrc.yml b/.yarnrc.yml
index ff35b50cbe..e1e4cf05ca 100644
--- a/.yarnrc.yml
+++ b/.yarnrc.yml
@@ -4,4 +4,4 @@ httpTimeout: 300000
nodeLinker: node-modules
-yarnPath: .yarn/releases/yarn-4.6.0.cjs
+yarnPath: .yarn/releases/yarn-4.9.1.cjs
diff --git a/README.md b/README.md
index 96f727a96e..e5315f5ce9 100644
--- a/README.md
+++ b/README.md
@@ -96,7 +96,7 @@ Refer to the [development documentation](docs/dev.md)
Refer to the [Architecture overview documentation](https://deepwiki.com/CherryHQ/cherry-studio)
-Refer to the [Branching Strategy](docs/branching-strategy.md) for contribution guidelines
+Refer to the [Branching Strategy](docs/branching-strategy-en.md) for contribution guidelines
# 🤝 Contributing
diff --git a/docs/README.ja.md b/docs/README.ja.md
index 02983db685..ce5d2f6ef7 100644
--- a/docs/README.ja.md
+++ b/docs/README.ja.md
@@ -6,17 +6,19 @@

@@ -74,12 +75,12 @@ https://docs.cherry-ai.com
- 📝 完整的 Markdown 渲染
- 🤲 便捷的内容分享功能
-# 📝 待辦事項
+# 📝 待办事项
- [x] 快捷弹窗(读取剪贴板、快速提问、解释、翻译、总结)
- [x] 多模型回答对比
-- [x] 支持使用服务供应商提供的 SSO 进行登入
-- [x] 全部模型支持连网(开发中...)
+- [x] 支持使用服务供应商提供的 SSO 进行登录
+- [x] 所有模型支持联网
- [x] 推出第一个正式版
- [x] 错误修复和改进(开发中...)
- [ ] 插件功能(JavaScript)
@@ -93,9 +94,9 @@ https://docs.cherry-ai.com
- 主题库:https://cherrycss.com
- Aero 主题:https://github.com/hakadao/CherryStudio-Aero
-- PaperMaterial 主题: https://github.com/rainoffallingstar/CherryStudio-PaperMaterial
-- 仿Claude 主题: https://github.com/bjl101501/CherryStudio-Claudestyle-dynamic
-- 霓虹枫叶字体主题: https://github.com/BoningtonChen/CherryStudio_themes
+- PaperMaterial 主题:https://github.com/rainoffallingstar/CherryStudio-PaperMaterial
+- 仿 Claude 主题:https://github.com/bjl101501/CherryStudio-Claudestyle-dynamic
+- 霓虹枫叶主题:https://github.com/BoningtonChen/CherryStudio_themes
欢迎 PR 更多主题
@@ -103,26 +104,30 @@ https://docs.cherry-ai.com
参考[开发文档](dev.md)
+参考[架构概览文档](https://deepwiki.com/CherryHQ/cherry-studio)
+
+参考[分支策略](branching-strategy-zh.md)了解贡献指南
+
# 🤝 贡献
我们欢迎对 Cherry Studio 的贡献!您可以通过以下方式贡献:
-1. **贡献代码**:开发新功能或优化现有代码。
-2. **修复错误**:提交您发现的错误修复。
-3. **维护问题**:帮助管理 GitHub 问题。
-4. **产品设计**:参与设计讨论。
-5. **撰写文档**:改进用户手册和指南。
-6. **社区参与**:加入讨论并帮助用户。
-7. **推广使用**:宣传 Cherry Studio。
+1. **贡献代码**:开发新功能或优化现有代码
+2. **修复错误**:提交您发现的错误修复
+3. **维护问题**:帮助管理 GitHub 问题
+4. **产品设计**:参与设计讨论
+5. **撰写文档**:改进用户手册和指南
+6. **社区参与**:加入讨论并帮助用户
+7. **推广使用**:宣传 Cherry Studio
## 入门
-1. **Fork 仓库**:Fork 并克隆到您的本地机器。
-2. **创建分支**:为您的更改创建分支。
-3. **提交更改**:提交并推送您的更改。
-4. **打开 Pull Request**:描述您的更改和原因。
+1. **Fork 仓库**:Fork 并克隆到您的本地机器
+2. **创建分支**:为您的更改创建分支
+3. **提交更改**:提交并推送您的更改
+4. **打开 Pull Request**:描述您的更改和原因
-有关更详细的指南,请参阅我们的 [贡献指南](./CONTRIBUTING.zh.md)。
+有关更详细的指南,请参阅我们的 [贡献指南](./CONTRIBUTING.zh.md)
感谢您的支持和贡献!
@@ -130,10 +135,12 @@ https://docs.cherry-ai.com
- [one-api](https://github.com/songquanpeng/one-api):LLM API 管理及分发系统,支持 OpenAI、Azure、Anthropic 等主流模型,统一 API 接口,可用于密钥管理与二次分发。
+- [ublacklist](https://github.com/iorate/ublacklist):屏蔽特定网站在 Google 搜索结果中显示
+
# 🚀 贡献者
-
+
@@ -143,7 +150,7 @@ https://docs.cherry-ai.com
# ☕ 赞助
-[微信赞赏码](sponsor.md)
+[赞助开发者](sponsor.md)
# 📃 许可证
@@ -155,4 +162,4 @@ yinsenho@cherry-ai.com
# ⭐️ Star 记录
-[](https://star-history.com/#kangfenmao/cherry-studio&Timeline)
\ No newline at end of file
+[](https://star-history.com/#kangfenmao/cherry-studio&Timeline)
diff --git a/docs/branching-strategy-en.md b/docs/branching-strategy-en.md
new file mode 100644
index 0000000000..f3b7ddf508
--- /dev/null
+++ b/docs/branching-strategy-en.md
@@ -0,0 +1,71 @@
+# 🌿 Branching Strategy
+
+Cherry Studio implements a structured branching strategy to maintain code quality and streamline the development process.
+
+## Main Branches
+
+- `main`: Main development branch
+
+ - Contains the latest development code
+ - Direct commits are not allowed - changes must come through pull requests
+ - Code may contain features in development and might not be fully stable
+
+- `release/*`: Release branches
+ - Created from `main` branch
+ - Contains stable code ready for release
+ - Only accepts documentation updates and bug fixes
+ - Thoroughly tested before production deployment
+
+## Contributing Branches
+
+When contributing to Cherry Studio, please follow these guidelines:
+
+1. **Feature Branches:**
+
+ - Create from `main` branch
+ - Naming format: `feature/issue-number-brief-description`
+ - Submit PR back to `main`
+
+2. **Bug Fix Branches:**
+
+ - Create from `main` branch
+ - Naming format: `fix/issue-number-brief-description`
+ - Submit PR back to `main`
+
+3. **Documentation Branches:**
+
+ - Create from `main` branch
+ - Naming format: `docs/brief-description`
+ - Submit PR back to `main`
+
+4. **Hotfix Branches:**
+
+ - Create from `main` branch
+ - Naming format: `hotfix/issue-number-brief-description`
+ - Submit PR to both `main` and relevant `release` branches
+
+5. **Release Branches:**
+ - Create from `main` branch
+ - Naming format: `release/version-number`
+ - Used for final preparation work before version release
+ - Only accepts bug fixes and documentation updates
+ - After testing and preparation, merge back to `main` and tag with version
+
+## Workflow Diagram
+
+
+
+## Pull Request Guidelines
+
+- All PRs should be submitted to the `main` branch unless fixing a critical production issue
+- Ensure your branch is up to date with the latest `main` changes before submitting
+- Include relevant issue numbers in your PR description
+- Make sure all tests pass and code meets our quality standards
+- Add before/after screenshots if you add a new feature or modify a UI component
+
+## Version Tag Management
+
+- Major releases: v1.0.0, v2.0.0, etc.
+- Feature releases: v1.1.0, v1.2.0, etc.
+- Patch releases: v1.0.1, v1.0.2, etc.
+- Hotfix releases: v1.0.1-hotfix, etc.
diff --git a/docs/branching-strategy-zh.md b/docs/branching-strategy-zh.md
new file mode 100644
index 0000000000..b1379537a5
--- /dev/null
+++ b/docs/branching-strategy-zh.md
@@ -0,0 +1,71 @@
+# 🌿 分支策略
+
+Cherry Studio 采用结构化的分支策略来维护代码质量并简化开发流程。
+
+## 主要分支
+
+- `main`:主开发分支
+
+ - 包含最新的开发代码
+ - 禁止直接提交 - 所有更改必须通过拉取请求(Pull Request)
+ - 此分支上的代码可能包含正在开发的功能,不一定完全稳定
+
+- `release/*`:发布分支
+ - 从 `main` 分支创建
+ - 包含准备发布的稳定代码
+ - 只接受文档更新和 bug 修复
+ - 经过完整测试后可以发布到生产环境
+
+## 贡献分支
+
+在为 Cherry Studio 贡献代码时,请遵循以下准则:
+
+1. **功能开发分支:**
+
+ - 从 `main` 分支创建
+ - 命名格式:`feature/issue-number-brief-description`
+ - 完成后提交 PR 到 `main` 分支
+
+2. **Bug 修复分支:**
+
+ - 从 `main` 分支创建
+ - 命名格式:`fix/issue-number-brief-description`
+ - 完成后提交 PR 到 `main` 分支
+
+3. **文档更新分支:**
+
+ - 从 `main` 分支创建
+ - 命名格式:`docs/brief-description`
+ - 完成后提交 PR 到 `main` 分支
+
+4. **紧急修复分支:**
+
+ - 从 `main` 分支创建
+ - 命名格式:`hotfix/issue-number-brief-description`
+ - 完成后需要同时合并到 `main` 和相关的 `release` 分支
+
+5. **发布分支:**
+ - 从 `main` 分支创建
+ - 命名格式:`release/version-number`
+ - 用于版本发布前的最终准备工作
+ - 只允许合并 bug 修复和文档更新
+ - 完成测试和准备工作后,将代码合并回 `main` 分支并打上版本标签
+
+## 工作流程
+
+
+
+## 拉取请求(PR)指南
+
+- 除非是修复生产环境的关键问题,否则所有 PR 都应该提交到 `main` 分支
+- 提交 PR 前确保你的分支已经同步了最新的 `main` 分支内容
+- 在 PR 描述中包含相关的 issue 编号
+- 确保所有测试通过,且代码符合我们的质量标准
+- 如果你添加了新功能或修改了 UI 组件,请附上更改前后的截图
+
+## 版本标签管理
+
+- 主要版本发布:v1.0.0、v2.0.0 等
+- 功能更新发布:v1.1.0、v1.2.0 等
+- 补丁修复发布:v1.0.1、v1.0.2 等
+- 紧急修复发布:v1.0.1-hotfix 等
diff --git a/docs/branching-strategy.md b/docs/branching-strategy.md
deleted file mode 100644
index 897763af16..0000000000
--- a/docs/branching-strategy.md
+++ /dev/null
@@ -1,52 +0,0 @@
-# 🌿 Branching Strategy
-
-Cherry Studio follows a structured branching strategy to maintain code quality and streamline the development process:
-
-## Main Branches
-
-- `main`: Production-ready branch containing stable releases
-
- - All code here is thoroughly tested and ready for production
- - Direct commits are not allowed - changes must come through pull requests
- - Each merge to main represents a new release
-
-- `develop` (default): Primary development branch
- - Contains the latest delivered development changes for the next release
- - Relatively stable but may contain features in progress
- - This is the default branch for development
-
-## Contributing Branches
-
-When contributing to Cherry Studio, please follow these guidelines:
-
-1. **For bug fixes:**
-
- - Create a branch from `develop`
- - Name format: `fix/issue-number-brief-description`
- - Submit pull request back to `develop`
-
-2. **For new features:**
-
- - Create a branch from `develop`
- - Name format: `feature/issue-number-brief-description`
- - Submit pull request back to `develop`
-
-3. **For documentation:**
-
- - Create a branch from `develop`
- - Name format: `docs/brief-description`
- - Submit pull request back to `develop`
-
-4. **For critical hotfixes:**
- - Create a branch from `main`
- - Name format: `hotfix/issue-number-brief-description`
- - Submit pull request to both `main` and `develop`
-
-## Pull Request Guidelines
-
-- Always create pull requests against the `develop` branch unless fixing a critical production issue
-- Ensure your branch is up to date with the latest `develop` changes before submitting
-- Include relevant issue numbers in your PR description
-- Make sure all tests pass and code meets our quality standards
-- Critical hotfixes may be submitted against `main` but must also be merged into `develop`
-- Add a photo to show what is different if you add a new feature or modify a component in the UI.
diff --git a/electron-builder.yml b/electron-builder.yml
index 4598455544..294335c36a 100644
--- a/electron-builder.yml
+++ b/electron-builder.yml
@@ -45,6 +45,8 @@ win:
target:
- target: nsis
- target: portable
+ signtoolOptions:
+ sign: scripts/win-sign.js
nsis:
artifactName: ${productName}-${version}-${arch}-setup.${ext}
shortcutName: ${productName}
@@ -92,9 +94,10 @@ artifactBuildCompleted: scripts/artifact-build-completed.js
releaseInfo:
releaseNotes: |
⚠️ 注意:升级前请备份数据,否则将无法降级
- 优化软件启动速度
- 优化软件进入后台后性能问题
- 修复导出对话时自动重命名失败问题
- 防止输入法切换期间误发消息问题
- 修复群组消息重发功能问题及富文本粘贴兼容性问题
- 改进 MCP 服务处理及 IPC 注册逻辑
+ 增加消息通知功能
+ 增加 Google 小程序
+ MCP 支持运行 Python 代码
+ 修复 MCP SSE 连接问题
+ 修复消息编辑和消息多选相关问题
+ 修复消息显示问题
+ 修复话题提示词无效问题
diff --git a/package.json b/package.json
index d72d1ef99c..9741b6ca73 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "CherryStudio",
- "version": "1.3.6",
+ "version": "1.3.9",
"private": true,
"description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js",
@@ -120,11 +120,11 @@
"@google/genai": "^0.13.0",
"@hello-pangea/dnd": "^16.6.0",
"@kangfenmao/keyv-storage": "^0.1.0",
- "@modelcontextprotocol/sdk": "^1.11.3",
+ "@modelcontextprotocol/sdk": "^1.11.4",
"@mozilla/readability": "^0.6.0",
"@notionhq/client": "^2.2.15",
"@reduxjs/toolkit": "^2.2.5",
- "@shikijs/markdown-it": "^3.2.2",
+ "@shikijs/markdown-it": "^3.4.2",
"@swc/plugin-styled-components": "^7.1.5",
"@tryfabric/martian": "^1.2.4",
"@types/better-sqlite3": "^7.6.13",
@@ -203,7 +203,7 @@
"remark-math": "^6.0.0",
"rollup-plugin-visualizer": "^5.12.0",
"sass": "^1.88.0",
- "shiki": "^3.2.2",
+ "shiki": "^3.4.2",
"string-width": "^7.2.0",
"styled-components": "^6.1.11",
"tiny-pinyin": "^1.3.2",
@@ -222,11 +222,10 @@
"openai@npm:^4.77.0": "patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch",
"pkce-challenge@npm:^4.1.0": "patch:pkce-challenge@npm%3A4.1.0#~/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch",
"app-builder-lib@npm:26.0.13": "patch:app-builder-lib@npm%3A26.0.13#~/.yarn/patches/app-builder-lib-npm-26.0.13-a064c9e1d0.patch",
- "shiki": "3.2.2",
"openai@npm:^4.87.3": "patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch",
"app-builder-lib@npm:26.0.15": "patch:app-builder-lib@npm%3A26.0.15#~/.yarn/patches/app-builder-lib-npm-26.0.15-360e5b0476.patch"
},
- "packageManager": "yarn@4.6.0",
+ "packageManager": "yarn@4.9.1",
"lint-staged": {
"*.{js,jsx,ts,tsx,cjs,mjs,cts,mts}": [
"prettier --write",
diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts
index 7c536015a9..7ba4164969 100644
--- a/packages/shared/IpcChannel.ts
+++ b/packages/shared/IpcChannel.ts
@@ -21,6 +21,9 @@ export enum IpcChannel {
App_InstallUvBinary = 'app:install-uv-binary',
App_InstallBunBinary = 'app:install-bun-binary',
+ Notification_Send = 'notification:send',
+ Notification_OnClick = 'notification:on-click',
+
Webview_SetOpenLinkExternal = 'webview:set-open-link-external',
// Open
@@ -52,6 +55,7 @@ export enum IpcChannel {
Mcp_GetInstallInfo = 'mcp:get-install-info',
Mcp_ServersChanged = 'mcp:servers-changed',
Mcp_ServersUpdated = 'mcp:servers-updated',
+ Mcp_CheckConnectivity = 'mcp:check-connectivity',
//copilot
Copilot_GetAuthMessage = 'copilot:get-auth-message',
@@ -112,7 +116,7 @@ export enum IpcChannel {
File_BinaryImage = 'file:binaryImage',
File_Base64File = 'file:base64File',
Fs_Read = 'fs:read',
- File_ResolveFilePath = 'file:resolveFilePath',
+
Export_Word = 'export:word',
Shortcuts_Update = 'shortcuts:update',
diff --git a/scripts/win-sign.js b/scripts/win-sign.js
new file mode 100644
index 0000000000..f9b37c3aed
--- /dev/null
+++ b/scripts/win-sign.js
@@ -0,0 +1,19 @@
+const { execSync } = require('child_process')
+
+exports.default = async function (configuration) {
+ if (process.env.WIN_SIGN) {
+ const { path } = configuration
+ if (configuration.path) {
+ try {
+ console.log('Start code signing...')
+ console.log('Signing file:', path)
+ const signCommand = `signtool sign /tr http://timestamp.comodoca.com /td sha256 /fd sha256 /a /v "${path}"`
+ execSync(signCommand, { stdio: 'inherit' })
+ console.log('Code signing completed')
+ } catch (error) {
+ console.error('Code signing failed:', error)
+ throw error
+ }
+ }
+ }
+}
diff --git a/src/main/index.ts b/src/main/index.ts
index 44d516a5ca..12b1c9c16f 100644
--- a/src/main/index.ts
+++ b/src/main/index.ts
@@ -115,7 +115,7 @@ if (!app.requestSingleInstanceLock()) {
app.on('will-quit', async () => {
// event.preventDefault()
try {
- await mcpService().cleanup()
+ await mcpService.cleanup()
} catch (error) {
Logger.error('Error cleaning up MCP service:', error)
}
diff --git a/src/main/ipc.ts b/src/main/ipc.ts
index e40b19aca1..45e9d7b72d 100644
--- a/src/main/ipc.ts
+++ b/src/main/ipc.ts
@@ -8,6 +8,7 @@ import { IpcChannel } from '@shared/IpcChannel'
import { Shortcut, ThemeMode } from '@types'
import { BrowserWindow, ipcMain, nativeTheme, session, shell } from 'electron'
import log from 'electron-log'
+import { Notification } from 'src/renderer/src/types/notification'
import { titleBarOverlayDark, titleBarOverlayLight } from './config'
import AppUpdater from './services/AppUpdater'
@@ -19,7 +20,8 @@ import FileService from './services/FileService'
import FileStorage from './services/FileStorage'
import { GeminiService } from './services/GeminiService'
import KnowledgeService from './services/KnowledgeService'
-import { getMcpInstance } from './services/MCPService'
+import mcpService from './services/MCPService'
+import NotificationService from './services/NotificationService'
import * as NutstoreService from './services/NutstoreService'
import ObsidianVaultService from './services/ObsidianVaultService'
import { ProxyConfig, proxyManager } from './services/ProxyManager'
@@ -41,6 +43,7 @@ const obsidianVaultService = new ObsidianVaultService()
export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
const appUpdater = new AppUpdater(mainWindow)
+ const notificationService = new NotificationService(mainWindow)
ipcMain.handle(IpcChannel.App_Info, () => ({
version: app.getVersion(),
@@ -200,6 +203,14 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
await appUpdater.checkForUpdates()
})
+ // notification
+ ipcMain.handle(IpcChannel.Notification_Send, async (_, notification: Notification) => {
+ await notificationService.sendNotification(notification)
+ })
+ ipcMain.handle(IpcChannel.Notification_OnClick, (_, notification: Notification) => {
+ mainWindow.webContents.send('notification-click', notification)
+ })
+
// zip
ipcMain.handle(IpcChannel.Zip_Compress, (_, text: string) => compress(text))
ipcMain.handle(IpcChannel.Zip_Decompress, (_, text: Buffer) => decompress(text))
@@ -242,7 +253,6 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle(IpcChannel.File_Download, fileManager.downloadFile)
ipcMain.handle(IpcChannel.File_Copy, fileManager.copyFile)
ipcMain.handle(IpcChannel.File_BinaryImage, fileManager.binaryImage)
- ipcMain.handle(IpcChannel.File_ResolveFilePath, (_, name) => fileManager.resolveFilePath(name))
// fs
ipcMain.handle(IpcChannel.Fs_Read, FileService.readFile)
@@ -310,16 +320,17 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
)
// Register MCP handlers
- ipcMain.handle(IpcChannel.Mcp_RemoveServer, (event, server) => getMcpInstance().removeServer(event, server))
- ipcMain.handle(IpcChannel.Mcp_RestartServer, (event, server) => getMcpInstance().restartServer(event, server))
- ipcMain.handle(IpcChannel.Mcp_StopServer, (event, server) => getMcpInstance().stopServer(event, server))
- ipcMain.handle(IpcChannel.Mcp_ListTools, (event, server) => getMcpInstance().listTools(event, server))
- ipcMain.handle(IpcChannel.Mcp_CallTool, (event, params) => getMcpInstance().callTool(event, params))
- ipcMain.handle(IpcChannel.Mcp_ListPrompts, (event, server) => getMcpInstance().listPrompts(event, server))
- ipcMain.handle(IpcChannel.Mcp_GetPrompt, (event, params) => getMcpInstance().getPrompt(event, params))
- ipcMain.handle(IpcChannel.Mcp_ListResources, (event, server) => getMcpInstance().listResources(event, server))
- ipcMain.handle(IpcChannel.Mcp_GetResource, (event, params) => getMcpInstance().getResource(event, params))
- ipcMain.handle(IpcChannel.Mcp_GetInstallInfo, () => getMcpInstance().getInstallInfo())
+ ipcMain.handle(IpcChannel.Mcp_RemoveServer, mcpService.removeServer)
+ ipcMain.handle(IpcChannel.Mcp_RestartServer, mcpService.restartServer)
+ ipcMain.handle(IpcChannel.Mcp_StopServer, mcpService.stopServer)
+ ipcMain.handle(IpcChannel.Mcp_ListTools, mcpService.listTools)
+ ipcMain.handle(IpcChannel.Mcp_CallTool, mcpService.callTool)
+ ipcMain.handle(IpcChannel.Mcp_ListPrompts, mcpService.listPrompts)
+ ipcMain.handle(IpcChannel.Mcp_GetPrompt, mcpService.getPrompt)
+ ipcMain.handle(IpcChannel.Mcp_ListResources, mcpService.listResources)
+ ipcMain.handle(IpcChannel.Mcp_GetResource, mcpService.getResource)
+ ipcMain.handle(IpcChannel.Mcp_GetInstallInfo, mcpService.getInstallInfo)
+ ipcMain.handle(IpcChannel.Mcp_CheckConnectivity, mcpService.checkMcpConnectivity)
ipcMain.handle(IpcChannel.App_IsBinaryExist, (_, name: string) => isBinaryExists(name))
ipcMain.handle(IpcChannel.App_GetBinaryPath, (_, name: string) => getBinaryPath(name))
diff --git a/src/main/services/BackupManager.ts b/src/main/services/BackupManager.ts
index 4d951f3698..18dbd00422 100644
--- a/src/main/services/BackupManager.ts
+++ b/src/main/services/BackupManager.ts
@@ -255,19 +255,26 @@ class BackupManager {
const sourcePath = path.join(this.tempDir, 'Data')
const destPath = path.join(app.getPath('userData'), 'Data')
- // 获取源目录总大小
- const totalSize = await this.getDirSize(sourcePath)
- let copiedSize = 0
+ const dataExists = await fs.pathExists(sourcePath)
+ const dataFiles = dataExists ? await fs.readdir(sourcePath) : []
- await this.setWritableRecursive(destPath)
- await fs.remove(destPath)
+ if (dataExists && dataFiles.length > 0) {
+ // 获取源目录总大小
+ const totalSize = await this.getDirSize(sourcePath)
+ let copiedSize = 0
- // 使用流式复制
- await this.copyDirWithProgress(sourcePath, destPath, (size) => {
- copiedSize += size
- const progress = Math.min(85, 35 + Math.floor((copiedSize / totalSize) * 50))
- onProgress({ stage: 'copying_files', progress, total: 100 })
- })
+ await this.setWritableRecursive(destPath)
+ await fs.remove(destPath)
+
+ // 使用流式复制
+ await this.copyDirWithProgress(sourcePath, destPath, (size) => {
+ copiedSize += size
+ const progress = Math.min(85, 35 + Math.floor((copiedSize / totalSize) * 50))
+ onProgress({ stage: 'copying_files', progress, total: 100 })
+ })
+ } else {
+ Logger.log('[backup] skipBackupFile is true, skip restoring Data directory')
+ }
Logger.log('[backup] step 4: clean up temp directory')
// 清理临时目录
diff --git a/src/main/services/ConfigManager.ts b/src/main/services/ConfigManager.ts
index bb0cbfc422..6242709385 100644
--- a/src/main/services/ConfigManager.ts
+++ b/src/main/services/ConfigManager.ts
@@ -62,7 +62,7 @@ export class ConfigManager {
}
getTrayOnClose(): boolean {
- return !!this.get(ConfigKeys.TrayOnClose, false)
+ return !!this.get(ConfigKeys.TrayOnClose, true)
}
setTrayOnClose(value: boolean) {
diff --git a/src/main/services/FileStorage.ts b/src/main/services/FileStorage.ts
index ee8d21a3a6..f055bdc5fb 100644
--- a/src/main/services/FileStorage.ts
+++ b/src/main/services/FileStorage.ts
@@ -27,10 +27,6 @@ class FileStorage {
this.initStorageDir()
}
- public resolveFilePath = (name: string): string => {
- return path.join(this.storageDir, name)
- }
-
private initStorageDir = (): void => {
try {
if (!fs.existsSync(this.storageDir)) {
@@ -332,7 +328,7 @@ class FileStorage {
fileName: string,
content: string,
options?: SaveDialogOptions
- ): Promise
=> {
+ ): Promise => {
try {
const result: SaveDialogReturnValue = await dialog.showSaveDialog({
title: '保存文件',
@@ -340,14 +336,18 @@ class FileStorage {
...options
})
+ if (result.canceled) {
+ return Promise.reject(new Error('User canceled the save dialog'))
+ }
+
if (!result.canceled && result.filePath) {
await writeFileSync(result.filePath, content, { encoding: 'utf-8' })
}
return result.filePath
- } catch (err) {
+ } catch (err: any) {
logger.error('[IPC - Error]', 'An error occurred saving the file:', err)
- return null
+ return Promise.reject('An error occurred saving the file: ' + err?.message)
}
}
diff --git a/src/main/services/GeminiService.ts b/src/main/services/GeminiService.ts
index 8427e1304e..e7b8310664 100644
--- a/src/main/services/GeminiService.ts
+++ b/src/main/services/GeminiService.ts
@@ -8,8 +8,19 @@ export class GeminiService {
private static readonly FILE_LIST_CACHE_KEY = 'gemini_file_list'
private static readonly CACHE_DURATION = 3000
- static async uploadFile(_: Electron.IpcMainInvokeEvent, file: FileType, apiKey: string): Promise {
- const sdk = new GoogleGenAI({ vertexai: false, apiKey })
+ static async uploadFile(
+ _: Electron.IpcMainInvokeEvent,
+ file: FileType,
+ { apiKey, baseURL }: { apiKey: string; baseURL: string }
+ ): Promise {
+ const sdk = new GoogleGenAI({
+ vertexai: false,
+ apiKey,
+ httpOptions: {
+ baseUrl: baseURL
+ }
+ })
+
return await sdk.files.upload({
file: file.path,
config: {
diff --git a/src/main/services/MCPService.ts b/src/main/services/MCPService.ts
index 90e50ec65a..bb6fc2835a 100644
--- a/src/main/services/MCPService.ts
+++ b/src/main/services/MCPService.ts
@@ -69,18 +69,10 @@ function withCache(
}
class McpService {
- private static instance: McpService | null = null
private clients: Map = new Map()
private pendingClients: Map> = new Map()
- public static getInstance(): McpService {
- if (!McpService.instance) {
- McpService.instance = new McpService()
- }
- return McpService.instance
- }
-
- private constructor() {
+ constructor() {
this.initClient = this.initClient.bind(this)
this.listTools = this.listTools.bind(this)
this.callTool = this.callTool.bind(this)
@@ -251,6 +243,12 @@ class McpService {
Logger.info(`[MCP] Starting server with command: ${cmd} ${args ? args.join(' ') : ''}`)
// Logger.info(`[MCP] Environment variables for server:`, server.env)
const loginShellEnv = await this.getLoginShellEnv()
+
+ // Bun not support proxy https://github.com/oven-sh/bun/issues/16812
+ if (cmd.endsWith('bun')) {
+ this.removeProxyEnv(loginShellEnv)
+ }
+
const stdioTransport = new StdioClientTransport({
command: cmd,
args,
@@ -396,6 +394,26 @@ class McpService {
}
}
+ /**
+ * Check connectivity for an MCP server
+ */
+ public async checkMcpConnectivity(_: Electron.IpcMainInvokeEvent, server: MCPServer): Promise {
+ Logger.info(`[MCP] Checking connectivity for server: ${server.name}`)
+ try {
+ const client = await this.initClient(server)
+ // Attempt to list tools as a way to check connectivity
+ await client.listTools()
+ Logger.info(`[MCP] Connectivity check successful for server: ${server.name}`)
+ return true
+ } catch (error) {
+ Logger.error(`[MCP] Connectivity check failed for server: ${server.name}`, error)
+ // Close the client if connectivity check fails to ensure a clean state for the next attempt
+ const serverKey = this.getServerKey(server)
+ await this.closeClient(serverKey)
+ return false
+ }
+ }
+
private async listToolsImpl(server: MCPServer): Promise {
Logger.info(`[MCP] Listing tools for server: ${server.name}`)
const client = await this.initClient(server)
@@ -639,15 +657,14 @@ class McpService {
return {}
}
})
-}
-let mcpInstance: ReturnType | null = null
-
-export const getMcpInstance = () => {
- if (!mcpInstance) {
- mcpInstance = McpService.getInstance()
+ private removeProxyEnv(env: Record) {
+ delete env.HTTPS_PROXY
+ delete env.HTTP_PROXY
+ delete env.grpc_proxy
+ delete env.http_proxy
+ delete env.https_proxy
}
- return mcpInstance
}
-export default McpService.getInstance
+export default new McpService()
diff --git a/src/main/services/NotificationService.ts b/src/main/services/NotificationService.ts
new file mode 100644
index 0000000000..e06036b523
--- /dev/null
+++ b/src/main/services/NotificationService.ts
@@ -0,0 +1,31 @@
+import { BrowserWindow, Notification as ElectronNotification } from 'electron'
+import { Notification } from 'src/renderer/src/types/notification'
+
+import icon from '../../../build/icon.png?asset'
+
+class NotificationService {
+ private window: BrowserWindow
+
+ constructor(window: BrowserWindow) {
+ // Initialize the service
+ this.window = window
+ }
+
+ public async sendNotification(notification: Notification) {
+ // 使用 Electron Notification API
+ const electronNotification = new ElectronNotification({
+ title: notification.title,
+ body: notification.message,
+ icon: icon
+ })
+
+ electronNotification.on('click', () => {
+ this.window.show()
+ this.window.webContents.send('notification-click', notification)
+ })
+
+ electronNotification.show()
+ }
+}
+
+export default NotificationService
diff --git a/src/main/services/WindowService.ts b/src/main/services/WindowService.ts
index ba508d7048..b67cac03d6 100644
--- a/src/main/services/WindowService.ts
+++ b/src/main/services/WindowService.ts
@@ -331,11 +331,6 @@ export class WindowService {
event.preventDefault()
- if (mainWindow.isFullScreen()) {
- mainWindow.setFullScreen(false)
- return
- }
-
mainWindow.hide()
//for mac users, should hide dock icon if close to tray
diff --git a/src/preload/index.ts b/src/preload/index.ts
index f70ca04d20..81174d22d0 100644
--- a/src/preload/index.ts
+++ b/src/preload/index.ts
@@ -3,6 +3,7 @@ import { electronAPI } from '@electron-toolkit/preload'
import { IpcChannel } from '@shared/IpcChannel'
import { FileType, KnowledgeBaseParams, KnowledgeItem, MCPServer, Shortcut, WebDavConfig } from '@types'
import { contextBridge, ipcRenderer, OpenDialogOptions, shell, webUtils } from 'electron'
+import { Notification } from 'src/renderer/src/types/notification'
import { CreateDirectoryOptions } from 'webdav'
// Custom APIs for renderer
@@ -25,6 +26,9 @@ const api = {
openWebsite: (url: string) => ipcRenderer.invoke(IpcChannel.Open_Website, url),
getCacheSize: () => ipcRenderer.invoke(IpcChannel.App_GetCacheSize),
clearCache: () => ipcRenderer.invoke(IpcChannel.App_ClearCache),
+ notification: {
+ send: (notification: Notification) => ipcRenderer.invoke(IpcChannel.Notification_Send, notification)
+ },
system: {
getDeviceType: () => ipcRenderer.invoke(IpcChannel.System_GetDeviceType),
getHostname: () => ipcRenderer.invoke(IpcChannel.System_GetHostname)
@@ -55,7 +59,6 @@ const api = {
},
file: {
select: (options?: OpenDialogOptions) => ipcRenderer.invoke(IpcChannel.File_Select, options),
- resolveFilePath: (name: string) => ipcRenderer.invoke(IpcChannel.File_ResolveFilePath, name),
upload: (file: FileType) => ipcRenderer.invoke(IpcChannel.File_Upload, file),
delete: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_Delete, fileId),
read: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_Read, fileId),
@@ -113,7 +116,8 @@ const api = {
resetMinimumSize: () => ipcRenderer.invoke(IpcChannel.Windows_ResetMinimumSize)
},
gemini: {
- uploadFile: (file: FileType, apiKey: string) => ipcRenderer.invoke(IpcChannel.Gemini_UploadFile, file, apiKey),
+ uploadFile: (file: FileType, { apiKey, baseURL }: { apiKey: string; baseURL: string }) =>
+ ipcRenderer.invoke(IpcChannel.Gemini_UploadFile, file, { apiKey, baseURL }),
base64File: (file: FileType) => ipcRenderer.invoke(IpcChannel.Gemini_Base64File, file),
retrieveFile: (file: FileType, apiKey: string) => ipcRenderer.invoke(IpcChannel.Gemini_RetrieveFile, file, apiKey),
listFiles: (apiKey: string) => ipcRenderer.invoke(IpcChannel.Gemini_ListFiles, apiKey),
@@ -149,7 +153,8 @@ const api = {
listResources: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_ListResources, server),
getResource: ({ server, uri }: { server: MCPServer; uri: string }) =>
ipcRenderer.invoke(IpcChannel.Mcp_GetResource, { server, uri }),
- getInstallInfo: () => ipcRenderer.invoke(IpcChannel.Mcp_GetInstallInfo)
+ getInstallInfo: () => ipcRenderer.invoke(IpcChannel.Mcp_GetInstallInfo),
+ checkMcpConnectivity: (server: any) => ipcRenderer.invoke(IpcChannel.Mcp_CheckConnectivity, server)
},
shell: {
openExternal: (url: string, options?: Electron.OpenExternalOptions) => shell.openExternal(url, options)
diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx
index 24024374ec..b46910cd65 100644
--- a/src/renderer/src/App.tsx
+++ b/src/renderer/src/App.tsx
@@ -9,6 +9,7 @@ import Sidebar from './components/app/Sidebar'
import TopViewContainer from './components/TopView'
import AntdProvider from './context/AntdProvider'
import { CodeStyleProvider } from './context/CodeStyleProvider'
+import { NotificationProvider } from './context/NotificationProvider'
import StyleSheetManager from './context/StyleSheetManager'
import { ThemeProvider } from './context/ThemeProvider'
import NavigationHandler from './handler/NavigationHandler'
@@ -27,26 +28,28 @@ function App(): React.ReactElement {
-
-
-
-
-
-
-
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
-
-
-
-
-
+
+
+
+
+
+
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+
+
+
+
diff --git a/src/renderer/src/assets/images/apps/google.svg b/src/renderer/src/assets/images/apps/google.svg
new file mode 100644
index 0000000000..b518c52704
--- /dev/null
+++ b/src/renderer/src/assets/images/apps/google.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/renderer/src/assets/images/models/hailuo.png b/src/renderer/src/assets/images/models/hailuo.png
index e89ca0f26b..ca0d1e3f07 100644
Binary files a/src/renderer/src/assets/images/models/hailuo.png and b/src/renderer/src/assets/images/models/hailuo.png differ
diff --git a/src/renderer/src/assets/images/models/hailuo_dark.png b/src/renderer/src/assets/images/models/hailuo_dark.png
index b783bb6c62..8330a88375 100644
Binary files a/src/renderer/src/assets/images/models/hailuo_dark.png and b/src/renderer/src/assets/images/models/hailuo_dark.png differ
diff --git a/src/renderer/src/assets/images/providers/burncloud.png b/src/renderer/src/assets/images/providers/burncloud.png
new file mode 100644
index 0000000000..22888bff25
Binary files /dev/null and b/src/renderer/src/assets/images/providers/burncloud.png differ
diff --git a/src/renderer/src/assets/images/providers/qwenlm.png b/src/renderer/src/assets/images/providers/qwenlm.png
deleted file mode 100644
index d207a28997..0000000000
Binary files a/src/renderer/src/assets/images/providers/qwenlm.png and /dev/null differ
diff --git a/src/renderer/src/assets/styles/ant.scss b/src/renderer/src/assets/styles/ant.scss
index 3bbaa516b9..3dd08edc02 100644
--- a/src/renderer/src/assets/styles/ant.scss
+++ b/src/renderer/src/assets/styles/ant.scss
@@ -17,7 +17,7 @@
}
.ant-tabs-tab-btn {
- outline: none;
+ outline: none !important;
}
.ant-segmented-group {
diff --git a/src/renderer/src/assets/styles/font.scss b/src/renderer/src/assets/styles/font.scss
index 9d2d139b53..9bb6a01633 100644
--- a/src/renderer/src/assets/styles/font.scss
+++ b/src/renderer/src/assets/styles/font.scss
@@ -5,8 +5,9 @@
'Noto Color Emoji';
--font-family-serif:
- serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, Ubuntu, Roboto, Oxygen, Cantarell, 'Open Sans',
- 'Helvetica Neue', Arial, 'Noto Sans', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
+ -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, Ubuntu, Roboto, Oxygen, Cantarell, 'Open Sans',
+ 'Helvetica Neue', serif, Arial, 'Noto Sans', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
+ 'Noto Color Emoji';
--code-font-family: 'Cascadia Code', 'Fira Code', 'Consolas', Menlo, Courier, monospace;
}
diff --git a/src/renderer/src/assets/styles/markdown.scss b/src/renderer/src/assets/styles/markdown.scss
index 40c0255468..aed4919e85 100644
--- a/src/renderer/src/assets/styles/markdown.scss
+++ b/src/renderer/src/assets/styles/markdown.scss
@@ -321,7 +321,6 @@ mjx-container {
.cm-lineWrapping * {
word-wrap: break-word;
- white-space: pre-wrap;
}
}
}
diff --git a/src/renderer/src/components/Alert/OpenAIAlert.tsx b/src/renderer/src/components/Alert/OpenAIAlert.tsx
index 7f8695fa05..455ab62987 100644
--- a/src/renderer/src/components/Alert/OpenAIAlert.tsx
+++ b/src/renderer/src/components/Alert/OpenAIAlert.tsx
@@ -17,7 +17,7 @@ const OpenAIAlert = () => {
return (
{
diff --git a/src/renderer/src/components/CodeBlockView/CodePreview.tsx b/src/renderer/src/components/CodeBlockView/CodePreview.tsx
index 965d6ad14b..d17a146112 100644
--- a/src/renderer/src/components/CodeBlockView/CodePreview.tsx
+++ b/src/renderer/src/components/CodeBlockView/CodePreview.tsx
@@ -162,21 +162,25 @@ const CodePreview = ({ children, language }: CodePreviewProps) => {
}
}, [highlightCode])
+ const hasHighlightedCode = useMemo(() => {
+ return tokenLines.length > 0
+ }, [tokenLines.length])
+
return (
- {tokenLines.length > 0 ? (
-
+ {hasHighlightedCode ? (
+
+
+
) : (
- {children}
+ {children}
)}
)
@@ -223,15 +227,20 @@ const ShikiTokensRenderer: React.FC<{ language: string; tokenLines: ThemedToken[
)
const ContentContainer = styled.div<{
- $isShowLineNumbers: boolean
- $isUnwrapped: boolean
- $isCodeWrappable: boolean
+ $lineNumbers: boolean
+ $wrap: boolean
}>`
position: relative;
+ overflow: auto;
+ display: flex;
+ flex-direction: column;
border: 0.5px solid transparent;
border-radius: 5px;
margin-top: 0;
- transition: opacity 0.3s ease;
+
+ ::-webkit-scrollbar-thumb {
+ border-radius: 10px;
+ }
.shiki {
padding: 1em;
@@ -244,13 +253,18 @@ const ContentContainer = styled.div<{
.line {
display: block;
min-height: 1.3rem;
- padding-left: ${(props) => (props.$isShowLineNumbers ? '2rem' : '0')};
+ padding-left: ${(props) => (props.$lineNumbers ? '2rem' : '0')};
+
+ * {
+ word-wrap: ${(props) => (props.$wrap ? 'break-word' : undefined)};
+ white-space: ${(props) => (props.$wrap ? 'pre-wrap' : 'pre')};
+ }
}
}
}
${(props) =>
- props.$isShowLineNumbers &&
+ props.$lineNumbers &&
`
code {
counter-reset: step;
@@ -269,15 +283,28 @@ const ContentContainer = styled.div<{
}
`}
- ${(props) =>
- props.$isCodeWrappable &&
- !props.$isUnwrapped &&
- `
- code .line * {
- word-wrap: break-word;
- white-space: pre-wrap;
- }
- `}
+ @keyframes contentFadeIn {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+ }
+
+ .fade-in-effect {
+ animation: contentFadeIn 0.3s ease-in-out forwards;
+ }
+`
+
+const CodePlaceholder = styled.div`
+ opacity: 0.1;
+ flex-direction: column;
+ white-space: pre-wrap;
+ word-break: break-all;
+ overflow-x: hidden;
+ display: block;
+ min-height: 1.3rem;
`
CodePreview.displayName = 'CodePreview'
diff --git a/src/renderer/src/components/CodeBlockView/HtmlArtifacts.tsx b/src/renderer/src/components/CodeBlockView/HtmlArtifacts.tsx
index e979ea1541..0dbb0aabb2 100644
--- a/src/renderer/src/components/CodeBlockView/HtmlArtifacts.tsx
+++ b/src/renderer/src/components/CodeBlockView/HtmlArtifacts.tsx
@@ -13,7 +13,6 @@ interface Props {
const Artifacts: FC = ({ html }) => {
const { t } = useTranslation()
- const title = extractTitle(html) || 'Artifacts ' + t('chat.artifacts.button.preview')
const { openMinapp } = useMinappPopup()
/**
@@ -23,6 +22,7 @@ const Artifacts: FC = ({ html }) => {
const path = await window.api.file.create('artifacts-preview.html')
await window.api.file.write(path, html)
const filePath = `file://${path}`
+ const title = extractTitle(html) || 'Artifacts ' + t('chat.artifacts.button.preview')
openMinapp({
id: 'artifacts-preview',
name: title,
diff --git a/src/renderer/src/components/CodeBlockView/index.tsx b/src/renderer/src/components/CodeBlockView/index.tsx
index 86a5f0d043..19b8796d09 100644
--- a/src/renderer/src/components/CodeBlockView/index.tsx
+++ b/src/renderer/src/components/CodeBlockView/index.tsx
@@ -9,7 +9,7 @@ import dayjs from 'dayjs'
import { CirclePlay, CodeXml, Copy, Download, Eye, Square, SquarePen, SquareSplitHorizontal } from 'lucide-react'
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
-import styled, { css } from 'styled-components'
+import styled from 'styled-components'
import CodePreview from './CodePreview'
import HtmlArtifacts from './HtmlArtifacts'
@@ -199,12 +199,11 @@ const CodeBlockView: React.FC = ({ children, language, onSave }) => {
// 源代码视图组件
const sourceView = useMemo(() => {
- const SourceView = codeEditor.enabled ? CodeEditor : CodePreview
- return (
-
- {children}
-
- )
+ if (codeEditor.enabled) {
+ return
+ } else {
+ return {children}
+ }
}, [children, codeEditor.enabled, language, onSave])
// 特殊视图组件映射
@@ -259,6 +258,9 @@ const CodeBlockWrapper = styled.div<{ $isInSpecialView: boolean }>`
position: relative;
.code-toolbar {
+ margin-top: ${(props) => (props.$isInSpecialView ? '20px' : '0')};
+ background-color: ${(props) => (props.$isInSpecialView ? 'transparent' : 'var(--color-background-mute)')};
+ border-radius: ${(props) => (props.$isInSpecialView ? '0' : '4px')};
opacity: 0;
transition: opacity 0.2s ease;
transform: translateZ(0);
@@ -272,23 +274,6 @@ const CodeBlockWrapper = styled.div<{ $isInSpecialView: boolean }>`
opacity: 1;
}
}
-
- ${(props) =>
- props.$isInSpecialView &&
- css`
- .code-toolbar {
- margin-top: 20px;
- }
- `}
-
- ${(props) =>
- !props.$isInSpecialView &&
- css`
- .code-toolbar {
- background-color: var(--color-background-mute);
- border-radius: 4px;
- }
- `}
`
const CodeHeader = styled.div<{ $isInSpecialView: boolean }>`
@@ -298,16 +283,10 @@ const CodeHeader = styled.div<{ $isInSpecialView: boolean }>`
color: var(--color-text);
font-size: 14px;
font-weight: bold;
- height: 34px;
padding: 0 10px;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
-
- ${(props) =>
- props.$isInSpecialView &&
- css`
- height: 16px;
- `}
+ height: ${(props) => (props.$isInSpecialView ? '16px' : '34px')};
`
const SplitViewWrapper = styled.div`
@@ -316,8 +295,9 @@ const SplitViewWrapper = styled.div`
> * {
flex: 1 1 0;
+ width: 0;
min-width: 0;
- overflow: auto;
+ max-width: 100%;
}
`
diff --git a/src/renderer/src/components/CodeEditor/hook.ts b/src/renderer/src/components/CodeEditor/hook.ts
new file mode 100644
index 0000000000..c5bbab2d0d
--- /dev/null
+++ b/src/renderer/src/components/CodeEditor/hook.ts
@@ -0,0 +1,65 @@
+import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
+import { Extension } from '@uiw/react-codemirror'
+import { useEffect, useState } from 'react'
+
+let linterPromise: Promise | null = null
+function importLintPackage() {
+ if (!linterPromise) {
+ linterPromise = import('@codemirror/lint').then((mod) => mod.linter)
+ }
+ return linterPromise
+}
+
+// 语言对应的 linter 加载器
+const linterLoaders: Record Promise> = {
+ json: async () => {
+ const [linter, jsonParseLinter] = await Promise.all([
+ importLintPackage(),
+ import('@codemirror/lang-json').then((mod) => mod.jsonParseLinter)
+ ])
+ return linter(jsonParseLinter())
+ }
+}
+
+export const useLanguageExtensions = (language: string, lint?: boolean) => {
+ const { languageMap } = useCodeStyle()
+ const [extensions, setExtensions] = useState([])
+
+ // 加载语言
+ useEffect(() => {
+ let normalizedLang = languageMap[language as keyof typeof languageMap] || language.toLowerCase()
+
+ // 如果语言名包含 `-`,转换为驼峰命名法
+ if (normalizedLang.includes('-')) {
+ normalizedLang = normalizedLang.replace(/-([a-z])/g, (_, char) => char.toUpperCase())
+ }
+
+ import('@uiw/codemirror-extensions-langs')
+ .then(({ loadLanguage }) => {
+ const extension = loadLanguage(normalizedLang as any)
+ if (extension) {
+ setExtensions((prev) => [...prev, extension])
+ }
+ })
+ .catch((error) => {
+ console.debug(`Failed to load language: ${normalizedLang}`, error)
+ })
+ }, [language, languageMap])
+
+ useEffect(() => {
+ if (!lint) return
+
+ const loader = linterLoaders[language]
+ if (loader) {
+ loader()
+ .then((extension) => {
+ setExtensions((prev) => [...prev, extension])
+ })
+ .catch((error) => {
+ console.error(`Failed to load linter for ${language}`, error)
+ })
+ }
+ }, [language, lint])
+
+ return extensions
+}
diff --git a/src/renderer/src/components/CodeEditor/index.tsx b/src/renderer/src/components/CodeEditor/index.tsx
index 124912e277..6000e91b49 100644
--- a/src/renderer/src/components/CodeEditor/index.tsx
+++ b/src/renderer/src/components/CodeEditor/index.tsx
@@ -14,31 +14,50 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
+import { useLanguageExtensions } from './hook'
+
// 标记非用户编辑的变更
const External = Annotation.define()
interface Props {
- children: string
+ value: string
+ placeholder?: string | HTMLElement
language: string
onSave?: (newContent: string) => void
onChange?: (newContent: string) => void
+ minHeight?: string
maxHeight?: string
/** 用于覆写编辑器的某些设置 */
options?: {
+ stream?: boolean // 用于流式响应场景,默认 false
+ lint?: boolean
collapsible?: boolean
wrappable?: boolean
keymap?: boolean
} & BasicSetupOptions
+ /** 用于追加 extensions */
+ extensions?: Extension[]
/** 用于覆写编辑器的样式,会直接传给 CodeMirror 的 style 属性 */
style?: React.CSSProperties
}
/**
- * 源代码编辑器,基于 CodeMirror
+ * 源代码编辑器,基于 CodeMirror,封装了 ReactCodeMirror。
*
* 目前必须和 CodeToolbar 配合使用。
*/
-const CodeEditor = ({ children, language, onSave, onChange, maxHeight, options, style }: Props) => {
+const CodeEditor = ({
+ value,
+ placeholder,
+ language,
+ onSave,
+ onChange,
+ minHeight,
+ maxHeight,
+ options,
+ extensions,
+ style
+}: Props) => {
const {
fontSize,
codeShowLineNumbers: _lineNumbers,
@@ -59,38 +78,18 @@ const CodeEditor = ({ children, language, onSave, onChange, maxHeight, options,
}
}, [codeEditor, _lineNumbers, options])
- const { activeCmTheme, languageMap } = useCodeStyle()
+ const { activeCmTheme } = useCodeStyle()
const [isExpanded, setIsExpanded] = useState(!collapsible)
const [isUnwrapped, setIsUnwrapped] = useState(!wrappable)
- const initialContent = useRef(children?.trimEnd() ?? '')
- const [langExtension, setLangExtension] = useState([])
+ const initialContent = useRef(options?.stream ? (value ?? '').trimEnd() : (value ?? ''))
const [editorReady, setEditorReady] = useState(false)
const editorViewRef = useRef(null)
const { t } = useTranslation()
+ const langExtensions = useLanguageExtensions(language, options?.lint)
+
const { registerTool, removeTool } = useCodeToolbar()
- // 加载语言
- useEffect(() => {
- let normalizedLang = languageMap[language as keyof typeof languageMap] || language.toLowerCase()
-
- // 如果语言名包含 `-`,转换为驼峰命名法
- if (normalizedLang.includes('-')) {
- normalizedLang = normalizedLang.replace(/-([a-z])/g, (_, char) => char.toUpperCase())
- }
-
- import('@uiw/codemirror-extensions-langs')
- .then(({ loadLanguage }) => {
- const extension = loadLanguage(normalizedLang as any)
- if (extension) {
- setLangExtension([extension])
- }
- })
- .catch((error) => {
- console.debug(`Failed to load language: ${normalizedLang}`, error)
- })
- }, [language, languageMap])
-
// 展开/折叠工具
useEffect(() => {
registerTool({
@@ -142,7 +141,7 @@ const CodeEditor = ({ children, language, onSave, onChange, maxHeight, options,
useEffect(() => {
if (!editorViewRef.current) return
- const newContent = children?.trimEnd() ?? ''
+ const newContent = options?.stream ? (value ?? '').trimEnd() : (value ?? '')
const currentDoc = editorViewRef.current.state.doc.toString()
const changes = prepareCodeChanges(currentDoc, newContent)
@@ -153,7 +152,7 @@ const CodeEditor = ({ children, language, onSave, onChange, maxHeight, options,
annotations: [External.of(true)]
})
}
- }, [children])
+ }, [options?.stream, value])
useEffect(() => {
setIsExpanded(!collapsible)
@@ -177,20 +176,27 @@ const CodeEditor = ({ children, language, onSave, onChange, maxHeight, options,
])
}, [handleSave])
- const enabledExtensions = useMemo(() => {
- return [...langExtension, ...(isUnwrapped ? [] : [EditorView.lineWrapping]), ...(enableKeymap ? [saveKeymap] : [])]
- }, [enableKeymap, langExtension, isUnwrapped, saveKeymap])
+ const customExtensions = useMemo(() => {
+ return [
+ ...(extensions ?? []),
+ ...langExtensions,
+ ...(isUnwrapped ? [] : [EditorView.lineWrapping]),
+ ...(enableKeymap ? [saveKeymap] : [])
+ ]
+ }, [extensions, langExtensions, isUnwrapped, enableKeymap, saveKeymap])
return (
{
editorViewRef.current = view
setEditorReady(true)
@@ -217,8 +223,6 @@ const CodeEditor = ({ children, language, onSave, onChange, maxHeight, options,
}}
style={{
fontSize: `${fontSize - 1}px`,
- overflow: collapsible && !isExpanded ? 'auto' : 'visible',
- position: 'relative',
border: '0.5px solid transparent',
borderRadius: '5px',
marginTop: 0,
diff --git a/src/renderer/src/components/CodeToolbar/usePreviewTools.tsx b/src/renderer/src/components/CodeToolbar/usePreviewTools.tsx
index 7cd49f95da..d1af8f49f2 100644
--- a/src/renderer/src/components/CodeToolbar/usePreviewTools.tsx
+++ b/src/renderer/src/components/CodeToolbar/usePreviewTools.tsx
@@ -284,16 +284,6 @@ export const usePreviewTools = ({ handleZoom, handleCopyImage, handleDownload }:
const { t } = useTranslation()
const { registerTool, removeTool } = useCodeToolbar()
- const toolIds = useCallback(() => {
- return {
- zoomIn: 'preview-zoom-in',
- zoomOut: 'preview-zoom-out',
- copyImage: 'preview-copy-image',
- downloadSvg: 'preview-download-svg',
- downloadPng: 'preview-download-png'
- }
- }, [])
-
useEffect(() => {
// 根据提供的功能有选择性地注册工具
if (handleZoom) {
@@ -356,5 +346,5 @@ export const usePreviewTools = ({ handleZoom, handleCopyImage, handleDownload }:
removeTool(TOOL_SPECS['download-png'].id)
}
}
- }, [handleCopyImage, handleDownload, handleZoom, registerTool, removeTool, t, toolIds])
+ }, [handleCopyImage, handleDownload, handleZoom, registerTool, removeTool, t])
}
diff --git a/src/renderer/src/components/ContentSearch.tsx b/src/renderer/src/components/ContentSearch.tsx
index 08f056a181..08a1fd415a 100644
--- a/src/renderer/src/components/ContentSearch.tsx
+++ b/src/renderer/src/components/ContentSearch.tsx
@@ -399,6 +399,7 @@ export const ContentSearch = React.forwardRef(
searchInputFocus()
}
+ // eslint-disable-next-line react-hooks/exhaustive-deps
const implementation = {
disable() {
setEnableContentSearch(false)
@@ -526,8 +527,7 @@ export const ContentSearch = React.forwardRef(
if (enableContentSearch && searchInputRef.current?.value.trim()) {
implementation.search()
}
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [isCaseSensitive, isWholeWord, enableContentSearch]) // Add enableContentSearch dependency
+ }, [isCaseSensitive, isWholeWord, enableContentSearch, implementation]) // Add enableContentSearch dependency
const prevButtonOnClick = () => {
implementation.searchPrev()
@@ -558,7 +558,13 @@ export const ContentSearch = React.forwardRef(
-
+
@@ -624,17 +630,19 @@ const Container = styled.div`
`
const SearchBarContainer = styled.div`
- border: 1px solid var(--color-border);
+ border: 1px solid var(--color-primary);
border-radius: 10px;
transition: all 0.2s ease;
- position: relative;
- margin: 5px 20px;
- margin-bottom: 0;
- padding: 6px 15px 8px;
+ position: fixed;
+ top: 15px;
+ left: 20px;
+ right: 20px;
+ margin-bottom: 5px;
+ padding: 5px 15px;
display: flex;
align-items: center;
justify-content: center;
- background-color: var(--color-background-opacity);
+ background-color: var(--color-background);
flex: 1 1 auto; /* Take up input's previous space */
`
@@ -682,18 +690,18 @@ const SearchResults = styled.div`
width: 80px;
margin: 0 2px;
flex: 0 0 auto;
- color: var(--color-text-secondary);
+ color: var(--color-text-1);
font-size: 14px;
font-family: Ubuntu;
`
const SearchResultsPlaceholder = styled.span`
- color: var(--color-text-secondary);
+ color: var(--color-text-1);
opacity: 0.5;
`
const NoResults = styled.span`
- color: var(--color-text-secondary);
+ color: var(--color-text-1);
`
const SearchResultCount = styled.span`
diff --git a/src/renderer/src/components/ContextMenu/index.tsx b/src/renderer/src/components/ContextMenu/index.tsx
index 02b9b3eafd..d0eace1dbf 100644
--- a/src/renderer/src/components/ContextMenu/index.tsx
+++ b/src/renderer/src/components/ContextMenu/index.tsx
@@ -2,6 +2,7 @@ import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { Dropdown } from 'antd'
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
+import styled from 'styled-components'
interface ContextMenuProps {
children: React.ReactNode
@@ -73,7 +74,7 @@ const ContextMenu: React.FC = ({ children, onContextMenu }) =>
]
return (
-
+
{contextMenuPosition && (
= ({ children, onContextMenu }) =>
)}
{children}
-
+
)
}
+const ContextContainer = styled.div``
+
export default ContextMenu
diff --git a/src/renderer/src/components/MinApp/WebviewContainer.tsx b/src/renderer/src/components/MinApp/WebviewContainer.tsx
index 884c9a015a..e5f08c350b 100644
--- a/src/renderer/src/components/MinApp/WebviewContainer.tsx
+++ b/src/renderer/src/components/MinApp/WebviewContainer.tsx
@@ -67,6 +67,11 @@ const WebviewContainer = memo(
style={WebviewStyle}
allowpopups={'true' as any}
partition="persist:webview"
+ useragent={
+ appid === 'google'
+ ? 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36'
+ : undefined
+ }
/>
)
}
diff --git a/src/renderer/src/components/Popups/FloatingSidebar.tsx b/src/renderer/src/components/Popups/FloatingSidebar.tsx
index df77619d90..d5a576309d 100644
--- a/src/renderer/src/components/Popups/FloatingSidebar.tsx
+++ b/src/renderer/src/components/Popups/FloatingSidebar.tsx
@@ -52,29 +52,28 @@ const FloatingSidebar: FC = ({
setActiveAssistant={setActiveAssistant}
setActiveTopic={setActiveTopic}
position={position}
- forceToSeeAllTab={true}>
+ forceToSeeAllTab={true}
+ style={{
+ background: 'transparent',
+ border: 'none'
+ }}
+ />
)
return (
{
- setOpen(visible)
- }}
+ onOpenChange={setOpen}
content={content}
- trigger={['hover', 'click']}
+ trigger={['hover', 'click', 'contextMenu']}
placement="bottomRight"
- arrow={false}
+ showArrow
mouseEnterDelay={0.8} // 800ms delay before showing
mouseLeaveDelay={20}
styles={{
body: {
- padding: 0,
- background: 'var(--color-background)',
- border: '1px solid var(--color-border)',
- borderRadius: '8px',
- boxShadow: '0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12)'
+ padding: 0
}
}}>
{children}
diff --git a/src/renderer/src/components/Popups/MultiSelectionPopup.tsx b/src/renderer/src/components/Popups/MultiSelectionPopup.tsx
new file mode 100644
index 0000000000..f021b631f9
--- /dev/null
+++ b/src/renderer/src/components/Popups/MultiSelectionPopup.tsx
@@ -0,0 +1,97 @@
+import { useChatContext } from '@renderer/hooks/useChatContext'
+import { Topic } from '@renderer/types'
+import { Button, Tooltip } from 'antd'
+import { Copy, Save, Trash, X } from 'lucide-react'
+import { FC } from 'react'
+import { useTranslation } from 'react-i18next'
+import styled from 'styled-components'
+
+interface Props {
+ topic: Topic
+}
+
+const MultiSelectActionPopup: FC = ({ topic }) => {
+ const { t } = useTranslation()
+ const { toggleMultiSelectMode, selectedMessageIds, isMultiSelectMode, handleMultiSelectAction } =
+ useChatContext(topic)
+
+ const handleAction = (action: string) => {
+ handleMultiSelectAction(action, selectedMessageIds)
+ }
+
+ const handleClose = () => {
+ toggleMultiSelectMode(false)
+ }
+
+ if (!isMultiSelectMode) return null
+
+ // TODO: 视情况调整
+ // const isActionDisabled = selectedMessages.some((msg) => msg.role === 'user')
+ const isActionDisabled = false
+
+ return (
+
+
+ {t('common.selectedMessages', { count: selectedMessageIds.length })}
+
+
+ } disabled={isActionDisabled} onClick={() => handleAction('save')} />
+
+
+ } disabled={isActionDisabled} onClick={() => handleAction('copy')} />
+
+
+ } onClick={() => handleAction('delete')} />
+
+
+
+ } onClick={handleClose} />
+
+
+
+ )
+}
+
+const Container = styled.div`
+ width: 100%;
+ padding: 36px 20px;
+ background-color: var(--color-background);
+ border-top: 1px solid var(--color-border);
+`
+
+const ActionBar = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+`
+
+const ActionButtons = styled.div`
+ display: flex;
+ gap: 16px;
+`
+
+const ActionButton = styled(Button)`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 8px 16px;
+ border-radius: 50%;
+ .anticon {
+ font-size: 16px;
+ }
+ &:hover {
+ background-color: var(--color-background-mute);
+ }
+ &:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+`
+
+const SelectionCount = styled.div`
+ margin-right: 15px;
+ color: var(--color-text-2);
+ font-size: 14px;
+`
+
+export default MultiSelectActionPopup
diff --git a/src/renderer/src/components/app/Navbar.tsx b/src/renderer/src/components/app/Navbar.tsx
index 85a5265560..e5fe83b98c 100644
--- a/src/renderer/src/components/app/Navbar.tsx
+++ b/src/renderer/src/components/app/Navbar.tsx
@@ -1,6 +1,6 @@
import { isLinux, isMac, isWindows } from '@renderer/config/constant'
-import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor'
import { useFullscreen } from '@renderer/hooks/useFullscreen'
+import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor'
import type { FC, PropsWithChildren } from 'react'
import type { HTMLAttributes } from 'react'
import styled from 'styled-components'
diff --git a/src/renderer/src/config/minapps.ts b/src/renderer/src/config/minapps.ts
index 4dc80ad751..6990e6d157 100644
--- a/src/renderer/src/config/minapps.ts
+++ b/src/renderer/src/config/minapps.ts
@@ -18,6 +18,7 @@ import FlowithAppLogo from '@renderer/assets/images/apps/flowith.svg?url'
import GeminiAppLogo from '@renderer/assets/images/apps/gemini.png?url'
import GensparkLogo from '@renderer/assets/images/apps/genspark.jpg?url'
import GithubCopilotLogo from '@renderer/assets/images/apps/github-copilot.webp?url'
+import GoogleAppLogo from '@renderer/assets/images/apps/google.svg?url'
import GrokAppLogo from '@renderer/assets/images/apps/grok.png?url'
import GrokXAppLogo from '@renderer/assets/images/apps/grok-x.png?url'
import HikaLogo from '@renderer/assets/images/apps/hika.webp?url'
@@ -164,7 +165,8 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [
id: 'minimax',
name: '海螺',
url: 'https://chat.minimaxi.com/',
- logo: HailuoModelLogo
+ logo: HailuoModelLogo,
+ bodered: true
},
{
id: 'groq',
@@ -178,6 +180,16 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [
url: 'https://claude.ai/',
logo: ClaudeAppLogo
},
+ {
+ id: 'google',
+ name: 'Google',
+ url: 'https://google.com/',
+ logo: GoogleAppLogo,
+ bodered: true,
+ style: {
+ padding: 5
+ }
+ },
{
id: 'baidu-ai-chat',
name: '文心一言',
diff --git a/src/renderer/src/config/models.ts b/src/renderer/src/config/models.ts
index 3e3656aacf..8cb7bf8209 100644
--- a/src/renderer/src/config/models.ts
+++ b/src/renderer/src/config/models.ts
@@ -146,6 +146,8 @@ const visionAllowedModels = [
'gemini-2\\.5',
'gemini-exp',
'claude-3',
+ 'claude-sonnet-4',
+ 'claude-opus-4',
'vision',
'glm-4v',
'qwen-vl',
@@ -232,7 +234,7 @@ export const FUNCTION_CALLING_REGEX = new RegExp(
)
export const CLAUDE_SUPPORTED_WEBSEARCH_REGEX = new RegExp(
- `\\b(?:claude-3(-|\\.)(7|5)-sonnet(?:-[\\w-]+)|claude-3(-|\\.)5-haiku(?:-[\\w-]+))\\b`,
+ `\\b(?:claude-3(-|\\.)(7|5)-sonnet(?:-[\\w-]+)|claude-3(-|\\.)5-haiku(?:-[\\w-]+)|claude-sonnet-4(?:-[\\w-]+)?|claude-opus-4(?:-[\\w-]+)?)\\b`,
'i'
)
@@ -419,6 +421,30 @@ export const SYSTEM_MODELS: Record = {
group: 'Qwen'
}
],
+
+ burncloud: [
+ { id: 'claude-3-7-sonnet-20250219-thinking', provider: 'burncloud', name: 'Claude 3.7 thinking', group: 'Claude' },
+ { id: 'claude-3-7-sonnet-20250219', provider: 'burncloud', name: 'Claude 3.7 Sonnet', group: 'Claude 3.7' },
+ { id: 'claude-3-5-sonnet-20241022', provider: 'burncloud', name: 'Claude 3.5 Sonnet', group: 'Claude 3.5' },
+ { id: 'claude-3-5-haiku-20241022', provider: 'burncloud', name: 'Claude 3.5 Haiku', group: 'Claude 3.5' },
+
+ { id: 'gpt-4.5-preview', provider: 'burncloud', name: 'gpt-4.5-preview', group: 'gpt-4.5' },
+ { id: 'gpt-4o', provider: 'burncloud', name: 'GPT-4o', group: 'GPT 4o' },
+ { id: 'gpt-4o-mini', provider: 'burncloud', name: 'GPT-4o-mini', group: 'GPT 4o' },
+ { id: 'o3', provider: 'burncloud', name: 'GPT-o1-mini', group: 'o1' },
+ { id: 'o3-mini', provider: 'burncloud', name: 'GPT-o1-preview', group: 'o1' },
+ { id: 'o1-mini', provider: 'burncloud', name: 'GPT-o1-mini', group: 'o1' },
+
+ { id: 'gemini-2.5-pro-preview-03-25', provider: 'burncloud', name: 'Gemini 2.5 Preview', group: 'Geminit 2.5' },
+ { id: 'gemini-2.5-pro-exp-03-25', provider: 'burncloud', name: 'Gemini 2.5 Pro Exp', group: 'Geminit 2.5' },
+ { id: 'gemini-2.0-flash-lite', provider: 'burncloud', name: 'Gemini 2.0 Flash Lite', group: 'Geminit 2.0' },
+ { id: 'gemini-2.0-flash-exp', provider: 'burncloud', name: 'Gemini 2.0 Flash Exp', group: 'Geminit 2.0' },
+ { id: 'gemini-2.0-flash', provider: 'burncloud', name: 'Gemini 2.0 Flash', group: 'Geminit 2.0' },
+
+ { id: 'deepseek-r1', name: 'DeepSeek-R1', provider: 'burncloud', group: 'deepseek-ai' },
+ { id: 'deepseek-v3', name: 'DeepSeek-V3', provider: 'burncloud', group: 'deepseek-ai' }
+ ],
+
o3: [
{
id: 'gpt-4o',
@@ -686,6 +712,18 @@ export const SYSTEM_MODELS: Record = {
}
],
anthropic: [
+ {
+ id: 'claude-sonnet-4-20250514',
+ provider: 'anthropic',
+ name: 'Claude Sonnet 4',
+ group: 'Claude 4'
+ },
+ {
+ id: 'claude-opus-4-20250514',
+ provider: 'anthropic',
+ name: 'Claude Opus 4',
+ group: 'Claude 4'
+ },
{
id: 'claude-3-7-sonnet-20250219',
provider: 'anthropic',
@@ -2412,7 +2450,12 @@ export function isClaudeReasoningModel(model?: Model): boolean {
if (!model) {
return false
}
- return model.id.includes('claude-3-7-sonnet') || model.id.includes('claude-3.7-sonnet')
+ return (
+ model.id.includes('claude-3-7-sonnet') ||
+ model.id.includes('claude-3.7-sonnet') ||
+ model.id.includes('claude-sonnet-4') ||
+ model.id.includes('claude-opus-4')
+ )
}
export const isSupportedThinkingTokenClaudeModel = isClaudeReasoningModel
@@ -2546,6 +2589,10 @@ export function isWebSearchModel(model: Model): boolean {
return true
}
+ if (provider.id === 'grok') {
+ return true
+ }
+
return false
}
@@ -2576,6 +2623,16 @@ export function getOpenAIWebSearchParams(assistant: Assistant, model: Model): Re
if (assistant.enableWebSearch) {
const webSearchTools = getWebSearchTools(model)
+ if (model.provider === 'grok') {
+ return {
+ search_parameters: {
+ mode: 'auto',
+ return_citations: true,
+ sources: [{ type: 'web' }, { type: 'x' }, { type: 'news' }]
+ }
+ }
+ }
+
if (model.provider === 'hunyuan') {
return { enable_enhancement: true, citation: true, search_info: true }
}
@@ -2678,7 +2735,8 @@ export const THINKING_TOKEN_MAP: Record =
'qwen3-.*$': { min: 1024, max: 38912 },
// Claude models
- 'claude-3[.-]7.*sonnet.*$': { min: 1024, max: 64000 }
+ 'claude-3[.-]7.*sonnet.*$': { min: 1024, max: 64000 },
+ 'claude-(:?sonnet|opus)-4.*$': { min: 1024, max: 64000 }
}
export const findTokenLimit = (modelId: string): { min: number; max: number } | undefined => {
diff --git a/src/renderer/src/config/providers.ts b/src/renderer/src/config/providers.ts
index 6ffbb4a550..e995e20df0 100644
--- a/src/renderer/src/config/providers.ts
+++ b/src/renderer/src/config/providers.ts
@@ -7,6 +7,7 @@ import AnthropicProviderLogo from '@renderer/assets/images/providers/anthropic.p
import BaichuanProviderLogo from '@renderer/assets/images/providers/baichuan.png'
import BaiduCloudProviderLogo from '@renderer/assets/images/providers/baidu-cloud.svg'
import BailianProviderLogo from '@renderer/assets/images/providers/bailian.png'
+import BurnCloudProviderLogo from '@renderer/assets/images/providers/burncloud.png'
import DeepSeekProviderLogo from '@renderer/assets/images/providers/deepseek.png'
import DmxapiProviderLogo from '@renderer/assets/images/providers/DMXAPI.png'
import FireworksProviderLogo from '@renderer/assets/images/providers/fireworks.png'
@@ -61,6 +62,7 @@ const PROVIDER_LOGO_MAP = {
xirang: XirangProviderLogo,
anthropic: AnthropicProviderLogo,
aihubmix: AiHubMixProviderLogo,
+ burncloud: BurnCloudProviderLogo,
gemini: GoogleProviderLogo,
stepfun: StepProviderLogo,
doubao: BytedanceProviderLogo,
@@ -121,6 +123,17 @@ export const PROVIDER_CONFIG = {
models: 'https://docs.o3.fan/models'
}
},
+ burncloud: {
+ api: {
+ url: 'https://ai.burncloud.com'
+ },
+ websites: {
+ official: 'https://ai.burncloud.com/',
+ apiKey: 'https://ai.burncloud.com/token',
+ docs: 'https://ai.burncloud.com/docs',
+ models: 'https://ai.burncloud.com/pricing'
+ }
+ },
ppio: {
api: {
url: 'https://api.ppinfra.com/v3/openai'
diff --git a/src/renderer/src/context/CodeStyleProvider.tsx b/src/renderer/src/context/CodeStyleProvider.tsx
index 23e50f8deb..06dc345f7a 100644
--- a/src/renderer/src/context/CodeStyleProvider.tsx
+++ b/src/renderer/src/context/CodeStyleProvider.tsx
@@ -3,6 +3,7 @@ import { useMermaid } from '@renderer/hooks/useMermaid'
import { useSettings } from '@renderer/hooks/useSettings'
import { HighlightChunkResult, ShikiPreProperties, shikiStreamService } from '@renderer/services/ShikiStreamService'
import { ThemeMode } from '@renderer/types'
+import { getHighlighter, getMarkdownIt, getShiki, loadLanguageIfNeeded, loadThemeIfNeeded } from '@renderer/utils/shiki'
import * as cmThemes from '@uiw/codemirror-themes-all'
import type React from 'react'
import { createContext, type PropsWithChildren, use, useCallback, useEffect, useMemo, useState } from 'react'
@@ -11,6 +12,8 @@ interface CodeStyleContextType {
highlightCodeChunk: (trunk: string, language: string, callerId: string) => Promise
cleanupTokenizers: (callerId: string) => void
getShikiPreProperties: (language: string) => Promise
+ highlightCode: (code: string, language: string) => Promise
+ shikiMarkdownIt: (code: string) => Promise
themeNames: string[]
activeShikiTheme: string
activeCmTheme: any
@@ -21,6 +24,8 @@ const defaultCodeStyleContext: CodeStyleContextType = {
highlightCodeChunk: async () => ({ lines: [], recall: 0 }),
cleanupTokenizers: () => {},
getShikiPreProperties: async () => ({ class: '', style: '', tabindex: 0 }),
+ highlightCode: async () => '',
+ shikiMarkdownIt: async () => '',
themeNames: ['auto'],
activeShikiTheme: 'auto',
activeCmTheme: null,
@@ -37,7 +42,7 @@ export const CodeStyleProvider: React.FC = ({ children }) =>
useEffect(() => {
if (!codeEditor.enabled) {
- import('shiki').then(({ bundledThemes }) => {
+ getShiki().then(({ bundledThemes }) => {
setShikiThemes(bundledThemes)
})
}
@@ -118,11 +123,35 @@ export const CodeStyleProvider: React.FC = ({ children }) =>
[activeShikiTheme, languageMap]
)
+ const highlightCode = useCallback(
+ async (code: string, language: string) => {
+ const highlighter = await getHighlighter()
+ await loadLanguageIfNeeded(highlighter, language)
+ await loadThemeIfNeeded(highlighter, activeShikiTheme)
+ return highlighter.codeToHtml(code, { lang: language, theme: activeShikiTheme })
+ },
+ [activeShikiTheme]
+ )
+
+ // 使用 Shiki 和 Markdown-it 渲染代码
+ const shikiMarkdownIt = useCallback(
+ async (code: string) => {
+ const renderer = await getMarkdownIt(activeShikiTheme, code)
+ if (!renderer) {
+ return code
+ }
+ return renderer.render(code)
+ },
+ [activeShikiTheme]
+ )
+
const contextValue = useMemo(
() => ({
highlightCodeChunk,
cleanupTokenizers,
getShikiPreProperties,
+ highlightCode,
+ shikiMarkdownIt,
themeNames,
activeShikiTheme,
activeCmTheme,
@@ -132,6 +161,8 @@ export const CodeStyleProvider: React.FC = ({ children }) =>
highlightCodeChunk,
cleanupTokenizers,
getShikiPreProperties,
+ highlightCode,
+ shikiMarkdownIt,
themeNames,
activeShikiTheme,
activeCmTheme,
diff --git a/src/renderer/src/context/NotificationProvider.tsx b/src/renderer/src/context/NotificationProvider.tsx
new file mode 100644
index 0000000000..c6de09b722
--- /dev/null
+++ b/src/renderer/src/context/NotificationProvider.tsx
@@ -0,0 +1,76 @@
+import { NotificationQueue } from '@renderer/queue/NotificationQueue'
+import { Notification } from '@renderer/types/notification'
+import { isFocused } from '@renderer/utils/window'
+import { notification } from 'antd'
+import React, { createContext, use, useEffect, useMemo } from 'react'
+
+type NotificationContextType = {
+ open: typeof notification.open
+ destroy: typeof notification.destroy
+}
+
+const typeMap: Record = {
+ error: 'error',
+ success: 'success',
+ warning: 'warning',
+ info: 'info',
+ progress: 'info',
+ action: 'info'
+}
+
+const NotificationContext = createContext(undefined)
+
+export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+ const [api, contextHolder] = notification.useNotification({
+ stack: {
+ threshold: 3
+ },
+ showProgress: true
+ })
+
+ useEffect(() => {
+ const queue = NotificationQueue.getInstance()
+ const listener = async (notification: Notification) => {
+ // 判断是否需要系统通知
+ if (notification.channel === 'system' || !isFocused()) {
+ window.api.notification.send(notification)
+ return
+ }
+ return new Promise((resolve) => {
+ api.open({
+ message: notification.title,
+ description:
+ notification.message.length > 50 ? notification.message.slice(0, 47) + '...' : notification.message,
+ duration: 3,
+ placement: 'topRight',
+ type: typeMap[notification.type] || 'info',
+ key: notification.id,
+ onClose: resolve
+ })
+ })
+ }
+ queue.subscribe(listener)
+ return () => queue.unsubscribe(listener)
+ }, [api])
+
+ const value = useMemo(
+ () => ({
+ open: api.open,
+ destroy: api.destroy
+ }),
+ [api]
+ )
+
+ return (
+
+ {contextHolder}
+ {children}
+
+ )
+}
+
+export const useNotification = () => {
+ const ctx = use(NotificationContext)
+ if (!ctx) throw new Error('useNotification must be used within a NotificationProvider')
+ return ctx
+}
diff --git a/src/renderer/src/hooks/useChatContext.ts b/src/renderer/src/hooks/useChatContext.ts
new file mode 100644
index 0000000000..ae67a85413
--- /dev/null
+++ b/src/renderer/src/hooks/useChatContext.ts
@@ -0,0 +1,185 @@
+import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
+import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
+import { RootState } from '@renderer/store'
+import { messageBlocksSelectors } from '@renderer/store/messageBlock'
+import { selectMessagesForTopic } from '@renderer/store/newMessage'
+import { setActiveTopic, setSelectedMessageIds, toggleMultiSelectMode } from '@renderer/store/runtime'
+import { Topic } from '@renderer/types'
+import { useCallback, useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { useDispatch, useSelector, useStore } from 'react-redux'
+
+export const useChatContext = (activeTopic: Topic) => {
+ const { t } = useTranslation()
+ const dispatch = useDispatch()
+ const store = useStore()
+ const { deleteMessage } = useMessageOperations(activeTopic)
+
+ const [messageRefs, setMessageRefs] = useState
-
+
= (props) => {
/>
{enableMaxTokens && (
-
+
= (props) => {
)}
- {isOpenAI && (
-
- )}
+ {isOpenAI && (
+
+ )}
-
{t('settings.messages.prompt')}
dispatch(setShowPrompt(checked))} />
@@ -440,7 +447,6 @@ const SettingsTab: FC = (props) => {
-
{t('message.message.code_style')}
= (props) => {
-
{t('settings.messages.input.show_estimated_tokens')}
`
+const SettingGroup = styled.div<{ theme?: ThemeMode }>`
padding: 0 5px;
width: 100%;
margin-top: 0;
diff --git a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx
index d2fed14e03..2df5fb5db8 100644
--- a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx
+++ b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx
@@ -396,9 +396,46 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic
onClick={() => onSwitchTopic(topic)}
style={{ borderRadius }}>
{isPending(topic.id) && !isActive && }
-
- {topicName}
-
+
+
+ {topicName}
+
+ {isActive && !topic.pinned && (
+
+
+ {t('chat.topics.delete.shortcut', { key: isMac ? '⌘' : 'Ctrl' })}
+
+
+ }>
+