w-dispatch-ai 1.0.21 → 1.0.23
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/README.md +627 -569
- package/dist/w-dispatch-ai.umd.js +2 -2
- package/dist/w-dispatch-ai.umd.js.map +1 -1
- package/docs/WDispatchAi.mjs.html +12 -4
- package/docs/adapters.mjs.html +5 -3
- package/docs/budgetFor.mjs.html +2 -2
- package/docs/buildValidator.mjs.html +166 -0
- package/docs/castPintOr.mjs.html +2 -2
- package/docs/dfTimeoutMs.mjs.html +2 -2
- package/docs/dispatchAi.mjs.html +2 -2
- package/docs/dispatchAiFallback.mjs.html +2 -2
- package/docs/dispatchAiWkf.mjs.html +2 -2
- package/docs/dispatchAntigravity.mjs.html +2 -2
- package/docs/dispatchApiOpenaiCompat.mjs.html +4 -68
- package/docs/dispatchApiOpenaiResponses.mjs.html +498 -0
- package/docs/dispatchClaude.mjs.html +2 -2
- package/docs/dispatchCodex.mjs.html +2 -2
- package/docs/dispatchOpencode.mjs.html +2 -2
- package/docs/getCliArgs.mjs.html +2 -2
- package/docs/getErrorResult.mjs.html +2 -2
- package/docs/getErrorType.mjs.html +5 -3
- package/docs/global.html +11790 -3425
- package/docs/index.html +2 -2
- package/docs/quota_dfQuotaTimeoutMs.mjs.html +99 -0
- package/docs/quota_fetchQuotaJson.mjs.html +348 -0
- package/docs/quota_fromCodexUsageHttp.mjs.html +335 -0
- package/docs/quota_getQuotaAntigravity.mjs.html +463 -0
- package/docs/quota_getQuotaClaude.mjs.html +457 -0
- package/docs/quota_getQuotaCodex.mjs.html +515 -0
- package/docs/quota_readJsonOrNull.mjs.html +113 -0
- package/docs/quota_toQuotaLabel.mjs.html +146 -0
- package/docs/quota_toQuotaResult.mjs.html +214 -0
- package/docs/quota_toQuotaScopedLabel.mjs.html +117 -0
- package/docs/quota_toQuotaWindow.mjs.html +239 -0
- package/docs/readEnvFile.mjs.html +2 -2
- package/docs/resolveProviders.mjs.html +3 -3
- package/docs/wkf_callAiWithFallback.mjs.html +2 -2
- package/docs/wkf_createFileStore.mjs.html +2 -2
- package/docs/wkf_createUsageCounter.mjs.html +2 -2
- package/docs/wkf_extractJsonLoose.mjs.html +2 -2
- package/docs/wkf_noSideEffectPrefix.mjs.html +2 -2
- package/docs/wkf_runFanout.mjs.html +2 -2
- package/docs/wkf_runFanoutPipeline.mjs.html +2 -2
- package/docs/wkf_runRolePipeline.mjs.html +2 -2
- package/docs/wkf_salvageTruncatedArray.mjs.html +2 -2
- package/g.mjs +2 -2
- package/package.json +2 -2
- package/src/WDispatchAi.mjs +10 -2
- package/src/adapters.mjs +3 -1
- package/src/buildValidator.mjs +94 -0
- package/src/dispatchApiOpenaiCompat.mjs +2 -66
- package/src/dispatchApiOpenaiResponses.mjs +426 -0
- package/src/getErrorType.mjs +3 -1
- package/src/providers.mjs +303 -213
- package/src/quota/dfQuotaTimeoutMs.mjs +27 -0
- package/src/quota/fetchQuotaJson.mjs +276 -0
- package/src/quota/fromCodexUsageHttp.mjs +263 -0
- package/src/quota/getQuotaAntigravity.mjs +391 -0
- package/src/quota/getQuotaClaude.mjs +385 -0
- package/src/quota/getQuotaCodex.mjs +443 -0
- package/src/quota/readJsonOrNull.mjs +41 -0
- package/src/quota/toQuotaLabel.mjs +74 -0
- package/src/quota/toQuotaResult.mjs +142 -0
- package/src/quota/toQuotaScopedLabel.mjs +45 -0
- package/src/quota/toQuotaWindow.mjs +167 -0
- package/src/resolveProviders.mjs +1 -1
- package/test/tools/fakeCliForTest.mjs +4 -3
- package/test/tools/fakeServerForApiTest.mjs +108 -3
- package/test/tools/fakeServerForQuotaTest.mjs +171 -0
- package/test/unit-WDispatchAi.test.mjs +19 -6
- package/test/unit-adapters.test.mjs +5 -3
- package/test/unit-dispatchAi.test.mjs +1 -1
- package/test/unit-dispatchApiOpenaiResponses.test.mjs +198 -0
- package/test/unit-fetchQuotaJson.test.mjs +70 -0
- package/test/unit-fromCodexUsageHttp.test.mjs +76 -0
- package/test/unit-getQuotaAntigravity.test.mjs +123 -0
- package/test/unit-getQuotaClaude.test.mjs +137 -0
- package/test/unit-getQuotaCodex.test.mjs +191 -0
- package/test/unit-readJsonOrNull.test.mjs +44 -0
- package/test/unit-toQuotaLabel.test.mjs +43 -0
- package/test/unit-toQuotaResult.test.mjs +53 -0
- package/test/unit-toQuotaWindow.test.mjs +73 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<title>quota/getQuotaClaude.mjs - Documentation</title>
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
<script src="scripts/prettify/prettify.js"></script>
|
|
10
|
+
<script src="scripts/prettify/lang-css.js"></script>
|
|
11
|
+
<!--[if lt IE 9]>
|
|
12
|
+
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
|
|
13
|
+
<![endif]-->
|
|
14
|
+
<link type="text/css" rel="stylesheet" href="styles/prettify.css">
|
|
15
|
+
<link type="text/css" rel="stylesheet" href="styles/jsdoc.css">
|
|
16
|
+
<script src="scripts/nav.js" defer></script>
|
|
17
|
+
|
|
18
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
19
|
+
</head>
|
|
20
|
+
<body>
|
|
21
|
+
|
|
22
|
+
<input type="checkbox" id="nav-trigger" class="nav-trigger" />
|
|
23
|
+
<label for="nav-trigger" class="navicon-button x">
|
|
24
|
+
<div class="navicon"></div>
|
|
25
|
+
</label>
|
|
26
|
+
|
|
27
|
+
<label for="nav-trigger" class="overlay"></label>
|
|
28
|
+
|
|
29
|
+
<nav >
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
<h2><a href="index.html">Home</a></h2><h3>Global</h3><ul><li><a href="global.html#NO_SIDE_EFFECT">NO_SIDE_EFFECT</a></li><li><a href="global.html#WDispatchAi">WDispatchAi</a></li><li><a href="global.html#adapters">adapters</a></li><li><a href="global.html#agyPrint">agyPrint</a></li><li><a href="global.html#attachErrorType">attachErrorType</a></li><li><a href="global.html#budgetFor">budgetFor</a></li><li><a href="global.html#buildChain">buildChain</a></li><li><a href="global.html#buildValidator">buildValidator</a></li><li><a href="global.html#callAiWithFallback">callAiWithFallback</a></li><li><a href="global.html#callOnce">callOnce</a></li><li><a href="global.html#castPintOr">castPintOr</a></li><li><a href="global.html#collectAdditional">collectAdditional</a></li><li><a href="global.html#createFileStore">createFileStore</a></li><li><a href="global.html#createUsageCounter">createUsageCounter</a></li><li><a href="global.html#decodeJwtPayload">decodeJwtPayload</a></li><li><a href="global.html#defaultIntegratePrompt">defaultIntegratePrompt</a></li><li><a href="global.html#dfQuotaTimeoutMs">dfQuotaTimeoutMs</a></li><li><a href="global.html#dfTimeoutMs">dfTimeoutMs</a></li><li><a href="global.html#directWindow">directWindow</a></li><li><a href="global.html#dispatchAi">dispatchAi</a></li><li><a href="global.html#dispatchAiFallback">dispatchAiFallback</a></li><li><a href="global.html#dispatchAiWkf">dispatchAiWkf</a></li><li><a href="global.html#dispatchAntigravity">dispatchAntigravity</a></li><li><a href="global.html#dispatchApiOpenaiCompat">dispatchApiOpenaiCompat</a></li><li><a href="global.html#dispatchApiOpenaiResponses">dispatchApiOpenaiResponses</a></li><li><a href="global.html#dispatchClaude">dispatchClaude</a></li><li><a href="global.html#dispatchCodex">dispatchCodex</a></li><li><a href="global.html#dispatchOpencode">dispatchOpencode</a></li><li><a href="global.html#extractJsonLoose">extractJsonLoose</a></li><li><a href="global.html#extractOutputText">extractOutputText</a></li><li><a href="global.html#fetchQuotaJson">fetchQuotaJson</a></li><li><a href="global.html#firstStr">firstStr</a></li><li><a href="global.html#fromBucket">fromBucket</a></li><li><a href="global.html#fromCodexUsageHttp">fromCodexUsageHttp</a></li><li><a href="global.html#fromLimitItem">fromLimitItem</a></li><li><a href="global.html#fromRpcWindow">fromRpcWindow</a></li><li><a href="global.html#fromTopField">fromTopField</a></li><li><a href="global.html#fromWindow">fromWindow</a></li><li><a href="global.html#getCliArgs">getCliArgs</a></li><li><a href="global.html#getErrorResult">getErrorResult</a></li><li><a href="global.html#getErrorType">getErrorType</a></li><li><a href="global.html#getQuotaAntigravity">getQuotaAntigravity</a></li><li><a href="global.html#getQuotaClaude">getQuotaClaude</a></li><li><a href="global.html#getQuotaCodex">getQuotaCodex</a></li><li><a href="global.html#initState">initState</a></li><li><a href="global.html#isKeyIndependentFail">isKeyIndependentFail</a></li><li><a href="global.html#pickWindow">pickWindow</a></li><li><a href="global.html#readBodyCapped">readBodyCapped</a></li><li><a href="global.html#readEnvFile">readEnvFile</a></li><li><a href="global.html#readJsonOrNull">readJsonOrNull</a></li><li><a href="global.html#redactText">redactText</a></li><li><a href="global.html#reorderByCooling">reorderByCooling</a></li><li><a href="global.html#resolveProviders">resolveProviders</a></li><li><a href="global.html#runFanout">runFanout</a></li><li><a href="global.html#runFanoutPipeline">runFanoutPipeline</a></li><li><a href="global.html#runRolePipeline">runRolePipeline</a></li><li><a href="global.html#salvageTruncatedArray">salvageTruncatedArray</a></li><li><a href="global.html#toQuotaLabel">toQuotaLabel</a></li><li><a href="global.html#toQuotaResult">toQuotaResult</a></li><li><a href="global.html#toQuotaScopedLabel">toQuotaScopedLabel</a></li><li><a href="global.html#toQuotaWindow">toQuotaWindow</a></li><li><a href="global.html#viaAppServer">viaAppServer</a></li><li><a href="global.html#viaHttp">viaHttp</a></li><li><a href="global.html#windowToSeconds">windowToSeconds</a></li></ul>
|
|
33
|
+
|
|
34
|
+
</nav>
|
|
35
|
+
|
|
36
|
+
<div id="main">
|
|
37
|
+
|
|
38
|
+
<h1 class="page-title">quota/getQuotaClaude.mjs</h1>
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
<section>
|
|
47
|
+
<article>
|
|
48
|
+
<pre class="prettyprint source linenums"><code>import os from 'os'
|
|
49
|
+
import path from 'path'
|
|
50
|
+
import get from 'lodash-es/get.js'
|
|
51
|
+
import isobj from 'wsemi/src/isobj.mjs'
|
|
52
|
+
import isestr from 'wsemi/src/isestr.mjs'
|
|
53
|
+
import isearr from 'wsemi/src/isearr.mjs'
|
|
54
|
+
import isnum from 'wsemi/src/isnum.mjs'
|
|
55
|
+
import castPintOr from '../castPintOr.mjs'
|
|
56
|
+
import dfQuotaTimeoutMs from './dfQuotaTimeoutMs.mjs'
|
|
57
|
+
import readJsonOrNull from './readJsonOrNull.mjs'
|
|
58
|
+
import fetchQuotaJson from './fetchQuotaJson.mjs'
|
|
59
|
+
import toQuotaScopedLabel from './toQuotaScopedLabel.mjs'
|
|
60
|
+
import toQuotaWindow from './toQuotaWindow.mjs'
|
|
61
|
+
import toQuotaResult from './toQuotaResult.mjs'
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
// getQuotaClaude.mjs — 查詢Claude(Claude Code訂閱)帳號之當前額度
|
|
65
|
+
//
|
|
66
|
+
// 【資料來源, 2026-09-05於本機實測】
|
|
67
|
+
// 設定目錄: CLAUDE_CONFIG_DIR(未設則~/.claude); 實測設此變數指向空目錄後claude auth status回
|
|
68
|
+
// loggedIn:false且該目錄長出.claude.json, 即憑證檔與帳號檔皆隨此變數搬遷——它正是官方的
|
|
69
|
+
// 多帳號機制(anthropics/claude-code issue #33430), 也是本套件email比對設計存在的理由。
|
|
70
|
+
// 憑證: <configDir>/.credentials.json 之 claudeAiOauth.accessToken, 這是唯一權杖來源。
|
|
71
|
+
// 環境變數CLAUDE_CODE_OAUTH_TOKEN(claude setup-token產生之一年期token)【刻意不作為來源】: 2026-09-05實測
|
|
72
|
+
// 以setup-token打usage端點回403 {error_code:'oauth_scope_insufficient', required_scopes:['user:profile']},
|
|
73
|
+
// 即setup-token僅有user:inference(官方文件「can only make model requests」), 查額度必須用/login存檔之
|
|
74
|
+
// 權杖(scopes含user:profile)。故該變數只在憑證檔不存在時用於錯誤提示, 提醒使用者它不能拿來查額度。
|
|
75
|
+
// 帳號: <configDir>/.claude.json(設變數時)或~/.claude.json 之 oauthAccount.emailAddress。
|
|
76
|
+
// 額度: GET https://api.anthropic.com/api/oauth/usage(即/usage指令之同一來源; 同類工具
|
|
77
|
+
// token-burn、wakamex/ccusage、pinkpixel quota皆用此端點)。
|
|
78
|
+
//
|
|
79
|
+
// 【為何不自行刷新權杖(刻意)】Anthropic之refresh token每次使用即輪替並作廢前一枚;
|
|
80
|
+
// 本函數若刷新而不寫回, Claude Code存檔的refresh token立即失效, 使用者被迫重新登入;
|
|
81
|
+
// 寫回則與Claude Code競爭同一檔(anthropics/claude-code #54443即此類race)。
|
|
82
|
+
// 同類工具token-burn與ccusage以「原子寫回+身分守衛+重讀」做到了, 但監控程式不擁有憑證
|
|
83
|
+
// 生命週期是更穩的邊界, 故本函數維持唯讀。代價: access token實測壽命8小時, Claude Code閒置
|
|
84
|
+
// 逾8小時後查詢會401——此時正確指引是「執行一次claude讓它自行刷新」; 不需重新登入,
|
|
85
|
+
// 因為存檔之refresh token仍有效(本機實測其期限約登入後30天), 只是要由Claude Code去用它。
|
|
86
|
+
//
|
|
87
|
+
// 【為何windows優先取limits[]】回應同時存在頂層five_hour/seven_day(舊欄位)與limits[](新結構),
|
|
88
|
+
// 後者多出weekly_scoped(帶模型別, 實測Fable 25%), 是實際會先觸頂的窗口; 故優先limits[],
|
|
89
|
+
// 缺漏才回退頂層。limits[]實測欄位: kind/group/percent/severity/resets_at/scope{model{display_name},surface}/is_active。
|
|
90
|
+
//
|
|
91
|
+
// 【已知限制】macOS之Claude Code將憑證存於Keychain, 本函數於該平台會回notfound(未實測故不臆造);
|
|
92
|
+
// 該平台亦無替代來源(setup-token不能查額度, 見上方實測)。
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
//額度查詢端點與帳號端點
|
|
96
|
+
let URL_USAGE = 'https://api.anthropic.com/api/oauth/usage'
|
|
97
|
+
let URL_PROFILE = 'https://api.anthropic.com/api/oauth/profile'
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
//端點所需之beta旗標與預設UA(未帶claude-code UA會落入嚴苛之429請求池, 見claude-code #30930)
|
|
101
|
+
let BETA_HEADER = 'oauth-2025-04-20'
|
|
102
|
+
let DEFAULT_UA = 'claude-code/2.1.259'
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
//各窗口群組之長度秒數。端點未回傳窗口長度, 此為Anthropic公告之定義(session=5小時、weekly=7天),
|
|
106
|
+
//屬推導值; 供應商若調整而此處未更新, 僅label與windowSeconds失準, usedPercent與resets_at仍為實值
|
|
107
|
+
let SEC_SESSION = 18000
|
|
108
|
+
let SEC_WEEKLY = 604800
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
//頂層舊欄位回退清單: [欄位鍵, 窗口秒數, 範圍]
|
|
112
|
+
let TOP_FIELDS = [
|
|
113
|
+
['five_hour', SEC_SESSION, ''],
|
|
114
|
+
['seven_day', SEC_WEEKLY, ''],
|
|
115
|
+
['seven_day_opus', SEC_WEEKLY, 'Opus'],
|
|
116
|
+
['seven_day_sonnet', SEC_WEEKLY, 'Sonnet'],
|
|
117
|
+
['seven_day_cowork', SEC_WEEKLY, 'Cowork'],
|
|
118
|
+
['seven_day_oauth_apps', SEC_WEEKLY, 'OAuth apps'],
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
//代表Claude Code以非訂閱模式運作之環境變數; 任一有值即無訂閱額度窗口可查
|
|
123
|
+
let ENV_NON_SUBSCRIPTION = ['ANTHROPIC_API_KEY', 'CLAUDE_CODE_USE_BEDROCK', 'CLAUDE_CODE_USE_VERTEX']
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 將limits[]之單筆正規化為統一窗口物件
|
|
128
|
+
*
|
|
129
|
+
* @param {Object} it 輸入limits[]之單筆物件
|
|
130
|
+
* @returns {Object} 回傳統一窗口物件
|
|
131
|
+
*/
|
|
132
|
+
function fromLimitItem(it) {
|
|
133
|
+
|
|
134
|
+
//group, 決定窗口長度
|
|
135
|
+
let group = get(it, 'group', '')
|
|
136
|
+
let windowSeconds = null
|
|
137
|
+
if (group === 'session') {
|
|
138
|
+
windowSeconds = SEC_SESSION
|
|
139
|
+
}
|
|
140
|
+
else if (group === 'weekly') {
|
|
141
|
+
windowSeconds = SEC_WEEKLY
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
//scope, 模型別與介面別皆可能出現(實測scope:{model:{display_name:'Fable'},surface:null}),
|
|
145
|
+
//兩者並列以斜線相接, 令surface-scoped窗口不會與全域窗口同標籤而無從分辨
|
|
146
|
+
let parts = []
|
|
147
|
+
let model = get(it, 'scope.model.display_name', '')
|
|
148
|
+
if (isestr(model)) {
|
|
149
|
+
parts.push(model)
|
|
150
|
+
}
|
|
151
|
+
let surface = get(it, 'scope.surface', '')
|
|
152
|
+
if (isestr(surface)) {
|
|
153
|
+
parts.push(surface)
|
|
154
|
+
}
|
|
155
|
+
let scope = parts.join('/')
|
|
156
|
+
|
|
157
|
+
return toQuotaWindow({
|
|
158
|
+
key: get(it, 'kind', ''),
|
|
159
|
+
label: toQuotaScopedLabel(windowSeconds, scope),
|
|
160
|
+
windowSeconds,
|
|
161
|
+
usedPercent: get(it, 'percent', null),
|
|
162
|
+
resetAt: get(it, 'resets_at', ''),
|
|
163
|
+
scope,
|
|
164
|
+
active: get(it, 'is_active', false),
|
|
165
|
+
severity: get(it, 'severity', ''),
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* 將頂層舊欄位正規化為統一窗口物件
|
|
172
|
+
*
|
|
173
|
+
* @param {Object} data 輸入端點回應物件
|
|
174
|
+
* @param {String} key 輸入欄位鍵字串
|
|
175
|
+
* @param {Number} windowSeconds 輸入窗口長度秒數
|
|
176
|
+
* @param {String} scope 輸入適用範圍字串
|
|
177
|
+
* @returns {Object|null} 回傳統一窗口物件, 該欄位不存在或為null回傳null
|
|
178
|
+
*/
|
|
179
|
+
function fromTopField(data, key, windowSeconds, scope) {
|
|
180
|
+
let o = get(data, key, null)
|
|
181
|
+
if (o === null || o === undefined) {
|
|
182
|
+
return null
|
|
183
|
+
}
|
|
184
|
+
return toQuotaWindow({
|
|
185
|
+
key,
|
|
186
|
+
label: toQuotaScopedLabel(windowSeconds, scope),
|
|
187
|
+
windowSeconds,
|
|
188
|
+
usedPercent: get(o, 'utilization', null),
|
|
189
|
+
resetAt: get(o, 'resets_at', ''),
|
|
190
|
+
scope,
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* 查詢Claude(Claude Code訂閱)帳號之當前額度
|
|
197
|
+
*
|
|
198
|
+
* 額度綁定本機Claude Code之登入憑證,無「給email查任意帳號」之公開介面,
|
|
199
|
+
* 故email參數之作用為比對——查出本機實際登入者後與其核對,
|
|
200
|
+
* 不符時ok為false且matched為false,並於error載明本機實際登入之帳號;
|
|
201
|
+
* 未給email時不比對,直接回報本機當前帳號之額度。
|
|
202
|
+
* 本函數唯讀憑證、不自行刷新權杖(理由見檔頭);不會reject,一律以結果物件之ok與error欄位回報成敗
|
|
203
|
+
*
|
|
204
|
+
* @param {String} [email=''] 輸入欲查詢之帳號email字串,預設''代表不比對而直接回報本機當前帳號
|
|
205
|
+
* @param {Object} [opt={}] 輸入設定物件,預設{}
|
|
206
|
+
* @param {String} [opt.configDir] 輸入Claude設定目錄字串,預設取環境變數CLAUDE_CONFIG_DIR,未設則<homeDir>/.claude
|
|
207
|
+
* @param {String} [opt.homeDir=os.homedir()] 輸入家目錄字串,僅於未指定configDir且未設環境變數時使用,供測試指向替身目錄
|
|
208
|
+
* @param {String} [opt.userAgent='claude-code/2.1.259'] 輸入User-Agent字串,須為claude-code/<版本>形式否則端點將以嚴苛頻率限制回應429
|
|
209
|
+
* @param {Boolean} [opt.profileFallback=true] 輸入本機帳號檔無email時是否改打/api/oauth/profile取得布林值,預設true
|
|
210
|
+
* @param {String} [opt.usageUrl='https://api.anthropic.com/api/oauth/usage'] 輸入額度端點網址字串,供測試指向假伺服器或經企業代理,預設官方端點
|
|
211
|
+
* @param {String} [opt.profileUrl='https://api.anthropic.com/api/oauth/profile'] 輸入帳號端點網址字串,用途同usageUrl,預設官方端點
|
|
212
|
+
* @param {Object} [opt.env=process.env] 輸入環境變數來源物件(讀CLAUDE_CONFIG_DIR、非訂閱模式判定用之ANTHROPIC_API_KEY等;CLAUDE_CODE_OAUTH_TOKEN不作為權杖來源,僅於無憑證時用於錯誤提示),供測試隔離本機環境,預設process.env
|
|
213
|
+
* @param {Number} [opt.timeoutMs=20000] 輸入單次請求之逾時毫秒正整數,預設20000
|
|
214
|
+
* @returns {Promise} 回傳Promise,resolve回傳結果物件,內含ok、provider('claude')、email(本機實際登入帳號)、matched、plan(方案別,例如'max')、planTier(級距,例如'default_claude_max_20x')、source('anthropic-oauth-usage-api')、windows(額度窗口陣列,含5小時、7天、7天模型別)、credits(額外用量與花費資訊)、raw(原始回應)、error、errorType、durationMs,本函數不會reject
|
|
215
|
+
* @example
|
|
216
|
+
* //need claude code logged in
|
|
217
|
+
*
|
|
218
|
+
* import getQuotaClaude from './src/quota/getQuotaClaude.mjs'
|
|
219
|
+
*
|
|
220
|
+
* let test = async () => {
|
|
221
|
+
* let r = await getQuotaClaude('firsemisphere2@gmail.com')
|
|
222
|
+
* console.log(r.ok, r.plan)
|
|
223
|
+
* // => true max
|
|
224
|
+
* console.log(r.windows[0].label, r.windows[0].usedPercent)
|
|
225
|
+
* // => 5小時 11 (百分比為查詢當下之即時值, 每次不同)
|
|
226
|
+
* }
|
|
227
|
+
* test()
|
|
228
|
+
*
|
|
229
|
+
*/
|
|
230
|
+
async function getQuotaClaude(email = '', opt = {}) {
|
|
231
|
+
|
|
232
|
+
let t0 = Date.now()
|
|
233
|
+
let source = 'anthropic-oauth-usage-api'
|
|
234
|
+
|
|
235
|
+
//emailWant, 非有效字串視為不比對
|
|
236
|
+
let emailWant = isestr(email) ? email : ''
|
|
237
|
+
|
|
238
|
+
//env, 環境變數來源可注入(測試以替身隔離本機環境; 亦供多帳號部署逐呼叫給定), 預設process.env
|
|
239
|
+
let env = get(opt, 'env', null)
|
|
240
|
+
if (!isobj(env)) {
|
|
241
|
+
env = process.env
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
//homeDir
|
|
245
|
+
let homeDir = get(opt, 'homeDir', null)
|
|
246
|
+
if (!isestr(homeDir)) {
|
|
247
|
+
homeDir = os.homedir()
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
//configDir, 依序取opt、CLAUDE_CONFIG_DIR、<homeDir>/.claude; 前二者代表使用者刻意搬遷,
|
|
251
|
+
//此時.claude.json亦在該目錄(實測), 否則在家目錄
|
|
252
|
+
let configDir = get(opt, 'configDir', null)
|
|
253
|
+
let relocated = true
|
|
254
|
+
if (!isestr(configDir)) {
|
|
255
|
+
configDir = get(env, 'CLAUDE_CONFIG_DIR', '')
|
|
256
|
+
if (!isestr(configDir)) {
|
|
257
|
+
configDir = path.join(homeDir, '.claude')
|
|
258
|
+
relocated = false
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
let fpCred = path.join(configDir, '.credentials.json')
|
|
262
|
+
let fpConf = relocated ? path.join(configDir, '.claude.json') : path.join(homeDir, '.claude.json')
|
|
263
|
+
|
|
264
|
+
//timeoutMs
|
|
265
|
+
let timeoutMs = castPintOr(get(opt, 'timeoutMs', null), dfQuotaTimeoutMs)
|
|
266
|
+
|
|
267
|
+
//userAgent
|
|
268
|
+
let userAgent = get(opt, 'userAgent', null)
|
|
269
|
+
if (!isestr(userAgent)) {
|
|
270
|
+
userAgent = DEFAULT_UA
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
//profileFallback
|
|
274
|
+
let profileFallback = get(opt, 'profileFallback', true) !== false
|
|
275
|
+
|
|
276
|
+
//usageUrl與profileUrl, 可覆寫(測試指向假伺服器、或經企業代理), 預設官方端點
|
|
277
|
+
let usageUrl = get(opt, 'usageUrl', null)
|
|
278
|
+
if (!isestr(usageUrl)) {
|
|
279
|
+
usageUrl = URL_USAGE
|
|
280
|
+
}
|
|
281
|
+
let profileUrl = get(opt, 'profileUrl', null)
|
|
282
|
+
if (!isestr(profileUrl)) {
|
|
283
|
+
profileUrl = URL_PROFILE
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
//cred與conf
|
|
287
|
+
let cred = readJsonOrNull(fpCred)
|
|
288
|
+
let conf = readJsonOrNull(fpConf)
|
|
289
|
+
|
|
290
|
+
//token, 僅取憑證檔(登入所存之access token, 帶user:profile scope); 環境變數CLAUDE_CODE_OAUTH_TOKEN
|
|
291
|
+
//不作為來源(setup-token缺usage端點要求之scope, 見檔頭實測), 僅於下方無憑證時用來給更準的提示
|
|
292
|
+
let token = get(cred, 'claudeAiOauth.accessToken', '')
|
|
293
|
+
|
|
294
|
+
//emailHave, 僅帳號檔載有email, 額度端點本身不回傳帳號
|
|
295
|
+
let emailHave = get(conf, 'oauthAccount.emailAddress', '')
|
|
296
|
+
if (!isestr(emailHave)) {
|
|
297
|
+
emailHave = ''
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
//plan與planTier, 憑證檔為先(登入當下寫入), 帳號檔次之
|
|
301
|
+
let plan = get(cred, 'claudeAiOauth.subscriptionType', '')
|
|
302
|
+
if (!isestr(plan)) {
|
|
303
|
+
plan = get(conf, 'oauthAccount.organizationType', '')
|
|
304
|
+
if (!isestr(plan)) {
|
|
305
|
+
plan = ''
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
let planTier = get(cred, 'claudeAiOauth.rateLimitTier', '')
|
|
309
|
+
if (!isestr(planTier)) {
|
|
310
|
+
planTier = get(conf, 'oauthAccount.organizationRateLimitTier', '')
|
|
311
|
+
if (!isestr(planTier)) {
|
|
312
|
+
planTier = ''
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
//fin, 統一收尾
|
|
317
|
+
let fin = (o) => {
|
|
318
|
+
return toQuotaResult('claude', {
|
|
319
|
+
email: emailHave,
|
|
320
|
+
emailWant,
|
|
321
|
+
plan,
|
|
322
|
+
planTier,
|
|
323
|
+
source,
|
|
324
|
+
durationMs: Date.now() - t0,
|
|
325
|
+
...o,
|
|
326
|
+
})
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
//check token
|
|
330
|
+
if (!isestr(token)) {
|
|
331
|
+
|
|
332
|
+
//unsupported, 以API key或雲端閘道模式運作者按用量計費, 無訂閱額度窗口, 指引其登入只是誤導
|
|
333
|
+
let envs = ENV_NON_SUBSCRIPTION.filter((k) => isestr(get(env, k, '')))
|
|
334
|
+
if (envs.length > 0) {
|
|
335
|
+
return fin({
|
|
336
|
+
error: `Claude Code is running in non-subscription mode (env ${envs.join(', ')} is set); this mode is billed by usage and has no 5-hour/7-day subscription quota windows`,
|
|
337
|
+
errorType: 'unsupported',
|
|
338
|
+
})
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
//notfound; 環境變數有setup-token時特別說明它不能拿來查額度(2026-09-05實測403 oauth_scope_insufficient), 免得使用者以為設了就該能查
|
|
342
|
+
let noteEnv = isestr(get(env, 'CLAUDE_CODE_OAUTH_TOKEN', ''))
|
|
343
|
+
? ' Env CLAUDE_CODE_OAUTH_TOKEN is set but is ignored here: a setup-token lacks the user:profile scope the usage endpoint requires, so only a stored login can be queried.'
|
|
344
|
+
: ''
|
|
345
|
+
return fin({
|
|
346
|
+
error: `Claude Code credential not found (${fpCred}); run [claude auth login] first (with the same CLAUDE_CONFIG_DIR if this config dir is relocated). On macOS the credential lives in Keychain instead of this file, which this function cannot read.${noteEnv}`,
|
|
347
|
+
errorType: 'notfound',
|
|
348
|
+
})
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
//headers
|
|
352
|
+
let headers = {
|
|
353
|
+
'Authorization': `Bearer ${token}`,
|
|
354
|
+
'anthropic-beta': BETA_HEADER,
|
|
355
|
+
'User-Agent': userAgent,
|
|
356
|
+
'Content-Type': 'application/json',
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
//fetch usage, 錯誤訊息中之權杖一律遮蔽
|
|
360
|
+
let r = await fetchQuotaJson(usageUrl, { headers, timeoutMs, redact: [token] })
|
|
361
|
+
if (!r.ok) {
|
|
362
|
+
|
|
363
|
+
let msg = r.error
|
|
364
|
+
|
|
365
|
+
//auth, 最常見成因是access token自然到期(實測壽命8小時); Claude Code每次執行皆會自行刷新,
|
|
366
|
+
//正確指引是執行一次claude; 不需重新登入(存檔之refresh token仍有效, 只是要由Claude Code去用它)
|
|
367
|
+
if (r.errorType === 'auth') {
|
|
368
|
+
let expiresAt = get(cred, 'claudeAiOauth.expiresAt', null)
|
|
369
|
+
let hint = 'run any claude command (just open claude) so Claude Code refreshes the token itself, then retry; re-login is not needed, the stored refresh token is still valid and only has to be used by Claude Code'
|
|
370
|
+
let when = isnum(expiresAt) ? `(local recorded expiry: ${new Date(expiresAt).toISOString()})` : ''
|
|
371
|
+
msg = `${msg} ${when} → ${hint}`
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
//forbidden, 組織層級禁用OAuth用量查詢時端點回403並帶此錯誤碼(見wakamex/ccusage)
|
|
375
|
+
if (r.errorType === 'forbidden' && /oauth_not_allowed_for_organization/.test(msg)) {
|
|
376
|
+
msg = `this account's organization disallows OAuth usage queries (oauth_not_allowed_for_organization); subscription quota must be viewed by an org admin in the console: ${msg}`
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return fin({ error: msg, errorType: r.errorType })
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
let data = r.data
|
|
383
|
+
|
|
384
|
+
//emailHave, 帳號檔無email(例如僅以環境變數提供權杖)時改打profile端點; 形狀採寬容取法
|
|
385
|
+
if (emailHave === '' && profileFallback) {
|
|
386
|
+
let rp = await fetchQuotaJson(profileUrl, { headers, timeoutMs, redact: [token] })
|
|
387
|
+
if (rp.ok) {
|
|
388
|
+
for (let k of ['account.email', 'account.email_address', 'email', 'email_address']) {
|
|
389
|
+
let v = get(rp.data, k, '')
|
|
390
|
+
if (isestr(v)) {
|
|
391
|
+
emailHave = v
|
|
392
|
+
break
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
//windows, 優先取新版limits[](含模型別與介面別週限額), 缺漏才回退頂層舊欄位
|
|
399
|
+
let windows = []
|
|
400
|
+
let limits = get(data, 'limits', null)
|
|
401
|
+
if (isearr(limits)) {
|
|
402
|
+
windows = limits.map(fromLimitItem)
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
windows = TOP_FIELDS
|
|
406
|
+
.map(([key, sec, scope]) => fromTopField(data, key, sec, scope))
|
|
407
|
+
.filter((v) => v !== null)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
//credits, 額外用量(超出方案額度後之付費用量)與已花費金額
|
|
411
|
+
let credits = {
|
|
412
|
+
extraUsageEnabled: get(data, 'extra_usage.is_enabled', false),
|
|
413
|
+
monthlyLimit: get(data, 'extra_usage.monthly_limit', null),
|
|
414
|
+
usedCredits: get(data, 'extra_usage.used_credits', null),
|
|
415
|
+
utilization: get(data, 'extra_usage.utilization', null),
|
|
416
|
+
spendUsedMinor: get(data, 'spend.used.amount_minor', null),
|
|
417
|
+
spendCurrency: get(data, 'spend.used.currency', ''),
|
|
418
|
+
spendPercent: get(data, 'spend.percent', null),
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return fin({
|
|
422
|
+
windows,
|
|
423
|
+
credits,
|
|
424
|
+
raw: {
|
|
425
|
+
configDir,
|
|
426
|
+
usage: data,
|
|
427
|
+
},
|
|
428
|
+
})
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
export default getQuotaClaude
|
|
433
|
+
</code></pre>
|
|
434
|
+
</article>
|
|
435
|
+
</section>
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
</div>
|
|
443
|
+
|
|
444
|
+
<br class="clear">
|
|
445
|
+
|
|
446
|
+
<footer>
|
|
447
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sat Sep 05 2026 22:03:08 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
448
|
+
</footer>
|
|
449
|
+
|
|
450
|
+
<script>prettyPrint();</script>
|
|
451
|
+
<script src="scripts/polyfill.js"></script>
|
|
452
|
+
<script src="scripts/linenumber.js"></script>
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
</body>
|
|
457
|
+
</html>
|