From cfa61aa01ae74918904f5d335c03d6f8e04110e9 Mon Sep 17 00:00:00 2001 From: JianMiau Date: Mon, 17 Aug 2026 16:08:58 +0800 Subject: [PATCH] =?UTF-8?q?=E7=99=BC=E4=BD=88=200.1.11=EF=BC=9A=E9=80=A3?= =?UTF-8?q?=E7=B7=9A=E4=B8=AD=E6=96=B7=E8=87=AA=E5=8B=95=E9=87=8D=E8=A9=A6?= =?UTF-8?q?=EF=BC=8C=E5=B7=B2=E7=94=9F=E6=88=90=E7=9A=84=E5=9C=96=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E4=B8=9F=E5=A4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根本原因: imagegen 產圖成功後,模型把含圖結果串流回來時 OpenAI 端偶發切斷 WebSocket(stream disconnected / websocket closed),codex 回報 turn.failed, bot 視為致命錯誤,連已生好的圖一起丟掉,使用者只看到錯誤。 修法: 1. codex.js 失敗時附帶 threadId 與 transient 旗標(辨識暫時性連線錯誤)。 2. bot.js 改為重試迴圈:暫時性錯誤時,若本輪 generated_images 已有產圖, 直接交付並註明「連線中斷但圖已生成」,不重跑(避免重複生圖); 沒產出才 resume 同一會話重試一次,狀態訊息顯示「自動重試中」。 會話失效/換模型的自動開新會話邏輯併入同一迴圈。 Co-Authored-By: Claude Fable 5 --- package.json | 2 +- src/bot.js | 53 +++++++++++++++++++++++++++++++++++++--------------- src/codex.js | 6 +++++- 3 files changed, 44 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 1f4311c..ed410bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "telegram-codex-bot", - "version": "0.1.10", + "version": "0.1.11", "description": "Telegram bot powered by the local Codex CLI — zero-dependency, pm2-managed, with a built-in PM2 web viewer", "license": "MIT", "type": "commonjs", diff --git a/src/bot.js b/src/bot.js index e8f0c96..6bb8b38 100644 --- a/src/bot.js +++ b/src/bot.js @@ -322,21 +322,44 @@ function startBot(config) { let result; let sessionResetNote = ''; - try { - result = await runCodex({ config, prompt, threadId: existing?.threadId || null, images, onProgress }); - } catch (err) { - // 舊會話 resume 不了的情況,自動開新會話重試一次: - // 1) 會話已被 codex 清掉(not found) - // 2) bot 換了模型,codex 拒絕跨模型 resume(recorded with model X but resuming with Y) - const sessionGone = /session|thread|conversation/i.test(err.message) && /not.*found|找不到|no .*(session|thread)/i.test(err.message); - const modelMismatch = /recorded with model/i.test(err.message); - if (existing && (sessionGone || modelMismatch)) { - sessions.clear(sessionKey); - result = await runCodex({ config, prompt, threadId: null, images, onProgress }); - sessionResetNote = modelMismatch - ? '🆕 模型設定已變更,舊會話無法沿用,已自動開新會話(先前的對話記憶未帶入)。\n\n' - : '🆕 舊會話已失效,已自動開新會話。\n\n'; - } else { + let tid = existing?.threadId || null; + const MAX_ATTEMPTS = 2; + for (let attempt = 1; ; attempt++) { + try { + result = await runCodex({ config, prompt, threadId: tid, images, onProgress }); + break; + } catch (err) { + // 舊會話 resume 不了:1) 會話已被 codex 清掉 2) 換模型後 codex 拒絕跨模型 resume + // → 清掉會話、開新會話重跑(tid=null 之後不會再進這個分支,不會無限迴圈) + const sessionGone = /session|thread|conversation/i.test(err.message) && /not.*found|找不到|no .*(session|thread)/i.test(err.message); + const modelMismatch = /recorded with model/i.test(err.message); + if (tid && (sessionGone || modelMismatch)) { + sessions.clear(sessionKey); + tid = null; + sessionResetNote = modelMismatch + ? '🆕 模型設定已變更,舊會話無法沿用,已自動開新會話(先前的對話記憶未帶入)。\n\n' + : '🆕 舊會話已失效,已自動開新會話。\n\n'; + continue; + } + // 暫時性連線中斷(OpenAI 端切斷串流等) + if (err.transient) { + if (err.threadId) tid = err.threadId; + if (tid) sessions.set(sessionKey, tid); // 會話已建立,先記下來以便 resume + // 中斷前若圖已生成,直接交付,不重跑(避免重複生圖、多花時間) + if (collectGeneratedImages(tid, turnStart).length > 0) { + result = { text: '(回覆文字因連線中斷遺失,圖片如下)', threadId: tid, usage: null }; + sessionResetNote += '⚠️ 連線在回覆途中中斷,但圖片已生成完畢,直接送上。\n\n'; + break; + } + if (attempt < MAX_ATTEMPTS) { + await t.tg('editMessageText', { + chat_id: chatId, + message_id: statusMsg.message_id, + text: `⚠️ 連線中斷,自動重試中(${attempt + 1}/${MAX_ATTEMPTS})…`, + }).catch(() => {}); + continue; + } + } throw err; } } diff --git a/src/codex.js b/src/codex.js index 334520c..df3be02 100644 --- a/src/codex.js +++ b/src/codex.js @@ -198,7 +198,11 @@ function runCodex({ config, prompt, threadId, images = [], onProgress }) { proc.on('close', (code) => { clearTimeout(timer); if (turnFailedMessage) { - return reject(new Error(`Codex 執行失敗:${turnFailedMessage}`)); + const e = new Error(`Codex 執行失敗:${turnFailedMessage}`); + e.threadId = resultThreadId; // 會話可能已建立,讓呼叫端能 resume 重試 + // 暫時性連線問題(OpenAI 端切斷串流等),呼叫端可重試 + e.transient = /stream disconnected|websocket closed|Falling back from WebSockets|connection reset|ECONNRESET|socket hang up|timed out/i.test(turnFailedMessage); + return reject(e); } if (turnCompleted || messages.length > 0) { return resolve({ text: messages.join('\n\n'), threadId: resultThreadId, usage });