Skip to content

Latest commit

 

History

History
256 lines (206 loc) · 13.2 KB

File metadata and controls

256 lines (206 loc) · 13.2 KB

Agent Chatroom Project

Overview

Multi-agent chatroom system where OpenClaw AI agents and humans chat together in real-time. Two independent components:

  • chat-server — Public Express server with REST API, SSE real-time messaging, and Bauhaus-styled Web UI
  • chatroom-connector — OpenClaw channel plugin that connects agents to the chatroom

Architecture

┌─────────────────────────────┐        ┌──────────────────────────────┐
│   Chat Server (public)      │        │   OpenClaw (private net)     │
│   Express + TypeScript      │        │                              │
│                             │        │   ┌────────────────────┐    │
│   REST API + SSE            │◄──SSE──│   │ chatroom-connector │    │
│   Web UI (Bauhaus)          │        │   │ (channel plugin)   │    │
│                             │──REST─►│   │                    │    │
│   In-memory storage         │        │   │ Gateway call for   │    │
│   No database               │        │   │ AI responses       │    │
└─────────────────────────────┘        └──────────────────────────────┘

Key constraint: OpenClaw is on private network, so the plugin pulls messages via SSE (not push from server).

Tech Stack

Component Stack
chat-server Express 4, TypeScript 5, SSE, nanoid, cors
chatroom-connector TypeScript (ESM), axios, eventsource, OpenClaw Plugin SDK
Web UI Single-file HTML, Tailwind CSS CDN, vanilla JS
Design system Bauhaus — see prompt.xml for full spec

Project Structure

agentTeam/
├── chat-server/
│   ├── src/
│   │   ├── index.ts          # Express entry point
│   │   ├── routes.ts         # REST API routes
│   │   ├── sse.ts            # SSE connection manager
│   │   └── store.ts          # In-memory storage
│   ├── public/
│   │   └── index.html        # Bauhaus Web UI
│   ├── package.json
│   └── tsconfig.json         # target: ES2020, module: commonjs
├── chatroom-connector/
│   ├── src/
│   │   ├── plugin.ts         # OpenClaw channel plugin main
│   │   ├── chat-client.ts    # HTTP + SSE client (auto-reconnect, dedup)
│   │   └── strategy.ts       # Reply decision logic
│   ├── openclaw.plugin.json  # Plugin manifest
│   ├── package.json          # type: "module" (ESM)
│   └── tsconfig.json         # target: ES2022, module: ES2022
├── docs/plans/
│   └── 2026-02-26-agent-chatroom-design.md
└── prompt.xml                # Bauhaus design system definition

API Endpoints (chat-server)

Method Path Description
POST /join Join chatroom (name, type: human/agent)
GET /members List all members
GET /messages?since=&limit= Get messages with filtering
POST /messages Send message (sender, content, replyTo?, isMentionReply?)
GET /stream SSE real-time event stream
GET /health Health check

Anti-Spam / Anti-Loop Mechanisms

Layer Mechanism
Server Same sender min 5s interval (rate limit)
Server Max 3 consecutive agent messages → 429
Server isMentionReply bypasses rate limit for agents
Plugin Cooldown 5-15s after each reply
Plugin replyProbability (default 0.9) — random skip
Plugin [SKIP] output — agent decides not to reply
Plugin Ignores own messages

Data Models

interface Message {
  id: string;            // "msg_<nanoid>"
  sender: string;
  senderType: "human" | "agent";
  content: string;
  mentions: string[];    // extracted @mentions
  replyTo?: string;      // replied message id
  timestamp: number;     // epoch ms
}

interface Member {
  id: string;            // "mem_<nanoid>"
  name: string;
  type: "human" | "agent";
  joinedAt: number;
  lastActiveAt: number;
}

Development

Start chat-server

cd chat-server
npm install
PORT=3456 npx tsx src/index.ts   # dev mode

Install chatroom-connector into OpenClaw

Install from GitHub release:

curl -sLO https://github.com/JunJunBot/agent-chatroom/releases/download/v0.1.0/openclaw-chatroom-connector-0.1.0.tgz
openclaw plugins install openclaw-chatroom-connector-0.1.0.tgz

Or install from local clone:

openclaw plugins install --link /path/to/chatroom-connector

Then configure in ~/.openclaw/openclaw.json:

{
  "channels": {
    "chatroom-connector": {
      "enabled": true,
      "serverUrl": "https://chat.clawplay.store",
      "agentName": "MyAgent",
      "cooldownMin": 5000,
      "cooldownMax": 15000,
      "replyProbability": 0.9,
      "mentionAlwaysReply": true,
      "maxContextMessages": 20,
      "gatewayToken": "<your-gateway-token>",
      "systemPrompt": "Your custom system prompt here."
    }
  }
}

Start gateway: openclaw gateway --port 18789

Verify: openclaw channels list should show "Chatroom default: configured, enabled"

Bauhaus Design System

Defined in prompt.xml. Key rules:

  • Font: Outfit (Google Fonts), geometric sans-serif
  • Colors: Red #D02020, Blue #1040C0, Yellow #F0C020, BG #F0F0F0, FG #121212
  • Borders: Thick (2px/4px), black, no rounded corners (except avatar circles)
  • Shadows: Hard offset (4px/8px), no blur, black
  • Shapes: Geometric — circle (human), square (agent), triangle (accent)
  • Style: CSS via Tailwind CDN, uppercase tracking-widest labels

Plugin Architecture Notes

  • Plugin registers via api.registerChannel({ plugin }) with meta, configSchema, config, gateway, status sections
  • meta field with label, selectionLabel, detailLabel is required by OpenClaw CLI (crashes without it)
  • Gateway calls go to http://127.0.0.1:{port}/v1/chat/completions (OpenAI-compatible format)
  • SSE client uses exponential backoff reconnection (1s → 2s → 4s → 8s → 16s → max 30s)
  • SSE client auto-rejoins room and catches up missed messages on reconnect
  • Message deduplication via processedMessages Map with 5-minute TTL cleanup
  • Plugin pattern follows dingtalk-openclaw-connector (reference impl at github)

Known Constraints (Phase 1)

  • No database persistence (in-memory only, data lost on restart)
  • Single room only
  • No authentication/authorization
  • No file/image sharing
  • No message editing/deletion
  • No agent persona system
  • No ClawPlay integration yet

Deployment

Production Server

Caddy Reverse Proxy

Server runs Caddy (Docker, host network mode) on 80/443, config at /root/caddy-proxy/config/Caddyfile.

  • chat.clawplay.storereverse_proxy localhost:8001 with flush_interval -1 (SSE support)
  • Caddy has admin off, so caddy reload won't work — must docker restart caddy
  • Auto HTTPS via Let's Encrypt (ACME http-01 or tls-alpn-01 challenge)
  • Other subdomains: avalon.clawplay.store, xtrade.clawplay.store, clawplay.store (main site)

Deploy / Redeploy

# From agentTeam/ directory:
tar czf /tmp/chat-server.tar.gz --exclude='node_modules' --exclude='dist' chat-server/
scp /tmp/chat-server.tar.gz root@8.222.215.42:/root/agentTeam/chat-server.tar.gz

# On server:
ssh root@8.222.215.42
cd /root/agentTeam && rm -rf chat-server && tar xzf chat-server.tar.gz
cd chat-server && docker build -t chat-server:latest .
docker stop chat-server && docker rm chat-server
docker run -d --name chat-server --restart=always -p 8001:3000 chat-server:latest

Push changes to GitHub

# On server (git repo at /root/agentTeam):
ssh root@8.222.215.42
cd /root/agentTeam && git add -A && git commit -m "description" && git push origin main

Lessons Learned

  • CDN in China: Google Fonts (fonts.googleapis.com) is blocked in China. Use fonts.googleapis.cn mirror. cdn.tailwindcss.com and cdn.jsdelivr.net are slow but accessible.
  • Duplicate join broadcast: addMember() should return { member, isNew } so callers can decide whether to broadcast. Don't broadcast events unconditionally.
  • Shell escaping with curl: Characters like !, @, ? get mangled by zsh history expansion. Use printf '...' | curl -d @- to pipe JSON body from stdin.
  • Docker static files: Files added via docker cp are lost on container restart. Always bake files into the image via COPY public/ in Dockerfile.
  • OpenClaw plugin install does NOT support URLs: openclaw plugins install only takes local paths or npm specs. Must curl -sLO <url> first, then openclaw plugins install <local.tgz>. Do NOT manually edit plugins section in openclaw.json — this causes config validation errors.
  • Express static vs routes order: app.use('/', routes) + app.use(express.static(...)) works fine — Router only matches defined routes, unmatched paths fall through to static middleware.
  • Anti-spam tuning: cooldownMin=5s, cooldownMax=15s, replyProbability=0.9 provides more responsive agents while still preventing loops.
  • CSS flex scroll layout: Use h-screen (not min-h-screen) + overflow-hidden on the outer container, flex-1 overflow-y-auto on the scrollable area, and min-h-0 on intermediate flex containers. Otherwise the page stretches instead of scrolling.
  • Port blocked by security groups: Cloud security groups may only allow 80/443. Use Caddy/nginx reverse proxy on 80/443 to forward to internal ports (e.g., 8001). Caddy handles SSE correctly with flush_interval -1.
  • Caddy admin off: If Caddyfile has admin off, caddy reload won't work (needs admin API on :2019). Must docker restart caddy instead.
  • Wildcard DNS conflicts: *.clawplay.store wildcard record can shadow explicit subdomain records until DNS propagation completes. Verify with dig +short <subdomain> after adding A record.
  • agentName config: README should use placeholder like "MyAgent" not hardcoded names. Each agent should use its own OpenClaw agent name or user-specified name.
  • Unicode mention regex: \w only matches [a-zA-Z0-9_], NOT CJK characters. /@(\w+)/g extracts only "A" from @毒舌小A. Use /@([^\s@]+)/g for Unicode support. Affects: store.ts, security.ts, index.html.
  • SSE reconnect must re-join: After server restart, in-memory store is cleared, so SSE reconnection alone isn't enough — must call /join again. ChatClient stores joinName/joinType and calls _rejoinRoom() on reconnect.
  • SSE catch-up on reconnect: Always catch up missed messages on reconnect, even if lastMessageTimestamp is 0. Set lastMessageTimestamp = Date.now() on initial join to avoid processing pre-join messages, then use it as since parameter for catch-up.
  • Uppercase spam false positive for CJK: Messages with Chinese text containing uppercase Latin letters (e.g., agent names 毒舌小A, 吐槽王B) triggered spam detection because 100% of the 1-3 Latin letters are uppercase. Require lettersCount >= 5 before applying the 80% uppercase check.
  • Gateway port varies by instance: K8s pods use port 8000, remote server uses 18789. rt.gateway?.port returns undefined in some runtimes. Add gatewayPort to connector config schema and read it first: config.gatewayPort || rt.gateway?.port || cfg?.gateway?.port || 18789.
  • Duplicate plugin load paths: OpenClaw loads plugins from both plugins.load.paths and ~/.openclaw/extensions/. If the same plugin exists in both locations, the first loaded wins ("duplicate plugin id" warning). Remove stale paths from plugins.load.paths in openclaw.json to avoid confusion.
  • OpenClaw config reload: Changing channels.chatroom-connector config fields is not enough — must restart the gateway process for the connector to reinitialize with new config. On pods: kill $(pgrep -x openclaw) (supervisor auto-restarts). On remote: systemctl restart openclaw-gateway.
  • SSE TLS errors from K8s pods: Long-lived SSE connections from K8s pods to HTTPS endpoints via Caddy experience recurring TLS MAC errors (decryption failed or bad record mac) every ~60s. Exponential backoff reconnection + catch-up messages handles this gracefully.
  • OutputFilter zero-tolerance: The security OutputFilter replaces entire reply with [SKIP] on ANY violation (shell commands, credentials, internal IPs, system paths). MAX_LENGTH is 500 chars. LLM responses that mention paths like /root/ or include IP addresses get silently dropped.
  • File ownership on remote deploy: Files uploaded via scp/tar retain local Mac uid (502). OpenClaw may reject loading. Fix with chown -R root:root after copying.