mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 14:41:24 +08:00
Merge remote-tracking branch 'origin/main' into feat/agents-new
This commit is contained in:
commit
45961d2eda
36
.yarn/patches/@ai-sdk-google-npm-2.0.14-376d8b03cc.patch
vendored
Normal file
36
.yarn/patches/@ai-sdk-google-npm-2.0.14-376d8b03cc.patch
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
diff --git a/dist/index.mjs b/dist/index.mjs
|
||||||
|
index 110f37ec18c98b1d55ae2b73cc716194e6f9094d..91d0f336b318833c6cee9599fe91370c0ff75323 100644
|
||||||
|
--- a/dist/index.mjs
|
||||||
|
+++ b/dist/index.mjs
|
||||||
|
@@ -447,7 +447,10 @@ function convertToGoogleGenerativeAIMessages(prompt, options) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/get-model-path.ts
|
||||||
|
-function getModelPath(modelId) {
|
||||||
|
+function getModelPath(modelId, baseURL) {
|
||||||
|
+ if (baseURL?.includes('cherryin')) {
|
||||||
|
+ return `models/${modelId}`;
|
||||||
|
+ }
|
||||||
|
return modelId.includes("/") ? modelId : `models/${modelId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -856,7 +859,8 @@ var GoogleGenerativeAILanguageModel = class {
|
||||||
|
rawValue: rawResponse
|
||||||
|
} = await postJsonToApi2({
|
||||||
|
url: `${this.config.baseURL}/${getModelPath(
|
||||||
|
- this.modelId
|
||||||
|
+ this.modelId,
|
||||||
|
+ this.config.baseURL
|
||||||
|
)}:generateContent`,
|
||||||
|
headers: mergedHeaders,
|
||||||
|
body: args,
|
||||||
|
@@ -962,7 +966,8 @@ var GoogleGenerativeAILanguageModel = class {
|
||||||
|
);
|
||||||
|
const { responseHeaders, value: response } = await postJsonToApi2({
|
||||||
|
url: `${this.config.baseURL}/${getModelPath(
|
||||||
|
- this.modelId
|
||||||
|
+ this.modelId,
|
||||||
|
+ this.config.baseURL
|
||||||
|
)}:streamGenerateContent?alt=sse`,
|
||||||
|
headers,
|
||||||
|
body: args,
|
||||||
@ -125,6 +125,7 @@ afterSign: scripts/notarize.js
|
|||||||
artifactBuildCompleted: scripts/artifact-build-completed.js
|
artifactBuildCompleted: scripts/artifact-build-completed.js
|
||||||
releaseInfo:
|
releaseInfo:
|
||||||
releaseNotes: |
|
releaseNotes: |
|
||||||
|
<!--LANG:en-->
|
||||||
🚀 New Features:
|
🚀 New Features:
|
||||||
- Refactored AI core engine for more efficient and stable content generation
|
- Refactored AI core engine for more efficient and stable content generation
|
||||||
- Added support for multiple AI model providers: CherryIN, AiOnly
|
- Added support for multiple AI model providers: CherryIN, AiOnly
|
||||||
@ -151,4 +152,32 @@ releaseInfo:
|
|||||||
- Improved scrollbar component with horizontal scrolling support
|
- Improved scrollbar component with horizontal scrolling support
|
||||||
- Fixed multiple translation issues: paste handling, file processing, state management
|
- Fixed multiple translation issues: paste handling, file processing, state management
|
||||||
- Various UI optimizations and bug fixes
|
- Various UI optimizations and bug fixes
|
||||||
|
<!--LANG:zh-CN-->
|
||||||
|
🚀 新功能:
|
||||||
|
- 重构 AI 核心引擎,提供更高效稳定的内容生成
|
||||||
|
- 新增多个 AI 模型提供商支持:CherryIN、AiOnly
|
||||||
|
- 新增 API 服务器功能,支持外部应用集成
|
||||||
|
- 新增 PaddleOCR 文档识别,增强文档处理能力
|
||||||
|
- 新增 Anthropic OAuth 认证支持
|
||||||
|
- 新增数据存储空间限制提醒
|
||||||
|
- 新增字体设置,支持全局字体和代码字体自定义
|
||||||
|
- 新增翻译完成后自动复制功能
|
||||||
|
- 新增键盘快捷键:重命名主题、编辑最后一条消息等
|
||||||
|
- 新增文本附件预览,可查看消息中的文件内容
|
||||||
|
- 新增自定义窗口控制按钮(最小化、最大化、关闭)
|
||||||
|
- 支持通义千问长文本(qwen-long)和文档分析(qwen-doc)模型,原生文件上传
|
||||||
|
- 支持通义千问图像识别模型(Qwen-Image)
|
||||||
|
- 新增 iFlow CLI 支持
|
||||||
|
- 知识库和网页搜索转换为工具调用方式,提升灵活性
|
||||||
|
|
||||||
|
🎨 界面改进与问题修复:
|
||||||
|
- 集成 HeroUI 和 Tailwind CSS 框架
|
||||||
|
- 优化消息通知样式,统一 toast 组件
|
||||||
|
- 免费模型移至底部固定位置,便于访问
|
||||||
|
- 重构快捷面板和输入栏工具,操作更流畅
|
||||||
|
- 优化导航栏和侧边栏响应式设计
|
||||||
|
- 改进滚动条组件,支持水平滚动
|
||||||
|
- 修复多个翻译问题:粘贴处理、文件处理、状态管理
|
||||||
|
- 各种界面优化和问题修复
|
||||||
|
<!--LANG:END-->
|
||||||
|
|
||||||
|
|||||||
@ -380,7 +380,8 @@
|
|||||||
"pkce-challenge@npm:^4.1.0": "patch:pkce-challenge@npm%3A4.1.0#~/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch",
|
"pkce-challenge@npm:^4.1.0": "patch:pkce-challenge@npm%3A4.1.0#~/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch",
|
||||||
"undici": "6.21.2",
|
"undici": "6.21.2",
|
||||||
"vite": "npm:rolldown-vite@latest",
|
"vite": "npm:rolldown-vite@latest",
|
||||||
"tesseract.js@npm:*": "patch:tesseract.js@npm%3A6.0.1#~/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch"
|
"tesseract.js@npm:*": "patch:tesseract.js@npm%3A6.0.1#~/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch",
|
||||||
|
"@ai-sdk/google@npm:2.0.14": "patch:@ai-sdk/google@npm%3A2.0.14#~/.yarn/patches/@ai-sdk-google-npm-2.0.14-376d8b03cc.patch"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.9.1",
|
"packageManager": "yarn@4.9.1",
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
|
|||||||
@ -39,7 +39,7 @@
|
|||||||
"@ai-sdk/anthropic": "^2.0.17",
|
"@ai-sdk/anthropic": "^2.0.17",
|
||||||
"@ai-sdk/azure": "^2.0.30",
|
"@ai-sdk/azure": "^2.0.30",
|
||||||
"@ai-sdk/deepseek": "^1.0.17",
|
"@ai-sdk/deepseek": "^1.0.17",
|
||||||
"@ai-sdk/google": "^2.0.14",
|
"@ai-sdk/google": "patch:@ai-sdk/google@npm%3A2.0.14#~/.yarn/patches/@ai-sdk-google-npm-2.0.14-376d8b03cc.patch",
|
||||||
"@ai-sdk/openai": "^2.0.30",
|
"@ai-sdk/openai": "^2.0.30",
|
||||||
"@ai-sdk/openai-compatible": "^1.0.17",
|
"@ai-sdk/openai-compatible": "^1.0.17",
|
||||||
"@ai-sdk/provider": "^2.0.0",
|
"@ai-sdk/provider": "^2.0.0",
|
||||||
|
|||||||
@ -368,16 +368,27 @@ export const WINDOWS_TERMINALS_WITH_COMMANDS: TerminalConfigWithCommand[] = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// Helper function to escape strings for AppleScript
|
||||||
|
const escapeForAppleScript = (str: string): string => {
|
||||||
|
// In AppleScript strings, backslashes and double quotes need to be escaped
|
||||||
|
// When passed through osascript -e with single quotes, we need:
|
||||||
|
// 1. Backslash: \ -> \\
|
||||||
|
// 2. Double quote: " -> \"
|
||||||
|
return str
|
||||||
|
.replace(/\\/g, '\\\\') // Escape backslashes first
|
||||||
|
.replace(/"/g, '\\"') // Then escape double quotes
|
||||||
|
}
|
||||||
|
|
||||||
export const MACOS_TERMINALS_WITH_COMMANDS: TerminalConfigWithCommand[] = [
|
export const MACOS_TERMINALS_WITH_COMMANDS: TerminalConfigWithCommand[] = [
|
||||||
{
|
{
|
||||||
id: terminalApps.systemDefault,
|
id: terminalApps.systemDefault,
|
||||||
name: 'Terminal',
|
name: 'Terminal',
|
||||||
bundleId: 'com.apple.Terminal',
|
bundleId: 'com.apple.Terminal',
|
||||||
command: (directory: string, fullCommand: string) => ({
|
command: (_directory: string, fullCommand: string) => ({
|
||||||
command: 'sh',
|
command: 'sh',
|
||||||
args: [
|
args: [
|
||||||
'-c',
|
'-c',
|
||||||
`open -na Terminal && sleep 0.5 && osascript -e 'tell application "Terminal" to activate' -e 'tell application "Terminal" to do script "cd '${directory.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}' && clear && ${fullCommand.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}" in front window'`
|
`open -na Terminal && sleep 0.5 && osascript -e 'tell application "Terminal" to activate' -e 'tell application "Terminal" to do script "${escapeForAppleScript(fullCommand)}" in front window'`
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -385,11 +396,11 @@ export const MACOS_TERMINALS_WITH_COMMANDS: TerminalConfigWithCommand[] = [
|
|||||||
id: terminalApps.iterm2,
|
id: terminalApps.iterm2,
|
||||||
name: 'iTerm2',
|
name: 'iTerm2',
|
||||||
bundleId: 'com.googlecode.iterm2',
|
bundleId: 'com.googlecode.iterm2',
|
||||||
command: (directory: string, fullCommand: string) => ({
|
command: (_directory: string, fullCommand: string) => ({
|
||||||
command: 'sh',
|
command: 'sh',
|
||||||
args: [
|
args: [
|
||||||
'-c',
|
'-c',
|
||||||
`open -na iTerm && sleep 0.8 && osascript -e 'on waitUntilRunning()\n repeat 50 times\n tell application "System Events"\n if (exists process "iTerm2") then exit repeat\n end tell\n delay 0.1\n end repeat\nend waitUntilRunning\n\nwaitUntilRunning()\n\ntell application "iTerm2"\n if (count of windows) = 0 then\n create window with default profile\n delay 0.3\n else\n tell current window\n create tab with default profile\n end tell\n delay 0.3\n end if\n tell current session of current window to write text "cd '${directory.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}' && clear && ${fullCommand.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"\n activate\nend tell'`
|
`open -na iTerm && sleep 0.8 && osascript -e 'on waitUntilRunning()\n repeat 50 times\n tell application "System Events"\n if (exists process "iTerm2") then exit repeat\n end tell\n delay 0.1\n end repeat\nend waitUntilRunning\n\nwaitUntilRunning()\n\ntell application "iTerm2"\n if (count of windows) = 0 then\n create window with default profile\n delay 0.3\n else\n tell current window\n create tab with default profile\n end tell\n delay 0.3\n end if\n tell current session of current window to write text "${escapeForAppleScript(fullCommand)}"\n activate\nend tell'`
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -397,11 +408,11 @@ export const MACOS_TERMINALS_WITH_COMMANDS: TerminalConfigWithCommand[] = [
|
|||||||
id: terminalApps.kitty,
|
id: terminalApps.kitty,
|
||||||
name: 'kitty',
|
name: 'kitty',
|
||||||
bundleId: 'net.kovidgoyal.kitty',
|
bundleId: 'net.kovidgoyal.kitty',
|
||||||
command: (directory: string, fullCommand: string) => ({
|
command: (_directory: string, fullCommand: string) => ({
|
||||||
command: 'sh',
|
command: 'sh',
|
||||||
args: [
|
args: [
|
||||||
'-c',
|
'-c',
|
||||||
`cd "${directory}" && open -na kitty --args --directory="${directory}" sh -c "${fullCommand.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}; exec \\$SHELL" && sleep 0.5 && osascript -e 'tell application "kitty" to activate'`
|
`cd "${_directory}" && open -na kitty --args --directory="${_directory}" sh -c "${fullCommand.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}; exec \\$SHELL" && sleep 0.5 && osascript -e 'tell application "kitty" to activate'`
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -409,11 +420,11 @@ export const MACOS_TERMINALS_WITH_COMMANDS: TerminalConfigWithCommand[] = [
|
|||||||
id: terminalApps.alacritty,
|
id: terminalApps.alacritty,
|
||||||
name: 'Alacritty',
|
name: 'Alacritty',
|
||||||
bundleId: 'org.alacritty',
|
bundleId: 'org.alacritty',
|
||||||
command: (directory: string, fullCommand: string) => ({
|
command: (_directory: string, fullCommand: string) => ({
|
||||||
command: 'sh',
|
command: 'sh',
|
||||||
args: [
|
args: [
|
||||||
'-c',
|
'-c',
|
||||||
`open -na Alacritty --args --working-directory "${directory}" -e sh -c "${fullCommand.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}; exec \\$SHELL" && sleep 0.5 && osascript -e 'tell application "Alacritty" to activate'`
|
`open -na Alacritty --args --working-directory "${_directory}" -e sh -c "${fullCommand.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}; exec \\$SHELL" && sleep 0.5 && osascript -e 'tell application "Alacritty" to activate'`
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -421,11 +432,11 @@ export const MACOS_TERMINALS_WITH_COMMANDS: TerminalConfigWithCommand[] = [
|
|||||||
id: terminalApps.wezterm,
|
id: terminalApps.wezterm,
|
||||||
name: 'WezTerm',
|
name: 'WezTerm',
|
||||||
bundleId: 'com.github.wez.wezterm',
|
bundleId: 'com.github.wez.wezterm',
|
||||||
command: (directory: string, fullCommand: string) => ({
|
command: (_directory: string, fullCommand: string) => ({
|
||||||
command: 'sh',
|
command: 'sh',
|
||||||
args: [
|
args: [
|
||||||
'-c',
|
'-c',
|
||||||
`open -na WezTerm --args start --new-tab --cwd "${directory}" -- sh -c "${fullCommand.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}; exec \\$SHELL" && sleep 0.5 && osascript -e 'tell application "WezTerm" to activate'`
|
`open -na WezTerm --args start --new-tab --cwd "${_directory}" -- sh -c "${fullCommand.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}; exec \\$SHELL" && sleep 0.5 && osascript -e 'tell application "WezTerm" to activate'`
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -433,11 +444,11 @@ export const MACOS_TERMINALS_WITH_COMMANDS: TerminalConfigWithCommand[] = [
|
|||||||
id: terminalApps.ghostty,
|
id: terminalApps.ghostty,
|
||||||
name: 'Ghostty',
|
name: 'Ghostty',
|
||||||
bundleId: 'com.mitchellh.ghostty',
|
bundleId: 'com.mitchellh.ghostty',
|
||||||
command: (directory: string, fullCommand: string) => ({
|
command: (_directory: string, fullCommand: string) => ({
|
||||||
command: 'sh',
|
command: 'sh',
|
||||||
args: [
|
args: [
|
||||||
'-c',
|
'-c',
|
||||||
`cd "${directory}" && open -na Ghostty --args --working-directory="${directory}" -e sh -c "${fullCommand.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}; exec \\$SHELL" && sleep 0.5 && osascript -e 'tell application "Ghostty" to activate'`
|
`cd "${_directory}" && open -na Ghostty --args --working-directory="${_directory}" -e sh -c "${fullCommand.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}; exec \\$SHELL" && sleep 0.5 && osascript -e 'tell application "Ghostty" to activate'`
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -445,7 +456,7 @@ export const MACOS_TERMINALS_WITH_COMMANDS: TerminalConfigWithCommand[] = [
|
|||||||
id: terminalApps.tabby,
|
id: terminalApps.tabby,
|
||||||
name: 'Tabby',
|
name: 'Tabby',
|
||||||
bundleId: 'org.tabby',
|
bundleId: 'org.tabby',
|
||||||
command: (directory: string, fullCommand: string) => ({
|
command: (_directory: string, fullCommand: string) => ({
|
||||||
command: 'sh',
|
command: 'sh',
|
||||||
args: [
|
args: [
|
||||||
'-c',
|
'-c',
|
||||||
@ -453,7 +464,7 @@ export const MACOS_TERMINALS_WITH_COMMANDS: TerminalConfigWithCommand[] = [
|
|||||||
open -na Tabby --args open && sleep 0.3
|
open -na Tabby --args open && sleep 0.3
|
||||||
else
|
else
|
||||||
open -na Tabby --args open && sleep 2
|
open -na Tabby --args open && sleep 2
|
||||||
fi && osascript -e 'tell application "Tabby" to activate' -e 'set the clipboard to "cd \\"${directory.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}\\" && clear && ${fullCommand.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"' -e 'tell application "System Events" to tell process "Tabby" to keystroke "v" using {command down}' -e 'tell application "System Events" to key code 36'`
|
fi && osascript -e 'tell application "Tabby" to activate' -e 'set the clipboard to "${escapeForAppleScript(fullCommand)}"' -e 'tell application "System Events" to tell process "Tabby" to keystroke "v" using {command down}' -e 'tell application "System Events" to key code 36'`
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,13 @@ import { windowService } from './WindowService'
|
|||||||
|
|
||||||
const logger = loggerService.withContext('AppUpdater')
|
const logger = loggerService.withContext('AppUpdater')
|
||||||
|
|
||||||
|
// Language markers constants for multi-language release notes
|
||||||
|
const LANG_MARKERS = {
|
||||||
|
EN_START: '<!--LANG:en-->',
|
||||||
|
ZH_CN_START: '<!--LANG:zh-CN-->',
|
||||||
|
END: '<!--LANG:END-->'
|
||||||
|
} as const
|
||||||
|
|
||||||
export default class AppUpdater {
|
export default class AppUpdater {
|
||||||
autoUpdater: _AppUpdater = autoUpdater
|
autoUpdater: _AppUpdater = autoUpdater
|
||||||
private releaseInfo: UpdateInfo | undefined
|
private releaseInfo: UpdateInfo | undefined
|
||||||
@ -41,7 +48,8 @@ export default class AppUpdater {
|
|||||||
|
|
||||||
autoUpdater.on('update-available', (releaseInfo: UpdateInfo) => {
|
autoUpdater.on('update-available', (releaseInfo: UpdateInfo) => {
|
||||||
logger.info('update available', releaseInfo)
|
logger.info('update available', releaseInfo)
|
||||||
windowService.getMainWindow()?.webContents.send(IpcChannel.UpdateAvailable, releaseInfo)
|
const processedReleaseInfo = this.processReleaseInfo(releaseInfo)
|
||||||
|
windowService.getMainWindow()?.webContents.send(IpcChannel.UpdateAvailable, processedReleaseInfo)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 检测到不需要更新时
|
// 检测到不需要更新时
|
||||||
@ -56,9 +64,10 @@ export default class AppUpdater {
|
|||||||
|
|
||||||
// 当需要更新的内容下载完成后
|
// 当需要更新的内容下载完成后
|
||||||
autoUpdater.on('update-downloaded', (releaseInfo: UpdateInfo) => {
|
autoUpdater.on('update-downloaded', (releaseInfo: UpdateInfo) => {
|
||||||
windowService.getMainWindow()?.webContents.send(IpcChannel.UpdateDownloaded, releaseInfo)
|
const processedReleaseInfo = this.processReleaseInfo(releaseInfo)
|
||||||
this.releaseInfo = releaseInfo
|
windowService.getMainWindow()?.webContents.send(IpcChannel.UpdateDownloaded, processedReleaseInfo)
|
||||||
logger.info('update downloaded', releaseInfo)
|
this.releaseInfo = processedReleaseInfo
|
||||||
|
logger.info('update downloaded', processedReleaseInfo)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (isWin) {
|
if (isWin) {
|
||||||
@ -271,16 +280,99 @@ export default class AppUpdater {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if release notes contain multi-language markers
|
||||||
|
*/
|
||||||
|
private hasMultiLanguageMarkers(releaseNotes: string): boolean {
|
||||||
|
return releaseNotes.includes(LANG_MARKERS.EN_START)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse multi-language release notes and return the appropriate language version
|
||||||
|
* @param releaseNotes - Release notes string with language markers
|
||||||
|
* @returns Parsed release notes for the user's language
|
||||||
|
*
|
||||||
|
* Expected format:
|
||||||
|
* <!--LANG:en-->English content<!--LANG:zh-CN-->Chinese content<!--LANG:END-->
|
||||||
|
*/
|
||||||
|
private parseMultiLangReleaseNotes(releaseNotes: string): string {
|
||||||
|
try {
|
||||||
|
const language = configManager.getLanguage()
|
||||||
|
const isChineseUser = language === 'zh-CN' || language === 'zh-TW'
|
||||||
|
|
||||||
|
// Create regex patterns using constants
|
||||||
|
const enPattern = new RegExp(
|
||||||
|
`${LANG_MARKERS.EN_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([\\s\\S]*?)${LANG_MARKERS.ZH_CN_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`
|
||||||
|
)
|
||||||
|
const zhPattern = new RegExp(
|
||||||
|
`${LANG_MARKERS.ZH_CN_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([\\s\\S]*?)${LANG_MARKERS.END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`
|
||||||
|
)
|
||||||
|
|
||||||
|
// Extract language sections
|
||||||
|
const enMatch = releaseNotes.match(enPattern)
|
||||||
|
const zhMatch = releaseNotes.match(zhPattern)
|
||||||
|
|
||||||
|
// Return appropriate language version with proper fallback
|
||||||
|
if (isChineseUser && zhMatch) {
|
||||||
|
return zhMatch[1].trim()
|
||||||
|
} else if (enMatch) {
|
||||||
|
return enMatch[1].trim()
|
||||||
|
} else {
|
||||||
|
// Clean fallback: remove all language markers
|
||||||
|
logger.warn('Failed to extract language-specific release notes, using cleaned fallback')
|
||||||
|
return releaseNotes
|
||||||
|
.replace(new RegExp(`${LANG_MARKERS.EN_START}|${LANG_MARKERS.ZH_CN_START}|${LANG_MARKERS.END}`, 'g'), '')
|
||||||
|
.trim()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to parse multi-language release notes', error as Error)
|
||||||
|
// Return original notes as safe fallback
|
||||||
|
return releaseNotes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process release info to handle multi-language release notes
|
||||||
|
* @param releaseInfo - Original release info from updater
|
||||||
|
* @returns Processed release info with localized release notes
|
||||||
|
*/
|
||||||
|
private processReleaseInfo(releaseInfo: UpdateInfo): UpdateInfo {
|
||||||
|
const processedInfo = { ...releaseInfo }
|
||||||
|
|
||||||
|
// Handle multi-language release notes in string format
|
||||||
|
if (releaseInfo.releaseNotes && typeof releaseInfo.releaseNotes === 'string') {
|
||||||
|
// Check if it contains multi-language markers
|
||||||
|
if (this.hasMultiLanguageMarkers(releaseInfo.releaseNotes)) {
|
||||||
|
processedInfo.releaseNotes = this.parseMultiLangReleaseNotes(releaseInfo.releaseNotes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return processedInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format release notes for display
|
||||||
|
* @param releaseNotes - Release notes in various formats
|
||||||
|
* @returns Formatted string for display
|
||||||
|
*/
|
||||||
private formatReleaseNotes(releaseNotes: string | ReleaseNoteInfo[] | null | undefined): string {
|
private formatReleaseNotes(releaseNotes: string | ReleaseNoteInfo[] | null | undefined): string {
|
||||||
if (!releaseNotes) {
|
if (!releaseNotes) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof releaseNotes === 'string') {
|
if (typeof releaseNotes === 'string') {
|
||||||
|
// Check if it contains multi-language markers
|
||||||
|
if (this.hasMultiLanguageMarkers(releaseNotes)) {
|
||||||
|
return this.parseMultiLangReleaseNotes(releaseNotes)
|
||||||
|
}
|
||||||
return releaseNotes
|
return releaseNotes
|
||||||
}
|
}
|
||||||
|
|
||||||
return releaseNotes.map((note) => note.note).join('\n')
|
if (Array.isArray(releaseNotes)) {
|
||||||
|
return releaseNotes.map((note) => note.note).join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
interface GithubReleaseInfo {
|
interface GithubReleaseInfo {
|
||||||
|
|||||||
@ -666,7 +666,7 @@ class CodeToolsService {
|
|||||||
const command = envPrefix ? `${envPrefix} && ${baseCommand}` : baseCommand
|
const command = envPrefix ? `${envPrefix} && ${baseCommand}` : baseCommand
|
||||||
|
|
||||||
// Combine directory change with the main command to ensure they execute in the same shell session
|
// Combine directory change with the main command to ensure they execute in the same shell session
|
||||||
const fullCommand = `cd '${directory.replace(/'/g, "\\'")}' && clear && ${command}`
|
const fullCommand = `cd "${directory.replace(/"/g, '\\"')}" && clear && ${command}`
|
||||||
|
|
||||||
const terminalConfig = await this.getTerminalConfig(options.terminal)
|
const terminalConfig = await this.getTerminalConfig(options.terminal)
|
||||||
logger.info(`Using terminal: ${terminalConfig.name} (${terminalConfig.id})`)
|
logger.info(`Using terminal: ${terminalConfig.name} (${terminalConfig.id})`)
|
||||||
|
|||||||
319
src/main/services/__tests__/AppUpdater.test.ts
Normal file
319
src/main/services/__tests__/AppUpdater.test.ts
Normal file
@ -0,0 +1,319 @@
|
|||||||
|
import { UpdateInfo } from 'builder-util-runtime'
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
vi.mock('@logger', () => ({
|
||||||
|
loggerService: {
|
||||||
|
withContext: () => ({
|
||||||
|
info: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
warn: vi.fn()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../ConfigManager', () => ({
|
||||||
|
configManager: {
|
||||||
|
getLanguage: vi.fn(),
|
||||||
|
getAutoUpdate: vi.fn(() => false),
|
||||||
|
getTestPlan: vi.fn(() => false),
|
||||||
|
getTestChannel: vi.fn(),
|
||||||
|
getClientId: vi.fn(() => 'test-client-id')
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../WindowService', () => ({
|
||||||
|
windowService: {
|
||||||
|
getMainWindow: vi.fn()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@main/constant', () => ({
|
||||||
|
isWin: false
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@main/utils/ipService', () => ({
|
||||||
|
getIpCountry: vi.fn(() => 'US')
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@main/utils/locales', () => ({
|
||||||
|
locales: {
|
||||||
|
en: { translation: { update: {} } },
|
||||||
|
'zh-CN': { translation: { update: {} } }
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@main/utils/systemInfo', () => ({
|
||||||
|
generateUserAgent: vi.fn(() => 'test-user-agent')
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('electron', () => ({
|
||||||
|
app: {
|
||||||
|
isPackaged: true,
|
||||||
|
getVersion: vi.fn(() => '1.0.0'),
|
||||||
|
getPath: vi.fn(() => '/test/path')
|
||||||
|
},
|
||||||
|
dialog: {
|
||||||
|
showMessageBox: vi.fn()
|
||||||
|
},
|
||||||
|
BrowserWindow: vi.fn(),
|
||||||
|
net: {
|
||||||
|
fetch: vi.fn()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('electron-updater', () => ({
|
||||||
|
autoUpdater: {
|
||||||
|
logger: null,
|
||||||
|
forceDevUpdateConfig: false,
|
||||||
|
autoDownload: false,
|
||||||
|
autoInstallOnAppQuit: false,
|
||||||
|
requestHeaders: {},
|
||||||
|
on: vi.fn(),
|
||||||
|
setFeedURL: vi.fn(),
|
||||||
|
checkForUpdates: vi.fn(),
|
||||||
|
downloadUpdate: vi.fn(),
|
||||||
|
quitAndInstall: vi.fn(),
|
||||||
|
channel: '',
|
||||||
|
allowDowngrade: false,
|
||||||
|
disableDifferentialDownload: false,
|
||||||
|
currentVersion: '1.0.0'
|
||||||
|
},
|
||||||
|
Logger: vi.fn(),
|
||||||
|
NsisUpdater: vi.fn(),
|
||||||
|
AppUpdater: vi.fn()
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Import after mocks
|
||||||
|
import AppUpdater from '../AppUpdater'
|
||||||
|
import { configManager } from '../ConfigManager'
|
||||||
|
|
||||||
|
describe('AppUpdater', () => {
|
||||||
|
let appUpdater: AppUpdater
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
appUpdater = new AppUpdater()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('parseMultiLangReleaseNotes', () => {
|
||||||
|
const sampleReleaseNotes = `<!--LANG:en-->
|
||||||
|
🚀 New Features:
|
||||||
|
- Feature A
|
||||||
|
- Feature B
|
||||||
|
|
||||||
|
🎨 UI Improvements:
|
||||||
|
- Improvement A
|
||||||
|
<!--LANG:zh-CN-->
|
||||||
|
🚀 新功能:
|
||||||
|
- 功能 A
|
||||||
|
- 功能 B
|
||||||
|
|
||||||
|
🎨 界面改进:
|
||||||
|
- 改进 A
|
||||||
|
<!--LANG:END-->`
|
||||||
|
|
||||||
|
it('should return Chinese notes for zh-CN users', () => {
|
||||||
|
vi.mocked(configManager.getLanguage).mockReturnValue('zh-CN')
|
||||||
|
|
||||||
|
const result = (appUpdater as any).parseMultiLangReleaseNotes(sampleReleaseNotes)
|
||||||
|
|
||||||
|
expect(result).toContain('新功能')
|
||||||
|
expect(result).toContain('功能 A')
|
||||||
|
expect(result).not.toContain('New Features')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return Chinese notes for zh-TW users', () => {
|
||||||
|
vi.mocked(configManager.getLanguage).mockReturnValue('zh-TW')
|
||||||
|
|
||||||
|
const result = (appUpdater as any).parseMultiLangReleaseNotes(sampleReleaseNotes)
|
||||||
|
|
||||||
|
expect(result).toContain('新功能')
|
||||||
|
expect(result).toContain('功能 A')
|
||||||
|
expect(result).not.toContain('New Features')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return English notes for non-Chinese users', () => {
|
||||||
|
vi.mocked(configManager.getLanguage).mockReturnValue('en-US')
|
||||||
|
|
||||||
|
const result = (appUpdater as any).parseMultiLangReleaseNotes(sampleReleaseNotes)
|
||||||
|
|
||||||
|
expect(result).toContain('New Features')
|
||||||
|
expect(result).toContain('Feature A')
|
||||||
|
expect(result).not.toContain('新功能')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return English notes for other language users', () => {
|
||||||
|
vi.mocked(configManager.getLanguage).mockReturnValue('ru-RU')
|
||||||
|
|
||||||
|
const result = (appUpdater as any).parseMultiLangReleaseNotes(sampleReleaseNotes)
|
||||||
|
|
||||||
|
expect(result).toContain('New Features')
|
||||||
|
expect(result).not.toContain('新功能')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle missing language sections gracefully', () => {
|
||||||
|
const malformedNotes = 'Simple release notes without markers'
|
||||||
|
|
||||||
|
const result = (appUpdater as any).parseMultiLangReleaseNotes(malformedNotes)
|
||||||
|
|
||||||
|
expect(result).toBe('Simple release notes without markers')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle malformed markers', () => {
|
||||||
|
const malformedNotes = `<!--LANG:en-->English only`
|
||||||
|
vi.mocked(configManager.getLanguage).mockReturnValue('zh-CN')
|
||||||
|
|
||||||
|
const result = (appUpdater as any).parseMultiLangReleaseNotes(malformedNotes)
|
||||||
|
|
||||||
|
// Should clean up markers and return cleaned content
|
||||||
|
expect(result).toContain('English only')
|
||||||
|
expect(result).not.toContain('<!--LANG:')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle empty release notes', () => {
|
||||||
|
const result = (appUpdater as any).parseMultiLangReleaseNotes('')
|
||||||
|
|
||||||
|
expect(result).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle errors gracefully', () => {
|
||||||
|
// Force an error by mocking configManager to throw
|
||||||
|
vi.mocked(configManager.getLanguage).mockImplementation(() => {
|
||||||
|
throw new Error('Test error')
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = (appUpdater as any).parseMultiLangReleaseNotes(sampleReleaseNotes)
|
||||||
|
|
||||||
|
// Should return original notes as fallback
|
||||||
|
expect(result).toBe(sampleReleaseNotes)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('hasMultiLanguageMarkers', () => {
|
||||||
|
it('should return true when markers are present', () => {
|
||||||
|
const notes = '<!--LANG:en-->Test'
|
||||||
|
|
||||||
|
const result = (appUpdater as any).hasMultiLanguageMarkers(notes)
|
||||||
|
|
||||||
|
expect(result).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false when no markers are present', () => {
|
||||||
|
const notes = 'Simple text without markers'
|
||||||
|
|
||||||
|
const result = (appUpdater as any).hasMultiLanguageMarkers(notes)
|
||||||
|
|
||||||
|
expect(result).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('processReleaseInfo', () => {
|
||||||
|
it('should process multi-language release notes in string format', () => {
|
||||||
|
vi.mocked(configManager.getLanguage).mockReturnValue('zh-CN')
|
||||||
|
|
||||||
|
const releaseInfo = {
|
||||||
|
version: '1.0.0',
|
||||||
|
files: [],
|
||||||
|
path: '',
|
||||||
|
sha512: '',
|
||||||
|
releaseDate: new Date().toISOString(),
|
||||||
|
releaseNotes: `<!--LANG:en-->English notes<!--LANG:zh-CN-->中文说明<!--LANG:END-->`
|
||||||
|
} as UpdateInfo
|
||||||
|
|
||||||
|
const result = (appUpdater as any).processReleaseInfo(releaseInfo)
|
||||||
|
|
||||||
|
expect(result.releaseNotes).toBe('中文说明')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not process release notes without markers', () => {
|
||||||
|
const releaseInfo = {
|
||||||
|
version: '1.0.0',
|
||||||
|
files: [],
|
||||||
|
path: '',
|
||||||
|
sha512: '',
|
||||||
|
releaseDate: new Date().toISOString(),
|
||||||
|
releaseNotes: 'Simple release notes'
|
||||||
|
} as UpdateInfo
|
||||||
|
|
||||||
|
const result = (appUpdater as any).processReleaseInfo(releaseInfo)
|
||||||
|
|
||||||
|
expect(result.releaseNotes).toBe('Simple release notes')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle array format release notes', () => {
|
||||||
|
const releaseInfo = {
|
||||||
|
version: '1.0.0',
|
||||||
|
files: [],
|
||||||
|
path: '',
|
||||||
|
sha512: '',
|
||||||
|
releaseDate: new Date().toISOString(),
|
||||||
|
releaseNotes: [
|
||||||
|
{ version: '1.0.0', note: 'Note 1' },
|
||||||
|
{ version: '1.0.1', note: 'Note 2' }
|
||||||
|
]
|
||||||
|
} as UpdateInfo
|
||||||
|
|
||||||
|
const result = (appUpdater as any).processReleaseInfo(releaseInfo)
|
||||||
|
|
||||||
|
expect(result.releaseNotes).toEqual(releaseInfo.releaseNotes)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle null release notes', () => {
|
||||||
|
const releaseInfo = {
|
||||||
|
version: '1.0.0',
|
||||||
|
files: [],
|
||||||
|
path: '',
|
||||||
|
sha512: '',
|
||||||
|
releaseDate: new Date().toISOString(),
|
||||||
|
releaseNotes: null
|
||||||
|
} as UpdateInfo
|
||||||
|
|
||||||
|
const result = (appUpdater as any).processReleaseInfo(releaseInfo)
|
||||||
|
|
||||||
|
expect(result.releaseNotes).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('formatReleaseNotes', () => {
|
||||||
|
it('should format string release notes with markers', () => {
|
||||||
|
vi.mocked(configManager.getLanguage).mockReturnValue('en-US')
|
||||||
|
const notes = `<!--LANG:en-->English<!--LANG:zh-CN-->中文<!--LANG:END-->`
|
||||||
|
|
||||||
|
const result = (appUpdater as any).formatReleaseNotes(notes)
|
||||||
|
|
||||||
|
expect(result).toBe('English')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should format string release notes without markers', () => {
|
||||||
|
const notes = 'Simple notes'
|
||||||
|
|
||||||
|
const result = (appUpdater as any).formatReleaseNotes(notes)
|
||||||
|
|
||||||
|
expect(result).toBe('Simple notes')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should format array release notes', () => {
|
||||||
|
const notes = [
|
||||||
|
{ version: '1.0.0', note: 'Note 1' },
|
||||||
|
{ version: '1.0.1', note: 'Note 2' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const result = (appUpdater as any).formatReleaseNotes(notes)
|
||||||
|
|
||||||
|
expect(result).toBe('Note 1\nNote 2')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle null release notes', () => {
|
||||||
|
const result = (appUpdater as any).formatReleaseNotes(null)
|
||||||
|
|
||||||
|
expect(result).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle undefined release notes', () => {
|
||||||
|
const result = (appUpdater as any).formatReleaseNotes(undefined)
|
||||||
|
|
||||||
|
expect(result).toBe('')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -185,8 +185,7 @@ export class AiSdkToChunkAdapter {
|
|||||||
case 'reasoning-end':
|
case 'reasoning-end':
|
||||||
this.onChunk({
|
this.onChunk({
|
||||||
type: ChunkType.THINKING_COMPLETE,
|
type: ChunkType.THINKING_COMPLETE,
|
||||||
text: (chunk.providerMetadata?.metadata?.thinking_content as string) || final.reasoningContent,
|
text: (chunk.providerMetadata?.metadata?.thinking_content as string) || final.reasoningContent
|
||||||
thinking_millsec: (chunk.providerMetadata?.metadata?.thinking_millsec as number) || 0
|
|
||||||
})
|
})
|
||||||
final.reasoningContent = ''
|
final.reasoningContent = ''
|
||||||
break
|
break
|
||||||
|
|||||||
@ -7,18 +7,14 @@ export default definePlugin({
|
|||||||
transformStream: () => () => {
|
transformStream: () => () => {
|
||||||
// === 时间跟踪状态 ===
|
// === 时间跟踪状态 ===
|
||||||
let thinkingStartTime = 0
|
let thinkingStartTime = 0
|
||||||
let hasStartedThinking = false
|
|
||||||
let accumulatedThinkingContent = ''
|
let accumulatedThinkingContent = ''
|
||||||
let reasoningBlockId = ''
|
|
||||||
|
|
||||||
return new TransformStream<TextStreamPart<ToolSet>, TextStreamPart<ToolSet>>({
|
return new TransformStream<TextStreamPart<ToolSet>, TextStreamPart<ToolSet>>({
|
||||||
transform(chunk: TextStreamPart<ToolSet>, controller: TransformStreamDefaultController<TextStreamPart<ToolSet>>) {
|
transform(chunk: TextStreamPart<ToolSet>, controller: TransformStreamDefaultController<TextStreamPart<ToolSet>>) {
|
||||||
// === 处理 reasoning 类型 ===
|
// === 处理 reasoning 类型 ===
|
||||||
if (chunk.type === 'reasoning-start') {
|
if (chunk.type === 'reasoning-start') {
|
||||||
controller.enqueue(chunk)
|
controller.enqueue(chunk)
|
||||||
hasStartedThinking = true
|
|
||||||
thinkingStartTime = performance.now()
|
thinkingStartTime = performance.now()
|
||||||
reasoningBlockId = chunk.id
|
|
||||||
} else if (chunk.type === 'reasoning-delta') {
|
} else if (chunk.type === 'reasoning-delta') {
|
||||||
accumulatedThinkingContent += chunk.text
|
accumulatedThinkingContent += chunk.text
|
||||||
controller.enqueue({
|
controller.enqueue({
|
||||||
@ -32,21 +28,6 @@ export default definePlugin({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else if (chunk.type === 'reasoning-end' && hasStartedThinking) {
|
|
||||||
controller.enqueue({
|
|
||||||
type: 'reasoning-end',
|
|
||||||
id: reasoningBlockId,
|
|
||||||
providerMetadata: {
|
|
||||||
metadata: {
|
|
||||||
thinking_millsec: performance.now() - thinkingStartTime,
|
|
||||||
thinking_content: accumulatedThinkingContent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
accumulatedThinkingContent = ''
|
|
||||||
hasStartedThinking = false
|
|
||||||
thinkingStartTime = 0
|
|
||||||
reasoningBlockId = ''
|
|
||||||
} else {
|
} else {
|
||||||
controller.enqueue(chunk)
|
controller.enqueue(chunk)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,7 +18,8 @@ export function renderSvgInShadowHost(svgContent: string, hostElement: HTMLEleme
|
|||||||
// Sanitize the SVG content
|
// Sanitize the SVG content
|
||||||
const sanitizedContent = DOMPurify.sanitize(svgContent, {
|
const sanitizedContent = DOMPurify.sanitize(svgContent, {
|
||||||
ADD_TAGS: ['animate', 'foreignObject', 'use'],
|
ADD_TAGS: ['animate', 'foreignObject', 'use'],
|
||||||
ADD_ATTR: ['from', 'to']
|
ADD_ATTR: ['from', 'to'],
|
||||||
|
HTML_INTEGRATION_POINTS: { foreignobject: true }
|
||||||
})
|
})
|
||||||
|
|
||||||
const shadowRoot = hostElement.shadowRoot || hostElement.attachShadow({ mode: 'open' })
|
const shadowRoot = hostElement.shadowRoot || hostElement.attachShadow({ mode: 'open' })
|
||||||
@ -36,6 +37,7 @@ export function renderSvgInShadowHost(svgContent: string, hostElement: HTMLEleme
|
|||||||
border-radius: var(--shadow-host-border-radius);
|
border-radius: var(--shadow-host-border-radius);
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
overflow: hidden; /* Prevent scrollbars, as scaling is now handled */
|
overflow: hidden; /* Prevent scrollbars, as scaling is now handled */
|
||||||
|
white-space: normal;
|
||||||
display: block;
|
display: block;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import { loggerService } from '@renderer/services/LoggerService'
|
|||||||
import { getModelUniqId } from '@renderer/services/ModelService'
|
import { getModelUniqId } from '@renderer/services/ModelService'
|
||||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||||
import { setIsBunInstalled } from '@renderer/store/mcp'
|
import { setIsBunInstalled } from '@renderer/store/mcp'
|
||||||
import { Model } from '@renderer/types'
|
import { EndpointType, Model } from '@renderer/types'
|
||||||
import { getClaudeSupportedProviders } from '@renderer/utils/provider'
|
import { getClaudeSupportedProviders } from '@renderer/utils/provider'
|
||||||
import { codeTools, terminalApps, TerminalConfig } from '@shared/config/constant'
|
import { codeTools, terminalApps, TerminalConfig } from '@shared/config/constant'
|
||||||
import { Alert, Avatar, Button, Checkbox, Input, Popover, Select, Space, Tooltip } from 'antd'
|
import { Alert, Avatar, Button, Checkbox, Input, Popover, Select, Space, Tooltip } from 'antd'
|
||||||
@ -70,18 +70,43 @@ const CodeToolsPage: FC = () => {
|
|||||||
if (isEmbeddingModel(m) || isRerankModel(m) || isTextToImageModel(m)) {
|
if (isEmbeddingModel(m) || isRerankModel(m) || isTextToImageModel(m)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m.provider === 'cherryai') {
|
if (m.provider === 'cherryai') {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedCliTool === codeTools.claudeCode) {
|
if (selectedCliTool === codeTools.claudeCode) {
|
||||||
|
if (m.supported_endpoint_types) {
|
||||||
|
return m.supported_endpoint_types.includes('anthropic')
|
||||||
|
}
|
||||||
return m.id.includes('claude') || CLAUDE_OFFICIAL_SUPPORTED_PROVIDERS.includes(m.provider)
|
return m.id.includes('claude') || CLAUDE_OFFICIAL_SUPPORTED_PROVIDERS.includes(m.provider)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedCliTool === codeTools.geminiCli) {
|
if (selectedCliTool === codeTools.geminiCli) {
|
||||||
|
if (m.supported_endpoint_types) {
|
||||||
|
return m.supported_endpoint_types.includes('gemini')
|
||||||
|
}
|
||||||
return m.id.includes('gemini')
|
return m.id.includes('gemini')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedCliTool === codeTools.openaiCodex) {
|
if (selectedCliTool === codeTools.openaiCodex) {
|
||||||
|
if (m.supported_endpoint_types) {
|
||||||
|
return ['openai', 'openai-response'].some((type) =>
|
||||||
|
m.supported_endpoint_types?.includes(type as EndpointType)
|
||||||
|
)
|
||||||
|
}
|
||||||
return m.id.includes('openai') || OPENAI_CODEX_SUPPORTED_PROVIDERS.includes(m.provider)
|
return m.id.includes('openai') || OPENAI_CODEX_SUPPORTED_PROVIDERS.includes(m.provider)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (selectedCliTool === codeTools.qwenCode || selectedCliTool === codeTools.iFlowCli) {
|
||||||
|
if (m.supported_endpoint_types) {
|
||||||
|
return ['openai', 'openai-response'].some((type) =>
|
||||||
|
m.supported_endpoint_types?.includes(type as EndpointType)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
[selectedCliTool]
|
[selectedCliTool]
|
||||||
|
|||||||
@ -23,10 +23,16 @@ export const CLI_TOOLS = [
|
|||||||
{ value: codeTools.iFlowCli, label: 'iFlow CLI' }
|
{ value: codeTools.iFlowCli, label: 'iFlow CLI' }
|
||||||
]
|
]
|
||||||
|
|
||||||
export const GEMINI_SUPPORTED_PROVIDERS = ['aihubmix', 'dmxapi', 'new-api']
|
export const GEMINI_SUPPORTED_PROVIDERS = ['aihubmix', 'dmxapi', 'new-api', 'cherryin']
|
||||||
export const CLAUDE_OFFICIAL_SUPPORTED_PROVIDERS = ['deepseek', 'moonshot', 'zhipu', 'dashscope', 'modelscope']
|
export const CLAUDE_OFFICIAL_SUPPORTED_PROVIDERS = ['deepseek', 'moonshot', 'zhipu', 'dashscope', 'modelscope']
|
||||||
export const CLAUDE_SUPPORTED_PROVIDERS = ['aihubmix', 'dmxapi', 'new-api', ...CLAUDE_OFFICIAL_SUPPORTED_PROVIDERS]
|
export const CLAUDE_SUPPORTED_PROVIDERS = [
|
||||||
export const OPENAI_CODEX_SUPPORTED_PROVIDERS = ['openai', 'openrouter', 'aihubmix', 'new-api']
|
'aihubmix',
|
||||||
|
'dmxapi',
|
||||||
|
'new-api',
|
||||||
|
'cherryin',
|
||||||
|
...CLAUDE_OFFICIAL_SUPPORTED_PROVIDERS
|
||||||
|
]
|
||||||
|
export const OPENAI_CODEX_SUPPORTED_PROVIDERS = ['openai', 'openrouter', 'aihubmix', 'new-api', 'cherryin']
|
||||||
|
|
||||||
// Provider 过滤映射
|
// Provider 过滤映射
|
||||||
export const CLI_TOOL_PROVIDER_MAP: Record<string, (providers: Provider[]) => Provider[]> = {
|
export const CLI_TOOL_PROVIDER_MAP: Record<string, (providers: Provider[]) => Provider[]> = {
|
||||||
|
|||||||
@ -273,7 +273,7 @@ const AboutSettings: FC = () => {
|
|||||||
<IndicatorLight color="green" />
|
<IndicatorLight color="green" />
|
||||||
</SettingRowTitle>
|
</SettingRowTitle>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
<UpdateNotesWrapper>
|
<UpdateNotesWrapper className="markdown">
|
||||||
<Markdown>
|
<Markdown>
|
||||||
{typeof update.info.releaseNotes === 'string'
|
{typeof update.info.releaseNotes === 'string'
|
||||||
? update.info.releaseNotes.replace(/\n/g, '\n\n')
|
? update.info.releaseNotes.replace(/\n/g, '\n\n')
|
||||||
|
|||||||
@ -15,22 +15,23 @@ export const createThinkingCallbacks = (deps: ThinkingCallbacksDependencies) =>
|
|||||||
|
|
||||||
// 内部维护的状态
|
// 内部维护的状态
|
||||||
let thinkingBlockId: string | null = null
|
let thinkingBlockId: string | null = null
|
||||||
|
let _thinking_millsec = 0
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onThinkingStart: async () => {
|
onThinkingStart: async () => {
|
||||||
if (blockManager.hasInitialPlaceholder) {
|
if (blockManager.hasInitialPlaceholder) {
|
||||||
const changes = {
|
const changes: Partial<MessageBlock> = {
|
||||||
type: MessageBlockType.THINKING,
|
type: MessageBlockType.THINKING,
|
||||||
content: '',
|
content: '',
|
||||||
status: MessageBlockStatus.STREAMING,
|
status: MessageBlockStatus.STREAMING,
|
||||||
thinking_millsec: 0
|
thinking_millsec: _thinking_millsec
|
||||||
}
|
}
|
||||||
thinkingBlockId = blockManager.initialPlaceholderBlockId!
|
thinkingBlockId = blockManager.initialPlaceholderBlockId!
|
||||||
blockManager.smartBlockUpdate(thinkingBlockId, changes, MessageBlockType.THINKING, true)
|
blockManager.smartBlockUpdate(thinkingBlockId, changes, MessageBlockType.THINKING, true)
|
||||||
} else if (!thinkingBlockId) {
|
} else if (!thinkingBlockId) {
|
||||||
const newBlock = createThinkingBlock(assistantMsgId, '', {
|
const newBlock = createThinkingBlock(assistantMsgId, '', {
|
||||||
status: MessageBlockStatus.STREAMING,
|
status: MessageBlockStatus.STREAMING,
|
||||||
thinking_millsec: 0
|
thinking_millsec: _thinking_millsec
|
||||||
})
|
})
|
||||||
thinkingBlockId = newBlock.id
|
thinkingBlockId = newBlock.id
|
||||||
await blockManager.handleBlockTransition(newBlock, MessageBlockType.THINKING)
|
await blockManager.handleBlockTransition(newBlock, MessageBlockType.THINKING)
|
||||||
@ -38,26 +39,27 @@ export const createThinkingCallbacks = (deps: ThinkingCallbacksDependencies) =>
|
|||||||
},
|
},
|
||||||
|
|
||||||
onThinkingChunk: async (text: string, thinking_millsec?: number) => {
|
onThinkingChunk: async (text: string, thinking_millsec?: number) => {
|
||||||
|
_thinking_millsec = thinking_millsec || 0
|
||||||
if (thinkingBlockId) {
|
if (thinkingBlockId) {
|
||||||
const blockChanges: Partial<MessageBlock> = {
|
const blockChanges: Partial<MessageBlock> = {
|
||||||
content: text,
|
content: text,
|
||||||
status: MessageBlockStatus.STREAMING,
|
status: MessageBlockStatus.STREAMING,
|
||||||
thinking_millsec: thinking_millsec || 0
|
thinking_millsec: _thinking_millsec
|
||||||
}
|
}
|
||||||
blockManager.smartBlockUpdate(thinkingBlockId, blockChanges, MessageBlockType.THINKING)
|
blockManager.smartBlockUpdate(thinkingBlockId, blockChanges, MessageBlockType.THINKING)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onThinkingComplete: (finalText: string, final_thinking_millsec?: number) => {
|
onThinkingComplete: (finalText: string) => {
|
||||||
if (thinkingBlockId) {
|
if (thinkingBlockId) {
|
||||||
const changes = {
|
const changes: Partial<MessageBlock> = {
|
||||||
type: MessageBlockType.THINKING,
|
|
||||||
content: finalText,
|
content: finalText,
|
||||||
status: MessageBlockStatus.SUCCESS,
|
status: MessageBlockStatus.SUCCESS,
|
||||||
thinking_millsec: final_thinking_millsec || 0
|
thinking_millsec: _thinking_millsec
|
||||||
}
|
}
|
||||||
blockManager.smartBlockUpdate(thinkingBlockId, changes, MessageBlockType.THINKING, true)
|
blockManager.smartBlockUpdate(thinkingBlockId, changes, MessageBlockType.THINKING, true)
|
||||||
thinkingBlockId = null
|
thinkingBlockId = null
|
||||||
|
_thinking_millsec = 0
|
||||||
} else {
|
} else {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`[onThinkingComplete] Received thinking.complete but last block was not THINKING (was ${blockManager.lastBlockType}) or lastBlockId is null.`
|
`[onThinkingComplete] Received thinking.complete but last block was not THINKING (was ${blockManager.lastBlockType}) or lastBlockId is null.`
|
||||||
|
|||||||
@ -410,7 +410,8 @@ describe('streamCallback Integration Tests', () => {
|
|||||||
{ type: ChunkType.THINKING_START },
|
{ type: ChunkType.THINKING_START },
|
||||||
{ type: ChunkType.THINKING_DELTA, text: 'Let me think...', thinking_millsec: 1000 },
|
{ type: ChunkType.THINKING_DELTA, text: 'Let me think...', thinking_millsec: 1000 },
|
||||||
{ type: ChunkType.THINKING_DELTA, text: 'I need to consider...', thinking_millsec: 2000 },
|
{ type: ChunkType.THINKING_DELTA, text: 'I need to consider...', thinking_millsec: 2000 },
|
||||||
{ type: ChunkType.THINKING_COMPLETE, text: 'Final thoughts', thinking_millsec: 3000 },
|
{ type: ChunkType.THINKING_DELTA, text: 'Final thoughts', thinking_millsec: 3000 },
|
||||||
|
{ type: ChunkType.THINKING_COMPLETE, text: 'Final thoughts' },
|
||||||
{ type: ChunkType.BLOCK_COMPLETE }
|
{ type: ChunkType.BLOCK_COMPLETE }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
16
yarn.lock
16
yarn.lock
@ -155,7 +155,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@ai-sdk/google@npm:2.0.14, @ai-sdk/google@npm:^2.0.14":
|
"@ai-sdk/google@npm:2.0.14":
|
||||||
version: 2.0.14
|
version: 2.0.14
|
||||||
resolution: "@ai-sdk/google@npm:2.0.14"
|
resolution: "@ai-sdk/google@npm:2.0.14"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -167,6 +167,18 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@ai-sdk/google@patch:@ai-sdk/google@npm%3A2.0.14#~/.yarn/patches/@ai-sdk-google-npm-2.0.14-376d8b03cc.patch":
|
||||||
|
version: 2.0.14
|
||||||
|
resolution: "@ai-sdk/google@patch:@ai-sdk/google@npm%3A2.0.14#~/.yarn/patches/@ai-sdk-google-npm-2.0.14-376d8b03cc.patch::version=2.0.14&hash=a91bb2"
|
||||||
|
dependencies:
|
||||||
|
"@ai-sdk/provider": "npm:2.0.0"
|
||||||
|
"@ai-sdk/provider-utils": "npm:3.0.9"
|
||||||
|
peerDependencies:
|
||||||
|
zod: ^3.25.76 || ^4
|
||||||
|
checksum: 10c0/5ec33dc9898457b1f48ed14cb767817345032c539dd21b7e21985ed47bc21b0820922b581bf349bb3898136790b12da3a0a7c9903c333a28ead0c3c2cd5230f2
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@ai-sdk/mistral@npm:^2.0.14":
|
"@ai-sdk/mistral@npm:^2.0.14":
|
||||||
version: 2.0.14
|
version: 2.0.14
|
||||||
resolution: "@ai-sdk/mistral@npm:2.0.14"
|
resolution: "@ai-sdk/mistral@npm:2.0.14"
|
||||||
@ -2374,7 +2386,7 @@ __metadata:
|
|||||||
"@ai-sdk/anthropic": "npm:^2.0.17"
|
"@ai-sdk/anthropic": "npm:^2.0.17"
|
||||||
"@ai-sdk/azure": "npm:^2.0.30"
|
"@ai-sdk/azure": "npm:^2.0.30"
|
||||||
"@ai-sdk/deepseek": "npm:^1.0.17"
|
"@ai-sdk/deepseek": "npm:^1.0.17"
|
||||||
"@ai-sdk/google": "npm:^2.0.14"
|
"@ai-sdk/google": "patch:@ai-sdk/google@npm%3A2.0.14#~/.yarn/patches/@ai-sdk-google-npm-2.0.14-376d8b03cc.patch"
|
||||||
"@ai-sdk/openai": "npm:^2.0.30"
|
"@ai-sdk/openai": "npm:^2.0.30"
|
||||||
"@ai-sdk/openai-compatible": "npm:^1.0.17"
|
"@ai-sdk/openai-compatible": "npm:^1.0.17"
|
||||||
"@ai-sdk/provider": "npm:^2.0.0"
|
"@ai-sdk/provider": "npm:^2.0.0"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user