'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: '', // 速度固定 Fast(priority = 1.5x);要改只能手動編輯設定檔 serviceTier: 'priority', // 單次回應逾時(分鐘) timeoutMinutes: 30, // pm2 程序名稱(web 檢視工具會叫 -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; } // 速度政策固定 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; 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 };