feat: add cherryin provider (#9681)
* feat: add Zhipu logo and update related images - Introduced a new Zhipu logo component in SVG format. - Updated existing image assets for chatglm, zhipu, and zhipu_dark. - Added a new zhipu image for search functionality. * feat: integrate Cherryin signature generation - Added a new integration for Cherryin, including a signature generation feature. - Updated IPC channels to handle Cherryin requests. - Introduced a new JavaScript file for Cherryin integration. - Modified configuration files to include Cherryin client secrets and paths. - Enhanced the ESLint and TypeScript configurations to accommodate the new integration. * feat: add Zhipu search provider and logo integration - Implemented a new ZhipuProvider for web search functionality. - Added Zhipu logo to the WebSearchButton component. - Updated WebSearchProviderFactory to include Zhipu as a search option. - Enhanced error handling and logging for Zhipu search requests. * feat: add cherryin provider * fix: correct import path for CherryinAPIClient and update paintings state structure in migration * chore: update version number to 1.5.8 in package.json * feat: enhance model filtering in SelectModelPopup - Added support for identifying free trial models in the model filtering logic. - Updated the condition for determining free models to include both free and free trial models. * refactor: update navigation to use query parameters for provider settings - Modified navigation logic in FreeTrialModelTag and related utilities to use query parameters instead of state for provider identification. - Removed unused useLocation hook in ProviderList component to streamline state management. * fix: remove provider ID from search parameters on selection change in ProviderList * refactor: remove free trial model references and update related logic - Eliminated the FreeTrialTag component and its associated logic from ModelTagsWithLabel and SelectModelPopup. - Updated model filtering to only consider free models without the free trial distinction. - Removed translations and utility functions related to free trial models across multiple locales. * fix: prevent mutation of read-only properties in web search provider - Updated the addWebSearchProvider function to clone the provider object before pushing it to the state, preventing mutation of read-only properties. - Enhanced the migration logic to update the apiKey for the zhipu web search provider if it exists. * refactor: streamline provider selection and navigation logic - Updated FreeTrialModelTag to directly navigate to provider settings using query parameters, removing unnecessary provider fetching. - Simplified ProviderList by eliminating the EventEmitter for provider selection and ensuring search parameters are updated correctly.
15
.github/workflows/nightly-build.yml
vendored
@ -98,8 +98,11 @@ jobs:
|
||||
yarn build:linux
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
MAIN_VITE_CHERRYIN_CLIENT_SECRET: ${{ secrets.MAIN_VITE_CHERRYIN_CLIENT_SECRET }}
|
||||
MAIN_VITE_MINERU_API_KEY: ${{ vars.MAIN_VITE_MINERU_API_KEY }}
|
||||
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
|
||||
RENDERER_VITE_PPIO_APP_SECRET: ${{ vars.RENDERER_VITE_PPIO_APP_SECRET }}
|
||||
|
||||
- name: Build Mac
|
||||
if: matrix.os == 'macos-latest'
|
||||
@ -112,9 +115,12 @@ jobs:
|
||||
APPLE_ID: ${{ vars.APPLE_ID }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ vars.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
|
||||
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
MAIN_VITE_CHERRYIN_CLIENT_SECRET: ${{ secrets.MAIN_VITE_CHERRYIN_CLIENT_SECRET }}
|
||||
MAIN_VITE_MINERU_API_KEY: ${{ vars.MAIN_VITE_MINERU_API_KEY }}
|
||||
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
|
||||
RENDERER_VITE_PPIO_APP_SECRET: ${{ vars.RENDERER_VITE_PPIO_APP_SECRET }}
|
||||
|
||||
- name: Build Windows
|
||||
if: matrix.os == 'windows-latest'
|
||||
@ -123,8 +129,11 @@ jobs:
|
||||
yarn build:win
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
MAIN_VITE_CHERRYIN_CLIENT_SECRET: ${{ secrets.MAIN_VITE_CHERRYIN_CLIENT_SECRET }}
|
||||
MAIN_VITE_MINERU_API_KEY: ${{ vars.MAIN_VITE_MINERU_API_KEY }}
|
||||
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
|
||||
RENDERER_VITE_PPIO_APP_SECRET: ${{ vars.RENDERER_VITE_PPIO_APP_SECRET }}
|
||||
|
||||
- name: Rename artifacts with nightly format
|
||||
shell: bash
|
||||
|
||||
3
.github/workflows/release.yml
vendored
@ -86,6 +86,7 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
MAIN_VITE_CHERRYIN_CLIENT_SECRET: ${{ secrets.MAIN_VITE_CHERRYIN_CLIENT_SECRET }}
|
||||
MAIN_VITE_MINERU_API_KEY: ${{ vars.MAIN_VITE_MINERU_API_KEY }}
|
||||
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
|
||||
RENDERER_VITE_PPIO_APP_SECRET: ${{ vars.RENDERER_VITE_PPIO_APP_SECRET }}
|
||||
@ -104,6 +105,7 @@ jobs:
|
||||
APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
MAIN_VITE_CHERRYIN_CLIENT_SECRET: ${{ secrets.MAIN_VITE_CHERRYIN_CLIENT_SECRET }}
|
||||
MAIN_VITE_MINERU_API_KEY: ${{ vars.MAIN_VITE_MINERU_API_KEY }}
|
||||
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
|
||||
RENDERER_VITE_PPIO_APP_SECRET: ${{ vars.RENDERER_VITE_PPIO_APP_SECRET }}
|
||||
@ -116,6 +118,7 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
MAIN_VITE_CHERRYIN_CLIENT_SECRET: ${{ secrets.MAIN_VITE_CHERRYIN_CLIENT_SECRET }}
|
||||
MAIN_VITE_MINERU_API_KEY: ${{ vars.MAIN_VITE_MINERU_API_KEY }}
|
||||
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
|
||||
RENDERER_VITE_PPIO_APP_SECRET: ${{ vars.RENDERER_VITE_PPIO_APP_SECRET }}
|
||||
|
||||
@ -7,3 +7,4 @@ tsconfig.*.json
|
||||
CHANGELOG*.md
|
||||
agents.json
|
||||
src/renderer/src/integration/nutstore/sso/lib
|
||||
src/main/integration/cherryin/index.js
|
||||
|
||||
@ -122,7 +122,8 @@ export default defineConfig([
|
||||
'.yarn/**',
|
||||
'.gitignore',
|
||||
'scripts/cloudflare-worker.js',
|
||||
'src/main/integration/nutstore/sso/lib/**'
|
||||
'src/main/integration/nutstore/sso/lib/**',
|
||||
'src/main/integration/cherryin/index.js'
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "CherryStudio",
|
||||
"version": "1.5.7-rc.2",
|
||||
"version": "1.5.8",
|
||||
"private": true,
|
||||
"description": "A powerful AI assistant for producer.",
|
||||
"main": "./out/main/index.js",
|
||||
|
||||
@ -285,5 +285,8 @@ export enum IpcChannel {
|
||||
CodeTools_Run = 'code-tools:run',
|
||||
|
||||
// OCR
|
||||
OCR_ocr = 'ocr:ocr'
|
||||
OCR_ocr = 'ocr:ocr',
|
||||
|
||||
// Cherryin
|
||||
Cherryin_GetSignature = 'cherryin:get-signature'
|
||||
}
|
||||
|
||||
@ -20,3 +20,5 @@ export const titleBarOverlayLight = {
|
||||
color: 'rgba(255,255,255,0)',
|
||||
symbolColor: '#000'
|
||||
}
|
||||
|
||||
global.CHERRYIN_CLIENT_SECRET = import.meta.env.MAIN_VITE_CHERRYIN_CLIENT_SECRET
|
||||
|
||||
1
src/main/integration/cherryin/index.js
Normal file
@ -0,0 +1 @@
|
||||
var _0x6gg;const crypto=require("\u0063\u0072\u0079\u0070\u0074\u006F");_0x6gg='\u006D\u006F\u006C\u006A\u0065\u0065';var _0x111cbe;const CLIENT_ID="oiduts-yrrehc".split("").reverse().join("");_0x111cbe=(977158^977167)+(164595^164594);var _0x6d6adc=(756649^756650)+(497587^497587);const CLIENT_SECRET_SUFFIX="\u0047\u0076\u0049\u0036\u0049\u0035\u005A\u0072\u0045\u0048\u0063\u0047\u004F\u0057\u006A\u004F\u0035\u0041\u004B\u0068\u004A\u004B\u0047\u006D\u006E\u0077\u0077\u0047\u0066\u004D\u0036\u0032\u0058\u004B\u0070\u0057\u0071\u006B\u006A\u0068\u0076\u007A\u0052\u0055\u0032\u004E\u005A\u0049\u0069\u006E\u004D\u0037\u0037\u0061\u0054\u0047\u0049\u0071\u0068\u0071\u0079\u0073\u0030\u0067";_0x6d6adc=233169^233176;const CLIENT_SECRET=global['\u0043\u0048\u0045\u0052\u0052\u0059\u0049\u004E\u005F\u0043\u004C\u0049\u0045\u004E\u0054\u005F\u0053\u0045\u0043\u0052\u0045\u0054']+"\u002E"+CLIENT_SECRET_SUFFIX;class SignatureClient{constructor(clientId,clientSecret){this['\u0063\u006C\u0069\u0065\u006E\u0074\u0049\u0064']=clientId||CLIENT_ID;this['\u0063\u006C\u0069\u0065\u006E\u0074\u0053\u0065\u0063\u0072\u0065\u0074']=clientSecret||CLIENT_SECRET;this['\u0067\u0065\u006E\u0065\u0072\u0061\u0074\u0065\u0053\u0069\u0067\u006E\u0061\u0074\u0075\u0072\u0065']=this['\u0067\u0065\u006E\u0065\u0072\u0061\u0074\u0065\u0053\u0069\u0067\u006E\u0061\u0074\u0075\u0072\u0065']['\u0062\u0069\u006E\u0064'](this);}generateSignature(options){const{"method":method,"path":path,"query":query='',"body":body=''}=options;const timestamp=Math['\u0066\u006C\u006F\u006F\u0072'](Date['\u006E\u006F\u0077']()/(110765^111429))['\u0074\u006F\u0053\u0074\u0072\u0069\u006E\u0067']();var _0xe08cc=(212246^212244)+(773521^773523);let bodyString='';_0xe08cc=(606778^606776)+(962748^962740);if(body){if(typeof body==="\u006F\u0062\u006A\u0065\u0063\u0074"){bodyString=JSON['\u0073\u0074\u0072\u0069\u006E\u0067\u0069\u0066\u0079'](body);}else{bodyString=body['\u0074\u006F\u0053\u0074\u0072\u0069\u006E\u0067']();}}const signatureParts=[method['\u0074\u006F\u0055\u0070\u0070\u0065\u0072\u0043\u0061\u0073\u0065'](),path,query,this['\u0063\u006C\u0069\u0065\u006E\u0074\u0049\u0064'],timestamp,bodyString];var _0x5693g=(936664^936668)+(685268^685277);const signatureString=signatureParts['\u006A\u006F\u0069\u006E']("\u000A");_0x5693g=(266582^266576)+(337322^337315);const hmac=crypto['\u0063\u0072\u0065\u0061\u0074\u0065\u0048\u006D\u0061\u0063']("\u0073\u0068\u0061\u0032\u0035\u0036",this['\u0063\u006C\u0069\u0065\u006E\u0074\u0053\u0065\u0063\u0072\u0065\u0074']);hmac['\u0075\u0070\u0064\u0061\u0074\u0065'](signatureString);var _0x5fba=(354480^354481)+(537437^537434);const signature=hmac['\u0064\u0069\u0067\u0065\u0073\u0074']("\u0068\u0065\u0078");_0x5fba=(249614^249610)+(915906^915914);return{'X-Client-ID':this['\u0063\u006C\u0069\u0065\u006E\u0074\u0049\u0064'],'X-Timestamp':timestamp,'X-Signature':signature};}}const signatureClient=new SignatureClient();const generateSignature=signatureClient['\u0067\u0065\u006E\u0065\u0072\u0061\u0074\u0065\u0053\u0069\u0067\u006E\u0061\u0074\u0075\u0072\u0065'];module['\u0065\u0078\u0070\u006F\u0072\u0074\u0073']={'\u0053\u0069\u0067\u006E\u0061\u0074\u0075\u0072\u0065\u0043\u006C\u0069\u0065\u006E\u0074':SignatureClient,'\u0067\u0065\u006E\u0065\u0072\u0061\u0074\u0065\u0053\u0069\u0067\u006E\u0061\u0074\u0075\u0072\u0065':generateSignature};
|
||||
@ -4,6 +4,7 @@ import path from 'node:path'
|
||||
|
||||
import { loggerService } from '@logger'
|
||||
import { isLinux, isMac, isPortable, isWin } from '@main/constant'
|
||||
import { generateSignature } from '@main/integration/cherryin'
|
||||
import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process'
|
||||
import { handleZoomFactor } from '@main/utils/zoom'
|
||||
import { SpanEntity, TokenUsage } from '@mcp-trace/trace-core'
|
||||
@ -714,4 +715,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
|
||||
// OCR
|
||||
ipcMain.handle(IpcChannel.OCR_ocr, (_, ...args: Parameters<typeof ocrService.ocr>) => ocrService.ocr(...args))
|
||||
|
||||
// CherryIN
|
||||
ipcMain.handle(IpcChannel.Cherryin_GetSignature, (_, params) => generateSignature(params))
|
||||
}
|
||||
|
||||
@ -415,6 +415,10 @@ const api = {
|
||||
ocr: {
|
||||
ocr: (file: SupportedOcrFile, provider: OcrProvider): Promise<OcrResult> =>
|
||||
ipcRenderer.invoke(IpcChannel.OCR_ocr, file, provider)
|
||||
},
|
||||
cherryin: {
|
||||
generateSignature: (params: { method: string; path: string; query: string; body: Record<string, any> }) =>
|
||||
ipcRenderer.invoke(IpcChannel.Cherryin_GetSignature, params)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { AihubmixAPIClient } from '@renderer/aiCore/clients/AihubmixAPIClient'
|
||||
import { AihubmixAPIClient } from '@renderer/aiCore/clients/aihubmix/AihubmixAPIClient'
|
||||
import { AnthropicAPIClient } from '@renderer/aiCore/clients/anthropic/AnthropicAPIClient'
|
||||
import { ApiClientFactory } from '@renderer/aiCore/clients/ApiClientFactory'
|
||||
import { GeminiAPIClient } from '@renderer/aiCore/clients/gemini/GeminiAPIClient'
|
||||
import { VertexAPIClient } from '@renderer/aiCore/clients/gemini/VertexAPIClient'
|
||||
import { NewAPIClient } from '@renderer/aiCore/clients/NewAPIClient'
|
||||
import { NewAPIClient } from '@renderer/aiCore/clients/newapi/NewAPIClient'
|
||||
import { OpenAIAPIClient } from '@renderer/aiCore/clients/openai/OpenAIApiClient'
|
||||
import { OpenAIResponseAPIClient } from '@renderer/aiCore/clients/openai/OpenAIResponseAPIClient'
|
||||
import { EndpointType, Model, Provider } from '@renderer/types'
|
||||
@ -16,6 +16,7 @@ vi.mock('@renderer/config/models', () => ({
|
||||
{ id: 'gpt-4', name: 'GPT-4' },
|
||||
{ id: 'gpt-4', name: 'GPT-4' }
|
||||
],
|
||||
zhipu: [],
|
||||
silicon: [],
|
||||
openai: [],
|
||||
anthropic: [],
|
||||
@ -32,7 +33,13 @@ vi.mock('@renderer/config/models', () => ({
|
||||
isWebSearchModel: vi.fn().mockReturnValue(false),
|
||||
findTokenLimit: vi.fn().mockReturnValue(4096),
|
||||
isFunctionCallingModel: vi.fn().mockReturnValue(false),
|
||||
DEFAULT_MAX_TOKENS: 4096
|
||||
DEFAULT_MAX_TOKENS: 4096,
|
||||
glm45FlashModel: {
|
||||
id: 'glm-4.5-flash',
|
||||
name: 'GLM-4.5-Flash',
|
||||
provider: 'cherryin',
|
||||
group: 'GLM-4.5'
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@renderer/services/AssistantService', () => ({
|
||||
|
||||
@ -1,16 +1,18 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { Provider } from '@renderer/types'
|
||||
|
||||
import { AihubmixAPIClient } from './AihubmixAPIClient'
|
||||
import { AihubmixAPIClient } from './aihubmix/AihubmixAPIClient'
|
||||
import { AnthropicAPIClient } from './anthropic/AnthropicAPIClient'
|
||||
import { AwsBedrockAPIClient } from './aws/AwsBedrockAPIClient'
|
||||
import { BaseApiClient } from './BaseApiClient'
|
||||
import { CherryinAPIClient } from './cherryin/CherryinAPIClient'
|
||||
import { GeminiAPIClient } from './gemini/GeminiAPIClient'
|
||||
import { VertexAPIClient } from './gemini/VertexAPIClient'
|
||||
import { NewAPIClient } from './NewAPIClient'
|
||||
import { NewAPIClient } from './newapi/NewAPIClient'
|
||||
import { OpenAIAPIClient } from './openai/OpenAIApiClient'
|
||||
import { OpenAIResponseAPIClient } from './openai/OpenAIResponseAPIClient'
|
||||
import { PPIOAPIClient } from './ppio/PPIOAPIClient'
|
||||
import { ZhipuAPIClient } from './zhipu/ZhipuAPIClient'
|
||||
|
||||
const logger = loggerService.withContext('ApiClientFactory')
|
||||
|
||||
@ -31,24 +33,36 @@ export class ApiClientFactory {
|
||||
|
||||
let instance: BaseApiClient
|
||||
|
||||
// 首先检查特殊的provider id
|
||||
// 首先检查特殊的 Provider ID
|
||||
if (provider.id === 'cherryin') {
|
||||
instance = new CherryinAPIClient(provider) as BaseApiClient
|
||||
return instance
|
||||
}
|
||||
|
||||
if (provider.id === 'aihubmix') {
|
||||
logger.debug(`Creating AihubmixAPIClient for provider: ${provider.id}`)
|
||||
instance = new AihubmixAPIClient(provider) as BaseApiClient
|
||||
return instance
|
||||
}
|
||||
|
||||
if (provider.id === 'new-api') {
|
||||
logger.debug(`Creating NewAPIClient for provider: ${provider.id}`)
|
||||
instance = new NewAPIClient(provider) as BaseApiClient
|
||||
return instance
|
||||
}
|
||||
|
||||
if (provider.id === 'ppio') {
|
||||
logger.debug(`Creating PPIOAPIClient for provider: ${provider.id}`)
|
||||
instance = new PPIOAPIClient(provider) as BaseApiClient
|
||||
return instance
|
||||
}
|
||||
|
||||
// 然后检查标准的provider type
|
||||
if (provider.id === 'zhipu') {
|
||||
instance = new ZhipuAPIClient(provider) as BaseApiClient
|
||||
return instance
|
||||
}
|
||||
|
||||
// 然后检查标准的 Provider Type
|
||||
switch (provider.type) {
|
||||
case 'openai':
|
||||
instance = new OpenAIAPIClient(provider) as BaseApiClient
|
||||
@ -78,8 +92,3 @@ export class ApiClientFactory {
|
||||
return instance
|
||||
}
|
||||
}
|
||||
|
||||
// 移除这个函数,它已经移动到 utils/index.ts
|
||||
// export function isOpenAIProvider(provider: Provider) {
|
||||
// return !['anthropic', 'gemini'].includes(provider.type)
|
||||
// }
|
||||
|
||||
@ -2,13 +2,13 @@ import { Provider } from '@renderer/types'
|
||||
import { isOpenAIProvider } from '@renderer/utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { AihubmixAPIClient } from '../AihubmixAPIClient'
|
||||
import { AihubmixAPIClient } from '../aihubmix/AihubmixAPIClient'
|
||||
import { AnthropicAPIClient } from '../anthropic/AnthropicAPIClient'
|
||||
import { ApiClientFactory } from '../ApiClientFactory'
|
||||
import { AwsBedrockAPIClient } from '../aws/AwsBedrockAPIClient'
|
||||
import { GeminiAPIClient } from '../gemini/GeminiAPIClient'
|
||||
import { VertexAPIClient } from '../gemini/VertexAPIClient'
|
||||
import { NewAPIClient } from '../NewAPIClient'
|
||||
import { NewAPIClient } from '../newapi/NewAPIClient'
|
||||
import { OpenAIAPIClient } from '../openai/OpenAIApiClient'
|
||||
import { OpenAIResponseAPIClient } from '../openai/OpenAIResponseAPIClient'
|
||||
import { PPIOAPIClient } from '../ppio/PPIOAPIClient'
|
||||
@ -26,7 +26,7 @@ const createTestProvider = (id: string, type: string): Provider => ({
|
||||
})
|
||||
|
||||
// Mock 所有客户端模块
|
||||
vi.mock('../AihubmixAPIClient', () => ({
|
||||
vi.mock('../aihubmix/AihubmixAPIClient', () => ({
|
||||
AihubmixAPIClient: vi.fn().mockImplementation(() => ({}))
|
||||
}))
|
||||
vi.mock('../anthropic/AnthropicAPIClient', () => ({
|
||||
@ -41,7 +41,7 @@ vi.mock('../gemini/GeminiAPIClient', () => ({
|
||||
vi.mock('../gemini/VertexAPIClient', () => ({
|
||||
VertexAPIClient: vi.fn().mockImplementation(() => ({}))
|
||||
}))
|
||||
vi.mock('../NewAPIClient', () => ({
|
||||
vi.mock('../newapi/NewAPIClient', () => ({
|
||||
NewAPIClient: vi.fn().mockImplementation(() => ({}))
|
||||
}))
|
||||
vi.mock('../openai/OpenAIApiClient', () => ({
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import { isOpenAILLMModel } from '@renderer/config/models'
|
||||
import { Model, Provider } from '@renderer/types'
|
||||
|
||||
import { AnthropicAPIClient } from './anthropic/AnthropicAPIClient'
|
||||
import { BaseApiClient } from './BaseApiClient'
|
||||
import { GeminiAPIClient } from './gemini/GeminiAPIClient'
|
||||
import { MixedBaseAPIClient } from './MixedBaseApiClient'
|
||||
import { OpenAIAPIClient } from './openai/OpenAIApiClient'
|
||||
import { OpenAIResponseAPIClient } from './openai/OpenAIResponseAPIClient'
|
||||
import { AnthropicAPIClient } from '../anthropic/AnthropicAPIClient'
|
||||
import { BaseApiClient } from '../BaseApiClient'
|
||||
import { GeminiAPIClient } from '../gemini/GeminiAPIClient'
|
||||
import { MixedBaseAPIClient } from '../MixedBaseApiClient'
|
||||
import { OpenAIAPIClient } from '../openai/OpenAIApiClient'
|
||||
import { OpenAIResponseAPIClient } from '../openai/OpenAIResponseAPIClient'
|
||||
|
||||
/**
|
||||
* AihubmixAPIClient - 根据模型类型自动选择合适的ApiClient
|
||||
@ -0,0 +1,51 @@
|
||||
import { Provider } from '@renderer/types'
|
||||
import { OpenAISdkParams, OpenAISdkRawOutput } from '@renderer/types/sdk'
|
||||
import OpenAI from 'openai'
|
||||
|
||||
import { OpenAIAPIClient } from '../openai/OpenAIApiClient'
|
||||
|
||||
export class CherryinAPIClient extends OpenAIAPIClient {
|
||||
constructor(provider: Provider) {
|
||||
super(provider)
|
||||
}
|
||||
|
||||
override async createCompletions(
|
||||
payload: OpenAISdkParams,
|
||||
options?: OpenAI.RequestOptions
|
||||
): Promise<OpenAISdkRawOutput> {
|
||||
const sdk = await this.getSdkInstance()
|
||||
options = options || {}
|
||||
options.headers = options.headers || {}
|
||||
|
||||
const signature = await window.api.cherryin.generateSignature({
|
||||
method: 'POST',
|
||||
path: '/chat/completions',
|
||||
query: '',
|
||||
body: payload
|
||||
})
|
||||
|
||||
options.headers = {
|
||||
...options.headers,
|
||||
...signature
|
||||
}
|
||||
|
||||
// @ts-ignore - SDK参数可能有额外的字段
|
||||
return await sdk.chat.completions.create(payload, options)
|
||||
}
|
||||
|
||||
override getClientCompatibilityType(): string[] {
|
||||
return ['CherryinAPIClient']
|
||||
}
|
||||
|
||||
public async listModels(): Promise<OpenAI.Models.Model[]> {
|
||||
const models = ['glm-4.5-flash', 'Qwen/Qwen3-8B']
|
||||
|
||||
const created = Date.now()
|
||||
return models.map((id) => ({
|
||||
id,
|
||||
owned_by: 'cherryin',
|
||||
object: 'model' as const,
|
||||
created
|
||||
}))
|
||||
}
|
||||
}
|
||||
@ -3,4 +3,6 @@ export * from './BaseApiClient'
|
||||
export * from './types'
|
||||
|
||||
// Export specific clients from subdirectories
|
||||
export * from './anthropic/AnthropicAPIClient'
|
||||
export * from './openai/OpenAIApiClient'
|
||||
export * from './openai/OpenAIResponseAPIClient'
|
||||
|
||||
@ -3,12 +3,12 @@ import { isSupportedModel } from '@renderer/config/models'
|
||||
import { Model, Provider } from '@renderer/types'
|
||||
import { NewApiModel } from '@renderer/types/sdk'
|
||||
|
||||
import { AnthropicAPIClient } from './anthropic/AnthropicAPIClient'
|
||||
import { BaseApiClient } from './BaseApiClient'
|
||||
import { GeminiAPIClient } from './gemini/GeminiAPIClient'
|
||||
import { MixedBaseAPIClient } from './MixedBaseApiClient'
|
||||
import { OpenAIAPIClient } from './openai/OpenAIApiClient'
|
||||
import { OpenAIResponseAPIClient } from './openai/OpenAIResponseAPIClient'
|
||||
import { AnthropicAPIClient } from '../anthropic/AnthropicAPIClient'
|
||||
import { BaseApiClient } from '../BaseApiClient'
|
||||
import { GeminiAPIClient } from '../gemini/GeminiAPIClient'
|
||||
import { MixedBaseAPIClient } from '../MixedBaseApiClient'
|
||||
import { OpenAIAPIClient } from '../openai/OpenAIApiClient'
|
||||
import { OpenAIResponseAPIClient } from '../openai/OpenAIResponseAPIClient'
|
||||
|
||||
const logger = loggerService.withContext('NewAPIClient')
|
||||
|
||||
@ -122,13 +122,11 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
|
||||
if (!isReasoningModel(model)) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const reasoningEffort = assistant?.settings?.reasoning_effort
|
||||
|
||||
if (isSupportedThinkingTokenZhipuModel(model)) {
|
||||
if (!reasoningEffort) {
|
||||
return { thinking: { type: 'disabled' } }
|
||||
}
|
||||
return { thinking: { type: 'enabled' } }
|
||||
return { thinking: { type: reasoningEffort ? 'enabled' : 'disabled' } }
|
||||
}
|
||||
|
||||
if (!reasoningEffort) {
|
||||
|
||||
100
src/renderer/src/aiCore/clients/zhipu/ZhipuAPIClient.ts
Normal file
@ -0,0 +1,100 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { Provider } from '@renderer/types'
|
||||
import { GenerateImageParams } from '@renderer/types'
|
||||
import OpenAI from 'openai'
|
||||
|
||||
import { OpenAIAPIClient } from '../openai/OpenAIApiClient'
|
||||
|
||||
const logger = loggerService.withContext('ZhipuAPIClient')
|
||||
|
||||
export class ZhipuAPIClient extends OpenAIAPIClient {
|
||||
constructor(provider: Provider) {
|
||||
super(provider)
|
||||
}
|
||||
|
||||
override getClientCompatibilityType(): string[] {
|
||||
return ['ZhipuAPIClient']
|
||||
}
|
||||
|
||||
override async generateImage({
|
||||
model,
|
||||
prompt,
|
||||
negativePrompt,
|
||||
imageSize,
|
||||
batchSize,
|
||||
signal,
|
||||
quality
|
||||
}: GenerateImageParams): Promise<string[]> {
|
||||
const sdk = await this.getSdkInstance()
|
||||
|
||||
// 智谱AI使用不同的参数格式
|
||||
const body: any = {
|
||||
model,
|
||||
prompt
|
||||
}
|
||||
|
||||
// 智谱AI特有的参数格式
|
||||
body.size = imageSize
|
||||
body.n = batchSize
|
||||
if (negativePrompt) {
|
||||
body.negative_prompt = negativePrompt
|
||||
}
|
||||
|
||||
// 只有cogview-4-250304模型支持quality和style参数
|
||||
if (model === 'cogview-4-250304') {
|
||||
if (quality) {
|
||||
body.quality = quality
|
||||
}
|
||||
body.style = 'vivid'
|
||||
}
|
||||
|
||||
try {
|
||||
logger.debug('Calling Zhipu image generation API with params:', body)
|
||||
|
||||
const response = await sdk.images.generate(body, { signal })
|
||||
|
||||
if (response.data && response.data.length > 0) {
|
||||
return response.data.map((image: any) => image.url).filter(Boolean)
|
||||
}
|
||||
|
||||
return []
|
||||
} catch (error) {
|
||||
logger.error('Zhipu image generation failed:', error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
public async listModels(): Promise<OpenAI.Models.Model[]> {
|
||||
const models = [
|
||||
'glm-4.5',
|
||||
'glm-4.5-x',
|
||||
'glm-4.5-air',
|
||||
'glm-4.5-airx',
|
||||
'glm-4.5-flash',
|
||||
'glm-4.5v',
|
||||
'glm-z1-air',
|
||||
'glm-z1-airx',
|
||||
'cogview-3-flash',
|
||||
'cogview-4-250304',
|
||||
'glm-4-long',
|
||||
'glm-4-plus',
|
||||
'glm-4-air-250414',
|
||||
'glm-4-airx',
|
||||
'glm-4-flashx',
|
||||
'glm-4v',
|
||||
'glm-4v-flash',
|
||||
'glm-4v-plus-0111',
|
||||
'glm-4.1v-thinking-flash',
|
||||
'glm-4-alltools',
|
||||
'embedding-3'
|
||||
]
|
||||
|
||||
const created = Date.now()
|
||||
return models.map((id) => ({
|
||||
id,
|
||||
owned_by: 'zhipu',
|
||||
object: 'model' as const,
|
||||
created
|
||||
}))
|
||||
}
|
||||
}
|
||||
@ -9,9 +9,9 @@ import type { GenerateImageParams, Model, Provider } from '@renderer/types'
|
||||
import type { RequestOptions, SdkModel } from '@renderer/types/sdk'
|
||||
import { isEnabledToolUse } from '@renderer/utils/mcp-tools'
|
||||
|
||||
import { AihubmixAPIClient } from './clients/AihubmixAPIClient'
|
||||
import { AihubmixAPIClient } from './clients/aihubmix/AihubmixAPIClient'
|
||||
import { VertexAPIClient } from './clients/gemini/VertexAPIClient'
|
||||
import { NewAPIClient } from './clients/NewAPIClient'
|
||||
import { NewAPIClient } from './clients/newapi/NewAPIClient'
|
||||
import { OpenAIResponseAPIClient } from './clients/openai/OpenAIResponseAPIClient'
|
||||
import { CompletionsMiddlewareBuilder } from './middleware/builder'
|
||||
import { MIDDLEWARE_NAME as AbortHandlerMiddlewareName } from './middleware/common/AbortHandlerMiddleware'
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { isZhipuModel } from '@renderer/config/models'
|
||||
import store from '@renderer/store'
|
||||
import { Chunk } from '@renderer/types/chunk'
|
||||
|
||||
import { CompletionsResult } from '../schemas'
|
||||
import { CompletionsParams, CompletionsResult } from '../schemas'
|
||||
import { CompletionsContext } from '../types'
|
||||
import { createErrorChunk } from '../utils'
|
||||
|
||||
@ -28,17 +30,22 @@ export const ErrorHandlerMiddleware =
|
||||
// 尝试执行下一个中间件
|
||||
return await next(ctx, params)
|
||||
} catch (error: any) {
|
||||
logger.error('ErrorHandlerMiddleware_error', error)
|
||||
logger.error(error)
|
||||
|
||||
let processedError = error
|
||||
processedError = handleError(error, params)
|
||||
|
||||
// 1. 使用通用的工具函数将错误解析为标准格式
|
||||
const errorChunk = createErrorChunk(error)
|
||||
const errorChunk = createErrorChunk(processedError)
|
||||
|
||||
// 2. 调用从外部传入的 onError 回调
|
||||
if (params.onError) {
|
||||
params.onError(error)
|
||||
params.onError(processedError)
|
||||
}
|
||||
|
||||
// 3. 根据配置决定是重新抛出错误,还是将其作为流的一部分向下传递
|
||||
if (shouldThrow) {
|
||||
throw error
|
||||
throw processedError
|
||||
}
|
||||
|
||||
// 如果不抛出,则创建一个只包含该错误块的流并向下传递
|
||||
@ -57,3 +64,70 @@ export const ErrorHandlerMiddleware =
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleError(error: any, params: CompletionsParams): any {
|
||||
if (isZhipuModel(params.assistant.model) && error.status && !params.enableGenerateImage) {
|
||||
return handleZhipuError(error)
|
||||
}
|
||||
|
||||
if (error.status === 401 || error.message.includes('401')) {
|
||||
return {
|
||||
...error,
|
||||
i18nKey: 'chat.no_api_key',
|
||||
providerId: params.assistant?.model?.provider
|
||||
}
|
||||
}
|
||||
|
||||
return error
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理智谱特定错误
|
||||
* 1. 只有对话功能(enableGenerateImage为false)才使用自定义错误处理
|
||||
* 2. 绘画功能(enableGenerateImage为true)使用通用错误处理
|
||||
*/
|
||||
function handleZhipuError(error: any): any {
|
||||
const provider = store.getState().llm.providers.find((p) => p.id === 'zhipu')
|
||||
const logger = loggerService.withContext('handleZhipuError')
|
||||
|
||||
// 定义错误模式映射
|
||||
const errorPatterns = [
|
||||
{
|
||||
condition: () => error.status === 401 || /令牌已过期|AuthenticationError|Unauthorized/i.test(error.message),
|
||||
i18nKey: 'chat.no_api_key',
|
||||
providerId: provider?.id
|
||||
},
|
||||
{
|
||||
condition: () => error.error?.code === '1304' || /限额|免费配额|free quota|rate limit/i.test(error.message),
|
||||
i18nKey: 'chat.quota_exceeded',
|
||||
providerId: provider?.id
|
||||
},
|
||||
{
|
||||
condition: () =>
|
||||
(error.status === 429 && error.error?.code === '1113') || /余额不足|insufficient balance/i.test(error.message),
|
||||
i18nKey: 'chat.insufficient_balance',
|
||||
providerId: provider?.id
|
||||
},
|
||||
{
|
||||
condition: () => !provider?.apiKey?.trim(),
|
||||
i18nKey: 'chat.no_api_key',
|
||||
providerId: provider?.id
|
||||
}
|
||||
]
|
||||
|
||||
// 遍历错误模式,返回第一个匹配的错误
|
||||
for (const pattern of errorPatterns) {
|
||||
if (pattern.condition()) {
|
||||
return {
|
||||
...error,
|
||||
providerId: pattern.providerId,
|
||||
i18nKey: pattern.i18nKey
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果不是智谱特定错误,返回原始错误
|
||||
logger.debug('🔧 不是智谱特定错误,返回原始错误')
|
||||
|
||||
return error
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 19 KiB |
BIN
src/renderer/src/assets/images/providers/cherryin.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 19 KiB |
BIN
src/renderer/src/assets/images/search/zhipu.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
81
src/renderer/src/components/FreeTrialModelTag.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
import { getProviderLabel } from '@renderer/i18n/label'
|
||||
import NavigationService from '@renderer/services/NavigationService'
|
||||
import { Model } from '@renderer/types'
|
||||
import { ArrowUpRight } from 'lucide-react'
|
||||
import { FC, MouseEvent } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import IndicatorLight from './IndicatorLight'
|
||||
import SelectModelPopup from './Popups/SelectModelPopup'
|
||||
import CustomTag from './Tags/CustomTag'
|
||||
|
||||
interface Props {
|
||||
model: Model
|
||||
showLabel?: boolean
|
||||
}
|
||||
|
||||
export const FreeTrialModelTag: FC<Props> = ({ model, showLabel = true }) => {
|
||||
if (model.provider !== 'cherryin') {
|
||||
return null
|
||||
}
|
||||
|
||||
let providerId
|
||||
|
||||
if (model.id === 'glm-4.5-flash') {
|
||||
providerId = 'zhipu'
|
||||
}
|
||||
|
||||
if (model.id === 'Qwen/Qwen3-8B') {
|
||||
providerId = 'silicon'
|
||||
}
|
||||
|
||||
const onSelectProvider = () => {
|
||||
NavigationService.navigate!(`/settings/provider?id=${providerId}`)
|
||||
}
|
||||
|
||||
const onNavigateProvider = (e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
SelectModelPopup.hide()
|
||||
NavigationService.navigate!(`/settings/provider?id=${providerId}`)
|
||||
}
|
||||
|
||||
if (!showLabel) {
|
||||
return (
|
||||
<Container>
|
||||
<CustomTag
|
||||
color="var(--color-link)"
|
||||
size={11}
|
||||
onClick={onNavigateProvider}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
{getProviderLabel(providerId)}
|
||||
<ArrowUpRight size={12} />
|
||||
</CustomTag>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<IndicatorLight size={6} color="var(--color-primary)" animation={false} shadow={false} />
|
||||
<PoweredBy>Powered by </PoweredBy>
|
||||
<LinkText onClick={onSelectProvider}>{getProviderLabel(providerId)}</LinkText>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
`
|
||||
|
||||
const PoweredBy = styled.span`
|
||||
font-size: 12px;
|
||||
color: var(--color-text-2);
|
||||
`
|
||||
|
||||
const LinkText = styled.a`
|
||||
font-size: 12px;
|
||||
color: var(--color-link);
|
||||
`
|
||||
@ -239,6 +239,18 @@ export function BochaLogo(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function ZhipuLogo(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M 11.699219 1.800781 C 12.300781 1.984375 12.832031 2.203125 13.375 2.515625 C 13.523438 2.597656 13.671875 2.679688 13.820312 2.765625 C 14.058594 2.902344 14.058594 2.902344 14.296875 3.039062 C 14.632812 3.226562 14.96875 3.417969 15.304688 3.605469 C 15.476562 3.707031 15.652344 3.804688 15.824219 3.902344 C 16.671875 4.382812 17.523438 4.859375 18.375 5.335938 C 18.804688 5.574219 19.234375 5.816406 19.660156 6.054688 C 20.257812 6.386719 20.855469 6.71875 21.449219 7.050781 C 21.460938 8.449219 21.46875 9.851562 21.472656 11.25 C 21.476562 11.902344 21.480469 12.550781 21.484375 13.203125 C 21.488281 13.828125 21.492188 14.457031 21.492188 15.082031 C 21.492188 15.324219 21.496094 15.5625 21.496094 15.800781 C 21.5 16.136719 21.5 16.472656 21.5 16.808594 C 21.503906 17.09375 21.503906 17.09375 21.503906 17.386719 C 21.449219 17.851562 21.449219 17.851562 21.242188 18.125 C 20.917969 18.359375 20.585938 18.558594 20.238281 18.757812 C 20.085938 18.84375 19.929688 18.933594 19.769531 19.027344 C 19.605469 19.121094 19.4375 19.214844 19.265625 19.3125 C 19.003906 19.460938 18.742188 19.613281 18.480469 19.761719 C 18.203125 19.917969 17.929688 20.074219 17.65625 20.234375 C 16.929688 20.648438 16.203125 21.066406 15.476562 21.484375 C 15.140625 21.675781 14.804688 21.867188 14.46875 22.0625 C 14.238281 22.195312 14.238281 22.195312 14.003906 22.328125 C 13.863281 22.410156 13.71875 22.492188 13.574219 22.574219 C 13.265625 22.761719 12.984375 22.957031 12.695312 23.171875 C 12.179688 23.46875 11.972656 23.390625 11.398438 23.25 C 10.996094 23.058594 10.996094 23.058594 10.585938 22.816406 C 10.429688 22.726562 10.277344 22.636719 10.117188 22.542969 C 9.871094 22.398438 9.871094 22.398438 9.617188 22.246094 C 9.445312 22.144531 9.273438 22.042969 9.101562 21.945312 C 8.746094 21.734375 8.386719 21.523438 8.03125 21.316406 C 7.359375 20.917969 6.683594 20.523438 6.007812 20.128906 C 5.703125 19.953125 5.402344 19.773438 5.101562 19.597656 C 4.34375 19.15625 3.578125 18.722656 2.800781 18.308594 C 2.550781 18.148438 2.550781 18.148438 2.398438 17.851562 C 2.378906 17.519531 2.367188 17.183594 2.363281 16.851562 C 2.359375 16.75 2.355469 16.648438 2.355469 16.542969 C 2.335938 15.597656 2.324219 14.652344 2.316406 13.707031 C 2.3125 13.070312 2.304688 12.4375 2.289062 11.800781 C 2.277344 11.1875 2.269531 10.574219 2.265625 9.960938 C 2.265625 9.726562 2.257812 9.492188 2.253906 9.257812 C 2.203125 7.4375 2.203125 7.4375 2.675781 6.871094 C 3.023438 6.632812 3.363281 6.464844 3.75 6.300781 C 3.914062 6.203125 4.078125 6.109375 4.246094 6.007812 C 4.402344 5.925781 4.554688 5.839844 4.710938 5.753906 C 4.839844 5.683594 4.839844 5.683594 4.972656 5.613281 C 5.152344 5.515625 5.332031 5.417969 5.515625 5.316406 C 5.992188 5.058594 6.472656 4.792969 6.949219 4.53125 C 7.046875 4.480469 7.144531 4.425781 7.242188 4.371094 C 8.195312 3.847656 9.140625 3.320312 10.085938 2.785156 C 10.234375 2.703125 10.378906 2.621094 10.53125 2.535156 C 10.664062 2.460938 10.796875 2.382812 10.933594 2.308594 C 11.109375 2.207031 11.109375 2.207031 11.285156 2.109375 C 11.542969 1.96875 11.542969 1.96875 11.699219 1.800781 Z M 11.851562 4.5 C 11.820312 4.652344 11.789062 4.800781 11.753906 4.957031 C 11.207031 7.453125 10.085938 9.570312 7.941406 11.066406 C 6.816406 11.78125 5.628906 12.113281 4.351562 12.449219 C 4.351562 12.5 4.351562 12.550781 4.351562 12.601562 C 4.582031 12.648438 4.582031 12.648438 4.824219 12.699219 C 6.101562 12.984375 7.183594 13.300781 8.25 14.101562 C 8.363281 14.183594 8.476562 14.265625 8.59375 14.351562 C 10.558594 15.933594 11.488281 18.261719 11.851562 20.699219 C 11.898438 20.699219 11.949219 20.699219 12 20.699219 C 12.03125 20.554688 12.058594 20.410156 12.089844 20.257812 C 12.6875 17.503906 13.816406 15.222656 16.242188 13.65625 C 17.253906 13.054688 18.351562 12.8125 19.5 12.601562 C 19.035156 12.289062 18.695312 12.191406 18.160156 12.046875 C 15.792969 11.332031 14.222656 9.945312 13.050781 7.800781 C 12.527344 6.726562 12.175781 5.679688 12 4.5 C 11.949219 4.5 11.902344 4.5 11.851562 4.5 Z M 11.851562 4.5 "
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
export function PoeLogo(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { PushpinOutlined } from '@ant-design/icons'
|
||||
import { FreeTrialModelTag } from '@renderer/components/FreeTrialModelTag'
|
||||
import ModelTagsWithLabel from '@renderer/components/ModelTagsWithLabel'
|
||||
import {
|
||||
EmbeddingTag,
|
||||
@ -28,7 +29,7 @@ import { classNames, filterModelsByKeywords, getFancyProviderName } from '@rende
|
||||
import { getModelTags, isFreeModel } from '@renderer/utils/model'
|
||||
import { Avatar, Button, Divider, Empty, Flex, Modal, Tooltip } from 'antd'
|
||||
import { first, sortBy } from 'lodash'
|
||||
import { SettingsIcon } from 'lucide-react'
|
||||
import { Settings2 } from 'lucide-react'
|
||||
import React, {
|
||||
ReactNode,
|
||||
startTransition,
|
||||
@ -201,6 +202,7 @@ const PopupContainer: React.FC<Props> = ({ model, resolve, modelFilter, userFilt
|
||||
(model: Model, provider: Provider, isPinned: boolean): FlatListModel => {
|
||||
const modelId = getModelUniqId(model)
|
||||
const groupName = getFancyProviderName(provider)
|
||||
const isCherryin = provider.id === 'cherryin'
|
||||
|
||||
return {
|
||||
key: isPinned ? `${modelId}_pinned` : modelId,
|
||||
@ -209,11 +211,12 @@ const PopupContainer: React.FC<Props> = ({ model, resolve, modelFilter, userFilt
|
||||
<ModelName>
|
||||
{model.name}
|
||||
{isPinned && <span style={{ color: 'var(--color-text-3)' }}> | {groupName}</span>}
|
||||
{isCherryin && <FreeTrialModelTag model={model} showLabel={false} />}
|
||||
</ModelName>
|
||||
),
|
||||
tags: (
|
||||
<TagsContainer>
|
||||
<ModelTagsWithLabel model={model} size={11} showLabel={false} showTooltip={false} />
|
||||
<ModelTagsWithLabel model={model} size={11} showLabel={true} />
|
||||
</TagsContainer>
|
||||
),
|
||||
icon: (
|
||||
@ -280,7 +283,7 @@ const PopupContainer: React.FC<Props> = ({ model, resolve, modelFilter, userFilt
|
||||
type="text"
|
||||
size="small"
|
||||
shape="circle"
|
||||
icon={<SettingsIcon size={14} color="var(--color-text-3)" style={{ pointerEvents: 'none' }} />}
|
||||
icon={<Settings2 size={12} color="var(--color-text-3)" style={{ pointerEvents: 'none' }} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setOpen(false)
|
||||
@ -576,7 +579,7 @@ const ListContainer = styled.div`
|
||||
const GroupItem = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 2px;
|
||||
position: relative;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
@ -585,6 +588,17 @@ const GroupItem = styled.div`
|
||||
color: var(--color-text-3);
|
||||
z-index: 1;
|
||||
background: var(--modal-background);
|
||||
|
||||
&:hover {
|
||||
.ant-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-btn {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
`
|
||||
|
||||
const ModelItem = styled.div`
|
||||
@ -640,13 +654,16 @@ const ModelItemLeft = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const ModelName = styled.span`
|
||||
const ModelName = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
margin: 0 8px;
|
||||
min-width: 0;
|
||||
gap: 5px;
|
||||
`
|
||||
|
||||
const TagsContainer = styled.div`
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { CloseOutlined } from '@ant-design/icons'
|
||||
import { Tooltip } from 'antd'
|
||||
import { CSSProperties, FC, memo, useMemo } from 'react'
|
||||
import { CSSProperties, FC, memo, MouseEventHandler, useMemo } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
export interface CustomTagProps {
|
||||
@ -12,7 +12,7 @@ export interface CustomTagProps {
|
||||
tooltip?: string
|
||||
closable?: boolean
|
||||
onClose?: () => void
|
||||
onClick?: () => void
|
||||
onClick?: MouseEventHandler<HTMLDivElement>
|
||||
disabled?: boolean
|
||||
inactive?: boolean
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ export const FreeTag = ({ size, showTooltip, ...restProps }: Props) => {
|
||||
color="#7cb305"
|
||||
icon={t('models.type.free')}
|
||||
tooltip={showTooltip ? t('models.type.free') : undefined}
|
||||
{...restProps}></CustomTag>
|
||||
{...restProps}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -35,7 +35,6 @@ import NamiAiSearchLogo from '@renderer/assets/images/apps/nm-search.webp?url'
|
||||
import NotebookLMAppLogo from '@renderer/assets/images/apps/notebooklm.svg?url'
|
||||
import PerplexityAppLogo from '@renderer/assets/images/apps/perplexity.webp?url'
|
||||
import PoeAppLogo from '@renderer/assets/images/apps/poe.webp?url'
|
||||
import ZhipuProviderLogo from '@renderer/assets/images/apps/qingyan.png?url'
|
||||
import QwenlmAppLogo from '@renderer/assets/images/apps/qwenlm.webp?url'
|
||||
import SensetimeAppLogo from '@renderer/assets/images/apps/sensetime.png?url'
|
||||
import SparkDeskAppLogo from '@renderer/assets/images/apps/sparkdesk.webp?url'
|
||||
@ -56,6 +55,7 @@ import DeepSeekProviderLogo from '@renderer/assets/images/providers/deepseek.png
|
||||
import GroqProviderLogo from '@renderer/assets/images/providers/groq.png?url'
|
||||
import OpenAiProviderLogo from '@renderer/assets/images/providers/openai.png?url'
|
||||
import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png?url'
|
||||
import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png?url'
|
||||
import i18n from '@renderer/i18n'
|
||||
import { MinAppType } from '@renderer/types'
|
||||
|
||||
@ -126,7 +126,8 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [
|
||||
id: 'zhipu',
|
||||
name: i18n.t('minapps.chatglm'),
|
||||
url: 'https://chatglm.cn/main/alltoolsdetail',
|
||||
logo: ZhipuProviderLogo
|
||||
logo: ZhipuProviderLogo,
|
||||
bodered: true
|
||||
},
|
||||
{
|
||||
id: 'moonshot',
|
||||
|
||||
@ -146,8 +146,11 @@ import XirangModelLogo from '@renderer/assets/images/models/xirang.png'
|
||||
import XirangModelLogoDark from '@renderer/assets/images/models/xirang_dark.png'
|
||||
import YiModelLogo from '@renderer/assets/images/models/yi.png'
|
||||
import YiModelLogoDark from '@renderer/assets/images/models/yi_dark.png'
|
||||
import ZhipuModelLogo from '@renderer/assets/images/models/zhipu.png'
|
||||
import ZhipuModelLogoDark from '@renderer/assets/images/models/zhipu_dark.png'
|
||||
import YoudaoLogo from '@renderer/assets/images/providers/netease-youdao.svg'
|
||||
import NomicLogo from '@renderer/assets/images/providers/nomic.png'
|
||||
import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png'
|
||||
import { getProviderByModel } from '@renderer/services/AssistantService'
|
||||
import {
|
||||
isSystemProviderId,
|
||||
@ -277,7 +280,8 @@ const FUNCTION_CALLING_EXCLUDED_MODELS = [
|
||||
'AIDC-AI/Marco-o1',
|
||||
'gemini-1(?:\\.[\\w-]+)?',
|
||||
'qwen-mt(?:-[\\w-]+)?',
|
||||
'gpt-5-chat(?:-[\\w-]+)?'
|
||||
'gpt-5-chat(?:-[\\w-]+)?',
|
||||
'glm-4\\.5v'
|
||||
]
|
||||
|
||||
export const FUNCTION_CALLING_REGEX = new RegExp(
|
||||
@ -515,6 +519,7 @@ export function getModelLogo(modelId: string) {
|
||||
xirang: isLight ? XirangModelLogo : XirangModelLogoDark,
|
||||
hugging: isLight ? HuggingfaceModelLogo : HuggingfaceModelLogoDark,
|
||||
youdao: YoudaoLogo,
|
||||
'embedding-3': ZhipuProviderLogo,
|
||||
embedding: isLight ? EmbeddingModelLogo : EmbeddingModelLogoDark,
|
||||
perplexity: isLight ? PerplexityModelLogo : PerplexityModelLogoDark,
|
||||
sonar: isLight ? PerplexityModelLogo : PerplexityModelLogoDark,
|
||||
@ -522,7 +527,9 @@ export function getModelLogo(modelId: string) {
|
||||
'voyage-': VoyageModelLogo,
|
||||
tokenflux: isLight ? TokenFluxModelLogo : TokenFluxModelLogoDark,
|
||||
'nomic-': NomicLogo,
|
||||
'pangu-': PanguModelLogo
|
||||
'pangu-': PanguModelLogo,
|
||||
cogview: isLight ? ZhipuModelLogo : ZhipuModelLogoDark,
|
||||
zhipu: isLight ? ZhipuModelLogo : ZhipuModelLogoDark
|
||||
}
|
||||
|
||||
for (const key in logoMap) {
|
||||
@ -535,35 +542,43 @@ export function getModelLogo(modelId: string) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
export const glm45FlashModel: Model = {
|
||||
id: 'glm-4.5-flash',
|
||||
name: 'GLM-4.5-Flash',
|
||||
provider: 'cherryin',
|
||||
group: 'GLM-4.5'
|
||||
}
|
||||
|
||||
export const qwen38bModel: Model = {
|
||||
id: 'Qwen/Qwen3-8B',
|
||||
name: 'Qwen3-8B',
|
||||
provider: 'cherryin',
|
||||
group: 'Qwen'
|
||||
}
|
||||
|
||||
export const SYSTEM_MODELS: Record<SystemProviderId | 'defaultModel', Model[]> = {
|
||||
defaultModel: [
|
||||
// Default assistant model
|
||||
glm45FlashModel,
|
||||
// Default topic naming model
|
||||
qwen38bModel,
|
||||
// Default translation model
|
||||
glm45FlashModel,
|
||||
// Default quick assistant model
|
||||
glm45FlashModel
|
||||
],
|
||||
cherryin: [
|
||||
{
|
||||
// 默认助手模型
|
||||
id: 'deepseek-ai/DeepSeek-V3',
|
||||
name: 'deepseek-ai/DeepSeek-V3',
|
||||
provider: 'silicon',
|
||||
group: 'deepseek-ai'
|
||||
id: 'glm-4.5-flash',
|
||||
name: 'GLM-4.5-Flash',
|
||||
provider: 'cherryin',
|
||||
group: 'GLM-4.5'
|
||||
},
|
||||
{
|
||||
// 默认话题命名模型
|
||||
id: 'Qwen/Qwen3-8B',
|
||||
name: 'Qwen/Qwen3-8B',
|
||||
provider: 'silicon',
|
||||
name: 'Qwen3-8B',
|
||||
provider: 'cherryin',
|
||||
group: 'Qwen'
|
||||
},
|
||||
{
|
||||
// 默认翻译模型
|
||||
id: 'deepseek-ai/DeepSeek-V3',
|
||||
name: 'deepseek-ai/DeepSeek-V3',
|
||||
provider: 'silicon',
|
||||
group: 'deepseek-ai'
|
||||
},
|
||||
{
|
||||
// 默认快捷助手模型
|
||||
id: 'deepseek-ai/DeepSeek-V3',
|
||||
name: 'deepseek-ai/DeepSeek-V3',
|
||||
provider: 'silicon',
|
||||
group: 'deepseek-ai'
|
||||
}
|
||||
],
|
||||
vertexai: [],
|
||||
@ -1225,113 +1240,35 @@ export const SYSTEM_MODELS: Record<SystemProviderId | 'defaultModel', Model[]> =
|
||||
{ id: 'yi-vision-v2', name: 'Yi Vision v2', provider: 'yi', group: 'yi-vision', owned_by: '01.ai' }
|
||||
],
|
||||
zhipu: [
|
||||
{
|
||||
id: 'glm-4.5',
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-4.5',
|
||||
group: 'GLM-4.5'
|
||||
},
|
||||
{
|
||||
id: 'glm-4.5-flash',
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-4.5-Flash',
|
||||
group: 'GLM-4.5'
|
||||
},
|
||||
{
|
||||
id: 'glm-4.5',
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-4.5',
|
||||
group: 'GLM-4.5'
|
||||
},
|
||||
{
|
||||
id: 'glm-4.5-air',
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-4.5-AIR',
|
||||
name: 'GLM-4.5-Air',
|
||||
group: 'GLM-4.5'
|
||||
},
|
||||
{
|
||||
id: 'glm-4.5-airx',
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-4.5-AIRX',
|
||||
name: 'GLM-4.5-AirX',
|
||||
group: 'GLM-4.5'
|
||||
},
|
||||
{
|
||||
id: 'glm-4.5-x',
|
||||
id: 'glm-4.5v',
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-4.5-X',
|
||||
group: 'GLM-4.5'
|
||||
},
|
||||
{
|
||||
id: 'glm-z1-air',
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-Z1-AIR',
|
||||
group: 'GLM-Z1'
|
||||
},
|
||||
{
|
||||
id: 'glm-z1-airx',
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-Z1-AIRX',
|
||||
group: 'GLM-Z1'
|
||||
},
|
||||
{
|
||||
id: 'glm-z1-flash',
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-Z1-FLASH',
|
||||
group: 'GLM-Z1'
|
||||
},
|
||||
{
|
||||
id: 'glm-4-long',
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-4-Long',
|
||||
group: 'GLM-4'
|
||||
},
|
||||
{
|
||||
id: 'glm-4-plus',
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-4-Plus',
|
||||
group: 'GLM-4'
|
||||
},
|
||||
{
|
||||
id: 'glm-4-air-250414',
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-4-Air-250414',
|
||||
group: 'GLM-4'
|
||||
},
|
||||
{
|
||||
id: 'glm-4-airx',
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-4-AirX',
|
||||
group: 'GLM-4'
|
||||
},
|
||||
{
|
||||
id: 'glm-4-flash-250414',
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-4-Flash-250414',
|
||||
group: 'GLM-4'
|
||||
},
|
||||
{
|
||||
id: 'glm-4-flashx',
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-4-FlashX',
|
||||
group: 'GLM-4'
|
||||
},
|
||||
{
|
||||
id: 'glm-4v',
|
||||
provider: 'zhipu',
|
||||
name: 'GLM 4V',
|
||||
group: 'GLM-4v'
|
||||
},
|
||||
{
|
||||
id: 'glm-4v-flash',
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-4V-Flash',
|
||||
group: 'GLM-4v'
|
||||
},
|
||||
{
|
||||
id: 'glm-4v-plus-0111',
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-4V-Plus-0111',
|
||||
group: 'GLM-4v'
|
||||
},
|
||||
{
|
||||
id: 'glm-4-alltools',
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-4-AllTools',
|
||||
group: 'GLM-4-AllTools'
|
||||
name: 'GLM-4.5V',
|
||||
group: 'GLM-4.5V'
|
||||
},
|
||||
{
|
||||
id: 'embedding-3',
|
||||
|
||||
@ -11,6 +11,7 @@ import BaiduCloudProviderLogo from '@renderer/assets/images/providers/baidu-clou
|
||||
import BailianProviderLogo from '@renderer/assets/images/providers/bailian.png'
|
||||
import BurnCloudProviderLogo from '@renderer/assets/images/providers/burncloud.png'
|
||||
import CephalonProviderLogo from '@renderer/assets/images/providers/cephalon.jpeg'
|
||||
import CherryInProviderLogo from '@renderer/assets/images/providers/cherryin.png'
|
||||
import DeepSeekProviderLogo from '@renderer/assets/images/providers/deepseek.png'
|
||||
import DmxapiProviderLogo from '@renderer/assets/images/providers/DMXAPI.png'
|
||||
import FireworksProviderLogo from '@renderer/assets/images/providers/fireworks.png'
|
||||
@ -65,6 +66,16 @@ import { TOKENFLUX_HOST } from './constant'
|
||||
import { SYSTEM_MODELS } from './models'
|
||||
|
||||
export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> = {
|
||||
cherryin: {
|
||||
id: 'cherryin',
|
||||
name: 'CherryIN',
|
||||
type: 'openai',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.cherry-ai.com/',
|
||||
models: SYSTEM_MODELS.cherryin,
|
||||
isSystem: true,
|
||||
enabled: true
|
||||
},
|
||||
silicon: {
|
||||
id: 'silicon',
|
||||
name: 'Silicon',
|
||||
@ -73,7 +84,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
|
||||
apiHost: 'https://api.siliconflow.cn',
|
||||
models: SYSTEM_MODELS.silicon,
|
||||
isSystem: true,
|
||||
enabled: true
|
||||
enabled: false
|
||||
},
|
||||
aihubmix: {
|
||||
id: 'aihubmix',
|
||||
@ -95,6 +106,16 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
zhipu: {
|
||||
id: 'zhipu',
|
||||
name: 'ZhiPu',
|
||||
type: 'openai',
|
||||
apiKey: '',
|
||||
apiHost: 'https://open.bigmodel.cn/api/paas/v4/',
|
||||
models: SYSTEM_MODELS.zhipu,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
deepseek: {
|
||||
id: 'deepseek',
|
||||
name: 'deepseek',
|
||||
@ -320,16 +341,6 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
|
||||
enabled: false,
|
||||
isAuthed: false
|
||||
},
|
||||
zhipu: {
|
||||
id: 'zhipu',
|
||||
name: 'ZhiPu',
|
||||
type: 'openai',
|
||||
apiKey: '',
|
||||
apiHost: 'https://open.bigmodel.cn/api/paas/v4/',
|
||||
models: SYSTEM_MODELS.zhipu,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
yi: {
|
||||
id: 'yi',
|
||||
name: 'Yi',
|
||||
@ -595,6 +606,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
|
||||
export const SYSTEM_PROVIDERS: SystemProvider[] = Object.values(SYSTEM_PROVIDERS_CONFIG)
|
||||
|
||||
export const PROVIDER_LOGO_MAP: AtLeast<SystemProviderId, string> = {
|
||||
cherryin: CherryInProviderLogo,
|
||||
ph8: Ph8ProviderLogo,
|
||||
'302ai': Ai302ProviderLogo,
|
||||
openai: OpenAiProviderLogo,
|
||||
@ -673,6 +685,16 @@ type ProviderUrls = {
|
||||
}
|
||||
|
||||
export const PROVIDER_URLS: Record<SystemProviderId, ProviderUrls> = {
|
||||
cherryin: {
|
||||
api: {
|
||||
url: 'https://api.cherry-ai.com'
|
||||
},
|
||||
websites: {
|
||||
official: 'https://cherry-ai.com',
|
||||
docs: 'https://docs.cherry-ai.com',
|
||||
models: 'https://docs.cherry-ai.com/pre-basic/providers/cherryin'
|
||||
}
|
||||
},
|
||||
ph8: {
|
||||
api: {
|
||||
url: 'https://ph8.co'
|
||||
|
||||
@ -8,6 +8,12 @@ type WebSearchProviderConfig = {
|
||||
}
|
||||
|
||||
export const WEB_SEARCH_PROVIDER_CONFIG: Record<WebSearchProviderId, WebSearchProviderConfig> = {
|
||||
zhipu: {
|
||||
websites: {
|
||||
official: 'https://docs.bigmodel.cn/cn/guide/tools/web-search',
|
||||
apiKey: 'https://zhipuaishengchan.datasink.sensorsdata.cn/t/yv'
|
||||
}
|
||||
},
|
||||
tavily: {
|
||||
websites: {
|
||||
official: 'https://tavily.com',
|
||||
@ -49,6 +55,12 @@ export const WEB_SEARCH_PROVIDER_CONFIG: Record<WebSearchProviderId, WebSearchPr
|
||||
}
|
||||
|
||||
export const WEB_SEARCH_PROVIDERS: WebSearchProvider[] = [
|
||||
{
|
||||
id: 'zhipu',
|
||||
name: 'Zhipu',
|
||||
apiHost: 'https://open.bigmodel.cn/api/paas/v4/web_search',
|
||||
apiKey: ''
|
||||
},
|
||||
{
|
||||
id: 'tavily',
|
||||
name: 'Tavily',
|
||||
|
||||
@ -4,32 +4,29 @@ import { addPainting, removePainting, updatePainting, updatePaintings } from '@r
|
||||
import { PaintingAction, PaintingsState } from '@renderer/types'
|
||||
|
||||
export function usePaintings() {
|
||||
const paintings = useAppSelector((state) => state.paintings.paintings)
|
||||
const generate = useAppSelector((state) => state.paintings.generate)
|
||||
const remix = useAppSelector((state) => state.paintings.remix)
|
||||
const edit = useAppSelector((state) => state.paintings.edit)
|
||||
const upscale = useAppSelector((state) => state.paintings.upscale)
|
||||
const DMXAPIPaintings = useAppSelector((state) => state.paintings.DMXAPIPaintings)
|
||||
const tokenFluxPaintings = useAppSelector((state) => state.paintings.tokenFluxPaintings)
|
||||
const siliconflow_paintings = useAppSelector((state) => state.paintings.siliconflow_paintings)
|
||||
const dmxapi_paintings = useAppSelector((state) => state.paintings.dmxapi_paintings)
|
||||
const tokenflux_paintings = useAppSelector((state) => state.paintings.tokenflux_paintings)
|
||||
const zhipu_paintings = useAppSelector((state) => state.paintings.zhipu_paintings)
|
||||
const aihubmix_image_generate = useAppSelector((state) => state.paintings.aihubmix_image_generate)
|
||||
const aihubmix_image_remix = useAppSelector((state) => state.paintings.aihubmix_image_remix)
|
||||
const aihubmix_image_edit = useAppSelector((state) => state.paintings.aihubmix_image_edit)
|
||||
const aihubmix_image_upscale = useAppSelector((state) => state.paintings.aihubmix_image_upscale)
|
||||
const openai_image_generate = useAppSelector((state) => state.paintings.openai_image_generate)
|
||||
const openai_image_edit = useAppSelector((state) => state.paintings.openai_image_edit)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
return {
|
||||
paintings,
|
||||
DMXAPIPaintings,
|
||||
tokenFluxPaintings,
|
||||
persistentData: {
|
||||
generate,
|
||||
remix,
|
||||
edit,
|
||||
upscale,
|
||||
tokenFluxPaintings
|
||||
},
|
||||
newApiPaintings: {
|
||||
openai_image_generate,
|
||||
openai_image_edit
|
||||
},
|
||||
siliconflow_paintings,
|
||||
dmxapi_paintings,
|
||||
tokenflux_paintings,
|
||||
zhipu_paintings,
|
||||
aihubmix_image_generate,
|
||||
aihubmix_image_remix,
|
||||
aihubmix_image_edit,
|
||||
aihubmix_image_upscale,
|
||||
openai_image_generate,
|
||||
openai_image_edit,
|
||||
addPainting: (namespace: keyof PaintingsState, painting: PaintingAction) => {
|
||||
dispatch(addPainting({ namespace, painting }))
|
||||
return painting
|
||||
|
||||
@ -13,7 +13,7 @@ const t = i18n.t
|
||||
|
||||
const logger = loggerService.withContext('i18n:label')
|
||||
|
||||
const getLabel = (key: string, keyMap: Record<string, string>, fallback?: string) => {
|
||||
const getLabel = (keyMap: Record<string, string>, key: string, fallback?: string) => {
|
||||
const result = keyMap[key]
|
||||
if (result) {
|
||||
return t(result)
|
||||
@ -34,6 +34,7 @@ const providerKeyMap = {
|
||||
'baidu-cloud': 'provider.baidu-cloud',
|
||||
burncloud: 'provider.burncloud',
|
||||
cephalon: 'provider.cephalon',
|
||||
cherryin: 'provider.cherryin',
|
||||
copilot: 'provider.copilot',
|
||||
dashscope: 'provider.dashscope',
|
||||
deepseek: 'provider.deepseek',
|
||||
@ -92,7 +93,7 @@ const providerKeyMap = {
|
||||
* 对于可能处理自定义供应商的情况,使用 getProviderName 或 getFancyProviderName 更安全
|
||||
*/
|
||||
export const getProviderLabel = (id: string): string => {
|
||||
return getLabel(id, providerKeyMap)
|
||||
return getLabel(providerKeyMap, id)
|
||||
}
|
||||
|
||||
const backupProgressKeyMap = {
|
||||
@ -106,7 +107,7 @@ const backupProgressKeyMap = {
|
||||
} as const
|
||||
|
||||
export const getBackupProgressLabel = (key: string): string => {
|
||||
return getLabel(key, backupProgressKeyMap)
|
||||
return getLabel(backupProgressKeyMap, key)
|
||||
}
|
||||
|
||||
const restoreProgressKeyMap = {
|
||||
@ -120,7 +121,7 @@ const restoreProgressKeyMap = {
|
||||
}
|
||||
|
||||
export const getRestoreProgressLabel = (key: string): string => {
|
||||
return getLabel(key, restoreProgressKeyMap)
|
||||
return getLabel(restoreProgressKeyMap, key)
|
||||
}
|
||||
|
||||
const titleKeyMap = {
|
||||
@ -139,7 +140,7 @@ const titleKeyMap = {
|
||||
} as const
|
||||
|
||||
export const getTitleLabel = (key: string): string => {
|
||||
return getLabel(key, titleKeyMap)
|
||||
return getLabel(titleKeyMap, key)
|
||||
}
|
||||
|
||||
const themeModeKeyMap = {
|
||||
@ -149,7 +150,7 @@ const themeModeKeyMap = {
|
||||
} as const
|
||||
|
||||
export const getThemeModeLabel = (key: string): string => {
|
||||
return getLabel(key, themeModeKeyMap)
|
||||
return getLabel(themeModeKeyMap, key)
|
||||
}
|
||||
|
||||
const sidebarIconKeyMap = {
|
||||
@ -164,7 +165,7 @@ const sidebarIconKeyMap = {
|
||||
} as const
|
||||
|
||||
export const getSidebarIconLabel = (key: string): string => {
|
||||
return getLabel(key, sidebarIconKeyMap)
|
||||
return getLabel(sidebarIconKeyMap, key)
|
||||
}
|
||||
|
||||
const shortcutKeyMap = {
|
||||
@ -198,7 +199,7 @@ const shortcutKeyMap = {
|
||||
} as const
|
||||
|
||||
export const getShortcutLabel = (key: string): string => {
|
||||
return getLabel(key, shortcutKeyMap)
|
||||
return getLabel(shortcutKeyMap, key)
|
||||
}
|
||||
|
||||
const selectionDescriptionKeyMap = {
|
||||
@ -207,7 +208,7 @@ const selectionDescriptionKeyMap = {
|
||||
} as const
|
||||
|
||||
export const getSelectionDescriptionLabel = (key: string): string => {
|
||||
return getLabel(key, selectionDescriptionKeyMap)
|
||||
return getLabel(selectionDescriptionKeyMap, key)
|
||||
}
|
||||
|
||||
const paintingsImageSizeOptionsKeyMap = {
|
||||
@ -215,7 +216,7 @@ const paintingsImageSizeOptionsKeyMap = {
|
||||
} as const
|
||||
|
||||
export const getPaintingsImageSizeOptionsLabel = (key: string): string => {
|
||||
return getLabel(key, paintingsImageSizeOptionsKeyMap)
|
||||
return getLabel(paintingsImageSizeOptionsKeyMap, key)
|
||||
}
|
||||
|
||||
const paintingsQualityOptionsKeyMap = {
|
||||
@ -226,7 +227,7 @@ const paintingsQualityOptionsKeyMap = {
|
||||
} as const
|
||||
|
||||
export const getPaintingsQualityOptionsLabel = (key: string): string => {
|
||||
return getLabel(key, paintingsQualityOptionsKeyMap)
|
||||
return getLabel(paintingsQualityOptionsKeyMap, key)
|
||||
}
|
||||
|
||||
const paintingsModerationOptionsKeyMap = {
|
||||
@ -235,7 +236,7 @@ const paintingsModerationOptionsKeyMap = {
|
||||
} as const
|
||||
|
||||
export const getPaintingsModerationOptionsLabel = (key: string): string => {
|
||||
return getLabel(key, paintingsModerationOptionsKeyMap)
|
||||
return getLabel(paintingsModerationOptionsKeyMap, key)
|
||||
}
|
||||
|
||||
const paintingsBackgroundOptionsKeyMap = {
|
||||
@ -245,7 +246,7 @@ const paintingsBackgroundOptionsKeyMap = {
|
||||
} as const
|
||||
|
||||
export const getPaintingsBackgroundOptionsLabel = (key: string): string => {
|
||||
return getLabel(key, paintingsBackgroundOptionsKeyMap)
|
||||
return getLabel(paintingsBackgroundOptionsKeyMap, key)
|
||||
}
|
||||
|
||||
const mcpTypeKeyMap = {
|
||||
@ -256,7 +257,7 @@ const mcpTypeKeyMap = {
|
||||
} as const
|
||||
|
||||
export const getMcpTypeLabel = (key: string): string => {
|
||||
return getLabel(key, mcpTypeKeyMap)
|
||||
return getLabel(mcpTypeKeyMap, key)
|
||||
}
|
||||
|
||||
const miniappsStatusKeyMap = {
|
||||
@ -265,7 +266,7 @@ const miniappsStatusKeyMap = {
|
||||
} as const
|
||||
|
||||
export const getMiniappsStatusLabel = (key: string): string => {
|
||||
return getLabel(key, miniappsStatusKeyMap)
|
||||
return getLabel(miniappsStatusKeyMap, key)
|
||||
}
|
||||
|
||||
const httpMessageKeyMap = {
|
||||
@ -281,7 +282,7 @@ const httpMessageKeyMap = {
|
||||
} as const
|
||||
|
||||
export const getHttpMessageLabel = (key: string): string => {
|
||||
return getLabel(key, httpMessageKeyMap)
|
||||
return getLabel(httpMessageKeyMap, key)
|
||||
}
|
||||
|
||||
const reasoningEffortOptionsKeyMap: Record<ThinkingOption, string> = {
|
||||
@ -294,7 +295,7 @@ const reasoningEffortOptionsKeyMap: Record<ThinkingOption, string> = {
|
||||
} as const
|
||||
|
||||
export const getReasoningEffortOptionsLabel = (key: string): string => {
|
||||
return getLabel(key, reasoningEffortOptionsKeyMap)
|
||||
return getLabel(reasoningEffortOptionsKeyMap, key)
|
||||
}
|
||||
|
||||
const fileFieldKeyMap = {
|
||||
@ -304,7 +305,7 @@ const fileFieldKeyMap = {
|
||||
} as const
|
||||
|
||||
export const getFileFieldLabel = (key: string): string => {
|
||||
return getLabel(key, fileFieldKeyMap)
|
||||
return getLabel(fileFieldKeyMap, key)
|
||||
}
|
||||
|
||||
const builtInMcpDescriptionKeyMap: Record<BuiltinMCPServerName, string> = {
|
||||
@ -319,7 +320,7 @@ const builtInMcpDescriptionKeyMap: Record<BuiltinMCPServerName, string> = {
|
||||
} as const
|
||||
|
||||
export const getBuiltInMcpServerDescriptionLabel = (key: string): string => {
|
||||
return getLabel(key, builtInMcpDescriptionKeyMap, t('settings.mcp.builtinServersDescriptions.no'))
|
||||
return getLabel(builtInMcpDescriptionKeyMap, key, t('settings.mcp.builtinServersDescriptions.no'))
|
||||
}
|
||||
|
||||
const builtinOcrProviderKeyMap = {
|
||||
@ -329,5 +330,5 @@ const builtinOcrProviderKeyMap = {
|
||||
|
||||
export const getBuiltinOcrProviderLabel = (key: BuiltinOcrProviderId) => {
|
||||
if (key === 'tesseract') return 'Tesseract'
|
||||
else return getLabel(key, builtinOcrProviderKeyMap)
|
||||
else return getLabel(builtinOcrProviderKeyMap, key)
|
||||
}
|
||||
|
||||
@ -756,6 +756,7 @@
|
||||
"footnote": "Reference content",
|
||||
"footnotes": "References",
|
||||
"fullscreen": "Entered fullscreen mode. Press F11 to exit",
|
||||
"go_to_settings": "Go to settings",
|
||||
"i_know": "I know",
|
||||
"inspect": "Inspect",
|
||||
"knowledge_base": "Knowledge Base",
|
||||
@ -824,6 +825,9 @@
|
||||
"chunk": {
|
||||
"non_json": "Returned an invalid data format"
|
||||
},
|
||||
"insufficient_balance": "Please go to the <provider>{{provider}}</provider> to recharge.",
|
||||
"no_api_key": "You have not configured an API key. Please go to the <provider>{{provider}}</provider> to obtain an API key.",
|
||||
"quota_exceeded": "Your daily {{quota}} free quota has been exhausted. Please go to the <provider>{{provider}}</provider> to obtain an API key and configure the API key to continue using.",
|
||||
"response": "Something went wrong. Please check if you have set your API key in the Settings > Providers"
|
||||
},
|
||||
"http": {
|
||||
@ -1824,6 +1828,7 @@
|
||||
"baidu-cloud": "Baidu Cloud",
|
||||
"burncloud": "BurnCloud",
|
||||
"cephalon": "Cephalon",
|
||||
"cherryin": "CherryIN",
|
||||
"copilot": "GitHub Copilot",
|
||||
"dashscope": "Alibaba Cloud",
|
||||
"deepseek": "DeepSeek",
|
||||
@ -1869,7 +1874,7 @@
|
||||
"xirang": "State Cloud Xirang",
|
||||
"yi": "Yi",
|
||||
"zhinao": "360AI",
|
||||
"zhipu": "ZHIPU AI"
|
||||
"zhipu": "BigModel"
|
||||
},
|
||||
"restore": {
|
||||
"confirm": {
|
||||
@ -2911,7 +2916,8 @@
|
||||
"modelscope": "ModelScope Community MCP Server",
|
||||
"official": "Official MCP Server Collection",
|
||||
"pulsemcp": "Pulse MCP Server",
|
||||
"smithery": "Smithery MCP Tools"
|
||||
"smithery": "Smithery MCP Tools",
|
||||
"zhipu": "Curated MCP, Fast Integration"
|
||||
},
|
||||
"name": "Name",
|
||||
"newServer": "MCP Server",
|
||||
|
||||
@ -756,6 +756,7 @@
|
||||
"footnote": "引用内容",
|
||||
"footnotes": "脚注",
|
||||
"fullscreen": "全画面モードに入りました。F11キーで終了します",
|
||||
"go_to_settings": "設定に移動",
|
||||
"i_know": "わかりました",
|
||||
"inspect": "検査",
|
||||
"knowledge_base": "ナレッジベース",
|
||||
@ -824,6 +825,9 @@
|
||||
"chunk": {
|
||||
"non_json": "無効なデータ形式が返されました"
|
||||
},
|
||||
"insufficient_balance": "<provider>{{provider}}</provider>でチャージしてください。",
|
||||
"no_api_key": "APIキーが設定されていません。<provider>{{provider}}</provider>でAPIキーを取得してください。",
|
||||
"quota_exceeded": "本日の{{quota}}無料クォータが使い果たされました。<provider>{{provider}}</provider>でAPIキーを取得し、APIキーを設定して使用を続けてください。",
|
||||
"response": "エラーが発生しました。APIキーが設定されていない場合は、設定 > プロバイダーでキーを設定してください"
|
||||
},
|
||||
"http": {
|
||||
@ -1824,6 +1828,7 @@
|
||||
"baidu-cloud": "Baidu Cloud",
|
||||
"burncloud": "BurnCloud",
|
||||
"cephalon": "Cephalon",
|
||||
"cherryin": "CherryIN",
|
||||
"copilot": "GitHub Copilot",
|
||||
"dashscope": "Alibaba Cloud",
|
||||
"deepseek": "DeepSeek",
|
||||
@ -1869,7 +1874,7 @@
|
||||
"xirang": "天翼クラウド 息壤",
|
||||
"yi": "零一万物",
|
||||
"zhinao": "360智脳",
|
||||
"zhipu": "智譜AI"
|
||||
"zhipu": "BigModel"
|
||||
},
|
||||
"restore": {
|
||||
"confirm": {
|
||||
@ -2911,7 +2916,8 @@
|
||||
"modelscope": "魔搭コミュニティ MCP サーバー",
|
||||
"official": "公式 MCP サーバーコレクション",
|
||||
"pulsemcp": "Pulse MCP サーバー",
|
||||
"smithery": "Smithery MCP ツール"
|
||||
"smithery": "Smithery MCP ツール",
|
||||
"zhipu": "厳選MCP、高速統合"
|
||||
},
|
||||
"name": "名前",
|
||||
"newServer": "MCP サーバー",
|
||||
|
||||
@ -756,6 +756,7 @@
|
||||
"footnote": "Цитируемый контент",
|
||||
"footnotes": "Сноски",
|
||||
"fullscreen": "Вы вошли в полноэкранный режим. Нажмите F11 для выхода",
|
||||
"go_to_settings": "Перейти в настройки",
|
||||
"i_know": "Я понял",
|
||||
"inspect": "Осмотреть",
|
||||
"knowledge_base": "База знаний",
|
||||
@ -824,6 +825,9 @@
|
||||
"chunk": {
|
||||
"non_json": "Вернулся недопустимый формат данных"
|
||||
},
|
||||
"insufficient_balance": "Пожалуйста, перейдите в <provider>{{provider}}</provider> для пополнения баланса.",
|
||||
"no_api_key": "Вы не настроили ключ API. Пожалуйста, перейдите в <provider>{{provider}}</provider> для получения ключа API.",
|
||||
"quota_exceeded": "Ваша ежедневная {{quota}} бесплатная квота исчерпана. Пожалуйста, перейдите в <provider>{{provider}}</provider> для получения ключа API и настройте ключ API для продолжения использования.",
|
||||
"response": "Что-то пошло не так. Пожалуйста, проверьте, установлен ли ваш ключ API в Настройки > Провайдеры"
|
||||
},
|
||||
"http": {
|
||||
@ -1824,6 +1828,7 @@
|
||||
"baidu-cloud": "Baidu Cloud",
|
||||
"burncloud": "BurnCloud",
|
||||
"cephalon": "Cephalon",
|
||||
"cherryin": "CherryIN",
|
||||
"copilot": "GitHub Copilot",
|
||||
"dashscope": "Alibaba Cloud",
|
||||
"deepseek": "DeepSeek",
|
||||
@ -1869,7 +1874,7 @@
|
||||
"xirang": "State Cloud Xirang",
|
||||
"yi": "Yi",
|
||||
"zhinao": "360AI",
|
||||
"zhipu": "ZHIPU AI"
|
||||
"zhipu": "BigModel"
|
||||
},
|
||||
"restore": {
|
||||
"confirm": {
|
||||
@ -2911,7 +2916,8 @@
|
||||
"modelscope": "Сервер MCP сообщества ModelScope",
|
||||
"official": "Официальная коллекция серверов MCP",
|
||||
"pulsemcp": "Сервер Pulse MCP",
|
||||
"smithery": "Инструменты Smithery MCP"
|
||||
"smithery": "Инструменты Smithery MCP",
|
||||
"zhipu": "Кураторские MCP, быстрая интеграция"
|
||||
},
|
||||
"name": "Имя",
|
||||
"newServer": "MCP сервер",
|
||||
|
||||
@ -756,6 +756,7 @@
|
||||
"footnote": "引用内容",
|
||||
"footnotes": "引用内容",
|
||||
"fullscreen": "已进入全屏模式,按 F11 退出",
|
||||
"go_to_settings": "前往设置",
|
||||
"i_know": "我知道了",
|
||||
"inspect": "检查",
|
||||
"knowledge_base": "知识库",
|
||||
@ -824,6 +825,9 @@
|
||||
"chunk": {
|
||||
"non_json": "返回了无效的数据格式"
|
||||
},
|
||||
"insufficient_balance": "请前往 <provider>{{provider}}</provider> 充值",
|
||||
"no_api_key": "您未配置 API 密钥,请前往 <provider>{{provider}}</provider> 获取API密钥",
|
||||
"quota_exceeded": "您今日免费配额已用尽,请前往 <provider>{{provider}}</provider> 获取API密钥,配置API密钥后继续使用",
|
||||
"response": "出错了,如果没有配置 API 密钥,请前往设置 > 模型提供商中配置密钥"
|
||||
},
|
||||
"http": {
|
||||
@ -1824,6 +1828,7 @@
|
||||
"baidu-cloud": "百度云千帆",
|
||||
"burncloud": "BurnCloud",
|
||||
"cephalon": "Cephalon",
|
||||
"cherryin": "CherryIN",
|
||||
"copilot": "GitHub Copilot",
|
||||
"dashscope": "阿里云百炼",
|
||||
"deepseek": "深度求索",
|
||||
@ -1869,7 +1874,7 @@
|
||||
"xirang": "天翼云息壤",
|
||||
"yi": "零一万物",
|
||||
"zhinao": "360 智脑",
|
||||
"zhipu": "智谱 AI"
|
||||
"zhipu": "智谱开放平台"
|
||||
},
|
||||
"restore": {
|
||||
"confirm": {
|
||||
@ -2911,7 +2916,8 @@
|
||||
"modelscope": "魔搭社区 MCP 服务器",
|
||||
"official": "官方 MCP 服务器集合",
|
||||
"pulsemcp": "Pulse MCP 服务器",
|
||||
"smithery": "Smithery MCP 工具"
|
||||
"smithery": "Smithery MCP 工具",
|
||||
"zhipu": "精选MCP,极速接入"
|
||||
},
|
||||
"name": "名称",
|
||||
"newServer": "MCP 服务器",
|
||||
|
||||
@ -756,6 +756,7 @@
|
||||
"footnote": "引用內容",
|
||||
"footnotes": "引用",
|
||||
"fullscreen": "已進入全螢幕模式,按 F11 結束",
|
||||
"go_to_settings": "前往設定",
|
||||
"i_know": "我知道了",
|
||||
"inspect": "檢查",
|
||||
"knowledge_base": "知識庫",
|
||||
@ -824,6 +825,9 @@
|
||||
"chunk": {
|
||||
"non_json": "返回了無效的資料格式"
|
||||
},
|
||||
"insufficient_balance": "請前往 <provider>{{provider}}</provider> 充值",
|
||||
"no_api_key": "您未配置 API 密钥,请前往 <provider>{{provider}}</provider> 获取API密钥",
|
||||
"quota_exceeded": "您今日{{quota}}免费配额已用尽,请前往 <provider>{{provider}}</provider> 获取API密钥,配置API密钥后继续使用",
|
||||
"response": "出現錯誤。如果尚未設定 API 金鑰,請前往設定 > 模型提供者中設定金鑰"
|
||||
},
|
||||
"http": {
|
||||
@ -1824,6 +1828,7 @@
|
||||
"baidu-cloud": "百度雲千帆",
|
||||
"burncloud": "BurnCloud",
|
||||
"cephalon": "Cephalon",
|
||||
"cherryin": "CherryIN",
|
||||
"copilot": "GitHub Copilot",
|
||||
"dashscope": "阿里雲百鍊",
|
||||
"deepseek": "深度求索",
|
||||
@ -1869,7 +1874,7 @@
|
||||
"xirang": "天翼雲息壤",
|
||||
"yi": "零一萬物",
|
||||
"zhinao": "360 智腦",
|
||||
"zhipu": "智譜 AI"
|
||||
"zhipu": "智譜開放平台"
|
||||
},
|
||||
"restore": {
|
||||
"confirm": {
|
||||
@ -2911,7 +2916,8 @@
|
||||
"modelscope": "魔搭社區 MCP 伺服器",
|
||||
"official": "官方 MCP 伺服器集合",
|
||||
"pulsemcp": "Pulse MCP 伺服器",
|
||||
"smithery": "Smithery MCP 工具"
|
||||
"smithery": "Smithery MCP 工具",
|
||||
"zhipu": "精選MCP,極速接入"
|
||||
},
|
||||
"name": "名稱",
|
||||
"newServer": "MCP 伺服器",
|
||||
|
||||
@ -756,6 +756,7 @@
|
||||
"footnote": "Παραπομπή",
|
||||
"footnotes": "Παραπομπές",
|
||||
"fullscreen": "Εισήχθη σε πλήρη οθόνη, πατήστε F11 για να έξω",
|
||||
"go_to_settings": "Πηγαίνετε στις ρυθμίσεις",
|
||||
"i_know": "Το έχω καταλάβει",
|
||||
"inspect": "Επιθεώρηση",
|
||||
"knowledge_base": "Βάση Γνώσεων",
|
||||
@ -824,6 +825,9 @@
|
||||
"chunk": {
|
||||
"non_json": "Επέστρεψε μη έγκυρη μορφή δεδομένων"
|
||||
},
|
||||
"insufficient_balance": "Παρακαλώ μεταβείτε στο <provider>{{provider}}</provider> για επαναφόρτωση.",
|
||||
"no_api_key": "Δεν έχετε ρυθμίσει το κλειδί API. Παρακαλώ μεταβείτε στο <provider>{{provider}}</provider> για να λάβετε ένα κλειδί API.",
|
||||
"quota_exceeded": "Η ημερήσια δωρεάν ποσόστωση {{quota}} tokens σας έχει εξαντληθεί. Παρακαλώ μεταβείτε στο <provider>{{provider}}</provider> για να λάβετε ένα κλειδί API και να ρυθμίσετε το κλειδί API για να συνεχίσετε τη χρήση.",
|
||||
"response": "Σφάλμα. Εάν δεν έχετε ρυθμίσει το κλειδί API, πηγαίνετε στο ρυθμισμένα > παρέχοντας το πρόσωπο του μοντέλου"
|
||||
},
|
||||
"http": {
|
||||
@ -1824,6 +1828,7 @@
|
||||
"baidu-cloud": "Baidu Cloud Qianfan",
|
||||
"burncloud": "BurnCloud",
|
||||
"cephalon": "Cephalon",
|
||||
"cherryin": "CherryIN",
|
||||
"copilot": "GitHub Copilot",
|
||||
"dashscope": "AliCloud Bailian",
|
||||
"deepseek": "Βαθιά Αναζήτηση",
|
||||
@ -1869,7 +1874,7 @@
|
||||
"xirang": "China Telecom Xiran",
|
||||
"yi": "Zero One Wanyiwu",
|
||||
"zhinao": "360 Intelligent Brain",
|
||||
"zhipu": "Zhipu AI"
|
||||
"zhipu": "BigModel"
|
||||
},
|
||||
"restore": {
|
||||
"confirm": {
|
||||
@ -2911,7 +2916,8 @@
|
||||
"modelscope": "Διακομιστής MCP κοινότητας ModelScope",
|
||||
"official": "Επίσημη συλλογή διακομιστών MCP",
|
||||
"pulsemcp": "Διακομιστής Pulse MCP",
|
||||
"smithery": "Εργαλείο Smithery MCP"
|
||||
"smithery": "Εργαλείο Smithery MCP",
|
||||
"zhipu": "Επιλεγμένο MCP, Γρήγορη Ενσωμάτωση"
|
||||
},
|
||||
"name": "Όνομα",
|
||||
"newServer": "Διακομιστής MCP",
|
||||
|
||||
@ -756,6 +756,7 @@
|
||||
"footnote": "Nota al pie",
|
||||
"footnotes": "Notas al pie",
|
||||
"fullscreen": "En modo pantalla completa, presione F11 para salir",
|
||||
"go_to_settings": "Ir a la configuración",
|
||||
"i_know": "Entendido",
|
||||
"inspect": "Inspeccionar",
|
||||
"knowledge_base": "Base de conocimiento",
|
||||
@ -824,6 +825,9 @@
|
||||
"chunk": {
|
||||
"non_json": "Devuelve un formato de datos no válido"
|
||||
},
|
||||
"insufficient_balance": "Por favor, vaya a <provider>{{provider}}</provider> para recargar.",
|
||||
"no_api_key": "No ha configurado una clave API. Por favor, vaya a <provider>{{provider}}</provider> para obtener una clave API.",
|
||||
"quota_exceeded": "Su cuota gratuita diaria de {{quota}} tokens se ha agotado. Por favor, vaya a <provider>{{provider}}</provider> para obtener una clave API y configurar la clave API para continuar usando.",
|
||||
"response": "Ha ocurrido un error, si no ha configurado la clave API, vaya a Configuración > Proveedor de modelos para configurar la clave"
|
||||
},
|
||||
"http": {
|
||||
@ -1824,6 +1828,7 @@
|
||||
"baidu-cloud": "Baidu Nube Qiánfān",
|
||||
"burncloud": "BurnCloud",
|
||||
"cephalon": "Cephalon",
|
||||
"cherryin": "CherryIN",
|
||||
"copilot": "GitHub Copiloto",
|
||||
"dashscope": "Álibaba Nube BaiLiàn",
|
||||
"deepseek": "Profundo Buscar",
|
||||
@ -1869,7 +1874,7 @@
|
||||
"xirang": "Telecom Nube XiRang",
|
||||
"yi": "Cero Uno Todo",
|
||||
"zhinao": "360 Inteligente",
|
||||
"zhipu": "ZhiPu IA"
|
||||
"zhipu": "BigModel"
|
||||
},
|
||||
"restore": {
|
||||
"confirm": {
|
||||
@ -2911,7 +2916,8 @@
|
||||
"modelscope": "Servidor MCP de la comunidad ModelScope",
|
||||
"official": "Colección oficial de servidores MCP",
|
||||
"pulsemcp": "Servidor MCP Pulse",
|
||||
"smithery": "Herramienta Smithery MCP"
|
||||
"smithery": "Herramienta Smithery MCP",
|
||||
"zhipu": "MCP Curado, Integración Rápida"
|
||||
},
|
||||
"name": "Nombre",
|
||||
"newServer": "Servidor MCP",
|
||||
|
||||
@ -756,6 +756,7 @@
|
||||
"footnote": "Note de bas de page",
|
||||
"footnotes": "Notes de bas de page",
|
||||
"fullscreen": "Mode plein écran, appuyez sur F11 pour quitter",
|
||||
"go_to_settings": "Aller aux paramètres",
|
||||
"i_know": "J'ai compris",
|
||||
"inspect": "Vérifier",
|
||||
"knowledge_base": "Base de connaissances",
|
||||
@ -824,6 +825,9 @@
|
||||
"chunk": {
|
||||
"non_json": "a renvoyé un format de données invalide"
|
||||
},
|
||||
"insufficient_balance": "Veuillez vous rendre sur <provider>{{provider}}</provider> pour recharger.",
|
||||
"no_api_key": "Vous n'avez pas configuré de clé API. Veuillez vous rendre sur <provider>{{provider}}</provider> pour obtenir une clé API.",
|
||||
"quota_exceeded": "Votre quota gratuit quotidien de {{quota}} tokens a été épuisé. Veuillez vous rendre sur <provider>{{provider}}</provider> pour obtenir une clé API et configurer la clé API pour continuer à utiliser.",
|
||||
"response": "Une erreur s'est produite, si l'API n'est pas configurée, veuillez aller dans Paramètres > Fournisseurs de modèles pour configurer la clé"
|
||||
},
|
||||
"http": {
|
||||
@ -1824,6 +1828,7 @@
|
||||
"baidu-cloud": "Baidu Cloud Qianfan",
|
||||
"burncloud": "BurnCloud",
|
||||
"cephalon": "Cephalon",
|
||||
"cherryin": "CherryIN",
|
||||
"copilot": "GitHub Copilote",
|
||||
"dashscope": "AliCloud BaiLian",
|
||||
"deepseek": "DeepSeek",
|
||||
@ -1869,7 +1874,7 @@
|
||||
"xirang": "CTyun XiRang",
|
||||
"yi": "ZéroUnInfini",
|
||||
"zhinao": "360 ZhiNao",
|
||||
"zhipu": "ZhiPu IA"
|
||||
"zhipu": "BigModel"
|
||||
},
|
||||
"restore": {
|
||||
"confirm": {
|
||||
@ -2911,7 +2916,8 @@
|
||||
"modelscope": "Serveur MCP de la communauté ModelScope",
|
||||
"official": "Collection officielle de serveurs MCP",
|
||||
"pulsemcp": "Serveur MCP Pulse",
|
||||
"smithery": "Outils Smithery MCP"
|
||||
"smithery": "Outils Smithery MCP",
|
||||
"zhipu": "MCP Curaté, Intégration Rapide"
|
||||
},
|
||||
"name": "Nom",
|
||||
"newServer": "Сервер MCP",
|
||||
|
||||
@ -756,6 +756,7 @@
|
||||
"footnote": "Nota de rodapé",
|
||||
"footnotes": "Notas de rodapé",
|
||||
"fullscreen": "Entrou no modo de tela cheia, pressione F11 para sair",
|
||||
"go_to_settings": "Ir para configurações",
|
||||
"i_know": "Entendi",
|
||||
"inspect": "Verificar",
|
||||
"knowledge_base": "Base de Conhecimento",
|
||||
@ -824,6 +825,9 @@
|
||||
"chunk": {
|
||||
"non_json": "Devolveu um formato de dados inválido"
|
||||
},
|
||||
"insufficient_balance": "Por favor, vá para <provider>{{provider}}</provider> para recarregar.",
|
||||
"no_api_key": "Você não configurou uma chave API. Por favor, vá para <provider>{{provider}}</provider> para obter uma chave API.",
|
||||
"quota_exceeded": "Sua cota gratuita diária de {{quota}} tokens foi esgotada. Por favor, vá para <provider>{{provider}}</provider> para obter uma chave API e configurar a chave API para continuar usando.",
|
||||
"response": "Ocorreu um erro, se a chave da API não foi configurada, por favor vá para Configurações > Provedores de Modelo para configurar a chave"
|
||||
},
|
||||
"http": {
|
||||
@ -1824,6 +1828,7 @@
|
||||
"baidu-cloud": "Nuvem Baidu",
|
||||
"burncloud": "BurnCloud",
|
||||
"cephalon": "Cephalon",
|
||||
"cherryin": "CherryIN",
|
||||
"copilot": "GitHub Copiloto",
|
||||
"dashscope": "Área de Atuação AliCloud",
|
||||
"deepseek": "Busca Profunda",
|
||||
@ -1869,7 +1874,7 @@
|
||||
"xirang": "XiRang do Nuvem Telecom",
|
||||
"yi": "ZeroUmTudo",
|
||||
"zhinao": "360 Inteligência Artificial",
|
||||
"zhipu": "ZhiPu IA"
|
||||
"zhipu": "BigModel"
|
||||
},
|
||||
"restore": {
|
||||
"confirm": {
|
||||
@ -2911,7 +2916,8 @@
|
||||
"modelscope": "Servidor MCP da comunidade ModelScope",
|
||||
"official": "Coleção oficial de servidores MCP",
|
||||
"pulsemcp": "Servidor MCP Pulse",
|
||||
"smithery": "Ferramentas Smithery MCP"
|
||||
"smithery": "Ferramentas Smithery MCP",
|
||||
"zhipu": "MCP Curado, Integração Rápida"
|
||||
},
|
||||
"name": "Nome",
|
||||
"newServer": "Servidor MCP",
|
||||
|
||||
@ -57,6 +57,9 @@ const CodeToolsPage: FC = () => {
|
||||
if (isEmbeddingModel(m) || isRerankModel(m) || isTextToImageModel(m)) {
|
||||
return false
|
||||
}
|
||||
if (m.provider === 'cherryin') {
|
||||
return false
|
||||
}
|
||||
if (selectedCliTool === 'claude-code') {
|
||||
return m.id.includes('claude') || CLAUDE_OFFICIAL_SUPPORTED_PROVIDERS.includes(m.provider)
|
||||
}
|
||||
|
||||
@ -53,8 +53,11 @@ const FilesPage: FC = () => {
|
||||
const selectedFiles = await Promise.all(selectedFileIds.map((id) => FileManager.getFile(id)))
|
||||
const validFiles = selectedFiles.filter((file) => file !== null && file !== undefined)
|
||||
|
||||
const paintings = store.getState().paintings.paintings
|
||||
const paintingsFiles = paintings.flatMap((p) => p.files)
|
||||
const paintings = store.getState().paintings
|
||||
const paintingsFiles = Object.values(paintings)
|
||||
.flat()
|
||||
.filter((painting) => painting?.files?.length > 0)
|
||||
.flatMap((painting) => painting.files)
|
||||
|
||||
const filesInPaintings = validFiles.filter((file) => paintingsFiles.some((p) => p.id === file.id))
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { BaiduOutlined, GoogleOutlined } from '@ant-design/icons'
|
||||
import { loggerService } from '@logger'
|
||||
import { BingLogo, BochaLogo, ExaLogo, SearXNGLogo, TavilyLogo } from '@renderer/components/Icons'
|
||||
import { BingLogo, BochaLogo, ExaLogo, SearXNGLogo, TavilyLogo, ZhipuLogo } from '@renderer/components/Icons'
|
||||
import { QuickPanelListItem, useQuickPanel } from '@renderer/components/QuickPanel'
|
||||
import { isGeminiModel, isWebSearchModel } from '@renderer/config/models'
|
||||
import { isGeminiWebSearchProvider } from '@renderer/config/providers'
|
||||
@ -50,6 +50,8 @@ const WebSearchButton: FC<Props> = ({ ref, assistant, ToolbarButton }) => {
|
||||
return <ExaLogo width={size - 2} height={size} color={color} />
|
||||
case 'tavily':
|
||||
return <TavilyLogo width={size} height={size} color={color} />
|
||||
case 'zhipu':
|
||||
return <ZhipuLogo width={size} height={size} color={color} />
|
||||
case 'searxng':
|
||||
return <SearXNGLogo width={size} height={size} color={color} />
|
||||
case 'local-baidu':
|
||||
|
||||
@ -1,13 +1,17 @@
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { getHttpMessageLabel } from '@renderer/i18n/label'
|
||||
import { getHttpMessageLabel, getProviderLabel } from '@renderer/i18n/label'
|
||||
import { getProviderById } from '@renderer/services/ProviderService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { removeBlocksThunk } from '@renderer/store/thunk/messageThunk'
|
||||
import type { ErrorMessageBlock, Message } from '@renderer/types/newMessage'
|
||||
import { Alert as AntdAlert } from 'antd'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { Link } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const HTTP_ERROR_CODES = [400, 401, 403, 404, 429, 500, 502, 503, 504]
|
||||
|
||||
interface Props {
|
||||
block: ErrorMessageBlock
|
||||
message: Message
|
||||
@ -17,36 +21,68 @@ const ErrorBlock: React.FC<Props> = ({ block, message }) => {
|
||||
return <MessageErrorInfo block={block} message={message} />
|
||||
}
|
||||
|
||||
const MessageErrorInfo: React.FC<{ block: ErrorMessageBlock; message: Message }> = ({ block, message }) => {
|
||||
const ErrorMessage: React.FC<{ block: ErrorMessageBlock }> = ({ block }) => {
|
||||
const { t, i18n } = useTranslation()
|
||||
|
||||
const i18nKey = `error.${block.error?.i18nKey}`
|
||||
const errorKey = `error.${block.error?.message}`
|
||||
const errorStatus = block.error?.status
|
||||
|
||||
if (i18n.exists(i18nKey)) {
|
||||
const providerId = block.error?.providerId
|
||||
if (providerId) {
|
||||
return (
|
||||
<Trans
|
||||
i18nKey={i18nKey}
|
||||
values={{ provider: getProviderLabel(providerId) }}
|
||||
components={{
|
||||
provider: (
|
||||
<Link
|
||||
style={{ color: 'var(--color-link)' }}
|
||||
to={`/settings/provider`}
|
||||
state={{ provider: getProviderById(providerId) }}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (i18n.exists(errorKey)) {
|
||||
return t(errorKey)
|
||||
}
|
||||
|
||||
if (HTTP_ERROR_CODES.includes(errorStatus)) {
|
||||
return (
|
||||
<h5>
|
||||
{getHttpMessageLabel(errorStatus)} {block.error?.message}
|
||||
</h5>
|
||||
)
|
||||
}
|
||||
|
||||
return block.error?.message || ''
|
||||
}
|
||||
|
||||
const ErrorDescription: React.FC<{ block: ErrorMessageBlock }> = ({ block }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (block.error) {
|
||||
return <ErrorMessage block={block} />
|
||||
}
|
||||
|
||||
return <>{t('error.chat.response')}</>
|
||||
}
|
||||
|
||||
const MessageErrorInfo: React.FC<{ block: ErrorMessageBlock; message: Message }> = ({ block, message }) => {
|
||||
const dispatch = useAppDispatch()
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
|
||||
const HTTP_ERROR_CODES = [400, 401, 403, 404, 429, 500, 502, 503, 504]
|
||||
|
||||
const onRemoveBlock = () => {
|
||||
setTimeoutTimer('onRemoveBlock', () => dispatch(removeBlocksThunk(message.topicId, message.id, [block.id])), 350)
|
||||
}
|
||||
|
||||
if (block.error && HTTP_ERROR_CODES.includes(block.error?.status)) {
|
||||
return (
|
||||
<Alert
|
||||
description={getHttpMessageLabel(block.error.status)}
|
||||
message={block.error?.message}
|
||||
type="error"
|
||||
closable
|
||||
onClose={onRemoveBlock}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (block?.error?.message) {
|
||||
const errorKey = `error.${block.error.message}`
|
||||
const pauseErrorLanguagePlaceholder = i18n.exists(errorKey) ? t(errorKey) : block.error.message
|
||||
return <Alert description={pauseErrorLanguagePlaceholder} type="error" closable onClose={onRemoveBlock} />
|
||||
}
|
||||
|
||||
return <Alert description={t('error.chat.response')} type="error" closable onClose={onRemoveBlock} />
|
||||
return <Alert description={<ErrorDescription block={block} />} type="error" closable onClose={onRemoveBlock} />
|
||||
}
|
||||
|
||||
const Alert = styled(AntdAlert)`
|
||||
|
||||
@ -264,7 +264,7 @@ const MessageContainer = styled.div`
|
||||
const MessageContentContainer = styled(Scrollbar)`
|
||||
max-width: 100%;
|
||||
padding-left: 46px;
|
||||
margin-top: 5px;
|
||||
margin-top: 0;
|
||||
overflow-y: auto;
|
||||
`
|
||||
|
||||
|
||||
@ -49,7 +49,7 @@ const SelectModelButton: FC<Props> = ({ assistant }) => {
|
||||
return null
|
||||
}
|
||||
|
||||
const providerName = getProviderName(model?.provider)
|
||||
const providerName = getProviderName(model)
|
||||
|
||||
return (
|
||||
<DropdownButton size="small" type="text" onClick={onSelectModel}>
|
||||
|
||||
@ -34,7 +34,7 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
const [progressMap, setProgressMap] = useState<Map<string, number>>(new Map())
|
||||
const [preprocessMap, setPreprocessMap] = useState<Map<string, boolean>>(new Map())
|
||||
|
||||
const providerName = getProviderName(base?.model.provider || '')
|
||||
const providerName = getProviderName(base?.model)
|
||||
|
||||
useEffect(() => {
|
||||
const handlers = [
|
||||
|
||||
@ -43,7 +43,7 @@ const KnowledgeDirectories: FC<KnowledgeContentProps> = ({ selectedBase, progres
|
||||
selectedBase.id || ''
|
||||
)
|
||||
|
||||
const providerName = getProviderName(base?.model.provider || '')
|
||||
const providerName = getProviderName(base?.model)
|
||||
const disabled = !base?.version || !providerName
|
||||
|
||||
const reversedItems = useMemo(() => [...directoryItems].reverse(), [directoryItems])
|
||||
|
||||
@ -62,7 +62,7 @@ const KnowledgeFiles: FC<KnowledgeContentProps> = ({ selectedBase, progressMap,
|
||||
return () => window.removeEventListener('resize', handleResize)
|
||||
}, [])
|
||||
|
||||
const providerName = getProviderName(base?.model.provider || '')
|
||||
const providerName = getProviderName(base?.model)
|
||||
const disabled = !base?.version || !providerName
|
||||
|
||||
const estimateSize = useCallback(() => 75, [])
|
||||
|
||||
@ -31,7 +31,7 @@ const KnowledgeNotes: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
selectedBase.id || ''
|
||||
)
|
||||
|
||||
const providerName = getProviderName(base?.model.provider || '')
|
||||
const providerName = getProviderName(base?.model)
|
||||
const disabled = !base?.version || !providerName
|
||||
|
||||
const reversedItems = useMemo(() => [...noteItems].reverse(), [noteItems])
|
||||
|
||||
@ -43,7 +43,7 @@ const KnowledgeSitemaps: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
selectedBase.id || ''
|
||||
)
|
||||
|
||||
const providerName = getProviderName(base?.model.provider || '')
|
||||
const providerName = getProviderName(base?.model)
|
||||
const disabled = !base?.version || !providerName
|
||||
|
||||
const reversedItems = useMemo(() => [...sitemapItems].reverse(), [sitemapItems])
|
||||
|
||||
@ -40,7 +40,7 @@ const KnowledgeUrls: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
selectedBase.id || ''
|
||||
)
|
||||
|
||||
const providerName = getProviderName(base?.model.provider || '')
|
||||
const providerName = getProviderName(base?.model)
|
||||
const disabled = !base?.version || !providerName
|
||||
|
||||
const reversedItems = useMemo(() => [...urlItems].reverse(), [urlItems])
|
||||
|
||||
@ -36,6 +36,7 @@ import { SettingHelpLink, SettingTitle } from '../settings'
|
||||
import Artboard from './components/Artboard'
|
||||
import PaintingsList from './components/PaintingsList'
|
||||
import { type ConfigItem, createModeConfigs, DEFAULT_PAINTING } from './config/aihubmixConfig'
|
||||
import { checkProviderEnabled } from './utils'
|
||||
|
||||
const logger = loggerService.withContext('AihubmixPage')
|
||||
|
||||
@ -43,9 +44,27 @@ const logger = loggerService.withContext('AihubmixPage')
|
||||
const modeConfigs = createModeConfigs()
|
||||
|
||||
const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
const [mode, setMode] = useState<keyof PaintingsState>('generate')
|
||||
const { addPainting, removePainting, updatePainting, persistentData } = usePaintings()
|
||||
const filteredPaintings = useMemo(() => persistentData[mode] || [], [persistentData, mode])
|
||||
const [mode, setMode] = useState<keyof PaintingsState>('aihubmix_image_generate')
|
||||
const {
|
||||
addPainting,
|
||||
removePainting,
|
||||
updatePainting,
|
||||
aihubmix_image_generate,
|
||||
aihubmix_image_remix,
|
||||
aihubmix_image_edit,
|
||||
aihubmix_image_upscale
|
||||
} = usePaintings()
|
||||
|
||||
const paintings = useMemo(() => {
|
||||
return {
|
||||
aihubmix_image_generate,
|
||||
aihubmix_image_remix,
|
||||
aihubmix_image_edit,
|
||||
aihubmix_image_upscale
|
||||
}
|
||||
}, [aihubmix_image_generate, aihubmix_image_remix, aihubmix_image_edit, aihubmix_image_upscale])
|
||||
|
||||
const filteredPaintings = useMemo(() => paintings[mode] || [], [paintings, mode])
|
||||
const [painting, setPainting] = useState<PaintingAction>(filteredPaintings[0] || DEFAULT_PAINTING)
|
||||
const [currentImageIndex, setCurrentImageIndex] = useState(0)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
@ -88,7 +107,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
const getNewPainting = useCallback(() => {
|
||||
return {
|
||||
...DEFAULT_PAINTING,
|
||||
model: mode === 'generate' ? 'gpt-image-1' : 'V_3',
|
||||
model: mode === 'aihubmix_image_generate' ? 'gpt-image-1' : 'V_3',
|
||||
id: uuid()
|
||||
}
|
||||
}, [mode])
|
||||
@ -143,6 +162,8 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
}
|
||||
|
||||
const onGenerate = async () => {
|
||||
await checkProviderEnabled(aihubmixProvider, t)
|
||||
|
||||
if (painting.files.length > 0) {
|
||||
const confirmed = await window.modal.confirm({
|
||||
content: t('paintings.regenerate.confirm'),
|
||||
@ -156,14 +177,6 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
const prompt = textareaRef.current?.resizableTextArea?.textArea?.value || ''
|
||||
updatePaintingState({ prompt })
|
||||
|
||||
if (!aihubmixProvider.enabled) {
|
||||
window.modal.error({
|
||||
content: t('error.provider_disabled'),
|
||||
centered: true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!aihubmixProvider.apiKey) {
|
||||
window.modal.error({
|
||||
content: t('error.no_api_key'),
|
||||
@ -188,7 +201,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
let url = aihubmixProvider.apiHost + `/ideogram/` + mode
|
||||
|
||||
try {
|
||||
if (mode === 'generate') {
|
||||
if (mode === 'aihubmix_image_generate') {
|
||||
if (painting.model.startsWith('imagen-')) {
|
||||
const AI = new AiProvider(aihubmixProvider)
|
||||
const base64s = await AI.generateImage({
|
||||
@ -344,7 +357,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
body = JSON.stringify(requestData)
|
||||
headers['Content-Type'] = 'application/json'
|
||||
}
|
||||
} else if (mode === 'remix') {
|
||||
} else if (mode === 'aihubmix_image_remix') {
|
||||
if (!painting.imageFile) {
|
||||
window.modal.error({
|
||||
content: t('paintings.image_file_required'),
|
||||
@ -439,7 +452,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
form.append('image_file', fileMap[painting.imageFile] as unknown as Blob)
|
||||
body = form
|
||||
}
|
||||
} else if (mode === 'upscale') {
|
||||
} else if (mode === 'aihubmix_image_upscale') {
|
||||
if (!painting.imageFile) {
|
||||
window.modal.error({
|
||||
content: t('paintings.image_file_required'),
|
||||
@ -470,7 +483,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
}
|
||||
|
||||
// 只针对非V3模型使用通用接口
|
||||
if (!painting.model?.includes('V_3') || mode === 'upscale') {
|
||||
if (!painting.model?.includes('V_3') || mode === 'aihubmix_image_upscale') {
|
||||
// 直接调用自定义接口
|
||||
const response = await fetch(url, { method: 'POST', headers, body })
|
||||
|
||||
@ -617,8 +630,8 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
// 处理模式切换
|
||||
const handleModeChange = (value: string) => {
|
||||
setMode(value as keyof PaintingsState)
|
||||
if (persistentData[value as keyof PaintingsState] && persistentData[value as keyof PaintingsState].length > 0) {
|
||||
setPainting(persistentData[value as keyof PaintingsState][0])
|
||||
if (paintings[value as keyof PaintingsState] && paintings[value as keyof PaintingsState].length > 0) {
|
||||
setPainting(paintings[value as keyof PaintingsState][0])
|
||||
} else {
|
||||
setPainting(DEFAULT_PAINTING)
|
||||
}
|
||||
@ -843,7 +856,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
</SettingHelpLink>
|
||||
</ProviderTitleContainer>
|
||||
|
||||
<Select value={providerOptions[0].value} onChange={handleProviderChange} style={{ marginBottom: 15 }}>
|
||||
<Select value={providerOptions[1].value} onChange={handleProviderChange} style={{ marginBottom: 15 }}>
|
||||
{providerOptions.map((provider) => (
|
||||
<Select.Option value={provider.value} key={provider.value}>
|
||||
<SelectOptionContainer>
|
||||
|
||||
@ -12,7 +12,7 @@ import { getProviderLabel } from '@renderer/i18n/label'
|
||||
import FileManager from '@renderer/services/FileManager'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setGenerating } from '@renderer/store/runtime'
|
||||
import type { FileMetadata, PaintingsState } from '@renderer/types'
|
||||
import type { FileMetadata } from '@renderer/types'
|
||||
import { convertToBase64, uuid } from '@renderer/utils'
|
||||
import { DmxapiPainting } from '@types'
|
||||
import { Avatar, Button, Input, InputNumber, Segmented, Select, Switch, Tooltip } from 'antd'
|
||||
@ -37,13 +37,13 @@ import {
|
||||
STYLE_TYPE_OPTIONS,
|
||||
TOP_UP_URL
|
||||
} from './config/DmxapiConfig'
|
||||
import { checkProviderEnabled } from './utils'
|
||||
|
||||
const generateRandomSeed = () => Math.floor(Math.random() * 1000000).toString()
|
||||
|
||||
const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
const [mode] = useState<keyof PaintingsState>('DMXAPIPaintings')
|
||||
const { DMXAPIPaintings, addPainting, removePainting, updatePainting } = usePaintings()
|
||||
const [painting, setPainting] = useState<DmxapiPainting>(DMXAPIPaintings?.[0] || DEFAULT_PAINTING)
|
||||
const { dmxapi_paintings, addPainting, removePainting, updatePainting } = usePaintings()
|
||||
const [painting, setPainting] = useState<DmxapiPainting>(dmxapi_paintings?.[0] || DEFAULT_PAINTING)
|
||||
const { t } = useTranslation()
|
||||
const providers = useAllProviders()
|
||||
const providerOptions = Options.map((option) => {
|
||||
@ -144,7 +144,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
const updatePaintingState = (updates: Partial<DmxapiPainting>) => {
|
||||
const updatedPainting = { ...painting, ...updates }
|
||||
setPainting(updatedPainting)
|
||||
updatePainting('DMXAPIPaintings', updatedPainting)
|
||||
updatePainting('dmxapi_paintings', updatedPainting)
|
||||
}
|
||||
|
||||
const getFirstModelInfo = (v: generationModeType) => {
|
||||
@ -197,7 +197,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
id: uuid()
|
||||
}
|
||||
|
||||
setPainting(addPainting('DMXAPIPaintings', copyPainting))
|
||||
setPainting(addPainting('dmxapi_paintings', copyPainting))
|
||||
}
|
||||
|
||||
const onSelectModel = (modelId: string) => {
|
||||
@ -316,7 +316,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
generationMode: v,
|
||||
model
|
||||
})
|
||||
const addedPainting = addPainting('DMXAPIPaintings', newPainting)
|
||||
const addedPainting = addPainting('dmxapi_paintings', newPainting)
|
||||
setPainting(addedPainting)
|
||||
} else {
|
||||
// 否则更新当前painting
|
||||
@ -333,7 +333,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
if (isLoading) {
|
||||
return
|
||||
}
|
||||
setPainting(addPainting('DMXAPIPaintings', getNewPainting()))
|
||||
setPainting(addPainting('dmxapi_paintings', getNewPainting()))
|
||||
}
|
||||
|
||||
// 检查提供者状态函数
|
||||
@ -536,6 +536,12 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
if (isLoading) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!dmxapiProvider.enabled) {
|
||||
checkProviderEnabled(dmxapiProvider, t)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取提示词
|
||||
const prompt = textareaRef.current?.resizableTextArea?.textArea?.value || ''
|
||||
@ -624,23 +630,23 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
return
|
||||
}
|
||||
|
||||
const currentIndex = DMXAPIPaintings.findIndex((p) => p.id === paintingToDelete.id)
|
||||
const currentIndex = dmxapi_paintings.findIndex((p) => p.id === paintingToDelete.id)
|
||||
|
||||
if (currentIndex > 0) {
|
||||
setPainting(DMXAPIPaintings[currentIndex - 1])
|
||||
} else if (DMXAPIPaintings.length > 1) {
|
||||
setPainting(DMXAPIPaintings[1])
|
||||
setPainting(dmxapi_paintings[currentIndex - 1])
|
||||
} else if (dmxapi_paintings.length > 1) {
|
||||
setPainting(dmxapi_paintings[1])
|
||||
}
|
||||
}
|
||||
|
||||
// 删除绘画
|
||||
await removePainting(mode, paintingToDelete)
|
||||
await removePainting('dmxapi_paintings', paintingToDelete)
|
||||
|
||||
// 检查是否删除空了
|
||||
if (!DMXAPIPaintings || DMXAPIPaintings.length === 1) {
|
||||
if (!dmxapi_paintings || dmxapi_paintings.length === 1) {
|
||||
// 如果删除后没有绘画了,创建一个新的
|
||||
const newPainting = getNewPainting()
|
||||
const addedPainting = addPainting('DMXAPIPaintings', newPainting)
|
||||
const addedPainting = addPainting('dmxapi_paintings', newPainting)
|
||||
setPainting(addedPainting)
|
||||
}
|
||||
}
|
||||
@ -692,7 +698,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (painting?.urls?.length > 0 || DMXAPIPaintings?.length > 1) {
|
||||
if (painting?.urls?.length > 0 || dmxapi_paintings?.length > 1) {
|
||||
return null
|
||||
} else {
|
||||
return (
|
||||
@ -733,22 +739,22 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
return
|
||||
}
|
||||
|
||||
if (!DMXAPIPaintings || DMXAPIPaintings.length === 0) {
|
||||
if (!dmxapi_paintings || dmxapi_paintings.length === 0) {
|
||||
const newPainting = getNewPainting()
|
||||
addPainting('DMXAPIPaintings', newPainting)
|
||||
addPainting('dmxapi_paintings', newPainting)
|
||||
setPainting(newPainting)
|
||||
} else if (painting && !painting.generationMode) {
|
||||
// 如果当前painting没有generationMode,添加默认值
|
||||
const updatedPainting = { ...painting, generationMode: MODEOPTIONS[0].value }
|
||||
setPainting(updatedPainting)
|
||||
updatePainting('DMXAPIPaintings', updatedPainting)
|
||||
updatePainting('dmxapi_paintings', updatedPainting)
|
||||
}
|
||||
|
||||
// 确保所有paintings都有generationMode属性
|
||||
DMXAPIPaintings.forEach((p) => {
|
||||
dmxapi_paintings.forEach((p) => {
|
||||
if (!p.generationMode) {
|
||||
const updatedPainting = { ...p, generationMode: MODEOPTIONS[0].value }
|
||||
updatePainting('DMXAPIPaintings', updatedPainting)
|
||||
updatePainting('dmxapi_paintings', updatedPainting)
|
||||
}
|
||||
})
|
||||
|
||||
@ -816,7 +822,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
/>
|
||||
</div>
|
||||
</ProviderTitleContainer>
|
||||
<Select value={providerOptions[2].value} onChange={handleProviderChange} style={{ marginBottom: 15 }}>
|
||||
<Select value={providerOptions[3].value} onChange={handleProviderChange} style={{ marginBottom: 15 }}>
|
||||
{providerOptions.map((provider) => (
|
||||
<Select.Option value={provider.value} key={provider.value}>
|
||||
<SelectOptionContainer>
|
||||
@ -1005,8 +1011,8 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
</InputContainer>
|
||||
</MainContainer>
|
||||
<PaintingsList
|
||||
namespace="DMXAPIPaintings"
|
||||
paintings={DMXAPIPaintings}
|
||||
namespace="dmxapi_paintings"
|
||||
paintings={dmxapi_paintings}
|
||||
selectedPainting={painting}
|
||||
onSelectPainting={onSelectPainting}
|
||||
onDeletePainting={onDeletePainting}
|
||||
|
||||
@ -40,12 +40,21 @@ import styled from 'styled-components'
|
||||
import SendMessageButton from '../home/Inputbar/SendMessageButton'
|
||||
import { SettingHelpLink, SettingTitle } from '../settings'
|
||||
import Artboard from './components/Artboard'
|
||||
import { checkProviderEnabled } from './utils'
|
||||
|
||||
const logger = loggerService.withContext('NewApiPage')
|
||||
|
||||
const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
const [mode, setMode] = useState<keyof PaintingsState>('openai_image_generate')
|
||||
const { addPainting, removePainting, updatePainting, newApiPaintings } = usePaintings()
|
||||
const { addPainting, removePainting, updatePainting, openai_image_generate, openai_image_edit } = usePaintings()
|
||||
|
||||
const newApiPaintings = useMemo(() => {
|
||||
return {
|
||||
openai_image_generate,
|
||||
openai_image_edit
|
||||
}
|
||||
}, [openai_image_generate, openai_image_edit])
|
||||
|
||||
const filteredPaintings = useMemo(() => newApiPaintings[mode] || [], [newApiPaintings, mode])
|
||||
const [painting, setPainting] = useState<PaintingAction>(filteredPaintings[0] || DEFAULT_PAINTING)
|
||||
const [currentImageIndex, setCurrentImageIndex] = useState(0)
|
||||
@ -216,6 +225,8 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
}
|
||||
|
||||
const onGenerate = async () => {
|
||||
await checkProviderEnabled(newApiProvider, t)
|
||||
|
||||
if (painting.files.length > 0) {
|
||||
const confirmed = await window.modal.confirm({
|
||||
content: t('paintings.regenerate.confirm'),
|
||||
@ -229,14 +240,6 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
const prompt = textareaRef.current?.resizableTextArea?.textArea?.value || ''
|
||||
updatePaintingState({ prompt })
|
||||
|
||||
if (!newApiProvider.enabled) {
|
||||
window.modal.error({
|
||||
content: t('error.provider_disabled'),
|
||||
centered: true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const AI = new AiProvider(newApiProvider)
|
||||
|
||||
if (!AI.getApiKey()) {
|
||||
|
||||
@ -10,10 +10,11 @@ import DmxapiPage from './DmxapiPage'
|
||||
import NewApiPage from './NewApiPage'
|
||||
import SiliconPage from './SiliconPage'
|
||||
import TokenFluxPage from './TokenFluxPage'
|
||||
import ZhipuPage from './ZhipuPage'
|
||||
|
||||
const logger = loggerService.withContext('PaintingsRoutePage')
|
||||
|
||||
const Options = ['aihubmix', 'silicon', 'dmxapi', 'tokenflux', 'new-api']
|
||||
const Options = ['zhipu', 'aihubmix', 'silicon', 'dmxapi', 'tokenflux', 'new-api']
|
||||
|
||||
const PaintingsRoutePage: FC = () => {
|
||||
const params = useParams()
|
||||
@ -29,7 +30,8 @@ const PaintingsRoutePage: FC = () => {
|
||||
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="*" element={<AihubmixPage Options={Options} />} />
|
||||
<Route path="*" element={<ZhipuPage Options={Options} />} />
|
||||
<Route path="/zhipu" element={<ZhipuPage Options={Options} />} />
|
||||
<Route path="/aihubmix" element={<AihubmixPage Options={Options} />} />
|
||||
<Route path="/silicon" element={<SiliconPage Options={Options} />} />
|
||||
<Route path="/dmxapi" element={<DmxapiPage Options={Options} />} />
|
||||
|
||||
@ -40,6 +40,7 @@ import SendMessageButton from '../home/Inputbar/SendMessageButton'
|
||||
import { SettingTitle } from '../settings'
|
||||
import Artboard from './components/Artboard'
|
||||
import PaintingsList from './components/PaintingsList'
|
||||
import { checkProviderEnabled } from './utils'
|
||||
|
||||
const logger = loggerService.withContext('SiliconPage')
|
||||
|
||||
@ -95,8 +96,8 @@ const DEFAULT_PAINTING: Painting = {
|
||||
|
||||
const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
const { t } = useTranslation()
|
||||
const { paintings, addPainting, removePainting, updatePainting } = usePaintings()
|
||||
const [painting, setPainting] = useState<Painting>(paintings[0] || DEFAULT_PAINTING)
|
||||
const { siliconflow_paintings, addPainting, removePainting, updatePainting } = usePaintings()
|
||||
const [painting, setPainting] = useState<Painting>(siliconflow_paintings[0] || DEFAULT_PAINTING)
|
||||
const { theme } = useTheme()
|
||||
const providers = useAllProviders()
|
||||
const providerOptions = Options.map((option) => {
|
||||
@ -113,6 +114,8 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const siliconflowProvider = providers.find((p) => p.id === 'silicon')
|
||||
const [currentImageIndex, setCurrentImageIndex] = useState(0)
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
@ -141,7 +144,7 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
const updatePaintingState = (updates: Partial<Painting>) => {
|
||||
const updatedPainting = { ...painting, ...updates }
|
||||
setPainting(updatedPainting)
|
||||
updatePainting('paintings', updatedPainting)
|
||||
updatePainting('siliconflow_paintings', updatedPainting)
|
||||
}
|
||||
|
||||
const onSelectModel = (modelId: string) => {
|
||||
@ -152,6 +155,8 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
}
|
||||
|
||||
const onGenerate = async () => {
|
||||
await checkProviderEnabled(siliconflowProvider!, t)
|
||||
|
||||
if (painting.files.length > 0) {
|
||||
const confirmed = await window.modal.confirm({
|
||||
content: t('paintings.regenerate.confirm'),
|
||||
@ -172,14 +177,6 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
const model = TEXT_TO_IMAGES_MODELS.find((m) => m.id === painting.model)
|
||||
const provider = getProviderByModel(model)
|
||||
|
||||
if (!provider.enabled) {
|
||||
window.modal.error({
|
||||
content: t('error.provider_disabled'),
|
||||
centered: true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!provider.apiKey) {
|
||||
window.modal.error({
|
||||
content: t('error.no_api_key'),
|
||||
@ -280,16 +277,16 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
|
||||
const onDeletePainting = (paintingToDelete: Painting) => {
|
||||
if (paintingToDelete.id === painting.id) {
|
||||
const currentIndex = paintings.findIndex((p) => p.id === paintingToDelete.id)
|
||||
const currentIndex = siliconflow_paintings.findIndex((p) => p.id === paintingToDelete.id)
|
||||
|
||||
if (currentIndex > 0) {
|
||||
setPainting(paintings[currentIndex - 1])
|
||||
} else if (paintings.length > 1) {
|
||||
setPainting(paintings[1])
|
||||
setPainting(siliconflow_paintings[currentIndex - 1])
|
||||
} else if (siliconflow_paintings.length > 1) {
|
||||
setPainting(siliconflow_paintings[1])
|
||||
}
|
||||
}
|
||||
|
||||
removePainting('paintings', paintingToDelete)
|
||||
removePainting('siliconflow_paintings', paintingToDelete)
|
||||
}
|
||||
|
||||
const onSelectPainting = (newPainting: Painting) => {
|
||||
@ -351,9 +348,9 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (paintings.length === 0) {
|
||||
if (siliconflow_paintings.length === 0) {
|
||||
const newPainting = getNewPainting()
|
||||
addPainting('paintings', newPainting)
|
||||
addPainting('siliconflow_paintings', newPainting)
|
||||
setPainting(newPainting)
|
||||
}
|
||||
|
||||
@ -362,7 +359,7 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
clearTimeout(spaceClickTimer.current)
|
||||
}
|
||||
}
|
||||
}, [paintings.length, addPainting])
|
||||
}, [siliconflow_paintings.length, addPainting])
|
||||
|
||||
return (
|
||||
<Container>
|
||||
@ -374,7 +371,7 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
size="small"
|
||||
className="nodrag"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setPainting(addPainting('paintings', getNewPainting()))}>
|
||||
onClick={() => setPainting(addPainting('siliconflow_paintings', getNewPainting()))}>
|
||||
{t('paintings.button.new.image')}
|
||||
</Button>
|
||||
</NavbarRight>
|
||||
@ -383,7 +380,7 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
<ContentContainer id="content-container">
|
||||
<LeftContainer>
|
||||
<SettingTitle style={{ marginBottom: 5 }}>{t('common.provider')}</SettingTitle>
|
||||
<Select value={providerOptions[1].value} onChange={handleProviderChange} options={providerOptions} />
|
||||
<Select value={providerOptions[2].value} onChange={handleProviderChange} options={providerOptions} />
|
||||
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('common.model')}</SettingTitle>
|
||||
<Select value={painting.model} options={modelOptions} onChange={onSelectModel} />
|
||||
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('paintings.image.size')}</SettingTitle>
|
||||
@ -529,12 +526,12 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
</InputContainer>
|
||||
</MainContainer>
|
||||
<PaintingsList
|
||||
namespace="paintings"
|
||||
paintings={paintings}
|
||||
namespace="siliconflow_paintings"
|
||||
paintings={siliconflow_paintings}
|
||||
selectedPainting={painting}
|
||||
onSelectPainting={onSelectPainting}
|
||||
onDeletePainting={onDeletePainting}
|
||||
onNewPainting={() => setPainting(addPainting('paintings', getNewPainting()))}
|
||||
onNewPainting={() => setPainting(addPainting('siliconflow_paintings', getNewPainting()))}
|
||||
/>
|
||||
</ContentContainer>
|
||||
</Container>
|
||||
|
||||
@ -32,6 +32,7 @@ import Artboard from './components/Artboard'
|
||||
import { DynamicFormRender } from './components/DynamicFormRender'
|
||||
import PaintingsList from './components/PaintingsList'
|
||||
import { DEFAULT_TOKENFLUX_PAINTING, type TokenFluxModel } from './config/tokenFluxConfig'
|
||||
import { checkProviderEnabled } from './utils'
|
||||
import TokenFluxService from './utils/TokenFluxService'
|
||||
|
||||
const logger = loggerService.withContext('TokenFluxPage')
|
||||
@ -48,8 +49,8 @@ const TokenFluxPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
|
||||
const { t, i18n } = useTranslation()
|
||||
const providers = useAllProviders()
|
||||
const { addPainting, removePainting, updatePainting, persistentData } = usePaintings()
|
||||
const tokenFluxPaintings = useMemo(() => persistentData.tokenFluxPaintings || [], [persistentData.tokenFluxPaintings])
|
||||
const { addPainting, removePainting, updatePainting, tokenflux_paintings } = usePaintings()
|
||||
const tokenFluxPaintings = tokenflux_paintings
|
||||
const [painting, setPainting] = useState<TokenFluxPainting>(
|
||||
tokenFluxPaintings[0] || { ...DEFAULT_TOKENFLUX_PAINTING, id: uuid() }
|
||||
)
|
||||
@ -105,7 +106,7 @@ const TokenFluxPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
(updates: Partial<TokenFluxPainting>) => {
|
||||
setPainting((prevPainting) => {
|
||||
const updatedPainting = { ...prevPainting, ...updates }
|
||||
updatePainting('tokenFluxPaintings', updatedPainting)
|
||||
updatePainting('tokenflux_paintings', updatedPainting)
|
||||
return updatedPainting
|
||||
})
|
||||
},
|
||||
@ -137,6 +138,8 @@ const TokenFluxPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
}
|
||||
|
||||
const onGenerate = async () => {
|
||||
await checkProviderEnabled(tokenfluxProvider, t)
|
||||
|
||||
if (painting.files.length > 0) {
|
||||
const confirmed = await window.modal.confirm({
|
||||
content: t('paintings.regenerate.confirm'),
|
||||
@ -149,22 +152,6 @@ const TokenFluxPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
|
||||
const prompt = textareaRef.current?.resizableTextArea?.textArea?.value || ''
|
||||
|
||||
if (!tokenfluxProvider.enabled) {
|
||||
window.modal.error({
|
||||
content: t('error.provider_disabled'),
|
||||
centered: true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!tokenfluxProvider.apiKey) {
|
||||
window.modal.error({
|
||||
content: t('error.no_api_key'),
|
||||
centered: true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!selectedModel || !prompt) {
|
||||
window.modal.error({
|
||||
content: t('paintings.text_desc_required'),
|
||||
@ -236,8 +223,8 @@ const TokenFluxPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
}
|
||||
|
||||
const handleAddPainting = () => {
|
||||
const newPainting = addPainting('tokenFluxPaintings', getNewPainting())
|
||||
updatePainting('tokenFluxPaintings', newPainting)
|
||||
const newPainting = addPainting('tokenflux_paintings', getNewPainting())
|
||||
updatePainting('tokenflux_paintings', newPainting)
|
||||
setPainting(newPainting as TokenFluxPainting)
|
||||
return newPainting
|
||||
}
|
||||
@ -253,7 +240,7 @@ const TokenFluxPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
}
|
||||
}
|
||||
|
||||
removePainting('tokenFluxPaintings', paintingToDelete)
|
||||
removePainting('tokenflux_paintings', paintingToDelete)
|
||||
}
|
||||
|
||||
const translate = async () => {
|
||||
@ -338,7 +325,7 @@ const TokenFluxPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
useEffect(() => {
|
||||
if (tokenFluxPaintings.length === 0) {
|
||||
const newPainting = getNewPainting()
|
||||
addPainting('tokenFluxPaintings', newPainting)
|
||||
addPainting('tokenflux_paintings', newPainting)
|
||||
setPainting(newPainting)
|
||||
}
|
||||
}, [tokenFluxPaintings, addPainting, getNewPainting])
|
||||
@ -573,7 +560,7 @@ const TokenFluxPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
</MainContainer>
|
||||
|
||||
<PaintingsList
|
||||
namespace="tokenFluxPaintings"
|
||||
namespace="tokenflux_paintings"
|
||||
paintings={tokenFluxPaintings}
|
||||
selectedPainting={painting}
|
||||
onSelectPainting={onSelectPainting as any}
|
||||
|
||||
588
src/renderer/src/pages/paintings/ZhipuPage.tsx
Normal file
@ -0,0 +1,588 @@
|
||||
import { PlusOutlined } from '@ant-design/icons'
|
||||
import AiProvider from '@renderer/aiCore'
|
||||
import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navbar'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
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'
|
||||
import { getErrorMessage, uuid } from '@renderer/utils'
|
||||
import { Avatar, Button, InputNumber, Radio, Select } from 'antd'
|
||||
import TextArea from 'antd/es/input/TextArea'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import SendMessageButton from '../home/Inputbar/SendMessageButton'
|
||||
import { SettingHelpLink, SettingTitle } from '../settings'
|
||||
import Artboard from './components/Artboard'
|
||||
import PaintingsList from './components/PaintingsList'
|
||||
import {
|
||||
COURSE_URL,
|
||||
DEFAULT_PAINTING,
|
||||
IMAGE_SIZES,
|
||||
QUALITY_OPTIONS,
|
||||
TOP_UP_URL,
|
||||
ZHIPU_PAINTING_MODELS
|
||||
} from './config/ZhipuConfig'
|
||||
import { checkProviderEnabled } from './utils'
|
||||
|
||||
const ZhipuPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
const { zhipu_paintings, addPainting, removePainting, updatePainting } = usePaintings()
|
||||
const [painting, setPainting] = useState<any>(zhipu_paintings?.[0] || DEFAULT_PAINTING)
|
||||
const { t } = useTranslation()
|
||||
const providers = useAllProviders()
|
||||
|
||||
// 确保painting使用智谱的cogview系列模型
|
||||
useEffect(() => {
|
||||
if (painting && !painting.model?.startsWith('cogview')) {
|
||||
const updatedPainting = { ...painting, model: 'cogview-3-flash' }
|
||||
setPainting(updatedPainting)
|
||||
updatePainting('zhipu_paintings', updatedPainting)
|
||||
}
|
||||
// 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)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [abortController, setAbortController] = useState<AbortController | null>(null)
|
||||
const dispatch = useAppDispatch()
|
||||
const { generating } = useRuntime()
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
|
||||
// 自定义尺寸相关状态
|
||||
const [isCustomSize, setIsCustomSize] = useState(false)
|
||||
const [customWidth, setCustomWidth] = useState<number | undefined>()
|
||||
const [customHeight, setCustomHeight] = useState<number | undefined>()
|
||||
|
||||
const updatePaintingState = (updates: Partial<any>) => {
|
||||
const updatedPainting = { ...painting, ...updates }
|
||||
setPainting(updatedPainting)
|
||||
updatePainting('zhipu_paintings', updatedPainting)
|
||||
}
|
||||
|
||||
const getNewPainting = (params?: Partial<any>) => {
|
||||
return {
|
||||
...DEFAULT_PAINTING,
|
||||
id: uuid(),
|
||||
...params
|
||||
}
|
||||
}
|
||||
|
||||
const onGenerate = async () => {
|
||||
await checkProviderEnabled(zhipuProvider, t)
|
||||
|
||||
if (isLoading) return
|
||||
|
||||
if (!painting.prompt.trim()) {
|
||||
window.modal.error({
|
||||
content: t('paintings.prompt_required'),
|
||||
centered: true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否需要重新生成(如果已有图片)
|
||||
if (painting.files.length > 0) {
|
||||
const confirmed = await window.modal.confirm({
|
||||
content: t('paintings.regenerate.confirm'),
|
||||
centered: true
|
||||
})
|
||||
if (!confirmed) return
|
||||
await FileManager.deleteFiles(painting.files)
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
dispatch(setGenerating(true))
|
||||
const controller = new AbortController()
|
||||
setAbortController(controller)
|
||||
|
||||
try {
|
||||
// 使用AiProvider调用智谱AI绘图API
|
||||
const aiProvider = new AiProvider(zhipuProvider)
|
||||
|
||||
// 准备API请求参数
|
||||
let actualImageSize = painting.imageSize
|
||||
|
||||
// 如果是自定义尺寸,使用实际的宽高值
|
||||
if (painting.imageSize === 'custom') {
|
||||
if (!customWidth || !customHeight) {
|
||||
window.modal.error({
|
||||
content: '请设置自定义尺寸的宽度和高度',
|
||||
centered: true
|
||||
})
|
||||
return
|
||||
}
|
||||
// 验证自定义尺寸是否符合智谱AI的要求
|
||||
if (customWidth < 512 || customWidth > 2048 || customHeight < 512 || customHeight > 2048) {
|
||||
window.modal.error({
|
||||
content: '自定义尺寸必须在512px-2048px之间',
|
||||
centered: true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (customWidth % 16 !== 0 || customHeight % 16 !== 0) {
|
||||
window.modal.error({
|
||||
content: '自定义尺寸必须能被16整除',
|
||||
centered: true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const totalPixels = customWidth * customHeight
|
||||
if (totalPixels > 2097152) {
|
||||
// 2^21 = 2097152
|
||||
window.modal.error({
|
||||
content: '自定义尺寸的总像素数不能超过2,097,152',
|
||||
centered: true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
actualImageSize = `${customWidth}x${customHeight}`
|
||||
}
|
||||
|
||||
const request = {
|
||||
model: painting.model,
|
||||
prompt: painting.prompt,
|
||||
negativePrompt: painting.negativePrompt,
|
||||
imageSize: actualImageSize,
|
||||
batchSize: painting.numImages,
|
||||
quality: painting.quality,
|
||||
signal: controller.signal
|
||||
}
|
||||
|
||||
// 调用智谱AI绘图API
|
||||
const imageUrls = await aiProvider.generateImage(request)
|
||||
|
||||
// 下载图片到本地文件
|
||||
if (imageUrls.length > 0) {
|
||||
const downloadedFiles = await Promise.all(
|
||||
imageUrls.map(async (url) => {
|
||||
try {
|
||||
if (!url || url.trim() === '') {
|
||||
window.message.warning({
|
||||
content: t('message.empty_url'),
|
||||
key: 'empty-url-warning'
|
||||
})
|
||||
return null
|
||||
}
|
||||
return await window.api.file.download(url)
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
(error.message.includes('Failed to parse URL') || error.message.includes('Invalid URL'))
|
||||
) {
|
||||
window.message.warning({
|
||||
content: t('message.empty_url'),
|
||||
key: 'empty-url-warning'
|
||||
})
|
||||
}
|
||||
return null
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const validFiles = downloadedFiles.filter((file): file is any => file !== null)
|
||||
|
||||
await FileManager.addFiles(validFiles)
|
||||
|
||||
// 处理响应结果
|
||||
const newPainting = {
|
||||
...painting,
|
||||
urls: imageUrls,
|
||||
files: validFiles
|
||||
}
|
||||
|
||||
updatePaintingState(newPainting)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name !== 'AbortError') {
|
||||
window.modal.error({
|
||||
content: getErrorMessage(error),
|
||||
centered: true
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
dispatch(setGenerating(false))
|
||||
setAbortController(null)
|
||||
}
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
if (abortController) {
|
||||
abortController.abort()
|
||||
}
|
||||
}
|
||||
|
||||
const nextImage = () => {
|
||||
setCurrentImageIndex((prev) => (prev + 1) % painting.files.length)
|
||||
}
|
||||
|
||||
const prevImage = () => {
|
||||
setCurrentImageIndex((prev) => (prev - 1 + painting.files.length) % painting.files.length)
|
||||
}
|
||||
|
||||
const onDeletePainting = async (paintingToDelete: any) => {
|
||||
if (paintingToDelete.id === painting.id) {
|
||||
if (isLoading) return
|
||||
|
||||
const currentIndex = zhipu_paintings.findIndex((p) => p.id === paintingToDelete.id)
|
||||
|
||||
if (currentIndex > 0) {
|
||||
setPainting(zhipu_paintings[currentIndex - 1])
|
||||
} else if (zhipu_paintings.length > 1) {
|
||||
setPainting(zhipu_paintings[1])
|
||||
}
|
||||
}
|
||||
|
||||
await removePainting('zhipu_paintings', paintingToDelete)
|
||||
|
||||
if (!zhipu_paintings || zhipu_paintings.length === 1) {
|
||||
const newPainting = getNewPainting()
|
||||
const addedPainting = addPainting('zhipu_paintings', newPainting)
|
||||
setPainting(addedPainting)
|
||||
}
|
||||
}
|
||||
|
||||
const onSelectPainting = (newPainting: any) => {
|
||||
if (generating) return
|
||||
setPainting(newPainting)
|
||||
setCurrentImageIndex(0)
|
||||
}
|
||||
|
||||
const handleProviderChange = (providerId: string) => {
|
||||
const routeName = location.pathname.split('/').pop()
|
||||
if (providerId !== routeName) {
|
||||
navigate('../' + providerId, { replace: true })
|
||||
}
|
||||
}
|
||||
|
||||
const onSelectModel = (modelId: string) => {
|
||||
updatePaintingState({ model: modelId })
|
||||
}
|
||||
|
||||
const onSelectQuality = (quality: string) => {
|
||||
updatePaintingState({ quality })
|
||||
}
|
||||
|
||||
const onSelectImageSize = (size: string) => {
|
||||
if (size === 'custom') {
|
||||
setIsCustomSize(true)
|
||||
updatePaintingState({ imageSize: 'custom' })
|
||||
} else {
|
||||
setIsCustomSize(false)
|
||||
updatePaintingState({ imageSize: size })
|
||||
}
|
||||
}
|
||||
|
||||
const onCustomSizeChange = (value: number | undefined, dimension: 'width' | 'height') => {
|
||||
if (dimension === 'width') {
|
||||
setCustomWidth(value)
|
||||
updatePaintingState({ customWidth: value })
|
||||
} else {
|
||||
setCustomHeight(value)
|
||||
updatePaintingState({ customHeight: value })
|
||||
}
|
||||
}
|
||||
|
||||
const createNewPainting = () => {
|
||||
if (generating) return
|
||||
const newPainting = getNewPainting()
|
||||
const addedPainting = addPainting('zhipu_paintings', newPainting)
|
||||
setPainting(addedPainting)
|
||||
}
|
||||
|
||||
// 移除modelOptions的定义,直接在Select中使用
|
||||
|
||||
useEffect(() => {
|
||||
if (!zhipu_paintings || zhipu_paintings.length === 0) {
|
||||
const newPainting = getNewPainting()
|
||||
addPainting('zhipu_paintings', newPainting)
|
||||
}
|
||||
}, [zhipu_paintings, addPainting])
|
||||
|
||||
// 同步自定义尺寸状态
|
||||
useEffect(() => {
|
||||
if (painting.imageSize === 'custom') {
|
||||
setIsCustomSize(true)
|
||||
// 恢复自定义尺寸的宽高值
|
||||
if (painting.customWidth) {
|
||||
setCustomWidth(painting.customWidth)
|
||||
}
|
||||
if (painting.customHeight) {
|
||||
setCustomHeight(painting.customHeight)
|
||||
}
|
||||
} else {
|
||||
setIsCustomSize(false)
|
||||
}
|
||||
}, [painting.imageSize, painting.customWidth, painting.customHeight])
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Navbar>
|
||||
<NavbarCenter>
|
||||
<Title>{t('title.paintings')}</Title>
|
||||
</NavbarCenter>
|
||||
{isMac && (
|
||||
<NavbarRight>
|
||||
<Button type="text" icon={<PlusOutlined />} onClick={createNewPainting} disabled={generating} />
|
||||
</NavbarRight>
|
||||
)}
|
||||
</Navbar>
|
||||
<ContentContainer id="content-container">
|
||||
<LeftContainer>
|
||||
<ProviderTitleContainer>
|
||||
<SettingTitle style={{ marginBottom: 5 }}>{t('common.provider')}</SettingTitle>
|
||||
<div>
|
||||
<SettingHelpLink target="_blank" href={TOP_UP_URL}>
|
||||
{t('paintings.top_up')}
|
||||
</SettingHelpLink>
|
||||
<SettingHelpLink target="_blank" href={COURSE_URL}>
|
||||
{t('paintings.paint_course')}
|
||||
</SettingHelpLink>
|
||||
<ProviderLogo
|
||||
shape="square"
|
||||
src={getProviderLogo(zhipuProvider.id)}
|
||||
size={16}
|
||||
style={{ marginLeft: 5 }}
|
||||
/>
|
||||
</div>
|
||||
</ProviderTitleContainer>
|
||||
<Select value={providerOptions[0].value} onChange={handleProviderChange} style={{ marginBottom: 15 }}>
|
||||
{providerOptions.map((provider) => (
|
||||
<Select.Option value={provider.value} key={provider.value}>
|
||||
<SelectOptionContainer>
|
||||
<ProviderLogo shape="square" src={getProviderLogo(provider.value || '')} size={16} />
|
||||
{provider.label}
|
||||
</SelectOptionContainer>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('common.model')}</SettingTitle>
|
||||
<Select
|
||||
value={painting.model}
|
||||
onChange={onSelectModel}
|
||||
style={{ width: '100%' }}
|
||||
options={ZHIPU_PAINTING_MODELS.map((model) => ({
|
||||
label: model.name,
|
||||
value: model.id
|
||||
}))}
|
||||
/>
|
||||
|
||||
{painting.model === 'cogview-4-250304' && (
|
||||
<>
|
||||
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('paintings.quality')}</SettingTitle>
|
||||
<Radio.Group value={painting.quality} onChange={(e) => onSelectQuality(e.target.value)}>
|
||||
{QUALITY_OPTIONS.map((option) => (
|
||||
<Radio key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</Radio>
|
||||
))}
|
||||
</Radio.Group>
|
||||
</>
|
||||
)}
|
||||
|
||||
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('paintings.image.size')}</SettingTitle>
|
||||
<Select
|
||||
value={isCustomSize ? 'custom' : painting.imageSize}
|
||||
onChange={onSelectImageSize}
|
||||
style={{ width: '100%' }}>
|
||||
{IMAGE_SIZES.map((size) => (
|
||||
<Select.Option key={size.value} value={size.value}>
|
||||
{size.label}
|
||||
</Select.Option>
|
||||
))}
|
||||
<Select.Option value="custom" key="custom">
|
||||
{t('paintings.custom_size')}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
|
||||
{/* 自定义尺寸输入框 */}
|
||||
{isCustomSize && (
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<HStack style={{ gap: 8, alignItems: 'center' }}>
|
||||
<InputNumber
|
||||
placeholder="W"
|
||||
value={customWidth}
|
||||
controls={false}
|
||||
onChange={(value) => onCustomSizeChange(value || undefined, 'width')}
|
||||
min={512}
|
||||
max={2048}
|
||||
style={{ width: 80, flex: 1 }}
|
||||
/>
|
||||
<span style={{ color: 'var(--color-text-2)', fontSize: '12px' }}>x</span>
|
||||
<InputNumber
|
||||
placeholder="H"
|
||||
value={customHeight}
|
||||
controls={false}
|
||||
onChange={(value) => onCustomSizeChange(value || undefined, 'height')}
|
||||
min={512}
|
||||
max={2048}
|
||||
style={{ width: 80, flex: 1 }}
|
||||
/>
|
||||
<span style={{ color: 'var(--color-text-2)', fontSize: '12px' }}>px</span>
|
||||
</HStack>
|
||||
<div style={{ marginTop: 5, fontSize: '12px', color: 'var(--color-text-3)' }}>
|
||||
长宽均需满足512px-2048px之间, 需被16整除, 并保证最大像素数不超过2^21px
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</LeftContainer>
|
||||
<MainContainer>
|
||||
<Artboard
|
||||
painting={painting}
|
||||
isLoading={isLoading}
|
||||
currentImageIndex={currentImageIndex}
|
||||
onPrevImage={prevImage}
|
||||
onNextImage={nextImage}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
<InputContainer>
|
||||
<Textarea
|
||||
variant="borderless"
|
||||
disabled={isLoading}
|
||||
value={painting.prompt}
|
||||
spellCheck={false}
|
||||
onChange={(e) => updatePaintingState({ prompt: e.target.value })}
|
||||
placeholder={t('paintings.prompt_placeholder')}
|
||||
/>
|
||||
<Toolbar>
|
||||
<ToolbarMenu>
|
||||
<SendMessageButton sendMessage={onGenerate} disabled={isLoading} />
|
||||
</ToolbarMenu>
|
||||
</Toolbar>
|
||||
</InputContainer>
|
||||
</MainContainer>
|
||||
<PaintingsList
|
||||
namespace="zhipu_paintings"
|
||||
paintings={zhipu_paintings}
|
||||
selectedPainting={painting}
|
||||
onSelectPainting={onSelectPainting}
|
||||
onDeletePainting={onDeletePainting}
|
||||
onNewPainting={createNewPainting}
|
||||
/>
|
||||
</ContentContainer>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
const ContentContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
`
|
||||
|
||||
const LeftContainer = styled(Scrollbar)`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
background-color: var(--color-background);
|
||||
max-width: var(--assistants-width);
|
||||
border-right: 0.5px solid var(--color-border);
|
||||
`
|
||||
const MainContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background-color: var(--color-background);
|
||||
`
|
||||
|
||||
const Textarea = styled(TextArea)`
|
||||
padding: 10px;
|
||||
border-radius: 0;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
resize: none !important;
|
||||
overflow: auto;
|
||||
width: auto;
|
||||
`
|
||||
|
||||
const InputContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 95px;
|
||||
max-height: 95px;
|
||||
position: relative;
|
||||
border: 1px solid var(--color-border-soft);
|
||||
transition: all 0.3s ease;
|
||||
margin: 0 20px 15px 20px;
|
||||
border-radius: 10px;
|
||||
`
|
||||
|
||||
const Toolbar = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
padding: 0 8px;
|
||||
padding-bottom: 0;
|
||||
height: 40px;
|
||||
`
|
||||
|
||||
const ToolbarMenu = styled.div`
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
const Title = styled.h1`
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
`
|
||||
|
||||
const ProviderTitleContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
`
|
||||
|
||||
const SelectOptionContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
const ProviderLogo = styled(Avatar)`
|
||||
border-radius: 4px;
|
||||
`
|
||||
|
||||
export default ZhipuPage
|
||||
48
src/renderer/src/pages/paintings/config/ZhipuConfig.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { Model } from '@renderer/types'
|
||||
import { uuid } from '@renderer/utils'
|
||||
|
||||
export const COURSE_URL = 'https://docs.bigmodel.cn/cn/guide/models/image-generation/cogview-4'
|
||||
export const TOP_UP_URL = 'https://zhipuaishengchan.datasink.sensorsdata.cn/t/iv'
|
||||
|
||||
export const ZHIPU_PAINTING_MODELS: Model[] = [
|
||||
{
|
||||
id: 'cogview-3-flash',
|
||||
provider: 'zhipu',
|
||||
name: 'CogView-3-Flash',
|
||||
group: 'CogView'
|
||||
},
|
||||
{
|
||||
id: 'cogview-4-250304',
|
||||
provider: 'zhipu',
|
||||
name: 'CogView-4-250304',
|
||||
group: 'CogView'
|
||||
}
|
||||
]
|
||||
|
||||
export const DEFAULT_PAINTING = {
|
||||
id: uuid(),
|
||||
urls: [],
|
||||
files: [],
|
||||
prompt: '',
|
||||
negativePrompt: '',
|
||||
imageSize: '1024x1024',
|
||||
numImages: 1,
|
||||
seed: '',
|
||||
model: 'cogview-3-flash',
|
||||
quality: 'standard'
|
||||
}
|
||||
|
||||
export const QUALITY_OPTIONS = [
|
||||
{ label: '标准(默认)', value: 'standard' },
|
||||
{ label: '高清', value: 'hd' }
|
||||
]
|
||||
|
||||
export const IMAGE_SIZES = [
|
||||
{ label: '1024x1024 (默认)', value: '1024x1024' },
|
||||
{ label: '768x1344', value: '768x1344' },
|
||||
{ label: '864x1152', value: '864x1152' },
|
||||
{ label: '1344x768', value: '1344x768' },
|
||||
{ label: '1152x864', value: '1152x864' },
|
||||
{ label: '1440x720', value: '1440x720' },
|
||||
{ label: '720x1440', value: '720x1440' }
|
||||
]
|
||||
@ -51,12 +51,12 @@ export type ConfigItem = {
|
||||
condition?: (painting: PaintingAction) => boolean
|
||||
}
|
||||
|
||||
export type AihubmixMode = 'generate' | 'remix' | 'upscale'
|
||||
export type AihubmixMode = 'aihubmix_image_generate' | 'aihubmix_image_remix' | 'aihubmix_image_upscale'
|
||||
|
||||
// 创建配置项函数
|
||||
export const createModeConfigs = (): Record<AihubmixMode, ConfigItem[]> => {
|
||||
return {
|
||||
generate: [
|
||||
aihubmix_image_generate: [
|
||||
{
|
||||
type: 'select',
|
||||
key: 'model',
|
||||
@ -266,7 +266,7 @@ export const createModeConfigs = (): Record<AihubmixMode, ConfigItem[]> => {
|
||||
condition: (painting) => painting.model === 'FLUX.1-Kontext-pro'
|
||||
}
|
||||
],
|
||||
remix: [
|
||||
aihubmix_image_remix: [
|
||||
{
|
||||
type: 'image',
|
||||
key: 'imageFile',
|
||||
@ -349,7 +349,7 @@ export const createModeConfigs = (): Record<AihubmixMode, ConfigItem[]> => {
|
||||
tooltip: 'paintings.remix.magic_prompt_option_tip'
|
||||
}
|
||||
],
|
||||
upscale: [
|
||||
aihubmix_image_upscale: [
|
||||
{
|
||||
type: 'image',
|
||||
key: 'imageFile',
|
||||
|
||||
24
src/renderer/src/pages/paintings/utils/index.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { Provider } from '@renderer/types'
|
||||
import { TFunction } from 'i18next'
|
||||
import { isEmpty } from 'lodash'
|
||||
|
||||
export function checkProviderEnabled(provider: Provider, t: TFunction): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (provider.enabled && !isEmpty(provider.apiKey)) {
|
||||
resolve(true)
|
||||
return
|
||||
}
|
||||
|
||||
window.modal.warning({
|
||||
content: provider.apiKey ? t('error.no_api_key') : t('error.provider_disabled'),
|
||||
centered: true,
|
||||
closable: true,
|
||||
okText: t('common.go_to_settings'),
|
||||
onOk: () => {
|
||||
window.navigate?.(`/settings/provider?id=${provider.id}`)
|
||||
reject('Provider disabled')
|
||||
},
|
||||
onCancel: () => reject('Provider disabled')
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
import ZhipuLogo from '@renderer/assets/images/providers/zhipu.png'
|
||||
import { ExternalLink } from 'lucide-react'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -6,6 +7,12 @@ import styled from 'styled-components'
|
||||
import { SettingTitle } from '..'
|
||||
|
||||
const mcpMarkets = [
|
||||
{
|
||||
name: 'BigModel MCP Market',
|
||||
url: 'https://bigmodel.cn/marketplace/index/mcp',
|
||||
logo: ZhipuLogo,
|
||||
descriptionKey: 'settings.mcp.more.zhipu'
|
||||
},
|
||||
{
|
||||
name: 'modelscope.cn',
|
||||
url: 'https://www.modelscope.cn/mcp',
|
||||
|
||||
@ -50,6 +50,7 @@ const ModelList: React.FC<ModelListProps> = ({ providerId }) => {
|
||||
const providerConfig = PROVIDER_URLS[provider.id]
|
||||
const docsWebsite = providerConfig?.websites?.docs
|
||||
const modelsWebsite = providerConfig?.websites?.models
|
||||
const editable = provider.id !== 'cherryin'
|
||||
|
||||
const [searchText, _setSearchText] = useState('')
|
||||
const [displayedModelGroups, setDisplayedModelGroups] = useState<ModelGroups | null>(() => {
|
||||
@ -112,15 +113,17 @@ const ModelList: React.FC<ModelListProps> = ({ providerId }) => {
|
||||
tooltip={t('models.search.tooltip')}
|
||||
/>
|
||||
</HStack>
|
||||
<HStack>
|
||||
<Tooltip title={t('settings.models.check.button_caption')} mouseLeaveDelay={0}>
|
||||
<Button
|
||||
type="text"
|
||||
onClick={runHealthCheck}
|
||||
icon={<StreamlineGoodHealthAndWellBeing size={16} isActive={isHealthChecking} />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</HStack>
|
||||
{editable && (
|
||||
<HStack>
|
||||
<Tooltip title={t('settings.models.check.button_caption')} mouseLeaveDelay={0}>
|
||||
<Button
|
||||
type="text"
|
||||
onClick={runHealthCheck}
|
||||
icon={<StreamlineGoodHealthAndWellBeing size={16} isActive={isHealthChecking} />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</HStack>
|
||||
)}
|
||||
</HStack>
|
||||
</SettingSubtitle>
|
||||
<Spin spinning={isLoading} indicator={<LoadingIcon color="var(--color-text-2)" />}>
|
||||
@ -136,6 +139,7 @@ const ModelList: React.FC<ModelListProps> = ({ providerId }) => {
|
||||
onEditModel={(model) => EditModelPopup.show({ provider, model })}
|
||||
onRemoveModel={removeModel}
|
||||
onRemoveGroup={() => displayedModelGroups[group].forEach((model) => removeModel(model))}
|
||||
disabled={!editable}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
@ -163,14 +167,16 @@ const ModelList: React.FC<ModelListProps> = ({ providerId }) => {
|
||||
<div style={{ height: 5 }} />
|
||||
)}
|
||||
</Flex>
|
||||
<Flex gap={10} style={{ marginTop: 12 }}>
|
||||
<Button type="primary" onClick={onManageModel} icon={<ListCheck size={16} />} disabled={isHealthChecking}>
|
||||
{t('button.manage')}
|
||||
</Button>
|
||||
<Button type="default" onClick={onAddModel} icon={<Plus size={16} />} disabled={isHealthChecking}>
|
||||
{t('button.add')}
|
||||
</Button>
|
||||
</Flex>
|
||||
{editable && (
|
||||
<Flex gap={10} style={{ marginTop: 12 }}>
|
||||
<Button type="primary" onClick={onManageModel} icon={<ListCheck size={16} />} disabled={isHealthChecking}>
|
||||
{t('button.manage')}
|
||||
</Button>
|
||||
<Button type="default" onClick={onAddModel} icon={<Plus size={16} />} disabled={isHealthChecking}>
|
||||
{t('button.add')}
|
||||
</Button>
|
||||
</Flex>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { FreeTrialModelTag } from '@renderer/components/FreeTrialModelTag'
|
||||
import { type HealthResult, HealthStatusIndicator } from '@renderer/components/HealthStatusIndicator'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import ModelIdWithTags from '@renderer/components/ModelIdWithTags'
|
||||
@ -46,6 +47,7 @@ const ModelListItem: React.FC<ModelListItemProps> = ({ ref, model, modelStatus,
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
/>
|
||||
<FreeTrialModelTag model={model} />
|
||||
</HStack>
|
||||
<HStack alignItems="center" gap={6}>
|
||||
<HealthStatusIndicator results={healthResults} loading={isChecking} showLatency />
|
||||
|
||||
@ -37,7 +37,7 @@ const logger = loggerService.withContext('ProviderList')
|
||||
const BUTTON_WRAPPER_HEIGHT = 50
|
||||
|
||||
const ProviderList: FC = () => {
|
||||
const [searchParams] = useSearchParams()
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const providers = useAllProviders()
|
||||
const { updateProviders, addProvider, removeProvider, updateProvider } = useProviders()
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
@ -48,12 +48,9 @@ const ProviderList: FC = () => {
|
||||
const [providerLogos, setProviderLogos] = useState<Record<string, string>>({})
|
||||
const listRef = useRef<DraggableVirtualListRef>(null)
|
||||
|
||||
const setSelectedProvider = useCallback(
|
||||
(provider: Provider) => {
|
||||
startTransition(() => _setSelectedProvider(provider))
|
||||
},
|
||||
[_setSelectedProvider]
|
||||
)
|
||||
const setSelectedProvider = useCallback((provider: Provider) => {
|
||||
startTransition(() => _setSelectedProvider(provider))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const loadAllLogos = async () => {
|
||||
@ -94,8 +91,10 @@ const ProviderList: FC = () => {
|
||||
} else {
|
||||
setSelectedProvider(providers[0])
|
||||
}
|
||||
searchParams.delete('id')
|
||||
setSearchParams(searchParams)
|
||||
}
|
||||
}, [providers, searchParams, setSelectedProvider, setTimeoutTimer])
|
||||
}, [providers, searchParams, setSearchParams, setSelectedProvider, setTimeoutTimer])
|
||||
|
||||
// Handle provider add key from URL schema
|
||||
useEffect(() => {
|
||||
|
||||
@ -11,6 +11,8 @@ import i18n from '@renderer/i18n'
|
||||
import { ModelList } from '@renderer/pages/settings/ProviderSettings/ModelList'
|
||||
import { checkApi } from '@renderer/services/ApiService'
|
||||
import { isProviderSupportAuth } from '@renderer/services/ProviderService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { updateWebSearchProvider } from '@renderer/store/websearch'
|
||||
import { isSystemProvider } from '@renderer/types'
|
||||
import { ApiKeyConnectivity, HealthStatus } from '@renderer/types/healthCheck'
|
||||
import { formatApiHost, formatApiKeys, getFancyProviderName, isOpenAIProvider } from '@renderer/utils'
|
||||
@ -55,10 +57,11 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const isAzureOpenAI = provider.id === 'azure-openai' || provider.type === 'azure-openai'
|
||||
|
||||
const isDmxapi = provider.id === 'dmxapi'
|
||||
const hideApiInput = ['vertexai', 'aws-bedrock', 'cherryin'].includes(provider.id)
|
||||
|
||||
const providerConfig = PROVIDER_URLS[provider.id]
|
||||
const officialWebsite = providerConfig?.websites?.official
|
||||
@ -73,10 +76,15 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
checking: false
|
||||
})
|
||||
|
||||
const updateWebSearchProviderKey = ({ apiKey }: { apiKey: string }) => {
|
||||
provider.id === 'zhipu' && dispatch(updateWebSearchProvider({ id: 'zhipu', apiKey: apiKey.split(',')[0] }))
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const debouncedUpdateApiKey = useCallback(
|
||||
debounce((value) => {
|
||||
updateProvider({ apiKey: formatApiKeys(value) })
|
||||
updateWebSearchProviderKey({ apiKey: formatApiKeys(value) })
|
||||
}, 150),
|
||||
[]
|
||||
)
|
||||
@ -268,7 +276,7 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
{isProviderSupportAuth(provider) && <ProviderOAuth providerId={provider.id} />}
|
||||
{provider.id === 'openai' && <OpenAIAlert />}
|
||||
{isDmxapi && <DMXAPISettings providerId={provider.id} />}
|
||||
{provider.id !== 'vertexai' && provider.id !== 'aws-bedrock' && (
|
||||
{!hideApiInput && (
|
||||
<>
|
||||
<SettingSubtitle
|
||||
style={{
|
||||
|
||||
@ -4,6 +4,7 @@ import BochaLogo from '@renderer/assets/images/search/bocha.webp'
|
||||
import ExaLogo from '@renderer/assets/images/search/exa.png'
|
||||
import SearxngLogo from '@renderer/assets/images/search/searxng.svg'
|
||||
import TavilyLogo from '@renderer/assets/images/search/tavily.png'
|
||||
import ZhipuLogo from '@renderer/assets/images/search/zhipu.png'
|
||||
import ApiKeyListPopup from '@renderer/components/Popups/ApiKeyListPopup/popup'
|
||||
import { WEB_SEARCH_PROVIDER_CONFIG } from '@renderer/config/webSearchProviders'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
@ -140,6 +141,8 @@ const WebSearchProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
|
||||
const getWebSearchProviderLogo = (providerId: WebSearchProviderId) => {
|
||||
switch (providerId) {
|
||||
case 'zhipu':
|
||||
return ZhipuLogo
|
||||
case 'tavily':
|
||||
return TavilyLogo
|
||||
case 'searxng':
|
||||
|
||||
@ -747,7 +747,7 @@ const TranslatePage: FC = () => {
|
||||
</InputContainerDraggingHintContainer>
|
||||
)}
|
||||
<FloatButton
|
||||
style={{ position: 'absolute', left: 8, bottom: 8 }}
|
||||
style={{ position: 'absolute', left: 10, bottom: 10, width: 35, height: 35 }}
|
||||
className="float-button"
|
||||
icon={<PlusOutlined />}
|
||||
tooltip={t('common.upload_files')}
|
||||
|
||||
@ -9,10 +9,13 @@ import LocalBingProvider from './LocalBingProvider'
|
||||
import LocalGoogleProvider from './LocalGoogleProvider'
|
||||
import SearxngProvider from './SearxngProvider'
|
||||
import TavilyProvider from './TavilyProvider'
|
||||
import ZhipuProvider from './ZhipuProvider'
|
||||
|
||||
export default class WebSearchProviderFactory {
|
||||
static create(provider: WebSearchProvider): BaseWebSearchProvider {
|
||||
switch (provider.id) {
|
||||
case 'zhipu':
|
||||
return new ZhipuProvider(provider)
|
||||
case 'tavily':
|
||||
return new TavilyProvider(provider)
|
||||
case 'bocha':
|
||||
|
||||
@ -0,0 +1,91 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { WebSearchState } from '@renderer/store/websearch'
|
||||
import { WebSearchProvider, WebSearchProviderResponse } from '@renderer/types'
|
||||
|
||||
import BaseWebSearchProvider from './BaseWebSearchProvider'
|
||||
|
||||
const logger = loggerService.withContext('ZhipuProvider')
|
||||
|
||||
interface ZhipuWebSearchRequest {
|
||||
search_query: string
|
||||
search_engine?: string
|
||||
search_intent?: boolean
|
||||
}
|
||||
|
||||
interface ZhipuWebSearchResponse {
|
||||
id: string
|
||||
created: number
|
||||
request_id: string
|
||||
search_intent?: Array<{
|
||||
query: string
|
||||
intent: string
|
||||
keywords: string
|
||||
}>
|
||||
search_result: Array<{
|
||||
title: string
|
||||
content: string
|
||||
link: string
|
||||
media?: string
|
||||
icon?: string
|
||||
refer?: string
|
||||
publish_date?: string
|
||||
}>
|
||||
}
|
||||
|
||||
export default class ZhipuProvider extends BaseWebSearchProvider {
|
||||
constructor(provider: WebSearchProvider) {
|
||||
super(provider)
|
||||
if (!this.apiKey) {
|
||||
throw new Error('API key is required for Zhipu provider')
|
||||
}
|
||||
if (!this.apiHost) {
|
||||
throw new Error('API host is required for Zhipu provider')
|
||||
}
|
||||
}
|
||||
|
||||
public async search(query: string, websearch: WebSearchState): Promise<WebSearchProviderResponse> {
|
||||
try {
|
||||
if (!query.trim()) {
|
||||
throw new Error('Search query cannot be empty')
|
||||
}
|
||||
|
||||
const requestBody: ZhipuWebSearchRequest = {
|
||||
search_query: query,
|
||||
search_engine: 'search_std',
|
||||
search_intent: false
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.apiHost}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
...this.defaultHeaders()
|
||||
},
|
||||
body: JSON.stringify(requestBody)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('Zhipu search failed:', { status: response.status, error: errorText })
|
||||
throw new Error(`HTTP ${response.status}: ${errorText}`)
|
||||
}
|
||||
|
||||
const data: ZhipuWebSearchResponse = await response.json()
|
||||
|
||||
return {
|
||||
query: query,
|
||||
results: data.search_result.slice(0, websearch.maxResults).map((result) => {
|
||||
return {
|
||||
title: result.title || 'No title',
|
||||
content: result.content || '',
|
||||
url: result.link || ''
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Zhipu search failed:', error as Error)
|
||||
throw new Error(`Search failed: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -810,7 +810,7 @@ export async function fetchGenerate({
|
||||
|
||||
export function hasApiKey(provider: Provider) {
|
||||
if (!provider) return false
|
||||
if (provider.id === 'ollama' || provider.id === 'lmstudio' || provider.type === 'vertexai') return true
|
||||
if (['ollama', 'lmstudio', 'vertexai', 'cherryin'].includes(provider.id)) return true
|
||||
return !isEmpty(provider.apiKey)
|
||||
}
|
||||
|
||||
|
||||
@ -46,13 +46,17 @@ export async function handleDelete(fileId: string, t: (key: string) => string) {
|
||||
const file = await FileManager.getFile(fileId)
|
||||
if (!file) return
|
||||
|
||||
const paintings = await store.getState().paintings.paintings
|
||||
const paintingsFiles = paintings.flatMap((p) => p.files)
|
||||
const paintings = store.getState().paintings
|
||||
const paintingsFiles = Object.values(paintings)
|
||||
.flat()
|
||||
.filter((painting) => painting?.files?.length > 0)
|
||||
.flatMap((painting) => painting.files)
|
||||
|
||||
if (paintingsFiles.some((p) => p.id === fileId)) {
|
||||
window.modal.warning({ content: t('files.delete.paintings.warning'), centered: true })
|
||||
return
|
||||
}
|
||||
|
||||
await FileManager.deleteFile(fileId, true)
|
||||
|
||||
const relatedBlocks = await db.message_blocks.where('file.id').equals(fileId).toArray()
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import store from '@renderer/store'
|
||||
import { Model } from '@renderer/types'
|
||||
import { getFancyProviderName } from '@renderer/utils'
|
||||
import { pick } from 'lodash'
|
||||
|
||||
import { getProviderName } from './ProviderService'
|
||||
|
||||
export const getModelUniqId = (m?: Model) => {
|
||||
return m?.id ? JSON.stringify(pick(m, ['id', 'provider'])) : ''
|
||||
}
|
||||
@ -22,7 +23,7 @@ export function getModelName(model?: Model) {
|
||||
const modelName = model?.name || model?.id || ''
|
||||
|
||||
if (provider) {
|
||||
const providerName = getFancyProviderName(provider)
|
||||
const providerName = getProviderName(model)
|
||||
return `${modelName} | ${providerName}`
|
||||
}
|
||||
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import store from '@renderer/store'
|
||||
import { Provider } from '@renderer/types'
|
||||
import { Model, Provider } from '@renderer/types'
|
||||
import { getFancyProviderName } from '@renderer/utils'
|
||||
|
||||
export function getProviderName(id: string) {
|
||||
const provider = store.getState().llm.providers.find((p) => p.id === id)
|
||||
export function getProviderName(model?: Model) {
|
||||
const provider = getProviderByModel(model)
|
||||
|
||||
if (!provider) {
|
||||
return ''
|
||||
}
|
||||
@ -11,6 +12,26 @@ export function getProviderName(id: string) {
|
||||
return getFancyProviderName(provider)
|
||||
}
|
||||
|
||||
export function getProviderByModel(model?: Model) {
|
||||
const id = model?.provider
|
||||
const provider = store.getState().llm.providers.find((p) => p.id === id)
|
||||
|
||||
if (provider?.id === 'cherryin') {
|
||||
const map = {
|
||||
'glm-4.5-flash': 'zhipu',
|
||||
'Qwen/Qwen3-8B': 'silicon'
|
||||
}
|
||||
|
||||
const providerId = map[model?.id as keyof typeof map]
|
||||
|
||||
if (providerId) {
|
||||
return getProviderById(providerId)
|
||||
}
|
||||
}
|
||||
|
||||
return provider
|
||||
}
|
||||
|
||||
export function isProviderSupportAuth(provider: Provider) {
|
||||
const supportProviders = ['302ai', 'silicon', 'aihubmix', 'ppio', 'tokenflux']
|
||||
return supportProviders.includes(provider.id)
|
||||
@ -20,3 +41,7 @@ export function isProviderSupportCharge(provider: Provider) {
|
||||
const supportProviders = ['302ai', 'silicon', 'aihubmix', 'ppio']
|
||||
return supportProviders.includes(provider.id)
|
||||
}
|
||||
|
||||
export function getProviderById(id: string) {
|
||||
return store.getState().llm.providers.find((p) => p.id === id)
|
||||
}
|
||||
|
||||
@ -83,7 +83,9 @@ export const createBaseCallbacks = (deps: BaseCallbacksDependencies) => {
|
||||
originalMessage: error.message,
|
||||
stack: error.stack,
|
||||
status: error.status || error.code,
|
||||
requestId: error.request_id
|
||||
requestId: error.request_id,
|
||||
providerId: error.providerId,
|
||||
i18nKey: error.i18nKey
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime
|
||||
|
||||
@ -64,7 +64,7 @@ const persistedReducer = persistReducer(
|
||||
{
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 138,
|
||||
version: 140,
|
||||
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'],
|
||||
migrate
|
||||
},
|
||||
|
||||
@ -2,7 +2,12 @@ import { loggerService } from '@logger'
|
||||
import { nanoid } from '@reduxjs/toolkit'
|
||||
import { DEFAULT_CONTEXTCOUNT, DEFAULT_TEMPERATURE, isMac } from '@renderer/config/constant'
|
||||
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
||||
import { isFunctionCallingModel, isNotSupportedTextDelta, SYSTEM_MODELS } from '@renderer/config/models'
|
||||
import {
|
||||
glm45FlashModel,
|
||||
isFunctionCallingModel,
|
||||
isNotSupportedTextDelta,
|
||||
SYSTEM_MODELS
|
||||
} from '@renderer/config/models'
|
||||
import { BUILTIN_OCR_PROVIDERS, BUILTIN_OCR_PROVIDERS_MAP, DEFAULT_OCR_PROVIDER } from '@renderer/config/ocr'
|
||||
import { TRANSLATE_PROMPT } from '@renderer/config/prompts'
|
||||
import {
|
||||
@ -100,7 +105,9 @@ function addWebSearchProvider(state: RootState, id: string) {
|
||||
if (!state.websearch.providers.find((p) => p.id === id)) {
|
||||
const provider = defaultWebSearchProviders.find((p) => p.id === id)
|
||||
if (provider) {
|
||||
state.websearch.providers.push(provider)
|
||||
// Prevent mutating read only property of object
|
||||
// Otherwise, it will cause the error: Cannot assign to read only property 'apiKey' of object '#<Object>'
|
||||
state.websearch.providers.push({ ...provider })
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1563,8 +1570,8 @@ const migrateConfig = {
|
||||
},
|
||||
'107': (state: RootState) => {
|
||||
try {
|
||||
if (state.paintings && !state.paintings.DMXAPIPaintings) {
|
||||
state.paintings.DMXAPIPaintings = []
|
||||
if (state.paintings && !state.paintings.dmxapi_paintings) {
|
||||
state.paintings.dmxapi_paintings = []
|
||||
}
|
||||
return state
|
||||
} catch (error) {
|
||||
@ -1593,8 +1600,8 @@ const migrateConfig = {
|
||||
},
|
||||
'110': (state: RootState) => {
|
||||
try {
|
||||
if (state.paintings && !state.paintings.tokenFluxPaintings) {
|
||||
state.paintings.tokenFluxPaintings = []
|
||||
if (state.paintings && !state.paintings.tokenflux_paintings) {
|
||||
state.paintings.tokenflux_paintings = []
|
||||
}
|
||||
state.settings.showTokens = true
|
||||
state.settings.testPlan = false
|
||||
@ -2205,6 +2212,81 @@ const migrateConfig = {
|
||||
logger.error('migrate 138 error', error as Error)
|
||||
return state
|
||||
}
|
||||
},
|
||||
'139': (state: RootState) => {
|
||||
try {
|
||||
addProvider(state, 'cherryin')
|
||||
state.llm.providers = moveProvider(state.llm.providers, 'cherryin', 1)
|
||||
|
||||
const zhipuProvider = state.llm.providers.find((p) => p.id === 'zhipu')
|
||||
|
||||
if (zhipuProvider) {
|
||||
// Update zhipu model list
|
||||
if (!zhipuProvider.enabled) {
|
||||
zhipuProvider.models = SYSTEM_MODELS.zhipu
|
||||
}
|
||||
|
||||
// Update zhipu model list
|
||||
if (zhipuProvider.models.length === 0) {
|
||||
zhipuProvider.models = SYSTEM_MODELS.zhipu
|
||||
}
|
||||
|
||||
// Add GLM-4.5-Flash model if not exists
|
||||
const hasGlm45FlashModel = zhipuProvider?.models.find((m) => m.id === 'glm-4.5-flash')
|
||||
|
||||
if (!hasGlm45FlashModel) {
|
||||
zhipuProvider?.models.push(glm45FlashModel)
|
||||
}
|
||||
|
||||
// Update default painting provider to zhipu
|
||||
state.settings.defaultPaintingProvider = 'zhipu'
|
||||
|
||||
// Add zhipu web search provider
|
||||
addWebSearchProvider(state, 'zhipu')
|
||||
|
||||
// Update zhipu web search provider api key
|
||||
if (zhipuProvider.apiKey) {
|
||||
state?.websearch?.providers.forEach((provider) => {
|
||||
if (provider.id === 'zhipu') {
|
||||
provider.apiKey = zhipuProvider.apiKey
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return state
|
||||
} catch (error) {
|
||||
logger.error('migrate 139 error', error as Error)
|
||||
return state
|
||||
}
|
||||
},
|
||||
'140': (state: RootState) => {
|
||||
try {
|
||||
state.paintings = {
|
||||
// @ts-ignore paintings
|
||||
siliconflow_paintings: state?.paintings?.paintings || [],
|
||||
// @ts-ignore DMXAPIPaintings
|
||||
dmxapi_paintings: state?.paintings?.DMXAPIPaintings || [],
|
||||
// @ts-ignore tokenFluxPaintings
|
||||
tokenflux_paintings: state?.paintings?.tokenFluxPaintings || [],
|
||||
zhipu_paintings: [],
|
||||
// @ts-ignore generate
|
||||
aihubmix_image_generate: state?.paintings?.generate || [],
|
||||
// @ts-ignore remix
|
||||
aihubmix_image_remix: state?.paintings?.remix || [],
|
||||
// @ts-ignore edit
|
||||
aihubmix_image_edit: state?.paintings?.edit || [],
|
||||
// @ts-ignore upscale
|
||||
aihubmix_image_upscale: state?.paintings?.upscale || [],
|
||||
openai_image_generate: state?.paintings?.openai_image_generate || [],
|
||||
openai_image_edit: state?.paintings?.openai_image_edit || []
|
||||
}
|
||||
|
||||
return state
|
||||
} catch (error) {
|
||||
logger.error('migrate 140 error', error as Error)
|
||||
return state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,13 +5,19 @@ import { PaintingAction, PaintingsState } from '@renderer/types'
|
||||
const logger = loggerService.withContext('Store:paintings')
|
||||
|
||||
const initialState: PaintingsState = {
|
||||
paintings: [],
|
||||
generate: [],
|
||||
remix: [],
|
||||
edit: [],
|
||||
upscale: [],
|
||||
DMXAPIPaintings: [],
|
||||
tokenFluxPaintings: [],
|
||||
// SiliconFlow
|
||||
siliconflow_paintings: [],
|
||||
// DMXAPI
|
||||
dmxapi_paintings: [],
|
||||
// TokenFlux
|
||||
tokenflux_paintings: [],
|
||||
zhipu_paintings: [],
|
||||
// Aihubmix
|
||||
aihubmix_image_generate: [],
|
||||
aihubmix_image_remix: [],
|
||||
aihubmix_image_edit: [],
|
||||
aihubmix_image_upscale: [],
|
||||
// OpenAI
|
||||
openai_image_generate: [],
|
||||
openai_image_edit: []
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { TRANSLATE_PROMPT } from '@renderer/config/prompts'
|
||||
import { DEFAULT_SIDEBAR_ICONS } from '@renderer/config/sidebar'
|
||||
import {
|
||||
@ -240,7 +241,7 @@ export const initialState: SettingsState = {
|
||||
userTheme: {
|
||||
colorPrimary: '#00b96b'
|
||||
},
|
||||
windowStyle: 'opaque',
|
||||
windowStyle: isMac ? 'transparent' : 'opaque',
|
||||
fontSize: 14,
|
||||
topicPosition: 'left',
|
||||
showTopicTime: false,
|
||||
@ -375,7 +376,7 @@ export const initialState: SettingsState = {
|
||||
localBackupSyncInterval: 0,
|
||||
localBackupMaxBackups: 0,
|
||||
localBackupSkipBackupFile: false,
|
||||
defaultPaintingProvider: 'aihubmix',
|
||||
defaultPaintingProvider: 'zhipu',
|
||||
s3: {
|
||||
endpoint: '',
|
||||
region: '',
|
||||
|
||||
@ -259,6 +259,7 @@ export type Provider = {
|
||||
}
|
||||
|
||||
export const SystemProviderIds = {
|
||||
cherryin: 'cherryin',
|
||||
silicon: 'silicon',
|
||||
aihubmix: 'aihubmix',
|
||||
ocoolai: 'ocoolai',
|
||||
@ -394,7 +395,7 @@ export type PaintingParams = {
|
||||
files: FileMetadata[]
|
||||
}
|
||||
|
||||
export type PaintingProvider = 'aihubmix' | 'silicon' | 'dmxapi' | 'new-api'
|
||||
export type PaintingProvider = 'zhipu' | 'aihubmix' | 'silicon' | 'dmxapi' | 'new-api'
|
||||
|
||||
export interface Painting extends PaintingParams {
|
||||
model?: string
|
||||
@ -500,13 +501,20 @@ export type PaintingAction = Partial<
|
||||
PaintingParams
|
||||
|
||||
export interface PaintingsState {
|
||||
paintings: Painting[]
|
||||
generate: Partial<GeneratePainting> & PaintingParams[]
|
||||
remix: Partial<RemixPainting> & PaintingParams[]
|
||||
edit: Partial<EditPainting> & PaintingParams[]
|
||||
upscale: Partial<ScalePainting> & PaintingParams[]
|
||||
DMXAPIPaintings: DmxapiPainting[]
|
||||
tokenFluxPaintings: TokenFluxPainting[]
|
||||
// SiliconFlow
|
||||
siliconflow_paintings: Painting[]
|
||||
// DMXAPI
|
||||
dmxapi_paintings: DmxapiPainting[]
|
||||
// TokenFlux
|
||||
tokenflux_paintings: TokenFluxPainting[]
|
||||
// Zhipu
|
||||
zhipu_paintings: Painting[]
|
||||
// Aihubmix
|
||||
aihubmix_image_generate: Partial<GeneratePainting> & PaintingParams[]
|
||||
aihubmix_image_remix: Partial<RemixPainting> & PaintingParams[]
|
||||
aihubmix_image_edit: Partial<EditPainting> & PaintingParams[]
|
||||
aihubmix_image_upscale: Partial<ScalePainting> & PaintingParams[]
|
||||
// OpenAI
|
||||
openai_image_generate: Partial<GeneratePainting> & PaintingParams[]
|
||||
openai_image_edit: Partial<EditPainting> & PaintingParams[]
|
||||
}
|
||||
@ -667,6 +675,7 @@ export type GenerateImageParams = {
|
||||
signal?: AbortSignal
|
||||
promptEnhancement?: boolean
|
||||
personGeneration?: PersonGeneration
|
||||
quality?: string
|
||||
}
|
||||
|
||||
export type GenerateImageResponse = {
|
||||
@ -735,6 +744,7 @@ export type ExternalToolResult = {
|
||||
}
|
||||
|
||||
export const WebSearchProviderIds = {
|
||||
zhipu: 'zhipu',
|
||||
tavily: 'tavily',
|
||||
searxng: 'searxng',
|
||||
exa: 'exa',
|
||||
|
||||
@ -64,5 +64,9 @@ export const getModelTags = (models: Model[]): Record<ModelTag, boolean> => {
|
||||
}
|
||||
|
||||
export function isFreeModel(model: Model) {
|
||||
if (model.provider === 'cherryin') {
|
||||
return true
|
||||
}
|
||||
|
||||
return (model.id + model.name).toLocaleLowerCase().includes('free')
|
||||
}
|
||||
|
||||
@ -6,7 +6,8 @@
|
||||
"local/src/renderer/**/*",
|
||||
"packages/shared/**/*",
|
||||
"tests/__mocks__/**/*",
|
||||
"packages/mcp-trace/**/*"
|
||||
"packages/mcp-trace/**/*",
|
||||
"src/main/integration/cherryin/index.js"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
|
||||