fix: resolve gateway URL at runtime via /api/studio fallback (#66)

Fixes #57 — NEXT_PUBLIC_GATEWAY_URL is a build-time variable that gets
baked into the client bundle. Changing it in .env and restarting has no
effect without a rebuild.

- normalizeLocalGatewayDefaults now accepts the sanitized public form
  ({url, tokenConfigured}) from /api/studio
- When no saved gateway URL exists, prefer runtime localGatewayDefaults
  (from openclaw.json or CLAW3D_GATEWAY_URL env var) over the
  potentially stale build-time NEXT_PUBLIC_GATEWAY_URL
- loadLocalGatewayDefaults falls back to CLAW3D_GATEWAY_URL/TOKEN env
  vars when openclaw.json is absent
- Added runtime env vars documentation to .env.example and README

Co-authored-by: robotica4us-collab <neo@openclaw.ai>
Made-with: Cursor
This commit is contained in:
iamlukethedev
2026-03-27 14:45:02 -05:00
committed by iamlukethedev
parent 456cfae771
commit e71b62444c
7 changed files with 152 additions and 12 deletions
+22 -8
View File
@@ -103,10 +103,14 @@ const DEFAULT_UPSTREAM_GATEWAY_URL =
const normalizeLocalGatewayDefaults = (value: unknown): StudioGatewaySettings | null => {
if (!value || typeof value !== "object") return null;
const raw = value as { url?: unknown; token?: unknown };
const raw = value as { url?: unknown; token?: unknown; tokenConfigured?: unknown };
const url = typeof raw.url === "string" ? raw.url.trim() : "";
if (!url) return null;
// Accept both full settings ({ url, token }) and the sanitized public
// form ({ url, tokenConfigured }) returned by /api/studio. When only
// tokenConfigured is present the actual token isn't available on the
// client — leave it empty so the connection dialog can prompt if needed.
const token = typeof raw.token === "string" ? raw.token.trim() : "";
if (!url || !token) return null;
return { url, token };
};
@@ -548,12 +552,22 @@ export const useGatewayConnection = (
const settings = envelope.settings ?? null;
const gateway = settings?.gateway ?? null;
if (cancelled) return;
setLocalGatewayDefaults(normalizeLocalGatewayDefaults(envelope.localGatewayDefaults));
const nextGatewayUrl = gateway?.url?.trim() ? gateway.url : DEFAULT_UPSTREAM_GATEWAY_URL;
const nextToken =
gateway && "token" in gateway && typeof gateway.token === "string"
? gateway.token
: "";
const normalizedDefaults = normalizeLocalGatewayDefaults(envelope.localGatewayDefaults);
setLocalGatewayDefaults(normalizedDefaults);
// When the user has no saved gateway URL, prefer the runtime
// localGatewayDefaults (from openclaw.json / CLAW3D_GATEWAY_URL)
// over the build-time NEXT_PUBLIC_GATEWAY_URL which may be stale
// or empty if the operator forgot to rebuild after .env changes.
const hasSavedUrl = Boolean(gateway?.url?.trim());
const resolvedUrl = hasSavedUrl
? gateway!.url
: normalizedDefaults?.url || DEFAULT_UPSTREAM_GATEWAY_URL;
const nextGatewayUrl = resolvedUrl;
const nextToken = hasSavedUrl
? (gateway && "token" in gateway && typeof gateway.token === "string"
? gateway.token
: "")
: normalizedDefaults?.token ?? "";
loadedGatewaySettings.current = {
gatewayUrl: nextGatewayUrl.trim(),
token: nextToken,
+9 -2
View File
@@ -44,8 +44,15 @@ const readOpenclawGatewayDefaults = (): { url: string; token: string } | null =>
}
};
export const loadLocalGatewayDefaults = () => {
return readOpenclawGatewayDefaults();
export const loadLocalGatewayDefaults = (): { url: string; token: string } | null => {
const fromFile = readOpenclawGatewayDefaults();
if (fromFile) return fromFile;
// Fall back to env vars so operators can configure the gateway URL at
// runtime without openclaw.json and without a rebuild.
const envUrl = process.env.CLAW3D_GATEWAY_URL?.trim();
const envToken = process.env.CLAW3D_GATEWAY_TOKEN?.trim();
if (envUrl) return { url: envUrl, token: envToken ?? "" };
return null;
};
export const loadStudioSettings = (): StudioSettings => {