openclaw-elys 1.7.6 → 1.8.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 +114 -0
- package/dist/src/monitor.js +37 -8
- package/dist/src/mqtt-client.d.ts +4 -1
- package/dist/src/mqtt-client.js +5 -1
- package/dist/src/types.d.ts +8 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -52,6 +52,120 @@ One command to revoke device, clean up config, and remove the plugin:
|
|
|
52
52
|
npx openclaw-elys@latest uninstall
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
## Streaming / 流式输出
|
|
56
|
+
|
|
57
|
+
OpenClaw 支持 block streaming,需要在 `~/.openclaw/openclaw.json` 中配置:
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"agents": {
|
|
62
|
+
"defaults": {
|
|
63
|
+
"blockStreamingDefault": "on"
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"channels": {
|
|
67
|
+
"elys": {
|
|
68
|
+
"blockStreaming": true
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
配置后需要**完全重启 OpenClaw**(非热重载)才能生效。
|
|
75
|
+
|
|
76
|
+
发送消息时设置 `stream: true` 可以获取流式输出(多个 chunk),否则只返回最终结果。
|
|
77
|
+
|
|
78
|
+
## MQTT Protocol / 通信协议
|
|
79
|
+
|
|
80
|
+
### Topics
|
|
81
|
+
|
|
82
|
+
| 方向 | Topic | 说明 |
|
|
83
|
+
|------|-------|------|
|
|
84
|
+
| 下行(命令) | `elys/down/{device_id}` | Gateway → Plugin |
|
|
85
|
+
| 上行(响应) | `elys/up/{device_id}` | Plugin → Gateway |
|
|
86
|
+
|
|
87
|
+
### Message Types
|
|
88
|
+
|
|
89
|
+
#### Command(下行)
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"id": "cmd_xxx",
|
|
94
|
+
"type": "command",
|
|
95
|
+
"timestamp": 1709827200,
|
|
96
|
+
"command": "chat",
|
|
97
|
+
"args": { "text": "你好" },
|
|
98
|
+
"stream": true,
|
|
99
|
+
"media_url": "https://example.com/photo.jpg",
|
|
100
|
+
"media_urls": ["https://example.com/1.jpg", "https://example.com/2.jpg"],
|
|
101
|
+
"media_type": "image/jpeg"
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
- `stream`: 是否请求流式响应(可选,默认 false)
|
|
106
|
+
- `media_url`: 单个媒体 URL(可选)
|
|
107
|
+
- `media_urls`: 多个媒体 URL(可选)
|
|
108
|
+
- `media_type`: 媒体 MIME 类型(可选,默认 `application/octet-stream`)
|
|
109
|
+
- `args.text` 和 `media_url` 可以同时存在(文字 + 附件)
|
|
110
|
+
|
|
111
|
+
#### Ack(上行)
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{ "id": "cmd_xxx", "type": "ack", "timestamp": 1709827200 }
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
#### Stream(上行,流式响应)
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"id": "cmd_xxx",
|
|
122
|
+
"type": "stream",
|
|
123
|
+
"timestamp": 1709827200,
|
|
124
|
+
"chunk": "这是一段回复文字",
|
|
125
|
+
"seq": 1,
|
|
126
|
+
"done": false,
|
|
127
|
+
"media_url": "https://example.com/generated.png",
|
|
128
|
+
"media_urls": ["https://example.com/1.png", "https://example.com/2.png"]
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
- `seq`: 序号,从 1 开始递增
|
|
133
|
+
- `done`: 最后一个 chunk 设为 true
|
|
134
|
+
- `media_url`/`media_urls`: AI 生成的媒体(可选)
|
|
135
|
+
|
|
136
|
+
#### Result(上行,最终结果)
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"id": "cmd_xxx",
|
|
141
|
+
"type": "result",
|
|
142
|
+
"timestamp": 1709827200,
|
|
143
|
+
"status": "success",
|
|
144
|
+
"result": { "text": "完整回复内容" },
|
|
145
|
+
"media_url": "https://example.com/output.png",
|
|
146
|
+
"media_urls": ["https://example.com/1.png"]
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Supported Media Types / 支持的媒体类型
|
|
151
|
+
|
|
152
|
+
| 类型 | MIME type | 说明 |
|
|
153
|
+
|------|-----------|------|
|
|
154
|
+
| 图片 | `image/jpeg`, `image/png`, `image/webp` | 照片、截图 |
|
|
155
|
+
| GIF | `image/gif` | 动图 |
|
|
156
|
+
| 视频 | `video/mp4`, `video/quicktime` | 视频 |
|
|
157
|
+
| 音频 | `audio/mpeg`, `audio/ogg`, `audio/wav` | 语音/音频 |
|
|
158
|
+
| 文件 | `application/pdf`, `application/octet-stream` | 通用文件 |
|
|
159
|
+
|
|
160
|
+
不传 `media_type` 时默认为 `application/octet-stream`。
|
|
161
|
+
|
|
162
|
+
### Behavior / 行为
|
|
163
|
+
|
|
164
|
+
- **Debounce**:500ms 内的多条消息会合并为一条发送给 AI
|
|
165
|
+
- **Sequential Queue**:推理中收到新消息会排队等待当前推理完成后再处理(与 Discord/Slack 一致)
|
|
166
|
+
- **Dedup**:QoS 1 重传的消息会自动去重(基于 command ID)
|
|
167
|
+
- **Auto ID**:Gateway 会自动为空 ID 的命令生成 `cmd_` 前缀的唯一 ID
|
|
168
|
+
|
|
55
169
|
## License
|
|
56
170
|
|
|
57
171
|
MIT
|
package/dist/src/monitor.js
CHANGED
|
@@ -39,6 +39,15 @@ export async function monitorElysProvider(opts) {
|
|
|
39
39
|
try {
|
|
40
40
|
let seq = 0;
|
|
41
41
|
let fullText = "";
|
|
42
|
+
// Build media context for inbound (user-sent media)
|
|
43
|
+
const mediaUrls = cmd.mediaUrls?.length
|
|
44
|
+
? cmd.mediaUrls
|
|
45
|
+
: cmd.mediaUrl
|
|
46
|
+
? [cmd.mediaUrl]
|
|
47
|
+
: undefined;
|
|
48
|
+
const mediaTypes = mediaUrls
|
|
49
|
+
? mediaUrls.map(() => cmd.mediaType ?? "application/octet-stream")
|
|
50
|
+
: undefined;
|
|
42
51
|
const inboundCtx = finalizeCtx({
|
|
43
52
|
Body: formatCommandAsText(cmd),
|
|
44
53
|
BodyForAgent: formatCommandAsText(cmd),
|
|
@@ -54,24 +63,44 @@ export async function monitorElysProvider(opts) {
|
|
|
54
63
|
CommandAuthorized: true,
|
|
55
64
|
OriginatingChannel: "elys",
|
|
56
65
|
OriginatingTo: credentials.deviceId,
|
|
66
|
+
// Inbound media from user
|
|
67
|
+
...(mediaUrls && {
|
|
68
|
+
MediaUrl: mediaUrls[0],
|
|
69
|
+
MediaUrls: mediaUrls,
|
|
70
|
+
MediaType: mediaTypes[0],
|
|
71
|
+
MediaTypes: mediaTypes,
|
|
72
|
+
}),
|
|
57
73
|
});
|
|
58
74
|
// Deliver callback: stream chunks back via MQTT
|
|
75
|
+
const wantStream = cmd.stream === true;
|
|
59
76
|
const deliver = async (payload, info) => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
77
|
+
const mediaUrl = payload.mediaUrl?.trim();
|
|
78
|
+
const mediaUrls = payload.mediaUrls?.filter((u) => u?.trim());
|
|
79
|
+
const hasMedia = Boolean(mediaUrl || mediaUrls?.length);
|
|
80
|
+
const media = hasMedia ? { mediaUrl, mediaUrls } : undefined;
|
|
81
|
+
if (payload.text || hasMedia) {
|
|
82
|
+
if (payload.text)
|
|
83
|
+
fullText += payload.text;
|
|
84
|
+
if (wantStream || info.kind === "final") {
|
|
85
|
+
seq++;
|
|
86
|
+
const done = info.kind === "final";
|
|
87
|
+
mqttClient.publishStreamChunk(cmd.id, payload.text ?? "", seq, done, media);
|
|
88
|
+
}
|
|
89
|
+
if (hasMedia) {
|
|
90
|
+
log(`[elys] media: ${mediaUrl ?? mediaUrls?.join(", ")}`);
|
|
91
|
+
}
|
|
65
92
|
if (info.kind === "block") {
|
|
66
|
-
log(`[elys] stream chunk #${seq}: ${payload.text.slice(0, 80)}...`);
|
|
93
|
+
log(`[elys] stream chunk #${seq}: ${(payload.text ?? "").slice(0, 80)}...`);
|
|
67
94
|
}
|
|
68
95
|
else if (info.kind === "final") {
|
|
69
96
|
log(`[elys] final reply delivered`);
|
|
70
97
|
}
|
|
71
98
|
}
|
|
72
99
|
else if (info.kind === "final") {
|
|
73
|
-
|
|
74
|
-
|
|
100
|
+
if (wantStream) {
|
|
101
|
+
seq++;
|
|
102
|
+
mqttClient.publishStreamChunk(cmd.id, "", seq, true);
|
|
103
|
+
}
|
|
75
104
|
log(`[elys] final reply delivered (empty)`);
|
|
76
105
|
}
|
|
77
106
|
};
|
|
@@ -37,7 +37,10 @@ export declare class ElysDeviceMQTTClient {
|
|
|
37
37
|
connect(abortSignal?: AbortSignal): Promise<void>;
|
|
38
38
|
disconnect(): void;
|
|
39
39
|
/** Send a stream chunk (for streaming AI responses) */
|
|
40
|
-
publishStreamChunk(commandId: string, chunk: string, seq: number, done: boolean
|
|
40
|
+
publishStreamChunk(commandId: string, chunk: string, seq: number, done: boolean, media?: {
|
|
41
|
+
mediaUrl?: string;
|
|
42
|
+
mediaUrls?: string[];
|
|
43
|
+
}): void;
|
|
41
44
|
private onMessage;
|
|
42
45
|
private flushDebounce;
|
|
43
46
|
private startCommand;
|
package/dist/src/mqtt-client.js
CHANGED
|
@@ -123,7 +123,7 @@ export class ElysDeviceMQTTClient {
|
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
/** Send a stream chunk (for streaming AI responses) */
|
|
126
|
-
publishStreamChunk(commandId, chunk, seq, done) {
|
|
126
|
+
publishStreamChunk(commandId, chunk, seq, done, media) {
|
|
127
127
|
const msg = {
|
|
128
128
|
id: commandId,
|
|
129
129
|
type: "stream",
|
|
@@ -132,6 +132,10 @@ export class ElysDeviceMQTTClient {
|
|
|
132
132
|
seq,
|
|
133
133
|
done,
|
|
134
134
|
};
|
|
135
|
+
if (media?.mediaUrl)
|
|
136
|
+
msg.mediaUrl = media.mediaUrl;
|
|
137
|
+
if (media?.mediaUrls?.length)
|
|
138
|
+
msg.mediaUrls = media.mediaUrls;
|
|
135
139
|
this.publish(msg);
|
|
136
140
|
}
|
|
137
141
|
// ─── Inbound message pipeline: dedup → ack → debounce → abort → execute ───
|
package/dist/src/types.d.ts
CHANGED
|
@@ -24,6 +24,10 @@ export interface CommandMessage extends MQTTBaseMessage {
|
|
|
24
24
|
type: "command";
|
|
25
25
|
command: string;
|
|
26
26
|
args?: Record<string, unknown>;
|
|
27
|
+
stream?: boolean;
|
|
28
|
+
mediaUrl?: string;
|
|
29
|
+
mediaUrls?: string[];
|
|
30
|
+
mediaType?: string;
|
|
27
31
|
}
|
|
28
32
|
export interface AckMessage extends MQTTBaseMessage {
|
|
29
33
|
type: "ack";
|
|
@@ -33,10 +37,14 @@ export interface StreamMessage extends MQTTBaseMessage {
|
|
|
33
37
|
chunk: string;
|
|
34
38
|
done: boolean;
|
|
35
39
|
seq: number;
|
|
40
|
+
mediaUrl?: string;
|
|
41
|
+
mediaUrls?: string[];
|
|
36
42
|
}
|
|
37
43
|
export interface ResultMessage extends MQTTBaseMessage {
|
|
38
44
|
type: "result";
|
|
39
45
|
status: "success" | "error";
|
|
40
46
|
result?: Record<string, unknown>;
|
|
41
47
|
error?: string;
|
|
48
|
+
mediaUrl?: string;
|
|
49
|
+
mediaUrls?: string[];
|
|
42
50
|
}
|