diff --git a/frontend/src/components/ai/AIMCPClientInstallPanel.tsx b/frontend/src/components/ai/AIMCPClientInstallPanel.tsx index 2c23632..ed30401 100644 --- a/frontend/src/components/ai/AIMCPClientInstallPanel.tsx +++ b/frontend/src/components/ai/AIMCPClientInstallPanel.tsx @@ -5,6 +5,16 @@ import { CheckCircleFilled, CopyOutlined, ReloadOutlined } from '@ant-design/ico import type { AIMCPClientInstallStatus } from '../../types'; import type { MCPClientKey } from '../../utils/mcpClientInstallStatus'; import type { OverlayWorkbenchTheme } from '../../utils/overlayWorkbenchTheme'; +import { + getMCPClientDetectionSummary, + getMCPClientInstallStateLabel, + getMCPClientOptionSummary, + getMCPClientStatusSummary, + getMCPClientStatusTone, + getSelectedMCPClientStateLine, + resolveMCPClientCommandName, + resolveMCPClientInstallActionLabel, +} from './mcpClientInstallPanelState'; interface AIMCPClientInstallPanelProps { statuses: AIMCPClientInstallStatus[]; @@ -24,119 +34,6 @@ interface AIMCPClientInstallPanelProps { onInstall: () => void; } -const hasStatusIssue = (status: AIMCPClientInstallStatus | undefined) => - /失败|异常|错误/u.test(String(status?.message || '')); - -const getStatusTone = (status: AIMCPClientInstallStatus | undefined, darkMode: boolean) => { - if (status?.matchesCurrent) { - return { - label: '已接入', - color: '#16a34a', - bg: darkMode ? 'rgba(34,197,94,0.18)' : 'rgba(34,197,94,0.12)', - }; - } - if (status?.installed) { - return { - label: '需更新', - color: '#d97706', - bg: darkMode ? 'rgba(245,158,11,0.18)' : 'rgba(245,158,11,0.12)', - }; - } - if (hasStatusIssue(status)) { - return { - label: '状态异常', - color: '#dc2626', - bg: darkMode ? 'rgba(239,68,68,0.18)' : 'rgba(239,68,68,0.1)', - }; - } - return { - label: '未接入', - color: darkMode ? 'rgba(255,255,255,0.72)' : '#64748b', - bg: darkMode ? 'rgba(255,255,255,0.08)' : 'rgba(100,116,139,0.08)', - }; -}; - -const getInstallStateLabel = (status: AIMCPClientInstallStatus | undefined) => { - if (status?.matchesCurrent) { - return '外部工具接入状态:已接入当前 GoNavi'; - } - if (status?.installed) { - return '外部工具接入状态:已存在旧配置,需更新'; - } - if (hasStatusIssue(status)) { - return '外部工具接入状态:读取失败'; - } - return '外部工具接入状态:未接入'; -}; - -const resolveClientCommandName = (status: AIMCPClientInstallStatus | undefined) => { - const command = String(status?.clientCommand || '').trim(); - if (command) { - return command; - } - return status?.client === 'codex' ? 'codex' : 'claude'; -}; - -const getStatusSummary = (status: AIMCPClientInstallStatus | undefined) => { - const label = status?.displayName || '这个客户端'; - if (status?.matchesCurrent) { - return `${label} 已接入当前这份 GoNavi MCP,可直接在这个客户端里调用。`; - } - if (status?.installed) { - return `${label} 里已经有旧的 GoNavi 接入记录,更新后会切到当前这份 GoNavi。`; - } - if (hasStatusIssue(status)) { - return `${label} 的接入状态读取失败,建议先刷新检测。`; - } - return `当前还没有把这份 GoNavi MCP 接入 ${label}。`; -}; - -const getClientOptionSummary = (status: AIMCPClientInstallStatus | undefined) => { - if (status?.matchesCurrent) { - return '当前这份 GoNavi MCP 已接入到这个客户端。'; - } - if (status?.installed) { - return '检测到旧的 GoNavi 接入记录,建议更新为当前安装路径。'; - } - if (hasStatusIssue(status)) { - return '接入状态读取异常,建议先刷新再处理。'; - } - return '尚未把当前 GoNavi MCP 接入到这里。'; -}; - -const getClientDetectionSummary = (status: AIMCPClientInstallStatus | undefined) => { - const label = status?.displayName || '这个客户端'; - const commandName = resolveClientCommandName(status); - if (status?.clientDetected) { - return `已检测到本机 ${commandName} 命令,接入或更新后重启 ${label} 即可验证。`; - } - return `未检测到本机 ${commandName} 命令;如果 CLI 还没加入 PATH,也可以先写入 ${label} 的接入配置,稍后再重启验证。`; -}; - -const getSelectedClientStateLine = (status: AIMCPClientInstallStatus | undefined) => { - if (status?.matchesCurrent) { - return '已接入当前 GoNavi,无需重复操作'; - } - if (status?.installed) { - return '已存在旧接入记录,建议更新到当前 GoNavi 路径'; - } - if (hasStatusIssue(status)) { - return '状态读取异常,建议先刷新检测'; - } - return '当前还没有接入 GoNavi MCP'; -}; - -const resolveActionLabel = (status: AIMCPClientInstallStatus | undefined) => { - const label = status?.displayName || '目标客户端'; - if (status?.matchesCurrent) { - return `${label} 已接入,无需重复安装`; - } - if (status?.installed) { - return `更新 ${label} 接入配置`; - } - return `安装到 ${label}(外部工具)`; -}; - const AIMCPClientInstallPanel: React.FC = ({ statuses, selectedClient, @@ -250,7 +147,7 @@ const AIMCPClientInstallPanel: React.FC = ({ {statuses.map((status) => { const client = status.client === 'codex' ? 'codex' : 'claude-code'; const active = selectedClient === client; - const tone = getStatusTone(status, darkMode); + const tone = getMCPClientStatusTone(status, darkMode); return ( diff --git a/frontend/src/components/ai/mcpClientInstallPanelState.test.ts b/frontend/src/components/ai/mcpClientInstallPanelState.test.ts new file mode 100644 index 0000000..fd96d54 --- /dev/null +++ b/frontend/src/components/ai/mcpClientInstallPanelState.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from 'vitest'; + +import type { AIMCPClientInstallStatus } from '../../types'; +import { + getMCPClientDetectionSummary, + getMCPClientInstallStateLabel, + getMCPClientOptionSummary, + getMCPClientStatusSummary, + getMCPClientStatusTone, + getSelectedMCPClientStateLine, + resolveMCPClientCommandName, + resolveMCPClientInstallActionLabel, +} from './mcpClientInstallPanelState'; + +const buildStatus = (patch: Partial): AIMCPClientInstallStatus => ({ + client: 'claude-code', + displayName: 'Claude Code', + installed: false, + matchesCurrent: false, + clientDetected: false, + clientCommand: 'claude', + message: '未检测到 Claude Code 用户级 GoNavi MCP 配置', + ...patch, +}); + +describe('mcpClientInstallPanelState', () => { + it('marks a current client as already connected and prevents repeated install wording', () => { + const status = buildStatus({ + installed: true, + matchesCurrent: true, + clientDetected: true, + message: '已检测到 Claude Code 用户级 GoNavi MCP 配置,且与当前 GoNavi 安装路径一致', + }); + + expect(getMCPClientStatusTone(status, false).label).toBe('已接入'); + expect(getMCPClientInstallStateLabel(status)).toBe('外部工具接入状态:已接入当前 GoNavi'); + expect(getSelectedMCPClientStateLine(status)).toBe('已接入当前 GoNavi,无需重复操作'); + expect(resolveMCPClientInstallActionLabel(status)).toBe('Claude Code 已接入,无需重复安装'); + expect(getMCPClientStatusSummary(status)).toContain('可直接在这个客户端里调用'); + }); + + it('asks users to update stale external client records instead of reinstalling blindly', () => { + const status = buildStatus({ + client: 'codex', + displayName: 'Codex', + installed: true, + matchesCurrent: false, + clientDetected: true, + clientCommand: 'codex', + message: '已检测到 Codex 中的 GoNavi MCP 记录,但与当前 GoNavi 安装路径不一致,建议更新', + }); + + expect(getMCPClientStatusTone(status, false).label).toBe('需更新'); + expect(getMCPClientOptionSummary(status)).toContain('建议更新为当前安装路径'); + expect(getSelectedMCPClientStateLine(status)).toBe('已存在旧接入记录,建议更新到当前 GoNavi 路径'); + expect(resolveMCPClientInstallActionLabel(status)).toBe('更新 Codex 接入配置'); + }); + + it('explains that config can be written before the target CLI is detected in PATH', () => { + const status = buildStatus({ + clientDetected: false, + clientCommand: '', + }); + + expect(resolveMCPClientCommandName(status)).toBe('claude'); + expect(getMCPClientDetectionSummary(status)).toContain('CLI 还没加入 PATH'); + expect(resolveMCPClientInstallActionLabel(status)).toBe('安装到 Claude Code(外部工具)'); + }); +}); diff --git a/frontend/src/components/ai/mcpClientInstallPanelState.ts b/frontend/src/components/ai/mcpClientInstallPanelState.ts new file mode 100644 index 0000000..2b74e11 --- /dev/null +++ b/frontend/src/components/ai/mcpClientInstallPanelState.ts @@ -0,0 +1,123 @@ +import type { AIMCPClientInstallStatus } from '../../types'; + +export interface MCPClientInstallStatusTone { + label: string; + color: string; + bg: string; +} + +export const hasMCPClientStatusIssue = (status: AIMCPClientInstallStatus | undefined): boolean => + /失败|异常|错误/u.test(String(status?.message || '')); + +export const getMCPClientStatusTone = ( + status: AIMCPClientInstallStatus | undefined, + darkMode: boolean, +): MCPClientInstallStatusTone => { + if (status?.matchesCurrent) { + return { + label: '已接入', + color: '#16a34a', + bg: darkMode ? 'rgba(34,197,94,0.18)' : 'rgba(34,197,94,0.12)', + }; + } + if (status?.installed) { + return { + label: '需更新', + color: '#d97706', + bg: darkMode ? 'rgba(245,158,11,0.18)' : 'rgba(245,158,11,0.12)', + }; + } + if (hasMCPClientStatusIssue(status)) { + return { + label: '状态异常', + color: '#dc2626', + bg: darkMode ? 'rgba(239,68,68,0.18)' : 'rgba(239,68,68,0.1)', + }; + } + return { + label: '未接入', + color: darkMode ? 'rgba(255,255,255,0.72)' : '#64748b', + bg: darkMode ? 'rgba(255,255,255,0.08)' : 'rgba(100,116,139,0.08)', + }; +}; + +export const getMCPClientInstallStateLabel = (status: AIMCPClientInstallStatus | undefined): string => { + if (status?.matchesCurrent) { + return '外部工具接入状态:已接入当前 GoNavi'; + } + if (status?.installed) { + return '外部工具接入状态:已存在旧配置,需更新'; + } + if (hasMCPClientStatusIssue(status)) { + return '外部工具接入状态:读取失败'; + } + return '外部工具接入状态:未接入'; +}; + +export const resolveMCPClientCommandName = (status: AIMCPClientInstallStatus | undefined): string => { + const command = String(status?.clientCommand || '').trim(); + if (command) { + return command; + } + return status?.client === 'codex' ? 'codex' : 'claude'; +}; + +export const getMCPClientStatusSummary = (status: AIMCPClientInstallStatus | undefined): string => { + const label = status?.displayName || '这个客户端'; + if (status?.matchesCurrent) { + return `${label} 已接入当前这份 GoNavi MCP,可直接在这个客户端里调用。`; + } + if (status?.installed) { + return `${label} 里已经有旧的 GoNavi 接入记录,更新后会切到当前这份 GoNavi。`; + } + if (hasMCPClientStatusIssue(status)) { + return `${label} 的接入状态读取失败,建议先刷新检测。`; + } + return `当前还没有把这份 GoNavi MCP 接入 ${label}。`; +}; + +export const getMCPClientOptionSummary = (status: AIMCPClientInstallStatus | undefined): string => { + if (status?.matchesCurrent) { + return '当前这份 GoNavi MCP 已接入到这个客户端。'; + } + if (status?.installed) { + return '检测到旧的 GoNavi 接入记录,建议更新为当前安装路径。'; + } + if (hasMCPClientStatusIssue(status)) { + return '接入状态读取异常,建议先刷新再处理。'; + } + return '尚未把当前 GoNavi MCP 接入到这里。'; +}; + +export const getMCPClientDetectionSummary = (status: AIMCPClientInstallStatus | undefined): string => { + const label = status?.displayName || '这个客户端'; + const commandName = resolveMCPClientCommandName(status); + if (status?.clientDetected) { + return `已检测到本机 ${commandName} 命令,接入或更新后重启 ${label} 即可验证。`; + } + return `未检测到本机 ${commandName} 命令;如果 CLI 还没加入 PATH,也可以先写入 ${label} 的接入配置,稍后再重启验证。`; +}; + +export const getSelectedMCPClientStateLine = (status: AIMCPClientInstallStatus | undefined): string => { + if (status?.matchesCurrent) { + return '已接入当前 GoNavi,无需重复操作'; + } + if (status?.installed) { + return '已存在旧接入记录,建议更新到当前 GoNavi 路径'; + } + if (hasMCPClientStatusIssue(status)) { + return '状态读取异常,建议先刷新检测'; + } + return '当前还没有接入 GoNavi MCP'; +}; + +export const resolveMCPClientInstallActionLabel = (status: AIMCPClientInstallStatus | undefined): string => { + const label = status?.displayName || '目标客户端'; + if (status?.matchesCurrent) { + return `${label} 已接入,无需重复安装`; + } + if (status?.installed) { + return `更新 ${label} 接入配置`; + } + return `安装到 ${label}(外部工具)`; +};