@folotoy/folotoy-openclaw-plugin 0.1.0 → 0.1.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
@@ -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,12 +40,27 @@ 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
 
@@ -55,7 +70,7 @@ Both inbound and outbound messages use the same topic:
55
70
  /openapi/folotoy/{sn}/thing/data/post
56
71
  ```
57
72
 
58
- The plugin connects to the MQTT broker with an `openapi:` prefix on the username to distinguish itself from the toy's own connection:
73
+ The plugin connects with an `openapi:` prefix on the username to distinguish itself from the toy's own connection:
59
74
 
60
75
  ```
61
76
  username: openapi:{toy_sn}
@@ -78,8 +93,6 @@ password: {toy_key}
78
93
 
79
94
  **Plugin → Toy (outbound)**
80
95
 
81
- Single response message with `msgId` matching the inbound request:
82
-
83
96
  ```json
84
97
  {
85
98
  "msgId": 1,
@@ -90,20 +103,26 @@ Single response message with `msgId` matching the inbound request:
90
103
  }
91
104
  ```
92
105
 
106
+ `msgId` starts at 1 per session and auto-increments.
107
+
93
108
  ## Environments
94
109
 
95
110
  | Environment | MQTT Host | Port |
96
111
  |-------------|-----------|------|
97
- | Development | `192.168.10.138` | 1883 |
112
+ | Development | `198.19.249.25` | 1883 |
98
113
  | Testing | `f.qrc92.cn` | 1883 |
99
114
  | Production | `f.folotoy.cn` | 1883 |
100
115
 
101
- Switch environments via the `FOLOTOY_MQTT_HOST` environment variable.
116
+ Switch environments via the `FOLOTOY_MQTT_HOST` environment variable or `mqtt_host` config field.
102
117
 
103
118
  ## Development
104
119
 
105
120
  ```bash
106
121
  npm install
107
- npm run build
108
122
  npm test
123
+ npm run build
109
124
  ```
125
+
126
+ ## License
127
+
128
+ 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.0",
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.1",
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
  }
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
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'
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,19 +75,17 @@ 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
86
  const topic = buildTopic(credentials.toy_sn)
116
87
 
117
- activeClients.set(accountId, { client, toy_sn: credentials.toy_sn })
88
+ activeClients.set(accountId, { client, toy_sn: credentials.toy_sn, nextMsgId: 1 })
118
89
  log?.info?.(`Connected to MQTT broker, subscribed to ${topic}`)
119
90
 
120
91
  client.subscribe(topic, (err) => {
@@ -160,10 +131,14 @@ const folotoyChannel: ChannelPlugin<FoloToyAccount> = {
160
131
  })
161
132
  })
162
133
 
163
- abortSignal.addEventListener('abort', () => {
164
- activeClients.delete(accountId)
165
- client.end()
166
- log?.info?.('MQTT client disconnected')
134
+ // Keep the account alive until aborted
135
+ return new Promise<void>((resolve) => {
136
+ abortSignal.addEventListener('abort', () => {
137
+ activeClients.delete(accountId)
138
+ client.end()
139
+ log?.info?.('MQTT client disconnected')
140
+ resolve()
141
+ })
167
142
  })
168
143
  },
169
144
 
@@ -180,7 +155,7 @@ const folotoyChannel: ChannelPlugin<FoloToyAccount> = {
180
155
  if (!entry) throw new Error(`No active MQTT client for account "${key}"`)
181
156
 
182
157
  const topic = buildTopic(entry.toy_sn)
183
- const msgId = Date.now()
158
+ const msgId = entry.nextMsgId++
184
159
  const outMsg: OutboundMessage = {
185
160
  msgId,
186
161
  identifier: 'chat_output',