Mari Membangun Obrolan Web Streaming (Proxy Express + JS Murni)
Dalam tutorial ini Anda akan membuat halaman obrolan kecil dalam sekitar 100 baris. Server Express kecil menyimpan kunci rahasia Anda dan meneruskan permintaan ke Uncensored Chatbot API, sementara antarmuka JavaScript murni membaca balasan yang di-streaming kata per kata. Tanpa framework, tanpa langkah build, dan kunci Anda tidak pernah menyentuh browser.
Diperbarui
Poin utama
- Jangan pernah memanggil API dari kode browser; rute server menjaga kunci tetap privat dan memungkinkan Anda memvalidasi input.
- Proxy hanya perlu meneruskan peristiwa yang dikirim server; halaman mem-parsing baris data hingga [DONE].
- Gunakan textContent saat streaming dan sanitasi sebelum merender markdown.
- Batasi panjang riwayat dan max_tokens di server agar satu pengguna tidak menghabiskan seluruh saldo Anda.
Apa yang sedang Anda bangun, dan mengapa menggunakan proxy
Bayangkan satu halaman dengan kotak teks dan log gulir. Anda mengetik baris, halaman mengirim percakapan ke /api/chat di server Anda sendiri, dan server Anda meneruskannya ke hulu dengan kunci asli yang dilampirkan. Balasan dialirkan kembali, dan setiap fragmen muncul segera setelah tiba. Kami akan memberi bot sedikit kepribadian, seorang penjaga mercusuar bernama Wren, agar demo terasa hidup.
Anda memerlukan Node 18 atau yang lebih baru (untuk fetch bawaan) dan kunci API. Jika Anda belum memilikinya, dapatkan uji coba gratis dari halaman kunci: akun baru mendapatkan $0,50 kredit selama 7 hari, tanpa perlu detail pembayaran. Base URL adalah https://api.uncensoredchatbotapi.com/v1 dan id model adalah uncensored.
Mengapa Anda memerlukan proxy
Sangat menggoda untuk menempelkan kunci ke kode front-end dan memanggil API secara langsung. Jangan lakukan itu. Apa pun yang dikirim ke browser dapat dibaca oleh siapa saja yang membuka alat pengembang, dan kunci yang bocor berarti saldo terkuras. Proxy memperbaiki ini dan memberikan tiga manfaat tambahan.
- Kerahasiaan. Kunci berada di variabel lingkungan di server.
- Kontrol. Anda menentukan prompt sistem, panjang riwayat, dan
max_tokens, sehingga pengguna tidak dapat mengabaikannya. - Tempat untuk aturan. Batas per pengguna, gerbang usia dan kebijakan logging semuanya berada di lapisan ini. panduan keamanan dibangun di atas rute ini.
Langkah 1: siapkan proyek
Buat folder, inisialisasi sebagai proyek modul ES dan instal Express. Yang lainnya sudah dibangun.
mkdir lantern-chat && cd lantern-chat
npm init -y
npm pkg set type=module
npm install express
mkdir publicAnda akan memiliki dua hal: server.js di direktori root dan folder public yang berisi halaman dan skripnya.
Langkah 2: tulis proxy Express
Server melayani file statis dan mengekspos satu rute POST. Bacalah dalam tiga bagian. Pertama, ia membersihkan riwayat yang masuk: hanya peran user dan assistant yang lolos, setiap pesan dipangkas hingga 4.000 karakter, dan hanya 30 giliran terakhir yang disimpan. Kedua, ia menambahkan pesan sistem Anda sendiri, yang tidak dapat diubah browser. Ketiga, ia memanggil endpoint hulu dengan stream: true dan mengalirkan byte langsung kembali.
import express from "express";
const app = express();
app.use(express.json({ limit: "256kb" }));
app.use(express.static("public"));
const UPSTREAM = "https://api.uncensoredchatbotapi.com/v1/chat/completions";
const SYSTEM = "You are Wren, a dry-witted night-shift lighthouse keeper. Stay in character.";
app.post("/api/chat", async (req, res) => {
const history = Array.isArray(req.body.messages) ? req.body.messages : [];
// Keep only well-formed turns and cap the history we forward.
const turns = history
.filter((m) => ["user", "assistant"].includes(m.role) && typeof m.content === "string")
.slice(-30)
.map((m) => ({ role: m.role, content: m.content.slice(0, 4000) }));
if (turns.length === 0) return res.status(400).json({ error: "no messages" });
const upstream = await fetch(UPSTREAM, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "uncensored",
messages: [{ role: "system", content: SYSTEM }, ...turns],
stream: true,
max_tokens: 500,
temperature: 0.8,
}),
});
if (!upstream.ok) {
const info = await upstream.json().catch(() => ({}));
return res.status(upstream.status).json(info);
}
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
for await (const chunk of upstream.body) res.write(chunk);
res.end();
});
app.listen(3000, () => console.log("http://localhost:3000"));Dua pilihan yang layak dijelaskan. Meneruskan byte mentah berarti Anda tidak perlu memahami format peristiwa di server sama sekali. Dan mengembalikan status hulu untuk kegagalan memungkinkan halaman bereaksi secara wajar; misalnya, 402 berarti saldo Anda kosong dan 429 berarti seseorang terlalu cepat.
Langkah 3: halaman dan pembaca streaming
Sekarang antarmuka. Simpan ini sebagai public/index.html; ini sengaja polos agar Anda dapat mengubah gayanya nanti.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Lantern Chat</title>
<style>
body { font: 16px system-ui; max-width: 640px; margin: 2rem auto; padding: 0 1rem; }
#log { min-height: 320px; border: 1px solid #ccc; padding: 1rem; white-space: pre-wrap; }
.me { color: #234; font-weight: 600; }
form { display: flex; gap: .5rem; margin-top: 1rem; }
input { flex: 1; padding: .6rem; }
</style>
</head>
<body>
<h1>Lantern Chat</h1>
<div id="log"></div>
<form id="form">
<input id="text" autocomplete="off" placeholder="Say something...">
<button>Send</button>
</form>
<script src="chat.js"></script>
</body>
</html>Selanjutnya, public/chat.js. Bagian yang menarik adalah loop baca. Chunk jaringan tidak mematuhi batas baris, jadi Anda menyimpan buffer, memisahkan berdasarkan baris baru, dan menahan baris parsial terakhir hingga data lebih banyak tiba. Setiap baris lengkap yang dimulai dengan data: adalah JSON, kecuali penanda [DONE] di akhir.
const log = document.getElementById("log");
const form = document.getElementById("form");
const input = document.getElementById("text");
const history = [];
function addLine(cls, text) {
const div = document.createElement("div");
div.className = cls;
div.textContent = text; // textContent, never innerHTML, for untrusted text
log.appendChild(div);
return div;
}
form.addEventListener("submit", async (e) => {
e.preventDefault();
const text = input.value.trim();
if (!text) return;
input.value = "";
history.push({ role: "user", content: text });
addLine("me", "You: " + text);
const bubble = addLine("bot", "");
const res = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ messages: history }),
});
if (!res.ok) {
bubble.textContent = "(the chat is unavailable right now)";
history.pop();
return;
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "", reply = "";
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop(); // keep a partial line for the next read
for (const line of lines) {
if (!line.startsWith("data: ")) continue;
const data = line.slice(6).trim();
if (data === "[DONE]") continue;
const json = JSON.parse(data);
const piece = json.choices?.[0]?.delta?.content;
if (piece) {
reply += piece;
bubble.textContent = reply;
}
}
}
history.push({ role: "assistant", content: reply });
});Perhatikan array history. API tidak menyimpan memori antara panggilan, sehingga halaman mengirim ulang seluruh percakapan setiap kali, dan server memotongnya. Mulai aplikasi dan buka di browser:
export API_KEY="paste-your-key-here"
node server.js
Catatan tentang perenderan markdown
Model obrolan menyukai tanda bintang, daftar, dan pagar kode sesekali. Demo kami menampilkan teks mentah, yang aman. Saat Anda menginginkan output yang rapi, render markdown dengan pustaka, tetapi ikuti dua aturan. Jalankan HTML melalui pembersih sebelum menyisipkannya dengan innerHTML, karena model, atau pengguna yang menipu model tersebut, dapat menghasilkan tag dan atribut yang tidak Anda maksud. Dan render secara inkremental dengan hati-hati: mengurai ulang seluruh balasan pada setiap fragmen baik untuk pesan pendek, tetapi markdown yang belum selesai dapat berkedip, sehingga beberapa pembuat aplikasi menampilkan teks biasa saat streaming dan beralih ke output yang diformat saat streaming berakhir.
Format roleplay adalah keanehan terpisah. Banyak karakter membungkus tindakan dalam asterisk, seperti *menyesuaikan lampu*. Putuskan apakah aplikasi Anda menggayakan itu sebagai miring, dan beri tahu karakter dalam prompt sistem konvensi mana yang harus diikuti. panduan desain persona kami menunjukkan kata-kata prompt untuk itu.
Langkah 4: uji asap rute
Sebelum menyalahkan browser, periksa proxy secara langsung. Jika terminal berfungsi, server baik-baik saja dan bug yang tersisa ada di halaman.
curl -N http://localhost:3000/api/chat \
-H "Content-Type: application/json" \
-d '{"messages":[{"role":"user","content":"Wren, is the fog coming in?"}]}'Bendera -N menonaktifkan buffering bawaan curl, sehingga Anda harus melihat baris data: muncul secara bertahap, diakhiri dengan data: [DONE]. Jika Anda mendapatkan kesalahan JSON, baca statusnya: 401 berarti kunci di lingkungan Anda salah, 402 berarti saldo kosong, dan 404 berarti URL hulu memiliki typo.
| Gejala | Penyebab yang mungkin | Perbaikan |
|---|---|---|
| Halaman menampilkan pesan tidak tersedia | Server mengembalikan status non-200 | Jalankan uji curl dan baca status |
| Teks hanya muncul di akhir | Lapisan proxy melakukan buffering respons | Nonaktifkan buffering untuk rute |
| Karakter yang terdistorsi | Decoder digunakan tanpa stream: true | Tetap setel opsi pada TextDecoder.decode |
| Balasan terpotong di tengah kalimat | max_tokens tercapai | Tingkatkan batas di sisi server |
Dengan empat perbaikan tersebut, Anda dapat mendiagnosis hampir setiap masalah saat pertama kali berjalan dalam waktu kurang dari satu menit. Simpan perintah curl dalam catatan Anda; perintah ini juga merupakan pemeriksaan kesehatan yang berguna setelah Anda mengubah proxy nanti.
Sentuhan kecil yang membuatnya terasa selesai
Percakapan yang melakukan streaming sudah menyenangkan, tetapi beberapa detail memisahkan demo dari produk yang kembali dikunjungi pengguna.
- Nonaktifkan tombol kirim saat balasan sedang tiba. Pengiriman ganda menciptakan riwayat yang saling bersilangan yang membingungkan pengguna maupun model.
- Tambahkan tombol berhenti. Buat
AbortController, teruskan sinyalnya kefetch, dan panggilabort()saat tombol diklik. Simpan teks yang sudah diterima sejauh ini sebagai giliran asisten. - Persistkan log. Simpan riwayat di
sessionStorageagar pembaruan halaman tidak menghapus percakapan, dan tawarkan tombol hapus obrolan yang mengosongkannya. - Gulir otomatis secara wajar. Ikuti bagian bawah hanya ketika pengguna sudah berada di dekatnya; jika tidak, biarkan mereka membaca baris lama dengan tenang.
- Tampilkan kegagalan yang lembut. Ganti gelembung kosong dengan tautan coba ulang yang mengirim ulang pesan pengguna terakhir.
Masing-masing dari ini adalah beberapa baris JavaScript murni, jadi tahan diri untuk tidak menggunakan framework sampai antarmuka Anda benar-benar membutuhkan komponen, routing, atau status bersama.
Penguatan dan langkah selanjutnya
Anda sekarang memiliki percakapan yang berfungsi. Sebelum pengguna nyata tiba, tambahkan beberapa pengaman di server.js. Batasi permintaan per IP atau sesi, karena satu kunci diizinkan 300 permintaan per menit secara total, dan satu pengguna yang antusias dapat menghabiskan seluruh kuota tersebut. Tangani kesalahan hulu secara eksplisit: 503 dengan upstream_busy layak mendapatkan tombol coba ulang yang ramah, dan 403 content_blocked layak mendapatkan pesan yang jelas daripada gelembung kosong. Pertahankan max_tokens pada nilai yang wajar; 500 sudah cukup untuk percakapan, sedangkan batas maksimum yang diizinkan adalah 32.000 per permintaan.
Kemudian pikirkan tentang biaya. Sebagai ilustrasi, asumsikan setiap putaran mengirim 1.200 token prompt dan menerima 250 token penyelesaian. Itu sekitar $0,0003 untuk input dan $0,00025 untuk output, sekitar $0,00055 per putaran. Jumlah token tersebut adalah asumsi, jadi ukur sendiri Anda dengan chunk usage. docs mencakup bidang tersebut, dan panduan LLM tanpa sensor yang di-hosting menjelaskan apa yang harus diharapkan dari layanan jenis ini.
Tanya jawab
Bisakah saya memanggil API langsung dari browser?
Secara teknis bisa, tetapi Anda akan mengekspos kunci Anda kepada setiap pengunjung. Gunakan rute server seperti yang ada dalam tutorial ini agar kunci tetap berada di variabel lingkungan.
Apakah server perlu menguraikan stream?
Tidak. Server dapat meneruskan byte tanpa perubahan, dan halaman membaca baris data. Parse pada server hanya jika Anda ingin mencatat teks atau memfilter output.
Mengapa balasan saya tiba sekaligus?
Sesuatu di antaranya adalah buffering. Periksa bahwa permintaan mengatur stream menjadi true dan bahwa proxy terbalik atau lapisan kompresi tidak menahan respons.
Bagaimana saya memberi memori pada bot?
Kirimkan kembali percakapan dalam pesan pada setiap panggilan, pangkas giliran tertua ketika percakapan bertambah panjang. Jendela konteks 100.000 token dibagikan dengan balasan.
Kunci Anda hanya selangkah lagi dari satu formulir
Buat akun, salin kunci, ubah URL dasar. Itu saja pengaturannya.