@ynhcj/xiaoyi-channel 0.0.9 → 0.0.10-beta
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/src/bot.js +29 -3
- package/dist/src/channel.js +5 -1
- package/dist/src/client.d.ts +15 -0
- package/dist/src/client.js +94 -0
- package/dist/src/file-download.js +10 -1
- package/dist/src/formatter.d.ts +17 -0
- package/dist/src/formatter.js +47 -1
- package/dist/src/heartbeat.d.ts +2 -1
- package/dist/src/heartbeat.js +6 -1
- package/dist/src/monitor.d.ts +5 -0
- package/dist/src/monitor.js +54 -4
- package/dist/src/outbound.js +45 -3
- package/dist/src/parser.d.ts +5 -0
- package/dist/src/parser.js +15 -0
- package/dist/src/push.d.ts +7 -1
- package/dist/src/push.js +110 -19
- package/dist/src/reply-dispatcher.js +147 -4
- package/dist/src/tools/calendar-tool.d.ts +6 -0
- package/dist/src/tools/calendar-tool.js +167 -0
- package/dist/src/tools/location-tool.js +16 -6
- package/dist/src/tools/note-tool.js +4 -4
- package/dist/src/tools/search-calendar-tool.d.ts +12 -0
- package/dist/src/tools/search-calendar-tool.js +220 -0
- package/dist/src/tools/search-note-tool.js +4 -4
- package/dist/src/tools/session-manager.js +7 -0
- package/dist/src/types.d.ts +5 -9
- package/dist/src/utils/config-manager.d.ts +26 -0
- package/dist/src/utils/config-manager.js +56 -0
- package/dist/src/websocket.d.ts +39 -0
- package/dist/src/websocket.js +129 -31
- package/package.json +1 -1
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { getXYWebSocketManager } from "../client.js";
|
|
2
|
+
import { sendCommand } from "../formatter.js";
|
|
3
|
+
import { getLatestSessionContext } from "./session-manager.js";
|
|
4
|
+
import { logger } from "../utils/logger.js";
|
|
5
|
+
/**
|
|
6
|
+
* XY search calendar event tool - searches calendar events on user's device.
|
|
7
|
+
* Returns matching events based on time range and optional title filter.
|
|
8
|
+
*
|
|
9
|
+
* Time range guidelines:
|
|
10
|
+
* - For a specific day: use 00:00:00 to 23:59:59 of that day
|
|
11
|
+
* - For morning: 06:00:00 to 12:00:00
|
|
12
|
+
* - For afternoon: 12:00:00 to 18:00:00
|
|
13
|
+
* - For evening: 18:00:00 to 24:00:00
|
|
14
|
+
* - For a specific time: use ±1 hour range (e.g., for 3PM, use 14:00:00 to 16:00:00)
|
|
15
|
+
*/
|
|
16
|
+
export const searchCalendarTool = {
|
|
17
|
+
name: "search_calendar_event",
|
|
18
|
+
label: "Search Calendar Event",
|
|
19
|
+
description: `检索用户日历中的日程安排。根据时间范围和可选的日程标题进行检索。时间格式必须为:YYYYMMDD hhmmss(例如:20240115 143000)。
|
|
20
|
+
|
|
21
|
+
时间范围说明:
|
|
22
|
+
- 查询某一天的日程:使用该天的 00:00:00 到 23:59:59(例如:20240115 000000 到 20240115 235959)
|
|
23
|
+
- 查询上午的日程:使用 06:00:00 到 12:00:00
|
|
24
|
+
- 查询下午的日程:使用 12:00:00 到 18:00:00
|
|
25
|
+
- 查询晚上的日程:使用 18:00:00 到 23:59:59
|
|
26
|
+
- 查询某个时刻附近的日程:使用该时刻前后1小时的区间(例如:查询3点左右的日程,使用 14:00:00 到 16:00:00)
|
|
27
|
+
|
|
28
|
+
注意:该工具执行时间较长(最多60秒),请勿重复调用,超时或失败时最多重试一次。`,
|
|
29
|
+
parameters: {
|
|
30
|
+
type: "object",
|
|
31
|
+
properties: {
|
|
32
|
+
startTime: {
|
|
33
|
+
type: "string",
|
|
34
|
+
description: "日程起始时间,格式必须为:YYYYMMDD hhmmss(例如:20240115 143000 表示 2024年1月15日 14:30:00)",
|
|
35
|
+
},
|
|
36
|
+
endTime: {
|
|
37
|
+
type: "string",
|
|
38
|
+
description: "日程结束时间,格式必须为:YYYYMMDD hhmmss(例如:20240115 173000 表示 2024年1月15日 17:30:00)",
|
|
39
|
+
},
|
|
40
|
+
title: {
|
|
41
|
+
type: "string",
|
|
42
|
+
description: "日程标题/类型(可选),用于过滤特定类型的日程",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
required: ["startTime", "endTime"],
|
|
46
|
+
},
|
|
47
|
+
async execute(toolCallId, params) {
|
|
48
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] 🚀 Starting execution`);
|
|
49
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - toolCallId: ${toolCallId}`);
|
|
50
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - params:`, JSON.stringify(params));
|
|
51
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - timestamp: ${new Date().toISOString()}`);
|
|
52
|
+
// Validate parameters
|
|
53
|
+
if (!params.startTime || !params.endTime) {
|
|
54
|
+
logger.error(`[SEARCH_CALENDAR_TOOL] ❌ Missing required parameters`);
|
|
55
|
+
throw new Error("Missing required parameters: startTime and endTime are required");
|
|
56
|
+
}
|
|
57
|
+
// Convert time strings to millisecond timestamps
|
|
58
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] 🕒 Converting time strings to timestamps...`);
|
|
59
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - startTime input: ${params.startTime}`);
|
|
60
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - endTime input: ${params.endTime}`);
|
|
61
|
+
// Parse YYYYMMDD hhmmss format
|
|
62
|
+
const parseTimeString = (timeStr) => {
|
|
63
|
+
// Remove any extra spaces and split
|
|
64
|
+
const cleaned = timeStr.trim().replace(/\s+/g, ' ');
|
|
65
|
+
const parts = cleaned.split(' ');
|
|
66
|
+
if (parts.length !== 2) {
|
|
67
|
+
throw new Error(`Invalid time format: ${timeStr}. Expected format: YYYYMMDD hhmmss`);
|
|
68
|
+
}
|
|
69
|
+
const datePart = parts[0]; // YYYYMMDD
|
|
70
|
+
const timePart = parts[1]; // hhmmss
|
|
71
|
+
if (datePart.length !== 8 || timePart.length !== 6) {
|
|
72
|
+
throw new Error(`Invalid time format: ${timeStr}. Expected format: YYYYMMDD hhmmss`);
|
|
73
|
+
}
|
|
74
|
+
const year = parseInt(datePart.substring(0, 4), 10);
|
|
75
|
+
const month = parseInt(datePart.substring(4, 6), 10) - 1; // Month is 0-indexed
|
|
76
|
+
const day = parseInt(datePart.substring(6, 8), 10);
|
|
77
|
+
const hours = parseInt(timePart.substring(0, 2), 10);
|
|
78
|
+
const minutes = parseInt(timePart.substring(2, 4), 10);
|
|
79
|
+
const seconds = parseInt(timePart.substring(4, 6), 10);
|
|
80
|
+
const date = new Date(year, month, day, hours, minutes, seconds);
|
|
81
|
+
return date.getTime();
|
|
82
|
+
};
|
|
83
|
+
let startTimeMs;
|
|
84
|
+
let endTimeMs;
|
|
85
|
+
try {
|
|
86
|
+
startTimeMs = parseTimeString(params.startTime);
|
|
87
|
+
endTimeMs = parseTimeString(params.endTime);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
logger.error(`[SEARCH_CALENDAR_TOOL] ❌ Time parsing error:`, error);
|
|
91
|
+
throw new Error(`Invalid time format. Required format: YYYYMMDD hhmmss (e.g., 20240115 143000). Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
92
|
+
}
|
|
93
|
+
if (isNaN(startTimeMs) || isNaN(endTimeMs)) {
|
|
94
|
+
logger.error(`[SEARCH_CALENDAR_TOOL] ❌ Invalid time format`);
|
|
95
|
+
throw new Error("Invalid time format. Required format: YYYYMMDD hhmmss (e.g., 20240115 143000)");
|
|
96
|
+
}
|
|
97
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] ✅ Time conversion successful`);
|
|
98
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - startTime timestamp: ${startTimeMs}`);
|
|
99
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - endTime timestamp: ${endTimeMs}`);
|
|
100
|
+
// Get session context
|
|
101
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] 🔍 Attempting to get session context...`);
|
|
102
|
+
const sessionContext = getLatestSessionContext();
|
|
103
|
+
if (!sessionContext) {
|
|
104
|
+
logger.error(`[SEARCH_CALENDAR_TOOL] ❌ FAILED: No active session found!`);
|
|
105
|
+
logger.error(`[SEARCH_CALENDAR_TOOL] - toolCallId: ${toolCallId}`);
|
|
106
|
+
throw new Error("No active XY session found. Search calendar tool can only be used during an active conversation.");
|
|
107
|
+
}
|
|
108
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] ✅ Session context found`);
|
|
109
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - sessionId: ${sessionContext.sessionId}`);
|
|
110
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - taskId: ${sessionContext.taskId}`);
|
|
111
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - messageId: ${sessionContext.messageId}`);
|
|
112
|
+
const { config, sessionId, taskId, messageId } = sessionContext;
|
|
113
|
+
// Get WebSocket manager
|
|
114
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] 🔌 Getting WebSocket manager...`);
|
|
115
|
+
const wsManager = getXYWebSocketManager(config);
|
|
116
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] ✅ WebSocket manager obtained`);
|
|
117
|
+
// Build SearchCalendarEvent command
|
|
118
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] 📦 Building SearchCalendarEvent command...`);
|
|
119
|
+
// Build intentParam with timeInterval and optional title
|
|
120
|
+
const intentParam = {
|
|
121
|
+
timeInterval: [startTimeMs, endTimeMs],
|
|
122
|
+
};
|
|
123
|
+
if (params.title) {
|
|
124
|
+
intentParam.title = params.title;
|
|
125
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - Including title filter: ${params.title}`);
|
|
126
|
+
}
|
|
127
|
+
const command = {
|
|
128
|
+
header: {
|
|
129
|
+
namespace: "Common",
|
|
130
|
+
name: "Action",
|
|
131
|
+
},
|
|
132
|
+
payload: {
|
|
133
|
+
cardParam: {},
|
|
134
|
+
executeParam: {
|
|
135
|
+
executeMode: "background",
|
|
136
|
+
intentName: "SearchCalendarEvent",
|
|
137
|
+
bundleName: "com.huawei.hmos.calendardata",
|
|
138
|
+
dimension: "",
|
|
139
|
+
needUnlock: true,
|
|
140
|
+
actionResponse: true,
|
|
141
|
+
appType: "OHOS_APP",
|
|
142
|
+
timeOut: 5,
|
|
143
|
+
intentParam,
|
|
144
|
+
permissionId: [],
|
|
145
|
+
achieveType: "INTENT",
|
|
146
|
+
},
|
|
147
|
+
responses: [
|
|
148
|
+
{
|
|
149
|
+
resultCode: "",
|
|
150
|
+
displayText: "",
|
|
151
|
+
ttsText: "",
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
needUploadResult: true,
|
|
155
|
+
noHalfPage: false,
|
|
156
|
+
pageControlRelated: false,
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
// Send command and wait for response (60 second timeout)
|
|
160
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] ⏳ Setting up promise to wait for calendar search response...`);
|
|
161
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - Timeout: 60 seconds`);
|
|
162
|
+
return new Promise((resolve, reject) => {
|
|
163
|
+
const timeout = setTimeout(() => {
|
|
164
|
+
logger.error(`[SEARCH_CALENDAR_TOOL] ⏰ Timeout: No response received within 60 seconds`);
|
|
165
|
+
wsManager.off("data-event", handler);
|
|
166
|
+
reject(new Error("检索日程超时(60秒)"));
|
|
167
|
+
}, 60000);
|
|
168
|
+
// Listen for data events from WebSocket
|
|
169
|
+
const handler = (event) => {
|
|
170
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] 📨 Received data event:`, JSON.stringify(event));
|
|
171
|
+
if (event.intentName === "SearchCalendarEvent") {
|
|
172
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] 🎯 SearchCalendarEvent event received`);
|
|
173
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - status: ${event.status}`);
|
|
174
|
+
clearTimeout(timeout);
|
|
175
|
+
wsManager.off("data-event", handler);
|
|
176
|
+
if (event.status === "success" && event.outputs) {
|
|
177
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] ✅ Calendar events retrieved successfully`);
|
|
178
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - outputs:`, JSON.stringify(event.outputs));
|
|
179
|
+
// Return the result directly as requested
|
|
180
|
+
const result = event.outputs.result;
|
|
181
|
+
resolve({
|
|
182
|
+
content: [
|
|
183
|
+
{
|
|
184
|
+
type: "text",
|
|
185
|
+
text: JSON.stringify(result),
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
logger.error(`[SEARCH_CALENDAR_TOOL] ❌ Calendar event search failed`);
|
|
192
|
+
logger.error(`[SEARCH_CALENDAR_TOOL] - status: ${event.status}`);
|
|
193
|
+
reject(new Error(`检索日程失败: ${event.status}`));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
// Register event handler
|
|
198
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] 📡 Registering data-event handler on WebSocket manager`);
|
|
199
|
+
wsManager.on("data-event", handler);
|
|
200
|
+
// Send the command
|
|
201
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] 📤 Sending SearchCalendarEvent command...`);
|
|
202
|
+
sendCommand({
|
|
203
|
+
config,
|
|
204
|
+
sessionId,
|
|
205
|
+
taskId,
|
|
206
|
+
messageId,
|
|
207
|
+
command,
|
|
208
|
+
})
|
|
209
|
+
.then(() => {
|
|
210
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] ✅ Command sent successfully, waiting for response...`);
|
|
211
|
+
})
|
|
212
|
+
.catch((error) => {
|
|
213
|
+
logger.error(`[SEARCH_CALENDAR_TOOL] ❌ Failed to send command:`, error);
|
|
214
|
+
clearTimeout(timeout);
|
|
215
|
+
wsManager.off("data-event", handler);
|
|
216
|
+
reject(error);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
},
|
|
220
|
+
};
|
|
@@ -9,7 +9,7 @@ import { logger } from "../utils/logger.js";
|
|
|
9
9
|
export const searchNoteTool = {
|
|
10
10
|
name: "search_notes",
|
|
11
11
|
label: "Search Notes",
|
|
12
|
-
description: "
|
|
12
|
+
description: "搜索用户设备上的备忘录内容。根据关键词在备忘录的标题、内容和附件名称中进行检索。注意:操作超时时间为60秒,请勿重复调用此工具,如果超时或失败,最多重试一次。",
|
|
13
13
|
parameters: {
|
|
14
14
|
type: "object",
|
|
15
15
|
properties: {
|
|
@@ -67,12 +67,12 @@ export const searchNoteTool = {
|
|
|
67
67
|
pageControlRelated: false,
|
|
68
68
|
},
|
|
69
69
|
};
|
|
70
|
-
// Send command and wait for response (
|
|
70
|
+
// Send command and wait for response (60 second timeout)
|
|
71
71
|
return new Promise((resolve, reject) => {
|
|
72
72
|
const timeout = setTimeout(() => {
|
|
73
73
|
wsManager.off("data-event", handler);
|
|
74
|
-
reject(new Error("搜索备忘录超时(
|
|
75
|
-
},
|
|
74
|
+
reject(new Error("搜索备忘录超时(60秒)"));
|
|
75
|
+
}, 60000);
|
|
76
76
|
// Listen for data events from WebSocket
|
|
77
77
|
const handler = (event) => {
|
|
78
78
|
logger.debug("Received data event:", event);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { logger } from "../utils/logger.js";
|
|
2
|
+
import { configManager } from "../utils/config-manager.js";
|
|
2
3
|
// Map of sessionKey -> SessionContext
|
|
3
4
|
const activeSessions = new Map();
|
|
4
5
|
/**
|
|
@@ -24,7 +25,13 @@ export function unregisterSession(sessionKey) {
|
|
|
24
25
|
logger.log(`[SESSION_MANAGER] 🗑️ Unregistering session: ${sessionKey}`);
|
|
25
26
|
logger.log(`[SESSION_MANAGER] - Active sessions before: ${activeSessions.size}`);
|
|
26
27
|
logger.log(`[SESSION_MANAGER] - Session existed: ${activeSessions.has(sessionKey)}`);
|
|
28
|
+
// Get session context before deleting to clear associated pushId
|
|
29
|
+
const context = activeSessions.get(sessionKey);
|
|
27
30
|
const existed = activeSessions.delete(sessionKey);
|
|
31
|
+
// Clear cached pushId for this session
|
|
32
|
+
if (context) {
|
|
33
|
+
configManager.clearSession(context.sessionId);
|
|
34
|
+
}
|
|
28
35
|
logger.log(`[SESSION_MANAGER] - Deleted: ${existed}`);
|
|
29
36
|
logger.log(`[SESSION_MANAGER] - Active sessions after: ${activeSessions.size}`);
|
|
30
37
|
logger.log(`[SESSION_MANAGER] - Remaining session keys: [${Array.from(activeSessions.keys()).join(", ")}]`);
|
package/dist/src/types.d.ts
CHANGED
|
@@ -75,7 +75,11 @@ export interface A2AArtifact {
|
|
|
75
75
|
artifactId: string;
|
|
76
76
|
parts: A2AArtifactPart[];
|
|
77
77
|
}
|
|
78
|
-
export
|
|
78
|
+
export interface A2AReasoningTextPart {
|
|
79
|
+
kind: "reasoningText";
|
|
80
|
+
reasoningText: string;
|
|
81
|
+
}
|
|
82
|
+
export type A2AArtifactPart = A2ATextPart | A2ADataPart | A2ACommandPart | A2AReasoningTextPart;
|
|
79
83
|
export interface A2ACommandPart {
|
|
80
84
|
kind: "command";
|
|
81
85
|
command: A2ACommand;
|
|
@@ -155,14 +159,6 @@ export interface FileUploadCompleteRequest {
|
|
|
155
159
|
export interface FileUploadCompleteResponse {
|
|
156
160
|
fileId: string;
|
|
157
161
|
}
|
|
158
|
-
export interface PushMessageRequest {
|
|
159
|
-
apiKey: string;
|
|
160
|
-
apiId: string;
|
|
161
|
-
pushId: string;
|
|
162
|
-
sessionId: string;
|
|
163
|
-
title: string;
|
|
164
|
-
content: string;
|
|
165
|
-
}
|
|
166
162
|
export type ServerIdentifier = "server1" | "server2";
|
|
167
163
|
export interface SessionBinding {
|
|
168
164
|
sessionId: string;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manages dynamic configuration updates that can change at runtime.
|
|
3
|
+
* Specifically handles pushId which can be updated per-session.
|
|
4
|
+
*/
|
|
5
|
+
declare class ConfigManager {
|
|
6
|
+
private sessionPushIds;
|
|
7
|
+
private globalPushId;
|
|
8
|
+
/**
|
|
9
|
+
* Update push ID for a specific session.
|
|
10
|
+
*/
|
|
11
|
+
updatePushId(sessionId: string, pushId: string): void;
|
|
12
|
+
/**
|
|
13
|
+
* Get push ID for a session (falls back to global if not found).
|
|
14
|
+
*/
|
|
15
|
+
getPushId(sessionId?: string): string | null;
|
|
16
|
+
/**
|
|
17
|
+
* Clear push ID for a session.
|
|
18
|
+
*/
|
|
19
|
+
clearSession(sessionId: string): void;
|
|
20
|
+
/**
|
|
21
|
+
* Clear all cached push IDs.
|
|
22
|
+
*/
|
|
23
|
+
clear(): void;
|
|
24
|
+
}
|
|
25
|
+
export declare const configManager: ConfigManager;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Dynamic configuration manager for runtime updates
|
|
2
|
+
import { logger } from "./logger.js";
|
|
3
|
+
/**
|
|
4
|
+
* Manages dynamic configuration updates that can change at runtime.
|
|
5
|
+
* Specifically handles pushId which can be updated per-session.
|
|
6
|
+
*/
|
|
7
|
+
class ConfigManager {
|
|
8
|
+
sessionPushIds = new Map();
|
|
9
|
+
globalPushId = null;
|
|
10
|
+
/**
|
|
11
|
+
* Update push ID for a specific session.
|
|
12
|
+
*/
|
|
13
|
+
updatePushId(sessionId, pushId) {
|
|
14
|
+
if (!pushId) {
|
|
15
|
+
logger.warn(`[ConfigManager] Attempted to set empty pushId for session ${sessionId}`);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const previous = this.sessionPushIds.get(sessionId);
|
|
19
|
+
if (previous !== pushId) {
|
|
20
|
+
logger.log(`[ConfigManager] ✨ Updated pushId for session ${sessionId}`);
|
|
21
|
+
logger.log(`[ConfigManager] - Previous: ${previous ? previous.substring(0, 20) + '...' : 'none'}`);
|
|
22
|
+
logger.log(`[ConfigManager] - New: ${pushId.substring(0, 20)}...`);
|
|
23
|
+
logger.log(`[ConfigManager] - Full new pushId: ${pushId}`);
|
|
24
|
+
this.sessionPushIds.set(sessionId, pushId);
|
|
25
|
+
this.globalPushId = pushId; // Also update global for backward compatibility
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Get push ID for a session (falls back to global if not found).
|
|
30
|
+
*/
|
|
31
|
+
getPushId(sessionId) {
|
|
32
|
+
if (sessionId) {
|
|
33
|
+
const sessionPushId = this.sessionPushIds.get(sessionId);
|
|
34
|
+
if (sessionPushId) {
|
|
35
|
+
return sessionPushId;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return this.globalPushId;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Clear push ID for a session.
|
|
42
|
+
*/
|
|
43
|
+
clearSession(sessionId) {
|
|
44
|
+
this.sessionPushIds.delete(sessionId);
|
|
45
|
+
logger.debug(`[ConfigManager] Cleared pushId for session ${sessionId}`);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Clear all cached push IDs.
|
|
49
|
+
*/
|
|
50
|
+
clear() {
|
|
51
|
+
this.sessionPushIds.clear();
|
|
52
|
+
this.globalPushId = null;
|
|
53
|
+
logger.debug(`[ConfigManager] Cleared all cached pushIds`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export const configManager = new ConfigManager();
|
package/dist/src/websocket.d.ts
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
import { EventEmitter } from "events";
|
|
2
2
|
import type { RuntimeEnv } from "openclaw/plugin-sdk";
|
|
3
3
|
import type { XYChannelConfig, OutboundWebSocketMessage } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Diagnostics for a single WebSocket connection
|
|
6
|
+
*/
|
|
7
|
+
export interface ConnectionDiagnostic {
|
|
8
|
+
exists: boolean;
|
|
9
|
+
readyState: string;
|
|
10
|
+
stateConnected: boolean;
|
|
11
|
+
stateReady: boolean;
|
|
12
|
+
reconnectAttempts: number;
|
|
13
|
+
lastHeartbeat: number;
|
|
14
|
+
heartbeatActive: boolean;
|
|
15
|
+
hasReconnectTimer: boolean;
|
|
16
|
+
listenerCount: number;
|
|
17
|
+
isOrphan: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Full diagnostics for WebSocket manager
|
|
21
|
+
*/
|
|
22
|
+
export interface ManagerDiagnostics {
|
|
23
|
+
cacheKey: string;
|
|
24
|
+
server1: ConnectionDiagnostic;
|
|
25
|
+
server2: ConnectionDiagnostic;
|
|
26
|
+
isShuttingDown: boolean;
|
|
27
|
+
totalEventListeners: number;
|
|
28
|
+
}
|
|
4
29
|
/**
|
|
5
30
|
* Manages dual WebSocket connections to XY servers.
|
|
6
31
|
* Implements session-to-server binding for message routing.
|
|
@@ -27,7 +52,12 @@ export declare class XYWebSocketManager extends EventEmitter {
|
|
|
27
52
|
private isShuttingDown;
|
|
28
53
|
private log;
|
|
29
54
|
private error;
|
|
55
|
+
private onHealthEvent?;
|
|
30
56
|
constructor(config: XYChannelConfig, runtime?: RuntimeEnv);
|
|
57
|
+
/**
|
|
58
|
+
* Set health event callback to report activity to OpenClaw framework.
|
|
59
|
+
*/
|
|
60
|
+
setHealthEventCallback(callback: () => void): void;
|
|
31
61
|
/**
|
|
32
62
|
* Check if config matches the current instance.
|
|
33
63
|
*/
|
|
@@ -49,6 +79,15 @@ export declare class XYWebSocketManager extends EventEmitter {
|
|
|
49
79
|
* Check if at least one server is ready.
|
|
50
80
|
*/
|
|
51
81
|
isReady(): boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Get detailed connection diagnostics for monitoring and debugging.
|
|
84
|
+
* Helps identify orphan connections and connection leaks.
|
|
85
|
+
*/
|
|
86
|
+
getConnectionDiagnostics(): ManagerDiagnostics;
|
|
87
|
+
/**
|
|
88
|
+
* Get diagnostic info for a single server connection.
|
|
89
|
+
*/
|
|
90
|
+
private getServerDiagnostic;
|
|
52
91
|
/**
|
|
53
92
|
* Connect to a specific server.
|
|
54
93
|
*/
|
package/dist/src/websocket.js
CHANGED
|
@@ -41,6 +41,8 @@ export class XYWebSocketManager extends EventEmitter {
|
|
|
41
41
|
// Logging functions following feishu pattern
|
|
42
42
|
log;
|
|
43
43
|
error;
|
|
44
|
+
// Health event callback
|
|
45
|
+
onHealthEvent;
|
|
44
46
|
constructor(config, runtime) {
|
|
45
47
|
super();
|
|
46
48
|
this.config = config;
|
|
@@ -48,6 +50,12 @@ export class XYWebSocketManager extends EventEmitter {
|
|
|
48
50
|
this.log = runtime?.log ?? console.log;
|
|
49
51
|
this.error = runtime?.error ?? console.error;
|
|
50
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Set health event callback to report activity to OpenClaw framework.
|
|
55
|
+
*/
|
|
56
|
+
setHealthEventCallback(callback) {
|
|
57
|
+
this.onHealthEvent = callback;
|
|
58
|
+
}
|
|
51
59
|
/**
|
|
52
60
|
* Check if config matches the current instance.
|
|
53
61
|
*/
|
|
@@ -144,19 +152,97 @@ export class XYWebSocketManager extends EventEmitter {
|
|
|
144
152
|
isReady() {
|
|
145
153
|
return this.state1.ready || this.state2.ready;
|
|
146
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Get detailed connection diagnostics for monitoring and debugging.
|
|
157
|
+
* Helps identify orphan connections and connection leaks.
|
|
158
|
+
*/
|
|
159
|
+
getConnectionDiagnostics() {
|
|
160
|
+
const cacheKey = `${this.config.apiKey}-${this.config.agentId}`;
|
|
161
|
+
const server1Diag = this.getServerDiagnostic("server1", this.ws1, this.state1, this.heartbeat1, this.reconnectTimer1);
|
|
162
|
+
const server2Diag = this.getServerDiagnostic("server2", this.ws2, this.state2, this.heartbeat2, this.reconnectTimer2);
|
|
163
|
+
// Count total event listeners on the manager
|
|
164
|
+
const totalEventListeners = this.listenerCount('message') +
|
|
165
|
+
this.listenerCount('connected') +
|
|
166
|
+
this.listenerCount('disconnected') +
|
|
167
|
+
this.listenerCount('error') +
|
|
168
|
+
this.listenerCount('ready') +
|
|
169
|
+
this.listenerCount('data-event');
|
|
170
|
+
return {
|
|
171
|
+
cacheKey,
|
|
172
|
+
server1: server1Diag,
|
|
173
|
+
server2: server2Diag,
|
|
174
|
+
isShuttingDown: this.isShuttingDown,
|
|
175
|
+
totalEventListeners,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Get diagnostic info for a single server connection.
|
|
180
|
+
*/
|
|
181
|
+
getServerDiagnostic(serverId, ws, state, heartbeat, reconnectTimer) {
|
|
182
|
+
const exists = ws !== null;
|
|
183
|
+
let readyState = 'NULL';
|
|
184
|
+
let listenerCount = 0;
|
|
185
|
+
if (ws) {
|
|
186
|
+
switch (ws.readyState) {
|
|
187
|
+
case WebSocket.CONNECTING:
|
|
188
|
+
readyState = 'CONNECTING';
|
|
189
|
+
break;
|
|
190
|
+
case WebSocket.OPEN:
|
|
191
|
+
readyState = 'OPEN';
|
|
192
|
+
break;
|
|
193
|
+
case WebSocket.CLOSING:
|
|
194
|
+
readyState = 'CLOSING';
|
|
195
|
+
break;
|
|
196
|
+
case WebSocket.CLOSED:
|
|
197
|
+
readyState = 'CLOSED';
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
// Count event listeners on the WebSocket
|
|
201
|
+
listenerCount = ws.listenerCount('message') +
|
|
202
|
+
ws.listenerCount('close') +
|
|
203
|
+
ws.listenerCount('error') +
|
|
204
|
+
ws.listenerCount('open') +
|
|
205
|
+
ws.listenerCount('pong');
|
|
206
|
+
}
|
|
207
|
+
// Orphan detection: connection is OPEN but has no message listeners
|
|
208
|
+
const isOrphan = exists &&
|
|
209
|
+
ws.readyState === WebSocket.OPEN &&
|
|
210
|
+
ws.listenerCount('message') === 0;
|
|
211
|
+
return {
|
|
212
|
+
exists,
|
|
213
|
+
readyState,
|
|
214
|
+
stateConnected: state.connected,
|
|
215
|
+
stateReady: state.ready,
|
|
216
|
+
reconnectAttempts: state.reconnectAttempts,
|
|
217
|
+
lastHeartbeat: state.lastHeartbeat,
|
|
218
|
+
heartbeatActive: heartbeat !== null,
|
|
219
|
+
hasReconnectTimer: reconnectTimer !== null,
|
|
220
|
+
listenerCount,
|
|
221
|
+
isOrphan,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
147
224
|
/**
|
|
148
225
|
* Connect to a specific server.
|
|
149
226
|
*/
|
|
150
227
|
async connectServer(serverId, url) {
|
|
151
228
|
return new Promise((resolve, reject) => {
|
|
152
|
-
|
|
229
|
+
// Check if URL is wss with IP address to bypass certificate validation
|
|
230
|
+
const urlObj = new URL(url);
|
|
231
|
+
const isWssWithIP = urlObj.protocol === 'wss:' && /^(\d{1,3}\.){3}\d{1,3}$/.test(urlObj.hostname);
|
|
232
|
+
const wsOptions = {
|
|
153
233
|
headers: {
|
|
154
234
|
"x-uid": this.config.uid,
|
|
155
235
|
"x-api-key": this.config.apiKey,
|
|
156
236
|
"x-agent-id": this.config.agentId,
|
|
157
237
|
"x-request-from": "openclaw",
|
|
158
238
|
},
|
|
159
|
-
}
|
|
239
|
+
};
|
|
240
|
+
// Bypass certificate validation for wss with IP address
|
|
241
|
+
if (isWssWithIP) {
|
|
242
|
+
this.log(`${serverId}: Bypassing certificate validation for IP address: ${urlObj.hostname}`);
|
|
243
|
+
wsOptions.rejectUnauthorized = false;
|
|
244
|
+
}
|
|
245
|
+
const ws = new WebSocket(url, wsOptions);
|
|
160
246
|
const state = serverId === "server1" ? this.state1 : this.state2;
|
|
161
247
|
// Set the WebSocket instance
|
|
162
248
|
if (serverId === "server1") {
|
|
@@ -271,7 +357,8 @@ export class XYWebSocketManager extends EventEmitter {
|
|
|
271
357
|
}, () => {
|
|
272
358
|
this.error(`Heartbeat timeout for ${serverId}, reconnecting...`);
|
|
273
359
|
this.reconnectServer(serverId);
|
|
274
|
-
}, serverId, this.log, this.error
|
|
360
|
+
}, serverId, this.log, this.error, this.onHealthEvent // ✅ Pass health event callback
|
|
361
|
+
);
|
|
275
362
|
heartbeat.start();
|
|
276
363
|
if (serverId === "server1") {
|
|
277
364
|
this.heartbeat1 = heartbeat;
|
|
@@ -317,19 +404,23 @@ export class XYWebSocketManager extends EventEmitter {
|
|
|
317
404
|
// Only emit data-event, don't send to openclaw
|
|
318
405
|
console.log(`[XY-${serverId}] Message contains only data parts, processing as tool result`);
|
|
319
406
|
for (const dataPart of dataParts) {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
407
|
+
// Data format: {events: [{header, payload}, ...]}
|
|
408
|
+
const events = dataPart.data?.events;
|
|
409
|
+
if (!Array.isArray(events)) {
|
|
410
|
+
console.warn(`[XY-${serverId}] dataPart.data.events is not an array, skipping`);
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
console.log(`[XY-${serverId}] Processing ${events.length} events from data.events`);
|
|
414
|
+
for (const item of events) {
|
|
415
|
+
// Check if it's an UploadExeResult (intent execution result)
|
|
416
|
+
if (item.header?.name === "UploadExeResult" && item.payload?.intentName) {
|
|
417
|
+
const dataEvent = {
|
|
418
|
+
intentName: item.payload.intentName,
|
|
419
|
+
outputs: item.payload.outputs || {},
|
|
420
|
+
status: "success",
|
|
421
|
+
};
|
|
422
|
+
console.log(`[XY-${serverId}] Emitting data-event:`, dataEvent);
|
|
423
|
+
this.emit("data-event", dataEvent);
|
|
333
424
|
}
|
|
334
425
|
}
|
|
335
426
|
}
|
|
@@ -343,9 +434,12 @@ export class XYWebSocketManager extends EventEmitter {
|
|
|
343
434
|
// Wrapped format (InboundWebSocketMessage)
|
|
344
435
|
const inboundMsg = parsed;
|
|
345
436
|
console.log(`[XY-${serverId}] Message type: Wrapped, msgType: ${inboundMsg.msgType}`);
|
|
346
|
-
//
|
|
437
|
+
// Handle heartbeat responses
|
|
347
438
|
if (inboundMsg.msgType === "heartbeat") {
|
|
348
|
-
console.log(`[XY-${serverId}]
|
|
439
|
+
console.log(`[XY-${serverId}] Received heartbeat response`);
|
|
440
|
+
// ✅ Report health: application-level heartbeat received
|
|
441
|
+
// This prevents openclaw health-monitor from marking connection as stale
|
|
442
|
+
this.onHealthEvent?.();
|
|
349
443
|
return;
|
|
350
444
|
}
|
|
351
445
|
// Handle data messages (e.g., intent execution results)
|
|
@@ -356,19 +450,23 @@ export class XYWebSocketManager extends EventEmitter {
|
|
|
356
450
|
const dataParts = a2aRequest.params?.message?.parts?.filter((p) => p.kind === "data");
|
|
357
451
|
if (dataParts && dataParts.length > 0) {
|
|
358
452
|
for (const dataPart of dataParts) {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
453
|
+
// Data format: {events: [{header, payload}, ...]}
|
|
454
|
+
const events = dataPart.data?.events;
|
|
455
|
+
if (!Array.isArray(events)) {
|
|
456
|
+
console.warn(`[XY-${serverId}] dataPart.data.events is not an array, skipping`);
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
console.log(`[XY-${serverId}] Processing ${events.length} events from data.events`);
|
|
460
|
+
for (const item of events) {
|
|
461
|
+
// Check if it's an UploadExeResult (intent execution result)
|
|
462
|
+
if (item.header?.name === "UploadExeResult" && item.payload?.intentName) {
|
|
463
|
+
const dataEvent = {
|
|
464
|
+
intentName: item.payload.intentName,
|
|
465
|
+
outputs: item.payload.outputs || {},
|
|
466
|
+
status: "success",
|
|
467
|
+
};
|
|
468
|
+
console.log(`[XY-${serverId}] Emitting data-event:`, dataEvent);
|
|
469
|
+
this.emit("data-event", dataEvent);
|
|
372
470
|
}
|
|
373
471
|
}
|
|
374
472
|
}
|