發佈 0.1.12:會話模式新增「每則獨立」,ctx 達 80% 自動開新會話

根本原因:
會話越用越慢——每張生成圖都會塞回上下文,實測同一會話從 36 秒
一路漲到 135 秒,也更容易在串流時被切斷。

修法:
1. 會話模式 sharedSession 布林改為 sessionMode(per-chat / shared / stateless),
   stateless = 每則訊息獨立、不保留記憶,適合單張生圖這類不需上下文的 bot;
   舊設定檔的 sharedSession 自動相容轉換。
2. 新增 autoResetCtxPercent(預設 80):任一會話上下文用量達門檻即自動
   開新會話並在回覆末尾提示,避免無限累積。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-17 16:18:33 +08:00
co-authored by Claude Fable 5
parent cfa61aa01a
commit 24c66130d0
5 changed files with 54 additions and 25 deletions
+33 -13
View File
@@ -94,6 +94,8 @@ const OUTBOX_RULE =
'bot 會在回覆後自動傳送到 Telegram 並清空該資料夾。';
const MAX_OUTBOX_FILES = 10;
const SESSION_MODE_LABELS = { 'per-chat': '各聊天室獨立', shared: '所有聊天室共用', stateless: '每則訊息獨立(不保留記憶)' };
const NEW_COMMANDS = ['/new', '!clear', '!reset', '!new', '!清除', '!重置', '!新會話'];
const STATUS_COMMANDS = ['/status', '!status', '!狀態'];
const HELP_COMMANDS = ['/start', '/help', '!help', '!幫助'];
@@ -225,15 +227,17 @@ function startBot(config) {
}
function statusText(chatId) {
const s = sessions.get(sessionKeyOf(chatId));
const s = sessionKeyOf(chatId) ? sessions.get(sessionKeyOf(chatId)) : null;
return [
'📋 目前狀態',
`工作目錄:${config.workDir}`,
`沙盒模式:${config.sandbox}`,
`模型:${config.model || 'codex 預設)'}`,
`推理強度:${config.reasoningEffort || '(預設)'}`,
`會話模式:${config.sharedSession ? '所有聊天室共用' : '各聊天室獨立'}`,
s ? `會話:${s.threadId}\n最後使用:${s.updatedAt}` : '會話:尚未建立(下一則訊息會開新會話)',
`會話模式:${SESSION_MODE_LABELS[config.sessionMode] || config.sessionMode}`,
config.sessionMode === 'stateless'
? '會話:每則訊息獨立,不保留記憶'
: s ? `會話:${s.threadId}\n最後使用:${s.updatedAt}` : '會話:尚未建立(下一則訊息會開新會話)',
].join('\n');
}
@@ -249,8 +253,10 @@ function startBot(config) {
].join('\n');
// 會話 key:獨立模式用 chat id,共用模式所有聊天室用同一把
// stateless 模式回傳 null:永遠開新會話、不存
function sessionKeyOf(chatId) {
return config.sharedSession ? 'shared' : chatId;
if (config.sessionMode === 'stateless') return null;
return config.sessionMode === 'shared' ? 'shared' : chatId;
}
async function handleMessage(message) {
@@ -269,12 +275,14 @@ function startBot(config) {
return;
}
if (matchCommand(content, NEW_COMMANDS)) {
sessions.clear(sessionKey);
if (sessionKey) sessions.clear(sessionKey);
await t.sendMessage(
chatId,
config.sharedSession
? '🆕 已開新會話(此 bot 為共用會話模式,所有聊天室的記憶一併清除)。'
: '🆕 已開新會話,之前的對話記憶已清除。',
config.sessionMode === 'stateless'
? 'ℹ️ 此 bot 每則訊息都是獨立會話,本來就不保留記憶。'
: config.sessionMode === 'shared'
? '🆕 已開新會話(此 bot 為共用會話模式,所有聊天室的記憶一併清除)。'
: '🆕 已開新會話,之前的對話記憶已清除。',
{ replyTo: message.message_id }
);
return;
@@ -317,7 +325,7 @@ function startBot(config) {
}
const prompt = buildPrompt(message, content || '(使用者只傳了圖片,請描述並依上下文處理)');
const existing = sessions.get(sessionKey);
const existing = sessionKey ? sessions.get(sessionKey) : null;
const turnStart = Date.now();
let result;
@@ -334,7 +342,7 @@ function startBot(config) {
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);
if (sessionKey) sessions.clear(sessionKey);
tid = null;
sessionResetNote = modelMismatch
? '🆕 模型設定已變更,舊會話無法沿用,已自動開新會話(先前的對話記憶未帶入)。\n\n'
@@ -344,7 +352,7 @@ function startBot(config) {
// 暫時性連線中斷(OpenAI 端切斷串流等)
if (err.transient) {
if (err.threadId) tid = err.threadId;
if (tid) sessions.set(sessionKey, tid); // 會話已建立,先記下來以便 resume
if (tid && sessionKey) sessions.set(sessionKey, tid); // 會話已建立,先記下來以便 resume
// 中斷前若圖已生成,直接交付,不重跑(避免重複生圖、多花時間)
if (collectGeneratedImages(tid, turnStart).length > 0) {
result = { text: '(回覆文字因連線中斷遺失,圖片如下)', threadId: tid, usage: null };
@@ -364,10 +372,22 @@ function startBot(config) {
}
}
if (result.threadId) sessions.set(sessionKey, result.threadId);
if (result.threadId && sessionKey) sessions.set(sessionKey, result.threadId);
// 上下文用量過高就自動開新會話:越積越慢(尤其含圖),也容易在串流時被切斷
let ctxNote = '';
const cwModel = config.model || codexDefaults.model;
const cw = modelMeta[cwModel]?.context_window;
if (sessionKey && config.autoResetCtxPercent > 0 && result.usage && cw) {
const pct = Math.round(((result.usage.input_tokens || 0) + (result.usage.output_tokens || 0)) / cw * 100);
if (pct >= config.autoResetCtxPercent) {
sessions.clear(sessionKey);
ctxNote = `\n\n🧹 對話記憶已用到 ${pct}%,下一則訊息會自動開新會話(避免越來越慢)。`;
}
}
const footer = buildFooter(config, result.usage, modelMeta, codexDefaults);
const html = t.mdToTgHtml(sessionResetNote + (result.text || 'Codex 沒有回覆文字)') + footer);
const html = t.mdToTgHtml(sessionResetNote + (result.text || 'Codex 沒有回覆文字)') + ctxNote + footer);
await t.editOrSplit(chatId, statusMsg.message_id, html, { html: true });
await flushOutputs(chatId, message.message_id, result.threadId, turnStart);
await t.react(chatId, message.message_id, '👍');