@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.
- package/dist/auth.d.ts +1 -1
- package/dist/channel.d.ts +116 -14
- package/dist/channel.js +199 -665
- package/dist/config-schema.d.ts +8 -8
- package/dist/config-schema.js +5 -5
- 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.d.ts +1 -4
- package/dist/index.js +7 -11
- package/dist/push.d.ts +28 -0
- package/dist/push.js +135 -0
- package/dist/runtime.d.ts +48 -2
- package/dist/runtime.js +117 -3
- package/dist/types.d.ts +95 -1
- package/dist/websocket.d.ts +49 -1
- package/dist/websocket.js +279 -20
- 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 -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
|
|
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
|
|
@@ -32,7 +67,64 @@ exports.xiaoyiPlugin = {
|
|
|
32
67
|
media: true,
|
|
33
68
|
nativeCommands: false,
|
|
34
69
|
},
|
|
35
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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
|
-
//
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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:
|
|
203
|
-
|
|
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:
|
|
296
|
+
normalizeTarget: (raw) => {
|
|
781
297
|
// For XiaoYi, we use sessionId as the target
|
|
782
|
-
// The
|
|
783
|
-
return
|
|
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
|
-
|
|
791
|
-
const runtime = (0,
|
|
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
|
-
|
|
796
|
-
|
|
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
|
-
|
|
803
|
-
|
|
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
|
-
|
|
809
|
-
|
|
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
|
-
|
|
815
|
-
|
|
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
|
},
|