@ynhcj/xiaoyi 2.5.2 → 2.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/channel.d.ts +2 -1
- package/dist/channel.js +363 -250
- package/dist/config-schema.d.ts +18 -18
- package/dist/onboarding.d.ts +6 -0
- package/dist/onboarding.js +167 -0
- package/dist/runtime.d.ts +17 -5
- package/dist/runtime.js +45 -22
- package/dist/session-manager.d.ts +78 -0
- package/dist/session-manager.js +258 -0
- package/dist/websocket.d.ts +16 -0
- package/dist/websocket.js +21 -0
- package/dist/xiaoyi-media.d.ts +81 -0
- package/dist/xiaoyi-media.js +216 -0
- package/package.json +62 -62
package/dist/channel.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.xiaoyiPlugin = void 0;
|
|
4
4
|
const runtime_1 = require("./runtime");
|
|
5
|
-
const
|
|
5
|
+
const onboarding_1 = require("./onboarding");
|
|
6
|
+
const xiaoyi_media_1 = require("./xiaoyi-media");
|
|
6
7
|
/**
|
|
7
8
|
* XiaoYi Channel Plugin
|
|
8
9
|
* Implements OpenClaw ChannelPlugin interface for XiaoYi A2A protocol
|
|
@@ -26,6 +27,7 @@ exports.xiaoyiPlugin = {
|
|
|
26
27
|
media: true,
|
|
27
28
|
nativeCommands: false,
|
|
28
29
|
},
|
|
30
|
+
onboarding: onboarding_1.xiaoyiOnboardingAdapter,
|
|
29
31
|
/**
|
|
30
32
|
* Config adapter - single account mode
|
|
31
33
|
*/
|
|
@@ -119,8 +121,11 @@ exports.xiaoyiPlugin = {
|
|
|
119
121
|
const agentId = resolvedAccount.config.agentId;
|
|
120
122
|
// Use 'to' as sessionId (it's set from incoming message's sessionId)
|
|
121
123
|
const sessionId = ctx.to;
|
|
122
|
-
// Get taskId from runtime's session mapping
|
|
123
|
-
const taskId = runtime.getTaskIdForSession(sessionId)
|
|
124
|
+
// Get taskId from runtime's session mapping (must exist - from original A2A request)
|
|
125
|
+
const taskId = runtime.getTaskIdForSession(sessionId);
|
|
126
|
+
if (!taskId) {
|
|
127
|
+
throw new Error(`Cannot send outbound message: No taskId found for session ${sessionId}. Outbound messages must be in response to an incoming A2A request.`);
|
|
128
|
+
}
|
|
124
129
|
// Build A2A response message
|
|
125
130
|
const response = {
|
|
126
131
|
sessionId: sessionId,
|
|
@@ -160,8 +165,11 @@ exports.xiaoyiPlugin = {
|
|
|
160
165
|
const agentId = resolvedAccount.config.agentId;
|
|
161
166
|
// Use 'to' as sessionId
|
|
162
167
|
const sessionId = ctx.to;
|
|
163
|
-
// Get taskId from runtime's session mapping
|
|
164
|
-
const taskId = runtime.getTaskIdForSession(sessionId)
|
|
168
|
+
// Get taskId from runtime's session mapping (must exist - from original A2A request)
|
|
169
|
+
const taskId = runtime.getTaskIdForSession(sessionId);
|
|
170
|
+
if (!taskId) {
|
|
171
|
+
throw new Error(`Cannot send outbound media: No taskId found for session ${sessionId}. Outbound messages must be in response to an incoming A2A request.`);
|
|
172
|
+
}
|
|
165
173
|
// Build A2A response message with media
|
|
166
174
|
const response = {
|
|
167
175
|
sessionId: sessionId,
|
|
@@ -214,6 +222,16 @@ exports.xiaoyiPlugin = {
|
|
|
214
222
|
const { getXiaoYiRuntime } = require("./runtime");
|
|
215
223
|
const runtime = getXiaoYiRuntime();
|
|
216
224
|
console.log(`XiaoYi: [Message Handler] Using runtime instance: ${runtime.getInstanceId()}`);
|
|
225
|
+
// CRITICAL FIX: Extract and store config values at message handler level
|
|
226
|
+
// This prevents "Cannot read properties of undefined" errors in concurrent scenarios
|
|
227
|
+
// where the outer scope's resolvedAccount might become unavailable
|
|
228
|
+
const messageHandlerAgentId = resolvedAccount.config?.agentId;
|
|
229
|
+
const messageHandlerAccountId = resolvedAccount.accountId;
|
|
230
|
+
if (!messageHandlerAgentId) {
|
|
231
|
+
console.error("XiaoYi: [FATAL] agentId not available in resolvedAccount.config");
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
console.log(`XiaoYi: [Message Handler] Stored config values - agentId: ${messageHandlerAgentId}, accountId: ${messageHandlerAccountId}`);
|
|
217
235
|
// For message/stream, prioritize params.sessionId, fallback to top-level sessionId
|
|
218
236
|
const sessionId = message.params?.sessionId || message.sessionId;
|
|
219
237
|
// Validate sessionId exists
|
|
@@ -221,8 +239,6 @@ exports.xiaoyiPlugin = {
|
|
|
221
239
|
console.error("XiaoYi: Missing sessionId in message, cannot process");
|
|
222
240
|
return;
|
|
223
241
|
}
|
|
224
|
-
// Store sessionId -> taskId mapping in runtime (use params.id as taskId)
|
|
225
|
-
runtime.setTaskIdForSession(sessionId, message.params.id);
|
|
226
242
|
// Get PluginRuntime from our runtime wrapper
|
|
227
243
|
const pluginRuntime = runtime.getPluginRuntime();
|
|
228
244
|
if (!pluginRuntime) {
|
|
@@ -231,8 +247,8 @@ exports.xiaoyiPlugin = {
|
|
|
231
247
|
}
|
|
232
248
|
// Extract text, file, and image content from parts array
|
|
233
249
|
let bodyText = "";
|
|
234
|
-
let images = [];
|
|
235
250
|
let fileAttachments = [];
|
|
251
|
+
const mediaFiles = [];
|
|
236
252
|
for (const part of message.params.message.parts) {
|
|
237
253
|
if (part.kind === "text" && part.text) {
|
|
238
254
|
// Handle text content
|
|
@@ -246,35 +262,27 @@ exports.xiaoyiPlugin = {
|
|
|
246
262
|
continue;
|
|
247
263
|
}
|
|
248
264
|
try {
|
|
249
|
-
//
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
267
|
-
// Handle text-based files
|
|
268
|
-
else if ((0, file_handler_1.isTextMimeType)(mimeType)) {
|
|
269
|
-
console.log(`XiaoYi: Processing text file: ${name} (${mimeType})`);
|
|
270
|
-
const textContent = await (0, file_handler_1.extractTextFromUrl)(uri, 5000000, 30000);
|
|
271
|
-
bodyText += `\n\n[文件内容: ${name}]\n${textContent}`;
|
|
272
|
-
fileAttachments.push(`[文件: ${name}]`);
|
|
273
|
-
console.log(`XiaoYi: Successfully processed text file: ${name}`);
|
|
265
|
+
// All files are downloaded to local disk and passed to OpenClaw
|
|
266
|
+
// No type validation - let Agent decide how to handle them
|
|
267
|
+
console.log(`XiaoYi: Processing file: ${name} (${mimeType})`);
|
|
268
|
+
mediaFiles.push({ uri, mimeType, name });
|
|
269
|
+
// For text-based files, also extract content inline
|
|
270
|
+
if ((0, xiaoyi_media_1.isTextMimeType)(mimeType)) {
|
|
271
|
+
try {
|
|
272
|
+
const textContent = await (0, xiaoyi_media_1.extractTextFromUrl)(uri, 5000000, 30000);
|
|
273
|
+
bodyText += `\n\n[文件内容: ${name}]\n${textContent}`;
|
|
274
|
+
fileAttachments.push(`[文件: ${name}]`);
|
|
275
|
+
console.log(`XiaoYi: Successfully extracted text from: ${name}`);
|
|
276
|
+
}
|
|
277
|
+
catch (textError) {
|
|
278
|
+
// Text extraction failed, but file is still in mediaFiles
|
|
279
|
+
console.warn(`XiaoYi: Text extraction failed for ${name}, will download as binary`);
|
|
280
|
+
fileAttachments.push(`[文件: ${name}]`);
|
|
281
|
+
}
|
|
274
282
|
}
|
|
275
283
|
else {
|
|
276
|
-
|
|
277
|
-
fileAttachments.push(`[
|
|
284
|
+
// Binary files (images, pdf, office docs, etc.)
|
|
285
|
+
fileAttachments.push(`[文件: ${name}]`);
|
|
278
286
|
}
|
|
279
287
|
}
|
|
280
288
|
catch (error) {
|
|
@@ -289,12 +297,18 @@ exports.xiaoyiPlugin = {
|
|
|
289
297
|
if (fileAttachments.length > 0) {
|
|
290
298
|
console.log(`XiaoYi: Processed ${fileAttachments.length} file(s): ${fileAttachments.join(", ")}`);
|
|
291
299
|
}
|
|
292
|
-
|
|
293
|
-
|
|
300
|
+
// Download media files to local disk (like feishu does)
|
|
301
|
+
let mediaPayload = {};
|
|
302
|
+
if (mediaFiles.length > 0) {
|
|
303
|
+
console.log(`XiaoYi: Downloading ${mediaFiles.length} media file(s) to local disk...`);
|
|
304
|
+
const downloadedMedia = await (0, xiaoyi_media_1.downloadAndSaveMediaList)(pluginRuntime, mediaFiles, { maxBytes: 30000000, timeoutMs: 60000 });
|
|
305
|
+
console.log(`XiaoYi: Successfully downloaded ${downloadedMedia.length}/${mediaFiles.length} file(s)`);
|
|
306
|
+
mediaPayload = (0, xiaoyi_media_1.buildXiaoYiMediaPayload)(downloadedMedia);
|
|
294
307
|
}
|
|
295
308
|
// Determine sender ID from role
|
|
296
309
|
const senderId = message.params.message.role === "user" ? "user" : message.agentId;
|
|
297
310
|
// Build MsgContext for OpenClaw's message pipeline
|
|
311
|
+
// Include media payload so OpenClaw can access local file paths
|
|
298
312
|
const msgContext = {
|
|
299
313
|
Body: bodyText,
|
|
300
314
|
From: senderId,
|
|
@@ -309,7 +323,24 @@ exports.xiaoyiPlugin = {
|
|
|
309
323
|
SenderName: message.params.message.role, // Use role as sender name
|
|
310
324
|
SenderId: senderId,
|
|
311
325
|
OriginatingChannel: "xiaoyi",
|
|
326
|
+
...mediaPayload, // Spread MediaPath, MediaPaths, MediaType, MediaTypes
|
|
312
327
|
};
|
|
328
|
+
// Log the message context for debugging
|
|
329
|
+
console.log("\n" + "=".repeat(60));
|
|
330
|
+
console.log("XiaoYi: [DEBUG] Message Context");
|
|
331
|
+
console.log(" " + JSON.stringify({
|
|
332
|
+
Body: msgContext.Body.substring(0, 50) + "...",
|
|
333
|
+
From: msgContext.From,
|
|
334
|
+
To: msgContext.To,
|
|
335
|
+
SessionKey: msgContext.SessionKey,
|
|
336
|
+
AccountId: msgContext.AccountId,
|
|
337
|
+
Provider: msgContext.Provider,
|
|
338
|
+
Surface: msgContext.Surface,
|
|
339
|
+
MediaPath: msgContext.MediaPath,
|
|
340
|
+
MediaPaths: msgContext.MediaPaths,
|
|
341
|
+
MediaType: msgContext.MediaType,
|
|
342
|
+
}, null, 2));
|
|
343
|
+
console.log("=".repeat(60) + "\n");
|
|
313
344
|
// Dispatch message using OpenClaw's reply dispatcher
|
|
314
345
|
try {
|
|
315
346
|
console.log("\n" + "=".repeat(60));
|
|
@@ -317,39 +348,82 @@ exports.xiaoyiPlugin = {
|
|
|
317
348
|
console.log(` Session: ${sessionId}`);
|
|
318
349
|
console.log(` Task ID: ${message.params.id}`);
|
|
319
350
|
console.log(` User input: ${bodyText.substring(0, 50)}${bodyText.length > 50 ? "..." : ""}`);
|
|
320
|
-
console.log(` Images: ${
|
|
351
|
+
console.log(` Images: ${mediaFiles.length}`);
|
|
321
352
|
console.log("=".repeat(60) + "\n");
|
|
322
|
-
|
|
353
|
+
// Get taskId from this message's params.id
|
|
354
|
+
// NOTE: We store this AFTER concurrent check to avoid overwriting active task's taskId
|
|
355
|
+
const currentTaskId = message.params.id;
|
|
356
|
+
// ==================== CONCURRENT REQUEST DETECTION ====================
|
|
357
|
+
// Check if this session already has an active agent run
|
|
358
|
+
// If so, send an immediate "busy" response and skip processing
|
|
359
|
+
if (runtime.isSessionActive(sessionId)) {
|
|
360
|
+
console.log("\n" + "=".repeat(60));
|
|
361
|
+
console.log(`[CONCURRENT] Session ${sessionId} has an active agent run`);
|
|
362
|
+
console.log(` Action: Sending busy response and skipping message`);
|
|
363
|
+
console.log("=".repeat(60) + "\n");
|
|
364
|
+
const conn = runtime.getConnection();
|
|
365
|
+
if (conn) {
|
|
366
|
+
try {
|
|
367
|
+
await conn.sendResponse({
|
|
368
|
+
sessionId: sessionId,
|
|
369
|
+
messageId: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
370
|
+
timestamp: Date.now(),
|
|
371
|
+
agentId: messageHandlerAgentId,
|
|
372
|
+
sender: {
|
|
373
|
+
id: messageHandlerAgentId,
|
|
374
|
+
name: "OpenClaw Agent",
|
|
375
|
+
type: "agent",
|
|
376
|
+
},
|
|
377
|
+
content: {
|
|
378
|
+
type: "text",
|
|
379
|
+
text: "上一个任务仍在处理中,请稍后再试",
|
|
380
|
+
},
|
|
381
|
+
status: "success",
|
|
382
|
+
}, currentTaskId, sessionId, true, false);
|
|
383
|
+
console.log(`[CONCURRENT] Busy response sent to session ${sessionId}\n`);
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
console.error(`[CONCURRENT] Failed to send busy response:`, error);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return; // Skip processing this concurrent request
|
|
390
|
+
}
|
|
391
|
+
// =================================================================
|
|
392
|
+
// Store sessionId -> taskId mapping (only after passing concurrent check)
|
|
393
|
+
runtime.setTaskIdForSession(sessionId, currentTaskId);
|
|
323
394
|
const startTime = Date.now();
|
|
324
395
|
let accumulatedText = "";
|
|
325
396
|
let sentTextLength = 0; // Track sent text length for streaming
|
|
326
397
|
let hasSentFinal = false; // Track if final content has been sent (to prevent duplicate isFinal=true)
|
|
327
398
|
// ==================== CREATE ABORT CONTROLLER ====================
|
|
328
399
|
// Create AbortController for this session to allow cancelation
|
|
329
|
-
const
|
|
400
|
+
const abortControllerResult = runtime.createAbortControllerForSession(sessionId);
|
|
401
|
+
if (!abortControllerResult) {
|
|
402
|
+
console.error(`[ERROR] Failed to create AbortController for session ${sessionId}`);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const { controller: abortController, signal: abortSignal } = abortControllerResult;
|
|
330
406
|
// ================================================================
|
|
331
407
|
// ==================== START TIMEOUT PROTECTION ====================
|
|
332
|
-
// Start 60-second timeout timer
|
|
408
|
+
// Start periodic 60-second timeout timer
|
|
409
|
+
// Will trigger every 60 seconds until a response is received or session completes
|
|
333
410
|
const timeoutConfig = runtime.getTimeoutConfig();
|
|
334
|
-
console.log(`[TIMEOUT] Starting ${timeoutConfig.duration}ms timeout protection for session ${sessionId}`);
|
|
335
|
-
// Define
|
|
411
|
+
console.log(`[TIMEOUT] Starting ${timeoutConfig.duration}ms periodic timeout protection for session ${sessionId}`);
|
|
412
|
+
// Define periodic timeout handler (will be called every 60 seconds)
|
|
336
413
|
const createTimeoutHandler = () => {
|
|
337
414
|
return async () => {
|
|
338
415
|
const elapsed = Date.now() - startTime;
|
|
339
416
|
console.log("\n" + "=".repeat(60));
|
|
340
417
|
console.log(`[TIMEOUT] Timeout triggered for session ${sessionId}`);
|
|
341
418
|
console.log(` Elapsed: ${elapsed}ms`);
|
|
342
|
-
console.log(` Task ID: ${
|
|
419
|
+
console.log(` Task ID: ${currentTaskId}`);
|
|
343
420
|
console.log("=".repeat(60) + "\n");
|
|
344
421
|
const conn = runtime.getConnection();
|
|
345
422
|
if (conn) {
|
|
346
423
|
try {
|
|
347
|
-
// Send status update
|
|
348
|
-
await conn.sendStatusUpdate(
|
|
424
|
+
// Send status update to keep conversation active
|
|
425
|
+
await conn.sendStatusUpdate(currentTaskId, sessionId, timeoutConfig.message);
|
|
349
426
|
console.log(`[TIMEOUT] Status update sent successfully to session ${sessionId}\n`);
|
|
350
|
-
// Restart the timeout timer - allow repeated timeout messages
|
|
351
|
-
console.log(`[TIMEOUT] Restarting timeout timer for session ${sessionId}...\n`);
|
|
352
|
-
runtime.setTimeoutForSession(sessionId, createTimeoutHandler());
|
|
353
427
|
}
|
|
354
428
|
catch (error) {
|
|
355
429
|
console.error(`[TIMEOUT] Failed to send status update:`, error);
|
|
@@ -358,234 +432,273 @@ exports.xiaoyiPlugin = {
|
|
|
358
432
|
else {
|
|
359
433
|
console.error(`[TIMEOUT] Connection not available, cannot send status update\n`);
|
|
360
434
|
}
|
|
435
|
+
// Note: Timeout will trigger again in 60 seconds if still active
|
|
361
436
|
};
|
|
362
437
|
};
|
|
363
|
-
// Start
|
|
438
|
+
// Start periodic timeout
|
|
364
439
|
runtime.setTimeoutForSession(sessionId, createTimeoutHandler());
|
|
365
440
|
// ==================== END TIMEOUT PROTECTION ====================
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
console.log(` Sent so far: ${sentTextLength} chars`);
|
|
384
|
-
if (completeText.length > 0) {
|
|
385
|
-
console.log(` Text preview: "${completeText.substring(0, 80)}${completeText.length > 80 ? "..." : ""}"`);
|
|
386
|
-
}
|
|
387
|
-
console.log(` Full info object:`, JSON.stringify(info, null, 2));
|
|
388
|
-
console.log(` Full payload keys:`, Object.keys(payload));
|
|
389
|
-
console.log("█".repeat(70) + "\n");
|
|
390
|
-
// =================================================================
|
|
391
|
-
// Check if session was aborted
|
|
392
|
-
if (runtime.isSessionAborted(sessionId)) {
|
|
393
|
-
console.log("\n" + "=".repeat(60));
|
|
394
|
-
console.log(`[ABORT] Response received AFTER abort`);
|
|
395
|
-
console.log(` Session: ${sessionId}`);
|
|
396
|
-
console.log(` Kind: ${kind}`);
|
|
397
|
-
console.log(` Elapsed: ${elapsed}ms`);
|
|
398
|
-
console.log(` Action: DISCARDING (session was canceled)`);
|
|
399
|
-
console.log("=".repeat(60) + "\n");
|
|
400
|
-
return;
|
|
441
|
+
// ==================== CREATE STREAMING DISPATCHER ====================
|
|
442
|
+
// Use createReplyDispatcherWithTyping for real-time streaming feedback
|
|
443
|
+
const { dispatcher, replyOptions, markDispatchIdle } = pluginRuntime.channel.reply.createReplyDispatcherWithTyping({
|
|
444
|
+
humanDelay: 0,
|
|
445
|
+
onReplyStart: async () => {
|
|
446
|
+
const elapsed = Date.now() - startTime;
|
|
447
|
+
console.log("\n" + "=".repeat(60));
|
|
448
|
+
console.log("XiaoYi: [START] Reply started after " + elapsed + "ms");
|
|
449
|
+
console.log(" Session: " + sessionId);
|
|
450
|
+
console.log(" Task ID: " + currentTaskId);
|
|
451
|
+
console.log("=".repeat(60) + "\n");
|
|
452
|
+
// Send immediate status update to let user know Agent is working
|
|
453
|
+
const conn = runtime.getConnection();
|
|
454
|
+
if (conn) {
|
|
455
|
+
try {
|
|
456
|
+
await conn.sendStatusUpdate(currentTaskId, sessionId, "任务正在处理中,请稍后");
|
|
457
|
+
console.log("✓ [START] Initial status update sent\n");
|
|
401
458
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
if (runtime.isSessionTimeout(sessionId)) {
|
|
405
|
-
console.log("\n" + "=".repeat(60));
|
|
406
|
-
console.log(`[TIMEOUT] Response received AFTER timeout`);
|
|
407
|
-
console.log(` Session: ${sessionId}`);
|
|
408
|
-
console.log(` Kind: ${kind}`);
|
|
409
|
-
console.log(` Elapsed: ${elapsed}ms`);
|
|
410
|
-
console.log(` Action: DISCARDING (timeout message already sent)`);
|
|
411
|
-
console.log("=".repeat(60) + "\n");
|
|
412
|
-
return;
|
|
459
|
+
catch (error) {
|
|
460
|
+
console.error("✗ [START] Failed to send initial status update:", error);
|
|
413
461
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
462
|
+
}
|
|
463
|
+
},
|
|
464
|
+
deliver: async (payload, info) => {
|
|
465
|
+
const elapsed = Date.now() - startTime;
|
|
466
|
+
const text = payload.text || "";
|
|
467
|
+
const kind = info.kind;
|
|
468
|
+
const payloadStatus = payload.status;
|
|
469
|
+
// IMPORTANT: Check if this is actually the final message
|
|
470
|
+
// Check multiple sources: payload.status, payload.queuedFinal, AND info.kind
|
|
471
|
+
// info.kind is the most reliable indicator for final messages
|
|
472
|
+
const isFinal = payloadStatus === "final" || payload.queuedFinal === true || kind === "final";
|
|
473
|
+
accumulatedText = text;
|
|
474
|
+
console.log("\n" + "█".repeat(70));
|
|
475
|
+
console.log("📨 [DELIVER] Payload received");
|
|
476
|
+
console.log(" Session: " + sessionId);
|
|
477
|
+
console.log(" Elapsed: " + elapsed + "ms");
|
|
478
|
+
console.log(" Info Kind: \"" + kind + "\"");
|
|
479
|
+
console.log(" Payload Status: \"" + (payloadStatus || "unknown") + "\"");
|
|
480
|
+
console.log(" Is Final: " + isFinal);
|
|
481
|
+
console.log(" Text length: " + text.length + " chars");
|
|
482
|
+
console.log(" Sent so far: " + sentTextLength + " chars");
|
|
483
|
+
if (text.length > 0) {
|
|
484
|
+
console.log(" Text preview: \"" + text.substring(0, 80) + (text.length > 80 ? "..." : "") + "\"");
|
|
485
|
+
}
|
|
486
|
+
console.log("█".repeat(70) + "\n");
|
|
487
|
+
// Only check for abort, NOT timeout
|
|
488
|
+
// Timeout is just for user notification, final responses should still be delivered
|
|
489
|
+
if (runtime.isSessionAborted(sessionId)) {
|
|
490
|
+
console.log("\n" + "=".repeat(60));
|
|
491
|
+
console.log("[ABORT] Response received AFTER abort");
|
|
492
|
+
console.log(" Session: " + sessionId);
|
|
493
|
+
console.log(" Action: DISCARDING");
|
|
494
|
+
console.log("=".repeat(60) + "\n");
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
// NOTE: We DON'T check timeout here anymore
|
|
498
|
+
// Even if timeout occurred, we should still deliver the final response
|
|
499
|
+
// Timeout was just to keep user informed, not to discard results
|
|
500
|
+
const conn = runtime.getConnection();
|
|
501
|
+
if (!conn) {
|
|
502
|
+
console.error("✗ XiaoYi: Connection not available\n");
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
// ==================== FIX: Empty text handling ====================
|
|
506
|
+
// If text is empty but this is not final, ALWAYS send a status update
|
|
507
|
+
// This ensures user gets feedback for EVERY Agent activity (tool calls, subagent calls, etc.)
|
|
508
|
+
if ((!text || text.length === 0) && !isFinal) {
|
|
509
|
+
console.log("\n" + "=".repeat(60));
|
|
510
|
+
console.log("[STREAM] Empty " + kind + " response detected");
|
|
511
|
+
console.log(" Session: " + sessionId);
|
|
512
|
+
console.log(" Action: Sending status update (no throttling)");
|
|
513
|
+
console.log("=".repeat(60) + "\n");
|
|
514
|
+
try {
|
|
515
|
+
await conn.sendStatusUpdate(currentTaskId, sessionId, "任务正在处理中,请稍后");
|
|
516
|
+
console.log("✓ Status update sent\n");
|
|
425
517
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
console.error(`✗ XiaoYi: Connection not available\n`);
|
|
429
|
-
return;
|
|
518
|
+
catch (error) {
|
|
519
|
+
console.error("✗ Failed to send status update:", error);
|
|
430
520
|
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
console.log("\n" + "-".repeat(60));
|
|
470
|
-
console.log(`XiaoYi: [STREAM] No new text to send`);
|
|
471
|
-
console.log(` Session: ${sessionId}`);
|
|
472
|
-
console.log(` Total length: ${completeText.length} chars`);
|
|
473
|
-
console.log(` Already sent: ${sentTextLength} chars`);
|
|
474
|
-
console.log("-".repeat(60) + "\n");
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
// ==================== END FIX ====================
|
|
524
|
+
const responseStatus = isFinal ? "success" : "processing";
|
|
525
|
+
const incrementalText = text.slice(sentTextLength);
|
|
526
|
+
const isAppend = !isFinal && incrementalText.length > 0;
|
|
527
|
+
if (incrementalText.length > 0 || isFinal) {
|
|
528
|
+
console.log("\n" + "-".repeat(60));
|
|
529
|
+
console.log("XiaoYi: [STREAM] Sending response");
|
|
530
|
+
console.log(" Response Status: " + responseStatus);
|
|
531
|
+
console.log(" Is Final: " + isFinal);
|
|
532
|
+
console.log(" Is Append: " + isAppend);
|
|
533
|
+
console.log("-".repeat(60) + "\n");
|
|
534
|
+
const response = {
|
|
535
|
+
sessionId: sessionId,
|
|
536
|
+
messageId: "msg_" + Date.now() + "_" + Math.random().toString(36).substr(2, 9),
|
|
537
|
+
timestamp: Date.now(),
|
|
538
|
+
agentId: messageHandlerAgentId, // Use stored value instead of resolvedAccount.config.agentId
|
|
539
|
+
sender: {
|
|
540
|
+
id: messageHandlerAgentId, // Use stored value instead of resolvedAccount.config.agentId
|
|
541
|
+
name: "OpenClaw Agent",
|
|
542
|
+
type: "agent",
|
|
543
|
+
},
|
|
544
|
+
content: {
|
|
545
|
+
type: "text",
|
|
546
|
+
text: isFinal ? text : incrementalText,
|
|
547
|
+
},
|
|
548
|
+
status: responseStatus,
|
|
549
|
+
};
|
|
550
|
+
// ==================== FIX: Prevent duplicate final messages ====================
|
|
551
|
+
// Only send isFinal=true if we haven't sent it before AND this is actually the final message
|
|
552
|
+
const shouldSendFinal = isFinal && !hasSentFinal;
|
|
553
|
+
try {
|
|
554
|
+
await conn.sendResponse(response, currentTaskId, sessionId, shouldSendFinal, isAppend);
|
|
555
|
+
console.log("✓ Sent (status=" + responseStatus + ", isFinal=" + shouldSendFinal + ", append=" + isAppend + ")\n");
|
|
556
|
+
// Mark that we've sent a final message (even if we're still processing subagent responses)
|
|
557
|
+
if (isFinal) {
|
|
558
|
+
hasSentFinal = true;
|
|
475
559
|
}
|
|
476
560
|
}
|
|
477
|
-
|
|
478
|
-
console.
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
561
|
+
catch (error) {
|
|
562
|
+
console.error("✗ Failed to send:", error);
|
|
563
|
+
}
|
|
564
|
+
sentTextLength = text.length;
|
|
565
|
+
}
|
|
566
|
+
// ==================== FIX: SubAgent-friendly cleanup logic ====================
|
|
567
|
+
// Only mark session as completed if we're truly done (no more subagent responses expected)
|
|
568
|
+
// The key insight: we should NOT cleanup on every "final" payload, because subagents
|
|
569
|
+
// can generate additional responses after the main agent returns "final".
|
|
570
|
+
//
|
|
571
|
+
// Instead, we let onIdle handle the cleanup, which is called after ALL processing is done.
|
|
572
|
+
if (isFinal) {
|
|
573
|
+
// Clear timeout but DON'T mark session as completed yet
|
|
574
|
+
// SubAgent might still send more responses
|
|
575
|
+
runtime.clearSessionTimeout(sessionId);
|
|
576
|
+
console.log("[CLEANUP] Final payload received, but NOT marking session completed yet (waiting for onIdle)\n");
|
|
577
|
+
}
|
|
578
|
+
// ==================== END FIX ====================
|
|
579
|
+
},
|
|
580
|
+
onError: (err, info) => {
|
|
581
|
+
console.error("\n" + "=".repeat(60));
|
|
582
|
+
console.error("XiaoYi: [ERROR] " + info.kind + " failed: " + String(err));
|
|
583
|
+
console.log("=".repeat(60) + "\n");
|
|
584
|
+
runtime.clearSessionTimeout(sessionId);
|
|
585
|
+
runtime.clearAbortControllerForSession(sessionId);
|
|
586
|
+
runtime.markSessionCompleted(sessionId);
|
|
587
|
+
},
|
|
588
|
+
onIdle: async () => {
|
|
589
|
+
const elapsed = Date.now() - startTime;
|
|
590
|
+
console.log("\n" + "=".repeat(60));
|
|
591
|
+
console.log("XiaoYi: [IDLE] Processing complete");
|
|
592
|
+
console.log(" Total time: " + elapsed + "ms");
|
|
593
|
+
console.log("=".repeat(60) + "\n");
|
|
594
|
+
// ==================== PUSH MESSAGE FOR BACKGROUND RESULTS ====================
|
|
595
|
+
// NOTE: Push logic disabled because we cannot reliably distinguish between:
|
|
596
|
+
// - Normal responses (should be sent via WebSocket)
|
|
597
|
+
// - Background task completion (should be sent via HTTP push)
|
|
598
|
+
// TODO: Implement proper push message detection and HTTP API call
|
|
599
|
+
console.log("[IDLE] All agent processing complete");
|
|
600
|
+
// ==================== END PUSH MESSAGE ====================
|
|
601
|
+
// This is called AFTER all processing is done (including subagents)
|
|
602
|
+
// NOW we can safely mark the session as completed
|
|
603
|
+
runtime.clearAbortControllerForSession(sessionId);
|
|
604
|
+
runtime.markSessionCompleted(sessionId);
|
|
605
|
+
console.log("[CLEANUP] Session marked as completed in onIdle\n");
|
|
606
|
+
},
|
|
607
|
+
});
|
|
608
|
+
try {
|
|
609
|
+
const result = await pluginRuntime.channel.reply.dispatchReplyFromConfig({
|
|
610
|
+
ctx: msgContext,
|
|
611
|
+
cfg: config,
|
|
612
|
+
dispatcher,
|
|
613
|
+
replyOptions: {
|
|
614
|
+
...replyOptions,
|
|
615
|
+
abortSignal: abortSignal,
|
|
616
|
+
},
|
|
617
|
+
});
|
|
618
|
+
const { queuedFinal, counts } = result;
|
|
619
|
+
console.log("\n" + "=".repeat(60));
|
|
620
|
+
console.log("XiaoYi: [DISPATCH] Summary");
|
|
621
|
+
console.log(" Queued Final: " + queuedFinal);
|
|
622
|
+
if (counts && Object.keys(counts).length > 0) {
|
|
623
|
+
console.log(" Counts:", JSON.stringify(counts, null, 2));
|
|
624
|
+
}
|
|
625
|
+
console.log("=".repeat(60) + "\n");
|
|
626
|
+
// ==================== ANALYZE EXECUTION RESULT ====================
|
|
627
|
+
// Check if Agent produced any output
|
|
628
|
+
const hasAnyCounts = counts && ((counts.tool && counts.tool > 0) ||
|
|
629
|
+
(counts.block && counts.block > 0) ||
|
|
630
|
+
(counts.final && counts.final > 0));
|
|
631
|
+
if (!hasAnyCounts) {
|
|
632
|
+
// Scenario 1: No Agent output detected
|
|
633
|
+
// This could mean:
|
|
634
|
+
// a) SubAgent running in background (main Agent returned)
|
|
635
|
+
// b) Concurrent request (another Agent already running on this session)
|
|
636
|
+
console.log("\n" + "=".repeat(60));
|
|
637
|
+
console.log("[NO OUTPUT] Agent produced no output");
|
|
638
|
+
console.log(" Session: " + sessionId);
|
|
639
|
+
console.log(" Checking if there's another active Agent...");
|
|
640
|
+
console.log("=".repeat(60) + "\n");
|
|
641
|
+
// Check if there's an active Agent on this session
|
|
642
|
+
// We use the existence of deliver callback triggers as an indicator
|
|
643
|
+
// If the dispatcher's onIdle will be called later, an Agent is still running
|
|
644
|
+
const conn = runtime.getConnection();
|
|
645
|
+
if (conn) {
|
|
646
|
+
// IMPORTANT: Send a response to user for THIS request
|
|
647
|
+
// User needs to know what's happening
|
|
648
|
+
try {
|
|
493
649
|
const response = {
|
|
494
650
|
sessionId: sessionId,
|
|
495
651
|
messageId: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
496
652
|
timestamp: Date.now(),
|
|
497
|
-
agentId: resolvedAccount.config.agentId
|
|
653
|
+
agentId: messageHandlerAgentId, // Use stored value instead of resolvedAccount.config.agentId
|
|
498
654
|
sender: {
|
|
499
|
-
id: resolvedAccount.config.agentId
|
|
655
|
+
id: messageHandlerAgentId, // Use stored value instead of resolvedAccount.config.agentId
|
|
500
656
|
name: "OpenClaw Agent",
|
|
501
657
|
type: "agent",
|
|
502
658
|
},
|
|
503
659
|
content: {
|
|
504
660
|
type: "text",
|
|
505
|
-
text:
|
|
661
|
+
text: "任务正在处理中,请稍候...",
|
|
506
662
|
},
|
|
507
663
|
status: "success",
|
|
508
664
|
};
|
|
509
|
-
// Send
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
console.log(`✓ XiaoYi: Final response content sent (${completeText.length} chars, stream kept alive)\n`);
|
|
513
|
-
// Mark that we've sent the final content
|
|
514
|
-
hasSentFinal = true;
|
|
515
|
-
// Clear timeout to prevent timeout warnings, but don't mark session as completed yet
|
|
516
|
-
runtime.clearSessionTimeout(sessionId);
|
|
517
|
-
// Note: We DON'T call markSessionCompleted here
|
|
518
|
-
// The session will be marked completed in onIdle when we send isFinal=true
|
|
519
|
-
}
|
|
520
|
-
else if (kind === "tool") {
|
|
521
|
-
// Tool results - typically not sent as messages
|
|
522
|
-
console.log("\n" + "-".repeat(60));
|
|
523
|
-
console.log(`XiaoYi: [STREAM] Tool result received (not forwarded)`);
|
|
524
|
-
console.log(` Session: ${sessionId}`);
|
|
525
|
-
console.log(` Elapsed: ${elapsed}ms`);
|
|
526
|
-
console.log("-".repeat(60) + "\n");
|
|
527
|
-
}
|
|
528
|
-
},
|
|
529
|
-
onIdle: async () => {
|
|
530
|
-
const elapsed = Date.now() - startTime;
|
|
531
|
-
console.log("\n" + "=".repeat(60));
|
|
532
|
-
console.log(`XiaoYi: [IDLE] Processing complete`);
|
|
533
|
-
console.log(` Total time: ${elapsed}ms`);
|
|
534
|
-
console.log(` Final length: ${accumulatedText.length} chars`);
|
|
535
|
-
console.log(` Has sent final: ${hasSentFinal}`);
|
|
536
|
-
// Only send final close message if we have valid content
|
|
537
|
-
if (accumulatedText.length > 0) {
|
|
538
|
-
const conn = runtime.getConnection();
|
|
539
|
-
if (conn) {
|
|
540
|
-
try {
|
|
541
|
-
// Send the true final response with isFinal=true to properly close the stream
|
|
542
|
-
const finalResponse = {
|
|
543
|
-
sessionId: sessionId,
|
|
544
|
-
messageId: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
545
|
-
timestamp: Date.now(),
|
|
546
|
-
agentId: resolvedAccount.config.agentId,
|
|
547
|
-
sender: {
|
|
548
|
-
id: resolvedAccount.config.agentId,
|
|
549
|
-
name: "OpenClaw Agent",
|
|
550
|
-
type: "agent",
|
|
551
|
-
},
|
|
552
|
-
content: {
|
|
553
|
-
type: "text",
|
|
554
|
-
text: accumulatedText,
|
|
555
|
-
},
|
|
556
|
-
status: "success",
|
|
557
|
-
};
|
|
558
|
-
// Send with isFinal=true to properly close the A2A communication stream
|
|
559
|
-
await conn.sendResponse(finalResponse, taskId, sessionId, true, false);
|
|
560
|
-
console.log(`✓ XiaoYi: True isFinal=true sent to close stream\n`);
|
|
561
|
-
}
|
|
562
|
-
catch (error) {
|
|
563
|
-
console.error(`✗ XiaoYi: Failed to send final close message:`, error);
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
// Now mark session as completed and clean up
|
|
567
|
-
runtime.markSessionCompleted(sessionId);
|
|
568
|
-
runtime.clearAbortControllerForSession(sessionId);
|
|
569
|
-
console.log(`[TIMEOUT] Session completed and cleaned up\n`);
|
|
665
|
+
// Send response with isFinal=true to close THIS request
|
|
666
|
+
await conn.sendResponse(response, currentTaskId, sessionId, true, false);
|
|
667
|
+
console.log("✓ [NO OUTPUT] Response sent to user\n");
|
|
570
668
|
}
|
|
571
|
-
|
|
572
|
-
console.
|
|
669
|
+
catch (error) {
|
|
670
|
+
console.error("✗ [NO OUTPUT] Failed to send response:", error);
|
|
573
671
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
672
|
+
}
|
|
673
|
+
// CRITICAL: Don't cleanup resources yet!
|
|
674
|
+
// The original Agent might still be running and needs these resources
|
|
675
|
+
// onIdle will be called when the original Agent completes
|
|
676
|
+
console.log("[NO OUTPUT] Keeping resources alive for potential background Agent\n");
|
|
677
|
+
markDispatchIdle();
|
|
678
|
+
}
|
|
679
|
+
else {
|
|
680
|
+
// Scenario 2: Normal execution with output
|
|
681
|
+
// - Agent produced output synchronously
|
|
682
|
+
// - All cleanup is already handled in deliver/onIdle callbacks
|
|
683
|
+
console.log("[NORMAL] Agent produced output, cleanup handled in callbacks");
|
|
684
|
+
markDispatchIdle();
|
|
685
|
+
}
|
|
686
|
+
// ==================== END ANALYSIS ====================
|
|
687
|
+
}
|
|
688
|
+
catch (error) {
|
|
689
|
+
console.error("XiaoYi: [ERROR] Error dispatching message:", error);
|
|
690
|
+
// Clear timeout on error
|
|
691
|
+
runtime.clearSessionTimeout(sessionId);
|
|
692
|
+
// Clear abort controller on error
|
|
693
|
+
runtime.clearAbortControllerForSession(sessionId);
|
|
694
|
+
// Mark session as completed on error
|
|
695
|
+
runtime.markSessionCompleted(sessionId);
|
|
696
|
+
// Mark dispatcher as idle even on error
|
|
697
|
+
markDispatchIdle();
|
|
698
|
+
}
|
|
582
699
|
}
|
|
583
700
|
catch (error) {
|
|
584
|
-
console.error("XiaoYi: [ERROR]
|
|
585
|
-
// Clear timeout on error
|
|
586
|
-
runtime.clearSessionTimeout(sessionId);
|
|
587
|
-
// Clear abort controller on error
|
|
588
|
-
runtime.clearAbortControllerForSession(sessionId);
|
|
701
|
+
console.error("XiaoYi: [ERROR] Unexpected error in message handler:", error);
|
|
589
702
|
}
|
|
590
703
|
});
|
|
591
704
|
// Setup cancel handler
|