Let admins enable AI chat by pasting an OpenRouter key in the admin panel
Key is stored on the persistent volume (0600, never in git), read dynamically per request, maskable status only in admin state, and clearable to fall back to built-in answers. OPENROUTER_API_KEY env var still takes precedence if ever set. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,8 +15,12 @@ const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'changeme';
|
||||
const IS_PROD = process.env.NODE_ENV === 'production';
|
||||
const SESSION_TTL = 8 * 60 * 60 * 1000;
|
||||
const LEVELS = ['Scintilla','Ascensus','Fabrica','Culmen','Apex','Fastigium','Vertex','Corona'];
|
||||
const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY || '';
|
||||
const OPENROUTER_MODEL = process.env.OPENROUTER_MODEL || 'deepseek/deepseek-v4-flash:nitro';
|
||||
const OPENROUTER_KEY_FILE = path.join(DATA_DIR, 'openrouter.key');
|
||||
function getOpenRouterKey() {
|
||||
if (process.env.OPENROUTER_API_KEY) return process.env.OPENROUTER_API_KEY;
|
||||
try { return fs.readFileSync(OPENROUTER_KEY_FILE, 'utf8').trim(); } catch (e) { return ''; }
|
||||
}
|
||||
const chatHits = new Map();
|
||||
function chatRateLimited(ip) {
|
||||
const now = Date.now(), rec = chatHits.get(ip);
|
||||
@@ -46,7 +50,8 @@ RULES:
|
||||
async function handleChat(req, res) {
|
||||
const ip = String(req.headers['x-forwarded-for']||req.socket.remoteAddress||'').split(',')[0].trim();
|
||||
if (chatRateLimited(ip)) return json(res, 429, { error: 'Too many messages — give it a minute.' });
|
||||
if (!OPENROUTER_API_KEY) return json(res, 200, { fallback: true });
|
||||
const apiKey = getOpenRouterKey();
|
||||
if (!apiKey) return json(res, 200, { fallback: true });
|
||||
const b = await bodyJson(req).catch(()=>null);
|
||||
if (!b || !Array.isArray(b.messages)) return json(res, 400, { error: 'Invalid request' });
|
||||
const msgs = b.messages.slice(-8)
|
||||
@@ -57,7 +62,7 @@ async function handleChat(req, res) {
|
||||
const ctrl = new AbortController(); const timer = setTimeout(()=>ctrl.abort(), 20000);
|
||||
const r = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
||||
method: 'POST', signal: ctrl.signal,
|
||||
headers: { 'Authorization': `Bearer ${OPENROUTER_API_KEY}`, 'Content-Type': 'application/json', 'HTTP-Referer': 'https://rmcircle.saasy.top', 'X-Title': 'RM Circle Team Help' },
|
||||
headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', 'HTTP-Referer': 'https://rmcircle.saasy.top', 'X-Title': 'RM Circle Team Help' },
|
||||
body: JSON.stringify({ model: OPENROUTER_MODEL, max_tokens: 350, temperature: 0.3, messages: [{ role: 'system', content: chatSystemPrompt() }, ...msgs] })
|
||||
});
|
||||
clearTimeout(timer);
|
||||
@@ -170,7 +175,15 @@ async function handleApi(req,res,pathname){
|
||||
const s=getSession(req);if(s)sessions.delete(s.token);return json(res,200,{ok:true},{'Set-Cookie':'ctb.sid=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0'});
|
||||
}
|
||||
if(pathname.startsWith('/api/admin/')&&!requireAdmin(req,res))return;
|
||||
if(req.method==='GET'&&pathname==='/api/admin/state')return json(res,200,{sponsors:getSponsors(),config:getConfig(),analytics:getAnalytics()});
|
||||
if(req.method==='GET'&&pathname==='/api/admin/state')return json(res,200,{sponsors:getSponsors(),config:getConfig(),analytics:getAnalytics(),aiChat:{configured:!!getOpenRouterKey(),model:OPENROUTER_MODEL}});
|
||||
if(req.method==='POST'&&pathname==='/api/admin/openrouter-key'){
|
||||
const b=await bodyJson(req);const key=typeof b.key==='string'?b.key.trim():null;
|
||||
if(key===null)return json(res,400,{error:'Invalid request.'});
|
||||
if(key===''){try{fs.unlinkSync(OPENROUTER_KEY_FILE)}catch(e){}return json(res,200,{configured:!!getOpenRouterKey()});}
|
||||
if(key.length<20||/\s/.test(key))return json(res,400,{error:'That does not look like a valid API key.'});
|
||||
fs.writeFileSync(OPENROUTER_KEY_FILE,key,{mode:0o600});
|
||||
return json(res,200,{configured:true});
|
||||
}
|
||||
if(req.method==='POST'&&pathname==='/api/admin/sponsors'){
|
||||
const b=await bodyJson(req);const {id,name,parentId='',level='Scintilla',notes=''}=b;if(!id||!name)return json(res,400,{error:'ID and name are required.'});if(!LEVELS.includes(level))return json(res,400,{error:'Invalid level.'});let sponsors=getSponsors();if(sponsors.some(s=>String(s.id)===String(id)))return json(res,409,{error:'That sponsor ID already exists.'});
|
||||
const maxOrder=sponsors.reduce((m,s)=>Math.max(m,s.sortOrder||0),0);sponsors.push({id:String(id).trim(),name:String(name).trim(),parentId:String(parentId||'').trim(),directs:0,level,status:sponsors.some(s=>s.status==='active')?'waiting':'active',sortOrder:maxOrder+10,clicks:0,notes:String(notes||'').trim()});sponsors=normalizeStatuses(sponsors);saveSponsors(sponsors);return json(res,201,{sponsors});
|
||||
|
||||
Reference in New Issue
Block a user