mirror of
https://github.com/iamlukethedev/Claw3D.git
synced 2026-07-30 11:12:32 +00:00
feat: formalize runtime profile resolution
This commit is contained in:
@@ -14,6 +14,10 @@ import type {
|
||||
StudioSettingsPatch,
|
||||
StudioSettingsPublic,
|
||||
} from "@/lib/studio/settings";
|
||||
import {
|
||||
resolveDefaultStudioGatewayProfile,
|
||||
resolveStudioGatewayProfiles,
|
||||
} from "@/lib/studio/settings";
|
||||
import type {
|
||||
StudioSettingsLoadOptions,
|
||||
StudioSettingsResponse,
|
||||
@@ -115,7 +119,6 @@ const parseConnectFailedCloseReason = (
|
||||
|
||||
const DEFAULT_UPSTREAM_GATEWAY_URL =
|
||||
process.env.NEXT_PUBLIC_GATEWAY_URL || "ws://localhost:18789";
|
||||
const DEFAULT_CUSTOM_RUNTIME_URL = "http://localhost:7770";
|
||||
const INITIAL_AUTO_CONNECT_DELAY_MS = 900;
|
||||
const INITIAL_CONNECT_RETRY_DELAY_MS = 1_200;
|
||||
const OPENCLAW_CONTROL_UI_CLIENT_ID = "openclaw-control-ui";
|
||||
@@ -162,25 +165,6 @@ export const resolveInitialGatewayConnectAttemptCount = (
|
||||
}
|
||||
};
|
||||
|
||||
const resolveDefaultGatewayProfile = (
|
||||
adapterType: StudioGatewayAdapterType,
|
||||
localDefaults: StudioGatewaySettings | null
|
||||
): { url: string; token: string } => {
|
||||
switch (adapterType) {
|
||||
case "custom":
|
||||
return { url: DEFAULT_CUSTOM_RUNTIME_URL, token: "" };
|
||||
case "demo":
|
||||
case "hermes":
|
||||
return { url: "ws://localhost:18789", token: "" };
|
||||
case "openclaw":
|
||||
default:
|
||||
return {
|
||||
url: localDefaults?.url || DEFAULT_UPSTREAM_GATEWAY_URL,
|
||||
token: localDefaults?.token ?? "",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const normalizeLocalGatewayDefaults = (value: unknown): StudioGatewaySettings | null => {
|
||||
if (!value || typeof value !== "object") return null;
|
||||
const raw = value as {
|
||||
@@ -734,7 +718,7 @@ export const useGatewayConnection = (
|
||||
(value: StudioGatewayAdapterType) => {
|
||||
setSelectedAdapterTypeState(value);
|
||||
const profile =
|
||||
adapterProfiles[value] ?? resolveDefaultGatewayProfile(value, localGatewayDefaults);
|
||||
adapterProfiles[value] ?? resolveDefaultStudioGatewayProfile(value, localGatewayDefaults);
|
||||
setGatewayUrl(profile.url);
|
||||
setToken(profile.token);
|
||||
setError(null);
|
||||
@@ -780,72 +764,45 @@ export const useGatewayConnection = (
|
||||
: "openclaw",
|
||||
}
|
||||
: null;
|
||||
// When the user has no saved gateway URL, prefer the runtime
|
||||
// localGatewayDefaults (from openclaw.json, CLAW3D_GATEWAY_URL,
|
||||
// or detected local Hermes/demo adapter ports)
|
||||
// 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 savedAdapterType =
|
||||
hasSavedUrl && gateway && "adapterType" in gateway && typeof gateway.adapterType === "string"
|
||||
? ((gateway.adapterType === "demo" ||
|
||||
const gatewaySettings: StudioGatewaySettings | null = gateway
|
||||
? {
|
||||
url: typeof gateway.url === "string" ? gateway.url.trim() : "",
|
||||
token: "",
|
||||
adapterType:
|
||||
gateway.adapterType === "demo" ||
|
||||
gateway.adapterType === "hermes" ||
|
||||
gateway.adapterType === "openclaw" ||
|
||||
gateway.adapterType === "custom"
|
||||
? gateway.adapterType
|
||||
: "openclaw") as StudioGatewayAdapterType)
|
||||
: null;
|
||||
const nextAdapterType =
|
||||
savedAdapterType ??
|
||||
lastKnownGood?.adapterType ??
|
||||
normalizedDefaults?.adapterType ??
|
||||
"openclaw";
|
||||
const lastKnownGoodForSelectedAdapter =
|
||||
lastKnownGood?.adapterType === nextAdapterType ? lastKnownGood : null;
|
||||
const resolvedUrl = hasSavedUrl
|
||||
? gateway!.url
|
||||
: lastKnownGoodForSelectedAdapter?.url ||
|
||||
normalizedDefaults?.url ||
|
||||
DEFAULT_UPSTREAM_GATEWAY_URL;
|
||||
const baseProfiles = {
|
||||
...(gateway?.profiles
|
||||
? normalizeGatewayProfilesPublic(gateway.profiles)
|
||||
: undefined),
|
||||
...(normalizedDefaults?.profiles ?? {}),
|
||||
};
|
||||
const mergedProfiles = {
|
||||
...baseProfiles,
|
||||
...(hasSavedUrl
|
||||
? {
|
||||
[nextAdapterType]: {
|
||||
url: resolvedUrl,
|
||||
token:
|
||||
gateway && "token" in gateway && typeof gateway.token === "string"
|
||||
? gateway.token
|
||||
: "",
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
const selectedProfile = (
|
||||
mergedProfiles[nextAdapterType] ??
|
||||
lastKnownGoodForSelectedAdapter ??
|
||||
resolveDefaultGatewayProfile(nextAdapterType, normalizedDefaults)
|
||||
);
|
||||
? gateway.adapterType
|
||||
: "openclaw",
|
||||
...(gateway?.profiles
|
||||
? { profiles: normalizeGatewayProfilesPublic(gateway.profiles) }
|
||||
: {}),
|
||||
...(lastKnownGood ? { lastKnownGood } : {}),
|
||||
}
|
||||
: null;
|
||||
const resolvedGatewayProfiles = resolveStudioGatewayProfiles({
|
||||
gateway: gatewaySettings,
|
||||
localDefaults: normalizedDefaults,
|
||||
});
|
||||
const nextAdapterType = resolvedGatewayProfiles.selectedAdapterType;
|
||||
const selectedProfile =
|
||||
resolvedGatewayProfiles.activeProfile ??
|
||||
resolveDefaultStudioGatewayProfile(nextAdapterType, normalizedDefaults);
|
||||
const nextGatewayUrl = selectedProfile.url;
|
||||
const nextToken = selectedProfile.token;
|
||||
loadedGatewaySettings.current = {
|
||||
gatewayUrl: nextGatewayUrl.trim(),
|
||||
token: nextToken,
|
||||
adapterType: nextAdapterType,
|
||||
profiles: mergedProfiles,
|
||||
hasLastKnownGood: Boolean(lastKnownGoodForSelectedAdapter?.url),
|
||||
profiles: resolvedGatewayProfiles.profiles,
|
||||
hasLastKnownGood: Boolean(resolvedGatewayProfiles.lastKnownGoodForSelected?.url),
|
||||
};
|
||||
setGatewayUrl(nextGatewayUrl);
|
||||
setToken(nextToken);
|
||||
setSelectedAdapterTypeState(nextAdapterType);
|
||||
setAdapterProfiles(mergedProfiles);
|
||||
setHasLastKnownGoodState(Boolean(lastKnownGoodForSelectedAdapter?.url));
|
||||
setAdapterProfiles(resolvedGatewayProfiles.profiles);
|
||||
setHasLastKnownGoodState(Boolean(resolvedGatewayProfiles.lastKnownGoodForSelected?.url));
|
||||
} catch (err) {
|
||||
if (!cancelled) {
|
||||
const message = err instanceof Error ? err.message : "Failed to load gateway settings.";
|
||||
|
||||
@@ -24,6 +24,7 @@ export type StudioGatewaySettings = {
|
||||
};
|
||||
|
||||
export type StudioGatewayAdapterType = "openclaw" | "hermes" | "demo" | "custom";
|
||||
export const STUDIO_GATEWAY_ADAPTER_TYPES = ["openclaw", "hermes", "demo", "custom"] as const;
|
||||
|
||||
export type StudioGatewayProfile = {
|
||||
url: string;
|
||||
@@ -74,6 +75,13 @@ export type StudioGatewayConnectionStatePatch = {
|
||||
adapterType?: StudioGatewayAdapterType | null;
|
||||
};
|
||||
|
||||
export type ResolvedStudioGatewayProfiles = {
|
||||
selectedAdapterType: StudioGatewayAdapterType;
|
||||
activeProfile: StudioGatewayProfile;
|
||||
profiles: Partial<Record<StudioGatewayAdapterType, StudioGatewayProfile>>;
|
||||
lastKnownGoodForSelected: StudioGatewayConnectionState | null;
|
||||
};
|
||||
|
||||
export type FocusFilter = "all" | "running" | "approvals";
|
||||
export type StudioViewMode = "focused";
|
||||
|
||||
@@ -222,6 +230,9 @@ export type StudioSettingsPatch = {
|
||||
};
|
||||
|
||||
const SETTINGS_VERSION = 1 as const;
|
||||
const DEFAULT_OPENCLAW_GATEWAY_URL = "ws://localhost:18789";
|
||||
const DEFAULT_LOCAL_ADAPTER_GATEWAY_URL = "ws://localhost:18789";
|
||||
const DEFAULT_CUSTOM_RUNTIME_URL = "http://localhost:7770";
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
Boolean(value && typeof value === "object");
|
||||
@@ -830,6 +841,87 @@ const normalizeGatewayAdapterType = (
|
||||
return fallback;
|
||||
};
|
||||
|
||||
export const resolveDefaultStudioGatewayProfile = (
|
||||
adapterType: StudioGatewayAdapterType,
|
||||
localDefaults: StudioGatewaySettings | null = null
|
||||
): StudioGatewayProfile => {
|
||||
const explicitProfile = localDefaults?.profiles?.[adapterType];
|
||||
if (explicitProfile?.url) {
|
||||
return {
|
||||
url: explicitProfile.url,
|
||||
token: explicitProfile.token,
|
||||
};
|
||||
}
|
||||
|
||||
if (localDefaults?.adapterType === adapterType && localDefaults.url.trim()) {
|
||||
return {
|
||||
url: localDefaults.url,
|
||||
token: localDefaults.token,
|
||||
};
|
||||
}
|
||||
|
||||
switch (adapterType) {
|
||||
case "custom":
|
||||
return { url: DEFAULT_CUSTOM_RUNTIME_URL, token: "" };
|
||||
case "hermes":
|
||||
case "demo":
|
||||
return { url: DEFAULT_LOCAL_ADAPTER_GATEWAY_URL, token: "" };
|
||||
case "openclaw":
|
||||
default:
|
||||
return { url: DEFAULT_OPENCLAW_GATEWAY_URL, token: "" };
|
||||
}
|
||||
};
|
||||
|
||||
export const resolveStudioGatewayProfiles = ({
|
||||
gateway,
|
||||
localDefaults = null,
|
||||
}: {
|
||||
gateway: StudioGatewaySettings | null;
|
||||
localDefaults?: StudioGatewaySettings | null;
|
||||
}): ResolvedStudioGatewayProfiles => {
|
||||
const selectedAdapterType =
|
||||
gateway?.adapterType ??
|
||||
gateway?.lastKnownGood?.adapterType ??
|
||||
localDefaults?.adapterType ??
|
||||
"openclaw";
|
||||
|
||||
const profiles: Partial<Record<StudioGatewayAdapterType, StudioGatewayProfile>> = {
|
||||
...(localDefaults?.profiles ?? {}),
|
||||
...(gateway?.profiles ?? {}),
|
||||
};
|
||||
|
||||
if (gateway?.url?.trim()) {
|
||||
profiles[selectedAdapterType] = {
|
||||
url: gateway.url,
|
||||
token: gateway.token,
|
||||
};
|
||||
}
|
||||
|
||||
const lastKnownGoodForSelected =
|
||||
gateway?.lastKnownGood?.adapterType === selectedAdapterType ? gateway.lastKnownGood : null;
|
||||
|
||||
if (!profiles[selectedAdapterType] && lastKnownGoodForSelected?.url) {
|
||||
profiles[selectedAdapterType] = {
|
||||
url: lastKnownGoodForSelected.url,
|
||||
token: lastKnownGoodForSelected.token,
|
||||
};
|
||||
}
|
||||
|
||||
for (const adapterType of STUDIO_GATEWAY_ADAPTER_TYPES) {
|
||||
if (profiles[adapterType]?.url) continue;
|
||||
profiles[adapterType] = resolveDefaultStudioGatewayProfile(adapterType, localDefaults);
|
||||
}
|
||||
|
||||
return {
|
||||
selectedAdapterType,
|
||||
activeProfile:
|
||||
profiles[selectedAdapterType] ??
|
||||
resolveDefaultStudioGatewayProfile(selectedAdapterType, localDefaults),
|
||||
profiles,
|
||||
lastKnownGoodForSelected,
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeFocused = (value: unknown): Record<string, StudioFocusedPreference> => {
|
||||
if (!isRecord(value)) return {};
|
||||
const focused: Record<string, StudioFocusedPreference> = {};
|
||||
|
||||
@@ -4,6 +4,8 @@ import { createDefaultAgentAvatarProfile } from "@/lib/avatars/profile";
|
||||
import {
|
||||
mergeStudioSettings,
|
||||
normalizeStudioSettings,
|
||||
resolveDefaultStudioGatewayProfile,
|
||||
resolveStudioGatewayProfiles,
|
||||
} from "@/lib/studio/settings";
|
||||
|
||||
describe("studio settings normalization", () => {
|
||||
@@ -300,4 +302,45 @@ describe("studio settings normalization", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("resolves simultaneous runtime profiles without collapsing them to one active url", () => {
|
||||
const resolved = resolveStudioGatewayProfiles({
|
||||
gateway: normalizeStudioSettings({
|
||||
gateway: {
|
||||
url: "ws://localhost:28789",
|
||||
token: "",
|
||||
adapterType: "hermes",
|
||||
profiles: {
|
||||
openclaw: { url: "ws://localhost:18789", token: "open-token" },
|
||||
demo: { url: "ws://localhost:38789", token: "" },
|
||||
},
|
||||
},
|
||||
}).gateway,
|
||||
localDefaults: null,
|
||||
});
|
||||
|
||||
expect(resolved.selectedAdapterType).toBe("hermes");
|
||||
expect(resolved.activeProfile).toEqual({
|
||||
url: "ws://localhost:28789",
|
||||
token: "",
|
||||
});
|
||||
expect(resolved.profiles).toEqual(
|
||||
expect.objectContaining({
|
||||
openclaw: { url: "ws://localhost:18789", token: "open-token" },
|
||||
hermes: { url: "ws://localhost:28789", token: "" },
|
||||
demo: { url: "ws://localhost:38789", token: "" },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("resolves adapter-specific defaults for dormant profiles", () => {
|
||||
expect(resolveDefaultStudioGatewayProfile("custom", null)).toEqual({
|
||||
url: "http://localhost:7770",
|
||||
token: "",
|
||||
});
|
||||
expect(resolveDefaultStudioGatewayProfile("demo", null)).toEqual({
|
||||
url: "ws://localhost:18789",
|
||||
token: "",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user