Add configurable OpenClaw origin override
This commit is contained in:
parent
87d573c528
commit
33862d353b
@ -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':
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -30,6 +30,7 @@ export function AskAIDialog({
|
||||
const router = useRouter();
|
||||
const resolvedDefaults: IntegrationDefaults = defaults ?? {
|
||||
openclawUrl: "",
|
||||
openclawOrigin: "",
|
||||
openclawTokenConfigured: false,
|
||||
vaultUrl: "",
|
||||
vaultNamespace: "",
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -79,6 +79,7 @@ export type OpenClawStreamEvent =
|
||||
|
||||
export type IntegrationDefaults = {
|
||||
openclawUrl: string
|
||||
openclawOrigin: string
|
||||
openclawTokenConfigured: boolean
|
||||
vaultUrl: string
|
||||
vaultNamespace: string
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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) ?? '',
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
40
types/ws.d.ts
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user