diff --git a/apps/server/integration/OrchestrationEngineHarness.integration.ts b/apps/server/integration/OrchestrationEngineHarness.integration.ts index b8f94d22c..b76a735bd 100644 --- a/apps/server/integration/OrchestrationEngineHarness.integration.ts +++ b/apps/server/integration/OrchestrationEngineHarness.integration.ts @@ -40,6 +40,7 @@ import { ProviderUnsupportedError } from "../src/provider/Errors.ts"; import { ProviderAdapterRegistry } from "../src/provider/Services/ProviderAdapterRegistry.ts"; import { ProviderRuntimeEventFeedLive } from "../src/provider/Layers/ProviderRuntimeEventFeed.ts"; import { ProviderSessionDirectoryLive } from "../src/provider/Layers/ProviderSessionDirectory.ts"; +import { ProviderRuntimeEventFeedLive } from "../src/provider/Layers/ProviderRuntimeEventFeed.ts"; import { makeProviderServiceLive } from "../src/provider/Layers/ProviderService.ts"; import { makeCodexAdapterLive } from "../src/provider/Layers/CodexAdapter.ts"; import { CodexAdapter } from "../src/provider/Services/CodexAdapter.ts"; diff --git a/apps/server/integration/providerService.integration.test.ts b/apps/server/integration/providerService.integration.test.ts index 73ae2f629..ee9352c58 100644 --- a/apps/server/integration/providerService.integration.test.ts +++ b/apps/server/integration/providerService.integration.test.ts @@ -8,6 +8,7 @@ import { ProviderUnsupportedError } from "../src/provider/Errors.ts"; import { ProviderAdapterRegistry } from "../src/provider/Services/ProviderAdapterRegistry.ts"; import { ProviderRuntimeEventFeedLive } from "../src/provider/Layers/ProviderRuntimeEventFeed.ts"; import { ProviderSessionDirectoryLive } from "../src/provider/Layers/ProviderSessionDirectory.ts"; +import { ProviderRuntimeEventFeedLive } from "../src/provider/Layers/ProviderRuntimeEventFeed.ts"; import { makeProviderServiceLive } from "../src/provider/Layers/ProviderService.ts"; import { ProviderService, diff --git a/apps/server/src/openclaw/GatewayClient.ts b/apps/server/src/openclaw/GatewayClient.ts index 3bff51c5f..734bbee23 100644 --- a/apps/server/src/openclaw/GatewayClient.ts +++ b/apps/server/src/openclaw/GatewayClient.ts @@ -466,7 +466,7 @@ export class OpenclawGatewayClient { if (frame.type === "event" && typeof frame.event === "string") { let matchedWaiter = false; - for (const waiter of this.pendingEventWaiters) { + for (const waiter of [...this.pendingEventWaiters]) { if (waiter.eventName === frame.event) { matchedWaiter = true; this.pendingEventWaiters.delete(waiter); diff --git a/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts b/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts index 3a1cfa6e8..6ed0d6228 100644 --- a/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts +++ b/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts @@ -99,6 +99,7 @@ function createProviderServiceHarness( return { service, + emit: (event: LegacyProviderRuntimeEvent) => event, rollbackConversation, }; } diff --git a/apps/server/src/persistence/Layers/OpenclawGatewayConfig.ts b/apps/server/src/persistence/Layers/OpenclawGatewayConfig.ts index 310e0939d..26ec67d32 100644 --- a/apps/server/src/persistence/Layers/OpenclawGatewayConfig.ts +++ b/apps/server/src/persistence/Layers/OpenclawGatewayConfig.ts @@ -68,7 +68,7 @@ function normalizeScopes(scopes: ReadonlyArray | undefined): string[] { unique.add(trimmed); } } - return [...unique].toSorted((left, right) => left.localeCompare(right)); + return [...unique].sort((left, right) => left.localeCompare(right)); } function fromGeneratedIdentity(identity: ReturnType) { diff --git a/apps/web/src/routes/_chat.settings.tsx b/apps/web/src/routes/_chat.settings.tsx index 6569ebd66..9a1366116 100644 --- a/apps/web/src/routes/_chat.settings.tsx +++ b/apps/web/src/routes/_chat.settings.tsx @@ -108,7 +108,11 @@ import { getSelectableThreadProviders, isProviderReadyForThreadSelection, } from "../lib/providerAvailability"; -import { serverConfigQueryOptions, serverQueryKeys } from "../lib/serverReactQuery"; +import { + openclawGatewayConfigQueryOptions, + serverConfigQueryOptions, + serverQueryKeys, +} from "../lib/serverReactQuery"; import { cn } from "../lib/utils"; import { ensureNativeApi, readNativeApi } from "../nativeApi"; import { useStore } from "../store"; @@ -797,6 +801,7 @@ function SettingsRouteView() { const { theme, setTheme, colorTheme, setColorTheme, fontFamily, setFontFamily } = useTheme(); const { settings, defaults, updateSettings, resetSettings } = useAppSettings(); const serverConfigQuery = useQuery(serverConfigQueryOptions()); + const openclawGatewayConfigQuery = useQuery(openclawGatewayConfigQueryOptions()); const queryClient = useQueryClient(); const trimmedBrowserPreviewStartPageUrl = settings.browserPreviewStartPageUrl.trim(); const browserPreviewStartPageValidation = @@ -847,6 +852,12 @@ function SettingsRouteView() { null, ); const [openclawTestLoading, setOpenclawTestLoading] = useState(false); + const [openclawGatewayDraft, setOpenclawGatewayDraft] = useState(null); + const [openclawSharedSecretDraft, setOpenclawSharedSecretDraft] = useState(""); + const [openclawSaveLoading, setOpenclawSaveLoading] = useState(false); + const [openclawResetLoading, setOpenclawResetLoading] = useState<"token" | "identity" | null>( + null, + ); const { copyToClipboard: copyOpenclawDebugReport, isCopied: openclawDebugReportCopied } = useCopyToClipboard(); @@ -887,14 +898,21 @@ function SettingsRouteView() { CLAUDE_AUTH_TOKEN_HELPER_PRESETS.find( (preset) => preset.command === claudeAuthTokenHelperCommand, )?.label ?? ""; + const savedOpenclawGatewayUrl = openclawGatewayConfigQuery.data?.gatewayUrl ?? ""; + const savedOpenclawHasSharedSecret = openclawGatewayConfigQuery.data?.hasSharedSecret ?? false; + const effectiveOpenclawGatewayUrl = openclawGatewayDraft ?? savedOpenclawGatewayUrl; const keybindingsConfigPath = serverConfigQuery.data?.keybindingsConfigPath ?? null; const availableEditors = serverConfigQuery.data?.availableEditors; const providerStatuses = serverConfigQuery.data?.providers ?? []; const selectableProviders = getSelectableThreadProviders({ statuses: providerStatuses, - openclawGatewayUrl: settings.openclawGatewayUrl, + openclawGatewayUrl: effectiveOpenclawGatewayUrl, claudeAuthTokenHelperCommand: settings.claudeAuthTokenHelperCommand, }); + const canImportLegacyOpenclawSettings = + openclawGatewayConfigQuery.isSuccess && + !savedOpenclawGatewayUrl && + Boolean(settings.openclawGatewayUrl || settings.openclawPassword); const gitTextGenerationModelOptions = getAppModelOptions( "codex", @@ -934,8 +952,8 @@ function SettingsRouteView() { settings.codexBinaryPath !== defaults.codexBinaryPath || settings.codexHomePath !== defaults.codexHomePath; const isOpenClawSettingsDirty = - settings.openclawGatewayUrl !== defaults.openclawGatewayUrl || - settings.openclawPassword !== defaults.openclawPassword; + (openclawGatewayDraft !== null && openclawGatewayDraft !== savedOpenclawGatewayUrl) || + openclawSharedSecretDraft.length > 0; const changedSettingLabels = [ ...(theme !== "system" ? ["Theme"] : []), ...(colorTheme !== DEFAULT_COLOR_THEME ? ["Color theme"] : []), @@ -1090,9 +1108,11 @@ function SettingsRouteView() { setOpenclawTestResult(null); try { const api = ensureNativeApi(); + const gatewayUrl = effectiveOpenclawGatewayUrl.trim(); + const sharedSecret = openclawSharedSecretDraft.trim(); const result = await api.server.testOpenclawGateway({ - gatewayUrl: settings.openclawGatewayUrl, - password: settings.openclawPassword || undefined, + ...(gatewayUrl ? { gatewayUrl } : {}), + password: sharedSecret || undefined, }); setOpenclawTestResult(result); } catch (err) { @@ -1105,13 +1125,110 @@ function SettingsRouteView() { } finally { setOpenclawTestLoading(false); } - }, [openclawTestLoading, settings.openclawGatewayUrl, settings.openclawPassword]); + }, [effectiveOpenclawGatewayUrl, openclawSharedSecretDraft, openclawTestLoading]); const handleCopyOpenclawDebugReport = useCallback(() => { if (!openclawTestResult) return; copyOpenclawDebugReport(formatOpenclawGatewayDebugReport(openclawTestResult), undefined); }, [copyOpenclawDebugReport, openclawTestResult]); + const saveOpenclawGatewayConfig = useCallback(async () => { + if (openclawSaveLoading) return; + const gatewayUrl = effectiveOpenclawGatewayUrl.trim(); + if (!gatewayUrl) { + throw new Error("Gateway URL is required."); + } + + setOpenclawSaveLoading(true); + try { + const api = ensureNativeApi(); + const sharedSecret = openclawSharedSecretDraft.trim(); + const summary = await api.server.saveOpenclawGatewayConfig({ + gatewayUrl, + ...(sharedSecret ? { sharedSecret } : {}), + }); + queryClient.setQueryData(serverQueryKeys.openclawGatewayConfig(), summary); + setOpenclawGatewayDraft(null); + setOpenclawSharedSecretDraft(""); + setOpenclawTestResult(null); + } finally { + setOpenclawSaveLoading(false); + } + }, [effectiveOpenclawGatewayUrl, openclawSaveLoading, openclawSharedSecretDraft, queryClient]); + + const clearSavedOpenclawSharedSecret = useCallback(async () => { + const gatewayUrl = effectiveOpenclawGatewayUrl.trim(); + if (!gatewayUrl) { + throw new Error("Gateway URL is required before clearing the saved secret."); + } + + setOpenclawSaveLoading(true); + try { + const api = ensureNativeApi(); + const summary = await api.server.saveOpenclawGatewayConfig({ + gatewayUrl, + clearSharedSecret: true, + }); + queryClient.setQueryData(serverQueryKeys.openclawGatewayConfig(), summary); + setOpenclawSharedSecretDraft(""); + setOpenclawTestResult(null); + } finally { + setOpenclawSaveLoading(false); + } + }, [effectiveOpenclawGatewayUrl, queryClient]); + + const resetOpenclawDeviceState = useCallback( + async (regenerateIdentity: boolean) => { + if (openclawResetLoading) return; + setOpenclawResetLoading(regenerateIdentity ? "identity" : "token"); + try { + const api = ensureNativeApi(); + const summary = await api.server.resetOpenclawGatewayDeviceState({ + regenerateIdentity, + }); + queryClient.setQueryData(serverQueryKeys.openclawGatewayConfig(), summary); + setOpenclawTestResult(null); + } finally { + setOpenclawResetLoading(null); + } + }, + [openclawResetLoading, queryClient], + ); + + const importLegacyOpenclawSettings = useCallback(async () => { + const gatewayUrl = settings.openclawGatewayUrl.trim(); + if (!gatewayUrl) { + throw new Error("Legacy OpenClaw settings do not contain a gateway URL."); + } + + setOpenclawSaveLoading(true); + try { + const api = ensureNativeApi(); + const sharedSecret = settings.openclawPassword.trim(); + const summary = await api.server.saveOpenclawGatewayConfig({ + gatewayUrl, + ...(sharedSecret ? { sharedSecret } : {}), + }); + queryClient.setQueryData(serverQueryKeys.openclawGatewayConfig(), summary); + updateSettings({ + openclawGatewayUrl: defaults.openclawGatewayUrl, + openclawPassword: defaults.openclawPassword, + }); + setOpenclawGatewayDraft(null); + setOpenclawSharedSecretDraft(""); + setOpenclawTestResult(null); + } finally { + setOpenclawSaveLoading(false); + } + }, [ + defaults.openclawGatewayUrl, + defaults.openclawPassword, + queryClient, + settings.openclawGatewayUrl, + settings.openclawPassword, + updateSettings, + ]); + const addCustomModel = useCallback( (provider: ProviderKind) => { const customModelInput = customModelInputByProvider[provider]; @@ -2499,7 +2616,7 @@ function SettingsRouteView() { providerStatuses.find((status) => status.provider === provider) ?? null } - openclawGatewayUrl={settings.openclawGatewayUrl} + openclawGatewayUrl={effectiveOpenclawGatewayUrl} claudeAuthTokenHelperCommand={settings.claudeAuthTokenHelperCommand} /> ))} @@ -2737,25 +2854,45 @@ function SettingsRouteView() { title="OpenClaw gateway" description="Connect to an OpenClaw gateway for remote agent sessions." status={ - settings.openclawGatewayUrl.trim().length > 0 - ? `Configured for ${settings.openclawGatewayUrl}` + savedOpenclawGatewayUrl + ? `Saved for ${savedOpenclawGatewayUrl}` : "Not configured" } resetAction={ isOpenClawSettingsDirty ? ( - updateSettings({ - openclawGatewayUrl: defaults.openclawGatewayUrl, - openclawPassword: defaults.openclawPassword, - }) - } + onClick={() => { + setOpenclawGatewayDraft(null); + setOpenclawSharedSecretDraft(""); + setOpenclawTestResult(null); + }} /> ) : null } >
+ {canImportLegacyOpenclawSettings ? ( +
+
+
+ Legacy local settings were found for OpenClaw. Import them into the + persisted gateway config to unlock saved credentials and device + state. +
+ +
+
+ ) : null} +
+
+ Saved gateway:{" "} + + {savedOpenclawGatewayUrl || "Not saved"} + +
+
+ Saved shared secret:{" "} + + {savedOpenclawHasSharedSecret ? "Configured" : "Not configured"} + +
+
+ Device fingerprint:{" "} + + {openclawGatewayConfigQuery.data?.deviceFingerprint ?? "Not created"} + +
+
+ Cached device token:{" "} + + {openclawGatewayConfigQuery.data?.hasDeviceToken + ? "Configured" + : "Not configured"} + +
+
+
- +
+ + + {savedOpenclawHasSharedSecret ? ( + + ) : null} + + +
{openclawTestResult ? ( @@ -2883,6 +3123,177 @@ function SettingsRouteView() {
) : null} + {openclawTestResult.serverInfo ? ( +
+ + Server Info + +
+ {openclawTestResult.serverInfo.version ? ( +
+ Version:{" "} + + {openclawTestResult.serverInfo.version} + +
+ ) : null} + {openclawTestResult.serverInfo.sessionId ? ( +
+ Session:{" "} + + {openclawTestResult.serverInfo.sessionId} + +
+ ) : null} +
+
+ ) : null} + + {openclawTestResult.diagnostics ? ( +
+ + Debugging Context + +
+ {openclawTestResult.diagnostics.normalizedUrl ? ( +
+ Endpoint:{" "} + + {openclawTestResult.diagnostics.normalizedUrl} + +
+ ) : null} + {openclawTestResult.diagnostics.hostKind ? ( +
+ Host type:{" "} + + {describeOpenclawGatewayHostKind( + openclawTestResult.diagnostics.hostKind, + )} + +
+ ) : null} + {openclawTestResult.diagnostics.resolvedAddresses.length > 0 ? ( +
+ Resolved:{" "} + + {openclawTestResult.diagnostics.resolvedAddresses.join( + ", ", + )} + +
+ ) : null} + {describeOpenclawGatewayHealthStatus(openclawTestResult) ? ( +
+ Health probe:{" "} + + {describeOpenclawGatewayHealthStatus(openclawTestResult)} + + {openclawTestResult.diagnostics.healthUrl ? ( + <> + {" "} + at{" "} + + {openclawTestResult.diagnostics.healthUrl} + + + ) : null} +
+ ) : null} + {openclawTestResult.diagnostics.socketCloseCode !== undefined ? ( +
+ Socket close:{" "} + + {openclawTestResult.diagnostics.socketCloseCode} + {openclawTestResult.diagnostics.socketCloseReason + ? ` (${openclawTestResult.diagnostics.socketCloseReason})` + : ""} + +
+ ) : null} + {openclawTestResult.diagnostics.socketError ? ( +
+ Socket error:{" "} + + {openclawTestResult.diagnostics.socketError} + +
+ ) : null} + {openclawTestResult.diagnostics.gatewayErrorCode ? ( +
+ Gateway error code:{" "} + + {openclawTestResult.diagnostics.gatewayErrorCode} + +
+ ) : null} + {openclawTestResult.diagnostics.gatewayErrorDetailCode ? ( +
+ Gateway detail code:{" "} + + {openclawTestResult.diagnostics.gatewayErrorDetailCode} + +
+ ) : null} + {openclawTestResult.diagnostics.gatewayErrorDetailReason ? ( +
+ Gateway detail reason:{" "} + + {openclawTestResult.diagnostics.gatewayErrorDetailReason} + +
+ ) : null} + {openclawTestResult.diagnostics.gatewayRecommendedNextStep ? ( +
+ Gateway next step:{" "} + + {openclawTestResult.diagnostics.gatewayRecommendedNextStep} + +
+ ) : null} + {openclawTestResult.diagnostics.gatewayCanRetryWithDeviceToken !== + undefined ? ( +
+ Device-token retry available:{" "} + + {openclawTestResult.diagnostics + .gatewayCanRetryWithDeviceToken + ? "Yes" + : "No"} + +
+ ) : null} + {openclawTestResult.diagnostics.observedNotifications.length > + 0 ? ( +
+ Gateway events:{" "} + + {openclawTestResult.diagnostics.observedNotifications.join( + ", ", + )} + +
+ ) : null} +
+
+ ) : null} + + {openclawTestResult.diagnostics?.hints.length ? ( +
+ + Troubleshooting + +
    + {openclawTestResult.diagnostics.hints.map((hint) => ( +
  • + + {hint} +
  • + ))} +
+
+ ) : null} + {openclawTestResult.error && !openclawTestResult.steps.some((step) => step.status === "fail") ? (