@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/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