openclaw-webchat-plugin 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -0
- package/index.js +17 -0
- package/openclaw.plugin.json +38 -0
- package/package.json +23 -0
- package/src/accounts.js +27 -0
- package/src/channel.js +101 -0
- package/src/const.js +5 -0
- package/src/runtime.js +6 -0
- package/src/ws-client.js +257 -0
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# openclaw-webchat-plugin
|
|
2
|
+
|
|
3
|
+
Browser WebChat channel for OpenClaw. Chat with your OpenClaw agents directly from a web browser — no third-party IM platform needed.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- OpenClaw Gateway >= 2026.3.28
|
|
8
|
+
- A WebChat Chat Server running at a public (or reachable) address
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
openclaw plugins install openclaw-webchat-plugin
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
### 1. Deploy the Chat Server
|
|
19
|
+
|
|
20
|
+
On any machine with a public IP (or reachable from both browser and plugin), clone the [repo](https://github.com/wutao667/openclaw-wechat) and run:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
git clone https://github.com/wutao667/openclaw-wechat.git
|
|
24
|
+
cd openclaw-wechat/server
|
|
25
|
+
npm install
|
|
26
|
+
node server.js
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The Chat Server listens on port `3100`:
|
|
30
|
+
- Browser → `ws://<host>:3100/ws`
|
|
31
|
+
- Plugin → `ws://<host>:3100/plugin`
|
|
32
|
+
|
|
33
|
+
For production, add a TLS reverse proxy (Caddy/Nginx) for WSS support.
|
|
34
|
+
|
|
35
|
+
### 2. Configure Channel
|
|
36
|
+
|
|
37
|
+
Edit `~/.openclaw/openclaw.json`:
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"channels": {
|
|
42
|
+
"webchat": {
|
|
43
|
+
"enabled": true,
|
|
44
|
+
"serverUrl": "wss://your-domain.com/plugin",
|
|
45
|
+
"pluginId": "webchat-openclaw-plugin",
|
|
46
|
+
"agents": [
|
|
47
|
+
{ "agentId": "main", "name": "我的助手" }
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
| Field | Description | Default |
|
|
55
|
+
|-------|-------------|---------|
|
|
56
|
+
| `enabled` | Enable this channel | `true` |
|
|
57
|
+
| `serverUrl` | Chat Server `/plugin` WebSocket URL | `ws://localhost:3100/plugin` |
|
|
58
|
+
| `agents` | Agents exposed to browser users | `[{ agentId: "nezha", name: "哪吒" }]` |
|
|
59
|
+
| `dmPolicy` | Direct message policy | `open` |
|
|
60
|
+
|
|
61
|
+
### 3. Restart Gateway
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
openclaw gateway restart
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 4. Open Browser
|
|
68
|
+
|
|
69
|
+
Visit `https://your-domain.com`, enter a username, select an agent, and start chatting.
|
|
70
|
+
|
|
71
|
+
## Architecture
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
Browser ──WS──→ Chat Server (public) ──WS──→ Plugin ──dispatch──→ OpenClaw Core → Agent
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The plugin initiates an outbound WebSocket connection to the Chat Server (long-connection, not webhook). This means OpenClaw instances behind NAT/firewalls can still connect as long as they have outbound internet access.
|
|
78
|
+
|
|
79
|
+
## Multiple OpenClaw Instances
|
|
80
|
+
|
|
81
|
+
Each instance installs the plugin with a different `agentId`, all connecting to the same Chat Server. The server routes messages by `agentId`.
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
// Instance A
|
|
85
|
+
{ "agents": [{ "agentId": "instance-a", "name": "Bot A" }] }
|
|
86
|
+
|
|
87
|
+
// Instance B
|
|
88
|
+
{ "agents": [{ "agentId": "instance-b", "name": "Bot B" }] }
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Session Isolation
|
|
92
|
+
|
|
93
|
+
- Session key: `webchat:{userId}:{agentId}`
|
|
94
|
+
- Same user + same agent across browsers → shared history
|
|
95
|
+
- Different users → completely isolated
|
|
96
|
+
|
|
97
|
+
## Development
|
|
98
|
+
|
|
99
|
+
Full source code at [github.com/wutao667/openclaw-wechat](https://github.com/wutao667/openclaw-wechat).
|
package/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
2
|
+
import { webchatPlugin } from "./src/channel.js";
|
|
3
|
+
import { setWebChatRuntime } from "./src/runtime.js";
|
|
4
|
+
|
|
5
|
+
const plugin = {
|
|
6
|
+
id: "webchat-openclaw-plugin",
|
|
7
|
+
name: "WebChat",
|
|
8
|
+
description: "Browser WebChat channel for OpenClaw",
|
|
9
|
+
configSchema: emptyPluginConfigSchema(),
|
|
10
|
+
|
|
11
|
+
register(api) {
|
|
12
|
+
setWebChatRuntime(api.runtime);
|
|
13
|
+
api.registerChannel({ plugin: webchatPlugin });
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default plugin;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "webchat-openclaw-plugin",
|
|
3
|
+
"kind": "channel",
|
|
4
|
+
"channels": ["webchat"],
|
|
5
|
+
"name": "WebChat",
|
|
6
|
+
"description": "Browser-based WebChat channel for OpenClaw",
|
|
7
|
+
"configSchema": {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"additionalProperties": false,
|
|
10
|
+
"properties": {}
|
|
11
|
+
},
|
|
12
|
+
"channelConfigs": {
|
|
13
|
+
"webchat": {
|
|
14
|
+
"schema": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"additionalProperties": false,
|
|
17
|
+
"properties": {
|
|
18
|
+
"enabled": { "type": "boolean" },
|
|
19
|
+
"serverUrl": { "type": "string", "description": "Chat Server WebSocket URL" },
|
|
20
|
+
"pluginId": { "type": "string" },
|
|
21
|
+
"agents": {
|
|
22
|
+
"type": "array",
|
|
23
|
+
"items": {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"properties": {
|
|
26
|
+
"agentId": { "type": "string" },
|
|
27
|
+
"name": { "type": "string" }
|
|
28
|
+
},
|
|
29
|
+
"required": ["agentId"]
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"allowFrom": { "type": "array", "items": { "type": "string" } },
|
|
33
|
+
"dmPolicy": { "type": "string", "enum": ["open", "allowlist", "pairing"] }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openclaw-webchat-plugin",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"ws": "^8.18.0"
|
|
8
|
+
},
|
|
9
|
+
"peerDependencies": {
|
|
10
|
+
"openclaw": ">=2026.3.28"
|
|
11
|
+
},
|
|
12
|
+
"openclaw": {
|
|
13
|
+
"extensions": [
|
|
14
|
+
"./index.js"
|
|
15
|
+
],
|
|
16
|
+
"channel": {
|
|
17
|
+
"id": "webchat",
|
|
18
|
+
"label": "WebChat",
|
|
19
|
+
"selectionLabel": "WebChat",
|
|
20
|
+
"blurb": "Browser chat channel for OpenClaw"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
package/src/accounts.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { DEFAULT_ACCOUNT_ID, DEFAULT_PLUGIN_ID, DEFAULT_SERVER_URL } from "./const.js";
|
|
2
|
+
|
|
3
|
+
export function resolveWebChatConfig(cfg) {
|
|
4
|
+
if (!cfg || !cfg.channels) return {};
|
|
5
|
+
return cfg.channels.webchat || {};
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function listWebChatAccountIds(_cfg) {
|
|
9
|
+
return [DEFAULT_ACCOUNT_ID];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function resolveDefaultWebChatAccountId(_cfg) {
|
|
13
|
+
return DEFAULT_ACCOUNT_ID;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function resolveWebChatAccount(cfg, accountId = DEFAULT_ACCOUNT_ID) {
|
|
17
|
+
const section = resolveWebChatConfig(cfg);
|
|
18
|
+
return {
|
|
19
|
+
accountId,
|
|
20
|
+
enabled: section.enabled !== false,
|
|
21
|
+
serverUrl: section.serverUrl || DEFAULT_SERVER_URL,
|
|
22
|
+
pluginId: section.pluginId || DEFAULT_PLUGIN_ID,
|
|
23
|
+
agents: section.agents || [{ agentId: "nezha", name: "哪吒" }],
|
|
24
|
+
allowFrom: section.allowFrom || ["*"],
|
|
25
|
+
dmPolicy: section.dmPolicy || "open",
|
|
26
|
+
};
|
|
27
|
+
}
|
package/src/channel.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { CHANNEL_ID, TEXT_CHUNK_LIMIT } from "./const.js";
|
|
2
|
+
import { startWebChatWsClient, stopWebChatWsClient, sendOutgoingMessage } from "./ws-client.js";
|
|
3
|
+
import { listWebChatAccountIds, resolveWebChatAccount, resolveDefaultWebChatAccountId } from "./accounts.js";
|
|
4
|
+
import { getWebChatRuntime } from "./runtime.js";
|
|
5
|
+
|
|
6
|
+
export const webchatPlugin = {
|
|
7
|
+
id: CHANNEL_ID,
|
|
8
|
+
meta: {
|
|
9
|
+
label: "WebChat",
|
|
10
|
+
selectionLabel: "WebChat",
|
|
11
|
+
blurb: "Browser chat channel for OpenClaw",
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
capabilities: {
|
|
15
|
+
blockStreaming: true,
|
|
16
|
+
directChatOnly: true,
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
config: {
|
|
20
|
+
listAccountIds: listWebChatAccountIds,
|
|
21
|
+
resolveAccount: async ({ cfg, accountId }) => resolveWebChatAccount(cfg, accountId),
|
|
22
|
+
isConfigured: async ({ cfg, accountId }) => {
|
|
23
|
+
const account = resolveWebChatAccount(cfg, accountId);
|
|
24
|
+
return account.enabled;
|
|
25
|
+
},
|
|
26
|
+
describeAccount: async ({ cfg, accountId }) => {
|
|
27
|
+
const account = resolveWebChatAccount(cfg, accountId);
|
|
28
|
+
return {
|
|
29
|
+
label: CHANNEL_ID,
|
|
30
|
+
description: "WebChat channel",
|
|
31
|
+
configured: account.enabled,
|
|
32
|
+
inbound: true,
|
|
33
|
+
outbound: true,
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
resolveAllowFrom: async ({ cfg, accountId }) => {
|
|
37
|
+
const account = resolveWebChatAccount(cfg, accountId);
|
|
38
|
+
return { allowFrom: account.allowFrom };
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
security: {
|
|
43
|
+
resolveDmPolicy: async ({ cfg, accountId }) => {
|
|
44
|
+
const account = resolveWebChatAccount(cfg, accountId);
|
|
45
|
+
return account.dmPolicy;
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
messaging: {
|
|
50
|
+
normalizeTarget: async ({ to }) => {
|
|
51
|
+
return { target: to };
|
|
52
|
+
},
|
|
53
|
+
targetResolver: async ({ to }) => {
|
|
54
|
+
const result = { targets: [{ to, chatId: to }] };
|
|
55
|
+
return result;
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
outbound: {
|
|
60
|
+
deliveryMode: "gateway",
|
|
61
|
+
chunker: (text, limit) => getWebChatRuntime().channel.text.chunkMarkdownText(text, limit),
|
|
62
|
+
textChunkLimit: TEXT_CHUNK_LIMIT,
|
|
63
|
+
sendText: async ({ to, text, accountId, cfg }) => {
|
|
64
|
+
return sendOutgoingMessage({ to, text, accountId, cfg });
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
status: {
|
|
69
|
+
defaultRuntime: async ({ cfg }) => {
|
|
70
|
+
return {
|
|
71
|
+
accountId: resolveDefaultWebChatAccountId(cfg),
|
|
72
|
+
status: "starting",
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
collectStatusIssues: async ({ cfg, accountId }) => {
|
|
76
|
+
const account = resolveWebChatAccount(cfg, accountId);
|
|
77
|
+
const issues = [];
|
|
78
|
+
if (!account.enabled) {
|
|
79
|
+
issues.push({ kind: "warning", message: "WebChat channel is disabled" });
|
|
80
|
+
}
|
|
81
|
+
return issues;
|
|
82
|
+
},
|
|
83
|
+
buildChannelSummary: async ({ cfg, accountId }) => {
|
|
84
|
+
const account = resolveWebChatAccount(cfg, accountId);
|
|
85
|
+
return { label: CHANNEL_ID, summary: `WebChat: ${account.serverUrl}` };
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
gateway: {
|
|
90
|
+
startAccount: async ({ cfg, accountId }) => {
|
|
91
|
+
const account = resolveWebChatAccount(cfg, accountId);
|
|
92
|
+
const runtime = getWebChatRuntime();
|
|
93
|
+
startWebChatWsClient(runtime, account);
|
|
94
|
+
return { accountId };
|
|
95
|
+
},
|
|
96
|
+
logoutAccount: async ({ cfg, accountId }) => {
|
|
97
|
+
stopWebChatWsClient();
|
|
98
|
+
return { accountId };
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
};
|
package/src/const.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export const CHANNEL_ID = "webchat";
|
|
2
|
+
export const DEFAULT_ACCOUNT_ID = "default";
|
|
3
|
+
export const DEFAULT_PLUGIN_ID = "webchat-openclaw-plugin";
|
|
4
|
+
export const DEFAULT_SERVER_URL = process.env.WEBCHAT_SERVER_URL || "ws://localhost:3100/plugin";
|
|
5
|
+
export const TEXT_CHUNK_LIMIT = 3500;
|
package/src/runtime.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
|
2
|
+
|
|
3
|
+
const { setRuntime: setWebChatRuntime, getRuntime: getWebChatRuntime } =
|
|
4
|
+
createPluginRuntimeStore("WebChat runtime not initialized");
|
|
5
|
+
|
|
6
|
+
export { setWebChatRuntime, getWebChatRuntime };
|
package/src/ws-client.js
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import { WebSocket } from "ws";
|
|
5
|
+
import { CHANNEL_ID } from "./const.js";
|
|
6
|
+
|
|
7
|
+
let state = {
|
|
8
|
+
ws: null,
|
|
9
|
+
runtime: null,
|
|
10
|
+
account: null,
|
|
11
|
+
reconnectTimer: null,
|
|
12
|
+
heartbeatTimer: null,
|
|
13
|
+
reconnectAttempts: 0,
|
|
14
|
+
intentionalClose: false,
|
|
15
|
+
fullCfg: {},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function loadGatewayConfig() {
|
|
19
|
+
try {
|
|
20
|
+
const configPath = path.resolve(os.homedir(), ".openclaw/openclaw.json");
|
|
21
|
+
const content = fs.readFileSync(configPath, "utf8");
|
|
22
|
+
return JSON.parse(content);
|
|
23
|
+
} catch (err) {
|
|
24
|
+
console.warn("[webchat] could not load gateway config:", err.message);
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function clearReconnectTimer() {
|
|
30
|
+
if (state.reconnectTimer) {
|
|
31
|
+
clearTimeout(state.reconnectTimer);
|
|
32
|
+
state.reconnectTimer = null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function clearHeartbeatTimer() {
|
|
37
|
+
if (state.heartbeatTimer) {
|
|
38
|
+
clearInterval(state.heartbeatTimer);
|
|
39
|
+
state.heartbeatTimer = null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function startHeartbeat() {
|
|
44
|
+
clearHeartbeatTimer();
|
|
45
|
+
state.heartbeatTimer = setInterval(() => {
|
|
46
|
+
if (state.ws?.readyState === WebSocket.OPEN) {
|
|
47
|
+
state.ws.send(JSON.stringify({ type: "ping", pluginId: state.account?.pluginId }));
|
|
48
|
+
}
|
|
49
|
+
}, 30_000);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function scheduleReconnect() {
|
|
53
|
+
if (state.intentionalClose || state.reconnectTimer) return;
|
|
54
|
+
|
|
55
|
+
state.reconnectAttempts += 1;
|
|
56
|
+
const delay = Math.min(30_000, Math.max(2_000, 2 ** state.reconnectAttempts * 1_000));
|
|
57
|
+
|
|
58
|
+
state.reconnectTimer = setTimeout(() => {
|
|
59
|
+
state.reconnectTimer = null;
|
|
60
|
+
connectWebSocket();
|
|
61
|
+
}, delay);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function connectWebSocket() {
|
|
65
|
+
const account = state.account;
|
|
66
|
+
if (!account?.serverUrl) return;
|
|
67
|
+
|
|
68
|
+
clearReconnectTimer();
|
|
69
|
+
clearHeartbeatTimer();
|
|
70
|
+
|
|
71
|
+
const ws = new WebSocket(account.serverUrl);
|
|
72
|
+
state.ws = ws;
|
|
73
|
+
|
|
74
|
+
ws.on("open", () => {
|
|
75
|
+
state.reconnectAttempts = 0;
|
|
76
|
+
ws.send(
|
|
77
|
+
JSON.stringify({
|
|
78
|
+
type: "register",
|
|
79
|
+
pluginId: account.pluginId,
|
|
80
|
+
agents: account.agents,
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
startHeartbeat();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
ws.on("message", async (data) => {
|
|
87
|
+
let message;
|
|
88
|
+
try {
|
|
89
|
+
message = JSON.parse(String(data));
|
|
90
|
+
} catch (err) {
|
|
91
|
+
console.error("[webchat] failed to parse websocket message", err);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (message.type === "incoming") {
|
|
96
|
+
try {
|
|
97
|
+
await dispatchIncoming(message);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.error("[webchat] failed to dispatch incoming message", err);
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (message.type === "registered") {
|
|
105
|
+
console.log("[webchat] registered with chat server");
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (message.type === "agent_list") {
|
|
110
|
+
console.log("[webchat] agent list received", message.agents || []);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
ws.on("close", () => {
|
|
115
|
+
if (state.ws === ws) {
|
|
116
|
+
state.ws = null;
|
|
117
|
+
}
|
|
118
|
+
clearHeartbeatTimer();
|
|
119
|
+
if (!state.intentionalClose) {
|
|
120
|
+
scheduleReconnect();
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
ws.on("error", (err) => {
|
|
125
|
+
console.error("[webchat] websocket error", err);
|
|
126
|
+
ws.close();
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function startWebChatWsClient(runtime, account) {
|
|
131
|
+
state.runtime = runtime;
|
|
132
|
+
state.account = account;
|
|
133
|
+
state.fullCfg = loadGatewayConfig();
|
|
134
|
+
state.intentionalClose = false;
|
|
135
|
+
connectWebSocket();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function stopWebChatWsClient() {
|
|
139
|
+
state.intentionalClose = true;
|
|
140
|
+
clearReconnectTimer();
|
|
141
|
+
clearHeartbeatTimer();
|
|
142
|
+
|
|
143
|
+
if (state.ws) {
|
|
144
|
+
state.ws.close();
|
|
145
|
+
state.ws = null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function sendOutgoingMessage({ to, text, accountId, cfg }) {
|
|
150
|
+
const acc = state.account;
|
|
151
|
+
const rawTo = String(to || "");
|
|
152
|
+
let userId = rawTo.replace(/^webchat:/, "");
|
|
153
|
+
if (!userId) {
|
|
154
|
+
userId = rawTo.match(/webchat:([^:\s/]+)/)?.[1] || rawTo.match(/([^:\s/]+)$/)?.[1] || "";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const agentId = acc?.agents?.[0]?.agentId || "default";
|
|
158
|
+
const messageId = `webchat_out_${Date.now()}`;
|
|
159
|
+
|
|
160
|
+
if (!state.ws || state.ws.readyState !== WebSocket.OPEN) {
|
|
161
|
+
throw new Error("WebChat websocket is not connected");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
state.ws.send(
|
|
165
|
+
JSON.stringify({
|
|
166
|
+
type: "outgoing",
|
|
167
|
+
pluginId: acc?.pluginId,
|
|
168
|
+
agentId,
|
|
169
|
+
userId,
|
|
170
|
+
content: text,
|
|
171
|
+
messageId,
|
|
172
|
+
}),
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
return { channel: CHANNEL_ID, messageId, chatId: userId };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function dispatchIncoming(message) {
|
|
179
|
+
const core = state.runtime;
|
|
180
|
+
if (!core) return;
|
|
181
|
+
|
|
182
|
+
const content = String(message.content || message.Body || "");
|
|
183
|
+
const userId = String(message.userId || "");
|
|
184
|
+
const userName = String(message.userName || userId);
|
|
185
|
+
const agentId = String(message.agentId || "");
|
|
186
|
+
const conversationId = String(message.conversationId || userId);
|
|
187
|
+
const acc = state.account;
|
|
188
|
+
const cfg = state.fullCfg || {};
|
|
189
|
+
|
|
190
|
+
const route = core.channel.routing.resolveAgentRoute({
|
|
191
|
+
cfg,
|
|
192
|
+
channel: CHANNEL_ID,
|
|
193
|
+
accountId: acc.accountId,
|
|
194
|
+
peer: { kind: "direct", id: conversationId },
|
|
195
|
+
agentId: agentId || acc.agents?.[0]?.agentId || "default",
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
route.sessionKey = `${CHANNEL_ID}:${userId}:${agentId}`;
|
|
199
|
+
|
|
200
|
+
const storePath = core.channel.session.resolveStorePath(cfg.session?.store, {
|
|
201
|
+
agentId: route.agentId,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
205
|
+
Body: content,
|
|
206
|
+
RawBody: content,
|
|
207
|
+
CommandBody: content,
|
|
208
|
+
MessageSid: message.messageId || `webchat_${Date.now()}`,
|
|
209
|
+
From: `${CHANNEL_ID}:${userId}`,
|
|
210
|
+
To: `${CHANNEL_ID}:${conversationId}`,
|
|
211
|
+
SenderId: userId,
|
|
212
|
+
SessionKey: route.sessionKey,
|
|
213
|
+
AccountId: route.accountId,
|
|
214
|
+
ChatType: "direct",
|
|
215
|
+
ConversationLabel: `user:${userName}`,
|
|
216
|
+
Timestamp: Date.now(),
|
|
217
|
+
Provider: CHANNEL_ID,
|
|
218
|
+
Surface: CHANNEL_ID,
|
|
219
|
+
OriginatingChannel: CHANNEL_ID,
|
|
220
|
+
OriginatingTo: `${CHANNEL_ID}:${conversationId}`,
|
|
221
|
+
CommandAuthorized: true,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
await core.channel.session.recordInboundSession({
|
|
225
|
+
storePath,
|
|
226
|
+
sessionKey: ctxPayload.SessionKey || route.sessionKey,
|
|
227
|
+
ctx: ctxPayload,
|
|
228
|
+
updateLastRoute: {
|
|
229
|
+
sessionKey: route.mainSessionKey || route.sessionKey,
|
|
230
|
+
channel: CHANNEL_ID,
|
|
231
|
+
to: `${CHANNEL_ID}:${conversationId}`,
|
|
232
|
+
accountId: route.accountId,
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
237
|
+
ctx: ctxPayload,
|
|
238
|
+
cfg,
|
|
239
|
+
dispatcherOptions: {
|
|
240
|
+
deliver: async (payload) => {
|
|
241
|
+
if (!payload.text) return;
|
|
242
|
+
if (state.ws?.readyState === WebSocket.OPEN) {
|
|
243
|
+
state.ws.send(
|
|
244
|
+
JSON.stringify({
|
|
245
|
+
type: "outgoing",
|
|
246
|
+
pluginId: acc.pluginId,
|
|
247
|
+
agentId: message.agentId,
|
|
248
|
+
userId,
|
|
249
|
+
content: payload.text,
|
|
250
|
+
messageId: `webchat_reply_${Date.now()}`,
|
|
251
|
+
}),
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
}
|