Webchat: auto-start server and simplify config
This commit is contained in:
@@ -23,8 +23,8 @@ This flow lets the macOS app act as a full remote control for a Clawdis relay ru
|
|||||||
4) Health checks and Web Chat will now run through this SSH tunnel automatically.
|
4) Health checks and Web Chat will now run through this SSH tunnel automatically.
|
||||||
|
|
||||||
## Web Chat over SSH
|
## Web Chat over SSH
|
||||||
- The relay hosts a loopback-only HTTP server (`clawdis webchat --port <port>`; default 18788).
|
- The relay hosts a loopback-only HTTP server (default 18788, see `webchat.port`).
|
||||||
- The mac app forwards `127.0.0.1:<port>` over SSH (`ssh -L <ephemeral>:127.0.0.1:<port>`), loads `/webchat/info`, and serves the Web Chat UI in-app.
|
- The mac app forwards `127.0.0.1:<port>` over SSH (`ssh -L <ephemeral>:127.0.0.1:<port>`), then loads `/webchat/?session=<key>` in-app.
|
||||||
- Keep the feature enabled in *Settings → Config → Web chat*. Disable it to hide the menu entry entirely.
|
- Keep the feature enabled in *Settings → Config → Web chat*. Disable it to hide the menu entry entirely.
|
||||||
|
|
||||||
## Permissions
|
## Permissions
|
||||||
@@ -38,7 +38,7 @@ This flow lets the macOS app act as a full remote control for a Clawdis relay ru
|
|||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
- **exit 127 / not found**: `clawdis` isn’t on PATH for non-login shells. Add it to `/etc/paths`, your shell rc, or symlink into `/usr/local/bin`/`/opt/homebrew/bin`.
|
- **exit 127 / not found**: `clawdis` isn’t on PATH for non-login shells. Add it to `/etc/paths`, your shell rc, or symlink into `/usr/local/bin`/`/opt/homebrew/bin`.
|
||||||
- **Health probe failed**: check SSH reachability, PATH, and that Baileys is logged in (`clawdis status --json`).
|
- **Health probe failed**: check SSH reachability, PATH, and that Baileys is logged in (`clawdis status --json`).
|
||||||
- **Web Chat stuck**: confirm the remote webchat server is running (`clawdis webchat --json`) and the port matches *Settings → Config*.
|
- **Web Chat stuck**: confirm the relay is running on the remote host and `webchat.enabled` is true; ensure the forwarded port matches *Settings → Config*.
|
||||||
- **Voice Wake**: trigger phrases are forwarded automatically in remote mode; no separate forwarder is needed.
|
- **Voice Wake**: trigger phrases are forwarded automatically in remote mode; no separate forwarder is needed.
|
||||||
|
|
||||||
## Notification sounds
|
## Notification sounds
|
||||||
|
|||||||
@@ -3,19 +3,18 @@
|
|||||||
Updated: 2025-12-08
|
Updated: 2025-12-08
|
||||||
|
|
||||||
## What shipped
|
## What shipped
|
||||||
- A lightweight HTTP server now lives inside the Node relay (`clawdis webchat --port 18788`).
|
- The relay now starts a loopback-only web chat server automatically (default port **18788**, configurable via `webchat.port`).
|
||||||
- It binds to **127.0.0.1** only and serves:
|
- Endpoints:
|
||||||
- `GET /webchat/info?session=<key>` → `{port, token, sessionId, initialMessages, basePath}` plus history from the relay’s session store.
|
- `GET /webchat/info?session=<key>` → `{port, sessionId, initialMessages, basePath}` plus history from the relay’s session store.
|
||||||
- `GET /webchat/*` → static Web Chat assets.
|
- `GET /webchat/*` → static Web Chat assets.
|
||||||
- `POST /webchat/rpc` → runs `clawdis agent --json` and returns `{ ok, payloads?, error? }`.
|
- `POST /webchat/rpc` → runs `clawdis agent --json` and returns `{ ok, payloads?, error? }`.
|
||||||
- The macOS app embeds this UI in a WKWebView. In **remote mode** it first opens an SSH tunnel (`ssh -L <local>:127.0.0.1:<port>`) to the remote host, then loads `/webchat/info` through that tunnel.
|
- The macOS app simply loads `http://127.0.0.1:<port>/webchat/?session=<key>` (or the SSH-forwarded port in remote mode). No Swift bridge is used for sends; all chat traffic stays inside the Node relay.
|
||||||
- Initial messages are preloaded from the relay’s session store, so remote sessions appear immediately.
|
- Initial messages are fetched from `/webchat/info`, so history appears immediately.
|
||||||
- Sending now goes over the HTTP `/webchat/rpc` endpoint (no more AgentRPC fallback).
|
- Enable/disable via `webchat.enabled` (default **true**); set the port with `webchat.port`.
|
||||||
- Feature flag + port live in *Settings → Config → Web chat*. When disabled, the “Open Chat” menu entry is hidden.
|
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
- Loopback only; remote access requires SSH port-forwarding.
|
- Loopback only; remote access requires SSH port-forwarding.
|
||||||
- Optional bearer token support is wired; tokens are returned by `/webchat/info` and accepted by `/webchat/rpc`.
|
- No bearer token; the trust model is “local machine or your SSH tunnel”.
|
||||||
|
|
||||||
## Failure handling
|
## Failure handling
|
||||||
- Bootstrap errors show in-app (“Web chat failed to connect …”) instead of hanging.
|
- Bootstrap errors show in-app (“Web chat failed to connect …”) instead of hanging.
|
||||||
@@ -24,7 +23,7 @@ Updated: 2025-12-08
|
|||||||
## Dev notes
|
## Dev notes
|
||||||
- Static assets stay in `apps/macos/Sources/Clawdis/Resources/WebChat`; the server reads them directly.
|
- Static assets stay in `apps/macos/Sources/Clawdis/Resources/WebChat`; the server reads them directly.
|
||||||
- Server code: `src/webchat/server.ts`.
|
- Server code: `src/webchat/server.ts`.
|
||||||
- CLI entrypoint: `clawdis webchat --json [--port N]`.
|
- CLI entrypoint (optional): `clawdis webchat --json [--port N]` to query/start manually.
|
||||||
- Mac glue: `WebChatWindow.swift` (bootstrap + tunnel) and `WebChatTunnel` (SSH -L).
|
- Mac glue: `WebChatWindow.swift` (bootstrap + tunnel) and `WebChatTunnel` (SSH -L).
|
||||||
|
|
||||||
## TODO / nice-to-haves
|
## TODO / nice-to-haves
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ const monitorWebProvider = vi.fn();
|
|||||||
const logWebSelfId = vi.fn();
|
const logWebSelfId = vi.fn();
|
||||||
const waitForever = vi.fn();
|
const waitForever = vi.fn();
|
||||||
const monitorTelegramProvider = vi.fn();
|
const monitorTelegramProvider = vi.fn();
|
||||||
const startWebChatServer = vi.fn(async () => ({ port: 18788, token: null }));
|
const startWebChatServer = vi.fn(async () => ({ port: 18788 }));
|
||||||
|
const ensureWebChatServerFromConfig = vi.fn(async () => ({ port: 18788 }));
|
||||||
|
|
||||||
const runtime = {
|
const runtime = {
|
||||||
log: vi.fn(),
|
log: vi.fn(),
|
||||||
@@ -29,6 +30,7 @@ vi.mock("../telegram/monitor.js", () => ({
|
|||||||
}));
|
}));
|
||||||
vi.mock("../webchat/server.js", () => ({
|
vi.mock("../webchat/server.js", () => ({
|
||||||
startWebChatServer,
|
startWebChatServer,
|
||||||
|
ensureWebChatServerFromConfig,
|
||||||
getWebChatServer: () => null,
|
getWebChatServer: () => null,
|
||||||
}));
|
}));
|
||||||
vi.mock("./deps.js", () => ({
|
vi.mock("./deps.js", () => ({
|
||||||
@@ -104,7 +106,7 @@ describe("cli program", () => {
|
|||||||
await program.parseAsync(["webchat", "--json"], { from: "user" });
|
await program.parseAsync(["webchat", "--json"], { from: "user" });
|
||||||
expect(startWebChatServer).toHaveBeenCalled();
|
expect(startWebChatServer).toHaveBeenCalled();
|
||||||
expect(runtime.log).toHaveBeenCalledWith(
|
expect(runtime.log).toHaveBeenCalledWith(
|
||||||
JSON.stringify({ port: 18788, token: null, basePath: "/webchat/", host: "127.0.0.1" }),
|
JSON.stringify({ port: 18788, basePath: "/webchat/", host: "127.0.0.1" }),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,7 +17,11 @@ import {
|
|||||||
setHeartbeatsEnabled,
|
setHeartbeatsEnabled,
|
||||||
type WebMonitorTuning,
|
type WebMonitorTuning,
|
||||||
} from "../provider-web.js";
|
} from "../provider-web.js";
|
||||||
import { startWebChatServer, getWebChatServer } from "../webchat/server.js";
|
import {
|
||||||
|
startWebChatServer,
|
||||||
|
getWebChatServer,
|
||||||
|
ensureWebChatServerFromConfig,
|
||||||
|
} from "../webchat/server.js";
|
||||||
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
||||||
import { VERSION } from "../version.js";
|
import { VERSION } from "../version.js";
|
||||||
import {
|
import {
|
||||||
@@ -509,6 +513,16 @@ Examples:
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
|
// Start loopback web chat server unless disabled.
|
||||||
|
const webchatServer = await ensureWebChatServerFromConfig();
|
||||||
|
if (webchatServer) {
|
||||||
|
defaultRuntime.log(
|
||||||
|
info(
|
||||||
|
`webchat listening on http://127.0.0.1:${webchatServer.port}/webchat/`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await monitorWebProvider(
|
await monitorWebProvider(
|
||||||
Boolean(opts.verbose),
|
Boolean(opts.verbose),
|
||||||
undefined,
|
undefined,
|
||||||
@@ -748,7 +762,6 @@ Shows token usage per session when the agent reports it; set inbound.reply.agent
|
|||||||
const server = await startWebChatServer(port);
|
const server = await startWebChatServer(port);
|
||||||
const payload = {
|
const payload = {
|
||||||
port: server.port,
|
port: server.port,
|
||||||
token: server.token ?? null,
|
|
||||||
basePath: "/webchat/",
|
basePath: "/webchat/",
|
||||||
host: "127.0.0.1",
|
host: "127.0.0.1",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ export type WebConfig = {
|
|||||||
reconnect?: WebReconnectConfig;
|
reconnect?: WebReconnectConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type WebChatConfig = {
|
||||||
|
enabled?: boolean;
|
||||||
|
port?: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type TelegramConfig = {
|
export type TelegramConfig = {
|
||||||
botToken?: string;
|
botToken?: string;
|
||||||
requireMention?: boolean;
|
requireMention?: boolean;
|
||||||
@@ -101,6 +106,7 @@ export type WarelayConfig = {
|
|||||||
};
|
};
|
||||||
web?: WebConfig;
|
web?: WebConfig;
|
||||||
telegram?: TelegramConfig;
|
telegram?: TelegramConfig;
|
||||||
|
webchat?: WebChatConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
// New branding path (preferred)
|
// New branding path (preferred)
|
||||||
@@ -226,6 +232,12 @@ const WarelaySchema = z.object({
|
|||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
|
webchat: z
|
||||||
|
.object({
|
||||||
|
enabled: z.boolean().optional(),
|
||||||
|
port: z.number().int().positive().optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
telegram: z
|
telegram: z
|
||||||
.object({
|
.object({
|
||||||
botToken: z.string().optional(),
|
botToken: z.string().optional(),
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ const WEBCHAT_DEFAULT_PORT = 18788;
|
|||||||
type WebChatServerState = {
|
type WebChatServerState = {
|
||||||
server: http.Server;
|
server: http.Server;
|
||||||
port: number;
|
port: number;
|
||||||
token?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let state: WebChatServerState | null = null;
|
let state: WebChatServerState | null = null;
|
||||||
@@ -119,7 +118,7 @@ function notFound(res: http.ServerResponse) {
|
|||||||
res.end("Not Found");
|
res.end("Not Found");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function startWebChatServer(port = WEBCHAT_DEFAULT_PORT, token?: string) {
|
export async function startWebChatServer(port = WEBCHAT_DEFAULT_PORT) {
|
||||||
if (state) return state;
|
if (state) return state;
|
||||||
|
|
||||||
const root = resolveWebRoot();
|
const root = resolveWebRoot();
|
||||||
@@ -147,7 +146,6 @@ export async function startWebChatServer(port = WEBCHAT_DEFAULT_PORT, token?: st
|
|||||||
res.end(
|
res.end(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
port,
|
port,
|
||||||
token: token ?? null,
|
|
||||||
sessionKey,
|
sessionKey,
|
||||||
storePath,
|
storePath,
|
||||||
sessionId,
|
sessionId,
|
||||||
@@ -202,11 +200,23 @@ export async function startWebChatServer(port = WEBCHAT_DEFAULT_PORT, token?: st
|
|||||||
server.listen(port, "127.0.0.1", () => resolve());
|
server.listen(port, "127.0.0.1", () => resolve());
|
||||||
});
|
});
|
||||||
|
|
||||||
state = { server, port, token };
|
state = { server, port };
|
||||||
logDebug(info(`webchat server listening on 127.0.0.1:${port}`));
|
logDebug(info(`webchat server listening on 127.0.0.1:${port}`));
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function ensureWebChatServerFromConfig() {
|
||||||
|
const cfg = loadConfig();
|
||||||
|
if (cfg.webchat?.enabled === false) return null;
|
||||||
|
const port = cfg.webchat?.port ?? WEBCHAT_DEFAULT_PORT;
|
||||||
|
try {
|
||||||
|
return await startWebChatServer(port);
|
||||||
|
} catch (err) {
|
||||||
|
logDebug(`webchat server failed to start: ${String(err)}`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function getWebChatServer(): WebChatServerState | null {
|
export function getWebChatServer(): WebChatServerState | null {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user