dispatchAi.mjs

import keys from 'lodash-es/keys.js'
import includes from 'lodash-es/includes.js'
import isestr from 'wsemi/src/isestr.mjs'
import adapters from './adapters.mjs'
import getErrorResult from './getErrorResult.mjs'


/**
 * 依供應商種類(kind)分派至對應之CLI轉接器
 *
 * 三種供應商的差異(2026-08-08於本機實測確認):
 * opencode支援逐次注入金鑰(OPENCODE_AUTH_CONTENT),故可多把金鑰輪替;
 * claude與codex則沿用CLI既有登入狀態,無逐次金鑰概念。
 * 故「輪替」的單位是「供應商條目」而非單純的金鑰:一個條目即一組(kind, model, 可選的key/provider),
 * 輪到誰就用誰的CLI與模型
 *
 * @param {String} kind 輸入供應商種類字串,可選'opencode'、'claude'、'codex'
 * @param {String} prompt 輸入提示詞字串,一律以stdin傳入子進程
 * @param {Object} [opt={}] 輸入設定物件,原樣轉傳對應轉接器,各轉接器可用設定詳見dispatchOpencode、dispatchClaude、dispatchCodex,預設{}
 * @returns {Promise} 回傳Promise,resolve回傳結果物件,內含ok(是否成功布林值)、stdout(標準輸出字串)、stderr(標準錯誤字串)、code(離開碼)、error(錯誤訊息字串,成功時為空字串)、durationMs(耗時毫秒)、attempts(實際嘗試次數),本函數不會reject
 * @example
 * //need claude, codex or opencode cli in system PATH
 *
 * import dispatchAi from './src/dispatchAi.mjs'
 *
 * let test = async () => {
 *
 *     let r = await dispatchAi('claude', '請只回覆兩個字:完成', { model: 'sonnet' })
 *     console.log(r.ok, r.stdout.trim())
 *     // => true '完成'
 *
 *     let re = await dispatchAi('gemini', 'abc')
 *     console.log(re.ok, re.error)
 *     // => false 'unknown ai kind: "gemini" (available: opencode, claude, codex)'
 *
 * }
 * await test()
 *     .catch((err) => {
 *         console.log(err)
 *     })
 *
 */
async function dispatchAi(kind, prompt, opt = {}) {

    //ks
    let ks = keys(adapters)

    //check kind, 須為對照表既有鍵名, 不可用物件取值判斷, 否則'constructor'等原型鍵會誤判為有效
    if (!isestr(kind) || !includes(ks, kind)) {
        return getErrorResult(`unknown ai kind: "${kind}" (available: ${ks.join(', ')})`)
    }

    //fn
    let fn = adapters[kind]

    //dispatch, prompt與opt之檢核由各轉接器自行負責
    return fn(prompt, opt)
}


export default dispatchAi