w-dispatch-ai 1.0.4 → 1.0.6

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 (46) hide show
  1. package/README.md +94 -17
  2. package/dist/w-dispatch-ai.umd.js +2 -2
  3. package/dist/w-dispatch-ai.umd.js.map +1 -1
  4. package/docs/WDispatchAi.mjs.html +8 -4
  5. package/docs/adapters.mjs.html +2 -2
  6. package/docs/dfTimeoutMs.mjs.html +98 -0
  7. package/docs/dispatchAi.mjs.html +2 -2
  8. package/docs/dispatchAiFallback.mjs.html +16 -11
  9. package/docs/dispatchAiWkf.mjs.html +11 -2
  10. package/docs/dispatchAntigravity.mjs.html +23 -12
  11. package/docs/dispatchApiOpenaiCompat.mjs.html +5 -4
  12. package/docs/dispatchClaude.mjs.html +17 -4
  13. package/docs/dispatchCodex.mjs.html +16 -3
  14. package/docs/dispatchOpencode.mjs.html +16 -3
  15. package/docs/getCliArgs.mjs.html +2 -2
  16. package/docs/getErrorResult.mjs.html +2 -2
  17. package/docs/global.html +501 -35
  18. package/docs/index.html +2 -2
  19. package/docs/resolveProviders.mjs.html +189 -0
  20. package/docs/wkf_callAiWithFallback.mjs.html +29 -13
  21. package/docs/wkf_extractJsonLoose.mjs.html +2 -2
  22. package/docs/wkf_runFanout.mjs.html +2 -2
  23. package/docs/wkf_runFanoutPipeline.mjs.html +2 -2
  24. package/docs/wkf_runRolePipeline.mjs.html +2 -2
  25. package/g.mjs +7 -7
  26. package/package.json +1 -1
  27. package/src/WDispatchAi.mjs +6 -2
  28. package/src/dfTimeoutMs.mjs +26 -0
  29. package/src/dispatchAiFallback.mjs +14 -9
  30. package/src/dispatchAiWkf.mjs +9 -0
  31. package/src/dispatchAntigravity.mjs +21 -10
  32. package/src/dispatchApiOpenaiCompat.mjs +3 -2
  33. package/src/dispatchClaude.mjs +15 -2
  34. package/src/dispatchCodex.mjs +14 -1
  35. package/src/dispatchOpencode.mjs +14 -1
  36. package/src/providers.mjs +133 -0
  37. package/src/resolveProviders.mjs +117 -0
  38. package/src/wkf/callAiWithFallback.mjs +27 -11
  39. package/test/unit-WDispatchAi.test.mjs +9 -3
  40. package/test/unit-callAiWithFallback.test.mjs +15 -0
  41. package/test/unit-dispatchAi.test.mjs +1 -1
  42. package/test/unit-dispatchAiFallback.test.mjs +25 -0
  43. package/test/unit-dispatchAiWkf.test.mjs +8 -0
  44. package/test/unit-dispatchAntigravity.test.mjs +25 -5
  45. package/test/unit-providers.test.mjs +66 -0
  46. package/test/unit-resolveProviders.test.mjs +85 -0
@@ -135,6 +135,21 @@ describe('callAiWithFallback', function() {
135
135
  assert.strict.deepEqual(r, rr)
136
136
  })
137
137
 
138
+ it('minAttemptMs等fallback層設定可經工作流層原樣轉傳(曾因白名單漏鍵而失效)', async function() {
139
+ //budgetMs=60000(高於預設門檻20000)+minAttemptMs=600000(遠大於budget):
140
+ //minAttemptMs有被轉送才會立即budget exhausted; 漏鍵時會照樣執行並成功
141
+ let t = await callAiWithFallback('abc', {
142
+ providers,
143
+ spec: { use: 'p-claude' },
144
+ promptPrefix: '',
145
+ budgetMs: 60000,
146
+ minAttemptMs: 600000,
147
+ })
148
+ let r = [t.ok, t.error, t.tried.map((x) => x.outcome)]
149
+ let rr = [false, 'budget exhausted', ['budget-out']]
150
+ assert.strict.deepEqual(r, rr)
151
+ })
152
+
138
153
  it('多金鑰條目經由keys輪替且keyId帶索引', async function() {
139
154
  let t = await callAiWithFallback('abc', { providers, spec: { use: 'p-oc' }, promptPrefix: '' })
140
155
  let auth = JSON.parse(t.json.env.OPENCODE_AUTH_CONTENT)
@@ -70,7 +70,7 @@ describe('dispatchAi', function() {
70
70
  [true, ['run', '--agent', 'build', '-m', 'mdl']],
71
71
  [true, ['-p', '--dangerously-skip-permissions', '--model', 'mdl']],
72
72
  [true, ['exec', '--sandbox', 'workspace-write', '--skip-git-repo-check', '-m', 'mdl']],
73
- [true, ['--dangerously-skip-permissions', '--print-timeout', '270s', '--model', 'mdl', '--print', 'abc']],
73
+ [true, ['--dangerously-skip-permissions', '--print-timeout', '270s', '--model', 'mdl', '--add-dir', process.cwd(), '--print', 'abc']],
74
74
  ]
75
75
  assert.strict.deepEqual(r, rr)
76
76
  })
@@ -316,6 +316,31 @@ describe('dispatchAiFallback', function() {
316
316
  assert.strict.deepEqual(r, rr)
317
317
  })
318
318
 
319
+ it('失敗事件與tried帶被拒回覆(stdout)與錯誤輸出(stderr)供診斷', async function() {
320
+ let evs = []
321
+ let t = await dispatchAiFallback('abc', {
322
+ providers: [
323
+ //驗證失敗(skip-group): 被拒的回覆內容應可於事件與tried取得
324
+ { id: 'g-diag-v', kind: 'claude', exe: fake.exe, validate: 'min:100000' },
325
+ //exit 1(next-key): stderr之錯誤訊息應可於事件與tried取得
326
+ { id: 'g-diag-e', kind: 'claude', exe: fake.exe, extraArgs: ['--fake-exit=1', '--fake-stderr=Invalid API key.'] },
327
+ { id: 'g-diag-ok', kind: 'claude', exe: fake.exe },
328
+ ],
329
+ onEvent: (ev) => evs.push(ev),
330
+ })
331
+ let evV = evs.find((x) => x.type === 'skip-group')
332
+ let evE = evs.find((x) => x.type === 'next-key')
333
+ let r = [
334
+ t.ok,
335
+ evV.stdout.includes('"stdin"'), //被拒回覆即假CLI之echo JSON
336
+ t.tried[0].stdout.includes('"stdin"'),
337
+ evE.stderr,
338
+ t.tried[1].stderr,
339
+ ]
340
+ let rr = [true, true, true, 'Invalid API key.', 'Invalid API key.']
341
+ assert.strict.deepEqual(r, rr)
342
+ })
343
+
319
344
  it('onEvent依序回報事件, 回調拋出例外不影響主流程', async function() {
320
345
  let evs = []
321
346
  let t = await dispatchAiFallback('abc', {
@@ -86,6 +86,14 @@ describe('dispatchAiWkf', function() {
86
86
  assert.strict.deepEqual(r, rr)
87
87
  })
88
88
 
89
+ it('defaults之minAttemptMs經callAi生效(README第一層教學之回歸測試)', async function() {
90
+ let wkf = dispatchAiWkf({ providers, defaults: { promptPrefix: '', budgetMs: 60000, minAttemptMs: 600000 } })
91
+ let t = await wkf.callAi('abc', { spec: { use: 'p-a' } })
92
+ let r = [t.ok, t.error]
93
+ let rr = [false, 'budget exhausted']
94
+ assert.strict.deepEqual(r, rr)
95
+ })
96
+
89
97
  it('runFanout綁定providers與defaults(defaults併入callOpt)', async function() {
90
98
  let wkf = dispatchAiWkf({ providers, defaults: { promptPrefix: '' } })
91
99
  let t = await wkf.runFanout({
@@ -62,7 +62,7 @@ describe('dispatchAntigravity', function() {
62
62
  let t = await dispatchAntigravity('abc', { exe: fake.exe })
63
63
  let o = JSON.parse(t.stdout)
64
64
  let r = [t.ok, o.args]
65
- let rr = [true, ['--dangerously-skip-permissions', '--print-timeout', '270s', '--print', 'abc']]
65
+ let rr = [true, ['--dangerously-skip-permissions', '--print-timeout', '270s', '--add-dir', process.cwd(), '--print', 'abc']]
66
66
  assert.strict.deepEqual(r, rr)
67
67
  })
68
68
 
@@ -78,7 +78,7 @@ describe('dispatchAntigravity', function() {
78
78
  let t = await dispatchAntigravity('abc', { exe: fake.exe, model: 'gemini-3.6-flash-low' })
79
79
  let o = JSON.parse(t.stdout)
80
80
  let r = o.args
81
- let rr = ['--dangerously-skip-permissions', '--print-timeout', '270s', '--model', 'gemini-3.6-flash-low', '--print', 'abc']
81
+ let rr = ['--dangerously-skip-permissions', '--print-timeout', '270s', '--model', 'gemini-3.6-flash-low', '--add-dir', process.cwd(), '--print', 'abc']
82
82
  assert.strict.deepEqual(r, rr)
83
83
  })
84
84
 
@@ -86,7 +86,7 @@ describe('dispatchAntigravity', function() {
86
86
  let t = await dispatchAntigravity('abc', { exe: fake.exe, model: 'gemini-3.1-pro', effort: 'low' })
87
87
  let o = JSON.parse(t.stdout)
88
88
  let r = o.args
89
- let rr = ['--dangerously-skip-permissions', '--print-timeout', '270s', '--model', 'gemini-3.1-pro', '--effort', 'low', '--print', 'abc']
89
+ let rr = ['--dangerously-skip-permissions', '--print-timeout', '270s', '--model', 'gemini-3.1-pro', '--effort', 'low', '--add-dir', process.cwd(), '--print', 'abc']
90
90
  assert.strict.deepEqual(r, rr)
91
91
  })
92
92
 
@@ -94,7 +94,7 @@ describe('dispatchAntigravity', function() {
94
94
  let t = await dispatchAntigravity('abc', { exe: fake.exe, skipPermissions: false })
95
95
  let o = JSON.parse(t.stdout)
96
96
  let r = [t.ok, o.args]
97
- let rr = [true, ['--print-timeout', '270s', '--print', 'abc']]
97
+ let rr = [true, ['--print-timeout', '270s', '--add-dir', process.cwd(), '--print', 'abc']]
98
98
  assert.strict.deepEqual(r, rr)
99
99
  })
100
100
 
@@ -123,6 +123,26 @@ describe('dispatchAntigravity', function() {
123
123
  assert.strict.deepEqual(r, rr)
124
124
  })
125
125
 
126
+ it('未給addDirs時自動納入有效cwd(明給的cwd優先)', async function() {
127
+ let t1 = await dispatchAntigravity('abc', { exe: fake.exe }) //cwd未給 → process.cwd()
128
+ let t2 = await dispatchAntigravity('abc', { exe: fake.exe, cwd: fake.fd }) //cwd明給
129
+ let o1 = JSON.parse(t1.stdout)
130
+ let o2 = JSON.parse(t2.stdout)
131
+ let i1 = o1.args.indexOf('--add-dir')
132
+ let i2 = o2.args.indexOf('--add-dir')
133
+ let r = [o1.args[i1 + 1], o2.args[i2 + 1]]
134
+ let rr = [process.cwd(), fake.fd]
135
+ assert.strict.deepEqual(r, rr)
136
+ })
137
+
138
+ it('明示addDirs為空陣列時不自動加入(代表不揭露任何目錄)', async function() {
139
+ let t = await dispatchAntigravity('abc', { exe: fake.exe, addDirs: [] })
140
+ let o = JSON.parse(t.stdout)
141
+ let r = [t.ok, o.args.includes('--add-dir')]
142
+ let rr = [true, false]
143
+ assert.strict.deepEqual(r, rr)
144
+ })
145
+
126
146
  it('addDirs逐項展開為--add-dir並濾除非有效字串', async function() {
127
147
  let t = await dispatchAntigravity('abc', { exe: fake.exe, addDirs: ['c:/a', null, 'c:/b', 123] })
128
148
  let o = JSON.parse(t.stdout)
@@ -135,7 +155,7 @@ describe('dispatchAntigravity', function() {
135
155
  let t = await dispatchAntigravity('abc', { exe: fake.exe, extraArgs: ['--output-format', 'json'] })
136
156
  let o = JSON.parse(t.stdout)
137
157
  let r = o.args
138
- let rr = ['--dangerously-skip-permissions', '--print-timeout', '270s', '--output-format', 'json', '--print', 'abc']
158
+ let rr = ['--dangerously-skip-permissions', '--print-timeout', '270s', '--add-dir', process.cwd(), '--output-format', 'json', '--print', 'abc']
139
159
  assert.strict.deepEqual(r, rr)
140
160
  })
141
161
 
@@ -0,0 +1,66 @@
1
+ import assert from 'assert'
2
+ import keys from 'lodash-es/keys.js'
3
+ import providers from '../src/providers.mjs'
4
+ import adapters from '../src/adapters.mjs'
5
+
6
+
7
+ describe('providers', function() {
8
+
9
+ it('預設匯出為非空條目陣列', function() {
10
+ let r = [Array.isArray(providers), providers.length > 0]
11
+ let rr = [true, true]
12
+ assert.strict.deepEqual(r, rr)
13
+ })
14
+
15
+ it('各條目皆有非空字串之id、model、kind', function() {
16
+ let r = providers.map((p) => [typeof p.id === 'string' && p.id !== '', typeof p.model === 'string' && p.model !== '', typeof p.kind === 'string' && p.kind !== ''])
17
+ let rr = providers.map(() => [true, true, true])
18
+ assert.strict.deepEqual(r, rr)
19
+ })
20
+
21
+ it('各條目之kind皆為有效供應商種類', function() {
22
+ let ks = keys(adapters)
23
+ let r = providers.map((p) => [p.id, ks.includes(p.kind)])
24
+ let rr = providers.map((p) => [p.id, true])
25
+ assert.strict.deepEqual(r, rr)
26
+ })
27
+
28
+ it('id不重複且皆符合「路徑:模型」命名(含冒號)', function() {
29
+ let ids = providers.map((p) => p.id)
30
+ let r = [new Set(ids).size === ids.length, ids.every((id) => id.includes(':'))]
31
+ let rr = [true, true]
32
+ assert.strict.deepEqual(r, rr)
33
+ })
34
+
35
+ it('api-openai-compat條目皆有baseURL', function() {
36
+ let apis = providers.filter((p) => p.kind === 'api-openai-compat')
37
+ let r = [apis.length > 0, apis.every((p) => typeof p.baseURL === 'string' && p.baseURL.startsWith('http'))]
38
+ let rr = [true, true]
39
+ assert.strict.deepEqual(r, rr)
40
+ })
41
+
42
+ it('opencode條目皆有provider(金鑰注入所需之配對)', function() {
43
+ let ocs = providers.filter((p) => p.kind === 'opencode')
44
+ let r = [ocs.length > 0, ocs.every((p) => typeof p.provider === 'string' && p.provider !== '')]
45
+ let rr = [true, true]
46
+ assert.strict.deepEqual(r, rr)
47
+ })
48
+
49
+ it('envVar命名皆為大寫底線格式, 訂閱登入態條目(claude/codex)無envVar', function() {
50
+ let withVar = providers.filter((p) => p.envVar !== undefined)
51
+ let subs = providers.filter((p) => ['claude', 'codex'].includes(p.kind))
52
+ let r = [
53
+ withVar.every((p) => /^[A-Z][A-Z0-9_]*$/.test(p.envVar)),
54
+ subs.every((p) => p.envVar === undefined),
55
+ ]
56
+ let rr = [true, true]
57
+ assert.strict.deepEqual(r, rr)
58
+ })
59
+
60
+ it('條目不自帶timeoutMs(由上層依任務型態統一給定)', function() {
61
+ let r = providers.every((p) => p.timeoutMs === undefined)
62
+ let rr = true
63
+ assert.strict.deepEqual(r, rr)
64
+ })
65
+
66
+ })
@@ -0,0 +1,85 @@
1
+ import assert from 'assert'
2
+ import resolveProviders from '../src/resolveProviders.mjs'
3
+ import providersAll from '../src/providers.mjs'
4
+
5
+
6
+ describe('resolveProviders', function() {
7
+
8
+ //測試用定義, 不依賴真實環境變數
9
+ let defs = [
10
+ { id: 'api:m1', kind: 'api-openai-compat', baseURL: 'https://x/v1', model: 'm1', envVar: 'T_K1' },
11
+ { id: 'api:m2', kind: 'api-openai-compat', baseURL: 'https://x/v1', model: 'm2', envVar: 'T_K2' },
12
+ { id: 'claude:sonnet', kind: 'claude', model: 'sonnet' },
13
+ { id: 'api:m3', kind: 'api-openai-compat', baseURL: 'https://x/v1', model: 'm3', envVar: 'T_K1', keys: ['sk-own'] },
14
+ ]
15
+ let env = { T_K1: 'sk-a, sk-b,', T_K2: '' }
16
+
17
+ it('envVar展開為keys(逗號分隔且修剪空白), envVar欄位自輸出移除', function() {
18
+ let { providers } = resolveProviders(defs, { env })
19
+ let p = providers.find((x) => x.id === 'api:m1')
20
+ let r = [p.keys, p.envVar]
21
+ let rr = [['sk-a', 'sk-b'], undefined]
22
+ assert.strict.deepEqual(r, rr)
23
+ })
24
+
25
+ it('缺環境變數(或值為空)之條目停用並列入skipped', function() {
26
+ let { providers, skipped } = resolveProviders(defs, { env })
27
+ let r = [providers.map((x) => x.id), skipped]
28
+ let rr = [['api:m1', 'claude:sonnet', 'api:m3'], [{ id: 'api:m2', envVar: 'T_K2' }]]
29
+ assert.strict.deepEqual(r, rr)
30
+ })
31
+
32
+ it('無envVar之條目(訂閱登入態)原樣通過, 自帶keys者以keys為準', function() {
33
+ let { providers } = resolveProviders(defs, { env })
34
+ let pc = providers.find((x) => x.id === 'claude:sonnet')
35
+ let p3 = providers.find((x) => x.id === 'api:m3')
36
+ let r = [pc.model, p3.keys, p3.envVar]
37
+ let rr = ['sonnet', ['sk-own'], undefined]
38
+ assert.strict.deepEqual(r, rr)
39
+ })
40
+
41
+ it('pick自選子集且依pick順序回傳, 查無之id列入missing', function() {
42
+ let { providers, missing } = resolveProviders(defs, { env, pick: ['claude:sonnet', 'api:m1', 'api:nope'] })
43
+ let r = [providers.map((x) => x.id), missing]
44
+ let rr = [['claude:sonnet', 'api:m1'], ['api:nope']]
45
+ assert.strict.deepEqual(r, rr)
46
+ })
47
+
48
+ it('table以id為鍵, 可直接作dispatchAiWkf之定義表', function() {
49
+ let { table } = resolveProviders(defs, { env })
50
+ let r = [Object.keys(table), table['api:m1'].keys]
51
+ let rr = [['api:m1', 'claude:sonnet', 'api:m3'], ['sk-a', 'sk-b']]
52
+ assert.strict.deepEqual(r, rr)
53
+ })
54
+
55
+ it('輸入陣列與條目皆不被改動', function() {
56
+ let before = JSON.stringify(defs)
57
+ resolveProviders(defs, { env })
58
+ let r = JSON.stringify(defs)
59
+ assert.strict.deepEqual(r, before)
60
+ })
61
+
62
+ it('providers無效時回傳空結果不throw', function() {
63
+ let r = []
64
+ for (let v of [null, undefined, 'abc', {}, [null, 'x']]) {
65
+ let t = resolveProviders(v, { env })
66
+ r.push([t.providers.length, t.skipped.length, t.missing.length])
67
+ }
68
+ let rr = [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]]
69
+ assert.strict.deepEqual(r, rr)
70
+ })
71
+
72
+ it('可展開套件內建providers.mjs(以模擬env驗證, 不依賴真實.env)', function() {
73
+ let fakeEnv = { OPENCODE_KEYS: 'sk-1,sk-2', AGNES_KEYS: 'sk-3', POOLSIDE_KEYS: 'sk-4' }
74
+ let { providers, skipped } = resolveProviders(providersAll, { env: fakeEnv })
75
+ let r = [
76
+ providers.length === providersAll.length, //三個envVar皆有值 → 無停用
77
+ skipped.length,
78
+ providers.every((p) => p.envVar === undefined),
79
+ providers.filter((p) => Array.isArray(p.keys)).length > 0,
80
+ ]
81
+ let rr = [true, 0, true, true]
82
+ assert.strict.deepEqual(r, rr)
83
+ })
84
+
85
+ })