發佈 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,
|
||||
|
||||
Reference in New Issue
Block a user