@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 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
- console.log("Downloaded files:", JSON.stringify(downloadedFiles, null, 2));
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);
@@ -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
- console.log(`🗑️ [WS-MANAGER-CACHE] Removing manager from cache: ${cacheKey}`);
51
+ log(`🗑️ [WS-MANAGER-CACHE] Removing manager from cache: ${cacheKey}`);
51
52
  manager.disconnect();
52
53
  wsManagerCache.delete(cacheKey);
53
- console.log(`🗑️ [WS-MANAGER-CACHE] Manager removed, remaining managers: ${wsManagerCache.size}`);
54
+ log(`🗑️ [WS-MANAGER-CACHE] Manager removed, remaining managers: ${wsManagerCache.size}`);
54
55
  }
55
56
  else {
56
- console.log(`⚠️ [WS-MANAGER-CACHE] Manager not found in cache: ${cacheKey}`);
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
- console.log(`Total cached managers: ${wsManagerCache.size}`);
82
+ const log = runtime?.log ?? console.log;
83
+ log(`Total cached managers: ${wsManagerCache.size}`);
82
84
  if (wsManagerCache.size === 0) {
83
- console.log("ℹ️ No managers in cache");
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
- console.log(` Total event listeners on manager: ${diag.totalEventListeners}`);
91
+ log(` Total event listeners on manager: ${diag.totalEventListeners}`);
90
92
  // Connection
91
- console.log(` 🔌 Connection:`);
92
- console.log(` - Exists: ${diag.connection.exists}`);
93
- console.log(` - ReadyState: ${diag.connection.readyState}`);
94
- console.log(` - State connected/ready: ${diag.connection.stateConnected}/${diag.connection.stateReady}`);
95
- console.log(` - Reconnect attempts: ${diag.connection.reconnectAttempts}`);
96
- console.log(` - Listeners on WebSocket: ${diag.connection.listenerCount}`);
97
- console.log(` - Heartbeat active: ${diag.connection.heartbeatActive}`);
98
- console.log(` - Has reconnect timer: ${diag.connection.hasReconnectTimer}`);
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
- console.log(` ⚠️ ORPHAN CONNECTION DETECTED!`);
102
+ log(` ⚠️ ORPHAN CONNECTION DETECTED!`);
101
103
  orphanCount++;
102
104
  }
103
- console.log("");
105
+ log("");
104
106
  });
105
107
  if (orphanCount > 0) {
106
- console.log(`⚠️ Total orphan connections found: ${orphanCount}`);
107
- console.log(`💡 Suggestion: These connections should be cleaned up`);
108
+ log(`⚠️ Total orphan connections found: ${orphanCount}`);
109
+ log(`💡 Suggestion: These connections should be cleaned up`);
108
110
  }
109
111
  else {
110
- console.log(`✅ No orphan connections found`);
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
- console.log(`🧹 Cleaning up orphan connections in manager: ${key}`);
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
- console.log(`🧹 Cleaned up ${cleanedCount} manager(s) with orphan connections`);
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
- console.log(`[SENTINEL HOOK] trace-id: ${traceId}`);
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
- console.log(`[SENTINEL HOOK] ✅ 请求成功`);
69
+ logger.log(`[SENTINEL HOOK] ✅ 请求成功`);
69
70
  resolve(result);
70
71
  }
71
72
  catch (e) {
72
- console.error(`[SENTINEL HOOK] ❌ 请求失败: ${e instanceof Error ? e.message : String(e)}`);
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
- console.error(`[SENTINEL HOOK] ❌ 请求错误: ${error instanceof Error ? error.message : String(error)}`);
79
+ logger.error(`[SENTINEL HOOK] ❌ 请求错误: ${error instanceof Error ? error.message : String(error)}`);
79
80
  reject(error);
80
81
  });
81
82
  req.on("timeout", () => {
82
- console.error(`[SENTINEL HOOK] ⏰ 请求超时 (${config.api.timeout}ms)`);
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
- console.log(`Download timeout (30s) for ${url}`);
23
+ logger.log(`Download timeout (30s) for ${url}`);
23
24
  throw new Error(`Download timeout after 30 seconds`);
24
25
  }
25
- console.log(`Failed to download file from ${url}:`);
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
- console.log(`Failed to download file ${name}:`);
55
+ logger.log(`Failed to download file ${name}:`);
55
56
  // Continue with other files
56
57
  }
57
58
  }
@@ -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
- console.log(`[XY File Upload] Downloading remote file: ${url}`);
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
- console.log(`[XY File Upload] Downloaded to temp file: ${tempPath}`);
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
- console.log(`[XY File Upload] Starting file upload: ${filePath}`);
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
- console.log(`[XY File Upload] Phase 1: Prepare upload for ${fileName}`);
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
- console.log(`[XY File Upload] Phase 2: Upload file data`);
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
- console.log(`[XY File Upload] Upload complete`);
99
+ logger.log(`[XY File Upload] Upload complete`);
99
100
  // Phase 3: Complete
100
- console.log(`[XY File Upload] Phase 3: Complete upload`);
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
- console.log(`[XY File Upload] File upload successful: ${fileName} → objectId=${objectId}`);
119
+ logger.log(`[XY File Upload] File upload successful: ${fileName} → objectId=${objectId}`);
119
120
  return objectId;
120
121
  }
121
122
  catch (error) {
122
- console.error(`[XY File Upload] File upload failed for ${filePath}:`, error);
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
- console.log(`[XY File Upload] Phase 1: Prepare upload for ${fileName}`);
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
- console.log(`[XY File Upload] Prepare complete: objectId=${objectId}, draftId=${draftId}`);
183
+ logger.log(`[XY File Upload] Prepare complete: objectId=${objectId}, draftId=${draftId}`);
183
184
  // Phase 2: Upload
184
- console.log(`[XY File Upload] Phase 2: Upload file data`);
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
- console.log(`[XY File Upload] Upload response status: ${uploadResp.status}`);
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
- console.log(`[XY File Upload] Upload complete`);
197
+ logger.log(`[XY File Upload] Upload complete`);
197
198
  // Phase 3: CompleteAndQuery - get file URL
198
- console.log(`[XY File Upload] Phase 3: CompleteAndQuery to get file URL`);
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
- console.log(`[XY File Upload] File upload successful`);
222
+ logger.log(`[XY File Upload] File upload successful`);
222
223
  return fileUrl;
223
224
  }
224
225
  catch (error) {
225
- console.error(`[XY File Upload] File upload with URL retrieval failed for ${filePath}:`, error);
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
- console.error(`[XY File Upload] Failed to upload ${filePath}, skipping:`, error);
253
+ logger.error(`[XY File Upload] Failed to upload ${filePath}, skipping:`, error);
253
254
  // Continue with other files
254
255
  }
255
256
  }
@@ -67,7 +67,7 @@ export class HeartbeatManager {
67
67
  */
68
68
  sendHeartbeat() {
69
69
  if (this.ws.readyState !== WebSocket.OPEN) {
70
- console.warn(`Cannot send heartbeat for ${this.serverName}: WebSocket not open`);
70
+ this.log(`Cannot send heartbeat for ${this.serverName}: WebSocket not open`);
71
71
  return;
72
72
  }
73
73
  try {
@@ -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
- console.log("🔍 [DIAGNOSTICS] Checking WebSocket managers before gateway start...");
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
- console.warn(`XY gateway: ${serverId} disconnected`);
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
- console.log("🔍 [DIAGNOSTICS] Checking WebSocket managers before cleanup...");
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
- console.log("⏸️ Stopped periodic health check");
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
- console.log("🔍 [DIAGNOSTICS] Checking WebSocket managers after cleanup...");
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
- console.log("🏥 Starting periodic health check (every 6 hours)...");
263
+ log("🏥 Starting periodic health check (every 6 hours)...");
262
264
  healthCheckInterval = setInterval(() => {
263
- console.log("🏥 [HEALTH CHECK] Periodic WebSocket diagnostics...");
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
- console.log(`🧹 [HEALTH CHECK] Auto-cleaned ${cleaned} manager(s) with orphan connections`);
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
- console.log(`🧹 [HEALTH CHECK] Sessions: cleaned=${cleanedSessions}, active=${remainingSessions}`);
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();
@@ -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
- console.log(`[xyOutbound.resolveTarget] No target specified, using default push marker`);
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
- console.log(`[xyOutbound.resolveTarget] Target "${trimmedTo}" missing taskId, looking up session context`);
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
- console.log(`[xyOutbound.resolveTarget] Enhanced target: ${enhancedTarget}`);
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
- console.log(`[xyOutbound.resolveTarget] Could not find matching session context for "${trimmedTo}"`);
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
- console.log(`[xyOutbound.resolveTarget] Using provided target:`, trimmedTo);
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
- console.log(`[xyOutbound.sendText] Using default push delivery (no specific target)`);
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
- console.log(`[xyOutbound.sendText] Saving push data to local storage...`);
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
- console.log(`[xyOutbound.sendText] ✅ Push data saved with ID: ${pushDataId.substring(0, 20)}`);
109
+ logger.log(`[xyOutbound.sendText] ✅ Push data saved with ID: ${pushDataId.substring(0, 20)}`);
109
110
  }
110
111
  catch (error) {
111
- console.error(`[xyOutbound.sendText] ❌ Failed to save push data:`, error);
112
+ logger.error(`[xyOutbound.sendText] ❌ Failed to save push data:`, error);
112
113
  // 如果持久化失败,仍然继续发送(不阻塞主流程)
113
114
  pushDataId = "";
114
115
  }
115
116
  // 2. 读取所有 pushId
116
- console.log(`[xyOutbound.sendText] Loading all pushIds...`);
117
+ logger.log(`[xyOutbound.sendText] Loading all pushIds...`);
117
118
  let pushIdList = [];
118
119
  try {
119
120
  pushIdList = await getAllPushIds();
120
- console.log(`[xyOutbound.sendText] ✅ Loaded ${pushIdList.length} pushIds`);
121
+ logger.log(`[xyOutbound.sendText] ✅ Loaded ${pushIdList.length} pushIds`);
121
122
  }
122
123
  catch (error) {
123
- console.error(`[xyOutbound.sendText] ❌ Failed to load pushIds:`, error);
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
- console.log(`[xyOutbound.sendText] ⚠️ No pushIds found, falling back to config pushId`);
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
- console.log(`[xyOutbound.sendText] 📤 Broadcasting to ${pushIdList.length} pushId(s)...`);
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
- console.log(`[xyOutbound.sendText] ✅ Sent successfully to pushId: ${pushId.substring(0, 20)}...`);
146
+ logger.log(`[xyOutbound.sendText] ✅ Sent successfully to pushId: ${pushId.substring(0, 20)}...`);
146
147
  }
147
148
  catch (error) {
148
149
  failureCount++;
149
- console.error(`[xyOutbound.sendText] ❌ Failed to send to pushId: ${pushId.substring(0, 20)}...`, error);
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
- console.log(`[xyOutbound.sendMedia] File uploaded:`, {
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
- console.log(`[xyOutbound.sendMedia] WebSocket message sent successfully`);
226
+ logger.log(`[xyOutbound.sendMedia] WebSocket message sent successfully`);
226
227
  // Return message info
227
228
  return {
228
229
  channel: "xiaoyi-channel",
@@ -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
- console.log(`[xiaoyiprovider] stream completed (no content), usage: input=${event.message?.usage?.input} output=${event.message?.usage?.output}`);
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
- console.log("[xiaoyiprovider] first content event received, switching to streaming mode");
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
- console.log(`[xiaoyiprovider] stream completed, usage: input=${event.message?.usage?.input} output=${event.message?.usage?.output}`);
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
- console.log(`[xiaoyiprovider] stream error after content: ${event.error?.errorMessage}`);
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
- console.log(`[xiaoyiprovider] retryable error (attempt ${attempt + 1}/${MAX_RETRY_ATTEMPTS}): ` +
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
- console.log(`[xiaoyiprovider] all ${MAX_RETRY_ATTEMPTS} retries exhausted, surfacing last error`);
176
+ logger.log(`[xiaoyiprovider] all ${MAX_RETRY_ATTEMPTS} retries exhausted, surfacing last error`);
177
177
  }
178
178
  else if (errorResult) {
179
- console.log(`[xiaoyiprovider] non-retryable error: ${errorResult.errorMessage}`);
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
- console.log("[xiaoyiprovider] entering final fallback attempt");
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
- console.log(`[xiaoyiprovider] input messages count: ${context.messages?.length ?? 0}`);
488
+ logger.log(`[xiaoyiprovider] input messages count: ${context.messages?.length ?? 0}`);
489
489
  if (context.systemPrompt) {
490
- console.log(`[xiaoyiprovider] system prompt length: ${context.systemPrompt.length}`);
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
- console.log(`[xiaoyiprovider] system prompt optimized: ${beforeLen} -> ${sp.length}`);
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
- console.log("[xiaoyiprovider] detected cron-triggered request, using extended retry delays");
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
- console.log(`[PUSH] 📤 Preparing to send push message`);
37
- console.log(`[PUSH] - Using pushId: ${actualPushId.substring(0, 20)}...`);
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
- console.log(`[PUSH] 📥 Response received`);
84
- console.log(`[PUSH] - HTTP Status: ${response.status} ${response.statusText}`);
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
- console.log(`[PUSH] ❌ Push request failed`);
88
- console.log(`[PUSH] - HTTP Status: ${response.status}`);
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
- console.log(`[PUSH] ⚠️ Received empty response body`);
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
- console.log(`[PUSH] ❌ Failed to parse JSON response`);
105
- console.log(`[PUSH] - Parse error: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
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
- console.log(`[PUSH] ✅ Push message sent successfully`);
109
- console.log(`[PUSH] - Trace ID: ${traceId}`);
109
+ logger.log(`[PUSH] ✅ Push message sent successfully`);
110
+ logger.log(`[PUSH] - Trace ID: ${traceId}`);
110
111
  }
111
112
  catch (error) {
112
- console.log(`[PUSH] ❌ Failed to send push message`);
113
+ logger.log(`[PUSH] ❌ Failed to send push message`);
113
114
  if (error instanceof Error) {
114
- console.log(`[PUSH] - Error name: ${error.name}`);
115
- console.log(`[PUSH] - Error message: ${error.message}`);
115
+ logger.log(`[PUSH] - Error name: ${error.name}`);
116
+ logger.log(`[PUSH] - Error message: ${error.message}`);
116
117
  }
117
118
  else {
118
- console.log(`[PUSH] - Error:`, error);
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
- console.log(`[CLEANUP] 🧹 Cleaned ${cleanedCount} stale files (>${TEMP_FILE_TTL_MS / 1000 / 3600}h) from ${tempDir}`);
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
- console.error(`[CLEANUP] ❌ Failed to cleanup temp dir:`, err);
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
- console.log(`${PLUGIN_LOG_PREFIX} [RESULT] Found ${searchResult.tools.length} skills, building context...`);
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
- console.log(`${PLUGIN_LOG_PREFIX} [ERROR] Failed to format skills context`);
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
- console.error(`${PLUGIN_LOG_PREFIX} [ERROR] ${errorMessage}, original query: "${extractedQuery}"`);
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
- console.warn(`${PLUGIN_LOG_PREFIX} Missing required configuration. serviceUrl: "${serviceUrl}", apiKey: "${apiKey ? '(set)' : '(missing)'} ", uid: "${uid ? '(set)' : '(missing)'}"`);
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
- console.warn(`${PLUGIN_LOG_PREFIX} HTTP error: ${response.status} ${response.statusText}`);
115
+ logger.warn(`${PLUGIN_LOG_PREFIX} HTTP error: ${response.status} ${response.statusText}`);
115
116
  return null;
116
117
  }
117
- console.log(`${PLUGIN_LOG_PREFIX} Received response, status: ${response.status}`);
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
- console.log(`${PLUGIN_LOG_PREFIX} [DEBUG] All top 2 skills are installed, returning null`);
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
- console.log(`${PLUGIN_LOG_PREFIX} [DEBUG] Top 2 has installed skill with rrfScore >= 0.016, returning null`);
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
- console.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
+ 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
- console.log(`${PLUGIN_LOG_PREFIX} [DEBUG] No uninstalled skills with rrfScore >= 0.016, returning null`);
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
- console.warn(`${PLUGIN_LOG_PREFIX} Invalid response format: ${JSON.stringify(responseData).slice(0, 200)}`);
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
- console.warn(`${PLUGIN_LOG_PREFIX} [ERROR] Fetch failed - name: ${errorName}, message: ${errorMessage}, cause: ${errorCause}, stack: ${errorStack}`);
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
- console.log(`[STEER] Injecting steer for sessionId=${sessionId}, taskId=${syntheticMessage.params.id}`);
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
- console.log(`send ${fileName} file to user success`);
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 — find the most recently active one by task-manager activity
146
- // Prefer sessions whose taskId matches the current active task (from task-manager),
147
- // with recency as tiebreaker.
148
- let bestMatch = null;
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 latestTaskId = getCurrentTaskId(entry.sessionId);
160
- const recency = latestTaskId ? 2 : 1; // sessions with active task get higher priority
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
- console.error(`[SELF_EVOLUTION] Failed to read ${SELF_EVOLUTION_ENV_FILE}:`, error);
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
- console.error(`[SELF_EVOLUTION] Failed to read ${SELF_EVOLUTION_ENV_FILE}:`, error);
63
+ logger.error(`[SELF_EVOLUTION] Failed to read ${SELF_EVOLUTION_ENV_FILE}:`, error);
63
64
  }
64
65
  return false;
65
66
  }
@@ -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
- console.log("[WS-SEND] Sending init message frame:", JSON.stringify(initMessage, null, 2));
304
+ this.log("[WS-SEND] Sending init message frame:", JSON.stringify(initMessage, null, 2));
302
305
  this.ws.send(initMessageStr);
303
- console.log(`[WS-SEND] Init message sent successfully, size: ${initMessageStr.length} bytes`);
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
- console.log(`[WS-RECV] Raw message frame, size: ${messageStr.length} characters`);
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
- console.log("[WS-RECV] Text:", maskedText);
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
- console.error("[XY] Message missing sessionId");
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
- console.warn("[XY] dataPart.data.events is not an array, skipping");
413
+ this.log("[XY] dataPart.data.events is not an array, skipping");
411
414
  continue;
412
415
  }
413
- console.log(`[XY] Processing ${events.length} events from data.events`);
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
- console.log(`[XY] Emitting data-event, intentName: ${item.payload.intentName}, size: ${JSON.stringify(dataEvent).length} bytes`);
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
- console.log(`[XY] Emitting gui-agent-response, size: ${JSON.stringify(item).length} bytes`);
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
- console.log("[XY] Trigger event detected, emitting trigger-event with context");
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
- console.log("[XY] ClawSelfEvolutionState event detected, emitting self-evolution-event");
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
- console.log("[XY] ClawSelfEvolutionStateGet event detected, emitting self-evolution-state-get-event");
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
- console.log("[XY] LoginTokenEvent.ClawAutoLogin detected, emitting login-token-event");
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
- console.log(`[XY] Message type: Wrapped, msgType: ${inboundMsg.msgType}`);
471
+ this.log(`[XY] Message type: Wrapped, msgType: ${inboundMsg.msgType}`);
469
472
  // Handle heartbeat responses
470
473
  if (inboundMsg.msgType === "heartbeat") {
471
- console.log("[XY] Received heartbeat response");
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
- console.log("[XY] Processing data message");
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
- console.warn("[XY] dataPart.data.events is not an array, skipping");
488
+ this.log("[XY] dataPart.data.events is not an array, skipping");
486
489
  continue;
487
490
  }
488
- console.log(`[XY] Processing ${events.length} events from data.events`);
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
- console.log(`[XY] Emitting data-event, intentName: ${item.payload.intentName}, size: ${JSON.stringify(dataEvent).length} bytes`);
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
- console.log(`[XY] Emitting gui-agent-response, size: ${JSON.stringify(item).length} bytes`);
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
- console.log("[XY] Trigger event detected (wrapped format), emitting trigger-event with context");
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
- console.log("[XY] LoginTokenEvent.ClawAutoLogin detected (wrapped format), emitting login-token-event");
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
- console.error("[XY] Failed to process data message:", error);
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
- console.log(`[XY] Parsed A2A request, method: ${a2aRequest.method}`);
532
+ this.log(`[XY] Parsed A2A request, method: ${a2aRequest.method}`);
530
533
  const sessionId = inboundMsg.sessionId;
531
- console.log(`[XY] Session ID: ${sessionId}`);
534
+ this.log(`[XY] Session ID: ${sessionId}`);
532
535
  // Emit message event
533
- console.log("[XY] *** EMITTING message event (Wrapped path) ***");
536
+ this.log("[XY] *** EMITTING message event (Wrapped path) ***");
534
537
  this.emit("message", a2aRequest, sessionId);
535
538
  }
536
539
  catch (error) {
537
- console.error("[XY] Failed to parse message:", error);
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
- console.warn(`WebSocket disconnected: code=${code}, reason=${reason}`);
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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynhcj/xiaoyi-channel",
3
- "version": "0.0.124-beta",
3
+ "version": "0.0.126-beta",
4
4
  "description": "OpenClaw Xiaoyi Channel plugin - Xiaoyi A2A protocol integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",