w-dispatch-ai 1.0.7 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -1
- package/dist/w-dispatch-ai.umd.js +2 -2
- package/dist/w-dispatch-ai.umd.js.map +1 -1
- package/docs/WDispatchAi.mjs.html +4 -3
- package/docs/adapters.mjs.html +2 -2
- package/docs/castPintOr.mjs.html +109 -0
- package/docs/dfTimeoutMs.mjs.html +2 -2
- package/docs/dispatchAi.mjs.html +14 -13
- package/docs/dispatchAiFallback.mjs.html +156 -97
- package/docs/dispatchAiWkf.mjs.html +2 -2
- package/docs/dispatchAntigravity.mjs.html +10 -14
- package/docs/dispatchApiOpenaiCompat.mjs.html +50 -64
- package/docs/dispatchClaude.mjs.html +8 -13
- package/docs/dispatchCodex.mjs.html +8 -13
- package/docs/dispatchOpencode.mjs.html +8 -13
- package/docs/getCliArgs.mjs.html +2 -2
- package/docs/getErrorResult.mjs.html +12 -5
- package/docs/getErrorType.mjs.html +171 -0
- package/docs/global.html +1275 -88
- package/docs/index.html +2 -2
- package/docs/resolveProviders.mjs.html +2 -2
- package/docs/wkf_callAiWithFallback.mjs.html +14 -10
- package/docs/wkf_extractJsonLoose.mjs.html +2 -2
- package/docs/wkf_runFanout.mjs.html +8 -8
- package/docs/wkf_runFanoutPipeline.mjs.html +14 -18
- package/docs/wkf_runRolePipeline.mjs.html +5 -4
- package/package.json +1 -1
- package/src/WDispatchAi.mjs +2 -1
- package/src/castPintOr.mjs +37 -0
- package/src/dispatchAi.mjs +12 -11
- package/src/dispatchAiFallback.mjs +154 -95
- package/src/dispatchAntigravity.mjs +8 -12
- package/src/dispatchApiOpenaiCompat.mjs +48 -62
- package/src/dispatchClaude.mjs +6 -11
- package/src/dispatchCodex.mjs +6 -11
- package/src/dispatchOpencode.mjs +6 -11
- package/src/getErrorResult.mjs +10 -3
- package/src/getErrorType.mjs +99 -0
- package/src/wkf/callAiWithFallback.mjs +12 -8
- package/src/wkf/runFanout.mjs +6 -6
- package/src/wkf/runFanoutPipeline.mjs +12 -16
- package/src/wkf/runRolePipeline.mjs +3 -2
- package/test/tools/fakeServerForApiTest.mjs +4 -1
- package/test/unit-callAiWithFallback.test.mjs +24 -0
- package/test/unit-castPintOr.test.mjs +28 -0
- package/test/unit-dispatchAiFallback.test.mjs +137 -0
- package/test/unit-dispatchApiOpenaiCompat.test.mjs +39 -0
- package/test/unit-getErrorResult.test.mjs +13 -1
- package/test/unit-getErrorType.test.mjs +36 -0
- package/test/unit-runFanout.test.mjs +14 -0
- package/test/unit-runRolePipeline.test.mjs +12 -0
|
@@ -8,6 +8,7 @@ import ispint from 'wsemi/src/ispint.mjs'
|
|
|
8
8
|
import cint from 'wsemi/src/cint.mjs'
|
|
9
9
|
import dispatchAi from './dispatchAi.mjs'
|
|
10
10
|
import getErrorResult from './getErrorResult.mjs'
|
|
11
|
+
import castPintOr from './castPintOr.mjs'
|
|
11
12
|
import dfTimeoutMs from './dfTimeoutMs.mjs'
|
|
12
13
|
|
|
13
14
|
|
|
@@ -32,9 +33,23 @@ import dfTimeoutMs from './dfTimeoutMs.mjs'
|
|
|
32
33
|
// (使用端實測: 一次107秒的多階段請求中72秒耗在重複踩同一組429, 啟用冷卻後降至15秒)。
|
|
33
34
|
// 設計與「金鑰停用清單」(已否決)的關鍵差異: 以「條目」為單位、短視窗、且「只降序不移除」
|
|
34
35
|
// ——冷卻中的條目移到鏈尾而非移除, 前面全敗時照樣會被嘗試, 故不存在把已恢復服務冰住的問題;
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
36
|
+
// 任一次成功立即解除。內建觸發限於限流(HTTP 429, 僅api-openai-compat可靠偵測; CLI類之
|
|
37
|
+
// 限流埋在stderr文字中, 各家字樣不同且隨版本漂移, 本套件不維護簽章表)與逾時(TIMEOUT開頭,
|
|
38
|
+
// 各kind皆可)兩類——其餘失敗已有換金鑰換家機制處理, 納入反而誤傷。
|
|
39
|
+
// CLI類限流之偵測採依賴注入: 呼叫端於實測中觀察到穩定字樣時, 以coolDetect(r)=>Boolean
|
|
40
|
+
// 自行判定(收到完整失敗結果含stderr), 命中即視同冷卻觸發——簽章表由觀察到字樣的呼叫端
|
|
41
|
+
// 維護, 漏判僅退回現狀(每階段重探一次), 誤判也只是降尾非移除, 兩邊代價都有上限。
|
|
42
|
+
// 狀態存於state.cooling, 與cursors同走store持久化。
|
|
43
|
+
//
|
|
44
|
+
// 【中止(shouldStop)】呼叫端(如server於客戶端斷線後)可注入shouldStop()=>Boolean,
|
|
45
|
+
// 於「每次嘗試之間」檢查, true即停止遞補回報ABORTED——把「斷線後仍空耗整條鏈」
|
|
46
|
+
// 縮成「至多再耗當前這一家」。檢查點只此一處: 工作流各層經omit轉傳自動獲得,
|
|
47
|
+
// 中止後每個後續呼叫進門即回ABORTED, 整條工作流自然快速收束, 不需逐層實作。
|
|
48
|
+
// 不中止進行中之嘗試(不殺子進程/不斷開請求), 此為已知設計取捨(避免侵入execCli層)。
|
|
49
|
+
//
|
|
50
|
+
// 【meta保留鍵】「剔除自用鍵後原樣轉傳」令條目即調校點, 但呼叫端放進條目/opt的任何
|
|
51
|
+
// 自有欄位都會被靜默轉傳——保留meta一鍵保證永不轉傳, 呼叫端要掛分類/標籤/註記
|
|
52
|
+
// 一律放meta, 與轉傳機制永久絕緣(工作流各層之規格物件同此約定)。
|
|
38
53
|
//
|
|
39
54
|
// 【時間預算】budgetMs限制整輪遞補的總時長, 剩餘預算會壓進每次呼叫的timeoutMs,
|
|
40
55
|
// 防止多家連續卡逾時而撞破外部排程的執行上限。
|
|
@@ -69,11 +84,12 @@ import dfTimeoutMs from './dfTimeoutMs.mjs'
|
|
|
69
84
|
|
|
70
85
|
|
|
71
86
|
//fallback層自用之設定鍵, 其餘鍵作為各attempt之共用預設原樣轉傳
|
|
72
|
-
|
|
87
|
+
//meta為保留鍵: 呼叫端掛自有資訊(分類/標籤/註記)用, 保證永不轉傳(見檔頭【meta保留鍵】)
|
|
88
|
+
let FALLBACK_KEYS = ['providers', 'budgetMs', 'minAttemptMs', 'cooldownMs', 'shouldStop', 'coolDetect', 'store', 'onEvent', 'meta']
|
|
73
89
|
|
|
74
90
|
|
|
75
91
|
//providers條目自用之設定鍵, 其餘鍵(含kind)即該條目之opt原樣轉傳對應轉接器
|
|
76
|
-
let ENTRY_KEYS = ['id', 'keys']
|
|
92
|
+
let ENTRY_KEYS = ['id', 'keys', 'meta']
|
|
77
93
|
|
|
78
94
|
|
|
79
95
|
//預設值
|
|
@@ -85,6 +101,79 @@ let DEFAULT_TIMEOUT_MS = dfTimeoutMs //全套件統一預設300000
|
|
|
85
101
|
let memoryState = { cursors: {} }
|
|
86
102
|
|
|
87
103
|
|
|
104
|
+
/**
|
|
105
|
+
* 初始化游標與冷卻狀態(store有效即載入持久化狀態, 否則用行程內記憶體)
|
|
106
|
+
*
|
|
107
|
+
* @param {Object} store 輸入狀態持久化物件{get,set},無效代表用行程內記憶體
|
|
108
|
+
* @returns {Object} 回傳物件,內含state(狀態物件,保證有cursors與cooling)與saveState(寫回函數,store無效或寫入失敗皆靜默)
|
|
109
|
+
*/
|
|
110
|
+
function initState(store) {
|
|
111
|
+
let useStore = isobj(store) && isfun(store.get) && isfun(store.set)
|
|
112
|
+
let state = null
|
|
113
|
+
if (useStore) {
|
|
114
|
+
try {
|
|
115
|
+
state = store.get()
|
|
116
|
+
}
|
|
117
|
+
catch {}
|
|
118
|
+
}
|
|
119
|
+
if (!isobj(state)) {
|
|
120
|
+
state = useStore ? { cursors: {} } : memoryState
|
|
121
|
+
}
|
|
122
|
+
if (!isobj(state.cursors)) {
|
|
123
|
+
state.cursors = {}
|
|
124
|
+
}
|
|
125
|
+
if (!isobj(state.cooling)) {
|
|
126
|
+
state.cooling = {}
|
|
127
|
+
}
|
|
128
|
+
let saveState = () => {
|
|
129
|
+
if (useStore) {
|
|
130
|
+
try {
|
|
131
|
+
store.set(state)
|
|
132
|
+
}
|
|
133
|
+
catch {}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return { state, saveState }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 依冷卻狀態重排providers:冷卻中的條目「只降序不移除」——移到鏈尾, 前面全敗時仍會被嘗試,
|
|
142
|
+
* 故不存在把已恢復服務冰住的問題(此為與「金鑰停用清單」的關鍵差異, 後者已被否決)。
|
|
143
|
+
* 僅追蹤有明給id之條目(索引式id會因重排而錯位); 過期紀錄順手清除並寫回
|
|
144
|
+
*
|
|
145
|
+
* @param {Array} providers 輸入供應商條目陣列
|
|
146
|
+
* @param {Object} state 輸入狀態物件(取其cooling)
|
|
147
|
+
* @param {Number} cooldownMs 輸入冷卻視窗毫秒正整數
|
|
148
|
+
* @param {Function} saveState 輸入狀態寫回函數
|
|
149
|
+
* @returns {Array} 回傳重排後之條目陣列(active在前, 冷卻中殿後, 各自保持原相對順序)
|
|
150
|
+
*/
|
|
151
|
+
function reorderByCooling(providers, state, cooldownMs, saveState) {
|
|
152
|
+
let now = Date.now()
|
|
153
|
+
let act = []
|
|
154
|
+
let cool = []
|
|
155
|
+
let dirty = false
|
|
156
|
+
for (let p of providers) {
|
|
157
|
+
let pid = get(p, 'id', null)
|
|
158
|
+
let ts = isestr(pid) ? get(state.cooling, pid, null) : null
|
|
159
|
+
if (ispint(ts) && (now - ts) < cooldownMs) {
|
|
160
|
+
cool.push(p)
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
if (isestr(pid) && state.cooling[pid] !== undefined) {
|
|
164
|
+
delete state.cooling[pid] //冷卻已過期, 清除
|
|
165
|
+
dirty = true
|
|
166
|
+
}
|
|
167
|
+
act.push(p)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (dirty) {
|
|
171
|
+
saveState()
|
|
172
|
+
}
|
|
173
|
+
return [...act, ...cool]
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
88
177
|
/**
|
|
89
178
|
* 判斷失敗結果是否與「哪一把金鑰」無關(換組內金鑰必然再敗, 應整組跳過)
|
|
90
179
|
*
|
|
@@ -146,15 +235,19 @@ function isKeyIndependentFail(r) {
|
|
|
146
235
|
* @param {Array} opt.providers 輸入供應商條目物件陣列,順序即優先序。各條目除下列鍵外,其餘鍵(kind、model、exe、provider、config、sandbox、timeoutMs等)即該條目之opt原樣透傳對應轉接器
|
|
147
236
|
* @param {String} [opt.providers[].id=條目索引字串] 輸入群組識別字串,游標以此為鍵、亦為日誌標籤,本套件不解讀其內容。須區分到「模型」而非只到「廠商」(如'claude:sonnet'而非'claude'),同一模型經不同路徑取得時須帶上路徑(如'poolside:laguna-s-2.1'與'or:poolside/laguna-s-2.1:free'),且務必唯一。省略時回退為陣列索引字串——索引是位置不是身分,日後插入條目會令後續條目繼承他人游標進度,故正式設定一律明給。詳見本檔檔頭之id設計規則
|
|
148
237
|
* @param {Array} [opt.providers[].keys=[]] 輸入同一服務之多把API key字串陣列,逐次注入輪替(kind為opencode時須同時於條目給予provider),省略代表沿用CLI既有登入狀態之單一虛擬金鑰
|
|
238
|
+
* @param {*} [opt.providers[].meta=undefined] 輸入呼叫端自有資訊(分類、標籤、註記),保留鍵保證永不轉傳對應轉接器——條目其餘鍵一律原樣轉傳,自有欄位放此鍵可與轉傳機制永久絕緣,預設undefined
|
|
149
239
|
* @param {Number} [opt.budgetMs=null] 輸入整輪遞補之時間上限毫秒正整數,剩餘預算會壓進每次呼叫之timeoutMs,預設null代表不限
|
|
150
240
|
* @param {Number} [opt.minAttemptMs=20000] 輸入單次嘗試之最低剩餘預算毫秒正整數,剩餘低於此值即停止嘗試回報budget exhausted,預設20000
|
|
151
241
|
* @param {Object} [opt.store=null] 輸入狀態持久化物件{get:()=>state,set:(state)=>{}},state內含cursors(逐群組游標)與cooling(供應商冷卻時間戳,僅cooldownMs>0時使用),省略代表用行程內記憶體(跨呼叫有效,重啟歸零)。假定單行程序列調用,並行請自行加鎖
|
|
152
|
-
* @param {Number} [opt.cooldownMs=0] 輸入供應商冷卻視窗毫秒非負整數,>0啟用:條目(限有明給id者)遭遇限流(HTTP 429,僅api
|
|
153
|
-
* @param {Function} [opt.
|
|
242
|
+
* @param {Number} [opt.cooldownMs=0] 輸入供應商冷卻視窗毫秒非負整數,>0啟用:條目(限有明給id者)遭遇限流(HTTP 429,僅api類可偵測;CLI類可經coolDetect注入判定)或逾時(TIMEOUT開頭)後,於冷卻視窗內之後續呼叫中被移至鏈尾——只降序不移除,前面全敗時仍會被嘗試,任一次成功立即解除;注意啟用時「providers順序即優先序」會被暫時重排,此即本機制之目的;預設0代表不啟用
|
|
243
|
+
* @param {Function} [opt.coolDetect=null] 輸入冷卻觸發判定函數(r)=>Boolean,收完整失敗結果物件(含stdout、stderr、code、error),回傳true即視同冷卻觸發(內建429/TIMEOUT觸發不受影響)——CLI類限流埋在stderr且各家字樣不同,簽章表由觀察到字樣的呼叫端維護,如(r)=>/FreeUsageLimitError/i.test(r.stderr||'');僅cooldownMs>0時有效,回調拋出例外視同false,預設null
|
|
244
|
+
* @param {Function} [opt.shouldStop=null] 輸入中止判定函數()=>Boolean,於每次嘗試之間檢查,回傳true即停止遞補回報ABORTED(不中止進行中之嘗試)——供呼叫端於成果已無人接收時(如客戶端斷線)止損;經工作流層原樣轉傳,中止後各後續呼叫進門即回ABORTED令整條工作流快速收束;回調拋出例外視同false,預設null
|
|
245
|
+
* @param {*} [opt.meta=undefined] 輸入呼叫端自有資訊,保留鍵保證永不轉傳各轉接器,預設undefined
|
|
246
|
+
* @param {Function} [opt.onEvent=null] 輸入事件回調函數(ev)=>{},ev.type可為'try'、'ok'、'next-key'、'skip-group'、'budget-out'、'aborted'、'cooled'(冷卻觸發,帶error與cooldownMs,僅cooldownMs>0時出現);失敗事件(next-key/skip-group)另帶errorType、stdout(被拒回覆)與stderr(錯誤輸出)供診斷,後兩者於失敗路徑已由轉接器截斷;回調拋出例外不影響主流程,預設null
|
|
154
247
|
* @param {Number} [opt.timeoutMs=300000] 輸入各attempt共用之逾時毫秒正整數,條目可覆寫,全套件統一預設300000
|
|
155
248
|
* @param {String|Function} [opt.validate=undefined] 輸入各attempt共用之stdout驗證規則,條目可覆寫,預設undefined
|
|
156
249
|
* @param {Number} [opt.maxRetries=0] 輸入各attempt共用之同家重試次數非負整數,韌性建議交給換家而非重試同一家,預設0
|
|
157
|
-
* @returns {Promise} 回傳Promise,resolve回傳結果物件,除execCli既有欄位(ok、stdout、stderr、code、error、durationMs、attempts、pid)外,追加providerId(實際使用之群組)、keyIndex(實際使用之金鑰索引,無keys時為null)、kind、model、tried(全部嘗試歷程陣列,成功時亦回傳;失敗項含stdout與stderr供診斷被拒原因)
|
|
250
|
+
* @returns {Promise} 回傳Promise,resolve回傳結果物件,除execCli既有欄位(ok、stdout、stderr、code、error、durationMs、attempts、pid)外,追加providerId(實際使用之群組)、keyIndex(實際使用之金鑰索引,無keys時為null)、kind、model、tried(全部嘗試歷程陣列,成功時亦回傳;失敗項含errorType、stdout與stderr供診斷被拒原因);失敗結果帶機器可讀之errorType(一覽見getErrorType.mjs檔頭);api類轉接器提供usage(token用量)時原樣流出於結果與tried各項,CLI類無此欄;本函數不會reject
|
|
158
251
|
* @example
|
|
159
252
|
* //need opencode, claude, codex cli in system PATH
|
|
160
253
|
*
|
|
@@ -211,87 +304,44 @@ async function dispatchAiFallback(prompt, opt = {}) {
|
|
|
211
304
|
}
|
|
212
305
|
|
|
213
306
|
//budgetMs, 無效視為不限
|
|
214
|
-
let budgetMs = get(opt, 'budgetMs', null)
|
|
215
|
-
if (!ispint(budgetMs)) {
|
|
216
|
-
budgetMs = null
|
|
217
|
-
}
|
|
218
|
-
else {
|
|
219
|
-
budgetMs = cint(budgetMs)
|
|
220
|
-
}
|
|
307
|
+
let budgetMs = castPintOr(get(opt, 'budgetMs', null), null)
|
|
221
308
|
let deadline = (budgetMs === null) ? null : Date.now() + budgetMs
|
|
222
309
|
|
|
223
310
|
//minAttemptMs, 無效回退預設20000
|
|
224
|
-
let minAttemptMs = get(opt, 'minAttemptMs', null)
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
|
|
311
|
+
let minAttemptMs = castPintOr(get(opt, 'minAttemptMs', null), DEFAULT_MIN_ATTEMPT_MS)
|
|
312
|
+
|
|
313
|
+
//state與saveState, store須同時具get與set函數才視為有效, 否則用行程內記憶體
|
|
314
|
+
let { state, saveState } = initState(get(opt, 'store', null))
|
|
315
|
+
|
|
316
|
+
//cooldownMs, 無效視為0=不啟用(現行行為零改變)
|
|
317
|
+
let cooldownMs = castPintOr(get(opt, 'cooldownMs', null), 0)
|
|
318
|
+
|
|
319
|
+
//coolDetect, 冷卻觸發之注入判定(CLI類限流簽章由呼叫端維護, 見檔頭), 僅cooldownMs>0時有意義
|
|
320
|
+
let coolDetect = get(opt, 'coolDetect', null)
|
|
321
|
+
if (!isfun(coolDetect)) {
|
|
322
|
+
coolDetect = null
|
|
230
323
|
}
|
|
231
324
|
|
|
232
|
-
//
|
|
233
|
-
let
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
325
|
+
//shouldStop, 中止判定, 於每次嘗試之間檢查; 回調拋出例外視同false(不中止), 不得中斷主流程
|
|
326
|
+
let shouldStop = get(opt, 'shouldStop', null)
|
|
327
|
+
if (!isfun(shouldStop)) {
|
|
328
|
+
shouldStop = null
|
|
329
|
+
}
|
|
330
|
+
let stopRequested = () => {
|
|
331
|
+
if (shouldStop === null) {
|
|
332
|
+
return false
|
|
333
|
+
}
|
|
237
334
|
try {
|
|
238
|
-
|
|
335
|
+
return shouldStop() === true
|
|
239
336
|
}
|
|
240
|
-
catch {
|
|
241
|
-
|
|
242
|
-
if (!isobj(state)) {
|
|
243
|
-
state = useStore ? { cursors: {} } : memoryState
|
|
244
|
-
}
|
|
245
|
-
if (!isobj(state.cursors)) {
|
|
246
|
-
state.cursors = {}
|
|
247
|
-
}
|
|
248
|
-
if (!isobj(state.cooling)) {
|
|
249
|
-
state.cooling = {}
|
|
250
|
-
}
|
|
251
|
-
let saveState = () => {
|
|
252
|
-
if (useStore) {
|
|
253
|
-
try {
|
|
254
|
-
store.set(state)
|
|
255
|
-
}
|
|
256
|
-
catch {}
|
|
337
|
+
catch {
|
|
338
|
+
return false
|
|
257
339
|
}
|
|
258
340
|
}
|
|
259
341
|
|
|
260
|
-
|
|
261
|
-
let cooldownMs = get(opt, 'cooldownMs', null)
|
|
262
|
-
if (!ispint(cooldownMs)) {
|
|
263
|
-
cooldownMs = 0
|
|
264
|
-
}
|
|
265
|
-
else {
|
|
266
|
-
cooldownMs = cint(cooldownMs)
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
//供應商冷卻: 冷卻中的條目「只降序不移除」——移到鏈尾, 前面全敗時仍會被嘗試,
|
|
270
|
-
//故不存在把已恢復服務冰住的問題(此為與「金鑰停用清單」的關鍵差異, 後者已被否決)。
|
|
271
|
-
//僅追蹤有明給id之條目(索引式id會因重排而錯位); 過期項順手清除
|
|
342
|
+
//供應商冷卻: 冷卻中的條目降至鏈尾(細節見reorderByCooling)
|
|
272
343
|
if (cooldownMs > 0) {
|
|
273
|
-
|
|
274
|
-
let act = []
|
|
275
|
-
let cool = []
|
|
276
|
-
let dirty = false
|
|
277
|
-
for (let p of providers) {
|
|
278
|
-
let pid = get(p, 'id', null)
|
|
279
|
-
let ts = isestr(pid) ? get(state.cooling, pid, null) : null
|
|
280
|
-
if (ispint(ts) && (now - ts) < cooldownMs) {
|
|
281
|
-
cool.push(p)
|
|
282
|
-
}
|
|
283
|
-
else {
|
|
284
|
-
if (isestr(pid) && state.cooling[pid] !== undefined) {
|
|
285
|
-
delete state.cooling[pid] //冷卻已過期, 清除
|
|
286
|
-
dirty = true
|
|
287
|
-
}
|
|
288
|
-
act.push(p)
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
providers = [...act, ...cool]
|
|
292
|
-
if (dirty) {
|
|
293
|
-
saveState()
|
|
294
|
-
}
|
|
344
|
+
providers = reorderByCooling(providers, state, cooldownMs, saveState)
|
|
295
345
|
}
|
|
296
346
|
|
|
297
347
|
//onEvent, 回調拋出例外不得中斷主流程
|
|
@@ -347,6 +397,13 @@ async function dispatchAiFallback(prompt, opt = {}) {
|
|
|
347
397
|
let skipGroup = false
|
|
348
398
|
for (let a = 0; a < nAttempts && !skipGroup; a++) {
|
|
349
399
|
|
|
400
|
+
//中止檢查(嘗試邊界): 成果已無人接收時止損, 不中止進行中之嘗試(見檔頭【中止】)
|
|
401
|
+
if (stopRequested()) {
|
|
402
|
+
emit({ type: 'aborted', providerId: id, keyIndex: null, keyId: id })
|
|
403
|
+
tried.push({ providerId: id, keyIndex: null, keyId: id, outcome: 'aborted' })
|
|
404
|
+
return { ...getErrorResult('ABORTED', 'aborted'), tried }
|
|
405
|
+
}
|
|
406
|
+
|
|
350
407
|
//keyIndex, 無keys時為null
|
|
351
408
|
let keyIndex = (nk > 0) ? (cursor + a) % nk : null
|
|
352
409
|
let keyId = (keyIndex === null) ? id : `${id}#${keyIndex}`
|
|
@@ -358,13 +415,7 @@ async function dispatchAiFallback(prompt, opt = {}) {
|
|
|
358
415
|
}
|
|
359
416
|
|
|
360
417
|
//有效timeout, 有預算時以剩餘預算封頂
|
|
361
|
-
let effTimeout = get(attemptOpt, 'timeoutMs', null)
|
|
362
|
-
if (!ispint(effTimeout)) {
|
|
363
|
-
effTimeout = DEFAULT_TIMEOUT_MS
|
|
364
|
-
}
|
|
365
|
-
else {
|
|
366
|
-
effTimeout = cint(effTimeout)
|
|
367
|
-
}
|
|
418
|
+
let effTimeout = castPintOr(get(attemptOpt, 'timeoutMs', null), DEFAULT_TIMEOUT_MS)
|
|
368
419
|
if (deadline !== null) {
|
|
369
420
|
let remaining = deadline - Date.now()
|
|
370
421
|
|
|
@@ -372,7 +423,7 @@ async function dispatchAiFallback(prompt, opt = {}) {
|
|
|
372
423
|
if (remaining < minAttemptMs) {
|
|
373
424
|
emit({ type: 'budget-out', providerId: id, keyIndex, keyId, remainingMs: remaining })
|
|
374
425
|
tried.push({ providerId: id, keyIndex, keyId, outcome: 'budget-out' })
|
|
375
|
-
return { ...getErrorResult('budget exhausted'), tried }
|
|
426
|
+
return { ...getErrorResult('budget exhausted', 'budget'), tried }
|
|
376
427
|
}
|
|
377
428
|
effTimeout = Math.min(effTimeout, remaining)
|
|
378
429
|
}
|
|
@@ -393,20 +444,28 @@ async function dispatchAiFallback(prompt, opt = {}) {
|
|
|
393
444
|
saveState()
|
|
394
445
|
}
|
|
395
446
|
emit({ type: 'ok', providerId: id, keyIndex, keyId, durationMs: r.durationMs })
|
|
396
|
-
tried.push({ providerId: id, keyIndex, keyId, outcome: 'ok', durationMs: r.durationMs })
|
|
447
|
+
tried.push({ providerId: id, keyIndex, keyId, outcome: 'ok', durationMs: r.durationMs, ...(r.usage !== undefined ? { usage: r.usage } : {}) })
|
|
397
448
|
return { ...r, providerId: id, keyIndex, kind, model, tried }
|
|
398
449
|
}
|
|
399
450
|
|
|
400
451
|
//失敗分流
|
|
401
452
|
lastResult = r
|
|
402
453
|
lastMeta = { providerId: id, keyIndex, kind, model }
|
|
403
|
-
//冷卻觸發:
|
|
404
|
-
//其餘失敗(金鑰無效/服務端錯誤)已有換金鑰換家機制處理,
|
|
454
|
+
//冷卻觸發: 內建為限流(HTTP 429, 僅api類可偵測)與逾時(TIMEOUT開頭, CLI與api皆可)——
|
|
455
|
+
//其餘失敗(金鑰無效/服務端錯誤)已有換金鑰換家機制處理, 納入冷卻反而誤傷;
|
|
456
|
+
//CLI類限流簽章經coolDetect注入判定(呼叫端維護, 見檔頭), 拋出例外視同false
|
|
405
457
|
if (cooldownMs > 0 && idExplicit) {
|
|
406
458
|
let isCoolTrigger = (r.code === 429) || (isestr(r.error) && r.error.indexOf('TIMEOUT') === 0)
|
|
459
|
+
if (!isCoolTrigger && coolDetect !== null) {
|
|
460
|
+
try {
|
|
461
|
+
isCoolTrigger = coolDetect(r) === true
|
|
462
|
+
}
|
|
463
|
+
catch {}
|
|
464
|
+
}
|
|
407
465
|
if (isCoolTrigger) {
|
|
408
466
|
state.cooling[id] = Date.now()
|
|
409
467
|
saveState()
|
|
468
|
+
emit({ type: 'cooled', providerId: id, keyIndex, keyId, error: r.error, cooldownMs })
|
|
410
469
|
}
|
|
411
470
|
}
|
|
412
471
|
|
|
@@ -415,22 +474,22 @@ async function dispatchAiFallback(prompt, opt = {}) {
|
|
|
415
474
|
if (isKeyIndependentFail(r)) {
|
|
416
475
|
|
|
417
476
|
//與金鑰無關, 整組跳過
|
|
418
|
-
emit({ type: 'skip-group', providerId: id, keyIndex, keyId, error: r.error, stdout: r.stdout, stderr: r.stderr })
|
|
419
|
-
tried.push({ providerId: id, keyIndex, keyId, outcome: 'skip-group', error: r.error, stdout: r.stdout, stderr: r.stderr, durationMs: r.durationMs })
|
|
477
|
+
emit({ type: 'skip-group', providerId: id, keyIndex, keyId, error: r.error, errorType: r.errorType, stdout: r.stdout, stderr: r.stderr })
|
|
478
|
+
tried.push({ providerId: id, keyIndex, keyId, outcome: 'skip-group', error: r.error, errorType: r.errorType, stdout: r.stdout, stderr: r.stderr, durationMs: r.durationMs, ...(r.usage !== undefined ? { usage: r.usage } : {}) })
|
|
420
479
|
skipGroup = true
|
|
421
480
|
}
|
|
422
481
|
else {
|
|
423
482
|
|
|
424
483
|
//其餘(含額度上限/金鑰無效/未分類), 換組內下一把, 不記憶不停用
|
|
425
|
-
emit({ type: 'next-key', providerId: id, keyIndex, keyId, error: r.error, stdout: r.stdout, stderr: r.stderr })
|
|
426
|
-
tried.push({ providerId: id, keyIndex, keyId, outcome: 'next-key', error: r.error, stdout: r.stdout, stderr: r.stderr, durationMs: r.durationMs })
|
|
484
|
+
emit({ type: 'next-key', providerId: id, keyIndex, keyId, error: r.error, errorType: r.errorType, stdout: r.stdout, stderr: r.stderr })
|
|
485
|
+
tried.push({ providerId: id, keyIndex, keyId, outcome: 'next-key', error: r.error, errorType: r.errorType, stdout: r.stdout, stderr: r.stderr, durationMs: r.durationMs, ...(r.usage !== undefined ? { usage: r.usage } : {}) })
|
|
427
486
|
}
|
|
428
487
|
|
|
429
488
|
}
|
|
430
489
|
}
|
|
431
490
|
|
|
432
|
-
//全數失敗,
|
|
433
|
-
let r = lastResult || getErrorResult('all providers failed')
|
|
491
|
+
//全數失敗, 回傳最後一筆失敗結果(含其errorType)與完整歷程
|
|
492
|
+
let r = lastResult || getErrorResult('all providers failed', 'exec')
|
|
434
493
|
return { ...r, ...(lastMeta || {}), tried }
|
|
435
494
|
}
|
|
436
495
|
|
|
@@ -3,11 +3,11 @@ import omit from 'lodash-es/omit.js'
|
|
|
3
3
|
import isarr from 'wsemi/src/isarr.mjs'
|
|
4
4
|
import isbol from 'wsemi/src/isbol.mjs'
|
|
5
5
|
import isestr from 'wsemi/src/isestr.mjs'
|
|
6
|
-
import ispint from 'wsemi/src/ispint.mjs'
|
|
7
|
-
import cint from 'wsemi/src/cint.mjs'
|
|
8
6
|
import execCli from 'wsemi/src/execCli.mjs'
|
|
9
7
|
import getCliArgs from './getCliArgs.mjs'
|
|
10
8
|
import getErrorResult from './getErrorResult.mjs'
|
|
9
|
+
import { attachErrorType } from './getErrorType.mjs'
|
|
10
|
+
import castPintOr from './castPintOr.mjs'
|
|
11
11
|
import dfTimeoutMs from './dfTimeoutMs.mjs'
|
|
12
12
|
|
|
13
13
|
|
|
@@ -134,13 +134,7 @@ async function dispatchAntigravity(prompt, opt = {}) {
|
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
//timeoutMs, 先行取值以供printTimeout推導, 無效回退全套件統一預設300000
|
|
137
|
-
let timeoutMs = get(opt, 'timeoutMs', null)
|
|
138
|
-
if (!ispint(timeoutMs)) {
|
|
139
|
-
timeoutMs = DEFAULT_TIMEOUT_MS
|
|
140
|
-
}
|
|
141
|
-
else {
|
|
142
|
-
timeoutMs = cint(timeoutMs)
|
|
143
|
-
}
|
|
137
|
+
let timeoutMs = castPintOr(get(opt, 'timeoutMs', null), DEFAULT_TIMEOUT_MS)
|
|
144
138
|
|
|
145
139
|
//printTimeout, 未給時由timeoutMs推導並預留緩衝, 令agy先於外層逾時而回報自身錯誤訊息
|
|
146
140
|
let printTimeout = get(opt, 'printTimeout', null)
|
|
@@ -187,15 +181,17 @@ async function dispatchAntigravity(prompt, opt = {}) {
|
|
|
187
181
|
let optCli = omit(opt, OWN_KEYS)
|
|
188
182
|
|
|
189
183
|
//execCli, 不帶input(agy由--print取得prompt); try/catch兜底轉義膨脹等
|
|
190
|
-
//前置檢查漏網之spawn同步拋出(如ENAMETOOLONG), 維持不reject
|
|
184
|
+
//前置檢查漏網之spawn同步拋出(如ENAMETOOLONG), 維持不reject契約;
|
|
185
|
+
//失敗結果補上機器可讀之errorType(僅機械可判者, 見getErrorType.mjs)
|
|
191
186
|
try {
|
|
192
|
-
|
|
187
|
+
let r = await execCli(exe, args, {
|
|
193
188
|
...optCli,
|
|
194
189
|
timeoutMs,
|
|
195
190
|
})
|
|
191
|
+
return attachErrorType(r)
|
|
196
192
|
}
|
|
197
193
|
catch (err) {
|
|
198
|
-
return getErrorResult(`${err.code || 'UNKNOWN'}: ${err.message}
|
|
194
|
+
return getErrorResult(`${err.code || 'UNKNOWN'}: ${err.message}`, 'spawn')
|
|
199
195
|
}
|
|
200
196
|
}
|
|
201
197
|
|
|
@@ -3,10 +3,10 @@ import isobj from 'wsemi/src/isobj.mjs'
|
|
|
3
3
|
import isfun from 'wsemi/src/isfun.mjs'
|
|
4
4
|
import isnum from 'wsemi/src/isnum.mjs'
|
|
5
5
|
import isestr from 'wsemi/src/isestr.mjs'
|
|
6
|
-
import ispint from 'wsemi/src/ispint.mjs'
|
|
7
6
|
import isp0int from 'wsemi/src/isp0int.mjs'
|
|
8
7
|
import cint from 'wsemi/src/cint.mjs'
|
|
9
8
|
import delay from 'wsemi/src/delay.mjs'
|
|
9
|
+
import castPintOr from './castPintOr.mjs'
|
|
10
10
|
import strleft from 'wsemi/src/strleft.mjs'
|
|
11
11
|
import strdelleft from 'wsemi/src/strdelleft.mjs'
|
|
12
12
|
import strTruncate from 'wsemi/src/strTruncate.mjs'
|
|
@@ -44,7 +44,11 @@ import dfTimeoutMs from './dfTimeoutMs.mjs'
|
|
|
44
44
|
// 【重試語意對齊execCli】4xx(429除外)為客戶端錯誤不可重試而立即中止;
|
|
45
45
|
// 429/5xx/網路錯誤/逾時依maxRetries線性退避重試(間隔retryDelayMs*次數, 上限15000ms)。
|
|
46
46
|
//
|
|
47
|
-
// 【結果結構對齊execCli】{ ok, stdout, stderr, code, error, durationMs, attempts },
|
|
47
|
+
// 【結果結構對齊execCli】{ ok, stdout, stderr, code, error, durationMs, attempts, usage },
|
|
48
|
+
// usage為原始回應之token用量原樣透傳(無則null; 驗證失敗等已耗token之失敗亦帶出),
|
|
49
|
+
// CLI類轉接器無可靠來源故無此欄——呼叫端可據此把「真實用量」與「只能估算」分開處理。
|
|
50
|
+
// 失敗結果另帶機器可讀之errorType(timeout/fetch/http/tool-unsupported/invalid-response/
|
|
51
|
+
// validation/params, 一覽見getErrorType.mjs檔頭), error字串保留不動, 兩者並存。
|
|
48
52
|
// stdout為回覆內容、code為HTTP狀態碼(網路錯誤與逾時為null)、逾時error以TIMEOUT開頭、
|
|
49
53
|
// 驗證失敗error為OUTPUT_VALIDATION_FAILED——故dispatchAiFallback之失敗分流
|
|
50
54
|
// (TIMEOUT/驗證失敗跳組, 其餘換金鑰)對本轉接器同樣成立, 無須任何修改。
|
|
@@ -138,6 +142,19 @@ async function callOnce(url, headers, body, timeoutMs, validator) {
|
|
|
138
142
|
|
|
139
143
|
let t0 = Date.now()
|
|
140
144
|
|
|
145
|
+
//mkResult, 結果形狀之單一來源(欄位對齊execCli, 追加errorType與usage),
|
|
146
|
+
//durationMs於呼叫當下計算; 失敗分支各自給errorType, 成功分支不帶(僅失敗結果有此欄)
|
|
147
|
+
let mkResult = (patch) => ({
|
|
148
|
+
ok: false,
|
|
149
|
+
stdout: '',
|
|
150
|
+
stderr: '',
|
|
151
|
+
code: null,
|
|
152
|
+
error: '',
|
|
153
|
+
durationMs: Date.now() - t0,
|
|
154
|
+
usage: null,
|
|
155
|
+
...patch,
|
|
156
|
+
})
|
|
157
|
+
|
|
141
158
|
//AbortController, 逾時中止(含回應本體之串流讀取)
|
|
142
159
|
let controller = new AbortController()
|
|
143
160
|
let timer = setTimeout(() => {
|
|
@@ -157,81 +174,65 @@ async function callOnce(url, headers, body, timeoutMs, validator) {
|
|
|
157
174
|
}
|
|
158
175
|
catch (err) {
|
|
159
176
|
clearTimeout(timer)
|
|
160
|
-
let durationMs = Date.now() - t0
|
|
161
177
|
|
|
162
178
|
//逾時, error以TIMEOUT開頭令dispatchAiFallback視為與金鑰無關而跳組
|
|
163
179
|
if (err.name === 'AbortError') {
|
|
164
|
-
return {
|
|
165
|
-
ok: false,
|
|
166
|
-
stdout: '',
|
|
167
|
-
stderr: '',
|
|
168
|
-
code: null,
|
|
169
|
-
error: `TIMEOUT after ${timeoutMs / 1000}s`,
|
|
170
|
-
durationMs,
|
|
171
|
-
}
|
|
180
|
+
return mkResult({ error: `TIMEOUT after ${timeoutMs / 1000}s`, errorType: 'timeout' })
|
|
172
181
|
}
|
|
173
182
|
|
|
174
183
|
//網路層錯誤(DNS/連線拒絕等)
|
|
175
184
|
let cause = get(err, 'cause.code', '') || err.message
|
|
176
|
-
return {
|
|
177
|
-
ok: false,
|
|
178
|
-
stdout: '',
|
|
179
|
-
stderr: '',
|
|
180
|
-
code: null,
|
|
181
|
-
error: `FETCH_ERROR: ${cause}`,
|
|
182
|
-
durationMs,
|
|
183
|
-
}
|
|
185
|
+
return mkResult({ error: `FETCH_ERROR: ${cause}`, errorType: 'fetch' })
|
|
184
186
|
}
|
|
185
187
|
clearTimeout(timer)
|
|
186
188
|
|
|
187
|
-
let durationMs = Date.now() - t0
|
|
188
|
-
|
|
189
189
|
//HTTP非2xx, 原始回應本體放stderr供除錯與分類
|
|
190
190
|
if (!res.ok) {
|
|
191
|
-
return {
|
|
192
|
-
ok: false,
|
|
193
|
-
stdout: '',
|
|
191
|
+
return mkResult({
|
|
194
192
|
stderr: strTruncate(txt, 1000, optTruncate),
|
|
195
193
|
code: res.status,
|
|
196
194
|
error: `HTTP ${res.status}`,
|
|
197
|
-
|
|
198
|
-
}
|
|
195
|
+
errorType: 'http',
|
|
196
|
+
})
|
|
199
197
|
}
|
|
200
198
|
|
|
201
|
-
//取出choices[0]
|
|
199
|
+
//取出choices[0]與usage(token用量, 原樣透傳; 失敗回應亦可能已耗token, 一併帶出)
|
|
202
200
|
let content = null
|
|
203
201
|
let finishReason = ''
|
|
204
202
|
let toolCalls = null
|
|
203
|
+
let usage = null
|
|
205
204
|
try {
|
|
206
205
|
let j = JSON.parse(txt)
|
|
207
206
|
content = get(j, 'choices.0.message.content', null)
|
|
208
207
|
finishReason = get(j, 'choices.0.finish_reason', '')
|
|
209
208
|
toolCalls = get(j, 'choices.0.message.tool_calls', null)
|
|
209
|
+
usage = get(j, 'usage', null)
|
|
210
|
+
if (!isobj(usage)) {
|
|
211
|
+
usage = null
|
|
212
|
+
}
|
|
210
213
|
}
|
|
211
214
|
catch {}
|
|
212
215
|
|
|
213
216
|
//tool_calls, 本轉接器不支援工具迴圈(見檔頭), 明確回報而不假裝成功
|
|
214
217
|
//(Agnes於tool_calls時content為"\n\n"非null, 不攔截會靜默回傳空白內容)
|
|
215
218
|
if (finishReason === 'tool_calls' || (toolCalls !== null && toolCalls !== undefined)) {
|
|
216
|
-
return {
|
|
217
|
-
ok: false,
|
|
218
|
-
stdout: '',
|
|
219
|
+
return mkResult({
|
|
219
220
|
stderr: strTruncate(txt, 1000, optTruncate),
|
|
220
221
|
code: res.status,
|
|
221
222
|
error: 'TOOL_CALLS_UNSUPPORTED: use a cli kind (opencode/claude/codex/antigravity) when tools are needed',
|
|
222
|
-
|
|
223
|
-
|
|
223
|
+
errorType: 'tool-unsupported',
|
|
224
|
+
usage,
|
|
225
|
+
})
|
|
224
226
|
}
|
|
225
227
|
|
|
226
228
|
if (content === null || content === undefined) {
|
|
227
|
-
return {
|
|
228
|
-
ok: false,
|
|
229
|
-
stdout: '',
|
|
229
|
+
return mkResult({
|
|
230
230
|
stderr: strTruncate(txt, 500, optTruncate),
|
|
231
231
|
code: res.status,
|
|
232
232
|
error: 'INVALID_RESPONSE: missing choices[0].message.content',
|
|
233
|
-
|
|
234
|
-
|
|
233
|
+
errorType: 'invalid-response',
|
|
234
|
+
usage,
|
|
235
|
+
})
|
|
235
236
|
}
|
|
236
237
|
if (typeof content !== 'string') {
|
|
237
238
|
content = JSON.stringify(content) //少數閘道回array形態
|
|
@@ -239,24 +240,21 @@ async function callOnce(url, headers, body, timeoutMs, validator) {
|
|
|
239
240
|
|
|
240
241
|
//validator, error與execCli一致令dispatchAiFallback可統一分流
|
|
241
242
|
if (validator && !validator(content)) {
|
|
242
|
-
return {
|
|
243
|
-
ok: false,
|
|
243
|
+
return mkResult({
|
|
244
244
|
stdout: strTruncate(content, 500, optTruncate),
|
|
245
|
-
stderr: '',
|
|
246
245
|
code: res.status,
|
|
247
246
|
error: 'OUTPUT_VALIDATION_FAILED',
|
|
248
|
-
|
|
249
|
-
|
|
247
|
+
errorType: 'validation',
|
|
248
|
+
usage,
|
|
249
|
+
})
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
-
return {
|
|
252
|
+
return mkResult({
|
|
253
253
|
ok: true,
|
|
254
254
|
stdout: content,
|
|
255
|
-
stderr: '',
|
|
256
255
|
code: res.status,
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
}
|
|
256
|
+
usage,
|
|
257
|
+
})
|
|
260
258
|
}
|
|
261
259
|
|
|
262
260
|
|
|
@@ -287,7 +285,7 @@ async function callOnce(url, headers, body, timeoutMs, validator) {
|
|
|
287
285
|
* @param {String|Function} [opt.validate=undefined] 輸入回覆內容驗證規則字串或自訂驗證函數,規則字串支援'nonempty'、'json'、'min:100',多規則可用逗號串接,預設undefined代表不驗證
|
|
288
286
|
* @param {Number} [opt.maxRetries=0] 輸入失敗後最大重試次數非負整數,4xx(429除外)不重試,預設0
|
|
289
287
|
* @param {Number} [opt.retryDelayMs=5000] 輸入重試間隔毫秒正整數,實際間隔為retryDelayMs乘以重試次數且上限15000ms,預設5000
|
|
290
|
-
* @returns {Promise} 回傳Promise,resolve回傳結果物件,內含ok(是否成功布林值)、stdout(回覆內容字串)、stderr(失敗時之原始回應本體)、code(HTTP狀態碼,網路錯誤與逾時為null)、error(錯誤訊息字串,成功時為空字串)、durationMs(耗時毫秒)、attempts(實際嘗試次數),本函數不會reject
|
|
288
|
+
* @returns {Promise} 回傳Promise,resolve回傳結果物件,內含ok(是否成功布林值)、stdout(回覆內容字串)、stderr(失敗時之原始回應本體)、code(HTTP狀態碼,網路錯誤與逾時為null)、error(錯誤訊息字串,成功時為空字串)、errorType(僅失敗時,機器可讀錯誤類別字串,一覽見getErrorType.mjs檔頭)、durationMs(耗時毫秒)、attempts(實際嘗試次數)、usage(原始回應之token用量物件原樣透傳,無則null),本函數不會reject
|
|
291
289
|
* @example
|
|
292
290
|
* //need network, no cli required
|
|
293
291
|
*
|
|
@@ -362,13 +360,7 @@ async function dispatchApiOpenaiCompat(prompt, opt = {}) {
|
|
|
362
360
|
}
|
|
363
361
|
|
|
364
362
|
//timeoutMs
|
|
365
|
-
let timeoutMs = get(opt, 'timeoutMs', null)
|
|
366
|
-
if (!ispint(timeoutMs)) {
|
|
367
|
-
timeoutMs = DEFAULT_TIMEOUT_MS
|
|
368
|
-
}
|
|
369
|
-
else {
|
|
370
|
-
timeoutMs = cint(timeoutMs)
|
|
371
|
-
}
|
|
363
|
+
let timeoutMs = castPintOr(get(opt, 'timeoutMs', null), DEFAULT_TIMEOUT_MS)
|
|
372
364
|
|
|
373
365
|
//maxRetries
|
|
374
366
|
let maxRetries = get(opt, 'maxRetries', null)
|
|
@@ -380,13 +372,7 @@ async function dispatchApiOpenaiCompat(prompt, opt = {}) {
|
|
|
380
372
|
}
|
|
381
373
|
|
|
382
374
|
//retryDelayMs
|
|
383
|
-
let retryDelayMs = get(opt, 'retryDelayMs', null)
|
|
384
|
-
if (!ispint(retryDelayMs)) {
|
|
385
|
-
retryDelayMs = DEFAULT_RETRY_DELAY_MS
|
|
386
|
-
}
|
|
387
|
-
else {
|
|
388
|
-
retryDelayMs = cint(retryDelayMs)
|
|
389
|
-
}
|
|
375
|
+
let retryDelayMs = castPintOr(get(opt, 'retryDelayMs', null), DEFAULT_RETRY_DELAY_MS)
|
|
390
376
|
|
|
391
377
|
//validator
|
|
392
378
|
let validator = buildValidator(get(opt, 'validate', null))
|