mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-31 08:29:07 +08:00
Merge branch 'main' of github.com:CherryHQ/cherry-studio into v2
This commit is contained in:
commit
d7eb88f7e2
4
.github/workflows/claude-translator.yml
vendored
4
.github/workflows/claude-translator.yml
vendored
@ -45,7 +45,7 @@ jobs:
|
||||
# See: https://github.com/anthropics/claude-code-action/blob/main/docs/security.md
|
||||
github_token: ${{ secrets.TOKEN_GITHUB_WRITE }}
|
||||
allowed_non_write_users: "*"
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
anthropic_api_key: ${{ secrets.CLAUDE_TRANSLATOR_APIKEY }}
|
||||
claude_args: "--allowed-tools Bash(gh issue:*),Bash(gh api:repos/*/issues:*),Bash(gh api:repos/*/pulls/*/reviews/*),Bash(gh api:repos/*/pulls/comments/*)"
|
||||
prompt: |
|
||||
你是一个多语言翻译助手。你需要响应 GitHub Webhooks 中的以下四种事件:
|
||||
@ -108,3 +108,5 @@ jobs:
|
||||
|
||||
使用以下命令获取完整信息:
|
||||
gh issue view ${{ github.event.issue.number }} --json title,body,comments
|
||||
env:
|
||||
ANTHROPIC_BASE_URL: ${{ secrets.CLAUDE_TRANSLATOR_BASEURL }}
|
||||
|
||||
@ -128,21 +128,113 @@ afterSign: scripts/notarize.js
|
||||
artifactBuildCompleted: scripts/artifact-build-completed.js
|
||||
releaseInfo:
|
||||
releaseNotes: |
|
||||
What's New in v1.6.3
|
||||
<!--LANG:en-->
|
||||
What's New in v1.7.0-beta.1
|
||||
|
||||
Features:
|
||||
- Notes: Add spell-check control, automatic table line wrapping, export functionality, and LLM-based renaming
|
||||
- UI: Expand topic rename clickable area, add middle-click tab closing, remove redundant scrollbars, fix message menubar overflow
|
||||
- Editor: Add read-only extension support, make TextFilePreview read-only but copyable
|
||||
- Models: Update support for DeepSeek v3.2, Claude 4.5, GLM 4.6, Gemini regex, and vision models
|
||||
- Code Tools: Add GitHub Copilot CLI integration
|
||||
Major Features:
|
||||
- Agent System: Introducing intelligent Agent capabilities alongside Assistants. Agents can autonomously solve complex problems using Claude Code SDK with tool calling, file operations, and multi-turn reasoning
|
||||
- Agent Management: Create, configure, and manage agents with custom settings including model selection, tool permissions, accessible paths, and MCP server integrations
|
||||
- Agent Sessions: Dedicated session management for agent interactions with persistent message history and context tracking
|
||||
- Unified UI: Streamlined interface combining Assistants and Agents tabs with improved navigation and settings management
|
||||
|
||||
Agent Features:
|
||||
- Tool Support: Web search, file operations, bash commands, and custom MCP tools
|
||||
- Advanced Configuration: Max turns, temperature, token limits
|
||||
- Permission Control: Configurable tool approval modes (manual, automatic, none)
|
||||
- Session Persistence: Automatic message saving with optimized streaming and database integration
|
||||
- Model Selection: API-based model filtering with provider-specific support
|
||||
|
||||
UI/UX Improvements:
|
||||
- Unified assistant/agent tabs with smooth animations
|
||||
- In-place session name editing
|
||||
- Virtual list rendering for improved performance
|
||||
- Session count indicators for active agents
|
||||
- Enhanced settings popup with tabbed interface
|
||||
- Webview keyboard shortcut interception for search functionality
|
||||
|
||||
API & Infrastructure:
|
||||
- RESTful API for agent and session management
|
||||
- Drizzle ORM integration for agent database
|
||||
- OAuth support for Claude Code authentication
|
||||
- Express validator for request validation
|
||||
- Comprehensive error handling with Zod schemas
|
||||
|
||||
Model Updates:
|
||||
- Gemini 2.5 Image Flash support
|
||||
- Grok 4 Fast with reasoning capabilities
|
||||
- Qwen3-omni and Qwen3-vl thinking models
|
||||
- DeepSeek, Claude 4.5, GLM 4.6 support
|
||||
- GitHub Copilot CLI integration with gpt-5-codex
|
||||
|
||||
Bug Fixes:
|
||||
- Fix Swagger UI accessibility issues
|
||||
- Fix AI SDK error display with syntax highlighting
|
||||
- Fix webview search shortcut handling
|
||||
- Fix agent model visibility for CherryIn provider
|
||||
- Fix session message ordering and persistence
|
||||
- Fix anthropic model visibility in agent configuration
|
||||
- Fix knowledge base deletion and web search RAG errors
|
||||
- Fix migration for missing providers
|
||||
- Fix forked topic retaining old name after rename
|
||||
- Restore first token latency reporting in metrics
|
||||
- Fix UI scrollbar and overflow issues
|
||||
|
||||
Technical Updates:
|
||||
- Upgrade to Electron 37.6.0
|
||||
- Update dependencies across packages
|
||||
- React 19.2.0 upgrade
|
||||
- Enhanced Claude Code service with streaming support
|
||||
- Improved message transformation and streaming lifecycle
|
||||
- Database migration system with automatic schema sync
|
||||
- Optimized bundle size and dependency management
|
||||
|
||||
<!--LANG:zh-CN-->
|
||||
v1.7.0-beta.1 新特性
|
||||
|
||||
核心功能:
|
||||
- Agent 系统:引入智能 Agent 能力,与助手(Assistant)并存。Agent 基于 Claude Code SDK 构建,具备工具调用、文件操作和多轮推理能力,可自主解决复杂问题
|
||||
- Agent 管理:创建、配置和管理 Agent,支持自定义模型选择、工具权限、可访问路径和 MCP 服务器集成
|
||||
- Agent 会话:专属会话管理系统,支持持久化消息历史和上下文追踪
|
||||
- 统一界面:精简的助手和 Agent 标签页界面,改进导航和设置管理体验
|
||||
|
||||
Agent 功能特性:
|
||||
- 工具支持:网页搜索、文件操作、Bash 命令执行和自定义 MCP 工具
|
||||
- 高级配置:最大轮次、温度、Token 限制
|
||||
- 权限控制:可配置的工具批准模式(手动、自动、无需批准)
|
||||
- 会话持久化:自动消息保存,优化的流式传输和数据库集成
|
||||
- 模型选择:基于 API 的模型过滤,支持特定提供商
|
||||
|
||||
界面与交互优化:
|
||||
- 统一的助手/Agent 标签页,带有流畅动画效果
|
||||
- 会话名称原地编辑功能
|
||||
- 虚拟列表渲染,提升性能表现
|
||||
- 活跃 Agent 的会话计数指示器
|
||||
- 增强的设置弹窗,采用标签页界面
|
||||
- Webview 键盘快捷键拦截,支持搜索功能
|
||||
|
||||
API 与基础设施:
|
||||
- RESTful API 用于 Agent 和会话管理
|
||||
- 集成 Drizzle ORM 管理 Agent 数据库
|
||||
- Claude Code OAuth 认证支持
|
||||
- Express validator 请求验证
|
||||
- 基于 Zod 模式的完善错误处理
|
||||
|
||||
模型更新:
|
||||
- 支持 Gemini 2.5 Image Flash
|
||||
- Grok 4 Fast 推理能力
|
||||
- Qwen3-omni 和 Qwen3-vl 思考模型
|
||||
- DeepSeek、Claude 4.5、GLM 4.6 支持
|
||||
- GitHub Copilot CLI 集成 gpt-5-codex
|
||||
|
||||
问题修复:
|
||||
- 修复 Swagger UI 无法打开
|
||||
- 修复 AI SDK 错误显示,添加语法高亮
|
||||
- 修复 Webview 搜索快捷键处理
|
||||
- 修复 CherryIn 提供商的 Agent 模型可见性
|
||||
- 修复会话消息排序和持久化
|
||||
- 修复 Anthropic 模型在 Agent 配置中的可见性
|
||||
- 修复知识库删除和网页搜索 RAG 错误
|
||||
- 修复缺失提供商的迁移问题
|
||||
|
||||
技术更新:
|
||||
- 升级至 React 19.2.0
|
||||
- 增强 Claude Code 服务流式传输支持
|
||||
- 改进消息转换和流式生命周期
|
||||
- 数据库迁移系统,支持自动模式同步
|
||||
- 优化打包大小和依赖管理
|
||||
<!--LANG:END-->
|
||||
|
||||
@ -86,6 +86,7 @@
|
||||
"@libsql/win32-x64-msvc": "^0.4.7",
|
||||
"@napi-rs/system-ocr": "patch:@napi-rs/system-ocr@npm%3A1.0.2#~/.yarn/patches/@napi-rs-system-ocr-npm-1.0.2-59e7a78e8b.patch",
|
||||
"@strongtz/win32-arm64-msvc": "^0.4.7",
|
||||
"express": "^5.1.0",
|
||||
"font-list": "^2.0.0",
|
||||
"graceful-fs": "^4.2.11",
|
||||
"jsdom": "26.1.0",
|
||||
@ -95,6 +96,7 @@
|
||||
"selection-hook": "^1.0.12",
|
||||
"sharp": "^0.34.3",
|
||||
"swagger-jsdoc": "^6.2.8",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"tesseract.js": "patch:tesseract.js@npm%3A6.0.1#~/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch",
|
||||
"turndown": "7.2.0"
|
||||
},
|
||||
@ -265,7 +267,6 @@
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"express": "^5.1.0",
|
||||
"express-validator": "^7.2.1",
|
||||
"fast-diff": "^1.3.0",
|
||||
"fast-xml-parser": "^5.2.0",
|
||||
@ -306,8 +307,8 @@
|
||||
"pdf-parse": "^1.1.1",
|
||||
"playwright": "^1.52.0",
|
||||
"proxy-agent": "^6.5.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-error-boundary": "^6.0.0",
|
||||
"react-hotkeys-hook": "^4.6.1",
|
||||
"react-i18next": "^14.1.2",
|
||||
@ -339,7 +340,6 @@
|
||||
"string-width": "^7.2.0",
|
||||
"striptags": "^3.2.0",
|
||||
"styled-components": "^6.1.11",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"swr": "^2.3.6",
|
||||
"tailwindcss": "^4.1.13",
|
||||
"tar": "^7.4.3",
|
||||
|
||||
@ -349,6 +349,7 @@ export enum IpcChannel {
|
||||
ApiServer_Stop = 'api-server:stop',
|
||||
ApiServer_Restart = 'api-server:restart',
|
||||
ApiServer_GetStatus = 'api-server:get-status',
|
||||
// NOTE: This api is not be used.
|
||||
ApiServer_GetConfig = 'api-server:get-config',
|
||||
|
||||
// Anthropic OAuth
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { isEmpty } from 'lodash'
|
||||
|
||||
import type { ApiModel, ApiModelsFilter, ApiModelsResponse } from '../../../renderer/src/types/apiModels'
|
||||
import { loggerService } from '../../services/LoggerService'
|
||||
import { getAvailableProviders, listAllAvailableModels, transformModelToOpenAI } from '../utils'
|
||||
@ -8,6 +10,10 @@ const logger = loggerService.withContext('ModelsService')
|
||||
|
||||
export type ModelsFilter = ApiModelsFilter
|
||||
|
||||
const isAnthropicProvider = (provider: { type: string; anthropicApiHost?: string }) => {
|
||||
return provider.type === 'anthropic' || !isEmpty(provider.anthropicApiHost?.trim())
|
||||
}
|
||||
|
||||
export class ModelsService {
|
||||
async getModels(filter: ModelsFilter): Promise<ApiModelsResponse> {
|
||||
try {
|
||||
@ -16,9 +22,7 @@ export class ModelsService {
|
||||
let providers = await getAvailableProviders()
|
||||
|
||||
if (filter.providerType === 'anthropic') {
|
||||
providers = providers.filter(
|
||||
(p) => p.type === 'anthropic' || (p.anthropicApiHost !== undefined && p.anthropicApiHost.trim() !== '')
|
||||
)
|
||||
providers = providers.filter(isAnthropicProvider)
|
||||
}
|
||||
|
||||
const models = await listAllAvailableModels(providers)
|
||||
@ -41,6 +45,10 @@ export class ModelsService {
|
||||
continue
|
||||
}
|
||||
|
||||
if (filter.supportAnthropic && model.endpoint_type !== 'anthropic' && !isAnthropicProvider(provider)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const openAIModel = transformModelToOpenAI(model, provider)
|
||||
const fullModelId = openAIModel.id // This is already in format "provider:model_id"
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { cacheService } from '@data/CacheService'
|
||||
import { loggerService } from '@logger'
|
||||
import { reduxService } from '@main/services/ReduxService'
|
||||
import type { ApiModel, Model, Provider } from '@types'
|
||||
import type { ApiModel, EndpointType, Model, Provider } from '@types'
|
||||
|
||||
const logger = loggerService.withContext('ApiServerUtils')
|
||||
|
||||
@ -114,6 +114,7 @@ export async function validateModelId(model: string): Promise<{
|
||||
error?: ModelValidationError
|
||||
provider?: Provider
|
||||
modelId?: string
|
||||
modelEndpointType?: EndpointType
|
||||
}> {
|
||||
try {
|
||||
if (!model || typeof model !== 'string') {
|
||||
@ -166,7 +167,8 @@ export async function validateModelId(model: string): Promise<{
|
||||
}
|
||||
|
||||
// Check if model exists in provider
|
||||
const modelExists = provider.models?.some((m) => m.id === modelId)
|
||||
const modelInProvider = provider.models?.find((m) => m.id === modelId)
|
||||
const modelExists = !!modelInProvider
|
||||
if (!modelExists) {
|
||||
const availableModels = provider.models?.map((m) => m.id).join(', ') || 'none'
|
||||
return {
|
||||
@ -179,10 +181,13 @@ export async function validateModelId(model: string): Promise<{
|
||||
}
|
||||
}
|
||||
|
||||
const modelEndpointType = modelInProvider?.endpoint_type
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
provider,
|
||||
modelId
|
||||
modelId,
|
||||
modelEndpointType
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error('Error validating model ID', { error, model })
|
||||
|
||||
@ -277,7 +277,7 @@ export default class MineruPreprocessProvider extends BasePreprocessProvider {
|
||||
|
||||
const response = await net.fetch(uploadUrl, {
|
||||
method: 'PUT',
|
||||
body: fileBuffer,
|
||||
body: fileBuffer as unknown as BodyInit,
|
||||
headers: {
|
||||
'Content-Type': 'application/pdf'
|
||||
}
|
||||
|
||||
@ -1,5 +1,11 @@
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import type { ApiServerConfig } from '@types'
|
||||
import type {
|
||||
ApiServerConfig,
|
||||
GetApiServerStatusResult,
|
||||
RestartApiServerStatusResult,
|
||||
StartApiServerStatusResult,
|
||||
StopApiServerStatusResult
|
||||
} from '@types'
|
||||
import { ipcMain } from 'electron'
|
||||
|
||||
import { apiServer } from '../apiServer'
|
||||
@ -52,7 +58,7 @@ export class ApiServerService {
|
||||
|
||||
registerIpcHandlers(): void {
|
||||
// API Server
|
||||
ipcMain.handle(IpcChannel.ApiServer_Start, async () => {
|
||||
ipcMain.handle(IpcChannel.ApiServer_Start, async (): Promise<StartApiServerStatusResult> => {
|
||||
try {
|
||||
await this.start()
|
||||
return { success: true }
|
||||
@ -61,7 +67,7 @@ export class ApiServerService {
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.ApiServer_Stop, async () => {
|
||||
ipcMain.handle(IpcChannel.ApiServer_Stop, async (): Promise<StopApiServerStatusResult> => {
|
||||
try {
|
||||
await this.stop()
|
||||
return { success: true }
|
||||
@ -70,7 +76,7 @@ export class ApiServerService {
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.ApiServer_Restart, async () => {
|
||||
ipcMain.handle(IpcChannel.ApiServer_Restart, async (): Promise<RestartApiServerStatusResult> => {
|
||||
try {
|
||||
await this.restart()
|
||||
return { success: true }
|
||||
@ -79,7 +85,7 @@ export class ApiServerService {
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.ApiServer_GetStatus, async () => {
|
||||
ipcMain.handle(IpcChannel.ApiServer_GetStatus, async (): Promise<GetApiServerStatusResult> => {
|
||||
try {
|
||||
const config = await this.getCurrentConfig()
|
||||
return {
|
||||
|
||||
@ -9,6 +9,7 @@ import { config as apiConfigService } from '@main/apiServer/config'
|
||||
import { validateModelId } from '@main/apiServer/utils'
|
||||
import getLoginShellEnvironment from '@main/utils/shell-env'
|
||||
import { app } from 'electron'
|
||||
import { isEmpty } from 'lodash'
|
||||
|
||||
import type { GetAgentSessionResponse } from '../..'
|
||||
import type { AgentServiceInterface, AgentStream, AgentStreamEvent } from '../../interfaces/AgentStreamInterface'
|
||||
@ -61,11 +62,20 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
})
|
||||
return aiStream
|
||||
}
|
||||
if (
|
||||
(modelInfo.provider?.type !== 'anthropic' &&
|
||||
(modelInfo.provider?.anthropicApiHost === undefined || modelInfo.provider.anthropicApiHost.trim() === '')) ||
|
||||
modelInfo.provider.apiKey === ''
|
||||
) {
|
||||
|
||||
const validateModelInfo: (m: typeof modelInfo) => boolean = (m) => {
|
||||
const { provider, modelEndpointType } = m
|
||||
if (!provider) return false
|
||||
if (isEmpty(provider.apiKey?.trim())) return false
|
||||
|
||||
const isAnthropicType = provider.type === 'anthropic'
|
||||
const isAnthropicEndpoint = modelEndpointType === 'anthropic'
|
||||
const hasValidApiHost = !isEmpty(provider.anthropicApiHost?.trim())
|
||||
|
||||
return !(!isAnthropicType && !isAnthropicEndpoint && !hasValidApiHost)
|
||||
}
|
||||
|
||||
if (!modelInfo.provider || !validateModelInfo(modelInfo)) {
|
||||
logger.error('Anthropic provider configuration is missing', {
|
||||
modelInfo
|
||||
})
|
||||
|
||||
@ -19,6 +19,7 @@ import type {
|
||||
FileListResponse,
|
||||
FileMetadata,
|
||||
FileUploadResponse,
|
||||
GetApiServerStatusResult,
|
||||
KnowledgeBaseParams,
|
||||
KnowledgeItem,
|
||||
KnowledgeSearchResult,
|
||||
@ -29,8 +30,11 @@ import type {
|
||||
OcrProvider,
|
||||
OcrResult,
|
||||
Provider,
|
||||
RestartApiServerStatusResult,
|
||||
S3Config,
|
||||
Shortcut,
|
||||
StartApiServerStatusResult,
|
||||
StopApiServerStatusResult,
|
||||
SupportedOcrFile,
|
||||
WebDavConfig
|
||||
} from '@types'
|
||||
@ -537,6 +541,12 @@ const api = {
|
||||
ipcRenderer.on(channel, listener)
|
||||
return () => ipcRenderer.off(channel, listener)
|
||||
}
|
||||
},
|
||||
apiServer: {
|
||||
getStatus: (): Promise<GetApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_GetStatus),
|
||||
start: (): Promise<StartApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_Start),
|
||||
restart: (): Promise<RestartApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_Restart),
|
||||
stop: (): Promise<StopApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_Stop)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4,10 +4,11 @@
|
||||
*/
|
||||
|
||||
import { loggerService } from '@logger'
|
||||
import type { AISDKWebSearchResult, MCPTool, WebSearchResults } from '@renderer/types'
|
||||
import { WebSearchSource } from '@renderer/types'
|
||||
import type { Chunk } from '@renderer/types/chunk'
|
||||
import { ChunkType } from '@renderer/types/chunk'
|
||||
import type { AISDKWebSearchResult, MCPTool, WebSearchResults } from '@types'
|
||||
import { WebSearchSource } from '@types'
|
||||
import { type Chunk, ChunkType } from '@renderer/types/chunk'
|
||||
import { ProviderSpecificError } from '@renderer/types/provider-specific-error'
|
||||
import { formatErrorMessage } from '@renderer/utils/error'
|
||||
import { convertLinks, flushLinkConverterBuffer } from '@renderer/utils/linkConverter'
|
||||
import type { ClaudeCodeRawValue } from '@shared/agents/claudecode/types'
|
||||
import type { TextStreamPart, ToolSet } from 'ai'
|
||||
@ -357,7 +358,11 @@ export class AiSdkToChunkAdapter {
|
||||
case 'error':
|
||||
this.onChunk({
|
||||
type: ChunkType.ERROR,
|
||||
error: chunk.error as Record<string, any>
|
||||
error: new ProviderSpecificError({
|
||||
message: formatErrorMessage(chunk.error),
|
||||
provider: 'unknown',
|
||||
cause: chunk.error
|
||||
})
|
||||
})
|
||||
break
|
||||
|
||||
|
||||
@ -50,7 +50,11 @@ export const ImageGenerationMiddleware: CompletionsMiddleware =
|
||||
if (!block.file) return null
|
||||
const binaryData: Uint8Array = await FileManager.readBinaryImage(block.file)
|
||||
const mimeType = `${block.file.type}/${block.file.ext.slice(1)}`
|
||||
return await toFile(new Blob([binaryData]), block.file.origin_name || 'image.png', { type: mimeType })
|
||||
return await toFile(
|
||||
new Blob([binaryData as unknown as BlobPart]),
|
||||
block.file.origin_name || 'image.png',
|
||||
{ type: mimeType }
|
||||
)
|
||||
})
|
||||
)
|
||||
imageFiles = imageFiles.concat(userImages.filter(Boolean) as Blob[])
|
||||
|
||||
@ -5,6 +5,8 @@ import type { Chunk } from '@renderer/types/chunk'
|
||||
import type { LanguageModelMiddleware } from 'ai'
|
||||
import { extractReasoningMiddleware, simulateStreamingMiddleware } from 'ai'
|
||||
|
||||
import { noThinkMiddleware } from './noThinkMiddleware'
|
||||
|
||||
const logger = loggerService.withContext('AiSdkMiddlewareBuilder')
|
||||
|
||||
/**
|
||||
@ -187,6 +189,14 @@ function addProviderSpecificMiddlewares(builder: AiSdkMiddlewareBuilder, config:
|
||||
// 其他provider的通用处理
|
||||
break
|
||||
}
|
||||
|
||||
// OVMS+MCP's specific middleware
|
||||
if (config.provider.id === 'ovms' && config.mcpTools && config.mcpTools.length > 0) {
|
||||
builder.add({
|
||||
name: 'no-think',
|
||||
middleware: noThinkMiddleware()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
52
src/renderer/src/aiCore/middleware/noThinkMiddleware.ts
Normal file
52
src/renderer/src/aiCore/middleware/noThinkMiddleware.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { loggerService } from '@logger'
|
||||
import type { LanguageModelMiddleware } from 'ai'
|
||||
|
||||
const logger = loggerService.withContext('noThinkMiddleware')
|
||||
|
||||
/**
|
||||
* No Think Middleware
|
||||
* Automatically appends ' /no_think' string to the end of user messages for the provider
|
||||
* This prevents the model from generating unnecessary thinking process and returns results directly
|
||||
* @returns LanguageModelMiddleware
|
||||
*/
|
||||
export function noThinkMiddleware(): LanguageModelMiddleware {
|
||||
return {
|
||||
middlewareVersion: 'v2',
|
||||
|
||||
transformParams: async ({ params }) => {
|
||||
const transformedParams = { ...params }
|
||||
// Process messages in prompt
|
||||
if (transformedParams.prompt && Array.isArray(transformedParams.prompt)) {
|
||||
transformedParams.prompt = transformedParams.prompt.map((message) => {
|
||||
// Only process user messages
|
||||
if (message.role === 'user') {
|
||||
// Process content array
|
||||
if (Array.isArray(message.content)) {
|
||||
const lastContent = message.content[message.content.length - 1]
|
||||
// If the last content is text type, append ' /no_think'
|
||||
if (lastContent && lastContent.type === 'text' && typeof lastContent.text === 'string') {
|
||||
// Avoid duplicate additions
|
||||
if (!lastContent.text.endsWith('/no_think')) {
|
||||
logger.debug('Adding /no_think to user message')
|
||||
return {
|
||||
...message,
|
||||
content: [
|
||||
...message.content.slice(0, -1),
|
||||
{
|
||||
...lastContent,
|
||||
text: lastContent.text + ' /no_think'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return message
|
||||
})
|
||||
}
|
||||
|
||||
return transformedParams
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -63,13 +63,14 @@ function handleSpecialProviders(model: Model, provider: Provider): Provider {
|
||||
// return createVertexProvider(provider)
|
||||
// }
|
||||
|
||||
if (isNewApiProvider(provider)) {
|
||||
return newApiResolverCreator(model, provider)
|
||||
}
|
||||
|
||||
if (isSystemProvider(provider)) {
|
||||
if (provider.id === 'aihubmix') {
|
||||
return aihubmixProviderCreator(model, provider)
|
||||
}
|
||||
if (isNewApiProvider(provider)) {
|
||||
return newApiResolverCreator(model, provider)
|
||||
}
|
||||
if (provider.id === 'vertexai') {
|
||||
return vertexAnthropicProviderCreator(model, provider)
|
||||
}
|
||||
|
||||
@ -39,7 +39,7 @@ const ImageViewer: React.FC<ImageViewerProps> = ({ src, style, ...props }) => {
|
||||
if (!match) throw new Error('Invalid base64 image format')
|
||||
const mimeType = match[1]
|
||||
const byteArray = Base64.toUint8Array(match[2])
|
||||
const blob = new Blob([byteArray], { type: mimeType })
|
||||
const blob = new Blob([byteArray as unknown as BlobPart], { type: mimeType })
|
||||
await navigator.clipboard.write([new ClipboardItem({ [mimeType]: blob })])
|
||||
} else if (src.startsWith('file://')) {
|
||||
// 处理本地文件路径
|
||||
|
||||
@ -101,7 +101,7 @@ export const AgentModal: React.FC<Props> = ({ agent, trigger, isOpen: _isOpen, o
|
||||
const { addAgent } = useAgents()
|
||||
const { updateAgent } = useUpdateAgent()
|
||||
// hard-coded. We only support anthropic for now.
|
||||
const { models } = useApiModels({ providerType: 'anthropic' })
|
||||
const { models } = useApiModels({ supportAnthropic: true })
|
||||
const isEditing = (agent?: AgentWithTools) => agent !== undefined
|
||||
|
||||
const [form, setForm] = useState<BaseAgentForm>(() => buildAgentForm(agent))
|
||||
|
||||
@ -21,7 +21,7 @@ export const CodeBlockShiki = CodeBlock.extend<CodeBlockShikiOptions>({
|
||||
HTMLAttributes: {
|
||||
class: 'code-block-shiki'
|
||||
}
|
||||
}
|
||||
} as CodeBlockShikiOptions
|
||||
},
|
||||
|
||||
addInputRules() {
|
||||
|
||||
@ -281,7 +281,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
|
||||
'new-api': {
|
||||
id: 'new-api',
|
||||
name: 'New API',
|
||||
type: 'openai',
|
||||
type: 'new-api',
|
||||
apiKey: '',
|
||||
apiHost: 'http://localhost:3000',
|
||||
anthropicApiHost: 'http://localhost:3000',
|
||||
@ -1424,5 +1424,5 @@ export const isGeminiWebSearchProvider = (provider: Provider) => {
|
||||
}
|
||||
|
||||
export const isNewApiProvider = (provider: Provider) => {
|
||||
return ['new-api', 'cherryin'].includes(provider.id)
|
||||
return ['new-api', 'cherryin'].includes(provider.id) || provider.type === 'new-api'
|
||||
}
|
||||
|
||||
@ -1,18 +1,28 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useSWR from 'swr'
|
||||
|
||||
import { useApiServer } from '../useApiServer'
|
||||
import { useAgentClient } from './useAgentClient'
|
||||
|
||||
export const useAgent = (id: string | null) => {
|
||||
const { t } = useTranslation()
|
||||
const client = useAgentClient()
|
||||
const key = id ? client.agentPaths.withId(id) : null
|
||||
const { apiServerConfig, apiServerRunning } = useApiServer()
|
||||
const fetcher = useCallback(async () => {
|
||||
if (!id || id === 'fake') {
|
||||
return null
|
||||
}
|
||||
if (!apiServerConfig.enabled) {
|
||||
throw new Error(t('apiServer.messages.notEnabled'))
|
||||
}
|
||||
if (!apiServerRunning) {
|
||||
throw new Error(t('agent.server.error.not_running'))
|
||||
}
|
||||
const result = await client.getAgent(id)
|
||||
return result
|
||||
}, [client, id])
|
||||
}, [apiServerConfig.enabled, apiServerRunning, client, id, t])
|
||||
const { data, error, isLoading } = useSWR(key, id ? fetcher : null)
|
||||
|
||||
return {
|
||||
|
||||
@ -6,6 +6,7 @@ import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useSWR from 'swr'
|
||||
|
||||
import { useApiServer } from '../useApiServer'
|
||||
import { useRuntime } from '../useRuntime'
|
||||
import { useAgentClient } from './useAgentClient'
|
||||
|
||||
@ -23,11 +24,18 @@ export const useAgents = () => {
|
||||
const { t } = useTranslation()
|
||||
const client = useAgentClient()
|
||||
const key = client.agentPaths.base
|
||||
const { apiServerConfig, apiServerRunning } = useApiServer()
|
||||
const fetcher = useCallback(async () => {
|
||||
if (!apiServerConfig.enabled) {
|
||||
throw new Error(t('apiServer.messages.notEnabled'))
|
||||
}
|
||||
if (!apiServerRunning) {
|
||||
throw new Error(t('agent.server.error.not_running'))
|
||||
}
|
||||
const result = await client.listAgents()
|
||||
// NOTE: We only use the array for now. useUpdateAgent depends on this behavior.
|
||||
return result.data
|
||||
}, [client])
|
||||
}, [apiServerConfig.enabled, apiServerRunning, client, t])
|
||||
const { data, error, isLoading, mutate } = useSWR(key, fetcher)
|
||||
const { chat } = useRuntime()
|
||||
const { activeAgentId } = chat
|
||||
|
||||
@ -21,7 +21,7 @@ export const useSessions = (agentId: string) => {
|
||||
async (form: CreateSessionForm) => {
|
||||
try {
|
||||
const result = await client.createSession(agentId, form)
|
||||
await mutate((prev) => [...(prev ?? []), result], { revalidate: false })
|
||||
await mutate((prev) => [result, ...(prev ?? [])], { revalidate: false })
|
||||
return result
|
||||
} catch (error) {
|
||||
window.toast.error(formatErrorMessageWithPrefix(error, t('agent.session.create.error.failed')))
|
||||
|
||||
112
src/renderer/src/hooks/useApiServer.ts
Normal file
112
src/renderer/src/hooks/useApiServer.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { setApiServerEnabled as setApiServerEnabledAction } from '@renderer/store/settings'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const logger = loggerService.withContext('useApiServer')
|
||||
|
||||
export const useApiServer = () => {
|
||||
const { t } = useTranslation()
|
||||
// FIXME: We currently store two copies of the config data in both the renderer and the main processes,
|
||||
// which carries the risk of data inconsistency. This should be modified so that the main process stores
|
||||
// the data, and the renderer retrieves it.
|
||||
const apiServerConfig = useAppSelector((state) => state.settings.apiServer)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
// Optimistic initial state.
|
||||
const [apiServerRunning, setApiServerRunning] = useState(apiServerConfig.enabled)
|
||||
const [apiServerLoading, setApiServerLoading] = useState(true)
|
||||
|
||||
const setApiServerEnabled = useCallback(
|
||||
(enabled: boolean) => {
|
||||
dispatch(setApiServerEnabledAction(enabled))
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
// API Server functions
|
||||
const checkApiServerStatus = useCallback(async () => {
|
||||
setApiServerLoading(true)
|
||||
try {
|
||||
const status = await window.api.apiServer.getStatus()
|
||||
setApiServerRunning(status.running)
|
||||
} catch (error: any) {
|
||||
logger.error('Failed to check API server status:', error)
|
||||
} finally {
|
||||
setApiServerLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const startApiServer = useCallback(async () => {
|
||||
if (apiServerLoading) return
|
||||
|
||||
setApiServerLoading(true)
|
||||
try {
|
||||
const result = await window.api.apiServer.start()
|
||||
if (result.success) {
|
||||
setApiServerRunning(true)
|
||||
window.toast.success(t('apiServer.messages.startSuccess'))
|
||||
} else {
|
||||
window.toast.error(t('apiServer.messages.startError') + result.error)
|
||||
}
|
||||
} catch (error: any) {
|
||||
window.toast.error(t('apiServer.messages.startError') + (error.message || error))
|
||||
} finally {
|
||||
setApiServerLoading(false)
|
||||
}
|
||||
}, [apiServerLoading, t])
|
||||
|
||||
const stopApiServer = useCallback(async () => {
|
||||
if (apiServerLoading) return
|
||||
|
||||
setApiServerLoading(true)
|
||||
try {
|
||||
const result = await window.api.apiServer.stop()
|
||||
if (result.success) {
|
||||
setApiServerRunning(false)
|
||||
window.toast.success(t('apiServer.messages.stopSuccess'))
|
||||
} else {
|
||||
window.toast.error(t('apiServer.messages.stopError') + result.error)
|
||||
}
|
||||
} catch (error: any) {
|
||||
window.toast.error(t('apiServer.messages.stopError') + (error.message || error))
|
||||
} finally {
|
||||
setApiServerLoading(false)
|
||||
}
|
||||
}, [apiServerLoading, t])
|
||||
|
||||
const restartApiServer = useCallback(async () => {
|
||||
if (apiServerLoading) return
|
||||
|
||||
setApiServerLoading(true)
|
||||
try {
|
||||
const result = await window.api.apiServer.restart()
|
||||
if (result.success) {
|
||||
await checkApiServerStatus()
|
||||
window.toast.success(t('apiServer.messages.restartSuccess'))
|
||||
} else {
|
||||
window.toast.error(t('apiServer.messages.restartError') + result.error)
|
||||
}
|
||||
} catch (error) {
|
||||
window.toast.error(t('apiServer.messages.restartFailed') + (error as Error).message)
|
||||
} finally {
|
||||
setApiServerLoading(false)
|
||||
}
|
||||
}, [apiServerLoading, checkApiServerStatus, t])
|
||||
|
||||
useEffect(() => {
|
||||
checkApiServerStatus()
|
||||
}, [checkApiServerStatus])
|
||||
|
||||
return {
|
||||
apiServerConfig,
|
||||
apiServerRunning,
|
||||
apiServerLoading,
|
||||
startApiServer,
|
||||
stopApiServer,
|
||||
restartApiServer,
|
||||
checkApiServerStatus,
|
||||
setApiServerEnabled
|
||||
}
|
||||
}
|
||||
@ -30,6 +30,11 @@
|
||||
"failed": "Failed to list agents."
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"error": {
|
||||
"not_running": "The API server is enabled but not running properly."
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
"accessible_paths": {
|
||||
"add": "Add directory",
|
||||
@ -237,6 +242,7 @@
|
||||
"messages": {
|
||||
"apiKeyCopied": "API Key copied to clipboard",
|
||||
"apiKeyRegenerated": "API Key regenerated",
|
||||
"notEnabled": "The API Server is not enabled.",
|
||||
"operationFailed": "API Server operation failed: ",
|
||||
"restartError": "Failed to restart API Server: ",
|
||||
"restartFailed": "API Server restart failed: ",
|
||||
|
||||
@ -30,6 +30,11 @@
|
||||
"failed": "获取智能体列表失败"
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"error": {
|
||||
"not_running": "API 服务器已启用但未正常运行。"
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
"accessible_paths": {
|
||||
"add": "添加目录",
|
||||
@ -237,6 +242,7 @@
|
||||
"messages": {
|
||||
"apiKeyCopied": "API 密钥已复制到剪贴板",
|
||||
"apiKeyRegenerated": "API 密钥已重新生成",
|
||||
"notEnabled": "API 服务器未启用。",
|
||||
"operationFailed": "API 服务器操作失败:",
|
||||
"restartError": "重启 API 服务器失败:",
|
||||
"restartFailed": "API 服务器重启失败:",
|
||||
|
||||
@ -30,6 +30,11 @@
|
||||
"failed": "無法列出代理程式。"
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"error": {
|
||||
"not_running": "API 伺服器已啟用,但運行不正常。"
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
"accessible_paths": {
|
||||
"add": "新增目錄",
|
||||
@ -237,6 +242,7 @@
|
||||
"messages": {
|
||||
"apiKeyCopied": "API 金鑰已複製到剪貼簿",
|
||||
"apiKeyRegenerated": "API 金鑰已重新生成",
|
||||
"notEnabled": "API 伺服器未啟用。",
|
||||
"operationFailed": "API 伺服器操作失敗:",
|
||||
"restartError": "重新啟動 API 伺服器失敗:",
|
||||
"restartFailed": "API 伺服器重新啟動失敗:",
|
||||
|
||||
@ -30,6 +30,11 @@
|
||||
"failed": "Αποτυχία καταχώρησης πρακτόρων."
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"error": {
|
||||
"not_running": "Ο διακομιστής API είναι ενεργοποιημένος αλλά δεν λειτουργεί σωστά."
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
"accessible_paths": {
|
||||
"add": "Προσθήκη καταλόγου",
|
||||
@ -237,6 +242,7 @@
|
||||
"messages": {
|
||||
"apiKeyCopied": "Το κλειδί API αντιγράφηκε στο πρόχειρο",
|
||||
"apiKeyRegenerated": "Το κλειδί API αναδημιουργήθηκε",
|
||||
"notEnabled": "Ο διακομιστής API δεν είναι ενεργοποιημένος.",
|
||||
"operationFailed": "Η λειτουργία του Διακομιστή API απέτυχε: ",
|
||||
"restartError": "Αποτυχία επανεκκίνησης του Διακομιστή API: ",
|
||||
"restartFailed": "Η επανεκκίνηση του Διακομιστή API απέτυχε: ",
|
||||
|
||||
@ -30,6 +30,11 @@
|
||||
"failed": "Error al listar agentes."
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"error": {
|
||||
"not_running": "El servidor de API está habilitado pero no funciona correctamente."
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
"accessible_paths": {
|
||||
"add": "Agregar directorio",
|
||||
@ -237,6 +242,7 @@
|
||||
"messages": {
|
||||
"apiKeyCopied": "Clave API copiada al portapapeles",
|
||||
"apiKeyRegenerated": "Clave API regenerada",
|
||||
"notEnabled": "El servidor de API no está habilitado.",
|
||||
"operationFailed": "Falló la operación del Servidor API: ",
|
||||
"restartError": "Error al reiniciar el Servidor API: ",
|
||||
"restartFailed": "Falló el reinicio del Servidor API: ",
|
||||
|
||||
@ -30,6 +30,11 @@
|
||||
"failed": "Échec de la liste des agents."
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"error": {
|
||||
"not_running": "Le serveur API est activé mais ne fonctionne pas correctement."
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
"accessible_paths": {
|
||||
"add": "Ajouter un répertoire",
|
||||
@ -237,6 +242,7 @@
|
||||
"messages": {
|
||||
"apiKeyCopied": "Clé API copiée dans le presse-papiers",
|
||||
"apiKeyRegenerated": "Clé API régénérée",
|
||||
"notEnabled": "Le serveur API n'est pas activé.",
|
||||
"operationFailed": "Opération du Serveur API échouée : ",
|
||||
"restartError": "Échec du redémarrage du Serveur API : ",
|
||||
"restartFailed": "Redémarrage du Serveur API échoué : ",
|
||||
|
||||
@ -30,6 +30,11 @@
|
||||
"failed": "エージェントの一覧取得に失敗しました。"
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"error": {
|
||||
"not_running": "APIサーバーは有効になっていますが、正常に動作していません。"
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
"accessible_paths": {
|
||||
"add": "ディレクトリを追加",
|
||||
@ -237,6 +242,7 @@
|
||||
"messages": {
|
||||
"apiKeyCopied": "API キーがクリップボードにコピーされました",
|
||||
"apiKeyRegenerated": "API キーが再生成されました",
|
||||
"notEnabled": "APIサーバーが有効になっていません。",
|
||||
"operationFailed": "API サーバーの操作に失敗しました:",
|
||||
"restartError": "API サーバーの再起動に失敗しました:",
|
||||
"restartFailed": "API サーバーの再起動に失敗しました:",
|
||||
|
||||
@ -30,6 +30,11 @@
|
||||
"failed": "Falha ao listar agentes."
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"error": {
|
||||
"not_running": "O servidor de API está habilitado, mas não está funcionando corretamente."
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
"accessible_paths": {
|
||||
"add": "Adicionar diretório",
|
||||
@ -237,6 +242,7 @@
|
||||
"messages": {
|
||||
"apiKeyCopied": "Chave API copiada para a área de transferência",
|
||||
"apiKeyRegenerated": "Chave API regenerada",
|
||||
"notEnabled": "O Servidor de API não está habilitado.",
|
||||
"operationFailed": "Operação do Servidor API falhou: ",
|
||||
"restartError": "Falha ao reiniciar o Servidor API: ",
|
||||
"restartFailed": "Reinício do Servidor API falhou: ",
|
||||
|
||||
@ -30,6 +30,11 @@
|
||||
"failed": "Не удалось получить список агентов."
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"error": {
|
||||
"not_running": "API-сервер включен, но работает неправильно."
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
"accessible_paths": {
|
||||
"add": "Добавить каталог",
|
||||
@ -237,6 +242,7 @@
|
||||
"messages": {
|
||||
"apiKeyCopied": "API ключ скопирован в буфер обмена",
|
||||
"apiKeyRegenerated": "API ключ перегенерирован",
|
||||
"notEnabled": "API-сервер не включен.",
|
||||
"operationFailed": "Операция API сервера не удалась: ",
|
||||
"restartError": "Не удалось перезапустить API сервер: ",
|
||||
"restartFailed": "Перезапуск API сервера не удался: ",
|
||||
|
||||
@ -263,7 +263,9 @@ const AgentSessionInputbar: FC<Props> = ({ agentId, sessionId }) => {
|
||||
value={text}
|
||||
onChange={onChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={t('chat.input.placeholder', { key: getSendMessageShortcutLabel(sendMessageShortcut) })}
|
||||
placeholder={t('chat.input.placeholder_without_triggers', {
|
||||
key: getSendMessageShortcutLabel(sendMessageShortcut)
|
||||
})}
|
||||
autoFocus
|
||||
variant="borderless"
|
||||
spellCheck={enableSpellCheck}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Button } from '@heroui/button'
|
||||
import CodeViewer from '@renderer/components/CodeViewer'
|
||||
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { getHttpMessageLabel, getProviderLabel } from '@renderer/i18n/label'
|
||||
import { getProviderById } from '@renderer/services/ProviderService'
|
||||
@ -33,7 +34,7 @@ import {
|
||||
import type { ErrorMessageBlock, Message } from '@renderer/types/newMessage'
|
||||
import { formatAiSdkError, formatError, safeToString } from '@renderer/utils/error'
|
||||
import { Alert as AntdAlert, Modal } from 'antd'
|
||||
import React, { useState } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { Link } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
@ -303,14 +304,36 @@ const BuiltinError = ({ error }: { error: SerializedError }) => {
|
||||
// 作为 base,渲染公共字段,应当在 ErrorDetailList 中渲染
|
||||
const AiSdkErrorBase = ({ error }: { error: SerializedAiSdkError }) => {
|
||||
const { t } = useTranslation()
|
||||
const { highlightCode } = useCodeStyle()
|
||||
const [highlightedString, setHighlightedString] = useState('')
|
||||
const cause = error.cause
|
||||
|
||||
useEffect(() => {
|
||||
const highlight = async () => {
|
||||
try {
|
||||
const result = await highlightCode(JSON.stringify(JSON.parse(cause || '{}'), null, 2), 'json')
|
||||
setHighlightedString(result)
|
||||
} catch {
|
||||
setHighlightedString(cause || '')
|
||||
}
|
||||
}
|
||||
const timer = setTimeout(highlight, 0)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [highlightCode, cause])
|
||||
|
||||
return (
|
||||
<>
|
||||
<BuiltinError error={error} />
|
||||
{cause && (
|
||||
<ErrorDetailItem>
|
||||
<ErrorDetailLabel>{t('error.cause')}:</ErrorDetailLabel>
|
||||
<ErrorDetailValue>{error.cause}</ErrorDetailValue>
|
||||
<ErrorDetailValue>
|
||||
<div
|
||||
className="markdown [&_pre]:!bg-transparent [&_pre_span]:whitespace-pre-wrap"
|
||||
dangerouslySetInnerHTML={{ __html: highlightedString }}
|
||||
/>
|
||||
</ErrorDetailValue>
|
||||
</ErrorDetailItem>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -62,7 +62,7 @@ const MessageImage: FC<Props> = ({ block }) => {
|
||||
byteArrays.push(byteArray)
|
||||
}
|
||||
|
||||
const blob = new Blob(byteArrays, { type: mimeType })
|
||||
const blob = new Blob(byteArrays as unknown as BlobPart[], { type: mimeType })
|
||||
await navigator.clipboard.write([new ClipboardItem({ [mimeType]: blob })])
|
||||
} else {
|
||||
throw new Error('无效的 base64 图片格式')
|
||||
|
||||
@ -1,15 +1,16 @@
|
||||
import { Alert, Spinner } from '@heroui/react'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { useAgents } from '@renderer/hooks/agents/useAgents'
|
||||
import { useApiServer } from '@renderer/hooks/useApiServer'
|
||||
import { useAssistants } from '@renderer/hooks/useAssistant'
|
||||
import { useAssistantPresets } from '@renderer/hooks/useAssistantPresets'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useAssistantsTabSortType } from '@renderer/hooks/useStore'
|
||||
import { useTags } from '@renderer/hooks/useTags'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { addIknowAction } from '@renderer/store/runtime'
|
||||
import type { Assistant } from '@renderer/types'
|
||||
import type { Assistant } from '@types'
|
||||
import { getErrorMessage } from '@renderer/utils'
|
||||
import type { AssistantTabSortType } from '@shared/data/preference/preferenceTypes'
|
||||
import type { FC } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
@ -37,7 +38,8 @@ const AssistantsTab: FC<AssistantsTabProps> = (props) => {
|
||||
const { activeAssistant, setActiveAssistant, onCreateAssistant, onCreateDefaultAssistant } = props
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const { t } = useTranslation()
|
||||
const { apiServer } = useSettings()
|
||||
const { apiServerConfig, apiServerRunning } = useApiServer()
|
||||
const apiServerEnabled = apiServerConfig.enabled
|
||||
const { iknow, chat } = useRuntime()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
@ -57,7 +59,7 @@ const AssistantsTab: FC<AssistantsTabProps> = (props) => {
|
||||
const { unifiedItems, handleUnifiedListReorder } = useUnifiedItems({
|
||||
agents,
|
||||
assistants,
|
||||
apiServerEnabled: apiServer.enabled,
|
||||
apiServerEnabled,
|
||||
agentsLoading,
|
||||
agentsError,
|
||||
updateAssistants
|
||||
@ -74,17 +76,17 @@ const AssistantsTab: FC<AssistantsTabProps> = (props) => {
|
||||
unifiedItems,
|
||||
assistants,
|
||||
agents,
|
||||
apiServerEnabled: apiServer.enabled,
|
||||
apiServerEnabled,
|
||||
agentsLoading,
|
||||
agentsError,
|
||||
updateAssistants
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!agentsLoading && agents.length > 0 && !activeAgentId && apiServer.enabled) {
|
||||
if (!agentsLoading && agents.length > 0 && !activeAgentId && apiServerConfig.enabled) {
|
||||
setActiveAgentId(agents[0].id)
|
||||
}
|
||||
}, [agentsLoading, agents, activeAgentId, setActiveAgentId, apiServer.enabled])
|
||||
}, [agentsLoading, agents, activeAgentId, setActiveAgentId, apiServerConfig.enabled])
|
||||
|
||||
const onDeleteAssistant = useCallback(
|
||||
(assistant: Assistant) => {
|
||||
@ -107,7 +109,7 @@ const AssistantsTab: FC<AssistantsTabProps> = (props) => {
|
||||
|
||||
return (
|
||||
<Container className="assistants-tab" ref={containerRef}>
|
||||
{!apiServer.enabled && !iknow[ALERT_KEY] && (
|
||||
{!apiServerConfig.enabled && !iknow[ALERT_KEY] && (
|
||||
<Alert
|
||||
color="warning"
|
||||
title={t('agent.warning.enable_server')}
|
||||
@ -115,11 +117,22 @@ const AssistantsTab: FC<AssistantsTabProps> = (props) => {
|
||||
onClose={() => {
|
||||
dispatch(addIknowAction(ALERT_KEY))
|
||||
}}
|
||||
className="mb-2"
|
||||
/>
|
||||
)}
|
||||
|
||||
{agentsLoading && <Spinner />}
|
||||
{apiServer.enabled && agentsError && <Alert color="danger" title={t('agent.list.error.failed')} />}
|
||||
{apiServerConfig.enabled && !apiServerRunning && (
|
||||
<Alert color="danger" title={t('agent.server.error.not_running')} isClosable className="mb-2" />
|
||||
)}
|
||||
{apiServerConfig.enabled && apiServerRunning && agentsError && (
|
||||
<Alert
|
||||
color="danger"
|
||||
title={t('agent.list.error.failed')}
|
||||
description={getErrorMessage(agentsError)}
|
||||
className="mb-2"
|
||||
/>
|
||||
)}
|
||||
|
||||
{assistantsTabSortType === 'tags' ? (
|
||||
<UnifiedTagGroups
|
||||
|
||||
@ -297,13 +297,7 @@ export const Topics: React.FC<Props> = ({ assistant: _assistant, activeTopic, se
|
||||
label: t('chat.topics.clear.title'),
|
||||
key: 'clear-messages',
|
||||
icon: <BrushCleaning size={14} />,
|
||||
async onClick() {
|
||||
window.modal.confirm({
|
||||
title: t('chat.input.clear.content'),
|
||||
centered: true,
|
||||
onOk: () => onClearMessages(topic)
|
||||
})
|
||||
}
|
||||
onClick: () => onClearMessages(topic)
|
||||
},
|
||||
{
|
||||
label: t('settings.topic.position.label'),
|
||||
|
||||
@ -47,15 +47,58 @@ const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, app
|
||||
setActiveIndex(0)
|
||||
}, [])
|
||||
|
||||
const stopSearch = useCallback(() => {
|
||||
const target = webviewRef.current ?? attachedWebviewRef.current
|
||||
if (!target) return
|
||||
try {
|
||||
target.stopFindInPage('clearSelection')
|
||||
} catch (error) {
|
||||
logger.error('stopFindInPage failed', { error })
|
||||
const ensureWebviewReady = useCallback(
|
||||
(candidate: WebviewTag | null) => {
|
||||
if (!candidate) return null
|
||||
try {
|
||||
const webContentsId = candidate.getWebContentsId?.()
|
||||
if (!webContentsId) {
|
||||
logger.debug('WebviewSearch: missing webContentsId before action', { appId })
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
logger.debug('WebviewSearch: getWebContentsId failed before action', { appId, error })
|
||||
return null
|
||||
}
|
||||
|
||||
return candidate
|
||||
},
|
||||
[appId]
|
||||
)
|
||||
|
||||
const stopFindOnWebview = useCallback(
|
||||
(webview: WebviewTag | null) => {
|
||||
const usable = ensureWebviewReady(webview)
|
||||
if (!usable) return false
|
||||
try {
|
||||
usable.stopFindInPage('clearSelection')
|
||||
return true
|
||||
} catch (error) {
|
||||
logger.debug('stopFindInPage failed', { appId, error })
|
||||
return false
|
||||
}
|
||||
},
|
||||
[appId, ensureWebviewReady]
|
||||
)
|
||||
|
||||
const getUsableWebview = useCallback(() => {
|
||||
const candidates = [webviewRef.current, attachedWebviewRef.current]
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const usable = ensureWebviewReady(candidate)
|
||||
if (usable) {
|
||||
return usable
|
||||
}
|
||||
}
|
||||
}, [webviewRef])
|
||||
|
||||
return null
|
||||
}, [ensureWebviewReady, webviewRef])
|
||||
|
||||
const stopSearch = useCallback(() => {
|
||||
const target = getUsableWebview()
|
||||
if (!target) return
|
||||
stopFindOnWebview(target)
|
||||
}, [getUsableWebview, stopFindOnWebview])
|
||||
|
||||
const closeSearch = useCallback(() => {
|
||||
setIsVisible(false)
|
||||
@ -65,7 +108,7 @@ const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, app
|
||||
|
||||
const performSearch = useCallback(
|
||||
(text: string, options?: Electron.FindInPageOptions) => {
|
||||
const target = webviewRef.current ?? attachedWebviewRef.current
|
||||
const target = getUsableWebview()
|
||||
if (!target) {
|
||||
logger.debug('Skip performSearch: webview not attached')
|
||||
return
|
||||
@ -82,7 +125,7 @@ const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, app
|
||||
window.toast?.error(t('common.error'))
|
||||
}
|
||||
},
|
||||
[resetSearchState, stopSearch, t, webviewRef]
|
||||
[getUsableWebview, resetSearchState, stopSearch, t]
|
||||
)
|
||||
|
||||
const handleFoundInPage = useCallback((event: Event & { result?: FoundInPageResult }) => {
|
||||
@ -130,22 +173,26 @@ const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, app
|
||||
return () => {
|
||||
activeWebview.removeEventListener('found-in-page', handle)
|
||||
if (attachedWebviewRef.current === activeWebview) {
|
||||
try {
|
||||
activeWebview.stopFindInPage('clearSelection')
|
||||
} catch (error) {
|
||||
logger.error('stopFindInPage failed', { error })
|
||||
}
|
||||
stopFindOnWebview(activeWebview)
|
||||
attachedWebviewRef.current = null
|
||||
}
|
||||
}
|
||||
}, [activeWebview, handleFoundInPage])
|
||||
}, [activeWebview, handleFoundInPage, stopFindOnWebview])
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeWebview) return
|
||||
if (!isWebviewReady) return
|
||||
const onFindShortcut = window.api?.webview?.onFindShortcut
|
||||
if (!onFindShortcut) return
|
||||
|
||||
const webContentsId = activeWebview.getWebContentsId?.()
|
||||
let webContentsId: number | undefined
|
||||
try {
|
||||
webContentsId = activeWebview.getWebContentsId?.()
|
||||
} catch (error) {
|
||||
logger.debug('WebviewSearch: getWebContentsId failed', { appId, error })
|
||||
return
|
||||
}
|
||||
|
||||
if (!webContentsId) {
|
||||
logger.warn('WebviewSearch: missing webContentsId', { appId })
|
||||
return
|
||||
@ -178,7 +225,7 @@ const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, app
|
||||
return () => {
|
||||
unsubscribe?.()
|
||||
}
|
||||
}, [appId, activeWebview, closeSearch, goToNext, goToPrevious, isVisible, openSearch])
|
||||
}, [appId, activeWebview, closeSearch, goToNext, goToPrevious, isVisible, isWebviewReady, openSearch])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isVisible) return
|
||||
|
||||
@ -164,6 +164,61 @@ describe('WebviewSearch', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('skips shortcut wiring when getWebContentsId throws', async () => {
|
||||
const { webview } = createWebviewMock()
|
||||
const error = new Error('not ready')
|
||||
;(webview as any).getWebContentsId = vi.fn(() => {
|
||||
throw error
|
||||
})
|
||||
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
||||
|
||||
const getWebContentsIdMock = vi.fn(() => {
|
||||
throw error
|
||||
})
|
||||
;(webview as any).getWebContentsId = getWebContentsIdMock
|
||||
const { rerender } = render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getWebContentsIdMock).toHaveBeenCalled()
|
||||
})
|
||||
expect(onFindShortcutMock).not.toHaveBeenCalled()
|
||||
|
||||
;(webview as any).getWebContentsId = vi.fn(() => 1)
|
||||
|
||||
rerender(<WebviewSearch webviewRef={webviewRef} isWebviewReady={false} appId="app-1" />)
|
||||
rerender(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onFindShortcutMock).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('does not call stopFindInPage when webview is not ready', async () => {
|
||||
const { stopFindInPageMock, webview } = createWebviewMock()
|
||||
const error = new Error('loading')
|
||||
const getWebContentsIdMock = vi.fn(() => {
|
||||
throw error
|
||||
})
|
||||
;(webview as any).getWebContentsId = getWebContentsIdMock
|
||||
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
||||
|
||||
const { rerender, unmount } = render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getWebContentsIdMock).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
stopFindInPageMock.mockImplementation(() => {
|
||||
throw new Error('should not be called')
|
||||
})
|
||||
|
||||
rerender(<WebviewSearch webviewRef={webviewRef} isWebviewReady={false} appId="app-1" />)
|
||||
expect(stopFindInPageMock).not.toHaveBeenCalled()
|
||||
|
||||
unmount()
|
||||
expect(stopFindInPageMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('closes the search overlay when escape is forwarded from the webview', async () => {
|
||||
const { webview } = createWebviewMock()
|
||||
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
||||
|
||||
@ -14,7 +14,6 @@ import { LanguagesEnum } from '@renderer/config/translate'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { usePaintings } from '@renderer/hooks/usePaintings'
|
||||
import { useAllProviders } from '@renderer/hooks/useProvider'
|
||||
import { getProviderLabel } from '@renderer/i18n/label'
|
||||
import FileManager from '@renderer/services/FileManager'
|
||||
import { translateText } from '@renderer/services/TranslateService'
|
||||
import type { FileMetadata } from '@renderer/types'
|
||||
@ -32,6 +31,7 @@ import SendMessageButton from '../home/Inputbar/SendMessageButton'
|
||||
import { SettingHelpLink, SettingTitle } from '../settings'
|
||||
import Artboard from './components/Artboard'
|
||||
import PaintingsList from './components/PaintingsList'
|
||||
import ProviderSelect from './components/ProviderSelect'
|
||||
import { type ConfigItem, createModeConfigs, DEFAULT_PAINTING } from './config/aihubmixConfig'
|
||||
import { checkProviderEnabled } from './utils'
|
||||
|
||||
@ -73,20 +73,6 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const providers = useAllProviders()
|
||||
const providerOptions = Options.map((option) => {
|
||||
const provider = providers.find((p) => p.id === option)
|
||||
if (provider) {
|
||||
return {
|
||||
label: getProviderLabel(provider.id),
|
||||
value: provider.id
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
label: 'Unknown Provider',
|
||||
value: undefined
|
||||
}
|
||||
}
|
||||
})
|
||||
const [generating, setGenerating] = useCache('chat.generating')
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
@ -841,21 +827,12 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
/>
|
||||
</SettingHelpLink>
|
||||
</ProviderTitleContainer>
|
||||
|
||||
<Select value={providerOptions[1].value} onChange={handleProviderChange} style={{ marginBottom: 15 }}>
|
||||
{providerOptions.map((provider) => (
|
||||
<Select.Option value={provider.value} key={provider.value}>
|
||||
<SelectOptionContainer>
|
||||
<Avatar
|
||||
radius="md"
|
||||
src={getProviderLogo(provider.value || '')}
|
||||
className="h-4 w-4 border-[0.5px] border-[var(--color-border)]"
|
||||
/>
|
||||
{provider.label}
|
||||
</SelectOptionContainer>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<ProviderSelect
|
||||
provider={aihubmixProvider}
|
||||
options={Options}
|
||||
onChange={handleProviderChange}
|
||||
className={'mb-4'}
|
||||
/>
|
||||
|
||||
{/* 使用JSON配置渲染设置项 */}
|
||||
{modeConfigs[mode].filter((item) => (item.condition ? item.condition(painting) : true)).map(renderConfigItem)}
|
||||
@ -1013,12 +990,6 @@ const ModeSegmentedContainer = styled.div`
|
||||
padding-top: 24px;
|
||||
`
|
||||
|
||||
const SelectOptionContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
// 添加新的样式组件
|
||||
const ProviderTitleContainer = styled.div`
|
||||
display: flex;
|
||||
|
||||
@ -8,7 +8,6 @@ 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 { getProviderLabel } from '@renderer/i18n/label'
|
||||
import FileManager from '@renderer/services/FileManager'
|
||||
import type { FileMetadata } from '@renderer/types'
|
||||
import { convertToBase64, uuid } from '@renderer/utils'
|
||||
@ -27,6 +26,7 @@ import { SettingHelpLink, SettingTitle } from '../settings'
|
||||
import Artboard from './components/Artboard'
|
||||
import ImageUploader from './components/ImageUploader'
|
||||
import PaintingsList from './components/PaintingsList'
|
||||
import ProviderSelect from './components/ProviderSelect'
|
||||
import {
|
||||
COURSE_URL,
|
||||
DEFAULT_PAINTING,
|
||||
@ -44,20 +44,6 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
const [painting, setPainting] = useState<DmxapiPainting>(dmxapi_paintings?.[0] || DEFAULT_PAINTING)
|
||||
const { t } = useTranslation()
|
||||
const providers = useAllProviders()
|
||||
const providerOptions = Options.map((option) => {
|
||||
const provider = providers.find((p) => p.id === option)
|
||||
if (provider) {
|
||||
return {
|
||||
label: getProviderLabel(provider.id),
|
||||
value: provider.id
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
label: 'Unknown Provider',
|
||||
value: undefined
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const dmxapiProvider = providers.find((p) => p.id === 'dmxapi')!
|
||||
|
||||
@ -782,9 +768,9 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
return (
|
||||
<Container>
|
||||
<Navbar>
|
||||
<NavbarCenter style={{ borderRight: 'none' }}>{t('paintings.title')}</NavbarCenter>
|
||||
<NavbarCenter className="border-r-0">{t('paintings.title')}</NavbarCenter>
|
||||
{isMac && (
|
||||
<NavbarRight style={{ justifyContent: 'flex-end' }}>
|
||||
<NavbarRight className="justify-end">
|
||||
<Button size="sm" className="nodrag" startContent={<PlusOutlined />} onPress={createNewPainting}>
|
||||
{t('paintings.button.new.image')}
|
||||
</Button>
|
||||
@ -794,7 +780,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
<ContentContainer id="content-container">
|
||||
<LeftContainer>
|
||||
<ProviderTitleContainer>
|
||||
<SettingTitle style={{ marginBottom: 5 }}>{t('common.provider')}</SettingTitle>
|
||||
<SettingTitle className="mb-1">{t('common.provider')}</SettingTitle>
|
||||
<div>
|
||||
<SettingHelpLink target="_blank" href={COURSE_URL}>
|
||||
{t('paintings.paint_course')}
|
||||
@ -802,32 +788,19 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
<SettingHelpLink target="_blank" href={TOP_UP_URL}>
|
||||
{t('paintings.top_up')}
|
||||
</SettingHelpLink>
|
||||
<Avatar
|
||||
radius="md"
|
||||
src={getProviderLogo(dmxapiProvider.id)}
|
||||
className="h-4 w-4 border-[0.5px] border-[var(--color-border)]"
|
||||
style={{ marginLeft: 5 }}
|
||||
/>
|
||||
<ProviderLogo radius="md" src={getProviderLogo(dmxapiProvider.id)} className="ml-1" />
|
||||
</div>
|
||||
</ProviderTitleContainer>
|
||||
<Select value={providerOptions[3].value} onChange={handleProviderChange} style={{ marginBottom: 15 }}>
|
||||
{providerOptions.map((provider) => (
|
||||
<Select.Option value={provider.value} key={provider.value}>
|
||||
<SelectOptionContainer>
|
||||
<Avatar
|
||||
radius="md"
|
||||
src={getProviderLogo(provider.value || '')}
|
||||
className="h-4 w-4 border-[0.5px] border-[var(--color-border)]"
|
||||
/>
|
||||
{provider.label}
|
||||
</SelectOptionContainer>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<ProviderSelect
|
||||
provider={dmxapiProvider}
|
||||
options={Options}
|
||||
onChange={handleProviderChange}
|
||||
className="mb-4"
|
||||
/>
|
||||
{painting.generationMode &&
|
||||
[generationModeType.EDIT, generationModeType.MERGE].includes(painting.generationMode) && (
|
||||
<>
|
||||
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>参考图</SettingTitle>
|
||||
<SettingTitle className="mt-4 mb-1">参考图</SettingTitle>
|
||||
<ImageUploader
|
||||
fileMap={fileMap}
|
||||
maxImages={painting.generationMode === generationModeType.EDIT ? 1 : 3}
|
||||
@ -839,13 +812,13 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
</>
|
||||
)}
|
||||
|
||||
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>
|
||||
<SettingTitle className="mt-4 mb-1">
|
||||
{t('common.model')} <SettingPrice>{painting.priceModel !== '0' ? painting.priceModel : ''}</SettingPrice>
|
||||
</SettingTitle>
|
||||
<Select
|
||||
value={painting.model}
|
||||
onChange={onSelectModel}
|
||||
style={{ width: '100%' }}
|
||||
className="w-full"
|
||||
loading={isLoadingModels}
|
||||
placeholder={isLoadingModels ? t('common.loading') : t('paintings.select_model')}>
|
||||
{Object.entries(modelOptions).map(([provider, models]) => {
|
||||
@ -862,11 +835,11 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
})}
|
||||
</Select>
|
||||
|
||||
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('paintings.image.size')}</SettingTitle>
|
||||
<SettingTitle className="mt-4 mb-1">{t('paintings.image.size')}</SettingTitle>
|
||||
<Select
|
||||
value={isCustomSize ? 'custom' : painting.image_size}
|
||||
onChange={(value) => onSelectImageSize(value)}
|
||||
style={{ width: '100%' }}>
|
||||
className="w-full">
|
||||
{(() => {
|
||||
const currentModel = allModels.find((m) => m.id === painting.model)
|
||||
const modelImageSizes = currentModel?.image_sizes || []
|
||||
@ -894,7 +867,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
|
||||
{/* 自定义尺寸输入框 */}
|
||||
{isCustomSize && allModels.find((m) => m.id === painting.model)?.is_custom_size && (
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<div className="mt-2.5">
|
||||
<RowFlex className="items-center gap-2">
|
||||
<InputNumber
|
||||
placeholder="W"
|
||||
@ -922,7 +895,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
|
||||
{painting.generationMode === generationModeType.GENERATION && (
|
||||
<>
|
||||
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>
|
||||
<SettingTitle className="mt-4 mb-1">
|
||||
{t('paintings.seed')}
|
||||
<InfoTooltip content={t('paintings.seed_desc_tip')} />
|
||||
</SettingTitle>
|
||||
@ -940,7 +913,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
</>
|
||||
)}
|
||||
|
||||
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('paintings.style_type')}</SettingTitle>
|
||||
<SettingTitle className="mt-4 mb-1">{t('paintings.style_type')}</SettingTitle>
|
||||
<SliderContainer>
|
||||
<RadioTextBox>
|
||||
{STYLE_TYPE_OPTIONS.map((ele) => (
|
||||
@ -954,7 +927,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
</RadioTextBox>
|
||||
</SliderContainer>
|
||||
|
||||
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>
|
||||
<SettingTitle className="mt-4 mb-1">
|
||||
{t('paintings.auto_create_paint')}
|
||||
<InfoTooltip content={t('paintings.auto_create_paint_tip')} />
|
||||
</SettingTitle>
|
||||
@ -1026,10 +999,8 @@ const ProviderTitleContainer = styled.div`
|
||||
margin-bottom: 5px;
|
||||
`
|
||||
|
||||
const SelectOptionContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
const ProviderLogo = styled(Avatar)`
|
||||
border: 0.5px solid var(--color-border);
|
||||
`
|
||||
|
||||
const ContentContainer = styled.div`
|
||||
|
||||
@ -10,7 +10,7 @@ import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navb
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import TranslateButton from '@renderer/components/TranslateButton'
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { getProviderLogo } from '@renderer/config/providers'
|
||||
import { getProviderLogo, isNewApiProvider, PROVIDER_URLS } from '@renderer/config/providers'
|
||||
import { LanguagesEnum } from '@renderer/config/translate'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { usePaintings } from '@renderer/hooks/usePaintings'
|
||||
@ -19,8 +19,7 @@ import {
|
||||
getPaintingsBackgroundOptionsLabel,
|
||||
getPaintingsImageSizeOptionsLabel,
|
||||
getPaintingsModerationOptionsLabel,
|
||||
getPaintingsQualityOptionsLabel,
|
||||
getProviderLabel
|
||||
getPaintingsQualityOptionsLabel
|
||||
} from '@renderer/i18n/label'
|
||||
import PaintingsList from '@renderer/pages/paintings/components/PaintingsList'
|
||||
import { DEFAULT_PAINTING, MODELS, SUPPORTED_MODELS } from '@renderer/pages/paintings/config/NewApiConfig'
|
||||
@ -41,6 +40,7 @@ import styled from 'styled-components'
|
||||
import SendMessageButton from '../home/Inputbar/SendMessageButton'
|
||||
import { SettingHelpLink, SettingTitle } from '../settings'
|
||||
import Artboard from './components/Artboard'
|
||||
import ProviderSelect from './components/ProviderSelect'
|
||||
import { checkProviderEnabled } from './utils'
|
||||
|
||||
const logger = loggerService.withContext('NewApiPage')
|
||||
@ -56,8 +56,7 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
}
|
||||
}, [openai_image_generate, openai_image_edit])
|
||||
|
||||
const filteredPaintings = useMemo(() => newApiPaintings[mode] || [], [newApiPaintings, mode])
|
||||
const [painting, setPainting] = useState<PaintingAction>(filteredPaintings[0] || DEFAULT_PAINTING)
|
||||
// moved below after newApiProvider is defined
|
||||
const [currentImageIndex, setCurrentImageIndex] = useState(0)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [abortController, setAbortController] = useState<AbortController | null>(null)
|
||||
@ -68,26 +67,21 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const providers = useAllProviders()
|
||||
const providerOptions = Options.map((option) => {
|
||||
const provider = providers.find((p) => p.id === option)
|
||||
if (provider) {
|
||||
return {
|
||||
label: getProviderLabel(provider.id),
|
||||
value: provider.id
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
label: 'Unknown Provider',
|
||||
value: undefined
|
||||
}
|
||||
}
|
||||
})
|
||||
const location = useLocation()
|
||||
const routeName = location.pathname.split('/').pop() || 'new-api'
|
||||
const newApiProviders = providers.filter((p) => isNewApiProvider(p))
|
||||
|
||||
const [generating, setGenerating] = useCache('chat.generating')
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const [autoTranslateWithSpace] = usePreference('chat.input.translate.auto_translate_with_space')
|
||||
const spaceClickTimer = useRef<NodeJS.Timeout>(null)
|
||||
const newApiProvider = providers.find((p) => p.id === 'new-api')!
|
||||
const newApiProvider = newApiProviders.find((p) => p.id === routeName) || newApiProviders[0]
|
||||
|
||||
const filteredPaintings = useMemo(
|
||||
() => (newApiPaintings[mode] || []).filter((p) => p.providerId === newApiProvider.id),
|
||||
[newApiPaintings, mode, newApiProvider.id]
|
||||
)
|
||||
const [painting, setPainting] = useState<PaintingAction>({ ...DEFAULT_PAINTING, providerId: newApiProvider.id })
|
||||
|
||||
const modeOptions = [
|
||||
{ label: t('paintings.mode.generate'), value: 'openai_image_generate' },
|
||||
@ -102,7 +96,7 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
}, [editImageFiles])
|
||||
|
||||
const updatePaintingState = (updates: Partial<PaintingAction>) => {
|
||||
const updatedPainting = { ...painting, ...updates }
|
||||
const updatedPainting = { ...painting, providerId: newApiProvider.id, ...updates }
|
||||
setPainting(updatedPainting)
|
||||
updatePainting(mode, updatedPainting)
|
||||
}
|
||||
@ -138,9 +132,10 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
return {
|
||||
...DEFAULT_PAINTING,
|
||||
model: painting.model || modelOptions[0]?.value || '',
|
||||
id: uuid()
|
||||
id: uuid(),
|
||||
providerId: newApiProvider.id
|
||||
}
|
||||
}, [modelOptions, painting.model])
|
||||
}, [modelOptions, painting.model, newApiProvider.id])
|
||||
|
||||
const selectedModelConfig = useMemo(
|
||||
() => MODELS.find((m) => m.name === painting.model) || MODELS[0],
|
||||
@ -444,11 +439,10 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
// 处理模式切换
|
||||
const handleModeChange = (value: string) => {
|
||||
setMode(value as keyof PaintingsState)
|
||||
if (newApiPaintings[value as keyof PaintingsState] && newApiPaintings[value as keyof PaintingsState].length > 0) {
|
||||
setPainting(newApiPaintings[value as keyof PaintingsState][0])
|
||||
} else {
|
||||
setPainting(DEFAULT_PAINTING)
|
||||
}
|
||||
const list = (newApiPaintings[value as keyof PaintingsState] || []).filter(
|
||||
(p) => p.providerId === newApiProvider.id
|
||||
)
|
||||
setPainting(list[0] || { ...DEFAULT_PAINTING, providerId: newApiProvider.id })
|
||||
}
|
||||
|
||||
// 渲染配置项的函数
|
||||
@ -473,8 +467,10 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
const newPainting = getNewPainting()
|
||||
addPainting(mode, newPainting)
|
||||
setPainting(newPainting)
|
||||
} else {
|
||||
setPainting(filteredPaintings[0])
|
||||
}
|
||||
}, [filteredPaintings, mode, addPainting, painting, getNewPainting])
|
||||
}, [filteredPaintings, mode, addPainting, getNewPainting])
|
||||
|
||||
useEffect(() => {
|
||||
const timer = spaceClickTimer.current
|
||||
@ -501,9 +497,11 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
<LeftContainer>
|
||||
<ProviderTitleContainer>
|
||||
<SettingTitle style={{ marginBottom: 5 }}>{t('common.provider')}</SettingTitle>
|
||||
<SettingHelpLink target="_blank" href={'https://docs.newapi.pro/apps/cherry-studio/'}>
|
||||
<SettingHelpLink
|
||||
target="_blank"
|
||||
href={PROVIDER_URLS[newApiProvider.id]?.websites?.docs || 'https://docs.newapi.pro/apps/cherry-studio/'}>
|
||||
{t('paintings.learn_more')}
|
||||
<Avatar
|
||||
<ProviderLogo
|
||||
radius="md"
|
||||
src={getProviderLogo(newApiProvider.id)}
|
||||
className="h-4 w-4 border-[0.5px] border-[var(--color-border)]"
|
||||
@ -512,23 +510,7 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
</SettingHelpLink>
|
||||
</ProviderTitleContainer>
|
||||
|
||||
<Select
|
||||
value={providerOptions.find((p) => p.value === 'new-api')?.value}
|
||||
onChange={handleProviderChange}
|
||||
style={{ width: '100%' }}>
|
||||
{providerOptions.map((provider) => (
|
||||
<Select.Option value={provider.value} key={provider.value}>
|
||||
<SelectOptionContainer>
|
||||
<Avatar
|
||||
radius="md"
|
||||
src={getProviderLogo(provider.value || '')}
|
||||
className="h-4 w-4 border-[0.5px] border-[var(--color-border)]"
|
||||
/>
|
||||
{provider.label}
|
||||
</SelectOptionContainer>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<ProviderSelect provider={newApiProvider} options={Options} onChange={handleProviderChange} />
|
||||
|
||||
{/* 当没有可用的 Image Generation 模型时,提示用户先去新增 */}
|
||||
{modelOptions.length === 0 && (
|
||||
@ -792,20 +774,16 @@ const ToolbarMenu = styled.div`
|
||||
gap: 6px;
|
||||
`
|
||||
|
||||
// 添加新的样式组件
|
||||
const ProviderLogo = styled(Avatar)`
|
||||
border: 0.5px solid var(--color-border);
|
||||
`
|
||||
|
||||
const ModeSegmentedContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: 24px;
|
||||
`
|
||||
|
||||
const SelectOptionContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
// 添加新的样式组件
|
||||
const ProviderTitleContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { isNewApiProvider } from '@renderer/config/providers'
|
||||
import { useAllProviders } from '@renderer/hooks/useProvider'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setDefaultPaintingProvider } from '@renderer/store/settings'
|
||||
import type { PaintingProvider } from '@renderer/types'
|
||||
import type { PaintingProvider, SystemProviderId } from '@renderer/types'
|
||||
import type { FC } from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { Route, Routes, useParams } from 'react-router-dom'
|
||||
|
||||
import AihubmixPage from './AihubmixPage'
|
||||
@ -15,19 +17,23 @@ import ZhipuPage from './ZhipuPage'
|
||||
|
||||
const logger = loggerService.withContext('PaintingsRoutePage')
|
||||
|
||||
const Options = ['zhipu', 'aihubmix', 'silicon', 'dmxapi', 'tokenflux', 'new-api']
|
||||
const BASE_OPTIONS: SystemProviderId[] = ['zhipu', 'aihubmix', 'silicon', 'dmxapi', 'tokenflux']
|
||||
|
||||
const PaintingsRoutePage: FC = () => {
|
||||
const params = useParams()
|
||||
const provider = params['*']
|
||||
const dispatch = useAppDispatch()
|
||||
const providers = useAllProviders()
|
||||
const Options = useMemo(() => {
|
||||
return [...BASE_OPTIONS, ...providers.filter((p) => isNewApiProvider(p)).map((p) => p.id)]
|
||||
}, [providers])
|
||||
|
||||
useEffect(() => {
|
||||
logger.debug(`defaultPaintingProvider: ${provider}`)
|
||||
if (provider && Options.includes(provider)) {
|
||||
dispatch(setDefaultPaintingProvider(provider as PaintingProvider))
|
||||
}
|
||||
}, [provider, dispatch])
|
||||
}, [provider, dispatch, Options])
|
||||
|
||||
return (
|
||||
<Routes>
|
||||
@ -37,7 +43,12 @@ const PaintingsRoutePage: FC = () => {
|
||||
<Route path="/silicon" element={<SiliconPage Options={Options} />} />
|
||||
<Route path="/dmxapi" element={<DmxapiPage Options={Options} />} />
|
||||
<Route path="/tokenflux" element={<TokenFluxPage Options={Options} />} />
|
||||
<Route path="/new-api" element={<NewApiPage Options={Options} />} />
|
||||
{/* new-api family providers are mounted dynamically below */}
|
||||
{providers
|
||||
.filter((p) => isNewApiProvider(p))
|
||||
.map((p) => (
|
||||
<Route key={p.id} path={`/${p.id}`} element={<NewApiPage Options={Options} />} />
|
||||
))}
|
||||
</Routes>
|
||||
)
|
||||
}
|
||||
|
||||
@ -14,18 +14,16 @@ import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navb
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import TranslateButton from '@renderer/components/TranslateButton'
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { getProviderLogo } from '@renderer/config/providers'
|
||||
import { LanguagesEnum } from '@renderer/config/translate'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { usePaintings } from '@renderer/hooks/usePaintings'
|
||||
import { useAllProviders } from '@renderer/hooks/useProvider'
|
||||
import { getProviderLabel } from '@renderer/i18n/label'
|
||||
import { getProviderByModel } from '@renderer/services/AssistantService'
|
||||
import FileManager from '@renderer/services/FileManager'
|
||||
import { translateText } from '@renderer/services/TranslateService'
|
||||
import type { FileMetadata, Painting } from '@renderer/types'
|
||||
import { getErrorMessage, uuid } from '@renderer/utils'
|
||||
import { Avatar, Input, InputNumber, Radio, Select, Slider } from 'antd'
|
||||
import { Input, InputNumber, Radio, Select, Slider } from 'antd'
|
||||
import TextArea from 'antd/es/input/TextArea'
|
||||
import type { FC } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
@ -37,6 +35,7 @@ import SendMessageButton from '../home/Inputbar/SendMessageButton'
|
||||
import { SettingTitle } from '../settings'
|
||||
import Artboard from './components/Artboard'
|
||||
import PaintingsList from './components/PaintingsList'
|
||||
import ProviderSelect from './components/ProviderSelect'
|
||||
import { checkProviderEnabled } from './utils'
|
||||
|
||||
export const TEXT_TO_IMAGES_MODELS = [
|
||||
@ -112,22 +111,8 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
const [painting, setPainting] = useState<Painting>(siliconflow_paintings[0] || DEFAULT_PAINTING)
|
||||
const { theme } = useTheme()
|
||||
const providers = useAllProviders()
|
||||
const providerOptions = Options.map((option) => {
|
||||
const provider = providers.find((p) => p.id === option)
|
||||
if (provider) {
|
||||
return {
|
||||
label: getProviderLabel(provider.id),
|
||||
value: provider.id
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
label: 'Unknown Provider',
|
||||
value: undefined
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const siliconflowProvider = providers.find((p) => p.id === 'silicon')
|
||||
const siliconFlowProvider = providers.find((p) => p.id === 'silicon')!
|
||||
const [currentImageIndex, setCurrentImageIndex] = useState(0)
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
@ -166,7 +151,7 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
}
|
||||
|
||||
const onGenerate = async () => {
|
||||
await checkProviderEnabled(siliconflowProvider!, t)
|
||||
await checkProviderEnabled(siliconFlowProvider!, t)
|
||||
|
||||
if (painting.files.length > 0) {
|
||||
const confirmed = await window.modal.confirm({
|
||||
@ -385,17 +370,8 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
<ContentContainer id="content-container">
|
||||
<LeftContainer>
|
||||
<SettingTitle style={{ marginBottom: 5 }}>{t('common.provider')}</SettingTitle>
|
||||
<Select value={providerOptions[2].value} onChange={handleProviderChange}>
|
||||
{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>
|
||||
<ProviderSelect provider={siliconFlowProvider} options={Options} onChange={handleProviderChange} />
|
||||
<SettingTitle className="mt-4 mb-1">{t('common.model')}</SettingTitle>
|
||||
<Select value={painting.model} options={modelOptions} onChange={onSelectModel} />
|
||||
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('paintings.image.size')}</SettingTitle>
|
||||
<Radio.Group
|
||||
@ -643,14 +619,4 @@ const StyledInputNumber = styled(InputNumber)`
|
||||
width: 70px;
|
||||
`
|
||||
|
||||
const SelectOptionContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
const ProviderLogo = styled(Avatar)`
|
||||
flex-shrink: 0;
|
||||
`
|
||||
|
||||
export default SiliconPage
|
||||
|
||||
@ -11,7 +11,6 @@ import { getProviderLogo } from '@renderer/config/providers'
|
||||
import { LanguagesEnum } from '@renderer/config/translate'
|
||||
import { usePaintings } from '@renderer/hooks/usePaintings'
|
||||
import { useAllProviders } from '@renderer/hooks/useProvider'
|
||||
import { getProviderLabel } from '@renderer/i18n/label'
|
||||
import FileManager from '@renderer/services/FileManager'
|
||||
import { translateText } from '@renderer/services/TranslateService'
|
||||
import type { TokenFluxPainting } from '@renderer/types'
|
||||
@ -29,6 +28,7 @@ import { SettingHelpLink, SettingTitle } from '../settings'
|
||||
import Artboard from './components/Artboard'
|
||||
import { DynamicFormRender } from './components/DynamicFormRender'
|
||||
import PaintingsList from './components/PaintingsList'
|
||||
import ProviderSelect from './components/ProviderSelect'
|
||||
import { DEFAULT_TOKENFLUX_PAINTING, type TokenFluxModel } from './config/tokenFluxConfig'
|
||||
import { checkProviderEnabled } from './utils'
|
||||
import TokenFluxService from './utils/TokenFluxService'
|
||||
@ -54,21 +54,6 @@ const TokenFluxPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
tokenFluxPaintings[0] || { ...DEFAULT_TOKENFLUX_PAINTING, id: uuid() }
|
||||
)
|
||||
|
||||
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 navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const [autoTranslateWithSpace] = usePreference('chat.input.translate.auto_translate_with_space')
|
||||
@ -388,23 +373,7 @@ const TokenFluxPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
</SettingHelpLink>
|
||||
</ProviderTitleContainer>
|
||||
|
||||
<Select
|
||||
value={providerOptions.find((p) => p.value === 'tokenflux')?.value}
|
||||
onChange={handleProviderChange}
|
||||
style={{ width: '100%' }}>
|
||||
{providerOptions.map((provider) => (
|
||||
<Select.Option value={provider.value} key={provider.value}>
|
||||
<SelectOptionContainer>
|
||||
<Avatar
|
||||
radius="md"
|
||||
src={getProviderLogo(provider.value || '')}
|
||||
className="h-4 w-4 border-[0.5px] border-[var(--color-border)]"
|
||||
/>
|
||||
{provider.label}
|
||||
</SelectOptionContainer>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<ProviderSelect provider={tokenfluxProvider} options={Options} onChange={handleProviderChange} />
|
||||
|
||||
{/* Model & Pricing Section */}
|
||||
<SectionTitle
|
||||
@ -759,11 +728,4 @@ const ProviderTitleContainer = styled.div`
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
`
|
||||
|
||||
const SelectOptionContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
export default TokenFluxPage
|
||||
|
||||
@ -10,7 +10,6 @@ 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 { getProviderLabel } from '@renderer/i18n/label'
|
||||
import FileManager from '@renderer/services/FileManager'
|
||||
import { getErrorMessage, uuid } from '@renderer/utils'
|
||||
import { InputNumber, Radio, Select } from 'antd'
|
||||
@ -25,6 +24,7 @@ import SendMessageButton from '../home/Inputbar/SendMessageButton'
|
||||
import { SettingHelpLink, SettingTitle } from '../settings'
|
||||
import Artboard from './components/Artboard'
|
||||
import PaintingsList from './components/PaintingsList'
|
||||
import ProviderSelect from './components/ProviderSelect'
|
||||
import {
|
||||
COURSE_URL,
|
||||
DEFAULT_PAINTING,
|
||||
@ -51,21 +51,6 @@ const ZhipuPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [painting?.id]) // 只在painting的id改变时执行,避免无限循环
|
||||
|
||||
const providerOptions = Options.map((option) => {
|
||||
const provider = providers.find((p) => p.id === option)
|
||||
if (provider) {
|
||||
return {
|
||||
label: getProviderLabel(provider.id),
|
||||
value: provider.id
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
label: 'Unknown Provider',
|
||||
value: undefined
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const zhipuProvider = providers.find((p) => p.id === 'zhipu')!
|
||||
|
||||
const [currentImageIndex, setCurrentImageIndex] = useState(0)
|
||||
@ -375,16 +360,7 @@ const ZhipuPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
/>
|
||||
</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 radius="md" src={getProviderLogo(provider.value || '')} className="h-4 w-4" />
|
||||
{provider.label}
|
||||
</SelectOptionContainer>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<ProviderSelect provider={zhipuProvider} options={Options} onChange={handleProviderChange} className="mb-4" />
|
||||
|
||||
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('common.model')}</SettingTitle>
|
||||
<Select
|
||||
@ -568,12 +544,6 @@ const ProviderTitleContainer = styled.div`
|
||||
margin-bottom: 10px;
|
||||
`
|
||||
|
||||
const SelectOptionContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
const ProviderLogo = styled(Avatar)`
|
||||
border-radius: 4px;
|
||||
`
|
||||
|
||||
100
src/renderer/src/pages/paintings/components/ProviderSelect.tsx
Normal file
100
src/renderer/src/pages/paintings/components/ProviderSelect.tsx
Normal file
@ -0,0 +1,100 @@
|
||||
import { Select, SelectItem } from '@heroui/react'
|
||||
import { ProviderAvatarPrimitive } from '@renderer/components/ProviderAvatar'
|
||||
import { getProviderLogo } from '@renderer/config/providers'
|
||||
import ImageStorage from '@renderer/services/ImageStorage'
|
||||
import { getProviderNameById } from '@renderer/services/ProviderService'
|
||||
import type { Provider } from '@types'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
type ProviderSelectProps = {
|
||||
provider: Provider
|
||||
options: string[]
|
||||
onChange: (value: string) => void
|
||||
style?: React.CSSProperties
|
||||
className?: string
|
||||
}
|
||||
|
||||
const ProviderSelect: FC<ProviderSelectProps> = ({ provider, options, onChange, style, className }) => {
|
||||
const [customLogos, setCustomLogos] = useState<Record<string, string>>({})
|
||||
|
||||
useEffect(() => {
|
||||
const loadLogos = async () => {
|
||||
const logos: Record<string, string> = {}
|
||||
for (const providerId of options) {
|
||||
try {
|
||||
const logoData = await ImageStorage.get(`provider-${providerId}`)
|
||||
if (logoData) {
|
||||
logos[providerId] = logoData
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore errors for providers without custom logos
|
||||
}
|
||||
}
|
||||
setCustomLogos(logos)
|
||||
}
|
||||
|
||||
loadLogos()
|
||||
}, [options])
|
||||
|
||||
const getProviderLogoSrc = (providerId: string) => {
|
||||
const systemLogo = getProviderLogo(providerId)
|
||||
if (systemLogo) {
|
||||
return systemLogo
|
||||
}
|
||||
return customLogos[providerId]
|
||||
}
|
||||
|
||||
const providerOptions = options.map((option) => {
|
||||
return {
|
||||
label: getProviderNameById(option),
|
||||
value: option
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Select
|
||||
selectedKeys={[provider.id]}
|
||||
onSelectionChange={(keys) => {
|
||||
const selectedKey = Array.from(keys)[0] as string
|
||||
onChange(selectedKey)
|
||||
}}
|
||||
style={style}
|
||||
className={`w-full ${className || ''}`}
|
||||
renderValue={(items) => {
|
||||
return items.map((item) => (
|
||||
<div key={item.key} className="flex items-center gap-2">
|
||||
<div className="flex h-4 w-4 items-center justify-center">
|
||||
<ProviderAvatarPrimitive
|
||||
providerId={item.key as string}
|
||||
providerName={item.textValue || ''}
|
||||
logoSrc={getProviderLogoSrc(item.key as string)}
|
||||
size={16}
|
||||
/>
|
||||
</div>
|
||||
<span>{item.textValue}</span>
|
||||
</div>
|
||||
))
|
||||
}}>
|
||||
{providerOptions.map((providerOption) => (
|
||||
<SelectItem
|
||||
key={providerOption.value}
|
||||
textValue={providerOption.label}
|
||||
startContent={
|
||||
<div className="flex h-4 w-4 items-center justify-center">
|
||||
<ProviderAvatarPrimitive
|
||||
providerId={providerOption.value}
|
||||
providerName={providerOption.label}
|
||||
logoSrc={getProviderLogoSrc(providerOption.value)}
|
||||
size={16}
|
||||
/>
|
||||
</div>
|
||||
}>
|
||||
{providerOption.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProviderSelect
|
||||
@ -252,7 +252,8 @@ const PopupContainer: React.FC<Props> = ({ provider, resolve }) => {
|
||||
{ label: 'OpenAI-Response', value: 'openai-response' },
|
||||
{ label: 'Gemini', value: 'gemini' },
|
||||
{ label: 'Anthropic', value: 'anthropic' },
|
||||
{ label: 'Azure OpenAI', value: 'azure-openai' }
|
||||
{ label: 'Azure OpenAI', value: 'azure-openai' },
|
||||
{ label: 'New API', value: 'new-api' }
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@ -1,16 +1,14 @@
|
||||
// TODO: Refactor this component to use HeroUI
|
||||
import { Button, Tooltip } from '@cherrystudio/ui'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { loggerService } from '@renderer/services/LoggerService'
|
||||
import { useApiServer } from '@renderer/hooks/useApiServer'
|
||||
import type { RootState } from '@renderer/store'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setApiServerApiKey, setApiServerEnabled, setApiServerPort } from '@renderer/store/settings'
|
||||
import { setApiServerApiKey, setApiServerPort } from '@renderer/store/settings'
|
||||
import { formatErrorMessage } from '@renderer/utils/error'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { Input, InputNumber, Typography } from 'antd'
|
||||
import { Copy, ExternalLink, Play, RotateCcw, Square } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import styled from 'styled-components'
|
||||
@ -18,7 +16,6 @@ import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import { SettingContainer } from '../..'
|
||||
|
||||
const logger = loggerService.withContext('ApiServerSettings')
|
||||
const { Text, Title } = Typography
|
||||
|
||||
const ApiServerSettings: FC = () => {
|
||||
@ -28,67 +25,25 @@ const ApiServerSettings: FC = () => {
|
||||
|
||||
// API Server state with proper defaults
|
||||
const apiServerConfig = useSelector((state: RootState) => state.settings.apiServer)
|
||||
|
||||
const [apiServerRunning, setApiServerRunning] = useState(false)
|
||||
const [apiServerLoading, setApiServerLoading] = useState(false)
|
||||
|
||||
// API Server functions
|
||||
const checkApiServerStatus = async () => {
|
||||
try {
|
||||
const status = await window.electron.ipcRenderer.invoke(IpcChannel.ApiServer_GetStatus)
|
||||
setApiServerRunning(status.running)
|
||||
} catch (error: any) {
|
||||
logger.error('Failed to check API server status:', error)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
checkApiServerStatus()
|
||||
}, [])
|
||||
const { apiServerRunning, apiServerLoading, startApiServer, stopApiServer, restartApiServer, setApiServerEnabled } =
|
||||
useApiServer()
|
||||
|
||||
const handleApiServerToggle = async (enabled: boolean) => {
|
||||
setApiServerLoading(true)
|
||||
try {
|
||||
if (enabled) {
|
||||
const result = await window.electron.ipcRenderer.invoke(IpcChannel.ApiServer_Start)
|
||||
if (result.success) {
|
||||
setApiServerRunning(true)
|
||||
window.toast.success(t('apiServer.messages.startSuccess'))
|
||||
} else {
|
||||
window.toast.error(t('apiServer.messages.startError') + result.error)
|
||||
}
|
||||
await startApiServer()
|
||||
} else {
|
||||
const result = await window.electron.ipcRenderer.invoke(IpcChannel.ApiServer_Stop)
|
||||
if (result.success) {
|
||||
setApiServerRunning(false)
|
||||
window.toast.success(t('apiServer.messages.stopSuccess'))
|
||||
} else {
|
||||
window.toast.error(t('apiServer.messages.stopError') + result.error)
|
||||
}
|
||||
await stopApiServer()
|
||||
}
|
||||
} catch (error) {
|
||||
window.toast.error(t('apiServer.messages.operationFailed') + formatErrorMessage(error))
|
||||
} finally {
|
||||
dispatch(setApiServerEnabled(enabled))
|
||||
setApiServerLoading(false)
|
||||
setApiServerEnabled(enabled)
|
||||
}
|
||||
}
|
||||
|
||||
const handleApiServerRestart = async () => {
|
||||
setApiServerLoading(true)
|
||||
try {
|
||||
const result = await window.electron.ipcRenderer.invoke(IpcChannel.ApiServer_Restart)
|
||||
if (result.success) {
|
||||
await checkApiServerStatus()
|
||||
window.toast.success(t('apiServer.messages.restartSuccess'))
|
||||
} else {
|
||||
window.toast.error(t('apiServer.messages.restartError') + result.error)
|
||||
}
|
||||
} catch (error) {
|
||||
window.toast.error(t('apiServer.messages.restartFailed') + (error as Error).message)
|
||||
} finally {
|
||||
setApiServerLoading(false)
|
||||
}
|
||||
await restartApiServer()
|
||||
}
|
||||
|
||||
const copyApiKey = () => {
|
||||
|
||||
@ -6,6 +6,7 @@ import { ProviderTypeSchema } from './provider'
|
||||
// Request schema for /v1/models
|
||||
export const ApiModelsFilterSchema = z.object({
|
||||
providerType: ProviderTypeSchema.optional(),
|
||||
supportAnthropic: z.coerce.boolean().optional(),
|
||||
offset: z.coerce.number().min(0).default(0).optional(),
|
||||
limit: z.coerce.number().min(1).default(20).optional()
|
||||
})
|
||||
|
||||
38
src/renderer/src/types/apiServer.ts
Normal file
38
src/renderer/src/types/apiServer.ts
Normal file
@ -0,0 +1,38 @@
|
||||
export type ApiServerConfig = {
|
||||
enabled: boolean
|
||||
host: string
|
||||
port: number
|
||||
apiKey: string
|
||||
}
|
||||
|
||||
export type GetApiServerStatusResult = {
|
||||
running: boolean
|
||||
config: ApiServerConfig | null
|
||||
}
|
||||
|
||||
export type StartApiServerStatusResult =
|
||||
| {
|
||||
success: true
|
||||
}
|
||||
| {
|
||||
success: false
|
||||
error: string
|
||||
}
|
||||
|
||||
export type RestartApiServerStatusResult =
|
||||
| {
|
||||
success: true
|
||||
}
|
||||
| {
|
||||
success: false
|
||||
error: string
|
||||
}
|
||||
|
||||
export type StopApiServerStatusResult =
|
||||
| {
|
||||
success: true
|
||||
}
|
||||
| {
|
||||
success: false
|
||||
error: string
|
||||
}
|
||||
@ -20,6 +20,7 @@ import type {
|
||||
UnsupportedFunctionalityError
|
||||
} from 'ai'
|
||||
|
||||
import type { ProviderSpecificError } from './provider-specific-error'
|
||||
import type { Serializable } from './serialize'
|
||||
|
||||
export interface SerializedError {
|
||||
@ -80,7 +81,7 @@ export interface SerializedAiSdkInvalidArgumentError extends SerializedAiSdkErro
|
||||
export const isSerializedAiSdkInvalidArgumentError = (
|
||||
error: SerializedError
|
||||
): error is SerializedAiSdkInvalidArgumentError => {
|
||||
return isSerializedAiSdkError(error) && 'parameter' in error && 'value' in error
|
||||
return isSerializedAiSdkError(error) && 'message' in error && error.name === 'AI_InvalidArgumentError'
|
||||
}
|
||||
|
||||
export interface SerializedAiSdkInvalidDataContentError extends SerializedAiSdkError {
|
||||
@ -198,10 +199,20 @@ export interface SerializedAiSdkNoSuchToolError extends SerializedAiSdkError {
|
||||
readonly availableTools: string[] | null
|
||||
}
|
||||
|
||||
export interface SerializedAiSdkProviderSpecificError extends SerializedAiSdkError {
|
||||
readonly provider: string
|
||||
}
|
||||
|
||||
export const isSerializedAiSdkNoSuchToolError = (error: SerializedError): error is SerializedAiSdkNoSuchToolError => {
|
||||
return isSerializedAiSdkError(error) && 'toolName' in error && 'availableTools' in error
|
||||
}
|
||||
|
||||
export const isSerializedAiSdkProviderSpecificError = (
|
||||
error: SerializedError
|
||||
): error is SerializedAiSdkProviderSpecificError => {
|
||||
return isSerializedAiSdkError(error) && 'provider' in error
|
||||
}
|
||||
|
||||
export interface SerializedAiSdkRetryError extends SerializedAiSdkError {
|
||||
readonly reason: string
|
||||
readonly lastError: Serializable
|
||||
@ -277,6 +288,7 @@ export type AiSdkErrorUnion =
|
||||
| NoSuchModelError
|
||||
| NoSuchProviderError
|
||||
| NoSuchToolError
|
||||
| ProviderSpecificError
|
||||
| RetryError
|
||||
| ToolCallRepairError
|
||||
| TypeValidationError
|
||||
@ -297,6 +309,7 @@ export type SerializedAiSdkErrorUnion =
|
||||
| SerializedAiSdkNoSuchModelError
|
||||
| SerializedAiSdkNoSuchProviderError
|
||||
| SerializedAiSdkNoSuchToolError
|
||||
| SerializedAiSdkProviderSpecificError
|
||||
| SerializedAiSdkRetryError
|
||||
| SerializedAiSdkToolCallRepairError
|
||||
| SerializedAiSdkTypeValidationError
|
||||
@ -317,6 +330,7 @@ export const isSerializedAiSdkErrorUnion = (error: SerializedError): error is Se
|
||||
isSerializedAiSdkNoSuchModelError(error) ||
|
||||
isSerializedAiSdkNoSuchProviderError(error) ||
|
||||
isSerializedAiSdkNoSuchToolError(error) ||
|
||||
isSerializedAiSdkProviderSpecificError(error) ||
|
||||
isSerializedAiSdkRetryError(error) ||
|
||||
isSerializedAiSdkToolCallRepairError(error) ||
|
||||
isSerializedAiSdkTypeValidationError(error) ||
|
||||
|
||||
@ -17,6 +17,7 @@ import type { BaseTool, MCPTool } from './tool'
|
||||
|
||||
export * from './agent'
|
||||
export * from './apiModels'
|
||||
export * from './apiServer'
|
||||
export * from './knowledge'
|
||||
export * from './mcp'
|
||||
export * from './notification'
|
||||
@ -272,6 +273,8 @@ export type PaintingParams = {
|
||||
id: string
|
||||
urls: string[]
|
||||
files: FileMetadata[]
|
||||
// provider that this painting belongs to (for new-api family separation)
|
||||
providerId?: string
|
||||
}
|
||||
|
||||
export type PaintingProvider = 'zhipu' | 'aihubmix' | 'silicon' | 'dmxapi' | 'new-api'
|
||||
@ -843,13 +846,6 @@ export type S3Config = {
|
||||
}
|
||||
|
||||
export type { Message } from './newMessage'
|
||||
|
||||
export interface ApiServerConfig {
|
||||
enabled: boolean
|
||||
host: string
|
||||
port: number
|
||||
apiKey: string
|
||||
}
|
||||
export * from './tool'
|
||||
|
||||
// Memory Service Types
|
||||
|
||||
29
src/renderer/src/types/provider-specific-error.ts
Normal file
29
src/renderer/src/types/provider-specific-error.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { AISDKError } from 'ai'
|
||||
|
||||
const name = 'AI_ProviderSpecificError'
|
||||
const marker = `vercel.ai.error.${name}`
|
||||
const symbol = Symbol.for(marker)
|
||||
|
||||
export class ProviderSpecificError extends AISDKError {
|
||||
// @ts-ignore
|
||||
private readonly [symbol] = true // used in isInstance
|
||||
|
||||
readonly provider: string
|
||||
|
||||
constructor({
|
||||
message,
|
||||
provider,
|
||||
cause
|
||||
}: {
|
||||
message: string
|
||||
provider: string
|
||||
cause?: unknown
|
||||
}) {
|
||||
super({ name, message, cause })
|
||||
this.provider = provider
|
||||
}
|
||||
|
||||
static isInstance(error: unknown): error is ProviderSpecificError {
|
||||
return AISDKError.hasMarker(error, marker)
|
||||
}
|
||||
}
|
||||
@ -11,7 +11,8 @@ export const ProviderTypeSchema = z.enum([
|
||||
'vertexai',
|
||||
'mistral',
|
||||
'aws-bedrock',
|
||||
'vertex-anthropic'
|
||||
'vertex-anthropic',
|
||||
'new-api'
|
||||
])
|
||||
|
||||
export type ProviderType = z.infer<typeof ProviderTypeSchema>
|
||||
|
||||
@ -18,7 +18,7 @@ export const getModelFilterByAgentType = (type: AgentType): ApiModelsFilter => {
|
||||
switch (type) {
|
||||
case 'claude-code':
|
||||
return {
|
||||
providerType: 'anthropic'
|
||||
supportAnthropic: true
|
||||
}
|
||||
default:
|
||||
return {}
|
||||
|
||||
@ -201,6 +201,7 @@ export const serializeError = (error: AiSdkErrorUnion): SerializedError => {
|
||||
? serializeInvalidToolInputError(error.originalError)
|
||||
: serializeNoSuchToolError(error.originalError)
|
||||
if ('functionality' in error) serializedError.functionality = error.functionality
|
||||
if ('provider' in error) serializedError.provider = error.provider
|
||||
|
||||
return serializedError
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user