@fontdo/5g-message 1.0.8 → 1.0.10
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 +29 -10
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +275 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +699 -0
- package/dist/index.js.map +1 -0
- package/package.json +12 -4
- package/cli.ts +0 -168
- package/index.ts +0 -872
- package/tsconfig.json +0 -20
package/dist/index.js
ADDED
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenClaw Channel Plugin for Fontdo 5G Message Platform
|
|
3
|
+
*
|
|
4
|
+
* This plugin connects OpenClaw to your Fontdo 5G message platform via WebSocket
|
|
5
|
+
* using JSON-RPC 2.0 protocol.
|
|
6
|
+
*/
|
|
7
|
+
// 模拟函数
|
|
8
|
+
const emptyPluginConfigSchema = () => ({});
|
|
9
|
+
const buildBaseChannelStatusSummary = (snapshot) => ({});
|
|
10
|
+
const createDefaultChannelRuntimeState = (accountId) => ({});
|
|
11
|
+
const DEFAULT_ACCOUNT_ID = "default";
|
|
12
|
+
const createReplyPrefixContext = (options) => ({
|
|
13
|
+
responsePrefix: "",
|
|
14
|
+
responsePrefixContextProvider: () => { },
|
|
15
|
+
onModelSelected: () => { }
|
|
16
|
+
});
|
|
17
|
+
import WebSocket from "ws";
|
|
18
|
+
import crypto from "crypto";
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Runtime (set during plugin registration)
|
|
21
|
+
// ============================================================================
|
|
22
|
+
let pluginRuntime = null;
|
|
23
|
+
function getRuntime() {
|
|
24
|
+
if (!pluginRuntime) {
|
|
25
|
+
throw new Error("Custom IM plugin runtime not initialized");
|
|
26
|
+
}
|
|
27
|
+
return pluginRuntime;
|
|
28
|
+
}
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// Global Connection Registry (one WebSocket connection per account)
|
|
31
|
+
// ============================================================================
|
|
32
|
+
const activeConnections = new Map();
|
|
33
|
+
function registerConnection(accountId, client) {
|
|
34
|
+
activeConnections.set(accountId, client);
|
|
35
|
+
console.log(`[CustomIM] Registered connection for account ${accountId}`);
|
|
36
|
+
}
|
|
37
|
+
function unregisterConnection(accountId) {
|
|
38
|
+
activeConnections.delete(accountId);
|
|
39
|
+
console.log(`[CustomIM] Unregistered connection for account ${accountId}`);
|
|
40
|
+
}
|
|
41
|
+
function getActiveConnection(accountId) {
|
|
42
|
+
return activeConnections.get(accountId);
|
|
43
|
+
}
|
|
44
|
+
// ============================================================================
|
|
45
|
+
// Account Resolution
|
|
46
|
+
// ============================================================================
|
|
47
|
+
function resolveCustomIMAccount(cfg, accountId) {
|
|
48
|
+
const actualAccountId = accountId ?? DEFAULT_ACCOUNT_ID;
|
|
49
|
+
const channels = cfg.channels;
|
|
50
|
+
const customImChannel = channels?.["5g-message"];
|
|
51
|
+
const accounts = customImChannel?.accounts;
|
|
52
|
+
const account = accounts?.[actualAccountId];
|
|
53
|
+
if (!account) {
|
|
54
|
+
return {
|
|
55
|
+
accountId: actualAccountId,
|
|
56
|
+
host: "",
|
|
57
|
+
appId: "",
|
|
58
|
+
appKey: "",
|
|
59
|
+
botName: "OpenClaw Bot",
|
|
60
|
+
enabled: false,
|
|
61
|
+
configured: false,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
accountId: actualAccountId,
|
|
66
|
+
host: account.host ?? "",
|
|
67
|
+
appId: account.appId ?? "",
|
|
68
|
+
appKey: account.appKey ?? "",
|
|
69
|
+
botName: account.botName ?? "OpenClaw Bot",
|
|
70
|
+
enabled: account.enabled ?? true,
|
|
71
|
+
configured: !!(account.host && account.appId && account.appKey),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function listCustomIMAccountIds(cfg) {
|
|
75
|
+
const channels = cfg.channels;
|
|
76
|
+
const customImChannel = channels?.["5g-message"];
|
|
77
|
+
const accounts = customImChannel?.accounts;
|
|
78
|
+
return accounts ? Object.keys(accounts) : [];
|
|
79
|
+
}
|
|
80
|
+
// ============================================================================
|
|
81
|
+
// WebSocket Client for Custom IM
|
|
82
|
+
// ============================================================================
|
|
83
|
+
class CustomIMClient {
|
|
84
|
+
ws = null;
|
|
85
|
+
account;
|
|
86
|
+
onMessage;
|
|
87
|
+
onClose;
|
|
88
|
+
onHeartbeat;
|
|
89
|
+
logger;
|
|
90
|
+
heartbeatTimer = null;
|
|
91
|
+
heartbeatInterval = 30000; // 30 seconds
|
|
92
|
+
constructor(account, onMessage, onClose, logger, onHeartbeat) {
|
|
93
|
+
this.account = account;
|
|
94
|
+
this.onMessage = onMessage;
|
|
95
|
+
this.onClose = onClose;
|
|
96
|
+
this.onHeartbeat = onHeartbeat;
|
|
97
|
+
this.logger = logger;
|
|
98
|
+
}
|
|
99
|
+
generateSignature(timestamp) {
|
|
100
|
+
const data = this.account.appId + this.account.appKey + timestamp;
|
|
101
|
+
return crypto.createHash("sha256").update(data).digest("hex");
|
|
102
|
+
}
|
|
103
|
+
async connect() {
|
|
104
|
+
const url = `wss://${this.account.host}/clawgw/ws/v1/chat`;
|
|
105
|
+
const timestamp = Date.now();
|
|
106
|
+
const signature = this.generateSignature(timestamp);
|
|
107
|
+
this.logger.log(`[CustomIM] Connecting to ${url}...`);
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
109
|
+
this.ws = new WebSocket(url, {
|
|
110
|
+
headers: {
|
|
111
|
+
"X-App-Id": this.account.appId,
|
|
112
|
+
"X-Timestamp": String(timestamp),
|
|
113
|
+
"X-Signature": signature,
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
this.ws.on("open", () => {
|
|
117
|
+
this.logger.log(`[CustomIM] Connected to ${url}`);
|
|
118
|
+
this.startHeartbeat();
|
|
119
|
+
resolve();
|
|
120
|
+
});
|
|
121
|
+
this.ws.on("message", (data) => {
|
|
122
|
+
this.logger.log(`[CustomIM] Raw message received: ${data.toString()}`);
|
|
123
|
+
try {
|
|
124
|
+
this.handleMessage(data);
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
this.logger.error("[CustomIM] Error handling message:", error);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
this.ws.on("error", (error) => {
|
|
131
|
+
this.logger.error("[CustomIM] WebSocket error:", error);
|
|
132
|
+
reject(error);
|
|
133
|
+
});
|
|
134
|
+
this.ws.on("close", (code, reason) => {
|
|
135
|
+
this.logger.log(`[CustomIM] Connection closed (code=${code}, reason=${reason.toString() || 'none'})`);
|
|
136
|
+
this.stopHeartbeat();
|
|
137
|
+
this.onClose();
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
handleMessage(data) {
|
|
142
|
+
try {
|
|
143
|
+
const msg = JSON.parse(data.toString());
|
|
144
|
+
this.logger.log(`[CustomIM] Parsed message:`, JSON.stringify(msg));
|
|
145
|
+
// Handle heartbeat request from server - must respond to keep connection alive
|
|
146
|
+
if (msg.method === "heartbeat" && msg.id) {
|
|
147
|
+
this.logger.log(`[CustomIM] Received heartbeat request, sending response`);
|
|
148
|
+
this.sendHeartbeatResponse(msg.id, msg.params?.timestamp);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
// Handle direct notification: {"jsonrpc":"2.0","method":"onMessage","params":{...}}
|
|
152
|
+
if (msg.method === "onMessage" && msg.params) {
|
|
153
|
+
const params = msg.params;
|
|
154
|
+
this.logger.log(`[CustomIM] Processing onMessage from ${params.sender}: ${params.payload.content}`);
|
|
155
|
+
this.onMessage({
|
|
156
|
+
messageId: params.messageId,
|
|
157
|
+
sender: params.sender,
|
|
158
|
+
content: params.payload.content,
|
|
159
|
+
contentType: params.msgType ?? "text",
|
|
160
|
+
timestamp: params.timestamp,
|
|
161
|
+
chatId: params.chatId,
|
|
162
|
+
isGroup: params.isGroup,
|
|
163
|
+
});
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
// Handle response with result containing method (old format)
|
|
167
|
+
const response = msg;
|
|
168
|
+
if (response.error) {
|
|
169
|
+
this.logger.error("[CustomIM] RPC error:", response.error);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (response.result &&
|
|
173
|
+
typeof response.result === "object" &&
|
|
174
|
+
"method" in response.result) {
|
|
175
|
+
const notification = response.result;
|
|
176
|
+
if (notification.method === "onMessage") {
|
|
177
|
+
const params = notification.params;
|
|
178
|
+
this.onMessage({
|
|
179
|
+
messageId: params.messageId,
|
|
180
|
+
sender: params.sender,
|
|
181
|
+
content: params.content,
|
|
182
|
+
contentType: params.contentType,
|
|
183
|
+
timestamp: params.timestamp,
|
|
184
|
+
chatId: params.chatId,
|
|
185
|
+
isGroup: params.isGroup,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
this.logger.error("[CustomIM] Failed to parse message:", error);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async sendText(receiver, text, options) {
|
|
195
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
196
|
+
throw new Error("WebSocket not connected");
|
|
197
|
+
}
|
|
198
|
+
const id = crypto.randomUUID();
|
|
199
|
+
const request = {
|
|
200
|
+
jsonrpc: "2.0",
|
|
201
|
+
method: "sendMessage",
|
|
202
|
+
params: {
|
|
203
|
+
msgType: "text",
|
|
204
|
+
receiver,
|
|
205
|
+
payload: {
|
|
206
|
+
content: text, // Changed from 'text' to 'content' to match server API
|
|
207
|
+
},
|
|
208
|
+
replyTo: options?.replyTo,
|
|
209
|
+
},
|
|
210
|
+
id,
|
|
211
|
+
};
|
|
212
|
+
return new Promise((resolve, reject) => {
|
|
213
|
+
const timeout = setTimeout(() => {
|
|
214
|
+
this.ws?.off("message", handler);
|
|
215
|
+
reject(new Error("Request timeout"));
|
|
216
|
+
}, 30000);
|
|
217
|
+
const handler = (data) => {
|
|
218
|
+
try {
|
|
219
|
+
const response = JSON.parse(data.toString());
|
|
220
|
+
if (response.id === id) {
|
|
221
|
+
clearTimeout(timeout);
|
|
222
|
+
this.ws?.off("message", handler);
|
|
223
|
+
if (response.error) {
|
|
224
|
+
reject(new Error(response.error.message));
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
resolve(response.result);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// Ignore
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
if (this.ws) {
|
|
236
|
+
this.ws.on("message", handler);
|
|
237
|
+
this.ws.send(JSON.stringify(request));
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
reject(new Error("WebSocket not connected"));
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
sendHeartbeatResponse(requestId, timestamp) {
|
|
245
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
246
|
+
this.logger.log(`[CustomIM] Cannot send heartbeat response: WebSocket not connected`);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const response = {
|
|
250
|
+
jsonrpc: "2.0",
|
|
251
|
+
method: "heartbeat",
|
|
252
|
+
result: {
|
|
253
|
+
timestamp: timestamp ?? Date.now(),
|
|
254
|
+
},
|
|
255
|
+
id: requestId,
|
|
256
|
+
};
|
|
257
|
+
this.ws.send(JSON.stringify(response));
|
|
258
|
+
this.logger.log(`[CustomIM] Heartbeat response sent for request ${requestId}`);
|
|
259
|
+
}
|
|
260
|
+
startHeartbeat() {
|
|
261
|
+
this.stopHeartbeat();
|
|
262
|
+
this.heartbeatTimer = setInterval(() => {
|
|
263
|
+
this.sendHeartbeat();
|
|
264
|
+
}, this.heartbeatInterval);
|
|
265
|
+
this.logger.log(`[CustomIM] Heartbeat started, interval=${this.heartbeatInterval}ms`);
|
|
266
|
+
}
|
|
267
|
+
stopHeartbeat() {
|
|
268
|
+
if (this.heartbeatTimer) {
|
|
269
|
+
clearInterval(this.heartbeatTimer);
|
|
270
|
+
this.heartbeatTimer = null;
|
|
271
|
+
this.logger.log(`[CustomIM] Heartbeat stopped`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
sendHeartbeat() {
|
|
275
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
276
|
+
this.logger.log(`[CustomIM] Cannot send heartbeat: WebSocket not connected`);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const request = {
|
|
280
|
+
jsonrpc: "2.0",
|
|
281
|
+
method: "heartbeat",
|
|
282
|
+
params: {
|
|
283
|
+
clientTime: Date.now(),
|
|
284
|
+
},
|
|
285
|
+
id: crypto.randomUUID(),
|
|
286
|
+
};
|
|
287
|
+
this.ws.send(JSON.stringify(request));
|
|
288
|
+
this.logger.log(`[CustomIM] Heartbeat sent`);
|
|
289
|
+
// Report connection is active to prevent stale-socket detection
|
|
290
|
+
if (this.onHeartbeat) {
|
|
291
|
+
this.onHeartbeat();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
disconnect() {
|
|
295
|
+
this.stopHeartbeat();
|
|
296
|
+
if (this.ws) {
|
|
297
|
+
this.ws.close();
|
|
298
|
+
this.ws = null;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// ============================================================================
|
|
303
|
+
// Message Dispatcher (using OpenClaw core API)
|
|
304
|
+
// ============================================================================
|
|
305
|
+
async function dispatchCustomIMMessage(params) {
|
|
306
|
+
const { cfg, runtime, account, msg, client } = params;
|
|
307
|
+
console.log(`[CustomIM] dispatchCustomIMMessage called`);
|
|
308
|
+
// Get runtime
|
|
309
|
+
let core;
|
|
310
|
+
try {
|
|
311
|
+
core = getRuntime();
|
|
312
|
+
console.log(`[CustomIM] getRuntime() returned: ${core ? 'valid' : 'null'}`);
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
console.error(`[CustomIM] getRuntime() error: ${error instanceof Error ? error.message : String(error)}`);
|
|
316
|
+
throw error;
|
|
317
|
+
}
|
|
318
|
+
const isGroup = msg.isGroup ?? false;
|
|
319
|
+
const chatId = msg.chatId ?? msg.sender;
|
|
320
|
+
// Resolve agent route
|
|
321
|
+
let route;
|
|
322
|
+
try {
|
|
323
|
+
route = core.channel.routing.resolveAgentRoute({
|
|
324
|
+
cfg,
|
|
325
|
+
channel: "5g-message",
|
|
326
|
+
accountId: account.accountId,
|
|
327
|
+
peer: {
|
|
328
|
+
kind: isGroup ? "group" : "direct",
|
|
329
|
+
id: chatId,
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
console.log(`[CustomIM] Routing message to session: ${route.sessionKey}`);
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
console.error(`[CustomIM] resolveAgentRoute error: ${error instanceof Error ? error.message : String(error)}`);
|
|
336
|
+
throw error;
|
|
337
|
+
}
|
|
338
|
+
// Build the message context
|
|
339
|
+
const customImFrom = `5g-message:${msg.sender}`;
|
|
340
|
+
const customImTo = isGroup ? `chat:${chatId}` : `user:${msg.sender}`;
|
|
341
|
+
// Format message envelope
|
|
342
|
+
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
|
|
343
|
+
const body = core.channel.reply.formatAgentEnvelope({
|
|
344
|
+
channel: "Custom IM",
|
|
345
|
+
from: msg.sender,
|
|
346
|
+
timestamp: new Date(),
|
|
347
|
+
envelope: envelopeOptions,
|
|
348
|
+
body: msg.content,
|
|
349
|
+
});
|
|
350
|
+
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
351
|
+
Body: body,
|
|
352
|
+
BodyForAgent: msg.content,
|
|
353
|
+
RawBody: msg.content,
|
|
354
|
+
CommandBody: msg.content,
|
|
355
|
+
From: customImFrom,
|
|
356
|
+
To: customImTo,
|
|
357
|
+
SessionKey: route.sessionKey,
|
|
358
|
+
AccountId: route.accountId,
|
|
359
|
+
ChatType: isGroup ? "group" : "direct",
|
|
360
|
+
GroupSubject: isGroup ? chatId : undefined,
|
|
361
|
+
SenderName: msg.sender,
|
|
362
|
+
SenderId: msg.sender,
|
|
363
|
+
Provider: "5g-message",
|
|
364
|
+
Surface: "5g-message",
|
|
365
|
+
MessageSid: msg.messageId,
|
|
366
|
+
Timestamp: msg.timestamp,
|
|
367
|
+
WasMentioned: false,
|
|
368
|
+
CommandAuthorized: true,
|
|
369
|
+
OriginatingChannel: "5g-message",
|
|
370
|
+
OriginatingTo: customImTo,
|
|
371
|
+
});
|
|
372
|
+
console.log(`[CustomIM] ctxPayload created: SessionKey=${ctxPayload.SessionKey}, ChatType=${ctxPayload.ChatType}`);
|
|
373
|
+
// Create reply dispatcher using OpenClaw's helper
|
|
374
|
+
const prefixContext = createReplyPrefixContext({ cfg, agentId: route.agentId });
|
|
375
|
+
console.log(`[CustomIM] Creating reply dispatcher...`);
|
|
376
|
+
console.log(`[CustomIM] prefixContext.responsePrefix: ${prefixContext.responsePrefix}`);
|
|
377
|
+
const dispatcherResult = core.channel.reply.createReplyDispatcherWithTyping({
|
|
378
|
+
responsePrefix: prefixContext.responsePrefix,
|
|
379
|
+
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
|
|
380
|
+
humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId),
|
|
381
|
+
onReplyStart: () => {
|
|
382
|
+
console.log(`[CustomIM] Starting reply...`);
|
|
383
|
+
},
|
|
384
|
+
deliver: async (payload, info) => {
|
|
385
|
+
console.log(`[CustomIM] deliver called, payload:`, JSON.stringify(payload ?? 'null'));
|
|
386
|
+
const text = payload?.text ?? "";
|
|
387
|
+
console.log(`[CustomIM] deliver text length: ${text.length}`);
|
|
388
|
+
if (!text.trim()) {
|
|
389
|
+
console.log(`[CustomIM] deliver: text is empty, skipping`);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
console.log(`[CustomIM] Sending reply to ${msg.sender}: ${text.slice(0, 100)}...`);
|
|
393
|
+
try {
|
|
394
|
+
const result = await client.sendText(msg.sender, text);
|
|
395
|
+
console.log(`[CustomIM] Reply sent: ${result.messageId}`);
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
console.log(`[CustomIM] Failed to send reply: ${error instanceof Error ? error.message : String(error)}`);
|
|
399
|
+
throw error;
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
onError: async (error, info) => {
|
|
403
|
+
console.log(`[CustomIM] ${info.kind} reply failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
404
|
+
},
|
|
405
|
+
onIdle: async () => {
|
|
406
|
+
console.log(`[CustomIM] Reply session idle`);
|
|
407
|
+
},
|
|
408
|
+
onCleanup: () => {
|
|
409
|
+
console.log(`[CustomIM] Reply session cleanup`);
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
console.log(`[CustomIM] dispatcherResult keys: ${Object.keys(dispatcherResult).join(', ')}`);
|
|
413
|
+
console.log(`[CustomIM] dispatcher type: ${typeof dispatcherResult.dispatcher}`);
|
|
414
|
+
console.log(`[CustomIM] dispatcher keys: ${dispatcherResult.dispatcher ? Object.keys(dispatcherResult.dispatcher).join(', ') : 'null'}`);
|
|
415
|
+
const { dispatcher, replyOptions, markDispatchIdle } = dispatcherResult;
|
|
416
|
+
const finalReplyOptions = {
|
|
417
|
+
...replyOptions,
|
|
418
|
+
onModelSelected: prefixContext.onModelSelected,
|
|
419
|
+
};
|
|
420
|
+
console.log(`[CustomIM] Dispatching to agent (session=${route.sessionKey})`);
|
|
421
|
+
try {
|
|
422
|
+
const { queuedFinal, counts } = await core.channel.reply.dispatchReplyFromConfig({
|
|
423
|
+
ctx: ctxPayload,
|
|
424
|
+
cfg,
|
|
425
|
+
dispatcher,
|
|
426
|
+
replyOptions: finalReplyOptions,
|
|
427
|
+
});
|
|
428
|
+
console.log(`[CustomIM] Dispatch complete (queuedFinal=${queuedFinal})`);
|
|
429
|
+
console.log(`[CustomIM] counts: ${JSON.stringify(counts)}`);
|
|
430
|
+
console.log(`[CustomIM] dispatcher.getQueuedCounts(): ${JSON.stringify(dispatcher.getQueuedCounts())}`);
|
|
431
|
+
// Wait for the dispatcher to become idle (agent to finish processing)
|
|
432
|
+
await dispatcher.waitForIdle();
|
|
433
|
+
console.log(`[CustomIM] Dispatcher idle, final counts: ${JSON.stringify(dispatcher.getQueuedCounts())}`);
|
|
434
|
+
markDispatchIdle();
|
|
435
|
+
}
|
|
436
|
+
catch (error) {
|
|
437
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
438
|
+
const errorStack = error instanceof Error ? error.stack : 'N/A';
|
|
439
|
+
console.error(`[CustomIM] Dispatch error: ${errorMsg}`);
|
|
440
|
+
console.error(`[CustomIM] Dispatch stack: ${errorStack}`);
|
|
441
|
+
throw error;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
// ============================================================================
|
|
445
|
+
// Channel Plugin Definition
|
|
446
|
+
// ============================================================================
|
|
447
|
+
export const fontdo5GMessagePlugin = {
|
|
448
|
+
id: "5g-message",
|
|
449
|
+
meta: {
|
|
450
|
+
id: "5g-message",
|
|
451
|
+
label: "Fontdo 5G Message",
|
|
452
|
+
selectionLabel: "Fontdo 5G Message (WebSocket)",
|
|
453
|
+
docsPath: "/channels/5g-message",
|
|
454
|
+
blurb: "Fontdo 5G 消息平台集成,通过 WebSocket JSON-RPC 2.0 协议",
|
|
455
|
+
aliases: ["fontdo", "5g", "5g-message"],
|
|
456
|
+
},
|
|
457
|
+
capabilities: {
|
|
458
|
+
chatTypes: ["direct", "group"],
|
|
459
|
+
polls: false,
|
|
460
|
+
threads: false,
|
|
461
|
+
media: false,
|
|
462
|
+
reactions: false,
|
|
463
|
+
edit: false,
|
|
464
|
+
reply: true,
|
|
465
|
+
},
|
|
466
|
+
reload: { configPrefixes: ["channels.5g-message"] },
|
|
467
|
+
configSchema: {
|
|
468
|
+
schema: {
|
|
469
|
+
type: "object",
|
|
470
|
+
additionalProperties: false,
|
|
471
|
+
properties: {
|
|
472
|
+
enabled: { type: "boolean" },
|
|
473
|
+
host: { type: "string" },
|
|
474
|
+
appId: { type: "string" },
|
|
475
|
+
appKey: { type: "string" },
|
|
476
|
+
botName: { type: "string" },
|
|
477
|
+
dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
|
|
478
|
+
groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
|
|
479
|
+
autoRestart: { type: "boolean" },
|
|
480
|
+
restartDelayMs: { type: "number" },
|
|
481
|
+
maxRestartDelayMs: { type: "number" },
|
|
482
|
+
maxRestartAttempts: { type: "number" },
|
|
483
|
+
accounts: {
|
|
484
|
+
type: "object",
|
|
485
|
+
additionalProperties: {
|
|
486
|
+
type: "object",
|
|
487
|
+
properties: {
|
|
488
|
+
enabled: { type: "boolean" },
|
|
489
|
+
host: { type: "string" },
|
|
490
|
+
appId: { type: "string" },
|
|
491
|
+
appKey: { type: "string" },
|
|
492
|
+
botName: { type: "string" },
|
|
493
|
+
autoRestart: { type: "boolean" },
|
|
494
|
+
restartDelayMs: { type: "number" },
|
|
495
|
+
maxRestartDelayMs: { type: "number" },
|
|
496
|
+
maxRestartAttempts: { type: "number" },
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
config: {
|
|
504
|
+
listAccountIds: listCustomIMAccountIds,
|
|
505
|
+
resolveAccount: resolveCustomIMAccount,
|
|
506
|
+
defaultAccountId: () => DEFAULT_ACCOUNT_ID,
|
|
507
|
+
isConfigured: (account) => account.configured,
|
|
508
|
+
describeAccount: (account) => ({
|
|
509
|
+
accountId: account.accountId,
|
|
510
|
+
enabled: account.enabled,
|
|
511
|
+
configured: account.configured,
|
|
512
|
+
host: account.host,
|
|
513
|
+
botName: account.botName,
|
|
514
|
+
}),
|
|
515
|
+
},
|
|
516
|
+
setup: {
|
|
517
|
+
resolveAccountId: () => DEFAULT_ACCOUNT_ID,
|
|
518
|
+
applyAccountConfig: ({ cfg, accountId }) => {
|
|
519
|
+
const isDefault = !accountId || accountId === DEFAULT_ACCOUNT_ID;
|
|
520
|
+
if (isDefault) {
|
|
521
|
+
return {
|
|
522
|
+
...cfg,
|
|
523
|
+
channels: {
|
|
524
|
+
...cfg.channels,
|
|
525
|
+
"5g-message": {
|
|
526
|
+
...cfg.channels?.["5g-message"],
|
|
527
|
+
enabled: true,
|
|
528
|
+
},
|
|
529
|
+
},
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
const customImCfg = cfg.channels?.["5g-message"];
|
|
533
|
+
return {
|
|
534
|
+
...cfg,
|
|
535
|
+
channels: {
|
|
536
|
+
...cfg.channels,
|
|
537
|
+
"5g-message": {
|
|
538
|
+
...customImCfg,
|
|
539
|
+
accounts: {
|
|
540
|
+
...customImCfg?.accounts,
|
|
541
|
+
[accountId]: {
|
|
542
|
+
...(customImCfg?.accounts?.[accountId] || {}),
|
|
543
|
+
enabled: true,
|
|
544
|
+
},
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
};
|
|
549
|
+
},
|
|
550
|
+
},
|
|
551
|
+
status: {
|
|
552
|
+
defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID),
|
|
553
|
+
buildChannelSummary: ({ snapshot }) => ({
|
|
554
|
+
...buildBaseChannelStatusSummary(snapshot),
|
|
555
|
+
}),
|
|
556
|
+
buildAccountSnapshot: ({ account, runtime }) => ({
|
|
557
|
+
accountId: account.accountId,
|
|
558
|
+
enabled: account.enabled,
|
|
559
|
+
configured: account.configured,
|
|
560
|
+
host: account.host,
|
|
561
|
+
botName: account.botName,
|
|
562
|
+
running: runtime?.running ?? false,
|
|
563
|
+
lastStartAt: runtime?.lastStartAt ?? null,
|
|
564
|
+
lastStopAt: runtime?.lastStopAt ?? null,
|
|
565
|
+
lastError: runtime?.lastError ?? null,
|
|
566
|
+
}),
|
|
567
|
+
},
|
|
568
|
+
gateway: {
|
|
569
|
+
startAccount: async (ctx) => {
|
|
570
|
+
const account = resolveCustomIMAccount(ctx.cfg, ctx.accountId);
|
|
571
|
+
if (!account.configured) {
|
|
572
|
+
throw new Error(`Custom IM account ${ctx.accountId} not configured`);
|
|
573
|
+
}
|
|
574
|
+
ctx.log?.info(`starting 5g-message[${ctx.accountId}]`);
|
|
575
|
+
// Set initial status
|
|
576
|
+
ctx.setStatus({ accountId: ctx.accountId, connected: false });
|
|
577
|
+
// Create a promise that will be resolved when we need to stop
|
|
578
|
+
let stopResolver = null;
|
|
579
|
+
const stopPromise = new Promise((resolve) => {
|
|
580
|
+
stopResolver = resolve;
|
|
581
|
+
});
|
|
582
|
+
const client = new CustomIMClient(account, async (msg) => {
|
|
583
|
+
ctx.log?.info(`[CustomIM] Received message from ${msg.sender}: ${msg.content}`);
|
|
584
|
+
try {
|
|
585
|
+
await dispatchCustomIMMessage({
|
|
586
|
+
cfg: ctx.cfg,
|
|
587
|
+
runtime: ctx.runtime,
|
|
588
|
+
account,
|
|
589
|
+
msg,
|
|
590
|
+
client,
|
|
591
|
+
});
|
|
592
|
+
ctx.log?.info(`[CustomIM] Message processed successfully`);
|
|
593
|
+
}
|
|
594
|
+
catch (error) {
|
|
595
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
596
|
+
const errorStack = error instanceof Error ? error.stack : 'N/A';
|
|
597
|
+
ctx.log?.error(`[CustomIM] Error processing message: ${errorMsg}`);
|
|
598
|
+
ctx.log?.error(`[CustomIM] Error stack: ${errorStack}`);
|
|
599
|
+
}
|
|
600
|
+
}, () => {
|
|
601
|
+
// Called when WebSocket connection is closed
|
|
602
|
+
ctx.log?.info(`[CustomIM] Connection lost, triggering reconnect...`);
|
|
603
|
+
unregisterConnection(ctx.accountId);
|
|
604
|
+
ctx.setStatus({ accountId: ctx.accountId, connected: false });
|
|
605
|
+
// Trigger reconnect by resolving the stop promise
|
|
606
|
+
if (stopResolver) {
|
|
607
|
+
stopResolver();
|
|
608
|
+
}
|
|
609
|
+
}, console, () => {
|
|
610
|
+
// Called on each heartbeat - report connection is active to prevent stale-socket detection
|
|
611
|
+
ctx.setStatus({ accountId: ctx.accountId, connected: true });
|
|
612
|
+
});
|
|
613
|
+
await client.connect();
|
|
614
|
+
// Register the connection for this account
|
|
615
|
+
registerConnection(ctx.accountId, client);
|
|
616
|
+
// Update status to connected
|
|
617
|
+
ctx.setStatus({ accountId: ctx.accountId, connected: true });
|
|
618
|
+
ctx.log?.info(`[CustomIM] Connection established, channel is now running`);
|
|
619
|
+
// Wait for either abort signal or connection close
|
|
620
|
+
return new Promise((resolve) => {
|
|
621
|
+
const stopHandler = () => {
|
|
622
|
+
ctx.log?.info(`stopping 5g-message[${ctx.accountId}]`);
|
|
623
|
+
ctx.setStatus({ accountId: ctx.accountId, connected: false });
|
|
624
|
+
unregisterConnection(ctx.accountId);
|
|
625
|
+
client.disconnect();
|
|
626
|
+
resolve({ stop: stopHandler });
|
|
627
|
+
};
|
|
628
|
+
// Listen for abort signal
|
|
629
|
+
if (ctx.abortSignal) {
|
|
630
|
+
ctx.abortSignal.addEventListener('abort', stopHandler);
|
|
631
|
+
}
|
|
632
|
+
// Also resolve when connection is closed (stopPromise)
|
|
633
|
+
// This triggers immediate reconnect instead of waiting for OpenClaw's backoff
|
|
634
|
+
stopPromise.then(() => {
|
|
635
|
+
if (ctx.abortSignal) {
|
|
636
|
+
ctx.abortSignal.removeEventListener('abort', stopHandler);
|
|
637
|
+
}
|
|
638
|
+
// Don't resolve immediately - let OpenClaw handle the restart
|
|
639
|
+
// But signal that we want to restart by throwing an error
|
|
640
|
+
resolve({ stop: stopHandler });
|
|
641
|
+
});
|
|
642
|
+
});
|
|
643
|
+
},
|
|
644
|
+
},
|
|
645
|
+
outbound: {
|
|
646
|
+
deliveryMode: "direct",
|
|
647
|
+
sendText: async ({ cfg, accountId, peerId, text }) => {
|
|
648
|
+
const account = resolveCustomIMAccount(cfg, accountId);
|
|
649
|
+
if (!account.configured) {
|
|
650
|
+
throw new Error(`Custom IM account ${accountId} not configured`);
|
|
651
|
+
}
|
|
652
|
+
const client = getActiveConnection(accountId);
|
|
653
|
+
if (!client) {
|
|
654
|
+
throw new Error(`Custom IM account ${accountId} is not connected`);
|
|
655
|
+
}
|
|
656
|
+
const result = await client.sendText(peerId, text);
|
|
657
|
+
return { ok: true, messageId: result.messageId };
|
|
658
|
+
},
|
|
659
|
+
},
|
|
660
|
+
pairing: {
|
|
661
|
+
idLabel: "customImUserId",
|
|
662
|
+
normalizeAllowEntry: (entry) => entry.replace(/^(custom|cim):/i, ""),
|
|
663
|
+
notifyApproval: async ({ cfg, id, accountId }) => {
|
|
664
|
+
const account = resolveCustomIMAccount(cfg, accountId);
|
|
665
|
+
if (!account.configured)
|
|
666
|
+
return;
|
|
667
|
+
const client = getActiveConnection(accountId);
|
|
668
|
+
if (!client) {
|
|
669
|
+
console.error(`[CustomIM] Account ${accountId} is not connected, cannot send approval message`);
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
try {
|
|
673
|
+
await client.sendText(id, "✅ You have been approved to chat with me!");
|
|
674
|
+
}
|
|
675
|
+
catch (error) {
|
|
676
|
+
console.error("[CustomIM] Failed to send approval message:", error);
|
|
677
|
+
}
|
|
678
|
+
},
|
|
679
|
+
},
|
|
680
|
+
security: {
|
|
681
|
+
dmPolicy: "open",
|
|
682
|
+
},
|
|
683
|
+
};
|
|
684
|
+
// ============================================================================
|
|
685
|
+
// Plugin Registration
|
|
686
|
+
// ============================================================================
|
|
687
|
+
const plugin = {
|
|
688
|
+
id: "5g-message",
|
|
689
|
+
name: "Fontdo 5G Message",
|
|
690
|
+
description: "Fontdo 5G 消息平台集成,通过 WebSocket JSON-RPC 2.0 协议",
|
|
691
|
+
configSchema: emptyPluginConfigSchema(),
|
|
692
|
+
register(api) {
|
|
693
|
+
// Store the runtime for use in message dispatching
|
|
694
|
+
pluginRuntime = api.runtime;
|
|
695
|
+
api.registerChannel({ plugin: fontdo5GMessagePlugin });
|
|
696
|
+
},
|
|
697
|
+
};
|
|
698
|
+
export default plugin;
|
|
699
|
+
//# sourceMappingURL=index.js.map
|