feat: 重构MCP功能和AI服务提供者架构
This commit is contained in:
@@ -37,6 +37,14 @@ interface GenerationSteps {
|
||||
outline: GenerationStep;
|
||||
}
|
||||
|
||||
interface WorldBuildingResult {
|
||||
project_id: string;
|
||||
time_period: string;
|
||||
location: string;
|
||||
atmosphere: string;
|
||||
rules: string;
|
||||
}
|
||||
|
||||
export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
|
||||
config,
|
||||
storagePrefix,
|
||||
@@ -64,7 +72,7 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
|
||||
// 保存生成数据,用于重试
|
||||
const [generationData, setGenerationData] = useState<GenerationConfig | null>(null);
|
||||
// 保存世界观生成结果,用于后续步骤
|
||||
const [worldBuildingResult, setWorldBuildingResult] = useState<any>(null);
|
||||
const [worldBuildingResult, setWorldBuildingResult] = useState<WorldBuildingResult | null>(null);
|
||||
|
||||
// LocalStorage 键名
|
||||
const storageKeys = {
|
||||
@@ -102,6 +110,7 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
|
||||
handleAutoGenerate(config);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [config, resumeProjectId]);
|
||||
|
||||
// 恢复未完成项目的生成
|
||||
@@ -125,33 +134,40 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
|
||||
const wizardStep = project.wizard_step || 0;
|
||||
|
||||
// 根据wizard_step判断从哪里继续
|
||||
// wizard_step: 0=未开始, 1=世界观已完成, 2=职业体系已完成, 3=角色已完成, 4=大纲已完成
|
||||
// 获取世界观数据(用于后续步骤)
|
||||
const worldResult = {
|
||||
project_id: projectIdParam,
|
||||
time_period: project.world_time_period || '',
|
||||
location: project.world_location || '',
|
||||
atmosphere: project.world_atmosphere || '',
|
||||
rules: project.world_rules || ''
|
||||
};
|
||||
|
||||
if (wizardStep === 0) {
|
||||
// 从世界观开始
|
||||
message.info('从世界观步骤开始生成...');
|
||||
setGenerationSteps({ worldBuilding: 'processing', careers: 'pending', characters: 'pending', outline: 'pending' });
|
||||
await resumeFromWorldBuilding(data);
|
||||
} else if (wizardStep === 1) {
|
||||
// 世界观已完成,从角色开始
|
||||
message.info('世界观已完成,从角色步骤继续...');
|
||||
setGenerationSteps({ worldBuilding: 'completed', careers: 'completed', characters: 'processing', outline: 'pending' });
|
||||
|
||||
// 获取世界观数据
|
||||
const worldResult = {
|
||||
project_id: projectIdParam,
|
||||
time_period: project.world_time_period || '',
|
||||
location: project.world_location || '',
|
||||
atmosphere: project.world_atmosphere || '',
|
||||
rules: project.world_rules || ''
|
||||
};
|
||||
// 世界观已完成,从职业体系开始
|
||||
message.info('世界观已完成,从职业体系步骤继续...');
|
||||
setGenerationSteps({ worldBuilding: 'completed', careers: 'processing', characters: 'pending', outline: 'pending' });
|
||||
setWorldBuildingResult(worldResult);
|
||||
setProgress(33);
|
||||
|
||||
await resumeFromCharacters(data, worldResult);
|
||||
setProgress(20);
|
||||
await resumeFromCareers(data, worldResult);
|
||||
} else if (wizardStep === 2) {
|
||||
// 世界观和角色已完成,从大纲开始
|
||||
message.info('世界观和角色已完成,从大纲步骤继续...');
|
||||
// 职业体系已完成,从角色开始
|
||||
message.info('职业体系已完成,从角色步骤继续...');
|
||||
setGenerationSteps({ worldBuilding: 'completed', careers: 'completed', characters: 'processing', outline: 'pending' });
|
||||
setWorldBuildingResult(worldResult);
|
||||
setProgress(40);
|
||||
await resumeFromCharacters(data, worldResult);
|
||||
} else if (wizardStep === 3) {
|
||||
// 角色已完成,从大纲开始
|
||||
message.info('角色已完成,从大纲步骤继续...');
|
||||
setGenerationSteps({ worldBuilding: 'completed', careers: 'completed', characters: 'completed', outline: 'processing' });
|
||||
setProgress(66);
|
||||
setProgress(70);
|
||||
await resumeFromOutline(data, projectIdParam);
|
||||
} else {
|
||||
// 已全部完成
|
||||
@@ -211,11 +227,47 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
|
||||
}
|
||||
);
|
||||
|
||||
await resumeFromCareers(data, worldResult);
|
||||
};
|
||||
|
||||
// 恢复:从职业体系步骤继续
|
||||
const resumeFromCareers = async (data: GenerationConfig, worldResult: WorldBuildingResult) => {
|
||||
const pid = projectId || worldResult.project_id;
|
||||
|
||||
setGenerationSteps(prev => ({ ...prev, careers: 'processing' }));
|
||||
setProgressMessage('正在生成职业体系...');
|
||||
|
||||
await wizardStreamApi.generateCareerSystemStream(
|
||||
{
|
||||
project_id: pid,
|
||||
},
|
||||
{
|
||||
onProgress: (msg, prog) => {
|
||||
setProgress(prog);
|
||||
setProgressMessage(msg);
|
||||
},
|
||||
onResult: (result) => {
|
||||
console.log(`成功生成职业体系:主职业${result.main_careers_count}个,副职业${result.sub_careers_count}个`);
|
||||
setGenerationSteps(prev => ({ ...prev, careers: 'completed' }));
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('职业体系生成失败:', error);
|
||||
setErrorDetails(`职业体系生成失败: ${error}`);
|
||||
setGenerationSteps(prev => ({ ...prev, careers: 'error' }));
|
||||
setLoading(false);
|
||||
throw new Error(error);
|
||||
},
|
||||
onComplete: () => {
|
||||
console.log('职业体系生成完成');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
await resumeFromCharacters(data, worldResult);
|
||||
};
|
||||
|
||||
// 恢复:从角色步骤继续
|
||||
const resumeFromCharacters = async (data: GenerationConfig, worldResult: any) => {
|
||||
const resumeFromCharacters = async (data: GenerationConfig, worldResult: WorldBuildingResult) => {
|
||||
const genreString = Array.isArray(data.genre) ? data.genre.join('、') : data.genre;
|
||||
const pid = projectId || worldResult.project_id;
|
||||
|
||||
@@ -342,26 +394,11 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
|
||||
// 直接使用后端返回的进度值
|
||||
setProgress(prog);
|
||||
setProgressMessage(msg);
|
||||
|
||||
// 检测职业体系生成阶段
|
||||
if (msg.includes('职业体系')) {
|
||||
if (msg.includes('开始') || msg.includes('生成')) {
|
||||
setGenerationSteps(prev => ({
|
||||
...prev,
|
||||
worldBuilding: 'completed',
|
||||
careers: 'processing'
|
||||
}));
|
||||
}
|
||||
if (msg.includes('完成') || msg.includes('✅')) {
|
||||
setGenerationSteps(prev => ({ ...prev, careers: 'completed' }));
|
||||
}
|
||||
}
|
||||
},
|
||||
onResult: (result) => {
|
||||
setProjectId(result.project_id);
|
||||
setWorldBuildingResult(result);
|
||||
setGenerationSteps(prev => ({ ...prev, worldBuilding: 'completed' }));
|
||||
// 职业体系状态已在onProgress中更新
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('世界观生成失败:', error);
|
||||
@@ -385,7 +422,37 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
|
||||
setWorldBuildingResult(worldResult);
|
||||
saveProgress(createdProjectId, data, 'generating');
|
||||
|
||||
// 步骤2: 生成角色
|
||||
// 步骤2: 生成职业体系
|
||||
setGenerationSteps(prev => ({ ...prev, careers: 'processing' }));
|
||||
setProgressMessage('正在生成职业体系...');
|
||||
|
||||
await wizardStreamApi.generateCareerSystemStream(
|
||||
{
|
||||
project_id: createdProjectId,
|
||||
},
|
||||
{
|
||||
onProgress: (msg, prog) => {
|
||||
setProgress(prog);
|
||||
setProgressMessage(msg);
|
||||
},
|
||||
onResult: (result) => {
|
||||
console.log(`成功生成职业体系:主职业${result.main_careers_count}个,副职业${result.sub_careers_count}个`);
|
||||
setGenerationSteps(prev => ({ ...prev, careers: 'completed' }));
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('职业体系生成失败:', error);
|
||||
setErrorDetails(`职业体系生成失败: ${error}`);
|
||||
setGenerationSteps(prev => ({ ...prev, careers: 'error' }));
|
||||
setLoading(false);
|
||||
throw new Error(error);
|
||||
},
|
||||
onComplete: () => {
|
||||
console.log('职业体系生成完成');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 步骤3: 生成角色
|
||||
setGenerationSteps(prev => ({ ...prev, characters: 'processing' }));
|
||||
setProgressMessage('正在生成角色...');
|
||||
|
||||
@@ -497,6 +564,9 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
|
||||
if (generationSteps.worldBuilding === 'error') {
|
||||
message.info('从世界观步骤开始重新生成...');
|
||||
await retryFromWorldBuilding();
|
||||
} else if (generationSteps.careers === 'error') {
|
||||
message.info('从职业体系步骤继续生成...');
|
||||
await retryFromCareers();
|
||||
} else if (generationSteps.characters === 'error') {
|
||||
message.info('从角色步骤继续生成...');
|
||||
await retryFromCharacters();
|
||||
@@ -504,9 +574,10 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
|
||||
message.info('从大纲步骤继续生成...');
|
||||
await retryFromOutline();
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch (error) {
|
||||
console.error('智能重试失败:', error);
|
||||
message.error('重试失败:' + (error.message || '未知错误'));
|
||||
const errorMessage = error instanceof Error ? error.message : '未知错误';
|
||||
message.error('重试失败:' + errorMessage);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
@@ -537,20 +608,6 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
|
||||
// 直接使用后端返回的进度值
|
||||
setProgress(prog);
|
||||
setProgressMessage(msg);
|
||||
|
||||
// 检测职业体系生成阶段
|
||||
if (msg.includes('职业体系')) {
|
||||
if (msg.includes('开始') || msg.includes('生成')) {
|
||||
setGenerationSteps(prev => ({
|
||||
...prev,
|
||||
worldBuilding: 'completed',
|
||||
careers: 'processing'
|
||||
}));
|
||||
}
|
||||
if (msg.includes('完成') || msg.includes('✅')) {
|
||||
setGenerationSteps(prev => ({ ...prev, careers: 'completed' }));
|
||||
}
|
||||
}
|
||||
},
|
||||
onResult: (result) => {
|
||||
setProjectId(result.project_id);
|
||||
@@ -574,17 +631,72 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
|
||||
throw new Error('项目创建失败:未获取到项目ID');
|
||||
}
|
||||
|
||||
await continueFromCharacters(worldResult);
|
||||
await continueFromCareers(worldResult);
|
||||
};
|
||||
|
||||
// 从职业体系步骤继续
|
||||
const retryFromCareers = async () => {
|
||||
if (!worldBuildingResult) {
|
||||
message.warning('缺少必要数据,无法从职业体系步骤继续');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const pid = worldBuildingResult.project_id || projectId;
|
||||
if (!pid) {
|
||||
message.warning('缺少项目ID,无法从职业体系步骤继续');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setGenerationSteps(prev => ({ ...prev, careers: 'processing' }));
|
||||
setProgressMessage('重新生成职业体系...');
|
||||
|
||||
await wizardStreamApi.generateCareerSystemStream(
|
||||
{
|
||||
project_id: pid,
|
||||
},
|
||||
{
|
||||
onProgress: (msg, prog) => {
|
||||
setProgress(prog);
|
||||
setProgressMessage(msg);
|
||||
},
|
||||
onResult: (result) => {
|
||||
console.log(`成功生成职业体系:主职业${result.main_careers_count}个,副职业${result.sub_careers_count}个`);
|
||||
setGenerationSteps(prev => ({ ...prev, careers: 'completed' }));
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('职业体系生成失败:', error);
|
||||
setErrorDetails(`职业体系生成失败: ${error}`);
|
||||
setGenerationSteps(prev => ({ ...prev, careers: 'error' }));
|
||||
setLoading(false);
|
||||
throw new Error(error);
|
||||
},
|
||||
onComplete: () => {
|
||||
console.log('职业体系重新生成完成');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
await continueFromCharacters(worldBuildingResult);
|
||||
};
|
||||
|
||||
// 从角色步骤继续
|
||||
const retryFromCharacters = async () => {
|
||||
if (!generationData || !projectId || !worldBuildingResult) {
|
||||
if (!generationData || !worldBuildingResult) {
|
||||
message.warning('缺少必要数据,无法从角色步骤继续');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 优先使用 worldBuildingResult 中的 project_id,因为重试可能创建了新项目
|
||||
const pid = worldBuildingResult.project_id || projectId;
|
||||
if (!pid) {
|
||||
message.warning('缺少项目ID,无法从角色步骤继续');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setGenerationSteps(prev => ({ ...prev, characters: 'processing' }));
|
||||
setProgressMessage('重新生成角色...');
|
||||
|
||||
@@ -592,7 +704,7 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
|
||||
|
||||
await wizardStreamApi.generateCharactersStream(
|
||||
{
|
||||
project_id: projectId,
|
||||
project_id: pid,
|
||||
count: generationData.character_count,
|
||||
world_context: {
|
||||
time_period: worldBuildingResult.time_period || '',
|
||||
@@ -626,23 +738,31 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
|
||||
}
|
||||
);
|
||||
|
||||
await continueFromOutline();
|
||||
await continueFromOutline(pid);
|
||||
};
|
||||
|
||||
// 从大纲步骤继续
|
||||
const retryFromOutline = async () => {
|
||||
if (!generationData || !projectId) {
|
||||
if (!generationData) {
|
||||
message.warning('缺少必要数据,无法从大纲步骤继续');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 优先使用 worldBuildingResult 中的 project_id,fallback 到状态中的 projectId
|
||||
const pid = (worldBuildingResult?.project_id) || projectId;
|
||||
if (!pid) {
|
||||
message.warning('缺少项目ID,无法从大纲步骤继续');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setGenerationSteps(prev => ({ ...prev, outline: 'processing' }));
|
||||
setProgressMessage('重新生成大纲...');
|
||||
|
||||
await wizardStreamApi.generateCompleteOutlineStream(
|
||||
{
|
||||
project_id: projectId,
|
||||
project_id: pid,
|
||||
chapter_count: generationData.chapter_count,
|
||||
narrative_perspective: generationData.narrative_perspective,
|
||||
target_words: generationData.target_words,
|
||||
@@ -676,20 +796,59 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
|
||||
setLoading(false);
|
||||
|
||||
// 调用完成回调
|
||||
if (projectId) {
|
||||
onComplete(projectId);
|
||||
if (pid) {
|
||||
onComplete(pid);
|
||||
|
||||
// 延迟1秒后自动跳转到项目详情页
|
||||
setTimeout(() => {
|
||||
navigate(`/project/${projectId}`);
|
||||
navigate(`/project/${pid}`);
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
// 从角色步骤开始的完整流程
|
||||
const continueFromCharacters = async (worldResult: any) => {
|
||||
// 从职业体系步骤开始的完整流程
|
||||
const continueFromCareers = async (worldResult: WorldBuildingResult) => {
|
||||
if (!generationData || !worldResult?.project_id) return;
|
||||
|
||||
const pid = worldResult.project_id;
|
||||
|
||||
setGenerationSteps(prev => ({ ...prev, careers: 'processing' }));
|
||||
setProgressMessage('正在生成职业体系...');
|
||||
|
||||
await wizardStreamApi.generateCareerSystemStream(
|
||||
{
|
||||
project_id: pid,
|
||||
},
|
||||
{
|
||||
onProgress: (msg, prog) => {
|
||||
setProgress(prog);
|
||||
setProgressMessage(msg);
|
||||
},
|
||||
onResult: (result) => {
|
||||
console.log(`成功生成职业体系:主职业${result.main_careers_count}个,副职业${result.sub_careers_count}个`);
|
||||
setGenerationSteps(prev => ({ ...prev, careers: 'completed' }));
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('职业体系生成失败:', error);
|
||||
setErrorDetails(`职业体系生成失败: ${error}`);
|
||||
setGenerationSteps(prev => ({ ...prev, careers: 'error' }));
|
||||
setLoading(false);
|
||||
throw new Error(error);
|
||||
},
|
||||
onComplete: () => {
|
||||
console.log('职业体系生成完成');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
await continueFromCharacters(worldResult);
|
||||
};
|
||||
|
||||
// 从角色步骤开始的完整流程
|
||||
const continueFromCharacters = async (worldResult: WorldBuildingResult) => {
|
||||
if (!generationData || !worldResult?.project_id) return;
|
||||
|
||||
const pid = worldResult.project_id;
|
||||
const genreString = Array.isArray(generationData.genre) ? generationData.genre.join('、') : generationData.genre;
|
||||
|
||||
setGenerationSteps(prev => ({ ...prev, characters: 'processing' }));
|
||||
@@ -697,7 +856,7 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
|
||||
|
||||
await wizardStreamApi.generateCharactersStream(
|
||||
{
|
||||
project_id: worldResult.project_id,
|
||||
project_id: pid,
|
||||
count: generationData.character_count,
|
||||
world_context: {
|
||||
time_period: worldResult.time_period || '',
|
||||
@@ -731,19 +890,19 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
|
||||
}
|
||||
);
|
||||
|
||||
await continueFromOutline();
|
||||
await continueFromOutline(pid);
|
||||
};
|
||||
|
||||
// 从大纲步骤开始的完整流程
|
||||
const continueFromOutline = async () => {
|
||||
if (!generationData || !projectId) return;
|
||||
const continueFromOutline = async (pid: string) => {
|
||||
if (!generationData || !pid) return;
|
||||
|
||||
setGenerationSteps(prev => ({ ...prev, outline: 'processing' }));
|
||||
setProgressMessage('正在生成大纲...');
|
||||
|
||||
await wizardStreamApi.generateCompleteOutlineStream(
|
||||
{
|
||||
project_id: projectId,
|
||||
project_id: pid,
|
||||
chapter_count: generationData.chapter_count,
|
||||
narrative_perspective: generationData.narrative_perspective,
|
||||
target_words: generationData.target_words,
|
||||
@@ -777,12 +936,12 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
|
||||
setLoading(false);
|
||||
|
||||
// 调用完成回调
|
||||
if (projectId) {
|
||||
onComplete(projectId);
|
||||
if (pid) {
|
||||
onComplete(pid);
|
||||
|
||||
// 延迟1秒后自动跳转到项目详情页
|
||||
setTimeout(() => {
|
||||
navigate(`/project/${projectId}`);
|
||||
navigate(`/project/${pid}`);
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -28,8 +28,11 @@ import {
|
||||
InfoCircleOutlined,
|
||||
ToolOutlined,
|
||||
ArrowLeftOutlined,
|
||||
ApiOutlined,
|
||||
QuestionCircleOutlined,
|
||||
WarningOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { mcpPluginApi } from '../services/api';
|
||||
import { mcpPluginApi, settingsApi } from '../services/api';
|
||||
import type { MCPPlugin, MCPTool } from '../types';
|
||||
|
||||
const { Paragraph, Text, Title } = Typography;
|
||||
@@ -46,24 +49,112 @@ export default function MCPPluginsPage() {
|
||||
const [editingPlugin, setEditingPlugin] = useState<MCPPlugin | null>(null);
|
||||
const [testingPluginId, setTestingPluginId] = useState<string | null>(null);
|
||||
const [viewingTools, setViewingTools] = useState<{ pluginId: string; tools: MCPTool[] } | null>(null);
|
||||
const [checkingFunctionCalling, setCheckingFunctionCalling] = useState(false);
|
||||
const [modelSupportStatus, setModelSupportStatus] = useState<'unknown' | 'supported' | 'unsupported'>('unknown');
|
||||
|
||||
useEffect(() => {
|
||||
loadPlugins();
|
||||
}, []);
|
||||
const initPage = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 1. 并行获取插件列表和当前设置
|
||||
const [pluginsData, settings] = await Promise.all([
|
||||
mcpPluginApi.getPlugins(),
|
||||
settingsApi.getSettings()
|
||||
]);
|
||||
|
||||
setPlugins(pluginsData);
|
||||
|
||||
// 2. 检查配置一致性
|
||||
const verifiedConfigStr = localStorage.getItem('mcp_verified_config');
|
||||
if (verifiedConfigStr) {
|
||||
try {
|
||||
const verifiedConfig = JSON.parse(verifiedConfigStr);
|
||||
const currentConfig = {
|
||||
provider: settings.api_provider,
|
||||
baseUrl: settings.api_base_url,
|
||||
model: settings.llm_model
|
||||
};
|
||||
|
||||
// 比较关键配置是否发生变更
|
||||
const isConfigChanged =
|
||||
verifiedConfig.provider !== currentConfig.provider ||
|
||||
verifiedConfig.baseUrl !== currentConfig.baseUrl ||
|
||||
verifiedConfig.model !== currentConfig.model;
|
||||
|
||||
if (isConfigChanged) {
|
||||
// 配置已变更
|
||||
setModelSupportStatus('unknown');
|
||||
|
||||
// 检查是否有正在运行的插件
|
||||
const activePlugins = pluginsData.filter(p => p.enabled);
|
||||
if (activePlugins.length > 0) {
|
||||
// 自动禁用所有插件
|
||||
message.loading({ content: '检测到模型配置变更,正在为了安全自动禁用插件...', key: 'auto_disable' });
|
||||
|
||||
await Promise.all(activePlugins.map(p => mcpPluginApi.togglePlugin(p.id, false)));
|
||||
|
||||
// 重新加载插件列表状态
|
||||
const updatedPlugins = await mcpPluginApi.getPlugins();
|
||||
setPlugins(updatedPlugins);
|
||||
|
||||
message.success({ content: '已自动禁用所有插件,请重新检测模型能力', key: 'auto_disable' });
|
||||
|
||||
modal.warning({
|
||||
title: '配置变更提醒',
|
||||
centered: true,
|
||||
content: '检测到您更换了 AI 模型或接口地址。为了防止错误调用,系统已自动暂停所有 MCP 插件。请重新进行"模型能力检查",确认新模型支持 Function Calling 后再启用插件。',
|
||||
okText: '知道了',
|
||||
});
|
||||
} else {
|
||||
// 没有运行中的插件,仅提示
|
||||
message.info('检测到模型配置已变更,请重新检测模型能力');
|
||||
}
|
||||
|
||||
// 清除旧的验证状态
|
||||
localStorage.removeItem('mcp_verified_config');
|
||||
} else {
|
||||
// 配置未变更,恢复验证状态(根据缓存的状态恢复)
|
||||
const cachedStatus = verifiedConfig.status || 'supported';
|
||||
setModelSupportStatus(cachedStatus as 'unknown' | 'supported' | 'unsupported');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to parse verified config:', e);
|
||||
localStorage.removeItem('mcp_verified_config');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Init page failed:', error);
|
||||
message.error('页面初始化失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
initPage();
|
||||
}, [modal]);
|
||||
|
||||
const loadPlugins = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await mcpPluginApi.getPlugins();
|
||||
setPlugins(data);
|
||||
} catch (error) {
|
||||
console.error('Load plugins failed:', error);
|
||||
message.error('加载插件列表失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
if (modelSupportStatus !== 'supported') {
|
||||
modal.confirm({
|
||||
title: '模型能力检查',
|
||||
centered: true,
|
||||
icon: <WarningOutlined />,
|
||||
content: '为了确保 MCP 插件正常工作,您当前使用的 AI 模型必须支持 Function Calling(工具调用)能力。请先进行模型支持检测。',
|
||||
okText: '去检测',
|
||||
cancelText: '取消',
|
||||
onOk: handleCheckFunctionCalling,
|
||||
});
|
||||
return;
|
||||
}
|
||||
setEditingPlugin(null);
|
||||
form.resetFields();
|
||||
form.setFieldsValue({
|
||||
@@ -86,7 +177,7 @@ export default function MCPPluginsPage() {
|
||||
setEditingPlugin(plugin);
|
||||
|
||||
// 重构为标准MCP配置格式
|
||||
const mcpConfig: any = {
|
||||
const mcpConfig: Record<string, Record<string, Record<string, unknown>>> = {
|
||||
mcpServers: {
|
||||
[plugin.plugin_name]: {
|
||||
type: plugin.plugin_type || 'http'
|
||||
@@ -94,7 +185,7 @@ export default function MCPPluginsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
if (plugin.plugin_type === 'http') {
|
||||
if (plugin.plugin_type === 'http' || plugin.plugin_type === 'streamable_http' || plugin.plugin_type === 'sse') {
|
||||
mcpConfig.mcpServers[plugin.plugin_name].url = plugin.server_url;
|
||||
mcpConfig.mcpServers[plugin.plugin_name].headers = plugin.headers || {};
|
||||
} else {
|
||||
@@ -125,6 +216,7 @@ export default function MCPPluginsPage() {
|
||||
message.success('插件已删除');
|
||||
loadPlugins();
|
||||
} catch (error) {
|
||||
console.error('Delete plugin failed:', error);
|
||||
message.error('删除插件失败');
|
||||
}
|
||||
},
|
||||
@@ -137,6 +229,7 @@ export default function MCPPluginsPage() {
|
||||
message.success(enabled ? '插件已启用' : '插件已禁用');
|
||||
loadPlugins();
|
||||
} catch (error) {
|
||||
console.error('Toggle plugin failed:', error);
|
||||
message.error('切换插件状态失败');
|
||||
}
|
||||
};
|
||||
@@ -150,45 +243,62 @@ export default function MCPPluginsPage() {
|
||||
await loadPlugins();
|
||||
|
||||
if (result.success) {
|
||||
const suggestions = result.suggestions || [];
|
||||
const aiChoice = suggestions.find((s: string) => s.startsWith('🤖'))?.replace('🤖 AI选择: ', '') || '';
|
||||
const paramsStr = suggestions.find((s: string) => s.startsWith('📝'))?.replace('📝 参数: ', '') || '';
|
||||
const callTime = suggestions.find((s: string) => s.startsWith('⏱️'))?.replace('⏱️ 耗时: ', '') || '';
|
||||
const resultStr = suggestions.find((s: string) => s.startsWith('📊'))?.replace('📊 结果:\n', '') || '';
|
||||
|
||||
modal.success({
|
||||
title: '测试成功',
|
||||
title: '🎉 测试成功',
|
||||
centered: true,
|
||||
width: isMobile ? '90%' : 600,
|
||||
width: isMobile ? '95%' : 700,
|
||||
content: (
|
||||
<div style={{ padding: '8px 0' }}>
|
||||
<div style={{ marginBottom: 24, padding: 16, background: 'var(--color-success-bg)', border: '1px solid var(--color-success-border)', borderRadius: 8 }}>
|
||||
<Typography.Text strong style={{ color: 'var(--color-success)' }}>
|
||||
<div style={{ marginBottom: 16, padding: 12, background: 'var(--color-success-bg)', border: '1px solid var(--color-success-border)', borderRadius: 8 }}>
|
||||
<Typography.Text strong style={{ color: 'var(--color-success)', fontSize: 14 }}>
|
||||
✓ {result.message}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
{(result.tools_count !== undefined || result.response_time_ms !== undefined) && (
|
||||
<div style={{
|
||||
padding: 16,
|
||||
background: 'var(--color-bg-layout)',
|
||||
borderRadius: 8,
|
||||
marginBottom: 16
|
||||
}}>
|
||||
{result.tools_count !== undefined && (
|
||||
<div style={{ marginBottom: 8, fontSize: 14 }}>
|
||||
<Text type="secondary">可用工具数:</Text>
|
||||
<Text strong>{result.tools_count}</Text>
|
||||
</div>
|
||||
)}
|
||||
{result.response_time_ms !== undefined && (
|
||||
<div style={{ fontSize: 14 }}>
|
||||
<Text type="secondary">响应时间:</Text>
|
||||
<Text strong>{result.response_time_ms}ms</Text>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: 12, marginBottom: 16 }}>
|
||||
<div style={{ padding: 12, background: 'var(--color-bg-layout)', borderRadius: 8 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>可用工具数</Text>
|
||||
<div><Text strong style={{ fontSize: 20 }}>{result.tools_count || 0}</Text></div>
|
||||
</div>
|
||||
<div style={{ padding: 12, background: 'var(--color-bg-layout)', borderRadius: 8 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>总响应时间</Text>
|
||||
<div><Text strong style={{ fontSize: 20 }}>{result.response_time_ms?.toFixed(0) || 0}ms</Text></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{aiChoice && (
|
||||
<div style={{ marginBottom: 12, padding: 12, background: 'var(--color-info-bg)', borderRadius: 8, border: '1px solid var(--color-info-border)' }}>
|
||||
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>🤖 AI选择的工具</Text>
|
||||
<Text code strong>{aiChoice}</Text>
|
||||
{callTime && <Tag color="blue" style={{ marginLeft: 8 }}>{callTime}</Tag>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Alert
|
||||
message='插件状态已自动更新为"运行中"'
|
||||
type="success"
|
||||
showIcon
|
||||
/>
|
||||
{paramsStr && (
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>📝 调用参数</Text>
|
||||
<pre style={{ margin: 0, padding: 8, background: 'var(--color-bg-layout)', borderRadius: 4, fontSize: 12, overflow: 'auto', maxHeight: 100 }}>
|
||||
{(() => { try { return JSON.stringify(JSON.parse(paramsStr), null, 2); } catch { return paramsStr; } })()}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{resultStr && (
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>📊 返回结果预览</Text>
|
||||
<pre style={{ margin: 0, padding: 8, background: 'var(--color-bg-layout)', borderRadius: 4, fontSize: 11, overflow: 'auto', maxHeight: 150, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
||||
{resultStr}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Alert message='插件状态已自动更新为"运行中"' type="success" showIcon />
|
||||
</div>
|
||||
),
|
||||
});
|
||||
@@ -248,7 +358,7 @@ export default function MCPPluginsPage() {
|
||||
),
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch {
|
||||
message.error('测试插件失败');
|
||||
} finally {
|
||||
setTestingPluginId(null);
|
||||
@@ -260,17 +370,181 @@ export default function MCPPluginsPage() {
|
||||
const result = await mcpPluginApi.getPluginTools(pluginId);
|
||||
setViewingTools({ pluginId, tools: result.tools });
|
||||
} catch (error) {
|
||||
console.error('Get tools failed:', error);
|
||||
message.error('获取工具列表失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (values: any) => {
|
||||
const handleCheckFunctionCalling = async () => {
|
||||
// 从设置中获取当前配置
|
||||
setCheckingFunctionCalling(true);
|
||||
try {
|
||||
const settings = await settingsApi.getSettings();
|
||||
|
||||
if (!settings.api_key || !settings.llm_model) {
|
||||
message.warning('请先在设置页面配置 API Key 和模型');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await settingsApi.checkFunctionCalling({
|
||||
api_key: settings.api_key,
|
||||
api_base_url: settings.api_base_url || '',
|
||||
provider: settings.api_provider || 'openai',
|
||||
llm_model: settings.llm_model,
|
||||
});
|
||||
|
||||
// 无论成功失败,都缓存当前测试的配置和状态
|
||||
const configToCache = {
|
||||
provider: settings.api_provider,
|
||||
baseUrl: settings.api_base_url,
|
||||
model: settings.llm_model,
|
||||
status: result.success && result.supported ? 'supported' : 'unsupported',
|
||||
testedAt: new Date().toISOString()
|
||||
};
|
||||
localStorage.setItem('mcp_verified_config', JSON.stringify(configToCache));
|
||||
|
||||
if (result.success && result.supported) {
|
||||
setModelSupportStatus('supported');
|
||||
|
||||
modal.success({
|
||||
title: '✅ Function Calling 支持检测',
|
||||
centered: true,
|
||||
width: isMobile ? '95%' : 700,
|
||||
content: (
|
||||
<div style={{ padding: '8px 0' }}>
|
||||
<div style={{ marginBottom: 16, padding: 12, background: 'var(--color-success-bg)', border: '1px solid var(--color-success-border)', borderRadius: 8 }}>
|
||||
<Typography.Text strong style={{ color: 'var(--color-success)', fontSize: 14 }}>
|
||||
✓ {result.message}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: 12, marginBottom: 16 }}>
|
||||
<div style={{ padding: 12, background: 'var(--color-bg-layout)', borderRadius: 8 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>API 提供商</Text>
|
||||
<div><Text strong style={{ fontSize: 16 }}>{result.provider}</Text></div>
|
||||
</div>
|
||||
<div style={{ padding: 12, background: 'var(--color-bg-layout)', borderRadius: 8 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>响应时间</Text>
|
||||
<div><Text strong style={{ fontSize: 16 }}>{result.response_time_ms?.toFixed(0) || 0}ms</Text></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 12, padding: 12, background: 'var(--color-info-bg)', borderRadius: 8, border: '1px solid var(--color-info-border)' }}>
|
||||
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>🔧 模型信息</Text>
|
||||
<Text code strong>{result.model}</Text>
|
||||
{result.details?.finish_reason && (
|
||||
<Tag color="green" style={{ marginLeft: 8 }}>finish_reason: {result.details.finish_reason}</Tag>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{result.details && (
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>📊 检测详情</Text>
|
||||
<div style={{ padding: 8, background: 'var(--color-bg-layout)', borderRadius: 4, fontSize: 12 }}>
|
||||
<div>✓ 工具调用数量: {result.details.tool_call_count || 0}</div>
|
||||
<div>✓ 测试工具: {result.details.test_tool || 'N/A'}</div>
|
||||
<div>✓ 响应类型: {result.details.response_type || 'N/A'}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{result.tool_calls && result.tool_calls.length > 0 && (
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>🔨 工具调用示例</Text>
|
||||
<pre style={{ margin: 0, padding: 8, background: 'var(--color-bg-layout)', borderRadius: 4, fontSize: 11, overflow: 'auto', maxHeight: 150 }}>
|
||||
{JSON.stringify(result.tool_calls[0], null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{result.suggestions && result.suggestions.length > 0 && (
|
||||
<div style={{ padding: 12, background: 'var(--color-success-bg)', border: '1px solid var(--color-success-border)', borderRadius: 8 }}>
|
||||
<Text strong style={{ fontSize: 13, display: 'block', marginBottom: 8 }}>💡 建议</Text>
|
||||
<ul style={{ margin: 0, paddingLeft: 20, fontSize: 12 }}>
|
||||
{result.suggestions.map((s: string, i: number) => (
|
||||
<li key={i} style={{ marginBottom: 4 }}>{s}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
});
|
||||
} else {
|
||||
setModelSupportStatus('unsupported');
|
||||
modal.warning({
|
||||
title: '❌ Function Calling 支持检测',
|
||||
centered: true,
|
||||
width: isMobile ? '95%' : 700,
|
||||
content: (
|
||||
<div style={{ padding: '8px 0' }}>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Alert
|
||||
message={result.message || '模型不支持 Function Calling'}
|
||||
type="warning"
|
||||
showIcon
|
||||
/>
|
||||
</div>
|
||||
|
||||
{result.error && (
|
||||
<div style={{
|
||||
padding: 16,
|
||||
background: 'var(--color-warning-bg)',
|
||||
border: '1px solid var(--color-warning-border)',
|
||||
borderRadius: 8,
|
||||
marginBottom: 16
|
||||
}}>
|
||||
<Text strong style={{ fontSize: 14, display: 'block', marginBottom: 8 }}>错误信息:</Text>
|
||||
<Text style={{ fontSize: 13, fontFamily: 'monospace' }}>
|
||||
{result.error}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{result.response_preview && (
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>📝 模型返回内容(前200字符)</Text>
|
||||
<pre style={{ margin: 0, padding: 8, background: 'var(--color-bg-layout)', borderRadius: 4, fontSize: 11, overflow: 'auto', maxHeight: 100, whiteSpace: 'pre-wrap' }}>
|
||||
{result.response_preview}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{result.suggestions && result.suggestions.length > 0 && (
|
||||
<div style={{
|
||||
padding: 16,
|
||||
background: 'var(--color-info-bg)',
|
||||
border: '1px solid var(--color-info-border)',
|
||||
borderRadius: 8
|
||||
}}>
|
||||
<Text strong style={{ fontSize: 14, display: 'block', marginBottom: 8 }}>💡 建议:</Text>
|
||||
<ul style={{ margin: 0, paddingLeft: 20, fontSize: 13 }}>
|
||||
{result.suggestions.map((s: string, i: number) => (
|
||||
<li key={i} style={{ marginBottom: 4 }}>{s}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Check function calling failed:', error);
|
||||
message.error('检测失败,请稍后重试');
|
||||
setModelSupportStatus('unsupported');
|
||||
} finally {
|
||||
setCheckingFunctionCalling(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (values: { config_json: string; enabled: boolean; category?: string }) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 验证JSON格式
|
||||
try {
|
||||
JSON.parse(values.config_json);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
message.error('配置JSON格式错误,请检查');
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -289,8 +563,9 @@ export default function MCPPluginsPage() {
|
||||
setModalVisible(false);
|
||||
form.resetFields();
|
||||
loadPlugins();
|
||||
} catch (error: any) {
|
||||
const errorMsg = error?.response?.data?.detail || '操作失败';
|
||||
} catch (error: unknown) {
|
||||
const err = error as { response?: { data?: { detail?: string } } };
|
||||
const errorMsg = err?.response?.data?.detail || '操作失败';
|
||||
message.error(errorMsg);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -407,38 +682,104 @@ export default function MCPPluginsPage() {
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 使用提示 */}
|
||||
<Alert
|
||||
message={
|
||||
<Space align="center">
|
||||
<InfoCircleOutlined style={{ fontSize: 16, color: 'var(--color-primary)' }} />
|
||||
<Text strong style={{ fontSize: isMobile ? 13 : 14, color: 'var(--color-text-primary)' }}>什么是 MCP 插件?</Text>
|
||||
</Space>
|
||||
}
|
||||
description={
|
||||
<div>
|
||||
<Text style={{ fontSize: isMobile ? 12 : 13, display: 'block', marginBottom: 8 }}>
|
||||
• <strong>MCP (Model Context Protocol)</strong> 是一个标准化的协议,允许 AI 调用外部工具获取数据。
|
||||
</Text>
|
||||
<Text style={{ fontSize: isMobile ? 12 : 13, display: 'block' }}>
|
||||
• 通过添加 MCP 插件,AI 可以访问搜索引擎、数据库、API 等外部服务,增强创作能力。
|
||||
</Text>
|
||||
<div style={{ marginTop: isMobile ? 16 : 24, display: 'flex', gap: 16, flexDirection: isMobile ? 'column' : 'row' }}>
|
||||
<Card
|
||||
variant="borderless"
|
||||
style={{
|
||||
flex: 1,
|
||||
borderRadius: 12,
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.6)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.03)'
|
||||
}}
|
||||
bodyStyle={{ padding: 20 }}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Space align="start">
|
||||
<div style={{
|
||||
width: 40, height: 40, borderRadius: '50%',
|
||||
background: modelSupportStatus === 'supported' ? 'var(--color-success-bg)' : modelSupportStatus === 'unsupported' ? 'var(--color-error-bg)' : 'var(--color-info-bg)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
border: `1px solid ${modelSupportStatus === 'supported' ? 'var(--color-success-border)' : modelSupportStatus === 'unsupported' ? 'var(--color-error-border)' : 'var(--color-info-border)'}`
|
||||
}}>
|
||||
{modelSupportStatus === 'supported' ? (
|
||||
<CheckCircleOutlined style={{ fontSize: 20, color: 'var(--color-success)' }} />
|
||||
) : modelSupportStatus === 'unsupported' ? (
|
||||
<CloseCircleOutlined style={{ fontSize: 20, color: 'var(--color-error)' }} />
|
||||
) : (
|
||||
<QuestionCircleOutlined style={{ fontSize: 20, color: 'var(--color-info)' }} />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Text strong style={{ fontSize: 16, display: 'block', color: 'var(--color-text-primary)' }}>模型能力检查</Text>
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>
|
||||
{modelSupportStatus === 'supported'
|
||||
? '当前模型支持 Function Calling,可正常使用 MCP 插件'
|
||||
: modelSupportStatus === 'unsupported'
|
||||
? '当前模型不支持 Function Calling,无法使用 MCP 插件'
|
||||
: '请先检测模型是否支持 Function Calling 能力'}
|
||||
</Text>
|
||||
</div>
|
||||
</Space>
|
||||
<Button
|
||||
type={modelSupportStatus === 'supported' ? 'default' : 'primary'}
|
||||
icon={<ApiOutlined />}
|
||||
onClick={handleCheckFunctionCalling}
|
||||
loading={checkingFunctionCalling}
|
||||
style={{ borderRadius: 8 }}
|
||||
>
|
||||
{modelSupportStatus === 'unknown' ? '开始检测' : '重新检测'}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
type="info"
|
||||
showIcon={false}
|
||||
style={{
|
||||
marginTop: isMobile ? 16 : 24,
|
||||
borderRadius: 12,
|
||||
background: 'rgba(230, 247, 255, 0.6)',
|
||||
border: '1px solid rgba(145, 213, 255, 0.6)',
|
||||
backdropFilter: 'blur(5px)'
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
variant="borderless"
|
||||
style={{
|
||||
flex: 1,
|
||||
borderRadius: 12,
|
||||
background: 'rgba(230, 247, 255, 0.6)',
|
||||
border: '1px solid rgba(145, 213, 255, 0.6)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.03)'
|
||||
}}
|
||||
bodyStyle={{ padding: 20 }}
|
||||
>
|
||||
<Space align="start">
|
||||
<InfoCircleOutlined style={{ fontSize: 20, color: 'var(--color-primary)', marginTop: 4 }} />
|
||||
<div>
|
||||
<Text strong style={{ fontSize: 16, display: 'block', color: 'var(--color-text-primary)', marginBottom: 4 }}>什么是 MCP 插件?</Text>
|
||||
<Text style={{ fontSize: 13, display: 'block', color: 'var(--color-text-secondary)', lineHeight: 1.6 }}>
|
||||
MCP (Model Context Protocol) 协议允许 AI 调用外部工具获取数据。通过添加插件,AI 可以访问搜索引擎、数据库、API 等服务,大幅增强创作能力。
|
||||
</Text>
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 主内容区 */}
|
||||
<div style={{ flex: 1 }}>
|
||||
{/* 模型能力未验证时的警告提示 */}
|
||||
{modelSupportStatus !== 'supported' && plugins.length > 0 && (
|
||||
<Alert
|
||||
message={
|
||||
modelSupportStatus === 'unsupported'
|
||||
? '当前模型不支持 Function Calling,所有插件操作已禁用'
|
||||
: '请先完成模型能力检查,才能操作插件'
|
||||
}
|
||||
type={modelSupportStatus === 'unsupported' ? 'error' : 'warning'}
|
||||
showIcon
|
||||
icon={modelSupportStatus === 'unsupported' ? <CloseCircleOutlined /> : <WarningOutlined />}
|
||||
style={{ marginBottom: 16, borderRadius: 8 }}
|
||||
action={
|
||||
<Button size="small" type="primary" onClick={handleCheckFunctionCalling} loading={checkingFunctionCalling}>
|
||||
{modelSupportStatus === 'unknown' ? '开始检测' : '重新检测'}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 插件列表 */}
|
||||
<Spin spinning={loading}>
|
||||
@@ -479,7 +820,7 @@ export default function MCPPluginsPage() {
|
||||
{plugin.display_name || plugin.plugin_name}
|
||||
</Text>
|
||||
{getStatusTag(plugin)}
|
||||
<Tag color={plugin.plugin_type === 'http' ? 'blue' : 'cyan'}>
|
||||
<Tag color={plugin.plugin_type === 'http' || plugin.plugin_type === 'streamable_http' || plugin.plugin_type === 'sse' ? 'blue' : 'cyan'}>
|
||||
{plugin.plugin_type?.toUpperCase() || 'UNKNOWN'}
|
||||
</Tag>
|
||||
{plugin.category && plugin.category !== 'general' && (
|
||||
@@ -500,7 +841,7 @@ export default function MCPPluginsPage() {
|
||||
)}
|
||||
|
||||
{/* 只显示有值的URL或命令,脱敏处理敏感信息 */}
|
||||
{plugin.plugin_type === 'http' && plugin.server_url && (
|
||||
{(plugin.plugin_type === 'http' || plugin.plugin_type === 'streamable_http' || plugin.plugin_type === 'sse') && plugin.server_url && (
|
||||
<div style={{ fontSize: isMobile ? '11px' : '12px' }}>
|
||||
<Text type="secondary" code>
|
||||
{(() => {
|
||||
@@ -551,9 +892,10 @@ export default function MCPPluginsPage() {
|
||||
|
||||
<Space size="small" wrap>
|
||||
<Switch
|
||||
title={plugin.enabled ? '禁用插件' : '启用插件'}
|
||||
title={modelSupportStatus !== 'supported' ? '请先完成模型能力检查' : (plugin.enabled ? '禁用插件' : '启用插件')}
|
||||
checked={plugin.enabled}
|
||||
onChange={(checked) => handleToggle(plugin, checked)}
|
||||
disabled={modelSupportStatus !== 'supported'}
|
||||
size={isMobile ? 'small' : 'default'}
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
@@ -563,30 +905,33 @@ export default function MCPPluginsPage() {
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
title="测试连接"
|
||||
title={modelSupportStatus !== 'supported' ? '请先完成模型能力检查' : '测试连接'}
|
||||
icon={<ThunderboltOutlined />}
|
||||
onClick={() => handleTest(plugin.id)}
|
||||
loading={testingPluginId === plugin.id}
|
||||
disabled={modelSupportStatus !== 'supported'}
|
||||
size={isMobile ? 'small' : 'middle'}
|
||||
/>
|
||||
<Button
|
||||
title="查看工具"
|
||||
title={modelSupportStatus !== 'supported' ? '请先完成模型能力检查' : '查看工具'}
|
||||
icon={<ToolOutlined />}
|
||||
onClick={() => handleViewTools(plugin.id)}
|
||||
disabled={!plugin.enabled || plugin.status !== 'active'}
|
||||
disabled={modelSupportStatus !== 'supported' || !plugin.enabled || plugin.status !== 'active'}
|
||||
size={isMobile ? 'small' : 'middle'}
|
||||
/>
|
||||
<Button
|
||||
title="编辑"
|
||||
title={modelSupportStatus !== 'supported' ? '请先完成模型能力检查' : '编辑'}
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleEdit(plugin)}
|
||||
disabled={modelSupportStatus !== 'supported'}
|
||||
size={isMobile ? 'small' : 'middle'}
|
||||
/>
|
||||
<Button
|
||||
title="删除"
|
||||
title={modelSupportStatus !== 'supported' ? '请先完成模型能力检查' : '删除'}
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleDelete(plugin)}
|
||||
disabled={modelSupportStatus !== 'supported'}
|
||||
size={isMobile ? 'small' : 'middle'}
|
||||
/>
|
||||
</Space>
|
||||
@@ -627,7 +972,7 @@ export default function MCPPluginsPage() {
|
||||
{
|
||||
"mcpServers": {
|
||||
"exa": {
|
||||
"type": "http",
|
||||
"type": "streamable_http",
|
||||
"url": "https://mcp.exa.ai/mcp?exaApiKey=YOUR_API_KEY",
|
||||
"headers": {}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Card, Form, Input, Button, Select, Slider, InputNumber, message, Space, Typography, Spin, Modal, Alert, Grid, Tabs, List, Tag, Popconfirm, Empty, Row, Col } from 'antd';
|
||||
import { SettingOutlined, SaveOutlined, DeleteOutlined, ReloadOutlined, ArrowLeftOutlined, InfoCircleOutlined, CheckCircleOutlined, CloseCircleOutlined, ThunderboltOutlined, PlusOutlined, EditOutlined, CopyOutlined } from '@ant-design/icons';
|
||||
import { settingsApi } from '../services/api';
|
||||
import { SettingOutlined, SaveOutlined, DeleteOutlined, ReloadOutlined, ArrowLeftOutlined, InfoCircleOutlined, CheckCircleOutlined, CloseCircleOutlined, ThunderboltOutlined, PlusOutlined, EditOutlined, CopyOutlined, WarningOutlined } from '@ant-design/icons';
|
||||
import { settingsApi, mcpPluginApi } from '../services/api';
|
||||
import type { SettingsUpdate, APIKeyPreset, PresetCreateRequest, APIKeyPresetConfig } from '../types';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
@@ -95,10 +95,86 @@ export default function SettingsPage() {
|
||||
const handleSave = async (values: SettingsUpdate) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 检查是否与 MCP 缓存的配置不一致
|
||||
const verifiedConfigStr = localStorage.getItem('mcp_verified_config');
|
||||
let configChanged = false;
|
||||
|
||||
if (verifiedConfigStr) {
|
||||
try {
|
||||
const verifiedConfig = JSON.parse(verifiedConfigStr);
|
||||
configChanged =
|
||||
verifiedConfig.provider !== values.api_provider ||
|
||||
verifiedConfig.baseUrl !== values.api_base_url ||
|
||||
verifiedConfig.model !== values.llm_model;
|
||||
} catch (e) {
|
||||
console.error('Failed to parse verified config:', e);
|
||||
}
|
||||
}
|
||||
|
||||
await settingsApi.saveSettings(values);
|
||||
message.success('设置已保存');
|
||||
setHasSettings(true);
|
||||
setIsDefaultSettings(false);
|
||||
|
||||
// 如果配置发生变化,需要处理 MCP 插件
|
||||
if (configChanged) {
|
||||
// 清除 MCP 验证缓存
|
||||
localStorage.removeItem('mcp_verified_config');
|
||||
|
||||
// 检查并禁用所有 MCP 插件
|
||||
try {
|
||||
const plugins = await mcpPluginApi.getPlugins();
|
||||
const activePlugins = plugins.filter(p => p.enabled);
|
||||
|
||||
if (activePlugins.length > 0) {
|
||||
// 禁用所有插件
|
||||
message.loading({ content: '正在禁用 MCP 插件...', key: 'disable_mcp' });
|
||||
await Promise.all(activePlugins.map(p => mcpPluginApi.togglePlugin(p.id, false)));
|
||||
message.success({ content: '已禁用所有 MCP 插件', key: 'disable_mcp' });
|
||||
|
||||
// 显示提示弹窗
|
||||
modal.warning({
|
||||
title: (
|
||||
<Space>
|
||||
<WarningOutlined style={{ color: '#faad14' }} />
|
||||
<span>API 配置已更改</span>
|
||||
</Space>
|
||||
),
|
||||
centered: true,
|
||||
content: (
|
||||
<div style={{ padding: '8px 0' }}>
|
||||
<Alert
|
||||
message="检测到您修改了 API 配置(提供商、地址或模型),为确保 MCP 插件正常工作,系统已自动禁用所有插件。"
|
||||
type="warning"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
<div style={{
|
||||
padding: 12,
|
||||
background: 'var(--color-info-bg)',
|
||||
border: '1px solid var(--color-info-border)',
|
||||
borderRadius: 8
|
||||
}}>
|
||||
<Text strong style={{ display: 'block', marginBottom: 8 }}>请完成以下步骤:</Text>
|
||||
<ol style={{ margin: 0, paddingLeft: 20, fontSize: 13 }}>
|
||||
<li>前往 MCP 插件管理页面</li>
|
||||
<li>重新进行"模型能力检查"</li>
|
||||
<li>确认新模型支持 Function Calling 后再启用插件</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
okText: '前往 MCP 页面',
|
||||
cancelText: '稍后处理',
|
||||
onOk: () => {
|
||||
navigate('/mcp-plugins');
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to disable MCP plugins:', err);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('保存设置失败');
|
||||
} finally {
|
||||
@@ -348,10 +424,94 @@ export default function SettingsPage() {
|
||||
|
||||
const handlePresetActivate = async (presetId: string, presetName: string) => {
|
||||
try {
|
||||
// 获取预设配置用于比较
|
||||
const preset = presets.find(p => p.id === presetId);
|
||||
|
||||
await settingsApi.activatePreset(presetId);
|
||||
message.success(`已激活预设: ${presetName}`);
|
||||
loadPresets();
|
||||
loadSettings(); // 重新加载当前配置
|
||||
|
||||
// 检查是否与 MCP 缓存的配置不一致
|
||||
if (preset) {
|
||||
const verifiedConfigStr = localStorage.getItem('mcp_verified_config');
|
||||
let configChanged = false;
|
||||
|
||||
if (verifiedConfigStr) {
|
||||
try {
|
||||
const verifiedConfig = JSON.parse(verifiedConfigStr);
|
||||
configChanged =
|
||||
verifiedConfig.provider !== preset.config.api_provider ||
|
||||
verifiedConfig.baseUrl !== preset.config.api_base_url ||
|
||||
verifiedConfig.model !== preset.config.llm_model;
|
||||
} catch (e) {
|
||||
console.error('Failed to parse verified config:', e);
|
||||
configChanged = true; // 解析失败也视为配置变化
|
||||
}
|
||||
} else {
|
||||
// 没有缓存的配置,如果有启用的插件也需要处理
|
||||
configChanged = true;
|
||||
}
|
||||
|
||||
if (configChanged) {
|
||||
// 清除 MCP 验证缓存
|
||||
localStorage.removeItem('mcp_verified_config');
|
||||
|
||||
// 检查并禁用所有 MCP 插件
|
||||
try {
|
||||
const plugins = await mcpPluginApi.getPlugins();
|
||||
const activePlugins = plugins.filter(p => p.enabled);
|
||||
|
||||
if (activePlugins.length > 0) {
|
||||
// 禁用所有插件
|
||||
message.loading({ content: '正在禁用 MCP 插件...', key: 'disable_mcp' });
|
||||
await Promise.all(activePlugins.map(p => mcpPluginApi.togglePlugin(p.id, false)));
|
||||
message.success({ content: '已禁用所有 MCP 插件', key: 'disable_mcp' });
|
||||
|
||||
// 显示提示弹窗
|
||||
modal.warning({
|
||||
title: (
|
||||
<Space>
|
||||
<WarningOutlined style={{ color: '#faad14' }} />
|
||||
<span>API 配置已更改</span>
|
||||
</Space>
|
||||
),
|
||||
centered: true,
|
||||
content: (
|
||||
<div style={{ padding: '8px 0' }}>
|
||||
<Alert
|
||||
message={`切换到预设「${presetName}」后,API 配置发生了变化。为确保 MCP 插件正常工作,系统已自动禁用所有插件。`}
|
||||
type="warning"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
<div style={{
|
||||
padding: 12,
|
||||
background: 'var(--color-info-bg)',
|
||||
border: '1px solid var(--color-info-border)',
|
||||
borderRadius: 8
|
||||
}}>
|
||||
<Text strong style={{ display: 'block', marginBottom: 8 }}>请完成以下步骤:</Text>
|
||||
<ol style={{ margin: 0, paddingLeft: 20, fontSize: 13 }}>
|
||||
<li>前往 MCP 插件管理页面</li>
|
||||
<li>重新进行"模型能力检查"</li>
|
||||
<li>确认新模型支持 Function Calling 后再启用插件</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
okText: '前往 MCP 页面',
|
||||
cancelText: '稍后处理',
|
||||
onOk: () => {
|
||||
navigate('/mcp-plugins');
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to disable MCP plugins:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('激活失败');
|
||||
console.error(error);
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import axios from 'axios';
|
||||
|
||||
interface MCPPluginSimpleCreate {
|
||||
config_json: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
import { message } from 'antd';
|
||||
import { ssePost } from '../utils/sseClient';
|
||||
import type { SSEClientOptions } from '../utils/sseClient';
|
||||
@@ -50,8 +45,14 @@ import type {
|
||||
PresetCreateRequest,
|
||||
PresetUpdateRequest,
|
||||
PresetListResponse,
|
||||
ChapterPlanItem,
|
||||
} from '../types';
|
||||
|
||||
interface MCPPluginSimpleCreate {
|
||||
config_json: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: '/api',
|
||||
timeout: 120000,
|
||||
@@ -205,6 +206,36 @@ export const settingsApi = {
|
||||
suggestions?: string[];
|
||||
}>('/settings/test', params),
|
||||
|
||||
checkFunctionCalling: (params: { api_key: string; api_base_url: string; provider: string; llm_model: string }) =>
|
||||
api.post<unknown, {
|
||||
success: boolean;
|
||||
supported: boolean;
|
||||
message: string;
|
||||
response_time_ms?: number;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
details?: {
|
||||
finish_reason?: string;
|
||||
has_tool_calls?: boolean;
|
||||
tool_call_count?: number;
|
||||
test_tool?: string;
|
||||
test_prompt?: string;
|
||||
response_type?: string;
|
||||
};
|
||||
tool_calls?: Array<{
|
||||
id?: string;
|
||||
type?: string;
|
||||
function?: {
|
||||
name: string;
|
||||
arguments: string;
|
||||
};
|
||||
}>;
|
||||
response_preview?: string;
|
||||
error?: string;
|
||||
error_type?: string;
|
||||
suggestions?: string[];
|
||||
}>('/settings/check-function-calling', params),
|
||||
|
||||
// API配置预设管理
|
||||
getPresets: () =>
|
||||
api.get<unknown, PresetListResponse>('/settings/presets'),
|
||||
@@ -410,7 +441,7 @@ export const outlineApi = {
|
||||
api.post<unknown, OutlineExpansionResponse>(`/outlines/${outlineId}/expand`, data),
|
||||
|
||||
// 根据已有规划创建章节(避免重复AI调用)
|
||||
createChaptersFromPlans: (outlineId: string, chapterPlans: any[]) =>
|
||||
createChaptersFromPlans: (outlineId: string, chapterPlans: ChapterPlanItem[]) =>
|
||||
api.post<unknown, {
|
||||
outline_id: string;
|
||||
outline_title: string;
|
||||
@@ -711,6 +742,25 @@ export const wizardStreamApi = {
|
||||
options
|
||||
),
|
||||
|
||||
generateCareerSystemStream: (
|
||||
data: {
|
||||
project_id: string;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
},
|
||||
options?: SSEClientOptions
|
||||
) => ssePost<{
|
||||
project_id: string;
|
||||
main_careers_count: number;
|
||||
sub_careers_count: number;
|
||||
main_careers: string[];
|
||||
sub_careers: string[];
|
||||
}>(
|
||||
'/api/wizard-stream/career-system',
|
||||
data,
|
||||
options
|
||||
),
|
||||
|
||||
generateCompleteOutlineStream: (
|
||||
data: {
|
||||
project_id: string;
|
||||
|
||||
@@ -356,7 +356,7 @@ export function useChapterSync() {
|
||||
message.progress || 0
|
||||
);
|
||||
}
|
||||
} else if (message.type === 'content' && message.content) {
|
||||
} else if ((message.type === 'content' || message.type === 'chunk') && message.content) {
|
||||
fullContent += message.content;
|
||||
if (onProgress) {
|
||||
onProgress(fullContent);
|
||||
|
||||
@@ -667,7 +667,7 @@ export interface MCPPlugin {
|
||||
plugin_name: string;
|
||||
display_name: string;
|
||||
description?: string;
|
||||
plugin_type: 'http' | 'stdio';
|
||||
plugin_type: 'http' | 'stdio' | 'streamable_http' | 'sse';
|
||||
category: string;
|
||||
|
||||
// HTTP类型字段
|
||||
@@ -693,7 +693,7 @@ export interface MCPPluginCreate {
|
||||
plugin_name: string;
|
||||
display_name?: string;
|
||||
description?: string;
|
||||
server_type: 'http' | 'stdio';
|
||||
server_type: 'http' | 'stdio' | 'streamable_http' | 'sse';
|
||||
server_url?: string;
|
||||
command?: string;
|
||||
args?: string[];
|
||||
|
||||
Reference in New Issue
Block a user