@ekworp/opencode-mystatus 0.0.0-stage → 1.2.8
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/LICENSE +21 -0
- package/README.md +240 -2
- package/README.zh-CN.md +239 -0
- package/command/mystatus.md +5 -0
- package/dist/plugin/lib/config.d.ts +6 -0
- package/dist/plugin/lib/config.js +29 -0
- package/dist/plugin/lib/copilot.d.ts +20 -0
- package/dist/plugin/lib/copilot.d.ts.map +1 -0
- package/dist/plugin/lib/copilot.js +346 -0
- package/dist/plugin/lib/deepseek.d.ts +3 -0
- package/dist/plugin/lib/deepseek.js +46 -0
- package/dist/plugin/lib/google.d.ts +14 -0
- package/dist/plugin/lib/google.d.ts.map +1 -0
- package/dist/plugin/lib/google.js +265 -0
- package/dist/plugin/lib/i18n.d.ts +111 -0
- package/dist/plugin/lib/i18n.d.ts.map +1 -0
- package/dist/plugin/lib/i18n.js +176 -0
- package/dist/plugin/lib/openai.d.ts +16 -0
- package/dist/plugin/lib/openai.d.ts.map +1 -0
- package/dist/plugin/lib/openai.js +161 -0
- package/dist/plugin/lib/opencode-auth.d.ts +9 -0
- package/dist/plugin/lib/opencode-auth.js +48 -0
- package/dist/plugin/lib/types.d.ts +97 -0
- package/dist/plugin/lib/types.d.ts.map +1 -0
- package/dist/plugin/lib/types.js +13 -0
- package/dist/plugin/lib/utils.d.ts +43 -0
- package/dist/plugin/lib/utils.d.ts.map +1 -0
- package/dist/plugin/lib/utils.js +115 -0
- package/dist/plugin/lib/zhipu.d.ts +22 -0
- package/dist/plugin/lib/zhipu.d.ts.map +1 -0
- package/dist/plugin/lib/zhipu.js +134 -0
- package/dist/plugin/mystatus.d.ts +11 -0
- package/dist/plugin/mystatus.d.ts.map +1 -0
- package/dist/plugin/mystatus.js +126 -0
- package/package.json +67 -4
|
@@ -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,46 @@
|
|
|
1
|
+
/** DeepSeek account balance query module. */
|
|
2
|
+
import { t } from "./i18n";
|
|
3
|
+
import { fetchWithTimeout, maskString } from "./utils";
|
|
4
|
+
const DEEPSEEK_BALANCE_URL = "https://api.deepseek.com/user/balance";
|
|
5
|
+
async function fetchBalance(apiKey) {
|
|
6
|
+
const response = await fetchWithTimeout(DEEPSEEK_BALANCE_URL, {
|
|
7
|
+
headers: {
|
|
8
|
+
Authorization: `Bearer ${apiKey}`,
|
|
9
|
+
"Content-Type": "application/json",
|
|
10
|
+
"User-Agent": "OpenCode-Status-Plugin/1.0",
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
if (!response.ok) {
|
|
14
|
+
const errorText = await response.text();
|
|
15
|
+
throw new Error(t.deepseekApiError(response.status, errorText));
|
|
16
|
+
}
|
|
17
|
+
return (await response.json());
|
|
18
|
+
}
|
|
19
|
+
function formatBalance(data, apiKey) {
|
|
20
|
+
const lines = [
|
|
21
|
+
`${t.account} ${maskString(apiKey)} (${t.deepseekAccountName})`,
|
|
22
|
+
"",
|
|
23
|
+
];
|
|
24
|
+
if (!data.is_available || !data.balance_infos?.length) {
|
|
25
|
+
lines.push(t.deepseekNoBalance);
|
|
26
|
+
return lines.join("\n");
|
|
27
|
+
}
|
|
28
|
+
for (const balance of data.balance_infos) {
|
|
29
|
+
lines.push(`${t.deepseekBalance}: ${balance.total_balance} ${balance.currency}`);
|
|
30
|
+
}
|
|
31
|
+
return lines.join("\n");
|
|
32
|
+
}
|
|
33
|
+
export async function queryDeepSeekUsage(authData) {
|
|
34
|
+
if (authData?.type !== "api" || !authData.key)
|
|
35
|
+
return null;
|
|
36
|
+
try {
|
|
37
|
+
const balance = await fetchBalance(authData.key);
|
|
38
|
+
return { success: true, output: formatBalance(balance, authData.key) };
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
return {
|
|
42
|
+
success: false,
|
|
43
|
+
error: err instanceof Error ? err.message : String(err),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
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>;
|
|
@@ -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
|
+
}
|