From 9a915ae08043fbe004199ae3bf3e215ce4cc1441 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Thu, 12 Mar 2026 12:18:25 +0800 Subject: [PATCH] feat: integrate openclaw assistant workspace --- .env.example | 13 +- README.md | 13 + docs/README.md | 14 + docs/getting-started/installation.md | 42 +- docs/zh/README.md | 18 + docs/zh/getting-started/installation.md | 42 +- src/app/AppProviders.tsx | 37 +- src/app/api/integrations/defaults/route.ts | 8 + src/app/api/integrations/probe/route.ts | 182 ++++ src/app/api/openclaw/assistant/route.ts | 340 +++++++ src/app/layout.tsx | 5 +- .../services/openclaw/chats/MoltbotChat.tsx | 127 --- src/app/services/openclaw/chats/page.tsx | 17 - src/app/services/openclaw/page.tsx | 23 + src/app/services/page.tsx | 9 +- src/components/AskAIButton.tsx | 15 +- src/components/AskAIDialog.tsx | 418 ++------- src/components/Navbar.tsx | 4 +- .../openclaw/OpenClawAssistantPane.tsx | 836 ++++++++++++++++++ .../openclaw/OpenClawWorkspacePage.tsx | 113 +++ src/i18n/translations.ts | 4 +- src/lib/navigation.ts | 2 +- src/lib/openclaw/types.ts | 151 ++++ .../components/IntegrationsConsole.tsx | 451 ++++++++++ .../extensions/builtin/user-center/index.ts | 10 +- .../builtin/user-center/routes/api.tsx | 12 +- src/server/consoleIntegrations.ts | 141 +++ src/server/openclaw/gateway-client.ts | 439 +++++++++ src/state/openclawConsoleStore.ts | 90 ++ 29 files changed, 3015 insertions(+), 561 deletions(-) create mode 100644 src/app/api/integrations/defaults/route.ts create mode 100644 src/app/api/integrations/probe/route.ts create mode 100644 src/app/api/openclaw/assistant/route.ts delete mode 100644 src/app/services/openclaw/chats/MoltbotChat.tsx delete mode 100644 src/app/services/openclaw/chats/page.tsx create mode 100644 src/app/services/openclaw/page.tsx create mode 100644 src/components/openclaw/OpenClawAssistantPane.tsx create mode 100644 src/components/openclaw/OpenClawWorkspacePage.tsx create mode 100644 src/lib/openclaw/types.ts create mode 100644 src/modules/extensions/builtin/user-center/components/IntegrationsConsole.tsx create mode 100644 src/server/consoleIntegrations.ts create mode 100644 src/server/openclaw/gateway-client.ts create mode 100644 src/state/openclawConsoleStore.ts diff --git a/.env.example b/.env.example index b1381de..55edf36 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,13 @@ -# Moltbot Service URL -# Defaults to https://moltbot.svc.plus if not set -MOLTBOT_SERVICE_URL=https://moltbot.svc.plus +# OpenClaw assistant integrations +# Use environment variables to prefill the assistant and integrations page. +# Values are read server-side and are not hardcoded into the UI. +OPENCLAW_GATEWAY_REMOTE_URL= +OPENCLAW_GATEWAY_TOKEN= +VAULT_SERVER_URL= +VAULT_NAMESPACE= +VAULT_TOKEN= +APISIX_AI_GATEWAY_URL= +AI_GATEWAY_ACCESS_TOKEN= # Giscus Configuration (GitHub Discussions Integration) # See https://giscus.app to generate these values diff --git a/README.md b/README.md index 6e70a85..cf9efd6 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,18 @@ yarn dev cp .env.example .env ``` +AI 助手与集成页使用以下环境变量做服务端预填,不在前端 UI 中硬编码: + +- `OPENCLAW_GATEWAY_REMOTE_URL` +- `OPENCLAW_GATEWAY_TOKEN` +- `VAULT_SERVER_URL` +- `VAULT_NAMESPACE` +- `VAULT_TOKEN` +- `APISIX_AI_GATEWAY_URL` +- `AI_GATEWAY_ACCESS_TOKEN` + +建议参考 `/Users/shenlan/workspaces/cloud-neutral-toolkit/openclaw-deploy-example/.env` 填写,并同时查看 `docs/getting-started/installation.md`。 + ## 核心特性 & 技术栈 (Features & Tech Stack) 核心特性: @@ -57,6 +69,7 @@ cp .env.example .env 常用链接: * OIDC: `docs/integrations/oidc-auth.md` * Cloudflare Web Analytics: `docs/integrations/cloudflare-web-analytics.md` +* Assistant / Integrations env setup: `docs/getting-started/installation.md` 其他: * Agent rules: `AGENTS.md` diff --git a/docs/README.md b/docs/README.md index b43f961..f1ca1ad 100644 --- a/docs/README.md +++ b/docs/README.md @@ -26,6 +26,20 @@ This directory follows a standard open-source documentation layout and mirrors t - Library layer (vendored): `packages/neurapress` - Build/runtime glue: `scripts`, `config`, `public` +## Assistant Integrations + +The homepage AI assistant and `/panel/api` integrations page read their defaults from environment variables on the server side. Use `.env.example` plus `getting-started/installation.md` for the canonical setup. + +Canonical variables: + +- `OPENCLAW_GATEWAY_REMOTE_URL` +- `OPENCLAW_GATEWAY_TOKEN` +- `VAULT_SERVER_URL` +- `VAULT_NAMESPACE` +- `VAULT_TOKEN` +- `APISIX_AI_GATEWAY_URL` +- `AI_GATEWAY_ACCESS_TOKEN` + ## Index - Getting Started diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index d56a80a..bdbe1c7 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -2,8 +2,44 @@ ## Purpose -- TODO: Add content specific to Installation. +- Set up the local development environment for `console.svc.plus`. +- Define the assistant and integrations defaults without hardcoding gateway values into the UI. -## Notes +## Environment setup -- TODO: Link to related documents in this section. +1. Copy the example file: + +```bash +cp .env.example .env +``` + +2. Use `/Users/shenlan/workspaces/cloud-neutral-toolkit/openclaw-deploy-example/.env` as the reference source for deployment-aligned values when available. + +## Assistant and integrations variables + +These variables are read on the server side and used to prefill: + +- the homepage AI assistant +- the sidebar assistant dialog +- the `/panel/api` integrations page + +| Variable | Used by | Notes | +|---|---|---| +| `OPENCLAW_GATEWAY_REMOTE_URL` | OpenClaw assistant | Preferred remote WebSocket endpoint, for example `wss://openclaw.svc.plus:443` | +| `OPENCLAW_GATEWAY_TOKEN` | OpenClaw assistant | Gateway token used by the server-side assistant bridge | +| `VAULT_SERVER_URL` | Vault integration | Base Vault address for connectivity checks and defaults | +| `VAULT_NAMESPACE` | Vault integration | Optional namespace when Vault Enterprise namespaces are used | +| `VAULT_TOKEN` | Vault integration | Token used for Vault probe requests | +| `APISIX_AI_GATEWAY_URL` | APISIX AI Gateway integration | Base HTTP(S) endpoint for AI gateway probing | +| `AI_GATEWAY_ACCESS_TOKEN` | APISIX AI Gateway integration | Access token used for gateway probe requests | + +## Behavior + +- These values are not hardcoded into React components. +- UI forms can still be overridden per request or per session when needed. +- Empty values simply disable prefill; they do not break the page layout. + +## Related documents + +- `../README.md` +- `../usage/config.md` diff --git a/docs/zh/README.md b/docs/zh/README.md index ac05808..428b15f 100644 --- a/docs/zh/README.md +++ b/docs/zh/README.md @@ -22,3 +22,21 @@ - 每个中文文件顶部会链接到对应的英文原文路径。 - 如果中文内容尚未完善,会保留 `TODO` 占位,逐步补齐。 +## AI 助手集成环境变量 + +首页 AI 助手和 `/panel/api` 集成页会在服务端读取环境变量做默认值预填,不会把网关地址或令牌硬编码进前端 UI。 + +建议优先查看: + +- `../../.env.example` +- `getting-started/installation.md` + +当前约定的主变量: + +- `OPENCLAW_GATEWAY_REMOTE_URL` +- `OPENCLAW_GATEWAY_TOKEN` +- `VAULT_SERVER_URL` +- `VAULT_NAMESPACE` +- `VAULT_TOKEN` +- `APISIX_AI_GATEWAY_URL` +- `AI_GATEWAY_ACCESS_TOKEN` diff --git a/docs/zh/getting-started/installation.md b/docs/zh/getting-started/installation.md index d5b3178..b3d1bb5 100644 --- a/docs/zh/getting-started/installation.md +++ b/docs/zh/getting-started/installation.md @@ -4,8 +4,44 @@ ## 目的 -- TODO: 补充中文内容。 +- 为 `console.svc.plus` 准备本地运行环境。 +- 用环境变量给 AI 助手和集成功能做默认值预填,而不是把网关配置写死在 UI 里。 -## 备注 +## 环境准备 -- TODO: 链接到本章节相关文档。 +1. 复制示例文件: + +```bash +cp .env.example .env +``` + +2. 如有联调环境,优先参考 `/Users/shenlan/workspaces/cloud-neutral-toolkit/openclaw-deploy-example/.env` 中对应值。 + +## AI 助手与集成变量 + +下面这些变量会由服务端读取,并用于以下场景的默认值预填: + +- 首页 AI 助手 +- 侧栏 AI 助手 +- `/panel/api` 集成页 + +| 变量 | 用途 | 说明 | +|---|---|---| +| `OPENCLAW_GATEWAY_REMOTE_URL` | OpenClaw 助手 | 远端 WebSocket 地址,例如 `wss://openclaw.svc.plus:443` | +| `OPENCLAW_GATEWAY_TOKEN` | OpenClaw 助手 | 服务端桥接 OpenClaw gateway 时使用的令牌 | +| `VAULT_SERVER_URL` | Vault 集成 | Vault 服务地址,用于默认值和连通性探测 | +| `VAULT_NAMESPACE` | Vault 集成 | Vault Enterprise namespace,可选 | +| `VAULT_TOKEN` | Vault 集成 | Vault 探测请求使用的令牌 | +| `APISIX_AI_GATEWAY_URL` | APISIX AI Gateway 集成 | AI gateway 的 HTTP(S) 地址 | +| `AI_GATEWAY_ACCESS_TOKEN` | APISIX AI Gateway 集成 | AI gateway 探测请求使用的访问令牌 | + +## 行为说明 + +- 这些变量不会被硬编码进前端 React 组件。 +- 页面中的输入项仍然支持会话级覆盖。 +- 变量留空只会取消默认值预填,不会影响现有布局。 + +## 相关文档 + +- `../README.md` +- `../usage/config.md` diff --git a/src/app/AppProviders.tsx b/src/app/AppProviders.tsx index a66116c..76c9372 100644 --- a/src/app/AppProviders.tsx +++ b/src/app/AppProviders.tsx @@ -1,18 +1,34 @@ 'use client' -import type { ReactNode } from 'react' +import { useEffect, type ReactNode } from 'react' +import { usePathname } from 'next/navigation' import { ThemeProvider } from '../components/theme' import { LanguageProvider } from '../i18n/LanguageProvider' import { AskAIDialog } from '../components/AskAIDialog' import { useMoltbotStore } from '../lib/moltbotStore' import { cn } from '../lib/utils' +import type { IntegrationDefaults } from '@/lib/openclaw/types' +import { useOpenClawConsoleStore } from '@/state/openclawConsoleStore' -export function AppProviders({ children }: { children: ReactNode }) { - const { isOpen, isMinimized, setIsOpen, setMinimized, close, mode, toggleOpen } = useMoltbotStore() +export function AppProviders({ + children, + assistantDefaults, +}: { + children: ReactNode + assistantDefaults: IntegrationDefaults +}) { + const { isOpen, isMinimized, close, toggleOpen } = useMoltbotStore() + const applyDefaults = useOpenClawConsoleStore((state) => state.applyDefaults) + const pathname = usePathname() + const isOpenClawWorkspace = pathname.startsWith('/services/openclaw') // Always reserve space if open and not minimized, since we only have "Float/Sidebar" mode now // and user wants it to NEVER cover the homepage. - const reserveSpace = isOpen && !isMinimized + const reserveSpace = !isOpenClawWorkspace && isOpen && !isMinimized + + useEffect(() => { + applyDefaults(assistantDefaults) + }, [applyDefaults, assistantDefaults]) return ( @@ -25,11 +41,14 @@ export function AppProviders({ children }: { children: ReactNode }) {
{children}
- + {!isOpenClawWorkspace ? ( + + ) : null} diff --git a/src/app/api/integrations/defaults/route.ts b/src/app/api/integrations/defaults/route.ts new file mode 100644 index 0000000..69a63f2 --- /dev/null +++ b/src/app/api/integrations/defaults/route.ts @@ -0,0 +1,8 @@ +import { getConsoleIntegrationDefaults } from '@/server/consoleIntegrations' + +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +export async function GET(): Promise { + return Response.json(getConsoleIntegrationDefaults()) +} diff --git a/src/app/api/integrations/probe/route.ts b/src/app/api/integrations/probe/route.ts new file mode 100644 index 0000000..a01d4cd --- /dev/null +++ b/src/app/api/integrations/probe/route.ts @@ -0,0 +1,182 @@ +import type { NextRequest } from 'next/server' + +import { + resolveApisixProbeConfig, + resolveOpenClawGatewayConfig, + resolveVaultProbeConfig, +} from '@/server/consoleIntegrations' +import { OpenClawGatewayClient, OpenClawGatewayError } from '@/server/openclaw/gateway-client' + +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +type ProbeBody = { + target?: 'openclaw' | 'vault' | 'apisix' + gatewayUrl?: string + gatewayToken?: string + vaultUrl?: string + vaultToken?: string + apisixUrl?: string + apisixToken?: string +} + +function jsonError(message: string, status = 400): Response { + return Response.json({ error: message }, { status }) +} + +async function probeOpenClaw(body: ProbeBody): Promise { + const config = resolveOpenClawGatewayConfig({ + gatewayUrl: body.gatewayUrl, + gatewayToken: body.gatewayToken, + }) + + if (!config.gatewayUrl) { + return jsonError('OpenClaw gateway URL is not configured.', 400) + } + + const client = new OpenClawGatewayClient() + + try { + await client.connect({ + gatewayUrl: config.gatewayUrl, + gatewayToken: config.gatewayToken, + }) + + const [statusPayload, healthPayload] = await Promise.all([client.status(), client.health()]) + + return Response.json({ + ok: true, + target: 'openclaw', + tokenSource: config.tokenSource, + gatewayUrl: config.gatewayUrl, + statusPayload, + healthPayload, + }) + } catch (error) { + const gatewayError = error instanceof OpenClawGatewayError ? error : null + return Response.json( + { + ok: false, + target: 'openclaw', + gatewayUrl: config.gatewayUrl, + tokenSource: config.tokenSource, + error: gatewayError?.message ?? 'Failed to probe OpenClaw gateway.', + code: gatewayError?.code, + }, + { status: 502 }, + ) + } finally { + await client.close() + } +} + +async function probeVault(body: ProbeBody): Promise { + const config = resolveVaultProbeConfig({ + vaultUrl: body.vaultUrl, + vaultToken: body.vaultToken, + }) + + if (!config.vaultUrl) { + return jsonError('Vault URL is not configured.', 400) + } + + try { + const response = await fetch(`${config.vaultUrl}/v1/sys/health`, { + method: 'GET', + headers: config.vaultToken + ? { + 'X-Vault-Token': config.vaultToken, + } + : undefined, + cache: 'no-store', + }) + + const text = await response.text() + + return Response.json({ + ok: response.ok, + target: 'vault', + vaultUrl: config.vaultUrl, + tokenSource: config.tokenSource, + status: response.status, + body: text.slice(0, 2000), + }) + } catch (error) { + return Response.json( + { + ok: false, + target: 'vault', + vaultUrl: config.vaultUrl, + tokenSource: config.tokenSource, + error: error instanceof Error ? error.message : 'Failed to probe Vault.', + }, + { status: 502 }, + ) + } +} + +async function probeApisix(body: ProbeBody): Promise { + const config = resolveApisixProbeConfig({ + apisixUrl: body.apisixUrl, + apisixToken: body.apisixToken, + }) + + if (!config.apisixUrl) { + return jsonError('APISIX AI gateway URL is not configured.', 400) + } + + try { + const response = await fetch(`${config.apisixUrl}/v1/models`, { + method: 'GET', + headers: config.apisixToken + ? { + Authorization: `Bearer ${config.apisixToken}`, + } + : undefined, + cache: 'no-store', + }) + + const text = await response.text() + + return Response.json({ + ok: response.ok, + target: 'apisix', + apisixUrl: config.apisixUrl, + tokenSource: config.tokenSource, + status: response.status, + body: text.slice(0, 2000), + }) + } catch (error) { + return Response.json( + { + ok: false, + target: 'apisix', + apisixUrl: config.apisixUrl, + tokenSource: config.tokenSource, + error: error instanceof Error ? error.message : 'Failed to probe APISIX AI gateway.', + }, + { status: 502 }, + ) + } +} + +export async function POST(request: NextRequest): Promise { + let body: ProbeBody + + try { + body = (await request.json()) as ProbeBody + } catch { + return jsonError('Invalid JSON body.', 400) + } + + switch (body.target) { + case 'openclaw': + return probeOpenClaw(body) + case 'vault': + return probeVault(body) + case 'apisix': + return probeApisix(body) + default: + return jsonError('Unsupported probe target.', 400) + } +} diff --git a/src/app/api/openclaw/assistant/route.ts b/src/app/api/openclaw/assistant/route.ts new file mode 100644 index 0000000..d78af49 --- /dev/null +++ b/src/app/api/openclaw/assistant/route.ts @@ -0,0 +1,340 @@ +import type { NextRequest } from 'next/server' + +import { + extractMessageText, + makeAgentSessionKey, + matchesSessionKey, + normalizeMainSessionKey, + type GatewayChatAttachmentPayload, + type OpenClawBootstrapResponse, + type OpenClawStreamEvent, +} from '@/lib/openclaw/types' +import { resolveOpenClawGatewayConfig } from '@/server/consoleIntegrations' +import { OpenClawGatewayClient, OpenClawGatewayError } from '@/server/openclaw/gateway-client' + +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +type BootstrapBody = { + action: 'bootstrap' + gatewayUrl?: string + gatewayToken?: string + agentId?: string + sessionKey?: string +} + +type SendBody = { + action: 'send' + gatewayUrl?: string + gatewayToken?: string + agentId?: string + sessionKey?: string + message?: string + thinking?: string + attachments?: GatewayChatAttachmentPayload[] +} + +function jsonError(message: string, status = 400, code?: string): Response { + return Response.json( + { + error: message, + code, + }, + { status }, + ) +} + +function resolveSessionKey(params: { + sessionKey?: string + agentId?: string + mainSessionKey: string +}): string { + const explicit = params.sessionKey?.trim() + if (explicit) { + return explicit + } + + return makeAgentSessionKey(params.agentId?.trim() ?? '', params.mainSessionKey) +} + +async function handleBootstrap(body: BootstrapBody): Promise { + const gateway = resolveOpenClawGatewayConfig({ + gatewayUrl: body.gatewayUrl, + gatewayToken: body.gatewayToken, + }) + + if (!gateway.gatewayUrl) { + return jsonError('OpenClaw gateway URL is not configured.', 400, 'MISSING_GATEWAY_URL') + } + + const client = new OpenClawGatewayClient() + + try { + const connected = await client.connect({ + gatewayUrl: gateway.gatewayUrl, + gatewayToken: gateway.gatewayToken, + }) + + const mainSessionKey = normalizeMainSessionKey(connected.mainSessionKey) + const activeSessionKey = resolveSessionKey({ + sessionKey: body.sessionKey, + agentId: body.agentId, + mainSessionKey, + }) + + const [statusPayload, healthPayload, agents, sessions, messages] = await Promise.all([ + client.status(), + client.health(), + client.listAgents(), + client.listSessions(body.agentId?.trim() || undefined, 24), + client.loadHistory(activeSessionKey), + ]) + + const payload: OpenClawBootstrapResponse = { + activeSessionKey, + mainSessionKey, + gatewayUrl: gateway.gatewayUrl, + tokenSource: gateway.tokenSource, + connectedAt: new Date().toISOString(), + agents, + sessions, + messages, + statusPayload, + healthPayload, + } + + return Response.json(payload) + } catch (error) { + const gatewayError = error instanceof OpenClawGatewayError ? error : null + return jsonError( + gatewayError?.message ?? 'Failed to connect to OpenClaw gateway.', + gatewayError?.code === 'OFFLINE' ? 503 : 502, + gatewayError?.code, + ) + } finally { + await client.close() + } +} + +async function handleSend(body: SendBody): Promise { + const gateway = resolveOpenClawGatewayConfig({ + gatewayUrl: body.gatewayUrl, + gatewayToken: body.gatewayToken, + }) + + if (!gateway.gatewayUrl) { + return jsonError('OpenClaw gateway URL is not configured.', 400, 'MISSING_GATEWAY_URL') + } + + const message = body.message?.trim() ?? '' + const attachments = Array.isArray(body.attachments) ? body.attachments : [] + + if (!message && attachments.length === 0) { + return jsonError('A message or attachment is required.', 400, 'EMPTY_MESSAGE') + } + + const encoder = new TextEncoder() + let client: OpenClawGatewayClient | null = null + + const stream = new ReadableStream({ + async start(controller) { + let activeSessionKey = body.sessionKey?.trim() ?? 'main' + let activeRunId = '' + let finalized = false + let latestAssistantText = '' + let stopListening = () => {} + let finalTimer: NodeJS.Timeout | null = null + + const write = (event: OpenClawStreamEvent) => { + controller.enqueue(encoder.encode(`${JSON.stringify(event)}\n`)) + } + + const finalize = async ( + state: 'final' | 'aborted' | 'error', + errorMessage?: string, + ): Promise => { + if (finalized) { + return + } + + finalized = true + stopListening() + if (finalTimer) { + clearTimeout(finalTimer) + finalTimer = null + } + + try { + const [messages, sessions] = client + ? await Promise.all([ + client.loadHistory(activeSessionKey), + client.listSessions(body.agentId?.trim() || undefined, 24), + ]) + : [[], []] + + write({ + type: 'final', + state, + messages, + sessions, + ...(errorMessage ? { errorMessage } : {}), + }) + } finally { + controller.close() + if (client) { + await client.close() + } + } + } + + try { + client = new OpenClawGatewayClient() + const connected = await client.connect({ + gatewayUrl: gateway.gatewayUrl, + gatewayToken: gateway.gatewayToken, + }) + + const mainSessionKey = normalizeMainSessionKey(connected.mainSessionKey) + activeSessionKey = resolveSessionKey({ + sessionKey: body.sessionKey, + agentId: body.agentId, + mainSessionKey, + }) + + stopListening = client.onEvent((event) => { + if (event.event === 'chat') { + const payload = (event.payload ?? {}) as Record + const runId = typeof payload.runId === 'string' ? payload.runId : '' + const state = typeof payload.state === 'string' ? payload.state : '' + const incomingSessionKey = + typeof payload.sessionKey === 'string' ? payload.sessionKey : activeSessionKey + + if (runId && activeRunId && runId !== activeRunId) { + return + } + + if (!matchesSessionKey(incomingSessionKey, activeSessionKey)) { + return + } + + const messagePayload = (payload.message ?? {}) as Record + const role = typeof messagePayload.role === 'string' ? messagePayload.role.toLowerCase() : '' + const text = extractMessageText(messagePayload) + + if ( + role === 'assistant' && + text && + (state === 'delta' || state === 'final') && + text !== latestAssistantText + ) { + latestAssistantText = text + write({ + type: 'delta', + text, + }) + } + + if (state === 'error') { + void finalize( + 'error', + typeof payload.errorMessage === 'string' ? payload.errorMessage : 'Chat failed.', + ) + return + } + + if (state === 'final' || state === 'aborted') { + void finalize(state) + } + return + } + + if (event.event === 'agent') { + const payload = (event.payload ?? {}) as Record + const runId = typeof payload.runId === 'string' ? payload.runId : '' + if (!runId || !activeRunId || runId !== activeRunId) { + return + } + + if (payload.stream !== 'assistant') { + return + } + + const data = (payload.data ?? {}) as Record + const text = + typeof data.text === 'string' && data.text.trim().length > 0 + ? data.text + : extractMessageText(data) + + if (text && text !== latestAssistantText) { + latestAssistantText = text + write({ + type: 'delta', + text, + }) + } + } + }) + + activeRunId = await client.sendChat({ + sessionKey: activeSessionKey, + message: message || 'See attached.', + thinking: body.thinking?.trim() || 'high', + attachments, + }) + + write({ + type: 'ack', + runId: activeRunId, + sessionKey: activeSessionKey, + }) + + finalTimer = setTimeout(() => { + void finalize('error', 'Gateway stream timed out.') + }, 45000) + } catch (error) { + const gatewayError = error instanceof OpenClawGatewayError ? error : null + write({ + type: 'error', + message: gatewayError?.message ?? 'Failed to send message to OpenClaw gateway.', + ...(gatewayError?.code ? { code: gatewayError.code } : {}), + }) + controller.close() + if (client) { + await client.close() + } + } + }, + async cancel() { + if (client) { + await client.close() + } + }, + }) + + return new Response(stream, { + headers: { + 'Content-Type': 'application/x-ndjson; charset=utf-8', + 'Cache-Control': 'no-store', + }, + }) +} + +export async function POST(request: NextRequest): Promise { + let body: BootstrapBody | SendBody + + try { + body = (await request.json()) as BootstrapBody | SendBody + } catch { + return jsonError('Invalid JSON body.', 400, 'INVALID_JSON') + } + + if (body.action === 'bootstrap') { + return handleBootstrap(body) + } + + if (body.action === 'send') { + return handleSend(body) + } + + return jsonError('Unsupported assistant action.', 400, 'UNSUPPORTED_ACTION') +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 55480b5..5934bf1 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -7,6 +7,7 @@ import type { Metadata } from 'next' import Script from 'next/script' import { Analytics } from '@vercel/analytics/react' import { AppProviders } from './AppProviders' +import { getConsoleIntegrationDefaults } from '@/server/consoleIntegrations' const DEFAULT_TITLE = 'Cloud-Neutral Console | Unified Cloud Native Tools' const DEFAULT_DESCRIPTION = @@ -74,6 +75,8 @@ const bodyClassName = 'bg-[var(--color-background)] text-[var(--color-text)]' const GA_ID = 'G-T4VM8G4Q42' export default function RootLayout({ children }: { children: React.ReactNode }) { + const assistantDefaults = getConsoleIntegrationDefaults() + return ( @@ -126,7 +129,7 @@ export default function RootLayout({ children }: { children: React.ReactNode }) {/* End Cloudflare Web Analytics */} - {children} + {children} diff --git a/src/app/services/openclaw/chats/MoltbotChat.tsx b/src/app/services/openclaw/chats/MoltbotChat.tsx deleted file mode 100644 index 7a2aefe..0000000 --- a/src/app/services/openclaw/chats/MoltbotChat.tsx +++ /dev/null @@ -1,127 +0,0 @@ -'use client' - -import { useState } from 'react' -import { useRouter } from 'next/navigation' -import { - PanelLeft, - PanelRight, - Maximize2, - Minus, - X, -} from 'lucide-react' -import { cn } from '@lib/utils' -import { - HeroSection, - NextStepsSection, - StatsSection, - ShortcutsSection -} from '@/app/page' -import Footer from '@components/Footer' - -import { useLanguage } from '@/i18n/LanguageProvider' -import { translations } from '@/i18n/translations' - -export type ChatLayoutMode = 'left' | 'right' | 'full' - -export function MoltbotChat() { - const router = useRouter() - const { language } = useLanguage() - const t = translations[language].askAI - - // Layout state - const [layout, setLayout] = useState('full') - - // Home content for side-by-side mode - const HomeContent = () => ( -
- - - - -
-
- ) - - // Render the chat interface (now an iframe) - const renderChat = (isSidebar = false) => ( -
- {/* Header */} -
-
-
- 🦞 -
-
-

{translations[language].chat}

-

Online

-
-
- -
- - - - - -
-
- - {/* Iframe Content */} -
-