Compare commits

..

8 Commits

Author SHA1 Message Date
kangfenmao 06ce720d6e fix: S3 auto sync ENOENT error with custom-minapps.json
- Add file existence check in FileStorage.readFile to prevent ENOENT errors
- Improve error handling in custom-minapps.json loading across all components
- Add robust JSON parsing and validation for custom minapps
- Handle concurrent access during S3 auto sync operations

Fixes #11255
2025-11-14 14:17:31 +08:00
Zhaokun 75fcf8fbb5 fix: notes content search next scroll (#10908)
* fix: topic branch incomplete copy - split ID mapping into two passes

Fix the bug where topic branching would not copy all message relationships completely.The issue was that askId mapping lookup happened in the same loop as ID generation, causing later messages' askIds to fail mapping when they referenced messages that hadn't been processed yet.

Solution: Split into two passes:
 1. First pass: Generate new IDs for all messages and build complete mapping
 2. Second pass: Clone messages and blocks using the complete ID mapping

This ensures all message relationships (especially assistant message askId references)are properly maintained in the new topic.

* fix(notes): 保持 Ctrl+F ‘下一个’在编辑器容器内滚动,避免索引提前回到第一条

- 使用传入的滚动容器计算相对偏移并 target.scrollTo 居中
- 容器不可滚动时回退到 scrollIntoView,兼容其他页面
- 将 target 纳入依赖,确保引用最新容器

受影响文件:
- src/renderer/src/components/ContentSearch.tsx:165

* fix(search): improve notes content search next-scroll behavior

* Update dom.ts

---------

Co-authored-by: Pleasurecruise <3196812536@qq.com>
2025-11-14 11:51:18 +08:00
Phantom 35aa9d7355 fix: Incorrect navigation when creating new message with @ (#10930)
* fix(message): Incorrect navigation when creating new message with @

Update variable name from newAssistantStub to newAssistantMessageStub for clarity
Add dispatch calls to update message folding state
Remove unused message length tracking effect in MessageGroup

Fixes #10928

* refactor(MessageGroup): remove unused prevMessageLengthRef variable
2025-11-14 11:45:10 +08:00
Pleasure1234 b08aecb22b fix: enable numeric sorting for note names (#11261)
Updated the sorting logic in getSorter to use the 'numeric' option in localeCompare for all name-based sorts. This ensures that note names containing numbers are sorted in a more natural, human-friendly order.
2025-11-14 11:37:19 +08:00
Phantom 45fc6c2afd fix: minimax new api host & anthropic api support (#11269)
* feat(models): add MiniMax M2 models to default configuration

* fix(config): update minimax api host and add anthropic host

Update the API endpoint for MiniMax provider and add a new endpoint for Anthropic integration

* feat: add minimax to ANTHROPIC_COMPATIBLE_PROVIDER_IDS

* docs(ProviderSetting): add todo comment for reset button

* fix(store): update minimax provider config in migration 174

Add anthropicApiHost to minimax provider configuration during state migration

* fix(store): revert version and remove unused migration

Remove migration for version 175 and revert persisted reducer version to 174
2025-11-14 10:55:41 +08:00
defi-failure d6e7ce330e feat: move error response to top and enlarge window for easier debugging (#11169) 2025-11-13 18:22:00 +08:00
枫亚 4f7d8731ea fix: correct typo in zh-cn locale (#11270) 2025-11-13 17:04:39 +08:00
SuYao 2b5ac5ab51 feat: 添加 AI Gateway Provider (#11064)
* feat: 添加 AI Gateway 提供者支持,包括配置、类型定义和本地化文本

* fix(i18n): Auto update translations for PR #11064

* fix/typecheck

* fix(i18n): Auto update translations for PR #11064

* fix(i18n): Auto update translations for PR #11064

* feat: cerebras

* fix: glm

* fix: minimax api host

---------

Co-authored-by: GitHub Action <action@github.com>
2025-11-13 16:09:49 +08:00
37 changed files with 436 additions and 96 deletions
+2
View File
@@ -107,6 +107,8 @@
"@agentic/searxng": "^7.3.3",
"@agentic/tavily": "^7.3.3",
"@ai-sdk/amazon-bedrock": "^3.0.53",
"@ai-sdk/cerebras": "^1.0.31",
"@ai-sdk/gateway": "^2.0.9",
"@ai-sdk/google-vertex": "^3.0.62",
"@ai-sdk/huggingface": "patch:@ai-sdk/huggingface@npm%3A0.0.8#~/.yarn/patches/@ai-sdk-huggingface-npm-0.0.8-d4d0aaac93.patch",
"@ai-sdk/mistral": "^2.0.23",
+6
View File
@@ -485,6 +485,12 @@ class FileStorage {
): Promise<string> => {
const filePath = path.join(this.storageDir, id)
// 先检查文件是否存在,避免 ENOENT 错误
if (!fs.existsSync(filePath)) {
logger.debug(`File not found: ${filePath}`)
throw new Error(`ENOENT: no such file or directory, open '${id}'`)
}
const fileExtension = path.extname(filePath)
if (documentExts.includes(fileExtension)) {
+15 -2
View File
@@ -7,16 +7,17 @@
* 2. 暂时保持接口兼容性
*/
import type { GatewayLanguageModelEntry } from '@ai-sdk/gateway'
import { createExecutor } from '@cherrystudio/ai-core'
import { loggerService } from '@logger'
import { getEnableDeveloperMode } from '@renderer/hooks/useSettings'
import { addSpan, endSpan } from '@renderer/services/SpanManagerService'
import type { StartSpanParams } from '@renderer/trace/types/ModelSpanEntity'
import type { Assistant, GenerateImageParams, Model, Provider } from '@renderer/types'
import { type Assistant, type GenerateImageParams, type Model, type Provider, SystemProviderIds } from '@renderer/types'
import type { AiSdkModel, StreamTextParams } from '@renderer/types/aiCoreTypes'
import { SUPPORTED_IMAGE_ENDPOINT_LIST } from '@renderer/utils'
import { buildClaudeCodeSystemModelMessage } from '@shared/anthropic'
import { type ImageModel, type LanguageModel, type Provider as AiSdkProvider, wrapLanguageModel } from 'ai'
import { gateway, type ImageModel, type LanguageModel, type Provider as AiSdkProvider, wrapLanguageModel } from 'ai'
import AiSdkToChunkAdapter from './chunk/AiSdkToChunkAdapter'
import LegacyAiProvider from './legacy/index'
@@ -439,6 +440,18 @@ export default class ModernAiProvider {
// 代理其他方法到原有实现
public async models() {
if (this.actualProvider.id === SystemProviderIds['ai-gateway']) {
const formatModel = function (models: GatewayLanguageModelEntry[]): Model[] {
return models.map((m) => ({
id: m.id,
name: m.name,
provider: 'gateway',
group: m.id.split('/')[0],
description: m.description ?? undefined
}))
}
return formatModel((await gateway.getAvailableModels()).models)
}
return this.legacyProvider.models()
}
@@ -71,6 +71,21 @@ export const NEW_PROVIDER_CONFIGS: ProviderConfig[] = [
creatorFunctionName: 'createHuggingFace',
supportsImageGeneration: true,
aliases: ['hf', 'hugging-face']
},
{
id: 'ai-gateway',
name: 'AI Gateway',
import: () => import('@ai-sdk/gateway'),
creatorFunctionName: 'createGateway',
supportsImageGeneration: true,
aliases: ['gateway']
},
{
id: 'cerebras',
name: 'Cerebras',
import: () => import('@ai-sdk/cerebras'),
creatorFunctionName: 'createCerebras',
supportsImageGeneration: false
}
] as const
+3 -2
View File
@@ -151,11 +151,12 @@ export function buildProviderOptions(
...providerSpecificOptions,
...getCustomParameters(assistant)
}
// vertex需要映射到google或anthropic
const rawProviderKey =
{
'google-vertex': 'google',
'google-vertex-anthropic': 'anthropic'
'google-vertex-anthropic': 'anthropic',
'ai-gateway': 'gateway'
}[rawProviderId] || rawProviderId
// 返回 AI Core SDK 要求的格式:{ 'providerId': providerOptions }
@@ -109,6 +109,11 @@ export function getReasoningEffort(assistant: Assistant, model: Model): Reasonin
// use thinking, doubao, zhipu, etc.
if (isSupportedThinkingTokenDoubaoModel(model) || isSupportedThinkingTokenZhipuModel(model)) {
if (provider.id === SystemProviderIds.cerebras) {
return {
disable_reasoning: true
}
}
return { thinking: { type: 'disabled' } }
}
@@ -306,6 +311,9 @@ export function getReasoningEffort(assistant: Assistant, model: Model): Reasonin
return {}
}
if (isSupportedThinkingTokenZhipuModel(model)) {
if (provider.id === SystemProviderIds.cerebras) {
return {}
}
return { thinking: { type: 'enabled' } }
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Vercel</title><path d="M12 0l12 20.785H0L12 0z"></path></svg>

After

Width:  |  Height:  |  Size: 225 B

@@ -1,5 +1,6 @@
import { ActionIconButton } from '@renderer/components/Buttons'
import NarrowLayout from '@renderer/pages/home/Messages/NarrowLayout'
import { scrollElementIntoView } from '@renderer/utils'
import { Tooltip } from 'antd'
import { debounce } from 'lodash'
import { CaseSensitive, ChevronDown, ChevronUp, User, WholeWord, X } from 'lucide-react'
@@ -181,17 +182,14 @@ export const ContentSearch = React.forwardRef<ContentSearchRef, Props>(
// 3. 将当前项滚动到视图中
// 获取第一个文本节点的父元素来进行滚动
const parentElement = currentMatchRange.startContainer.parentElement
if (shouldScroll) {
parentElement?.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'nearest'
})
if (shouldScroll && parentElement) {
// 优先在指定的滚动容器内滚动,避免滚动整个页面导致索引错乱/看起来"跳到第一条"
scrollElementIntoView(parentElement, target)
}
}
}
},
[allRanges, currentIndex]
[allRanges, currentIndex, target]
)
const search = useCallback(
+21 -2
View File
@@ -87,8 +87,27 @@ const MinApp: FC<Props> = ({ app, onClick, size = 60, isLast }) => {
danger: true,
onClick: async () => {
try {
const content = await window.api.file.read('custom-minapps.json')
const customApps = JSON.parse(content)
let content: string
let customApps: MinAppType[]
try {
content = await window.api.file.read('custom-minapps.json')
customApps = JSON.parse(content)
// 确保解析结果是数组
if (!Array.isArray(customApps)) {
customApps = []
}
} catch (error: any) {
// 如果文件不存在或解析失败,使用空数组
if (error.message?.includes('ENOENT') || error.message?.includes('no such file')) {
customApps = []
} else {
// 其他解析错误,使用空数组
logger.warn('Failed to read custom-minapps.json, using empty array:', error as Error)
customApps = []
}
}
const updatedApps = customApps.filter((customApp: MinAppType) => customApp.id !== app.id)
await window.api.file.writeWithId('custom-minapps.json', JSON.stringify(updatedApps, null, 2))
window.toast.success(t('settings.miniapps.custom.remove_success'))
+33 -4
View File
@@ -67,13 +67,42 @@ const loadCustomMiniApp = async (): Promise<MinAppType[]> => {
let content: string
try {
content = await window.api.file.read('custom-minapps.json')
} catch (error) {
} catch (error: any) {
// 如果文件不存在,创建一个空的 JSON 数组
content = '[]'
await window.api.file.writeWithId('custom-minapps.json', content)
// 检查是否是文件不存在的错误
if (error.message?.includes('ENOENT') || error.message?.includes('no such file')) {
content = '[]'
try {
await window.api.file.writeWithId('custom-minapps.json', content)
} catch (writeError) {
logger.warn('Failed to create custom-minapps.json file:', writeError as Error)
// 如果创建文件失败,仍然返回空数组
}
} else {
// 其他类型的错误,记录并返回空数组
logger.error('Failed to read custom-minapps.json:', error as Error)
return []
}
}
// 确保内容有效
if (!content || content.trim() === '') {
content = '[]'
}
let customApps: any[]
try {
customApps = JSON.parse(content)
// 确保解析结果是数组
if (!Array.isArray(customApps)) {
logger.warn('custom-minapps.json content is not an array, using empty array')
customApps = []
}
} catch (parseError) {
logger.error('Failed to parse custom-minapps.json content:', parseError as Error)
customApps = []
}
const customApps = JSON.parse(content)
const now = new Date().toISOString()
return customApps.map((app: any) => ({
+34 -1
View File
@@ -1003,6 +1003,18 @@ export const SYSTEM_MODELS: Record<SystemProviderId | 'defaultModel', Model[]> =
provider: 'minimax',
name: 'minimax-01',
group: 'minimax-01'
},
{
id: 'MiniMax-M2',
provider: 'minimax',
name: 'MiniMax M2',
group: 'minimax-m2'
},
{
id: 'MiniMax-M2-Stable',
provider: 'minimax',
name: 'MiniMax M2 Stable',
group: 'minimax-m2'
}
],
hyperbolic: [
@@ -1840,5 +1852,26 @@ export const SYSTEM_MODELS: Record<SystemProviderId | 'defaultModel', Model[]> =
group: 'LongCat'
}
],
huggingface: []
huggingface: [],
'ai-gateway': [],
cerebras: [
{
id: 'gpt-oss-120b',
name: 'GPT oss 120B',
provider: 'cerebras',
group: 'openai'
},
{
id: 'zai-glm-4.6',
name: 'GLM 4.6',
provider: 'cerebras',
group: 'zai'
},
{
id: 'qwen-3-235b-a22b-instruct-2507',
name: 'Qwen 3 235B A22B Instruct',
provider: 'cerebras',
group: 'qwen'
}
]
}
+55 -4
View File
@@ -12,6 +12,7 @@ import BaiduCloudProviderLogo from '@renderer/assets/images/providers/baidu-clou
import BailianProviderLogo from '@renderer/assets/images/providers/bailian.png'
import BurnCloudProviderLogo from '@renderer/assets/images/providers/burncloud.png'
import CephalonProviderLogo from '@renderer/assets/images/providers/cephalon.jpeg'
import CerebrasProviderLogo from '@renderer/assets/images/providers/cerebras.webp'
import CherryInProviderLogo from '@renderer/assets/images/providers/cherryin.png'
import DeepSeekProviderLogo from '@renderer/assets/images/providers/deepseek.png'
import DmxapiProviderLogo from '@renderer/assets/images/providers/DMXAPI.png'
@@ -51,6 +52,7 @@ import StepProviderLogo from '@renderer/assets/images/providers/step.png'
import TencentCloudProviderLogo from '@renderer/assets/images/providers/tencent-cloud-ti.png'
import TogetherProviderLogo from '@renderer/assets/images/providers/together.png'
import TokenFluxProviderLogo from '@renderer/assets/images/providers/tokenflux.png'
import AIGatewayProviderLogo from '@renderer/assets/images/providers/vercel.svg'
import VertexAIProviderLogo from '@renderer/assets/images/providers/vertexai.svg'
import BytedanceProviderLogo from '@renderer/assets/images/providers/volcengine.png'
import VoyageAIProviderLogo from '@renderer/assets/images/providers/voyageai.png'
@@ -470,7 +472,8 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
name: 'MiniMax',
type: 'openai',
apiKey: '',
apiHost: 'https://api.minimax.chat/v1/',
apiHost: 'https://api.minimaxi.com/v1',
anthropicApiHost: 'https://api.minimaxi.com/anthropic',
models: SYSTEM_MODELS.minimax,
isSystem: true,
enabled: false
@@ -675,6 +678,26 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
models: [],
isSystem: true,
enabled: false
},
'ai-gateway': {
id: 'ai-gateway',
name: 'AI Gateway',
type: 'ai-gateway',
apiKey: '',
apiHost: 'https://ai-gateway.vercel.sh/v1',
models: [],
isSystem: true,
enabled: false
},
cerebras: {
id: 'cerebras',
name: 'Cerebras AI',
type: 'openai',
apiKey: '',
apiHost: 'https://api.cerebras.ai/v1',
models: SYSTEM_MODELS.cerebras,
isSystem: true,
enabled: false
}
} as const
@@ -741,7 +764,9 @@ export const PROVIDER_LOGO_MAP: AtLeast<SystemProviderId, string> = {
aionly: AiOnlyProviderLogo,
longcat: LongCatProviderLogo,
huggingface: HuggingfaceProviderLogo,
sophnet: SophnetProviderLogo
sophnet: SophnetProviderLogo,
'ai-gateway': AIGatewayProviderLogo,
cerebras: CerebrasProviderLogo
} as const
export function getProviderLogo(providerId: string) {
@@ -1048,7 +1073,7 @@ export const PROVIDER_URLS: Record<SystemProviderId, ProviderUrls> = {
},
minimax: {
api: {
url: 'https://api.minimax.chat/v1/'
url: 'https://api.minimaxi.com/v1/'
},
websites: {
official: 'https://platform.minimaxi.com/',
@@ -1390,6 +1415,28 @@ export const PROVIDER_URLS: Record<SystemProviderId, ProviderUrls> = {
docs: 'https://huggingface.co/docs',
models: 'https://huggingface.co/models'
}
},
'ai-gateway': {
api: {
url: 'https://ai-gateway.vercel.sh/v1/ai'
},
websites: {
official: 'https://vercel.com/ai-gateway',
apiKey: 'https://vercel.com/',
docs: 'https://vercel.com/docs/ai-gateway',
models: 'https://vercel.com/ai-gateway/models'
}
},
cerebras: {
api: {
url: 'https://api.cerebras.ai/v1'
},
websites: {
official: 'https://www.cerebras.ai',
apiKey: 'https://cloud.cerebras.ai',
docs: 'https://inference-docs.cerebras.ai/introduction',
models: 'https://inference-docs.cerebras.ai/models/overview'
}
}
}
@@ -1452,7 +1499,7 @@ export const isSupportEnableThinkingProvider = (provider: Provider) => {
)
}
const NOT_SUPPORT_SERVICE_TIER_PROVIDERS = ['github', 'copilot'] as const satisfies SystemProviderId[]
const NOT_SUPPORT_SERVICE_TIER_PROVIDERS = ['github', 'copilot', 'cerebras'] as const satisfies SystemProviderId[]
/**
* 判断提供商是否支持 service_tier 设置。 Only for OpenAI API.
@@ -1519,6 +1566,10 @@ export function isGeminiProvider(provider: Provider): boolean {
return provider.type === 'gemini'
}
export function isAIGatewayProvider(provider: Provider): boolean {
return provider.type === 'ai-gateway'
}
const NOT_SUPPORT_API_VERSION_PROVIDERS = ['github', 'copilot', 'perplexity'] as const satisfies SystemProviderId[]
export const isSupportAPIVersionProvider = (provider: Provider) => {
+3 -1
View File
@@ -86,7 +86,9 @@ const providerKeyMap = {
aionly: 'provider.aionly',
longcat: 'provider.longcat',
huggingface: 'provider.huggingface',
sophnet: 'provider.sophnet'
sophnet: 'provider.sophnet',
'ai-gateway': 'provider.ai-gateway',
cerebras: 'provider.cerebras'
} as const
/**
+2
View File
@@ -2484,6 +2484,7 @@
},
"provider": {
"302ai": "302.AI",
"ai-gateway": "AI Gateway",
"aihubmix": "AiHubMix",
"aionly": "AiOnly",
"alayanew": "Alaya NeW",
@@ -2494,6 +2495,7 @@
"baidu-cloud": "Baidu Cloud",
"burncloud": "BurnCloud",
"cephalon": "Cephalon",
"cerebras": "Cerebras AI",
"cherryin": "CherryIN",
"copilot": "GitHub Copilot",
"dashscope": "Alibaba Cloud",
+3 -1
View File
@@ -904,7 +904,7 @@
"show_line_numbers": "代码显示行号",
"temperature": {
"label": "模型温度",
"tip": "模型生成文本的随机程度。值越大,回复内容越有多样性、创造性、随机性;设为 0 根据事实回答。日常聊天建议设置为 0.7"
"tip": "模型生成文本的随机程度。值越大,回复内容越有多样性、创造性、随机性;设为 0 根据事实回答。日常聊天建议设置为 0.7"
},
"thought_auto_collapse": {
"label": "思考内容自动折叠",
@@ -2484,6 +2484,7 @@
},
"provider": {
"302ai": "302.AI",
"ai-gateway": "AI Gateway",
"aihubmix": "AiHubMix",
"aionly": "唯一AI (AiOnly)",
"alayanew": "Alaya NeW",
@@ -2494,6 +2495,7 @@
"baidu-cloud": "百度云千帆",
"burncloud": "BurnCloud",
"cephalon": "Cephalon",
"cerebras": "Cerebras AI",
"cherryin": "CherryIN",
"copilot": "GitHub Copilot",
"dashscope": "阿里云百炼",
+2
View File
@@ -2484,6 +2484,7 @@
},
"provider": {
"302ai": "302.AI",
"ai-gateway": "AI 閘道器",
"aihubmix": "AiHubMix",
"aionly": "唯一AI (AiOnly)",
"alayanew": "Alaya NeW",
@@ -2494,6 +2495,7 @@
"baidu-cloud": "百度雲千帆",
"burncloud": "BurnCloud",
"cephalon": "Cephalon",
"cerebras": "Cerebras AI",
"cherryin": "CherryIN",
"copilot": "GitHub Copilot",
"dashscope": "阿里雲百鍊",
@@ -2484,6 +2484,7 @@
},
"provider": {
"302ai": "302.AI",
"ai-gateway": "KI-Gateway",
"aihubmix": "AiHubMix",
"aionly": "Einzige KI (AiOnly)",
"alayanew": "Alaya NeW",
@@ -2494,6 +2495,7 @@
"baidu-cloud": "Baidu Cloud Qianfan",
"burncloud": "BurnCloud",
"cephalon": "Cephalon",
"cerebras": "Cerebras AI",
"cherryin": "CherryIN",
"copilot": "GitHub Copilot",
"dashscope": "Alibaba Cloud Bailian",
@@ -2484,6 +2484,7 @@
},
"provider": {
"302ai": "302.AI",
"ai-gateway": "Πύλη Τεχνητής Νοημοσύνης",
"aihubmix": "AiHubMix",
"aionly": "AiOnly",
"alayanew": "Alaya NeW",
@@ -2494,6 +2495,7 @@
"baidu-cloud": "Baidu Cloud Qianfan",
"burncloud": "BurnCloud",
"cephalon": "Cephalon",
"cerebras": "Cerebras AI",
"cherryin": "CherryIN",
"copilot": "GitHub Copilot",
"dashscope": "AliCloud Bailian",
@@ -2484,6 +2484,7 @@
},
"provider": {
"302ai": "302.AI",
"ai-gateway": "Puerta de enlace de IA",
"aihubmix": "AiHubMix",
"aionly": "AiOnly",
"alayanew": "Alaya NeW",
@@ -2494,6 +2495,7 @@
"baidu-cloud": "Baidu Nube Qiánfān",
"burncloud": "BurnCloud",
"cephalon": "Cephalon",
"cerebras": "Cerebras AI",
"cherryin": "CherryIN",
"copilot": "GitHub Copiloto",
"dashscope": "Álibaba Nube BaiLiàn",
@@ -2484,6 +2484,7 @@
},
"provider": {
"302ai": "302.AI",
"ai-gateway": "Passerelle IA",
"aihubmix": "AiHubMix",
"aionly": "AiOnly",
"alayanew": "Alaya NeW",
@@ -2494,6 +2495,7 @@
"baidu-cloud": "Baidu Cloud Qianfan",
"burncloud": "BurnCloud",
"cephalon": "Cephalon",
"cerebras": "Cerebras AI",
"cherryin": "CherryIN",
"copilot": "GitHub Copilote",
"dashscope": "AliCloud BaiLian",
@@ -2484,6 +2484,7 @@
},
"provider": {
"302ai": "302.AI",
"ai-gateway": "AIゲートウェイ",
"aihubmix": "AiHubMix",
"aionly": "AiOnly",
"alayanew": "Alaya NeW",
@@ -2494,6 +2495,7 @@
"baidu-cloud": "Baidu Cloud",
"burncloud": "BurnCloud",
"cephalon": "Cephalon",
"cerebras": "Cerebras AI",
"cherryin": "CherryIN",
"copilot": "GitHub Copilot",
"dashscope": "Alibaba Cloud",
@@ -2484,6 +2484,7 @@
},
"provider": {
"302ai": "302.AI",
"ai-gateway": "Gateway de IA",
"aihubmix": "AiHubMix",
"aionly": "AiOnly",
"alayanew": "Alaya NeW",
@@ -2494,6 +2495,7 @@
"baidu-cloud": "Nuvem Baidu",
"burncloud": "BurnCloud",
"cephalon": "Cephalon",
"cerebras": "Cerebras AI",
"cherryin": "CherryIN",
"copilot": "GitHub Copiloto",
"dashscope": "Área de Atuação AliCloud",
@@ -2484,6 +2484,7 @@
},
"provider": {
"302ai": "302.AI",
"ai-gateway": "AI-шлюз",
"aihubmix": "AiHubMix",
"aionly": "AiOnly",
"alayanew": "Alaya NeW",
@@ -2494,6 +2495,7 @@
"baidu-cloud": "Baidu Cloud",
"burncloud": "BurnCloud",
"cephalon": "Cephalon",
"cerebras": "Cerebras AI",
"cherryin": "CherryIN",
"copilot": "GitHub Copilot",
"dashscope": "Alibaba Cloud",
@@ -207,14 +207,15 @@ const ErrorDetailModal: React.FC<ErrorDetailModalProps> = ({ open, onClose, erro
{t('common.close')}
</Button>
]}
width={600}>
width="80%"
style={{ maxWidth: '1200px', minWidth: '600px' }}>
<ErrorDetailContainer>{renderErrorDetails(error)}</ErrorDetailContainer>
</Modal>
)
}
const ErrorDetailContainer = styled.div`
max-height: 400px;
max-height: 60vh;
overflow-y: auto;
`
@@ -347,16 +348,8 @@ const AiSdkError = ({ error }: { error: SerializedAiSdkErrorUnion }) => {
return (
<ErrorDetailList>
<AiSdkErrorBase error={error} />
{(isSerializedAiSdkAPICallError(error) || isSerializedAiSdkDownloadError(error)) && (
<>
{error.statusCode && (
<ErrorDetailItem>
<ErrorDetailLabel>{t('error.statusCode')}:</ErrorDetailLabel>
<ErrorDetailValue>{error.statusCode}</ErrorDetailValue>
</ErrorDetailItem>
)}
{error.url && (
<ErrorDetailItem>
<ErrorDetailLabel>{t('error.requestUrl')}:</ErrorDetailLabel>
@@ -374,12 +367,27 @@ const AiSdkError = ({ error }: { error: SerializedAiSdkErrorUnion }) => {
<CodeViewer value={error.responseBody} className="source-view" language="json" expanded />
</ErrorDetailItem>
)}
</>
)}
{error.requestBodyValues && (
{(isSerializedAiSdkAPICallError(error) || isSerializedAiSdkDownloadError(error)) && (
<>
{error.statusCode && (
<ErrorDetailItem>
<ErrorDetailLabel>{t('error.requestBodyValues')}:</ErrorDetailLabel>
<ErrorDetailLabel>{t('error.statusCode')}:</ErrorDetailLabel>
<ErrorDetailValue>{error.statusCode}</ErrorDetailValue>
</ErrorDetailItem>
)}
</>
)}
{isSerializedAiSdkAPICallError(error) && (
<>
{error.responseHeaders && (
<ErrorDetailItem>
<ErrorDetailLabel>{t('error.responseHeaders')}:</ErrorDetailLabel>
<CodeViewer
value={safeToString(error.requestBodyValues)}
value={JSON.stringify(error.responseHeaders, null, 2)}
className="source-view"
language="json"
expanded
@@ -387,11 +395,11 @@ const AiSdkError = ({ error }: { error: SerializedAiSdkErrorUnion }) => {
</ErrorDetailItem>
)}
{error.responseHeaders && (
{error.requestBodyValues && (
<ErrorDetailItem>
<ErrorDetailLabel>{t('error.responseHeaders')}:</ErrorDetailLabel>
<ErrorDetailLabel>{t('error.requestBodyValues')}:</ErrorDetailLabel>
<CodeViewer
value={JSON.stringify(error.responseHeaders, null, 2)}
value={safeToString(error.requestBodyValues)}
className="source-view"
language="json"
expanded
@@ -627,6 +635,8 @@ const AiSdkError = ({ error }: { error: SerializedAiSdkErrorUnion }) => {
<ErrorDetailValue>{error.functionality}</ErrorDetailValue>
</ErrorDetailItem>
)}
<AiSdkErrorBase error={error} />
</ErrorDetailList>
)
}
@@ -11,7 +11,8 @@ import type { Topic } from '@renderer/types'
import type { Message } from '@renderer/types/newMessage'
import { classNames } from '@renderer/utils'
import { Popover } from 'antd'
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import type { ComponentProps} from 'react';
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import styled from 'styled-components'
import { useChatMaxWidth } from '../Chat'
@@ -43,9 +44,6 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => {
)
const [selectedIndex, setSelectedIndex] = useState(messageLength - 1)
// Refs
const prevMessageLengthRef = useRef(messageLength)
// 对于单模型消息,采用简单的样式,避免 overflow 影响内部的 sticky 效果
const multiModelMessageStyle = useMemo(
() => (messageLength < 2 ? 'fold' : _multiModelMessageStyle),
@@ -83,24 +81,6 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => {
},
[editMessage, selectedMessageId, setTimeoutTimer]
)
useEffect(() => {
if (messageLength > prevMessageLengthRef.current) {
setSelectedIndex(messageLength - 1)
const lastMessage = messages[messageLength - 1]
if (lastMessage) {
setSelectedMessage(lastMessage)
}
} else {
const newIndex = messages.findIndex((msg) => msg.id === selectedMessageId)
if (newIndex !== -1) {
setSelectedIndex(newIndex)
}
}
prevMessageLengthRef.current = messageLength
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [messageLength])
// 添加对流程图节点点击事件的监听
useEffect(() => {
// 只在组件挂载和消息数组变化时添加监听器
@@ -223,7 +203,7 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => {
message,
topic,
index: message.index
}
} satisfies ComponentProps<typeof MessageItem>
const messageContent = (
<MessageWrapper
@@ -277,7 +257,7 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => {
isGrouped,
topic,
multiModelMessageStyle,
messages.length,
messages,
selectedMessageId,
onUpdateUseful,
groupContextMessageId,
@@ -32,8 +32,26 @@ const NewAppButton: FC<Props> = ({ size = 60 }) => {
const handleAddCustomApp = async (values: any) => {
try {
const content = await window.api.file.read('custom-minapps.json')
const customApps = JSON.parse(content)
let content: string
let customApps: MinAppType[]
try {
content = await window.api.file.read('custom-minapps.json')
customApps = JSON.parse(content)
// 确保解析结果是数组
if (!Array.isArray(customApps)) {
customApps = []
}
} catch (error: any) {
// 如果文件不存在或解析失败,使用空数组
if (error.message?.includes('ENOENT') || error.message?.includes('no such file')) {
customApps = []
} else {
// 其他解析错误,使用空数组
logger.warn('Failed to read custom-minapps.json, using empty array:', error as Error)
customApps = []
}
}
// Check for duplicate ID
if (customApps.some((app: MinAppType) => app.id === values.id)) {
@@ -5,6 +5,7 @@ import { ApiKeyListPopup } from '@renderer/components/Popups/ApiKeyListPopup'
import Selector from '@renderer/components/Selector'
import { isEmbeddingModel, isRerankModel } from '@renderer/config/models'
import {
isAIGatewayProvider,
isAnthropicProvider,
isAzureOpenAIProvider,
isGeminiProvider,
@@ -80,7 +81,8 @@ const ANTHROPIC_COMPATIBLE_PROVIDER_IDS = [
SystemProviderIds.aihubmix,
SystemProviderIds.grok,
SystemProviderIds.cherryin,
SystemProviderIds.longcat
SystemProviderIds.longcat,
SystemProviderIds.minimax
] as const
type AnthropicCompatibleProviderId = (typeof ANTHROPIC_COMPATIBLE_PROVIDER_IDS)[number]
@@ -305,6 +307,9 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
if (isVertexProvider(provider)) {
return formatVertexApiHost(provider) + '/publishers/google'
}
if (isAIGatewayProvider(provider)) {
return formatApiHost(apiHost) + '/language-model'
}
return formatApiHost(apiHost)
}
@@ -520,24 +525,17 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
<SettingHelpText>{t('settings.provider.vertex_ai.api_host_help')}</SettingHelpText>
</SettingHelpTextRow>
)}
{(isOpenAICompatibleProvider(provider) ||
isAzureOpenAIProvider(provider) ||
isAnthropicProvider(provider) ||
isGeminiProvider(provider) ||
isVertexProvider(provider) ||
isOpenAIProvider(provider)) && (
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
<SettingHelpText
style={{
marginLeft: 6,
marginRight: '1em',
whiteSpace: 'break-spaces',
wordBreak: 'break-all'
}}>
{t('settings.provider.api_host_preview', { url: hostPreview() })}
</SettingHelpText>
</SettingHelpTextRow>
)}
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
<SettingHelpText
style={{
marginLeft: 6,
marginRight: '1em',
whiteSpace: 'break-spaces',
wordBreak: 'break-all'
}}>
{t('settings.provider.api_host_preview', { url: hostPreview() })}
</SettingHelpText>
</SettingHelpTextRow>
</>
)}
@@ -550,6 +548,7 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
onChange={(e) => setAnthropicHost(e.target.value)}
onBlur={onUpdateAnthropicHost}
/>
{/* TODO: Add a reset button here. */}
</Space.Compact>
<SettingHelpTextRow style={{ flexDirection: 'column', alignItems: 'flex-start', gap: '4px' }}>
<SettingHelpText style={{ marginLeft: 6, whiteSpace: 'break-spaces', wordBreak: 'break-all' }}>
+3 -3
View File
@@ -128,9 +128,9 @@ export async function uploadNotes(files: File[], targetPath: string): Promise<Up
function getSorter(sortType: NotesSortType): (a: NotesTreeNode, b: NotesTreeNode) => number {
switch (sortType) {
case 'sort_a2z':
return (a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'accent' })
return (a, b) => a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'accent' })
case 'sort_z2a':
return (a, b) => b.name.localeCompare(a.name, undefined, { sensitivity: 'accent' })
return (a, b) => b.name.localeCompare(a.name, undefined, { numeric: true, sensitivity: 'accent' })
case 'sort_updated_desc':
return (a, b) => getTime(b.updatedAt) - getTime(a.updatedAt)
case 'sort_updated_asc':
@@ -140,7 +140,7 @@ function getSorter(sortType: NotesSortType): (a: NotesTreeNode, b: NotesTreeNode
case 'sort_created_asc':
return (a, b) => getTime(a.createdAt) - getTime(b.createdAt)
default:
return (a, b) => a.name.localeCompare(b.name)
return (a, b) => a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'accent' })
}
}
+1 -1
View File
@@ -67,7 +67,7 @@ const persistedReducer = persistReducer(
{
key: 'cherry-studio',
storage,
version: 173,
version: 174,
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs', 'toolPermissions'],
migrate
},
+17
View File
@@ -2802,6 +2802,23 @@ const migrateConfig = {
logger.error('migrate 173 error', error as Error)
return state
}
},
'174': (state: RootState) => {
try {
addProvider(state, SystemProviderIds.longcat)
addProvider(state, SystemProviderIds['ai-gateway'])
addProvider(state, 'cerebras')
state.llm.providers.forEach((provider) => {
if (provider.id === SystemProviderIds.minimax) {
provider.anthropicApiHost = 'https://api.minimaxi.com/anthropic'
}
})
return state
} catch (error) {
logger.error('migrate 174 error', error as Error)
return state
}
}
}
+9 -4
View File
@@ -1446,7 +1446,7 @@ export const appendAssistantResponseThunk =
}
// 2. Create the new assistant message stub
const newAssistantStub = createAssistantMessage(assistant.id, topicId, {
const newAssistantMessageStub = createAssistantMessage(assistant.id, topicId, {
askId: askId, // Crucial: Use the original askId
model: newModel,
modelId: newModel.id,
@@ -1459,9 +1459,14 @@ export const appendAssistantResponseThunk =
const insertAtIndex = existingMessageIndex !== -1 ? existingMessageIndex + 1 : currentTopicMessageIds.length
// 4. Update Database (Save the stub to the topic's message list)
await saveMessageAndBlocksToDB(newAssistantStub, [], insertAtIndex)
await saveMessageAndBlocksToDB(newAssistantMessageStub, [], insertAtIndex)
dispatch(newMessagesActions.insertMessageAtIndex({ topicId, message: newAssistantStub, index: insertAtIndex }))
dispatch(
newMessagesActions.insertMessageAtIndex({ topicId, message: newAssistantMessageStub, index: insertAtIndex })
)
dispatch(updateMessageAndBlocksThunk(topicId, { id: existingAssistantMessageId, foldSelected: false }, []))
dispatch(updateMessageAndBlocksThunk(topicId, { id: newAssistantMessageStub.id, foldSelected: true }, []))
// 5. Prepare and queue the processing task
const assistantConfigForThisCall = {
@@ -1475,7 +1480,7 @@ export const appendAssistantResponseThunk =
getState,
topicId,
assistantConfigForThisCall,
newAssistantStub // Pass the newly created stub
newAssistantMessageStub // Pass the newly created stub
)
})
} catch (error) {
+5 -2
View File
@@ -11,7 +11,8 @@ export const ProviderTypeSchema = z.enum([
'mistral',
'aws-bedrock',
'vertex-anthropic',
'new-api'
'new-api',
'ai-gateway'
])
export type ProviderType = z.infer<typeof ProviderTypeSchema>
@@ -176,7 +177,9 @@ export const SystemProviderIds = {
poe: 'poe',
aionly: 'aionly',
longcat: 'longcat',
huggingface: 'huggingface'
huggingface: 'huggingface',
'ai-gateway': 'ai-gateway',
cerebras: 'cerebras'
} as const
export type SystemProviderId = keyof typeof SystemProviderIds
+1
View File
@@ -97,6 +97,7 @@ export type ReasoningEffortOptionalParams = {
}
}
}
disable_reasoning?: boolean
// Add any other potential reasoning-related keys here if they exist
}
+55
View File
@@ -0,0 +1,55 @@
/**
* Simple wrapper for scrollIntoView with common default options.
* Provides a unified interface with sensible defaults.
*
* @param element - The target element to scroll into view
* @param options - Scroll options. If not provided, uses { behavior: 'smooth', block: 'center', inline: 'nearest' }
*/
export function scrollIntoView(element: HTMLElement, options?: ScrollIntoViewOptions): void {
const defaultOptions: ScrollIntoViewOptions = {
behavior: 'smooth',
block: 'center',
inline: 'nearest'
}
element.scrollIntoView(options ?? defaultOptions)
}
/**
* Intelligently scrolls an element into view at the center position.
* Prioritizes scrolling within the specified container to avoid scrolling the entire page.
*
* @param element - The target element to scroll into view
* @param scrollContainer - Optional scroll container. If provided and scrollable, scrolling happens within it; otherwise uses browser default scrolling
* @param behavior - Scroll behavior, defaults to 'smooth'
*/
export function scrollElementIntoView(
element: HTMLElement,
scrollContainer?: HTMLElement | null,
behavior: ScrollBehavior = 'smooth'
): void {
if (!scrollContainer) {
// No container specified, use browser default scrolling
scrollIntoView(element, { behavior, block: 'center', inline: 'nearest' })
return
}
// Check if container is scrollable
const canScroll =
scrollContainer.scrollHeight > scrollContainer.clientHeight ||
scrollContainer.scrollWidth > scrollContainer.clientWidth
if (canScroll) {
// Container is scrollable, scroll within the container
const containerRect = scrollContainer.getBoundingClientRect()
const elRect = element.getBoundingClientRect()
// Calculate element's scrollable offset position relative to the container
const elementTopWithinContainer = elRect.top - containerRect.top + scrollContainer.scrollTop
const desiredTop = elementTopWithinContainer - Math.max(0, scrollContainer.clientHeight - elRect.height) / 2
scrollContainer.scrollTo({ top: Math.max(0, desiredTop), behavior })
} else {
// Container is not scrollable, fallback to browser default scrolling
scrollIntoView(element, { behavior, block: 'center', inline: 'nearest' })
}
}
+1
View File
@@ -214,6 +214,7 @@ export function uniqueObjectArray<T>(array: T[]): T[] {
export * from './api'
export * from './collection'
export * from './dataLimit'
export * from './dom'
export * from './file'
export * from './image'
export * from './json'
+54 -1
View File
@@ -127,6 +127,19 @@ __metadata:
languageName: node
linkType: hard
"@ai-sdk/cerebras@npm:^1.0.31":
version: 1.0.31
resolution: "@ai-sdk/cerebras@npm:1.0.31"
dependencies:
"@ai-sdk/openai-compatible": "npm:1.0.27"
"@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.17"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/0723f0041b767acfb7a9d903d51d5c95af83c31c89b83f242cb5c02a076d8b98f6567334eb32dcdbc8565b55ded2aa5195ca68612bbe7b13e68253cf4ef412d6
languageName: node
linkType: hard
"@ai-sdk/deepseek@npm:^1.0.27":
version: 1.0.27
resolution: "@ai-sdk/deepseek@npm:1.0.27"
@@ -153,6 +166,19 @@ __metadata:
languageName: node
linkType: hard
"@ai-sdk/gateway@npm:^2.0.9":
version: 2.0.9
resolution: "@ai-sdk/gateway@npm:2.0.9"
dependencies:
"@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.17"
"@vercel/oidc": "npm:3.0.3"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/840f94795b96c0fa6e73897ea8dba95fc78af1f8482f3b7d8439b6233b4f4de6979a8b67206f4bbf32649baf2acfb1153a46792dfa20259ca9f5fd214fb25fa5
languageName: node
linkType: hard
"@ai-sdk/google-vertex@npm:^3.0.62":
version: 3.0.62
resolution: "@ai-sdk/google-vertex@npm:3.0.62"
@@ -242,6 +268,18 @@ __metadata:
languageName: node
linkType: hard
"@ai-sdk/openai-compatible@npm:1.0.27":
version: 1.0.27
resolution: "@ai-sdk/openai-compatible@npm:1.0.27"
dependencies:
"@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.17"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/9f656e4f2ea4d714dc05be588baafd962b2e0360e9195fef373e745efeb20172698ea87e1033c0c5e1f1aa6e0db76a32629427bc8433eb42bd1a0ee00e04af0c
languageName: node
linkType: hard
"@ai-sdk/openai-compatible@npm:^1.0.19":
version: 1.0.19
resolution: "@ai-sdk/openai-compatible@npm:1.0.19"
@@ -316,7 +354,7 @@ __metadata:
languageName: node
linkType: hard
"@ai-sdk/provider-utils@npm:3.0.17, @ai-sdk/provider-utils@npm:^3.0.10, @ai-sdk/provider-utils@npm:^3.0.12":
"@ai-sdk/provider-utils@npm:3.0.17, @ai-sdk/provider-utils@npm:^3.0.10":
version: 3.0.17
resolution: "@ai-sdk/provider-utils@npm:3.0.17"
dependencies:
@@ -329,6 +367,19 @@ __metadata:
languageName: node
linkType: hard
"@ai-sdk/provider-utils@npm:^3.0.12":
version: 3.0.12
resolution: "@ai-sdk/provider-utils@npm:3.0.12"
dependencies:
"@ai-sdk/provider": "npm:2.0.0"
"@standard-schema/spec": "npm:^1.0.0"
eventsource-parser: "npm:^3.0.5"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/83886bf188cad0cc655b680b710a10413989eaba9ec59dd24a58b985c02a8a1d50ad0f96dd5259385c07592ec3c37a7769fdf4a1ef569a73c9edbdb2cd585915
languageName: node
linkType: hard
"@ai-sdk/provider@npm:2.0.0, @ai-sdk/provider@npm:^2.0.0":
version: 2.0.0
resolution: "@ai-sdk/provider@npm:2.0.0"
@@ -9879,6 +9930,8 @@ __metadata:
"@agentic/searxng": "npm:^7.3.3"
"@agentic/tavily": "npm:^7.3.3"
"@ai-sdk/amazon-bedrock": "npm:^3.0.53"
"@ai-sdk/cerebras": "npm:^1.0.31"
"@ai-sdk/gateway": "npm:^2.0.9"
"@ai-sdk/google-vertex": "npm:^3.0.62"
"@ai-sdk/huggingface": "patch:@ai-sdk/huggingface@npm%3A0.0.8#~/.yarn/patches/@ai-sdk-huggingface-npm-0.0.8-d4d0aaac93.patch"
"@ai-sdk/mistral": "npm:^2.0.23"