@hzttt/lucychat
v2026.2.26
Published
OpenClaw LucyChat channel plugin
Readme
LucyChat Channel Plugin
lucychat is an OpenClaw channel plugin + Go relay for custom clients.
V1 scope:
- Direct message text inbound
- AI reply text + media (image/audio) outbound
- At-least-once delivery with client
ack - In-memory relay queue (no persistence on restart)
- Media is sent as binary upload (
plugin -> relay -> client), never as local file path
Structure
index.ts,src/*: OpenClaw channel pluginrelay-go/: standalone Go relay (ws+ enqueue bridge)web/: minimal browser client
Plugin Install (local)
openclaw plugins install -l ./extensions/lucychatOpenClaw Config
Add to ~/.openclaw/openclaw.json:
{
channels: {
lucychat: {
enabled: true,
inboundPath: "/api/channels/lucychat/inbound",
pluginToken: "replace-with-shared-plugin-token",
relayBaseUrl: "http://127.0.0.1:31890",
relayEnqueuePath: "/api/v1/plugin/enqueue",
relayUploadPath: "/api/v1/plugin/media/upload",
mediaMaxMb: 32,
compressImageOnOverflow: true,
requestTimeoutMs: 5000,
retry: {
maxAttempts: 3,
baseDelayMs: 200,
maxDelayMs: 1200,
},
},
},
}Relay Config (env)
export LUCYCHAT_CLIENT_TOKEN="replace-client-token"
export LUCYCHAT_PLUGIN_TOKEN="replace-with-shared-plugin-token"
export LUCYCHAT_GATEWAY_TOKEN="replace-with-openclaw-gateway-auth-token" # optional; defaults to LUCYCHAT_PLUGIN_TOKEN
export LUCYCHAT_PLUGIN_INBOUND_URL="http://127.0.0.1:18789/api/channels/lucychat/inbound"
export LUCYCHAT_RELAY_BIND=":31890"
export LUCYCHAT_MEDIA_MAX_BYTES="33554432" # 32MB hard cap
export LUCYCHAT_MEDIA_URL_TTL_MS="1800000" # 30m signed URL TTL
export LUCYCHAT_MEDIA_SIGNING_KEY="replace-with-signing-key" # optional, defaults to LUCYCHAT_PLUGIN_TOKEN
# export LUCYCHAT_PUBLIC_BASE_URL="https://relay.example.com" # optional, default inferred from request hostNote: OpenClaw protects /api/channels/* with gateway auth. If your gateway auth token differs
from LUCYCHAT_PLUGIN_TOKEN, set LUCYCHAT_GATEWAY_TOKEN so relay-go can pass gateway auth and
still present the plugin token via x-lucychat-plugin-token.
Run Relay
cd extensions/lucychat/relay-go
go run ./cmd/lucychat-relayWeb Client
Serve static files:
cd extensions/lucychat/web
python3 -m http.server 5174Open http://127.0.0.1:5174, fill:
Relay WS URL:ws://127.0.0.1:31890/wsappUserId: your custom user idclientToken:LUCYCHAT_CLIENT_TOKEN
The default web UI is user-facing: it only renders visible chat replies. Protocol-level frames like
accepted/deliver metadata are not shown in the message timeline.
iOS Local HTTP Readonly Sync Demo
When the iOS app is in foreground, it also starts a local HTTP server on port 31901.
You can open http://127.0.0.1:5174/http-sync.html and set:
iOS HTTP Base URL:http://<iphone-lan-ip>:31901Cursor:0for initial snapshot
Notes:
- This is a demo-only, readonly sync endpoint.
- Foreground-only behavior: when app enters background, local HTTP sync stops.
- The iOS app also serves the same readonly page directly at
http://<iphone-lan-ip>:31901/http-sync.html.
Protocol
WS (client <-> relay)
auth:{type:"auth", appUserId, clientToken}send:{type:"send", messageId, text, senderName?}accepted:{type:"accepted", messageId, requestId, sessionKey, duplicate?}deliver:{type:"deliver", item:{id,requestId,text,media?,createdAt}}ack:{type:"ack", ids:["..."]}error:{type:"error", code, message}
deliver.item.media[] shape:
id: stringkind: "image" | "audio" | "video" | "document" | "unknown"url: string(signed relay download URL)mimeType?: stringfileName?: stringsizeBytes?: numberaudioAsVoice?: boolean
Relay HTTP
GET /healthzPOST /api/v1/plugin/enqueue(Authorization: Bearer <plugin-token>)POST /api/v1/plugin/media/upload(Authorization: Bearer <plugin-token>, multipart)GET /api/v1/media/:mediaId?uid=<appUserId>&exp=<unix>&sig=<hmac>
Plugin HTTP
POST /api/channels/lucychat/inbound(Authorization: Bearer <plugin-token>)
iOS Local HTTP (demo, no auth)
GET /healthzGET /v1/messages?cursor=<number>&limit=<number>GET /v1/stream?cursor=<number>(SSE, event type:upsert)
Smoke Script
bash extensions/lucychat/scripts/smoke.shSecurity Note
This v1 accepts HTTP-only deployment if you choose to expose it publicly.
That is risky. For non-local deployments, add TLS termination (reverse proxy) and strong token hygiene.
Media Behavior
- Local path + HTTP(S) media sources are both supported by the plugin.
- Plugin uploads media bytes to relay first, then enqueues message metadata.
- Single file hard cap is 32MB by default (
mediaMaxMb/LUCYCHAT_MEDIA_MAX_BYTES). - If media exceeds cap:
- images: plugin tries compression and retries upload
- non-images: request fails with a clear error
- Signed media URLs expire after 30 minutes by default.
