@folotoy/folotoy-openclaw-plugin 0.5.7 → 0.6.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/dist/config.d.ts CHANGED
@@ -27,11 +27,21 @@ export type FlatChannelConfig = {
27
27
  mqtt_port?: number;
28
28
  summary_enabled?: boolean;
29
29
  summary_max_chars?: number;
30
+ sentence_split_enabled?: boolean;
31
+ sentence_split_delimiters?: string;
32
+ soothing_loop_enabled?: boolean;
33
+ soothing_loop_interval_ms?: number;
34
+ soothing_loop_max_count?: number;
30
35
  };
31
36
  export declare const DEFAULT_API_URL = "https://api.folotoy.cn";
32
37
  export declare const DEFAULT_MQTT_HOST: string;
33
38
  export declare const DEFAULT_MQTT_PORT = 1883;
34
39
  export declare const DEFAULT_SUMMARY_ENABLED = true;
35
40
  export declare const DEFAULT_SUMMARY_MAX_CHARS = 200;
41
+ export declare const DEFAULT_SENTENCE_SPLIT_ENABLED = true;
42
+ export declare const DEFAULT_SENTENCE_SPLIT_DELIMITERS = "\uFF01\u3002\uFF1F\uFF1B!.?;~";
43
+ export declare const DEFAULT_SOOTHING_LOOP_ENABLED = true;
44
+ export declare const DEFAULT_SOOTHING_LOOP_INTERVAL_MS = 200;
45
+ export declare const DEFAULT_SOOTHING_LOOP_MAX_COUNT = 3;
36
46
  export declare function flatToPluginConfig(flat: FlatChannelConfig): PluginConfig;
37
47
  //# sourceMappingURL=config.d.ts.map
@@ -1 +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"}
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;IAC1B,sBAAsB,CAAC,EAAE,OAAO,CAAA;IAChC,yBAAyB,CAAC,EAAE,MAAM,CAAA;IAClC,qBAAqB,CAAC,EAAE,OAAO,CAAA;IAC/B,yBAAyB,CAAC,EAAE,MAAM,CAAA;IAClC,uBAAuB,CAAC,EAAE,MAAM,CAAA;CACjC,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;AAC5C,eAAO,MAAM,8BAA8B,OAAO,CAAA;AAClD,eAAO,MAAM,iCAAiC,kCAAc,CAAA;AAC5D,eAAO,MAAM,6BAA6B,OAAO,CAAA;AACjD,eAAO,MAAM,iCAAiC,MAAM,CAAA;AACpD,eAAO,MAAM,+BAA+B,IAAI,CAAA;AAEhD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,iBAAiB,GAAG,YAAY,CAaxE"}
package/dist/config.js CHANGED
@@ -3,6 +3,11 @@ export const DEFAULT_MQTT_HOST = process.env.FOLOTOY_MQTT_HOST ?? 'f.qrc92.cn';
3
3
  export const DEFAULT_MQTT_PORT = 1883;
4
4
  export const DEFAULT_SUMMARY_ENABLED = true;
5
5
  export const DEFAULT_SUMMARY_MAX_CHARS = 200;
6
+ export const DEFAULT_SENTENCE_SPLIT_ENABLED = true;
7
+ export const DEFAULT_SENTENCE_SPLIT_DELIMITERS = '!。?;!.?;~';
8
+ export const DEFAULT_SOOTHING_LOOP_ENABLED = true;
9
+ export const DEFAULT_SOOTHING_LOOP_INTERVAL_MS = 200;
10
+ export const DEFAULT_SOOTHING_LOOP_MAX_COUNT = 3;
6
11
  export function flatToPluginConfig(flat) {
7
12
  const flow = flat.flow ?? 'direct';
8
13
  const auth = flow === 'api'
@@ -1 +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"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAuCA,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;AAC5C,MAAM,CAAC,MAAM,8BAA8B,GAAG,IAAI,CAAA;AAClD,MAAM,CAAC,MAAM,iCAAiC,GAAG,WAAW,CAAA;AAC5D,MAAM,CAAC,MAAM,6BAA6B,GAAG,IAAI,CAAA;AACjD,MAAM,CAAC,MAAM,iCAAiC,GAAG,GAAG,CAAA;AACpD,MAAM,CAAC,MAAM,+BAA+B,GAAG,CAAC,CAAA;AAEhD,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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAgC,MAAM,0BAA0B,CAAA;AA8S/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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAgC,MAAM,0BAA0B,CAAA;AAyb/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 CHANGED
@@ -1,6 +1,7 @@
1
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';
2
+ import { createSoothingPicker } from './soothing.js';
3
+ import { stripMarkdown } from './strip-markdown.js';
4
+ import { DEFAULT_MQTT_HOST, DEFAULT_MQTT_PORT, DEFAULT_SUMMARY_ENABLED, DEFAULT_SUMMARY_MAX_CHARS, DEFAULT_SENTENCE_SPLIT_ENABLED, DEFAULT_SENTENCE_SPLIT_DELIMITERS, DEFAULT_SOOTHING_LOOP_ENABLED, DEFAULT_SOOTHING_LOOP_INTERVAL_MS, DEFAULT_SOOTHING_LOOP_MAX_COUNT, flatToPluginConfig } from './config.js';
4
5
  // Per-account MQTT clients and msgId counters
5
6
  const activeClients = new Map();
6
7
  // Subagent reference, set at plugin registration
@@ -30,6 +31,11 @@ const folotoyChannel = {
30
31
  mqtt_port: { type: 'number', default: DEFAULT_MQTT_PORT },
31
32
  summary_enabled: { type: 'boolean', default: DEFAULT_SUMMARY_ENABLED },
32
33
  summary_max_chars: { type: 'number', default: DEFAULT_SUMMARY_MAX_CHARS },
34
+ sentence_split_enabled: { type: 'boolean', default: DEFAULT_SENTENCE_SPLIT_ENABLED },
35
+ sentence_split_delimiters: { type: 'string', default: DEFAULT_SENTENCE_SPLIT_DELIMITERS },
36
+ soothing_loop_enabled: { type: 'boolean', default: DEFAULT_SOOTHING_LOOP_ENABLED },
37
+ soothing_loop_interval_ms: { type: 'number', default: DEFAULT_SOOTHING_LOOP_INTERVAL_MS },
38
+ soothing_loop_max_count: { type: 'number', default: DEFAULT_SOOTHING_LOOP_MAX_COUNT },
33
39
  },
34
40
  },
35
41
  uiHints: {
@@ -42,6 +48,11 @@ const folotoyChannel = {
42
48
  mqtt_port: { label: 'MQTT Port' },
43
49
  summary_enabled: { label: 'Enable Summary' },
44
50
  summary_max_chars: { label: 'Summary Max Characters' },
51
+ sentence_split_enabled: { label: 'Enable Sentence Splitting' },
52
+ sentence_split_delimiters: { label: 'Sentence Delimiters' },
53
+ soothing_loop_enabled: { label: 'Enable Soothing Loop' },
54
+ soothing_loop_interval_ms: { label: 'Soothing Loop Interval (ms)' },
55
+ soothing_loop_max_count: { label: 'Soothing Loop Max Count' },
45
56
  },
46
57
  },
47
58
  config: {
@@ -86,6 +97,11 @@ const folotoyChannel = {
86
97
  });
87
98
  const summaryEnabled = account.summary_enabled ?? DEFAULT_SUMMARY_ENABLED;
88
99
  const summaryMaxChars = account.summary_max_chars ?? DEFAULT_SUMMARY_MAX_CHARS;
100
+ const sentenceSplitEnabled = account.sentence_split_enabled ?? DEFAULT_SENTENCE_SPLIT_ENABLED;
101
+ const sentenceSplitDelimiters = account.sentence_split_delimiters ?? DEFAULT_SENTENCE_SPLIT_DELIMITERS;
102
+ const soothingLoopEnabled = account.soothing_loop_enabled ?? DEFAULT_SOOTHING_LOOP_ENABLED;
103
+ const soothingLoopIntervalMs = account.soothing_loop_interval_ms ?? DEFAULT_SOOTHING_LOOP_INTERVAL_MS;
104
+ const soothingLoopMaxCount = account.soothing_loop_max_count ?? DEFAULT_SOOTHING_LOOP_MAX_COUNT;
89
105
  client.on('message', (_topic, payload) => {
90
106
  let msg;
91
107
  try {
@@ -98,12 +114,14 @@ const folotoyChannel = {
98
114
  return;
99
115
  const { msgId, inputParams: { text, recording_id } } = msg;
100
116
  let order = 1;
117
+ // Create a per-message picker that cycles through candidates without repeats
118
+ const soothingPick = createSoothingPicker(text);
101
119
  // Send a quick soothing acknowledgment before AI processing (order=1).
102
120
  // AI replies continue from order=2.
103
121
  const ackMsg = {
104
122
  msgId,
105
123
  identifier: 'chat_output',
106
- outParams: { content: pickSoothingReply(text), recording_id, order, is_finished: false },
124
+ outParams: { content: soothingPick(), recording_id, order, is_finished: false },
107
125
  };
108
126
  client.publish(outboundTopic, JSON.stringify(ackMsg));
109
127
  const peer = { kind: 'direct', id: credentials.toy_sn };
@@ -127,13 +145,120 @@ const folotoyChannel = {
127
145
  });
128
146
  // dispatch using dispatchReplyFromConfig (full agent capabilities including tools)
129
147
  void (async () => {
130
- const replyChunks = [];
148
+ // Sentence-level streaming: buffer incoming text and flush complete
149
+ // sentences (delimited by punctuation) to the toy immediately.
150
+ const sentenceDelimiterRe = sentenceSplitEnabled
151
+ ? new RegExp(`[${sentenceSplitDelimiters.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&')}]`)
152
+ : null;
153
+ let sentenceBuffer = '';
154
+ let totalSentChars = 0;
155
+ let firstSentenceFlushed = false;
156
+ let llmStarted = false;
157
+ // Send soothing replies while waiting for the LLM to start responding.
158
+ // Once any LLM chunk arrives, stop immediately. Max 5 soothing messages.
159
+ let soothingTimer = null;
160
+ let soothingCount = 0;
161
+ const stopSoothingLoop = () => {
162
+ if (soothingTimer) {
163
+ clearTimeout(soothingTimer);
164
+ soothingTimer = null;
165
+ }
166
+ };
167
+ const resetSoothingTimer = () => {
168
+ if (!soothingLoopEnabled || llmStarted || soothingCount >= soothingLoopMaxCount)
169
+ return;
170
+ if (soothingTimer)
171
+ clearTimeout(soothingTimer);
172
+ soothingTimer = setTimeout(() => {
173
+ if (llmStarted || soothingCount >= soothingLoopMaxCount)
174
+ return;
175
+ soothingCount++;
176
+ order++;
177
+ const extraAck = {
178
+ msgId,
179
+ identifier: 'chat_output',
180
+ outParams: { content: soothingPick(), recording_id, order, is_finished: false },
181
+ };
182
+ client.publish(outboundTopic, JSON.stringify(extraAck));
183
+ // Restart timer in case LLM stays silent even longer
184
+ resetSoothingTimer();
185
+ }, soothingLoopIntervalMs);
186
+ };
187
+ resetSoothingTimer();
188
+ // First sentence: split purely by punctuation (fast first response).
189
+ // Subsequent sentences: require a minimum length that grows with each
190
+ // sentence (base 20 chars, +10 per sentence), so later chunks are longer.
191
+ let sentenceCount = 0;
192
+ const minLenForSentence = () => {
193
+ if (sentenceCount === 0)
194
+ return 0; // first sentence: no minimum
195
+ return Math.min(100, 20 + (sentenceCount - 1) * 10);
196
+ };
197
+ const FORCE_FLUSH_LEN = 200; // force flush if buffer has no delimiter for this many chars
198
+ const publishSentence = (sentence) => {
199
+ sentenceCount++;
200
+ if (!firstSentenceFlushed)
201
+ firstSentenceFlushed = true;
202
+ order++;
203
+ totalSentChars += sentence.length;
204
+ const outMsg = {
205
+ msgId,
206
+ identifier: 'chat_output',
207
+ outParams: { content: sentence, recording_id, order, is_finished: false },
208
+ };
209
+ client.publish(outboundTopic, JSON.stringify(outMsg));
210
+ };
211
+ const flushSentences = () => {
212
+ if (!sentenceDelimiterRe)
213
+ return;
214
+ while (true) {
215
+ const minLen = minLenForSentence();
216
+ // Search for a delimiter that is at or beyond the minimum length
217
+ let idx = -1;
218
+ const searchFrom = Math.max(0, minLen - 1);
219
+ if (searchFrom < sentenceBuffer.length) {
220
+ const sub = sentenceBuffer.slice(searchFrom);
221
+ const match = sub.search(sentenceDelimiterRe);
222
+ if (match !== -1)
223
+ idx = searchFrom + match;
224
+ }
225
+ if (idx !== -1) {
226
+ const sentence = sentenceBuffer.slice(0, idx + 1).trim();
227
+ sentenceBuffer = sentenceBuffer.slice(idx + 1);
228
+ if (sentence)
229
+ publishSentence(sentence);
230
+ continue;
231
+ }
232
+ // No delimiter found — force flush if buffer exceeds limit
233
+ if (sentenceBuffer.length >= FORCE_FLUSH_LEN) {
234
+ // Try to break at the last space/comma for a cleaner cut
235
+ let cutIdx = FORCE_FLUSH_LEN;
236
+ const lastSpace = sentenceBuffer.lastIndexOf(' ', FORCE_FLUSH_LEN);
237
+ const lastComma = sentenceBuffer.lastIndexOf(',', FORCE_FLUSH_LEN);
238
+ const lastBreak = Math.max(lastSpace, lastComma);
239
+ if (lastBreak > FORCE_FLUSH_LEN / 2)
240
+ cutIdx = lastBreak + 1;
241
+ const sentence = sentenceBuffer.slice(0, cutIdx).trim();
242
+ sentenceBuffer = sentenceBuffer.slice(cutIdx);
243
+ if (sentence)
244
+ publishSentence(sentence);
245
+ continue;
246
+ }
247
+ break;
248
+ }
249
+ };
131
250
  const dispatcher = {
132
251
  sendToolResult: () => true,
133
252
  sendBlockReply: () => true,
134
253
  sendFinalReply: (payload) => {
135
- if (payload.text)
136
- replyChunks.push(payload.text);
254
+ if (payload.text) {
255
+ if (!llmStarted) {
256
+ llmStarted = true;
257
+ stopSoothingLoop();
258
+ }
259
+ sentenceBuffer += stripMarkdown(payload.text);
260
+ flushSentences();
261
+ }
137
262
  return true;
138
263
  },
139
264
  waitForIdle: async () => { },
@@ -149,50 +274,56 @@ const folotoyChannel = {
149
274
  dispatcher,
150
275
  }),
151
276
  });
152
- let finalText = replyChunks.join('');
153
- // Summarize if enabled and text exceeds threshold
154
- if (summaryEnabled && subagent && finalText.length > summaryMaxChars) {
155
- try {
156
- const sessionKey = `folotoy-summary-${accountId}-${Date.now()}`;
157
- const { runId } = await subagent.run({
158
- sessionKey,
159
- message: [
160
- `You are an assistant that summarizes texts concisely while keeping the most important information.`,
161
- `Summarize the text to approximately ${summaryMaxChars} characters.`,
162
- `Maintain the original tone and style. Reply only with the summary, without additional explanations.`,
163
- ``,
164
- `<text_to_summarize>`,
165
- finalText,
166
- `</text_to_summarize>`,
167
- ].join('\n'),
168
- deliver: false,
169
- });
170
- const result = await subagent.waitForRun({ runId, timeoutMs: 30_000 });
171
- if (result.status === 'ok') {
172
- const { messages } = await subagent.getSessionMessages({ sessionKey, limit: 1 });
173
- const lastMsg = messages[messages.length - 1];
174
- const summaryText = lastMsg?.content ?? lastMsg?.text;
175
- if (summaryText)
176
- finalText = summaryText;
277
+ // Flush remaining buffer (text after the last punctuation)
278
+ let remaining = sentenceBuffer.trim();
279
+ if (remaining) {
280
+ // Summarize remaining text if enabled and total response exceeds threshold
281
+ if (summaryEnabled && subagent && (totalSentChars + remaining.length) > summaryMaxChars) {
282
+ try {
283
+ const summarySessionKey = `folotoy-summary-${accountId}-${Date.now()}`;
284
+ const { runId } = await subagent.run({
285
+ sessionKey: summarySessionKey,
286
+ message: [
287
+ `You are an assistant that summarizes texts concisely while keeping the most important information.`,
288
+ `Summarize the text to approximately ${Math.max(50, summaryMaxChars - totalSentChars)} characters.`,
289
+ `Maintain the original tone and style. Reply only with the summary, without additional explanations.`,
290
+ ``,
291
+ `<text_to_summarize>`,
292
+ remaining,
293
+ `</text_to_summarize>`,
294
+ ].join('\n'),
295
+ deliver: false,
296
+ });
297
+ const result = await subagent.waitForRun({ runId, timeoutMs: 30_000 });
298
+ if (result.status === 'ok') {
299
+ const { messages } = await subagent.getSessionMessages({ sessionKey: summarySessionKey, limit: 1 });
300
+ const lastMsg = messages[messages.length - 1];
301
+ const summaryText = lastMsg?.content ?? lastMsg?.text;
302
+ if (summaryText)
303
+ remaining = summaryText;
304
+ }
305
+ await subagent.deleteSession({ sessionKey: summarySessionKey }).catch(() => { });
306
+ }
307
+ catch (err) {
308
+ log?.warn?.(`Summary failed, truncating text: ${String(err)}`);
309
+ const maxRemaining = Math.max(50, summaryMaxChars - totalSentChars);
310
+ remaining = `${remaining.slice(0, maxRemaining - 3)}...`;
177
311
  }
178
- await subagent.deleteSession({ sessionKey }).catch(() => { });
179
- }
180
- catch (err) {
181
- log?.warn?.(`Summary failed, truncating text: ${String(err)}`);
182
- finalText = `${finalText.slice(0, summaryMaxChars - 3)}...`;
183
312
  }
184
- }
185
- if (finalText) {
186
313
  order++;
187
314
  const outMsg = {
188
315
  msgId,
189
316
  identifier: 'chat_output',
190
- outParams: { content: finalText, recording_id, order, is_finished: false },
317
+ outParams: { content: remaining, recording_id, order, is_finished: false },
191
318
  };
192
319
  client.publish(outboundTopic, JSON.stringify(outMsg));
193
320
  }
194
321
  }
195
322
  finally {
323
+ if (soothingTimer) {
324
+ clearTimeout(soothingTimer);
325
+ soothingTimer = null;
326
+ }
196
327
  order++;
197
328
  const finishMsg = {
198
329
  msgId,
@@ -222,9 +353,26 @@ const folotoyChannel = {
222
353
  const folotoy = cfg
223
354
  ?.channels?.folotoy;
224
355
  const sn = folotoy?.toy_sn ?? '<toy_sn>';
356
+ const now = new Date();
357
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
358
+ const tzOffsetMin = -now.getTimezoneOffset();
359
+ const tzSign = tzOffsetMin >= 0 ? '+' : '-';
360
+ const tzH = String(Math.floor(Math.abs(tzOffsetMin) / 60)).padStart(2, '0');
361
+ const tzM = String(Math.abs(tzOffsetMin) % 60).padStart(2, '0');
362
+ const tzSuffix = `${tzSign}${tzH}:${tzM}`;
363
+ const pad = (n) => String(n).padStart(2, '0');
364
+ const nowLocal = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}${tzSuffix}`;
225
365
  return [
226
- `[FoloToy] This message is from a FoloToy toy (SN: ${sn}).`,
227
- `To set reminders/timers, use the cron tool with: sessionTarget="isolated", payload.kind="agentTurn", delivery.mode="announce", delivery.channel="folotoy", delivery.to="${sn}", delivery.accountId="default".`,
366
+ `[FoloToy] This message is from a FoloToy toy (SN: ${sn}). Current time: ${nowLocal} (${tz}). IMPORTANT: Your reply will be converted to speech (TTS). Use plain text only — no markdown, no bullet points, no numbered lists, no code blocks, no URLs. Write in a natural, conversational tone.`,
367
+ [
368
+ `To set reminders/timers, use the cron tool with action="add". IMPORTANT:`,
369
+ `- schedule.at MUST use the same timezone offset as current time (${tzSuffix}), NEVER use "Z"`,
370
+ `- For "N分钟后" reminders, add N minutes to current time ${nowLocal}`,
371
+ `- payload.kind MUST be "systemEvent" with a "text" field containing the reminder message`,
372
+ `- sessionTarget MUST be "isolated"`,
373
+ `- delivery MUST be: {"mode":"announce","channel":"folotoy","to":"${sn}","accountId":"default"}`,
374
+ `Example: {"action":"add","job":{"name":"喝水提醒","schedule":{"kind":"at","at":"2026-03-27T21:03:00${tzSuffix}"},"payload":{"kind":"systemEvent","text":"时间到啦,该喝水了!"},"sessionTarget":"isolated","delivery":{"mode":"announce","channel":"folotoy","to":"${sn}","accountId":"default"},"enabled":true}}`,
375
+ ].join('\n'),
228
376
  ];
229
377
  },
230
378
  },
@@ -255,7 +403,7 @@ const folotoyChannel = {
255
403
  const notifMsg = {
256
404
  msgId,
257
405
  identifier: 'send_notification',
258
- outParams: { text },
406
+ outParams: { text: stripMarkdown(text) },
259
407
  };
260
408
  entry.client.publish(notificationTopic, JSON.stringify(notifMsg));
261
409
  return { channel: 'folotoy', messageId: String(msgId) };
@@ -272,7 +420,7 @@ export function sendNotification({ text, accountId }) {
272
420
  const notifMsg = {
273
421
  msgId,
274
422
  identifier: 'send_notification',
275
- outParams: { text },
423
+ outParams: { text: stripMarkdown(text) },
276
424
  };
277
425
  entry.client.publish(notificationTopic, JSON.stringify(notifMsg));
278
426
  return { channel: 'folotoy', messageId: String(msgId) };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -4,6 +4,16 @@
4
4
  * 12 intent categories, 5 candidates each — one is chosen at random.
5
5
  * Principles: convey "I heard you + I'm working on it", no time promises.
6
6
  */
7
+ /**
8
+ * Creates a soothing reply picker that guarantees no repeats until all
9
+ * candidates in the matched category are exhausted, then reshuffles.
10
+ *
11
+ * Usage:
12
+ * const picker = createSoothingPicker(text)
13
+ * picker() // first reply (no repeat)
14
+ * picker() // second reply (different from first)
15
+ */
16
+ export declare function createSoothingPicker(text?: string): () => string;
7
17
  /** Returns one randomly chosen soothing reply matching the input intent. */
8
- export declare function pickSoothingReply(text: string): string;
18
+ export declare function pickSoothingReply(text?: string): string;
9
19
  //# sourceMappingURL=soothing.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"soothing.d.ts","sourceRoot":"","sources":["../src/soothing.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAoJH;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,MAAM,CAmBhE;AAED,4EAA4E;AAC5E,wBAAgB,iBAAiB,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAEvD"}
package/dist/soothing.js CHANGED
@@ -139,16 +139,44 @@ const DEFAULT_CANDIDATES = [
139
139
  '嗯,给我一点时间想想',
140
140
  '让我好好想想...',
141
141
  ];
142
- function pickRandom(arr) {
143
- return arr[Math.floor(Math.random() * arr.length)];
142
+ /** Fisher-Yates shuffle (in-place) */
143
+ function shuffle(arr) {
144
+ for (let i = arr.length - 1; i > 0; i--) {
145
+ const j = Math.floor(Math.random() * (i + 1));
146
+ [arr[i], arr[j]] = [arr[j], arr[i]];
147
+ }
148
+ return arr;
144
149
  }
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
+ * Creates a soothing reply picker that guarantees no repeats until all
152
+ * candidates in the matched category are exhausted, then reshuffles.
153
+ *
154
+ * Usage:
155
+ * const picker = createSoothingPicker(text)
156
+ * picker() // first reply (no repeat)
157
+ * picker() // second reply (different from first)
158
+ */
159
+ export function createSoothingPicker(text) {
160
+ let candidates;
161
+ if (text) {
162
+ for (const rule of INTENT_RULES) {
163
+ if (rule.pattern.test(text)) {
164
+ candidates = rule.candidates;
165
+ break;
166
+ }
150
167
  }
151
168
  }
152
- return pickRandom(DEFAULT_CANDIDATES);
169
+ const pool = candidates ?? DEFAULT_CANDIDATES;
170
+ let queue = [];
171
+ return () => {
172
+ if (queue.length === 0) {
173
+ queue = shuffle([...pool]);
174
+ }
175
+ return queue.pop();
176
+ };
177
+ }
178
+ /** Returns one randomly chosen soothing reply matching the input intent. */
179
+ export function pickSoothingReply(text) {
180
+ return createSoothingPicker(text)();
153
181
  }
154
182
  //# sourceMappingURL=soothing.js.map
@@ -1 +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"}
1
+ {"version":3,"file":"soothing.js","sourceRoot":"","sources":["../src/soothing.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Strip Markdown / rich-text formatting for TTS.
3
+ *
4
+ * Designed to be safe for streaming chunks — removes syntax characters
5
+ * individually rather than matching complete open/close patterns,
6
+ * so partial markdown across chunks is handled correctly.
7
+ * Plain text without markdown passes through unchanged.
8
+ */
9
+ export declare function stripMarkdown(text: string): string;
10
+ //# sourceMappingURL=strip-markdown.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"strip-markdown.d.ts","sourceRoot":"","sources":["../src/strip-markdown.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAmClD"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Strip Markdown / rich-text formatting for TTS.
3
+ *
4
+ * Designed to be safe for streaming chunks — removes syntax characters
5
+ * individually rather than matching complete open/close patterns,
6
+ * so partial markdown across chunks is handled correctly.
7
+ * Plain text without markdown passes through unchanged.
8
+ */
9
+ export function stripMarkdown(text) {
10
+ return (text
11
+ // Code blocks (``` ... ```) — remove fences and content when complete
12
+ .replace(/```[\s\S]*?```/g, '')
13
+ // Remaining fences (incomplete code block in a chunk)
14
+ .replace(/```/g, '')
15
+ // Inline code backticks
16
+ .replace(/`/g, '')
17
+ // Images ![alt](url) — keep alt text
18
+ .replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1')
19
+ // Links [text](url) — keep text
20
+ .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
21
+ // Bare URLs (https://...)
22
+ .replace(/https?:\/\/\S+/g, '')
23
+ // Heading markers
24
+ .replace(/^#{1,6}\s*/gm, '')
25
+ // Bold/italic/strikethrough markers (*, _, ~)
26
+ .replace(/[*_~]+/g, '')
27
+ // Blockquote markers
28
+ .replace(/^>\s?/gm, '')
29
+ // Horizontal rules (---, ***, ___)
30
+ .replace(/^[-]{3,}\s*$/gm, '')
31
+ // Unordered list markers (- / + at line start, * already removed above)
32
+ .replace(/^[\s]*[-+]\s+/gm, '')
33
+ // Ordered list markers (1. / 2. ...)
34
+ .replace(/^[\s]*\d+\.\s+/gm, '')
35
+ // HTML tags
36
+ .replace(/<[^>]+>/g, '')
37
+ // Collapse multiple spaces
38
+ .replace(/ {2,}/g, ' ')
39
+ // Collapse multiple blank lines
40
+ .replace(/\n{3,}/g, '\n\n')
41
+ .trim());
42
+ }
43
+ //# sourceMappingURL=strip-markdown.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"strip-markdown.js","sourceRoot":"","sources":["../src/strip-markdown.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,OAAO,CACL,IAAI;QACF,sEAAsE;SACrE,OAAO,CAAC,iBAAiB,EAAE,EAAE,CAAC;QAC/B,sDAAsD;SACrD,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;QACpB,wBAAwB;SACvB,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC;QAClB,qCAAqC;SACpC,OAAO,CAAC,yBAAyB,EAAE,IAAI,CAAC;QACzC,gCAAgC;SAC/B,OAAO,CAAC,wBAAwB,EAAE,IAAI,CAAC;QACxC,0BAA0B;SACzB,OAAO,CAAC,iBAAiB,EAAE,EAAE,CAAC;QAC/B,kBAAkB;SACjB,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC;QAC5B,8CAA8C;SAC7C,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC;QACvB,qBAAqB;SACpB,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC;QACvB,mCAAmC;SAClC,OAAO,CAAC,gBAAgB,EAAE,EAAE,CAAC;QAC9B,wEAAwE;SACvE,OAAO,CAAC,iBAAiB,EAAE,EAAE,CAAC;QAC/B,qCAAqC;SACpC,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC;QAChC,YAAY;SACX,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC;QACxB,2BAA2B;SAC1B,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC;QACvB,gCAAgC;SAC/B,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC;SAC1B,IAAI,EAAE,CACV,CAAA;AACH,CAAC"}
@@ -2,7 +2,7 @@
2
2
  "id": "folotoy-openclaw-plugin",
3
3
  "name": "FoloToy",
4
4
  "description": "Empower your FoloToy with OpenClaw AI capabilities.",
5
- "version": "0.5.7",
5
+ "version": "0.6.0",
6
6
  "channels": ["folotoy"],
7
7
  "configSchema": {
8
8
  "type": "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@folotoy/folotoy-openclaw-plugin",
3
- "version": "0.5.7",
3
+ "version": "0.6.0",
4
4
  "description": "Empower your FoloToy with OpenClaw AI capabilities.",
5
5
  "keywords": [
6
6
  "folotoy",
package/src/config.ts CHANGED
@@ -30,6 +30,11 @@ export type FlatChannelConfig = {
30
30
  mqtt_port?: number
31
31
  summary_enabled?: boolean
32
32
  summary_max_chars?: number
33
+ sentence_split_enabled?: boolean
34
+ sentence_split_delimiters?: string
35
+ soothing_loop_enabled?: boolean
36
+ soothing_loop_interval_ms?: number
37
+ soothing_loop_max_count?: number
33
38
  }
34
39
 
35
40
  export const DEFAULT_API_URL = 'https://api.folotoy.cn'
@@ -37,6 +42,11 @@ export const DEFAULT_MQTT_HOST = process.env.FOLOTOY_MQTT_HOST ?? 'f.qrc92.cn'
37
42
  export const DEFAULT_MQTT_PORT = 1883
38
43
  export const DEFAULT_SUMMARY_ENABLED = true
39
44
  export const DEFAULT_SUMMARY_MAX_CHARS = 200
45
+ export const DEFAULT_SENTENCE_SPLIT_ENABLED = true
46
+ export const DEFAULT_SENTENCE_SPLIT_DELIMITERS = '!。?;!.?;~'
47
+ export const DEFAULT_SOOTHING_LOOP_ENABLED = true
48
+ export const DEFAULT_SOOTHING_LOOP_INTERVAL_MS = 200
49
+ export const DEFAULT_SOOTHING_LOOP_MAX_COUNT = 3
40
50
 
41
51
  export function flatToPluginConfig(flat: FlatChannelConfig): PluginConfig {
42
52
  const flow = flat.flow ?? 'direct'
package/src/index.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import type { OpenClawPluginApi, ChannelPlugin, PluginRuntime } from 'openclaw/plugin-sdk/core'
2
2
  import { resolveCredentials, createMqttClient, buildInboundTopic, buildOutboundTopic, buildNotificationTopic } from './mqtt.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'
3
+ import { createSoothingPicker } from './soothing.js'
4
+ import { stripMarkdown } from './strip-markdown.js'
5
+ import { DEFAULT_MQTT_HOST, DEFAULT_MQTT_PORT, DEFAULT_SUMMARY_ENABLED, DEFAULT_SUMMARY_MAX_CHARS, DEFAULT_SENTENCE_SPLIT_ENABLED, DEFAULT_SENTENCE_SPLIT_DELIMITERS, DEFAULT_SOOTHING_LOOP_ENABLED, DEFAULT_SOOTHING_LOOP_INTERVAL_MS, DEFAULT_SOOTHING_LOOP_MAX_COUNT, flatToPluginConfig } from './config.js'
5
6
  import type { FlatChannelConfig } from './config.js'
6
7
  import type { MqttClient } from 'mqtt'
7
8
 
@@ -54,6 +55,11 @@ const folotoyChannel: ChannelPlugin<FlatChannelConfig> = {
54
55
  mqtt_port: { type: 'number', default: DEFAULT_MQTT_PORT },
55
56
  summary_enabled: { type: 'boolean', default: DEFAULT_SUMMARY_ENABLED },
56
57
  summary_max_chars: { type: 'number', default: DEFAULT_SUMMARY_MAX_CHARS },
58
+ sentence_split_enabled: { type: 'boolean', default: DEFAULT_SENTENCE_SPLIT_ENABLED },
59
+ sentence_split_delimiters: { type: 'string', default: DEFAULT_SENTENCE_SPLIT_DELIMITERS },
60
+ soothing_loop_enabled: { type: 'boolean', default: DEFAULT_SOOTHING_LOOP_ENABLED },
61
+ soothing_loop_interval_ms: { type: 'number', default: DEFAULT_SOOTHING_LOOP_INTERVAL_MS },
62
+ soothing_loop_max_count: { type: 'number', default: DEFAULT_SOOTHING_LOOP_MAX_COUNT },
57
63
  },
58
64
  },
59
65
  uiHints: {
@@ -66,6 +72,11 @@ const folotoyChannel: ChannelPlugin<FlatChannelConfig> = {
66
72
  mqtt_port: { label: 'MQTT Port' },
67
73
  summary_enabled: { label: 'Enable Summary' },
68
74
  summary_max_chars: { label: 'Summary Max Characters' },
75
+ sentence_split_enabled: { label: 'Enable Sentence Splitting' },
76
+ sentence_split_delimiters: { label: 'Sentence Delimiters' },
77
+ soothing_loop_enabled: { label: 'Enable Soothing Loop' },
78
+ soothing_loop_interval_ms: { label: 'Soothing Loop Interval (ms)' },
79
+ soothing_loop_max_count: { label: 'Soothing Loop Max Count' },
69
80
  },
70
81
  },
71
82
  config: {
@@ -115,6 +126,11 @@ const folotoyChannel: ChannelPlugin<FlatChannelConfig> = {
115
126
 
116
127
  const summaryEnabled = account.summary_enabled ?? DEFAULT_SUMMARY_ENABLED
117
128
  const summaryMaxChars = account.summary_max_chars ?? DEFAULT_SUMMARY_MAX_CHARS
129
+ const sentenceSplitEnabled = account.sentence_split_enabled ?? DEFAULT_SENTENCE_SPLIT_ENABLED
130
+ const sentenceSplitDelimiters = account.sentence_split_delimiters ?? DEFAULT_SENTENCE_SPLIT_DELIMITERS
131
+ const soothingLoopEnabled = account.soothing_loop_enabled ?? DEFAULT_SOOTHING_LOOP_ENABLED
132
+ const soothingLoopIntervalMs = account.soothing_loop_interval_ms ?? DEFAULT_SOOTHING_LOOP_INTERVAL_MS
133
+ const soothingLoopMaxCount = account.soothing_loop_max_count ?? DEFAULT_SOOTHING_LOOP_MAX_COUNT
118
134
 
119
135
  client.on('message', (_topic, payload) => {
120
136
  let msg: InboundMessage
@@ -128,12 +144,15 @@ const folotoyChannel: ChannelPlugin<FlatChannelConfig> = {
128
144
  const { msgId, inputParams: { text, recording_id } } = msg
129
145
  let order = 1
130
146
 
147
+ // Create a per-message picker that cycles through candidates without repeats
148
+ const soothingPick = createSoothingPicker(text)
149
+
131
150
  // Send a quick soothing acknowledgment before AI processing (order=1).
132
151
  // AI replies continue from order=2.
133
152
  const ackMsg: OutboundMessage = {
134
153
  msgId,
135
154
  identifier: 'chat_output',
136
- outParams: { content: pickSoothingReply(text), recording_id, order, is_finished: false },
155
+ outParams: { content: soothingPick(), recording_id, order, is_finished: false },
137
156
  }
138
157
  client.publish(outboundTopic, JSON.stringify(ackMsg))
139
158
 
@@ -160,12 +179,113 @@ const folotoyChannel: ChannelPlugin<FlatChannelConfig> = {
160
179
 
161
180
  // dispatch using dispatchReplyFromConfig (full agent capabilities including tools)
162
181
  void (async () => {
163
- const replyChunks: string[] = []
182
+ // Sentence-level streaming: buffer incoming text and flush complete
183
+ // sentences (delimited by punctuation) to the toy immediately.
184
+ const sentenceDelimiterRe = sentenceSplitEnabled
185
+ ? new RegExp(`[${sentenceSplitDelimiters.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&')}]`)
186
+ : null
187
+ let sentenceBuffer = ''
188
+ let totalSentChars = 0
189
+ let firstSentenceFlushed = false
190
+ let llmStarted = false
191
+
192
+ // Send soothing replies while waiting for the LLM to start responding.
193
+ // Once any LLM chunk arrives, stop immediately. Max 5 soothing messages.
194
+ let soothingTimer: ReturnType<typeof setTimeout> | null = null
195
+ let soothingCount = 0
196
+ const stopSoothingLoop = () => {
197
+ if (soothingTimer) { clearTimeout(soothingTimer); soothingTimer = null }
198
+ }
199
+ const resetSoothingTimer = () => {
200
+ if (!soothingLoopEnabled || llmStarted || soothingCount >= soothingLoopMaxCount) return
201
+ if (soothingTimer) clearTimeout(soothingTimer)
202
+ soothingTimer = setTimeout(() => {
203
+ if (llmStarted || soothingCount >= soothingLoopMaxCount) return
204
+ soothingCount++
205
+ order++
206
+ const extraAck: OutboundMessage = {
207
+ msgId,
208
+ identifier: 'chat_output',
209
+ outParams: { content: soothingPick(), recording_id, order, is_finished: false },
210
+ }
211
+ client.publish(outboundTopic, JSON.stringify(extraAck))
212
+ // Restart timer in case LLM stays silent even longer
213
+ resetSoothingTimer()
214
+ }, soothingLoopIntervalMs)
215
+ }
216
+ resetSoothingTimer()
217
+
218
+ // First sentence: split purely by punctuation (fast first response).
219
+ // Subsequent sentences: require a minimum length that grows with each
220
+ // sentence (base 20 chars, +10 per sentence), so later chunks are longer.
221
+ let sentenceCount = 0
222
+ const minLenForSentence = () => {
223
+ if (sentenceCount === 0) return 0 // first sentence: no minimum
224
+ return Math.min(100, 20 + (sentenceCount - 1) * 10)
225
+ }
226
+
227
+ const FORCE_FLUSH_LEN = 200 // force flush if buffer has no delimiter for this many chars
228
+
229
+ const publishSentence = (sentence: string) => {
230
+ sentenceCount++
231
+ if (!firstSentenceFlushed) firstSentenceFlushed = true
232
+ order++
233
+ totalSentChars += sentence.length
234
+ const outMsg: OutboundMessage = {
235
+ msgId,
236
+ identifier: 'chat_output',
237
+ outParams: { content: sentence, recording_id, order, is_finished: false },
238
+ }
239
+ client.publish(outboundTopic, JSON.stringify(outMsg))
240
+ }
241
+
242
+ const flushSentences = () => {
243
+ if (!sentenceDelimiterRe) return
244
+ while (true) {
245
+ const minLen = minLenForSentence()
246
+ // Search for a delimiter that is at or beyond the minimum length
247
+ let idx = -1
248
+ const searchFrom = Math.max(0, minLen - 1)
249
+ if (searchFrom < sentenceBuffer.length) {
250
+ const sub = sentenceBuffer.slice(searchFrom)
251
+ const match = sub.search(sentenceDelimiterRe)
252
+ if (match !== -1) idx = searchFrom + match
253
+ }
254
+ if (idx !== -1) {
255
+ const sentence = sentenceBuffer.slice(0, idx + 1).trim()
256
+ sentenceBuffer = sentenceBuffer.slice(idx + 1)
257
+ if (sentence) publishSentence(sentence)
258
+ continue
259
+ }
260
+ // No delimiter found — force flush if buffer exceeds limit
261
+ if (sentenceBuffer.length >= FORCE_FLUSH_LEN) {
262
+ // Try to break at the last space/comma for a cleaner cut
263
+ let cutIdx = FORCE_FLUSH_LEN
264
+ const lastSpace = sentenceBuffer.lastIndexOf(' ', FORCE_FLUSH_LEN)
265
+ const lastComma = sentenceBuffer.lastIndexOf(',', FORCE_FLUSH_LEN)
266
+ const lastBreak = Math.max(lastSpace, lastComma)
267
+ if (lastBreak > FORCE_FLUSH_LEN / 2) cutIdx = lastBreak + 1
268
+ const sentence = sentenceBuffer.slice(0, cutIdx).trim()
269
+ sentenceBuffer = sentenceBuffer.slice(cutIdx)
270
+ if (sentence) publishSentence(sentence)
271
+ continue
272
+ }
273
+ break
274
+ }
275
+ }
276
+
164
277
  const dispatcher = {
165
278
  sendToolResult: () => true,
166
279
  sendBlockReply: () => true,
167
280
  sendFinalReply: (payload: { text?: string }) => {
168
- if (payload.text) replyChunks.push(payload.text)
281
+ if (payload.text) {
282
+ if (!llmStarted) {
283
+ llmStarted = true
284
+ stopSoothingLoop()
285
+ }
286
+ sentenceBuffer += stripMarkdown(payload.text)
287
+ flushSentences()
288
+ }
169
289
  return true
170
290
  },
171
291
  waitForIdle: async () => {},
@@ -183,49 +303,51 @@ const folotoyChannel: ChannelPlugin<FlatChannelConfig> = {
183
303
  }),
184
304
  })
185
305
 
186
- let finalText = replyChunks.join('')
187
-
188
- // Summarize if enabled and text exceeds threshold
189
- if (summaryEnabled && subagent && finalText.length > summaryMaxChars) {
190
- try {
191
- const sessionKey = `folotoy-summary-${accountId}-${Date.now()}`
192
- const { runId } = await subagent.run({
193
- sessionKey,
194
- message: [
195
- `You are an assistant that summarizes texts concisely while keeping the most important information.`,
196
- `Summarize the text to approximately ${summaryMaxChars} characters.`,
197
- `Maintain the original tone and style. Reply only with the summary, without additional explanations.`,
198
- ``,
199
- `<text_to_summarize>`,
200
- finalText,
201
- `</text_to_summarize>`,
202
- ].join('\n'),
203
- deliver: false,
204
- })
205
- const result = await subagent.waitForRun({ runId, timeoutMs: 30_000 })
206
- if (result.status === 'ok') {
207
- const { messages } = await subagent.getSessionMessages({ sessionKey, limit: 1 })
208
- const lastMsg = messages[messages.length - 1] as { content?: string; text?: string } | undefined
209
- const summaryText = lastMsg?.content ?? lastMsg?.text
210
- if (summaryText) finalText = summaryText
306
+ // Flush remaining buffer (text after the last punctuation)
307
+ let remaining = sentenceBuffer.trim()
308
+ if (remaining) {
309
+ // Summarize remaining text if enabled and total response exceeds threshold
310
+ if (summaryEnabled && subagent && (totalSentChars + remaining.length) > summaryMaxChars) {
311
+ try {
312
+ const summarySessionKey = `folotoy-summary-${accountId}-${Date.now()}`
313
+ const { runId } = await subagent.run({
314
+ sessionKey: summarySessionKey,
315
+ message: [
316
+ `You are an assistant that summarizes texts concisely while keeping the most important information.`,
317
+ `Summarize the text to approximately ${Math.max(50, summaryMaxChars - totalSentChars)} characters.`,
318
+ `Maintain the original tone and style. Reply only with the summary, without additional explanations.`,
319
+ ``,
320
+ `<text_to_summarize>`,
321
+ remaining,
322
+ `</text_to_summarize>`,
323
+ ].join('\n'),
324
+ deliver: false,
325
+ })
326
+ const result = await subagent.waitForRun({ runId, timeoutMs: 30_000 })
327
+ if (result.status === 'ok') {
328
+ const { messages } = await subagent.getSessionMessages({ sessionKey: summarySessionKey, limit: 1 })
329
+ const lastMsg = messages[messages.length - 1] as { content?: string; text?: string } | undefined
330
+ const summaryText = lastMsg?.content ?? lastMsg?.text
331
+ if (summaryText) remaining = summaryText
332
+ }
333
+ await subagent.deleteSession({ sessionKey: summarySessionKey }).catch(() => {})
334
+ } catch (err) {
335
+ log?.warn?.(`Summary failed, truncating text: ${String(err)}`)
336
+ const maxRemaining = Math.max(50, summaryMaxChars - totalSentChars)
337
+ remaining = `${remaining.slice(0, maxRemaining - 3)}...`
211
338
  }
212
- await subagent.deleteSession({ sessionKey }).catch(() => {})
213
- } catch (err) {
214
- log?.warn?.(`Summary failed, truncating text: ${String(err)}`)
215
- finalText = `${finalText.slice(0, summaryMaxChars - 3)}...`
216
339
  }
217
- }
218
340
 
219
- if (finalText) {
220
341
  order++
221
342
  const outMsg: OutboundMessage = {
222
343
  msgId,
223
344
  identifier: 'chat_output',
224
- outParams: { content: finalText, recording_id, order, is_finished: false },
345
+ outParams: { content: remaining, recording_id, order, is_finished: false },
225
346
  }
226
347
  client.publish(outboundTopic, JSON.stringify(outMsg))
227
348
  }
228
349
  } finally {
350
+ if (soothingTimer) { clearTimeout(soothingTimer); soothingTimer = null }
229
351
  order++
230
352
  const finishMsg: OutboundMessage = {
231
353
  msgId,
@@ -258,9 +380,26 @@ const folotoyChannel: ChannelPlugin<FlatChannelConfig> = {
258
380
  const folotoy = (cfg as Record<string, unknown> & { channels?: { folotoy?: FlatChannelConfig } })
259
381
  ?.channels?.folotoy
260
382
  const sn = folotoy?.toy_sn ?? '<toy_sn>'
383
+ const now = new Date()
384
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone
385
+ const tzOffsetMin = -now.getTimezoneOffset()
386
+ const tzSign = tzOffsetMin >= 0 ? '+' : '-'
387
+ const tzH = String(Math.floor(Math.abs(tzOffsetMin) / 60)).padStart(2, '0')
388
+ const tzM = String(Math.abs(tzOffsetMin) % 60).padStart(2, '0')
389
+ const tzSuffix = `${tzSign}${tzH}:${tzM}`
390
+ const pad = (n: number) => String(n).padStart(2, '0')
391
+ const nowLocal = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}${tzSuffix}`
261
392
  return [
262
- `[FoloToy] This message is from a FoloToy toy (SN: ${sn}).`,
263
- `To set reminders/timers, use the cron tool with: sessionTarget="isolated", payload.kind="agentTurn", delivery.mode="announce", delivery.channel="folotoy", delivery.to="${sn}", delivery.accountId="default".`,
393
+ `[FoloToy] This message is from a FoloToy toy (SN: ${sn}). Current time: ${nowLocal} (${tz}). IMPORTANT: Your reply will be converted to speech (TTS). Use plain text only — no markdown, no bullet points, no numbered lists, no code blocks, no URLs. Write in a natural, conversational tone.`,
394
+ [
395
+ `To set reminders/timers, use the cron tool with action="add". IMPORTANT:`,
396
+ `- schedule.at MUST use the same timezone offset as current time (${tzSuffix}), NEVER use "Z"`,
397
+ `- For "N分钟后" reminders, add N minutes to current time ${nowLocal}`,
398
+ `- payload.kind MUST be "systemEvent" with a "text" field containing the reminder message`,
399
+ `- sessionTarget MUST be "isolated"`,
400
+ `- delivery MUST be: {"mode":"announce","channel":"folotoy","to":"${sn}","accountId":"default"}`,
401
+ `Example: {"action":"add","job":{"name":"喝水提醒","schedule":{"kind":"at","at":"2026-03-27T21:03:00${tzSuffix}"},"payload":{"kind":"systemEvent","text":"时间到啦,该喝水了!"},"sessionTarget":"isolated","delivery":{"mode":"announce","channel":"folotoy","to":"${sn}","accountId":"default"},"enabled":true}}`,
402
+ ].join('\n'),
264
403
  ]
265
404
  },
266
405
  },
@@ -292,7 +431,7 @@ const folotoyChannel: ChannelPlugin<FlatChannelConfig> = {
292
431
  const notifMsg: NotificationMessage = {
293
432
  msgId,
294
433
  identifier: 'send_notification',
295
- outParams: { text },
434
+ outParams: { text: stripMarkdown(text) },
296
435
  }
297
436
  entry.client.publish(notificationTopic, JSON.stringify(notifMsg))
298
437
  return { channel: 'folotoy', messageId: String(msgId) }
@@ -310,7 +449,7 @@ export function sendNotification({ text, accountId }: { text: string; accountId?
310
449
  const notifMsg: NotificationMessage = {
311
450
  msgId,
312
451
  identifier: 'send_notification',
313
- outParams: { text },
452
+ outParams: { text: stripMarkdown(text) },
314
453
  }
315
454
  entry.client.publish(notificationTopic, JSON.stringify(notifMsg))
316
455
  return { channel: 'folotoy', messageId: String(msgId) }
package/src/soothing.ts CHANGED
@@ -142,16 +142,46 @@ const DEFAULT_CANDIDATES = [
142
142
  '让我好好想想...',
143
143
  ]
144
144
 
145
- function pickRandom<T>(arr: T[]): T {
146
- return arr[Math.floor(Math.random() * arr.length)] as T
145
+ /** Fisher-Yates shuffle (in-place) */
146
+ function shuffle<T>(arr: T[]): T[] {
147
+ for (let i = arr.length - 1; i > 0; i--) {
148
+ const j = Math.floor(Math.random() * (i + 1))
149
+ ;[arr[i], arr[j]] = [arr[j]!, arr[i]!]
150
+ }
151
+ return arr
147
152
  }
148
153
 
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
+ * Creates a soothing reply picker that guarantees no repeats until all
156
+ * candidates in the matched category are exhausted, then reshuffles.
157
+ *
158
+ * Usage:
159
+ * const picker = createSoothingPicker(text)
160
+ * picker() // first reply (no repeat)
161
+ * picker() // second reply (different from first)
162
+ */
163
+ export function createSoothingPicker(text?: string): () => string {
164
+ let candidates: string[] | undefined
165
+ if (text) {
166
+ for (const rule of INTENT_RULES) {
167
+ if (rule.pattern.test(text)) {
168
+ candidates = rule.candidates
169
+ break
170
+ }
154
171
  }
155
172
  }
156
- return pickRandom(DEFAULT_CANDIDATES)
173
+ const pool = candidates ?? DEFAULT_CANDIDATES
174
+
175
+ let queue: string[] = []
176
+ return () => {
177
+ if (queue.length === 0) {
178
+ queue = shuffle([...pool])
179
+ }
180
+ return queue.pop()!
181
+ }
182
+ }
183
+
184
+ /** Returns one randomly chosen soothing reply matching the input intent. */
185
+ export function pickSoothingReply(text?: string): string {
186
+ return createSoothingPicker(text)()
157
187
  }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Strip Markdown / rich-text formatting for TTS.
3
+ *
4
+ * Designed to be safe for streaming chunks — removes syntax characters
5
+ * individually rather than matching complete open/close patterns,
6
+ * so partial markdown across chunks is handled correctly.
7
+ * Plain text without markdown passes through unchanged.
8
+ */
9
+ export function stripMarkdown(text: string): string {
10
+ return (
11
+ text
12
+ // Code blocks (``` ... ```) — remove fences and content when complete
13
+ .replace(/```[\s\S]*?```/g, '')
14
+ // Remaining fences (incomplete code block in a chunk)
15
+ .replace(/```/g, '')
16
+ // Inline code backticks
17
+ .replace(/`/g, '')
18
+ // Images ![alt](url) — keep alt text
19
+ .replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1')
20
+ // Links [text](url) — keep text
21
+ .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
22
+ // Bare URLs (https://...)
23
+ .replace(/https?:\/\/\S+/g, '')
24
+ // Heading markers
25
+ .replace(/^#{1,6}\s*/gm, '')
26
+ // Bold/italic/strikethrough markers (*, _, ~)
27
+ .replace(/[*_~]+/g, '')
28
+ // Blockquote markers
29
+ .replace(/^>\s?/gm, '')
30
+ // Horizontal rules (---, ***, ___)
31
+ .replace(/^[-]{3,}\s*$/gm, '')
32
+ // Unordered list markers (- / + at line start, * already removed above)
33
+ .replace(/^[\s]*[-+]\s+/gm, '')
34
+ // Ordered list markers (1. / 2. ...)
35
+ .replace(/^[\s]*\d+\.\s+/gm, '')
36
+ // HTML tags
37
+ .replace(/<[^>]+>/g, '')
38
+ // Collapse multiple spaces
39
+ .replace(/ {2,}/g, ' ')
40
+ // Collapse multiple blank lines
41
+ .replace(/\n{3,}/g, '\n\n')
42
+ .trim()
43
+ )
44
+ }