Add files via upload

This commit is contained in:
公明
2025-11-15 21:37:24 +08:00
committed by GitHub
parent 8cd2536ccb
commit b5b8a2cb14
5 changed files with 165 additions and 40 deletions
+14 -12
View File
@@ -465,24 +465,26 @@ func (h *ConfigHandler) UpdateConfig(c *gin.Context) {
// 在循环外部统一更新,避免重复调用
h.externalMCPMgr.LoadConfigs(&h.config.ExternalMCP)
// 处理MCP连接状态
// 处理MCP连接状态(异步启动,避免阻塞)
for mcpName := range externalMCPToolMap {
cfg := h.config.ExternalMCP.Servers[mcpName]
// 如果MCP需要启用,确保客户端已启动
if cfg.ExternalMCPEnable {
// 启动外部MCP(如果未启动)
// 启动外部MCP(如果未启动)- 异步执行,避免阻塞
client, exists := h.externalMCPMgr.GetClient(mcpName)
if !exists || !client.IsConnected() {
if err := h.externalMCPMgr.StartClient(mcpName); err != nil {
h.logger.Warn("启动外部MCP失败",
zap.String("mcp", mcpName),
zap.Error(err),
)
} else {
h.logger.Info("启动外部MCP",
zap.String("mcp", mcpName),
)
}
go func(name string) {
if err := h.externalMCPMgr.StartClient(name); err != nil {
h.logger.Warn("启动外部MCP失败",
zap.String("mcp", name),
zap.Error(err),
)
} else {
h.logger.Info("启动外部MCP",
zap.String("mcp", name),
)
}
}(mcpName)
}
}
}
+20 -6
View File
@@ -56,11 +56,16 @@ func (h *ExternalMCPHandler) GetExternalMCPs(c *gin.Context) {
}
toolCount := toolCounts[name]
errorMsg := ""
if status == "error" {
errorMsg = h.manager.GetError(name)
}
result[name] = ExternalMCPResponse{
Config: cfg,
Status: status,
ToolCount: toolCount,
Error: errorMsg,
}
}
@@ -102,10 +107,17 @@ func (h *ExternalMCPHandler) GetExternalMCP(c *gin.Context) {
}
}
// 获取错误信息
errorMsg := ""
if status == "error" {
errorMsg = h.manager.GetError(name)
}
c.JSON(http.StatusOK, ExternalMCPResponse{
Config: cfg,
Status: status,
ToolCount: toolCount,
Error: errorMsg,
})
}
@@ -238,7 +250,7 @@ func (h *ExternalMCPHandler) StartExternalMCP(c *gin.Context) {
return
}
// 启动客户端(这可能会花费一些时间
// 启动客户端(立即创建客户端并设置状态为connecting,实际连接在后台进行
h.logger.Info("开始启动外部MCP", zap.String("name", name))
if err := h.manager.StartClient(name); err != nil {
h.logger.Error("启动外部MCP失败", zap.String("name", name), zap.Error(err))
@@ -249,16 +261,17 @@ func (h *ExternalMCPHandler) StartExternalMCP(c *gin.Context) {
return
}
// 获取连接状态
// 获取客户端状态(应该是connecting)
client, exists := h.manager.GetClient(name)
status := "disconnected"
status := "connecting"
if exists {
status = client.GetStatus()
}
h.logger.Info("外部MCP启动完成", zap.String("name", name), zap.String("status", status))
// 立即返回,不等待连接完成
// 客户端会在后台异步连接,用户可以通过状态查询接口查看连接状态
c.JSON(http.StatusOK, gin.H{
"message": "外部MCP启动完成",
"message": "外部MCP启动请求已提交,正在后台连接中",
"status": status,
})
}
@@ -504,7 +517,8 @@ type AddOrUpdateExternalMCPRequest struct {
// ExternalMCPResponse 外部MCP响应
type ExternalMCPResponse struct {
Config config.ExternalMCPServerConfig `json:"config"`
Status string `json:"status"` // "connected", "disconnected", "disabled", "error"
Status string `json:"status"` // "connected", "disconnected", "disabled", "error", "connecting"
ToolCount int `json:"tool_count"` // 工具数量
Error string `json:"error,omitempty"` // 错误信息(仅在status为error时存在)
}
+112 -21
View File
@@ -22,6 +22,7 @@ type ExternalMCPManager struct {
storage MonitorStorage // 可选的持久化存储
executions map[string]*ToolExecution // 执行记录
stats map[string]*ToolStats // 工具统计信息
errors map[string]string // 错误信息
mu sync.RWMutex
}
@@ -39,6 +40,7 @@ func NewExternalMCPManagerWithStorage(logger *zap.Logger, storage MonitorStorage
storage: storage,
executions: make(map[string]*ToolExecution),
stats: make(map[string]*ToolStats),
errors: make(map[string]string),
}
}
@@ -117,12 +119,12 @@ func (m *ExternalMCPManager) StartClient(name string) error {
// 检查是否已经有连接的客户端
m.mu.RLock()
_, hasClient := m.clients[name]
existingClient, hasClient := m.clients[name]
m.mu.RUnlock()
if hasClient {
// 检查客户端是否已连接
if client, ok := m.GetClient(name); ok && client.IsConnected() {
if existingClient.IsConnected() {
// 客户端已连接,直接返回成功(目标状态已达成)
// 更新配置为启用(确保配置一致)
m.mu.Lock()
@@ -132,22 +134,55 @@ func (m *ExternalMCPManager) StartClient(name string) error {
return nil
}
// 如果有客户端但未连接,先关闭
if client, ok := m.GetClient(name); ok {
client.Close()
m.mu.Lock()
delete(m.clients, name)
m.mu.Unlock()
}
existingClient.Close()
m.mu.Lock()
delete(m.clients, name)
m.mu.Unlock()
}
// 更新配置为启用
m.mu.Lock()
serverCfg.ExternalMCPEnable = true
m.configs[name] = serverCfg
// 清除之前的错误信息(重新启动时)
delete(m.errors, name)
m.mu.Unlock()
// 连接客户端
return m.connectClient(name, serverCfg)
// 立即创建客户端并设置为"connecting"状态,这样前端可以立即看到状态
client := m.createClient(serverCfg)
if client == nil {
return fmt.Errorf("无法创建客户端:不支持的传输模式")
}
// 设置状态为connecting
m.setClientStatus(client, "connecting")
// 立即保存客户端,这样前端查询时就能看到"connecting"状态
m.mu.Lock()
m.clients[name] = client
m.mu.Unlock()
// 在后台异步进行实际连接
go func() {
if err := m.doConnect(name, serverCfg, client); err != nil {
m.logger.Error("连接外部MCP客户端失败",
zap.String("name", name),
zap.Error(err),
)
// 连接失败,设置状态为error并保存错误信息
m.setClientStatus(client, "error")
m.mu.Lock()
m.errors[name] = err.Error()
m.mu.Unlock()
} else {
// 连接成功,清除错误信息
m.mu.Lock()
delete(m.errors, name)
m.mu.Unlock()
}
}()
return nil
}
// StopClient 停止客户端
@@ -166,6 +201,9 @@ func (m *ExternalMCPManager) StopClient(name string) error {
delete(m.clients, name)
}
// 清除错误信息
delete(m.errors, name)
// 更新配置为禁用
serverCfg.ExternalMCPEnable = false
m.configs[name] = serverCfg
@@ -182,6 +220,14 @@ func (m *ExternalMCPManager) GetClient(name string) (ExternalMCPClient, bool) {
return client, exists
}
// GetError 获取错误信息
func (m *ExternalMCPManager) GetError(name string) string {
m.mu.RLock()
defer m.mu.RUnlock()
return m.errors[name]
}
// GetAllTools 获取所有外部MCP的工具
func (m *ExternalMCPManager) GetAllTools(ctx context.Context) ([]Tool, error) {
m.mu.RLock()
@@ -543,10 +589,8 @@ func (m *ExternalMCPManager) GetToolCounts() map[string]int {
return result
}
// connectClient 连接客户端(异步
func (m *ExternalMCPManager) connectClient(name string, serverCfg config.ExternalMCPServerConfig) error {
var client ExternalMCPClient
// createClient 创建客户端(不连接
func (m *ExternalMCPManager) createClient(serverCfg config.ExternalMCPServerConfig) ExternalMCPClient {
timeout := time.Duration(serverCfg.Timeout) * time.Second
if timeout <= 0 {
timeout = 30 * time.Second
@@ -561,29 +605,77 @@ func (m *ExternalMCPManager) connectClient(name string, serverCfg config.Externa
} else if serverCfg.URL != "" {
transport = "http"
} else {
return fmt.Errorf("无法确定传输模式: 需要指定command或url")
return nil
}
}
switch transport {
case "http":
if serverCfg.URL == "" {
return fmt.Errorf("HTTP模式需要URL")
return nil
}
client = NewHTTPMCPClient(serverCfg.URL, timeout, m.logger)
return NewHTTPMCPClient(serverCfg.URL, timeout, m.logger)
case "stdio":
if serverCfg.Command == "" {
return fmt.Errorf("stdio模式需要command")
return nil
}
client = NewStdioMCPClient(serverCfg.Command, serverCfg.Args, timeout, m.logger)
return NewStdioMCPClient(serverCfg.Command, serverCfg.Args, timeout, m.logger)
default:
return fmt.Errorf("不支持的传输模式: %s", transport)
return nil
}
}
// doConnect 执行实际连接
func (m *ExternalMCPManager) doConnect(name string, serverCfg config.ExternalMCPServerConfig, client ExternalMCPClient) error {
timeout := time.Duration(serverCfg.Timeout) * time.Second
if timeout <= 0 {
timeout = 30 * time.Second
}
// 初始化连接
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
if err := client.Initialize(ctx); err != nil {
return err
}
m.logger.Info("外部MCP客户端已连接",
zap.String("name", name),
)
return nil
}
// setClientStatus 设置客户端状态(通过类型断言)
func (m *ExternalMCPManager) setClientStatus(client ExternalMCPClient, status string) {
switch c := client.(type) {
case *HTTPMCPClient:
c.setStatus(status)
case *StdioMCPClient:
c.setStatus(status)
}
}
// connectClient 连接客户端(异步)- 保留用于向后兼容
func (m *ExternalMCPManager) connectClient(name string, serverCfg config.ExternalMCPServerConfig) error {
client := m.createClient(serverCfg)
if client == nil {
return fmt.Errorf("无法创建客户端:不支持的传输模式")
}
// 设置状态为connecting
m.setClientStatus(client, "connecting")
// 初始化连接
timeout := time.Duration(serverCfg.Timeout) * time.Second
if timeout <= 0 {
timeout = 30 * time.Second
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
if err := client.Initialize(ctx); err != nil {
m.logger.Error("初始化外部MCP客户端失败",
zap.String("name", name),
@@ -599,7 +691,6 @@ func (m *ExternalMCPManager) connectClient(name string, serverCfg config.Externa
m.logger.Info("外部MCP客户端已连接",
zap.String("name", name),
zap.String("transport", transport),
)
return nil
+12
View File
@@ -2331,6 +2331,18 @@ header {
border: 1px solid rgba(255, 193, 7, 0.3);
}
.external-mcp-status.status-error {
background: rgba(220, 53, 69, 0.12);
color: var(--error-color);
border: 1px solid rgba(220, 53, 69, 0.3);
}
.external-mcp-status.status-error::before {
content: '❌';
display: inline-block;
margin-right: 4px;
}
.external-mcp-item-actions {
display: flex;
align-items: center;
+7 -1
View File
@@ -2652,9 +2652,11 @@ function renderExternalMCPList(servers) {
const status = server.status || 'disconnected';
const statusClass = status === 'connected' ? 'status-connected' :
status === 'connecting' ? 'status-connecting' :
status === 'error' ? 'status-error' :
status === 'disabled' ? 'status-disabled' : 'status-disconnected';
const statusText = status === 'connected' ? '已连接' :
status === 'connecting' ? '连接中...' :
status === 'error' ? '连接失败' :
status === 'disabled' ? '已禁用' : '未连接';
const transport = server.config.transport || (server.config.command ? 'stdio' : 'http');
const transportIcon = transport === 'stdio' ? '⚙️' : '🌐';
@@ -2667,7 +2669,7 @@ function renderExternalMCPList(servers) {
<span class="external-mcp-status ${statusClass}">${statusText}</span>
</div>
<div class="external-mcp-item-actions">
${status === 'connected' || status === 'disconnected' ?
${status === 'connected' || status === 'disconnected' || status === 'error' ?
`<button class="btn-small" id="btn-toggle-${escapeHtml(name)}" onclick="toggleExternalMCP('${escapeHtml(name)}', '${status}')" title="${status === 'connected' ? '停止连接' : '启动连接'}">
${status === 'connected' ? '⏸ 停止' : '▶ 启动'}
</button>` :
@@ -2679,6 +2681,10 @@ function renderExternalMCPList(servers) {
<button class="btn-small btn-danger" onclick="deleteExternalMCP('${escapeHtml(name)}')" title="删除配置" ${status === 'connecting' ? 'disabled' : ''}>🗑 删除</button>
</div>
</div>
${status === 'error' && server.error ? `
<div class="external-mcp-error" style="margin: 12px 0; padding: 12px; background: #fee; border-left: 3px solid #f44; border-radius: 4px; color: #c33; font-size: 0.875rem;">
<strong>❌ 连接错误:</strong>${escapeHtml(server.error)}
</div>` : ''}
<div class="external-mcp-item-details">
<div>
<strong>传输模式</strong>