- Add 3 schedule tools (create/list/delete) and 5 utility tools (crypto, random, email, URL, regex) - Add frontend AgentSchedules.vue page with full CRUD, cron presets, manual trigger - Integrate Celery Beat for automatic schedule execution - Update startup scripts with Celery Beat launch - Fix schedule list API to show all schedules for admin users - Add celrybeat-schedule.* to .gitignore Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2147 lines
76 KiB
Python
2147 lines
76 KiB
Python
"""
|
||
内置工具实现
|
||
"""
|
||
from typing import Dict, Any, Optional, List, Tuple
|
||
from pathlib import Path
|
||
import httpx
|
||
import json
|
||
import os
|
||
import logging
|
||
import re
|
||
import math
|
||
from datetime import datetime, timedelta
|
||
import platform
|
||
import sys
|
||
import asyncio
|
||
import subprocess
|
||
import shlex
|
||
from sqlalchemy import text
|
||
from sqlalchemy.exc import SQLAlchemyError
|
||
from app.core.config import settings
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
def _local_file_workspace_root() -> Path:
|
||
"""file_read/file_write 允许操作的根目录(解析后路径)。"""
|
||
raw = (getattr(settings, "LOCAL_FILE_TOOLS_ROOT", None) or "").strip()
|
||
if raw:
|
||
return Path(raw).expanduser().resolve()
|
||
# backend/app/services/builtin_tools.py -> 上级 x4 = 仓库根(与历史上 join(services_dir, '../../..') 一致)
|
||
return Path(__file__).resolve().parent.parent.parent.parent
|
||
|
||
|
||
def _sanitize_tool_path_string(file_path: str) -> str:
|
||
"""
|
||
去掉 LLM/DSML/Markdown 泄漏到路径末尾的非法字符(如 123.md<|、引号),避免 WinError 123。
|
||
"""
|
||
if not isinstance(file_path, str):
|
||
return str(file_path)
|
||
s = file_path.replace("\ufeff", "").strip()
|
||
s = s.strip('"').strip("'")
|
||
# 行尾非法:Windows 文件名不能含 <>:"|?*;模型常把半角/全角尖括号粘在扩展名后
|
||
tail_bad = '<><>"|?*:"|*'
|
||
while s and s[-1] in tail_bad:
|
||
s = s[:-1].rstrip()
|
||
# 若仍含未配对尖括号片段在行尾(如 xxx.md<)
|
||
s = re.sub(r'[<<][||]?\s*$', "", s).rstrip()
|
||
return s
|
||
|
||
|
||
def _resolve_path_under_workspace(file_path: str) -> Tuple[Optional[Path], Optional[str]]:
|
||
"""将用户给定路径解析为绝对路径,且必须位于工作区内。"""
|
||
file_path = _sanitize_tool_path_string(file_path)
|
||
root = _local_file_workspace_root()
|
||
try:
|
||
p = Path(file_path).expanduser()
|
||
if not p.is_absolute():
|
||
p = (root / p).resolve()
|
||
else:
|
||
p = p.resolve()
|
||
except (OSError, ValueError) as e:
|
||
return None, str(e)
|
||
try:
|
||
p.relative_to(root)
|
||
except ValueError:
|
||
return None, f"不允许访问工作区外路径,允许根目录: {root}"
|
||
return p, None
|
||
|
||
|
||
_HTTP_RESPONSE_HEADER_ALLOWLIST = frozenset(
|
||
{
|
||
"content-type",
|
||
"content-length",
|
||
"content-encoding",
|
||
"date",
|
||
"last-modified",
|
||
"location",
|
||
"server",
|
||
"cache-control",
|
||
}
|
||
)
|
||
|
||
|
||
def _compact_http_response_headers(response: httpx.Response) -> Dict[str, str]:
|
||
"""减少 Set-Cookie 等大字段进入 LLM 上下文。"""
|
||
out: Dict[str, str] = {}
|
||
for key, value in response.headers.multi_items():
|
||
lk = key.lower()
|
||
if lk in ("set-cookie", "cookie"):
|
||
continue
|
||
if lk in _HTTP_RESPONSE_HEADER_ALLOWLIST:
|
||
out[key] = value if len(value) <= 2048 else value[:2048] + "..."
|
||
if not out:
|
||
n = 0
|
||
for key, value in response.headers.multi_items():
|
||
lk = key.lower()
|
||
if lk in ("set-cookie", "cookie"):
|
||
continue
|
||
if n >= 12:
|
||
break
|
||
out[key] = value if len(value) <= 512 else value[:512] + "..."
|
||
n += 1
|
||
return out
|
||
|
||
|
||
def _truncate_http_body_for_tool(body: Any, max_chars: int) -> Tuple[Any, bool, Optional[str]]:
|
||
"""
|
||
将 HTTP 正文限制在 max_chars 字符以内,避免门户首页等大 HTML 撑爆模型 context。
|
||
返回 (可能被截断后的 body, 是否截断, 说明文案)
|
||
"""
|
||
if max_chars <= 0:
|
||
max_chars = 32_000
|
||
note: Optional[str] = None
|
||
if isinstance(body, str):
|
||
if len(body) <= max_chars:
|
||
return body, False, None
|
||
note = (
|
||
f"正文已截断:原始约 {len(body)} 字符,仅保留前 {max_chars} 字符。"
|
||
"门户/频道首页通常过大,摘要请优先使用具体文章页 URL。"
|
||
)
|
||
return body[:max_chars], True, note
|
||
try:
|
||
serialized = json.dumps(body, ensure_ascii=False)
|
||
except (TypeError, ValueError):
|
||
serialized = str(body)
|
||
if len(serialized) <= max_chars:
|
||
return body, False, None
|
||
note = f"JSON 响应已截断:序列化长度约 {len(serialized)} 字符,仅保留前 {max_chars} 字符。"
|
||
return serialized[:max_chars], True, note
|
||
|
||
|
||
async def http_request_tool(
|
||
url: str,
|
||
method: str = "GET",
|
||
headers: Optional[Dict[str, str]] = None,
|
||
body: Any = None,
|
||
max_body_chars: Optional[int] = None,
|
||
) -> str:
|
||
"""
|
||
HTTP请求工具
|
||
|
||
Args:
|
||
url: 请求URL
|
||
method: HTTP方法 (GET, POST, PUT, DELETE)
|
||
headers: 请求头
|
||
body: 请求体(POST/PUT时使用)
|
||
max_body_chars: 响应正文写入工具结果的最大字符数;默认读取配置 HTTP_REQUEST_MAX_BODY_CHARS
|
||
|
||
Returns:
|
||
JSON格式的响应结果
|
||
"""
|
||
try:
|
||
limit = max_body_chars
|
||
if limit is None:
|
||
limit = int(getattr(settings, "HTTP_REQUEST_MAX_BODY_CHARS", 32_000) or 32_000)
|
||
limit = max(4096, min(limit, 200_000))
|
||
|
||
async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
|
||
method_upper = method.upper()
|
||
|
||
if method_upper == "GET":
|
||
response = await client.get(url, headers=headers)
|
||
elif method_upper == "POST":
|
||
response = await client.post(url, json=body, headers=headers)
|
||
elif method_upper == "PUT":
|
||
response = await client.put(url, json=body, headers=headers)
|
||
elif method_upper == "DELETE":
|
||
response = await client.delete(url, headers=headers)
|
||
else:
|
||
raise ValueError(f"不支持的HTTP方法: {method}")
|
||
|
||
# 尝试解析JSON响应
|
||
try:
|
||
response_body = response.json()
|
||
except Exception:
|
||
response_body = response.text
|
||
|
||
truncated_body, truncated, trunc_note = _truncate_http_body_for_tool(response_body, limit)
|
||
result: Dict[str, Any] = {
|
||
"status_code": response.status_code,
|
||
"headers": _compact_http_response_headers(response),
|
||
"body": truncated_body,
|
||
}
|
||
if truncated:
|
||
result["body_truncated"] = True
|
||
if trunc_note:
|
||
result["truncation_note"] = trunc_note
|
||
|
||
return json.dumps(result, ensure_ascii=False)
|
||
except Exception as e:
|
||
logger.error(f"HTTP请求工具执行失败: {str(e)}")
|
||
return json.dumps({"error": str(e)}, ensure_ascii=False)
|
||
|
||
|
||
_FILE_READ_TEXT_SUFFIXES = frozenset(
|
||
{
|
||
".txt",
|
||
".md",
|
||
".markdown",
|
||
".csv",
|
||
".tsv",
|
||
".json",
|
||
".jsonl",
|
||
".xml",
|
||
".html",
|
||
".htm",
|
||
".css",
|
||
".js",
|
||
".mjs",
|
||
".cjs",
|
||
".ts",
|
||
".tsx",
|
||
".jsx",
|
||
".vue",
|
||
".py",
|
||
".java",
|
||
".go",
|
||
".rs",
|
||
".sql",
|
||
".sh",
|
||
".bat",
|
||
".ps1",
|
||
".yaml",
|
||
".yml",
|
||
".toml",
|
||
".ini",
|
||
".cfg",
|
||
".log",
|
||
".rst",
|
||
".tex",
|
||
".gitignore",
|
||
".env",
|
||
".properties",
|
||
}
|
||
)
|
||
|
||
_FILE_READ_IMAGE_SUFFIXES = frozenset(
|
||
{".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".tif", ".tiff"}
|
||
)
|
||
|
||
|
||
def _truncate_utf8_text(s: str, max_bytes: int) -> str:
|
||
raw = s.encode("utf-8")
|
||
if len(raw) <= max_bytes:
|
||
return s
|
||
cut = raw[:max_bytes].decode("utf-8", errors="ignore")
|
||
return cut + "\n\n...[内容已按字节上限截断]"
|
||
|
||
|
||
def _read_pdf_text_sync(path: Path, max_bytes: int) -> str:
|
||
from pypdf import PdfReader
|
||
|
||
reader = PdfReader(str(path))
|
||
parts: List[str] = []
|
||
for page in reader.pages:
|
||
t = page.extract_text() or ""
|
||
if t.strip():
|
||
parts.append(t)
|
||
body = "\n\n".join(parts) if parts else ""
|
||
if not body.strip():
|
||
return "[PDF 未解析出文本,可能是扫描件或图片型 PDF;可将页导出为图片后上传,或安装 OCR 流程]"
|
||
return _truncate_utf8_text(body, max_bytes)
|
||
|
||
|
||
def _read_docx_text_sync(path: Path, max_bytes: int) -> str:
|
||
import docx
|
||
|
||
doc = docx.Document(str(path))
|
||
lines = [p.text for p in doc.paragraphs if p.text and p.text.strip()]
|
||
body = "\n".join(lines)
|
||
return _truncate_utf8_text(body or "[docx 中未找到段落文本]", max_bytes)
|
||
|
||
|
||
def _read_xlsx_text_sync(path: Path, max_bytes: int) -> str:
|
||
from openpyxl import load_workbook
|
||
|
||
wb = load_workbook(filename=str(path), read_only=True, data_only=True)
|
||
try:
|
||
lines: List[str] = []
|
||
for ws in wb.worksheets:
|
||
lines.append(f"## {ws.title}")
|
||
for row in ws.iter_rows(values_only=True):
|
||
cells = ["" if c is None else str(c) for c in row]
|
||
if any(x.strip() for x in cells):
|
||
lines.append("\t".join(cells))
|
||
body = "\n".join(lines)
|
||
finally:
|
||
wb.close()
|
||
return _truncate_utf8_text(body or "[xlsx 中未读到单元格文本]", max_bytes)
|
||
|
||
|
||
def _tessdata_dir_for_ocr() -> Optional[Path]:
|
||
"""返回 tessdata 目录(内含 .traineddata),传给 Tesseract --tessdata-dir。"""
|
||
raw = (getattr(settings, "TESSERACT_TESSDATA_DIR", None) or "").strip()
|
||
if raw:
|
||
p = Path(raw).expanduser().resolve()
|
||
return p if p.is_dir() else None
|
||
root = _local_file_workspace_root()
|
||
loc = root / "tessdata"
|
||
if loc.is_dir() and any(loc.glob("*.traineddata")):
|
||
return loc.resolve()
|
||
return None
|
||
|
||
|
||
def _read_image_ocr_sync(path: Path, max_bytes: int) -> str:
|
||
from PIL import Image
|
||
import pytesseract
|
||
|
||
cmd = (getattr(settings, "TESSERACT_CMD", None) or "").strip()
|
||
if cmd:
|
||
pytesseract.pytesseract.tesseract_cmd = cmd
|
||
tess_cfg = ""
|
||
td = _tessdata_dir_for_ocr()
|
||
if td is not None:
|
||
tess_cfg = f"--tessdata-dir {shlex.quote(str(td))}"
|
||
img = Image.open(path)
|
||
if img.mode not in ("RGB", "L"):
|
||
img = img.convert("RGB")
|
||
text = ""
|
||
last_err: Optional[Exception] = None
|
||
for lang in ("chi_sim+eng", "eng"):
|
||
try:
|
||
chunk = pytesseract.image_to_string(img, lang=lang, config=tess_cfg) or ""
|
||
if chunk.strip():
|
||
text = chunk
|
||
break
|
||
except Exception as e:
|
||
last_err = e
|
||
continue
|
||
if not text.strip() and last_err:
|
||
raise last_err
|
||
if not text.strip():
|
||
return "[图片中未识别到文字;可尝试更清晰、正向拍摄的作业照片]"
|
||
return _truncate_utf8_text(text, max_bytes)
|
||
|
||
|
||
async def file_read_tool(file_path: str) -> str:
|
||
"""
|
||
文件读取工具:UTF-8 文本、PDF 文本、docx、xlsx 单元格文本、常见图片 OCR(需本机 Tesseract)。
|
||
|
||
Args:
|
||
file_path: 文件路径(相对工作区根或工作区内绝对路径)
|
||
|
||
Returns:
|
||
JSON:file_path, content, size;或 error
|
||
"""
|
||
try:
|
||
max_bytes = int(getattr(settings, "LOCAL_FILE_READ_MAX_BYTES", 2_097_152) or 2_097_152)
|
||
path, err = _resolve_path_under_workspace(file_path)
|
||
if err:
|
||
return json.dumps({"error": err}, ensure_ascii=False)
|
||
if not path.is_file():
|
||
return json.dumps({"error": f"文件不存在或不是普通文件: {path}"}, ensure_ascii=False)
|
||
fsize = path.stat().st_size
|
||
if fsize > max_bytes:
|
||
return json.dumps(
|
||
{"error": f"文件过大({fsize} 字节),上限 {max_bytes}"},
|
||
ensure_ascii=False,
|
||
)
|
||
suffix = path.suffix.lower()
|
||
|
||
if suffix == ".doc":
|
||
return json.dumps(
|
||
{
|
||
"error": "暂不支持旧版 .doc,请在 Word 中另存为 .docx 后上传",
|
||
"file_path": str(path),
|
||
},
|
||
ensure_ascii=False,
|
||
)
|
||
|
||
content: str
|
||
extract_mode = "text"
|
||
|
||
if suffix in _FILE_READ_IMAGE_SUFFIXES:
|
||
extract_mode = "image_ocr"
|
||
try:
|
||
content = await asyncio.to_thread(_read_image_ocr_sync, path, max_bytes)
|
||
except ImportError as e:
|
||
return json.dumps(
|
||
{
|
||
"error": f"图片识别依赖未安装: {e}。请在后端环境执行 pip install Pillow pytesseract",
|
||
"file_path": str(path),
|
||
},
|
||
ensure_ascii=False,
|
||
)
|
||
except Exception as e:
|
||
err_low = str(e).lower()
|
||
if "tesseract" in err_low or "tesseractnotfound" in type(e).__name__.lower():
|
||
return json.dumps(
|
||
{
|
||
"error": (
|
||
"未找到 Tesseract OCR。请安装 Tesseract(Windows 可装官方安装包),"
|
||
"并在 .env 中配置 TESSERACT_CMD 指向 tesseract.exe,或将其加入 PATH;"
|
||
"中文识别需额外下载 chi_sim 语言包。"
|
||
),
|
||
"file_path": str(path),
|
||
},
|
||
ensure_ascii=False,
|
||
)
|
||
logger.warning("图片 OCR 失败: %s", e)
|
||
return json.dumps({"error": f"图片识别失败: {e}", "file_path": str(path)}, ensure_ascii=False)
|
||
|
||
elif suffix == ".pdf":
|
||
extract_mode = "pdf"
|
||
try:
|
||
content = await asyncio.to_thread(_read_pdf_text_sync, path, max_bytes)
|
||
except ImportError as e:
|
||
return json.dumps(
|
||
{"error": f"PDF 解析依赖未安装: {e}。请 pip install pypdf", "file_path": str(path)},
|
||
ensure_ascii=False,
|
||
)
|
||
except Exception as e:
|
||
logger.warning("PDF 解析失败: %s", e)
|
||
return json.dumps({"error": f"PDF 解析失败: {e}", "file_path": str(path)}, ensure_ascii=False)
|
||
|
||
elif suffix == ".docx":
|
||
extract_mode = "docx"
|
||
try:
|
||
content = await asyncio.to_thread(_read_docx_text_sync, path, max_bytes)
|
||
except ImportError as e:
|
||
return json.dumps(
|
||
{"error": f"Word 解析依赖未安装: {e}。请 pip install python-docx", "file_path": str(path)},
|
||
ensure_ascii=False,
|
||
)
|
||
except Exception as e:
|
||
logger.warning("docx 解析失败: %s", e)
|
||
return json.dumps({"error": f"docx 解析失败: {e}", "file_path": str(path)}, ensure_ascii=False)
|
||
|
||
elif suffix == ".xlsx":
|
||
extract_mode = "xlsx"
|
||
try:
|
||
content = await asyncio.to_thread(_read_xlsx_text_sync, path, max_bytes)
|
||
except ImportError as e:
|
||
return json.dumps(
|
||
{"error": f"Excel 解析依赖未安装: {e}。请 pip install openpyxl", "file_path": str(path)},
|
||
ensure_ascii=False,
|
||
)
|
||
except Exception as e:
|
||
logger.warning("xlsx 解析失败: %s", e)
|
||
return json.dumps({"error": f"xlsx 解析失败: {e}", "file_path": str(path)}, ensure_ascii=False)
|
||
|
||
elif suffix in _FILE_READ_TEXT_SUFFIXES or suffix == "":
|
||
extract_mode = "text"
|
||
content = path.read_text(encoding="utf-8", errors="replace")
|
||
content = _truncate_utf8_text(content, max_bytes)
|
||
else:
|
||
# 未知扩展名:仍尝试按 UTF-8 文本读(兼容无后缀文本)
|
||
extract_mode = "text"
|
||
content = path.read_text(encoding="utf-8", errors="replace")
|
||
content = _truncate_utf8_text(content, max_bytes)
|
||
|
||
out = {
|
||
"file_path": str(path),
|
||
"content": content,
|
||
"size": len(content.encode("utf-8")),
|
||
"extract_mode": extract_mode,
|
||
}
|
||
logger.info("file_read %s mode=%s content_len=%s", path, extract_mode, out["size"])
|
||
return json.dumps(out, ensure_ascii=False)
|
||
except FileNotFoundError:
|
||
return json.dumps({"error": f"文件不存在: {file_path}"}, ensure_ascii=False)
|
||
except Exception as e:
|
||
logger.error(f"文件读取工具执行失败: {str(e)}")
|
||
return json.dumps({"error": str(e)}, ensure_ascii=False)
|
||
|
||
|
||
async def file_write_tool(file_path: str, content: str, mode: str = "w") -> str:
|
||
"""
|
||
文件写入工具
|
||
|
||
Args:
|
||
file_path: 文件路径
|
||
content: 要写入的内容
|
||
mode: 写入模式(w=覆盖,a=追加)
|
||
|
||
Returns:
|
||
写入结果
|
||
"""
|
||
try:
|
||
max_bytes = int(getattr(settings, "LOCAL_FILE_WRITE_MAX_BYTES", 2_097_152) or 2_097_152)
|
||
raw = content if isinstance(content, str) else str(content)
|
||
enc_len = len(raw.encode("utf-8"))
|
||
if enc_len > max_bytes:
|
||
return json.dumps(
|
||
{"error": f"写入内容过大({enc_len} 字节 UTF-8),上限 {max_bytes}"},
|
||
ensure_ascii=False,
|
||
)
|
||
path, err = _resolve_path_under_workspace(file_path)
|
||
if err:
|
||
return json.dumps({"error": err}, ensure_ascii=False)
|
||
path.parent.mkdir(parents=True, exist_ok=True)
|
||
write_mode = "a" if mode == "a" else "w"
|
||
if write_mode == "a":
|
||
with path.open("a", encoding="utf-8") as f:
|
||
f.write(raw)
|
||
else:
|
||
path.write_text(raw, encoding="utf-8")
|
||
return json.dumps(
|
||
{
|
||
"success": True,
|
||
"file_path": str(path),
|
||
"mode": write_mode,
|
||
"content_length": enc_len,
|
||
},
|
||
ensure_ascii=False,
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"文件写入工具执行失败: {str(e)}")
|
||
return json.dumps({"error": str(e)}, ensure_ascii=False)
|
||
|
||
|
||
async def text_analyze_tool(text: str, operation: str = "count") -> str:
|
||
"""
|
||
文本分析工具
|
||
|
||
Args:
|
||
text: 要分析的文本
|
||
operation: 操作类型(count=统计, keywords=提取关键词, summary=摘要)
|
||
|
||
Returns:
|
||
分析结果
|
||
"""
|
||
try:
|
||
if operation == "count":
|
||
# 统计字数、字符数、行数等
|
||
char_count = len(text)
|
||
char_count_no_spaces = len(text.replace(" ", ""))
|
||
word_count = len(text.split())
|
||
line_count = len(text.splitlines())
|
||
paragraph_count = len([p for p in text.split("\n\n") if p.strip()])
|
||
|
||
return json.dumps({
|
||
"char_count": char_count,
|
||
"char_count_no_spaces": char_count_no_spaces,
|
||
"word_count": word_count,
|
||
"line_count": line_count,
|
||
"paragraph_count": paragraph_count
|
||
}, ensure_ascii=False)
|
||
|
||
elif operation == "keywords":
|
||
# 简单的关键词提取(基于词频)
|
||
words = re.findall(r'\b\w+\b', text.lower())
|
||
word_freq = {}
|
||
for word in words:
|
||
if len(word) > 2: # 忽略太短的词
|
||
word_freq[word] = word_freq.get(word, 0) + 1
|
||
|
||
# 取频率最高的10个词
|
||
top_keywords = sorted(word_freq.items(), key=lambda x: x[1], reverse=True)[:10]
|
||
|
||
return json.dumps({
|
||
"keywords": [{"word": k, "frequency": v} for k, v in top_keywords]
|
||
}, ensure_ascii=False)
|
||
|
||
elif operation == "summary":
|
||
# 简单的摘要(取前3句)
|
||
sentences = re.split(r'[.!?。!?]\s+', text)
|
||
summary = ". ".join(sentences[:3]) + "."
|
||
|
||
return json.dumps({
|
||
"summary": summary,
|
||
"original_length": len(text),
|
||
"summary_length": len(summary)
|
||
}, ensure_ascii=False)
|
||
|
||
else:
|
||
return json.dumps({
|
||
"error": f"不支持的操作类型: {operation}"
|
||
}, ensure_ascii=False)
|
||
except Exception as e:
|
||
logger.error(f"文本分析工具执行失败: {str(e)}")
|
||
return json.dumps({
|
||
"error": str(e)
|
||
}, ensure_ascii=False)
|
||
|
||
|
||
async def datetime_tool(operation: str = "now", format: str = "%Y-%m-%d %H:%M:%S",
|
||
timezone: Optional[str] = None) -> str:
|
||
"""
|
||
日期时间工具
|
||
|
||
Args:
|
||
operation: 操作类型(now=当前时间, format=格式化, parse=解析)
|
||
format: 时间格式
|
||
timezone: 时区(暂未实现)
|
||
|
||
Returns:
|
||
日期时间结果
|
||
"""
|
||
try:
|
||
if operation == "now":
|
||
now = datetime.now()
|
||
return json.dumps({
|
||
"datetime": now.strftime(format),
|
||
"timestamp": now.timestamp(),
|
||
"iso_format": now.isoformat()
|
||
}, ensure_ascii=False)
|
||
|
||
elif operation == "format":
|
||
now = datetime.now()
|
||
return json.dumps({
|
||
"formatted": now.strftime(format)
|
||
}, ensure_ascii=False)
|
||
|
||
else:
|
||
return json.dumps({
|
||
"error": f"不支持的操作类型: {operation}"
|
||
}, ensure_ascii=False)
|
||
except Exception as e:
|
||
logger.error(f"日期时间工具执行失败: {str(e)}")
|
||
return json.dumps({
|
||
"error": str(e)
|
||
}, ensure_ascii=False)
|
||
|
||
|
||
async def math_calculate_tool(expression: str) -> str:
|
||
"""
|
||
数学计算工具(安全限制:只允许基本数学运算)
|
||
|
||
Args:
|
||
expression: 数学表达式(如 "2+2", "sqrt(16)", "sin(0)")
|
||
|
||
Returns:
|
||
计算结果
|
||
"""
|
||
try:
|
||
# 安全检查:只允许数字、基本运算符和安全的数学函数
|
||
allowed_chars = set("0123456789+-*/.() ")
|
||
allowed_functions = ['sqrt', 'sin', 'cos', 'tan', 'log', 'exp', 'abs', 'pow']
|
||
|
||
# 检查表达式是否安全
|
||
if not all(c in allowed_chars or any(f in expression for f in allowed_functions) for c in expression):
|
||
return json.dumps({
|
||
"error": "表达式包含不安全的字符"
|
||
}, ensure_ascii=False)
|
||
|
||
# 构建安全的数学环境
|
||
safe_dict = {
|
||
"__builtins__": {},
|
||
"abs": abs,
|
||
"sqrt": math.sqrt,
|
||
"sin": math.sin,
|
||
"cos": math.cos,
|
||
"tan": math.tan,
|
||
"log": math.log,
|
||
"exp": math.exp,
|
||
"pow": pow,
|
||
"pi": math.pi,
|
||
"e": math.e
|
||
}
|
||
|
||
result = eval(expression, safe_dict)
|
||
|
||
return json.dumps({
|
||
"expression": expression,
|
||
"result": result,
|
||
"result_type": type(result).__name__
|
||
}, ensure_ascii=False)
|
||
except Exception as e:
|
||
logger.error(f"数学计算工具执行失败: {str(e)}")
|
||
return json.dumps({
|
||
"error": str(e)
|
||
}, ensure_ascii=False)
|
||
|
||
|
||
async def system_info_tool() -> str:
|
||
"""
|
||
系统信息工具
|
||
|
||
Returns:
|
||
系统信息
|
||
"""
|
||
try:
|
||
info = {
|
||
"platform": platform.system(),
|
||
"platform_version": platform.version(),
|
||
"architecture": platform.machine(),
|
||
"processor": platform.processor(),
|
||
"python_version": sys.version,
|
||
"python_version_info": {
|
||
"major": sys.version_info.major,
|
||
"minor": sys.version_info.minor,
|
||
"micro": sys.version_info.micro,
|
||
},
|
||
# file_read/file_write 使用的允许根目录(与 LOCAL_FILE_TOOLS_ROOT 一致)
|
||
"local_file_workspace_root": str(_local_file_workspace_root()),
|
||
}
|
||
|
||
return json.dumps(info, ensure_ascii=False)
|
||
except Exception as e:
|
||
logger.error(f"系统信息工具执行失败: {str(e)}")
|
||
return json.dumps({
|
||
"error": str(e)
|
||
}, ensure_ascii=False)
|
||
|
||
|
||
async def json_process_tool(json_string: str, operation: str = "parse") -> str:
|
||
"""
|
||
JSON处理工具
|
||
|
||
Args:
|
||
json_string: JSON字符串
|
||
operation: 操作类型(parse=解析, stringify=序列化, validate=验证)
|
||
|
||
Returns:
|
||
处理结果
|
||
"""
|
||
try:
|
||
if operation == "parse":
|
||
data = json.loads(json_string)
|
||
return json.dumps({
|
||
"parsed": data,
|
||
"type": type(data).__name__
|
||
}, ensure_ascii=False)
|
||
|
||
elif operation == "stringify":
|
||
# 如果输入已经是字符串,尝试解析后再序列化
|
||
try:
|
||
data = json.loads(json_string)
|
||
except:
|
||
data = json_string
|
||
|
||
return json.dumps({
|
||
"stringified": json.dumps(data, ensure_ascii=False, indent=2)
|
||
}, ensure_ascii=False)
|
||
|
||
elif operation == "validate":
|
||
try:
|
||
json.loads(json_string)
|
||
return json.dumps({
|
||
"valid": True
|
||
}, ensure_ascii=False)
|
||
except json.JSONDecodeError as e:
|
||
return json.dumps({
|
||
"valid": False,
|
||
"error": str(e)
|
||
}, ensure_ascii=False)
|
||
|
||
else:
|
||
return json.dumps({
|
||
"error": f"不支持的操作类型: {operation}"
|
||
}, ensure_ascii=False)
|
||
except Exception as e:
|
||
logger.error(f"JSON处理工具执行失败: {str(e)}")
|
||
return json.dumps({
|
||
"error": str(e)
|
||
}, ensure_ascii=False)
|
||
|
||
|
||
def _validate_sql_query(sql: str) -> Tuple[bool, Optional[str]]:
|
||
"""
|
||
验证SQL查询的安全性
|
||
|
||
Args:
|
||
sql: SQL查询语句
|
||
|
||
Returns:
|
||
(是否安全, 错误信息)
|
||
"""
|
||
# 去除注释和多余空白
|
||
sql_clean = re.sub(r'--.*?\n', '', sql)
|
||
sql_clean = re.sub(r'/\*.*?\*/', '', sql_clean, flags=re.DOTALL)
|
||
sql_clean = ' '.join(sql_clean.split())
|
||
|
||
# 转换为小写以便检查
|
||
sql_lower = sql_clean.lower().strip()
|
||
|
||
# 只允许SELECT查询
|
||
if not sql_lower.startswith('select'):
|
||
return False, "只允许SELECT查询,不允许INSERT、UPDATE、DELETE、DROP等操作"
|
||
|
||
# 检查危险关键字
|
||
dangerous_keywords = [
|
||
'insert', 'update', 'delete', 'drop', 'truncate', 'alter',
|
||
'create', 'grant', 'revoke', 'exec', 'execute', 'call',
|
||
'commit', 'rollback', 'savepoint'
|
||
]
|
||
|
||
for keyword in dangerous_keywords:
|
||
# 使用单词边界匹配,避免误判(如"select"中包含"select")
|
||
pattern = r'\b' + keyword + r'\b'
|
||
if re.search(pattern, sql_lower):
|
||
return False, f"检测到危险关键字: {keyword.upper()},查询被拒绝"
|
||
|
||
# 检查是否有多个SQL语句(防止SQL注入)
|
||
if ';' in sql_clean and sql_clean.count(';') > 1:
|
||
return False, "不允许执行多个SQL语句"
|
||
|
||
return True, None
|
||
|
||
|
||
async def _execute_default_db_query(sql: str, timeout: int = 30) -> List[Dict[str, Any]]:
|
||
"""
|
||
执行默认数据库查询
|
||
|
||
Args:
|
||
sql: SQL查询语句
|
||
timeout: 查询超时时间(秒)
|
||
|
||
Returns:
|
||
查询结果列表
|
||
"""
|
||
from app.core.database import engine
|
||
|
||
try:
|
||
# 使用asyncio实现超时控制
|
||
loop = asyncio.get_event_loop()
|
||
|
||
def _execute():
|
||
with engine.connect() as connection:
|
||
result = connection.execute(text(sql))
|
||
# 获取列名
|
||
columns = result.keys()
|
||
# 获取所有行
|
||
rows = result.fetchall()
|
||
# 转换为字典列表
|
||
return [dict(zip(columns, row)) for row in rows]
|
||
|
||
# 执行查询,带超时控制
|
||
result = await asyncio.wait_for(
|
||
asyncio.to_thread(_execute),
|
||
timeout=timeout
|
||
)
|
||
return result
|
||
except asyncio.TimeoutError:
|
||
raise Exception(f"查询超时(超过{timeout}秒)")
|
||
except SQLAlchemyError as e:
|
||
raise Exception(f"数据库查询失败: {str(e)}")
|
||
except Exception as e:
|
||
raise Exception(f"执行查询时发生错误: {str(e)}")
|
||
|
||
|
||
async def _execute_data_source_query(data_source_id: str, sql: str, timeout: int = 30) -> List[Dict[str, Any]]:
|
||
"""
|
||
通过数据源ID执行查询
|
||
|
||
Args:
|
||
data_source_id: 数据源ID
|
||
sql: SQL查询语句
|
||
timeout: 查询超时时间(秒)
|
||
|
||
Returns:
|
||
查询结果列表
|
||
"""
|
||
from app.core.database import SessionLocal
|
||
from app.models.data_source import DataSource
|
||
from app.services.data_source_connector import create_connector
|
||
|
||
db = SessionLocal()
|
||
try:
|
||
# 获取数据源配置
|
||
data_source = db.query(DataSource).filter(DataSource.id == data_source_id).first()
|
||
if not data_source:
|
||
raise Exception(f"数据源不存在: {data_source_id}")
|
||
|
||
if data_source.status != 'active':
|
||
raise Exception(f"数据源状态异常: {data_source.status}")
|
||
|
||
# 创建连接器
|
||
connector = create_connector(data_source.type, data_source.config)
|
||
|
||
# 执行查询(带超时控制)
|
||
query_params = {'sql': sql}
|
||
|
||
# 使用asyncio实现超时控制
|
||
def _execute():
|
||
return connector.query(query_params)
|
||
|
||
result = await asyncio.wait_for(
|
||
asyncio.to_thread(_execute),
|
||
timeout=timeout
|
||
)
|
||
|
||
# 确保结果是列表格式
|
||
if not isinstance(result, list):
|
||
result = [result] if result else []
|
||
|
||
return result
|
||
except asyncio.TimeoutError:
|
||
raise Exception(f"查询超时(超过{timeout}秒)")
|
||
except Exception as e:
|
||
raise Exception(f"数据源查询失败: {str(e)}")
|
||
finally:
|
||
db.close()
|
||
|
||
|
||
async def database_query_tool(
|
||
query: str,
|
||
database: str = "default",
|
||
data_source_id: Optional[str] = None,
|
||
timeout: int = 30
|
||
) -> str:
|
||
"""
|
||
数据库查询工具
|
||
|
||
Args:
|
||
query: SQL查询语句(只允许SELECT)
|
||
database: 数据库名称(已废弃,保留用于兼容)
|
||
data_source_id: 数据源ID(可选,如果提供则使用指定数据源)
|
||
timeout: 查询超时时间(秒,默认30秒)
|
||
|
||
Returns:
|
||
JSON格式的查询结果
|
||
"""
|
||
try:
|
||
# 验证SQL安全性
|
||
is_safe, error_msg = _validate_sql_query(query)
|
||
if not is_safe:
|
||
return json.dumps({
|
||
"error": error_msg,
|
||
"query": query
|
||
}, ensure_ascii=False)
|
||
|
||
# 验证超时时间
|
||
if timeout <= 0 or timeout > 300:
|
||
return json.dumps({
|
||
"error": "超时时间必须在1-300秒之间",
|
||
"timeout": timeout
|
||
}, ensure_ascii=False)
|
||
|
||
# 执行查询
|
||
if data_source_id:
|
||
# 使用指定的数据源
|
||
result = await _execute_data_source_query(data_source_id, query, timeout)
|
||
else:
|
||
# 使用默认数据库
|
||
result = await _execute_default_db_query(query, timeout)
|
||
|
||
# 格式化结果
|
||
return json.dumps({
|
||
"success": True,
|
||
"row_count": len(result),
|
||
"data": result,
|
||
"query": query
|
||
}, ensure_ascii=False, default=str)
|
||
|
||
except Exception as e:
|
||
logger.error(f"数据库查询工具执行失败: {str(e)}")
|
||
return json.dumps({
|
||
"error": str(e),
|
||
"query": query
|
||
}, ensure_ascii=False)
|
||
|
||
|
||
async def adb_log_tool(
|
||
command: str = "logcat",
|
||
filter_tag: Optional[str] = None,
|
||
level: Optional[str] = None,
|
||
max_lines: int = 100,
|
||
timeout: int = 10
|
||
) -> str:
|
||
"""
|
||
ADB日志工具 - 获取Android设备日志
|
||
|
||
Args:
|
||
command: ADB命令类型(logcat=获取日志, devices=列出设备, shell=执行shell命令)
|
||
filter_tag: 日志标签过滤(如 "ActivityManager", "SystemServer")
|
||
level: 日志级别过滤(V/D/I/W/E/F/S,如 "E" 只显示错误)
|
||
max_lines: 最大返回行数(默认100行,避免输出过长)
|
||
timeout: 命令执行超时时间(秒,默认10秒)
|
||
|
||
Returns:
|
||
JSON格式的执行结果
|
||
"""
|
||
try:
|
||
# 验证超时时间
|
||
if timeout <= 0 or timeout > 60:
|
||
return json.dumps({
|
||
"error": "超时时间必须在1-60秒之间",
|
||
"timeout": timeout
|
||
}, ensure_ascii=False)
|
||
|
||
# 验证最大行数
|
||
if max_lines <= 0 or max_lines > 10000:
|
||
return json.dumps({
|
||
"error": "最大行数必须在1-10000之间",
|
||
"max_lines": max_lines
|
||
}, ensure_ascii=False)
|
||
|
||
# 构建adb命令
|
||
if command == "logcat":
|
||
# 获取日志
|
||
adb_cmd = ["adb", "logcat", "-d"] # -d 表示获取已缓存的日志
|
||
|
||
# 添加日志级别过滤
|
||
if level:
|
||
level = level.upper()
|
||
if level in ["V", "D", "I", "W", "E", "F", "S"]:
|
||
adb_cmd.extend([f"*:{level}"])
|
||
else:
|
||
return json.dumps({
|
||
"error": f"无效的日志级别: {level},支持: V/D/I/W/E/F/S"
|
||
}, ensure_ascii=False)
|
||
|
||
# 添加标签过滤
|
||
if filter_tag:
|
||
adb_cmd.append(filter_tag)
|
||
|
||
# 限制输出行数
|
||
adb_cmd.extend(["-t", str(max_lines)])
|
||
|
||
elif command == "devices":
|
||
# 列出连接的设备
|
||
adb_cmd = ["adb", "devices", "-l"]
|
||
|
||
elif command == "shell":
|
||
# 执行shell命令(受限,只允许安全命令)
|
||
if filter_tag:
|
||
# filter_tag在这里作为shell命令
|
||
# 只允许安全的命令
|
||
safe_commands = ["getprop", "dumpsys", "pm", "am", "settings"]
|
||
cmd_parts = filter_tag.split()
|
||
if not cmd_parts or cmd_parts[0] not in safe_commands:
|
||
return json.dumps({
|
||
"error": f"不允许执行命令: {filter_tag},只允许: {', '.join(safe_commands)}"
|
||
}, ensure_ascii=False)
|
||
adb_cmd = ["adb", "shell"] + cmd_parts
|
||
else:
|
||
return json.dumps({
|
||
"error": "shell命令需要提供filter_tag参数作为命令"
|
||
}, ensure_ascii=False)
|
||
else:
|
||
return json.dumps({
|
||
"error": f"不支持的ADB命令: {command},支持: logcat/devices/shell"
|
||
}, ensure_ascii=False)
|
||
|
||
# 执行adb命令
|
||
def _execute_adb():
|
||
try:
|
||
result = subprocess.run(
|
||
adb_cmd,
|
||
capture_output=True,
|
||
text=True,
|
||
timeout=timeout,
|
||
encoding='utf-8',
|
||
errors='replace' # 处理编码错误
|
||
)
|
||
return result
|
||
except subprocess.TimeoutExpired:
|
||
raise Exception(f"ADB命令执行超时(超过{timeout}秒)")
|
||
except FileNotFoundError:
|
||
raise Exception("未找到adb命令,请确保已安装Android SDK Platform Tools并配置PATH")
|
||
except Exception as e:
|
||
raise Exception(f"执行ADB命令失败: {str(e)}")
|
||
|
||
# 异步执行命令
|
||
result = await asyncio.wait_for(
|
||
asyncio.to_thread(_execute_adb),
|
||
timeout=timeout + 2 # 额外2秒缓冲
|
||
)
|
||
|
||
# 处理结果
|
||
output_lines = result.stdout.split('\n') if result.stdout else []
|
||
error_lines = result.stderr.split('\n') if result.stderr else []
|
||
|
||
# 如果输出太长,截断
|
||
if len(output_lines) > max_lines:
|
||
output_lines = output_lines[:max_lines]
|
||
output_lines.append(f"... (已截断,共 {len(result.stdout.split(chr(10)))} 行)")
|
||
|
||
return json.dumps({
|
||
"success": result.returncode == 0,
|
||
"command": " ".join(adb_cmd),
|
||
"return_code": result.returncode,
|
||
"output": "\n".join(output_lines),
|
||
"output_lines": len(output_lines),
|
||
"error": "\n".join(error_lines) if error_lines and any(error_lines) else None,
|
||
"timestamp": datetime.now().isoformat()
|
||
}, ensure_ascii=False)
|
||
|
||
except asyncio.TimeoutError:
|
||
return json.dumps({
|
||
"error": f"ADB命令执行超时(超过{timeout}秒)",
|
||
"command": " ".join(adb_cmd) if 'adb_cmd' in locals() else command
|
||
}, ensure_ascii=False)
|
||
except Exception as e:
|
||
logger.error(f"ADB日志工具执行失败: {str(e)}")
|
||
return json.dumps({
|
||
"error": str(e),
|
||
"command": " ".join(adb_cmd) if 'adb_cmd' in locals() else command
|
||
}, ensure_ascii=False)
|
||
|
||
|
||
# 工具定义(OpenAI Function格式)
|
||
HTTP_REQUEST_SCHEMA = {
|
||
"type": "function",
|
||
"function": {
|
||
"name": "http_request",
|
||
"description": "发送HTTP请求,支持GET、POST、PUT、DELETE方法。可以用于调用API、获取网页内容等。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"url": {
|
||
"type": "string",
|
||
"description": "请求的URL地址"
|
||
},
|
||
"method": {
|
||
"type": "string",
|
||
"enum": ["GET", "POST", "PUT", "DELETE"],
|
||
"description": "HTTP请求方法",
|
||
"default": "GET"
|
||
},
|
||
"headers": {
|
||
"type": "object",
|
||
"description": "HTTP请求头(可选)",
|
||
"additionalProperties": {
|
||
"type": "string"
|
||
}
|
||
},
|
||
"body": {
|
||
"type": "object",
|
||
"description": "请求体(POST/PUT时使用,可选)"
|
||
},
|
||
"max_body_chars": {
|
||
"type": "integer",
|
||
"description": (
|
||
"响应正文写入结果的最大字符数(可选)。"
|
||
"门户首页等大 HTML 默认会按平台配置截断;摘要单篇文章可适当调大(如 80000),仍可能受模型总上下文限制。"
|
||
),
|
||
},
|
||
},
|
||
"required": ["url", "method"]
|
||
}
|
||
}
|
||
}
|
||
|
||
FILE_READ_SCHEMA = {
|
||
"type": "function",
|
||
"function": {
|
||
"name": "file_read",
|
||
"description": (
|
||
"读取工作区内文件并尽量提取可读文本:UTF-8 文本、.pdf 文字层、.docx 段落、.xlsx 单元格、"
|
||
"常见图片(.png/.jpg 等)用 OCR 提取文字(需服务器安装 Tesseract,可选配置 TESSERACT_CMD)。"
|
||
"用户上传附件后消息中会给出相对路径,请用本工具读取该路径。路径须落在 LOCAL_FILE_TOOLS_ROOT 下。"
|
||
),
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"file_path": {
|
||
"type": "string",
|
||
"description": "要读取的文件路径(相对于项目根目录或绝对路径)"
|
||
}
|
||
},
|
||
"required": ["file_path"]
|
||
}
|
||
}
|
||
}
|
||
|
||
FILE_WRITE_SCHEMA = {
|
||
"type": "function",
|
||
"function": {
|
||
"name": "file_write",
|
||
"description": "写入本地文本文件(UTF-8,w 覆盖 / a 追加)。路径须落在工作区内;父目录不存在会自动创建。勿写入密码、密钥等大型敏感内容。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"file_path": {
|
||
"type": "string",
|
||
"description": "要写入的文件路径(相对于项目根目录或绝对路径)"
|
||
},
|
||
"content": {
|
||
"type": "string",
|
||
"description": "要写入的内容"
|
||
},
|
||
"mode": {
|
||
"type": "string",
|
||
"enum": ["w", "a"],
|
||
"description": "写入模式:w=覆盖写入,a=追加写入",
|
||
"default": "w"
|
||
}
|
||
},
|
||
"required": ["file_path", "content"]
|
||
}
|
||
}
|
||
}
|
||
|
||
TEXT_ANALYZE_SCHEMA = {
|
||
"type": "function",
|
||
"function": {
|
||
"name": "text_analyze",
|
||
"description": "分析文本内容,支持统计字数、提取关键词、生成摘要等功能。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"text": {
|
||
"type": "string",
|
||
"description": "要分析的文本内容"
|
||
},
|
||
"operation": {
|
||
"type": "string",
|
||
"enum": ["count", "keywords", "summary"],
|
||
"description": "操作类型:count=统计字数等信息,keywords=提取关键词,summary=生成摘要",
|
||
"default": "count"
|
||
}
|
||
},
|
||
"required": ["text"]
|
||
}
|
||
}
|
||
}
|
||
|
||
DATETIME_SCHEMA = {
|
||
"type": "function",
|
||
"function": {
|
||
"name": "datetime",
|
||
"description": "获取和处理日期时间信息,支持获取当前时间、格式化时间等。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"operation": {
|
||
"type": "string",
|
||
"enum": ["now", "format"],
|
||
"description": "操作类型:now=获取当前时间,format=格式化时间",
|
||
"default": "now"
|
||
},
|
||
"format": {
|
||
"type": "string",
|
||
"description": "时间格式字符串(如:%Y-%m-%d %H:%M:%S)",
|
||
"default": "%Y-%m-%d %H:%M:%S"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
MATH_CALCULATE_SCHEMA = {
|
||
"type": "function",
|
||
"function": {
|
||
"name": "math_calculate",
|
||
"description": "执行数学计算,支持基本运算和常用数学函数(如sqrt, sin, cos, log等)。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"expression": {
|
||
"type": "string",
|
||
"description": "数学表达式(如:2+2, sqrt(16), sin(0), pow(2,3))"
|
||
}
|
||
},
|
||
"required": ["expression"]
|
||
}
|
||
}
|
||
}
|
||
|
||
SYSTEM_INFO_SCHEMA = {
|
||
"type": "function",
|
||
"function": {
|
||
"name": "system_info",
|
||
"description": "获取系统信息(含 local_file_workspace_root:file_read/file_write 允许访问的根目录)。用户问「工作区路径」时应调用本工具并如实转述该字段。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {}
|
||
}
|
||
}
|
||
}
|
||
|
||
JSON_PROCESS_SCHEMA = {
|
||
"type": "function",
|
||
"function": {
|
||
"name": "json_process",
|
||
"description": "处理JSON数据,支持解析、序列化、验证等功能。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"json_string": {
|
||
"type": "string",
|
||
"description": "JSON字符串"
|
||
},
|
||
"operation": {
|
||
"type": "string",
|
||
"enum": ["parse", "stringify", "validate"],
|
||
"description": "操作类型:parse=解析JSON,stringify=序列化为JSON,validate=验证JSON格式",
|
||
"default": "parse"
|
||
}
|
||
},
|
||
"required": ["json_string"]
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
ADB_LOG_SCHEMA = {
|
||
"type": "function",
|
||
"function": {
|
||
"name": "adb_log",
|
||
"description": "执行ADB命令获取Android设备日志。支持logcat(获取日志)、devices(列出设备)、shell(执行shell命令)。可以过滤日志标签和级别。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"command": {
|
||
"type": "string",
|
||
"enum": ["logcat", "devices", "shell"],
|
||
"description": "ADB命令类型:logcat=获取日志,devices=列出连接的设备,shell=执行shell命令",
|
||
"default": "logcat"
|
||
},
|
||
"filter_tag": {
|
||
"type": "string",
|
||
"description": "日志标签过滤(如 'ActivityManager', 'SystemServer')或shell命令(当command=shell时)"
|
||
},
|
||
"level": {
|
||
"type": "string",
|
||
"enum": ["V", "D", "I", "W", "E", "F", "S"],
|
||
"description": "日志级别过滤:V=Verbose, D=Debug, I=Info, W=Warning, E=Error, F=Fatal, S=Silent"
|
||
},
|
||
"max_lines": {
|
||
"type": "integer",
|
||
"description": "最大返回行数(默认100行,避免输出过长)",
|
||
"default": 100
|
||
},
|
||
"timeout": {
|
||
"type": "integer",
|
||
"description": "命令执行超时时间(秒,默认10秒,最大60秒)",
|
||
"default": 10
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
DATABASE_QUERY_SCHEMA = {
|
||
"type": "function",
|
||
"function": {
|
||
"name": "database_query",
|
||
"description": "执行数据库查询(只允许SELECT查询,支持默认数据库和指定数据源)。可以查询工作流、Agent、执行记录等系统数据,或通过数据源ID查询外部数据库。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"query": {
|
||
"type": "string",
|
||
"description": "SQL查询语句(只允许SELECT查询,不允许INSERT、UPDATE、DELETE等操作)"
|
||
},
|
||
"data_source_id": {
|
||
"type": "string",
|
||
"description": "数据源ID(可选,如果提供则使用指定的数据源,否则使用默认数据库)"
|
||
},
|
||
"timeout": {
|
||
"type": "integer",
|
||
"description": "查询超时时间(秒,默认30秒,最大300秒)",
|
||
"default": 30,
|
||
"minimum": 1,
|
||
"maximum": 300
|
||
}
|
||
},
|
||
"required": ["query"]
|
||
}
|
||
}
|
||
}
|
||
|
||
# ─── 加密/哈希/编码工具 ──────────────────────────────────
|
||
|
||
async def crypto_util_tool(
|
||
operation: str = "uuid",
|
||
data: Optional[str] = None,
|
||
algorithm: str = "sha256",
|
||
) -> str:
|
||
"""
|
||
加密/哈希/编码工具。
|
||
|
||
Args:
|
||
operation: 操作类型
|
||
- uuid: 生成 UUID
|
||
- base64_encode: Base64 编码
|
||
- base64_decode: Base64 解码
|
||
- hash: 计算哈希值
|
||
data: 输入数据(base64/hash 操作需要)
|
||
algorithm: 哈希算法(hash 操作时使用,支持 md5/sha1/sha256/sha512)
|
||
|
||
Returns:
|
||
操作结果
|
||
"""
|
||
import hashlib
|
||
import base64
|
||
import uuid as _uuid
|
||
|
||
try:
|
||
if operation == "uuid":
|
||
u = str(_uuid.uuid4())
|
||
return json.dumps({"uuid": u, "type": "uuid4"}, ensure_ascii=False)
|
||
|
||
elif operation == "base64_encode":
|
||
if not data:
|
||
return json.dumps({"error": "data 参数不能为空"}, ensure_ascii=False)
|
||
encoded = base64.b64encode(data.encode("utf-8")).decode("utf-8")
|
||
return json.dumps({"encoded": encoded}, ensure_ascii=False)
|
||
|
||
elif operation == "base64_decode":
|
||
if not data:
|
||
return json.dumps({"error": "data 参数不能为空"}, ensure_ascii=False)
|
||
try:
|
||
decoded = base64.b64decode(data.encode("utf-8")).decode("utf-8")
|
||
return json.dumps({"decoded": decoded}, ensure_ascii=False)
|
||
except Exception as e:
|
||
return json.dumps({"error": f"Base64 解码失败: {e}"}, ensure_ascii=False)
|
||
|
||
elif operation == "hash":
|
||
if not data:
|
||
return json.dumps({"error": "data 参数不能为空"}, ensure_ascii=False)
|
||
valid_algos = {"md5": hashlib.md5, "sha1": hashlib.sha1,
|
||
"sha256": hashlib.sha256, "sha512": hashlib.sha512}
|
||
if algorithm not in valid_algos:
|
||
return json.dumps(
|
||
{"error": f"不支持的算法: {algorithm},支持: {list(valid_algos.keys())}"},
|
||
ensure_ascii=False,
|
||
)
|
||
h = valid_algos[algorithm](data.encode("utf-8")).hexdigest()
|
||
return json.dumps({"algorithm": algorithm, "hash": h}, ensure_ascii=False)
|
||
|
||
else:
|
||
return json.dumps(
|
||
{"error": f"不支持的操作: {operation},支持: uuid/base64_encode/base64_decode/hash"},
|
||
ensure_ascii=False,
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"crypto_util 工具执行失败: {e}", exc_info=True)
|
||
return json.dumps({"error": f"操作失败: {e}"}, ensure_ascii=False)
|
||
|
||
|
||
# ─── 随机生成工具 ─────────────────────────────────────────
|
||
|
||
async def random_generate_tool(
|
||
generate_type: str = "password",
|
||
length: int = 16,
|
||
count: int = 1,
|
||
min_val: float = 0,
|
||
max_val: float = 100,
|
||
) -> str:
|
||
"""
|
||
随机数据生成工具。
|
||
|
||
Args:
|
||
generate_type: 生成类型
|
||
- password: 安全密码(含大小写字母+数字+符号)
|
||
- string: 随机字符串(字母+数字)
|
||
- number: 随机整数
|
||
- float: 随机浮点数
|
||
length: 密码/字符串长度(默认16)
|
||
count: 生成数量(默认1)
|
||
min_val: 随机数最小值(number/float 时使用)
|
||
max_val: 随机数最大值(number/float 时使用)
|
||
|
||
Returns:
|
||
生成的随机数据
|
||
"""
|
||
import random
|
||
import string as _string
|
||
import secrets
|
||
|
||
try:
|
||
count = max(1, min(count, 50)) # 限制最多 50 个
|
||
|
||
if generate_type == "password":
|
||
length = max(8, min(length, 128))
|
||
chars = _string.ascii_letters + _string.digits + "!@#$%^&*()_+-=[]{}|;:,.<>?"
|
||
result = []
|
||
for _ in range(count):
|
||
pwd = "".join(secrets.choice(chars) for _ in range(length))
|
||
result.append(pwd)
|
||
return json.dumps({
|
||
"passwords": result,
|
||
"length": length,
|
||
"strength": "high" if length >= 16 else "medium" if length >= 12 else "basic",
|
||
}, ensure_ascii=False)
|
||
|
||
elif generate_type == "string":
|
||
length = max(1, min(length, 256))
|
||
chars = _string.ascii_letters + _string.digits
|
||
result = ["".join(secrets.choice(chars) for _ in range(length)) for _ in range(count)]
|
||
return json.dumps({"strings": result, "length": length}, ensure_ascii=False)
|
||
|
||
elif generate_type == "number":
|
||
min_val, max_val = int(min_val), int(max_val)
|
||
if min_val > max_val:
|
||
min_val, max_val = max_val, min_val
|
||
result = [secrets.randbelow(max_val - min_val + 1) + min_val for _ in range(count)]
|
||
return json.dumps({"numbers": result, "range": [min_val, max_val]}, ensure_ascii=False)
|
||
|
||
elif generate_type == "float":
|
||
if min_val > max_val:
|
||
min_val, max_val = max_val, min_val
|
||
result = [round(random.uniform(min_val, max_val), 6) for _ in range(count)]
|
||
return json.dumps({"floats": result, "range": [min_val, max_val]}, ensure_ascii=False)
|
||
|
||
else:
|
||
return json.dumps(
|
||
{"error": f"不支持的类型: {generate_type},支持: password/string/number/float"},
|
||
ensure_ascii=False,
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"random_generate 工具执行失败: {e}", exc_info=True)
|
||
return json.dumps({"error": f"生成失败: {e}"}, ensure_ascii=False)
|
||
|
||
|
||
# ─── 邮件发送工具 ─────────────────────────────────────────
|
||
|
||
async def send_email_tool(
|
||
to: str,
|
||
subject: str,
|
||
body: str,
|
||
smtp_host: str = "",
|
||
smtp_port: int = 587,
|
||
smtp_user: str = "",
|
||
smtp_password: str = "",
|
||
from_name: str = "",
|
||
body_type: str = "plain",
|
||
) -> str:
|
||
"""
|
||
发送邮件工具。
|
||
|
||
Args:
|
||
to: 收件人邮箱地址(多个用逗号分隔)
|
||
subject: 邮件主题
|
||
body: 邮件正文
|
||
smtp_host: SMTP 服务器地址(如 smtp.qq.com),留空从环境变量读取
|
||
smtp_port: SMTP 端口(默认 587)
|
||
smtp_user: SMTP 用户名/发件人邮箱,留空从环境变量读取
|
||
smtp_password: SMTP 密码/授权码,留空从环境变量读取
|
||
from_name: 发件人显示名称(可选)
|
||
body_type: 正文类型 plain=纯文本, html=HTML
|
||
|
||
Returns:
|
||
发送结果
|
||
"""
|
||
try:
|
||
# 从环境变量读取 SMTP 配置(如果未传参)
|
||
import os
|
||
host = smtp_host or os.getenv("SMTP_HOST", "")
|
||
user = smtp_user or os.getenv("SMTP_USER", "")
|
||
password = smtp_password or os.getenv("SMTP_PASSWORD", "")
|
||
|
||
if not host or not user or not password:
|
||
return json.dumps({
|
||
"error": "SMTP 配置不完整。请在 .env 中设置 SMTP_HOST/SMTP_USER/SMTP_PASSWORD,或在调用时传入 smtp_host/smtp_user/smtp_password 参数",
|
||
}, ensure_ascii=False)
|
||
|
||
import aiosmtplib
|
||
from email.mime.text import MIMEText
|
||
from email.mime.multipart import MIMEMultipart
|
||
|
||
recipients = [r.strip() for r in to.split(",") if r.strip()]
|
||
if not recipients:
|
||
return json.dumps({"error": "收件人地址不能为空"}, ensure_ascii=False)
|
||
|
||
msg = MIMEMultipart()
|
||
msg["From"] = f"{from_name} <{user}>" if from_name else user
|
||
msg["To"] = ", ".join(recipients)
|
||
msg["Subject"] = subject
|
||
msg.attach(MIMEText(body, body_type if body_type == "html" else "plain", "utf-8"))
|
||
|
||
await aiosmtplib.send(
|
||
msg,
|
||
hostname=host,
|
||
port=smtp_port,
|
||
username=user,
|
||
password=password,
|
||
start_tls=True,
|
||
)
|
||
|
||
return json.dumps({
|
||
"success": True,
|
||
"message": f"邮件已发送到 {len(recipients)} 个收件人",
|
||
"recipients": recipients,
|
||
}, ensure_ascii=False)
|
||
|
||
except Exception as e:
|
||
logger.error(f"send_email 工具执行失败: {e}", exc_info=True)
|
||
return json.dumps({"error": f"邮件发送失败: {e}"}, ensure_ascii=False)
|
||
|
||
|
||
# ─── URL 解析工具 ─────────────────────────────────────────
|
||
|
||
async def url_parse_tool(
|
||
url: str,
|
||
operation: str = "parse",
|
||
key: Optional[str] = None,
|
||
params: Optional[str] = None,
|
||
) -> str:
|
||
"""
|
||
URL 解析和构建工具。
|
||
|
||
Args:
|
||
url: 要处理的 URL
|
||
operation: 操作类型
|
||
- parse: 解析 URL 为各部分(协议/主机/端口/路径/参数/片段)
|
||
- get_param: 获取指定查询参数的值(需传 key)
|
||
- all_params: 获取所有查询参数
|
||
- build: 构建 URL(url=基础URL, params=要追加的参数 JSON)
|
||
key: 查询参数名(operation=get_param 时使用)
|
||
params: 参数 JSON 字符串(operation=build 时使用,如 {"page": "1", "size": "10"})
|
||
|
||
Returns:
|
||
处理结果
|
||
"""
|
||
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
|
||
|
||
try:
|
||
if operation == "parse":
|
||
parsed = urlparse(url)
|
||
return json.dumps({
|
||
"scheme": parsed.scheme,
|
||
"hostname": parsed.hostname,
|
||
"port": parsed.port,
|
||
"path": parsed.path,
|
||
"params": parsed.params,
|
||
"query": parsed.query,
|
||
"fragment": parsed.fragment,
|
||
}, ensure_ascii=False)
|
||
|
||
elif operation == "get_param":
|
||
if not key:
|
||
return json.dumps({"error": "需要提供 key 参数"}, ensure_ascii=False)
|
||
parsed = urlparse(url)
|
||
query_params = parse_qs(parsed.query)
|
||
val = query_params.get(key, [None])[0]
|
||
return json.dumps({"key": key, "value": val}, ensure_ascii=False)
|
||
|
||
elif operation == "all_params":
|
||
parsed = urlparse(url)
|
||
query_params = parse_qs(parsed.query)
|
||
# parse_qs 返回 {key: [value1, value2]},简化为单值
|
||
flat = {k: v[0] if len(v) == 1 else v for k, v in query_params.items()}
|
||
return json.dumps({"params": flat}, ensure_ascii=False)
|
||
|
||
elif operation == "build":
|
||
parsed = urlparse(url)
|
||
existing = parse_qs(parsed.query, keep_blank_values=True)
|
||
if params:
|
||
try:
|
||
extra = json.loads(params)
|
||
if isinstance(extra, dict):
|
||
existing.update(extra)
|
||
except json.JSONDecodeError as e:
|
||
return json.dumps({"error": f"params JSON 解析失败: {e}"}, ensure_ascii=False)
|
||
new_query = urlencode(existing, doseq=True)
|
||
new_parsed = parsed._replace(query=new_query)
|
||
new_url = urlunparse(new_parsed)
|
||
return json.dumps({"url": new_url}, ensure_ascii=False)
|
||
|
||
else:
|
||
return json.dumps(
|
||
{"error": f"不支持的操作: {operation},支持: parse/get_param/all_params/build"},
|
||
ensure_ascii=False,
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"url_parse 工具执行失败: {e}", exc_info=True)
|
||
return json.dumps({"error": f"URL 处理失败: {e}"}, ensure_ascii=False)
|
||
|
||
|
||
# ─── 正则表达式工具 ─────────────────────────────────────
|
||
|
||
async def regex_test_tool(
|
||
pattern: str,
|
||
test_string: str,
|
||
operation: str = "match",
|
||
flags: str = "",
|
||
replacement: str = "",
|
||
) -> str:
|
||
"""
|
||
正则表达式测试工具。
|
||
|
||
Args:
|
||
pattern: 正则表达式模式
|
||
test_string: 要测试的字符串
|
||
operation: 操作类型
|
||
- match: 查找第一个匹配
|
||
- findall: 查找所有匹配
|
||
- replace: 替换匹配内容(需传 replacement)
|
||
- split: 按正则分割字符串
|
||
- validate: 验证完整字符串是否匹配
|
||
flags: 标志(可选组合:i=忽略大小写, m=多行, s=点匹配换行)
|
||
replacement: 替换文本(operation=replace 时使用)
|
||
|
||
Returns:
|
||
处理结果
|
||
"""
|
||
import re as _re
|
||
|
||
try:
|
||
# 解析 flags
|
||
flag_val = 0
|
||
for c in flags.lower():
|
||
if c == "i":
|
||
flag_val |= _re.IGNORECASE
|
||
elif c == "m":
|
||
flag_val |= _re.MULTILINE
|
||
elif c == "s":
|
||
flag_val |= _re.DOTALL
|
||
|
||
if operation == "match":
|
||
m = _re.search(pattern, test_string, flag_val)
|
||
if m:
|
||
info = {"match": m.group(0), "start": m.start(), "end": m.end()}
|
||
if m.groups():
|
||
info["groups"] = list(m.groups())
|
||
if m.groupdict():
|
||
info["named_groups"] = {k: v for k, v in m.groupdict().items()}
|
||
return json.dumps(info, ensure_ascii=False)
|
||
return json.dumps({"match": None, "message": "无匹配"}, ensure_ascii=False)
|
||
|
||
elif operation == "findall":
|
||
matches = _re.findall(pattern, test_string, flag_val)
|
||
return json.dumps({"count": len(matches), "matches": matches[:100]}, ensure_ascii=False)
|
||
|
||
elif operation == "replace":
|
||
result = _re.sub(pattern, replacement, test_string, flags=flag_val)
|
||
return json.dumps({"result": result}, ensure_ascii=False)
|
||
|
||
elif operation == "split":
|
||
parts = _re.split(pattern, test_string, flags=flag_val)
|
||
return json.dumps({"parts": parts, "count": len(parts)}, ensure_ascii=False)
|
||
|
||
elif operation == "validate":
|
||
is_match = bool(_re.fullmatch(pattern, test_string, flag_val))
|
||
return json.dumps({"valid": is_match, "pattern": pattern}, ensure_ascii=False)
|
||
|
||
else:
|
||
return json.dumps(
|
||
{"error": f"不支持的操作: {operation},支持: match/findall/replace/split/validate"},
|
||
ensure_ascii=False,
|
||
)
|
||
except _re.error as e:
|
||
return json.dumps({"error": f"正则表达式语法错误: {e}"}, ensure_ascii=False)
|
||
except Exception as e:
|
||
logger.error(f"regex_test 工具执行失败: {e}", exc_info=True)
|
||
return json.dumps({"error": f"操作失败: {e}"}, ensure_ascii=False)
|
||
|
||
|
||
# ─── 定时任务工具 ─────────────────────────────────────────
|
||
|
||
async def schedule_create_tool(
|
||
agent_id: str,
|
||
name: str,
|
||
cron_expression: str,
|
||
input_message: str,
|
||
webhook_url: Optional[str] = None,
|
||
) -> str:
|
||
"""
|
||
为 Agent 创建定时任务。
|
||
|
||
Args:
|
||
agent_id: Agent ID
|
||
name: 任务名称,如"每日早报"
|
||
cron_expression: 标准 5 位 cron 表达式,如 0 9 * * * 表示每天9点
|
||
input_message: 定时执行时发送给 Agent 的消息
|
||
webhook_url: 可选,飞书机器人 Webhook URL,执行完成后推送通知
|
||
|
||
Returns:
|
||
执行结果
|
||
"""
|
||
try:
|
||
from app.core.database import SessionLocal
|
||
from app.models.agent import Agent
|
||
from app.models.agent_schedule import AgentSchedule
|
||
from app.services.agent_schedule_service import compute_next_run
|
||
|
||
db = SessionLocal()
|
||
try:
|
||
agent = db.query(Agent).filter(Agent.id == agent_id).first()
|
||
if not agent:
|
||
return json.dumps({"error": f"Agent 不存在: {agent_id}"}, ensure_ascii=False)
|
||
|
||
# 尝试计算下次执行时间
|
||
try:
|
||
next_run = compute_next_run(cron_expression)
|
||
except (ValueError, KeyError) as e:
|
||
return json.dumps({"error": f"cron 表达式无效: {e}(标准 5 位格式,如 0 9 * * *)"}, ensure_ascii=False)
|
||
|
||
# 获取 user_id:优先代理的 owner,否则 fallback 到第一个用户
|
||
user_id = agent.user_id
|
||
if not user_id:
|
||
from app.models.user import User
|
||
first_user = db.query(User).first()
|
||
if not first_user:
|
||
return json.dumps({"error": "数据库无用户,无法创建定时任务"}, ensure_ascii=False)
|
||
user_id = first_user.id
|
||
|
||
schedule = AgentSchedule(
|
||
agent_id=agent_id,
|
||
name=name,
|
||
cron_expression=cron_expression,
|
||
input_message=input_message,
|
||
webhook_url=webhook_url,
|
||
enabled=True,
|
||
next_run_at=next_run,
|
||
user_id=user_id,
|
||
)
|
||
db.add(schedule)
|
||
db.commit()
|
||
db.refresh(schedule)
|
||
|
||
return json.dumps({
|
||
"success": True,
|
||
"schedule_id": schedule.id,
|
||
"name": schedule.name,
|
||
"cron": schedule.cron_expression,
|
||
"next_run_at": next_run.isoformat(),
|
||
"message": f"定时任务「{name}」已创建,将于 {next_run.isoformat()} 首次执行",
|
||
}, ensure_ascii=False)
|
||
finally:
|
||
db.close()
|
||
except Exception as e:
|
||
logger.error(f"schedule_create 工具执行失败: {e}", exc_info=True)
|
||
return json.dumps({"error": f"创建定时任务失败: {e}"}, ensure_ascii=False)
|
||
|
||
|
||
async def schedule_list_tool(agent_id: str) -> str:
|
||
"""
|
||
列出 Agent 的所有定时任务。
|
||
|
||
Args:
|
||
agent_id: Agent ID
|
||
|
||
Returns:
|
||
定时任务列表
|
||
"""
|
||
try:
|
||
from app.core.database import SessionLocal
|
||
from app.models.agent_schedule import AgentSchedule
|
||
|
||
db = SessionLocal()
|
||
try:
|
||
schedules = (
|
||
db.query(AgentSchedule)
|
||
.filter(
|
||
AgentSchedule.agent_id == agent_id,
|
||
)
|
||
.order_by(AgentSchedule.created_at.desc())
|
||
.all()
|
||
)
|
||
|
||
items = []
|
||
for s in schedules:
|
||
items.append({
|
||
"id": s.id,
|
||
"name": s.name,
|
||
"cron": s.cron_expression,
|
||
"enabled": s.enabled,
|
||
"next_run": s.next_run_at.isoformat() if s.next_run_at else None,
|
||
"last_run": s.last_run_at.isoformat() if s.last_run_at else None,
|
||
"last_status": s.last_run_status,
|
||
})
|
||
|
||
return json.dumps({
|
||
"count": len(items),
|
||
"schedules": items,
|
||
}, ensure_ascii=False)
|
||
finally:
|
||
db.close()
|
||
except Exception as e:
|
||
logger.error(f"schedule_list 工具执行失败: {e}", exc_info=True)
|
||
return json.dumps({"error": f"查询定时任务失败: {e}"}, ensure_ascii=False)
|
||
|
||
|
||
async def schedule_delete_tool(schedule_id: str) -> str:
|
||
"""
|
||
删除指定的定时任务。
|
||
|
||
Args:
|
||
schedule_id: 定时任务 ID
|
||
|
||
Returns:
|
||
删除结果
|
||
"""
|
||
try:
|
||
from app.core.database import SessionLocal
|
||
from app.models.agent_schedule import AgentSchedule
|
||
|
||
db = SessionLocal()
|
||
try:
|
||
schedule = db.query(AgentSchedule).filter(AgentSchedule.id == schedule_id).first()
|
||
if not schedule:
|
||
return json.dumps({"error": f"定时任务不存在: {schedule_id}"}, ensure_ascii=False)
|
||
|
||
name = schedule.name
|
||
db.delete(schedule)
|
||
db.commit()
|
||
|
||
return json.dumps({
|
||
"success": True,
|
||
"message": f"定时任务「{name}」已删除",
|
||
}, ensure_ascii=False)
|
||
finally:
|
||
db.close()
|
||
except Exception as e:
|
||
logger.error(f"schedule_delete 工具执行失败: {e}", exc_info=True)
|
||
return json.dumps({"error": f"删除定时任务失败: {e}"}, ensure_ascii=False)
|
||
|
||
|
||
CRYPTO_UTIL_SCHEMA = {
|
||
"type": "function",
|
||
"function": {
|
||
"name": "crypto_util",
|
||
"description": "加密/哈希/编码工具:生成 UUID、Base64 编解码、计算 MD5/SHA256 等哈希值。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"operation": {
|
||
"type": "string",
|
||
"enum": ["uuid", "base64_encode", "base64_decode", "hash"],
|
||
"description": "操作:uuid=生成唯一ID, base64_encode=编码, base64_decode=解码, hash=计算哈希",
|
||
"default": "uuid",
|
||
},
|
||
"data": {
|
||
"type": "string",
|
||
"description": "输入数据(base64编解码或哈希时必填)",
|
||
},
|
||
"algorithm": {
|
||
"type": "string",
|
||
"enum": ["md5", "sha1", "sha256", "sha512"],
|
||
"description": "哈希算法(hash 操作时使用,默认 sha256)",
|
||
"default": "sha256",
|
||
},
|
||
},
|
||
},
|
||
},
|
||
}
|
||
|
||
RANDOM_GENERATE_SCHEMA = {
|
||
"type": "function",
|
||
"function": {
|
||
"name": "random_generate",
|
||
"description": "安全随机数据生成:密码、随机字符串、随机整数、随机浮点数。使用 secrets 模块保证安全性。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"generate_type": {
|
||
"type": "string",
|
||
"enum": ["password", "string", "number", "float"],
|
||
"description": "类型:password=安全密码, string=随机字符串, number=随机整数, float=随机浮点数",
|
||
"default": "password",
|
||
},
|
||
"length": {
|
||
"type": "integer",
|
||
"description": "密码/字符串长度(password 默认16,最少8;string 默认16)",
|
||
"default": 16,
|
||
"minimum": 1,
|
||
"maximum": 256,
|
||
},
|
||
"count": {
|
||
"type": "integer",
|
||
"description": "生成数量(默认1,最多50)",
|
||
"default": 1,
|
||
"minimum": 1,
|
||
"maximum": 50,
|
||
},
|
||
"min_val": {
|
||
"type": "number",
|
||
"description": "最小值(number/float 类型使用)",
|
||
"default": 0,
|
||
},
|
||
"max_val": {
|
||
"type": "number",
|
||
"description": "最大值(number/float 类型使用)",
|
||
"default": 100,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
}
|
||
|
||
SEND_EMAIL_SCHEMA = {
|
||
"type": "function",
|
||
"function": {
|
||
"name": "send_email",
|
||
"description": "发送邮件。SMTP 配置可从环境变量读取(SMTP_HOST/SMTP_USER/SMTP_PASSWORD),也可在调用时直接传入。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"to": {
|
||
"type": "string",
|
||
"description": "收件人邮箱地址,多个用逗号分隔,如 a@x.com,b@x.com",
|
||
},
|
||
"subject": {
|
||
"type": "string",
|
||
"description": "邮件主题",
|
||
},
|
||
"body": {
|
||
"type": "string",
|
||
"description": "邮件正文内容",
|
||
},
|
||
"smtp_host": {
|
||
"type": "string",
|
||
"description": "SMTP 服务器地址(如 smtp.qq.com),留空从环境变量 SMTP_HOST 读取",
|
||
},
|
||
"smtp_port": {
|
||
"type": "integer",
|
||
"description": "SMTP 端口,默认 587",
|
||
"default": 587,
|
||
},
|
||
"smtp_user": {
|
||
"type": "string",
|
||
"description": "发件人邮箱 / SMTP 用户名,留空从环境变量 SMTP_USER 读取",
|
||
},
|
||
"smtp_password": {
|
||
"type": "string",
|
||
"description": "SMTP 授权码/密码,留空从环境变量 SMTP_PASSWORD 读取",
|
||
},
|
||
"from_name": {
|
||
"type": "string",
|
||
"description": "发件人显示名称(可选)",
|
||
},
|
||
"body_type": {
|
||
"type": "string",
|
||
"enum": ["plain", "html"],
|
||
"description": "正文类型,默认 plain",
|
||
"default": "plain",
|
||
},
|
||
},
|
||
"required": ["to", "subject", "body"],
|
||
},
|
||
},
|
||
}
|
||
|
||
URL_PARSE_SCHEMA = {
|
||
"type": "function",
|
||
"function": {
|
||
"name": "url_parse",
|
||
"description": "URL 解析与构建工具:解析 URL 各部分、提取查询参数、构建带参数的 URL。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"url": {
|
||
"type": "string",
|
||
"description": "要处理的 URL",
|
||
},
|
||
"operation": {
|
||
"type": "string",
|
||
"enum": ["parse", "get_param", "all_params", "build"],
|
||
"description": "操作:parse=解析结构, get_param=取指定参数, all_params=所有参数, build=构建URL追加参数",
|
||
},
|
||
"key": {
|
||
"type": "string",
|
||
"description": "查询参数名(get_param 操作时使用)",
|
||
},
|
||
"params": {
|
||
"type": "string",
|
||
"description": "参数 JSON(build 操作时使用),如 {\"page\":\"1\"}",
|
||
},
|
||
},
|
||
"required": ["url"],
|
||
},
|
||
},
|
||
}
|
||
|
||
REGEX_TEST_SCHEMA = {
|
||
"type": "function",
|
||
"function": {
|
||
"name": "regex_test",
|
||
"description": "正则表达式测试工具:匹配查找、查找全部、替换、分割、验证等操作。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"pattern": {
|
||
"type": "string",
|
||
"description": "正则表达式模式,如 \\d{4}-\\d{2}-\\d{2}",
|
||
},
|
||
"test_string": {
|
||
"type": "string",
|
||
"description": "要测试的目标字符串",
|
||
},
|
||
"operation": {
|
||
"type": "string",
|
||
"enum": ["match", "findall", "replace", "split", "validate"],
|
||
"description": "操作:match=首个匹配, findall=全部匹配, replace=替换, split=分割, validate=验证完整匹配",
|
||
"default": "match",
|
||
},
|
||
"flags": {
|
||
"type": "string",
|
||
"description": "标志组合:i=忽略大小写, m=多行, s=点匹配换行(如 is 表示忽略大小写+点匹配换行)",
|
||
},
|
||
"replacement": {
|
||
"type": "string",
|
||
"description": "替换文本(replace 操作时使用)",
|
||
},
|
||
},
|
||
"required": ["pattern", "test_string"],
|
||
},
|
||
},
|
||
}
|
||
|
||
SCHEDULE_CREATE_SCHEMA = {
|
||
"type": "function",
|
||
"function": {
|
||
"name": "schedule_create",
|
||
"description": "为 Agent 创建定时任务,按 cron 表达式周期自动执行。可用于设置每日推送、定时检查等。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"agent_id": {
|
||
"type": "string",
|
||
"description": "Agent ID,即当前对话所属的 Agent",
|
||
},
|
||
"name": {
|
||
"type": "string",
|
||
"description": "任务名称,如「每日早报推送」「每小时检查」",
|
||
},
|
||
"cron_expression": {
|
||
"type": "string",
|
||
"description": "标准 5 位 cron 表达式,空格分隔。常见:每分钟=* * * * *,每小时=0 * * * *,每天9点=0 9 * * *,每天18点=0 18 * * *,每周一9点=0 9 * * 1,每月1号=0 9 1 * *",
|
||
},
|
||
"input_message": {
|
||
"type": "string",
|
||
"description": "每次定时触发时发送给 Agent 的消息内容,如「帮我生成今日工作汇总并推送」",
|
||
},
|
||
"webhook_url": {
|
||
"type": "string",
|
||
"description": "可选,飞书机器人 Webhook URL,执行完成后推送到飞书群",
|
||
},
|
||
},
|
||
"required": ["agent_id", "name", "cron_expression", "input_message"],
|
||
},
|
||
},
|
||
}
|
||
|
||
SCHEDULE_LIST_SCHEMA = {
|
||
"type": "function",
|
||
"function": {
|
||
"name": "schedule_list",
|
||
"description": "列出某个 Agent 的所有定时任务。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"agent_id": {
|
||
"type": "string",
|
||
"description": "Agent ID,要查询的 Agent",
|
||
},
|
||
},
|
||
"required": ["agent_id"],
|
||
},
|
||
},
|
||
}
|
||
|
||
SCHEDULE_DELETE_SCHEMA = {
|
||
"type": "function",
|
||
"function": {
|
||
"name": "schedule_delete",
|
||
"description": "删除指定的定时任务。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"schedule_id": {
|
||
"type": "string",
|
||
"description": "定时任务 ID(可从 schedule_list 查询获得)",
|
||
},
|
||
},
|
||
"required": ["schedule_id"],
|
||
},
|
||
},
|
||
}
|