From 80e1784777a674e184d4181d9094e958f5c14da7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?George=C2=B7Dong?= <98630204+GeorgeDong32@users.noreply.github.com> Date: Tue, 14 Oct 2025 01:23:34 +0800 Subject: [PATCH 01/13] chore(ci): switch Claude action to custom endpoint (#10701) --- .github/workflows/claude-translator.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/claude-translator.yml b/.github/workflows/claude-translator.yml index 356e10d0f1..23f359021d 100644 --- a/.github/workflows/claude-translator.yml +++ b/.github/workflows/claude-translator.yml @@ -45,7 +45,7 @@ jobs: # See: https://github.com/anthropics/claude-code-action/blob/main/docs/security.md github_token: ${{ secrets.TOKEN_GITHUB_WRITE }} allowed_non_write_users: "*" - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + anthropic_api_key: ${{ secrets.CLAUDE_TRANSLATOR_APIKEY }} claude_args: "--allowed-tools Bash(gh issue:*),Bash(gh api:repos/*/issues:*),Bash(gh api:repos/*/pulls/*/reviews/*),Bash(gh api:repos/*/pulls/comments/*)" prompt: | 你是一个多语言翻译助手。你需要响应 GitHub Webhooks 中的以下四种事件: @@ -108,3 +108,5 @@ jobs: 使用以下命令获取完整信息: gh issue view ${{ github.event.issue.number }} --json title,body,comments + env: + ANTHROPIC_BASE_URL: ${{ secrets.CLAUDE_TRANSLATOR_BASEURL }} From 866e8e8734d8fba81e7963cb9bbf343566db8ad8 Mon Sep 17 00:00:00 2001 From: beyondkmp Date: Tue, 14 Oct 2025 14:04:57 +0800 Subject: [PATCH 02/13] fix: guard webview search against destroyed webviews (#10704) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🐛 fix: guard webview search against destroyed webviews * delete code * delete code --- .../minapps/components/WebviewSearch.tsx | 83 +++++++++++++++---- .../__tests__/WebviewSearch.test.tsx | 55 ++++++++++++ 2 files changed, 120 insertions(+), 18 deletions(-) diff --git a/src/renderer/src/pages/minapps/components/WebviewSearch.tsx b/src/renderer/src/pages/minapps/components/WebviewSearch.tsx index bff5605c1c..a5340ee2c0 100644 --- a/src/renderer/src/pages/minapps/components/WebviewSearch.tsx +++ b/src/renderer/src/pages/minapps/components/WebviewSearch.tsx @@ -46,15 +46,58 @@ const WebviewSearch: FC = ({ webviewRef, isWebviewReady, app setActiveIndex(0) }, []) - const stopSearch = useCallback(() => { - const target = webviewRef.current ?? attachedWebviewRef.current - if (!target) return - try { - target.stopFindInPage('clearSelection') - } catch (error) { - logger.error('stopFindInPage failed', { error }) + const ensureWebviewReady = useCallback( + (candidate: WebviewTag | null) => { + if (!candidate) return null + try { + const webContentsId = candidate.getWebContentsId?.() + if (!webContentsId) { + logger.debug('WebviewSearch: missing webContentsId before action', { appId }) + return null + } + } catch (error) { + logger.debug('WebviewSearch: getWebContentsId failed before action', { appId, error }) + return null + } + + return candidate + }, + [appId] + ) + + const stopFindOnWebview = useCallback( + (webview: WebviewTag | null) => { + const usable = ensureWebviewReady(webview) + if (!usable) return false + try { + usable.stopFindInPage('clearSelection') + return true + } catch (error) { + logger.debug('stopFindInPage failed', { appId, error }) + return false + } + }, + [appId, ensureWebviewReady] + ) + + const getUsableWebview = useCallback(() => { + const candidates = [webviewRef.current, attachedWebviewRef.current] + + for (const candidate of candidates) { + const usable = ensureWebviewReady(candidate) + if (usable) { + return usable + } } - }, [webviewRef]) + + return null + }, [ensureWebviewReady, webviewRef]) + + const stopSearch = useCallback(() => { + const target = getUsableWebview() + if (!target) return + stopFindOnWebview(target) + }, [getUsableWebview, stopFindOnWebview]) const closeSearch = useCallback(() => { setIsVisible(false) @@ -64,7 +107,7 @@ const WebviewSearch: FC = ({ webviewRef, isWebviewReady, app const performSearch = useCallback( (text: string, options?: Electron.FindInPageOptions) => { - const target = webviewRef.current ?? attachedWebviewRef.current + const target = getUsableWebview() if (!target) { logger.debug('Skip performSearch: webview not attached') return @@ -81,7 +124,7 @@ const WebviewSearch: FC = ({ webviewRef, isWebviewReady, app window.toast?.error(t('common.error')) } }, - [resetSearchState, stopSearch, t, webviewRef] + [getUsableWebview, resetSearchState, stopSearch, t] ) const handleFoundInPage = useCallback((event: Event & { result?: FoundInPageResult }) => { @@ -129,22 +172,26 @@ const WebviewSearch: FC = ({ webviewRef, isWebviewReady, app return () => { activeWebview.removeEventListener('found-in-page', handle) if (attachedWebviewRef.current === activeWebview) { - try { - activeWebview.stopFindInPage('clearSelection') - } catch (error) { - logger.error('stopFindInPage failed', { error }) - } + stopFindOnWebview(activeWebview) attachedWebviewRef.current = null } } - }, [activeWebview, handleFoundInPage]) + }, [activeWebview, handleFoundInPage, stopFindOnWebview]) useEffect(() => { if (!activeWebview) return + if (!isWebviewReady) return const onFindShortcut = window.api?.webview?.onFindShortcut if (!onFindShortcut) return - const webContentsId = activeWebview.getWebContentsId?.() + let webContentsId: number | undefined + try { + webContentsId = activeWebview.getWebContentsId?.() + } catch (error) { + logger.debug('WebviewSearch: getWebContentsId failed', { appId, error }) + return + } + if (!webContentsId) { logger.warn('WebviewSearch: missing webContentsId', { appId }) return @@ -177,7 +224,7 @@ const WebviewSearch: FC = ({ webviewRef, isWebviewReady, app return () => { unsubscribe?.() } - }, [appId, activeWebview, closeSearch, goToNext, goToPrevious, isVisible, openSearch]) + }, [appId, activeWebview, closeSearch, goToNext, goToPrevious, isVisible, isWebviewReady, openSearch]) useEffect(() => { if (!isVisible) return diff --git a/src/renderer/src/pages/minapps/components/__tests__/WebviewSearch.test.tsx b/src/renderer/src/pages/minapps/components/__tests__/WebviewSearch.test.tsx index 50e8c60ca2..4deee62ad5 100644 --- a/src/renderer/src/pages/minapps/components/__tests__/WebviewSearch.test.tsx +++ b/src/renderer/src/pages/minapps/components/__tests__/WebviewSearch.test.tsx @@ -164,6 +164,61 @@ describe('WebviewSearch', () => { }) }) + it('skips shortcut wiring when getWebContentsId throws', async () => { + const { webview } = createWebviewMock() + const error = new Error('not ready') + ;(webview as any).getWebContentsId = vi.fn(() => { + throw error + }) + const webviewRef = { current: webview } as React.RefObject + + const getWebContentsIdMock = vi.fn(() => { + throw error + }) + ;(webview as any).getWebContentsId = getWebContentsIdMock + const { rerender } = render() + + await waitFor(() => { + expect(getWebContentsIdMock).toHaveBeenCalled() + }) + expect(onFindShortcutMock).not.toHaveBeenCalled() + + ;(webview as any).getWebContentsId = vi.fn(() => 1) + + rerender() + rerender() + + await waitFor(() => { + expect(onFindShortcutMock).toHaveBeenCalled() + }) + }) + + it('does not call stopFindInPage when webview is not ready', async () => { + const { stopFindInPageMock, webview } = createWebviewMock() + const error = new Error('loading') + const getWebContentsIdMock = vi.fn(() => { + throw error + }) + ;(webview as any).getWebContentsId = getWebContentsIdMock + const webviewRef = { current: webview } as React.RefObject + + const { rerender, unmount } = render() + + await waitFor(() => { + expect(getWebContentsIdMock).toHaveBeenCalled() + }) + + stopFindInPageMock.mockImplementation(() => { + throw new Error('should not be called') + }) + + rerender() + expect(stopFindInPageMock).not.toHaveBeenCalled() + + unmount() + expect(stopFindInPageMock).not.toHaveBeenCalled() + }) + it('closes the search overlay when escape is forwarded from the webview', async () => { const { webview } = createWebviewMock() const webviewRef = { current: webview } as React.RefObject From 7cf57adceb0520415cb7bae8a95d80b3948c4afd Mon Sep 17 00:00:00 2001 From: Kejiang Ma Date: Tue, 14 Oct 2025 20:01:56 +0800 Subject: [PATCH 03/13] feat: new middleware to add 'no_think' (#10675) * new middleware to add 'no_think' Signed-off-by: Kejiang Ma * translate comments to English Signed-off-by: Kejiang Ma --------- Signed-off-by: Kejiang Ma --- .../middleware/AiSdkMiddlewareBuilder.ts | 10 ++++ .../aiCore/middleware/noThinkMiddleware.ts | 52 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 src/renderer/src/aiCore/middleware/noThinkMiddleware.ts diff --git a/src/renderer/src/aiCore/middleware/AiSdkMiddlewareBuilder.ts b/src/renderer/src/aiCore/middleware/AiSdkMiddlewareBuilder.ts index 20b89cf2e5..6034a88586 100644 --- a/src/renderer/src/aiCore/middleware/AiSdkMiddlewareBuilder.ts +++ b/src/renderer/src/aiCore/middleware/AiSdkMiddlewareBuilder.ts @@ -4,6 +4,8 @@ import type { MCPTool, Message, Model, Provider } from '@renderer/types' import type { Chunk } from '@renderer/types/chunk' import { extractReasoningMiddleware, LanguageModelMiddleware, simulateStreamingMiddleware } from 'ai' +import { noThinkMiddleware } from './noThinkMiddleware' + const logger = loggerService.withContext('AiSdkMiddlewareBuilder') /** @@ -186,6 +188,14 @@ function addProviderSpecificMiddlewares(builder: AiSdkMiddlewareBuilder, config: // 其他provider的通用处理 break } + + // OVMS+MCP's specific middleware + if (config.provider.id === 'ovms' && config.mcpTools && config.mcpTools.length > 0) { + builder.add({ + name: 'no-think', + middleware: noThinkMiddleware() + }) + } } /** diff --git a/src/renderer/src/aiCore/middleware/noThinkMiddleware.ts b/src/renderer/src/aiCore/middleware/noThinkMiddleware.ts new file mode 100644 index 0000000000..9d7d933bc1 --- /dev/null +++ b/src/renderer/src/aiCore/middleware/noThinkMiddleware.ts @@ -0,0 +1,52 @@ +import { loggerService } from '@logger' +import { LanguageModelMiddleware } from 'ai' + +const logger = loggerService.withContext('noThinkMiddleware') + +/** + * No Think Middleware + * Automatically appends ' /no_think' string to the end of user messages for the provider + * This prevents the model from generating unnecessary thinking process and returns results directly + * @returns LanguageModelMiddleware + */ +export function noThinkMiddleware(): LanguageModelMiddleware { + return { + middlewareVersion: 'v2', + + transformParams: async ({ params }) => { + const transformedParams = { ...params } + // Process messages in prompt + if (transformedParams.prompt && Array.isArray(transformedParams.prompt)) { + transformedParams.prompt = transformedParams.prompt.map((message) => { + // Only process user messages + if (message.role === 'user') { + // Process content array + if (Array.isArray(message.content)) { + const lastContent = message.content[message.content.length - 1] + // If the last content is text type, append ' /no_think' + if (lastContent && lastContent.type === 'text' && typeof lastContent.text === 'string') { + // Avoid duplicate additions + if (!lastContent.text.endsWith('/no_think')) { + logger.debug('Adding /no_think to user message') + return { + ...message, + content: [ + ...message.content.slice(0, -1), + { + ...lastContent, + text: lastContent.text + ' /no_think' + } + ] + } + } + } + } + } + return message + }) + } + + return transformedParams + } + } +} From 004d6d8201efe1447b691dce2533303b1756f5aa Mon Sep 17 00:00:00 2001 From: defi-failure <159208748+defi-failure@users.noreply.github.com> Date: Tue, 14 Oct 2025 20:56:22 +0800 Subject: [PATCH 04/13] fix: move newly created agent session to top (#10711) --- src/renderer/src/hooks/agents/useSessions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/src/hooks/agents/useSessions.ts b/src/renderer/src/hooks/agents/useSessions.ts index 3307d0353d..fce29b6bf9 100644 --- a/src/renderer/src/hooks/agents/useSessions.ts +++ b/src/renderer/src/hooks/agents/useSessions.ts @@ -21,7 +21,7 @@ export const useSessions = (agentId: string) => { async (form: CreateSessionForm) => { try { const result = await client.createSession(agentId, form) - await mutate((prev) => [...(prev ?? []), result], { revalidate: false }) + await mutate((prev) => [result, ...(prev ?? [])], { revalidate: false }) return result } catch (error) { window.toast.error(formatErrorMessageWithPrefix(error, t('agent.session.create.error.failed'))) From 7b3b73d390d665582cc2cdfab1fd673f56c4b7bd Mon Sep 17 00:00:00 2001 From: defi-failure <159208748+defi-failure@users.noreply.github.com> Date: Tue, 14 Oct 2025 20:59:07 +0800 Subject: [PATCH 05/13] fix: make anthropic model provided by cherryin visible to agent (#10695) --- src/main/apiServer/services/models.ts | 14 ++++++++++--- src/main/apiServer/utils/index.ts | 11 +++++++--- .../agents/services/claudecode/index.ts | 20 ++++++++++++++----- .../components/Popups/agent/AgentModal.tsx | 2 +- src/renderer/src/types/apiModels.ts | 1 + src/renderer/src/utils/agentSession.ts | 2 +- 6 files changed, 37 insertions(+), 13 deletions(-) diff --git a/src/main/apiServer/services/models.ts b/src/main/apiServer/services/models.ts index 684d7f10a8..6c0056b27e 100644 --- a/src/main/apiServer/services/models.ts +++ b/src/main/apiServer/services/models.ts @@ -1,3 +1,5 @@ +import { isEmpty } from 'lodash' + import { ApiModel, ApiModelsFilter, ApiModelsResponse } from '../../../renderer/src/types/apiModels' import { loggerService } from '../../services/LoggerService' import { getAvailableProviders, listAllAvailableModels, transformModelToOpenAI } from '../utils' @@ -8,6 +10,10 @@ const logger = loggerService.withContext('ModelsService') export type ModelsFilter = ApiModelsFilter +const isAnthropicProvider = (provider: { type: string; anthropicApiHost?: string }) => { + return provider.type === 'anthropic' || !isEmpty(provider.anthropicApiHost?.trim()) +} + export class ModelsService { async getModels(filter: ModelsFilter): Promise { try { @@ -16,9 +22,7 @@ export class ModelsService { let providers = await getAvailableProviders() if (filter.providerType === 'anthropic') { - providers = providers.filter( - (p) => p.type === 'anthropic' || (p.anthropicApiHost !== undefined && p.anthropicApiHost.trim() !== '') - ) + providers = providers.filter(isAnthropicProvider) } const models = await listAllAvailableModels(providers) @@ -41,6 +45,10 @@ export class ModelsService { continue } + if (filter.supportAnthropic && model.endpoint_type !== 'anthropic' && !isAnthropicProvider(provider)) { + continue + } + const openAIModel = transformModelToOpenAI(model, provider) const fullModelId = openAIModel.id // This is already in format "provider:model_id" diff --git a/src/main/apiServer/utils/index.ts b/src/main/apiServer/utils/index.ts index bd80c73f51..865f961db9 100644 --- a/src/main/apiServer/utils/index.ts +++ b/src/main/apiServer/utils/index.ts @@ -1,7 +1,7 @@ import { CacheService } from '@main/services/CacheService' import { loggerService } from '@main/services/LoggerService' import { reduxService } from '@main/services/ReduxService' -import { ApiModel, Model, Provider } from '@types' +import { ApiModel, EndpointType, Model, Provider } from '@types' const logger = loggerService.withContext('ApiServerUtils') @@ -114,6 +114,7 @@ export async function validateModelId(model: string): Promise<{ error?: ModelValidationError provider?: Provider modelId?: string + modelEndpointType?: EndpointType }> { try { if (!model || typeof model !== 'string') { @@ -166,7 +167,8 @@ export async function validateModelId(model: string): Promise<{ } // Check if model exists in provider - const modelExists = provider.models?.some((m) => m.id === modelId) + const modelInProvider = provider.models?.find((m) => m.id === modelId) + const modelExists = !!modelInProvider if (!modelExists) { const availableModels = provider.models?.map((m) => m.id).join(', ') || 'none' return { @@ -179,10 +181,13 @@ export async function validateModelId(model: string): Promise<{ } } + const modelEndpointType = modelInProvider?.endpoint_type + return { valid: true, provider, - modelId + modelId, + modelEndpointType } } catch (error: any) { logger.error('Error validating model ID', { error, model }) diff --git a/src/main/services/agents/services/claudecode/index.ts b/src/main/services/agents/services/claudecode/index.ts index 7b2f119afb..7dae2f9e9e 100644 --- a/src/main/services/agents/services/claudecode/index.ts +++ b/src/main/services/agents/services/claudecode/index.ts @@ -8,6 +8,7 @@ import { config as apiConfigService } from '@main/apiServer/config' import { validateModelId } from '@main/apiServer/utils' import getLoginShellEnvironment from '@main/utils/shell-env' import { app } from 'electron' +import { isEmpty } from 'lodash' import { GetAgentSessionResponse } from '../..' import { AgentServiceInterface, AgentStream, AgentStreamEvent } from '../../interfaces/AgentStreamInterface' @@ -60,11 +61,20 @@ class ClaudeCodeService implements AgentServiceInterface { }) return aiStream } - if ( - (modelInfo.provider?.type !== 'anthropic' && - (modelInfo.provider?.anthropicApiHost === undefined || modelInfo.provider.anthropicApiHost.trim() === '')) || - modelInfo.provider.apiKey === '' - ) { + + const validateModelInfo: (m: typeof modelInfo) => boolean = (m) => { + const { provider, modelEndpointType } = m + if (!provider) return false + if (isEmpty(provider.apiKey?.trim())) return false + + const isAnthropicType = provider.type === 'anthropic' + const isAnthropicEndpoint = modelEndpointType === 'anthropic' + const hasValidApiHost = !isEmpty(provider.anthropicApiHost?.trim()) + + return !(!isAnthropicType && !isAnthropicEndpoint && !hasValidApiHost) + } + + if (!modelInfo.provider || !validateModelInfo(modelInfo)) { logger.error('Anthropic provider configuration is missing', { modelInfo }) diff --git a/src/renderer/src/components/Popups/agent/AgentModal.tsx b/src/renderer/src/components/Popups/agent/AgentModal.tsx index fa76e0e330..63b614944a 100644 --- a/src/renderer/src/components/Popups/agent/AgentModal.tsx +++ b/src/renderer/src/components/Popups/agent/AgentModal.tsx @@ -100,7 +100,7 @@ export const AgentModal: React.FC = ({ agent, trigger, isOpen: _isOpen, o const { addAgent } = useAgents() const { updateAgent } = useUpdateAgent() // hard-coded. We only support anthropic for now. - const { models } = useApiModels({ providerType: 'anthropic' }) + const { models } = useApiModels({ supportAnthropic: true }) const isEditing = (agent?: AgentWithTools) => agent !== undefined const [form, setForm] = useState(() => buildAgentForm(agent)) diff --git a/src/renderer/src/types/apiModels.ts b/src/renderer/src/types/apiModels.ts index 68141bf68c..7b4ec96c9d 100644 --- a/src/renderer/src/types/apiModels.ts +++ b/src/renderer/src/types/apiModels.ts @@ -6,6 +6,7 @@ import { ProviderTypeSchema } from './provider' // Request schema for /v1/models export const ApiModelsFilterSchema = z.object({ providerType: ProviderTypeSchema.optional(), + supportAnthropic: z.coerce.boolean().optional(), offset: z.coerce.number().min(0).default(0).optional(), limit: z.coerce.number().min(1).default(20).optional() }) diff --git a/src/renderer/src/utils/agentSession.ts b/src/renderer/src/utils/agentSession.ts index df34413641..b2cf14d174 100644 --- a/src/renderer/src/utils/agentSession.ts +++ b/src/renderer/src/utils/agentSession.ts @@ -18,7 +18,7 @@ export const getModelFilterByAgentType = (type: AgentType): ApiModelsFilter => { switch (type) { case 'claude-code': return { - providerType: 'anthropic' + supportAnthropic: true } default: return {} From 011b6f2df16218b98102f5acf1c8b62150b0be42 Mon Sep 17 00:00:00 2001 From: Phantom <59059173+EurFelux@users.noreply.github.com> Date: Wed, 15 Oct 2025 13:03:12 +0800 Subject: [PATCH 06/13] build: update react and react-dom to v19.2.0 (#10710) Update dependencies to latest stable versions to benefit from bug fixes and performance improvements --- package.json | 4 ++-- yarn.lock | 32 ++++++++++++++++---------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 3c5d89ffeb..46e6e005e4 100644 --- a/package.json +++ b/package.json @@ -303,8 +303,8 @@ "pdf-parse": "^1.1.1", "playwright": "^1.52.0", "proxy-agent": "^6.5.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", "react-error-boundary": "^6.0.0", "react-hotkeys-hook": "^4.6.1", "react-i18next": "^14.1.2", diff --git a/yarn.lock b/yarn.lock index 8befb6e5db..4fcea693ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14151,8 +14151,8 @@ __metadata: pdf-parse: "npm:^1.1.1" playwright: "npm:^1.52.0" proxy-agent: "npm:^6.5.0" - react: "npm:^19.0.0" - react-dom: "npm:^19.0.0" + react: "npm:^19.2.0" + react-dom: "npm:^19.2.0" react-error-boundary: "npm:^6.0.0" react-hotkeys-hook: "npm:^4.6.1" react-i18next: "npm:^14.1.2" @@ -25881,14 +25881,14 @@ __metadata: languageName: node linkType: hard -"react-dom@npm:^19.0.0": - version: 19.1.0 - resolution: "react-dom@npm:19.1.0" +"react-dom@npm:^19.2.0": + version: 19.2.0 + resolution: "react-dom@npm:19.2.0" dependencies: - scheduler: "npm:^0.26.0" + scheduler: "npm:^0.27.0" peerDependencies: - react: ^19.1.0 - checksum: 10c0/3e26e89bb6c67c9a6aa86cb888c7a7f8258f2e347a6d2a15299c17eb16e04c19194e3452bc3255bd34000a61e45e2cb51e46292392340432f133e5a5d2dfb5fc + react: ^19.2.0 + checksum: 10c0/fa2cae05248d01288e91523b590ce4e7635b1e13f1344e225f850d722a8da037bf0782f63b1c1d46353334e0c696909b82e582f8cad607948fde6f7646cc18d9 languageName: node linkType: hard @@ -26161,10 +26161,10 @@ __metadata: languageName: node linkType: hard -"react@npm:^19.0.0": - version: 19.1.0 - resolution: "react@npm:19.1.0" - checksum: 10c0/530fb9a62237d54137a13d2cfb67a7db6a2156faed43eecc423f4713d9b20c6f2728b026b45e28fcd72e8eadb9e9ed4b089e99f5e295d2f0ad3134251bdd3698 +"react@npm:^19.2.0": + version: 19.2.0 + resolution: "react@npm:19.2.0" + checksum: 10c0/1b6d64eacb9324725bfe1e7860cb7a6b8a34bc89a482920765ebff5c10578eb487e6b46b2f0df263bd27a25edbdae2c45e5ea5d81ae61404301c1a7192c38330 languageName: node linkType: hard @@ -27023,10 +27023,10 @@ __metadata: languageName: node linkType: hard -"scheduler@npm:^0.26.0": - version: 0.26.0 - resolution: "scheduler@npm:0.26.0" - checksum: 10c0/5b8d5bfddaae3513410eda54f2268e98a376a429931921a81b5c3a2873aab7ca4d775a8caac5498f8cbc7d0daeab947cf923dbd8e215d61671f9f4e392d34356 +"scheduler@npm:^0.27.0": + version: 0.27.0 + resolution: "scheduler@npm:0.27.0" + checksum: 10c0/4f03048cb05a3c8fddc45813052251eca00688f413a3cee236d984a161da28db28ba71bd11e7a3dd02f7af84ab28d39fb311431d3b3772fed557945beb00c452 languageName: node linkType: hard From 4028b26c1d424c4bd6064db404d034e4b9580998 Mon Sep 17 00:00:00 2001 From: defi-failure <159208748+defi-failure@users.noreply.github.com> Date: Wed, 15 Oct 2025 15:53:30 +0800 Subject: [PATCH 07/13] fix: remove agent session input trigger placeholder (#10729) --- src/renderer/src/pages/home/Inputbar/AgentSessionInputbar.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/pages/home/Inputbar/AgentSessionInputbar.tsx b/src/renderer/src/pages/home/Inputbar/AgentSessionInputbar.tsx index 320aad44a8..3c87462c6a 100644 --- a/src/renderer/src/pages/home/Inputbar/AgentSessionInputbar.tsx +++ b/src/renderer/src/pages/home/Inputbar/AgentSessionInputbar.tsx @@ -262,7 +262,9 @@ const AgentSessionInputbar: FC = ({ agentId, sessionId }) => { value={text} onChange={onChange} onKeyDown={handleKeyDown} - placeholder={t('chat.input.placeholder', { key: getSendMessageShortcutLabel(sendMessageShortcut) })} + placeholder={t('chat.input.placeholder_without_triggers', { + key: getSendMessageShortcutLabel(sendMessageShortcut) + })} autoFocus variant="borderless" spellCheck={enableSpellCheck} From f27a481c3c61311ff7a7288541906c6a3aaf4350 Mon Sep 17 00:00:00 2001 From: SuYao Date: Wed, 15 Oct 2025 16:47:45 +0800 Subject: [PATCH 08/13] Fix/aisdk error (#10563) * Add syntax highlighting to AI SDK error cause display - Parse and format error cause as JSON with syntax highlighting - Use CodeStyleProvider context for consistent code styling - Maintain plain text fallback for non-JSON content * fix patch * chore: yarn lock * feat: provider-specific-error * chore * chore * fix: handle JSON parsing errors in AiSdkErrorBase component * fix: improve error message formatting in AiSdkToChunkAdapter * fix: remove unused MarkdownContainer and update AiSdkErrorBase to use styled div --- .../src/aiCore/chunk/AiSdkToChunkAdapter.ts | 8 ++++- .../pages/home/Messages/Blocks/ErrorBlock.tsx | 27 +++++++++++++++-- src/renderer/src/types/error.ts | 16 +++++++++- .../src/types/provider-specific-error.ts | 29 +++++++++++++++++++ src/renderer/src/utils/error.ts | 1 + 5 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 src/renderer/src/types/provider-specific-error.ts diff --git a/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts b/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts index 5ea1ddf966..f4894a203f 100644 --- a/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts +++ b/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts @@ -6,6 +6,8 @@ import { loggerService } from '@logger' import { AISDKWebSearchResult, MCPTool, WebSearchResults, WebSearchSource } from '@renderer/types' import { Chunk, ChunkType } from '@renderer/types/chunk' +import { ProviderSpecificError } from '@renderer/types/provider-specific-error' +import { formatErrorMessage } from '@renderer/utils/error' import { convertLinks, flushLinkConverterBuffer } from '@renderer/utils/linkConverter' import type { ClaudeCodeRawValue } from '@shared/agents/claudecode/types' import type { TextStreamPart, ToolSet } from 'ai' @@ -355,7 +357,11 @@ export class AiSdkToChunkAdapter { case 'error': this.onChunk({ type: ChunkType.ERROR, - error: chunk.error as Record + error: new ProviderSpecificError({ + message: formatErrorMessage(chunk.error), + provider: 'unknown', + cause: chunk.error + }) }) break diff --git a/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx b/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx index 2cf85a3d63..243bec8ccc 100644 --- a/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx +++ b/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx @@ -1,5 +1,6 @@ import { Button } from '@heroui/button' import CodeViewer from '@renderer/components/CodeViewer' +import { useCodeStyle } from '@renderer/context/CodeStyleProvider' import { useTimer } from '@renderer/hooks/useTimer' import { getHttpMessageLabel, getProviderLabel } from '@renderer/i18n/label' import { getProviderById } from '@renderer/services/ProviderService' @@ -35,7 +36,7 @@ import { import type { ErrorMessageBlock, Message } from '@renderer/types/newMessage' import { formatAiSdkError, formatError, safeToString } from '@renderer/utils/error' import { Alert as AntdAlert, Modal } from 'antd' -import React, { useState } from 'react' +import React, { useEffect, useState } from 'react' import { Trans, useTranslation } from 'react-i18next' import { Link } from 'react-router-dom' import styled from 'styled-components' @@ -305,14 +306,36 @@ const BuiltinError = ({ error }: { error: SerializedError }) => { // 作为 base,渲染公共字段,应当在 ErrorDetailList 中渲染 const AiSdkErrorBase = ({ error }: { error: SerializedAiSdkError }) => { const { t } = useTranslation() + const { highlightCode } = useCodeStyle() + const [highlightedString, setHighlightedString] = useState('') const cause = error.cause + + useEffect(() => { + const highlight = async () => { + try { + const result = await highlightCode(JSON.stringify(JSON.parse(cause || '{}'), null, 2), 'json') + setHighlightedString(result) + } catch { + setHighlightedString(cause || '') + } + } + const timer = setTimeout(highlight, 0) + + return () => clearTimeout(timer) + }, [highlightCode, cause]) + return ( <> {cause && ( {t('error.cause')}: - {error.cause} + +
+ )} diff --git a/src/renderer/src/types/error.ts b/src/renderer/src/types/error.ts index c260c9c09f..78bfe0a526 100644 --- a/src/renderer/src/types/error.ts +++ b/src/renderer/src/types/error.ts @@ -20,6 +20,7 @@ import { UnsupportedFunctionalityError } from 'ai' +import { ProviderSpecificError } from './provider-specific-error' import { Serializable } from './serialize' export interface SerializedError { @@ -80,7 +81,7 @@ export interface SerializedAiSdkInvalidArgumentError extends SerializedAiSdkErro export const isSerializedAiSdkInvalidArgumentError = ( error: SerializedError ): error is SerializedAiSdkInvalidArgumentError => { - return isSerializedAiSdkError(error) && 'parameter' in error && 'value' in error + return isSerializedAiSdkError(error) && 'message' in error && error.name === 'AI_InvalidArgumentError' } export interface SerializedAiSdkInvalidDataContentError extends SerializedAiSdkError { @@ -198,10 +199,20 @@ export interface SerializedAiSdkNoSuchToolError extends SerializedAiSdkError { readonly availableTools: string[] | null } +export interface SerializedAiSdkProviderSpecificError extends SerializedAiSdkError { + readonly provider: string +} + export const isSerializedAiSdkNoSuchToolError = (error: SerializedError): error is SerializedAiSdkNoSuchToolError => { return isSerializedAiSdkError(error) && 'toolName' in error && 'availableTools' in error } +export const isSerializedAiSdkProviderSpecificError = ( + error: SerializedError +): error is SerializedAiSdkProviderSpecificError => { + return isSerializedAiSdkError(error) && 'provider' in error +} + export interface SerializedAiSdkRetryError extends SerializedAiSdkError { readonly reason: string readonly lastError: Serializable @@ -277,6 +288,7 @@ export type AiSdkErrorUnion = | NoSuchModelError | NoSuchProviderError | NoSuchToolError + | ProviderSpecificError | RetryError | ToolCallRepairError | TypeValidationError @@ -297,6 +309,7 @@ export type SerializedAiSdkErrorUnion = | SerializedAiSdkNoSuchModelError | SerializedAiSdkNoSuchProviderError | SerializedAiSdkNoSuchToolError + | SerializedAiSdkProviderSpecificError | SerializedAiSdkRetryError | SerializedAiSdkToolCallRepairError | SerializedAiSdkTypeValidationError @@ -317,6 +330,7 @@ export const isSerializedAiSdkErrorUnion = (error: SerializedError): error is Se isSerializedAiSdkNoSuchModelError(error) || isSerializedAiSdkNoSuchProviderError(error) || isSerializedAiSdkNoSuchToolError(error) || + isSerializedAiSdkProviderSpecificError(error) || isSerializedAiSdkRetryError(error) || isSerializedAiSdkToolCallRepairError(error) || isSerializedAiSdkTypeValidationError(error) || diff --git a/src/renderer/src/types/provider-specific-error.ts b/src/renderer/src/types/provider-specific-error.ts new file mode 100644 index 0000000000..7d624b3ebe --- /dev/null +++ b/src/renderer/src/types/provider-specific-error.ts @@ -0,0 +1,29 @@ +import { AISDKError } from 'ai' + +const name = 'AI_ProviderSpecificError' +const marker = `vercel.ai.error.${name}` +const symbol = Symbol.for(marker) + +export class ProviderSpecificError extends AISDKError { + // @ts-ignore + private readonly [symbol] = true // used in isInstance + + readonly provider: string + + constructor({ + message, + provider, + cause + }: { + message: string + provider: string + cause?: unknown + }) { + super({ name, message, cause }) + this.provider = provider + } + + static isInstance(error: unknown): error is ProviderSpecificError { + return AISDKError.hasMarker(error, marker) + } +} diff --git a/src/renderer/src/utils/error.ts b/src/renderer/src/utils/error.ts index 737baf4119..2e7f10964e 100644 --- a/src/renderer/src/utils/error.ts +++ b/src/renderer/src/utils/error.ts @@ -199,6 +199,7 @@ export const serializeError = (error: AiSdkErrorUnion): SerializedError => { ? serializeInvalidToolInputError(error.originalError) : serializeNoSuchToolError(error.originalError) if ('functionality' in error) serializedError.functionality = error.functionality + if ('provider' in error) serializedError.provider = error.provider return serializedError } From b74655651d029efdb0345b415e395b89285b8b43 Mon Sep 17 00:00:00 2001 From: defi-failure <159208748+defi-failure@users.noreply.github.com> Date: Wed, 15 Oct 2025 17:24:25 +0800 Subject: [PATCH 09/13] fix: swagger ui can't open (#10732) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 46e6e005e4..29cb40b0a8 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "selection-hook": "^1.0.12", "sharp": "^0.34.3", "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1", "tesseract.js": "patch:tesseract.js@npm%3A6.0.1#~/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch", "turndown": "7.2.0" }, @@ -336,7 +337,6 @@ "string-width": "^7.2.0", "striptags": "^3.2.0", "styled-components": "^6.1.11", - "swagger-ui-express": "^5.0.1", "swr": "^2.3.6", "tailwindcss": "^4.1.13", "tar": "^7.4.3", From c457d4a86826a66037434d74205494f72fbef58e Mon Sep 17 00:00:00 2001 From: ABucket Date: Wed, 15 Oct 2025 18:48:30 +0800 Subject: [PATCH 10/13] fix: Duplicate dialog when clearing messages (#10721) --- src/renderer/src/pages/home/Tabs/components/Topics.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/renderer/src/pages/home/Tabs/components/Topics.tsx b/src/renderer/src/pages/home/Tabs/components/Topics.tsx index d7214ff450..ce2cd25d35 100644 --- a/src/renderer/src/pages/home/Tabs/components/Topics.tsx +++ b/src/renderer/src/pages/home/Tabs/components/Topics.tsx @@ -288,13 +288,7 @@ export const Topics: React.FC = ({ assistant: _assistant, activeTopic, se label: t('chat.topics.clear.title'), key: 'clear-messages', icon: , - async onClick() { - window.modal.confirm({ - title: t('chat.input.clear.content'), - centered: true, - onOk: () => onClearMessages(topic) - }) - } + onClick: () => onClearMessages(topic) }, { label: t('settings.topic.position.label'), From 2e173631a074af1e23be0de779359f88df7a99f4 Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Wed, 15 Oct 2025 19:46:36 +0800 Subject: [PATCH 11/13] chore: update release notes for v1.7.0-beta.1 - Major features introduced: Agent System, Agent Management, and Unified UI. - Added detailed agent features and UI/UX improvements. - Included bug fixes and technical updates, such as React upgrade and enhanced Claude Code service. - Updated version in package.json to 1.7.0-beta.1. --- electron-builder.yml | 116 ++++++++++++++++++++++++++++++++++++++----- package.json | 4 +- 2 files changed, 106 insertions(+), 14 deletions(-) diff --git a/electron-builder.yml b/electron-builder.yml index e1e29eb111..8577a307a2 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -125,21 +125,113 @@ afterSign: scripts/notarize.js artifactBuildCompleted: scripts/artifact-build-completed.js releaseInfo: releaseNotes: | - What's New in v1.6.3 + + What's New in v1.7.0-beta.1 - Features: - - Notes: Add spell-check control, automatic table line wrapping, export functionality, and LLM-based renaming - - UI: Expand topic rename clickable area, add middle-click tab closing, remove redundant scrollbars, fix message menubar overflow - - Editor: Add read-only extension support, make TextFilePreview read-only but copyable - - Models: Update support for DeepSeek v3.2, Claude 4.5, GLM 4.6, Gemini regex, and vision models - - Code Tools: Add GitHub Copilot CLI integration + Major Features: + - Agent System: Introducing intelligent Agent capabilities alongside Assistants. Agents can autonomously solve complex problems using Claude Code SDK with tool calling, file operations, and multi-turn reasoning + - Agent Management: Create, configure, and manage agents with custom settings including model selection, tool permissions, accessible paths, and MCP server integrations + - Agent Sessions: Dedicated session management for agent interactions with persistent message history and context tracking + - Unified UI: Streamlined interface combining Assistants and Agents tabs with improved navigation and settings management + + Agent Features: + - Tool Support: Web search, file operations, bash commands, and custom MCP tools + - Advanced Configuration: Max turns, temperature, token limits + - Permission Control: Configurable tool approval modes (manual, automatic, none) + - Session Persistence: Automatic message saving with optimized streaming and database integration + - Model Selection: API-based model filtering with provider-specific support + + UI/UX Improvements: + - Unified assistant/agent tabs with smooth animations + - In-place session name editing + - Virtual list rendering for improved performance + - Session count indicators for active agents + - Enhanced settings popup with tabbed interface + - Webview keyboard shortcut interception for search functionality + + API & Infrastructure: + - RESTful API for agent and session management + - Drizzle ORM integration for agent database + - OAuth support for Claude Code authentication + - Express validator for request validation + - Comprehensive error handling with Zod schemas + + Model Updates: + - Gemini 2.5 Image Flash support + - Grok 4 Fast with reasoning capabilities + - Qwen3-omni and Qwen3-vl thinking models + - DeepSeek, Claude 4.5, GLM 4.6 support + - GitHub Copilot CLI integration with gpt-5-codex Bug Fixes: + - Fix Swagger UI accessibility issues + - Fix AI SDK error display with syntax highlighting + - Fix webview search shortcut handling + - Fix agent model visibility for CherryIn provider + - Fix session message ordering and persistence + - Fix anthropic model visibility in agent configuration + - Fix knowledge base deletion and web search RAG errors - Fix migration for missing providers - - Fix forked topic retaining old name after rename - - Restore first token latency reporting in metrics - - Fix UI scrollbar and overflow issues Technical Updates: - - Upgrade to Electron 37.6.0 - - Update dependencies across packages + - React 19.2.0 upgrade + - Enhanced Claude Code service with streaming support + - Improved message transformation and streaming lifecycle + - Database migration system with automatic schema sync + - Optimized bundle size and dependency management + + + v1.7.0-beta.1 新特性 + + 核心功能: + - Agent 系统:引入智能 Agent 能力,与助手(Assistant)并存。Agent 基于 Claude Code SDK 构建,具备工具调用、文件操作和多轮推理能力,可自主解决复杂问题 + - Agent 管理:创建、配置和管理 Agent,支持自定义模型选择、工具权限、可访问路径和 MCP 服务器集成 + - Agent 会话:专属会话管理系统,支持持久化消息历史和上下文追踪 + - 统一界面:精简的助手和 Agent 标签页界面,改进导航和设置管理体验 + + Agent 功能特性: + - 工具支持:网页搜索、文件操作、Bash 命令执行和自定义 MCP 工具 + - 高级配置:最大轮次、温度、Token 限制 + - 权限控制:可配置的工具批准模式(手动、自动、无需批准) + - 会话持久化:自动消息保存,优化的流式传输和数据库集成 + - 模型选择:基于 API 的模型过滤,支持特定提供商 + + 界面与交互优化: + - 统一的助手/Agent 标签页,带有流畅动画效果 + - 会话名称原地编辑功能 + - 虚拟列表渲染,提升性能表现 + - 活跃 Agent 的会话计数指示器 + - 增强的设置弹窗,采用标签页界面 + - Webview 键盘快捷键拦截,支持搜索功能 + + API 与基础设施: + - RESTful API 用于 Agent 和会话管理 + - 集成 Drizzle ORM 管理 Agent 数据库 + - Claude Code OAuth 认证支持 + - Express validator 请求验证 + - 基于 Zod 模式的完善错误处理 + + 模型更新: + - 支持 Gemini 2.5 Image Flash + - Grok 4 Fast 推理能力 + - Qwen3-omni 和 Qwen3-vl 思考模型 + - DeepSeek、Claude 4.5、GLM 4.6 支持 + - GitHub Copilot CLI 集成 gpt-5-codex + + 问题修复: + - 修复 Swagger UI 无法打开 + - 修复 AI SDK 错误显示,添加语法高亮 + - 修复 Webview 搜索快捷键处理 + - 修复 CherryIn 提供商的 Agent 模型可见性 + - 修复会话消息排序和持久化 + - 修复 Anthropic 模型在 Agent 配置中的可见性 + - 修复知识库删除和网页搜索 RAG 错误 + - 修复缺失提供商的迁移问题 + + 技术更新: + - 升级至 React 19.2.0 + - 增强 Claude Code 服务流式传输支持 + - 改进消息转换和流式生命周期 + - 数据库迁移系统,支持自动模式同步 + - 优化打包大小和依赖管理 + diff --git a/package.json b/package.json index 29cb40b0a8..e82153922c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "CherryStudio", - "version": "1.7.0-alpha.5", + "version": "1.7.0-beta.1", "private": true, "description": "A powerful AI assistant for producer.", "main": "./out/main/index.js", @@ -83,6 +83,7 @@ "@libsql/win32-x64-msvc": "^0.4.7", "@napi-rs/system-ocr": "patch:@napi-rs/system-ocr@npm%3A1.0.2#~/.yarn/patches/@napi-rs-system-ocr-npm-1.0.2-59e7a78e8b.patch", "@strongtz/win32-arm64-msvc": "^0.4.7", + "express": "^5.1.0", "font-list": "^2.0.0", "graceful-fs": "^4.2.11", "jsdom": "26.1.0", @@ -263,7 +264,6 @@ "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-unused-imports": "^4.1.4", - "express": "^5.1.0", "express-validator": "^7.2.1", "fast-diff": "^1.3.0", "fast-xml-parser": "^5.2.0", From 1a972ac0e0d96ef2ccb7bb3b649254401749e326 Mon Sep 17 00:00:00 2001 From: Phantom Date: Thu, 16 Oct 2025 12:49:31 +0800 Subject: [PATCH 12/13] fix: api server status (#10734) * refactor(apiServer): move api server types to dedicated module Restructure api server type definitions by moving them from index.ts to a dedicated apiServer.ts file. This improves code organization and maintainability by grouping related types together. * feat(api-server): add api server management hooks and integration Extract api server management logic into reusable hook and integrate with settings page * feat(api-server): improve api server status handling and error messages - add new error messages for api server status - optimize initial state and loading in useApiServer hook - centralize api server enabled check via useApiServer hook - update components to use new api server status handling * fix(agents): update error message key for agent server not running * fix(i18n): update api server status messages across locales Remove redundant 'notRunning' message in en-us locale Add consistent 'not_running' error message in all locales Add missing 'notEnabled' message in several locales * refactor: update api server type imports to use @types Move api server related type imports from renderer/src/types to @types package for better code organization and maintainability * docs(IpcChannel): add comment about unused api-server:get-config Add TODO comment about data inconsistency in useApiServer hook * refactor(assistants): pass apiServerEnabled as prop instead of using hook Move apiServerEnabled from being fetched via useApiServer hook to being passed as a prop through component hierarchy. This improves maintainability by making dependencies more explicit and reducing hook usage in child components. * style(AssistantsTab): add consistent margin-bottom to alert components * feat(useAgent): add api server status checks before fetching agent Ensure api server is enabled and running before attempting to fetch agent data --- packages/shared/IpcChannel.ts | 1 + src/main/services/ApiServerService.ts | 16 ++- src/preload/index.ts | 10 ++ src/renderer/src/hooks/agents/useAgent.ts | 12 +- src/renderer/src/hooks/agents/useAgents.ts | 10 +- src/renderer/src/hooks/useApiServer.ts | 112 ++++++++++++++++++ src/renderer/src/i18n/locales/en-us.json | 6 + src/renderer/src/i18n/locales/zh-cn.json | 6 + src/renderer/src/i18n/locales/zh-tw.json | 6 + src/renderer/src/i18n/translate/el-gr.json | 6 + src/renderer/src/i18n/translate/es-es.json | 6 + src/renderer/src/i18n/translate/fr-fr.json | 6 + src/renderer/src/i18n/translate/ja-jp.json | 6 + src/renderer/src/i18n/translate/pt-pt.json | 6 + src/renderer/src/i18n/translate/ru-ru.json | 6 + .../src/pages/home/Tabs/AssistantsTab.tsx | 29 +++-- .../ApiServerSettings/ApiServerSettings.tsx | 62 ++-------- src/renderer/src/types/apiServer.ts | 38 ++++++ src/renderer/src/types/index.ts | 8 +- 19 files changed, 277 insertions(+), 75 deletions(-) create mode 100644 src/renderer/src/hooks/useApiServer.ts create mode 100644 src/renderer/src/types/apiServer.ts diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index 720d1ec4bb..93679f5faa 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -317,6 +317,7 @@ export enum IpcChannel { ApiServer_Stop = 'api-server:stop', ApiServer_Restart = 'api-server:restart', ApiServer_GetStatus = 'api-server:get-status', + // NOTE: This api is not be used. ApiServer_GetConfig = 'api-server:get-config', // Anthropic OAuth diff --git a/src/main/services/ApiServerService.ts b/src/main/services/ApiServerService.ts index 9a7bfad7e0..76a5eca617 100644 --- a/src/main/services/ApiServerService.ts +++ b/src/main/services/ApiServerService.ts @@ -1,5 +1,11 @@ import { IpcChannel } from '@shared/IpcChannel' -import { ApiServerConfig } from '@types' +import { + ApiServerConfig, + GetApiServerStatusResult, + RestartApiServerStatusResult, + StartApiServerStatusResult, + StopApiServerStatusResult +} from '@types' import { ipcMain } from 'electron' import { apiServer } from '../apiServer' @@ -52,7 +58,7 @@ export class ApiServerService { registerIpcHandlers(): void { // API Server - ipcMain.handle(IpcChannel.ApiServer_Start, async () => { + ipcMain.handle(IpcChannel.ApiServer_Start, async (): Promise => { try { await this.start() return { success: true } @@ -61,7 +67,7 @@ export class ApiServerService { } }) - ipcMain.handle(IpcChannel.ApiServer_Stop, async () => { + ipcMain.handle(IpcChannel.ApiServer_Stop, async (): Promise => { try { await this.stop() return { success: true } @@ -70,7 +76,7 @@ export class ApiServerService { } }) - ipcMain.handle(IpcChannel.ApiServer_Restart, async () => { + ipcMain.handle(IpcChannel.ApiServer_Restart, async (): Promise => { try { await this.restart() return { success: true } @@ -79,7 +85,7 @@ export class ApiServerService { } }) - ipcMain.handle(IpcChannel.ApiServer_GetStatus, async () => { + ipcMain.handle(IpcChannel.ApiServer_GetStatus, async (): Promise => { try { const config = await this.getCurrentConfig() return { diff --git a/src/preload/index.ts b/src/preload/index.ts index a90174baa5..34656092b2 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -12,6 +12,7 @@ import { FileListResponse, FileMetadata, FileUploadResponse, + GetApiServerStatusResult, KnowledgeBaseParams, KnowledgeItem, KnowledgeSearchResult, @@ -22,8 +23,11 @@ import { OcrProvider, OcrResult, Provider, + RestartApiServerStatusResult, S3Config, Shortcut, + StartApiServerStatusResult, + StopApiServerStatusResult, SupportedOcrFile, ThemeMode, WebDavConfig @@ -496,6 +500,12 @@ const api = { ipcRenderer.removeListener(channel, listener) } } + }, + apiServer: { + getStatus: (): Promise => ipcRenderer.invoke(IpcChannel.ApiServer_GetStatus), + start: (): Promise => ipcRenderer.invoke(IpcChannel.ApiServer_Start), + restart: (): Promise => ipcRenderer.invoke(IpcChannel.ApiServer_Restart), + stop: (): Promise => ipcRenderer.invoke(IpcChannel.ApiServer_Stop) } } diff --git a/src/renderer/src/hooks/agents/useAgent.ts b/src/renderer/src/hooks/agents/useAgent.ts index 62d2393618..a6755a4293 100644 --- a/src/renderer/src/hooks/agents/useAgent.ts +++ b/src/renderer/src/hooks/agents/useAgent.ts @@ -1,18 +1,28 @@ import { useCallback } from 'react' +import { useTranslation } from 'react-i18next' import useSWR from 'swr' +import { useApiServer } from '../useApiServer' import { useAgentClient } from './useAgentClient' export const useAgent = (id: string | null) => { + const { t } = useTranslation() const client = useAgentClient() const key = id ? client.agentPaths.withId(id) : null + const { apiServerConfig, apiServerRunning } = useApiServer() const fetcher = useCallback(async () => { if (!id || id === 'fake') { return null } + if (!apiServerConfig.enabled) { + throw new Error(t('apiServer.messages.notEnabled')) + } + if (!apiServerRunning) { + throw new Error(t('agent.server.error.not_running')) + } const result = await client.getAgent(id) return result - }, [client, id]) + }, [apiServerConfig.enabled, apiServerRunning, client, id, t]) const { data, error, isLoading } = useSWR(key, id ? fetcher : null) return { diff --git a/src/renderer/src/hooks/agents/useAgents.ts b/src/renderer/src/hooks/agents/useAgents.ts index c49cf46d40..6af38228cb 100644 --- a/src/renderer/src/hooks/agents/useAgents.ts +++ b/src/renderer/src/hooks/agents/useAgents.ts @@ -6,6 +6,7 @@ import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import useSWR from 'swr' +import { useApiServer } from '../useApiServer' import { useRuntime } from '../useRuntime' import { useAgentClient } from './useAgentClient' @@ -23,11 +24,18 @@ export const useAgents = () => { const { t } = useTranslation() const client = useAgentClient() const key = client.agentPaths.base + const { apiServerConfig, apiServerRunning } = useApiServer() const fetcher = useCallback(async () => { + if (!apiServerConfig.enabled) { + throw new Error(t('apiServer.messages.notEnabled')) + } + if (!apiServerRunning) { + throw new Error(t('agent.server.error.not_running')) + } const result = await client.listAgents() // NOTE: We only use the array for now. useUpdateAgent depends on this behavior. return result.data - }, [client]) + }, [apiServerConfig.enabled, apiServerRunning, client, t]) const { data, error, isLoading, mutate } = useSWR(key, fetcher) const { chat } = useRuntime() const { activeAgentId } = chat diff --git a/src/renderer/src/hooks/useApiServer.ts b/src/renderer/src/hooks/useApiServer.ts new file mode 100644 index 0000000000..ae418f6cc0 --- /dev/null +++ b/src/renderer/src/hooks/useApiServer.ts @@ -0,0 +1,112 @@ +import { loggerService } from '@logger' +import { useAppDispatch, useAppSelector } from '@renderer/store' +import { setApiServerEnabled as setApiServerEnabledAction } from '@renderer/store/settings' +import { useCallback, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' + +const logger = loggerService.withContext('useApiServer') + +export const useApiServer = () => { + const { t } = useTranslation() + // FIXME: We currently store two copies of the config data in both the renderer and the main processes, + // which carries the risk of data inconsistency. This should be modified so that the main process stores + // the data, and the renderer retrieves it. + const apiServerConfig = useAppSelector((state) => state.settings.apiServer) + const dispatch = useAppDispatch() + + // Optimistic initial state. + const [apiServerRunning, setApiServerRunning] = useState(apiServerConfig.enabled) + const [apiServerLoading, setApiServerLoading] = useState(true) + + const setApiServerEnabled = useCallback( + (enabled: boolean) => { + dispatch(setApiServerEnabledAction(enabled)) + }, + [dispatch] + ) + + // API Server functions + const checkApiServerStatus = useCallback(async () => { + setApiServerLoading(true) + try { + const status = await window.api.apiServer.getStatus() + setApiServerRunning(status.running) + } catch (error: any) { + logger.error('Failed to check API server status:', error) + } finally { + setApiServerLoading(false) + } + }, []) + + const startApiServer = useCallback(async () => { + if (apiServerLoading) return + + setApiServerLoading(true) + try { + const result = await window.api.apiServer.start() + if (result.success) { + setApiServerRunning(true) + window.toast.success(t('apiServer.messages.startSuccess')) + } else { + window.toast.error(t('apiServer.messages.startError') + result.error) + } + } catch (error: any) { + window.toast.error(t('apiServer.messages.startError') + (error.message || error)) + } finally { + setApiServerLoading(false) + } + }, [apiServerLoading, t]) + + const stopApiServer = useCallback(async () => { + if (apiServerLoading) return + + setApiServerLoading(true) + try { + const result = await window.api.apiServer.stop() + if (result.success) { + setApiServerRunning(false) + window.toast.success(t('apiServer.messages.stopSuccess')) + } else { + window.toast.error(t('apiServer.messages.stopError') + result.error) + } + } catch (error: any) { + window.toast.error(t('apiServer.messages.stopError') + (error.message || error)) + } finally { + setApiServerLoading(false) + } + }, [apiServerLoading, t]) + + const restartApiServer = useCallback(async () => { + if (apiServerLoading) return + + setApiServerLoading(true) + try { + const result = await window.api.apiServer.restart() + if (result.success) { + await checkApiServerStatus() + window.toast.success(t('apiServer.messages.restartSuccess')) + } else { + window.toast.error(t('apiServer.messages.restartError') + result.error) + } + } catch (error) { + window.toast.error(t('apiServer.messages.restartFailed') + (error as Error).message) + } finally { + setApiServerLoading(false) + } + }, [apiServerLoading, checkApiServerStatus, t]) + + useEffect(() => { + checkApiServerStatus() + }, [checkApiServerStatus]) + + return { + apiServerConfig, + apiServerRunning, + apiServerLoading, + startApiServer, + stopApiServer, + restartApiServer, + checkApiServerStatus, + setApiServerEnabled + } +} diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 0d46390767..288f0964b0 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -30,6 +30,11 @@ "failed": "Failed to list agents." } }, + "server": { + "error": { + "not_running": "The API server is enabled but not running properly." + } + }, "session": { "accessible_paths": { "add": "Add directory", @@ -237,6 +242,7 @@ "messages": { "apiKeyCopied": "API Key copied to clipboard", "apiKeyRegenerated": "API Key regenerated", + "notEnabled": "The API Server is not enabled.", "operationFailed": "API Server operation failed: ", "restartError": "Failed to restart API Server: ", "restartFailed": "API Server restart failed: ", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 2ae391419f..3e6bac4708 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -30,6 +30,11 @@ "failed": "获取智能体列表失败" } }, + "server": { + "error": { + "not_running": "API 服务器已启用但未正常运行。" + } + }, "session": { "accessible_paths": { "add": "添加目录", @@ -237,6 +242,7 @@ "messages": { "apiKeyCopied": "API 密钥已复制到剪贴板", "apiKeyRegenerated": "API 密钥已重新生成", + "notEnabled": "API 服务器未启用。", "operationFailed": "API 服务器操作失败:", "restartError": "重启 API 服务器失败:", "restartFailed": "API 服务器重启失败:", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 3e9f321f17..b87e5e3256 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -30,6 +30,11 @@ "failed": "無法列出代理程式。" } }, + "server": { + "error": { + "not_running": "API 伺服器已啟用,但運行不正常。" + } + }, "session": { "accessible_paths": { "add": "新增目錄", @@ -237,6 +242,7 @@ "messages": { "apiKeyCopied": "API 金鑰已複製到剪貼簿", "apiKeyRegenerated": "API 金鑰已重新生成", + "notEnabled": "API 伺服器未啟用。", "operationFailed": "API 伺服器操作失敗:", "restartError": "重新啟動 API 伺服器失敗:", "restartFailed": "API 伺服器重新啟動失敗:", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index ed2ae1aee7..a39c429d1c 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -30,6 +30,11 @@ "failed": "Αποτυχία καταχώρησης πρακτόρων." } }, + "server": { + "error": { + "not_running": "Ο διακομιστής API είναι ενεργοποιημένος αλλά δεν λειτουργεί σωστά." + } + }, "session": { "accessible_paths": { "add": "Προσθήκη καταλόγου", @@ -237,6 +242,7 @@ "messages": { "apiKeyCopied": "Το κλειδί API αντιγράφηκε στο πρόχειρο", "apiKeyRegenerated": "Το κλειδί API αναδημιουργήθηκε", + "notEnabled": "Ο διακομιστής API δεν είναι ενεργοποιημένος.", "operationFailed": "Η λειτουργία του Διακομιστή API απέτυχε: ", "restartError": "Αποτυχία επανεκκίνησης του Διακομιστή API: ", "restartFailed": "Η επανεκκίνηση του Διακομιστή API απέτυχε: ", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index eeb943c39f..219f809187 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -30,6 +30,11 @@ "failed": "Error al listar agentes." } }, + "server": { + "error": { + "not_running": "El servidor de API está habilitado pero no funciona correctamente." + } + }, "session": { "accessible_paths": { "add": "Agregar directorio", @@ -237,6 +242,7 @@ "messages": { "apiKeyCopied": "Clave API copiada al portapapeles", "apiKeyRegenerated": "Clave API regenerada", + "notEnabled": "El servidor de API no está habilitado.", "operationFailed": "Falló la operación del Servidor API: ", "restartError": "Error al reiniciar el Servidor API: ", "restartFailed": "Falló el reinicio del Servidor API: ", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index b0e4a1afed..72899c3c13 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -30,6 +30,11 @@ "failed": "Échec de la liste des agents." } }, + "server": { + "error": { + "not_running": "Le serveur API est activé mais ne fonctionne pas correctement." + } + }, "session": { "accessible_paths": { "add": "Ajouter un répertoire", @@ -237,6 +242,7 @@ "messages": { "apiKeyCopied": "Clé API copiée dans le presse-papiers", "apiKeyRegenerated": "Clé API régénérée", + "notEnabled": "Le serveur API n'est pas activé.", "operationFailed": "Opération du Serveur API échouée : ", "restartError": "Échec du redémarrage du Serveur API : ", "restartFailed": "Redémarrage du Serveur API échoué : ", diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index 7d07988276..5c7e77400c 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -30,6 +30,11 @@ "failed": "エージェントの一覧取得に失敗しました。" } }, + "server": { + "error": { + "not_running": "APIサーバーは有効になっていますが、正常に動作していません。" + } + }, "session": { "accessible_paths": { "add": "ディレクトリを追加", @@ -237,6 +242,7 @@ "messages": { "apiKeyCopied": "API キーがクリップボードにコピーされました", "apiKeyRegenerated": "API キーが再生成されました", + "notEnabled": "APIサーバーが有効になっていません。", "operationFailed": "API サーバーの操作に失敗しました:", "restartError": "API サーバーの再起動に失敗しました:", "restartFailed": "API サーバーの再起動に失敗しました:", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index e4707b846d..f10aea90df 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -30,6 +30,11 @@ "failed": "Falha ao listar agentes." } }, + "server": { + "error": { + "not_running": "O servidor de API está habilitado, mas não está funcionando corretamente." + } + }, "session": { "accessible_paths": { "add": "Adicionar diretório", @@ -237,6 +242,7 @@ "messages": { "apiKeyCopied": "Chave API copiada para a área de transferência", "apiKeyRegenerated": "Chave API regenerada", + "notEnabled": "O Servidor de API não está habilitado.", "operationFailed": "Operação do Servidor API falhou: ", "restartError": "Falha ao reiniciar o Servidor API: ", "restartFailed": "Reinício do Servidor API falhou: ", diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index d4c631d81c..bf4533a4ac 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -30,6 +30,11 @@ "failed": "Не удалось получить список агентов." } }, + "server": { + "error": { + "not_running": "API-сервер включен, но работает неправильно." + } + }, "session": { "accessible_paths": { "add": "Добавить каталог", @@ -237,6 +242,7 @@ "messages": { "apiKeyCopied": "API ключ скопирован в буфер обмена", "apiKeyRegenerated": "API ключ перегенерирован", + "notEnabled": "API-сервер не включен.", "operationFailed": "Операция API сервера не удалась: ", "restartError": "Не удалось перезапустить API сервер: ", "restartFailed": "Перезапуск API сервера не удался: ", diff --git a/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx b/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx index 2eaa2ef1e3..f4588f60a0 100644 --- a/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx @@ -1,15 +1,16 @@ import { Alert, Spinner } from '@heroui/react' import Scrollbar from '@renderer/components/Scrollbar' import { useAgents } from '@renderer/hooks/agents/useAgents' +import { useApiServer } from '@renderer/hooks/useApiServer' import { useAssistants } from '@renderer/hooks/useAssistant' import { useAssistantPresets } from '@renderer/hooks/useAssistantPresets' import { useRuntime } from '@renderer/hooks/useRuntime' -import { useSettings } from '@renderer/hooks/useSettings' import { useAssistantsTabSortType } from '@renderer/hooks/useStore' import { useTags } from '@renderer/hooks/useTags' import { useAppDispatch } from '@renderer/store' import { addIknowAction } from '@renderer/store/runtime' import { Assistant, AssistantsSortType } from '@renderer/types' +import { getErrorMessage } from '@renderer/utils' import { FC, useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' @@ -35,7 +36,8 @@ const AssistantsTab: FC = (props) => { const { activeAssistant, setActiveAssistant, onCreateAssistant, onCreateDefaultAssistant } = props const containerRef = useRef(null) const { t } = useTranslation() - const { apiServer } = useSettings() + const { apiServerConfig, apiServerRunning } = useApiServer() + const apiServerEnabled = apiServerConfig.enabled const { iknow, chat } = useRuntime() const dispatch = useAppDispatch() @@ -55,7 +57,7 @@ const AssistantsTab: FC = (props) => { const { unifiedItems, handleUnifiedListReorder } = useUnifiedItems({ agents, assistants, - apiServerEnabled: apiServer.enabled, + apiServerEnabled, agentsLoading, agentsError, updateAssistants @@ -72,17 +74,17 @@ const AssistantsTab: FC = (props) => { unifiedItems, assistants, agents, - apiServerEnabled: apiServer.enabled, + apiServerEnabled, agentsLoading, agentsError, updateAssistants }) useEffect(() => { - if (!agentsLoading && agents.length > 0 && !activeAgentId && apiServer.enabled) { + if (!agentsLoading && agents.length > 0 && !activeAgentId && apiServerConfig.enabled) { setActiveAgentId(agents[0].id) } - }, [agentsLoading, agents, activeAgentId, setActiveAgentId, apiServer.enabled]) + }, [agentsLoading, agents, activeAgentId, setActiveAgentId, apiServerConfig.enabled]) const onDeleteAssistant = useCallback( (assistant: Assistant) => { @@ -105,7 +107,7 @@ const AssistantsTab: FC = (props) => { return ( - {!apiServer.enabled && !iknow[ALERT_KEY] && ( + {!apiServerConfig.enabled && !iknow[ALERT_KEY] && ( = (props) => { onClose={() => { dispatch(addIknowAction(ALERT_KEY)) }} + className="mb-2" /> )} {agentsLoading && } - {apiServer.enabled && agentsError && } + {apiServerConfig.enabled && !apiServerRunning && ( + + )} + {apiServerConfig.enabled && apiServerRunning && agentsError && ( + + )} {assistantsTabSortType === 'tags' ? ( { @@ -25,67 +23,25 @@ const ApiServerSettings: FC = () => { // API Server state with proper defaults const apiServerConfig = useSelector((state: RootState) => state.settings.apiServer) - - const [apiServerRunning, setApiServerRunning] = useState(false) - const [apiServerLoading, setApiServerLoading] = useState(false) - - // API Server functions - const checkApiServerStatus = async () => { - try { - const status = await window.electron.ipcRenderer.invoke(IpcChannel.ApiServer_GetStatus) - setApiServerRunning(status.running) - } catch (error: any) { - logger.error('Failed to check API server status:', error) - } - } - - useEffect(() => { - checkApiServerStatus() - }, []) + const { apiServerRunning, apiServerLoading, startApiServer, stopApiServer, restartApiServer, setApiServerEnabled } = + useApiServer() const handleApiServerToggle = async (enabled: boolean) => { - setApiServerLoading(true) try { if (enabled) { - const result = await window.electron.ipcRenderer.invoke(IpcChannel.ApiServer_Start) - if (result.success) { - setApiServerRunning(true) - window.toast.success(t('apiServer.messages.startSuccess')) - } else { - window.toast.error(t('apiServer.messages.startError') + result.error) - } + await startApiServer() } else { - const result = await window.electron.ipcRenderer.invoke(IpcChannel.ApiServer_Stop) - if (result.success) { - setApiServerRunning(false) - window.toast.success(t('apiServer.messages.stopSuccess')) - } else { - window.toast.error(t('apiServer.messages.stopError') + result.error) - } + await stopApiServer() } } catch (error) { window.toast.error(t('apiServer.messages.operationFailed') + formatErrorMessage(error)) } finally { - dispatch(setApiServerEnabled(enabled)) - setApiServerLoading(false) + setApiServerEnabled(enabled) } } const handleApiServerRestart = async () => { - setApiServerLoading(true) - try { - const result = await window.electron.ipcRenderer.invoke(IpcChannel.ApiServer_Restart) - if (result.success) { - await checkApiServerStatus() - window.toast.success(t('apiServer.messages.restartSuccess')) - } else { - window.toast.error(t('apiServer.messages.restartError') + result.error) - } - } catch (error) { - window.toast.error(t('apiServer.messages.restartFailed') + (error as Error).message) - } finally { - setApiServerLoading(false) - } + await restartApiServer() } const copyApiKey = () => { diff --git a/src/renderer/src/types/apiServer.ts b/src/renderer/src/types/apiServer.ts new file mode 100644 index 0000000000..316b8fe632 --- /dev/null +++ b/src/renderer/src/types/apiServer.ts @@ -0,0 +1,38 @@ +export type ApiServerConfig = { + enabled: boolean + host: string + port: number + apiKey: string +} + +export type GetApiServerStatusResult = { + running: boolean + config: ApiServerConfig | null +} + +export type StartApiServerStatusResult = + | { + success: true + } + | { + success: false + error: string + } + +export type RestartApiServerStatusResult = + | { + success: true + } + | { + success: false + error: string + } + +export type StopApiServerStatusResult = + | { + success: true + } + | { + success: false + error: string + } diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index 2f201b3fc8..96bbdbd4e5 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -17,6 +17,7 @@ import type { BaseTool, MCPTool } from './tool' export * from './agent' export * from './apiModels' +export * from './apiServer' export * from './knowledge' export * from './mcp' export * from './notification' @@ -848,13 +849,6 @@ export type S3Config = { } export type { Message } from './newMessage' - -export interface ApiServerConfig { - enabled: boolean - host: string - port: number - apiKey: string -} export * from './tool' // Memory Service Types From 96ce6450642eaaa0fc7d94b0fe2a7d3e37b8a2fc Mon Sep 17 00:00:00 2001 From: Calcium-Ion <61247483+Calcium-Ion@users.noreply.github.com> Date: Thu, 16 Oct 2025 13:07:28 +0800 Subject: [PATCH 13/13] feat: support NewAPI as a generic provider type (#10696) * feat: add support for New API providerType * feat: support New API as a generic painting provider * refactor: update styling in painting pages to use Tailwind classes - Replaced inline styles with Tailwind CSS classes for margin adjustments in AihubmixPage, DmxapiPage, SiliconPage, TokenFluxPage, and ZhipuPage. - Enhanced consistency and maintainability of the codebase by standardizing styling approach across components. - Minor refactor in ProviderSelect component to support className prop for better styling flexibility. --- .../src/aiCore/provider/providerConfig.ts | 7 +- src/renderer/src/config/providers.ts | 4 +- .../src/pages/paintings/AihubmixPage.tsx | 39 ++------ .../src/pages/paintings/DmxapiPage.tsx | 72 +++++--------- .../src/pages/paintings/NewApiPage.tsx | 82 ++++++--------- .../pages/paintings/PaintingsRoutePage.tsx | 21 +++- .../src/pages/paintings/SiliconPage.tsx | 46 ++------- .../src/pages/paintings/TokenFluxPage.tsx | 38 +------ .../src/pages/paintings/ZhipuPage.tsx | 34 +------ .../paintings/components/ProviderSelect.tsx | 99 +++++++++++++++++++ .../ProviderSettings/AddProviderPopup.tsx | 3 +- src/renderer/src/types/index.ts | 2 + src/renderer/src/types/provider.ts | 3 +- 13 files changed, 196 insertions(+), 254 deletions(-) create mode 100644 src/renderer/src/pages/paintings/components/ProviderSelect.tsx diff --git a/src/renderer/src/aiCore/provider/providerConfig.ts b/src/renderer/src/aiCore/provider/providerConfig.ts index 1fa34e5a1a..0a3bbc7b58 100644 --- a/src/renderer/src/aiCore/provider/providerConfig.ts +++ b/src/renderer/src/aiCore/provider/providerConfig.ts @@ -63,13 +63,14 @@ function handleSpecialProviders(model: Model, provider: Provider): Provider { // return createVertexProvider(provider) // } + if (isNewApiProvider(provider)) { + return newApiResolverCreator(model, provider) + } + if (isSystemProvider(provider)) { if (provider.id === 'aihubmix') { return aihubmixProviderCreator(model, provider) } - if (isNewApiProvider(provider)) { - return newApiResolverCreator(model, provider) - } if (provider.id === 'vertexai') { return vertexAnthropicProviderCreator(model, provider) } diff --git a/src/renderer/src/config/providers.ts b/src/renderer/src/config/providers.ts index aa734260f7..22faf0fb0e 100644 --- a/src/renderer/src/config/providers.ts +++ b/src/renderer/src/config/providers.ts @@ -289,7 +289,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record = 'new-api': { id: 'new-api', name: 'New API', - type: 'openai', + type: 'new-api', apiKey: '', apiHost: 'http://localhost:3000', anthropicApiHost: 'http://localhost:3000', @@ -1432,5 +1432,5 @@ export const isGeminiWebSearchProvider = (provider: Provider) => { } export const isNewApiProvider = (provider: Provider) => { - return ['new-api', 'cherryin'].includes(provider.id) + return ['new-api', 'cherryin'].includes(provider.id) || provider.type === 'new-api' } diff --git a/src/renderer/src/pages/paintings/AihubmixPage.tsx b/src/renderer/src/pages/paintings/AihubmixPage.tsx index cf6a264a54..507554f1ce 100644 --- a/src/renderer/src/pages/paintings/AihubmixPage.tsx +++ b/src/renderer/src/pages/paintings/AihubmixPage.tsx @@ -14,7 +14,6 @@ import { usePaintings } from '@renderer/hooks/usePaintings' import { useAllProviders } from '@renderer/hooks/useProvider' import { useRuntime } from '@renderer/hooks/useRuntime' import { useSettings } from '@renderer/hooks/useSettings' -import { getProviderLabel } from '@renderer/i18n/label' import FileManager from '@renderer/services/FileManager' import { translateText } from '@renderer/services/TranslateService' import { useAppDispatch } from '@renderer/store' @@ -35,6 +34,7 @@ import SendMessageButton from '../home/Inputbar/SendMessageButton' import { SettingHelpLink, SettingTitle } from '../settings' import Artboard from './components/Artboard' import PaintingsList from './components/PaintingsList' +import ProviderSelect from './components/ProviderSelect' import { type ConfigItem, createModeConfigs, DEFAULT_PAINTING } from './config/aihubmixConfig' import { checkProviderEnabled } from './utils' @@ -76,20 +76,6 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => { const { t } = useTranslation() const { theme } = useTheme() const providers = useAllProviders() - const providerOptions = Options.map((option) => { - const provider = providers.find((p) => p.id === option) - if (provider) { - return { - label: getProviderLabel(provider.id), - value: provider.id - } - } else { - return { - label: 'Unknown Provider', - value: undefined - } - } - }) const dispatch = useAppDispatch() const { generating } = useRuntime() const navigate = useNavigate() @@ -849,17 +835,12 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => { /> - - + {/* 使用JSON配置渲染设置项 */} {modeConfigs[mode].filter((item) => (item.condition ? item.condition(painting) : true)).map(renderConfigItem)} @@ -1034,12 +1015,6 @@ const ModeSegmentedContainer = styled.div` padding-top: 24px; ` -const SelectOptionContainer = styled.div` - display: flex; - align-items: center; - gap: 8px; -` - // 添加新的样式组件 const ProviderTitleContainer = styled.div` display: flex; diff --git a/src/renderer/src/pages/paintings/DmxapiPage.tsx b/src/renderer/src/pages/paintings/DmxapiPage.tsx index 2ffc9fd958..976dc4f24d 100644 --- a/src/renderer/src/pages/paintings/DmxapiPage.tsx +++ b/src/renderer/src/pages/paintings/DmxapiPage.tsx @@ -8,7 +8,6 @@ import { getProviderLogo } from '@renderer/config/providers' import { usePaintings } from '@renderer/hooks/usePaintings' import { useAllProviders } from '@renderer/hooks/useProvider' import { useRuntime } from '@renderer/hooks/useRuntime' -import { getProviderLabel } from '@renderer/i18n/label' import FileManager from '@renderer/services/FileManager' import { useAppDispatch } from '@renderer/store' import { setGenerating } from '@renderer/store/runtime' @@ -29,6 +28,7 @@ import { SettingHelpLink, SettingTitle } from '../settings' import Artboard from './components/Artboard' import ImageUploader from './components/ImageUploader' import PaintingsList from './components/PaintingsList' +import ProviderSelect from './components/ProviderSelect' import { COURSE_URL, DEFAULT_PAINTING, @@ -46,20 +46,6 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => { const [painting, setPainting] = useState(dmxapi_paintings?.[0] || DEFAULT_PAINTING) const { t } = useTranslation() const providers = useAllProviders() - const providerOptions = Options.map((option) => { - const provider = providers.find((p) => p.id === option) - if (provider) { - return { - label: getProviderLabel(provider.id), - value: provider.id - } - } else { - return { - label: 'Unknown Provider', - value: undefined - } - } - }) const dmxapiProvider = providers.find((p) => p.id === 'dmxapi')! @@ -785,9 +771,9 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => { return ( - {t('paintings.title')} + {t('paintings.title')} {isMac && ( - + @@ -797,7 +783,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => { - {t('common.provider')} + {t('common.provider')}
{t('paintings.paint_course')} @@ -805,28 +791,19 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => { {t('paintings.top_up')} - +
- + {painting.generationMode && [generationModeType.EDIT, generationModeType.MERGE].includes(painting.generationMode) && ( <> - 参考图 + 参考图 = ({ Options }) => { )} - + {t('common.model')} {painting.priceModel !== '0' ? painting.priceModel : ''} - {t('paintings.image.size')} + {t('paintings.image.size')} p.value === 'new-api')?.value} - onChange={handleProviderChange} - style={{ width: '100%' }}> - {providerOptions.map((provider) => ( - - - - {provider.label} - - - ))} - + {/* 当没有可用的 Image Generation 模型时,提示用户先去新增 */} {modelOptions.length === 0 && ( @@ -792,20 +778,12 @@ const ProviderLogo = styled(Avatar)` border: 0.5px solid var(--color-border); ` -// 添加新的样式组件 const ModeSegmentedContainer = styled.div` display: flex; justify-content: center; padding-top: 24px; ` -const SelectOptionContainer = styled.div` - display: flex; - align-items: center; - gap: 8px; -` - -// 添加新的样式组件 const ProviderTitleContainer = styled.div` display: flex; justify-content: space-between; diff --git a/src/renderer/src/pages/paintings/PaintingsRoutePage.tsx b/src/renderer/src/pages/paintings/PaintingsRoutePage.tsx index 9c0ec8a2af..a817334f6a 100644 --- a/src/renderer/src/pages/paintings/PaintingsRoutePage.tsx +++ b/src/renderer/src/pages/paintings/PaintingsRoutePage.tsx @@ -1,8 +1,10 @@ import { loggerService } from '@logger' +import { isNewApiProvider } from '@renderer/config/providers' +import { useAllProviders } from '@renderer/hooks/useProvider' import { useAppDispatch } from '@renderer/store' import { setDefaultPaintingProvider } from '@renderer/store/settings' -import { PaintingProvider } from '@renderer/types' -import { FC, useEffect } from 'react' +import { PaintingProvider, SystemProviderId } from '@renderer/types' +import { FC, useEffect, useMemo } from 'react' import { Route, Routes, useParams } from 'react-router-dom' import AihubmixPage from './AihubmixPage' @@ -14,19 +16,23 @@ import ZhipuPage from './ZhipuPage' const logger = loggerService.withContext('PaintingsRoutePage') -const Options = ['zhipu', 'aihubmix', 'silicon', 'dmxapi', 'tokenflux', 'new-api'] +const BASE_OPTIONS: SystemProviderId[] = ['zhipu', 'aihubmix', 'silicon', 'dmxapi', 'tokenflux'] const PaintingsRoutePage: FC = () => { const params = useParams() const provider = params['*'] const dispatch = useAppDispatch() + const providers = useAllProviders() + const Options = useMemo(() => { + return [...BASE_OPTIONS, ...providers.filter((p) => isNewApiProvider(p)).map((p) => p.id)] + }, [providers]) useEffect(() => { logger.debug(`defaultPaintingProvider: ${provider}`) if (provider && Options.includes(provider)) { dispatch(setDefaultPaintingProvider(provider as PaintingProvider)) } - }, [provider, dispatch]) + }, [provider, dispatch, Options]) return ( @@ -36,7 +42,12 @@ const PaintingsRoutePage: FC = () => { } /> } /> } /> - } /> + {/* new-api family providers are mounted dynamically below */} + {providers + .filter((p) => isNewApiProvider(p)) + .map((p) => ( + } /> + ))} ) } diff --git a/src/renderer/src/pages/paintings/SiliconPage.tsx b/src/renderer/src/pages/paintings/SiliconPage.tsx index 54f413f420..dac2577a44 100644 --- a/src/renderer/src/pages/paintings/SiliconPage.tsx +++ b/src/renderer/src/pages/paintings/SiliconPage.tsx @@ -12,14 +12,12 @@ import { HStack, VStack } from '@renderer/components/Layout' import Scrollbar from '@renderer/components/Scrollbar' import TranslateButton from '@renderer/components/TranslateButton' import { isMac } from '@renderer/config/constant' -import { getProviderLogo } from '@renderer/config/providers' import { LanguagesEnum } from '@renderer/config/translate' import { useTheme } from '@renderer/context/ThemeProvider' import { usePaintings } from '@renderer/hooks/usePaintings' import { useAllProviders } from '@renderer/hooks/useProvider' import { useRuntime } from '@renderer/hooks/useRuntime' import { useSettings } from '@renderer/hooks/useSettings' -import { getProviderLabel } from '@renderer/i18n/label' import { getProviderByModel } from '@renderer/services/AssistantService' import FileManager from '@renderer/services/FileManager' import { translateText } from '@renderer/services/TranslateService' @@ -27,7 +25,7 @@ import { useAppDispatch } from '@renderer/store' import { setGenerating } from '@renderer/store/runtime' import type { FileMetadata, Painting } from '@renderer/types' import { getErrorMessage, uuid } from '@renderer/utils' -import { Avatar, Button, Input, InputNumber, Radio, Select, Slider, Switch, Tooltip } from 'antd' +import { Button, Input, InputNumber, Radio, Select, Slider, Switch, Tooltip } from 'antd' import TextArea from 'antd/es/input/TextArea' import { Info } from 'lucide-react' import type { FC } from 'react' @@ -40,6 +38,7 @@ import SendMessageButton from '../home/Inputbar/SendMessageButton' import { SettingTitle } from '../settings' import Artboard from './components/Artboard' import PaintingsList from './components/PaintingsList' +import ProviderSelect from './components/ProviderSelect' import { checkProviderEnabled } from './utils' export const TEXT_TO_IMAGES_MODELS = [ @@ -115,22 +114,8 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => { const [painting, setPainting] = useState(siliconflow_paintings[0] || DEFAULT_PAINTING) const { theme } = useTheme() const providers = useAllProviders() - const providerOptions = Options.map((option) => { - const provider = providers.find((p) => p.id === option) - if (provider) { - return { - label: getProviderLabel(provider.id), - value: provider.id - } - } else { - return { - label: 'Unknown Provider', - value: undefined - } - } - }) - const siliconflowProvider = providers.find((p) => p.id === 'silicon') + const siliconFlowProvider = providers.find((p) => p.id === 'silicon')! const [currentImageIndex, setCurrentImageIndex] = useState(0) const [isLoading, setIsLoading] = useState(false) @@ -170,7 +155,7 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => { } const onGenerate = async () => { - await checkProviderEnabled(siliconflowProvider!, t) + await checkProviderEnabled(siliconFlowProvider!, t) if (painting.files.length > 0) { const confirmed = await window.modal.confirm({ @@ -389,17 +374,8 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => { {t('common.provider')} - - {t('common.model')} + + {t('common.model')} p.value === 'tokenflux')?.value} - onChange={handleProviderChange} - style={{ width: '100%' }}> - {providerOptions.map((provider) => ( - - - - {provider.label} - - - ))} - + {/* Model & Pricing Section */} = ({ Options }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [painting?.id]) // 只在painting的id改变时执行,避免无限循环 - const providerOptions = Options.map((option) => { - const provider = providers.find((p) => p.id === option) - if (provider) { - return { - label: getProviderLabel(provider.id), - value: provider.id - } - } else { - return { - label: 'Unknown Provider', - value: undefined - } - } - }) - const zhipuProvider = providers.find((p) => p.id === 'zhipu')! const [currentImageIndex, setCurrentImageIndex] = useState(0) @@ -370,16 +355,7 @@ const ZhipuPage: FC<{ Options: string[] }> = ({ Options }) => { />
- + {t('common.model')} { + const selectedKey = Array.from(keys)[0] as string + onChange(selectedKey) + }} + style={style} + className={`w-full ${className || ''}`} + renderValue={(items) => { + return items.map((item) => ( +
+
+ +
+ {item.textValue} +
+ )) + }}> + {providerOptions.map((providerOption) => ( + + + + }> + {providerOption.label} + + ))} + + ) +} + +export default ProviderSelect diff --git a/src/renderer/src/pages/settings/ProviderSettings/AddProviderPopup.tsx b/src/renderer/src/pages/settings/ProviderSettings/AddProviderPopup.tsx index f02485c454..5a20279594 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/AddProviderPopup.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/AddProviderPopup.tsx @@ -252,7 +252,8 @@ const PopupContainer: React.FC = ({ provider, resolve }) => { { label: 'OpenAI-Response', value: 'openai-response' }, { label: 'Gemini', value: 'gemini' }, { label: 'Anthropic', value: 'anthropic' }, - { label: 'Azure OpenAI', value: 'azure-openai' } + { label: 'Azure OpenAI', value: 'azure-openai' }, + { label: 'New API', value: 'new-api' } ]} /> diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index 96bbdbd4e5..7bdf186e62 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -273,6 +273,8 @@ export type PaintingParams = { id: string urls: string[] files: FileMetadata[] + // provider that this painting belongs to (for new-api family separation) + providerId?: string } export type PaintingProvider = 'zhipu' | 'aihubmix' | 'silicon' | 'dmxapi' | 'new-api' diff --git a/src/renderer/src/types/provider.ts b/src/renderer/src/types/provider.ts index e5233c196e..782d8e98fd 100644 --- a/src/renderer/src/types/provider.ts +++ b/src/renderer/src/types/provider.ts @@ -11,7 +11,8 @@ export const ProviderTypeSchema = z.enum([ 'vertexai', 'mistral', 'aws-bedrock', - 'vertex-anthropic' + 'vertex-anthropic', + 'new-api' ]) export type ProviderType = z.infer