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 });