@ynhcj/xiaoyi 2.5.4 → 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/README.md +207 -207
- package/dist/channel.d.ts +57 -0
- package/dist/channel.js +703 -453
- 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/openclaw.plugin.json +9 -9
- package/package.json +2 -1
- package/xiaoyi.js +1 -1
- package/dist/session-manager.d.ts +0 -78
- package/dist/session-manager.js +0 -258
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
|
}
|