v0.1.4-p2
parent
5673cb49b6
commit
5d489d9bc4
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
|
|
|
|||
|
|
@ -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 配置保存失败。',
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Reference in New Issue