id ?? 0); // 1. 权限校验:未授权(含权限已回收的漂移场景)直接拒绝并记失败审计 $node = $mcpService->verifyNode($auth, $toolName); if ($node === null) { $mcpService->logCall($keyId, $toolName, $arguments, false, 0); return '[ERROR] 无权限或权限已回收: ' . $toolName; } // 2. base URL 三级策略:显式配置 -> 入站请求同源回环 -> 均不可用报错 $baseUrl = $this->resolveBaseUrl(); if ($baseUrl === '') { $mcpService->logCall($keyId, $node, $arguments, false, 0); return '[ERROR] 未配置 app.app_host,无法内部分发'; } // 3. 节点串转 admin URL:system.quick/index -> /admin/system.quick/index $url = '/admin/' . implode('/', explode('/', $node)); // 4. 一次性 creator token:写入 login 缓存即被 admin 认证链接受; // mcp_internal 标记仅供 CSRF 中间件豁免识别,不落库、不外发 $token = bin2hex(random_bytes(16)); $creatorData = $auth['creator']; $creatorData['expire_time'] = time() + $this->internalTokenTtl; $creatorData['mcp_internal'] = true; Cache::store('login')->set($token, $creatorData, $this->internalTokenTtl); $startTime = microtime(true); $success = false; $result = ''; try { $client = new Client([ 'timeout' => $this->httpTimeout, 'verify' => false, 'http_errors' => false, ]); $jsonBody = json_encode($arguments, JSON_UNESCAPED_UNICODE); if ($jsonBody === false) { $jsonBody = '{}'; } $response = $client->post($baseUrl . $url, [ 'headers' => [ 'Accept' => 'application/json', 'Content-Type' => 'application/json', 'X-Requested-With' => 'XMLHttpRequest', 'Authorization' => 'Bearer ' . $token, ], 'body' => $jsonBody, ]); $statusCode = $response->getStatusCode(); $body = (string) $response->getBody(); if ($statusCode === 200) { // 三态判定之一/二:200 + 合法 JSON(layui code=0 与 success code=200 // 均算成功,不拆信封,原始 JSON 字符串原样返回);200 + 非 JSON // 视为 HTML 页面,加前缀标识返回(不自动追加 get_page_data=1) json_decode($body); if (json_last_error() === JSON_ERROR_NONE) { $result = $body; } else { $result = '[HTML页面内容] ' . $body; } $success = true; } else { // 三态判定之三:非 200,优先取 JSON 错误信封的 msg $result = '[ERROR] ' . $this->extractErrorMessage($body, 'HTTP ' . $statusCode); } } catch (\Throwable $e) { // 超时 / 连接失败等(Guzzle 抛出,含 ConnectException) $result = '[ERROR] ' . $e->getMessage(); } finally { // 审计尽力而为(logCall 内部吞异常);一次性 token 无论成败必删, // 且本身带 TTL 兜底,不残留任何可复用的豁免通道 $costMs = (int) round((microtime(true) - $startTime) * 1000); $mcpService->logCall($keyId, $node, $arguments, $success, $costMs); Cache::store('login')->delete($token); } return $result; } /** * base URL 三级策略. * * a. env('app.app_host') 显式配置优先(生产域名/反代场景明确指定) * b. 当前入站请求同源回环(scheme + host,缺失端口时用 SERVER_PORT 补齐): * MCP 端点收到什么域名,内部分发就打什么域名,天然绕开外部映射不确定性 * c. 两者皆不可用(如 CLI 上下文且未配置)返回空串,由调用方报错 */ protected function resolveBaseUrl(): string { $appHost = env('app.app_host', ''); if (!empty($appHost)) { return rtrim((string) $appHost, '/'); } $request = app('request'); if ($request instanceof \think\Request) { $scheme = $request->scheme(); $host = (string) $request->host(); if ($scheme !== '' && $host !== '') { // nginx fastcgi_params 标准实践传 HTTP_HOST $host(已去端口), // 非标准端口部署需用 SERVER_PORT 补齐,否则自请求会打到 80/443 if (!str_contains($host, ':') || str_ends_with($host, ']')) { $port = (string) $request->server('SERVER_PORT', ''); $defaultPort = ($scheme === 'https') ? '443' : '80'; if ($port !== '' && $port !== $defaultPort) { $host .= ':' . $port; } } return $scheme . '://' . $host; } } return ''; } /** * 提取错误响应文案:优先 JSON 错误信封的 msg,否则使用兜底文案. */ protected function extractErrorMessage(string $body, string $fallback): string { $decoded = json_decode($body, true); if (is_array($decoded) && !empty($decoded['msg'])) { return (string) $decoded['msg']; } return $fallback; } }