w-dispatch-ai 1.0.25 → 1.0.27

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 (62) hide show
  1. package/README.md +57 -4
  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 +6 -4
  5. package/docs/adapters.mjs.html +10 -3
  6. package/docs/budgetFor.mjs.html +2 -2
  7. package/docs/buildValidator.mjs.html +2 -2
  8. package/docs/castPintOr.mjs.html +2 -2
  9. package/docs/dfTimeoutMs.mjs.html +2 -2
  10. package/docs/dispatchAi.mjs.html +2 -2
  11. package/docs/dispatchAiFallback.mjs.html +2 -2
  12. package/docs/dispatchAiWkf.mjs.html +2 -2
  13. package/docs/dispatchAntigravity.mjs.html +2 -2
  14. package/docs/dispatchApiOpenaiCompat.mjs.html +2 -2
  15. package/docs/dispatchApiOpenaiResponses.mjs.html +2 -2
  16. package/docs/dispatchApiTypesafeSystemone.mjs.html +447 -0
  17. package/docs/dispatchClaude.mjs.html +2 -2
  18. package/docs/dispatchCodex.mjs.html +2 -2
  19. package/docs/dispatchOpencode.mjs.html +27 -5
  20. package/docs/getCliArgs.mjs.html +2 -2
  21. package/docs/getErrorResult.mjs.html +2 -2
  22. package/docs/getErrorType.mjs.html +4 -3
  23. package/docs/global.html +1254 -247
  24. package/docs/index.html +2 -2
  25. package/docs/quota_dfQuotaTimeoutMs.mjs.html +2 -2
  26. package/docs/quota_fetchQuotaJson.mjs.html +2 -2
  27. package/docs/quota_fromCodexUsageHttp.mjs.html +2 -2
  28. package/docs/quota_getQuotaAntigravity.mjs.html +2 -2
  29. package/docs/quota_getQuotaClaude.mjs.html +2 -2
  30. package/docs/quota_getQuotaCodex.mjs.html +2 -2
  31. package/docs/quota_readJsonOrNull.mjs.html +2 -2
  32. package/docs/quota_toQuotaLabel.mjs.html +2 -2
  33. package/docs/quota_toQuotaResult.mjs.html +2 -2
  34. package/docs/quota_toQuotaScopedLabel.mjs.html +2 -2
  35. package/docs/quota_toQuotaWindow.mjs.html +2 -2
  36. package/docs/readEnvFile.mjs.html +2 -2
  37. package/docs/resolveProviders.mjs.html +2 -2
  38. package/docs/wkf_callAiWithFallback.mjs.html +2 -2
  39. package/docs/wkf_createFileStore.mjs.html +2 -2
  40. package/docs/wkf_createUsageCounter.mjs.html +2 -2
  41. package/docs/wkf_extractJsonLoose.mjs.html +2 -2
  42. package/docs/wkf_noSideEffectPrefix.mjs.html +2 -2
  43. package/docs/wkf_runFanout.mjs.html +2 -2
  44. package/docs/wkf_runFanoutPipeline.mjs.html +2 -2
  45. package/docs/wkf_runRolePipeline.mjs.html +2 -2
  46. package/docs/wkf_salvageTruncatedArray.mjs.html +2 -2
  47. package/g.mjs +2 -2
  48. package/package.json +1 -1
  49. package/src/WDispatchAi.mjs +4 -2
  50. package/src/adapters.mjs +8 -1
  51. package/src/dispatchApiTypesafeSystemone.mjs +375 -0
  52. package/src/dispatchOpencode.mjs +25 -3
  53. package/src/getErrorType.mjs +2 -1
  54. package/src/providers.mjs +35 -1
  55. package/test/tools/fakeServerForApiTest.mjs +75 -5
  56. package/test/unit-WDispatchAi.test.mjs +10 -6
  57. package/test/unit-adapters.test.mjs +5 -3
  58. package/test/unit-dispatchAi.test.mjs +1 -1
  59. package/test/unit-dispatchApiTypesafeSystemone.test.mjs +159 -0
  60. package/test/unit-dispatchOpencode.test.mjs +25 -0
  61. package/test/unit-providers.test.mjs +14 -0
  62. package/test/unit-resolveProviders.test.mjs +2 -2
@@ -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#NO_SIDE_EFFECT">NO_SIDE_EFFECT</a></li><li><a href="global.html#WDispatchAi">WDispatchAi</a></li><li><a href="global.html#adapters">adapters</a></li><li><a href="global.html#agyPrint">agyPrint</a></li><li><a href="global.html#attachErrorType">attachErrorType</a></li><li><a href="global.html#budgetFor">budgetFor</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#collectAdditional">collectAdditional</a></li><li><a href="global.html#createFileStore">createFileStore</a></li><li><a href="global.html#createUsageCounter">createUsageCounter</a></li><li><a href="global.html#decodeJwtPayload">decodeJwtPayload</a></li><li><a href="global.html#defaultIntegratePrompt">defaultIntegratePrompt</a></li><li><a href="global.html#dfQuotaTimeoutMs">dfQuotaTimeoutMs</a></li><li><a href="global.html#dfTimeoutMs">dfTimeoutMs</a></li><li><a href="global.html#directWindow">directWindow</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#dispatchApiOpenaiResponses">dispatchApiOpenaiResponses</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#extractOutputText">extractOutputText</a></li><li><a href="global.html#fetchQuotaJson">fetchQuotaJson</a></li><li><a href="global.html#firstStr">firstStr</a></li><li><a href="global.html#fromBucket">fromBucket</a></li><li><a href="global.html#fromCodexUsageHttp">fromCodexUsageHttp</a></li><li><a href="global.html#fromLimitItem">fromLimitItem</a></li><li><a href="global.html#fromRpcWindow">fromRpcWindow</a></li><li><a href="global.html#fromTopField">fromTopField</a></li><li><a href="global.html#fromWindow">fromWindow</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#getQuotaAntigravity">getQuotaAntigravity</a></li><li><a href="global.html#getQuotaClaude">getQuotaClaude</a></li><li><a href="global.html#getQuotaCodex">getQuotaCodex</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#pickWindow">pickWindow</a></li><li><a href="global.html#readBodyCapped">readBodyCapped</a></li><li><a href="global.html#readEnvFile">readEnvFile</a></li><li><a href="global.html#readJsonOrNull">readJsonOrNull</a></li><li><a href="global.html#redactText">redactText</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><li><a href="global.html#salvageTruncatedArray">salvageTruncatedArray</a></li><li><a href="global.html#toQuotaLabel">toQuotaLabel</a></li><li><a href="global.html#toQuotaResult">toQuotaResult</a></li><li><a href="global.html#toQuotaScopedLabel">toQuotaScopedLabel</a></li><li><a href="global.html#toQuotaWindow">toQuotaWindow</a></li><li><a href="global.html#viaAppServer">viaAppServer</a></li><li><a href="global.html#viaHttp">viaHttp</a></li><li><a href="global.html#windowToSeconds">windowToSeconds</a></li></ul>
32
+ <h2><a href="index.html">Home</a></h2><h3>Global</h3><ul><li><a href="global.html#NO_SIDE_EFFECT">NO_SIDE_EFFECT</a></li><li><a href="global.html#WDispatchAi">WDispatchAi</a></li><li><a href="global.html#adapters">adapters</a></li><li><a href="global.html#agyPrint">agyPrint</a></li><li><a href="global.html#attachErrorType">attachErrorType</a></li><li><a href="global.html#budgetFor">budgetFor</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#collectAdditional">collectAdditional</a></li><li><a href="global.html#createFileStore">createFileStore</a></li><li><a href="global.html#createUsageCounter">createUsageCounter</a></li><li><a href="global.html#decodeJwtPayload">decodeJwtPayload</a></li><li><a href="global.html#defaultIntegratePrompt">defaultIntegratePrompt</a></li><li><a href="global.html#dfQuotaTimeoutMs">dfQuotaTimeoutMs</a></li><li><a href="global.html#dfTimeoutMs">dfTimeoutMs</a></li><li><a href="global.html#directWindow">directWindow</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#dispatchApiOpenaiResponses">dispatchApiOpenaiResponses</a></li><li><a href="global.html#dispatchApiTypesafeSystemone">dispatchApiTypesafeSystemone</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#extractOutputText">extractOutputText</a></li><li><a href="global.html#fetchQuotaJson">fetchQuotaJson</a></li><li><a href="global.html#firstStr">firstStr</a></li><li><a href="global.html#fromBucket">fromBucket</a></li><li><a href="global.html#fromCodexUsageHttp">fromCodexUsageHttp</a></li><li><a href="global.html#fromLimitItem">fromLimitItem</a></li><li><a href="global.html#fromRpcWindow">fromRpcWindow</a></li><li><a href="global.html#fromTopField">fromTopField</a></li><li><a href="global.html#fromWindow">fromWindow</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#getQuotaAntigravity">getQuotaAntigravity</a></li><li><a href="global.html#getQuotaClaude">getQuotaClaude</a></li><li><a href="global.html#getQuotaCodex">getQuotaCodex</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#pickWindow">pickWindow</a></li><li><a href="global.html#readBodyCapped">readBodyCapped</a></li><li><a href="global.html#readEnvFile">readEnvFile</a></li><li><a href="global.html#readJsonOrNull">readJsonOrNull</a></li><li><a href="global.html#redactText">redactText</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><li><a href="global.html#salvageTruncatedArray">salvageTruncatedArray</a></li><li><a href="global.html#toQuotaLabel">toQuotaLabel</a></li><li><a href="global.html#toQuotaResult">toQuotaResult</a></li><li><a href="global.html#toQuotaScopedLabel">toQuotaScopedLabel</a></li><li><a href="global.html#toQuotaWindow">toQuotaWindow</a></li><li><a href="global.html#viaAppServer">viaAppServer</a></li><li><a href="global.html#viaHttp">viaHttp</a></li><li><a href="global.html#windowToSeconds">windowToSeconds</a></li></ul>
33
33
 
34
34
  </nav>
35
35
 
@@ -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 Mon Sep 14 2026 12:59:17 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 17 2026 20:41:36 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/g.mjs CHANGED
@@ -27,7 +27,7 @@ let test = async () => {
27
27
 
28
28
  //可用之AI供應商種類
29
29
  console.log('KINDS:', wdi.KINDS)
30
- // => KINDS: [ 'opencode', 'claude', 'codex', 'antigravity', 'api-openai-compat', 'api-openai-responses' ]
30
+ // => KINDS: [ 'opencode', 'claude', 'codex', 'antigravity', 'api-openai-compat', 'api-openai-responses', 'api-typesafe-systemone' ]
31
31
 
32
32
  let prompt = '請只回覆兩個字:完成,不要有任何其他文字'
33
33
 
@@ -84,7 +84,7 @@ let test = async () => {
84
84
  //未知供應商回傳error結果物件, 不會reject
85
85
  let r4 = await wdi.dispatchAi('gemini', prompt)
86
86
  console.log('invalid kind:', r4.ok, r4.error)
87
- // => invalid kind: false unknown ai kind: "gemini" (available: opencode, claude, codex, antigravity, api-openai-compat, api-openai-responses)
87
+ // => invalid kind: false unknown ai kind: "gemini" (available: opencode, claude, codex, antigravity, api-openai-compat, api-openai-responses, api-typesafe-systemone)
88
88
 
89
89
  //prompt非有效字串亦回傳error結果物件
90
90
  let r5 = await wdi.dispatchClaude('')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "w-dispatch-ai",
3
- "version": "1.0.25",
3
+ "version": "1.0.27",
4
4
  "main": "dist/w-dispatch-ai.umd.js",
5
5
  "dependencies": {
6
6
  "wsemi": "^1.8.92"
@@ -9,6 +9,7 @@ import dispatchCodex from './dispatchCodex.mjs'
9
9
  import dispatchAntigravity from './dispatchAntigravity.mjs'
10
10
  import dispatchApiOpenaiCompat from './dispatchApiOpenaiCompat.mjs'
11
11
  import dispatchApiOpenaiResponses from './dispatchApiOpenaiResponses.mjs'
12
+ import dispatchApiTypesafeSystemone from './dispatchApiTypesafeSystemone.mjs'
12
13
  import providers from './providers.mjs'
13
14
  import resolveProviders from './resolveProviders.mjs'
14
15
  import readEnvFile from './readEnvFile.mjs'
@@ -37,10 +38,10 @@ let KINDS = keys(adapters)
37
38
  /**
38
39
  * AI供應商分派
39
40
  *
40
- * @returns {Object} 回傳物件,其內含KINDS(可用供應商種類字串陣列),dispatchAiWkf之工作流工廠函數,dispatchAi、dispatchAiFallback、dispatchOpencode、dispatchClaude、dispatchCodex、dispatchAntigravity、dispatchApiOpenaiCompat、dispatchApiOpenaiResponses之async函數,providers(預設providers定義檔)與resolveProviders(envVar展開器),以及readEnvFile(讀.env不污染process.env)、budgetFor(遞補鏈時間預算推導)、createFileStore(store檔案持久化)、createUsageCounter(逐日用量計帳)、salvageTruncatedArray(截斷陣列搶救)、NO_SIDE_EFFECT(防副作用prompt前綴)等工具,以及getQuotaClaude、getQuotaCodex、getQuotaAntigravity(查詢各訂閱制CLI本機登入帳號之額度窗口,統一結構,可比對指定email)
41
+ * @returns {Object} 回傳物件,其內含KINDS(可用供應商種類字串陣列),dispatchAiWkf之工作流工廠函數,dispatchAi、dispatchAiFallback、dispatchOpencode、dispatchClaude、dispatchCodex、dispatchAntigravity、dispatchApiOpenaiCompat、dispatchApiOpenaiResponses、dispatchApiTypesafeSystemone(TypeSafe jev之型別化決策API,非文字生成)之async函數,providers(預設providers定義檔)與resolveProviders(envVar展開器),以及readEnvFile(讀.env不污染process.env)、budgetFor(遞補鏈時間預算推導)、createFileStore(store檔案持久化)、createUsageCounter(逐日用量計帳)、salvageTruncatedArray(截斷陣列搶救)、NO_SIDE_EFFECT(防副作用prompt前綴)等工具,以及getQuotaClaude、getQuotaCodex、getQuotaAntigravity(查詢各訂閱制CLI本機登入帳號之額度窗口,統一結構,可比對指定email)
41
42
  * @example
42
43
  *
43
- * 詳見dispatchAi、dispatchAiFallback、dispatchAiWkf、dispatchOpencode、dispatchClaude、dispatchCodex、dispatchAntigravity、dispatchApiOpenaiCompat、dispatchApiOpenaiResponses、resolveProviders、getQuotaClaude、getQuotaCodex、getQuotaAntigravity範例
44
+ * 詳見dispatchAi、dispatchAiFallback、dispatchAiWkf、dispatchOpencode、dispatchClaude、dispatchCodex、dispatchAntigravity、dispatchApiOpenaiCompat、dispatchApiOpenaiResponses、dispatchApiTypesafeSystemone、resolveProviders、getQuotaClaude、getQuotaCodex、getQuotaAntigravity範例
44
45
  *
45
46
  */
46
47
  let WDispatchAi = {
@@ -55,6 +56,7 @@ let WDispatchAi = {
55
56
  dispatchAntigravity,
56
57
  dispatchApiOpenaiCompat,
57
58
  dispatchApiOpenaiResponses,
59
+ dispatchApiTypesafeSystemone,
58
60
  providers,
59
61
  resolveProviders,
60
62
  readEnvFile,
package/src/adapters.mjs CHANGED
@@ -4,6 +4,7 @@ import dispatchCodex from './dispatchCodex.mjs'
4
4
  import dispatchAntigravity from './dispatchAntigravity.mjs'
5
5
  import dispatchApiOpenaiCompat from './dispatchApiOpenaiCompat.mjs'
6
6
  import dispatchApiOpenaiResponses from './dispatchApiOpenaiResponses.mjs'
7
+ import dispatchApiTypesafeSystemone from './dispatchApiTypesafeSystemone.mjs'
7
8
 
8
9
 
9
10
  // adapters.mjs — kind對照表, 亦為「CLI或API」之選型判準所在
@@ -38,6 +39,11 @@ import dispatchApiOpenaiResponses from './dispatchApiOpenaiResponses.mjs'
38
39
  // 同一條dispatchAiFallback鏈可逐條目混搭kind, 工作流各階段亦然:
39
40
  // 產生候選、整合收斂等純文字階段用API(快且省), 需要讀專案檔案或grep的
40
41
  // 階段換CLI。判準永遠是「這一步需不需要碰外部世界」, 而非整條鏈二選一。
42
+ //
43
+ // ══ 例外: api-typesafe-systemone 不是文字生成 ══
44
+ //
45
+ // TypeSafe之jev(System One模型)對prompt(state)回答型別化questions, stdout為答案JSON,
46
+ // 與上述文字生成kind之輸出形狀不同, 不可與其混在同一條遞補鏈; 詳見dispatchApiTypesafeSystemone.mjs檔頭。
41
47
 
42
48
 
43
49
  /**
@@ -52,7 +58,7 @@ import dispatchApiOpenaiResponses from './dispatchApiOpenaiResponses.mjs'
52
58
  * import adapters from './src/adapters.mjs'
53
59
  *
54
60
  * console.log(Object.keys(adapters))
55
- * // => ['opencode', 'claude', 'codex', 'antigravity', 'api-openai-compat', 'api-openai-responses']
61
+ * // => ['opencode', 'claude', 'codex', 'antigravity', 'api-openai-compat', 'api-openai-responses', 'api-typesafe-systemone']
56
62
  *
57
63
  */
58
64
  let adapters = {
@@ -62,6 +68,7 @@ let adapters = {
62
68
  'antigravity': dispatchAntigravity,
63
69
  'api-openai-compat': dispatchApiOpenaiCompat,
64
70
  'api-openai-responses': dispatchApiOpenaiResponses,
71
+ 'api-typesafe-systemone': dispatchApiTypesafeSystemone,
65
72
  }
66
73
 
67
74
 
@@ -0,0 +1,375 @@
1
+ import get from 'lodash-es/get.js'
2
+ import isobj from 'wsemi/src/isobj.mjs'
3
+ import iseobj from 'wsemi/src/iseobj.mjs'
4
+ import isnum from 'wsemi/src/isnum.mjs'
5
+ import cint from 'wsemi/src/cint.mjs'
6
+ import isestr from 'wsemi/src/isestr.mjs'
7
+ import isp0int from 'wsemi/src/isp0int.mjs'
8
+ import delay from 'wsemi/src/delay.mjs'
9
+ import strTruncate from 'wsemi/src/strTruncate.mjs'
10
+ import castPintOr from './castPintOr.mjs'
11
+ import buildValidator from './buildValidator.mjs'
12
+ import getErrorResult from './getErrorResult.mjs'
13
+ import dfTimeoutMs from './dfTimeoutMs.mjs'
14
+
15
+
16
+ // dispatchApiTypesafeSystemone.mjs — 以fetch直呼TypeSafe AI之System One API(POST /v1/systemone)
17
+ //
18
+ // 【這不是文字生成模型(2026-09-17實測後新增)】TypeSafe之jev為「System One」決策模型:
19
+ // 不產生文字, 而是對一段state(被評估的內容)回答呼叫端定義的型別化問題(questions),
20
+ // 每題回傳受限於呼叫端選項之答案與機率。權威文件: https://docs.typesafe.ai/api
21
+ // (索引 https://docs.typesafe.ai/llms.txt)。官方僅提供Python/JavaScript SDK、Playground與
22
+ // agent skill, 無CLI(npm之@typesafe-ai/sdk無bin、PyPI之typesafe-sdk無console script), 故只有API版。
23
+ //
24
+ // 【請求】POST <baseURL>/systemone, Authorization: Bearer <key>, body為{ model, state, questions }:
25
+ // state ← 本轉接器之prompt(字串)。結構化內容請以JSON.stringify(物件)傳入——2026-09-17實測
26
+ // 同一組引用欄位路徑之問題, 物件state與JSON字串state之答案一致(差異≤0.01)。
27
+ // questions ← opt.questions(必填, 非空物件), 鍵為呼叫端自訂之題目id, 值為:
28
+ // { type:'noul', instructions, criteria?:{true,false} } 是非題, 回yes之機率
29
+ // { type:'choice', instructions, criteria:{選項:描述|null} } 單選題
30
+ // { type:'score', instructions, criteria:[層級描述...] } 有序量表(至少2級)
31
+ // 題型與欄位由伺服器驗證(不合規回422), 本轉接器只擋「未給questions」, 不重複伺服器規則。
32
+ // model ← opt.model, 預設'jev-latest'(官方SDK之預設; 回應之model為實際版本如'jev-1.13.0')。
33
+ //
34
+ // 【回應(實測)】{ model:'jev-1.13.0', answers:{ <id>:{type,choice|score|noul,probabilities?,confidence?,legend?} },
35
+ // usage:{ input_tokens, output_tokens } }; 回應標頭帶x-typesafe-request-id。
36
+ // 本轉接器之stdout為JSON.stringify(answers)(令dispatchAiFallback與工作流層之parse/check無須修改即可用),
37
+ // 另於結果物件追加answers(已解析物件)與modelResolved(實際模型版本)。
38
+ // 請求的題目id在answers中缺任一個即視為INVALID_RESPONSE, 不讓下游取到undefined。
39
+ //
40
+ // 【錯誤(實測)】壞金鑰401 {detail:{error_type:'authentication_error'}}; 未知model 400
41
+ // {detail:{error_type:'api_usage_error', message:'Unknown model: X'}}; 題型不合規或questions為空422
42
+ // {detail:[{type,loc,msg,...}]}。皆以HTTP <code>回報、原始本體置stderr; 4xx(429除外)不重試。
43
+ //
44
+ // 【混用注意】
45
+ // 1. 答案形狀與文字模型完全不同, 不可與文字生成條目混在同一條dispatchAiFallback鏈中遞補;
46
+ // 同一鏈只放本kind之條目(可多把金鑰輪替)。預設providers.mjs收有typesafe:jev-latest,
47
+ // 請以pick單獨取出; 全取做文字遞補時該條因無questions以params錯誤0ms失敗後換下一家。
48
+ // 2. questions於dispatchAiFallback可放呼叫層opt(非fallback自用鍵會透傳給各條目), 不必寫進條目。
49
+ // 3. 工作流callAi預設在prompt前掛防寫檔前綴(NO_SIDE_EFFECT), 對本kind等於把前綴混進被評估的state,
50
+ // 經工作流呼叫時務必傳promptPrefix:''。
51
+
52
+
53
+ //預設值
54
+ let DEFAULT_BASE_URL = 'https://api.typesafe.ai/v1'
55
+ let DEFAULT_MODEL = 'jev-latest'
56
+ let DEFAULT_TIMEOUT_MS = dfTimeoutMs //全套件統一預設300000
57
+ let DEFAULT_RETRY_DELAY_MS = 5000
58
+ let MAX_RETRY_DELAY_MS = 15000
59
+
60
+
61
+ //optTruncate, 裁切失敗結果之內容時於刪節號後標註原始總長度(同execCli)
62
+ let optTruncate = {
63
+ funWithMsg: (str) => `(truncated, total ${str.length} chars)`,
64
+ }
65
+
66
+
67
+ /**
68
+ * 單次HTTP呼叫(內部使用, 不含重試邏輯)
69
+ *
70
+ * @param {String} url 輸入完整端點網址字串
71
+ * @param {Object} headers 輸入請求標頭物件
72
+ * @param {Object} body 輸入請求本體物件
73
+ * @param {Array} ids 輸入請求之題目id陣列,用於檢核回應是否每題皆有答案
74
+ * @param {Number} timeoutMs 輸入逾時毫秒
75
+ * @param {Function|null} validator 輸入驗證函式
76
+ * @returns {Promise} 回傳Promise,resolve回傳結果物件
77
+ */
78
+ async function callOnce(url, headers, body, ids, timeoutMs, validator) {
79
+
80
+ let t0 = Date.now()
81
+
82
+ //mkResult, 結果形狀之單一來源(欄位對齊其他api類轉接器, 追加answers與modelResolved)
83
+ let mkResult = (patch) => ({
84
+ ok: false,
85
+ stdout: '',
86
+ stderr: '',
87
+ code: null,
88
+ error: '',
89
+ durationMs: Date.now() - t0,
90
+ usage: null,
91
+ answers: null,
92
+ modelResolved: '',
93
+ ...patch,
94
+ })
95
+
96
+ //AbortController, 逾時中止(含回應本體之讀取)
97
+ let controller = new AbortController()
98
+ let timer = setTimeout(() => {
99
+ controller.abort()
100
+ }, timeoutMs)
101
+
102
+ let res = null
103
+ let txt = ''
104
+ try {
105
+ res = await fetch(url, {
106
+ method: 'POST',
107
+ headers,
108
+ body: JSON.stringify(body),
109
+ signal: controller.signal,
110
+ })
111
+ txt = await res.text()
112
+ }
113
+ catch (err) {
114
+ clearTimeout(timer)
115
+
116
+ //逾時, error以TIMEOUT開頭令dispatchAiFallback視為與金鑰無關而跳組
117
+ if (err.name === 'AbortError') {
118
+ return mkResult({ error: `TIMEOUT after ${timeoutMs / 1000}s`, errorType: 'timeout' })
119
+ }
120
+
121
+ //網路層錯誤(DNS/連線拒絕等)
122
+ let cause = get(err, 'cause.code', '') || err.message
123
+ return mkResult({ error: `FETCH_ERROR: ${cause}`, errorType: 'fetch' })
124
+ }
125
+ clearTimeout(timer)
126
+
127
+ //HTTP非2xx, 原始回應本體放stderr(401/400之detail.message、422之detail陣列皆在其中)
128
+ if (!res.ok) {
129
+ return mkResult({
130
+ stderr: strTruncate(txt, 1000, optTruncate),
131
+ code: res.status,
132
+ error: `HTTP ${res.status}`,
133
+ errorType: 'http',
134
+ })
135
+ }
136
+
137
+ //解析answers/model/usage
138
+ let answers = null
139
+ let modelResolved = ''
140
+ let usage = null
141
+ try {
142
+ let j = JSON.parse(txt)
143
+ answers = get(j, 'answers', null)
144
+ modelResolved = get(j, 'model', '')
145
+ if (!isestr(modelResolved)) {
146
+ modelResolved = ''
147
+ }
148
+ usage = get(j, 'usage', null)
149
+ if (!isobj(usage)) {
150
+ usage = null
151
+ }
152
+ }
153
+ catch {
154
+ answers = null
155
+ }
156
+ if (!isobj(answers)) {
157
+ return mkResult({
158
+ stderr: strTruncate(txt, 500, optTruncate),
159
+ code: res.status,
160
+ error: 'INVALID_RESPONSE: missing answers object',
161
+ errorType: 'invalid-response',
162
+ usage,
163
+ modelResolved,
164
+ })
165
+ }
166
+
167
+ //每個請求之題目皆須有答案, 缺漏即不合規(避免下游取到undefined)
168
+ let missing = ids.filter((id) => !isobj(answers[id]))
169
+ if (missing.length > 0) {
170
+ return mkResult({
171
+ stderr: strTruncate(txt, 500, optTruncate),
172
+ code: res.status,
173
+ error: `INVALID_RESPONSE: missing answers for [${missing.join(', ')}]`,
174
+ errorType: 'invalid-response',
175
+ usage,
176
+ modelResolved,
177
+ })
178
+ }
179
+
180
+ //content, answers序列化為stdout(令遞補層與工作流層之parse/check通用)
181
+ let content = JSON.stringify(answers)
182
+
183
+ //validator, error與execCli一致令dispatchAiFallback可統一分流
184
+ if (validator && !validator(content)) {
185
+ return mkResult({
186
+ stdout: strTruncate(content, 500, optTruncate),
187
+ code: res.status,
188
+ error: 'OUTPUT_VALIDATION_FAILED',
189
+ errorType: 'validation',
190
+ usage,
191
+ modelResolved,
192
+ })
193
+ }
194
+
195
+ return mkResult({
196
+ ok: true,
197
+ stdout: content,
198
+ code: res.status,
199
+ usage,
200
+ answers,
201
+ modelResolved,
202
+ })
203
+ }
204
+
205
+
206
+ //本轉接器不使用execCli, 全部設定鍵自理, 未知鍵一律忽略
207
+
208
+
209
+ /**
210
+ * 以fetch直呼TypeSafe AI之System One API(POST /v1/systemone),對prompt(即state)回答型別化問題
211
+ *
212
+ * 特點:
213
+ * 非文字生成——jev等System One模型對state回答呼叫端定義之questions(noul是非題、choice單選題、score量表),
214
+ * 每題回傳受限於選項之答案與機率;官方無CLI,僅此API路徑;
215
+ * prompt即state,結構化內容以JSON.stringify(物件)傳入(實測答案與物件state一致);
216
+ * stdout為answers之JSON字串,另於結果追加answers(已解析物件)與modelResolved(實際模型版本);
217
+ * 請求之題目id於回應缺任一即以INVALID_RESPONSE回報;
218
+ * 不可與文字生成條目混在同一條遞補鏈,經工作流callAi呼叫時須傳promptPrefix:''(理由見檔頭);
219
+ * 本函數不會reject,一律以結果物件之ok與error欄位回報成敗
220
+ *
221
+ * @param {String} prompt 輸入被評估之內容字串,作為state置於HTTP body,結構化內容請先JSON.stringify
222
+ * @param {Object} [opt={}] 輸入設定物件,預設{}
223
+ * @param {Object} opt.questions 輸入題目物件(必填且非空),鍵為自訂題目id,值為{type:'noul'|'choice'|'score', instructions, criteria},格式見檔頭與https://docs.typesafe.ai/api
224
+ * @param {String} [opt.baseURL='https://api.typesafe.ai/v1'] 輸入API基底網址字串,將於尾端接上/systemone,預設官方端點
225
+ * @param {String} [opt.model='jev-latest'] 輸入模型名稱字串,預設'jev-latest'(另有'jev-preview')
226
+ * @param {String} [opt.key=''] 輸入API key字串,以Bearer置於Authorization標頭,預設''代表不帶認證標頭
227
+ * @param {Object} [opt.body={}] 輸入額外請求本體物件,將併入預設body(同名鍵以此為準),預設{}
228
+ * @param {Object} [opt.headers={}] 輸入額外請求標頭物件,預設{}
229
+ * @param {Number} [opt.timeoutMs=300000] 輸入逾時毫秒正整數,逾時將中止請求,全套件統一預設300000
230
+ * @param {String|Function} [opt.validate=undefined] 輸入stdout(answers之JSON字串)驗證規則字串或自訂驗證函數,規則字串支援'nonempty'、'json'、'min:100',多規則可用逗號串接,預設undefined代表不驗證
231
+ * @param {Number} [opt.maxRetries=0] 輸入失敗後最大重試次數非負整數,4xx(429除外)不重試,預設0
232
+ * @param {Number} [opt.retryDelayMs=5000] 輸入重試間隔毫秒正整數,實際間隔為retryDelayMs乘以重試次數且上限15000ms,預設5000
233
+ * @returns {Promise} 回傳Promise,resolve回傳結果物件,內含ok(是否成功布林值)、stdout(answers之JSON字串)、stderr(失敗時之原始回應本體)、code(HTTP狀態碼,網路錯誤與逾時為null)、error(錯誤訊息字串,成功時為空字串)、errorType(僅失敗時,機器可讀錯誤類別字串,一覽見getErrorType.mjs檔頭)、durationMs(耗時毫秒)、attempts(實際嘗試次數)、usage(原始回應之token用量物件原樣透傳,欄位名為input_tokens/output_tokens,無則null)、answers(成功時為已解析之答案物件,否則null)、modelResolved(回應所載之實際模型版本字串,如'jev-1.13.0',無則''),本函數不會reject
234
+ * @example
235
+ * //need network and a TypeSafe api key, no cli required
236
+ *
237
+ * import dispatchApiTypesafeSystemone from './src/dispatchApiTypesafeSystemone.mjs'
238
+ *
239
+ * let test = async () => {
240
+ *
241
+ * let r = await dispatchApiTypesafeSystemone('房間浴室水龍頭一直滴水,吵到睡不著', {
242
+ * key: 'apikey_xxxxxx',
243
+ * questions: {
244
+ * category: {
245
+ * type: 'choice',
246
+ * instructions: '這則客房訊息屬於哪一類?',
247
+ * criteria: {
248
+ * '設備故障報修': '客人回報房間硬體設備損壞、水電問題或故障',
249
+ * '索取備品': '客人需要毛巾、牙刷、礦泉水等客房備品',
250
+ * '其他': null,
251
+ * },
252
+ * },
253
+ * urgent: { type: 'noul', instructions: '客人是否表達急迫性?' },
254
+ * },
255
+ * })
256
+ * console.log(r.ok, r.answers.category.choice, r.answers.urgent.noul)
257
+ * // => true 設備故障報修 0.86 (2026-09-17實測; noul為機率, 每次可能差0.01)
258
+ *
259
+ * let re = await dispatchApiTypesafeSystemone('abc', { key: 'apikey_bad', questions: { a: { type: 'noul', instructions: 'x?' } } })
260
+ * console.log(re.ok, re.code, re.errorType)
261
+ * // => false 401 http
262
+ *
263
+ * }
264
+ * await test()
265
+ * .catch((err) => {
266
+ * console.log(err)
267
+ * })
268
+ *
269
+ */
270
+ async function dispatchApiTypesafeSystemone(prompt, opt = {}) {
271
+
272
+ //check prompt, 不reject故以錯誤結果物件回報
273
+ if (!isestr(prompt)) {
274
+ return getErrorResult('prompt must be a non-empty string')
275
+ }
276
+
277
+ //questions必填且非空, 題型細節交伺服器驗證(回422)
278
+ let questions = get(opt, 'questions', null)
279
+ if (!iseobj(questions)) {
280
+ return getErrorResult('questions must be a non-empty object')
281
+ }
282
+
283
+ //baseURL, 無效回退官方端點
284
+ let baseURL = get(opt, 'baseURL', null)
285
+ if (!isestr(baseURL)) {
286
+ baseURL = DEFAULT_BASE_URL
287
+ }
288
+
289
+ //model, 無效回退官方預設
290
+ let model = get(opt, 'model', null)
291
+ if (!isestr(model)) {
292
+ model = DEFAULT_MODEL
293
+ }
294
+
295
+ //key, 無效代表不帶認證標頭
296
+ let key = get(opt, 'key', null)
297
+
298
+ //bodyExtra
299
+ let bodyExtra = get(opt, 'body', null)
300
+ if (!isobj(bodyExtra)) {
301
+ bodyExtra = {}
302
+ }
303
+
304
+ //headersExtra
305
+ let headersExtra = get(opt, 'headers', null)
306
+ if (!isobj(headersExtra)) {
307
+ headersExtra = {}
308
+ }
309
+
310
+ //timeoutMs
311
+ let timeoutMs = castPintOr(get(opt, 'timeoutMs', null), DEFAULT_TIMEOUT_MS)
312
+
313
+ //maxRetries
314
+ let maxRetries = get(opt, 'maxRetries', null)
315
+ if (!isp0int(maxRetries)) {
316
+ maxRetries = 0
317
+ }
318
+ else {
319
+ maxRetries = cint(maxRetries)
320
+ }
321
+
322
+ //retryDelayMs
323
+ let retryDelayMs = castPintOr(get(opt, 'retryDelayMs', null), DEFAULT_RETRY_DELAY_MS)
324
+
325
+ //validator
326
+ let validator = buildValidator(get(opt, 'validate', null))
327
+
328
+ //url, baseURL尾端斜線正規化後接上端點
329
+ let url = baseURL.replace(/\/+$/, '') + '/systemone'
330
+
331
+ //body, prompt即state; 額外鍵以bodyExtra為準
332
+ let body = { model, state: prompt, questions, ...bodyExtra }
333
+
334
+ //ids, 用於檢核回應是否每題皆有答案(以實際送出之questions為準)
335
+ let ids = Object.keys(isobj(body.questions) ? body.questions : questions)
336
+
337
+ //headers
338
+ let headers = { 'Content-Type': 'application/json', ...headersExtra }
339
+ if (isestr(key)) {
340
+ headers['Authorization'] = `Bearer ${key}`
341
+ }
342
+
343
+ let lastResult = null
344
+ let totalAttempts = 0
345
+
346
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
347
+
348
+ //delay, 重試間隔隨次數遞增, 上限15000ms(同execCli)
349
+ if (attempt > 0) {
350
+ await delay(Math.min(retryDelayMs * attempt, MAX_RETRY_DELAY_MS))
351
+ }
352
+
353
+ lastResult = await callOnce(url, headers, body, ids, timeoutMs, validator)
354
+ totalAttempts = attempt + 1
355
+
356
+ if (lastResult.ok) {
357
+ lastResult.attempts = totalAttempts
358
+ return lastResult
359
+ }
360
+
361
+ //不可重試: 4xx(429除外)為客戶端錯誤, 重試無意義
362
+ let c = lastResult.code
363
+ if (isnum(c) && c >= 400 && c < 500 && c !== 429) {
364
+ break
365
+ }
366
+
367
+ }
368
+
369
+ lastResult.attempts = totalAttempts
370
+
371
+ return lastResult
372
+ }
373
+
374
+
375
+ export default dispatchApiTypesafeSystemone
@@ -18,6 +18,19 @@ import dfTimeoutMs from './dfTimeoutMs.mjs'
18
18
  // 帶入真實key → 正常回應。以env傳入「不會改寫」auth.json(實測md5前後一致),
19
19
  // 屬process層級,可與同時執行的其他專案並行而互不干擾。
20
20
  // 註:OPENCODE_API_KEY在auth.json已有憑證時不生效,故一律走OPENCODE_AUTH_CONTENT。
21
+ // 原始碼依據(anomalyco/opencode dev, packages/opencode/src/auth/index.ts之Auth.all):
22
+ // `if (process.env.OPENCODE_AUTH_CONTENT) return JSON.parse(...)`, 解析成功即整份取代, 不與auth.json合併;
23
+ // 空字串為falsy、非法JSON會落回讀auth.json。auth.json位於<XDG_DATA_HOME|~/.local/share>/opencode/。
24
+ //
25
+ // 【不讀auth.json之匿名呼叫: useStoredAuth:false(2026-09-17實測後新增)】
26
+ // 未注入金鑰時opencode會自動沿用auth.json之登入; 該帳號工作區若未開某免費模型(如union-alpha),
27
+ // 就回`Error: Model is disabled`, 而同一模型在「無auth.json」之機器上匿名可用——結果隨執行機器而異。
28
+ // useStoredAuth:false時注入OPENCODE_AUTH_CONTENT='{}'(空憑證), 令本次不讀auth.json而以匿名免費存取。
29
+ // 實測(以XDG_DATA_HOME指向內含opencode金鑰之暫存auth.json): 不帶key→Model is disabled;
30
+ // 加'{}'→7.0s成功; 改注入''→仍Model is disabled(空字串無效, 故不可讓呼叫端自填); auth.json前後md5一致。
31
+ // 同時給key與provider時金鑰注入本就整份取代auth.json, 此旗標不再作用。
32
+ // 風險備忘: 原始碼Auth.set會以Auth.all()(即注入內容)為底寫回auth.json, 僅於OAuth權杖刷新等寫入時發生;
33
+ // api型金鑰與空憑證皆無刷新, 實測未觸發。
21
34
  //
22
35
  // 【provider與model必須配對】不同provider的金鑰不可互換:
23
36
  // 實測把agnes-ai的金鑰用於opencode/...模型 → `Invalid API key`。
@@ -38,7 +51,7 @@ let DEFAULT_AGENT = 'build'
38
51
 
39
52
 
40
53
  //本轉接器自用之設定鍵, 其餘鍵一律原樣轉傳execCli
41
- let OWN_KEYS = ['exe', 'model', 'key', 'provider', 'agent', 'config', 'extraArgs', 'input', 'env']
54
+ let OWN_KEYS = ['exe', 'model', 'key', 'provider', 'useStoredAuth', 'agent', 'config', 'extraArgs', 'input', 'env']
42
55
 
43
56
 
44
57
  /**
@@ -49,7 +62,7 @@ let OWN_KEYS = ['exe', 'model', 'key', 'provider', 'agent', 'config', 'extraArgs
49
62
  * 而opencode run未帶位置message時即從stdin讀取;
50
63
  * 同時給予key與provider時,以OPENCODE_AUTH_CONTENT環境變數逐次注入金鑰,
51
64
  * 該注入僅作用於當次子進程且不改寫auth.json,故可多把金鑰輪替並與其他程序並行;
52
- * 未給key或provider時沿用CLI既有登入狀態;
65
+ * 未給key或provider時沿用CLI既有登入狀態(auth.json),useStoredAuth為false則改以匿名存取;
53
66
  * 使用opencode未內建之第三方provider時,須另以config給予其provider定義;
54
67
  * 本函數不會reject,一律以結果物件之ok與error欄位回報成敗
55
68
  *
@@ -59,6 +72,7 @@ let OWN_KEYS = ['exe', 'model', 'key', 'provider', 'agent', 'config', 'extraArgs
59
72
  * @param {String} [opt.model=''] 輸入模型ID字串,例如'opencode/deepseek-v4-flash-free',預設''代表不帶`-m`旗標
60
73
  * @param {String} [opt.key=''] 輸入該provider之API key字串,須與provider同時給予才會注入,預設''代表沿用CLI既有登入狀態
61
74
  * @param {String} [opt.provider=''] 輸入key所屬provider名稱字串,須與key同時給予才會注入,且須與model為同一組,預設''
75
+ * @param {Boolean} [opt.useStoredAuth=true] 輸入未注入金鑰時是否沿用本機auth.json之登入布林值,false代表注入空憑證令本次以匿名存取(適用opencode之免費模型,避免結果隨本機登入帳號而異);已同時給key與provider時不作用,預設true
62
76
  * @param {Object|String} [opt.config=null] 輸入opencode設定內容物件或其JSON字串,將以OPENCODE_CONFIG_CONTENT逐次注入,供補上第三方provider之定義,預設null代表沿用使用者既有設定檔
63
77
  * @param {String} [opt.agent='build'] 輸入opencode代理名稱字串,預設'build'
64
78
  * @param {Array} [opt.extraArgs=[]] 輸入額外命令列旗標字串陣列,將接於固定旗標之後,預設[]
@@ -166,15 +180,23 @@ async function dispatchOpencode(prompt, opt = {}) {
166
180
  }
167
181
  }
168
182
 
169
- //key與provider皆有效才注入, 否則沿用auth.json既有登入狀態(單金鑰時即為原有行為)
183
+ //key與provider皆有效才注入, 否則沿用auth.json既有登入狀態(單金鑰時即為原有行為);
184
+ //useStoredAuth為false且未注入金鑰時, 注入'{}'令opencode本次不讀auth.json(匿名, 見檔頭)
170
185
  let key = get(opt, 'key', null)
171
186
  let provider = get(opt, 'provider', null)
187
+ let useStoredAuth = get(opt, 'useStoredAuth', null) !== false
172
188
  if (isestr(key) && isestr(provider)) {
173
189
  env = {
174
190
  ...env,
175
191
  OPENCODE_AUTH_CONTENT: JSON.stringify({ [provider]: { type: 'api', key } }),
176
192
  }
177
193
  }
194
+ else if (!useStoredAuth) {
195
+ env = {
196
+ ...env,
197
+ OPENCODE_AUTH_CONTENT: '{}',
198
+ }
199
+ }
178
200
 
179
201
  //timeoutMs, 無效回退全套件統一預設(dfTimeoutMs=300000), 各轉接器一致令呼叫方無須記多套數字
180
202
  let timeoutMs = castPintOr(get(opt, 'timeoutMs', null), dfTimeoutMs)
@@ -22,7 +22,8 @@ import isestr from 'wsemi/src/isestr.mjs'
22
22
  // 'http' HTTP非2xx(code為狀態碼, 僅api類)
23
23
  // 'fetch' 網路層錯誤(DNS/連線拒絕, 僅api類)
24
24
  // 'tool-unsupported' 模型回tool_calls而本轉接器不支援工具(僅api類)
25
- // 'invalid-response' 回應結構不合規(缺choices[0].message.content或缺output陣列, 僅api類)
25
+ // 'invalid-response' 回應結構不合規(缺choices[0].message.content、缺output陣列,
26
+ // 或api-typesafe-systemone缺answers物件/缺所請求題目之答案, 僅api類)
26
27
  // 'incomplete' Responses API之status非completed(如max_output_tokens耗盡而截斷,
27
28
  // 僅api-openai-responses; 半截內容不當成功回傳, 見該轉接器檔頭)
28
29
  // 'aborted' shouldStop中止(僅dispatchAiFallback)