diff --git a/backend/main.py b/backend/main.py index 2be3e1a..76548b9 100644 --- a/backend/main.py +++ b/backend/main.py @@ -2362,8 +2362,6 @@ def get_bot_mcp_config(bot_id: str, session: Session = Depends(get_session)): if not bot: raise HTTPException(status_code=404, detail="Bot not found") config_data = _read_bot_config(bot_id) - _ensure_topic_mcp_server(bot_id, config_data=config_data, persist=True) - config_data = _read_bot_config(bot_id) tools_cfg = config_data.get("tools") if not isinstance(tools_cfg, dict): tools_cfg = {} @@ -2372,7 +2370,32 @@ def get_bot_mcp_config(bot_id: str, session: Session = Depends(get_session)): return { "bot_id": bot_id, "mcp_servers": mcp_servers, - "locked_servers": [TOPIC_MCP_SERVER_NAME], + "locked_servers": [TOPIC_MCP_SERVER_NAME] if TOPIC_MCP_SERVER_NAME in mcp_servers else [], + "restart_required": True, + } + + +@app.post("/api/bots/{bot_id}/mcp-config/topic-mcp/enable") +def enable_bot_topic_mcp(bot_id: str, session: Session = Depends(get_session)): + bot = session.get(BotInstance, bot_id) + if not bot: + raise HTTPException(status_code=404, detail="Bot not found") + config_data = _read_bot_config(bot_id) + if not isinstance(config_data, dict): + config_data = {} + _ensure_topic_mcp_server(bot_id, config_data=config_data, persist=True) + config_data = _read_bot_config(bot_id) + tools_cfg = config_data.get("tools") + if not isinstance(tools_cfg, dict): + tools_cfg = {} + mcp_servers = _normalize_mcp_servers(tools_cfg.get("mcpServers")) + mcp_servers = _annotate_locked_mcp_servers(mcp_servers) + _invalidate_bot_detail_cache(bot_id) + return { + "status": "enabled", + "bot_id": bot_id, + "mcp_servers": mcp_servers, + "locked_servers": [TOPIC_MCP_SERVER_NAME] if TOPIC_MCP_SERVER_NAME in mcp_servers else [], "restart_required": True, } diff --git a/backend/services/topic_service.py b/backend/services/topic_service.py index 0ddd87d..d9cc8d9 100644 --- a/backend/services/topic_service.py +++ b/backend/services/topic_service.py @@ -102,7 +102,7 @@ def _annotate_locked_mcp_servers(raw_servers: Dict[str, Dict[str, Any]]) -> Dict def _ensure_topic_mcp_server(bot_id: str, config_data: Optional[Dict[str, Any]] = None, persist: bool = True) -> Dict[str, Any]: - working = dict(config_data) if isinstance(config_data, dict) else _read_bot_config(bot_id) + working = config_data if isinstance(config_data, dict) else _read_bot_config(bot_id) tools_cfg = working.get("tools") if not isinstance(tools_cfg, dict): tools_cfg = {} diff --git a/frontend/src/i18n/dashboard.en.ts b/frontend/src/i18n/dashboard.en.ts index faad7d2..a16fee7 100644 --- a/frontend/src/i18n/dashboard.en.ts +++ b/frontend/src/i18n/dashboard.en.ts @@ -204,6 +204,11 @@ export const dashboardEn = { mcpDraftRequired: 'MCP server name and URL are required.', mcpDraftAdded: 'Added to the MCP list. Save config to apply.', addMcpServer: 'Add MCP Server', + topicMcpEnableTitle: 'Enable topic_mcp', + topicMcpEnableConfirm: 'This bot has not configured topic_mcp yet. Enable it now?', + topicMcpEnableAction: 'Enable Now', + topicMcpEnabled: 'topic_mcp enabled.', + topicMcpEnableFail: 'Failed to enable topic_mcp.', saveMcpConfig: 'Save MCP Config', mcpSaved: 'MCP config saved.', mcpSaveFail: 'Failed to save MCP config.', diff --git a/frontend/src/i18n/dashboard.zh-cn.ts b/frontend/src/i18n/dashboard.zh-cn.ts index 92823cc..210979e 100644 --- a/frontend/src/i18n/dashboard.zh-cn.ts +++ b/frontend/src/i18n/dashboard.zh-cn.ts @@ -204,6 +204,11 @@ export const dashboardZhCn = { mcpDraftRequired: '请先填写 MCP 服务名称和 URL。', mcpDraftAdded: '已加入 MCP 列表,记得保存配置。', addMcpServer: '新增 MCP Server', + topicMcpEnableTitle: '开通 topic_mcp', + topicMcpEnableConfirm: '当前 Bot 尚未配置 topic_mcp。是否立即开通?', + topicMcpEnableAction: '立即开通', + topicMcpEnabled: 'topic_mcp 已开通。', + topicMcpEnableFail: 'topic_mcp 开通失败。', saveMcpConfig: '保存 MCP 配置', mcpSaved: 'MCP 配置已保存。', mcpSaveFail: 'MCP 配置保存失败。', diff --git a/frontend/src/modules/dashboard/BotDashboardModule.tsx b/frontend/src/modules/dashboard/BotDashboardModule.tsx index af1bca7..6f879da 100644 --- a/frontend/src/modules/dashboard/BotDashboardModule.tsx +++ b/frontend/src/modules/dashboard/BotDashboardModule.tsx @@ -129,7 +129,9 @@ interface MCPServerConfig { interface MCPConfigResponse { bot_id: string; mcp_servers?: Record; + locked_servers?: string[]; restart_required?: boolean; + status?: string; } interface MCPTestResponse { @@ -2356,6 +2358,48 @@ export function BotDashboardModule({ }); }; + const mapMcpResponseToDrafts = (payload?: MCPConfigResponse | null): MCPServerDraft[] => { + const rows = payload?.mcp_servers && typeof payload.mcp_servers === 'object' ? payload.mcp_servers : {}; + return Object.entries(rows).map(([name, cfg]) => { + const rawHeaders = cfg?.headers && typeof cfg.headers === 'object' ? cfg.headers : {}; + const headers: Record = {}; + Object.entries(rawHeaders).forEach(([k, v]) => { + const key = String(k || '').trim(); + if (!key) return; + headers[key] = String(v ?? '').trim(); + }); + const headerEntries = Object.entries(headers); + const botIdHeader = headerEntries.find(([k]) => String(k || '').trim().toLowerCase() === 'x-bot-id'); + const botSecretHeader = headerEntries.find(([k]) => String(k || '').trim().toLowerCase() === 'x-bot-secret'); + return { + name: String(name || '').trim(), + type: String(cfg?.type || 'streamableHttp') === 'sse' ? 'sse' : 'streamableHttp', + url: String(cfg?.url || '').trim(), + botId: String(botIdHeader?.[1] || '').trim(), + botSecret: String(botSecretHeader?.[1] || '').trim(), + toolTimeout: String(Number(cfg?.toolTimeout || 60) || 60), + headers, + locked: Boolean(cfg?.locked), + originName: String(name || '').trim(), + }; + }); + }; + + const applyMcpDrafts = (drafts: MCPServerDraft[]) => { + setMcpServers(drafts); + setPersistedMcpServers(drafts); + setExpandedMcpByKey((prev) => { + const next: Record = {}; + drafts.forEach((row, idx) => { + const key = mcpDraftUiKey(row, idx); + next[key] = typeof prev[key] === 'boolean' ? prev[key] : idx === 0; + }); + return next; + }); + setMcpTestByIndex({}); + return drafts; + }; + const openChannelModal = (botId: string) => { if (!botId) return; setExpandedChannelByKey({}); @@ -2376,12 +2420,24 @@ export function BotDashboardModule({ setShowTopicModal(true); }; - const openMcpModal = (botId: string) => { + const openMcpModal = async (botId: string) => { if (!botId) return; setExpandedMcpByKey({}); setNewMcpPanelOpen(false); resetNewMcpDraft(); - void loadBotMcpConfig(botId); + let drafts = await loadBotMcpConfig(botId); + if (!drafts.some((row) => isTopicMcpServerRow(row))) { + const ok = await confirm({ + title: t.topicMcpEnableTitle, + message: t.topicMcpEnableConfirm, + tone: 'warning', + confirmText: t.topicMcpEnableAction, + cancelText: t.cancel, + }); + if (ok) { + drafts = await enableBotTopicMcp(botId); + } + } setShowMcpModal(true); }; @@ -2761,50 +2817,27 @@ export function BotDashboardModule({ }); }; - const loadBotMcpConfig = async (botId: string) => { - if (!botId) return; + const loadBotMcpConfig = async (botId: string): Promise => { + if (!botId) return []; try { const res = await axios.get(`${APP_ENDPOINTS.apiBase}/bots/${botId}/mcp-config`); - const rows = res.data?.mcp_servers && typeof res.data.mcp_servers === 'object' ? res.data.mcp_servers : {}; - const drafts: MCPServerDraft[] = Object.entries(rows).map(([name, cfg]) => { - const rawHeaders = cfg?.headers && typeof cfg.headers === 'object' ? cfg.headers : {}; - const headers: Record = {}; - Object.entries(rawHeaders).forEach(([k, v]) => { - const key = String(k || '').trim(); - if (!key) return; - headers[key] = String(v ?? '').trim(); - }); - const headerEntries = Object.entries(headers); - const botIdHeader = headerEntries.find(([k]) => String(k || '').trim().toLowerCase() === 'x-bot-id'); - const botSecretHeader = headerEntries.find(([k]) => String(k || '').trim().toLowerCase() === 'x-bot-secret'); - return { - name: String(name || '').trim(), - type: String(cfg?.type || 'streamableHttp') === 'sse' ? 'sse' : 'streamableHttp', - url: String(cfg?.url || '').trim(), - botId: String(botIdHeader?.[1] || '').trim(), - botSecret: String(botSecretHeader?.[1] || '').trim(), - toolTimeout: String(Number(cfg?.toolTimeout || 60) || 60), - headers, - locked: Boolean(cfg?.locked), - originName: String(name || '').trim(), - }; - }); - setMcpServers(drafts); - setPersistedMcpServers(drafts); - setExpandedMcpByKey((prev) => { - const next: Record = {}; - drafts.forEach((row, idx) => { - const key = mcpDraftUiKey(row, idx); - next[key] = typeof prev[key] === 'boolean' ? prev[key] : idx === 0; - }); - return next; - }); - setMcpTestByIndex({}); + const drafts = mapMcpResponseToDrafts(res.data); + return applyMcpDrafts(drafts); } catch { - setMcpServers([]); - setPersistedMcpServers([]); - setExpandedMcpByKey({}); - setMcpTestByIndex({}); + applyMcpDrafts([]); + return []; + } + }; + + const enableBotTopicMcp = async (botId: string): Promise => { + try { + const res = await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${botId}/mcp-config/topic-mcp/enable`); + const drafts = mapMcpResponseToDrafts(res.data); + notify(t.topicMcpEnabled, { tone: 'success' }); + return applyMcpDrafts(drafts); + } catch (error: any) { + notify(error?.response?.data?.detail || t.topicMcpEnableFail, { tone: 'error' }); + return []; } }; @@ -2866,21 +2899,21 @@ export function BotDashboardModule({ return isTopicMcpServerRow(row); }; - const removeMcpServer = (index: number) => { + const removeMcpServer = async (index: number) => { const row = mcpServers[index]; if (!canRemoveMcpServer(row)) { notify(isZh ? '内置 MCP 服务不可删除。' : 'Built-in MCP server cannot be removed.', { tone: 'warning' }); return; } - setMcpServers((prev) => prev.filter((_, i) => i !== index)); + const nextRows = mcpServers.filter((_, i) => i !== index); + setMcpServers(nextRows); + setPersistedMcpServers(nextRows); setExpandedMcpByKey((prev) => { const next: Record = {}; - mcpServers - .filter((_, i) => i !== index) - .forEach((server, idx) => { - const key = mcpDraftUiKey(server, idx); - next[key] = typeof prev[key] === 'boolean' ? prev[key] : idx === 0; - }); + nextRows.forEach((server, idx) => { + const key = mcpDraftUiKey(server, idx); + next[key] = typeof prev[key] === 'boolean' ? prev[key] : idx === 0; + }); return next; }); setMcpTestByIndex((prev) => { @@ -2892,6 +2925,7 @@ export function BotDashboardModule({ }); return next; }); + await saveBotMcpConfig(nextRows); }; const buildMcpHeaders = (row: MCPServerDraft): Record => { @@ -6118,7 +6152,7 @@ export function BotDashboardModule({ {row.name || `${t.mcpServer} #${idx + 1}`}
{summary}
{row.locked ? ( -
{isZh ? '内置 topic_mcp 服务(只读)' : 'Built-in topic_mcp server (read-only)'}
+
{isZh ? '内置 topic_mcp 服务(只读,可删除后重新开通)' : 'Built-in topic_mcp server (read-only, removable for reprovision)'}
) : null}