@folotoy/folotoy-openclaw-plugin 0.3.1 → 0.4.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.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import('../dist/cli/install.js')
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=install.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../src/cli/install.ts"],"names":[],"mappings":""}
@@ -0,0 +1,113 @@
1
+ import { execSync } from 'node:child_process';
2
+ import qrcode from 'qrcode-terminal';
3
+ import { DEFAULT_MQTT_HOST, DEFAULT_MQTT_PORT } from '../config.js';
4
+ const PAIR_API_BASE = process.env.PAIR_API_BASE ?? 'https://pair.folotoy.cn';
5
+ const POLL_INTERVAL_MS = 3000;
6
+ const POLL_TIMEOUT_MS = 300_000; // 5 minutes
7
+ // ── Helpers ────────────────────────────────────────────
8
+ function checkOpenClaw() {
9
+ try {
10
+ execSync('openclaw --version', { stdio: 'pipe' });
11
+ }
12
+ catch {
13
+ console.error('Error: openclaw is not installed or not in PATH.');
14
+ console.error('Install it first: npm i -g openclaw');
15
+ process.exit(1);
16
+ }
17
+ }
18
+ function installPlugin() {
19
+ try {
20
+ const list = execSync('openclaw plugins list', { stdio: 'pipe' }).toString();
21
+ if (list.includes('folotoy-openclaw-plugin')) {
22
+ return; // already installed
23
+ }
24
+ }
25
+ catch {
26
+ // ignore
27
+ }
28
+ console.log('Installing FoloToy plugin...');
29
+ execSync('openclaw plugins install @folotoy/folotoy-openclaw-plugin', { stdio: 'inherit' });
30
+ }
31
+ async function createSession() {
32
+ const res = await fetch(`${PAIR_API_BASE}/api/pair`, { method: 'POST' });
33
+ if (!res.ok)
34
+ throw new Error(`Failed to create pairing session (HTTP ${res.status})`);
35
+ return res.json();
36
+ }
37
+ function displayQR(url) {
38
+ qrcode.generate(url, { small: true }, (qr) => {
39
+ console.log(qr);
40
+ });
41
+ console.log(`Or open this URL on your phone: ${url}\n`);
42
+ }
43
+ async function sleep(ms) {
44
+ return new Promise((r) => setTimeout(r, ms));
45
+ }
46
+ async function pollSession(sessionId) {
47
+ const deadline = Date.now() + POLL_TIMEOUT_MS;
48
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
49
+ let i = 0;
50
+ while (Date.now() < deadline) {
51
+ process.stdout.write(`\r${frames[i++ % frames.length]} Waiting for pairing...`);
52
+ const res = await fetch(`${PAIR_API_BASE}/api/pair/${sessionId}`);
53
+ if (!res.ok)
54
+ throw new Error(`Poll failed (HTTP ${res.status})`);
55
+ const data = (await res.json());
56
+ if (data.status === 'completed') {
57
+ process.stdout.write('\r\x1b[32m✓\x1b[0m Paired successfully! \n');
58
+ return data;
59
+ }
60
+ if (data.status === 'expired') {
61
+ process.stdout.write('\r');
62
+ throw new Error('Pairing session expired. Please try again.');
63
+ }
64
+ await sleep(POLL_INTERVAL_MS);
65
+ }
66
+ throw new Error('Pairing timed out after 5 minutes.');
67
+ }
68
+ function writeConfig(result) {
69
+ execSync(`openclaw config set channels.folotoy.flow direct`, { stdio: 'pipe' });
70
+ execSync(`openclaw config set channels.folotoy.toy_sn ${result.toy_sn}`, { stdio: 'pipe' });
71
+ execSync(`openclaw config set channels.folotoy.toy_key ${result.toy_key}`, { stdio: 'pipe' });
72
+ const mqttHost = result.mqtt_host ?? DEFAULT_MQTT_HOST;
73
+ const mqttPort = result.mqtt_port ?? DEFAULT_MQTT_PORT;
74
+ execSync(`openclaw config set channels.folotoy.mqtt_host ${mqttHost}`, { stdio: 'pipe' });
75
+ execSync(`openclaw config set channels.folotoy.mqtt_port ${mqttPort}`, { stdio: 'pipe' });
76
+ }
77
+ // ── Main ───────────────────────────────────────────────
78
+ async function main() {
79
+ const command = process.argv[2];
80
+ if (command !== 'install') {
81
+ console.log('Usage: npx @folotoy/folotoy-openclaw-plugin install');
82
+ process.exit(command ? 1 : 0);
83
+ }
84
+ console.log('🧸 FoloToy OpenClaw Plugin Installer\n');
85
+ // Step 1: check prerequisites
86
+ console.log('Checking openclaw...');
87
+ checkOpenClaw();
88
+ console.log('✓ openclaw found\n');
89
+ // Step 2: install plugin if not present
90
+ installPlugin();
91
+ // Step 3: create pairing session
92
+ console.log('Creating pairing session...\n');
93
+ const session = await createSession();
94
+ // Step 4: display QR code
95
+ console.log('Scan this QR code with your phone,');
96
+ console.log('then scan your toy\'s QR code on the phone:\n');
97
+ displayQR(session.pair_url);
98
+ // Step 5: poll for result
99
+ const result = await pollSession(session.session_id);
100
+ // Step 6: write config
101
+ console.log('\nWriting configuration...');
102
+ writeConfig(result);
103
+ // Step 7: done
104
+ console.log('\n\x1b[32m✓ FoloToy plugin installed and configured!\x1b[0m');
105
+ console.log(` Toy SN: ${result.toy_sn}`);
106
+ console.log(` MQTT Host: ${result.mqtt_host ?? DEFAULT_MQTT_HOST}`);
107
+ console.log('\nRestart the gateway to apply: openclaw gateway start --force');
108
+ }
109
+ main().catch((err) => {
110
+ console.error(`\n\x1b[31mError:\x1b[0m ${err instanceof Error ? err.message : String(err)}`);
111
+ process.exit(1);
112
+ });
113
+ //# sourceMappingURL=install.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install.js","sourceRoot":"","sources":["../../src/cli/install.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ export type AuthFlow1Config = {
2
+ flow: 'api';
3
+ api_url: string;
4
+ api_key: string;
5
+ toy_sn: string;
6
+ };
7
+ export type AuthFlow2Config = {
8
+ flow: 'direct';
9
+ toy_sn: string;
10
+ toy_key: string;
11
+ };
12
+ export type PluginConfig = {
13
+ auth: AuthFlow1Config | AuthFlow2Config;
14
+ mqtt: {
15
+ host: string;
16
+ port: number;
17
+ };
18
+ };
19
+ /** Flat config as stored in openclaw.json channels.folotoy */
20
+ export type FlatChannelConfig = {
21
+ flow?: string;
22
+ toy_sn?: string;
23
+ toy_key?: string;
24
+ api_url?: string;
25
+ api_key?: string;
26
+ mqtt_host?: string;
27
+ mqtt_port?: number;
28
+ };
29
+ export declare const DEFAULT_API_URL = "https://api.folotoy.cn";
30
+ export declare const DEFAULT_MQTT_HOST: string;
31
+ export declare const DEFAULT_MQTT_PORT = 1883;
32
+ export declare function flatToPluginConfig(flat: FlatChannelConfig): PluginConfig;
33
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,KAAK,CAAA;IACX,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;CACf,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,QAAQ,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,eAAe,GAAG,eAAe,CAAA;IACvC,IAAI,EAAE;QACJ,IAAI,EAAE,MAAM,CAAA;QACZ,IAAI,EAAE,MAAM,CAAA;KACb,CAAA;CACF,CAAA;AAED,8DAA8D;AAC9D,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,eAAO,MAAM,eAAe,2BAA2B,CAAA;AACvD,eAAO,MAAM,iBAAiB,QAAgD,CAAA;AAC9E,eAAO,MAAM,iBAAiB,OAAO,CAAA;AAErC,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,iBAAiB,GAAG,YAAY,CAaxE"}
package/dist/config.js ADDED
@@ -0,0 +1,17 @@
1
+ export const DEFAULT_API_URL = 'https://api.folotoy.cn';
2
+ export const DEFAULT_MQTT_HOST = process.env.FOLOTOY_MQTT_HOST ?? 'f.qrc92.cn';
3
+ export const DEFAULT_MQTT_PORT = 1883;
4
+ export function flatToPluginConfig(flat) {
5
+ const flow = flat.flow ?? 'direct';
6
+ const auth = flow === 'api'
7
+ ? { flow: 'api', api_url: flat.api_url ?? DEFAULT_API_URL, api_key: flat.api_key ?? '', toy_sn: flat.toy_sn ?? '' }
8
+ : { flow: 'direct', toy_sn: flat.toy_sn ?? '', toy_key: flat.toy_key ?? '' };
9
+ return {
10
+ auth,
11
+ mqtt: {
12
+ host: flat.mqtt_host ?? DEFAULT_MQTT_HOST,
13
+ port: flat.mqtt_port ?? DEFAULT_MQTT_PORT,
14
+ },
15
+ };
16
+ }
17
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAgCA,MAAM,CAAC,MAAM,eAAe,GAAG,wBAAwB,CAAA;AACvD,MAAM,CAAC,MAAM,iBAAiB,GAAG,OAAO,CAAC,GAAG,CAAC,iBAAiB,IAAI,YAAY,CAAA;AAC9E,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,CAAA;AAErC,MAAM,UAAU,kBAAkB,CAAC,IAAuB;IACxD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,QAAQ,CAAA;IAClC,MAAM,IAAI,GAAG,IAAI,KAAK,KAAK;QACzB,CAAC,CAAC,EAAE,IAAI,EAAE,KAAc,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,eAAe,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,EAAE,EAAE;QAC5H,CAAC,CAAC,EAAE,IAAI,EAAE,QAAiB,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,EAAE,EAAE,CAAA;IAEvF,OAAO;QACL,IAAI;QACJ,IAAI,EAAE;YACJ,IAAI,EAAE,IAAI,CAAC,SAAS,IAAI,iBAAiB;YACzC,IAAI,EAAE,IAAI,CAAC,SAAS,IAAI,iBAAiB;SAC1C;KACF,CAAA;AACH,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { OpenClawPluginApi } from 'openclaw/plugin-sdk/core';
2
+ export declare function sendNotification({ text, accountId }: {
3
+ text: string;
4
+ accountId?: string;
5
+ }): {
6
+ channel: string;
7
+ messageId: string;
8
+ };
9
+ declare const _default: (api: OpenClawPluginApi) => void;
10
+ export default _default;
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAiB,MAAM,0BAA0B,CAAA;AAgOhF,wBAAgB,gBAAgB,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE;;;EAczF;yBAEe,KAAK,iBAAiB;AAAtC,wBAEC"}
package/dist/index.js ADDED
@@ -0,0 +1,208 @@
1
+ import { resolveCredentials, createMqttClient, buildInboundTopic, buildOutboundTopic, buildNotificationTopic } from './mqtt.js';
2
+ import { DEFAULT_MQTT_HOST, DEFAULT_MQTT_PORT, flatToPluginConfig } from './config.js';
3
+ /** Pick a soothing acknowledgment that loosely matches the input. */
4
+ function pickSoothingReply(text) {
5
+ const t = text.toLowerCase();
6
+ if (/难过|伤心|哭|不开心|sad|upset|cry/.test(t))
7
+ return '抱抱你,我在听呢,让我想想怎么帮你。';
8
+ if (/害怕|恐惧|怕|scared|afraid/.test(t))
9
+ return '别怕,有我在呢,让我想一想。';
10
+ if (/生气|愤怒|烦|angry|mad/.test(t))
11
+ return '我理解你的感受,让我来帮你想想办法。';
12
+ if (/累|疲|困|tired|exhausted/.test(t))
13
+ return '辛苦了,休息一下,我来帮你想。';
14
+ if (/无聊|没意思|bored/.test(t))
15
+ return '我来陪你聊聊吧,让我想想。';
16
+ if (/谢|感谢|thank/.test(t))
17
+ return '不客气呀,让我想想还能帮你什么。';
18
+ if (/你好|嗨|hello|hi|hey/.test(t))
19
+ return '你好呀!让我想想怎么回答你。';
20
+ if (/帮|help|怎么办/.test(t))
21
+ return '没问题,让我帮你想想办法。';
22
+ return '好的,让我想一想,马上回复你。';
23
+ }
24
+ // Per-account MQTT clients and msgId counters
25
+ const activeClients = new Map();
26
+ const folotoyChannel = {
27
+ id: 'folotoy',
28
+ meta: {
29
+ id: 'folotoy',
30
+ label: 'FoloToy',
31
+ selectionLabel: 'FoloToy',
32
+ docsPath: '/channels/folotoy',
33
+ blurb: 'Empower your FoloToy with OpenClaw AI capabilities.',
34
+ },
35
+ capabilities: {
36
+ chatTypes: ['direct'],
37
+ },
38
+ configSchema: {
39
+ schema: {
40
+ type: 'object',
41
+ properties: {
42
+ flow: { type: 'string', enum: ['direct', 'api'], default: 'direct' },
43
+ toy_sn: { type: 'string' },
44
+ toy_key: { type: 'string' },
45
+ api_url: { type: 'string', default: 'https://api.folotoy.cn' },
46
+ api_key: { type: 'string' },
47
+ mqtt_host: { type: 'string', default: DEFAULT_MQTT_HOST },
48
+ mqtt_port: { type: 'number', default: DEFAULT_MQTT_PORT },
49
+ },
50
+ },
51
+ uiHints: {
52
+ flow: { label: 'Auth Flow' },
53
+ toy_sn: { label: 'Toy SN' },
54
+ toy_key: { label: 'Toy Key', sensitive: true },
55
+ api_url: { label: 'API URL', placeholder: 'https://api.folotoy.com' },
56
+ api_key: { label: 'API Key', sensitive: true },
57
+ mqtt_host: { label: 'MQTT Host', placeholder: DEFAULT_MQTT_HOST },
58
+ mqtt_port: { label: 'MQTT Port' },
59
+ },
60
+ },
61
+ config: {
62
+ listAccountIds: (cfg) => {
63
+ const folotoy = cfg
64
+ .channels?.folotoy;
65
+ return folotoy ? ['default'] : [];
66
+ },
67
+ resolveAccount: (cfg, _accountId) => {
68
+ const folotoy = cfg
69
+ .channels?.folotoy;
70
+ return folotoy ?? {};
71
+ },
72
+ },
73
+ gateway: {
74
+ startAccount: async (ctx) => {
75
+ const { account, cfg, accountId, abortSignal, channelRuntime, log } = ctx;
76
+ if (!channelRuntime) {
77
+ log?.warn?.('channelRuntime not available — skipping MQTT connection');
78
+ return;
79
+ }
80
+ if (!account.toy_sn) {
81
+ log?.warn?.('toy_sn not configured — skipping MQTT connection');
82
+ return;
83
+ }
84
+ const mqttConfig = flatToPluginConfig(account);
85
+ log?.info?.(`Connecting to MQTT broker ${mqttConfig.mqtt.host}:${mqttConfig.mqtt.port}...`);
86
+ const credentials = await resolveCredentials(mqttConfig);
87
+ const client = await createMqttClient(mqttConfig, credentials);
88
+ const inboundTopic = buildInboundTopic(credentials.toy_sn);
89
+ const outboundTopic = buildOutboundTopic(credentials.toy_sn);
90
+ activeClients.set(accountId, { client, toy_sn: credentials.toy_sn, nextMsgId: 1 });
91
+ log?.info?.(`Connected to MQTT broker, subscribed to ${inboundTopic}`);
92
+ client.subscribe(inboundTopic, (err) => {
93
+ if (err)
94
+ log?.error?.(`Failed to subscribe: ${err.message}`);
95
+ });
96
+ client.on('message', (_topic, payload) => {
97
+ let msg;
98
+ try {
99
+ msg = JSON.parse(payload.toString());
100
+ }
101
+ catch {
102
+ return;
103
+ }
104
+ if (msg.identifier !== 'chat_input' || typeof msg.inputParams?.text !== 'string')
105
+ return;
106
+ const { msgId, inputParams: { text, recording_id } } = msg;
107
+ let order = 1;
108
+ // Send a quick soothing acknowledgment before AI processing (order=1).
109
+ // AI replies continue from order=2.
110
+ const ackMsg = {
111
+ msgId,
112
+ identifier: 'chat_output',
113
+ outParams: { content: pickSoothingReply(text), recording_id, order, is_finished: false },
114
+ };
115
+ client.publish(outboundTopic, JSON.stringify(ackMsg));
116
+ const inboundCtx = channelRuntime.reply.finalizeInboundContext({
117
+ Body: text,
118
+ From: credentials.toy_sn,
119
+ To: credentials.toy_sn,
120
+ SessionKey: `folotoy-${accountId}-${credentials.toy_sn}`,
121
+ AccountId: accountId,
122
+ Provider: 'folotoy',
123
+ });
124
+ // dispatch and send finish message when done
125
+ void (async () => {
126
+ try {
127
+ await channelRuntime.reply.dispatchReplyWithBufferedBlockDispatcher({
128
+ ctx: inboundCtx,
129
+ cfg,
130
+ dispatcherOptions: {
131
+ deliver: async (replyPayload) => {
132
+ if (!replyPayload.text)
133
+ return;
134
+ order++;
135
+ const outMsg = {
136
+ msgId,
137
+ identifier: 'chat_output',
138
+ outParams: { content: replyPayload.text, recording_id, order, is_finished: false },
139
+ };
140
+ client.publish(outboundTopic, JSON.stringify(outMsg));
141
+ },
142
+ onError: (err) => log?.error?.(`Dispatch error: ${String(err)}`),
143
+ },
144
+ });
145
+ }
146
+ finally {
147
+ order++;
148
+ const finishMsg = {
149
+ msgId,
150
+ identifier: 'chat_output',
151
+ outParams: { content: '', recording_id, order, is_finished: true },
152
+ };
153
+ client.publish(outboundTopic, JSON.stringify(finishMsg));
154
+ }
155
+ })();
156
+ });
157
+ // Keep the account alive until aborted
158
+ return new Promise((resolve) => {
159
+ abortSignal.addEventListener('abort', () => {
160
+ activeClients.delete(accountId);
161
+ client.end();
162
+ log?.info?.('MQTT client disconnected');
163
+ resolve();
164
+ });
165
+ });
166
+ },
167
+ stopAccount: async (_ctx) => {
168
+ // cleanup handled by abortSignal listener in startAccount
169
+ },
170
+ },
171
+ outbound: {
172
+ deliveryMode: 'direct',
173
+ sendText: async ({ text, accountId }) => {
174
+ const key = accountId ?? 'default';
175
+ const entry = activeClients.get(key);
176
+ if (!entry)
177
+ throw new Error(`No active MQTT client for account "${key}"`);
178
+ const outboundTopic = buildOutboundTopic(entry.toy_sn);
179
+ const msgId = entry.nextMsgId++;
180
+ const outMsg = {
181
+ msgId,
182
+ identifier: 'chat_output',
183
+ outParams: { content: text },
184
+ };
185
+ entry.client.publish(outboundTopic, JSON.stringify(outMsg));
186
+ return { channel: 'folotoy', messageId: String(msgId) };
187
+ },
188
+ },
189
+ };
190
+ export function sendNotification({ text, accountId }) {
191
+ const key = accountId ?? 'default';
192
+ const entry = activeClients.get(key);
193
+ if (!entry)
194
+ throw new Error(`No active MQTT client for account "${key}"`);
195
+ const notificationTopic = buildNotificationTopic(entry.toy_sn);
196
+ const msgId = entry.nextMsgId++;
197
+ const notifMsg = {
198
+ msgId,
199
+ identifier: 'send_notification',
200
+ outParams: { text },
201
+ };
202
+ entry.client.publish(notificationTopic, JSON.stringify(notifMsg));
203
+ return { channel: 'folotoy', messageId: String(msgId) };
204
+ }
205
+ export default (api) => {
206
+ api.registerChannel({ plugin: folotoyChannel });
207
+ };
208
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/mqtt.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { MqttClient } from 'mqtt';
2
+ import { PluginConfig } from './config.js';
3
+ export type MqttCredentials = {
4
+ username: string;
5
+ password: string;
6
+ toy_sn: string;
7
+ };
8
+ export declare function resolveCredentials(config: PluginConfig): Promise<MqttCredentials>;
9
+ export declare function buildInboundTopic(toy_sn: string): string;
10
+ export declare function buildOutboundTopic(toy_sn: string): string;
11
+ export declare function buildNotificationTopic(toy_sn: string): string;
12
+ export declare function createMqttClient(config: PluginConfig, credentials: MqttCredentials): Promise<MqttClient>;
13
+ //# sourceMappingURL=mqtt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mqtt.d.ts","sourceRoot":"","sources":["../src/mqtt.ts"],"names":[],"mappings":"AAAA,OAAa,EAAE,UAAU,EAAE,MAAM,MAAM,CAAA;AACvC,OAAO,EAAoC,YAAY,EAAE,MAAM,aAAa,CAAA;AAE5E,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;CACf,CAAA;AAgCD,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,eAAe,CAAC,CAKvF;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,YAAY,EAAE,WAAW,EAAE,eAAe,GAAG,OAAO,CAAC,UAAU,CAAC,CAgB9G"}
package/dist/mqtt.js ADDED
@@ -0,0 +1,58 @@
1
+ import mqtt from 'mqtt';
2
+ async function fetchCredentials(auth) {
3
+ const res = await fetch(`${auth.api_url}/v1/openapi/create_mqtt_token`, {
4
+ method: 'POST',
5
+ headers: {
6
+ 'Authorization': `Bearer ${auth.api_key}`,
7
+ 'Content-Type': 'application/json',
8
+ },
9
+ body: JSON.stringify({ toy_sn: auth.toy_sn }),
10
+ });
11
+ if (!res.ok) {
12
+ throw new Error(`Failed to fetch MQTT token: ${res.status} ${res.statusText}`);
13
+ }
14
+ const data = await res.json();
15
+ return {
16
+ username: data.username,
17
+ password: data.password,
18
+ toy_sn: auth.toy_sn,
19
+ };
20
+ }
21
+ function directCredentials(auth) {
22
+ return {
23
+ username: auth.toy_sn,
24
+ password: auth.toy_key,
25
+ toy_sn: auth.toy_sn,
26
+ };
27
+ }
28
+ export async function resolveCredentials(config) {
29
+ if (config.auth.flow === 'api') {
30
+ return fetchCredentials(config.auth);
31
+ }
32
+ return directCredentials(config.auth);
33
+ }
34
+ export function buildInboundTopic(toy_sn) {
35
+ return `/openapi/folotoy/${toy_sn}/thing/command/call`;
36
+ }
37
+ export function buildOutboundTopic(toy_sn) {
38
+ return `/openapi/folotoy/${toy_sn}/thing/command/callAck`;
39
+ }
40
+ export function buildNotificationTopic(toy_sn) {
41
+ return `/openapi/folotoy/${toy_sn}/thing/event/post`;
42
+ }
43
+ export async function createMqttClient(config, credentials) {
44
+ const { host, port } = config.mqtt;
45
+ const { username, password } = credentials;
46
+ return new Promise((resolve, reject) => {
47
+ const clientId = `openapi:${username}`;
48
+ const client = mqtt.connect(`mqtt://${host}:${port}`, {
49
+ clientId,
50
+ username: `openapi:${username}`,
51
+ password,
52
+ clean: true,
53
+ });
54
+ client.once('connect', () => resolve(client));
55
+ client.once('error', reject);
56
+ });
57
+ }
58
+ //# sourceMappingURL=mqtt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mqtt.js","sourceRoot":"","sources":["../src/mqtt.ts"],"names":[],"mappings":"AAAA,OAAO,IAAoB,MAAM,MAAM,CAAA;AASvC,KAAK,UAAU,gBAAgB,CAAC,IAAqB;IACnD,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,OAAO,+BAA+B,EAAE;QACtE,MAAM,EAAE,MAAM;QACd,OAAO,EAAE;YACP,eAAe,EAAE,UAAU,IAAI,CAAC,OAAO,EAAE;YACzC,cAAc,EAAE,kBAAkB;SACnC;QACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;KAC9C,CAAC,CAAA;IAEF,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,+BAA+B,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,UAAU,EAAE,CAAC,CAAA;IAChF,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAA4C,CAAA;IACvE,OAAO;QACL,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,MAAM,EAAE,IAAI,CAAC,MAAM;KACpB,CAAA;AACH,CAAC;AAED,SAAS,iBAAiB,CAAC,IAAqB;IAC9C,OAAO;QACL,QAAQ,EAAE,IAAI,CAAC,MAAM;QACrB,QAAQ,EAAE,IAAI,CAAC,OAAO;QACtB,MAAM,EAAE,IAAI,CAAC,MAAM;KACpB,CAAA;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,MAAoB;IAC3D,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC;QAC/B,OAAO,gBAAgB,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IACtC,CAAC;IACD,OAAO,iBAAiB,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;AACvC,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,MAAc;IAC9C,OAAO,oBAAoB,MAAM,qBAAqB,CAAA;AACxD,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,MAAc;IAC/C,OAAO,oBAAoB,MAAM,wBAAwB,CAAA;AAC3D,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,MAAc;IACnD,OAAO,oBAAoB,MAAM,mBAAmB,CAAA;AACtD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,MAAoB,EAAE,WAA4B;IACvF,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,MAAM,CAAC,IAAI,CAAA;IAClC,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,WAAW,CAAA;IAE1C,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,QAAQ,GAAG,WAAW,QAAQ,EAAE,CAAA;QACtC,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,IAAI,IAAI,IAAI,EAAE,EAAE;YACpD,QAAQ;YACR,QAAQ,EAAE,WAAW,QAAQ,EAAE;YAC/B,QAAQ;YACR,KAAK,EAAE,IAAI;SACZ,CAAC,CAAA;QAEF,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAA;QAC7C,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;IAC9B,CAAC,CAAC,CAAA;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@folotoy/folotoy-openclaw-plugin",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "Empower your FoloToy with OpenClaw AI capabilities.",
5
5
  "keywords": [
6
6
  "folotoy",
@@ -22,12 +22,18 @@
22
22
  },
23
23
  "main": "./src/index.ts",
24
24
  "types": "./src/index.ts",
25
+ "bin": {
26
+ "folotoy-openclaw-plugin": "./bin/folotoy.mjs"
27
+ },
25
28
  "files": [
26
29
  "src",
30
+ "bin",
31
+ "dist",
27
32
  "openclaw.plugin.json"
28
33
  ],
29
34
  "scripts": {
30
35
  "build": "tsc",
36
+ "prepublishOnly": "npm run build",
31
37
  "dev": "tsc --watch",
32
38
  "typecheck": "tsc --noEmit",
33
39
  "test": "vitest run",
@@ -35,7 +41,8 @@
35
41
  "clean": "rm -rf dist"
36
42
  },
37
43
  "dependencies": {
38
- "mqtt": "^5.10.1"
44
+ "mqtt": "^5.10.1",
45
+ "qrcode-terminal": "^0.12.0"
39
46
  },
40
47
  "devDependencies": {
41
48
  "@types/node": "^22.13.10",
@@ -0,0 +1,147 @@
1
+ import { execSync } from 'node:child_process'
2
+ import qrcode from 'qrcode-terminal'
3
+ import { DEFAULT_MQTT_HOST, DEFAULT_MQTT_PORT } from '../config.js'
4
+
5
+ const PAIR_API_BASE = process.env.PAIR_API_BASE ?? 'https://pair.folotoy.cn'
6
+ const POLL_INTERVAL_MS = 3000
7
+ const POLL_TIMEOUT_MS = 300_000 // 5 minutes
8
+
9
+ // ── Types ──────────────────────────────────────────────
10
+
11
+ type CreateSessionResponse = {
12
+ session_id: string
13
+ pair_url: string
14
+ expires_at: string
15
+ }
16
+
17
+ type PollResponse =
18
+ | { status: 'pending' }
19
+ | { status: 'completed'; toy_sn: string; toy_key: string; mqtt_host?: string; mqtt_port?: number }
20
+ | { status: 'expired' }
21
+
22
+ // ── Helpers ────────────────────────────────────────────
23
+
24
+ function checkOpenClaw(): void {
25
+ try {
26
+ execSync('openclaw --version', { stdio: 'pipe' })
27
+ } catch {
28
+ console.error('Error: openclaw is not installed or not in PATH.')
29
+ console.error('Install it first: npm i -g openclaw')
30
+ process.exit(1)
31
+ }
32
+ }
33
+
34
+ function installPlugin(): void {
35
+ try {
36
+ const list = execSync('openclaw plugins list', { stdio: 'pipe' }).toString()
37
+ if (list.includes('folotoy-openclaw-plugin')) {
38
+ return // already installed
39
+ }
40
+ } catch {
41
+ // ignore
42
+ }
43
+ console.log('Installing FoloToy plugin...')
44
+ execSync('openclaw plugins install @folotoy/folotoy-openclaw-plugin', { stdio: 'inherit' })
45
+ }
46
+
47
+ async function createSession(): Promise<CreateSessionResponse> {
48
+ const res = await fetch(`${PAIR_API_BASE}/api/pair`, { method: 'POST' })
49
+ if (!res.ok) throw new Error(`Failed to create pairing session (HTTP ${res.status})`)
50
+ return res.json() as Promise<CreateSessionResponse>
51
+ }
52
+
53
+ function displayQR(url: string): void {
54
+ qrcode.generate(url, { small: true }, (qr: string) => {
55
+ console.log(qr)
56
+ })
57
+ console.log(`Or open this URL on your phone: ${url}\n`)
58
+ }
59
+
60
+ async function sleep(ms: number): Promise<void> {
61
+ return new Promise((r) => setTimeout(r, ms))
62
+ }
63
+
64
+ async function pollSession(sessionId: string): Promise<PollResponse & { status: 'completed' }> {
65
+ const deadline = Date.now() + POLL_TIMEOUT_MS
66
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
67
+ let i = 0
68
+
69
+ while (Date.now() < deadline) {
70
+ process.stdout.write(`\r${frames[i++ % frames.length]} Waiting for pairing...`)
71
+
72
+ const res = await fetch(`${PAIR_API_BASE}/api/pair/${sessionId}`)
73
+ if (!res.ok) throw new Error(`Poll failed (HTTP ${res.status})`)
74
+ const data = (await res.json()) as PollResponse
75
+
76
+ if (data.status === 'completed') {
77
+ process.stdout.write('\r\x1b[32m✓\x1b[0m Paired successfully! \n')
78
+ return data as PollResponse & { status: 'completed' }
79
+ }
80
+ if (data.status === 'expired') {
81
+ process.stdout.write('\r')
82
+ throw new Error('Pairing session expired. Please try again.')
83
+ }
84
+
85
+ await sleep(POLL_INTERVAL_MS)
86
+ }
87
+ throw new Error('Pairing timed out after 5 minutes.')
88
+ }
89
+
90
+ function writeConfig(result: { toy_sn: string; toy_key: string; mqtt_host?: string; mqtt_port?: number }): void {
91
+ execSync(`openclaw config set channels.folotoy.flow direct`, { stdio: 'pipe' })
92
+ execSync(`openclaw config set channels.folotoy.toy_sn ${result.toy_sn}`, { stdio: 'pipe' })
93
+ execSync(`openclaw config set channels.folotoy.toy_key ${result.toy_key}`, { stdio: 'pipe' })
94
+
95
+ const mqttHost = result.mqtt_host ?? DEFAULT_MQTT_HOST
96
+ const mqttPort = result.mqtt_port ?? DEFAULT_MQTT_PORT
97
+ execSync(`openclaw config set channels.folotoy.mqtt_host ${mqttHost}`, { stdio: 'pipe' })
98
+ execSync(`openclaw config set channels.folotoy.mqtt_port ${mqttPort}`, { stdio: 'pipe' })
99
+ }
100
+
101
+ // ── Main ───────────────────────────────────────────────
102
+
103
+ async function main() {
104
+ const command = process.argv[2]
105
+
106
+ if (command !== 'install') {
107
+ console.log('Usage: npx @folotoy/folotoy-openclaw-plugin install')
108
+ process.exit(command ? 1 : 0)
109
+ }
110
+
111
+ console.log('🧸 FoloToy OpenClaw Plugin Installer\n')
112
+
113
+ // Step 1: check prerequisites
114
+ console.log('Checking openclaw...')
115
+ checkOpenClaw()
116
+ console.log('✓ openclaw found\n')
117
+
118
+ // Step 2: install plugin if not present
119
+ installPlugin()
120
+
121
+ // Step 3: create pairing session
122
+ console.log('Creating pairing session...\n')
123
+ const session = await createSession()
124
+
125
+ // Step 4: display QR code
126
+ console.log('Scan this QR code with your phone,')
127
+ console.log('then scan your toy\'s QR code on the phone:\n')
128
+ displayQR(session.pair_url)
129
+
130
+ // Step 5: poll for result
131
+ const result = await pollSession(session.session_id)
132
+
133
+ // Step 6: write config
134
+ console.log('\nWriting configuration...')
135
+ writeConfig(result)
136
+
137
+ // Step 7: done
138
+ console.log('\n\x1b[32m✓ FoloToy plugin installed and configured!\x1b[0m')
139
+ console.log(` Toy SN: ${result.toy_sn}`)
140
+ console.log(` MQTT Host: ${result.mqtt_host ?? DEFAULT_MQTT_HOST}`)
141
+ console.log('\nRestart the gateway to apply: openclaw gateway start --force')
142
+ }
143
+
144
+ main().catch((err) => {
145
+ console.error(`\n\x1b[31mError:\x1b[0m ${err instanceof Error ? err.message : String(err)}`)
146
+ process.exit(1)
147
+ })
@@ -0,0 +1,3 @@
1
+ declare module 'qrcode-terminal' {
2
+ export function generate(text: string, opts?: { small?: boolean }, cb?: (qr: string) => void): void
3
+ }