feat: 重构MCP功能和AI服务提供者架构

This commit is contained in:
xiamuceer-j
2026-01-09 17:13:19 +08:00
parent f3c224261d
commit 77c5489ff8
49 changed files with 4763 additions and 4307 deletions
+230 -71
View File
@@ -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_idfallback 到状态中的 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);
}
};
+422 -77
View File
@@ -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": {}
}
+162 -2
View File
@@ -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);
+56 -6
View File
@@ -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;
+1 -1
View File
@@ -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);
+2 -2
View File
@@ -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[];