Akışlı Web Sohbeti Oluşturalım (Express Proxy + Saf JS)
Bu örnekte yaklaşık 100 satırda küçük bir sohbet sayfası oluşturacaksınız. Küçük bir Express sunucusu gizli anahtarınızı tutar ve istekleri Uncensored Chatbot API'a iletir; saf JavaScript ön yüzü, kelime kelime akışlı yanıtı okur. Framework yok, build adımı yok ve anahtarınız asla tarayıcıya ulaşmaz.
Güncelleme:
Temel noktalar
- API'yi tarayıcı kodundan asla çağırmayın; bir sunucu rotası anahtarı özel tutar ve girdiyi doğrulamanızı sağlar.
- Proxy'nin yapması gereken tek şey sunucu gönderilen olayları (SSE) geçirmektir; sayfa [DONE] işaretine kadar veri satırlarını parse eder.
- Akış sırasında textContent kullanın ve markdown render etmeden önce temizleyin.
- Sunucuda geçmiş uzunluğunu ve max_tokens'u sınırlandırın, böylece tek bir kullanıcı tüm bakiyenizi harcayamaz.
Ne inşa ediyoruz ve neden proxy
Tek bir sayya, bir metin kutusu ve kaydırılabilir bir günlük düşünün. Bir satır yazarsınız, sayfa konuşmayı kendi sunucunuzdaki /api/chat uç noktasına gönderir ve sunucunuz gerçek anahtarı ekleyerek bunu yukarıya iletir. Yanıt akış halinde geri döner ve her parça geldiği anda görünür. Botun biraz kişiliği olsun diye, deneme ortamının canlı hissettirmesi için Wren adında bir fener bekçisi ekleyeceğiz.
Node 18 veya daha yeni bir sürüm (dahili fetch için) ve bir API anahtarına ihtiyacınız var. Henüz bir tane yoksa, ücretsiz deneme kredisini anahtar sayfasından edinin: yeni hesaplar için 7 gün boyunca $0.50 kredi verilir, ödeme bilgisi gerekmez. Base URL https://api.uncensoredchatbotapi.com/v1 ve model kimliği uncensored şeklindedir.
Neden proxy'ye ihtiyacınız var
Anahtarı ön yüz koduna yapıştırıp API'yi doğrudan çağırmak cazip gelebilir. Lütfen yapmayın. Tarayıcıya gönderilen her şey, geliştirici araçlarını açan herkes tarafından okunabilir ve sızan bir anahtar bakiyenizin bitmesine neden olur. Bir proxy bunu düzeltir ve üç ek avantaj sağlar.
- Gizlilik. Anahtar, sunucudaki bir ortam değişkeninde yaşar.
- Kontrol. Sistem istemini, geçmiş uzunluğunu ve
max_tokens'i siz belirlersiniz, böylece kullanıcılar bunları geçersiz kılamaz. - Kurallar için bir yer. Kullanıcı başına limitler, yaş kapıları ve günlük politikası bu katmanda olmalıdır. güvenlik kılavuzu bu rotaya dayanır.
Adım 1: projeyi kurun
Bir klasör oluşturun, onu bir ES-module projesi olarak başlatın ve Express'i yükleyin. Geri kalanı dahili olarak gelir.
mkdir lantern-chat && cd lantern-chat
npm init -y
npm pkg set type=module
npm install express
mkdir publicİki şeyle sonuçlanacaksınız: kökte server.js ve sayfa ile betiği barındıran bir public klasörü.
Adım 2: Express proxy'sini yazın
Sunucu statik dosyaları sunar ve tek bir POST yolunu açığa çıkarır. Bunu üç parçada okuyun. İlk olarak gelen geçmişi temizler: yalnızca user ve assistant rolleri geçer, her mesaj 4.000 karaktere kısaltılır ve yalnızca son 30 dönüş korunur. İkinci olarak, tarayıcının asla değiştiremeyeceği kendi sistem mesajınızı ekler. Üçüncü olarak, stream: true ile yukarı akış uç noktasını çağırır ve baytları doğrudan geri iletir.
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"));Açıklamaya değer iki seçenek var. Ham baytları geçirmek, sunucuda etkinlik biçimini hiç anlamanıza gerek kalmadığı anlamına gelir. Ve başarısızlıklar için yukarı akış durumunu döndürmek sayfanın mantıklı tepki vermesini sağlar; örneğin 402 bakiyenizin boş olduğunu ve 429 birinin çok hızlı gittiğini gösterir.
Adım 3: sayfa ve akış okuyucu
Şimdi ön yüz. Bunu public/index.html olarak kaydedin; daha sonra yeniden stil verebilmeniz için kasıtlı olarak sade bırakılmıştır.
<!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>Sonra, public/chat.js. İlginç kısım okuma döngüsüdür. Ağ parçaları satır sınırlarına saygı duymaz, bu yüzden bir buffer tutarız, yeni satırlara böleriz ve daha fazla veri gelene kadar son kısmi satırı bekletiriz. data: ile başlayan her tam satır JSON'dur, son [DONE] işaretçisi hariç.
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 });
});history dizisine dikkat edin. API çağrılar arasında hafıza tutmaz, bu yüzden sayfa her seferinde tüm konuşmayı yeniden gönderir ve sunucu kısaltır. Uygulamayı başlatın ve tarayıcıda açın:
export API_KEY="paste-your-key-here"
node server.js
Markdown render etme hakkında bir not
Sohbet modelleri yıldız işaretlerini, listeleri ve ara sıra kod çitlerini sever. Demo uygulamamız ham metin gösterir ki bu güvenlidir. Güzel bir çıktı almak istediğinizde, bir kütüphaneyle markdown render edin, ancak iki kurala uyun. HTML'yi innerHTML ile eklemeden önce bir temizleyiciden geçirin, çünkü bir model veya onu kandıran bir kullanıcı, niyet etmediğiniz etiketler ve öznitelikler üretebilir. Ve render işlemini dikkatli bir şekilde artımlı olarak yapın: her parça için tüm yanıtı yeniden parse etmek kısa mesajlar için uygundur, ancak yarım kalmış markdown titremeye neden olabilir, bu nedenle bazı geliştiriciler akış sırasında düz metin gösterir ve akış bittiğinde biçimlendirilmiş çıktıya geçer.
Rol yapma biçimlendirmesi ayrı bir tuhaflıktır. Karakterlerin çoğu eylemleri yıldızlarla sarar, örneğin *feneri ayarlar*. Uygulamanızın bunları italik olarak stillendirip biçimlendireceğine karar verin ve karaktere sistem isteminde hangi kuralı takip edeceğini söyleyin. karakter tasarım kılavuzumuz bunun için istem sözcüklerini gösterir.
Adım 4: rotayı duman testi
Tarayıcıyı suçlamadan önce proxy'ye doğrudan dokunun. Terminal çalışıyorsa, sunucu iyidir ve kalan hata sayfadadır.
curl -N http://localhost:3000/api/chat \
-H "Content-Type: application/json" \
-d '{"messages":[{"role":"user","content":"Wren, is the fog coming in?"}]}'-N bayrağı curl'un kendi tamponlamasını devre dışı bırakır, bu yüzden data: satırlarının kademeli olarak belirmesini ve data: [DONE] ile bitmesini beklemelisiniz. Bunun yerine bir JSON hatası alırsanız durumunu okuyun: 401 ortam değişkenindeki anahtarın yanlış olduğunu, 402 bakiyenin boş olduğunu ve 404'ün yukarı URL'sinde bir yazım hatası olduğunu gösterir.
| Belirti | Olası neden | Çözüm |
|---|---|---|
| Sayfa kullanılamaz mesajını gösteriyor | Sunucu 200 olmayan bir durum döndürdü | curl testini çalıştırın ve durumu okuyun |
| Metin yalnızca sonunda görünür | Bir proxy katmanı yanıtı tamponluyor | Rotanın tamponlamasını devre dışı bırakın |
| Bozuk karakterler | stream: true olmadan kullanılan Decoder | Seçeneği TextDecoder.decode olarak ayarlayın |
| Yanıt cümle ortasında kesiliyor | max_tokens ulaşıldı | Sunucudaki limiti artırın |
Bu dört düzeltmeyle ilk çalıştırma sırasında karşılaşılan neredeyse tüm sorunları bir dakikadan kısa sürede teşhis edebilirsiniz. curl komutunu notlarınızda tutun; daha sonra proxy'yi değiştirdikten sonra da kullanışlı bir sağlık kontrolü olarak işe yarar.
Tamamlanmış hissi veren küçük dokunuşlar
Akış modunda çalışan bir sohbet zaten keyiflidir, ancak bir demo ile insanların tekrar döndüğü bir ürün arasındaki farkı birkaç detay belirler.
- Yanıt gelirken gönder butonunu devre dışı bırakın. Çift gönderimler, hem kullanıcıyı hem de modeli kafa karıştıracak olan iç içe geçmiş geçmişler oluşturur.
- Bir durdurma düğmesi ekleyin. Bir
AbortControlleroluşturun, sinyalinifetch'e iletin ve tıklama durumundaabort()'i çağırın. Şimdiye kadar gelen metni asistan yanıtı olarak kaydedin. - Günlüğü kalıcı hale getirin. Geçmişi
sessionStorageiçinde tutun, böylece yenileme konuşmayı silmez ve temizle butonu ile içeriği boşaltma seçeneği sunun. - Akıllıca kaydırma. Kullanıcı zaten alt kısımdaysa yalnızca orayı takip edin; aksi takdirde onlar eski satırları huzur içinde okusun.
- Nazik bir hata gösterin. Boş balonu, son kullanıcı mesajını yeniden gönderen bir yeniden dene bağlantısıyla değiştirin.
Bunların her biri düz JavaScript ile yazılmış onlarca satırdan oluşur; bu nedenle arayüzünüz gerçekten bileşenlere, yönlendirmeye veya paylaşılan duruma ihtiyaç duymadıkça bir çerçeveye yönelme eğilimine karşı koyun.
Sertleştirme ve sonraki adımlar
Artık çalışan bir sohbetiniz var. Gerçek kullanıcılar gelmeden önce server.js içinde birkaç güvenlik önlemi ekleyin. İstekleri IP veya oturum başına sınırlayın, çünkü tek bir anahtar toplamda dakikada 300 istek için izinlidir ve tek bir hevesli kullanıcı bunların hepsini kullanabilir. Yukarı akış hatalarını açıkça yönetin: upstream_busy ile 503 dostça bir yeniden deneme düğmesi hak eder ve 403 content_blocked boş bir balon yerine net bir mesaj hak eder. max_tokens'i makul tutun; sohbet için 500 yeterlidir, izin verilen maksimum ise istek başına 32.000'dir.
Sonra maliyeti düşünün. Bir örnek olarak, her dönüşün 1.200 prompt token'ı gönderdiğini ve 250 tamamlama token'ı aldığını varsayın. Bu, giriş için yaklaşık $0.0003 ve çıkış için $0.00025, dönüş başına yaklaşık $0.00055 eder. Bu token sayıları varsayımdır, bu yüzden kendi kullanımınızı usage parçasıyla ölçün. docs bu alanı kapsar ve hosted uncensored LLMs'e dair kılavuz bu tür bir hizmetten ne bekleyeceğinizi açıklar.
Sorular ve cevaplar
API'ye tarayıcıdan doğrudan erişebilir miyim?
Teknik olarak evet, ancak anahtarınızı her ziyaretçiye açığa çıkarırsınız. Anahtarın bir ortam değişkeninde kalması için bu öğreticideki gibi bir sunucu rotası kullanın.
Sunucunun akışı ayrıştırmaya ihtiyacı var mı?
Hayır. Baytları değişmeden iletebilir ve sayfa veri satırlarını okur. Metni loglamak veya çıktıyı filtrelemek istiyorsanız yalnızca sunucuda ayrıştırın.
Neden yanıtım birdenbire geliyor?
Arada bir şey tamponlamadır. İsteğin stream değerinin true olarak ayarlandığından ve herhangi bir ters proxy veya sıkıştırma katmanının yanıtı tutmadığından emin olun.
Bot'a hafıza nasıl verilir?
Her çağrıda konuşmayı mesajlar halinde yeniden gönderin, büyüdüğünde en eski dönüşleri kısaltın. 100.000 token'lık bağlam penceresi yanıtla paylaşılır.
Anahtarınız tek bir formun uzağında
Bir hesap oluşturun, anahtarı kopyalayın, base URL'yi değiştirin. Kurulumun tamamı budur.