koishi-plugin-query-balance 0.1.0 → 0.1.1

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/lib/index.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ import { Context, Schema } from 'koishi';
2
+ export declare const name = "query-balance";
3
+ export declare const inject: string[];
4
+ export interface AccountConfig {
5
+ name: string;
6
+ type: 'usage' | 'deepseek';
7
+ baseUrl: string;
8
+ apiKey?: string;
9
+ proxy?: string;
10
+ noProxy?: string[];
11
+ }
12
+ export interface Config {
13
+ accounts: AccountConfig[];
14
+ timeout: number;
15
+ timezone: string;
16
+ }
17
+ export declare const Config: Schema<Config>;
18
+ export declare function apply(ctx: Context, config: Config): void;
package/lib/index.js ADDED
@@ -0,0 +1,273 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
6
+ var __export = (target, all) => {
7
+ for (var name2 in all)
8
+ __defProp(target, name2, { get: all[name2], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ Config: () => Config,
24
+ apply: () => apply,
25
+ inject: () => inject,
26
+ name: () => name
27
+ });
28
+ module.exports = __toCommonJS(src_exports);
29
+ var import_koishi = require("koishi");
30
+ var name = "query-balance";
31
+ var inject = ["http"];
32
+ var AccountConfig = import_koishi.Schema.object({
33
+ name: import_koishi.Schema.string().default("\u9ED8\u8BA4\u8D26\u53F7").description("API \u8D26\u53F7\u6635\u79F0\u3002"),
34
+ type: import_koishi.Schema.union(["usage", "deepseek"]).role("radio").default("usage").description("\u63A5\u53E3\u7C7B\u578B\u3002usage \u4E3A /v1/usage\uFF0C\u4E2D\u8F6C\u7AD9\uFF1Bdeepseek \u4E3A DeepSeek \u5B98\u65B9\u4F59\u989D\u63A5\u53E3\u3002"),
35
+ baseUrl: import_koishi.Schema.string().default("https://api.777358.xyz").description("API \u57FA\u7840\u5730\u5740\u3002"),
36
+ apiKey: import_koishi.Schema.string().role("secret").description("API \u5BC6\u94A5\u3002"),
37
+ proxy: import_koishi.Schema.string().description("\u8BE5\u8D26\u53F7\u4F7F\u7528\u7684\u4EE3\u7406\u5730\u5740\u3002"),
38
+ noProxy: import_koishi.Schema.array(String).default([]).description("\u8BE5\u8D26\u53F7\u4E0D\u4F7F\u7528\u4EE3\u7406\u7684\u4E3B\u673A\u540D\u6216\u5730\u5740\u3002")
39
+ });
40
+ var Config = import_koishi.Schema.object({
41
+ accounts: import_koishi.Schema.array(AccountConfig).default([{
42
+ name: "\u9ED8\u8BA4\u8D26\u53F7",
43
+ type: "usage",
44
+ baseUrl: "https://api.777358.xyz",
45
+ apiKey: process.env.OPENAI_API_KEY || "",
46
+ proxy: "",
47
+ noProxy: []
48
+ }]).description("\u53EF\u67E5\u8BE2\u7684 API \u8D26\u53F7\u5217\u8868\u3002"),
49
+ timeout: import_koishi.Schema.natural().role("ms").default(15e3).description("\u8BF7\u6C42\u8D85\u65F6\u65F6\u95F4\u3002"),
50
+ timezone: import_koishi.Schema.string().default("Asia/Shanghai").description("\u6708\u5EA6\u7EDF\u8BA1\u4F7F\u7528\u7684\u65F6\u533A\u3002")
51
+ });
52
+ function isRecord(value) {
53
+ return !!value && typeof value === "object" && !Array.isArray(value);
54
+ }
55
+ __name(isRecord, "isRecord");
56
+ function buildUsageUrl(baseUrl) {
57
+ const normalized = baseUrl.trim().replace(/`/g, "").replace(/\/+$/, "");
58
+ return `${normalized.endsWith("/v1") ? normalized.slice(0, -3).replace(/\/+$/, "") : normalized}/v1/usage`;
59
+ }
60
+ __name(buildUsageUrl, "buildUsageUrl");
61
+ function buildDeepSeekBalanceUrl(baseUrl) {
62
+ const normalized = baseUrl.trim().replace(/`/g, "").replace(/\/+$/, "");
63
+ return `${normalized.replace(/\/v1$/, "")}/user/balance`;
64
+ }
65
+ __name(buildDeepSeekBalanceUrl, "buildDeepSeekBalanceUrl");
66
+ function getNumber(value) {
67
+ if (typeof value === "number" && Number.isFinite(value)) return value;
68
+ if (typeof value === "string" && value.trim() && Number.isFinite(Number(value))) return Number(value);
69
+ return void 0;
70
+ }
71
+ __name(getNumber, "getNumber");
72
+ function getBalance(data) {
73
+ const quota = isRecord(data.quota) ? data.quota : void 0;
74
+ const remaining = getNumber(quota?.remaining) ?? getNumber(quota?.balance) ?? getNumber(data.remaining) ?? getNumber(data.balance);
75
+ const unit = String(quota?.unit ?? data.unit ?? "USD");
76
+ return { remaining, unit };
77
+ }
78
+ __name(getBalance, "getBalance");
79
+ function getDeepSeekBalances(data) {
80
+ return Array.isArray(data.balance_infos) ? data.balance_infos.filter(isRecord) : [];
81
+ }
82
+ __name(getDeepSeekBalances, "getDeepSeekBalances");
83
+ function formatNumber(value) {
84
+ const number = getNumber(value);
85
+ if (number === void 0) return value == null ? "-" : String(value);
86
+ return Number.isInteger(number) ? number.toLocaleString("en-US") : number.toLocaleString("en-US", { maximumFractionDigits: 6 });
87
+ }
88
+ __name(formatNumber, "formatNumber");
89
+ function formatUsage(value, unit) {
90
+ if (!isRecord(value)) return `\u7528\u91CF\uFF1A${formatNumber(value)}${unit ? ` ${unit}` : ""}`;
91
+ const labels = {
92
+ actual_cost: "\u5B9E\u9645\u8D39\u7528",
93
+ cache_creation_tokens: "\u7F13\u5B58\u521B\u5EFA\u4EE4\u724C\u6570",
94
+ cache_hit_rate: "\u7F13\u5B58\u547D\u4E2D\u7387",
95
+ cache_read_tokens: "\u7F13\u5B58\u8BFB\u53D6\u4EE4\u724C\u6570",
96
+ cache_write_tokens: "\u7F13\u5B58\u5199\u5165\u4EE4\u724C\u6570",
97
+ cost: "\u8D39\u7528",
98
+ input_tokens: "\u8F93\u5165\u4EE4\u724C\u6570",
99
+ output_tokens: "\u8F93\u51FA\u4EE4\u724C\u6570",
100
+ requests: "\u8BF7\u6C42\u6B21\u6570",
101
+ total_cost: "\u603B\u8D39\u7528",
102
+ total_tokens: "\u603B\u4EE4\u724C\u6570"
103
+ };
104
+ const lines = Object.entries(value).filter(([, item]) => item !== void 0 && item !== null).map(([key, item]) => {
105
+ if (key === "cache_hit_rate") {
106
+ const rate = getNumber(item);
107
+ return `${labels[key]}\uFF1A${rate === void 0 ? formatNumber(item) : `${formatNumber(rate * 100)}%`}`;
108
+ }
109
+ return `${labels[key] || key}\uFF1A${formatNumber(item)}${["cost", "actual_cost", "amount", "total_cost"].includes(key) ? ` ${unit}` : ""}`;
110
+ });
111
+ return lines.length ? lines.join("\n") : "\u7528\u91CF\uFF1A-";
112
+ }
113
+ __name(formatUsage, "formatUsage");
114
+ function getDateKey(value) {
115
+ if (typeof value !== "string" && typeof value !== "number") return void 0;
116
+ const date = new Date(value);
117
+ if (Number.isNaN(date.getTime())) return void 0;
118
+ return date;
119
+ }
120
+ __name(getDateKey, "getDateKey");
121
+ function getMonthKey(date, timezone) {
122
+ const parts = new Intl.DateTimeFormat("en-CA", { timeZone: timezone, year: "numeric", month: "2-digit" }).formatToParts(date);
123
+ const year = parts.find((item) => item.type === "year")?.value;
124
+ const month = parts.find((item) => item.type === "month")?.value;
125
+ return year && month ? `${year}-${month}` : void 0;
126
+ }
127
+ __name(getMonthKey, "getMonthKey");
128
+ function aggregateMonth(items, timezone) {
129
+ const month = getMonthKey(/* @__PURE__ */ new Date(), timezone);
130
+ const result = {};
131
+ for (const item of items) {
132
+ if (!isRecord(item)) continue;
133
+ const date = getDateKey(item.date ?? item.day ?? item.timestamp);
134
+ if (!date || getMonthKey(date, timezone) !== month) continue;
135
+ for (const [key, value] of Object.entries(item)) {
136
+ if (["date", "day", "timestamp"].includes(key)) continue;
137
+ const number = getNumber(value);
138
+ if (number !== void 0) result[key] = (getNumber(result[key]) ?? 0) + number;
139
+ }
140
+ }
141
+ return result;
142
+ }
143
+ __name(aggregateMonth, "aggregateMonth");
144
+ function getMonthlyUsage(data, timezone) {
145
+ const usage = isRecord(data.usage) ? data.usage : void 0;
146
+ if (usage?.month !== void 0) return usage.month;
147
+ const daily = Array.isArray(data.daily_usage) ? data.daily_usage : [];
148
+ return aggregateMonth(daily, timezone);
149
+ }
150
+ __name(getMonthlyUsage, "getMonthlyUsage");
151
+ function isNoProxy(url, noProxy) {
152
+ const hostname = new URL(url).hostname;
153
+ return noProxy.some((pattern) => {
154
+ const value = pattern.trim().toLowerCase();
155
+ if (!value) return false;
156
+ if (value === "*") return true;
157
+ const host = hostname.toLowerCase();
158
+ return host === value || (value.startsWith(".") ? host.endsWith(value) : host.endsWith(`.${value}`));
159
+ });
160
+ }
161
+ __name(isNoProxy, "isNoProxy");
162
+ function errorText(error) {
163
+ if (error instanceof Error && error.name === "AbortError") return "\u8BF7\u6C42\u8D85\u65F6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
164
+ if (isRecord(error) && isRecord(error.response)) {
165
+ const status = getNumber(error.response.status);
166
+ return status ? `\u63A5\u53E3\u8BF7\u6C42\u5931\u8D25\uFF08HTTP ${status}\uFF09\u3002` : "\u63A5\u53E3\u8BF7\u6C42\u5931\u8D25\u3002";
167
+ }
168
+ if (error instanceof Error && /timeout|ETIMEDOUT/i.test(error.message)) return "\u8BF7\u6C42\u8D85\u65F6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
169
+ return "\u65E0\u6CD5\u8FDE\u63A5\u4F59\u989D\u63A5\u53E3\uFF0C\u8BF7\u68C0\u67E5\u5730\u5740\u3001\u7F51\u7EDC\u548C\u914D\u7F6E\u3002";
170
+ }
171
+ __name(errorText, "errorText");
172
+ function getAccounts(config) {
173
+ return (config.accounts || []).filter((account) => account && account.baseUrl?.trim());
174
+ }
175
+ __name(getAccounts, "getAccounts");
176
+ function resolveAccount(accounts, input) {
177
+ const value = input?.trim();
178
+ if (!value && accounts.length === 1) return accounts[0];
179
+ if (!value) return void 0;
180
+ const index = Number(value);
181
+ if (Number.isInteger(index) && index >= 1 && index <= accounts.length) return accounts[index - 1];
182
+ const exact = accounts.find((account) => account.name === value);
183
+ if (exact) return exact;
184
+ const matched = accounts.filter((account) => account.name.includes(value));
185
+ return matched.length === 1 ? matched[0] : void 0;
186
+ }
187
+ __name(resolveAccount, "resolveAccount");
188
+ function accountList(accounts) {
189
+ return accounts.map((account, index) => `${index + 1}. ${account.name || `\u8D26\u53F7${index + 1}`}`).join("\n");
190
+ }
191
+ __name(accountList, "accountList");
192
+ function apply(ctx, config) {
193
+ const request = /* @__PURE__ */ __name(async (account) => {
194
+ const apiKey = account.apiKey || process.env.OPENAI_API_KEY;
195
+ if (!apiKey) throw new Error("\u672A\u914D\u7F6E API \u5BC6\u94A5\u3002");
196
+ const url = account.type === "deepseek" ? buildDeepSeekBalanceUrl(account.baseUrl || "https://api.deepseek.com") : buildUsageUrl(account.baseUrl);
197
+ const requestConfig = {
198
+ timeout: config.timeout,
199
+ headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" }
200
+ };
201
+ if (account.proxy && !isNoProxy(url, account.noProxy || [])) requestConfig.proxyAgent = account.proxy;
202
+ return ctx.http.get(url, requestConfig);
203
+ }, "request");
204
+ const chooseAccounts = /* @__PURE__ */ __name(async (session, input) => {
205
+ const accounts = getAccounts(config);
206
+ if (!accounts.length) throw new Error("\u672A\u914D\u7F6E API \u8D26\u53F7\u3002");
207
+ if (!input?.trim()) return accounts;
208
+ const account = resolveAccount(accounts, input);
209
+ if (account) return [account];
210
+ if (!session?.prompt) throw new Error(`\u672A\u627E\u5230\u8D26\u53F7\uFF0C\u8BF7\u4F7F\u7528\u8D26\u53F7\u5E8F\u53F7\u6216\u6635\u79F0\u3002
211
+ ${accountList(accounts)}`);
212
+ await session.send(`\u8BF7\u9009\u62E9 API \u8D26\u53F7\uFF1A
213
+ ${accountList(accounts)}`);
214
+ const answer = await session.prompt(3e4);
215
+ const selected = resolveAccount(accounts, answer);
216
+ if (selected) return [selected];
217
+ throw new Error(`\u672A\u9009\u62E9\u6709\u6548\u8D26\u53F7\uFF0C\u8BF7\u4F7F\u7528\u8D26\u53F7\u5E8F\u53F7\u6216\u6635\u79F0\u3002
218
+ ${accountList(accounts)}`);
219
+ }, "chooseAccounts");
220
+ const queryAccount = /* @__PURE__ */ __name(async (account, monthly) => {
221
+ try {
222
+ const data = await request(account);
223
+ if (account.type === "deepseek") {
224
+ const balances = getDeepSeekBalances(data);
225
+ const lines = balances.length ? balances.flatMap((balance) => [
226
+ `${String(balance.currency || "\u672A\u77E5\u5E01\u79CD")} \u603B\u4F59\u989D\uFF1A${formatNumber(balance.total_balance)}`,
227
+ `${String(balance.currency || "\u672A\u77E5\u5E01\u79CD")} \u8D60\u91D1\u4F59\u989D\uFF1A${formatNumber(balance.granted_balance)}`,
228
+ `${String(balance.currency || "\u672A\u77E5\u5E01\u79CD")} \u5145\u503C\u4F59\u989D\uFF1A${formatNumber(balance.topped_up_balance)}`
229
+ ]) : ["\u4F59\u989D\u4FE1\u606F\uFF1A-"];
230
+ return [
231
+ `\u8D26\u53F7\uFF1A${account.name || "-"}`,
232
+ `\u72B6\u6001\uFF1A${data.is_available === false ? "\u4E0D\u53EF\u7528" : "\u53EF\u7528"}`,
233
+ ...lines
234
+ ].join("\n");
235
+ }
236
+ const { remaining, unit } = getBalance(data);
237
+ const usage = monthly ? getMonthlyUsage(data, config.timezone) : isRecord(data.usage) ? data.usage.today : void 0;
238
+ const label = monthly ? "\u672C\u6708\u7528\u91CF" : "\u4ECA\u65E5\u7528\u91CF";
239
+ const status = data.isValid ?? data.is_active;
240
+ return [
241
+ `\u8D26\u53F7\uFF1A${account.name || "-"}`,
242
+ `\u4F59\u989D\uFF1A${formatNumber(remaining)} ${unit}`,
243
+ `\u72B6\u6001\uFF1A${status === false ? "\u65E0\u6548" : "\u6709\u6548"}`,
244
+ `${label}\uFF1A`,
245
+ formatUsage(usage, unit)
246
+ ].join("\n");
247
+ } catch (error) {
248
+ const message = error instanceof Error && error.message === "\u672A\u914D\u7F6E API \u5BC6\u94A5\u3002" ? error.message : errorText(error);
249
+ return `\u8D26\u53F7\uFF1A${account.name || "-"}
250
+ \u4F59\u989D\u67E5\u8BE2\u5931\u8D25\uFF1A${message}`;
251
+ }
252
+ }, "queryAccount");
253
+ const report = /* @__PURE__ */ __name(async (session, monthly, input) => {
254
+ try {
255
+ const accounts = await chooseAccounts(session, input);
256
+ const reports = await Promise.all(accounts.map((account) => queryAccount(account, monthly)));
257
+ return [monthly ? "API \u6708\u5EA6\u4F59\u989D\u67E5\u8BE2" : "API \u4ECA\u65E5\u4F59\u989D\u67E5\u8BE2", ...reports].join("\n\n");
258
+ } catch (error) {
259
+ const message = error instanceof Error && ["\u672A\u914D\u7F6E API \u5BC6\u94A5\u3002", "\u672A\u914D\u7F6E API \u8D26\u53F7\u3002"].includes(error.message) ? error.message : error instanceof Error && error.message.includes("\u672A\u627E\u5230\u8D26\u53F7") || error instanceof Error && error.message.includes("\u672A\u9009\u62E9\u6709\u6548\u8D26\u53F7") ? error.message : errorText(error);
260
+ return `\u4F59\u989D\u67E5\u8BE2\u5931\u8D25\uFF1A${message}`;
261
+ }
262
+ }, "report");
263
+ ctx.command("api.day [account:text]", "\u67E5\u8BE2\u4ECA\u65E5 API \u4F59\u989D\u4E0E\u7528\u91CF").action(({ session }, account) => report(session, false, account));
264
+ ctx.command("api.month [account:text]", "\u67E5\u8BE2\u672C\u6708 API \u4F59\u989D\u4E0E\u7528\u91CF").action(({ session }, account) => report(session, true, account));
265
+ }
266
+ __name(apply, "apply");
267
+ // Annotate the CommonJS export names for ESM import in node:
268
+ 0 && (module.exports = {
269
+ Config,
270
+ apply,
271
+ inject,
272
+ name
273
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-query-balance",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "仅自用:目前主要适配蓝钦与 DeepSeek 的 API 余额和用量查询",
5
5
  "koishi": {
6
6
  "description": {
@@ -16,10 +16,20 @@
16
16
  "peerDependencies": {
17
17
  "koishi": "^4.18.7"
18
18
  },
19
- "main": "src/index.ts",
19
+ "main": "lib/index.js",
20
+ "types": "lib/index.d.ts",
21
+ "scripts": {
22
+ "build": "tsc -p tsconfig.json && esbuild src/index.ts --bundle --platform=node --format=cjs --target=es2022 --packages=external --keep-names --outfile=lib/index.js",
23
+ "prepack": "npm run build"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^22.7.5",
27
+ "esbuild": "^0.23.1",
28
+ "typescript": "^5.6.2"
29
+ },
20
30
  "license": "MIT",
21
31
  "files": [
22
- "src",
32
+ "lib",
23
33
  "README.md"
24
34
  ],
25
35
  "keywords": [
package/src/index.ts DELETED
@@ -1,270 +0,0 @@
1
- import { Context, Schema } from 'koishi'
2
-
3
- export const name = 'query-balance'
4
- export const inject = ['http']
5
-
6
- export interface AccountConfig {
7
- name: string
8
- type: 'usage' | 'deepseek'
9
- baseUrl: string
10
- apiKey?: string
11
- proxy?: string
12
- noProxy?: string[]
13
- }
14
-
15
- export interface Config {
16
- accounts: AccountConfig[]
17
- timeout: number
18
- timezone: string
19
- }
20
-
21
- const AccountConfig: Schema<AccountConfig> = Schema.object({
22
- name: Schema.string().default('默认账号').description('API 账号昵称。'),
23
- type: Schema.union(['usage', 'deepseek']).role('radio').default('usage').description('接口类型。usage 为 /v1/usage,中转站;deepseek 为 DeepSeek 官方余额接口。'),
24
- baseUrl: Schema.string().default('https://api.777358.xyz').description('API 基础地址。'),
25
- apiKey: Schema.string().role('secret').description('API 密钥。'),
26
- proxy: Schema.string().description('该账号使用的代理地址。'),
27
- noProxy: Schema.array(String).default([]).description('该账号不使用代理的主机名或地址。'),
28
- })
29
-
30
- export const Config: Schema<Config> = Schema.object({
31
- accounts: Schema.array(AccountConfig).default([{
32
- name: '默认账号',
33
- type: 'usage',
34
- baseUrl: 'https://api.777358.xyz',
35
- apiKey: process.env.OPENAI_API_KEY || '',
36
- proxy: '',
37
- noProxy: [],
38
- }]).description('可查询的 API 账号列表。'),
39
- timeout: Schema.natural().role('ms').default(15000).description('请求超时时间。'),
40
- timezone: Schema.string().default('Asia/Shanghai').description('月度统计使用的时区。'),
41
- })
42
-
43
- type RecordValue = Record<string, unknown>
44
- type CommandSession = {
45
- send: (value: string) => Promise<unknown>
46
- prompt?: (timeout?: number) => Promise<string | undefined>
47
- }
48
-
49
- function isRecord(value: unknown): value is RecordValue {
50
- return !!value && typeof value === 'object' && !Array.isArray(value)
51
- }
52
-
53
- function buildUsageUrl(baseUrl: string) {
54
- const normalized = baseUrl.trim().replace(/`/g, '').replace(/\/+$/, '')
55
- return `${normalized.endsWith('/v1') ? normalized.slice(0, -3).replace(/\/+$/, '') : normalized}/v1/usage`
56
- }
57
-
58
- function buildDeepSeekBalanceUrl(baseUrl: string) {
59
- const normalized = baseUrl.trim().replace(/`/g, '').replace(/\/+$/, '')
60
- return `${normalized.replace(/\/v1$/, '')}/user/balance`
61
- }
62
-
63
- function getNumber(value: unknown): number | undefined {
64
- if (typeof value === 'number' && Number.isFinite(value)) return value
65
- if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) return Number(value)
66
- return undefined
67
- }
68
-
69
- function getBalance(data: RecordValue) {
70
- const quota = isRecord(data.quota) ? data.quota : undefined
71
- const remaining = getNumber(quota?.remaining) ?? getNumber(quota?.balance) ?? getNumber(data.remaining) ?? getNumber(data.balance)
72
- const unit = String(quota?.unit ?? data.unit ?? 'USD')
73
- return { remaining, unit }
74
- }
75
-
76
- function getDeepSeekBalances(data: RecordValue) {
77
- return Array.isArray(data.balance_infos) ? data.balance_infos.filter(isRecord) : []
78
- }
79
-
80
- function formatNumber(value: unknown) {
81
- const number = getNumber(value)
82
- if (number === undefined) return value == null ? '-' : String(value)
83
- return Number.isInteger(number) ? number.toLocaleString('en-US') : number.toLocaleString('en-US', { maximumFractionDigits: 6 })
84
- }
85
-
86
- function formatUsage(value: unknown, unit: string) {
87
- if (!isRecord(value)) return `用量:${formatNumber(value)}${unit ? ` ${unit}` : ''}`
88
- const labels: Record<string, string> = {
89
- actual_cost: '实际费用',
90
- cache_creation_tokens: '缓存创建令牌数',
91
- cache_hit_rate: '缓存命中率',
92
- cache_read_tokens: '缓存读取令牌数',
93
- cache_write_tokens: '缓存写入令牌数',
94
- cost: '费用',
95
- input_tokens: '输入令牌数',
96
- output_tokens: '输出令牌数',
97
- requests: '请求次数',
98
- total_cost: '总费用',
99
- total_tokens: '总令牌数',
100
- }
101
- const lines = Object.entries(value)
102
- .filter(([, item]) => item !== undefined && item !== null)
103
- .map(([key, item]) => {
104
- if (key === 'cache_hit_rate') {
105
- const rate = getNumber(item)
106
- return `${labels[key]}:${rate === undefined ? formatNumber(item) : `${formatNumber(rate * 100)}%`}`
107
- }
108
- return `${labels[key] || key}:${formatNumber(item)}${['cost', 'actual_cost', 'amount', 'total_cost'].includes(key) ? ` ${unit}` : ''}`
109
- })
110
- return lines.length ? lines.join('\n') : '用量:-'
111
- }
112
-
113
- function getDateKey(value: unknown) {
114
- if (typeof value !== 'string' && typeof value !== 'number') return undefined
115
- const date = new Date(value)
116
- if (Number.isNaN(date.getTime())) return undefined
117
- return date
118
- }
119
-
120
- function getMonthKey(date: Date, timezone: string) {
121
- const parts = new Intl.DateTimeFormat('en-CA', { timeZone: timezone, year: 'numeric', month: '2-digit' }).formatToParts(date)
122
- const year = parts.find(item => item.type === 'year')?.value
123
- const month = parts.find(item => item.type === 'month')?.value
124
- return year && month ? `${year}-${month}` : undefined
125
- }
126
-
127
- function aggregateMonth(items: unknown[], timezone: string) {
128
- const month = getMonthKey(new Date(), timezone)
129
- const result: RecordValue = {}
130
- for (const item of items) {
131
- if (!isRecord(item)) continue
132
- const date = getDateKey(item.date ?? item.day ?? item.timestamp)
133
- if (!date || getMonthKey(date, timezone) !== month) continue
134
- for (const [key, value] of Object.entries(item)) {
135
- if (['date', 'day', 'timestamp'].includes(key)) continue
136
- const number = getNumber(value)
137
- if (number !== undefined) result[key] = (getNumber(result[key]) ?? 0) + number
138
- }
139
- }
140
- return result
141
- }
142
-
143
- function getMonthlyUsage(data: RecordValue, timezone: string) {
144
- const usage = isRecord(data.usage) ? data.usage : undefined
145
- if (usage?.month !== undefined) return usage.month
146
- const daily = Array.isArray(data.daily_usage) ? data.daily_usage : []
147
- return aggregateMonth(daily, timezone)
148
- }
149
-
150
- function isNoProxy(url: string, noProxy: string[]) {
151
- const hostname = new URL(url).hostname
152
- return noProxy.some(pattern => {
153
- const value = pattern.trim().toLowerCase()
154
- if (!value) return false
155
- if (value === '*') return true
156
- const host = hostname.toLowerCase()
157
- return host === value || (value.startsWith('.') ? host.endsWith(value) : host.endsWith(`.${value}`))
158
- })
159
- }
160
-
161
- function errorText(error: unknown) {
162
- if (error instanceof Error && error.name === 'AbortError') return '请求超时,请稍后重试。'
163
- if (isRecord(error) && isRecord(error.response)) {
164
- const status = getNumber(error.response.status)
165
- return status ? `接口请求失败(HTTP ${status})。` : '接口请求失败。'
166
- }
167
- if (error instanceof Error && /timeout|ETIMEDOUT/i.test(error.message)) return '请求超时,请稍后重试。'
168
- return '无法连接余额接口,请检查地址、网络和配置。'
169
- }
170
-
171
- function getAccounts(config: Config) {
172
- return (config.accounts || []).filter(account => account && account.baseUrl?.trim())
173
- }
174
-
175
- function resolveAccount(accounts: AccountConfig[], input?: string) {
176
- const value = input?.trim()
177
- if (!value && accounts.length === 1) return accounts[0]
178
- if (!value) return undefined
179
- const index = Number(value)
180
- if (Number.isInteger(index) && index >= 1 && index <= accounts.length) return accounts[index - 1]
181
- const exact = accounts.find(account => account.name === value)
182
- if (exact) return exact
183
- const matched = accounts.filter(account => account.name.includes(value))
184
- return matched.length === 1 ? matched[0] : undefined
185
- }
186
-
187
- function accountList(accounts: AccountConfig[]) {
188
- return accounts.map((account, index) => `${index + 1}. ${account.name || `账号${index + 1}`}`).join('\n')
189
- }
190
-
191
- export function apply(ctx: Context, config: Config) {
192
- const request = async (account: AccountConfig) => {
193
- const apiKey = account.apiKey || process.env.OPENAI_API_KEY
194
- if (!apiKey) throw new Error('未配置 API 密钥。')
195
- const url = account.type === 'deepseek' ? buildDeepSeekBalanceUrl(account.baseUrl || 'https://api.deepseek.com') : buildUsageUrl(account.baseUrl)
196
- const requestConfig: Record<string, unknown> = {
197
- timeout: config.timeout,
198
- headers: { Authorization: `Bearer ${apiKey}`, Accept: 'application/json' },
199
- }
200
- if (account.proxy && !isNoProxy(url, account.noProxy || [])) requestConfig.proxyAgent = account.proxy
201
- return ctx.http.get<RecordValue>(url, requestConfig as never)
202
- }
203
-
204
- const chooseAccounts = async (session: CommandSession | undefined, input?: string) => {
205
- const accounts = getAccounts(config)
206
- if (!accounts.length) throw new Error('未配置 API 账号。')
207
- if (!input?.trim()) return accounts
208
- const account = resolveAccount(accounts, input)
209
- if (account) return [account]
210
- if (!session?.prompt) throw new Error(`未找到账号,请使用账号序号或昵称。\n${accountList(accounts)}`)
211
- await session.send(`请选择 API 账号:\n${accountList(accounts)}`)
212
- const answer = await session.prompt(30000)
213
- const selected = resolveAccount(accounts, answer)
214
- if (selected) return [selected]
215
- throw new Error(`未选择有效账号,请使用账号序号或昵称。\n${accountList(accounts)}`)
216
- }
217
-
218
- const queryAccount = async (account: AccountConfig, monthly: boolean) => {
219
- try {
220
- const data = await request(account)
221
- if (account.type === 'deepseek') {
222
- const balances = getDeepSeekBalances(data)
223
- const lines = balances.length
224
- ? balances.flatMap(balance => [
225
- `${String(balance.currency || '未知币种')} 总余额:${formatNumber(balance.total_balance)}`,
226
- `${String(balance.currency || '未知币种')} 赠金余额:${formatNumber(balance.granted_balance)}`,
227
- `${String(balance.currency || '未知币种')} 充值余额:${formatNumber(balance.topped_up_balance)}`,
228
- ])
229
- : ['余额信息:-']
230
- return [
231
- `账号:${account.name || '-'}`,
232
- `状态:${data.is_available === false ? '不可用' : '可用'}`,
233
- ...lines,
234
- ].join('\n')
235
- }
236
- const { remaining, unit } = getBalance(data)
237
- const usage = monthly ? getMonthlyUsage(data, config.timezone) : isRecord(data.usage) ? data.usage.today : undefined
238
- const label = monthly ? '本月用量' : '今日用量'
239
- const status = data.isValid ?? data.is_active
240
- return [
241
- `账号:${account.name || '-'}`,
242
- `余额:${formatNumber(remaining)} ${unit}`,
243
- `状态:${status === false ? '无效' : '有效'}`,
244
- `${label}:`,
245
- formatUsage(usage, unit),
246
- ].join('\n')
247
- } catch (error) {
248
- const message = error instanceof Error && error.message === '未配置 API 密钥。' ? error.message : errorText(error)
249
- return `账号:${account.name || '-'}\n余额查询失败:${message}`
250
- }
251
- }
252
-
253
- const report = async (session: CommandSession | undefined, monthly: boolean, input?: string) => {
254
- try {
255
- const accounts = await chooseAccounts(session, input)
256
- const reports = await Promise.all(accounts.map(account => queryAccount(account, monthly)))
257
- return [monthly ? 'API 月度余额查询' : 'API 今日余额查询', ...reports].join('\n\n')
258
- } catch (error) {
259
- const message = error instanceof Error && ['未配置 API 密钥。', '未配置 API 账号。'].includes(error.message)
260
- ? error.message
261
- : error instanceof Error && error.message.includes('未找到账号') || error instanceof Error && error.message.includes('未选择有效账号')
262
- ? error.message
263
- : errorText(error)
264
- return `余额查询失败:${message}`
265
- }
266
- }
267
-
268
- ctx.command('api.day [account:text]', '查询今日 API 余额与用量').action(({ session }, account) => report(session, false, account))
269
- ctx.command('api.month [account:text]', '查询本月 API 余额与用量').action(({ session }, account) => report(session, true, account))
270
- }