@omnixal/openclaw-nats-plugin 0.1.15 → 0.1.17

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnixal/openclaw-nats-plugin",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "NATS JetStream event-driven plugin for OpenClaw",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -17,7 +17,7 @@ export const envSchema = {
17
17
  },
18
18
  gateway: {
19
19
  wsUrl: Env.string({ default: 'ws://localhost:18789', env: 'OPENCLAW_WS_URL' }),
20
- token: Env.string({ default: '', env: 'OPENCLAW_GATEWAY_TOKEN' }),
20
+ token: Env.string({ default: '', env: 'OPENCLAW_DEVICE_TOKEN' }),
21
21
  },
22
22
  consumer: {
23
23
  name: Env.string({ default: 'openclaw-main', env: 'NATS_CONSUMER_NAME' }),
@@ -15,6 +15,7 @@ export interface GatewayInjectPayload {
15
15
  export class GatewayClientService extends BaseService implements OnModuleInit, OnModuleDestroy {
16
16
  private ws: WebSocket | null = null;
17
17
  private connected = false;
18
+ private connectSent = false;
18
19
  private reconnectAttempt = 0;
19
20
  private reconnectTimer: Timer | null = null;
20
21
  private requestId = 0;
@@ -24,33 +25,35 @@ export class GatewayClientService extends BaseService implements OnModuleInit, O
24
25
  async onModuleInit(): Promise<void> {
25
26
  this.wsUrl = this.config.get('gateway.wsUrl');
26
27
  this.token = this.config.get('gateway.token');
27
- if (this.wsUrl) {
28
+ if (this.wsUrl && this.token) {
28
29
  this.connect();
30
+ } else {
31
+ this.logger.warn('Gateway WebSocket not configured — skipping connection (need wsUrl + deviceToken)');
29
32
  }
30
33
  }
31
34
 
32
35
  private connect(): void {
33
36
  try {
34
- const url = this.token ? `${this.wsUrl}?token=${this.token}` : this.wsUrl;
35
- this.ws = new WebSocket(url);
37
+ this.connectSent = false;
38
+ this.ws = new WebSocket(this.wsUrl);
36
39
 
37
40
  this.ws.onopen = () => {
38
- this.logger.info('Gateway WebSocket connected');
39
41
  this.reconnectAttempt = 0;
40
- this.sendConnect();
42
+ this.logger.info('Gateway WebSocket opened, waiting for connect.challenge');
41
43
  };
42
44
 
43
45
  this.ws.onmessage = (event) => {
44
- this.handleMessage(event);
46
+ this.handleMessage(String(event.data));
45
47
  };
46
48
 
47
49
  this.ws.onclose = () => {
48
50
  this.connected = false;
51
+ this.connectSent = false;
49
52
  this.scheduleReconnect();
50
53
  };
51
54
 
52
- this.ws.onerror = (event) => {
53
- this.logger.warn('Gateway WebSocket error', event);
55
+ this.ws.onerror = () => {
56
+ this.logger.warn('Gateway WebSocket error');
54
57
  this.connected = false;
55
58
  };
56
59
  } catch (err) {
@@ -59,23 +62,77 @@ export class GatewayClientService extends BaseService implements OnModuleInit, O
59
62
  }
60
63
  }
61
64
 
62
- private handleMessage(event: { data: unknown }): void {
65
+ private handleMessage(data: string): void {
66
+ let frame: any;
63
67
  try {
64
- const frame = JSON.parse(String(event.data));
65
- if (frame.type === 'res' && frame.ok) {
66
- this.connected = true;
67
- }
68
+ frame = JSON.parse(data);
68
69
  } catch {
69
- // ignore parse errors
70
+ this.logger.warn(`Failed to parse WebSocket message: ${data.slice(0, 200)}`);
71
+ return;
72
+ }
73
+
74
+ // Server challenge — respond with connect frame
75
+ if (frame.type === 'event' && frame.event === 'connect.challenge') {
76
+ this.logger.debug('Received connect.challenge from server');
77
+ this.sendConnectFrame();
78
+ return;
79
+ }
80
+
81
+ // Some gateway versions send an event before challenge; treat any pre-connect event as trigger
82
+ if (!this.connectSent && frame.type === 'event') {
83
+ this.logger.debug('Received event before connect sent, sending connect frame');
84
+ this.sendConnectFrame();
85
+ return;
86
+ }
87
+
88
+ // Successful connect response — must be hello-ok
89
+ if (frame.type === 'res' && frame.ok === true) {
90
+ const payload = frame.payload;
91
+ if (payload?.type === 'hello-ok') {
92
+ if (!this.connected) {
93
+ this.connected = true;
94
+ this.logger.info('OpenClaw handshake complete — connected');
95
+ }
96
+ return;
97
+ }
98
+ // Regular RPC ok response (e.g. for inject calls)
99
+ this.logger.debug('Received RPC ok response', { id: frame.id });
100
+ return;
101
+ }
102
+
103
+ // Error response
104
+ if (frame.type === 'res' && frame.ok === false) {
105
+ this.logger.warn('Gateway RPC error', { id: frame.id, error: frame.error });
70
106
  }
71
107
  }
72
108
 
73
- private sendConnect(): void {
109
+ private sendConnectFrame(): void {
110
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN || this.connectSent) return;
111
+ this.connectSent = true;
112
+ this.logger.info('Sending connect frame');
113
+
74
114
  this.send({
75
115
  type: 'req',
76
- id: ++this.requestId,
116
+ id: `connect-${++this.requestId}`,
77
117
  method: 'connect',
78
- params: {},
118
+ params: {
119
+ minProtocol: 3,
120
+ maxProtocol: 3,
121
+ client: {
122
+ id: 'nats-sidecar',
123
+ version: '1.0.0',
124
+ platform: 'linux',
125
+ mode: 'backend',
126
+ },
127
+ role: 'operator',
128
+ scopes: ['operator.read'],
129
+ caps: [],
130
+ commands: [],
131
+ permissions: {},
132
+ auth: { token: this.token },
133
+ locale: 'en-US',
134
+ userAgent: 'nats-sidecar/1.0.0',
135
+ },
79
136
  });
80
137
  }
81
138
 
@@ -87,7 +144,7 @@ export class GatewayClientService extends BaseService implements OnModuleInit, O
87
144
 
88
145
  private scheduleReconnect(): void {
89
146
  if (this.reconnectTimer) return;
90
- const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempt), 30000);
147
+ const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempt), 30_000);
91
148
  this.reconnectAttempt++;
92
149
  this.logger.debug(`Reconnecting to Gateway in ${delay}ms (attempt ${this.reconnectAttempt})`);
93
150
  this.reconnectTimer = setTimeout(() => {
@@ -102,7 +159,7 @@ export class GatewayClientService extends BaseService implements OnModuleInit, O
102
159
  }
103
160
  this.send({
104
161
  type: 'req',
105
- id: ++this.requestId,
162
+ id: `rpc-${++this.requestId}`,
106
163
  method: 'send',
107
164
  params: {
108
165
  target: payload.target,
@@ -127,5 +184,6 @@ export class GatewayClientService extends BaseService implements OnModuleInit, O
127
184
  this.ws = null;
128
185
  }
129
186
  this.connected = false;
187
+ this.connectSent = false;
130
188
  }
131
189
  }