w-dispatch-ai 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,406 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+
5
+ <meta charset="utf-8">
6
+ <title>dispatchAiFallback.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#WDispatchAi">WDispatchAi</a></li><li><a href="global.html#adapters">adapters</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#dispatchAntigravity">dispatchAntigravity</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#getCliArgs">getCliArgs</a></li><li><a href="global.html#getErrorResult">getErrorResult</a></li><li><a href="global.html#isKeyIndependentFail">isKeyIndependentFail</a></li></ul>
33
+
34
+ </nav>
35
+
36
+ <div id="main">
37
+
38
+ <h1 class="page-title">dispatchAiFallback.mjs</h1>
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+ <section>
47
+ <article>
48
+ <pre class="prettyprint source linenums"><code>import get from 'lodash-es/get.js'
49
+ import omit from 'lodash-es/omit.js'
50
+ import isarr from 'wsemi/src/isarr.mjs'
51
+ import isobj from 'wsemi/src/isobj.mjs'
52
+ import isfun from 'wsemi/src/isfun.mjs'
53
+ import isestr from 'wsemi/src/isestr.mjs'
54
+ import ispint from 'wsemi/src/ispint.mjs'
55
+ import cint from 'wsemi/src/cint.mjs'
56
+ import dispatchAi from './dispatchAi.mjs'
57
+ import getErrorResult from './getErrorResult.mjs'
58
+
59
+
60
+ // dispatchAiFallback.mjs — 多供應商自動遞補層
61
+ //
62
+ // 【兩層策略】群組之間依providers宣告順序(優先序), 群組之內(keys多把)以游標輪替(額度均攤)。
63
+ //
64
+ // 【失敗分流】只分兩路:
65
+ // 與金鑰無關之失敗(TIMEOUT/ENOENT/參數錯誤/驗證失敗/未知kind) → 整組跳過——
66
+ // 同組各金鑰共用同一exe與model, 換金鑰必然再敗一次, 純屬空耗;
67
+ // 其餘失敗(含額度上限/金鑰無效/服務回錯等一切未分類者) → 換組內下一把, 不記憶、不停用。
68
+ // 不可把正確性建立在「錯誤分類器必須窮盡」之上——實測各家額度/金鑰錯誤訊息
69
+ // 含中文(无效的令牌)與無特徵字串(UnknownError), 正則涵蓋不了; 而額度視窗有5小時滾動、
70
+ // 逐小時、逐日等多種形態, 「命中即停用到當日結束」會把已復活的金鑰冰到隔天。
71
+ // 故跨次執行不設停用清單: 額度恢復的偵測就是「下次再打一次」, 代價僅一次快速失敗。
72
+ //
73
+ // 【跨次記憶只有游標】成功後游標推進至下一把, 令額度在同組多把金鑰間自動均攤;
74
+ // 死金鑰的代價也被游標攤平——這輪從key1敗轉key2成功後游標停在key3, 下輪不會先碰key1。
75
+ //
76
+ // 【時間預算】budgetMs限制整輪遞補的總時長, 剩餘預算會壓進每次呼叫的timeoutMs,
77
+ // 防止多家連續卡逾時而撞破外部排程的執行上限。
78
+
79
+
80
+ //fallback層自用之設定鍵, 其餘鍵作為各attempt之共用預設原樣轉傳
81
+ let FALLBACK_KEYS = ['providers', 'budgetMs', 'minAttemptMs', 'store', 'onEvent']
82
+
83
+
84
+ //providers條目自用之設定鍵, 其餘鍵(含kind)即該條目之opt原樣轉傳對應轉接器
85
+ let ENTRY_KEYS = ['id', 'keys']
86
+
87
+
88
+ //預設值
89
+ let DEFAULT_MIN_ATTEMPT_MS = 20000
90
+ let DEFAULT_TIMEOUT_MS = 120000
91
+
92
+
93
+ //memoryState, 未注入store時之行程內預設狀態(跨呼叫有效, 重啟歸零)
94
+ let memoryState = { cursors: {} }
95
+
96
+
97
+ /**
98
+ * 判斷失敗結果是否與「哪一把金鑰」無關(換組內金鑰必然再敗, 應整組跳過)
99
+ *
100
+ * @param {Object} r 輸入dispatchAi失敗結果物件
101
+ * @returns {Boolean} 回傳是否應整組跳過之布林值
102
+ */
103
+ function isKeyIndependentFail(r) {
104
+
105
+ let error = get(r, 'error', '')
106
+ if (!isestr(error)) {
107
+ error = ''
108
+ }
109
+ let code = get(r, 'code', null)
110
+
111
+ //逾時, 該服務卡住, 同服務其他金鑰只會再空耗一次完整timeout
112
+ if (error.indexOf('TIMEOUT') === 0) {
113
+ return true
114
+ }
115
+
116
+ //執行檔不存在, 同組共用同一exe
117
+ if (error.includes('ENOENT')) {
118
+ return true
119
+ }
120
+
121
+ //參數錯誤, 同組共用同一組旗標
122
+ if (code === 2) {
123
+ return true
124
+ }
125
+
126
+ //輸出未過驗證, CLI正常結束(code=0)且模型有回應, 換金鑰仍是同一模型之產出習慣
127
+ //注意判定依據是error字串而非code===0(code===0且ok===true是成功)
128
+ if (error === 'OUTPUT_VALIDATION_FAILED') {
129
+ return true
130
+ }
131
+
132
+ //kind無效, 屬條目設定錯誤
133
+ if (error.indexOf('unknown ai kind') === 0) {
134
+ return true
135
+ }
136
+
137
+ return false
138
+ }
139
+
140
+
141
+ /**
142
+ * 依供應商清單順序自動遞補調用AI,組內多金鑰以游標輪替
143
+ *
144
+ * 特點:
145
+ * providers陣列順序即優先序,排前面的先用;
146
+ * 條目本身即該次調用之opt(除id與keys外原樣透傳對應轉接器),與dispatchAi「條目直接當opt」同一約定;
147
+ * 條目給予keys(多把金鑰)時以游標輪替,某把失敗自動換下一把,全數失敗才遞補下一組;
148
+ * 與金鑰無關之失敗(逾時/執行檔不存在/參數錯誤/輸出未過驗證/未知kind)直接整組跳過,不逐把空耗;
149
+ * 跨次執行僅記憶游標(經store注入持久化),不設金鑰停用清單——額度視窗形態多樣(5小時滾動/逐時/逐日),
150
+ * 停用會把已恢復的金鑰閒置,而重探的代價僅一次快速失敗;
151
+ * 本函數不會reject,一律以結果物件之ok與error欄位回報成敗
152
+ *
153
+ * @param {String} prompt 輸入提示詞字串,一律以stdin傳入子進程
154
+ * @param {Object} [opt={}] 輸入設定物件,預設{}
155
+ * @param {Array} opt.providers 輸入供應商條目物件陣列,順序即優先序。各條目除下列鍵外,其餘鍵(kind、model、exe、provider、config、sandbox、timeoutMs等)即該條目之opt原樣透傳對應轉接器
156
+ * @param {String} [opt.providers[].id=條目索引字串] 輸入群組識別字串,游標以此為鍵,多金鑰條目應給予穩定id,預設為條目索引字串
157
+ * @param {Array} [opt.providers[].keys=[]] 輸入同一服務之多把API key字串陣列,逐次注入輪替(kind為opencode時須同時於條目給予provider),省略代表沿用CLI既有登入狀態之單一虛擬金鑰
158
+ * @param {Number} [opt.budgetMs=null] 輸入整輪遞補之時間上限毫秒正整數,剩餘預算會壓進每次呼叫之timeoutMs,預設null代表不限
159
+ * @param {Number} [opt.minAttemptMs=20000] 輸入單次嘗試之最低剩餘預算毫秒正整數,剩餘低於此值即停止嘗試回報budget exhausted,預設20000
160
+ * @param {Object} [opt.store=null] 輸入狀態持久化物件{get:()=>state,set:(state)=>{}},state內含cursors(逐群組游標),省略代表用行程內記憶體(跨呼叫有效,重啟歸零)。假定單行程序列調用,並行請自行加鎖
161
+ * @param {Function} [opt.onEvent=null] 輸入事件回調函數(ev)=>{},ev.type可為'try'、'ok'、'next-key'、'skip-group'、'budget-out',回調拋出例外不影響主流程,預設null
162
+ * @param {Number} [opt.timeoutMs=120000] 輸入各attempt共用之逾時毫秒正整數,條目可覆寫,預設120000
163
+ * @param {String|Function} [opt.validate=undefined] 輸入各attempt共用之stdout驗證規則,條目可覆寫,預設undefined
164
+ * @param {Number} [opt.maxRetries=0] 輸入各attempt共用之同家重試次數非負整數,韌性建議交給換家而非重試同一家,預設0
165
+ * @returns {Promise} 回傳Promise,resolve回傳結果物件,除execCli既有欄位(ok、stdout、stderr、code、error、durationMs、attempts、pid)外,追加providerId(實際使用之群組)、keyIndex(實際使用之金鑰索引,無keys時為null)、kind、model、tried(全部嘗試歷程陣列,成功時亦回傳),本函數不會reject
166
+ * @example
167
+ * //need opencode, claude, codex cli in system PATH
168
+ *
169
+ * import dispatchAiFallback from './src/dispatchAiFallback.mjs'
170
+ *
171
+ * let test = async () => {
172
+ *
173
+ * let r = await dispatchAiFallback('請只回覆兩個字:完成', {
174
+ * providers: [
175
+ * {
176
+ * id: 'deepseek',
177
+ * kind: 'opencode',
178
+ * model: 'opencode/deepseek-v4-flash-free',
179
+ * provider: 'opencode',
180
+ * keys: ['sk-aaa', 'sk-bbb'], //多把金鑰, 某把失敗自動換下一把
181
+ * timeoutMs: 180000,
182
+ * },
183
+ * { id: 'claude', kind: 'claude', model: 'sonnet' }, //deepseek全敗時遞補
184
+ * { id: 'codex', kind: 'codex', model: 'gpt-5.6-luna', sandbox: 'read-only' },
185
+ * ],
186
+ * budgetMs: 600000,
187
+ * onEvent: (ev) => console.log(ev.type, ev.providerId, ev.keyIndex),
188
+ * })
189
+ * console.log(r.ok, r.providerId, r.keyIndex, r.tried.length)
190
+ * // => true 'deepseek' 0 1
191
+ *
192
+ * }
193
+ * await test()
194
+ * .catch((err) => {
195
+ * console.log(err)
196
+ * })
197
+ *
198
+ */
199
+ async function dispatchAiFallback(prompt, opt = {}) {
200
+
201
+ //check prompt, 於頂層先擋, 否則會逐組空跑同一錯誤
202
+ if (!isestr(prompt)) {
203
+ return { ...getErrorResult('prompt must be a non-empty string'), tried: [] }
204
+ }
205
+
206
+ //providers, 濾除非物件條目
207
+ let providersRaw = get(opt, 'providers', null)
208
+ let providers = isarr(providersRaw) ? providersRaw.filter(isobj) : []
209
+ if (providers.length === 0) {
210
+ return { ...getErrorResult('providers must be a non-empty array'), tried: [] }
211
+ }
212
+
213
+ //budgetMs, 無效視為不限
214
+ let budgetMs = get(opt, 'budgetMs', null)
215
+ if (!ispint(budgetMs)) {
216
+ budgetMs = null
217
+ }
218
+ else {
219
+ budgetMs = cint(budgetMs)
220
+ }
221
+ let deadline = (budgetMs === null) ? null : Date.now() + budgetMs
222
+
223
+ //minAttemptMs, 無效回退預設20000
224
+ let minAttemptMs = get(opt, 'minAttemptMs', null)
225
+ if (!ispint(minAttemptMs)) {
226
+ minAttemptMs = DEFAULT_MIN_ATTEMPT_MS
227
+ }
228
+ else {
229
+ minAttemptMs = cint(minAttemptMs)
230
+ }
231
+
232
+ //store, 須同時具get與set函數才視為有效, 否則用行程內記憶體
233
+ let store = get(opt, 'store', null)
234
+ let useStore = isobj(store) &amp;&amp; isfun(store.get) &amp;&amp; isfun(store.set)
235
+ let state = null
236
+ if (useStore) {
237
+ try {
238
+ state = store.get()
239
+ }
240
+ catch {}
241
+ }
242
+ if (!isobj(state)) {
243
+ state = useStore ? { cursors: {} } : memoryState
244
+ }
245
+ if (!isobj(state.cursors)) {
246
+ state.cursors = {}
247
+ }
248
+ let saveState = () => {
249
+ if (useStore) {
250
+ try {
251
+ store.set(state)
252
+ }
253
+ catch {}
254
+ }
255
+ }
256
+
257
+ //onEvent, 回調拋出例外不得中斷主流程
258
+ let onEvent = get(opt, 'onEvent', null)
259
+ let emit = (ev) => {
260
+ if (isfun(onEvent)) {
261
+ try {
262
+ onEvent(ev)
263
+ }
264
+ catch {}
265
+ }
266
+ }
267
+
268
+ //sharedOpt, 剔除fallback層自用鍵後作為各attempt共用預設, 條目覆寫之
269
+ let sharedOpt = omit(opt, FALLBACK_KEYS)
270
+
271
+ let tried = []
272
+ let lastResult = null
273
+ let lastMeta = null
274
+
275
+ //逐群組(宣告順序即優先序), 單向單輪不回頭
276
+ for (let ig = 0; ig &lt; providers.length; ig++) {
277
+ let entry = providers[ig]
278
+
279
+ //id, 無效回退條目索引字串
280
+ let id = get(entry, 'id', null)
281
+ if (!isestr(id)) {
282
+ id = String(ig)
283
+ }
284
+
285
+ //kind與model僅供事件與回傳meta, kind有效性由dispatchAi判定
286
+ let kind = get(entry, 'kind', null)
287
+ let model = get(entry, 'model', null)
288
+
289
+ //keys, 濾除非有效字串, 空陣列視同未給(登入態單一虛擬金鑰)
290
+ let keysRaw = get(entry, 'keys', null)
291
+ let keys = isarr(keysRaw) ? keysRaw.filter(isestr) : []
292
+ let nk = keys.length
293
+
294
+ //entryOpt, 剔除條目自用鍵後即該條目之opt
295
+ let entryOpt = omit(entry, ENTRY_KEYS)
296
+
297
+ //游標, 逐群組記錄, 以現行keys長度取模自癒(金鑰陣列改動時不出界)
298
+ let cursor = 0
299
+ if (nk > 0) {
300
+ let c = get(state.cursors, id, 0)
301
+ cursor = ispint(c) ? cint(c) % nk : 0
302
+ }
303
+
304
+ //組內逐把嘗試, 每把至多一次, 全敗即組盡遞補下一組
305
+ let nAttempts = (nk > 0) ? nk : 1
306
+ let skipGroup = false
307
+ for (let a = 0; a &lt; nAttempts &amp;&amp; !skipGroup; a++) {
308
+
309
+ //keyIndex, 無keys時為null
310
+ let keyIndex = (nk > 0) ? (cursor + a) % nk : null
311
+ let keyId = (keyIndex === null) ? id : `${id}#${keyIndex}`
312
+
313
+ //attemptOpt, 共用預設 &lt;- 條目覆寫 &lt;- 本把金鑰
314
+ let attemptOpt = { ...sharedOpt, ...entryOpt }
315
+ if (nk > 0) {
316
+ attemptOpt.key = keys[keyIndex]
317
+ }
318
+
319
+ //有效timeout, 有預算時以剩餘預算封頂
320
+ let effTimeout = get(attemptOpt, 'timeoutMs', null)
321
+ if (!ispint(effTimeout)) {
322
+ effTimeout = DEFAULT_TIMEOUT_MS
323
+ }
324
+ else {
325
+ effTimeout = cint(effTimeout)
326
+ }
327
+ if (deadline !== null) {
328
+ let remaining = deadline - Date.now()
329
+
330
+ //剩餘不足一次最低嘗試, 停止遞補回報預算用盡
331
+ if (remaining &lt; minAttemptMs) {
332
+ emit({ type: 'budget-out', providerId: id, keyIndex, keyId, remainingMs: remaining })
333
+ tried.push({ providerId: id, keyIndex, keyId, outcome: 'budget-out' })
334
+ return { ...getErrorResult('budget exhausted'), tried }
335
+ }
336
+ effTimeout = Math.min(effTimeout, remaining)
337
+ }
338
+ attemptOpt.timeoutMs = effTimeout
339
+
340
+ //dispatch
341
+ emit({ type: 'try', providerId: id, keyIndex, keyId, kind, model })
342
+ let r = await dispatchAi(kind, prompt, attemptOpt)
343
+
344
+ //成功, 推進游標(額度均攤)並回傳
345
+ if (r.ok) {
346
+ if (nk > 0) {
347
+ state.cursors[id] = (keyIndex + 1) % nk
348
+ saveState()
349
+ }
350
+ emit({ type: 'ok', providerId: id, keyIndex, keyId, durationMs: r.durationMs })
351
+ tried.push({ providerId: id, keyIndex, keyId, outcome: 'ok', durationMs: r.durationMs })
352
+ return { ...r, providerId: id, keyIndex, kind, model, tried }
353
+ }
354
+
355
+ //失敗分流
356
+ lastResult = r
357
+ lastMeta = { providerId: id, keyIndex, kind, model }
358
+ if (isKeyIndependentFail(r)) {
359
+
360
+ //與金鑰無關, 整組跳過
361
+ emit({ type: 'skip-group', providerId: id, keyIndex, keyId, error: r.error })
362
+ tried.push({ providerId: id, keyIndex, keyId, outcome: 'skip-group', error: r.error, durationMs: r.durationMs })
363
+ skipGroup = true
364
+ }
365
+ else {
366
+
367
+ //其餘(含額度上限/金鑰無效/未分類), 換組內下一把, 不記憶不停用
368
+ emit({ type: 'next-key', providerId: id, keyIndex, keyId, error: r.error })
369
+ tried.push({ providerId: id, keyIndex, keyId, outcome: 'next-key', error: r.error, durationMs: r.durationMs })
370
+ }
371
+
372
+ }
373
+ }
374
+
375
+ //全數失敗, 回傳最後一筆失敗結果與完整歷程
376
+ let r = lastResult || getErrorResult('all providers failed')
377
+ return { ...r, ...(lastMeta || {}), tried }
378
+ }
379
+
380
+
381
+ export default dispatchAiFallback
382
+ </code></pre>
383
+ </article>
384
+ </section>
385
+
386
+
387
+
388
+
389
+
390
+
391
+ </div>
392
+
393
+ <br class="clear">
394
+
395
+ <footer>
396
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Mon Aug 10 2026 09:36:49 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
397
+ </footer>
398
+
399
+ <script>prettyPrint();</script>
400
+ <script src="scripts/polyfill.js"></script>
401
+ <script src="scripts/linenumber.js"></script>
402
+
403
+
404
+
405
+ </body>
406
+ </html>
@@ -0,0 +1,264 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+
5
+ <meta charset="utf-8">
6
+ <title>dispatchAntigravity.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#WDispatchAi">WDispatchAi</a></li><li><a href="global.html#adapters">adapters</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#dispatchAntigravity">dispatchAntigravity</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#getCliArgs">getCliArgs</a></li><li><a href="global.html#getErrorResult">getErrorResult</a></li><li><a href="global.html#isKeyIndependentFail">isKeyIndependentFail</a></li></ul>
33
+
34
+ </nav>
35
+
36
+ <div id="main">
37
+
38
+ <h1 class="page-title">dispatchAntigravity.mjs</h1>
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+ <section>
47
+ <article>
48
+ <pre class="prettyprint source linenums"><code>import get from 'lodash-es/get.js'
49
+ import omit from 'lodash-es/omit.js'
50
+ import isarr from 'wsemi/src/isarr.mjs'
51
+ import isbol from 'wsemi/src/isbol.mjs'
52
+ import isestr from 'wsemi/src/isestr.mjs'
53
+ import ispint from 'wsemi/src/ispint.mjs'
54
+ import cint from 'wsemi/src/cint.mjs'
55
+ import execCli from 'wsemi/src/execCli.mjs'
56
+ import getCliArgs from './getCliArgs.mjs'
57
+ import getErrorResult from './getErrorResult.mjs'
58
+
59
+
60
+ // dispatchAntigravity.mjs — 以Google Antigravity CLI(agy)呼叫AI模型
61
+ //
62
+ // agy為Google官方之Gemini CLI後繼者(Gemini CLI已於2026-06-18停服), 命令名為agy非antigravity。
63
+ // 實測其models清單除Gemini系外亦含claude與gpt-oss等模型, 屬多模型閘道。
64
+ //
65
+ // 【prompt走--print旗標, 不走stdin(2026-08-10於本機agy 1.1.11實測確認)】
66
+ // --print為帶值旗標(無值即回`flag needs an argument: -print`), 不可能由stdin取prompt;
67
+ // 把prompt塞stdin而args無--print會進互動模式卡住。此為與另三家轉接器最大差異。
68
+ //
69
+ // 【命令列長度上限(實測)】prompt作為命令列參數受Windows CreateProcess之32767字元上限:
70
+ // 純文字32600可spawn、32700即spawn ENAMETOOLONG(同步拋出, 會穿透execCli成rejection)。
71
+ // 且含引號之prompt經spawn轉義後長度會膨脹(實測2萬個引號轉義後仍炸), 故防護須兩層:
72
+ // 前置長度檢查(常見情境給清楚錯誤訊息) + try/catch兜底(轉義膨脹等漏網情境),
73
+ // 兩層皆回錯誤結果物件, 維持本套件「不reject」之契約。
74
+ //
75
+ // 【model與effort之衝突規則(實測)】帶檔位之slug(如gemini-3.1-pro-high)與--effort同時給定時,
76
+ // 檔位「不一致」才拒絕(`--model gemini-3.1-pro-high conflicts with --effort=low`, exit 1),
77
+ // 一致(gemini-3.1-pro-high + --effort high)則放行; 基礎slug(如gemini-3.1-pro)搭配effort亦放行。
78
+ // 本轉接器不預判slug格式(模型清單會演進), 衝突時由agy回報conflicts錯誤。
79
+ //
80
+ // 【雙逾時對齊】agy自身有--print-timeout(預設5m0s), 與execCli之timeoutMs為兩層獨立逾時。
81
+ // printTimeout未給時由timeoutMs推導並預留30秒緩衝, 令CLI先於外層逾時,
82
+ // 使錯誤訊息來自agy自身而非被外層強殺。
83
+
84
+
85
+ //預設值
86
+ let DEFAULT_EXE = 'agy'
87
+ let DEFAULT_TIMEOUT_MS = 300000 //agent型CLI, 對齊agy自身print-timeout預設5m0s, 不沿用套件通用120000
88
+ let MAX_PROMPT_LENGTH = 30000 //命令列上限32767扣除exe路徑與旗標後之保守值
89
+ let PRINT_TIMEOUT_BUFFER_S = 30
90
+ let MIN_PRINT_TIMEOUT_S = 30
91
+
92
+
93
+ //本轉接器自用之設定鍵, 其餘鍵一律原樣轉傳execCli
94
+ let OWN_KEYS = ['exe', 'model', 'effort', 'skipPermissions', 'printTimeout', 'addDirs', 'extraArgs', 'input']
95
+
96
+
97
+ /**
98
+ * 以Google Antigravity CLI(agy)呼叫AI模型
99
+ *
100
+ * 特點:
101
+ * prompt作為`--print`旗標之值傳遞而非stdin(agy介面如此,塞stdin會進互動模式卡住),
102
+ * 故prompt受命令列長度上限約束,超過30000字元回傳錯誤結果物件;
103
+ * model須為`agy models`第一欄之slug(如gemini-3.6-flash-low),注意agy錯誤訊息列出的是顯示名稱而非slug;
104
+ * 帶檔位之slug(-high/-medium/-low結尾)與effort同時給定且檔位不一致時agy回conflicts錯誤(一致則放行),
105
+ * effort需agy>=1.1.11,建議搭配不帶檔位之基礎slug(如gemini-3.1-pro)使用;
106
+ * 預設帶`--dangerously-skip-permissions`令非互動print模式不卡權限確認,可給予skipPermissions為false保留權限閘門;
107
+ * agy自身之--print-timeout未給時由timeoutMs推導並預留30秒緩衝,令CLI先於外層逾時;
108
+ * 沿用agy既有OAuth登入狀態(首次須於桌面互動模式完成登入);
109
+ * 本函數不會reject,一律以結果物件之ok與error欄位回報成敗
110
+ *
111
+ * @param {String} prompt 輸入提示詞字串,作為`--print`旗標之值傳遞,長度上限30000字元
112
+ * @param {Object} [opt={}] 輸入設定物件,預設{}
113
+ * @param {String} [opt.exe='agy'] 輸入agy執行檔名稱或絕對路徑字串,命令名為agy非antigravity,給予名稱時由execCli自系統PATH解析,預設'agy'
114
+ * @param {String} [opt.model=''] 輸入模型slug字串,須為`agy models`第一欄之slug,例如'gemini-3.6-flash-low'、'gemini-3.1-pro-high',預設''代表不帶`--model`旗標由agy自行決定
115
+ * @param {String} [opt.effort=''] 輸入推理深度字串,可選'low'、'medium'、'high',需agy>=1.1.11,建議搭配不帶檔位之基礎slug;與帶檔位slug併用且檔位不一致時agy回conflicts錯誤,預設''代表不帶
116
+ * @param {Boolean} [opt.skipPermissions=true] 輸入是否帶`--dangerously-skip-permissions`旗標布林值,false代表保留CLI權限閘門,預設true
117
+ * @param {String} [opt.printTimeout=''] 輸入agy自身等待上限字串(如'10m'、'570s'),預設''代表由timeoutMs推導(扣30秒緩衝,下限30秒)
118
+ * @param {Array} [opt.addDirs=[]] 輸入加入workspace之目錄字串陣列,逐項展開為`--add-dir`,預設[]
119
+ * @param {Array} [opt.extraArgs=[]] 輸入額外命令列旗標字串陣列(如--output-format、--json-schema、--mode),將接於固定旗標之後、`--print`之前,預設[]
120
+ * @param {Number} [opt.timeoutMs=300000] 輸入逾時毫秒正整數,逾時將強制關閉子進程及其子孫程序,agy為agent型CLI故預設較長之300000,預設300000
121
+ * @param {String} [opt.cwd=process.cwd()] 輸入子進程工作目錄字串,預設process.cwd()
122
+ * @param {String|Function} [opt.validate=undefined] 輸入stdout驗證規則字串或自訂驗證函數,規則字串支援'nonempty'、'json'、'min:100',多規則可用逗號串接,預設undefined代表不驗證
123
+ * @param {Number} [opt.maxRetries=0] 輸入失敗後最大重試次數非負整數,預設0
124
+ * @returns {Promise} 回傳Promise,resolve回傳結果物件,內含ok(是否成功布林值)、stdout(標準輸出字串)、stderr(標準錯誤字串)、code(離開碼)、error(錯誤訊息字串,成功時為空字串)、durationMs(耗時毫秒)、attempts(實際嘗試次數),本函數不會reject
125
+ * @example
126
+ * //need agy cli in system PATH, and OAuth login completed
127
+ *
128
+ * import dispatchAntigravity from './src/dispatchAntigravity.mjs'
129
+ *
130
+ * let test = async () => {
131
+ *
132
+ * let r = await dispatchAntigravity('請只回覆兩個字:完成', { model: 'gemini-3.6-flash-low' })
133
+ * console.log(r.ok, r.stdout.trim())
134
+ * // => true 完成
135
+ *
136
+ * //基礎slug搭配effort(不可用帶檔位之slug併用effort)
137
+ * let r2 = await dispatchAntigravity('請只回覆兩個字:完成', { model: 'gemini-3.1-pro', effort: 'low' })
138
+ * console.log(r2.ok)
139
+ * // => true
140
+ *
141
+ * let re = await dispatchAntigravity('')
142
+ * console.log(re.ok, re.error)
143
+ * // => false 'prompt must be a non-empty string'
144
+ *
145
+ * }
146
+ * await test()
147
+ * .catch((err) => {
148
+ * console.log(err)
149
+ * })
150
+ *
151
+ */
152
+ async function dispatchAntigravity(prompt, opt = {}) {
153
+
154
+ //check prompt, 不reject故以錯誤結果物件回報
155
+ if (!isestr(prompt)) {
156
+ return getErrorResult('prompt must be a non-empty string')
157
+ }
158
+
159
+ //check prompt length, agy之prompt走命令列旗標而非stdin, 受CreateProcess 32767字元上限約束
160
+ if (prompt.length > MAX_PROMPT_LENGTH) {
161
+ return getErrorResult(`prompt too long (${prompt.length} chars > ${MAX_PROMPT_LENGTH}), agy passes prompt via --print flag and Windows command line is capped at 32767 chars`)
162
+ }
163
+
164
+ //exe, 無效回退預設'agy', 由execCli自系統PATH解析實體路徑
165
+ let exe = get(opt, 'exe', null)
166
+ if (!isestr(exe)) {
167
+ exe = DEFAULT_EXE
168
+ }
169
+
170
+ //model, 無效時整段`--model`旗標不出現, 由agy自行決定使用模型
171
+ let model = get(opt, 'model', null)
172
+
173
+ //effort, 無效時整段`--effort`旗標不出現; 與帶檔位slug之衝突由agy判定(檔位不一致時conflicts錯誤, exit 1)
174
+ let effort = get(opt, 'effort', null)
175
+
176
+ //skipPermissions, 非布林值回退預設true(非互動print模式不卡權限確認)
177
+ let skipPermissions = get(opt, 'skipPermissions', null)
178
+ if (!isbol(skipPermissions)) {
179
+ skipPermissions = true
180
+ }
181
+
182
+ //timeoutMs, 先行取值以供printTimeout推導, agy專屬預設300000
183
+ let timeoutMs = get(opt, 'timeoutMs', null)
184
+ if (!ispint(timeoutMs)) {
185
+ timeoutMs = DEFAULT_TIMEOUT_MS
186
+ }
187
+ else {
188
+ timeoutMs = cint(timeoutMs)
189
+ }
190
+
191
+ //printTimeout, 未給時由timeoutMs推導並預留緩衝, 令agy先於外層逾時而回報自身錯誤訊息
192
+ let printTimeout = get(opt, 'printTimeout', null)
193
+ if (!isestr(printTimeout)) {
194
+ printTimeout = `${Math.max(MIN_PRINT_TIMEOUT_S, Math.floor(timeoutMs / 1000) - PRINT_TIMEOUT_BUFFER_S)}s`
195
+ }
196
+
197
+ //addDirs, 逐項展開為--add-dir(agy該旗標可重複)
198
+ let addDirs = get(opt, 'addDirs', null)
199
+ let addDirArgs = []
200
+ if (isarr(addDirs)) {
201
+ for (let d of addDirs) {
202
+ if (isestr(d)) {
203
+ addDirArgs.push('--add-dir', d)
204
+ }
205
+ }
206
+ }
207
+
208
+ //extraArgs
209
+ let extraArgs = get(opt, 'extraArgs', null)
210
+
211
+ //args, prompt作為--print之值放最後
212
+ let args = getCliArgs(
213
+ skipPermissions ? '--dangerously-skip-permissions' : [],
214
+ ['--print-timeout', printTimeout],
215
+ isestr(model) ? ['--model', model] : [],
216
+ isestr(effort) ? ['--effort', effort] : [],
217
+ addDirArgs,
218
+ extraArgs,
219
+ ['--print', prompt],
220
+ )
221
+
222
+ //optCli, 剔除本轉接器自用鍵後原樣轉傳, 令呼叫端可用execCli全部設定(例如onStdout、maxBuffer)
223
+ let optCli = omit(opt, OWN_KEYS)
224
+
225
+ //execCli, 不帶input(agy由--print取得prompt); try/catch兜底轉義膨脹等
226
+ //前置檢查漏網之spawn同步拋出(如ENAMETOOLONG), 維持不reject契約
227
+ try {
228
+ return await execCli(exe, args, {
229
+ ...optCli,
230
+ timeoutMs,
231
+ })
232
+ }
233
+ catch (err) {
234
+ return getErrorResult(`${err.code || 'UNKNOWN'}: ${err.message}`)
235
+ }
236
+ }
237
+
238
+
239
+ export default dispatchAntigravity
240
+ </code></pre>
241
+ </article>
242
+ </section>
243
+
244
+
245
+
246
+
247
+
248
+
249
+ </div>
250
+
251
+ <br class="clear">
252
+
253
+ <footer>
254
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Mon Aug 10 2026 09:36:49 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
255
+ </footer>
256
+
257
+ <script>prettyPrint();</script>
258
+ <script src="scripts/polyfill.js"></script>
259
+ <script src="scripts/linenumber.js"></script>
260
+
261
+
262
+
263
+ </body>
264
+ </html>