├── src ├── index.css ├── store │ └── index.ts ├── hooks │ └── useI18n.ts ├── components │ ├── LanguageSwitcher.tsx │ ├── HistoryPanel.tsx │ ├── ConfigPanel.tsx │ ├── SecurityLogViewer.tsx │ ├── AuthConfig.tsx │ ├── MCPListPanel.tsx │ ├── LLMConfig.tsx │ └── SecurityHistoryPanel.tsx ├── main.tsx ├── App.tsx ├── services │ └── detectionEngine.ts ├── types │ └── mcp.ts └── utils │ └── storage.ts ├── images ├── index.png ├── explorer.png └── history.png ├── public ├── icons │ ├── icon128.png │ ├── icon16.png │ ├── icon32.png │ └── icon48.png ├── _locales │ ├── zh_CN │ │ └── messages.json │ └── en │ │ └── messages.json ├── background.js ├── index.html └── manifest.json ├── .cursor └── rules │ └── rule.mdc ├── .gitignore ├── .vscode └── settings.json ├── index.html ├── tsconfig.json ├── manifest.json ├── store-description.md ├── package.json ├── test_mcp_server ├── streamable.py ├── noauth.py ├── api_key.py └── combine_auth.py ├── publish-checklist.md ├── vite.config.ts ├── debug_passive_detection.js ├── privacy-policy.md ├── store-description-en.md ├── privacy-policy-en.md ├── docs └── privacy-policy.html └── README.md /src/index.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purpleroc/mcp-security-inspector/HEAD/images/index.png -------------------------------------------------------------------------------- /images/explorer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purpleroc/mcp-security-inspector/HEAD/images/explorer.png -------------------------------------------------------------------------------- /images/history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purpleroc/mcp-security-inspector/HEAD/images/history.png -------------------------------------------------------------------------------- /public/icons/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purpleroc/mcp-security-inspector/HEAD/public/icons/icon128.png -------------------------------------------------------------------------------- /public/icons/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purpleroc/mcp-security-inspector/HEAD/public/icons/icon16.png -------------------------------------------------------------------------------- /public/icons/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purpleroc/mcp-security-inspector/HEAD/public/icons/icon32.png -------------------------------------------------------------------------------- /public/icons/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purpleroc/mcp-security-inspector/HEAD/public/icons/icon48.png -------------------------------------------------------------------------------- /.cursor/rules/rule.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 安全内置规则 3 | globs: 4 | alwaysApply: true 5 | --- 6 | 7 | - 界面相关操作,均需要适配中英双语 8 | - 不需要额外编写测试、验证脚本 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist/* 2 | /node_modules/* 3 | /.vite/* 4 | .DS_Store 5 | .env 6 | .gitignore 7 | mcp-security-inspector-*.zip 8 | .python-version 9 | create_icons.py 10 | python_mcp 11 | /.codebuddy/* -------------------------------------------------------------------------------- /public/_locales/zh_CN/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extName": { 3 | "message": "MCP安全检测器", 4 | "description": "扩展名称" 5 | }, 6 | "extDescription": { 7 | "message": "Model Context Protocol安全性检测工具 - 测试MCP服务器工具、资源和提示的安全性", 8 | "description": "扩展描述" 9 | }, 10 | "actionTitle": { 11 | "message": "MCP安全检测器", 12 | "description": "扩展按钮标题" 13 | } 14 | } -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import mcpReducer from './mcpSlice'; 3 | 4 | export const store = configureStore({ 5 | reducer: { 6 | mcp: mcpReducer 7 | }, 8 | devTools: process.env.NODE_ENV === 'development' 9 | }); 10 | 11 | export type RootState = ReturnType; 12 | export type AppDispatch = typeof store.dispatch; -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.quickSuggestions": { 3 | "comments": "off", 4 | "strings": "off", 5 | "other": "off" 6 | }, 7 | "editor.suggestOnTriggerCharacters": false, 8 | "editor.acceptSuggestionOnEnter": "off", 9 | "editor.tabCompletion": "off", 10 | "typescript.suggest.enabled": false, 11 | "javascript.suggest.enabled": false 12 | } -------------------------------------------------------------------------------- /public/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extName": { 3 | "message": "MCP Security Inspector", 4 | "description": "Extension name" 5 | }, 6 | "extDescription": { 7 | "message": "Model Context Protocol Security Testing Tool - Test the security of MCP server tools, resources and prompts", 8 | "description": "Extension description" 9 | }, 10 | "actionTitle": { 11 | "message": "MCP Security Inspector", 12 | "description": "Extension action title" 13 | } 14 | } -------------------------------------------------------------------------------- /public/background.js: -------------------------------------------------------------------------------- 1 | // Background script for MCP Security Inspector 2 | console.log('MCP Security Inspector background script 开始加载'); 3 | 4 | // 当用户点击扩展图标时,打开新标签页 5 | chrome.action.onClicked.addListener((tab) => { 6 | chrome.tabs.create({ 7 | url: chrome.runtime.getURL('index.html') 8 | }); 9 | }); 10 | 11 | // 监听扩展安装事件 12 | chrome.runtime.onInstalled.addListener(() => { 13 | console.log('MCP Security Inspector 已安装'); 14 | }); 15 | 16 | console.log('MCP Security Inspector background script 加载完成'); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | MCP Security Inspector 7 | 20 | 21 | 22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | MCP Security Inspector 7 | 20 | 21 | 22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 5 | "module": "ESNext", 6 | "skipLibCheck": true, 7 | "allowJs": false, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "moduleResolution": "bundler", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | "baseUrl": ".", 18 | "paths": { 19 | "@/*": ["src/*"] 20 | }, 21 | "types": ["chrome", "vite/client"] 22 | }, 23 | "include": [ 24 | "src", 25 | "vite.config.ts", 26 | "public" 27 | ], 28 | "references": [] 29 | } -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "MCP Security Inspector", 4 | "version": "2.0.5", 5 | "description": "Model Security Inspector - 用于对MCP工具、资源、Prompt进行安全分析(MCP server tools, resources and prompts security analysis)", 6 | "action": { 7 | "default_title": "MCP Security Inspector" 8 | }, 9 | "background": { 10 | "service_worker": "background.js" 11 | }, 12 | "permissions": [ 13 | ], 14 | "host_permissions": [ 15 | "" 16 | ], 17 | "content_security_policy": { 18 | "extension_pages": "script-src 'self'; object-src 'self';" 19 | }, 20 | "icons": { 21 | "16": "icons/icon16.png", 22 | "32": "icons/icon32.png", 23 | "48": "icons/icon48.png", 24 | "128": "icons/icon128.png" 25 | }, 26 | "homepage_url": "https://github.com/purpleroc/mcp-security-inspector", 27 | "default_locale": "zh_CN" 28 | } -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "MCP Security Inspector", 4 | "version": "2.0.4", 5 | "description": "Model Security Inspector - 用于对MCP工具、资源、Prompt进行安全分析(MCP server tools, resources and prompts security analysis)", 6 | "action": { 7 | "default_title": "MCP Security Inspector" 8 | }, 9 | "background": { 10 | "service_worker": "background.js" 11 | }, 12 | "permissions": [ 13 | ], 14 | "host_permissions": [ 15 | "" 16 | ], 17 | "content_security_policy": { 18 | "extension_pages": "script-src 'self'; object-src 'self';" 19 | }, 20 | "icons": { 21 | "16": "icons/icon16.png", 22 | "32": "icons/icon32.png", 23 | "48": "icons/icon48.png", 24 | "128": "icons/icon128.png" 25 | }, 26 | "homepage_url": "https://github.com/purpleroc/mcp-security-inspector", 27 | "default_locale": "zh_CN" 28 | } -------------------------------------------------------------------------------- /src/hooks/useI18n.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { i18n, Language, TranslationKey } from '../i18n'; 3 | 4 | export const useI18n = () => { 5 | const [language, setLanguageState] = useState(() => i18n.getCurrentLanguage()); 6 | const [translations, setTranslations] = useState(() => i18n.t()); 7 | 8 | useEffect(() => { 9 | const handleLanguageChange = (newLanguage: Language) => { 10 | setLanguageState(newLanguage); 11 | setTranslations(i18n.t()); 12 | }; 13 | 14 | // 添加语言变化监听器 15 | i18n.addLanguageChangeListener(handleLanguageChange); 16 | 17 | // 清理函数 18 | return () => { 19 | i18n.removeLanguageChangeListener(handleLanguageChange); 20 | }; 21 | }, []); 22 | 23 | const changeLanguage = (newLanguage: Language) => { 24 | i18n.setLanguage(newLanguage); 25 | }; 26 | 27 | return { 28 | language, 29 | t: translations, 30 | changeLanguage, 31 | }; 32 | }; -------------------------------------------------------------------------------- /src/components/LanguageSwitcher.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Select, Space } from 'antd'; 3 | import { GlobalOutlined } from '@ant-design/icons'; 4 | import { useI18n } from '../hooks/useI18n'; 5 | import { LANGUAGE_OPTIONS, Language } from '../i18n'; 6 | 7 | interface LanguageSwitcherProps { 8 | size?: 'small' | 'middle' | 'large'; 9 | showIcon?: boolean; 10 | style?: React.CSSProperties; 11 | } 12 | 13 | const LanguageSwitcher: React.FC = ({ 14 | size = 'small', 15 | showIcon = true, 16 | style 17 | }) => { 18 | const { language, changeLanguage } = useI18n(); 19 | 20 | const handleLanguageChange = (value: Language) => { 21 | changeLanguage(value); 22 | }; 23 | 24 | return ( 25 | 26 | {showIcon && } 27 | 168 | 169 | 170 | 176 | 180 | 181 | 182 | 194 | 195 | 196 | 197 | prevValues.transport !== currentValues.transport} 200 | > 201 | {({ getFieldValue, setFieldsValue }) => { 202 | const transport = getFieldValue('transport'); 203 | 204 | // 当传输模式改变时,自动更新路径默认值(移除useEffect,直接在渲染时检查) 205 | const currentPath = getFieldValue('ssePath'); 206 | if (!currentPath || currentPath === '/sse' || currentPath === '/mcp') { 207 | const newPath = transport === 'sse' ? '/sse' : '/mcp'; 208 | if (currentPath !== newPath) { 209 | // 使用 setTimeout 避免在渲染过程中更新状态 210 | setTimeout(() => { 211 | setFieldsValue({ 212 | ssePath: newPath 213 | }); 214 | }, 0); 215 | } 216 | } 217 | 218 | return ( 219 | 225 | 226 | 227 | ); 228 | }} 229 | 230 | 231 | 232 | 236 | 237 | 238 | 239 | 245 | 246 | 247 | {connectionStatus !== 'connected' && ( 248 | 249 | 250 | 259 | 267 | 268 | 269 | )} 270 | 271 | 272 | {/* 连接成功后的状态和断开按钮 */} 273 | {connectionStatus === 'connected' && ( 274 |
275 |
276 |

{t.config.connectionStatus.connected}: {serverConfig?.name}

277 |

{t.config.serverHost}: {serverConfig?.host}

278 |

{t.config.transportMode}: { 279 | serverConfig?.transport === 'streamable' 280 | ? t.config.transportModes.streamable 281 | : t.config.transportModes.sse 282 | }

283 |

{t.config.authType}: { 284 | (() => { 285 | const authType = serverConfig?.auth?.type; 286 | if (!authType || authType === 'none') return t.auth.none; 287 | if (authType === 'combined') return t.auth.combined; 288 | return t.auth.none; 289 | })() 290 | }

291 | {serverInfo && ( 292 | <> 293 |

{t.config.protocolVersion}: {serverInfo.protocolVersion}

294 |

{t.config.serverVersion}: {serverInfo.serverInfo.name} v{serverInfo.serverInfo.version}

295 | 296 | )} 297 |
298 | 299 | 308 |
309 | )} 310 | 311 | 312 | ); 313 | }; 314 | 315 | export default ConfigPanel; -------------------------------------------------------------------------------- /src/components/SecurityLogViewer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import { 3 | Card, 4 | Tag, 5 | Typography, 6 | Space, 7 | Timeline, 8 | Badge, 9 | Button, 10 | Modal, 11 | Collapse, 12 | Alert, 13 | Switch, 14 | Tooltip 15 | } from 'antd'; 16 | import { 17 | ClockCircleOutlined, 18 | CheckCircleOutlined, 19 | ExclamationCircleOutlined, 20 | CloseCircleOutlined, 21 | InfoCircleOutlined, 22 | EyeOutlined, 23 | DownloadOutlined, 24 | ClearOutlined 25 | } from '@ant-design/icons'; 26 | import { useI18n } from '../hooks/useI18n'; 27 | 28 | const { Text, Paragraph } = Typography; 29 | const { Panel } = Collapse; 30 | 31 | export interface SecurityLogEntry { 32 | id: string; 33 | timestamp: number; 34 | type: 'info' | 'success' | 'warning' | 'error' | 'step'; 35 | phase: 'init' | 'tool_analysis' | 'prompt_analysis' | 'resource_analysis' | 'test_generation' | 'test_execution' | 'evaluation' | 'summary'; 36 | title: string; 37 | message: string; 38 | details?: any; 39 | progress?: number; 40 | duration?: number; 41 | metadata?: { 42 | toolName?: string; 43 | testCase?: string; 44 | riskLevel?: string; 45 | securityStatus?: string; 46 | }; 47 | } 48 | 49 | interface SecurityLogViewerProps { 50 | logs: SecurityLogEntry[]; 51 | isScanning: boolean; 52 | onClearLogs: () => void; 53 | onExportLogs: () => void; 54 | } 55 | 56 | const SecurityLogViewer: React.FC = ({ 57 | logs, 58 | isScanning, 59 | onClearLogs, 60 | onExportLogs 61 | }) => { 62 | const { t } = useI18n(); 63 | const [autoScroll, setAutoScroll] = useState(true); 64 | const [selectedLog, setSelectedLog] = useState(null); 65 | const [showDetails, setShowDetails] = useState(false); 66 | const logContainerRef = useRef(null); 67 | 68 | // 自动滚动到最新日志 69 | useEffect(() => { 70 | if (autoScroll && logContainerRef.current) { 71 | logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight; 72 | } 73 | }, [logs, autoScroll]); 74 | 75 | const getLogIcon = (type: SecurityLogEntry['type']) => { 76 | switch (type) { 77 | case 'success': 78 | return ; 79 | case 'warning': 80 | return ; 81 | case 'error': 82 | return ; 83 | case 'step': 84 | return ; 85 | default: 86 | return ; 87 | } 88 | }; 89 | 90 | const getLogColor = (type: SecurityLogEntry['type']) => { 91 | switch (type) { 92 | case 'success': return '#f6ffed'; 93 | case 'warning': return '#fffbe6'; 94 | case 'error': return '#fff2f0'; 95 | case 'step': return '#f0f9ff'; 96 | default: return '#fafafa'; 97 | } 98 | }; 99 | 100 | const getPhaseText = (phase: SecurityLogEntry['phase']) => { 101 | return t.security.phases[phase] || phase; 102 | }; 103 | 104 | const getRiskLevelTag = (level?: string) => { 105 | if (!level) return null; 106 | 107 | const colors = { 108 | low: 'green', 109 | medium: 'orange', 110 | high: 'red', 111 | critical: 'magenta' 112 | }; 113 | 114 | return ( 115 | 116 | {level.toUpperCase()} 117 | 118 | ); 119 | }; 120 | 121 | const getSecurityStatusTag = (status?: string) => { 122 | if (!status) return null; 123 | 124 | const configs = { 125 | SAFE: { color: 'green', text: t.security.securityStatuses.SAFE }, 126 | WARNING: { color: 'orange', text: t.security.securityStatuses.WARNING }, 127 | VULNERABLE: { color: 'red', text: t.security.securityStatuses.VULNERABLE }, 128 | CRITICAL: { color: 'magenta', text: t.security.securityStatuses.CRITICAL } 129 | }; 130 | 131 | const config = configs[status as keyof typeof configs]; 132 | if (!config) return {status}; 133 | 134 | return {config.text}; 135 | }; 136 | 137 | const formatDuration = (duration?: number) => { 138 | if (!duration) return ''; 139 | return duration < 1000 ? `${duration}ms` : `${(duration / 1000).toFixed(2)}s`; 140 | }; 141 | 142 | const showLogDetails = (log: SecurityLogEntry) => { 143 | setSelectedLog(log); 144 | setShowDetails(true); 145 | }; 146 | 147 | return ( 148 | 151 | {t.security.detectionLogs} 152 | 156 | {isScanning && ( 157 | 158 | )} 159 |
160 | } 161 | extra={ 162 | 163 | 164 | 171 | 172 | 180 | 188 | 189 | } 190 | size="small" 191 | > 192 |
202 | {logs.length === 0 ? ( 203 |
204 | {t.security.noLogsRecord} 205 |
206 | ) : ( 207 | 208 | {logs.map((log) => ( 209 | 214 |
215 | 216 | {log.title} 217 | {getPhaseText(log.phase)} 218 | {log.metadata?.riskLevel && getRiskLevelTag(log.metadata.riskLevel)} 219 | {log.metadata?.securityStatus && getSecurityStatusTag(log.metadata.securityStatus)} 220 | {log.duration && ( 221 | 222 | {formatDuration(log.duration)} 223 | 224 | )} 225 | 226 | 227 |
228 | {log.message} 229 | {/* 统一为所有日志添加详情按钮,即使没有额外details */} 230 | 239 |
240 | 241 | {log.metadata?.toolName && ( 242 | 243 | {t.security.toolLabel}: {log.metadata.toolName} 244 | 245 | )} 246 | 247 | {log.progress !== undefined && ( 248 |
249 |
257 |
265 |
266 |
267 | )} 268 | 269 | 270 | {new Date(log.timestamp).toLocaleTimeString()} 271 | 272 |
273 | 274 | ))} 275 | 276 | )} 277 |
278 | 279 | {/* 详情模态框 */} 280 | setShowDetails(false)} 284 | width={800} 285 | footer={[ 286 | 289 | ]} 290 | > 291 | {selectedLog && ( 292 |
293 | 294 |
295 | {t.security.phase}: 296 | {getPhaseText(selectedLog.phase)} 297 |
298 | 299 |
300 | {t.security.time}: 301 | {new Date(selectedLog.timestamp).toLocaleString()} 302 |
303 | 304 | {selectedLog.duration && ( 305 |
306 | {t.security.duration}: 307 | {formatDuration(selectedLog.duration)} 308 |
309 | )} 310 | 311 |
312 | {t.security.message}: 313 | {selectedLog.message} 314 |
315 | 316 | {selectedLog.metadata && Object.keys(selectedLog.metadata).length > 0 && ( 317 |
318 | {t.security.metadata}: 319 | 320 | 321 |
322 |                         {JSON.stringify(selectedLog.metadata, null, 2)}
323 |                       
324 |
325 |
326 |
327 | )} 328 | 329 | {selectedLog.details && ( 330 |
331 | {t.security.detailData}: 332 | 333 | 334 |
335 |                         {typeof selectedLog.details === 'string' 
336 |                           ? selectedLog.details 
337 |                           : JSON.stringify(selectedLog.details, null, 2)
338 |                         }
339 |                       
340 |
341 |
342 |
343 | )} 344 |
345 |
346 | )} 347 |
348 | 349 | ); 350 | }; 351 | 352 | export default SecurityLogViewer; -------------------------------------------------------------------------------- /src/services/detectionEngine.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DetectionRule, 3 | DetectionRuleMatch, 4 | DetectionScope, 5 | RuleValidationResult 6 | } from '../types/mcp'; 7 | import { BuiltinDetectionRules } from './detectionRules'; 8 | import { detectionRuleStorage } from '../utils/storage'; 9 | 10 | /** 11 | * 检测引擎 - 负责执行检测规则 12 | */ 13 | export class DetectionEngine { 14 | private static instance: DetectionEngine; 15 | private rules: DetectionRule[] = []; 16 | private compiledRules: Map = new Map(); 17 | 18 | private constructor() { 19 | this.loadRules(); 20 | } 21 | 22 | static getInstance(): DetectionEngine { 23 | if (!DetectionEngine.instance) { 24 | DetectionEngine.instance = new DetectionEngine(); 25 | } 26 | return DetectionEngine.instance; 27 | } 28 | 29 | /** 30 | * 加载所有规则(内置规则 + 自定义规则) 31 | */ 32 | private loadRules(): void { 33 | // 加载内置规则 34 | const builtinRules = BuiltinDetectionRules.getAllBuiltinRules(); 35 | 36 | // 加载保存的自定义规则 37 | const savedCustomRules = detectionRuleStorage.getDetectionRules(); 38 | 39 | // 合并规则,确保内置规则不被覆盖 40 | const allRules = [...builtinRules]; 41 | 42 | // 添加自定义规则,但要检查是否与内置规则冲突 43 | for (const customRule of savedCustomRules) { 44 | const existingBuiltin = builtinRules.find(r => r.id === customRule.id); 45 | if (!existingBuiltin) { 46 | // 确保自定义规则有正确的标识 47 | customRule.isBuiltin = false; 48 | allRules.push(customRule); 49 | } 50 | } 51 | 52 | this.rules = allRules; 53 | this.compileRules(); 54 | } 55 | 56 | /** 57 | * 保存自定义规则到本地存储 58 | */ 59 | private saveCustomRules(): void { 60 | const customRules = this.rules.filter(rule => !rule.isBuiltin); 61 | detectionRuleStorage.saveDetectionRules(customRules); 62 | } 63 | 64 | /** 65 | * 编译正则表达式规则 66 | */ 67 | private compileRules(): void { 68 | this.compiledRules.clear(); 69 | 70 | for (const rule of this.rules) { 71 | if (rule.enabled) { 72 | try { 73 | const regex = new RegExp(rule.pattern, rule.flags || 'gi'); 74 | this.compiledRules.set(rule.id, regex); 75 | } catch (error) { 76 | console.error(`规则 ${rule.id} 编译失败:`, error); 77 | } 78 | } 79 | } 80 | } 81 | 82 | /** 83 | * 执行检测 84 | */ 85 | async detectThreats( 86 | parameters: Record, 87 | output: any, 88 | scope?: DetectionScope 89 | ): Promise { 90 | const matches: DetectionRuleMatch[] = []; 91 | 92 | // 准备检测文本 93 | const parameterText = JSON.stringify(parameters); 94 | const outputText = JSON.stringify(output); 95 | 96 | for (const rule of this.rules) { 97 | if (!rule.enabled || !this.compiledRules.has(rule.id)) { 98 | continue; 99 | } 100 | 101 | // 检查作用域 102 | if (scope && rule.scope !== scope && rule.scope !== 'both') { 103 | continue; 104 | } 105 | 106 | const regex = this.compiledRules.get(rule.id)!; 107 | const ruleMatches: DetectionRuleMatch = { 108 | rule, 109 | matches: [], 110 | severity: rule.riskLevel 111 | }; 112 | 113 | // 根据规则范围执行检测 114 | const textsToCheck: Array<{text: string, type: string}> = []; 115 | 116 | if (rule.scope === 'parameters' || rule.scope === 'both') { 117 | textsToCheck.push({text: parameterText, type: 'parameters'}); 118 | } 119 | 120 | if (rule.scope === 'output' || rule.scope === 'both') { 121 | textsToCheck.push({text: outputText, type: 'output'}); 122 | } 123 | 124 | // 执行正则匹配 125 | for (const {text, type} of textsToCheck) { 126 | regex.lastIndex = 0; // 重置正则位置 127 | let match; 128 | let matchCount = 0; 129 | 130 | while ( 131 | (match = regex.exec(text)) !== null && 132 | matchCount < (rule.maxMatches || 10) 133 | ) { 134 | const fullMatch = match[0]; 135 | const capturedGroups = match.slice(1); 136 | const startIndex = match.index; 137 | const endIndex = match.index + fullMatch.length; 138 | 139 | // 获取上下文(前后50个字符) 140 | const contextStart = Math.max(0, startIndex - 50); 141 | const contextEnd = Math.min(text.length, endIndex + 50); 142 | const context = text.substring(contextStart, contextEnd); 143 | 144 | ruleMatches.matches.push({ 145 | fullMatch, 146 | capturedGroups: capturedGroups.length > 0 ? capturedGroups : undefined, 147 | startIndex, 148 | endIndex, 149 | context: `...${context}...` 150 | }); 151 | 152 | matchCount++; 153 | 154 | // 如果不是全局匹配,跳出循环 155 | if (!rule.flags?.includes('g')) { 156 | break; 157 | } 158 | } 159 | } 160 | 161 | // 如果有匹配结果,添加到结果列表 162 | if (ruleMatches.matches.length > 0) { 163 | // 处理敏感数据遮蔽 164 | if (rule.maskSensitiveData) { 165 | ruleMatches.maskedContent = this.maskSensitiveContent( 166 | ruleMatches.matches, 167 | rule 168 | ); 169 | } 170 | 171 | matches.push(ruleMatches); 172 | } 173 | } 174 | 175 | return matches; 176 | } 177 | 178 | /** 179 | * 遮蔽敏感内容 180 | */ 181 | private maskSensitiveContent( 182 | matches: DetectionRuleMatch['matches'], 183 | rule: DetectionRule 184 | ): string { 185 | const maskedItems: string[] = []; 186 | 187 | for (const match of matches) { 188 | if (match.capturedGroups && match.capturedGroups.length > 0) { 189 | // 遮蔽捕获组内容 190 | for (const captured of match.capturedGroups) { 191 | const masked = this.maskString(captured); 192 | maskedItems.push(`${rule.threatType}: ${masked}`); 193 | } 194 | } else { 195 | // 遮蔽完整匹配内容 196 | const masked = this.maskString(match.fullMatch); 197 | maskedItems.push(`${rule.threatType}: ${masked}`); 198 | } 199 | } 200 | 201 | return maskedItems.join(', '); 202 | } 203 | 204 | /** 205 | * 字符串遮蔽处理 206 | */ 207 | private maskString(str: string): string { 208 | if (str.length <= 4) { 209 | return '*'.repeat(str.length); 210 | } 211 | 212 | return str.substring(0, 2) + '*'.repeat(str.length - 4) + str.substring(str.length - 2); 213 | } 214 | 215 | /** 216 | * 验证规则正确性 217 | */ 218 | validateRule(rule: Partial): RuleValidationResult { 219 | const errors: string[] = []; 220 | const warnings: string[] = []; 221 | 222 | // 基本字段验证 223 | if (!rule.name?.trim()) { 224 | errors.push('规则名称不能为空'); 225 | } 226 | 227 | if (!rule.pattern?.trim()) { 228 | errors.push('正则表达式模式不能为空'); 229 | } 230 | 231 | if (!rule.threatType?.trim()) { 232 | errors.push('威胁类型不能为空'); 233 | } 234 | 235 | // 正则表达式验证 236 | if (rule.pattern) { 237 | try { 238 | const regex = new RegExp(rule.pattern, rule.flags || 'gi'); 239 | 240 | // 测试一些常见输入 241 | const testInputs = [ 242 | 'test string', 243 | 'password=secret123', 244 | 'api_key=abcd1234567890', 245 | 'rm -rf /', 246 | '', 247 | "'; DROP TABLE users; --" 248 | ]; 249 | 250 | const testResults: RuleValidationResult['testResults'] = []; 251 | 252 | for (const input of testInputs) { 253 | regex.lastIndex = 0; 254 | const matches = regex.test(input); 255 | const captured = matches ? regex.exec(input)?.slice(1) : undefined; 256 | 257 | testResults.push({ 258 | input, 259 | matches, 260 | captured 261 | }); 262 | } 263 | 264 | // 如果正则表达式匹配所有输入,给出警告 265 | if (testResults.every(result => result.matches)) { 266 | warnings.push('正则表达式可能过于宽泛,匹配了所有测试输入'); 267 | } 268 | 269 | return { 270 | valid: errors.length === 0, 271 | errors, 272 | warnings, 273 | testResults 274 | }; 275 | 276 | } catch (error) { 277 | errors.push(`正则表达式语法错误: ${(error as Error).message}`); 278 | } 279 | } 280 | 281 | return { 282 | valid: errors.length === 0, 283 | errors, 284 | warnings 285 | }; 286 | } 287 | 288 | /** 289 | * 添加自定义规则 290 | */ 291 | addCustomRule(rule: DetectionRule): void { 292 | const validation = this.validateRule(rule); 293 | if (!validation.valid) { 294 | throw new Error(`规则验证失败: ${validation.errors.join(', ')}`); 295 | } 296 | 297 | // 移除同ID的现有规则 298 | this.rules = this.rules.filter(r => r.id !== rule.id); 299 | 300 | // 添加新规则 301 | this.rules.push(rule); 302 | 303 | // 重新编译规则 304 | this.compileRules(); 305 | 306 | // 保存自定义规则到本地存储 307 | this.saveCustomRules(); 308 | } 309 | 310 | /** 311 | * 更新规则 312 | */ 313 | updateRule(rule: DetectionRule): void { 314 | const validation = this.validateRule(rule); 315 | if (!validation.valid) { 316 | throw new Error(`规则验证失败: ${validation.errors.join(', ')}`); 317 | } 318 | 319 | const index = this.rules.findIndex(r => r.id === rule.id); 320 | if (index === -1) { 321 | throw new Error(`规则 ${rule.id} 不存在`); 322 | } 323 | 324 | this.rules[index] = rule; 325 | this.compileRules(); 326 | 327 | // 保存自定义规则到本地存储 328 | this.saveCustomRules(); 329 | } 330 | 331 | /** 332 | * 删除规则 333 | */ 334 | removeRule(ruleId: string): void { 335 | // 不能删除内置规则 336 | const rule = this.rules.find(r => r.id === ruleId); 337 | if (rule?.isBuiltin) { 338 | throw new Error('不能删除内置规则'); 339 | } 340 | 341 | this.rules = this.rules.filter(r => r.id !== ruleId); 342 | this.compiledRules.delete(ruleId); 343 | 344 | // 保存自定义规则到本地存储 345 | this.saveCustomRules(); 346 | } 347 | 348 | /** 349 | * 启用/禁用规则 350 | */ 351 | toggleRule(ruleId: string, enabled: boolean): void { 352 | const rule = this.rules.find(r => r.id === ruleId); 353 | if (!rule) { 354 | throw new Error(`规则 ${ruleId} 不存在`); 355 | } 356 | 357 | rule.enabled = enabled; 358 | rule.updatedAt = Date.now(); 359 | 360 | if (enabled) { 361 | try { 362 | const regex = new RegExp(rule.pattern, rule.flags || 'gi'); 363 | this.compiledRules.set(rule.id, regex); 364 | } catch (error) { 365 | console.error(`规则 ${rule.id} 编译失败:`, error); 366 | } 367 | } else { 368 | this.compiledRules.delete(rule.id); 369 | } 370 | 371 | // 如果是自定义规则,保存到本地存储 372 | if (!rule.isBuiltin) { 373 | this.saveCustomRules(); 374 | } 375 | } 376 | 377 | /** 378 | * 获取所有规则 379 | */ 380 | getAllRules(): DetectionRule[] { 381 | return [...this.rules]; 382 | } 383 | 384 | /** 385 | * 根据分类获取规则 386 | */ 387 | getRulesByCategory(category: string): DetectionRule[] { 388 | return this.rules.filter(rule => rule.category === category); 389 | } 390 | 391 | /** 392 | * 获取启用的规则 393 | */ 394 | getEnabledRules(): DetectionRule[] { 395 | return this.rules.filter(rule => rule.enabled); 396 | } 397 | 398 | /** 399 | * 搜索规则 400 | */ 401 | searchRules(query: string): DetectionRule[] { 402 | const lowercaseQuery = query.toLowerCase(); 403 | 404 | return this.rules.filter(rule => 405 | rule.name.toLowerCase().includes(lowercaseQuery) || 406 | rule.description.toLowerCase().includes(lowercaseQuery) || 407 | rule.threatType.toLowerCase().includes(lowercaseQuery) || 408 | rule.tags?.some(tag => tag.toLowerCase().includes(lowercaseQuery)) 409 | ); 410 | } 411 | 412 | /** 413 | * 导出规则配置 414 | */ 415 | exportRules(): string { 416 | const customRules = this.rules.filter(rule => !rule.isBuiltin); 417 | return JSON.stringify(customRules, null, 2); 418 | } 419 | 420 | /** 421 | * 导入规则配置 422 | */ 423 | importRules(rulesJson: string): void { 424 | try { 425 | const importedRules: DetectionRule[] = JSON.parse(rulesJson); 426 | 427 | for (const rule of importedRules) { 428 | // 验证规则 429 | const validation = this.validateRule(rule); 430 | if (!validation.valid) { 431 | throw new Error(`规则 ${rule.name} 验证失败: ${validation.errors.join(', ')}`); 432 | } 433 | 434 | // 确保不是内置规则 435 | rule.isBuiltin = false; 436 | rule.updatedAt = Date.now(); 437 | 438 | // 添加规则 439 | this.addCustomRule(rule); 440 | } 441 | 442 | } catch (error) { 443 | throw new Error(`导入规则失败: ${(error as Error).message}`); 444 | } 445 | } 446 | 447 | /** 448 | * 重置为默认规则 449 | */ 450 | resetToDefaults(): void { 451 | // 清除保存的自定义规则 452 | detectionRuleStorage.clearDetectionRules(); 453 | 454 | // 重新加载规则(只包含内置规则) 455 | this.loadRules(); 456 | } 457 | } -------------------------------------------------------------------------------- /src/components/AuthConfig.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Form, Input, Switch, Card, Space, Button } from 'antd'; 3 | import { PlusOutlined, MinusCircleOutlined } from '@ant-design/icons'; 4 | import { AuthConfig } from '../types/mcp'; 5 | import { useI18n } from '../hooks/useI18n'; 6 | 7 | interface AuthConfigProps { 8 | value?: AuthConfig; 9 | onChange?: (value: AuthConfig) => void; 10 | } 11 | 12 | // 验证字符串是否只包含 ISO-8859-1 字符(HTTP 请求头要求) 13 | const isValidHeaderString = (str: string): boolean => { 14 | if (!str) return true; 15 | for (let i = 0; i < str.length; i++) { 16 | const charCode = str.charCodeAt(i); 17 | // ISO-8859-1 字符范围: 0-255 18 | if (charCode > 255) { 19 | return false; 20 | } 21 | } 22 | return true; 23 | }; 24 | 25 | const AuthConfigComponent: React.FC = ({ value, onChange }) => { 26 | const { t } = useI18n(); 27 | const authEnabled = value?.type === 'combined'; 28 | 29 | const handleAuthToggle = (enabled: boolean) => { 30 | if (enabled) { 31 | onChange?.({ 32 | type: 'combined' 33 | }); 34 | } else { 35 | onChange?.({ type: 'none' }); 36 | } 37 | }; 38 | 39 | const handleCombinedChange = (section: 'apiKey' | 'urlParams' | 'customHeaders' | 'basicAuth', data: any) => { 40 | if (value?.type === 'combined') { 41 | onChange?.({ 42 | ...value, 43 | [section]: data 44 | }); 45 | } 46 | }; 47 | 48 | return ( 49 | 50 | 51 | 55 | 56 | {authEnabled ? t.auth.enableAuthMode : t.auth.none} 57 | 58 | 59 | 60 | {authEnabled && ( 61 |
62 |
63 |
64 | {t.auth.combinedMode} 65 |
66 |
67 | {t.auth.combinedModeDesc} 68 |
69 |
70 | 71 | {/* API Key 配置 */} 72 | 73 | { 76 | if (checked) { 77 | handleCombinedChange('apiKey', { 78 | apiKey: '', 79 | headerName: 'Authorization', 80 | prefix: 'Bearer ' 81 | }); 82 | } else { 83 | handleCombinedChange('apiKey', undefined); 84 | } 85 | }} 86 | /> 87 | 88 | {t.auth.enableApiKey} 89 | 90 | 91 | 92 | {value?.type === 'combined' && value.apiKey && ( 93 |
94 | 95 | handleCombinedChange('apiKey', { ...value.apiKey, apiKey: e.target.value })} 99 | /> 100 | 101 | 102 | handleCombinedChange('apiKey', { ...value.apiKey, headerName: e.target.value })} 106 | /> 107 | 108 | 109 | handleCombinedChange('apiKey', { ...value.apiKey, prefix: e.target.value })} 113 | /> 114 | 115 |
116 | )} 117 | 118 | {/* URL 参数配置 */} 119 | 120 | { 123 | if (checked) { 124 | handleCombinedChange('urlParams', [{ name: '', value: '' }]); 125 | } else { 126 | handleCombinedChange('urlParams', undefined); 127 | } 128 | }} 129 | /> 130 | 131 | {t.auth.enableUrlParams} 132 | 133 | 134 | 135 | {value?.type === 'combined' && value.urlParams !== undefined && ( 136 |
137 | {value.urlParams.map((param, index) => ( 138 | 139 | { 144 | const params = [...value.urlParams!]; 145 | params[index] = { ...params[index], name: e.target.value }; 146 | handleCombinedChange('urlParams', params); 147 | }} 148 | /> 149 | { 154 | const params = [...value.urlParams!]; 155 | params[index] = { ...params[index], value: e.target.value }; 156 | handleCombinedChange('urlParams', params); 157 | }} 158 | /> 159 | { 160 | const params = [...value.urlParams!]; 161 | params.splice(index, 1); 162 | handleCombinedChange('urlParams', params.length > 0 ? params : [{ name: '', value: '' }]); 163 | }} /> 164 | 165 | ))} 166 | 179 |
180 | )} 181 | 182 | {/* Basic Auth 配置 */} 183 | 184 | { 187 | if (checked) { 188 | handleCombinedChange('basicAuth', { username: '', password: '' }); 189 | } else { 190 | handleCombinedChange('basicAuth', undefined); 191 | } 192 | }} 193 | /> 194 | 195 | {t.auth.basic} 196 | 197 | 198 | 199 | {value?.type === 'combined' && value.basicAuth && ( 200 |
201 | 202 | handleCombinedChange('basicAuth', { ...value.basicAuth, username: e.target.value })} 206 | /> 207 | 208 | 209 | handleCombinedChange('basicAuth', { ...value.basicAuth, password: e.target.value })} 213 | /> 214 | 215 |
216 | )} 217 | 218 | {/* 自定义请求头配置 */} 219 | 220 | { 223 | if (checked) { 224 | handleCombinedChange('customHeaders', [{ name: '', value: '' }]); 225 | } else { 226 | handleCombinedChange('customHeaders', undefined); 227 | } 228 | }} 229 | /> 230 | 231 | {t.auth.enableCustomHeaders} 232 | 233 | 234 | 235 | {value?.type === 'combined' && value.customHeaders !== undefined && ( 236 |
237 | {value.customHeaders.map((header, index) => { 238 | const nameValid = isValidHeaderString(header.name || ''); 239 | const valueValid = isValidHeaderString(header.value || ''); 240 | const hasError = !nameValid || !valueValid; 241 | 242 | return ( 243 |
244 | 245 | { 251 | const headers = [...value.customHeaders!]; 252 | headers[index] = { ...headers[index], name: e.target.value }; 253 | handleCombinedChange('customHeaders', headers); 254 | }} 255 | /> 256 | { 262 | const headers = [...value.customHeaders!]; 263 | headers[index] = { ...headers[index], value: e.target.value }; 264 | handleCombinedChange('customHeaders', headers); 265 | }} 266 | /> 267 | { 268 | const headers = [...value.customHeaders!]; 269 | headers.splice(index, 1); 270 | handleCombinedChange('customHeaders', headers.length > 0 ? headers : [{ name: '', value: '' }]); 271 | }} /> 272 | 273 | {hasError && ( 274 |
275 | {t.auth.headerInvalidChars} 276 |
277 | )} 278 |
279 | ); 280 | })} 281 | 294 |
295 | )} 296 |
297 | )} 298 |
299 | ); 300 | }; 301 | 302 | export default AuthConfigComponent; -------------------------------------------------------------------------------- /src/types/mcp.ts: -------------------------------------------------------------------------------- 1 | // MCP协议类型定义 2 | 3 | /** 4 | * 被动检测结果 5 | */ 6 | export interface PassiveDetectionResult { 7 | id: string; 8 | timestamp: number; 9 | type: 'tool' | 'resource' | 'prompt'; 10 | targetName: string; 11 | uri?: string; 12 | parameters: Record; 13 | result: any; 14 | riskLevel: SecurityRiskLevel; 15 | threats: Array<{ 16 | type: string; 17 | severity: SecurityRiskLevel; 18 | description: string; 19 | evidence?: string; 20 | }>; 21 | sensitiveDataLeaks: Array<{ 22 | type: string; 23 | content: string; 24 | severity: SecurityRiskLevel; 25 | }>; 26 | recommendation: string; 27 | } 28 | 29 | /** 30 | * JSON-RPC 2.0 消息基础接口 31 | */ 32 | export interface JSONRPCMessage { 33 | jsonrpc: '2.0'; 34 | id?: string | number; 35 | } 36 | 37 | /** 38 | * JSON-RPC 请求 39 | */ 40 | export interface JSONRPCRequest extends JSONRPCMessage { 41 | method: string; 42 | params?: Record; 43 | id: string | number; 44 | } 45 | 46 | /** 47 | * JSON-RPC 响应 48 | */ 49 | export interface JSONRPCResponse extends JSONRPCMessage { 50 | result?: unknown; 51 | error?: { 52 | code: number; 53 | message: string; 54 | data?: unknown; 55 | }; 56 | id: string | number; 57 | } 58 | 59 | /** 60 | * JSON-RPC 通知 61 | */ 62 | export interface JSONRPCNotification extends JSONRPCMessage { 63 | method: string; 64 | params?: Record; 65 | } 66 | 67 | /** 68 | * 认证类型 69 | */ 70 | export type AuthType = 'none' | 'combined'; 71 | 72 | /** 73 | * 组合认证配置 74 | */ 75 | export interface CombinedAuth { 76 | type: 'combined'; 77 | apiKey?: { 78 | apiKey: string; 79 | headerName?: string; // 默认为 'Authorization' 80 | prefix?: string; // 默认为 'Bearer ' 81 | }; 82 | urlParams?: Array<{ 83 | name: string; 84 | value: string; 85 | }>; 86 | customHeaders?: Array<{ 87 | name: string; 88 | value: string; 89 | }>; 90 | basicAuth?: { 91 | username: string; 92 | password: string; 93 | }; 94 | } 95 | 96 | /** 97 | * 认证配置联合类型 98 | */ 99 | export type AuthConfig = 100 | | { type: 'none' } 101 | | CombinedAuth; 102 | 103 | /** 104 | * MCP 工具参数 Schema 105 | */ 106 | export interface MCPToolInputSchema { 107 | type: 'object'; 108 | properties: Record; 114 | required?: string[]; 115 | } 116 | 117 | /** 118 | * MCP 工具定义 119 | */ 120 | export interface MCPTool { 121 | name: string; 122 | description?: string; 123 | inputSchema: MCPToolInputSchema; 124 | } 125 | 126 | /** 127 | * MCP 资源定义 128 | */ 129 | export interface MCPResource { 130 | uri: string; 131 | name?: string; 132 | description?: string; 133 | mimeType?: string; 134 | uriTemplate?: string; // 资源模板的URI模板 135 | } 136 | 137 | /** 138 | * MCP 提示定义 139 | */ 140 | export interface MCPPrompt { 141 | name: string; 142 | description?: string; 143 | arguments?: Array<{ 144 | name: string; 145 | description?: string; 146 | required?: boolean; 147 | }>; 148 | } 149 | 150 | /** 151 | * MCP 工具调用结果 152 | */ 153 | export interface MCPToolResult { 154 | content: Array<{ 155 | type: 'text' | 'image' | 'resource'; 156 | text?: string; 157 | data?: string; 158 | mimeType?: string; 159 | }>; 160 | isError?: boolean; 161 | } 162 | 163 | /** 164 | * MCP 资源内容 165 | */ 166 | export interface MCPResourceContent { 167 | uri: string; 168 | mimeType?: string; 169 | text?: string; 170 | blob?: string; 171 | } 172 | 173 | /** 174 | * MCP 传输模式 175 | */ 176 | export type MCPTransportMode = 'sse' | 'streamable'; 177 | 178 | /** 179 | * MCP 服务器配置 180 | */ 181 | export interface MCPServerConfig { 182 | name: string; 183 | host: string; // 主机地址,如 http://127.0.0.1:8020 184 | ssePath?: string; // MCP路径,如 /sse (SSE模式) 或 /mcp (Streamable模式) 185 | messagePath?: string; // 消息路径,现在从SSE自动获取,可选 186 | transport: MCPTransportMode; // 传输模式:'sse' | 'streamable' 187 | sessionId?: string; 188 | headers?: Record; 189 | auth?: AuthConfig; // 认证配置 190 | } 191 | 192 | /** 193 | * MCP 连接状态 194 | */ 195 | export type MCPConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'error'; 196 | 197 | /** 198 | * MCP 调用历史记录 199 | */ 200 | export interface MCPCallHistory { 201 | id: string; 202 | timestamp: number; 203 | type: 'tool' | 'resource' | 'prompt'; 204 | name: string; 205 | parameters?: Record; 206 | result?: MCPToolResult | MCPResourceContent; 207 | error?: string; 208 | duration?: number; 209 | securityWarnings?: string[]; 210 | } 211 | 212 | /** 213 | * 安全风险级别 214 | */ 215 | export type SecurityRiskLevel = 'low' | 'medium' | 'high' | 'critical'; 216 | 217 | /** 218 | * 安全检查结果 219 | */ 220 | export interface SecurityCheckResult { 221 | level: SecurityRiskLevel; 222 | warnings: string[]; 223 | recommendations: string[]; 224 | } 225 | 226 | /** 227 | * MCP 服务器能力 228 | */ 229 | export interface MCPServerCapabilities { 230 | resources?: boolean; 231 | tools?: boolean; 232 | prompts?: boolean; 233 | logging?: boolean; 234 | } 235 | 236 | /** 237 | * 初始化响应 238 | */ 239 | export interface InitializeResult { 240 | protocolVersion: string; 241 | capabilities: MCPServerCapabilities; 242 | serverInfo: { 243 | name: string; 244 | version: string; 245 | }; 246 | } 247 | 248 | /** 249 | * LLM模型类型 250 | */ 251 | export type LLMType = 'openai' | 'claude' | 'gemini' | 'custom' | 'ollama'; 252 | 253 | /** 254 | * LLM配置 255 | */ 256 | export interface LLMConfig { 257 | id: string; 258 | name: string; 259 | type: LLMType; 260 | endpoint: string; 261 | apiKey?: string; 262 | model: string; 263 | maxTokens?: number; 264 | temperature?: number; 265 | customHeaders?: Record; 266 | enabled: boolean; 267 | description?: string; 268 | } 269 | 270 | /** 271 | * LLM请求参数 272 | */ 273 | export interface LLMRequest { 274 | messages: Array<{ 275 | role: 'system' | 'user' | 'assistant'; 276 | content: string; 277 | }>; 278 | model?: string; 279 | maxTokens?: number; 280 | temperature?: number; 281 | stream?: boolean; 282 | } 283 | 284 | /** 285 | * LLM响应 286 | */ 287 | export interface LLMResponse { 288 | content: string; 289 | usage?: { 290 | promptTokens: number; 291 | completionTokens: number; 292 | totalTokens: number; 293 | }; 294 | model?: string; 295 | } 296 | 297 | /** 298 | * 安全检测类型 299 | */ 300 | export type SecurityCheckType = 'tool' | 'prompt' | 'resource'; 301 | 302 | /** 303 | * 安全检测配置 304 | */ 305 | export interface SecurityCheckConfig { 306 | enabled: boolean; 307 | llmConfigId: string; 308 | autoGenerate: boolean; 309 | maxTestCases: number; 310 | timeout: number; 311 | enableLLMAnalysis?: boolean; 312 | } 313 | 314 | /** 315 | * LLM分析结果结构 316 | */ 317 | export interface LLMAnalysisResult { 318 | summary: string; 319 | riskLevel: SecurityRiskLevel; 320 | analysis: { 321 | description?: string; 322 | potentialImpact?: string; 323 | mitigation?: string; 324 | sideEffects?: string; 325 | [key: string]: any; 326 | }; 327 | raw?: string; // 原始分析文本 328 | } 329 | 330 | /** 331 | * 工具安全检测结果 332 | */ 333 | export interface SecurityTestResult { 334 | name: string; // toolName, promptName, resourceName 335 | scanType: string; // active, passive 336 | uri?: string; 337 | riskLevel: SecurityRiskLevel; 338 | vulnerabilities: Array<{ 339 | type: string; 340 | severity: SecurityRiskLevel; 341 | description: string; 342 | uri?: string; 343 | testCase?: string; 344 | source?: string; 345 | evidence?: string; 346 | recommendation: string; 347 | }>; 348 | testResults: Array<{ 349 | testCase: string; 350 | parameters: Record; 351 | result: any; 352 | riskAssessment: string; 353 | passed: boolean; 354 | }>; 355 | llmAnalysis: string | LLMAnalysisResult; 356 | timestamp: number; 357 | } 358 | 359 | /** 360 | * 综合安全报告 361 | * const accessTest = { 362 | testType: 'direct_access', 363 | success: !error, 364 | duration: duration, 365 | result: testResult, 366 | riskAssessment: 'low' 367 | }; 368 | */ 369 | export interface SecurityReport { 370 | id: string; 371 | serverName: string; 372 | timestamp: number; 373 | overallRisk: SecurityRiskLevel; 374 | toolResults: SecurityTestResult[]; 375 | promptResults: SecurityTestResult[]; 376 | resourceResults: SecurityTestResult[]; 377 | summary: { 378 | totalIssues: number; 379 | criticalIssues: number; 380 | highIssues: number; 381 | mediumIssues: number; 382 | lowIssues: number; 383 | }; 384 | recommendations: string[]; 385 | comprehensiveRiskAnalysis?: string; // LLM生成的综合风险分析报告 386 | } 387 | 388 | /** 389 | * 安全检测历史记录 390 | */ 391 | export interface SecurityHistoryRecord { 392 | id: string; 393 | serverName: string; 394 | serverConfig: MCPServerConfig; 395 | timestamp: number; 396 | scanType: 'active' | 'passive'; 397 | report: SecurityReport | null; 398 | passiveResults?: PassiveDetectionResult[]; 399 | status: 'completed' | 'failed' | 'cancelled'; 400 | errorMessage?: string; 401 | duration?: number; // 扫描持续时间(毫秒) 402 | config: SecurityCheckConfig; 403 | } 404 | 405 | /** 406 | * 被动检测规则类型定义 407 | */ 408 | 409 | // 检测规则分类 410 | export type DetectionRuleCategory = 411 | | 'security' // 安全威胁 412 | | 'privacy' // 隐私泄漏 413 | | 'compliance' // 合规检查 414 | | 'data_quality' // 数据质量 415 | | 'performance' // 性能问题 416 | | 'custom'; // 自定义规则 417 | 418 | // 检测范围 419 | export type DetectionScope = 420 | | 'parameters' // 仅检测输入参数 421 | | 'output' // 仅检测输出结果 422 | | 'both'; // 检测输入和输出 423 | 424 | // 检测规则接口 425 | export interface DetectionRule { 426 | id: string; 427 | name: string; 428 | description: string; 429 | category: DetectionRuleCategory; 430 | enabled: boolean; 431 | 432 | // 正则表达式规则 433 | pattern: string; // 正则表达式模式 434 | flags?: string; // 正则标志 (g, i, m, s, u, y) 435 | scope: DetectionScope; // 检测范围 436 | 437 | // 风险评估 438 | riskLevel: SecurityRiskLevel; // 风险等级 439 | threatType: string; // 威胁类型 440 | 441 | // 匹配处理 442 | captureGroups?: string[]; // 捕获组名称 443 | maskSensitiveData?: boolean; // 是否遮蔽敏感数据 444 | maxMatches?: number; // 最大匹配数量 445 | 446 | // 元数据 447 | isBuiltin: boolean; // 是否为内置规则 448 | createdAt: number; // 创建时间 449 | updatedAt: number; // 更新时间 450 | tags?: string[]; // 标签 451 | 452 | // 自定义处理 453 | customProcessor?: string; // 自定义处理函数名 454 | 455 | // 建议和修复 456 | recommendation?: string; // 安全建议 457 | remediation?: string; // 修复建议 458 | references?: string[]; // 参考链接 459 | } 460 | 461 | // 检测规则匹配结果 462 | export interface DetectionRuleMatch { 463 | rule: DetectionRule; 464 | matches: Array<{ 465 | fullMatch: string; // 完整匹配 466 | capturedGroups?: string[]; // 捕获组 467 | startIndex: number; // 开始位置 468 | endIndex: number; // 结束位置 469 | context?: string; // 上下文 470 | }>; 471 | maskedContent?: string; // 遮蔽后的内容 472 | severity: SecurityRiskLevel; // 实际严重程度 473 | } 474 | 475 | // 规则集合接口 476 | export interface DetectionRuleSet { 477 | id: string; 478 | name: string; 479 | description: string; 480 | version: string; 481 | rules: DetectionRule[]; 482 | enabled: boolean; 483 | createdAt: number; 484 | updatedAt: number; 485 | } 486 | 487 | // 规则验证结果 488 | export interface RuleValidationResult { 489 | valid: boolean; 490 | errors: string[]; 491 | warnings: string[]; 492 | testResults?: Array<{ 493 | input: string; 494 | matches: boolean; 495 | captured?: string[]; 496 | }>; 497 | } 498 | 499 | // 规则统计信息 500 | 501 | /** 502 | * 组件参数分析结果 503 | */ 504 | export interface ComponentParameterAnalysis { 505 | hasParameters: boolean; 506 | parameterCount: number; 507 | parameters: Array<{ 508 | name: string; 509 | type?: string; 510 | description?: string; 511 | required?: boolean; 512 | isOptional?: boolean; 513 | }>; 514 | requiresLLMGeneration: boolean; // 是否需要LLM生成参数 515 | } 516 | 517 | /** 518 | * 增强的工具定义(包含参数分析) 519 | */ 520 | export interface EnhancedMCPTool extends MCPTool { 521 | parameterAnalysis: ComponentParameterAnalysis; 522 | componentType: 'tool'; 523 | } 524 | 525 | /** 526 | * 增强的提示定义(包含参数分析) 527 | */ 528 | export interface EnhancedMCPPrompt extends MCPPrompt { 529 | parameterAnalysis: ComponentParameterAnalysis; 530 | componentType: 'prompt'; 531 | } 532 | 533 | /** 534 | * 增强的资源定义(包含参数分析) 535 | */ 536 | export interface EnhancedMCPResource extends MCPResource { 537 | parameterAnalysis: ComponentParameterAnalysis; 538 | componentType: 'resource'; 539 | resourceType: '静态资源' | '动态资源'; 540 | } 541 | 542 | /** 543 | * 统一的增强组件类型 544 | */ 545 | export type EnhancedMCPComponent = EnhancedMCPTool | EnhancedMCPPrompt | EnhancedMCPResource; 546 | 547 | /** 548 | * 统一的风险条目 549 | */ 550 | export interface UnifiedRiskItem { 551 | id: string; // 唯一标识 552 | source: string; // 来源(工具名/提示名/资源URI) 553 | sourceType: 'tool' | 'prompt' | 'resource'; // 来源类型 554 | scanType: 'active' | 'passive'; // 扫描类型 555 | riskType: 'vulnerability' | 'threat' | 'risk' | 'test_failure' | 'llm_analysis'; // 风险类型 556 | severity: SecurityRiskLevel; // 严重程度 557 | title: string; // 标题 558 | description: string; // 描述 559 | evidence?: string; // 证据 560 | recommendation?: string; // 建议 561 | timestamp: number; // 时间戳 562 | testCase?: string; // 测试用例名称(如果是测试失败) 563 | rawData?: any; // 原始数据 564 | } 565 | 566 | /** 567 | * 统一的安全概览数据 568 | */ 569 | export interface UnifiedSecurityOverview { 570 | totalRisks: number; 571 | risksBySeverity: { 572 | critical: number; 573 | high: number; 574 | medium: number; 575 | low: number; 576 | }; 577 | risksBySource: { 578 | tool: number; 579 | prompt: number; 580 | resource: number; 581 | }; 582 | risksByScanType: { 583 | active: number; 584 | passive: number; 585 | }; 586 | risksByType: { 587 | vulnerability: number; 588 | threat: number; 589 | risk: number; 590 | test_failure: number; 591 | llm_analysis: number; 592 | }; 593 | risks: UnifiedRiskItem[]; 594 | } 595 | -------------------------------------------------------------------------------- /src/components/MCPListPanel.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { 3 | Card, 4 | List, 5 | Button, 6 | Space, 7 | Popconfirm, 8 | message, 9 | Upload, 10 | Typography, 11 | Tooltip, 12 | Empty 13 | } from 'antd'; 14 | import { 15 | PlayCircleOutlined, 16 | DeleteOutlined, 17 | DownloadOutlined, 18 | UploadOutlined, 19 | DatabaseOutlined, 20 | DisconnectOutlined 21 | } from '@ant-design/icons'; 22 | import { useDispatch, useSelector } from 'react-redux'; 23 | import { connectToServer, disconnectFromServer } from '../store/mcpSlice'; 24 | import { RootState } from '../store'; 25 | import { storage } from '../utils/storage'; 26 | import { MCPServerConfig } from '../types/mcp'; 27 | import { useI18n } from '../hooks/useI18n'; 28 | 29 | const { Text } = Typography; 30 | 31 | interface SavedConfig extends MCPServerConfig { 32 | createdAt?: number; 33 | updatedAt?: number; 34 | } 35 | 36 | interface MCPListPanelProps { 37 | onConfigLoad?: (config: MCPServerConfig) => void; 38 | refreshTrigger?: number; 39 | } 40 | 41 | const MCPListPanel: React.FC = ({ onConfigLoad, refreshTrigger }) => { 42 | const { t } = useI18n(); 43 | const dispatch = useDispatch(); 44 | const [savedConfigs, setSavedConfigs] = useState([]); 45 | const [loading, setLoading] = useState(null); 46 | 47 | // 从状态中获取连接信息 48 | const { connectionStatus, serverConfig } = useSelector((state: RootState) => state.mcp); 49 | 50 | // 加载已保存的配置 51 | const loadSavedConfigs = () => { 52 | const configs = storage.getSavedConfigs(); 53 | setSavedConfigs(configs); 54 | }; 55 | 56 | useEffect(() => { 57 | loadSavedConfigs(); 58 | }, []); 59 | 60 | // 监听刷新触发器 61 | useEffect(() => { 62 | if (refreshTrigger && refreshTrigger > 0) { 63 | loadSavedConfigs(); 64 | } 65 | }, [refreshTrigger]); 66 | 67 | // 检查配置是否为当前连接的配置 68 | const isCurrentConnection = (config: MCPServerConfig) => { 69 | return connectionStatus === 'connected' && 70 | serverConfig && 71 | serverConfig.name === config.name && 72 | serverConfig.host === config.host && 73 | serverConfig.ssePath === config.ssePath; 74 | }; 75 | 76 | // 连接到指定配置 77 | const handleConnect = async (config: MCPServerConfig) => { 78 | setLoading(config.name); 79 | try { 80 | // 如果当前已连接到其他服务器,先断开连接 81 | if (connectionStatus === 'connected') { 82 | console.log(t.config.messages.disconnectSuccess); 83 | await dispatch(disconnectFromServer() as any).unwrap(); 84 | } 85 | 86 | await dispatch(connectToServer(config) as any).unwrap(); 87 | message.success(`${t.success.connected} - ${config.name}`); 88 | 89 | // 通知父组件配置已加载 90 | if (onConfigLoad) { 91 | onConfigLoad(config); 92 | } 93 | } catch (error) { 94 | message.error(`${t.errors.connectionFailed}: ${error}`); 95 | } finally { 96 | setLoading(null); 97 | } 98 | }; 99 | 100 | // 断开连接 101 | const handleDisconnect = async () => { 102 | try { 103 | await dispatch(disconnectFromServer() as any).unwrap(); 104 | message.success(t.config.messages.disconnectSuccess); 105 | } catch (error) { 106 | message.error(`${t.config.messages.disconnectFailed}: ${error}`); 107 | } 108 | }; 109 | 110 | // 删除配置 111 | const handleDelete = (name: string) => { 112 | const success = storage.deleteMCPConfig(name); 113 | if (success) { 114 | message.success(t.success.configDeleted); 115 | loadSavedConfigs(); 116 | } else { 117 | message.error(t.errors.saveConfigFailed); 118 | } 119 | }; 120 | 121 | // 导出所有配置 122 | const handleExport = () => { 123 | const success = storage.exportAllConfigs(); 124 | if (success) { 125 | message.success(t.success.exportSuccess); 126 | } else { 127 | message.error(t.errors.exportFailed); 128 | } 129 | }; 130 | 131 | // 导入配置 132 | const handleImport = async (file: File) => { 133 | const success = await storage.importConfigs(file); 134 | if (success) { 135 | message.success(t.success.importSuccess); 136 | loadSavedConfigs(); 137 | } else { 138 | message.error(t.errors.importFailed); 139 | } 140 | return false; // 阻止默认上传行为 141 | }; 142 | 143 | // 格式化时间 144 | const formatTime = (timestamp?: number) => { 145 | if (!timestamp) return ''; 146 | const date = new Date(timestamp); 147 | const now = new Date(); 148 | const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); 149 | const targetDate = new Date(date.getFullYear(), date.getMonth(), date.getDate()); 150 | 151 | // 其他日期显示月/日 时:分 152 | return date.toLocaleDateString('zh-CN', { 153 | month: 'numeric', 154 | day: 'numeric', 155 | hour: '2-digit', 156 | minute: '2-digit' 157 | }); 158 | 159 | }; 160 | 161 | return ( 162 |
163 | 166 | 167 | {t.config.savedConfigs} 168 | 169 | } 170 | size="small" 171 | extra={ 172 | 173 | 178 |
391 |
392 | 393 | 394 | ); 395 | }} 396 | /> 397 | )} 398 | 399 | 400 | ); 401 | }; 402 | 403 | export default MCPListPanel; -------------------------------------------------------------------------------- /src/components/LLMConfig.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { 3 | Card, 4 | Form, 5 | Input, 6 | Select, 7 | InputNumber, 8 | Switch, 9 | Button, 10 | Space, 11 | Table, 12 | Modal, 13 | message, 14 | Popconfirm, 15 | Tag, 16 | Typography, 17 | Divider, 18 | Row, 19 | Col 20 | } from 'antd'; 21 | import { 22 | PlusOutlined, 23 | EditOutlined, 24 | DeleteOutlined, 25 | ThunderboltOutlined, 26 | SaveOutlined, 27 | CloseOutlined 28 | } from '@ant-design/icons'; 29 | import { LLMConfig, LLMType } from '../types/mcp'; 30 | import { useI18n } from '../hooks/useI18n'; 31 | import { getLLMConfigs, saveLLMConfig, deleteLLMConfig, testLLMConnection } from '../utils/storage'; 32 | 33 | const { Title, Text } = Typography; 34 | const { TextArea } = Input; 35 | 36 | interface LLMConfigProps { 37 | onConfigChange?: (configs: LLMConfig[]) => void; 38 | } 39 | 40 | const LLMConfigComponent: React.FC = ({ onConfigChange }) => { 41 | const { t } = useI18n(); 42 | const [form] = Form.useForm(); 43 | const [configs, setConfigs] = useState([]); 44 | const [isModalVisible, setIsModalVisible] = useState(false); 45 | const [editingConfig, setEditingConfig] = useState(null); 46 | const [testing, setTesting] = useState(null); 47 | const [endpointHelpText, setEndpointHelpText] = useState(t.llm.endpointHelpDefault); 48 | const [endpointPlaceholder, setEndpointPlaceholder] = useState(t.llm.endpointPlaceholder); 49 | 50 | // LLM类型选项 51 | const llmTypeOptions = [ 52 | { value: 'openai', label: t.llm.types.openai }, 53 | { value: 'claude', label: t.llm.types.claude }, 54 | { value: 'gemini', label: t.llm.types.gemini }, 55 | { value: 'ollama', label: t.llm.types.ollama }, 56 | { value: 'custom', label: t.llm.types.custom } 57 | ]; 58 | 59 | // 预定义模型选项 60 | const modelOptions: Record = { 61 | openai: ['gpt-4', 'gpt-4-turbo', 'gpt-3.5-turbo', 'gpt-3.5-turbo-16k'], 62 | claude: ['claude-3-opus-20240229', 'claude-3-sonnet-20240229', 'claude-3-haiku-20240307'], 63 | gemini: ['gemini-pro', 'gemini-pro-vision'], 64 | ollama: ['llama2', 'codellama', 'mistral', 'neural-chat'], 65 | custom: [] 66 | }; 67 | 68 | // 默认完整端点 69 | const defaultEndpoints: Record = { 70 | openai: 'https://api.openai.com/v1/chat/completions', 71 | claude: 'https://api.anthropic.com/v1/messages', 72 | gemini: 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent', 73 | ollama: 'http://localhost:11434/api/chat', 74 | custom: '' 75 | }; 76 | 77 | useEffect(() => { 78 | loadConfigs(); 79 | }, []); 80 | 81 | const loadConfigs = async () => { 82 | try { 83 | const savedConfigs = await getLLMConfigs(); 84 | setConfigs(savedConfigs); 85 | onConfigChange?.(savedConfigs); 86 | } catch (error) { 87 | console.error(t.errors.loadConfigFailed, error); 88 | } 89 | }; 90 | 91 | const showModal = (config?: LLMConfig) => { 92 | if (config) { 93 | setEditingConfig(config); 94 | form.setFieldsValue({ 95 | ...config, 96 | customHeaders: config.customHeaders ? Object.entries(config.customHeaders).map(([key, value]) => ({ key, value })) : [] 97 | }); 98 | // 设置对应类型的帮助文本,但不覆盖配置值 99 | setHelpTextForType(config.type); 100 | } else { 101 | setEditingConfig(null); 102 | form.resetFields(); 103 | form.setFieldsValue({ 104 | type: 'openai', 105 | endpoint: defaultEndpoints.openai, 106 | model: modelOptions.openai[0], 107 | maxTokens: 4096, 108 | temperature: 0.7, 109 | enabled: true 110 | }); 111 | // 设置默认类型的帮助文本和placeholder 112 | setEndpointHelpText(t.llm.endpointHelpOpenAI); 113 | setEndpointPlaceholder(defaultEndpoints.openai); 114 | } 115 | setIsModalVisible(true); 116 | }; 117 | 118 | const handleCancel = () => { 119 | setIsModalVisible(false); 120 | setEditingConfig(null); 121 | form.resetFields(); 122 | }; 123 | 124 | // 只设置帮助文本和占位符,不修改表单值(用于编辑模式) 125 | const setHelpTextForType = (type: LLMType) => { 126 | let helpText = ''; 127 | let placeholder = ''; 128 | switch (type) { 129 | case 'openai': 130 | helpText = t.llm.endpointHelpOpenAI; 131 | placeholder = defaultEndpoints.openai; 132 | break; 133 | case 'custom': 134 | helpText = t.llm.endpointHelpCustom; 135 | placeholder = 'https://your-api.com/v1/chat/completions'; 136 | break; 137 | case 'claude': 138 | helpText = t.llm.endpointHelpClaude; 139 | placeholder = defaultEndpoints.claude; 140 | break; 141 | case 'gemini': 142 | helpText = t.llm.endpointHelpGemini; 143 | placeholder = defaultEndpoints.gemini; 144 | break; 145 | case 'ollama': 146 | helpText = t.llm.endpointHelpOllama; 147 | placeholder = defaultEndpoints.ollama; 148 | break; 149 | } 150 | 151 | setEndpointHelpText(helpText); 152 | setEndpointPlaceholder(placeholder); 153 | }; 154 | 155 | const handleTypeChange = (type: LLMType) => { 156 | const currentEndpoint = form.getFieldValue('endpoint'); 157 | const defaultEndpoint = defaultEndpoints[type]; 158 | const model = modelOptions[type][0] || ''; 159 | 160 | // 只有在当前endpoint为空或者是其他类型的默认值时,才自动设置新的默认endpoint 161 | const shouldUpdateEndpoint = !currentEndpoint || 162 | Object.values(defaultEndpoints).includes(currentEndpoint); 163 | 164 | form.setFieldsValue({ 165 | ...(shouldUpdateEndpoint && { endpoint: defaultEndpoint }), 166 | model 167 | }); 168 | 169 | // 设置帮助文本 170 | setHelpTextForType(type); 171 | }; 172 | 173 | const handleSave = async () => { 174 | try { 175 | const values = await form.validateFields(); 176 | 177 | // 处理自定义头部 178 | const customHeaders: Record = {}; 179 | if (values.customHeaders) { 180 | values.customHeaders.forEach((header: any) => { 181 | if (header.key && header.value) { 182 | customHeaders[header.key] = header.value; 183 | } 184 | }); 185 | } 186 | 187 | const config: LLMConfig = { 188 | id: editingConfig?.id || `llm_${Date.now()}`, 189 | name: values.name, 190 | type: values.type, 191 | endpoint: values.endpoint, 192 | apiKey: values.apiKey, 193 | model: values.model, 194 | maxTokens: values.maxTokens, 195 | temperature: values.temperature, 196 | customHeaders: Object.keys(customHeaders).length > 0 ? customHeaders : undefined, 197 | enabled: values.enabled, 198 | description: values.description 199 | }; 200 | 201 | await saveLLMConfig(config); 202 | await loadConfigs(); 203 | setIsModalVisible(false); 204 | setEditingConfig(null); 205 | form.resetFields(); 206 | message.success(editingConfig ? t.llm.configUpdated : t.llm.configSaved); 207 | } catch (error) { 208 | console.error('保存配置失败:', error); 209 | message.error(t.llm.saveConfigFailed); 210 | } 211 | }; 212 | 213 | const handleDelete = async (id: string) => { 214 | try { 215 | await deleteLLMConfig(id); 216 | await loadConfigs(); 217 | message.success(t.llm.configDeleted); 218 | } catch (error) { 219 | console.error('删除配置失败:', error); 220 | message.error(t.errors.loadConfigFailed); 221 | } 222 | }; 223 | 224 | const handleTest = async (config: LLMConfig) => { 225 | setTesting(config.id); 226 | try { 227 | await testLLMConnection(config); 228 | message.success(t.llm.connectionTestSuccess); 229 | } catch (error) { 230 | console.error('连接测试失败:', error); 231 | message.error(`${t.llm.connectionTestFailed}: ${error instanceof Error ? error.message : '未知错误'}`); 232 | } finally { 233 | setTesting(null); 234 | } 235 | }; 236 | 237 | const toggleEnabled = async (config: LLMConfig) => { 238 | try { 239 | const updatedConfig = { ...config, enabled: !config.enabled }; 240 | await saveLLMConfig(updatedConfig); 241 | await loadConfigs(); 242 | message.success(updatedConfig.enabled ? t.llm.configEnabled : t.llm.configDisabled); 243 | } catch (error) { 244 | console.error('更新配置失败:', error); 245 | message.error(t.llm.saveConfigFailed); 246 | } 247 | }; 248 | 249 | const columns = [ 250 | { 251 | title: t.llm.name, 252 | dataIndex: 'name', 253 | key: 'name', 254 | render: (text: string, record: LLMConfig) => ( 255 |
256 |
{text}
257 | {record.description && ( 258 | 259 | {record.description} 260 | 261 | )} 262 |
263 | ), 264 | }, 265 | { 266 | title: t.llm.type, 267 | dataIndex: 'type', 268 | key: 'type', 269 | render: (type: LLMType) => { 270 | const colors: Record = { 271 | openai: 'blue', 272 | claude: 'purple', 273 | gemini: 'green', 274 | ollama: 'orange', 275 | custom: 'gray' 276 | }; 277 | return {llmTypeOptions.find(opt => opt.value === type)?.label}; 278 | }, 279 | }, 280 | { 281 | title: t.llm.endpointUrl, 282 | dataIndex: 'endpoint', 283 | key: 'endpoint', 284 | render: (endpoint: string) => ( 285 | 286 | {endpoint.length > 40 ? `${endpoint.substring(0, 40)}...` : endpoint} 287 | 288 | ), 289 | }, 290 | { 291 | title: t.llm.model, 292 | dataIndex: 'model', 293 | key: 'model', 294 | }, 295 | { 296 | title: t.llm.status, 297 | dataIndex: 'enabled', 298 | key: 'enabled', 299 | render: (enabled: boolean, record: LLMConfig) => ( 300 | toggleEnabled(record)} 303 | checkedChildren={t.llm.enabled} 304 | unCheckedChildren={t.llm.disabled} 305 | /> 306 | ), 307 | }, 308 | { 309 | title: t.llm.actions, 310 | key: 'actions', 311 | render: (_: any, record: LLMConfig) => ( 312 | 313 | 322 | 330 | handleDelete(record.id)} 333 | okText={t.common.ok} 334 | cancelText={t.common.cancel} 335 | > 336 | 344 | 345 | 346 | ), 347 | }, 348 | ]; 349 | 350 | return ( 351 |
352 | 355 | {t.llm.title} 356 | 363 |
364 | } 365 | > 366 | 373 | 374 | 375 | }> 382 | {t.common.cancel} 383 | , 384 | , 387 | ]} 388 | > 389 |
399 | 400 |
401 | 406 | 407 | 408 | 409 | 410 | 415 | 429 | 430 | 431 | 432 | 433 | 438 | 439 | 440 | 441 | 442 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 457 | 458 | 459 | 460 | 461 | 465 | 466 | 467 | 468 | 469 | 474 | 475 | 476 | 477 | 478 | 479 | 483 |