w-dispatch-ai 1.0.3 → 1.0.5

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 (50) hide show
  1. package/README.md +153 -43
  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 +8 -4
  5. package/docs/adapters.mjs.html +36 -2
  6. package/docs/dfTimeoutMs.mjs.html +98 -0
  7. package/docs/dispatchAi.mjs.html +2 -2
  8. package/docs/dispatchAiFallback.mjs.html +55 -16
  9. package/docs/dispatchAiWkf.mjs.html +42 -8
  10. package/docs/dispatchAntigravity.mjs.html +23 -12
  11. package/docs/dispatchApiOpenaiCompat.mjs.html +40 -6
  12. package/docs/dispatchClaude.mjs.html +17 -4
  13. package/docs/dispatchCodex.mjs.html +16 -3
  14. package/docs/dispatchOpencode.mjs.html +16 -3
  15. package/docs/getCliArgs.mjs.html +2 -2
  16. package/docs/getErrorResult.mjs.html +2 -2
  17. package/docs/global.html +511 -66
  18. package/docs/index.html +2 -2
  19. package/docs/resolveProviders.mjs.html +189 -0
  20. package/docs/wkf_callAiWithFallback.mjs.html +22 -11
  21. package/docs/wkf_extractJsonLoose.mjs.html +2 -2
  22. package/docs/wkf_runFanout.mjs.html +7 -7
  23. package/docs/wkf_runFanoutPipeline.mjs.html +7 -7
  24. package/docs/wkf_runRolePipeline.mjs.html +6 -6
  25. package/g.mjs +30 -23
  26. package/package.json +1 -1
  27. package/src/WDispatchAi.mjs +6 -2
  28. package/src/adapters.mjs +34 -0
  29. package/src/dfTimeoutMs.mjs +26 -0
  30. package/src/dispatchAiFallback.mjs +53 -14
  31. package/src/dispatchAiWkf.mjs +40 -6
  32. package/src/dispatchAntigravity.mjs +21 -10
  33. package/src/dispatchApiOpenaiCompat.mjs +38 -4
  34. package/src/dispatchClaude.mjs +15 -2
  35. package/src/dispatchCodex.mjs +14 -1
  36. package/src/dispatchOpencode.mjs +14 -1
  37. package/src/providers.mjs +133 -0
  38. package/src/resolveProviders.mjs +117 -0
  39. package/src/wkf/callAiWithFallback.mjs +20 -9
  40. package/src/wkf/runFanout.mjs +5 -5
  41. package/src/wkf/runFanoutPipeline.mjs +5 -5
  42. package/src/wkf/runRolePipeline.mjs +4 -4
  43. package/test/tools/fakeServerForApiTest.mjs +14 -0
  44. package/test/unit-WDispatchAi.test.mjs +9 -3
  45. package/test/unit-dispatchAi.test.mjs +1 -1
  46. package/test/unit-dispatchAiFallback.test.mjs +25 -0
  47. package/test/unit-dispatchAntigravity.test.mjs +25 -5
  48. package/test/unit-dispatchApiOpenaiCompat.test.mjs +31 -0
  49. package/test/unit-providers.test.mjs +66 -0
  50. package/test/unit-resolveProviders.test.mjs +85 -0
@@ -8,6 +8,8 @@ import dispatchClaude from './dispatchClaude.mjs'
8
8
  import dispatchCodex from './dispatchCodex.mjs'
9
9
  import dispatchAntigravity from './dispatchAntigravity.mjs'
10
10
  import dispatchApiOpenaiCompat from './dispatchApiOpenaiCompat.mjs'
11
+ import providers from './providers.mjs'
12
+ import resolveProviders from './resolveProviders.mjs'
11
13
 
12
14
 
13
15
  // WDispatchAi.mjs — AI供應商分派層
@@ -24,10 +26,10 @@ let KINDS = keys(adapters)
24
26
  /**
25
27
  * AI供應商分派
26
28
  *
27
- * @returns {Object} 回傳物件,其內含KINDS(可用供應商種類字串陣列),dispatchAiWkf之工作流工廠函數,以及dispatchAi、dispatchAiFallback、dispatchOpencode、dispatchClaude、dispatchCodex、dispatchAntigravity、dispatchApiOpenaiCompat之async函數
29
+ * @returns {Object} 回傳物件,其內含KINDS(可用供應商種類字串陣列),dispatchAiWkf之工作流工廠函數,dispatchAi、dispatchAiFallback、dispatchOpencode、dispatchClaude、dispatchCodex、dispatchAntigravity、dispatchApiOpenaiCompat之async函數,以及providers(預設providers定義檔)與resolveProviders(envVar展開器)
28
30
  * @example
29
31
  *
30
- * 詳見dispatchAi、dispatchAiFallback、dispatchAiWkf、dispatchOpencode、dispatchClaude、dispatchCodex、dispatchAntigravity、dispatchApiOpenaiCompat範例
32
+ * 詳見dispatchAi、dispatchAiFallback、dispatchAiWkf、dispatchOpencode、dispatchClaude、dispatchCodex、dispatchAntigravity、dispatchApiOpenaiCompat、resolveProviders範例
31
33
  *
32
34
  */
33
35
  let WDispatchAi = {
@@ -40,6 +42,8 @@ let WDispatchAi = {
40
42
  dispatchCodex,
41
43
  dispatchAntigravity,
42
44
  dispatchApiOpenaiCompat,
45
+ providers,
46
+ resolveProviders,
43
47
  }
44
48
 
45
49
 
package/src/adapters.mjs CHANGED
@@ -5,6 +5,40 @@ import dispatchAntigravity from './dispatchAntigravity.mjs'
5
5
  import dispatchApiOpenaiCompat from './dispatchApiOpenaiCompat.mjs'
6
6
 
7
7
 
8
+ // adapters.mjs — kind對照表, 亦為「CLI或API」之選型判準所在
9
+ //
10
+ // ══ 選 kind 的唯一判準: 這次呼叫需不需要「工具」? ══
11
+ //
12
+ // 需要工具 → 用CLI類kind(opencode/claude/codex/antigravity)
13
+ // 所謂工具即: 讀取本機檔案、grep搜尋、執行shell指令、抓取網頁、寫檔。
14
+ // CLI本身是agentic harness, 自帶完整工具迴圈, 呼叫端什麼都不必做。
15
+ //
16
+ // 純文字生成 → 用API類kind(api-openai-compat)
17
+ // 所謂純文字即: 摘要、分析、改寫、翻譯、產出JSON——所有素材都已在prompt內,
18
+ // 模型只需讀prompt再輸出文字, 全程不需要碰外部世界。
19
+ // 免安裝CLI、免預先登入, 且實測比CLI快(agnes: API 1~2.5s vs CLI 4~6s)。
20
+ //
21
+ // ══ 為何API類不自建工具迴圈(2026-08-11實測後之決策, 勿再自行推翻) ══
22
+ //
23
+ // 1. 閘道端零內建工具: 實測Zen與Agnes皆然, 連web_search都沒有——
24
+ // 對`tools:[{type:'web_search'}]`回400並要求function.parameters,
25
+ // 即端點只接受「呼叫端自行定義且自行執行」的function工具。
26
+ // 2. 協定雖支援function calling(Zen之nemotron與Agnes皆實測回tool_calls),
27
+ // 但工具的定義、執行、錯誤處理、安全邊界全部得由本套件實作與維護,
28
+ // 等同重造CLI已經提供的harness; 工作流日後仍會持續擴充,
29
+ // 自建工具集之維護成本只會擴大, 故一律不走此路。
30
+ // 3. tool_calls有會話束縛(tool_call_id須於同一條messages串回填),
31
+ // 無法跨行程外傳給上層agent(如hermes)代為執行——
32
+ // 工作流是被上層阻塞呼叫的函式, 沒有反向請求工具的通道。
33
+ // 詳見dispatchApiOpenaiCompat.mjs檔頭。
34
+ //
35
+ // ══ 混用才是常態 ══
36
+ //
37
+ // 同一條dispatchAiFallback鏈可逐條目混搭kind, 工作流各階段亦然:
38
+ // 產生候選、整合收斂等純文字階段用API(快且省), 需要讀專案檔案或grep的
39
+ // 階段換CLI。判準永遠是「這一步需不需要碰外部世界」, 而非整條鏈二選一。
40
+
41
+
8
42
  /**
9
43
  * 各AI供應商種類(kind)對CLI轉接器函數之對照表
10
44
  *
@@ -0,0 +1,26 @@
1
+ // dfTimeoutMs.mjs — 全套件統一之預設逾時毫秒(單一來源)
2
+ //
3
+ // 【為何統一】各轉接器與各層若各有預設(曾為CLI 120s/agy 300s/api 120s),
4
+ // 呼叫方難以理解為何不一致; 統一後呼叫方只需記一個數字, 要改就由opt傳入覆寫。
5
+ //
6
+ // 【為何取300000(5分鐘)】對齊agy自身print-timeout之預設5m0s;
7
+ // agy為agent型CLI, 實測合法執行可達116秒, 統一成120秒會誤殺;
8
+ // 快速任務有需要可自行下調, 複雜任務(如單一AI工作15分鐘)依README之Timeout規劃上調。
9
+
10
+
11
+ /**
12
+ * 全套件統一之預設逾時毫秒
13
+ *
14
+ * @returns {Number} 回傳預設逾時毫秒整數300000(5分鐘)
15
+ * @example
16
+ *
17
+ * import dfTimeoutMs from './src/dfTimeoutMs.mjs'
18
+ *
19
+ * console.log(dfTimeoutMs)
20
+ * // => 300000
21
+ *
22
+ */
23
+ let dfTimeoutMs = 300000
24
+
25
+
26
+ export default dfTimeoutMs
@@ -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 dfTimeoutMs from './dfTimeoutMs.mjs'
11
12
 
12
13
 
13
14
  // dispatchAiFallback.mjs — 多供應商自動遞補層
@@ -28,6 +29,34 @@ import getErrorResult from './getErrorResult.mjs'
28
29
  //
29
30
  // 【時間預算】budgetMs限制整輪遞補的總時長, 剩餘預算會壓進每次呼叫的timeoutMs,
30
31
  // 防止多家連續卡逾時而撞破外部排程的執行上限。
32
+ //
33
+ // ══ 條目id之設計規則(呼叫端負責, 本套件不解讀其內容) ══
34
+ //
35
+ // id於本套件內只有兩個用途: 游標的物件鍵(state.cursors[id])與日誌標籤
36
+ // (providerId、keyId=`${id}#${keyIndex}`)。不查表、不比對、無格式要求,
37
+ // 純粹是呼叫端的命名空間——故「什麼算同一個供應商」由呼叫端定義, 本套件不猜。
38
+ //
39
+ // ① id須能區分到「模型」而非只到「廠商」
40
+ // ✗ id:'claude' —— 日後要同時掛sonnet與opus就無法並存, 且日誌看不出用了哪個模型
41
+ // ✓ id:'claude:sonnet' / id:'claude:opus'
42
+ //
43
+ // ② 同一模型經不同路徑取得時, id須帶上路徑, 且前綴用「具體路徑名」不用泛稱
44
+ // 同一個laguna可經Poolside官方REST、OpenRouter、opencode CLI三條路,
45
+ // 三者額度池與故障域各自獨立, 屬三個供應商:
46
+ // ✓ 'poolside:laguna-s-2.1' / 'or:poolside/laguna-s-2.1:free' / 'oc:poolside/poolside/laguna-s-2.1'
47
+ // ✗ 'api:laguna-s-2.1' —— 泛稱api:在同模型有多個REST閘道時會撞名, 且日誌看不出走哪個閘道
48
+ // 慣用前綴: CLI類=oc:/agy:/claude:/codex:(即kind或CLI名); REST類=閘道名(zen:/agnes:/poolside:/or:/nv:)
49
+ //
50
+ // ③ id務必給且務必唯一
51
+ // 未給時本套件回退為「陣列索引字串」——索引是位置不是身分, 日後於鏈中插入條目
52
+ // 會讓後續條目繼承他人的游標進度(輪替張冠李戴), 故正式設定一律明給。
53
+ // 兩個條目同id則共用同一游標且日誌無法區分, 屬設定錯誤。
54
+ //
55
+ // ④ 同一組金鑰用於多個條目時, 各條目游標獨立
56
+ // 例如agnes的CLI版與REST版共用同一批金鑰時, 兩者各自從游標起點輪替,
57
+ // 同一把金鑰可能被連續使用而另一把閒置(帳號額度未均攤)。
58
+ // 要讓它們共享輪替進度就給相同id(代價: 日誌無法區分兩者);
59
+ // 要能區分就分開命名(代價: 額度不均攤)。此取捨由呼叫端依實際需求決定。
31
60
 
32
61
 
33
62
  //fallback層自用之設定鍵, 其餘鍵作為各attempt之共用預設原樣轉傳
@@ -40,7 +69,7 @@ let ENTRY_KEYS = ['id', 'keys']
40
69
 
41
70
  //預設值
42
71
  let DEFAULT_MIN_ATTEMPT_MS = 20000
43
- let DEFAULT_TIMEOUT_MS = 120000
72
+ let DEFAULT_TIMEOUT_MS = dfTimeoutMs //全套件統一預設300000
44
73
 
45
74
 
46
75
  //memoryState, 未注入store時之行程內預設狀態(跨呼叫有效, 重啟歸零)
@@ -106,16 +135,16 @@ function isKeyIndependentFail(r) {
106
135
  * @param {String} prompt 輸入提示詞字串,一律以stdin傳入子進程
107
136
  * @param {Object} [opt={}] 輸入設定物件,預設{}
108
137
  * @param {Array} opt.providers 輸入供應商條目物件陣列,順序即優先序。各條目除下列鍵外,其餘鍵(kind、model、exe、provider、config、sandbox、timeoutMs等)即該條目之opt原樣透傳對應轉接器
109
- * @param {String} [opt.providers[].id=條目索引字串] 輸入群組識別字串,游標以此為鍵,多金鑰條目應給予穩定id,預設為條目索引字串
138
+ * @param {String} [opt.providers[].id=條目索引字串] 輸入群組識別字串,游標以此為鍵、亦為日誌標籤,本套件不解讀其內容。須區分到「模型」而非只到「廠商」(如'claude:sonnet'而非'claude'),同一模型經不同路徑取得時須帶上路徑(如'poolside:laguna-s-2.1'與'or:poolside/laguna-s-2.1:free'),且務必唯一。省略時回退為陣列索引字串——索引是位置不是身分,日後插入條目會令後續條目繼承他人游標進度,故正式設定一律明給。詳見本檔檔頭之id設計規則
110
139
  * @param {Array} [opt.providers[].keys=[]] 輸入同一服務之多把API key字串陣列,逐次注入輪替(kind為opencode時須同時於條目給予provider),省略代表沿用CLI既有登入狀態之單一虛擬金鑰
111
140
  * @param {Number} [opt.budgetMs=null] 輸入整輪遞補之時間上限毫秒正整數,剩餘預算會壓進每次呼叫之timeoutMs,預設null代表不限
112
141
  * @param {Number} [opt.minAttemptMs=20000] 輸入單次嘗試之最低剩餘預算毫秒正整數,剩餘低於此值即停止嘗試回報budget exhausted,預設20000
113
142
  * @param {Object} [opt.store=null] 輸入狀態持久化物件{get:()=>state,set:(state)=>{}},state內含cursors(逐群組游標),省略代表用行程內記憶體(跨呼叫有效,重啟歸零)。假定單行程序列調用,並行請自行加鎖
114
- * @param {Function} [opt.onEvent=null] 輸入事件回調函數(ev)=>{},ev.type可為'try'、'ok'、'next-key'、'skip-group'、'budget-out',回調拋出例外不影響主流程,預設null
115
- * @param {Number} [opt.timeoutMs=120000] 輸入各attempt共用之逾時毫秒正整數,條目可覆寫,預設120000
143
+ * @param {Function} [opt.onEvent=null] 輸入事件回調函數(ev)=>{},ev.type可為'try'、'ok'、'next-key'、'skip-group'、'budget-out';失敗事件(next-key/skip-group)另帶stdout(被拒回覆)與stderr(錯誤輸出)供診斷,兩者於失敗路徑已由轉接器截斷;回調拋出例外不影響主流程,預設null
144
+ * @param {Number} [opt.timeoutMs=300000] 輸入各attempt共用之逾時毫秒正整數,條目可覆寫,全套件統一預設300000
116
145
  * @param {String|Function} [opt.validate=undefined] 輸入各attempt共用之stdout驗證規則,條目可覆寫,預設undefined
117
146
  * @param {Number} [opt.maxRetries=0] 輸入各attempt共用之同家重試次數非負整數,韌性建議交給換家而非重試同一家,預設0
118
- * @returns {Promise} 回傳Promise,resolve回傳結果物件,除execCli既有欄位(ok、stdout、stderr、code、error、durationMs、attempts、pid)外,追加providerId(實際使用之群組)、keyIndex(實際使用之金鑰索引,無keys時為null)、kind、model、tried(全部嘗試歷程陣列,成功時亦回傳),本函數不會reject
147
+ * @returns {Promise} 回傳Promise,resolve回傳結果物件,除execCli既有欄位(ok、stdout、stderr、code、error、durationMs、attempts、pid)外,追加providerId(實際使用之群組)、keyIndex(實際使用之金鑰索引,無keys時為null)、kind、model、tried(全部嘗試歷程陣列,成功時亦回傳;失敗項含stdout與stderr供診斷被拒原因),本函數不會reject
119
148
  * @example
120
149
  * //need opencode, claude, codex cli in system PATH
121
150
  *
@@ -126,21 +155,29 @@ function isKeyIndependentFail(r) {
126
155
  * let r = await dispatchAiFallback('請只回覆兩個字:完成', {
127
156
  * providers: [
128
157
  * {
129
- * id: 'deepseek',
158
+ * //id區分到模型且帶路徑: 同一模型經REST與CLI取得屬兩個供應商
159
+ * id: 'zen:deepseek-v4-flash-free',
160
+ * kind: 'api-openai-compat',
161
+ * baseURL: 'https://opencode.ai/zen/v1',
162
+ * model: 'deepseek-v4-flash-free',
163
+ * keys: ['sk-aaa', 'sk-bbb'], //多把金鑰, 某把失敗自動換下一把
164
+ * },
165
+ * {
166
+ * id: 'oc:opencode/deepseek-v4-flash-free', //同一模型之CLI版(有工具, 較慢)
130
167
  * kind: 'opencode',
131
168
  * model: 'opencode/deepseek-v4-flash-free',
132
169
  * provider: 'opencode',
133
- * keys: ['sk-aaa', 'sk-bbb'], //多把金鑰, 某把失敗自動換下一把
170
+ * keys: ['sk-aaa', 'sk-bbb'],
134
171
  * timeoutMs: 180000,
135
172
  * },
136
- * { id: 'claude', kind: 'claude', model: 'sonnet' }, //deepseek全敗時遞補
137
- * { id: 'codex', kind: 'codex', model: 'gpt-5.6-luna', sandbox: 'read-only' },
173
+ * { id: 'claude:sonnet', kind: 'claude', model: 'sonnet' }, //以上全敗時遞補
174
+ * { id: 'codex:gpt-5.6-luna', kind: 'codex', model: 'gpt-5.6-luna', sandbox: 'read-only' },
138
175
  * ],
139
176
  * budgetMs: 600000,
140
177
  * onEvent: (ev) => console.log(ev.type, ev.providerId, ev.keyIndex),
141
178
  * })
142
179
  * console.log(r.ok, r.providerId, r.keyIndex, r.tried.length)
143
- * // => true 'deepseek' 0 1
180
+ * // => true 'zen:deepseek-v4-flash-free' 0 1
144
181
  *
145
182
  * }
146
183
  * await test()
@@ -308,18 +345,20 @@ async function dispatchAiFallback(prompt, opt = {}) {
308
345
  //失敗分流
309
346
  lastResult = r
310
347
  lastMeta = { providerId: id, keyIndex, kind, model }
348
+ //失敗事件與tried一併帶被拒回覆(stdout)與錯誤輸出(stderr), 供呼叫端診斷失敗原因
349
+ //(如驗證失敗時模型究竟回了什麼); 兩者於失敗路徑已由轉接器截斷(≤500/1000字元), 不會過大
311
350
  if (isKeyIndependentFail(r)) {
312
351
 
313
352
  //與金鑰無關, 整組跳過
314
- emit({ type: 'skip-group', providerId: id, keyIndex, keyId, error: r.error })
315
- tried.push({ providerId: id, keyIndex, keyId, outcome: 'skip-group', error: r.error, durationMs: r.durationMs })
353
+ emit({ type: 'skip-group', providerId: id, keyIndex, keyId, error: r.error, stdout: r.stdout, stderr: r.stderr })
354
+ tried.push({ providerId: id, keyIndex, keyId, outcome: 'skip-group', error: r.error, stdout: r.stdout, stderr: r.stderr, durationMs: r.durationMs })
316
355
  skipGroup = true
317
356
  }
318
357
  else {
319
358
 
320
359
  //其餘(含額度上限/金鑰無效/未分類), 換組內下一把, 不記憶不停用
321
- emit({ type: 'next-key', providerId: id, keyIndex, keyId, error: r.error })
322
- tried.push({ providerId: id, keyIndex, keyId, outcome: 'next-key', error: r.error, durationMs: r.durationMs })
360
+ emit({ type: 'next-key', providerId: id, keyIndex, keyId, error: r.error, stdout: r.stdout, stderr: r.stderr })
361
+ tried.push({ providerId: id, keyIndex, keyId, outcome: 'next-key', error: r.error, stdout: r.stdout, stderr: r.stderr, durationMs: r.durationMs })
323
362
  }
324
363
 
325
364
  }
@@ -17,6 +17,32 @@ import runFanoutPipeline from './wkf/runFanoutPipeline.mjs'
17
17
  //
18
18
  // 【本函數為同步工廠會throw】providers無效屬設定錯誤, 應於啟動期即失敗(fail fast),
19
19
  // 與各dispatch函數「不reject」之約定不衝突——後者是執行期呼叫, 前者是組裝期設定。
20
+ //
21
+ // 【定義表之鍵名即條目id】該鍵名會成為dispatchAiFallback之條目id(游標鍵與日誌標籤),
22
+ // 須區分到「模型」而非只到「廠商」——鍵名取'claude'則日後無法同時掛sonnet與opus,
23
+ // 且日誌看不出實際用了哪個模型; 同一模型經不同路徑(REST/CLI/不同閘道)取得時,
24
+ // 額度池與故障域各自獨立而屬不同供應商, 鍵名須帶上路徑加以區分。
25
+ // 命名規則與取捨詳見dispatchAiFallback.mjs檔頭之「條目id之設計規則」。
26
+ //
27
+ // 【timeout速覽】(完整說明見README之「Timeout 總覽」)
28
+ // 單次AI嘗試: timeoutMs, 全套件統一預設300000(5分鐘, 單一來源dfTimeoutMs.mjs),
29
+ // 直接呼叫轉接器與工作流內皆同一數字;
30
+ // 單一名額(遞補鏈): budgetMs, 預設null不限; minAttemptMs開工門檻預設20000(無budget時不作用);
31
+ // 工作流總時長: 無獨立參數(刻意)——由結構推導: Fanout≈最慢名額+整合(並行),
32
+ // RolePipeline≈Σ各階段(序列); 要上限就對每名額設budgetMs。
33
+ // 最壞情況公式: 名額≤K×timeoutMs(K=鏈組數, 逾時每組只燒一次即跳組)。
34
+ // 調整四層(細者覆蓋粗者): defaults(此處) → 各工作流callOpt → 階段/名額規格 → provider條目。
35
+ //
36
+ // 【定義providers時如何選kind: CLI或API】判準是「該階段需不需要工具」:
37
+ // 需要讀本機檔案、grep、執行指令、抓網頁 → CLI類kind(opencode/claude/codex/antigravity);
38
+ // 純文字生成(素材皆已在prompt內) → API類kind(api-openai-compat), 免安裝免登入且較快。
39
+ // 工作流常態是混用: runFanout之候選生成與整合、runRolePipeline之審計修訂等
40
+ // 多屬純文字階段可走API; 唯獨需要實際翻閱專案檔案的階段必須走CLI。
41
+ // API類不支援工具且不會自建工具迴圈, 理由詳見adapters.mjs檔頭之選型判準區塊。
42
+ //
43
+ // 【工具無法向上層轉送】工作流各名額(如runFanout之agents)只是同行程之async函數呼叫,
44
+ // 非獨立agent; 模型回傳之tool_calls受會話束縛而無法外傳給上層agent(如hermes)代跑,
45
+ // 故「讓外殼提供工具給工作流內的模型使用」在本架構下不成立——需要工具就選CLI類kind。
20
46
 
21
47
 
22
48
  /**
@@ -36,11 +62,15 @@ import runFanoutPipeline from './wkf/runFanoutPipeline.mjs'
36
62
  *
37
63
  * import dispatchAiWkf from './src/dispatchAiWkf.mjs'
38
64
  *
65
+ * //定義表之鍵名即dispatchAiFallback之條目id, 須區分到模型而非只到廠商,
66
+ * //同一模型經不同路徑取得時須帶上路徑(REST與CLI屬兩個供應商, 能力與速度皆不同)
39
67
  * let wkf = dispatchAiWkf({
40
68
  * providers: {
41
- * 'deepseek': { kind: 'opencode', model: 'opencode/deepseek-v4-flash-free', provider: 'opencode', keys: ['sk-xxx'] },
42
- * 'sonnet': { kind: 'claude', model: 'sonnet' },
43
- * 'luna': { kind: 'codex', model: 'gpt-5.6-luna' },
69
+ * 'zen:deepseek-v4-flash-free': { kind: 'api-openai-compat', baseURL: 'https://opencode.ai/zen/v1', model: 'deepseek-v4-flash-free', keys: ['sk-xxx'] },
70
+ * 'oc:opencode/deepseek-v4-flash-free': { kind: 'opencode', model: 'opencode/deepseek-v4-flash-free', provider: 'opencode', keys: ['sk-xxx'] },
71
+ * 'claude:sonnet': { kind: 'claude', model: 'sonnet' },
72
+ * 'claude:opus': { kind: 'claude', model: 'opus' },
73
+ * 'codex:gpt-5.6-luna': { kind: 'codex', model: 'gpt-5.6-luna' },
44
74
  * },
45
75
  * defaults: { timeoutMs: 300000 },
46
76
  * })
@@ -48,15 +78,19 @@ import runFanoutPipeline from './wkf/runFanoutPipeline.mjs'
48
78
  * let test = async () => {
49
79
  *
50
80
  * //單一名額: 主模型+遞補鏈
51
- * let r1 = await wkf.callAi('只回覆JSON: {"a":1}', { spec: { use: 'deepseek', fallback: ['sonnet'] }, check: (j) => j.a === 1 })
81
+ * let r1 = await wkf.callAi('只回覆JSON: {"a":1}', { spec: { use: 'zen:deepseek-v4-flash-free', fallback: ['claude:sonnet'] }, check: (j) => j.a === 1 })
52
82
  * console.log(r1.ok, r1.json)
53
83
  * // => true { a: 1 }
54
84
  *
55
85
  * //Fanout工作流: 多開執行+單點整合
86
+ * //純文字階段用REST(快), 需要讀專案檔案之階段才用CLI(有工具)
56
87
  * let r2 = await wkf.runFanout({
57
88
  * task: '分析並只回覆JSON: {"essence":"..."}',
58
- * agents: [{ use: 'deepseek', fallback: ['sonnet'] }, { use: 'sonnet' }],
59
- * integrate: { use: 'luna' },
89
+ * agents: [
90
+ * { use: 'zen:deepseek-v4-flash-free', fallback: ['claude:sonnet'] },
91
+ * { use: 'claude:sonnet' },
92
+ * ],
93
+ * integrate: { use: 'codex:gpt-5.6-luna' },
60
94
  * check: (j) => !!j.essence,
61
95
  * })
62
96
  * console.log(r2.ok, r2.integrated)
@@ -8,6 +8,7 @@ import cint from 'wsemi/src/cint.mjs'
8
8
  import execCli from 'wsemi/src/execCli.mjs'
9
9
  import getCliArgs from './getCliArgs.mjs'
10
10
  import getErrorResult from './getErrorResult.mjs'
11
+ import dfTimeoutMs from './dfTimeoutMs.mjs'
11
12
 
12
13
 
13
14
  // dispatchAntigravity.mjs — 以Google Antigravity CLI(agy)呼叫AI模型
@@ -37,7 +38,7 @@ import getErrorResult from './getErrorResult.mjs'
37
38
 
38
39
  //預設值
39
40
  let DEFAULT_EXE = 'agy'
40
- let DEFAULT_TIMEOUT_MS = 300000 //agent型CLI, 對齊agy自身print-timeout預設5m0s, 不沿用套件通用120000
41
+ let DEFAULT_TIMEOUT_MS = dfTimeoutMs //全套件統一預設300000(恰對齊agy自身print-timeout預設5m0s)
41
42
  let MAX_PROMPT_LENGTH = 30000 //命令列上限32767扣除exe路徑與旗標後之保守值
42
43
  let PRINT_TIMEOUT_BUFFER_S = 30
43
44
  let MIN_PRINT_TIMEOUT_S = 30
@@ -68,10 +69,10 @@ let OWN_KEYS = ['exe', 'model', 'effort', 'skipPermissions', 'printTimeout', 'ad
68
69
  * @param {String} [opt.effort=''] 輸入推理深度字串,可選'low'、'medium'、'high',需agy>=1.1.11,建議搭配不帶檔位之基礎slug;與帶檔位slug併用且檔位不一致時agy回conflicts錯誤,預設''代表不帶
69
70
  * @param {Boolean} [opt.skipPermissions=true] 輸入是否帶`--dangerously-skip-permissions`旗標布林值,false代表保留CLI權限閘門,預設true
70
71
  * @param {String} [opt.printTimeout=''] 輸入agy自身等待上限字串(如'10m'、'570s'),預設''代表由timeoutMs推導(扣30秒緩衝,下限30秒)
71
- * @param {Array} [opt.addDirs=[]] 輸入加入workspace之目錄字串陣列,逐項展開為`--add-dir`,預設[]
72
+ * @param {Array} [opt.addDirs=自動納入cwd] 輸入加入workspace之目錄字串陣列,逐項展開為`--add-dir`。agy以自身scratch目錄為工作區而不採子進程cwd,故未給本參數時自動納入有效cwd令檔案可視範圍與其他CLI轉接器一致;明示給陣列(含空陣列[]代表不揭露任何目錄)則完全尊重呼叫端
72
73
  * @param {Array} [opt.extraArgs=[]] 輸入額外命令列旗標字串陣列(如--output-format、--json-schema、--mode),將接於固定旗標之後、`--print`之前,預設[]
73
- * @param {Number} [opt.timeoutMs=300000] 輸入逾時毫秒正整數,逾時將強制關閉子進程及其子孫程序,agy為agent型CLI故預設較長之300000,預設300000
74
- * @param {String} [opt.cwd=process.cwd()] 輸入子進程工作目錄字串,預設process.cwd()
74
+ * @param {Number} [opt.timeoutMs=300000] 輸入逾時毫秒正整數,逾時將強制關閉子進程及其子孫程序,全套件統一預設300000(恰對齊agy自身print-timeout預設5m0s)
75
+ * @param {String} [opt.cwd=process.cwd()] 輸入子進程工作目錄字串,預設process.cwd()。注意本參數不影響agy之檔案可視範圍(agy以自身scratch目錄為工作區),可視範圍由addDirs決定(未給addDirs時自動納入本目錄)
75
76
  * @param {String|Function} [opt.validate=undefined] 輸入stdout驗證規則字串或自訂驗證函數,規則字串支援'nonempty'、'json'、'min:100',多規則可用逗號串接,預設undefined代表不驗證
76
77
  * @param {Number} [opt.maxRetries=0] 輸入失敗後最大重試次數非負整數,預設0
77
78
  * @returns {Promise} 回傳Promise,resolve回傳結果物件,內含ok(是否成功布林值)、stdout(標準輸出字串)、stderr(標準錯誤字串)、code(離開碼)、error(錯誤訊息字串,成功時為空字串)、durationMs(耗時毫秒)、attempts(實際嘗試次數),本函數不會reject
@@ -132,7 +133,7 @@ async function dispatchAntigravity(prompt, opt = {}) {
132
133
  skipPermissions = true
133
134
  }
134
135
 
135
- //timeoutMs, 先行取值以供printTimeout推導, agy專屬預設300000
136
+ //timeoutMs, 先行取值以供printTimeout推導, 無效回退全套件統一預設300000
136
137
  let timeoutMs = get(opt, 'timeoutMs', null)
137
138
  if (!ispint(timeoutMs)) {
138
139
  timeoutMs = DEFAULT_TIMEOUT_MS
@@ -148,13 +149,23 @@ async function dispatchAntigravity(prompt, opt = {}) {
148
149
  }
149
150
 
150
151
  //addDirs, 逐項展開為--add-dir(agy該旗標可重複)
152
+ //未給時自動納入有效cwd(明給的或預設process.cwd()): agy以自身scratch目錄為工作區,
153
+ //不採子進程之cwd, 僅--add-dir能擴其檔案可視範圍——僅給cwd時弱模型直接回「找不到檔案」、
154
+ //強模型得自行摸索(實測116s vs 帶--add-dir僅6s), 且兩者皆ok:true屬靜默失敗,
155
+ //極易被誤判為模型能力不足(2026-08-13以隨機token實測確認)。
156
+ //明示addDirs(含空陣列[]代表不揭露任何目錄)則完全尊重呼叫端, 不自動加入
151
157
  let addDirs = get(opt, 'addDirs', null)
158
+ if (!isarr(addDirs)) {
159
+ let cwdEff = get(opt, 'cwd', null)
160
+ if (!isestr(cwdEff)) {
161
+ cwdEff = process.cwd()
162
+ }
163
+ addDirs = [cwdEff]
164
+ }
152
165
  let addDirArgs = []
153
- if (isarr(addDirs)) {
154
- for (let d of addDirs) {
155
- if (isestr(d)) {
156
- addDirArgs.push('--add-dir', d)
157
- }
166
+ for (let d of addDirs) {
167
+ if (isestr(d)) {
168
+ addDirArgs.push('--add-dir', d)
158
169
  }
159
170
  }
160
171
 
@@ -11,6 +11,7 @@ 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'
13
13
  import getErrorResult from './getErrorResult.mjs'
14
+ import dfTimeoutMs from './dfTimeoutMs.mjs'
14
15
 
15
16
 
16
17
  // dispatchApiOpenaiCompat.mjs — 以fetch直呼OpenAI相容API(chat/completions)
@@ -27,6 +28,19 @@ import getErrorResult from './getErrorResult.mjs'
27
28
  // 能力(不讀檔不跑指令), 天然無寫檔風險。
28
29
  // 注意: claude與codex走訂閱帳號登入態而非API金鑰, 無法比照, 仍須CLI轉接器。
29
30
  //
31
+ // 【本轉接器不支援工具, 需要工具請改用CLI類kind(2026-08-11實測後之決策)】
32
+ // 閘道端零內建工具: 實測Zen與Agnes皆對`tools:[{type:'web_search'}]`回400並要求
33
+ // function.parameters, 即只接受「呼叫端自行定義且自行執行」之function工具;
34
+ // 協定層雖支援function calling(Zen之nemotron-3-ultra-free與Agnes皆實測回
35
+ // finish_reason:'tool_calls'且tool_calls格式標準), 但工具之定義、執行、錯誤處理與
36
+ // 安全邊界全須本套件自行實作與維護, 等同重造CLI已提供之harness, 故不做。
37
+ // 又tool_calls有會話束縛(tool_call_id須於同一條messages串內回填, 且須保留前文),
38
+ // 該messages串活在本函數單次呼叫之生命週期內, 無法暫停後跨行程外傳給上層agent代跑
39
+ // ——工作流是被上層阻塞呼叫的函式, 沒有反向請求工具的通道; 同理工作流(如runFanout)
40
+ // 之各名額亦只是同行程之async函數呼叫, 無法把tool_calls往上層轉送。
41
+ // 故呼叫端若於body帶入tools, 本函數一律以TOOL_CALLS_UNSUPPORTED回報失敗而不假裝成功
42
+ // (實測Agnes於tool_calls時content為"\n\n"而非null, 不特別處理會靜默回傳空白內容)。
43
+ //
30
44
  // 【重試語意對齊execCli】4xx(429除外)為客戶端錯誤不可重試而立即中止;
31
45
  // 429/5xx/網路錯誤/逾時依maxRetries線性退避重試(間隔retryDelayMs*次數, 上限15000ms)。
32
46
  //
@@ -37,7 +51,7 @@ import getErrorResult from './getErrorResult.mjs'
37
51
 
38
52
 
39
53
  //預設值
40
- let DEFAULT_TIMEOUT_MS = 120000
54
+ let DEFAULT_TIMEOUT_MS = dfTimeoutMs //全套件統一預設300000
41
55
  let DEFAULT_RETRY_DELAY_MS = 5000
42
56
  let MAX_RETRY_DELAY_MS = 15000
43
57
 
@@ -184,13 +198,31 @@ async function callOnce(url, headers, body, timeoutMs, validator) {
184
198
  }
185
199
  }
186
200
 
187
- //取出choices[0].message.content
201
+ //取出choices[0]
188
202
  let content = null
203
+ let finishReason = ''
204
+ let toolCalls = null
189
205
  try {
190
206
  let j = JSON.parse(txt)
191
207
  content = get(j, 'choices.0.message.content', null)
208
+ finishReason = get(j, 'choices.0.finish_reason', '')
209
+ toolCalls = get(j, 'choices.0.message.tool_calls', null)
192
210
  }
193
211
  catch {}
212
+
213
+ //tool_calls, 本轉接器不支援工具迴圈(見檔頭), 明確回報而不假裝成功
214
+ //(Agnes於tool_calls時content為"\n\n"非null, 不攔截會靜默回傳空白內容)
215
+ if (finishReason === 'tool_calls' || (toolCalls !== null && toolCalls !== undefined)) {
216
+ return {
217
+ ok: false,
218
+ stdout: '',
219
+ stderr: strTruncate(txt, 1000, optTruncate),
220
+ code: res.status,
221
+ error: 'TOOL_CALLS_UNSUPPORTED: use a cli kind (opencode/claude/codex/antigravity) when tools are needed',
222
+ durationMs,
223
+ }
224
+ }
225
+
194
226
  if (content === null || content === undefined) {
195
227
  return {
196
228
  ok: false,
@@ -236,6 +268,8 @@ async function callOnce(url, headers, body, timeoutMs, validator) {
236
268
  *
237
269
  * 特點:
238
270
  * 免安裝CLI、免預先登入,給baseURL+key+model即可呼叫(如OpenCode Zen、Agnes等OpenAI相容閘道);
271
+ * 僅供純文字生成(摘要、分析、改寫、產出JSON等素材已在prompt內之任務)——
272
+ * 需要讀本機檔案、grep、執行指令、抓網頁等工具能力時,請改用CLI類kind(opencode/claude/codex/antigravity);
239
273
  * prompt走HTTP body,無命令列長度限制;
240
274
  * 錯誤依HTTP狀態碼分流:4xx(429除外)為客戶端錯誤不重試,429/5xx/網路錯誤/逾時依maxRetries線性退避重試;
241
275
  * 結果結構與逾時/驗證失敗之error字樣對齊execCli,可直接作為dispatchAi與dispatchAiFallback之kind('api-openai-compat')使用;
@@ -247,9 +281,9 @@ async function callOnce(url, headers, body, timeoutMs, validator) {
247
281
  * @param {String} opt.model 輸入模型ID字串,例如'deepseek-v4-flash-free'(Zen之模型名不帶opencode/前綴)、'agnes-2.0-flash'
248
282
  * @param {String} [opt.key=''] 輸入API key字串,以Bearer置於Authorization標頭,預設''代表不帶認證標頭
249
283
  * @param {String} [opt.system=''] 輸入system提示詞字串,將以system角色置於messages首位,預設''代表不帶
250
- * @param {Object} [opt.body={}] 輸入額外請求本體物件(如temperature、max_tokens、response_format),將併入預設body(同名鍵以此為準),預設{}
284
+ * @param {Object} [opt.body={}] 輸入額外請求本體物件(如temperature、max_tokens、response_format),將併入預設body(同名鍵以此為準),預設{}。注意本轉接器不支援工具,帶入tools而模型回tool_calls時一律以TOOL_CALLS_UNSUPPORTED回報失敗,需要工具請改用CLI類kind
251
285
  * @param {Object} [opt.headers={}] 輸入額外請求標頭物件,預設{}
252
- * @param {Number} [opt.timeoutMs=120000] 輸入逾時毫秒正整數,逾時將中止請求(含回應串流讀取),預設120000
286
+ * @param {Number} [opt.timeoutMs=300000] 輸入逾時毫秒正整數,逾時將中止請求(含回應串流讀取),全套件統一預設300000
253
287
  * @param {String|Function} [opt.validate=undefined] 輸入回覆內容驗證規則字串或自訂驗證函數,規則字串支援'nonempty'、'json'、'min:100',多規則可用逗號串接,預設undefined代表不驗證
254
288
  * @param {Number} [opt.maxRetries=0] 輸入失敗後最大重試次數非負整數,4xx(429除外)不重試,預設0
255
289
  * @param {Number} [opt.retryDelayMs=5000] 輸入重試間隔毫秒正整數,實際間隔為retryDelayMs乘以重試次數且上限15000ms,預設5000
@@ -1,10 +1,13 @@
1
1
  import get from 'lodash-es/get.js'
2
2
  import omit from 'lodash-es/omit.js'
3
+ import ispint from 'wsemi/src/ispint.mjs'
4
+ import cint from 'wsemi/src/cint.mjs'
3
5
  import isbol from 'wsemi/src/isbol.mjs'
4
6
  import isestr from 'wsemi/src/isestr.mjs'
5
7
  import execCli from 'wsemi/src/execCli.mjs'
6
8
  import getCliArgs from './getCliArgs.mjs'
7
9
  import getErrorResult from './getErrorResult.mjs'
10
+ import dfTimeoutMs from './dfTimeoutMs.mjs'
8
11
 
9
12
 
10
13
  // dispatchClaude.mjs — 以Claude Code CLI呼叫Claude模型
@@ -44,7 +47,7 @@ let OWN_KEYS = ['exe', 'model', 'skipPermissions', 'extraArgs', 'input']
44
47
  * @param {String} [opt.model=''] 輸入模型別名或模型ID字串,例如'sonnet'、'opus',預設''代表不帶`--model`旗標
45
48
  * @param {Boolean} [opt.skipPermissions=true] 輸入是否帶`--dangerously-skip-permissions`旗標布林值,false代表保留CLI權限閘門,預設true
46
49
  * @param {Array} [opt.extraArgs=[]] 輸入額外命令列旗標字串陣列,將接於固定旗標之後,預設[]
47
- * @param {Number} [opt.timeoutMs=120000] 輸入逾時毫秒正整數,逾時將強制關閉子進程及其子孫程序,預設120000
50
+ * @param {Number} [opt.timeoutMs=300000] 輸入逾時毫秒正整數,逾時將強制關閉子進程及其子孫程序,全套件統一預設300000
48
51
  * @param {String} [opt.cwd=process.cwd()] 輸入子進程工作目錄字串,預設process.cwd()
49
52
  * @param {String|Function} [opt.validate=undefined] 輸入stdout驗證規則字串或自訂驗證函數,規則字串支援'nonempty'、'json'、'min:100',多規則可用逗號串接,預設undefined代表不驗證
50
53
  * @param {Number} [opt.maxRetries=0] 輸入失敗後最大重試次數非負整數,預設0
@@ -56,7 +59,7 @@ let OWN_KEYS = ['exe', 'model', 'skipPermissions', 'extraArgs', 'input']
56
59
  *
57
60
  * let test = async () => {
58
61
  *
59
- * let r = await dispatchClaude('請只回覆兩個字:完成', { model: 'sonnet', timeoutMs: 120000 })
62
+ * let r = await dispatchClaude('請只回覆兩個字:完成', { model: 'sonnet' })
60
63
  * console.log(r.ok, r.stdout.trim())
61
64
  * // => true '完成'
62
65
  *
@@ -104,6 +107,15 @@ async function dispatchClaude(prompt, opt = {}) {
104
107
  extraArgs,
105
108
  )
106
109
 
110
+ //timeoutMs, 無效回退全套件統一預設(dfTimeoutMs=300000), 各轉接器一致令呼叫方無須記多套數字
111
+ let timeoutMs = get(opt, 'timeoutMs', null)
112
+ if (!ispint(timeoutMs)) {
113
+ timeoutMs = dfTimeoutMs
114
+ }
115
+ else {
116
+ timeoutMs = cint(timeoutMs)
117
+ }
118
+
107
119
  //optCli, 剔除本轉接器自用鍵後原樣轉傳, 令呼叫端可用execCli全部設定(例如onStdout、maxBuffer)
108
120
  let optCli = omit(opt, OWN_KEYS)
109
121
 
@@ -111,6 +123,7 @@ async function dispatchClaude(prompt, opt = {}) {
111
123
  return execCli(exe, args, {
112
124
  ...optCli,
113
125
  input: prompt,
126
+ timeoutMs,
114
127
  })
115
128
  }
116
129
 
@@ -1,9 +1,12 @@
1
1
  import get from 'lodash-es/get.js'
2
2
  import omit from 'lodash-es/omit.js'
3
+ import ispint from 'wsemi/src/ispint.mjs'
4
+ import cint from 'wsemi/src/cint.mjs'
3
5
  import isestr from 'wsemi/src/isestr.mjs'
4
6
  import execCli from 'wsemi/src/execCli.mjs'
5
7
  import getCliArgs from './getCliArgs.mjs'
6
8
  import getErrorResult from './getErrorResult.mjs'
9
+ import dfTimeoutMs from './dfTimeoutMs.mjs'
7
10
 
8
11
 
9
12
  // dispatchCodex.mjs — 以OpenAI Codex CLI呼叫GPT模型
@@ -44,7 +47,7 @@ let OWN_KEYS = ['exe', 'model', 'sandbox', 'extraArgs', 'input']
44
47
  * @param {String} [opt.model=''] 輸入模型ID字串,例如'gpt-5.6-luna',預設''代表不帶`-m`旗標
45
48
  * @param {String} [opt.sandbox='workspace-write'] 輸入沙箱模式字串,例如'read-only'、'workspace-write'、'danger-full-access',預設'workspace-write'
46
49
  * @param {Array} [opt.extraArgs=[]] 輸入額外命令列旗標字串陣列,例如['--config', 'model_reasoning_effort="max"'],將接於固定旗標之後,預設[]
47
- * @param {Number} [opt.timeoutMs=120000] 輸入逾時毫秒正整數,逾時將強制關閉子進程及其子孫程序,預設120000
50
+ * @param {Number} [opt.timeoutMs=300000] 輸入逾時毫秒正整數,逾時將強制關閉子進程及其子孫程序,全套件統一預設300000
48
51
  * @param {String} [opt.cwd=process.cwd()] 輸入子進程工作目錄字串,預設process.cwd()
49
52
  * @param {String|Function} [opt.validate=undefined] 輸入stdout驗證規則字串或自訂驗證函數,規則字串支援'nonempty'、'json'、'min:100',多規則可用逗號串接,預設undefined代表不驗證
50
53
  * @param {Number} [opt.maxRetries=0] 輸入失敗後最大重試次數非負整數,預設0
@@ -105,6 +108,15 @@ async function dispatchCodex(prompt, opt = {}) {
105
108
  extraArgs,
106
109
  )
107
110
 
111
+ //timeoutMs, 無效回退全套件統一預設(dfTimeoutMs=300000), 各轉接器一致令呼叫方無須記多套數字
112
+ let timeoutMs = get(opt, 'timeoutMs', null)
113
+ if (!ispint(timeoutMs)) {
114
+ timeoutMs = dfTimeoutMs
115
+ }
116
+ else {
117
+ timeoutMs = cint(timeoutMs)
118
+ }
119
+
108
120
  //optCli, 剔除本轉接器自用鍵後原樣轉傳, 令呼叫端可用execCli全部設定(例如onStdout、maxBuffer)
109
121
  let optCli = omit(opt, OWN_KEYS)
110
122
 
@@ -112,6 +124,7 @@ async function dispatchCodex(prompt, opt = {}) {
112
124
  return execCli(exe, args, {
113
125
  ...optCli,
114
126
  input: prompt,
127
+ timeoutMs,
115
128
  })
116
129
  }
117
130
 
@@ -1,10 +1,13 @@
1
1
  import get from 'lodash-es/get.js'
2
2
  import omit from 'lodash-es/omit.js'
3
+ import ispint from 'wsemi/src/ispint.mjs'
4
+ import cint from 'wsemi/src/cint.mjs'
3
5
  import isobj from 'wsemi/src/isobj.mjs'
4
6
  import isestr from 'wsemi/src/isestr.mjs'
5
7
  import execCli from 'wsemi/src/execCli.mjs'
6
8
  import getCliArgs from './getCliArgs.mjs'
7
9
  import getErrorResult from './getErrorResult.mjs'
10
+ import dfTimeoutMs from './dfTimeoutMs.mjs'
8
11
 
9
12
 
10
13
  // dispatchOpencode.mjs — 以opencode CLI呼叫AI模型
@@ -60,7 +63,7 @@ let OWN_KEYS = ['exe', 'model', 'key', 'provider', 'agent', 'config', 'extraArgs
60
63
  * @param {String} [opt.agent='build'] 輸入opencode代理名稱字串,預設'build'
61
64
  * @param {Array} [opt.extraArgs=[]] 輸入額外命令列旗標字串陣列,將接於固定旗標之後,預設[]
62
65
  * @param {Object} [opt.env=undefined] 輸入本次調用額外注入之環境變數物件,同時給予key與provider時會再併入OPENCODE_AUTH_CONTENT,預設undefined
63
- * @param {Number} [opt.timeoutMs=120000] 輸入逾時毫秒正整數,逾時將強制關閉子進程及其子孫程序,預設120000
66
+ * @param {Number} [opt.timeoutMs=300000] 輸入逾時毫秒正整數,逾時將強制關閉子進程及其子孫程序,全套件統一預設300000
64
67
  * @param {String} [opt.cwd=process.cwd()] 輸入子進程工作目錄字串,預設process.cwd()
65
68
  * @param {String|Function} [opt.validate=undefined] 輸入stdout驗證規則字串或自訂驗證函數,規則字串支援'nonempty'、'json'、'min:100',多規則可用逗號串接,預設undefined代表不驗證
66
69
  * @param {Number} [opt.maxRetries=0] 輸入失敗後最大重試次數非負整數,預設0
@@ -173,6 +176,15 @@ async function dispatchOpencode(prompt, opt = {}) {
173
176
  }
174
177
  }
175
178
 
179
+ //timeoutMs, 無效回退全套件統一預設(dfTimeoutMs=300000), 各轉接器一致令呼叫方無須記多套數字
180
+ let timeoutMs = get(opt, 'timeoutMs', null)
181
+ if (!ispint(timeoutMs)) {
182
+ timeoutMs = dfTimeoutMs
183
+ }
184
+ else {
185
+ timeoutMs = cint(timeoutMs)
186
+ }
187
+
176
188
  //optCli, 剔除本轉接器自用鍵後原樣轉傳, 令呼叫端可用execCli全部設定(例如onStdout、maxBuffer)
177
189
  let optCli = omit(opt, OWN_KEYS)
178
190
 
@@ -181,6 +193,7 @@ async function dispatchOpencode(prompt, opt = {}) {
181
193
  ...optCli,
182
194
  input: prompt,
183
195
  env,
196
+ timeoutMs,
184
197
  })
185
198
  }
186
199