migration: preserve frontend shell pages and styles
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
(function exposeApiClient(global) {
|
||||
"use strict";
|
||||
|
||||
let csrfTokenSupplier = () => "";
|
||||
let unauthorizedHandler = () => {};
|
||||
|
||||
class ApiError extends Error {
|
||||
constructor(message, status = 0, payload = null) {
|
||||
super(message);
|
||||
this.name = "ApiError";
|
||||
this.status = status;
|
||||
this.payload = payload;
|
||||
}
|
||||
}
|
||||
|
||||
function configure(options = {}) {
|
||||
if (typeof options.csrfToken === "function") csrfTokenSupplier = options.csrfToken;
|
||||
if (typeof options.onUnauthorized === "function") unauthorizedHandler = options.onUnauthorized;
|
||||
}
|
||||
|
||||
function requestOptions(method, body, signal) {
|
||||
const normalizedMethod = String(method || "GET").toUpperCase();
|
||||
const options = { method: normalizedMethod, headers: {}, signal };
|
||||
const csrfToken = csrfTokenSupplier();
|
||||
if (!["GET", "HEAD", "OPTIONS"].includes(normalizedMethod) && csrfToken) {
|
||||
options.headers["X-CSRF-Token"] = csrfToken;
|
||||
}
|
||||
if (body !== null && body !== undefined) {
|
||||
options.headers["Content-Type"] = "application/json";
|
||||
options.body = JSON.stringify(body);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
async function parseJson(response) {
|
||||
try {
|
||||
return await response.json();
|
||||
} catch (_error) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function handleUnauthorized(response, url) {
|
||||
if (response.status === 401 && !String(url).startsWith("/api/auth/")) {
|
||||
unauthorizedHandler({ response, url });
|
||||
}
|
||||
}
|
||||
|
||||
async function request(url, method = "GET", body = null, options = {}) {
|
||||
const response = await fetch(url, requestOptions(method, body, options.signal));
|
||||
const payload = await parseJson(response);
|
||||
handleUnauthorized(response, url);
|
||||
if (!response.ok || payload.error) {
|
||||
throw new ApiError(payload.error || "请求失败", response.status, payload);
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function streamNdjson(url, options = {}) {
|
||||
const response = await fetch(
|
||||
url,
|
||||
requestOptions(options.method || "POST", options.body, options.signal),
|
||||
);
|
||||
if (!response.ok) {
|
||||
const payload = await parseJson(response);
|
||||
handleUnauthorized(response, url);
|
||||
throw new ApiError(
|
||||
payload.error || options.errorMessage || "流式请求暂不可用",
|
||||
response.status,
|
||||
payload,
|
||||
);
|
||||
}
|
||||
if (!response.body) throw new ApiError("当前浏览器不支持流式回答", response.status);
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
const consume = (line) => {
|
||||
if (!line.trim()) return;
|
||||
let event;
|
||||
try {
|
||||
event = JSON.parse(line);
|
||||
} catch (_error) {
|
||||
throw new ApiError("流式响应格式错误", response.status);
|
||||
}
|
||||
if (event.type === "error") {
|
||||
throw new ApiError(event.error || options.errorMessage || "流式请求失败", response.status, event);
|
||||
}
|
||||
options.onEvent?.(event);
|
||||
};
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
buffer += decoder.decode(value || new Uint8Array(), { stream: !done });
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() || "";
|
||||
lines.forEach(consume);
|
||||
if (done) break;
|
||||
}
|
||||
if (buffer.trim()) consume(buffer);
|
||||
}
|
||||
|
||||
global.XiaobaiAPI = Object.freeze({ ApiError, configure, request, streamNdjson });
|
||||
})(window);
|
||||
Reference in New Issue
Block a user