Compare commits

...

11 Commits

Author SHA1 Message Date
Bubu
dac3067910
Merge 6d06aef563 into 8ab375161d 2025-12-18 20:30:23 +08:00
George·Dong
8ab375161d
fix: disable reasoning mode for translation to improve efficiency (#11998)
* fix: disable reasoning mode for translation to improve efficiency

- 修改 getDefaultTranslateAssistant 函数,将默认推理选项设置为 'none'
- 避免 PR #11942 引入的 'default' 选项导致翻译重新启用思考模式
- 显著提升翻译速度和性能
- 符合翻译场景不需要复杂推理的业务逻辑

* fix(AssistantService): adjust reasoning effort

Set reasoning effort to 'none' only if supported by model, otherwise use 'default'.

---------

Co-authored-by: icarus <eurfelux@gmail.com>
2025-12-18 20:16:09 +08:00
GeekMr
42260710d8
fix(azure): restore deployment-based URLs for non-v1 apiVersion and add tests (#11966)
* fix: support Azure OpenAI deployment URLs

* test: stabilize renderer setup

---------

Co-authored-by: William Wang <WilliamOnline1721@hotmail.com>
2025-12-18 18:12:26 +08:00
kangfenmao
5e8646c6a5 fix: update API path for image generation requests in OpenAIBaseClient 2025-12-18 14:45:30 +08:00
Phantom
7e93e8b9b2
feat(gemini): add support for Gemini 3 Flash and Pro model detection (#11984)
* feat(gemini): update model types and add support for gemini3 variants

add new model type identifiers for gemini3 flash and pro variants
implement utility functions to detect gemini3 flash and pro models
update reasoning configuration and tests for new gemini variants

* docs(i18n): update chinese translation for minimal_description

* chore: update @ai-sdk/google and @ai-sdk/google-vertex dependencies

- Update @ai-sdk/google to version 2.0.49 with patch for model path fix
- Update @ai-sdk/google-vertex to version 3.0.94 with updated dependencies

* feat(gemini): add thinking level mapping for Gemini 3 models

Implement mapping between reasoning effort options and Gemini's thinking levels. Enable thinking config for Gemini 3 models to support advanced reasoning features.

* chore: update yarn.lock with patched @ai-sdk/google dependency

* test(reasoning): update tests for Gemini model type classification and reasoning options

Update test cases to reflect new Gemini model type classifications (gemini2_flash, gemini3_flash, gemini2_pro, gemini3_pro) and their corresponding reasoning effort options. Add tests for Gemini 3 models and adjust existing ones to match current behavior.

* docs(reasoning): remove outdated TODO comment about model support
2025-12-18 14:35:36 +08:00
Bubu
6d06aef563
Merge branch 'CherryHQ:main' into main 2025-11-24 13:36:02 +08:00
xu-ya
cd4c5589db feat: repair bot comment 2025-11-18 16:41:21 +08:00
xu-ya
69cb5eaed4 feat: repair bot comment 2025-11-18 16:24:24 +08:00
xu-ya
304ab82e86 feat: add mcp Execute Tool 2025-11-18 15:07:37 +08:00
xu-ya
8d227a5164 feat: add mcp Execute Tool 2025-11-18 14:45:10 +08:00
xu-ya
f30f06f40f feat: add mcp Execute Tool 2025-11-18 14:30:44 +08:00
27 changed files with 1302 additions and 117 deletions

View File

@ -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

View 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'
}
},
{

View File

@ -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": {

View File

@ -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,

View File

@ -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()
})
})

View File

@ -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') {

View File

@ -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]

View File

@ -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)
})
})
})

View File

@ -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)

View File

@ -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
}

View File

@ -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)
}

View File

@ -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"

View File

@ -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": "允许的值"

View File

@ -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": "允許的值"

View File

@ -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"

View File

@ -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": "Επιτρεπόμενες τιμές"

View File

@ -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"

View File

@ -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"

View File

@ -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": "許可された値"

View File

@ -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"

View File

@ -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": "Допустимые значения"

View 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

View File

@ -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)
}}
/>
</>
)
}

View File

@ -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,

View File

@ -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',

View File

@ -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
View File

@ -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: