Merge pull request #53 from cloud-neutral-toolkit/codex/openclaw-origin-override

Add configurable OpenClaw origin override
This commit is contained in:
Haitao Pan 2026-03-12 18:59:30 +08:00 committed by GitHub
commit f7a9c23b87
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 151 additions and 39 deletions

View File

@ -13,6 +13,7 @@ export const dynamic = 'force-dynamic'
type ProbeBody = {
target?: 'openclaw' | 'vault' | 'apisix'
gatewayUrl?: string
gatewayOrigin?: string
gatewayToken?: string
vaultUrl?: string
vaultNamespace?: string
@ -64,7 +65,21 @@ function formatGatewayError(error: OpenClawGatewayError | null, client: OpenClaw
return error.message
}
async function probeOpenClaw(body: ProbeBody): Promise<Response> {
function resolveGatewayOrigin(override: string | undefined, request: NextRequest): string {
const explicit = override?.trim()
if (explicit) {
return explicit
}
const headerOrigin = request.headers.get('origin')?.trim()
if (headerOrigin) {
return headerOrigin
}
return request.nextUrl.origin
}
async function probeOpenClaw(body: ProbeBody, request: NextRequest): Promise<Response> {
const config = await resolveOpenClawGatewayConfig({
gatewayUrl: body.gatewayUrl,
gatewayToken: body.gatewayToken,
@ -84,6 +99,7 @@ async function probeOpenClaw(body: ProbeBody): Promise<Response> {
try {
await client.connect({
gatewayUrl: config.gatewayUrl,
gatewayOrigin: resolveGatewayOrigin(body.gatewayOrigin, request),
gatewayToken: config.gatewayToken,
clientLabel: 'console.svc.plus Probe',
})
@ -223,7 +239,7 @@ export async function POST(request: NextRequest): Promise<Response> {
switch (body.target) {
case 'openclaw':
return probeOpenClaw(body)
return probeOpenClaw(body, request)
case 'vault':
return probeVault(body)
case 'apisix':

View File

@ -18,6 +18,7 @@ export const dynamic = 'force-dynamic'
type BootstrapBody = {
action: 'bootstrap'
gatewayUrl?: string
gatewayOrigin?: string
gatewayToken?: string
vaultUrl?: string
vaultNamespace?: string
@ -31,6 +32,7 @@ type BootstrapBody = {
type SendBody = {
action: 'send'
gatewayUrl?: string
gatewayOrigin?: string
gatewayToken?: string
vaultUrl?: string
vaultNamespace?: string
@ -112,7 +114,21 @@ function resolveSessionKey(params: {
return makeAgentSessionKey(params.agentId?.trim() ?? '', params.mainSessionKey)
}
async function handleBootstrap(body: BootstrapBody): Promise<Response> {
function resolveGatewayOrigin(override: string | undefined, request: NextRequest): string {
const explicit = override?.trim()
if (explicit) {
return explicit
}
const headerOrigin = request.headers.get('origin')?.trim()
if (headerOrigin) {
return headerOrigin
}
return request.nextUrl.origin
}
async function handleBootstrap(body: BootstrapBody, request: NextRequest): Promise<Response> {
const gateway = await resolveOpenClawGatewayConfig({
gatewayUrl: body.gatewayUrl,
gatewayToken: body.gatewayToken,
@ -132,6 +148,7 @@ async function handleBootstrap(body: BootstrapBody): Promise<Response> {
try {
const connected = await client.connect({
gatewayUrl: gateway.gatewayUrl,
gatewayOrigin: resolveGatewayOrigin(body.gatewayOrigin, request),
gatewayToken: gateway.gatewayToken,
})
@ -178,7 +195,7 @@ async function handleBootstrap(body: BootstrapBody): Promise<Response> {
}
}
async function handleSend(body: SendBody): Promise<Response> {
async function handleSend(body: SendBody, request: NextRequest): Promise<Response> {
const gateway = await resolveOpenClawGatewayConfig({
gatewayUrl: body.gatewayUrl,
gatewayToken: body.gatewayToken,
@ -258,6 +275,7 @@ async function handleSend(body: SendBody): Promise<Response> {
client = new OpenClawGatewayClient()
const connected = await client.connect({
gatewayUrl: gateway.gatewayUrl,
gatewayOrigin: resolveGatewayOrigin(body.gatewayOrigin, request),
gatewayToken: gateway.gatewayToken,
})
@ -396,11 +414,11 @@ export async function POST(request: NextRequest): Promise<Response> {
}
if (body.action === 'bootstrap') {
return handleBootstrap(body)
return handleBootstrap(body, request)
}
if (body.action === 'send') {
return handleSend(body)
return handleSend(body, request)
}
return jsonError('Unsupported assistant action.', 400, 'UNSUPPORTED_ACTION')

View File

@ -30,6 +30,7 @@ export function AskAIDialog({
const router = useRouter();
const resolvedDefaults: IntegrationDefaults = defaults ?? {
openclawUrl: "",
openclawOrigin: "",
openclawTokenConfigured: false,
vaultUrl: "",
vaultNamespace: "",

View File

@ -230,6 +230,7 @@ export function OpenClawAssistantPane({
);
const applyDefaults = useOpenClawConsoleStore((state) => state.applyDefaults);
const openclawUrl = useOpenClawConsoleStore((state) => state.openclawUrl);
const openclawOrigin = useOpenClawConsoleStore((state) => state.openclawOrigin);
const openclawToken = useOpenClawConsoleStore((state) => state.openclawToken);
const vaultUrl = useOpenClawConsoleStore((state) => state.vaultUrl);
const vaultNamespace = useOpenClawConsoleStore(
@ -436,6 +437,7 @@ export function OpenClawAssistantPane({
body: JSON.stringify({
action: "bootstrap",
gatewayUrl: openclawUrl,
gatewayOrigin: openclawOrigin,
gatewayToken: openclawToken,
vaultUrl,
vaultNamespace,
@ -479,6 +481,7 @@ export function OpenClawAssistantPane({
copy.connectFailed,
copy.serverMissing,
openclawToken,
openclawOrigin,
openclawUrl,
vaultNamespace,
vaultSecretKey,
@ -585,6 +588,7 @@ export function OpenClawAssistantPane({
body: JSON.stringify({
action: "send",
gatewayUrl: openclawUrl,
gatewayOrigin: openclawOrigin,
gatewayToken: openclawToken,
vaultUrl,
vaultNamespace,
@ -668,6 +672,7 @@ export function OpenClawAssistantPane({
isChinese,
mainSessionKey,
openclawToken,
openclawOrigin,
openclawUrl,
vaultNamespace,
vaultSecretKey,

View File

@ -79,6 +79,7 @@ export type OpenClawStreamEvent =
export type IntegrationDefaults = {
openclawUrl: string
openclawOrigin: string
openclawTokenConfigured: boolean
vaultUrl: string
vaultNamespace: string

View File

@ -89,6 +89,7 @@ function stringValue(value: unknown): string | undefined {
const EMPTY_DEFAULTS: IntegrationDefaults = {
openclawUrl: "",
openclawOrigin: "",
openclawTokenConfigured: false,
vaultUrl: "",
vaultNamespace: "",
@ -118,6 +119,7 @@ export function IntegrationsConsole({
const applyDefaults = useOpenClawConsoleStore((state) => state.applyDefaults);
const openclawUrl = useOpenClawConsoleStore((state) => state.openclawUrl);
const openclawOrigin = useOpenClawConsoleStore((state) => state.openclawOrigin);
const openclawToken = useOpenClawConsoleStore((state) => state.openclawToken);
const vaultUrl = useOpenClawConsoleStore((state) => state.vaultUrl);
const vaultNamespace = useOpenClawConsoleStore(
@ -135,6 +137,9 @@ export function IntegrationsConsole({
const setOpenclawUrl = useOpenClawConsoleStore(
(state) => state.setOpenclawUrl,
);
const setOpenclawOrigin = useOpenClawConsoleStore(
(state) => state.setOpenclawOrigin,
);
const setOpenclawToken = useOpenClawConsoleStore(
(state) => state.setOpenclawToken,
);
@ -240,6 +245,7 @@ export function IntegrationsConsole({
body: JSON.stringify({
target,
gatewayUrl: openclawUrl,
gatewayOrigin: openclawOrigin,
gatewayToken: openclawToken,
vaultUrl,
vaultNamespace,
@ -382,6 +388,19 @@ export function IntegrationsConsole({
</Field>
</div>
<Field
label="Origin Override"
hint="可选。留空时自动使用当前页面 origin例如 https://preview.svc.plus。"
>
<input
type="text"
value={openclawOrigin}
onChange={(event) => setOpenclawOrigin(event.target.value)}
className={inputClassName()}
placeholder="https://preview.svc.plus"
/>
</Field>
<div className="flex flex-wrap items-center gap-3">
<button
type="button"

View File

@ -83,6 +83,7 @@ function normalizeWsUrl(value?: string): string {
export function getConsoleIntegrationDefaults(): IntegrationDefaults {
return {
openclawUrl: normalizeWsUrl(readEnvValue(...OPENCLAW_URL_KEYS)),
openclawOrigin: '',
openclawTokenConfigured: Boolean(readEnvValue(...OPENCLAW_TOKEN_KEYS)),
vaultUrl: normalizeHttpUrl(readEnvValue(...VAULT_URL_KEYS)),
vaultNamespace: readEnvValue(...VAULT_NAMESPACE_KEYS) ?? '',

View File

@ -1,6 +1,7 @@
import 'server-only'
import { randomUUID } from 'node:crypto'
import WebSocket from 'ws'
import {
extractMessageText,
@ -50,6 +51,8 @@ type GatewayEventFrame = {
payload?: unknown
}
type GatewaySocket = WebSocket
function asRecord(value: unknown): Record<string, unknown> {
return value && typeof value === 'object' && !Array.isArray(value)
? (value as Record<string, unknown>)
@ -135,13 +138,13 @@ export class OpenClawGatewayError extends Error {
}
export class OpenClawGatewayClient {
private socket: WebSocket | null = null
private socket: GatewaySocket | null = null
private currentDeviceId = ''
private connectChallengeNonce: string | null = null
private pending = new Map<string, PendingRequest>()
private listeners = new Set<(event: GatewayEventFrame) => void>()
private handleMessageRef = (event: MessageEvent) => {
void this.handleMessage(event)
private handleMessageRef = (data: unknown) => {
void this.handleMessage(data)
}
private handleCloseRef = () => {
this.failPending(new OpenClawGatewayError('Gateway connection closed', 'SOCKET_CLOSED'))
@ -152,42 +155,44 @@ export class OpenClawGatewayClient {
async connect(params: {
gatewayUrl: string
gatewayOrigin?: string
gatewayToken: string
clientId?: string
clientMode?: string
clientLabel?: string
}): Promise<{ mainSessionKey: string; deviceId: string }> {
const url = resolveGatewayUrl(params.gatewayUrl)
const socket = new WebSocket(url)
const origin = params.gatewayOrigin?.trim()
const socket = new WebSocket(url, {
...(origin
? {
headers: {
Origin: origin,
},
}
: {}),
})
this.socket = socket
this.connectChallengeNonce = null
socket.addEventListener('message', this.handleMessageRef)
socket.addEventListener('close', this.handleCloseRef)
socket.addEventListener('error', this.handleErrorRef)
socket.on('message', this.handleMessageRef)
socket.on('close', this.handleCloseRef)
socket.on('error', this.handleErrorRef)
await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new OpenClawGatewayError('Gateway open timeout', 'OPEN_TIMEOUT'))
}, 8000)
socket.addEventListener(
'open',
() => {
clearTimeout(timeout)
resolve()
},
{ once: true },
)
socket.once('open', () => {
clearTimeout(timeout)
resolve()
})
socket.addEventListener(
'error',
() => {
clearTimeout(timeout)
reject(new OpenClawGatewayError('Gateway open failed', 'OPEN_FAILED'))
},
{ once: true },
)
socket.once('error', () => {
clearTimeout(timeout)
reject(new OpenClawGatewayError('Gateway open failed', 'OPEN_FAILED'))
})
})
const clientId = params.clientId ?? OPENCLAW_CLIENT_IDS.assistant
@ -297,7 +302,7 @@ export class OpenClawGatewayClient {
return this.currentDeviceId
}
private async waitForConnectChallenge(socket: WebSocket): Promise<string> {
private async waitForConnectChallenge(socket: GatewaySocket): Promise<string> {
if (this.connectChallengeNonce) {
return this.connectChallengeNonce
}
@ -341,12 +346,12 @@ export class OpenClawGatewayClient {
const cleanup = () => {
clearTimeout(timeout)
stopListening()
socket.removeEventListener('close', onClose)
socket.removeEventListener('error', onError)
socket.off('close', onClose)
socket.off('error', onError)
}
socket.addEventListener('close', onClose, { once: true })
socket.addEventListener('error', onError, { once: true })
socket.once('close', onClose)
socket.once('error', onError)
})
}
@ -509,17 +514,17 @@ export class OpenClawGatewayClient {
return
}
socket.removeEventListener('message', this.handleMessageRef)
socket.removeEventListener('close', this.handleCloseRef)
socket.removeEventListener('error', this.handleErrorRef)
socket.off('message', this.handleMessageRef)
socket.off('close', this.handleCloseRef)
socket.off('error', this.handleErrorRef)
if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) {
socket.close()
}
}
private async handleMessage(event: MessageEvent): Promise<void> {
const text = toText(event.data)
private async handleMessage(data: unknown): Promise<void> {
const text = toText(data)
let payload: Record<string, unknown>
try {

View File

@ -8,6 +8,7 @@ import type { AssistantMode, IntegrationDefaults, ThinkingLevel } from '@/lib/op
type OpenClawConsoleState = {
defaultsLoaded: boolean
openclawUrl: string
openclawOrigin: string
openclawToken: string
vaultUrl: string
vaultNamespace: string
@ -22,6 +23,7 @@ type OpenClawConsoleState = {
selectedSessionKey: string
applyDefaults: (defaults: IntegrationDefaults) => void
setOpenclawUrl: (value: string) => void
setOpenclawOrigin: (value: string) => void
setOpenclawToken: (value: string) => void
setVaultUrl: (value: string) => void
setVaultNamespace: (value: string) => void
@ -41,6 +43,7 @@ export const useOpenClawConsoleStore = create<OpenClawConsoleState>()(
(set, get) => ({
defaultsLoaded: false,
openclawUrl: '',
openclawOrigin: '',
openclawToken: '',
vaultUrl: '',
vaultNamespace: '',
@ -58,6 +61,7 @@ export const useOpenClawConsoleStore = create<OpenClawConsoleState>()(
set({
defaultsLoaded: true,
openclawUrl: current.openclawUrl || defaults.openclawUrl,
openclawOrigin: current.openclawOrigin || defaults.openclawOrigin,
vaultUrl: current.vaultUrl || defaults.vaultUrl,
vaultNamespace: current.vaultNamespace || defaults.vaultNamespace,
vaultSecretPath: current.vaultSecretPath || defaults.vaultSecretPath,
@ -66,6 +70,7 @@ export const useOpenClawConsoleStore = create<OpenClawConsoleState>()(
})
},
setOpenclawUrl: (openclawUrl) => set({ openclawUrl }),
setOpenclawOrigin: (openclawOrigin) => set({ openclawOrigin }),
setOpenclawToken: (openclawToken) => set({ openclawToken }),
setVaultUrl: (vaultUrl) => set({ vaultUrl }),
setVaultNamespace: (vaultNamespace) => set({ vaultNamespace }),
@ -84,6 +89,7 @@ export const useOpenClawConsoleStore = create<OpenClawConsoleState>()(
storage: createJSONStorage(() => sessionStorage),
partialize: (state) => ({
openclawUrl: state.openclawUrl,
openclawOrigin: state.openclawOrigin,
openclawToken: state.openclawToken,
vaultUrl: state.vaultUrl,
vaultNamespace: state.vaultNamespace,

40
types/ws.d.ts vendored Normal file
View File

@ -0,0 +1,40 @@
declare module "ws" {
export type MessageData =
| string
| ArrayBuffer
| Buffer
| Buffer[]
| ArrayBufferView
export type ClientOptions = {
headers?: Record<string, string>
}
export default class WebSocket {
static readonly CONNECTING: number
static readonly OPEN: number
static readonly CLOSING: number
static readonly CLOSED: number
readonly readyState: number
constructor(address: string | URL, options?: ClientOptions)
on(event: "open", listener: () => void): this
on(event: "close", listener: () => void): this
on(event: "error", listener: (error: Error) => void): this
on(event: "message", listener: (data: MessageData) => void): this
once(event: "open", listener: () => void): this
once(event: "close", listener: () => void): this
once(event: "error", listener: (error: Error) => void): this
off(event: "open", listener: () => void): this
off(event: "close", listener: () => void): this
off(event: "error", listener: (error: Error) => void): this
off(event: "message", listener: (data: MessageData) => void): this
send(data: string): void
close(): void
}
}