mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-14 18:39:54 +08:00
♻️ refactor(ai-settings): 拆分 AI 设置预设与服务桥接配置
This commit is contained in:
@@ -1,12 +1,8 @@
|
||||
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||
import { Modal, Form, message as antdMessage } from 'antd';
|
||||
import { ApiOutlined, RobotOutlined, ThunderboltOutlined, CloudOutlined, ExperimentOutlined, AppstoreOutlined } from '@ant-design/icons';
|
||||
import { RobotOutlined } from '@ant-design/icons';
|
||||
import type { AIProviderConfig, AIProviderType, AISafetyLevel, AIContextLevel, AIUserPromptSettings, AIMCPServerConfig, AIMCPToolDescriptor, AIMCPClientInstallStatus, AISkillConfig } from '../types';
|
||||
import {
|
||||
QWEN_BAILIAN_ANTHROPIC_BASE_URL,
|
||||
QWEN_CODING_PLAN_ANTHROPIC_BASE_URL,
|
||||
QWEN_CODING_PLAN_MODELS,
|
||||
resolveProviderPresetKey,
|
||||
resolvePresetBaseURL,
|
||||
resolvePresetModelSelection,
|
||||
resolvePresetTransport,
|
||||
@@ -25,6 +21,16 @@ import AISettingsProvidersSection from './ai/AISettingsProvidersSection';
|
||||
import AISettingsPromptsSection from './ai/AISettingsPromptsSection';
|
||||
import AISettingsSkillsSection from './ai/AISettingsSkillsSection';
|
||||
import { useAIMCPClientInstaller } from './ai/useAIMCPClientInstaller';
|
||||
import {
|
||||
EMPTY_AI_USER_PROMPT_SETTINGS,
|
||||
EMPTY_MCP_SERVER,
|
||||
EMPTY_SKILL,
|
||||
PROVIDER_PRESETS,
|
||||
findPreset,
|
||||
matchProviderPreset,
|
||||
type ProviderPreset,
|
||||
waitForAIService,
|
||||
} from './ai/aiSettingsModalConfig';
|
||||
interface AISettingsModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
@@ -33,101 +39,6 @@ interface AISettingsModalProps {
|
||||
focusProviderId?: string;
|
||||
}
|
||||
|
||||
// 预设配置:每个预设映射到后端 type(openai/anthropic/gemini/custom)并附带默认 URL 和 Model
|
||||
interface ProviderPreset {
|
||||
key: string;
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
desc: string;
|
||||
color: string;
|
||||
backendType: AIProviderType;
|
||||
fixedApiFormat?: string;
|
||||
defaultBaseUrl: string;
|
||||
defaultModel: string;
|
||||
models: string[];
|
||||
}
|
||||
|
||||
const PROVIDER_PRESETS: ProviderPreset[] = [
|
||||
{ key: 'openai', label: 'OpenAI', icon: <ApiOutlined />, desc: 'GPT-5.4 / 5.3 系列', color: '#10b981', backendType: 'openai', defaultBaseUrl: 'https://api.openai.com/v1', defaultModel: 'gpt-4o', models: [] },
|
||||
{ key: 'deepseek', label: 'DeepSeek', icon: <ThunderboltOutlined />, desc: 'DeepSeek-V4 / R1', color: '#3b82f6', backendType: 'openai', defaultBaseUrl: 'https://api.deepseek.com/v1', defaultModel: 'deepseek-chat', models: [] },
|
||||
{ key: 'qwen-bailian', label: '通义千问(百炼通用)', icon: <CloudOutlined />, desc: '百炼 Anthropic 兼容 / 模型从远端拉取', color: '#6366f1', backendType: 'anthropic', defaultBaseUrl: QWEN_BAILIAN_ANTHROPIC_BASE_URL, defaultModel: '', models: [] },
|
||||
{ key: 'qwen-coding-plan', label: '通义千问(Coding Plan)', icon: <CloudOutlined />, desc: 'Claude Code CLI 代理链路 / 使用官方支持模型清单', color: '#4f46e5', backendType: 'custom', fixedApiFormat: 'claude-cli', defaultBaseUrl: QWEN_CODING_PLAN_ANTHROPIC_BASE_URL, defaultModel: '', models: QWEN_CODING_PLAN_MODELS },
|
||||
{ key: 'zhipu', label: '智谱 GLM', icon: <ExperimentOutlined />, desc: 'GLM-5 / GLM-5-Turbo', color: '#0ea5e9', backendType: 'openai', defaultBaseUrl: 'https://open.bigmodel.cn/api/paas/v4', defaultModel: 'glm-4', models: [] },
|
||||
{ key: 'moonshot', label: 'Kimi', icon: <ExperimentOutlined />, desc: 'Kimi K2.5 (Anthropic 兼容)', color: '#0d9488', backendType: 'anthropic', defaultBaseUrl: 'https://api.moonshot.cn/anthropic', defaultModel: 'moonshot-v1-8k', models: [] },
|
||||
{ key: 'anthropic', label: 'Claude', icon: <ExperimentOutlined />, desc: 'Claude Opus/Sonnet', color: '#d97706', backendType: 'anthropic', defaultBaseUrl: 'https://api.anthropic.com', defaultModel: 'claude-3-5-sonnet-20241022', models: [] },
|
||||
{ key: 'gemini', label: 'Gemini', icon: <CloudOutlined />, desc: 'Gemini 3.1 / 2.5 系列', color: '#059669', backendType: 'gemini', defaultBaseUrl: 'https://generativelanguage.googleapis.com', defaultModel: 'gemini-2.5-flash', models: [] },
|
||||
{ key: 'volcengine-ark', label: '火山方舟', icon: <CloudOutlined />, desc: 'Ark 通用推理 / 豆包模型', color: '#0ea5e9', backendType: 'openai', defaultBaseUrl: 'https://ark.cn-beijing.volces.com/api/v3', defaultModel: '', models: [] },
|
||||
{ key: 'volcengine-coding', label: '火山 Coding Plan', icon: <CloudOutlined />, desc: 'Ark Code / Coding Plan', color: '#0284c7', backendType: 'openai', defaultBaseUrl: 'https://ark.cn-beijing.volces.com/api/coding/v3', defaultModel: '', models: [] },
|
||||
{ key: 'minimax', label: 'MiniMax', icon: <ExperimentOutlined />, desc: 'M3 / M2.7 系列 (Anthropic 兼容)', color: '#e11d48', backendType: 'anthropic', defaultBaseUrl: 'https://api.minimaxi.com/anthropic', defaultModel: 'MiniMax-M3', models: ['MiniMax-M3', 'MiniMax-M2.7', 'MiniMax-M2.7-highspeed'] },
|
||||
{ key: 'ollama', label: 'Ollama', icon: <AppstoreOutlined />, desc: '本地部署开源模型', color: '#78716c', backendType: 'openai', defaultBaseUrl: 'http://localhost:11434/v1', defaultModel: 'llama3', models: [] },
|
||||
{ key: 'custom', label: '自定义', icon: <AppstoreOutlined />, desc: '自定义 API 端点', color: '#64748b', backendType: 'custom', defaultBaseUrl: '', defaultModel: '', models: [] },
|
||||
];
|
||||
|
||||
const findPreset = (key: string): ProviderPreset => PROVIDER_PRESETS.find(p => p.key === key) || PROVIDER_PRESETS[PROVIDER_PRESETS.length - 1];
|
||||
|
||||
const matchProviderPreset = (provider: Pick<AIProviderConfig, 'type' | 'baseUrl' | 'apiFormat'>): ProviderPreset => {
|
||||
const presetKey = resolveProviderPresetKey(provider, PROVIDER_PRESETS, 'custom');
|
||||
return findPreset(presetKey);
|
||||
};
|
||||
|
||||
const EMPTY_AI_USER_PROMPT_SETTINGS: AIUserPromptSettings = {
|
||||
global: '',
|
||||
database: '',
|
||||
jvm: '',
|
||||
jvmDiagnostic: '',
|
||||
};
|
||||
|
||||
const EMPTY_MCP_SERVER = (seed?: Partial<AIMCPServerConfig>): AIMCPServerConfig => {
|
||||
const base: AIMCPServerConfig = {
|
||||
id: `mcp-draft-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
name: '',
|
||||
transport: 'stdio',
|
||||
command: '',
|
||||
args: [],
|
||||
env: {},
|
||||
enabled: true,
|
||||
timeoutSeconds: 20,
|
||||
};
|
||||
return {
|
||||
...base,
|
||||
...seed,
|
||||
transport: seed?.transport || base.transport,
|
||||
args: Array.isArray(seed?.args) ? seed.args : base.args,
|
||||
env: seed?.env || base.env,
|
||||
enabled: seed?.enabled ?? base.enabled,
|
||||
timeoutSeconds: seed?.timeoutSeconds || base.timeoutSeconds,
|
||||
};
|
||||
};
|
||||
|
||||
const waitFor = (delayMs: number) => new Promise<void>((resolve) => {
|
||||
window.setTimeout(resolve, delayMs);
|
||||
});
|
||||
|
||||
const readAIService = () => (window as any).go?.aiservice?.Service;
|
||||
|
||||
const waitForAIService = async (attempts = 6, delayMs = 80) => {
|
||||
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
||||
const service = readAIService();
|
||||
if (service) {
|
||||
return service;
|
||||
}
|
||||
if (attempt < attempts - 1) {
|
||||
await waitFor(delayMs);
|
||||
}
|
||||
}
|
||||
return readAIService();
|
||||
};
|
||||
|
||||
const EMPTY_SKILL = (): AISkillConfig => ({
|
||||
id: `skill-draft-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
name: '',
|
||||
description: '',
|
||||
systemPrompt: '',
|
||||
enabled: true,
|
||||
scopes: ['global'],
|
||||
requiredTools: [],
|
||||
});
|
||||
|
||||
const AISettingsModal: React.FC<AISettingsModalProps> = ({ open, onClose, darkMode, overlayTheme, focusProviderId }) => {
|
||||
const [providers, setProviders] = useState<AIProviderConfig[]>([]);
|
||||
const [activeProviderId, setActiveProviderId] = useState<string>('');
|
||||
|
||||
44
frontend/src/components/ai/aiSettingsModalConfig.test.tsx
Normal file
44
frontend/src/components/ai/aiSettingsModalConfig.test.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { QWEN_CODING_PLAN_ANTHROPIC_BASE_URL } from '../../utils/aiProviderPresets';
|
||||
import {
|
||||
EMPTY_MCP_SERVER,
|
||||
EMPTY_SKILL,
|
||||
PROVIDER_PRESETS,
|
||||
findPreset,
|
||||
matchProviderPreset,
|
||||
} from './aiSettingsModalConfig';
|
||||
|
||||
describe('aiSettingsModalConfig', () => {
|
||||
it('finds the matching preset and falls back to custom when the key is unknown', () => {
|
||||
expect(findPreset('openai').label).toBe('OpenAI');
|
||||
expect(findPreset('missing-preset').key).toBe('custom');
|
||||
});
|
||||
|
||||
it('matches an anthropic-compatible provider back to the qwen coding plan preset', () => {
|
||||
const preset = matchProviderPreset({
|
||||
type: 'custom',
|
||||
baseUrl: QWEN_CODING_PLAN_ANTHROPIC_BASE_URL,
|
||||
apiFormat: 'claude-cli',
|
||||
});
|
||||
|
||||
expect(preset.key).toBe('qwen-coding-plan');
|
||||
});
|
||||
|
||||
it('creates MCP server drafts and skill drafts with stable defaults', () => {
|
||||
const server = EMPTY_MCP_SERVER({ name: 'Browser', args: ['stdio'] });
|
||||
const skill = EMPTY_SKILL();
|
||||
|
||||
expect(server.transport).toBe('stdio');
|
||||
expect(server.timeoutSeconds).toBe(20);
|
||||
expect(server.args).toEqual(['stdio']);
|
||||
expect(skill.enabled).toBe(true);
|
||||
expect(skill.scopes).toEqual(['global']);
|
||||
});
|
||||
|
||||
it('keeps the provider preset list available for the settings modal', () => {
|
||||
expect(PROVIDER_PRESETS.some((item) => item.key === 'codex')).toBe(false);
|
||||
expect(PROVIDER_PRESETS.some((item) => item.key === 'openai')).toBe(true);
|
||||
expect(PROVIDER_PRESETS.some((item) => item.key === 'custom')).toBe(true);
|
||||
});
|
||||
});
|
||||
119
frontend/src/components/ai/aiSettingsModalConfig.tsx
Normal file
119
frontend/src/components/ai/aiSettingsModalConfig.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
ApiOutlined,
|
||||
AppstoreOutlined,
|
||||
CloudOutlined,
|
||||
ExperimentOutlined,
|
||||
ThunderboltOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
import type {
|
||||
AIMCPServerConfig,
|
||||
AIProviderConfig,
|
||||
AIProviderType,
|
||||
AISkillConfig,
|
||||
AIUserPromptSettings,
|
||||
} from '../../types';
|
||||
import {
|
||||
QWEN_BAILIAN_ANTHROPIC_BASE_URL,
|
||||
QWEN_CODING_PLAN_ANTHROPIC_BASE_URL,
|
||||
QWEN_CODING_PLAN_MODELS,
|
||||
resolveProviderPresetKey,
|
||||
} from '../../utils/aiProviderPresets';
|
||||
|
||||
export interface ProviderPreset {
|
||||
key: string;
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
desc: string;
|
||||
color: string;
|
||||
backendType: AIProviderType;
|
||||
fixedApiFormat?: string;
|
||||
defaultBaseUrl: string;
|
||||
defaultModel: string;
|
||||
models: string[];
|
||||
}
|
||||
|
||||
export const PROVIDER_PRESETS: ProviderPreset[] = [
|
||||
{ key: 'openai', label: 'OpenAI', icon: <ApiOutlined />, desc: 'GPT-5.4 / 5.3 系列', color: '#10b981', backendType: 'openai', defaultBaseUrl: 'https://api.openai.com/v1', defaultModel: 'gpt-4o', models: [] },
|
||||
{ key: 'deepseek', label: 'DeepSeek', icon: <ThunderboltOutlined />, desc: 'DeepSeek-V4 / R1', color: '#3b82f6', backendType: 'openai', defaultBaseUrl: 'https://api.deepseek.com/v1', defaultModel: 'deepseek-chat', models: [] },
|
||||
{ key: 'qwen-bailian', label: '通义千问(百炼通用)', icon: <CloudOutlined />, desc: '百炼 Anthropic 兼容 / 模型从远端拉取', color: '#6366f1', backendType: 'anthropic', defaultBaseUrl: QWEN_BAILIAN_ANTHROPIC_BASE_URL, defaultModel: '', models: [] },
|
||||
{ key: 'qwen-coding-plan', label: '通义千问(Coding Plan)', icon: <CloudOutlined />, desc: 'Claude Code CLI 代理链路 / 使用官方支持模型清单', color: '#4f46e5', backendType: 'custom', fixedApiFormat: 'claude-cli', defaultBaseUrl: QWEN_CODING_PLAN_ANTHROPIC_BASE_URL, defaultModel: '', models: QWEN_CODING_PLAN_MODELS },
|
||||
{ key: 'zhipu', label: '智谱 GLM', icon: <ExperimentOutlined />, desc: 'GLM-5 / GLM-5-Turbo', color: '#0ea5e9', backendType: 'openai', defaultBaseUrl: 'https://open.bigmodel.cn/api/paas/v4', defaultModel: 'glm-4', models: [] },
|
||||
{ key: 'moonshot', label: 'Kimi', icon: <ExperimentOutlined />, desc: 'Kimi K2.5 (Anthropic 兼容)', color: '#0d9488', backendType: 'anthropic', defaultBaseUrl: 'https://api.moonshot.cn/anthropic', defaultModel: 'moonshot-v1-8k', models: [] },
|
||||
{ key: 'anthropic', label: 'Claude', icon: <ExperimentOutlined />, desc: 'Claude Opus/Sonnet', color: '#d97706', backendType: 'anthropic', defaultBaseUrl: 'https://api.anthropic.com', defaultModel: 'claude-3-5-sonnet-20241022', models: [] },
|
||||
{ key: 'gemini', label: 'Gemini', icon: <CloudOutlined />, desc: 'Gemini 3.1 / 2.5 系列', color: '#059669', backendType: 'gemini', defaultBaseUrl: 'https://generativelanguage.googleapis.com', defaultModel: 'gemini-2.5-flash', models: [] },
|
||||
{ key: 'volcengine-ark', label: '火山方舟', icon: <CloudOutlined />, desc: 'Ark 通用推理 / 豆包模型', color: '#0ea5e9', backendType: 'openai', defaultBaseUrl: 'https://ark.cn-beijing.volces.com/api/v3', defaultModel: '', models: [] },
|
||||
{ key: 'volcengine-coding', label: '火山 Coding Plan', icon: <CloudOutlined />, desc: 'Ark Code / Coding Plan', color: '#0284c7', backendType: 'openai', defaultBaseUrl: 'https://ark.cn-beijing.volces.com/api/coding/v3', defaultModel: '', models: [] },
|
||||
{ key: 'minimax', label: 'MiniMax', icon: <ExperimentOutlined />, desc: 'M3 / M2.7 系列 (Anthropic 兼容)', color: '#e11d48', backendType: 'anthropic', defaultBaseUrl: 'https://api.minimaxi.com/anthropic', defaultModel: 'MiniMax-M3', models: ['MiniMax-M3', 'MiniMax-M2.7', 'MiniMax-M2.7-highspeed'] },
|
||||
{ key: 'ollama', label: 'Ollama', icon: <AppstoreOutlined />, desc: '本地部署开源模型', color: '#78716c', backendType: 'openai', defaultBaseUrl: 'http://localhost:11434/v1', defaultModel: 'llama3', models: [] },
|
||||
{ key: 'custom', label: '自定义', icon: <AppstoreOutlined />, desc: '自定义 API 端点', color: '#64748b', backendType: 'custom', defaultBaseUrl: '', defaultModel: '', models: [] },
|
||||
];
|
||||
|
||||
export const findPreset = (key: string): ProviderPreset =>
|
||||
PROVIDER_PRESETS.find((preset) => preset.key === key) || PROVIDER_PRESETS[PROVIDER_PRESETS.length - 1];
|
||||
|
||||
export const matchProviderPreset = (
|
||||
provider: Pick<AIProviderConfig, 'type' | 'baseUrl' | 'apiFormat'>,
|
||||
): ProviderPreset => {
|
||||
const presetKey = resolveProviderPresetKey(provider, PROVIDER_PRESETS, 'custom');
|
||||
return findPreset(presetKey);
|
||||
};
|
||||
|
||||
export const EMPTY_AI_USER_PROMPT_SETTINGS: AIUserPromptSettings = {
|
||||
global: '',
|
||||
database: '',
|
||||
jvm: '',
|
||||
jvmDiagnostic: '',
|
||||
};
|
||||
|
||||
export const EMPTY_MCP_SERVER = (seed?: Partial<AIMCPServerConfig>): AIMCPServerConfig => {
|
||||
const base: AIMCPServerConfig = {
|
||||
id: `mcp-draft-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
name: '',
|
||||
transport: 'stdio',
|
||||
command: '',
|
||||
args: [],
|
||||
env: {},
|
||||
enabled: true,
|
||||
timeoutSeconds: 20,
|
||||
};
|
||||
return {
|
||||
...base,
|
||||
...seed,
|
||||
transport: seed?.transport || base.transport,
|
||||
args: Array.isArray(seed?.args) ? seed.args : base.args,
|
||||
env: seed?.env || base.env,
|
||||
enabled: seed?.enabled ?? base.enabled,
|
||||
timeoutSeconds: seed?.timeoutSeconds || base.timeoutSeconds,
|
||||
};
|
||||
};
|
||||
|
||||
const waitFor = (delayMs: number) => new Promise<void>((resolve) => {
|
||||
window.setTimeout(resolve, delayMs);
|
||||
});
|
||||
|
||||
const readAIService = () => (window as any).go?.aiservice?.Service;
|
||||
|
||||
export const waitForAIService = async (attempts = 6, delayMs = 80) => {
|
||||
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
||||
const service = readAIService();
|
||||
if (service) {
|
||||
return service;
|
||||
}
|
||||
if (attempt < attempts - 1) {
|
||||
await waitFor(delayMs);
|
||||
}
|
||||
}
|
||||
return readAIService();
|
||||
};
|
||||
|
||||
export const EMPTY_SKILL = (): AISkillConfig => ({
|
||||
id: `skill-draft-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
name: '',
|
||||
description: '',
|
||||
systemPrompt: '',
|
||||
enabled: true,
|
||||
scopes: ['global'],
|
||||
requiredTools: [],
|
||||
});
|
||||
Reference in New Issue
Block a user