Files
sproutclaw/.pi/agent/extensions/webui/backend/http/static.ts
root cf5edd6394
Some checks failed
CI / build-check-test (push) Has been cancelled
feat(sproutclaw): modularize webui, extensions, and agent config layout
Restructure local extensions into per-feature directories, split WebUI
into backend modules with slash commands and systemd support, and track
prompts/skills under .pi/agent for portable Gitea deployment.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 16:57:08 +08:00

84 lines
2.2 KiB
TypeScript

import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { gzipSync } from "node:zlib";
import type { ServerResponse, IncomingMessage } from "node:http";
const MIME: Record<string, string> = {
".html": "text/html; charset=utf-8",
".js": "application/javascript; charset=utf-8",
".css": "text/css; charset=utf-8",
".png": "image/png",
".svg": "image/svg+xml",
".woff2": "font/woff2",
".woff": "font/woff",
".ico": "image/x-icon",
".webmanifest": "application/manifest+json",
".json": "application/json",
};
const GZIPABLE = new Set([".html", ".js", ".css", ".json", ".webmanifest"]);
// Vite build outputs hashed filenames like index-DeT2iZAf.js
const HASHED_FILE_RE = /-[A-Za-z0-9_-]{6,}\.\w+$/;
function getCacheControl(file: string): string | undefined {
if (file === "/index.html") {
return "no-cache";
}
if (HASHED_FILE_RE.test(file) || file.endsWith(".woff2") || file.endsWith(".woff")) {
return "public, max-age=31536000, immutable";
}
return undefined;
}
export function serveStatic(
publicDir: string,
urlPath: string,
req: IncomingMessage,
res: ServerResponse,
spaFallback = false,
): void {
const file = urlPath === "/" ? "/index.html" : urlPath;
const full = join(publicDir, file);
if (!full.startsWith(publicDir)) {
res.writeHead(403);
res.end("Forbidden");
return;
}
if (!existsSync(full)) {
if (spaFallback && !urlPath.includes(".")) {
serveStatic(publicDir, "/index.html", req, res, false);
return;
}
res.writeHead(404);
res.end("Not found");
return;
}
const ext = file.match(/\.\w+$/)?.[0] || ".html";
const mimeType = MIME[ext] || "application/octet-stream";
const content = readFileSync(full);
const headers: Record<string, string> = {
"Content-Type": mimeType,
};
const cacheControl = getCacheControl(file);
if (cacheControl) {
headers["Cache-Control"] = cacheControl;
}
const acceptEncoding = req.headers["accept-encoding"] || "";
const shouldGzip = GZIPABLE.has(ext) && acceptEncoding.includes("gzip");
if (shouldGzip) {
const compressed = gzipSync(content);
headers["Content-Encoding"] = "gzip";
res.writeHead(200, headers);
res.end(compressed);
return;
}
res.writeHead(200, headers);
res.end(content);
}