w-dispatch-ai 1.0.6 → 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.
Files changed (51) hide show
  1. package/README.md +31 -2
  2. package/dist/w-dispatch-ai.umd.js +2 -2
  3. package/dist/w-dispatch-ai.umd.js.map +1 -1
  4. package/docs/WDispatchAi.mjs.html +4 -3
  5. package/docs/adapters.mjs.html +2 -2
  6. package/docs/castPintOr.mjs.html +109 -0
  7. package/docs/dfTimeoutMs.mjs.html +2 -2
  8. package/docs/dispatchAi.mjs.html +14 -13
  9. package/docs/dispatchAiFallback.mjs.html +182 -58
  10. package/docs/dispatchAiWkf.mjs.html +2 -2
  11. package/docs/dispatchAntigravity.mjs.html +10 -14
  12. package/docs/dispatchApiOpenaiCompat.mjs.html +50 -64
  13. package/docs/dispatchClaude.mjs.html +8 -13
  14. package/docs/dispatchCodex.mjs.html +8 -13
  15. package/docs/dispatchOpencode.mjs.html +8 -13
  16. package/docs/getCliArgs.mjs.html +2 -2
  17. package/docs/getErrorResult.mjs.html +12 -5
  18. package/docs/getErrorType.mjs.html +171 -0
  19. package/docs/global.html +1336 -109
  20. package/docs/index.html +2 -2
  21. package/docs/resolveProviders.mjs.html +2 -2
  22. package/docs/wkf_callAiWithFallback.mjs.html +14 -10
  23. package/docs/wkf_extractJsonLoose.mjs.html +2 -2
  24. package/docs/wkf_runFanout.mjs.html +12 -9
  25. package/docs/wkf_runFanoutPipeline.mjs.html +15 -19
  26. package/docs/wkf_runRolePipeline.mjs.html +5 -4
  27. package/package.json +1 -1
  28. package/src/WDispatchAi.mjs +2 -1
  29. package/src/castPintOr.mjs +37 -0
  30. package/src/dispatchAi.mjs +12 -11
  31. package/src/dispatchAiFallback.mjs +180 -56
  32. package/src/dispatchAntigravity.mjs +8 -12
  33. package/src/dispatchApiOpenaiCompat.mjs +48 -62
  34. package/src/dispatchClaude.mjs +6 -11
  35. package/src/dispatchCodex.mjs +6 -11
  36. package/src/dispatchOpencode.mjs +6 -11
  37. package/src/getErrorResult.mjs +10 -3
  38. package/src/getErrorType.mjs +99 -0
  39. package/src/wkf/callAiWithFallback.mjs +12 -8
  40. package/src/wkf/runFanout.mjs +10 -7
  41. package/src/wkf/runFanoutPipeline.mjs +13 -17
  42. package/src/wkf/runRolePipeline.mjs +3 -2
  43. package/test/tools/fakeServerForApiTest.mjs +4 -1
  44. package/test/unit-callAiWithFallback.test.mjs +24 -0
  45. package/test/unit-castPintOr.test.mjs +28 -0
  46. package/test/unit-dispatchAiFallback.test.mjs +232 -0
  47. package/test/unit-dispatchApiOpenaiCompat.test.mjs +67 -0
  48. package/test/unit-getErrorResult.test.mjs +13 -1
  49. package/test/unit-getErrorType.test.mjs +36 -0
  50. package/test/unit-runFanout.test.mjs +60 -0
  51. package/test/unit-runRolePipeline.test.mjs +12 -0
@@ -29,7 +29,7 @@
29
29
  <nav >
30
30
 
31
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#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#defaultIntegratePrompt">defaultIntegratePrompt</a></li><li><a href="global.html#dfTimeoutMs">dfTimeoutMs</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#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#getCliArgs">getCliArgs</a></li><li><a href="global.html#getErrorResult">getErrorResult</a></li><li><a href="global.html#isKeyIndependentFail">isKeyIndependentFail</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></ul>
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#attachErrorType">attachErrorType</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#defaultIntegratePrompt">defaultIntegratePrompt</a></li><li><a href="global.html#dfTimeoutMs">dfTimeoutMs</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#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#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#initState">initState</a></li><li><a href="global.html#isKeyIndependentFail">isKeyIndependentFail</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></ul>
33
33
 
34
34
  </nav>
35
35
 
@@ -85,7 +85,7 @@ export default dfTimeoutMs
85
85
  <br class="clear">
86
86
 
87
87
  <footer>
88
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Aug 14 2026 12:30:07 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
88
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sat Aug 15 2026 08:45:29 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
89
89
  </footer>
90
90
 
91
91
  <script>prettyPrint();</script>
@@ -29,7 +29,7 @@
29
29
  <nav >
30
30
 
31
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#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#defaultIntegratePrompt">defaultIntegratePrompt</a></li><li><a href="global.html#dfTimeoutMs">dfTimeoutMs</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#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#getCliArgs">getCliArgs</a></li><li><a href="global.html#getErrorResult">getErrorResult</a></li><li><a href="global.html#isKeyIndependentFail">isKeyIndependentFail</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></ul>
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#attachErrorType">attachErrorType</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#defaultIntegratePrompt">defaultIntegratePrompt</a></li><li><a href="global.html#dfTimeoutMs">dfTimeoutMs</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#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#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#initState">initState</a></li><li><a href="global.html#isKeyIndependentFail">isKeyIndependentFail</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></ul>
33
33
 
34
34
  </nav>
35
35
 
@@ -53,18 +53,19 @@ import getErrorResult from './getErrorResult.mjs'
53
53
 
54
54
 
55
55
  /**
56
- * 依供應商種類(kind)分派至對應之CLI轉接器
56
+ * 依供應商種類(kind)分派至對應之轉接器
57
57
  *
58
- * 三種供應商的差異(2026-08-08於本機實測確認):
59
- * opencode支援逐次注入金鑰(OPENCODE_AUTH_CONTENT),故可多把金鑰輪替;
60
- * claudecodex則沿用CLI既有登入狀態,無逐次金鑰概念。
58
+ * kind清單以adapters.mjs對照表為唯一來源(目前為'opencode'、'claude'、'codex'、'antigravity'、
59
+ * 'api-openai-compat',CLI或API之選型判準見adapters.mjs檔頭)
60
+ * 各家金鑰模式不同(2026-08-08起於本機實測確認):opencodeapi-openai-compat支援逐次注入金鑰,
61
+ * 故可多把金鑰輪替;claude/codex/antigravity沿用CLI既有登入狀態,無逐次金鑰概念。
61
62
  * 故「輪替」的單位是「供應商條目」而非單純的金鑰:一個條目即一組(kind, model, 可選的key/provider),
62
- * 輪到誰就用誰的CLI與模型
63
+ * 輪到誰就用誰的轉接器與模型
63
64
  *
64
- * @param {String} kind 輸入供應商種類字串,可選'opencode'、'claude'、'codex'
65
- * @param {String} prompt 輸入提示詞字串,一律以stdin傳入子進程
66
- * @param {Object} [opt={}] 輸入設定物件,原樣轉傳對應轉接器,各轉接器可用設定詳見dispatchOpencode、dispatchClaude、dispatchCodex,預設{}
67
- * @returns {Promise} 回傳Promise,resolve回傳結果物件,內含ok(是否成功布林值)、stdout(標準輸出字串)、stderr(標準錯誤字串)、code(離開碼)、error(錯誤訊息字串,成功時為空字串)、durationMs(耗時毫秒)、attempts(實際嘗試次數),本函數不會reject
65
+ * @param {String} kind 輸入供應商種類字串,須為adapters.mjs對照表之鍵名,目前可選'opencode'、'claude'、'codex'、'antigravity'、'api-openai-compat'
66
+ * @param {String} prompt 輸入提示詞字串
67
+ * @param {Object} [opt={}] 輸入設定物件,原樣轉傳對應轉接器,各轉接器可用設定詳見dispatchOpencode、dispatchClaude、dispatchCodex、dispatchAntigravity、dispatchApiOpenaiCompat,預設{}
68
+ * @returns {Promise} 回傳Promise,resolve回傳結果物件,內含ok(是否成功布林值)、stdout(標準輸出字串)、stderr(標準錯誤字串)、code(離開碼)、error(錯誤訊息字串,成功時為空字串)、errorType(僅失敗時,機器可讀錯誤類別字串)、durationMs(耗時毫秒)、attempts(實際嘗試次數),本函數不會reject
68
69
  * @example
69
70
  * //need claude, codex or opencode cli in system PATH
70
71
  *
@@ -77,8 +78,8 @@ import getErrorResult from './getErrorResult.mjs'
77
78
  * // => true '完成'
78
79
  *
79
80
  * let re = await dispatchAi('gemini', 'abc')
80
- * console.log(re.ok, re.error)
81
- * // => false 'unknown ai kind: "gemini" (available: opencode, claude, codex)'
81
+ * console.log(re.ok, re.error.indexOf('unknown ai kind: "gemini"') === 0)
82
+ * // => false true
82
83
  *
83
84
  * }
84
85
  * await test()
@@ -120,7 +121,7 @@ export default dispatchAi
120
121
  <br class="clear">
121
122
 
122
123
  <footer>
123
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Aug 14 2026 12:30:07 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
124
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sat Aug 15 2026 08:45:29 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
124
125
  </footer>
125
126
 
126
127
  <script>prettyPrint();</script>
@@ -29,7 +29,7 @@
29
29
  <nav >
30
30
 
31
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#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#defaultIntegratePrompt">defaultIntegratePrompt</a></li><li><a href="global.html#dfTimeoutMs">dfTimeoutMs</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#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#getCliArgs">getCliArgs</a></li><li><a href="global.html#getErrorResult">getErrorResult</a></li><li><a href="global.html#isKeyIndependentFail">isKeyIndependentFail</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></ul>
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#attachErrorType">attachErrorType</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#defaultIntegratePrompt">defaultIntegratePrompt</a></li><li><a href="global.html#dfTimeoutMs">dfTimeoutMs</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#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#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#initState">initState</a></li><li><a href="global.html#isKeyIndependentFail">isKeyIndependentFail</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></ul>
33
33
 
34
34
  </nav>
35
35
 
@@ -55,6 +55,7 @@ import ispint from 'wsemi/src/ispint.mjs'
55
55
  import cint from 'wsemi/src/cint.mjs'
56
56
  import dispatchAi from './dispatchAi.mjs'
57
57
  import getErrorResult from './getErrorResult.mjs'
58
+ import castPintOr from './castPintOr.mjs'
58
59
  import dfTimeoutMs from './dfTimeoutMs.mjs'
59
60
 
60
61
 
@@ -74,6 +75,29 @@ import dfTimeoutMs from './dfTimeoutMs.mjs'
74
75
  // 【跨次記憶只有游標】成功後游標推進至下一把, 令額度在同組多把金鑰間自動均攤;
75
76
  // 死金鑰的代價也被游標攤平——這輪從key1敗轉key2成功後游標停在key3, 下輪不會先碰key1。
76
77
  //
78
+ // 【供應商冷卻(選用, cooldownMs>0啟用, 預設關閉)】多階段工作流的每一階段都會從鏈首
79
+ // 重新探測同一家已失效的供應商——限流時每階段各踩一輪429、卡死時各燒一次完整逾時
80
+ // (使用端實測: 一次107秒的多階段請求中72秒耗在重複踩同一組429, 啟用冷卻後降至15秒)。
81
+ // 設計與「金鑰停用清單」(已否決)的關鍵差異: 以「條目」為單位、短視窗、且「只降序不移除」
82
+ // ——冷卻中的條目移到鏈尾而非移除, 前面全敗時照樣會被嘗試, 故不存在把已恢復服務冰住的問題;
83
+ // 任一次成功立即解除。內建觸發限於限流(HTTP 429, 僅api-openai-compat可靠偵測; CLI類之
84
+ // 限流埋在stderr文字中, 各家字樣不同且隨版本漂移, 本套件不維護簽章表)與逾時(TIMEOUT開頭,
85
+ // 各kind皆可)兩類——其餘失敗已有換金鑰換家機制處理, 納入反而誤傷。
86
+ // CLI類限流之偵測採依賴注入: 呼叫端於實測中觀察到穩定字樣時, 以coolDetect(r)=>Boolean
87
+ // 自行判定(收到完整失敗結果含stderr), 命中即視同冷卻觸發——簽章表由觀察到字樣的呼叫端
88
+ // 維護, 漏判僅退回現狀(每階段重探一次), 誤判也只是降尾非移除, 兩邊代價都有上限。
89
+ // 狀態存於state.cooling, 與cursors同走store持久化。
90
+ //
91
+ // 【中止(shouldStop)】呼叫端(如server於客戶端斷線後)可注入shouldStop()=>Boolean,
92
+ // 於「每次嘗試之間」檢查, true即停止遞補回報ABORTED——把「斷線後仍空耗整條鏈」
93
+ // 縮成「至多再耗當前這一家」。檢查點只此一處: 工作流各層經omit轉傳自動獲得,
94
+ // 中止後每個後續呼叫進門即回ABORTED, 整條工作流自然快速收束, 不需逐層實作。
95
+ // 不中止進行中之嘗試(不殺子進程/不斷開請求), 此為已知設計取捨(避免侵入execCli層)。
96
+ //
97
+ // 【meta保留鍵】「剔除自用鍵後原樣轉傳」令條目即調校點, 但呼叫端放進條目/opt的任何
98
+ // 自有欄位都會被靜默轉傳——保留meta一鍵保證永不轉傳, 呼叫端要掛分類/標籤/註記
99
+ // 一律放meta, 與轉傳機制永久絕緣(工作流各層之規格物件同此約定)。
100
+ //
77
101
  // 【時間預算】budgetMs限制整輪遞補的總時長, 剩餘預算會壓進每次呼叫的timeoutMs,
78
102
  // 防止多家連續卡逾時而撞破外部排程的執行上限。
79
103
  //
@@ -107,11 +131,12 @@ import dfTimeoutMs from './dfTimeoutMs.mjs'
107
131
 
108
132
 
109
133
  //fallback層自用之設定鍵, 其餘鍵作為各attempt之共用預設原樣轉傳
110
- let FALLBACK_KEYS = ['providers', 'budgetMs', 'minAttemptMs', 'store', 'onEvent']
134
+ //meta為保留鍵: 呼叫端掛自有資訊(分類/標籤/註記)用, 保證永不轉傳(見檔頭【meta保留鍵】)
135
+ let FALLBACK_KEYS = ['providers', 'budgetMs', 'minAttemptMs', 'cooldownMs', 'shouldStop', 'coolDetect', 'store', 'onEvent', 'meta']
111
136
 
112
137
 
113
138
  //providers條目自用之設定鍵, 其餘鍵(含kind)即該條目之opt原樣轉傳對應轉接器
114
- let ENTRY_KEYS = ['id', 'keys']
139
+ let ENTRY_KEYS = ['id', 'keys', 'meta']
115
140
 
116
141
 
117
142
  //預設值
@@ -123,6 +148,79 @@ let DEFAULT_TIMEOUT_MS = dfTimeoutMs //全套件統一預設300000
123
148
  let memoryState = { cursors: {} }
124
149
 
125
150
 
151
+ /**
152
+ * 初始化游標與冷卻狀態(store有效即載入持久化狀態, 否則用行程內記憶體)
153
+ *
154
+ * @param {Object} store 輸入狀態持久化物件{get,set},無效代表用行程內記憶體
155
+ * @returns {Object} 回傳物件,內含state(狀態物件,保證有cursors與cooling)與saveState(寫回函數,store無效或寫入失敗皆靜默)
156
+ */
157
+ function initState(store) {
158
+ let useStore = isobj(store) &amp;&amp; isfun(store.get) &amp;&amp; isfun(store.set)
159
+ let state = null
160
+ if (useStore) {
161
+ try {
162
+ state = store.get()
163
+ }
164
+ catch {}
165
+ }
166
+ if (!isobj(state)) {
167
+ state = useStore ? { cursors: {} } : memoryState
168
+ }
169
+ if (!isobj(state.cursors)) {
170
+ state.cursors = {}
171
+ }
172
+ if (!isobj(state.cooling)) {
173
+ state.cooling = {}
174
+ }
175
+ let saveState = () => {
176
+ if (useStore) {
177
+ try {
178
+ store.set(state)
179
+ }
180
+ catch {}
181
+ }
182
+ }
183
+ return { state, saveState }
184
+ }
185
+
186
+
187
+ /**
188
+ * 依冷卻狀態重排providers:冷卻中的條目「只降序不移除」——移到鏈尾, 前面全敗時仍會被嘗試,
189
+ * 故不存在把已恢復服務冰住的問題(此為與「金鑰停用清單」的關鍵差異, 後者已被否決)。
190
+ * 僅追蹤有明給id之條目(索引式id會因重排而錯位); 過期紀錄順手清除並寫回
191
+ *
192
+ * @param {Array} providers 輸入供應商條目陣列
193
+ * @param {Object} state 輸入狀態物件(取其cooling)
194
+ * @param {Number} cooldownMs 輸入冷卻視窗毫秒正整數
195
+ * @param {Function} saveState 輸入狀態寫回函數
196
+ * @returns {Array} 回傳重排後之條目陣列(active在前, 冷卻中殿後, 各自保持原相對順序)
197
+ */
198
+ function reorderByCooling(providers, state, cooldownMs, saveState) {
199
+ let now = Date.now()
200
+ let act = []
201
+ let cool = []
202
+ let dirty = false
203
+ for (let p of providers) {
204
+ let pid = get(p, 'id', null)
205
+ let ts = isestr(pid) ? get(state.cooling, pid, null) : null
206
+ if (ispint(ts) &amp;&amp; (now - ts) &lt; cooldownMs) {
207
+ cool.push(p)
208
+ }
209
+ else {
210
+ if (isestr(pid) &amp;&amp; state.cooling[pid] !== undefined) {
211
+ delete state.cooling[pid] //冷卻已過期, 清除
212
+ dirty = true
213
+ }
214
+ act.push(p)
215
+ }
216
+ }
217
+ if (dirty) {
218
+ saveState()
219
+ }
220
+ return [...act, ...cool]
221
+ }
222
+
223
+
126
224
  /**
127
225
  * 判斷失敗結果是否與「哪一把金鑰」無關(換組內金鑰必然再敗, 應整組跳過)
128
226
  *
@@ -184,14 +282,19 @@ function isKeyIndependentFail(r) {
184
282
  * @param {Array} opt.providers 輸入供應商條目物件陣列,順序即優先序。各條目除下列鍵外,其餘鍵(kind、model、exe、provider、config、sandbox、timeoutMs等)即該條目之opt原樣透傳對應轉接器
185
283
  * @param {String} [opt.providers[].id=條目索引字串] 輸入群組識別字串,游標以此為鍵、亦為日誌標籤,本套件不解讀其內容。須區分到「模型」而非只到「廠商」(如'claude:sonnet'而非'claude'),同一模型經不同路徑取得時須帶上路徑(如'poolside:laguna-s-2.1'與'or:poolside/laguna-s-2.1:free'),且務必唯一。省略時回退為陣列索引字串——索引是位置不是身分,日後插入條目會令後續條目繼承他人游標進度,故正式設定一律明給。詳見本檔檔頭之id設計規則
186
284
  * @param {Array} [opt.providers[].keys=[]] 輸入同一服務之多把API key字串陣列,逐次注入輪替(kind為opencode時須同時於條目給予provider),省略代表沿用CLI既有登入狀態之單一虛擬金鑰
285
+ * @param {*} [opt.providers[].meta=undefined] 輸入呼叫端自有資訊(分類、標籤、註記),保留鍵保證永不轉傳對應轉接器——條目其餘鍵一律原樣轉傳,自有欄位放此鍵可與轉傳機制永久絕緣,預設undefined
187
286
  * @param {Number} [opt.budgetMs=null] 輸入整輪遞補之時間上限毫秒正整數,剩餘預算會壓進每次呼叫之timeoutMs,預設null代表不限
188
287
  * @param {Number} [opt.minAttemptMs=20000] 輸入單次嘗試之最低剩餘預算毫秒正整數,剩餘低於此值即停止嘗試回報budget exhausted,預設20000
189
- * @param {Object} [opt.store=null] 輸入狀態持久化物件{get:()=>state,set:(state)=>{}},state內含cursors(逐群組游標),省略代表用行程內記憶體(跨呼叫有效,重啟歸零)。假定單行程序列調用,並行請自行加鎖
190
- * @param {Function} [opt.onEvent=null] 輸入事件回調函數(ev)=>{},ev.type可為'try'、'ok'、'next-key'、'skip-group'、'budget-out';失敗事件(next-key/skip-group)另帶stdout(被拒回覆)與stderr(錯誤輸出)供診斷,兩者於失敗路徑已由轉接器截斷;回調拋出例外不影響主流程,預設null
288
+ * @param {Object} [opt.store=null] 輸入狀態持久化物件{get:()=>state,set:(state)=>{}},state內含cursors(逐群組游標)與cooling(供應商冷卻時間戳,僅cooldownMs>0時使用),省略代表用行程內記憶體(跨呼叫有效,重啟歸零)。假定單行程序列調用,並行請自行加鎖
289
+ * @param {Number} [opt.cooldownMs=0] 輸入供應商冷卻視窗毫秒非負整數,>0啟用:條目(限有明給id者)遭遇限流(HTTP 429,僅api類可偵測;CLI類可經coolDetect注入判定)或逾時(TIMEOUT開頭)後,於冷卻視窗內之後續呼叫中被移至鏈尾——只降序不移除,前面全敗時仍會被嘗試,任一次成功立即解除;注意啟用時「providers順序即優先序」會被暫時重排,此即本機制之目的;預設0代表不啟用
290
+ * @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
291
+ * @param {Function} [opt.shouldStop=null] 輸入中止判定函數()=>Boolean,於每次嘗試之間檢查,回傳true即停止遞補回報ABORTED(不中止進行中之嘗試)——供呼叫端於成果已無人接收時(如客戶端斷線)止損;經工作流層原樣轉傳,中止後各後續呼叫進門即回ABORTED令整條工作流快速收束;回調拋出例外視同false,預設null
292
+ * @param {*} [opt.meta=undefined] 輸入呼叫端自有資訊,保留鍵保證永不轉傳各轉接器,預設undefined
293
+ * @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
191
294
  * @param {Number} [opt.timeoutMs=300000] 輸入各attempt共用之逾時毫秒正整數,條目可覆寫,全套件統一預設300000
192
295
  * @param {String|Function} [opt.validate=undefined] 輸入各attempt共用之stdout驗證規則,條目可覆寫,預設undefined
193
296
  * @param {Number} [opt.maxRetries=0] 輸入各attempt共用之同家重試次數非負整數,韌性建議交給換家而非重試同一家,預設0
194
- * @returns {Promise} 回傳Promise,resolve回傳結果物件,除execCli既有欄位(ok、stdout、stderr、code、error、durationMs、attempts、pid)外,追加providerId(實際使用之群組)、keyIndex(實際使用之金鑰索引,無keys時為null)、kind、model、tried(全部嘗試歷程陣列,成功時亦回傳;失敗項含stdout與stderr供診斷被拒原因),本函數不會reject
297
+ * @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
195
298
  * @example
196
299
  * //need opencode, claude, codex cli in system PATH
197
300
  *
@@ -248,49 +351,46 @@ async function dispatchAiFallback(prompt, opt = {}) {
248
351
  }
249
352
 
250
353
  //budgetMs, 無效視為不限
251
- let budgetMs = get(opt, 'budgetMs', null)
252
- if (!ispint(budgetMs)) {
253
- budgetMs = null
254
- }
255
- else {
256
- budgetMs = cint(budgetMs)
257
- }
354
+ let budgetMs = castPintOr(get(opt, 'budgetMs', null), null)
258
355
  let deadline = (budgetMs === null) ? null : Date.now() + budgetMs
259
356
 
260
357
  //minAttemptMs, 無效回退預設20000
261
- let minAttemptMs = get(opt, 'minAttemptMs', null)
262
- if (!ispint(minAttemptMs)) {
263
- minAttemptMs = DEFAULT_MIN_ATTEMPT_MS
264
- }
265
- else {
266
- minAttemptMs = cint(minAttemptMs)
358
+ let minAttemptMs = castPintOr(get(opt, 'minAttemptMs', null), DEFAULT_MIN_ATTEMPT_MS)
359
+
360
+ //state與saveState, store須同時具get與set函數才視為有效, 否則用行程內記憶體
361
+ let { state, saveState } = initState(get(opt, 'store', null))
362
+
363
+ //cooldownMs, 無效視為0=不啟用(現行行為零改變)
364
+ let cooldownMs = castPintOr(get(opt, 'cooldownMs', null), 0)
365
+
366
+ //coolDetect, 冷卻觸發之注入判定(CLI類限流簽章由呼叫端維護, 見檔頭), 僅cooldownMs>0時有意義
367
+ let coolDetect = get(opt, 'coolDetect', null)
368
+ if (!isfun(coolDetect)) {
369
+ coolDetect = null
267
370
  }
268
371
 
269
- //store, 須同時具get與set函數才視為有效, 否則用行程內記憶體
270
- let store = get(opt, 'store', null)
271
- let useStore = isobj(store) &amp;&amp; isfun(store.get) &amp;&amp; isfun(store.set)
272
- let state = null
273
- if (useStore) {
372
+ //shouldStop, 中止判定, 於每次嘗試之間檢查; 回調拋出例外視同false(不中止), 不得中斷主流程
373
+ let shouldStop = get(opt, 'shouldStop', null)
374
+ if (!isfun(shouldStop)) {
375
+ shouldStop = null
376
+ }
377
+ let stopRequested = () => {
378
+ if (shouldStop === null) {
379
+ return false
380
+ }
274
381
  try {
275
- state = store.get()
382
+ return shouldStop() === true
276
383
  }
277
- catch {}
278
- }
279
- if (!isobj(state)) {
280
- state = useStore ? { cursors: {} } : memoryState
281
- }
282
- if (!isobj(state.cursors)) {
283
- state.cursors = {}
284
- }
285
- let saveState = () => {
286
- if (useStore) {
287
- try {
288
- store.set(state)
289
- }
290
- catch {}
384
+ catch {
385
+ return false
291
386
  }
292
387
  }
293
388
 
389
+ //供應商冷卻: 冷卻中的條目降至鏈尾(細節見reorderByCooling)
390
+ if (cooldownMs > 0) {
391
+ providers = reorderByCooling(providers, state, cooldownMs, saveState)
392
+ }
393
+
294
394
  //onEvent, 回調拋出例外不得中斷主流程
295
395
  let onEvent = get(opt, 'onEvent', null)
296
396
  let emit = (ev) => {
@@ -313,9 +413,10 @@ async function dispatchAiFallback(prompt, opt = {}) {
313
413
  for (let ig = 0; ig &lt; providers.length; ig++) {
314
414
  let entry = providers[ig]
315
415
 
316
- //id, 無效回退條目索引字串
416
+ //id, 無效回退條目索引字串; idExplicit供冷卻機制判別(索引式id不參與冷卻)
317
417
  let id = get(entry, 'id', null)
318
- if (!isestr(id)) {
418
+ let idExplicit = isestr(id)
419
+ if (!idExplicit) {
319
420
  id = String(ig)
320
421
  }
321
422
 
@@ -343,6 +444,13 @@ async function dispatchAiFallback(prompt, opt = {}) {
343
444
  let skipGroup = false
344
445
  for (let a = 0; a &lt; nAttempts &amp;&amp; !skipGroup; a++) {
345
446
 
447
+ //中止檢查(嘗試邊界): 成果已無人接收時止損, 不中止進行中之嘗試(見檔頭【中止】)
448
+ if (stopRequested()) {
449
+ emit({ type: 'aborted', providerId: id, keyIndex: null, keyId: id })
450
+ tried.push({ providerId: id, keyIndex: null, keyId: id, outcome: 'aborted' })
451
+ return { ...getErrorResult('ABORTED', 'aborted'), tried }
452
+ }
453
+
346
454
  //keyIndex, 無keys時為null
347
455
  let keyIndex = (nk > 0) ? (cursor + a) % nk : null
348
456
  let keyId = (keyIndex === null) ? id : `${id}#${keyIndex}`
@@ -354,13 +462,7 @@ async function dispatchAiFallback(prompt, opt = {}) {
354
462
  }
355
463
 
356
464
  //有效timeout, 有預算時以剩餘預算封頂
357
- let effTimeout = get(attemptOpt, 'timeoutMs', null)
358
- if (!ispint(effTimeout)) {
359
- effTimeout = DEFAULT_TIMEOUT_MS
360
- }
361
- else {
362
- effTimeout = cint(effTimeout)
363
- }
465
+ let effTimeout = castPintOr(get(attemptOpt, 'timeoutMs', null), DEFAULT_TIMEOUT_MS)
364
466
  if (deadline !== null) {
365
467
  let remaining = deadline - Date.now()
366
468
 
@@ -368,7 +470,7 @@ async function dispatchAiFallback(prompt, opt = {}) {
368
470
  if (remaining &lt; minAttemptMs) {
369
471
  emit({ type: 'budget-out', providerId: id, keyIndex, keyId, remainingMs: remaining })
370
472
  tried.push({ providerId: id, keyIndex, keyId, outcome: 'budget-out' })
371
- return { ...getErrorResult('budget exhausted'), tried }
473
+ return { ...getErrorResult('budget exhausted', 'budget'), tried }
372
474
  }
373
475
  effTimeout = Math.min(effTimeout, remaining)
374
476
  }
@@ -378,41 +480,63 @@ async function dispatchAiFallback(prompt, opt = {}) {
378
480
  emit({ type: 'try', providerId: id, keyIndex, keyId, kind, model })
379
481
  let r = await dispatchAi(kind, prompt, attemptOpt)
380
482
 
381
- //成功, 推進游標(額度均攤)並回傳
483
+ //成功, 推進游標(額度均攤)並回傳; 任一次成功立即解除該家冷卻
382
484
  if (r.ok) {
485
+ if (cooldownMs > 0 &amp;&amp; idExplicit &amp;&amp; state.cooling[id] !== undefined) {
486
+ delete state.cooling[id]
487
+ saveState()
488
+ }
383
489
  if (nk > 0) {
384
490
  state.cursors[id] = (keyIndex + 1) % nk
385
491
  saveState()
386
492
  }
387
493
  emit({ type: 'ok', providerId: id, keyIndex, keyId, durationMs: r.durationMs })
388
- tried.push({ providerId: id, keyIndex, keyId, outcome: 'ok', durationMs: r.durationMs })
494
+ tried.push({ providerId: id, keyIndex, keyId, outcome: 'ok', durationMs: r.durationMs, ...(r.usage !== undefined ? { usage: r.usage } : {}) })
389
495
  return { ...r, providerId: id, keyIndex, kind, model, tried }
390
496
  }
391
497
 
392
498
  //失敗分流
393
499
  lastResult = r
394
500
  lastMeta = { providerId: id, keyIndex, kind, model }
501
+ //冷卻觸發: 內建為限流(HTTP 429, 僅api類可偵測)與逾時(TIMEOUT開頭, CLI與api皆可)——
502
+ //其餘失敗(金鑰無效/服務端錯誤)已有換金鑰換家機制處理, 納入冷卻反而誤傷;
503
+ //CLI類限流簽章經coolDetect注入判定(呼叫端維護, 見檔頭), 拋出例外視同false
504
+ if (cooldownMs > 0 &amp;&amp; idExplicit) {
505
+ let isCoolTrigger = (r.code === 429) || (isestr(r.error) &amp;&amp; r.error.indexOf('TIMEOUT') === 0)
506
+ if (!isCoolTrigger &amp;&amp; coolDetect !== null) {
507
+ try {
508
+ isCoolTrigger = coolDetect(r) === true
509
+ }
510
+ catch {}
511
+ }
512
+ if (isCoolTrigger) {
513
+ state.cooling[id] = Date.now()
514
+ saveState()
515
+ emit({ type: 'cooled', providerId: id, keyIndex, keyId, error: r.error, cooldownMs })
516
+ }
517
+ }
518
+
395
519
  //失敗事件與tried一併帶被拒回覆(stdout)與錯誤輸出(stderr), 供呼叫端診斷失敗原因
396
520
  //(如驗證失敗時模型究竟回了什麼); 兩者於失敗路徑已由轉接器截斷(≤500/1000字元), 不會過大
397
521
  if (isKeyIndependentFail(r)) {
398
522
 
399
523
  //與金鑰無關, 整組跳過
400
- emit({ type: 'skip-group', providerId: id, keyIndex, keyId, error: r.error, stdout: r.stdout, stderr: r.stderr })
401
- tried.push({ providerId: id, keyIndex, keyId, outcome: 'skip-group', error: r.error, stdout: r.stdout, stderr: r.stderr, durationMs: r.durationMs })
524
+ emit({ type: 'skip-group', providerId: id, keyIndex, keyId, error: r.error, errorType: r.errorType, stdout: r.stdout, stderr: r.stderr })
525
+ 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 } : {}) })
402
526
  skipGroup = true
403
527
  }
404
528
  else {
405
529
 
406
530
  //其餘(含額度上限/金鑰無效/未分類), 換組內下一把, 不記憶不停用
407
- emit({ type: 'next-key', providerId: id, keyIndex, keyId, error: r.error, stdout: r.stdout, stderr: r.stderr })
408
- tried.push({ providerId: id, keyIndex, keyId, outcome: 'next-key', error: r.error, stdout: r.stdout, stderr: r.stderr, durationMs: r.durationMs })
531
+ emit({ type: 'next-key', providerId: id, keyIndex, keyId, error: r.error, errorType: r.errorType, stdout: r.stdout, stderr: r.stderr })
532
+ 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 } : {}) })
409
533
  }
410
534
 
411
535
  }
412
536
  }
413
537
 
414
- //全數失敗, 回傳最後一筆失敗結果與完整歷程
415
- let r = lastResult || getErrorResult('all providers failed')
538
+ //全數失敗, 回傳最後一筆失敗結果(含其errorType)與完整歷程
539
+ let r = lastResult || getErrorResult('all providers failed', 'exec')
416
540
  return { ...r, ...(lastMeta || {}), tried }
417
541
  }
418
542
 
@@ -432,7 +556,7 @@ export default dispatchAiFallback
432
556
  <br class="clear">
433
557
 
434
558
  <footer>
435
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Aug 14 2026 12:30:07 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
559
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sat Aug 15 2026 08:45:29 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
436
560
  </footer>
437
561
 
438
562
  <script>prettyPrint();</script>
@@ -29,7 +29,7 @@
29
29
  <nav >
30
30
 
31
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#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#defaultIntegratePrompt">defaultIntegratePrompt</a></li><li><a href="global.html#dfTimeoutMs">dfTimeoutMs</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#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#getCliArgs">getCliArgs</a></li><li><a href="global.html#getErrorResult">getErrorResult</a></li><li><a href="global.html#isKeyIndependentFail">isKeyIndependentFail</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></ul>
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#attachErrorType">attachErrorType</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#defaultIntegratePrompt">defaultIntegratePrompt</a></li><li><a href="global.html#dfTimeoutMs">dfTimeoutMs</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#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#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#initState">initState</a></li><li><a href="global.html#isKeyIndependentFail">isKeyIndependentFail</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></ul>
33
33
 
34
34
  </nav>
35
35
 
@@ -193,7 +193,7 @@ export default dispatchAiWkf
193
193
  <br class="clear">
194
194
 
195
195
  <footer>
196
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Aug 14 2026 12:30:07 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
196
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sat Aug 15 2026 08:45:29 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
197
197
  </footer>
198
198
 
199
199
  <script>prettyPrint();</script>
@@ -29,7 +29,7 @@
29
29
  <nav >
30
30
 
31
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#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#defaultIntegratePrompt">defaultIntegratePrompt</a></li><li><a href="global.html#dfTimeoutMs">dfTimeoutMs</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#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#getCliArgs">getCliArgs</a></li><li><a href="global.html#getErrorResult">getErrorResult</a></li><li><a href="global.html#isKeyIndependentFail">isKeyIndependentFail</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></ul>
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#attachErrorType">attachErrorType</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#defaultIntegratePrompt">defaultIntegratePrompt</a></li><li><a href="global.html#dfTimeoutMs">dfTimeoutMs</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#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#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#initState">initState</a></li><li><a href="global.html#isKeyIndependentFail">isKeyIndependentFail</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></ul>
33
33
 
34
34
  </nav>
35
35
 
@@ -50,11 +50,11 @@ import omit from 'lodash-es/omit.js'
50
50
  import isarr from 'wsemi/src/isarr.mjs'
51
51
  import isbol from 'wsemi/src/isbol.mjs'
52
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
53
  import execCli from 'wsemi/src/execCli.mjs'
56
54
  import getCliArgs from './getCliArgs.mjs'
57
55
  import getErrorResult from './getErrorResult.mjs'
56
+ import { attachErrorType } from './getErrorType.mjs'
57
+ import castPintOr from './castPintOr.mjs'
58
58
  import dfTimeoutMs from './dfTimeoutMs.mjs'
59
59
 
60
60
 
@@ -181,13 +181,7 @@ async function dispatchAntigravity(prompt, opt = {}) {
181
181
  }
182
182
 
183
183
  //timeoutMs, 先行取值以供printTimeout推導, 無效回退全套件統一預設300000
184
- let timeoutMs = get(opt, 'timeoutMs', null)
185
- if (!ispint(timeoutMs)) {
186
- timeoutMs = DEFAULT_TIMEOUT_MS
187
- }
188
- else {
189
- timeoutMs = cint(timeoutMs)
190
- }
184
+ let timeoutMs = castPintOr(get(opt, 'timeoutMs', null), DEFAULT_TIMEOUT_MS)
191
185
 
192
186
  //printTimeout, 未給時由timeoutMs推導並預留緩衝, 令agy先於外層逾時而回報自身錯誤訊息
193
187
  let printTimeout = get(opt, 'printTimeout', null)
@@ -234,15 +228,17 @@ async function dispatchAntigravity(prompt, opt = {}) {
234
228
  let optCli = omit(opt, OWN_KEYS)
235
229
 
236
230
  //execCli, 不帶input(agy由--print取得prompt); try/catch兜底轉義膨脹等
237
- //前置檢查漏網之spawn同步拋出(如ENAMETOOLONG), 維持不reject契約
231
+ //前置檢查漏網之spawn同步拋出(如ENAMETOOLONG), 維持不reject契約;
232
+ //失敗結果補上機器可讀之errorType(僅機械可判者, 見getErrorType.mjs)
238
233
  try {
239
- return await execCli(exe, args, {
234
+ let r = await execCli(exe, args, {
240
235
  ...optCli,
241
236
  timeoutMs,
242
237
  })
238
+ return attachErrorType(r)
243
239
  }
244
240
  catch (err) {
245
- return getErrorResult(`${err.code || 'UNKNOWN'}: ${err.message}`)
241
+ return getErrorResult(`${err.code || 'UNKNOWN'}: ${err.message}`, 'spawn')
246
242
  }
247
243
  }
248
244
 
@@ -262,7 +258,7 @@ export default dispatchAntigravity
262
258
  <br class="clear">
263
259
 
264
260
  <footer>
265
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Aug 14 2026 12:30:07 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
261
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sat Aug 15 2026 08:45:29 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
266
262
  </footer>
267
263
 
268
264
  <script>prettyPrint();</script>