Files
pikasTech-HWLAB/internal/dev-entrypoint/http.mjs
T
2026-05-23 19:36:16 +00:00

176 lines
5.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createServer, request as httpRequest } from "node:http";
import { buildMetadataFromEnv } from "../build-metadata.mjs";
import { DEV_ENDPOINT, ENVIRONMENT_DEV } from "../protocol/index.mjs";
const DEFAULT_PROXY_TIMEOUT_MS = 150000;
export function parsePort(value, fallback) {
const parsed = Number.parseInt(value ?? "", 10);
if (Number.isInteger(parsed) && parsed > 0 && parsed < 65536) {
return parsed;
}
return fallback;
}
export function resolveHostPort({ listenEnv, hostEnv, portEnv, fallbackPort }) {
const listen = process.env[listenEnv] ?? "";
const [listenHost, listenPort] = listen.includes(":") ? listen.split(":") : ["", listen];
const host = process.env[hostEnv] || listenHost || "0.0.0.0";
const port = parsePort(process.env[portEnv] || listenPort, fallbackPort);
return { host, port };
}
export function healthPayload({ serviceId, role, details = {} }) {
const metadata = buildMetadataFromEnv(process.env, {
serviceId,
fallbackImageRepository: `ghcr.io/pikastech/${serviceId}`
});
return {
serviceId,
environment: ENVIRONMENT_DEV,
status: "ok",
revision: metadata.revision,
service: {
id: serviceId,
role,
healthPath: "/health",
livePath: "/health/live"
},
commit: metadata.commit,
image: metadata.image,
build: metadata.build,
endpoint: process.env.HWLAB_PUBLIC_ENDPOINT || DEV_ENDPOINT,
observedAt: new Date().toISOString(),
details
};
}
export function sendJson(response, statusCode, body) {
const payload = JSON.stringify(body, null, 2);
response.writeHead(statusCode, {
"content-type": "application/json; charset=utf-8",
"content-length": Buffer.byteLength(payload)
});
response.end(payload);
}
export function createServiceServer({ serviceId, role, routes = new Map(), details = () => ({}) }) {
return createServer(async (request, response) => {
try {
const url = new URL(request.url || "/", `http://${serviceId}.local`);
if (request.method === "GET" && (url.pathname === "/health" || url.pathname === "/health/live")) {
sendJson(response, 200, healthPayload({ serviceId, role, details: details() }));
return;
}
if (request.method === "GET" && url.pathname === "/help") {
sendJson(response, 200, {
serviceId,
commands: ["GET /health", "GET /health/live", ...routes.keys()]
});
return;
}
const handler = routes.get(`${request.method ?? "GET"} ${url.pathname}`);
if (!handler) {
sendJson(response, 404, { error: "not_found", serviceId, path: url.pathname });
return;
}
sendJson(response, 200, await handler({ request, url }));
} catch (error) {
sendJson(response, 500, {
error: "internal_error",
serviceId,
message: error instanceof Error ? error.message : String(error)
});
}
});
}
export function listen(server, { serviceId, host, port }) {
server.listen(port, host, () => {
process.stdout.write(`${JSON.stringify({ serviceId, status: "listening", host, port })}\n`);
});
for (const signal of ["SIGINT", "SIGTERM"]) {
process.on(signal, () => {
server.close(() => {
process.exit(0);
});
});
}
}
export function proxyHttpRequest({ request, response, upstream, timeoutMs = DEFAULT_PROXY_TIMEOUT_MS }) {
const target = new URL(request.url || "/", upstream);
const traceId = request.headers["x-trace-id"] ?? null;
let timedOut = false;
const timeout = setTimeout(() => {
timedOut = true;
proxy.destroy(new Error(`upstream timed out after ${timeoutMs}ms`));
}, timeoutMs);
const proxy = httpRequest(
target,
{
method: request.method,
headers: {
...request.headers,
host: target.host
}
},
(upstreamResponse) => {
response.writeHead(upstreamResponse.statusCode ?? 502, upstreamResponse.headers);
upstreamResponse.on("end", () => clearTimeout(timeout));
upstreamResponse.on("close", () => clearTimeout(timeout));
upstreamResponse.pipe(response);
}
);
let settled = false;
proxy.on("error", (error) => {
clearTimeout(timeout);
if (settled || response.headersSent) {
response.destroy(error);
return;
}
settled = true;
const code = timedOut ? "proxy_timeout" : "upstream_unavailable";
const category = timedOut ? "timeout" : "proxy";
const userMessage = timedOut
? `Code Agent 代理等待上游超过 ${timeoutMs}ms;输入已保留,可稍后重试。`
: "Code Agent 代理暂时无法连接上游输入已保留可稍后重试";
sendJson(response, 502, {
status: "failed",
error: {
code,
layer: "proxy",
category,
retryable: true,
userMessage,
message: userMessage,
timeoutMs: timedOut ? timeoutMs : null,
traceId,
route: request.url || null,
blocker: {
code,
layer: "proxy",
category,
retryable: true,
summary: userMessage,
userMessage,
traceId,
route: request.url || null
}
},
upstream,
traceId,
message: userMessage
});
});
request.pipe(proxy);
}