Files
Enterprise-Knowledge-Base/internal/handler/mcp_service.go
T
nullkey c753c67608 docs(api): add and fix swag annotations on handlers
将 swag 注解覆盖率从 ~88% 提升到 100%,并修复审计中发现的若干既有
注解 bug。

新增注解(24 个):
- im.go: 4 (UpdateIMChannel/DeleteIMChannel/ToggleIMChannel/IMCallback)
  其中 IMCallback 同时注册 GET 和 POST,两个方法都加了 @Router 声明。
- web_search.go: 1 (GetProviders)
- web_search_provider.go: 6
- wechat_qrcode.go: 2
- weknoracloud.go: 2
- knowledge.go: 2 (MoveKnowledge/GetKnowledgeMoveProgress)
- knowledgebase.go: 1 (ListMoveTargets)
- organization.go: 3 (ListSharedAgents/ListOrgAgentShares/RemoveAgentShare)
- mcp_service.go: 2 (SetMCPToolApproval/ResolveToolApproval)
- chunker_debug.go: 1 (PreviewChunking)

修复既有注解 bug(12 处):
- initialization.go: 10 个 @Router 路径与真实路由不符
  (e.g. /initialization/kb/{kbId}/config → /initialization/config/{kbId}
   /initialization/fabri/tag GET → /initialization/extract/fabri-tag POST)
- session/stream.go: ContinueStream 路径
  /sessions/{session_id}/continue → /sessions/continue-stream/{session_id}
- mcp_service.go: ResolveToolApproval 请求体 {approve: bool}
  → {decision: "approve"|"reject", reason?, modified_args?}

收敛响应 schema 到已有命名 struct(之前用 map[string]interface{}):
- knowledge.go MoveKnowledge → handler.MoveKnowledgeResponse
- knowledge.go GetKnowledgeMoveProgress → types.KnowledgeMoveProgress
- web_search_provider.go GetProvider/UpdateProvider → types.WebSearchProviderEntity
- initialization.go InitializeByKB → handler.InitializationRequest
- initialization.go 三个 model-test endpoint → handler.ModelTestRequest

修正 organization.go 中 errors.AppError 应使用 apperrors 别名(该文件
stdlib errors 未别名化)。

不补 /files 和 /api/v1/files/presigned 内联闭包路由(文件服务,非业务 REST)。

Refs #890 #1049 #1168
2026-05-12 13:16:58 +08:00

625 lines
22 KiB
Go

package handler
import (
"encoding/json"
stderrors "errors"
"fmt"
"net/http"
"strings"
"github.com/Tencent/WeKnora/internal/agent/approval"
"github.com/Tencent/WeKnora/internal/errors"
"github.com/Tencent/WeKnora/internal/logger"
"github.com/Tencent/WeKnora/internal/types"
"github.com/Tencent/WeKnora/internal/types/interfaces"
secutils "github.com/Tencent/WeKnora/internal/utils"
"github.com/gin-gonic/gin"
)
// MCPServiceHandler handles MCP service related HTTP requests
type MCPServiceHandler struct {
mcpServiceService interfaces.MCPServiceService
mcpToolApprovalService interfaces.MCPToolApprovalService
toolApprovalGate *approval.Gate
}
// NewMCPServiceHandler creates a new MCP service handler
func NewMCPServiceHandler(
mcpServiceService interfaces.MCPServiceService,
mcpToolApprovalService interfaces.MCPToolApprovalService,
toolApprovalGate *approval.Gate,
) *MCPServiceHandler {
return &MCPServiceHandler{
mcpServiceService: mcpServiceService,
mcpToolApprovalService: mcpToolApprovalService,
toolApprovalGate: toolApprovalGate,
}
}
// CreateMCPService godoc
// @Summary 创建MCP服务
// @Description 创建新的MCP服务配置
// @Tags MCP服务
// @Accept json
// @Produce json
// @Param request body types.MCPService true "MCP服务配置"
// @Success 200 {object} map[string]interface{} "创建的MCP服务"
// @Failure 400 {object} errors.AppError "请求参数错误"
// @Security Bearer
// @Security ApiKeyAuth
// @Router /mcp-services [post]
func (h *MCPServiceHandler) CreateMCPService(c *gin.Context) {
ctx := c.Request.Context()
var service types.MCPService
if err := c.ShouldBindJSON(&service); err != nil {
logger.Error(ctx, "Failed to parse MCP service request", err)
c.Error(errors.NewBadRequestError(err.Error()))
return
}
tenantID := c.GetUint64(types.TenantIDContextKey.String())
if tenantID == 0 {
logger.Error(ctx, "Tenant ID is empty")
c.Error(errors.NewBadRequestError("Tenant ID cannot be empty"))
return
}
service.TenantID = tenantID
// SSRF validation for MCP service URL
if service.URL != nil && *service.URL != "" {
if err := secutils.ValidateURLForSSRF(*service.URL); err != nil {
logger.Warnf(ctx, "SSRF validation failed for MCP service URL: %v", err)
c.Error(errors.NewBadRequestError(fmt.Sprintf("MCP service URL 未通过安全校验: %v", err)))
return
}
}
if err := h.mcpServiceService.CreateMCPService(ctx, &service); err != nil {
logger.ErrorWithFields(ctx, err, map[string]interface{}{"service_name": secutils.SanitizeForLog(service.Name)})
c.Error(errors.NewInternalServerError("Failed to create MCP service: " + err.Error()))
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": service,
})
}
// ListMCPServices godoc
// @Summary 获取MCP服务列表
// @Description 获取当前租户的所有MCP服务
// @Tags MCP服务
// @Accept json
// @Produce json
// @Success 200 {object} map[string]interface{} "MCP服务列表"
// @Failure 400 {object} errors.AppError "请求参数错误"
// @Security Bearer
// @Security ApiKeyAuth
// @Router /mcp-services [get]
func (h *MCPServiceHandler) ListMCPServices(c *gin.Context) {
ctx := c.Request.Context()
tenantID := c.GetUint64(types.TenantIDContextKey.String())
if tenantID == 0 {
logger.Error(ctx, "Tenant ID is empty")
c.Error(errors.NewBadRequestError("Tenant ID cannot be empty"))
return
}
services, err := h.mcpServiceService.ListMCPServices(ctx, tenantID)
if err != nil {
logger.ErrorWithFields(ctx, err, map[string]interface{}{"tenant_id": tenantID})
c.Error(errors.NewInternalServerError("Failed to list MCP services: " + err.Error()))
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": services,
})
}
// GetMCPService godoc
// @Summary 获取MCP服务详情
// @Description 根据ID获取MCP服务详情
// @Tags MCP服务
// @Accept json
// @Produce json
// @Param id path string true "MCP服务ID"
// @Success 200 {object} map[string]interface{} "MCP服务详情"
// @Failure 404 {object} errors.AppError "服务不存在"
// @Security Bearer
// @Security ApiKeyAuth
// @Router /mcp-services/{id} [get]
func (h *MCPServiceHandler) GetMCPService(c *gin.Context) {
ctx := c.Request.Context()
serviceID := secutils.SanitizeForLog(c.Param("id"))
tenantID := c.GetUint64(types.TenantIDContextKey.String())
if tenantID == 0 {
logger.Error(ctx, "Tenant ID is empty")
c.Error(errors.NewBadRequestError("Tenant ID cannot be empty"))
return
}
service, err := h.mcpServiceService.GetMCPServiceByID(ctx, tenantID, serviceID)
if err != nil {
logger.ErrorWithFields(ctx, err, map[string]interface{}{"service_id": secutils.SanitizeForLog(serviceID)})
c.Error(errors.NewNotFoundError("MCP service not found"))
return
}
// Hide sensitive information for builtin MCP services
responseService := service
if service.IsBuiltin {
responseService = service.HideSensitiveInfo()
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": responseService,
})
}
// UpdateMCPService godoc
// @Summary 更新MCP服务
// @Description 更新MCP服务配置
// @Tags MCP服务
// @Accept json
// @Produce json
// @Param id path string true "MCP服务ID"
// @Param request body object true "更新字段"
// @Success 200 {object} map[string]interface{} "更新后的MCP服务"
// @Failure 400 {object} errors.AppError "请求参数错误"
// @Security Bearer
// @Security ApiKeyAuth
// @Router /mcp-services/{id} [put]
func (h *MCPServiceHandler) UpdateMCPService(c *gin.Context) {
ctx := c.Request.Context()
serviceID := secutils.SanitizeForLog(c.Param("id"))
tenantID := c.GetUint64(types.TenantIDContextKey.String())
if tenantID == 0 {
logger.Error(ctx, "Tenant ID is empty")
c.Error(errors.NewBadRequestError("Tenant ID cannot be empty"))
return
}
// Use map to handle partial updates, including false values
var updateData map[string]interface{}
if err := c.ShouldBindJSON(&updateData); err != nil {
logger.Error(ctx, "Failed to parse MCP service update request", err)
c.Error(errors.NewBadRequestError(err.Error()))
return
}
// Convert map to MCPService struct for validation and processing
var service types.MCPService
service.ID = serviceID
service.TenantID = tenantID
// Track which fields are being updated
updateFields := make(map[string]bool)
// Map the update data to service struct
if name, ok := updateData["name"].(string); ok {
service.Name = name
updateFields["name"] = true
}
if desc, ok := updateData["description"].(string); ok {
service.Description = desc
updateFields["description"] = true
}
if enabled, ok := updateData["enabled"].(bool); ok {
if enabled {
service.Enabled = true
} else {
service.Enabled = false
}
updateFields["enabled"] = true
}
if transportType, ok := updateData["transport_type"].(string); ok {
service.TransportType = types.MCPTransportType(transportType)
}
if url, ok := updateData["url"].(string); ok && url != "" {
service.URL = &url
} else if _, exists := updateData["url"]; exists {
// Explicitly set to nil if provided as null/empty
service.URL = nil
}
// SSRF validation for updated MCP service URL
if service.URL != nil && *service.URL != "" {
if err := secutils.ValidateURLForSSRF(*service.URL); err != nil {
logger.Warnf(ctx, "SSRF validation failed for MCP service URL: %v", err)
c.Error(errors.NewBadRequestError(fmt.Sprintf("MCP service URL 未通过安全校验: %v", err)))
return
}
}
if stdioConfig, ok := updateData["stdio_config"].(map[string]interface{}); ok {
config := &types.MCPStdioConfig{}
if command, ok := stdioConfig["command"].(string); ok {
config.Command = command
}
if args, ok := stdioConfig["args"].([]interface{}); ok {
config.Args = make([]string, len(args))
for i, arg := range args {
if str, ok := arg.(string); ok {
config.Args[i] = str
}
}
}
service.StdioConfig = config
}
if envVars, ok := updateData["env_vars"].(map[string]interface{}); ok {
service.EnvVars = make(types.MCPEnvVars)
for k, v := range envVars {
if str, ok := v.(string); ok {
service.EnvVars[k] = str
}
}
}
if headers, ok := updateData["headers"].(map[string]interface{}); ok {
service.Headers = make(types.MCPHeaders)
for k, v := range headers {
if str, ok := v.(string); ok {
service.Headers[k] = str
}
}
}
if authConfig, ok := updateData["auth_config"].(map[string]interface{}); ok {
service.AuthConfig = &types.MCPAuthConfig{}
if apiKey, ok := authConfig["api_key"].(string); ok {
service.AuthConfig.APIKey = apiKey
}
if token, ok := authConfig["token"].(string); ok {
service.AuthConfig.Token = token
}
}
if advancedConfig, ok := updateData["advanced_config"].(map[string]interface{}); ok {
service.AdvancedConfig = &types.MCPAdvancedConfig{}
if timeout, ok := advancedConfig["timeout"].(float64); ok {
service.AdvancedConfig.Timeout = int(timeout)
}
if retryCount, ok := advancedConfig["retry_count"].(float64); ok {
service.AdvancedConfig.RetryCount = int(retryCount)
}
if retryDelay, ok := advancedConfig["retry_delay"].(float64); ok {
service.AdvancedConfig.RetryDelay = int(retryDelay)
}
}
if err := h.mcpServiceService.UpdateMCPService(ctx, &service); err != nil {
logger.ErrorWithFields(ctx, err, map[string]interface{}{"service_id": secutils.SanitizeForLog(serviceID)})
c.Error(errors.NewInternalServerError("Failed to update MCP service: " + err.Error()))
return
}
logger.Infof(ctx, "MCP service updated successfully: %s", secutils.SanitizeForLog(serviceID))
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": service,
})
}
// DeleteMCPService godoc
// @Summary 删除MCP服务
// @Description 删除指定的MCP服务
// @Tags MCP服务
// @Accept json
// @Produce json
// @Param id path string true "MCP服务ID"
// @Success 200 {object} map[string]interface{} "删除成功"
// @Failure 500 {object} errors.AppError "服务器错误"
// @Security Bearer
// @Security ApiKeyAuth
// @Router /mcp-services/{id} [delete]
func (h *MCPServiceHandler) DeleteMCPService(c *gin.Context) {
ctx := c.Request.Context()
serviceID := secutils.SanitizeForLog(c.Param("id"))
tenantID := c.GetUint64(types.TenantIDContextKey.String())
if tenantID == 0 {
logger.Error(ctx, "Tenant ID is empty")
c.Error(errors.NewBadRequestError("Tenant ID cannot be empty"))
return
}
if err := h.mcpServiceService.DeleteMCPService(ctx, tenantID, serviceID); err != nil {
logger.ErrorWithFields(ctx, err, map[string]interface{}{"service_id": secutils.SanitizeForLog(serviceID)})
c.Error(errors.NewInternalServerError("Failed to delete MCP service: " + err.Error()))
return
}
logger.Infof(ctx, "MCP service deleted successfully: %s", secutils.SanitizeForLog(serviceID))
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "MCP service deleted successfully",
})
}
// TestMCPService godoc
// @Summary 测试MCP服务连接
// @Description 测试MCP服务是否可以正常连接
// @Tags MCP服务
// @Accept json
// @Produce json
// @Param id path string true "MCP服务ID"
// @Success 200 {object} map[string]interface{} "测试结果"
// @Failure 400 {object} errors.AppError "请求参数错误"
// @Security Bearer
// @Security ApiKeyAuth
// @Router /mcp-services/{id}/test [post]
func (h *MCPServiceHandler) TestMCPService(c *gin.Context) {
ctx := c.Request.Context()
serviceID := secutils.SanitizeForLog(c.Param("id"))
tenantID := c.GetUint64(types.TenantIDContextKey.String())
if tenantID == 0 {
logger.Error(ctx, "Tenant ID is empty")
c.Error(errors.NewBadRequestError("Tenant ID cannot be empty"))
return
}
logger.Infof(ctx, "Testing MCP service: %s", secutils.SanitizeForLog(serviceID))
result, err := h.mcpServiceService.TestMCPService(ctx, tenantID, serviceID)
if err != nil {
logger.ErrorWithFields(ctx, err, map[string]interface{}{"service_id": secutils.SanitizeForLog(serviceID)})
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": types.MCPTestResult{
Success: false,
Message: "Test failed: " + err.Error(),
},
})
return
}
logger.Infof(ctx, "MCP service test completed: %s, success: %v", secutils.SanitizeForLog(serviceID), result.Success)
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": result,
})
}
// GetMCPServiceTools godoc
// @Summary 获取MCP服务工具列表
// @Description 获取MCP服务提供的工具列表
// @Tags MCP服务
// @Accept json
// @Produce json
// @Param id path string true "MCP服务ID"
// @Success 200 {object} map[string]interface{} "工具列表"
// @Failure 500 {object} errors.AppError "服务器错误"
// @Security Bearer
// @Security ApiKeyAuth
// @Router /mcp-services/{id}/tools [get]
func (h *MCPServiceHandler) GetMCPServiceTools(c *gin.Context) {
ctx := c.Request.Context()
serviceID := secutils.SanitizeForLog(c.Param("id"))
tenantID := c.GetUint64(types.TenantIDContextKey.String())
if tenantID == 0 {
logger.Error(ctx, "Tenant ID is empty")
c.Error(errors.NewBadRequestError("Tenant ID cannot be empty"))
return
}
tools, err := h.mcpServiceService.GetMCPServiceTools(ctx, tenantID, serviceID)
if err != nil {
logger.ErrorWithFields(ctx, err, map[string]interface{}{"service_id": secutils.SanitizeForLog(serviceID)})
c.Error(errors.NewInternalServerError("Failed to get MCP service tools: " + err.Error()))
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": tools,
})
}
// GetMCPServiceResources godoc
// @Summary 获取MCP服务资源列表
// @Description 获取MCP服务提供的资源列表
// @Tags MCP服务
// @Accept json
// @Produce json
// @Param id path string true "MCP服务ID"
// @Success 200 {object} map[string]interface{} "资源列表"
// @Failure 500 {object} errors.AppError "服务器错误"
// @Security Bearer
// @Security ApiKeyAuth
// @Router /mcp-services/{id}/resources [get]
func (h *MCPServiceHandler) GetMCPServiceResources(c *gin.Context) {
ctx := c.Request.Context()
serviceID := secutils.SanitizeForLog(c.Param("id"))
tenantID := c.GetUint64(types.TenantIDContextKey.String())
if tenantID == 0 {
logger.Error(ctx, "Tenant ID is empty")
c.Error(errors.NewBadRequestError("Tenant ID cannot be empty"))
return
}
resources, err := h.mcpServiceService.GetMCPServiceResources(ctx, tenantID, serviceID)
if err != nil {
logger.ErrorWithFields(ctx, err, map[string]interface{}{"service_id": secutils.SanitizeForLog(serviceID)})
c.Error(errors.NewInternalServerError("Failed to get MCP service resources: " + err.Error()))
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": resources,
})
}
// ListMCPToolApprovals returns persisted require_approval flags for tools on an MCP service.
func (h *MCPServiceHandler) ListMCPToolApprovals(c *gin.Context) {
ctx := c.Request.Context()
serviceID := secutils.SanitizeForLog(c.Param("id"))
tenantID := c.GetUint64(types.TenantIDContextKey.String())
if tenantID == 0 {
c.Error(errors.NewBadRequestError("Tenant ID cannot be empty"))
return
}
if h.mcpToolApprovalService == nil {
c.Error(errors.NewInternalServerError("MCP tool approval is not configured"))
return
}
rows, err := h.mcpToolApprovalService.ListByService(ctx, tenantID, serviceID)
if err != nil {
// Distinguish "service not found" from internal errors so the client
// gets an accurate status code instead of an opaque 404.
if strings.Contains(err.Error(), "not found") {
c.Error(errors.NewNotFoundError(err.Error()))
return
}
logger.ErrorWithFields(ctx, err, map[string]interface{}{"service_id": serviceID})
c.Error(errors.NewInternalServerError(err.Error()))
return
}
c.JSON(http.StatusOK, gin.H{"success": true, "data": rows})
}
type setMCPToolApprovalBody struct {
RequireApproval bool `json:"require_approval"`
}
// SetMCPToolApproval sets whether a tool requires human approval before the agent may call it.
//
// SetMCPToolApproval godoc
// @Summary 设置 MCP 工具人工审批策略
// @Description 为指定 MCP 服务下的某个工具设置/更新审批要求
// @Tags MCP服务
// @Accept json
// @Produce json
// @Param id path string true "MCP 服务 ID"
// @Param tool_name path string true "工具名"
// @Param request body map[string]interface{} true "{require_approval: bool}"
// @Success 200 {object} map[string]interface{} "更新结果"
// @Failure 400 {object} errors.AppError "请求参数错误"
// @Failure 404 {object} errors.AppError "MCP 服务或工具不存在"
// @Security Bearer
// @Security ApiKeyAuth
// @Router /mcp-services/{id}/tool-approvals/{tool_name} [put]
func (h *MCPServiceHandler) SetMCPToolApproval(c *gin.Context) {
ctx := c.Request.Context()
serviceID := secutils.SanitizeForLog(c.Param("id"))
// Gin already URL-decodes path params; do not call url.PathUnescape again
// or names containing literal "%" become corrupted.
toolName := c.Param("tool_name")
tenantID := c.GetUint64(types.TenantIDContextKey.String())
if tenantID == 0 {
c.Error(errors.NewBadRequestError("Tenant ID cannot be empty"))
return
}
if h.mcpToolApprovalService == nil {
c.Error(errors.NewInternalServerError("MCP tool approval is not configured"))
return
}
var body setMCPToolApprovalBody
if err := c.ShouldBindJSON(&body); err != nil {
c.Error(errors.NewBadRequestError(err.Error()))
return
}
if err := h.mcpToolApprovalService.SetRequireApproval(ctx, tenantID, serviceID, toolName, body.RequireApproval); err != nil {
c.Error(errors.NewInternalServerError(err.Error()))
return
}
c.JSON(http.StatusOK, gin.H{"success": true})
}
type resolveToolApprovalBody struct {
Decision string `json:"decision" binding:"required"` // approve | reject
ModifiedArgs json.RawMessage `json:"modified_args"`
Reason string `json:"reason"`
}
// ResolveToolApproval completes a pending MCP tool approval (agent execution resumes).
//
// ResolveToolApproval godoc
// @Summary 处理 MCP 工具调用待审批请求
// @Description 用户审批通过或驳回一次工具调用(用于 Agent 阻塞等待审批的场景)
// @Tags MCP服务
// @Accept json
// @Produce json
// @Param pending_id path string true "待审批记录 ID"
// @Param request body map[string]interface{} true "{decision: \"approve\"|\"reject\", reason?: string, modified_args?: object}"
// @Success 200 {object} map[string]interface{} "审批结果"
// @Failure 400 {object} errors.AppError "请求参数错误"
// @Failure 404 {object} errors.AppError "待审批记录不存在"
// @Security Bearer
// @Security ApiKeyAuth
// @Router /agent/tool-approvals/{pending_id} [post]
func (h *MCPServiceHandler) ResolveToolApproval(c *gin.Context) {
ctx := c.Request.Context()
pendingID := c.Param("pending_id")
tenantID := c.GetUint64(types.TenantIDContextKey.String())
if tenantID == 0 {
c.Error(errors.NewBadRequestError("Tenant ID cannot be empty"))
return
}
if h.toolApprovalGate == nil {
c.Error(errors.NewInternalServerError("Tool approval gate is not configured"))
return
}
var body resolveToolApprovalBody
if err := c.ShouldBindJSON(&body); err != nil {
c.Error(errors.NewBadRequestError(err.Error()))
return
}
dec := approval.Decision{Reason: body.Reason}
switch body.Decision {
case "approve":
dec.Approved = true
// Reject "null" / non-object payloads up front. Without this, "null"
// (4 bytes) passes the len>0 check and the downstream tool sees a nil
// argument map, silently losing the original args.
trimmed := strings.TrimSpace(string(body.ModifiedArgs))
if len(trimmed) > 0 && trimmed != "null" {
var probe map[string]interface{}
if err := json.Unmarshal(body.ModifiedArgs, &probe); err != nil || probe == nil {
c.Error(errors.NewBadRequestError("modified_args must be a non-null JSON object"))
return
}
dec.ModifiedArgs = body.ModifiedArgs
}
case "reject":
dec.Approved = false
default:
c.Error(errors.NewBadRequestError("decision must be approve or reject"))
return
}
userID, _ := c.Get(types.UserIDContextKey.String())
userIDStr, _ := userID.(string)
// Reject calls without an authenticated user up front. The gate's
// per-user authorization is fail-close, but surfacing 401 here gives
// a clearer signal that auth middleware did not populate the context.
if strings.TrimSpace(userIDStr) == "" {
c.Error(errors.NewUnauthorizedError("authenticated user required to resolve tool approval"))
return
}
if err := h.toolApprovalGate.Resolve(tenantID, userIDStr, pendingID, dec); err != nil {
switch {
case stderrors.Is(err, approval.ErrPendingNotFound):
c.Error(errors.NewNotFoundError("pending approval not found or already completed"))
case stderrors.Is(err, approval.ErrAlreadyResolved):
c.Error(errors.NewBadRequestError("pending approval already resolved (timeout / cancel raced your action)"))
case stderrors.Is(err, approval.ErrTenantMismatch):
c.Error(errors.NewBadRequestError("tenant mismatch"))
case stderrors.Is(err, approval.ErrUserMismatch):
c.Error(errors.NewBadRequestError("user mismatch: only the session owner may resolve this approval"))
default:
logger.ErrorWithFields(ctx, err, map[string]interface{}{"pending_id": pendingID})
c.Error(errors.NewInternalServerError(err.Error()))
}
return
}
c.JSON(http.StatusOK, gin.H{"success": true})
}