851 lines · 34.4 KB · Swift
RawDownload
import Foundation#if canImport(FoundationNetworking)import FoundationNetworking#endif public struct MiniMaxUsageFetcher: Sendable {    private static let log = CodexBarLog.logger(LogCategories.minimaxUsage)    private static let codingPlanPath = "user-center/payment/coding-plan"    private static let codingPlanQuery = "cycle_type=3"    private static let codingPlanRemainsPath = "v1/api/openplatform/coding_plan/remains"    private static let apiRemainsURL = URL(string: "https://api.minimaxi.com/v1/api/openplatform/coding_plan/remains")!    private struct RemainsContext: Sendable {        let authorizationToken: String?        let groupID: String?    }     public static func fetchUsage(        cookieHeader: String,        authorizationToken: String? = nil,        groupID: String? = nil,        region: MiniMaxAPIRegion = .global,        environment: [String: String] = ProcessInfo.processInfo.environment,        now: Date = Date()) async throws -> MiniMaxUsageSnapshot    {        guard let cookie = MiniMaxCookieHeader.normalized(from: cookieHeader) else {            throw MiniMaxUsageError.invalidCredentials        }         do {            return try await self.fetchCodingPlanHTML(                cookie: cookie,                authorizationToken: authorizationToken,                region: region,                environment: environment,                now: now)        } catch let error as MiniMaxUsageError {            if case .parseFailed = error {                Self.log.debug("MiniMax coding plan HTML parse failed, trying remains API")                return try await self.fetchCodingPlanRemains(                    cookie: cookie,                    remainsContext: RemainsContext(                        authorizationToken: authorizationToken,                        groupID: groupID),                    region: region,                    environment: environment,                    now: now)            }            throw error        }    }     public static func fetchUsage(        apiToken: String,        now: Date = Date()) async throws -> MiniMaxUsageSnapshot    {        let cleaned = apiToken.trimmingCharacters(in: .whitespacesAndNewlines)        guard !cleaned.isEmpty else {            throw MiniMaxUsageError.invalidCredentials        }         var request = URLRequest(url: self.apiRemainsURL)        request.httpMethod = "GET"        request.setValue("Bearer \(cleaned)", forHTTPHeaderField: "Authorization")        request.setValue("application/json", forHTTPHeaderField: "accept")        request.setValue("application/json", forHTTPHeaderField: "Content-Type")        request.setValue("CodexBar", forHTTPHeaderField: "MM-API-Source")         let (data, response) = try await URLSession.shared.data(for: request)        guard let httpResponse = response as? HTTPURLResponse else {            throw MiniMaxUsageError.networkError("Invalid response")        }         guard httpResponse.statusCode == 200 else {            let body = String(data: data, encoding: .utf8) ?? ""            Self.log.error("MiniMax returned \(httpResponse.statusCode): \(body)")            if httpResponse.statusCode == 401 || httpResponse.statusCode == 403 {                throw MiniMaxUsageError.invalidCredentials            }            throw MiniMaxUsageError.apiError("HTTP \(httpResponse.statusCode)")        }         return try MiniMaxUsageParser.parseCodingPlanRemains(data: data, now: now)    }     private static func fetchCodingPlanHTML(        cookie: String,        authorizationToken: String?,        region: MiniMaxAPIRegion,        environment: [String: String],        now: Date) async throws -> MiniMaxUsageSnapshot    {        let url = self.resolveCodingPlanURL(region: region, environment: environment)        var request = URLRequest(url: url)        request.httpMethod = "GET"        request.setValue(cookie, forHTTPHeaderField: "Cookie")        if let authorizationToken {            request.setValue("Bearer \(authorizationToken)", forHTTPHeaderField: "Authorization")        }        let acceptHeader = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"        request.setValue(acceptHeader, forHTTPHeaderField: "accept")        let userAgent =            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " +            "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"        request.setValue(userAgent, forHTTPHeaderField: "user-agent")        request.setValue("en-US,en;q=0.9", forHTTPHeaderField: "accept-language")        let origin = self.originURL(from: url)        request.setValue(origin.absoluteString, forHTTPHeaderField: "origin")        request.setValue(            self.resolveCodingPlanRefererURL(region: region, environment: environment).absoluteString,            forHTTPHeaderField: "referer")         let (data, response) = try await URLSession.shared.data(for: request)        guard let httpResponse = response as? HTTPURLResponse else {            throw MiniMaxUsageError.networkError("Invalid response")        }         guard httpResponse.statusCode == 200 else {            let body = String(data: data, encoding: .utf8) ?? ""            Self.log.error("MiniMax returned \(httpResponse.statusCode): \(body)")            if httpResponse.statusCode == 401 || httpResponse.statusCode == 403 {                throw MiniMaxUsageError.invalidCredentials            }            throw MiniMaxUsageError.apiError("HTTP \(httpResponse.statusCode)")        }         if let contentType = httpResponse.value(forHTTPHeaderField: "Content-Type"),           contentType.lowercased().contains("application/json")        {            return try MiniMaxUsageParser.parseCodingPlanRemains(data: data, now: now)        }         let html = String(data: data, encoding: .utf8) ?? ""        if html.contains("__NEXT_DATA__") {            Self.log.debug("MiniMax coding plan HTML contains __NEXT_DATA__")        }        if self.looksSignedOut(html: html) {            throw MiniMaxUsageError.invalidCredentials        }        return try MiniMaxUsageParser.parse(html: html, now: now)    }     private static func fetchCodingPlanRemains(        cookie: String,        remainsContext: RemainsContext,        region: MiniMaxAPIRegion,        environment: [String: String],        now: Date) async throws -> MiniMaxUsageSnapshot    {        let baseRemainsURL = self.resolveRemainsURL(region: region, environment: environment)        let remainsURL = self.appendGroupID(remainsContext.groupID, to: baseRemainsURL)        var request = URLRequest(url: remainsURL)        request.httpMethod = "GET"        request.setValue(cookie, forHTTPHeaderField: "Cookie")        if let authorizationToken = remainsContext.authorizationToken {            request.setValue("Bearer \(authorizationToken)", forHTTPHeaderField: "Authorization")        }        let acceptHeader = "application/json, text/plain, */*"        request.setValue(acceptHeader, forHTTPHeaderField: "accept")        request.setValue("XMLHttpRequest", forHTTPHeaderField: "x-requested-with")        let userAgent =            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " +            "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"        request.setValue(userAgent, forHTTPHeaderField: "user-agent")        request.setValue("en-US,en;q=0.9", forHTTPHeaderField: "accept-language")        let origin = self.originURL(from: baseRemainsURL)        request.setValue(origin.absoluteString, forHTTPHeaderField: "origin")        request.setValue(            self.resolveCodingPlanRefererURL(region: region, environment: environment).absoluteString,            forHTTPHeaderField: "referer")         let (data, response) = try await URLSession.shared.data(for: request)        guard let httpResponse = response as? HTTPURLResponse else {            throw MiniMaxUsageError.networkError("Invalid response")        }         guard httpResponse.statusCode == 200 else {            let body = String(data: data, encoding: .utf8) ?? ""            Self.log.error("MiniMax returned \(httpResponse.statusCode): \(body)")            if httpResponse.statusCode == 401 || httpResponse.statusCode == 403 {                throw MiniMaxUsageError.invalidCredentials            }            throw MiniMaxUsageError.apiError("HTTP \(httpResponse.statusCode)")        }         if let contentType = httpResponse.value(forHTTPHeaderField: "Content-Type"),           contentType.lowercased().contains("application/json")        {            let payload = try MiniMaxUsageParser.decodePayload(data: data)            self.logCodingPlanStatus(payload: payload)            return try MiniMaxUsageParser.parseCodingPlanRemains(payload: payload, now: now)        }         let html = String(data: data, encoding: .utf8) ?? ""        if self.looksSignedOut(html: html) {            throw MiniMaxUsageError.invalidCredentials        }        return try MiniMaxUsageParser.parse(html: html, now: now)    }     private static func appendGroupID(_ groupID: String?, to url: URL) -> URL {        guard let groupID, !groupID.isEmpty else { return url }        guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return url }        var queryItems = components.queryItems ?? []        queryItems.append(URLQueryItem(name: "GroupId", value: groupID))        components.queryItems = queryItems        return components.url ?? url    }     static func originURL(from url: URL) -> URL {        guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return url }        components.path = ""        components.query = nil        components.fragment = nil        return components.url ?? url    }     static func resolveCodingPlanURL(        region: MiniMaxAPIRegion,        environment: [String: String]) -> URL    {        if let override = MiniMaxSettingsReader.codingPlanURL(environment: environment) {            return override        }        if let host = MiniMaxSettingsReader.hostOverride(environment: environment),           let hostURL = self.url(from: host, path: Self.codingPlanPath, query: Self.codingPlanQuery)        {            return hostURL        }        return region.codingPlanURL    }     static func resolveCodingPlanRefererURL(        region: MiniMaxAPIRegion,        environment: [String: String]) -> URL    {        if let override = MiniMaxSettingsReader.codingPlanURL(environment: environment) {            if var components = URLComponents(url: override, resolvingAgainstBaseURL: false) {                components.query = nil                return components.url ?? override            }            return override        }        if let host = MiniMaxSettingsReader.hostOverride(environment: environment),           let hostURL = self.url(from: host, path: Self.codingPlanPath)        {            return hostURL        }        return region.codingPlanRefererURL    }     static func resolveRemainsURL(        region: MiniMaxAPIRegion,        environment: [String: String]) -> URL    {        if let override = MiniMaxSettingsReader.remainsURL(environment: environment) {            return override        }        if let host = MiniMaxSettingsReader.hostOverride(environment: environment),           let hostURL = self.url(from: host, path: Self.codingPlanRemainsPath)        {            return hostURL        }        return region.remainsURL    }     static func url(from raw: String, path: String? = nil, query: String? = nil) -> URL? {        guard let cleaned = MiniMaxSettingsReader.cleaned(raw) else { return nil }         func compose(_ base: URL) -> URL? {            var components = URLComponents(url: base, resolvingAgainstBaseURL: false)!            if let path { components.path = "/" + path }            if let query { components.query = query }            return components.url        }         if let url = URL(string: cleaned), url.scheme != nil {            if let composed = compose(url) { return composed }            return url        }        guard let base = URL(string: "https://\(cleaned)") else { return nil }        return compose(base)    }     private static func logCodingPlanStatus(payload: MiniMaxCodingPlanPayload) {        let baseResponse = payload.data.baseResp ?? payload.baseResp        guard let status = baseResponse?.statusCode else { return }        let message = baseResponse?.statusMessage ?? ""        if !message.isEmpty {            Self.log.debug("MiniMax coding plan status \(status): \(message)")        } else {            Self.log.debug("MiniMax coding plan status \(status)")        }    }     private static func looksSignedOut(html: String) -> Bool {        let lower = html.lowercased()        return lower.contains("sign in") || lower.contains("log in") || lower.contains("登录") || lower.contains("登入")    }} struct MiniMaxCodingPlanPayload: Decodable, Sendable {    let baseResp: MiniMaxBaseResponse?    let data: MiniMaxCodingPlanData     private enum CodingKeys: String, CodingKey {        case baseResp = "base_resp"        case data    }     init(from decoder: Decoder) throws {        let container = try decoder.container(keyedBy: CodingKeys.self)        self.baseResp = try container.decodeIfPresent(MiniMaxBaseResponse.self, forKey: .baseResp)        if container.contains(.data) {            let dataDecoder = try container.superDecoder(forKey: .data)            self.data = try MiniMaxCodingPlanData(from: dataDecoder)        } else {            self.data = try MiniMaxCodingPlanData(from: decoder)        }    }} struct MiniMaxCodingPlanData: Decodable, Sendable {    let baseResp: MiniMaxBaseResponse?    let currentSubscribeTitle: String?    let planName: String?    let comboTitle: String?    let currentPlanTitle: String?    let currentComboCard: MiniMaxComboCard?    let modelRemains: [MiniMaxModelRemains]     private enum CodingKeys: String, CodingKey {        case baseResp = "base_resp"        case currentSubscribeTitle = "current_subscribe_title"        case planName = "plan_name"        case comboTitle = "combo_title"        case currentPlanTitle = "current_plan_title"        case currentComboCard = "current_combo_card"        case modelRemains = "model_remains"    }     init(from decoder: Decoder) throws {        let container = try decoder.container(keyedBy: CodingKeys.self)        self.baseResp = try container.decodeIfPresent(MiniMaxBaseResponse.self, forKey: .baseResp)        self.currentSubscribeTitle = try container.decodeIfPresent(String.self, forKey: .currentSubscribeTitle)        self.planName = try container.decodeIfPresent(String.self, forKey: .planName)        self.comboTitle = try container.decodeIfPresent(String.self, forKey: .comboTitle)        self.currentPlanTitle = try container.decodeIfPresent(String.self, forKey: .currentPlanTitle)        self.currentComboCard = try container.decodeIfPresent(MiniMaxComboCard.self, forKey: .currentComboCard)        self.modelRemains = try (container.decodeIfPresent([MiniMaxModelRemains].self, forKey: .modelRemains)) ?? []    }} struct MiniMaxComboCard: Decodable, Sendable {    let title: String?} struct MiniMaxModelRemains: Decodable, Sendable {    let currentIntervalTotalCount: Int?    let currentIntervalUsageCount: Int?    let startTime: Int?    let endTime: Int?    let remainsTime: Int?     private enum CodingKeys: String, CodingKey {        case currentIntervalTotalCount = "current_interval_total_count"        case currentIntervalUsageCount = "current_interval_usage_count"        case startTime = "start_time"        case endTime = "end_time"        case remainsTime = "remains_time"    }     init(from decoder: Decoder) throws {        let container = try decoder.container(keyedBy: CodingKeys.self)        self.currentIntervalTotalCount = MiniMaxDecoding.decodeInt(container, forKey: .currentIntervalTotalCount)        self.currentIntervalUsageCount = MiniMaxDecoding.decodeInt(container, forKey: .currentIntervalUsageCount)        self.startTime = MiniMaxDecoding.decodeInt(container, forKey: .startTime)        self.endTime = MiniMaxDecoding.decodeInt(container, forKey: .endTime)        self.remainsTime = MiniMaxDecoding.decodeInt(container, forKey: .remainsTime)    }} struct MiniMaxBaseResponse: Decodable, Sendable {    let statusCode: Int?    let statusMessage: String?     private enum CodingKeys: String, CodingKey {        case statusCode = "status_code"        case statusMessage = "status_msg"    }     init(from decoder: Decoder) throws {        let container = try decoder.container(keyedBy: CodingKeys.self)        self.statusCode = MiniMaxDecoding.decodeInt(container, forKey: .statusCode)        self.statusMessage = try container.decodeIfPresent(String.self, forKey: .statusMessage)    }} enum MiniMaxDecoding {    static func decodeInt<K: CodingKey>(_ container: KeyedDecodingContainer<K>, forKey key: K) -> Int? {        if let value = try? container.decodeIfPresent(Int.self, forKey: key) {            return value        }        if let value = try? container.decodeIfPresent(Int64.self, forKey: key) {            return Int(value)        }        if let value = try? container.decodeIfPresent(Double.self, forKey: key) {            return Int(value)        }        if let value = try? container.decodeIfPresent(String.self, forKey: key) {            let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)            return Int(trimmed)        }        return nil    }} enum MiniMaxUsageParser {    static func decodePayload(data: Data) throws -> MiniMaxCodingPlanPayload {        let decoder = JSONDecoder()        return try decoder.decode(MiniMaxCodingPlanPayload.self, from: data)    }     static func decodePayload(json: [String: Any]) throws -> MiniMaxCodingPlanPayload {        let normalized = self.normalizeCodingPlanPayload(json)        let data = try JSONSerialization.data(withJSONObject: normalized, options: [])        return try self.decodePayload(data: data)    }     static func parseCodingPlanRemains(data: Data, now: Date = Date()) throws -> MiniMaxUsageSnapshot {        let payload = try self.decodePayload(data: data)        return try self.parseCodingPlanRemains(payload: payload, now: now)    }     static func parse(html: String, now: Date = Date()) throws -> MiniMaxUsageSnapshot {        if let snapshot = self.parseNextData(html: html, now: now) {            return snapshot        }        let text = self.stripHTML(html)         let planName = self.parsePlanName(html: html, text: text)        let available = self.parseAvailableUsage(text: text)        let usedPercent = self.parseUsedPercent(text: text)        let resetsAt = self.parseResetsAt(text: text, now: now)         if planName == nil, available == nil, usedPercent == nil {            throw MiniMaxUsageError.parseFailed("Missing coding plan data.")        }         return MiniMaxUsageSnapshot(            planName: planName,            availablePrompts: available?.prompts,            currentPrompts: nil,            remainingPrompts: nil,            windowMinutes: available?.windowMinutes,            usedPercent: usedPercent,            resetsAt: resetsAt,            updatedAt: now)    }     static func parseCodingPlanRemains(        payload: MiniMaxCodingPlanPayload,        now: Date = Date()) throws -> MiniMaxUsageSnapshot    {        let baseResponse = payload.data.baseResp ?? payload.baseResp        if let status = baseResponse?.statusCode, status != 0 {            let message = baseResponse?.statusMessage ?? "status_code \(status)"            let lower = message.lowercased()            if status == 1004 || lower.contains("cookie") || lower.contains("log in") || lower.contains("login") {                throw MiniMaxUsageError.invalidCredentials            }            throw MiniMaxUsageError.apiError(message)        }         guard let first = payload.data.modelRemains.first else {            throw MiniMaxUsageError.parseFailed("Missing coding plan data.")        }         let total = first.currentIntervalTotalCount        let remaining = first.currentIntervalUsageCount        let usedPercent = self.usedPercent(total: total, remaining: remaining)         let windowMinutes = self.windowMinutes(            start: self.dateFromEpoch(first.startTime),            end: self.dateFromEpoch(first.endTime))         let resetsAt = self.resetsAt(            end: self.dateFromEpoch(first.endTime),            remains: first.remainsTime,            now: now)         let planName = self.parsePlanName(data: payload.data)         if planName == nil, total == nil, usedPercent == nil {            throw MiniMaxUsageError.parseFailed("Missing coding plan data.")        }         let currentPrompts: Int? = if let total, let remaining {            max(0, total - remaining)        } else {            nil

Lines 1–500 of 851.