mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-30 06:51:22 +08:00
🐛 fix(ai-settings): 修正MCP环境变量录入反馈
- 抽离环境变量草稿解析工具,区分有效项和无效行 - 保留用户原始输入,避免无效行被静默吞掉 - 在 MCP 服务卡片中显示识别数量与无效行提示 - 补充环境变量解析与卡片提示测试
This commit is contained in:
@@ -38,6 +38,7 @@ describe('AIMCPServerCard', () => {
|
||||
expect(markup).toContain('自动拆分到下方字段');
|
||||
expect(markup).toContain('每个参数单独录入一个标签');
|
||||
expect(markup).toContain('每行一个 KEY=VALUE');
|
||||
expect(markup).toContain('没有等号或 key 含空格的行不会保存');
|
||||
expect(markup).toContain('当前阶段只支持 stdio');
|
||||
expect(markup).toContain('实际启动命令预览');
|
||||
expect(markup).toContain('node server.js --stdio');
|
||||
|
||||
@@ -5,6 +5,7 @@ import { DeleteOutlined } from '@ant-design/icons';
|
||||
import type { OverlayWorkbenchTheme } from '../../utils/overlayWorkbenchTheme';
|
||||
import type { AIMCPServerConfig, AIMCPToolDescriptor } from '../../types';
|
||||
import { parseMCPCommandDraft } from '../../utils/mcpCommandDraft';
|
||||
import { formatMCPEnvDraft, parseMCPEnvDraft } from '../../utils/mcpEnvDraft';
|
||||
|
||||
interface AIMCPServerCardProps {
|
||||
server: AIMCPServerConfig;
|
||||
@@ -88,13 +89,20 @@ export const AIMCPServerCard: React.FC<AIMCPServerCardProps> = ({
|
||||
onDelete,
|
||||
}) => {
|
||||
const [rawCommandDraft, setRawCommandDraft] = React.useState('');
|
||||
const [envDraft, setEnvDraft] = React.useState(() => formatMCPEnvDraft(server.env));
|
||||
const launchPreview = formatLaunchPreview(server.command, server.args);
|
||||
const parsedCommandDraft = parseMCPCommandDraft(rawCommandDraft);
|
||||
const parsedEnvDraft = parseMCPEnvDraft(envDraft);
|
||||
|
||||
React.useEffect(() => {
|
||||
setEnvDraft(formatMCPEnvDraft(server.env));
|
||||
}, [server.id]);
|
||||
|
||||
const handleApplyCommandDraft = () => {
|
||||
if (!parsedCommandDraft.ok || !parsedCommandDraft.draft) {
|
||||
return;
|
||||
}
|
||||
setEnvDraft(formatMCPEnvDraft(parsedCommandDraft.draft.env));
|
||||
onChange({
|
||||
command: parsedCommandDraft.draft.command,
|
||||
args: parsedCommandDraft.draft.args,
|
||||
@@ -211,28 +219,22 @@ export const AIMCPServerCard: React.FC<AIMCPServerCardProps> = ({
|
||||
<MCPHelpBlock title="环境变量" description="每行一个 KEY=VALUE,通常用于 API Key、工作目录、服务地址等配置;不需要时可以留空。" overlayTheme={overlayTheme} example="OPENAI_API_KEY=...">
|
||||
<Input.TextArea
|
||||
rows={3}
|
||||
value={Object.entries(server.env || {}).map(([key, value]) => `${key}=${value}`).join('\n')}
|
||||
onChange={(event) => onChange({
|
||||
env: event.target.value
|
||||
.split(/\r?\n/u)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.reduce<Record<string, string>>((acc, line) => {
|
||||
const separatorIndex = line.indexOf('=');
|
||||
if (separatorIndex <= 0) {
|
||||
return acc;
|
||||
}
|
||||
const key = line.slice(0, separatorIndex).trim();
|
||||
if (!key) {
|
||||
return acc;
|
||||
}
|
||||
acc[key] = line.slice(separatorIndex + 1);
|
||||
return acc;
|
||||
}, {}),
|
||||
})}
|
||||
value={envDraft}
|
||||
onChange={(event) => {
|
||||
const nextValue = event.target.value;
|
||||
setEnvDraft(nextValue);
|
||||
onChange({ env: parseMCPEnvDraft(nextValue).env });
|
||||
}}
|
||||
placeholder={"环境变量,每行一个 KEY=VALUE,例如:\nOPENAI_API_KEY=...\nGITHUB_TOKEN=..."}
|
||||
style={{ borderRadius: 10, background: inputBg, border: `1px solid ${cardBorder}`, fontFamily: 'var(--gn-font-mono)' }}
|
||||
/>
|
||||
<div style={{ ...hintStyle(parsedEnvDraft.invalidLines.length > 0 ? '#d97706' : overlayTheme.mutedText) }}>
|
||||
{envDraft.trim()
|
||||
? parsedEnvDraft.invalidLines.length > 0
|
||||
? `已识别 ${parsedEnvDraft.validLines} 条环境变量,另有 ${parsedEnvDraft.invalidLines.length} 行格式无效,本次不会保存:${parsedEnvDraft.invalidLines.slice(0, 2).join(' / ')}`
|
||||
: `已识别 ${parsedEnvDraft.validLines} 条环境变量。`
|
||||
: '每行都要写成 KEY=VALUE;没有等号或 key 含空格的行不会保存。'}
|
||||
</div>
|
||||
</MCPHelpBlock>
|
||||
|
||||
{serverTools.length > 0 && (
|
||||
|
||||
31
frontend/src/utils/mcpEnvDraft.test.ts
Normal file
31
frontend/src/utils/mcpEnvDraft.test.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { formatMCPEnvDraft, parseMCPEnvDraft } from './mcpEnvDraft';
|
||||
|
||||
describe('mcpEnvDraft helpers', () => {
|
||||
it('formats env objects into editable KEY=VALUE lines', () => {
|
||||
expect(formatMCPEnvDraft({
|
||||
OPENAI_API_KEY: 'abc',
|
||||
BASE_URL: 'https://example.com',
|
||||
})).toBe('OPENAI_API_KEY=abc\nBASE_URL=https://example.com');
|
||||
});
|
||||
|
||||
it('parses valid env lines and preserves invalid ones for warning', () => {
|
||||
const result = parseMCPEnvDraft([
|
||||
'OPENAI_API_KEY=abc',
|
||||
'BAD LINE',
|
||||
'HAS SPACE =wrong',
|
||||
'EMPTY_VALUE=',
|
||||
'BASE_URL=https://example.com?a=1',
|
||||
].join('\n'));
|
||||
|
||||
expect(result.env).toEqual({
|
||||
OPENAI_API_KEY: 'abc',
|
||||
EMPTY_VALUE: '',
|
||||
BASE_URL: 'https://example.com?a=1',
|
||||
});
|
||||
expect(result.validLines).toBe(3);
|
||||
expect(result.invalidLines).toEqual(['BAD LINE', 'HAS SPACE =wrong']);
|
||||
expect(result.totalLines).toBe(5);
|
||||
});
|
||||
});
|
||||
47
frontend/src/utils/mcpEnvDraft.ts
Normal file
47
frontend/src/utils/mcpEnvDraft.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
export interface ParsedMCPEnvDraft {
|
||||
env: Record<string, string>;
|
||||
invalidLines: string[];
|
||||
totalLines: number;
|
||||
validLines: number;
|
||||
}
|
||||
|
||||
export const formatMCPEnvDraft = (env?: Record<string, string>): string =>
|
||||
Object.entries(env || {})
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join('\n');
|
||||
|
||||
export const parseMCPEnvDraft = (input: string): ParsedMCPEnvDraft => {
|
||||
const env: Record<string, string> = {};
|
||||
const invalidLines: string[] = [];
|
||||
let totalLines = 0;
|
||||
let validLines = 0;
|
||||
|
||||
String(input || '')
|
||||
.split(/\r?\n/u)
|
||||
.map((line) => line.trim())
|
||||
.forEach((line) => {
|
||||
if (!line) {
|
||||
return;
|
||||
}
|
||||
totalLines += 1;
|
||||
const separatorIndex = line.indexOf('=');
|
||||
if (separatorIndex <= 0) {
|
||||
invalidLines.push(line);
|
||||
return;
|
||||
}
|
||||
const key = line.slice(0, separatorIndex).trim();
|
||||
if (!key || /\s/u.test(key)) {
|
||||
invalidLines.push(line);
|
||||
return;
|
||||
}
|
||||
env[key] = line.slice(separatorIndex + 1);
|
||||
validLines += 1;
|
||||
});
|
||||
|
||||
return {
|
||||
env,
|
||||
invalidLines,
|
||||
totalLines,
|
||||
validLines,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user