@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 +42 -23
- package/openclaw.plugin.json +50 -3
- package/package.json +3 -3
- package/src/config.ts +27 -0
- package/src/index.ts +41 -66
package/README.md
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# @folotoy/folotoy-openclaw-plugin
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
| `
|
|
32
|
-
| `
|
|
33
|
-
| `
|
|
34
|
-
| `
|
|
35
|
-
| `
|
|
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
|
-
| `
|
|
44
|
-
| `
|
|
45
|
-
| `
|
|
46
|
-
| `
|
|
47
|
-
| `
|
|
48
|
-
| `
|
|
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
|
|
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 | `
|
|
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
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
{
|
|
2
|
-
"id": "folotoy",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
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.
|
|
4
|
-
"description": "
|
|
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": "
|
|
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 {
|
|
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
|
|
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<
|
|
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: '
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
87
|
-
.channels?.folotoy
|
|
88
|
-
return
|
|
59
|
+
const folotoy = (cfg as Record<string, unknown> & { channels?: { folotoy?: FlatChannelConfig } })
|
|
60
|
+
.channels?.folotoy
|
|
61
|
+
return folotoy ? ['default'] : []
|
|
89
62
|
},
|
|
90
|
-
resolveAccount: (cfg,
|
|
91
|
-
const
|
|
92
|
-
.channels?.folotoy
|
|
93
|
-
return
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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 =
|
|
158
|
+
const msgId = entry.nextMsgId++
|
|
184
159
|
const outMsg: OutboundMessage = {
|
|
185
160
|
msgId,
|
|
186
161
|
identifier: 'chat_output',
|