w-dispatch-ai 1.0.34 → 1.0.36

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 (56) hide show
  1. package/README.md +11 -9
  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 +1 -1
  5. package/docs/adapters.mjs.html +1 -1
  6. package/docs/budgetFor.mjs.html +1 -1
  7. package/docs/buildValidator.mjs.html +1 -1
  8. package/docs/castPintOr.mjs.html +1 -1
  9. package/docs/dfTimeoutMs.mjs.html +1 -1
  10. package/docs/dispatchAi.mjs.html +1 -1
  11. package/docs/dispatchAiFallback.mjs.html +1 -1
  12. package/docs/dispatchAiWkf.mjs.html +1 -1
  13. package/docs/dispatchAntigravity.mjs.html +1 -1
  14. package/docs/dispatchApiOpenaiCompat.mjs.html +3 -2
  15. package/docs/dispatchApiOpenaiResponses.mjs.html +1 -1
  16. package/docs/dispatchApiTypesafeSystemone.mjs.html +1 -1
  17. package/docs/dispatchClaude.mjs.html +13 -3
  18. package/docs/dispatchCodex.mjs.html +1 -1
  19. package/docs/dispatchOpencode.mjs.html +43 -11
  20. package/docs/getCliArgs.mjs.html +1 -1
  21. package/docs/getErrorResult.mjs.html +1 -1
  22. package/docs/getErrorType.mjs.html +1 -1
  23. package/docs/global.html +11 -10
  24. package/docs/index.html +1 -1
  25. package/docs/quota_dfQuotaTimeoutMs.mjs.html +1 -1
  26. package/docs/quota_fetchQuotaJson.mjs.html +1 -1
  27. package/docs/quota_fromCodexUsageHttp.mjs.html +1 -1
  28. package/docs/quota_getQuotaAntigravity.mjs.html +1 -1
  29. package/docs/quota_getQuotaClaude.mjs.html +1 -1
  30. package/docs/quota_getQuotaCodex.mjs.html +1 -1
  31. package/docs/quota_readJsonOrNull.mjs.html +1 -1
  32. package/docs/quota_toQuotaLabel.mjs.html +1 -1
  33. package/docs/quota_toQuotaResult.mjs.html +1 -1
  34. package/docs/quota_toQuotaScopedLabel.mjs.html +1 -1
  35. package/docs/quota_toQuotaWindow.mjs.html +1 -1
  36. package/docs/readEnvFile.mjs.html +1 -1
  37. package/docs/resolveProviders.mjs.html +2 -2
  38. package/docs/wkf_callAiWithFallback.mjs.html +2 -2
  39. package/docs/wkf_createFileStore.mjs.html +1 -1
  40. package/docs/wkf_createUsageCounter.mjs.html +1 -1
  41. package/docs/wkf_extractJsonLoose.mjs.html +1 -1
  42. package/docs/wkf_noSideEffectPrefix.mjs.html +4 -3
  43. package/docs/wkf_runFanout.mjs.html +1 -1
  44. package/docs/wkf_runFanoutPipeline.mjs.html +1 -1
  45. package/docs/wkf_runRolePipeline.mjs.html +1 -1
  46. package/docs/wkf_salvageTruncatedArray.mjs.html +1 -1
  47. package/package.json +1 -1
  48. package/src/dispatchApiOpenaiCompat.mjs +2 -1
  49. package/src/dispatchClaude.mjs +12 -2
  50. package/src/dispatchOpencode.mjs +41 -9
  51. package/src/providers.mjs +47 -9
  52. package/src/resolveProviders.mjs +1 -1
  53. package/src/wkf/callAiWithFallback.mjs +1 -1
  54. package/src/wkf/noSideEffectPrefix.mjs +3 -2
  55. package/test/tools/fakeCliForTest.mjs +1 -0
  56. package/test/unit-dispatchOpencode.test.mjs +21 -0
@@ -70,7 +70,7 @@ import NO_SIDE_EFFECT from './noSideEffectPrefix.mjs'
70
70
  // 與金鑰無關之失敗而「整組跳過換下一家」(不換組內金鑰——同模型換金鑰仍是
71
71
  // 同樣的產出習慣); 端點不穩而偶發空回的模型, 以maxRetries調高令同鍵重試。
72
72
  //
73
- // 【防寫檔前綴】agentic CLI對cwd隔離免疫(會自行解析專案根目錄寫檔),
73
+ // 【防寫檔前綴】agentic CLI之cwd不是隔離邊界(可用絕對路徑寫到cwd外),
74
74
  // 故預設在prompt前掛「禁止建檔」約束(實測有效); 不需要時傳promptPrefix:''關閉。
75
75
  // 措辭已移至獨立模組wkf/noSideEffectPrefix.mjs(單一來源)——不經本層、
76
76
  // 直接呼叫dispatchAiFallback的呼叫端亦應引用同一份, 措辭修正時全體同步;
@@ -274,7 +274,7 @@ export { buildChain, NO_SIDE_EFFECT }
274
274
  <br class="clear">
275
275
 
276
276
  <footer>
277
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Wed Sep 23 2026 14:35:11 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
277
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Thu Sep 24 2026 08:47:53 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
278
278
  </footer>
279
279
 
280
280
  <script>prettyPrint();</script>
@@ -162,7 +162,7 @@ export default createFileStore
162
162
  <br class="clear">
163
163
 
164
164
  <footer>
165
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Wed Sep 23 2026 14:35:11 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
165
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Thu Sep 24 2026 08:47:53 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
166
166
  </footer>
167
167
 
168
168
  <script>prettyPrint();</script>
@@ -200,7 +200,7 @@ export default createUsageCounter
200
200
  <br class="clear">
201
201
 
202
202
  <footer>
203
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Wed Sep 23 2026 14:35:11 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
203
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Thu Sep 24 2026 08:47:53 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
204
204
  </footer>
205
205
 
206
206
  <script>prettyPrint();</script>
@@ -167,7 +167,7 @@ export default extractJsonLoose
167
167
  <br class="clear">
168
168
 
169
169
  <footer>
170
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Wed Sep 23 2026 14:35:11 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
170
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Thu Sep 24 2026 08:47:53 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
171
171
  </footer>
172
172
 
173
173
  <script>prettyPrint();</script>
@@ -53,8 +53,9 @@
53
53
  // 本套件早已修正, 形成「套件修了、消費端沒跟上」的分岔。抽成獨立模組後
54
54
  // 所有路徑引用同一來源, 措辭再修正時全體同步。
55
55
  //
56
- // 【為何需要此前綴】agentic CLI自帶檔案讀寫工具且對cwd隔離免疫(會自行解析
57
- // 專案根目錄以絕對路徑寫檔), 實測會「順手」把結果另存孤兒檔,
56
+ // 【為何需要此前綴】agentic CLI自帶檔案讀寫工具, 且cwd不是隔離邊界(可用絕對路徑
57
+ // 寫到cwd外; 另opencode曾因沿用父進程之PWD而在啟動目錄作業, 2026-09-23已於
58
+ // dispatchOpencode修正, 見該檔檔頭), 實測會「順手」把結果另存孤兒檔,
58
59
  // 甚至可能覆蓋呼叫端資料本體(殷鑑: 2026-08-10執行任務歷史.md遭AI覆寫、
59
60
  // 評比腳本繞過前綴又產生根目錄孤兒檔); 各家CLI的權限機制彼此不同(codex有sandbox、
60
61
  // claude有--disallowedTools、opencode有config.permission),
@@ -108,7 +109,7 @@ export default NO_SIDE_EFFECT
108
109
  <br class="clear">
109
110
 
110
111
  <footer>
111
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Wed Sep 23 2026 14:35:11 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
112
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Thu Sep 24 2026 08:47:53 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
112
113
  </footer>
113
114
 
114
115
  <script>prettyPrint();</script>
@@ -217,7 +217,7 @@ export { defaultIntegratePrompt }
217
217
  <br class="clear">
218
218
 
219
219
  <footer>
220
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Wed Sep 23 2026 14:35:11 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
220
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Thu Sep 24 2026 08:47:53 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
221
221
  </footer>
222
222
 
223
223
  <script>prettyPrint();</script>
@@ -161,7 +161,7 @@ export default runFanoutPipeline
161
161
  <br class="clear">
162
162
 
163
163
  <footer>
164
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Wed Sep 23 2026 14:35:11 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
164
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Thu Sep 24 2026 08:47:53 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
165
165
  </footer>
166
166
 
167
167
  <script>prettyPrint();</script>
@@ -183,7 +183,7 @@ export default runRolePipeline
183
183
  <br class="clear">
184
184
 
185
185
  <footer>
186
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Wed Sep 23 2026 14:35:11 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
186
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Thu Sep 24 2026 08:47:53 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
187
187
  </footer>
188
188
 
189
189
  <script>prettyPrint();</script>
@@ -154,7 +154,7 @@ export default salvageTruncatedArray
154
154
  <br class="clear">
155
155
 
156
156
  <footer>
157
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Wed Sep 23 2026 14:35:11 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
157
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Thu Sep 24 2026 08:47:53 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
158
158
  </footer>
159
159
 
160
160
  <script>prettyPrint();</script>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "w-dispatch-ai",
3
- "version": "1.0.34",
3
+ "version": "1.0.36",
4
4
  "main": "dist/w-dispatch-ai.umd.js",
5
5
  "dependencies": {
6
6
  "wsemi": "^1.9.3"
@@ -239,7 +239,8 @@ async function callOnce(url, headers, body, timeoutMs, validator) {
239
239
  * // => true 完成
240
240
  *
241
241
  * //OpenCode Zen(即opencode CLI之自家閘道), 模型名不帶opencode/前綴; 走此端點者見providers.mjs檔頭之端點表
242
- * //注意Zen之免費模型自2026-09-17起禁止REST直呼(403 FreeTierError), 該類模型須改走opencode CLI(kind:'opencode')
242
+ * //注意Zen之免費模型自2026-09-17起多數禁止REST直呼(403 FreeTierError), 該類模型須改走opencode CLI(kind:'opencode');
243
+ * //閘門係逐模型套用(如space-bunny-free於2026-09-24實測REST仍200), 能否走REST以實測為準, 例外見providers.mjs檔頭
243
244
  * let r2 = await dispatchApiOpenaiCompat('請只回覆兩個字:完成', {
244
245
  * baseURL: 'https://opencode.ai/zen/v1',
245
246
  * key: 'sk-xxxxxx',
@@ -19,6 +19,16 @@ import dfTimeoutMs from './dfTimeoutMs.mjs'
19
19
  //
20
20
  // 【認證】沿用Claude Code既有登入狀態(帳號層級),不需也不支援逐次注入API key,
21
21
  // 故本轉接器無key參數——輪替時它是「一個獨立的供應商」而非「另一把金鑰」。
22
+ //
23
+ // 【勿帶--bare, 並留意其未來成為-p預設(2026-09-23查官方headless文件)】文件原文:
24
+ // 「--bare is the recommended mode for scripted and SDK calls, and will become the default for -p in a
25
+ // future release」且「In bare mode, Claude Code never reads OAuth credentials or the system keychain」。
26
+ // 本轉接器靠訂閱登入(OAuth), 故extraArgs勿帶--bare(會直接認證失敗); 若日後某版把-p預設改為bare,
27
+ // 所有訂閱條目會一併失效——徵狀為非零離開碼, 失敗訊息印在stdout而非stderr(官方: 執行期失敗如未登入
28
+ // 以result印在stdout)。2.1.280尚無退出bare之旗標, 無法預先處理; 每次升級Claude Code後跑一次claude
29
+ // 條目即可偵測, 屆時再查該版之退出方式調整此處。
30
+ // 另兩點同日查證: stdin傳入之prompt上限10MB(超過即非零離開, 見@param prompt);
31
+ // 2.1.280起Pro/Team Standard方案未給model時預設由Sonnet改為Opus(只影響未帶--model之呼叫)。
22
32
 
23
33
 
24
34
  //預設值
@@ -41,10 +51,10 @@ let OWN_KEYS = ['exe', 'model', 'skipPermissions', 'extraArgs', 'input']
41
51
  * 可給予skipPermissions為false保留CLI權限閘門;
42
52
  * 本函數不會reject,一律以結果物件之ok與error欄位回報成敗
43
53
  *
44
- * @param {String} prompt 輸入提示詞字串,一律以stdin傳入子進程
54
+ * @param {String} prompt 輸入提示詞字串,一律以stdin傳入子進程(Claude Code之stdin上限10MB,超過即非零離開)
45
55
  * @param {Object} [opt={}] 輸入設定物件,預設{}
46
56
  * @param {String} [opt.exe='claude'] 輸入claude執行檔名稱或絕對路徑字串,給予名稱時由execCli自系統PATH解析,預設'claude'
47
- * @param {String} [opt.model=''] 輸入模型別名或模型ID字串,例如'claude-opus-5-5'(全名, 固定版本)、'opus'或'sonnet'(別名, 隨CLI指向最新版),預設''代表不帶`--model`旗標
57
+ * @param {String} [opt.model=''] 輸入模型別名或模型ID字串,例如'claude-opus-5-5'(全名, 固定版本)、'opus'或'sonnet'(別名, 隨CLI指向最新版),預設''代表不帶`--model`旗標,由CLI依方案決定(2.1.280起Pro/Team Standard亦預設Opus)
48
58
  * @param {Boolean} [opt.skipPermissions=true] 輸入是否帶`--dangerously-skip-permissions`旗標布林值,false代表保留CLI權限閘門,預設true
49
59
  * @param {Array} [opt.extraArgs=[]] 輸入額外命令列旗標字串陣列,將接於固定旗標之後,預設[]
50
60
  * @param {Number} [opt.timeoutMs=300000] 輸入逾時毫秒正整數,逾時將強制關閉子進程及其子孫程序,全套件統一預設300000
@@ -1,3 +1,4 @@
1
+ import path from 'path'
1
2
  import get from 'lodash-es/get.js'
2
3
  import omit from 'lodash-es/omit.js'
3
4
  import isobj from 'wsemi/src/isobj.mjs'
@@ -22,16 +23,30 @@ import dfTimeoutMs from './dfTimeoutMs.mjs'
22
23
  // `if (process.env.OPENCODE_AUTH_CONTENT) return JSON.parse(...)`, 解析成功即整份取代, 不與auth.json合併;
23
24
  // 空字串為falsy、非法JSON會落回讀auth.json。auth.json位於<XDG_DATA_HOME|~/.local/share>/opencode/。
24
25
  //
25
- // 【本轉接器只驗證過opencode v1(實測至1.18.31), v2尚未支援(2026-09-22查證)】
26
- // opencode已另發布v2(當時2.0.6, npm套件@opencode/cli, 文件自成一棵樹 https://opencode.ai/v2/docs),
27
- // 官方〈Migrate from V1〉明載「OpenCode 1 and OpenCode 2 both use the opencode command and are no longer
28
- // installed side by side by default... the V2 curl installer replaces the V1 binary」——即升級v2會取代同名執行檔。
29
- // v2仍有`opencode run`(文件列為自動化用途)與`--agent`, 但至少三處與本轉接器之假設不同, 升級前須逐一實測:
26
+ // 【本轉接器只驗證過opencode v1(實測至1.18.32), v2尚未支援(2026-09-22查證, 2026-09-23補④)】
27
+ // opencode已另發布v2(2026-09-23已至2.0.15, npm套件@opencode/cli, bin另含opencode2; 文件自成一棵樹
28
+ // https://opencode.ai/v2/docs), 官方〈Migrate from V1〉明載「OpenCode 1 and OpenCode 2 both use the opencode
29
+ // command and are no longer installed side by side by default... the V2 curl installer replaces the V1 binary」
30
+ // ——即升級v2會取代同名執行檔。v1之npm套件opencode-ai(1.18.32)仍為latest且未標deprecated。
31
+ // v2仍有`opencode run`(文件列為自動化用途)與`--agent`, 但至少四處與本轉接器之假設不同, 升級前須逐一實測:
30
32
  // ① 模型旗標: v2文件一律寫`--model provider/model`, 本轉接器用`-m`(v2是否保留為別名未驗證);
31
- // ② provider設定形狀: v2改為`providers.<id>.package: "@opencode/ai/providers/openai-compatible"`,
33
+ // ② provider設定形狀: v2改為`providers.<id>.package`(單數provider→providers, npm→package並加aisdk:前綴),
32
34
  // 與本轉接器經OPENCODE_CONFIG_CONTENT注入之v1形狀(provider.<id>.npm)不同;
33
- // ③ 金鑰注入: v2憑證走/connect與新憑證庫, OPENCODE_AUTH_CONTENT是否仍被讀取未驗證。
34
- // 故本機若升v2, 請先以單一呼叫實測上述三項再調整本檔; 條目端可先以exe指定v1執行檔路徑過渡。
35
+ // ③ 金鑰注入: v2憑證走/connect與新憑證庫, OPENCODE_AUTH_CONTENT是否仍被讀取未驗證;
36
+ // ④ 權限設定形狀: v2改為單一有序`permissions`陣列, 動作亦改名(bash→shell、task→subagent、write/patch→edit),
37
+ // providers.mjs之OC_READONLY({edit:'deny', bash:'ask'})為v1形狀, 在v2不會被認得——唯讀鎖等同失效,
38
+ // 升v2前須以金絲雀(要求寫檔, 驗未落地)重驗防寫, 不可假設仍有效。
39
+ // 故本機若升v2, 請先以單一呼叫實測上述四項再調整本檔; 條目端可先以exe指定v1執行檔路徑過渡。
40
+ //
41
+ // 【cwd須同步PWD環境變數(2026-09-23實測後新增)】opencode run決定session目錄之原始碼為
42
+ // `const root = Filesystem.resolve(process.env.PWD ?? process.cwd())`(packages/opencode/src/cli/cmd/run.ts),
43
+ // 即「繼承來的PWD優先於子進程真實cwd」。Git Bash與Linux/macOS之shell皆會設PWD, 故呼叫端給opt.cwd而
44
+ // 父進程PWD指向他處時, opencode會在父進程目錄作業: 讀相對路徑落空(回NOTFOUND且ok:true, 靜默失敗),
45
+ // 允許寫檔時檔案落在父進程目錄(本專案曾因此於根目錄留下金絲雀孤兒檔, 當時誤判為「以git根目錄為準」)。
46
+ // 實測(cwd指向探針目錄, 父PWD=專案根): 不處理→working directory為專案根、讀MARKER.txt失敗;
47
+ // 注入PWD=cwd→正確; 帶--dir亦正確。採注入PWD: 不動命令列(與--attach等extraArgs無衝突), 語意即shell
48
+ // 於該目錄啟動程式時之環境; 且以本函數之值蓋過opt.env之PWD(呼叫端常整份展開process.env, 若讓其覆寫
49
+ // 則缺陷靜默回歸)。claude與codex以同組探針實測皆遵循spawn cwd, 不受影響, 故只在本轉接器處理。
35
50
  //
36
51
  // 【Zen免費層閘門與config注入(2026-09-18實測)】opencode自2026-09-17起限制免費模型只准在opencode本體內使用
37
52
  // (403 FreeTierError: free tier can only be used from within OpenCode)。CLI本身可通, 但經OPENCODE_CONFIG_CONTENT
@@ -39,6 +54,14 @@ import dfTimeoutMs from './dfTimeoutMs.mjs'
39
54
  // 少了bash即被判非opencode而403; 改permission.bash:'ask'則通過, 且非互動run下ask自動拒絕, 仍為機械防寫。
40
55
  // 詳見providers.mjs檔頭【Zen免費層閘門】與OC_READONLY常數。
41
56
  //
57
+ // 【Zen免費層429限流時run不會結束(2026-09-23實測, 1.18.32)】REST探測0.7s即回429 FreeUsageLimitError
58
+ // (Rate limit exceeded), 但`opencode run`收到同一錯誤後不退出: 預設stderr只有session標頭, 直到呼叫端逾時;
59
+ // 帶--print-logs --log-level DEBUG可見`stream error ... AI_APICallError: Rate limit exceeded`, 其後再無輸出。
60
+ // 不經本套件直接跑原始CLI亦同, 非本轉接器所致。對本套件之意義: 此情形以TIMEOUT回報(errorType 'timeout'),
61
+ // dispatchAiFallback內建之TIMEOUT冷卻觸發可涵蓋, 但每次嘗試都耗滿timeoutMs——免費oc:條目宜以
62
+ // resolveProviders之patch給較短timeoutMs; 需在stderr看到字樣(供coolDetect)時於extraArgs加
63
+ // --print-logs --log-level ERROR。
64
+ //
42
65
  // 【不讀auth.json之匿名呼叫: useStoredAuth:false(2026-09-17實測後新增)】
43
66
  // 未注入金鑰時opencode會自動沿用auth.json之登入; 該帳號工作區若未開某免費模型(如union-alpha),
44
67
  // 就回`Error: Model is disabled`, 而同一模型在「無auth.json」之機器上匿名可用——結果隨執行機器而異。
@@ -95,7 +118,7 @@ let OWN_KEYS = ['exe', 'model', 'key', 'provider', 'useStoredAuth', 'agent', 'co
95
118
  * @param {Array} [opt.extraArgs=[]] 輸入額外命令列旗標字串陣列,將接於固定旗標之後,預設[]
96
119
  * @param {Object} [opt.env=undefined] 輸入本次調用額外注入之環境變數物件,同時給予key與provider時會再併入OPENCODE_AUTH_CONTENT,預設undefined
97
120
  * @param {Number} [opt.timeoutMs=300000] 輸入逾時毫秒正整數,逾時將強制關閉子進程及其子孫程序,全套件統一預設300000
98
- * @param {String} [opt.cwd=process.cwd()] 輸入子進程工作目錄字串,預設process.cwd()
121
+ * @param {String} [opt.cwd=process.cwd()] 輸入子進程工作目錄字串,預設process.cwd();其絕對路徑並同步注入環境變數PWD(opencode以繼承之PWD優先於真實cwd決定session目錄,見檔頭),呼叫端env內之PWD會被覆寫
99
122
  * @param {String|Function} [opt.validate=undefined] 輸入stdout驗證規則字串或自訂驗證函數,規則字串支援'nonempty'、'json'、'min:100',多規則可用逗號串接,預設undefined代表不驗證
100
123
  * @param {Number} [opt.maxRetries=0] 輸入失敗後最大重試次數非負整數,預設0
101
124
  * @returns {Promise} 回傳Promise,resolve回傳結果物件,內含ok(是否成功布林值)、stdout(標準輸出字串)、stderr(標準錯誤字串)、code(離開碼)、error(錯誤訊息字串,成功時為空字串)、durationMs(耗時毫秒)、attempts(實際嘗試次數),本函數不會reject
@@ -214,6 +237,15 @@ async function dispatchOpencode(prompt, opt = {}) {
214
237
  }
215
238
  }
216
239
 
240
+ //PWD同步為有效cwd之絕對路徑(有給cwd則解析之, 否則process.cwd(), 與execCli之spawn cwd同源),
241
+ //以本值蓋過呼叫端env之PWD——opencode以繼承之PWD優先於真實cwd決定session目錄(見檔頭【cwd須同步PWD】)
242
+ let cwdEff = get(opt, 'cwd', null)
243
+ cwdEff = isestr(cwdEff) ? path.resolve(cwdEff) : process.cwd()
244
+ env = {
245
+ ...env,
246
+ PWD: cwdEff,
247
+ }
248
+
217
249
  //timeoutMs, 無效回退全套件統一預設(dfTimeoutMs=300000), 各轉接器一致令呼叫方無須記多套數字
218
250
  let timeoutMs = castPintOr(get(opt, 'timeoutMs', null), dfTimeoutMs)
219
251
 
package/src/providers.mjs CHANGED
@@ -24,14 +24,16 @@
24
24
  // v2頁列為Free但實測全不可用(CLI回UnknownError、REST回400 Upstream request failed: Model is unavailable):
25
25
  // deepseek-v4-flash-free / laguna-s-2.1-free / ling-3.0-tiny-free / longcat-2.0-free / north-mini-code-free
26
26
  // v1頁有而v2頁未列, 但實測可用: mimo-v2.6-flash-free / jev-1.13-free / muse-spark-1.2與1.3 / nemotron-3.5-lightning-free
27
- // 兩頁皆列且實測可用: big-pickle(CLI 7.1s) / mimo-v2.5-free / nemotron-3-ultra-free
27
+ // 兩頁皆列且實測可用: big-pickle(CLI 7.1s) / mimo-v2.5-free / nemotron-3-ultra-free /
28
+ // space-bunny-free(2026-09-24新上線: CLI 8.1s、REST 1.9s)
28
29
  // 故判準固定為: 文件只用於「端點種類與計價」, 「能不能用」一律以/zen/v1/models或`opencode models`加實測為準。
29
30
  //
30
31
  // ★ 核心事實: zen「不是」單一OpenAI相容端點, 端點依模型家族而異 ★
31
32
  // 依官方文件之端點欄(2026-09-22以v2 Console/Models頁複查, 分佈與v1一致):
32
33
  // /zen/v1/chat/completions @ai-sdk/openai-compatible ← 本套件api-openai-compat僅支援此種
33
34
  // mimo-v2.5-free / ling-3.0-flash-fin-free / nemotron-3-ultra-free /
34
- // nemotron-3.5-lightning-free / big-pickle / glm-* / kimi-* / minimax-* / deepseek-v4-*
35
+ // nemotron-3.5-lightning-free / big-pickle / space-bunny-free(2026-09-24查證) /
36
+ // glm-* / kimi-* / minimax-* / deepseek-v4-*
35
37
  // /zen/v1/responses @ai-sdk/openai (OpenAI Responses API, 非chat/completions)
36
38
  // muse-spark-1.2-contributor-free / muse-spark-1.3-contributor-free / GPT系 / Grok系
37
39
  // /zen/v1/messages @ai-sdk/anthropic Claude系 / Qwen系(qwen3.5~3.7-plus/max)
@@ -77,9 +79,11 @@
77
79
  // 屬刻意之反濫用政策; 付費模型不受限) ★
78
80
  // 症狀: 403 FreeTierError「OpenCode's free tier can only be used from within OpenCode」。
79
81
  // 判定依據(2026-09-18本機矩陣實測, opencode 1.18.31):
80
- // - REST直呼(任何金鑰): /chat/completions與/responses之免費模型一律403 → 該類zen:條目自此為政策性失效。
81
- // 例外(2026-09-22實測): /systemone之jev-1.13-free不受此閘門, 帶金鑰或匿名皆200(0.6~1.2s)——
82
- // 閘門只套在對話型免費模型, 故zen:jev-1.13-free之REST條目可用(見該條)。
82
+ // - REST直呼(任何金鑰): /chat/completions與/responses之免費模型(當日逐一實測者)一律403 → 該類zen:條目自此為政策性失效。
83
+ // 例外(2026-09-22實測): /systemone之jev-1.13-free不受此閘門, 帶金鑰或匿名皆200(0.6~1.2s),
84
+ // 故zen:jev-1.13-free之REST條目可用(見該條)。
85
+ // 例外(2026-09-24實測): /chat/completions之space-bunny-free亦不受此閘門, 匿名與帶金鑰皆200(1.9~2.0s)——
86
+ // 即閘門係逐模型套用, 並非「對話型免費模型一律擋」; 例外可能隨時收回, 新模型能否走REST一律以實測為準。
83
87
  // - opencode CLI匿名或帶金鑰: 通過; 但注入的設定若把bash工具deny掉(permission.bash:'deny'、
84
88
  // 舊式tools.bash:false、agent覆寫deny)即被判非opencode而403; 只deny edit、或改為ask則通過。
85
89
  // 即閘門以「請求之工具清單是否含bash」為指紋之一(另一指紋為User-Agent之版本字串, 非正式版build被拒)。
@@ -92,8 +96,8 @@
92
96
  // (實測: muse-spark 1.2/1.3之REST 500而CLI皆秒級成功)。CLI版模型id為
93
97
  // `opencode/<model-id>`, 可用 `opencode models` 列出; 兩者屬不同供應商條目(見id命名)。
94
98
  //
95
- // 【zen免費模型清單為「更新日快照」, 不保證即為當前狀態】zen:系收錄截至2026-08-21
96
- // 經GET https://opencode.ai/zen/v1/models 查得之免費模型(*-free), 不做好用篩選——
99
+ // 【zen免費模型清單為「更新日快照」, 不保證即為當前狀態】zen:系收錄起於2026-08-21
100
+ // 經GET https://opencode.ai/zen/v1/models 查得之免費模型(*-free), 其後之增刪見本段漂移紀錄(最近一次2026-09-24), 不做好用篩選——
97
101
  // 新模型會上線、舊模型可能下架或限流, 且各模型能力/速度/輸出習慣差異極大
98
102
  // (各條目註解記錄已測特性), 由呼叫端自行評估選用; 要查當前清單, 以OPENCODE_KEYS
99
103
  // 打上述/models端點即可。暫時打不通的條目(限流/額度/上游中斷)依本套件哲學保留不移除:
@@ -134,6 +138,9 @@
134
138
  // 且維護者已明示為刻意政策而非故障, 故不適用「暫時失敗保留」之哲學。同模型之CLI版(oc:opencode/*)
135
139
  // 不受影響且已收錄, 能力覆蓋相同。日後政策放寬時, 依檔頭收錄檢核重新收錄即可(端點種類見端點表)。
136
140
  // 副作用: 移除後預設清單已無api-openai-responses之條目(該kind仍受支援, 僅無預設條目)。
141
+ // 2026-09-24之漂移: 新增space-bunny-free之兩條目(oc:與zen:, 使用者指示)。官方v1/v2文件〈Pricing〉全Free,
142
+ // 說明為限時免費之stealth推理模型(供應商零保留、不以資料訓練), 端點/chat/completions; CLI清單與/zen/v1/models皆有。
143
+ // 與其他對話型免費模型不同, 其REST未被免費層閘門擋下(匿名與帶金鑰皆200), 故兩路皆收; 實測數據見兩條目註解。
137
144
  //
138
145
  // 【使用方式】
139
146
  // import providers from 'w-dispatch-ai/src/providers.mjs'
@@ -164,7 +171,8 @@
164
171
  // bash:'ask' 而非'deny'——2026-09-18實測: Zen免費層閘門以「bash工具是否存在」判定是否為opencode本體,
165
172
  // deny會把bash自工具清單移除而被判非opencode(403 FreeTierError), ask則工具仍在;
166
173
  // 而`opencode run`為非互動, ask一律自動拒絕(stderr: The user rejected permission),
167
- // 金絲雀實測寫檔與shell建檔皆未落地, 故仍為機械鎖。注意呼叫端勿另傳--auto(會把ask放行)。
174
+ // 金絲雀實測寫檔與shell建檔皆未落地, 故仍為機械鎖(2026-09-23於1.18.32重驗仍自動拒絕)。
175
+ // 注意呼叫端勿另傳--auto(2026-09-23實測: 帶上即放行ask, 模型以node寫檔落地)。
168
176
  // agnes/poolside走opencode但非Zen免費層, 不受閘門影響, 為對稱亦用同一鎖(bash:ask之拒絕行為相同)。
169
177
  let OC_READONLY = { edit: 'deny', bash: 'ask' }
170
178
 
@@ -179,6 +187,10 @@ let OC_READONLY = { edit: 'deny', bash: 'ask' }
179
187
  // --strict-mcp-config 排除所有MCP工具(--tools不管MCP, 單用時claude.ai Docs之寫入工具仍在)。
180
188
  // 同日實測: 工具清單恰為Glob,Grep,Read; 要求寫檔(含指明可用PowerShell)未落地; 讀檔正常作答。
181
189
  // 代價: 失去WebFetch/WebSearch等網路讀取工具; 需要時於條目覆寫extraArgs自行加入。
190
+ // 邊界(2026-09-23查官方permissions文件): 此鎖只管模型可用之工具; 資料夾未受信任時-p仍會執行該專案
191
+ // .claude/settings.json之hooks與env(官方列為「Used」)。於不信任之目錄派工可於條目另加
192
+ // '--setting-sources user'(同日實測可與本鎖及skip並用); 更嚴之'--restricted'須搭配skipPermissions:false
193
+ // (restricted拒絕bypassPermissions, 同日實測帶skip即報錯)。勿帶--bare(不讀OAuth登入, 見dispatchClaude檔頭)。
182
194
  let CLAUDE_READONLY = ['--tools', 'Read,Glob,Grep', '--strict-mcp-config']
183
195
 
184
196
 
@@ -273,6 +285,20 @@ let providers = [
273
285
  //2026-09-22新增(官方文件與CLI清單皆有, 端點/chat/completions): 匿名CLI實測7.0s成功;
274
286
  //REST不收zen:版——同日實測403 FreeTierError(對話型免費模型之閘門, 見檔頭)
275
287
  },
288
+ {
289
+ id: 'oc:opencode/space-bunny-free',
290
+ model: 'opencode/space-bunny-free',
291
+ kind: 'opencode',
292
+ provider: 'opencode',
293
+ useStoredAuth: false,
294
+ config: {
295
+ permission: OC_READONLY,
296
+ },
297
+ //2026-09-24新增(官方v1/v2文件、CLI清單與/zen/v1/models皆有; 〈Pricing〉全Free, 限時免費之stealth推理模型,
298
+ //context 1M、可輸入圖片與影片)。匿名CLI實測(opencode 1.18.32): 簡答8.1s、推理題34.9s答對、
299
+ //read工具讀相對路徑13.2s正確、要求寫檔被OC_READONLY擋下未落地。官方〈Privacy〉載明其供應商零保留
300
+ //且不以資料訓練(與big-pickle及各*-free模型之「可能用於改進模型」不同)。同模型另有zen:REST版(見該條)
301
+ },
276
302
  {
277
303
  id: 'agy:gemini-3.8-flash-high',
278
304
  model: 'gemini-3.8-flash-high', //2026-09-03自3.7升版(agy 1.1.25): 實測12.5s vs 3.7之62.2s, 讀檔14.6s vs 57.6s
@@ -343,7 +369,7 @@ let providers = [
343
369
  //付費版jev-1.13未收: 同日以第1把金鑰實測403(Upstream request failed: Model access is disabled),
344
370
  //屬該金鑰工作區未開此模型; 要收須先於Zen後台開啟並確認計費($0.042/1M輸入)。
345
371
  },
346
- //zen:系為2026-08-21快照(檔頭聲明), 各條註記當日以「請只回覆兩個字:完成」實測之結果
372
+ //REST文字生成條目(api-openai-compat): agnes:/poolside:走各家官方端點; zen:走OpenCode Zen轉售(端點表與免費層閘門見檔頭)
347
373
  {
348
374
  id: 'agnes:agnes-3.0-flash',
349
375
  model: 'agnes-3.0-flash',
@@ -363,6 +389,18 @@ let providers = [
363
389
  chat_template_kwargs: { enable_thinking: false },
364
390
  },
365
391
  },
392
+ {
393
+ id: 'zen:space-bunny-free',
394
+ model: 'space-bunny-free',
395
+ kind: 'api-openai-compat',
396
+ envVar: 'OPENCODE_KEYS',
397
+ baseURL: 'https://opencode.ai/zen/v1',
398
+ body: { max_tokens: 32768 },
399
+ //2026-09-24新增: 對話型免費模型中少數REST可通者(免費層閘門未套用, 見檔頭; 例外可能隨時收回)——
400
+ //匿名1.9s、第1把金鑰2.0s皆200。max_tokens刻意不用zen條目慣例之8192: 此為推理模型, 推理token計入max_tokens,
401
+ //同日實測列10縣市一題即用5222(推理4849+正文373), 8192易截斷, 而本轉接器遇截斷仍回ok(內容殘缺或為空);
402
+ //長文(README前8000字)摘要成JSON一題用3385(推理3045), 三題finish_reason皆stop, 故取32768留約6倍餘裕。
403
+ },
366
404
 
367
405
  ]
368
406
 
@@ -57,7 +57,7 @@ import strFindSimilar from 'wsemi/src/strFindSimilar.mjs'
57
57
  * //全取: envVar展開為keys, 缺環境變數者列入skipped
58
58
  * let { providers, table, skipped } = resolveProviders(providersAll, { env })
59
59
  * console.log(providers.length, skipped)
60
- * // => 15 []
60
+ * // => 17 []
61
61
  *
62
62
  * //pick打錯字時, missing附拼寫提示hints(最接近之可用id)
63
63
  * let rm = resolveProviders(providersAll, { env, pick: ['poolside/laguna-s-2.1'] })
@@ -23,7 +23,7 @@ import NO_SIDE_EFFECT from './noSideEffectPrefix.mjs'
23
23
  // 與金鑰無關之失敗而「整組跳過換下一家」(不換組內金鑰——同模型換金鑰仍是
24
24
  // 同樣的產出習慣); 端點不穩而偶發空回的模型, 以maxRetries調高令同鍵重試。
25
25
  //
26
- // 【防寫檔前綴】agentic CLI對cwd隔離免疫(會自行解析專案根目錄寫檔),
26
+ // 【防寫檔前綴】agentic CLI之cwd不是隔離邊界(可用絕對路徑寫到cwd外),
27
27
  // 故預設在prompt前掛「禁止建檔」約束(實測有效); 不需要時傳promptPrefix:''關閉。
28
28
  // 措辭已移至獨立模組wkf/noSideEffectPrefix.mjs(單一來源)——不經本層、
29
29
  // 直接呼叫dispatchAiFallback的呼叫端亦應引用同一份, 措辭修正時全體同步;
@@ -6,8 +6,9 @@
6
6
  // 本套件早已修正, 形成「套件修了、消費端沒跟上」的分岔。抽成獨立模組後
7
7
  // 所有路徑引用同一來源, 措辭再修正時全體同步。
8
8
  //
9
- // 【為何需要此前綴】agentic CLI自帶檔案讀寫工具且對cwd隔離免疫(會自行解析
10
- // 專案根目錄以絕對路徑寫檔), 實測會「順手」把結果另存孤兒檔,
9
+ // 【為何需要此前綴】agentic CLI自帶檔案讀寫工具, 且cwd不是隔離邊界(可用絕對路徑
10
+ // 寫到cwd外; 另opencode曾因沿用父進程之PWD而在啟動目錄作業, 2026-09-23已於
11
+ // dispatchOpencode修正, 見該檔檔頭), 實測會「順手」把結果另存孤兒檔,
11
12
  // 甚至可能覆蓋呼叫端資料本體(殷鑑: 2026-08-10執行任務歷史.md遭AI覆寫、
12
13
  // 評比腳本繞過前綴又產生根目錄孤兒檔); 各家CLI的權限機制彼此不同(codex有sandbox、
13
14
  // claude有--disallowedTools、opencode有config.permission),
@@ -61,6 +61,7 @@ process.stdin.on('end', () => {
61
61
  OPENCODE_AUTH_CONTENT: process.env.OPENCODE_AUTH_CONTENT || '',
62
62
  OPENCODE_CONFIG_CONTENT: process.env.OPENCODE_CONFIG_CONTENT || '',
63
63
  FAKE_ENV: process.env.FAKE_ENV || '',
64
+ PWD: process.env.PWD || '',
64
65
  },
65
66
  }
66
67
  let end = () => {
@@ -1,4 +1,5 @@
1
1
  import assert from 'assert'
2
+ import path from 'path'
2
3
  import dispatchOpencode from '../src/dispatchOpencode.mjs'
3
4
  import createFakeCli from './tools/fakeCliForTest.mjs'
4
5
 
@@ -182,6 +183,26 @@ describe('dispatchOpencode', function() {
182
183
  assert.strict.deepEqual(r, rr)
183
184
  })
184
185
 
186
+ it('PWD同步為有效cwd之絕對路徑: 有給cwd(絕對或相對)即解析之且等於子進程真實cwd, 未給則為process.cwd()', async function() {
187
+ let t1 = await dispatchOpencode('abc', { exe: fake.exe, cwd: fake.fd })
188
+ let t2 = await dispatchOpencode('abc', { exe: fake.exe, cwd: path.relative(process.cwd(), fake.fd) })
189
+ let t3 = await dispatchOpencode('abc', { exe: fake.exe })
190
+ let o1 = JSON.parse(t1.stdout)
191
+ let o2 = JSON.parse(t2.stdout)
192
+ let o3 = JSON.parse(t3.stdout)
193
+ let r = [o1.env.PWD, o1.env.PWD === o1.cwd, o2.env.PWD, o3.env.PWD, o3.env.PWD === o3.cwd]
194
+ let rr = [path.resolve(fake.fd), true, path.resolve(fake.fd), process.cwd(), true]
195
+ assert.strict.deepEqual(r, rr)
196
+ })
197
+
198
+ it('呼叫端env內之PWD(含整份展開process.env之寫法)被有效cwd覆寫, 其餘變數保留', async function() {
199
+ let t = await dispatchOpencode('abc', { exe: fake.exe, cwd: fake.fd, env: { ...process.env, PWD: 'C:/somewhere/else', FAKE_ENV: 'x' } })
200
+ let o = JSON.parse(t.stdout)
201
+ let r = [o.env.PWD, o.env.FAKE_ENV]
202
+ let rr = [path.resolve(fake.fd), 'x']
203
+ assert.strict.deepEqual(r, rr)
204
+ })
205
+
185
206
  it('prompt以stdin傳入, 中文與多行皆完整保留', async function() {
186
207
  let prompt = '第一行中文\n第二行 with "quote" & <sym>\n第三行'
187
208
  let t = await dispatchOpencode(prompt, { exe: fake.exe })