@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,346 @@
1
+ /**
2
+ * GitHub Copilot Premium Requests Quota Module
3
+ *
4
+ * [Input]: GitHub token from ~/.local/share/opencode/auth.json (github-copilot provider)
5
+ * [Output]: Formatted quota usage information with progress bars
6
+ * [Location]: Called by mystatus.ts to handle GitHub Copilot accounts
7
+ * [Sync]: mystatus.ts, types.ts, utils.ts, i18n.ts
8
+ *
9
+ * [Updated]: Jan 2026 - Handle new OpenCode official partnership auth flow
10
+ * The new OAuth tokens (gho_) need to be exchanged for Copilot session tokens
11
+ * before calling the internal quota API.
12
+ */
13
+ import * as fs from "node:fs";
14
+ import * as os from "node:os";
15
+ import * as path from "node:path";
16
+ import { t } from "./i18n";
17
+ import { createProgressBar, fetchWithTimeout } from "./utils";
18
+ // ============================================================================
19
+ // Constants
20
+ // ============================================================================
21
+ const GITHUB_API_BASE_URL = "https://api.github.com";
22
+ // Config file path for user's fine-grained PAT
23
+ const COPILOT_QUOTA_CONFIG_PATH = path.join(os.homedir(), ".config", "opencode", "copilot-quota-token.json");
24
+ // Updated to match current VS Code Copilot extension version
25
+ const COPILOT_VERSION = "0.35.0";
26
+ const EDITOR_VERSION = "vscode/1.107.0";
27
+ const EDITOR_PLUGIN_VERSION = `copilot-chat/${COPILOT_VERSION}`;
28
+ const USER_AGENT = `GitHubCopilotChat/${COPILOT_VERSION}`;
29
+ // Headers matching opencode-copilot-auth plugin (required for token exchange)
30
+ const COPILOT_HEADERS = {
31
+ "User-Agent": USER_AGENT,
32
+ "Editor-Version": EDITOR_VERSION,
33
+ "Editor-Plugin-Version": EDITOR_PLUGIN_VERSION,
34
+ "Copilot-Integration-Id": "vscode-chat",
35
+ };
36
+ // ============================================================================
37
+ // Token Exchange (New auth flow for official OpenCode partnership)
38
+ // ============================================================================
39
+ /**
40
+ * Read optional Copilot quota config from user's config file
41
+ * Returns null if file doesn't exist or is invalid
42
+ */
43
+ function readQuotaConfig() {
44
+ try {
45
+ if (!fs.existsSync(COPILOT_QUOTA_CONFIG_PATH)) {
46
+ return null;
47
+ }
48
+ const content = fs.readFileSync(COPILOT_QUOTA_CONFIG_PATH, "utf-8");
49
+ const config = JSON.parse(content);
50
+ // Validate required fields
51
+ if (!config.token || !config.username || !config.tier) {
52
+ return null;
53
+ }
54
+ // Validate tier is valid
55
+ const validTiers = [
56
+ "free",
57
+ "pro",
58
+ "pro+",
59
+ "business",
60
+ "enterprise",
61
+ ];
62
+ if (!validTiers.includes(config.tier)) {
63
+ return null;
64
+ }
65
+ return config;
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
71
+ /**
72
+ * Fetch quota using the public GitHub REST API
73
+ * Requires a fine-grained PAT with "Plan" read permission
74
+ */
75
+ async function fetchPublicBillingUsage(config) {
76
+ const response = await fetchWithTimeout(`${GITHUB_API_BASE_URL}/users/${config.username}/settings/billing/premium_request/usage`, {
77
+ headers: {
78
+ Accept: "application/vnd.github+json",
79
+ Authorization: `Bearer ${config.token}`,
80
+ "X-GitHub-Api-Version": "2022-11-28",
81
+ },
82
+ });
83
+ if (!response.ok) {
84
+ const errorText = await response.text();
85
+ throw new Error(t.copilotApiError(response.status, errorText));
86
+ }
87
+ return response.json();
88
+ }
89
+ /**
90
+ * Exchange OAuth token for a Copilot session token
91
+ * Required for the new OpenCode official partnership auth flow (Jan 2026+)
92
+ */
93
+ async function exchangeForCopilotToken(oauthToken) {
94
+ try {
95
+ const response = await fetchWithTimeout(`${GITHUB_API_BASE_URL}/copilot_internal/v2/token`, {
96
+ headers: {
97
+ Accept: "application/json",
98
+ Authorization: `Bearer ${oauthToken}`,
99
+ ...COPILOT_HEADERS,
100
+ },
101
+ });
102
+ if (!response.ok) {
103
+ // Token exchange failed - might be old token format or API change
104
+ return null;
105
+ }
106
+ const tokenData = await response.json();
107
+ return tokenData.token;
108
+ }
109
+ catch {
110
+ return null;
111
+ }
112
+ }
113
+ // ============================================================================
114
+ // API Call
115
+ // ============================================================================
116
+ /**
117
+ * Build headers for GitHub API requests (quota endpoint)
118
+ */
119
+ function buildGitHubHeaders(token) {
120
+ return {
121
+ "Content-Type": "application/json",
122
+ Accept: "application/json",
123
+ Authorization: `Bearer ${token}`,
124
+ ...COPILOT_HEADERS,
125
+ };
126
+ }
127
+ /**
128
+ * Build headers for legacy token format
129
+ */
130
+ function buildLegacyHeaders(token) {
131
+ return {
132
+ "Content-Type": "application/json",
133
+ Accept: "application/json",
134
+ Authorization: `token ${token}`,
135
+ ...COPILOT_HEADERS,
136
+ };
137
+ }
138
+ /**
139
+ * Fetch GitHub Copilot usage data
140
+ * Tries multiple authentication methods to handle both old and new token formats
141
+ */
142
+ async function fetchCopilotUsage(authData) {
143
+ // Use refresh token as the OAuth token (required)
144
+ // In new auth flow, access === refresh (both are the OAuth token)
145
+ const oauthToken = authData.refresh || authData.access;
146
+ if (!oauthToken) {
147
+ throw new Error("No OAuth token found in auth data");
148
+ }
149
+ const cachedAccessToken = authData.access;
150
+ const tokenExpiry = authData.expires || 0;
151
+ // Strategy 1: If we have a valid cached access token (from previous exchange), use it
152
+ if (cachedAccessToken &&
153
+ cachedAccessToken !== oauthToken &&
154
+ tokenExpiry > Date.now()) {
155
+ const response = await fetchWithTimeout(`${GITHUB_API_BASE_URL}/copilot_internal/user`, { headers: buildGitHubHeaders(cachedAccessToken) });
156
+ if (response.ok) {
157
+ return response.json();
158
+ }
159
+ }
160
+ // Strategy 2: Try direct call with OAuth token (works with older token formats)
161
+ const directResponse = await fetchWithTimeout(`${GITHUB_API_BASE_URL}/copilot_internal/user`, { headers: buildLegacyHeaders(oauthToken) });
162
+ if (directResponse.ok) {
163
+ return directResponse.json();
164
+ }
165
+ // Strategy 3: Exchange OAuth token for Copilot session token (new auth flow)
166
+ const copilotToken = await exchangeForCopilotToken(oauthToken);
167
+ if (copilotToken) {
168
+ const exchangedResponse = await fetchWithTimeout(`${GITHUB_API_BASE_URL}/copilot_internal/user`, { headers: buildGitHubHeaders(copilotToken) });
169
+ if (exchangedResponse.ok) {
170
+ return exchangedResponse.json();
171
+ }
172
+ const errorText = await exchangedResponse.text();
173
+ throw new Error(t.copilotApiError(exchangedResponse.status, errorText));
174
+ }
175
+ // All strategies failed - likely due to OpenCode's OAuth token lacking copilot scope
176
+ // The new OpenCode partnership uses a different OAuth client that doesn't grant
177
+ // access to the /copilot_internal/* endpoints
178
+ throw new Error(`${t.copilotQuotaUnavailable}\n\n${t.copilotQuotaWorkaround}`);
179
+ }
180
+ // ============================================================================
181
+ // Formatting
182
+ // ============================================================================
183
+ /**
184
+ * Format a single quota line
185
+ */
186
+ function formatQuotaLine(name, quota, width = 20) {
187
+ if (!quota)
188
+ return "";
189
+ if (quota.unlimited) {
190
+ return `${name.padEnd(14)} Unlimited`;
191
+ }
192
+ const total = quota.entitlement;
193
+ const used = total - quota.remaining;
194
+ const percentRemaining = Math.round(quota.percent_remaining);
195
+ const progressBar = createProgressBar(percentRemaining, width);
196
+ return `${name.padEnd(14)} ${progressBar} ${percentRemaining}% (${used}/${total})`;
197
+ }
198
+ /**
199
+ * Calculate days until reset
200
+ */
201
+ function getResetCountdown(resetDate) {
202
+ const reset = new Date(resetDate);
203
+ const now = new Date();
204
+ const diffMs = reset.getTime() - now.getTime();
205
+ if (diffMs <= 0)
206
+ return t.resetsSoon;
207
+ const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
208
+ const hours = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
209
+ if (days > 0) {
210
+ return `${days}d ${hours}h`;
211
+ }
212
+ return `${hours}h`;
213
+ }
214
+ /**
215
+ * Format GitHub Copilot usage information
216
+ */
217
+ function formatCopilotUsage(data) {
218
+ const lines = [];
219
+ // Account info
220
+ lines.push(`${t.account} GitHub Copilot (${data.copilot_plan})`);
221
+ lines.push("");
222
+ // Premium requests (main quota)
223
+ const premium = data.quota_snapshots.premium_interactions;
224
+ if (premium) {
225
+ const premiumLine = formatQuotaLine(t.premiumRequests, premium);
226
+ if (premiumLine)
227
+ lines.push(premiumLine);
228
+ // Show overage info if applicable
229
+ if (premium.overage_count > 0) {
230
+ lines.push(`${t.overage}: ${premium.overage_count} ${t.overageRequests}`);
231
+ }
232
+ }
233
+ // Chat quota (if separate)
234
+ const chat = data.quota_snapshots.chat;
235
+ if (chat && !chat.unlimited) {
236
+ const chatLine = formatQuotaLine(t.chatQuota, chat);
237
+ if (chatLine)
238
+ lines.push(chatLine);
239
+ }
240
+ // Completions quota (if separate)
241
+ const completions = data.quota_snapshots.completions;
242
+ if (completions && !completions.unlimited) {
243
+ const completionsLine = formatQuotaLine(t.completionsQuota, completions);
244
+ if (completionsLine)
245
+ lines.push(completionsLine);
246
+ }
247
+ // Reset date
248
+ lines.push("");
249
+ const resetCountdown = getResetCountdown(data.quota_reset_date);
250
+ lines.push(`${t.quotaResets}: ${resetCountdown} (${data.quota_reset_date})`);
251
+ return lines.join("\n");
252
+ }
253
+ // Copilot plan limits (premium requests per month)
254
+ // Source: https://docs.github.com/en/copilot/about-github-copilot/subscription-plans-for-github-copilot
255
+ const COPILOT_PLAN_LIMITS = {
256
+ free: 50, // Copilot Free: 50 premium requests/month
257
+ pro: 300, // Copilot Pro: 300 premium requests/month
258
+ "pro+": 1500, // Copilot Pro+: 1500 premium requests/month
259
+ business: 300, // Copilot Business: 300 premium requests/month
260
+ enterprise: 1000, // Copilot Enterprise: 1000 premium requests/month
261
+ };
262
+ /**
263
+ * Format public billing API response
264
+ * Different structure from internal API - aggregates usage items
265
+ * Uses grossQuantity (total requests made) since netQuantity shows post-discount amount
266
+ */
267
+ function formatPublicBillingUsage(data, tier) {
268
+ const lines = [];
269
+ // Account info
270
+ lines.push(`${t.account} GitHub Copilot (@${data.user})`);
271
+ lines.push("");
272
+ // Aggregate all premium request usage (sum grossQuantity across all models)
273
+ const premiumItems = data.usageItems.filter((item) => item.sku === "Copilot Premium Request" || item.sku.includes("Premium"));
274
+ const totalUsed = premiumItems.reduce((sum, item) => sum + item.grossQuantity, 0);
275
+ // Get limit from tier
276
+ const limit = COPILOT_PLAN_LIMITS[tier];
277
+ const remaining = Math.max(0, limit - totalUsed);
278
+ const percentRemaining = Math.round((remaining / limit) * 100);
279
+ const progressBar = createProgressBar(percentRemaining, 20);
280
+ lines.push(`${t.premiumRequests.padEnd(14)} ${progressBar} ${percentRemaining}% (${totalUsed}/${limit})`);
281
+ // Show model breakdown
282
+ const modelItems = data.usageItems.filter((item) => item.model && item.grossQuantity > 0);
283
+ if (modelItems.length > 0) {
284
+ lines.push("");
285
+ lines.push(t.modelBreakdown || "Model breakdown:");
286
+ // Sort by usage descending
287
+ const sortedItems = [...modelItems].sort((a, b) => b.grossQuantity - a.grossQuantity);
288
+ for (const item of sortedItems.slice(0, 5)) {
289
+ // Show top 5 models
290
+ lines.push(` ${item.model}: ${item.grossQuantity} ${item.unitType}`);
291
+ }
292
+ }
293
+ // Time period info
294
+ lines.push("");
295
+ const period = data.timePeriod;
296
+ const periodStr = period.month
297
+ ? `${period.year}-${String(period.month).padStart(2, "0")}`
298
+ : `${period.year}`;
299
+ lines.push(`${t.billingPeriod || "Period"}: ${periodStr}`);
300
+ return lines.join("\n");
301
+ }
302
+ /**
303
+ * Query GitHub Copilot account quota
304
+ * @param authData GitHub Copilot authentication data (optional if using PAT config)
305
+ * @returns Query result, null if no account configured
306
+ */
307
+ export async function queryCopilotUsage(authData) {
308
+ // Strategy 1: Try public billing API with user's fine-grained PAT
309
+ const quotaConfig = readQuotaConfig();
310
+ if (quotaConfig) {
311
+ try {
312
+ const billingUsage = await fetchPublicBillingUsage(quotaConfig);
313
+ return {
314
+ success: true,
315
+ output: formatPublicBillingUsage(billingUsage, quotaConfig.tier),
316
+ };
317
+ }
318
+ catch (err) {
319
+ // PAT config exists but failed - report the error
320
+ return {
321
+ success: false,
322
+ error: err instanceof Error ? err.message : String(err),
323
+ };
324
+ }
325
+ }
326
+ // Strategy 2: Try internal API with OAuth token (legacy, may not work with new OpenCode auth)
327
+ // Check if account exists and has a refresh token (the GitHub OAuth token)
328
+ if (authData?.type !== "oauth" || !authData.refresh) {
329
+ // No Copilot account is configured. Do not query GitHub or report an
330
+ // error for platforms the user has not enabled.
331
+ return null;
332
+ }
333
+ try {
334
+ const usage = await fetchCopilotUsage(authData);
335
+ return {
336
+ success: true,
337
+ output: formatCopilotUsage(usage),
338
+ };
339
+ }
340
+ catch (err) {
341
+ return {
342
+ success: false,
343
+ error: err instanceof Error ? err.message : String(err),
344
+ };
345
+ }
346
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Google Cloud 额度查询模块
3
+ *
4
+ * [输入]: ~/.config/opencode/antigravity-accounts.json 中的账号信息
5
+ * [输出]: 格式化的额度使用情况(按重置时间自动分组)
6
+ * [定位]: 被 mystatus.ts 调用,处理 Google Cloud 账号
7
+ * [同步]: mystatus.ts, types.ts, utils.ts, i18n.ts
8
+ */
9
+ import { type QueryResult } from "./types";
10
+ /**
11
+ * 查询所有 Antigravity 账号的额度
12
+ * @returns 查询结果
13
+ */
14
+ export declare function queryGoogleUsage(): Promise<QueryResult | null>;
15
+ //# sourceMappingURL=google.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"google.d.ts","sourceRoot":"","sources":["../../../plugin/lib/google.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,SAAS,CAAC;AAsRjB;;;GAGG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CA2EpE"}
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Google Cloud 额度查询模块
3
+ *
4
+ * [输入]: ~/.config/opencode/antigravity-accounts.json 中的账号信息
5
+ * [输出]: 格式化的额度使用情况(按重置时间自动分组)
6
+ * [定位]: 被 mystatus.ts 调用,处理 Google Cloud 账号
7
+ * [同步]: mystatus.ts, types.ts, utils.ts, i18n.ts
8
+ */
9
+ import { readFile } from "node:fs/promises";
10
+ import { homedir } from "node:os";
11
+ import { join } from "node:path";
12
+ import { currentLang, t } from "./i18n";
13
+ import { HIGH_USAGE_THRESHOLD, } from "./types";
14
+ import { createProgressBar, fetchWithTimeout, safeMax } from "./utils";
15
+ // ============================================================================
16
+ // 常量
17
+ // ============================================================================
18
+ const GOOGLE_QUOTA_API_URL = "https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels";
19
+ const USER_AGENT = "antigravity/1.11.9 windows/amd64";
20
+ // 需要显示的 4 个模型配置
21
+ const MODELS_TO_DISPLAY = [
22
+ { key: "gemini-3-pro-high", altKey: "gemini-3-pro-low", display: "G3 Pro" },
23
+ { key: "gemini-3-pro-image", display: "G3 Image" },
24
+ { key: "gemini-3-flash", display: "G3 Flash" },
25
+ {
26
+ key: "claude-opus-4-5-thinking",
27
+ altKey: "claude-opus-4-5",
28
+ display: "Claude",
29
+ },
30
+ ];
31
+ // 获取 Antigravity 账号文件路径
32
+ function getAntigravityAccountsPath() {
33
+ const home = homedir();
34
+ const configDir = process.platform === "win32"
35
+ ? process.env.APPDATA || join(home, "AppData", "Roaming")
36
+ : join(home, ".config");
37
+ return join(configDir, "opencode", "antigravity-accounts.json");
38
+ }
39
+ const GOOGLE_TOKEN_REFRESH_URL = "https://oauth2.googleapis.com/token";
40
+ // OAuth credentials from opencode-antigravity-auth project
41
+ // Source: https://github.com/NoeFabris/opencode-antigravity-auth
42
+ const GOOGLE_CLIENT_ID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com";
43
+ const GOOGLE_CLIENT_SECRET = "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";
44
+ // ============================================================================
45
+ // 工具函数
46
+ // ============================================================================
47
+ /**
48
+ * 格式化重置时间为简短显示(如 "4h 59m")
49
+ */
50
+ function formatResetTimeShort(isoTime) {
51
+ if (!isoTime)
52
+ return "-";
53
+ try {
54
+ const resetDate = new Date(isoTime);
55
+ const now = new Date();
56
+ const diffMs = resetDate.getTime() - now.getTime();
57
+ if (diffMs <= 0)
58
+ return currentLang === "zh" ? "已重置" : "reset";
59
+ const diffMinutes = Math.floor(diffMs / 60000);
60
+ const days = Math.floor(diffMinutes / 1440);
61
+ const hours = Math.floor((diffMinutes % 1440) / 60);
62
+ const minutes = diffMinutes % 60;
63
+ if (days > 0) {
64
+ return `${days}d ${hours}h`;
65
+ }
66
+ return `${hours}h ${minutes}m`;
67
+ }
68
+ catch {
69
+ return "-";
70
+ }
71
+ }
72
+ /**
73
+ * 从 API 响应中提取 4 个模型的额度信息
74
+ */
75
+ function extractModelQuotas(data) {
76
+ const quotas = [];
77
+ for (const modelConfig of MODELS_TO_DISPLAY) {
78
+ let modelInfo = data.models[modelConfig.key];
79
+ // 如果主 key 没有数据,尝试 altKey
80
+ if (!modelInfo && modelConfig.altKey) {
81
+ modelInfo = data.models[modelConfig.altKey];
82
+ }
83
+ // 只要模型存在就显示(即使没有 quotaInfo 或额度为 0)
84
+ if (modelInfo) {
85
+ const remainingFraction = modelInfo.quotaInfo?.remainingFraction ?? 0;
86
+ quotas.push({
87
+ displayName: modelConfig.display,
88
+ remainPercent: Math.round(remainingFraction * 100),
89
+ resetTimeDisplay: formatResetTimeShort(modelInfo.quotaInfo?.resetTime || ""),
90
+ });
91
+ }
92
+ }
93
+ return quotas;
94
+ }
95
+ /**
96
+ * 刷新 Google access token
97
+ */
98
+ async function refreshAccessToken(refreshToken) {
99
+ const params = new URLSearchParams({
100
+ client_id: GOOGLE_CLIENT_ID,
101
+ client_secret: GOOGLE_CLIENT_SECRET,
102
+ refresh_token: refreshToken,
103
+ grant_type: "refresh_token",
104
+ });
105
+ const response = await fetch(GOOGLE_TOKEN_REFRESH_URL, {
106
+ method: "POST",
107
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
108
+ body: params,
109
+ });
110
+ if (!response.ok) {
111
+ const errorText = await response.text();
112
+ throw new Error(t.googleApiError(response.status, errorText));
113
+ }
114
+ return response.json();
115
+ }
116
+ // ============================================================================
117
+ // API 调用
118
+ // ============================================================================
119
+ /**
120
+ * 获取 Google Cloud 使用情况
121
+ */
122
+ async function fetchGoogleUsage(accessToken, projectId) {
123
+ const response = await fetchWithTimeout(GOOGLE_QUOTA_API_URL, {
124
+ method: "POST",
125
+ headers: {
126
+ "Content-Type": "application/json",
127
+ Authorization: `Bearer ${accessToken}`,
128
+ "User-Agent": USER_AGENT,
129
+ },
130
+ body: JSON.stringify({ project: projectId }),
131
+ });
132
+ if (!response.ok) {
133
+ const errorText = await response.text();
134
+ throw new Error(t.googleApiError(response.status, errorText));
135
+ }
136
+ return response.json();
137
+ }
138
+ /**
139
+ * 查询单个账号的额度
140
+ */
141
+ async function fetchAccountQuota(account) {
142
+ try {
143
+ // 刷新 access token
144
+ const { access_token } = await refreshAccessToken(account.refreshToken);
145
+ // 使用 projectId 或 managedProjectId
146
+ const projectId = account.projectId || account.managedProjectId;
147
+ if (!projectId) {
148
+ return { success: false, error: t.googleNoProjectId };
149
+ }
150
+ // 查询额度
151
+ const data = await fetchGoogleUsage(access_token, projectId);
152
+ // 提取 4 个模型的额度
153
+ const models = extractModelQuotas(data);
154
+ if (models.length === 0) {
155
+ return { success: true, models: undefined, maxUsage: 0 };
156
+ }
157
+ // 计算最大使用率
158
+ const maxUsage = safeMax(models.map((m) => 100 - m.remainPercent));
159
+ return { success: true, models, maxUsage };
160
+ }
161
+ catch (err) {
162
+ return {
163
+ success: false,
164
+ error: err instanceof Error ? err.message : String(err),
165
+ };
166
+ }
167
+ }
168
+ // ============================================================================
169
+ // 格式化输出
170
+ // ============================================================================
171
+ /**
172
+ * 格式化单个账号的额度(4 个模型分别显示)
173
+ */
174
+ function formatAccountQuota(quotaInfo) {
175
+ const lines = [];
176
+ // 标题行
177
+ lines.push(`### ${quotaInfo.email}`);
178
+ if (quotaInfo.models.length === 0) {
179
+ lines.push("");
180
+ lines.push(t.noQuotaData);
181
+ return lines.join("\n");
182
+ }
183
+ lines.push("");
184
+ // 每个模型一行:模型名 | 重置时间 | 百分比
185
+ for (const model of quotaInfo.models) {
186
+ const progressBar = createProgressBar(model.remainPercent, 20);
187
+ lines.push(`${model.displayName.padEnd(10)} ${model.resetTimeDisplay.padEnd(10)} ${progressBar} ${model.remainPercent}%`);
188
+ }
189
+ // 警告
190
+ if (quotaInfo.maxUsage >= HIGH_USAGE_THRESHOLD) {
191
+ lines.push("");
192
+ lines.push(t.limitReached);
193
+ }
194
+ return lines.join("\n");
195
+ }
196
+ // ============================================================================
197
+ // 导出接口
198
+ // ============================================================================
199
+ /**
200
+ * 查询所有 Antigravity 账号的额度
201
+ * @returns 查询结果
202
+ */
203
+ export async function queryGoogleUsage() {
204
+ try {
205
+ // 读取账号文件
206
+ const accountsPath = getAntigravityAccountsPath();
207
+ let content;
208
+ try {
209
+ content = await readFile(accountsPath, "utf-8");
210
+ }
211
+ catch (err) {
212
+ // No Antigravity configuration means Google is not enabled.
213
+ if (err &&
214
+ typeof err === "object" &&
215
+ "code" in err &&
216
+ err.code === "ENOENT") {
217
+ return null;
218
+ }
219
+ throw err;
220
+ }
221
+ const file = JSON.parse(content);
222
+ if (!file.accounts || file.accounts.length === 0) {
223
+ return null;
224
+ }
225
+ // 过滤掉没有邮箱的账号
226
+ const validAccounts = file.accounts.filter((account) => account.email);
227
+ if (validAccounts.length === 0) {
228
+ return null;
229
+ }
230
+ // 并行查询所有账号
231
+ const results = await Promise.all(validAccounts.map((account) => fetchAccountQuota(account).then((result) => ({ account, result }))));
232
+ // 收集输出
233
+ const outputs = [];
234
+ for (const { account, result } of results) {
235
+ if (!result.success) {
236
+ outputs.push(`${account.email || t.unknown}: ${result.error}`);
237
+ }
238
+ else if (result.models && result.models.length > 0) {
239
+ const quotaInfo = {
240
+ email: account.email || t.unknown,
241
+ models: result.models,
242
+ maxUsage: result.maxUsage || 0,
243
+ };
244
+ outputs.push(formatAccountQuota(quotaInfo));
245
+ }
246
+ }
247
+ // 如果没有符合条件的账号
248
+ if (outputs.length === 0) {
249
+ return {
250
+ success: true,
251
+ output: t.noQuotaData,
252
+ };
253
+ }
254
+ return {
255
+ success: true,
256
+ output: outputs.join("\n\n"),
257
+ };
258
+ }
259
+ catch (err) {
260
+ return {
261
+ success: false,
262
+ error: err instanceof Error ? err.message : String(err),
263
+ };
264
+ }
265
+ }