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 secret | QURAVIN_CLIENT_SECRET(名稱您自訂) | 您的 backend | 用於 POST /auth/token 的 Authorization: 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.run()。ai.button(selector, config)— 直接把按鈕綁定到 pipeline 呼叫,不必自己寫點擊處理、輪詢或 disabled 狀態邏輯。- Raw-prompt 模式 — 執行自訂的 system/user prompt,而非具名 pipeline。完整的請求/回應格式、 模型選擇,以及它與具名 pipeline 的差異,請見 Raw prompt mode。
- 疑難排解表 — SDK/HTTP 錯誤字串的完整清單,以及各自的成因。
ai.runMany(...)、ai.button(...) 與疑難排解表記錄在 SDK 更完整的 Integration Guide 中 —
它並非公開文件,如果您需要,請洽詢您的平台維運方索取。