v0.1.4-p2

main
mula.liu 2026-03-13 19:23:06 +08:00
parent 5673cb49b6
commit 5d489d9bc4
5 changed files with 123 additions and 56 deletions

View File

@ -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,
}

View File

@ -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 = {}

View File

@ -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.',

View File

@ -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 配置保存失败。',

View File

@ -129,7 +129,9 @@ interface MCPServerConfig {
interface MCPConfigResponse {
bot_id: string;
mcp_servers?: Record<string, MCPServerConfig>;
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<string, string> = {};
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<string, boolean> = {};
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<MCPServerDraft[]> => {
if (!botId) return [];
try {
const res = await axios.get<MCPConfigResponse>(`${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<string, string> = {};
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<string, boolean> = {};
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<MCPServerDraft[]> => {
try {
const res = await axios.post<MCPConfigResponse>(`${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,18 +2899,18 @@ 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<string, boolean> = {};
mcpServers
.filter((_, i) => i !== index)
.forEach((server, idx) => {
nextRows.forEach((server, idx) => {
const key = mcpDraftUiKey(server, idx);
next[key] = typeof prev[key] === 'boolean' ? prev[key] : idx === 0;
});
@ -2892,6 +2925,7 @@ export function BotDashboardModule({
});
return next;
});
await saveBotMcpConfig(nextRows);
};
const buildMcpHeaders = (row: MCPServerDraft): Record<string, string> => {
@ -6118,7 +6152,7 @@ export function BotDashboardModule({
<strong>{row.name || `${t.mcpServer} #${idx + 1}`}</strong>
<div className="ops-config-collapsed-meta">{summary}</div>
{row.locked ? (
<div className="field-label">{isZh ? '内置 topic_mcp 服务(只读' : 'Built-in topic_mcp server (read-only)'}</div>
<div className="field-label">{isZh ? '内置 topic_mcp 服务(只读,可删除后重新开通' : 'Built-in topic_mcp server (read-only, removable for reprovision)'}</div>
) : null}
</div>
<div className="ops-config-card-actions">