@folotoy/folotoy-openclaw-plugin 0.1.0 → 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
@@ -1,9 +1,11 @@
1
1
  # @folotoy/folotoy-openclaw-plugin
2
2
 
3
- FoloToy channel plugin for [OpenClaw](https://openclaw.ai). Bridges FoloToy smart toys with OpenClaw via MQTT, allowing users to interact with OpenClaw through their FoloToy devices.
3
+ Empower your FoloToy with OpenClaw AI capabilities.
4
+
5
+ An [OpenClaw](https://openclaw.ai) channel plugin that bridges FoloToy smart toys with OpenClaw via MQTT.
4
6
 
5
7
  ```
6
- FoloToy 玩具 <──MQTT──> FoloToy MQTT Broker <──MQTT──> This Plugin <──> OpenClaw
8
+ FoloToy Toy <──MQTT──> FoloToy MQTT Broker <──MQTT──> Plugin <──> OpenClaw
7
9
  ```
8
10
 
9
11
  ## Installation
@@ -12,7 +14,7 @@ FoloToy 玩具 <──MQTT──> FoloToy MQTT Broker <──MQTT──> Thi
12
14
  openclaw plugins install @folotoy/folotoy-openclaw-plugin
13
15
  ```
14
16
 
15
- Or install locally for development:
17
+ For local development:
16
18
 
17
19
  ```bash
18
20
  openclaw plugins install -l .
@@ -20,19 +22,17 @@ openclaw plugins install -l .
20
22
 
21
23
  ## Configuration
22
24
 
23
- The plugin supports two authentication flows.
25
+ The plugin supports two authentication flows. All fields are configured as flat key-value pairs in `openclaw.json` under `channels.folotoy`.
24
26
 
25
27
  ### Flow 2: Direct SN + Key (Default)
26
28
 
27
- Configure your toy SN and key directly:
28
-
29
29
  | Field | Description |
30
30
  |-------|-------------|
31
- | `auth.flow` | `"direct"` |
32
- | `auth.toy_sn` | Toy serial number |
33
- | `auth.toy_key` | Toy key (used as MQTT password) |
34
- | `mqtt.host` | MQTT broker host (default: `192.168.10.138`) |
35
- | `mqtt.port` | MQTT broker port (default: `1883`) |
31
+ | `flow` | `"direct"` |
32
+ | `toy_sn` | Toy serial number |
33
+ | `toy_key` | Toy key (used as MQTT password) |
34
+ | `mqtt_host` | MQTT broker host (default: `198.19.249.25`) |
35
+ | `mqtt_port` | MQTT broker port (default: `1883`) |
36
36
 
37
37
  ### Flow 1: HTTP API Login
38
38
 
@@ -40,22 +40,38 @@ Exchange an API key for MQTT credentials via the FoloToy API:
40
40
 
41
41
  | Field | Description |
42
42
  |-------|-------------|
43
- | `auth.flow` | `"api"` |
44
- | `auth.api_url` | FoloToy API base URL, e.g. `https://api.folotoy.com` |
45
- | `auth.api_key` | Bearer token |
46
- | `auth.toy_sn` | Toy serial number |
47
- | `mqtt.host` | MQTT broker host |
48
- | `mqtt.port` | MQTT broker port (default: `1883`) |
43
+ | `flow` | `"api"` |
44
+ | `toy_sn` | Toy serial number |
45
+ | `api_url` | FoloToy API base URL (default: `https://api.folotoy.cn`) |
46
+ | `api_key` | Bearer token |
47
+ | `mqtt_host` | MQTT broker host |
48
+ | `mqtt_port` | MQTT broker port (default: `1883`) |
49
+
50
+ Example `openclaw.json`:
51
+
52
+ ```json
53
+ {
54
+ "channels": {
55
+ "folotoy": {
56
+ "flow": "direct",
57
+ "toy_sn": "your-toy-sn",
58
+ "toy_key": "your-toy-key",
59
+ "mqtt_host": "198.19.249.25"
60
+ }
61
+ }
62
+ }
63
+ ```
49
64
 
50
65
  ## MQTT
51
66
 
52
- Both inbound and outbound messages use the same topic:
67
+ Inbound and outbound use separate topics:
53
68
 
54
69
  ```
55
- /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
56
72
  ```
57
73
 
58
- The plugin connects to the MQTT broker with an `openapi:` prefix on the username to distinguish itself from the toy's own connection:
74
+ The plugin connects with an `openapi:` prefix on the username to distinguish itself from the toy's own connection:
59
75
 
60
76
  ```
61
77
  username: openapi:{toy_sn}
@@ -78,8 +94,6 @@ password: {toy_key}
78
94
 
79
95
  **Plugin → Toy (outbound)**
80
96
 
81
- Single response message with `msgId` matching the inbound request:
82
-
83
97
  ```json
84
98
  {
85
99
  "msgId": 1,
@@ -90,20 +104,26 @@ Single response message with `msgId` matching the inbound request:
90
104
  }
91
105
  ```
92
106
 
107
+ `msgId` starts at 1 per session and auto-increments.
108
+
93
109
  ## Environments
94
110
 
95
111
  | Environment | MQTT Host | Port |
96
112
  |-------------|-----------|------|
97
- | Development | `192.168.10.138` | 1883 |
113
+ | Development | `198.19.249.25` | 1883 |
98
114
  | Testing | `f.qrc92.cn` | 1883 |
99
115
  | Production | `f.folotoy.cn` | 1883 |
100
116
 
101
- Switch environments via the `FOLOTOY_MQTT_HOST` environment variable.
117
+ Switch environments via the `FOLOTOY_MQTT_HOST` environment variable or `mqtt_host` config field.
102
118
 
103
119
  ## Development
104
120
 
105
121
  ```bash
106
122
  npm install
107
- npm run build
108
123
  npm test
124
+ npm run build
109
125
  ```
126
+
127
+ ## License
128
+
129
+ MIT
@@ -1,5 +1,52 @@
1
1
  {
2
- "id": "folotoy",
3
- "type": "channel",
4
- "channels": ["folotoy"]
2
+ "id": "folotoy-openclaw-plugin",
3
+ "name": "FoloToy",
4
+ "description": "Empower your FoloToy with OpenClaw AI capabilities.",
5
+ "version": "0.1.2",
6
+ "channels": ["folotoy"],
7
+ "configSchema": {
8
+ "type": "object",
9
+ "additionalProperties": false,
10
+ "properties": {
11
+ "flow": {
12
+ "type": "string",
13
+ "enum": ["direct", "api"],
14
+ "description": "Authentication flow",
15
+ "default": "direct"
16
+ },
17
+ "toy_sn": {
18
+ "type": "string",
19
+ "description": "Toy serial number"
20
+ },
21
+ "toy_key": {
22
+ "type": "string",
23
+ "description": "Toy key (direct flow)"
24
+ },
25
+ "api_url": {
26
+ "type": "string",
27
+ "description": "FoloToy API base URL (api flow)"
28
+ },
29
+ "api_key": {
30
+ "type": "string",
31
+ "description": "FoloToy API key (api flow)"
32
+ },
33
+ "mqtt_host": {
34
+ "type": "string",
35
+ "description": "MQTT broker host"
36
+ },
37
+ "mqtt_port": {
38
+ "type": "number",
39
+ "description": "MQTT broker port"
40
+ }
41
+ }
42
+ },
43
+ "uiHints": {
44
+ "flow": { "label": "Auth Flow" },
45
+ "toy_sn": { "label": "Toy SN" },
46
+ "toy_key": { "label": "Toy Key", "sensitive": true },
47
+ "api_url": { "label": "API URL", "placeholder": "https://api.folotoy.cn" },
48
+ "api_key": { "label": "API Key", "sensitive": true },
49
+ "mqtt_host": { "label": "MQTT Host", "placeholder": "198.19.249.25" },
50
+ "mqtt_port": { "label": "MQTT Port", "placeholder": "1883" }
51
+ }
5
52
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@folotoy/folotoy-openclaw-plugin",
3
- "version": "0.1.0",
4
- "description": "OpenClaw channel plugin for FoloToy smart toys via MQTT",
3
+ "version": "0.1.2",
4
+ "description": "Empower your FoloToy with OpenClaw AI capabilities.",
5
5
  "keywords": [
6
6
  "folotoy",
7
7
  "openclaw"
@@ -55,7 +55,7 @@
55
55
  "label": "FoloToy",
56
56
  "selectionLabel": "FoloToy",
57
57
  "docsPath": "/channels/folotoy",
58
- "blurb": "Connect FoloToy smart toys via MQTT."
58
+ "blurb": "Empower your FoloToy with OpenClaw AI capabilities."
59
59
  }
60
60
  }
61
61
  }
@@ -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/config.ts CHANGED
@@ -19,5 +19,32 @@ export type PluginConfig = {
19
19
  }
20
20
  }
21
21
 
22
+ /** Flat config as stored in openclaw.json channels.folotoy */
23
+ export type FlatChannelConfig = {
24
+ flow?: string
25
+ toy_sn?: string
26
+ toy_key?: string
27
+ api_url?: string
28
+ api_key?: string
29
+ mqtt_host?: string
30
+ mqtt_port?: number
31
+ }
32
+
33
+ export const DEFAULT_API_URL = 'https://api.folotoy.cn'
22
34
  export const DEFAULT_MQTT_HOST = process.env.FOLOTOY_MQTT_HOST ?? '198.19.249.25'
23
35
  export const DEFAULT_MQTT_PORT = 1883
36
+
37
+ export function flatToPluginConfig(flat: FlatChannelConfig): PluginConfig {
38
+ const flow = flat.flow ?? 'direct'
39
+ const auth = flow === 'api'
40
+ ? { flow: 'api' as const, api_url: flat.api_url ?? DEFAULT_API_URL, api_key: flat.api_key ?? '', toy_sn: flat.toy_sn ?? '' }
41
+ : { flow: 'direct' as const, toy_sn: flat.toy_sn ?? '', toy_key: flat.toy_key ?? '' }
42
+
43
+ return {
44
+ auth,
45
+ mqtt: {
46
+ host: flat.mqtt_host ?? DEFAULT_MQTT_HOST,
47
+ port: flat.mqtt_port ?? DEFAULT_MQTT_PORT,
48
+ },
49
+ }
50
+ }
package/src/index.ts CHANGED
@@ -1,11 +1,9 @@
1
1
  import type { OpenClawPluginApi, ChannelPlugin } from 'openclaw/plugin-sdk/core'
2
- import { resolveCredentials, createMqttClient, buildTopic } from './mqtt.js'
3
- import { DEFAULT_MQTT_HOST, DEFAULT_MQTT_PORT } from './config.js'
4
- import type { PluginConfig } from './config.js'
2
+ import { resolveCredentials, createMqttClient, buildInboundTopic, buildOutboundTopic } from './mqtt.js'
3
+ import { DEFAULT_MQTT_HOST, DEFAULT_MQTT_PORT, flatToPluginConfig } from './config.js'
4
+ import type { FlatChannelConfig } from './config.js'
5
5
  import type { MqttClient } from 'mqtt'
6
6
 
7
- type FoloToyAccount = PluginConfig
8
-
9
7
  type InboundMessage = {
10
8
  msgId: number
11
9
  identifier: 'chat_input'
@@ -18,17 +16,17 @@ type OutboundMessage = {
18
16
  outParams: { content: string }
19
17
  }
20
18
 
21
- // Per-account MQTT clients, kept for proactive outbound sends
22
- const activeClients = new Map<string, { client: MqttClient; toy_sn: string }>()
19
+ // Per-account MQTT clients and msgId counters
20
+ const activeClients = new Map<string, { client: MqttClient; toy_sn: string; nextMsgId: number }>()
23
21
 
24
- const folotoyChannel: ChannelPlugin<FoloToyAccount> = {
22
+ const folotoyChannel: ChannelPlugin<FlatChannelConfig> = {
25
23
  id: 'folotoy',
26
24
  meta: {
27
25
  id: 'folotoy',
28
26
  label: 'FoloToy',
29
27
  selectionLabel: 'FoloToy',
30
28
  docsPath: '/channels/folotoy',
31
- blurb: 'Connect FoloToy smart toys via MQTT.',
29
+ blurb: 'Empower your FoloToy with OpenClaw AI capabilities.',
32
30
  },
33
31
  capabilities: {
34
32
  chatTypes: ['direct'],
@@ -37,60 +35,35 @@ const folotoyChannel: ChannelPlugin<FoloToyAccount> = {
37
35
  schema: {
38
36
  type: 'object',
39
37
  properties: {
40
- auth: {
41
- type: 'object',
42
- oneOf: [
43
- {
44
- title: 'Flow 2: SN + Key (Default)',
45
- properties: {
46
- flow: { type: 'string', const: 'direct' },
47
- toy_sn: { type: 'string' },
48
- toy_key: { type: 'string' },
49
- },
50
- required: ['flow', 'toy_sn', 'toy_key'],
51
- },
52
- {
53
- title: 'Flow 1: HTTP API Login',
54
- properties: {
55
- flow: { type: 'string', const: 'api' },
56
- api_url: { type: 'string' },
57
- api_key: { type: 'string' },
58
- toy_sn: { type: 'string' },
59
- },
60
- required: ['flow', 'api_url', 'api_key', 'toy_sn'],
61
- },
62
- ],
63
- default: { flow: 'direct' },
64
- },
65
- mqtt: {
66
- type: 'object',
67
- properties: {
68
- host: { type: 'string', default: DEFAULT_MQTT_HOST },
69
- port: { type: 'number', default: DEFAULT_MQTT_PORT },
70
- },
71
- },
38
+ flow: { type: 'string', enum: ['direct', 'api'], default: 'direct' },
39
+ toy_sn: { type: 'string' },
40
+ toy_key: { type: 'string' },
41
+ api_url: { type: 'string', default: 'https://api.folotoy.cn' },
42
+ api_key: { type: 'string' },
43
+ mqtt_host: { type: 'string', default: DEFAULT_MQTT_HOST },
44
+ mqtt_port: { type: 'number', default: DEFAULT_MQTT_PORT },
72
45
  },
73
- required: ['auth'],
74
46
  },
75
47
  uiHints: {
76
- 'auth.toy_sn': { label: 'Toy SN' },
77
- 'auth.toy_key': { label: 'Toy Key', sensitive: true },
78
- 'auth.api_url': { label: 'API URL', placeholder: 'https://api.folotoy.com' },
79
- 'auth.api_key': { label: 'API Key', sensitive: true },
80
- 'mqtt.host': { label: 'MQTT Host', placeholder: DEFAULT_MQTT_HOST },
81
- 'mqtt.port': { label: 'MQTT Port' },
48
+ flow: { label: 'Auth Flow' },
49
+ toy_sn: { label: 'Toy SN' },
50
+ toy_key: { label: 'Toy Key', sensitive: true },
51
+ api_url: { label: 'API URL', placeholder: 'https://api.folotoy.com' },
52
+ api_key: { label: 'API Key', sensitive: true },
53
+ mqtt_host: { label: 'MQTT Host', placeholder: DEFAULT_MQTT_HOST },
54
+ mqtt_port: { label: 'MQTT Port' },
82
55
  },
83
56
  },
84
57
  config: {
85
58
  listAccountIds: (cfg) => {
86
- const accounts = (cfg as Record<string, unknown> & { channels?: { folotoy?: { accounts?: Record<string, unknown> } } })
87
- .channels?.folotoy?.accounts ?? {}
88
- return Object.keys(accounts)
59
+ const folotoy = (cfg as Record<string, unknown> & { channels?: { folotoy?: FlatChannelConfig } })
60
+ .channels?.folotoy
61
+ return folotoy ? ['default'] : []
89
62
  },
90
- resolveAccount: (cfg, accountId) => {
91
- const accounts = (cfg as Record<string, unknown> & { channels?: { folotoy?: { accounts?: Record<string, FoloToyAccount> } } })
92
- .channels?.folotoy?.accounts ?? {}
93
- return accounts[accountId ?? 'default'] ?? ({} as FoloToyAccount)
63
+ resolveAccount: (cfg, _accountId) => {
64
+ const folotoy = (cfg as Record<string, unknown> & { channels?: { folotoy?: FlatChannelConfig } })
65
+ .channels?.folotoy
66
+ return folotoy ?? ({} as FlatChannelConfig)
94
67
  },
95
68
  },
96
69
  gateway: {
@@ -102,22 +75,21 @@ const folotoyChannel: ChannelPlugin<FoloToyAccount> = {
102
75
  return
103
76
  }
104
77
 
105
- const mqttConfig: PluginConfig = {
106
- auth: account.auth,
107
- mqtt: {
108
- host: account.mqtt?.host ?? DEFAULT_MQTT_HOST,
109
- port: account.mqtt?.port ?? DEFAULT_MQTT_PORT,
110
- },
78
+ if (!account.toy_sn) {
79
+ log?.warn?.('toy_sn not configured — skipping MQTT connection')
80
+ return
111
81
  }
112
82
 
83
+ const mqttConfig = flatToPluginConfig(account)
113
84
  const credentials = await resolveCredentials(mqttConfig)
114
85
  const client = await createMqttClient(mqttConfig, credentials)
115
- const topic = buildTopic(credentials.toy_sn)
86
+ const inboundTopic = buildInboundTopic(credentials.toy_sn)
87
+ const outboundTopic = buildOutboundTopic(credentials.toy_sn)
116
88
 
117
- activeClients.set(accountId, { client, toy_sn: credentials.toy_sn })
118
- log?.info?.(`Connected to MQTT broker, subscribed to ${topic}`)
89
+ activeClients.set(accountId, { client, toy_sn: credentials.toy_sn, nextMsgId: 1 })
90
+ log?.info?.(`Connected to MQTT broker, subscribed to ${inboundTopic}`)
119
91
 
120
- client.subscribe(topic, (err) => {
92
+ client.subscribe(inboundTopic, (err) => {
121
93
  if (err) log?.error?.(`Failed to subscribe: ${err.message}`)
122
94
  })
123
95
 
@@ -153,17 +125,21 @@ const folotoyChannel: ChannelPlugin<FoloToyAccount> = {
153
125
  identifier: 'chat_output',
154
126
  outParams: { content: replyPayload.text },
155
127
  }
156
- client.publish(topic, JSON.stringify(outMsg))
128
+ client.publish(outboundTopic, JSON.stringify(outMsg))
157
129
  },
158
130
  onError: (err) => log?.error?.(`Dispatch error: ${String(err)}`),
159
131
  },
160
132
  })
161
133
  })
162
134
 
163
- abortSignal.addEventListener('abort', () => {
164
- activeClients.delete(accountId)
165
- client.end()
166
- log?.info?.('MQTT client disconnected')
135
+ // Keep the account alive until aborted
136
+ return new Promise<void>((resolve) => {
137
+ abortSignal.addEventListener('abort', () => {
138
+ activeClients.delete(accountId)
139
+ client.end()
140
+ log?.info?.('MQTT client disconnected')
141
+ resolve()
142
+ })
167
143
  })
168
144
  },
169
145
 
@@ -179,14 +155,14 @@ const folotoyChannel: ChannelPlugin<FoloToyAccount> = {
179
155
  const entry = activeClients.get(key)
180
156
  if (!entry) throw new Error(`No active MQTT client for account "${key}"`)
181
157
 
182
- const topic = buildTopic(entry.toy_sn)
183
- const msgId = Date.now()
158
+ const outboundTopic = buildOutboundTopic(entry.toy_sn)
159
+ const msgId = entry.nextMsgId++
184
160
  const outMsg: OutboundMessage = {
185
161
  msgId,
186
162
  identifier: 'chat_output',
187
163
  outParams: { content: text },
188
164
  }
189
- entry.client.publish(topic, JSON.stringify(outMsg))
165
+ entry.client.publish(outboundTopic, JSON.stringify(outMsg))
190
166
  return { channel: 'folotoy', messageId: String(msgId) }
191
167
  },
192
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> {