fix(portable): Gemini keys, Telegram pairing UI, and config durability
Improve first-run and channel setup for non-technical users: detect newer Gemini key formats, pin Node 22.22.3, add Control Panel Telegram approve flow, and keep channels/models when config is rewritten. Persist uclaw wizard state via uclaw-meta.json so restarts skip language/persona prompts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -59,7 +59,9 @@ h1 .lobster { color: #ff6b35; }
|
||||
}
|
||||
.form-group input,
|
||||
input[type="text"],
|
||||
input[type="password"] {
|
||||
input[type="password"],
|
||||
.form-group select,
|
||||
select {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
@@ -74,6 +76,20 @@ input[type="password"] {
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.form-group select,
|
||||
select {
|
||||
padding-right: 36px;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%23888' d='M1 1l5 5 5-5'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 14px center;
|
||||
}
|
||||
.form-group select:focus,
|
||||
select:focus { border-color: #ff6b35; }
|
||||
.form-group select option,
|
||||
select option { background: #222; color: #fff; }
|
||||
input:focus { border-color: #ff6b35; }
|
||||
.input-wrap { position: relative; display: block; width: 100%; min-width: 0; }
|
||||
.input-wrap input { padding-right: 70px; }
|
||||
@@ -114,6 +130,37 @@ input:focus { border-color: #ff6b35; }
|
||||
.btn-secondary:hover { background: #444; }
|
||||
.btn-row { display: flex; gap: 10px; margin-top: 20px; justify-content: flex-end; }
|
||||
|
||||
/* Key verified: model picker + continue on one row */
|
||||
.key-actions {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.key-actions .model-picker {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.key-actions .model-picker label {
|
||||
margin-bottom: 6px;
|
||||
font-size: 0.85em;
|
||||
color: #aaa;
|
||||
}
|
||||
.key-actions .btn-primary {
|
||||
flex-shrink: 0;
|
||||
min-width: 128px;
|
||||
min-height: 46px;
|
||||
padding: 12px 22px;
|
||||
margin-left: auto;
|
||||
}
|
||||
.key-actions.has-model .btn-primary { margin-left: 0; }
|
||||
@media (max-width: 520px) {
|
||||
.key-actions { flex-direction: column; align-items: stretch; }
|
||||
.key-actions .btn-primary { margin-left: 0; width: 100%; }
|
||||
}
|
||||
|
||||
/* Result */
|
||||
.result { text-align: center; padding: 40px 20px; }
|
||||
.result .icon { font-size: 3em; margin-bottom: 12px; }
|
||||
@@ -126,15 +173,29 @@ input:focus { border-color: #ff6b35; }
|
||||
.toast.show { opacity: 1; transform: translateY(0); }
|
||||
.toast.error { background: #f44336; }
|
||||
|
||||
/* Channel form */
|
||||
/* Channel form — open state must scroll (Telegram pairing steps are tall) */
|
||||
.channel-card { transition: all 0.2s; }
|
||||
.channel-card.selected { border-color: #ff6b35; background: rgba(255,107,53,0.06); }
|
||||
.channel-form { padding-top: 0; margin-top: 10px; border-top: 1px solid #333; max-height: 0; overflow: hidden; transition: max-height 0.3s ease, padding 0.3s ease; }
|
||||
.channel-form.open { max-height: 300px; padding-top: 12px; }
|
||||
.channel-form { padding-top: 0; margin-top: 10px; border-top: 1px solid #333; max-height: 0; overflow: hidden; transition: max-height 0.35s ease, padding 0.35s ease; }
|
||||
.channel-form.open {
|
||||
max-height: min(72vh, 560px);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding-top: 12px;
|
||||
padding-bottom: 4px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
.channel-form.open::-webkit-scrollbar { width: 8px; }
|
||||
.channel-form.open::-webkit-scrollbar-thumb { background: #444; border-radius: 4px; }
|
||||
.channel-form.open::-webkit-scrollbar-thumb:hover { background: #555; }
|
||||
/* Telegram needs the full row when expanded */
|
||||
.channel-card.channel-expanded { grid-column: 1 / -1; }
|
||||
.channel-form .form-group { margin-bottom: 10px; }
|
||||
.channel-form label { font-size: 0.82em; color: #aaa; margin-bottom: 4px; }
|
||||
.channel-form input { font-size: 0.88em; padding: 9px 12px; }
|
||||
.channel-form .hint { font-size: 0.75em; margin-top: 4px; }
|
||||
.tg-steps code { background: #2a2a2a; padding: 1px 5px; border-radius: 4px; color: #ffb84d; font-size: 0.95em; }
|
||||
|
||||
/* Status bar */
|
||||
.status-bar {
|
||||
@@ -203,12 +264,11 @@ body[data-tier="standard"] [data-tier-min="expert"] { display: none !important;
|
||||
|
||||
<!-- Appears once the key checks out. Without it, picking a key would
|
||||
also lock the model, and there would be no way to switch later. -->
|
||||
<div class="form-group" id="modelPicker" style="display:none;" data-tier-min="standard">
|
||||
<label data-i18n="key.which_model">Which model?</label>
|
||||
<select id="modelChoice"></select>
|
||||
</div>
|
||||
|
||||
<div class="btn-row">
|
||||
<div class="key-actions" id="keyActions">
|
||||
<div class="form-group model-picker" id="modelPicker" style="display:none;" data-tier-min="standard">
|
||||
<label data-i18n="key.which_model">Which model?</label>
|
||||
<select id="modelChoice"></select>
|
||||
</div>
|
||||
<button class="btn btn-primary" id="continueBtn" onclick="saveConfig()" disabled data-i18n="key.continue">Continue →</button>
|
||||
</div>
|
||||
|
||||
@@ -268,6 +328,16 @@ body[data-tier="standard"] [data-tier-min="expert"] { display: none !important;
|
||||
<input type="text" id="ch-telegram-token" placeholder="123456:ABC-DEF...">
|
||||
<p class="hint"><a href="https://t.me/BotFather" target="_blank" rel="noopener" data-i18n="ch.telegram_get">→ Get one from @BotFather</a></p>
|
||||
</div>
|
||||
<div class="telegram-pairing-panel" id="telegram-pairing-panel" style="margin-top:12px;padding-top:12px;border-top:1px solid #333;">
|
||||
<p class="hint" style="color:#ccc;margin-bottom:8px;" data-i18n="tg.steps_title">After saving, connect your Telegram account:</p>
|
||||
<ol class="hint tg-steps" style="color:#aaa;font-size:0.82em;margin:0 0 12px 18px;line-height:1.55;">
|
||||
<li data-i18n="tg.step1">Save the bot token above (click “Open dashboard” or save channels).</li>
|
||||
<li data-i18n="tg.step2">On your phone, open Telegram, find your bot, and send <code>/start</code>.</li>
|
||||
<li data-i18n="tg.step3">Come back here — a button will appear. Click it once. No command line.</li>
|
||||
</ol>
|
||||
<div id="telegram-pairing-status" class="hint" style="margin-bottom:10px;color:#888;"></div>
|
||||
<button type="button" class="btn btn-primary" id="telegram-approve-btn" style="display:none;width:100%;margin-bottom:4px;" onclick="approveTelegramPairing()" data-i18n="tg.approve">Allow this Telegram account</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="model-card channel-card" onclick="toggleChannel(this, 'slack')">
|
||||
@@ -495,11 +565,16 @@ async function checkKey() {
|
||||
|
||||
const modelPicker = document.getElementById('modelPicker');
|
||||
const modelChoice = document.getElementById('modelChoice');
|
||||
const keyActions = document.getElementById('keyActions');
|
||||
|
||||
function populateModels(provider, current) {
|
||||
var options = (provider && provider.models) ? provider.models.slice() : [];
|
||||
if (current && options.indexOf(current) === -1) options.unshift(current);
|
||||
if (options.length < 2) { modelPicker.style.display = 'none'; return; }
|
||||
if (options.length < 2) {
|
||||
modelPicker.style.display = 'none';
|
||||
keyActions.classList.remove('has-model');
|
||||
return;
|
||||
}
|
||||
modelChoice.innerHTML = '';
|
||||
options.forEach(function (name) {
|
||||
var opt = document.createElement('option');
|
||||
@@ -509,6 +584,7 @@ function populateModels(provider, current) {
|
||||
modelChoice.appendChild(opt);
|
||||
});
|
||||
modelPicker.style.display = 'block';
|
||||
keyActions.classList.add('has-model');
|
||||
}
|
||||
|
||||
// Check as they paste, but wait for typing to settle first.
|
||||
@@ -581,6 +657,14 @@ async function saveConfig() {
|
||||
var config = buildConfig(detected.id, detected.baseUrl, model, apiKey);
|
||||
|
||||
try {
|
||||
var existingRes = await fetch('/api/config');
|
||||
var existing = existingRes.ok ? await existingRes.json() : {};
|
||||
delete existing.agent;
|
||||
config = Object.assign(existing, config);
|
||||
if (existing.channels) config.channels = existing.channels;
|
||||
if (existing.commands) {
|
||||
config.commands = Object.assign({}, existing.commands, config.commands || {});
|
||||
}
|
||||
var res = await fetch('/api/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -601,11 +685,93 @@ function toggleChannel(card, type) {
|
||||
if (isOpen) {
|
||||
form.classList.remove('open');
|
||||
card.classList.remove('selected');
|
||||
card.classList.remove('channel-expanded');
|
||||
if (type === 'telegram') stopTelegramPairingPoll();
|
||||
} else {
|
||||
form.classList.add('open');
|
||||
card.classList.add('selected');
|
||||
if (type === 'telegram') card.classList.add('channel-expanded');
|
||||
var firstInput = form.querySelector('input');
|
||||
if (firstInput) setTimeout(function() { firstInput.focus(); }, 100);
|
||||
if (type === 'telegram') startTelegramPairingPoll();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Telegram pairing (Control Panel, no CLI) ---
|
||||
var telegramPairingTimer = null;
|
||||
var telegramPendingCode = null;
|
||||
|
||||
function stopTelegramPairingPoll() {
|
||||
if (telegramPairingTimer) {
|
||||
clearTimeout(telegramPairingTimer);
|
||||
telegramPairingTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function formatTelegramRequestLabel(req) {
|
||||
if (req.username) return '@' + req.username;
|
||||
if (req.firstName) return req.firstName;
|
||||
return req.id || '';
|
||||
}
|
||||
|
||||
async function pollTelegramPairing() {
|
||||
var statusEl = document.getElementById('telegram-pairing-status');
|
||||
var btn = document.getElementById('telegram-approve-btn');
|
||||
if (!statusEl || !btn) return;
|
||||
|
||||
try {
|
||||
var res = await fetch('/api/telegram/pairing');
|
||||
var data = res.ok ? await res.json() : { requests: [] };
|
||||
var requests = data.requests || [];
|
||||
|
||||
if (requests.length > 0) {
|
||||
var req = requests[0];
|
||||
telegramPendingCode = req.code;
|
||||
var who = formatTelegramRequestLabel(req);
|
||||
statusEl.innerHTML = '<span style="color:#ffb84d;">' + t('tg.pending', { who: who || req.id }) + '</span>';
|
||||
btn.style.display = 'block';
|
||||
btn.disabled = false;
|
||||
} else {
|
||||
telegramPendingCode = null;
|
||||
btn.style.display = 'none';
|
||||
btn.disabled = true;
|
||||
statusEl.textContent = t('tg.waiting');
|
||||
}
|
||||
} catch (e) {
|
||||
statusEl.textContent = t('tg.poll_failed');
|
||||
}
|
||||
|
||||
telegramPairingTimer = setTimeout(pollTelegramPairing, 2000);
|
||||
}
|
||||
|
||||
function startTelegramPairingPoll() {
|
||||
stopTelegramPairingPoll();
|
||||
pollTelegramPairing();
|
||||
}
|
||||
|
||||
async function approveTelegramPairing() {
|
||||
if (!telegramPendingCode) return;
|
||||
var btn = document.getElementById('telegram-approve-btn');
|
||||
var statusEl = document.getElementById('telegram-pairing-status');
|
||||
btn.disabled = true;
|
||||
statusEl.textContent = t('tg.approving');
|
||||
|
||||
try {
|
||||
var res = await fetch('/api/telegram/pairing/approve', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ code: telegramPendingCode })
|
||||
});
|
||||
var data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || String(res.status));
|
||||
telegramPendingCode = null;
|
||||
btn.style.display = 'none';
|
||||
statusEl.innerHTML = '<span style="color:#4caf50;font-weight:600;">' + t('tg.approved') + '</span>';
|
||||
showToast(t('tg.approved_toast'));
|
||||
} catch (e) {
|
||||
statusEl.textContent = t('tg.approve_failed', { reason: e.message });
|
||||
btn.disabled = false;
|
||||
showToast(t('tg.approve_failed', { reason: e.message }), true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -748,6 +914,7 @@ async function saveChannelsAndOpen() {
|
||||
body: JSON.stringify(existing)
|
||||
});
|
||||
showToast(t('ch.saved'));
|
||||
if (channels.telegram) startTelegramPairingPoll();
|
||||
} catch (e) {
|
||||
showToast(t('ch.err_save'), true);
|
||||
}
|
||||
@@ -865,6 +1032,11 @@ async function loadConfig() {
|
||||
if (modelChoice.options[i].value === model) { modelChoice.selectedIndex = i; break; }
|
||||
}
|
||||
}
|
||||
|
||||
if (cfg.channels && cfg.channels.telegram && cfg.channels.telegram.botToken) {
|
||||
var tgInput = document.getElementById('ch-telegram-token');
|
||||
if (tgInput) tgInput.value = cfg.channels.telegram.botToken;
|
||||
}
|
||||
} catch (e) { /* first run: nothing configured yet */ }
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,144 @@
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execFileSync } = require('child_process');
|
||||
const { deflateSync } = require('zlib');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const PORT_RANGE_START = 18788;
|
||||
const PORT_RANGE_END = 18798;
|
||||
const CONFIG_PATH = path.join(__dirname, '../data/.openclaw/openclaw.json');
|
||||
const RUNTIME_PATH = path.join(__dirname, '../data/.openclaw/runtime.json');
|
||||
const DATA_DIR = path.join(__dirname, '../data');
|
||||
const CONFIG_PATH = path.join(DATA_DIR, '.openclaw/openclaw.json');
|
||||
const RUNTIME_PATH = path.join(DATA_DIR, '.openclaw/runtime.json');
|
||||
const UCLAW_META_PATH = path.join(DATA_DIR, '.openclaw/uclaw-meta.json');
|
||||
|
||||
function getStateDir() {
|
||||
return process.env.OPENCLAW_STATE_DIR || path.join(DATA_DIR, '.openclaw');
|
||||
}
|
||||
|
||||
function resolveNodeBinary() {
|
||||
const runtimeRoot = path.join(__dirname, '../app/runtime');
|
||||
if (process.platform === 'win32') {
|
||||
return path.join(runtimeRoot, 'node-win-x64/node.exe');
|
||||
}
|
||||
if (process.platform === 'darwin') {
|
||||
const dir = process.arch === 'arm64' ? 'node-mac-arm64' : 'node-mac-x64';
|
||||
return path.join(runtimeRoot, dir, 'bin/node');
|
||||
}
|
||||
const dir = process.arch === 'arm64' ? 'node-linux-arm64' : 'node-linux-x64';
|
||||
return path.join(runtimeRoot, dir, 'bin/node');
|
||||
}
|
||||
|
||||
const OPENCLAW_ENTRY = path.join(__dirname, '../app/core/node_modules/openclaw/openclaw.mjs');
|
||||
|
||||
function openclawEnv() {
|
||||
return {
|
||||
...process.env,
|
||||
OPENCLAW_HOME: DATA_DIR,
|
||||
OPENCLAW_STATE_DIR: getStateDir(),
|
||||
OPENCLAW_CONFIG_PATH: CONFIG_PATH,
|
||||
};
|
||||
}
|
||||
|
||||
function readConfigFile() {
|
||||
if (!fs.existsSync(CONFIG_PATH)) return {};
|
||||
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
||||
}
|
||||
|
||||
function extractUclawMeta(config) {
|
||||
if (!config.uclaw) return config;
|
||||
const uclaw = config.uclaw;
|
||||
delete config.uclaw;
|
||||
try {
|
||||
const existing = fs.existsSync(UCLAW_META_PATH)
|
||||
? JSON.parse(fs.readFileSync(UCLAW_META_PATH, 'utf8'))
|
||||
: {};
|
||||
fs.writeFileSync(UCLAW_META_PATH, `${JSON.stringify({ ...existing, ...uclaw }, null, 2)}\n`);
|
||||
} catch (err) {
|
||||
console.error('[uclaw-meta] write failed:', err.message);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
function mergeConfigPreserve(existing, incoming) {
|
||||
const next = { ...incoming };
|
||||
if (existing.channels) {
|
||||
next.channels = { ...existing.channels, ...(next.channels || {}) };
|
||||
}
|
||||
if (existing.commands?.ownerAllowFrom?.length) {
|
||||
next.commands = {
|
||||
...(existing.commands || {}),
|
||||
...(next.commands || {}),
|
||||
ownerAllowFrom: next.commands?.ownerAllowFrom?.length
|
||||
? next.commands.ownerAllowFrom
|
||||
: existing.commands.ownerAllowFrom,
|
||||
};
|
||||
}
|
||||
if (existing.plugins && !next.plugins) next.plugins = existing.plugins;
|
||||
return next;
|
||||
}
|
||||
|
||||
function writeConfigFile(config) {
|
||||
const existing = readConfigFile();
|
||||
let next = mergeConfigPreserve(existing, config);
|
||||
next = extractUclawMeta(next);
|
||||
delete next.agent;
|
||||
const dir = path.dirname(CONFIG_PATH);
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(CONFIG_PATH, `${JSON.stringify(next, null, 2)}\n`);
|
||||
return next;
|
||||
}
|
||||
|
||||
function runOpenclawCli(args) {
|
||||
const node = resolveNodeBinary();
|
||||
if (!fs.existsSync(node) || !fs.existsSync(OPENCLAW_ENTRY)) {
|
||||
throw new Error('OpenClaw runtime not found on this drive');
|
||||
}
|
||||
return execFileSync(node, [OPENCLAW_ENTRY, ...args], {
|
||||
env: openclawEnv(),
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
}).trim();
|
||||
}
|
||||
|
||||
function telegramPairingPath() {
|
||||
return path.join(getStateDir(), 'credentials', 'telegram-pairing.json');
|
||||
}
|
||||
|
||||
function readTelegramPairingRequests() {
|
||||
const filePath = telegramPairingPath();
|
||||
if (!fs.existsSync(filePath)) return [];
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
const requests = Array.isArray(parsed.requests) ? parsed.requests : [];
|
||||
const now = Date.now();
|
||||
return requests.filter((req) => {
|
||||
const created = Date.parse(req.createdAt || '');
|
||||
return Number.isFinite(created) && now - created < 3600 * 1000;
|
||||
}).map((req) => ({
|
||||
code: req.code,
|
||||
id: req.id,
|
||||
createdAt: req.createdAt,
|
||||
username: req.meta?.username || req.meta?.name || '',
|
||||
firstName: req.meta?.firstName || '',
|
||||
}));
|
||||
}
|
||||
|
||||
function approveTelegramPairing(code) {
|
||||
const snapshot = readConfigFile();
|
||||
const channels = snapshot.channels ? { ...snapshot.channels } : null;
|
||||
const out = runOpenclawCli(['pairing', 'approve', 'telegram', code, '--notify']);
|
||||
const after = readConfigFile();
|
||||
if (channels) {
|
||||
after.channels = { ...channels, ...(after.channels || {}) };
|
||||
writeConfigFile(after);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ── WeChat Login State ──────────────────────────────────────────────────────
|
||||
const DEFAULT_WECHAT_BASE_URL = 'https://ilinkai.weixin.qq.com';
|
||||
@@ -493,7 +624,7 @@ const server = http.createServer((req, res) => {
|
||||
const apiKey = String(parsed.apiKey || '').trim();
|
||||
if (!apiKey) return reply({ ok: false, reason: 'key.err_empty' });
|
||||
|
||||
const { detectProvider, classifyFailure } = await import('../lib/provider-detect.mjs');
|
||||
const { detectProvider, classifyFailure, modelsToTry, discoverGoogleOpenAiModels } = await import('../lib/provider-detect.mjs');
|
||||
const detected = detectProvider(apiKey);
|
||||
const baseUrl = String(parsed.baseUrl || detected?.baseUrl || '').replace(/\/+$/, '');
|
||||
const model = String(parsed.model || detected?.model || '');
|
||||
@@ -505,7 +636,9 @@ const server = http.createServer((req, res) => {
|
||||
const startedAt = Date.now();
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), 15000);
|
||||
try {
|
||||
const candidates = modelsToTry(detected, model);
|
||||
let lastStatus = null;
|
||||
const tryChat = async (tryModel) => {
|
||||
const response = await fetch(`${baseUrl}/chat/completions`, {
|
||||
method: 'POST',
|
||||
signal: controller.signal,
|
||||
@@ -516,12 +649,39 @@ const server = http.createServer((req, res) => {
|
||||
'x-api-key': apiKey,
|
||||
'anthropic-version': '2023-06-01',
|
||||
},
|
||||
body: JSON.stringify({ model, max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),
|
||||
body: JSON.stringify({ model: tryModel, max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
return reply({ ok: false, provider: detected, status: response.status, reason: classifyFailure({ status: response.status }) });
|
||||
return response;
|
||||
};
|
||||
try {
|
||||
for (const tryModel of candidates) {
|
||||
const response = await tryChat(tryModel);
|
||||
if (response.ok) {
|
||||
return reply({ ok: true, provider: detected, baseUrl, model: tryModel, latencyMs: Date.now() - startedAt });
|
||||
}
|
||||
lastStatus = response.status;
|
||||
// Only try the next Gemini model when this account cannot see the first one.
|
||||
if (!(detected?.id === 'google' && response.status === 404)) {
|
||||
return reply({ ok: false, provider: detected, status: response.status, reason: classifyFailure({ status: response.status }) });
|
||||
}
|
||||
}
|
||||
return reply({ ok: true, provider: detected, baseUrl, model, latencyMs: Date.now() - startedAt });
|
||||
if (detected?.id === 'google' && lastStatus === 404) {
|
||||
const discovered = await discoverGoogleOpenAiModels(baseUrl, apiKey);
|
||||
const extra = discovered.filter((id) => !candidates.includes(id));
|
||||
const rank = (id) => (id.includes('flash') ? 0 : 10) + (id.includes('2.0') ? 0 : 5) + (id.includes('lite') ? 1 : 0);
|
||||
extra.sort((a, b) => rank(a) - rank(b));
|
||||
for (const tryModel of extra) {
|
||||
const response = await tryChat(tryModel);
|
||||
if (response.ok) {
|
||||
return reply({ ok: true, provider: detected, baseUrl, model: tryModel, latencyMs: Date.now() - startedAt });
|
||||
}
|
||||
lastStatus = response.status;
|
||||
if (response.status !== 404) {
|
||||
return reply({ ok: false, provider: detected, status: response.status, reason: classifyFailure({ status: response.status }) });
|
||||
}
|
||||
}
|
||||
}
|
||||
return reply({ ok: false, provider: detected, status: lastStatus, reason: classifyFailure({ status: lastStatus }) });
|
||||
} catch (err) {
|
||||
return reply({ ok: false, provider: detected, reason: classifyFailure({ code: err?.cause?.code || err?.code || err?.name }) });
|
||||
} finally {
|
||||
@@ -624,18 +784,49 @@ const server = http.createServer((req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// API: Telegram pairing — list pending DM approvals (no CLI for the user)
|
||||
if (req.url === '/api/telegram/pairing' && req.method === 'GET') {
|
||||
try {
|
||||
const requests = readTelegramPairingRequests();
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ requests }));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: err.message }));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// API: Telegram pairing — one-click approve from the Control Panel
|
||||
if (req.url === '/api/telegram/pairing/approve' && req.method === 'POST') {
|
||||
let body = '';
|
||||
req.on('data', chunk => { body += chunk; });
|
||||
req.on('end', () => {
|
||||
try {
|
||||
const { code } = JSON.parse(body || '{}');
|
||||
if (!code || typeof code !== 'string') {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Missing pairing code' }));
|
||||
return;
|
||||
}
|
||||
const message = approveTelegramPairing(code.trim().toUpperCase());
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: true, message }));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: err.message }));
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// API: Save config
|
||||
if (req.url === '/api/config' && req.method === 'POST') {
|
||||
let body = '';
|
||||
req.on('data', chunk => body += chunk);
|
||||
req.on('end', () => {
|
||||
try {
|
||||
const config = JSON.parse(body);
|
||||
// 清除旧版废弃键,防止 OpenClaw 报 "agent.* was moved" 错误
|
||||
delete config.agent;
|
||||
const dir = path.dirname(CONFIG_PATH);
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
||||
writeConfigFile(JSON.parse(body));
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: true }));
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user