發佈 0.2.0:改名 telegram-bot,新增 Claude Code 引擎(新增 bot 時可選 Codex / Claude),start 自動從 0.1.x 遷移
This commit is contained in:
+90
-20
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>tgcodex 控制台</title>
|
||||
<title>telegram-bot 控制台</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
@@ -99,12 +99,16 @@
|
||||
.notice-steps { color: #e2e8f0; }
|
||||
.notice-steps b { color: #e2e8f0; background: rgba(88,101,242,0.18); padding: 1px 7px;
|
||||
border-radius: 5px; font-weight: 600; }
|
||||
.tag { display: inline-block; padding: 1px 7px; border-radius: 5px; font-size: 10px; font-weight: 700;
|
||||
margin-left: 6px; vertical-align: middle; letter-spacing: .3px; }
|
||||
.tag-codex { background: rgba(16,163,127,0.18); color: #34d399; }
|
||||
.tag-claude { background: rgba(217,119,87,0.20); color: #f8a583; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1><span>●</span> tgcodex 控制台</h1>
|
||||
<h1><span>●</span> telegram-bot 控制台</h1>
|
||||
<div class="toolbar">
|
||||
<label class="auto"><input type="checkbox" id="auto-refresh" checked> 自動更新(5s)</label>
|
||||
<button class="btn" onclick="refreshActive()">🔄 重新整理</button>
|
||||
@@ -143,7 +147,7 @@
|
||||
</table>
|
||||
</div>
|
||||
<p class="meta" style="margin-top:12px">
|
||||
Bot 設定存於 <code>~/.tgcodex/bots/<名稱>/</code>;啟動後掛在 pm2 上(同名程序)。
|
||||
Bot 設定存於 <code>~/.tgbot/bots/<名稱>/</code>;啟動後掛在 pm2 上(同名程序)。每個 bot 可各自選 Codex 或 Claude 引擎。
|
||||
私訊直接回應;群組中要 @bot 或回覆 bot 的訊息才會回應。
|
||||
</p>
|
||||
</div>
|
||||
@@ -188,36 +192,44 @@
|
||||
<input id="f-name" placeholder="my-project-bot" autocomplete="off">
|
||||
<div class="hint">英數字、點、底線、連字號;也是 pm2 程序名稱,建立後不可改</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>AI 引擎</label>
|
||||
<select id="f-engine" onchange="onEngineChange()">
|
||||
<option value="codex">Codex(OpenAI Codex CLI)</option>
|
||||
<option value="claude">Claude(Claude Code CLI)</option>
|
||||
</select>
|
||||
<div class="hint" id="f-engine-hint"></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Telegram Bot Token <b id="f-token-req">*</b></label>
|
||||
<input id="f-token" placeholder="123456:ABC-DEF..." autocomplete="off">
|
||||
<div class="hint" id="f-token-hint">跟 @BotFather 申請;儲存時會自動驗證</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>工作目錄(Codex 可讀寫)<b>*</b></label>
|
||||
<label>工作目錄(AI 可讀寫)<b>*</b></label>
|
||||
<div style="display:flex;gap:8px">
|
||||
<input id="f-workdir" placeholder="按「瀏覽」選擇資料夾" autocomplete="off" style="flex:1">
|
||||
<button type="button" class="btn" id="f-workdir-btn" onclick="pickFolder()" style="padding:0 14px">📂 瀏覽…</button>
|
||||
</div>
|
||||
<div class="hint">Codex 預設能讀寫這個目錄(workspace-write);選擇視窗會開在這台機器的桌面上</div>
|
||||
<div class="hint">bot 預設能讀寫這個目錄(workspace-write);選擇視窗會開在這台機器的桌面上。<span id="f-workdir-hint"></span></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>沙盒模式</label>
|
||||
<select id="f-sandbox">
|
||||
<option value="workspace-write" selected>workspace-write(可讀寫工作目錄,預設)</option>
|
||||
<option value="read-only">read-only(唯讀)</option>
|
||||
<option value="danger-full-access">danger-full-access(不設限,危險)</option>
|
||||
<option value="workspace-write" id="f-sb-ww" selected>workspace-write(可讀寫工作目錄,預設)</option>
|
||||
<option value="read-only" id="f-sb-ro">read-only(唯讀)</option>
|
||||
<option value="danger-full-access" id="f-sb-full">danger-full-access(不設限,危險)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>模型</label>
|
||||
<select id="f-model" onchange="syncEffortTier()"></select>
|
||||
<div class="hint">清單來自本機 codex 的模型快取</div>
|
||||
<div class="hint" id="f-model-hint">清單來自本機 codex 的模型快取</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>推理強度</label>
|
||||
<select id="f-effort"></select>
|
||||
<div class="hint">速度固定為 Fast(1.5x)</div>
|
||||
<div class="hint" id="f-effort-hint">速度固定為 Fast(1.5x)</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>會話模式</label>
|
||||
@@ -228,7 +240,7 @@
|
||||
</select>
|
||||
<div class="hint">記憶會隨對話變慢(尤其含圖);用量達 80% 時所有模式都會自動開新會話</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="field" id="f-network-wrap">
|
||||
<label class="check" style="margin-top:2px"><input type="checkbox" id="f-network"> 允許 Codex 連網</label>
|
||||
<div class="hint">workspace-write 沙盒預設禁網;要呼叫外部 API(例如生圖服務)才需要打開</div>
|
||||
</div>
|
||||
@@ -305,9 +317,55 @@ function refreshActive() {
|
||||
if (activeTab === 'bots') loadBots(); else loadPM2();
|
||||
}
|
||||
|
||||
// ---------- 模型清單(來自 /api/models,讀 codex 的 models cache) ----------
|
||||
let modelsCache = [];
|
||||
let modelDefaults = { model: '', effort: '', tier: '' }; // codex 全域預設(config.toml)
|
||||
// ---------- 模型清單(來自 /api/models:codex 讀 models cache,claude 用內建清單) ----------
|
||||
let enginesCache = {}; // { codex: {available, models, defaults}, claude: {...} }
|
||||
let defaultEngine = 'codex';
|
||||
const ENGINE_UI = {
|
||||
codex: {
|
||||
hint: '本機 OpenAI Codex CLI(codex exec)。專案規範讀工作目錄的 AGENTS.md',
|
||||
workdir: '規範檔:AGENTS.md',
|
||||
modelHint: '清單來自本機 codex 的模型快取',
|
||||
effortHint: '速度固定為 Fast(1.5x)',
|
||||
sandbox: {
|
||||
'workspace-write': 'workspace-write(可讀寫工作目錄,預設)',
|
||||
'read-only': 'read-only(唯讀)',
|
||||
'danger-full-access': 'danger-full-access(不設限,危險)',
|
||||
},
|
||||
},
|
||||
claude: {
|
||||
hint: '本機 Claude Code CLI(claude -p)。專案規範讀工作目錄的 CLAUDE.md',
|
||||
workdir: '規範檔:CLAUDE.md',
|
||||
modelHint: '別名 opus / sonnet / haiku 永遠指向最新版;也可在編輯時手動改成任何模型 id',
|
||||
effortHint: 'Claude 的 effort 對應 --effort 參數',
|
||||
sandbox: {
|
||||
'workspace-write': 'workspace-write(自動接受檔案編輯;不執行指令,預設)',
|
||||
'read-only': 'read-only(只讀:不改檔、不執行指令)',
|
||||
'danger-full-access': 'danger-full-access(跳過所有權限確認,危險)',
|
||||
},
|
||||
},
|
||||
};
|
||||
function currentEngine() { return document.getElementById('f-engine').value || 'codex'; }
|
||||
function engineData() { return enginesCache[currentEngine()] || { models: [], defaults: {} }; }
|
||||
function engineTag(engine) {
|
||||
const e = engine === 'claude' ? 'claude' : 'codex';
|
||||
return '<span class="tag tag-' + e + '">' + (e === 'claude' ? 'Claude' : 'Codex') + '</span>';
|
||||
}
|
||||
// 引擎切換:改文案、顯示/隱藏引擎專屬選項、重載模型清單
|
||||
function onEngineChange(selModel, selEffort) {
|
||||
const ui = ENGINE_UI[currentEngine()];
|
||||
const avail = enginesCache[currentEngine()] && enginesCache[currentEngine()].available;
|
||||
document.getElementById('f-engine-hint').textContent = ui.hint + (avail === false ? ' ⚠ 這台機器找不到此 CLI,bot 會啟動失敗' : '');
|
||||
document.getElementById('f-workdir-hint').textContent = ui.workdir;
|
||||
document.getElementById('f-model-hint').textContent = ui.modelHint;
|
||||
document.getElementById('f-effort-hint').textContent = ui.effortHint;
|
||||
document.getElementById('f-sb-ww').textContent = ui.sandbox['workspace-write'];
|
||||
document.getElementById('f-sb-ro').textContent = ui.sandbox['read-only'];
|
||||
document.getElementById('f-sb-full').textContent = ui.sandbox['danger-full-access'];
|
||||
document.getElementById('f-network-wrap').style.display = currentEngine() === 'codex' ? '' : 'none';
|
||||
// 換引擎時強度清單重來,不要把上一個引擎選的值帶過去
|
||||
if (!selEffort) document.getElementById('f-effort').innerHTML = '';
|
||||
populateModelSelects(selModel || '', selEffort || '');
|
||||
}
|
||||
const EFFORT_LABELS = {
|
||||
low: 'low(快速、輕度推理)',
|
||||
medium: 'medium(平衡,一般預設)',
|
||||
@@ -319,13 +377,20 @@ const EFFORT_LABELS = {
|
||||
async function loadModels() {
|
||||
try {
|
||||
const d = await fetchJson('/api/models');
|
||||
modelsCache = d.models || [];
|
||||
modelDefaults = d.defaults || modelDefaults;
|
||||
} catch { modelsCache = []; }
|
||||
enginesCache = d.engines || {};
|
||||
defaultEngine = d.defaultEngine || 'codex';
|
||||
// 沒裝的引擎在下拉選單標出來
|
||||
for (const opt of document.getElementById('f-engine').options) {
|
||||
const e = enginesCache[opt.value];
|
||||
if (e && e.available === false) opt.textContent = opt.textContent.replace(/(未安裝)$/, '') + '(未安裝)';
|
||||
}
|
||||
} catch { enginesCache = {}; }
|
||||
}
|
||||
// 下拉沒有「預設」空選項:直接把真實預設值選起來
|
||||
function populateModelSelects(selModel, selEffort) {
|
||||
const mSel = document.getElementById('f-model');
|
||||
const modelsCache = engineData().models || [];
|
||||
const modelDefaults = engineData().defaults || {};
|
||||
let opts = modelsCache.map(m => '<option value="' + esc(m.slug) + '">' + esc(m.label) + '</option>').join('');
|
||||
// 設定檔裡有但快取沒有的模型(手填過的)也要能顯示
|
||||
if (selModel && !modelsCache.some(m => m.slug === selModel)) {
|
||||
@@ -340,6 +405,8 @@ function populateModelSelects(selModel, selEffort) {
|
||||
}
|
||||
function syncEffortTier(selEffort) {
|
||||
const slug = document.getElementById('f-model').value;
|
||||
const modelsCache = engineData().models || [];
|
||||
const modelDefaults = engineData().defaults || {};
|
||||
const m = modelsCache.find(x => x.slug === slug);
|
||||
let efforts = m ? m.efforts.slice() : ['low', 'medium', 'high', 'xhigh', 'max'];
|
||||
if (!efforts.length) efforts = ['medium'];
|
||||
@@ -382,7 +449,7 @@ function renderBots(bots) {
|
||||
const uptime = (b.status === 'online' && b.uptime) ? fmtUptime(Date.now() - b.uptime) : '-';
|
||||
const online = b.status === 'online';
|
||||
return '<tr>' +
|
||||
'<td><strong>' + esc(b.name) + '</strong>' +
|
||||
'<td><strong>' + esc(b.name) + '</strong>' + engineTag(b.engine) +
|
||||
(b.error ? '<br><span style="font-size:11px;color:#ed4245">' + esc(b.error) + '</span>' : '') + '</td>' +
|
||||
'<td>' + statusBadge(b.status) + '</td>' +
|
||||
'<td><span class="mono" title="' + esc(b.workDir) + '">' + esc(shortPath(b.workDir)) + '</span></td>' +
|
||||
@@ -456,7 +523,8 @@ function openCreate() {
|
||||
document.getElementById('f-token-hint').textContent = '跟 @BotFather 申請;儲存時會自動驗證';
|
||||
document.getElementById('f-workdir').value = '';
|
||||
document.getElementById('f-sandbox').value = 'workspace-write';
|
||||
populateModelSelects('', '');
|
||||
document.getElementById('f-engine').value = defaultEngine;
|
||||
onEngineChange('', '');
|
||||
document.getElementById('f-session').value = 'per-chat';
|
||||
document.getElementById('f-network').checked = false;
|
||||
document.getElementById('f-timeout').value = '30';
|
||||
@@ -479,7 +547,8 @@ function openEdit(name) {
|
||||
document.getElementById('f-token-hint').textContent = '留空表示沿用現有 token';
|
||||
document.getElementById('f-workdir').value = b.workDir || '';
|
||||
document.getElementById('f-sandbox').value = b.sandbox || 'workspace-write';
|
||||
populateModelSelects(b.model || '', b.reasoningEffort || '');
|
||||
document.getElementById('f-engine').value = b.engine || 'codex';
|
||||
onEngineChange(b.model || '', b.reasoningEffort || '');
|
||||
document.getElementById('f-session').value = b.sessionMode || 'per-chat';
|
||||
document.getElementById('f-network').checked = !!b.networkAccess;
|
||||
document.getElementById('f-timeout').value = b.timeoutMinutes || 30;
|
||||
@@ -499,6 +568,7 @@ async function saveBot() {
|
||||
name: document.getElementById('f-name').value.trim(),
|
||||
token: document.getElementById('f-token').value.trim(),
|
||||
workDir: document.getElementById('f-workdir').value.trim(),
|
||||
engine: document.getElementById('f-engine').value,
|
||||
sandbox: document.getElementById('f-sandbox').value,
|
||||
model: document.getElementById('f-model').value,
|
||||
reasoningEffort: document.getElementById('f-effort').value,
|
||||
|
||||
+64
-25
@@ -1,10 +1,11 @@
|
||||
'use strict';
|
||||
// tgcodex 控制台:管理 TG bot(新增/編輯/啟停,token 等設定都在網頁填)+ PM2 檢視分頁。
|
||||
// tgbot 控制台:管理 TG bot(新增/編輯/啟停,token 等設定都在網頁填)+ PM2 檢視分頁。
|
||||
// 零依賴(內建 http)。預設只綁 127.0.0.1 —— 這個介面能控制 pm2 與 bot token,不要裸露到外網。
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { jlist, actionByName, startBotOnPm2 } = require('../pm2util');
|
||||
const { jlist, actionByName, startBotOnPm2, TGBOT_HOME } = require('../pm2util');
|
||||
const { listBots, createBot, updateBot, removeBot, getBotConfig } = require('../registry');
|
||||
const { assertBotConfig, ensureStateDirs } = require('../config');
|
||||
|
||||
@@ -94,35 +95,41 @@ async function apiPm2Log(pmId, type, lines) {
|
||||
}
|
||||
|
||||
// ---------- 模型清單 ----------
|
||||
// 直接讀 codex 自己的 models cache(~/.codex/models_cache.json),
|
||||
// 新模型上線後下拉選單自動跟上;讀不到就退回內建清單。
|
||||
const FALLBACK_MODELS = [
|
||||
{ slug: 'gpt-5.6-sol', label: 'GPT-5.6-Sol', efforts: ['low', 'medium', 'high', 'xhigh', 'max', 'ultra'], defaultEffort: 'medium', tiers: [{ id: 'priority', name: 'Fast', description: '1.5x speed' }] },
|
||||
{ slug: 'gpt-5.6-terra', label: 'GPT-5.6-Terra', efforts: ['low', 'medium', 'high', 'xhigh', 'max', 'ultra'], defaultEffort: 'medium', tiers: [{ id: 'priority', name: 'Fast', description: '1.5x speed' }] },
|
||||
{ slug: 'gpt-5.6-luna', label: 'GPT-5.6-Luna', efforts: ['low', 'medium', 'high', 'xhigh', 'max'], defaultEffort: 'medium', tiers: [{ id: 'priority', name: 'Fast', description: '1.5x speed' }] },
|
||||
// codex:直接讀它自己的 models cache(~/.codex/models_cache.json),新模型上線後下拉選單自動跟上;
|
||||
// 讀不到就退回內建清單。
|
||||
// claude:CLI 沒有模型清單可查,用內建清單(別名 opus/sonnet/haiku 永遠指向最新版)。
|
||||
const CODEX_FALLBACK_MODELS = [
|
||||
{ slug: 'gpt-5.6-sol', label: 'GPT-5.6-Sol', efforts: ['low', 'medium', 'high', 'xhigh', 'max', 'ultra'], defaultEffort: 'medium' },
|
||||
{ slug: 'gpt-5.6-terra', label: 'GPT-5.6-Terra', efforts: ['low', 'medium', 'high', 'xhigh', 'max', 'ultra'], defaultEffort: 'medium' },
|
||||
{ slug: 'gpt-5.6-luna', label: 'GPT-5.6-Luna', efforts: ['low', 'medium', 'high', 'xhigh', 'max'], defaultEffort: 'medium' },
|
||||
];
|
||||
|
||||
const CLAUDE_EFFORTS = ['low', 'medium', 'high', 'max'];
|
||||
const CLAUDE_MODELS = [
|
||||
{ slug: 'opus', label: 'Opus(最新版,目前 Opus 5)', efforts: CLAUDE_EFFORTS, defaultEffort: 'high' },
|
||||
{ slug: 'sonnet', label: 'Sonnet(最新版,較快)', efforts: CLAUDE_EFFORTS, defaultEffort: 'high' },
|
||||
{ slug: 'haiku', label: 'Haiku(最快、最省)', efforts: ['low', 'medium', 'high'], defaultEffort: 'medium' },
|
||||
{ slug: 'claude-opus-5', label: 'claude-opus-5(鎖定版本)', efforts: CLAUDE_EFFORTS, defaultEffort: 'high' },
|
||||
{ slug: 'claude-sonnet-5', label: 'claude-sonnet-5(鎖定版本)', efforts: CLAUDE_EFFORTS, defaultEffort: 'high' },
|
||||
];
|
||||
|
||||
// codex 全域預設(~/.codex/config.toml),前端用來當下拉選單的預選值
|
||||
function codexGlobalDefaults() {
|
||||
try {
|
||||
const os = require('os');
|
||||
const toml = fs.readFileSync(path.join(os.homedir(), '.codex', 'config.toml'), 'utf-8');
|
||||
return {
|
||||
model: toml.match(/^\s*model\s*=\s*"([^"]+)"/m)?.[1] || '',
|
||||
effort: toml.match(/^\s*model_reasoning_effort\s*=\s*"([^"]+)"/m)?.[1] || '',
|
||||
tier: toml.match(/^\s*service_tier\s*=\s*"([^"]+)"/m)?.[1] || '',
|
||||
};
|
||||
} catch {
|
||||
return { model: '', effort: '', tier: '' };
|
||||
return { model: '', effort: '' };
|
||||
}
|
||||
}
|
||||
|
||||
function apiModels() {
|
||||
function codexModels() {
|
||||
const defaults = codexGlobalDefaults();
|
||||
try {
|
||||
const os = require('os');
|
||||
const cachePath = path.join(os.homedir(), '.codex', 'models_cache.json');
|
||||
const cache = JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
|
||||
const cache = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.codex', 'models_cache.json'), 'utf-8'));
|
||||
const models = (cache.models || [])
|
||||
.filter((m) => m.visibility !== 'hide' && m.slug)
|
||||
.map((m) => ({
|
||||
@@ -130,12 +137,41 @@ function apiModels() {
|
||||
label: m.display_name || m.slug,
|
||||
efforts: (m.supported_reasoning_levels || []).map((e) => e.effort).filter(Boolean),
|
||||
defaultEffort: m.default_reasoning_level || '',
|
||||
tiers: (m.service_tiers || []).map((t) => ({ id: t.id, name: t.name || t.id, description: t.description || '' })),
|
||||
defaultTier: m.default_service_tier || '',
|
||||
}));
|
||||
if (models.length > 0) return { models, defaults, source: 'codex-cache' };
|
||||
} catch { /* fall through */ }
|
||||
return { models: FALLBACK_MODELS, defaults, source: 'fallback' };
|
||||
return { models: CODEX_FALLBACK_MODELS, defaults, source: 'fallback' };
|
||||
}
|
||||
|
||||
// claude 全域預設(~/.claude/settings.json 的 model;沒有就 opus)
|
||||
function claudeModels() {
|
||||
let model = '';
|
||||
try {
|
||||
const s = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.claude', 'settings.json'), 'utf-8'));
|
||||
if (typeof s.model === 'string') model = s.model;
|
||||
} catch { /* ignore */ }
|
||||
return { models: CLAUDE_MODELS, defaults: { model: model || 'opus', effort: '' }, source: 'builtin' };
|
||||
}
|
||||
|
||||
// 引擎是否裝了(開控制台時查一次就好,之後不變)
|
||||
let availabilityCache = null;
|
||||
function engineAvailability() {
|
||||
if (!availabilityCache) {
|
||||
const { codexVersion, claudeVersion } = require('../doctor');
|
||||
availabilityCache = { codex: !!codexVersion(), claude: !!claudeVersion() };
|
||||
}
|
||||
return availabilityCache;
|
||||
}
|
||||
|
||||
function apiModels() {
|
||||
const avail = engineAvailability();
|
||||
return {
|
||||
engines: {
|
||||
codex: { available: avail.codex, ...codexModels() },
|
||||
claude: { available: avail.claude, ...claudeModels() },
|
||||
},
|
||||
defaultEngine: avail.codex || !avail.claude ? 'codex' : 'claude',
|
||||
};
|
||||
}
|
||||
|
||||
// ---------- Bot 管理分頁 ----------
|
||||
@@ -259,7 +295,7 @@ function pickFolderNative() {
|
||||
'$o.Location = [System.Drawing.Point]::new($wa.X + [int]($wa.Width/2), $wa.Y + [int]($wa.Height/2))',
|
||||
'$o.Show(); $o.Activate()',
|
||||
'$f = New-Object System.Windows.Forms.FolderBrowserDialog',
|
||||
"$f.Description = '選擇 Codex 工作目錄'",
|
||||
"$f.Description = '選擇 bot 的工作目錄'",
|
||||
'$f.ShowNewFolderButton = $true',
|
||||
'$r = $f.ShowDialog($o)',
|
||||
'$o.Close()',
|
||||
@@ -269,11 +305,11 @@ function pickFolderNative() {
|
||||
settle(child, err ? new Error('無法開啟資料夾選擇視窗:' + err.message) : null, err ? null : (stdout.trim() || null));
|
||||
});
|
||||
} else if (process.platform === 'darwin') {
|
||||
child = execFile('osascript', ['-e', 'POSIX path of (choose folder with prompt "選擇 Codex 工作目錄")'], opts, (err, stdout) => {
|
||||
child = execFile('osascript', ['-e', 'POSIX path of (choose folder with prompt "選擇 bot 的工作目錄")'], opts, (err, stdout) => {
|
||||
settle(child, null, err ? null : (stdout.trim().replace(/\/$/, '') || null)); // 取消會回非零,視同 null
|
||||
});
|
||||
} else {
|
||||
child = execFile('zenity', ['--file-selection', '--directory', '--title=選擇 Codex 工作目錄'], opts, (err, stdout) => {
|
||||
child = execFile('zenity', ['--file-selection', '--directory', '--title=選擇 bot 的工作目錄'], opts, (err, stdout) => {
|
||||
settle(child, null, err ? null : (stdout.trim() || null));
|
||||
});
|
||||
}
|
||||
@@ -311,6 +347,9 @@ function createWebServer({ port = 3799, host = '127.0.0.1' } = {}) {
|
||||
if (url.pathname === '/api/models' && req.method === 'GET') {
|
||||
return json(200, apiModels());
|
||||
}
|
||||
if (url.pathname === '/api/info' && req.method === 'GET') {
|
||||
return json(200, { home: TGBOT_HOME, version: require('../../package.json').version });
|
||||
}
|
||||
if (url.pathname === '/api/pick-folder' && req.method === 'POST') {
|
||||
const picked = await pickFolderNative();
|
||||
return json(200, picked ? { path: picked } : { canceled: true });
|
||||
@@ -360,7 +399,7 @@ function createWebServer({ port = 3799, host = '127.0.0.1' } = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
server.once('error', reject);
|
||||
server.listen(port, host, () => {
|
||||
console.log(`✅ tgcodex 控制台已啟動:http://${host === '0.0.0.0' ? 'localhost' : host}:${port}`);
|
||||
console.log(`✅ telegram-bot 控制台已啟動:http://${host === '0.0.0.0' ? 'localhost' : host}:${port}`);
|
||||
resolve(server);
|
||||
});
|
||||
});
|
||||
@@ -371,12 +410,12 @@ function createWebServer({ port = 3799, host = '127.0.0.1' } = {}) {
|
||||
|
||||
module.exports = { createWebServer };
|
||||
|
||||
// 直接執行:node src/web/server.js(pm2 的 tgcodex-console 跑的就是這個)
|
||||
// 直接執行:node src/web/server.js(pm2 的 tgbot-console 跑的就是這個)
|
||||
if (require.main === module) {
|
||||
const { loadConsoleSettings } = require('../pm2util');
|
||||
const settings = loadConsoleSettings();
|
||||
const port = Number(process.env.TGCODEX_WEB_PORT) || settings.port;
|
||||
const host = process.env.TGCODEX_WEB_HOST || settings.host;
|
||||
const port = Number(process.env.TGBOT_WEB_PORT || process.env.TGCODEX_WEB_PORT) || settings.port;
|
||||
const host = process.env.TGBOT_WEB_HOST || process.env.TGCODEX_WEB_HOST || settings.host;
|
||||
createWebServer({ port, host }).listen().catch((err) => {
|
||||
console.error('啟動失敗:', err.message);
|
||||
process.exit(1);
|
||||
|
||||
Reference in New Issue
Block a user