mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-27 01:23:55 +08:00
perf:
1、优化小智后端智能体搜索条件逻辑 2、优化工具调用准确率问题,修改工具提示词,去除非必要的few-shot逻辑 3、优化工具描述内容
This commit is contained in:
+12
-20
@@ -140,40 +140,32 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
|||||||
QueryWrapper<AgentEntity> queryWrapper = new QueryWrapper<>();
|
QueryWrapper<AgentEntity> queryWrapper = new QueryWrapper<>();
|
||||||
queryWrapper.eq("user_id", userId).orderByDesc("created_at");
|
queryWrapper.eq("user_id", userId).orderByDesc("created_at");
|
||||||
|
|
||||||
// 如果有搜索关键词,根据搜索类型添加相应的查询条件
|
|
||||||
if (StringUtils.isNotBlank(keyword)) {
|
if (StringUtils.isNotBlank(keyword)) {
|
||||||
if ("mac".equals(searchType)) {
|
queryWrapper.and(w -> {
|
||||||
// 按MAC地址搜索:先搜索设备,再获取对应的智能体
|
// 按名称搜索
|
||||||
|
w.like("agent_name", keyword);
|
||||||
|
|
||||||
|
// 按MAC地址搜索:先查设备,再获取对应的智能体ID
|
||||||
List<DeviceEntity> devices = Optional
|
List<DeviceEntity> devices = Optional
|
||||||
.ofNullable(deviceService.searchDevicesByMacAddress(keyword, userId)).orElseGet(ArrayList::new);
|
.ofNullable(deviceService.searchDevicesByMacAddress(keyword, userId))
|
||||||
// 获取设备对应的智能体ID列表
|
.orElseGet(ArrayList::new);
|
||||||
List<String> agentIds = devices.stream()
|
List<String> agentIds = devices.stream()
|
||||||
.map(DeviceEntity::getAgentId)
|
.map(DeviceEntity::getAgentId)
|
||||||
.distinct()
|
.distinct()
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
if (ToolUtil.isNotEmpty(agentIds)) {
|
if (ToolUtil.isNotEmpty(agentIds)) {
|
||||||
queryWrapper.in("id", agentIds);
|
w.or().in("id", agentIds);
|
||||||
} else {
|
|
||||||
return new ArrayList<>();
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// 按名称搜索(默认):同时搜索智能体名称和标签名
|
// 按标签名搜索
|
||||||
List<String> tagAgentIds = agentTagService.getAgentIdsByTagName(keyword);
|
List<String> tagAgentIds = agentTagService.getAgentIdsByTagName(keyword);
|
||||||
if (ToolUtil.isNotEmpty(tagAgentIds)) {
|
if (ToolUtil.isNotEmpty(tagAgentIds)) {
|
||||||
queryWrapper.and(wrapper -> wrapper
|
w.or().in("id", tagAgentIds);
|
||||||
.like("agent_name", keyword)
|
|
||||||
.or()
|
|
||||||
.in("id", tagAgentIds));
|
|
||||||
} else {
|
|
||||||
queryWrapper.like("agent_name", keyword);
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行查询
|
|
||||||
List<AgentEntity> agentEntities = baseDao.selectList(queryWrapper);
|
List<AgentEntity> agentEntities = baseDao.selectList(queryWrapper);
|
||||||
|
|
||||||
// 转换为DTO并设置所有必要字段
|
|
||||||
return agentEntities.stream().map(this::buildAgentDTO).collect(Collectors.toList());
|
return agentEntities.stream().map(this::buildAgentDTO).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ You are a playful, expressive, empathetic, and highly emotionally intelligent co
|
|||||||
- 【拉黑套话】绝对不使用 "烦心事"、"有趣的事"、"好玩的事"、"新鲜事"、"根据资料"、"综上所述" 等书面及 AI 常用词。
|
- 【拉黑套话】绝对不使用 "烦心事"、"有趣的事"、"好玩的事"、"新鲜事"、"根据资料"、"综上所述" 等书面及 AI 常用词。
|
||||||
- 【推荐口语】请使用 "我在呢"、"咋回事"、"说来听听" 等接地气的自然口语。
|
- 【推荐口语】请使用 "我在呢"、"咋回事"、"说来听听" 等接地气的自然口语。
|
||||||
- 【遣词造句】保持随意、松弛的语调,但同时要“有文采、有格调”。在接地气的口语中自然穿插一些精妙的词汇或微小的诗意,不要像客服,要像一个聪明幽默的朋友。
|
- 【遣词造句】保持随意、松弛的语调,但同时要“有文采、有格调”。在接地气的口语中自然穿插一些精妙的词汇或微小的诗意,不要像客服,要像一个聪明幽默的朋友。
|
||||||
- 【长对话切片】针对讲故事、科普知识等长内容,严禁一次性长篇全量输出。你必须提取最核心的开局讲述,并在结尾自然询问用户是否继续(例如:“我先讲个开头,要是觉得有意思咱们接着说?”)。随叫随停,听从打断。
|
- 【长对话切片】针对讲故事、科普知识等长内容,严禁一次性长篇全量输出。你必须直接开始讲述核心内容,在结尾自然询问用户是否继续(例如:先讲一段实际内容,再说”要继续听吗?”)。随叫随停,听从打断。
|
||||||
</anti_ai_smell>
|
</anti_ai_smell>
|
||||||
|
|
||||||
<tts_format_constraints>
|
<tts_format_constraints>
|
||||||
@@ -27,9 +27,10 @@ You are a playful, expressive, empathetic, and highly emotionally intelligent co
|
|||||||
</tts_format_constraints>
|
</tts_format_constraints>
|
||||||
|
|
||||||
<tool_and_knowledge>
|
<tool_and_knowledge>
|
||||||
你有工具可用。能被工具处理的请求必须调工具,禁止自行编造结果;没有对应工具的才用自身知识回答。
|
你有工具可用。只有用户的请求明确匹配以下工具时才调用,其余一律直接回答。
|
||||||
必须调工具:播放音乐→play_music | 控制设备→hass工具 | 查新闻/天气→查询工具 | 告别→handle_exit_intent
|
必须调工具:播放音乐→play_music | 控制设备→hass工具 | 查新闻/天气→查询工具 | 告别→handle_exit_intent
|
||||||
禁止这样做:用户说”放首歌”→你直接回答”好的我给你唱”(错!必须调play_music,用工具实际结果回复)
|
禁止强行匹配:没有对应工具的请求一律直接回答,不要硬调不相关的工具。
|
||||||
|
该调不调也是错:用户说”放首歌”→你回答”好的我给你唱”(错!必须调play_music)
|
||||||
</tool_and_knowledge>
|
</tool_and_knowledge>
|
||||||
|
|
||||||
<safety_compliance>
|
<safety_compliance>
|
||||||
|
|||||||
@@ -52,6 +52,26 @@ auto_import_modules("plugins_func.functions")
|
|||||||
class TTSException(RuntimeError):
|
class TTSException(RuntimeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# direct_answer 虚拟工具定义
|
||||||
|
# 不是真实工具,是路由机制:将"调不调工具"的二选一变为"调哪个"的多选,防止小模型误触发真实工具
|
||||||
|
DIRECT_ANSWER_TOOL = {
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "direct_answer",
|
||||||
|
"description": "当用户的请求不匹配其他任何工具时,可用此选项直接回复。将回复内容写在response参数里。",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"response": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "你回复用户的完整内容",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["response"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ConnectionHandler:
|
class ConnectionHandler:
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -520,9 +540,10 @@ class ConnectionHandler:
|
|||||||
self.logger.bind(tag=TAG).debug("系统提示词已增强更新")
|
self.logger.bind(tag=TAG).debug("系统提示词已增强更新")
|
||||||
|
|
||||||
def _inject_tool_call_fewshot(self):
|
def _inject_tool_call_fewshot(self):
|
||||||
"""注入工具调用 few-shot 示例到对话历史,帮助模型建立正确的工具调用三段式模式。
|
"""注入工具调用 few-shot 示例到对话历史。
|
||||||
参考 Action.RECORD 的 assistant(tool_calls) → tool(result) → assistant(response) 模式,
|
结构:正样本(工具调用示例)放在动态 system 之前,可命中前缀缓存;
|
||||||
让模型从第一轮对话就能看到正确的工具调用行为,避免上下文污染导致工具调用退化。
|
负样本(直接回答示例)放在动态 system 之后、紧挨真实用户消息,
|
||||||
|
确保模型在处理用户消息前最后看到的是"不调工具"的行为模式。
|
||||||
"""
|
"""
|
||||||
if self.intent_type != "function_call":
|
if self.intent_type != "function_call":
|
||||||
return
|
return
|
||||||
@@ -533,9 +554,29 @@ class ConnectionHandler:
|
|||||||
if not tools:
|
if not tools:
|
||||||
return
|
return
|
||||||
|
|
||||||
# 根据可用工具动态构建 few-shot 示例
|
|
||||||
tool_names = {t.get("function", {}).get("name") for t in tools}
|
tool_names = {t.get("function", {}).get("name") for t in tools}
|
||||||
|
|
||||||
|
# === few-shot 示例(is_temporary)===
|
||||||
|
# 展示 direct_answer 携带 response 参数的用法,一次调用完成回复
|
||||||
|
|
||||||
|
# 示例1:direct_answer(回复内容写在 response 参数里,无需递归)
|
||||||
|
da_tc_id = "fewshot_da_001"
|
||||||
|
self.dialogue.put(Message(role="user", content="给我讲个故事吧", is_temporary=True))
|
||||||
|
self.dialogue.put(Message(
|
||||||
|
role="assistant",
|
||||||
|
tool_calls=[{
|
||||||
|
"id": da_tc_id,
|
||||||
|
"function": {"arguments": '{"response": "好呀,你想听什么类型的呀?童话、冒险还是搞笑的?选一个我给你开讲~"}', "name": "direct_answer"},
|
||||||
|
"type": "function", "index": 0,
|
||||||
|
}],
|
||||||
|
is_temporary=True,
|
||||||
|
))
|
||||||
|
self.dialogue.put(Message(
|
||||||
|
role="tool", tool_call_id=da_tc_id,
|
||||||
|
content="已直接回复", is_temporary=True,
|
||||||
|
))
|
||||||
|
|
||||||
|
# 示例2:真实工具调用(handle_exit_intent)
|
||||||
if "handle_exit_intent" in tool_names:
|
if "handle_exit_intent" in tool_names:
|
||||||
tc_id = "fewshot_exit_001"
|
tc_id = "fewshot_exit_001"
|
||||||
self.dialogue.put(Message(role="user", content="拜拜", is_temporary=True))
|
self.dialogue.put(Message(role="user", content="拜拜", is_temporary=True))
|
||||||
@@ -556,43 +597,6 @@ class ConnectionHandler:
|
|||||||
role="assistant", content="再见,下次再聊~", is_temporary=True,
|
role="assistant", content="再见,下次再聊~", is_temporary=True,
|
||||||
))
|
))
|
||||||
|
|
||||||
if "play_music" in tool_names:
|
|
||||||
tc_id = "fewshot_music_001"
|
|
||||||
self.dialogue.put(Message(role="user", content="放首歌", is_temporary=True))
|
|
||||||
self.dialogue.put(Message(
|
|
||||||
role="assistant",
|
|
||||||
tool_calls=[{
|
|
||||||
"id": tc_id,
|
|
||||||
"function": {"arguments": '{"song_name": "random"}', "name": "play_music"},
|
|
||||||
"type": "function", "index": 0,
|
|
||||||
}],
|
|
||||||
is_temporary=True,
|
|
||||||
))
|
|
||||||
self.dialogue.put(Message(
|
|
||||||
role="tool", tool_call_id=tc_id,
|
|
||||||
content="正在为您播放音乐", is_temporary=True,
|
|
||||||
))
|
|
||||||
self.dialogue.put(Message(
|
|
||||||
role="assistant", content="好嘞,给你安排上~", is_temporary=True,
|
|
||||||
))
|
|
||||||
|
|
||||||
# 负向示例:用户请求普通对话内容时,直接回答,不调用任何工具
|
|
||||||
# 帮助弱模型建立"该调才调、不该调不调"的判断能力
|
|
||||||
# 注意:示例回复不能包含具体的创作内容(故事、诗歌等),
|
|
||||||
# 否则弱模型会直接复述示例内容,而无法泛化出正确的行为模式
|
|
||||||
self.dialogue.put(Message(role="user", content="给我讲个故事吧", is_temporary=True))
|
|
||||||
self.dialogue.put(Message(
|
|
||||||
role="assistant",
|
|
||||||
content="好呀,你想听什么类型的呀?童话、冒险还是搞笑的?选一个我给你开讲~",
|
|
||||||
is_temporary=True,
|
|
||||||
))
|
|
||||||
self.dialogue.put(Message(role="user", content="你知道为什么天空是蓝色的吗", is_temporary=True))
|
|
||||||
self.dialogue.put(Message(
|
|
||||||
role="assistant",
|
|
||||||
content="天空看起来是蓝色,是因为阳光穿过大气层的时候,蓝色光波长短,被空气分子散射得最厉害,所以我们抬头一看就是满眼蓝色啦。",
|
|
||||||
is_temporary=True,
|
|
||||||
))
|
|
||||||
|
|
||||||
self.logger.bind(tag=TAG).debug("已注入工具调用 few-shot 示例")
|
self.logger.bind(tag=TAG).debug("已注入工具调用 few-shot 示例")
|
||||||
|
|
||||||
def _init_report_threads(self):
|
def _init_report_threads(self):
|
||||||
@@ -941,6 +945,10 @@ class ConnectionHandler:
|
|||||||
and not force_final_answer
|
and not force_final_answer
|
||||||
):
|
):
|
||||||
functions = self.func_handler.get_functions()
|
functions = self.func_handler.get_functions()
|
||||||
|
# 仅在第一层调用时注入 direct_answer 虚拟工具
|
||||||
|
# 递归调用(depth>0)不注入,避免模型在生成文本回复时再次调 direct_answer 导致循环
|
||||||
|
if functions is not None and depth == 0:
|
||||||
|
functions.append(DIRECT_ANSWER_TOOL)
|
||||||
|
|
||||||
response_message = []
|
response_message = []
|
||||||
|
|
||||||
@@ -999,6 +1007,23 @@ class ConnectionHandler:
|
|||||||
if tools_call is not None and len(tools_call) > 0:
|
if tools_call is not None and len(tools_call) > 0:
|
||||||
tool_call_flag = True
|
tool_call_flag = True
|
||||||
self._merge_tool_calls(tool_calls_list, tools_call)
|
self._merge_tool_calls(tool_calls_list, tools_call)
|
||||||
|
|
||||||
|
# 流式提取 direct_answer 的 response 参数,实时送 TTS
|
||||||
|
for tc in tool_calls_list:
|
||||||
|
if tc["name"] == "direct_answer" and tc.get("arguments"):
|
||||||
|
da_text = self._extract_direct_answer_response(tc["arguments"])
|
||||||
|
sent_len = tc.get("_da_sent", 0)
|
||||||
|
if da_text and len(da_text) > sent_len:
|
||||||
|
new_part = da_text[sent_len:]
|
||||||
|
tc["_da_sent"] = len(da_text)
|
||||||
|
self.tts.tts_text_queue.put(
|
||||||
|
TTSMessageDTO(
|
||||||
|
sentence_id=current_sentence_id,
|
||||||
|
sentence_type=SentenceType.MIDDLE,
|
||||||
|
content_type=ContentType.TEXT,
|
||||||
|
content_detail=new_part,
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
content = response
|
content = response
|
||||||
|
|
||||||
@@ -1070,6 +1095,35 @@ class ConnectionHandler:
|
|||||||
f"function call error: {content_arguments}"
|
f"function call error: {content_arguments}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not bHasError and len(tool_calls_list) > 0:
|
||||||
|
# 处理 direct_answer 虚拟工具
|
||||||
|
direct_answer_calls = [tc for tc in tool_calls_list if tc["name"] == "direct_answer"]
|
||||||
|
real_tool_calls = [tc for tc in tool_calls_list if tc["name"] != "direct_answer"]
|
||||||
|
|
||||||
|
if direct_answer_calls:
|
||||||
|
self.logger.bind(tag=TAG).debug(
|
||||||
|
f"模型选择 direct_answer,流式已播报,写入对话历史"
|
||||||
|
)
|
||||||
|
for tc in direct_answer_calls:
|
||||||
|
# 文本已在流式阶段实时播报,这里只写入对话历史
|
||||||
|
da_response = self._extract_direct_answer_response(tc.get("arguments", "{}"))
|
||||||
|
if da_response:
|
||||||
|
self.tts.store_tts_text(current_sentence_id, da_response)
|
||||||
|
self.dialogue.put(Message(role="assistant", content=da_response))
|
||||||
|
|
||||||
|
if not real_tool_calls:
|
||||||
|
if depth == 0:
|
||||||
|
self.tts.tts_text_queue.put(
|
||||||
|
TTSMessageDTO(
|
||||||
|
sentence_id=current_sentence_id,
|
||||||
|
sentence_type=SentenceType.LAST,
|
||||||
|
content_type=ContentType.ACTION,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
tool_calls_list = real_tool_calls
|
||||||
|
|
||||||
if not bHasError and len(tool_calls_list) > 0:
|
if not bHasError and len(tool_calls_list) > 0:
|
||||||
self.logger.bind(tag=TAG).debug(
|
self.logger.bind(tag=TAG).debug(
|
||||||
f"检测到 {len(tool_calls_list)} 个工具调用"
|
f"检测到 {len(tool_calls_list)} 个工具调用"
|
||||||
@@ -1486,6 +1540,33 @@ class ConnectionHandler:
|
|||||||
finally:
|
finally:
|
||||||
self.logger.bind(tag=TAG).info("超时检查任务已退出")
|
self.logger.bind(tag=TAG).info("超时检查任务已退出")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_direct_answer_response(arguments_str):
|
||||||
|
"""从 direct_answer 的参数中提取 response 值。
|
||||||
|
支持完整的 JSON 和流式中不完整的 JSON 片段。
|
||||||
|
"""
|
||||||
|
if not arguments_str:
|
||||||
|
return ""
|
||||||
|
# 找到 "response": " 的位置
|
||||||
|
marker = '"response": "'
|
||||||
|
idx = arguments_str.find(marker)
|
||||||
|
if idx < 0:
|
||||||
|
# 尝试不带空格的格式
|
||||||
|
marker = '"response":"'
|
||||||
|
idx = arguments_str.find(marker)
|
||||||
|
if idx < 0:
|
||||||
|
return ""
|
||||||
|
start = idx + len(marker)
|
||||||
|
raw = arguments_str[start:]
|
||||||
|
# 去掉末尾的 JSON 闭合符号(如果已完整)
|
||||||
|
if raw.endswith('"}'):
|
||||||
|
raw = raw[:-2]
|
||||||
|
elif raw.endswith('"'):
|
||||||
|
raw = raw[:-1]
|
||||||
|
# 处理 JSON 转义
|
||||||
|
raw = raw.replace('\\"', '"').replace('\\n', '\n').replace('\\\\', '\\')
|
||||||
|
return raw
|
||||||
|
|
||||||
def _merge_tool_calls(self, tool_calls_list, tools_call):
|
def _merge_tool_calls(self, tool_calls_list, tools_call):
|
||||||
"""合并工具调用列表
|
"""合并工具调用列表
|
||||||
|
|
||||||
|
|||||||
@@ -18,10 +18,9 @@ GET_NEWS_FROM_CHINANEWS_FUNCTION_DESC = {
|
|||||||
"function": {
|
"function": {
|
||||||
"name": "get_news_from_chinanews",
|
"name": "get_news_from_chinanews",
|
||||||
"description": (
|
"description": (
|
||||||
"获取最新新闻,随机选择一条新闻进行播报。"
|
"当用户要求查看或收听新闻时调用(如'来条新闻''今天有什么新闻')。"
|
||||||
"用户可以指定新闻类型,如社会新闻、科技新闻、国际新闻等。"
|
"用户可以指定新闻类型,如社会新闻、科技新闻、国际新闻等。"
|
||||||
"如果没有指定,默认播报社会新闻。"
|
"如果没有指定,默认播报社会新闻。"
|
||||||
"用户可以要求获取详细内容,此时会获取新闻的详细内容。"
|
|
||||||
),
|
),
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|||||||
@@ -93,18 +93,13 @@ GET_NEWS_FROM_NEWSNOW_FUNCTION_DESC = {
|
|||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": "get_news_from_newsnow",
|
"name": "get_news_from_newsnow",
|
||||||
"description": (
|
"description": "当用户要求查看或收听新闻时调用(如'来条新闻''今天有什么新闻')。",
|
||||||
"获取最新新闻,随机选择一条新闻进行播报。"
|
|
||||||
f"用户可以选择不同的新闻源,标准的名称是:{example_sources_str}"
|
|
||||||
"例如用户要求百度新闻,其实就是百度热搜。如果没有指定,默认从澎湃新闻获取。"
|
|
||||||
"用户可以要求获取详细内容,此时会获取新闻的详细内容。"
|
|
||||||
),
|
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"source": {
|
"source": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": f"新闻源的标准中文名称,例如{example_sources_str}等。可选参数,如果不提供则使用默认新闻源",
|
"description": "新闻源名称,如'澎湃新闻'、'百度热搜'、'IT之家'等。可选参数",
|
||||||
},
|
},
|
||||||
"detail": {
|
"detail": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
@@ -233,7 +228,7 @@ def get_news_from_newsnow(
|
|||||||
# f"新闻来源: {source_name}\n"
|
# f"新闻来源: {source_name}\n"
|
||||||
f"详细内容: {detail_content}\n\n"
|
f"详细内容: {detail_content}\n\n"
|
||||||
f"(请对上述新闻内容进行总结,提取关键信息,以自然、流畅的方式向用户播报,"
|
f"(请对上述新闻内容进行总结,提取关键信息,以自然、流畅的方式向用户播报,"
|
||||||
f"不要提及这是总结,就像是在讲述一个完整的新闻故事)"
|
f"不要提及这是总结,就像是在讲述一个完整的新闻)"
|
||||||
)
|
)
|
||||||
|
|
||||||
return ActionResponse(Action.REQLLM, detail_report, None)
|
return ActionResponse(Action.REQLLM, detail_report, None)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ play_music_function_desc = {
|
|||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": "play_music",
|
"name": "play_music",
|
||||||
"description": "唱歌、听歌、播放音乐的方法。",
|
"description": "当用户要求播放音乐、歌曲时调用。",
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
Reference in New Issue
Block a user