refresh review pipeline
This commit is contained in:
+81
-17
@@ -12,12 +12,16 @@ export const LLM_CONCURRENCY = Number(process.env.AI_ASSISTANT_CONCURRENCY) || 0
|
||||
* 對 items 並行執行 async fn(保序回傳),加速多個獨立的 LLM 子行程呼叫。
|
||||
*
|
||||
* limit 為同時執行上限;`limit <= 0`、非數字或大於項目數時「不限制」(全部並行)。
|
||||
* fn 需自行處理例外(內部 try/catch);本函式不會因單一項目 reject 而中斷其餘工作。
|
||||
* fn 需自行處理例外(內部 try/catch);若 fn 未處理而 reject,本函式會立即向外
|
||||
* 拋出該錯誤(Promise.all fail-fast),但其他已啟動、尚在執行中的併發工作並不會
|
||||
* 被取消,仍會在背景繼續處理剩餘項目,只是其結果會被捨棄。
|
||||
*
|
||||
* @template T, R
|
||||
* @param {T[]} items - 要處理的項目。
|
||||
* @param {T[]} items - 要處理的項目;非陣列(含 null/undefined)會被視為空陣列,不拋錯。
|
||||
* @param {number} limit - 同時執行的上限;<=0/非數字表示不限制。
|
||||
* @param {(item: T, index: number) => Promise<R>} fn - 對每個項目執行的 async 函式。
|
||||
* @returns {Promise<R[]>} 與 items 對應(同索引)的結果陣列。
|
||||
* @throws 若任一次 fn 呼叫 reject 且未在內部處理,該錯誤會直接向外傳播。
|
||||
*/
|
||||
export async function mapWithConcurrency(items, limit, fn) {
|
||||
const list = Array.isArray(items) ? items : [];
|
||||
@@ -26,6 +30,26 @@ export async function mapWithConcurrency(items, limit, fn) {
|
||||
const n = Number(limit);
|
||||
const workers = (!Number.isFinite(n) || n <= 0) ? list.length : Math.min(n, list.length);
|
||||
let cursor = 0;
|
||||
/**
|
||||
* mapWithConcurrency 的工作者(worker)迴圈:從共用游標 `cursor` 依序搶下一個尚未
|
||||
* 處理的索引,呼叫外層傳入的 `fn`,並把結果寫入外層 `results` 陣列對應位置;直到
|
||||
* `cursor` 到達 `list.length` 為止。
|
||||
*
|
||||
* 多個 `run()` 會被同時啟動(依 `workers` 數量),透過共用的 `cursor` 變數達到
|
||||
* 「限制併發數、動態搶下一筆」的效果——先完成者會先搶到下一個索引,因此各次 `fn`
|
||||
* 呼叫的完成順序不保證,但因寫入位置以原始索引 `i` 為準,`results` 仍能保持與
|
||||
* `items` 相同順序。
|
||||
*
|
||||
* 本函式為 `mapWithConcurrency` 內部使用的閉包(closure),依賴外層作用域的
|
||||
* `list`、`results`、`fn`、`cursor` 變數運作;不接受參數,也不可、不應在外部
|
||||
* 單獨呼叫或匯出。
|
||||
*
|
||||
* @returns {Promise<void>} 無回傳值;副作用為寫入外層 `results` 陣列與推進 `cursor`。
|
||||
* @throws 若某次 `fn(list[i], i)` reject,本函式會原樣向外拋出該錯誤(不吞例外),
|
||||
* 使 `mapWithConcurrency` 的 `Promise.all` 立即 reject;但其他已啟動、尚在執行
|
||||
* 中的 `run()` 實例不會被取消,仍會在背景繼續搬移 `cursor` 並寫入 `results`,
|
||||
* 只是其結果最終會被捨棄。
|
||||
*/
|
||||
async function run() {
|
||||
while (cursor < list.length) {
|
||||
const i = cursor++;
|
||||
@@ -37,7 +61,19 @@ export async function mapWithConcurrency(items, limit, fn) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 將既有 system/user prompt 合併成一次 HTTP 呼叫用的輸入。
|
||||
* 將 system prompt 與 user content 合併成單一文字,作為送往 CLIProxyAPI 的
|
||||
* 「使用者訊息」內容。
|
||||
*
|
||||
* 注意:此函式回傳的合併文字,會被 chat() 整段放入 HTTP request 的 user role
|
||||
* 內容;實際送出的 HTTP system role 訊息是固定的通用指示(見 runProxyAPI),
|
||||
* 並非這裡傳入的 systemPrompt——systemPrompt 是以 `<system>` 標籤形式內嵌在
|
||||
* user 內容中,而非透過 API 的 system role 傳遞。
|
||||
*
|
||||
* @param {string} systemPrompt - 系統提示詞內容,會被包在 `<system>...</system>`
|
||||
* 標籤內;`null`/`undefined` 會被視為空字串。
|
||||
* @param {string} userContent - 使用者輸入內容,會被包在 `<user>...</user>`
|
||||
* 標籤內;`null`/`undefined` 會被視為空字串。
|
||||
* @returns {string} 合併後、以換行分隔的完整 prompt 文字。
|
||||
*/
|
||||
function buildPrompt(systemPrompt, userContent) {
|
||||
return [
|
||||
@@ -73,11 +109,20 @@ export function extractMeaningfulError(raw, limit = 1000) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 將 HTTP 例外整理成較精簡的錯誤摘要。
|
||||
* 將 HTTP 例外整理成較精簡的錯誤摘要,格式為 `"HTTP <status> <訊息>"`
|
||||
* (無 status 時只有訊息)。
|
||||
*
|
||||
* @param {*} e - 被拋出的錯誤物件,可能含 `stderr`、`stdout`、`message`。
|
||||
* 依序嘗試:`response.data`(字串或物件的 error.message/message/error 欄位)→
|
||||
* `stderr` → `stdout` → `e.message` → `String(e)`,取第一個非空來源後交給
|
||||
* extractMeaningfulError 濃縮成精簡訊息,再與 HTTP 狀態碼(若有)合併。
|
||||
*
|
||||
* @param {*} e - 被拋出的錯誤物件,預期含 `response.data`/`response.status`/
|
||||
* `stderr`/`stdout`/`message` 其中之一或多個。
|
||||
* @returns {string} 精簡後的錯誤訊息;兩者皆空則回傳空字串。
|
||||
* @remarks 適合在 log 與錯誤重新拋出前先整理訊息。
|
||||
* @remarks 若錯誤物件結構和預期不同,仍會退回字串化處理,屬保守容錯。
|
||||
* @remarks 容錯僅涵蓋「e 是物件但欄位缺失或型態不符」的情況;若 e 本身為
|
||||
* `null`/`undefined`,存取 `e.stderr`/`e.stdout`/`e.message` 會直接拋出
|
||||
* TypeError,並非完全的保守容錯(需人工確認是否要補上 optional chaining 修正)。
|
||||
*/
|
||||
function summarizeApiError(e) {
|
||||
const responseData = e?.response?.data;
|
||||
@@ -95,13 +140,24 @@ function summarizeApiError(e) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 透過 CLIProxyAPI 執行一次對話並回傳純文字結果。
|
||||
* 透過 CLIProxyAPI 執行一次對話 HTTP 請求,回傳 API 的原始回應資料(物件),
|
||||
* 並記錄回應 header 中的速率配額資訊。
|
||||
*
|
||||
* @param {{provider: string, baseURL: string, apiKeys: string[], model: string}} cfg - 連線設定。
|
||||
* @param {string} prompt - 送給 API 的完整 prompt 內容。
|
||||
* @remarks 適合用在需呼叫外部 AI API 的情境。
|
||||
* @remarks 逾時與輸出上限由環境變數控制,預設值是保守設定。
|
||||
* @remarks 若 HTTP 回傳非 2xx,錯誤訊息會由上層摘要處理。
|
||||
* 僅送出一次請求,不含任何重試邏輯——失敗(逾時、網路錯誤、非 2xx 狀態碼)時
|
||||
* 由 axios 直接拋出例外,交由呼叫端(chat())攔截並摘要。僅使用
|
||||
* `apiKeys` 陣列的第一個元素,不會輪替其他金鑰。
|
||||
*
|
||||
* @param {{provider: string, baseURL: string, apiKeys: string[], model: string}} cfg - 連線設定;
|
||||
* 僅使用 `apiKeys[0]`。
|
||||
* @param {string} prompt - 送給 API 的完整 prompt 內容,會作為 user 訊息內容;
|
||||
* HTTP 層的 system 訊息為固定的通用指示,與 prompt 內可能內嵌的 `<system>` 內容無關。
|
||||
* @returns {Promise<any>} API 回應的原始資料物件(`resp.data`),並非純文字;
|
||||
* 純文字需由呼叫端自行從 `data.choices[0].message.content` 等欄位擷取。
|
||||
* @throws 當 HTTP 請求失敗(逾時、網路錯誤、非 2xx 狀態碼)時,`axios` 拋出的
|
||||
* 例外會原樣向外傳播,本函式不攔截、不重試。
|
||||
* @remarks 逾時與輸出上限由環境變數 `AI_ASSISTANT_TIMEOUT_MS`/`AI_ASSISTANT_MAX_BUFFER`
|
||||
* 控制,預設值為 15 分鐘/20 MB。
|
||||
* @remarks 使用 `getInsecureHttpsAgent()`(停用 TLS 憑證驗證),適用內部自簽憑證環境。
|
||||
*/
|
||||
async function runProxyAPI({ provider, baseURL, apiKeys, model }, prompt) {
|
||||
const timeout = Number(process.env.AI_ASSISTANT_TIMEOUT_MS || 15 * 60 * 1000);
|
||||
@@ -139,12 +195,16 @@ async function runProxyAPI({ provider, baseURL, apiKeys, model }, prompt) {
|
||||
* 對目前環境可用的 CLIProxyAPI 送出一次對話請求並回傳純文字回應。
|
||||
*
|
||||
* 從設定取得 provider/baseURL/model;未偵測到 proxy 時拋錯。成功時記錄一次
|
||||
* usage 呼叫並回傳內容。
|
||||
* usage 呼叫並回傳內容。**不含任何重試邏輯**——無論是設定缺失、底層 HTTP 請求
|
||||
* 失敗,或回應內容為空,都是失敗一次即向外拋出(重新包裝為新的 Error,只保留
|
||||
* 摘要後訊息),不會自動重試或切換金鑰/provider。呼叫前後皆會透過 line() 記錄
|
||||
* 一行 log(成功記啟動資訊,失敗記錯誤摘要)。
|
||||
*
|
||||
* @param {string} systemPrompt - 系統提示詞。
|
||||
* @param {string} userContent - 使用者輸入內容。
|
||||
* @returns {Promise<string>} 模型回應的純文字內容。
|
||||
* @throws {Error} 當未偵測到可用 CLIProxyAPI,或 API 呼叫失敗時。
|
||||
* @throws {Error} 當未偵測到可用 CLIProxyAPI 設定、底層 API 呼叫失敗,或回應
|
||||
* 缺少可用文字內容時。
|
||||
*/
|
||||
export async function chat(systemPrompt, userContent) {
|
||||
const cfg = getLLMConfig();
|
||||
@@ -174,12 +234,16 @@ export async function chat(systemPrompt, userContent) {
|
||||
/**
|
||||
* 對 CLIProxyAPI 送出對話並將回應解析為 JSON 物件/陣列。
|
||||
*
|
||||
* 先取得文字回應,經 {@link extractJSONText} 抽出 JSON 片段後解析。
|
||||
* 解析失敗時記錄錯誤並回傳空陣列,不向外拋錯(容錯設計)。
|
||||
* 先呼叫 {@link chat} 取得文字回應,再經 {@link extractJSONText} 抽出 JSON 片段後
|
||||
* 以 JSON.parse 解析。**僅 JSON 解析失敗時容錯**(記錄錯誤並回傳空陣列 `[]`,不
|
||||
* 向外拋錯);若 `chat()` 本身失敗(例如未偵測到可用 CLIProxyAPI 設定、API 呼叫
|
||||
* 失敗,或回應缺少文字內容),該例外不會被本函式攔截,會直接向外拋出。
|
||||
*
|
||||
* @param {string} systemPrompt - 系統提示詞。
|
||||
* @param {string} userContent - 使用者輸入內容。
|
||||
* @returns {Promise<any>} 解析後的 JSON 值;解析失敗時回傳空陣列 `[]`。
|
||||
* @returns {Promise<any>} 解析後的 JSON 值;僅當 JSON 解析失敗時回傳空陣列 `[]`。
|
||||
* @throws {Error} 當底層 {@link chat} 呼叫失敗時(設定缺失、API 錯誤、回應無文字
|
||||
* 內容等),例外會原樣向外傳播。
|
||||
*/
|
||||
export async function chatJSON(systemPrompt, userContent) {
|
||||
const text = await chat(systemPrompt, userContent);
|
||||
|
||||
Reference in New Issue
Block a user