繁中 ▾
Uncensored Chatbot API直接 API 存取單一無審查 LLM取得 API 金鑰

Uncensored Chatbot API網頁聊天

讓我們建立串流網頁聊天(Express 代理 + 純 JavaScript)

在本教學中,你將建立一個約 100 行的簡易聊天頁面。一個輕量的 Express 伺服器持有你的金鑰並將請求轉送至 Uncensored Chatbot API,而純 JavaScript 前端則逐字讀取串流輸出的回覆。無需框架、無需建置步驟,且你的金鑰不會傳到瀏覽器。

更新於

重點

  • 永遠不要從瀏覽器程式碼呼叫 API;伺服器路由可讓金鑰保持私密並允許你驗證輸入。
  • 代理只需透過伺服器發送的事件;頁面解析資料行直到 [DONE]。
  • 串流期間使用 textContent,並在渲染 Markdown 前進行消毒。
  • 在伺服器端限制歷史長度與 max_tokens,以免單一使用者耗盡你的餘額。

我們要建立什麼,以及為何需要代理

想像一個包含文字輸入框和滾動日誌的單一頁面。你輸入一行文字,頁面將對話發送到你自己伺服器上的 /api/chat,你的伺服器會將請求轉送至上游並附上真實金鑰。回覆會串流回來,每個片段都會在到達時立即顯示。我們將賦予機器人一點個性,一位名叫 Wren 的燈塔看守員,讓演示更具生動感。

你需要 Node 18 或更新版本(用於內建 fetch)以及 API 金鑰。如果你還沒有金鑰,請從 金鑰頁面取得免費試用額度:新帳號可獲得 $0.50 的額度,有效期 7 天,無需提供付款資訊。Base URL 為 https://api.uncensoredchatbotapi.com/v1,模型 ID 為 uncensored。

為何你需要代理

將金鑰貼入前端程式碼並直接呼叫 API 很有誘惑力。請不要這樣做。任何傳送至瀏覽器的程式碼都可以被任何開啟開發者工具的人讀取,而金鑰洩漏意味著餘額被耗盡。代理可以解決這個問題,並提供三個額外的好處。

  • 機密性。 金鑰位於伺服器的環境變數中。
  • 控制權。 你可以決定系統提示詞、歷史長度與 max_tokens,讓使用者無法覆蓋它們。
  • 規則存放之處。 每個使用者的限制、年齡門檻與記錄政策都應歸屬於此層級。安全指南 建立在這個路由之上。

步驟 1:設定專案

建立一個資料夾,將其初始化為 ES 模組專案並安裝 Express。其他功能皆為內建。

mkdir lantern-chat && cd lantern-chat
npm init -y
npm pkg set type=module
npm install express
mkdir public

你最終將得到兩樣東西:位於根目錄的 server.js 以及包含頁面與腳本的 public 資料夾。

步驟 2:編寫 Express 代理

伺服器提供靜態檔案並公開一個 POST 路由。請分三部分閱讀。首先,它消毒輸入的歷史:只有 user 和 assistant 角色會通過,每則訊息會截斷至 4,000 個字元,且僅保留最後 30 個對話輪次。其次,它會預先附加你自己的系統訊息,瀏覽器無法更改。第三,它會以 stream: true 呼叫上游端點,並將位元組直接轉送回去。

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"));

有兩個選擇值得解釋。轉送原始位元組意味著你完全不需要了解伺服器上的事件格式。而對於失敗情況返回上游狀態碼,讓頁面能做出合理的反應;例如,402 表示你的餘額已空,429 表示有人請求過快。

步驟 3:頁面與串流讀取器

現在是前端。將此儲存為 public/index.html;它刻意保持簡潔,以便你稍後重新設計樣式。

<!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>

接著,public/chat.js。有趣的部分在於讀取迴圈。網路區塊不會尊重換行邊界,因此我們保留一個buffer,按換行符號分割,並將最後一個不完整的行保留下來,直到更多資料到達。每個以data: 開頭的完整行都是 JSON,最後一個[DONE]標記除外。

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 陣列。API 在呼叫之間不保留記憶,因此頁面每次都會重新傳送整個對話,伺服器會進行修剪。啟動應用程式並在瀏覽器中開啟它:

export API_KEY="paste-your-key-here"
node server.js

關於 Markdown 渲染的注意事項

聊天模型喜歡星號、列表和偶爾出現的程式碼區塊。我們的示範顯示純文字,這很安全。當你想要漂亮的輸出時,請使用函式庫渲染 Markdown,但請遵循兩項規則。在透過 innerHTML 插入之前,先將 HTML 經過消毒器處理,因為模型或誘騙它的用戶可能會產生你未預期的標籤和屬性。並且謹慎地增量渲染:對每個片段重新解析整個回覆對於短訊息來說沒問題,但未完成的 Markdown 可能會閃爍,因此某些開發者在串流輸出期間顯示純文字,並在串流結束時切換為格式化輸出。

角色扮演格式是一個獨立的怪癖。許多角色會用星號將動作包起來,例如 *調整燈具*。決定你的應用程式是否將這些樣式化為斜體,並在系統提示詞中告訴角色遵循哪種慣例。我們的角色設計指南 展示了相關的提示詞寫法。

步驟 4:對路由進行煙霧測試

在怪罪瀏覽器之前,直接測試代理。如果終端機正常運作,伺服器沒問題,任何剩餘的錯誤都在頁面中。

curl -N http://localhost:3000/api/chat \
  -H "Content-Type: application/json" \
  -d '{"messages":[{"role":"user","content":"Wren, is the fog coming in?"}]}'

-N 標誌會停用 curl 自身的緩衝,因此你應該看到 data: 行逐漸出現,最後以 data: [DONE] 結束。如果你收到 JSON 錯誤,請讀取其狀態碼:401 表示環境中的金鑰有誤,402 表示餘額已空,404 表示上游 URL 有拼寫錯誤。

症狀可能原因修復方法
頁面顯示不可用訊息伺服器返回非 200 狀態碼執行 curl 測試並讀取狀態碼
文字只在最後出現代理層緩衝了回應停用該路由的緩衝
字元錯亂未使用 stream: true 的解碼器在 TextDecoder.decode 上保持選項設定
回覆在中途截斷max_tokens 已達上限提高伺服器上限

透過這四項修正,你可以在一分鐘內診斷出幾乎所有首次執行的問題。將 curl 指令保留在你的筆記中;在稍後更改代理伺服器後,它也是一個方便的健康檢查工具。

讓產品感覺完整的細部調整

串流輸出的聊天體驗已經很愉快,但少少的細節就能讓它從展示品變成使用者會再次回訪的產品。

  • 在收到回覆時停用發送按鈕。 重複提交會產生交錯的歷史記錄,讓用戶和模型都感到困惑。
  • 新增停止按鈕。 建立一個 AbortController,將其訊號傳遞給 fetch,並在點擊時呼叫 abort()。將已到達的所有文字儲存為助理回應回合。
  • 儲存日誌。 將歷史記錄保存在 sessionStorage 中,這樣重新整理就不會清除對話,並提供一個清除聊天按鈕來清空它。
  • 智慧自動捲動。 僅在使用者已經接近底部時才跟隨捲動;否則讓使用者可以安靜地閱讀較舊的內容。
  • 顯示溫和的錯誤提示。 將空白的對話框替換為重新傳送上一則使用者訊息的重新嘗試連結。

這些功能每個都只需要幾行純 JavaScript 程式碼,因此在介面真正需要元件、路由或共享狀態之前,請克制住不要引入框架。

強化與後續步驟

你現在擁有一個可運作的聊天應用程式。在真實用戶到來之前,請在 server.js 中加入一些防護措施。限制每個 IP 或工作階段的請求數量,因為單一金鑰總共允許每分鐘 300 次請求,而一個急切的使用者可能會耗盡所有額度。明確處理上游錯誤:帶有 upstream_busy 的 503 錯誤值得一個友善的重試按鈕,而 403 content_blocked 則應顯示清晰的訊息而非空白對話框。將 max_tokens 保持適中;聊天用途 500 已足夠,而每個請求允許的最大值是 32,000。

接著考慮成本。作為範例,假設每個回合發送 1,200 個提示詞 token 並接收 250 個完成 token。這大約是輸入 $0.0003 和輸出 $0.00025,每個回合約 $0.00055。這些 token 計數是假設值,因此請使用 usage 區塊測量你自己的數據。文件涵蓋該欄位,託管無審查 LLM 指南 則說明此類服務的預期情況。

問答

我可以從瀏覽器直接呼叫 API 嗎?

技術上可以,但這會將你的金鑰暴露給所有訪客。使用本教學中的伺服器路由,讓金鑰保留在環境變數中。

伺服器需要解析串流嗎?

不需要。它可以原封不動地轉送位元組,頁面則讀取資料行。如果你想要記錄文字或過濾輸出,才在伺服器端進行解析。

為什麼我的回覆會一次全部到達?

中間的某個環節正在緩衝。檢查請求是否設定了 stream 為 true,以及任何反向代理或壓縮層是否正在保留回應。

我該如何讓機器人擁有記憶?

在每次呼叫時以訊息形式重新傳送對話,並在對話變長時移除最舊的回合。100,000 token 的上下文視窗與回覆共用。

只差一張表單,即可取得金鑰

建立帳號、複製金鑰、更改基礎 URL。這就是整個設定。

取得 API 金鑰閱讀文件