@ynhcj/xiaoyi 2.5.5 → 2.5.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.d.ts +57 -0
- package/dist/channel.js +251 -45
- package/dist/config-schema.d.ts +8 -8
- package/dist/config-schema.js +5 -5
- package/dist/index.d.ts +1 -4
- package/dist/index.js +2 -6
- package/dist/push.d.ts +28 -0
- package/dist/push.js +135 -0
- package/dist/runtime.d.ts +46 -0
- package/dist/runtime.js +115 -1
- package/dist/types.d.ts +21 -0
- package/dist/websocket.d.ts +33 -1
- package/dist/websocket.js +116 -7
- package/package.json +2 -1
package/dist/channel.d.ts
CHANGED
|
@@ -30,6 +30,63 @@ export declare const xiaoyiPlugin: {
|
|
|
30
30
|
media: boolean;
|
|
31
31
|
nativeCommands: boolean;
|
|
32
32
|
};
|
|
33
|
+
/**
|
|
34
|
+
* Config schema for UI form rendering
|
|
35
|
+
*/
|
|
36
|
+
configSchema: {
|
|
37
|
+
schema: {
|
|
38
|
+
type: string;
|
|
39
|
+
properties: {
|
|
40
|
+
enabled: {
|
|
41
|
+
type: string;
|
|
42
|
+
default: boolean;
|
|
43
|
+
description: string;
|
|
44
|
+
};
|
|
45
|
+
wsUrl1: {
|
|
46
|
+
type: string;
|
|
47
|
+
default: string;
|
|
48
|
+
description: string;
|
|
49
|
+
};
|
|
50
|
+
wsUrl2: {
|
|
51
|
+
type: string;
|
|
52
|
+
default: string;
|
|
53
|
+
description: string;
|
|
54
|
+
};
|
|
55
|
+
ak: {
|
|
56
|
+
type: string;
|
|
57
|
+
description: string;
|
|
58
|
+
};
|
|
59
|
+
sk: {
|
|
60
|
+
type: string;
|
|
61
|
+
description: string;
|
|
62
|
+
};
|
|
63
|
+
agentId: {
|
|
64
|
+
type: string;
|
|
65
|
+
description: string;
|
|
66
|
+
};
|
|
67
|
+
debug: {
|
|
68
|
+
type: string;
|
|
69
|
+
default: boolean;
|
|
70
|
+
description: string;
|
|
71
|
+
};
|
|
72
|
+
apiId: {
|
|
73
|
+
type: string;
|
|
74
|
+
default: string;
|
|
75
|
+
description: string;
|
|
76
|
+
};
|
|
77
|
+
pushId: {
|
|
78
|
+
type: string;
|
|
79
|
+
default: string;
|
|
80
|
+
description: string;
|
|
81
|
+
};
|
|
82
|
+
taskTimeoutMs: {
|
|
83
|
+
type: string;
|
|
84
|
+
default: number;
|
|
85
|
+
description: string;
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
};
|
|
33
90
|
onboarding: any;
|
|
34
91
|
/**
|
|
35
92
|
* Config adapter - single account mode
|
package/dist/channel.js
CHANGED
|
@@ -32,6 +32,63 @@ exports.xiaoyiPlugin = {
|
|
|
32
32
|
media: true,
|
|
33
33
|
nativeCommands: false,
|
|
34
34
|
},
|
|
35
|
+
/**
|
|
36
|
+
* Config schema for UI form rendering
|
|
37
|
+
*/
|
|
38
|
+
configSchema: {
|
|
39
|
+
schema: {
|
|
40
|
+
type: "object",
|
|
41
|
+
properties: {
|
|
42
|
+
enabled: {
|
|
43
|
+
type: "boolean",
|
|
44
|
+
default: false,
|
|
45
|
+
description: "Enable XiaoYi channel",
|
|
46
|
+
},
|
|
47
|
+
wsUrl1: {
|
|
48
|
+
type: "string",
|
|
49
|
+
default: "wss://hag.cloud.huawei.com/openclaw/v1/ws/link",
|
|
50
|
+
description: "Primary WebSocket server URL",
|
|
51
|
+
},
|
|
52
|
+
wsUrl2: {
|
|
53
|
+
type: "string",
|
|
54
|
+
default: "wss://116.63.174.231/openclaw/v1/ws/link",
|
|
55
|
+
description: "Secondary WebSocket server URL",
|
|
56
|
+
},
|
|
57
|
+
ak: {
|
|
58
|
+
type: "string",
|
|
59
|
+
description: "Access Key",
|
|
60
|
+
},
|
|
61
|
+
sk: {
|
|
62
|
+
type: "string",
|
|
63
|
+
description: "Secret Key",
|
|
64
|
+
},
|
|
65
|
+
agentId: {
|
|
66
|
+
type: "string",
|
|
67
|
+
description: "Agent ID",
|
|
68
|
+
},
|
|
69
|
+
debug: {
|
|
70
|
+
type: "boolean",
|
|
71
|
+
default: false,
|
|
72
|
+
description: "Enable debug logging",
|
|
73
|
+
},
|
|
74
|
+
apiId: {
|
|
75
|
+
type: "string",
|
|
76
|
+
default: "",
|
|
77
|
+
description: "API ID for push notifications",
|
|
78
|
+
},
|
|
79
|
+
pushId: {
|
|
80
|
+
type: "string",
|
|
81
|
+
default: "",
|
|
82
|
+
description: "Push ID for push notifications",
|
|
83
|
+
},
|
|
84
|
+
taskTimeoutMs: {
|
|
85
|
+
type: "number",
|
|
86
|
+
default: 3600000,
|
|
87
|
+
description: "Task timeout in milliseconds (default: 1 hour)",
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
35
92
|
onboarding: onboarding_1.xiaoyiOnboardingAdapter,
|
|
36
93
|
/**
|
|
37
94
|
* Config adapter - single account mode
|
|
@@ -251,10 +308,13 @@ exports.xiaoyiPlugin = {
|
|
|
251
308
|
// where the outer scope's resolvedAccount might become unavailable
|
|
252
309
|
const messageHandlerAgentId = resolvedAccount.config?.agentId;
|
|
253
310
|
const messageHandlerAccountId = resolvedAccount.accountId;
|
|
311
|
+
const messageHandlerConfig = resolvedAccount.config;
|
|
254
312
|
if (!messageHandlerAgentId) {
|
|
255
313
|
console.error("XiaoYi: [FATAL] agentId not available in resolvedAccount.config");
|
|
256
314
|
return;
|
|
257
315
|
}
|
|
316
|
+
// Set task timeout time from configuration
|
|
317
|
+
runtime.setTaskTimeout(messageHandlerConfig.taskTimeoutMs || 3600000);
|
|
258
318
|
console.log(`XiaoYi: [Message Handler] Stored config values - agentId: ${messageHandlerAgentId}, accountId: ${messageHandlerAccountId}`);
|
|
259
319
|
// For message/stream, prioritize params.sessionId, fallback to top-level sessionId
|
|
260
320
|
const sessionId = message.params?.sessionId || message.sessionId;
|
|
@@ -418,7 +478,6 @@ exports.xiaoyiPlugin = {
|
|
|
418
478
|
const startTime = Date.now();
|
|
419
479
|
let accumulatedText = "";
|
|
420
480
|
let sentTextLength = 0; // Track sent text length for streaming
|
|
421
|
-
let hasSentFinal = false; // Track if final content has been sent (to prevent duplicate isFinal=true)
|
|
422
481
|
// ==================== CREATE ABORT CONTROLLER ====================
|
|
423
482
|
// Create AbortController for this session to allow cancelation
|
|
424
483
|
const abortControllerResult = runtime.createAbortControllerForSession(sessionId);
|
|
@@ -428,24 +487,62 @@ exports.xiaoyiPlugin = {
|
|
|
428
487
|
}
|
|
429
488
|
const { controller: abortController, signal: abortSignal } = abortControllerResult;
|
|
430
489
|
// ================================================================
|
|
431
|
-
// ====================
|
|
432
|
-
// Start
|
|
433
|
-
// Will trigger
|
|
490
|
+
// ==================== 1-HOUR TASK TIMEOUT PROTECTION ====================
|
|
491
|
+
// Start 1-hour task timeout timer
|
|
492
|
+
// Will trigger once after 1 hour if no response received
|
|
493
|
+
console.log(`[TASK TIMEOUT] Starting ${messageHandlerConfig.taskTimeoutMs || 3600000}ms task timeout protection for session ${sessionId}`);
|
|
494
|
+
// Define task timeout handler (will be called once after 1 hour)
|
|
495
|
+
const createTaskTimeoutHandler = () => {
|
|
496
|
+
return async (timeoutSessionId, timeoutTaskId) => {
|
|
497
|
+
const elapsed = Date.now() - startTime;
|
|
498
|
+
console.log("\n" + "=".repeat(60));
|
|
499
|
+
console.log(`[TASK TIMEOUT] 1-hour timeout triggered for session ${sessionId}`);
|
|
500
|
+
console.log(` Elapsed: ${elapsed}ms`);
|
|
501
|
+
console.log(` Task ID: ${currentTaskId}`);
|
|
502
|
+
console.log("=".repeat(60) + "\n");
|
|
503
|
+
const conn = runtime.getConnection();
|
|
504
|
+
if (conn) {
|
|
505
|
+
try {
|
|
506
|
+
// Send default message with isFinal=true
|
|
507
|
+
await conn.sendResponse({
|
|
508
|
+
sessionId: timeoutSessionId,
|
|
509
|
+
messageId: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
510
|
+
timestamp: Date.now(),
|
|
511
|
+
agentId: messageHandlerAgentId,
|
|
512
|
+
sender: { id: messageHandlerAgentId, name: "OpenClaw Agent", type: "agent" },
|
|
513
|
+
content: { type: "text", text: "任务还在处理中,完成后将提醒您~" },
|
|
514
|
+
status: "success",
|
|
515
|
+
}, timeoutTaskId, timeoutSessionId, true, false); // isFinal=true
|
|
516
|
+
console.log(`[TASK TIMEOUT] Default message sent (isFinal=true) to session ${timeoutSessionId}\n`);
|
|
517
|
+
}
|
|
518
|
+
catch (error) {
|
|
519
|
+
console.error(`[TASK TIMEOUT] Failed to send default message:`, error);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
// Cancel 60-second periodic timeout
|
|
523
|
+
runtime.clearSessionTimeout(timeoutSessionId);
|
|
524
|
+
// Mark as waiting for push state
|
|
525
|
+
runtime.markSessionWaitingForPush(timeoutSessionId, timeoutTaskId);
|
|
526
|
+
};
|
|
527
|
+
};
|
|
528
|
+
// Start 1-hour task timeout timer
|
|
529
|
+
runtime.setTaskTimeoutForSession(sessionId, currentTaskId, createTaskTimeoutHandler());
|
|
530
|
+
// Also start 60-second periodic timeout for status updates (for messages before 1-hour timeout)
|
|
434
531
|
const timeoutConfig = runtime.getTimeoutConfig();
|
|
435
|
-
|
|
436
|
-
// Define periodic timeout handler (will be called every 60 seconds)
|
|
437
|
-
const createTimeoutHandler = () => {
|
|
532
|
+
const createPeriodicTimeoutHandler = () => {
|
|
438
533
|
return async () => {
|
|
534
|
+
// Skip if already waiting for push (1-hour timeout already triggered)
|
|
535
|
+
if (runtime.isSessionWaitingForPush(sessionId, currentTaskId)) {
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
439
538
|
const elapsed = Date.now() - startTime;
|
|
440
539
|
console.log("\n" + "=".repeat(60));
|
|
441
|
-
console.log(`[TIMEOUT]
|
|
540
|
+
console.log(`[TIMEOUT] Periodic timeout triggered for session ${sessionId}`);
|
|
442
541
|
console.log(` Elapsed: ${elapsed}ms`);
|
|
443
|
-
console.log(` Task ID: ${currentTaskId}`);
|
|
444
542
|
console.log("=".repeat(60) + "\n");
|
|
445
543
|
const conn = runtime.getConnection();
|
|
446
544
|
if (conn) {
|
|
447
545
|
try {
|
|
448
|
-
// Send status update to keep conversation active
|
|
449
546
|
await conn.sendStatusUpdate(currentTaskId, sessionId, timeoutConfig.message);
|
|
450
547
|
console.log(`[TIMEOUT] Status update sent successfully to session ${sessionId}\n`);
|
|
451
548
|
}
|
|
@@ -453,18 +550,15 @@ exports.xiaoyiPlugin = {
|
|
|
453
550
|
console.error(`[TIMEOUT] Failed to send status update:`, error);
|
|
454
551
|
}
|
|
455
552
|
}
|
|
456
|
-
else {
|
|
457
|
-
console.error(`[TIMEOUT] Connection not available, cannot send status update\n`);
|
|
458
|
-
}
|
|
459
|
-
// Note: Timeout will trigger again in 60 seconds if still active
|
|
460
553
|
};
|
|
461
554
|
};
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
// ==================== END TIMEOUT PROTECTION ====================
|
|
555
|
+
runtime.setTimeoutForSession(sessionId, createPeriodicTimeoutHandler());
|
|
556
|
+
// ==================== END TASK TIMEOUT PROTECTION ====================
|
|
465
557
|
// ==================== CREATE STREAMING DISPATCHER ====================
|
|
466
|
-
//
|
|
467
|
-
|
|
558
|
+
// ==================== DISPATCHER OPTIONS ====================
|
|
559
|
+
// Define dispatcher options for dispatchInboundMessageWithBufferedDispatcher
|
|
560
|
+
// This uses the standard OpenClaw pattern which properly handles dispatcher lifecycle
|
|
561
|
+
const dispatcherOptions = {
|
|
468
562
|
humanDelay: 0,
|
|
469
563
|
onReplyStart: async () => {
|
|
470
564
|
const elapsed = Date.now() - startTime;
|
|
@@ -494,6 +588,11 @@ exports.xiaoyiPlugin = {
|
|
|
494
588
|
// Check multiple sources: payload.status, payload.queuedFinal, AND info.kind
|
|
495
589
|
// info.kind is the most reliable indicator for final messages
|
|
496
590
|
const isFinal = payloadStatus === "final" || payload.queuedFinal === true || kind === "final";
|
|
591
|
+
// If session is waiting for push (1-hour timeout occurred), ignore non-final responses
|
|
592
|
+
if (runtime.isSessionWaitingForPush(sessionId, currentTaskId) && !payload.queuedFinal && info.kind !== "final") {
|
|
593
|
+
console.log(`[TASK TIMEOUT] Ignoring non-final response for session ${sessionId} (already timed out)`);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
497
596
|
accumulatedText = text;
|
|
498
597
|
console.log("\n" + "█".repeat(70));
|
|
499
598
|
console.log("📨 [DELIVER] Payload received");
|
|
@@ -547,7 +646,12 @@ exports.xiaoyiPlugin = {
|
|
|
547
646
|
// ==================== END FIX ====================
|
|
548
647
|
const responseStatus = isFinal ? "success" : "processing";
|
|
549
648
|
const incrementalText = text.slice(sentTextLength);
|
|
550
|
-
|
|
649
|
+
// ==================== FIX: Always send isFinal=false in deliver ====================
|
|
650
|
+
// All responses from deliver callback are sent with isFinal=false
|
|
651
|
+
// The final isFinal=true will be sent in onIdle callback when ALL processing is complete
|
|
652
|
+
const shouldSendFinal = false;
|
|
653
|
+
// Always use append=true for all responses
|
|
654
|
+
const isAppend = true;
|
|
551
655
|
if (incrementalText.length > 0 || isFinal) {
|
|
552
656
|
console.log("\n" + "-".repeat(60));
|
|
553
657
|
console.log("XiaoYi: [STREAM] Sending response");
|
|
@@ -571,16 +675,9 @@ exports.xiaoyiPlugin = {
|
|
|
571
675
|
},
|
|
572
676
|
status: responseStatus,
|
|
573
677
|
};
|
|
574
|
-
// ==================== FIX: Prevent duplicate final messages ====================
|
|
575
|
-
// Only send isFinal=true if we haven't sent it before AND this is actually the final message
|
|
576
|
-
const shouldSendFinal = isFinal && !hasSentFinal;
|
|
577
678
|
try {
|
|
578
679
|
await conn.sendResponse(response, currentTaskId, sessionId, shouldSendFinal, isAppend);
|
|
579
|
-
console.log("✓ Sent (status=" + responseStatus + ", isFinal=
|
|
580
|
-
// Mark that we've sent a final message (even if we're still processing subagent responses)
|
|
581
|
-
if (isFinal) {
|
|
582
|
-
hasSentFinal = true;
|
|
583
|
-
}
|
|
680
|
+
console.log("✓ Sent (status=" + responseStatus + ", isFinal=false, append=" + isAppend + ")\n");
|
|
584
681
|
}
|
|
585
682
|
catch (error) {
|
|
586
683
|
console.error("✗ Failed to send:", error);
|
|
@@ -606,7 +703,14 @@ exports.xiaoyiPlugin = {
|
|
|
606
703
|
console.error("XiaoYi: [ERROR] " + info.kind + " failed: " + String(err));
|
|
607
704
|
console.log("=".repeat(60) + "\n");
|
|
608
705
|
runtime.clearSessionTimeout(sessionId);
|
|
706
|
+
runtime.clearTaskTimeoutForSession(sessionId);
|
|
707
|
+
runtime.clearSessionWaitingForPush(sessionId, currentTaskId);
|
|
609
708
|
runtime.clearAbortControllerForSession(sessionId);
|
|
709
|
+
// Check if session was cleared
|
|
710
|
+
const conn = runtime.getConnection();
|
|
711
|
+
if (conn && conn.isSessionPendingCleanup(sessionId)) {
|
|
712
|
+
conn.forceCleanupSession(sessionId);
|
|
713
|
+
}
|
|
610
714
|
runtime.markSessionCompleted(sessionId);
|
|
611
715
|
},
|
|
612
716
|
onIdle: async () => {
|
|
@@ -615,27 +719,101 @@ exports.xiaoyiPlugin = {
|
|
|
615
719
|
console.log("XiaoYi: [IDLE] Processing complete");
|
|
616
720
|
console.log(" Total time: " + elapsed + "ms");
|
|
617
721
|
console.log("=".repeat(60) + "\n");
|
|
618
|
-
//
|
|
619
|
-
|
|
620
|
-
//
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
// ====================
|
|
722
|
+
// Clear 1-hour task timeout timer
|
|
723
|
+
runtime.clearTaskTimeoutForSession(sessionId);
|
|
724
|
+
// ==================== CHECK IF SESSION WAS CLEARED ====================
|
|
725
|
+
const conn = runtime.getConnection();
|
|
726
|
+
const isPendingCleanup = conn && conn.isSessionPendingCleanup(sessionId);
|
|
727
|
+
const isWaitingForPush = runtime.isSessionWaitingForPush(sessionId, currentTaskId);
|
|
728
|
+
// ==================== PUSH NOTIFICATION LOGIC ====================
|
|
729
|
+
// Send push if task timeout was triggered (regardless of session cleanup status)
|
|
730
|
+
// This ensures users get notified when long-running tasks complete
|
|
731
|
+
if (isWaitingForPush && accumulatedText.length > 0) {
|
|
732
|
+
const pushReason = isPendingCleanup
|
|
733
|
+
? `Session ${sessionId} was cleared`
|
|
734
|
+
: `Session ${sessionId} task timeout triggered`;
|
|
735
|
+
console.log(`[CLEANUP] ${pushReason}, sending push notification`);
|
|
736
|
+
try {
|
|
737
|
+
const { XiaoYiPushService } = require("./push");
|
|
738
|
+
const pushService = new XiaoYiPushService(messageHandlerConfig);
|
|
739
|
+
if (pushService.isConfigured()) {
|
|
740
|
+
// Generate summary
|
|
741
|
+
const summary = accumulatedText.length > 30
|
|
742
|
+
? accumulatedText.substring(0, 30) + "..."
|
|
743
|
+
: accumulatedText;
|
|
744
|
+
await pushService.sendPush(summary, "后台任务已完成:" + summary);
|
|
745
|
+
console.log("✓ [CLEANUP] Push notification sent\n");
|
|
746
|
+
// Clear push waiting state for this specific task
|
|
747
|
+
runtime.clearSessionWaitingForPush(sessionId, currentTaskId);
|
|
748
|
+
}
|
|
749
|
+
else {
|
|
750
|
+
console.log("[CLEANUP] Push not configured, skipping notification");
|
|
751
|
+
runtime.clearSessionWaitingForPush(sessionId, currentTaskId);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
catch (error) {
|
|
755
|
+
console.error("[CLEANUP] Error sending push:", error);
|
|
756
|
+
runtime.clearSessionWaitingForPush(sessionId, currentTaskId);
|
|
757
|
+
}
|
|
758
|
+
// If session was cleared, update cleanup state
|
|
759
|
+
if (isPendingCleanup) {
|
|
760
|
+
conn?.updateAccumulatedTextForCleanup(sessionId, accumulatedText);
|
|
761
|
+
conn?.forceCleanupSession(sessionId);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
// ==================== NORMAL WEBSOCKET FLOW (no timeout triggered) ====================
|
|
765
|
+
else if (!isPendingCleanup) {
|
|
766
|
+
// Normal flow: send WebSocket response (no timeout, session still active)
|
|
767
|
+
const conn = runtime.getConnection();
|
|
768
|
+
if (conn) {
|
|
769
|
+
try {
|
|
770
|
+
await conn.sendResponse({
|
|
771
|
+
sessionId: sessionId,
|
|
772
|
+
messageId: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
773
|
+
timestamp: Date.now(),
|
|
774
|
+
agentId: messageHandlerAgentId,
|
|
775
|
+
sender: {
|
|
776
|
+
id: messageHandlerAgentId,
|
|
777
|
+
name: "OpenClaw Agent",
|
|
778
|
+
type: "agent",
|
|
779
|
+
},
|
|
780
|
+
content: {
|
|
781
|
+
type: "text",
|
|
782
|
+
text: accumulatedText,
|
|
783
|
+
},
|
|
784
|
+
status: "success",
|
|
785
|
+
}, currentTaskId, sessionId, true, true); // isFinal=true, append=true
|
|
786
|
+
console.log("✓ [IDLE] Final response sent (isFinal=true)\n");
|
|
787
|
+
}
|
|
788
|
+
catch (error) {
|
|
789
|
+
console.error("✗ [IDLE] Failed to send final response:", error);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
// ==================== SESSION CLEARED BUT NO TIMEOUT ====================
|
|
794
|
+
else {
|
|
795
|
+
// Session was cleared but no timeout triggered - edge case, just cleanup
|
|
796
|
+
console.log(`[CLEANUP] Session ${sessionId} was cleared but no push needed`);
|
|
797
|
+
conn?.forceCleanupSession(sessionId);
|
|
798
|
+
}
|
|
625
799
|
// This is called AFTER all processing is done (including subagents)
|
|
626
800
|
// NOW we can safely mark the session as completed
|
|
627
801
|
runtime.clearAbortControllerForSession(sessionId);
|
|
628
802
|
runtime.markSessionCompleted(sessionId);
|
|
629
803
|
console.log("[CLEANUP] Session marked as completed in onIdle\n");
|
|
630
804
|
},
|
|
631
|
-
}
|
|
805
|
+
};
|
|
632
806
|
try {
|
|
633
|
-
|
|
807
|
+
// Use standard OpenClaw pattern with dispatchReplyWithBufferedBlockDispatcher
|
|
808
|
+
// This properly handles dispatcher lifecycle:
|
|
809
|
+
// 1. Calls dispatcher.markComplete() after run() completes
|
|
810
|
+
// 2. Waits for waitForIdle() to ensure all deliveries are done
|
|
811
|
+
// 3. Then calls markDispatchIdle() in the finally block
|
|
812
|
+
const result = await pluginRuntime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
634
813
|
ctx: msgContext,
|
|
635
814
|
cfg: config,
|
|
636
|
-
|
|
815
|
+
dispatcherOptions: dispatcherOptions,
|
|
637
816
|
replyOptions: {
|
|
638
|
-
...replyOptions,
|
|
639
817
|
abortSignal: abortSignal,
|
|
640
818
|
},
|
|
641
819
|
});
|
|
@@ -687,7 +865,7 @@ exports.xiaoyiPlugin = {
|
|
|
687
865
|
status: "success",
|
|
688
866
|
};
|
|
689
867
|
// Send response with isFinal=true to close THIS request
|
|
690
|
-
await conn.sendResponse(response, currentTaskId, sessionId, true,
|
|
868
|
+
await conn.sendResponse(response, currentTaskId, sessionId, true, true);
|
|
691
869
|
console.log("✓ [NO OUTPUT] Response sent to user\n");
|
|
692
870
|
}
|
|
693
871
|
catch (error) {
|
|
@@ -698,14 +876,16 @@ exports.xiaoyiPlugin = {
|
|
|
698
876
|
// The original Agent might still be running and needs these resources
|
|
699
877
|
// onIdle will be called when the original Agent completes
|
|
700
878
|
console.log("[NO OUTPUT] Keeping resources alive for potential background Agent\n");
|
|
701
|
-
markDispatchIdle()
|
|
879
|
+
// Note: No need to call markDispatchIdle() manually
|
|
880
|
+
// dispatchInboundMessageWithBufferedDispatcher handles this in its finally block
|
|
702
881
|
}
|
|
703
882
|
else {
|
|
704
883
|
// Scenario 2: Normal execution with output
|
|
705
884
|
// - Agent produced output synchronously
|
|
706
885
|
// - All cleanup is already handled in deliver/onIdle callbacks
|
|
707
886
|
console.log("[NORMAL] Agent produced output, cleanup handled in callbacks");
|
|
708
|
-
markDispatchIdle()
|
|
887
|
+
// Note: No need to call markDispatchIdle() manually
|
|
888
|
+
// dispatchInboundMessageWithBufferedDispatcher handles this in its finally block
|
|
709
889
|
}
|
|
710
890
|
// ==================== END ANALYSIS ====================
|
|
711
891
|
}
|
|
@@ -713,12 +893,14 @@ exports.xiaoyiPlugin = {
|
|
|
713
893
|
console.error("XiaoYi: [ERROR] Error dispatching message:", error);
|
|
714
894
|
// Clear timeout on error
|
|
715
895
|
runtime.clearSessionTimeout(sessionId);
|
|
896
|
+
runtime.clearTaskTimeoutForSession(sessionId);
|
|
897
|
+
runtime.clearSessionWaitingForPush(sessionId, currentTaskId);
|
|
716
898
|
// Clear abort controller on error
|
|
717
899
|
runtime.clearAbortControllerForSession(sessionId);
|
|
718
900
|
// Mark session as completed on error
|
|
719
901
|
runtime.markSessionCompleted(sessionId);
|
|
720
|
-
//
|
|
721
|
-
|
|
902
|
+
// Note: No need to call markDispatchIdle() manually
|
|
903
|
+
// dispatchInboundMessageWithBufferedDispatcher handles this in its finally block
|
|
722
904
|
}
|
|
723
905
|
}
|
|
724
906
|
catch (error) {
|
|
@@ -742,9 +924,33 @@ exports.xiaoyiPlugin = {
|
|
|
742
924
|
else {
|
|
743
925
|
console.log(`[CANCEL] No active agent run found for session ${sessionId}`);
|
|
744
926
|
}
|
|
745
|
-
// Clear timeout as the session is being canceled
|
|
927
|
+
// Clear timeout and push state as the session is being canceled
|
|
928
|
+
runtime.clearTaskTimeoutForSession(sessionId);
|
|
929
|
+
runtime.clearSessionWaitingForPush(sessionId, data.taskId);
|
|
746
930
|
runtime.markSessionCompleted(sessionId);
|
|
747
931
|
});
|
|
932
|
+
// Handle clear context events
|
|
933
|
+
connection.on("clear", async (data) => {
|
|
934
|
+
const { sessionId, serverId } = data;
|
|
935
|
+
console.log("\n" + "=".repeat(60));
|
|
936
|
+
console.log("[CLEAR] Context cleared by user");
|
|
937
|
+
console.log(` Session: ${sessionId}`);
|
|
938
|
+
console.log("=".repeat(60) + "\n");
|
|
939
|
+
// Check if there's an active task for this session
|
|
940
|
+
const hasActiveTask = runtime.isSessionActive(sessionId);
|
|
941
|
+
if (hasActiveTask) {
|
|
942
|
+
console.log(`[CLEAR] Active task exists for session ${sessionId}, will continue in background`);
|
|
943
|
+
// Session is already marked for cleanup in websocket.ts
|
|
944
|
+
// Just track that we're waiting for completion
|
|
945
|
+
}
|
|
946
|
+
else {
|
|
947
|
+
console.log(`[CLEAR] No active task for session ${sessionId}, clean up immediately`);
|
|
948
|
+
const conn = runtime.getConnection();
|
|
949
|
+
if (conn) {
|
|
950
|
+
conn.forceCleanupSession(sessionId);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
});
|
|
748
954
|
// Mark handlers as registered to prevent duplicate registration
|
|
749
955
|
handlersRegistered = true;
|
|
750
956
|
}
|
package/dist/config-schema.d.ts
CHANGED
|
@@ -8,8 +8,10 @@ export declare const XiaoYiConfigSchema: z.ZodObject<{
|
|
|
8
8
|
name: z.ZodOptional<z.ZodString>;
|
|
9
9
|
/** Whether this channel is enabled */
|
|
10
10
|
enabled: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
11
|
-
/** WebSocket URL
|
|
12
|
-
|
|
11
|
+
/** First WebSocket server URL */
|
|
12
|
+
wsUrl1: z.ZodDefault<z.ZodOptional<z.ZodString>>;
|
|
13
|
+
/** Second WebSocket server URL */
|
|
14
|
+
wsUrl2: z.ZodDefault<z.ZodOptional<z.ZodString>>;
|
|
13
15
|
/** Access Key for authentication */
|
|
14
16
|
ak: z.ZodOptional<z.ZodString>;
|
|
15
17
|
/** Secret Key for authentication */
|
|
@@ -18,27 +20,25 @@ export declare const XiaoYiConfigSchema: z.ZodObject<{
|
|
|
18
20
|
agentId: z.ZodOptional<z.ZodString>;
|
|
19
21
|
/** Enable debug logging */
|
|
20
22
|
debug: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
21
|
-
/** Enable streaming responses (default: false) */
|
|
22
|
-
enableStreaming: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
23
23
|
/** Multi-account configuration */
|
|
24
24
|
accounts: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
25
25
|
}, "strip", z.ZodTypeAny, {
|
|
26
26
|
enabled?: boolean;
|
|
27
|
-
|
|
27
|
+
wsUrl1?: string;
|
|
28
|
+
wsUrl2?: string;
|
|
28
29
|
ak?: string;
|
|
29
30
|
sk?: string;
|
|
30
31
|
agentId?: string;
|
|
31
|
-
enableStreaming?: boolean;
|
|
32
32
|
name?: string;
|
|
33
33
|
debug?: boolean;
|
|
34
34
|
accounts?: Record<string, unknown>;
|
|
35
35
|
}, {
|
|
36
36
|
enabled?: boolean;
|
|
37
|
-
|
|
37
|
+
wsUrl1?: string;
|
|
38
|
+
wsUrl2?: string;
|
|
38
39
|
ak?: string;
|
|
39
40
|
sk?: string;
|
|
40
41
|
agentId?: string;
|
|
41
|
-
enableStreaming?: boolean;
|
|
42
42
|
name?: string;
|
|
43
43
|
debug?: boolean;
|
|
44
44
|
accounts?: Record<string, unknown>;
|
package/dist/config-schema.js
CHANGED
|
@@ -10,9 +10,11 @@ exports.XiaoYiConfigSchema = zod_1.z.object({
|
|
|
10
10
|
/** Account name (optional display name) */
|
|
11
11
|
name: zod_1.z.string().optional(),
|
|
12
12
|
/** Whether this channel is enabled */
|
|
13
|
-
enabled: zod_1.z.boolean().optional().default(
|
|
14
|
-
/** WebSocket URL
|
|
15
|
-
|
|
13
|
+
enabled: zod_1.z.boolean().optional().default(false),
|
|
14
|
+
/** First WebSocket server URL */
|
|
15
|
+
wsUrl1: zod_1.z.string().optional().default("wss://hag.cloud.huawei.com/openclaw/v1/ws/link"),
|
|
16
|
+
/** Second WebSocket server URL */
|
|
17
|
+
wsUrl2: zod_1.z.string().optional().default("wss://116.63.174.231/openclaw/v1/ws/link"),
|
|
16
18
|
/** Access Key for authentication */
|
|
17
19
|
ak: zod_1.z.string().optional(),
|
|
18
20
|
/** Secret Key for authentication */
|
|
@@ -21,8 +23,6 @@ exports.XiaoYiConfigSchema = zod_1.z.object({
|
|
|
21
23
|
agentId: zod_1.z.string().optional(),
|
|
22
24
|
/** Enable debug logging */
|
|
23
25
|
debug: zod_1.z.boolean().optional().default(false),
|
|
24
|
-
/** Enable streaming responses (default: false) */
|
|
25
|
-
enableStreaming: zod_1.z.boolean().optional().default(false),
|
|
26
26
|
/** Multi-account configuration */
|
|
27
27
|
accounts: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional(),
|
|
28
28
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -14,13 +14,10 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
|
14
14
|
* "wsUrl2": "ws://localhost:8766/ws/link",
|
|
15
15
|
* "ak": "test_ak",
|
|
16
16
|
* "sk": "test_sk",
|
|
17
|
-
* "agentId": "your-agent-id"
|
|
18
|
-
* "enableStreaming": true
|
|
17
|
+
* "agentId": "your-agent-id"
|
|
19
18
|
* }
|
|
20
19
|
* }
|
|
21
20
|
* }
|
|
22
|
-
*
|
|
23
|
-
* Backward compatibility: Can use "wsUrl" instead of "wsUrl1" (wsUrl2 will use default)
|
|
24
21
|
*/
|
|
25
22
|
declare const plugin: {
|
|
26
23
|
id: string;
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const plugin_sdk_1 = require("openclaw/plugin-sdk");
|
|
4
3
|
const channel_1 = require("./channel");
|
|
5
4
|
const runtime_1 = require("./runtime");
|
|
6
5
|
/**
|
|
@@ -18,19 +17,16 @@ const runtime_1 = require("./runtime");
|
|
|
18
17
|
* "wsUrl2": "ws://localhost:8766/ws/link",
|
|
19
18
|
* "ak": "test_ak",
|
|
20
19
|
* "sk": "test_sk",
|
|
21
|
-
* "agentId": "your-agent-id"
|
|
22
|
-
* "enableStreaming": true
|
|
20
|
+
* "agentId": "your-agent-id"
|
|
23
21
|
* }
|
|
24
22
|
* }
|
|
25
23
|
* }
|
|
26
|
-
*
|
|
27
|
-
* Backward compatibility: Can use "wsUrl" instead of "wsUrl1" (wsUrl2 will use default)
|
|
28
24
|
*/
|
|
29
25
|
const plugin = {
|
|
30
26
|
id: "xiaoyi",
|
|
31
27
|
name: "XiaoYi Channel",
|
|
32
28
|
description: "XiaoYi channel plugin with A2A protocol support",
|
|
33
|
-
configSchema:
|
|
29
|
+
configSchema: undefined,
|
|
34
30
|
register(api) {
|
|
35
31
|
console.log("XiaoYi: register() called - START");
|
|
36
32
|
// Set runtime for managing WebSocket connections
|
package/dist/push.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { XiaoYiChannelConfig } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Push message sending service
|
|
4
|
+
* Sends notifications to XiaoYi clients via webhook API
|
|
5
|
+
*/
|
|
6
|
+
export declare class XiaoYiPushService {
|
|
7
|
+
private config;
|
|
8
|
+
private readonly pushUrl;
|
|
9
|
+
constructor(config: XiaoYiChannelConfig);
|
|
10
|
+
/**
|
|
11
|
+
* Check if push functionality is configured
|
|
12
|
+
*/
|
|
13
|
+
isConfigured(): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Generate HMAC-SHA256 signature
|
|
16
|
+
*/
|
|
17
|
+
private generateSignature;
|
|
18
|
+
/**
|
|
19
|
+
* Generate UUID
|
|
20
|
+
*/
|
|
21
|
+
private generateUUID;
|
|
22
|
+
/**
|
|
23
|
+
* Send push notification (with summary text)
|
|
24
|
+
* @param text - Summary text to send (e.g., first 30 characters)
|
|
25
|
+
* @param pushText - Push notification message (e.g., "任务已完成:xxx...")
|
|
26
|
+
*/
|
|
27
|
+
sendPush(text: string, pushText: string): Promise<boolean>;
|
|
28
|
+
}
|
package/dist/push.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.XiaoYiPushService = void 0;
|
|
37
|
+
const crypto = __importStar(require("crypto"));
|
|
38
|
+
/**
|
|
39
|
+
* Push message sending service
|
|
40
|
+
* Sends notifications to XiaoYi clients via webhook API
|
|
41
|
+
*/
|
|
42
|
+
class XiaoYiPushService {
|
|
43
|
+
constructor(config) {
|
|
44
|
+
this.pushUrl = "https://hag.cloud.huawei.com/open-ability-agent/v1/agent-webhook";
|
|
45
|
+
this.config = config;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Check if push functionality is configured
|
|
49
|
+
*/
|
|
50
|
+
isConfigured() {
|
|
51
|
+
return Boolean(this.config.apiId?.trim() &&
|
|
52
|
+
this.config.pushId?.trim() &&
|
|
53
|
+
this.config.ak?.trim() &&
|
|
54
|
+
this.config.sk?.trim());
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Generate HMAC-SHA256 signature
|
|
58
|
+
*/
|
|
59
|
+
generateSignature(timestamp) {
|
|
60
|
+
const hmac = crypto.createHmac("sha256", this.config.sk);
|
|
61
|
+
hmac.update(timestamp);
|
|
62
|
+
return hmac.digest().toString("base64");
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Generate UUID
|
|
66
|
+
*/
|
|
67
|
+
generateUUID() {
|
|
68
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
69
|
+
const r = (Math.random() * 16) | 0;
|
|
70
|
+
const v = c === "x" ? r : (r & 0x3) | 0x8;
|
|
71
|
+
return v.toString(16);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Send push notification (with summary text)
|
|
76
|
+
* @param text - Summary text to send (e.g., first 30 characters)
|
|
77
|
+
* @param pushText - Push notification message (e.g., "任务已完成:xxx...")
|
|
78
|
+
*/
|
|
79
|
+
async sendPush(text, pushText) {
|
|
80
|
+
if (!this.isConfigured()) {
|
|
81
|
+
console.log("[PUSH] Push not configured, skipping");
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const timestamp = Date.now().toString();
|
|
86
|
+
const signature = this.generateSignature(timestamp);
|
|
87
|
+
const messageId = this.generateUUID();
|
|
88
|
+
const payload = {
|
|
89
|
+
jsonrpc: "2.0",
|
|
90
|
+
id: messageId,
|
|
91
|
+
result: {
|
|
92
|
+
id: this.generateUUID(),
|
|
93
|
+
apiId: this.config.apiId,
|
|
94
|
+
pushId: this.config.pushId,
|
|
95
|
+
pushText: pushText,
|
|
96
|
+
kind: "task",
|
|
97
|
+
artifacts: [{
|
|
98
|
+
artifactId: this.generateUUID(),
|
|
99
|
+
parts: [{
|
|
100
|
+
kind: "text",
|
|
101
|
+
text: text, // Summary text
|
|
102
|
+
}]
|
|
103
|
+
}],
|
|
104
|
+
status: { state: "completed" }
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
console.log(`[PUSH] Sending push notification: ${pushText}`);
|
|
108
|
+
const response = await fetch(this.pushUrl, {
|
|
109
|
+
method: "POST",
|
|
110
|
+
headers: {
|
|
111
|
+
"Content-Type": "application/json",
|
|
112
|
+
"Accept": "application/json",
|
|
113
|
+
"x-hag-trace-id": this.generateUUID(),
|
|
114
|
+
"X-Access-Key": this.config.ak,
|
|
115
|
+
"X-Sign": signature,
|
|
116
|
+
"X-Ts": timestamp,
|
|
117
|
+
},
|
|
118
|
+
body: JSON.stringify(payload),
|
|
119
|
+
});
|
|
120
|
+
if (response.ok) {
|
|
121
|
+
console.log("[PUSH] Push notification sent successfully");
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
console.error(`[PUSH] Failed: HTTP ${response.status}`);
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
console.error("[PUSH] Error:", error);
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
exports.XiaoYiPushService = XiaoYiPushService;
|
package/dist/runtime.d.ts
CHANGED
|
@@ -23,6 +23,11 @@ export declare class XiaoYiRuntime {
|
|
|
23
23
|
private timeoutConfig;
|
|
24
24
|
private sessionAbortControllerMap;
|
|
25
25
|
private sessionActiveRunMap;
|
|
26
|
+
private sessionStartTimeMap;
|
|
27
|
+
private static readonly SESSION_STALE_TIMEOUT_MS;
|
|
28
|
+
private sessionTaskTimeoutMap;
|
|
29
|
+
private sessionPushPendingMap;
|
|
30
|
+
private taskTimeoutMs;
|
|
26
31
|
constructor();
|
|
27
32
|
getInstanceId(): string;
|
|
28
33
|
/**
|
|
@@ -115,6 +120,7 @@ export declare class XiaoYiRuntime {
|
|
|
115
120
|
} | null;
|
|
116
121
|
/**
|
|
117
122
|
* Check if a session has an active agent run
|
|
123
|
+
* If session is active but stale (超过 SESSION_STALE_TIMEOUT_MS), automatically clean up
|
|
118
124
|
* @param sessionId - Session ID
|
|
119
125
|
* @returns true if session is busy
|
|
120
126
|
*/
|
|
@@ -140,6 +146,46 @@ export declare class XiaoYiRuntime {
|
|
|
140
146
|
* Clear all AbortControllers
|
|
141
147
|
*/
|
|
142
148
|
clearAllAbortControllers(): void;
|
|
149
|
+
/**
|
|
150
|
+
* Generate a composite key for session+task combination
|
|
151
|
+
* This ensures each task has its own push state, even within the same session
|
|
152
|
+
*/
|
|
153
|
+
private getPushStateKey;
|
|
154
|
+
/**
|
|
155
|
+
* Set task timeout time (from configuration)
|
|
156
|
+
*/
|
|
157
|
+
setTaskTimeout(timeoutMs: number): void;
|
|
158
|
+
/**
|
|
159
|
+
* Set a 1-hour task timeout timer for a session
|
|
160
|
+
* @returns timeout ID
|
|
161
|
+
*/
|
|
162
|
+
setTaskTimeoutForSession(sessionId: string, taskId: string, callback: (sessionId: string, taskId: string) => void): NodeJS.Timeout;
|
|
163
|
+
/**
|
|
164
|
+
* Clear the task timeout timer for a session
|
|
165
|
+
*/
|
|
166
|
+
clearTaskTimeoutForSession(sessionId: string): void;
|
|
167
|
+
/**
|
|
168
|
+
* Check if session+task is waiting for push notification
|
|
169
|
+
* @param sessionId - Session ID
|
|
170
|
+
* @param taskId - Task ID (optional, for per-task tracking)
|
|
171
|
+
*/
|
|
172
|
+
isSessionWaitingForPush(sessionId: string, taskId?: string): boolean;
|
|
173
|
+
/**
|
|
174
|
+
* Mark session+task as waiting for push notification
|
|
175
|
+
* @param sessionId - Session ID
|
|
176
|
+
* @param taskId - Task ID (optional, for per-task tracking)
|
|
177
|
+
*/
|
|
178
|
+
markSessionWaitingForPush(sessionId: string, taskId?: string): void;
|
|
179
|
+
/**
|
|
180
|
+
* Clear the waiting push state for a session+task
|
|
181
|
+
* @param sessionId - Session ID
|
|
182
|
+
* @param taskId - Task ID (optional, for per-task tracking)
|
|
183
|
+
*/
|
|
184
|
+
clearSessionWaitingForPush(sessionId: string, taskId?: string): void;
|
|
185
|
+
/**
|
|
186
|
+
* Clear all task timeout related state for a session
|
|
187
|
+
*/
|
|
188
|
+
clearTaskTimeoutState(sessionId: string): void;
|
|
143
189
|
}
|
|
144
190
|
export declare function getXiaoYiRuntime(): XiaoYiRuntime;
|
|
145
191
|
export declare function setXiaoYiRuntime(runtime: any): void;
|
package/dist/runtime.js
CHANGED
|
@@ -30,6 +30,12 @@ class XiaoYiRuntime {
|
|
|
30
30
|
this.sessionAbortControllerMap = new Map();
|
|
31
31
|
// Track if a session has an active agent run (for concurrent request detection)
|
|
32
32
|
this.sessionActiveRunMap = new Map();
|
|
33
|
+
// Track session start time for timeout detection
|
|
34
|
+
this.sessionStartTimeMap = new Map();
|
|
35
|
+
// 1-hour task timeout mechanism
|
|
36
|
+
this.sessionTaskTimeoutMap = new Map();
|
|
37
|
+
this.sessionPushPendingMap = new Map();
|
|
38
|
+
this.taskTimeoutMs = 3600000; // Default 1 hour
|
|
33
39
|
this.instanceId = `runtime_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
34
40
|
console.log(`XiaoYi: Created new runtime instance: ${this.instanceId}`);
|
|
35
41
|
}
|
|
@@ -106,6 +112,10 @@ class XiaoYiRuntime {
|
|
|
106
112
|
this.clearAllTimeouts();
|
|
107
113
|
// Clear all abort controllers
|
|
108
114
|
this.clearAllAbortControllers();
|
|
115
|
+
// Clear all task timeout state
|
|
116
|
+
for (const sessionId of this.sessionTaskTimeoutMap.keys()) {
|
|
117
|
+
this.clearTaskTimeoutState(sessionId);
|
|
118
|
+
}
|
|
109
119
|
}
|
|
110
120
|
/**
|
|
111
121
|
* Set timeout configuration
|
|
@@ -246,16 +256,35 @@ class XiaoYiRuntime {
|
|
|
246
256
|
const controller = new AbortController();
|
|
247
257
|
this.sessionAbortControllerMap.set(sessionId, controller);
|
|
248
258
|
this.sessionActiveRunMap.set(sessionId, true);
|
|
259
|
+
this.sessionStartTimeMap.set(sessionId, Date.now());
|
|
249
260
|
console.log(`[ABORT] Created AbortController for session ${sessionId}`);
|
|
250
261
|
return { controller, signal: controller.signal };
|
|
251
262
|
}
|
|
252
263
|
/**
|
|
253
264
|
* Check if a session has an active agent run
|
|
265
|
+
* If session is active but stale (超过 SESSION_STALE_TIMEOUT_MS), automatically clean up
|
|
254
266
|
* @param sessionId - Session ID
|
|
255
267
|
* @returns true if session is busy
|
|
256
268
|
*/
|
|
257
269
|
isSessionActive(sessionId) {
|
|
258
|
-
|
|
270
|
+
const isActive = this.sessionActiveRunMap.get(sessionId) || false;
|
|
271
|
+
if (isActive) {
|
|
272
|
+
// Check if the session has been active for too long
|
|
273
|
+
const startTime = this.sessionStartTimeMap.get(sessionId);
|
|
274
|
+
if (startTime) {
|
|
275
|
+
const elapsed = Date.now() - startTime;
|
|
276
|
+
if (elapsed > XiaoYiRuntime.SESSION_STALE_TIMEOUT_MS) {
|
|
277
|
+
// Session is stale, auto-cleanup and return false
|
|
278
|
+
console.log(`[CONCURRENT] Session ${sessionId} is stale (active for ${elapsed}ms), auto-cleaning`);
|
|
279
|
+
this.clearAbortControllerForSession(sessionId);
|
|
280
|
+
this.clearTaskIdForSession(sessionId);
|
|
281
|
+
this.clearSessionTimeout(sessionId);
|
|
282
|
+
this.sessionStartTimeMap.delete(sessionId);
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return isActive;
|
|
259
288
|
}
|
|
260
289
|
/**
|
|
261
290
|
* Abort a session's agent run
|
|
@@ -294,6 +323,8 @@ class XiaoYiRuntime {
|
|
|
294
323
|
}
|
|
295
324
|
// Also clear the active run flag
|
|
296
325
|
this.sessionActiveRunMap.delete(sessionId);
|
|
326
|
+
// Clear the session start time
|
|
327
|
+
this.sessionStartTimeMap.delete(sessionId);
|
|
297
328
|
console.log(`[CONCURRENT] Session ${sessionId} marked as inactive`);
|
|
298
329
|
}
|
|
299
330
|
/**
|
|
@@ -303,8 +334,91 @@ class XiaoYiRuntime {
|
|
|
303
334
|
this.sessionAbortControllerMap.clear();
|
|
304
335
|
console.log("[ABORT] All AbortControllers cleared");
|
|
305
336
|
}
|
|
337
|
+
// ==================== PUSH STATE MANAGEMENT HELPERS ====================
|
|
338
|
+
/**
|
|
339
|
+
* Generate a composite key for session+task combination
|
|
340
|
+
* This ensures each task has its own push state, even within the same session
|
|
341
|
+
*/
|
|
342
|
+
getPushStateKey(sessionId, taskId) {
|
|
343
|
+
return `${sessionId}:${taskId}`;
|
|
344
|
+
}
|
|
345
|
+
// ==================== END PUSH STATE MANAGEMENT HELPERS ====================
|
|
346
|
+
// ==================== 1-HOUR TASK TIMEOUT METHODS ====================
|
|
347
|
+
/**
|
|
348
|
+
* Set task timeout time (from configuration)
|
|
349
|
+
*/
|
|
350
|
+
setTaskTimeout(timeoutMs) {
|
|
351
|
+
this.taskTimeoutMs = timeoutMs;
|
|
352
|
+
console.log(`[TASK TIMEOUT] Task timeout set to ${timeoutMs}ms`);
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Set a 1-hour task timeout timer for a session
|
|
356
|
+
* @returns timeout ID
|
|
357
|
+
*/
|
|
358
|
+
setTaskTimeoutForSession(sessionId, taskId, callback) {
|
|
359
|
+
this.clearTaskTimeoutForSession(sessionId);
|
|
360
|
+
const timeoutId = setTimeout(() => {
|
|
361
|
+
console.log(`[TASK TIMEOUT] ${this.taskTimeoutMs}ms timeout triggered for session ${sessionId}, task ${taskId}`);
|
|
362
|
+
callback(sessionId, taskId);
|
|
363
|
+
}, this.taskTimeoutMs);
|
|
364
|
+
this.sessionTaskTimeoutMap.set(sessionId, timeoutId);
|
|
365
|
+
console.log(`[TASK TIMEOUT] ${this.taskTimeoutMs}ms task timeout started for session ${sessionId}`);
|
|
366
|
+
return timeoutId;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Clear the task timeout timer for a session
|
|
370
|
+
*/
|
|
371
|
+
clearTaskTimeoutForSession(sessionId) {
|
|
372
|
+
const timeoutId = this.sessionTaskTimeoutMap.get(sessionId);
|
|
373
|
+
if (timeoutId) {
|
|
374
|
+
clearTimeout(timeoutId);
|
|
375
|
+
this.sessionTaskTimeoutMap.delete(sessionId);
|
|
376
|
+
console.log(`[TASK TIMEOUT] Timeout cleared for session ${sessionId}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Check if session+task is waiting for push notification
|
|
381
|
+
* @param sessionId - Session ID
|
|
382
|
+
* @param taskId - Task ID (optional, for per-task tracking)
|
|
383
|
+
*/
|
|
384
|
+
isSessionWaitingForPush(sessionId, taskId) {
|
|
385
|
+
const key = taskId ? this.getPushStateKey(sessionId, taskId) : sessionId;
|
|
386
|
+
return this.sessionPushPendingMap.get(key) === true;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Mark session+task as waiting for push notification
|
|
390
|
+
* @param sessionId - Session ID
|
|
391
|
+
* @param taskId - Task ID (optional, for per-task tracking)
|
|
392
|
+
*/
|
|
393
|
+
markSessionWaitingForPush(sessionId, taskId) {
|
|
394
|
+
const key = taskId ? this.getPushStateKey(sessionId, taskId) : sessionId;
|
|
395
|
+
this.sessionPushPendingMap.set(key, true);
|
|
396
|
+
const taskInfo = taskId ? `, task ${taskId}` : '';
|
|
397
|
+
console.log(`[PUSH] Session ${sessionId}${taskInfo} marked as waiting for push`);
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Clear the waiting push state for a session+task
|
|
401
|
+
* @param sessionId - Session ID
|
|
402
|
+
* @param taskId - Task ID (optional, for per-task tracking)
|
|
403
|
+
*/
|
|
404
|
+
clearSessionWaitingForPush(sessionId, taskId) {
|
|
405
|
+
const key = taskId ? this.getPushStateKey(sessionId, taskId) : sessionId;
|
|
406
|
+
this.sessionPushPendingMap.delete(key);
|
|
407
|
+
const taskInfo = taskId ? `, task ${taskId}` : '';
|
|
408
|
+
console.log(`[PUSH] Session ${sessionId}${taskInfo} cleared from waiting for push`);
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Clear all task timeout related state for a session
|
|
412
|
+
*/
|
|
413
|
+
clearTaskTimeoutState(sessionId) {
|
|
414
|
+
this.clearTaskTimeoutForSession(sessionId);
|
|
415
|
+
this.clearSessionWaitingForPush(sessionId);
|
|
416
|
+
console.log(`[TASK TIMEOUT] All timeout state cleared for session ${sessionId}`);
|
|
417
|
+
}
|
|
306
418
|
}
|
|
307
419
|
exports.XiaoYiRuntime = XiaoYiRuntime;
|
|
420
|
+
// Maximum time a session can be active before we consider it stale (5 minutes)
|
|
421
|
+
XiaoYiRuntime.SESSION_STALE_TIMEOUT_MS = 5 * 60 * 1000;
|
|
308
422
|
// Global runtime instance - use global object to survive module reloads
|
|
309
423
|
// CRITICAL: Use string key instead of Symbol to ensure consistency across module reloads
|
|
310
424
|
const GLOBAL_KEY = '__xiaoyi_runtime_instance__';
|
package/dist/types.d.ts
CHANGED
|
@@ -152,6 +152,15 @@ export interface XiaoYiChannelConfig {
|
|
|
152
152
|
sk: string;
|
|
153
153
|
agentId: string;
|
|
154
154
|
enableStreaming?: boolean;
|
|
155
|
+
apiId?: string;
|
|
156
|
+
pushId?: string;
|
|
157
|
+
taskTimeoutMs?: number;
|
|
158
|
+
/**
|
|
159
|
+
* Session cleanup timeout in milliseconds
|
|
160
|
+
* When user clears context, old sessions are cleaned up after this timeout
|
|
161
|
+
* Default: 1 hour (60 * 60 * 1000)
|
|
162
|
+
*/
|
|
163
|
+
sessionCleanupTimeoutMs?: number;
|
|
155
164
|
}
|
|
156
165
|
export interface AuthCredentials {
|
|
157
166
|
ak: string;
|
|
@@ -176,6 +185,7 @@ export interface InternalWebSocketConfig {
|
|
|
176
185
|
ak: string;
|
|
177
186
|
sk: string;
|
|
178
187
|
enableStreaming?: boolean;
|
|
188
|
+
sessionCleanupTimeoutMs?: number;
|
|
179
189
|
}
|
|
180
190
|
export type ServerId = 'server1' | 'server2';
|
|
181
191
|
export interface ServerConnectionState {
|
|
@@ -184,3 +194,14 @@ export interface ServerConnectionState {
|
|
|
184
194
|
lastHeartbeat: number;
|
|
185
195
|
reconnectAttempts: number;
|
|
186
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* Session cleanup state for delayed cleanup
|
|
199
|
+
*/
|
|
200
|
+
export interface SessionCleanupState {
|
|
201
|
+
sessionId: string;
|
|
202
|
+
serverId: ServerId;
|
|
203
|
+
markedForCleanupAt: number;
|
|
204
|
+
cleanupTimeoutId?: NodeJS.Timeout;
|
|
205
|
+
reason: 'user_cleared' | 'timeout' | 'error';
|
|
206
|
+
accumulatedText?: string;
|
|
207
|
+
}
|
package/dist/websocket.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { EventEmitter } from "events";
|
|
2
|
-
import { A2AResponseMessage, WebSocketConnectionState, XiaoYiChannelConfig, ServerId, ServerConnectionState } from "./types";
|
|
2
|
+
import { A2AResponseMessage, WebSocketConnectionState, XiaoYiChannelConfig, ServerId, ServerConnectionState, SessionCleanupState } from "./types";
|
|
3
3
|
export declare class XiaoYiWebSocketManager extends EventEmitter {
|
|
4
4
|
private ws1;
|
|
5
5
|
private ws2;
|
|
6
6
|
private state1;
|
|
7
7
|
private state2;
|
|
8
8
|
private sessionServerMap;
|
|
9
|
+
private sessionCleanupStateMap;
|
|
10
|
+
private static readonly DEFAULT_CLEANUP_TIMEOUT_MS;
|
|
9
11
|
private auth;
|
|
10
12
|
private config;
|
|
11
13
|
private heartbeatTimeout1?;
|
|
@@ -176,4 +178,34 @@ export declare class XiaoYiWebSocketManager extends EventEmitter {
|
|
|
176
178
|
* Remove session mapping
|
|
177
179
|
*/
|
|
178
180
|
removeSession(sessionId: string): void;
|
|
181
|
+
/**
|
|
182
|
+
* Mark a session for delayed cleanup
|
|
183
|
+
* @param sessionId The session ID to mark for cleanup
|
|
184
|
+
* @param serverId The server ID associated with this session
|
|
185
|
+
* @param timeoutMs Timeout in milliseconds before forcing cleanup
|
|
186
|
+
*/
|
|
187
|
+
private markSessionForCleanup;
|
|
188
|
+
/**
|
|
189
|
+
* Force cleanup a session immediately
|
|
190
|
+
* @param sessionId The session ID to cleanup
|
|
191
|
+
*/
|
|
192
|
+
forceCleanupSession(sessionId: string): void;
|
|
193
|
+
/**
|
|
194
|
+
* Check if a session is pending cleanup
|
|
195
|
+
* @param sessionId The session ID to check
|
|
196
|
+
* @returns True if session is pending cleanup
|
|
197
|
+
*/
|
|
198
|
+
isSessionPendingCleanup(sessionId: string): boolean;
|
|
199
|
+
/**
|
|
200
|
+
* Get cleanup state for a session
|
|
201
|
+
* @param sessionId The session ID to check
|
|
202
|
+
* @returns Cleanup state if exists, undefined otherwise
|
|
203
|
+
*/
|
|
204
|
+
getSessionCleanupState(sessionId: string): SessionCleanupState | undefined;
|
|
205
|
+
/**
|
|
206
|
+
* Update accumulated text for a pending cleanup session
|
|
207
|
+
* @param sessionId The session ID
|
|
208
|
+
* @param text The accumulated text
|
|
209
|
+
*/
|
|
210
|
+
updateAccumulatedTextForCleanup(sessionId: string, text: string): void;
|
|
179
211
|
}
|
package/dist/websocket.js
CHANGED
|
@@ -30,6 +30,9 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
30
30
|
};
|
|
31
31
|
// ==================== Session → Server Mapping ====================
|
|
32
32
|
this.sessionServerMap = new Map();
|
|
33
|
+
// ==================== Session Cleanup State ====================
|
|
34
|
+
// Track sessions that are pending cleanup (user cleared context but task still running)
|
|
35
|
+
this.sessionCleanupStateMap = new Map();
|
|
33
36
|
// ==================== Active Tasks ====================
|
|
34
37
|
this.activeTasks = new Map();
|
|
35
38
|
// Resolve configuration with defaults and backward compatibility
|
|
@@ -107,6 +110,7 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
107
110
|
ak: userConfig.ak,
|
|
108
111
|
sk: userConfig.sk,
|
|
109
112
|
enableStreaming: userConfig.enableStreaming ?? true,
|
|
113
|
+
sessionCleanupTimeoutMs: userConfig.sessionCleanupTimeoutMs ?? XiaoYiWebSocketManager.DEFAULT_CLEANUP_TIMEOUT_MS,
|
|
110
114
|
};
|
|
111
115
|
}
|
|
112
116
|
/**
|
|
@@ -245,6 +249,13 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
245
249
|
this.state2.ready = false;
|
|
246
250
|
this.sessionServerMap.clear();
|
|
247
251
|
this.activeTasks.clear();
|
|
252
|
+
// Cleanup session cleanup state map
|
|
253
|
+
for (const [sessionId, state] of this.sessionCleanupStateMap.entries()) {
|
|
254
|
+
if (state.cleanupTimeoutId) {
|
|
255
|
+
clearTimeout(state.cleanupTimeoutId);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
this.sessionCleanupStateMap.clear();
|
|
248
259
|
this.emit("disconnected");
|
|
249
260
|
}
|
|
250
261
|
/**
|
|
@@ -382,7 +393,14 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
382
393
|
/**
|
|
383
394
|
* Send A2A response message with automatic routing
|
|
384
395
|
*/
|
|
385
|
-
async sendResponse(response, taskId, sessionId, isFinal = true, append =
|
|
396
|
+
async sendResponse(response, taskId, sessionId, isFinal = true, append = true) {
|
|
397
|
+
// Check if session is pending cleanup
|
|
398
|
+
const cleanupState = this.sessionCleanupStateMap.get(sessionId);
|
|
399
|
+
if (cleanupState) {
|
|
400
|
+
// Session is pending cleanup, silently discard response
|
|
401
|
+
console.log(`[RESPONSE] Discarding response for pending cleanup session ${sessionId}`);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
386
404
|
// Find which server this session belongs to
|
|
387
405
|
const targetServer = this.sessionServerMap.get(sessionId);
|
|
388
406
|
if (!targetServer) {
|
|
@@ -461,6 +479,13 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
461
479
|
* This uses "status-update" event type which keeps the conversation active
|
|
462
480
|
*/
|
|
463
481
|
async sendStatusUpdate(taskId, sessionId, message, targetServer) {
|
|
482
|
+
// Check if session is pending cleanup
|
|
483
|
+
const cleanupState = this.sessionCleanupStateMap.get(sessionId);
|
|
484
|
+
if (cleanupState) {
|
|
485
|
+
// Session is pending cleanup, silently discard status updates
|
|
486
|
+
console.log(`[STATUS] Discarding status update for pending cleanup session ${sessionId}`);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
464
489
|
const serverId = targetServer || this.sessionServerMap.get(sessionId);
|
|
465
490
|
if (!serverId) {
|
|
466
491
|
console.error(`[STATUS] Unknown server for session ${sessionId}`);
|
|
@@ -591,8 +616,8 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
591
616
|
id: message.id,
|
|
592
617
|
serverId: sourceServer,
|
|
593
618
|
});
|
|
594
|
-
//
|
|
595
|
-
this.
|
|
619
|
+
// Mark session for cleanup instead of immediate deletion
|
|
620
|
+
this.markSessionForCleanup(sessionId, sourceServer, this.config.sessionCleanupTimeoutMs ?? XiaoYiWebSocketManager.DEFAULT_CLEANUP_TIMEOUT_MS);
|
|
596
621
|
}
|
|
597
622
|
/**
|
|
598
623
|
* Handle clear message (legacy format)
|
|
@@ -606,7 +631,8 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
606
631
|
id: message.id,
|
|
607
632
|
serverId: sourceServer,
|
|
608
633
|
});
|
|
609
|
-
|
|
634
|
+
// Mark session for cleanup instead of immediate deletion
|
|
635
|
+
this.markSessionForCleanup(message.sessionId, sourceServer, this.config.sessionCleanupTimeoutMs ?? XiaoYiWebSocketManager.DEFAULT_CLEANUP_TIMEOUT_MS);
|
|
610
636
|
}
|
|
611
637
|
/**
|
|
612
638
|
* Handle tasks/cancel message
|
|
@@ -636,7 +662,7 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
636
662
|
/**
|
|
637
663
|
* Convert A2AResponseMessage to JSON-RPC 2.0 format
|
|
638
664
|
*/
|
|
639
|
-
convertToJsonRpcFormat(response, taskId, isFinal = true, append =
|
|
665
|
+
convertToJsonRpcFormat(response, taskId, isFinal = true, append = true) {
|
|
640
666
|
const artifactId = `artifact_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
641
667
|
if (response.status === "error" && response.error) {
|
|
642
668
|
return {
|
|
@@ -650,9 +676,11 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
650
676
|
}
|
|
651
677
|
const parts = [];
|
|
652
678
|
if (response.content.type === "text" && response.content.text) {
|
|
679
|
+
// When isFinal=true, use empty string for text (no content needed for final chunk)
|
|
680
|
+
const textContent = isFinal ? "" : response.content.text;
|
|
653
681
|
parts.push({
|
|
654
682
|
kind: "text",
|
|
655
|
-
text:
|
|
683
|
+
text: textContent,
|
|
656
684
|
});
|
|
657
685
|
}
|
|
658
686
|
else if (response.content.type === "file") {
|
|
@@ -665,10 +693,11 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
665
693
|
},
|
|
666
694
|
});
|
|
667
695
|
}
|
|
696
|
+
// When isFinal=true, append should be true and text should be empty
|
|
668
697
|
const artifactEvent = {
|
|
669
698
|
taskId: taskId,
|
|
670
699
|
kind: "artifact-update",
|
|
671
|
-
append: append,
|
|
700
|
+
append: isFinal ? true : append,
|
|
672
701
|
lastChunk: isFinal,
|
|
673
702
|
final: isFinal,
|
|
674
703
|
artifact: {
|
|
@@ -918,6 +947,86 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
918
947
|
removeSession(sessionId) {
|
|
919
948
|
this.sessionServerMap.delete(sessionId);
|
|
920
949
|
}
|
|
950
|
+
/**
|
|
951
|
+
* Mark a session for delayed cleanup
|
|
952
|
+
* @param sessionId The session ID to mark for cleanup
|
|
953
|
+
* @param serverId The server ID associated with this session
|
|
954
|
+
* @param timeoutMs Timeout in milliseconds before forcing cleanup
|
|
955
|
+
*/
|
|
956
|
+
markSessionForCleanup(sessionId, serverId, timeoutMs) {
|
|
957
|
+
// Check if already marked
|
|
958
|
+
const existingState = this.sessionCleanupStateMap.get(sessionId);
|
|
959
|
+
if (existingState) {
|
|
960
|
+
// Already pending cleanup, reset timeout
|
|
961
|
+
if (existingState.cleanupTimeoutId) {
|
|
962
|
+
clearTimeout(existingState.cleanupTimeoutId);
|
|
963
|
+
}
|
|
964
|
+
console.log(`[CLEANUP] Session ${sessionId} already pending cleanup, resetting timeout`);
|
|
965
|
+
}
|
|
966
|
+
// Create new cleanup state
|
|
967
|
+
const newState = {
|
|
968
|
+
sessionId,
|
|
969
|
+
serverId,
|
|
970
|
+
markedForCleanupAt: Date.now(),
|
|
971
|
+
reason: 'user_cleared',
|
|
972
|
+
};
|
|
973
|
+
// Start cleanup timeout
|
|
974
|
+
const timeoutId = setTimeout(() => {
|
|
975
|
+
console.log(`[CLEANUP] Timeout reached for session ${sessionId}, forcing cleanup`);
|
|
976
|
+
this.forceCleanupSession(sessionId);
|
|
977
|
+
}, timeoutMs);
|
|
978
|
+
newState.cleanupTimeoutId = timeoutId;
|
|
979
|
+
this.sessionCleanupStateMap.set(sessionId, newState);
|
|
980
|
+
console.log(`[CLEANUP] Session ${sessionId} marked for cleanup (timeout: ${timeoutMs}ms)`);
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Force cleanup a session immediately
|
|
984
|
+
* @param sessionId The session ID to cleanup
|
|
985
|
+
*/
|
|
986
|
+
forceCleanupSession(sessionId) {
|
|
987
|
+
// Check if already cleaned
|
|
988
|
+
const state = this.sessionCleanupStateMap.get(sessionId);
|
|
989
|
+
if (!state) {
|
|
990
|
+
console.log(`[CLEANUP] Session ${sessionId} already cleaned up, skipping`);
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
// Clear timeout
|
|
994
|
+
if (state.cleanupTimeoutId) {
|
|
995
|
+
clearTimeout(state.cleanupTimeoutId);
|
|
996
|
+
}
|
|
997
|
+
// Remove from both maps
|
|
998
|
+
this.sessionServerMap.delete(sessionId);
|
|
999
|
+
this.sessionCleanupStateMap.delete(sessionId);
|
|
1000
|
+
console.log(`[CLEANUP] Session ${sessionId} cleanup completed`);
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Check if a session is pending cleanup
|
|
1004
|
+
* @param sessionId The session ID to check
|
|
1005
|
+
* @returns True if session is pending cleanup
|
|
1006
|
+
*/
|
|
1007
|
+
isSessionPendingCleanup(sessionId) {
|
|
1008
|
+
return this.sessionCleanupStateMap.has(sessionId);
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Get cleanup state for a session
|
|
1012
|
+
* @param sessionId The session ID to check
|
|
1013
|
+
* @returns Cleanup state if exists, undefined otherwise
|
|
1014
|
+
*/
|
|
1015
|
+
getSessionCleanupState(sessionId) {
|
|
1016
|
+
return this.sessionCleanupStateMap.get(sessionId);
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Update accumulated text for a pending cleanup session
|
|
1020
|
+
* @param sessionId The session ID
|
|
1021
|
+
* @param text The accumulated text
|
|
1022
|
+
*/
|
|
1023
|
+
updateAccumulatedTextForCleanup(sessionId, text) {
|
|
1024
|
+
const state = this.sessionCleanupStateMap.get(sessionId);
|
|
1025
|
+
if (state) {
|
|
1026
|
+
state.accumulatedText = text;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
921
1029
|
}
|
|
922
1030
|
exports.XiaoYiWebSocketManager = XiaoYiWebSocketManager;
|
|
1031
|
+
XiaoYiWebSocketManager.DEFAULT_CLEANUP_TIMEOUT_MS = 60 * 60 * 1000; // 1 hour
|
|
923
1032
|
XiaoYiWebSocketManager.STABLE_CONNECTION_THRESHOLD = 10000; // 10 seconds
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ynhcj/xiaoyi",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.6",
|
|
4
4
|
"description": "XiaoYi channel plugin for OpenClaw",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@types/node": "^20.11.0",
|
|
53
53
|
"@types/ws": "^8.5.10",
|
|
54
|
+
"openclaw": "^2026.2.24",
|
|
54
55
|
"typescript": "^5.3.3"
|
|
55
56
|
},
|
|
56
57
|
"files": [
|