diff --git a/README.md b/README.md index 5ab5080..369da89 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,14 @@ An [OpenCode](https://opencode.ai) plugin to query account quota usage for multi ## Supported Platforms -| Platform | Account Type | Data Source | -| ------------ | ----------------- | ---------------------------------------------- | -| OpenAI | Plus / Team / Pro | `~/.local/share/opencode/auth.json` | -| Zhipu AI | Coding Plan | `~/.local/share/opencode/auth.json` | -| Z.ai | Coding Plan | `~/.local/share/opencode/auth.json` | -| GitHub Copilot | Individual / Business | `~/.local/share/opencode/auth.json` | -| Google Cloud | Antigravity | `~/.config/opencode/antigravity-accounts.json` | +| Platform | Account Type | Data Source | +| ------------ | --------------------------- | ---------------------------------------------- | +| Anthropic | Claude Code / Pro / Max | `~/.local/share/opencode/auth.json` | +| OpenAI | Plus / Team / Pro | `~/.local/share/opencode/auth.json` | +| Zhipu AI | Coding Plan | `~/.local/share/opencode/auth.json` | +| Z.ai | Coding Plan | `~/.local/share/opencode/auth.json` | +| GitHub Copilot | Individual / Business | `~/.local/share/opencode/auth.json` | +| Google Cloud | Antigravity | `~/.config/opencode/antigravity-accounts.json` | ## Installation @@ -104,6 +105,18 @@ OpenCode will automatically use the mystatus tool to answer your question. ## Output Example ``` +## Anthropic Account Quota + +Account: Claude + +5-hour limit +██████████████████████████░░░░ 86% remaining +Resets in: 4h 42m + +7-day limit +█████████████████████████████░ 96% remaining +Resets in: 6d 23h 42m + ## OpenAI Account Quota Account: user@example.com (team) @@ -161,9 +174,13 @@ Claude 2d 9h ░░░░░░░░░░░░░░░░░░░ No additional configuration required. The plugin automatically reads credentials from: -- **OpenAI, Zhipu AI, Z.ai & GitHub Copilot**: `~/.local/share/opencode/auth.json` +- **Anthropic, OpenAI, Zhipu AI, Z.ai & GitHub Copilot**: `~/.local/share/opencode/auth.json` - **Google Cloud**: `~/.config/opencode/antigravity-accounts.json` +### Anthropic Setup + +To query Anthropic quota, install and authenticate [opencode-claude-auth](https://github.com/griffinmartin/opencode-claude-auth) first so Claude OAuth credentials are synced into OpenCode auth storage. + ### Google Cloud Setup To query Google Cloud (Antigravity) account quota, you need to install the [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) plugin first to authenticate your Google account. @@ -177,8 +194,9 @@ This plugin is safe to use: - `~/.local/share/opencode/auth.json` - OpenCode's official auth storage - `~/.config/opencode/antigravity-accounts.json` - Antigravity plugin's account storage -**API Endpoints (all official):** +**API Endpoints:** +- `https://api.anthropic.com/api/oauth/usage` - Anthropic OAuth quota endpoint - `https://chatgpt.com/backend-api/wham/usage` - OpenAI official quota API - `https://bigmodel.cn/api/monitor/usage/quota/limit` - Zhipu AI official quota API - `https://api.z.ai/api/monitor/usage/quota/limit` - Z.ai official quota API @@ -192,6 +210,10 @@ This plugin is safe to use: - Sensitive information (API keys) is automatically masked in output - Source code is fully open for review +### Anthropic Notes + +Anthropic quota support relies on OAuth credentials made available by Claude Code / [opencode-claude-auth](https://github.com/griffinmartin/opencode-claude-auth) and a private quota endpoint. It may change if Anthropic changes their OAuth infrastructure. + ## Google Cloud Models The plugin displays quota for these models: diff --git a/README.zh-CN.md b/README.zh-CN.md index 06bb45e..03bf2a5 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -12,13 +12,14 @@ ## 支持的平台 -| 平台 | 账号类型 | 数据来源 | -| ------------ | ----------------- | ---------------------------------------------- | -| OpenAI | Plus / Team / Pro | `~/.local/share/opencode/auth.json` | -| 智谱 AI | Coding Plan | `~/.local/share/opencode/auth.json` | -| Z.ai | Coding Plan | `~/.local/share/opencode/auth.json` | -| GitHub Copilot | Individual / Business | `~/.local/share/opencode/auth.json` | -| Google Cloud | Antigravity | `~/.config/opencode/antigravity-accounts.json` | +| 平台 | 账号类型 | 数据来源 | +| ------------ | ---------------------------- | ---------------------------------------------- | +| Anthropic | Claude Code / Pro / Max | `~/.local/share/opencode/auth.json` | +| OpenAI | Plus / Team / Pro | `~/.local/share/opencode/auth.json` | +| 智谱 AI | Coding Plan | `~/.local/share/opencode/auth.json` | +| Z.ai | Coding Plan | `~/.local/share/opencode/auth.json` | +| GitHub Copilot | Individual / Business | `~/.local/share/opencode/auth.json` | +| Google Cloud | Antigravity | `~/.config/opencode/antigravity-accounts.json` | ## 安装 @@ -104,6 +105,18 @@ OpenCode 会自动调用 mystatus 工具来回答你的问题。 ## 输出示例 ``` +## Anthropic 账号额度 + +Account: Claude + +5小时限额 +██████████████████████████░░░░ 剩余 86% +重置: 4小时42分钟后 + +7天限额 +█████████████████████████████░ 剩余 96% +重置: 6天23小时42分钟后 + ## OpenAI 账号额度 Account: user@example.com (team) @@ -161,9 +174,13 @@ Claude 2d 9h ░░░░░░░░░░░░░░░░░░░ 无需额外配置。插件自动从以下位置读取认证信息: -- **OpenAI、智谱 AI、Z.ai 和 GitHub Copilot**: `~/.local/share/opencode/auth.json` +- **Anthropic、OpenAI、智谱 AI、Z.ai 和 GitHub Copilot**: `~/.local/share/opencode/auth.json` - **Google Cloud**: `~/.config/opencode/antigravity-accounts.json` +### Anthropic 设置 + +如需查询 Anthropic 额度,请先安装并完成 [opencode-claude-auth](https://github.com/griffinmartin/opencode-claude-auth) 认证,让 Claude OAuth 凭据同步到 OpenCode 的认证存储中。 + ### Google Cloud 设置 如需查询 Google Cloud (Antigravity) 账号额度,需要先安装 [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) 插件来完成 Google 账号认证。 @@ -177,8 +194,9 @@ Claude 2d 9h ░░░░░░░░░░░░░░░░░░░ - `~/.local/share/opencode/auth.json` - OpenCode 官方认证存储 - `~/.config/opencode/antigravity-accounts.json` - Antigravity 插件的账号存储 -**请求的 API 接口(均为官方接口):** +**请求的 API 接口:** +- `https://api.anthropic.com/api/oauth/usage` - Anthropic OAuth 额度接口 - `https://chatgpt.com/backend-api/wham/usage` - OpenAI 官方额度查询接口 - `https://bigmodel.cn/api/monitor/usage/quota/limit` - 智谱 AI 官方额度查询接口 - `https://api.z.ai/api/monitor/usage/quota/limit` - Z.ai 官方额度查询接口 @@ -192,6 +210,10 @@ Claude 2d 9h ░░░░░░░░░░░░░░░░░░░ - 敏感信息(API Key)在输出时自动脱敏显示 - 源代码完全开源,可随时审查 +### Anthropic 说明 + +Anthropic 额度支持依赖 Claude Code / [opencode-claude-auth](https://github.com/griffinmartin/opencode-claude-auth) 提供的 OAuth 凭据以及一个私有额度接口。如果 Anthropic 调整 OAuth 基础设施,这部分功能可能会失效。 + ## Google Cloud 模型 插件显示以下模型的额度: diff --git a/package.json b/package.json index 2a5530c..31e09a3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "opencode-mystatus", "version": "1.2.4", - "description": "Check all your AI subscription quotas in one command. Supports OpenAI, Zhipu AI, Z.ai, and Google Antigravity. More platforms coming soon.", + "description": "Check all your AI subscription quotas in one command. Supports Anthropic, OpenAI, Zhipu AI, Z.ai, GitHub Copilot, and Google Antigravity.", "type": "module", "main": "dist/plugin/mystatus.js", "types": "dist/plugin/mystatus.d.ts", diff --git a/plugin/lib/anthropic.ts b/plugin/lib/anthropic.ts new file mode 100644 index 0000000..9feda33 --- /dev/null +++ b/plugin/lib/anthropic.ts @@ -0,0 +1,133 @@ +import { t } from "./i18n"; +import { type AnthropicAuthData, type QueryResult } from "./types"; +import { + calcRemainPercent, + createProgressBar, + fetchWithTimeout, + formatDuration, + getResetAfterSeconds, +} from "./utils"; + +interface AnthropicUsageWindow { + utilization: number; + resets_at?: string; +} + +interface AnthropicExtraUsage { + is_enabled?: boolean; + used_credits?: number; + monthly_limit?: number; +} + +interface AnthropicUsageResponse { + five_hour?: AnthropicUsageWindow; + seven_day?: AnthropicUsageWindow; + extra_usage?: AnthropicExtraUsage; +} + +const ANTHROPIC_USAGE_URL = "https://api.anthropic.com/api/oauth/usage"; +const ANTHROPIC_BETA = "oauth-2025-04-20"; + +function formatCredits(cents: number): string { + return `$${(cents / 100).toFixed(2)}`; +} + +function formatWindow( + title: string, + window: AnthropicUsageWindow | undefined, +): string[] { + if (!window) + return []; + + const remainPercent = calcRemainPercent(window.utilization); + const progressBar = createProgressBar(remainPercent); + const lines = [title, `${progressBar} ${t.remaining(remainPercent)}`]; + const resetAfterSeconds = getResetAfterSeconds(window.resets_at); + + if (resetAfterSeconds !== null) { + lines.push(t.resetIn(formatDuration(resetAfterSeconds))); + } + + return lines; +} + +async function fetchAnthropicUsage( + accessToken: string, +): Promise { + const response = await fetchWithTimeout(ANTHROPIC_USAGE_URL, { + headers: { + Authorization: `Bearer ${accessToken}`, + "anthropic-beta": ANTHROPIC_BETA, + "Content-Type": "application/json", + "User-Agent": "OpenCode-Status-Plugin/1.0", + }, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(t.anthropicApiError(response.status, errorText)); + } + + return response.json() as Promise; +} + +function formatAnthropicUsage(data: AnthropicUsageResponse): string { + const lines: string[] = []; + + lines.push(`${t.account} ${t.anthropicAccountName}`); + + const fiveHourLines = formatWindow(t.fiveHourLimit, data.five_hour); + const sevenDayLines = formatWindow(t.sevenDayLimit, data.seven_day); + + if (fiveHourLines.length > 0) { + lines.push(""); + lines.push(...fiveHourLines); + } + + if (sevenDayLines.length > 0) { + lines.push(""); + lines.push(...sevenDayLines); + } + + if (data.extra_usage?.is_enabled && data.extra_usage.monthly_limit) { + lines.push(""); + lines.push( + `${t.extraUsage}: ${formatCredits(data.extra_usage.used_credits || 0)} / ${formatCredits(data.extra_usage.monthly_limit)}`, + ); + } + + if (lines.length === 1) { + lines.push(""); + lines.push(t.noQuotaData); + } + + return lines.join("\n"); +} + +export async function queryAnthropicUsage( + authData: AnthropicAuthData | undefined, +): Promise { + if (!authData || authData.type !== "oauth" || !authData.access) { + return null; + } + + if (authData.expires && authData.expires < Date.now()) { + return { + success: false, + error: t.anthropicTokenExpired, + }; + } + + try { + const usage = await fetchAnthropicUsage(authData.access); + return { + success: true, + output: formatAnthropicUsage(usage), + }; + } catch (err) { + return { + success: false, + error: err instanceof Error ? err.message : String(err), + }; + } +} diff --git a/plugin/lib/i18n.ts b/plugin/lib/i18n.ts index 2c15d4b..24d8efc 100644 --- a/plugin/lib/i18n.ts +++ b/plugin/lib/i18n.ts @@ -53,9 +53,12 @@ const translations = { // 限额相关 hourLimit: (h: number) => `${h}小时限额`, dayLimit: (d: number) => `${d}天限额`, + fiveHourLimit: "5小时限额", + sevenDayLimit: "7天限额", remaining: (p: number) => `剩余 ${p}%`, resetIn: (t: string) => `重置: ${t}后`, limitReached: "⚠️ 已达到限额上限!", + extraUsage: "额外用量", // 通用 account: "Account:", @@ -67,17 +70,23 @@ const translations = { `❌ 无法读取认证文件: ${path}\n错误: ${err}`, apiError: (status: number, text: string) => `OpenAI API 请求失败 (${status}): ${text}`, + anthropicApiError: (status: number, text: string) => + `Anthropic API 请求失败 (${status}): ${text}`, timeoutError: (seconds: number) => `请求超时 (${seconds}秒)`, tokenExpired: "⚠️ OAuth 授权已过期,请在 OpenCode 中使用一次 OpenAI 模型以刷新授权。", + anthropicTokenExpired: + "⚠️ Claude OAuth 授权已过期,请在 OpenCode 中使用一次 Anthropic 模型以刷新授权。", noAccounts: - "未找到任何已配置的账号。\n\n支持的账号类型:\n- OpenAI (Plus/Team/Pro 订阅用户)\n- 智谱 AI (Coding Plan)\n- Z.ai (Coding Plan)\n- Google Cloud (Antigravity)", + "未找到任何已配置的账号。\n\n支持的账号类型:\n- Anthropic (Claude Code / Pro / Max)\n- OpenAI (Plus/Team/Pro 订阅用户)\n- 智谱 AI (Coding Plan)\n- Z.ai (Coding Plan)\n- GitHub Copilot\n- Google Cloud (Antigravity)", queryFailed: "❌ 查询失败的账号:\n", // 平台标题 + anthropicTitle: "## Anthropic 账号额度", openaiTitle: "## OpenAI 账号额度", zhipuTitle: "## 智谱 AI 账号额度", zaiTitle: "## Z.ai 账号额度", + anthropicAccountName: "Claude", // 智谱 AI 相关 zhipuApiError: (status: number, text: string) => @@ -131,9 +140,12 @@ const translations = { // 限额相关 hourLimit: (h: number) => `${h}-hour limit`, dayLimit: (d: number) => `${d}-day limit`, + fiveHourLimit: "5-hour limit", + sevenDayLimit: "7-day limit", remaining: (p: number) => `${p}% remaining`, resetIn: (t: string) => `Resets in: ${t}`, limitReached: "⚠️ Rate limit reached!", + extraUsage: "Extra usage", // 通用 account: "Account:", @@ -145,17 +157,23 @@ const translations = { `❌ Failed to read auth file: ${path}\nError: ${err}`, apiError: (status: number, text: string) => `OpenAI API request failed (${status}): ${text}`, + anthropicApiError: (status: number, text: string) => + `Anthropic API request failed (${status}): ${text}`, timeoutError: (seconds: number) => `Request timeout (${seconds}s)`, tokenExpired: "⚠️ OAuth token expired. Please use an OpenAI model in OpenCode to refresh authorization.", + anthropicTokenExpired: + "⚠️ Claude OAuth token expired. Please use an Anthropic model in OpenCode to refresh authorization.", noAccounts: - "No configured accounts found.\n\nSupported account types:\n- OpenAI (Plus/Team/Pro subscribers)\n- Zhipu AI (Coding Plan)\n- Z.ai (Coding Plan)\n- Google Cloud (Antigravity)", + "No configured accounts found.\n\nSupported account types:\n- Anthropic (Claude Code / Pro / Max)\n- OpenAI (Plus/Team/Pro subscribers)\n- Zhipu AI (Coding Plan)\n- Z.ai (Coding Plan)\n- GitHub Copilot\n- Google Cloud (Antigravity)", queryFailed: "❌ Failed to query accounts:\n", // 平台标题 + anthropicTitle: "## Anthropic Account Quota", openaiTitle: "## OpenAI Account Quota", zhipuTitle: "## Zhipu AI Account Quota", zaiTitle: "## Z.ai Account Quota", + anthropicAccountName: "Claude", // 智谱 AI 相关 zhipuApiError: (status: number, text: string) => diff --git a/plugin/lib/types.ts b/plugin/lib/types.ts index 01543f8..6048a9b 100644 --- a/plugin/lib/types.ts +++ b/plugin/lib/types.ts @@ -32,6 +32,16 @@ export interface OpenAIAuthData { expires?: number; } +/** + * Anthropic OAuth 认证数据 + */ +export interface AnthropicAuthData { + type: string; + access?: string; + refresh?: string; + expires?: number; +} + /** * 智谱 AI API 认证数据 */ @@ -97,6 +107,7 @@ export interface AntigravityAccountsFile { * 完整认证数据结构 */ export interface AuthData { + anthropic?: AnthropicAuthData; openai?: OpenAIAuthData; "zhipuai-coding-plan"?: ZhipuAuthData; "zai-coding-plan"?: ZhipuAuthData; diff --git a/plugin/lib/utils.ts b/plugin/lib/utils.ts index 157874e..a1bf0f7 100644 --- a/plugin/lib/utils.ts +++ b/plugin/lib/utils.ts @@ -28,6 +28,18 @@ export function formatDuration(seconds: number): string { return parts.join(currentLang === "en" ? " " : ""); } +/** + * 将 ISO 时间转换为距离现在的剩余秒数 + */ +export function getResetAfterSeconds(isoTime: string | undefined): number | null { + if (!isoTime) return null; + + const resetDate = new Date(isoTime); + if (Number.isNaN(resetDate.getTime())) return null; + + return Math.max(0, Math.floor((resetDate.getTime() - Date.now()) / 1000)); +} + // ============================================================================ // 进度条 // ============================================================================ diff --git a/plugin/mystatus.ts b/plugin/mystatus.ts index 00eddee..f91a8b6 100644 --- a/plugin/mystatus.ts +++ b/plugin/mystatus.ts @@ -14,6 +14,7 @@ import { join } from "path"; import { t } from "./lib/i18n"; import { type AuthData, type QueryResult } from "./lib/types"; +import { queryAnthropicUsage } from "./lib/anthropic"; import { queryOpenAIUsage } from "./lib/openai"; import { queryZaiUsage, queryZhipuUsage } from "./lib/zhipu"; import { queryGoogleUsage } from "./lib/google"; @@ -28,7 +29,7 @@ export const MyStatusPlugin: Plugin = async () => { tool: { mystatus: tool({ 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.", + "Query account quota usage for all configured AI platforms. Returns remaining quota percentages, usage stats, and reset countdowns with visual progress bars. Currently supports Anthropic (Claude), OpenAI (ChatGPT/Codex), Zhipu AI, Z.ai, Google Antigravity, and GitHub Copilot.", args: {}, async execute() { // 1. 读取 auth.json @@ -46,8 +47,9 @@ export const MyStatusPlugin: Plugin = async () => { } // 2. 并行查询所有平台(Google 不依赖 authData) - const [openaiResult, zhipuResult, zaiResult, googleResult, copilotResult] = + const [anthropicResult, openaiResult, zhipuResult, zaiResult, googleResult, copilotResult] = await Promise.all([ + queryAnthropicUsage(authData.anthropic), queryOpenAIUsage(authData.openai), queryZhipuUsage(authData["zhipuai-coding-plan"]), queryZaiUsage(authData["zai-coding-plan"]), @@ -59,6 +61,8 @@ export const MyStatusPlugin: Plugin = async () => { const results: string[] = []; const errors: string[] = []; + collectResult(anthropicResult, t.anthropicTitle, results, errors); + // 处理 OpenAI 结果 collectResult(openaiResult, t.openaiTitle, results, errors);