@ynhcj/xiaoyi-channel 0.0.124-beta → 0.0.126-beta
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/src/bot.js +1 -1
- package/dist/src/client.js +24 -21
- package/dist/src/cspl/call-api.js +6 -5
- package/dist/src/file-download.js +4 -3
- package/dist/src/file-upload.js +19 -18
- package/dist/src/heartbeat.js +1 -1
- package/dist/src/monitor.js +12 -10
- package/dist/src/outbound.js +19 -18
- package/dist/src/provider.js +12 -12
- package/dist/src/push.js +16 -15
- package/dist/src/reply-dispatcher.js +3 -2
- package/dist/src/skill-retriever/hooks.js +4 -3
- package/dist/src/skill-retriever/tool-search.js +10 -9
- package/dist/src/steer-injector.js +1 -1
- package/dist/src/tools/send-file-to-user-tool.js +4 -1
- package/dist/src/tools/session-manager.js +31 -15
- package/dist/src/utils/self-evolution-manager.js +3 -2
- package/dist/src/websocket.js +32 -29
- package/package.json +1 -1
package/dist/src/bot.js
CHANGED
|
@@ -200,7 +200,7 @@ export async function handleXYMessage(params) {
|
|
|
200
200
|
const fileParts = extractFileParts(parsed.parts);
|
|
201
201
|
// Download files to local disk
|
|
202
202
|
const downloadedFiles = await downloadFilesFromParts(fileParts);
|
|
203
|
-
|
|
203
|
+
log("Downloaded files:", JSON.stringify(downloadedFiles, null, 2));
|
|
204
204
|
const mediaPayload = buildXYMediaPayload(downloadedFiles);
|
|
205
205
|
// Resolve envelope format options (following feishu pattern)
|
|
206
206
|
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
|
package/dist/src/client.js
CHANGED
|
@@ -44,16 +44,17 @@ export function getXYWebSocketManager(config) {
|
|
|
44
44
|
* Disconnects the manager and removes it from the cache.
|
|
45
45
|
*/
|
|
46
46
|
export function removeXYWebSocketManager(config) {
|
|
47
|
+
const log = runtime?.log ?? console.log;
|
|
47
48
|
const cacheKey = `${config.apiKey}-${config.agentId}`;
|
|
48
49
|
const manager = wsManagerCache.get(cacheKey);
|
|
49
50
|
if (manager) {
|
|
50
|
-
|
|
51
|
+
log(`🗑️ [WS-MANAGER-CACHE] Removing manager from cache: ${cacheKey}`);
|
|
51
52
|
manager.disconnect();
|
|
52
53
|
wsManagerCache.delete(cacheKey);
|
|
53
|
-
|
|
54
|
+
log(`🗑️ [WS-MANAGER-CACHE] Manager removed, remaining managers: ${wsManagerCache.size}`);
|
|
54
55
|
}
|
|
55
56
|
else {
|
|
56
|
-
|
|
57
|
+
log(`⚠️ [WS-MANAGER-CACHE] Manager not found in cache: ${cacheKey}`);
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
/**
|
|
@@ -78,36 +79,37 @@ export function getCachedManagerCount() {
|
|
|
78
79
|
* Helps identify connection issues and orphan connections.
|
|
79
80
|
*/
|
|
80
81
|
export function diagnoseAllManagers() {
|
|
81
|
-
|
|
82
|
+
const log = runtime?.log ?? console.log;
|
|
83
|
+
log(`Total cached managers: ${wsManagerCache.size}`);
|
|
82
84
|
if (wsManagerCache.size === 0) {
|
|
83
|
-
|
|
85
|
+
log("ℹ️ No managers in cache");
|
|
84
86
|
return;
|
|
85
87
|
}
|
|
86
88
|
let orphanCount = 0;
|
|
87
89
|
wsManagerCache.forEach((manager, key) => {
|
|
88
90
|
const diag = manager.getConnectionDiagnostics();
|
|
89
|
-
|
|
91
|
+
log(` Total event listeners on manager: ${diag.totalEventListeners}`);
|
|
90
92
|
// Connection
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
93
|
+
log(` 🔌 Connection:`);
|
|
94
|
+
log(` - Exists: ${diag.connection.exists}`);
|
|
95
|
+
log(` - ReadyState: ${diag.connection.readyState}`);
|
|
96
|
+
log(` - State connected/ready: ${diag.connection.stateConnected}/${diag.connection.stateReady}`);
|
|
97
|
+
log(` - Reconnect attempts: ${diag.connection.reconnectAttempts}`);
|
|
98
|
+
log(` - Listeners on WebSocket: ${diag.connection.listenerCount}`);
|
|
99
|
+
log(` - Heartbeat active: ${diag.connection.heartbeatActive}`);
|
|
100
|
+
log(` - Has reconnect timer: ${diag.connection.hasReconnectTimer}`);
|
|
99
101
|
if (diag.connection.isOrphan) {
|
|
100
|
-
|
|
102
|
+
log(` ⚠️ ORPHAN CONNECTION DETECTED!`);
|
|
101
103
|
orphanCount++;
|
|
102
104
|
}
|
|
103
|
-
|
|
105
|
+
log("");
|
|
104
106
|
});
|
|
105
107
|
if (orphanCount > 0) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
+
log(`⚠️ Total orphan connections found: ${orphanCount}`);
|
|
109
|
+
log(`💡 Suggestion: These connections should be cleaned up`);
|
|
108
110
|
}
|
|
109
111
|
else {
|
|
110
|
-
|
|
112
|
+
log(`✅ No orphan connections found`);
|
|
111
113
|
}
|
|
112
114
|
}
|
|
113
115
|
/**
|
|
@@ -115,17 +117,18 @@ export function diagnoseAllManagers() {
|
|
|
115
117
|
* Returns the number of managers that had orphan connections.
|
|
116
118
|
*/
|
|
117
119
|
export function cleanupOrphanConnections() {
|
|
120
|
+
const log = runtime?.log ?? console.log;
|
|
118
121
|
let cleanedCount = 0;
|
|
119
122
|
wsManagerCache.forEach((manager, key) => {
|
|
120
123
|
const diag = manager.getConnectionDiagnostics();
|
|
121
124
|
if (diag.connection.isOrphan) {
|
|
122
|
-
|
|
125
|
+
log(`🧹 Cleaning up orphan connections in manager: ${key}`);
|
|
123
126
|
manager.disconnect();
|
|
124
127
|
cleanedCount++;
|
|
125
128
|
}
|
|
126
129
|
});
|
|
127
130
|
if (cleanedCount > 0) {
|
|
128
|
-
|
|
131
|
+
log(`🧹 Cleaned up ${cleanedCount} manager(s) with orphan connections`);
|
|
129
132
|
}
|
|
130
133
|
return cleanedCount;
|
|
131
134
|
}
|
|
@@ -4,12 +4,13 @@ import { URL } from "node:url";
|
|
|
4
4
|
import { randomBytes } from "node:crypto";
|
|
5
5
|
import { getCsplConfig } from "./config.js";
|
|
6
6
|
import { DEFAULT_HTTP_PORT, HTTP_STATUS_BAD_REQUEST } from "./constants.js";
|
|
7
|
+
import { logger } from "../utils/logger.js";
|
|
7
8
|
function generateTraceId() {
|
|
8
9
|
return randomBytes(16).toString("hex");
|
|
9
10
|
}
|
|
10
11
|
function buildHeaders(config) {
|
|
11
12
|
const traceId = generateTraceId();
|
|
12
|
-
|
|
13
|
+
logger.log(`[SENTINEL HOOK] trace-id: ${traceId}`);
|
|
13
14
|
return {
|
|
14
15
|
"x-hag-trace-id": traceId,
|
|
15
16
|
"x-uid": config.uid,
|
|
@@ -65,21 +66,21 @@ export async function callCsplApi(questionText, cfg) {
|
|
|
65
66
|
res.on("end", () => {
|
|
66
67
|
try {
|
|
67
68
|
const result = parseResponse(data);
|
|
68
|
-
|
|
69
|
+
logger.log(`[SENTINEL HOOK] ✅ 请求成功`);
|
|
69
70
|
resolve(result);
|
|
70
71
|
}
|
|
71
72
|
catch (e) {
|
|
72
|
-
|
|
73
|
+
logger.error(`[SENTINEL HOOK] ❌ 请求失败: ${e instanceof Error ? e.message : String(e)}`);
|
|
73
74
|
reject(e);
|
|
74
75
|
}
|
|
75
76
|
});
|
|
76
77
|
});
|
|
77
78
|
req.on("error", (error) => {
|
|
78
|
-
|
|
79
|
+
logger.error(`[SENTINEL HOOK] ❌ 请求错误: ${error instanceof Error ? error.message : String(error)}`);
|
|
79
80
|
reject(error);
|
|
80
81
|
});
|
|
81
82
|
req.on("timeout", () => {
|
|
82
|
-
|
|
83
|
+
logger.error(`[SENTINEL HOOK] ⏰ 请求超时 (${config.api.timeout}ms)`);
|
|
83
84
|
req.destroy();
|
|
84
85
|
reject(new Error("[SENTINEL HOOK] Request timeout"));
|
|
85
86
|
});
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import fetch from "node-fetch";
|
|
3
3
|
import fs from "fs/promises";
|
|
4
4
|
import path from "path";
|
|
5
|
+
import { logger } from "./utils/logger.js";
|
|
5
6
|
/**
|
|
6
7
|
* Download a file from URL to local path.
|
|
7
8
|
*/
|
|
@@ -19,10 +20,10 @@ export async function downloadFile(url, destPath) {
|
|
|
19
20
|
}
|
|
20
21
|
catch (error) {
|
|
21
22
|
if (error.name === 'AbortError') {
|
|
22
|
-
|
|
23
|
+
logger.log(`Download timeout (30s) for ${url}`);
|
|
23
24
|
throw new Error(`Download timeout after 30 seconds`);
|
|
24
25
|
}
|
|
25
|
-
|
|
26
|
+
logger.log(`Failed to download file from ${url}:`);
|
|
26
27
|
throw error;
|
|
27
28
|
}
|
|
28
29
|
finally {
|
|
@@ -51,7 +52,7 @@ export async function downloadFilesFromParts(fileParts, tempDir = "/tmp/xy_chann
|
|
|
51
52
|
});
|
|
52
53
|
}
|
|
53
54
|
catch (error) {
|
|
54
|
-
|
|
55
|
+
logger.log(`Failed to download file ${name}:`);
|
|
55
56
|
// Continue with other files
|
|
56
57
|
}
|
|
57
58
|
}
|
package/dist/src/file-upload.js
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
import fetch from "node-fetch";
|
|
4
4
|
import fs from "fs/promises";
|
|
5
5
|
import os from "os";
|
|
6
|
+
import { logger } from "./utils/logger.js";
|
|
6
7
|
import path from "path";
|
|
7
8
|
import { calculateSHA256 } from "./utils/crypto.js";
|
|
8
9
|
function isRemoteUrl(filePath) {
|
|
9
10
|
return filePath.startsWith("http://") || filePath.startsWith("https://");
|
|
10
11
|
}
|
|
11
12
|
async function downloadToTempFile(url) {
|
|
12
|
-
|
|
13
|
+
logger.log(`[XY File Upload] Downloading remote file: ${url}`);
|
|
13
14
|
const response = await fetch(url);
|
|
14
15
|
if (!response.ok) {
|
|
15
16
|
throw new Error(`Failed to download remote file: HTTP ${response.status}`);
|
|
@@ -18,7 +19,7 @@ async function downloadToTempFile(url) {
|
|
|
18
19
|
const urlFileName = path.basename(new URL(url).pathname) || "download";
|
|
19
20
|
const tempPath = path.join(os.tmpdir(), `xy-upload-${Date.now()}-${urlFileName}`);
|
|
20
21
|
await fs.writeFile(tempPath, buffer);
|
|
21
|
-
|
|
22
|
+
logger.log(`[XY File Upload] Downloaded to temp file: ${tempPath}`);
|
|
22
23
|
return tempPath;
|
|
23
24
|
}
|
|
24
25
|
/**
|
|
@@ -39,7 +40,7 @@ export class XYFileUploadService {
|
|
|
39
40
|
* Returns the objectId (as fileId) for use in A2A messages.
|
|
40
41
|
*/
|
|
41
42
|
async uploadFile(filePath, objectType = "TEMPORARY_MATERIAL_DOC") {
|
|
42
|
-
|
|
43
|
+
logger.log(`[XY File Upload] Starting file upload: ${filePath}`);
|
|
43
44
|
let localFilePath = filePath;
|
|
44
45
|
let isTempFile = false;
|
|
45
46
|
try {
|
|
@@ -54,7 +55,7 @@ export class XYFileUploadService {
|
|
|
54
55
|
const fileSha256 = calculateSHA256(fileBuffer);
|
|
55
56
|
const fileSize = fileBuffer.length;
|
|
56
57
|
// Phase 1: Prepare
|
|
57
|
-
|
|
58
|
+
logger.log(`[XY File Upload] Phase 1: Prepare upload for ${fileName}`);
|
|
58
59
|
const prepareResp = await fetch(`${this.baseUrl}/osms/v1/file/manager/prepare`, {
|
|
59
60
|
method: "POST",
|
|
60
61
|
headers: {
|
|
@@ -84,7 +85,7 @@ export class XYFileUploadService {
|
|
|
84
85
|
}
|
|
85
86
|
const { objectId, draftId, uploadInfos } = prepareData;
|
|
86
87
|
// Phase 2: Upload
|
|
87
|
-
|
|
88
|
+
logger.log(`[XY File Upload] Phase 2: Upload file data`);
|
|
88
89
|
const uploadInfo = uploadInfos[0]; // Single-part upload
|
|
89
90
|
const uploadResp = await fetch(uploadInfo.url, {
|
|
90
91
|
method: uploadInfo.method,
|
|
@@ -95,9 +96,9 @@ export class XYFileUploadService {
|
|
|
95
96
|
const uploadErrorText = await uploadResp.text();
|
|
96
97
|
throw new Error(`Upload failed: HTTP ${uploadResp.status}`);
|
|
97
98
|
}
|
|
98
|
-
|
|
99
|
+
logger.log(`[XY File Upload] Upload complete`);
|
|
99
100
|
// Phase 3: Complete
|
|
100
|
-
|
|
101
|
+
logger.log(`[XY File Upload] Phase 3: Complete upload`);
|
|
101
102
|
const completeResp = await fetch(`${this.baseUrl}/osms/v1/file/manager/complete`, {
|
|
102
103
|
method: "POST",
|
|
103
104
|
headers: {
|
|
@@ -115,11 +116,11 @@ export class XYFileUploadService {
|
|
|
115
116
|
throw new Error(`Complete failed: HTTP ${completeResp.status}`);
|
|
116
117
|
}
|
|
117
118
|
const completeData = await completeResp.json();
|
|
118
|
-
|
|
119
|
+
logger.log(`[XY File Upload] File upload successful: ${fileName} → objectId=${objectId}`);
|
|
119
120
|
return objectId;
|
|
120
121
|
}
|
|
121
122
|
catch (error) {
|
|
122
|
-
|
|
123
|
+
logger.error(`[XY File Upload] File upload failed for ${filePath}:`, error);
|
|
123
124
|
throw error;
|
|
124
125
|
}
|
|
125
126
|
finally {
|
|
@@ -150,7 +151,7 @@ export class XYFileUploadService {
|
|
|
150
151
|
const fileSha256 = calculateSHA256(fileBuffer);
|
|
151
152
|
const fileSize = fileBuffer.length;
|
|
152
153
|
// Phase 1: Prepare
|
|
153
|
-
|
|
154
|
+
logger.log(`[XY File Upload] Phase 1: Prepare upload for ${fileName}`);
|
|
154
155
|
const prepareResp = await fetch(`${this.baseUrl}/osms/v1/file/manager/prepare`, {
|
|
155
156
|
method: "POST",
|
|
156
157
|
headers: {
|
|
@@ -179,23 +180,23 @@ export class XYFileUploadService {
|
|
|
179
180
|
throw new Error(`Prepare failed: ${prepareData.desc}`);
|
|
180
181
|
}
|
|
181
182
|
const { objectId, draftId, uploadInfos } = prepareData;
|
|
182
|
-
|
|
183
|
+
logger.log(`[XY File Upload] Prepare complete: objectId=${objectId}, draftId=${draftId}`);
|
|
183
184
|
// Phase 2: Upload
|
|
184
|
-
|
|
185
|
+
logger.log(`[XY File Upload] Phase 2: Upload file data`);
|
|
185
186
|
const uploadInfo = uploadInfos[0]; // Single-part upload
|
|
186
187
|
const uploadResp = await fetch(uploadInfo.url, {
|
|
187
188
|
method: uploadInfo.method,
|
|
188
189
|
headers: uploadInfo.headers,
|
|
189
190
|
body: fileBuffer,
|
|
190
191
|
});
|
|
191
|
-
|
|
192
|
+
logger.log(`[XY File Upload] Upload response status: ${uploadResp.status}`);
|
|
192
193
|
if (!uploadResp.ok) {
|
|
193
194
|
const uploadErrorText = await uploadResp.text();
|
|
194
195
|
throw new Error(`Upload failed: HTTP ${uploadResp.status}`);
|
|
195
196
|
}
|
|
196
|
-
|
|
197
|
+
logger.log(`[XY File Upload] Upload complete`);
|
|
197
198
|
// Phase 3: CompleteAndQuery - get file URL
|
|
198
|
-
|
|
199
|
+
logger.log(`[XY File Upload] Phase 3: CompleteAndQuery to get file URL`);
|
|
199
200
|
const completeResp = await fetch(`${this.baseUrl}/osms/v1/file/manager/completeAndQuery`, {
|
|
200
201
|
method: "POST",
|
|
201
202
|
headers: {
|
|
@@ -218,11 +219,11 @@ export class XYFileUploadService {
|
|
|
218
219
|
if (!fileUrl) {
|
|
219
220
|
throw new Error("No file URL returned from completeAndQuery");
|
|
220
221
|
}
|
|
221
|
-
|
|
222
|
+
logger.log(`[XY File Upload] File upload successful`);
|
|
222
223
|
return fileUrl;
|
|
223
224
|
}
|
|
224
225
|
catch (error) {
|
|
225
|
-
|
|
226
|
+
logger.error(`[XY File Upload] File upload with URL retrieval failed for ${filePath}:`, error);
|
|
226
227
|
throw error;
|
|
227
228
|
}
|
|
228
229
|
finally {
|
|
@@ -249,7 +250,7 @@ export class XYFileUploadService {
|
|
|
249
250
|
});
|
|
250
251
|
}
|
|
251
252
|
catch (error) {
|
|
252
|
-
|
|
253
|
+
logger.error(`[XY File Upload] Failed to upload ${filePath}, skipping:`, error);
|
|
253
254
|
// Continue with other files
|
|
254
255
|
}
|
|
255
256
|
}
|
package/dist/src/heartbeat.js
CHANGED
|
@@ -67,7 +67,7 @@ export class HeartbeatManager {
|
|
|
67
67
|
*/
|
|
68
68
|
sendHeartbeat() {
|
|
69
69
|
if (this.ws.readyState !== WebSocket.OPEN) {
|
|
70
|
-
|
|
70
|
+
this.log(`Cannot send heartbeat for ${this.serverName}: WebSocket not open`);
|
|
71
71
|
return;
|
|
72
72
|
}
|
|
73
73
|
try {
|
package/dist/src/monitor.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolveXYConfig } from "./config.js";
|
|
2
|
-
import { getXYWebSocketManager, diagnoseAllManagers, cleanupOrphanConnections, removeXYWebSocketManager } from "./client.js";
|
|
2
|
+
import { getXYWebSocketManager, setClientRuntime, diagnoseAllManagers, cleanupOrphanConnections, removeXYWebSocketManager } from "./client.js";
|
|
3
3
|
import { handleXYMessage } from "./bot.js";
|
|
4
4
|
import { parseA2AMessage } from "./parser.js";
|
|
5
5
|
import { hasActiveTask, getAllActiveTaskBindings } from "./task-manager.js";
|
|
@@ -51,8 +51,10 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
51
51
|
opts.setStatus({ lastEventAt: Date.now(), lastInboundAt: Date.now() });
|
|
52
52
|
}
|
|
53
53
|
: undefined;
|
|
54
|
+
// ✅ Set runtime for WebSocket manager logging before creating/getting manager
|
|
55
|
+
setClientRuntime(runtime);
|
|
54
56
|
// 🔍 Diagnose WebSocket managers before gateway start
|
|
55
|
-
|
|
57
|
+
log("🔍 [DIAGNOSTICS] Checking WebSocket managers before gateway start...");
|
|
56
58
|
diagnoseAllManagers();
|
|
57
59
|
// Get WebSocket manager (cached)
|
|
58
60
|
const wsManager = getXYWebSocketManager(account);
|
|
@@ -141,7 +143,7 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
141
143
|
opts.setStatus?.({ connected: true });
|
|
142
144
|
};
|
|
143
145
|
const disconnectedHandler = (serverId) => {
|
|
144
|
-
|
|
146
|
+
log(`XY gateway: ${serverId} disconnected`);
|
|
145
147
|
loggedServers.delete(serverId);
|
|
146
148
|
// ✅ Report disconnection status (only if all servers disconnected)
|
|
147
149
|
if (loggedServers.size === 0) {
|
|
@@ -177,13 +179,13 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
177
179
|
const cleanup = () => {
|
|
178
180
|
log("XY gateway: cleaning up...");
|
|
179
181
|
// 🔍 Diagnose before cleanup
|
|
180
|
-
|
|
182
|
+
log("🔍 [DIAGNOSTICS] Checking WebSocket managers before cleanup...");
|
|
181
183
|
diagnoseAllManagers();
|
|
182
184
|
// Stop health check interval
|
|
183
185
|
if (healthCheckInterval) {
|
|
184
186
|
clearInterval(healthCheckInterval);
|
|
185
187
|
healthCheckInterval = null;
|
|
186
|
-
|
|
188
|
+
log("⏸️ Stopped periodic health check");
|
|
187
189
|
}
|
|
188
190
|
// Remove event handlers to prevent duplicate calls on gateway restart
|
|
189
191
|
wsManager.off("message", messageHandler);
|
|
@@ -205,7 +207,7 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
205
207
|
activeMessages.clear();
|
|
206
208
|
log(`[MONITOR-HANDLER] 🧹 Cleanup complete, cleared active messages and sessions`);
|
|
207
209
|
// 🔍 Diagnose after cleanup
|
|
208
|
-
|
|
210
|
+
log("🔍 [DIAGNOSTICS] Checking WebSocket managers after cleanup...");
|
|
209
211
|
diagnoseAllManagers();
|
|
210
212
|
};
|
|
211
213
|
const handleAbort = async () => {
|
|
@@ -258,20 +260,20 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
258
260
|
wsManager.on("self-evolution-state-get-event", selfEvolutionStateGetHandler);
|
|
259
261
|
wsManager.on("login-token-event", loginTokenEventHandler);
|
|
260
262
|
// Start periodic health check (every 6 hours)
|
|
261
|
-
|
|
263
|
+
log("🏥 Starting periodic health check (every 6 hours)...");
|
|
262
264
|
healthCheckInterval = setInterval(() => {
|
|
263
|
-
|
|
265
|
+
log("🏥 [HEALTH CHECK] Periodic WebSocket diagnostics...");
|
|
264
266
|
diagnoseAllManagers();
|
|
265
267
|
// Auto-cleanup orphan connections
|
|
266
268
|
const cleaned = cleanupOrphanConnections();
|
|
267
269
|
if (cleaned > 0) {
|
|
268
|
-
|
|
270
|
+
log(`🧹 [HEALTH CHECK] Auto-cleaned ${cleaned} manager(s) with orphan connections`);
|
|
269
271
|
}
|
|
270
272
|
// Cleanup stale sessions (older than 10min TTL)
|
|
271
273
|
const cleanedSessions = cleanupStaleSessions();
|
|
272
274
|
const remainingSessions = getActiveSessionCount();
|
|
273
275
|
if (cleanedSessions > 0 || remainingSessions > 0) {
|
|
274
|
-
|
|
276
|
+
log(`🧹 [HEALTH CHECK] Sessions: cleaned=${cleanedSessions}, active=${remainingSessions}`);
|
|
275
277
|
}
|
|
276
278
|
// Cleanup stale temp files (older than 24 hours)
|
|
277
279
|
void cleanupStaleTempFiles();
|
package/dist/src/outbound.js
CHANGED
|
@@ -4,6 +4,7 @@ import { XYPushService } from "./push.js";
|
|
|
4
4
|
import { getCurrentSessionContext } from "./tools/session-manager.js";
|
|
5
5
|
import { savePushData } from "./utils/pushdata-manager.js";
|
|
6
6
|
import { getAllPushIds } from "./utils/pushid-manager.js";
|
|
7
|
+
import { logger } from "./utils/logger.js";
|
|
7
8
|
// Special marker for default push delivery when no target is specified
|
|
8
9
|
const DEFAULT_PUSH_MARKER = "default";
|
|
9
10
|
// File extension to MIME type mapping
|
|
@@ -57,7 +58,7 @@ export const xyOutbound = {
|
|
|
57
58
|
resolveTarget: ({ cfg, to, accountId, mode }) => {
|
|
58
59
|
// If no target provided, use default marker for push delivery
|
|
59
60
|
if (!to || to.trim() === "") {
|
|
60
|
-
|
|
61
|
+
logger.log(`[xyOutbound.resolveTarget] No target specified, using default push marker`);
|
|
61
62
|
return {
|
|
62
63
|
ok: true,
|
|
63
64
|
to: DEFAULT_PUSH_MARKER,
|
|
@@ -66,24 +67,24 @@ export const xyOutbound = {
|
|
|
66
67
|
const trimmedTo = to.trim();
|
|
67
68
|
// If the target doesn't contain "::", try to enhance it with taskId from session context
|
|
68
69
|
if (!trimmedTo.includes("::")) {
|
|
69
|
-
|
|
70
|
+
logger.log(`[xyOutbound.resolveTarget] Target "${trimmedTo}" missing taskId, looking up session context`);
|
|
70
71
|
// Try to get the current session context
|
|
71
72
|
const sessionContext = getCurrentSessionContext();
|
|
72
73
|
if (sessionContext && sessionContext.sessionId === trimmedTo) {
|
|
73
74
|
const enhancedTarget = `${trimmedTo}::${sessionContext.taskId}`;
|
|
74
|
-
|
|
75
|
+
logger.log(`[xyOutbound.resolveTarget] Enhanced target: ${enhancedTarget}`);
|
|
75
76
|
return {
|
|
76
77
|
ok: true,
|
|
77
78
|
to: enhancedTarget,
|
|
78
79
|
};
|
|
79
80
|
}
|
|
80
81
|
else {
|
|
81
|
-
|
|
82
|
+
logger.log(`[xyOutbound.resolveTarget] Could not find matching session context for "${trimmedTo}"`);
|
|
82
83
|
// Still return the original target, but it may fail in sendMedia
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
// Otherwise, use the provided target (either already in correct format or for sendText)
|
|
86
|
-
|
|
87
|
+
logger.log(`[xyOutbound.resolveTarget] Using provided target:`, trimmedTo);
|
|
87
88
|
return {
|
|
88
89
|
ok: true,
|
|
89
90
|
to: trimmedTo,
|
|
@@ -95,36 +96,36 @@ export const xyOutbound = {
|
|
|
95
96
|
// Handle default push marker (for cron jobs without explicit target)
|
|
96
97
|
let actualTo = to;
|
|
97
98
|
if (to === DEFAULT_PUSH_MARKER) {
|
|
98
|
-
|
|
99
|
+
logger.log(`[xyOutbound.sendText] Using default push delivery (no specific target)`);
|
|
99
100
|
// For push notifications, we don't need a specific target
|
|
100
101
|
// The push service will handle it based on config
|
|
101
102
|
actualTo = config.defaultSessionId || "";
|
|
102
103
|
}
|
|
103
104
|
// 1. 持久化推送消息内容,获取 pushDataId
|
|
104
|
-
|
|
105
|
+
logger.log(`[xyOutbound.sendText] Saving push data to local storage...`);
|
|
105
106
|
let pushDataId;
|
|
106
107
|
try {
|
|
107
108
|
pushDataId = await savePushData(text);
|
|
108
|
-
|
|
109
|
+
logger.log(`[xyOutbound.sendText] ✅ Push data saved with ID: ${pushDataId.substring(0, 20)}`);
|
|
109
110
|
}
|
|
110
111
|
catch (error) {
|
|
111
|
-
|
|
112
|
+
logger.error(`[xyOutbound.sendText] ❌ Failed to save push data:`, error);
|
|
112
113
|
// 如果持久化失败,仍然继续发送(不阻塞主流程)
|
|
113
114
|
pushDataId = "";
|
|
114
115
|
}
|
|
115
116
|
// 2. 读取所有 pushId
|
|
116
|
-
|
|
117
|
+
logger.log(`[xyOutbound.sendText] Loading all pushIds...`);
|
|
117
118
|
let pushIdList = [];
|
|
118
119
|
try {
|
|
119
120
|
pushIdList = await getAllPushIds();
|
|
120
|
-
|
|
121
|
+
logger.log(`[xyOutbound.sendText] ✅ Loaded ${pushIdList.length} pushIds`);
|
|
121
122
|
}
|
|
122
123
|
catch (error) {
|
|
123
|
-
|
|
124
|
+
logger.error(`[xyOutbound.sendText] ❌ Failed to load pushIds:`, error);
|
|
124
125
|
}
|
|
125
126
|
// 3. 如果 pushIdList 为空,回退到原有逻辑(使用 config pushId)
|
|
126
127
|
if (pushIdList.length === 0) {
|
|
127
|
-
|
|
128
|
+
logger.log(`[xyOutbound.sendText] ⚠️ No pushIds found, falling back to config pushId`);
|
|
128
129
|
pushIdList = [config.pushId];
|
|
129
130
|
}
|
|
130
131
|
// Create push service
|
|
@@ -134,7 +135,7 @@ export const xyOutbound = {
|
|
|
134
135
|
// Truncate push content to max length 1000
|
|
135
136
|
const pushText = text.length > 1000 ? text.slice(0, 1000) : text;
|
|
136
137
|
// 4. 遍历所有 pushId,依次发送推送通知
|
|
137
|
-
|
|
138
|
+
logger.log(`[xyOutbound.sendText] 📤 Broadcasting to ${pushIdList.length} pushId(s)...`);
|
|
138
139
|
let successCount = 0;
|
|
139
140
|
let failureCount = 0;
|
|
140
141
|
for (const pushId of pushIdList) {
|
|
@@ -142,11 +143,11 @@ export const xyOutbound = {
|
|
|
142
143
|
// 传入 pushId 和 pushDataId,使用 kind="data" 格式
|
|
143
144
|
await pushService.sendPush(pushText, title, undefined, actualTo, pushDataId, pushId);
|
|
144
145
|
successCount++;
|
|
145
|
-
|
|
146
|
+
logger.log(`[xyOutbound.sendText] ✅ Sent successfully to pushId: ${pushId.substring(0, 20)}...`);
|
|
146
147
|
}
|
|
147
148
|
catch (error) {
|
|
148
149
|
failureCount++;
|
|
149
|
-
|
|
150
|
+
logger.error(`[xyOutbound.sendText] ❌ Failed to send to pushId: ${pushId.substring(0, 20)}...`, error);
|
|
150
151
|
// 单个 pushId 发送失败不影响其他,继续处理下一个
|
|
151
152
|
}
|
|
152
153
|
}
|
|
@@ -178,7 +179,7 @@ export const xyOutbound = {
|
|
|
178
179
|
if (!fileId) {
|
|
179
180
|
throw new Error(`File upload returned empty fileId for: ${mediaUrl}`);
|
|
180
181
|
}
|
|
181
|
-
|
|
182
|
+
logger.log(`[xyOutbound.sendMedia] File uploaded:`, {
|
|
182
183
|
fileId,
|
|
183
184
|
sessionId,
|
|
184
185
|
taskId,
|
|
@@ -222,7 +223,7 @@ export const xyOutbound = {
|
|
|
222
223
|
const { getXYWebSocketManager } = await import("./client.js");
|
|
223
224
|
const wsManager = getXYWebSocketManager(config);
|
|
224
225
|
await wsManager.sendMessage(sessionId, agentResponse);
|
|
225
|
-
|
|
226
|
+
logger.log(`[xyOutbound.sendMedia] WebSocket message sent successfully`);
|
|
226
227
|
// Return message info
|
|
227
228
|
return {
|
|
228
229
|
channel: "xiaoyi-channel",
|
package/dist/src/provider.js
CHANGED
|
@@ -127,7 +127,7 @@ function createRetryingStream(createStream, cronJob) {
|
|
|
127
127
|
if (!hasContent && !isContent) {
|
|
128
128
|
// ── Buffer phase (no content yet) ──
|
|
129
129
|
if (event.type === "done") {
|
|
130
|
-
|
|
130
|
+
logger.log(`[xiaoyiprovider] stream completed (no content), usage: input=${event.message?.usage?.input} output=${event.message?.usage?.output}`);
|
|
131
131
|
for (const b of buffer)
|
|
132
132
|
yield b;
|
|
133
133
|
resultResolve(event.message);
|
|
@@ -142,7 +142,7 @@ function createRetryingStream(createStream, cronJob) {
|
|
|
142
142
|
else {
|
|
143
143
|
// ── Streaming phase ──
|
|
144
144
|
if (!hasContent) {
|
|
145
|
-
|
|
145
|
+
logger.log("[xiaoyiprovider] first content event received, switching to streaming mode");
|
|
146
146
|
hasContent = true;
|
|
147
147
|
for (const b of buffer)
|
|
148
148
|
yield b;
|
|
@@ -151,13 +151,13 @@ function createRetryingStream(createStream, cronJob) {
|
|
|
151
151
|
// The SDK calls result() when it sees done/error — if we yield first, the generator
|
|
152
152
|
// suspends and can never reach resolve, causing a permanent deadlock.
|
|
153
153
|
if (event.type === "done") {
|
|
154
|
-
|
|
154
|
+
logger.log(`[xiaoyiprovider] stream completed, usage: input=${event.message?.usage?.input} output=${event.message?.usage?.output}`);
|
|
155
155
|
resultResolve(event.message);
|
|
156
156
|
yield event;
|
|
157
157
|
return;
|
|
158
158
|
}
|
|
159
159
|
if (event.type === "error") {
|
|
160
|
-
|
|
160
|
+
logger.log(`[xiaoyiprovider] stream error after content: ${event.error?.errorMessage}`);
|
|
161
161
|
errorResult = event.error;
|
|
162
162
|
break; // break inner loop, proceed to retry decision
|
|
163
163
|
}
|
|
@@ -168,15 +168,15 @@ function createRetryingStream(createStream, cronJob) {
|
|
|
168
168
|
if (errorResult?.stopReason === "error" && isRetryableProviderError(errorResult.errorMessage)) {
|
|
169
169
|
if (attempt < MAX_RETRY_ATTEMPTS - 1) {
|
|
170
170
|
const delayMs = getRetryDelayMs(attempt + 1, cronJob);
|
|
171
|
-
|
|
171
|
+
logger.log(`[xiaoyiprovider] retryable error (attempt ${attempt + 1}/${MAX_RETRY_ATTEMPTS}): ` +
|
|
172
172
|
`${errorResult.errorMessage} — retrying in ${delayMs}ms`);
|
|
173
173
|
await sleep(delayMs);
|
|
174
174
|
continue; // discard buffer, retry with a new stream
|
|
175
175
|
}
|
|
176
|
-
|
|
176
|
+
logger.log(`[xiaoyiprovider] all ${MAX_RETRY_ATTEMPTS} retries exhausted, surfacing last error`);
|
|
177
177
|
}
|
|
178
178
|
else if (errorResult) {
|
|
179
|
-
|
|
179
|
+
logger.log(`[xiaoyiprovider] non-retryable error: ${errorResult.errorMessage}`);
|
|
180
180
|
}
|
|
181
181
|
// Non-retryable or retries exhausted — yield buffered events.
|
|
182
182
|
// Resolve before yielding the terminal event to avoid the same deadlock.
|
|
@@ -196,7 +196,7 @@ function createRetryingStream(createStream, cronJob) {
|
|
|
196
196
|
return;
|
|
197
197
|
}
|
|
198
198
|
// Safety: final fallback attempt
|
|
199
|
-
|
|
199
|
+
logger.log("[xiaoyiprovider] entering final fallback attempt");
|
|
200
200
|
const lastStream = await createStream();
|
|
201
201
|
for await (const event of lastStream) {
|
|
202
202
|
if (event.type === "done") {
|
|
@@ -485,9 +485,9 @@ export const xiaoyiProvider = {
|
|
|
485
485
|
}
|
|
486
486
|
}
|
|
487
487
|
// 记录输入
|
|
488
|
-
|
|
488
|
+
logger.log(`[xiaoyiprovider] input messages count: ${context.messages?.length ?? 0}`);
|
|
489
489
|
if (context.systemPrompt) {
|
|
490
|
-
|
|
490
|
+
logger.log(`[xiaoyiprovider] system prompt length: ${context.systemPrompt.length}`);
|
|
491
491
|
}
|
|
492
492
|
// Reuse deviceType from extraParams instead of calling getCurrentSessionContext()
|
|
493
493
|
// again (which may be ambiguous in multi-session or async scenarios).
|
|
@@ -518,7 +518,7 @@ export const xiaoyiProvider = {
|
|
|
518
518
|
sp = sp.replace('## Runtime', combined + '\n\n## Runtime');
|
|
519
519
|
}
|
|
520
520
|
}
|
|
521
|
-
|
|
521
|
+
logger.log(`[xiaoyiprovider] system prompt optimized: ${beforeLen} -> ${sp.length}`);
|
|
522
522
|
context.systemPrompt = sp;
|
|
523
523
|
}
|
|
524
524
|
const selfEvolutionEnabled = await selfEvolutionManager.isEnabled();
|
|
@@ -550,7 +550,7 @@ export const xiaoyiProvider = {
|
|
|
550
550
|
// ── Retry-capable streaming ──────────────────────────────
|
|
551
551
|
const cronJob = isCronTriggered(context.messages);
|
|
552
552
|
if (cronJob)
|
|
553
|
-
|
|
553
|
+
logger.log("[xiaoyiprovider] detected cron-triggered request, using extended retry delays");
|
|
554
554
|
const makeStream = () => underlying(model, context, {
|
|
555
555
|
...options,
|
|
556
556
|
headers: {
|
package/dist/src/push.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Push message service for scheduled tasks
|
|
2
2
|
import fetch from "node-fetch";
|
|
3
3
|
import { randomUUID } from "crypto";
|
|
4
|
+
import { logger } from "./utils/logger.js";
|
|
4
5
|
/**
|
|
5
6
|
* Service for sending push messages to users.
|
|
6
7
|
* Used for outbound messages and scheduled tasks.
|
|
@@ -33,8 +34,8 @@ export class XYPushService {
|
|
|
33
34
|
const traceId = this.generateTraceId();
|
|
34
35
|
// Use provided pushId or fall back to config pushId
|
|
35
36
|
const actualPushId = pushId || this.config.pushId;
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
logger.log(`[PUSH] 📤 Preparing to send push message`);
|
|
38
|
+
logger.log(`[PUSH] - Using pushId: ${actualPushId.substring(0, 20)}...`);
|
|
38
39
|
try {
|
|
39
40
|
const requestBody = {
|
|
40
41
|
jsonrpc: "2.0",
|
|
@@ -80,12 +81,12 @@ export class XYPushService {
|
|
|
80
81
|
body: JSON.stringify(requestBody),
|
|
81
82
|
});
|
|
82
83
|
// Log response status and headers
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
logger.log(`[PUSH] 📥 Response received`);
|
|
85
|
+
logger.log(`[PUSH] - HTTP Status: ${response.status} ${response.statusText}`);
|
|
85
86
|
if (!response.ok) {
|
|
86
87
|
const errorText = await response.text();
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
logger.log(`[PUSH] ❌ Push request failed`);
|
|
89
|
+
logger.log(`[PUSH] - HTTP Status: ${response.status}`);
|
|
89
90
|
throw new Error(`Push failed: HTTP ${response.status} - ${errorText}`);
|
|
90
91
|
}
|
|
91
92
|
// Try to parse JSON response with detailed error handling
|
|
@@ -93,7 +94,7 @@ export class XYPushService {
|
|
|
93
94
|
try {
|
|
94
95
|
const responseText = await response.text();
|
|
95
96
|
if (!responseText || responseText.trim() === '') {
|
|
96
|
-
|
|
97
|
+
logger.log(`[PUSH] ⚠️ Received empty response body`);
|
|
97
98
|
result = {};
|
|
98
99
|
}
|
|
99
100
|
else {
|
|
@@ -101,21 +102,21 @@ export class XYPushService {
|
|
|
101
102
|
}
|
|
102
103
|
}
|
|
103
104
|
catch (parseError) {
|
|
104
|
-
|
|
105
|
-
|
|
105
|
+
logger.log(`[PUSH] ❌ Failed to parse JSON response`);
|
|
106
|
+
logger.log(`[PUSH] - Parse error: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
|
|
106
107
|
throw new Error(`Invalid JSON response from push service: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
|
|
107
108
|
}
|
|
108
|
-
|
|
109
|
-
|
|
109
|
+
logger.log(`[PUSH] ✅ Push message sent successfully`);
|
|
110
|
+
logger.log(`[PUSH] - Trace ID: ${traceId}`);
|
|
110
111
|
}
|
|
111
112
|
catch (error) {
|
|
112
|
-
|
|
113
|
+
logger.log(`[PUSH] ❌ Failed to send push message`);
|
|
113
114
|
if (error instanceof Error) {
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
logger.log(`[PUSH] - Error name: ${error.name}`);
|
|
116
|
+
logger.log(`[PUSH] - Error message: ${error.message}`);
|
|
116
117
|
}
|
|
117
118
|
else {
|
|
118
|
-
|
|
119
|
+
logger.log(`[PUSH] - Error:`, error);
|
|
119
120
|
}
|
|
120
121
|
throw error;
|
|
121
122
|
}
|
|
@@ -4,6 +4,7 @@ import { resolveXYConfig } from "./config.js";
|
|
|
4
4
|
import { getCurrentTaskId, getCurrentMessageId } from "./task-manager.js";
|
|
5
5
|
import fs from "fs/promises";
|
|
6
6
|
import path from "path";
|
|
7
|
+
import { logger } from "./utils/logger.js";
|
|
7
8
|
const TEMP_FILE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
8
9
|
/**
|
|
9
10
|
* 清理 /tmp/xy_channel 目录中超过 24 小时的旧文件
|
|
@@ -31,11 +32,11 @@ export async function cleanupStaleTempFiles(tempDir = "/tmp/xy_channel") {
|
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
if (cleanedCount > 0) {
|
|
34
|
-
|
|
35
|
+
logger.log(`[CLEANUP] 🧹 Cleaned ${cleanedCount} stale files (>${TEMP_FILE_TTL_MS / 1000 / 3600}h) from ${tempDir}`);
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
catch (err) {
|
|
38
|
-
|
|
39
|
+
logger.error(`[CLEANUP] ❌ Failed to cleanup temp dir:`, err);
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
42
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { searchTools, formatToolsForContext, extractUserQuery } from "./tool-search.js";
|
|
2
|
+
import { logger } from "../utils/logger.js";
|
|
2
3
|
const TOOL_RETRIEVER_HEADER = `[系统消息,非用户发言]
|
|
3
4
|
|
|
4
5
|
`;
|
|
@@ -63,10 +64,10 @@ export function createBeforePromptBuildHandler(config) {
|
|
|
63
64
|
if (!searchResult || searchResult.tools.length === 0) {
|
|
64
65
|
return undefined;
|
|
65
66
|
}
|
|
66
|
-
|
|
67
|
+
logger.log(`${PLUGIN_LOG_PREFIX} [RESULT] Found ${searchResult.tools.length} skills, building context...`);
|
|
67
68
|
const toolsContext = formatToolsForContext(searchResult, config.includeUninstalledOnly);
|
|
68
69
|
if (!toolsContext) {
|
|
69
|
-
|
|
70
|
+
logger.log(`${PLUGIN_LOG_PREFIX} [ERROR] Failed to format skills context`);
|
|
70
71
|
return undefined;
|
|
71
72
|
}
|
|
72
73
|
return {
|
|
@@ -75,7 +76,7 @@ export function createBeforePromptBuildHandler(config) {
|
|
|
75
76
|
}
|
|
76
77
|
catch (error) {
|
|
77
78
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
78
|
-
|
|
79
|
+
logger.error(`${PLUGIN_LOG_PREFIX} [ERROR] ${errorMessage}, original query: "${extractedQuery}"`);
|
|
79
80
|
return undefined;
|
|
80
81
|
}
|
|
81
82
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import * as os from "os";
|
|
4
|
+
import { logger } from "../utils/logger.js";
|
|
4
5
|
const SKILL_ID = "celia_find_skills";
|
|
5
6
|
const PLUGIN_LOG_PREFIX = "[skill-retriever]";
|
|
6
7
|
export function extractUserQuery(fullPrompt) {
|
|
@@ -89,7 +90,7 @@ export async function searchTools(options) {
|
|
|
89
90
|
const apiKey = configApiKey ?? envConfig.PERSONAL_API_KEY;
|
|
90
91
|
const uid = configUid ?? envConfig.PERSONAL_UID;
|
|
91
92
|
if (!serviceUrl || !apiKey || !uid) {
|
|
92
|
-
|
|
93
|
+
logger.warn(`${PLUGIN_LOG_PREFIX} Missing required configuration. serviceUrl: "${serviceUrl}", apiKey: "${apiKey ? '(set)' : '(missing)'} ", uid: "${uid ? '(set)' : '(missing)'}"`);
|
|
93
94
|
return null;
|
|
94
95
|
}
|
|
95
96
|
const traceId = crypto.randomUUID();
|
|
@@ -111,10 +112,10 @@ export async function searchTools(options) {
|
|
|
111
112
|
signal: AbortSignal.timeout(timeoutMs),
|
|
112
113
|
});
|
|
113
114
|
if (!response.ok) {
|
|
114
|
-
|
|
115
|
+
logger.warn(`${PLUGIN_LOG_PREFIX} HTTP error: ${response.status} ${response.statusText}`);
|
|
115
116
|
return null;
|
|
116
117
|
}
|
|
117
|
-
|
|
118
|
+
logger.log(`${PLUGIN_LOG_PREFIX} Received response, status: ${response.status}`);
|
|
118
119
|
const responseData = await response.json();
|
|
119
120
|
if (responseData.errorCode === "0" &&
|
|
120
121
|
responseData.content &&
|
|
@@ -125,18 +126,18 @@ export async function searchTools(options) {
|
|
|
125
126
|
const topTools = formattedData.slice(0, 2);
|
|
126
127
|
const allInstalled = topTools.every((tool) => tool.status === "已安装");
|
|
127
128
|
if (allInstalled) {
|
|
128
|
-
|
|
129
|
+
logger.log(`${PLUGIN_LOG_PREFIX} [DEBUG] All top 2 skills are installed, returning null`);
|
|
129
130
|
return null;
|
|
130
131
|
}
|
|
131
132
|
const hasInstalledWithHighScore = topTools.some((tool) => tool.status === "已安装" && (tool.rrfScore ?? 0) >= 0.016);
|
|
132
133
|
if (hasInstalledWithHighScore) {
|
|
133
|
-
|
|
134
|
+
logger.log(`${PLUGIN_LOG_PREFIX} [DEBUG] Top 2 has installed skill with rrfScore >= 0.016, returning null`);
|
|
134
135
|
return null;
|
|
135
136
|
}
|
|
136
137
|
let filteredTools = topTools.filter((tool) => tool.status === "未安装" && (tool.rrfScore ?? 0) >= 0.016);
|
|
137
|
-
|
|
138
|
+
logger.log(`${PLUGIN_LOG_PREFIX} [DEBUG] After filtering uninstalled with rrfScore >= 0.016: ${filteredTools.length}, details: ${filteredTools.map((t) => `${t.skillId}(rrfScore=${t.rrfScore})`).join(", ")}`);
|
|
138
139
|
if (filteredTools.length === 0) {
|
|
139
|
-
|
|
140
|
+
logger.log(`${PLUGIN_LOG_PREFIX} [DEBUG] No uninstalled skills with rrfScore >= 0.016, returning null`);
|
|
140
141
|
return null;
|
|
141
142
|
}
|
|
142
143
|
return {
|
|
@@ -145,7 +146,7 @@ export async function searchTools(options) {
|
|
|
145
146
|
timestamp: Date.now(),
|
|
146
147
|
};
|
|
147
148
|
}
|
|
148
|
-
|
|
149
|
+
logger.warn(`${PLUGIN_LOG_PREFIX} Invalid response format: ${JSON.stringify(responseData).slice(0, 200)}`);
|
|
149
150
|
return null;
|
|
150
151
|
}
|
|
151
152
|
catch (error) {
|
|
@@ -153,7 +154,7 @@ export async function searchTools(options) {
|
|
|
153
154
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
154
155
|
const errorCause = error instanceof Error && error.cause ? JSON.stringify(error.cause) : "N/A";
|
|
155
156
|
const errorStack = error instanceof Error ? error.stack?.split("\n").slice(0, 3).join(" | ") : "N/A";
|
|
156
|
-
|
|
157
|
+
logger.warn(`${PLUGIN_LOG_PREFIX} [ERROR] Fetch failed - name: ${errorName}, message: ${errorMessage}, cause: ${errorCause}, stack: ${errorStack}`);
|
|
157
158
|
return null;
|
|
158
159
|
}
|
|
159
160
|
}
|
|
@@ -57,7 +57,7 @@ export async function tryInjectSteer(sessionKey, message) {
|
|
|
57
57
|
},
|
|
58
58
|
},
|
|
59
59
|
};
|
|
60
|
-
|
|
60
|
+
logger.log(`[STEER] Injecting steer for sessionId=${sessionId}, taskId=${syntheticMessage.params.id}`);
|
|
61
61
|
try {
|
|
62
62
|
await handleXYMessage({
|
|
63
63
|
cfg: cachedCfg,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getXYWebSocketManager } from "../client.js";
|
|
2
2
|
import { XYFileUploadService } from "../file-upload.js";
|
|
3
|
+
import { logger } from "../utils/logger.js";
|
|
3
4
|
import fetch from "node-fetch";
|
|
4
5
|
import fs from "fs/promises";
|
|
5
6
|
import path from "path";
|
|
@@ -97,6 +98,7 @@ async function downloadRemoteFile(url) {
|
|
|
97
98
|
*/
|
|
98
99
|
export function createSendFileToUserTool(ctx) {
|
|
99
100
|
const { config, sessionId, taskId, messageId } = ctx;
|
|
101
|
+
logger.log(`[SEND-FILE-TO-USER] 🏭 CREATE: sessionId=${sessionId} taskId=${taskId}`);
|
|
100
102
|
return {
|
|
101
103
|
name: "send_file_to_user",
|
|
102
104
|
label: "Send File to User",
|
|
@@ -235,9 +237,10 @@ b. 操作超时时间为2分钟(120秒),请勿重复调用此工具,如
|
|
|
235
237
|
error: { code: 0 },
|
|
236
238
|
}),
|
|
237
239
|
};
|
|
240
|
+
logger.log(`[SEND-FILE-TO-USER] 🚀 EXEC sending: sessionId=${sessionId} taskId=${taskId} fileName=${fileName}`);
|
|
238
241
|
// Send WebSocket message
|
|
239
242
|
await wsManager.sendMessage(sessionId, agentResponse);
|
|
240
|
-
|
|
243
|
+
logger.log(`send ${fileName} file to user success`);
|
|
241
244
|
sentFiles.push({ fileName, fileId });
|
|
242
245
|
}
|
|
243
246
|
return {
|
|
@@ -19,6 +19,13 @@ if (!_g.__xyActiveSessions) {
|
|
|
19
19
|
_g.__xyActiveSessions = new Map();
|
|
20
20
|
}
|
|
21
21
|
const activeSessions = _g.__xyActiveSessions;
|
|
22
|
+
// Track the most recently registered sessionKey for reliable fallback
|
|
23
|
+
// when AsyncLocalStorage context is lost across openclaw's embedded runner boundary.
|
|
24
|
+
if (!_g.__xyLastRegisteredSessionKey) {
|
|
25
|
+
_g.__xyLastRegisteredSessionKey = "";
|
|
26
|
+
}
|
|
27
|
+
const getLastRegisteredKey = () => _g.__xyLastRegisteredSessionKey;
|
|
28
|
+
const setLastRegisteredKey = (key) => { _g.__xyLastRegisteredSessionKey = key; };
|
|
22
29
|
// AsyncLocalStorage for thread-safe session context isolation
|
|
23
30
|
const asyncLocalStorage = new AsyncLocalStorage();
|
|
24
31
|
/**
|
|
@@ -26,6 +33,8 @@ const asyncLocalStorage = new AsyncLocalStorage();
|
|
|
26
33
|
* Should be called when starting to process a message.
|
|
27
34
|
*/
|
|
28
35
|
export function registerSession(sessionKey, context) {
|
|
36
|
+
// Track last registered session for reliable ALS-miss fallback
|
|
37
|
+
setLastRegisteredKey(sessionKey);
|
|
29
38
|
const existing = activeSessions.get(sessionKey);
|
|
30
39
|
if (existing) {
|
|
31
40
|
// 更新上下文,增加引用计数,刷新存活时间
|
|
@@ -94,6 +103,7 @@ export function getLatestSessionContext() {
|
|
|
94
103
|
* This ensures thread-safe context isolation for concurrent requests.
|
|
95
104
|
*/
|
|
96
105
|
export function runWithSessionContext(context, callback) {
|
|
106
|
+
logger.log(`[SESSION-MGR] 🔵 ALS SET: sessionId=${context.sessionId} taskId=${context.taskId}`);
|
|
97
107
|
return asyncLocalStorage.run(context, callback);
|
|
98
108
|
}
|
|
99
109
|
/**
|
|
@@ -112,6 +122,9 @@ export function getCurrentSessionContext(sessionKey) {
|
|
|
112
122
|
if (alsContext) {
|
|
113
123
|
return enrichWithLatestTaskInfo(alsContext);
|
|
114
124
|
}
|
|
125
|
+
// ALS not available — logging to understand when/why
|
|
126
|
+
const stack = new Error().stack?.split("\n").slice(2, 5).map(s => s.trim()).join(" | ");
|
|
127
|
+
logger.log(`[SESSION-MGR] ⚠️ ALS miss, falling back to Map (size=${activeSessions.size}), callers: ${stack}`);
|
|
115
128
|
// 2. Fallback: look up from global activeSessions Map
|
|
116
129
|
if (activeSessions.size === 0) {
|
|
117
130
|
return null;
|
|
@@ -142,29 +155,32 @@ export function getCurrentSessionContext(sessionKey) {
|
|
|
142
155
|
}
|
|
143
156
|
return null;
|
|
144
157
|
}
|
|
145
|
-
// 2c. Multiple sessions —
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
|
|
158
|
+
// 2c. Multiple sessions — prefer the last registered session.
|
|
159
|
+
// This is the most reliable heuristic when ALS is lost across openclaw's
|
|
160
|
+
// embedded runner boundary: registerSession() is called just before
|
|
161
|
+
// runWithSessionContext(), and agentTools() is called during tool
|
|
162
|
+
// compilation shortly after. The last registered session is always the
|
|
163
|
+
// one currently being set up.
|
|
164
|
+
const lastKey = getLastRegisteredKey();
|
|
165
|
+
if (lastKey) {
|
|
166
|
+
const lastEntry = activeSessions.get(lastKey);
|
|
167
|
+
if (lastEntry) {
|
|
168
|
+
logger.log(`[SESSION-MGR] 🎯 using lastRegistered session: ${lastKey}`);
|
|
169
|
+
const { refCount, createdAt, ...context } = lastEntry;
|
|
170
|
+
return enrichWithLatestTaskInfo(context);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// 2d. Fallback: find any non-stale session
|
|
149
174
|
const now = Date.now();
|
|
150
175
|
for (const [key, entry] of activeSessions) {
|
|
151
|
-
// Skip stale sessions
|
|
152
176
|
if (now - entry.createdAt > SESSION_TTL_MS) {
|
|
153
|
-
logger.log(`[SESSION-MGR] stale session detected, cleaning up: ${key}`);
|
|
154
177
|
configManager.clearSession(entry.sessionId);
|
|
155
178
|
toolCallNudgeManager.clearSession(key);
|
|
156
179
|
activeSessions.delete(key);
|
|
157
180
|
continue;
|
|
158
181
|
}
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
if (!bestMatch || recency > bestMatch.recency) {
|
|
162
|
-
const { refCount, createdAt, ...context } = entry;
|
|
163
|
-
bestMatch = { context, recency };
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
if (bestMatch) {
|
|
167
|
-
return enrichWithLatestTaskInfo(bestMatch.context);
|
|
182
|
+
const { refCount, createdAt, ...context } = entry;
|
|
183
|
+
return enrichWithLatestTaskInfo(context);
|
|
168
184
|
}
|
|
169
185
|
return null;
|
|
170
186
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import fsp from "node:fs/promises";
|
|
3
|
+
import { logger } from "./logger.js";
|
|
3
4
|
const SELF_EVOLUTION_ENV_FILE = "/home/sandbox/.openclaw/.xiaoyiruntime";
|
|
4
5
|
const SELF_EVOLUTION_ENV_KEY = "selfEvolutionState";
|
|
5
6
|
function parseBooleanLike(value) {
|
|
@@ -24,7 +25,7 @@ class SelfEvolutionManager {
|
|
|
24
25
|
catch (error) {
|
|
25
26
|
const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
|
|
26
27
|
if (code !== "ENOENT") {
|
|
27
|
-
|
|
28
|
+
logger.error(`[SELF_EVOLUTION] Failed to read ${SELF_EVOLUTION_ENV_FILE}:`, error);
|
|
28
29
|
}
|
|
29
30
|
return false;
|
|
30
31
|
}
|
|
@@ -59,7 +60,7 @@ class SelfEvolutionManager {
|
|
|
59
60
|
catch (error) {
|
|
60
61
|
const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
|
|
61
62
|
if (code !== "ENOENT") {
|
|
62
|
-
|
|
63
|
+
logger.error(`[SELF_EVOLUTION] Failed to read ${SELF_EVOLUTION_ENV_FILE}:`, error);
|
|
63
64
|
}
|
|
64
65
|
return false;
|
|
65
66
|
}
|
package/dist/src/websocket.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// WebSocket connection manager (Single connection)
|
|
2
|
+
import os from "os";
|
|
2
3
|
import WebSocket from "ws";
|
|
3
4
|
import { EventEmitter } from "events";
|
|
4
5
|
import { HeartbeatManager } from "./heartbeat.js";
|
|
@@ -113,6 +114,7 @@ export class XYWebSocketManager extends EventEmitter {
|
|
|
113
114
|
throw new Error("WebSocket not ready");
|
|
114
115
|
}
|
|
115
116
|
const messageStr = JSON.stringify(message);
|
|
117
|
+
this.log(`[WS-SEND] sessionId=${sessionId} taskId=${message.taskId} msgType=${message.msgType} len=${messageStr.length}`);
|
|
116
118
|
this.ws.send(messageStr);
|
|
117
119
|
}
|
|
118
120
|
/**
|
|
@@ -292,15 +294,16 @@ export class XYWebSocketManager extends EventEmitter {
|
|
|
292
294
|
this.error("Cannot send init message: WebSocket not open");
|
|
293
295
|
return;
|
|
294
296
|
}
|
|
297
|
+
const hostname = os.hostname();
|
|
295
298
|
const initMessage = {
|
|
296
299
|
msgType: "clawd_bot_init",
|
|
297
300
|
agentId: this.config.agentId,
|
|
298
|
-
msgDetail: JSON.stringify({ agentId: this.config.agentId }),
|
|
301
|
+
msgDetail: JSON.stringify({ agentId: this.config.agentId, hostname }),
|
|
299
302
|
};
|
|
300
303
|
const initMessageStr = JSON.stringify(initMessage);
|
|
301
|
-
|
|
304
|
+
this.log("[WS-SEND] Sending init message frame:", JSON.stringify(initMessage, null, 2));
|
|
302
305
|
this.ws.send(initMessageStr);
|
|
303
|
-
|
|
306
|
+
this.log(`[WS-SEND] Init message sent successfully, size: ${initMessageStr.length} bytes`);
|
|
304
307
|
// Mark as ready after init
|
|
305
308
|
this.state.ready = true;
|
|
306
309
|
this.emit("ready");
|
|
@@ -360,7 +363,7 @@ export class XYWebSocketManager extends EventEmitter {
|
|
|
360
363
|
handleMessage(data) {
|
|
361
364
|
try {
|
|
362
365
|
const messageStr = data.toString();
|
|
363
|
-
|
|
366
|
+
this.log(`[WS-RECV] Raw message frame, size: ${messageStr.length} characters`);
|
|
364
367
|
const parsed = JSON.parse(messageStr);
|
|
365
368
|
// 提取并打印消息内容(只显示 text,data 只打印提示)
|
|
366
369
|
const parts = parsed.params?.message?.parts;
|
|
@@ -386,7 +389,7 @@ export class XYWebSocketManager extends EventEmitter {
|
|
|
386
389
|
// 如果长度 > 8,显示前5个 + *** + 后5个
|
|
387
390
|
maskedText = `${textContents.slice(0, 5)}***${textContents.slice(-5)}`;
|
|
388
391
|
}
|
|
389
|
-
|
|
392
|
+
this.log("[WS-RECV] Text:", maskedText);
|
|
390
393
|
}
|
|
391
394
|
}
|
|
392
395
|
}
|
|
@@ -396,7 +399,7 @@ export class XYWebSocketManager extends EventEmitter {
|
|
|
396
399
|
// Extract sessionId from params
|
|
397
400
|
const sessionId = a2aRequest.params?.sessionId;
|
|
398
401
|
if (!sessionId) {
|
|
399
|
-
|
|
402
|
+
this.error("[XY] Message missing sessionId");
|
|
400
403
|
return;
|
|
401
404
|
}
|
|
402
405
|
// Check if message contains only data parts (tool results)
|
|
@@ -407,10 +410,10 @@ export class XYWebSocketManager extends EventEmitter {
|
|
|
407
410
|
for (const dataPart of dataParts) {
|
|
408
411
|
const events = dataPart.data?.events;
|
|
409
412
|
if (!Array.isArray(events)) {
|
|
410
|
-
|
|
413
|
+
this.log("[XY] dataPart.data.events is not an array, skipping");
|
|
411
414
|
continue;
|
|
412
415
|
}
|
|
413
|
-
|
|
416
|
+
this.log(`[XY] Processing ${events.length} events from data.events`);
|
|
414
417
|
for (const item of events) {
|
|
415
418
|
if (item.header?.name === "UploadExeResult" && item.payload?.intentName) {
|
|
416
419
|
const dataEvent = {
|
|
@@ -418,15 +421,15 @@ export class XYWebSocketManager extends EventEmitter {
|
|
|
418
421
|
outputs: item.payload.outputs || {},
|
|
419
422
|
status: "success",
|
|
420
423
|
};
|
|
421
|
-
|
|
424
|
+
this.log(`[XY] Emitting data-event, intentName: ${item.payload.intentName}, size: ${JSON.stringify(dataEvent).length} bytes`);
|
|
422
425
|
this.emit("data-event", dataEvent);
|
|
423
426
|
}
|
|
424
427
|
else if (item.header?.namespace === "ClawAgent" && item.header?.name === "InvokeJarvisGUIAgentResponse") {
|
|
425
|
-
|
|
428
|
+
this.log(`[XY] Emitting gui-agent-response, size: ${JSON.stringify(item).length} bytes`);
|
|
426
429
|
this.emit("gui-agent-response", item);
|
|
427
430
|
}
|
|
428
431
|
else if (item.header?.namespace === "Common" && item.header?.name === "Trigger") {
|
|
429
|
-
|
|
432
|
+
this.log("[XY] Trigger event detected, emitting trigger-event with context");
|
|
430
433
|
// 传递完整上下文:event、sessionId、taskId
|
|
431
434
|
this.emit("trigger-event", {
|
|
432
435
|
event: item,
|
|
@@ -435,13 +438,13 @@ export class XYWebSocketManager extends EventEmitter {
|
|
|
435
438
|
});
|
|
436
439
|
}
|
|
437
440
|
else if (item.header?.namespace === "AgentEvent" && item.header?.name === "ClawSelfEvolutionState") {
|
|
438
|
-
|
|
441
|
+
this.log("[XY] ClawSelfEvolutionState event detected, emitting self-evolution-event");
|
|
439
442
|
this.emit("self-evolution-event", {
|
|
440
443
|
event: item,
|
|
441
444
|
});
|
|
442
445
|
}
|
|
443
446
|
else if (item.header?.namespace === "AgentEvent" && item.header?.name === "ClawSelfEvolutionStateGet") {
|
|
444
|
-
|
|
447
|
+
this.log("[XY] ClawSelfEvolutionStateGet event detected, emitting self-evolution-state-get-event");
|
|
445
448
|
this.emit("self-evolution-state-get-event", {
|
|
446
449
|
event: item,
|
|
447
450
|
sessionId: sessionId,
|
|
@@ -450,7 +453,7 @@ export class XYWebSocketManager extends EventEmitter {
|
|
|
450
453
|
});
|
|
451
454
|
}
|
|
452
455
|
else if (item.header?.namespace === "LoginTokenEvent" && item.header?.name === "ClawAutoLogin") {
|
|
453
|
-
|
|
456
|
+
this.log("[XY] LoginTokenEvent.ClawAutoLogin detected, emitting login-token-event");
|
|
454
457
|
this.emit("login-token-event", {
|
|
455
458
|
event: item,
|
|
456
459
|
});
|
|
@@ -465,16 +468,16 @@ export class XYWebSocketManager extends EventEmitter {
|
|
|
465
468
|
}
|
|
466
469
|
// Wrapped format (InboundWebSocketMessage)
|
|
467
470
|
const inboundMsg = parsed;
|
|
468
|
-
|
|
471
|
+
this.log(`[XY] Message type: Wrapped, msgType: ${inboundMsg.msgType}`);
|
|
469
472
|
// Handle heartbeat responses
|
|
470
473
|
if (inboundMsg.msgType === "heartbeat") {
|
|
471
|
-
|
|
474
|
+
this.log("[XY] Received heartbeat response");
|
|
472
475
|
this.onHealthEvent?.();
|
|
473
476
|
return;
|
|
474
477
|
}
|
|
475
478
|
// Handle data messages
|
|
476
479
|
if (inboundMsg.msgType === "data") {
|
|
477
|
-
|
|
480
|
+
this.log("[XY] Processing data message");
|
|
478
481
|
try {
|
|
479
482
|
const a2aRequest = JSON.parse(inboundMsg.msgDetail);
|
|
480
483
|
const dataParts = a2aRequest.params?.message?.parts?.filter((p) => p.kind === "data");
|
|
@@ -482,10 +485,10 @@ export class XYWebSocketManager extends EventEmitter {
|
|
|
482
485
|
for (const dataPart of dataParts) {
|
|
483
486
|
const events = dataPart.data?.events;
|
|
484
487
|
if (!Array.isArray(events)) {
|
|
485
|
-
|
|
488
|
+
this.log("[XY] dataPart.data.events is not an array, skipping");
|
|
486
489
|
continue;
|
|
487
490
|
}
|
|
488
|
-
|
|
491
|
+
this.log(`[XY] Processing ${events.length} events from data.events`);
|
|
489
492
|
for (const item of events) {
|
|
490
493
|
if (item.header?.name === "UploadExeResult" && item.payload?.intentName) {
|
|
491
494
|
const dataEvent = {
|
|
@@ -493,15 +496,15 @@ export class XYWebSocketManager extends EventEmitter {
|
|
|
493
496
|
outputs: item.payload.outputs || {},
|
|
494
497
|
status: "success",
|
|
495
498
|
};
|
|
496
|
-
|
|
499
|
+
this.log(`[XY] Emitting data-event, intentName: ${item.payload.intentName}, size: ${JSON.stringify(dataEvent).length} bytes`);
|
|
497
500
|
this.emit("data-event", dataEvent);
|
|
498
501
|
}
|
|
499
502
|
else if (item.header?.namespace === "ClawAgent" && item.header?.name === "InvokeJarvisGUIAgentResponse") {
|
|
500
|
-
|
|
503
|
+
this.log(`[XY] Emitting gui-agent-response, size: ${JSON.stringify(item).length} bytes`);
|
|
501
504
|
this.emit("gui-agent-response", item);
|
|
502
505
|
}
|
|
503
506
|
else if (item.header?.namespace === "Common" && item.header?.name === "Trigger") {
|
|
504
|
-
|
|
507
|
+
this.log("[XY] Trigger event detected (wrapped format), emitting trigger-event with context");
|
|
505
508
|
// 传递完整上下文:event、sessionId、taskId
|
|
506
509
|
this.emit("trigger-event", {
|
|
507
510
|
event: item,
|
|
@@ -510,7 +513,7 @@ export class XYWebSocketManager extends EventEmitter {
|
|
|
510
513
|
});
|
|
511
514
|
}
|
|
512
515
|
else if (item.header?.namespace === "LoginTokenEvent" && item.header?.name === "ClawAutoLogin") {
|
|
513
|
-
|
|
516
|
+
this.log("[XY] LoginTokenEvent.ClawAutoLogin detected (wrapped format), emitting login-token-event");
|
|
514
517
|
this.emit("login-token-event", {
|
|
515
518
|
event: item,
|
|
516
519
|
});
|
|
@@ -520,28 +523,28 @@ export class XYWebSocketManager extends EventEmitter {
|
|
|
520
523
|
}
|
|
521
524
|
}
|
|
522
525
|
catch (error) {
|
|
523
|
-
|
|
526
|
+
this.error("[XY] Failed to process data message:", error);
|
|
524
527
|
}
|
|
525
528
|
return;
|
|
526
529
|
}
|
|
527
530
|
// Parse msgDetail as A2AJsonRpcRequest
|
|
528
531
|
const a2aRequest = JSON.parse(inboundMsg.msgDetail);
|
|
529
|
-
|
|
532
|
+
this.log(`[XY] Parsed A2A request, method: ${a2aRequest.method}`);
|
|
530
533
|
const sessionId = inboundMsg.sessionId;
|
|
531
|
-
|
|
534
|
+
this.log(`[XY] Session ID: ${sessionId}`);
|
|
532
535
|
// Emit message event
|
|
533
|
-
|
|
536
|
+
this.log("[XY] *** EMITTING message event (Wrapped path) ***");
|
|
534
537
|
this.emit("message", a2aRequest, sessionId);
|
|
535
538
|
}
|
|
536
539
|
catch (error) {
|
|
537
|
-
|
|
540
|
+
this.error("[XY] Failed to parse message:", error);
|
|
538
541
|
}
|
|
539
542
|
}
|
|
540
543
|
/**
|
|
541
544
|
* Handle connection close.
|
|
542
545
|
*/
|
|
543
546
|
handleClose(code, reason) {
|
|
544
|
-
|
|
547
|
+
this.log(`WebSocket disconnected: code=${code}, reason=${reason}`);
|
|
545
548
|
// Only process if this is the current connection
|
|
546
549
|
if (!this.ws) {
|
|
547
550
|
this.log("Ignoring close event for already cleaned connection");
|