摘要: 1. 交件匣機制:每輪 prompt 附系統規則,Codex 把要給使用者的檔案放進 工作目錄的 .tgcodex-outbox/,回覆後 bot 自動傳到 Telegram(圖片走 sendPhoto、其餘或超過 10MB 走 sendDocument)並清空,一輪上限 10 個檔案。 2. bot 設定新增「允許 Codex 連網」開關(-c sandbox_workspace_write.network_access), 開啟後可用 codex 的 imagegen 技能生圖,已實測產出 PNG 進交件匣。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
117 lines
4.3 KiB
JavaScript
117 lines
4.3 KiB
JavaScript
'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',
|
||
// 會話模式:false = 每個聊天室獨立會話(預設);true = 所有聊天室共用一個會話
|
||
sharedSession: false,
|
||
// 允許 Codex 在沙盒內連網(workspace-write 預設禁網;要呼叫外部 API 如生圖服務才開)
|
||
networkAccess: false,
|
||
// 單次回應逾時(分鐘)
|
||
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;
|
||
}
|
||
// 速度政策固定 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 };
|