@ekworp/opencode-mystatus 0.0.0-stage → 1.2.7

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,115 @@
1
+ /**
2
+ * 共享工具函数
3
+ *
4
+ * [定位]: 被所有平台模块共享使用的工具函数
5
+ * [同步]: openai.ts, zhipu.ts, google.ts
6
+ */
7
+ import { currentLang, t } from "./i18n";
8
+ import { REQUEST_TIMEOUT_MS } from "./types";
9
+ // ============================================================================
10
+ // 时间格式化
11
+ // ============================================================================
12
+ /**
13
+ * 将秒数转换为人类可读的时间格式
14
+ */
15
+ export function formatDuration(seconds) {
16
+ const days = Math.floor(seconds / 86400);
17
+ const hours = Math.floor((seconds % 86400) / 3600);
18
+ const minutes = Math.floor((seconds % 3600) / 60);
19
+ const parts = [];
20
+ if (days > 0)
21
+ parts.push(t.days(days));
22
+ if (hours > 0)
23
+ parts.push(t.hours(hours));
24
+ if (minutes > 0 || parts.length === 0)
25
+ parts.push(t.minutes(minutes));
26
+ return parts.join(currentLang === "en" ? " " : "");
27
+ }
28
+ // ============================================================================
29
+ // 进度条
30
+ // ============================================================================
31
+ /**
32
+ * 生成进度条(实心代表剩余额度)
33
+ * @param remainPercent 剩余百分比 (0-100)
34
+ * @param width 进度条宽度(字符数)
35
+ */
36
+ export function createProgressBar(remainPercent, width = 30) {
37
+ // 确保百分比在有效范围内
38
+ const safePercent = Math.max(0, Math.min(100, remainPercent));
39
+ const filled = Math.round((safePercent / 100) * width);
40
+ const empty = width - filled;
41
+ const filledChar = "█";
42
+ const emptyChar = "░";
43
+ return filledChar.repeat(filled) + emptyChar.repeat(empty);
44
+ }
45
+ // ============================================================================
46
+ // 数值格式化
47
+ // ============================================================================
48
+ /**
49
+ * 计算剩余百分比并取整
50
+ * @param usedPercent 已使用百分比
51
+ */
52
+ export function calcRemainPercent(usedPercent) {
53
+ return Math.round(100 - usedPercent);
54
+ }
55
+ /**
56
+ * 格式化 Token 数量(以百万为单位)
57
+ */
58
+ export function formatTokens(tokens) {
59
+ return `${(tokens / 1000000).toFixed(1)}M`;
60
+ }
61
+ // ============================================================================
62
+ // 网络请求
63
+ // ============================================================================
64
+ /**
65
+ * 带超时的 fetch 请求
66
+ * @param url 请求 URL
67
+ * @param options fetch 选项
68
+ * @param timeoutMs 超时时间(毫秒),默认使用全局配置
69
+ */
70
+ export async function fetchWithTimeout(url, options, timeoutMs = REQUEST_TIMEOUT_MS) {
71
+ const controller = new AbortController();
72
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
73
+ try {
74
+ const response = await fetch(url, {
75
+ ...options,
76
+ signal: controller.signal,
77
+ });
78
+ return response;
79
+ }
80
+ catch (err) {
81
+ if (err instanceof Error && err.name === "AbortError") {
82
+ throw new Error(t.timeoutError(Math.round(timeoutMs / 1000)));
83
+ }
84
+ throw err;
85
+ }
86
+ finally {
87
+ clearTimeout(timeoutId);
88
+ }
89
+ }
90
+ // ============================================================================
91
+ // 安全计算
92
+ // ============================================================================
93
+ /**
94
+ * 安全获取数组最大值,空数组返回 0
95
+ */
96
+ export function safeMax(arr) {
97
+ if (arr.length === 0)
98
+ return 0;
99
+ return Math.max(...arr);
100
+ }
101
+ // ============================================================================
102
+ // 字符串处理
103
+ // ============================================================================
104
+ /**
105
+ * 脱敏显示敏感字符串
106
+ * 显示前 N 位和后 N 位,中间用 **** 替代
107
+ * @param str 原始字符串
108
+ * @param showChars 前后各显示的字符数,默认 4
109
+ */
110
+ export function maskString(str, showChars = 4) {
111
+ if (str.length <= showChars * 2) {
112
+ return str;
113
+ }
114
+ return `${str.slice(0, showChars)}****${str.slice(-showChars)}`;
115
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * 智谱 AI 额度查询模块
3
+ *
4
+ * [输入]: API Key
5
+ * [输出]: 格式化的额度使用情况
6
+ * [定位]: 被 mystatus.ts 调用,处理智谱 AI 账号
7
+ * [同步]: mystatus.ts, types.ts, utils.ts, i18n.ts
8
+ */
9
+ import { type QueryResult, type ZhipuAuthData } from "./types";
10
+ export type { ZhipuAuthData };
11
+ /**
12
+ * 查询智谱 AI 账号额度
13
+ * @param authData 智谱认证数据
14
+ * @returns 查询结果,如果账号不存在或无效返回 null
15
+ */
16
+ export declare function queryZhipuUsage(authData: ZhipuAuthData | undefined): Promise<QueryResult | null>;
17
+ /**
18
+ * 查询 Z.ai 账号额度
19
+ * @param authData Z.ai 认证数据
20
+ * @returns 查询结果,如果账号不存在或无效返回 null
21
+ */
22
+ export declare function queryZaiUsage(authData: ZhipuAuthData | undefined): Promise<QueryResult | null>;
23
+ //# sourceMappingURL=zhipu.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zhipu.d.ts","sourceRoot":"","sources":["../../../plugin/lib/zhipu.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,aAAa,EACnB,MAAM,SAAS,CAAC;AAwKjB,YAAY,EAAE,aAAa,EAAE,CAAC;AAyB9B;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,aAAa,GAAG,SAAS,GAClC,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAE7B;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,QAAQ,EAAE,aAAa,GAAG,SAAS,GAClC,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAE7B"}
@@ -0,0 +1,134 @@
1
+ /**
2
+ * 智谱 AI 额度查询模块
3
+ *
4
+ * [输入]: API Key
5
+ * [输出]: 格式化的额度使用情况
6
+ * [定位]: 被 mystatus.ts 调用,处理智谱 AI 账号
7
+ * [同步]: mystatus.ts, types.ts, utils.ts, i18n.ts
8
+ */
9
+ import { t } from "./i18n";
10
+ import { HIGH_USAGE_THRESHOLD, } from "./types";
11
+ import { calcRemainPercent, createProgressBar, fetchWithTimeout, formatDuration, formatTokens, maskString, safeMax, } from "./utils";
12
+ // ============================================================================
13
+ // API 调用
14
+ // ============================================================================
15
+ const ZHIPU_QUOTA_QUERY_URL = "https://bigmodel.cn/api/monitor/usage/quota/limit";
16
+ const ZAI_QUOTA_QUERY_URL = "https://api.z.ai/api/monitor/usage/quota/limit";
17
+ const ZHIPU_CONFIG = {
18
+ apiUrl: ZHIPU_QUOTA_QUERY_URL,
19
+ apiError: t.zhipuApiError,
20
+ accountLabel: t.zhipuAccountName,
21
+ };
22
+ const ZAI_CONFIG = {
23
+ apiUrl: ZAI_QUOTA_QUERY_URL,
24
+ apiError: t.zaiApiError,
25
+ accountLabel: t.zaiAccountName,
26
+ };
27
+ /**
28
+ * 获取智谱/Z.ai 使用情况
29
+ */
30
+ async function fetchUsage(apiKey, config) {
31
+ const response = await fetchWithTimeout(config.apiUrl, {
32
+ method: "GET",
33
+ headers: {
34
+ Authorization: apiKey,
35
+ "Content-Type": "application/json",
36
+ "User-Agent": "OpenCode-Status-Plugin/1.0",
37
+ },
38
+ });
39
+ if (!response.ok) {
40
+ const errorText = await response.text();
41
+ throw new Error(config.apiError(response.status, errorText));
42
+ }
43
+ const data = (await response.json());
44
+ if (!data.success || data.code !== 200) {
45
+ throw new Error(config.apiError(data.code, data.msg || "Unknown error"));
46
+ }
47
+ return data;
48
+ }
49
+ // ============================================================================
50
+ // 格式化输出
51
+ // ============================================================================
52
+ /**
53
+ * 格式化智谱 AI 使用情况
54
+ */
55
+ function formatZhipuUsage(data, apiKey, accountLabel) {
56
+ const lines = [];
57
+ const limits = data.data.limits;
58
+ // 标题行:Account: API Key (Plan) - 显示脱敏后的 key
59
+ const maskedKey = maskString(apiKey);
60
+ lines.push(`${t.account} ${maskedKey} (${accountLabel})`);
61
+ lines.push("");
62
+ // 空数组检查
63
+ if (!limits || limits.length === 0) {
64
+ lines.push(t.noQuotaData);
65
+ return lines.join("\n");
66
+ }
67
+ // 查找 TOKENS_LIMIT(5小时 Token 限额)
68
+ const tokensLimit = limits.find((l) => l.type === "TOKENS_LIMIT");
69
+ if (tokensLimit) {
70
+ const remainPercent = calcRemainPercent(tokensLimit.percentage);
71
+ const progressBar = createProgressBar(remainPercent);
72
+ lines.push(t.zhipuTokensLimit);
73
+ lines.push(`${progressBar} ${t.remaining(remainPercent)}`);
74
+ lines.push(`${t.used}: ${formatTokens(tokensLimit.currentValue)} / ${formatTokens(tokensLimit.usage)}`);
75
+ // 重置时间
76
+ if (tokensLimit.nextResetTime) {
77
+ const resetSeconds = Math.max(0, Math.floor((tokensLimit.nextResetTime - Date.now()) / 1000));
78
+ lines.push(t.resetIn(formatDuration(resetSeconds)));
79
+ }
80
+ }
81
+ // 查找 TIME_LIMIT(MCP 搜索次数)
82
+ const timeLimit = limits.find((l) => l.type === "TIME_LIMIT");
83
+ if (timeLimit) {
84
+ if (tokensLimit)
85
+ lines.push(""); // 分隔符
86
+ const remainPercent = calcRemainPercent(timeLimit.percentage);
87
+ const progressBar = createProgressBar(remainPercent);
88
+ lines.push(t.zhipuMcpLimit);
89
+ lines.push(`${progressBar} ${t.remaining(remainPercent)}`);
90
+ lines.push(`${t.used}: ${timeLimit.currentValue} / ${timeLimit.usage}`);
91
+ }
92
+ // 警告:如果任一限额使用率超过阈值
93
+ const maxPercentage = safeMax(limits.map((l) => l.percentage));
94
+ if (maxPercentage >= HIGH_USAGE_THRESHOLD) {
95
+ lines.push("");
96
+ lines.push(t.limitReached);
97
+ }
98
+ return lines.join("\n");
99
+ }
100
+ async function queryUsage(authData, config) {
101
+ // 检查账号是否存在且有效
102
+ if (authData?.type !== "api" || !authData.key) {
103
+ return null;
104
+ }
105
+ try {
106
+ const usage = await fetchUsage(authData.key, config);
107
+ return {
108
+ success: true,
109
+ output: formatZhipuUsage(usage, authData.key, config.accountLabel),
110
+ };
111
+ }
112
+ catch (err) {
113
+ return {
114
+ success: false,
115
+ error: err instanceof Error ? err.message : String(err),
116
+ };
117
+ }
118
+ }
119
+ /**
120
+ * 查询智谱 AI 账号额度
121
+ * @param authData 智谱认证数据
122
+ * @returns 查询结果,如果账号不存在或无效返回 null
123
+ */
124
+ export async function queryZhipuUsage(authData) {
125
+ return queryUsage(authData, ZHIPU_CONFIG);
126
+ }
127
+ /**
128
+ * 查询 Z.ai 账号额度
129
+ * @param authData Z.ai 认证数据
130
+ * @returns 查询结果,如果账号不存在或无效返回 null
131
+ */
132
+ export async function queryZaiUsage(authData) {
133
+ return queryUsage(authData, ZAI_CONFIG);
134
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * OpenCode 额度状态查询插件
3
+ *
4
+ * [输入]: ~/.local/share/opencode/auth.json 和 ~/.config/opencode/antigravity-accounts.json 中的认证信息
5
+ * [输出]: 带进度条的额度使用情况展示
6
+ * [定位]: 通过 mystatus 工具查询各账号额度
7
+ * [同步]: lib/openai.ts, lib/zhipu.ts, lib/google.ts, lib/types.ts, lib/i18n.ts
8
+ */
9
+ import { Plugin } from "@opencode/plugin";
10
+ declare const _default: Plugin.Plugin;
11
+ export default _default;
12
+ //# sourceMappingURL=mystatus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mystatus.d.ts","sourceRoot":"","sources":["../../plugin/mystatus.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;;AAY1C,wBAiFG"}
@@ -0,0 +1,100 @@
1
+ /**
2
+ * OpenCode 额度状态查询插件
3
+ *
4
+ * [输入]: ~/.local/share/opencode/auth.json 和 ~/.config/opencode/antigravity-accounts.json 中的认证信息
5
+ * [输出]: 带进度条的额度使用情况展示
6
+ * [定位]: 通过 mystatus 工具查询各账号额度
7
+ * [同步]: lib/openai.ts, lib/zhipu.ts, lib/google.ts, lib/types.ts, lib/i18n.ts
8
+ */
9
+ import { readFile } from "node:fs/promises";
10
+ import { homedir } from "node:os";
11
+ import { join } from "node:path";
12
+ import { Plugin } from "@opencode/plugin";
13
+ import { queryCopilotUsage } from "./lib/copilot";
14
+ import { queryGoogleUsage } from "./lib/google";
15
+ import { t } from "./lib/i18n";
16
+ import { queryOpenAIUsage } from "./lib/openai";
17
+ import { queryZaiUsage, queryZhipuUsage } from "./lib/zhipu";
18
+ // ============================================================================
19
+ // V2 插件导出
20
+ // ============================================================================
21
+ export default Plugin.define({
22
+ id: "opencode-mystatus",
23
+ async setup(ctx) {
24
+ await ctx.tool.transform((editor) => {
25
+ editor.add({
26
+ name: "mystatus",
27
+ description: "Query account quota usage for all configured AI platforms. Returns remaining quota percentages, usage stats, and reset countdowns with visual progress bars. Currently supports OpenAI (ChatGPT/Codex), Zhipu AI, Z.ai, Google Antigravity, and GitHub Copilot.",
28
+ input: {
29
+ type: "object",
30
+ properties: {},
31
+ additionalProperties: false,
32
+ },
33
+ async execute() {
34
+ // 1. 读取 auth.json
35
+ const authPath = join(homedir(), ".local/share/opencode/auth.json");
36
+ let authData;
37
+ try {
38
+ const content = await readFile(authPath, "utf-8");
39
+ authData = JSON.parse(content);
40
+ }
41
+ catch (err) {
42
+ return {
43
+ content: t.authError(authPath, err instanceof Error ? err.message : String(err)),
44
+ };
45
+ }
46
+ // 2. 并行查询所有平台(Google 不依赖 authData)
47
+ const [openaiResult, zhipuResult, zaiResult, googleResult, copilotResult,] = await Promise.all([
48
+ queryOpenAIUsage(authData.openai),
49
+ queryZhipuUsage(authData["zhipuai-coding-plan"]),
50
+ queryZaiUsage(authData["zai-coding-plan"]),
51
+ queryGoogleUsage(),
52
+ queryCopilotUsage(authData["github-copilot"]),
53
+ ]);
54
+ // 3. 收集结果
55
+ const results = [];
56
+ const errors = [];
57
+ // 处理 OpenAI 结果
58
+ collectResult(openaiResult, t.openaiTitle, results, errors);
59
+ // 处理智谱结果
60
+ collectResult(zhipuResult, t.zhipuTitle, results, errors);
61
+ // 处理 Z.ai 结果
62
+ collectResult(zaiResult, t.zaiTitle, results, errors);
63
+ // 处理 Google 结果
64
+ collectResult(googleResult, t.googleTitle, results, errors);
65
+ // 处理 GitHub Copilot 结果
66
+ collectResult(copilotResult, t.copilotTitle, results, errors);
67
+ // 4. 汇总输出
68
+ if (results.length === 0 && errors.length === 0) {
69
+ return { content: t.noAccounts };
70
+ }
71
+ let output = results.join("\n");
72
+ if (errors.length > 0) {
73
+ if (output)
74
+ output += "\n\n";
75
+ output += t.queryFailed + errors.join("\n");
76
+ }
77
+ return { content: output };
78
+ },
79
+ });
80
+ });
81
+ },
82
+ });
83
+ /**
84
+ * 收集查询结果到 results 和 errors 数组
85
+ * 注意:这是内部函数,不导出,避免被 OpenCode 当作插件加载
86
+ */
87
+ function collectResult(result, title, results, errors) {
88
+ if (!result)
89
+ return;
90
+ if (result.success && result.output) {
91
+ if (results.length > 0)
92
+ results.push(""); // 分隔符
93
+ results.push(title);
94
+ results.push("");
95
+ results.push(result.output);
96
+ }
97
+ else if (result.error) {
98
+ errors.push(result.error);
99
+ }
100
+ }
package/package.json CHANGED
@@ -1,6 +1,65 @@
1
1
  {
2
2
  "name": "@ekworp/opencode-mystatus",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.2.7",
4
+ "description": "Check all your AI subscription quotas in one command. Supports OpenAI, Zhipu AI, Z.ai, and Google Antigravity. More platforms coming soon.",
5
+ "type": "module",
6
+ "main": "dist/plugin/mystatus.js",
7
+ "types": "dist/plugin/mystatus.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/plugin/mystatus.js",
11
+ "types": "./dist/plugin/mystatus.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "command",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "keywords": [
21
+ "opencode",
22
+ "plugin",
23
+ "quota",
24
+ "status",
25
+ "openai",
26
+ "zhipu",
27
+ "zai",
28
+ "google",
29
+ "antigravity",
30
+ "gemini",
31
+ "claude"
32
+ ],
33
+ "author": "javazys",
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/vbgate/opencode-mystatus.git"
38
+ },
39
+ "bugs": {
40
+ "url": "https://github.com/vbgate/opencode-mystatus/issues"
41
+ },
42
+ "homepage": "https://github.com/vbgate/opencode-mystatus#readme",
43
+ "dependencies": {
44
+ "@opencode/plugin": "2.0.25"
45
+ },
46
+ "devDependencies": {
47
+ "@biomejs/biome": "^2.5.15",
48
+ "@types/node": "^22.0.0",
49
+ "husky": "^9.1.7",
50
+ "typescript": "^5.8.0"
51
+ },
52
+ "peerDependencies": {
53
+ "@opencode/plugin": "2.0.25"
54
+ },
55
+ "engines": {
56
+ "node": ">=18.0.0"
57
+ },
58
+ "scripts": {
59
+ "build": "tsc",
60
+ "typecheck": "tsc --noEmit",
61
+ "lint": "biome check .",
62
+ "lint:fix": "biome check --write .",
63
+ "format": "biome format --write ."
64
+ }
6
65
  }