建立 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:
+109
@@ -0,0 +1,109 @@
|
||||
'use strict';
|
||||
// 設定載入與驗證。優先序:env 變數 > 設定檔 > 預設值。
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const CONFIG_BASENAME = 'tgcodex.config.json';
|
||||
|
||||
const SANDBOX_MODES = ['read-only', 'workspace-write', 'danger-full-access'];
|
||||
|
||||
const DEFAULTS = {
|
||||
// Telegram Bot token(BotFather 取得)
|
||||
telegramToken: '',
|
||||
// Codex 可讀寫的專案目錄(相對路徑以設定檔所在目錄為基準)
|
||||
workDir: '.',
|
||||
// Codex 沙盒模式,預設可讀寫專案目錄
|
||||
sandbox: 'workspace-write',
|
||||
// 指定模型;空字串 = 使用 codex 預設
|
||||
model: '',
|
||||
// 推理強度(low/medium/high/xhigh/max/ultra…依模型而定);空字串 = codex 預設
|
||||
reasoningEffort: '',
|
||||
// 速度(service tier,如 priority = Fast 1.5x);空字串 = codex 預設
|
||||
serviceTier: '',
|
||||
// 單次回應逾時(分鐘)
|
||||
timeoutMinutes: 30,
|
||||
// pm2 程序名稱(web 檢視工具會叫 <pm2Name>-web)
|
||||
pm2Name: 'tgcodex-bot',
|
||||
// 內建 PM2 web 檢視工具
|
||||
web: { enabled: true, port: 3799, host: '127.0.0.1' },
|
||||
// 手動指定 codex 入口(codex.js 或執行檔路徑);空字串 = 自動偵測
|
||||
codexPath: '',
|
||||
};
|
||||
|
||||
function findConfigPath(explicit) {
|
||||
if (explicit) return path.resolve(explicit);
|
||||
if (process.env.TGCODEX_CONFIG) return path.resolve(process.env.TGCODEX_CONFIG);
|
||||
return path.resolve(process.cwd(), CONFIG_BASENAME);
|
||||
}
|
||||
|
||||
function loadConfig(explicitPath) {
|
||||
const file = findConfigPath(explicitPath);
|
||||
let raw = {};
|
||||
if (fs.existsSync(file)) {
|
||||
try {
|
||||
raw = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
||||
} catch (err) {
|
||||
throw new Error(`設定檔 ${file} 不是合法 JSON:${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const cfg = {
|
||||
...DEFAULTS,
|
||||
...raw,
|
||||
web: { ...DEFAULTS.web, ...(raw.web || {}) },
|
||||
};
|
||||
|
||||
if (process.env.TGCODEX_TOKEN) cfg.telegramToken = process.env.TGCODEX_TOKEN;
|
||||
if (process.env.TGCODEX_WORKDIR) cfg.workDir = process.env.TGCODEX_WORKDIR;
|
||||
if (process.env.TGCODEX_SANDBOX) cfg.sandbox = process.env.TGCODEX_SANDBOX;
|
||||
if (process.env.TGCODEX_MODEL) cfg.model = process.env.TGCODEX_MODEL;
|
||||
if (process.env.TGCODEX_WEB_PORT) cfg.web.port = Number(process.env.TGCODEX_WEB_PORT);
|
||||
if (process.env.TGCODEX_WEB_HOST) cfg.web.host = process.env.TGCODEX_WEB_HOST;
|
||||
|
||||
cfg.configPath = file;
|
||||
cfg.configDir = path.dirname(file);
|
||||
cfg.workDir = path.resolve(cfg.configDir, cfg.workDir);
|
||||
cfg.stateDir = path.join(cfg.configDir, '.tgcodex');
|
||||
cfg.logsDir = path.join(cfg.stateDir, 'logs');
|
||||
cfg.tmpDir = path.join(cfg.stateDir, 'tmp');
|
||||
|
||||
if (!SANDBOX_MODES.includes(cfg.sandbox)) {
|
||||
throw new Error(`sandbox 必須是 ${SANDBOX_MODES.join(' / ')},收到:${cfg.sandbox}`);
|
||||
}
|
||||
if (!/^[A-Za-z0-9._-]+$/.test(cfg.pm2Name)) {
|
||||
throw new Error(`pm2Name 只能包含英數字、點、底線、連字號:${cfg.pm2Name}`);
|
||||
}
|
||||
if (cfg.web.enabled) {
|
||||
const port = Number(cfg.web.port);
|
||||
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
||||
throw new Error(`web.port 必須是 1-65535 的整數:${cfg.web.port}`);
|
||||
}
|
||||
cfg.web.port = port;
|
||||
}
|
||||
const minutes = Number(cfg.timeoutMinutes);
|
||||
if (!(minutes > 0)) throw new Error(`timeoutMinutes 必須是正數:${cfg.timeoutMinutes}`);
|
||||
cfg.timeoutMinutes = minutes;
|
||||
|
||||
return cfg;
|
||||
}
|
||||
|
||||
// bot 執行前的額外檢查(web 檢視工具不需要 token,所以拆開)
|
||||
function assertBotConfig(cfg) {
|
||||
if (!cfg.telegramToken) {
|
||||
throw new Error(
|
||||
`缺少 Telegram token。請在 ${cfg.configPath} 設定 telegramToken,` +
|
||||
'或設環境變數 TGCODEX_TOKEN。(先跑 `telegram-codex-bot init` 產生設定檔)'
|
||||
);
|
||||
}
|
||||
if (!fs.existsSync(cfg.workDir)) {
|
||||
throw new Error(`workDir 不存在:${cfg.workDir}`);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureStateDirs(cfg) {
|
||||
for (const dir of [cfg.stateDir, cfg.logsDir, cfg.tmpDir]) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { CONFIG_BASENAME, DEFAULTS, SANDBOX_MODES, loadConfig, assertBotConfig, ensureStateDirs, findConfigPath };
|
||||
Reference in New Issue
Block a user