@fontdo/5g-message 1.0.4
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 +88 -0
- package/index.ts +872 -0
- package/openclaw.plugin.json +37 -0
- package/package.json +38 -0
- package/tsconfig.json +20 -0
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Fontdo 5G Message Channel Plugin
|
|
2
|
+
|
|
3
|
+
OpenClaw 通道插件,用于连接 Fontdo 5G 消息平台。
|
|
4
|
+
|
|
5
|
+
## 协议支持
|
|
6
|
+
|
|
7
|
+
- **传输协议**: WebSocket over TLS (wss://)
|
|
8
|
+
- **消息协议**: JSON-RPC 2.0
|
|
9
|
+
- **认证方式**: Headers 签名认证
|
|
10
|
+
|
|
11
|
+
## 安装
|
|
12
|
+
|
|
13
|
+
### 方法 1: 本地安装
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# 将插件复制到 OpenClaw 扩展目录
|
|
17
|
+
cp -r . ~/.openclaw/extensions/fontdo-5g-message
|
|
18
|
+
|
|
19
|
+
# 或者使用链接方式(开发模式)
|
|
20
|
+
openclaw plugins install -l /path/to/fontdo-5g-message
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### 方法 2: NPM 安装
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
openclaw plugins install @openclaw/fontdo-5g-message
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 配置
|
|
30
|
+
|
|
31
|
+
在 `~/.openclaw/openclaw.json` 中添加配置:
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"channels": {
|
|
36
|
+
"fontdo-5g-message": {
|
|
37
|
+
"enabled": true,
|
|
38
|
+
"accounts": {
|
|
39
|
+
"default": {
|
|
40
|
+
"host": "5g.fontdo.com",
|
|
41
|
+
"appId": "your-app-id",
|
|
42
|
+
"appKey": "your-app-secret",
|
|
43
|
+
"botName": "OpenClaw Bot",
|
|
44
|
+
"enabled": true
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"dmPolicy": "pairing",
|
|
48
|
+
"allowFrom": []
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 配置项说明
|
|
55
|
+
|
|
56
|
+
| 字段 | 说明 | 必填 |
|
|
57
|
+
|------|------|------|
|
|
58
|
+
| `host` | IM 服务器地址(不含协议前缀) | ✅ |
|
|
59
|
+
| `appId` | 应用 ID | ✅ |
|
|
60
|
+
| `appKey` | 应用密钥 | ✅ |
|
|
61
|
+
| `botName` | 机器人显示名称 | ❌ |
|
|
62
|
+
| `enabled` | 是否启用该账号 | ❌ (默认 true) |
|
|
63
|
+
|
|
64
|
+
### 多账号配置
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"channels": {
|
|
69
|
+
"fontdo-5g-message": {
|
|
70
|
+
"defaultAccount": "prod",
|
|
71
|
+
"accounts": {
|
|
72
|
+
"prod": {
|
|
73
|
+
"host": "5g.fontdo.com",
|
|
74
|
+
"appId": "your-app-id",
|
|
75
|
+
"appKey": "your-app-secret",
|
|
76
|
+
"botName": "Production Bot"
|
|
77
|
+
},
|
|
78
|
+
"dev": {
|
|
79
|
+
"host": "test.fontdo.com",
|
|
80
|
+
"appId": "your-app-id",
|
|
81
|
+
"appKey": "your-app-secret",
|
|
82
|
+
"botName": "Dev Bot"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
package/index.ts
ADDED
|
@@ -0,0 +1,872 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenClaw Channel Plugin for Custom IM Platform
|
|
3
|
+
*
|
|
4
|
+
* This plugin connects OpenClaw to your custom IM platform via WebSocket
|
|
5
|
+
* using JSON-RPC 2.0 protocol.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { OpenClawPluginApi, ChannelPlugin, RuntimeEnv, ClawdbotConfig, PluginRuntime } from "openclaw/plugin-sdk";
|
|
9
|
+
import {
|
|
10
|
+
emptyPluginConfigSchema,
|
|
11
|
+
buildBaseChannelStatusSummary,
|
|
12
|
+
createDefaultChannelRuntimeState,
|
|
13
|
+
DEFAULT_ACCOUNT_ID,
|
|
14
|
+
createReplyPrefixContext,
|
|
15
|
+
} from "openclaw/plugin-sdk";
|
|
16
|
+
import WebSocket from "ws";
|
|
17
|
+
import crypto from "crypto";
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Runtime (set during plugin registration)
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
let pluginRuntime: PluginRuntime | null = null;
|
|
24
|
+
|
|
25
|
+
function getRuntime(): PluginRuntime {
|
|
26
|
+
if (!pluginRuntime) {
|
|
27
|
+
throw new Error("Custom IM plugin runtime not initialized");
|
|
28
|
+
}
|
|
29
|
+
return pluginRuntime;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// Global Connection Registry (one WebSocket connection per account)
|
|
34
|
+
// ============================================================================
|
|
35
|
+
|
|
36
|
+
const activeConnections: Map<string, CustomIMClient> = new Map();
|
|
37
|
+
|
|
38
|
+
function registerConnection(accountId: string, client: CustomIMClient): void {
|
|
39
|
+
activeConnections.set(accountId, client);
|
|
40
|
+
console.log(`[CustomIM] Registered connection for account ${accountId}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function unregisterConnection(accountId: string): void {
|
|
44
|
+
activeConnections.delete(accountId);
|
|
45
|
+
console.log(`[CustomIM] Unregistered connection for account ${accountId}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getActiveConnection(accountId: string): CustomIMClient | undefined {
|
|
49
|
+
return activeConnections.get(accountId);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ============================================================================
|
|
53
|
+
// Types
|
|
54
|
+
// ============================================================================
|
|
55
|
+
|
|
56
|
+
interface CustomIMAccount {
|
|
57
|
+
accountId: string;
|
|
58
|
+
host: string;
|
|
59
|
+
appId: string;
|
|
60
|
+
appKey: string;
|
|
61
|
+
botName: string;
|
|
62
|
+
enabled: boolean;
|
|
63
|
+
configured: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface JsonRpcRequest {
|
|
67
|
+
jsonrpc: "2.0";
|
|
68
|
+
method: string;
|
|
69
|
+
params: Record<string, unknown>;
|
|
70
|
+
id: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface JsonRpcResponse {
|
|
74
|
+
jsonrpc: "2.0";
|
|
75
|
+
result?: unknown;
|
|
76
|
+
error?: { code: number; message: string; data?: unknown };
|
|
77
|
+
id: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface MessagePayload {
|
|
81
|
+
messageId: string;
|
|
82
|
+
sender: string;
|
|
83
|
+
content: string;
|
|
84
|
+
contentType: "text" | "card" | "file";
|
|
85
|
+
timestamp: number;
|
|
86
|
+
chatId?: string;
|
|
87
|
+
isGroup?: boolean;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ============================================================================
|
|
91
|
+
// Account Resolution
|
|
92
|
+
// ============================================================================
|
|
93
|
+
|
|
94
|
+
function resolveCustomIMAccount(
|
|
95
|
+
cfg: Record<string, unknown>,
|
|
96
|
+
accountId?: string
|
|
97
|
+
): CustomIMAccount {
|
|
98
|
+
const actualAccountId = accountId ?? DEFAULT_ACCOUNT_ID;
|
|
99
|
+
const channels = cfg.channels as Record<string, Record<string, unknown>> | undefined;
|
|
100
|
+
const customImChannel = channels?.["custom-im"] as Record<string, unknown> | undefined;
|
|
101
|
+
const accounts = customImChannel?.accounts as Record<string, Record<string, unknown>> | undefined;
|
|
102
|
+
const account = accounts?.[actualAccountId] as Record<string, unknown> | undefined;
|
|
103
|
+
|
|
104
|
+
if (!account) {
|
|
105
|
+
return {
|
|
106
|
+
accountId: actualAccountId,
|
|
107
|
+
host: "",
|
|
108
|
+
appId: "",
|
|
109
|
+
appKey: "",
|
|
110
|
+
botName: "OpenClaw Bot",
|
|
111
|
+
enabled: false,
|
|
112
|
+
configured: false,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
accountId: actualAccountId,
|
|
118
|
+
host: (account.host as string) ?? "",
|
|
119
|
+
appId: (account.appId as string) ?? "",
|
|
120
|
+
appKey: (account.appKey as string) ?? "",
|
|
121
|
+
botName: (account.botName as string) ?? "OpenClaw Bot",
|
|
122
|
+
enabled: (account.enabled as boolean) ?? true,
|
|
123
|
+
configured: !!(account.host && account.appId && account.appKey),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function listCustomIMAccountIds(cfg: Record<string, unknown>): string[] {
|
|
128
|
+
const channels = cfg.channels as Record<string, Record<string, unknown>> | undefined;
|
|
129
|
+
const customImChannel = channels?.["custom-im"] as Record<string, unknown> | undefined;
|
|
130
|
+
const accounts = customImChannel?.accounts as Record<string, Record<string, unknown>> | undefined;
|
|
131
|
+
return accounts ? Object.keys(accounts) : [];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ============================================================================
|
|
135
|
+
// WebSocket Client for Custom IM
|
|
136
|
+
// ============================================================================
|
|
137
|
+
|
|
138
|
+
class CustomIMClient {
|
|
139
|
+
private ws: WebSocket | null = null;
|
|
140
|
+
private account: CustomIMAccount;
|
|
141
|
+
private onMessage: (msg: MessagePayload) => void;
|
|
142
|
+
private onClose: () => void;
|
|
143
|
+
private onHeartbeat?: () => void;
|
|
144
|
+
private logger: Console;
|
|
145
|
+
private heartbeatTimer: NodeJS.Timeout | null = null;
|
|
146
|
+
private readonly heartbeatInterval: number = 30000; // 30 seconds
|
|
147
|
+
|
|
148
|
+
constructor(
|
|
149
|
+
account: CustomIMAccount,
|
|
150
|
+
onMessage: (msg: MessagePayload) => void,
|
|
151
|
+
onClose: () => void,
|
|
152
|
+
logger: Console,
|
|
153
|
+
onHeartbeat?: () => void
|
|
154
|
+
) {
|
|
155
|
+
this.account = account;
|
|
156
|
+
this.onMessage = onMessage;
|
|
157
|
+
this.onClose = onClose;
|
|
158
|
+
this.onHeartbeat = onHeartbeat;
|
|
159
|
+
this.logger = logger;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private generateSignature(timestamp: number): string {
|
|
163
|
+
const data = this.account.appId + this.account.appKey + timestamp;
|
|
164
|
+
return crypto.createHash("sha256").update(data).digest("hex");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async connect(): Promise<void> {
|
|
168
|
+
const url = `wss://${this.account.host}/clawgw/ws/v1/chat`;
|
|
169
|
+
const timestamp = Date.now();
|
|
170
|
+
const signature = this.generateSignature(timestamp);
|
|
171
|
+
|
|
172
|
+
this.logger.log(`[CustomIM] Connecting to ${url}...`);
|
|
173
|
+
|
|
174
|
+
return new Promise((resolve, reject) => {
|
|
175
|
+
this.ws = new WebSocket(url, {
|
|
176
|
+
headers: {
|
|
177
|
+
"X-App-Id": this.account.appId,
|
|
178
|
+
"X-Timestamp": String(timestamp),
|
|
179
|
+
"X-Signature": signature,
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
this.ws.on("open", () => {
|
|
184
|
+
this.logger.log(`[CustomIM] Connected to ${url}`);
|
|
185
|
+
this.startHeartbeat();
|
|
186
|
+
resolve();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
this.ws.on("message", (data: Buffer) => {
|
|
190
|
+
this.logger.log(`[CustomIM] Raw message received: ${data.toString()}`);
|
|
191
|
+
try {
|
|
192
|
+
this.handleMessage(data);
|
|
193
|
+
} catch (error) {
|
|
194
|
+
this.logger.error("[CustomIM] Error handling message:", error);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
this.ws.on("error", (error) => {
|
|
199
|
+
this.logger.error("[CustomIM] WebSocket error:", error);
|
|
200
|
+
reject(error);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
this.ws.on("close", (code, reason) => {
|
|
204
|
+
this.logger.log(`[CustomIM] Connection closed (code=${code}, reason=${reason.toString() || 'none'})`);
|
|
205
|
+
this.stopHeartbeat();
|
|
206
|
+
this.onClose();
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private handleMessage(data: Buffer): void {
|
|
212
|
+
try {
|
|
213
|
+
const msg = JSON.parse(data.toString());
|
|
214
|
+
this.logger.log(`[CustomIM] Parsed message:`, JSON.stringify(msg));
|
|
215
|
+
|
|
216
|
+
// Handle heartbeat request from server - must respond to keep connection alive
|
|
217
|
+
if (msg.method === "heartbeat" && msg.id) {
|
|
218
|
+
this.logger.log(`[CustomIM] Received heartbeat request, sending response`);
|
|
219
|
+
this.sendHeartbeatResponse(msg.id, msg.params?.timestamp);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Handle direct notification: {"jsonrpc":"2.0","method":"onMessage","params":{...}}
|
|
224
|
+
if (msg.method === "onMessage" && msg.params) {
|
|
225
|
+
const params = msg.params as {
|
|
226
|
+
messageId: string;
|
|
227
|
+
sender: string;
|
|
228
|
+
payload: object;
|
|
229
|
+
msgType?: string;
|
|
230
|
+
timestamp: number;
|
|
231
|
+
chatId?: string;
|
|
232
|
+
isGroup?: boolean;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
this.logger.log(`[CustomIM] Processing onMessage from ${params.sender}: ${params.payload.content}`);
|
|
236
|
+
|
|
237
|
+
this.onMessage({
|
|
238
|
+
messageId: params.messageId,
|
|
239
|
+
sender: params.sender,
|
|
240
|
+
content: params.payload.content,
|
|
241
|
+
contentType: (params.msgType as "text" | "card" | "file") ?? "text",
|
|
242
|
+
timestamp: params.timestamp,
|
|
243
|
+
chatId: params.chatId,
|
|
244
|
+
isGroup: params.isGroup,
|
|
245
|
+
});
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Handle response with result containing method (old format)
|
|
250
|
+
const response = msg as JsonRpcResponse;
|
|
251
|
+
if (response.error) {
|
|
252
|
+
this.logger.error("[CustomIM] RPC error:", response.error);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (
|
|
257
|
+
response.result &&
|
|
258
|
+
typeof response.result === "object" &&
|
|
259
|
+
"method" in response.result
|
|
260
|
+
) {
|
|
261
|
+
const notification = response.result as { method: string; params: Record<string, unknown> };
|
|
262
|
+
|
|
263
|
+
if (notification.method === "onMessage") {
|
|
264
|
+
const params = notification.params as {
|
|
265
|
+
messageId: string;
|
|
266
|
+
sender: string;
|
|
267
|
+
content: string;
|
|
268
|
+
contentType: string;
|
|
269
|
+
timestamp: number;
|
|
270
|
+
chatId?: string;
|
|
271
|
+
isGroup?: boolean;
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
this.onMessage({
|
|
275
|
+
messageId: params.messageId,
|
|
276
|
+
sender: params.sender,
|
|
277
|
+
content: params.content,
|
|
278
|
+
contentType: params.contentType as "text" | "card" | "file",
|
|
279
|
+
timestamp: params.timestamp,
|
|
280
|
+
chatId: params.chatId,
|
|
281
|
+
isGroup: params.isGroup,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
} catch (error) {
|
|
286
|
+
this.logger.error("[CustomIM] Failed to parse message:", error);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async sendText(
|
|
291
|
+
receiver: string,
|
|
292
|
+
text: string,
|
|
293
|
+
options?: { replyTo?: string }
|
|
294
|
+
): Promise<{ messageId: string }> {
|
|
295
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
296
|
+
throw new Error("WebSocket not connected");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const id = crypto.randomUUID();
|
|
300
|
+
const request: JsonRpcRequest = {
|
|
301
|
+
jsonrpc: "2.0",
|
|
302
|
+
method: "sendMessage",
|
|
303
|
+
params: {
|
|
304
|
+
msgType: "text",
|
|
305
|
+
receiver,
|
|
306
|
+
payload: {
|
|
307
|
+
content: text, // Changed from 'text' to 'content' to match server API
|
|
308
|
+
},
|
|
309
|
+
replyTo: options?.replyTo,
|
|
310
|
+
},
|
|
311
|
+
id,
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
return new Promise((resolve, reject) => {
|
|
315
|
+
const timeout = setTimeout(() => {
|
|
316
|
+
this.ws?.off("message", handler);
|
|
317
|
+
reject(new Error("Request timeout"));
|
|
318
|
+
}, 30000);
|
|
319
|
+
|
|
320
|
+
const handler = (data: Buffer) => {
|
|
321
|
+
try {
|
|
322
|
+
const response: JsonRpcResponse = JSON.parse(data.toString());
|
|
323
|
+
if (response.id === id) {
|
|
324
|
+
clearTimeout(timeout);
|
|
325
|
+
this.ws?.off("message", handler);
|
|
326
|
+
|
|
327
|
+
if (response.error) {
|
|
328
|
+
reject(new Error(response.error.message));
|
|
329
|
+
} else {
|
|
330
|
+
resolve(response.result as { messageId: string });
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
} catch {
|
|
334
|
+
// Ignore
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
this.ws.on("message", handler);
|
|
339
|
+
this.ws.send(JSON.stringify(request));
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private sendHeartbeatResponse(requestId: string, timestamp?: number): void {
|
|
344
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
345
|
+
this.logger.log(`[CustomIM] Cannot send heartbeat response: WebSocket not connected`);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const response = {
|
|
350
|
+
jsonrpc: "2.0",
|
|
351
|
+
method: "heartbeat",
|
|
352
|
+
result: {
|
|
353
|
+
timestamp: timestamp ?? Date.now(),
|
|
354
|
+
},
|
|
355
|
+
id: requestId,
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
this.ws.send(JSON.stringify(response));
|
|
359
|
+
this.logger.log(`[CustomIM] Heartbeat response sent for request ${requestId}`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private startHeartbeat(): void {
|
|
363
|
+
this.stopHeartbeat();
|
|
364
|
+
this.heartbeatTimer = setInterval(() => {
|
|
365
|
+
this.sendHeartbeat();
|
|
366
|
+
}, this.heartbeatInterval);
|
|
367
|
+
this.logger.log(`[CustomIM] Heartbeat started, interval=${this.heartbeatInterval}ms`);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private stopHeartbeat(): void {
|
|
371
|
+
if (this.heartbeatTimer) {
|
|
372
|
+
clearInterval(this.heartbeatTimer);
|
|
373
|
+
this.heartbeatTimer = null;
|
|
374
|
+
this.logger.log(`[CustomIM] Heartbeat stopped`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private sendHeartbeat(): void {
|
|
379
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
380
|
+
this.logger.log(`[CustomIM] Cannot send heartbeat: WebSocket not connected`);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const request = {
|
|
385
|
+
jsonrpc: "2.0",
|
|
386
|
+
method: "heartbeat",
|
|
387
|
+
params: {
|
|
388
|
+
clientTime: Date.now(),
|
|
389
|
+
},
|
|
390
|
+
id: crypto.randomUUID(),
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
this.ws.send(JSON.stringify(request));
|
|
394
|
+
this.logger.log(`[CustomIM] Heartbeat sent`);
|
|
395
|
+
|
|
396
|
+
// Report connection is active to prevent stale-socket detection
|
|
397
|
+
if (this.onHeartbeat) {
|
|
398
|
+
this.onHeartbeat();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
disconnect(): void {
|
|
403
|
+
this.stopHeartbeat();
|
|
404
|
+
if (this.ws) {
|
|
405
|
+
this.ws.close();
|
|
406
|
+
this.ws = null;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ============================================================================
|
|
412
|
+
// Message Dispatcher (using OpenClaw core API)
|
|
413
|
+
// ============================================================================
|
|
414
|
+
|
|
415
|
+
async function dispatchCustomIMMessage(params: {
|
|
416
|
+
cfg: ClawdbotConfig;
|
|
417
|
+
runtime: RuntimeEnv;
|
|
418
|
+
account: CustomIMAccount;
|
|
419
|
+
msg: MessagePayload;
|
|
420
|
+
client: CustomIMClient;
|
|
421
|
+
}): Promise<void> {
|
|
422
|
+
const { cfg, runtime, account, msg, client } = params;
|
|
423
|
+
|
|
424
|
+
console.log(`[CustomIM] dispatchCustomIMMessage called`);
|
|
425
|
+
|
|
426
|
+
// Get runtime
|
|
427
|
+
let core: PluginRuntime;
|
|
428
|
+
try {
|
|
429
|
+
core = getRuntime();
|
|
430
|
+
console.log(`[CustomIM] getRuntime() returned: ${core ? 'valid' : 'null'}`);
|
|
431
|
+
} catch (error) {
|
|
432
|
+
console.error(`[CustomIM] getRuntime() error: ${error instanceof Error ? error.message : String(error)}`);
|
|
433
|
+
throw error;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const isGroup = msg.isGroup ?? false;
|
|
437
|
+
const chatId = msg.chatId ?? msg.sender;
|
|
438
|
+
|
|
439
|
+
// Resolve agent route
|
|
440
|
+
let route;
|
|
441
|
+
try {
|
|
442
|
+
route = core.channel.routing.resolveAgentRoute({
|
|
443
|
+
cfg,
|
|
444
|
+
channel: "custom-im",
|
|
445
|
+
accountId: account.accountId,
|
|
446
|
+
peer: {
|
|
447
|
+
kind: isGroup ? "group" : "direct",
|
|
448
|
+
id: chatId,
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
console.log(`[CustomIM] Routing message to session: ${route.sessionKey}`);
|
|
452
|
+
} catch (error) {
|
|
453
|
+
console.error(`[CustomIM] resolveAgentRoute error: ${error instanceof Error ? error.message : String(error)}`);
|
|
454
|
+
throw error;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Build the message context
|
|
458
|
+
const customImFrom = `custom-im:${msg.sender}`;
|
|
459
|
+
const customImTo = isGroup ? `chat:${chatId}` : `user:${msg.sender}`;
|
|
460
|
+
|
|
461
|
+
// Format message envelope
|
|
462
|
+
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
|
|
463
|
+
const body = core.channel.reply.formatAgentEnvelope({
|
|
464
|
+
channel: "Custom IM",
|
|
465
|
+
from: msg.sender,
|
|
466
|
+
timestamp: new Date(),
|
|
467
|
+
envelope: envelopeOptions,
|
|
468
|
+
body: msg.content,
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
472
|
+
Body: body,
|
|
473
|
+
BodyForAgent: msg.content,
|
|
474
|
+
RawBody: msg.content,
|
|
475
|
+
CommandBody: msg.content,
|
|
476
|
+
From: customImFrom,
|
|
477
|
+
To: customImTo,
|
|
478
|
+
SessionKey: route.sessionKey,
|
|
479
|
+
AccountId: route.accountId,
|
|
480
|
+
ChatType: isGroup ? "group" : "direct",
|
|
481
|
+
GroupSubject: isGroup ? chatId : undefined,
|
|
482
|
+
SenderName: msg.sender,
|
|
483
|
+
SenderId: msg.sender,
|
|
484
|
+
Provider: "custom-im" as const,
|
|
485
|
+
Surface: "custom-im" as const,
|
|
486
|
+
MessageSid: msg.messageId,
|
|
487
|
+
Timestamp: msg.timestamp,
|
|
488
|
+
WasMentioned: false,
|
|
489
|
+
CommandAuthorized: true,
|
|
490
|
+
OriginatingChannel: "custom-im" as const,
|
|
491
|
+
OriginatingTo: customImTo,
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
console.log(`[CustomIM] ctxPayload created: SessionKey=${ctxPayload.SessionKey}, ChatType=${ctxPayload.ChatType}`);
|
|
495
|
+
|
|
496
|
+
// Create reply dispatcher using OpenClaw's helper
|
|
497
|
+
const prefixContext = createReplyPrefixContext({ cfg, agentId: route.agentId });
|
|
498
|
+
|
|
499
|
+
console.log(`[CustomIM] Creating reply dispatcher...`);
|
|
500
|
+
console.log(`[CustomIM] prefixContext.responsePrefix: ${prefixContext.responsePrefix}`);
|
|
501
|
+
|
|
502
|
+
const dispatcherResult = core.channel.reply.createReplyDispatcherWithTyping({
|
|
503
|
+
responsePrefix: prefixContext.responsePrefix,
|
|
504
|
+
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
|
|
505
|
+
humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId),
|
|
506
|
+
onReplyStart: () => {
|
|
507
|
+
console.log(`[CustomIM] Starting reply...`);
|
|
508
|
+
},
|
|
509
|
+
deliver: async (payload, info) => {
|
|
510
|
+
console.log(`[CustomIM] deliver called, payload:`, JSON.stringify(payload ?? 'null'));
|
|
511
|
+
const text = payload?.text ?? "";
|
|
512
|
+
console.log(`[CustomIM] deliver text length: ${text.length}`);
|
|
513
|
+
if (!text.trim()) {
|
|
514
|
+
console.log(`[CustomIM] deliver: text is empty, skipping`);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
console.log(`[CustomIM] Sending reply to ${msg.sender}: ${text.slice(0, 100)}...`);
|
|
519
|
+
|
|
520
|
+
try {
|
|
521
|
+
const result = await client.sendText(msg.sender, text);
|
|
522
|
+
console.log(`[CustomIM] Reply sent: ${result.messageId}`);
|
|
523
|
+
} catch (error) {
|
|
524
|
+
console.log(`[CustomIM] Failed to send reply: ${error instanceof Error ? error.message : String(error)}`);
|
|
525
|
+
throw error;
|
|
526
|
+
}
|
|
527
|
+
},
|
|
528
|
+
onError: async (error, info) => {
|
|
529
|
+
console.log(`[CustomIM] ${info.kind} reply failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
530
|
+
},
|
|
531
|
+
onIdle: async () => {
|
|
532
|
+
console.log(`[CustomIM] Reply session idle`);
|
|
533
|
+
},
|
|
534
|
+
onCleanup: () => {
|
|
535
|
+
console.log(`[CustomIM] Reply session cleanup`);
|
|
536
|
+
},
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
console.log(`[CustomIM] dispatcherResult keys: ${Object.keys(dispatcherResult).join(', ')}`);
|
|
540
|
+
console.log(`[CustomIM] dispatcher type: ${typeof dispatcherResult.dispatcher}`);
|
|
541
|
+
console.log(`[CustomIM] dispatcher keys: ${dispatcherResult.dispatcher ? Object.keys(dispatcherResult.dispatcher).join(', ') : 'null'}`);
|
|
542
|
+
|
|
543
|
+
const { dispatcher, replyOptions, markDispatchIdle } = dispatcherResult;
|
|
544
|
+
|
|
545
|
+
const finalReplyOptions = {
|
|
546
|
+
...replyOptions,
|
|
547
|
+
onModelSelected: prefixContext.onModelSelected,
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
console.log(`[CustomIM] Dispatching to agent (session=${route.sessionKey})`);
|
|
551
|
+
|
|
552
|
+
try {
|
|
553
|
+
const { queuedFinal, counts } = await core.channel.reply.dispatchReplyFromConfig({
|
|
554
|
+
ctx: ctxPayload,
|
|
555
|
+
cfg,
|
|
556
|
+
dispatcher,
|
|
557
|
+
replyOptions: finalReplyOptions,
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
console.log(`[CustomIM] Dispatch complete (queuedFinal=${queuedFinal})`);
|
|
561
|
+
console.log(`[CustomIM] counts: ${JSON.stringify(counts)}`);
|
|
562
|
+
console.log(`[CustomIM] dispatcher.getQueuedCounts(): ${JSON.stringify(dispatcher.getQueuedCounts())}`);
|
|
563
|
+
|
|
564
|
+
// Wait for the dispatcher to become idle (agent to finish processing)
|
|
565
|
+
await dispatcher.waitForIdle();
|
|
566
|
+
console.log(`[CustomIM] Dispatcher idle, final counts: ${JSON.stringify(dispatcher.getQueuedCounts())}`);
|
|
567
|
+
|
|
568
|
+
markDispatchIdle();
|
|
569
|
+
} catch (error) {
|
|
570
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
571
|
+
const errorStack = error instanceof Error ? error.stack : 'N/A';
|
|
572
|
+
console.error(`[CustomIM] Dispatch error: ${errorMsg}`);
|
|
573
|
+
console.error(`[CustomIM] Dispatch stack: ${errorStack}`);
|
|
574
|
+
throw error;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// ============================================================================
|
|
579
|
+
// Channel Plugin Definition
|
|
580
|
+
// ============================================================================
|
|
581
|
+
|
|
582
|
+
export const customIMPlugin: ChannelPlugin<CustomIMAccount> = {
|
|
583
|
+
id: "custom-im",
|
|
584
|
+
|
|
585
|
+
meta: {
|
|
586
|
+
id: "custom-im",
|
|
587
|
+
label: "Custom IM",
|
|
588
|
+
selectionLabel: "Custom IM (WebSocket)",
|
|
589
|
+
docsPath: "/channels/custom-im",
|
|
590
|
+
blurb: "Custom IM platform integration via WebSocket JSON-RPC 2.0",
|
|
591
|
+
aliases: ["cim", "custom"],
|
|
592
|
+
},
|
|
593
|
+
|
|
594
|
+
capabilities: {
|
|
595
|
+
chatTypes: ["direct", "group"],
|
|
596
|
+
polls: false,
|
|
597
|
+
threads: false,
|
|
598
|
+
media: false,
|
|
599
|
+
reactions: false,
|
|
600
|
+
edit: false,
|
|
601
|
+
reply: true,
|
|
602
|
+
},
|
|
603
|
+
|
|
604
|
+
reload: { configPrefixes: ["channels.custom-im"] },
|
|
605
|
+
|
|
606
|
+
configSchema: {
|
|
607
|
+
schema: {
|
|
608
|
+
type: "object",
|
|
609
|
+
additionalProperties: false,
|
|
610
|
+
properties: {
|
|
611
|
+
enabled: { type: "boolean" },
|
|
612
|
+
host: { type: "string" },
|
|
613
|
+
appId: { type: "string" },
|
|
614
|
+
appKey: { type: "string" },
|
|
615
|
+
botName: { type: "string" },
|
|
616
|
+
dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
|
|
617
|
+
groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
|
|
618
|
+
autoRestart: { type: "boolean" },
|
|
619
|
+
restartDelayMs: { type: "number" },
|
|
620
|
+
maxRestartDelayMs: { type: "number" },
|
|
621
|
+
maxRestartAttempts: { type: "number" },
|
|
622
|
+
accounts: {
|
|
623
|
+
type: "object",
|
|
624
|
+
additionalProperties: {
|
|
625
|
+
type: "object",
|
|
626
|
+
properties: {
|
|
627
|
+
enabled: { type: "boolean" },
|
|
628
|
+
host: { type: "string" },
|
|
629
|
+
appId: { type: "string" },
|
|
630
|
+
appKey: { type: "string" },
|
|
631
|
+
botName: { type: "string" },
|
|
632
|
+
autoRestart: { type: "boolean" },
|
|
633
|
+
restartDelayMs: { type: "number" },
|
|
634
|
+
maxRestartDelayMs: { type: "number" },
|
|
635
|
+
maxRestartAttempts: { type: "number" },
|
|
636
|
+
},
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
},
|
|
642
|
+
|
|
643
|
+
config: {
|
|
644
|
+
listAccountIds: listCustomIMAccountIds,
|
|
645
|
+
resolveAccount: resolveCustomIMAccount,
|
|
646
|
+
defaultAccountId: () => DEFAULT_ACCOUNT_ID,
|
|
647
|
+
isConfigured: (account) => account.configured,
|
|
648
|
+
describeAccount: (account) => ({
|
|
649
|
+
accountId: account.accountId,
|
|
650
|
+
enabled: account.enabled,
|
|
651
|
+
configured: account.configured,
|
|
652
|
+
host: account.host,
|
|
653
|
+
botName: account.botName,
|
|
654
|
+
}),
|
|
655
|
+
},
|
|
656
|
+
|
|
657
|
+
setup: {
|
|
658
|
+
resolveAccountId: () => DEFAULT_ACCOUNT_ID,
|
|
659
|
+
applyAccountConfig: ({ cfg, accountId }) => {
|
|
660
|
+
const isDefault = !accountId || accountId === DEFAULT_ACCOUNT_ID;
|
|
661
|
+
|
|
662
|
+
if (isDefault) {
|
|
663
|
+
return {
|
|
664
|
+
...cfg,
|
|
665
|
+
channels: {
|
|
666
|
+
...cfg.channels,
|
|
667
|
+
"custom-im": {
|
|
668
|
+
...cfg.channels?.["custom-im"],
|
|
669
|
+
enabled: true,
|
|
670
|
+
},
|
|
671
|
+
},
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const customImCfg = cfg.channels?.["custom-im"] as Record<string, unknown> | undefined;
|
|
676
|
+
return {
|
|
677
|
+
...cfg,
|
|
678
|
+
channels: {
|
|
679
|
+
...cfg.channels,
|
|
680
|
+
"custom-im": {
|
|
681
|
+
...customImCfg,
|
|
682
|
+
accounts: {
|
|
683
|
+
...(customImCfg?.accounts as Record<string, unknown>),
|
|
684
|
+
[accountId]: {
|
|
685
|
+
...(customImCfg?.accounts as Record<string, unknown>)?.[accountId],
|
|
686
|
+
enabled: true,
|
|
687
|
+
},
|
|
688
|
+
},
|
|
689
|
+
},
|
|
690
|
+
},
|
|
691
|
+
};
|
|
692
|
+
},
|
|
693
|
+
},
|
|
694
|
+
|
|
695
|
+
status: {
|
|
696
|
+
defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID),
|
|
697
|
+
buildChannelSummary: ({ snapshot }) => ({
|
|
698
|
+
...buildBaseChannelStatusSummary(snapshot),
|
|
699
|
+
}),
|
|
700
|
+
buildAccountSnapshot: ({ account, runtime }) => ({
|
|
701
|
+
accountId: account.accountId,
|
|
702
|
+
enabled: account.enabled,
|
|
703
|
+
configured: account.configured,
|
|
704
|
+
host: account.host,
|
|
705
|
+
botName: account.botName,
|
|
706
|
+
running: runtime?.running ?? false,
|
|
707
|
+
lastStartAt: runtime?.lastStartAt ?? null,
|
|
708
|
+
lastStopAt: runtime?.lastStopAt ?? null,
|
|
709
|
+
lastError: runtime?.lastError ?? null,
|
|
710
|
+
}),
|
|
711
|
+
},
|
|
712
|
+
|
|
713
|
+
gateway: {
|
|
714
|
+
startAccount: async (ctx) => {
|
|
715
|
+
const account = resolveCustomIMAccount(ctx.cfg, ctx.accountId);
|
|
716
|
+
|
|
717
|
+
if (!account.configured) {
|
|
718
|
+
throw new Error(`Custom IM account ${ctx.accountId} not configured`);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
ctx.log?.info(`starting custom-im[${ctx.accountId}]`);
|
|
722
|
+
|
|
723
|
+
// Set initial status
|
|
724
|
+
ctx.setStatus({ accountId: ctx.accountId, connected: false });
|
|
725
|
+
|
|
726
|
+
// Create a promise that will be resolved when we need to stop
|
|
727
|
+
let stopResolver: (() => void) | null = null;
|
|
728
|
+
const stopPromise = new Promise<void>((resolve) => {
|
|
729
|
+
stopResolver = resolve;
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
const client = new CustomIMClient(
|
|
733
|
+
account,
|
|
734
|
+
async (msg) => {
|
|
735
|
+
ctx.log?.info(`[CustomIM] Received message from ${msg.sender}: ${msg.content}`);
|
|
736
|
+
|
|
737
|
+
try {
|
|
738
|
+
await dispatchCustomIMMessage({
|
|
739
|
+
cfg: ctx.cfg,
|
|
740
|
+
runtime: ctx.runtime,
|
|
741
|
+
account,
|
|
742
|
+
msg,
|
|
743
|
+
client,
|
|
744
|
+
});
|
|
745
|
+
ctx.log?.info(`[CustomIM] Message processed successfully`);
|
|
746
|
+
} catch (error) {
|
|
747
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
748
|
+
const errorStack = error instanceof Error ? error.stack : 'N/A';
|
|
749
|
+
ctx.log?.error(`[CustomIM] Error processing message: ${errorMsg}`);
|
|
750
|
+
ctx.log?.error(`[CustomIM] Error stack: ${errorStack}`);
|
|
751
|
+
}
|
|
752
|
+
},
|
|
753
|
+
() => {
|
|
754
|
+
// Called when WebSocket connection is closed
|
|
755
|
+
ctx.log?.info(`[CustomIM] Connection lost, triggering reconnect...`);
|
|
756
|
+
unregisterConnection(ctx.accountId);
|
|
757
|
+
ctx.setStatus({ accountId: ctx.accountId, connected: false });
|
|
758
|
+
// Trigger reconnect by resolving the stop promise
|
|
759
|
+
if (stopResolver) {
|
|
760
|
+
stopResolver();
|
|
761
|
+
}
|
|
762
|
+
},
|
|
763
|
+
console,
|
|
764
|
+
() => {
|
|
765
|
+
// Called on each heartbeat - report connection is active to prevent stale-socket detection
|
|
766
|
+
ctx.setStatus({ accountId: ctx.accountId, connected: true });
|
|
767
|
+
}
|
|
768
|
+
);
|
|
769
|
+
|
|
770
|
+
await client.connect();
|
|
771
|
+
|
|
772
|
+
// Register the connection for this account
|
|
773
|
+
registerConnection(ctx.accountId, client);
|
|
774
|
+
|
|
775
|
+
// Update status to connected
|
|
776
|
+
ctx.setStatus({ accountId: ctx.accountId, connected: true });
|
|
777
|
+
ctx.log?.info(`[CustomIM] Connection established, channel is now running`);
|
|
778
|
+
|
|
779
|
+
// Wait for either abort signal or connection close
|
|
780
|
+
return new Promise<{ stop: () => void }>((resolve) => {
|
|
781
|
+
const stopHandler = () => {
|
|
782
|
+
ctx.log?.info(`stopping custom-im[${ctx.accountId}]`);
|
|
783
|
+
ctx.setStatus({ accountId: ctx.accountId, connected: false });
|
|
784
|
+
unregisterConnection(ctx.accountId);
|
|
785
|
+
client.disconnect();
|
|
786
|
+
resolve({ stop: stopHandler });
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
// Listen for abort signal
|
|
790
|
+
if (ctx.abortSignal) {
|
|
791
|
+
ctx.abortSignal.addEventListener('abort', stopHandler);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Also resolve when connection is closed (stopPromise)
|
|
795
|
+
// This triggers immediate reconnect instead of waiting for OpenClaw's backoff
|
|
796
|
+
stopPromise.then(() => {
|
|
797
|
+
if (ctx.abortSignal) {
|
|
798
|
+
ctx.abortSignal.removeEventListener('abort', stopHandler);
|
|
799
|
+
}
|
|
800
|
+
// Don't resolve immediately - let OpenClaw handle the restart
|
|
801
|
+
// But signal that we want to restart by throwing an error
|
|
802
|
+
resolve({ stop: stopHandler });
|
|
803
|
+
});
|
|
804
|
+
});
|
|
805
|
+
},
|
|
806
|
+
},
|
|
807
|
+
|
|
808
|
+
outbound: {
|
|
809
|
+
deliveryMode: "direct",
|
|
810
|
+
|
|
811
|
+
sendText: async ({ cfg, accountId, peerId, text }) => {
|
|
812
|
+
const account = resolveCustomIMAccount(cfg, accountId);
|
|
813
|
+
|
|
814
|
+
if (!account.configured) {
|
|
815
|
+
throw new Error(`Custom IM account ${accountId} not configured`);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const client = getActiveConnection(accountId);
|
|
819
|
+
if (!client) {
|
|
820
|
+
throw new Error(`Custom IM account ${accountId} is not connected`);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
const result = await client.sendText(peerId, text);
|
|
824
|
+
return { ok: true, messageId: result.messageId };
|
|
825
|
+
},
|
|
826
|
+
},
|
|
827
|
+
|
|
828
|
+
pairing: {
|
|
829
|
+
idLabel: "customImUserId",
|
|
830
|
+
normalizeAllowEntry: (entry) => entry.replace(/^(custom|cim):/i, ""),
|
|
831
|
+
notifyApproval: async ({ cfg, id, accountId }) => {
|
|
832
|
+
const account = resolveCustomIMAccount(cfg, accountId);
|
|
833
|
+
|
|
834
|
+
if (!account.configured) return;
|
|
835
|
+
|
|
836
|
+
const client = getActiveConnection(accountId);
|
|
837
|
+
if (!client) {
|
|
838
|
+
console.error(`[CustomIM] Account ${accountId} is not connected, cannot send approval message`);
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
try {
|
|
843
|
+
await client.sendText(id, "✅ You have been approved to chat with me!");
|
|
844
|
+
} catch (error) {
|
|
845
|
+
console.error("[CustomIM] Failed to send approval message:", error);
|
|
846
|
+
}
|
|
847
|
+
},
|
|
848
|
+
},
|
|
849
|
+
|
|
850
|
+
security: {
|
|
851
|
+
dmPolicy: "open",
|
|
852
|
+
},
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
// ============================================================================
|
|
856
|
+
// Plugin Registration
|
|
857
|
+
// ============================================================================
|
|
858
|
+
|
|
859
|
+
const plugin = {
|
|
860
|
+
id: "custom-im",
|
|
861
|
+
name: "Custom IM",
|
|
862
|
+
description: "Custom IM platform integration via WebSocket JSON-RPC 2.0",
|
|
863
|
+
configSchema: emptyPluginConfigSchema(),
|
|
864
|
+
|
|
865
|
+
register(api: OpenClawPluginApi) {
|
|
866
|
+
// Store the runtime for use in message dispatching
|
|
867
|
+
pluginRuntime = api.runtime;
|
|
868
|
+
api.registerChannel({ plugin: customIMPlugin });
|
|
869
|
+
},
|
|
870
|
+
};
|
|
871
|
+
|
|
872
|
+
export default plugin;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "fontdo-5g-message",
|
|
3
|
+
"channels": ["fontdo-5g-message"],
|
|
4
|
+
"configSchema": {
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"properties": {
|
|
8
|
+
"host": {
|
|
9
|
+
"type": "string",
|
|
10
|
+
"title": "服务器地址",
|
|
11
|
+
"description": "Fontdo 5G 消息平台地址(不含协议前缀,如 5g.fontdo.com)"
|
|
12
|
+
},
|
|
13
|
+
"appId": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"title": "应用 ID",
|
|
16
|
+
"description": "从平台获取的应用 ID"
|
|
17
|
+
},
|
|
18
|
+
"appKey": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"title": "应用密钥",
|
|
21
|
+
"description": "从平台获取的应用密钥"
|
|
22
|
+
},
|
|
23
|
+
"botName": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"title": "机器人名称",
|
|
26
|
+
"description": "机器人在对话中的显示名称",
|
|
27
|
+
"default": "Fontdo Bot"
|
|
28
|
+
},
|
|
29
|
+
"enabled": {
|
|
30
|
+
"type": "boolean",
|
|
31
|
+
"title": "启用账号",
|
|
32
|
+
"description": "是否启用该账号",
|
|
33
|
+
"default": true
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fontdo/5g-message",
|
|
3
|
+
"version": "1.0.4",
|
|
4
|
+
"description": "OpenClaw 通道插件,用于连接 Fontdo 5G 消息平台",
|
|
5
|
+
"main": "index.ts",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "tsc",
|
|
8
|
+
"test": "vitest"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"ws": "^8.16.0"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@types/node": "^20.11.0",
|
|
15
|
+
"@types/ws": "^8.5.10",
|
|
16
|
+
"typescript": "^5.3.0",
|
|
17
|
+
"vitest": "^1.2.0"
|
|
18
|
+
},
|
|
19
|
+
"openclaw": {
|
|
20
|
+
"extensions": [
|
|
21
|
+
"./index.ts"
|
|
22
|
+
],
|
|
23
|
+
"channel": {
|
|
24
|
+
"id": "fontdo-5g-message",
|
|
25
|
+
"label": "Fontdo 5G Message",
|
|
26
|
+
"selectionLabel": "Fontdo 5G Message (WebSocket)",
|
|
27
|
+
"docsPath": "/channels/fontdo-5g-message",
|
|
28
|
+
"docsLabel": "fontdo-5g-message",
|
|
29
|
+
"blurb": "Fontdo 5G 消息平台集成,通过 WebSocket JSON-RPC 2.0 协议",
|
|
30
|
+
"order": 100,
|
|
31
|
+
"aliases": [
|
|
32
|
+
"fontdo",
|
|
33
|
+
"5g",
|
|
34
|
+
"5g-message"
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"declarationMap": true,
|
|
13
|
+
"sourceMap": true,
|
|
14
|
+
"outDir": "./dist",
|
|
15
|
+
"rootDir": ".",
|
|
16
|
+
"resolveJsonModule": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["*.ts"],
|
|
19
|
+
"exclude": ["node_modules", "dist"]
|
|
20
|
+
}
|