openclaw-xiaoyou 1.3.2 → 1.3.4

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/channel.ts +69 -28
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-xiaoyou",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
4
4
  "type": "module",
5
5
  "description": "Xiaoyou channel plugin for OpenClaw — connects enterprise services via persistent outbound WebSocket",
6
6
  "openclaw": {
package/src/channel.ts CHANGED
@@ -16,6 +16,25 @@ let _runtime: any = null;
16
16
  export function setRuntime(rt: any) { _runtime = rt; }
17
17
  export function getRuntime() { return _runtime; }
18
18
 
19
+ // ─── 按标点/换行拆分文本 ─────────────────────────────
20
+
21
+ /**
22
+ * 将文本按中英文标点符号和换行符拆分为多个片段。
23
+ * 每个片段以标点或换行结尾(保留标点在片段内)。
24
+ */
25
+ function splitBySentence(text: string): string[] {
26
+ // 匹配:中文标点(。!?;)、英文标点(.!?;)后跟空格或结尾、换行符
27
+ const parts = text.split(/(?<=[。!?;\n])|(?<=[.!?;]\s)/);
28
+ const result: string[] = [];
29
+ for (const part of parts) {
30
+ const trimmed = part;
31
+ if (trimmed.length > 0) {
32
+ result.push(trimmed);
33
+ }
34
+ }
35
+ return result.length > 0 ? result : [text];
36
+ }
37
+
19
38
  // ─── Config Adapter ──────────────────────────────────
20
39
 
21
40
  function getChannelConfig(cfg: any): any {
@@ -180,14 +199,15 @@ export const xiayouPlugin = {
180
199
  Timestamp: Date.now(),
181
200
  });
182
201
 
183
- // 4. 分发消息给 Agent
202
+ // 4. 分发并获取回复(流式 — 按标点拆分)
184
203
  //
185
- // Block streaming 机制说明:
186
- // blockStreaming=true 且 Gateway 配置了 blockStreamingDefault="on" 时,
187
- // Gateway 会在 LLM 生成过程中通过 outbound.send 逐块推送消息,
188
- // 不经过 deliver 回调。deliver 仅在非流式场景下被调用(作为 fallback)。
204
+ // Gateway 的 block streaming 会多次调用 deliver,每次一个 block。
205
+ // 我们在 deliver 内部进一步按标点/换行拆分,实现更细粒度的流式推送。
189
206
  //
190
207
  const replyMessageId = `xiaoyou-${Date.now()}`;
208
+ let chunkSeq = 1;
209
+ let fullText = "";
210
+ let replyEndSent = false;
191
211
 
192
212
  await rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
193
213
  ctx: inboundCtx,
@@ -195,24 +215,62 @@ export const xiayouPlugin = {
195
215
  dispatcherOptions: {
196
216
  responsePrefix: "",
197
217
  deliver: async (payload: any) => {
198
- // Fallback: 当 block streaming 未生效时,deliver 被调用
199
218
  const textToSend = payload.markdown || payload.text;
200
219
  if (!textToSend) return;
201
220
 
221
+ // 按标点/换行拆分为更细的片段
222
+ const sentences = splitBySentence(textToSend);
223
+
224
+ for (const sentence of sentences) {
225
+ fullText += sentence;
226
+ if (_client && _client.isConnected()) {
227
+ _client.sendReply({
228
+ type: "reply",
229
+ conversationId,
230
+ messageId: replyMessageId,
231
+ replyToMessageId: inboundMessageId,
232
+ text: sentence,
233
+ streamStatus: "chunk",
234
+ seq: chunkSeq++,
235
+ timestamp: Date.now(),
236
+ });
237
+ }
238
+ }
239
+ logger.info(`[xiaoyou] ${sentences.length} chunks sent to ${conversationId} (total seq=${chunkSeq})`);
240
+ },
241
+ onComplete: async () => {
242
+ replyEndSent = true;
202
243
  if (_client && _client.isConnected()) {
203
244
  _client.sendReply({
204
245
  type: "reply",
205
246
  conversationId,
206
247
  messageId: replyMessageId,
207
248
  replyToMessageId: inboundMessageId,
208
- text: textToSend,
249
+ text: fullText,
250
+ streamStatus: "end",
209
251
  timestamp: Date.now(),
210
252
  });
211
- logger.info(`[xiaoyou] reply sent to ${conversationId}${inboundMessageId ? ` (replyTo=${inboundMessageId})` : ""}`);
253
+ logger.info(`[xiaoyou] stream end sent to ${conversationId} (${chunkSeq} chunks)${inboundMessageId ? ` (replyTo=${inboundMessageId})` : ""}`);
212
254
  }
213
255
  },
214
256
  },
215
257
  });
258
+
259
+ // dispatch resolve 后,如果 onComplete 未被调用(兼容),手动发 end
260
+ if (!replyEndSent && chunkSeq > 1 && _client && _client.isConnected()) {
261
+ _client.sendReply({
262
+ type: "reply",
263
+ conversationId,
264
+ messageId: replyMessageId,
265
+ replyToMessageId: inboundMessageId,
266
+ text: fullText,
267
+ streamStatus: "end",
268
+ timestamp: Date.now(),
269
+ });
270
+ logger.info(`[xiaoyou] stream end (fallback) sent to ${conversationId} (${chunkSeq} chunks)${inboundMessageId ? ` (replyTo=${inboundMessageId})` : ""}`);
271
+ } else if (chunkSeq === 1) {
272
+ logger.warn(`[xiaoyou] no reply generated for ${conversationId}`);
273
+ }
216
274
  },
217
275
  });
218
276
 
@@ -240,34 +298,17 @@ export const xiayouPlugin = {
240
298
 
241
299
  // ── Outbound 出站 ──────────────────────────────────
242
300
  outbound: {
243
- send: async ({ to, payload, meta, ...rest }: any) => {
301
+ send: async ({ to, payload }: any) => {
244
302
  if (!_client || !_client.isConnected()) {
245
303
  return { ok: false, error: "xiaoyou: not connected" };
246
304
  }
247
305
 
248
- // 调试日志:观察 Gateway block streaming 时传入的完整参数
249
- const rt = getRuntime();
250
- const logger = rt?.log || console;
251
- logger.info(`[xiaoyou] outbound.send called: to=${to}, payload=${JSON.stringify(payload)}, meta=${JSON.stringify(meta)}, rest=${JSON.stringify(rest)}`);
252
-
253
- // 推断 streamStatus:
254
- // Gateway block streaming 可能通过 meta 或 payload 传递流式信息
255
- const streamMeta = meta?.blockStream || payload?.blockStream || meta?.stream;
256
- const isFinal = streamMeta?.final ?? meta?.final ?? payload?.final;
257
- const seq = streamMeta?.seq ?? meta?.seq ?? payload?.seq;
258
- const streamStatus = streamMeta
259
- ? (isFinal ? "end" : "chunk")
260
- : (meta?.isBlock ? "chunk" : undefined);
261
-
262
- const baseReply: any = {
306
+ const baseReply = {
263
307
  type: "reply" as const,
264
308
  conversationId: to,
265
- messageId: payload.messageId || meta?.messageId || `xiaoyou-${Date.now()}`,
266
- replyToMessageId: payload.replyToMessageId || meta?.replyToMessageId,
309
+ messageId: `xiaoyou-${Date.now()}`,
267
310
  agentId: payload.agentId,
268
311
  timestamp: Date.now(),
269
- ...(streamStatus && { streamStatus }),
270
- ...(seq !== undefined && { seq }),
271
312
  };
272
313
 
273
314
  if (payload.kind === "text") {