@ynhcj/xiaoyi 2.5.6 → 2.5.7

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.js CHANGED
@@ -1,9 +1,44 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.xiaoyiPlugin = void 0;
4
- const runtime_1 = require("./runtime");
5
- const onboarding_1 = require("./onboarding");
6
- const xiaoyi_media_1 = require("./xiaoyi-media");
37
+ const runtime_js_1 = require("./runtime.js");
38
+ const onboarding_js_1 = require("./onboarding.js");
39
+ const session_manager_js_1 = require("./xy-tools/session-manager.js");
40
+ // Special marker for default push delivery when no target is specified (cron/announce mode)
41
+ const DEFAULT_PUSH_MARKER = "default";
7
42
  /**
8
43
  * Track if message handlers have been registered to prevent duplicate registrations
9
44
  * when startAccount() is called multiple times due to auto-restart attempts
@@ -89,7 +124,7 @@ exports.xiaoyiPlugin = {
89
124
  },
90
125
  },
91
126
  },
92
- onboarding: onboarding_1.xiaoyiOnboardingAdapter,
127
+ onboarding: onboarding_js_1.xiaoyiOnboardingAdapter,
93
128
  /**
94
129
  * Config adapter - single account mode
95
130
  */
@@ -167,858 +202,151 @@ exports.xiaoyiPlugin = {
167
202
  }),
168
203
  },
169
204
  /**
170
- * Outbound adapter - send messages
205
+ * Gateway adapter - manage connections
206
+ * Using xy-monitor for message handling (xy_channel architecture)
207
+ */
208
+ gateway: {
209
+ startAccount: async (ctx) => {
210
+ console.log("XiaoYi: startAccount() called - START");
211
+ const { monitorXYProvider } = await Promise.resolve().then(() => __importStar(require("./xy-monitor.js")));
212
+ const account = ctx.account;
213
+ const config = ctx.cfg;
214
+ console.log(`[xiaoyi] Starting xiaoyi channel with xy_monitor architecture`);
215
+ console.log(`[xiaoyi] Account ID: ${account.accountId}`);
216
+ console.log(`[xiaoyi] Agent ID: ${account.config.agentId}`);
217
+ return monitorXYProvider({
218
+ config: config,
219
+ runtime: ctx.runtime,
220
+ abortSignal: ctx.abortSignal,
221
+ accountId: account.accountId,
222
+ setStatus: ctx.setStatus,
223
+ });
224
+ },
225
+ stopAccount: async (ctx) => {
226
+ const runtime = (0, runtime_js_1.getXiaoYiRuntime)();
227
+ runtime.stop();
228
+ },
229
+ },
230
+ /**
231
+ * Outbound adapter - send messages via push
171
232
  */
172
233
  outbound: {
173
234
  deliveryMode: "direct",
174
235
  textChunkLimit: 4000,
175
- sendText: async (ctx) => {
176
- const runtime = (0, runtime_1.getXiaoYiRuntime)();
177
- const connection = runtime.getConnection();
178
- if (!connection || !connection.isReady()) {
179
- throw new Error("XiaoYi channel not connected");
236
+ resolveTarget: ({ cfg, to, accountId, mode }) => {
237
+ if (!to || to.trim() === "") {
238
+ console.log(`[xiaoyi.resolveTarget] No target specified, using default push marker`);
239
+ return { ok: true, to: DEFAULT_PUSH_MARKER };
180
240
  }
181
- // Get account config to retrieve agentId
182
- const resolvedAccount = ctx.account;
183
- const agentId = resolvedAccount.config.agentId;
184
- // Use 'to' as sessionId (it's set from incoming message's sessionId)
185
- const sessionId = ctx.to;
186
- // Get taskId from runtime's session mapping (must exist - from original A2A request)
187
- const taskId = runtime.getTaskIdForSession(sessionId);
188
- if (!taskId) {
189
- throw new Error(`Cannot send outbound message: No taskId found for session ${sessionId}. Outbound messages must be in response to an incoming A2A request.`);
241
+ const trimmedTo = to.trim();
242
+ if (!trimmedTo.includes("::")) {
243
+ console.log(`[xiaoyi.resolveTarget] Target "${trimmedTo}" missing taskId, looking up session context`);
244
+ const sessionContext = (0, session_manager_js_1.getLatestSessionContext)();
245
+ if (sessionContext && sessionContext.sessionId === trimmedTo) {
246
+ const enhancedTarget = `${trimmedTo}::${sessionContext.taskId}`;
247
+ console.log(`[xiaoyi.resolveTarget] Enhanced target: ${enhancedTarget}`);
248
+ return { ok: true, to: enhancedTarget };
249
+ }
250
+ console.log(`[xiaoyi.resolveTarget] Could not find matching session context for "${trimmedTo}"`);
190
251
  }
191
- // Build A2A response message
192
- const response = {
193
- sessionId: sessionId,
194
- messageId: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
195
- timestamp: Date.now(),
196
- agentId: agentId,
197
- sender: {
198
- id: agentId,
199
- name: "OpenClaw Agent",
200
- type: "agent",
201
- },
202
- content: {
203
- type: "text",
204
- text: ctx.text,
205
- },
206
- context: ctx.replyToId ? {
207
- replyToMessageId: ctx.replyToId,
208
- } : undefined,
209
- status: "success",
210
- };
211
- // Send via WebSocket with taskId and sessionId
212
- await connection.sendResponse(response, taskId, sessionId);
213
- return {
214
- channel: "xiaoyi",
215
- messageId: response.messageId,
216
- conversationId: sessionId,
217
- timestamp: response.timestamp,
218
- };
252
+ return { ok: true, to: trimmedTo };
219
253
  },
220
- sendMedia: async (ctx) => {
221
- const runtime = (0, runtime_1.getXiaoYiRuntime)();
222
- const connection = runtime.getConnection();
223
- if (!connection || !connection.isReady()) {
224
- throw new Error("XiaoYi channel not connected");
254
+ sendText: async (ctx) => {
255
+ const { cfg, to, text, accountId } = ctx;
256
+ console.log(`[xiaoyi.sendText] Called with: to=${to}, textLength=${text?.length || 0}`);
257
+ const { resolveXYConfig } = await Promise.resolve().then(() => __importStar(require("./xy-config.js")));
258
+ const { XiaoYiPushService } = await Promise.resolve().then(() => __importStar(require("./push.js")));
259
+ const { configManager } = await Promise.resolve().then(() => __importStar(require("./xy-utils/config-manager.js")));
260
+ const config = { ...resolveXYConfig(cfg) };
261
+ // Resolve actual target (strip taskId portion if present)
262
+ let actualTo = to;
263
+ if (to === DEFAULT_PUSH_MARKER) {
264
+ actualTo = config.defaultSessionId || "";
225
265
  }
226
- const resolvedAccount = ctx.account;
227
- const agentId = resolvedAccount.config.agentId;
228
- // Use 'to' as sessionId
229
- const sessionId = ctx.to;
230
- // Get taskId from runtime's session mapping (must exist - from original A2A request)
231
- const taskId = runtime.getTaskIdForSession(sessionId);
232
- if (!taskId) {
233
- throw new Error(`Cannot send outbound media: No taskId found for session ${sessionId}. Outbound messages must be in response to an incoming A2A request.`);
266
+ else if (to.includes("::")) {
267
+ actualTo = to.split("::")[0];
234
268
  }
235
- // Build A2A response message with media
236
- const response = {
237
- sessionId: sessionId,
238
- messageId: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
239
- timestamp: Date.now(),
240
- agentId: agentId,
241
- sender: {
242
- id: agentId,
243
- name: "OpenClaw Agent",
244
- type: "agent",
245
- },
246
- content: {
247
- type: "image", // Assume image for now, could be extended
248
- text: ctx.text,
249
- mediaUrl: ctx.mediaUrl,
250
- },
251
- context: ctx.replyToId ? {
252
- replyToMessageId: ctx.replyToId,
253
- } : undefined,
254
- status: "success",
255
- };
256
- await connection.sendResponse(response, taskId, sessionId);
269
+ // Override pushId with dynamic per-session pushId if available
270
+ const dynamicPushId = configManager.getPushId(actualTo);
271
+ if (dynamicPushId) {
272
+ config.pushId = dynamicPushId;
273
+ }
274
+ const pushService = new XiaoYiPushService(config);
275
+ // Extract title (first line, up to 57 chars)
276
+ const title = text.split("\n")[0].slice(0, 57);
277
+ // Truncate content to 1000 chars
278
+ const pushText = text.length > 1000 ? text.slice(0, 1000) : text;
279
+ await pushService.sendPush(pushText, title);
280
+ console.log(`[xiaoyi.sendText] Push sent successfully`);
257
281
  return {
258
282
  channel: "xiaoyi",
259
- messageId: response.messageId,
260
- conversationId: sessionId,
261
- timestamp: response.timestamp,
283
+ messageId: Date.now().toString(),
284
+ chatId: actualTo,
262
285
  };
263
286
  },
264
- },
265
- /**
266
- * Gateway adapter - manage connections
267
- */
268
- gateway: {
269
- startAccount: async (ctx) => {
270
- console.log("XiaoYi: startAccount() called - START");
271
- const runtime = (0, runtime_1.getXiaoYiRuntime)();
272
- const resolvedAccount = ctx.account;
273
- const config = ctx.cfg;
274
- // Start WebSocket connection (single account mode)
275
- // Wrap in try-catch to prevent startup errors from causing auto-restart
276
- let connection = null;
277
- try {
278
- await runtime.start(resolvedAccount.config);
279
- connection = runtime.getConnection();
280
- }
281
- catch (error) {
282
- console.error("XiaoYi: [STARTUP] Failed to start WebSocket connection:", error);
283
- // Don't throw - let the connection retry logic handle reconnection
284
- // The runtime.start() will handle reconnection internally
285
- }
286
- // Setup message handler IMMEDIATELY after connection is established
287
- if (!connection) {
288
- connection = runtime.getConnection();
289
- }
290
- if (!connection) {
291
- console.warn("XiaoYi: [STARTUP] No WebSocket connection available yet, will retry...");
292
- // Throw error to prevent auto-restart - let runtime handle reconnection
293
- // The runtime.start() will keep trying to reconnect internally
294
- throw new Error("XiaoYi: WebSocket connection not available, runtime will retry");
295
- }
296
- // Only register handlers once to prevent duplicate message processing
297
- // when startAccount() is called multiple times due to auto-restart attempts
298
- if (!handlersRegistered) {
299
- console.log("XiaoYi: [STARTUP] Registering message and cancel handlers");
300
- // Setup message handler with try-catch to prevent individual message errors from crashing the channel
301
- connection.on("message", async (message) => {
302
- // CRITICAL: Use dynamic require to get the latest runtime module after hot-reload
303
- const { getXiaoYiRuntime } = require("./runtime");
304
- const runtime = getXiaoYiRuntime();
305
- console.log(`XiaoYi: [Message Handler] Using runtime instance: ${runtime.getInstanceId()}`);
306
- // CRITICAL FIX: Extract and store config values at message handler level
307
- // This prevents "Cannot read properties of undefined" errors in concurrent scenarios
308
- // where the outer scope's resolvedAccount might become unavailable
309
- const messageHandlerAgentId = resolvedAccount.config?.agentId;
310
- const messageHandlerAccountId = resolvedAccount.accountId;
311
- const messageHandlerConfig = resolvedAccount.config;
312
- if (!messageHandlerAgentId) {
313
- console.error("XiaoYi: [FATAL] agentId not available in resolvedAccount.config");
314
- return;
315
- }
316
- // Set task timeout time from configuration
317
- runtime.setTaskTimeout(messageHandlerConfig.taskTimeoutMs || 3600000);
318
- console.log(`XiaoYi: [Message Handler] Stored config values - agentId: ${messageHandlerAgentId}, accountId: ${messageHandlerAccountId}`);
319
- // For message/stream, prioritize params.sessionId, fallback to top-level sessionId
320
- const sessionId = message.params?.sessionId || message.sessionId;
321
- // Validate sessionId exists
322
- if (!sessionId) {
323
- console.error("XiaoYi: Missing sessionId in message, cannot process");
324
- return;
325
- }
326
- // Get PluginRuntime from our runtime wrapper
327
- const pluginRuntime = runtime.getPluginRuntime();
328
- if (!pluginRuntime) {
329
- console.error("PluginRuntime not available");
330
- return;
331
- }
332
- // Extract text, file, and image content from parts array
333
- let bodyText = "";
334
- let fileAttachments = [];
335
- const mediaFiles = [];
336
- for (const part of message.params.message.parts) {
337
- if (part.kind === "text" && part.text) {
338
- // Handle text content
339
- bodyText += part.text;
340
- }
341
- else if (part.kind === "file" && part.file) {
342
- // Handle file content
343
- const { uri, mimeType, name } = part.file;
344
- if (!uri) {
345
- console.warn(`XiaoYi: File part without URI, skipping: ${name}`);
346
- continue;
347
- }
348
- try {
349
- // All files are downloaded to local disk and passed to OpenClaw
350
- // No type validation - let Agent decide how to handle them
351
- console.log(`XiaoYi: Processing file: ${name} (${mimeType})`);
352
- mediaFiles.push({ uri, mimeType, name });
353
- // For text-based files, also extract content inline
354
- if ((0, xiaoyi_media_1.isTextMimeType)(mimeType)) {
355
- try {
356
- const textContent = await (0, xiaoyi_media_1.extractTextFromUrl)(uri, 5000000, 30000);
357
- bodyText += `\n\n[文件内容: ${name}]\n${textContent}`;
358
- fileAttachments.push(`[文件: ${name}]`);
359
- console.log(`XiaoYi: Successfully extracted text from: ${name}`);
360
- }
361
- catch (textError) {
362
- // Text extraction failed, but file is still in mediaFiles
363
- console.warn(`XiaoYi: Text extraction failed for ${name}, will download as binary`);
364
- fileAttachments.push(`[文件: ${name}]`);
365
- }
366
- }
367
- else {
368
- // Binary files (images, pdf, office docs, etc.)
369
- fileAttachments.push(`[文件: ${name}]`);
370
- }
371
- }
372
- catch (error) {
373
- const errorMsg = error instanceof Error ? error.message : String(error);
374
- console.error(`XiaoYi: Failed to process file ${name}: ${errorMsg}`);
375
- fileAttachments.push(`[文件处理失败: ${name} - ${errorMsg}]`);
376
- }
377
- }
378
- // Ignore kind: "data" as per user request
379
- }
380
- // Log summary of processed attachments
381
- if (fileAttachments.length > 0) {
382
- console.log(`XiaoYi: Processed ${fileAttachments.length} file(s): ${fileAttachments.join(", ")}`);
383
- }
384
- // Download media files to local disk (like feishu does)
385
- let mediaPayload = {};
386
- if (mediaFiles.length > 0) {
387
- console.log(`XiaoYi: Downloading ${mediaFiles.length} media file(s) to local disk...`);
388
- const downloadedMedia = await (0, xiaoyi_media_1.downloadAndSaveMediaList)(pluginRuntime, mediaFiles, { maxBytes: 30000000, timeoutMs: 60000 });
389
- console.log(`XiaoYi: Successfully downloaded ${downloadedMedia.length}/${mediaFiles.length} file(s)`);
390
- mediaPayload = (0, xiaoyi_media_1.buildXiaoYiMediaPayload)(downloadedMedia);
391
- }
392
- // Determine sender ID from role
393
- const senderId = message.params.message.role === "user" ? "user" : message.agentId;
394
- // Build MsgContext for OpenClaw's message pipeline
395
- // Include media payload so OpenClaw can access local file paths
396
- const msgContext = {
397
- Body: bodyText,
398
- From: senderId,
399
- To: sessionId,
400
- SessionKey: `xiaoyi:${resolvedAccount.accountId}:${sessionId}`,
401
- AccountId: resolvedAccount.accountId,
402
- MessageSid: message.id, // Use top-level id as message sequence number
403
- Timestamp: Date.now(), // Generate timestamp since new format doesn't include it
404
- Provider: "xiaoyi",
405
- Surface: "xiaoyi",
406
- ChatType: "direct",
407
- SenderName: message.params.message.role, // Use role as sender name
408
- SenderId: senderId,
409
- OriginatingChannel: "xiaoyi",
410
- ...mediaPayload, // Spread MediaPath, MediaPaths, MediaType, MediaTypes
411
- };
412
- // Log the message context for debugging
413
- console.log("\n" + "=".repeat(60));
414
- console.log("XiaoYi: [DEBUG] Message Context");
415
- console.log(" " + JSON.stringify({
416
- Body: msgContext.Body.substring(0, 50) + "...",
417
- From: msgContext.From,
418
- To: msgContext.To,
419
- SessionKey: msgContext.SessionKey,
420
- AccountId: msgContext.AccountId,
421
- Provider: msgContext.Provider,
422
- Surface: msgContext.Surface,
423
- MediaPath: msgContext.MediaPath,
424
- MediaPaths: msgContext.MediaPaths,
425
- MediaType: msgContext.MediaType,
426
- }, null, 2));
427
- console.log("=".repeat(60) + "\n");
428
- // Dispatch message using OpenClaw's reply dispatcher
429
- try {
430
- console.log("\n" + "=".repeat(60));
431
- console.log(`XiaoYi: [MESSAGE] Processing user message`);
432
- console.log(` Session: ${sessionId}`);
433
- console.log(` Task ID: ${message.params.id}`);
434
- console.log(` User input: ${bodyText.substring(0, 50)}${bodyText.length > 50 ? "..." : ""}`);
435
- console.log(` Images: ${mediaFiles.length}`);
436
- console.log("=".repeat(60) + "\n");
437
- // Get taskId from this message's params.id
438
- // NOTE: We store this AFTER concurrent check to avoid overwriting active task's taskId
439
- const currentTaskId = message.params.id;
440
- // ==================== CONCURRENT REQUEST DETECTION ====================
441
- // Check if this session already has an active agent run
442
- // If so, send an immediate "busy" response and skip processing
443
- if (runtime.isSessionActive(sessionId)) {
444
- console.log("\n" + "=".repeat(60));
445
- console.log(`[CONCURRENT] Session ${sessionId} has an active agent run`);
446
- console.log(` Action: Sending busy response and skipping message`);
447
- console.log("=".repeat(60) + "\n");
448
- const conn = runtime.getConnection();
449
- if (conn) {
450
- try {
451
- await conn.sendResponse({
452
- sessionId: sessionId,
453
- messageId: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
454
- timestamp: Date.now(),
455
- agentId: messageHandlerAgentId,
456
- sender: {
457
- id: messageHandlerAgentId,
458
- name: "OpenClaw Agent",
459
- type: "agent",
460
- },
461
- content: {
462
- type: "text",
463
- text: "上一个任务仍在处理中,请稍后再试",
464
- },
465
- status: "success",
466
- }, currentTaskId, sessionId, true, false);
467
- console.log(`[CONCURRENT] Busy response sent to session ${sessionId}\n`);
468
- }
469
- catch (error) {
470
- console.error(`[CONCURRENT] Failed to send busy response:`, error);
471
- }
472
- }
473
- return; // Skip processing this concurrent request
474
- }
475
- // =================================================================
476
- // Store sessionId -> taskId mapping (only after passing concurrent check)
477
- runtime.setTaskIdForSession(sessionId, currentTaskId);
478
- const startTime = Date.now();
479
- let accumulatedText = "";
480
- let sentTextLength = 0; // Track sent text length for streaming
481
- // ==================== CREATE ABORT CONTROLLER ====================
482
- // Create AbortController for this session to allow cancelation
483
- const abortControllerResult = runtime.createAbortControllerForSession(sessionId);
484
- if (!abortControllerResult) {
485
- console.error(`[ERROR] Failed to create AbortController for session ${sessionId}`);
486
- return;
487
- }
488
- const { controller: abortController, signal: abortSignal } = abortControllerResult;
489
- // ================================================================
490
- // ==================== 1-HOUR TASK TIMEOUT PROTECTION ====================
491
- // Start 1-hour task timeout timer
492
- // Will trigger once after 1 hour if no response received
493
- console.log(`[TASK TIMEOUT] Starting ${messageHandlerConfig.taskTimeoutMs || 3600000}ms task timeout protection for session ${sessionId}`);
494
- // Define task timeout handler (will be called once after 1 hour)
495
- const createTaskTimeoutHandler = () => {
496
- return async (timeoutSessionId, timeoutTaskId) => {
497
- const elapsed = Date.now() - startTime;
498
- console.log("\n" + "=".repeat(60));
499
- console.log(`[TASK TIMEOUT] 1-hour timeout triggered for session ${sessionId}`);
500
- console.log(` Elapsed: ${elapsed}ms`);
501
- console.log(` Task ID: ${currentTaskId}`);
502
- console.log("=".repeat(60) + "\n");
503
- const conn = runtime.getConnection();
504
- if (conn) {
505
- try {
506
- // Send default message with isFinal=true
507
- await conn.sendResponse({
508
- sessionId: timeoutSessionId,
509
- messageId: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
510
- timestamp: Date.now(),
511
- agentId: messageHandlerAgentId,
512
- sender: { id: messageHandlerAgentId, name: "OpenClaw Agent", type: "agent" },
513
- content: { type: "text", text: "任务还在处理中,完成后将提醒您~" },
514
- status: "success",
515
- }, timeoutTaskId, timeoutSessionId, true, false); // isFinal=true
516
- console.log(`[TASK TIMEOUT] Default message sent (isFinal=true) to session ${timeoutSessionId}\n`);
517
- }
518
- catch (error) {
519
- console.error(`[TASK TIMEOUT] Failed to send default message:`, error);
520
- }
521
- }
522
- // Cancel 60-second periodic timeout
523
- runtime.clearSessionTimeout(timeoutSessionId);
524
- // Mark as waiting for push state
525
- runtime.markSessionWaitingForPush(timeoutSessionId, timeoutTaskId);
526
- };
527
- };
528
- // Start 1-hour task timeout timer
529
- runtime.setTaskTimeoutForSession(sessionId, currentTaskId, createTaskTimeoutHandler());
530
- // Also start 60-second periodic timeout for status updates (for messages before 1-hour timeout)
531
- const timeoutConfig = runtime.getTimeoutConfig();
532
- const createPeriodicTimeoutHandler = () => {
533
- return async () => {
534
- // Skip if already waiting for push (1-hour timeout already triggered)
535
- if (runtime.isSessionWaitingForPush(sessionId, currentTaskId)) {
536
- return;
537
- }
538
- const elapsed = Date.now() - startTime;
539
- console.log("\n" + "=".repeat(60));
540
- console.log(`[TIMEOUT] Periodic timeout triggered for session ${sessionId}`);
541
- console.log(` Elapsed: ${elapsed}ms`);
542
- console.log("=".repeat(60) + "\n");
543
- const conn = runtime.getConnection();
544
- if (conn) {
545
- try {
546
- await conn.sendStatusUpdate(currentTaskId, sessionId, timeoutConfig.message);
547
- console.log(`[TIMEOUT] Status update sent successfully to session ${sessionId}\n`);
548
- }
549
- catch (error) {
550
- console.error(`[TIMEOUT] Failed to send status update:`, error);
551
- }
552
- }
553
- };
554
- };
555
- runtime.setTimeoutForSession(sessionId, createPeriodicTimeoutHandler());
556
- // ==================== END TASK TIMEOUT PROTECTION ====================
557
- // ==================== CREATE STREAMING DISPATCHER ====================
558
- // ==================== DISPATCHER OPTIONS ====================
559
- // Define dispatcher options for dispatchInboundMessageWithBufferedDispatcher
560
- // This uses the standard OpenClaw pattern which properly handles dispatcher lifecycle
561
- const dispatcherOptions = {
562
- humanDelay: 0,
563
- onReplyStart: async () => {
564
- const elapsed = Date.now() - startTime;
565
- console.log("\n" + "=".repeat(60));
566
- console.log("XiaoYi: [START] Reply started after " + elapsed + "ms");
567
- console.log(" Session: " + sessionId);
568
- console.log(" Task ID: " + currentTaskId);
569
- console.log("=".repeat(60) + "\n");
570
- // Send immediate status update to let user know Agent is working
571
- const conn = runtime.getConnection();
572
- if (conn) {
573
- try {
574
- await conn.sendStatusUpdate(currentTaskId, sessionId, "任务正在处理中,请稍后");
575
- console.log("✓ [START] Initial status update sent\n");
576
- }
577
- catch (error) {
578
- console.error("✗ [START] Failed to send initial status update:", error);
579
- }
580
- }
581
- },
582
- deliver: async (payload, info) => {
583
- const elapsed = Date.now() - startTime;
584
- const text = payload.text || "";
585
- const kind = info.kind;
586
- const payloadStatus = payload.status;
587
- // IMPORTANT: Check if this is actually the final message
588
- // Check multiple sources: payload.status, payload.queuedFinal, AND info.kind
589
- // info.kind is the most reliable indicator for final messages
590
- const isFinal = payloadStatus === "final" || payload.queuedFinal === true || kind === "final";
591
- // If session is waiting for push (1-hour timeout occurred), ignore non-final responses
592
- if (runtime.isSessionWaitingForPush(sessionId, currentTaskId) && !payload.queuedFinal && info.kind !== "final") {
593
- console.log(`[TASK TIMEOUT] Ignoring non-final response for session ${sessionId} (already timed out)`);
594
- return;
595
- }
596
- accumulatedText = text;
597
- console.log("\n" + "█".repeat(70));
598
- console.log("📨 [DELIVER] Payload received");
599
- console.log(" Session: " + sessionId);
600
- console.log(" Elapsed: " + elapsed + "ms");
601
- console.log(" Info Kind: \"" + kind + "\"");
602
- console.log(" Payload Status: \"" + (payloadStatus || "unknown") + "\"");
603
- console.log(" Is Final: " + isFinal);
604
- console.log(" Text length: " + text.length + " chars");
605
- console.log(" Sent so far: " + sentTextLength + " chars");
606
- if (text.length > 0) {
607
- console.log(" Text preview: \"" + text.substring(0, 80) + (text.length > 80 ? "..." : "") + "\"");
608
- }
609
- console.log("█".repeat(70) + "\n");
610
- // Only check for abort, NOT timeout
611
- // Timeout is just for user notification, final responses should still be delivered
612
- if (runtime.isSessionAborted(sessionId)) {
613
- console.log("\n" + "=".repeat(60));
614
- console.log("[ABORT] Response received AFTER abort");
615
- console.log(" Session: " + sessionId);
616
- console.log(" Action: DISCARDING");
617
- console.log("=".repeat(60) + "\n");
618
- return;
619
- }
620
- // NOTE: We DON'T check timeout here anymore
621
- // Even if timeout occurred, we should still deliver the final response
622
- // Timeout was just to keep user informed, not to discard results
623
- const conn = runtime.getConnection();
624
- if (!conn) {
625
- console.error("✗ XiaoYi: Connection not available\n");
626
- return;
627
- }
628
- // ==================== FIX: Empty text handling ====================
629
- // If text is empty but this is not final, ALWAYS send a status update
630
- // This ensures user gets feedback for EVERY Agent activity (tool calls, subagent calls, etc.)
631
- if ((!text || text.length === 0) && !isFinal) {
632
- console.log("\n" + "=".repeat(60));
633
- console.log("[STREAM] Empty " + kind + " response detected");
634
- console.log(" Session: " + sessionId);
635
- console.log(" Action: Sending status update (no throttling)");
636
- console.log("=".repeat(60) + "\n");
637
- try {
638
- await conn.sendStatusUpdate(currentTaskId, sessionId, "任务正在处理中,请稍后");
639
- console.log("✓ Status update sent\n");
640
- }
641
- catch (error) {
642
- console.error("✗ Failed to send status update:", error);
643
- }
644
- return;
645
- }
646
- // ==================== END FIX ====================
647
- const responseStatus = isFinal ? "success" : "processing";
648
- const incrementalText = text.slice(sentTextLength);
649
- // ==================== FIX: Always send isFinal=false in deliver ====================
650
- // All responses from deliver callback are sent with isFinal=false
651
- // The final isFinal=true will be sent in onIdle callback when ALL processing is complete
652
- const shouldSendFinal = false;
653
- // Always use append=true for all responses
654
- const isAppend = true;
655
- if (incrementalText.length > 0 || isFinal) {
656
- console.log("\n" + "-".repeat(60));
657
- console.log("XiaoYi: [STREAM] Sending response");
658
- console.log(" Response Status: " + responseStatus);
659
- console.log(" Is Final: " + isFinal);
660
- console.log(" Is Append: " + isAppend);
661
- console.log("-".repeat(60) + "\n");
662
- const response = {
663
- sessionId: sessionId,
664
- messageId: "msg_" + Date.now() + "_" + Math.random().toString(36).substr(2, 9),
665
- timestamp: Date.now(),
666
- agentId: messageHandlerAgentId, // Use stored value instead of resolvedAccount.config.agentId
667
- sender: {
668
- id: messageHandlerAgentId, // Use stored value instead of resolvedAccount.config.agentId
669
- name: "OpenClaw Agent",
670
- type: "agent",
671
- },
672
- content: {
673
- type: "text",
674
- text: isFinal ? text : incrementalText,
675
- },
676
- status: responseStatus,
677
- };
678
- try {
679
- await conn.sendResponse(response, currentTaskId, sessionId, shouldSendFinal, isAppend);
680
- console.log("✓ Sent (status=" + responseStatus + ", isFinal=false, append=" + isAppend + ")\n");
681
- }
682
- catch (error) {
683
- console.error("✗ Failed to send:", error);
684
- }
685
- sentTextLength = text.length;
686
- }
687
- // ==================== FIX: SubAgent-friendly cleanup logic ====================
688
- // Only mark session as completed if we're truly done (no more subagent responses expected)
689
- // The key insight: we should NOT cleanup on every "final" payload, because subagents
690
- // can generate additional responses after the main agent returns "final".
691
- //
692
- // Instead, we let onIdle handle the cleanup, which is called after ALL processing is done.
693
- if (isFinal) {
694
- // Clear timeout but DON'T mark session as completed yet
695
- // SubAgent might still send more responses
696
- runtime.clearSessionTimeout(sessionId);
697
- console.log("[CLEANUP] Final payload received, but NOT marking session completed yet (waiting for onIdle)\n");
698
- }
699
- // ==================== END FIX ====================
700
- },
701
- onError: (err, info) => {
702
- console.error("\n" + "=".repeat(60));
703
- console.error("XiaoYi: [ERROR] " + info.kind + " failed: " + String(err));
704
- console.log("=".repeat(60) + "\n");
705
- runtime.clearSessionTimeout(sessionId);
706
- runtime.clearTaskTimeoutForSession(sessionId);
707
- runtime.clearSessionWaitingForPush(sessionId, currentTaskId);
708
- runtime.clearAbortControllerForSession(sessionId);
709
- // Check if session was cleared
710
- const conn = runtime.getConnection();
711
- if (conn && conn.isSessionPendingCleanup(sessionId)) {
712
- conn.forceCleanupSession(sessionId);
713
- }
714
- runtime.markSessionCompleted(sessionId);
715
- },
716
- onIdle: async () => {
717
- const elapsed = Date.now() - startTime;
718
- console.log("\n" + "=".repeat(60));
719
- console.log("XiaoYi: [IDLE] Processing complete");
720
- console.log(" Total time: " + elapsed + "ms");
721
- console.log("=".repeat(60) + "\n");
722
- // Clear 1-hour task timeout timer
723
- runtime.clearTaskTimeoutForSession(sessionId);
724
- // ==================== CHECK IF SESSION WAS CLEARED ====================
725
- const conn = runtime.getConnection();
726
- const isPendingCleanup = conn && conn.isSessionPendingCleanup(sessionId);
727
- const isWaitingForPush = runtime.isSessionWaitingForPush(sessionId, currentTaskId);
728
- // ==================== PUSH NOTIFICATION LOGIC ====================
729
- // Send push if task timeout was triggered (regardless of session cleanup status)
730
- // This ensures users get notified when long-running tasks complete
731
- if (isWaitingForPush && accumulatedText.length > 0) {
732
- const pushReason = isPendingCleanup
733
- ? `Session ${sessionId} was cleared`
734
- : `Session ${sessionId} task timeout triggered`;
735
- console.log(`[CLEANUP] ${pushReason}, sending push notification`);
736
- try {
737
- const { XiaoYiPushService } = require("./push");
738
- const pushService = new XiaoYiPushService(messageHandlerConfig);
739
- if (pushService.isConfigured()) {
740
- // Generate summary
741
- const summary = accumulatedText.length > 30
742
- ? accumulatedText.substring(0, 30) + "..."
743
- : accumulatedText;
744
- await pushService.sendPush(summary, "后台任务已完成:" + summary);
745
- console.log("✓ [CLEANUP] Push notification sent\n");
746
- // Clear push waiting state for this specific task
747
- runtime.clearSessionWaitingForPush(sessionId, currentTaskId);
748
- }
749
- else {
750
- console.log("[CLEANUP] Push not configured, skipping notification");
751
- runtime.clearSessionWaitingForPush(sessionId, currentTaskId);
752
- }
753
- }
754
- catch (error) {
755
- console.error("[CLEANUP] Error sending push:", error);
756
- runtime.clearSessionWaitingForPush(sessionId, currentTaskId);
757
- }
758
- // If session was cleared, update cleanup state
759
- if (isPendingCleanup) {
760
- conn?.updateAccumulatedTextForCleanup(sessionId, accumulatedText);
761
- conn?.forceCleanupSession(sessionId);
762
- }
763
- }
764
- // ==================== NORMAL WEBSOCKET FLOW (no timeout triggered) ====================
765
- else if (!isPendingCleanup) {
766
- // Normal flow: send WebSocket response (no timeout, session still active)
767
- const conn = runtime.getConnection();
768
- if (conn) {
769
- try {
770
- await conn.sendResponse({
771
- sessionId: sessionId,
772
- messageId: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
773
- timestamp: Date.now(),
774
- agentId: messageHandlerAgentId,
775
- sender: {
776
- id: messageHandlerAgentId,
777
- name: "OpenClaw Agent",
778
- type: "agent",
779
- },
780
- content: {
781
- type: "text",
782
- text: accumulatedText,
783
- },
784
- status: "success",
785
- }, currentTaskId, sessionId, true, true); // isFinal=true, append=true
786
- console.log("✓ [IDLE] Final response sent (isFinal=true)\n");
787
- }
788
- catch (error) {
789
- console.error("✗ [IDLE] Failed to send final response:", error);
790
- }
791
- }
792
- }
793
- // ==================== SESSION CLEARED BUT NO TIMEOUT ====================
794
- else {
795
- // Session was cleared but no timeout triggered - edge case, just cleanup
796
- console.log(`[CLEANUP] Session ${sessionId} was cleared but no push needed`);
797
- conn?.forceCleanupSession(sessionId);
798
- }
799
- // This is called AFTER all processing is done (including subagents)
800
- // NOW we can safely mark the session as completed
801
- runtime.clearAbortControllerForSession(sessionId);
802
- runtime.markSessionCompleted(sessionId);
803
- console.log("[CLEANUP] Session marked as completed in onIdle\n");
804
- },
805
- };
806
- try {
807
- // Use standard OpenClaw pattern with dispatchReplyWithBufferedBlockDispatcher
808
- // This properly handles dispatcher lifecycle:
809
- // 1. Calls dispatcher.markComplete() after run() completes
810
- // 2. Waits for waitForIdle() to ensure all deliveries are done
811
- // 3. Then calls markDispatchIdle() in the finally block
812
- const result = await pluginRuntime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
813
- ctx: msgContext,
814
- cfg: config,
815
- dispatcherOptions: dispatcherOptions,
816
- replyOptions: {
817
- abortSignal: abortSignal,
818
- },
819
- });
820
- const { queuedFinal, counts } = result;
821
- console.log("\n" + "=".repeat(60));
822
- console.log("XiaoYi: [DISPATCH] Summary");
823
- console.log(" Queued Final: " + queuedFinal);
824
- if (counts && Object.keys(counts).length > 0) {
825
- console.log(" Counts:", JSON.stringify(counts, null, 2));
826
- }
827
- console.log("=".repeat(60) + "\n");
828
- // ==================== ANALYZE EXECUTION RESULT ====================
829
- // Check if Agent produced any output
830
- const hasAnyCounts = counts && ((counts.tool && counts.tool > 0) ||
831
- (counts.block && counts.block > 0) ||
832
- (counts.final && counts.final > 0));
833
- if (!hasAnyCounts) {
834
- // Scenario 1: No Agent output detected
835
- // This could mean:
836
- // a) SubAgent running in background (main Agent returned)
837
- // b) Concurrent request (another Agent already running on this session)
838
- console.log("\n" + "=".repeat(60));
839
- console.log("[NO OUTPUT] Agent produced no output");
840
- console.log(" Session: " + sessionId);
841
- console.log(" Checking if there's another active Agent...");
842
- console.log("=".repeat(60) + "\n");
843
- // Check if there's an active Agent on this session
844
- // We use the existence of deliver callback triggers as an indicator
845
- // If the dispatcher's onIdle will be called later, an Agent is still running
846
- const conn = runtime.getConnection();
847
- if (conn) {
848
- // IMPORTANT: Send a response to user for THIS request
849
- // User needs to know what's happening
850
- try {
851
- const response = {
852
- sessionId: sessionId,
853
- messageId: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
854
- timestamp: Date.now(),
855
- agentId: messageHandlerAgentId, // Use stored value instead of resolvedAccount.config.agentId
856
- sender: {
857
- id: messageHandlerAgentId, // Use stored value instead of resolvedAccount.config.agentId
858
- name: "OpenClaw Agent",
859
- type: "agent",
860
- },
861
- content: {
862
- type: "text",
863
- text: "任务正在处理中,请稍候...",
864
- },
865
- status: "success",
866
- };
867
- // Send response with isFinal=true to close THIS request
868
- await conn.sendResponse(response, currentTaskId, sessionId, true, true);
869
- console.log("✓ [NO OUTPUT] Response sent to user\n");
870
- }
871
- catch (error) {
872
- console.error("✗ [NO OUTPUT] Failed to send response:", error);
873
- }
874
- }
875
- // CRITICAL: Don't cleanup resources yet!
876
- // The original Agent might still be running and needs these resources
877
- // onIdle will be called when the original Agent completes
878
- console.log("[NO OUTPUT] Keeping resources alive for potential background Agent\n");
879
- // Note: No need to call markDispatchIdle() manually
880
- // dispatchInboundMessageWithBufferedDispatcher handles this in its finally block
881
- }
882
- else {
883
- // Scenario 2: Normal execution with output
884
- // - Agent produced output synchronously
885
- // - All cleanup is already handled in deliver/onIdle callbacks
886
- console.log("[NORMAL] Agent produced output, cleanup handled in callbacks");
887
- // Note: No need to call markDispatchIdle() manually
888
- // dispatchInboundMessageWithBufferedDispatcher handles this in its finally block
889
- }
890
- // ==================== END ANALYSIS ====================
891
- }
892
- catch (error) {
893
- console.error("XiaoYi: [ERROR] Error dispatching message:", error);
894
- // Clear timeout on error
895
- runtime.clearSessionTimeout(sessionId);
896
- runtime.clearTaskTimeoutForSession(sessionId);
897
- runtime.clearSessionWaitingForPush(sessionId, currentTaskId);
898
- // Clear abort controller on error
899
- runtime.clearAbortControllerForSession(sessionId);
900
- // Mark session as completed on error
901
- runtime.markSessionCompleted(sessionId);
902
- // Note: No need to call markDispatchIdle() manually
903
- // dispatchInboundMessageWithBufferedDispatcher handles this in its finally block
904
- }
905
- }
906
- catch (error) {
907
- console.error("XiaoYi: [ERROR] Unexpected error in message handler:", error);
908
- }
909
- });
910
- // Setup cancel handler
911
- // When tasks/cancel is received, abort the current session's agent run
912
- connection.on("cancel", async (data) => {
913
- const { sessionId } = data;
914
- console.log("\n" + "=".repeat(60));
915
- console.log(`XiaoYi: [CANCEL] Cancel event received`);
916
- console.log(` Session: ${sessionId}`);
917
- console.log(` Task ID: ${data.taskId || "N/A"}`);
918
- console.log("=".repeat(60) + "\n");
919
- // Abort the session's agent run
920
- const aborted = runtime.abortSession(sessionId);
921
- if (aborted) {
922
- console.log(`[CANCEL] Successfully triggered abort for session ${sessionId}`);
923
- }
924
- else {
925
- console.log(`[CANCEL] No active agent run found for session ${sessionId}`);
926
- }
927
- // Clear timeout and push state as the session is being canceled
928
- runtime.clearTaskTimeoutForSession(sessionId);
929
- runtime.clearSessionWaitingForPush(sessionId, data.taskId);
930
- runtime.markSessionCompleted(sessionId);
931
- });
932
- // Handle clear context events
933
- connection.on("clear", async (data) => {
934
- const { sessionId, serverId } = data;
935
- console.log("\n" + "=".repeat(60));
936
- console.log("[CLEAR] Context cleared by user");
937
- console.log(` Session: ${sessionId}`);
938
- console.log("=".repeat(60) + "\n");
939
- // Check if there's an active task for this session
940
- const hasActiveTask = runtime.isSessionActive(sessionId);
941
- if (hasActiveTask) {
942
- console.log(`[CLEAR] Active task exists for session ${sessionId}, will continue in background`);
943
- // Session is already marked for cleanup in websocket.ts
944
- // Just track that we're waiting for completion
945
- }
946
- else {
947
- console.log(`[CLEAR] No active task for session ${sessionId}, clean up immediately`);
948
- const conn = runtime.getConnection();
949
- if (conn) {
950
- conn.forceCleanupSession(sessionId);
951
- }
952
- }
953
- });
954
- // Mark handlers as registered to prevent duplicate registration
955
- handlersRegistered = true;
956
- }
957
- else {
958
- console.log("XiaoYi: [STARTUP] Handlers already registered, skipping duplicate registration");
959
- }
960
- console.log("XiaoYi: Event handlers registered");
961
- // Keep the channel running by waiting for the abort signal
962
- // This prevents the Promise from resolving, keeping 'running' status as true
963
- // The channel will stop when stopAccount() is called or the abort signal is triggered
964
- await new Promise((resolve) => {
965
- ctx.abortSignal.addEventListener("abort", () => {
966
- console.log("XiaoYi: abort signal received, stopping channel");
967
- resolve();
968
- }, { once: true });
969
- // Also handle case where abort is already triggered
970
- if (ctx.abortSignal.aborted) {
971
- console.log("XiaoYi: abort signal already triggered");
972
- resolve();
973
- }
974
- });
975
- console.log("XiaoYi: startAccount() exiting - END");
976
- },
977
- stopAccount: async (ctx) => {
978
- const runtime = (0, runtime_1.getXiaoYiRuntime)();
979
- runtime.stop();
287
+ sendMedia: async (ctx) => {
288
+ throw new Error("暂不支持文件回传");
980
289
  },
981
290
  },
982
291
  /**
983
292
  * Messaging adapter - normalize targets
293
+ * In new openclaw version, normalizeTarget receives a string and returns a normalized string
984
294
  */
985
295
  messaging: {
986
- normalizeTarget: async (ctx) => {
296
+ normalizeTarget: (raw) => {
987
297
  // For XiaoYi, we use sessionId as the target
988
- // The sessionId comes from the incoming message's meta
989
- return ctx.to;
298
+ // The raw input is already the normalized target (sessionId)
299
+ return raw;
990
300
  },
991
301
  },
992
302
  /**
993
303
  * Status adapter - health checks
304
+ * Using buildAccountSnapshot for compatibility with new openclaw version
994
305
  */
995
306
  status: {
996
- getAccountStatus: async (ctx) => {
997
- const runtime = (0, runtime_1.getXiaoYiRuntime)();
307
+ buildAccountSnapshot: async (params) => {
308
+ const runtime = (0, runtime_js_1.getXiaoYiRuntime)();
998
309
  const connection = runtime.getConnection();
999
310
  if (!connection) {
1000
311
  return {
1001
- status: "offline",
1002
- message: "Not connected",
312
+ accountId: params.account.accountId,
313
+ state: "offline",
314
+ lastEventAt: Date.now(),
315
+ issues: [{
316
+ severity: "error",
317
+ message: "Not connected",
318
+ }],
1003
319
  };
1004
320
  }
1005
321
  const state = connection.getState();
1006
322
  if (state.connected && state.authenticated) {
1007
323
  return {
1008
- status: "online",
1009
- message: "Connected and authenticated",
324
+ accountId: params.account.accountId,
325
+ state: "ready",
326
+ lastEventAt: Date.now(),
327
+ lastInboundAt: Date.now(),
1010
328
  };
1011
329
  }
1012
330
  else if (state.connected) {
1013
331
  return {
1014
- status: "connecting",
1015
- message: "Connected but not authenticated",
332
+ accountId: params.account.accountId,
333
+ state: "authenticating",
334
+ lastEventAt: Date.now(),
335
+ issues: [{
336
+ severity: "warning",
337
+ message: "Connected but not authenticated",
338
+ }],
1016
339
  };
1017
340
  }
1018
341
  else {
1019
342
  return {
1020
- status: "offline",
1021
- message: `Reconnect attempts: ${state.reconnectAttempts}/${state.maxReconnectAttempts}`,
343
+ accountId: params.account.accountId,
344
+ state: "offline",
345
+ lastEventAt: Date.now(),
346
+ issues: [{
347
+ severity: "error",
348
+ message: `Reconnect attempts: ${state.reconnectAttempts}/${state.maxReconnectAttempts}`,
349
+ }],
1022
350
  };
1023
351
  }
1024
352
  },