mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-09 06:49:02 +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
|
# See: https://github.com/anthropics/claude-code-action/blob/main/docs/security.md
|
||||||
github_token: ${{ secrets.TOKEN_GITHUB_WRITE }}
|
github_token: ${{ secrets.TOKEN_GITHUB_WRITE }}
|
||||||
allowed_non_write_users: "*"
|
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/*)"
|
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: |
|
prompt: |
|
||||||
你是一个多语言翻译助手。你需要响应 GitHub Webhooks 中的以下四种事件:
|
你是一个多语言翻译助手。你需要响应 GitHub Webhooks 中的以下四种事件:
|
||||||
@ -108,3 +108,5 @@ jobs:
|
|||||||
|
|
||||||
使用以下命令获取完整信息:
|
使用以下命令获取完整信息:
|
||||||
gh issue view ${{ github.event.issue.number }} --json title,body,comments
|
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
|
artifactBuildCompleted: scripts/artifact-build-completed.js
|
||||||
releaseInfo:
|
releaseInfo:
|
||||||
releaseNotes: |
|
releaseNotes: |
|
||||||
What's New in v1.6.3
|
<!--LANG:en-->
|
||||||
|
What's New in v1.7.0-beta.1
|
||||||
|
|
||||||
Features:
|
Major Features:
|
||||||
- Notes: Add spell-check control, automatic table line wrapping, export functionality, and LLM-based renaming
|
- 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
|
||||||
- UI: Expand topic rename clickable area, add middle-click tab closing, remove redundant scrollbars, fix message menubar overflow
|
- Agent Management: Create, configure, and manage agents with custom settings including model selection, tool permissions, accessible paths, and MCP server integrations
|
||||||
- Editor: Add read-only extension support, make TextFilePreview read-only but copyable
|
- Agent Sessions: Dedicated session management for agent interactions with persistent message history and context tracking
|
||||||
- Models: Update support for DeepSeek v3.2, Claude 4.5, GLM 4.6, Gemini regex, and vision models
|
- Unified UI: Streamlined interface combining Assistants and Agents tabs with improved navigation and settings management
|
||||||
- Code Tools: Add GitHub Copilot CLI integration
|
|
||||||
|
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:
|
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 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:
|
Technical Updates:
|
||||||
- Upgrade to Electron 37.6.0
|
- React 19.2.0 upgrade
|
||||||
- Update dependencies across packages
|
- 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",
|
"@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",
|
"@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",
|
"@strongtz/win32-arm64-msvc": "^0.4.7",
|
||||||
|
"express": "^5.1.0",
|
||||||
"font-list": "^2.0.0",
|
"font-list": "^2.0.0",
|
||||||
"graceful-fs": "^4.2.11",
|
"graceful-fs": "^4.2.11",
|
||||||
"jsdom": "26.1.0",
|
"jsdom": "26.1.0",
|
||||||
@ -95,6 +96,7 @@
|
|||||||
"selection-hook": "^1.0.12",
|
"selection-hook": "^1.0.12",
|
||||||
"sharp": "^0.34.3",
|
"sharp": "^0.34.3",
|
||||||
"swagger-jsdoc": "^6.2.8",
|
"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",
|
"tesseract.js": "patch:tesseract.js@npm%3A6.0.1#~/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch",
|
||||||
"turndown": "7.2.0"
|
"turndown": "7.2.0"
|
||||||
},
|
},
|
||||||
@ -265,7 +267,6 @@
|
|||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
"eslint-plugin-unused-imports": "^4.1.4",
|
"eslint-plugin-unused-imports": "^4.1.4",
|
||||||
"express": "^5.1.0",
|
|
||||||
"express-validator": "^7.2.1",
|
"express-validator": "^7.2.1",
|
||||||
"fast-diff": "^1.3.0",
|
"fast-diff": "^1.3.0",
|
||||||
"fast-xml-parser": "^5.2.0",
|
"fast-xml-parser": "^5.2.0",
|
||||||
@ -306,8 +307,8 @@
|
|||||||
"pdf-parse": "^1.1.1",
|
"pdf-parse": "^1.1.1",
|
||||||
"playwright": "^1.52.0",
|
"playwright": "^1.52.0",
|
||||||
"proxy-agent": "^6.5.0",
|
"proxy-agent": "^6.5.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-error-boundary": "^6.0.0",
|
"react-error-boundary": "^6.0.0",
|
||||||
"react-hotkeys-hook": "^4.6.1",
|
"react-hotkeys-hook": "^4.6.1",
|
||||||
"react-i18next": "^14.1.2",
|
"react-i18next": "^14.1.2",
|
||||||
@ -339,7 +340,6 @@
|
|||||||
"string-width": "^7.2.0",
|
"string-width": "^7.2.0",
|
||||||
"striptags": "^3.2.0",
|
"striptags": "^3.2.0",
|
||||||
"styled-components": "^6.1.11",
|
"styled-components": "^6.1.11",
|
||||||
"swagger-ui-express": "^5.0.1",
|
|
||||||
"swr": "^2.3.6",
|
"swr": "^2.3.6",
|
||||||
"tailwindcss": "^4.1.13",
|
"tailwindcss": "^4.1.13",
|
||||||
"tar": "^7.4.3",
|
"tar": "^7.4.3",
|
||||||
|
|||||||
@ -349,6 +349,7 @@ export enum IpcChannel {
|
|||||||
ApiServer_Stop = 'api-server:stop',
|
ApiServer_Stop = 'api-server:stop',
|
||||||
ApiServer_Restart = 'api-server:restart',
|
ApiServer_Restart = 'api-server:restart',
|
||||||
ApiServer_GetStatus = 'api-server:get-status',
|
ApiServer_GetStatus = 'api-server:get-status',
|
||||||
|
// NOTE: This api is not be used.
|
||||||
ApiServer_GetConfig = 'api-server:get-config',
|
ApiServer_GetConfig = 'api-server:get-config',
|
||||||
|
|
||||||
// Anthropic OAuth
|
// Anthropic OAuth
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { isEmpty } from 'lodash'
|
||||||
|
|
||||||
import type { ApiModel, ApiModelsFilter, ApiModelsResponse } from '../../../renderer/src/types/apiModels'
|
import type { ApiModel, ApiModelsFilter, ApiModelsResponse } from '../../../renderer/src/types/apiModels'
|
||||||
import { loggerService } from '../../services/LoggerService'
|
import { loggerService } from '../../services/LoggerService'
|
||||||
import { getAvailableProviders, listAllAvailableModels, transformModelToOpenAI } from '../utils'
|
import { getAvailableProviders, listAllAvailableModels, transformModelToOpenAI } from '../utils'
|
||||||
@ -8,6 +10,10 @@ const logger = loggerService.withContext('ModelsService')
|
|||||||
|
|
||||||
export type ModelsFilter = ApiModelsFilter
|
export type ModelsFilter = ApiModelsFilter
|
||||||
|
|
||||||
|
const isAnthropicProvider = (provider: { type: string; anthropicApiHost?: string }) => {
|
||||||
|
return provider.type === 'anthropic' || !isEmpty(provider.anthropicApiHost?.trim())
|
||||||
|
}
|
||||||
|
|
||||||
export class ModelsService {
|
export class ModelsService {
|
||||||
async getModels(filter: ModelsFilter): Promise<ApiModelsResponse> {
|
async getModels(filter: ModelsFilter): Promise<ApiModelsResponse> {
|
||||||
try {
|
try {
|
||||||
@ -16,9 +22,7 @@ export class ModelsService {
|
|||||||
let providers = await getAvailableProviders()
|
let providers = await getAvailableProviders()
|
||||||
|
|
||||||
if (filter.providerType === 'anthropic') {
|
if (filter.providerType === 'anthropic') {
|
||||||
providers = providers.filter(
|
providers = providers.filter(isAnthropicProvider)
|
||||||
(p) => p.type === 'anthropic' || (p.anthropicApiHost !== undefined && p.anthropicApiHost.trim() !== '')
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const models = await listAllAvailableModels(providers)
|
const models = await listAllAvailableModels(providers)
|
||||||
@ -41,6 +45,10 @@ export class ModelsService {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (filter.supportAnthropic && model.endpoint_type !== 'anthropic' && !isAnthropicProvider(provider)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
const openAIModel = transformModelToOpenAI(model, provider)
|
const openAIModel = transformModelToOpenAI(model, provider)
|
||||||
const fullModelId = openAIModel.id // This is already in format "provider:model_id"
|
const fullModelId = openAIModel.id // This is already in format "provider:model_id"
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { cacheService } from '@data/CacheService'
|
import { cacheService } from '@data/CacheService'
|
||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import { reduxService } from '@main/services/ReduxService'
|
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')
|
const logger = loggerService.withContext('ApiServerUtils')
|
||||||
|
|
||||||
@ -114,6 +114,7 @@ export async function validateModelId(model: string): Promise<{
|
|||||||
error?: ModelValidationError
|
error?: ModelValidationError
|
||||||
provider?: Provider
|
provider?: Provider
|
||||||
modelId?: string
|
modelId?: string
|
||||||
|
modelEndpointType?: EndpointType
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
if (!model || typeof model !== 'string') {
|
if (!model || typeof model !== 'string') {
|
||||||
@ -166,7 +167,8 @@ export async function validateModelId(model: string): Promise<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if model exists in provider
|
// 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) {
|
if (!modelExists) {
|
||||||
const availableModels = provider.models?.map((m) => m.id).join(', ') || 'none'
|
const availableModels = provider.models?.map((m) => m.id).join(', ') || 'none'
|
||||||
return {
|
return {
|
||||||
@ -179,10 +181,13 @@ export async function validateModelId(model: string): Promise<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const modelEndpointType = modelInProvider?.endpoint_type
|
||||||
|
|
||||||
return {
|
return {
|
||||||
valid: true,
|
valid: true,
|
||||||
provider,
|
provider,
|
||||||
modelId
|
modelId,
|
||||||
|
modelEndpointType
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error('Error validating model ID', { error, model })
|
logger.error('Error validating model ID', { error, model })
|
||||||
|
|||||||
@ -277,7 +277,7 @@ export default class MineruPreprocessProvider extends BasePreprocessProvider {
|
|||||||
|
|
||||||
const response = await net.fetch(uploadUrl, {
|
const response = await net.fetch(uploadUrl, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: fileBuffer,
|
body: fileBuffer as unknown as BodyInit,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/pdf'
|
'Content-Type': 'application/pdf'
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,11 @@
|
|||||||
import { IpcChannel } from '@shared/IpcChannel'
|
import { IpcChannel } from '@shared/IpcChannel'
|
||||||
import type { ApiServerConfig } from '@types'
|
import type {
|
||||||
|
ApiServerConfig,
|
||||||
|
GetApiServerStatusResult,
|
||||||
|
RestartApiServerStatusResult,
|
||||||
|
StartApiServerStatusResult,
|
||||||
|
StopApiServerStatusResult
|
||||||
|
} from '@types'
|
||||||
import { ipcMain } from 'electron'
|
import { ipcMain } from 'electron'
|
||||||
|
|
||||||
import { apiServer } from '../apiServer'
|
import { apiServer } from '../apiServer'
|
||||||
@ -52,7 +58,7 @@ export class ApiServerService {
|
|||||||
|
|
||||||
registerIpcHandlers(): void {
|
registerIpcHandlers(): void {
|
||||||
// API Server
|
// API Server
|
||||||
ipcMain.handle(IpcChannel.ApiServer_Start, async () => {
|
ipcMain.handle(IpcChannel.ApiServer_Start, async (): Promise<StartApiServerStatusResult> => {
|
||||||
try {
|
try {
|
||||||
await this.start()
|
await this.start()
|
||||||
return { success: true }
|
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 {
|
try {
|
||||||
await this.stop()
|
await this.stop()
|
||||||
return { success: true }
|
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 {
|
try {
|
||||||
await this.restart()
|
await this.restart()
|
||||||
return { success: true }
|
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 {
|
try {
|
||||||
const config = await this.getCurrentConfig()
|
const config = await this.getCurrentConfig()
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { config as apiConfigService } from '@main/apiServer/config'
|
|||||||
import { validateModelId } from '@main/apiServer/utils'
|
import { validateModelId } from '@main/apiServer/utils'
|
||||||
import getLoginShellEnvironment from '@main/utils/shell-env'
|
import getLoginShellEnvironment from '@main/utils/shell-env'
|
||||||
import { app } from 'electron'
|
import { app } from 'electron'
|
||||||
|
import { isEmpty } from 'lodash'
|
||||||
|
|
||||||
import type { GetAgentSessionResponse } from '../..'
|
import type { GetAgentSessionResponse } from '../..'
|
||||||
import type { AgentServiceInterface, AgentStream, AgentStreamEvent } from '../../interfaces/AgentStreamInterface'
|
import type { AgentServiceInterface, AgentStream, AgentStreamEvent } from '../../interfaces/AgentStreamInterface'
|
||||||
@ -61,11 +62,20 @@ class ClaudeCodeService implements AgentServiceInterface {
|
|||||||
})
|
})
|
||||||
return aiStream
|
return aiStream
|
||||||
}
|
}
|
||||||
if (
|
|
||||||
(modelInfo.provider?.type !== 'anthropic' &&
|
const validateModelInfo: (m: typeof modelInfo) => boolean = (m) => {
|
||||||
(modelInfo.provider?.anthropicApiHost === undefined || modelInfo.provider.anthropicApiHost.trim() === '')) ||
|
const { provider, modelEndpointType } = m
|
||||||
modelInfo.provider.apiKey === ''
|
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', {
|
logger.error('Anthropic provider configuration is missing', {
|
||||||
modelInfo
|
modelInfo
|
||||||
})
|
})
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import type {
|
|||||||
FileListResponse,
|
FileListResponse,
|
||||||
FileMetadata,
|
FileMetadata,
|
||||||
FileUploadResponse,
|
FileUploadResponse,
|
||||||
|
GetApiServerStatusResult,
|
||||||
KnowledgeBaseParams,
|
KnowledgeBaseParams,
|
||||||
KnowledgeItem,
|
KnowledgeItem,
|
||||||
KnowledgeSearchResult,
|
KnowledgeSearchResult,
|
||||||
@ -29,8 +30,11 @@ import type {
|
|||||||
OcrProvider,
|
OcrProvider,
|
||||||
OcrResult,
|
OcrResult,
|
||||||
Provider,
|
Provider,
|
||||||
|
RestartApiServerStatusResult,
|
||||||
S3Config,
|
S3Config,
|
||||||
Shortcut,
|
Shortcut,
|
||||||
|
StartApiServerStatusResult,
|
||||||
|
StopApiServerStatusResult,
|
||||||
SupportedOcrFile,
|
SupportedOcrFile,
|
||||||
WebDavConfig
|
WebDavConfig
|
||||||
} from '@types'
|
} from '@types'
|
||||||
@ -537,6 +541,12 @@ const api = {
|
|||||||
ipcRenderer.on(channel, listener)
|
ipcRenderer.on(channel, listener)
|
||||||
return () => ipcRenderer.off(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 { loggerService } from '@logger'
|
||||||
import type { AISDKWebSearchResult, MCPTool, WebSearchResults } from '@renderer/types'
|
import type { AISDKWebSearchResult, MCPTool, WebSearchResults } from '@types'
|
||||||
import { WebSearchSource } from '@renderer/types'
|
import { WebSearchSource } from '@types'
|
||||||
import type { Chunk } from '@renderer/types/chunk'
|
import { type Chunk, ChunkType } from '@renderer/types/chunk'
|
||||||
import { 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 { convertLinks, flushLinkConverterBuffer } from '@renderer/utils/linkConverter'
|
||||||
import type { ClaudeCodeRawValue } from '@shared/agents/claudecode/types'
|
import type { ClaudeCodeRawValue } from '@shared/agents/claudecode/types'
|
||||||
import type { TextStreamPart, ToolSet } from 'ai'
|
import type { TextStreamPart, ToolSet } from 'ai'
|
||||||
@ -357,7 +358,11 @@ export class AiSdkToChunkAdapter {
|
|||||||
case 'error':
|
case 'error':
|
||||||
this.onChunk({
|
this.onChunk({
|
||||||
type: ChunkType.ERROR,
|
type: ChunkType.ERROR,
|
||||||
error: chunk.error as Record<string, any>
|
error: new ProviderSpecificError({
|
||||||
|
message: formatErrorMessage(chunk.error),
|
||||||
|
provider: 'unknown',
|
||||||
|
cause: chunk.error
|
||||||
|
})
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|||||||
@ -50,7 +50,11 @@ export const ImageGenerationMiddleware: CompletionsMiddleware =
|
|||||||
if (!block.file) return null
|
if (!block.file) return null
|
||||||
const binaryData: Uint8Array = await FileManager.readBinaryImage(block.file)
|
const binaryData: Uint8Array = await FileManager.readBinaryImage(block.file)
|
||||||
const mimeType = `${block.file.type}/${block.file.ext.slice(1)}`
|
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[])
|
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 type { LanguageModelMiddleware } from 'ai'
|
||||||
import { extractReasoningMiddleware, simulateStreamingMiddleware } from 'ai'
|
import { extractReasoningMiddleware, simulateStreamingMiddleware } from 'ai'
|
||||||
|
|
||||||
|
import { noThinkMiddleware } from './noThinkMiddleware'
|
||||||
|
|
||||||
const logger = loggerService.withContext('AiSdkMiddlewareBuilder')
|
const logger = loggerService.withContext('AiSdkMiddlewareBuilder')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -187,6 +189,14 @@ function addProviderSpecificMiddlewares(builder: AiSdkMiddlewareBuilder, config:
|
|||||||
// 其他provider的通用处理
|
// 其他provider的通用处理
|
||||||
break
|
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)
|
// return createVertexProvider(provider)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
if (isNewApiProvider(provider)) {
|
||||||
|
return newApiResolverCreator(model, provider)
|
||||||
|
}
|
||||||
|
|
||||||
if (isSystemProvider(provider)) {
|
if (isSystemProvider(provider)) {
|
||||||
if (provider.id === 'aihubmix') {
|
if (provider.id === 'aihubmix') {
|
||||||
return aihubmixProviderCreator(model, provider)
|
return aihubmixProviderCreator(model, provider)
|
||||||
}
|
}
|
||||||
if (isNewApiProvider(provider)) {
|
|
||||||
return newApiResolverCreator(model, provider)
|
|
||||||
}
|
|
||||||
if (provider.id === 'vertexai') {
|
if (provider.id === 'vertexai') {
|
||||||
return vertexAnthropicProviderCreator(model, provider)
|
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')
|
if (!match) throw new Error('Invalid base64 image format')
|
||||||
const mimeType = match[1]
|
const mimeType = match[1]
|
||||||
const byteArray = Base64.toUint8Array(match[2])
|
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 })])
|
await navigator.clipboard.write([new ClipboardItem({ [mimeType]: blob })])
|
||||||
} else if (src.startsWith('file://')) {
|
} else if (src.startsWith('file://')) {
|
||||||
// 处理本地文件路径
|
// 处理本地文件路径
|
||||||
|
|||||||
@ -101,7 +101,7 @@ export const AgentModal: React.FC<Props> = ({ agent, trigger, isOpen: _isOpen, o
|
|||||||
const { addAgent } = useAgents()
|
const { addAgent } = useAgents()
|
||||||
const { updateAgent } = useUpdateAgent()
|
const { updateAgent } = useUpdateAgent()
|
||||||
// hard-coded. We only support anthropic for now.
|
// 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 isEditing = (agent?: AgentWithTools) => agent !== undefined
|
||||||
|
|
||||||
const [form, setForm] = useState<BaseAgentForm>(() => buildAgentForm(agent))
|
const [form, setForm] = useState<BaseAgentForm>(() => buildAgentForm(agent))
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export const CodeBlockShiki = CodeBlock.extend<CodeBlockShikiOptions>({
|
|||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
class: 'code-block-shiki'
|
class: 'code-block-shiki'
|
||||||
}
|
}
|
||||||
}
|
} as CodeBlockShikiOptions
|
||||||
},
|
},
|
||||||
|
|
||||||
addInputRules() {
|
addInputRules() {
|
||||||
|
|||||||
@ -281,7 +281,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
|
|||||||
'new-api': {
|
'new-api': {
|
||||||
id: 'new-api',
|
id: 'new-api',
|
||||||
name: 'New API',
|
name: 'New API',
|
||||||
type: 'openai',
|
type: 'new-api',
|
||||||
apiKey: '',
|
apiKey: '',
|
||||||
apiHost: 'http://localhost:3000',
|
apiHost: 'http://localhost:3000',
|
||||||
anthropicApiHost: 'http://localhost:3000',
|
anthropicApiHost: 'http://localhost:3000',
|
||||||
@ -1424,5 +1424,5 @@ export const isGeminiWebSearchProvider = (provider: Provider) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const isNewApiProvider = (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 { useCallback } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import useSWR from 'swr'
|
import useSWR from 'swr'
|
||||||
|
|
||||||
|
import { useApiServer } from '../useApiServer'
|
||||||
import { useAgentClient } from './useAgentClient'
|
import { useAgentClient } from './useAgentClient'
|
||||||
|
|
||||||
export const useAgent = (id: string | null) => {
|
export const useAgent = (id: string | null) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
const client = useAgentClient()
|
const client = useAgentClient()
|
||||||
const key = id ? client.agentPaths.withId(id) : null
|
const key = id ? client.agentPaths.withId(id) : null
|
||||||
|
const { apiServerConfig, apiServerRunning } = useApiServer()
|
||||||
const fetcher = useCallback(async () => {
|
const fetcher = useCallback(async () => {
|
||||||
if (!id || id === 'fake') {
|
if (!id || id === 'fake') {
|
||||||
return null
|
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)
|
const result = await client.getAgent(id)
|
||||||
return result
|
return result
|
||||||
}, [client, id])
|
}, [apiServerConfig.enabled, apiServerRunning, client, id, t])
|
||||||
const { data, error, isLoading } = useSWR(key, id ? fetcher : null)
|
const { data, error, isLoading } = useSWR(key, id ? fetcher : null)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { useCallback } from 'react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import useSWR from 'swr'
|
import useSWR from 'swr'
|
||||||
|
|
||||||
|
import { useApiServer } from '../useApiServer'
|
||||||
import { useRuntime } from '../useRuntime'
|
import { useRuntime } from '../useRuntime'
|
||||||
import { useAgentClient } from './useAgentClient'
|
import { useAgentClient } from './useAgentClient'
|
||||||
|
|
||||||
@ -23,11 +24,18 @@ export const useAgents = () => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const client = useAgentClient()
|
const client = useAgentClient()
|
||||||
const key = client.agentPaths.base
|
const key = client.agentPaths.base
|
||||||
|
const { apiServerConfig, apiServerRunning } = useApiServer()
|
||||||
const fetcher = useCallback(async () => {
|
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()
|
const result = await client.listAgents()
|
||||||
// NOTE: We only use the array for now. useUpdateAgent depends on this behavior.
|
// NOTE: We only use the array for now. useUpdateAgent depends on this behavior.
|
||||||
return result.data
|
return result.data
|
||||||
}, [client])
|
}, [apiServerConfig.enabled, apiServerRunning, client, t])
|
||||||
const { data, error, isLoading, mutate } = useSWR(key, fetcher)
|
const { data, error, isLoading, mutate } = useSWR(key, fetcher)
|
||||||
const { chat } = useRuntime()
|
const { chat } = useRuntime()
|
||||||
const { activeAgentId } = chat
|
const { activeAgentId } = chat
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export const useSessions = (agentId: string) => {
|
|||||||
async (form: CreateSessionForm) => {
|
async (form: CreateSessionForm) => {
|
||||||
try {
|
try {
|
||||||
const result = await client.createSession(agentId, form)
|
const result = await client.createSession(agentId, form)
|
||||||
await mutate((prev) => [...(prev ?? []), result], { revalidate: false })
|
await mutate((prev) => [result, ...(prev ?? [])], { revalidate: false })
|
||||||
return result
|
return result
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.toast.error(formatErrorMessageWithPrefix(error, t('agent.session.create.error.failed')))
|
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."
|
"failed": "Failed to list agents."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"server": {
|
||||||
|
"error": {
|
||||||
|
"not_running": "The API server is enabled but not running properly."
|
||||||
|
}
|
||||||
|
},
|
||||||
"session": {
|
"session": {
|
||||||
"accessible_paths": {
|
"accessible_paths": {
|
||||||
"add": "Add directory",
|
"add": "Add directory",
|
||||||
@ -237,6 +242,7 @@
|
|||||||
"messages": {
|
"messages": {
|
||||||
"apiKeyCopied": "API Key copied to clipboard",
|
"apiKeyCopied": "API Key copied to clipboard",
|
||||||
"apiKeyRegenerated": "API Key regenerated",
|
"apiKeyRegenerated": "API Key regenerated",
|
||||||
|
"notEnabled": "The API Server is not enabled.",
|
||||||
"operationFailed": "API Server operation failed: ",
|
"operationFailed": "API Server operation failed: ",
|
||||||
"restartError": "Failed to restart API Server: ",
|
"restartError": "Failed to restart API Server: ",
|
||||||
"restartFailed": "API Server restart failed: ",
|
"restartFailed": "API Server restart failed: ",
|
||||||
|
|||||||
@ -30,6 +30,11 @@
|
|||||||
"failed": "获取智能体列表失败"
|
"failed": "获取智能体列表失败"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"server": {
|
||||||
|
"error": {
|
||||||
|
"not_running": "API 服务器已启用但未正常运行。"
|
||||||
|
}
|
||||||
|
},
|
||||||
"session": {
|
"session": {
|
||||||
"accessible_paths": {
|
"accessible_paths": {
|
||||||
"add": "添加目录",
|
"add": "添加目录",
|
||||||
@ -237,6 +242,7 @@
|
|||||||
"messages": {
|
"messages": {
|
||||||
"apiKeyCopied": "API 密钥已复制到剪贴板",
|
"apiKeyCopied": "API 密钥已复制到剪贴板",
|
||||||
"apiKeyRegenerated": "API 密钥已重新生成",
|
"apiKeyRegenerated": "API 密钥已重新生成",
|
||||||
|
"notEnabled": "API 服务器未启用。",
|
||||||
"operationFailed": "API 服务器操作失败:",
|
"operationFailed": "API 服务器操作失败:",
|
||||||
"restartError": "重启 API 服务器失败:",
|
"restartError": "重启 API 服务器失败:",
|
||||||
"restartFailed": "API 服务器重启失败:",
|
"restartFailed": "API 服务器重启失败:",
|
||||||
|
|||||||
@ -30,6 +30,11 @@
|
|||||||
"failed": "無法列出代理程式。"
|
"failed": "無法列出代理程式。"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"server": {
|
||||||
|
"error": {
|
||||||
|
"not_running": "API 伺服器已啟用,但運行不正常。"
|
||||||
|
}
|
||||||
|
},
|
||||||
"session": {
|
"session": {
|
||||||
"accessible_paths": {
|
"accessible_paths": {
|
||||||
"add": "新增目錄",
|
"add": "新增目錄",
|
||||||
@ -237,6 +242,7 @@
|
|||||||
"messages": {
|
"messages": {
|
||||||
"apiKeyCopied": "API 金鑰已複製到剪貼簿",
|
"apiKeyCopied": "API 金鑰已複製到剪貼簿",
|
||||||
"apiKeyRegenerated": "API 金鑰已重新生成",
|
"apiKeyRegenerated": "API 金鑰已重新生成",
|
||||||
|
"notEnabled": "API 伺服器未啟用。",
|
||||||
"operationFailed": "API 伺服器操作失敗:",
|
"operationFailed": "API 伺服器操作失敗:",
|
||||||
"restartError": "重新啟動 API 伺服器失敗:",
|
"restartError": "重新啟動 API 伺服器失敗:",
|
||||||
"restartFailed": "API 伺服器重新啟動失敗:",
|
"restartFailed": "API 伺服器重新啟動失敗:",
|
||||||
|
|||||||
@ -30,6 +30,11 @@
|
|||||||
"failed": "Αποτυχία καταχώρησης πρακτόρων."
|
"failed": "Αποτυχία καταχώρησης πρακτόρων."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"server": {
|
||||||
|
"error": {
|
||||||
|
"not_running": "Ο διακομιστής API είναι ενεργοποιημένος αλλά δεν λειτουργεί σωστά."
|
||||||
|
}
|
||||||
|
},
|
||||||
"session": {
|
"session": {
|
||||||
"accessible_paths": {
|
"accessible_paths": {
|
||||||
"add": "Προσθήκη καταλόγου",
|
"add": "Προσθήκη καταλόγου",
|
||||||
@ -237,6 +242,7 @@
|
|||||||
"messages": {
|
"messages": {
|
||||||
"apiKeyCopied": "Το κλειδί API αντιγράφηκε στο πρόχειρο",
|
"apiKeyCopied": "Το κλειδί API αντιγράφηκε στο πρόχειρο",
|
||||||
"apiKeyRegenerated": "Το κλειδί API αναδημιουργήθηκε",
|
"apiKeyRegenerated": "Το κλειδί API αναδημιουργήθηκε",
|
||||||
|
"notEnabled": "Ο διακομιστής API δεν είναι ενεργοποιημένος.",
|
||||||
"operationFailed": "Η λειτουργία του Διακομιστή API απέτυχε: ",
|
"operationFailed": "Η λειτουργία του Διακομιστή API απέτυχε: ",
|
||||||
"restartError": "Αποτυχία επανεκκίνησης του Διακομιστή API: ",
|
"restartError": "Αποτυχία επανεκκίνησης του Διακομιστή API: ",
|
||||||
"restartFailed": "Η επανεκκίνηση του Διακομιστή API απέτυχε: ",
|
"restartFailed": "Η επανεκκίνηση του Διακομιστή API απέτυχε: ",
|
||||||
|
|||||||
@ -30,6 +30,11 @@
|
|||||||
"failed": "Error al listar agentes."
|
"failed": "Error al listar agentes."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"server": {
|
||||||
|
"error": {
|
||||||
|
"not_running": "El servidor de API está habilitado pero no funciona correctamente."
|
||||||
|
}
|
||||||
|
},
|
||||||
"session": {
|
"session": {
|
||||||
"accessible_paths": {
|
"accessible_paths": {
|
||||||
"add": "Agregar directorio",
|
"add": "Agregar directorio",
|
||||||
@ -237,6 +242,7 @@
|
|||||||
"messages": {
|
"messages": {
|
||||||
"apiKeyCopied": "Clave API copiada al portapapeles",
|
"apiKeyCopied": "Clave API copiada al portapapeles",
|
||||||
"apiKeyRegenerated": "Clave API regenerada",
|
"apiKeyRegenerated": "Clave API regenerada",
|
||||||
|
"notEnabled": "El servidor de API no está habilitado.",
|
||||||
"operationFailed": "Falló la operación del Servidor API: ",
|
"operationFailed": "Falló la operación del Servidor API: ",
|
||||||
"restartError": "Error al reiniciar el Servidor API: ",
|
"restartError": "Error al reiniciar el Servidor API: ",
|
||||||
"restartFailed": "Falló el reinicio del Servidor API: ",
|
"restartFailed": "Falló el reinicio del Servidor API: ",
|
||||||
|
|||||||
@ -30,6 +30,11 @@
|
|||||||
"failed": "Échec de la liste des agents."
|
"failed": "Échec de la liste des agents."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"server": {
|
||||||
|
"error": {
|
||||||
|
"not_running": "Le serveur API est activé mais ne fonctionne pas correctement."
|
||||||
|
}
|
||||||
|
},
|
||||||
"session": {
|
"session": {
|
||||||
"accessible_paths": {
|
"accessible_paths": {
|
||||||
"add": "Ajouter un répertoire",
|
"add": "Ajouter un répertoire",
|
||||||
@ -237,6 +242,7 @@
|
|||||||
"messages": {
|
"messages": {
|
||||||
"apiKeyCopied": "Clé API copiée dans le presse-papiers",
|
"apiKeyCopied": "Clé API copiée dans le presse-papiers",
|
||||||
"apiKeyRegenerated": "Clé API régénérée",
|
"apiKeyRegenerated": "Clé API régénérée",
|
||||||
|
"notEnabled": "Le serveur API n'est pas activé.",
|
||||||
"operationFailed": "Opération du Serveur API échouée : ",
|
"operationFailed": "Opération du Serveur API échouée : ",
|
||||||
"restartError": "Échec du redémarrage du Serveur API : ",
|
"restartError": "Échec du redémarrage du Serveur API : ",
|
||||||
"restartFailed": "Redémarrage du Serveur API échoué : ",
|
"restartFailed": "Redémarrage du Serveur API échoué : ",
|
||||||
|
|||||||
@ -30,6 +30,11 @@
|
|||||||
"failed": "エージェントの一覧取得に失敗しました。"
|
"failed": "エージェントの一覧取得に失敗しました。"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"server": {
|
||||||
|
"error": {
|
||||||
|
"not_running": "APIサーバーは有効になっていますが、正常に動作していません。"
|
||||||
|
}
|
||||||
|
},
|
||||||
"session": {
|
"session": {
|
||||||
"accessible_paths": {
|
"accessible_paths": {
|
||||||
"add": "ディレクトリを追加",
|
"add": "ディレクトリを追加",
|
||||||
@ -237,6 +242,7 @@
|
|||||||
"messages": {
|
"messages": {
|
||||||
"apiKeyCopied": "API キーがクリップボードにコピーされました",
|
"apiKeyCopied": "API キーがクリップボードにコピーされました",
|
||||||
"apiKeyRegenerated": "API キーが再生成されました",
|
"apiKeyRegenerated": "API キーが再生成されました",
|
||||||
|
"notEnabled": "APIサーバーが有効になっていません。",
|
||||||
"operationFailed": "API サーバーの操作に失敗しました:",
|
"operationFailed": "API サーバーの操作に失敗しました:",
|
||||||
"restartError": "API サーバーの再起動に失敗しました:",
|
"restartError": "API サーバーの再起動に失敗しました:",
|
||||||
"restartFailed": "API サーバーの再起動に失敗しました:",
|
"restartFailed": "API サーバーの再起動に失敗しました:",
|
||||||
|
|||||||
@ -30,6 +30,11 @@
|
|||||||
"failed": "Falha ao listar agentes."
|
"failed": "Falha ao listar agentes."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"server": {
|
||||||
|
"error": {
|
||||||
|
"not_running": "O servidor de API está habilitado, mas não está funcionando corretamente."
|
||||||
|
}
|
||||||
|
},
|
||||||
"session": {
|
"session": {
|
||||||
"accessible_paths": {
|
"accessible_paths": {
|
||||||
"add": "Adicionar diretório",
|
"add": "Adicionar diretório",
|
||||||
@ -237,6 +242,7 @@
|
|||||||
"messages": {
|
"messages": {
|
||||||
"apiKeyCopied": "Chave API copiada para a área de transferência",
|
"apiKeyCopied": "Chave API copiada para a área de transferência",
|
||||||
"apiKeyRegenerated": "Chave API regenerada",
|
"apiKeyRegenerated": "Chave API regenerada",
|
||||||
|
"notEnabled": "O Servidor de API não está habilitado.",
|
||||||
"operationFailed": "Operação do Servidor API falhou: ",
|
"operationFailed": "Operação do Servidor API falhou: ",
|
||||||
"restartError": "Falha ao reiniciar o Servidor API: ",
|
"restartError": "Falha ao reiniciar o Servidor API: ",
|
||||||
"restartFailed": "Reinício do Servidor API falhou: ",
|
"restartFailed": "Reinício do Servidor API falhou: ",
|
||||||
|
|||||||
@ -30,6 +30,11 @@
|
|||||||
"failed": "Не удалось получить список агентов."
|
"failed": "Не удалось получить список агентов."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"server": {
|
||||||
|
"error": {
|
||||||
|
"not_running": "API-сервер включен, но работает неправильно."
|
||||||
|
}
|
||||||
|
},
|
||||||
"session": {
|
"session": {
|
||||||
"accessible_paths": {
|
"accessible_paths": {
|
||||||
"add": "Добавить каталог",
|
"add": "Добавить каталог",
|
||||||
@ -237,6 +242,7 @@
|
|||||||
"messages": {
|
"messages": {
|
||||||
"apiKeyCopied": "API ключ скопирован в буфер обмена",
|
"apiKeyCopied": "API ключ скопирован в буфер обмена",
|
||||||
"apiKeyRegenerated": "API ключ перегенерирован",
|
"apiKeyRegenerated": "API ключ перегенерирован",
|
||||||
|
"notEnabled": "API-сервер не включен.",
|
||||||
"operationFailed": "Операция API сервера не удалась: ",
|
"operationFailed": "Операция API сервера не удалась: ",
|
||||||
"restartError": "Не удалось перезапустить API сервер: ",
|
"restartError": "Не удалось перезапустить API сервер: ",
|
||||||
"restartFailed": "Перезапуск API сервера не удался: ",
|
"restartFailed": "Перезапуск API сервера не удался: ",
|
||||||
|
|||||||
@ -263,7 +263,9 @@ const AgentSessionInputbar: FC<Props> = ({ agentId, sessionId }) => {
|
|||||||
value={text}
|
value={text}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder={t('chat.input.placeholder', { key: getSendMessageShortcutLabel(sendMessageShortcut) })}
|
placeholder={t('chat.input.placeholder_without_triggers', {
|
||||||
|
key: getSendMessageShortcutLabel(sendMessageShortcut)
|
||||||
|
})}
|
||||||
autoFocus
|
autoFocus
|
||||||
variant="borderless"
|
variant="borderless"
|
||||||
spellCheck={enableSpellCheck}
|
spellCheck={enableSpellCheck}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { Button } from '@heroui/button'
|
import { Button } from '@heroui/button'
|
||||||
import CodeViewer from '@renderer/components/CodeViewer'
|
import CodeViewer from '@renderer/components/CodeViewer'
|
||||||
|
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
|
||||||
import { useTimer } from '@renderer/hooks/useTimer'
|
import { useTimer } from '@renderer/hooks/useTimer'
|
||||||
import { getHttpMessageLabel, getProviderLabel } from '@renderer/i18n/label'
|
import { getHttpMessageLabel, getProviderLabel } from '@renderer/i18n/label'
|
||||||
import { getProviderById } from '@renderer/services/ProviderService'
|
import { getProviderById } from '@renderer/services/ProviderService'
|
||||||
@ -33,7 +34,7 @@ import {
|
|||||||
import type { ErrorMessageBlock, Message } from '@renderer/types/newMessage'
|
import type { ErrorMessageBlock, Message } from '@renderer/types/newMessage'
|
||||||
import { formatAiSdkError, formatError, safeToString } from '@renderer/utils/error'
|
import { formatAiSdkError, formatError, safeToString } from '@renderer/utils/error'
|
||||||
import { Alert as AntdAlert, Modal } from 'antd'
|
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 { Trans, useTranslation } from 'react-i18next'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
@ -303,14 +304,36 @@ const BuiltinError = ({ error }: { error: SerializedError }) => {
|
|||||||
// 作为 base,渲染公共字段,应当在 ErrorDetailList 中渲染
|
// 作为 base,渲染公共字段,应当在 ErrorDetailList 中渲染
|
||||||
const AiSdkErrorBase = ({ error }: { error: SerializedAiSdkError }) => {
|
const AiSdkErrorBase = ({ error }: { error: SerializedAiSdkError }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { highlightCode } = useCodeStyle()
|
||||||
|
const [highlightedString, setHighlightedString] = useState('')
|
||||||
const cause = error.cause
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<BuiltinError error={error} />
|
<BuiltinError error={error} />
|
||||||
{cause && (
|
{cause && (
|
||||||
<ErrorDetailItem>
|
<ErrorDetailItem>
|
||||||
<ErrorDetailLabel>{t('error.cause')}:</ErrorDetailLabel>
|
<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>
|
</ErrorDetailItem>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -62,7 +62,7 @@ const MessageImage: FC<Props> = ({ block }) => {
|
|||||||
byteArrays.push(byteArray)
|
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 })])
|
await navigator.clipboard.write([new ClipboardItem({ [mimeType]: blob })])
|
||||||
} else {
|
} else {
|
||||||
throw new Error('无效的 base64 图片格式')
|
throw new Error('无效的 base64 图片格式')
|
||||||
|
|||||||
@ -1,15 +1,16 @@
|
|||||||
import { Alert, Spinner } from '@heroui/react'
|
import { Alert, Spinner } from '@heroui/react'
|
||||||
import Scrollbar from '@renderer/components/Scrollbar'
|
import Scrollbar from '@renderer/components/Scrollbar'
|
||||||
import { useAgents } from '@renderer/hooks/agents/useAgents'
|
import { useAgents } from '@renderer/hooks/agents/useAgents'
|
||||||
|
import { useApiServer } from '@renderer/hooks/useApiServer'
|
||||||
import { useAssistants } from '@renderer/hooks/useAssistant'
|
import { useAssistants } from '@renderer/hooks/useAssistant'
|
||||||
import { useAssistantPresets } from '@renderer/hooks/useAssistantPresets'
|
import { useAssistantPresets } from '@renderer/hooks/useAssistantPresets'
|
||||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
|
||||||
import { useAssistantsTabSortType } from '@renderer/hooks/useStore'
|
import { useAssistantsTabSortType } from '@renderer/hooks/useStore'
|
||||||
import { useTags } from '@renderer/hooks/useTags'
|
import { useTags } from '@renderer/hooks/useTags'
|
||||||
import { useAppDispatch } from '@renderer/store'
|
import { useAppDispatch } from '@renderer/store'
|
||||||
import { addIknowAction } from '@renderer/store/runtime'
|
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 { AssistantTabSortType } from '@shared/data/preference/preferenceTypes'
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import { useCallback, useEffect, useRef, useState } 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 { activeAssistant, setActiveAssistant, onCreateAssistant, onCreateDefaultAssistant } = props
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { apiServer } = useSettings()
|
const { apiServerConfig, apiServerRunning } = useApiServer()
|
||||||
|
const apiServerEnabled = apiServerConfig.enabled
|
||||||
const { iknow, chat } = useRuntime()
|
const { iknow, chat } = useRuntime()
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
@ -57,7 +59,7 @@ const AssistantsTab: FC<AssistantsTabProps> = (props) => {
|
|||||||
const { unifiedItems, handleUnifiedListReorder } = useUnifiedItems({
|
const { unifiedItems, handleUnifiedListReorder } = useUnifiedItems({
|
||||||
agents,
|
agents,
|
||||||
assistants,
|
assistants,
|
||||||
apiServerEnabled: apiServer.enabled,
|
apiServerEnabled,
|
||||||
agentsLoading,
|
agentsLoading,
|
||||||
agentsError,
|
agentsError,
|
||||||
updateAssistants
|
updateAssistants
|
||||||
@ -74,17 +76,17 @@ const AssistantsTab: FC<AssistantsTabProps> = (props) => {
|
|||||||
unifiedItems,
|
unifiedItems,
|
||||||
assistants,
|
assistants,
|
||||||
agents,
|
agents,
|
||||||
apiServerEnabled: apiServer.enabled,
|
apiServerEnabled,
|
||||||
agentsLoading,
|
agentsLoading,
|
||||||
agentsError,
|
agentsError,
|
||||||
updateAssistants
|
updateAssistants
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!agentsLoading && agents.length > 0 && !activeAgentId && apiServer.enabled) {
|
if (!agentsLoading && agents.length > 0 && !activeAgentId && apiServerConfig.enabled) {
|
||||||
setActiveAgentId(agents[0].id)
|
setActiveAgentId(agents[0].id)
|
||||||
}
|
}
|
||||||
}, [agentsLoading, agents, activeAgentId, setActiveAgentId, apiServer.enabled])
|
}, [agentsLoading, agents, activeAgentId, setActiveAgentId, apiServerConfig.enabled])
|
||||||
|
|
||||||
const onDeleteAssistant = useCallback(
|
const onDeleteAssistant = useCallback(
|
||||||
(assistant: Assistant) => {
|
(assistant: Assistant) => {
|
||||||
@ -107,7 +109,7 @@ const AssistantsTab: FC<AssistantsTabProps> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className="assistants-tab" ref={containerRef}>
|
<Container className="assistants-tab" ref={containerRef}>
|
||||||
{!apiServer.enabled && !iknow[ALERT_KEY] && (
|
{!apiServerConfig.enabled && !iknow[ALERT_KEY] && (
|
||||||
<Alert
|
<Alert
|
||||||
color="warning"
|
color="warning"
|
||||||
title={t('agent.warning.enable_server')}
|
title={t('agent.warning.enable_server')}
|
||||||
@ -115,11 +117,22 @@ const AssistantsTab: FC<AssistantsTabProps> = (props) => {
|
|||||||
onClose={() => {
|
onClose={() => {
|
||||||
dispatch(addIknowAction(ALERT_KEY))
|
dispatch(addIknowAction(ALERT_KEY))
|
||||||
}}
|
}}
|
||||||
|
className="mb-2"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{agentsLoading && <Spinner />}
|
{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' ? (
|
{assistantsTabSortType === 'tags' ? (
|
||||||
<UnifiedTagGroups
|
<UnifiedTagGroups
|
||||||
|
|||||||
@ -297,13 +297,7 @@ export const Topics: React.FC<Props> = ({ assistant: _assistant, activeTopic, se
|
|||||||
label: t('chat.topics.clear.title'),
|
label: t('chat.topics.clear.title'),
|
||||||
key: 'clear-messages',
|
key: 'clear-messages',
|
||||||
icon: <BrushCleaning size={14} />,
|
icon: <BrushCleaning size={14} />,
|
||||||
async onClick() {
|
onClick: () => onClearMessages(topic)
|
||||||
window.modal.confirm({
|
|
||||||
title: t('chat.input.clear.content'),
|
|
||||||
centered: true,
|
|
||||||
onOk: () => onClearMessages(topic)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('settings.topic.position.label'),
|
label: t('settings.topic.position.label'),
|
||||||
|
|||||||
@ -47,15 +47,58 @@ const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, app
|
|||||||
setActiveIndex(0)
|
setActiveIndex(0)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const stopSearch = useCallback(() => {
|
const ensureWebviewReady = useCallback(
|
||||||
const target = webviewRef.current ?? attachedWebviewRef.current
|
(candidate: WebviewTag | null) => {
|
||||||
if (!target) return
|
if (!candidate) return null
|
||||||
try {
|
try {
|
||||||
target.stopFindInPage('clearSelection')
|
const webContentsId = candidate.getWebContentsId?.()
|
||||||
} catch (error) {
|
if (!webContentsId) {
|
||||||
logger.error('stopFindInPage failed', { error })
|
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(() => {
|
const closeSearch = useCallback(() => {
|
||||||
setIsVisible(false)
|
setIsVisible(false)
|
||||||
@ -65,7 +108,7 @@ const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, app
|
|||||||
|
|
||||||
const performSearch = useCallback(
|
const performSearch = useCallback(
|
||||||
(text: string, options?: Electron.FindInPageOptions) => {
|
(text: string, options?: Electron.FindInPageOptions) => {
|
||||||
const target = webviewRef.current ?? attachedWebviewRef.current
|
const target = getUsableWebview()
|
||||||
if (!target) {
|
if (!target) {
|
||||||
logger.debug('Skip performSearch: webview not attached')
|
logger.debug('Skip performSearch: webview not attached')
|
||||||
return
|
return
|
||||||
@ -82,7 +125,7 @@ const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, app
|
|||||||
window.toast?.error(t('common.error'))
|
window.toast?.error(t('common.error'))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[resetSearchState, stopSearch, t, webviewRef]
|
[getUsableWebview, resetSearchState, stopSearch, t]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleFoundInPage = useCallback((event: Event & { result?: FoundInPageResult }) => {
|
const handleFoundInPage = useCallback((event: Event & { result?: FoundInPageResult }) => {
|
||||||
@ -130,22 +173,26 @@ const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, app
|
|||||||
return () => {
|
return () => {
|
||||||
activeWebview.removeEventListener('found-in-page', handle)
|
activeWebview.removeEventListener('found-in-page', handle)
|
||||||
if (attachedWebviewRef.current === activeWebview) {
|
if (attachedWebviewRef.current === activeWebview) {
|
||||||
try {
|
stopFindOnWebview(activeWebview)
|
||||||
activeWebview.stopFindInPage('clearSelection')
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('stopFindInPage failed', { error })
|
|
||||||
}
|
|
||||||
attachedWebviewRef.current = null
|
attachedWebviewRef.current = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [activeWebview, handleFoundInPage])
|
}, [activeWebview, handleFoundInPage, stopFindOnWebview])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeWebview) return
|
if (!activeWebview) return
|
||||||
|
if (!isWebviewReady) return
|
||||||
const onFindShortcut = window.api?.webview?.onFindShortcut
|
const onFindShortcut = window.api?.webview?.onFindShortcut
|
||||||
if (!onFindShortcut) return
|
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) {
|
if (!webContentsId) {
|
||||||
logger.warn('WebviewSearch: missing webContentsId', { appId })
|
logger.warn('WebviewSearch: missing webContentsId', { appId })
|
||||||
return
|
return
|
||||||
@ -178,7 +225,7 @@ const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, app
|
|||||||
return () => {
|
return () => {
|
||||||
unsubscribe?.()
|
unsubscribe?.()
|
||||||
}
|
}
|
||||||
}, [appId, activeWebview, closeSearch, goToNext, goToPrevious, isVisible, openSearch])
|
}, [appId, activeWebview, closeSearch, goToNext, goToPrevious, isVisible, isWebviewReady, openSearch])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isVisible) return
|
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 () => {
|
it('closes the search overlay when escape is forwarded from the webview', async () => {
|
||||||
const { webview } = createWebviewMock()
|
const { webview } = createWebviewMock()
|
||||||
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
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 { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
import { usePaintings } from '@renderer/hooks/usePaintings'
|
import { usePaintings } from '@renderer/hooks/usePaintings'
|
||||||
import { useAllProviders } from '@renderer/hooks/useProvider'
|
import { useAllProviders } from '@renderer/hooks/useProvider'
|
||||||
import { getProviderLabel } from '@renderer/i18n/label'
|
|
||||||
import FileManager from '@renderer/services/FileManager'
|
import FileManager from '@renderer/services/FileManager'
|
||||||
import { translateText } from '@renderer/services/TranslateService'
|
import { translateText } from '@renderer/services/TranslateService'
|
||||||
import type { FileMetadata } from '@renderer/types'
|
import type { FileMetadata } from '@renderer/types'
|
||||||
@ -32,6 +31,7 @@ import SendMessageButton from '../home/Inputbar/SendMessageButton'
|
|||||||
import { SettingHelpLink, SettingTitle } from '../settings'
|
import { SettingHelpLink, SettingTitle } from '../settings'
|
||||||
import Artboard from './components/Artboard'
|
import Artboard from './components/Artboard'
|
||||||
import PaintingsList from './components/PaintingsList'
|
import PaintingsList from './components/PaintingsList'
|
||||||
|
import ProviderSelect from './components/ProviderSelect'
|
||||||
import { type ConfigItem, createModeConfigs, DEFAULT_PAINTING } from './config/aihubmixConfig'
|
import { type ConfigItem, createModeConfigs, DEFAULT_PAINTING } from './config/aihubmixConfig'
|
||||||
import { checkProviderEnabled } from './utils'
|
import { checkProviderEnabled } from './utils'
|
||||||
|
|
||||||
@ -73,20 +73,6 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const providers = useAllProviders()
|
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 [generating, setGenerating] = useCache('chat.generating')
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
@ -841,21 +827,12 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
|
|||||||
/>
|
/>
|
||||||
</SettingHelpLink>
|
</SettingHelpLink>
|
||||||
</ProviderTitleContainer>
|
</ProviderTitleContainer>
|
||||||
|
<ProviderSelect
|
||||||
<Select value={providerOptions[1].value} onChange={handleProviderChange} style={{ marginBottom: 15 }}>
|
provider={aihubmixProvider}
|
||||||
{providerOptions.map((provider) => (
|
options={Options}
|
||||||
<Select.Option value={provider.value} key={provider.value}>
|
onChange={handleProviderChange}
|
||||||
<SelectOptionContainer>
|
className={'mb-4'}
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* 使用JSON配置渲染设置项 */}
|
{/* 使用JSON配置渲染设置项 */}
|
||||||
{modeConfigs[mode].filter((item) => (item.condition ? item.condition(painting) : true)).map(renderConfigItem)}
|
{modeConfigs[mode].filter((item) => (item.condition ? item.condition(painting) : true)).map(renderConfigItem)}
|
||||||
@ -1013,12 +990,6 @@ const ModeSegmentedContainer = styled.div`
|
|||||||
padding-top: 24px;
|
padding-top: 24px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const SelectOptionContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
`
|
|
||||||
|
|
||||||
// 添加新的样式组件
|
// 添加新的样式组件
|
||||||
const ProviderTitleContainer = styled.div`
|
const ProviderTitleContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import { isMac } from '@renderer/config/constant'
|
|||||||
import { getProviderLogo } from '@renderer/config/providers'
|
import { getProviderLogo } from '@renderer/config/providers'
|
||||||
import { usePaintings } from '@renderer/hooks/usePaintings'
|
import { usePaintings } from '@renderer/hooks/usePaintings'
|
||||||
import { useAllProviders } from '@renderer/hooks/useProvider'
|
import { useAllProviders } from '@renderer/hooks/useProvider'
|
||||||
import { getProviderLabel } from '@renderer/i18n/label'
|
|
||||||
import FileManager from '@renderer/services/FileManager'
|
import FileManager from '@renderer/services/FileManager'
|
||||||
import type { FileMetadata } from '@renderer/types'
|
import type { FileMetadata } from '@renderer/types'
|
||||||
import { convertToBase64, uuid } from '@renderer/utils'
|
import { convertToBase64, uuid } from '@renderer/utils'
|
||||||
@ -27,6 +26,7 @@ import { SettingHelpLink, SettingTitle } from '../settings'
|
|||||||
import Artboard from './components/Artboard'
|
import Artboard from './components/Artboard'
|
||||||
import ImageUploader from './components/ImageUploader'
|
import ImageUploader from './components/ImageUploader'
|
||||||
import PaintingsList from './components/PaintingsList'
|
import PaintingsList from './components/PaintingsList'
|
||||||
|
import ProviderSelect from './components/ProviderSelect'
|
||||||
import {
|
import {
|
||||||
COURSE_URL,
|
COURSE_URL,
|
||||||
DEFAULT_PAINTING,
|
DEFAULT_PAINTING,
|
||||||
@ -44,20 +44,6 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
|||||||
const [painting, setPainting] = useState<DmxapiPainting>(dmxapi_paintings?.[0] || DEFAULT_PAINTING)
|
const [painting, setPainting] = useState<DmxapiPainting>(dmxapi_paintings?.[0] || DEFAULT_PAINTING)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const providers = useAllProviders()
|
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')!
|
const dmxapiProvider = providers.find((p) => p.id === 'dmxapi')!
|
||||||
|
|
||||||
@ -782,9 +768,9 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
|||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Navbar>
|
<Navbar>
|
||||||
<NavbarCenter style={{ borderRight: 'none' }}>{t('paintings.title')}</NavbarCenter>
|
<NavbarCenter className="border-r-0">{t('paintings.title')}</NavbarCenter>
|
||||||
{isMac && (
|
{isMac && (
|
||||||
<NavbarRight style={{ justifyContent: 'flex-end' }}>
|
<NavbarRight className="justify-end">
|
||||||
<Button size="sm" className="nodrag" startContent={<PlusOutlined />} onPress={createNewPainting}>
|
<Button size="sm" className="nodrag" startContent={<PlusOutlined />} onPress={createNewPainting}>
|
||||||
{t('paintings.button.new.image')}
|
{t('paintings.button.new.image')}
|
||||||
</Button>
|
</Button>
|
||||||
@ -794,7 +780,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
|||||||
<ContentContainer id="content-container">
|
<ContentContainer id="content-container">
|
||||||
<LeftContainer>
|
<LeftContainer>
|
||||||
<ProviderTitleContainer>
|
<ProviderTitleContainer>
|
||||||
<SettingTitle style={{ marginBottom: 5 }}>{t('common.provider')}</SettingTitle>
|
<SettingTitle className="mb-1">{t('common.provider')}</SettingTitle>
|
||||||
<div>
|
<div>
|
||||||
<SettingHelpLink target="_blank" href={COURSE_URL}>
|
<SettingHelpLink target="_blank" href={COURSE_URL}>
|
||||||
{t('paintings.paint_course')}
|
{t('paintings.paint_course')}
|
||||||
@ -802,32 +788,19 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
|||||||
<SettingHelpLink target="_blank" href={TOP_UP_URL}>
|
<SettingHelpLink target="_blank" href={TOP_UP_URL}>
|
||||||
{t('paintings.top_up')}
|
{t('paintings.top_up')}
|
||||||
</SettingHelpLink>
|
</SettingHelpLink>
|
||||||
<Avatar
|
<ProviderLogo radius="md" src={getProviderLogo(dmxapiProvider.id)} className="ml-1" />
|
||||||
radius="md"
|
|
||||||
src={getProviderLogo(dmxapiProvider.id)}
|
|
||||||
className="h-4 w-4 border-[0.5px] border-[var(--color-border)]"
|
|
||||||
style={{ marginLeft: 5 }}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</ProviderTitleContainer>
|
</ProviderTitleContainer>
|
||||||
<Select value={providerOptions[3].value} onChange={handleProviderChange} style={{ marginBottom: 15 }}>
|
<ProviderSelect
|
||||||
{providerOptions.map((provider) => (
|
provider={dmxapiProvider}
|
||||||
<Select.Option value={provider.value} key={provider.value}>
|
options={Options}
|
||||||
<SelectOptionContainer>
|
onChange={handleProviderChange}
|
||||||
<Avatar
|
className="mb-4"
|
||||||
radius="md"
|
/>
|
||||||
src={getProviderLogo(provider.value || '')}
|
|
||||||
className="h-4 w-4 border-[0.5px] border-[var(--color-border)]"
|
|
||||||
/>
|
|
||||||
{provider.label}
|
|
||||||
</SelectOptionContainer>
|
|
||||||
</Select.Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
{painting.generationMode &&
|
{painting.generationMode &&
|
||||||
[generationModeType.EDIT, generationModeType.MERGE].includes(painting.generationMode) && (
|
[generationModeType.EDIT, generationModeType.MERGE].includes(painting.generationMode) && (
|
||||||
<>
|
<>
|
||||||
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>参考图</SettingTitle>
|
<SettingTitle className="mt-4 mb-1">参考图</SettingTitle>
|
||||||
<ImageUploader
|
<ImageUploader
|
||||||
fileMap={fileMap}
|
fileMap={fileMap}
|
||||||
maxImages={painting.generationMode === generationModeType.EDIT ? 1 : 3}
|
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>
|
{t('common.model')} <SettingPrice>{painting.priceModel !== '0' ? painting.priceModel : ''}</SettingPrice>
|
||||||
</SettingTitle>
|
</SettingTitle>
|
||||||
<Select
|
<Select
|
||||||
value={painting.model}
|
value={painting.model}
|
||||||
onChange={onSelectModel}
|
onChange={onSelectModel}
|
||||||
style={{ width: '100%' }}
|
className="w-full"
|
||||||
loading={isLoadingModels}
|
loading={isLoadingModels}
|
||||||
placeholder={isLoadingModels ? t('common.loading') : t('paintings.select_model')}>
|
placeholder={isLoadingModels ? t('common.loading') : t('paintings.select_model')}>
|
||||||
{Object.entries(modelOptions).map(([provider, models]) => {
|
{Object.entries(modelOptions).map(([provider, models]) => {
|
||||||
@ -862,11 +835,11 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
|||||||
})}
|
})}
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('paintings.image.size')}</SettingTitle>
|
<SettingTitle className="mt-4 mb-1">{t('paintings.image.size')}</SettingTitle>
|
||||||
<Select
|
<Select
|
||||||
value={isCustomSize ? 'custom' : painting.image_size}
|
value={isCustomSize ? 'custom' : painting.image_size}
|
||||||
onChange={(value) => onSelectImageSize(value)}
|
onChange={(value) => onSelectImageSize(value)}
|
||||||
style={{ width: '100%' }}>
|
className="w-full">
|
||||||
{(() => {
|
{(() => {
|
||||||
const currentModel = allModels.find((m) => m.id === painting.model)
|
const currentModel = allModels.find((m) => m.id === painting.model)
|
||||||
const modelImageSizes = currentModel?.image_sizes || []
|
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 && (
|
{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">
|
<RowFlex className="items-center gap-2">
|
||||||
<InputNumber
|
<InputNumber
|
||||||
placeholder="W"
|
placeholder="W"
|
||||||
@ -922,7 +895,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
|||||||
|
|
||||||
{painting.generationMode === generationModeType.GENERATION && (
|
{painting.generationMode === generationModeType.GENERATION && (
|
||||||
<>
|
<>
|
||||||
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>
|
<SettingTitle className="mt-4 mb-1">
|
||||||
{t('paintings.seed')}
|
{t('paintings.seed')}
|
||||||
<InfoTooltip content={t('paintings.seed_desc_tip')} />
|
<InfoTooltip content={t('paintings.seed_desc_tip')} />
|
||||||
</SettingTitle>
|
</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>
|
<SliderContainer>
|
||||||
<RadioTextBox>
|
<RadioTextBox>
|
||||||
{STYLE_TYPE_OPTIONS.map((ele) => (
|
{STYLE_TYPE_OPTIONS.map((ele) => (
|
||||||
@ -954,7 +927,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
|||||||
</RadioTextBox>
|
</RadioTextBox>
|
||||||
</SliderContainer>
|
</SliderContainer>
|
||||||
|
|
||||||
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>
|
<SettingTitle className="mt-4 mb-1">
|
||||||
{t('paintings.auto_create_paint')}
|
{t('paintings.auto_create_paint')}
|
||||||
<InfoTooltip content={t('paintings.auto_create_paint_tip')} />
|
<InfoTooltip content={t('paintings.auto_create_paint_tip')} />
|
||||||
</SettingTitle>
|
</SettingTitle>
|
||||||
@ -1026,10 +999,8 @@ const ProviderTitleContainer = styled.div`
|
|||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const SelectOptionContainer = styled.div`
|
const ProviderLogo = styled(Avatar)`
|
||||||
display: flex;
|
border: 0.5px solid var(--color-border);
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
`
|
`
|
||||||
|
|
||||||
const ContentContainer = styled.div`
|
const ContentContainer = styled.div`
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navb
|
|||||||
import Scrollbar from '@renderer/components/Scrollbar'
|
import Scrollbar from '@renderer/components/Scrollbar'
|
||||||
import TranslateButton from '@renderer/components/TranslateButton'
|
import TranslateButton from '@renderer/components/TranslateButton'
|
||||||
import { isMac } from '@renderer/config/constant'
|
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 { LanguagesEnum } from '@renderer/config/translate'
|
||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
import { usePaintings } from '@renderer/hooks/usePaintings'
|
import { usePaintings } from '@renderer/hooks/usePaintings'
|
||||||
@ -19,8 +19,7 @@ import {
|
|||||||
getPaintingsBackgroundOptionsLabel,
|
getPaintingsBackgroundOptionsLabel,
|
||||||
getPaintingsImageSizeOptionsLabel,
|
getPaintingsImageSizeOptionsLabel,
|
||||||
getPaintingsModerationOptionsLabel,
|
getPaintingsModerationOptionsLabel,
|
||||||
getPaintingsQualityOptionsLabel,
|
getPaintingsQualityOptionsLabel
|
||||||
getProviderLabel
|
|
||||||
} from '@renderer/i18n/label'
|
} from '@renderer/i18n/label'
|
||||||
import PaintingsList from '@renderer/pages/paintings/components/PaintingsList'
|
import PaintingsList from '@renderer/pages/paintings/components/PaintingsList'
|
||||||
import { DEFAULT_PAINTING, MODELS, SUPPORTED_MODELS } from '@renderer/pages/paintings/config/NewApiConfig'
|
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 SendMessageButton from '../home/Inputbar/SendMessageButton'
|
||||||
import { SettingHelpLink, SettingTitle } from '../settings'
|
import { SettingHelpLink, SettingTitle } from '../settings'
|
||||||
import Artboard from './components/Artboard'
|
import Artboard from './components/Artboard'
|
||||||
|
import ProviderSelect from './components/ProviderSelect'
|
||||||
import { checkProviderEnabled } from './utils'
|
import { checkProviderEnabled } from './utils'
|
||||||
|
|
||||||
const logger = loggerService.withContext('NewApiPage')
|
const logger = loggerService.withContext('NewApiPage')
|
||||||
@ -56,8 +56,7 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
|||||||
}
|
}
|
||||||
}, [openai_image_generate, openai_image_edit])
|
}, [openai_image_generate, openai_image_edit])
|
||||||
|
|
||||||
const filteredPaintings = useMemo(() => newApiPaintings[mode] || [], [newApiPaintings, mode])
|
// moved below after newApiProvider is defined
|
||||||
const [painting, setPainting] = useState<PaintingAction>(filteredPaintings[0] || DEFAULT_PAINTING)
|
|
||||||
const [currentImageIndex, setCurrentImageIndex] = useState(0)
|
const [currentImageIndex, setCurrentImageIndex] = useState(0)
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [abortController, setAbortController] = useState<AbortController | null>(null)
|
const [abortController, setAbortController] = useState<AbortController | null>(null)
|
||||||
@ -68,26 +67,21 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const providers = useAllProviders()
|
const providers = useAllProviders()
|
||||||
const providerOptions = Options.map((option) => {
|
const location = useLocation()
|
||||||
const provider = providers.find((p) => p.id === option)
|
const routeName = location.pathname.split('/').pop() || 'new-api'
|
||||||
if (provider) {
|
const newApiProviders = providers.filter((p) => isNewApiProvider(p))
|
||||||
return {
|
|
||||||
label: getProviderLabel(provider.id),
|
|
||||||
value: provider.id
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
label: 'Unknown Provider',
|
|
||||||
value: undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const [generating, setGenerating] = useCache('chat.generating')
|
const [generating, setGenerating] = useCache('chat.generating')
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
|
||||||
const [autoTranslateWithSpace] = usePreference('chat.input.translate.auto_translate_with_space')
|
const [autoTranslateWithSpace] = usePreference('chat.input.translate.auto_translate_with_space')
|
||||||
const spaceClickTimer = useRef<NodeJS.Timeout>(null)
|
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 = [
|
const modeOptions = [
|
||||||
{ label: t('paintings.mode.generate'), value: 'openai_image_generate' },
|
{ label: t('paintings.mode.generate'), value: 'openai_image_generate' },
|
||||||
@ -102,7 +96,7 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
|||||||
}, [editImageFiles])
|
}, [editImageFiles])
|
||||||
|
|
||||||
const updatePaintingState = (updates: Partial<PaintingAction>) => {
|
const updatePaintingState = (updates: Partial<PaintingAction>) => {
|
||||||
const updatedPainting = { ...painting, ...updates }
|
const updatedPainting = { ...painting, providerId: newApiProvider.id, ...updates }
|
||||||
setPainting(updatedPainting)
|
setPainting(updatedPainting)
|
||||||
updatePainting(mode, updatedPainting)
|
updatePainting(mode, updatedPainting)
|
||||||
}
|
}
|
||||||
@ -138,9 +132,10 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
|||||||
return {
|
return {
|
||||||
...DEFAULT_PAINTING,
|
...DEFAULT_PAINTING,
|
||||||
model: painting.model || modelOptions[0]?.value || '',
|
model: painting.model || modelOptions[0]?.value || '',
|
||||||
id: uuid()
|
id: uuid(),
|
||||||
|
providerId: newApiProvider.id
|
||||||
}
|
}
|
||||||
}, [modelOptions, painting.model])
|
}, [modelOptions, painting.model, newApiProvider.id])
|
||||||
|
|
||||||
const selectedModelConfig = useMemo(
|
const selectedModelConfig = useMemo(
|
||||||
() => MODELS.find((m) => m.name === painting.model) || MODELS[0],
|
() => MODELS.find((m) => m.name === painting.model) || MODELS[0],
|
||||||
@ -444,11 +439,10 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
|||||||
// 处理模式切换
|
// 处理模式切换
|
||||||
const handleModeChange = (value: string) => {
|
const handleModeChange = (value: string) => {
|
||||||
setMode(value as keyof PaintingsState)
|
setMode(value as keyof PaintingsState)
|
||||||
if (newApiPaintings[value as keyof PaintingsState] && newApiPaintings[value as keyof PaintingsState].length > 0) {
|
const list = (newApiPaintings[value as keyof PaintingsState] || []).filter(
|
||||||
setPainting(newApiPaintings[value as keyof PaintingsState][0])
|
(p) => p.providerId === newApiProvider.id
|
||||||
} else {
|
)
|
||||||
setPainting(DEFAULT_PAINTING)
|
setPainting(list[0] || { ...DEFAULT_PAINTING, providerId: newApiProvider.id })
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 渲染配置项的函数
|
// 渲染配置项的函数
|
||||||
@ -473,8 +467,10 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
|||||||
const newPainting = getNewPainting()
|
const newPainting = getNewPainting()
|
||||||
addPainting(mode, newPainting)
|
addPainting(mode, newPainting)
|
||||||
setPainting(newPainting)
|
setPainting(newPainting)
|
||||||
|
} else {
|
||||||
|
setPainting(filteredPaintings[0])
|
||||||
}
|
}
|
||||||
}, [filteredPaintings, mode, addPainting, painting, getNewPainting])
|
}, [filteredPaintings, mode, addPainting, getNewPainting])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = spaceClickTimer.current
|
const timer = spaceClickTimer.current
|
||||||
@ -501,9 +497,11 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
|||||||
<LeftContainer>
|
<LeftContainer>
|
||||||
<ProviderTitleContainer>
|
<ProviderTitleContainer>
|
||||||
<SettingTitle style={{ marginBottom: 5 }}>{t('common.provider')}</SettingTitle>
|
<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')}
|
{t('paintings.learn_more')}
|
||||||
<Avatar
|
<ProviderLogo
|
||||||
radius="md"
|
radius="md"
|
||||||
src={getProviderLogo(newApiProvider.id)}
|
src={getProviderLogo(newApiProvider.id)}
|
||||||
className="h-4 w-4 border-[0.5px] border-[var(--color-border)]"
|
className="h-4 w-4 border-[0.5px] border-[var(--color-border)]"
|
||||||
@ -512,23 +510,7 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
|||||||
</SettingHelpLink>
|
</SettingHelpLink>
|
||||||
</ProviderTitleContainer>
|
</ProviderTitleContainer>
|
||||||
|
|
||||||
<Select
|
<ProviderSelect provider={newApiProvider} options={Options} onChange={handleProviderChange} />
|
||||||
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>
|
|
||||||
|
|
||||||
{/* 当没有可用的 Image Generation 模型时,提示用户先去新增 */}
|
{/* 当没有可用的 Image Generation 模型时,提示用户先去新增 */}
|
||||||
{modelOptions.length === 0 && (
|
{modelOptions.length === 0 && (
|
||||||
@ -792,20 +774,16 @@ const ToolbarMenu = styled.div`
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
`
|
`
|
||||||
|
|
||||||
// 添加新的样式组件
|
const ProviderLogo = styled(Avatar)`
|
||||||
|
border: 0.5px solid var(--color-border);
|
||||||
|
`
|
||||||
|
|
||||||
const ModeSegmentedContainer = styled.div`
|
const ModeSegmentedContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding-top: 24px;
|
padding-top: 24px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const SelectOptionContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
`
|
|
||||||
|
|
||||||
// 添加新的样式组件
|
|
||||||
const ProviderTitleContainer = styled.div`
|
const ProviderTitleContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
|
import { isNewApiProvider } from '@renderer/config/providers'
|
||||||
|
import { useAllProviders } from '@renderer/hooks/useProvider'
|
||||||
import { useAppDispatch } from '@renderer/store'
|
import { useAppDispatch } from '@renderer/store'
|
||||||
import { setDefaultPaintingProvider } from '@renderer/store/settings'
|
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 type { FC } from 'react'
|
||||||
import { useEffect } from 'react'
|
import { useEffect, useMemo } from 'react'
|
||||||
import { Route, Routes, useParams } from 'react-router-dom'
|
import { Route, Routes, useParams } from 'react-router-dom'
|
||||||
|
|
||||||
import AihubmixPage from './AihubmixPage'
|
import AihubmixPage from './AihubmixPage'
|
||||||
@ -15,19 +17,23 @@ import ZhipuPage from './ZhipuPage'
|
|||||||
|
|
||||||
const logger = loggerService.withContext('PaintingsRoutePage')
|
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 PaintingsRoutePage: FC = () => {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const provider = params['*']
|
const provider = params['*']
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
const providers = useAllProviders()
|
||||||
|
const Options = useMemo(() => {
|
||||||
|
return [...BASE_OPTIONS, ...providers.filter((p) => isNewApiProvider(p)).map((p) => p.id)]
|
||||||
|
}, [providers])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
logger.debug(`defaultPaintingProvider: ${provider}`)
|
logger.debug(`defaultPaintingProvider: ${provider}`)
|
||||||
if (provider && Options.includes(provider)) {
|
if (provider && Options.includes(provider)) {
|
||||||
dispatch(setDefaultPaintingProvider(provider as PaintingProvider))
|
dispatch(setDefaultPaintingProvider(provider as PaintingProvider))
|
||||||
}
|
}
|
||||||
}, [provider, dispatch])
|
}, [provider, dispatch, Options])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
@ -37,7 +43,12 @@ const PaintingsRoutePage: FC = () => {
|
|||||||
<Route path="/silicon" element={<SiliconPage Options={Options} />} />
|
<Route path="/silicon" element={<SiliconPage Options={Options} />} />
|
||||||
<Route path="/dmxapi" element={<DmxapiPage Options={Options} />} />
|
<Route path="/dmxapi" element={<DmxapiPage Options={Options} />} />
|
||||||
<Route path="/tokenflux" element={<TokenFluxPage 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>
|
</Routes>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,18 +14,16 @@ import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navb
|
|||||||
import Scrollbar from '@renderer/components/Scrollbar'
|
import Scrollbar from '@renderer/components/Scrollbar'
|
||||||
import TranslateButton from '@renderer/components/TranslateButton'
|
import TranslateButton from '@renderer/components/TranslateButton'
|
||||||
import { isMac } from '@renderer/config/constant'
|
import { isMac } from '@renderer/config/constant'
|
||||||
import { getProviderLogo } from '@renderer/config/providers'
|
|
||||||
import { LanguagesEnum } from '@renderer/config/translate'
|
import { LanguagesEnum } from '@renderer/config/translate'
|
||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
import { usePaintings } from '@renderer/hooks/usePaintings'
|
import { usePaintings } from '@renderer/hooks/usePaintings'
|
||||||
import { useAllProviders } from '@renderer/hooks/useProvider'
|
import { useAllProviders } from '@renderer/hooks/useProvider'
|
||||||
import { getProviderLabel } from '@renderer/i18n/label'
|
|
||||||
import { getProviderByModel } from '@renderer/services/AssistantService'
|
import { getProviderByModel } from '@renderer/services/AssistantService'
|
||||||
import FileManager from '@renderer/services/FileManager'
|
import FileManager from '@renderer/services/FileManager'
|
||||||
import { translateText } from '@renderer/services/TranslateService'
|
import { translateText } from '@renderer/services/TranslateService'
|
||||||
import type { FileMetadata, Painting } from '@renderer/types'
|
import type { FileMetadata, Painting } from '@renderer/types'
|
||||||
import { getErrorMessage, uuid } from '@renderer/utils'
|
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 TextArea from 'antd/es/input/TextArea'
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
@ -37,6 +35,7 @@ import SendMessageButton from '../home/Inputbar/SendMessageButton'
|
|||||||
import { SettingTitle } from '../settings'
|
import { SettingTitle } from '../settings'
|
||||||
import Artboard from './components/Artboard'
|
import Artboard from './components/Artboard'
|
||||||
import PaintingsList from './components/PaintingsList'
|
import PaintingsList from './components/PaintingsList'
|
||||||
|
import ProviderSelect from './components/ProviderSelect'
|
||||||
import { checkProviderEnabled } from './utils'
|
import { checkProviderEnabled } from './utils'
|
||||||
|
|
||||||
export const TEXT_TO_IMAGES_MODELS = [
|
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 [painting, setPainting] = useState<Painting>(siliconflow_paintings[0] || DEFAULT_PAINTING)
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const providers = useAllProviders()
|
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 [currentImageIndex, setCurrentImageIndex] = useState(0)
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
@ -166,7 +151,7 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onGenerate = async () => {
|
const onGenerate = async () => {
|
||||||
await checkProviderEnabled(siliconflowProvider!, t)
|
await checkProviderEnabled(siliconFlowProvider!, t)
|
||||||
|
|
||||||
if (painting.files.length > 0) {
|
if (painting.files.length > 0) {
|
||||||
const confirmed = await window.modal.confirm({
|
const confirmed = await window.modal.confirm({
|
||||||
@ -385,17 +370,8 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => {
|
|||||||
<ContentContainer id="content-container">
|
<ContentContainer id="content-container">
|
||||||
<LeftContainer>
|
<LeftContainer>
|
||||||
<SettingTitle style={{ marginBottom: 5 }}>{t('common.provider')}</SettingTitle>
|
<SettingTitle style={{ marginBottom: 5 }}>{t('common.provider')}</SettingTitle>
|
||||||
<Select value={providerOptions[2].value} onChange={handleProviderChange}>
|
<ProviderSelect provider={siliconFlowProvider} options={Options} onChange={handleProviderChange} />
|
||||||
{providerOptions.map((provider) => (
|
<SettingTitle className="mt-4 mb-1">{t('common.model')}</SettingTitle>
|
||||||
<Select.Option value={provider.value} key={provider.value}>
|
|
||||||
<SelectOptionContainer>
|
|
||||||
<ProviderLogo shape="square" src={getProviderLogo(provider.value || '')} size={16} />
|
|
||||||
{provider.label}
|
|
||||||
</SelectOptionContainer>
|
|
||||||
</Select.Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('common.model')}</SettingTitle>
|
|
||||||
<Select value={painting.model} options={modelOptions} onChange={onSelectModel} />
|
<Select value={painting.model} options={modelOptions} onChange={onSelectModel} />
|
||||||
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('paintings.image.size')}</SettingTitle>
|
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('paintings.image.size')}</SettingTitle>
|
||||||
<Radio.Group
|
<Radio.Group
|
||||||
@ -643,14 +619,4 @@ const StyledInputNumber = styled(InputNumber)`
|
|||||||
width: 70px;
|
width: 70px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const SelectOptionContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const ProviderLogo = styled(Avatar)`
|
|
||||||
flex-shrink: 0;
|
|
||||||
`
|
|
||||||
|
|
||||||
export default SiliconPage
|
export default SiliconPage
|
||||||
|
|||||||
@ -11,7 +11,6 @@ import { getProviderLogo } from '@renderer/config/providers'
|
|||||||
import { LanguagesEnum } from '@renderer/config/translate'
|
import { LanguagesEnum } from '@renderer/config/translate'
|
||||||
import { usePaintings } from '@renderer/hooks/usePaintings'
|
import { usePaintings } from '@renderer/hooks/usePaintings'
|
||||||
import { useAllProviders } from '@renderer/hooks/useProvider'
|
import { useAllProviders } from '@renderer/hooks/useProvider'
|
||||||
import { getProviderLabel } from '@renderer/i18n/label'
|
|
||||||
import FileManager from '@renderer/services/FileManager'
|
import FileManager from '@renderer/services/FileManager'
|
||||||
import { translateText } from '@renderer/services/TranslateService'
|
import { translateText } from '@renderer/services/TranslateService'
|
||||||
import type { TokenFluxPainting } from '@renderer/types'
|
import type { TokenFluxPainting } from '@renderer/types'
|
||||||
@ -29,6 +28,7 @@ import { SettingHelpLink, SettingTitle } from '../settings'
|
|||||||
import Artboard from './components/Artboard'
|
import Artboard from './components/Artboard'
|
||||||
import { DynamicFormRender } from './components/DynamicFormRender'
|
import { DynamicFormRender } from './components/DynamicFormRender'
|
||||||
import PaintingsList from './components/PaintingsList'
|
import PaintingsList from './components/PaintingsList'
|
||||||
|
import ProviderSelect from './components/ProviderSelect'
|
||||||
import { DEFAULT_TOKENFLUX_PAINTING, type TokenFluxModel } from './config/tokenFluxConfig'
|
import { DEFAULT_TOKENFLUX_PAINTING, type TokenFluxModel } from './config/tokenFluxConfig'
|
||||||
import { checkProviderEnabled } from './utils'
|
import { checkProviderEnabled } from './utils'
|
||||||
import TokenFluxService from './utils/TokenFluxService'
|
import TokenFluxService from './utils/TokenFluxService'
|
||||||
@ -54,21 +54,6 @@ const TokenFluxPage: FC<{ Options: string[] }> = ({ Options }) => {
|
|||||||
tokenFluxPaintings[0] || { ...DEFAULT_TOKENFLUX_PAINTING, id: uuid() }
|
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 navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const [autoTranslateWithSpace] = usePreference('chat.input.translate.auto_translate_with_space')
|
const [autoTranslateWithSpace] = usePreference('chat.input.translate.auto_translate_with_space')
|
||||||
@ -388,23 +373,7 @@ const TokenFluxPage: FC<{ Options: string[] }> = ({ Options }) => {
|
|||||||
</SettingHelpLink>
|
</SettingHelpLink>
|
||||||
</ProviderTitleContainer>
|
</ProviderTitleContainer>
|
||||||
|
|
||||||
<Select
|
<ProviderSelect provider={tokenfluxProvider} options={Options} onChange={handleProviderChange} />
|
||||||
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>
|
|
||||||
|
|
||||||
{/* Model & Pricing Section */}
|
{/* Model & Pricing Section */}
|
||||||
<SectionTitle
|
<SectionTitle
|
||||||
@ -759,11 +728,4 @@ const ProviderTitleContainer = styled.div`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const SelectOptionContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
`
|
|
||||||
|
|
||||||
export default TokenFluxPage
|
export default TokenFluxPage
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import { isMac } from '@renderer/config/constant'
|
|||||||
import { getProviderLogo } from '@renderer/config/providers'
|
import { getProviderLogo } from '@renderer/config/providers'
|
||||||
import { usePaintings } from '@renderer/hooks/usePaintings'
|
import { usePaintings } from '@renderer/hooks/usePaintings'
|
||||||
import { useAllProviders } from '@renderer/hooks/useProvider'
|
import { useAllProviders } from '@renderer/hooks/useProvider'
|
||||||
import { getProviderLabel } from '@renderer/i18n/label'
|
|
||||||
import FileManager from '@renderer/services/FileManager'
|
import FileManager from '@renderer/services/FileManager'
|
||||||
import { getErrorMessage, uuid } from '@renderer/utils'
|
import { getErrorMessage, uuid } from '@renderer/utils'
|
||||||
import { InputNumber, Radio, Select } from 'antd'
|
import { InputNumber, Radio, Select } from 'antd'
|
||||||
@ -25,6 +24,7 @@ import SendMessageButton from '../home/Inputbar/SendMessageButton'
|
|||||||
import { SettingHelpLink, SettingTitle } from '../settings'
|
import { SettingHelpLink, SettingTitle } from '../settings'
|
||||||
import Artboard from './components/Artboard'
|
import Artboard from './components/Artboard'
|
||||||
import PaintingsList from './components/PaintingsList'
|
import PaintingsList from './components/PaintingsList'
|
||||||
|
import ProviderSelect from './components/ProviderSelect'
|
||||||
import {
|
import {
|
||||||
COURSE_URL,
|
COURSE_URL,
|
||||||
DEFAULT_PAINTING,
|
DEFAULT_PAINTING,
|
||||||
@ -51,21 +51,6 @@ const ZhipuPage: FC<{ Options: string[] }> = ({ Options }) => {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [painting?.id]) // 只在painting的id改变时执行,避免无限循环
|
}, [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 zhipuProvider = providers.find((p) => p.id === 'zhipu')!
|
||||||
|
|
||||||
const [currentImageIndex, setCurrentImageIndex] = useState(0)
|
const [currentImageIndex, setCurrentImageIndex] = useState(0)
|
||||||
@ -375,16 +360,7 @@ const ZhipuPage: FC<{ Options: string[] }> = ({ Options }) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ProviderTitleContainer>
|
</ProviderTitleContainer>
|
||||||
<Select value={providerOptions[0].value} onChange={handleProviderChange} style={{ marginBottom: 15 }}>
|
<ProviderSelect provider={zhipuProvider} options={Options} onChange={handleProviderChange} className="mb-4" />
|
||||||
{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>
|
|
||||||
|
|
||||||
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('common.model')}</SettingTitle>
|
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('common.model')}</SettingTitle>
|
||||||
<Select
|
<Select
|
||||||
@ -568,12 +544,6 @@ const ProviderTitleContainer = styled.div`
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const SelectOptionContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const ProviderLogo = styled(Avatar)`
|
const ProviderLogo = styled(Avatar)`
|
||||||
border-radius: 4px;
|
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: 'OpenAI-Response', value: 'openai-response' },
|
||||||
{ label: 'Gemini', value: 'gemini' },
|
{ label: 'Gemini', value: 'gemini' },
|
||||||
{ label: 'Anthropic', value: 'anthropic' },
|
{ label: 'Anthropic', value: 'anthropic' },
|
||||||
{ label: 'Azure OpenAI', value: 'azure-openai' }
|
{ label: 'Azure OpenAI', value: 'azure-openai' },
|
||||||
|
{ label: 'New API', value: 'new-api' }
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@ -1,16 +1,14 @@
|
|||||||
// TODO: Refactor this component to use HeroUI
|
// TODO: Refactor this component to use HeroUI
|
||||||
import { Button, Tooltip } from '@cherrystudio/ui'
|
import { Button, Tooltip } from '@cherrystudio/ui'
|
||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
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 type { RootState } from '@renderer/store'
|
||||||
import { useAppDispatch } 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 { formatErrorMessage } from '@renderer/utils/error'
|
||||||
import { IpcChannel } from '@shared/IpcChannel'
|
|
||||||
import { Input, InputNumber, Typography } from 'antd'
|
import { Input, InputNumber, Typography } from 'antd'
|
||||||
import { Copy, ExternalLink, Play, RotateCcw, Square } from 'lucide-react'
|
import { Copy, ExternalLink, Play, RotateCcw, Square } from 'lucide-react'
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
@ -18,7 +16,6 @@ import { v4 as uuidv4 } from 'uuid'
|
|||||||
|
|
||||||
import { SettingContainer } from '../..'
|
import { SettingContainer } from '../..'
|
||||||
|
|
||||||
const logger = loggerService.withContext('ApiServerSettings')
|
|
||||||
const { Text, Title } = Typography
|
const { Text, Title } = Typography
|
||||||
|
|
||||||
const ApiServerSettings: FC = () => {
|
const ApiServerSettings: FC = () => {
|
||||||
@ -28,67 +25,25 @@ const ApiServerSettings: FC = () => {
|
|||||||
|
|
||||||
// API Server state with proper defaults
|
// API Server state with proper defaults
|
||||||
const apiServerConfig = useSelector((state: RootState) => state.settings.apiServer)
|
const apiServerConfig = useSelector((state: RootState) => state.settings.apiServer)
|
||||||
|
const { apiServerRunning, apiServerLoading, startApiServer, stopApiServer, restartApiServer, setApiServerEnabled } =
|
||||||
const [apiServerRunning, setApiServerRunning] = useState(false)
|
useApiServer()
|
||||||
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 handleApiServerToggle = async (enabled: boolean) => {
|
const handleApiServerToggle = async (enabled: boolean) => {
|
||||||
setApiServerLoading(true)
|
|
||||||
try {
|
try {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
const result = await window.electron.ipcRenderer.invoke(IpcChannel.ApiServer_Start)
|
await startApiServer()
|
||||||
if (result.success) {
|
|
||||||
setApiServerRunning(true)
|
|
||||||
window.toast.success(t('apiServer.messages.startSuccess'))
|
|
||||||
} else {
|
|
||||||
window.toast.error(t('apiServer.messages.startError') + result.error)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const result = await window.electron.ipcRenderer.invoke(IpcChannel.ApiServer_Stop)
|
await stopApiServer()
|
||||||
if (result.success) {
|
|
||||||
setApiServerRunning(false)
|
|
||||||
window.toast.success(t('apiServer.messages.stopSuccess'))
|
|
||||||
} else {
|
|
||||||
window.toast.error(t('apiServer.messages.stopError') + result.error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.toast.error(t('apiServer.messages.operationFailed') + formatErrorMessage(error))
|
window.toast.error(t('apiServer.messages.operationFailed') + formatErrorMessage(error))
|
||||||
} finally {
|
} finally {
|
||||||
dispatch(setApiServerEnabled(enabled))
|
setApiServerEnabled(enabled)
|
||||||
setApiServerLoading(false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleApiServerRestart = async () => {
|
const handleApiServerRestart = async () => {
|
||||||
setApiServerLoading(true)
|
await restartApiServer()
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const copyApiKey = () => {
|
const copyApiKey = () => {
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { ProviderTypeSchema } from './provider'
|
|||||||
// Request schema for /v1/models
|
// Request schema for /v1/models
|
||||||
export const ApiModelsFilterSchema = z.object({
|
export const ApiModelsFilterSchema = z.object({
|
||||||
providerType: ProviderTypeSchema.optional(),
|
providerType: ProviderTypeSchema.optional(),
|
||||||
|
supportAnthropic: z.coerce.boolean().optional(),
|
||||||
offset: z.coerce.number().min(0).default(0).optional(),
|
offset: z.coerce.number().min(0).default(0).optional(),
|
||||||
limit: z.coerce.number().min(1).default(20).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
|
UnsupportedFunctionalityError
|
||||||
} from 'ai'
|
} from 'ai'
|
||||||
|
|
||||||
|
import type { ProviderSpecificError } from './provider-specific-error'
|
||||||
import type { Serializable } from './serialize'
|
import type { Serializable } from './serialize'
|
||||||
|
|
||||||
export interface SerializedError {
|
export interface SerializedError {
|
||||||
@ -80,7 +81,7 @@ export interface SerializedAiSdkInvalidArgumentError extends SerializedAiSdkErro
|
|||||||
export const isSerializedAiSdkInvalidArgumentError = (
|
export const isSerializedAiSdkInvalidArgumentError = (
|
||||||
error: SerializedError
|
error: SerializedError
|
||||||
): error is SerializedAiSdkInvalidArgumentError => {
|
): 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 {
|
export interface SerializedAiSdkInvalidDataContentError extends SerializedAiSdkError {
|
||||||
@ -198,10 +199,20 @@ export interface SerializedAiSdkNoSuchToolError extends SerializedAiSdkError {
|
|||||||
readonly availableTools: string[] | null
|
readonly availableTools: string[] | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SerializedAiSdkProviderSpecificError extends SerializedAiSdkError {
|
||||||
|
readonly provider: string
|
||||||
|
}
|
||||||
|
|
||||||
export const isSerializedAiSdkNoSuchToolError = (error: SerializedError): error is SerializedAiSdkNoSuchToolError => {
|
export const isSerializedAiSdkNoSuchToolError = (error: SerializedError): error is SerializedAiSdkNoSuchToolError => {
|
||||||
return isSerializedAiSdkError(error) && 'toolName' in error && 'availableTools' in error
|
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 {
|
export interface SerializedAiSdkRetryError extends SerializedAiSdkError {
|
||||||
readonly reason: string
|
readonly reason: string
|
||||||
readonly lastError: Serializable
|
readonly lastError: Serializable
|
||||||
@ -277,6 +288,7 @@ export type AiSdkErrorUnion =
|
|||||||
| NoSuchModelError
|
| NoSuchModelError
|
||||||
| NoSuchProviderError
|
| NoSuchProviderError
|
||||||
| NoSuchToolError
|
| NoSuchToolError
|
||||||
|
| ProviderSpecificError
|
||||||
| RetryError
|
| RetryError
|
||||||
| ToolCallRepairError
|
| ToolCallRepairError
|
||||||
| TypeValidationError
|
| TypeValidationError
|
||||||
@ -297,6 +309,7 @@ export type SerializedAiSdkErrorUnion =
|
|||||||
| SerializedAiSdkNoSuchModelError
|
| SerializedAiSdkNoSuchModelError
|
||||||
| SerializedAiSdkNoSuchProviderError
|
| SerializedAiSdkNoSuchProviderError
|
||||||
| SerializedAiSdkNoSuchToolError
|
| SerializedAiSdkNoSuchToolError
|
||||||
|
| SerializedAiSdkProviderSpecificError
|
||||||
| SerializedAiSdkRetryError
|
| SerializedAiSdkRetryError
|
||||||
| SerializedAiSdkToolCallRepairError
|
| SerializedAiSdkToolCallRepairError
|
||||||
| SerializedAiSdkTypeValidationError
|
| SerializedAiSdkTypeValidationError
|
||||||
@ -317,6 +330,7 @@ export const isSerializedAiSdkErrorUnion = (error: SerializedError): error is Se
|
|||||||
isSerializedAiSdkNoSuchModelError(error) ||
|
isSerializedAiSdkNoSuchModelError(error) ||
|
||||||
isSerializedAiSdkNoSuchProviderError(error) ||
|
isSerializedAiSdkNoSuchProviderError(error) ||
|
||||||
isSerializedAiSdkNoSuchToolError(error) ||
|
isSerializedAiSdkNoSuchToolError(error) ||
|
||||||
|
isSerializedAiSdkProviderSpecificError(error) ||
|
||||||
isSerializedAiSdkRetryError(error) ||
|
isSerializedAiSdkRetryError(error) ||
|
||||||
isSerializedAiSdkToolCallRepairError(error) ||
|
isSerializedAiSdkToolCallRepairError(error) ||
|
||||||
isSerializedAiSdkTypeValidationError(error) ||
|
isSerializedAiSdkTypeValidationError(error) ||
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import type { BaseTool, MCPTool } from './tool'
|
|||||||
|
|
||||||
export * from './agent'
|
export * from './agent'
|
||||||
export * from './apiModels'
|
export * from './apiModels'
|
||||||
|
export * from './apiServer'
|
||||||
export * from './knowledge'
|
export * from './knowledge'
|
||||||
export * from './mcp'
|
export * from './mcp'
|
||||||
export * from './notification'
|
export * from './notification'
|
||||||
@ -272,6 +273,8 @@ export type PaintingParams = {
|
|||||||
id: string
|
id: string
|
||||||
urls: string[]
|
urls: string[]
|
||||||
files: FileMetadata[]
|
files: FileMetadata[]
|
||||||
|
// provider that this painting belongs to (for new-api family separation)
|
||||||
|
providerId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PaintingProvider = 'zhipu' | 'aihubmix' | 'silicon' | 'dmxapi' | 'new-api'
|
export type PaintingProvider = 'zhipu' | 'aihubmix' | 'silicon' | 'dmxapi' | 'new-api'
|
||||||
@ -843,13 +846,6 @@ export type S3Config = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type { Message } from './newMessage'
|
export type { Message } from './newMessage'
|
||||||
|
|
||||||
export interface ApiServerConfig {
|
|
||||||
enabled: boolean
|
|
||||||
host: string
|
|
||||||
port: number
|
|
||||||
apiKey: string
|
|
||||||
}
|
|
||||||
export * from './tool'
|
export * from './tool'
|
||||||
|
|
||||||
// Memory Service Types
|
// 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',
|
'vertexai',
|
||||||
'mistral',
|
'mistral',
|
||||||
'aws-bedrock',
|
'aws-bedrock',
|
||||||
'vertex-anthropic'
|
'vertex-anthropic',
|
||||||
|
'new-api'
|
||||||
])
|
])
|
||||||
|
|
||||||
export type ProviderType = z.infer<typeof ProviderTypeSchema>
|
export type ProviderType = z.infer<typeof ProviderTypeSchema>
|
||||||
|
|||||||
@ -18,7 +18,7 @@ export const getModelFilterByAgentType = (type: AgentType): ApiModelsFilter => {
|
|||||||
switch (type) {
|
switch (type) {
|
||||||
case 'claude-code':
|
case 'claude-code':
|
||||||
return {
|
return {
|
||||||
providerType: 'anthropic'
|
supportAnthropic: true
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return {}
|
return {}
|
||||||
|
|||||||
@ -201,6 +201,7 @@ export const serializeError = (error: AiSdkErrorUnion): SerializedError => {
|
|||||||
? serializeInvalidToolInputError(error.originalError)
|
? serializeInvalidToolInputError(error.originalError)
|
||||||
: serializeNoSuchToolError(error.originalError)
|
: serializeNoSuchToolError(error.originalError)
|
||||||
if ('functionality' in error) serializedError.functionality = error.functionality
|
if ('functionality' in error) serializedError.functionality = error.functionality
|
||||||
|
if ('provider' in error) serializedError.provider = error.provider
|
||||||
|
|
||||||
return serializedError
|
return serializedError
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user