openclaw-elys 1.4.7 → 1.5.0

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 CHANGED
@@ -8,12 +8,9 @@ Elys App 的 OpenClaw 频道插件 — 将本地 OpenClaw 智能体连接到 Ely
8
8
 
9
9
  ```bash
10
10
  # 1. Install the plugin / 安装插件
11
- openclaw plugins install openclaw-elys
11
+ openclaw plugins install openclaw-elys@latest
12
12
 
13
- # 2. Restart gateway (required after install) / 重启网关(安装后必须)
14
- openclaw gateway restart
15
-
16
- # 3. Register device / 注册设备
13
+ # 2. Register device / 注册设备
17
14
  npx openclaw-elys@latest setup <gateway_url> <register_token>
18
15
  ```
19
16
 
@@ -65,7 +65,7 @@ export async function monitorElysProvider(opts) {
65
65
  fullText += payload.text;
66
66
  seq++;
67
67
  const done = info.kind === "final";
68
- mqttClient.publishStreamChunk(cmd.id, payload.text, seq, done);
68
+ mqttClient.publishStreamChunk(cmd.id, payload.text, seq, done, cmd.reply_to);
69
69
  if (info.kind === "block") {
70
70
  log(`[elys] stream chunk #${seq}: ${payload.text.slice(0, 80)}...`);
71
71
  }
@@ -75,7 +75,7 @@ export async function monitorElysProvider(opts) {
75
75
  }
76
76
  else if (info.kind === "final") {
77
77
  seq++;
78
- mqttClient.publishStreamChunk(cmd.id, "", seq, true);
78
+ mqttClient.publishStreamChunk(cmd.id, "", seq, true, cmd.reply_to);
79
79
  log(`[elys] final reply delivered (empty)`);
80
80
  }
81
81
  },
@@ -11,14 +11,14 @@ export declare class ElysDeviceMQTTClient {
11
11
  private log;
12
12
  private consecutiveFailures;
13
13
  private static readonly MAX_FAILURES_BEFORE_REVOKE_WARNING;
14
+ private commandQueue;
14
15
  constructor(credentials: DeviceCredentials, log?: (...args: unknown[]) => void);
15
16
  setCommandHandler(handler: CommandHandler): void;
16
17
  connect(abortSignal?: AbortSignal): Promise<void>;
17
18
  disconnect(): void;
18
19
  /** Send a stream chunk (for streaming AI responses) */
19
- publishStreamChunk(commandId: string, chunk: string, seq: number, done: boolean): void;
20
+ publishStreamChunk(commandId: string, chunk: string, seq: number, done: boolean, replyTo?: string): void;
20
21
  private handleMessage;
21
22
  private publishAck;
22
- private publishResult;
23
23
  private publish;
24
24
  }
@@ -10,6 +10,7 @@ export class ElysDeviceMQTTClient {
10
10
  log;
11
11
  consecutiveFailures = 0;
12
12
  static MAX_FAILURES_BEFORE_REVOKE_WARNING = 3;
13
+ commandQueue = Promise.resolve();
13
14
  constructor(credentials, log) {
14
15
  this.credentials = credentials;
15
16
  this.log = log ?? console.log;
@@ -42,7 +43,8 @@ export class ElysDeviceMQTTClient {
42
43
  });
43
44
  });
44
45
  this.client.on("message", (_topic, payload) => {
45
- this.handleMessage(payload).catch((err) => {
46
+ // Serial queue: commands are processed one at a time in arrival order
47
+ this.commandQueue = this.commandQueue.then(() => this.handleMessage(payload), () => this.handleMessage(payload)).catch((err) => {
46
48
  this.log("[elys] error handling message:", err);
47
49
  });
48
50
  });
@@ -102,7 +104,7 @@ export class ElysDeviceMQTTClient {
102
104
  }
103
105
  }
104
106
  /** Send a stream chunk (for streaming AI responses) */
105
- publishStreamChunk(commandId, chunk, seq, done) {
107
+ publishStreamChunk(commandId, chunk, seq, done, replyTo) {
106
108
  const msg = {
107
109
  id: commandId,
108
110
  type: "stream",
@@ -111,7 +113,7 @@ export class ElysDeviceMQTTClient {
111
113
  seq,
112
114
  done,
113
115
  };
114
- this.publish(msg);
116
+ this.publish(msg, replyTo);
115
117
  }
116
118
  async handleMessage(payload) {
117
119
  const raw = JSON.parse(payload.toString());
@@ -120,23 +122,25 @@ export class ElysDeviceMQTTClient {
120
122
  return;
121
123
  }
122
124
  this.log(`[elys] received command: ${raw.command} (id: ${raw.id})`);
123
- // Send ACK immediately
125
+ // ACK always goes to shared upstream topic (any gateway instance can process it)
124
126
  this.publishAck(raw.id);
127
+ // Result/stream goes to reply_to if present (routes to the specific gateway instance)
128
+ const replyTo = raw.reply_to;
125
129
  // Execute command
126
130
  if (this.commandHandler) {
127
131
  try {
128
132
  const result = await this.commandHandler(raw);
129
- this.publishResult(result);
133
+ this.publish(result, replyTo);
130
134
  }
131
135
  catch (err) {
132
136
  const errMsg = err instanceof Error ? err.message : String(err);
133
- this.publishResult({
137
+ this.publish({
134
138
  id: raw.id,
135
139
  type: "result",
136
140
  timestamp: Date.now() / 1000,
137
141
  status: "error",
138
142
  error: errMsg,
139
- });
143
+ }, replyTo);
140
144
  }
141
145
  }
142
146
  }
@@ -146,15 +150,13 @@ export class ElysDeviceMQTTClient {
146
150
  type: "ack",
147
151
  timestamp: Math.floor(Date.now() / 1000),
148
152
  };
153
+ // ACK always to default upstream topic (shared subscription)
149
154
  this.publish(msg);
150
155
  }
151
- publishResult(msg) {
152
- this.publish(msg);
153
- }
154
- publish(msg) {
156
+ publish(msg, replyTo) {
155
157
  if (!this.client)
156
158
  return;
157
- const topic = `elys/up/${this.credentials.deviceId}`;
159
+ const topic = replyTo || `elys/up/${this.credentials.deviceId}`;
158
160
  this.client.publish(topic, JSON.stringify(msg), { qos: 1 }, (err) => {
159
161
  if (err) {
160
162
  this.log(`[elys] failed to publish to ${topic}:`, err);
@@ -24,6 +24,8 @@ export interface CommandMessage extends MQTTBaseMessage {
24
24
  type: "command";
25
25
  command: string;
26
26
  args?: Record<string, unknown>;
27
+ /** If set, device should publish result/stream to this topic instead of elys/up/{device_id} */
28
+ reply_to?: string;
27
29
  }
28
30
  export interface AckMessage extends MQTTBaseMessage {
29
31
  type: "ack";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-elys",
3
- "version": "1.4.7",
3
+ "version": "1.5.0",
4
4
  "description": "OpenClaw Elys channel plugin — connects to Elys App",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",