openclaw-xiaoyou 1.3.0 → 1.3.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-xiaoyou",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
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
@@ -180,18 +180,14 @@ export const xiayouPlugin = {
180
180
  Timestamp: Date.now(),
181
181
  });
182
182
 
183
- // 4. 分发并获取回复(流式 — block 级)
183
+ // 4. 分发消息给 Agent
184
184
  //
185
- // OpenClaw 2026.3.x 的 block streaming 机制:
186
- // capabilities.blockStreaming = true 告诉 Gateway block 逐个调用 deliver,
187
- // 而非等全部生成完毕。每个 block(段落/代码块等)生成完成后立即 deliver。
188
- // 我们将每次 deliver 作为一个 reply_chunk 推送给企业服务,
189
- // dispatch promise resolve 后发送 reply_end 标记。
185
+ // Block streaming 机制说明:
186
+ // blockStreaming=true Gateway 配置了 blockStreamingDefault="on" 时,
187
+ // Gateway 会在 LLM 生成过程中通过 outbound.send 逐块推送消息,
188
+ // 不经过 deliver 回调。deliver 仅在非流式场景下被调用(作为 fallback)。
190
189
  //
191
190
  const replyMessageId = `xiaoyou-${Date.now()}`;
192
- let chunkSeq = 0;
193
- let fullText = "";
194
- let replyEndSent = false;
195
191
 
196
192
  await rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
197
193
  ctx: inboundCtx,
@@ -199,57 +195,24 @@ export const xiayouPlugin = {
199
195
  dispatcherOptions: {
200
196
  responsePrefix: "",
201
197
  deliver: async (payload: any) => {
198
+ // Fallback: 当 block streaming 未生效时,deliver 被调用
202
199
  const textToSend = payload.markdown || payload.text;
203
200
  if (!textToSend) return;
204
201
 
205
- fullText += (fullText ? "\n" : "") + textToSend;
206
-
207
202
  if (_client && _client.isConnected()) {
208
- // 每个 block 作为一个 chunk 推送
209
- _client.sendChunk({
210
- type: "reply_chunk",
203
+ _client.sendReply({
204
+ type: "reply",
211
205
  conversationId,
212
206
  messageId: replyMessageId,
213
207
  replyToMessageId: inboundMessageId,
214
- delta: textToSend,
215
- seq: chunkSeq++,
208
+ text: textToSend,
216
209
  timestamp: Date.now(),
217
210
  });
218
- logger.info(`[xiaoyou] chunk #${chunkSeq} sent to ${conversationId}`);
219
- }
220
- },
221
- onComplete: async () => {
222
- // Gateway 调用 onComplete 表示所有 block 已推送完毕
223
- replyEndSent = true;
224
- if (_client && _client.isConnected()) {
225
- _client.sendReplyEnd({
226
- type: "reply_end",
227
- conversationId,
228
- messageId: replyMessageId,
229
- replyToMessageId: inboundMessageId,
230
- fullText,
231
- timestamp: Date.now(),
232
- });
233
- logger.info(`[xiaoyou] streaming completed to ${conversationId} (${chunkSeq} blocks)${inboundMessageId ? ` (replyTo=${inboundMessageId})` : ""}`);
211
+ logger.info(`[xiaoyou] reply sent to ${conversationId}${inboundMessageId ? ` (replyTo=${inboundMessageId})` : ""}`);
234
212
  }
235
213
  },
236
214
  },
237
215
  });
238
-
239
- // dispatch resolve 后,如果 onComplete 未被调用(旧版本兼容),手动发 reply_end
240
- if (!replyEndSent && chunkSeq > 0 && _client && _client.isConnected()) {
241
- _client.sendReplyEnd({
242
- type: "reply_end",
243
- conversationId,
244
- messageId: replyMessageId,
245
- replyToMessageId: inboundMessageId,
246
- fullText,
247
- timestamp: Date.now(),
248
- });
249
- logger.info(`[xiaoyou] streaming completed (fallback) to ${conversationId} (${chunkSeq} blocks)${inboundMessageId ? ` (replyTo=${inboundMessageId})` : ""}`);
250
- } else if (chunkSeq === 0) {
251
- logger.warn(`[xiaoyou] no reply generated for ${conversationId}`);
252
- }
253
216
  },
254
217
  });
255
218
 
@@ -277,17 +240,34 @@ export const xiayouPlugin = {
277
240
 
278
241
  // ── Outbound 出站 ──────────────────────────────────
279
242
  outbound: {
280
- send: async ({ to, payload }: any) => {
243
+ send: async ({ to, payload, meta, ...rest }: any) => {
281
244
  if (!_client || !_client.isConnected()) {
282
245
  return { ok: false, error: "xiaoyou: not connected" };
283
246
  }
284
247
 
285
- const baseReply = {
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 = {
286
263
  type: "reply" as const,
287
264
  conversationId: to,
288
- messageId: `xiaoyou-${Date.now()}`,
265
+ messageId: payload.messageId || meta?.messageId || `xiaoyou-${Date.now()}`,
266
+ replyToMessageId: payload.replyToMessageId || meta?.replyToMessageId,
289
267
  agentId: payload.agentId,
290
268
  timestamp: Date.now(),
269
+ ...(streamStatus && { streamStatus }),
270
+ ...(seq !== undefined && { seq }),
291
271
  };
292
272
 
293
273
  if (payload.kind === "text") {
@@ -11,8 +11,6 @@ import type {
11
11
  Frame,
12
12
  InboundMessage,
13
13
  OutboundReply,
14
- OutboundReplyChunk,
15
- OutboundReplyEnd,
16
14
  PongFrame,
17
15
  } from "./types.js";
18
16
 
@@ -33,8 +31,6 @@ export type EnterpriseClient = {
33
31
  connect: () => void;
34
32
  disconnect: () => void;
35
33
  sendReply: (reply: OutboundReply) => boolean;
36
- sendChunk: (chunk: OutboundReplyChunk) => boolean;
37
- sendReplyEnd: (end: OutboundReplyEnd) => boolean;
38
34
  isConnected: () => boolean;
39
35
  };
40
36
 
@@ -186,27 +182,9 @@ export function createEnterpriseClient(opts: EnterpriseClientOptions): Enterpris
186
182
  return true;
187
183
  }
188
184
 
189
- function sendChunk(chunk: OutboundReplyChunk): boolean {
190
- if (!ws || ws.readyState !== WebSocket.OPEN || !authenticated) {
191
- logger.warn("[xiaoyou] cannot send chunk: not connected");
192
- return false;
193
- }
194
- ws.send(JSON.stringify(chunk));
195
- return true;
196
- }
197
-
198
- function sendReplyEnd(end: OutboundReplyEnd): boolean {
199
- if (!ws || ws.readyState !== WebSocket.OPEN || !authenticated) {
200
- logger.warn("[xiaoyou] cannot send reply_end: not connected");
201
- return false;
202
- }
203
- ws.send(JSON.stringify(end));
204
- return true;
205
- }
206
-
207
185
  function isConnected(): boolean {
208
186
  return ws !== null && ws.readyState === WebSocket.OPEN && authenticated;
209
187
  }
210
188
 
211
- return { connect, disconnect, sendReply, sendChunk, sendReplyEnd, isConnected };
189
+ return { connect, disconnect, sendReply, isConnected };
212
190
  }
package/src/types.ts CHANGED
@@ -47,6 +47,14 @@ export type Attachment = {
47
47
 
48
48
  // ─── 出站回复(插件 → 企业服务)─────────────────────────
49
49
 
50
+ /**
51
+ * 流式状态标记:
52
+ * - "chunk" — 流式片段,text 为增量内容
53
+ * - "end" — 流结束,text 为完整文本
54
+ * - undefined — 非流式,一次性完整回复(向后兼容)
55
+ */
56
+ export type StreamStatus = "chunk" | "end";
57
+
50
58
  export type OutboundReply = {
51
59
  type: "reply";
52
60
  conversationId: string;
@@ -56,33 +64,10 @@ export type OutboundReply = {
56
64
  mediaUrls?: string[];
57
65
  agentId?: string;
58
66
  timestamp: number;
59
- };
60
-
61
- /** 流式回复片段(插件 企业服务) */
62
- export type OutboundReplyChunk = {
63
- type: "reply_chunk";
64
- conversationId: string;
65
- messageId: string;
66
- replyToMessageId?: string;
67
- /** 本次增量文本 */
68
- delta: string;
69
- /** 该 chunk 在整条回复中的序号(从 0 开始) */
70
- seq: number;
71
- agentId?: string;
72
- timestamp: number;
73
- };
74
-
75
- /** 流式回复结束标记(插件 → 企业服务) */
76
- export type OutboundReplyEnd = {
77
- type: "reply_end";
78
- conversationId: string;
79
- messageId: string;
80
- replyToMessageId?: string;
81
- /** 完整文本(可选,方便企业侧校验拼接结果) */
82
- fullText?: string;
83
- mediaUrls?: string[];
84
- agentId?: string;
85
- timestamp: number;
67
+ /** 流式标记:chunk=增量片段, end=流结束, 不传=一次性完整回复 */
68
+ streamStatus?: StreamStatus;
69
+ /** 流式片段序号(仅 streamStatus="chunk" 时有值,从 0 开始) */
70
+ seq?: number;
86
71
  };
87
72
 
88
73
  // ─── 控制帧 ─────────────────────────────────────────────
@@ -108,8 +93,6 @@ export type PongFrame = { type: "pong"; seq: number; ts: number };
108
93
  export type Frame =
109
94
  | InboundMessage
110
95
  | OutboundReply
111
- | OutboundReplyChunk
112
- | OutboundReplyEnd
113
96
  | AuthFrame
114
97
  | AuthResultFrame
115
98
  | PingFrame