@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/auth.d.ts +1 -1
- package/dist/channel.d.ts +59 -14
- package/dist/channel.js +142 -814
- package/dist/file-download.d.ts +17 -0
- package/dist/file-download.js +69 -0
- package/dist/heartbeat.d.ts +39 -0
- package/dist/heartbeat.js +102 -0
- package/dist/index.js +5 -5
- package/dist/push.d.ts +1 -1
- package/dist/runtime.d.ts +2 -2
- package/dist/runtime.js +2 -2
- package/dist/types.d.ts +74 -1
- package/dist/websocket.d.ts +17 -1
- package/dist/websocket.js +163 -13
- package/dist/xy-bot.d.ts +19 -0
- package/dist/xy-bot.js +277 -0
- package/dist/xy-client.d.ts +26 -0
- package/dist/xy-client.js +78 -0
- package/dist/xy-config.d.ts +18 -0
- package/dist/xy-config.js +37 -0
- package/dist/xy-formatter.d.ts +94 -0
- package/dist/xy-formatter.js +303 -0
- package/dist/xy-monitor.d.ts +17 -0
- package/dist/xy-monitor.js +187 -0
- package/dist/xy-parser.d.ts +49 -0
- package/dist/xy-parser.js +109 -0
- package/dist/xy-reply-dispatcher.d.ts +17 -0
- package/dist/xy-reply-dispatcher.js +308 -0
- package/dist/xy-tools/session-manager.d.ts +29 -0
- package/dist/xy-tools/session-manager.js +80 -0
- package/dist/xy-utils/config-manager.d.ts +26 -0
- package/dist/xy-utils/config-manager.js +61 -0
- package/dist/xy-utils/crypto.d.ts +8 -0
- package/dist/xy-utils/crypto.js +21 -0
- package/dist/xy-utils/logger.d.ts +6 -0
- package/dist/xy-utils/logger.js +37 -0
- package/dist/xy-utils/session.d.ts +34 -0
- package/dist/xy-utils/session.js +55 -0
- package/package.json +32 -17
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
|
|
5
|
-
const
|
|
6
|
-
const
|
|
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:
|
|
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
|
-
*
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
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
|
-
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
227
|
-
|
|
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
|
-
//
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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:
|
|
260
|
-
|
|
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:
|
|
296
|
+
normalizeTarget: (raw) => {
|
|
987
297
|
// For XiaoYi, we use sessionId as the target
|
|
988
|
-
// The
|
|
989
|
-
return
|
|
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
|
-
|
|
997
|
-
const runtime = (0,
|
|
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
|
-
|
|
1002
|
-
|
|
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
|
-
|
|
1009
|
-
|
|
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
|
-
|
|
1015
|
-
|
|
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
|
-
|
|
1021
|
-
|
|
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
|
},
|