@folotoy/folotoy-openclaw-plugin 0.4.0 → 0.5.0

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
@@ -33,6 +33,8 @@ The plugin supports two authentication flows. All fields are configured as flat
33
33
  | `toy_key` | Toy key (used as MQTT password) |
34
34
  | `mqtt_host` | MQTT broker host (default: `198.19.249.25`) |
35
35
  | `mqtt_port` | MQTT broker port (default: `1883`) |
36
+ | `summary_enabled` | Enable reply summarization (default: `true`) |
37
+ | `summary_max_chars` | Character threshold for summarization (default: `200`) |
36
38
 
37
39
  ### Flow 1: HTTP API Login
38
40
 
@@ -56,7 +58,9 @@ Example `openclaw.json`:
56
58
  "flow": "direct",
57
59
  "toy_sn": "your-toy-sn",
58
60
  "toy_key": "your-toy-key",
59
- "mqtt_host": "198.19.249.25"
61
+ "mqtt_host": "198.19.249.25",
62
+ "summary_enabled": true,
63
+ "summary_max_chars": 200
60
64
  }
61
65
  }
62
66
  }
@@ -71,13 +75,16 @@ Inbound (Toy → Plugin): /openapi/folotoy/{sn}/thing/command/call
71
75
  Outbound (Plugin → Toy): /openapi/folotoy/{sn}/thing/command/callAck
72
76
  ```
73
77
 
74
- The plugin connects with an `openapi:` prefix on the username to distinguish itself from the toy's own connection:
78
+ The plugin connects with an `openapi:` prefix on the `clientId` to distinguish itself from the toy's own connection:
75
79
 
76
80
  ```
77
- username: openapi:{toy_sn}
81
+ clientId: openapi:{toy_sn}
82
+ username: {toy_sn}
78
83
  password: {toy_key}
79
84
  ```
80
85
 
86
+ Connection failures trigger exponential backoff reconnection (1s → 2s → 4s → ... → 60s max), resetting on successful connect.
87
+
81
88
  ## Message Format
82
89
 
83
90
  **Toy → Plugin (inbound)**
@@ -95,7 +102,7 @@ password: {toy_key}
95
102
 
96
103
  **Plugin → Toy (outbound)**
97
104
 
98
- Multiple response chunks with auto-incrementing `order`, followed by a finish message:
105
+ Reply is buffered, then summarized if it exceeds `summary_max_chars`. Sent as a single message with auto-incrementing `order`, followed by a finish message:
99
106
 
100
107
  ```json
101
108
  {
@@ -137,12 +144,26 @@ Finish message (`is_finished: true`, empty content):
137
144
 
138
145
  Switch environments via the `FOLOTOY_MQTT_HOST` environment variable or `mqtt_host` config field.
139
146
 
147
+ ## Features
148
+
149
+ ### Reply Summarization
150
+
151
+ When AI reply exceeds `summary_max_chars` (default 200), the plugin uses the primary model to generate a concise summary before sending to the toy. This avoids excessively long voice playback. Falls back to truncation on failure. Disable with `summary_enabled: false`.
152
+
153
+ ### Soothing Acknowledgment
154
+
155
+ Immediately sends a transitional reply (e.g., "Let me think...") upon receiving a message, providing instant feedback while the AI processes.
156
+
157
+ ### Exponential Backoff Reconnection
158
+
159
+ MQTT connection failures trigger automatic reconnection with exponential backoff (1s → 60s cap), resetting on success.
160
+
140
161
  ## Development
141
162
 
142
163
  ```bash
143
- npm install
144
- npm test
145
- npm run build
164
+ pnpm install
165
+ pnpm test
166
+ pnpm build
146
167
  ```
147
168
 
148
169
  ## License
@@ -65,6 +65,14 @@ async function pollSession(sessionId) {
65
65
  }
66
66
  throw new Error('Pairing timed out after 5 minutes.');
67
67
  }
68
+ function restartGateway() {
69
+ try {
70
+ execSync('openclaw gateway restart', { stdio: 'inherit' });
71
+ }
72
+ catch {
73
+ console.warn('⚠ Failed to restart gateway. You can restart manually: openclaw gateway restart');
74
+ }
75
+ }
68
76
  function writeConfig(result) {
69
77
  execSync(`openclaw config set channels.folotoy.flow direct`, { stdio: 'pipe' });
70
78
  execSync(`openclaw config set channels.folotoy.toy_sn ${result.toy_sn}`, { stdio: 'pipe' });
@@ -100,11 +108,13 @@ async function main() {
100
108
  // Step 6: write config
101
109
  console.log('\nWriting configuration...');
102
110
  writeConfig(result);
103
- // Step 7: done
111
+ // Step 7: restart gateway
112
+ console.log('\nRestarting gateway...');
113
+ restartGateway();
114
+ // Step 8: done
104
115
  console.log('\n\x1b[32m✓ FoloToy plugin installed and configured!\x1b[0m');
105
116
  console.log(` Toy SN: ${result.toy_sn}`);
106
117
  console.log(` MQTT Host: ${result.mqtt_host ?? DEFAULT_MQTT_HOST}`);
107
- console.log('\nRestart the gateway to apply: openclaw gateway start --force');
108
118
  }
109
119
  main().catch((err) => {
110
120
  console.error(`\n\x1b[31mError:\x1b[0m ${err instanceof Error ? err.message : String(err)}`);
@@ -1 +1 @@
1
- {"version":3,"file":"install.js","sourceRoot":"","sources":["../../src/cli/install.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"install.js","sourceRoot":"","sources":["../../src/cli/install.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
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
+ summary_enabled?: boolean;
29
+ summary_max_chars?: number;
30
+ };
31
+ export declare const DEFAULT_API_URL = "https://api.folotoy.cn";
32
+ export declare const DEFAULT_MQTT_HOST: string;
33
+ export declare const DEFAULT_MQTT_PORT = 1883;
34
+ export declare const DEFAULT_SUMMARY_ENABLED = true;
35
+ export declare const DEFAULT_SUMMARY_MAX_CHARS = 200;
36
+ export declare function flatToPluginConfig(flat: FlatChannelConfig): PluginConfig;
37
+ //# 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;IAClB,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B,CAAA;AAED,eAAO,MAAM,eAAe,2BAA2B,CAAA;AACvD,eAAO,MAAM,iBAAiB,QAAgD,CAAA;AAC9E,eAAO,MAAM,iBAAiB,OAAO,CAAA;AACrC,eAAO,MAAM,uBAAuB,OAAO,CAAA;AAC3C,eAAO,MAAM,yBAAyB,MAAM,CAAA;AAE5C,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,iBAAiB,GAAG,YAAY,CAaxE"}
package/dist/config.js ADDED
@@ -0,0 +1,19 @@
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 const DEFAULT_SUMMARY_ENABLED = true;
5
+ export const DEFAULT_SUMMARY_MAX_CHARS = 200;
6
+ export function flatToPluginConfig(flat) {
7
+ const flow = flat.flow ?? 'direct';
8
+ const auth = flow === 'api'
9
+ ? { flow: 'api', api_url: flat.api_url ?? DEFAULT_API_URL, api_key: flat.api_key ?? '', toy_sn: flat.toy_sn ?? '' }
10
+ : { flow: 'direct', toy_sn: flat.toy_sn ?? '', toy_key: flat.toy_key ?? '' };
11
+ return {
12
+ auth,
13
+ mqtt: {
14
+ host: flat.mqtt_host ?? DEFAULT_MQTT_HOST,
15
+ port: flat.mqtt_port ?? DEFAULT_MQTT_PORT,
16
+ },
17
+ };
18
+ }
19
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAkCA,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;AACrC,MAAM,CAAC,MAAM,uBAAuB,GAAG,IAAI,CAAA;AAC3C,MAAM,CAAC,MAAM,yBAAyB,GAAG,GAAG,CAAA;AAE5C,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,EAAgC,MAAM,0BAA0B,CAAA;AAyP/F,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,wBAGC"}
package/dist/index.js ADDED
@@ -0,0 +1,234 @@
1
+ import { resolveCredentials, createMqttClient, buildInboundTopic, buildOutboundTopic, buildNotificationTopic } from './mqtt.js';
2
+ import { pickSoothingReply } from './soothing.js';
3
+ import { DEFAULT_MQTT_HOST, DEFAULT_MQTT_PORT, DEFAULT_SUMMARY_ENABLED, DEFAULT_SUMMARY_MAX_CHARS, flatToPluginConfig } from './config.js';
4
+ // Per-account MQTT clients and msgId counters
5
+ const activeClients = new Map();
6
+ // Subagent reference, set at plugin registration
7
+ let subagent;
8
+ const folotoyChannel = {
9
+ id: 'folotoy',
10
+ meta: {
11
+ id: 'folotoy',
12
+ label: 'FoloToy',
13
+ selectionLabel: 'FoloToy',
14
+ docsPath: '/channels/folotoy',
15
+ blurb: 'Empower your FoloToy with OpenClaw AI capabilities.',
16
+ },
17
+ capabilities: {
18
+ chatTypes: ['direct'],
19
+ },
20
+ configSchema: {
21
+ schema: {
22
+ type: 'object',
23
+ properties: {
24
+ flow: { type: 'string', enum: ['direct', 'api'], default: 'direct' },
25
+ toy_sn: { type: 'string' },
26
+ toy_key: { type: 'string' },
27
+ api_url: { type: 'string', default: 'https://api.folotoy.cn' },
28
+ api_key: { type: 'string' },
29
+ mqtt_host: { type: 'string', default: DEFAULT_MQTT_HOST },
30
+ mqtt_port: { type: 'number', default: DEFAULT_MQTT_PORT },
31
+ summary_enabled: { type: 'boolean', default: DEFAULT_SUMMARY_ENABLED },
32
+ summary_max_chars: { type: 'number', default: DEFAULT_SUMMARY_MAX_CHARS },
33
+ },
34
+ },
35
+ uiHints: {
36
+ flow: { label: 'Auth Flow' },
37
+ toy_sn: { label: 'Toy SN' },
38
+ toy_key: { label: 'Toy Key', sensitive: true },
39
+ api_url: { label: 'API URL', placeholder: 'https://api.folotoy.com' },
40
+ api_key: { label: 'API Key', sensitive: true },
41
+ mqtt_host: { label: 'MQTT Host', placeholder: DEFAULT_MQTT_HOST },
42
+ mqtt_port: { label: 'MQTT Port' },
43
+ summary_enabled: { label: 'Enable Summary' },
44
+ summary_max_chars: { label: 'Summary Max Characters' },
45
+ },
46
+ },
47
+ config: {
48
+ listAccountIds: (cfg) => {
49
+ const folotoy = cfg
50
+ .channels?.folotoy;
51
+ return folotoy ? ['default'] : [];
52
+ },
53
+ resolveAccount: (cfg, _accountId) => {
54
+ const folotoy = cfg
55
+ .channels?.folotoy;
56
+ return folotoy ?? {};
57
+ },
58
+ },
59
+ gateway: {
60
+ startAccount: async (ctx) => {
61
+ const { account, cfg, accountId, abortSignal, channelRuntime, log } = ctx;
62
+ if (!channelRuntime) {
63
+ log?.warn?.('channelRuntime not available — skipping MQTT connection');
64
+ return;
65
+ }
66
+ if (!account.toy_sn) {
67
+ log?.warn?.('toy_sn not configured — skipping MQTT connection');
68
+ return;
69
+ }
70
+ const mqttConfig = flatToPluginConfig(account);
71
+ log?.info?.(`Connecting to MQTT broker ${mqttConfig.mqtt.host}:${mqttConfig.mqtt.port}...`);
72
+ const credentials = await resolveCredentials(mqttConfig);
73
+ const client = await createMqttClient(mqttConfig, credentials);
74
+ const inboundTopic = buildInboundTopic(credentials.toy_sn);
75
+ const outboundTopic = buildOutboundTopic(credentials.toy_sn);
76
+ activeClients.set(accountId, { client, toy_sn: credentials.toy_sn, nextMsgId: 1 });
77
+ log?.info?.(`Connected to MQTT broker, subscribed to ${inboundTopic}`);
78
+ client.subscribe(inboundTopic, (err) => {
79
+ if (err)
80
+ log?.error?.(`Failed to subscribe: ${err.message}`);
81
+ });
82
+ const summaryEnabled = account.summary_enabled ?? DEFAULT_SUMMARY_ENABLED;
83
+ const summaryMaxChars = account.summary_max_chars ?? DEFAULT_SUMMARY_MAX_CHARS;
84
+ client.on('message', (_topic, payload) => {
85
+ let msg;
86
+ try {
87
+ msg = JSON.parse(payload.toString());
88
+ }
89
+ catch {
90
+ return;
91
+ }
92
+ if (msg.identifier !== 'chat_input' || typeof msg.inputParams?.text !== 'string')
93
+ return;
94
+ const { msgId, inputParams: { text, recording_id } } = msg;
95
+ let order = 1;
96
+ // Send a quick soothing acknowledgment before AI processing (order=1).
97
+ // AI replies continue from order=2.
98
+ const ackMsg = {
99
+ msgId,
100
+ identifier: 'chat_output',
101
+ outParams: { content: pickSoothingReply(text), recording_id, order, is_finished: false },
102
+ };
103
+ client.publish(outboundTopic, JSON.stringify(ackMsg));
104
+ const inboundCtx = channelRuntime.reply.finalizeInboundContext({
105
+ Body: text,
106
+ From: credentials.toy_sn,
107
+ To: credentials.toy_sn,
108
+ SessionKey: `folotoy-${accountId}-${credentials.toy_sn}`,
109
+ AccountId: accountId,
110
+ Provider: 'folotoy',
111
+ });
112
+ // dispatch, optionally summarize, then send finish message
113
+ void (async () => {
114
+ const replyChunks = [];
115
+ try {
116
+ await channelRuntime.reply.dispatchReplyWithBufferedBlockDispatcher({
117
+ ctx: inboundCtx,
118
+ cfg,
119
+ dispatcherOptions: {
120
+ deliver: async (replyPayload) => {
121
+ if (!replyPayload.text)
122
+ return;
123
+ replyChunks.push(replyPayload.text);
124
+ },
125
+ onError: (err) => log?.error?.(`Dispatch error: ${String(err)}`),
126
+ },
127
+ });
128
+ let finalText = replyChunks.join('');
129
+ // Summarize if enabled and text exceeds threshold
130
+ if (summaryEnabled && subagent && finalText.length > summaryMaxChars) {
131
+ try {
132
+ const sessionKey = `folotoy-summary-${accountId}-${Date.now()}`;
133
+ const { runId } = await subagent.run({
134
+ sessionKey,
135
+ message: [
136
+ `You are an assistant that summarizes texts concisely while keeping the most important information.`,
137
+ `Summarize the text to approximately ${summaryMaxChars} characters.`,
138
+ `Maintain the original tone and style. Reply only with the summary, without additional explanations.`,
139
+ ``,
140
+ `<text_to_summarize>`,
141
+ finalText,
142
+ `</text_to_summarize>`,
143
+ ].join('\n'),
144
+ deliver: false,
145
+ });
146
+ const result = await subagent.waitForRun({ runId, timeoutMs: 30_000 });
147
+ if (result.status === 'ok') {
148
+ const { messages } = await subagent.getSessionMessages({ sessionKey, limit: 1 });
149
+ const lastMsg = messages[messages.length - 1];
150
+ const summaryText = lastMsg?.content ?? lastMsg?.text;
151
+ if (summaryText)
152
+ finalText = summaryText;
153
+ }
154
+ await subagent.deleteSession({ sessionKey }).catch(() => { });
155
+ }
156
+ catch (err) {
157
+ log?.warn?.(`Summary failed, truncating text: ${String(err)}`);
158
+ finalText = `${finalText.slice(0, summaryMaxChars - 3)}...`;
159
+ }
160
+ }
161
+ if (finalText) {
162
+ order++;
163
+ const outMsg = {
164
+ msgId,
165
+ identifier: 'chat_output',
166
+ outParams: { content: finalText, recording_id, order, is_finished: false },
167
+ };
168
+ client.publish(outboundTopic, JSON.stringify(outMsg));
169
+ }
170
+ }
171
+ finally {
172
+ order++;
173
+ const finishMsg = {
174
+ msgId,
175
+ identifier: 'chat_output',
176
+ outParams: { content: '', recording_id, order, is_finished: true },
177
+ };
178
+ client.publish(outboundTopic, JSON.stringify(finishMsg));
179
+ }
180
+ })();
181
+ });
182
+ // Keep the account alive until aborted
183
+ return new Promise((resolve) => {
184
+ abortSignal.addEventListener('abort', () => {
185
+ activeClients.delete(accountId);
186
+ client.end();
187
+ log?.info?.('MQTT client disconnected');
188
+ resolve();
189
+ });
190
+ });
191
+ },
192
+ stopAccount: async (_ctx) => {
193
+ // cleanup handled by abortSignal listener in startAccount
194
+ },
195
+ },
196
+ outbound: {
197
+ deliveryMode: 'direct',
198
+ sendText: async ({ text, accountId }) => {
199
+ const key = accountId ?? 'default';
200
+ const entry = activeClients.get(key);
201
+ if (!entry)
202
+ throw new Error(`No active MQTT client for account "${key}"`);
203
+ const outboundTopic = buildOutboundTopic(entry.toy_sn);
204
+ const msgId = entry.nextMsgId++;
205
+ const outMsg = {
206
+ msgId,
207
+ identifier: 'chat_output',
208
+ outParams: { content: text },
209
+ };
210
+ entry.client.publish(outboundTopic, JSON.stringify(outMsg));
211
+ return { channel: 'folotoy', messageId: String(msgId) };
212
+ },
213
+ },
214
+ };
215
+ export function sendNotification({ text, accountId }) {
216
+ const key = accountId ?? 'default';
217
+ const entry = activeClients.get(key);
218
+ if (!entry)
219
+ throw new Error(`No active MQTT client for account "${key}"`);
220
+ const notificationTopic = buildNotificationTopic(entry.toy_sn);
221
+ const msgId = entry.nextMsgId++;
222
+ const notifMsg = {
223
+ msgId,
224
+ identifier: 'send_notification',
225
+ outParams: { text },
226
+ };
227
+ entry.client.publish(notificationTopic, JSON.stringify(notifMsg));
228
+ return { channel: 'folotoy', messageId: String(msgId) };
229
+ }
230
+ export default (api) => {
231
+ subagent = api.runtime.subagent;
232
+ api.registerChannel({ plugin: folotoyChannel });
233
+ };
234
+ //# 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;AAKD,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,YAAY,EAAE,WAAW,EAAE,eAAe,GAAG,OAAO,CAAC,UAAU,CAAC,CA4B9G"}
package/dist/mqtt.js ADDED
@@ -0,0 +1,70 @@
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
+ const INITIAL_RECONNECT_MS = 1000;
44
+ const MAX_RECONNECT_MS = 60000;
45
+ export async function createMqttClient(config, credentials) {
46
+ const { host, port } = config.mqtt;
47
+ const { username, password } = credentials;
48
+ return new Promise((resolve, reject) => {
49
+ const clientId = `openapi:${credentials.toy_sn}`;
50
+ const client = mqtt.connect(`mqtt://${host}:${port}`, {
51
+ clientId,
52
+ username,
53
+ password,
54
+ clean: true,
55
+ reconnectPeriod: INITIAL_RECONNECT_MS,
56
+ });
57
+ // Exponential backoff: increase reconnectPeriod on each failed attempt,
58
+ // reset on successful connection.
59
+ client.on('reconnect', () => {
60
+ const current = client.options.reconnectPeriod ?? INITIAL_RECONNECT_MS;
61
+ client.options.reconnectPeriod = Math.min(current * 2, MAX_RECONNECT_MS);
62
+ });
63
+ client.on('connect', () => {
64
+ client.options.reconnectPeriod = INITIAL_RECONNECT_MS;
65
+ });
66
+ client.once('connect', () => resolve(client));
67
+ client.once('error', reject);
68
+ });
69
+ }
70
+ //# 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,oBAAoB,GAAG,IAAI,CAAA;AACjC,MAAM,gBAAgB,GAAG,KAAK,CAAA;AAE9B,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,WAAW,CAAC,MAAM,EAAE,CAAA;QAChD,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,IAAI,IAAI,IAAI,EAAE,EAAE;YACpD,QAAQ;YACR,QAAQ;YACR,QAAQ;YACR,KAAK,EAAE,IAAI;YACX,eAAe,EAAE,oBAAoB;SACtC,CAAC,CAAA;QAEF,wEAAwE;QACxE,kCAAkC;QAClC,MAAM,CAAC,EAAE,CAAC,WAAW,EAAE,GAAG,EAAE;YAC1B,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,eAAe,IAAI,oBAAoB,CAAA;YACtE,MAAM,CAAC,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,GAAG,CAAC,EAAE,gBAAgB,CAAC,CAAA;QAC1E,CAAC,CAAC,CAAA;QAEF,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE;YACxB,MAAM,CAAC,OAAO,CAAC,eAAe,GAAG,oBAAoB,CAAA;QACvD,CAAC,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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Soothing acknowledgment replies for FoloToy.
3
+ *
4
+ * 12 intent categories, 5 candidates each — one is chosen at random.
5
+ * Principles: convey "I heard you + I'm working on it", no time promises.
6
+ */
7
+ /** Returns one randomly chosen soothing reply matching the input intent. */
8
+ export declare function pickSoothingReply(text: string): string;
9
+ //# sourceMappingURL=soothing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"soothing.d.ts","sourceRoot":"","sources":["../src/soothing.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA+IH,4EAA4E;AAC5E,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAOtD"}
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Soothing acknowledgment replies for FoloToy.
3
+ *
4
+ * 12 intent categories, 5 candidates each — one is chosen at random.
5
+ * Principles: convey "I heard you + I'm working on it", no time promises.
6
+ */
7
+ const INTENT_RULES = [
8
+ {
9
+ // 1. 难过类
10
+ pattern: /难过|伤心|哭|不开心|委屈/,
11
+ candidates: [
12
+ '抱抱你,我在认真听呢,让我想想怎么说...',
13
+ '别难过,我陪着你,让我想一会儿...',
14
+ '嗯,我听到了,给我点时间好好想想怎么帮你',
15
+ '你说的我都记住了,让我想想...',
16
+ '我在认真想呢,你先缓缓',
17
+ ],
18
+ },
19
+ {
20
+ // 2. 生气类
21
+ pattern: /生气|愤怒|烦|讨厌|气死/,
22
+ candidates: [
23
+ '我理解你,让我想想怎么帮你...',
24
+ '消消气,我在认真想办法呢',
25
+ '听到了,让我好好想一下...',
26
+ '别气别气,我来帮你想想,给我一点时间',
27
+ '嗯,你说的我记住了,我想一下',
28
+ ],
29
+ },
30
+ {
31
+ // 3. 累/怕类
32
+ pattern: /累|疲|困|害怕|怕/,
33
+ candidates: [
34
+ '辛苦了,让我想想怎么帮你...',
35
+ '别怕,有我在呢,我想一下',
36
+ '你先歇歇,让我帮你想想办法...',
37
+ '我在想办法呢,你先放松一下',
38
+ '嗯嗯,我在认真想怎么帮你...',
39
+ ],
40
+ },
41
+ {
42
+ // 4. 故事类
43
+ pattern: /故事|story/i,
44
+ candidates: [
45
+ '让我好好想一个故事...',
46
+ '嗯,我在想一个好听的故事呢',
47
+ '让我想想讲什么好...',
48
+ '我在脑子里翻故事书呢,等等我',
49
+ '故事嘛,得好好想一个才行...',
50
+ ],
51
+ },
52
+ {
53
+ // 5. 唱歌/音乐类
54
+ pattern: /唱|歌|音乐|播放|儿歌/,
55
+ candidates: [
56
+ '让我想想唱什么好...',
57
+ '嗯,我在选歌呢,等我一下',
58
+ '我清清嗓子,想想唱什么...',
59
+ '让我找一首好听的...',
60
+ '好嘞,让我想想唱哪首',
61
+ ],
62
+ },
63
+ {
64
+ // 6. 笑话类
65
+ pattern: /笑话|joke|搞笑|好笑/i,
66
+ candidates: [
67
+ '让我想一个好笑的...',
68
+ '嗯,我在想笑话呢,先憋住',
69
+ '好嘞,让我想一个逗你乐的...',
70
+ '笑话嘛,得想一个真正好笑的才行...',
71
+ '让我翻翻我的笑话本...',
72
+ ],
73
+ },
74
+ {
75
+ // 7. 知识/古诗类
76
+ pattern: /为什么|怎么|什么是|古诗|诗|背/,
77
+ candidates: [
78
+ '好问题,让我好好想想...',
79
+ '嗯,这个问题我得想一下',
80
+ '让我仔细想想怎么跟你解释...',
81
+ '这个嘛,让我想一想...',
82
+ '嗯,让我好好想想这个',
83
+ ],
84
+ },
85
+ {
86
+ // 8. 继续/追问类
87
+ pattern: /继续|接着|然后呢|后来|下一个/,
88
+ candidates: [
89
+ '让我接着往下想...',
90
+ '嗯,后面的我得好好想想',
91
+ '让我理理思路...',
92
+ '好嘞,让我想想后面怎么接',
93
+ '我在想呢,等一下',
94
+ ],
95
+ },
96
+ {
97
+ // 9. 打招呼/呼唤类
98
+ pattern: /你好|嗨|hello|hi\b|hey|哈喽/i,
99
+ candidates: [
100
+ '来啦,让我想想说什么...',
101
+ '嘿,你来啦,让我想想...',
102
+ '我在呢,让我想想跟你说什么好',
103
+ '哎,来了,给我一点时间想想',
104
+ '哈,你来啦,让我想一下...',
105
+ ],
106
+ },
107
+ {
108
+ // 10. 告别/睡觉类
109
+ pattern: /关机|睡|拜拜|再见|bye|晚安/i,
110
+ candidates: ['好嘞~', '嗯嗯~', '好的~', '哦~', '嗯~'],
111
+ },
112
+ {
113
+ // 11. 配网/技术类
114
+ pattern: /网络|配网|wifi|蓝牙|连接/i,
115
+ candidates: [
116
+ '让我想想怎么帮你...',
117
+ '嗯,这个我得想一下',
118
+ '让我看看怎么办...',
119
+ '我在想呢,等一下',
120
+ '嗯,给我一点时间想想',
121
+ ],
122
+ },
123
+ {
124
+ // 12. 查询类
125
+ pattern: /天气|新闻|时间|几点|日期|查|搜/,
126
+ candidates: [
127
+ '我去帮你查一查...',
128
+ '好的,我来查一下',
129
+ '稍等,我帮你看看',
130
+ '我去查一下,等我一秒',
131
+ '让我来查查看',
132
+ ],
133
+ },
134
+ ];
135
+ const DEFAULT_CANDIDATES = [
136
+ '让我想一想...',
137
+ '嗯嗯,让我想想怎么回答你',
138
+ '好嘞,我想一下',
139
+ '嗯,给我一点时间想想',
140
+ '让我好好想想...',
141
+ ];
142
+ function pickRandom(arr) {
143
+ return arr[Math.floor(Math.random() * arr.length)];
144
+ }
145
+ /** Returns one randomly chosen soothing reply matching the input intent. */
146
+ export function pickSoothingReply(text) {
147
+ for (const rule of INTENT_RULES) {
148
+ if (rule.pattern.test(text)) {
149
+ return pickRandom(rule.candidates);
150
+ }
151
+ }
152
+ return pickRandom(DEFAULT_CANDIDATES);
153
+ }
154
+ //# sourceMappingURL=soothing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"soothing.js","sourceRoot":"","sources":["../src/soothing.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,YAAY,GAAqD;IACrE;QACE,SAAS;QACT,OAAO,EAAE,gBAAgB;QACzB,UAAU,EAAE;YACV,uBAAuB;YACvB,oBAAoB;YACpB,sBAAsB;YACtB,kBAAkB;YAClB,aAAa;SACd;KACF;IACD;QACE,SAAS;QACT,OAAO,EAAE,eAAe;QACxB,UAAU,EAAE;YACV,kBAAkB;YAClB,cAAc;YACd,gBAAgB;YAChB,oBAAoB;YACpB,gBAAgB;SACjB;KACF;IACD;QACE,UAAU;QACV,OAAO,EAAE,YAAY;QACrB,UAAU,EAAE;YACV,iBAAiB;YACjB,cAAc;YACd,kBAAkB;YAClB,eAAe;YACf,iBAAiB;SAClB;KACF;IACD;QACE,SAAS;QACT,OAAO,EAAE,WAAW;QACpB,UAAU,EAAE;YACV,cAAc;YACd,eAAe;YACf,aAAa;YACb,gBAAgB;YAChB,iBAAiB;SAClB;KACF;IACD;QACE,YAAY;QACZ,OAAO,EAAE,cAAc;QACvB,UAAU,EAAE;YACV,aAAa;YACb,cAAc;YACd,gBAAgB;YAChB,aAAa;YACb,YAAY;SACb;KACF;IACD;QACE,SAAS;QACT,OAAO,EAAE,gBAAgB;QACzB,UAAU,EAAE;YACV,aAAa;YACb,cAAc;YACd,iBAAiB;YACjB,oBAAoB;YACpB,cAAc;SACf;KACF;IACD;QACE,YAAY;QACZ,OAAO,EAAE,mBAAmB;QAC5B,UAAU,EAAE;YACV,eAAe;YACf,aAAa;YACb,iBAAiB;YACjB,cAAc;YACd,YAAY;SACb;KACF;IACD;QACE,YAAY;QACZ,OAAO,EAAE,kBAAkB;QAC3B,UAAU,EAAE;YACV,YAAY;YACZ,aAAa;YACb,WAAW;YACX,cAAc;YACd,UAAU;SACX;KACF;IACD;QACE,aAAa;QACb,OAAO,EAAE,yBAAyB;QAClC,UAAU,EAAE;YACV,eAAe;YACf,eAAe;YACf,gBAAgB;YAChB,eAAe;YACf,gBAAgB;SACjB;KACF;IACD;QACE,aAAa;QACb,OAAO,EAAE,oBAAoB;QAC7B,UAAU,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC;KAC9C;IACD;QACE,aAAa;QACb,OAAO,EAAE,mBAAmB;QAC5B,UAAU,EAAE;YACV,aAAa;YACb,WAAW;YACX,YAAY;YACZ,UAAU;YACV,YAAY;SACb;KACF;IACD;QACE,UAAU;QACV,OAAO,EAAE,oBAAoB;QAC7B,UAAU,EAAE;YACV,YAAY;YACZ,UAAU;YACV,UAAU;YACV,YAAY;YACZ,QAAQ;SACT;KACF;CACF,CAAA;AAED,MAAM,kBAAkB,GAAG;IACzB,UAAU;IACV,cAAc;IACd,SAAS;IACT,YAAY;IACZ,WAAW;CACZ,CAAA;AAED,SAAS,UAAU,CAAI,GAAQ;IAC7B,OAAO,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,CAAC,CAAM,CAAA;AACzD,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,KAAK,MAAM,IAAI,IAAI,YAAY,EAAE,CAAC;QAChC,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC5B,OAAO,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;QACpC,CAAC;IACH,CAAC;IACD,OAAO,UAAU,CAAC,kBAAkB,CAAC,CAAA;AACvC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@folotoy/folotoy-openclaw-plugin",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Empower your FoloToy with OpenClaw AI capabilities.",
5
5
  "keywords": [
6
6
  "folotoy",
@@ -28,7 +28,7 @@
28
28
  "files": [
29
29
  "src",
30
30
  "bin",
31
- "dist/cli",
31
+ "dist",
32
32
  "openclaw.plugin.json"
33
33
  ],
34
34
  "scripts": {
@@ -0,0 +1,47 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { pickSoothingReply } from '../soothing.js'
3
+
4
+ const CATEGORIES = [
5
+ { input: '我今天好难过', label: '难过类' },
6
+ { input: '我很生气', label: '生气类' },
7
+ { input: '我好累', label: '累/怕类' },
8
+ { input: '给我讲个故事', label: '故事类' },
9
+ { input: '唱首儿歌', label: '唱歌类' },
10
+ { input: '讲个笑话', label: '笑话类' },
11
+ { input: '为什么天是蓝的', label: '知识类' },
12
+ { input: '然后呢', label: '继续类' },
13
+ { input: '你好', label: '打招呼类' },
14
+ { input: '晚安', label: '告别类' },
15
+ { input: '帮我连接wifi', label: '配网类' },
16
+ { input: '帮我查天气', label: '查询类' },
17
+ { input: '随便说点什么', label: '兜底' },
18
+ ]
19
+
20
+ describe('pickSoothingReply', () => {
21
+ it.each(CATEGORIES)('$label — returns a non-empty string', ({ input }) => {
22
+ const reply = pickSoothingReply(input)
23
+ expect(typeof reply).toBe('string')
24
+ expect(reply.length).toBeGreaterThan(0)
25
+ })
26
+
27
+ it.each(CATEGORIES)('$label — never contains 马上', ({ input }) => {
28
+ // Run multiple times to cover random selection
29
+ for (let i = 0; i < 20; i++) {
30
+ expect(pickSoothingReply(input)).not.toContain('马上')
31
+ }
32
+ })
33
+
34
+ it('randomly selects from candidates (not always the same reply)', () => {
35
+ const results = new Set(
36
+ Array.from({ length: 40 }, () => pickSoothingReply('我今天好难过'))
37
+ )
38
+ expect(results.size).toBeGreaterThan(1)
39
+ })
40
+
41
+ it('告别类 returns only short affirmatives', () => {
42
+ const allowed = new Set(['好嘞~', '嗯嗯~', '好的~', '哦~', '嗯~'])
43
+ for (let i = 0; i < 20; i++) {
44
+ expect(allowed.has(pickSoothingReply('晚安'))).toBe(true)
45
+ }
46
+ })
47
+ })
@@ -87,6 +87,14 @@ async function pollSession(sessionId: string): Promise<PollResponse & { status:
87
87
  throw new Error('Pairing timed out after 5 minutes.')
88
88
  }
89
89
 
90
+ function restartGateway(): void {
91
+ try {
92
+ execSync('openclaw gateway restart', { stdio: 'inherit' })
93
+ } catch {
94
+ console.warn('⚠ Failed to restart gateway. You can restart manually: openclaw gateway restart')
95
+ }
96
+ }
97
+
90
98
  function writeConfig(result: { toy_sn: string; toy_key: string; mqtt_host?: string; mqtt_port?: number }): void {
91
99
  execSync(`openclaw config set channels.folotoy.flow direct`, { stdio: 'pipe' })
92
100
  execSync(`openclaw config set channels.folotoy.toy_sn ${result.toy_sn}`, { stdio: 'pipe' })
@@ -134,11 +142,14 @@ async function main() {
134
142
  console.log('\nWriting configuration...')
135
143
  writeConfig(result)
136
144
 
137
- // Step 7: done
145
+ // Step 7: restart gateway
146
+ console.log('\nRestarting gateway...')
147
+ restartGateway()
148
+
149
+ // Step 8: done
138
150
  console.log('\n\x1b[32m✓ FoloToy plugin installed and configured!\x1b[0m')
139
151
  console.log(` Toy SN: ${result.toy_sn}`)
140
152
  console.log(` MQTT Host: ${result.mqtt_host ?? DEFAULT_MQTT_HOST}`)
141
- console.log('\nRestart the gateway to apply: openclaw gateway start --force')
142
153
  }
143
154
 
144
155
  main().catch((err) => {
package/src/config.ts CHANGED
@@ -28,11 +28,15 @@ export type FlatChannelConfig = {
28
28
  api_key?: string
29
29
  mqtt_host?: string
30
30
  mqtt_port?: number
31
+ summary_enabled?: boolean
32
+ summary_max_chars?: number
31
33
  }
32
34
 
33
35
  export const DEFAULT_API_URL = 'https://api.folotoy.cn'
34
36
  export const DEFAULT_MQTT_HOST = process.env.FOLOTOY_MQTT_HOST ?? 'f.qrc92.cn'
35
37
  export const DEFAULT_MQTT_PORT = 1883
38
+ export const DEFAULT_SUMMARY_ENABLED = true
39
+ export const DEFAULT_SUMMARY_MAX_CHARS = 200
36
40
 
37
41
  export function flatToPluginConfig(flat: FlatChannelConfig): PluginConfig {
38
42
  const flow = flat.flow ?? 'direct'
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
- import type { OpenClawPluginApi, ChannelPlugin } from 'openclaw/plugin-sdk/core'
1
+ import type { OpenClawPluginApi, ChannelPlugin, PluginRuntime } from 'openclaw/plugin-sdk/core'
2
2
  import { resolveCredentials, createMqttClient, buildInboundTopic, buildOutboundTopic, buildNotificationTopic } from './mqtt.js'
3
- import { DEFAULT_MQTT_HOST, DEFAULT_MQTT_PORT, flatToPluginConfig } from './config.js'
3
+ import { pickSoothingReply } from './soothing.js'
4
+ import { DEFAULT_MQTT_HOST, DEFAULT_MQTT_PORT, DEFAULT_SUMMARY_ENABLED, DEFAULT_SUMMARY_MAX_CHARS, flatToPluginConfig } from './config.js'
4
5
  import type { FlatChannelConfig } from './config.js'
5
6
  import type { MqttClient } from 'mqtt'
6
7
 
@@ -22,33 +23,12 @@ type NotificationMessage = {
22
23
  outParams: { text: string }
23
24
  }
24
25
 
25
- /** Pick a soothing acknowledgment that loosely matches the input. */
26
- function pickSoothingReply(text: string): string {
27
- const t = text.toLowerCase()
28
-
29
- if (/难过|伤心|哭|不开心|sad|upset|cry/.test(t))
30
- return '抱抱你,我在听呢,让我想想怎么帮你。'
31
- if (/害怕|恐惧|怕|scared|afraid/.test(t))
32
- return '别怕,有我在呢,让我想一想。'
33
- if (/生气|愤怒|烦|angry|mad/.test(t))
34
- return '我理解你的感受,让我来帮你想想办法。'
35
- if (/累|疲|困|tired|exhausted/.test(t))
36
- return '辛苦了,休息一下,我来帮你想。'
37
- if (/无聊|没意思|bored/.test(t))
38
- return '我来陪你聊聊吧,让我想想。'
39
- if (/谢|感谢|thank/.test(t))
40
- return '不客气呀,让我想想还能帮你什么。'
41
- if (/你好|嗨|hello|hi|hey/.test(t))
42
- return '你好呀!让我想想怎么回答你。'
43
- if (/帮|help|怎么办/.test(t))
44
- return '没问题,让我帮你想想办法。'
45
-
46
- return '好的,让我想一想,马上回复你。'
47
- }
48
-
49
26
  // Per-account MQTT clients and msgId counters
50
27
  const activeClients = new Map<string, { client: MqttClient; toy_sn: string; nextMsgId: number }>()
51
28
 
29
+ // Subagent reference, set at plugin registration
30
+ let subagent: PluginRuntime['subagent'] | undefined
31
+
52
32
  const folotoyChannel: ChannelPlugin<FlatChannelConfig> = {
53
33
  id: 'folotoy',
54
34
  meta: {
@@ -72,6 +52,8 @@ const folotoyChannel: ChannelPlugin<FlatChannelConfig> = {
72
52
  api_key: { type: 'string' },
73
53
  mqtt_host: { type: 'string', default: DEFAULT_MQTT_HOST },
74
54
  mqtt_port: { type: 'number', default: DEFAULT_MQTT_PORT },
55
+ summary_enabled: { type: 'boolean', default: DEFAULT_SUMMARY_ENABLED },
56
+ summary_max_chars: { type: 'number', default: DEFAULT_SUMMARY_MAX_CHARS },
75
57
  },
76
58
  },
77
59
  uiHints: {
@@ -82,6 +64,8 @@ const folotoyChannel: ChannelPlugin<FlatChannelConfig> = {
82
64
  api_key: { label: 'API Key', sensitive: true },
83
65
  mqtt_host: { label: 'MQTT Host', placeholder: DEFAULT_MQTT_HOST },
84
66
  mqtt_port: { label: 'MQTT Port' },
67
+ summary_enabled: { label: 'Enable Summary' },
68
+ summary_max_chars: { label: 'Summary Max Characters' },
85
69
  },
86
70
  },
87
71
  config: {
@@ -124,6 +108,9 @@ const folotoyChannel: ChannelPlugin<FlatChannelConfig> = {
124
108
  if (err) log?.error?.(`Failed to subscribe: ${err.message}`)
125
109
  })
126
110
 
111
+ const summaryEnabled = account.summary_enabled ?? DEFAULT_SUMMARY_ENABLED
112
+ const summaryMaxChars = account.summary_max_chars ?? DEFAULT_SUMMARY_MAX_CHARS
113
+
127
114
  client.on('message', (_topic, payload) => {
128
115
  let msg: InboundMessage
129
116
  try {
@@ -154,8 +141,9 @@ const folotoyChannel: ChannelPlugin<FlatChannelConfig> = {
154
141
  Provider: 'folotoy',
155
142
  })
156
143
 
157
- // dispatch and send finish message when done
144
+ // dispatch, optionally summarize, then send finish message
158
145
  void (async () => {
146
+ const replyChunks: string[] = []
159
147
  try {
160
148
  await channelRuntime.reply.dispatchReplyWithBufferedBlockDispatcher({
161
149
  ctx: inboundCtx,
@@ -163,17 +151,54 @@ const folotoyChannel: ChannelPlugin<FlatChannelConfig> = {
163
151
  dispatcherOptions: {
164
152
  deliver: async (replyPayload) => {
165
153
  if (!replyPayload.text) return
166
- order++
167
- const outMsg: OutboundMessage = {
168
- msgId,
169
- identifier: 'chat_output',
170
- outParams: { content: replyPayload.text, recording_id, order, is_finished: false },
171
- }
172
- client.publish(outboundTopic, JSON.stringify(outMsg))
154
+ replyChunks.push(replyPayload.text)
173
155
  },
174
156
  onError: (err) => log?.error?.(`Dispatch error: ${String(err)}`),
175
157
  },
176
158
  })
159
+
160
+ let finalText = replyChunks.join('')
161
+
162
+ // Summarize if enabled and text exceeds threshold
163
+ if (summaryEnabled && subagent && finalText.length > summaryMaxChars) {
164
+ try {
165
+ const sessionKey = `folotoy-summary-${accountId}-${Date.now()}`
166
+ const { runId } = await subagent.run({
167
+ sessionKey,
168
+ message: [
169
+ `You are an assistant that summarizes texts concisely while keeping the most important information.`,
170
+ `Summarize the text to approximately ${summaryMaxChars} characters.`,
171
+ `Maintain the original tone and style. Reply only with the summary, without additional explanations.`,
172
+ ``,
173
+ `<text_to_summarize>`,
174
+ finalText,
175
+ `</text_to_summarize>`,
176
+ ].join('\n'),
177
+ deliver: false,
178
+ })
179
+ const result = await subagent.waitForRun({ runId, timeoutMs: 30_000 })
180
+ if (result.status === 'ok') {
181
+ const { messages } = await subagent.getSessionMessages({ sessionKey, limit: 1 })
182
+ const lastMsg = messages[messages.length - 1] as { content?: string; text?: string } | undefined
183
+ const summaryText = lastMsg?.content ?? lastMsg?.text
184
+ if (summaryText) finalText = summaryText
185
+ }
186
+ await subagent.deleteSession({ sessionKey }).catch(() => {})
187
+ } catch (err) {
188
+ log?.warn?.(`Summary failed, truncating text: ${String(err)}`)
189
+ finalText = `${finalText.slice(0, summaryMaxChars - 3)}...`
190
+ }
191
+ }
192
+
193
+ if (finalText) {
194
+ order++
195
+ const outMsg: OutboundMessage = {
196
+ msgId,
197
+ identifier: 'chat_output',
198
+ outParams: { content: finalText, recording_id, order, is_finished: false },
199
+ }
200
+ client.publish(outboundTopic, JSON.stringify(outMsg))
201
+ }
177
202
  } finally {
178
203
  order++
179
204
  const finishMsg: OutboundMessage = {
@@ -239,5 +264,6 @@ export function sendNotification({ text, accountId }: { text: string; accountId?
239
264
  }
240
265
 
241
266
  export default (api: OpenClawPluginApi) => {
267
+ subagent = api.runtime.subagent
242
268
  api.registerChannel({ plugin: folotoyChannel })
243
269
  }
package/src/mqtt.ts CHANGED
@@ -56,17 +56,32 @@ export function buildNotificationTopic(toy_sn: string): string {
56
56
  return `/openapi/folotoy/${toy_sn}/thing/event/post`
57
57
  }
58
58
 
59
+ const INITIAL_RECONNECT_MS = 1000
60
+ const MAX_RECONNECT_MS = 60000
61
+
59
62
  export async function createMqttClient(config: PluginConfig, credentials: MqttCredentials): Promise<MqttClient> {
60
63
  const { host, port } = config.mqtt
61
64
  const { username, password } = credentials
62
65
 
63
66
  return new Promise((resolve, reject) => {
64
- const clientId = `openapi:${username}`
67
+ const clientId = `openapi:${credentials.toy_sn}`
65
68
  const client = mqtt.connect(`mqtt://${host}:${port}`, {
66
69
  clientId,
67
- username: `openapi:${username}`,
70
+ username,
68
71
  password,
69
72
  clean: true,
73
+ reconnectPeriod: INITIAL_RECONNECT_MS,
74
+ })
75
+
76
+ // Exponential backoff: increase reconnectPeriod on each failed attempt,
77
+ // reset on successful connection.
78
+ client.on('reconnect', () => {
79
+ const current = client.options.reconnectPeriod ?? INITIAL_RECONNECT_MS
80
+ client.options.reconnectPeriod = Math.min(current * 2, MAX_RECONNECT_MS)
81
+ })
82
+
83
+ client.on('connect', () => {
84
+ client.options.reconnectPeriod = INITIAL_RECONNECT_MS
70
85
  })
71
86
 
72
87
  client.once('connect', () => resolve(client))
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Soothing acknowledgment replies for FoloToy.
3
+ *
4
+ * 12 intent categories, 5 candidates each — one is chosen at random.
5
+ * Principles: convey "I heard you + I'm working on it", no time promises.
6
+ */
7
+
8
+ const INTENT_RULES: Array<{ pattern: RegExp; candidates: string[] }> = [
9
+ {
10
+ // 1. 难过类
11
+ pattern: /难过|伤心|哭|不开心|委屈/,
12
+ candidates: [
13
+ '抱抱你,我在认真听呢,让我想想怎么说...',
14
+ '别难过,我陪着你,让我想一会儿...',
15
+ '嗯,我听到了,给我点时间好好想想怎么帮你',
16
+ '你说的我都记住了,让我想想...',
17
+ '我在认真想呢,你先缓缓',
18
+ ],
19
+ },
20
+ {
21
+ // 2. 生气类
22
+ pattern: /生气|愤怒|烦|讨厌|气死/,
23
+ candidates: [
24
+ '我理解你,让我想想怎么帮你...',
25
+ '消消气,我在认真想办法呢',
26
+ '听到了,让我好好想一下...',
27
+ '别气别气,我来帮你想想,给我一点时间',
28
+ '嗯,你说的我记住了,我想一下',
29
+ ],
30
+ },
31
+ {
32
+ // 3. 累/怕类
33
+ pattern: /累|疲|困|害怕|怕/,
34
+ candidates: [
35
+ '辛苦了,让我想想怎么帮你...',
36
+ '别怕,有我在呢,我想一下',
37
+ '你先歇歇,让我帮你想想办法...',
38
+ '我在想办法呢,你先放松一下',
39
+ '嗯嗯,我在认真想怎么帮你...',
40
+ ],
41
+ },
42
+ {
43
+ // 4. 故事类
44
+ pattern: /故事|story/i,
45
+ candidates: [
46
+ '让我好好想一个故事...',
47
+ '嗯,我在想一个好听的故事呢',
48
+ '让我想想讲什么好...',
49
+ '我在脑子里翻故事书呢,等等我',
50
+ '故事嘛,得好好想一个才行...',
51
+ ],
52
+ },
53
+ {
54
+ // 5. 唱歌/音乐类
55
+ pattern: /唱|歌|音乐|播放|儿歌/,
56
+ candidates: [
57
+ '让我想想唱什么好...',
58
+ '嗯,我在选歌呢,等我一下',
59
+ '我清清嗓子,想想唱什么...',
60
+ '让我找一首好听的...',
61
+ '好嘞,让我想想唱哪首',
62
+ ],
63
+ },
64
+ {
65
+ // 6. 笑话类
66
+ pattern: /笑话|joke|搞笑|好笑/i,
67
+ candidates: [
68
+ '让我想一个好笑的...',
69
+ '嗯,我在想笑话呢,先憋住',
70
+ '好嘞,让我想一个逗你乐的...',
71
+ '笑话嘛,得想一个真正好笑的才行...',
72
+ '让我翻翻我的笑话本...',
73
+ ],
74
+ },
75
+ {
76
+ // 7. 知识/古诗类
77
+ pattern: /为什么|怎么|什么是|古诗|诗|背/,
78
+ candidates: [
79
+ '好问题,让我好好想想...',
80
+ '嗯,这个问题我得想一下',
81
+ '让我仔细想想怎么跟你解释...',
82
+ '这个嘛,让我想一想...',
83
+ '嗯,让我好好想想这个',
84
+ ],
85
+ },
86
+ {
87
+ // 8. 继续/追问类
88
+ pattern: /继续|接着|然后呢|后来|下一个/,
89
+ candidates: [
90
+ '让我接着往下想...',
91
+ '嗯,后面的我得好好想想',
92
+ '让我理理思路...',
93
+ '好嘞,让我想想后面怎么接',
94
+ '我在想呢,等一下',
95
+ ],
96
+ },
97
+ {
98
+ // 9. 打招呼/呼唤类
99
+ pattern: /你好|嗨|hello|hi\b|hey|哈喽/i,
100
+ candidates: [
101
+ '来啦,让我想想说什么...',
102
+ '嘿,你来啦,让我想想...',
103
+ '我在呢,让我想想跟你说什么好',
104
+ '哎,来了,给我一点时间想想',
105
+ '哈,你来啦,让我想一下...',
106
+ ],
107
+ },
108
+ {
109
+ // 10. 告别/睡觉类
110
+ pattern: /关机|睡|拜拜|再见|bye|晚安/i,
111
+ candidates: ['好嘞~', '嗯嗯~', '好的~', '哦~', '嗯~'],
112
+ },
113
+ {
114
+ // 11. 配网/技术类
115
+ pattern: /网络|配网|wifi|蓝牙|连接/i,
116
+ candidates: [
117
+ '让我想想怎么帮你...',
118
+ '嗯,这个我得想一下',
119
+ '让我看看怎么办...',
120
+ '我在想呢,等一下',
121
+ '嗯,给我一点时间想想',
122
+ ],
123
+ },
124
+ {
125
+ // 12. 查询类
126
+ pattern: /天气|新闻|时间|几点|日期|查|搜/,
127
+ candidates: [
128
+ '我去帮你查一查...',
129
+ '好的,我来查一下',
130
+ '稍等,我帮你看看',
131
+ '我去查一下,等我一秒',
132
+ '让我来查查看',
133
+ ],
134
+ },
135
+ ]
136
+
137
+ const DEFAULT_CANDIDATES = [
138
+ '让我想一想...',
139
+ '嗯嗯,让我想想怎么回答你',
140
+ '好嘞,我想一下',
141
+ '嗯,给我一点时间想想',
142
+ '让我好好想想...',
143
+ ]
144
+
145
+ function pickRandom<T>(arr: T[]): T {
146
+ return arr[Math.floor(Math.random() * arr.length)] as T
147
+ }
148
+
149
+ /** Returns one randomly chosen soothing reply matching the input intent. */
150
+ export function pickSoothingReply(text: string): string {
151
+ for (const rule of INTENT_RULES) {
152
+ if (rule.pattern.test(text)) {
153
+ return pickRandom(rule.candidates)
154
+ }
155
+ }
156
+ return pickRandom(DEFAULT_CANDIDATES)
157
+ }