← 開發者指南

Node.js 伺服器整合

Node.js backend 的兩種模式:為瀏覽器前端發行 session token,或是直接從純伺服器端程式碼呼叫 API。

Node.js 在後端涵蓋兩種不同的工作,本篇都會提到:(1) 發行 session token,讓瀏覽器前端 (純網頁或 React)能安全地呼叫 API;(2) 直接從純伺服器端程式碼呼叫 API — cron job、webhook 處理器、CLI,任何不涉及瀏覽器的情境。請依您要建置的內容,找到相應的章節。

為瀏覽器前端發行 token

這就是把 選擇您的認證模式 的模式具體實作出來:在您的伺服器上 有一個 token 發行 endpoint,而瀏覽器頁面(見 純網頁整合React SPA 整合 的瀏覽器端做法)會在呼叫 SDK 之前先呼叫它。

1. 在您的 backend 加入 token endpoint

發行 session JWT 的建議做法,是讓您的 backend 呼叫平台自己的 POST /auth/token endpoint,並以 取得您的 API 金鑰 取得的 client_id/client_secret 組合做認證。 您的 backend 自己完全不做任何簽章 — 它只是轉發請求,並把收到的 token 交給瀏覽器。

const CLIENT_ID = process.env.QURAVIN_CLIENT_ID;         // 來自 Console
const CLIENT_SECRET = process.env.QURAVIN_CLIENT_SECRET; // 來自 Console,只顯示一次

app.get("/ai-token", requireUserLogin, async (req, res) => {
  const basic = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString("base64");

  const r = await fetch("https://api.quravin.com/auth/token", {
    method: "POST",
    headers: {
      Authorization: `Basic ${basic}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      subject: req.user.id,     // 您 app 中的使用者 id — 會成為 JWT 的 `sub`
      ttl_seconds: 900,         // 選填;60-3600,預設 900(15 分鐘)
      // pipelines: ["translate-string"],  // 選填 — 省略即可取得您 app 的完整允許清單
    }),
  });
  if (!r.ok) return res.status(502).json({ error: "token_mint_failed" });

  const { token, expires_in } = await r.json();
  res.json({ token, expires_in });
});

POST /auth/token 的回應為:

{ "token": "eyJhbGciOi...", "token_type": "Bearer", "expires_in": 900 }

您請求的 pipelines 會與您 Application 的 allowed_pipelines(在 Console 設定 — 見 取得您的 API 金鑰)取交集;省略此欄位即可取得您 app 被允許呼叫的 全部內容。requireUserLogin 是您自己的驗證中介層 — token 只會發給您伺服器已經驗證過的使用者。

替代方案:自行簽發 JWT(舊式做法)

有些 app 會改為持有自己的 per-app 簽章密鑰AI_PLATFORM_SIGNING_SECRET),並用像 jsonwebtoken 這樣的函式庫在本地發行 JWT,而不呼叫 POST /auth/token。這是一個真實、受支援的機制,但屬於舊式路徑 — 除非您有特定理由需要自行簽發 (例如您原本就這樣運作、不想更動),否則建議優先採用上面的 POST /auth/token

import jwt from "jsonwebtoken";

const SECRET = process.env.AI_PLATFORM_SIGNING_SECRET;   // 不是 client_secret — 詳見下方說明
const APP_ID = "your-app-id";                             // 您應用程式的註冊 id

app.get("/ai-token", requireUserLogin, (req, res) => {
  const token = jwt.sign(
    {
      iss: APP_ID,
      sub: req.user.id,                    // 您應用程式中的使用者 id
      aud: "ai-platform",
      pipelines: ["translate-string"],     // 這位使用者可以呼叫哪些 pipeline
      rate_limit: { rpm: 30 },
    },
    SECRET,
    { algorithm: "HS256", expiresIn: "15m" },
  );
  res.json({ token, expires_in: 15 * 60 });
});

這使用的憑證與 client_id/client_secret 不同。 簽章密鑰並非由 Console 發出 — 它是由平台 維運方另外透過安全管道提供的,專門給需要自行簽發的 app 使用。如果您手上還沒有這把密鑰,請改用上面的 POST /auth/token 路徑,而不是去申請一把。

兩種發行路徑的 iss 不同 — 除錯時看到的 JWT payload,不要假設 issuer 永遠一樣。POST /auth/token 發行的 token,iss 永遠是 "ai-platform"(平台自己簽發的)。自行簽發的 token(本節)則是 iss: <您的 app id> — 平台仍然會接受,並依該 app id 已註冊的簽章密鑰驗證。 兩者的 aud 都是 "ai-platform"

該用哪一把密鑰

這些認證文件中出現了三種不同的密鑰,名稱又頗為相似,很容易混淆。完整列表如下:

密鑰環境變數誰持有用途
Client secretQURAVIN_CLIENT_SECRET(名稱您自訂)您的 backend用於 POST /auth/tokenAuthorization: Basic client_id:client_secret — 上面的建議路徑。
Per-app 簽章密鑰AI_PLATFORM_SIGNING_SECRET您的 backend(僅在自行簽發時)在本地自行簽發 JWT,繞過 POST /auth/token — 本節的舊式路徑。
平台 JWT 簽章密鑰(platform-internal, not exposed)僅平台維運方持有,您不會拿到平台自己透過 POST /auth/token 發行 token 時所使用的簽章密鑰。您永遠不會看到或設定它。

如果您只呼叫 POST /auth/token(建議路徑),您只需要關心 client secret — 另外兩把屬於 內部/舊式的範疇。

2. 您的前端取得 token 並呼叫 SDK

fetch("/ai-token", { credentials: "include" }) 是對您自己 backend 路由(步驟 1)的同源呼叫 — 不涉及 CORS,只是您平常使用的 cookie-based session。SDK 自己對我們 API 發出的呼叫是另一個獨立 的跨來源請求,由 SDK 自行處理;您不需要為它設定 CORS。完整的瀏覽器端程式碼(純網頁或 React) 在 純網頁整合React SPA 整合 中 — 這個 endpoint 正是這兩篇指南假設已經 存在的那一塊。

到這裡整個流程就完成了:您的伺服器發行 token,瀏覽器永遠看不到 secret,每一次呼叫都會歸屬到實際 發出呼叫的使用者。

直接從純伺服器端程式碼呼叫 API

不涉及瀏覽器 — cron job、webhook 處理器、CLI、批次腳本。直接使用 API 金鑰模式;不需要發行 token,也不需要每次請求都往返一次 POST /auth/token

這裡沒有 window 可以讓 <script> 標籤掛上去,而且 SDK 自己的 import { Quravin } from "@quravin/sdk" 這種寫法目前還沒上到公開的 npm registry(如果您需要這條路徑,請洽詢您的 平台維運方)— 所以現階段純伺服器端的 Node.js 程式碼,請直接以 HTTP 呼叫 API。Node 18+ 全域內建 fetch,不需要額外安裝套件:

const API_BASE = process.env.QURAVIN_API_BASE;
const API_KEY = process.env.QURAVIN_API_KEY; // 來自 Console — 見「取得您的 API 金鑰」

async function runPipeline(pipelineId, inputs) {
  const submit = await fetch(`${API_BASE}/tickets`, {
    method: "POST",
    headers: { "Content-Type": "application/json", "x-api-key": API_KEY },
    body: JSON.stringify({ mode: "pipeline", pipeline_id: pipelineId, inputs }),
  });
  const { ticket_id } = await submit.json();

  while (true) {
    await new Promise((r) => setTimeout(r, 1000));
    const poll = await fetch(`${API_BASE}/tickets/${ticket_id}`, {
      headers: { "x-api-key": API_KEY },
    });
    const ticket = await poll.json();
    if (ticket.status === "DONE") return ticket.result;
    if (ticket.status === "FAILED") throw new Error(ticket.error);
  }
}

const out = await runPipeline("translate-string", { text: "Hello", target_language: "de" });
console.log(out.translation); // "Hallo"

完整的請求/回應格式與錯誤對照表,這個輪詢迴圈就是建立在其上,請見 Direct API 整合

這就是 取得您的 API 金鑰 的「關於靜態的 x-api-key?」章節中 提到的同一把 x-api-key — 適合存放在純伺服器端程式碼中(環境變數、secrets manager),因為它 永遠不會被送到瀏覽器。每次呼叫都會歸屬於這個 app,而不是某個特定使用者;如果即使是純伺服器端 程式碼,您也需要每位使用者的歸屬,請改為每位使用者發行一把 session JWT(呼叫方式與上面的 POST /auth/token 相同,只是由您自己的伺服器邏輯觸發,而不是傳入的瀏覽器請求 — 接著以 Authorization: Bearer <jwt> 取代 x-api-key 送出)。

處理錯誤

觸及配額或 rate limit 時會回傳型別化的錯誤 — 依您的情境顯示出來即可(瀏覽器流程中給使用者的訊息, 批次工作中的重試/警示):

try {
  const out = await runPipeline("translate-string", { text: "Hello", target_language: "de" });
} catch (err) {
  console.error("Translation failed:", err.message);
}

完整的狀態碼清單,以及每個狀態碼對已認證路徑代表的意義,請見 工具參考手冊 的 Errors 章節。

進階內容

SDK 還支援上面沒有展示的幾種模式:

ai.runMany(...)ai.button(...) 與疑難排解表記錄在 SDK 更完整的 Integration Guide 中 — 它並非公開文件,如果您需要,請洽詢您的平台維運方索取。