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 +18 -0
- package/lib/index.js +273 -0
- package/package.json +13 -3
- package/src/index.ts +0 -270
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.
|
|
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": "
|
|
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
|
-
"
|
|
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
|
-
}
|