Files
JianMiauandClaude Fable 5 5b32816ad4 建立 telegram-codex-bot:串接 Codex CLI 的 Telegram bot(npm 包)
摘要:
用 Telegram 操控本機 Codex CLI 的 bot,附 web 控制台,封裝為 npm 包。

內容:
- 零依賴(僅 Node 內建模組);bot 以 codex exec --json 驅動,支援會話延續
  (exec resume)、workspace-write 沙盒、圖片輸入、進度回報與 ctx%/tokens footer
- web 控制台(pm2 託管,預設 127.0.0.1:3799):Bot 管理分頁(新增/編輯/啟停,
  token 自動驗證、工作目錄用原生視窗選、模型/推理強度/速度下拉,清單來自
  ~/.codex/models_cache.json)+ PM2 檢視分頁(狀態/port/log/啟停)
- CLI:start(環境檢查後把控制台掛上 pm2)/ stop / restart / delete / status /
  logs / web / doctor
- Windows 相容:解析 codex.cmd shim 直接以 node 執行、taskkill 整樹砍程序、
  資料夾選擇視窗以 TopMost 透明 owner 置中

影響:
新專案初始版本;bot 設定存於 ~/.tgcodex/bots/<name>/,含明文 token 的實例
設定不進版控(.gitignore 已涵蓋 tgcodex.config.json 與 .tgcodex/)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 16:25:39 +08:00

171 lines
5.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
'use strict';
// telegram-codex-bot CLIstart 把 web 控制台掛上 pm2
// bot 的新增(token、工作目錄、權限)與啟停全部在網頁控制台操作。
const { execSync, spawn } = require('child_process');
const pm2 = require('../src/pm2util');
const HELP = `telegram-codex-bot — 用 Telegram 操控本機 Codex CLI 的 bot
用法:
telegram-codex-bot start 檢查環境(codex/pm2/git)並啟動 web 控制台(掛 pm2)
telegram-codex-bot start --port 3799 指定控制台 port(會記住)
telegram-codex-bot doctor 只做環境檢查
telegram-codex-bot stop 停止控制台(bot 不受影響)
telegram-codex-bot restart 重啟控制台
telegram-codex-bot delete 從 pm2 移除控制台
telegram-codex-bot status 顯示控制台與所有 bot 狀態
telegram-codex-bot logs [--name <bot>] 跟看 log(不帶 --name 看控制台)
telegram-codex-bot web 前景執行控制台(不經 pm2,除錯用)
bot 的新增 / token / 工作目錄 / 允許名單 / 啟停,都在控制台網頁的「🤖 Bot 管理」分頁設定。
`;
function parseArgs(argv) {
const args = { _: [], flags: {} };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a.startsWith('--')) {
const key = a.slice(2);
const next = argv[i + 1];
if (next !== undefined && !next.startsWith('--')) {
args.flags[key] = next;
i++;
} else {
args.flags[key] = true;
}
} else {
args._.push(a);
}
}
return args;
}
function requirePm2() {
try {
execSync('pm2 -v', { stdio: 'ignore', windowsHide: true });
} catch {
console.error('找不到 pm2。請先安裝:npm install -g pm2');
process.exit(1);
}
}
async function cmdStart(args) {
// 環境檢查:codex CLI / 登入 / pm2 / git
if (!args.flags['skip-checks']) {
const { runChecks, printChecks, hasBlocker } = require('../src/doctor');
const checks = runChecks();
printChecks(checks);
console.log('');
if (hasBlocker(checks)) {
console.error('必要工具缺失,請先安裝上面標 ❌ 的項目再啟動。(確定要硬跑可加 --skip-checks');
process.exit(1);
}
}
if (args.flags.port) {
const settings = pm2.loadConsoleSettings();
settings.port = Number(args.flags.port);
if (!Number.isInteger(settings.port) || settings.port < 1 || settings.port > 65535) {
console.error('--port 必須是 1-65535 的整數');
process.exit(1);
}
if (args.flags.host) settings.host = String(args.flags.host);
pm2.saveConsoleSettings(settings);
}
const { url, alreadyRunning } = await pm2.ensureConsoleOnPm2({ restart: !!args.flags.port });
console.log(`✅ 控制台${alreadyRunning ? '已在執行' : '已掛上 pm2'}${pm2.CONSOLE_NAME}`);
console.log('');
console.log(`📊 控制台:${url}`);
console.log(' 在「🤖 Bot 管理」分頁新增 bot(token、工作目錄、權限都在裡面設定)。');
}
async function cmdConsoleAction(action) {
requirePm2();
try {
await pm2.actionByName(action, pm2.CONSOLE_NAME);
console.log(`✅ pm2 ${action} ${pm2.CONSOLE_NAME}`);
} catch (err) {
console.warn(`pm2 ${action} ${pm2.CONSOLE_NAME} 失敗:${err.message}`);
}
}
async function cmdStatus() {
requirePm2();
const { listBots } = require('../src/registry');
const list = await pm2.jlist();
const settings = pm2.loadConsoleSettings();
const consoleProc = list.find((p) => p.name === pm2.CONSOLE_NAME);
console.log(`控制台(${pm2.CONSOLE_NAME}):${consoleProc ? consoleProc.pm2_env?.status : '未啟動'} ${pm2.consoleUrl(settings)}`);
const bots = await listBots();
if (bots.length === 0) {
console.log('還沒有 bot。打開控制台網頁新增。');
return;
}
console.table(bots.map((b) => ({
name: b.name,
status: b.status,
workDir: b.workDir,
sandbox: b.sandbox,
restarts: b.restarts,
})));
}
async function cmdLogs(args) {
requirePm2();
let name = pm2.CONSOLE_NAME;
if (args.flags.name) {
name = String(args.flags.name);
const list = await pm2.jlist();
if (!list.some((p) => p.name === name)) {
console.error(`pm2 上找不到:${name}`);
process.exit(1);
}
}
const extra = args.flags.err ? ' --err' : '';
const child = spawn(`pm2 logs ${JSON.stringify(name)} --lines 50${extra}`, { shell: true, stdio: 'inherit' });
child.on('exit', (code) => process.exit(code ?? 0));
}
async function cmdWeb(args) {
const { createWebServer } = require('../src/web/server');
const settings = pm2.loadConsoleSettings();
const port = args.flags.port ? Number(args.flags.port) : settings.port;
const host = args.flags.host ? String(args.flags.host) : settings.host;
await createWebServer({ port, host }).listen();
}
(async () => {
const args = parseArgs(process.argv.slice(2));
const cmd = args._[0];
switch (cmd) {
case 'start': return cmdStart(args);
case 'doctor': {
const { runChecks, printChecks, hasBlocker } = require('../src/doctor');
const checks = runChecks();
printChecks(checks);
process.exit(hasBlocker(checks) ? 1 : 0);
return;
}
case 'stop': return cmdConsoleAction('stop');
case 'restart': return cmdConsoleAction('restart');
case 'delete': return cmdConsoleAction('delete');
case 'status': return cmdStatus(args);
case 'logs': return cmdLogs(args);
case 'web': return cmdWeb(args);
case 'help':
case undefined:
case '--help':
case '-h':
console.log(HELP);
return;
default:
console.error(`未知指令:${cmd}\n`);
console.log(HELP);
process.exit(1);
}
})().catch((err) => {
console.error('❌', err.message);
process.exit(1);
});