dsh-llm-codebuddy 1.3.5 → 1.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/codebuddy-auth.js CHANGED
@@ -1,7 +1,10 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { createHash } from "node:crypto";
2
3
  import { setTimeout as delay } from "node:timers/promises";
3
4
 
4
5
  export const CODEBUDDY_SESSION_REF = "CODEBUDDY_LOGIN_SESSION";
6
+ export const CODEBUDDY_SESSIONS_REF = "CODEBUDDY_LOGIN_SESSIONS";
7
+ export const CODEBUDDY_API_KEYS_REF = "CODEBUDDY_API_KEYS";
5
8
 
6
9
  const BASE_URL = "https://copilot.tencent.com/v2/plugin";
7
10
  const USER_AGENT = "CLI/unknown CodeBuddy/2.137.1";
@@ -93,13 +96,186 @@ function openBrowser(url) {
93
96
  });
94
97
  }
95
98
 
96
- function normalizeAccount(account) {
99
+ function textValue(...values) {
100
+ for (const value of values) {
101
+ if (typeof value === "string" && value.trim()) return value.trim();
102
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
103
+ }
104
+ return undefined;
105
+ }
106
+
107
+ function tokenClaims(token) {
108
+ if (typeof token !== "string") return {};
109
+ try {
110
+ const payload = token.split(".")[1];
111
+ if (!payload) return {};
112
+ const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
113
+ return parsed && typeof parsed === "object" ? parsed : {};
114
+ } catch {
115
+ return {};
116
+ }
117
+ }
118
+
119
+ function normalizeApiKeyEntry(entry, now = Date.now()) {
120
+ const ref = textValue(entry?.ref);
121
+ if (!ref || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(ref)) throw new Error("CodeBuddy API Key 引用无效");
122
+ const id = textValue(entry?.id) ?? `dsh:${ref}`;
123
+ return {
124
+ id,
125
+ ref,
126
+ label: textValue(entry?.label) ?? "DSH 保存的 API Key",
127
+ createdAt: Number.isFinite(entry?.createdAt) ? entry.createdAt : now,
128
+ updatedAt: Number.isFinite(entry?.updatedAt) ? entry.updatedAt : now,
129
+ };
130
+ }
131
+
132
+ export function createCodeBuddyApiKeyStore(entries = [], activeId) {
133
+ const keys = [];
134
+ for (const entry of entries) {
135
+ const normalized = normalizeApiKeyEntry(entry);
136
+ if (!keys.some((item) => item.id === normalized.id || item.ref === normalized.ref)) keys.push(normalized);
137
+ }
138
+ const selected = activeId === null
139
+ ? null
140
+ : typeof activeId === "string" && keys.some((entry) => entry.id === activeId) ? activeId : keys[0]?.id;
141
+ return { version: 1, activeId: selected, entries: keys };
142
+ }
143
+
144
+ export function upsertCodeBuddyApiKey(store, entry, now = Date.now()) {
145
+ const current = createCodeBuddyApiKeyStore(store?.entries ?? [], store?.activeId);
146
+ const incoming = normalizeApiKeyEntry({ ...entry, updatedAt: now }, now);
147
+ const index = current.entries.findIndex((item) => item.id === incoming.id || item.ref === incoming.ref);
148
+ if (index >= 0) incoming.createdAt = current.entries[index].createdAt;
149
+ const entries = index >= 0
150
+ ? current.entries.map((item, position) => position === index ? incoming : item)
151
+ : [...current.entries, incoming];
152
+ return { version: 1, activeId: incoming.id, entries };
153
+ }
154
+
155
+ export function serializeCodeBuddyApiKeys(store) {
156
+ const normalized = createCodeBuddyApiKeyStore(store?.entries ?? [], store?.activeId);
157
+ return JSON.stringify(normalized);
158
+ }
159
+
160
+ export function parseCodeBuddyApiKeys(value) {
161
+ let parsed;
162
+ try {
163
+ parsed = JSON.parse(value);
164
+ } catch (error) {
165
+ throw new Error("CodeBuddy API Key 列表已损坏,请重新配置", { cause: error });
166
+ }
167
+ if (!Array.isArray(parsed?.entries)) throw new Error("CodeBuddy API Key 列表格式无效,请重新配置");
168
+ return createCodeBuddyApiKeyStore(parsed.entries, parsed.activeId);
169
+ }
170
+
171
+ export function codeBuddyApiKeyEntries(store) {
172
+ return (store?.entries ?? []).map(({ id, ref, label, createdAt, updatedAt }) => ({ id, ref, label, createdAt, updatedAt }));
173
+ }
174
+
175
+ function normalizeAccount(account, auth) {
176
+ // The account endpoint has returned both a flat object and wrapped objects
177
+ // across CodeBuddy versions. Keep all known wrappers in the lookup so a newly
178
+ // added account gets the same display metadata as an imported account.
179
+ const sources = [
180
+ account,
181
+ account?.account,
182
+ account?.user,
183
+ account?.userInfo,
184
+ account?.profile,
185
+ account?.data,
186
+ ].filter((source) => source && typeof source === "object");
187
+ const read = (...keys) => textValue(...sources.flatMap((source) => keys.map((key) => source[key])));
188
+ const claims = tokenClaims(auth?.accessToken);
189
+ const userId = read("userId", "uid", "user_id", "id") ?? textValue(claims.userId, claims.uid, claims.user_id, claims.sub);
190
+ const enterpriseId = read("enterpriseId", "tenantId", "enterprise_id", "tenant_id")
191
+ ?? textValue(claims.enterpriseId, claims.tenantId, claims.enterprise_id, claims.tenant_id);
192
+ const email = read("email", "mail", "emailAddress") ?? textValue(claims.email, claims.mail);
193
+ const uin = read("uin", "phoneNumber", "phone", "mobile", "mobilePhone")
194
+ ?? textValue(claims.uin, claims.phoneNumber, claims.phone_number, claims.mobile);
195
+ const type = read("type", "accountType", "account_type");
196
+ const displayName = read("displayName", "name", "nickname", "username", "accountName")
197
+ ?? uin
198
+ ?? textValue(claims.displayName, claims.name, claims.nickname, claims.username, claims.preferred_username)
199
+ ?? email;
97
200
  return {
98
- ...(account?.userId || account?.uid ? { userId: account.userId ?? account.uid } : {}),
99
- ...(account?.enterpriseId || account?.tenantId ? { enterpriseId: account.enterpriseId ?? account.tenantId } : {}),
201
+ ...(userId ? { userId } : {}),
202
+ ...(enterpriseId ? { enterpriseId } : {}),
203
+ ...(email ? { email } : {}),
204
+ ...(uin ? { uin } : {}),
205
+ ...(type ? { type } : {}),
206
+ ...(displayName ? { displayName } : {}),
100
207
  };
101
208
  }
102
209
 
210
+ export function codeBuddySessionId(session) {
211
+ const account = normalizeAccount(session?.account, session?.auth);
212
+ if (account.userId) return `user:${account.userId}`;
213
+ if (account.email) return `email:${account.email}`;
214
+ if (account.enterpriseId) return `enterprise:${account.enterpriseId}`;
215
+ const refreshToken = session?.auth?.refreshToken ?? session?.auth?.accessToken;
216
+ if (!refreshToken) throw new Error("CodeBuddy 登录会话缺少账号标识和令牌");
217
+ return `token:${createHash("sha256").update(refreshToken).digest("hex").slice(0, 24)}`;
218
+ }
219
+
220
+ export function codeBuddySessionLabel(session) {
221
+ const account = normalizeAccount(session?.account, session?.auth);
222
+ return account.displayName ?? account.email ?? account.userId ?? account.enterpriseId ?? `账号 ${codeBuddySessionId(session).slice(-8)}`;
223
+ }
224
+
225
+ export function normalizeCodeBuddySessionEntry(session, now = Date.now()) {
226
+ const normalized = {
227
+ auth: calculateExpiresAt(session?.auth),
228
+ account: normalizeAccount(session?.account, session?.auth),
229
+ };
230
+ if (!normalized.auth.accessToken || !normalized.auth.refreshToken) throw new Error("CodeBuddy 登录会话无效");
231
+ const id = typeof session?.id === "string" && session.id.trim() ? session.id : codeBuddySessionId(normalized);
232
+ const createdAt = Number.isFinite(session?.createdAt) ? session.createdAt : now;
233
+ return {
234
+ id,
235
+ label: typeof session?.label === "string" && session.label.trim() ? session.label : codeBuddySessionLabel(normalized),
236
+ createdAt,
237
+ updatedAt: Number.isFinite(session?.updatedAt) ? session.updatedAt : now,
238
+ ...normalized,
239
+ };
240
+ }
241
+
242
+ export function createCodeBuddySessionStore(entries = [], activeId) {
243
+ const sessions = [];
244
+ for (const entry of entries) {
245
+ const normalized = normalizeCodeBuddySessionEntry(entry);
246
+ if (!sessions.some((item) => item.id === normalized.id)) sessions.push(normalized);
247
+ }
248
+ const selected = typeof activeId === "string" && sessions.some((entry) => entry.id === activeId) ? activeId : sessions[0]?.id;
249
+ return { version: 1, activeId: selected, sessions };
250
+ }
251
+
252
+ export function upsertCodeBuddySession(store, session, now = Date.now()) {
253
+ const current = createCodeBuddySessionStore(store?.sessions ?? [], store?.activeId);
254
+ const incoming = normalizeCodeBuddySessionEntry({ ...session, updatedAt: now }, now);
255
+ const index = current.sessions.findIndex((entry) => entry.id === incoming.id);
256
+ if (index >= 0) incoming.createdAt = current.sessions[index].createdAt;
257
+ const sessions = index >= 0
258
+ ? current.sessions.map((entry, position) => position === index ? incoming : entry)
259
+ : [...current.sessions, incoming];
260
+ return { version: 1, activeId: incoming.id, sessions };
261
+ }
262
+
263
+ export function activeCodeBuddySession(store) {
264
+ return store?.sessions?.find((entry) => entry.id === store.activeId) ?? store?.sessions?.[0];
265
+ }
266
+
267
+ export function codeBuddySessionAccounts(store) {
268
+ return (store?.sessions ?? []).map(({ id, label, account, createdAt, updatedAt }) => ({
269
+ id,
270
+ label,
271
+ accountName: label,
272
+ userId: account?.userId ?? null,
273
+ account,
274
+ createdAt,
275
+ updatedAt,
276
+ }));
277
+ }
278
+
103
279
  export async function loginCodeBuddy(onAuthUrl, signal) {
104
280
  const state = await request("/auth/state?platform=CLI", {
105
281
  method: "POST",
@@ -129,7 +305,7 @@ export async function loginCodeBuddy(onAuthUrl, signal) {
129
305
  60_000,
130
306
  signal,
131
307
  );
132
- return { auth, account: normalizeAccount(account) };
308
+ return { auth, account: normalizeAccount(account, auth) };
133
309
  }
134
310
 
135
311
  export async function refreshCodeBuddySession(session, signal) {
@@ -148,12 +324,18 @@ export async function refreshCodeBuddySession(session, signal) {
148
324
  const fresh = calculateExpiresAt(auth);
149
325
  const merged = { ...session.auth, ...fresh, refreshToken: fresh?.refreshToken ?? session.auth.refreshToken };
150
326
  if (!merged.accessToken) throw new Error("CodeBuddy 刷新接口没有返回访问令牌");
151
- return { auth: merged, account: normalizeAccount(session.account) };
327
+ return { auth: merged, account: normalizeAccount(session.account, merged) };
152
328
  }
153
329
 
154
330
  export function serializeCodeBuddySession(session) {
155
331
  if (!session?.auth?.accessToken || !session?.auth?.refreshToken) throw new Error("CodeBuddy 登录会话无效");
156
- return JSON.stringify({ auth: calculateExpiresAt(session.auth), account: normalizeAccount(session.account) });
332
+ return JSON.stringify({ auth: calculateExpiresAt(session.auth), account: normalizeAccount(session.account, session.auth) });
333
+ }
334
+
335
+ export function serializeCodeBuddySessions(store) {
336
+ const normalized = createCodeBuddySessionStore(store?.sessions ?? [], store?.activeId);
337
+ if (normalized.sessions.length === 0) throw new Error("CodeBuddy 登录账号列表为空");
338
+ return JSON.stringify(normalized);
157
339
  }
158
340
 
159
341
  export function parseCodeBuddySession(value) {
@@ -164,7 +346,22 @@ export function parseCodeBuddySession(value) {
164
346
  throw new Error("CodeBuddy 登录凭据已损坏,请重新登录", { cause: error });
165
347
  }
166
348
  if (!session?.auth?.accessToken || !session?.auth?.refreshToken) throw new Error("CodeBuddy 登录凭据不完整,请重新登录");
167
- return { auth: calculateExpiresAt(session.auth), account: normalizeAccount(session.account) };
349
+ return { auth: calculateExpiresAt(session.auth), account: normalizeAccount(session.account, session.auth) };
350
+ }
351
+
352
+ export function parseCodeBuddySessions(value) {
353
+ let parsed;
354
+ try {
355
+ parsed = JSON.parse(value);
356
+ } catch (error) {
357
+ throw new Error("CodeBuddy 登录账号列表已损坏,请重新登录", { cause: error });
358
+ }
359
+ if (Array.isArray(parsed?.sessions)) return createCodeBuddySessionStore(parsed.sessions, parsed.activeId);
360
+ if (parsed?.auth) {
361
+ const session = parseCodeBuddySession(value);
362
+ return createCodeBuddySessionStore([session], codeBuddySessionId(session));
363
+ }
364
+ throw new Error("CodeBuddy 登录账号列表格式无效,请重新登录");
168
365
  }
169
366
 
170
367
  export function sessionNeedsRefresh(session, now = Date.now()) {
@@ -0,0 +1,441 @@
1
+ const DEFAULT_BILLING_HOST = "https://www.codebuddy.cn";
2
+ const BROWSER_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36";
3
+ const ENTERPRISE_EDITIONS = new Set(["ultimate", "exclusive"]);
4
+ const REMAINING_FIELDS = [
5
+ "SlicePeriodCapacityRemainPrecise",
6
+ "SlicePeriodCapacityRemain",
7
+ "CycleCapacityRemainPrecise",
8
+ "CycleCapacityRemain",
9
+ "CapacityRemainPrecise",
10
+ "CapacityRemain",
11
+ "RemainPrecise",
12
+ "Remain",
13
+ "Remaining",
14
+ "Balance",
15
+ ];
16
+ const TOTAL_FIELDS = [
17
+ "SlicePeriodCapacitySizePrecise",
18
+ "SlicePeriodCapacitySize",
19
+ "CycleCapacitySizePrecise",
20
+ "CycleCapacitySize",
21
+ "CycleCapacityPrecise",
22
+ "CycleCapacity",
23
+ "CapacityPrecise",
24
+ "Capacity",
25
+ "TotalCapacityPrecise",
26
+ "TotalCapacity",
27
+ "PackageCapacity",
28
+ "Quota",
29
+ "Amount",
30
+ ];
31
+ const EXPIRY_FIELDS = [
32
+ "DeductionEndTime",
33
+ "ExpiredTime",
34
+ "SlicePeriodEndTime",
35
+ "PackageEndTime",
36
+ "EndTime",
37
+ "CycleEndTime",
38
+ "ExpireTime",
39
+ "ExpirationTime",
40
+ "ValidEndTime",
41
+ "ValidPeriodEndTime",
42
+ "EndAt",
43
+ "ExpireAt",
44
+ ];
45
+ const LABEL_FIELDS = ["PackageName", "PackageTypeName", "AccountName", "ProductName", "Name", "RuleName", "Description"];
46
+ const MAX_USAGE_PAGES = 100;
47
+ const PAGE_SIZE = 100;
48
+
49
+ function normalizeHost(value) {
50
+ const fallback = new URL(DEFAULT_BILLING_HOST);
51
+ if (typeof value !== "string" || !value.trim()) return fallback.origin;
52
+ try {
53
+ const candidate = new URL(value.includes("://") ? value : `https://${value}`);
54
+ if (candidate.protocol !== "https:") return fallback.origin;
55
+ const host = candidate.hostname.toLowerCase();
56
+ if (!["codebuddy.cn", "www.codebuddy.cn", "workbuddy.cn", "www.workbuddy.cn"].includes(host)) return fallback.origin;
57
+ return candidate.origin;
58
+ } catch {
59
+ return fallback.origin;
60
+ }
61
+ }
62
+
63
+ function billingHost(session) {
64
+ return normalizeHost(session?.auth?.domain);
65
+ }
66
+
67
+ function authToken(session) {
68
+ const token = typeof session?.auth?.accessToken === "string" ? session.auth.accessToken.trim() : "";
69
+ if (!token) throw new Error("CodeBuddy 登录令牌为空");
70
+ return token;
71
+ }
72
+
73
+ function billingHeaders(session, host, enterpriseId) {
74
+ const headers = {
75
+ accept: "application/json, text/plain, */*",
76
+ "content-type": "application/json",
77
+ "x-client-platform": "web",
78
+ origin: host,
79
+ referer: `${host}/profile/plans-usage`,
80
+ authorization: `Bearer ${authToken(session)}`,
81
+ "user-agent": BROWSER_USER_AGENT,
82
+ };
83
+ const domain = typeof session?.auth?.domain === "string" ? session.auth.domain.trim() : "";
84
+ if (domain) headers["x-domain"] = domain;
85
+ const userId = typeof session?.account?.userId === "string" ? session.account.userId.trim() : "";
86
+ if (userId) headers["x-user-id"] = userId;
87
+ if (enterpriseId) {
88
+ headers["x-enterprise-id"] = String(enterpriseId);
89
+ headers["x-tenant-id"] = String(enterpriseId);
90
+ }
91
+ return headers;
92
+ }
93
+
94
+ function timeoutSignal(timeoutMs, externalSignal) {
95
+ if (externalSignal) return externalSignal;
96
+ if (typeof AbortSignal?.timeout === "function") return AbortSignal.timeout(timeoutMs);
97
+ const controller = new AbortController();
98
+ setTimeout(() => controller.abort(), timeoutMs).unref?.();
99
+ return controller.signal;
100
+ }
101
+
102
+ async function readJson(response, action) {
103
+ const raw = await response.text();
104
+ if (!raw.trim()) throw new Error(`${action}返回空响应(HTTP ${response.status})`);
105
+ let payload;
106
+ try {
107
+ payload = JSON.parse(raw);
108
+ } catch (error) {
109
+ throw new Error(`${action}返回了无法解析的数据`, { cause: error });
110
+ }
111
+ if (!response.ok) throw new Error(`${action} HTTP ${response.status}: ${raw.slice(0, 160)}`);
112
+ if (payload?.code !== undefined && payload.code !== null && payload.code !== 0) {
113
+ throw new Error(`${action}失败(${payload.msg ?? payload.message ?? `code=${payload.code}`})`);
114
+ }
115
+ return payload;
116
+ }
117
+
118
+ function firstNumber(value, fields) {
119
+ for (const field of fields) {
120
+ const raw = value?.[field];
121
+ if (raw === undefined || raw === null || raw === "") continue;
122
+ const number = Number(raw);
123
+ if (Number.isFinite(number)) return number;
124
+ }
125
+ return null;
126
+ }
127
+
128
+ function parseTimestamp(value) {
129
+ if (value === undefined || value === null || value === "") return null;
130
+ if (typeof value === "number" || /^\d+(?:\.\d+)?$/.test(String(value).trim())) {
131
+ const number = Number(value);
132
+ if (!Number.isFinite(number)) return null;
133
+ return number < 1e12 ? Math.round(number * 1000) : Math.round(number);
134
+ }
135
+ const parsed = Date.parse(String(value).replace(/^(\d{4}-\d\d-\d\d)\s+/, "$1T"));
136
+ return Number.isFinite(parsed) ? parsed : null;
137
+ }
138
+
139
+ function firstTimestamp(value, fields) {
140
+ for (const field of fields) {
141
+ const parsed = parseTimestamp(value?.[field]);
142
+ if (parsed !== null) return parsed;
143
+ }
144
+ return null;
145
+ }
146
+
147
+ function firstText(value, fields) {
148
+ for (const field of fields) {
149
+ const text = value?.[field];
150
+ if (typeof text === "string" && text.trim()) return text.trim();
151
+ }
152
+ return "";
153
+ }
154
+
155
+ function extractAccounts(payload) {
156
+ const candidates = [
157
+ payload?.data?.Response?.Data?.Accounts,
158
+ payload?.data?.data?.Response?.Data?.Accounts,
159
+ payload?.data?.accounts,
160
+ payload?.data?.data?.accounts,
161
+ payload?.Response?.Data?.Accounts,
162
+ ];
163
+ return candidates.find(Array.isArray) ?? [];
164
+ }
165
+
166
+ function extractCreditSegments(accounts, source = "积分") {
167
+ return (Array.isArray(accounts) ? accounts : [])
168
+ .flatMap((account) => {
169
+ const details = Array.isArray(account?.SlicePeriodUsageDetails) && account.SlicePeriodUsageDetails.length
170
+ ? account.SlicePeriodUsageDetails.map((detail) => ({ ...account, ...detail }))
171
+ : [account];
172
+ return details.map((item) => {
173
+ const remaining = firstNumber(item, REMAINING_FIELDS);
174
+ if (remaining === null || remaining <= 0) return null;
175
+ const total = firstNumber(item, TOTAL_FIELDS);
176
+ return {
177
+ remaining: Number(remaining.toFixed(2)),
178
+ total: Number((total === null ? remaining : Math.max(total, remaining)).toFixed(2)),
179
+ expiresAt: firstTimestamp(item, EXPIRY_FIELDS),
180
+ source: firstText(item, LABEL_FIELDS) || source,
181
+ packageCode: item?.PackageCode ? String(item.PackageCode) : "",
182
+ };
183
+ });
184
+ })
185
+ .filter(Boolean);
186
+ }
187
+
188
+ function sortSegments(segments) {
189
+ return (Array.isArray(segments) ? segments : [])
190
+ .filter((segment) => segment && Number(segment.remaining) > 0)
191
+ .map((segment) => ({
192
+ remaining: Number(Number(segment.remaining).toFixed(2)),
193
+ total: Number(Number(segment.total || segment.remaining).toFixed(2)),
194
+ expiresAt: segment.expiresAt === null || segment.expiresAt === undefined ? null : Number(segment.expiresAt),
195
+ source: String(segment.source || "积分"),
196
+ packageCode: String(segment.packageCode || ""),
197
+ }))
198
+ .sort((left, right) => {
199
+ if (left.expiresAt === null && right.expiresAt !== null) return 1;
200
+ if (left.expiresAt !== null && right.expiresAt === null) return -1;
201
+ return (left.expiresAt || 0) - (right.expiresAt || 0);
202
+ });
203
+ }
204
+
205
+ function mergeSegments(segments) {
206
+ const merged = new Map();
207
+ for (const segment of Array.isArray(segments) ? segments : []) {
208
+ if (!segment || Number(segment.remaining) <= 0) continue;
209
+ const key = [segment.packageCode || segment.source || "积分", segment.expiresAt ?? "unknown"].join("|");
210
+ const previous = merged.get(key);
211
+ if (previous) {
212
+ previous.remaining += Number(segment.remaining) || 0;
213
+ previous.total += Number(segment.total || segment.remaining) || 0;
214
+ } else {
215
+ merged.set(key, {
216
+ remaining: Number(segment.remaining) || 0,
217
+ total: Number(segment.total || segment.remaining) || 0,
218
+ expiresAt: segment.expiresAt === undefined ? null : segment.expiresAt,
219
+ source: String(segment.source || "积分"),
220
+ packageCode: String(segment.packageCode || ""),
221
+ });
222
+ }
223
+ }
224
+ return sortSegments(Array.from(merged.values()));
225
+ }
226
+
227
+ function buildCreditResourceBody(now = new Date()) {
228
+ const end = new Date(now.getTime());
229
+ end.setFullYear(end.getFullYear() + 101);
230
+ const format = (date) => {
231
+ const pad = (value) => String(value).padStart(2, "0");
232
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
233
+ };
234
+ return {
235
+ PageNumber: 1,
236
+ PageSize: 100,
237
+ ProductCode: "p_tcaca",
238
+ Status: [0, 3],
239
+ PackageEndTimeRangeBegin: format(now),
240
+ PackageEndTimeRangeEnd: format(end),
241
+ };
242
+ }
243
+
244
+ async function postJson(url, session, body, action, options = {}) {
245
+ const response = await (options.fetchImpl || globalThis.fetch)(url, {
246
+ method: "POST",
247
+ headers: billingHeaders(session, options.host, options.enterpriseId),
248
+ body: JSON.stringify(body),
249
+ signal: timeoutSignal(options.timeoutMs ?? 12_000, options.signal),
250
+ });
251
+ return readJson(response, action);
252
+ }
253
+
254
+ function enterpriseUsage(payload) {
255
+ const candidates = [payload?.data, payload?.data?.data, payload?.data?.Response?.Data, payload];
256
+ const data = candidates.find((value) => value && typeof value === "object" && ("limitNum" in value || "LimitNum" in value));
257
+ if (!data) return null;
258
+ const limitNum = Number(data.limitNum ?? data.LimitNum);
259
+ if (!Number.isFinite(limitNum)) return null;
260
+ const reset = firstTimestamp(data, ["cycleResetTime", "CycleResetTime", "CycleResetTimeMs"]);
261
+ if (limitNum === -1) return { unlimited: true, credits: null, total: null, count: 0, segments: [], cycleResetTime: reset };
262
+ const credit = Number(data.credit ?? data.Credit);
263
+ const used = Number.isFinite(credit) ? credit : 0;
264
+ const credits = Math.max(0, limitNum - used);
265
+ return {
266
+ unlimited: false,
267
+ credits: Number(credits.toFixed(2)),
268
+ total: Number(limitNum.toFixed(2)),
269
+ count: 1,
270
+ segments: sortSegments([{ remaining: credits, total: limitNum, expiresAt: reset, source: "企业配额" }]),
271
+ cycleResetTime: reset,
272
+ };
273
+ }
274
+
275
+ async function queryPersonalCredits(session, host, options = {}) {
276
+ const payload = await postJson(`${host}/v2/billing/meter/get-user-resource`, session, buildCreditResourceBody(), "CodeBuddy 积分接口", { ...options, host });
277
+ const accounts = extractAccounts(payload);
278
+ let credits = 0;
279
+ for (const account of accounts) {
280
+ const remaining = firstNumber(account, [
281
+ "CycleCapacityRemainPrecise",
282
+ "CycleCapacityRemain",
283
+ "CapacityRemainPrecise",
284
+ "CapacityRemain",
285
+ ]);
286
+ if (remaining !== null) credits += remaining;
287
+ }
288
+ return {
289
+ credits: Number(credits.toFixed(2)),
290
+ count: accounts.length,
291
+ totalDosage: payload?.data?.Response?.Data?.TotalDosage ?? payload?.data?.data?.Response?.Data?.TotalDosage ?? null,
292
+ segments: mergeSegments(extractCreditSegments(accounts)),
293
+ unlimited: false,
294
+ cycleResetTime: null,
295
+ };
296
+ }
297
+
298
+ async function resolveEnterpriseId(session, host, options = {}) {
299
+ const uid = session?.account?.userId;
300
+ if (!uid) return "";
301
+ const response = await (options.fetchImpl || globalThis.fetch)(`${host}/console/accounts`, {
302
+ method: "GET",
303
+ headers: billingHeaders(session, host),
304
+ signal: timeoutSignal(8_000, options.signal),
305
+ });
306
+ const payload = await readJson(response, "CodeBuddy 企业账号接口");
307
+ const accounts = payload?.data?.accounts;
308
+ const first = Array.isArray(accounts) ? accounts[0] : undefined;
309
+ return typeof first?.enterpriseId === "string" ? first.enterpriseId.trim() : "";
310
+ }
311
+
312
+ async function queryEnterpriseCredits(session, host, enterpriseId, options = {}) {
313
+ const payload = await postJson(`${host}/v2/billing/meter/get-enterprise-user-usage`, session, {}, "CodeBuddy 企业积分接口", {
314
+ ...options,
315
+ host,
316
+ enterpriseId,
317
+ });
318
+ const parsed = enterpriseUsage(payload);
319
+ if (!parsed) throw new Error("CodeBuddy 企业积分接口返回数据无法解析");
320
+ return parsed;
321
+ }
322
+
323
+ function pad2(value) {
324
+ return String(value).padStart(2, "0");
325
+ }
326
+
327
+ function formatLocalDateTime(date) {
328
+ return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())} ${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())}`;
329
+ }
330
+
331
+ function localDateString(date) {
332
+ return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;
333
+ }
334
+
335
+ function parseUsageTime(value) {
336
+ return parseTimestamp(value);
337
+ }
338
+
339
+ function usageRows(payload) {
340
+ const data = payload?.data;
341
+ const rows = data && Array.isArray(data.data) ? data.data : data && Array.isArray(data.rows) ? data.rows : [];
342
+ const total = Number(data?.total);
343
+ return { rows, total: Number.isSafeInteger(total) && total >= 0 ? total : rows.length };
344
+ }
345
+
346
+ async function queryTodayUsage(session, host, options = {}) {
347
+ const now = new Date();
348
+ const start = new Date(now.getTime());
349
+ start.setHours(0, 0, 0, 0);
350
+ const fetchImpl = options.fetchImpl || globalThis.fetch;
351
+ const records = [];
352
+ let expectedTotal = null;
353
+ let fetched = 0;
354
+ for (let pageNum = 1; pageNum <= MAX_USAGE_PAGES; pageNum += 1) {
355
+ const payload = await postJson(`${host}/billing/meter/get-user-request-usage`, session, {
356
+ startTime: formatLocalDateTime(start),
357
+ endTime: formatLocalDateTime(now),
358
+ pageNum,
359
+ pageSize: PAGE_SIZE,
360
+ }, "CodeBuddy 今日请求量接口", { ...options, host, fetchImpl, timeoutMs: options.usageTimeoutMs ?? 8_000 });
361
+ const page = usageRows(payload);
362
+ if (expectedTotal === null) expectedTotal = page.total;
363
+ if (!page.rows.length) break;
364
+ for (const row of page.rows) {
365
+ const requestTime = parseUsageTime(row?.requestTime ?? row?.RequestTime ?? row?.createdAt);
366
+ const credit = Number(row?.credit ?? row?.Credit ?? 0);
367
+ if (requestTime === null || !Number.isFinite(credit) || credit < 0) continue;
368
+ records.push({ requestTime, credit });
369
+ }
370
+ fetched += page.rows.length;
371
+ if (fetched >= expectedTotal || page.rows.length < PAGE_SIZE) break;
372
+ }
373
+ const used = records.reduce((sum, record) => sum + record.credit, 0);
374
+ return {
375
+ date: localDateString(now),
376
+ used: Number(used.toFixed(2)),
377
+ count: records.length,
378
+ synced: true,
379
+ };
380
+ }
381
+
382
+ async function retry(task, attempts = 2) {
383
+ let lastError;
384
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
385
+ try {
386
+ return await task();
387
+ } catch (error) {
388
+ lastError = error;
389
+ if (attempt < attempts) await new Promise((resolve) => setTimeout(resolve, 250 * attempt));
390
+ }
391
+ }
392
+ throw lastError;
393
+ }
394
+
395
+ export async function fetchCodeBuddyCredits(session, options = {}) {
396
+ const host = billingHost(session);
397
+ const account = session?.account && typeof session.account === "object" ? session.account : {};
398
+ let creditResult;
399
+ let creditError = null;
400
+ try {
401
+ const enterpriseId = typeof account.enterpriseId === "string" ? account.enterpriseId.trim() : "";
402
+ const enterpriseEdition = typeof account.type === "string" && ENTERPRISE_EDITIONS.has(account.type.toLowerCase());
403
+ if (enterpriseId || enterpriseEdition) {
404
+ const resolvedId = enterpriseId || await retry(() => resolveEnterpriseId(session, host, options));
405
+ if (!resolvedId) throw new Error("企业账号缺少 enterpriseId");
406
+ creditResult = await retry(() => queryEnterpriseCredits(session, host, resolvedId, options));
407
+ } else {
408
+ creditResult = await retry(() => queryPersonalCredits(session, host, options));
409
+ }
410
+ } catch (error) {
411
+ creditError = error instanceof Error ? error.message : String(error);
412
+ creditResult = { credits: null, count: 0, totalDosage: null, segments: [], unlimited: false, cycleResetTime: null };
413
+ }
414
+
415
+ let todayUsage;
416
+ let todayUsageError = null;
417
+ try {
418
+ todayUsage = await retry(() => queryTodayUsage(session, host, options));
419
+ } catch (error) {
420
+ todayUsageError = error instanceof Error ? error.message : String(error);
421
+ }
422
+ return {
423
+ ...creditResult,
424
+ creditError,
425
+ todayUsage: todayUsage ?? null,
426
+ todayUsageError,
427
+ };
428
+ }
429
+
430
+ export const __testing = Object.freeze({
431
+ billingHost,
432
+ buildCreditResourceBody,
433
+ enterpriseUsage,
434
+ extractAccounts,
435
+ extractCreditSegments,
436
+ formatLocalDateTime,
437
+ mergeSegments,
438
+ normalizeHost,
439
+ queryTodayUsage,
440
+ sortSegments,
441
+ });