w-dispatch-ai 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -5
- package/dist/w-dispatch-ai.umd.js +2 -2
- package/dist/w-dispatch-ai.umd.js.map +1 -1
- package/docs/WDispatchAi.mjs.html +8 -4
- package/docs/adapters.mjs.html +9 -7
- package/docs/dispatchAi.mjs.html +2 -2
- package/docs/dispatchAiFallback.mjs.html +2 -2
- package/docs/dispatchAiWkf.mjs.html +172 -0
- package/docs/dispatchAntigravity.mjs.html +2 -2
- package/docs/dispatchApiOpenaiCompat.mjs.html +483 -0
- package/docs/dispatchClaude.mjs.html +2 -2
- package/docs/dispatchCodex.mjs.html +2 -2
- package/docs/dispatchOpencode.mjs.html +2 -2
- package/docs/getCliArgs.mjs.html +2 -2
- package/docs/getErrorResult.mjs.html +2 -2
- package/docs/global.html +5747 -1467
- package/docs/index.html +2 -2
- package/docs/wkf_callAiWithFallback.mjs.html +281 -0
- package/docs/wkf_extractJsonLoose.mjs.html +180 -0
- package/docs/wkf_runFanout.mjs.html +227 -0
- package/docs/wkf_runFanoutPipeline.mjs.html +178 -0
- package/docs/wkf_runRolePipeline.mjs.html +195 -0
- package/g.mjs +11 -2
- package/package.json +1 -1
- package/src/WDispatchAi.mjs +6 -2
- package/src/adapters.mjs +7 -5
- package/src/dispatchAiWkf.mjs +100 -0
- package/src/dispatchApiOpenaiCompat.mjs +411 -0
- package/src/wkf/callAiWithFallback.mjs +209 -0
- package/src/wkf/extractJsonLoose.mjs +108 -0
- package/src/wkf/runFanout.mjs +155 -0
- package/src/wkf/runFanoutPipeline.mjs +106 -0
- package/src/wkf/runRolePipeline.mjs +123 -0
- package/test/tools/fakeServerForApiTest.mjs +137 -0
- package/test/unit-WDispatchAi.test.mjs +13 -6
- package/test/unit-adapters.test.mjs +5 -3
- package/test/unit-callAiWithFallback.test.mjs +146 -0
- package/test/unit-dispatchAi.test.mjs +1 -1
- package/test/unit-dispatchAiWkf.test.mjs +118 -0
- package/test/unit-dispatchApiOpenaiCompat.test.mjs +233 -0
- package/test/unit-extractJsonLoose.test.mjs +78 -0
- package/test/unit-runFanout.test.mjs +166 -0
- package/test/unit-runFanoutPipeline.test.mjs +86 -0
- package/test/unit-runRolePipeline.test.mjs +163 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<title>dispatchAiWkf.mjs - Documentation</title>
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
<script src="scripts/prettify/prettify.js"></script>
|
|
10
|
+
<script src="scripts/prettify/lang-css.js"></script>
|
|
11
|
+
<!--[if lt IE 9]>
|
|
12
|
+
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
|
|
13
|
+
<![endif]-->
|
|
14
|
+
<link type="text/css" rel="stylesheet" href="styles/prettify.css">
|
|
15
|
+
<link type="text/css" rel="stylesheet" href="styles/jsdoc.css">
|
|
16
|
+
<script src="scripts/nav.js" defer></script>
|
|
17
|
+
|
|
18
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
19
|
+
</head>
|
|
20
|
+
<body>
|
|
21
|
+
|
|
22
|
+
<input type="checkbox" id="nav-trigger" class="nav-trigger" />
|
|
23
|
+
<label for="nav-trigger" class="navicon-button x">
|
|
24
|
+
<div class="navicon"></div>
|
|
25
|
+
</label>
|
|
26
|
+
|
|
27
|
+
<label for="nav-trigger" class="overlay"></label>
|
|
28
|
+
|
|
29
|
+
<nav >
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
<h2><a href="index.html">Home</a></h2><h3>Global</h3><ul><li><a href="global.html#WDispatchAi">WDispatchAi</a></li><li><a href="global.html#adapters">adapters</a></li><li><a href="global.html#buildChain">buildChain</a></li><li><a href="global.html#buildValidator">buildValidator</a></li><li><a href="global.html#callAiWithFallback">callAiWithFallback</a></li><li><a href="global.html#callOnce">callOnce</a></li><li><a href="global.html#defaultIntegratePrompt">defaultIntegratePrompt</a></li><li><a href="global.html#dispatchAi">dispatchAi</a></li><li><a href="global.html#dispatchAiFallback">dispatchAiFallback</a></li><li><a href="global.html#dispatchAiWkf">dispatchAiWkf</a></li><li><a href="global.html#dispatchAntigravity">dispatchAntigravity</a></li><li><a href="global.html#dispatchApiOpenaiCompat">dispatchApiOpenaiCompat</a></li><li><a href="global.html#dispatchClaude">dispatchClaude</a></li><li><a href="global.html#dispatchCodex">dispatchCodex</a></li><li><a href="global.html#dispatchOpencode">dispatchOpencode</a></li><li><a href="global.html#extractJsonLoose">extractJsonLoose</a></li><li><a href="global.html#getCliArgs">getCliArgs</a></li><li><a href="global.html#getErrorResult">getErrorResult</a></li><li><a href="global.html#isKeyIndependentFail">isKeyIndependentFail</a></li><li><a href="global.html#runFanout">runFanout</a></li><li><a href="global.html#runFanoutPipeline">runFanoutPipeline</a></li><li><a href="global.html#runRolePipeline">runRolePipeline</a></li></ul>
|
|
33
|
+
|
|
34
|
+
</nav>
|
|
35
|
+
|
|
36
|
+
<div id="main">
|
|
37
|
+
|
|
38
|
+
<h1 class="page-title">dispatchAiWkf.mjs</h1>
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
<section>
|
|
47
|
+
<article>
|
|
48
|
+
<pre class="prettyprint source linenums"><code>import get from 'lodash-es/get.js'
|
|
49
|
+
import isobj from 'wsemi/src/isobj.mjs'
|
|
50
|
+
import callAiWithFallback from './wkf/callAiWithFallback.mjs'
|
|
51
|
+
import runFanout from './wkf/runFanout.mjs'
|
|
52
|
+
import runRolePipeline from './wkf/runRolePipeline.mjs'
|
|
53
|
+
import runFanoutPipeline from './wkf/runFanoutPipeline.mjs'
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
// dispatchAiWkf.mjs — 工作流工廠: 注入provider定義表與共用預設, 回傳綁定版API
|
|
57
|
+
//
|
|
58
|
+
// 【用途】專案端只需注入一次providers(名稱 → dispatchAiFallback條目)與共用設定
|
|
59
|
+
// (cwd、store、onEvent、timeoutMs…), 之後以名稱宣告工作流即可, 不必每次傳定義表。
|
|
60
|
+
//
|
|
61
|
+
// 【並行與游標之說明】多名額並行且共用同一store時, 游標read-modify-write
|
|
62
|
+
// 可能交錯, 造成金鑰輪替不完全均攤——只影響公平性、不影響正確性(每把金鑰仍有效),
|
|
63
|
+
// 故不加鎖; 要求嚴格均攤者可注入自帶佇列的store。
|
|
64
|
+
//
|
|
65
|
+
// 【本函數為同步工廠會throw】providers無效屬設定錯誤, 應於啟動期即失敗(fail fast),
|
|
66
|
+
// 與各dispatch函數「不reject」之約定不衝突——後者是執行期呼叫, 前者是組裝期設定。
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 建立AI工作流執行環境(工廠),注入provider定義表與共用預設後回傳綁定版API
|
|
71
|
+
*
|
|
72
|
+
* 特點:
|
|
73
|
+
* providers為名稱對dispatchAiFallback條目之定義表,之後各工作流以名稱宣告主模型與遞補鏈;
|
|
74
|
+
* defaults為共用呼叫設定,各工作流之callOpt與名額規格可逐項覆寫;
|
|
75
|
+
* 回傳之各函數皆不reject;本工廠為同步函數,providers無效時throw(設定錯誤應於啟動期即失敗)
|
|
76
|
+
*
|
|
77
|
+
* @param {Object} opt 輸入設定物件
|
|
78
|
+
* @param {Object} opt.providers 輸入provider定義表物件(名稱 → dispatchAiFallback條目:{ kind, model, keys, exe, provider, config, sandbox, extraArgs... })
|
|
79
|
+
* @param {Object} [opt.defaults={}] 輸入共用呼叫設定物件(cwd、store、onEvent、timeoutMs、budgetMs、maxRetries、promptPrefix、parse等),預設{}
|
|
80
|
+
* @returns {Object} 回傳綁定版API物件,內含callAi(單一名額呼叫)、runFanout(多開+整合)、runRolePipeline(串行角色鏈)、runFanoutPipeline(多開+整合+角色鏈)、providers(定義表原樣)
|
|
81
|
+
* @example
|
|
82
|
+
* //need cli in system PATH
|
|
83
|
+
*
|
|
84
|
+
* import dispatchAiWkf from './src/dispatchAiWkf.mjs'
|
|
85
|
+
*
|
|
86
|
+
* let wkf = dispatchAiWkf({
|
|
87
|
+
* providers: {
|
|
88
|
+
* 'deepseek': { kind: 'opencode', model: 'opencode/deepseek-v4-flash-free', provider: 'opencode', keys: ['sk-xxx'] },
|
|
89
|
+
* 'sonnet': { kind: 'claude', model: 'sonnet' },
|
|
90
|
+
* 'luna': { kind: 'codex', model: 'gpt-5.6-luna' },
|
|
91
|
+
* },
|
|
92
|
+
* defaults: { timeoutMs: 300000 },
|
|
93
|
+
* })
|
|
94
|
+
*
|
|
95
|
+
* let test = async () => {
|
|
96
|
+
*
|
|
97
|
+
* //單一名額: 主模型+遞補鏈
|
|
98
|
+
* let r1 = await wkf.callAi('只回覆JSON: {"a":1}', { spec: { use: 'deepseek', fallback: ['sonnet'] }, check: (j) => j.a === 1 })
|
|
99
|
+
* console.log(r1.ok, r1.json)
|
|
100
|
+
* // => true { a: 1 }
|
|
101
|
+
*
|
|
102
|
+
* //Fanout工作流: 多開執行+單點整合
|
|
103
|
+
* let r2 = await wkf.runFanout({
|
|
104
|
+
* task: '分析並只回覆JSON: {"essence":"..."}',
|
|
105
|
+
* agents: [{ use: 'deepseek', fallback: ['sonnet'] }, { use: 'sonnet' }],
|
|
106
|
+
* integrate: { use: 'luna' },
|
|
107
|
+
* check: (j) => !!j.essence,
|
|
108
|
+
* })
|
|
109
|
+
* console.log(r2.ok, r2.integrated)
|
|
110
|
+
* // => true true
|
|
111
|
+
*
|
|
112
|
+
* }
|
|
113
|
+
* await test()
|
|
114
|
+
* .catch((err) => {
|
|
115
|
+
* console.log(err)
|
|
116
|
+
* })
|
|
117
|
+
*
|
|
118
|
+
*/
|
|
119
|
+
function dispatchAiWkf(opt = {}) {
|
|
120
|
+
let providers = get(opt, 'providers', null)
|
|
121
|
+
if (!isobj(providers)) {
|
|
122
|
+
throw new Error('dispatchAiWkf: opt.providers must be an object (name → provider entry)')
|
|
123
|
+
}
|
|
124
|
+
let defaults = get(opt, 'defaults', null)
|
|
125
|
+
if (!isobj(defaults)) {
|
|
126
|
+
defaults = {}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
providers,
|
|
131
|
+
|
|
132
|
+
//單一名額呼叫: callAi(prompt, { spec:{use,fallback}, check, ... })
|
|
133
|
+
callAi: (prompt, o = {}) => callAiWithFallback(prompt, { ...defaults, ...o, providers }),
|
|
134
|
+
|
|
135
|
+
//Fanout工作流: runFanout({ task, agents, integrate, check, schema, minCandidates, callOpt? })
|
|
136
|
+
runFanout: (o = {}) => runFanout({ ...o, providers, callOpt: { ...defaults, ...get(o, 'callOpt', {}) } }),
|
|
137
|
+
|
|
138
|
+
//RolePipeline工作流: runRolePipeline({ input, stages, callOpt? })
|
|
139
|
+
runRolePipeline: (o = {}) => runRolePipeline({ ...o, providers, callOpt: { ...defaults, ...get(o, 'callOpt', {}) } }),
|
|
140
|
+
|
|
141
|
+
//FanoutPipeline工作流: runFanoutPipeline({ task, agents, integrate, stages, check, schema, callOpt? })
|
|
142
|
+
runFanoutPipeline: (o = {}) => runFanoutPipeline({ ...o, providers, callOpt: { ...defaults, ...get(o, 'callOpt', {}) } }),
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
export default dispatchAiWkf
|
|
148
|
+
</code></pre>
|
|
149
|
+
</article>
|
|
150
|
+
</section>
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<br class="clear">
|
|
160
|
+
|
|
161
|
+
<footer>
|
|
162
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Tue Aug 11 2026 13:18:34 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
163
|
+
</footer>
|
|
164
|
+
|
|
165
|
+
<script>prettyPrint();</script>
|
|
166
|
+
<script src="scripts/polyfill.js"></script>
|
|
167
|
+
<script src="scripts/linenumber.js"></script>
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
</body>
|
|
172
|
+
</html>
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
<nav >
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
<h2><a href="index.html">Home</a></h2><h3>Global</h3><ul><li><a href="global.html#WDispatchAi">WDispatchAi</a></li><li><a href="global.html#adapters">adapters</a></li><li><a href="global.html#dispatchAi">dispatchAi</a></li><li><a href="global.html#dispatchAiFallback">dispatchAiFallback</a></li><li><a href="global.html#dispatchAntigravity">dispatchAntigravity</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#getCliArgs">getCliArgs</a></li><li><a href="global.html#getErrorResult">getErrorResult</a></li><li><a href="global.html#isKeyIndependentFail">isKeyIndependentFail</a></li></ul>
|
|
32
|
+
<h2><a href="index.html">Home</a></h2><h3>Global</h3><ul><li><a href="global.html#WDispatchAi">WDispatchAi</a></li><li><a href="global.html#adapters">adapters</a></li><li><a href="global.html#buildChain">buildChain</a></li><li><a href="global.html#buildValidator">buildValidator</a></li><li><a href="global.html#callAiWithFallback">callAiWithFallback</a></li><li><a href="global.html#callOnce">callOnce</a></li><li><a href="global.html#defaultIntegratePrompt">defaultIntegratePrompt</a></li><li><a href="global.html#dispatchAi">dispatchAi</a></li><li><a href="global.html#dispatchAiFallback">dispatchAiFallback</a></li><li><a href="global.html#dispatchAiWkf">dispatchAiWkf</a></li><li><a href="global.html#dispatchAntigravity">dispatchAntigravity</a></li><li><a href="global.html#dispatchApiOpenaiCompat">dispatchApiOpenaiCompat</a></li><li><a href="global.html#dispatchClaude">dispatchClaude</a></li><li><a href="global.html#dispatchCodex">dispatchCodex</a></li><li><a href="global.html#dispatchOpencode">dispatchOpencode</a></li><li><a href="global.html#extractJsonLoose">extractJsonLoose</a></li><li><a href="global.html#getCliArgs">getCliArgs</a></li><li><a href="global.html#getErrorResult">getErrorResult</a></li><li><a href="global.html#isKeyIndependentFail">isKeyIndependentFail</a></li><li><a href="global.html#runFanout">runFanout</a></li><li><a href="global.html#runFanoutPipeline">runFanoutPipeline</a></li><li><a href="global.html#runRolePipeline">runRolePipeline</a></li></ul>
|
|
33
33
|
|
|
34
34
|
</nav>
|
|
35
35
|
|
|
@@ -251,7 +251,7 @@ export default dispatchAntigravity
|
|
|
251
251
|
<br class="clear">
|
|
252
252
|
|
|
253
253
|
<footer>
|
|
254
|
-
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on
|
|
254
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Tue Aug 11 2026 13:18:34 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
255
255
|
</footer>
|
|
256
256
|
|
|
257
257
|
<script>prettyPrint();</script>
|
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<title>dispatchApiOpenaiCompat.mjs - Documentation</title>
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
<script src="scripts/prettify/prettify.js"></script>
|
|
10
|
+
<script src="scripts/prettify/lang-css.js"></script>
|
|
11
|
+
<!--[if lt IE 9]>
|
|
12
|
+
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
|
|
13
|
+
<![endif]-->
|
|
14
|
+
<link type="text/css" rel="stylesheet" href="styles/prettify.css">
|
|
15
|
+
<link type="text/css" rel="stylesheet" href="styles/jsdoc.css">
|
|
16
|
+
<script src="scripts/nav.js" defer></script>
|
|
17
|
+
|
|
18
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
19
|
+
</head>
|
|
20
|
+
<body>
|
|
21
|
+
|
|
22
|
+
<input type="checkbox" id="nav-trigger" class="nav-trigger" />
|
|
23
|
+
<label for="nav-trigger" class="navicon-button x">
|
|
24
|
+
<div class="navicon"></div>
|
|
25
|
+
</label>
|
|
26
|
+
|
|
27
|
+
<label for="nav-trigger" class="overlay"></label>
|
|
28
|
+
|
|
29
|
+
<nav >
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
<h2><a href="index.html">Home</a></h2><h3>Global</h3><ul><li><a href="global.html#WDispatchAi">WDispatchAi</a></li><li><a href="global.html#adapters">adapters</a></li><li><a href="global.html#buildChain">buildChain</a></li><li><a href="global.html#buildValidator">buildValidator</a></li><li><a href="global.html#callAiWithFallback">callAiWithFallback</a></li><li><a href="global.html#callOnce">callOnce</a></li><li><a href="global.html#defaultIntegratePrompt">defaultIntegratePrompt</a></li><li><a href="global.html#dispatchAi">dispatchAi</a></li><li><a href="global.html#dispatchAiFallback">dispatchAiFallback</a></li><li><a href="global.html#dispatchAiWkf">dispatchAiWkf</a></li><li><a href="global.html#dispatchAntigravity">dispatchAntigravity</a></li><li><a href="global.html#dispatchApiOpenaiCompat">dispatchApiOpenaiCompat</a></li><li><a href="global.html#dispatchClaude">dispatchClaude</a></li><li><a href="global.html#dispatchCodex">dispatchCodex</a></li><li><a href="global.html#dispatchOpencode">dispatchOpencode</a></li><li><a href="global.html#extractJsonLoose">extractJsonLoose</a></li><li><a href="global.html#getCliArgs">getCliArgs</a></li><li><a href="global.html#getErrorResult">getErrorResult</a></li><li><a href="global.html#isKeyIndependentFail">isKeyIndependentFail</a></li><li><a href="global.html#runFanout">runFanout</a></li><li><a href="global.html#runFanoutPipeline">runFanoutPipeline</a></li><li><a href="global.html#runRolePipeline">runRolePipeline</a></li></ul>
|
|
33
|
+
|
|
34
|
+
</nav>
|
|
35
|
+
|
|
36
|
+
<div id="main">
|
|
37
|
+
|
|
38
|
+
<h1 class="page-title">dispatchApiOpenaiCompat.mjs</h1>
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
<section>
|
|
47
|
+
<article>
|
|
48
|
+
<pre class="prettyprint source linenums"><code>import get from 'lodash-es/get.js'
|
|
49
|
+
import isobj from 'wsemi/src/isobj.mjs'
|
|
50
|
+
import isfun from 'wsemi/src/isfun.mjs'
|
|
51
|
+
import isnum from 'wsemi/src/isnum.mjs'
|
|
52
|
+
import isestr from 'wsemi/src/isestr.mjs'
|
|
53
|
+
import ispint from 'wsemi/src/ispint.mjs'
|
|
54
|
+
import isp0int from 'wsemi/src/isp0int.mjs'
|
|
55
|
+
import cint from 'wsemi/src/cint.mjs'
|
|
56
|
+
import delay from 'wsemi/src/delay.mjs'
|
|
57
|
+
import strleft from 'wsemi/src/strleft.mjs'
|
|
58
|
+
import strdelleft from 'wsemi/src/strdelleft.mjs'
|
|
59
|
+
import strTruncate from 'wsemi/src/strTruncate.mjs'
|
|
60
|
+
import getErrorResult from './getErrorResult.mjs'
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
// dispatchApiOpenaiCompat.mjs — 以fetch直呼OpenAI相容API(chat/completions)
|
|
64
|
+
//
|
|
65
|
+
// 【為何需要】opencode CLI調用的deepseek(OpenCode Zen閘道)與agnes-ai本體都是
|
|
66
|
+
// OpenAI相容REST API, 直呼即可免安裝CLI、免預先登入(2026-08-11於本機實測):
|
|
67
|
+
// OpenCode Zen — https://opencode.ai/zen/v1 (金鑰同auth.json之sk-..., 模型名去掉opencode/前綴)
|
|
68
|
+
// Agnes — https://apihub.agnes-ai.com/v1
|
|
69
|
+
// 實測四把金鑰直呼皆200; 壞金鑰回401(Zen: `{"type":"AuthError","message":"Invalid API key."}`,
|
|
70
|
+
// Agnes: `{"message":"无效的令牌...","type":"AgnesAI_error"}`)。
|
|
71
|
+
//
|
|
72
|
+
// 【與CLI轉接器之差異】prompt走HTTP body無命令列長度限制; 錯誤依HTTP狀態碼精確分流
|
|
73
|
+
// (401/403金鑰、429限流、5xx服務端), 不再依賴stderr字串猜測; 純completion無agentic
|
|
74
|
+
// 能力(不讀檔不跑指令), 天然無寫檔風險。
|
|
75
|
+
// 注意: claude與codex走訂閱帳號登入態而非API金鑰, 無法比照, 仍須CLI轉接器。
|
|
76
|
+
//
|
|
77
|
+
// 【重試語意對齊execCli】4xx(429除外)為客戶端錯誤不可重試而立即中止;
|
|
78
|
+
// 429/5xx/網路錯誤/逾時依maxRetries線性退避重試(間隔retryDelayMs*次數, 上限15000ms)。
|
|
79
|
+
//
|
|
80
|
+
// 【結果結構對齊execCli】{ ok, stdout, stderr, code, error, durationMs, attempts },
|
|
81
|
+
// stdout為回覆內容、code為HTTP狀態碼(網路錯誤與逾時為null)、逾時error以TIMEOUT開頭、
|
|
82
|
+
// 驗證失敗error為OUTPUT_VALIDATION_FAILED——故dispatchAiFallback之失敗分流
|
|
83
|
+
// (TIMEOUT/驗證失敗跳組, 其餘換金鑰)對本轉接器同樣成立, 無須任何修改。
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
//預設值
|
|
87
|
+
let DEFAULT_TIMEOUT_MS = 120000
|
|
88
|
+
let DEFAULT_RETRY_DELAY_MS = 5000
|
|
89
|
+
let MAX_RETRY_DELAY_MS = 15000
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
//optTruncate, 裁切失敗結果之內容時於刪節號後標註原始總長度(同execCli)
|
|
93
|
+
let optTruncate = {
|
|
94
|
+
funWithMsg: (str) => `(truncated, total ${str.length} chars)`,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 建立驗證函式(規則語法同execCli之validate)
|
|
100
|
+
*
|
|
101
|
+
* @param {String|Function} rule 輸入驗證規則字串('nonempty'、'json'、'min:100', 逗號可串接)或自訂函式
|
|
102
|
+
* @returns {Function|null} 回傳驗證函式,無有效規則回傳null
|
|
103
|
+
*/
|
|
104
|
+
function buildValidator(rule) {
|
|
105
|
+
|
|
106
|
+
//自訂函式直接使用
|
|
107
|
+
if (isfun(rule)) {
|
|
108
|
+
return rule
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
//check
|
|
112
|
+
if (!isestr(rule)) {
|
|
113
|
+
return null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
//checks
|
|
117
|
+
let checks = rule.split(',').map((r) => r.trim()).filter(Boolean)
|
|
118
|
+
if (checks.length === 0) {
|
|
119
|
+
return null
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return (stdout) => {
|
|
123
|
+
for (let check of checks) {
|
|
124
|
+
|
|
125
|
+
if (check === 'nonempty') {
|
|
126
|
+
if (!isestr(stdout) || stdout.trim() === '') {
|
|
127
|
+
return false
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
else if (check === 'json') {
|
|
132
|
+
try {
|
|
133
|
+
JSON.parse(stdout)
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return false
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
else if (strleft(check, 4) === 'min:') {
|
|
141
|
+
|
|
142
|
+
//規則本身無效(如min:abc) → 視為驗證失敗, 不靜默跳過
|
|
143
|
+
let smin = strdelleft(check, 4)
|
|
144
|
+
if (!isnum(smin)) {
|
|
145
|
+
return false
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let min = cint(smin)
|
|
149
|
+
if (!isestr(stdout) || stdout.length < min) {
|
|
150
|
+
return false
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
}
|
|
155
|
+
return true
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 單次HTTP呼叫(內部使用, 不含重試邏輯)
|
|
162
|
+
*
|
|
163
|
+
* @param {String} url 輸入完整端點網址字串
|
|
164
|
+
* @param {Object} headers 輸入請求標頭物件
|
|
165
|
+
* @param {Object} body 輸入請求本體物件
|
|
166
|
+
* @param {Number} timeoutMs 輸入逾時毫秒
|
|
167
|
+
* @param {Function|null} validator 輸入驗證函式
|
|
168
|
+
* @returns {Promise} 回傳Promise,resolve回傳結果物件
|
|
169
|
+
*/
|
|
170
|
+
async function callOnce(url, headers, body, timeoutMs, validator) {
|
|
171
|
+
|
|
172
|
+
let t0 = Date.now()
|
|
173
|
+
|
|
174
|
+
//AbortController, 逾時中止(含回應本體之串流讀取)
|
|
175
|
+
let controller = new AbortController()
|
|
176
|
+
let timer = setTimeout(() => {
|
|
177
|
+
controller.abort()
|
|
178
|
+
}, timeoutMs)
|
|
179
|
+
|
|
180
|
+
let res = null
|
|
181
|
+
let txt = ''
|
|
182
|
+
try {
|
|
183
|
+
res = await fetch(url, {
|
|
184
|
+
method: 'POST',
|
|
185
|
+
headers,
|
|
186
|
+
body: JSON.stringify(body),
|
|
187
|
+
signal: controller.signal,
|
|
188
|
+
})
|
|
189
|
+
txt = await res.text()
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
clearTimeout(timer)
|
|
193
|
+
let durationMs = Date.now() - t0
|
|
194
|
+
|
|
195
|
+
//逾時, error以TIMEOUT開頭令dispatchAiFallback視為與金鑰無關而跳組
|
|
196
|
+
if (err.name === 'AbortError') {
|
|
197
|
+
return {
|
|
198
|
+
ok: false,
|
|
199
|
+
stdout: '',
|
|
200
|
+
stderr: '',
|
|
201
|
+
code: null,
|
|
202
|
+
error: `TIMEOUT after ${timeoutMs / 1000}s`,
|
|
203
|
+
durationMs,
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
//網路層錯誤(DNS/連線拒絕等)
|
|
208
|
+
let cause = get(err, 'cause.code', '') || err.message
|
|
209
|
+
return {
|
|
210
|
+
ok: false,
|
|
211
|
+
stdout: '',
|
|
212
|
+
stderr: '',
|
|
213
|
+
code: null,
|
|
214
|
+
error: `FETCH_ERROR: ${cause}`,
|
|
215
|
+
durationMs,
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
clearTimeout(timer)
|
|
219
|
+
|
|
220
|
+
let durationMs = Date.now() - t0
|
|
221
|
+
|
|
222
|
+
//HTTP非2xx, 原始回應本體放stderr供除錯與分類
|
|
223
|
+
if (!res.ok) {
|
|
224
|
+
return {
|
|
225
|
+
ok: false,
|
|
226
|
+
stdout: '',
|
|
227
|
+
stderr: strTruncate(txt, 1000, optTruncate),
|
|
228
|
+
code: res.status,
|
|
229
|
+
error: `HTTP ${res.status}`,
|
|
230
|
+
durationMs,
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
//取出choices[0].message.content
|
|
235
|
+
let content = null
|
|
236
|
+
try {
|
|
237
|
+
let j = JSON.parse(txt)
|
|
238
|
+
content = get(j, 'choices.0.message.content', null)
|
|
239
|
+
}
|
|
240
|
+
catch {}
|
|
241
|
+
if (content === null || content === undefined) {
|
|
242
|
+
return {
|
|
243
|
+
ok: false,
|
|
244
|
+
stdout: '',
|
|
245
|
+
stderr: strTruncate(txt, 500, optTruncate),
|
|
246
|
+
code: res.status,
|
|
247
|
+
error: 'INVALID_RESPONSE: missing choices[0].message.content',
|
|
248
|
+
durationMs,
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (typeof content !== 'string') {
|
|
252
|
+
content = JSON.stringify(content) //少數閘道回array形態
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
//validator, error與execCli一致令dispatchAiFallback可統一分流
|
|
256
|
+
if (validator && !validator(content)) {
|
|
257
|
+
return {
|
|
258
|
+
ok: false,
|
|
259
|
+
stdout: strTruncate(content, 500, optTruncate),
|
|
260
|
+
stderr: '',
|
|
261
|
+
code: res.status,
|
|
262
|
+
error: 'OUTPUT_VALIDATION_FAILED',
|
|
263
|
+
durationMs,
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
ok: true,
|
|
269
|
+
stdout: content,
|
|
270
|
+
stderr: '',
|
|
271
|
+
code: res.status,
|
|
272
|
+
error: '',
|
|
273
|
+
durationMs,
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
//本轉接器不使用execCli, 全部設定鍵自理, 未知鍵一律忽略
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* 以fetch直呼OpenAI相容API(chat/completions)呼叫AI模型
|
|
283
|
+
*
|
|
284
|
+
* 特點:
|
|
285
|
+
* 免安裝CLI、免預先登入,給baseURL+key+model即可呼叫(如OpenCode Zen、Agnes等OpenAI相容閘道);
|
|
286
|
+
* prompt走HTTP body,無命令列長度限制;
|
|
287
|
+
* 錯誤依HTTP狀態碼分流:4xx(429除外)為客戶端錯誤不重試,429/5xx/網路錯誤/逾時依maxRetries線性退避重試;
|
|
288
|
+
* 結果結構與逾時/驗證失敗之error字樣對齊execCli,可直接作為dispatchAi與dispatchAiFallback之kind('api-openai-compat')使用;
|
|
289
|
+
* 本函數不會reject,一律以結果物件之ok與error欄位回報成敗
|
|
290
|
+
*
|
|
291
|
+
* @param {String} prompt 輸入提示詞字串,作為user訊息置於HTTP body
|
|
292
|
+
* @param {Object} [opt={}] 輸入設定物件,預設{}
|
|
293
|
+
* @param {String} opt.baseURL 輸入API基底網址字串,例如'https://opencode.ai/zen/v1'、'https://apihub.agnes-ai.com/v1',將於尾端接上/chat/completions
|
|
294
|
+
* @param {String} opt.model 輸入模型ID字串,例如'deepseek-v4-flash-free'(Zen之模型名不帶opencode/前綴)、'agnes-2.0-flash'
|
|
295
|
+
* @param {String} [opt.key=''] 輸入API key字串,以Bearer置於Authorization標頭,預設''代表不帶認證標頭
|
|
296
|
+
* @param {String} [opt.system=''] 輸入system提示詞字串,將以system角色置於messages首位,預設''代表不帶
|
|
297
|
+
* @param {Object} [opt.body={}] 輸入額外請求本體物件(如temperature、max_tokens、response_format),將併入預設body(同名鍵以此為準),預設{}
|
|
298
|
+
* @param {Object} [opt.headers={}] 輸入額外請求標頭物件,預設{}
|
|
299
|
+
* @param {Number} [opt.timeoutMs=120000] 輸入逾時毫秒正整數,逾時將中止請求(含回應串流讀取),預設120000
|
|
300
|
+
* @param {String|Function} [opt.validate=undefined] 輸入回覆內容驗證規則字串或自訂驗證函數,規則字串支援'nonempty'、'json'、'min:100',多規則可用逗號串接,預設undefined代表不驗證
|
|
301
|
+
* @param {Number} [opt.maxRetries=0] 輸入失敗後最大重試次數非負整數,4xx(429除外)不重試,預設0
|
|
302
|
+
* @param {Number} [opt.retryDelayMs=5000] 輸入重試間隔毫秒正整數,實際間隔為retryDelayMs乘以重試次數且上限15000ms,預設5000
|
|
303
|
+
* @returns {Promise} 回傳Promise,resolve回傳結果物件,內含ok(是否成功布林值)、stdout(回覆內容字串)、stderr(失敗時之原始回應本體)、code(HTTP狀態碼,網路錯誤與逾時為null)、error(錯誤訊息字串,成功時為空字串)、durationMs(耗時毫秒)、attempts(實際嘗試次數),本函數不會reject
|
|
304
|
+
* @example
|
|
305
|
+
* //need network, no cli required
|
|
306
|
+
*
|
|
307
|
+
* import dispatchApiOpenaiCompat from './src/dispatchApiOpenaiCompat.mjs'
|
|
308
|
+
*
|
|
309
|
+
* let test = async () => {
|
|
310
|
+
*
|
|
311
|
+
* //OpenCode Zen(即opencode CLI之自家閘道), 模型名不帶opencode/前綴
|
|
312
|
+
* let r1 = await dispatchApiOpenaiCompat('請只回覆兩個字:完成', {
|
|
313
|
+
* baseURL: 'https://opencode.ai/zen/v1',
|
|
314
|
+
* key: 'sk-xxxxxx',
|
|
315
|
+
* model: 'deepseek-v4-flash-free',
|
|
316
|
+
* })
|
|
317
|
+
* console.log(r1.ok, r1.stdout.trim())
|
|
318
|
+
* // => true 完成
|
|
319
|
+
*
|
|
320
|
+
* //Agnes
|
|
321
|
+
* let r2 = await dispatchApiOpenaiCompat('請只回覆兩個字:完成', {
|
|
322
|
+
* baseURL: 'https://apihub.agnes-ai.com/v1',
|
|
323
|
+
* key: 'sk-xxxxxx',
|
|
324
|
+
* model: 'agnes-2.0-flash',
|
|
325
|
+
* })
|
|
326
|
+
* console.log(r2.ok, r2.stdout.trim())
|
|
327
|
+
* // => true 完成
|
|
328
|
+
*
|
|
329
|
+
* let re = await dispatchApiOpenaiCompat('abc', { baseURL: 'https://opencode.ai/zen/v1', key: 'sk-bad', model: 'deepseek-v4-flash-free' })
|
|
330
|
+
* console.log(re.ok, re.code, re.error)
|
|
331
|
+
* // => false 401 HTTP 401
|
|
332
|
+
*
|
|
333
|
+
* }
|
|
334
|
+
* await test()
|
|
335
|
+
* .catch((err) => {
|
|
336
|
+
* console.log(err)
|
|
337
|
+
* })
|
|
338
|
+
*
|
|
339
|
+
*/
|
|
340
|
+
async function dispatchApiOpenaiCompat(prompt, opt = {}) {
|
|
341
|
+
|
|
342
|
+
//check prompt, 不reject故以錯誤結果物件回報
|
|
343
|
+
if (!isestr(prompt)) {
|
|
344
|
+
return getErrorResult('prompt must be a non-empty string')
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
//baseURL必填, API無CLI可回退
|
|
348
|
+
let baseURL = get(opt, 'baseURL', null)
|
|
349
|
+
if (!isestr(baseURL)) {
|
|
350
|
+
return getErrorResult('baseURL must be a non-empty string')
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
//model必填, chat/completions無預設模型
|
|
354
|
+
let model = get(opt, 'model', null)
|
|
355
|
+
if (!isestr(model)) {
|
|
356
|
+
return getErrorResult('model must be a non-empty string')
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
//key, 無效代表不帶認證標頭(部分閘道免認證)
|
|
360
|
+
let key = get(opt, 'key', null)
|
|
361
|
+
|
|
362
|
+
//system
|
|
363
|
+
let system = get(opt, 'system', null)
|
|
364
|
+
|
|
365
|
+
//bodyExtra
|
|
366
|
+
let bodyExtra = get(opt, 'body', null)
|
|
367
|
+
if (!isobj(bodyExtra)) {
|
|
368
|
+
bodyExtra = {}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
//headersExtra
|
|
372
|
+
let headersExtra = get(opt, 'headers', null)
|
|
373
|
+
if (!isobj(headersExtra)) {
|
|
374
|
+
headersExtra = {}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
//timeoutMs
|
|
378
|
+
let timeoutMs = get(opt, 'timeoutMs', null)
|
|
379
|
+
if (!ispint(timeoutMs)) {
|
|
380
|
+
timeoutMs = DEFAULT_TIMEOUT_MS
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
timeoutMs = cint(timeoutMs)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
//maxRetries
|
|
387
|
+
let maxRetries = get(opt, 'maxRetries', null)
|
|
388
|
+
if (!isp0int(maxRetries)) {
|
|
389
|
+
maxRetries = 0
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
maxRetries = cint(maxRetries)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
//retryDelayMs
|
|
396
|
+
let retryDelayMs = get(opt, 'retryDelayMs', null)
|
|
397
|
+
if (!ispint(retryDelayMs)) {
|
|
398
|
+
retryDelayMs = DEFAULT_RETRY_DELAY_MS
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
retryDelayMs = cint(retryDelayMs)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
//validator
|
|
405
|
+
let validator = buildValidator(get(opt, 'validate', null))
|
|
406
|
+
|
|
407
|
+
//url, baseURL尾端斜線正規化後接上端點
|
|
408
|
+
let url = baseURL.replace(/\/+$/, '') + '/chat/completions'
|
|
409
|
+
|
|
410
|
+
//messages
|
|
411
|
+
let messages = []
|
|
412
|
+
if (isestr(system)) {
|
|
413
|
+
messages.push({ role: 'system', content: system })
|
|
414
|
+
}
|
|
415
|
+
messages.push({ role: 'user', content: prompt })
|
|
416
|
+
|
|
417
|
+
//body, 額外鍵以bodyExtra為準(可覆寫temperature等, 覆寫messages屬進階用法)
|
|
418
|
+
let body = { model, messages, ...bodyExtra }
|
|
419
|
+
|
|
420
|
+
//headers
|
|
421
|
+
let headers = { 'Content-Type': 'application/json', ...headersExtra }
|
|
422
|
+
if (isestr(key)) {
|
|
423
|
+
headers['Authorization'] = `Bearer ${key}`
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
let lastResult = null
|
|
427
|
+
let totalAttempts = 0
|
|
428
|
+
|
|
429
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
430
|
+
|
|
431
|
+
//delay, 重試間隔隨次數遞增, 上限15000ms(同execCli)
|
|
432
|
+
if (attempt > 0) {
|
|
433
|
+
await delay(Math.min(retryDelayMs * attempt, MAX_RETRY_DELAY_MS))
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
lastResult = await callOnce(url, headers, body, timeoutMs, validator)
|
|
437
|
+
totalAttempts = attempt + 1
|
|
438
|
+
|
|
439
|
+
if (lastResult.ok) {
|
|
440
|
+
lastResult.attempts = totalAttempts
|
|
441
|
+
return lastResult
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
//不可重試: 4xx(429除外)為客戶端錯誤, 重試無意義
|
|
445
|
+
let c = lastResult.code
|
|
446
|
+
if (isnum(c) && c >= 400 && c < 500 && c !== 429) {
|
|
447
|
+
break
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
lastResult.attempts = totalAttempts
|
|
453
|
+
|
|
454
|
+
return lastResult
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
export default dispatchApiOpenaiCompat
|
|
459
|
+
</code></pre>
|
|
460
|
+
</article>
|
|
461
|
+
</section>
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
</div>
|
|
469
|
+
|
|
470
|
+
<br class="clear">
|
|
471
|
+
|
|
472
|
+
<footer>
|
|
473
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Tue Aug 11 2026 13:18:34 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
474
|
+
</footer>
|
|
475
|
+
|
|
476
|
+
<script>prettyPrint();</script>
|
|
477
|
+
<script src="scripts/polyfill.js"></script>
|
|
478
|
+
<script src="scripts/linenumber.js"></script>
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
</body>
|
|
483
|
+
</html>
|