🐛 fix(ai-mcp): 优化外部客户端默认选中逻辑

- 抽离 MCP 客户端状态归一化与命令格式化工具
- 优先默认选中已接入但需要更新的外部客户端
- 补充状态选择与启动命令格式化测试
This commit is contained in:
Syngnat
2026-06-08 18:15:08 +08:00
parent a54a357e4b
commit 92e9b0ef75
3 changed files with 180 additions and 75 deletions

View File

@@ -15,6 +15,7 @@ import { resolveProviderSecretDraft } from '../utils/providerSecretDraft';
import { buildAddProviderEditorSession, buildClosedProviderEditorSession, buildEditProviderEditorSession, type ProviderEditorSession } from '../utils/aiProviderEditorState';
import type { OverlayWorkbenchTheme } from '../utils/overlayWorkbenchTheme';
import { BUILTIN_AI_TOOL_INFO } from '../utils/aiToolRegistry';
import { EMPTY_MCP_CLIENT_STATUSES, formatMCPLaunchCommand, normalizeMCPClientStatuses, pickPreferredMCPClient } from '../utils/mcpClientInstallStatus';
import AIBuiltinToolsCatalog from './ai/AIBuiltinToolsCatalog';
import AISettingsMCPSection, { type MCPClientKey } from './ai/AISettingsMCPSection';
import AISettingsSidebar, { type AISettingsSectionKey } from './ai/AISettingsSidebar';
@@ -106,62 +107,6 @@ const EMPTY_MCP_SERVER = (seed?: Partial<AIMCPServerConfig>): AIMCPServerConfig
};
};
const EMPTY_MCP_CLIENT_STATUSES: AIMCPClientInstallStatus[] = [
{
client: 'claude-code',
displayName: 'Claude Code',
installed: false,
matchesCurrent: false,
message: '未检测到 Claude Code 用户级 GoNavi MCP 配置',
},
{
client: 'codex',
displayName: 'Codex',
installed: false,
matchesCurrent: false,
message: '未检测到 Codex 用户级 GoNavi MCP 配置',
},
];
const normalizeMCPClientStatuses = (items?: AIMCPClientInstallStatus[]): AIMCPClientInstallStatus[] => {
const baseMap = new Map<string, AIMCPClientInstallStatus>(
EMPTY_MCP_CLIENT_STATUSES.map((item) => [item.client, { ...item }]),
);
(Array.isArray(items) ? items : []).forEach((item) => {
if (!item || !item.client) {
return;
}
const base = baseMap.get(item.client) || {
client: item.client,
displayName: item.client,
installed: false,
matchesCurrent: false,
message: '',
};
baseMap.set(item.client, {
...base,
...item,
displayName: item.displayName || base.displayName,
message: item.message || base.message,
args: Array.isArray(item.args) ? item.args : (base.args || []),
});
});
return (['claude-code', 'codex'] as MCPClientKey[])
.map((client) => baseMap.get(client))
.filter((item): item is AIMCPClientInstallStatus => Boolean(item));
};
const pickPreferredMCPClient = (items: AIMCPClientInstallStatus[], current?: MCPClientKey): MCPClientKey => {
if (current && items.some((item) => item.client === current)) {
return current;
}
const pending = items.find((item) => !item.matchesCurrent);
if (pending?.client === 'claude-code' || pending?.client === 'codex') {
return pending.client;
}
return 'claude-code';
};
const waitFor = (delayMs: number) => new Promise<void>((resolve) => {
window.setTimeout(resolve, delayMs);
});
@@ -181,25 +126,6 @@ const waitForAIService = async (attempts = 6, delayMs = 80) => {
return readAIService();
};
const quoteMCPCommandPart = (value: string): string => {
const text = String(value || '').trim();
if (!text) {
return '';
}
return /[\s"]/u.test(text) ? `"${text.replace(/"/g, '\\"')}"` : text;
};
const formatMCPLaunchCommand = (input?: Pick<AIMCPClientInstallStatus, 'command' | 'args'> | Pick<MCPClientInstallResult, 'command' | 'args'> | null): string => {
const command = String(input?.command || '').trim();
if (!command) {
return '';
}
const args = Array.isArray(input?.args)
? input.args.map((item) => String(item || '').trim()).filter(Boolean)
: [];
return [command, ...args].map(quoteMCPCommandPart).filter(Boolean).join(' ');
};
const EMPTY_SKILL = (): AISkillConfig => ({
id: `skill-draft-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
name: '',