Files
pikasTech-HWLAB/tools/src/runtime-endpoint-resolver.ts
T
2026-06-05 17:23:56 +08:00

348 lines
14 KiB
TypeScript

export type RuntimeEndpointKind = "web" | "api";
type EnvLike = Record<string, string | undefined>;
type ParsedArgs = Record<string, unknown>;
type RuntimeHints = {
lane: string | null;
rawLane: string | null;
namespace: string | null;
rawNamespace: string | null;
};
type EndpointCandidate = {
value: string | null;
source: string;
sourceName: string;
explicitOverride?: boolean;
};
export type RuntimeEndpoint = {
kind: RuntimeEndpointKind;
baseUrl: string;
source: string;
sourceName: string;
explicitOverride: boolean;
lane: string | null;
namespace: string | null;
urlLane: string | null;
urlNamespace: string | null;
warnings: Array<Record<string, unknown>>;
};
export class RuntimeEndpointError extends Error {
code: string;
details: Record<string, unknown>;
constructor(code: string, message: string, details: Record<string, unknown> = {}) {
super(message);
this.name = "RuntimeEndpointError";
this.code = code;
this.details = details;
}
}
export function resolveRuntimeEndpoint({ kind, parsed = {}, env = {} }: { kind: RuntimeEndpointKind; parsed?: ParsedArgs; env?: EnvLike }): RuntimeEndpoint {
const hints = runtimeHints(parsed, env);
assertNoManualUrlWhenLocked(kind, parsed, env);
for (const candidate of endpointCandidates(kind, parsed, env, hints)) {
const baseUrl = normalizeUrl(candidate.value);
if (!baseUrl) continue;
return validateEndpoint({ kind, baseUrl, candidate, hints });
}
throw new RuntimeEndpointError(
"runtime_endpoint_required",
`HWLAB ${kind} endpoint could not be resolved from the current runtime context`,
{
kind,
lane: hints.lane,
rawLane: hints.rawLane,
namespace: hints.namespace,
rawNamespace: hints.rawNamespace,
required: kind === "web"
? ["HWLAB_RUNTIME_WEB_URL", "HWLAB_RUNTIME_NAMESPACE", "HWLAB_CLOUD_WEB_URL"]
: ["HWLAB_RUNTIME_API_URL", "HWLAB_RUNTIME_NAMESPACE", "HWLAB_CLOUD_API_URL"],
suggestion: "let WEB/AgentRun assemble HWLAB_RUNTIME_* for the current lane; do not pass endpoint URLs in locked HWLAB runners"
}
);
}
export function runtimeEndpointVisibility(endpoint: RuntimeEndpoint | null | undefined) {
if (!endpoint) return null;
return pruneUndefined({
kind: endpoint.kind,
baseUrl: endpoint.baseUrl,
source: endpoint.source,
sourceName: endpoint.sourceName,
explicitOverride: endpoint.explicitOverride,
lane: endpoint.lane,
namespace: endpoint.namespace,
urlLane: endpoint.urlLane,
urlNamespace: endpoint.urlNamespace,
warnings: endpoint.warnings.length > 0 ? endpoint.warnings : undefined
});
}
export function runtimeEndpointScopeForUrl(value: unknown) {
const baseUrl = normalizeUrl(value);
if (!baseUrl) return { baseUrl: null, lane: null, namespace: null };
const namespace = namespaceForUrl(baseUrl);
const lane = laneForUrl(baseUrl) ?? laneForNamespace(namespace);
return { baseUrl, lane, namespace };
}
export function sameRuntimeEndpointScope(left: unknown, right: unknown) {
const leftScope = runtimeEndpointScopeForUrl(left);
const rightScope = runtimeEndpointScopeForUrl(right);
if (!leftScope.baseUrl || !rightScope.baseUrl) return false;
if (leftScope.baseUrl === rightScope.baseUrl) return true;
if (leftScope.namespace && rightScope.namespace) return leftScope.namespace === rightScope.namespace;
return Boolean(leftScope.lane && rightScope.lane && leftScope.lane === rightScope.lane);
}
export function runtimeHints(parsed: ParsedArgs = {}, env: EnvLike = {}): RuntimeHints {
const rawNamespace = firstText(
parsed.runtimeNamespace,
parsed.hwlabNamespace,
env.HWLAB_RUNTIME_NAMESPACE,
env.HWLAB_NAMESPACE,
validHwlabNamespace(env.POD_NAMESPACE),
validHwlabNamespace(env.KUBERNETES_NAMESPACE)
);
const rawLane = firstText(
parsed.runtimeLane,
parsed.runtimeProfile,
parsed.hwlabLane,
env.HWLAB_RUNTIME_LANE,
env.HWLAB_RUNTIME_PROFILE,
env.HWLAB_GITOPS_LANE,
env.HWLAB_ARTIFACT_LANE,
env.HWLAB_GITOPS_PROFILE,
env.HWLAB_ENVIRONMENT,
env.HWLAB_BOOT_REF
);
const laneFromRaw = normalizeLane(rawLane);
const namespace = normalizeNamespace(rawNamespace) ?? namespaceForLane(laneFromRaw);
return {
lane: laneFromRaw ?? laneForNamespace(namespace),
rawLane: rawLane || null,
namespace,
rawNamespace: rawNamespace || null
};
}
function endpointCandidates(kind: RuntimeEndpointKind, parsed: ParsedArgs, env: EnvLike, hints: RuntimeHints): EndpointCandidate[] {
const namespaceUrl = hints.namespace ? runtimeUrlForNamespace(kind, hints.namespace, parsed, env) : null;
if (kind === "web") {
return withLocalDebugOverride([
candidate(firstText(env.HWLAB_RUNTIME_WEB_URL, env.HWLAB_CURRENT_WEB_URL), "runtime-env", "HWLAB_RUNTIME_WEB_URL"),
candidate(namespaceUrl, "runtime-namespace", "HWLAB_RUNTIME_NAMESPACE"),
candidate(firstText(env.HWLAB_PUBLIC_WEB_URL, env.HWLAB_CLOUD_WEB_PUBLIC_URL), "public-env", "HWLAB_PUBLIC_WEB_URL"),
candidate(firstText(env.HWLAB_CLIENT_BASE_URL, env.HWLAB_CLOUD_WEB_URL, env.HWLAB_CLI_BASE_URL), "env", "HWLAB_CLIENT_BASE_URL/HWLAB_CLOUD_WEB_URL/HWLAB_CLI_BASE_URL")
], candidate(parsed.baseUrl, "explicit-override", "--base-url", true), env);
}
return withLocalDebugOverride([
candidate(firstText(env.HWLAB_RUNTIME_API_URL, env.HWLAB_CURRENT_API_URL), "runtime-env", "HWLAB_RUNTIME_API_URL"),
candidate(namespaceUrl, "runtime-namespace", "HWLAB_RUNTIME_NAMESPACE"),
candidate(firstText(env.HWLAB_PUBLIC_API_URL, env.HWLAB_PUBLIC_ENDPOINT), "public-env", "HWLAB_PUBLIC_API_URL/HWLAB_PUBLIC_ENDPOINT"),
candidate(firstText(env.HWLAB_CLOUD_API_URL, env.HWLAB_CLOUD_API_BASE_URL), "env", "HWLAB_CLOUD_API_URL")
], candidate(firstText(parsed.apiBaseUrl, parsed.apiUrl, parsed.cloudApiUrl, parsed.baseUrl), "explicit-override", "--api-base-url", true), env);
}
function withLocalDebugOverride(candidates: EndpointCandidate[], manual: EndpointCandidate, env: EnvLike) {
if (endpointLocked(env)) return candidates;
return manual.value ? [manual, ...candidates] : candidates;
}
function assertNoManualUrlWhenLocked(kind: RuntimeEndpointKind, parsed: ParsedArgs, env: EnvLike) {
if (!endpointLocked(env)) return;
const manualValue = kind === "web"
? firstText(parsed.baseUrl)
: firstText(parsed.apiBaseUrl, parsed.apiUrl, parsed.cloudApiUrl, parsed.baseUrl);
if (!manualValue) return;
throw new RuntimeEndpointError(
"runtime_endpoint_manual_url_forbidden",
`HWLAB ${kind} endpoint is assembled by runtime context; manual URL parameters are forbidden in locked runner environments`,
{
kind,
forbidden: kind === "web" ? ["--base-url"] : ["--api-base-url", "--api-url", "--cloud-api-url", "--base-url"],
source: "HWLAB_RUNTIME_ENDPOINT_LOCKED",
suggestion: "drop the URL argument and let hwpod-cli/hwlab-cli auto-locate from the assembled runtime env"
}
);
}
function endpointLocked(env: EnvLike) {
return [env.HWLAB_RUNTIME_ENDPOINT_LOCKED, env.HWLAB_CODE_AGENT_ASSEMBLED_RUNTIME].some((value) => ["1", "true", "yes", "locked"].includes(firstText(value).toLowerCase()));
}
function runtimeUrlForNamespace(kind: RuntimeEndpointKind, namespace: string, parsed: ParsedArgs, env: EnvLike) {
if (runningInsideKubernetes(env)) return serviceUrlForNamespace(kind, namespace, parsed, env);
return publicUrlForNamespace(kind, namespace);
}
function runningInsideKubernetes(env: EnvLike) {
return Boolean(firstText(env.KUBERNETES_SERVICE_HOST, env.KUBERNETES_PORT, env.POD_NAMESPACE, env.KUBERNETES_NAMESPACE));
}
function publicUrlForNamespace(kind: RuntimeEndpointKind, namespace: string) {
const lane = laneForNamespace(namespace);
if (lane === "v02") return kind === "web" ? "http://74.48.78.17:19666" : "http://74.48.78.17:19667";
if (lane === "prod") return kind === "web" ? "http://74.48.78.17:18666" : "http://74.48.78.17:18667";
if (lane === "dev") return kind === "web" ? "http://74.48.78.17:17666" : "http://74.48.78.17:17667";
return null;
}
function serviceUrlForNamespace(kind: RuntimeEndpointKind, namespace: string, parsed: ParsedArgs, env: EnvLike) {
if (kind === "web") {
const serviceName = firstText(parsed.webServiceName, env.HWLAB_CLOUD_WEB_SERVICE_NAME) || "hwlab-cloud-web";
const port = positiveInteger(firstText(parsed.webServicePort, env.HWLAB_CLOUD_WEB_PORT), 8080);
return `http://${serviceName}.${namespace}.svc.cluster.local:${port}`;
}
const serviceName = firstText(parsed.apiServiceName, env.HWLAB_CODE_AGENT_INTERNAL_API_SERVICE_NAME, env.HWLAB_CLOUD_API_SERVICE_NAME) || "hwlab-cloud-api";
const port = positiveInteger(firstText(parsed.apiServicePort, env.HWLAB_CODE_AGENT_INTERNAL_API_PORT, env.HWLAB_CLOUD_API_PORT), 6667);
return `http://${serviceName}.${namespace}.svc.cluster.local:${port}`;
}
function validateEndpoint({ kind, baseUrl, candidate, hints }: { kind: RuntimeEndpointKind; baseUrl: string; candidate: EndpointCandidate; hints: RuntimeHints }): RuntimeEndpoint {
const urlLane = laneForUrl(baseUrl);
const urlNamespace = namespaceForUrl(baseUrl);
const explicitOverride = candidate.explicitOverride === true;
const warnings: Array<Record<string, unknown>> = [];
if (!explicitOverride) {
if (hints.namespace && urlNamespace && urlNamespace !== hints.namespace) {
throw endpointMismatch(kind, baseUrl, candidate, hints, { urlLane, urlNamespace, field: "namespace" });
}
if (hints.lane && urlLane && urlLane !== hints.lane) {
throw endpointMismatch(kind, baseUrl, candidate, hints, { urlLane, urlNamespace, field: "lane" });
}
if (!hints.lane && !hints.namespace && candidate.source === "env" && urlLane === "dev") {
throw new RuntimeEndpointError(
"runtime_context_required",
`HWLAB ${kind} endpoint ${candidate.sourceName} points at the legacy DEV lane but no current runtime lane/namespace is set`,
{
kind,
baseUrl,
source: candidate.source,
sourceName: candidate.sourceName,
urlLane,
suggestion: "use assembled HWLAB_RUNTIME_* env for the current lane; manual URL is only a local debug override outside locked runners"
}
);
}
} else if ((hints.lane && urlLane && urlLane !== hints.lane) || (hints.namespace && urlNamespace && urlNamespace !== hints.namespace)) {
warnings.push({
code: "runtime_endpoint_explicit_mismatch",
message: "explicit endpoint override does not match current runtime hints",
expectedLane: hints.lane,
expectedNamespace: hints.namespace,
urlLane,
urlNamespace
});
}
return pruneUndefined({
kind,
baseUrl,
source: candidate.source,
sourceName: candidate.sourceName,
explicitOverride,
lane: hints.lane,
namespace: hints.namespace,
urlLane,
urlNamespace,
warnings
}) as RuntimeEndpoint;
}
function endpointMismatch(kind: RuntimeEndpointKind, baseUrl: string, candidate: EndpointCandidate, hints: RuntimeHints, observed: Record<string, unknown>) {
return new RuntimeEndpointError(
"runtime_endpoint_mismatch",
`HWLAB ${kind} endpoint source ${candidate.sourceName} does not match the current runtime lane/namespace`,
{
kind,
baseUrl,
source: candidate.source,
sourceName: candidate.sourceName,
expectedLane: hints.lane,
expectedNamespace: hints.namespace,
rawLane: hints.rawLane,
rawNamespace: hints.rawNamespace,
...observed,
suggestion: "fix the runtime env injected by WEB/AgentRun; manual URL is only a local debug override outside locked runners"
}
);
}
function candidate(value: unknown, source: string, sourceName: string, explicitOverride = false): EndpointCandidate {
return { value: firstText(value), source, sourceName, explicitOverride };
}
function normalizeUrl(value: unknown) {
const raw = firstText(value);
if (!raw) return null;
return raw.replace(/\/+$/u, "");
}
function laneForUrl(value: string) {
const text = value.toLowerCase();
if (/hwlab-v02|:19666(?:\/|$)|:19667(?:\/|$)/u.test(text)) return "v02";
if (/hwlab-prod|:18666(?:\/|$)|:18667(?:\/|$)/u.test(text)) return "prod";
if (/hwlab-dev|:17666(?:\/|$)|:17667(?:\/|$)|:16666(?:\/|$)|:16667(?:\/|$)/u.test(text)) return "dev";
return null;
}
function namespaceForUrl(value: string) {
const match = value.match(/\.((?:hwlab-[a-z0-9-]+))\.svc\.cluster\.local(?::|\/|$)/iu);
return normalizeNamespace(match?.[1]);
}
function normalizeNamespace(value: unknown) {
const namespace = firstText(value);
if (!namespace || !/^hwlab-[a-z0-9-]+$/u.test(namespace)) return null;
return namespace;
}
function validHwlabNamespace(value: unknown) {
return normalizeNamespace(value) ?? null;
}
function normalizeLane(value: unknown) {
const raw = firstText(value).toLowerCase();
if (!raw) return null;
if (["v02", "v0.2", "0.2", "v2", "v0_2", "v0-2"].includes(raw)) return "v02";
if (["g14", "dev", "development"].includes(raw)) return "dev";
if (["prod", "production"].includes(raw)) return "prod";
return null;
}
function namespaceForLane(lane: string | null) {
if (lane === "v02") return "hwlab-v02";
if (lane === "prod") return "hwlab-prod";
if (lane === "dev") return "hwlab-dev";
return null;
}
function laneForNamespace(namespace: string | null) {
if (namespace === "hwlab-v02") return "v02";
if (namespace === "hwlab-prod") return "prod";
if (namespace === "hwlab-dev") return "dev";
return null;
}
function positiveInteger(value: unknown, fallback: number) {
const parsed = Number.parseInt(firstText(value), 10);
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
}
function firstText(...values: unknown[]) {
for (const value of values) {
const text = String(value ?? "").trim();
if (text) return text;
}
return "";
}
function pruneUndefined<T extends Record<string, unknown>>(value: T): T {
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined)) as T;
}