@ynhcj/xiaoyi-channel 0.0.132-beta → 0.0.133-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/index.js CHANGED
@@ -5,6 +5,7 @@ import { callCsplApi } from "./src/cspl/call-api.js";
5
5
  import { ALLOWED_TOOLS, MAX_TEXT_LENGTH, MAX_TOTAL_LENGTH, MIN_TEXT_LENGTH, STEER_ABORT_MESSAGE, } from "./src/cspl/constants.js";
6
6
  import { extractResultText, parseSecurityResult, processText, validateAndTruncateText, } from "./src/cspl/utils.js";
7
7
  import { setXYRuntime } from "./src/runtime.js";
8
+ import { logger } from "./src/utils/logger.js";
8
9
  import { tryInjectSteer } from "./src/steer-injector.js";
9
10
  import { registerSelfEvolutionToolResultNudge } from "./src/self-evolution-tool-result-nudge.js";
10
11
  import { createBeforePromptBuildHandler } from "./src/skill-retriever/hooks.js";
@@ -26,7 +27,7 @@ function registerFullHooks(api) {
26
27
  if (!ALLOWED_TOOLS.includes(event.toolName)) {
27
28
  return;
28
29
  }
29
- console.log(`[SENTINEL HOOK] after_tool_call triggered: toolName=${event.toolName}, sessionKey=${ctx.sessionKey ?? "none"}`);
30
+ logger.log(`[SENTINEL HOOK] after_tool_call triggered: toolName=${event.toolName}, sessionKey=${ctx.sessionKey ?? "none"}`);
30
31
  try {
31
32
  const resultText = extractResultText(event, event.toolName);
32
33
  const resultLength = resultText.length;
@@ -49,13 +50,13 @@ function registerFullHooks(api) {
49
50
  }
50
51
  const response = await callCsplApi(finalJson, api.config);
51
52
  const result = parseSecurityResult(response);
52
- console.log(`[SENTINEL HOOK] Security result: status=${result.status}`);
53
+ logger.log(`[SENTINEL HOOK] Security result: status=${result.status}`);
53
54
  if (result.status === "REJECT") {
54
55
  await tryInjectSteer(ctx.sessionKey, STEER_ABORT_MESSAGE);
55
56
  }
56
57
  }
57
58
  catch (err) {
58
- api.logger.error(`[SENTINEL HOOK] after_tool_call error: ${err}`);
59
+ logger.error(`[SENTINEL HOOK] after_tool_call error: ${err}`);
59
60
  }
60
61
  });
61
62
  }
package/dist/src/bot.js CHANGED
@@ -14,6 +14,7 @@ import { selfEvolutionManager } from "./utils/self-evolution-manager.js";
14
14
  import { saveRuntimeInfo } from "./utils/runtime-manager.js";
15
15
  import { toolCallNudgeManager } from "./utils/tool-call-nudge-manager.js";
16
16
  import { registerTaskId, decrementTaskIdRef, lockTaskId, unlockTaskId, hasActiveTask, } from "./task-manager.js";
17
+ import { logger } from "./utils/logger.js";
17
18
  /**
18
19
  * Handle an incoming A2A message.
19
20
  * This is the main entry point for message processing.
@@ -21,8 +22,6 @@ import { registerTaskId, decrementTaskIdRef, lockTaskId, unlockTaskId, hasActive
21
22
  */
22
23
  export async function handleXYMessage(params) {
23
24
  const { cfg, runtime, message, accountId, webSocketSessionId } = params;
24
- const log = runtime?.log ?? console.log;
25
- const error = runtime?.error ?? console.error;
26
25
  // 每次收到消息时更新缓存,供 steer 注入使用
27
26
  setCachedContext(cfg, runtime, accountId);
28
27
  // Get runtime (already validated in monitor.ts, but get reference for use)
@@ -36,7 +35,7 @@ export async function handleXYMessage(params) {
36
35
  if (!sessionId) {
37
36
  throw new Error("clearContext request missing sessionId in params");
38
37
  }
39
- log(`Clear context request for session ${sessionId}`);
38
+ logger.log(`Clear context request for session ${sessionId}`);
40
39
  const config = resolveXYConfig(cfg);
41
40
  await sendClearContextResponse({
42
41
  config,
@@ -52,7 +51,7 @@ export async function handleXYMessage(params) {
52
51
  if (!sessionId) {
53
52
  throw new Error("tasks/cancel request missing sessionId in params");
54
53
  }
55
- log(`Tasks cancel request for session ${sessionId}, task ${taskId}`);
54
+ logger.log(`Tasks cancel request for session ${sessionId}, task ${taskId}`);
56
55
  const config = resolveXYConfig(cfg);
57
56
  await sendTasksCancelResponse({
58
57
  config,
@@ -68,18 +67,18 @@ export async function handleXYMessage(params) {
68
67
  // 如果消息中包含 Trigger 事件数据,直接返回 pushData 内容,不走正常流程
69
68
  const triggerData = extractTriggerData(parsed.parts);
70
69
  if (triggerData) {
71
- log(`[BOT] 📌 Detected Trigger message with pushDataId: ${triggerData.pushDataId}`);
72
- log(`[BOT] - Session ID: ${parsed.sessionId}`);
73
- log(`[BOT] - Task ID: ${parsed.taskId}`);
70
+ logger.log(`[BOT] 📌 Detected Trigger message with pushDataId: ${triggerData.pushDataId}`);
71
+ logger.log(`[BOT] - Session ID: ${parsed.sessionId}`);
72
+ logger.log(`[BOT] - Task ID: ${parsed.taskId}`);
74
73
  try {
75
74
  // 读取 pushData
76
75
  const pushDataItem = await getPushDataById(triggerData.pushDataId);
77
76
  if (!pushDataItem) {
78
- error(`[BOT] ❌ pushData not found for ID: ${triggerData.pushDataId}`);
77
+ logger.error(`[BOT] ❌ pushData not found for ID: ${triggerData.pushDataId}`);
79
78
  return;
80
79
  }
81
- log(`[BOT] ✅ Found pushData, sending direct response`);
82
- log(`[BOT] - pushDataId: ${pushDataItem.pushDataId}`);
80
+ logger.log(`[BOT] ✅ Found pushData, sending direct response`);
81
+ logger.log(`[BOT] - pushDataId: ${pushDataItem.pushDataId}`);
83
82
  const config = resolveXYConfig(cfg);
84
83
  // 直接发送响应(final=true,不走 openclaw 流程)
85
84
  await sendA2AResponse({
@@ -90,13 +89,12 @@ export async function handleXYMessage(params) {
90
89
  text: pushDataItem.dataDetail,
91
90
  append: false,
92
91
  final: true,
93
- runtime,
94
92
  });
95
- log(`[BOT] ✅ Trigger response sent successfully, exiting early`);
93
+ logger.log(`[BOT] ✅ Trigger response sent successfully, exiting early`);
96
94
  return; // 提前返回,不继续处理
97
95
  }
98
96
  catch (err) {
99
- error(`[BOT] ❌ Failed to handle Trigger message:`, err);
97
+ logger.error(`[BOT] ❌ Failed to handle Trigger message:`, err);
100
98
  return;
101
99
  }
102
100
  }
@@ -105,9 +103,9 @@ export async function handleXYMessage(params) {
105
103
  const isSteerMode = cfg.messages?.queue?.mode === "steer";
106
104
  const isSecondMessage = isSteerMode && hasActiveTask(parsed.sessionId);
107
105
  if (isSecondMessage) {
108
- log(`[BOT] 🔄 STEER MODE - Second message detected (will be follower)`);
109
- log(`[BOT] - Session: ${parsed.sessionId}`);
110
- log(`[BOT] - New taskId: ${parsed.taskId} (will replace current)`);
106
+ logger.log(`[BOT] 🔄 STEER MODE - Second message detected (will be follower)`);
107
+ logger.log(`[BOT] - Session: ${parsed.sessionId}`);
108
+ logger.log(`[BOT] - New taskId: ${parsed.taskId} (will replace current)`);
111
109
  }
112
110
  // 🔑 注册taskId(第二条消息会覆盖第一条的taskId)
113
111
  const { isUpdate, refCount } = registerTaskId(parsed.sessionId, parsed.taskId, parsed.messageId, { incrementRef: true } // 增加引用计数
@@ -115,32 +113,32 @@ export async function handleXYMessage(params) {
115
113
  // 🔑 如果是第一条消息,锁定taskId防止被过早清理
116
114
  if (!isUpdate) {
117
115
  lockTaskId(parsed.sessionId);
118
- log(`[BOT] 🔒 Locked taskId for first message`);
116
+ logger.log(`[BOT] 🔒 Locked taskId for first message`);
119
117
  }
120
118
  // Extract and update push_id if present
121
119
  const pushId = extractPushId(parsed.parts);
122
120
  if (pushId) {
123
- log(`[BOT] 📌 Extracted push_id from user message`);
121
+ logger.log(`[BOT] 📌 Extracted push_id from user message`);
124
122
  configManager.updatePushId(parsed.sessionId, pushId);
125
123
  // 持久化 pushId 到本地文件(异步,不阻塞主流程)
126
124
  addPushId(pushId).catch((err) => {
127
- error(`[BOT] Failed to persist pushId:`, err);
125
+ logger.error(`[BOT] Failed to persist pushId:`, err);
128
126
  });
129
127
  }
130
128
  else {
131
- log(`[BOT] ℹ️ No push_id found in message, will use config default`);
129
+ logger.log(`[BOT] ℹ️ No push_id found in message, will use config default`);
132
130
  }
133
131
  // Extract deviceType if present (same level as push_id in systemVariables)
134
132
  const deviceType = extractDeviceType(parsed.parts);
135
133
  if (deviceType) {
136
- log(`[BOT] 📱 Extracted deviceType from user message: ${deviceType}`);
134
+ logger.log(`[BOT] 📱 Extracted deviceType from user message: ${deviceType}`);
137
135
  }
138
136
  // 保存 runtime 信息到 .xiaoyiruntime 文件(异步,不阻塞主流程)
139
137
  saveRuntimeInfo(webSocketSessionId || parsed.sessionId, // SESSION_ID (WebSocket 层级,如果没有则 fallback)
140
138
  parsed.sessionId, // CONVERSATION_ID (param 里的 sessionId)
141
139
  parsed.taskId // TASK_ID (param.id)
142
140
  ).catch((err) => {
143
- error(`[BOT] Failed to save runtime info:`, err);
141
+ logger.error(`[BOT] Failed to save runtime info:`, err);
144
142
  });
145
143
  // Resolve configuration (needed for status updates)
146
144
  const config = resolveXYConfig(cfg);
@@ -156,7 +154,7 @@ export async function handleXYMessage(params) {
156
154
  id: parsed.sessionId, // ✅ Use sessionId to share context within the same conversation session
157
155
  },
158
156
  });
159
- log(`xy: resolved route accountId=${route.accountId}, sessionKey=${route.sessionKey}`);
157
+ logger.log(`xy: resolved route accountId=${route.accountId}, sessionKey=${route.sessionKey}`);
160
158
  registerSession(route.sessionKey, {
161
159
  config,
162
160
  sessionId: parsed.sessionId,
@@ -166,7 +164,7 @@ export async function handleXYMessage(params) {
166
164
  deviceType,
167
165
  });
168
166
  // 🔑 发送初始状态更新(第二条消息也要发,用新taskId)
169
- log(`[STATUS] Sending initial status update for session ${parsed.sessionId}`);
167
+ logger.log(`[STATUS] Sending initial status update for session ${parsed.sessionId}`);
170
168
  void sendStatusUpdate({
171
169
  config,
172
170
  sessionId: parsed.sessionId,
@@ -174,9 +172,8 @@ export async function handleXYMessage(params) {
174
172
  messageId: parsed.messageId,
175
173
  text: isSecondMessage ? "新消息已接收,正在处理..." : "任务正在处理中,请稍候~",
176
174
  state: "working",
177
- runtime,
178
175
  }).catch((err) => {
179
- error(`Failed to send initial status update:`, err);
176
+ logger.error(`Failed to send initial status update:`, err);
180
177
  });
181
178
  // Extract text and files from parts
182
179
  const text = extractTextFromParts(parsed.parts);
@@ -186,24 +183,24 @@ export async function handleXYMessage(params) {
186
183
  const selfEvolutionEnabled = await selfEvolutionManager.isEnabled();
187
184
  if (selfEvolutionEnabled && shouldNudgeForSelfEvolutionKeyword(textForAgent)) {
188
185
  const shouldNudge = toolCallNudgeManager.tryMarkKeywordNudge(route.sessionKey);
189
- log(`[SELF_EVOLUTION] Keyword check hit during inbound build: sessionKey=${route.sessionKey}, shouldNudge=${shouldNudge}`);
186
+ logger.log(`[SELF_EVOLUTION] Keyword check hit during inbound build: sessionKey=${route.sessionKey}, shouldNudge=${shouldNudge}`);
190
187
  if (shouldNudge) {
191
188
  const augmented = appendSelfEvolutionKeywordNudge(textForAgent);
192
189
  textForAgent = augmented.text;
193
190
  if (augmented.appended) {
194
- log(`[SELF_EVOLUTION] Keyword-triggered inline nudge appended: sessionKey=${route.sessionKey}`);
191
+ logger.log(`[SELF_EVOLUTION] Keyword-triggered inline nudge appended: sessionKey=${route.sessionKey}`);
195
192
  }
196
193
  }
197
194
  }
198
195
  }
199
196
  catch (selfEvolutionError) {
200
- error(`[SELF_EVOLUTION] Failed to append inline keyword nudge: ${String(selfEvolutionError)}`);
197
+ logger.error(`[SELF_EVOLUTION] Failed to append inline keyword nudge: ${String(selfEvolutionError)}`);
201
198
  }
202
199
  }
203
200
  const fileParts = extractFileParts(parsed.parts);
204
201
  // Download files to local disk
205
202
  const downloadedFiles = await downloadFilesFromParts(fileParts);
206
- log("Downloaded files:", JSON.stringify(downloadedFiles, null, 2));
203
+ logger.log("Downloaded files:", JSON.stringify(downloadedFiles, null, 2));
207
204
  const mediaPayload = buildXYMediaPayload(downloadedFiles);
208
205
  // Resolve envelope format options (following feishu pattern)
209
206
  const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
@@ -246,8 +243,8 @@ export async function handleXYMessage(params) {
246
243
  ...mediaPayload,
247
244
  });
248
245
  // 🔑 创建dispatcher(dispatcher会自动使用动态taskId)
249
- log(`[BOT-DISPATCHER] 🎯 Creating reply dispatcher`);
250
- log(`[BOT-DISPATCHER] - taskId: ${parsed.taskId}`);
246
+ logger.log(`[BOT-DISPATCHER] 🎯 Creating reply dispatcher`);
247
+ logger.log(`[BOT-DISPATCHER] - taskId: ${parsed.taskId}`);
251
248
  const { dispatcher, replyOptions, markDispatchIdle, startStatusInterval } = createXYReplyDispatcher({
252
249
  cfg,
253
250
  runtime,
@@ -261,7 +258,7 @@ export async function handleXYMessage(params) {
261
258
  // 第二条消息会很快返回,不需要定时器
262
259
  if (!isSecondMessage) {
263
260
  startStatusInterval();
264
- log(`[BOT-DISPATCHER] ✅ Status interval started for first message`);
261
+ logger.log(`[BOT-DISPATCHER] ✅ Status interval started for first message`);
265
262
  }
266
263
  // Build session context for AsyncLocalStorage
267
264
  const sessionContext = {
@@ -272,22 +269,22 @@ export async function handleXYMessage(params) {
272
269
  agentId: route.accountId,
273
270
  deviceType,
274
271
  };
275
- log(`[BOT-DISPATCH] ⏳ withReplyDispatcher starting, sessionKey=${route.sessionKey}`);
272
+ logger.log(`[BOT-DISPATCH] ⏳ withReplyDispatcher starting, sessionKey=${route.sessionKey}`);
276
273
  await core.channel.reply.withReplyDispatcher({
277
274
  dispatcher,
278
275
  onSettled: () => {
279
- log(`[BOT] 🏁 onSettled called for session: ${route.sessionKey}`);
280
- log(`[BOT] - isSecondMessage: ${isSecondMessage}`);
276
+ logger.log(`[BOT] 🏁 onSettled called for session: ${route.sessionKey}`);
277
+ logger.log(`[BOT] - isSecondMessage: ${isSecondMessage}`);
281
278
  // 🔑 减少引用计数
282
279
  decrementTaskIdRef(parsed.sessionId);
283
280
  // 🔑 如果是第一条消息完成,解锁
284
281
  if (!isSecondMessage) {
285
282
  unlockTaskId(parsed.sessionId);
286
- log(`[BOT] 🔓 Unlocked taskId (first message completed)`);
283
+ logger.log(`[BOT] 🔓 Unlocked taskId (first message completed)`);
287
284
  }
288
285
  // 减少session引用计数
289
286
  unregisterSession(route.sessionKey);
290
- log(`[BOT] ✅ Cleanup completed`);
287
+ logger.log(`[BOT] ✅ Cleanup completed`);
291
288
  },
292
289
  run: () => {
293
290
  // 🔐 Use AsyncLocalStorage to provide session context to tools.
@@ -296,12 +293,12 @@ export async function handleXYMessage(params) {
296
293
  // signal init complete to release the global dispatch gate
297
294
  // for the next session.
298
295
  const dispatchPromise = runWithSessionContext(sessionContext, async () => {
299
- log(`[BOT-DISPATCH] ⏳ dispatchReplyFromConfig starting...`);
300
- log(`[BOT-DISPATCH] - sessionKey: ${ctxPayload.SessionKey}`);
301
- log(`[BOT-DISPATCH] - provider: ${ctxPayload.Provider}`);
302
- log(`[BOT-DISPATCH] - surface: ${ctxPayload.Surface}`);
303
- log(`[BOT-DISPATCH] - from: ${ctxPayload.From}`);
304
- log(`[BOT-DISPATCH] - body length: ${ctxPayload.Body?.length ?? 0}`);
296
+ logger.log(`[BOT-DISPATCH] ⏳ dispatchReplyFromConfig starting...`);
297
+ logger.log(`[BOT-DISPATCH] - sessionKey: ${ctxPayload.SessionKey}`);
298
+ logger.log(`[BOT-DISPATCH] - provider: ${ctxPayload.Provider}`);
299
+ logger.log(`[BOT-DISPATCH] - surface: ${ctxPayload.Surface}`);
300
+ logger.log(`[BOT-DISPATCH] - from: ${ctxPayload.From}`);
301
+ logger.log(`[BOT-DISPATCH] - body length: ${ctxPayload.Body?.length ?? 0}`);
305
302
  try {
306
303
  const result = await core.channel.reply.dispatchReplyFromConfig({
307
304
  ctx: ctxPayload,
@@ -309,15 +306,15 @@ export async function handleXYMessage(params) {
309
306
  dispatcher,
310
307
  replyOptions,
311
308
  });
312
- log(`[BOT-DISPATCH] ✅ dispatchReplyFromConfig returned`);
313
- log(`[BOT-DISPATCH] - result: ${JSON.stringify(result)}`);
309
+ logger.log(`[BOT-DISPATCH] ✅ dispatchReplyFromConfig returned`);
310
+ logger.log(`[BOT-DISPATCH] - result: ${JSON.stringify(result)}`);
314
311
  return result;
315
312
  }
316
313
  catch (dispatchErr) {
317
- error(`[BOT-DISPATCH] ❌ dispatchReplyFromConfig threw`);
318
- error(`[BOT-DISPATCH] - error name: ${dispatchErr instanceof Error ? dispatchErr.name : "unknown"}`);
319
- error(`[BOT-DISPATCH] - error message: ${String(dispatchErr)}`);
320
- error(`[BOT-DISPATCH] - error stack: ${dispatchErr instanceof Error ? dispatchErr.stack?.slice(0, 500) : "N/A"}`);
314
+ logger.error(`[BOT-DISPATCH] ❌ dispatchReplyFromConfig threw`);
315
+ logger.error(`[BOT-DISPATCH] - error name: ${dispatchErr instanceof Error ? dispatchErr.name : "unknown"}`);
316
+ logger.error(`[BOT-DISPATCH] - error message: ${String(dispatchErr)}`);
317
+ logger.error(`[BOT-DISPATCH] - error stack: ${dispatchErr instanceof Error ? dispatchErr.stack?.slice(0, 500) : "N/A"}`);
321
318
  throw dispatchErr;
322
319
  }
323
320
  });
@@ -326,20 +323,20 @@ export async function handleXYMessage(params) {
326
323
  return dispatchPromise;
327
324
  },
328
325
  });
329
- log(`[BOT] ✅ Dispatcher completed for session: ${parsed.sessionId}`);
330
- log(`xy: dispatch complete (session=${parsed.sessionId})`);
326
+ logger.log(`[BOT] ✅ Dispatcher completed for session: ${parsed.sessionId}`);
327
+ logger.log(`xy: dispatch complete (session=${parsed.sessionId})`);
331
328
  }
332
329
  catch (err) {
333
330
  // ✅ Only log error, don't re-throw to prevent gateway restart
334
- error("Failed to handle XY message:", err);
331
+ logger.error("Failed to handle XY message:", err);
335
332
  runtime.error?.(`xy: Failed to handle message: ${String(err)}`);
336
- log(`[BOT] ❌ Error occurred, attempting cleanup...`);
333
+ logger.log(`[BOT] ❌ Error occurred, attempting cleanup...`);
337
334
  // 🔑 错误时也要清理taskId和session
338
335
  try {
339
336
  const params = message.params;
340
337
  const sessionId = params?.sessionId;
341
338
  if (sessionId) {
342
- log(`[BOT] 🧹 Cleaning up after error: ${sessionId}`);
339
+ logger.log(`[BOT] 🧹 Cleaning up after error: ${sessionId}`);
343
340
  // 清理 taskId
344
341
  decrementTaskIdRef(sessionId);
345
342
  unlockTaskId(sessionId);
@@ -355,11 +352,11 @@ export async function handleXYMessage(params) {
355
352
  },
356
353
  });
357
354
  unregisterSession(route.sessionKey);
358
- log(`[BOT] ✅ Cleanup completed after error`);
355
+ logger.log(`[BOT] ✅ Cleanup completed after error`);
359
356
  }
360
357
  }
361
358
  catch (cleanupErr) {
362
- log(`[BOT] ⚠️ Cleanup failed:`, cleanupErr);
359
+ logger.log(`[BOT] ⚠️ Cleanup failed:`, cleanupErr);
363
360
  // Ignore cleanup errors
364
361
  }
365
362
  // ❌ Don't re-throw: message processing error should not affect gateway stability
@@ -1,15 +1,11 @@
1
1
  import { XYWebSocketManager } from "./websocket.js";
2
2
  import type { XYChannelConfig } from "./types.js";
3
3
  import type { RuntimeEnv } from "openclaw/plugin-sdk";
4
- /**
5
- * Set the runtime for logging in client module.
6
- */
7
- export declare function setClientRuntime(rt: RuntimeEnv | undefined): void;
8
4
  /**
9
5
  * Get or create a WebSocket manager for the given configuration.
10
6
  * Reuses existing managers if config matches.
11
7
  */
12
- export declare function getXYWebSocketManager(config: XYChannelConfig): XYWebSocketManager;
8
+ export declare function getXYWebSocketManager(config: XYChannelConfig, runtime?: RuntimeEnv): XYWebSocketManager;
13
9
  /**
14
10
  * Remove a specific WebSocket manager from cache.
15
11
  * Disconnects the manager and removes it from the cache.
@@ -1,14 +1,7 @@
1
1
  // WebSocket client cache management
2
2
  // Follows feishu/client.ts pattern for caching client instances
3
3
  import { XYWebSocketManager } from "./websocket.js";
4
- // Runtime reference for logging
5
- let runtime;
6
- /**
7
- * Set the runtime for logging in client module.
8
- */
9
- export function setClientRuntime(rt) {
10
- runtime = rt;
11
- }
4
+ import { logger } from "./utils/logger.js";
12
5
  /**
13
6
  * Global cache for WebSocket managers.
14
7
  * Key format: `${apiKey}-${agentId}`
@@ -24,19 +17,17 @@ const wsManagerCache = _g.__xyWsManagerCache;
24
17
  * Get or create a WebSocket manager for the given configuration.
25
18
  * Reuses existing managers if config matches.
26
19
  */
27
- export function getXYWebSocketManager(config) {
20
+ export function getXYWebSocketManager(config, runtime) {
28
21
  const cacheKey = `${config.apiKey}-${config.agentId}`;
29
22
  let cached = wsManagerCache.get(cacheKey);
30
23
  if (cached && cached.isConfigMatch(config)) {
31
- const log = runtime?.log ?? console.log;
32
24
  return cached;
33
25
  }
34
26
  // Create new manager
35
- const log = runtime?.log ?? console.log;
36
- log(`[WS-MANAGER-CACHE] 🆕 Creating new WebSocket manager: ${cacheKey}, total managers before: ${wsManagerCache.size}`);
27
+ logger.log(`[WS-MANAGER-CACHE] 🆕 Creating new WebSocket manager: ${cacheKey}, total managers before: ${wsManagerCache.size}`);
37
28
  cached = new XYWebSocketManager(config, runtime);
38
29
  wsManagerCache.set(cacheKey, cached);
39
- log(`[WS-MANAGER-CACHE] 📊 Total managers after creation: ${wsManagerCache.size}`);
30
+ logger.log(`[WS-MANAGER-CACHE] 📊 Total managers after creation: ${wsManagerCache.size}`);
40
31
  return cached;
41
32
  }
42
33
  /**
@@ -44,25 +35,23 @@ export function getXYWebSocketManager(config) {
44
35
  * Disconnects the manager and removes it from the cache.
45
36
  */
46
37
  export function removeXYWebSocketManager(config) {
47
- const log = runtime?.log ?? console.log;
48
38
  const cacheKey = `${config.apiKey}-${config.agentId}`;
49
39
  const manager = wsManagerCache.get(cacheKey);
50
40
  if (manager) {
51
- log(`🗑️ [WS-MANAGER-CACHE] Removing manager from cache: ${cacheKey}`);
41
+ logger.log(`🗑️ [WS-MANAGER-CACHE] Removing manager from cache: ${cacheKey}`);
52
42
  manager.disconnect();
53
43
  wsManagerCache.delete(cacheKey);
54
- log(`🗑️ [WS-MANAGER-CACHE] Manager removed, remaining managers: ${wsManagerCache.size}`);
44
+ logger.log(`🗑️ [WS-MANAGER-CACHE] Manager removed, remaining managers: ${wsManagerCache.size}`);
55
45
  }
56
46
  else {
57
- log(`⚠️ [WS-MANAGER-CACHE] Manager not found in cache: ${cacheKey}`);
47
+ logger.log(`⚠️ [WS-MANAGER-CACHE] Manager not found in cache: ${cacheKey}`);
58
48
  }
59
49
  }
60
50
  /**
61
51
  * Clear all cached WebSocket managers.
62
52
  */
63
53
  export function clearXYWebSocketManagers() {
64
- const log = runtime?.log ?? console.log;
65
- log("Clearing all WebSocket manager caches");
54
+ logger.log("Clearing all WebSocket manager caches");
66
55
  for (const manager of wsManagerCache.values()) {
67
56
  manager.disconnect();
68
57
  }
@@ -79,37 +68,35 @@ export function getCachedManagerCount() {
79
68
  * Helps identify connection issues and orphan connections.
80
69
  */
81
70
  export function diagnoseAllManagers() {
82
- const log = runtime?.log ?? console.log;
83
- log(`Total cached managers: ${wsManagerCache.size}`);
71
+ logger.log(`Total cached managers: ${wsManagerCache.size}`);
84
72
  if (wsManagerCache.size === 0) {
85
- log("ℹ️ No managers in cache");
73
+ logger.log("ℹ️ No managers in cache");
86
74
  return;
87
75
  }
88
76
  let orphanCount = 0;
89
77
  wsManagerCache.forEach((manager, key) => {
90
78
  const diag = manager.getConnectionDiagnostics();
91
- log(` Total event listeners on manager: ${diag.totalEventListeners}`);
79
+ logger.log(` Total event listeners on manager: ${diag.totalEventListeners}`);
92
80
  // Connection
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}`);
81
+ logger.log(` 🔌 Connection:`);
82
+ logger.log(` - Exists: ${diag.connection.exists}`);
83
+ logger.log(` - ReadyState: ${diag.connection.readyState}`);
84
+ logger.log(` - State connected/ready: ${diag.connection.stateConnected}/${diag.connection.stateReady}`);
85
+ logger.log(` - Reconnect attempts: ${diag.connection.reconnectAttempts}`);
86
+ logger.log(` - Listeners on WebSocket: ${diag.connection.listenerCount}`);
87
+ logger.log(` - Heartbeat active: ${diag.connection.heartbeatActive}`);
88
+ logger.log(` - Has reconnect timer: ${diag.connection.hasReconnectTimer}`);
101
89
  if (diag.connection.isOrphan) {
102
- log(` ⚠️ ORPHAN CONNECTION DETECTED!`);
90
+ logger.log(` ⚠️ ORPHAN CONNECTION DETECTED!`);
103
91
  orphanCount++;
104
92
  }
105
- log("");
106
93
  });
107
94
  if (orphanCount > 0) {
108
- log(`⚠️ Total orphan connections found: ${orphanCount}`);
109
- log(`💡 Suggestion: These connections should be cleaned up`);
95
+ logger.log(`⚠️ Total orphan connections found: ${orphanCount}`);
96
+ logger.log(`💡 Suggestion: These connections should be cleaned up`);
110
97
  }
111
98
  else {
112
- log(`✅ No orphan connections found`);
99
+ logger.log(`✅ No orphan connections found`);
113
100
  }
114
101
  }
115
102
  /**
@@ -117,18 +104,17 @@ export function diagnoseAllManagers() {
117
104
  * Returns the number of managers that had orphan connections.
118
105
  */
119
106
  export function cleanupOrphanConnections() {
120
- const log = runtime?.log ?? console.log;
121
107
  let cleanedCount = 0;
122
108
  wsManagerCache.forEach((manager, key) => {
123
109
  const diag = manager.getConnectionDiagnostics();
124
110
  if (diag.connection.isOrphan) {
125
- log(`🧹 Cleaning up orphan connections in manager: ${key}`);
111
+ logger.log(`🧹 Cleaning up orphan connections in manager: ${key}`);
126
112
  manager.disconnect();
127
113
  cleanedCount++;
128
114
  }
129
115
  });
130
116
  if (cleanedCount > 0) {
131
- log(`🧹 Cleaned up ${cleanedCount} manager(s) with orphan connections`);
117
+ logger.log(`🧹 Cleaned up ${cleanedCount} manager(s) with orphan connections`);
132
118
  }
133
119
  return cleanedCount;
134
120
  }
@@ -20,10 +20,10 @@ export async function downloadFile(url, destPath) {
20
20
  }
21
21
  catch (error) {
22
22
  if (error.name === 'AbortError') {
23
- logger.log(`Download timeout (30s) for ${url}`);
23
+ logger.error(`Download timeout (30s) for ${url}`);
24
24
  throw new Error(`Download timeout after 30 seconds`);
25
25
  }
26
- logger.log(`Failed to download file from ${url}:`);
26
+ logger.error(`Failed to download file from ${url}:`);
27
27
  throw error;
28
28
  }
29
29
  finally {
@@ -52,7 +52,7 @@ export async function downloadFilesFromParts(fileParts, tempDir = "/tmp/xy_chann
52
52
  });
53
53
  }
54
54
  catch (error) {
55
- logger.log(`Failed to download file ${name}:`);
55
+ logger.error(`Failed to download file ${name}:`);
56
56
  // Continue with other files
57
57
  }
58
58
  }
@@ -17,7 +17,6 @@ export interface SendA2AResponseParams {
17
17
  }>;
18
18
  errorCode?: number | string;
19
19
  errorMessage?: string;
20
- runtime?: any;
21
20
  }
22
21
  /**
23
22
  * Send an A2A artifact update response.
@@ -50,7 +49,6 @@ export interface SendStatusUpdateParams {
50
49
  messageId: string;
51
50
  text: string;
52
51
  state: "submitted" | "working" | "input-required" | "completed" | "canceled" | "failed" | "unknown";
53
- runtime?: any;
54
52
  }
55
53
  /**
56
54
  * Send an A2A task status update.
@@ -7,8 +7,7 @@ import { getCurrentTaskId, getCurrentMessageId } from "./task-manager.js";
7
7
  * Send an A2A artifact update response.
8
8
  */
9
9
  export async function sendA2AResponse(params) {
10
- const { config, sessionId, taskId, messageId, text, append, final, files, errorCode, errorMessage, runtime } = params;
11
- const log = runtime?.log ?? console.log;
10
+ const { config, sessionId, taskId, messageId, text, append, final, files, errorCode, errorMessage } = params;
12
11
  // Build artifact update event
13
12
  const artifact = {
14
13
  taskId,
@@ -47,7 +46,7 @@ export async function sendA2AResponse(params) {
47
46
  code: errorCode,
48
47
  message: errorMessage ?? "任务执行异常,请重试",
49
48
  };
50
- log(`[A2A_RESPONSE] ⚠️ Including error code: ${errorCode}`);
49
+ logger.log(`[A2A_RESPONSE] ⚠️ Including error code: ${errorCode}`);
51
50
  }
52
51
  // Send via WebSocket
53
52
  const wsManager = getXYWebSocketManager(config);
@@ -59,13 +58,13 @@ export async function sendA2AResponse(params) {
59
58
  msgDetail: JSON.stringify(jsonRpcResponse),
60
59
  };
61
60
  // 📋 Log complete response body
62
- log(`[A2A_RESPONSE] 📤 Sending A2A artifact-update response: taskId: ${taskId}`);
63
- log(`[A2A_RESPONSE] - append: ${append}`);
64
- log(`[A2A_RESPONSE] - final: ${final}`);
65
- log(`[A2A_RESPONSE] - text: ${text.length <= 10 ? text : text.slice(0, 5) + '***' + text.slice(-5)}`);
66
- log(`[A2A_RESPONSE] - files count: ${files?.length ?? 0}`);
61
+ logger.log(`[A2A_RESPONSE] 📤 Sending A2A artifact-update response: taskId: ${taskId}`);
62
+ logger.log(`[A2A_RESPONSE] - append: ${append}`);
63
+ logger.log(`[A2A_RESPONSE] - final: ${final}`);
64
+ logger.log(`[A2A_RESPONSE] - text: ${text.length <= 10 ? text : text.slice(0, 5) + '***' + text.slice(-5)}`);
65
+ logger.log(`[A2A_RESPONSE] - files count: ${files?.length ?? 0}`);
67
66
  await wsManager.sendMessage(sessionId, outboundMessage);
68
- log(`[A2A_RESPONSE] ✅ Message sent successfully`);
67
+ logger.log(`[A2A_RESPONSE] ✅ Message sent successfully`);
69
68
  }
70
69
  /**
71
70
  * Send an A2A artifact-update with reasoningText part.
@@ -110,8 +109,7 @@ export async function sendReasoningTextUpdate(params) {
110
109
  * Follows A2A protocol standard format with nested status object.
111
110
  */
112
111
  export async function sendStatusUpdate(params) {
113
- const { config, sessionId, taskId, messageId, text, state, runtime } = params;
114
- const log = runtime?.log ?? console.log;
112
+ const { config, sessionId, taskId, messageId, text, state } = params;
115
113
  // Dynamic lookup: use latest taskId/messageId from task-manager (handles steer/interrupt),
116
114
  // fall back to closure-captured values
117
115
  const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
@@ -150,9 +148,9 @@ export async function sendStatusUpdate(params) {
150
148
  msgDetail: JSON.stringify(jsonRpcResponse),
151
149
  };
152
150
  // 📋 Log complete response body
153
- log(`[A2A_STATUS] 📤 Sending A2A status-update:`);
154
- log(`[A2A_STATUS] - taskId: ${currentTaskId}`);
155
- log(`[A2A_STATUS] - text: "${text}"`);
151
+ logger.log(`[A2A_STATUS] 📤 Sending A2A status-update:`);
152
+ logger.log(`[A2A_STATUS] - taskId: ${currentTaskId}`);
153
+ logger.log(`[A2A_STATUS] - text: "${text}"`);
156
154
  await wsManager.sendMessage(sessionId, outboundMessage);
157
155
  }
158
156
  /**
@@ -1,5 +1,6 @@
1
1
  // Heartbeat management for WebSocket connections
2
2
  import WebSocket from "ws";
3
+ import { logger } from "./utils/logger.js";
3
4
  /**
4
5
  * Manages heartbeat for a WebSocket connection.
5
6
  * Supports both application-level (30s) and protocol-level (20s) heartbeats.
@@ -23,8 +24,8 @@ export class HeartbeatManager {
23
24
  this.onTimeout = onTimeout;
24
25
  this.serverName = serverName;
25
26
  this.onHeartbeatSuccess = onHeartbeatSuccess;
26
- this.log = logFn ?? console.log;
27
- this.error = errorFn ?? console.error;
27
+ this.log = logFn ?? ((msg, ...args) => logger.log(msg, ...args));
28
+ this.error = errorFn ?? ((msg, ...args) => logger.error(msg, ...args));
28
29
  }
29
30
  /**
30
31
  * Start heartbeat monitoring.