Spaces:
Runtime error
Runtime error
File size: 7,020 Bytes
cd8bd0a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | /**
* 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<CodeBuddyUsageResult> {
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<CodeBuddyUsageResult["quotas"]> = {};
const seenRefill: Record<string, number> = {};
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;
|