@folotoy/folotoy-openclaw-plugin 0.1.1 → 0.1.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.
- package/README.md +3 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/__tests__/channel.test.ts +10 -10
- package/src/__tests__/mqtt.test.ts +10 -4
- package/src/index.ts +8 -7
- package/src/mqtt.ts +6 -2
package/README.md
CHANGED
|
@@ -64,10 +64,11 @@ Example `openclaw.json`:
|
|
|
64
64
|
|
|
65
65
|
## MQTT
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
Inbound and outbound use separate topics:
|
|
68
68
|
|
|
69
69
|
```
|
|
70
|
-
/openapi/folotoy/{sn}/thing/
|
|
70
|
+
Inbound (Toy → Plugin): /openapi/folotoy/{sn}/thing/command/call
|
|
71
|
+
Outbound (Plugin → Toy): /openapi/folotoy/{sn}/thing/command/callAck
|
|
71
72
|
```
|
|
72
73
|
|
|
73
74
|
The plugin connects with an `openapi:` prefix on the username to distinguish itself from the toy's own connection:
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from 'vitest'
|
|
2
2
|
import { EventEmitter } from 'events'
|
|
3
|
-
import {
|
|
3
|
+
import { buildInboundTopic, buildOutboundTopic } from '../mqtt.js'
|
|
4
4
|
|
|
5
5
|
// Replicate the message parsing logic from index.ts for unit testing
|
|
6
6
|
type InboundMessage = {
|
|
@@ -24,7 +24,7 @@ function makeMockClient() {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
function setupSubscriber(client: ReturnType<typeof makeMockClient>, toy_sn: string, onMessage: (msgId: number, text: string) => void) {
|
|
27
|
-
const topic =
|
|
27
|
+
const topic = buildInboundTopic(toy_sn)
|
|
28
28
|
client.subscribe(topic, () => {})
|
|
29
29
|
client.on('message', (_topic: string, payload: Buffer) => {
|
|
30
30
|
if (_topic !== topic) return
|
|
@@ -40,7 +40,7 @@ function setupSubscriber(client: ReturnType<typeof makeMockClient>, toy_sn: stri
|
|
|
40
40
|
|
|
41
41
|
describe('inbound message parsing', () => {
|
|
42
42
|
const toy_sn = 'SN001'
|
|
43
|
-
const
|
|
43
|
+
const inboundTopic = buildInboundTopic(toy_sn)
|
|
44
44
|
|
|
45
45
|
it('calls onMessage with msgId and text on valid chat_input', () => {
|
|
46
46
|
const client = makeMockClient()
|
|
@@ -48,7 +48,7 @@ describe('inbound message parsing', () => {
|
|
|
48
48
|
setupSubscriber(client, toy_sn, onMessage)
|
|
49
49
|
|
|
50
50
|
const msg: InboundMessage = { msgId: 42, identifier: 'chat_input', outParams: { text: 'hello' } }
|
|
51
|
-
client.emit('message',
|
|
51
|
+
client.emit('message', inboundTopic, Buffer.from(JSON.stringify(msg)))
|
|
52
52
|
|
|
53
53
|
expect(onMessage).toHaveBeenCalledWith(42, 'hello')
|
|
54
54
|
})
|
|
@@ -59,7 +59,7 @@ describe('inbound message parsing', () => {
|
|
|
59
59
|
setupSubscriber(client, toy_sn, onMessage)
|
|
60
60
|
|
|
61
61
|
const msg: InboundMessage = { msgId: 1, identifier: 'chat_input', outParams: { text: 'hi' } }
|
|
62
|
-
client.emit('message', '/openapi/folotoy/OTHER/thing/
|
|
62
|
+
client.emit('message', '/openapi/folotoy/OTHER/thing/command/call', Buffer.from(JSON.stringify(msg)))
|
|
63
63
|
|
|
64
64
|
expect(onMessage).not.toHaveBeenCalled()
|
|
65
65
|
})
|
|
@@ -69,7 +69,7 @@ describe('inbound message parsing', () => {
|
|
|
69
69
|
const onMessage = vi.fn()
|
|
70
70
|
setupSubscriber(client, toy_sn, onMessage)
|
|
71
71
|
|
|
72
|
-
client.emit('message',
|
|
72
|
+
client.emit('message', inboundTopic, Buffer.from(JSON.stringify({ msgId: 1, identifier: 'other', outParams: { text: 'hi' } })))
|
|
73
73
|
|
|
74
74
|
expect(onMessage).not.toHaveBeenCalled()
|
|
75
75
|
})
|
|
@@ -79,7 +79,7 @@ describe('inbound message parsing', () => {
|
|
|
79
79
|
const onMessage = vi.fn()
|
|
80
80
|
setupSubscriber(client, toy_sn, onMessage)
|
|
81
81
|
|
|
82
|
-
client.emit('message',
|
|
82
|
+
client.emit('message', inboundTopic, Buffer.from('not json'))
|
|
83
83
|
|
|
84
84
|
expect(onMessage).not.toHaveBeenCalled()
|
|
85
85
|
})
|
|
@@ -87,7 +87,7 @@ describe('inbound message parsing', () => {
|
|
|
87
87
|
|
|
88
88
|
describe('outbound message format', () => {
|
|
89
89
|
const toy_sn = 'SN001'
|
|
90
|
-
const
|
|
90
|
+
const outboundTopic = buildOutboundTopic(toy_sn)
|
|
91
91
|
|
|
92
92
|
it('publishes chat_output with correct msgId and content', () => {
|
|
93
93
|
const client = makeMockClient()
|
|
@@ -98,11 +98,11 @@ describe('outbound message format', () => {
|
|
|
98
98
|
identifier: 'chat_output',
|
|
99
99
|
outParams: { content },
|
|
100
100
|
}
|
|
101
|
-
client.publish(
|
|
101
|
+
client.publish(outboundTopic, JSON.stringify(outMsg))
|
|
102
102
|
|
|
103
103
|
expect(client.publish).toHaveBeenCalledOnce()
|
|
104
104
|
const [t, payload] = client.publish.mock.calls[0] as [string, string]
|
|
105
|
-
expect(t).toBe(
|
|
105
|
+
expect(t).toBe(outboundTopic)
|
|
106
106
|
expect(JSON.parse(payload)).toEqual({ msgId: 42, identifier: 'chat_output', outParams: { content: 'world' } })
|
|
107
107
|
})
|
|
108
108
|
})
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import {
|
|
2
|
+
import { buildInboundTopic, buildOutboundTopic } from '../mqtt.js'
|
|
3
3
|
|
|
4
|
-
describe('
|
|
5
|
-
it('builds the correct topic for a given SN', () => {
|
|
6
|
-
expect(
|
|
4
|
+
describe('buildInboundTopic', () => {
|
|
5
|
+
it('builds the correct inbound topic for a given SN', () => {
|
|
6
|
+
expect(buildInboundTopic('SN001')).toBe('/openapi/folotoy/SN001/thing/command/call')
|
|
7
|
+
})
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
describe('buildOutboundTopic', () => {
|
|
11
|
+
it('builds the correct outbound topic for a given SN', () => {
|
|
12
|
+
expect(buildOutboundTopic('SN001')).toBe('/openapi/folotoy/SN001/thing/command/callAck')
|
|
7
13
|
})
|
|
8
14
|
})
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { OpenClawPluginApi, ChannelPlugin } from 'openclaw/plugin-sdk/core'
|
|
2
|
-
import { resolveCredentials, createMqttClient,
|
|
2
|
+
import { resolveCredentials, createMqttClient, buildInboundTopic, buildOutboundTopic } from './mqtt.js'
|
|
3
3
|
import { DEFAULT_MQTT_HOST, DEFAULT_MQTT_PORT, flatToPluginConfig } from './config.js'
|
|
4
4
|
import type { FlatChannelConfig } from './config.js'
|
|
5
5
|
import type { MqttClient } from 'mqtt'
|
|
@@ -83,12 +83,13 @@ const folotoyChannel: ChannelPlugin<FlatChannelConfig> = {
|
|
|
83
83
|
const mqttConfig = flatToPluginConfig(account)
|
|
84
84
|
const credentials = await resolveCredentials(mqttConfig)
|
|
85
85
|
const client = await createMqttClient(mqttConfig, credentials)
|
|
86
|
-
const
|
|
86
|
+
const inboundTopic = buildInboundTopic(credentials.toy_sn)
|
|
87
|
+
const outboundTopic = buildOutboundTopic(credentials.toy_sn)
|
|
87
88
|
|
|
88
89
|
activeClients.set(accountId, { client, toy_sn: credentials.toy_sn, nextMsgId: 1 })
|
|
89
|
-
log?.info?.(`Connected to MQTT broker, subscribed to ${
|
|
90
|
+
log?.info?.(`Connected to MQTT broker, subscribed to ${inboundTopic}`)
|
|
90
91
|
|
|
91
|
-
client.subscribe(
|
|
92
|
+
client.subscribe(inboundTopic, (err) => {
|
|
92
93
|
if (err) log?.error?.(`Failed to subscribe: ${err.message}`)
|
|
93
94
|
})
|
|
94
95
|
|
|
@@ -124,7 +125,7 @@ const folotoyChannel: ChannelPlugin<FlatChannelConfig> = {
|
|
|
124
125
|
identifier: 'chat_output',
|
|
125
126
|
outParams: { content: replyPayload.text },
|
|
126
127
|
}
|
|
127
|
-
client.publish(
|
|
128
|
+
client.publish(outboundTopic, JSON.stringify(outMsg))
|
|
128
129
|
},
|
|
129
130
|
onError: (err) => log?.error?.(`Dispatch error: ${String(err)}`),
|
|
130
131
|
},
|
|
@@ -154,14 +155,14 @@ const folotoyChannel: ChannelPlugin<FlatChannelConfig> = {
|
|
|
154
155
|
const entry = activeClients.get(key)
|
|
155
156
|
if (!entry) throw new Error(`No active MQTT client for account "${key}"`)
|
|
156
157
|
|
|
157
|
-
const
|
|
158
|
+
const outboundTopic = buildOutboundTopic(entry.toy_sn)
|
|
158
159
|
const msgId = entry.nextMsgId++
|
|
159
160
|
const outMsg: OutboundMessage = {
|
|
160
161
|
msgId,
|
|
161
162
|
identifier: 'chat_output',
|
|
162
163
|
outParams: { content: text },
|
|
163
164
|
}
|
|
164
|
-
entry.client.publish(
|
|
165
|
+
entry.client.publish(outboundTopic, JSON.stringify(outMsg))
|
|
165
166
|
return { channel: 'folotoy', messageId: String(msgId) }
|
|
166
167
|
},
|
|
167
168
|
},
|
package/src/mqtt.ts
CHANGED
|
@@ -44,8 +44,12 @@ export async function resolveCredentials(config: PluginConfig): Promise<MqttCred
|
|
|
44
44
|
return directCredentials(config.auth)
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
export function
|
|
48
|
-
return `/openapi/folotoy/${toy_sn}/thing/
|
|
47
|
+
export function buildInboundTopic(toy_sn: string): string {
|
|
48
|
+
return `/openapi/folotoy/${toy_sn}/thing/command/call`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function buildOutboundTopic(toy_sn: string): string {
|
|
52
|
+
return `/openapi/folotoy/${toy_sn}/thing/command/callAck`
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
export async function createMqttClient(config: PluginConfig, credentials: MqttCredentials): Promise<MqttClient> {
|