Merge branch 'main' of github.com:CherryHQ/cherry-studio into v2

This commit is contained in:
fullex 2025-09-24 22:30:56 +08:00
commit 8a9b633af2
38 changed files with 330 additions and 163 deletions

22
.github/workflows/delete-branch.yml vendored Normal file
View File

@ -0,0 +1,22 @@
name: Delete merged branch
on:
pull_request:
types:
- closed
jobs:
delete-branch:
runs-on: ubuntu-latest
permissions:
contents: write
if: github.event.pull_request.merged == true && github.event.pull_request.head.repo.full_name == github.repository
steps:
- name: Delete merged branch
uses: actions/github-script@v7
with:
script: |
github.rest.git.deleteRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `heads/${context.payload.pull_request.head.ref}`,
})

View File

@ -128,16 +128,13 @@ afterSign: scripts/notarize.js
artifactBuildCompleted: scripts/artifact-build-completed.js artifactBuildCompleted: scripts/artifact-build-completed.js
releaseInfo: releaseInfo:
releaseNotes: | releaseNotes: |
✨ 新功能: 🎨 界面优化:
- 新增 CherryIN 服务商 - 优化了多个组件的布局和间距,提升视觉体验
- 新增 AiOnly AI 服务商 - 改进了导航栏和标签栏的样式显示
- 更新 MCP 服务器卡片布局和样式,改为列表视图 - MCP 服务器卡片宽度调整为 100%,提高响应式布局效果
- 优化了笔记侧边栏的滚动行为
🐛 问题修复: 🐛 问题修复:
- 修复 QwenMT 模型的翻译内容处理逻辑 - 修复了小应用打开功能无法正常工作的问题
- 修复无法将外部笔记添加到知识库的问题 - 修复了助手更新时 ID 丢失导致更新失败的问题
- 确保助手更新时 ID 字段为必填项,防止数据错误
🚀 性能优化:
- 提升输入框响应速度
- 优化模型切换性能
- 改进翻译功能的引用和邮件格式处理

View File

@ -18,6 +18,7 @@ interface ItemRendererProps<T> {
transform?: Transform | null transform?: Transform | null
transition?: string | null transition?: string | null
listeners?: DraggableSyntheticListeners listeners?: DraggableSyntheticListeners
itemStyle?: React.CSSProperties
} }
export function ItemRenderer<T>({ export function ItemRenderer<T>({
@ -31,6 +32,7 @@ export function ItemRenderer<T>({
transform, transform,
transition, transition,
listeners, listeners,
itemStyle,
...props ...props
}: ItemRendererProps<T>) { }: ItemRendererProps<T>) {
useEffect(() => { useEffect(() => {
@ -45,13 +47,17 @@ export function ItemRenderer<T>({
} }
}, [dragOverlay]) }, [dragOverlay])
const wrapperStyle = { const style = {
transition, transition,
transform: CSS.Transform.toString(transform ?? null) transform: CSS.Transform.toString(transform ?? null)
} as React.CSSProperties } as React.CSSProperties
return ( return (
<ItemWrapper ref={ref} data-index={index} className={cn({ dragOverlay: dragOverlay })} style={{ ...wrapperStyle }}> <ItemWrapper
ref={ref}
data-index={index}
className={cn({ dragOverlay: dragOverlay })}
style={{ ...style, ...itemStyle }}>
<DraggableItem <DraggableItem
className={cn({ dragging: dragging, dragOverlay: dragOverlay, ghost: ghost })} className={cn({ dragging: dragging, dragOverlay: dragOverlay, ghost: ghost })}
{...listeners} {...listeners}

View File

@ -65,6 +65,8 @@ interface SortableProps<T> {
className?: string className?: string
/** Item list style */ /** Item list style */
listStyle?: React.CSSProperties listStyle?: React.CSSProperties
/** Item style */
itemStyle?: React.CSSProperties
/** Item gap */ /** Item gap */
gap?: number | string gap?: number | string
/** Restrictions, shortcuts for some modifiers */ /** Restrictions, shortcuts for some modifiers */
@ -91,6 +93,7 @@ function Sortable<T>({
showGhost = false, showGhost = false,
className, className,
listStyle, listStyle,
itemStyle,
gap, gap,
restrictions, restrictions,
modifiers: customModifiers modifiers: customModifiers
@ -199,19 +202,19 @@ function Sortable<T>({
renderItem={renderItem} renderItem={renderItem}
useDragOverlay={useDragOverlay} useDragOverlay={useDragOverlay}
showGhost={showGhost} showGhost={showGhost}
itemStyle={itemStyle}
/> />
))} ))}
</ListWrapper> </ListWrapper>
</SortableContext> </SortableContext>
{useDragOverlay {useDragOverlay &&
? createPortal( createPortal(
<DragOverlay adjustScale dropAnimation={dropAnimation}> <DragOverlay adjustScale dropAnimation={dropAnimation}>
{activeItem ? <ItemRenderer item={activeItem} renderItem={renderItem} dragOverlay /> : null} {activeItem && <ItemRenderer item={activeItem} renderItem={renderItem} itemStyle={itemStyle} dragOverlay />}
</DragOverlay>, </DragOverlay>,
document.body document.body
) )}
: null}
</DndContext> </DndContext>
) )
} }

View File

@ -10,6 +10,7 @@ interface SortableItemProps<T> {
renderItem: RenderItemType<T> renderItem: RenderItemType<T>
useDragOverlay?: boolean useDragOverlay?: boolean
showGhost?: boolean showGhost?: boolean
itemStyle?: React.CSSProperties
} }
export function SortableItem<T>({ export function SortableItem<T>({
@ -18,7 +19,8 @@ export function SortableItem<T>({
index, index,
renderItem, renderItem,
useDragOverlay = true, useDragOverlay = true,
showGhost = true showGhost = true,
itemStyle
}: SortableItemProps<T>) { }: SortableItemProps<T>) {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id id
@ -36,6 +38,7 @@ export function SortableItem<T>({
transform={transform} transform={transform}
transition={transition} transition={transition}
listeners={listeners} listeners={listeners}
itemStyle={itemStyle}
{...attributes} {...attributes}
/> />
) )

View File

@ -1,4 +1,5 @@
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { isNewApiProvider } from '@renderer/config/providers'
import type { Provider } from '@renderer/types' import type { Provider } from '@renderer/types'
import { AihubmixAPIClient } from './aihubmix/AihubmixAPIClient' import { AihubmixAPIClient } from './aihubmix/AihubmixAPIClient'
@ -45,7 +46,7 @@ export class ApiClientFactory {
return instance return instance
} }
if (provider.id === 'new-api') { if (isNewApiProvider(provider)) {
logger.debug(`Creating NewAPIClient for provider: ${provider.id}`) logger.debug(`Creating NewAPIClient for provider: ${provider.id}`)
instance = new NewAPIClient(provider) as BaseApiClient instance = new NewAPIClient(provider) as BaseApiClient
return instance return instance

View File

@ -67,7 +67,9 @@ vi.mock('@renderer/config/models', () => ({
silicon: [], silicon: [],
defaultModel: [] defaultModel: []
}, },
isOpenAIModel: vi.fn(() => false) isOpenAIModel: vi.fn(() => false),
glm45FlashModel: {},
qwen38bModel: {}
})) }))
describe('ApiClientFactory', () => { describe('ApiClientFactory', () => {

View File

@ -35,18 +35,8 @@ vi.mock('@renderer/config/models', () => ({
findTokenLimit: vi.fn().mockReturnValue(4096), findTokenLimit: vi.fn().mockReturnValue(4096),
isFunctionCallingModel: vi.fn().mockReturnValue(false), isFunctionCallingModel: vi.fn().mockReturnValue(false),
DEFAULT_MAX_TOKENS: 4096, DEFAULT_MAX_TOKENS: 4096,
qwen38bModel: { qwen38bModel: {},
id: 'Qwen/Qwen3-8B', glm45FlashModel: {}
name: 'Qwen3-8B',
provider: 'cherryai',
group: 'Qwen'
},
glm45FlashModel: {
id: 'glm-4.5-flash',
name: 'GLM-4.5-Flash',
provider: 'cherryai',
group: 'GLM-4.5'
}
})) }))
vi.mock('@renderer/services/AssistantService', () => ({ vi.mock('@renderer/services/AssistantService', () => ({

View File

@ -6,6 +6,7 @@ import {
type ProviderSettingsMap type ProviderSettingsMap
} from '@cherrystudio/ai-core/provider' } from '@cherrystudio/ai-core/provider'
import { isOpenAIChatCompletionOnlyModel } from '@renderer/config/models' import { isOpenAIChatCompletionOnlyModel } from '@renderer/config/models'
import { isNewApiProvider } from '@renderer/config/providers'
import { import {
getAwsBedrockAccessKeyId, getAwsBedrockAccessKeyId,
getAwsBedrockRegion, getAwsBedrockRegion,
@ -65,7 +66,7 @@ function handleSpecialProviders(model: Model, provider: Provider): Provider {
if (provider.id === 'aihubmix') { if (provider.id === 'aihubmix') {
return aihubmixProviderCreator(model, provider) return aihubmixProviderCreator(model, provider)
} }
if (provider.id === 'new-api') { if (isNewApiProvider(provider)) {
return newApiResolverCreator(model, provider) return newApiResolverCreator(model, provider)
} }
if (provider.id === 'vertexai') { if (provider.id === 'vertexai') {

View File

@ -113,6 +113,8 @@ export function getReasoningEffort(assistant: Assistant, model: Model): Reasonin
return { return {
enable_thinking: true enable_thinking: true
} }
case SystemProviderIds.hunyuan:
case SystemProviderIds['tencent-cloud-ti']:
case SystemProviderIds.doubao: case SystemProviderIds.doubao:
return { return {
thinking: { thinking: {

View File

@ -1,7 +1,6 @@
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps' import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
import type { MinAppType } from '@renderer/types' import type { MinAppType } from '@renderer/types'
import type { FC } from 'react' import type { FC } from 'react'
import styled from 'styled-components'
interface Props { interface Props {
app: MinAppType app: MinAppType
@ -11,31 +10,52 @@ interface Props {
} }
const MinAppIcon: FC<Props> = ({ app, size = 48, style, sidebar = false }) => { const MinAppIcon: FC<Props> = ({ app, size = 48, style, sidebar = false }) => {
// First try to find in DEFAULT_MIN_APPS for predefined styling
const _app = DEFAULT_MIN_APPS.find((item) => item.id === app.id) const _app = DEFAULT_MIN_APPS.find((item) => item.id === app.id)
if (!_app) { // If found in DEFAULT_MIN_APPS, use predefined styling
return null if (_app) {
return (
<img
src={_app.logo}
className="select-none rounded-2xl"
style={{
border: _app.bodered ? '0.5px solid var(--color-border)' : 'none',
width: `${size}px`,
height: `${size}px`,
backgroundColor: _app.background,
userSelect: 'none',
...(sidebar ? {} : app.style),
...style
}}
draggable={false}
alt={app.name || 'MinApp Icon'}
/>
)
} }
return ( // If not found in DEFAULT_MIN_APPS but app has logo, use it (for temporary apps)
<Container if (app.logo) {
src={_app.logo} return (
style={{ <img
border: _app.bodered ? '0.5px solid var(--color-border)' : 'none', src={app.logo}
width: `${size}px`, className="select-none rounded-2xl"
height: `${size}px`, style={{
backgroundColor: _app.background, border: 'none',
...(sidebar ? {} : app.style), width: `${size}px`,
...style height: `${size}px`,
}} backgroundColor: 'transparent',
/> userSelect: 'none',
) ...(sidebar ? {} : app.style),
...style
}}
draggable={false}
alt={app.name || 'MinApp Icon'}
/>
)
}
return null
} }
const Container = styled.img`
border-radius: 16px;
user-select: none;
-webkit-user-drag: none;
`
export default MinAppIcon export default MinAppIcon

View File

@ -1,15 +1,11 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`MinAppIcon > should render correctly with various props 1`] = ` exports[`MinAppIcon > should render correctly with various props 1`] = `
.c0 {
border-radius: 16px;
user-select: none;
-webkit-user-drag: none;
}
<img <img
class="c0" alt="Test App"
class="select-none rounded-2xl"
draggable="false"
src="/test-logo-1.png" src="/test-logo-1.png"
style="border: 0.5px solid var(--color-border); width: 64px; height: 64px; background-color: rgb(240, 240, 240); opacity: 0.8; transform: scale(1.1); margin-top: 10px;" style="border: 0.5px solid var(--color-border); width: 64px; height: 64px; background-color: rgb(240, 240, 240); user-select: none; opacity: 0.8; transform: scale(1.1); margin-top: 10px;"
/> />
`; `;

View File

@ -181,7 +181,7 @@ const PopupContainer: React.FC<Props> = ({ model, filter: baseFilter, showTagFil
key: `provider-${p.id}`, key: `provider-${p.id}`,
type: 'group', type: 'group',
name: getFancyProviderName(p), name: getFancyProviderName(p),
actions: ( actions: p.id !== 'cherryai' && (
<Tooltip title={t('navigate.provider_settings')} mouseEnterDelay={0.5} mouseLeaveDelay={0}> <Tooltip title={t('navigate.provider_settings')} mouseEnterDelay={0.5} mouseLeaveDelay={0}>
<Settings2 <Settings2
size={12} size={12}

View File

@ -1,5 +1,6 @@
import { PlusOutlined } from '@ant-design/icons' import { PlusOutlined } from '@ant-design/icons'
import { Sortable, useDndReorder } from '@cherrystudio/ui' import { Sortable, useDndReorder } from '@cherrystudio/ui'
import { loggerService } from '@logger'
import HorizontalScrollContainer from '@renderer/components/HorizontalScrollContainer' import HorizontalScrollContainer from '@renderer/components/HorizontalScrollContainer'
import { isMac } from '@renderer/config/constant' import { isMac } from '@renderer/config/constant'
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps' import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
@ -12,9 +13,11 @@ import tabsService from '@renderer/services/TabsService'
import { useAppDispatch, useAppSelector } from '@renderer/store' import { useAppDispatch, useAppSelector } from '@renderer/store'
import type { Tab } from '@renderer/store/tabs' import type { Tab } from '@renderer/store/tabs'
import { addTab, removeTab, setActiveTab, setTabs } from '@renderer/store/tabs' import { addTab, removeTab, setActiveTab, setTabs } from '@renderer/store/tabs'
import type { MinAppType } from '@renderer/types'
import { classNames } from '@renderer/utils' import { classNames } from '@renderer/utils'
import { ThemeMode } from '@shared/data/preference/preferenceTypes' import { ThemeMode } from '@shared/data/preference/preferenceTypes'
import { Tooltip } from 'antd' import { Tooltip } from 'antd'
import type { LRUCache } from 'lru-cache'
import { import {
FileSearch, FileSearch,
Folder, Folder,
@ -45,14 +48,40 @@ interface TabsContainerProps {
children: React.ReactNode children: React.ReactNode
} }
const getTabIcon = (tabId: string, minapps: any[]): React.ReactNode | undefined => { const logger = loggerService.withContext('TabContainer')
const getTabIcon = (
tabId: string,
minapps: MinAppType[],
minAppsCache?: LRUCache<string, MinAppType>
): React.ReactNode | undefined => {
// Check if it's a minapp tab (format: apps:appId) // Check if it's a minapp tab (format: apps:appId)
if (tabId.startsWith('apps:')) { if (tabId.startsWith('apps:')) {
const appId = tabId.replace('apps:', '') const appId = tabId.replace('apps:', '')
const app = [...DEFAULT_MIN_APPS, ...minapps].find((app) => app.id === appId) let app = [...DEFAULT_MIN_APPS, ...minapps].find((app) => app.id === appId)
// If not found in permanent apps, search in temporary apps cache
// The cache stores apps opened via openSmartMinapp() for top navbar mode
// These are temporary MinApps that were opened but not yet saved to user's config
// The cache is LRU (Least Recently Used) with max size from settings
// Cache validity: Apps in cache are currently active/recently used, not outdated
if (!app && minAppsCache) {
app = minAppsCache.get(appId)
// Defensive programming: If app not found in cache but tab exists,
// the cache entry may have been evicted due to LRU policy
// Log warning for debugging potential sync issues
if (!app) {
logger.warn(`MinApp ${appId} not found in cache, using fallback icon`)
}
}
if (app) { if (app) {
return <MinAppIcon size={14} app={app} /> return <MinAppIcon size={14} app={app} />
} }
// Fallback: If no app found (cache evicted), show default icon
return <LayoutGrid size={14} />
} }
switch (tabId) { switch (tabId) {
@ -94,7 +123,7 @@ const TabsContainer: React.FC<TabsContainerProps> = ({ children }) => {
const activeTabId = useAppSelector((state) => state.tabs.activeTabId) const activeTabId = useAppSelector((state) => state.tabs.activeTabId)
const isFullscreen = useFullscreen() const isFullscreen = useFullscreen()
const { settedTheme, toggleTheme } = useTheme() const { settedTheme, toggleTheme } = useTheme()
const { hideMinappPopup } = useMinappPopup() const { hideMinappPopup, minAppsCache } = useMinappPopup()
const { minapps } = useMinapps() const { minapps } = useMinapps()
const { t } = useTranslation() const { t } = useTranslation()
@ -112,8 +141,23 @@ const TabsContainer: React.FC<TabsContainerProps> = ({ children }) => {
// Check if it's a minapp tab // Check if it's a minapp tab
if (tabId.startsWith('apps:')) { if (tabId.startsWith('apps:')) {
const appId = tabId.replace('apps:', '') const appId = tabId.replace('apps:', '')
const app = [...DEFAULT_MIN_APPS, ...minapps].find((app) => app.id === appId) let app = [...DEFAULT_MIN_APPS, ...minapps].find((app) => app.id === appId)
return app ? app.name : 'MinApp'
// If not found in permanent apps, search in temporary apps cache
// This ensures temporary MinApps display proper titles while being used
// The LRU cache automatically manages app lifecycle and prevents memory leaks
if (!app && minAppsCache) {
app = minAppsCache.get(appId)
// Defensive programming: If app not found in cache but tab exists,
// the cache entry may have been evicted due to LRU policy
if (!app) {
logger.warn(`MinApp ${appId} not found in cache, using fallback title`)
}
}
// Return app name if found, otherwise use fallback with appId
return app ? app.name : `MinApp-${appId}`
} }
return getTitleLabel(tabId) return getTitleLabel(tabId)
} }
@ -196,7 +240,7 @@ const TabsContainer: React.FC<TabsContainerProps> = ({ children }) => {
renderItem={(tab) => ( renderItem={(tab) => (
<Tab key={tab.id} active={tab.id === activeTabId} onClick={() => handleTabClick(tab)}> <Tab key={tab.id} active={tab.id === activeTabId} onClick={() => handleTabClick(tab)}>
<TabHeader> <TabHeader>
{tab.id && <TabIcon>{getTabIcon(tab.id, minapps)}</TabIcon>} {tab.id && <TabIcon>{getTabIcon(tab.id, minapps, minAppsCache)}</TabIcon>}
<TabTitle>{getTabTitle(tab.id)}</TabTitle> <TabTitle>{getTabTitle(tab.id)}</TabTitle>
</TabHeader> </TabHeader>
{tab.id !== 'home' && ( {tab.id !== 'home' && (
@ -259,7 +303,7 @@ const TabsBar = styled.div<{ $isFullscreen: boolean }>`
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
gap: 5px; gap: 5px;
padding-left: ${({ $isFullscreen }) => (!$isFullscreen && isMac ? 'env(titlebar-area-x)' : '15px')}; padding-left: ${({ $isFullscreen }) => (!$isFullscreen && isMac ? 'calc(env(titlebar-area-x) + 4px)' : '15px')};
padding-right: ${({ $isFullscreen }) => ($isFullscreen ? '12px' : '0')}; padding-right: ${({ $isFullscreen }) => ($isFullscreen ? '12px' : '0')};
height: var(--navbar-height); height: var(--navbar-height);
min-height: ${({ $isFullscreen }) => (!$isFullscreen && isMac ? 'env(titlebar-area-height)' : '')}; min-height: ${({ $isFullscreen }) => (!$isFullscreen && isMac ? 'env(titlebar-area-height)' : '')};

View File

@ -88,6 +88,7 @@ const NavbarCenterContainer = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0 ${isMac ? '20px' : 0}; padding: 0 ${isMac ? '20px' : 0};
padding-left: 10px;
font-weight: bold; font-weight: bold;
color: var(--color-text-1); color: var(--color-text-1);
position: relative; position: relative;
@ -108,7 +109,8 @@ const NavbarMainContainer = styled.div<{ $isFullscreen: boolean }>`
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 0 ${isMac ? '20px' : 0}; padding-right: ${isMac ? '20px' : 0};
padding-left: 10px;
font-weight: bold; font-weight: bold;
color: var(--color-text-1); color: var(--color-text-1);
padding-right: ${({ $isFullscreen }) => ($isFullscreen ? '12px' : isWin ? '140px' : isLinux ? '120px' : '12px')}; padding-right: ${({ $isFullscreen }) => ($isFullscreen ? '12px' : isWin ? '140px' : isLinux ? '120px' : '12px')};

View File

@ -93,7 +93,17 @@ export function isSupportedThinkingTokenModel(model?: Model): boolean {
// Specifically for DeepSeek V3.1. White list for now // Specifically for DeepSeek V3.1. White list for now
if (isDeepSeekHybridInferenceModel(model)) { if (isDeepSeekHybridInferenceModel(model)) {
return ( return (
['openrouter', 'dashscope', 'modelscope', 'doubao', 'silicon', 'nvidia', 'ppio'] satisfies SystemProviderId[] [
'openrouter',
'dashscope',
'modelscope',
'doubao',
'silicon',
'nvidia',
'ppio',
'hunyuan',
'tencent-cloud-ti'
] satisfies SystemProviderId[]
).some((id) => id === model.provider) ).some((id) => id === model.provider)
} }

View File

@ -131,16 +131,6 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
isSystem: true, isSystem: true,
enabled: false enabled: false
}, },
ppio: {
id: 'ppio',
name: 'PPIO',
type: 'openai',
apiKey: '',
apiHost: 'https://api.ppinfra.com/v3/openai/',
models: SYSTEM_MODELS.ppio,
isSystem: true,
enabled: false
},
alayanew: { alayanew: {
id: 'alayanew', id: 'alayanew',
name: 'AlayaNew', name: 'AlayaNew',
@ -151,16 +141,6 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
isSystem: true, isSystem: true,
enabled: false enabled: false
}, },
qiniu: {
id: 'qiniu',
name: 'Qiniu',
type: 'openai',
apiKey: '',
apiHost: 'https://api.qnaigc.com',
models: SYSTEM_MODELS.qiniu,
isSystem: true,
enabled: false
},
dmxapi: { dmxapi: {
id: 'dmxapi', id: 'dmxapi',
name: 'DMXAPI', name: 'DMXAPI',
@ -171,6 +151,16 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
isSystem: true, isSystem: true,
enabled: false enabled: false
}, },
aionly: {
id: 'aionly',
name: 'AIOnly',
type: 'openai',
apiKey: '',
apiHost: 'https://api.aiionly.com',
models: SYSTEM_MODELS.aionly,
isSystem: true,
enabled: false
},
burncloud: { burncloud: {
id: 'burncloud', id: 'burncloud',
name: 'BurnCloud', name: 'BurnCloud',
@ -231,6 +221,26 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
isSystem: true, isSystem: true,
enabled: false enabled: false
}, },
ppio: {
id: 'ppio',
name: 'PPIO',
type: 'openai',
apiKey: '',
apiHost: 'https://api.ppinfra.com/v3/openai/',
models: SYSTEM_MODELS.ppio,
isSystem: true,
enabled: false
},
qiniu: {
id: 'qiniu',
name: 'Qiniu',
type: 'openai',
apiKey: '',
apiHost: 'https://api.qnaigc.com',
models: SYSTEM_MODELS.qiniu,
isSystem: true,
enabled: false
},
openrouter: { openrouter: {
id: 'openrouter', id: 'openrouter',
name: 'OpenRouter', name: 'OpenRouter',
@ -605,16 +615,6 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
models: SYSTEM_MODELS['poe'], models: SYSTEM_MODELS['poe'],
isSystem: true, isSystem: true,
enabled: false enabled: false
},
aionly: {
id: 'aionly',
name: 'AIOnly',
type: 'openai',
apiKey: '',
apiHost: 'https://api.aiionly.com',
models: SYSTEM_MODELS.aionly,
isSystem: true,
enabled: false
} }
} as const } as const
@ -1368,3 +1368,7 @@ const SUPPORT_GEMINI_NATIVE_WEB_SEARCH_PROVIDERS = ['gemini', 'vertexai'] as con
export const isGeminiWebSearchProvider = (provider: Provider) => { export const isGeminiWebSearchProvider = (provider: Provider) => {
return SUPPORT_GEMINI_NATIVE_WEB_SEARCH_PROVIDERS.some((id) => id === provider.id) return SUPPORT_GEMINI_NATIVE_WEB_SEARCH_PROVIDERS.some((id) => id === provider.id)
} }
export const isNewApiProvider = (provider: Provider) => {
return ['new-api', 'cherryin'].includes(provider.id)
}

View File

@ -1,12 +1,15 @@
import { usePreference } from '@data/hooks/usePreference' import { usePreference } from '@data/hooks/usePreference'
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps' import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
import { useMinapps } from '@renderer/hooks/useMinapps' import { useMinapps } from '@renderer/hooks/useMinapps'
import NavigationService from '@renderer/services/NavigationService'
import TabsService from '@renderer/services/TabsService' import TabsService from '@renderer/services/TabsService'
import type { MinAppType } from '@renderer/types' import type { MinAppType } from '@renderer/types'
import { clearWebviewState } from '@renderer/utils/webviewStateManager' import { clearWebviewState } from '@renderer/utils/webviewStateManager'
import { LRUCache } from 'lru-cache' import { LRUCache } from 'lru-cache'
import { useCallback } from 'react' import { useCallback } from 'react'
import { useNavbarPosition } from './useNavbar'
let minAppsCache: LRUCache<string, MinAppType> let minAppsCache: LRUCache<string, MinAppType>
/** /**
@ -34,6 +37,7 @@ export const useMinappPopup = () => {
setMinappShow setMinappShow
} = useMinapps() } = useMinapps()
const [maxKeepAliveMinapps] = usePreference('feature.minapp.max_keep_alive') const [maxKeepAliveMinapps] = usePreference('feature.minapp.max_keep_alive')
const { isTopNavbar } = useNavbarPosition()
const createLRUCache = useCallback(() => { const createLRUCache = useCallback(() => {
return new LRUCache<string, MinAppType>({ return new LRUCache<string, MinAppType>({
@ -165,6 +169,33 @@ export const useMinappPopup = () => {
setMinappShow(false) setMinappShow(false)
}, [minappShow, openedOneOffMinapp, setOpenedOneOffMinapp, setCurrentMinappId, setMinappShow]) }, [minappShow, openedOneOffMinapp, setOpenedOneOffMinapp, setCurrentMinappId, setMinappShow])
/** Smart open minapp that adapts to navbar position */
const openSmartMinapp = useCallback(
(config: MinAppType, keepAlive: boolean = false) => {
if (isTopNavbar) {
// For top navbar mode, need to add to cache first for temporary apps
const cacheApp = minAppsCache.get(config.id)
if (!cacheApp) {
// Add temporary app to cache so MinAppPage can find it
minAppsCache.set(config.id, config)
}
// Set current minapp and show state
setCurrentMinappId(config.id)
setMinappShow(true)
// Then navigate to the app tab using NavigationService
if (NavigationService.navigate) {
NavigationService.navigate(`/apps/${config.id}`)
}
} else {
// For side navbar, use the traditional popup system
openMinapp(config, keepAlive)
}
},
[isTopNavbar, openMinapp, setCurrentMinappId, setMinappShow]
)
return { return {
openMinapp, openMinapp,
openMinappKeepAlive, openMinappKeepAlive,
@ -172,6 +203,7 @@ export const useMinappPopup = () => {
closeMinapp, closeMinapp,
hideMinappPopup, hideMinappPopup,
closeAllMinapps, closeAllMinapps,
openSmartMinapp,
// Expose cache instance for TabsService integration // Expose cache instance for TabsService integration
minAppsCache minAppsCache
} }

View File

@ -2,7 +2,7 @@ import { RowFlex } from '@cherrystudio/ui'
import { usePreference } from '@data/hooks/usePreference' import { usePreference } from '@data/hooks/usePreference'
import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar' import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
import SearchPopup from '@renderer/components/Popups/SearchPopup' import SearchPopup from '@renderer/components/Popups/SearchPopup'
import { isLinux, isWin } from '@renderer/config/constant' import { isLinux, isMac, isWin } from '@renderer/config/constant'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
import { modelGenerating } from '@renderer/hooks/useModel' import { modelGenerating } from '@renderer/hooks/useModel'
import { useShortcut } from '@renderer/hooks/useShortcuts' import { useShortcut } from '@renderer/hooks/useShortcuts'
@ -84,7 +84,14 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
)} )}
</AnimatePresence> </AnimatePresence>
{!showAssistants && ( {!showAssistants && (
<NavbarLeft style={{ justifyContent: 'flex-start', borderRight: 'none', padding: '0 10px', minWidth: 'auto' }}> <NavbarLeft
style={{
justifyContent: 'flex-start',
borderRight: 'none',
paddingLeft: 0,
paddingRight: 10,
minWidth: 'auto'
}}>
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={0.8}> <Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={0.8}>
<NavbarIcon onClick={() => toggleShowAssistants()}> <NavbarIcon onClick={() => toggleShowAssistants()}>
<PanelRightClose size={18} /> <PanelRightClose size={18} />
@ -104,7 +111,7 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
</AnimatePresence> </AnimatePresence>
</NavbarLeft> </NavbarLeft>
)} )}
<RowFlex className="items-center gap-1.5"> <RowFlex className="items-center gap-1.5" style={{ marginLeft: !isMac ? 16 : 0 }}>
<SelectModelButton assistant={assistant} /> <SelectModelButton assistant={assistant} />
</RowFlex> </RowFlex>
<NavbarRight <NavbarRight
@ -112,7 +119,7 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
justifyContent: 'flex-end', justifyContent: 'flex-end',
flex: 1, flex: 1,
position: 'relative', position: 'relative',
paddingRight: isWin || isLinux ? '144px' : '6px' paddingRight: isWin || isLinux ? '144px' : '15px'
}} }}
className="home-navbar-right"> className="home-navbar-right">
<RowFlex className="items-center gap-1.5"> <RowFlex className="items-center gap-1.5">

View File

@ -394,7 +394,7 @@ const SettingsTab: FC<Props> = (props) => {
<SettingDivider /> <SettingDivider />
</SettingGroup> </SettingGroup>
</CollapsibleSettingGroup> </CollapsibleSettingGroup>
<CollapsibleSettingGroup title={t('settings.math.title')} defaultExpanded={true}> <CollapsibleSettingGroup title={t('settings.math.title')} defaultExpanded={false}>
<SettingGroup> <SettingGroup>
<SettingRow> <SettingRow>
{/* <SettingRowTitleSmall>{t('settings.math.engine.label')}</SettingRowTitleSmall> */} {/* <SettingRowTitleSmall>{t('settings.math.engine.label')}</SettingRowTitleSmall> */}
@ -421,7 +421,7 @@ const SettingsTab: FC<Props> = (props) => {
<SettingDivider /> <SettingDivider />
</SettingGroup> </SettingGroup>
</CollapsibleSettingGroup> </CollapsibleSettingGroup>
<CollapsibleSettingGroup title={t('chat.settings.code.title')} defaultExpanded={true}> <CollapsibleSettingGroup title={t('chat.settings.code.title')} defaultExpanded={false}>
<SettingGroup> <SettingGroup>
<SettingRow> <SettingRow>
{/* <SettingRowTitleSmall>{t('message.message.code_style')}</SettingRowTitleSmall> */} {/* <SettingRowTitleSmall>{t('message.message.code_style')}</SettingRowTitleSmall> */}
@ -551,7 +551,7 @@ const SettingsTab: FC<Props> = (props) => {
</SettingGroup> </SettingGroup>
<SettingDivider /> <SettingDivider />
</CollapsibleSettingGroup> </CollapsibleSettingGroup>
<CollapsibleSettingGroup title={t('settings.messages.input.title')} defaultExpanded={true}> <CollapsibleSettingGroup title={t('settings.messages.input.title')} defaultExpanded={false}>
<SettingGroup> <SettingGroup>
<SettingRow> <SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.show_estimated_tokens')}</SettingRowTitleSmall> <SettingRowTitleSmall>{t('settings.messages.input.show_estimated_tokens')}</SettingRowTitleSmall>

View File

@ -45,11 +45,20 @@ const MinAppPage: FC = () => {
} }
}, [isTopNavbar]) }, [isTopNavbar])
// Find the app from all available apps // Find the app from all available apps (including cached ones)
const app = useMemo(() => { const app = useMemo(() => {
if (!appId) return null if (!appId) return null
return [...DEFAULT_MIN_APPS, ...minapps].find((app) => app.id === appId)
}, [appId, minapps]) // First try to find in default and custom mini-apps
let foundApp = [...DEFAULT_MIN_APPS, ...minapps].find((app) => app.id === appId)
// If not found and we have cache, try to find in cache (for temporary apps)
if (!foundApp && minAppsCache) {
foundApp = minAppsCache.get(appId)
}
return foundApp
}, [appId, minapps, minAppsCache])
useEffect(() => { useEffect(() => {
// If app not found, redirect to apps list // If app not found, redirect to apps list

View File

@ -113,7 +113,7 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
const targetScrollTop = elementOffsetTop - (containerHeight - elementHeight) / 2 const targetScrollTop = elementOffsetTop - (containerHeight - elementHeight) / 2
scrollContainer.scrollTo({ scrollContainer.scrollTo({
top: Math.max(0, targetScrollTop), top: Math.max(0, targetScrollTop),
behavior: 'smooth' behavior: 'instant'
}) })
} }
} }

View File

@ -305,7 +305,7 @@ const ZhipuPage: FC<{ Options: string[] }> = ({ Options }) => {
} }
} }
const createNewPainting = () => { const handleAddPainting = () => {
if (generating) return if (generating) return
const newPainting = getNewPainting() const newPainting = getNewPainting()
const addedPainting = addPainting('zhipu_paintings', newPainting) const addedPainting = addPainting('zhipu_paintings', newPainting)
@ -340,12 +340,12 @@ const ZhipuPage: FC<{ Options: string[] }> = ({ Options }) => {
return ( return (
<Container> <Container>
<Navbar> <Navbar>
<NavbarCenter> <NavbarCenter style={{ borderRight: 'none' }}>{t('paintings.title')}</NavbarCenter>
<Title>{t('title.paintings')}</Title>
</NavbarCenter>
{isMac && ( {isMac && (
<NavbarRight> <NavbarRight style={{ justifyContent: 'flex-end' }}>
<Button type="text" icon={<PlusOutlined />} onClick={createNewPainting} disabled={generating} /> <Button size="small" className="nodrag" icon={<PlusOutlined />} onClick={handleAddPainting}>
{t('paintings.button.new.image')}
</Button>
</NavbarRight> </NavbarRight>
)} )}
</Navbar> </Navbar>
@ -480,7 +480,7 @@ const ZhipuPage: FC<{ Options: string[] }> = ({ Options }) => {
selectedPainting={painting} selectedPainting={painting}
onSelectPainting={onSelectPainting} onSelectPainting={onSelectPainting}
onDeletePainting={onDeletePainting} onDeletePainting={onDeletePainting}
onNewPainting={createNewPainting} onNewPainting={handleAddPainting}
/> />
</ContentContainer> </ContentContainer>
</Container> </Container>
@ -554,12 +554,6 @@ const ToolbarMenu = styled.div`
gap: 8px; gap: 8px;
` `
const Title = styled.h1`
margin: 0;
font-size: 18px;
font-weight: 600;
`
const ProviderTitleContainer = styled.div` const ProviderTitleContainer = styled.div`
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;

View File

@ -16,7 +16,7 @@ import { UpgradeChannel } from '@shared/data/preference/preferenceTypes'
import { ThemeMode } from '@shared/data/preference/preferenceTypes' import { ThemeMode } from '@shared/data/preference/preferenceTypes'
import { Avatar, Button, Progress, Radio, Row, Tag, Tooltip } from 'antd' import { Avatar, Button, Progress, Radio, Row, Tag, Tooltip } from 'antd'
import { debounce } from 'lodash' import { debounce } from 'lodash'
import { Bug, FileCheck, Github, Globe, Mail, Rss } from 'lucide-react' import { Bug, FileCheck, Globe, Mail, Rss } from 'lucide-react'
import { BadgeQuestionMark } from 'lucide-react' import { BadgeQuestionMark } from 'lucide-react'
import type { FC } from 'react' import type { FC } from 'react'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
@ -38,7 +38,7 @@ const AboutSettings: FC = () => {
const { theme } = useTheme() const { theme } = useTheme()
// const dispatch = useAppDispatch() // const dispatch = useAppDispatch()
// const { update } = useRuntime() // const { update } = useRuntime()
const { openMinapp } = useMinappPopup() const { openSmartMinapp } = useMinappPopup()
const { appUpdateState, updateAppUpdateState } = useAppUpdateState() const { appUpdateState, updateAppUpdateState } = useAppUpdateState()
@ -87,7 +87,7 @@ const AboutSettings: FC = () => {
const showLicense = async () => { const showLicense = async () => {
const { appPath } = await window.api.getAppInfo() const { appPath } = await window.api.getAppInfo()
openMinapp({ openSmartMinapp({
id: 'cherrystudio-license', id: 'cherrystudio-license',
name: t('settings.about.license.title'), name: t('settings.about.license.title'),
url: `file://${appPath}/resources/cherry-studio/license.html`, url: `file://${appPath}/resources/cherry-studio/license.html`,
@ -97,7 +97,7 @@ const AboutSettings: FC = () => {
const showReleases = async () => { const showReleases = async () => {
const { appPath } = await window.api.getAppInfo() const { appPath } = await window.api.getAppInfo()
openMinapp({ openSmartMinapp({
id: 'cherrystudio-releases', id: 'cherrystudio-releases',
name: t('settings.about.releases.title'), name: t('settings.about.releases.title'),
url: `file://${appPath}/resources/cherry-studio/releases.html?theme=${theme === ThemeMode.dark ? 'dark' : 'light'}`, url: `file://${appPath}/resources/cherry-studio/releases.html?theme=${theme === ThemeMode.dark ? 'dark' : 'light'}`,
@ -313,7 +313,7 @@ const AboutSettings: FC = () => {
<SettingDivider /> <SettingDivider />
<SettingRow> <SettingRow>
<SettingRowTitle> <SettingRowTitle>
<Github size={18} /> <GithubOutlined size={18} />
{t('settings.about.feedback.title')} {t('settings.about.feedback.title')}
</SettingRowTitle> </SettingRowTitle>
<Button onClick={() => onOpenWebsite('https://github.com/CherryHQ/cherry-studio/issues/new/choose')}> <Button onClick={() => onOpenWebsite('https://github.com/CherryHQ/cherry-studio/issues/new/choose')}>

View File

@ -2,6 +2,7 @@ import { InfoCircleOutlined } from '@ant-design/icons'
import { RowFlex } from '@cherrystudio/ui' import { RowFlex } from '@cherrystudio/ui'
import { Switch } from '@cherrystudio/ui' import { Switch } from '@cherrystudio/ui'
import { usePreference } from '@data/hooks/usePreference' import { usePreference } from '@data/hooks/usePreference'
import { AppLogo } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup' import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { Button, Space, Tooltip } from 'antd' import { Button, Space, Tooltip } from 'antd'
@ -18,7 +19,7 @@ const JoplinSettings: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { theme } = useTheme() const { theme } = useTheme()
const { openMinapp } = useMinappPopup() const { openSmartMinapp } = useMinappPopup()
const handleJoplinTokenChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleJoplinTokenChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setJoplinToken(e.target.value) setJoplinToken(e.target.value)
@ -64,10 +65,11 @@ const JoplinSettings: FC = () => {
} }
const handleJoplinHelpClick = () => { const handleJoplinHelpClick = () => {
openMinapp({ openSmartMinapp({
id: 'joplin-help', id: 'joplin-help',
name: 'Joplin Help', name: 'Joplin Help',
url: 'https://joplinapp.org/help/apps/clipper' url: 'https://joplinapp.org/help/apps/clipper',
logo: AppLogo
}) })
} }

View File

@ -3,6 +3,7 @@ import { RowFlex } from '@cherrystudio/ui'
import { Switch } from '@cherrystudio/ui' import { Switch } from '@cherrystudio/ui'
import { usePreference } from '@data/hooks/usePreference' import { usePreference } from '@data/hooks/usePreference'
import { Client } from '@notionhq/client' import { Client } from '@notionhq/client'
import { AppLogo } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup' import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { Button, Space, Tooltip } from 'antd' import { Button, Space, Tooltip } from 'antd'
@ -19,7 +20,7 @@ const NotionSettings: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { theme } = useTheme() const { theme } = useTheme()
const { openMinapp } = useMinappPopup() const { openSmartMinapp } = useMinappPopup()
const handleNotionTokenChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleNotionTokenChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNotionApiKey(e.target.value) setNotionApiKey(e.target.value)
@ -60,10 +61,11 @@ const NotionSettings: FC = () => {
} }
const handleNotionTitleClick = () => { const handleNotionTitleClick = () => {
openMinapp({ openSmartMinapp({
id: 'notion-help', id: 'notion-help',
name: 'Notion Help', name: 'Notion Help',
url: 'https://docs.cherry-ai.com/advanced-basic/notion' url: 'https://docs.cherry-ai.com/advanced-basic/notion',
logo: AppLogo
}) })
} }

View File

@ -5,6 +5,7 @@ import { usePreference } from '@data/hooks/usePreference'
import { S3BackupManager } from '@renderer/components/S3BackupManager' import { S3BackupManager } from '@renderer/components/S3BackupManager'
import { S3BackupModal, useS3BackupModal } from '@renderer/components/S3Modals' import { S3BackupModal, useS3BackupModal } from '@renderer/components/S3Modals'
import Selector from '@renderer/components/Selector' import Selector from '@renderer/components/Selector'
import { AppLogo } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup' import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { startAutoSync, stopAutoSync } from '@renderer/services/BackupService' import { startAutoSync, stopAutoSync } from '@renderer/services/BackupService'
@ -33,7 +34,7 @@ const S3Settings: FC = () => {
const { theme } = useTheme() const { theme } = useTheme()
const { t } = useTranslation() const { t } = useTranslation()
const { openMinapp } = useMinappPopup() const { openSmartMinapp } = useMinappPopup()
const { s3Sync } = useAppSelector((state) => state.backup) const { s3Sync } = useAppSelector((state) => state.backup)
@ -49,10 +50,11 @@ const S3Settings: FC = () => {
} }
const handleTitleClick = () => { const handleTitleClick = () => {
openMinapp({ openSmartMinapp({
id: 's3-help', id: 's3-help',
name: 'S3 Compatible Storage Help', name: 'S3 Compatible Storage Help',
url: 'https://docs.cherry-ai.com/data-settings/s3-compatible' url: 'https://docs.cherry-ai.com/data-settings/s3-compatible',
logo: AppLogo
}) })
} }

View File

@ -2,6 +2,7 @@ import { InfoCircleOutlined } from '@ant-design/icons'
import { RowFlex } from '@cherrystudio/ui' import { RowFlex } from '@cherrystudio/ui'
import { usePreference } from '@data/hooks/usePreference' import { usePreference } from '@data/hooks/usePreference'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { AppLogo } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup' import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { Button, Space, Tooltip } from 'antd' import { Button, Space, Tooltip } from 'antd'
@ -19,7 +20,7 @@ const SiyuanSettings: FC = () => {
const [siyuanBoxId, setSiyuanBoxId] = usePreference('data.integration.siyuan.box_id') const [siyuanBoxId, setSiyuanBoxId] = usePreference('data.integration.siyuan.box_id')
const [siyuanRootPath, setSiyuanRootPath] = usePreference('data.integration.siyuan.root_path') const [siyuanRootPath, setSiyuanRootPath] = usePreference('data.integration.siyuan.root_path')
const { openMinapp } = useMinappPopup() const { openSmartMinapp } = useMinappPopup()
const { t } = useTranslation() const { t } = useTranslation()
const { theme } = useTheme() const { theme } = useTheme()
@ -40,10 +41,11 @@ const SiyuanSettings: FC = () => {
} }
const handleSiyuanHelpClick = () => { const handleSiyuanHelpClick = () => {
openMinapp({ openSmartMinapp({
id: 'siyuan-help', id: 'siyuan-help',
name: 'Siyuan Help', name: 'Siyuan Help',
url: 'https://docs.cherry-ai.com/advanced-basic/siyuan' url: 'https://docs.cherry-ai.com/advanced-basic/siyuan',
logo: AppLogo
}) })
} }

View File

@ -1,6 +1,7 @@
import { InfoCircleOutlined } from '@ant-design/icons' import { InfoCircleOutlined } from '@ant-design/icons'
import { RowFlex } from '@cherrystudio/ui' import { RowFlex } from '@cherrystudio/ui'
import { usePreference } from '@data/hooks/usePreference' import { usePreference } from '@data/hooks/usePreference'
import { AppLogo } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup' import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { Button, Space, Tooltip } from 'antd' import { Button, Space, Tooltip } from 'antd'
@ -13,7 +14,7 @@ import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle
const YuqueSettings: FC = () => { const YuqueSettings: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { theme } = useTheme() const { theme } = useTheme()
const { openMinapp } = useMinappPopup() const { openSmartMinapp } = useMinappPopup()
const [yuqueToken, setYuqueToken] = usePreference('data.integration.yuque.token') const [yuqueToken, setYuqueToken] = usePreference('data.integration.yuque.token')
const [yuqueUrl, setYuqueUrl] = usePreference('data.integration.yuque.url') const [yuqueUrl, setYuqueUrl] = usePreference('data.integration.yuque.url')
@ -63,10 +64,11 @@ const YuqueSettings: FC = () => {
} }
const handleYuqueHelpClick = () => { const handleYuqueHelpClick = () => {
openMinapp({ openSmartMinapp({
id: 'yuque-help', id: 'yuque-help',
name: 'Yuque Help', name: 'Yuque Help',
url: 'https://www.yuque.com/settings/tokens' url: 'https://www.yuque.com/settings/tokens',
logo: AppLogo
}) })
} }

View File

@ -185,7 +185,7 @@ const CardContainer = styled.div<{ $isActive: boolean }>`
margin-bottom: 5px; margin-bottom: 5px;
height: 125px; height: 125px;
opacity: ${(props) => (props.$isActive ? 1 : 0.6)}; opacity: ${(props) => (props.$isActive ? 1 : 0.6)};
width: calc(100vw - var(--settings-width) - 40px); width: 100%;
&:hover { &:hover {
opacity: 1; opacity: 1;

View File

@ -252,7 +252,8 @@ const McpServersList: FC = () => {
onSortEnd={onSortEnd} onSortEnd={onSortEnd}
layout="list" layout="list"
horizontal={false} horizontal={false}
listStyle={{ display: 'flex', flexDirection: 'column' }} listStyle={{ display: 'flex', flexDirection: 'column', width: '100%' }}
itemStyle={{ width: '100%' }}
gap="12px" gap="12px"
restrictions={{ scrollableAncestor: true }} restrictions={{ scrollableAncestor: true }}
useDragOverlay useDragOverlay

View File

@ -19,6 +19,7 @@ import {
isVisionModel, isVisionModel,
isWebSearchModel isWebSearchModel
} from '@renderer/config/models' } from '@renderer/config/models'
import { isNewApiProvider } from '@renderer/config/providers'
import { useDynamicLabelWidth } from '@renderer/hooks/useDynamicLabelWidth' import { useDynamicLabelWidth } from '@renderer/hooks/useDynamicLabelWidth'
import type { Model, ModelCapability, ModelType, Provider } from '@renderer/types' import type { Model, ModelCapability, ModelType, Provider } from '@renderer/types'
import { getDefaultGroupName, getDifference, getUnion, uniqueObjectArray } from '@renderer/utils' import { getDefaultGroupName, getDifference, getUnion, uniqueObjectArray } from '@renderer/utils'
@ -69,7 +70,7 @@ const ModelEditContent: FC<ModelEditContentProps & ModalProps> = ({ provider, mo
id: formValues.id || model.id, id: formValues.id || model.id,
name: formValues.name || model.name, name: formValues.name || model.name,
group: formValues.group || model.group, group: formValues.group || model.group,
endpoint_type: provider.id === 'new-api' ? formValues.endpointType : model.endpoint_type, endpoint_type: isNewApiProvider(provider) ? formValues.endpointType : model.endpoint_type,
capabilities: overrides?.capabilities ?? modelCapabilities, capabilities: overrides?.capabilities ?? modelCapabilities,
supported_text_delta: overrides?.supported_text_delta ?? supportedTextDelta, supported_text_delta: overrides?.supported_text_delta ?? supportedTextDelta,
pricing: { pricing: {
@ -88,7 +89,7 @@ const ModelEditContent: FC<ModelEditContentProps & ModalProps> = ({ provider, mo
id: values.id || model.id, id: values.id || model.id,
name: values.name || model.name, name: values.name || model.name,
group: values.group || model.group, group: values.group || model.group,
endpoint_type: provider.id === 'new-api' ? values.endpointType : model.endpoint_type, endpoint_type: isNewApiProvider(provider) ? values.endpointType : model.endpoint_type,
capabilities: modelCapabilities, capabilities: modelCapabilities,
supported_text_delta: supportedTextDelta, supported_text_delta: supportedTextDelta,
pricing: { pricing: {
@ -238,7 +239,7 @@ const ModelEditContent: FC<ModelEditContentProps & ModalProps> = ({ provider, mo
<Modal title={t('models.edit')} footer={null} transitionName="animation-move-down" centered {...props}> <Modal title={t('models.edit')} footer={null} transitionName="animation-move-down" centered {...props}>
<Form <Form
form={form} form={form}
labelCol={{ flex: provider.id === 'new-api' ? labelWidth : '110px' }} labelCol={{ flex: isNewApiProvider(provider) ? labelWidth : '110px' }}
labelAlign="left" labelAlign="left"
colon={false} colon={false}
style={{ marginTop: 15 }} style={{ marginTop: 15 }}
@ -300,7 +301,7 @@ const ModelEditContent: FC<ModelEditContentProps & ModalProps> = ({ provider, mo
tooltip={t('settings.models.add.group_name.tooltip')}> tooltip={t('settings.models.add.group_name.tooltip')}>
<Input placeholder={t('settings.models.add.group_name.placeholder')} spellCheck={false} /> <Input placeholder={t('settings.models.add.group_name.placeholder')} spellCheck={false} />
</Form.Item> </Form.Item>
{provider.id === 'new-api' && ( {isNewApiProvider(provider) && (
<Form.Item <Form.Item
name="endpointType" name="endpointType"
label={t('settings.models.add.endpoint_type.label')} label={t('settings.models.add.endpoint_type.label')}

View File

@ -4,6 +4,7 @@ import ModelIdWithTags from '@renderer/components/ModelIdWithTags'
import CustomTag from '@renderer/components/Tags/CustomTag' import CustomTag from '@renderer/components/Tags/CustomTag'
import { DynamicVirtualList } from '@renderer/components/VirtualList' import { DynamicVirtualList } from '@renderer/components/VirtualList'
import { getModelLogo } from '@renderer/config/models' import { getModelLogo } from '@renderer/config/models'
import { isNewApiProvider } from '@renderer/config/providers'
import FileItem from '@renderer/pages/files/FileItem' import FileItem from '@renderer/pages/files/FileItem'
import NewApiBatchAddModelPopup from '@renderer/pages/settings/ProviderSettings/ModelList/NewApiBatchAddModelPopup' import NewApiBatchAddModelPopup from '@renderer/pages/settings/ProviderSettings/ModelList/NewApiBatchAddModelPopup'
import type { Model, Provider } from '@renderer/types' import type { Model, Provider } from '@renderer/types'
@ -92,7 +93,7 @@ const ManageModelsList: React.FC<ManageModelsListProps> = ({ modelGroups, provid
// 添加整组 // 添加整组
const wouldAddModels = models.filter((model) => !isModelInProvider(provider, model.id)) const wouldAddModels = models.filter((model) => !isModelInProvider(provider, model.id))
if (provider.id === 'new-api') { if (isNewApiProvider(provider)) {
if (wouldAddModels.every(isValidNewApiModel)) { if (wouldAddModels.every(isValidNewApiModel)) {
wouldAddModels.forEach(onAddModel) wouldAddModels.forEach(onAddModel)
} else { } else {

View File

@ -14,6 +14,7 @@ import {
isWebSearchModel, isWebSearchModel,
SYSTEM_MODELS SYSTEM_MODELS
} from '@renderer/config/models' } from '@renderer/config/models'
import { isNewApiProvider } from '@renderer/config/providers'
import { useProvider } from '@renderer/hooks/useProvider' import { useProvider } from '@renderer/hooks/useProvider'
import NewApiAddModelPopup from '@renderer/pages/settings/ProviderSettings/ModelList/NewApiAddModelPopup' import NewApiAddModelPopup from '@renderer/pages/settings/ProviderSettings/ModelList/NewApiAddModelPopup'
import NewApiBatchAddModelPopup from '@renderer/pages/settings/ProviderSettings/ModelList/NewApiBatchAddModelPopup' import NewApiBatchAddModelPopup from '@renderer/pages/settings/ProviderSettings/ModelList/NewApiBatchAddModelPopup'
@ -130,7 +131,7 @@ const PopupContainer: React.FC<Props> = ({ providerId, resolve }) => {
const onAddModel = useCallback( const onAddModel = useCallback(
(model: Model) => { (model: Model) => {
if (!isEmpty(model.name)) { if (!isEmpty(model.name)) {
if (provider.id === 'new-api') { if (isNewApiProvider(provider)) {
if (model.supported_endpoint_types && model.supported_endpoint_types.length > 0) { if (model.supported_endpoint_types && model.supported_endpoint_types.length > 0) {
addModel({ addModel({
...model, ...model,
@ -161,7 +162,7 @@ const PopupContainer: React.FC<Props> = ({ providerId, resolve }) => {
content: t('settings.models.manage.add_listed.confirm'), content: t('settings.models.manage.add_listed.confirm'),
centered: true, centered: true,
onOk: () => { onOk: () => {
if (provider.id === 'new-api') { if (isNewApiProvider(provider)) {
if (models.every(isValidNewApiModel)) { if (models.every(isValidNewApiModel)) {
wouldAddModel.forEach(onAddModel) wouldAddModel.forEach(onAddModel)
} else { } else {

View File

@ -3,7 +3,7 @@ import { Flex } from '@cherrystudio/ui'
import CollapsibleSearchBar from '@renderer/components/CollapsibleSearchBar' import CollapsibleSearchBar from '@renderer/components/CollapsibleSearchBar'
import { LoadingIcon, StreamlineGoodHealthAndWellBeing } from '@renderer/components/Icons' import { LoadingIcon, StreamlineGoodHealthAndWellBeing } from '@renderer/components/Icons'
import CustomTag from '@renderer/components/Tags/CustomTag' import CustomTag from '@renderer/components/Tags/CustomTag'
import { PROVIDER_URLS } from '@renderer/config/providers' import { isNewApiProvider, PROVIDER_URLS } from '@renderer/config/providers'
import { useProvider } from '@renderer/hooks/useProvider' import { useProvider } from '@renderer/hooks/useProvider'
import { getProviderLabel } from '@renderer/i18n/label' import { getProviderLabel } from '@renderer/i18n/label'
import { SettingHelpLink, SettingHelpText, SettingHelpTextRow, SettingSubtitle } from '@renderer/pages/settings' import { SettingHelpLink, SettingHelpText, SettingHelpTextRow, SettingSubtitle } from '@renderer/pages/settings'
@ -87,7 +87,7 @@ const ModelList: React.FC<ModelListProps> = ({ providerId }) => {
}, [provider.id]) }, [provider.id])
const onAddModel = useCallback(() => { const onAddModel = useCallback(() => {
if (provider.id === 'new-api') { if (isNewApiProvider(provider)) {
NewApiAddModelPopup.show({ title: t('settings.models.add.add_model'), provider }) NewApiAddModelPopup.show({ title: t('settings.models.add.add_model'), provider })
} else { } else {
AddModelPopup.show({ title: t('settings.models.add.add_model'), provider }) AddModelPopup.show({ title: t('settings.models.add.add_model'), provider })

View File

@ -2,6 +2,7 @@ import { Flex } from '@cherrystudio/ui'
import { TopView } from '@renderer/components/TopView' import { TopView } from '@renderer/components/TopView'
import { endpointTypeOptions } from '@renderer/config/endpointTypes' import { endpointTypeOptions } from '@renderer/config/endpointTypes'
import { isNotSupportedTextDelta } from '@renderer/config/models' import { isNotSupportedTextDelta } from '@renderer/config/models'
import { isNewApiProvider } from '@renderer/config/providers'
import { useDynamicLabelWidth } from '@renderer/hooks/useDynamicLabelWidth' import { useDynamicLabelWidth } from '@renderer/hooks/useDynamicLabelWidth'
import { useProvider } from '@renderer/hooks/useProvider' import { useProvider } from '@renderer/hooks/useProvider'
import type { EndpointType, Model, Provider } from '@renderer/types' import type { EndpointType, Model, Provider } from '@renderer/types'
@ -62,7 +63,7 @@ const PopupContainer: React.FC<Props> = ({ title, provider, resolve, model, endp
provider: provider.id, provider: provider.id,
name: values.name ? values.name : id.toUpperCase(), name: values.name ? values.name : id.toUpperCase(),
group: values.group ?? getDefaultGroupName(id), group: values.group ?? getDefaultGroupName(id),
endpoint_type: provider.id === 'new-api' ? values.endpointType : undefined endpoint_type: isNewApiProvider(provider) ? values.endpointType : undefined
} }
addModel({ ...model, supported_text_delta: !isNotSupportedTextDelta(model) }) addModel({ ...model, supported_text_delta: !isNotSupportedTextDelta(model) })

View File

@ -339,6 +339,12 @@ const TranslatePage: FC = () => {
setTargetLanguage(source) setTargetLanguage(source)
}, [couldExchangeAuto, detectedLanguage, sourceLanguage, t, targetLanguage]) }, [couldExchangeAuto, detectedLanguage, sourceLanguage, t, targetLanguage])
// Clear translation content when component mounts
useEffect(() => {
setText('')
setTranslatedContent('')
}, [setText, setTranslatedContent])
useEffect(() => { useEffect(() => {
isEmpty(text) && setTranslatedContent('') isEmpty(text) && setTranslatedContent('')
}, [setTranslatedContent, text]) }, [setTranslatedContent, text])

View File

@ -2499,6 +2499,7 @@ const migrateConfig = {
'157': (state: RootState) => { '157': (state: RootState) => {
try { try {
addProvider(state, 'aionly') addProvider(state, 'aionly')
state.llm.providers = moveProvider(state.llm.providers, 'aionly', 10)
const cherryinProvider = state.llm.providers.find((provider) => provider.id === 'cherryin') const cherryinProvider = state.llm.providers.find((provider) => provider.id === 'cherryin')