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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,6 @@
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
import type { MinAppType } from '@renderer/types'
import type { FC } from 'react'
import styled from 'styled-components'
interface Props {
app: MinAppType
@ -11,31 +10,52 @@ interface Props {
}
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)
if (!_app) {
return null
// If found in DEFAULT_MIN_APPS, use predefined styling
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 (
<Container
src={_app.logo}
style={{
border: _app.bodered ? '0.5px solid var(--color-border)' : 'none',
width: `${size}px`,
height: `${size}px`,
backgroundColor: _app.background,
...(sidebar ? {} : app.style),
...style
}}
/>
)
// If not found in DEFAULT_MIN_APPS but app has logo, use it (for temporary apps)
if (app.logo) {
return (
<img
src={app.logo}
className="select-none rounded-2xl"
style={{
border: 'none',
width: `${size}px`,
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

View File

@ -1,15 +1,11 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`MinAppIcon > should render correctly with various props 1`] = `
.c0 {
border-radius: 16px;
user-select: none;
-webkit-user-drag: none;
}
<img
class="c0"
alt="Test App"
class="select-none rounded-2xl"
draggable="false"
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}`,
type: 'group',
name: getFancyProviderName(p),
actions: (
actions: p.id !== 'cherryai' && (
<Tooltip title={t('navigate.provider_settings')} mouseEnterDelay={0.5} mouseLeaveDelay={0}>
<Settings2
size={12}

View File

@ -1,5 +1,6 @@
import { PlusOutlined } from '@ant-design/icons'
import { Sortable, useDndReorder } from '@cherrystudio/ui'
import { loggerService } from '@logger'
import HorizontalScrollContainer from '@renderer/components/HorizontalScrollContainer'
import { isMac } from '@renderer/config/constant'
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 type { Tab } 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 { ThemeMode } from '@shared/data/preference/preferenceTypes'
import { Tooltip } from 'antd'
import type { LRUCache } from 'lru-cache'
import {
FileSearch,
Folder,
@ -45,14 +48,40 @@ interface TabsContainerProps {
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)
if (tabId.startsWith('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) {
return <MinAppIcon size={14} app={app} />
}
// Fallback: If no app found (cache evicted), show default icon
return <LayoutGrid size={14} />
}
switch (tabId) {
@ -94,7 +123,7 @@ const TabsContainer: React.FC<TabsContainerProps> = ({ children }) => {
const activeTabId = useAppSelector((state) => state.tabs.activeTabId)
const isFullscreen = useFullscreen()
const { settedTheme, toggleTheme } = useTheme()
const { hideMinappPopup } = useMinappPopup()
const { hideMinappPopup, minAppsCache } = useMinappPopup()
const { minapps } = useMinapps()
const { t } = useTranslation()
@ -112,8 +141,23 @@ const TabsContainer: React.FC<TabsContainerProps> = ({ children }) => {
// Check if it's a minapp tab
if (tabId.startsWith('apps:')) {
const appId = tabId.replace('apps:', '')
const app = [...DEFAULT_MIN_APPS, ...minapps].find((app) => app.id === appId)
return app ? app.name : 'MinApp'
let app = [...DEFAULT_MIN_APPS, ...minapps].find((app) => app.id === appId)
// 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)
}
@ -196,7 +240,7 @@ const TabsContainer: React.FC<TabsContainerProps> = ({ children }) => {
renderItem={(tab) => (
<Tab key={tab.id} active={tab.id === activeTabId} onClick={() => handleTabClick(tab)}>
<TabHeader>
{tab.id && <TabIcon>{getTabIcon(tab.id, minapps)}</TabIcon>}
{tab.id && <TabIcon>{getTabIcon(tab.id, minapps, minAppsCache)}</TabIcon>}
<TabTitle>{getTabTitle(tab.id)}</TabTitle>
</TabHeader>
{tab.id !== 'home' && (
@ -259,7 +303,7 @@ const TabsBar = styled.div<{ $isFullscreen: boolean }>`
flex-direction: row;
align-items: center;
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')};
height: var(--navbar-height);
min-height: ${({ $isFullscreen }) => (!$isFullscreen && isMac ? 'env(titlebar-area-height)' : '')};

View File

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

View File

@ -131,16 +131,6 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
isSystem: true,
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: {
id: 'alayanew',
name: 'AlayaNew',
@ -151,16 +141,6 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
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
},
dmxapi: {
id: 'dmxapi',
name: 'DMXAPI',
@ -171,6 +151,16 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
isSystem: true,
enabled: false
},
aionly: {
id: 'aionly',
name: 'AIOnly',
type: 'openai',
apiKey: '',
apiHost: 'https://api.aiionly.com',
models: SYSTEM_MODELS.aionly,
isSystem: true,
enabled: false
},
burncloud: {
id: 'burncloud',
name: 'BurnCloud',
@ -231,6 +221,26 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
isSystem: true,
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: {
id: 'openrouter',
name: 'OpenRouter',
@ -605,16 +615,6 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
models: SYSTEM_MODELS['poe'],
isSystem: true,
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
@ -1368,3 +1368,7 @@ const SUPPORT_GEMINI_NATIVE_WEB_SEARCH_PROVIDERS = ['gemini', 'vertexai'] as con
export const isGeminiWebSearchProvider = (provider: Provider) => {
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 { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
import { useMinapps } from '@renderer/hooks/useMinapps'
import NavigationService from '@renderer/services/NavigationService'
import TabsService from '@renderer/services/TabsService'
import type { MinAppType } from '@renderer/types'
import { clearWebviewState } from '@renderer/utils/webviewStateManager'
import { LRUCache } from 'lru-cache'
import { useCallback } from 'react'
import { useNavbarPosition } from './useNavbar'
let minAppsCache: LRUCache<string, MinAppType>
/**
@ -34,6 +37,7 @@ export const useMinappPopup = () => {
setMinappShow
} = useMinapps()
const [maxKeepAliveMinapps] = usePreference('feature.minapp.max_keep_alive')
const { isTopNavbar } = useNavbarPosition()
const createLRUCache = useCallback(() => {
return new LRUCache<string, MinAppType>({
@ -165,6 +169,33 @@ export const useMinappPopup = () => {
setMinappShow(false)
}, [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 {
openMinapp,
openMinappKeepAlive,
@ -172,6 +203,7 @@ export const useMinappPopup = () => {
closeMinapp,
hideMinappPopup,
closeAllMinapps,
openSmartMinapp,
// Expose cache instance for TabsService integration
minAppsCache
}

View File

@ -2,7 +2,7 @@ import { RowFlex } from '@cherrystudio/ui'
import { usePreference } from '@data/hooks/usePreference'
import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
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 { modelGenerating } from '@renderer/hooks/useModel'
import { useShortcut } from '@renderer/hooks/useShortcuts'
@ -84,7 +84,14 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
)}
</AnimatePresence>
{!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}>
<NavbarIcon onClick={() => toggleShowAssistants()}>
<PanelRightClose size={18} />
@ -104,7 +111,7 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
</AnimatePresence>
</NavbarLeft>
)}
<RowFlex className="items-center gap-1.5">
<RowFlex className="items-center gap-1.5" style={{ marginLeft: !isMac ? 16 : 0 }}>
<SelectModelButton assistant={assistant} />
</RowFlex>
<NavbarRight
@ -112,7 +119,7 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
justifyContent: 'flex-end',
flex: 1,
position: 'relative',
paddingRight: isWin || isLinux ? '144px' : '6px'
paddingRight: isWin || isLinux ? '144px' : '15px'
}}
className="home-navbar-right">
<RowFlex className="items-center gap-1.5">

View File

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

View File

@ -45,11 +45,20 @@ const MinAppPage: FC = () => {
}
}, [isTopNavbar])
// Find the app from all available apps
// Find the app from all available apps (including cached ones)
const app = useMemo(() => {
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(() => {
// If app not found, redirect to apps list

View File

@ -113,7 +113,7 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
const targetScrollTop = elementOffsetTop - (containerHeight - elementHeight) / 2
scrollContainer.scrollTo({
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
const newPainting = getNewPainting()
const addedPainting = addPainting('zhipu_paintings', newPainting)
@ -340,12 +340,12 @@ const ZhipuPage: FC<{ Options: string[] }> = ({ Options }) => {
return (
<Container>
<Navbar>
<NavbarCenter>
<Title>{t('title.paintings')}</Title>
</NavbarCenter>
<NavbarCenter style={{ borderRight: 'none' }}>{t('paintings.title')}</NavbarCenter>
{isMac && (
<NavbarRight>
<Button type="text" icon={<PlusOutlined />} onClick={createNewPainting} disabled={generating} />
<NavbarRight style={{ justifyContent: 'flex-end' }}>
<Button size="small" className="nodrag" icon={<PlusOutlined />} onClick={handleAddPainting}>
{t('paintings.button.new.image')}
</Button>
</NavbarRight>
)}
</Navbar>
@ -480,7 +480,7 @@ const ZhipuPage: FC<{ Options: string[] }> = ({ Options }) => {
selectedPainting={painting}
onSelectPainting={onSelectPainting}
onDeletePainting={onDeletePainting}
onNewPainting={createNewPainting}
onNewPainting={handleAddPainting}
/>
</ContentContainer>
</Container>
@ -554,12 +554,6 @@ const ToolbarMenu = styled.div`
gap: 8px;
`
const Title = styled.h1`
margin: 0;
font-size: 18px;
font-weight: 600;
`
const ProviderTitleContainer = styled.div`
display: flex;
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 { Avatar, Button, Progress, Radio, Row, Tag, Tooltip } from 'antd'
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 type { FC } from 'react'
import { useEffect, useState } from 'react'
@ -38,7 +38,7 @@ const AboutSettings: FC = () => {
const { theme } = useTheme()
// const dispatch = useAppDispatch()
// const { update } = useRuntime()
const { openMinapp } = useMinappPopup()
const { openSmartMinapp } = useMinappPopup()
const { appUpdateState, updateAppUpdateState } = useAppUpdateState()
@ -87,7 +87,7 @@ const AboutSettings: FC = () => {
const showLicense = async () => {
const { appPath } = await window.api.getAppInfo()
openMinapp({
openSmartMinapp({
id: 'cherrystudio-license',
name: t('settings.about.license.title'),
url: `file://${appPath}/resources/cherry-studio/license.html`,
@ -97,7 +97,7 @@ const AboutSettings: FC = () => {
const showReleases = async () => {
const { appPath } = await window.api.getAppInfo()
openMinapp({
openSmartMinapp({
id: 'cherrystudio-releases',
name: t('settings.about.releases.title'),
url: `file://${appPath}/resources/cherry-studio/releases.html?theme=${theme === ThemeMode.dark ? 'dark' : 'light'}`,
@ -313,7 +313,7 @@ const AboutSettings: FC = () => {
<SettingDivider />
<SettingRow>
<SettingRowTitle>
<Github size={18} />
<GithubOutlined size={18} />
{t('settings.about.feedback.title')}
</SettingRowTitle>
<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 { Switch } from '@cherrystudio/ui'
import { usePreference } from '@data/hooks/usePreference'
import { AppLogo } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { Button, Space, Tooltip } from 'antd'
@ -18,7 +19,7 @@ const JoplinSettings: FC = () => {
const { t } = useTranslation()
const { theme } = useTheme()
const { openMinapp } = useMinappPopup()
const { openSmartMinapp } = useMinappPopup()
const handleJoplinTokenChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setJoplinToken(e.target.value)
@ -64,10 +65,11 @@ const JoplinSettings: FC = () => {
}
const handleJoplinHelpClick = () => {
openMinapp({
openSmartMinapp({
id: '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 { usePreference } from '@data/hooks/usePreference'
import { Client } from '@notionhq/client'
import { AppLogo } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { Button, Space, Tooltip } from 'antd'
@ -19,7 +20,7 @@ const NotionSettings: FC = () => {
const { t } = useTranslation()
const { theme } = useTheme()
const { openMinapp } = useMinappPopup()
const { openSmartMinapp } = useMinappPopup()
const handleNotionTokenChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNotionApiKey(e.target.value)
@ -60,10 +61,11 @@ const NotionSettings: FC = () => {
}
const handleNotionTitleClick = () => {
openMinapp({
openSmartMinapp({
id: '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 { S3BackupModal, useS3BackupModal } from '@renderer/components/S3Modals'
import Selector from '@renderer/components/Selector'
import { AppLogo } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { startAutoSync, stopAutoSync } from '@renderer/services/BackupService'
@ -33,7 +34,7 @@ const S3Settings: FC = () => {
const { theme } = useTheme()
const { t } = useTranslation()
const { openMinapp } = useMinappPopup()
const { openSmartMinapp } = useMinappPopup()
const { s3Sync } = useAppSelector((state) => state.backup)
@ -49,10 +50,11 @@ const S3Settings: FC = () => {
}
const handleTitleClick = () => {
openMinapp({
openSmartMinapp({
id: 's3-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 { usePreference } from '@data/hooks/usePreference'
import { loggerService } from '@logger'
import { AppLogo } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { Button, Space, Tooltip } from 'antd'
@ -19,7 +20,7 @@ const SiyuanSettings: FC = () => {
const [siyuanBoxId, setSiyuanBoxId] = usePreference('data.integration.siyuan.box_id')
const [siyuanRootPath, setSiyuanRootPath] = usePreference('data.integration.siyuan.root_path')
const { openMinapp } = useMinappPopup()
const { openSmartMinapp } = useMinappPopup()
const { t } = useTranslation()
const { theme } = useTheme()
@ -40,10 +41,11 @@ const SiyuanSettings: FC = () => {
}
const handleSiyuanHelpClick = () => {
openMinapp({
openSmartMinapp({
id: '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 { RowFlex } from '@cherrystudio/ui'
import { usePreference } from '@data/hooks/usePreference'
import { AppLogo } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { Button, Space, Tooltip } from 'antd'
@ -13,7 +14,7 @@ import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle
const YuqueSettings: FC = () => {
const { t } = useTranslation()
const { theme } = useTheme()
const { openMinapp } = useMinappPopup()
const { openSmartMinapp } = useMinappPopup()
const [yuqueToken, setYuqueToken] = usePreference('data.integration.yuque.token')
const [yuqueUrl, setYuqueUrl] = usePreference('data.integration.yuque.url')
@ -63,10 +64,11 @@ const YuqueSettings: FC = () => {
}
const handleYuqueHelpClick = () => {
openMinapp({
openSmartMinapp({
id: '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;
height: 125px;
opacity: ${(props) => (props.$isActive ? 1 : 0.6)};
width: calc(100vw - var(--settings-width) - 40px);
width: 100%;
&:hover {
opacity: 1;

View File

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

View File

@ -19,6 +19,7 @@ import {
isVisionModel,
isWebSearchModel
} from '@renderer/config/models'
import { isNewApiProvider } from '@renderer/config/providers'
import { useDynamicLabelWidth } from '@renderer/hooks/useDynamicLabelWidth'
import type { Model, ModelCapability, ModelType, Provider } from '@renderer/types'
import { getDefaultGroupName, getDifference, getUnion, uniqueObjectArray } from '@renderer/utils'
@ -69,7 +70,7 @@ const ModelEditContent: FC<ModelEditContentProps & ModalProps> = ({ provider, mo
id: formValues.id || model.id,
name: formValues.name || model.name,
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,
supported_text_delta: overrides?.supported_text_delta ?? supportedTextDelta,
pricing: {
@ -88,7 +89,7 @@ const ModelEditContent: FC<ModelEditContentProps & ModalProps> = ({ provider, mo
id: values.id || model.id,
name: values.name || model.name,
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,
supported_text_delta: supportedTextDelta,
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}>
<Form
form={form}
labelCol={{ flex: provider.id === 'new-api' ? labelWidth : '110px' }}
labelCol={{ flex: isNewApiProvider(provider) ? labelWidth : '110px' }}
labelAlign="left"
colon={false}
style={{ marginTop: 15 }}
@ -300,7 +301,7 @@ const ModelEditContent: FC<ModelEditContentProps & ModalProps> = ({ provider, mo
tooltip={t('settings.models.add.group_name.tooltip')}>
<Input placeholder={t('settings.models.add.group_name.placeholder')} spellCheck={false} />
</Form.Item>
{provider.id === 'new-api' && (
{isNewApiProvider(provider) && (
<Form.Item
name="endpointType"
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 { DynamicVirtualList } from '@renderer/components/VirtualList'
import { getModelLogo } from '@renderer/config/models'
import { isNewApiProvider } from '@renderer/config/providers'
import FileItem from '@renderer/pages/files/FileItem'
import NewApiBatchAddModelPopup from '@renderer/pages/settings/ProviderSettings/ModelList/NewApiBatchAddModelPopup'
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))
if (provider.id === 'new-api') {
if (isNewApiProvider(provider)) {
if (wouldAddModels.every(isValidNewApiModel)) {
wouldAddModels.forEach(onAddModel)
} else {

View File

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

View File

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

View File

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

View File

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