openclaw-channel-dmwork 0.2.0 → 0.2.2

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.
Files changed (2) hide show
  1. package/package.json +3 -16
  2. package/src/socket.ts +30 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-channel-dmwork",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "DMWork channel plugin for OpenClaw via WuKongIM WebSocket",
5
5
  "main": "index.ts",
6
6
  "type": "module",
@@ -28,20 +28,7 @@
28
28
  "typescript": "^5.9.3"
29
29
  },
30
30
  "openclaw": {
31
- "extensions": [
32
- "./index.ts"
33
- ],
34
- "channel": {
35
- "id": "dmwork",
36
- "label": "DMWork",
37
- "selectionLabel": "DMWork (WuKongIM)",
38
- "docsLabel": "dmwork",
39
- "blurb": "WuKongIM gateway for DMWork",
40
- "order": 90
41
- },
42
- "install": {
43
- "localPath": "extensions/dmwork",
44
- "defaultChoice": "local"
45
- }
31
+ "id": "dmwork",
32
+ "type": "channel"
46
33
  }
47
34
  }
package/src/socket.ts CHANGED
@@ -12,14 +12,20 @@ interface WKSocketOptions {
12
12
  onError?: (err: Error) => void;
13
13
  }
14
14
 
15
+ /**
16
+ * Module-level singleton tracking — ensures only one set of SDK listeners
17
+ * exists at any time, even if startAccount is called multiple times
18
+ * (e.g. during auto-restart).
19
+ */
20
+ let activeSocket: WKSocket | null = null;
21
+
15
22
  /**
16
23
  * WuKongIM WebSocket client for bot connections.
17
24
  * Thin wrapper around wukongimjssdk — the SDK handles binary encoding,
18
25
  * DH key exchange, encryption, heartbeat, reconnect, and RECVACK.
19
26
  *
20
- * NOTE: WKSDK.shared() is a singleton. Each WKSocket must fully
21
- * disconnect before connecting to avoid connection leaks and msgKey
22
- * mismatch errors from concurrent WebSocket sessions.
27
+ * Only one WKSocket can be active at a time (WKSDK is a singleton).
28
+ * Creating a new connection automatically cleans up the previous one.
23
29
  */
24
30
  export class WKSocket extends EventEmitter {
25
31
  private statusListener: ((status: ConnectStatus, reasonCode?: number) => void) | null = null;
@@ -32,26 +38,37 @@ export class WKSocket extends EventEmitter {
32
38
 
33
39
  /** Connect to WuKongIM WebSocket */
34
40
  connect(): void {
41
+ // If another WKSocket was active, fully clean it up first
42
+ if (activeSocket && activeSocket !== this) {
43
+ activeSocket.disconnect();
44
+ }
45
+ activeSocket = this;
46
+
35
47
  const im = WKSDK.shared();
36
48
 
37
- // Ensure clean state — disconnect any prior session first
49
+ // Ensure clean state — disconnect any prior SDK session
38
50
  try { im.disconnect(); } catch { /* ignore */ }
39
51
 
40
52
  im.config.addr = this.opts.wsUrl;
41
53
  im.config.uid = this.opts.uid;
42
54
  im.config.token = this.opts.token;
43
- im.config.deviceFlag = 0; // APP — matches bot registration device flag
55
+ im.config.deviceFlag = 0;
44
56
 
45
- // Remove any stale listeners before adding new ones
57
+ // Remove own stale listeners (safety should already be null)
46
58
  if (this.statusListener) {
47
59
  im.connectManager.removeConnectStatusListener(this.statusListener);
60
+ this.statusListener = null;
48
61
  }
49
62
  if (this.messageListener) {
50
63
  im.chatManager.removeMessageListener(this.messageListener);
64
+ this.messageListener = null;
51
65
  }
52
66
 
53
- // Listen for connection status changes
67
+ // Register exactly one status listener
54
68
  this.statusListener = (status: ConnectStatus, reasonCode?: number) => {
69
+ // Ignore events if we're no longer the active socket
70
+ if (activeSocket !== this) return;
71
+
55
72
  switch (status) {
56
73
  case ConnectStatus.Connected:
57
74
  this.connected = true;
@@ -78,8 +95,10 @@ export class WKSocket extends EventEmitter {
78
95
  };
79
96
  im.connectManager.addConnectStatusListener(this.statusListener);
80
97
 
81
- // Listen for incoming messages — SDK auto-decrypts and sends RECVACK
98
+ // Register exactly one message listener
82
99
  this.messageListener = (message: Message) => {
100
+ if (activeSocket !== this) return;
101
+
83
102
  const content = message.content;
84
103
  const payload: MessagePayload = {
85
104
  type: content?.contentType ?? 0,
@@ -113,6 +132,9 @@ export class WKSocket extends EventEmitter {
113
132
  disconnect(): void {
114
133
  const im = WKSDK.shared();
115
134
  this.connected = false;
135
+ if (activeSocket === this) {
136
+ activeSocket = null;
137
+ }
116
138
  if (this.statusListener) {
117
139
  im.connectManager.removeConnectStatusListener(this.statusListener);
118
140
  this.statusListener = null;