mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-09-26 19:21:52 +02:00
Add files via upload
This commit is contained in:
@@ -293,8 +293,8 @@ func registerListVulnerabilitiesTool(mcpServer *mcp.Server, db *database.DB, log
|
|||||||
},
|
},
|
||||||
"status": map[string]interface{}{
|
"status": map[string]interface{}{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "按状态筛选:open、confirmed、fixed、false_positive",
|
"description": "按状态筛选:open、confirmed、fixed、false_positive、ignored",
|
||||||
"enum": []string{"open", "confirmed", "fixed", "false_positive"},
|
"enum": []string{"open", "confirmed", "fixed", "false_positive", "ignored"},
|
||||||
},
|
},
|
||||||
"q": map[string]interface{}{
|
"q": map[string]interface{}{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
+105
-58
@@ -298,7 +298,7 @@ func (h *ConfigHandler) GetConfig(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取外部MCP工具
|
// 获取外部MCP工具(走缓存,持锁期间通常不阻塞)
|
||||||
if h.externalMCPMgr != nil {
|
if h.externalMCPMgr != nil {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
externalTools := h.getExternalMCPTools(ctx)
|
externalTools := h.getExternalMCPTools(ctx)
|
||||||
@@ -359,9 +359,6 @@ type GetToolsResponse struct {
|
|||||||
|
|
||||||
// GetTools 获取工具列表(支持分页和搜索)
|
// GetTools 获取工具列表(支持分页和搜索)
|
||||||
func (h *ConfigHandler) GetTools(c *gin.Context) {
|
func (h *ConfigHandler) GetTools(c *gin.Context) {
|
||||||
h.mu.RLock()
|
|
||||||
defer h.mu.RUnlock()
|
|
||||||
|
|
||||||
c.Header("Cache-Control", "no-store, no-cache, must-revalidate")
|
c.Header("Cache-Control", "no-store, no-cache, must-revalidate")
|
||||||
|
|
||||||
// 解析分页参数
|
// 解析分页参数
|
||||||
@@ -407,12 +404,37 @@ func (h *ConfigHandler) GetTools(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
includeExternal := true
|
||||||
|
if v := strings.TrimSpace(strings.ToLower(c.Query("include_external"))); v == "0" || v == "false" || v == "no" {
|
||||||
|
includeExternal = false
|
||||||
|
}
|
||||||
|
refreshExternal := false
|
||||||
|
if v := strings.TrimSpace(strings.ToLower(c.Query("refresh_external"))); v == "1" || v == "true" || v == "yes" {
|
||||||
|
refreshExternal = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按外部 MCP 名称筛选(MCP 管理页左侧卡片 → 右侧工具列表联动)
|
||||||
|
externalMCPFilter := strings.TrimSpace(c.Query("external_mcp"))
|
||||||
|
|
||||||
|
// 快照配置后立即释放锁,避免外部 MCP 网络 IO 阻塞整个配置子系统
|
||||||
|
h.mu.RLock()
|
||||||
|
securityTools := append([]config.ToolConfig(nil), h.config.Security.Tools...)
|
||||||
|
roles := h.config.Roles
|
||||||
|
toolDescriptionMode := h.config.Security.ToolDescriptionMode
|
||||||
|
mcpServer := h.mcpServer
|
||||||
|
externalMCPMgr := h.externalMCPMgr
|
||||||
|
h.mu.RUnlock()
|
||||||
|
|
||||||
|
pickDesc := func(shortDesc, fullDesc string) string {
|
||||||
|
return pickToolDescriptionWithMode(toolDescriptionMode, shortDesc, fullDesc)
|
||||||
|
}
|
||||||
|
|
||||||
// 解析角色参数,用于过滤工具并标注启用状态
|
// 解析角色参数,用于过滤工具并标注启用状态
|
||||||
roleName := c.Query("role")
|
roleName := c.Query("role")
|
||||||
var roleToolsSet map[string]bool // 角色配置的工具集合
|
var roleToolsSet map[string]bool // 角色配置的工具集合
|
||||||
var roleUsesAllTools bool = true // 角色是否使用所有工具(默认角色)
|
var roleUsesAllTools bool = true // 角色是否使用所有工具(默认角色)
|
||||||
if roleName != "" && roleName != "默认" && h.config.Roles != nil {
|
if roleName != "" && roleName != "默认" && roles != nil {
|
||||||
if role, exists := h.config.Roles[roleName]; exists && role.Enabled {
|
if role, exists := roles[roleName]; exists && role.Enabled {
|
||||||
if len(role.Tools) > 0 {
|
if len(role.Tools) > 0 {
|
||||||
// 角色配置了工具列表,只使用这些工具
|
// 角色配置了工具列表,只使用这些工具
|
||||||
roleToolsSet = make(map[string]bool)
|
roleToolsSet = make(map[string]bool)
|
||||||
@@ -426,12 +448,12 @@ func (h *ConfigHandler) GetTools(c *gin.Context) {
|
|||||||
|
|
||||||
// 获取所有内部工具并应用搜索过滤
|
// 获取所有内部工具并应用搜索过滤
|
||||||
configToolMap := make(map[string]bool)
|
configToolMap := make(map[string]bool)
|
||||||
allTools := make([]ToolConfigInfo, 0, len(h.config.Security.Tools))
|
allTools := make([]ToolConfigInfo, 0, len(securityTools))
|
||||||
for _, tool := range h.config.Security.Tools {
|
for _, tool := range securityTools {
|
||||||
configToolMap[tool.Name] = true
|
configToolMap[tool.Name] = true
|
||||||
toolInfo := ToolConfigInfo{
|
toolInfo := ToolConfigInfo{
|
||||||
Name: tool.Name,
|
Name: tool.Name,
|
||||||
Description: h.pickToolDescription(tool.ShortDescription, tool.Description),
|
Description: pickDesc(tool.ShortDescription, tool.Description),
|
||||||
Enabled: tool.Enabled,
|
Enabled: tool.Enabled,
|
||||||
IsExternal: false,
|
IsExternal: false,
|
||||||
}
|
}
|
||||||
@@ -479,15 +501,15 @@ func (h *ConfigHandler) GetTools(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 从MCP服务器获取所有已注册的工具(包括直接注册的工具,如知识检索工具)
|
// 从MCP服务器获取所有已注册的工具(包括直接注册的工具,如知识检索工具)
|
||||||
if h.mcpServer != nil {
|
if mcpServer != nil {
|
||||||
mcpTools := h.mcpServer.GetAllTools()
|
mcpTools := mcpServer.GetAllTools()
|
||||||
for _, mcpTool := range mcpTools {
|
for _, mcpTool := range mcpTools {
|
||||||
// 跳过已经在配置文件中的工具(避免重复)
|
// 跳过已经在配置文件中的工具(避免重复)
|
||||||
if configToolMap[mcpTool.Name] {
|
if configToolMap[mcpTool.Name] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
description := h.pickToolDescription(mcpTool.ShortDescription, mcpTool.Description)
|
description := pickDesc(mcpTool.ShortDescription, mcpTool.Description)
|
||||||
|
|
||||||
toolInfo := ToolConfigInfo{
|
toolInfo := ToolConfigInfo{
|
||||||
Name: mcpTool.Name,
|
Name: mcpTool.Name,
|
||||||
@@ -534,11 +556,13 @@ func (h *ConfigHandler) GetTools(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取外部MCP工具
|
// 获取外部MCP工具(可走缓存,不持有 config 锁)
|
||||||
if h.externalMCPMgr != nil {
|
if includeExternal && externalMCPMgr != nil {
|
||||||
// 创建context用于获取外部工具
|
if refreshExternal {
|
||||||
|
externalMCPMgr.InvalidateAllToolCaches()
|
||||||
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
externalTools := h.getExternalMCPTools(ctx)
|
externalTools := h.getExternalMCPToolsWithManager(ctx, externalMCPMgr, pickDesc)
|
||||||
|
|
||||||
// 应用搜索过滤和角色配置
|
// 应用搜索过滤和角色配置
|
||||||
for _, toolInfo := range externalTools {
|
for _, toolInfo := range externalTools {
|
||||||
@@ -585,6 +609,16 @@ func (h *ConfigHandler) GetTools(c *gin.Context) {
|
|||||||
// 注意:这里我们不直接过滤掉工具,而是保留所有工具,但通过 role_enabled 字段标注状态
|
// 注意:这里我们不直接过滤掉工具,而是保留所有工具,但通过 role_enabled 字段标注状态
|
||||||
// 这样前端可以显示所有工具,并标注哪些工具在当前角色中可用
|
// 这样前端可以显示所有工具,并标注哪些工具在当前角色中可用
|
||||||
|
|
||||||
|
if externalMCPFilter != "" {
|
||||||
|
filtered := make([]ToolConfigInfo, 0)
|
||||||
|
for _, tool := range allTools {
|
||||||
|
if tool.IsExternal && tool.ExternalMCP == externalMCPFilter {
|
||||||
|
filtered = append(filtered, tool)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allTools = filtered
|
||||||
|
}
|
||||||
|
|
||||||
// 统一按名称排序后再分页,避免配置文件中顺序导致「全部」与「仅已启用」前几页看起来完全一致
|
// 统一按名称排序后再分页,避免配置文件中顺序导致「全部」与「仅已启用」前几页看起来完全一致
|
||||||
sort.SliceStable(allTools, func(i, j int) bool {
|
sort.SliceStable(allTools, func(i, j int) bool {
|
||||||
key := func(t ToolConfigInfo) string {
|
key := func(t ToolConfigInfo) string {
|
||||||
@@ -1906,50 +1940,52 @@ func setFloatInMap(mapNode *yaml.Node, key string, value float64) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getExternalMCPTools 获取外部MCP工具列表(公共方法)
|
// getExternalMCPTools 获取外部MCP工具列表(公共方法)
|
||||||
// 返回 ToolConfigInfo 列表,已处理启用状态和描述信息
|
|
||||||
func (h *ConfigHandler) getExternalMCPTools(ctx context.Context) []ToolConfigInfo {
|
func (h *ConfigHandler) getExternalMCPTools(ctx context.Context) []ToolConfigInfo {
|
||||||
var result []ToolConfigInfo
|
|
||||||
|
|
||||||
if h.externalMCPMgr == nil {
|
if h.externalMCPMgr == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return h.getExternalMCPToolsWithManager(ctx, h.externalMCPMgr, h.pickToolDescription)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getExternalMCPToolsWithManager 获取外部 MCP 工具(不持有 config 锁,供 GetTools 等热路径使用)
|
||||||
|
func (h *ConfigHandler) getExternalMCPToolsWithManager(
|
||||||
|
ctx context.Context,
|
||||||
|
mgr *mcp.ExternalMCPManager,
|
||||||
|
pickDesc func(shortDesc, fullDesc string) string,
|
||||||
|
) []ToolConfigInfo {
|
||||||
|
var result []ToolConfigInfo
|
||||||
|
if mgr == nil {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用较短的超时时间(5秒)进行快速失败,避免阻塞页面加载
|
|
||||||
timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
externalTools, err := h.externalMCPMgr.GetAllTools(timeoutCtx)
|
externalTools, err := mgr.GetAllTools(timeoutCtx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// 记录警告但不阻塞,继续返回已缓存的工具(如果有)
|
|
||||||
h.logger.Warn("获取外部MCP工具失败(可能连接断开),尝试返回缓存的工具",
|
h.logger.Warn("获取外部MCP工具失败(可能连接断开),尝试返回缓存的工具",
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
zap.String("hint", "如果外部MCP工具未显示,请检查连接状态或点击刷新按钮"),
|
zap.String("hint", "如果外部MCP工具未显示,请检查连接状态或点击刷新按钮"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果获取到了工具(即使有错误),继续处理
|
|
||||||
if len(externalTools) == 0 {
|
if len(externalTools) == 0 {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
externalMCPConfigs := h.externalMCPMgr.GetConfigs()
|
externalMCPConfigs := mgr.GetConfigs()
|
||||||
|
|
||||||
for _, externalTool := range externalTools {
|
for _, externalTool := range externalTools {
|
||||||
// 解析工具名称:mcpName::toolName
|
|
||||||
mcpName, actualToolName := h.parseExternalToolName(externalTool.Name)
|
mcpName, actualToolName := h.parseExternalToolName(externalTool.Name)
|
||||||
if mcpName == "" || actualToolName == "" {
|
if mcpName == "" || actualToolName == "" {
|
||||||
continue // 跳过格式不正确的工具
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算启用状态
|
enabled := h.calculateExternalToolEnabledWithManager(mcpName, actualToolName, externalMCPConfigs, mgr)
|
||||||
enabled := h.calculateExternalToolEnabled(mcpName, actualToolName, externalMCPConfigs)
|
|
||||||
|
|
||||||
// 处理描述信息
|
|
||||||
description := h.pickToolDescription(externalTool.ShortDescription, externalTool.Description)
|
|
||||||
|
|
||||||
result = append(result, ToolConfigInfo{
|
result = append(result, ToolConfigInfo{
|
||||||
Name: actualToolName,
|
Name: actualToolName,
|
||||||
Description: description,
|
Description: pickDesc(externalTool.ShortDescription, externalTool.Description),
|
||||||
Enabled: enabled,
|
Enabled: enabled,
|
||||||
IsExternal: true,
|
IsExternal: true,
|
||||||
ExternalMCP: mcpName,
|
ExternalMCP: mcpName,
|
||||||
@@ -1970,40 +2006,48 @@ func (h *ConfigHandler) parseExternalToolName(fullName string) (mcpName, toolNam
|
|||||||
|
|
||||||
// calculateExternalToolEnabled 计算外部工具的启用状态
|
// calculateExternalToolEnabled 计算外部工具的启用状态
|
||||||
func (h *ConfigHandler) calculateExternalToolEnabled(mcpName, toolName string, configs map[string]config.ExternalMCPServerConfig) bool {
|
func (h *ConfigHandler) calculateExternalToolEnabled(mcpName, toolName string, configs map[string]config.ExternalMCPServerConfig) bool {
|
||||||
|
return h.calculateExternalToolEnabledWithManager(mcpName, toolName, configs, h.externalMCPMgr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ConfigHandler) calculateExternalToolEnabledWithManager(
|
||||||
|
mcpName, toolName string,
|
||||||
|
configs map[string]config.ExternalMCPServerConfig,
|
||||||
|
mgr *mcp.ExternalMCPManager,
|
||||||
|
) bool {
|
||||||
cfg, exists := configs[mcpName]
|
cfg, exists := configs[mcpName]
|
||||||
if !exists {
|
if !exists {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 首先检查外部MCP是否启用
|
|
||||||
if !cfg.ExternalMCPEnable {
|
if !cfg.ExternalMCPEnable {
|
||||||
return false // MCP未启用,所有工具都禁用
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// MCP已启用,检查单个工具的启用状态
|
if cfg.ToolEnabled != nil {
|
||||||
// 如果ToolEnabled为空或未设置该工具,默认为启用(向后兼容)
|
if toolEnabled, exists := cfg.ToolEnabled[toolName]; exists && !toolEnabled {
|
||||||
if cfg.ToolEnabled == nil {
|
|
||||||
// 未设置工具状态,默认为启用
|
|
||||||
} else if toolEnabled, exists := cfg.ToolEnabled[toolName]; exists {
|
|
||||||
// 使用配置的工具状态
|
|
||||||
if !toolEnabled {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 工具未在配置中,默认为启用
|
|
||||||
|
|
||||||
// 最后检查外部MCP是否已连接
|
if mgr == nil {
|
||||||
client, exists := h.externalMCPMgr.GetClient(mcpName)
|
return false
|
||||||
|
}
|
||||||
|
client, exists := mgr.GetClient(mcpName)
|
||||||
if !exists || !client.IsConnected() {
|
if !exists || !client.IsConnected() {
|
||||||
return false // 未连接时视为禁用
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// pickToolDescription 根据 security.tool_description_mode 选择 short 或 full 描述并限制长度
|
// pickToolDescription 根据 security.tool_description_mode 选择 short 或 full 描述并限制长度。
|
||||||
|
// 调用方若已持有 h.mu 读锁,须直接读 mode 并调用 pickToolDescriptionWithMode,避免嵌套 RLock 死锁。
|
||||||
func (h *ConfigHandler) pickToolDescription(shortDesc, fullDesc string) string {
|
func (h *ConfigHandler) pickToolDescription(shortDesc, fullDesc string) string {
|
||||||
useFull := strings.TrimSpace(strings.ToLower(h.config.Security.ToolDescriptionMode)) == "full"
|
return pickToolDescriptionWithMode(h.config.Security.ToolDescriptionMode, shortDesc, fullDesc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pickToolDescriptionWithMode(mode, shortDesc, fullDesc string) string {
|
||||||
|
useFull := strings.TrimSpace(strings.ToLower(mode)) == "full"
|
||||||
description := shortDesc
|
description := shortDesc
|
||||||
if useFull {
|
if useFull {
|
||||||
description = fullDesc
|
description = fullDesc
|
||||||
@@ -2018,23 +2062,22 @@ func (h *ConfigHandler) pickToolDescription(shortDesc, fullDesc string) string {
|
|||||||
|
|
||||||
// GetToolSchema 获取单个工具的 inputSchema(按需加载,避免列表接口返回大量 schema 数据)
|
// GetToolSchema 获取单个工具的 inputSchema(按需加载,避免列表接口返回大量 schema 数据)
|
||||||
func (h *ConfigHandler) GetToolSchema(c *gin.Context) {
|
func (h *ConfigHandler) GetToolSchema(c *gin.Context) {
|
||||||
h.mu.RLock()
|
|
||||||
defer h.mu.RUnlock()
|
|
||||||
|
|
||||||
toolName := c.Param("name")
|
toolName := c.Param("name")
|
||||||
if toolName == "" {
|
if toolName == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "工具名称不能为空"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "工具名称不能为空"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否为外部工具(格式:mcpName::toolName)
|
|
||||||
externalMCP := c.Query("external_mcp")
|
externalMCP := c.Query("external_mcp")
|
||||||
if externalMCP != "" {
|
if externalMCP != "" {
|
||||||
// 外部 MCP 工具
|
h.mu.RLock()
|
||||||
if h.externalMCPMgr != nil {
|
externalMCPMgr := h.externalMCPMgr
|
||||||
|
h.mu.RUnlock()
|
||||||
|
|
||||||
|
if externalMCPMgr != nil {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
externalTools, _ := h.externalMCPMgr.GetAllTools(ctx)
|
externalTools, _ := externalMCPMgr.GetAllTools(ctx)
|
||||||
fullName := externalMCP + "::" + toolName
|
fullName := externalMCP + "::" + toolName
|
||||||
for _, t := range externalTools {
|
for _, t := range externalTools {
|
||||||
if t.Name == fullName {
|
if t.Name == fullName {
|
||||||
@@ -2047,8 +2090,12 @@ func (h *ConfigHandler) GetToolSchema(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 内部工具:从 YAML 配置的 Parameters 构建
|
h.mu.RLock()
|
||||||
for _, tool := range h.config.Security.Tools {
|
securityTools := append([]config.ToolConfig(nil), h.config.Security.Tools...)
|
||||||
|
mcpServer := h.mcpServer
|
||||||
|
h.mu.RUnlock()
|
||||||
|
|
||||||
|
for _, tool := range securityTools {
|
||||||
if tool.Name == toolName {
|
if tool.Name == toolName {
|
||||||
c.JSON(http.StatusOK, gin.H{"input_schema": buildInputSchemaFromParams(tool.Parameters)})
|
c.JSON(http.StatusOK, gin.H{"input_schema": buildInputSchemaFromParams(tool.Parameters)})
|
||||||
return
|
return
|
||||||
@@ -2056,8 +2103,8 @@ func (h *ConfigHandler) GetToolSchema(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MCP 注册工具(如知识检索)
|
// MCP 注册工具(如知识检索)
|
||||||
if h.mcpServer != nil {
|
if mcpServer != nil {
|
||||||
for _, mt := range h.mcpServer.GetAllTools() {
|
for _, mt := range mcpServer.GetAllTools() {
|
||||||
if mt.Name == toolName {
|
if mt.Name == toolName {
|
||||||
c.JSON(http.StatusOK, gin.H{"input_schema": mt.InputSchema})
|
c.JSON(http.StatusOK, gin.H{"input_schema": mt.InputSchema})
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
|
|||||||
"status": map[string]interface{}{
|
"status": map[string]interface{}{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "状态",
|
"description": "状态",
|
||||||
"enum": []string{"open", "closed", "fixed"},
|
"enum": []string{"open", "confirmed", "fixed", "false_positive", "ignored"},
|
||||||
},
|
},
|
||||||
"target": map[string]interface{}{
|
"target": map[string]interface{}{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -575,7 +575,7 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
|
|||||||
"status": map[string]interface{}{
|
"status": map[string]interface{}{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "状态",
|
"description": "状态",
|
||||||
"enum": []string{"open", "closed", "fixed"},
|
"enum": []string{"open", "confirmed", "fixed", "false_positive", "ignored"},
|
||||||
},
|
},
|
||||||
"type": map[string]interface{}{
|
"type": map[string]interface{}{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
Reference in New Issue
Block a user