buffer($bytes) ?: 'application/octet-stream'; $size = round(strlen($bytes) / 1024) . ' KB'; if (str_starts_with($mime, 'image/')) { return ['content' => [['type' => 'text', 'text' => $label . '(图片,' . $size . ')'], ['type' => 'image', 'data' => base64_encode($bytes), 'mimeType' => $mime]], 'isError' => false]; } if ($mime === 'application/pdf' || str_starts_with($mime, 'text/')) { return ['content' => [['type' => 'text', 'text' => $label . '(' . $mime . ',' . $size . ')'], ['type' => 'resource', 'resource' => ['uri' => 'zyt-file://' . hash('sha256', $url), 'mimeType' => $mime, 'blob' => base64_encode($bytes)]]], 'isError' => false]; } return ['content' => [['type' => 'text', 'text' => $label . ':该附件类型(' . $mime . ')不支持直接读取']], 'isError' => true]; } private static function read(string $url): string { $max = McpConfig::maxFileBytes(); $local = self::localPath($url); if ($local !== null) { if (filesize($local) > $max) { throw new McpException('附件超过 ' . round($max / 1048576, 1) . ' MB,无法读取', 'invalid'); } return (string) file_get_contents($local); } $parts = parse_url($url); $host = strtolower((string) ($parts['host'] ?? '')); if (!in_array($parts['scheme'] ?? '', ['http', 'https'], true) || $host === '' || !in_array($host, self::allowedHosts(), true)) { throw new McpException('附件不在本系统的存储空间内,无法读取', 'denied'); } $response = (new Client(['timeout' => 10, 'allow_redirects' => false, 'http_errors' => false]))->get($url, ['stream' => true]); if ($response->getStatusCode() !== 200) { throw new McpException('附件读取失败(HTTP ' . $response->getStatusCode() . ')', 'invalid'); } $body = $response->getBody(); $bytes = ''; while (!$body->eof()) { $bytes .= $body->read(65536); if (strlen($bytes) > $max) { throw new McpException('附件超过 ' . round($max / 1048576, 1) . ' MB,无法读取', 'invalid'); } } return $bytes; } /** 本地存储:相对路径或本站域名下的 uploads 路径 → public 目录里的真实文件 */ private static function localPath(string $url): ?string { $path = $url; if (preg_match('#^https?://#i', $url)) { $host = strtolower((string) parse_url($url, PHP_URL_HOST)); if ($host !== strtolower((string) request()->host(true))) { return null; } $path = (string) parse_url($url, PHP_URL_PATH); } $path = ltrim(str_replace('\\', '/', $path), '/'); if ($path === '' || str_contains($path, '..') || !preg_match('#^(uploads|storage)/#', $path)) { return null; } $public = realpath(public_path()); $full = realpath(public_path() . $path); return ($full && $public && str_starts_with($full, $public) && is_file($full)) ? $full : null; } private static function allowedHosts(): array { $hosts = [strtolower((string) request()->host(true))]; $default = ConfigService::get('storage', 'default', 'local'); if ($default !== 'local') { $storage = ConfigService::get('storage', $default); $domain = is_array($storage) ? (string) ($storage['domain'] ?? '') : ''; $host = parse_url(str_contains($domain, '://') ? $domain : 'https://' . $domain, PHP_URL_HOST); if ($host) { $hosts[] = strtolower($host); } } return array_values(array_unique(array_filter($hosts))); } }