diff --git a/frontend/src/components/AISettingsModal.tsx b/frontend/src/components/AISettingsModal.tsx index eba62193..0be74c7d 100644 --- a/frontend/src/components/AISettingsModal.tsx +++ b/frontend/src/components/AISettingsModal.tsx @@ -57,7 +57,8 @@ const DEFAULT_MCP_HTTP_SERVER_STATUS: AIMCPHTTPServerStatus = { addr: '127.0.0.1:8765', path: '/mcp', url: 'http://127.0.0.1:8765/mcp', - schemaOnly: true, + // 默认允许 execute_sql 查少量数据 + schemaOnly: false, message: '', }; @@ -65,6 +66,7 @@ const DEFAULT_MCP_HTTP_SERVER_DRAFT: AIMCPHTTPServerDraft = { addr: DEFAULT_MCP_HTTP_SERVER_STATUS.addr, path: DEFAULT_MCP_HTTP_SERVER_STATUS.path, authorizationHeader: '', + schemaOnly: false, }; const buildMCPHTTPServerDraftFromStatus = ( @@ -79,6 +81,10 @@ const buildMCPHTTPServerDraftFromStatus = ( fallback.authorizationHeader || '', ).trim(), + // 运行中用状态;未运行保留草稿选择 + schemaOnly: typeof status.schemaOnly === 'boolean' + ? status.schemaOnly + : (typeof fallback.schemaOnly === 'boolean' ? fallback.schemaOnly : false), }); const normalizeMCPHTTPAuthorizationToken = (value: string): string => { @@ -406,6 +412,7 @@ export const AISettingsContent: React.FC = ({ active, da models: resolvedModels, baseUrl: finalBaseUrl, apiFormat: resolvedTransport.apiFormat, + thinkingIntensity: String(values.thinkingIntensity || '').trim(), }; // 后端 AISaveProvider 统一处理新增和更新,返回 void,失败抛异常 await Service?.AISaveProvider?.(payload); @@ -549,7 +556,7 @@ export const AISettingsContent: React.FC = ({ active, da addr: mcpHTTPServerDraft.addr || DEFAULT_MCP_HTTP_SERVER_STATUS.addr, path: mcpHTTPServerDraft.path || DEFAULT_MCP_HTTP_SERVER_STATUS.path, token: normalizeMCPHTTPAuthorizationToken(mcpHTTPServerDraft.authorizationHeader), - schemaOnly: true, + schemaOnly: mcpHTTPServerDraft.schemaOnly === true, }) : await Service.AIStopMCPHTTPServer(); if (nextStatus) { @@ -678,6 +685,7 @@ export const AISettingsContent: React.FC = ({ active, da models: resolvedModels, maxTokens: Number(values.maxTokens) || 4096, temperature: Number(values.temperature) ?? 0.7, + thinkingIntensity: String(values.thinkingIntensity || '').trim(), apiFormat: resolvedTransport.apiFormat, }); if (res?.success) { setTestStatus('success'); void messageApi.success(t('ai_settings.message.test_success')); } diff --git a/frontend/src/components/ai/AIMCPHTTPServerPanel.test.tsx b/frontend/src/components/ai/AIMCPHTTPServerPanel.test.tsx index 3e4ea877..74d6853f 100644 --- a/frontend/src/components/ai/AIMCPHTTPServerPanel.test.tsx +++ b/frontend/src/components/ai/AIMCPHTTPServerPanel.test.tsx @@ -32,6 +32,12 @@ const REQUIRED_KEYS = [ 'ai_settings.mcp_http.panel.stopped_hint', 'ai_settings.mcp_http.panel.copy_url', 'ai_settings.mcp_http.panel.copy_authorization', + 'ai_settings.mcp_http.panel.mode.schema_only', + 'ai_settings.mcp_http.panel.mode.limited_query', + 'ai_settings.mcp_http.panel.limited_query.label', + 'ai_settings.mcp_http.panel.limited_query.hint', + 'ai_settings.mcp_http.panel.limited_query.on', + 'ai_settings.mcp_http.panel.limited_query.off', ]; const buildPanelProps = () => ({ @@ -40,7 +46,7 @@ const buildPanelProps = () => ({ addr: '127.0.0.1:8765', path: '/mcp', url: 'http://127.0.0.1:8765/mcp', - schemaOnly: true, + schemaOnly: false, authorizationHeader: 'Bearer gnv_test', message: '', }, @@ -48,6 +54,7 @@ const buildPanelProps = () => ({ addr: '127.0.0.1:8765', path: '/mcp', authorizationHeader: 'Bearer gnv_test', + schemaOnly: false, }, loading: false, cardBg: '#fff', @@ -98,7 +105,7 @@ describe('AIMCPHTTPServerPanel', () => { expect(markup).toContain('GoNavi MCP HTTP service'); expect(markup).toContain('Running'); - expect(markup).toContain('schema-only'); + expect(markup).toContain('Limited query'); expect(markup).toContain('Listen address / port'); expect(markup).toContain('Authorization'); expect(markup).toContain('127.0.0.1:8765'); diff --git a/frontend/src/components/ai/AIMCPHTTPServerPanel.tsx b/frontend/src/components/ai/AIMCPHTTPServerPanel.tsx index 44609f37..6a8b2e6e 100644 --- a/frontend/src/components/ai/AIMCPHTTPServerPanel.tsx +++ b/frontend/src/components/ai/AIMCPHTTPServerPanel.tsx @@ -11,6 +11,8 @@ export interface AIMCPHTTPServerDraft { addr: string; path: string; authorizationHeader: string; + /** false 时注册 execute_sql,允许查少量样例数据 */ + schemaOnly: boolean; } export interface AIMCPHTTPServerPanelProps { @@ -71,8 +73,10 @@ const AIMCPHTTPServerPanel: React.FC = ({ {copy(running ? 'ai_settings.mcp_http.panel.status.running' : 'ai_settings.mcp_http.panel.status.stopped')} - - schema-only + + {draft.schemaOnly || (running && status.schemaOnly) + ? copy('ai_settings.mcp_http.panel.mode.schema_only') + : copy('ai_settings.mcp_http.panel.mode.limited_query')}
@@ -126,6 +130,31 @@ const AIMCPHTTPServerPanel: React.FC = ({ />
+
+
+
+ {copy('ai_settings.mcp_http.panel.limited_query.label')} +
+
+ {copy('ai_settings.mcp_http.panel.limited_query.hint')} +
+
+ onDraftChange({ schemaOnly: !checked })} + checkedChildren={copy('ai_settings.mcp_http.panel.limited_query.on')} + unCheckedChildren={copy('ai_settings.mcp_http.panel.limited_query.off')} + /> +
{running ? status.message || copy('ai_settings.mcp_http.panel.running_hint') diff --git a/frontend/src/components/ai/AISettingsMCPSection.test.tsx b/frontend/src/components/ai/AISettingsMCPSection.test.tsx index 0230fd66..40f5db10 100644 --- a/frontend/src/components/ai/AISettingsMCPSection.test.tsx +++ b/frontend/src/components/ai/AISettingsMCPSection.test.tsx @@ -45,13 +45,14 @@ const buildMCPSectionProps = (patch: Partial = {}): A addr: '127.0.0.1:8765', path: '/mcp', url: 'http://127.0.0.1:8765/mcp', - schemaOnly: true, + schemaOnly: false, message: 'GoNavi MCP HTTP 服务未启动', }, mcpHTTPServerDraft: { addr: '127.0.0.1:8765', path: '/mcp', authorizationHeader: 'Bearer gnv_test', + schemaOnly: false, }, mcpServers: [], mcpTools: [], diff --git a/frontend/src/components/ai/AISettingsProvidersSection.tsx b/frontend/src/components/ai/AISettingsProvidersSection.tsx index 4a70f517..c8ed7fd2 100644 --- a/frontend/src/components/ai/AISettingsProvidersSection.tsx +++ b/frontend/src/components/ai/AISettingsProvidersSection.tsx @@ -14,6 +14,12 @@ import { PROVIDER_PRESET_GRID_STYLE, PROVIDER_PRESET_CARD_TITLE_STYLE, } from '../../utils/aiSettingsPresetLayout'; +import { + coerceThinkingIntensityForProfile, + resolveThinkingIntensityHintKey, + resolveThinkingIntensityOptions, + resolveThinkingIntensityProfile, +} from '../../utils/aiThinkingIntensity'; export interface AISettingsProviderPresetOption { key: string; @@ -120,8 +126,44 @@ const AISettingsProvidersSection: React.FC = ({ const currentFieldLabelStyle = fieldLabelStyle(sectionLabelColor); const watchedModel = Form.useWatch('model', form); const watchedModels = Form.useWatch('models', form); + const watchedBaseUrl = Form.useWatch('baseUrl', form); + const watchedType = Form.useWatch('type', form); + const watchedThinkingIntensity = Form.useWatch('thinkingIntensity', form); const watchedInlineCompletionModel = Form.useWatch('inlineCompletionModel', form); const presetFromForm = providerPresets.find((preset) => preset.key === presetKeyFromForm); + const thinkingProfile = React.useMemo(() => resolveThinkingIntensityProfile({ + type: watchedType || editingProvider?.type, + apiFormat: watchedApiFormat || editingProvider?.apiFormat, + baseUrl: watchedBaseUrl || editingProvider?.baseUrl || presetFromForm?.defaultBaseUrl, + model: watchedModel || editingProvider?.model || presetFromForm?.defaultModel, + }), [ + editingProvider?.apiFormat, + editingProvider?.baseUrl, + editingProvider?.model, + editingProvider?.type, + presetFromForm?.defaultBaseUrl, + presetFromForm?.defaultModel, + watchedApiFormat, + watchedBaseUrl, + watchedModel, + watchedType, + ]); + const thinkingIntensityOptions = React.useMemo( + () => resolveThinkingIntensityOptions(thinkingProfile).map((item) => ({ + value: item.value, + label: copy(item.labelKey), + })), + // copy 依赖 i18n 语言,label 随语言刷新即可 + // eslint-disable-next-line react-hooks/exhaustive-deps + [copy, thinkingProfile, i18n?.language], + ); + React.useEffect(() => { + if (!isEditing) return; + const next = coerceThinkingIntensityForProfile(watchedThinkingIntensity, thinkingProfile); + if (next && next !== String(watchedThinkingIntensity || '').trim()) { + form.setFieldsValue({ thinkingIntensity: next }); + } + }, [form, isEditing, thinkingProfile, watchedThinkingIntensity]); const inlineCompletionModelOptions = React.useMemo(() => { const values = [ watchedModel, @@ -370,6 +412,24 @@ const AISettingsProvidersSection: React.FC = ({ +
+
+ {copy('ai_settings.form.section.thinking')} +
+ {copy('ai_settings.form.thinking_intensity')}} + name="thinkingIntensity" + extra={copy(resolveThinkingIntensityHintKey(thinkingProfile))} + style={{ marginBottom: 0 }} + > +