發佈 0.1.7:新增會話模式開關(獨立/共用)
摘要: bot 設定新增「會話模式」:預設各聊天室獨立會話;可切為所有聊天室 共用一條記憶(單一專案助理情境)。共用模式下 /new 清除全部記憶, /status 會顯示目前模式,bot 列表小字標示「共用會話」。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "telegram-codex-bot",
|
"name": "telegram-codex-bot",
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"description": "Telegram bot powered by the local Codex CLI — zero-dependency, pm2-managed, with a built-in PM2 web viewer",
|
"description": "Telegram bot powered by the local Codex CLI — zero-dependency, pm2-managed, with a built-in PM2 web viewer",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
|
|||||||
+19
-7
@@ -136,14 +136,14 @@ function startBot(config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function statusText(chatId) {
|
function statusText(chatId) {
|
||||||
const s = sessions.get(chatId);
|
const s = sessions.get(sessionKeyOf(chatId));
|
||||||
return [
|
return [
|
||||||
'📋 目前狀態',
|
'📋 目前狀態',
|
||||||
`工作目錄:${config.workDir}`,
|
`工作目錄:${config.workDir}`,
|
||||||
`沙盒模式:${config.sandbox}`,
|
`沙盒模式:${config.sandbox}`,
|
||||||
`模型:${config.model || '(codex 預設)'}`,
|
`模型:${config.model || '(codex 預設)'}`,
|
||||||
`推理強度:${config.reasoningEffort || '(預設)'}`,
|
`推理強度:${config.reasoningEffort || '(預設)'}`,
|
||||||
`速度:${config.serviceTier || '(預設)'}`,
|
`會話模式:${config.sharedSession ? '所有聊天室共用' : '各聊天室獨立'}`,
|
||||||
s ? `會話:${s.threadId}\n最後使用:${s.updatedAt}` : '會話:尚未建立(下一則訊息會開新會話)',
|
s ? `會話:${s.threadId}\n最後使用:${s.updatedAt}` : '會話:尚未建立(下一則訊息會開新會話)',
|
||||||
].join('\n');
|
].join('\n');
|
||||||
}
|
}
|
||||||
@@ -159,9 +159,15 @@ function startBot(config) {
|
|||||||
'群組中要 @我 或回覆我的訊息才會觸發。可以直接傳圖片(附文字說明)。',
|
'群組中要 @我 或回覆我的訊息才會觸發。可以直接傳圖片(附文字說明)。',
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
|
// 會話 key:獨立模式用 chat id,共用模式所有聊天室用同一把
|
||||||
|
function sessionKeyOf(chatId) {
|
||||||
|
return config.sharedSession ? 'shared' : chatId;
|
||||||
|
}
|
||||||
|
|
||||||
async function handleMessage(message) {
|
async function handleMessage(message) {
|
||||||
if (!message || message.from?.id === botId) return;
|
if (!message || message.from?.id === botId) return;
|
||||||
const chatId = message.chat.id;
|
const chatId = message.chat.id;
|
||||||
|
const sessionKey = sessionKeyOf(chatId);
|
||||||
const text = message.text || message.caption || '';
|
const text = message.text || message.caption || '';
|
||||||
const photo = pickPhoto(message);
|
const photo = pickPhoto(message);
|
||||||
if (!text && !photo) return;
|
if (!text && !photo) return;
|
||||||
@@ -174,8 +180,14 @@ function startBot(config) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (matchCommand(content, NEW_COMMANDS)) {
|
if (matchCommand(content, NEW_COMMANDS)) {
|
||||||
sessions.clear(chatId);
|
sessions.clear(sessionKey);
|
||||||
await t.sendMessage(chatId, '🆕 已開新會話,之前的對話記憶已清除。', { replyTo: message.message_id });
|
await t.sendMessage(
|
||||||
|
chatId,
|
||||||
|
config.sharedSession
|
||||||
|
? '🆕 已開新會話(此 bot 為共用會話模式,所有聊天室的記憶一併清除)。'
|
||||||
|
: '🆕 已開新會話,之前的對話記憶已清除。',
|
||||||
|
{ replyTo: message.message_id }
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (matchCommand(content, STATUS_COMMANDS)) {
|
if (matchCommand(content, STATUS_COMMANDS)) {
|
||||||
@@ -216,7 +228,7 @@ function startBot(config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const prompt = buildPrompt(message, content || '(使用者只傳了圖片,請描述並依上下文處理)');
|
const prompt = buildPrompt(message, content || '(使用者只傳了圖片,請描述並依上下文處理)');
|
||||||
const existing = sessions.get(chatId);
|
const existing = sessions.get(sessionKey);
|
||||||
|
|
||||||
let result;
|
let result;
|
||||||
let sessionResetNote = '';
|
let sessionResetNote = '';
|
||||||
@@ -229,7 +241,7 @@ function startBot(config) {
|
|||||||
const sessionGone = /session|thread|conversation/i.test(err.message) && /not.*found|找不到|no .*(session|thread)/i.test(err.message);
|
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);
|
const modelMismatch = /recorded with model/i.test(err.message);
|
||||||
if (existing && (sessionGone || modelMismatch)) {
|
if (existing && (sessionGone || modelMismatch)) {
|
||||||
sessions.clear(chatId);
|
sessions.clear(sessionKey);
|
||||||
result = await runCodex({ config, prompt, threadId: null, images, onProgress });
|
result = await runCodex({ config, prompt, threadId: null, images, onProgress });
|
||||||
sessionResetNote = modelMismatch
|
sessionResetNote = modelMismatch
|
||||||
? '🆕 模型設定已變更,舊會話無法沿用,已自動開新會話(先前的對話記憶未帶入)。\n\n'
|
? '🆕 模型設定已變更,舊會話無法沿用,已自動開新會話(先前的對話記憶未帶入)。\n\n'
|
||||||
@@ -239,7 +251,7 @@ function startBot(config) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.threadId) sessions.set(chatId, result.threadId);
|
if (result.threadId) sessions.set(sessionKey, result.threadId);
|
||||||
|
|
||||||
const footer = buildFooter(config, result.usage, modelMeta, codexDefaults);
|
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 沒有回覆文字)') + footer);
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ const DEFAULTS = {
|
|||||||
reasoningEffort: '',
|
reasoningEffort: '',
|
||||||
// 速度固定 Fast(priority = 1.5x);要改只能手動編輯設定檔
|
// 速度固定 Fast(priority = 1.5x);要改只能手動編輯設定檔
|
||||||
serviceTier: 'priority',
|
serviceTier: 'priority',
|
||||||
|
// 會話模式:false = 每個聊天室獨立會話(預設);true = 所有聊天室共用一個會話
|
||||||
|
sharedSession: false,
|
||||||
// 單次回應逾時(分鐘)
|
// 單次回應逾時(分鐘)
|
||||||
timeoutMinutes: 30,
|
timeoutMinutes: 30,
|
||||||
// pm2 程序名稱(web 檢視工具會叫 <pm2Name>-web)
|
// pm2 程序名稱(web 檢視工具會叫 <pm2Name>-web)
|
||||||
|
|||||||
+4
-1
@@ -78,6 +78,7 @@ async function listBots() {
|
|||||||
sandbox: cfg?.sandbox || null,
|
sandbox: cfg?.sandbox || null,
|
||||||
model: cfg?.model || '',
|
model: cfg?.model || '',
|
||||||
reasoningEffort: cfg?.reasoningEffort || '',
|
reasoningEffort: cfg?.reasoningEffort || '',
|
||||||
|
sharedSession: !!cfg?.sharedSession,
|
||||||
serviceTier: cfg?.serviceTier || '',
|
serviceTier: cfg?.serviceTier || '',
|
||||||
timeoutMinutes: cfg?.timeoutMinutes || null,
|
timeoutMinutes: cfg?.timeoutMinutes || null,
|
||||||
tokenMasked: maskToken(cfg?.telegramToken),
|
tokenMasked: maskToken(cfg?.telegramToken),
|
||||||
@@ -112,7 +113,7 @@ function validateBotInput({ name, token, workDir, sandbox }, { isCreate }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createBot({ name, token, workDir, sandbox, model, reasoningEffort, timeoutMinutes }) {
|
async function createBot({ name, token, workDir, sandbox, model, reasoningEffort, sharedSession, timeoutMinutes }) {
|
||||||
validateBotInput({ name, token, workDir, sandbox }, { isCreate: true });
|
validateBotInput({ name, token, workDir, sandbox }, { isCreate: true });
|
||||||
const reg = loadRegistry();
|
const reg = loadRegistry();
|
||||||
if (reg.bots[name]) throw new Error(`已有同名 bot:${name}`);
|
if (reg.bots[name]) throw new Error(`已有同名 bot:${name}`);
|
||||||
@@ -128,6 +129,7 @@ async function createBot({ name, token, workDir, sandbox, model, reasoningEffort
|
|||||||
sandbox: sandbox || 'workspace-write',
|
sandbox: sandbox || 'workspace-write',
|
||||||
model: model || '',
|
model: model || '',
|
||||||
reasoningEffort: reasoningEffort || '',
|
reasoningEffort: reasoningEffort || '',
|
||||||
|
sharedSession: !!sharedSession,
|
||||||
serviceTier: 'priority', // 速度固定 Fast 1.5x
|
serviceTier: 'priority', // 速度固定 Fast 1.5x
|
||||||
timeoutMinutes: Number(timeoutMinutes) || 30,
|
timeoutMinutes: Number(timeoutMinutes) || 30,
|
||||||
pm2Name: name,
|
pm2Name: name,
|
||||||
@@ -158,6 +160,7 @@ async function updateBot(name, patch) {
|
|||||||
}
|
}
|
||||||
if (patch.model !== undefined) raw.model = patch.model;
|
if (patch.model !== undefined) raw.model = patch.model;
|
||||||
if (patch.reasoningEffort !== undefined) raw.reasoningEffort = patch.reasoningEffort;
|
if (patch.reasoningEffort !== undefined) raw.reasoningEffort = patch.reasoningEffort;
|
||||||
|
if (patch.sharedSession !== undefined) raw.sharedSession = !!patch.sharedSession;
|
||||||
if (patch.timeoutMinutes !== undefined && Number(patch.timeoutMinutes) > 0) {
|
if (patch.timeoutMinutes !== undefined && Number(patch.timeoutMinutes) > 0) {
|
||||||
raw.timeoutMinutes = Number(patch.timeoutMinutes);
|
raw.timeoutMinutes = Number(patch.timeoutMinutes);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -219,6 +219,14 @@
|
|||||||
<select id="f-effort"></select>
|
<select id="f-effort"></select>
|
||||||
<div class="hint">速度固定為 Fast(1.5x)</div>
|
<div class="hint">速度固定為 Fast(1.5x)</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>會話模式</label>
|
||||||
|
<select id="f-session">
|
||||||
|
<option value="per-chat" selected>各聊天室獨立(私訊、每個群組各自記憶)</option>
|
||||||
|
<option value="shared">所有聊天室共用(同一條記憶,適合單一專案助理)</option>
|
||||||
|
</select>
|
||||||
|
<div class="hint">共用模式下 /new 會清掉所有聊天室的記憶</div>
|
||||||
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>逾時(分鐘)</label>
|
<label>逾時(分鐘)</label>
|
||||||
<input id="f-timeout" type="number" value="30" min="1">
|
<input id="f-timeout" type="number" value="30" min="1">
|
||||||
@@ -374,8 +382,8 @@ function renderBots(bots) {
|
|||||||
'<td>' + statusBadge(b.status) + '</td>' +
|
'<td>' + statusBadge(b.status) + '</td>' +
|
||||||
'<td><span class="mono" title="' + esc(b.workDir) + '">' + esc(shortPath(b.workDir)) + '</span></td>' +
|
'<td><span class="mono" title="' + esc(b.workDir) + '">' + esc(shortPath(b.workDir)) + '</span></td>' +
|
||||||
'<td class="mono">' + esc(b.sandbox || '-') +
|
'<td class="mono">' + esc(b.sandbox || '-') +
|
||||||
((b.model || b.reasoningEffort)
|
((b.model || b.reasoningEffort || b.sharedSession)
|
||||||
? '<br><span class="script-name">' + esc([b.model, b.reasoningEffort].filter(Boolean).join(' / ')) + '</span>'
|
? '<br><span class="script-name">' + esc([b.model, b.reasoningEffort, b.sharedSession ? '共用會話' : ''].filter(Boolean).join(' / ')) + '</span>'
|
||||||
: '') + '</td>' +
|
: '') + '</td>' +
|
||||||
'<td class="mono">' + esc(b.tokenMasked || '-') + '</td>' +
|
'<td class="mono">' + esc(b.tokenMasked || '-') + '</td>' +
|
||||||
'<td>' + fmtBytes(b.memory) + '</td>' +
|
'<td>' + fmtBytes(b.memory) + '</td>' +
|
||||||
@@ -444,6 +452,7 @@ function openCreate() {
|
|||||||
document.getElementById('f-workdir').value = '';
|
document.getElementById('f-workdir').value = '';
|
||||||
document.getElementById('f-sandbox').value = 'workspace-write';
|
document.getElementById('f-sandbox').value = 'workspace-write';
|
||||||
populateModelSelects('', '');
|
populateModelSelects('', '');
|
||||||
|
document.getElementById('f-session').value = 'per-chat';
|
||||||
document.getElementById('f-timeout').value = '30';
|
document.getElementById('f-timeout').value = '30';
|
||||||
document.getElementById('f-autostart-wrap').style.display = '';
|
document.getElementById('f-autostart-wrap').style.display = '';
|
||||||
document.getElementById('f-autostart').checked = true;
|
document.getElementById('f-autostart').checked = true;
|
||||||
@@ -465,6 +474,7 @@ function openEdit(name) {
|
|||||||
document.getElementById('f-workdir').value = b.workDir || '';
|
document.getElementById('f-workdir').value = b.workDir || '';
|
||||||
document.getElementById('f-sandbox').value = b.sandbox || 'workspace-write';
|
document.getElementById('f-sandbox').value = b.sandbox || 'workspace-write';
|
||||||
populateModelSelects(b.model || '', b.reasoningEffort || '');
|
populateModelSelects(b.model || '', b.reasoningEffort || '');
|
||||||
|
document.getElementById('f-session').value = b.sharedSession ? 'shared' : 'per-chat';
|
||||||
document.getElementById('f-timeout').value = b.timeoutMinutes || 30;
|
document.getElementById('f-timeout').value = b.timeoutMinutes || 30;
|
||||||
document.getElementById('f-autostart-wrap').style.display = 'none';
|
document.getElementById('f-autostart-wrap').style.display = 'none';
|
||||||
document.getElementById('bot-modal').classList.add('show');
|
document.getElementById('bot-modal').classList.add('show');
|
||||||
@@ -485,6 +495,7 @@ async function saveBot() {
|
|||||||
sandbox: document.getElementById('f-sandbox').value,
|
sandbox: document.getElementById('f-sandbox').value,
|
||||||
model: document.getElementById('f-model').value,
|
model: document.getElementById('f-model').value,
|
||||||
reasoningEffort: document.getElementById('f-effort').value,
|
reasoningEffort: document.getElementById('f-effort').value,
|
||||||
|
sharedSession: document.getElementById('f-session').value === 'shared',
|
||||||
timeoutMinutes: document.getElementById('f-timeout').value,
|
timeoutMinutes: document.getElementById('f-timeout').value,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user