@ynhcj/xiaoyi 2.0.8 → 2.1.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/channel.js CHANGED
@@ -245,16 +245,22 @@ exports.xiaoyiPlugin = {
245
245
  SenderId: senderId,
246
246
  OriginatingChannel: "xiaoyi",
247
247
  };
248
- // Use the correct API to dispatch the message (non-streaming mode)
248
+ // Use the correct API to dispatch the message (streaming mode enabled)
249
249
  try {
250
+ // Track response state for streaming
251
+ let accumulatedText = "";
252
+ let taskId = runtime.getTaskIdForSession(message.sessionId) || `task_${Date.now()}`;
253
+ let frameCount = 0;
250
254
  await pluginRuntime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
251
255
  ctx: msgContext,
252
256
  cfg: config,
253
257
  dispatcherOptions: {
254
258
  deliver: async (payload) => {
255
259
  console.log("XiaoYi: Delivering final response:", payload.text?.substring(0, 100) + "...");
256
- // Send final response back through WebSocket (non-streaming, complete message)
257
- const taskId = runtime.getTaskIdForSession(message.sessionId) || `task_${Date.now()}`;
260
+ // Get the complete text
261
+ const completeText = payload.text || "";
262
+ accumulatedText = completeText;
263
+ // Send FINAL frame with complete content
258
264
  const response = {
259
265
  sessionId: message.sessionId,
260
266
  messageId: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
@@ -267,20 +273,58 @@ exports.xiaoyiPlugin = {
267
273
  },
268
274
  content: {
269
275
  type: "text",
270
- text: payload.text || "",
276
+ text: accumulatedText, // Complete content
271
277
  },
272
278
  status: "success",
273
279
  };
274
280
  const conn = runtime.getConnection();
275
281
  if (conn) {
276
- await conn.sendResponse(response, taskId, message.sessionId);
277
- console.log("XiaoYi: Final response sent successfully (non-streaming mode)");
282
+ // Use append=false for final frame (complete content)
283
+ await conn.sendResponse(response, taskId, message.sessionId, true, false);
284
+ console.log(`XiaoYi: Final frame sent (#${++frameCount}, isFinal=true, append=false, total length: ${accumulatedText.length})`);
278
285
  }
279
286
  },
287
+ onIdle: async () => {
288
+ console.log("XiaoYi: OpenClaw processing complete");
289
+ // All frames already sent, nothing more to do
290
+ },
280
291
  },
281
292
  replyOptions: {
282
- // Disable streaming by removing onPartialReply callback
283
- // This ensures all responses are buffered and delivered only once at the end
293
+ // Enable streaming responses through onPartialReply callback
294
+ onPartialReply: async (payload) => {
295
+ const newText = payload.text || "";
296
+ if (!newText) {
297
+ return; // Skip empty responses
298
+ }
299
+ console.log("XiaoYi: Streaming partial response:", newText.substring(0, 30) + "...");
300
+ // Calculate delta text (增量部分)
301
+ const previousLength = accumulatedText.length;
302
+ accumulatedText = newText;
303
+ const deltaText = newText.slice(previousLength);
304
+ // Send PARTIAL frame with delta content
305
+ const partialResponse = {
306
+ sessionId: message.sessionId,
307
+ messageId: `partial_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
308
+ timestamp: Date.now(),
309
+ agentId: resolvedAccount.config.agentId,
310
+ sender: {
311
+ id: resolvedAccount.config.agentId,
312
+ name: "OpenClaw Agent",
313
+ type: "agent",
314
+ },
315
+ content: {
316
+ type: "text",
317
+ text: deltaText || newText, // Send delta only (or full if delta is empty)
318
+ },
319
+ status: "processing", // Mark as processing
320
+ };
321
+ const conn = runtime.getConnection();
322
+ if (conn) {
323
+ // Use append=true for streaming frames (服务端会追加)
324
+ await conn.sendResponse(partialResponse, taskId, message.sessionId, false, true);
325
+ console.log(`XiaoYi: Partial frame sent (#${++frameCount}, isFinal=false, append=true, delta length: ${deltaText.length || newText.length})`);
326
+ }
327
+ },
284
328
  },
285
329
  });
286
330
  }
@@ -25,8 +25,13 @@ export declare class XiaoYiWebSocketManager extends EventEmitter {
25
25
  /**
26
26
  * Send A2A response message (converts to JSON-RPC 2.0 format)
27
27
  * This method is for regular agent responses only
28
+ * @param response - The response message
29
+ * @param taskId - The task ID
30
+ * @param sessionId - The session ID
31
+ * @param isFinal - Whether this is the final frame (default: true)
32
+ * @param append - Whether to append to previous content (default: false for complete content)
28
33
  */
29
- sendResponse(response: A2AResponseMessage, taskId: string, sessionId: string): Promise<void>;
34
+ sendResponse(response: A2AResponseMessage, taskId: string, sessionId: string, isFinal?: boolean, append?: boolean): Promise<void>;
30
35
  /**
31
36
  * Send A2A clear context response (uses specific clear context format)
32
37
  * Reference: https://developer.huawei.com/consumer/cn/doc/service/clear-context-0000002537681163
@@ -39,6 +44,10 @@ export declare class XiaoYiWebSocketManager extends EventEmitter {
39
44
  sendTasksCancelResponse(requestId: string, sessionId: string, success?: boolean): Promise<void>;
40
45
  /**
41
46
  * Convert A2AResponseMessage to A2A JSON-RPC 2.0 format
47
+ * @param response - The response message
48
+ * @param taskId - The task ID
49
+ * @param isFinal - Whether this is the final frame (default: true)
50
+ * @param append - Whether to append to previous content (default: false)
42
51
  */
43
52
  private convertToJsonRpcFormat;
44
53
  /**
package/dist/websocket.js CHANGED
@@ -42,11 +42,7 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
42
42
  });
43
43
  this.setupWebSocketHandlers();
44
44
  return new Promise((resolve, reject) => {
45
- const timeout = setTimeout(() => {
46
- reject(new Error("Connection timeout"));
47
- }, 30000); // Increased timeout to 30 seconds
48
45
  this.ws.once("open", () => {
49
- clearTimeout(timeout);
50
46
  this.state.connected = true;
51
47
  this.state.authenticated = true; // Authenticated via headers
52
48
  this.state.reconnectAttempts = 0;
@@ -59,7 +55,6 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
59
55
  resolve();
60
56
  });
61
57
  this.ws.once("error", (error) => {
62
- clearTimeout(timeout);
63
58
  reject(error);
64
59
  });
65
60
  });
@@ -97,13 +92,18 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
97
92
  /**
98
93
  * Send A2A response message (converts to JSON-RPC 2.0 format)
99
94
  * This method is for regular agent responses only
95
+ * @param response - The response message
96
+ * @param taskId - The task ID
97
+ * @param sessionId - The session ID
98
+ * @param isFinal - Whether this is the final frame (default: true)
99
+ * @param append - Whether to append to previous content (default: false for complete content)
100
100
  */
101
- async sendResponse(response, taskId, sessionId) {
101
+ async sendResponse(response, taskId, sessionId, isFinal = true, append = false) {
102
102
  if (!this.isReady()) {
103
103
  throw new Error("WebSocket not ready");
104
104
  }
105
105
  // Convert A2AResponseMessage to A2A JSON-RPC 2.0 format
106
- const jsonRpcResponse = this.convertToJsonRpcFormat(response, taskId);
106
+ const jsonRpcResponse = this.convertToJsonRpcFormat(response, taskId, isFinal, append);
107
107
  const message = {
108
108
  msgType: "agent_response",
109
109
  agentId: this.config.agentId,
@@ -167,8 +167,12 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
167
167
  }
168
168
  /**
169
169
  * Convert A2AResponseMessage to A2A JSON-RPC 2.0 format
170
+ * @param response - The response message
171
+ * @param taskId - The task ID
172
+ * @param isFinal - Whether this is the final frame (default: true)
173
+ * @param append - Whether to append to previous content (default: false)
170
174
  */
171
- convertToJsonRpcFormat(response, taskId) {
175
+ convertToJsonRpcFormat(response, taskId, isFinal = true, append = false) {
172
176
  // Generate artifact ID
173
177
  const artifactId = `artifact_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
174
178
  // Check if there's an error
@@ -200,13 +204,13 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
200
204
  },
201
205
  });
202
206
  }
203
- // Create TaskArtifactUpdateEvent
207
+ // Create TaskArtifactUpdateEvent with configurable flags
204
208
  const artifactEvent = {
205
209
  taskId: taskId,
206
210
  kind: "artifact-update",
207
- append: false,
208
- lastChunk: true,
209
- final: true, // Mark as final since this is the complete response
211
+ append: append, // Controls whether to append or replace content
212
+ lastChunk: isFinal, // Set based on isFinal parameter
213
+ final: isFinal, // Set based on isFinal parameter
210
214
  artifact: {
211
215
  artifactId: artifactId,
212
216
  parts: parts,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynhcj/xiaoyi",
3
- "version": "2.0.8",
3
+ "version": "2.1.0",
4
4
  "description": "XiaoYi channel plugin for OpenClaw",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",