發佈 0.2.0:改名 telegram-bot,新增 Claude Code 引擎(新增 bot 時可選 Codex / Claude),start 自動從 0.1.x 遷移

This commit is contained in:
2026-08-24 11:42:37 +08:00
parent a99152d765
commit f7e94e4bd7
21 changed files with 886 additions and 235 deletions
+36 -23
View File
@@ -1,16 +1,15 @@
'use strict';
// Bot 註冊表:web 控制台管理的 bot 實例清單。
// bot 設定放在 ~/.tgcodex/bots/<name>/tgcodex.config.json。
// pm2 上帶 TGCODEX_CONFIG 但不在註冊表裡的 bot 程序會被自動探索回來
// bot 設定放在 ~/.tgbot/bots/<name>/tgbot.config.json0.1.x 建的是 tgcodex.config.json,沿用)
// pm2 上帶設定檔路徑但不在註冊表裡的 bot 程序會被自動探索回來
//registry.json 遺失時的自我修復)。
const fs = require('fs');
const path = require('path');
const { TGCODEX_HOME, jlist } = require('./pm2util');
const { loadConfig, SANDBOX_MODES } = require('./config');
const { TGBOT_HOME, BOT_SCRIPT, jlist, botConfigPathOf } = require('./pm2util');
const { loadConfig, SANDBOX_MODES, ENGINE_IDS, SESSION_MODES, CONFIG_BASENAME } = require('./config');
const REG_FILE = path.join(TGCODEX_HOME, 'registry.json');
const BOTS_DIR = path.join(TGCODEX_HOME, 'bots');
const BOT_SCRIPT = path.join(__dirname, 'index.js');
const REG_FILE = path.join(TGBOT_HOME, 'registry.json');
const BOTS_DIR = path.join(TGBOT_HOME, 'bots');
const NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,39}$/;
@@ -24,7 +23,7 @@ function loadRegistry() {
}
function saveRegistry(reg) {
fs.mkdirSync(TGCODEX_HOME, { recursive: true });
fs.mkdirSync(TGBOT_HOME, { recursive: true });
fs.writeFileSync(REG_FILE, JSON.stringify(reg, null, 2) + '\n');
}
@@ -33,18 +32,17 @@ function maskToken(token) {
return token.length <= 10 ? '***' : token.slice(0, 6) + '…' + token.slice(-4);
}
// 從 pm2 探索不在註冊表裡的 bot(跑著我們 src/index.js 且帶 TGCODEX_CONFIG 的程序)
// 從 pm2 探索不在註冊表裡的 bot(跑著我們 src/index.js 且帶設定檔路徑的程序)
function discoverFromPm2(reg, list) {
let changed = false;
for (const p of list) {
// pm2 版本不同,自訂 env 可能在 pm2_env.env 或直接攤平在 pm2_env 上
const env = { ...(p.pm2_env || {}), ...(p.pm2_env?.env || {}) };
const cfgPath = botConfigPathOf(p);
if (!cfgPath) continue;
const script = p.pm2_env?.pm_exec_path || '';
if (!env.TGCODEX_CONFIG || typeof env.TGCODEX_CONFIG !== 'string') continue;
if (path.resolve(script) !== path.resolve(BOT_SCRIPT)) continue;
if (reg.bots[p.name]) continue;
if (!fs.existsSync(env.TGCODEX_CONFIG)) continue;
reg.bots[p.name] = { configPath: env.TGCODEX_CONFIG, source: 'external' };
if (!fs.existsSync(cfgPath)) continue;
reg.bots[p.name] = { configPath: cfgPath, source: 'external' };
changed = true;
}
if (changed) saveRegistry(reg);
@@ -74,6 +72,7 @@ async function listBots() {
source: entry.source || 'ui',
configPath: entry.configPath,
error,
engine: cfg?.engine || 'codex',
workDir: cfg?.workDir || null,
sandbox: cfg?.sandbox || null,
model: cfg?.model || '',
@@ -98,7 +97,7 @@ async function listBots() {
return bots;
}
function validateBotInput({ name, token, workDir, sandbox }, { isCreate }) {
function validateBotInput({ name, token, workDir, sandbox, engine }, { isCreate }) {
if (isCreate) {
if (!name || !NAME_RE.test(name)) {
throw new Error('名稱只能是英數字開頭,含英數字、點、底線、連字號,最長 40 字');
@@ -112,10 +111,13 @@ function validateBotInput({ name, token, workDir, sandbox }, { isCreate }) {
if (sandbox !== undefined && !SANDBOX_MODES.includes(sandbox)) {
throw new Error(`sandbox 必須是 ${SANDBOX_MODES.join(' / ')}`);
}
if (engine !== undefined && !ENGINE_IDS.includes(engine)) {
throw new Error(`engine 必須是 ${ENGINE_IDS.join(' / ')}`);
}
}
async function createBot({ name, token, workDir, sandbox, model, reasoningEffort, sessionMode, networkAccess, timeoutMinutes }) {
validateBotInput({ name, token, workDir, sandbox }, { isCreate: true });
async function createBot({ name, token, workDir, engine, sandbox, model, reasoningEffort, sessionMode, networkAccess, timeoutMinutes }) {
validateBotInput({ name, token, workDir, sandbox, engine }, { isCreate: true });
const reg = loadRegistry();
if (reg.bots[name]) throw new Error(`已有同名 bot${name}`);
const list = await jlist().catch(() => []);
@@ -123,16 +125,17 @@ async function createBot({ name, token, workDir, sandbox, model, reasoningEffort
const dir = path.join(BOTS_DIR, name);
fs.mkdirSync(dir, { recursive: true });
const configPath = path.join(dir, 'tgcodex.config.json');
const configPath = path.join(dir, CONFIG_BASENAME);
const config = {
telegramToken: token,
engine: engine || 'codex',
workDir: path.resolve(workDir),
sandbox: sandbox || 'workspace-write',
model: model || '',
reasoningEffort: reasoningEffort || '',
sessionMode: ['per-chat','shared','stateless'].includes(sessionMode) ? sessionMode : 'per-chat',
sessionMode: SESSION_MODES.includes(sessionMode) ? sessionMode : 'per-chat',
networkAccess: !!networkAccess,
serviceTier: 'priority', // 速度固定 Fast 1.5x
serviceTier: 'priority', // codex 速度固定 Fast 1.5x
timeoutMinutes: Number(timeoutMinutes) || 30,
pm2Name: name,
// UI 建的 bot 不各自開 web,統一由控制台管理
@@ -150,26 +153,36 @@ async function updateBot(name, patch) {
const entry = reg.bots[name];
if (!entry) throw new Error(`找不到 bot${name}`);
const raw = JSON.parse(fs.readFileSync(entry.configPath, 'utf-8'));
const before = loadConfig(entry.configPath);
if (patch.workDir !== undefined && patch.workDir !== '') {
validateBotInput({ workDir: patch.workDir }, { isCreate: false });
raw.workDir = path.resolve(patch.workDir);
}
if (patch.token) raw.telegramToken = patch.token; // 空字串 = 不變更
if (patch.engine !== undefined) {
validateBotInput({ engine: patch.engine }, { isCreate: false });
raw.engine = patch.engine;
}
if (patch.sandbox !== undefined) {
validateBotInput({ sandbox: patch.sandbox }, { isCreate: false });
raw.sandbox = patch.sandbox;
}
if (patch.model !== undefined) raw.model = patch.model;
if (patch.reasoningEffort !== undefined) raw.reasoningEffort = patch.reasoningEffort;
if (['per-chat','shared','stateless'].includes(patch.sessionMode)) { raw.sessionMode = patch.sessionMode; delete raw.sharedSession; }
if (SESSION_MODES.includes(patch.sessionMode)) { raw.sessionMode = patch.sessionMode; delete raw.sharedSession; }
if (patch.networkAccess !== undefined) raw.networkAccess = !!patch.networkAccess;
if (patch.timeoutMinutes !== undefined && Number(patch.timeoutMinutes) > 0) {
raw.timeoutMinutes = Number(patch.timeoutMinutes);
}
fs.writeFileSync(entry.configPath, JSON.stringify(raw, null, 2) + '\n');
return loadConfig(entry.configPath);
const after = loadConfig(entry.configPath);
// 換引擎:舊會話 id 對新引擎沒意義,清掉
if (after.engine !== before.engine) {
try { fs.unlinkSync(path.join(after.stateDir, 'sessions.json')); } catch { /* 沒有就算了 */ }
}
return after;
}
function getBotConfig(name) {
@@ -186,7 +199,7 @@ function removeBot(name, { deleteFiles = false } = {}) {
delete reg.bots[name];
saveRegistry(reg);
if (deleteFiles) {
// 只允許刪除放在 ~/.tgcodex/bots/ 底下的(UI 建的);folder 模式的設定檔不動
// 只允許刪除放在 ~/.tgbot/bots/ 底下的(UI 建的);外部設定檔不動
const dir = path.dirname(entry.configPath);
if (dir.startsWith(BOTS_DIR + path.sep)) {
fs.rmSync(dir, { recursive: true, force: true });