dsh-llm-codebuddy 1.3.4 → 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.
@@ -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
+ });