feat: organize tools and enable qwen realtime calls

This commit is contained in:
Xin Wang
2026-08-14 00:15:00 +08:00
parent eb111a3890
commit 5be21f5ccc
71 changed files with 5771 additions and 2026 deletions
+97 -1
View File
@@ -1,5 +1,7 @@
"""Reusable tool CRUD. Runtime execution is implemented per supported tool type."""
import json
import re
import uuid
from db.models import AssistantToolBinding, Tool
@@ -9,6 +11,7 @@ from schemas import ToolOut, ToolUpsert
from services.auth import require_admin
from services.masking import merge_secrets
from services.tool_resources import tool_to_out
from services.system_tools import RESERVED_TOOL_NAMES
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
@@ -20,17 +23,98 @@ router = APIRouter(
dependencies=[Depends(require_admin)],
)
TEMPLATE_PLACEHOLDER = re.compile(r"{{\s*([A-Za-z][A-Za-z0-9_]*)\s*}}")
def _normalize_http_template_inputs(definition: dict) -> None:
"""Keep Assistant-specific values out of a reusable HTTP resource."""
if definition.get("type") != "http":
return
config = definition.get("config") or {}
source = json.dumps(
{
"url": config.get("url"),
"headers": config.get("headers") or {},
"body": config.get("body") or {},
},
ensure_ascii=False,
)
placeholders = set(TEMPLATE_PLACEHOLDER.findall(source))
restricted_source = json.dumps(
{"url": config.get("url"), "body": config.get("body") or {}},
ensure_ascii=False,
)
misplaced_secrets = sorted(
name
for name in set(TEMPLATE_PLACEHOLDER.findall(restricted_source))
if name.startswith("secret__")
)
if misplaced_secrets:
raise HTTPException(
400,
"工具密钥变量只能用于 HTTP Header: "
+ ", ".join(misplaced_secrets),
)
invalid = sorted(
name
for name in placeholders
if not name.startswith(("input__", "system__", "secret__"))
)
if invalid:
raise HTTPException(
400,
"HTTP 资源不能直接引用 Assistant 变量;"
"请改为 {{input__name}}: " + ", ".join(invalid),
)
names = sorted(
name.removeprefix("input__")
for name in placeholders
if name.startswith("input__")
)
existing = {
str(item.get("name")): item
for item in config.get("template_inputs") or []
if item.get("name")
}
config["template_inputs"] = [
existing.get(name)
or {
"name": name,
"type": "string",
"location": "body",
"description": "HTTP 资源模板输入",
"required": True,
}
for name in names
]
def _payload(
body: ToolUpsert,
stored_secrets: dict | None = None,
) -> dict:
definition = body.definition.model_dump()
if body.function_name in RESERVED_TOOL_NAMES:
raise HTTPException(400, "工具函数名由平台内建工具保留")
_normalize_http_template_inputs(definition)
secrets = (
merge_secrets(body.secrets, stored_secrets or {})
if definition["type"] == "http"
else {}
)
secret_variables = (secrets.get("dynamic_variables") or {}) if secrets else {}
invalid_secret_names = sorted(
str(name)
for name in secret_variables
if not str(name).startswith("secret__")
)
if invalid_secret_names:
raise HTTPException(
400,
"工具密钥变量必须以 secret__ 开头: "
+ ", ".join(invalid_secret_names),
)
return {
"name": body.name.strip(),
"function_name": body.function_name,
@@ -68,7 +152,11 @@ async def _commit(session: AsyncSession, tool: Tool) -> ToolOut:
@router.get("", response_model=list[ToolOut])
async def list_tools(session: AsyncSession = Depends(get_session)):
rows = (
await session.execute(select(Tool).order_by(Tool.updated_at.desc()))
await session.execute(
select(Tool)
.where(Tool.type.in_(("http", "mcp", "client")))
.order_by(Tool.updated_at.desc())
)
).scalars().all()
return [tool_to_out(tool) for tool in rows]
@@ -87,6 +175,8 @@ async def get_tool(tool_id: str, session: AsyncSession = Depends(get_session)):
tool = await session.get(Tool, tool_id)
if not tool:
raise HTTPException(404, "工具不存在")
if tool.type == "system":
raise HTTPException(404, "工具不存在")
return tool_to_out(tool)
@@ -99,6 +189,8 @@ async def update_tool(
tool = await session.get(Tool, tool_id)
if not tool:
raise HTTPException(404, "工具不存在")
if tool.type == "system":
raise HTTPException(404, "工具不存在")
if tool.type == "mcp":
if body.definition.type != "mcp":
raise HTTPException(400, "同步生成的 MCP 工具不能修改类型")
@@ -117,6 +209,8 @@ async def duplicate_tool(tool_id: str, session: AsyncSession = Depends(get_sessi
source = await session.get(Tool, tool_id)
if not source:
raise HTTPException(404, "工具不存在")
if source.type == "system":
raise HTTPException(404, "工具不存在")
if source.type == "mcp":
raise HTTPException(400, "MCP 工具由 Server 同步维护,不能单独复制")
function_name = _duplicate_function_name(source.function_name)
@@ -148,6 +242,8 @@ async def delete_tool(tool_id: str, session: AsyncSession = Depends(get_session)
tool = await session.get(Tool, tool_id)
if not tool:
raise HTTPException(404, "工具不存在")
if tool.type == "system":
raise HTTPException(404, "工具不存在")
in_use = (
await session.execute(
select(AssistantToolBinding.assistant_id)