mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 14:41:24 +08:00
Compare commits
11 Commits
f3fc8f88e3
...
dac3067910
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dac3067910 | ||
|
|
8ab375161d | ||
|
|
42260710d8 | ||
|
|
5e8646c6a5 | ||
|
|
7e93e8b9b2 | ||
|
|
6d06aef563 | ||
|
|
cd4c5589db | ||
|
|
69cb5eaed4 | ||
|
|
304ab82e86 | ||
|
|
8d227a5164 | ||
|
|
f30f06f40f |
@ -1,5 +1,5 @@
|
||||
diff --git a/dist/index.js b/dist/index.js
|
||||
index 51ce7e423934fb717cb90245cdfcdb3dae6780e6..0f7f7009e2f41a79a8669d38c8a44867bbff5e1f 100644
|
||||
index d004b415c5841a1969705823614f395265ea5a8a..6b1e0dad4610b0424393ecc12e9114723bbe316b 100644
|
||||
--- a/dist/index.js
|
||||
+++ b/dist/index.js
|
||||
@@ -474,7 +474,7 @@ function convertToGoogleGenerativeAIMessages(prompt, options) {
|
||||
@ -12,7 +12,7 @@ index 51ce7e423934fb717cb90245cdfcdb3dae6780e6..0f7f7009e2f41a79a8669d38c8a44867
|
||||
|
||||
// src/google-generative-ai-options.ts
|
||||
diff --git a/dist/index.mjs b/dist/index.mjs
|
||||
index f4b77e35c0cbfece85a3ef0d4f4e67aa6dde6271..8d2fecf8155a226006a0bde72b00b6036d4014b6 100644
|
||||
index 1780dd2391b7f42224a0b8048c723d2f81222c44..1f12ed14399d6902107ce9b435d7d8e6cc61e06b 100644
|
||||
--- a/dist/index.mjs
|
||||
+++ b/dist/index.mjs
|
||||
@@ -480,7 +480,7 @@ function convertToGoogleGenerativeAIMessages(prompt, options) {
|
||||
@ -24,3 +24,14 @@ index f4b77e35c0cbfece85a3ef0d4f4e67aa6dde6271..8d2fecf8155a226006a0bde72b00b603
|
||||
}
|
||||
|
||||
// src/google-generative-ai-options.ts
|
||||
@@ -1909,8 +1909,7 @@ function createGoogleGenerativeAI(options = {}) {
|
||||
}
|
||||
var google = createGoogleGenerativeAI();
|
||||
export {
|
||||
- VERSION,
|
||||
createGoogleGenerativeAI,
|
||||
- google
|
||||
+ google, VERSION
|
||||
};
|
||||
//# sourceMappingURL=index.mjs.map
|
||||
\ No newline at end of file
|
||||
@ -48,7 +48,8 @@ export default defineConfig([
|
||||
'@eslint-react/no-unstable-context-value': 'off',
|
||||
'@eslint-react/hooks-extra/prefer-use-state-lazy-initialization': 'off',
|
||||
'@eslint-react/hooks-extra/no-unnecessary-use-prefix': 'off',
|
||||
'@eslint-react/no-children-to-array': 'off'
|
||||
'@eslint-react/no-children-to-array': 'off',
|
||||
'@eslint-react/dom/no-unsafe-iframe-sandbox': 'off'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@ -114,8 +114,8 @@
|
||||
"@ai-sdk/anthropic": "^2.0.49",
|
||||
"@ai-sdk/cerebras": "^1.0.31",
|
||||
"@ai-sdk/gateway": "^2.0.15",
|
||||
"@ai-sdk/google": "patch:@ai-sdk/google@npm%3A2.0.43#~/.yarn/patches/@ai-sdk-google-npm-2.0.43-689ed559b3.patch",
|
||||
"@ai-sdk/google-vertex": "^3.0.79",
|
||||
"@ai-sdk/google": "patch:@ai-sdk/google@npm%3A2.0.49#~/.yarn/patches/@ai-sdk-google-npm-2.0.49-84720f41bd.patch",
|
||||
"@ai-sdk/google-vertex": "^3.0.94",
|
||||
"@ai-sdk/huggingface": "^0.0.10",
|
||||
"@ai-sdk/mistral": "^2.0.24",
|
||||
"@ai-sdk/openai": "patch:@ai-sdk/openai@npm%3A2.0.85#~/.yarn/patches/@ai-sdk-openai-npm-2.0.85-27483d1d6a.patch",
|
||||
@ -416,7 +416,8 @@
|
||||
"@langchain/openai@npm:>=0.2.0 <0.7.0": "patch:@langchain/openai@npm%3A1.0.0#~/.yarn/patches/@langchain-openai-npm-1.0.0-474d0ad9d4.patch",
|
||||
"@ai-sdk/openai@npm:^2.0.42": "patch:@ai-sdk/openai@npm%3A2.0.85#~/.yarn/patches/@ai-sdk-openai-npm-2.0.85-27483d1d6a.patch",
|
||||
"@ai-sdk/google@npm:^2.0.40": "patch:@ai-sdk/google@npm%3A2.0.40#~/.yarn/patches/@ai-sdk-google-npm-2.0.40-47e0eeee83.patch",
|
||||
"@ai-sdk/openai-compatible@npm:^1.0.27": "patch:@ai-sdk/openai-compatible@npm%3A1.0.27#~/.yarn/patches/@ai-sdk-openai-compatible-npm-1.0.27-06f74278cf.patch"
|
||||
"@ai-sdk/openai-compatible@npm:^1.0.27": "patch:@ai-sdk/openai-compatible@npm%3A1.0.27#~/.yarn/patches/@ai-sdk-openai-compatible-npm-1.0.27-06f74278cf.patch",
|
||||
"@ai-sdk/google@npm:2.0.49": "patch:@ai-sdk/google@npm%3A2.0.49#~/.yarn/patches/@ai-sdk-google-npm-2.0.49-84720f41bd.patch"
|
||||
},
|
||||
"packageManager": "yarn@4.9.1",
|
||||
"lint-staged": {
|
||||
|
||||
@ -69,7 +69,7 @@ export abstract class OpenAIBaseClient<
|
||||
const sdk = await this.getSdkInstance()
|
||||
const response = (await sdk.request({
|
||||
method: 'post',
|
||||
path: '/images/generations',
|
||||
path: '/v1/images/generations',
|
||||
signal,
|
||||
body: {
|
||||
model,
|
||||
|
||||
@ -79,7 +79,7 @@ vi.mock('@renderer/services/AssistantService', () => ({
|
||||
import { getProviderByModel } from '@renderer/services/AssistantService'
|
||||
import type { Model, Provider } from '@renderer/types'
|
||||
import { formatApiHost } from '@renderer/utils/api'
|
||||
import { isCherryAIProvider, isPerplexityProvider } from '@renderer/utils/provider'
|
||||
import { isAzureOpenAIProvider, isCherryAIProvider, isPerplexityProvider } from '@renderer/utils/provider'
|
||||
|
||||
import { COPILOT_DEFAULT_HEADERS, COPILOT_EDITOR_VERSION, isCopilotResponsesModel } from '../constants'
|
||||
import { getActualProvider, providerToAiSdkConfig } from '../providerConfig'
|
||||
@ -133,6 +133,17 @@ const createPerplexityProvider = (): Provider => ({
|
||||
isSystem: false
|
||||
})
|
||||
|
||||
const createAzureProvider = (apiVersion: string): Provider => ({
|
||||
id: 'azure-openai',
|
||||
type: 'azure-openai',
|
||||
name: 'Azure OpenAI',
|
||||
apiKey: 'test-key',
|
||||
apiHost: 'https://example.openai.azure.com/openai',
|
||||
apiVersion,
|
||||
models: [],
|
||||
isSystem: true
|
||||
})
|
||||
|
||||
describe('Copilot responses routing', () => {
|
||||
beforeEach(() => {
|
||||
;(globalThis as any).window = {
|
||||
@ -504,3 +515,46 @@ describe('Stream options includeUsage configuration', () => {
|
||||
expect(config.providerId).toBe('github-copilot-openai-compatible')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Azure OpenAI traditional API routing', () => {
|
||||
beforeEach(() => {
|
||||
;(globalThis as any).window = {
|
||||
...(globalThis as any).window,
|
||||
keyv: createWindowKeyv()
|
||||
}
|
||||
mockGetState.mockReturnValue({
|
||||
settings: {
|
||||
openAI: {
|
||||
streamOptions: {
|
||||
includeUsage: undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
vi.mocked(isAzureOpenAIProvider).mockImplementation((provider) => provider.type === 'azure-openai')
|
||||
})
|
||||
|
||||
it('uses deployment-based URLs when apiVersion is a date version', () => {
|
||||
const provider = createAzureProvider('2024-02-15-preview')
|
||||
const config = providerToAiSdkConfig(provider, createModel('gpt-4o', 'GPT-4o', provider.id))
|
||||
|
||||
expect(config.providerId).toBe('azure')
|
||||
expect(config.options.apiVersion).toBe('2024-02-15-preview')
|
||||
expect(config.options.useDeploymentBasedUrls).toBe(true)
|
||||
})
|
||||
|
||||
it('does not force deployment-based URLs for apiVersion v1/preview', () => {
|
||||
const v1Provider = createAzureProvider('v1')
|
||||
const v1Config = providerToAiSdkConfig(v1Provider, createModel('gpt-4o', 'GPT-4o', v1Provider.id))
|
||||
expect(v1Config.providerId).toBe('azure-responses')
|
||||
expect(v1Config.options.apiVersion).toBe('v1')
|
||||
expect(v1Config.options.useDeploymentBasedUrls).toBeUndefined()
|
||||
|
||||
const previewProvider = createAzureProvider('preview')
|
||||
const previewConfig = providerToAiSdkConfig(previewProvider, createModel('gpt-4o', 'GPT-4o', previewProvider.id))
|
||||
expect(previewConfig.providerId).toBe('azure-responses')
|
||||
expect(previewConfig.options.apiVersion).toBe('preview')
|
||||
expect(previewConfig.options.useDeploymentBasedUrls).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@ -214,6 +214,15 @@ export function providerToAiSdkConfig(actualProvider: Provider, model: Model): A
|
||||
} else if (aiSdkProviderId === 'azure') {
|
||||
extraOptions.mode = 'chat'
|
||||
}
|
||||
if (isAzureOpenAIProvider(actualProvider)) {
|
||||
const apiVersion = actualProvider.apiVersion?.trim()
|
||||
if (apiVersion) {
|
||||
extraOptions.apiVersion = apiVersion
|
||||
if (!['preview', 'v1'].includes(apiVersion)) {
|
||||
extraOptions.useDeploymentBasedUrls = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// bedrock
|
||||
if (aiSdkProviderId === 'bedrock') {
|
||||
|
||||
@ -36,7 +36,7 @@ import {
|
||||
} from '@renderer/config/models'
|
||||
import { getStoreSetting } from '@renderer/hooks/useSettings'
|
||||
import { getAssistantSettings, getProviderByModel } from '@renderer/services/AssistantService'
|
||||
import type { Assistant, Model } from '@renderer/types'
|
||||
import type { Assistant, Model, ReasoningEffortOption } from '@renderer/types'
|
||||
import { EFFORT_RATIO, isSystemProvider, SystemProviderIds } from '@renderer/types'
|
||||
import type { OpenAIReasoningSummary } from '@renderer/types/aiCoreTypes'
|
||||
import type { ReasoningEffortOptionalParams } from '@renderer/types/sdk'
|
||||
@ -539,20 +539,25 @@ export function getAnthropicReasoningParams(
|
||||
return {}
|
||||
}
|
||||
|
||||
// type GoogleThinkingLevel = NonNullable<GoogleGenerativeAIProviderOptions['thinkingConfig']>['thinkingLevel']
|
||||
type GoogleThinkingLevel = NonNullable<GoogleGenerativeAIProviderOptions['thinkingConfig']>['thinkingLevel']
|
||||
|
||||
// function mapToGeminiThinkingLevel(reasoningEffort: ReasoningEffortOption): GoogelThinkingLevel {
|
||||
// switch (reasoningEffort) {
|
||||
// case 'low':
|
||||
// return 'low'
|
||||
// case 'medium':
|
||||
// return 'medium'
|
||||
// case 'high':
|
||||
// return 'high'
|
||||
// default:
|
||||
// return 'medium'
|
||||
// }
|
||||
// }
|
||||
function mapToGeminiThinkingLevel(reasoningEffort: ReasoningEffortOption): GoogleThinkingLevel {
|
||||
switch (reasoningEffort) {
|
||||
case 'default':
|
||||
return undefined
|
||||
case 'minimal':
|
||||
return 'minimal'
|
||||
case 'low':
|
||||
return 'low'
|
||||
case 'medium':
|
||||
return 'medium'
|
||||
case 'high':
|
||||
return 'high'
|
||||
default:
|
||||
logger.warn('Unknown thinking level for Gemini. Fallback to medium instead.', { reasoningEffort })
|
||||
return 'medium'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Gemini 推理参数
|
||||
@ -585,15 +590,15 @@ export function getGeminiReasoningParams(
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: 很多中转还不支持
|
||||
// https://ai.google.dev/gemini-api/docs/gemini-3?thinking=high#new_api_features_in_gemini_3
|
||||
// if (isGemini3ThinkingTokenModel(model)) {
|
||||
// return {
|
||||
// thinkingConfig: {
|
||||
// thinkingLevel: mapToGeminiThinkingLevel(reasoningEffort)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
if (isGemini3ThinkingTokenModel(model)) {
|
||||
return {
|
||||
thinkingConfig: {
|
||||
includeThoughts: true,
|
||||
thinkingLevel: mapToGeminiThinkingLevel(reasoningEffort)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const effortRatio = EFFORT_RATIO[reasoningEffort]
|
||||
|
||||
|
||||
@ -695,15 +695,20 @@ describe('getThinkModelType - Comprehensive Coverage', () => {
|
||||
})
|
||||
|
||||
describe('Gemini models', () => {
|
||||
it('should return gemini for Flash models', () => {
|
||||
expect(getThinkModelType(createModel({ id: 'gemini-2.5-flash-latest' }))).toBe('gemini')
|
||||
expect(getThinkModelType(createModel({ id: 'gemini-flash-latest' }))).toBe('gemini')
|
||||
expect(getThinkModelType(createModel({ id: 'gemini-flash-lite-latest' }))).toBe('gemini')
|
||||
it('should return gemini2_flash for Flash models', () => {
|
||||
expect(getThinkModelType(createModel({ id: 'gemini-2.5-flash-latest' }))).toBe('gemini2_flash')
|
||||
})
|
||||
it('should return gemini3_flash for Gemini 3 Flash models', () => {
|
||||
expect(getThinkModelType(createModel({ id: 'gemini-3-flash-preview' }))).toBe('gemini3_flash')
|
||||
expect(getThinkModelType(createModel({ id: 'gemini-flash-latest' }))).toBe('gemini3_flash')
|
||||
})
|
||||
|
||||
it('should return gemini_pro for Pro models', () => {
|
||||
expect(getThinkModelType(createModel({ id: 'gemini-2.5-pro-latest' }))).toBe('gemini_pro')
|
||||
expect(getThinkModelType(createModel({ id: 'gemini-pro-latest' }))).toBe('gemini_pro')
|
||||
it('should return gemini2_pro for Gemini 2.5 Pro models', () => {
|
||||
expect(getThinkModelType(createModel({ id: 'gemini-2.5-pro-latest' }))).toBe('gemini2_pro')
|
||||
})
|
||||
it('should return gemini3_pro for Gemini 3 Pro models', () => {
|
||||
expect(getThinkModelType(createModel({ id: 'gemini-3-pro-preview' }))).toBe('gemini3_pro')
|
||||
expect(getThinkModelType(createModel({ id: 'gemini-pro-latest' }))).toBe('gemini3_pro')
|
||||
})
|
||||
})
|
||||
|
||||
@ -810,7 +815,7 @@ describe('getThinkModelType - Comprehensive Coverage', () => {
|
||||
name: 'gemini-2.5-flash-latest'
|
||||
})
|
||||
)
|
||||
).toBe('gemini')
|
||||
).toBe('gemini2_flash')
|
||||
})
|
||||
|
||||
it('should use id result when id matches', () => {
|
||||
@ -835,7 +840,7 @@ describe('getThinkModelType - Comprehensive Coverage', () => {
|
||||
|
||||
it('should handle case insensitivity correctly', () => {
|
||||
expect(getThinkModelType(createModel({ id: 'GPT-5.1' }))).toBe('gpt5_1')
|
||||
expect(getThinkModelType(createModel({ id: 'Gemini-2.5-Flash-Latest' }))).toBe('gemini')
|
||||
expect(getThinkModelType(createModel({ id: 'Gemini-2.5-Flash-Latest' }))).toBe('gemini2_flash')
|
||||
expect(getThinkModelType(createModel({ id: 'DeepSeek-V3.1' }))).toBe('deepseek_hybrid')
|
||||
})
|
||||
|
||||
@ -855,7 +860,7 @@ describe('getThinkModelType - Comprehensive Coverage', () => {
|
||||
it('should handle models with version suffixes', () => {
|
||||
expect(getThinkModelType(createModel({ id: 'gpt-5-preview-2024' }))).toBe('gpt5')
|
||||
expect(getThinkModelType(createModel({ id: 'o3-mini-2024' }))).toBe('o')
|
||||
expect(getThinkModelType(createModel({ id: 'gemini-2.5-flash-latest-001' }))).toBe('gemini')
|
||||
expect(getThinkModelType(createModel({ id: 'gemini-2.5-flash-latest-001' }))).toBe('gemini2_flash')
|
||||
})
|
||||
|
||||
it('should prioritize GPT-5.1 over GPT-5 detection', () => {
|
||||
@ -955,6 +960,14 @@ describe('Gemini Models', () => {
|
||||
group: ''
|
||||
})
|
||||
).toBe(true)
|
||||
expect(
|
||||
isSupportedThinkingTokenGeminiModel({
|
||||
id: 'gemini-3-flash-preview',
|
||||
name: '',
|
||||
provider: '',
|
||||
group: ''
|
||||
})
|
||||
).toBe(true)
|
||||
expect(
|
||||
isSupportedThinkingTokenGeminiModel({
|
||||
id: 'google/gemini-3-pro-preview',
|
||||
@ -996,6 +1009,31 @@ describe('Gemini Models', () => {
|
||||
group: ''
|
||||
})
|
||||
).toBe(true)
|
||||
// Version with date suffixes
|
||||
expect(
|
||||
isSupportedThinkingTokenGeminiModel({
|
||||
id: 'gemini-3-flash-preview-09-2025',
|
||||
name: '',
|
||||
provider: '',
|
||||
group: ''
|
||||
})
|
||||
).toBe(true)
|
||||
expect(
|
||||
isSupportedThinkingTokenGeminiModel({
|
||||
id: 'gemini-3-pro-preview-09-2025',
|
||||
name: '',
|
||||
provider: '',
|
||||
group: ''
|
||||
})
|
||||
).toBe(true)
|
||||
expect(
|
||||
isSupportedThinkingTokenGeminiModel({
|
||||
id: 'gemini-3-flash-exp-1234',
|
||||
name: '',
|
||||
provider: '',
|
||||
group: ''
|
||||
})
|
||||
).toBe(true)
|
||||
// Version with decimals
|
||||
expect(
|
||||
isSupportedThinkingTokenGeminiModel({
|
||||
@ -1015,7 +1053,8 @@ describe('Gemini Models', () => {
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('should return true for gemini-3 image models', () => {
|
||||
it('should return true for gemini-3-pro-image models only', () => {
|
||||
// Only gemini-3-pro-image models should return true
|
||||
expect(
|
||||
isSupportedThinkingTokenGeminiModel({
|
||||
id: 'gemini-3-pro-image-preview',
|
||||
@ -1024,6 +1063,17 @@ describe('Gemini Models', () => {
|
||||
group: ''
|
||||
})
|
||||
).toBe(true)
|
||||
expect(
|
||||
isSupportedThinkingTokenGeminiModel({
|
||||
id: 'gemini-3-pro-image',
|
||||
name: '',
|
||||
provider: '',
|
||||
group: ''
|
||||
})
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false for other gemini-3 image models', () => {
|
||||
expect(
|
||||
isSupportedThinkingTokenGeminiModel({
|
||||
id: 'gemini-3.0-flash-image-preview',
|
||||
@ -1086,6 +1136,22 @@ describe('Gemini Models', () => {
|
||||
group: ''
|
||||
})
|
||||
).toBe(false)
|
||||
expect(
|
||||
isSupportedThinkingTokenGeminiModel({
|
||||
id: 'gemini-3-flash-preview-tts',
|
||||
name: '',
|
||||
provider: '',
|
||||
group: ''
|
||||
})
|
||||
).toBe(false)
|
||||
expect(
|
||||
isSupportedThinkingTokenGeminiModel({
|
||||
id: 'gemini-3-pro-tts',
|
||||
name: '',
|
||||
provider: '',
|
||||
group: ''
|
||||
})
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false for older gemini models', () => {
|
||||
@ -1811,7 +1877,7 @@ describe('getModelSupportedReasoningEffortOptions', () => {
|
||||
|
||||
describe('Gemini models', () => {
|
||||
it('should return correct options for Gemini Flash models', () => {
|
||||
expect(getModelSupportedReasoningEffortOptions(createModel({ id: 'gemini-2.5-flash-latest' }))).toEqual([
|
||||
expect(getModelSupportedReasoningEffortOptions(createModel({ id: 'gemini-2.5-flash' }))).toEqual([
|
||||
'default',
|
||||
'none',
|
||||
'low',
|
||||
@ -1819,36 +1885,46 @@ describe('getModelSupportedReasoningEffortOptions', () => {
|
||||
'high',
|
||||
'auto'
|
||||
])
|
||||
expect(getModelSupportedReasoningEffortOptions(createModel({ id: 'gemini-flash-latest' }))).toEqual([
|
||||
expect(getModelSupportedReasoningEffortOptions(createModel({ id: 'gemini-3-flash-preview' }))).toEqual([
|
||||
'default',
|
||||
'none',
|
||||
'minimal',
|
||||
'low',
|
||||
'medium',
|
||||
'high',
|
||||
'auto'
|
||||
'high'
|
||||
])
|
||||
expect(getModelSupportedReasoningEffortOptions(createModel({ id: 'gemini-flash-latest' }))).toEqual([
|
||||
'default',
|
||||
'minimal',
|
||||
'low',
|
||||
'medium',
|
||||
'high'
|
||||
])
|
||||
})
|
||||
|
||||
it('should return correct options for Gemini Pro models', () => {
|
||||
expect(getModelSupportedReasoningEffortOptions(createModel({ id: 'gemini-2.5-pro-latest' }))).toEqual([
|
||||
expect(getModelSupportedReasoningEffortOptions(createModel({ id: 'gemini-2.5-pro' }))).toEqual([
|
||||
'default',
|
||||
'low',
|
||||
'medium',
|
||||
'high',
|
||||
'auto'
|
||||
])
|
||||
expect(getModelSupportedReasoningEffortOptions(createModel({ id: 'gemini-3-pro-preview' }))).toEqual([
|
||||
'default',
|
||||
'low',
|
||||
'high'
|
||||
])
|
||||
expect(getModelSupportedReasoningEffortOptions(createModel({ id: 'gemini-pro-latest' }))).toEqual([
|
||||
'default',
|
||||
'low',
|
||||
'medium',
|
||||
'high',
|
||||
'auto'
|
||||
'high'
|
||||
])
|
||||
})
|
||||
|
||||
it('should return correct options for Gemini 3 models', () => {
|
||||
expect(getModelSupportedReasoningEffortOptions(createModel({ id: 'gemini-3-flash' }))).toEqual([
|
||||
'default',
|
||||
'minimal',
|
||||
'low',
|
||||
'medium',
|
||||
'high'
|
||||
@ -1856,7 +1932,6 @@ describe('getModelSupportedReasoningEffortOptions', () => {
|
||||
expect(getModelSupportedReasoningEffortOptions(createModel({ id: 'gemini-3-pro-preview' }))).toEqual([
|
||||
'default',
|
||||
'low',
|
||||
'medium',
|
||||
'high'
|
||||
])
|
||||
})
|
||||
@ -2078,7 +2153,7 @@ describe('getModelSupportedReasoningEffortOptions', () => {
|
||||
|
||||
const geminiModel = createModel({ id: 'gemini-2.5-flash-latest' })
|
||||
const geminiResult = getModelSupportedReasoningEffortOptions(geminiModel)
|
||||
expect(geminiResult).toEqual(MODEL_SUPPORTED_OPTIONS.gemini)
|
||||
expect(geminiResult).toEqual(MODEL_SUPPORTED_OPTIONS.gemini2_flash)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -20,6 +20,8 @@ import {
|
||||
getModelSupportedVerbosity,
|
||||
groupQwenModels,
|
||||
isAnthropicModel,
|
||||
isGemini3FlashModel,
|
||||
isGemini3ProModel,
|
||||
isGeminiModel,
|
||||
isGemmaModel,
|
||||
isGenerateImageModels,
|
||||
@ -432,6 +434,101 @@ describe('model utils', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('isGemini3FlashModel', () => {
|
||||
it('detects gemini-3-flash model', () => {
|
||||
expect(isGemini3FlashModel(createModel({ id: 'gemini-3-flash' }))).toBe(true)
|
||||
})
|
||||
|
||||
it('detects gemini-3-flash-preview model', () => {
|
||||
expect(isGemini3FlashModel(createModel({ id: 'gemini-3-flash-preview' }))).toBe(true)
|
||||
})
|
||||
|
||||
it('detects gemini-3-flash with version suffixes', () => {
|
||||
expect(isGemini3FlashModel(createModel({ id: 'gemini-3-flash-latest' }))).toBe(true)
|
||||
expect(isGemini3FlashModel(createModel({ id: 'gemini-3-flash-preview-09-2025' }))).toBe(true)
|
||||
expect(isGemini3FlashModel(createModel({ id: 'gemini-3-flash-exp-1234' }))).toBe(true)
|
||||
})
|
||||
|
||||
it('detects gemini-flash-latest alias', () => {
|
||||
expect(isGemini3FlashModel(createModel({ id: 'gemini-flash-latest' }))).toBe(true)
|
||||
expect(isGemini3FlashModel(createModel({ id: 'Gemini-Flash-Latest' }))).toBe(true)
|
||||
})
|
||||
|
||||
it('detects gemini-3-flash with uppercase', () => {
|
||||
expect(isGemini3FlashModel(createModel({ id: 'Gemini-3-Flash' }))).toBe(true)
|
||||
expect(isGemini3FlashModel(createModel({ id: 'GEMINI-3-FLASH-PREVIEW' }))).toBe(true)
|
||||
})
|
||||
|
||||
it('excludes gemini-3-flash-image models', () => {
|
||||
expect(isGemini3FlashModel(createModel({ id: 'gemini-3-flash-image-preview' }))).toBe(false)
|
||||
expect(isGemini3FlashModel(createModel({ id: 'gemini-3-flash-image' }))).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false for non-flash gemini-3 models', () => {
|
||||
expect(isGemini3FlashModel(createModel({ id: 'gemini-3-pro' }))).toBe(false)
|
||||
expect(isGemini3FlashModel(createModel({ id: 'gemini-3-pro-preview' }))).toBe(false)
|
||||
expect(isGemini3FlashModel(createModel({ id: 'gemini-3-pro-image-preview' }))).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false for other gemini models', () => {
|
||||
expect(isGemini3FlashModel(createModel({ id: 'gemini-2-flash' }))).toBe(false)
|
||||
expect(isGemini3FlashModel(createModel({ id: 'gemini-2-flash-preview' }))).toBe(false)
|
||||
expect(isGemini3FlashModel(createModel({ id: 'gemini-2.5-flash-preview-09-2025' }))).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false for null/undefined models', () => {
|
||||
expect(isGemini3FlashModel(null)).toBe(false)
|
||||
expect(isGemini3FlashModel(undefined)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isGemini3ProModel', () => {
|
||||
it('detects gemini-3-pro model', () => {
|
||||
expect(isGemini3ProModel(createModel({ id: 'gemini-3-pro' }))).toBe(true)
|
||||
})
|
||||
|
||||
it('detects gemini-3-pro-preview model', () => {
|
||||
expect(isGemini3ProModel(createModel({ id: 'gemini-3-pro-preview' }))).toBe(true)
|
||||
})
|
||||
|
||||
it('detects gemini-3-pro with version suffixes', () => {
|
||||
expect(isGemini3ProModel(createModel({ id: 'gemini-3-pro-latest' }))).toBe(true)
|
||||
expect(isGemini3ProModel(createModel({ id: 'gemini-3-pro-preview-09-2025' }))).toBe(true)
|
||||
expect(isGemini3ProModel(createModel({ id: 'gemini-3-pro-exp-1234' }))).toBe(true)
|
||||
})
|
||||
|
||||
it('detects gemini-pro-latest alias', () => {
|
||||
expect(isGemini3ProModel(createModel({ id: 'gemini-pro-latest' }))).toBe(true)
|
||||
expect(isGemini3ProModel(createModel({ id: 'Gemini-Pro-Latest' }))).toBe(true)
|
||||
})
|
||||
|
||||
it('detects gemini-3-pro with uppercase', () => {
|
||||
expect(isGemini3ProModel(createModel({ id: 'Gemini-3-Pro' }))).toBe(true)
|
||||
expect(isGemini3ProModel(createModel({ id: 'GEMINI-3-PRO-PREVIEW' }))).toBe(true)
|
||||
})
|
||||
|
||||
it('excludes gemini-3-pro-image models', () => {
|
||||
expect(isGemini3ProModel(createModel({ id: 'gemini-3-pro-image-preview' }))).toBe(false)
|
||||
expect(isGemini3ProModel(createModel({ id: 'gemini-3-pro-image' }))).toBe(false)
|
||||
expect(isGemini3ProModel(createModel({ id: 'gemini-3-pro-image-latest' }))).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false for non-pro gemini-3 models', () => {
|
||||
expect(isGemini3ProModel(createModel({ id: 'gemini-3-flash' }))).toBe(false)
|
||||
expect(isGemini3ProModel(createModel({ id: 'gemini-3-flash-preview' }))).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false for other gemini models', () => {
|
||||
expect(isGemini3ProModel(createModel({ id: 'gemini-2-pro' }))).toBe(false)
|
||||
expect(isGemini3ProModel(createModel({ id: 'gemini-2.5-pro-preview-09-2025' }))).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false for null/undefined models', () => {
|
||||
expect(isGemini3ProModel(null)).toBe(false)
|
||||
expect(isGemini3ProModel(undefined)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isZhipuModel', () => {
|
||||
it('detects Zhipu models by provider', () => {
|
||||
expect(isZhipuModel(createModel({ provider: 'zhipu' }))).toBe(true)
|
||||
|
||||
@ -20,7 +20,7 @@ import {
|
||||
isOpenAIReasoningModel,
|
||||
isSupportedReasoningEffortOpenAIModel
|
||||
} from './openai'
|
||||
import { GEMINI_FLASH_MODEL_REGEX, isGemini3ThinkingTokenModel } from './utils'
|
||||
import { GEMINI_FLASH_MODEL_REGEX, isGemini3FlashModel, isGemini3ProModel } from './utils'
|
||||
import { isTextToImageModel } from './vision'
|
||||
|
||||
// Reasoning models
|
||||
@ -43,9 +43,10 @@ export const MODEL_SUPPORTED_REASONING_EFFORT = {
|
||||
gpt52pro: ['medium', 'high', 'xhigh'] as const,
|
||||
grok: ['low', 'high'] as const,
|
||||
grok4_fast: ['auto'] as const,
|
||||
gemini: ['low', 'medium', 'high', 'auto'] as const,
|
||||
gemini3: ['low', 'medium', 'high'] as const,
|
||||
gemini_pro: ['low', 'medium', 'high', 'auto'] as const,
|
||||
gemini2_flash: ['low', 'medium', 'high', 'auto'] as const,
|
||||
gemini2_pro: ['low', 'medium', 'high', 'auto'] as const,
|
||||
gemini3_flash: ['minimal', 'low', 'medium', 'high'] as const,
|
||||
gemini3_pro: ['low', 'high'] as const,
|
||||
qwen: ['low', 'medium', 'high'] as const,
|
||||
qwen_thinking: ['low', 'medium', 'high'] as const,
|
||||
doubao: ['auto', 'high'] as const,
|
||||
@ -73,9 +74,10 @@ export const MODEL_SUPPORTED_OPTIONS: ThinkingOptionConfig = {
|
||||
gpt52pro: ['default', ...MODEL_SUPPORTED_REASONING_EFFORT.gpt52pro] as const,
|
||||
grok: ['default', ...MODEL_SUPPORTED_REASONING_EFFORT.grok] as const,
|
||||
grok4_fast: ['default', 'none', ...MODEL_SUPPORTED_REASONING_EFFORT.grok4_fast] as const,
|
||||
gemini: ['default', 'none', ...MODEL_SUPPORTED_REASONING_EFFORT.gemini] as const,
|
||||
gemini_pro: ['default', ...MODEL_SUPPORTED_REASONING_EFFORT.gemini_pro] as const,
|
||||
gemini3: ['default', ...MODEL_SUPPORTED_REASONING_EFFORT.gemini3] as const,
|
||||
gemini2_flash: ['default', 'none', ...MODEL_SUPPORTED_REASONING_EFFORT.gemini2_flash] as const,
|
||||
gemini2_pro: ['default', ...MODEL_SUPPORTED_REASONING_EFFORT.gemini2_pro] as const,
|
||||
gemini3_flash: ['default', ...MODEL_SUPPORTED_REASONING_EFFORT.gemini3_flash] as const,
|
||||
gemini3_pro: ['default', ...MODEL_SUPPORTED_REASONING_EFFORT.gemini3_pro] as const,
|
||||
qwen: ['default', 'none', ...MODEL_SUPPORTED_REASONING_EFFORT.qwen] as const,
|
||||
qwen_thinking: ['default', ...MODEL_SUPPORTED_REASONING_EFFORT.qwen_thinking] as const,
|
||||
doubao: ['default', 'none', ...MODEL_SUPPORTED_REASONING_EFFORT.doubao] as const,
|
||||
@ -102,8 +104,7 @@ const _getThinkModelType = (model: Model): ThinkingModelType => {
|
||||
const modelId = getLowerBaseModelName(model.id)
|
||||
if (isOpenAIDeepResearchModel(model)) {
|
||||
return 'openai_deep_research'
|
||||
}
|
||||
if (isGPT51SeriesModel(model)) {
|
||||
} else if (isGPT51SeriesModel(model)) {
|
||||
if (modelId.includes('codex')) {
|
||||
thinkingModelType = 'gpt5_1_codex'
|
||||
if (isGPT51CodexMaxModel(model)) {
|
||||
@ -131,16 +132,18 @@ const _getThinkModelType = (model: Model): ThinkingModelType => {
|
||||
} else if (isGrok4FastReasoningModel(model)) {
|
||||
thinkingModelType = 'grok4_fast'
|
||||
} else if (isSupportedThinkingTokenGeminiModel(model)) {
|
||||
if (GEMINI_FLASH_MODEL_REGEX.test(model.id)) {
|
||||
thinkingModelType = 'gemini'
|
||||
if (isGemini3FlashModel(model)) {
|
||||
thinkingModelType = 'gemini3_flash'
|
||||
} else if (isGemini3ProModel(model)) {
|
||||
thinkingModelType = 'gemini3_pro'
|
||||
} else if (GEMINI_FLASH_MODEL_REGEX.test(model.id)) {
|
||||
thinkingModelType = 'gemini2_flash'
|
||||
} else {
|
||||
thinkingModelType = 'gemini_pro'
|
||||
thinkingModelType = 'gemini2_pro'
|
||||
}
|
||||
if (isGemini3ThinkingTokenModel(model)) {
|
||||
thinkingModelType = 'gemini3'
|
||||
}
|
||||
} else if (isSupportedReasoningEffortGrokModel(model)) thinkingModelType = 'grok'
|
||||
else if (isSupportedThinkingTokenQwenModel(model)) {
|
||||
} else if (isSupportedReasoningEffortGrokModel(model)) {
|
||||
thinkingModelType = 'grok'
|
||||
} else if (isSupportedThinkingTokenQwenModel(model)) {
|
||||
if (isQwenAlwaysThinkModel(model)) {
|
||||
thinkingModelType = 'qwen_thinking'
|
||||
}
|
||||
@ -153,11 +156,17 @@ const _getThinkModelType = (model: Model): ThinkingModelType => {
|
||||
} else {
|
||||
thinkingModelType = 'doubao_no_auto'
|
||||
}
|
||||
} else if (isSupportedThinkingTokenHunyuanModel(model)) thinkingModelType = 'hunyuan'
|
||||
else if (isSupportedReasoningEffortPerplexityModel(model)) thinkingModelType = 'perplexity'
|
||||
else if (isSupportedThinkingTokenZhipuModel(model)) thinkingModelType = 'zhipu'
|
||||
else if (isDeepSeekHybridInferenceModel(model)) thinkingModelType = 'deepseek_hybrid'
|
||||
else if (isSupportedThinkingTokenMiMoModel(model)) thinkingModelType = 'mimo'
|
||||
} else if (isSupportedThinkingTokenHunyuanModel(model)) {
|
||||
thinkingModelType = 'hunyuan'
|
||||
} else if (isSupportedReasoningEffortPerplexityModel(model)) {
|
||||
thinkingModelType = 'perplexity'
|
||||
} else if (isSupportedThinkingTokenZhipuModel(model)) {
|
||||
thinkingModelType = 'zhipu'
|
||||
} else if (isDeepSeekHybridInferenceModel(model)) {
|
||||
thinkingModelType = 'deepseek_hybrid'
|
||||
} else if (isSupportedThinkingTokenMiMoModel(model)) {
|
||||
thinkingModelType = 'mimo'
|
||||
}
|
||||
return thinkingModelType
|
||||
}
|
||||
|
||||
|
||||
@ -267,3 +267,43 @@ export const isGemini3ThinkingTokenModel = (model: Model) => {
|
||||
const modelId = getLowerBaseModelName(model.id)
|
||||
return isGemini3Model(model) && !modelId.includes('image')
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the model is a Gemini 3 Flash model
|
||||
* Matches: gemini-3-flash, gemini-3-flash-preview, gemini-3-flash-preview-09-2025, gemini-flash-latest (alias)
|
||||
* Excludes: gemini-3-flash-image-preview
|
||||
* @param model - The model to check
|
||||
* @returns true if the model is a Gemini 3 Flash model
|
||||
*/
|
||||
export const isGemini3FlashModel = (model: Model | undefined | null): boolean => {
|
||||
if (!model) {
|
||||
return false
|
||||
}
|
||||
const modelId = getLowerBaseModelName(model.id)
|
||||
// Check for gemini-flash-latest alias (currently points to gemini-3-flash, may change in future)
|
||||
if (modelId === 'gemini-flash-latest') {
|
||||
return true
|
||||
}
|
||||
// Check for gemini-3-flash with optional suffixes, excluding image variants
|
||||
return /gemini-3-flash(?!-image)(?:-[\w-]+)*$/i.test(modelId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the model is a Gemini 3 Pro model
|
||||
* Matches: gemini-3-pro, gemini-3-pro-preview, gemini-3-pro-preview-09-2025, gemini-pro-latest (alias)
|
||||
* Excludes: gemini-3-pro-image-preview
|
||||
* @param model - The model to check
|
||||
* @returns true if the model is a Gemini 3 Pro model
|
||||
*/
|
||||
export const isGemini3ProModel = (model: Model | undefined | null): boolean => {
|
||||
if (!model) {
|
||||
return false
|
||||
}
|
||||
const modelId = getLowerBaseModelName(model.id)
|
||||
// Check for gemini-pro-latest alias (currently points to gemini-3-pro, may change in future)
|
||||
if (modelId === 'gemini-pro-latest') {
|
||||
return true
|
||||
}
|
||||
// Check for gemini-3-pro with optional suffixes, excluding image variants
|
||||
return /gemini-3-pro(?!-image)(?:-[\w-]+)*$/i.test(modelId)
|
||||
}
|
||||
|
||||
@ -4114,6 +4114,35 @@
|
||||
},
|
||||
"availableTools": "Available Tools",
|
||||
"enable": "Enable Tool",
|
||||
"execute": {
|
||||
"button": "Execute",
|
||||
"copied": "Copied to clipboard",
|
||||
"copy": "Copy",
|
||||
"copyFailed": "Failed to copy",
|
||||
"error": {
|
||||
"invalidJson": "Invalid JSON format: {{error}}",
|
||||
"noToolOrServer": "Tool or server not found"
|
||||
},
|
||||
"execute": "Execute",
|
||||
"label": "Execute",
|
||||
"params": "Parameters (JSON)",
|
||||
"paramsPlaceholder": "Enter JSON parameters...",
|
||||
"result": {
|
||||
"error": "Error Result",
|
||||
"success": "Result",
|
||||
"text": "Text"
|
||||
},
|
||||
"table": {
|
||||
"name": "Name",
|
||||
"value": "Value"
|
||||
},
|
||||
"title": "Execute Tool: {{name}}",
|
||||
"tooltip": "Execute tool with custom parameters",
|
||||
"view": {
|
||||
"formatted": "Formatted",
|
||||
"json": "JSON"
|
||||
}
|
||||
},
|
||||
"inputSchema": {
|
||||
"enum": {
|
||||
"allowedValues": "Allowed Values"
|
||||
|
||||
@ -560,7 +560,7 @@
|
||||
"medium": "斟酌",
|
||||
"medium_description": "中强度推理",
|
||||
"minimal": "微念",
|
||||
"minimal_description": "最小程度的思考",
|
||||
"minimal_description": "最小程度的推理",
|
||||
"off": "关闭",
|
||||
"off_description": "禁用推理",
|
||||
"xhigh": "穷究",
|
||||
@ -4114,6 +4114,35 @@
|
||||
},
|
||||
"availableTools": "可用工具",
|
||||
"enable": "启用工具",
|
||||
"execute": {
|
||||
"button": "执行",
|
||||
"copied": "已复制到剪贴板",
|
||||
"copy": "复制",
|
||||
"copyFailed": "复制失败",
|
||||
"error": {
|
||||
"invalidJson": "无效的 JSON 格式: {{error}}",
|
||||
"noToolOrServer": "未找到工具或服务器"
|
||||
},
|
||||
"execute": "执行",
|
||||
"label": "执行",
|
||||
"params": "参数 (JSON)",
|
||||
"paramsPlaceholder": "输入 JSON 参数...",
|
||||
"result": {
|
||||
"error": "错误结果",
|
||||
"success": "结果",
|
||||
"text": "文本"
|
||||
},
|
||||
"table": {
|
||||
"name": "名称",
|
||||
"value": "值"
|
||||
},
|
||||
"title": "执行工具: {{name}}",
|
||||
"tooltip": "使用自定义参数执行工具",
|
||||
"view": {
|
||||
"formatted": "美化",
|
||||
"json": "JSON"
|
||||
}
|
||||
},
|
||||
"inputSchema": {
|
||||
"enum": {
|
||||
"allowedValues": "允许的值"
|
||||
|
||||
@ -4114,6 +4114,35 @@
|
||||
},
|
||||
"availableTools": "可用工具",
|
||||
"enable": "啟用工具",
|
||||
"execute": {
|
||||
"button": "執行",
|
||||
"copied": "已複製到剪貼板",
|
||||
"copy": "複製",
|
||||
"copyFailed": "複製失敗",
|
||||
"error": {
|
||||
"invalidJson": "無效的 JSON 格式: {{error}}",
|
||||
"noToolOrServer": "未找到工具或伺服器"
|
||||
},
|
||||
"execute": "執行",
|
||||
"label": "執行",
|
||||
"params": "參數 (JSON)",
|
||||
"paramsPlaceholder": "輸入 JSON 參數...",
|
||||
"result": {
|
||||
"error": "錯誤結果",
|
||||
"success": "結果",
|
||||
"text": "文字"
|
||||
},
|
||||
"table": {
|
||||
"name": "名稱",
|
||||
"value": "值"
|
||||
},
|
||||
"title": "執行工具: {{name}}",
|
||||
"tooltip": "使用自訂參數執行工具",
|
||||
"view": {
|
||||
"formatted": "美化",
|
||||
"json": "JSON"
|
||||
}
|
||||
},
|
||||
"inputSchema": {
|
||||
"enum": {
|
||||
"allowedValues": "允許的值"
|
||||
|
||||
@ -4114,6 +4114,35 @@
|
||||
},
|
||||
"availableTools": "Verfügbare Tools",
|
||||
"enable": "Tool aktivieren",
|
||||
"execute": {
|
||||
"button": "Ausführen",
|
||||
"copied": "In Zwischenablage kopiert",
|
||||
"copy": "Kopieren",
|
||||
"copyFailed": "Kopieren fehlgeschlagen",
|
||||
"error": {
|
||||
"invalidJson": "Ungültiges JSON-Format: {{error}}",
|
||||
"noToolOrServer": "Tool oder Server nicht gefunden"
|
||||
},
|
||||
"execute": "Ausführen",
|
||||
"label": "Ausführen",
|
||||
"params": "Parameter (JSON)",
|
||||
"paramsPlaceholder": "JSON-Parameter eingeben...",
|
||||
"result": {
|
||||
"error": "Fehlerergebnis",
|
||||
"success": "Ergebnis",
|
||||
"text": "Text"
|
||||
},
|
||||
"table": {
|
||||
"name": "Name",
|
||||
"value": "Wert"
|
||||
},
|
||||
"title": "Tool ausführen: {{name}}",
|
||||
"tooltip": "Tool mit benutzerdefinierten Parametern ausführen",
|
||||
"view": {
|
||||
"formatted": "Formatiert",
|
||||
"json": "JSON"
|
||||
}
|
||||
},
|
||||
"inputSchema": {
|
||||
"enum": {
|
||||
"allowedValues": "Erlaubte Werte"
|
||||
|
||||
@ -4114,6 +4114,35 @@
|
||||
},
|
||||
"availableTools": "Διαθέσιμα Εργαλεία",
|
||||
"enable": "Ενεργοποίηση εργαλείου",
|
||||
"execute": {
|
||||
"button": "Εκτέλεση",
|
||||
"copied": "Αντιγράφηκε στο πρόχειρο",
|
||||
"copy": "Αντιγραφή",
|
||||
"copyFailed": "Αποτυχία αντιγραφής",
|
||||
"error": {
|
||||
"invalidJson": "Μη έγκυρη μορφή JSON: {{error}}",
|
||||
"noToolOrServer": "Εργαλείο ή διακομιστής δεν βρέθηκε"
|
||||
},
|
||||
"execute": "Εκτέλεση",
|
||||
"label": "Εκτέλεση",
|
||||
"params": "Παράμετροι (JSON)",
|
||||
"paramsPlaceholder": "Εισαγάγετε παραμέτρους JSON...",
|
||||
"result": {
|
||||
"error": "Αποτέλεσμα σφάλματος",
|
||||
"success": "Αποτέλεσμα",
|
||||
"text": "Κείμενο"
|
||||
},
|
||||
"table": {
|
||||
"name": "Όνομα",
|
||||
"value": "Τιμή"
|
||||
},
|
||||
"title": "Εκτέλεση εργαλείου: {{name}}",
|
||||
"tooltip": "Εκτέλεση εργαλείου με προσαρμοσμένες παραμέτρους",
|
||||
"view": {
|
||||
"formatted": "Μορφοποιημένο",
|
||||
"json": "JSON"
|
||||
}
|
||||
},
|
||||
"inputSchema": {
|
||||
"enum": {
|
||||
"allowedValues": "Επιτρεπόμενες τιμές"
|
||||
|
||||
@ -4114,6 +4114,35 @@
|
||||
},
|
||||
"availableTools": "Herramientas disponibles",
|
||||
"enable": "Habilitar herramienta",
|
||||
"execute": {
|
||||
"button": "Ejecutar",
|
||||
"copied": "Copiado al portapapeles",
|
||||
"copy": "Copiar",
|
||||
"copyFailed": "Error al copiar",
|
||||
"error": {
|
||||
"invalidJson": "Formato JSON inválido: {{error}}",
|
||||
"noToolOrServer": "Herramienta o servidor no encontrado"
|
||||
},
|
||||
"execute": "Ejecutar",
|
||||
"label": "Ejecutar",
|
||||
"params": "Parámetros (JSON)",
|
||||
"paramsPlaceholder": "Ingrese parámetros JSON...",
|
||||
"result": {
|
||||
"error": "Resultado de error",
|
||||
"success": "Resultado",
|
||||
"text": "Texto"
|
||||
},
|
||||
"table": {
|
||||
"name": "Nombre",
|
||||
"value": "Valor"
|
||||
},
|
||||
"title": "Ejecutar herramienta: {{name}}",
|
||||
"tooltip": "Ejecutar herramienta con parámetros personalizados",
|
||||
"view": {
|
||||
"formatted": "Formateado",
|
||||
"json": "JSON"
|
||||
}
|
||||
},
|
||||
"inputSchema": {
|
||||
"enum": {
|
||||
"allowedValues": "Valores permitidos"
|
||||
|
||||
@ -4114,6 +4114,35 @@
|
||||
},
|
||||
"availableTools": "Outils disponibles",
|
||||
"enable": "Activer l'outil",
|
||||
"execute": {
|
||||
"button": "Exécuter",
|
||||
"copied": "Copié dans le presse-papiers",
|
||||
"copy": "Copier",
|
||||
"copyFailed": "Échec de la copie",
|
||||
"error": {
|
||||
"invalidJson": "Format JSON invalide: {{error}}",
|
||||
"noToolOrServer": "Outil ou serveur introuvable"
|
||||
},
|
||||
"execute": "Exécuter",
|
||||
"label": "Exécuter",
|
||||
"params": "Paramètres (JSON)",
|
||||
"paramsPlaceholder": "Entrez les paramètres JSON...",
|
||||
"result": {
|
||||
"error": "Résultat d'erreur",
|
||||
"success": "Résultat",
|
||||
"text": "Texte"
|
||||
},
|
||||
"table": {
|
||||
"name": "Nom",
|
||||
"value": "Valeur"
|
||||
},
|
||||
"title": "Exécuter l'outil: {{name}}",
|
||||
"tooltip": "Exécuter l'outil avec des paramètres personnalisés",
|
||||
"view": {
|
||||
"formatted": "Formaté",
|
||||
"json": "JSON"
|
||||
}
|
||||
},
|
||||
"inputSchema": {
|
||||
"enum": {
|
||||
"allowedValues": "Valeurs autorisées"
|
||||
|
||||
@ -4114,6 +4114,35 @@
|
||||
},
|
||||
"availableTools": "利用可能なツール",
|
||||
"enable": "ツールを有効にする",
|
||||
"execute": {
|
||||
"button": "実行",
|
||||
"copied": "クリップボードにコピーしました",
|
||||
"copy": "コピー",
|
||||
"copyFailed": "コピーに失敗しました",
|
||||
"error": {
|
||||
"invalidJson": "無効なJSON形式: {{error}}",
|
||||
"noToolOrServer": "ツールまたはサーバーが見つかりません"
|
||||
},
|
||||
"execute": "実行",
|
||||
"label": "実行",
|
||||
"params": "パラメータ (JSON)",
|
||||
"paramsPlaceholder": "JSONパラメータを入力...",
|
||||
"result": {
|
||||
"error": "エラー結果",
|
||||
"success": "結果",
|
||||
"text": "テキスト"
|
||||
},
|
||||
"table": {
|
||||
"name": "名前",
|
||||
"value": "値"
|
||||
},
|
||||
"title": "ツールを実行: {{name}}",
|
||||
"tooltip": "カスタムパラメータでツールを実行",
|
||||
"view": {
|
||||
"formatted": "フォーマット済み",
|
||||
"json": "JSON"
|
||||
}
|
||||
},
|
||||
"inputSchema": {
|
||||
"enum": {
|
||||
"allowedValues": "許可された値"
|
||||
|
||||
@ -4114,6 +4114,35 @@
|
||||
},
|
||||
"availableTools": "Ferramentas Disponíveis",
|
||||
"enable": "Habilitar Ferramenta",
|
||||
"execute": {
|
||||
"button": "Executar",
|
||||
"copied": "Copiado para a área de transferência",
|
||||
"copy": "Copiar",
|
||||
"copyFailed": "Falha ao copiar",
|
||||
"error": {
|
||||
"invalidJson": "Formato JSON inválido: {{error}}",
|
||||
"noToolOrServer": "Ferramenta ou servidor não encontrado"
|
||||
},
|
||||
"execute": "Executar",
|
||||
"label": "Executar",
|
||||
"params": "Parâmetros (JSON)",
|
||||
"paramsPlaceholder": "Digite os parâmetros JSON...",
|
||||
"result": {
|
||||
"error": "Resultado de erro",
|
||||
"success": "Resultado",
|
||||
"text": "Texto"
|
||||
},
|
||||
"table": {
|
||||
"name": "Nome",
|
||||
"value": "Valor"
|
||||
},
|
||||
"title": "Executar ferramenta: {{name}}",
|
||||
"tooltip": "Executar ferramenta com parâmetros personalizados",
|
||||
"view": {
|
||||
"formatted": "Formatado",
|
||||
"json": "JSON"
|
||||
}
|
||||
},
|
||||
"inputSchema": {
|
||||
"enum": {
|
||||
"allowedValues": "Valores permitidos"
|
||||
|
||||
@ -4114,6 +4114,35 @@
|
||||
},
|
||||
"availableTools": "Доступные инструменты",
|
||||
"enable": "Включить инструмент",
|
||||
"execute": {
|
||||
"button": "Выполнить",
|
||||
"copied": "Скопировано в буфер обмена",
|
||||
"copy": "Копировать",
|
||||
"copyFailed": "Не удалось скопировать",
|
||||
"error": {
|
||||
"invalidJson": "Неверный формат JSON: {{error}}",
|
||||
"noToolOrServer": "Инструмент или сервер не найден"
|
||||
},
|
||||
"execute": "Выполнить",
|
||||
"label": "Выполнить",
|
||||
"params": "Параметры (JSON)",
|
||||
"paramsPlaceholder": "Введите параметры JSON...",
|
||||
"result": {
|
||||
"error": "Результат ошибки",
|
||||
"success": "Результат",
|
||||
"text": "Текст"
|
||||
},
|
||||
"table": {
|
||||
"name": "Имя",
|
||||
"value": "Значение"
|
||||
},
|
||||
"title": "Выполнить инструмент: {{name}}",
|
||||
"tooltip": "Выполнить инструмент с пользовательскими параметрами",
|
||||
"view": {
|
||||
"formatted": "Форматированный",
|
||||
"json": "JSON"
|
||||
}
|
||||
},
|
||||
"inputSchema": {
|
||||
"enum": {
|
||||
"allowedValues": "Допустимые значения"
|
||||
|
||||
442
src/renderer/src/pages/settings/MCPSettings/ExecuteToolModal.tsx
Normal file
442
src/renderer/src/pages/settings/MCPSettings/ExecuteToolModal.tsx
Normal file
@ -0,0 +1,442 @@
|
||||
import 'katex/dist/katex.min.css'
|
||||
|
||||
import { loggerService } from '@logger'
|
||||
import type { MCPServer, MCPTool } from '@renderer/types'
|
||||
import { Button, Flex, Input, message, Modal, Space, Table, Typography } from 'antd'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import { Code as CodeIcon, Copy, Play, Sparkles } from 'lucide-react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import rehypeKatex from 'rehype-katex'
|
||||
import rehypeRaw from 'rehype-raw'
|
||||
import remarkCjkFriendly from 'remark-cjk-friendly'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import remarkMath from 'remark-math'
|
||||
|
||||
const logger = loggerService.withContext('ExecuteToolModal')
|
||||
|
||||
// HTML 文档结构检测模式(在模块级别定义,避免重复创建)
|
||||
const HTML_DOCUMENT_PATTERNS = [/<!DOCTYPE\s+html/i, /<html[\s>]/i, /<head[\s>]/i, /<body[\s>]/i]
|
||||
|
||||
// Markdown 检测模式(在模块级别定义,避免重复创建)
|
||||
const MARKDOWN_PATTERNS = [
|
||||
/^#{1,6}\s+.+$/m, // 标题
|
||||
/^\s*[-*+]\s+.+$/m, // 列表
|
||||
/^\s*\d+\.\s+.+$/m, // 有序列表
|
||||
/\[.+\]\(.+\)/, // 链接
|
||||
/!\[.+\]\(.+\)/, // 图片
|
||||
/```[\s\S]*?```/, // 代码块
|
||||
/`[^`]+`/, // 行内代码
|
||||
/\*\*.*?\*\*/, // 粗体
|
||||
/\*.*?\*/, // 斜体
|
||||
/^>\s+.+$/m // 引用
|
||||
]
|
||||
|
||||
/**
|
||||
* 检测文本内容类型
|
||||
* @param text 要检测的文本
|
||||
* @returns 内容类型:'json' | 'markdown' | 'html' | 'text'
|
||||
*/
|
||||
function detectContentType(text: string): 'json' | 'markdown' | 'html' | 'text' {
|
||||
if (!text) return 'text'
|
||||
|
||||
// 检测 HTML 特征(优先检测,因为 HTML 可能包含其他格式)
|
||||
// 检查是否包含完整的 HTML 文档结构或大量 HTML 标签
|
||||
const hasHtmlDocument = HTML_DOCUMENT_PATTERNS.some((pattern) => pattern.test(text))
|
||||
|
||||
// 检查 HTML 标签数量
|
||||
// 注意:每次调用时创建新的正则表达式实例,避免全局标志的状态污染
|
||||
const htmlTags = text.match(/<[a-z][a-z0-9]*[\s>]/gi)
|
||||
const htmlTagCount = htmlTags ? htmlTags.length : 0
|
||||
|
||||
// 如果包含 HTML 文档结构,或者有多个 HTML 标签,认为是 HTML
|
||||
if (hasHtmlDocument || htmlTagCount >= 3) {
|
||||
return 'html'
|
||||
}
|
||||
|
||||
// 尝试解析为 JSON
|
||||
try {
|
||||
const parsed = JSON.parse(text)
|
||||
if (typeof parsed === 'object' && parsed !== null) {
|
||||
return 'json'
|
||||
}
|
||||
} catch {
|
||||
// 不是 JSON
|
||||
}
|
||||
|
||||
// 检测 Markdown 特征
|
||||
const hasMarkdown = MARKDOWN_PATTERNS.some((pattern) => pattern.test(text))
|
||||
if (hasMarkdown) {
|
||||
return 'markdown'
|
||||
}
|
||||
|
||||
return 'text'
|
||||
}
|
||||
|
||||
interface ExecuteToolModalProps {
|
||||
open: boolean
|
||||
tool: MCPTool | null
|
||||
server: MCPServer | null
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
interface TableData {
|
||||
key: string
|
||||
name: string
|
||||
value: any
|
||||
}
|
||||
|
||||
const ExecuteToolModal: React.FC<ExecuteToolModalProps> = ({ open, tool, server, onClose }) => {
|
||||
const { t } = useTranslation()
|
||||
const [paramsJson, setParamsJson] = useState('{}')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [result, setResult] = useState<{ content: any[]; isError?: boolean } | null>(null)
|
||||
const [viewMode, setViewMode] = useState<'json' | 'formatted'>('json')
|
||||
|
||||
// 初始化参数 JSON(基于工具的 inputSchema)
|
||||
const initialParams = useMemo(() => {
|
||||
if (!tool?.inputSchema?.properties) {
|
||||
return '{}'
|
||||
}
|
||||
|
||||
const params: Record<string, any> = {}
|
||||
const properties = tool.inputSchema.properties
|
||||
|
||||
// 为每个属性生成默认值或示例
|
||||
Object.keys(properties).forEach((key) => {
|
||||
const prop = properties[key] as {
|
||||
type?: string
|
||||
default?: any
|
||||
}
|
||||
if (prop.type === 'string') {
|
||||
params[key] = prop.default !== undefined ? prop.default : ''
|
||||
} else if (prop.type === 'number') {
|
||||
params[key] = prop.default !== undefined ? prop.default : 0
|
||||
} else if (prop.type === 'boolean') {
|
||||
params[key] = prop.default !== undefined ? prop.default : false
|
||||
} else if (prop.type === 'array') {
|
||||
params[key] = prop.default !== undefined ? prop.default : []
|
||||
} else if (prop.type === 'object') {
|
||||
params[key] = prop.default !== undefined ? prop.default : {}
|
||||
}
|
||||
})
|
||||
|
||||
return JSON.stringify(params, null, 2)
|
||||
}, [tool])
|
||||
|
||||
// 当工具改变时,重置参数
|
||||
// initialParams 是基于 tool 的 memoized 值,所以当 tool 改变时它会自动更新
|
||||
// 因此不需要将 initialParams 包含在依赖数组中
|
||||
useEffect(() => {
|
||||
if (open && tool) {
|
||||
setParamsJson(initialParams)
|
||||
setResult(null)
|
||||
setViewMode('json')
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open, tool])
|
||||
|
||||
// 获取主要文本内容
|
||||
const mainTextContent = useMemo(() => {
|
||||
if (!result || !result.content) return ''
|
||||
|
||||
// 查找第一个 text 类型的 content
|
||||
const textContent = result.content.find((item) => item.type === 'text')
|
||||
return textContent?.text || ''
|
||||
}, [result])
|
||||
|
||||
// 获取格式化的内容类型
|
||||
const formattedContentType = useMemo(() => {
|
||||
return detectContentType(mainTextContent)
|
||||
}, [mainTextContent])
|
||||
|
||||
// 验证 JSON 格式
|
||||
const validateJson = (jsonStr: string): { valid: boolean; data?: any; error?: string } => {
|
||||
try {
|
||||
const data = JSON.parse(jsonStr)
|
||||
return { valid: true, data }
|
||||
} catch (error) {
|
||||
return {
|
||||
valid: false,
|
||||
error: error instanceof Error ? error.message : 'Invalid JSON'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 执行工具
|
||||
const handleExecute = async () => {
|
||||
if (!tool || !server) {
|
||||
message.error(t('settings.mcp.tools.execute.error.noToolOrServer', 'Tool or server not found'))
|
||||
return
|
||||
}
|
||||
|
||||
// 验证 JSON
|
||||
const validation = validateJson(paramsJson)
|
||||
if (!validation.valid) {
|
||||
message.error(
|
||||
t('settings.mcp.tools.execute.error.invalidJson', 'Invalid JSON format: {{error}}', {
|
||||
error: validation.error
|
||||
})
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
setResult(null)
|
||||
|
||||
try {
|
||||
logger.info(`Executing tool: ${tool.name}`, { params: validation.data })
|
||||
|
||||
const resp = await window.api.mcp.callTool({
|
||||
server,
|
||||
name: tool.name,
|
||||
args: validation.data,
|
||||
callId: `manual-${Date.now()}`
|
||||
})
|
||||
|
||||
logger.info(`Tool executed successfully: ${tool.name}`, resp)
|
||||
setResult(resp)
|
||||
} catch (error) {
|
||||
logger.error(`Error executing tool: ${tool.name}`, error as Error)
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : typeof error === 'string' ? error : JSON.stringify(error)
|
||||
|
||||
setResult({
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: errorMessage
|
||||
}
|
||||
],
|
||||
isError: true
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 复制结果
|
||||
const handleCopy = () => {
|
||||
if (!result) return
|
||||
|
||||
const resultText = JSON.stringify(result, null, 2)
|
||||
navigator.clipboard.writeText(resultText).then(
|
||||
() => {
|
||||
message.success(t('settings.mcp.tools.execute.copied', 'Copied to clipboard'))
|
||||
},
|
||||
() => {
|
||||
message.error(t('settings.mcp.tools.execute.copyFailed', 'Failed to copy'))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 将结果转换为表格数据(仅当内容是 JSON 时)
|
||||
const tableData: TableData[] = useMemo(() => {
|
||||
if (!result || !result.content || formattedContentType !== 'json') return []
|
||||
|
||||
const data: TableData[] = []
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(mainTextContent)
|
||||
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
||||
// 如果是对象,展开为多行
|
||||
Object.entries(parsed).forEach(([key, value]) => {
|
||||
data.push({
|
||||
key: key,
|
||||
name: key,
|
||||
value: typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value)
|
||||
})
|
||||
})
|
||||
} else if (Array.isArray(parsed)) {
|
||||
// 如果是数组,显示索引和值
|
||||
parsed.forEach((item, idx) => {
|
||||
data.push({
|
||||
key: `item-${idx}`,
|
||||
name: String(idx),
|
||||
value: typeof item === 'object' ? JSON.stringify(item, null, 2) : String(item)
|
||||
})
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
// 解析失败,返回空数组
|
||||
}
|
||||
|
||||
return data
|
||||
}, [result, formattedContentType, mainTextContent])
|
||||
|
||||
const tableColumns: ColumnsType<TableData> = [
|
||||
{
|
||||
title: t('settings.mcp.tools.execute.table.name', 'Name'),
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
title: t('settings.mcp.tools.execute.table.value', 'Value'),
|
||||
dataIndex: 'value',
|
||||
key: 'value',
|
||||
render: (value: string) => (
|
||||
<Typography.Text
|
||||
style={{
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'pre-wrap',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '12px',
|
||||
maxWidth: '100%',
|
||||
display: 'block'
|
||||
}}>
|
||||
{value}
|
||||
</Typography.Text>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
if (!tool || !server) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<Flex align="center" gap={8}>
|
||||
<Play size={16} />
|
||||
<Typography.Text strong>
|
||||
{t('settings.mcp.tools.execute.title', 'Execute Tool: {{name}}', { name: tool.name })}
|
||||
</Typography.Text>
|
||||
</Flex>
|
||||
}
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
width={900}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={onClose}>
|
||||
{t('common.cancel', 'Cancel')}
|
||||
</Button>,
|
||||
<Button key="execute" type="primary" loading={loading} onClick={handleExecute} icon={<Play size={14} />}>
|
||||
{t('settings.mcp.tools.execute.execute', 'Execute')}
|
||||
</Button>
|
||||
]}>
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
{/* 参数输入 */}
|
||||
<div>
|
||||
<Typography.Title level={5}>{t('settings.mcp.tools.execute.params', 'Parameters (JSON)')}</Typography.Title>
|
||||
<Input.TextArea
|
||||
value={paramsJson}
|
||||
onChange={(e) => setParamsJson(e.target.value)}
|
||||
rows={8}
|
||||
placeholder={t('settings.mcp.tools.execute.paramsPlaceholder', 'Enter JSON parameters...')}
|
||||
style={{ fontFamily: 'monospace', fontSize: '12px' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 结果显示 */}
|
||||
{result && (
|
||||
<div>
|
||||
<Flex justify="space-between" align="center" style={{ marginBottom: 8 }}>
|
||||
<Typography.Title level={5} style={{ margin: 0, color: result.isError ? '#ff4d4f' : undefined }}>
|
||||
{result.isError
|
||||
? t('settings.mcp.tools.execute.result.error', 'Error Result')
|
||||
: t('settings.mcp.tools.execute.result.success', 'Result')}
|
||||
</Typography.Title>
|
||||
<Space>
|
||||
<Button.Group>
|
||||
<Button
|
||||
type={viewMode === 'json' ? 'primary' : 'default'}
|
||||
icon={<CodeIcon size={14} />}
|
||||
onClick={() => setViewMode('json')}
|
||||
size="small">
|
||||
{t('settings.mcp.tools.execute.view.json', 'JSON')}
|
||||
</Button>
|
||||
<Button
|
||||
type={viewMode === 'formatted' ? 'primary' : 'default'}
|
||||
icon={<Sparkles size={14} />}
|
||||
onClick={() => setViewMode('formatted')}
|
||||
size="small">
|
||||
{t('settings.mcp.tools.execute.view.formatted', 'Formatted')}
|
||||
</Button>
|
||||
</Button.Group>
|
||||
<Button icon={<Copy size={14} />} onClick={handleCopy} size="small">
|
||||
{t('settings.mcp.tools.execute.copy', 'Copy')}
|
||||
</Button>
|
||||
</Space>
|
||||
</Flex>
|
||||
|
||||
{viewMode === 'json' ? (
|
||||
<Input.TextArea
|
||||
value={JSON.stringify(result, null, 2)}
|
||||
readOnly
|
||||
rows={12}
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '12px',
|
||||
backgroundColor: result.isError ? '#fff1f0' : '#f6ffed',
|
||||
borderColor: result.isError ? '#ffccc7' : '#b7eb8f'
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: result.isError ? '#fff1f0' : '#f6ffed',
|
||||
border: `1px solid ${result.isError ? '#ffccc7' : '#b7eb8f'}`,
|
||||
borderRadius: '4px',
|
||||
padding: formattedContentType === 'html' ? '0' : '16px',
|
||||
maxHeight: '500px',
|
||||
overflow: formattedContentType === 'html' ? 'hidden' : 'auto',
|
||||
position: 'relative'
|
||||
}}>
|
||||
{formattedContentType === 'html' ? (
|
||||
<iframe
|
||||
key={mainTextContent} // 强制重新创建 iframe 当内容改变时
|
||||
srcDoc={mainTextContent}
|
||||
title="HTML Preview"
|
||||
sandbox="allow-scripts allow-same-origin allow-forms"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '500px',
|
||||
border: 'none',
|
||||
display: 'block',
|
||||
backgroundColor: 'white'
|
||||
}}
|
||||
/>
|
||||
) : formattedContentType === 'json' && tableData.length > 0 ? (
|
||||
<Table
|
||||
columns={tableColumns}
|
||||
dataSource={tableData}
|
||||
pagination={false}
|
||||
size="small"
|
||||
scroll={{ y: 400 }}
|
||||
style={{
|
||||
backgroundColor: 'transparent'
|
||||
}}
|
||||
/>
|
||||
) : formattedContentType === 'markdown' ? (
|
||||
<div className="markdown" style={{ wordBreak: 'break-word' }}>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm, remarkCjkFriendly, remarkMath]}
|
||||
rehypePlugins={[rehypeRaw, rehypeKatex]}>
|
||||
{mainTextContent}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
) : (
|
||||
<Typography.Paragraph
|
||||
style={{
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'pre-wrap',
|
||||
marginBottom: 0,
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '12px'
|
||||
}}>
|
||||
{mainTextContent || JSON.stringify(result.content, null, 2)}
|
||||
</Typography.Paragraph>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default ExecuteToolModal
|
||||
@ -1,10 +1,13 @@
|
||||
import type { MCPServer, MCPTool } from '@renderer/types'
|
||||
import { isToolAutoApproved } from '@renderer/utils/mcp-tools'
|
||||
import { Badge, Descriptions, Empty, Flex, Switch, Table, Tag, Tooltip, Typography } from 'antd'
|
||||
import { Badge, Button, Descriptions, Empty, Flex, Switch, Table, Tag, Tooltip, Typography } from 'antd'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import { Hammer, Info, Zap } from 'lucide-react'
|
||||
import { Hammer, Info, Play, Zap } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import ExecuteToolModal from './ExecuteToolModal'
|
||||
|
||||
interface MCPToolsSectionProps {
|
||||
tools: MCPTool[]
|
||||
server: MCPServer
|
||||
@ -14,6 +17,8 @@ interface MCPToolsSectionProps {
|
||||
|
||||
const MCPToolsSection = ({ tools, server, onToggleTool, onToggleAutoApprove }: MCPToolsSectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [executeModalOpen, setExecuteModalOpen] = useState(false)
|
||||
const [selectedTool, setSelectedTool] = useState<MCPTool | null>(null)
|
||||
|
||||
// Check if a tool is enabled (not in the disabledTools array)
|
||||
const isToolEnabled = (tool: MCPTool) => {
|
||||
@ -30,6 +35,12 @@ const MCPToolsSection = ({ tools, server, onToggleTool, onToggleAutoApprove }: M
|
||||
onToggleAutoApprove(tool, checked)
|
||||
}
|
||||
|
||||
// Handle execute tool
|
||||
const handleExecuteTool = (tool: MCPTool) => {
|
||||
setSelectedTool(tool)
|
||||
setExecuteModalOpen(true)
|
||||
}
|
||||
|
||||
// Render tool properties from the input schema
|
||||
const renderToolProperties = (tool: MCPTool) => {
|
||||
if (!tool.inputSchema?.properties) return null
|
||||
@ -102,6 +113,7 @@ const MCPToolsSection = ({ tools, server, onToggleTool, onToggleAutoApprove }: M
|
||||
}
|
||||
|
||||
const columns: ColumnsType<MCPTool> = [
|
||||
// 工具列表列定义:可用工具、启用工具、自动批准、执行工具
|
||||
{
|
||||
title: <Typography.Text strong>{t('settings.mcp.tools.availableTools')}</Typography.Text>,
|
||||
dataIndex: 'name',
|
||||
@ -141,7 +153,7 @@ const MCPToolsSection = ({ tools, server, onToggleTool, onToggleAutoApprove }: M
|
||||
</Flex>
|
||||
),
|
||||
key: 'enable',
|
||||
width: 150, // Fixed width might be good for alignment
|
||||
width: 130, // Fixed width might be good for alignment
|
||||
align: 'center',
|
||||
render: (_, tool) => (
|
||||
<Switch checked={isToolEnabled(tool)} onChange={(checked) => handleToggle(tool, checked)} size="small" />
|
||||
@ -155,7 +167,7 @@ const MCPToolsSection = ({ tools, server, onToggleTool, onToggleAutoApprove }: M
|
||||
</Flex>
|
||||
),
|
||||
key: 'autoApprove',
|
||||
width: 150, // Fixed width
|
||||
width: 130, // Fixed width
|
||||
align: 'center',
|
||||
render: (_, tool) => (
|
||||
<Tooltip
|
||||
@ -175,21 +187,57 @@ const MCPToolsSection = ({ tools, server, onToggleTool, onToggleAutoApprove }: M
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<Flex align="center" justify="center" gap={4}>
|
||||
<Play size={14} color="green" />
|
||||
<Typography.Text strong>{t('settings.mcp.tools.execute.label', 'Execute')}</Typography.Text>
|
||||
</Flex>
|
||||
),
|
||||
key: 'execute',
|
||||
width: 130,
|
||||
align: 'center',
|
||||
render: (_, tool) => (
|
||||
<Tooltip title={t('settings.mcp.tools.execute.tooltip', 'Execute tool with custom parameters')}>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={<Play size={12} />}
|
||||
onClick={() => handleExecuteTool(tool)}
|
||||
disabled={!isToolEnabled(tool)}>
|
||||
{t('settings.mcp.tools.execute.button', 'Execute')}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
return tools.length > 0 ? (
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={tools}
|
||||
pagination={false}
|
||||
expandable={{
|
||||
expandedRowRender: (tool) => renderToolProperties(tool)
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Empty description={t('settings.mcp.tools.noToolsAvailable')} image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
return (
|
||||
<>
|
||||
{tools.length > 0 ? (
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={tools}
|
||||
pagination={false}
|
||||
expandable={{
|
||||
expandedRowRender: (tool) => renderToolProperties(tool)
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Empty description={t('settings.mcp.tools.noToolsAvailable')} image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
)}
|
||||
<ExecuteToolModal
|
||||
open={executeModalOpen}
|
||||
tool={selectedTool}
|
||||
server={server}
|
||||
onClose={() => {
|
||||
setExecuteModalOpen(false)
|
||||
setSelectedTool(null)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -74,7 +74,9 @@ export function getDefaultTranslateAssistant(
|
||||
throw new Error('Unknown target language')
|
||||
}
|
||||
|
||||
const reasoningEffort = getModelSupportedReasoningEffortOptions(model)?.[0]
|
||||
const supportedOptions = getModelSupportedReasoningEffortOptions(model)
|
||||
// disable reasoning if it could be disabled, otherwise no configuration
|
||||
const reasoningEffort = supportedOptions?.includes('none') ? 'none' : 'default'
|
||||
const settings = {
|
||||
temperature: 0.7,
|
||||
reasoning_effort: reasoningEffort,
|
||||
|
||||
@ -94,9 +94,10 @@ const ThinkModelTypes = [
|
||||
'gpt52pro',
|
||||
'grok',
|
||||
'grok4_fast',
|
||||
'gemini',
|
||||
'gemini_pro',
|
||||
'gemini3',
|
||||
'gemini2_flash',
|
||||
'gemini2_pro',
|
||||
'gemini3_flash',
|
||||
'gemini3_pro',
|
||||
'qwen',
|
||||
'qwen_thinking',
|
||||
'doubao',
|
||||
|
||||
@ -1,8 +1,15 @@
|
||||
import '@testing-library/jest-dom/vitest'
|
||||
|
||||
import { createRequire } from 'node:module'
|
||||
import { styleSheetSerializer } from 'jest-styled-components/serializer'
|
||||
import { expect, vi } from 'vitest'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const bufferModule = require('buffer')
|
||||
if (!bufferModule.SlowBuffer) {
|
||||
bufferModule.SlowBuffer = bufferModule.Buffer
|
||||
}
|
||||
|
||||
expect.addSnapshotSerializer(styleSheetSerializer)
|
||||
|
||||
// Mock LoggerService globally for renderer tests
|
||||
@ -48,3 +55,29 @@ vi.stubGlobal('api', {
|
||||
writeWithId: vi.fn().mockResolvedValue(undefined)
|
||||
}
|
||||
})
|
||||
|
||||
if (typeof globalThis.localStorage === 'undefined' || typeof (globalThis.localStorage as any).getItem !== 'function') {
|
||||
let store = new Map<string, string>()
|
||||
|
||||
const localStorageMock = {
|
||||
getItem: (key: string) => store.get(key) ?? null,
|
||||
setItem: (key: string, value: string) => {
|
||||
store.set(key, String(value))
|
||||
},
|
||||
removeItem: (key: string) => {
|
||||
store.delete(key)
|
||||
},
|
||||
clear: () => {
|
||||
store.clear()
|
||||
},
|
||||
key: (index: number) => Array.from(store.keys())[index] ?? null,
|
||||
get length() {
|
||||
return store.size
|
||||
}
|
||||
}
|
||||
|
||||
vi.stubGlobal('localStorage', localStorageMock)
|
||||
if (typeof window !== 'undefined') {
|
||||
Object.defineProperty(window, 'localStorage', { value: localStorageMock })
|
||||
}
|
||||
}
|
||||
|
||||
111
yarn.lock
111
yarn.lock
@ -102,6 +102,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ai-sdk/anthropic@npm:2.0.56":
|
||||
version: 2.0.56
|
||||
resolution: "@ai-sdk/anthropic@npm:2.0.56"
|
||||
dependencies:
|
||||
"@ai-sdk/provider": "npm:2.0.0"
|
||||
"@ai-sdk/provider-utils": "npm:3.0.19"
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
checksum: 10c0/f2b6029c92443f831a2d124420e805d057668003067b1f677a4292d02f27aa3ad533374ea996d77ede7746a42c46fb94a8f2d8c0e7758a4555ea18c8b532052c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ai-sdk/azure@npm:^2.0.87":
|
||||
version: 2.0.87
|
||||
resolution: "@ai-sdk/azure@npm:2.0.87"
|
||||
@ -166,42 +178,42 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ai-sdk/google-vertex@npm:^3.0.79":
|
||||
version: 3.0.79
|
||||
resolution: "@ai-sdk/google-vertex@npm:3.0.79"
|
||||
"@ai-sdk/google-vertex@npm:^3.0.94":
|
||||
version: 3.0.94
|
||||
resolution: "@ai-sdk/google-vertex@npm:3.0.94"
|
||||
dependencies:
|
||||
"@ai-sdk/anthropic": "npm:2.0.49"
|
||||
"@ai-sdk/google": "npm:2.0.43"
|
||||
"@ai-sdk/anthropic": "npm:2.0.56"
|
||||
"@ai-sdk/google": "npm:2.0.49"
|
||||
"@ai-sdk/provider": "npm:2.0.0"
|
||||
"@ai-sdk/provider-utils": "npm:3.0.17"
|
||||
google-auth-library: "npm:^9.15.0"
|
||||
"@ai-sdk/provider-utils": "npm:3.0.19"
|
||||
google-auth-library: "npm:^10.5.0"
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
checksum: 10c0/a86949b8d4a855409acdf7dc8d93ad9ea8ccf2bc3849acbe1ecbe4d6d66f06bcb5242f0df8eea24214e78732618b71ec8a019cbbeab16366f9ad3c860c5d8d30
|
||||
checksum: 10c0/68e2ee9e6525a5e43f90304980e64bf2a4227fd3ce74a7bf17e5ace094ea1bca8f8f18a8cc332a492fee4b912568a768f7479a4eed8148b84e7de1adf4104ad0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ai-sdk/google@npm:2.0.43":
|
||||
version: 2.0.43
|
||||
resolution: "@ai-sdk/google@npm:2.0.43"
|
||||
"@ai-sdk/google@npm:2.0.49":
|
||||
version: 2.0.49
|
||||
resolution: "@ai-sdk/google@npm:2.0.49"
|
||||
dependencies:
|
||||
"@ai-sdk/provider": "npm:2.0.0"
|
||||
"@ai-sdk/provider-utils": "npm:3.0.17"
|
||||
"@ai-sdk/provider-utils": "npm:3.0.19"
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
checksum: 10c0/5a421a9746cf8cbdf3bb7fb49426453a4fe0e354ea55a0123e628afb7acf9bb19959d512c0f8e6d7dbefbfa7e1cef4502fc146149007258a8eeb57743ac5e9e5
|
||||
checksum: 10c0/f3f8acfcd956edc7d807d22963d5eff0f765418f1f2c7d18615955ccdfcebb4d43cc26ce1f712c6a53572f1d8becc0773311b77b1f1bf1af87d675c5f017d5a4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ai-sdk/google@patch:@ai-sdk/google@npm%3A2.0.43#~/.yarn/patches/@ai-sdk-google-npm-2.0.43-689ed559b3.patch":
|
||||
version: 2.0.43
|
||||
resolution: "@ai-sdk/google@patch:@ai-sdk/google@npm%3A2.0.43#~/.yarn/patches/@ai-sdk-google-npm-2.0.43-689ed559b3.patch::version=2.0.43&hash=4dde1e"
|
||||
"@ai-sdk/google@patch:@ai-sdk/google@npm%3A2.0.49#~/.yarn/patches/@ai-sdk-google-npm-2.0.49-84720f41bd.patch":
|
||||
version: 2.0.49
|
||||
resolution: "@ai-sdk/google@patch:@ai-sdk/google@npm%3A2.0.49#~/.yarn/patches/@ai-sdk-google-npm-2.0.49-84720f41bd.patch::version=2.0.49&hash=406c25"
|
||||
dependencies:
|
||||
"@ai-sdk/provider": "npm:2.0.0"
|
||||
"@ai-sdk/provider-utils": "npm:3.0.17"
|
||||
"@ai-sdk/provider-utils": "npm:3.0.19"
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
checksum: 10c0/4cfd17e9c47f2b742d8a0b1ca3532b4dc48753088363b74b01a042f63652174fa9a3fbf655a23f823974c673121dffbd2d192bb0c1bf158da4e2bf498fc76527
|
||||
checksum: 10c0/8d4d881583c2301dce8a4e3066af2ba7d99b30520b6219811f90271c93bf8a07dc23e752fa25ffd0e72c6ec56e97d40d32e04072a362accf7d01a745a2d2a352
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -10051,8 +10063,8 @@ __metadata:
|
||||
"@ai-sdk/anthropic": "npm:^2.0.49"
|
||||
"@ai-sdk/cerebras": "npm:^1.0.31"
|
||||
"@ai-sdk/gateway": "npm:^2.0.15"
|
||||
"@ai-sdk/google": "patch:@ai-sdk/google@npm%3A2.0.43#~/.yarn/patches/@ai-sdk-google-npm-2.0.43-689ed559b3.patch"
|
||||
"@ai-sdk/google-vertex": "npm:^3.0.79"
|
||||
"@ai-sdk/google": "patch:@ai-sdk/google@npm%3A2.0.49#~/.yarn/patches/@ai-sdk-google-npm-2.0.49-84720f41bd.patch"
|
||||
"@ai-sdk/google-vertex": "npm:^3.0.94"
|
||||
"@ai-sdk/huggingface": "npm:^0.0.10"
|
||||
"@ai-sdk/mistral": "npm:^2.0.24"
|
||||
"@ai-sdk/openai": "patch:@ai-sdk/openai@npm%3A2.0.85#~/.yarn/patches/@ai-sdk-openai-npm-2.0.85-27483d1d6a.patch"
|
||||
@ -15499,6 +15511,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"gaxios@npm:^7.0.0":
|
||||
version: 7.1.3
|
||||
resolution: "gaxios@npm:7.1.3"
|
||||
dependencies:
|
||||
extend: "npm:^3.0.2"
|
||||
https-proxy-agent: "npm:^7.0.1"
|
||||
node-fetch: "npm:^3.3.2"
|
||||
rimraf: "npm:^5.0.1"
|
||||
checksum: 10c0/a4a1cdf9a392c0c22e9734a40dca5a77a2903f505b939a50f1e68e312458b1289b7993d2f72d011426e89657cae77a3aa9fc62fb140e8ba90a1faa31fdbde4d2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"gcp-metadata@npm:^6.1.0":
|
||||
version: 6.1.1
|
||||
resolution: "gcp-metadata@npm:6.1.1"
|
||||
@ -15510,6 +15534,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"gcp-metadata@npm:^8.0.0":
|
||||
version: 8.1.2
|
||||
resolution: "gcp-metadata@npm:8.1.2"
|
||||
dependencies:
|
||||
gaxios: "npm:^7.0.0"
|
||||
google-logging-utils: "npm:^1.0.0"
|
||||
json-bigint: "npm:^1.0.0"
|
||||
checksum: 10c0/15a61231a9410dc11c2828d2c9fdc8b0a939f1af746195c44edc6f2ffea0acab52cef3a7b9828069a36fd5d68bda730f7328a415fe42a01258f6e249dfba6908
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"gensync@npm:^1.0.0-beta.2":
|
||||
version: 1.0.0-beta.2
|
||||
resolution: "gensync@npm:1.0.0-beta.2"
|
||||
@ -15733,7 +15768,22 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"google-auth-library@npm:^9.14.2, google-auth-library@npm:^9.15.0, google-auth-library@npm:^9.15.1, google-auth-library@npm:^9.4.2":
|
||||
"google-auth-library@npm:^10.5.0":
|
||||
version: 10.5.0
|
||||
resolution: "google-auth-library@npm:10.5.0"
|
||||
dependencies:
|
||||
base64-js: "npm:^1.3.0"
|
||||
ecdsa-sig-formatter: "npm:^1.0.11"
|
||||
gaxios: "npm:^7.0.0"
|
||||
gcp-metadata: "npm:^8.0.0"
|
||||
google-logging-utils: "npm:^1.0.0"
|
||||
gtoken: "npm:^8.0.0"
|
||||
jws: "npm:^4.0.0"
|
||||
checksum: 10c0/49d3931d20b1f4a4d075216bf5518e2b3396dcf441a8f1952611cf3b6080afb1261c3d32009609047ee4a1cc545269a74b4957e6bba9cce840581df309c4b145
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"google-auth-library@npm:^9.14.2, google-auth-library@npm:^9.15.1, google-auth-library@npm:^9.4.2":
|
||||
version: 9.15.1
|
||||
resolution: "google-auth-library@npm:9.15.1"
|
||||
dependencies:
|
||||
@ -15754,6 +15804,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"google-logging-utils@npm:^1.0.0":
|
||||
version: 1.1.3
|
||||
resolution: "google-logging-utils@npm:1.1.3"
|
||||
checksum: 10c0/e65201c7e96543bd1423b9324013736646b9eed60941e0bfa47b9bfd146d2f09cf3df1c99ca60b7d80a726075263ead049ee72de53372cb8458c3bc55c2c1e59
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"gopd@npm:^1.0.1, gopd@npm:^1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "gopd@npm:1.2.0"
|
||||
@ -15842,6 +15899,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"gtoken@npm:^8.0.0":
|
||||
version: 8.0.0
|
||||
resolution: "gtoken@npm:8.0.0"
|
||||
dependencies:
|
||||
gaxios: "npm:^7.0.0"
|
||||
jws: "npm:^4.0.0"
|
||||
checksum: 10c0/058538e5bbe081d30ada5f1fd34d3a8194357c2e6ecbf7c8a98daeefbf13f7e06c15649c7dace6a1d4cc3bc6dc5483bd484d6d7adc5852021896d7c05c439f37
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hachure-fill@npm:^0.5.2":
|
||||
version: 0.5.2
|
||||
resolution: "hachure-fill@npm:0.5.2"
|
||||
@ -22778,7 +22845,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rimraf@npm:^5.0.10":
|
||||
"rimraf@npm:^5.0.1, rimraf@npm:^5.0.10":
|
||||
version: 5.0.10
|
||||
resolution: "rimraf@npm:5.0.10"
|
||||
dependencies:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user