/* ZHILA · local server serves the instrument and runs the choir: a model with tools that act in the page. */ 'use strict'; const http = require('http'); const fs = require('fs'); const path = require('path'); const { SYSTEM, TOOLS } = require('./agent-tools'); const ROOT = __dirname; const PORT = Number(process.env.PORT || 4173); const HOST = '127.0.0.1'; const MODEL = process.env.ZHILA_MODEL || 'deepseek-chat'; const BASE = process.env.ZHILA_BASE_URL || 'https://api.deepseek.com'; const KEY = process.env.DEEPSEEK_API_KEY || ''; const MIME = { '.html': 'text/html; charset=utf-8', '.css': 'text/css; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.json': 'application/json; charset=utf-8', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.svg': 'image/svg+xml', '.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.woff2': 'font/woff2', '.ico': 'image/x-icon', '.md': 'text/plain; charset=utf-8' }; function send(res, code, type, body) { res.writeHead(code, { 'Content-Type': type, 'Cache-Control': 'no-store', 'Access-Control-Allow-Headers': 'content-type', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS' }); res.end(body); } function json(res, code, obj) { send(res, code, 'application/json; charset=utf-8', JSON.stringify(obj)); } function serveStatic(req, res, pathname) { let rel = pathname === '/' ? '/index.html' : pathname; try { rel = decodeURIComponent(rel); } catch (e) { return send(res, 400, 'text/plain', 'bad path'); } const file = path.resolve(ROOT, '.' + rel); if (!file.startsWith(ROOT + path.sep) || file === ROOT) return send(res, 403, 'text/plain', 'forbidden'); if (!MIME[path.extname(file).toLowerCase()]) return send(res, 404, 'text/plain', 'not found'); fs.readFile(file, function (err, data) { if (err) return send(res, 404, 'text/plain', 'not found'); send(res, 200, MIME[path.extname(file).toLowerCase()], data); }); } let cachedKey = KEY; /* The key may be exported to the launching environment; otherwise the start script injects it. */ if (!cachedKey) { console.warn('[zhila] no DEEPSEEK_API_KEY — the agent will run in local mode'); } function normaliseMessages(input) { if (!Array.isArray(input)) throw new Error('messages must be an array'); if (input.length > 80) throw new Error('conversation too long'); let chars = 0; return input.map(function (m) { if (!m || typeof m !== 'object') throw new Error('bad message'); const role = m.role; if (!['user', 'assistant', 'tool'].includes(role)) throw new Error('bad role'); const out = { role: role }; if (typeof m.content === 'string') { if (m.content.length > 12000) throw new Error('message too long'); chars += m.content.length; out.content = m.content; } else if (m.content === null) { out.content = null; } else { out.content = ''; } if (role === 'tool') { if (typeof m.tool_call_id !== 'string') throw new Error('tool message without id'); out.tool_call_id = m.tool_call_id; if (m.name) out.name = String(m.name).slice(0, 80); } if (role === 'assistant' && Array.isArray(m.tool_calls) && m.tool_calls.length) { out.tool_calls = m.tool_calls.map(function (tc) { if (!tc || tc.type !== 'function' || !tc.function || typeof tc.function.name !== 'string') { throw new Error('bad tool call'); } return { id: String(tc.id || '').slice(0, 80), type: 'function', function: { name: tc.function.name, arguments: typeof tc.function.arguments === 'string' ? tc.function.arguments.slice(0, 8000) : '{}' } }; }); } return out; }).filter(function (m) { return m.content && m.content.length || m.tool_calls && m.tool_calls.length || m.role === 'tool'; }).map(function (m) { if (chars > 200000) throw new Error('conversation too large'); return m; }); } async function callModel(messages) { if (!cachedKey) { const err = new Error('no API key'); err.code = 'NO_KEY'; throw err; } const body = { model: MODEL, messages: [{ role: 'system', content: SYSTEM }].concat(messages), tools: TOOLS, tool_choice: 'auto', temperature: 0.75, max_tokens: 900, stream: false }; const res = await fetch(BASE.replace(/\/+$/, '') + '/chat/completions', { method: 'POST', headers: { 'Authorization': 'Bearer ' + cachedKey, 'Content-Type': 'application/json' }, body: JSON.stringify(body), signal: AbortSignal.timeout(90000) }); const text = await res.text(); let data; try { data = JSON.parse(text); } catch (e) { throw new Error('model returned non-JSON (' + res.status + ')'); } if (!res.ok) { const msg = data && data.error && data.error.message ? data.error.message : 'model error ' + res.status; throw new Error(msg); } const choice = data.choices && data.choices[0]; if (!choice || !choice.message) throw new Error('model returned no message'); const m = choice.message; return { role: 'assistant', content: typeof m.content === 'string' ? m.content : '', tool_calls: Array.isArray(m.tool_calls) && m.tool_calls.length ? m.tool_calls : null }; } function readBody(req) { return new Promise(function (resolve, reject) { let size = 0; const chunks = []; req.on('data', function (c) { size += c.length; if (size > 400000) { reject(new Error('body too large')); req.destroy(); return; } chunks.push(c); }); req.on('end', function () { try { resolve(JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}')); } catch (e) { reject(new Error('bad JSON')); } }); req.on('error', reject); }); } function originAllowed(req) { const o = req.headers.origin; if (!o) return null; if (o === 'null') return 'null'; if (/^http:\/\/(127\.0\.0\.1|localhost)(:\d+)?$/.test(o)) return o; return false; } const server = http.createServer(async function (req, res) { const url = new URL(req.url, 'http://' + req.headers.host); const origin = originAllowed(req); if (origin) { res.setHeader('Access-Control-Allow-Origin', origin); res.setHeader('Vary', 'Origin'); } if (req.method === 'OPTIONS') return send(res, 204, 'text/plain', ''); if (url.pathname.startsWith('/api/') && origin === false) { return json(res, 403, { error: 'origin not allowed' }); } if (url.pathname === '/api/agent' && req.method === 'GET') { return json(res, 200, { live: !!cachedKey, mode: cachedKey ? 'model' : 'local', model: cachedKey ? MODEL : null }); } if (url.pathname === '/api/agent' && req.method === 'POST') { let body; try { body = await readBody(req); } catch (e) { return json(res, 400, { error: e.message }); } let messages; try { messages = normaliseMessages(body.messages); } catch (e) { return json(res, 400, { error: e.message }); } try { const message = await callModel(messages); return json(res, 200, { message: message, model: MODEL }); } catch (e) { if (e.code === 'NO_KEY') { return json(res, 503, { error: 'agent offline: no API key on the server', mode: 'local' }); } console.error('[zhila] model error:', e.message); return json(res, 502, { error: e.message || 'model error' }); } } if (url.pathname.startsWith('/api/')) return json(res, 404, { error: 'no such endpoint' }); if (req.method !== 'GET' && req.method !== 'HEAD') return send(res, 405, 'text/plain', 'method not allowed'); serveStatic(req, res, url.pathname); }); server.listen(PORT, HOST, function () { console.log(''); console.log(' ZHILA · http://' + HOST + ':' + PORT); console.log(' agent · ' + (cachedKey ? MODEL + ' (live)' : 'local conductor (no key)')); console.log(''); });