openclaw-elys 1.6.0 → 1.7.1

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
@@ -4,6 +4,22 @@ OpenClaw channel plugin for [Elys App](https://elys.ai) — connects your local
4
4
 
5
5
  Elys App 的 OpenClaw 频道插件 — 将本地 OpenClaw 智能体连接到 Elys 手机应用。
6
6
 
7
+ ## Requirements / 版本要求
8
+
9
+ OpenClaw >= **2026.2.19**(通过 `openclaw -v` 查看当前版本)
10
+
11
+ 低于该版本可能导致 AI 推理无法正常工作(缺少 channelRuntime 支持)。
12
+
13
+ 升级方式:
14
+
15
+ ```bash
16
+ npm install -g openclaw
17
+ # 国内网络慢可使用镜像源:
18
+ npm install -g openclaw --registry=https://registry.npmmirror.com
19
+ ```
20
+
21
+ 其他升级方式参考:[OpenClaw 安装文档](https://docs.openclaw.ai/install/development-channels#switching-channels)
22
+
7
23
  ## Install & Setup / 安装与配置
8
24
 
9
25
  ```bash
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { elysPlugin } from "./src/channel.js";
2
+ import { setElysRuntime } from "./src/runtime.js";
2
3
  export { elysPlugin } from "./src/channel.js";
3
4
  export { monitorElysProvider } from "./src/monitor.js";
4
5
  export { registerDevice } from "./src/register.js";
@@ -10,6 +11,7 @@ const plugin = {
10
11
  description: "Elys App channel plugin — connects to Elys App via MQTT gateway",
11
12
  configSchema: { type: "object", properties: {} },
12
13
  register(api) {
14
+ setElysRuntime(api.runtime);
13
15
  api.registerChannel({ plugin: elysPlugin });
14
16
  },
15
17
  };
@@ -83,7 +83,6 @@ export declare const elysPlugin: {
83
83
  error?: (...args: unknown[]) => void;
84
84
  };
85
85
  setStatus: (status: Record<string, unknown>) => void;
86
- channelRuntime?: Record<string, unknown>;
87
86
  }) => Promise<void>;
88
87
  };
89
88
  };
@@ -56,7 +56,6 @@ export const elysPlugin = {
56
56
  runtime: ctx.runtime,
57
57
  abortSignal: ctx.abortSignal,
58
58
  accountId: ctx.accountId,
59
- channelRuntime: ctx.channelRuntime,
60
59
  });
61
60
  },
62
61
  },
@@ -3,39 +3,7 @@ export interface MonitorElysOpts {
3
3
  runtime?: Record<string, unknown>;
4
4
  abortSignal?: AbortSignal;
5
5
  accountId?: string;
6
- channelRuntime?: ChannelRuntimeReply;
7
6
  }
8
- /** Subset of OpenClaw's PluginRuntime["channel"] that we use */
9
- type ReplyPayload = {
10
- text?: string;
11
- mediaUrl?: string;
12
- audioAsVoice?: boolean;
13
- [key: string]: unknown;
14
- };
15
- type ChannelRuntimeReply = {
16
- reply?: {
17
- dispatchReplyWithBufferedBlockDispatcher?: (params: {
18
- ctx: Record<string, unknown>;
19
- cfg: Record<string, unknown>;
20
- dispatcherOptions: {
21
- deliver: (payload: ReplyPayload, info: {
22
- kind: "tool" | "block" | "final";
23
- }) => Promise<void>;
24
- onError?: (err: unknown, info: {
25
- kind: string;
26
- }) => void;
27
- };
28
- replyOptions?: Record<string, unknown>;
29
- }) => Promise<{
30
- queuedFinal: boolean;
31
- counts: Record<string, number>;
32
- }>;
33
- finalizeInboundContext?: (ctx: Record<string, unknown>) => Record<string, unknown>;
34
- };
35
- routing?: {
36
- resolveAgentRoute?: (params: Record<string, unknown>) => Record<string, unknown>;
37
- };
38
- };
39
7
  /**
40
8
  * The main monitor loop for the Elys channel.
41
9
  * Ensures device is registered, then connects to MQTT and dispatches
@@ -43,4 +11,3 @@ type ChannelRuntimeReply = {
43
11
  * channelRuntime dispatch API (supports streaming).
44
12
  */
45
13
  export declare function monitorElysProvider(opts: MonitorElysOpts): Promise<void>;
46
- export {};
@@ -1,6 +1,7 @@
1
1
  import { loadCredentials } from "./config.js";
2
2
  import { registerDevice } from "./register.js";
3
3
  import { ElysDeviceMQTTClient } from "./mqtt-client.js";
4
+ import { getElysRuntime } from "./runtime.js";
4
5
  /**
5
6
  * The main monitor loop for the Elys channel.
6
7
  * Ensures device is registered, then connects to MQTT and dispatches
@@ -26,19 +27,18 @@ export async function monitorElysProvider(opts) {
26
27
  }
27
28
  // 2. Connect MQTT
28
29
  const mqttClient = new ElysDeviceMQTTClient(credentials, log);
29
- // 3. Set up command handler
30
- const channelRuntime = opts.channelRuntime;
31
- const dispatchReply = channelRuntime?.reply?.dispatchReplyWithBufferedBlockDispatcher;
32
- const finalizeCtx = channelRuntime?.reply?.finalizeInboundContext;
33
- log(`[elys] channelRuntime available: ${!!channelRuntime}, dispatchReply: ${!!dispatchReply}, finalizeCtx: ${!!finalizeCtx}`);
30
+ // 3. Set up command handler using PluginRuntime (same pattern as feishu)
31
+ const core = getElysRuntime();
32
+ const dispatchReplyFromConfig = core?.channel?.reply?.dispatchReplyFromConfig;
33
+ const withReplyDispatcher = core?.channel?.reply?.withReplyDispatcher;
34
+ const finalizeCtx = core?.channel?.reply?.finalizeInboundContext;
35
+ log(`[elys] pluginRuntime available: ${!!core}, dispatchReplyFromConfig: ${!!dispatchReplyFromConfig}, finalizeCtx: ${!!finalizeCtx}`);
34
36
  const commandHandler = async (cmd, signal) => {
35
37
  log(`[elys] executing command: ${cmd.command}`, cmd.args);
36
- // If we have the full OpenClaw channelRuntime, use the standard dispatch path
37
- if (dispatchReply && finalizeCtx) {
38
+ if (dispatchReplyFromConfig && finalizeCtx && withReplyDispatcher) {
38
39
  try {
39
40
  let seq = 0;
40
41
  let fullText = "";
41
- // Build inbound context following OpenClaw protocol
42
42
  const inboundCtx = finalizeCtx({
43
43
  Body: formatCommandAsText(cmd),
44
44
  BodyForAgent: formatCommandAsText(cmd),
@@ -55,37 +55,49 @@ export async function monitorElysProvider(opts) {
55
55
  OriginatingChannel: "elys",
56
56
  OriginatingTo: credentials.deviceId,
57
57
  });
58
- await dispatchReply({
59
- ctx: inboundCtx,
60
- cfg: opts.config,
61
- dispatcherOptions: {
62
- deliver: async (payload, info) => {
63
- // Skip publishing if this command was aborted by a newer one
64
- if (signal.aborted)
65
- return;
66
- if (payload.text) {
67
- fullText += payload.text;
68
- seq++;
69
- const done = info.kind === "final";
70
- mqttClient.publishStreamChunk(cmd.id, payload.text, seq, done, cmd.reply_to);
71
- if (info.kind === "block") {
72
- log(`[elys] stream chunk #${seq}: ${payload.text.slice(0, 80)}...`);
73
- }
74
- else if (info.kind === "final") {
75
- log(`[elys] final reply delivered`);
76
- }
77
- }
78
- else if (info.kind === "final") {
79
- seq++;
80
- mqttClient.publishStreamChunk(cmd.id, "", seq, true, cmd.reply_to);
81
- log(`[elys] final reply delivered (empty)`);
82
- }
83
- },
84
- onError: (err, info) => {
85
- log(`[elys] dispatch error (${info.kind}):`, err);
86
- },
58
+ // Deliver callback: stream chunks back via MQTT
59
+ const deliver = async (payload, info) => {
60
+ if (signal.aborted)
61
+ return;
62
+ if (payload.text) {
63
+ fullText += payload.text;
64
+ seq++;
65
+ const done = info.kind === "final";
66
+ mqttClient.publishStreamChunk(cmd.id, payload.text, seq, done, cmd.reply_to);
67
+ if (info.kind === "block") {
68
+ log(`[elys] stream chunk #${seq}: ${payload.text.slice(0, 80)}...`);
69
+ }
70
+ else if (info.kind === "final") {
71
+ log(`[elys] final reply delivered`);
72
+ }
73
+ }
74
+ else if (info.kind === "final") {
75
+ seq++;
76
+ mqttClient.publishStreamChunk(cmd.id, "", seq, true, cmd.reply_to);
77
+ log(`[elys] final reply delivered (empty)`);
78
+ }
79
+ };
80
+ // Create dispatcher + dispatch (same pattern as feishu built-in channel)
81
+ const createDispatcher = core.channel.reply.createReplyDispatcherWithTyping;
82
+ const { dispatcher, replyOptions, markDispatchIdle } = createDispatcher({
83
+ deliver,
84
+ onError: (err, info) => {
85
+ log(`[elys] dispatch error (${info.kind}):`, err);
87
86
  },
88
87
  });
88
+ try {
89
+ await dispatchReplyFromConfig({
90
+ ctx: inboundCtx,
91
+ cfg: opts.config,
92
+ dispatcher,
93
+ replyOptions,
94
+ });
95
+ }
96
+ finally {
97
+ dispatcher.markComplete();
98
+ await dispatcher.waitForIdle().catch(() => { });
99
+ markDispatchIdle();
100
+ }
89
101
  return {
90
102
  id: cmd.id,
91
103
  type: "result",
@@ -105,8 +117,8 @@ export async function monitorElysProvider(opts) {
105
117
  };
106
118
  }
107
119
  }
108
- // Fallback: echo the command back (no channelRuntime available)
109
- log(`[elys] no channelRuntime — using fallback echo handler`);
120
+ // Fallback: echo the command back (no pluginRuntime available)
121
+ log(`[elys] no pluginRuntime — using fallback echo handler`);
110
122
  return {
111
123
  id: cmd.id,
112
124
  type: "result",
@@ -0,0 +1,2 @@
1
+ export declare function setElysRuntime(next: unknown): void;
2
+ export declare function getElysRuntime(): any;
@@ -0,0 +1,10 @@
1
+ // Stores the PluginRuntime passed via api.runtime during plugin registration.
2
+ // This is the same pattern used by the built-in feishu channel plugin.
3
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4
+ let runtime = null;
5
+ export function setElysRuntime(next) {
6
+ runtime = next;
7
+ }
8
+ export function getElysRuntime() {
9
+ return runtime;
10
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-elys",
3
- "version": "1.6.0",
3
+ "version": "1.7.1",
4
4
  "description": "OpenClaw Elys channel plugin — connects to Elys App",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",