@ynhcj/xiaoyi-channel 0.0.9 β 0.0.11-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 +7 -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 +47 -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 +142 -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/modify-note-tool.d.ts +9 -0
- package/dist/src/tools/modify-note-tool.js +163 -0
- 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-contact-tool.d.ts +5 -0
- package/dist/src/tools/search-contact-tool.js +147 -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
package/dist/src/bot.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { getXYRuntime } from "./runtime.js";
|
|
2
2
|
import { createXYReplyDispatcher } from "./reply-dispatcher.js";
|
|
3
|
-
import { parseA2AMessage, extractTextFromParts, extractFileParts } from "./parser.js";
|
|
3
|
+
import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId } from "./parser.js";
|
|
4
4
|
import { downloadFilesFromParts } from "./file-download.js";
|
|
5
5
|
import { resolveXYConfig } from "./config.js";
|
|
6
6
|
import { sendStatusUpdate, sendClearContextResponse, sendTasksCancelResponse } from "./formatter.js";
|
|
7
7
|
import { registerSession, unregisterSession } from "./tools/session-manager.js";
|
|
8
|
+
import { configManager } from "./utils/config-manager.js";
|
|
8
9
|
/**
|
|
9
10
|
* Handle an incoming A2A message.
|
|
10
11
|
* This is the main entry point for message processing.
|
|
@@ -55,6 +56,18 @@ export async function handleXYMessage(params) {
|
|
|
55
56
|
}
|
|
56
57
|
// Parse the A2A message (for regular messages)
|
|
57
58
|
const parsed = parseA2AMessage(message);
|
|
59
|
+
// Extract and update push_id if present
|
|
60
|
+
const pushId = extractPushId(parsed.parts);
|
|
61
|
+
if (pushId) {
|
|
62
|
+
log(`[BOT] π Extracted push_id from user message`);
|
|
63
|
+
log(`[BOT] - Session ID: ${parsed.sessionId}`);
|
|
64
|
+
log(`[BOT] - Push ID preview: ${pushId.substring(0, 20)}...`);
|
|
65
|
+
log(`[BOT] - Full push_id: ${pushId}`);
|
|
66
|
+
configManager.updatePushId(parsed.sessionId, pushId);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
log(`[BOT] βΉοΈ No push_id found in message, will use config default`);
|
|
70
|
+
}
|
|
58
71
|
// Resolve configuration (needed for status updates)
|
|
59
72
|
const config = resolveXYConfig(cfg);
|
|
60
73
|
// β
Resolve agent route (following feishu pattern)
|
|
@@ -83,6 +96,18 @@ export async function handleXYMessage(params) {
|
|
|
83
96
|
agentId: route.accountId,
|
|
84
97
|
});
|
|
85
98
|
log(`[BOT] β
Session registered for tools`);
|
|
99
|
+
// Send initial status update immediately after parsing message
|
|
100
|
+
log(`[STATUS] Sending initial status update for session ${parsed.sessionId}`);
|
|
101
|
+
void sendStatusUpdate({
|
|
102
|
+
config,
|
|
103
|
+
sessionId: parsed.sessionId,
|
|
104
|
+
taskId: parsed.taskId,
|
|
105
|
+
messageId: parsed.messageId,
|
|
106
|
+
text: "δ»»ε‘ζ£ε¨ε€ηδΈοΌθ―·η¨ε~",
|
|
107
|
+
state: "working",
|
|
108
|
+
}).catch((err) => {
|
|
109
|
+
error(`Failed to send initial status update:`, err);
|
|
110
|
+
});
|
|
86
111
|
// Extract text and files from parts
|
|
87
112
|
const text = extractTextFromParts(parsed.parts);
|
|
88
113
|
const fileParts = extractFileParts(parsed.parts);
|
|
@@ -99,7 +124,7 @@ export async function handleXYMessage(params) {
|
|
|
99
124
|
messageBody = `${speaker}: ${messageBody}`;
|
|
100
125
|
// Format agent envelope (following feishu pattern)
|
|
101
126
|
const body = core.channel.reply.formatAgentEnvelope({
|
|
102
|
-
channel: "
|
|
127
|
+
channel: "xiaoyi-channel",
|
|
103
128
|
from: speaker,
|
|
104
129
|
timestamp: new Date(),
|
|
105
130
|
envelope: envelopeOptions,
|
|
@@ -180,6 +205,7 @@ export async function handleXYMessage(params) {
|
|
|
180
205
|
log(`xy: dispatch complete (session=${parsed.sessionId})`);
|
|
181
206
|
}
|
|
182
207
|
catch (err) {
|
|
208
|
+
// β
Only log error, don't re-throw to prevent gateway restart
|
|
183
209
|
error("Failed to handle XY message:", err);
|
|
184
210
|
runtime.error?.(`xy: Failed to handle message: ${String(err)}`);
|
|
185
211
|
log(`[BOT] β Error occurred, attempting cleanup...`);
|
|
@@ -208,7 +234,7 @@ export async function handleXYMessage(params) {
|
|
|
208
234
|
log(`[BOT] β οΈ Cleanup failed:`, cleanupErr);
|
|
209
235
|
// Ignore cleanup errors
|
|
210
236
|
}
|
|
211
|
-
throw
|
|
237
|
+
// β Don't re-throw: message processing error should not affect gateway stability
|
|
212
238
|
}
|
|
213
239
|
}
|
|
214
240
|
/**
|
package/dist/src/channel.js
CHANGED
|
@@ -5,6 +5,10 @@ import { xyOnboardingAdapter } from "./onboarding.js";
|
|
|
5
5
|
import { locationTool } from "./tools/location-tool.js";
|
|
6
6
|
import { noteTool } from "./tools/note-tool.js";
|
|
7
7
|
import { searchNoteTool } from "./tools/search-note-tool.js";
|
|
8
|
+
import { modifyNoteTool } from "./tools/modify-note-tool.js";
|
|
9
|
+
import { calendarTool } from "./tools/calendar-tool.js";
|
|
10
|
+
import { searchCalendarTool } from "./tools/search-calendar-tool.js";
|
|
11
|
+
import { searchContactTool } from "./tools/search-contact-tool.js";
|
|
8
12
|
/**
|
|
9
13
|
* Xiaoyi Channel Plugin for OpenClaw.
|
|
10
14
|
* Implements Xiaoyi A2A protocol with dual WebSocket connections.
|
|
@@ -22,6 +26,7 @@ export const xyPlugin = {
|
|
|
22
26
|
agentPrompt: {
|
|
23
27
|
messageToolHints: () => [
|
|
24
28
|
"- xiaoyi targeting: omit `target` to reply to the current conversation (auto-inferred). Explicit targets: `default`",
|
|
29
|
+
"- If the user requests a file, you can call the message tool with the xiaoyi-channel channel to return it. Note: sendMedia requires a text reply."
|
|
25
30
|
],
|
|
26
31
|
},
|
|
27
32
|
capabilities: {
|
|
@@ -43,7 +48,7 @@ export const xyPlugin = {
|
|
|
43
48
|
},
|
|
44
49
|
outbound: xyOutbound,
|
|
45
50
|
onboarding: xyOnboardingAdapter,
|
|
46
|
-
agentTools: [locationTool, noteTool, searchNoteTool],
|
|
51
|
+
agentTools: [locationTool, noteTool, searchNoteTool, modifyNoteTool, calendarTool, searchCalendarTool, searchContactTool],
|
|
47
52
|
messaging: {
|
|
48
53
|
normalizeTarget: (raw) => {
|
|
49
54
|
const trimmed = raw.trim();
|
|
@@ -79,6 +84,7 @@ export const xyPlugin = {
|
|
|
79
84
|
runtime: context.runtime,
|
|
80
85
|
abortSignal: context.abortSignal,
|
|
81
86
|
accountId: context.accountId,
|
|
87
|
+
setStatus: context.setStatus,
|
|
82
88
|
});
|
|
83
89
|
},
|
|
84
90
|
},
|
package/dist/src/client.d.ts
CHANGED
|
@@ -10,6 +10,11 @@ export declare function setClientRuntime(rt: RuntimeEnv | undefined): void;
|
|
|
10
10
|
* Reuses existing managers if config matches.
|
|
11
11
|
*/
|
|
12
12
|
export declare function getXYWebSocketManager(config: XYChannelConfig): XYWebSocketManager;
|
|
13
|
+
/**
|
|
14
|
+
* Remove a specific WebSocket manager from cache.
|
|
15
|
+
* Disconnects the manager and removes it from the cache.
|
|
16
|
+
*/
|
|
17
|
+
export declare function removeXYWebSocketManager(config: XYChannelConfig): void;
|
|
13
18
|
/**
|
|
14
19
|
* Clear all cached WebSocket managers.
|
|
15
20
|
*/
|
|
@@ -18,3 +23,13 @@ export declare function clearXYWebSocketManagers(): void;
|
|
|
18
23
|
* Get the number of cached managers.
|
|
19
24
|
*/
|
|
20
25
|
export declare function getCachedManagerCount(): number;
|
|
26
|
+
/**
|
|
27
|
+
* Diagnose all cached WebSocket managers.
|
|
28
|
+
* Helps identify connection issues and orphan connections.
|
|
29
|
+
*/
|
|
30
|
+
export declare function diagnoseAllManagers(): void;
|
|
31
|
+
/**
|
|
32
|
+
* Clean up orphan connections across all managers.
|
|
33
|
+
* Returns the number of managers that had orphan connections.
|
|
34
|
+
*/
|
|
35
|
+
export declare function cleanupOrphanConnections(): number;
|
package/dist/src/client.js
CHANGED
|
@@ -34,6 +34,23 @@ export function getXYWebSocketManager(config) {
|
|
|
34
34
|
log(`[WS-MANAGER-CACHE] π Total managers after creation: ${wsManagerCache.size}`);
|
|
35
35
|
return cached;
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Remove a specific WebSocket manager from cache.
|
|
39
|
+
* Disconnects the manager and removes it from the cache.
|
|
40
|
+
*/
|
|
41
|
+
export function removeXYWebSocketManager(config) {
|
|
42
|
+
const cacheKey = `${config.apiKey}-${config.agentId}`;
|
|
43
|
+
const manager = wsManagerCache.get(cacheKey);
|
|
44
|
+
if (manager) {
|
|
45
|
+
console.log(`ποΈ [WS-MANAGER-CACHE] Removing manager from cache: ${cacheKey}`);
|
|
46
|
+
manager.disconnect();
|
|
47
|
+
wsManagerCache.delete(cacheKey);
|
|
48
|
+
console.log(`ποΈ [WS-MANAGER-CACHE] Manager removed, remaining managers: ${wsManagerCache.size}`);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
console.log(`β οΈ [WS-MANAGER-CACHE] Manager not found in cache: ${cacheKey}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
37
54
|
/**
|
|
38
55
|
* Clear all cached WebSocket managers.
|
|
39
56
|
*/
|
|
@@ -51,3 +68,80 @@ export function clearXYWebSocketManagers() {
|
|
|
51
68
|
export function getCachedManagerCount() {
|
|
52
69
|
return wsManagerCache.size;
|
|
53
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Diagnose all cached WebSocket managers.
|
|
73
|
+
* Helps identify connection issues and orphan connections.
|
|
74
|
+
*/
|
|
75
|
+
export function diagnoseAllManagers() {
|
|
76
|
+
console.log("========================================");
|
|
77
|
+
console.log("π WebSocket Manager Global Diagnostics");
|
|
78
|
+
console.log("========================================");
|
|
79
|
+
console.log(`Total cached managers: ${wsManagerCache.size}`);
|
|
80
|
+
console.log("");
|
|
81
|
+
if (wsManagerCache.size === 0) {
|
|
82
|
+
console.log("βΉοΈ No managers in cache");
|
|
83
|
+
console.log("========================================");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
let orphanCount = 0;
|
|
87
|
+
wsManagerCache.forEach((manager, key) => {
|
|
88
|
+
const diag = manager.getConnectionDiagnostics();
|
|
89
|
+
console.log(`π Manager: ${key}`);
|
|
90
|
+
console.log(` Shutting down: ${diag.isShuttingDown}`);
|
|
91
|
+
console.log(` Total event listeners on manager: ${diag.totalEventListeners}`);
|
|
92
|
+
// Server 1
|
|
93
|
+
console.log(` π Server1:`);
|
|
94
|
+
console.log(` - Exists: ${diag.server1.exists}`);
|
|
95
|
+
console.log(` - ReadyState: ${diag.server1.readyState}`);
|
|
96
|
+
console.log(` - State connected/ready: ${diag.server1.stateConnected}/${diag.server1.stateReady}`);
|
|
97
|
+
console.log(` - Reconnect attempts: ${diag.server1.reconnectAttempts}`);
|
|
98
|
+
console.log(` - Listeners on WebSocket: ${diag.server1.listenerCount}`);
|
|
99
|
+
console.log(` - Heartbeat active: ${diag.server1.heartbeatActive}`);
|
|
100
|
+
console.log(` - Has reconnect timer: ${diag.server1.hasReconnectTimer}`);
|
|
101
|
+
if (diag.server1.isOrphan) {
|
|
102
|
+
console.log(` β οΈ ORPHAN CONNECTION DETECTED!`);
|
|
103
|
+
orphanCount++;
|
|
104
|
+
}
|
|
105
|
+
// Server 2
|
|
106
|
+
console.log(` π Server2:`);
|
|
107
|
+
console.log(` - Exists: ${diag.server2.exists}`);
|
|
108
|
+
console.log(` - ReadyState: ${diag.server2.readyState}`);
|
|
109
|
+
console.log(` - State connected/ready: ${diag.server2.stateConnected}/${diag.server2.stateReady}`);
|
|
110
|
+
console.log(` - Reconnect attempts: ${diag.server2.reconnectAttempts}`);
|
|
111
|
+
console.log(` - Listeners on WebSocket: ${diag.server2.listenerCount}`);
|
|
112
|
+
console.log(` - Heartbeat active: ${diag.server2.heartbeatActive}`);
|
|
113
|
+
console.log(` - Has reconnect timer: ${diag.server2.hasReconnectTimer}`);
|
|
114
|
+
if (diag.server2.isOrphan) {
|
|
115
|
+
console.log(` β οΈ ORPHAN CONNECTION DETECTED!`);
|
|
116
|
+
orphanCount++;
|
|
117
|
+
}
|
|
118
|
+
console.log("");
|
|
119
|
+
});
|
|
120
|
+
if (orphanCount > 0) {
|
|
121
|
+
console.log(`β οΈ Total orphan connections found: ${orphanCount}`);
|
|
122
|
+
console.log(`π‘ Suggestion: These connections should be cleaned up`);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
console.log(`β
No orphan connections found`);
|
|
126
|
+
}
|
|
127
|
+
console.log("========================================");
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Clean up orphan connections across all managers.
|
|
131
|
+
* Returns the number of managers that had orphan connections.
|
|
132
|
+
*/
|
|
133
|
+
export function cleanupOrphanConnections() {
|
|
134
|
+
let cleanedCount = 0;
|
|
135
|
+
wsManagerCache.forEach((manager, key) => {
|
|
136
|
+
const diag = manager.getConnectionDiagnostics();
|
|
137
|
+
if (diag.server1.isOrphan || diag.server2.isOrphan) {
|
|
138
|
+
console.log(`π§Ή Cleaning up orphan connections in manager: ${key}`);
|
|
139
|
+
manager.disconnect();
|
|
140
|
+
cleanedCount++;
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
if (cleanedCount > 0) {
|
|
144
|
+
console.log(`π§Ή Cleaned up ${cleanedCount} manager(s) with orphan connections`);
|
|
145
|
+
}
|
|
146
|
+
return cleanedCount;
|
|
147
|
+
}
|
|
@@ -8,8 +8,10 @@ import { logger } from "./utils/logger.js";
|
|
|
8
8
|
*/
|
|
9
9
|
export async function downloadFile(url, destPath) {
|
|
10
10
|
logger.debug(`Downloading file from ${url} to ${destPath}`);
|
|
11
|
+
const controller = new AbortController();
|
|
12
|
+
const timeout = setTimeout(() => controller.abort(), 30000); // 30 seconds timeout
|
|
11
13
|
try {
|
|
12
|
-
const response = await fetch(url);
|
|
14
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
13
15
|
if (!response.ok) {
|
|
14
16
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
15
17
|
}
|
|
@@ -19,9 +21,16 @@ export async function downloadFile(url, destPath) {
|
|
|
19
21
|
logger.debug(`File downloaded successfully: ${destPath}`);
|
|
20
22
|
}
|
|
21
23
|
catch (error) {
|
|
24
|
+
if (error.name === 'AbortError') {
|
|
25
|
+
logger.error(`Download timeout (30s) for ${url}`);
|
|
26
|
+
throw new Error(`Download timeout after 30 seconds`);
|
|
27
|
+
}
|
|
22
28
|
logger.error(`Failed to download file from ${url}:`, error);
|
|
23
29
|
throw error;
|
|
24
30
|
}
|
|
31
|
+
finally {
|
|
32
|
+
clearTimeout(timeout);
|
|
33
|
+
}
|
|
25
34
|
}
|
|
26
35
|
/**
|
|
27
36
|
* Download files from A2A file parts.
|
package/dist/src/formatter.d.ts
CHANGED
|
@@ -20,6 +20,23 @@ export interface SendA2AResponseParams {
|
|
|
20
20
|
* Send an A2A artifact update response.
|
|
21
21
|
*/
|
|
22
22
|
export declare function sendA2AResponse(params: SendA2AResponseParams): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Parameters for sending a reasoning text update (intermediate, streamed).
|
|
25
|
+
*/
|
|
26
|
+
export interface SendReasoningTextUpdateParams {
|
|
27
|
+
config: XYChannelConfig;
|
|
28
|
+
sessionId: string;
|
|
29
|
+
taskId: string;
|
|
30
|
+
messageId: string;
|
|
31
|
+
text: string;
|
|
32
|
+
append?: boolean;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Send an A2A artifact-update with reasoningText part.
|
|
36
|
+
* Used for onToolStart, onToolResult, onReasoningStream, onReasoningEnd, onPartialReply.
|
|
37
|
+
* append=true, final=false, lastChunk=true, text is suffixed with newline for markdown rendering.
|
|
38
|
+
*/
|
|
39
|
+
export declare function sendReasoningTextUpdate(params: SendReasoningTextUpdateParams): Promise<void>;
|
|
23
40
|
/**
|
|
24
41
|
* Parameters for sending a status update.
|
|
25
42
|
*/
|
package/dist/src/formatter.js
CHANGED
|
@@ -67,6 +67,49 @@ export async function sendA2AResponse(params) {
|
|
|
67
67
|
await wsManager.sendMessage(sessionId, outboundMessage);
|
|
68
68
|
log(`[A2A_RESPONSE] β
Message sent successfully`);
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Send an A2A artifact-update with reasoningText part.
|
|
72
|
+
* Used for onToolStart, onToolResult, onReasoningStream, onReasoningEnd, onPartialReply.
|
|
73
|
+
* append=true, final=false, lastChunk=true, text is suffixed with newline for markdown rendering.
|
|
74
|
+
*/
|
|
75
|
+
export async function sendReasoningTextUpdate(params) {
|
|
76
|
+
const { config, sessionId, taskId, messageId, text, append = true } = params;
|
|
77
|
+
const runtime = getXYRuntime();
|
|
78
|
+
const log = runtime?.log ?? console.log;
|
|
79
|
+
const error = runtime?.error ?? console.error;
|
|
80
|
+
const artifact = {
|
|
81
|
+
taskId,
|
|
82
|
+
kind: "artifact-update",
|
|
83
|
+
append,
|
|
84
|
+
lastChunk: true,
|
|
85
|
+
final: false,
|
|
86
|
+
artifact: {
|
|
87
|
+
artifactId: uuidv4(),
|
|
88
|
+
parts: [
|
|
89
|
+
{
|
|
90
|
+
kind: "reasoningText",
|
|
91
|
+
reasoningText: text,
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
const jsonRpcResponse = {
|
|
97
|
+
jsonrpc: "2.0",
|
|
98
|
+
id: messageId,
|
|
99
|
+
result: artifact,
|
|
100
|
+
};
|
|
101
|
+
const wsManager = getXYWebSocketManager(config);
|
|
102
|
+
const outboundMessage = {
|
|
103
|
+
msgType: "agent_response",
|
|
104
|
+
agentId: config.agentId,
|
|
105
|
+
sessionId,
|
|
106
|
+
taskId,
|
|
107
|
+
msgDetail: JSON.stringify(jsonRpcResponse),
|
|
108
|
+
};
|
|
109
|
+
log(`[REASONING_TEXT] π€ Sending reasoningText update: sessionId=${sessionId}, taskId=${taskId}, text.length=${text.length}`);
|
|
110
|
+
await wsManager.sendMessage(sessionId, outboundMessage);
|
|
111
|
+
log(`[REASONING_TEXT] β
Sent successfully`);
|
|
112
|
+
}
|
|
70
113
|
/**
|
|
71
114
|
* Send an A2A task status update.
|
|
72
115
|
* Follows A2A protocol standard format with nested status object.
|
|
@@ -132,6 +175,7 @@ export async function sendCommand(params) {
|
|
|
132
175
|
const log = runtime?.log ?? console.log;
|
|
133
176
|
const error = runtime?.error ?? console.error;
|
|
134
177
|
// Build artifact update with command as data
|
|
178
|
+
// Wrap command in commands array as per protocol requirement
|
|
135
179
|
const artifact = {
|
|
136
180
|
taskId,
|
|
137
181
|
kind: "artifact-update",
|
|
@@ -143,7 +187,9 @@ export async function sendCommand(params) {
|
|
|
143
187
|
parts: [
|
|
144
188
|
{
|
|
145
189
|
kind: "data",
|
|
146
|
-
data:
|
|
190
|
+
data: {
|
|
191
|
+
commands: [command],
|
|
192
|
+
},
|
|
147
193
|
},
|
|
148
194
|
],
|
|
149
195
|
},
|
package/dist/src/heartbeat.d.ts
CHANGED
|
@@ -13,12 +13,13 @@ export declare class HeartbeatManager {
|
|
|
13
13
|
private config;
|
|
14
14
|
private onTimeout;
|
|
15
15
|
private serverName;
|
|
16
|
+
private onHeartbeatSuccess?;
|
|
16
17
|
private intervalTimer;
|
|
17
18
|
private timeoutTimer;
|
|
18
19
|
private lastPongTime;
|
|
19
20
|
private log;
|
|
20
21
|
private error;
|
|
21
|
-
constructor(ws: WebSocket, config: HeartbeatConfig, onTimeout: () => void, serverName?: string, logFn?: (msg: string, ...args: any[]) => void, errorFn?: (msg: string, ...args: any[]) => void);
|
|
22
|
+
constructor(ws: WebSocket, config: HeartbeatConfig, onTimeout: () => void, serverName?: string, logFn?: (msg: string, ...args: any[]) => void, errorFn?: (msg: string, ...args: any[]) => void, onHeartbeatSuccess?: () => void);
|
|
22
23
|
/**
|
|
23
24
|
* Start heartbeat monitoring.
|
|
24
25
|
*/
|
package/dist/src/heartbeat.js
CHANGED
|
@@ -9,17 +9,20 @@ export class HeartbeatManager {
|
|
|
9
9
|
config;
|
|
10
10
|
onTimeout;
|
|
11
11
|
serverName;
|
|
12
|
+
onHeartbeatSuccess;
|
|
12
13
|
intervalTimer = null;
|
|
13
14
|
timeoutTimer = null;
|
|
14
15
|
lastPongTime = 0;
|
|
15
16
|
// Logging functions following feishu pattern
|
|
16
17
|
log;
|
|
17
18
|
error;
|
|
18
|
-
constructor(ws, config, onTimeout, serverName = "unknown", logFn, errorFn
|
|
19
|
+
constructor(ws, config, onTimeout, serverName = "unknown", logFn, errorFn, onHeartbeatSuccess // β
ζ°ε’οΌεΏθ·³ζεεθ°
|
|
20
|
+
) {
|
|
19
21
|
this.ws = ws;
|
|
20
22
|
this.config = config;
|
|
21
23
|
this.onTimeout = onTimeout;
|
|
22
24
|
this.serverName = serverName;
|
|
25
|
+
this.onHeartbeatSuccess = onHeartbeatSuccess;
|
|
23
26
|
this.log = logFn ?? console.log;
|
|
24
27
|
this.error = errorFn ?? console.error;
|
|
25
28
|
}
|
|
@@ -36,6 +39,8 @@ export class HeartbeatManager {
|
|
|
36
39
|
clearTimeout(this.timeoutTimer);
|
|
37
40
|
this.timeoutTimer = null;
|
|
38
41
|
}
|
|
42
|
+
// β
Report health: heartbeat successful
|
|
43
|
+
this.onHeartbeatSuccess?.();
|
|
39
44
|
});
|
|
40
45
|
// Start interval timer
|
|
41
46
|
this.intervalTimer = setInterval(() => {
|
package/dist/src/monitor.d.ts
CHANGED
|
@@ -4,6 +4,11 @@ export type MonitorXYOpts = {
|
|
|
4
4
|
runtime?: RuntimeEnv;
|
|
5
5
|
abortSignal?: AbortSignal;
|
|
6
6
|
accountId?: string;
|
|
7
|
+
setStatus?: (status: {
|
|
8
|
+
lastEventAt?: number;
|
|
9
|
+
lastInboundAt?: number;
|
|
10
|
+
connected?: boolean;
|
|
11
|
+
}) => void;
|
|
7
12
|
};
|
|
8
13
|
/**
|
|
9
14
|
* Monitor XY channel WebSocket connections.
|
package/dist/src/monitor.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolveXYConfig } from "./config.js";
|
|
2
|
-
import { getXYWebSocketManager } from "./client.js";
|
|
2
|
+
import { getXYWebSocketManager, diagnoseAllManagers, cleanupOrphanConnections, removeXYWebSocketManager } from "./client.js";
|
|
3
3
|
import { handleXYMessage } from "./bot.js";
|
|
4
4
|
/**
|
|
5
5
|
* Per-session serial queue that ensures messages from the same session are processed
|
|
@@ -37,19 +37,36 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
37
37
|
throw new Error(`XY account is disabled`);
|
|
38
38
|
}
|
|
39
39
|
const accountId = opts.accountId ?? "default";
|
|
40
|
+
// Create trackEvent function to report health to OpenClaw framework
|
|
41
|
+
const trackEvent = opts.setStatus
|
|
42
|
+
? () => {
|
|
43
|
+
opts.setStatus({ lastEventAt: Date.now(), lastInboundAt: Date.now() });
|
|
44
|
+
}
|
|
45
|
+
: undefined;
|
|
46
|
+
// π Diagnose WebSocket managers before gateway start
|
|
47
|
+
console.log("π [DIAGNOSTICS] Checking WebSocket managers before gateway start...");
|
|
48
|
+
diagnoseAllManagers();
|
|
40
49
|
// Get WebSocket manager (cached)
|
|
41
50
|
const wsManager = getXYWebSocketManager(account);
|
|
51
|
+
// β
Set health event callback for heartbeat reporting
|
|
52
|
+
if (trackEvent) {
|
|
53
|
+
wsManager.setHealthEventCallback(trackEvent);
|
|
54
|
+
}
|
|
42
55
|
// Track logged servers to avoid duplicate logs
|
|
43
56
|
const loggedServers = new Set();
|
|
44
57
|
// Track active message processing to detect duplicates
|
|
45
58
|
const activeMessages = new Set();
|
|
46
59
|
// Create session queue for ordered message processing
|
|
47
60
|
const enqueue = createSessionQueue();
|
|
61
|
+
// Health check interval
|
|
62
|
+
let healthCheckInterval = null;
|
|
48
63
|
return new Promise((resolve, reject) => {
|
|
49
64
|
// Event handlers (defined early so they can be referenced in cleanup)
|
|
50
65
|
const messageHandler = (message, sessionId, serverId) => {
|
|
51
66
|
const messageKey = `${sessionId}::${message.id}`;
|
|
52
67
|
log(`[MONITOR-HANDLER] ####### messageHandler triggered: serverId=${serverId}, sessionId=${sessionId}, messageId=${message.id} #######`);
|
|
68
|
+
// β
Report health: received a message
|
|
69
|
+
trackEvent?.();
|
|
53
70
|
// Check for duplicate message handling
|
|
54
71
|
if (activeMessages.has(messageKey)) {
|
|
55
72
|
error(`[MONITOR-HANDLER] β οΈ WARNING: Duplicate message detected! messageKey=${messageKey}, this may cause duplicate dispatchers!`);
|
|
@@ -68,8 +85,8 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
68
85
|
log(`[MONITOR-HANDLER] β
Completed handleXYMessage for messageKey=${messageKey}`);
|
|
69
86
|
}
|
|
70
87
|
catch (err) {
|
|
88
|
+
// β
Only log error, don't re-throw to prevent gateway restart
|
|
71
89
|
error(`XY gateway: error handling message from ${serverId}: ${String(err)}`);
|
|
72
|
-
throw err;
|
|
73
90
|
}
|
|
74
91
|
finally {
|
|
75
92
|
// Remove from active messages when done
|
|
@@ -88,26 +105,48 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
88
105
|
log(`XY gateway: ${serverId} connected`);
|
|
89
106
|
loggedServers.add(serverId);
|
|
90
107
|
}
|
|
108
|
+
// β
Report health: connection established
|
|
109
|
+
trackEvent?.();
|
|
110
|
+
opts.setStatus?.({ connected: true });
|
|
91
111
|
};
|
|
92
112
|
const disconnectedHandler = (serverId) => {
|
|
93
113
|
console.warn(`XY gateway: ${serverId} disconnected`);
|
|
94
114
|
loggedServers.delete(serverId);
|
|
115
|
+
// β
Report disconnection status (only if all servers disconnected)
|
|
116
|
+
if (loggedServers.size === 0) {
|
|
117
|
+
opts.setStatus?.({ connected: false });
|
|
118
|
+
}
|
|
95
119
|
};
|
|
96
120
|
const errorHandler = (err, serverId) => {
|
|
97
121
|
error(`XY gateway: ${serverId} error: ${String(err)}`);
|
|
98
122
|
};
|
|
99
123
|
const cleanup = () => {
|
|
100
124
|
log("XY gateway: cleaning up...");
|
|
125
|
+
// π Diagnose before cleanup
|
|
126
|
+
console.log("π [DIAGNOSTICS] Checking WebSocket managers before cleanup...");
|
|
127
|
+
diagnoseAllManagers();
|
|
128
|
+
// Stop health check interval
|
|
129
|
+
if (healthCheckInterval) {
|
|
130
|
+
clearInterval(healthCheckInterval);
|
|
131
|
+
healthCheckInterval = null;
|
|
132
|
+
console.log("βΈοΈ Stopped periodic health check");
|
|
133
|
+
}
|
|
101
134
|
// Remove event handlers to prevent duplicate calls on gateway restart
|
|
102
135
|
wsManager.off("message", messageHandler);
|
|
103
136
|
wsManager.off("connected", connectedHandler);
|
|
104
137
|
wsManager.off("disconnected", disconnectedHandler);
|
|
105
138
|
wsManager.off("error", errorHandler);
|
|
106
|
-
//
|
|
107
|
-
//
|
|
139
|
+
// β
Disconnect the wsManager to prevent connection leaks
|
|
140
|
+
// This is safe because each gateway lifecycle should have clean connections
|
|
141
|
+
wsManager.disconnect();
|
|
142
|
+
// β
Remove manager from cache to prevent reusing dirty state
|
|
143
|
+
removeXYWebSocketManager(account);
|
|
108
144
|
loggedServers.clear();
|
|
109
145
|
activeMessages.clear();
|
|
110
146
|
log(`[MONITOR-HANDLER] π§Ή Cleanup complete, cleared active messages`);
|
|
147
|
+
// π Diagnose after cleanup
|
|
148
|
+
console.log("π [DIAGNOSTICS] Checking WebSocket managers after cleanup...");
|
|
149
|
+
diagnoseAllManagers();
|
|
111
150
|
};
|
|
112
151
|
const handleAbort = () => {
|
|
113
152
|
log("XY gateway: abort signal received, stopping");
|
|
@@ -126,6 +165,17 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
126
165
|
wsManager.on("connected", connectedHandler);
|
|
127
166
|
wsManager.on("disconnected", disconnectedHandler);
|
|
128
167
|
wsManager.on("error", errorHandler);
|
|
168
|
+
// Start periodic health check (every 5 minutes)
|
|
169
|
+
console.log("π₯ Starting periodic health check (every 5 minutes)...");
|
|
170
|
+
healthCheckInterval = setInterval(() => {
|
|
171
|
+
console.log("π₯ [HEALTH CHECK] Periodic WebSocket diagnostics...");
|
|
172
|
+
diagnoseAllManagers();
|
|
173
|
+
// Auto-cleanup orphan connections
|
|
174
|
+
const cleaned = cleanupOrphanConnections();
|
|
175
|
+
if (cleaned > 0) {
|
|
176
|
+
console.log(`π§Ή [HEALTH CHECK] Auto-cleaned ${cleaned} manager(s) with orphan connections`);
|
|
177
|
+
}
|
|
178
|
+
}, 5 * 60 * 1000); // 5 minutes
|
|
129
179
|
// Connect to WebSocket servers
|
|
130
180
|
wsManager.connect()
|
|
131
181
|
.then(() => {
|
package/dist/src/outbound.js
CHANGED
|
@@ -4,6 +4,39 @@ import { XYPushService } from "./push.js";
|
|
|
4
4
|
import { getLatestSessionContext } from "./tools/session-manager.js";
|
|
5
5
|
// Special marker for default push delivery when no target is specified
|
|
6
6
|
const DEFAULT_PUSH_MARKER = "default";
|
|
7
|
+
// File extension to MIME type mapping
|
|
8
|
+
const FILE_TYPE_TO_MIME_TYPE = {
|
|
9
|
+
txt: "text/plain",
|
|
10
|
+
html: "text/html",
|
|
11
|
+
css: "text/css",
|
|
12
|
+
js: "application/javascript",
|
|
13
|
+
json: "application/json",
|
|
14
|
+
png: "image/png",
|
|
15
|
+
jpeg: "image/jpeg",
|
|
16
|
+
jpg: "image/jpeg",
|
|
17
|
+
gif: "image/gif",
|
|
18
|
+
svg: "image/svg+xml",
|
|
19
|
+
pdf: "application/pdf",
|
|
20
|
+
zip: "application/zip",
|
|
21
|
+
doc: "application/msword",
|
|
22
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
23
|
+
xls: "application/vnd.ms-excel",
|
|
24
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
25
|
+
ppt: "application/vnd.ms-powerpoint",
|
|
26
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
27
|
+
mp3: "audio/mpeg",
|
|
28
|
+
mp4: "video/mp4",
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Get MIME type from file extension
|
|
32
|
+
*/
|
|
33
|
+
function getMimeTypeFromFilename(filename) {
|
|
34
|
+
const extension = filename.split(".").pop()?.toLowerCase();
|
|
35
|
+
if (extension && FILE_TYPE_TO_MIME_TYPE[extension]) {
|
|
36
|
+
return FILE_TYPE_TO_MIME_TYPE[extension];
|
|
37
|
+
}
|
|
38
|
+
return "text/plain"; // Default fallback
|
|
39
|
+
}
|
|
7
40
|
/**
|
|
8
41
|
* Outbound adapter for sending messages from OpenClaw to XY.
|
|
9
42
|
* Uses Push service for direct message delivery.
|
|
@@ -76,8 +109,10 @@ export const xyOutbound = {
|
|
|
76
109
|
const pushService = new XYPushService(config);
|
|
77
110
|
// Extract title (first 57 chars or first line)
|
|
78
111
|
const title = text.split("\n")[0].slice(0, 57);
|
|
79
|
-
//
|
|
80
|
-
|
|
112
|
+
// Truncate push content to max length 1000
|
|
113
|
+
const pushText = text.length > 1000 ? text.slice(0, 1000) : text;
|
|
114
|
+
// Send push message (content, title, data, sessionId)
|
|
115
|
+
await pushService.sendPush(pushText, title, undefined, actualTo);
|
|
81
116
|
console.log(`[xyOutbound.sendText] Completed successfully`);
|
|
82
117
|
// Return message info
|
|
83
118
|
return {
|
|
@@ -111,6 +146,15 @@ export const xyOutbound = {
|
|
|
111
146
|
}
|
|
112
147
|
// Upload file
|
|
113
148
|
const fileId = await uploadService.uploadFile(mediaUrl);
|
|
149
|
+
// Check if fileId is empty
|
|
150
|
+
if (!fileId) {
|
|
151
|
+
console.log(`[xyOutbound.sendMedia] β οΈ File upload failed: fileId is empty, aborting sendMedia`);
|
|
152
|
+
return {
|
|
153
|
+
channel: "xiaoyi-channel",
|
|
154
|
+
messageId: "",
|
|
155
|
+
chatId: to,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
114
158
|
console.log(`[xyOutbound.sendMedia] File uploaded:`, {
|
|
115
159
|
fileId,
|
|
116
160
|
sessionId,
|
|
@@ -119,7 +163,7 @@ export const xyOutbound = {
|
|
|
119
163
|
// Get filename and mime type from mediaUrl
|
|
120
164
|
// mediaUrl may be a local file path or URL
|
|
121
165
|
const fileName = mediaUrl.split("/").pop() || "unknown";
|
|
122
|
-
const mimeType =
|
|
166
|
+
const mimeType = getMimeTypeFromFilename(fileName);
|
|
123
167
|
// Build agent_response message
|
|
124
168
|
const agentResponse = {
|
|
125
169
|
msgType: "agent_response",
|
package/dist/src/parser.d.ts
CHANGED
|
@@ -38,6 +38,11 @@ export declare function isClearContextMessage(method: string): boolean;
|
|
|
38
38
|
* Check if message is a tasks/cancel request.
|
|
39
39
|
*/
|
|
40
40
|
export declare function isTasksCancelMessage(method: string): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Extract push_id from message parts.
|
|
43
|
+
* Looks for push_id in data parts under variables.systemVariables.push_id
|
|
44
|
+
*/
|
|
45
|
+
export declare function extractPushId(parts: A2AMessagePart[]): string | null;
|
|
41
46
|
/**
|
|
42
47
|
* Validate A2A request structure.
|
|
43
48
|
*/
|
package/dist/src/parser.js
CHANGED
|
@@ -57,6 +57,21 @@ export function isClearContextMessage(method) {
|
|
|
57
57
|
export function isTasksCancelMessage(method) {
|
|
58
58
|
return method === "tasks/cancel" || method === "tasks_cancel";
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Extract push_id from message parts.
|
|
62
|
+
* Looks for push_id in data parts under variables.systemVariables.push_id
|
|
63
|
+
*/
|
|
64
|
+
export function extractPushId(parts) {
|
|
65
|
+
for (const part of parts) {
|
|
66
|
+
if (part.kind === "data" && part.data) {
|
|
67
|
+
const pushId = part.data.variables?.systemVariables?.push_id;
|
|
68
|
+
if (pushId && typeof pushId === "string") {
|
|
69
|
+
return pushId;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
60
75
|
/**
|
|
61
76
|
* Validate A2A request structure.
|
|
62
77
|
*/
|