Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e8fc3fb0a2 | |||
| 4b254637b0 | |||
| 456b709c29 | |||
| 28bc89ac7c |
@@ -31,3 +31,16 @@ export type WebviewKeyEvent = {
|
||||
shift: boolean
|
||||
alt: boolean
|
||||
}
|
||||
|
||||
export interface WebSocketStatusResponse {
|
||||
isRunning: boolean
|
||||
port?: number
|
||||
ip?: string
|
||||
clientConnected: boolean
|
||||
}
|
||||
|
||||
export interface WebSocketCandidatesResponse {
|
||||
host: string
|
||||
interface: string
|
||||
priority: number
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { WebSocketCandidatesResponse, WebSocketStatusResponse } from '@shared/config/types'
|
||||
import * as fs from 'fs'
|
||||
import { networkInterfaces } from 'os'
|
||||
import * as path from 'path'
|
||||
@@ -202,12 +203,7 @@ class WebSocketService {
|
||||
}
|
||||
}
|
||||
|
||||
public getStatus = async (): Promise<{
|
||||
isRunning: boolean
|
||||
port?: number
|
||||
ip?: string
|
||||
clientConnected: boolean
|
||||
}> => {
|
||||
public getStatus = async (): Promise<WebSocketStatusResponse> => {
|
||||
return {
|
||||
isRunning: this.isStarted,
|
||||
port: this.isStarted ? this.port : undefined,
|
||||
@@ -216,13 +212,7 @@ class WebSocketService {
|
||||
}
|
||||
}
|
||||
|
||||
public getAllCandidates = async (): Promise<
|
||||
Array<{
|
||||
host: string
|
||||
interface: string
|
||||
priority: number
|
||||
}>
|
||||
> => {
|
||||
public getAllCandidates = async (): Promise<WebSocketCandidatesResponse[]> => {
|
||||
const interfaces = networkInterfaces()
|
||||
|
||||
// 按优先级排序的网络接口名称模式
|
||||
|
||||
@@ -3,7 +3,9 @@ import { Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@herou
|
||||
import { Progress } from '@heroui/progress'
|
||||
import { Spinner } from '@heroui/spinner'
|
||||
import { loggerService } from '@logger'
|
||||
import { AppLogo } from '@renderer/config/env'
|
||||
import { SettingHelpText, SettingRow } from '@renderer/pages/settings'
|
||||
import { WebSocketCandidatesResponse } from '@shared/config/types'
|
||||
import { QRCodeSVG } from 'qrcode.react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -38,12 +40,12 @@ const ScanQRCode: React.FC<{ qrCodeValue: string }> = ({ qrCodeValue }) => {
|
||||
<QRCodeSVG
|
||||
marginSize={2}
|
||||
value={qrCodeValue}
|
||||
level="Q"
|
||||
size={160}
|
||||
level="H"
|
||||
size={200}
|
||||
imageSettings={{
|
||||
src: '/src/assets/images/logo.png',
|
||||
width: 40,
|
||||
height: 40,
|
||||
src: AppLogo,
|
||||
width: 60,
|
||||
height: 60,
|
||||
excavate: true
|
||||
}}
|
||||
/>
|
||||
@@ -198,17 +200,28 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
const { port, ip } = await window.api.webSocket.status()
|
||||
|
||||
if (ip && port) {
|
||||
const candidates = await window.api.webSocket.getAllCandidates()
|
||||
const connectionInfo = {
|
||||
type: 'cherry-studio-app',
|
||||
candidates,
|
||||
selectedHost: ip,
|
||||
port,
|
||||
timestamp: Date.now()
|
||||
const candidatesData = await window.api.webSocket.getAllCandidates()
|
||||
|
||||
const optimizeConnectionInfo = () => {
|
||||
const ipToNumber = (ip: string) => {
|
||||
return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet), 0)
|
||||
}
|
||||
|
||||
const compressedData = [
|
||||
'CSA',
|
||||
ipToNumber(ip),
|
||||
candidatesData.map((candidate: WebSocketCandidatesResponse) => ipToNumber(candidate.host)),
|
||||
port, // 端口号
|
||||
Date.now() % 86400000
|
||||
]
|
||||
|
||||
return compressedData
|
||||
}
|
||||
setQrCodeValue(JSON.stringify(connectionInfo))
|
||||
|
||||
const compressedData = optimizeConnectionInfo()
|
||||
const qrCodeValue = JSON.stringify(compressedData)
|
||||
setQrCodeValue(qrCodeValue)
|
||||
setConnectionPhase('waiting_qr_scan')
|
||||
logger.info(`QR code generated: ${ip}:${port} with ${candidates.length} IP candidates`)
|
||||
} else {
|
||||
setError(t('settings.data.export_to_phone.lan.error.no_ip'))
|
||||
setConnectionPhase('error')
|
||||
|
||||
@@ -2923,15 +2923,14 @@
|
||||
},
|
||||
"description": "Ein KI-Assistent für Kreative",
|
||||
"downloading": "Update wird heruntergeladen...",
|
||||
"enterprise": {
|
||||
"title": "Unternehmen"
|
||||
},
|
||||
"feedback": {
|
||||
"button": "Feedback",
|
||||
"title": "Feedback"
|
||||
},
|
||||
"label": "Über uns",
|
||||
"license": {
|
||||
"button": "Anzeigen",
|
||||
"title": "Lizenz"
|
||||
},
|
||||
"releases": {
|
||||
"button": "Anzeigen",
|
||||
"title": "Changelog"
|
||||
|
||||
@@ -2923,15 +2923,14 @@
|
||||
},
|
||||
"description": "Ένα AI ασιστάντα που έχει σχεδιαστεί για δημιουργούς",
|
||||
"downloading": "Λήψη ενημερώσεων...",
|
||||
"enterprise": {
|
||||
"title": "Επιχείρηση"
|
||||
},
|
||||
"feedback": {
|
||||
"button": "Σχόλια και Παρατηρήσεις",
|
||||
"title": "Αποστολή σχολίων"
|
||||
},
|
||||
"label": "Περί μας",
|
||||
"license": {
|
||||
"button": "Προβολή",
|
||||
"title": "Licenses"
|
||||
},
|
||||
"releases": {
|
||||
"button": "Προβολή",
|
||||
"title": "Ημερολόγιο Ενημερώσεων"
|
||||
|
||||
@@ -2923,15 +2923,14 @@
|
||||
},
|
||||
"description": "Una asistente de IA creada para los creadores",
|
||||
"downloading": "Descargando actualización...",
|
||||
"enterprise": {
|
||||
"title": "Empresa"
|
||||
},
|
||||
"feedback": {
|
||||
"button": "Enviar feedback",
|
||||
"title": "Enviar comentarios"
|
||||
},
|
||||
"label": "Acerca de nosotros",
|
||||
"license": {
|
||||
"button": "Ver",
|
||||
"title": "Licencia"
|
||||
},
|
||||
"releases": {
|
||||
"button": "Ver",
|
||||
"title": "Registro de cambios"
|
||||
|
||||
@@ -2923,15 +2923,14 @@
|
||||
},
|
||||
"description": "Un assistant IA conçu pour les créateurs",
|
||||
"downloading": "Téléchargement de la mise à jour en cours...",
|
||||
"enterprise": {
|
||||
"title": "Entreprise"
|
||||
},
|
||||
"feedback": {
|
||||
"button": "Faire un retour",
|
||||
"title": "Retour d'information"
|
||||
},
|
||||
"label": "À propos de nous",
|
||||
"license": {
|
||||
"button": "Afficher",
|
||||
"title": "Licence"
|
||||
},
|
||||
"releases": {
|
||||
"button": "Afficher",
|
||||
"title": "Journal des mises à jour"
|
||||
|
||||
@@ -2923,15 +2923,14 @@
|
||||
},
|
||||
"description": "クリエイターのための強力なAIアシスタント",
|
||||
"downloading": "ダウンロード中...",
|
||||
"enterprise": {
|
||||
"title": "エンタープライズ"
|
||||
},
|
||||
"feedback": {
|
||||
"button": "フィードバック",
|
||||
"title": "フィードバック"
|
||||
},
|
||||
"label": "について",
|
||||
"license": {
|
||||
"button": "ライセンス",
|
||||
"title": "ライセンス"
|
||||
},
|
||||
"releases": {
|
||||
"button": "リリース",
|
||||
"title": "リリースノート"
|
||||
|
||||
@@ -2923,15 +2923,14 @@
|
||||
},
|
||||
"description": "Um assistente de IA criado para criadores",
|
||||
"downloading": "Baixando atualizações...",
|
||||
"enterprise": {
|
||||
"title": "Empresa"
|
||||
},
|
||||
"feedback": {
|
||||
"button": "Feedback",
|
||||
"title": "Enviar feedback"
|
||||
},
|
||||
"label": "Sobre Nós",
|
||||
"license": {
|
||||
"button": "Ver",
|
||||
"title": "Licença"
|
||||
},
|
||||
"releases": {
|
||||
"button": "Ver",
|
||||
"title": "Registro de alterações"
|
||||
|
||||
@@ -2923,15 +2923,14 @@
|
||||
},
|
||||
"description": "Мощный AI-ассистент для созидания",
|
||||
"downloading": "Загрузка...",
|
||||
"enterprise": {
|
||||
"title": "Предприятие"
|
||||
},
|
||||
"feedback": {
|
||||
"button": "Обратная связь",
|
||||
"title": "Обратная связь"
|
||||
},
|
||||
"label": "О программе и обратная связь",
|
||||
"license": {
|
||||
"button": "Лицензия",
|
||||
"title": "Лицензия"
|
||||
},
|
||||
"releases": {
|
||||
"button": "Релизы",
|
||||
"title": "Заметки о релизах"
|
||||
@@ -3043,7 +3042,7 @@
|
||||
"confirm": {
|
||||
"button": "Выберите файл резервной копии"
|
||||
},
|
||||
"content": "Экспорт части данных, включая чат и настройки. Пожалуйста, обратите внимание, что процесс резервного копирования может занять некоторое время. Благодарим за ваше терпение.",
|
||||
"content": "Экспорт части данных, включая историю чатов и настройки. Обратите внимание, процесс резервного копирования может занять некоторое время, благодарим за ваше терпение.",
|
||||
"lan": {
|
||||
"auto_close_tip": "Автоматическое закрытие через {{seconds}} секунд...",
|
||||
"confirm_close_message": "Передача файла в процессе. Закрытие прервет передачу. Вы уверены, что хотите принудительно закрыть?",
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { mcpToolsToOpenAIChatTools, mcpToolsToOpenAIResponseTools } from '../mcp-tools'
|
||||
import type { MCPTool } from '@renderer/types'
|
||||
|
||||
describe('MCP Tools - HTTP MCP Compatibility Fix', () => {
|
||||
const mockMCPTool: MCPTool = {
|
||||
id: 'test_server-search_tool',
|
||||
name: 'search_tool',
|
||||
description: 'Search for information',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'Search query'
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Optional limit',
|
||||
minimum: 1
|
||||
}
|
||||
},
|
||||
required: ['query'] // Only query is required, limit is optional
|
||||
},
|
||||
serverId: 'test-server-id',
|
||||
serverName: 'test-server',
|
||||
type: 'mcp'
|
||||
}
|
||||
|
||||
describe('mcpToolsToOpenAIResponseTools', () => {
|
||||
it('should preserve original schema without forcing all properties to be required', () => {
|
||||
const tools = mcpToolsToOpenAIResponseTools([mockMCPTool])
|
||||
|
||||
expect(tools).toHaveLength(1)
|
||||
const tool = tools[0]
|
||||
|
||||
// Should use the tool id
|
||||
expect(tool.name).toBe('test_server-search_tool')
|
||||
|
||||
// Should preserve the original required array (only 'query')
|
||||
expect(tool.parameters.required).toEqual(['query'])
|
||||
|
||||
// Should have both properties
|
||||
expect(tool.parameters.properties).toHaveProperty('query')
|
||||
expect(tool.parameters.properties).toHaveProperty('limit')
|
||||
})
|
||||
|
||||
it('should not include strict: true', () => {
|
||||
const tools = mcpToolsToOpenAIResponseTools([mockMCPTool])
|
||||
|
||||
expect(tools).toHaveLength(1)
|
||||
const tool = tools[0] as any
|
||||
|
||||
// strict property should not be present
|
||||
expect(tool.strict).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('mcpToolsToOpenAIChatTools', () => {
|
||||
it('should preserve original schema without forcing all properties to be required', () => {
|
||||
const tools = mcpToolsToOpenAIChatTools([mockMCPTool])
|
||||
|
||||
expect(tools).toHaveLength(1)
|
||||
const tool = tools[0]
|
||||
|
||||
// Should use the tool id
|
||||
expect(tool.function.name).toBe('test_server-search_tool')
|
||||
|
||||
// Should include description
|
||||
expect(tool.function.description).toBe('Search for information')
|
||||
|
||||
// Should preserve the original required array (only 'query')
|
||||
expect(tool.function.parameters.required).toEqual(['query'])
|
||||
|
||||
// Should have both properties
|
||||
expect(tool.function.parameters.properties).toHaveProperty('query')
|
||||
expect(tool.function.parameters.properties).toHaveProperty('limit')
|
||||
})
|
||||
|
||||
it('should not include strict: true in function parameters', () => {
|
||||
const tools = mcpToolsToOpenAIChatTools([mockMCPTool])
|
||||
|
||||
expect(tools).toHaveLength(1)
|
||||
const tool = tools[0] as any
|
||||
|
||||
// strict property should not be present
|
||||
expect(tool.function.strict).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('HTTP MCP with complex nested schemas', () => {
|
||||
it('should handle nested objects with optional fields', () => {
|
||||
const complexTool: MCPTool = {
|
||||
id: 'http_server-complex_tool',
|
||||
name: 'complex_tool',
|
||||
description: 'Complex tool with nested schemas',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
required_field: { type: 'string' },
|
||||
optional_object: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
nested_required: { type: 'string' },
|
||||
nested_optional: { type: 'number' }
|
||||
},
|
||||
required: ['nested_required']
|
||||
}
|
||||
},
|
||||
required: ['required_field']
|
||||
},
|
||||
serverId: 'http-server-id',
|
||||
serverName: 'http-server',
|
||||
type: 'mcp'
|
||||
}
|
||||
|
||||
const tools = mcpToolsToOpenAIChatTools([complexTool])
|
||||
const parameters = tools[0].function.parameters
|
||||
|
||||
// Top level should only require 'required_field'
|
||||
expect(parameters.required).toEqual(['required_field'])
|
||||
|
||||
// Nested object should only require 'nested_required'
|
||||
expect(parameters.properties.optional_object.required).toEqual(['nested_required'])
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -32,13 +32,14 @@ import { nanoid } from 'nanoid'
|
||||
|
||||
import { isToolUseModeFunction } from './assistant'
|
||||
import { convertBase64ImageToAwsBedrockFormat } from './aws-bedrock-utils'
|
||||
import { filterProperties, processSchemaForO3 } from './mcp-schema'
|
||||
import { filterProperties } from './mcp-schema'
|
||||
|
||||
const logger = loggerService.withContext('Utils:MCPTools')
|
||||
|
||||
export function mcpToolsToOpenAIResponseTools(mcpTools: MCPTool[]): OpenAI.Responses.Tool[] {
|
||||
return mcpTools.map((tool) => {
|
||||
const parameters = processSchemaForO3(tool.inputSchema)
|
||||
// Use the original schema without strict mode processing to maintain compatibility with all MCP types
|
||||
const parameters = tool.inputSchema
|
||||
|
||||
return {
|
||||
type: 'function',
|
||||
@@ -46,15 +47,15 @@ export function mcpToolsToOpenAIResponseTools(mcpTools: MCPTool[]): OpenAI.Respo
|
||||
parameters: {
|
||||
type: 'object' as const,
|
||||
...parameters
|
||||
},
|
||||
strict: true
|
||||
}
|
||||
} satisfies OpenAI.Responses.Tool
|
||||
})
|
||||
}
|
||||
|
||||
export function mcpToolsToOpenAIChatTools(mcpTools: MCPTool[]): Array<ChatCompletionTool> {
|
||||
return mcpTools.map((tool) => {
|
||||
const parameters = processSchemaForO3(tool.inputSchema)
|
||||
// Use the original schema without strict mode processing to maintain compatibility with all MCP types
|
||||
const parameters = tool.inputSchema
|
||||
|
||||
return {
|
||||
type: 'function',
|
||||
@@ -64,8 +65,7 @@ export function mcpToolsToOpenAIChatTools(mcpTools: MCPTool[]): Array<ChatComple
|
||||
parameters: {
|
||||
type: 'object' as const,
|
||||
...parameters
|
||||
},
|
||||
strict: true
|
||||
}
|
||||
}
|
||||
} as ChatCompletionTool
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user