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,463 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<title>quota/getQuotaAntigravity.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/getQuotaAntigravity.mjs</h1>
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
<section>
|
|
47
|
+
<article>
|
|
48
|
+
<pre class="prettyprint source linenums"><code>import fs from 'fs'
|
|
49
|
+
import os from 'os'
|
|
50
|
+
import path from 'path'
|
|
51
|
+
import crypto from 'crypto'
|
|
52
|
+
import get from 'lodash-es/get.js'
|
|
53
|
+
import isestr from 'wsemi/src/isestr.mjs'
|
|
54
|
+
import isearr from 'wsemi/src/isearr.mjs'
|
|
55
|
+
import isbol from 'wsemi/src/isbol.mjs'
|
|
56
|
+
import isnum from 'wsemi/src/isnum.mjs'
|
|
57
|
+
import cdbl from 'wsemi/src/cdbl.mjs'
|
|
58
|
+
import execCli from 'wsemi/src/execCli.mjs'
|
|
59
|
+
import strCompareForVersion from 'wsemi/src/strCompareForVersion.mjs'
|
|
60
|
+
import castPintOr from '../castPintOr.mjs'
|
|
61
|
+
import toQuotaLabel from './toQuotaLabel.mjs'
|
|
62
|
+
import toQuotaWindow from './toQuotaWindow.mjs'
|
|
63
|
+
import toQuotaResult from './toQuotaResult.mjs'
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
// getQuotaAntigravity.mjs — 查詢Antigravity(Google側, agy CLI)帳號之當前額度
|
|
67
|
+
//
|
|
68
|
+
// 【資料來源: agy自身之print模式(官方、無頭、不碰憑證), 2026-09-05本機實測(agy 1.1.27)】
|
|
69
|
+
// `agy -p "/usage" --output-format json` 回 {status:'SUCCESS', num_turns:0, usage:{全零},
|
|
70
|
+
// command:{name:'usage', data:{groups:[{name:'Gemini Models', buckets:[
|
|
71
|
+
// {id:'gemini-weekly', window:'weekly', remaining_fraction, reset_time, description}, {id:'gemini-5h', window:'5h', ...}]},
|
|
72
|
+
// {name:'Claude and GPT models', buckets:[{id:'3p-weekly',...},{id:'3p-5h',...}]}]}}}
|
|
73
|
+
// agy CHANGELOG 1.1.11原文: 「-p "/usage", /quota, /credits ... emit a structured payload under
|
|
74
|
+
// --output-format json ... without starting an agent turn, spending quota, or leaving a conversation behind」。
|
|
75
|
+
// 帳號email: 同一次spawn加`--log-file <暫存檔>`, log內有`applyAuthResult: email=..., authMethod=consumer`。
|
|
76
|
+
// 耗時實測5~8秒(agy啟動、認證、loadCodeAssist與fetchAvailableModels兩個後端往返), 故預設逾時較寬。
|
|
77
|
+
//
|
|
78
|
+
// 【為何不走Gemini CLI憑證或Code Assist端點】Google已於2026-06-18終止Gemini CLI對個人層之服務;
|
|
79
|
+
// ~/.gemini/google_accounts.json與oauth_creds.json為舊Gemini CLI所留, 實測其帳號(firsemisphere6)
|
|
80
|
+
// 與agy實際登入帳號(firsemisphere)不同, 且打Code Assist端點回403; 同類工具為此各自借token
|
|
81
|
+
// (讀IDE之state.vscdb、讀Windows認證管理員、掃agy二進位取client secret、自建OAuth登入),
|
|
82
|
+
// 全屬未公開之內部格式與灰色地帶。agy既已提供官方無頭介面, 上述路徑一概不做。
|
|
83
|
+
//
|
|
84
|
+
// 【版本閘門】1.1.11之前的agy會把「/usage」當一般prompt起一個agent turn(耗額度、留對話),
|
|
85
|
+
// 故先以`agy --version`把關(實測<1秒), 低於門檻即回unsupported而非冒險執行。
|
|
86
|
+
//
|
|
87
|
+
// 【Git Bash陷阱(僅影響手動測試)】MSYS會把以斜線開頭之參數「/usage」轉譯成Windows路徑
|
|
88
|
+
// (C:/Program Files/Git/usage), agy收到路徑字串就當成prompt執行; Node之spawn無此轉譯,
|
|
89
|
+
// 本函數不受影響, 但在Git Bash手動驗證須加MSYS_NO_PATHCONV=1。
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
//預設值
|
|
93
|
+
let DEFAULT_EXE = 'agy'
|
|
94
|
+
let DEFAULT_TIMEOUT_MS = 60000
|
|
95
|
+
let MIN_VERSION = '1.1.11'
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
//各窗口字串對應之秒數; 未列者以正則解析Nh/Nd, 皆不成則null(標籤退回桶名)
|
|
99
|
+
let WINDOW_SECONDS = {
|
|
100
|
+
'5h': 18000,
|
|
101
|
+
'five_hour': 18000,
|
|
102
|
+
'five-hour': 18000,
|
|
103
|
+
'weekly': 604800,
|
|
104
|
+
'week': 604800,
|
|
105
|
+
'daily': 86400,
|
|
106
|
+
'day': 86400,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 由桶之window字串推得窗口秒數
|
|
112
|
+
*
|
|
113
|
+
* @param {String} w 輸入window字串, 例如'5h'、'weekly'
|
|
114
|
+
* @returns {Number|null} 回傳秒數, 無法推得回傳null
|
|
115
|
+
*/
|
|
116
|
+
function windowToSeconds(w) {
|
|
117
|
+
if (!isestr(w)) {
|
|
118
|
+
return null
|
|
119
|
+
}
|
|
120
|
+
let k = w.trim().toLowerCase()
|
|
121
|
+
if (WINDOW_SECONDS[k] !== undefined) {
|
|
122
|
+
return WINDOW_SECONDS[k]
|
|
123
|
+
}
|
|
124
|
+
let m = k.match(/^(\d+)\s*(h|hr|hour|hours|d|day|days|m|min|minutes)$/)
|
|
125
|
+
if (!m) {
|
|
126
|
+
return null
|
|
127
|
+
}
|
|
128
|
+
let n = Number(m[1])
|
|
129
|
+
if (/^h/.test(m[2])) {
|
|
130
|
+
return n * 3600
|
|
131
|
+
}
|
|
132
|
+
if (/^d/.test(m[2])) {
|
|
133
|
+
return n * 86400
|
|
134
|
+
}
|
|
135
|
+
return n * 60
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 將群組內之單一桶正規化為統一窗口物件
|
|
141
|
+
*
|
|
142
|
+
* @param {Object} g 輸入群組物件
|
|
143
|
+
* @param {Object} b 輸入桶物件
|
|
144
|
+
* @returns {Object|null} 回傳統一窗口物件, 桶無id或無剩餘比例回傳null
|
|
145
|
+
*/
|
|
146
|
+
function fromBucket(g, b) {
|
|
147
|
+
|
|
148
|
+
let key = get(b, 'id', '')
|
|
149
|
+
if (!isestr(key)) {
|
|
150
|
+
return null
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
//usedPercent, 由剩餘比例反推; 文件載明可能為數值或字串故一律轉數值
|
|
154
|
+
let rf = get(b, 'remaining_fraction', null)
|
|
155
|
+
let usedPercent = null
|
|
156
|
+
if (isnum(rf)) {
|
|
157
|
+
usedPercent = Math.round((1 - cdbl(rf)) * 100 * 100) / 100
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
//scope與label, 群組名為範圍; 窗口秒數可推得時標籤為「5小時(Gemini Models)」, 否則退回桶名
|
|
161
|
+
let scope = get(g, 'name', '')
|
|
162
|
+
if (!isestr(scope)) {
|
|
163
|
+
scope = ''
|
|
164
|
+
}
|
|
165
|
+
let windowSeconds = windowToSeconds(get(b, 'window', ''))
|
|
166
|
+
let base = toQuotaLabel(windowSeconds)
|
|
167
|
+
if (base === '') {
|
|
168
|
+
base = get(b, 'name', '')
|
|
169
|
+
if (!isestr(base)) {
|
|
170
|
+
base = key
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
let label = scope === '' ? base : `${base}(${scope})`
|
|
174
|
+
|
|
175
|
+
//severity, 用罄與停用須明示; 其餘normal
|
|
176
|
+
let severity = 'normal'
|
|
177
|
+
if (get(b, 'disabled', false) === true) {
|
|
178
|
+
severity = 'disabled'
|
|
179
|
+
}
|
|
180
|
+
else if (usedPercent !== null && usedPercent >= 100) {
|
|
181
|
+
severity = 'exhausted'
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return toQuotaWindow({
|
|
185
|
+
key,
|
|
186
|
+
label,
|
|
187
|
+
windowSeconds,
|
|
188
|
+
usedPercent,
|
|
189
|
+
resetAt: get(b, 'reset_time', ''),
|
|
190
|
+
scope,
|
|
191
|
+
active: false,
|
|
192
|
+
severity,
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* 執行agy之print模式讀取型slash指令並解析其JSON回應
|
|
199
|
+
*
|
|
200
|
+
* @param {String} exe 輸入agy執行檔
|
|
201
|
+
* @param {String} cmd 輸入slash指令字串, 例如'/usage'
|
|
202
|
+
* @param {Object} o 輸入設定物件{timeoutMs, cwd, logFile}
|
|
203
|
+
* @returns {Promise} 回傳物件{ok, data, response, stderr, error, errorType, code}
|
|
204
|
+
*/
|
|
205
|
+
async function agyPrint(exe, cmd, o) {
|
|
206
|
+
|
|
207
|
+
let args = []
|
|
208
|
+
if (isestr(o.logFile)) {
|
|
209
|
+
args.push('--log-file', o.logFile)
|
|
210
|
+
}
|
|
211
|
+
args.push('-p', cmd, '--output-format', 'json', '--print-timeout', `${Math.max(5, Math.ceil(o.timeoutMs / 1000))}s`)
|
|
212
|
+
|
|
213
|
+
let r = await execCli(exe, args, { timeoutMs: o.timeoutMs, cwd: o.cwd })
|
|
214
|
+
|
|
215
|
+
//stdout最後一行非空者為JSON
|
|
216
|
+
let last = String(get(r, 'stdout', '')).trim().split('\n').map((s) => s.trim()).filter((s) => s !== '').pop() || ''
|
|
217
|
+
let j = null
|
|
218
|
+
try {
|
|
219
|
+
j = JSON.parse(last)
|
|
220
|
+
}
|
|
221
|
+
catch (err) {}
|
|
222
|
+
|
|
223
|
+
if (!r.ok) {
|
|
224
|
+
let tail = String(get(r, 'stderr', '')).trim().slice(-300)
|
|
225
|
+
let errorType = /timeout/i.test(get(r, 'error', '')) ? 'timeout' : 'exit'
|
|
226
|
+
if (/not logged in|login|sign in|authenticate/i.test(tail)) {
|
|
227
|
+
errorType = 'auth'
|
|
228
|
+
}
|
|
229
|
+
return { ok: false, data: null, response: '', stderr: tail, error: `agy ${cmd} failed: ${get(r, 'error', '')}${tail ? ` | stderr: ${tail}` : ''}`, errorType, code: get(r, 'code', null) }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (j === null) {
|
|
233
|
+
return { ok: false, data: null, response: '', stderr: '', error: `agy ${cmd} stdout is not JSON: ${last.slice(0, 200)}`, errorType: 'parse', code: r.code }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
//防呆: 若agy把指令當成prompt執行(num_turns>0或無command), 代表版本過舊或slash展開被停用
|
|
237
|
+
let numTurns = get(j, 'num_turns', 0)
|
|
238
|
+
let cname = get(j, 'command.name', '')
|
|
239
|
+
if (!isestr(cname) || (isnum(numTurns) && cdbl(numTurns) > 0)) {
|
|
240
|
+
return { ok: false, data: null, response: get(j, 'response', ''), stderr: '', error: `agy did not handle ${cmd} as a slash command (num_turns=${numTurns}); version may be below ${MIN_VERSION} or --disable-slash-commands is set; this call may have consumed quota`, errorType: 'unsupported', code: r.code }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (get(j, 'status', '') !== 'SUCCESS') {
|
|
244
|
+
return { ok: false, data: get(j, 'command.data', null), response: get(j, 'response', ''), stderr: '', error: `agy ${cmd} reported status=${get(j, 'status', '')}: ${String(get(j, 'response', '')).slice(0, 200)}`, errorType: 'exit', code: r.code }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return { ok: true, data: get(j, 'command.data', null), response: get(j, 'response', ''), stderr: '', error: '', errorType: '', code: r.code, raw: j }
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* 查詢Antigravity(Google側, agy CLI)帳號之當前額度
|
|
253
|
+
*
|
|
254
|
+
* 以agy自身之print模式取得額度(官方、無頭、不碰憑證):`agy -p "/usage" --output-format json`,
|
|
255
|
+
* 並以同一次執行之`--log-file`取得登入帳號email。
|
|
256
|
+
* 額度綁定本機agy之登入憑證,email參數之作用為比對——不符時ok為false且matched為false;
|
|
257
|
+
* 未給email時不比對。本函數不會reject,一律以結果物件之ok與error欄位回報成敗
|
|
258
|
+
*
|
|
259
|
+
* @param {String} [email=''] 輸入欲查詢之帳號email字串,預設''代表不比對而直接回報本機當前帳號
|
|
260
|
+
* @param {Object} [opt={}] 輸入設定物件,預設{}
|
|
261
|
+
* @param {String} [opt.exe='agy'] 輸入agy執行檔名稱或路徑字串,預設'agy'
|
|
262
|
+
* @param {Boolean} [opt.checkVersion=true] 輸入是否先以`agy --version`把關版本布林值,低於1.1.11即回unsupported而不執行(舊版會把/usage當prompt跑而耗額度),預設true
|
|
263
|
+
* @param {Boolean} [opt.withCredits=false] 輸入是否另執行`/credits`取得G1點數餘額布林值,會多一次約5秒之啟動,預設false
|
|
264
|
+
* @param {String} [opt.cwd=os.tmpdir()] 輸入子進程工作目錄字串,預設系統暫存目錄以免在專案目錄留下痕跡
|
|
265
|
+
* @param {Number} [opt.timeoutMs=60000] 輸入單次agy執行之逾時毫秒正整數,預設60000(agy啟動加兩個後端往返實測5~8秒)
|
|
266
|
+
* @returns {Promise} 回傳Promise,resolve回傳結果物件,內含ok、provider('antigravity')、email(agy登入帳號)、matched、plan(空字串,/usage不提供方案別)、planTier(空字串)、source('agy-print-usage')、windows(每群組之5小時與7天窗口,scope為群組名)、credits(withCredits時為{remainingCredits, upgradeUri},否則null)、raw(含agy版本、authMethod、原始回應)、error、errorType、durationMs,本函數不會reject
|
|
267
|
+
* @example
|
|
268
|
+
* //need agy cli (>=1.1.11) logged in
|
|
269
|
+
*
|
|
270
|
+
* import getQuotaAntigravity from './src/quota/getQuotaAntigravity.mjs'
|
|
271
|
+
*
|
|
272
|
+
* let test = async () => {
|
|
273
|
+
* let r = await getQuotaAntigravity('firsemisphere@gmail.com')
|
|
274
|
+
* console.log(r.ok, r.email)
|
|
275
|
+
* // => true firsemisphere@gmail.com
|
|
276
|
+
* console.log(r.windows.map((w) => `${w.label}:${w.remainingPercent}%`).join(' '))
|
|
277
|
+
* // => 7天(Gemini Models):85% 5小時(Gemini Models):90% 7天(Claude and GPT models):100% 5小時(Claude and GPT models):100% (即時值)
|
|
278
|
+
* }
|
|
279
|
+
* test()
|
|
280
|
+
*
|
|
281
|
+
*/
|
|
282
|
+
async function getQuotaAntigravity(email = '', opt = {}) {
|
|
283
|
+
|
|
284
|
+
let t0 = Date.now()
|
|
285
|
+
let source = 'agy-print-usage'
|
|
286
|
+
|
|
287
|
+
//emailWant
|
|
288
|
+
let emailWant = isestr(email) ? email : ''
|
|
289
|
+
|
|
290
|
+
//exe
|
|
291
|
+
let exe = get(opt, 'exe', null)
|
|
292
|
+
if (!isestr(exe)) {
|
|
293
|
+
exe = DEFAULT_EXE
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
//timeoutMs
|
|
297
|
+
let timeoutMs = castPintOr(get(opt, 'timeoutMs', null), DEFAULT_TIMEOUT_MS)
|
|
298
|
+
|
|
299
|
+
//cwd
|
|
300
|
+
let cwd = get(opt, 'cwd', null)
|
|
301
|
+
if (!isestr(cwd)) {
|
|
302
|
+
cwd = os.tmpdir()
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
//checkVersion與withCredits
|
|
306
|
+
let checkVersion = get(opt, 'checkVersion', true) !== false
|
|
307
|
+
let withCredits = get(opt, 'withCredits', null)
|
|
308
|
+
if (!isbol(withCredits)) {
|
|
309
|
+
withCredits = false
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
//fin
|
|
313
|
+
let fin = (o) => {
|
|
314
|
+
return toQuotaResult('antigravity', {
|
|
315
|
+
emailWant,
|
|
316
|
+
source,
|
|
317
|
+
durationMs: Date.now() - t0,
|
|
318
|
+
...o,
|
|
319
|
+
})
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
//version gate
|
|
323
|
+
let version = ''
|
|
324
|
+
if (checkVersion) {
|
|
325
|
+
let rv = await execCli(exe, ['--version'], { timeoutMs: Math.min(timeoutMs, 15000), cwd })
|
|
326
|
+
if (!rv.ok) {
|
|
327
|
+
return fin({
|
|
328
|
+
error: `agy not found or not executable (${exe}): ${get(rv, 'error', '')}${String(get(rv, 'stderr', '')).trim() ? ` | ${String(rv.stderr).trim().slice(-200)}` : ''}; see https://antigravity.google/docs/cli/ for install and login`,
|
|
329
|
+
errorType: 'notfound',
|
|
330
|
+
})
|
|
331
|
+
}
|
|
332
|
+
version = String(get(rv, 'stdout', '')).trim().split('\n')[0]
|
|
333
|
+
|
|
334
|
+
//strCompareForVersion可直接餵--version原始輸出; 解析不出三段版本號回null, 一律視為不達門檻
|
|
335
|
+
let cmp = strCompareForVersion(version, MIN_VERSION)
|
|
336
|
+
if (cmp === null || cmp < 0) {
|
|
337
|
+
return fin({
|
|
338
|
+
error: `agy version [${version || 'unknown'}] is below ${MIN_VERSION}; its print mode cannot run /usage non-interactively (it would treat it as a prompt and consume quota); run [agy update]`,
|
|
339
|
+
errorType: 'unsupported',
|
|
340
|
+
raw: { version },
|
|
341
|
+
})
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
//logFile, 供取得登入帳號; 落系統暫存目錄, 結束必刪
|
|
346
|
+
let logFile = path.join(os.tmpdir(), `w-dispatch-ai-agy-${process.pid}-${crypto.randomBytes(4).toString('hex')}.log`)
|
|
347
|
+
|
|
348
|
+
let ru = null
|
|
349
|
+
let rc = null
|
|
350
|
+
let emailHave = ''
|
|
351
|
+
let authMethod = ''
|
|
352
|
+
try {
|
|
353
|
+
|
|
354
|
+
//usage
|
|
355
|
+
ru = await agyPrint(exe, '/usage', { timeoutMs, cwd, logFile })
|
|
356
|
+
|
|
357
|
+
//email, 自log取得(agy於認證後寫入applyAuthResult行)
|
|
358
|
+
let log = ''
|
|
359
|
+
try {
|
|
360
|
+
log = fs.readFileSync(logFile, 'utf8')
|
|
361
|
+
}
|
|
362
|
+
catch (err) {}
|
|
363
|
+
let m = log.match(/applyAuthResult: email=([^,\s]+), authMethod=(\w+)/)
|
|
364
|
+
if (m) {
|
|
365
|
+
emailHave = m[1]
|
|
366
|
+
authMethod = m[2]
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
let m2 = log.match(/authenticated successfully as (\S+)/)
|
|
370
|
+
if (m2) {
|
|
371
|
+
emailHave = m2[1]
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
//credits, 選配
|
|
376
|
+
if (ru.ok && withCredits) {
|
|
377
|
+
rc = await agyPrint(exe, '/credits', { timeoutMs, cwd, logFile: '' })
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
}
|
|
381
|
+
finally {
|
|
382
|
+
try {
|
|
383
|
+
fs.unlinkSync(logFile)
|
|
384
|
+
}
|
|
385
|
+
catch (err) {}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (!ru.ok) {
|
|
389
|
+
return fin({
|
|
390
|
+
email: emailHave,
|
|
391
|
+
error: ru.error,
|
|
392
|
+
errorType: ru.errorType,
|
|
393
|
+
raw: { version, authMethod, response: ru.response, stderr: ru.stderr },
|
|
394
|
+
})
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
//windows
|
|
398
|
+
let windows = []
|
|
399
|
+
let groups = get(ru.data, 'groups', null)
|
|
400
|
+
if (isearr(groups)) {
|
|
401
|
+
for (let g of groups) {
|
|
402
|
+
let buckets = get(g, 'buckets', null)
|
|
403
|
+
if (!isearr(buckets)) {
|
|
404
|
+
continue
|
|
405
|
+
}
|
|
406
|
+
for (let b of buckets) {
|
|
407
|
+
let w = fromBucket(g, b)
|
|
408
|
+
if (w !== null) {
|
|
409
|
+
windows.push(w)
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
//credits
|
|
416
|
+
let credits = null
|
|
417
|
+
if (rc !== null) {
|
|
418
|
+
credits = rc.ok
|
|
419
|
+
? { remainingCredits: get(rc.data, 'remaining_credits', null), upgradeUri: get(rc.data, 'upgrade_uri', '') }
|
|
420
|
+
: { error: rc.error }
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return fin({
|
|
424
|
+
email: emailHave,
|
|
425
|
+
windows,
|
|
426
|
+
credits,
|
|
427
|
+
raw: {
|
|
428
|
+
version,
|
|
429
|
+
authMethod,
|
|
430
|
+
description: get(ru.data, 'description', ''),
|
|
431
|
+
usage: ru.raw,
|
|
432
|
+
credits: rc !== null ? get(rc, 'raw', null) : null,
|
|
433
|
+
},
|
|
434
|
+
})
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
export default getQuotaAntigravity
|
|
439
|
+
</code></pre>
|
|
440
|
+
</article>
|
|
441
|
+
</section>
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
</div>
|
|
449
|
+
|
|
450
|
+
<br class="clear">
|
|
451
|
+
|
|
452
|
+
<footer>
|
|
453
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sat Sep 05 2026 22:03:07 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
454
|
+
</footer>
|
|
455
|
+
|
|
456
|
+
<script>prettyPrint();</script>
|
|
457
|
+
<script src="scripts/polyfill.js"></script>
|
|
458
|
+
<script src="scripts/linenumber.js"></script>
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
</body>
|
|
463
|
+
</html>
|