@ynhcj/xiaoyi-channel 0.0.2 → 0.0.3-next
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 +115 -35
- package/dist/src/channel.js +22 -1
- package/dist/src/client.d.ts +15 -0
- package/dist/src/client.js +97 -2
- package/dist/src/file-download.js +10 -1
- package/dist/src/file-upload.js +1 -1
- package/dist/src/formatter.d.ts +17 -0
- package/dist/src/formatter.js +86 -6
- package/dist/src/heartbeat.d.ts +2 -1
- package/dist/src/heartbeat.js +9 -1
- package/dist/src/monitor.d.ts +5 -0
- package/dist/src/monitor.js +131 -25
- package/dist/src/onboarding.js +7 -7
- package/dist/src/outbound.js +150 -71
- 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.d.ts +1 -0
- package/dist/src/reply-dispatcher.js +210 -57
- package/dist/src/task-manager.d.ts +55 -0
- package/dist/src/task-manager.js +136 -0
- package/dist/src/tools/calendar-tool.d.ts +6 -0
- package/dist/src/tools/calendar-tool.js +167 -0
- package/dist/src/tools/call-phone-tool.d.ts +5 -0
- package/dist/src/tools/call-phone-tool.js +183 -0
- package/dist/src/tools/create-alarm-tool.d.ts +7 -0
- package/dist/src/tools/create-alarm-tool.js +444 -0
- package/dist/src/tools/delete-alarm-tool.d.ts +11 -0
- package/dist/src/tools/delete-alarm-tool.js +238 -0
- package/dist/src/tools/location-tool.js +48 -9
- package/dist/src/tools/modify-alarm-tool.d.ts +9 -0
- package/dist/src/tools/modify-alarm-tool.js +474 -0
- 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.d.ts +5 -0
- package/dist/src/tools/note-tool.js +146 -0
- package/dist/src/tools/search-alarm-tool.d.ts +8 -0
- package/dist/src/tools/search-alarm-tool.js +389 -0
- package/dist/src/tools/search-calendar-tool.d.ts +12 -0
- package/dist/src/tools/search-calendar-tool.js +259 -0
- package/dist/src/tools/search-contact-tool.d.ts +5 -0
- package/dist/src/tools/search-contact-tool.js +168 -0
- package/dist/src/tools/search-file-tool.d.ts +5 -0
- package/dist/src/tools/search-file-tool.js +185 -0
- package/dist/src/tools/search-message-tool.d.ts +5 -0
- package/dist/src/tools/search-message-tool.js +173 -0
- package/dist/src/tools/search-note-tool.d.ts +5 -0
- package/dist/src/tools/search-note-tool.js +130 -0
- package/dist/src/tools/search-photo-gallery-tool.d.ts +8 -0
- package/dist/src/tools/search-photo-gallery-tool.js +184 -0
- package/dist/src/tools/search-photo-tool.d.ts +9 -0
- package/dist/src/tools/search-photo-tool.js +270 -0
- package/dist/src/tools/send-file-to-user-tool.d.ts +5 -0
- package/dist/src/tools/send-file-to-user-tool.js +318 -0
- package/dist/src/tools/send-message-tool.d.ts +5 -0
- package/dist/src/tools/send-message-tool.js +189 -0
- package/dist/src/tools/session-manager.d.ts +15 -0
- package/dist/src/tools/session-manager.js +126 -6
- package/dist/src/tools/upload-file-tool.d.ts +13 -0
- package/dist/src/tools/upload-file-tool.js +265 -0
- package/dist/src/tools/upload-photo-tool.d.ts +9 -0
- package/dist/src/tools/upload-photo-tool.js +223 -0
- package/dist/src/tools/xiaoyi-gui-tool.d.ts +6 -0
- package/dist/src/tools/xiaoyi-gui-tool.js +151 -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 +41 -0
- package/dist/src/websocket.js +192 -9
- package/package.json +1 -2
package/dist/src/bot.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
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
|
-
import { registerSession, unregisterSession } from "./tools/session-manager.js";
|
|
7
|
+
import { registerSession, unregisterSession, runWithSessionContext } from "./tools/session-manager.js";
|
|
8
|
+
import { configManager } from "./utils/config-manager.js";
|
|
9
|
+
import { registerTaskId, decrementTaskIdRef, lockTaskId, unlockTaskId, hasActiveTask, } from "./task-manager.js";
|
|
8
10
|
/**
|
|
9
11
|
* Handle an incoming A2A message.
|
|
10
12
|
* This is the main entry point for message processing.
|
|
@@ -19,7 +21,8 @@ export async function handleXYMessage(params) {
|
|
|
19
21
|
try {
|
|
20
22
|
// Check for special messages BEFORE parsing (these have different param structures)
|
|
21
23
|
const messageMethod = message.method;
|
|
22
|
-
log(`[
|
|
24
|
+
log(`[BOT-ENTRY] <<<<<<< Received message with method: ${messageMethod}, id: ${message.id} >>>>>>>`);
|
|
25
|
+
log(`[BOT-ENTRY] Stack trace for debugging:`, new Error().stack?.split('\n').slice(1, 4).join('\n'));
|
|
23
26
|
// Handle clearContext messages (params only has sessionId)
|
|
24
27
|
if (messageMethod === "clearContext" || messageMethod === "clear_context") {
|
|
25
28
|
const sessionId = message.params?.sessionId;
|
|
@@ -54,6 +57,34 @@ export async function handleXYMessage(params) {
|
|
|
54
57
|
}
|
|
55
58
|
// Parse the A2A message (for regular messages)
|
|
56
59
|
const parsed = parseA2AMessage(message);
|
|
60
|
+
// 🔑 检测steer模式和是否是第二条消息
|
|
61
|
+
const isSteerMode = cfg.messages?.queue?.mode === "steer";
|
|
62
|
+
const isSecondMessage = isSteerMode && hasActiveTask(parsed.sessionId);
|
|
63
|
+
if (isSecondMessage) {
|
|
64
|
+
log(`[BOT] 🔄 STEER MODE - Second message detected (will be follower)`);
|
|
65
|
+
log(`[BOT] - Session: ${parsed.sessionId}`);
|
|
66
|
+
log(`[BOT] - New taskId: ${parsed.taskId} (will replace current)`);
|
|
67
|
+
}
|
|
68
|
+
// 🔑 注册taskId(第二条消息会覆盖第一条的taskId)
|
|
69
|
+
const { isUpdate, refCount } = registerTaskId(parsed.sessionId, parsed.taskId, parsed.messageId, { incrementRef: true } // 增加引用计数
|
|
70
|
+
);
|
|
71
|
+
// 🔑 如果是第一条消息,锁定taskId防止被过早清理
|
|
72
|
+
if (!isUpdate) {
|
|
73
|
+
lockTaskId(parsed.sessionId);
|
|
74
|
+
log(`[BOT] 🔒 Locked taskId for first message`);
|
|
75
|
+
}
|
|
76
|
+
// Extract and update push_id if present
|
|
77
|
+
const pushId = extractPushId(parsed.parts);
|
|
78
|
+
if (pushId) {
|
|
79
|
+
log(`[BOT] 📌 Extracted push_id from user message`);
|
|
80
|
+
log(`[BOT] - Session ID: ${parsed.sessionId}`);
|
|
81
|
+
log(`[BOT] - Push ID preview: ${pushId.substring(0, 20)}...`);
|
|
82
|
+
log(`[BOT] - Full push_id: ${pushId}`);
|
|
83
|
+
configManager.updatePushId(parsed.sessionId, pushId);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
log(`[BOT] ℹ️ No push_id found in message, will use config default`);
|
|
87
|
+
}
|
|
57
88
|
// Resolve configuration (needed for status updates)
|
|
58
89
|
const config = resolveXYConfig(cfg);
|
|
59
90
|
// ✅ Resolve agent route (following feishu pattern)
|
|
@@ -61,7 +92,7 @@ export async function handleXYMessage(params) {
|
|
|
61
92
|
// Use sessionId as peer.id to ensure all messages in the same session share context
|
|
62
93
|
let route = core.channel.routing.resolveAgentRoute({
|
|
63
94
|
cfg,
|
|
64
|
-
channel: "
|
|
95
|
+
channel: "xiaoyi-channel",
|
|
65
96
|
accountId, // "default"
|
|
66
97
|
peer: {
|
|
67
98
|
kind: "direct",
|
|
@@ -69,7 +100,12 @@ export async function handleXYMessage(params) {
|
|
|
69
100
|
},
|
|
70
101
|
});
|
|
71
102
|
log(`xy: resolved route accountId=${route.accountId}, sessionKey=${route.sessionKey}`);
|
|
72
|
-
//
|
|
103
|
+
// 🔑 注册session(带引用计数)
|
|
104
|
+
log(`[BOT] 📝 About to register session for tools...`);
|
|
105
|
+
log(`[BOT] - sessionKey: ${route.sessionKey}`);
|
|
106
|
+
log(`[BOT] - sessionId: ${parsed.sessionId}`);
|
|
107
|
+
log(`[BOT] - taskId: ${parsed.taskId}`);
|
|
108
|
+
log(`[BOT] - isSecondMessage: ${isSecondMessage}`);
|
|
73
109
|
registerSession(route.sessionKey, {
|
|
74
110
|
config,
|
|
75
111
|
sessionId: parsed.sessionId,
|
|
@@ -77,6 +113,19 @@ export async function handleXYMessage(params) {
|
|
|
77
113
|
messageId: parsed.messageId,
|
|
78
114
|
agentId: route.accountId,
|
|
79
115
|
});
|
|
116
|
+
log(`[BOT] ✅ Session registered for tools`);
|
|
117
|
+
// 🔑 发送初始状态更新(第二条消息也要发,用新taskId)
|
|
118
|
+
log(`[STATUS] Sending initial status update for session ${parsed.sessionId}`);
|
|
119
|
+
void sendStatusUpdate({
|
|
120
|
+
config,
|
|
121
|
+
sessionId: parsed.sessionId,
|
|
122
|
+
taskId: parsed.taskId,
|
|
123
|
+
messageId: parsed.messageId,
|
|
124
|
+
text: isSecondMessage ? "新消息已接收,正在处理..." : "任务正在处理中,请稍后~",
|
|
125
|
+
state: "working",
|
|
126
|
+
}).catch((err) => {
|
|
127
|
+
error(`Failed to send initial status update:`, err);
|
|
128
|
+
});
|
|
80
129
|
// Extract text and files from parts
|
|
81
130
|
const text = extractTextFromParts(parsed.parts);
|
|
82
131
|
const fileParts = extractFileParts(parsed.parts);
|
|
@@ -93,7 +142,7 @@ export async function handleXYMessage(params) {
|
|
|
93
142
|
messageBody = `${speaker}: ${messageBody}`;
|
|
94
143
|
// Format agent envelope (following feishu pattern)
|
|
95
144
|
const body = core.channel.reply.formatAgentEnvelope({
|
|
96
|
-
channel: "
|
|
145
|
+
channel: "xiaoyi-channel",
|
|
97
146
|
from: speaker,
|
|
98
147
|
timestamp: new Date(),
|
|
99
148
|
envelope: envelopeOptions,
|
|
@@ -113,84 +162,115 @@ export async function handleXYMessage(params) {
|
|
|
113
162
|
GroupSubject: undefined,
|
|
114
163
|
SenderName: parsed.sessionId,
|
|
115
164
|
SenderId: parsed.sessionId,
|
|
116
|
-
Provider: "
|
|
117
|
-
Surface: "
|
|
165
|
+
Provider: "xiaoyi-channel",
|
|
166
|
+
Surface: "xiaoyi-channel",
|
|
118
167
|
MessageSid: parsed.messageId,
|
|
119
168
|
Timestamp: Date.now(),
|
|
120
169
|
WasMentioned: false,
|
|
121
170
|
CommandAuthorized: true,
|
|
122
|
-
OriginatingChannel: "
|
|
171
|
+
OriginatingChannel: "xiaoyi-channel",
|
|
123
172
|
OriginatingTo: parsed.sessionId, // Original message target
|
|
124
173
|
ReplyToBody: undefined, // A2A protocol doesn't support reply/quote
|
|
125
174
|
...mediaPayload,
|
|
126
175
|
});
|
|
127
|
-
//
|
|
128
|
-
log(`[
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
taskId: parsed.taskId,
|
|
133
|
-
messageId: parsed.messageId,
|
|
134
|
-
text: "任务正在处理中,请稍后~",
|
|
135
|
-
state: "working",
|
|
136
|
-
}).catch((err) => {
|
|
137
|
-
error(`Failed to send initial status update:`, err);
|
|
138
|
-
});
|
|
139
|
-
// Create reply dispatcher (following feishu pattern)
|
|
176
|
+
// 🔑 创建dispatcher(dispatcher会自动使用动态taskId)
|
|
177
|
+
log(`[BOT-DISPATCHER] 🎯 Creating reply dispatcher`);
|
|
178
|
+
log(`[BOT-DISPATCHER] - session: ${parsed.sessionId}`);
|
|
179
|
+
log(`[BOT-DISPATCHER] - taskId: ${parsed.taskId}`);
|
|
180
|
+
log(`[BOT-DISPATCHER] - isSecondMessage: ${isSecondMessage}`);
|
|
140
181
|
const { dispatcher, replyOptions, markDispatchIdle, startStatusInterval } = createXYReplyDispatcher({
|
|
141
182
|
cfg,
|
|
142
183
|
runtime,
|
|
143
184
|
sessionId: parsed.sessionId,
|
|
144
185
|
taskId: parsed.taskId,
|
|
145
186
|
messageId: parsed.messageId,
|
|
146
|
-
accountId: route.accountId,
|
|
187
|
+
accountId: route.accountId,
|
|
188
|
+
isSteerFollower: isSecondMessage, // 🔑 标记第二条消息
|
|
147
189
|
});
|
|
148
|
-
|
|
149
|
-
//
|
|
150
|
-
|
|
190
|
+
log(`[BOT-DISPATCHER] ✅ Reply dispatcher created successfully`);
|
|
191
|
+
// 🔑 只有第一条消息启动状态定时器
|
|
192
|
+
// 第二条消息会很快返回,不需要定时器
|
|
193
|
+
if (!isSecondMessage) {
|
|
194
|
+
startStatusInterval();
|
|
195
|
+
log(`[BOT-DISPATCHER] ✅ Status interval started for first message`);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
log(`[BOT-DISPATCHER] ⏭️ Skipped status interval for steer follower`);
|
|
199
|
+
}
|
|
151
200
|
log(`xy: dispatching to agent (session=${parsed.sessionId})`);
|
|
152
201
|
// Dispatch to OpenClaw core using correct API (following feishu pattern)
|
|
202
|
+
log(`[BOT] 🚀 Starting dispatcher with session: ${route.sessionKey}`);
|
|
203
|
+
// Build session context for AsyncLocalStorage
|
|
204
|
+
const sessionContext = {
|
|
205
|
+
config,
|
|
206
|
+
sessionId: parsed.sessionId,
|
|
207
|
+
taskId: parsed.taskId,
|
|
208
|
+
messageId: parsed.messageId,
|
|
209
|
+
agentId: route.accountId,
|
|
210
|
+
};
|
|
153
211
|
await core.channel.reply.withReplyDispatcher({
|
|
154
212
|
dispatcher,
|
|
155
213
|
onSettled: () => {
|
|
214
|
+
log(`[BOT] 🏁 onSettled called for session: ${route.sessionKey}`);
|
|
215
|
+
log(`[BOT] - isSecondMessage: ${isSecondMessage}`);
|
|
156
216
|
markDispatchIdle();
|
|
157
|
-
//
|
|
217
|
+
// 🔑 减少引用计数
|
|
218
|
+
decrementTaskIdRef(parsed.sessionId);
|
|
219
|
+
// 🔑 如果是第一条消息完成,解锁
|
|
220
|
+
if (!isSecondMessage) {
|
|
221
|
+
unlockTaskId(parsed.sessionId);
|
|
222
|
+
log(`[BOT] 🔓 Unlocked taskId (first message completed)`);
|
|
223
|
+
}
|
|
224
|
+
// 减少session引用计数
|
|
158
225
|
unregisterSession(route.sessionKey);
|
|
226
|
+
log(`[BOT] ✅ Cleanup completed`);
|
|
159
227
|
},
|
|
160
|
-
run: () =>
|
|
228
|
+
run: () =>
|
|
229
|
+
// 🔐 Use AsyncLocalStorage to provide session context to tools
|
|
230
|
+
runWithSessionContext(sessionContext, () => core.channel.reply.dispatchReplyFromConfig({
|
|
161
231
|
ctx: ctxPayload,
|
|
162
232
|
cfg,
|
|
163
233
|
dispatcher,
|
|
164
234
|
replyOptions,
|
|
165
|
-
}),
|
|
235
|
+
})),
|
|
166
236
|
});
|
|
237
|
+
log(`[BOT] ✅ Dispatcher completed for session: ${parsed.sessionId}`);
|
|
167
238
|
log(`xy: dispatch complete (session=${parsed.sessionId})`);
|
|
168
239
|
}
|
|
169
240
|
catch (err) {
|
|
241
|
+
// ✅ Only log error, don't re-throw to prevent gateway restart
|
|
170
242
|
error("Failed to handle XY message:", err);
|
|
171
243
|
runtime.error?.(`xy: Failed to handle message: ${String(err)}`);
|
|
172
|
-
|
|
244
|
+
log(`[BOT] ❌ Error occurred, attempting cleanup...`);
|
|
245
|
+
// 🔑 错误时也要清理taskId和session
|
|
173
246
|
try {
|
|
174
|
-
const core = getXYRuntime();
|
|
175
247
|
const params = message.params;
|
|
176
248
|
const sessionId = params?.sessionId;
|
|
177
249
|
if (sessionId) {
|
|
250
|
+
log(`[BOT] 🧹 Cleaning up after error: ${sessionId}`);
|
|
251
|
+
// 清理 taskId
|
|
252
|
+
decrementTaskIdRef(sessionId);
|
|
253
|
+
unlockTaskId(sessionId);
|
|
254
|
+
// 清理 session
|
|
255
|
+
const core = getXYRuntime();
|
|
178
256
|
const route = core.channel.routing.resolveAgentRoute({
|
|
179
257
|
cfg,
|
|
180
|
-
channel: "
|
|
258
|
+
channel: "xiaoyi-channel",
|
|
181
259
|
accountId,
|
|
182
260
|
peer: {
|
|
183
261
|
kind: "direct",
|
|
184
|
-
id: sessionId,
|
|
262
|
+
id: sessionId,
|
|
185
263
|
},
|
|
186
264
|
});
|
|
187
265
|
unregisterSession(route.sessionKey);
|
|
266
|
+
log(`[BOT] ✅ Cleanup completed after error`);
|
|
188
267
|
}
|
|
189
268
|
}
|
|
190
|
-
catch {
|
|
269
|
+
catch (cleanupErr) {
|
|
270
|
+
log(`[BOT] ⚠️ Cleanup failed:`, cleanupErr);
|
|
191
271
|
// Ignore cleanup errors
|
|
192
272
|
}
|
|
193
|
-
throw
|
|
273
|
+
// ❌ Don't re-throw: message processing error should not affect gateway stability
|
|
194
274
|
}
|
|
195
275
|
}
|
|
196
276
|
/**
|
package/dist/src/channel.js
CHANGED
|
@@ -3,6 +3,25 @@ import { xyConfigSchema } from "./config-schema.js";
|
|
|
3
3
|
import { xyOutbound } from "./outbound.js";
|
|
4
4
|
import { xyOnboardingAdapter } from "./onboarding.js";
|
|
5
5
|
import { locationTool } from "./tools/location-tool.js";
|
|
6
|
+
import { noteTool } from "./tools/note-tool.js";
|
|
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"; // 暂时禁用
|
|
12
|
+
import { searchPhotoGalleryTool } from "./tools/search-photo-gallery-tool.js";
|
|
13
|
+
import { uploadPhotoTool } from "./tools/upload-photo-tool.js";
|
|
14
|
+
import { xiaoyiGuiTool } from "./tools/xiaoyi-gui-tool.js";
|
|
15
|
+
import { callPhoneTool } from "./tools/call-phone-tool.js";
|
|
16
|
+
import { searchMessageTool } from "./tools/search-message-tool.js";
|
|
17
|
+
import { searchFileTool } from "./tools/search-file-tool.js";
|
|
18
|
+
import { uploadFileTool } from "./tools/upload-file-tool.js";
|
|
19
|
+
import { createAlarmTool } from "./tools/create-alarm-tool.js";
|
|
20
|
+
import { searchAlarmTool } from "./tools/search-alarm-tool.js";
|
|
21
|
+
import { modifyAlarmTool } from "./tools/modify-alarm-tool.js";
|
|
22
|
+
import { deleteAlarmTool } from "./tools/delete-alarm-tool.js";
|
|
23
|
+
import { sendMessageTool } from "./tools/send-message-tool.js";
|
|
24
|
+
import { sendFileToUserTool } from "./tools/send-file-to-user-tool.js";
|
|
6
25
|
/**
|
|
7
26
|
* Xiaoyi Channel Plugin for OpenClaw.
|
|
8
27
|
* Implements Xiaoyi A2A protocol with dual WebSocket connections.
|
|
@@ -20,6 +39,7 @@ export const xyPlugin = {
|
|
|
20
39
|
agentPrompt: {
|
|
21
40
|
messageToolHints: () => [
|
|
22
41
|
"- xiaoyi targeting: omit `target` to reply to the current conversation (auto-inferred). Explicit targets: `default`",
|
|
42
|
+
"- 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."
|
|
23
43
|
],
|
|
24
44
|
},
|
|
25
45
|
capabilities: {
|
|
@@ -41,7 +61,7 @@ export const xyPlugin = {
|
|
|
41
61
|
},
|
|
42
62
|
outbound: xyOutbound,
|
|
43
63
|
onboarding: xyOnboardingAdapter,
|
|
44
|
-
agentTools: [locationTool],
|
|
64
|
+
agentTools: [locationTool, noteTool, searchNoteTool, modifyNoteTool, calendarTool, searchCalendarTool, searchPhotoGalleryTool, uploadPhotoTool, xiaoyiGuiTool, callPhoneTool, searchMessageTool, searchFileTool, uploadFileTool, createAlarmTool, searchAlarmTool, modifyAlarmTool, deleteAlarmTool, sendMessageTool, sendFileToUserTool], // searchContactTool 已暂时禁用
|
|
45
65
|
messaging: {
|
|
46
66
|
normalizeTarget: (raw) => {
|
|
47
67
|
const trimmed = raw.trim();
|
|
@@ -77,6 +97,7 @@ export const xyPlugin = {
|
|
|
77
97
|
runtime: context.runtime,
|
|
78
98
|
abortSignal: context.abortSignal,
|
|
79
99
|
accountId: context.accountId,
|
|
100
|
+
setStatus: context.setStatus,
|
|
80
101
|
});
|
|
81
102
|
},
|
|
82
103
|
},
|
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
|
@@ -23,16 +23,34 @@ export function getXYWebSocketManager(config) {
|
|
|
23
23
|
let cached = wsManagerCache.get(cacheKey);
|
|
24
24
|
if (cached && cached.isConfigMatch(config)) {
|
|
25
25
|
const log = runtime?.log ?? console.log;
|
|
26
|
-
log(`[
|
|
26
|
+
log(`[WS-MANAGER-CACHE] ✅ Reusing cached WebSocket manager: ${cacheKey}, total managers: ${wsManagerCache.size}`);
|
|
27
27
|
return cached;
|
|
28
28
|
}
|
|
29
29
|
// Create new manager
|
|
30
30
|
const log = runtime?.log ?? console.log;
|
|
31
|
-
log(`Creating new WebSocket manager: ${cacheKey}`);
|
|
31
|
+
log(`[WS-MANAGER-CACHE] 🆕 Creating new WebSocket manager: ${cacheKey}, total managers before: ${wsManagerCache.size}`);
|
|
32
32
|
cached = new XYWebSocketManager(config, runtime);
|
|
33
33
|
wsManagerCache.set(cacheKey, cached);
|
|
34
|
+
log(`[WS-MANAGER-CACHE] 📊 Total managers after creation: ${wsManagerCache.size}`);
|
|
34
35
|
return cached;
|
|
35
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
|
+
}
|
|
36
54
|
/**
|
|
37
55
|
* Clear all cached WebSocket managers.
|
|
38
56
|
*/
|
|
@@ -50,3 +68,80 @@ export function clearXYWebSocketManagers() {
|
|
|
50
68
|
export function getCachedManagerCount() {
|
|
51
69
|
return wsManagerCache.size;
|
|
52
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/file-upload.js
CHANGED
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
|
@@ -51,8 +51,64 @@ export async function sendA2AResponse(params) {
|
|
|
51
51
|
taskId,
|
|
52
52
|
msgDetail: JSON.stringify(jsonRpcResponse),
|
|
53
53
|
};
|
|
54
|
+
// 📋 Log complete response body
|
|
55
|
+
log(`[A2A_RESPONSE] 📤 Sending A2A artifact-update response:`);
|
|
56
|
+
log(`[A2A_RESPONSE] - sessionId: ${sessionId}`);
|
|
57
|
+
log(`[A2A_RESPONSE] - taskId: ${taskId}`);
|
|
58
|
+
log(`[A2A_RESPONSE] - messageId: ${messageId}`);
|
|
59
|
+
log(`[A2A_RESPONSE] - append: ${append}`);
|
|
60
|
+
log(`[A2A_RESPONSE] - final: ${final}`);
|
|
61
|
+
log(`[A2A_RESPONSE] - text length: ${text?.length ?? 0}`);
|
|
62
|
+
log(`[A2A_RESPONSE] - files count: ${files?.length ?? 0}`);
|
|
63
|
+
log(`[A2A_RESPONSE] 📦 Complete outbound message:`);
|
|
64
|
+
log(JSON.stringify(outboundMessage, null, 2));
|
|
65
|
+
log(`[A2A_RESPONSE] 📦 JSON-RPC response body:`);
|
|
66
|
+
log(JSON.stringify(jsonRpcResponse, null, 2));
|
|
54
67
|
await wsManager.sendMessage(sessionId, outboundMessage);
|
|
55
|
-
log(`
|
|
68
|
+
log(`[A2A_RESPONSE] ✅ Message sent successfully`);
|
|
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`);
|
|
56
112
|
}
|
|
57
113
|
/**
|
|
58
114
|
* Send an A2A task status update.
|
|
@@ -96,8 +152,19 @@ export async function sendStatusUpdate(params) {
|
|
|
96
152
|
taskId,
|
|
97
153
|
msgDetail: JSON.stringify(jsonRpcResponse),
|
|
98
154
|
};
|
|
155
|
+
// 📋 Log complete response body
|
|
156
|
+
log(`[A2A_STATUS] 📤 Sending A2A status-update:`);
|
|
157
|
+
log(`[A2A_STATUS] - sessionId: ${sessionId}`);
|
|
158
|
+
log(`[A2A_STATUS] - taskId: ${taskId}`);
|
|
159
|
+
log(`[A2A_STATUS] - messageId: ${messageId}`);
|
|
160
|
+
log(`[A2A_STATUS] - state: ${state}`);
|
|
161
|
+
log(`[A2A_STATUS] - text: "${text}"`);
|
|
162
|
+
log(`[A2A_STATUS] 📦 Complete outbound message:`);
|
|
163
|
+
log(JSON.stringify(outboundMessage, null, 2));
|
|
164
|
+
log(`[A2A_STATUS] 📦 JSON-RPC response body:`);
|
|
165
|
+
log(JSON.stringify(jsonRpcResponse, null, 2));
|
|
99
166
|
await wsManager.sendMessage(sessionId, outboundMessage);
|
|
100
|
-
log(`
|
|
167
|
+
log(`[A2A_STATUS] ✅ Status update sent successfully`);
|
|
101
168
|
}
|
|
102
169
|
/**
|
|
103
170
|
* Send a command as an artifact update (final=false).
|
|
@@ -107,7 +174,8 @@ export async function sendCommand(params) {
|
|
|
107
174
|
const runtime = getXYRuntime();
|
|
108
175
|
const log = runtime?.log ?? console.log;
|
|
109
176
|
const error = runtime?.error ?? console.error;
|
|
110
|
-
// Build artifact update with command
|
|
177
|
+
// Build artifact update with command as data
|
|
178
|
+
// Wrap command in commands array as per protocol requirement
|
|
111
179
|
const artifact = {
|
|
112
180
|
taskId,
|
|
113
181
|
kind: "artifact-update",
|
|
@@ -118,8 +186,10 @@ export async function sendCommand(params) {
|
|
|
118
186
|
artifactId: uuidv4(),
|
|
119
187
|
parts: [
|
|
120
188
|
{
|
|
121
|
-
kind: "
|
|
122
|
-
|
|
189
|
+
kind: "data",
|
|
190
|
+
data: {
|
|
191
|
+
commands: [command],
|
|
192
|
+
},
|
|
123
193
|
},
|
|
124
194
|
],
|
|
125
195
|
},
|
|
@@ -139,8 +209,18 @@ export async function sendCommand(params) {
|
|
|
139
209
|
taskId,
|
|
140
210
|
msgDetail: JSON.stringify(jsonRpcResponse),
|
|
141
211
|
};
|
|
212
|
+
// 📋 Log complete response body
|
|
213
|
+
log(`[A2A_COMMAND] 📤 Sending A2A command:`);
|
|
214
|
+
log(`[A2A_COMMAND] - sessionId: ${sessionId}`);
|
|
215
|
+
log(`[A2A_COMMAND] - taskId: ${taskId}`);
|
|
216
|
+
log(`[A2A_COMMAND] - messageId: ${messageId}`);
|
|
217
|
+
log(`[A2A_COMMAND] - command: ${command.header.namespace}::${command.header.name}`);
|
|
218
|
+
log(`[A2A_COMMAND] 📦 Complete outbound message:`);
|
|
219
|
+
log(JSON.stringify(outboundMessage, null, 2));
|
|
220
|
+
log(`[A2A_COMMAND] 📦 JSON-RPC response body:`);
|
|
221
|
+
log(JSON.stringify(jsonRpcResponse, null, 2));
|
|
142
222
|
await wsManager.sendMessage(sessionId, outboundMessage);
|
|
143
|
-
log(`
|
|
223
|
+
log(`[A2A_COMMAND] ✅ Command sent successfully`);
|
|
144
224
|
}
|
|
145
225
|
/**
|
|
146
226
|
* Send a clearContext response.
|
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
|
*/
|