mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-08 11:31:57 +08:00
✨ feat(mcp): 支持一键补齐缺失启动参数
- 在 MCP 参数提示中按当前 command 生成缺失必填参数 - 支持一键追加 docker、npx、python 等常见启动参数 - 增加组件交互测试覆盖参数补齐行为
This commit is contained in:
52
frontend/src/components/ai/AIMCPArgumentHints.test.tsx
Normal file
52
frontend/src/components/ai/AIMCPArgumentHints.test.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { act, create, type ReactTestRenderer } from 'react-test-renderer';
|
||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { buildOverlayWorkbenchTheme } from '../../utils/overlayWorkbenchTheme';
|
||||||
|
import AIMCPArgumentHints from './AIMCPArgumentHints';
|
||||||
|
|
||||||
|
const flattenRendererText = (node: any): string => {
|
||||||
|
if (node == null || typeof node === 'boolean') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (typeof node === 'string' || typeof node === 'number') {
|
||||||
|
return String(node);
|
||||||
|
}
|
||||||
|
if (Array.isArray(node)) {
|
||||||
|
return node.map((item) => flattenRendererText(item)).join('');
|
||||||
|
}
|
||||||
|
return flattenRendererText(node.children ?? node.props?.children);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('AIMCPArgumentHints', () => {
|
||||||
|
it('can append missing required args from command-specific hints', async () => {
|
||||||
|
const onArgsChange = vi.fn();
|
||||||
|
let renderer!: ReactTestRenderer;
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
renderer = create(
|
||||||
|
<AIMCPArgumentHints
|
||||||
|
command="docker"
|
||||||
|
args={['run', '--rm']}
|
||||||
|
onArgsChange={onArgsChange}
|
||||||
|
cardBorder="rgba(0,0,0,0.08)"
|
||||||
|
darkMode={false}
|
||||||
|
overlayTheme={buildOverlayWorkbenchTheme(false)}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const buttons = renderer.root.findAll(
|
||||||
|
(node) => node.type === 'button' && flattenRendererText(node).includes('一键补齐缺失必填参数'),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(buttons.length).toBe(1);
|
||||||
|
expect(flattenRendererText(buttons[0])).toContain('-i / mcp/server-fetch:latest');
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
buttons[0].props.onClick();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(onArgsChange).toHaveBeenCalledWith(['run', '--rm', '-i', 'mcp/server-fetch:latest']);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,19 +2,31 @@ import React from 'react';
|
|||||||
|
|
||||||
import type { OverlayWorkbenchTheme } from '../../utils/overlayWorkbenchTheme';
|
import type { OverlayWorkbenchTheme } from '../../utils/overlayWorkbenchTheme';
|
||||||
import { buildMCPArgumentHintProfile } from '../../utils/mcpArgumentHints';
|
import { buildMCPArgumentHintProfile } from '../../utils/mcpArgumentHints';
|
||||||
|
import { splitShellLikeCommand } from '../../utils/mcpCommandDraft';
|
||||||
import { buildMCPHintStyle, mcpLabelStyle } from './AIMCPHelpBlock';
|
import { buildMCPHintStyle, mcpLabelStyle } from './AIMCPHelpBlock';
|
||||||
|
|
||||||
interface AIMCPArgumentHintsProps {
|
interface AIMCPArgumentHintsProps {
|
||||||
command: string;
|
command: string;
|
||||||
args?: string[];
|
args?: string[];
|
||||||
|
onArgsChange?: (args: string[]) => void;
|
||||||
cardBorder: string;
|
cardBorder: string;
|
||||||
darkMode: boolean;
|
darkMode: boolean;
|
||||||
overlayTheme: OverlayWorkbenchTheme;
|
overlayTheme: OverlayWorkbenchTheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const buildMissingRequiredArgs = (
|
||||||
|
profile: NonNullable<ReturnType<typeof buildMCPArgumentHintProfile>>,
|
||||||
|
): string[] =>
|
||||||
|
profile.steps
|
||||||
|
.filter((step) => step.required && !step.satisfied)
|
||||||
|
.flatMap((step) => splitShellLikeCommand(step.example).tokens)
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
const AIMCPArgumentHints: React.FC<AIMCPArgumentHintsProps> = ({
|
const AIMCPArgumentHints: React.FC<AIMCPArgumentHintsProps> = ({
|
||||||
command,
|
command,
|
||||||
args,
|
args,
|
||||||
|
onArgsChange,
|
||||||
cardBorder,
|
cardBorder,
|
||||||
darkMode,
|
darkMode,
|
||||||
overlayTheme,
|
overlayTheme,
|
||||||
@@ -23,6 +35,8 @@ const AIMCPArgumentHints: React.FC<AIMCPArgumentHintsProps> = ({
|
|||||||
if (!profile) {
|
if (!profile) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
const missingRequiredArgs = buildMissingRequiredArgs(profile);
|
||||||
|
const canApplyMissingArgs = Boolean(onArgsChange && missingRequiredArgs.length > 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -74,6 +88,25 @@ const AIMCPArgumentHints: React.FC<AIMCPArgumentHintsProps> = ({
|
|||||||
必填参数看起来已经齐了,测试失败时再对照 README 检查业务参数和环境变量。
|
必填参数看起来已经齐了,测试失败时再对照 README 检查业务参数和环境变量。
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{canApplyMissingArgs ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onArgsChange?.([...(args || []), ...missingRequiredArgs])}
|
||||||
|
style={{
|
||||||
|
alignSelf: 'flex-start',
|
||||||
|
padding: '5px 11px',
|
||||||
|
borderRadius: 999,
|
||||||
|
border: `1px solid ${cardBorder}`,
|
||||||
|
background: darkMode ? 'rgba(245,158,11,0.14)' : 'rgba(245,158,11,0.10)',
|
||||||
|
color: '#b45309',
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 700,
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
一键补齐缺失必填参数:{missingRequiredArgs.join(' / ')}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ const AIMCPServerFormPanel: React.FC<AIMCPServerFormPanelProps> = ({
|
|||||||
<AIMCPArgumentHints
|
<AIMCPArgumentHints
|
||||||
command={server.command}
|
command={server.command}
|
||||||
args={server.args}
|
args={server.args}
|
||||||
|
onArgsChange={(args) => onChange({ args })}
|
||||||
cardBorder={cardBorder}
|
cardBorder={cardBorder}
|
||||||
darkMode={darkMode}
|
darkMode={darkMode}
|
||||||
overlayTheme={overlayTheme}
|
overlayTheme={overlayTheme}
|
||||||
|
|||||||
Reference in New Issue
Block a user