6 Commits
Author SHA1 Message Date
JianMiauandClaude Fable 5 d5c4464fe1 發佈 0.1.7:新增會話模式開關(獨立/共用)
摘要:
bot 設定新增「會話模式」:預設各聊天室獨立會話;可切為所有聊天室
共用一條記憶(單一專案助理情境)。共用模式下 /new 清除全部記憶,
/status 會顯示目前模式,bot 列表小字標示「共用會話」。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 12:05:30 +08:00
JianMiauandClaude Fable 5 6e39a8084f 發佈 0.1.6:速度固定 Fast,移除選項與 footer 顯示
摘要:
速度(service tier)固定為 priority(Fast 1.5x):表單移除速度下拉、
footer 不再顯示 Fast 字樣;舊設定檔存空值也會矯正回 priority。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 12:02:07 +08:00
JianMiauandClaude Fable 5 491ab5bc8f 發佈 0.1.5:換模型後舊會話自動重開
根本原因:
codex 拒絕用不同模型 resume 舊會話(recorded with model X but resuming with Y),
bot 換模型設定後,既有聊天室的會話會直接報錯卡住。

修法:
偵測到模型不匹配(或會話已被清掉)時自動清除會話、開新會話重試,
並在回覆開頭註明已自動開新會話、舊記憶未帶入。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 11:56:22 +08:00
JianMiauandClaude Fable 5 8d4cec9bb3 發佈與版控目標改為 jianmiau
摘要:
publishConfig 與 repository 改指向 jianmiau.ddns.net,之後裸 npm publish
直接發到公開 registry;git origin 也已對調為 jianmiau(catan 降為備用 remote)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 17:14:01 +08:00
JianMiauandClaude Fable 5 c8b3ff68a7 發佈 0.1.4:README 安裝指令統一用公開 registry
摘要:
安裝與升級指令改為只列 jianmiau.ddns.net 的公開 registry(免登入),
移除公司內部 registry 的說明。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 17:12:43 +08:00
JianMiauandClaude Fable 5 ef9fcb9d4a 發佈 0.1.3:README 圖片改公開網址、安裝說明補公開 registry
摘要:
README 截圖改指向 jianmiau.ddns.net(公開 repo,免登入可看),
兩邊套件頁的圖都能正常顯示;安裝說明補上公開 registry 的裝法。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 17:09:08 +08:00
6 changed files with 72 additions and 58 deletions
+8 -7
View File
@@ -20,9 +20,10 @@
## 快速開始
套件不在官方 npmjs 上,安裝要指定 `--registry`(不用登入):
```bash
# 套件發佈在公司的 npm registry(不是官方 npmjs),安裝要指定 --registry
npm install -g telegram-codex-bot --registry=https://git.catan.com.tw/api/packages/Frontend/npm/
npm install -g telegram-codex-bot --registry=https://jianmiau.ddns.net/api/packages/AI/npm/
telegram-codex-bot start # 先做環境檢查(codex/登入/pm2/git),再把控制台掛上 pm2
```
@@ -30,7 +31,7 @@ telegram-codex-bot start # 先做環境檢查(codex/登入/pm2/git),再
之後升級同樣帶 `--registry`:
```bash
npm update -g telegram-codex-bot --registry=https://git.catan.com.tw/api/packages/Frontend/npm/
npm update -g telegram-codex-bot --registry=https://jianmiau.ddns.net/api/packages/AI/npm/
```
打開 **http://localhost:3799** →「🤖 Bot 管理」→「➕ 新增 Bot」:
@@ -58,7 +59,7 @@ npm update -g telegram-codex-bot --registry=https://git.catan.com.tw/api/package
**① Bot 管理首頁** — 每個 bot 一列:狀態、工作目錄、沙盒與模型設定、遮罩後的 token。
右側按鈕:▶ 啟動 / ⏹ 停止、🔄 重啟、📄 log、✏️ 編輯、🗑 刪除。上方橫幅提醒群組使用要先關 Group Privacy。
![Bot 管理首頁](https://git.catan.com.tw/Frontend/telegram-codex-bot/media/branch/master/docs/01-bots.png)
![Bot 管理首頁](https://jianmiau.ddns.net/AI/telegram-codex-bot/media/branch/master/docs/01-bots.png)
**② 新增 Bot** — 按右上「➕ 新增 Bot」:
@@ -68,15 +69,15 @@ npm update -g telegram-codex-bot --registry=https://git.catan.com.tw/api/package
4. **模型 / 推理強度 / 速度**:下拉選單,預設值自動帶你機器上 codex 的全域設定;清單讀自本機 codex 模型快取,換模型時強度選項會連動(如 ultra 只有支援的模型才出現)
5. 勾「建立後立即啟動」→ 儲存,bot 就上線了
![新增 Bot 表單](https://git.catan.com.tw/Frontend/telegram-codex-bot/media/branch/master/docs/02-create.png)
![新增 Bot 表單](https://jianmiau.ddns.net/AI/telegram-codex-bot/media/branch/master/docs/02-create.png)
**③ PM2 分頁** — 整台機器所有 pm2 程序。Port 可直接點開對應網頁,右側可啟停/重啟/刪除任何程序。
![PM2 分頁](https://git.catan.com.tw/Frontend/telegram-codex-bot/media/branch/master/docs/03-pm2.png)
![PM2 分頁](https://jianmiau.ddns.net/AI/telegram-codex-bot/media/branch/master/docs/03-pm2.png)
**④ Log 檢視** — 點任一程序的「📄 out」/「⚠ err」,下方展開尾端 200 行,可勾自動更新(5s)。
![Log 檢視](https://git.catan.com.tw/Frontend/telegram-codex-bot/media/branch/master/docs/04-log.png)
![Log 檢視](https://jianmiau.ddns.net/AI/telegram-codex-bot/media/branch/master/docs/04-log.png)
- 兩個分頁每 5 秒自動更新
- Bot 設定存於 `~/.tgcodex/bots/<名稱>/tgcodex.config.json`,bot 掛 pm2 的程序名 = bot 名稱
+3 -3
View File
@@ -1,15 +1,15 @@
{
"name": "telegram-codex-bot",
"version": "0.1.2",
"version": "0.1.7",
"description": "Telegram bot powered by the local Codex CLI — zero-dependency, pm2-managed, with a built-in PM2 web viewer",
"license": "MIT",
"type": "commonjs",
"publishConfig": {
"registry": "https://git.catan.com.tw/api/packages/Frontend/npm/"
"registry": "https://jianmiau.ddns.net/api/packages/AI/npm/"
},
"repository": {
"type": "git",
"url": "git@git.catan.com.tw:Frontend/telegram-codex-bot.git"
"url": "ssh://git@jianmiau.ddns.net:8022/AI/telegram-codex-bot.git"
},
"main": "src/index.js",
"bin": {
+32 -17
View File
@@ -32,7 +32,7 @@ function loadCodexDefaults() {
}
}
// — ✅ 16:06:32 | ctx 24% +1.2k | gpt-5.6-sol / high / Fast
// — ✅ 16:06:32 | ctx 24% +1.2k | gpt-5.6-sol / high
function buildFooter(config, usage, modelMeta, codexDefaults) {
const time = new Date().toLocaleTimeString('zh-TW', { hour12: false });
const parts = [`— ✅ ${time}`];
@@ -50,12 +50,7 @@ function buildFooter(config, usage, modelMeta, codexDefaults) {
if (model) {
const effort = config.reasoningEffort || codexDefaults.effort || meta?.default_reasoning_level || '';
const tier = config.serviceTier || codexDefaults.tier || '';
// 顯示速度層級的名稱(priority → Fast),對照模型快取
const tierName = tier
? (meta?.service_tiers?.find((t) => t.id === tier)?.name || tier)
: '';
parts.push([model, effort, tierName].filter(Boolean).join(' / '));
parts.push(effort ? `${model} / ${effort}` : model);
}
return `\n\n${parts.join(' | ')}`;
}
@@ -141,14 +136,14 @@ function startBot(config) {
}
function statusText(chatId) {
const s = sessions.get(chatId);
const s = sessions.get(sessionKeyOf(chatId));
return [
'📋 目前狀態',
`工作目錄:${config.workDir}`,
`沙盒模式:${config.sandbox}`,
`模型:${config.model || 'codex 預設)'}`,
`推理強度:${config.reasoningEffort || '(預設)'}`,
`速度${config.serviceTier || '(預設)'}`,
`會話模式${config.sharedSession ? '所有聊天室共用' : '各聊天室獨立'}`,
s ? `會話:${s.threadId}\n最後使用:${s.updatedAt}` : '會話:尚未建立(下一則訊息會開新會話)',
].join('\n');
}
@@ -164,9 +159,15 @@ function startBot(config) {
'群組中要 @我 或回覆我的訊息才會觸發。可以直接傳圖片(附文字說明)。',
].join('\n');
// 會話 key:獨立模式用 chat id,共用模式所有聊天室用同一把
function sessionKeyOf(chatId) {
return config.sharedSession ? 'shared' : chatId;
}
async function handleMessage(message) {
if (!message || message.from?.id === botId) return;
const chatId = message.chat.id;
const sessionKey = sessionKeyOf(chatId);
const text = message.text || message.caption || '';
const photo = pickPhoto(message);
if (!text && !photo) return;
@@ -179,8 +180,14 @@ function startBot(config) {
return;
}
if (matchCommand(content, NEW_COMMANDS)) {
sessions.clear(chatId);
await t.sendMessage(chatId, '🆕 已開新會話,之前的對話記憶已清除。', { replyTo: message.message_id });
sessions.clear(sessionKey);
await t.sendMessage(
chatId,
config.sharedSession
? '🆕 已開新會話(此 bot 為共用會話模式,所有聊天室的記憶一併清除)。'
: '🆕 已開新會話,之前的對話記憶已清除。',
{ replyTo: message.message_id }
);
return;
}
if (matchCommand(content, STATUS_COMMANDS)) {
@@ -221,25 +228,33 @@ function startBot(config) {
}
const prompt = buildPrompt(message, content || '(使用者只傳了圖片,請描述並依上下文處理)');
const existing = sessions.get(chatId);
const existing = sessions.get(sessionKey);
let result;
let sessionResetNote = '';
try {
result = await runCodex({ config, prompt, threadId: existing?.threadId || null, images, onProgress });
} catch (err) {
// 舊會話可能已被 codex 清掉,找不到就自動開新會話重試一次
if (existing && /session|thread|conversation/i.test(err.message) && /not.*found|找不到|no .*(session|thread)/i.test(err.message)) {
sessions.clear(chatId);
// 舊會話 resume 不了的情況,自動開新會話重試一次
// 1) 會話已被 codex 清掉(not found
// 2) bot 換了模型,codex 拒絕跨模型 resumerecorded 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 {
throw err;
}
}
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 html = t.mdToTgHtml((result.text || 'Codex 沒有回覆文字)') + footer);
const html = t.mdToTgHtml(sessionResetNote + (result.text || 'Codex 沒有回覆文字)') + footer);
await t.editOrSplit(chatId, statusMsg.message_id, html, { html: true });
await t.react(chatId, message.message_id, '👍');
} catch (error) {
+7 -2
View File
@@ -18,8 +18,10 @@ const DEFAULTS = {
model: '',
// 推理強度(low/medium/high/xhigh/max/ultra…依模型而定);空字串 = codex 預設
reasoningEffort: '',
// 速度service tier,如 priority = Fast 1.5x);空字串 = codex 預設
serviceTier: '',
// 速度固定 Fastpriority = 1.5x);要改只能手動編輯設定檔
serviceTier: 'priority',
// 會話模式:false = 每個聊天室獨立會話(預設);true = 所有聊天室共用一個會話
sharedSession: false,
// 單次回應逾時(分鐘)
timeoutMinutes: 30,
// pm2 程序名稱(web 檢視工具會叫 <pm2Name>-web
@@ -80,6 +82,9 @@ function loadConfig(explicitPath) {
}
cfg.web.port = port;
}
// 速度政策固定 Fast:舊設定檔存了空值也矯正回 priority
if (!cfg.serviceTier) cfg.serviceTier = 'priority';
const minutes = Number(cfg.timeoutMinutes);
if (!(minutes > 0)) throw new Error(`timeoutMinutes 必須是正數:${cfg.timeoutMinutes}`);
cfg.timeoutMinutes = minutes;
+5 -3
View File
@@ -78,6 +78,7 @@ async function listBots() {
sandbox: cfg?.sandbox || null,
model: cfg?.model || '',
reasoningEffort: cfg?.reasoningEffort || '',
sharedSession: !!cfg?.sharedSession,
serviceTier: cfg?.serviceTier || '',
timeoutMinutes: cfg?.timeoutMinutes || null,
tokenMasked: maskToken(cfg?.telegramToken),
@@ -112,7 +113,7 @@ function validateBotInput({ name, token, workDir, sandbox }, { isCreate }) {
}
}
async function createBot({ name, token, workDir, sandbox, model, reasoningEffort, serviceTier, timeoutMinutes }) {
async function createBot({ name, token, workDir, sandbox, model, reasoningEffort, sharedSession, timeoutMinutes }) {
validateBotInput({ name, token, workDir, sandbox }, { isCreate: true });
const reg = loadRegistry();
if (reg.bots[name]) throw new Error(`已有同名 bot${name}`);
@@ -128,7 +129,8 @@ async function createBot({ name, token, workDir, sandbox, model, reasoningEffort
sandbox: sandbox || 'workspace-write',
model: model || '',
reasoningEffort: reasoningEffort || '',
serviceTier: serviceTier || '',
sharedSession: !!sharedSession,
serviceTier: 'priority', // 速度固定 Fast 1.5x
timeoutMinutes: Number(timeoutMinutes) || 30,
pm2Name: name,
// UI 建的 bot 不各自開 web,統一由控制台管理
@@ -158,7 +160,7 @@ async function updateBot(name, patch) {
}
if (patch.model !== undefined) raw.model = patch.model;
if (patch.reasoningEffort !== undefined) raw.reasoningEffort = patch.reasoningEffort;
if (patch.serviceTier !== undefined) raw.serviceTier = patch.serviceTier;
if (patch.sharedSession !== undefined) raw.sharedSession = !!patch.sharedSession;
if (patch.timeoutMinutes !== undefined && Number(patch.timeoutMinutes) > 0) {
raw.timeoutMinutes = Number(patch.timeoutMinutes);
}
+17 -26
View File
@@ -217,10 +217,15 @@
<div class="field">
<label>推理強度</label>
<select id="f-effort"></select>
<div class="hint">速度固定為 Fast1.5x</div>
</div>
<div class="field">
<label>速度</label>
<select id="f-tier"></select>
<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">
<label>逾時(分鐘)</label>
@@ -314,7 +319,7 @@ async function loadModels() {
} catch { modelsCache = []; }
}
// 下拉沒有「預設」空選項:直接把真實預設值選起來
function populateModelSelects(selModel, selEffort, selTier) {
function populateModelSelects(selModel, selEffort) {
const mSel = document.getElementById('f-model');
let opts = modelsCache.map(m => '<option value="' + esc(m.slug) + '">' + esc(m.label) + '</option>').join('');
// 設定檔裡有但快取沒有的模型(手填過的)也要能顯示
@@ -326,9 +331,9 @@ function populateModelSelects(selModel, selEffort, selTier) {
const wanted = selModel || modelDefaults.model;
mSel.value = wanted;
if (mSel.value !== wanted || !mSel.value) mSel.selectedIndex = 0;
syncEffortTier(selEffort, selTier);
syncEffortTier(selEffort);
}
function syncEffortTier(selEffort, selTier) {
function syncEffortTier(selEffort) {
const slug = document.getElementById('f-model').value;
const m = modelsCache.find(x => x.slug === slug);
let efforts = m ? m.efforts.slice() : ['low', 'medium', 'high', 'xhigh', 'max'];
@@ -341,22 +346,6 @@ function syncEffortTier(selEffort, selTier) {
: efforts.includes(modelDefaults.effort) ? modelDefaults.effort
: (m && efforts.includes(m.defaultEffort)) ? m.defaultEffort
: efforts[0];
const tiers = m ? m.tiers.slice() : [];
const tSel = document.getElementById('f-tier');
const prevT = (selTier !== undefined && selTier !== '') ? selTier : tSel.value;
if (!tiers.length) {
tSel.innerHTML = '<option value="">標準</option>';
tSel.value = '';
} else {
// 模型快取只列「額外」的速度層級,補一個標準選項在前面
tSel.innerHTML = '<option value="">標準</option>' +
tiers.map(t => '<option value="' + esc(t.id) + '">' + esc(t.name) + (t.description ? '' + esc(t.description) + '' : '') + '</option>').join('');
tSel.value = tiers.some(t => t.id === prevT) ? prevT
: tiers.some(t => t.id === modelDefaults.tier) ? modelDefaults.tier
: tiers.some(t => t.id === (m && m.defaultTier)) ? m.defaultTier
: '';
}
}
// ---------- Bot 管理 ----------
@@ -393,8 +382,8 @@ function renderBots(bots) {
'<td>' + statusBadge(b.status) + '</td>' +
'<td><span class="mono" title="' + esc(b.workDir) + '">' + esc(shortPath(b.workDir)) + '</span></td>' +
'<td class="mono">' + esc(b.sandbox || '-') +
((b.model || b.reasoningEffort || b.serviceTier)
? '<br><span class="script-name">' + esc([b.model, b.reasoningEffort, b.serviceTier].filter(Boolean).join(' / ')) + '</span>'
((b.model || b.reasoningEffort || b.sharedSession)
? '<br><span class="script-name">' + esc([b.model, b.reasoningEffort, b.sharedSession ? '共用會話' : ''].filter(Boolean).join(' / ')) + '</span>'
: '') + '</td>' +
'<td class="mono">' + esc(b.tokenMasked || '-') + '</td>' +
'<td>' + fmtBytes(b.memory) + '</td>' +
@@ -462,7 +451,8 @@ function openCreate() {
document.getElementById('f-token-hint').textContent = '跟 @BotFather 申請;儲存時會自動驗證';
document.getElementById('f-workdir').value = '';
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-autostart-wrap').style.display = '';
document.getElementById('f-autostart').checked = true;
@@ -483,7 +473,8 @@ function openEdit(name) {
document.getElementById('f-token-hint').textContent = '留空表示沿用現有 token';
document.getElementById('f-workdir').value = b.workDir || '';
document.getElementById('f-sandbox').value = b.sandbox || 'workspace-write';
populateModelSelects(b.model || '', b.reasoningEffort || '', b.serviceTier || '');
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-autostart-wrap').style.display = 'none';
document.getElementById('bot-modal').classList.add('show');
@@ -504,7 +495,7 @@ async function saveBot() {
sandbox: document.getElementById('f-sandbox').value,
model: document.getElementById('f-model').value,
reasoningEffort: document.getElementById('f-effort').value,
serviceTier: document.getElementById('f-tier').value,
sharedSession: document.getElementById('f-session').value === 'shared',
timeoutMinutes: document.getElementById('f-timeout').value,
};
try {