/** * CodeBuddy CN usage handler — scoped to the "codebuddy-cn" provider. * * Quota lives behind a Tencent billing endpoint (POST, payload wrapped twice * under data.Response.Data). It mixes two credit types that must NOT be merged: * * - Refill / base ("基础体验包"): recurring allowance whose cycle resets long * before the resource itself expires (CycleEndTime << DeductionEndTime). * Live numbers in the *Cycle* fields; resetAt = next refresh. * - Bonus ("活动赠送包"): one-shot credits that run a single cycle and expire * (CycleEndTime ≈ DeductionEndTime). Numbers in the plain Capacity fields. * * One quota row per package — cadence label (Monthly/Weekly/Daily) for refill * packs, "Bonus Pack N" for bonus packs (soonest-expiring first). */ const USAGE_URL = "https://copilot.tencent.com/v2/billing/meter/get-user-resource"; interface TencentAccount { PackageName?: string; SubProductName?: string; CycleStartTime?: string | number; CycleEndTime?: string | number; DeductionEndTime?: string | number; CycleCapacitySize?: number | string; CycleCapacitySizePrecise?: string | number; CycleCapacityUsed?: number | string; CycleCapacityUsedPrecise?: string | number; CapacitySize?: number | string; CapacitySizePrecise?: string | number; CapacityUsed?: number | string; CapacityUsedPrecise?: string | number; } function parseResetTime(value: unknown): string | null { if (!value) return null; try { if (value instanceof Date) return value.toISOString(); if (typeof value === "number") { const ts = value < 1e12 ? value * 1000 : value; const d = new Date(ts); return d.getTime() > 0 ? d.toISOString() : null; } if (typeof value === "string") { if (/^\d+$/.test(value)) { const n = Number(value); const d = new Date(n < 1e12 ? n * 1000 : n); return d.getTime() > 0 ? d.toISOString() : null; } const d = new Date(value); return Number.isNaN(d.getTime()) || d.getTime() <= 0 ? null : d.toISOString(); } return null; } catch { return null; } } // Prefer the *Precise string fields (exact), fall back to the numeric ones. function num(precise: unknown, plain: unknown): number { const n = Number(precise ?? plain); return Number.isFinite(n) ? n : 0; } // Label a refill pack by its cycle length (Monthly is the common CodeBuddy case). function refillCadence(acc: TencentAccount): "Monthly" | "Weekly" | "Daily" { const start = parseResetTime(acc.CycleStartTime); const end = parseResetTime(acc.CycleEndTime); if (start && end) { const days = (new Date(end).getTime() - new Date(start).getTime()) / 86400000; if (days <= 1.5) return "Daily"; if (days <= 10) return "Weekly"; } return "Monthly"; } function cycleEndMs(acc: TencentAccount): number { const r = parseResetTime(acc.CycleEndTime); return r ? new Date(r).getTime() : Number.POSITIVE_INFINITY; } function deductionEndMs(acc: TencentAccount): number { const v = acc.DeductionEndTime; if (typeof v === "number") return (v < 1e12 ? v * 1000 : v); if (typeof v === "string" && /^\d+$/.test(v)) { const n = Number(v); return n < 1e12 ? n * 1000 : n; } const r = parseResetTime(v); return r ? new Date(r).getTime() : Number.POSITIVE_INFINITY; } // Refill packs roll into a new cycle before the resource expires; bonus packs // end at expiry. >2d gap between cycle end and validity end = refill. const REFILL_GAP_MS = 2 * 24 * 60 * 60 * 1000; function isRefill(acc: TencentAccount): boolean { const ce = cycleEndMs(acc); const de = deductionEndMs(acc); return Number.isFinite(ce) && Number.isFinite(de) && de - ce > REFILL_GAP_MS; } interface CodeBuddyUsageResult { plan?: string; quotas?: Record< string, { used: number; total: number; resetAt: string | null; unlimited: boolean; } >; message?: string; } export async function getCodeBuddyCnUsage( accessToken?: string, apiKey?: string, _providerSpecificData?: unknown ): Promise { const token = accessToken || apiKey; if (!token) { return { message: "CodeBuddy CN credential not available." }; } try { const response = await fetch(USAGE_URL, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", Accept: "application/json", "User-Agent": "CLI/2.108.1 CodeBuddy/2.108.1", "X-Product": "SaaS", "X-IDE-Type": "CLI", "X-IDE-Name": "CLI", "x-requested-with": "XMLHttpRequest", "x-codebuddy-request": "1", }, body: "{}", }); if (response.status === 401 || response.status === 403) { return { message: "CodeBuddy CN credential invalid or expired." }; } if (!response.ok) { return { message: `CodeBuddy CN quota API error (${response.status}).` }; } const json: any = await response.json(); if (json?.code !== 0) { return { message: `CodeBuddy CN quota error: ${json?.msg || "unknown"}` }; } const data = json?.data?.Response?.Data || {}; const accountsRaw: TencentAccount[] = Array.isArray(data.Accounts) ? data.Accounts : []; if (accountsRaw.length === 0) { return { message: "CodeBuddy CN connected. No credit package found." }; } const byExpiry = (a: TencentAccount, b: TencentAccount) => cycleEndMs(a) - cycleEndMs(b); const refills = accountsRaw.filter(isRefill).sort(byExpiry); const bonuses = accountsRaw.filter((a) => !isRefill(a)).sort(byExpiry); const quotas: NonNullable = {}; const seenRefill: Record = {}; refills.forEach((acc) => { const base = refillCadence(acc); seenRefill[base] = (seenRefill[base] || 0) + 1; const name = seenRefill[base] > 1 ? `${base} ${seenRefill[base]}` : base; quotas[name] = { used: num(acc.CycleCapacityUsedPrecise, acc.CycleCapacityUsed), total: num(acc.CycleCapacitySizePrecise, acc.CycleCapacitySize), resetAt: parseResetTime(acc.CycleEndTime), unlimited: false, }; }); bonuses.forEach((acc, i) => { quotas[`Bonus Pack ${i + 1}`] = { used: num(acc.CapacityUsedPrecise, acc.CapacityUsed), total: num(acc.CapacitySizePrecise, acc.CapacitySize), resetAt: parseResetTime(acc.CycleEndTime), unlimited: false, }; }); const basePkg = refills[0] || accountsRaw[0] || {}; const plan = basePkg.PackageName || basePkg.SubProductName || "CodeBuddy CN"; return { plan, quotas }; } catch (error: any) { // Hard Rule #12: no raw err.message in any HTTP/SSE/executor response. Usage // handler returns a controlled string for the dashboard; do not include the // raw exception text in case it carries a path/stack snippet. return { message: "CodeBuddy CN error: failed to fetch quota." }; } } export default getCodeBuddyCnUsage;