建立 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>
This commit is contained in:
2026-08-04 16:25:39 +08:00
co-authored by Claude Fable 5
commit 5b32816ad4
16 changed files with 2540 additions and 0 deletions
+77
View File
@@ -0,0 +1,77 @@
'use strict';
// 環境檢查:start 前確認 codex CLI、pm2、git 等工具是否就緒。
const { execSync } = require('child_process');
const fs = require('fs');
const os = require('os');
const path = require('path');
function tryExec(cmd) {
try {
return execSync(cmd, { encoding: 'utf-8', windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'], timeout: 15000 }).trim();
} catch {
return null;
}
}
// 回傳 [{ name, ok, required, message }]
function runChecks() {
const checks = [];
const nodeMajor = Number(process.versions.node.split('.')[0]);
checks.push({
name: 'Node.js',
ok: nodeMajor >= 18,
required: true,
message: nodeMajor >= 18 ? process.version : `${process.version}(需要 ≥ 18`,
});
const codexVer = tryExec('codex --version');
checks.push({
name: 'Codex CLI',
ok: !!codexVer,
required: true,
message: codexVer || '找不到 codex,請安裝:npm install -g @openai/codex',
});
if (codexVer) {
const authFile = path.join(os.homedir(), '.codex', 'auth.json');
const loggedIn = fs.existsSync(authFile);
checks.push({
name: 'Codex 登入',
ok: loggedIn,
required: false,
message: loggedIn ? '已登入' : '尚未登入,請先執行:codex loginbot 啟動後才需要)',
});
}
const pm2Ver = tryExec('pm2 -v');
checks.push({
name: 'pm2',
ok: !!pm2Ver,
required: true,
message: pm2Ver ? `v${pm2Ver.split('\n').pop()}` : '找不到 pm2,請安裝:npm install -g pm2',
});
const gitVer = tryExec('git --version');
checks.push({
name: 'git',
ok: !!gitVer,
required: false,
message: gitVer || '找不到 git(非必要:bot 以 --skip-git-repo-check 執行,但 Codex 要做版控操作時會需要)',
});
return checks;
}
function printChecks(checks) {
for (const c of checks) {
const icon = c.ok ? '✅' : c.required ? '❌' : '⚠️';
console.log(`${icon} ${c.name}${c.message}`);
}
}
function hasBlocker(checks) {
return checks.some((c) => c.required && !c.ok);
}
module.exports = { runChecks, printChecks, hasBlocker };