@ynhcj/xiaoyi 2.2.4 → 2.2.6

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
@@ -3,57 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.xiaoyiPlugin = void 0;
4
4
  const runtime_1 = require("./runtime");
5
5
  const file_handler_1 = require("./file-handler");
6
- // Import agent event functions - will be resolved at runtime
7
- // Use dynamic require for compatibility
8
- let onAgentEvent = null;
9
- let registerAgentRunContext = null;
10
- try {
11
- // Try multiple import paths
12
- let agentEvents = null;
13
- // Try path 1: openclaw/dist/infra/agent-events
14
- try {
15
- agentEvents = require("openclaw/dist/infra/agent-events");
16
- console.log("XiaoYi: [AGENT EVENT] Imported from openclaw/dist/infra/agent-events");
17
- }
18
- catch (e1) {
19
- console.log("XiaoYi: [AGENT EVENT] Path 1 failed:", e1?.message || e1);
20
- // Try path 2: ../openclaw/dist/infra/agent-events
21
- try {
22
- agentEvents = require("../openclaw/dist/infra/agent-events");
23
- console.log("XiaoYi: [AGENT EVENT] Imported from ../openclaw/dist/infra/agent-events");
24
- }
25
- catch (e2) {
26
- console.log("XiaoYi: [AGENT EVENT] Path 2 failed:", e2?.message || e2);
27
- // Try path 3: ../../openclaw/dist/infra/agent-events
28
- try {
29
- agentEvents = require("../../openclaw/dist/infra/agent-events");
30
- console.log("XiaoYi: [AGENT EVENT] Imported from ../../openclaw/dist/infra/agent-events");
31
- }
32
- catch (e3) {
33
- console.error("XiaoYi: [AGENT EVENT] All import paths failed:");
34
- console.error(" Path 1 (openclaw/dist/infra/agent-events):", e1?.message || e1);
35
- console.error(" Path 2 (../openclaw/dist/infra/agent-events):", e2?.message || e2);
36
- console.error(" Path 3 (../../openclaw/dist/infra/agent-events):", e3?.message || e3);
37
- }
38
- }
39
- }
40
- if (agentEvents) {
41
- onAgentEvent = agentEvents.onAgentEvent;
42
- registerAgentRunContext = agentEvents.registerAgentRunContext;
43
- if (typeof onAgentEvent === "function") {
44
- console.log("XiaoYi: [AGENT EVENT] ✓ onAgentEvent function imported successfully");
45
- }
46
- else {
47
- console.error("XiaoYi: [AGENT EVENT] ✗ onAgentEvent is not a function, type:", typeof onAgentEvent);
48
- }
49
- }
50
- else {
51
- console.warn("XiaoYi: [AGENT EVENT] Could not import agent event module");
52
- }
53
- }
54
- catch (error) {
55
- console.error("XiaoYi: [AGENT EVENT] Fatal import error:", error?.message || error);
56
- }
57
6
  /**
58
7
  * XiaoYi Channel Plugin
59
8
  * Implements OpenClaw ChannelPlugin interface for XiaoYi A2A protocol
@@ -358,164 +307,87 @@ exports.xiaoyiPlugin = {
358
307
  SenderId: senderId,
359
308
  OriginatingChannel: "xiaoyi",
360
309
  };
361
- // Use the correct API to dispatch the message (streaming via onAgentEvent)
310
+ // Dispatch message using OpenClaw's reply dispatcher
362
311
  try {
363
- const streamingEnabled = resolvedAccount.config.enableStreaming === true;
364
- const hasAgentEventSupport = typeof onAgentEvent === "function";
365
312
  console.log("\n" + "=".repeat(60));
366
- console.log(`XiaoYi: [STREAMING] Starting message processing`);
313
+ console.log(`XiaoYi: [MESSAGE] Processing user message`);
367
314
  console.log(` Session: ${message.sessionId}`);
368
315
  console.log(` Task ID: ${message.params.id}`);
369
316
  console.log(` User input: ${bodyText.substring(0, 50)}${bodyText.length > 50 ? "..." : ""}`);
370
- console.log(` Config Streaming: ${streamingEnabled ? "ENABLED" : "DISABLED"}`);
371
- console.log(` Agent Event Support: ${hasAgentEventSupport ? "AVAILABLE" : "NOT AVAILABLE"}`);
372
- console.log(` Effective Streaming: ${streamingEnabled && hasAgentEventSupport ? "ENABLED (via onAgentEvent)" : "DISABLED"}`);
317
+ console.log(` Images: ${images.length}`);
373
318
  console.log("=".repeat(60) + "\n");
374
- // Track response state for streaming
375
- let accumulatedText = "";
376
- let taskId = runtime.getTaskIdForSession(message.sessionId) || `task_${Date.now()}`;
377
- let frameCount = 0;
378
- let partialCount = 0;
319
+ const taskId = runtime.getTaskIdForSession(message.sessionId) || `task_${Date.now()}`;
379
320
  const startTime = Date.now();
380
- // Register agent event listener for streaming (NEW METHOD)
381
- let unsubscribeAgentEvents;
382
- let runId;
383
- let isCompleted = false;
384
- if (streamingEnabled && hasAgentEventSupport) {
385
- // Subscribe to agent events BEFORE dispatching the message
386
- unsubscribeAgentEvents = onAgentEvent((evt) => {
387
- if (isCompleted)
388
- return;
389
- // Get runId from first event
390
- if (!runId && evt.runId) {
391
- runId = evt.runId;
392
- console.log(`XiaoYi: [AGENT EVENT] Registered runId: ${runId}`);
393
- }
394
- // Only process events for this run
395
- if (runId && evt.runId !== runId)
396
- return;
397
- const elapsed = Date.now() - startTime;
398
- // Handle streaming text events
399
- if (evt.stream === "assistant" && typeof evt.data?.text === "string") {
400
- const newText = evt.data.text;
401
- if (!newText)
402
- return;
403
- // Calculate delta text
404
- const previousLength = accumulatedText.length;
405
- accumulatedText = newText;
406
- const deltaText = newText.slice(previousLength);
407
- console.log(`\n>>> XiaoYi: [AGENT EVENT] Stream frame #${++partialCount} at ${elapsed}ms`);
408
- console.log(` Run ID: ${evt.runId}`);
409
- console.log(` Delta length: ${deltaText.length} chars`);
410
- console.log(` Total length: ${newText.length} chars`);
411
- console.log(` Delta content: "${deltaText}"`);
412
- // Send PARTIAL frame with delta content
413
- const partialResponse = {
414
- sessionId: message.sessionId,
415
- messageId: `partial_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
416
- timestamp: Date.now(),
417
- agentId: resolvedAccount.config.agentId,
418
- sender: {
419
- id: resolvedAccount.config.agentId,
420
- name: "OpenClaw Agent",
421
- type: "agent",
422
- },
423
- content: {
424
- type: "text",
425
- text: deltaText || newText,
426
- },
427
- status: "processing",
428
- };
429
- const conn = runtime.getConnection();
430
- if (conn) {
431
- conn.sendResponse(partialResponse, taskId, message.sessionId, false, true)
432
- .then(() => {
433
- console.log(` ✓ SENT (isFinal=false, append=true)\n`);
434
- })
435
- .catch((err) => {
436
- console.error(` ✗ Send failed: ${err}\n`);
437
- });
438
- }
321
+ let accumulatedText = "";
322
+ // ==================== START TIMEOUT PROTECTION ====================
323
+ // Start 60-second timeout timer
324
+ const timeoutConfig = runtime.getTimeoutConfig();
325
+ console.log(`[TIMEOUT] Starting ${timeoutConfig.duration}ms timeout protection for session ${message.sessionId}`);
326
+ runtime.setTimeoutForSession(message.sessionId, async () => {
327
+ // Timeout callback - send timeout message to user
328
+ const elapsed = Date.now() - startTime;
329
+ console.log("\n" + "=".repeat(60));
330
+ console.log(`[TIMEOUT] Timeout triggered for session ${message.sessionId}`);
331
+ console.log(` Elapsed: ${elapsed}ms`);
332
+ console.log(` Task ID: ${taskId}`);
333
+ console.log("=".repeat(60) + "\n");
334
+ const conn = runtime.getConnection();
335
+ if (conn) {
336
+ const timeoutResponse = {
337
+ sessionId: message.sessionId,
338
+ messageId: `timeout_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
339
+ timestamp: Date.now(),
340
+ agentId: resolvedAccount.config.agentId,
341
+ sender: {
342
+ id: resolvedAccount.config.agentId,
343
+ name: "OpenClaw Agent",
344
+ type: "agent",
345
+ },
346
+ content: {
347
+ type: "text",
348
+ text: timeoutConfig.message,
349
+ },
350
+ status: "success",
351
+ };
352
+ try {
353
+ await conn.sendResponse(timeoutResponse, taskId, message.sessionId, true, false);
354
+ console.log(`[TIMEOUT] Timeout message sent successfully to session ${message.sessionId}\n`);
439
355
  }
440
- // Handle lifecycle events (completion/error)
441
- if (evt.stream === "lifecycle") {
442
- const phase = evt.data?.phase;
443
- if (phase === "end") {
444
- isCompleted = true;
445
- console.log("\n" + "-".repeat(60));
446
- console.log(`XiaoYi: [AGENT EVENT] Lifecycle phase: end`);
447
- console.log(` Run ID: ${evt.runId}`);
448
- console.log(` Elapsed: ${elapsed}ms`);
449
- console.log(` Total frames: ${partialCount}`);
450
- console.log(` Final length: ${accumulatedText.length} chars`);
451
- console.log("-".repeat(60) + "\n");
452
- // Send FINAL frame
453
- const response = {
454
- sessionId: message.sessionId,
455
- messageId: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
456
- timestamp: Date.now(),
457
- agentId: resolvedAccount.config.agentId,
458
- sender: {
459
- id: resolvedAccount.config.agentId,
460
- name: "OpenClaw Agent",
461
- type: "agent",
462
- },
463
- content: {
464
- type: "text",
465
- text: accumulatedText,
466
- },
467
- status: "success",
468
- };
469
- const conn = runtime.getConnection();
470
- if (conn) {
471
- conn.sendResponse(response, taskId, message.sessionId, true, false)
472
- .then(() => {
473
- console.log(`✓ XiaoYi: FINAL frame sent (isFinal=true, append=false)\n`);
474
- })
475
- .catch((err) => {
476
- console.error(`✗ Final send failed: ${err}\n`);
477
- });
478
- }
479
- // Unsubscribe from events
480
- if (unsubscribeAgentEvents) {
481
- unsubscribeAgentEvents();
482
- unsubscribeAgentEvents = undefined;
483
- }
484
- }
485
- else if (phase === "error") {
486
- isCompleted = true;
487
- console.error(`XiaoYi: [AGENT EVENT] Error phase:`, evt.data?.error);
488
- // Unsubscribe from events
489
- if (unsubscribeAgentEvents) {
490
- unsubscribeAgentEvents();
491
- unsubscribeAgentEvents = undefined;
492
- }
493
- }
356
+ catch (error) {
357
+ console.error(`[TIMEOUT] Failed to send timeout message:`, error);
494
358
  }
495
- });
496
- console.log(`XiaoYi: [AGENT EVENT] Listener registered for streaming`);
497
- }
498
- else if (streamingEnabled && !hasAgentEventSupport) {
499
- console.warn(`XiaoYi: [WARN] Streaming enabled but onAgentEvent not available, falling back to non-streaming mode`);
500
- }
359
+ }
360
+ else {
361
+ console.error(`[TIMEOUT] Connection not available, cannot send timeout message\n`);
362
+ }
363
+ });
364
+ // ==================== END TIMEOUT PROTECTION ====================
501
365
  await pluginRuntime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
502
366
  ctx: msgContext,
503
367
  cfg: config,
504
368
  dispatcherOptions: {
505
369
  deliver: async (payload) => {
506
- // NOTE: onAgentEvent does NOT emit "assistant" stream events in production
507
- // Only lifecycle and tool events are emitted
508
- // So we must handle the final result here
509
370
  const elapsed = Date.now() - startTime;
510
371
  const completeText = payload.text || "";
511
372
  accumulatedText = completeText;
373
+ // ==================== CHECK TIMEOUT ====================
374
+ // If timeout already sent, discard this response
375
+ if (runtime.isSessionTimeout(message.sessionId)) {
376
+ console.log("\n" + "=".repeat(60));
377
+ console.log(`[TIMEOUT] Response received AFTER timeout`);
378
+ console.log(` Session: ${message.sessionId}`);
379
+ console.log(` Elapsed: ${elapsed}ms`);
380
+ console.log(` Action: DISCARDING (timeout message already sent)`);
381
+ console.log("=".repeat(60) + "\n");
382
+ return;
383
+ }
512
384
  console.log("\n" + "-".repeat(60));
513
- console.log(`XiaoYi: [DELIVER] Final result received`);
385
+ console.log(`XiaoYi: [DELIVER] AI response received`);
514
386
  console.log(` Elapsed: ${elapsed}ms`);
515
387
  console.log(` Length: ${completeText.length} chars`);
516
- console.log(` Streaming frames sent: ${partialCount}`);
388
+ console.log(` Preview: ${completeText.substring(0, 100)}...`);
517
389
  console.log("-".repeat(60) + "\n");
518
- // Send FINAL frame
390
+ // Send final response to XiaoYi
519
391
  const response = {
520
392
  sessionId: message.sessionId,
521
393
  messageId: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
@@ -535,31 +407,33 @@ exports.xiaoyiPlugin = {
535
407
  const conn = runtime.getConnection();
536
408
  if (conn) {
537
409
  await conn.sendResponse(response, taskId, message.sessionId, true, false);
538
- console.log(`✓ XiaoYi: FINAL frame sent (isFinal=true, append=false)\n`);
410
+ console.log(`✓ XiaoYi: Response sent successfully\n`);
539
411
  }
412
+ else {
413
+ console.error(`✗ XiaoYi: Connection not available\n`);
414
+ }
415
+ // Clear timeout as response was sent successfully
416
+ runtime.markSessionCompleted(message.sessionId);
540
417
  },
541
418
  onIdle: async () => {
542
419
  const elapsed = Date.now() - startTime;
543
420
  console.log("\n" + "=".repeat(60));
544
421
  console.log(`XiaoYi: [IDLE] Processing complete`);
545
422
  console.log(` Total time: ${elapsed}ms`);
546
- console.log(` Streaming frames: ${partialCount}`);
547
423
  console.log(` Final length: ${accumulatedText.length} chars`);
548
424
  console.log("=".repeat(60) + "\n");
549
- // Cleanup event listener if still active
550
- if (unsubscribeAgentEvents && !isCompleted) {
551
- console.log(`XiaoYi: [IDLE] Cleaning up event listener`);
552
- unsubscribeAgentEvents();
553
- unsubscribeAgentEvents = undefined;
554
- }
425
+ // Clear timeout on completion
426
+ runtime.markSessionCompleted(message.sessionId);
555
427
  },
556
428
  },
557
- replyOptions: undefined, // onPartialReply is not used
429
+ replyOptions: undefined,
558
430
  images: images.length > 0 ? images : undefined,
559
431
  });
560
432
  }
561
433
  catch (error) {
562
434
  console.error("XiaoYi: [ERROR] Error dispatching message:", error);
435
+ // Clear timeout on error
436
+ runtime.clearSessionTimeout(message.sessionId);
563
437
  }
564
438
  });
565
439
  // Setup cancel handler
@@ -33,8 +33,8 @@ export declare const XiaoYiConfigSchema: z.ZodObject<{
33
33
  agentId?: string | undefined;
34
34
  accounts?: Record<string, unknown> | undefined;
35
35
  }, {
36
- name?: string | undefined;
37
36
  enabled?: boolean | undefined;
37
+ name?: string | undefined;
38
38
  wsUrl?: string | undefined;
39
39
  ak?: string | undefined;
40
40
  sk?: string | undefined;
package/dist/runtime.d.ts CHANGED
@@ -1,5 +1,13 @@
1
1
  import { XiaoYiWebSocketManager } from "./websocket";
2
2
  import { XiaoYiChannelConfig } from "./types";
3
+ /**
4
+ * Timeout configuration
5
+ */
6
+ export interface TimeoutConfig {
7
+ enabled: boolean;
8
+ duration: number;
9
+ message: string;
10
+ }
3
11
  /**
4
12
  * Runtime state for XiaoYi channel
5
13
  * Manages single WebSocket connection (single account mode)
@@ -10,6 +18,9 @@ export declare class XiaoYiRuntime {
10
18
  private config;
11
19
  private sessionToTaskIdMap;
12
20
  private instanceId;
21
+ private sessionTimeoutMap;
22
+ private sessionTimeoutSent;
23
+ private timeoutConfig;
13
24
  constructor();
14
25
  getInstanceId(): string;
15
26
  /**
@@ -28,6 +39,40 @@ export declare class XiaoYiRuntime {
28
39
  * Stop connection
29
40
  */
30
41
  stop(): void;
42
+ /**
43
+ * Set timeout configuration
44
+ */
45
+ setTimeoutConfig(config: Partial<TimeoutConfig>): void;
46
+ /**
47
+ * Get timeout configuration
48
+ */
49
+ getTimeoutConfig(): TimeoutConfig;
50
+ /**
51
+ * Set timeout for a session
52
+ * @param sessionId - Session ID
53
+ * @param callback - Function to call when timeout occurs
54
+ * @returns The timeout ID (for cancellation)
55
+ */
56
+ setTimeoutForSession(sessionId: string, callback: () => void): NodeJS.Timeout | undefined;
57
+ /**
58
+ * Clear timeout for a session
59
+ * @param sessionId - Session ID
60
+ */
61
+ clearSessionTimeout(sessionId: string): void;
62
+ /**
63
+ * Check if timeout has been sent for a session
64
+ * @param sessionId - Session ID
65
+ */
66
+ isSessionTimeout(sessionId: string): boolean;
67
+ /**
68
+ * Mark session as completed (clear timeout and timeout flag)
69
+ * @param sessionId - Session ID
70
+ */
71
+ markSessionCompleted(sessionId: string): void;
72
+ /**
73
+ * Clear all timeouts
74
+ */
75
+ clearAllTimeouts(): void;
31
76
  /**
32
77
  * Get WebSocket manager
33
78
  */
package/dist/runtime.js CHANGED
@@ -4,6 +4,14 @@ exports.XiaoYiRuntime = void 0;
4
4
  exports.getXiaoYiRuntime = getXiaoYiRuntime;
5
5
  exports.setXiaoYiRuntime = setXiaoYiRuntime;
6
6
  const websocket_1 = require("./websocket");
7
+ /**
8
+ * Default timeout configuration
9
+ */
10
+ const DEFAULT_TIMEOUT_CONFIG = {
11
+ enabled: true,
12
+ duration: 60000, // 60 seconds
13
+ message: "任务还在处理中,请稍后回来查看",
14
+ };
7
15
  /**
8
16
  * Runtime state for XiaoYi channel
9
17
  * Manages single WebSocket connection (single account mode)
@@ -14,6 +22,10 @@ class XiaoYiRuntime {
14
22
  this.pluginRuntime = null; // Store PluginRuntime from OpenClaw
15
23
  this.config = null;
16
24
  this.sessionToTaskIdMap = new Map(); // Map sessionId to taskId
25
+ // Timeout management
26
+ this.sessionTimeoutMap = new Map();
27
+ this.sessionTimeoutSent = new Set();
28
+ this.timeoutConfig = DEFAULT_TIMEOUT_CONFIG;
17
29
  this.instanceId = `runtime_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
18
30
  console.log(`XiaoYi: Created new runtime instance: ${this.instanceId}`);
19
31
  }
@@ -73,6 +85,83 @@ class XiaoYiRuntime {
73
85
  }
74
86
  // Clear session mappings
75
87
  this.sessionToTaskIdMap.clear();
88
+ // Clear all timeouts
89
+ this.clearAllTimeouts();
90
+ }
91
+ /**
92
+ * Set timeout configuration
93
+ */
94
+ setTimeoutConfig(config) {
95
+ this.timeoutConfig = { ...this.timeoutConfig, ...config };
96
+ console.log(`XiaoYi: Timeout config updated:`, this.timeoutConfig);
97
+ }
98
+ /**
99
+ * Get timeout configuration
100
+ */
101
+ getTimeoutConfig() {
102
+ return { ...this.timeoutConfig };
103
+ }
104
+ /**
105
+ * Set timeout for a session
106
+ * @param sessionId - Session ID
107
+ * @param callback - Function to call when timeout occurs
108
+ * @returns The timeout ID (for cancellation)
109
+ */
110
+ setTimeoutForSession(sessionId, callback) {
111
+ if (!this.timeoutConfig.enabled) {
112
+ console.log(`[TIMEOUT] Timeout disabled, skipping for session ${sessionId}`);
113
+ return undefined;
114
+ }
115
+ // Clear existing timeout if any
116
+ this.clearSessionTimeout(sessionId);
117
+ const timeoutId = setTimeout(() => {
118
+ console.log(`[TIMEOUT] Timeout triggered for session ${sessionId}`);
119
+ this.sessionTimeoutMap.delete(sessionId);
120
+ this.sessionTimeoutSent.add(sessionId);
121
+ callback();
122
+ }, this.timeoutConfig.duration);
123
+ this.sessionTimeoutMap.set(sessionId, timeoutId);
124
+ console.log(`[TIMEOUT] ${this.timeoutConfig.duration}ms timeout started for session ${sessionId}`);
125
+ return timeoutId;
126
+ }
127
+ /**
128
+ * Clear timeout for a session
129
+ * @param sessionId - Session ID
130
+ */
131
+ clearSessionTimeout(sessionId) {
132
+ const timeoutId = this.sessionTimeoutMap.get(sessionId);
133
+ if (timeoutId) {
134
+ clearTimeout(timeoutId);
135
+ this.sessionTimeoutMap.delete(sessionId);
136
+ console.log(`[TIMEOUT] Timeout cleared for session ${sessionId}`);
137
+ }
138
+ }
139
+ /**
140
+ * Check if timeout has been sent for a session
141
+ * @param sessionId - Session ID
142
+ */
143
+ isSessionTimeout(sessionId) {
144
+ return this.sessionTimeoutSent.has(sessionId);
145
+ }
146
+ /**
147
+ * Mark session as completed (clear timeout and timeout flag)
148
+ * @param sessionId - Session ID
149
+ */
150
+ markSessionCompleted(sessionId) {
151
+ this.clearSessionTimeout(sessionId);
152
+ this.sessionTimeoutSent.delete(sessionId);
153
+ console.log(`[TIMEOUT] Session ${sessionId} marked as completed`);
154
+ }
155
+ /**
156
+ * Clear all timeouts
157
+ */
158
+ clearAllTimeouts() {
159
+ for (const [sessionId, timeoutId] of this.sessionTimeoutMap.entries()) {
160
+ clearTimeout(timeoutId);
161
+ }
162
+ this.sessionTimeoutMap.clear();
163
+ this.sessionTimeoutSent.clear();
164
+ console.log("[TIMEOUT] All timeouts cleared");
76
165
  }
77
166
  /**
78
167
  * Get WebSocket manager
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynhcj/xiaoyi",
3
- "version": "2.2.4",
3
+ "version": "2.2.6",
4
4
  "description": "XiaoYi channel plugin for OpenClaw",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",