@ynhcj/xiaoyi 2.5.5 → 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.
Files changed (43) hide show
  1. package/dist/auth.d.ts +1 -1
  2. package/dist/channel.d.ts +116 -14
  3. package/dist/channel.js +199 -665
  4. package/dist/config-schema.d.ts +8 -8
  5. package/dist/config-schema.js +5 -5
  6. package/dist/file-download.d.ts +17 -0
  7. package/dist/file-download.js +69 -0
  8. package/dist/heartbeat.d.ts +39 -0
  9. package/dist/heartbeat.js +102 -0
  10. package/dist/index.d.ts +1 -4
  11. package/dist/index.js +7 -11
  12. package/dist/push.d.ts +28 -0
  13. package/dist/push.js +135 -0
  14. package/dist/runtime.d.ts +48 -2
  15. package/dist/runtime.js +117 -3
  16. package/dist/types.d.ts +95 -1
  17. package/dist/websocket.d.ts +49 -1
  18. package/dist/websocket.js +279 -20
  19. package/dist/xy-bot.d.ts +19 -0
  20. package/dist/xy-bot.js +277 -0
  21. package/dist/xy-client.d.ts +26 -0
  22. package/dist/xy-client.js +78 -0
  23. package/dist/xy-config.d.ts +18 -0
  24. package/dist/xy-config.js +37 -0
  25. package/dist/xy-formatter.d.ts +94 -0
  26. package/dist/xy-formatter.js +303 -0
  27. package/dist/xy-monitor.d.ts +17 -0
  28. package/dist/xy-monitor.js +187 -0
  29. package/dist/xy-parser.d.ts +49 -0
  30. package/dist/xy-parser.js +109 -0
  31. package/dist/xy-reply-dispatcher.d.ts +17 -0
  32. package/dist/xy-reply-dispatcher.js +308 -0
  33. package/dist/xy-tools/session-manager.d.ts +29 -0
  34. package/dist/xy-tools/session-manager.js +80 -0
  35. package/dist/xy-utils/config-manager.d.ts +26 -0
  36. package/dist/xy-utils/config-manager.js +61 -0
  37. package/dist/xy-utils/crypto.d.ts +8 -0
  38. package/dist/xy-utils/crypto.js +21 -0
  39. package/dist/xy-utils/logger.d.ts +6 -0
  40. package/dist/xy-utils/logger.js +37 -0
  41. package/dist/xy-utils/session.d.ts +34 -0
  42. package/dist/xy-utils/session.js +55 -0
  43. package/package.json +32 -16
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
@@ -32,7 +67,64 @@ exports.xiaoyiPlugin = {
32
67
  media: true,
33
68
  nativeCommands: false,
34
69
  },
35
- onboarding: onboarding_1.xiaoyiOnboardingAdapter,
70
+ /**
71
+ * Config schema for UI form rendering
72
+ */
73
+ configSchema: {
74
+ schema: {
75
+ type: "object",
76
+ properties: {
77
+ enabled: {
78
+ type: "boolean",
79
+ default: false,
80
+ description: "Enable XiaoYi channel",
81
+ },
82
+ wsUrl1: {
83
+ type: "string",
84
+ default: "wss://hag.cloud.huawei.com/openclaw/v1/ws/link",
85
+ description: "Primary WebSocket server URL",
86
+ },
87
+ wsUrl2: {
88
+ type: "string",
89
+ default: "wss://116.63.174.231/openclaw/v1/ws/link",
90
+ description: "Secondary WebSocket server URL",
91
+ },
92
+ ak: {
93
+ type: "string",
94
+ description: "Access Key",
95
+ },
96
+ sk: {
97
+ type: "string",
98
+ description: "Secret Key",
99
+ },
100
+ agentId: {
101
+ type: "string",
102
+ description: "Agent ID",
103
+ },
104
+ debug: {
105
+ type: "boolean",
106
+ default: false,
107
+ description: "Enable debug logging",
108
+ },
109
+ apiId: {
110
+ type: "string",
111
+ default: "",
112
+ description: "API ID for push notifications",
113
+ },
114
+ pushId: {
115
+ type: "string",
116
+ default: "",
117
+ description: "Push ID for push notifications",
118
+ },
119
+ taskTimeoutMs: {
120
+ type: "number",
121
+ default: 3600000,
122
+ description: "Task timeout in milliseconds (default: 1 hour)",
123
+ },
124
+ },
125
+ },
126
+ },
127
+ onboarding: onboarding_js_1.xiaoyiOnboardingAdapter,
36
128
  /**
37
129
  * Config adapter - single account mode
38
130
  */
@@ -110,709 +202,151 @@ exports.xiaoyiPlugin = {
110
202
  }),
111
203
  },
112
204
  /**
113
- * 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
114
232
  */
115
233
  outbound: {
116
234
  deliveryMode: "direct",
117
235
  textChunkLimit: 4000,
118
- sendText: async (ctx) => {
119
- const runtime = (0, runtime_1.getXiaoYiRuntime)();
120
- const connection = runtime.getConnection();
121
- if (!connection || !connection.isReady()) {
122
- 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 };
123
240
  }
124
- // Get account config to retrieve agentId
125
- const resolvedAccount = ctx.account;
126
- const agentId = resolvedAccount.config.agentId;
127
- // Use 'to' as sessionId (it's set from incoming message's sessionId)
128
- const sessionId = ctx.to;
129
- // Get taskId from runtime's session mapping (must exist - from original A2A request)
130
- const taskId = runtime.getTaskIdForSession(sessionId);
131
- if (!taskId) {
132
- 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}"`);
133
251
  }
134
- // Build A2A response message
135
- const response = {
136
- sessionId: sessionId,
137
- messageId: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
138
- timestamp: Date.now(),
139
- agentId: agentId,
140
- sender: {
141
- id: agentId,
142
- name: "OpenClaw Agent",
143
- type: "agent",
144
- },
145
- content: {
146
- type: "text",
147
- text: ctx.text,
148
- },
149
- context: ctx.replyToId ? {
150
- replyToMessageId: ctx.replyToId,
151
- } : undefined,
152
- status: "success",
153
- };
154
- // Send via WebSocket with taskId and sessionId
155
- await connection.sendResponse(response, taskId, sessionId);
156
- return {
157
- channel: "xiaoyi",
158
- messageId: response.messageId,
159
- conversationId: sessionId,
160
- timestamp: response.timestamp,
161
- };
252
+ return { ok: true, to: trimmedTo };
162
253
  },
163
- sendMedia: async (ctx) => {
164
- const runtime = (0, runtime_1.getXiaoYiRuntime)();
165
- const connection = runtime.getConnection();
166
- if (!connection || !connection.isReady()) {
167
- 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 || "";
168
265
  }
169
- const resolvedAccount = ctx.account;
170
- const agentId = resolvedAccount.config.agentId;
171
- // Use 'to' as sessionId
172
- const sessionId = ctx.to;
173
- // Get taskId from runtime's session mapping (must exist - from original A2A request)
174
- const taskId = runtime.getTaskIdForSession(sessionId);
175
- if (!taskId) {
176
- 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];
177
268
  }
178
- // Build A2A response message with media
179
- const response = {
180
- sessionId: sessionId,
181
- messageId: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
182
- timestamp: Date.now(),
183
- agentId: agentId,
184
- sender: {
185
- id: agentId,
186
- name: "OpenClaw Agent",
187
- type: "agent",
188
- },
189
- content: {
190
- type: "image", // Assume image for now, could be extended
191
- text: ctx.text,
192
- mediaUrl: ctx.mediaUrl,
193
- },
194
- context: ctx.replyToId ? {
195
- replyToMessageId: ctx.replyToId,
196
- } : undefined,
197
- status: "success",
198
- };
199
- 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`);
200
281
  return {
201
282
  channel: "xiaoyi",
202
- messageId: response.messageId,
203
- conversationId: sessionId,
204
- timestamp: response.timestamp,
283
+ messageId: Date.now().toString(),
284
+ chatId: actualTo,
205
285
  };
206
286
  },
207
- },
208
- /**
209
- * Gateway adapter - manage connections
210
- */
211
- gateway: {
212
- startAccount: async (ctx) => {
213
- console.log("XiaoYi: startAccount() called - START");
214
- const runtime = (0, runtime_1.getXiaoYiRuntime)();
215
- const resolvedAccount = ctx.account;
216
- const config = ctx.cfg;
217
- // Start WebSocket connection (single account mode)
218
- // Wrap in try-catch to prevent startup errors from causing auto-restart
219
- let connection = null;
220
- try {
221
- await runtime.start(resolvedAccount.config);
222
- connection = runtime.getConnection();
223
- }
224
- catch (error) {
225
- console.error("XiaoYi: [STARTUP] Failed to start WebSocket connection:", error);
226
- // Don't throw - let the connection retry logic handle reconnection
227
- // The runtime.start() will handle reconnection internally
228
- }
229
- // Setup message handler IMMEDIATELY after connection is established
230
- if (!connection) {
231
- connection = runtime.getConnection();
232
- }
233
- if (!connection) {
234
- console.warn("XiaoYi: [STARTUP] No WebSocket connection available yet, will retry...");
235
- // Throw error to prevent auto-restart - let runtime handle reconnection
236
- // The runtime.start() will keep trying to reconnect internally
237
- throw new Error("XiaoYi: WebSocket connection not available, runtime will retry");
238
- }
239
- // Only register handlers once to prevent duplicate message processing
240
- // when startAccount() is called multiple times due to auto-restart attempts
241
- if (!handlersRegistered) {
242
- console.log("XiaoYi: [STARTUP] Registering message and cancel handlers");
243
- // Setup message handler with try-catch to prevent individual message errors from crashing the channel
244
- connection.on("message", async (message) => {
245
- // CRITICAL: Use dynamic require to get the latest runtime module after hot-reload
246
- const { getXiaoYiRuntime } = require("./runtime");
247
- const runtime = getXiaoYiRuntime();
248
- console.log(`XiaoYi: [Message Handler] Using runtime instance: ${runtime.getInstanceId()}`);
249
- // CRITICAL FIX: Extract and store config values at message handler level
250
- // This prevents "Cannot read properties of undefined" errors in concurrent scenarios
251
- // where the outer scope's resolvedAccount might become unavailable
252
- const messageHandlerAgentId = resolvedAccount.config?.agentId;
253
- const messageHandlerAccountId = resolvedAccount.accountId;
254
- if (!messageHandlerAgentId) {
255
- console.error("XiaoYi: [FATAL] agentId not available in resolvedAccount.config");
256
- return;
257
- }
258
- console.log(`XiaoYi: [Message Handler] Stored config values - agentId: ${messageHandlerAgentId}, accountId: ${messageHandlerAccountId}`);
259
- // For message/stream, prioritize params.sessionId, fallback to top-level sessionId
260
- const sessionId = message.params?.sessionId || message.sessionId;
261
- // Validate sessionId exists
262
- if (!sessionId) {
263
- console.error("XiaoYi: Missing sessionId in message, cannot process");
264
- return;
265
- }
266
- // Get PluginRuntime from our runtime wrapper
267
- const pluginRuntime = runtime.getPluginRuntime();
268
- if (!pluginRuntime) {
269
- console.error("PluginRuntime not available");
270
- return;
271
- }
272
- // Extract text, file, and image content from parts array
273
- let bodyText = "";
274
- let fileAttachments = [];
275
- const mediaFiles = [];
276
- for (const part of message.params.message.parts) {
277
- if (part.kind === "text" && part.text) {
278
- // Handle text content
279
- bodyText += part.text;
280
- }
281
- else if (part.kind === "file" && part.file) {
282
- // Handle file content
283
- const { uri, mimeType, name } = part.file;
284
- if (!uri) {
285
- console.warn(`XiaoYi: File part without URI, skipping: ${name}`);
286
- continue;
287
- }
288
- try {
289
- // All files are downloaded to local disk and passed to OpenClaw
290
- // No type validation - let Agent decide how to handle them
291
- console.log(`XiaoYi: Processing file: ${name} (${mimeType})`);
292
- mediaFiles.push({ uri, mimeType, name });
293
- // For text-based files, also extract content inline
294
- if ((0, xiaoyi_media_1.isTextMimeType)(mimeType)) {
295
- try {
296
- const textContent = await (0, xiaoyi_media_1.extractTextFromUrl)(uri, 5000000, 30000);
297
- bodyText += `\n\n[文件内容: ${name}]\n${textContent}`;
298
- fileAttachments.push(`[文件: ${name}]`);
299
- console.log(`XiaoYi: Successfully extracted text from: ${name}`);
300
- }
301
- catch (textError) {
302
- // Text extraction failed, but file is still in mediaFiles
303
- console.warn(`XiaoYi: Text extraction failed for ${name}, will download as binary`);
304
- fileAttachments.push(`[文件: ${name}]`);
305
- }
306
- }
307
- else {
308
- // Binary files (images, pdf, office docs, etc.)
309
- fileAttachments.push(`[文件: ${name}]`);
310
- }
311
- }
312
- catch (error) {
313
- const errorMsg = error instanceof Error ? error.message : String(error);
314
- console.error(`XiaoYi: Failed to process file ${name}: ${errorMsg}`);
315
- fileAttachments.push(`[文件处理失败: ${name} - ${errorMsg}]`);
316
- }
317
- }
318
- // Ignore kind: "data" as per user request
319
- }
320
- // Log summary of processed attachments
321
- if (fileAttachments.length > 0) {
322
- console.log(`XiaoYi: Processed ${fileAttachments.length} file(s): ${fileAttachments.join(", ")}`);
323
- }
324
- // Download media files to local disk (like feishu does)
325
- let mediaPayload = {};
326
- if (mediaFiles.length > 0) {
327
- console.log(`XiaoYi: Downloading ${mediaFiles.length} media file(s) to local disk...`);
328
- const downloadedMedia = await (0, xiaoyi_media_1.downloadAndSaveMediaList)(pluginRuntime, mediaFiles, { maxBytes: 30000000, timeoutMs: 60000 });
329
- console.log(`XiaoYi: Successfully downloaded ${downloadedMedia.length}/${mediaFiles.length} file(s)`);
330
- mediaPayload = (0, xiaoyi_media_1.buildXiaoYiMediaPayload)(downloadedMedia);
331
- }
332
- // Determine sender ID from role
333
- const senderId = message.params.message.role === "user" ? "user" : message.agentId;
334
- // Build MsgContext for OpenClaw's message pipeline
335
- // Include media payload so OpenClaw can access local file paths
336
- const msgContext = {
337
- Body: bodyText,
338
- From: senderId,
339
- To: sessionId,
340
- SessionKey: `xiaoyi:${resolvedAccount.accountId}:${sessionId}`,
341
- AccountId: resolvedAccount.accountId,
342
- MessageSid: message.id, // Use top-level id as message sequence number
343
- Timestamp: Date.now(), // Generate timestamp since new format doesn't include it
344
- Provider: "xiaoyi",
345
- Surface: "xiaoyi",
346
- ChatType: "direct",
347
- SenderName: message.params.message.role, // Use role as sender name
348
- SenderId: senderId,
349
- OriginatingChannel: "xiaoyi",
350
- ...mediaPayload, // Spread MediaPath, MediaPaths, MediaType, MediaTypes
351
- };
352
- // Log the message context for debugging
353
- console.log("\n" + "=".repeat(60));
354
- console.log("XiaoYi: [DEBUG] Message Context");
355
- console.log(" " + JSON.stringify({
356
- Body: msgContext.Body.substring(0, 50) + "...",
357
- From: msgContext.From,
358
- To: msgContext.To,
359
- SessionKey: msgContext.SessionKey,
360
- AccountId: msgContext.AccountId,
361
- Provider: msgContext.Provider,
362
- Surface: msgContext.Surface,
363
- MediaPath: msgContext.MediaPath,
364
- MediaPaths: msgContext.MediaPaths,
365
- MediaType: msgContext.MediaType,
366
- }, null, 2));
367
- console.log("=".repeat(60) + "\n");
368
- // Dispatch message using OpenClaw's reply dispatcher
369
- try {
370
- console.log("\n" + "=".repeat(60));
371
- console.log(`XiaoYi: [MESSAGE] Processing user message`);
372
- console.log(` Session: ${sessionId}`);
373
- console.log(` Task ID: ${message.params.id}`);
374
- console.log(` User input: ${bodyText.substring(0, 50)}${bodyText.length > 50 ? "..." : ""}`);
375
- console.log(` Images: ${mediaFiles.length}`);
376
- console.log("=".repeat(60) + "\n");
377
- // Get taskId from this message's params.id
378
- // NOTE: We store this AFTER concurrent check to avoid overwriting active task's taskId
379
- const currentTaskId = message.params.id;
380
- // ==================== CONCURRENT REQUEST DETECTION ====================
381
- // Check if this session already has an active agent run
382
- // If so, send an immediate "busy" response and skip processing
383
- if (runtime.isSessionActive(sessionId)) {
384
- console.log("\n" + "=".repeat(60));
385
- console.log(`[CONCURRENT] Session ${sessionId} has an active agent run`);
386
- console.log(` Action: Sending busy response and skipping message`);
387
- console.log("=".repeat(60) + "\n");
388
- const conn = runtime.getConnection();
389
- if (conn) {
390
- try {
391
- await conn.sendResponse({
392
- sessionId: sessionId,
393
- messageId: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
394
- timestamp: Date.now(),
395
- agentId: messageHandlerAgentId,
396
- sender: {
397
- id: messageHandlerAgentId,
398
- name: "OpenClaw Agent",
399
- type: "agent",
400
- },
401
- content: {
402
- type: "text",
403
- text: "上一个任务仍在处理中,请稍后再试",
404
- },
405
- status: "success",
406
- }, currentTaskId, sessionId, true, false);
407
- console.log(`[CONCURRENT] Busy response sent to session ${sessionId}\n`);
408
- }
409
- catch (error) {
410
- console.error(`[CONCURRENT] Failed to send busy response:`, error);
411
- }
412
- }
413
- return; // Skip processing this concurrent request
414
- }
415
- // =================================================================
416
- // Store sessionId -> taskId mapping (only after passing concurrent check)
417
- runtime.setTaskIdForSession(sessionId, currentTaskId);
418
- const startTime = Date.now();
419
- let accumulatedText = "";
420
- let sentTextLength = 0; // Track sent text length for streaming
421
- let hasSentFinal = false; // Track if final content has been sent (to prevent duplicate isFinal=true)
422
- // ==================== CREATE ABORT CONTROLLER ====================
423
- // Create AbortController for this session to allow cancelation
424
- const abortControllerResult = runtime.createAbortControllerForSession(sessionId);
425
- if (!abortControllerResult) {
426
- console.error(`[ERROR] Failed to create AbortController for session ${sessionId}`);
427
- return;
428
- }
429
- const { controller: abortController, signal: abortSignal } = abortControllerResult;
430
- // ================================================================
431
- // ==================== START TIMEOUT PROTECTION ====================
432
- // Start periodic 60-second timeout timer
433
- // Will trigger every 60 seconds until a response is received or session completes
434
- const timeoutConfig = runtime.getTimeoutConfig();
435
- console.log(`[TIMEOUT] Starting ${timeoutConfig.duration}ms periodic timeout protection for session ${sessionId}`);
436
- // Define periodic timeout handler (will be called every 60 seconds)
437
- const createTimeoutHandler = () => {
438
- return async () => {
439
- const elapsed = Date.now() - startTime;
440
- console.log("\n" + "=".repeat(60));
441
- console.log(`[TIMEOUT] Timeout triggered for session ${sessionId}`);
442
- console.log(` Elapsed: ${elapsed}ms`);
443
- console.log(` Task ID: ${currentTaskId}`);
444
- console.log("=".repeat(60) + "\n");
445
- const conn = runtime.getConnection();
446
- if (conn) {
447
- try {
448
- // Send status update to keep conversation active
449
- await conn.sendStatusUpdate(currentTaskId, sessionId, timeoutConfig.message);
450
- console.log(`[TIMEOUT] Status update sent successfully to session ${sessionId}\n`);
451
- }
452
- catch (error) {
453
- console.error(`[TIMEOUT] Failed to send status update:`, error);
454
- }
455
- }
456
- else {
457
- console.error(`[TIMEOUT] Connection not available, cannot send status update\n`);
458
- }
459
- // Note: Timeout will trigger again in 60 seconds if still active
460
- };
461
- };
462
- // Start periodic timeout
463
- runtime.setTimeoutForSession(sessionId, createTimeoutHandler());
464
- // ==================== END TIMEOUT PROTECTION ====================
465
- // ==================== CREATE STREAMING DISPATCHER ====================
466
- // Use createReplyDispatcherWithTyping for real-time streaming feedback
467
- const { dispatcher, replyOptions, markDispatchIdle } = pluginRuntime.channel.reply.createReplyDispatcherWithTyping({
468
- humanDelay: 0,
469
- onReplyStart: async () => {
470
- const elapsed = Date.now() - startTime;
471
- console.log("\n" + "=".repeat(60));
472
- console.log("XiaoYi: [START] Reply started after " + elapsed + "ms");
473
- console.log(" Session: " + sessionId);
474
- console.log(" Task ID: " + currentTaskId);
475
- console.log("=".repeat(60) + "\n");
476
- // Send immediate status update to let user know Agent is working
477
- const conn = runtime.getConnection();
478
- if (conn) {
479
- try {
480
- await conn.sendStatusUpdate(currentTaskId, sessionId, "任务正在处理中,请稍后");
481
- console.log("✓ [START] Initial status update sent\n");
482
- }
483
- catch (error) {
484
- console.error("✗ [START] Failed to send initial status update:", error);
485
- }
486
- }
487
- },
488
- deliver: async (payload, info) => {
489
- const elapsed = Date.now() - startTime;
490
- const text = payload.text || "";
491
- const kind = info.kind;
492
- const payloadStatus = payload.status;
493
- // IMPORTANT: Check if this is actually the final message
494
- // Check multiple sources: payload.status, payload.queuedFinal, AND info.kind
495
- // info.kind is the most reliable indicator for final messages
496
- const isFinal = payloadStatus === "final" || payload.queuedFinal === true || kind === "final";
497
- accumulatedText = text;
498
- console.log("\n" + "█".repeat(70));
499
- console.log("📨 [DELIVER] Payload received");
500
- console.log(" Session: " + sessionId);
501
- console.log(" Elapsed: " + elapsed + "ms");
502
- console.log(" Info Kind: \"" + kind + "\"");
503
- console.log(" Payload Status: \"" + (payloadStatus || "unknown") + "\"");
504
- console.log(" Is Final: " + isFinal);
505
- console.log(" Text length: " + text.length + " chars");
506
- console.log(" Sent so far: " + sentTextLength + " chars");
507
- if (text.length > 0) {
508
- console.log(" Text preview: \"" + text.substring(0, 80) + (text.length > 80 ? "..." : "") + "\"");
509
- }
510
- console.log("█".repeat(70) + "\n");
511
- // Only check for abort, NOT timeout
512
- // Timeout is just for user notification, final responses should still be delivered
513
- if (runtime.isSessionAborted(sessionId)) {
514
- console.log("\n" + "=".repeat(60));
515
- console.log("[ABORT] Response received AFTER abort");
516
- console.log(" Session: " + sessionId);
517
- console.log(" Action: DISCARDING");
518
- console.log("=".repeat(60) + "\n");
519
- return;
520
- }
521
- // NOTE: We DON'T check timeout here anymore
522
- // Even if timeout occurred, we should still deliver the final response
523
- // Timeout was just to keep user informed, not to discard results
524
- const conn = runtime.getConnection();
525
- if (!conn) {
526
- console.error("✗ XiaoYi: Connection not available\n");
527
- return;
528
- }
529
- // ==================== FIX: Empty text handling ====================
530
- // If text is empty but this is not final, ALWAYS send a status update
531
- // This ensures user gets feedback for EVERY Agent activity (tool calls, subagent calls, etc.)
532
- if ((!text || text.length === 0) && !isFinal) {
533
- console.log("\n" + "=".repeat(60));
534
- console.log("[STREAM] Empty " + kind + " response detected");
535
- console.log(" Session: " + sessionId);
536
- console.log(" Action: Sending status update (no throttling)");
537
- console.log("=".repeat(60) + "\n");
538
- try {
539
- await conn.sendStatusUpdate(currentTaskId, sessionId, "任务正在处理中,请稍后");
540
- console.log("✓ Status update sent\n");
541
- }
542
- catch (error) {
543
- console.error("✗ Failed to send status update:", error);
544
- }
545
- return;
546
- }
547
- // ==================== END FIX ====================
548
- const responseStatus = isFinal ? "success" : "processing";
549
- const incrementalText = text.slice(sentTextLength);
550
- const isAppend = !isFinal && incrementalText.length > 0;
551
- if (incrementalText.length > 0 || isFinal) {
552
- console.log("\n" + "-".repeat(60));
553
- console.log("XiaoYi: [STREAM] Sending response");
554
- console.log(" Response Status: " + responseStatus);
555
- console.log(" Is Final: " + isFinal);
556
- console.log(" Is Append: " + isAppend);
557
- console.log("-".repeat(60) + "\n");
558
- const response = {
559
- sessionId: sessionId,
560
- messageId: "msg_" + Date.now() + "_" + Math.random().toString(36).substr(2, 9),
561
- timestamp: Date.now(),
562
- agentId: messageHandlerAgentId, // Use stored value instead of resolvedAccount.config.agentId
563
- sender: {
564
- id: messageHandlerAgentId, // Use stored value instead of resolvedAccount.config.agentId
565
- name: "OpenClaw Agent",
566
- type: "agent",
567
- },
568
- content: {
569
- type: "text",
570
- text: isFinal ? text : incrementalText,
571
- },
572
- status: responseStatus,
573
- };
574
- // ==================== FIX: Prevent duplicate final messages ====================
575
- // Only send isFinal=true if we haven't sent it before AND this is actually the final message
576
- const shouldSendFinal = isFinal && !hasSentFinal;
577
- try {
578
- await conn.sendResponse(response, currentTaskId, sessionId, shouldSendFinal, isAppend);
579
- console.log("✓ Sent (status=" + responseStatus + ", isFinal=" + shouldSendFinal + ", append=" + isAppend + ")\n");
580
- // Mark that we've sent a final message (even if we're still processing subagent responses)
581
- if (isFinal) {
582
- hasSentFinal = true;
583
- }
584
- }
585
- catch (error) {
586
- console.error("✗ Failed to send:", error);
587
- }
588
- sentTextLength = text.length;
589
- }
590
- // ==================== FIX: SubAgent-friendly cleanup logic ====================
591
- // Only mark session as completed if we're truly done (no more subagent responses expected)
592
- // The key insight: we should NOT cleanup on every "final" payload, because subagents
593
- // can generate additional responses after the main agent returns "final".
594
- //
595
- // Instead, we let onIdle handle the cleanup, which is called after ALL processing is done.
596
- if (isFinal) {
597
- // Clear timeout but DON'T mark session as completed yet
598
- // SubAgent might still send more responses
599
- runtime.clearSessionTimeout(sessionId);
600
- console.log("[CLEANUP] Final payload received, but NOT marking session completed yet (waiting for onIdle)\n");
601
- }
602
- // ==================== END FIX ====================
603
- },
604
- onError: (err, info) => {
605
- console.error("\n" + "=".repeat(60));
606
- console.error("XiaoYi: [ERROR] " + info.kind + " failed: " + String(err));
607
- console.log("=".repeat(60) + "\n");
608
- runtime.clearSessionTimeout(sessionId);
609
- runtime.clearAbortControllerForSession(sessionId);
610
- runtime.markSessionCompleted(sessionId);
611
- },
612
- onIdle: async () => {
613
- const elapsed = Date.now() - startTime;
614
- console.log("\n" + "=".repeat(60));
615
- console.log("XiaoYi: [IDLE] Processing complete");
616
- console.log(" Total time: " + elapsed + "ms");
617
- console.log("=".repeat(60) + "\n");
618
- // ==================== PUSH MESSAGE FOR BACKGROUND RESULTS ====================
619
- // NOTE: Push logic disabled because we cannot reliably distinguish between:
620
- // - Normal responses (should be sent via WebSocket)
621
- // - Background task completion (should be sent via HTTP push)
622
- // TODO: Implement proper push message detection and HTTP API call
623
- console.log("[IDLE] All agent processing complete");
624
- // ==================== END PUSH MESSAGE ====================
625
- // This is called AFTER all processing is done (including subagents)
626
- // NOW we can safely mark the session as completed
627
- runtime.clearAbortControllerForSession(sessionId);
628
- runtime.markSessionCompleted(sessionId);
629
- console.log("[CLEANUP] Session marked as completed in onIdle\n");
630
- },
631
- });
632
- try {
633
- const result = await pluginRuntime.channel.reply.dispatchReplyFromConfig({
634
- ctx: msgContext,
635
- cfg: config,
636
- dispatcher,
637
- replyOptions: {
638
- ...replyOptions,
639
- abortSignal: abortSignal,
640
- },
641
- });
642
- const { queuedFinal, counts } = result;
643
- console.log("\n" + "=".repeat(60));
644
- console.log("XiaoYi: [DISPATCH] Summary");
645
- console.log(" Queued Final: " + queuedFinal);
646
- if (counts && Object.keys(counts).length > 0) {
647
- console.log(" Counts:", JSON.stringify(counts, null, 2));
648
- }
649
- console.log("=".repeat(60) + "\n");
650
- // ==================== ANALYZE EXECUTION RESULT ====================
651
- // Check if Agent produced any output
652
- const hasAnyCounts = counts && ((counts.tool && counts.tool > 0) ||
653
- (counts.block && counts.block > 0) ||
654
- (counts.final && counts.final > 0));
655
- if (!hasAnyCounts) {
656
- // Scenario 1: No Agent output detected
657
- // This could mean:
658
- // a) SubAgent running in background (main Agent returned)
659
- // b) Concurrent request (another Agent already running on this session)
660
- console.log("\n" + "=".repeat(60));
661
- console.log("[NO OUTPUT] Agent produced no output");
662
- console.log(" Session: " + sessionId);
663
- console.log(" Checking if there's another active Agent...");
664
- console.log("=".repeat(60) + "\n");
665
- // Check if there's an active Agent on this session
666
- // We use the existence of deliver callback triggers as an indicator
667
- // If the dispatcher's onIdle will be called later, an Agent is still running
668
- const conn = runtime.getConnection();
669
- if (conn) {
670
- // IMPORTANT: Send a response to user for THIS request
671
- // User needs to know what's happening
672
- try {
673
- const response = {
674
- sessionId: sessionId,
675
- messageId: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
676
- timestamp: Date.now(),
677
- agentId: messageHandlerAgentId, // Use stored value instead of resolvedAccount.config.agentId
678
- sender: {
679
- id: messageHandlerAgentId, // Use stored value instead of resolvedAccount.config.agentId
680
- name: "OpenClaw Agent",
681
- type: "agent",
682
- },
683
- content: {
684
- type: "text",
685
- text: "任务正在处理中,请稍候...",
686
- },
687
- status: "success",
688
- };
689
- // Send response with isFinal=true to close THIS request
690
- await conn.sendResponse(response, currentTaskId, sessionId, true, false);
691
- console.log("✓ [NO OUTPUT] Response sent to user\n");
692
- }
693
- catch (error) {
694
- console.error("✗ [NO OUTPUT] Failed to send response:", error);
695
- }
696
- }
697
- // CRITICAL: Don't cleanup resources yet!
698
- // The original Agent might still be running and needs these resources
699
- // onIdle will be called when the original Agent completes
700
- console.log("[NO OUTPUT] Keeping resources alive for potential background Agent\n");
701
- markDispatchIdle();
702
- }
703
- else {
704
- // Scenario 2: Normal execution with output
705
- // - Agent produced output synchronously
706
- // - All cleanup is already handled in deliver/onIdle callbacks
707
- console.log("[NORMAL] Agent produced output, cleanup handled in callbacks");
708
- markDispatchIdle();
709
- }
710
- // ==================== END ANALYSIS ====================
711
- }
712
- catch (error) {
713
- console.error("XiaoYi: [ERROR] Error dispatching message:", error);
714
- // Clear timeout on error
715
- runtime.clearSessionTimeout(sessionId);
716
- // Clear abort controller on error
717
- runtime.clearAbortControllerForSession(sessionId);
718
- // Mark session as completed on error
719
- runtime.markSessionCompleted(sessionId);
720
- // Mark dispatcher as idle even on error
721
- markDispatchIdle();
722
- }
723
- }
724
- catch (error) {
725
- console.error("XiaoYi: [ERROR] Unexpected error in message handler:", error);
726
- }
727
- });
728
- // Setup cancel handler
729
- // When tasks/cancel is received, abort the current session's agent run
730
- connection.on("cancel", async (data) => {
731
- const { sessionId } = data;
732
- console.log("\n" + "=".repeat(60));
733
- console.log(`XiaoYi: [CANCEL] Cancel event received`);
734
- console.log(` Session: ${sessionId}`);
735
- console.log(` Task ID: ${data.taskId || "N/A"}`);
736
- console.log("=".repeat(60) + "\n");
737
- // Abort the session's agent run
738
- const aborted = runtime.abortSession(sessionId);
739
- if (aborted) {
740
- console.log(`[CANCEL] Successfully triggered abort for session ${sessionId}`);
741
- }
742
- else {
743
- console.log(`[CANCEL] No active agent run found for session ${sessionId}`);
744
- }
745
- // Clear timeout as the session is being canceled
746
- runtime.markSessionCompleted(sessionId);
747
- });
748
- // Mark handlers as registered to prevent duplicate registration
749
- handlersRegistered = true;
750
- }
751
- else {
752
- console.log("XiaoYi: [STARTUP] Handlers already registered, skipping duplicate registration");
753
- }
754
- console.log("XiaoYi: Event handlers registered");
755
- // Keep the channel running by waiting for the abort signal
756
- // This prevents the Promise from resolving, keeping 'running' status as true
757
- // The channel will stop when stopAccount() is called or the abort signal is triggered
758
- await new Promise((resolve) => {
759
- ctx.abortSignal.addEventListener("abort", () => {
760
- console.log("XiaoYi: abort signal received, stopping channel");
761
- resolve();
762
- }, { once: true });
763
- // Also handle case where abort is already triggered
764
- if (ctx.abortSignal.aborted) {
765
- console.log("XiaoYi: abort signal already triggered");
766
- resolve();
767
- }
768
- });
769
- console.log("XiaoYi: startAccount() exiting - END");
770
- },
771
- stopAccount: async (ctx) => {
772
- const runtime = (0, runtime_1.getXiaoYiRuntime)();
773
- runtime.stop();
287
+ sendMedia: async (ctx) => {
288
+ throw new Error("暂不支持文件回传");
774
289
  },
775
290
  },
776
291
  /**
777
292
  * Messaging adapter - normalize targets
293
+ * In new openclaw version, normalizeTarget receives a string and returns a normalized string
778
294
  */
779
295
  messaging: {
780
- normalizeTarget: async (ctx) => {
296
+ normalizeTarget: (raw) => {
781
297
  // For XiaoYi, we use sessionId as the target
782
- // The sessionId comes from the incoming message's meta
783
- return ctx.to;
298
+ // The raw input is already the normalized target (sessionId)
299
+ return raw;
784
300
  },
785
301
  },
786
302
  /**
787
303
  * Status adapter - health checks
304
+ * Using buildAccountSnapshot for compatibility with new openclaw version
788
305
  */
789
306
  status: {
790
- getAccountStatus: async (ctx) => {
791
- const runtime = (0, runtime_1.getXiaoYiRuntime)();
307
+ buildAccountSnapshot: async (params) => {
308
+ const runtime = (0, runtime_js_1.getXiaoYiRuntime)();
792
309
  const connection = runtime.getConnection();
793
310
  if (!connection) {
794
311
  return {
795
- status: "offline",
796
- 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
+ }],
797
319
  };
798
320
  }
799
321
  const state = connection.getState();
800
322
  if (state.connected && state.authenticated) {
801
323
  return {
802
- status: "online",
803
- message: "Connected and authenticated",
324
+ accountId: params.account.accountId,
325
+ state: "ready",
326
+ lastEventAt: Date.now(),
327
+ lastInboundAt: Date.now(),
804
328
  };
805
329
  }
806
330
  else if (state.connected) {
807
331
  return {
808
- status: "connecting",
809
- 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
+ }],
810
339
  };
811
340
  }
812
341
  else {
813
342
  return {
814
- status: "offline",
815
- 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
+ }],
816
350
  };
817
351
  }
818
352
  },