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:
|
if not bot:
|
||||||
raise HTTPException(status_code=404, detail="Bot not found")
|
raise HTTPException(status_code=404, detail="Bot not found")
|
||||||
config_data = _read_bot_config(bot_id)
|
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")
|
tools_cfg = config_data.get("tools")
|
||||||
if not isinstance(tools_cfg, dict):
|
if not isinstance(tools_cfg, dict):
|
||||||
tools_cfg = {}
|
tools_cfg = {}
|
||||||
|
|
@ -2372,7 +2370,32 @@ def get_bot_mcp_config(bot_id: str, session: Session = Depends(get_session)):
|
||||||
return {
|
return {
|
||||||
"bot_id": bot_id,
|
"bot_id": bot_id,
|
||||||
"mcp_servers": mcp_servers,
|
"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,
|
"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]:
|
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")
|
tools_cfg = working.get("tools")
|
||||||
if not isinstance(tools_cfg, dict):
|
if not isinstance(tools_cfg, dict):
|
||||||
tools_cfg = {}
|
tools_cfg = {}
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,11 @@ export const dashboardEn = {
|
||||||
mcpDraftRequired: 'MCP server name and URL are required.',
|
mcpDraftRequired: 'MCP server name and URL are required.',
|
||||||
mcpDraftAdded: 'Added to the MCP list. Save config to apply.',
|
mcpDraftAdded: 'Added to the MCP list. Save config to apply.',
|
||||||
addMcpServer: 'Add MCP Server',
|
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',
|
saveMcpConfig: 'Save MCP Config',
|
||||||
mcpSaved: 'MCP config saved.',
|
mcpSaved: 'MCP config saved.',
|
||||||
mcpSaveFail: 'Failed to save MCP config.',
|
mcpSaveFail: 'Failed to save MCP config.',
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,11 @@ export const dashboardZhCn = {
|
||||||
mcpDraftRequired: '请先填写 MCP 服务名称和 URL。',
|
mcpDraftRequired: '请先填写 MCP 服务名称和 URL。',
|
||||||
mcpDraftAdded: '已加入 MCP 列表,记得保存配置。',
|
mcpDraftAdded: '已加入 MCP 列表,记得保存配置。',
|
||||||
addMcpServer: '新增 MCP Server',
|
addMcpServer: '新增 MCP Server',
|
||||||
|
topicMcpEnableTitle: '开通 topic_mcp',
|
||||||
|
topicMcpEnableConfirm: '当前 Bot 尚未配置 topic_mcp。是否立即开通?',
|
||||||
|
topicMcpEnableAction: '立即开通',
|
||||||
|
topicMcpEnabled: 'topic_mcp 已开通。',
|
||||||
|
topicMcpEnableFail: 'topic_mcp 开通失败。',
|
||||||
saveMcpConfig: '保存 MCP 配置',
|
saveMcpConfig: '保存 MCP 配置',
|
||||||
mcpSaved: 'MCP 配置已保存。',
|
mcpSaved: 'MCP 配置已保存。',
|
||||||
mcpSaveFail: 'MCP 配置保存失败。',
|
mcpSaveFail: 'MCP 配置保存失败。',
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,9 @@ interface MCPServerConfig {
|
||||||
interface MCPConfigResponse {
|
interface MCPConfigResponse {
|
||||||
bot_id: string;
|
bot_id: string;
|
||||||
mcp_servers?: Record<string, MCPServerConfig>;
|
mcp_servers?: Record<string, MCPServerConfig>;
|
||||||
|
locked_servers?: string[];
|
||||||
restart_required?: boolean;
|
restart_required?: boolean;
|
||||||
|
status?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MCPTestResponse {
|
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) => {
|
const openChannelModal = (botId: string) => {
|
||||||
if (!botId) return;
|
if (!botId) return;
|
||||||
setExpandedChannelByKey({});
|
setExpandedChannelByKey({});
|
||||||
|
|
@ -2376,12 +2420,24 @@ export function BotDashboardModule({
|
||||||
setShowTopicModal(true);
|
setShowTopicModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const openMcpModal = (botId: string) => {
|
const openMcpModal = async (botId: string) => {
|
||||||
if (!botId) return;
|
if (!botId) return;
|
||||||
setExpandedMcpByKey({});
|
setExpandedMcpByKey({});
|
||||||
setNewMcpPanelOpen(false);
|
setNewMcpPanelOpen(false);
|
||||||
resetNewMcpDraft();
|
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);
|
setShowMcpModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -2761,50 +2817,27 @@ export function BotDashboardModule({
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadBotMcpConfig = async (botId: string) => {
|
const loadBotMcpConfig = async (botId: string): Promise<MCPServerDraft[]> => {
|
||||||
if (!botId) return;
|
if (!botId) return [];
|
||||||
try {
|
try {
|
||||||
const res = await axios.get<MCPConfigResponse>(`${APP_ENDPOINTS.apiBase}/bots/${botId}/mcp-config`);
|
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 = mapMcpResponseToDrafts(res.data);
|
||||||
const drafts: MCPServerDraft[] = Object.entries(rows).map(([name, cfg]) => {
|
return applyMcpDrafts(drafts);
|
||||||
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({});
|
|
||||||
} catch {
|
} catch {
|
||||||
setMcpServers([]);
|
applyMcpDrafts([]);
|
||||||
setPersistedMcpServers([]);
|
return [];
|
||||||
setExpandedMcpByKey({});
|
}
|
||||||
setMcpTestByIndex({});
|
};
|
||||||
|
|
||||||
|
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);
|
return isTopicMcpServerRow(row);
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeMcpServer = (index: number) => {
|
const removeMcpServer = async (index: number) => {
|
||||||
const row = mcpServers[index];
|
const row = mcpServers[index];
|
||||||
if (!canRemoveMcpServer(row)) {
|
if (!canRemoveMcpServer(row)) {
|
||||||
notify(isZh ? '内置 MCP 服务不可删除。' : 'Built-in MCP server cannot be removed.', { tone: 'warning' });
|
notify(isZh ? '内置 MCP 服务不可删除。' : 'Built-in MCP server cannot be removed.', { tone: 'warning' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setMcpServers((prev) => prev.filter((_, i) => i !== index));
|
const nextRows = mcpServers.filter((_, i) => i !== index);
|
||||||
|
setMcpServers(nextRows);
|
||||||
|
setPersistedMcpServers(nextRows);
|
||||||
setExpandedMcpByKey((prev) => {
|
setExpandedMcpByKey((prev) => {
|
||||||
const next: Record<string, boolean> = {};
|
const next: Record<string, boolean> = {};
|
||||||
mcpServers
|
nextRows.forEach((server, idx) => {
|
||||||
.filter((_, i) => i !== index)
|
|
||||||
.forEach((server, idx) => {
|
|
||||||
const key = mcpDraftUiKey(server, idx);
|
const key = mcpDraftUiKey(server, idx);
|
||||||
next[key] = typeof prev[key] === 'boolean' ? prev[key] : idx === 0;
|
next[key] = typeof prev[key] === 'boolean' ? prev[key] : idx === 0;
|
||||||
});
|
});
|
||||||
|
|
@ -2892,6 +2925,7 @@ export function BotDashboardModule({
|
||||||
});
|
});
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
|
await saveBotMcpConfig(nextRows);
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildMcpHeaders = (row: MCPServerDraft): Record<string, string> => {
|
const buildMcpHeaders = (row: MCPServerDraft): Record<string, string> => {
|
||||||
|
|
@ -6118,7 +6152,7 @@ export function BotDashboardModule({
|
||||||
<strong>{row.name || `${t.mcpServer} #${idx + 1}`}</strong>
|
<strong>{row.name || `${t.mcpServer} #${idx + 1}`}</strong>
|
||||||
<div className="ops-config-collapsed-meta">{summary}</div>
|
<div className="ops-config-collapsed-meta">{summary}</div>
|
||||||
{row.locked ? (
|
{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}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="ops-config-card-actions">
|
<div className="ops-config-card-actions">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue