@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 CHANGED
@@ -64,10 +64,11 @@ Example `openclaw.json`:
64
64
 
65
65
  ## MQTT
66
66
 
67
- Both inbound and outbound messages use the same topic:
67
+ Inbound and outbound use separate topics:
68
68
 
69
69
  ```
70
- /openapi/folotoy/{sn}/thing/data/post
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:
@@ -2,7 +2,7 @@
2
2
  "id": "folotoy-openclaw-plugin",
3
3
  "name": "FoloToy",
4
4
  "description": "Empower your FoloToy with OpenClaw AI capabilities.",
5
- "version": "0.1.0",
5
+ "version": "0.1.2",
6
6
  "channels": ["folotoy"],
7
7
  "configSchema": {
8
8
  "type": "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@folotoy/folotoy-openclaw-plugin",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Empower your FoloToy with OpenClaw AI capabilities.",
5
5
  "keywords": [
6
6
  "folotoy",
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, vi } from 'vitest'
2
2
  import { EventEmitter } from 'events'
3
- import { buildTopic } from '../mqtt.js'
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 = buildTopic(toy_sn)
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 topic = buildTopic(toy_sn)
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', topic, Buffer.from(JSON.stringify(msg)))
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/data/post', Buffer.from(JSON.stringify(msg)))
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', topic, Buffer.from(JSON.stringify({ msgId: 1, identifier: 'other', outParams: { text: 'hi' } })))
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', topic, Buffer.from('not json'))
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 topic = buildTopic(toy_sn)
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(topic, JSON.stringify(outMsg))
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(topic)
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 { buildTopic } from '../mqtt.js'
2
+ import { buildInboundTopic, buildOutboundTopic } from '../mqtt.js'
3
3
 
4
- describe('buildTopic', () => {
5
- it('builds the correct topic for a given SN', () => {
6
- expect(buildTopic('SN001')).toBe('/openapi/folotoy/SN001/thing/data/post')
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, buildTopic } from './mqtt.js'
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 topic = buildTopic(credentials.toy_sn)
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 ${topic}`)
90
+ log?.info?.(`Connected to MQTT broker, subscribed to ${inboundTopic}`)
90
91
 
91
- client.subscribe(topic, (err) => {
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(topic, JSON.stringify(outMsg))
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 topic = buildTopic(entry.toy_sn)
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(topic, JSON.stringify(outMsg))
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 buildTopic(toy_sn: string): string {
48
- return `/openapi/folotoy/${toy_sn}/thing/data/post`
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> {