/**
 * HTTP 客户端模块
 * 封装 MCP Tool 的 HTTP 调用
 */
import { logger } from "./logger.js";
const DEFAULT_MCP_HTTP_REQUEST_TIMEOUT_MS = 120_000;
const MAX_ERROR_RESPONSE_BODY_LENGTH = 1_000;
function formatErrorResponseBody(raw) {
    const trimmed = raw.trim();
    if (!trimmed) {
        return "";
    }
    if (trimmed.length <= MAX_ERROR_RESPONSE_BODY_LENGTH) {
        return trimmed;
    }
    return `${trimmed.slice(0, MAX_ERROR_RESPONSE_BODY_LENGTH)}...(truncated)`;
}
function repairDuplicatePort(raw) {
    return raw.replace(/^((?:https?|wss?):\/\/(?:\[[^\]]+\]|[^/:?#]+):\d+):\d+(?=\/|$)/i, "$1");
}
function canonicalizeSchedulerHostname(hostname) {
    const normalized = hostname.toLowerCase();
    if (normalized === "localhost" || normalized === "::1" || normalized === "[::1]") {
        return "127.0.0.1";
    }
    return normalized;
}
function normalizeMcpHttpUrl(value, options = {}) {
    const raw = String(value || "").trim();
    if (!raw) {
        return undefined;
    }
    const repaired = repairDuplicatePort(raw);
    try {
        const url = new URL(repaired);
        if (url.protocol === "ws:") {
            url.protocol = "http:";
        }
        else if (url.protocol === "wss:") {
            url.protocol = "https:";
        }
        else {
            url.protocol = url.protocol.toLowerCase();
        }
        url.hostname = canonicalizeSchedulerHostname(url.hostname);
        const basePath = options.fromAgentWebSocketUrl
            ? url.pathname.replace(/\/ws\/agent\/?$/, "")
            : url.pathname;
        url.pathname = basePath.includes("/mcp/call")
            ? basePath
            : `${basePath.replace(/\/$/, "")}/mcp/call`;
        url.search = "";
        url.hash = "";
        return url.toString();
    }
    catch {
        const normalized = (options.fromAgentWebSocketUrl
            ? repaired.replace(/\/ws\/agent\/?$/i, "")
            : repaired).replace(/\/+$/, "");
        return normalized.endsWith("/mcp/call") ? normalized : `${normalized}/mcp/call`;
    }
}
export class HttpClient {
    serverConfig;
    mcpHttpUrl;
    /**
     * 共享的 token 刷新 Promise，避免并发请求重复刷新。
     */
    tokenRefreshPromise = null;
    constructor(serverConfig) {
        this.serverConfig = serverConfig;
        this.mcpHttpUrl = this.resolveMcpHttpUrl();
    }
    /**
     * 调用 MCP Tool
     */
    async callTool(toolName, args) {
        await this.ensureRuntimeAccessToken();
        const response = await this.executeToolRequest(toolName, args);
        if (response.status === 401) {
            logger.warn(`[HttpClient] MCP Tool ${toolName} 返回 401，尝试向 bridge 刷新 token`);
            const refreshed = await this.tryRefreshToken(args);
            if (refreshed) {
                const retryResponse = await this.executeToolRequest(toolName, args);
                if (!retryResponse.ok) {
                    const responseBody = typeof retryResponse.text === "function"
                        ? formatErrorResponseBody(await retryResponse.text().catch(() => ""))
                        : "";
                    throw new Error(`MCP tool ${toolName} call failed: ${retryResponse.status} ${retryResponse.statusText}${responseBody ? ` - ${responseBody}` : ""}`);
                }
                const retryResult = (await retryResponse.json());
                if (typeof retryResult.error === "string" && retryResult.error.trim().length > 0) {
                    throw new Error(`MCP tool ${toolName} failed: ${retryResult.error}`);
                }
                return retryResult;
            }
            logger.warn(`[HttpClient] token 刷新失败或无新 token，返回原始 401 错误`);
        }
        if (!response.ok) {
            const responseBody = typeof response.text === "function"
                ? formatErrorResponseBody(await response.text().catch(() => ""))
                : "";
            throw new Error(`MCP tool ${toolName} call failed: ${response.status} ${response.statusText}${responseBody ? ` - ${responseBody}` : ""}`);
        }
        const result = (await response.json());
        if (typeof result.error === "string" && result.error.trim().length > 0) {
            throw new Error(`MCP tool ${toolName} failed: ${result.error}`);
        }
        return result;
    }
    /**
     * 执行一次 MCP Tool HTTP 请求。
     * 主流程：构建当前 runtime token 请求头 -> POST 到调度中心 MCP 入口 -> 返回原始响应供调用方判断是否刷新 token。
     */
    async executeToolRequest(toolName, args) {
        return await this.fetchWithTimeout(this.mcpHttpUrl, {
            method: "POST",
            headers: this.buildRequestHeaders(),
            body: JSON.stringify({
                tool: toolName,
                arguments: args,
            }),
        }, `MCP tool ${toolName}`);
    }
    /**
     * 执行可配置硬超时的 fetch。
     * 主流程：创建 AbortController -> 超时 abort -> 将 abort 转成明确的工具超时错误。
     */
    async fetchWithTimeout(url, init, operationName) {
        const timeoutMs = this.resolveRequestTimeoutMs();
        const controller = new AbortController();
        const timeout = setTimeout(() => controller.abort(), timeoutMs);
        try {
            return await fetch(url, {
                ...init,
                signal: controller.signal,
            });
        }
        catch (error) {
            if (controller.signal.aborted) {
                throw new Error(`${operationName} timed out after ${timeoutMs}ms`);
            }
            throw error;
        }
        finally {
            clearTimeout(timeout);
        }
    }
    resolveRequestTimeoutMs() {
        const configured = this.serverConfig.mcpHttpRequestTimeoutMs;
        if (typeof configured === "number" &&
            Number.isFinite(configured) &&
            configured > 0) {
            return configured;
        }
        return DEFAULT_MCP_HTTP_REQUEST_TIMEOUT_MS;
    }
    /**
     * 构建 MCP HTTP 请求头。
     * 主流程：始终声明 JSON 内容类型 -> 当 runtime token 存在时同时兼容 X-Runtime-Token 与 Bearer 头。
     */
    buildRequestHeaders() {
        const headers = {
            "Content-Type": "application/json",
        };
        if (this.serverConfig.runtimeAccessToken) {
            headers["X-Runtime-Token"] = this.serverConfig.runtimeAccessToken;
            headers.Authorization =
                `Bearer ${this.serverConfig.runtimeAccessToken}`;
        }
        return headers;
    }
    async ensureRuntimeAccessToken() {
        if (this.serverConfig.runtimeAccessToken ||
            !this.serverConfig.runtimeBridgeBaseUrl) {
            return;
        }
        throw new Error("runtimeAccessToken is required; MCP nodes must claim a session token from launch binding");
    }
    /**
     * 向 bridge 查询最新 token，若获取到新 token 则更新本地配置。
     * 同一时刻仅一个刷新请求生效，并发调用共享同一 Promise。
     */
    async tryRefreshToken(args) {
        if (this.tokenRefreshPromise) {
            return this.tokenRefreshPromise;
        }
        this.tokenRefreshPromise = this.doRefreshToken(args);
        try {
            return await this.tokenRefreshPromise;
        }
        finally {
            this.tokenRefreshPromise = null;
        }
    }
    async doRefreshToken(args) {
        const bridgeBaseUrl = this.serverConfig.runtimeBridgeBaseUrl;
        if (!bridgeBaseUrl) {
            logger.warn("[HttpClient] 无 runtimeBridgeBaseUrl，无法刷新 token");
            return false;
        }
        const agentId = String(args?.agentId || "").trim();
        if (!agentId) {
            logger.warn("[HttpClient] 无 agentId，无法刷新 token");
            return false;
        }
        try {
            const response = await fetch(`${bridgeBaseUrl}/runtime/binding/refresh-token`, {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({
                    agentId,
                    currentToken: this.serverConfig.runtimeAccessToken || "",
                }),
            });
            if (!response.ok) {
                logger.warn(`[HttpClient] bridge refresh-token 返回 ${response.status}，无法刷新`);
                return false;
            }
            const data = (await response.json());
            const newToken = data.runtimeAccessToken;
            if (!newToken) {
                logger.warn("[HttpClient] bridge 未返回新 token");
                return false;
            }
            if (newToken === this.serverConfig.runtimeAccessToken) {
                return false;
            }
            this.serverConfig.runtimeAccessToken = newToken;
            logger.info(`[HttpClient] runtimeAccessToken 已通过 bridge refresh 更新`);
            return true;
        }
        catch (error) {
            logger.warn(`[HttpClient] 刷新 token 失败: ${error instanceof Error ? error.message : String(error)}`);
            return false;
        }
    }
    /**
     * 解析 MCP HTTP URL
     */
    resolveMcpHttpUrl() {
        const configured = this.serverConfig.mcpHttpUrl;
        if (configured && configured.trim().length > 0) {
            const normalized = normalizeMcpHttpUrl(configured);
            if (normalized) {
                return normalized;
            }
        }
        const fromServerUrl = normalizeMcpHttpUrl(this.serverConfig.serverUrl, {
            fromAgentWebSocketUrl: true,
        });
        if (fromServerUrl) {
            return fromServerUrl;
        }
        return this.serverConfig.serverUrl;
    }
    /**
     * 获取 MCP HTTP URL
     */
    getMcpHttpUrl() {
        return this.mcpHttpUrl;
    }
}
//# sourceMappingURL=http-client.js.map