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;