138 lines
4.7 KiB
Python
138 lines
4.7 KiB
Python
import json
|
|
import secrets
|
|
|
|
from django.conf import settings
|
|
from django.db.models import Q
|
|
from django.http import JsonResponse
|
|
from django.views.decorators.csrf import csrf_exempt
|
|
|
|
from apps.tickets.models import Ticket, TicketStatus
|
|
|
|
TICKET_SEARCH_TOOL = {
|
|
"name": "search_tickets",
|
|
"title": "内部工单查询",
|
|
"description": "按编号、标题或问题描述查询未归档工单,可选状态过滤。",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"query": {
|
|
"type": "string",
|
|
"description": "工单编号、标题或问题关键词",
|
|
},
|
|
"status": {
|
|
"type": "string",
|
|
"enum": [value for value, _label in TicketStatus.choices],
|
|
},
|
|
"limit": {
|
|
"type": "integer",
|
|
"minimum": 1,
|
|
"maximum": 20,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
def _rpc_error(rpc_id, code: int, message: str, *, status: int = 200):
|
|
return JsonResponse(
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": rpc_id,
|
|
"error": {"code": code, "message": message},
|
|
},
|
|
status=status,
|
|
)
|
|
|
|
|
|
def _authorized(request) -> bool:
|
|
expected = settings.INTERNAL_MCP_TOKEN
|
|
authorization = request.headers.get("Authorization", "")
|
|
if not expected or not authorization.startswith("Bearer "):
|
|
return False
|
|
return secrets.compare_digest(authorization.removeprefix("Bearer "), expected)
|
|
|
|
|
|
def _search_tickets(arguments: dict) -> dict:
|
|
query = str(arguments.get("query") or "").strip()
|
|
status = str(arguments.get("status") or "").strip()
|
|
try:
|
|
limit = min(max(int(arguments.get("limit", 10)), 1), 20)
|
|
except (TypeError, ValueError) as error:
|
|
raise ValueError("limit 必须是 1 到 20 的整数") from error
|
|
if status and status not in TicketStatus.values:
|
|
raise ValueError("status 不是有效工单状态")
|
|
queryset = Ticket.objects.select_related("category").filter(archived_at__isnull=True)
|
|
if query:
|
|
queryset = queryset.filter(
|
|
Q(ticket_no__icontains=query)
|
|
| Q(title__icontains=query)
|
|
| Q(description__icontains=query)
|
|
)
|
|
if status:
|
|
queryset = queryset.filter(status=status)
|
|
tickets = [
|
|
{
|
|
"id": ticket.id,
|
|
"ticket_no": ticket.ticket_no,
|
|
"title": ticket.title,
|
|
"description": ticket.description[:500],
|
|
"status": ticket.status,
|
|
"priority": ticket.priority,
|
|
"category_code": ticket.category.code if ticket.category else None,
|
|
"updated_at": ticket.updated_at.isoformat(),
|
|
}
|
|
for ticket in queryset[:limit]
|
|
]
|
|
return {
|
|
"query": query,
|
|
"status": status or None,
|
|
"count": len(tickets),
|
|
"tickets": tickets,
|
|
}
|
|
|
|
|
|
@csrf_exempt
|
|
def internal_mcp_view(request):
|
|
if request.method != "POST":
|
|
return _rpc_error(None, -32600, "仅支持 POST", status=405)
|
|
if not settings.INTERNAL_MCP_TOKEN:
|
|
return _rpc_error(None, -32001, "内部 MCP 尚未配置访问令牌", status=503)
|
|
if not _authorized(request):
|
|
return _rpc_error(None, -32000, "未授权的内部 MCP 请求", status=401)
|
|
try:
|
|
payload = json.loads(request.body)
|
|
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
return _rpc_error(None, -32700, "JSON 解析失败", status=400)
|
|
if not isinstance(payload, dict):
|
|
return _rpc_error(None, -32600, "JSON-RPC 请求结构无效", status=400)
|
|
rpc_id = payload.get("id")
|
|
method = payload.get("method")
|
|
params = payload.get("params")
|
|
if not isinstance(params, dict):
|
|
return _rpc_error(rpc_id, -32602, "params 必须是对象")
|
|
if method == "tools/list":
|
|
result = {"tools": [TICKET_SEARCH_TOOL]}
|
|
elif method == "tools/call":
|
|
if params.get("name") != TICKET_SEARCH_TOOL["name"]:
|
|
return _rpc_error(rpc_id, -32601, "工具不存在")
|
|
arguments = params.get("arguments", {})
|
|
if not isinstance(arguments, dict):
|
|
return _rpc_error(rpc_id, -32602, "arguments 必须是对象")
|
|
try:
|
|
structured = _search_tickets(arguments)
|
|
except ValueError as error:
|
|
return _rpc_error(rpc_id, -32602, str(error))
|
|
result = {
|
|
"content": [
|
|
{
|
|
"type": "text",
|
|
"text": json.dumps(structured, ensure_ascii=False),
|
|
}
|
|
],
|
|
"structuredContent": structured,
|
|
"isError": False,
|
|
}
|
|
else:
|
|
return _rpc_error(rpc_id, -32601, "不支持的 MCP 方法")
|
|
return JsonResponse({"jsonrpc": "2.0", "id": rpc_id, "result": result})
|