feat(ai-mcp): 增强完整命令拆分预览

This commit is contained in:
Syngnat
2026-06-08 22:04:24 +08:00
parent 7fa23e72c0
commit cc788d1b25
3 changed files with 143 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { describe, expect, it } from 'vitest';
import AIMCPCommandDraftPreview from './AIMCPCommandDraftPreview';
import { buildOverlayWorkbenchTheme } from '../../utils/overlayWorkbenchTheme';
describe('AIMCPCommandDraftPreview', () => {
it('renders parsed env keys, command, and args so users can verify the split result before applying', () => {
const markup = renderToStaticMarkup(
<AIMCPCommandDraftPreview
draft={{
command: 'python',
args: ['-m', 'your_mcp_server', '--stdio'],
env: {
OPENAI_API_KEY: '***',
HTTP_PROXY: 'http://127.0.0.1:7890',
},
}}
darkMode={false}
overlayTheme={buildOverlayWorkbenchTheme(false)}
cardBorder="rgba(0,0,0,0.08)"
/>,
);
expect(markup).toContain('自动拆分预览');
expect(markup).toContain('环境变量');
expect(markup).toContain('OPENAI_API_KEY');
expect(markup).toContain('HTTP_PROXY');
expect(markup).toContain('启动命令');
expect(markup).toContain('python');
expect(markup).toContain('命令参数');
expect(markup).toContain('your_mcp_server');
expect(markup).toContain('--stdio');
});
});

View File

@@ -0,0 +1,98 @@
import React from 'react';
import type { OverlayWorkbenchTheme } from '../../utils/overlayWorkbenchTheme';
import type { ParsedMCPCommandDraft } from '../../utils/mcpCommandDraft';
interface AIMCPCommandDraftPreviewProps {
draft: ParsedMCPCommandDraft;
darkMode: boolean;
overlayTheme: OverlayWorkbenchTheme;
cardBorder: string;
}
const chipStyle = (darkMode: boolean, overlayTheme: OverlayWorkbenchTheme): React.CSSProperties => ({
padding: '4px 8px',
borderRadius: 999,
fontSize: 12,
color: overlayTheme.titleText,
background: darkMode ? 'rgba(255,255,255,0.06)' : 'rgba(15,23,42,0.05)',
fontFamily: 'var(--gn-font-mono)',
});
const sectionTitleStyle = (overlayTheme: OverlayWorkbenchTheme): React.CSSProperties => ({
fontSize: 12,
fontWeight: 700,
color: overlayTheme.titleText,
});
const sectionHintStyle = (overlayTheme: OverlayWorkbenchTheme): React.CSSProperties => ({
fontSize: 11,
color: overlayTheme.mutedText,
lineHeight: 1.6,
});
const AIMCPCommandDraftPreview: React.FC<AIMCPCommandDraftPreviewProps> = ({
draft,
darkMode,
overlayTheme,
cardBorder,
}) => {
const envKeys = Object.keys(draft.env || {});
return (
<div
style={{
padding: '10px 12px',
borderRadius: 10,
border: `1px solid ${cardBorder}`,
background: darkMode ? 'rgba(255,255,255,0.03)' : 'rgba(255,255,255,0.72)',
display: 'flex',
flexDirection: 'column',
gap: 10,
}}
>
<div>
<div style={sectionTitleStyle(overlayTheme)}></div>
<div style={{ ...sectionHintStyle(overlayTheme), marginTop: 4 }}>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: 12 }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
<div style={sectionTitleStyle(overlayTheme)}></div>
<div style={sectionHintStyle(overlayTheme)}>
{envKeys.length > 0 ? `会写入 ${envKeys.length} 条环境变量。` : '这条命令里没有检测到前缀环境变量。'}
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
{envKeys.length > 0 ? envKeys.map((key) => (
<span key={key} style={chipStyle(darkMode, overlayTheme)}>{key}</span>
)) : <span style={chipStyle(darkMode, overlayTheme)}></span>}
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
<div style={sectionTitleStyle(overlayTheme)}></div>
<div style={sectionHintStyle(overlayTheme)}></div>
<code style={{ ...chipStyle(darkMode, overlayTheme), borderRadius: 10, display: 'inline-block' }}>
{draft.command}
</code>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
<div style={sectionTitleStyle(overlayTheme)}></div>
<div style={sectionHintStyle(overlayTheme)}>
{draft.args.length > 0 ? `会拆成 ${draft.args.length} 个独立参数标签。` : '这条命令里没有检测到额外参数。'}
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
{draft.args.length > 0 ? draft.args.map((arg) => (
<span key={arg} style={chipStyle(darkMode, overlayTheme)}>{arg}</span>
)) : <span style={chipStyle(darkMode, overlayTheme)}></span>}
</div>
</div>
</div>
</div>
);
};
export default AIMCPCommandDraftPreview;

View File

@@ -6,6 +6,7 @@ import type { OverlayWorkbenchTheme } from '../../utils/overlayWorkbenchTheme';
import type { AIMCPServerConfig, AIMCPToolDescriptor } from '../../types';
import { parseMCPCommandDraft } from '../../utils/mcpCommandDraft';
import { formatMCPEnvDraft, parseMCPEnvDraft } from '../../utils/mcpEnvDraft';
import AIMCPCommandDraftPreview from './AIMCPCommandDraftPreview';
interface AIMCPServerCardProps {
server: AIMCPServerConfig;
@@ -216,6 +217,14 @@ export const AIMCPServerCard: React.FC<AIMCPServerCardProps> = ({
</Button>
</div>
{parsedCommandDraft.ok && parsedCommandDraft.draft && rawCommandDraft.trim() && (
<AIMCPCommandDraftPreview
draft={parsedCommandDraft.draft}
darkMode={darkMode}
overlayTheme={overlayTheme}
cardBorder={cardBorder}
/>
)}
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0,1fr) 132px', gap: 12 }}>