feat: Function Calling 兼容

新增 xcli_core/fc_tools.py:
- MCP 定义 → OpenAI FC 格式转换
- FC 响应解析 (tool_calls 提取)
- 自动收集所有插件 FC 定义
- 400 错误自动降级为普通模式

引擎层改动 (openai_engine + ollama_engine):
- generate_response 新增 tools 参数
- 请求体自动注入 tools/tool_choice
- 响应检测 tool_calls 并返回 FC 标记
- 对话历史支持 tool 角色消息和 assistant.tool_calls
- _build_messages_with_history 兼容 FC 消息格式

cli_core.py 集成:
- _handle_fc_response: FC 响应 → 工具执行 → 结果回传
- _execute_tool_by_name: FC 调用路径的安全检查+工具分发
- process_conversation 循环自动处理 FC 工具调用
- TUI/CLI 双模式 FC 支持
This commit is contained in:
shuiyu1123 committed 2026-04-27 00:29:34 +08:00
1 parent 15e35ca8ad
commit 6de5c6f202
5 files changed
+549 -47

No files matched your search

+94 -32
View File
@@ -107,8 +107,16 @@ Ollama AI引擎插件帮助信息
# 如果出现异常,说明服务未运行 # 如果出现异常,说明服务未运行
return False return False
def generate_response(self, user_input, tool_results=None, system_prompt=None): def generate_response(self, user_input, tool_results=None, system_prompt=None, tools=None):
"""生成AI响应 - 使用Ollama API""" """
生成AI响应 - 使用Ollama API
Args:
user_input: 用户输入
tool_results: 工具执行结果
system_prompt: 系统提示词
tools: OpenAI FC 工具定义列表 (可选)
"""
try: try:
# 检查Ollama服务是否运行 # 检查Ollama服务是否运行
if not self.is_service_running: if not self.is_service_running:
@@ -117,64 +125,118 @@ Ollama AI引擎插件帮助信息
# 如果有工具结果,将其作为上下文返回给AI处理 # 如果有工具结果,将其作为上下文返回给AI处理
if tool_results: if tool_results:
tool_result_text = f"工具执行结果: {tool_results.get('result', '无结果')}" tool_result_text = f"工具执行结果: {tool_results.get('result', '无结果')}"
# 构建包含历史记录的消息列表
messages = self._build_messages_with_history(system_prompt, tool_result_text) messages = self._build_messages_with_history(system_prompt, tool_result_text)
else:
messages = self._build_messages_with_history(system_prompt, user_input)
# 使用Ollama Python库调用API # 构建调用参数
response = ollama_client.chat( kwargs = {
model=self.model, "model": self.model,
messages=messages, "messages": messages,
stream=False "stream": False
) }
# 返回AI的回复 # ── Function Calling: 注入工具定义 ──
return response["message"]["content"] if tools:
kwargs["tools"] = tools
# 构建包含历史记录的消息列表
messages = self._build_messages_with_history(system_prompt, user_input)
# 使用Ollama Python库调用API # 使用Ollama Python库调用API
response = ollama_client.chat( response = ollama_client.chat(**kwargs)
model=self.model,
messages=messages,
stream=False
)
# 返回AI的回复 # ── Function Calling: 检查 tool_calls ──
content = response["message"]["content"] message = response.get("message", {})
tool_calls_raw = message.get("tool_calls")
if tool_calls_raw:
# Ollama 的 tool_calls 格式: [{"function": {"name": ..., "arguments": {...}}}]
fc_calls = []
for i, tc in enumerate(tool_calls_raw):
func = tc.get("function", {})
name = func.get("name", "")
args = func.get("arguments", {})
call_id = f"call_{name}_{i}"
# 统一为小狸内部格式
if "args" in args and isinstance(args["args"], str):
final_args = args["args"]
elif "args" in args:
final_args = json.dumps(args["args"], ensure_ascii=False)
else:
parts = []
for k, v in args.items():
parts.append(str(v) if isinstance(v, str) else json.dumps(v, ensure_ascii=False))
final_args = " ".join(parts) if parts else ""
fc_calls.append({
"id": call_id,
"name": name,
"arguments": args,
"action": "use_tool",
"tool": name,
"args": final_args,
})
if fc_calls:
return json.dumps({
"_fc": True,
"tool_calls": fc_calls,
}, ensure_ascii=False)
# 普通文本响应
content = message.get("content", "")
# 清理无效的 UTF-8 代理字符 # 清理无效的 UTF-8 代理字符
content = content.encode('utf-8', 'replace').decode('utf-8') content = content.encode('utf-8', 'replace').decode('utf-8')
return content return content
except Exception as e: except Exception as e:
# 如果API调用失败,返回错误信息
return f"Ollama API调用失败: {str(e)}" return f"Ollama API调用失败: {str(e)}"
def _build_messages_with_history(self, system_prompt, user_input): def _build_messages_with_history(self, system_prompt, user_input):
"""构建包含历史记录的消息列表""" """构建包含历史记录的消息列表(支持 FC tool 消息)"""
messages = [] messages = []
# 添加系统提示词 # 添加系统提示词
if system_prompt: if system_prompt:
# 清理代理字符
clean_prompt = system_prompt.encode('utf-8', 'replace').decode('utf-8') clean_prompt = system_prompt.encode('utf-8', 'replace').decode('utf-8')
messages.append({ messages.append({
"role": "system", "role": "system",
"content": clean_prompt "content": clean_prompt
}) })
# 添加历史对话记录(使用共享历史) # 添加历史对话记录(使用共享历史)
if self.shared_conversation_history is not None: if self.shared_conversation_history is not None:
# 确保历史记录格式正确
for msg in self.shared_conversation_history[-self.max_history:]: for msg in self.shared_conversation_history[-self.max_history:]:
if isinstance(msg, dict) and "role" in msg and "content" in msg: if not isinstance(msg, dict) or "role" not in msg:
# 清理内容中的代理字符
clean_content = msg["content"].encode('utf-8', 'replace').decode('utf-8')
messages.append({"role": msg["role"], "content": clean_content})
else:
# 如果格式不正确,跳过该条记录
continue continue
role = msg["role"]
# tool 角色消息(FC 工具结果)
if role == "tool":
tool_msg = {"role": "tool", "content": msg.get("content", "")}
if "tool_call_id" in msg:
tool_msg["tool_call_id"] = msg["tool_call_id"]
if "name" in msg:
tool_msg["name"] = msg["name"]
messages.append(tool_msg)
continue
# assistant 消息可能包含 tool_calls
if role == "assistant" and "tool_calls" in msg:
assistant_msg = {
"role": "assistant",
"content": msg.get("content"),
"tool_calls": msg["tool_calls"]
}
messages.append(assistant_msg)
continue
# 普通 user/assistant 消息
if "content" in msg:
clean_content = msg["content"]
if isinstance(clean_content, str):
clean_content = clean_content.encode('utf-8', 'replace').decode('utf-8')
messages.append({"role": role, "content": clean_content})
# 清理用户输入 # 清理用户输入
if user_input: if user_input:
clean_input = user_input.encode('utf-8', 'replace').decode('utf-8') clean_input = user_input.encode('utf-8', 'replace').decode('utf-8')
+83 -13
View File
@@ -125,8 +125,16 @@ OpenAI 兼容格式引擎帮助信息
url = url.rstrip('/') + '/chat/completions' url = url.rstrip('/') + '/chat/completions'
return url return url
def generate_response(self, user_input, tool_results=None, system_prompt=None): def generate_response(self, user_input, tool_results=None, system_prompt=None, tools=None):
"""生成 AI 响应""" """
生成 AI 响应
Args:
user_input: 用户输入
tool_results: 工具执行结果
system_prompt: 系统提示词
tools: OpenAI FC 工具定义列表 (可选)
"""
try: try:
if not self.base_url: if not self.base_url:
return "错误: openai 引擎的 base_url 未配置,请在 config.json 中设置" return "错误: openai 引擎的 base_url 未配置,请在 config.json 中设置"
@@ -145,6 +153,11 @@ OpenAI 兼容格式引擎帮助信息
"stream": False "stream": False
} }
# ── Function Calling: 注入工具定义 ──
if tools:
body["tools"] = tools
body["tool_choice"] = "auto"
# 发送请求 # 发送请求
api_url = self._get_api_url() api_url = self._get_api_url()
response = requests.post( response = requests.post(
@@ -157,18 +170,49 @@ OpenAI 兼容格式引擎帮助信息
if response.status_code == 200: if response.status_code == 200:
response_data = response.json() response_data = response.json()
if "choices" in response_data and len(response_data["choices"]) > 0: if "choices" in response_data and len(response_data["choices"]) > 0:
ai_response = response_data["choices"][0]["message"]["content"] message = response_data["choices"][0].get("message", {})
# 清理无效的 UTF-8 代理字符
ai_response = ai_response.encode('utf-8', 'replace').decode('utf-8') # ── Function Calling: 检查 tool_calls ──
# 处理思考内容 tool_calls_raw = message.get("tool_calls")
ai_response = self._process_thinking_content(ai_response) if tool_calls_raw:
return ai_response # 返回 FC 结果(由调用方处理工具执行)
from xcli_core.fc_tools import parse_fc_response
fc_calls = parse_fc_response(response_data)
if fc_calls:
# 返回一个特殊的 JSON 字符串,标记为 FC 调用
return json.dumps({
"_fc": True,
"tool_calls": fc_calls,
"raw_message": message
}, ensure_ascii=False)
# 普通文本响应
ai_response = message.get("content", "")
if ai_response:
# 清理无效的 UTF-8 代理字符
ai_response = ai_response.encode('utf-8', 'replace').decode('utf-8')
# 处理思考内容
ai_response = self._process_thinking_content(ai_response)
return ai_response or ""
else: else:
return f"API 返回格式错误: {response_data}" return f"API 返回格式错误: {response_data}"
elif response.status_code == 401: elif response.status_code == 401:
return "API 认证失败,请检查 api_key 是否正确" return "API 认证失败,请检查 api_key 是否正确"
elif response.status_code == 404: elif response.status_code == 404:
return f"API 端点不存在,请检查 base_url 是否正确: {api_url}" return f"API 端点不存在,请检查 base_url 是否正确: {api_url}"
elif response.status_code == 400:
# 工具定义可能不兼容,降级为无工具重试
if tools and "tools" in str(response.text).lower():
print(f"{Fore.YELLOW}FC 不支持,降级为普通模式{Style.RESET_ALL}")
body.pop("tools", None)
body.pop("tool_choice", None)
retry = requests.post(url=api_url, json=body, headers=self._get_headers(), timeout=None)
if retry.status_code == 200:
rd = retry.json()
if "choices" in rd and rd["choices"]:
ai_response = rd["choices"][0].get("message", {}).get("content", "")
return ai_response.encode('utf-8', 'replace').decode('utf-8') if ai_response else ""
return f"API 调用失败: {response.status_code} - {response.text}"
else: else:
return f"API 调用失败: {response.status_code} - {response.text}" return f"API 调用失败: {response.status_code} - {response.text}"
@@ -178,7 +222,7 @@ OpenAI 兼容格式引擎帮助信息
return f"API 调用失败: {str(e)}" return f"API 调用失败: {str(e)}"
def _build_messages_with_history(self, system_prompt, user_input): def _build_messages_with_history(self, system_prompt, user_input):
"""构建包含历史记录的消息列表""" """构建包含历史记录的消息列表(支持 FC tool 消息)"""
messages = [] messages = []
# 添加系统提示词 # 添加系统提示词
@@ -192,12 +236,38 @@ OpenAI 兼容格式引擎帮助信息
# 添加历史对话记录(使用共享历史) # 添加历史对话记录(使用共享历史)
if self.shared_conversation_history is not None: if self.shared_conversation_history is not None:
for msg in self.shared_conversation_history[-self.max_history:]: for msg in self.shared_conversation_history[-self.max_history:]:
if isinstance(msg, dict) and "role" in msg and "content" in msg: if not isinstance(msg, dict) or "role" not in msg:
clean_content = msg["content"].encode('utf-8', 'replace').decode('utf-8')
messages.append({"role": msg["role"], "content": clean_content})
else:
continue continue
role = msg["role"]
# tool 角色消息(FC 工具结果)
if role == "tool":
tool_msg = {"role": "tool", "content": msg.get("content", "")}
if "tool_call_id" in msg:
tool_msg["tool_call_id"] = msg["tool_call_id"]
if "name" in msg:
tool_msg["name"] = msg["name"]
messages.append(tool_msg)
continue
# assistant 消息可能包含 tool_calls
if role == "assistant" and "tool_calls" in msg:
assistant_msg = {
"role": "assistant",
"content": msg.get("content"),
"tool_calls": msg["tool_calls"]
}
messages.append(assistant_msg)
continue
# 普通 user/assistant 消息
if "content" in msg:
clean_content = msg["content"]
if isinstance(clean_content, str):
clean_content = clean_content.encode('utf-8', 'replace').decode('utf-8')
messages.append({"role": role, "content": clean_content})
# 添加当前用户输入 # 添加当前用户输入
if user_input: if user_input:
clean_input = user_input.encode('utf-8', 'replace').decode('utf-8') clean_input = user_input.encode('utf-8', 'replace').decode('utf-8')
+1
View File
@@ -0,0 +1 @@
[]
+121 -2
View File
@@ -6,6 +6,7 @@ import time
import shutil import shutil
import threading import threading
import random import random
from typing import Optional
from colorama import Fore, Style from colorama import Fore, Style
from .constants import TEXTUAL_AVAILABLE, LOVE_FILE_PATH, DEFAULT_MAX_HISTORY from .constants import TEXTUAL_AVAILABLE, LOVE_FILE_PATH, DEFAULT_MAX_HISTORY
@@ -473,6 +474,14 @@ multi 操作支持一次修改多处:
processed_response = self.process_thinking_response(response) processed_response = self.process_thinking_response(response)
processed_response = self._process_code_blocks(processed_response) processed_response = self._process_code_blocks(processed_response)
# ── Function Calling 响应检测 ──
fc_handled = self._handle_fc_response(processed_response, user_input)
if fc_handled:
# FC 工具已执行,用结果继续循环
current_input = fc_handled
loop_count += 1
continue
try: try:
text_content, json_data = self._parse_mixed_response(processed_response) text_content, json_data = self._parse_mixed_response(processed_response)
@@ -707,6 +716,99 @@ multi 操作支持一次修改多处:
summary = summary[:77] + "..." summary = summary[:77] + "..."
return summary return summary
def _handle_fc_response(self, response: str, original_input: str) -> Optional[str]:
"""
处理 Function Calling 响应
如果响应是 FC 格式,执行工具并返回结果(供下一轮循环使用)
如果不是 FC 响应,返回 None
"""
if not response or '_fc' not in response:
return None
try:
data = json.loads(response)
if not data.get("_fc"):
return None
tool_calls = data.get("tool_calls", [])
if not tool_calls:
return None
# 记录 assistant 的 FC 消息到历史
fc_history_entry = {
"role": "assistant",
"content": None,
"tool_calls": [{
"id": tc.get("id", ""),
"type": "function",
"function": {
"name": tc.get("name", ""),
"arguments": json.dumps(tc.get("arguments", {}), ensure_ascii=False)
}
} for tc in tool_calls]
}
self.shared_conversation_history.append(fc_history_entry)
# 执行每个工具调用
results = []
for tc in tool_calls:
tool_name = tc.get("tool", tc.get("name", ""))
tool_args = tc.get("args", "")
call_id = tc.get("id", "")
print(f"\n{Fore.GREEN} FC 调用: {tool_name}({tool_args[:60]}){Style.RESET_ALL}")
# 通过插件管理器执行
result = self._execute_tool_by_name(tool_name, tool_args)
# 记录工具结果到历史
tool_result_msg = {
"role": "tool",
"tool_call_id": call_id,
"name": tool_name,
"content": result[:5000] if result else "执行完成"
}
self.shared_conversation_history.append(tool_result_msg)
results.append(f"[{tool_name}]: {result[:200] if result else '完成'}")
# 返回工具结果,供下一轮 AI 处理
return "工具执行结果:\n" + "\n".join(results)
except (ValueError, KeyError, TypeError):
return None
def _execute_tool_by_name(self, tool_name: str, tool_args: str) -> str:
"""根据名称执行工具(FC 调用路径)"""
# 安全检查
safety = get_safety()
allowed, msg = safety.check(tool_name, tool_args)
if not allowed:
return f"操作被拒绝: {msg}"
# 查找工具
tool = self.liugin_manager.get_tool_by_name(tool_name)
if tool and 'handler' in tool:
try:
return tool['handler'](tool_args)
except Exception as e:
return f"工具执行失败: {e}"
# memory 内置工具
if tool_name == 'memory' and hasattr(self, 'memory_manager'):
return self.memory_manager.handle_tool(tool_args)
# scheduler 内置工具
if tool_name == 'scheduler':
for t in self.liugin_manager.tools:
if t.get('name') == 'scheduler' and 'handler' in t:
try:
return t['handler'](tool_args)
except Exception as e:
return f"工具执行失败: {e}"
return f"未找到工具: {tool_name}"
def _generate_response_with_animation(self, current_input, liugin_prompts=None): def _generate_response_with_animation(self, current_input, liugin_prompts=None):
"""生成AI响应并显示等待动画 — 支持 ESC 跨平台取消""" """生成AI响应并显示等待动画 — 支持 ESC 跨平台取消"""
# TUI 模式:不做动画 # TUI 模式:不做动画
@@ -714,7 +816,14 @@ multi 操作支持一次修改多处:
system_prompt = self._build_system_prompt(liugin_prompts) system_prompt = self._build_system_prompt(liugin_prompts)
if not self.current_engine: if not self.current_engine:
return "错误: 当前没有可用的AI引擎,请检查ai_engines目录中的引擎插件" return "错误: 当前没有可用的AI引擎,请检查ai_engines目录中的引擎插件"
response = self.current_engine.generate_response(current_input, system_prompt=system_prompt) # TUI 模式也支持 FC
fc_tools = None
try:
from .fc_tools import mcp_to_openai_tools
fc_tools = mcp_to_openai_tools(self.liugin_manager)
except Exception:
pass
response = self.current_engine.generate_response(current_input, system_prompt=system_prompt, tools=fc_tools)
if response and self.shared_conversation_history is not None: if response and self.shared_conversation_history is not None:
self.shared_conversation_history.append({ self.shared_conversation_history.append({
"role": "assistant", "role": "assistant",
@@ -801,8 +910,18 @@ multi 操作支持一次修改多处:
if not self.current_engine: if not self.current_engine:
response_result[0] = "错误: 当前没有可用的AI引擎,请检查ai_engines目录中的引擎插件" response_result[0] = "错误: 当前没有可用的AI引擎,请检查ai_engines目录中的引擎插件"
else: else:
# ── 收集 FC 工具定义 ──
fc_tools = None
try:
from .fc_tools import mcp_to_openai_tools
fc_tools = mcp_to_openai_tools(self.liugin_manager)
if fc_tools:
fc_tools = fc_tools # 已经是列表
except Exception:
fc_tools = None
response_result[0] = self.current_engine.generate_response( response_result[0] = self.current_engine.generate_response(
current_input, system_prompt=system_prompt current_input, system_prompt=system_prompt, tools=fc_tools
) )
except Exception as e: except Exception as e:
response_result[0] = f"AI 响应生成失败: {e}" response_result[0] = f"AI 响应生成失败: {e}"
+250
View File
@@ -0,0 +1,250 @@
"""
Function Calling 工具格式转换
将 MCP 插件定义转为 OpenAI FC 格式,收集所有可用工具
"""
import json
from typing import List, Dict, Optional
def mcp_to_openai_tools(plugin_manager) -> List[Dict]:
"""
从插件管理器收集所有工具定义,转为 OpenAI function calling 格式
Returns:
[{"type": "function", "function": {"name": ..., "description": ..., "parameters": ...}}, ...]
"""
tools = []
# 从插件管理器获取工具
if not plugin_manager:
return tools
# 统一工具管理器
if hasattr(plugin_manager, 'plugins'):
for plugin in plugin_manager.plugins:
fc_def = _extract_fc_def(plugin)
if fc_def:
tools.append(fc_def)
# 普通插件管理器
elif hasattr(plugin_manager, 'tools'):
for tool_info in plugin_manager.tools:
fc_def = _tool_info_to_fc(tool_info)
if fc_def:
tools.append(fc_def)
# 追加内置工具(memory 等)
builtin_tools = _get_builtin_tools()
tools.extend(builtin_tools)
return tools
def _extract_fc_def(plugin) -> Optional[Dict]:
"""从插件实例提取 FC 定义"""
if not hasattr(plugin, 'get_mcp_definition'):
return None
try:
mcp_def = plugin.get_mcp_definition()
return mcp_to_fc(mcp_def)
except Exception:
return None
def _tool_info_to_fc(tool_info: dict) -> Optional[Dict]:
"""从 tool_info 字典转为 FC 定义"""
name = tool_info.get('name', '')
desc = tool_info.get('description', '')
if not name:
return None
return {
"type": "function",
"function": {
"name": name,
"description": desc,
"parameters": {
"type": "object",
"properties": {
"args": {
"type": "string",
"description": f"操作参数。格式参见 {name} 工具说明"
}
},
"required": ["args"]
}
}
}
def mcp_to_fc(mcp_def: dict) -> Optional[Dict]:
"""
MCP 定义 → OpenAI FC 定义
MCP 格式: {"name": ..., "description": ..., "inputSchema": {...}}
OpenAI FC: {"type": "function", "function": {"name": ..., "description": ..., "parameters": {...}}}
"""
name = mcp_def.get('name', '')
desc = mcp_def.get('description', '')
schema = mcp_def.get('inputSchema', mcp_def.get('parameters', {}))
if not name:
return None
# 如果 schema 为空,生成默认的 args 参数
if not schema or not schema.get('properties'):
schema = {
"type": "object",
"properties": {
"args": {
"type": "string",
"description": f"操作参数字符串,格式参见 {name} 工具说明"
}
},
"required": ["args"]
}
return {
"type": "function",
"function": {
"name": name,
"description": desc,
"parameters": schema
}
}
def fc_result_to_message(tool_call_id: str, tool_name: str, result: str) -> Dict:
"""
工具执行结果 → OpenAI 消息格式
Returns:
{"role": "tool", "tool_call_id": ..., "content": ...}
"""
return {
"role": "tool",
"tool_call_id": tool_call_id,
"name": tool_name,
"content": result[:10000] if result else "" # 截断过长结果
}
def parse_fc_response(response_data: dict) -> Optional[List[Dict]]:
"""
解析 OpenAI FC 响应,提取 tool_calls
Returns:
[{"id": "call_xxx", "name": "tool_name", "arguments": {...}}, ...]
或 None(如果不是 FC 响应)
"""
choices = response_data.get("choices", [])
if not choices:
return None
message = choices[0].get("message", {})
tool_calls_raw = message.get("tool_calls")
if not tool_calls_raw:
return None
tool_calls = []
for tc in tool_calls_raw:
func = tc.get("function", {})
name = func.get("name", "")
args_str = func.get("arguments", "{}")
# 解析 arguments JSON
try:
if isinstance(args_str, str):
args = json.loads(args_str)
else:
args = args_str
except json.JSONDecodeError:
args = {"raw": args_str}
# 统一为小狸内部格式: {"action": "use_tool", "tool": name, "args": "..." }
# 如果 args 里有 args 字段,直接用;否则把整个 args 序列化
if "args" in args and isinstance(args["args"], str):
final_args = args["args"]
elif "args" in args:
final_args = json.dumps(args["args"], ensure_ascii=False)
else:
# 把所有参数拼成字符串
parts = []
for k, v in args.items():
if isinstance(v, str):
parts.append(f"{v}")
else:
parts.append(json.dumps(v, ensure_ascii=False))
final_args = " ".join(parts) if parts else ""
tool_calls.append({
"id": tc.get("id", f"call_{name}"),
"name": name,
"arguments": args,
# 小狸内部格式
"action": "use_tool",
"tool": name,
"args": final_args,
})
return tool_calls
def _get_builtin_tools() -> List[Dict]:
"""内置工具定义(不依赖插件系统)"""
return [
{
"type": "function",
"function": {
"name": "memory",
"description": "持久化记忆系统。写日记、记录长期记忆、搜索历史记忆和聊天记录。",
"parameters": {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["write", "diary", "remember", "search", "context",
"diary_list", "diary_read", "chat_list", "chat_search"],
"description": "操作类型"
},
"content": {
"type": "string",
"description": "内容或关键词"
}
},
"required": ["action"]
}
}
},
{
"type": "function",
"function": {
"name": "scheduler",
"description": "定时任务工具,设定定时提醒。支持相对时间(30m/2h)、今日时间(14:30)、绝对时间、重复任务(r:前缀)。",
"parameters": {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["add", "list", "delete", "clear"],
"description": "操作类型"
},
"time": {
"type": "string",
"description": "时间: 30m / 14:30 / 2026-04-28 10:00 / r:30m"
},
"message": {
"type": "string",
"description": "任务描述"
},
"task_id": {
"type": "integer",
"description": "任务ID (用于 delete)"
}
},
"required": ["action"]
}
}
}
]