@ynhcj/xiaoyi-channel 0.0.142-beta → 0.0.142-next

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.
Files changed (77) hide show
  1. package/dist/index.js +3 -69
  2. package/dist/src/approval-bridge.d.ts +48 -0
  3. package/dist/src/approval-bridge.js +382 -0
  4. package/dist/src/bot.js +75 -73
  5. package/dist/src/client.js +13 -23
  6. package/dist/src/cspl/call_api.d.ts +2 -0
  7. package/dist/src/cspl/call_api.js +107 -0
  8. package/dist/src/cspl/config.d.ts +4 -17
  9. package/dist/src/cspl/config.js +80 -70
  10. package/dist/src/cspl/configs.json +10 -0
  11. package/dist/src/cspl/constants.d.ts +46 -24
  12. package/dist/src/cspl/constants.js +41 -16
  13. package/dist/src/cspl/sentinel_hook.d.ts +2 -0
  14. package/dist/src/cspl/sentinel_hook.js +84 -0
  15. package/dist/src/cspl/steer-context.js +1 -1
  16. package/dist/src/cspl/upload_file.d.ts +1 -0
  17. package/dist/src/cspl/upload_file.js +211 -0
  18. package/dist/src/cspl/utils.d.ts +11 -2
  19. package/dist/src/cspl/utils.js +265 -15
  20. package/dist/src/formatter.js +92 -37
  21. package/dist/src/monitor.js +18 -21
  22. package/dist/src/outbound.js +8 -9
  23. package/dist/src/provider.js +50 -70
  24. package/dist/src/push.js +8 -15
  25. package/dist/src/reply-dispatcher.js +39 -48
  26. package/dist/src/self-evolution-handler.js +1 -1
  27. package/dist/src/sensitive-redactor.d.ts +4 -0
  28. package/dist/src/sensitive-redactor.js +364 -0
  29. package/dist/src/task-manager.js +6 -10
  30. package/dist/src/tools/agent-as-skill-tool.d.ts +7 -0
  31. package/dist/src/tools/agent-as-skill-tool.js +138 -0
  32. package/dist/src/tools/calendar-tool.js +1 -1
  33. package/dist/src/tools/call-device-tool.js +3 -0
  34. package/dist/src/tools/call-phone-tool.js +1 -1
  35. package/dist/src/tools/create-alarm-tool.js +1 -1
  36. package/dist/src/tools/create-all-tools.js +5 -1
  37. package/dist/src/tools/delete-alarm-tool.js +1 -1
  38. package/dist/src/tools/find-pc-devices-tool.d.ts +2 -1
  39. package/dist/src/tools/find-pc-devices-tool.js +84 -88
  40. package/dist/src/tools/get-device-file-tool-schema.js +3 -2
  41. package/dist/src/tools/image-reading-tool.js +7 -6
  42. package/dist/src/tools/location-tool.js +1 -1
  43. package/dist/src/tools/modify-alarm-tool.js +1 -1
  44. package/dist/src/tools/modify-note-tool.js +1 -1
  45. package/dist/src/tools/note-tool.js +1 -1
  46. package/dist/src/tools/query-app-message-tool.js +1 -1
  47. package/dist/src/tools/query-memory-data-tool.js +1 -1
  48. package/dist/src/tools/query-todo-task-tool.js +1 -1
  49. package/dist/src/tools/save-file-to-phone-tool.js +1 -1
  50. package/dist/src/tools/save-media-to-gallery-tool.js +1 -1
  51. package/dist/src/tools/search-alarm-tool.js +1 -1
  52. package/dist/src/tools/search-calendar-tool.js +1 -1
  53. package/dist/src/tools/search-contact-tool.js +1 -1
  54. package/dist/src/tools/search-email-tool.js +1 -1
  55. package/dist/src/tools/search-file-tool.js +11 -8
  56. package/dist/src/tools/search-message-tool.js +1 -1
  57. package/dist/src/tools/search-note-tool.js +1 -1
  58. package/dist/src/tools/search-photo-gallery-tool.js +1 -1
  59. package/dist/src/tools/send-email-tool.js +1 -1
  60. package/dist/src/tools/send-file-to-user-tool.js +2 -2
  61. package/dist/src/tools/send-message-tool.js +1 -1
  62. package/dist/src/tools/session-manager.js +5 -0
  63. package/dist/src/tools/upload-file-tool.js +15 -5
  64. package/dist/src/tools/upload-photo-tool.js +1 -1
  65. package/dist/src/tools/xiaoyi-add-collection-tool.js +1 -1
  66. package/dist/src/tools/xiaoyi-collection-tool.js +1 -1
  67. package/dist/src/tools/xiaoyi-delete-collection-tool.js +1 -1
  68. package/dist/src/tools/xiaoyi-gui-tool.js +1 -1
  69. package/dist/src/trigger-handler.js +4 -7
  70. package/dist/src/utils/config-manager.js +3 -6
  71. package/dist/src/utils/logger.d.ts +8 -0
  72. package/dist/src/utils/logger.js +69 -34
  73. package/dist/src/utils/pushdata-manager.js +1 -5
  74. package/dist/src/utils/pushid-manager.js +1 -2
  75. package/dist/src/utils/runtime-manager.js +1 -4
  76. package/dist/src/websocket.js +37 -25
  77. package/package.json +1 -1
package/dist/src/bot.js CHANGED
@@ -35,7 +35,8 @@ export async function handleXYMessage(params) {
35
35
  if (!sessionId) {
36
36
  throw new Error("clearContext request missing sessionId in params");
37
37
  }
38
- logger.log(`Clear context request for session ${sessionId}`);
38
+ const log = logger.withContext(sessionId, "");
39
+ log.log(`[BOT] Clear context request`);
39
40
  const config = resolveXYConfig(cfg);
40
41
  await sendClearContextResponse({
41
42
  config,
@@ -51,7 +52,8 @@ export async function handleXYMessage(params) {
51
52
  if (!sessionId) {
52
53
  throw new Error("tasks/cancel request missing sessionId in params");
53
54
  }
54
- logger.log(`Tasks cancel request for session ${sessionId}, task ${taskId}`);
55
+ const log = logger.withContext(sessionId, taskId);
56
+ log.log(`[BOT] Tasks cancel request`);
55
57
  const config = resolveXYConfig(cfg);
56
58
  await sendTasksCancelResponse({
57
59
  config,
@@ -63,22 +65,21 @@ export async function handleXYMessage(params) {
63
65
  }
64
66
  // Parse the A2A message (for regular messages)
65
67
  const parsed = parseA2AMessage(message);
68
+ // Scoped logger for this session — avoids concurrent session log mixing
69
+ const log = logger.withContext(parsed.sessionId, parsed.taskId);
66
70
  // ========== 检测 Trigger 消息 ==========
67
71
  // 如果消息中包含 Trigger 事件数据,直接返回 pushData 内容,不走正常流程
68
72
  const triggerData = extractTriggerData(parsed.parts);
69
73
  if (triggerData) {
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
+ log.log(`[BOT] Detected Trigger message, pushDataId=${triggerData.pushDataId}`);
73
75
  try {
74
76
  // 读取 pushData
75
77
  const pushDataItem = await getPushDataById(triggerData.pushDataId);
76
78
  if (!pushDataItem) {
77
- logger.error(`[BOT] pushData not found for ID: ${triggerData.pushDataId}`);
79
+ log.error(`[BOT] pushData not found for ID: ${triggerData.pushDataId}`);
78
80
  return;
79
81
  }
80
- logger.log(`[BOT] Found pushData, sending direct response`);
81
- logger.log(`[BOT] - pushDataId: ${pushDataItem.pushDataId}`);
82
+ log.log(`[BOT] Found pushData, sending direct response, pushDataId=${pushDataItem.pushDataId}`);
82
83
  const config = resolveXYConfig(cfg);
83
84
  // 直接发送响应(final=true,不走 openclaw 流程)
84
85
  await sendA2AResponse({
@@ -90,11 +91,11 @@ export async function handleXYMessage(params) {
90
91
  append: false,
91
92
  final: true,
92
93
  });
93
- logger.log(`[BOT] Trigger response sent successfully, exiting early`);
94
+ log.log(`[BOT] Trigger response sent successfully`);
94
95
  return; // 提前返回,不继续处理
95
96
  }
96
97
  catch (err) {
97
- logger.error(`[BOT] Failed to handle Trigger message:`, err);
98
+ log.error(`[BOT] Failed to handle Trigger message:`, err);
98
99
  return;
99
100
  }
100
101
  }
@@ -103,9 +104,7 @@ export async function handleXYMessage(params) {
103
104
  const isUpdate = hasActiveTask(parsed.sessionId);
104
105
  const skipReg = params.skipRegistration === true;
105
106
  if (isUpdate) {
106
- logger.log(`[BOT] 🔄 STEER MODE - Second message detected (core will handle steer)`);
107
- logger.log(`[BOT] - Session: ${parsed.sessionId}`);
108
- logger.log(`[BOT] - New taskId: ${parsed.taskId}`);
107
+ log.log(`[BOT] STEER MODE - Second message detected, new taskId=${parsed.taskId}`);
109
108
  }
110
109
  // Steer injections skip taskId registration to avoid overwriting the active taskId
111
110
  if (!skipReg) {
@@ -113,28 +112,28 @@ export async function handleXYMessage(params) {
113
112
  // Extract and update push_id if present
114
113
  const pushId = extractPushId(parsed.parts);
115
114
  if (pushId) {
116
- logger.log(`[BOT] 📌 Extracted push_id from user message`);
115
+ log.log(`[BOT] Extracted push_id from user message`);
117
116
  configManager.updatePushId(parsed.sessionId, pushId);
118
117
  // 持久化 pushId 到本地文件(异步,不阻塞主流程)
119
118
  addPushId(pushId).catch((err) => {
120
- logger.error(`[BOT] Failed to persist pushId:`, err);
119
+ log.error(`[BOT] Failed to persist pushId:`, err);
121
120
  });
122
121
  }
123
122
  else {
124
- logger.log(`[BOT] ℹ️ No push_id found in message, will use config default`);
123
+ log.log(`[BOT] No push_id found in message, using config default`);
125
124
  }
126
125
  // 保存 runtime 信息到 .xiaoyiruntime 文件(异步,不阻塞主流程)
127
126
  saveRuntimeInfo(webSocketSessionId || parsed.sessionId, // SESSION_ID (WebSocket 层级,如果没有则 fallback)
128
127
  parsed.sessionId, // CONVERSATION_ID (param 里的 sessionId)
129
128
  parsed.taskId // TASK_ID (param.id)
130
129
  ).catch((err) => {
131
- logger.error(`[BOT] Failed to save runtime info:`, err);
130
+ log.error(`[BOT] Failed to save runtime info:`, err);
132
131
  });
133
132
  }
134
133
  // Extract deviceType if present (always parse — used in ctxPayload.MessageSid)
135
134
  const deviceType = extractDeviceType(parsed.parts);
136
135
  if (deviceType) {
137
- logger.log(`[BOT] 📱 Extracted deviceType from user message: ${deviceType}`);
136
+ log.log(`[BOT] Extracted deviceType: ${deviceType}`);
138
137
  }
139
138
  // Resolve configuration (needed for status updates)
140
139
  const config = resolveXYConfig(cfg);
@@ -150,7 +149,7 @@ export async function handleXYMessage(params) {
150
149
  id: parsed.sessionId, // ✅ Use sessionId to share context within the same conversation session
151
150
  },
152
151
  });
153
- logger.log(`xy: resolved route accountId=${route.accountId}, sessionKey=${route.sessionKey}`);
152
+ log.log(`[BOT] Resolved route, sessionKey=${route.sessionKey}`);
154
153
  // Steer injections skip session registration to avoid refCount leaks
155
154
  if (!skipReg) {
156
155
  registerSession(route.sessionKey, {
@@ -162,7 +161,7 @@ export async function handleXYMessage(params) {
162
161
  deviceType,
163
162
  });
164
163
  // 🔑 发送初始状态更新
165
- logger.log(`[STATUS] Sending initial status update for session ${parsed.sessionId}`);
164
+ log.log(`[BOT] Sending initial status update`);
166
165
  void sendStatusUpdate({
167
166
  config,
168
167
  sessionId: parsed.sessionId,
@@ -171,7 +170,7 @@ export async function handleXYMessage(params) {
171
170
  text: "任务正在处理中,请稍候~",
172
171
  state: "working",
173
172
  }).catch((err) => {
174
- logger.error(`Failed to send initial status update:`, err);
173
+ log.error(`Failed to send initial status update:`, err);
175
174
  });
176
175
  }
177
176
  // Extract text and files from parts
@@ -183,18 +182,18 @@ export async function handleXYMessage(params) {
183
182
  const selfEvolutionEnabled = await selfEvolutionManager.isEnabled();
184
183
  if (selfEvolutionEnabled && shouldNudgeForSelfEvolutionKeyword(textForAgent)) {
185
184
  const shouldNudge = toolCallNudgeManager.tryMarkKeywordNudge(route.sessionKey);
186
- logger.log(`[SELF_EVOLUTION] Keyword check hit during inbound build: sessionKey=${route.sessionKey}, shouldNudge=${shouldNudge}`);
185
+ log.log(`[SELF_EVOLUTION] Keyword check hit during inbound build: sessionKey=${route.sessionKey}, shouldNudge=${shouldNudge}`);
187
186
  if (shouldNudge) {
188
187
  const augmented = appendSelfEvolutionKeywordNudge(textForAgent);
189
188
  textForAgent = augmented.text;
190
189
  if (augmented.appended) {
191
- logger.log(`[SELF_EVOLUTION] Keyword-triggered inline nudge appended: sessionKey=${route.sessionKey}`);
190
+ log.log(`[SELF_EVOLUTION] Keyword-triggered inline nudge appended: sessionKey=${route.sessionKey}`);
192
191
  }
193
192
  }
194
193
  }
195
194
  }
196
195
  catch (selfEvolutionError) {
197
- logger.error(`[SELF_EVOLUTION] Failed to append inline keyword nudge: ${String(selfEvolutionError)}`);
196
+ log.error(`[SELF_EVOLUTION] Failed to append inline keyword nudge: ${String(selfEvolutionError)}`);
198
197
  }
199
198
  }
200
199
  // 🔑 Steer消息: 跳过旧路径直接进入 streaming-signal 队列
@@ -209,9 +208,11 @@ export async function handleXYMessage(params) {
209
208
  const steerDownloadedFiles = await downloadFilesFromParts(steerFileParts);
210
209
  const steerMediaPayload = buildXYMediaPayload(steerDownloadedFiles);
211
210
  if (steerFileParts.length > 0) {
212
- logger.log(`[BOT] 📎 Steer message with files: ${steerFileParts.length} file(s)`);
211
+ log.log(`[BOT] Steer message with ${steerFileParts.length} file(s), enqueuing to streaming-signal queue`);
212
+ }
213
+ else {
214
+ log.log(`[BOT] Steer message — enqueuing to streaming-signal queue`);
213
215
  }
214
- logger.log(`[BOT] 🔄 Steer message — enqueuing to streaming-signal queue`);
215
216
  await enqueueSteer({
216
217
  sessionId: parsed.sessionId,
217
218
  sessionKey: route.sessionKey,
@@ -223,8 +224,7 @@ export async function handleXYMessage(params) {
223
224
  route,
224
225
  deviceType,
225
226
  });
226
- logger.log(`[BOT] Steer queue completed for session: ${parsed.sessionId}`);
227
- logger.log(`xy: dispatch complete (session=${parsed.sessionId})`);
227
+ log.log(`[BOT] Steer queue completed`);
228
228
  return;
229
229
  }
230
230
  // ── First message (non-steer) path below ──────────────────────
@@ -236,7 +236,7 @@ export async function handleXYMessage(params) {
236
236
  if (!skipReg) {
237
237
  const fileParts = extractFileParts(parsed.parts);
238
238
  const downloadedFiles = await downloadFilesFromParts(fileParts);
239
- logger.log("Downloaded files:", JSON.stringify(downloadedFiles, null, 2));
239
+ log.log(`[BOT] Downloaded ${downloadedFiles.length} file(s)`);
240
240
  mediaPayload = buildXYMediaPayload(downloadedFiles);
241
241
  }
242
242
  // Resolve envelope format options (following feishu pattern)
@@ -270,7 +270,7 @@ export async function handleXYMessage(params) {
270
270
  SenderId: parsed.sessionId,
271
271
  Provider: "xiaoyi-channel",
272
272
  Surface: "xiaoyi-channel",
273
- MessageSid: `${parsed.taskId}_${deviceType}`,
273
+ MessageSid: `xiaoyi_${parsed.taskId}_${deviceType}`,
274
274
  Timestamp: Date.now(),
275
275
  WasMentioned: false,
276
276
  CommandAuthorized: true,
@@ -282,8 +282,7 @@ export async function handleXYMessage(params) {
282
282
  // 🔑 Streaming 信号已在上方创建(在文件下载之前)
283
283
  const steerState = { steered: false };
284
284
  // 🔑 创建dispatcher
285
- logger.log(`[BOT-DISPATCHER] 🎯 Creating reply dispatcher`);
286
- logger.log(`[BOT-DISPATCHER] - taskId: ${parsed.taskId}`);
285
+ log.log(`[BOT-DISPATCHER] Creating reply dispatcher`);
287
286
  const { dispatcher, replyOptions, markDispatchIdle, startStatusInterval } = createXYReplyDispatcher({
288
287
  cfg,
289
288
  runtime,
@@ -306,20 +305,20 @@ export async function handleXYMessage(params) {
306
305
  agentId: route.accountId,
307
306
  deviceType,
308
307
  };
309
- logger.log(`[BOT-DISPATCH] withReplyDispatcher starting, sessionKey=${route.sessionKey}`);
308
+ log.log(`[BOT-DISPATCH] withReplyDispatcher starting, sessionKey=${route.sessionKey}`);
310
309
  await core.channel.reply.withReplyDispatcher({
311
310
  dispatcher,
312
311
  onSettled: () => {
313
- logger.log(`[BOT] 🏁 onSettled called for session: ${route.sessionKey}`);
314
- logger.log(`[BOT] - steered: ${steerState.steered}`);
312
+ log.log(`[BOT] onSettled, steered=${steerState.steered}`);
315
313
  // 🔑 When steered, skip heavy cleanup — the first message's dispatcher is still running
316
314
  if (steerState.steered) {
317
- logger.log(`[BOT] Steered dispatch settled (skipping cleanup)`);
315
+ log.log(`[BOT] Steered dispatch settled, skipping cleanup`);
318
316
  return;
319
317
  }
318
+ streamingSignals.delete(parsed.sessionId);
320
319
  decrementTaskIdRef(parsed.sessionId);
321
320
  unregisterSession(route.sessionKey);
322
- logger.log(`[BOT] Cleanup completed`);
321
+ log.log(`[BOT] Cleanup completed`);
323
322
  },
324
323
  run: () => {
325
324
  // 🔐 Use AsyncLocalStorage to provide session context to tools.
@@ -328,12 +327,7 @@ export async function handleXYMessage(params) {
328
327
  // signal init complete to release the global dispatch gate
329
328
  // for the next session.
330
329
  const dispatchPromise = runWithSessionContext(sessionContext, async () => {
331
- logger.log(`[BOT-DISPATCH] dispatchReplyFromConfig starting...`);
332
- logger.log(`[BOT-DISPATCH] - sessionKey: ${ctxPayload.SessionKey}`);
333
- logger.log(`[BOT-DISPATCH] - provider: ${ctxPayload.Provider}`);
334
- logger.log(`[BOT-DISPATCH] - surface: ${ctxPayload.Surface}`);
335
- logger.log(`[BOT-DISPATCH] - from: ${ctxPayload.From}`);
336
- logger.log(`[BOT-DISPATCH] - body length: ${ctxPayload.Body?.length ?? 0}`);
330
+ log.log(`[BOT-DISPATCH] dispatchReplyFromConfig starting, body.length=${ctxPayload.Body?.length ?? 0}`);
337
331
  try {
338
332
  const result = await core.channel.reply.dispatchReplyFromConfig({
339
333
  ctx: ctxPayload,
@@ -341,15 +335,11 @@ export async function handleXYMessage(params) {
341
335
  dispatcher,
342
336
  replyOptions,
343
337
  });
344
- logger.log(`[BOT-DISPATCH] dispatchReplyFromConfig returned`);
345
- logger.log(`[BOT-DISPATCH] - result: ${JSON.stringify(result)}`);
338
+ log.log(`[BOT-DISPATCH] dispatchReplyFromConfig returned, result=${JSON.stringify(result)}`);
346
339
  return result;
347
340
  }
348
341
  catch (dispatchErr) {
349
- logger.error(`[BOT-DISPATCH] dispatchReplyFromConfig threw`);
350
- logger.error(`[BOT-DISPATCH] - error name: ${dispatchErr instanceof Error ? dispatchErr.name : "unknown"}`);
351
- logger.error(`[BOT-DISPATCH] - error message: ${String(dispatchErr)}`);
352
- logger.error(`[BOT-DISPATCH] - error stack: ${dispatchErr instanceof Error ? dispatchErr.stack?.slice(0, 500) : "N/A"}`);
342
+ log.error(`[BOT-DISPATCH] dispatchReplyFromConfig threw: ${dispatchErr instanceof Error ? `${dispatchErr.name}: ${dispatchErr.message}` : String(dispatchErr)}`, dispatchErr instanceof Error ? dispatchErr.stack?.slice(0, 500) : undefined);
353
343
  throw dispatchErr;
354
344
  }
355
345
  });
@@ -358,20 +348,23 @@ export async function handleXYMessage(params) {
358
348
  return dispatchPromise;
359
349
  },
360
350
  });
361
- logger.log(`[BOT] Dispatcher completed for session: ${parsed.sessionId}`);
362
- logger.log(`xy: dispatch complete (session=${parsed.sessionId})`);
351
+ log.log(`[BOT] Dispatcher completed`);
363
352
  }
364
353
  catch (err) {
365
354
  // ✅ Only log error, don't re-throw to prevent gateway restart
366
- logger.error("Failed to handle XY message:", err);
355
+ // Note: if error occurs before parseA2AMessage, `log` may not be defined yet
356
+ const errSessionId = message.params?.sessionId || "";
357
+ const errTaskId = message.params?.id || message.id || "";
358
+ const errLog = logger.withContext(errSessionId, errTaskId);
359
+ errLog.error("Failed to handle XY message:", err);
367
360
  runtime.error?.(`xy: Failed to handle message: ${String(err)}`);
368
- logger.log(`[BOT] Error occurred, attempting cleanup...`);
361
+ errLog.log(`[BOT] Error occurred, attempting cleanup`);
369
362
  // 🔑 错误时也要清理taskId和session
370
363
  try {
371
364
  const params = message.params;
372
365
  const sessionId = params?.sessionId;
373
366
  if (sessionId) {
374
- logger.log(`[BOT] 🧹 Cleaning up after error: ${sessionId}`);
367
+ errLog.log(`[BOT] Cleaning up after error`);
375
368
  // 清理 taskId
376
369
  decrementTaskIdRef(sessionId);
377
370
  // 清理 session
@@ -386,11 +379,11 @@ export async function handleXYMessage(params) {
386
379
  },
387
380
  });
388
381
  unregisterSession(route.sessionKey);
389
- logger.log(`[BOT] Cleanup completed after error`);
382
+ errLog.log(`[BOT] Cleanup completed after error`);
390
383
  }
391
384
  }
392
385
  catch (cleanupErr) {
393
- logger.log(`[BOT] ⚠️ Cleanup failed:`, cleanupErr);
386
+ errLog.log(`[BOT] Cleanup failed:`, cleanupErr);
394
387
  // Ignore cleanup errors
395
388
  }
396
389
  // ❌ Don't re-throw: message processing error should not affect gateway stability
@@ -427,19 +420,22 @@ const steerQueues = _g.__xySteerQueues;
427
420
  * 这是模型 API 被调用的精确时刻,此时 isStreaming 一定为 true。
428
421
  */
429
422
  export function notifyModelStreaming(sessionId) {
423
+ const log = logger.withContext(sessionId, "");
430
424
  const signal = streamingSignals.get(sessionId);
431
425
  if (signal) {
432
- streamingSignals.delete(sessionId);
426
+ // 不删除 signal——后续 steer 需要靠它判断模型已在 streaming。
427
+ // 清理由第一条消息的 onSettled 兜底。
433
428
  signal.notify();
434
- logger.log(`[STEER-QUEUE] 📡 Model streaming signal fired for session=${sessionId}`);
429
+ log.log(`[STEER-QUEUE] Model streaming signal fired`);
435
430
  }
436
431
  }
437
432
  function createStreamingSignal(sessionId) {
433
+ const log = logger.withContext(sessionId, "");
438
434
  let resolve;
439
435
  const promise = new Promise(r => { resolve = r; });
440
436
  const signal = { promise, notify: resolve };
441
437
  streamingSignals.set(sessionId, signal);
442
- logger.log(`[STEER-QUEUE] 🟢 Streaming signal created for session ${sessionId}`);
438
+ log.log(`[STEER-QUEUE] Streaming signal created`);
443
439
  return signal;
444
440
  }
445
441
  /**
@@ -449,13 +445,14 @@ function createStreamingSignal(sessionId) {
449
445
  */
450
446
  function enqueueSteer(params) {
451
447
  const { sessionId } = params;
448
+ const log = logger.withContext(sessionId, params.parsed.taskId);
452
449
  // 取出当前队列尾部(或 undefined),然后链上新的 Promise
453
450
  const prev = steerQueues.get(sessionId);
454
451
  const next = (prev ?? Promise.resolve()).then(() => dispatchSteerWhenReady(params));
455
452
  steerQueues.set(sessionId, next);
456
453
  // 链条结束后清理
457
454
  next.catch((err) => {
458
- logger.error(`[STEER-QUEUE] Steer chain failed: ${String(err)}`);
455
+ log.error(`[STEER-QUEUE] Steer chain failed: ${String(err)}`);
459
456
  }).finally(() => {
460
457
  if (steerQueues.get(sessionId) === next) {
461
458
  steerQueues.delete(sessionId);
@@ -465,44 +462,49 @@ function enqueueSteer(params) {
465
462
  }
466
463
  async function dispatchSteerWhenReady(params) {
467
464
  const { sessionId, sessionKey, steerText } = params;
465
+ const log = logger.withContext(sessionId, params.parsed.taskId);
468
466
  // 1. 等待第一条消息开始 streaming
469
467
  // signal 可能尚未创建(第一条消息还在文件下载等耗时操作中),
470
468
  // 轮询等待直到 signal 出现,最長等待 ~5 秒。
471
469
  let signal = streamingSignals.get(sessionId);
472
470
  if (!signal) {
473
- logger.log(`[STEER-QUEUE] Signal not yet created, polling for session=${sessionId}`);
471
+ log.log(`[STEER-QUEUE] Signal not yet created, polling`);
474
472
  for (let i = 0; i < 50; i++) {
475
473
  await new Promise(r => setTimeout(r, 100));
476
474
  signal = streamingSignals.get(sessionId);
477
475
  if (signal)
478
476
  break;
479
477
  if (!hasActiveTask(sessionId)) {
480
- logger.log(`[STEER-QUEUE] ℹ️ First message completed while waiting, skip steer`);
478
+ log.log(`[STEER-QUEUE] First message completed while waiting, skip steer`);
481
479
  return;
482
480
  }
483
481
  }
484
482
  }
485
483
  if (signal) {
486
- logger.log(`[STEER-QUEUE] Waiting for streaming signal, session=${sessionId}`);
484
+ log.log(`[STEER-QUEUE] Waiting for streaming signal`);
487
485
  await signal.promise;
488
- streamingSignals.delete(sessionId);
489
- logger.log(`[STEER-QUEUE] ✅ Streaming signal received, session=${sessionId}`);
486
+ log.log(`[STEER-QUEUE] Streaming signal received`);
490
487
  }
491
488
  else {
492
489
  // 轮询超时且 hasActiveTask 仍为 true——说明第一条消息可能卡在异常路径,
493
490
  // 没有创建 signal。此时 dispatch 会与第一条消息的模型调用并发冲突,放弃。
494
- logger.log(`[STEER-QUEUE] ⚠️ Signal never appeared after polling, skip steer to avoid collision`);
491
+ log.log(`[STEER-QUEUE] Signal never appeared after polling, skip steer to avoid collision`);
495
492
  return;
496
493
  }
497
494
  // 2. 第一条消息已结束 → 放弃
498
495
  if (!hasActiveTask(sessionId)) {
499
- logger.log(`[STEER-QUEUE] ℹ️ First message completed, skip steer`);
496
+ log.log(`[STEER-QUEUE] First message completed, skip steer`);
500
497
  return;
501
498
  }
502
499
  // 3. 构建 dispatch 上下文并 dispatch /steer
503
500
  const core = getXYRuntime();
504
501
  const speaker = sessionId;
505
- const steerCommand = `/steer ${steerText}`;
502
+ // 如果有文件附件,把路径拼到 steer 文本末尾,让模型通过工具读取
503
+ const mediaPaths = params.mediaPayload?.MediaPaths;
504
+ const fileHint = mediaPaths && mediaPaths.length > 0
505
+ ? `\n【用户上传附件】:${JSON.stringify(mediaPaths)}`
506
+ : "";
507
+ const steerCommand = `/steer ${steerText}${fileHint}`;
506
508
  const messageBody = `${speaker}: ${steerCommand}`;
507
509
  const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(params.cfg);
508
510
  const body = core.channel.reply.formatAgentEnvelope({
@@ -526,7 +528,7 @@ async function dispatchSteerWhenReady(params) {
526
528
  SenderId: sessionId,
527
529
  Provider: "xiaoyi-channel",
528
530
  Surface: "xiaoyi-channel",
529
- MessageSid: `${params.parsed.taskId}_${params.deviceType}`,
531
+ MessageSid: `xiaoyi_${params.parsed.taskId}_${params.deviceType}`,
530
532
  Timestamp: Date.now(),
531
533
  WasMentioned: false,
532
534
  CommandAuthorized: true,
@@ -553,11 +555,11 @@ async function dispatchSteerWhenReady(params) {
553
555
  agentId: params.route.accountId,
554
556
  deviceType: params.deviceType,
555
557
  };
556
- logger.log(`[STEER-QUEUE] 🚀 Dispatching steer for session=${sessionId}`);
558
+ log.log(`[STEER-QUEUE] Dispatching steer`);
557
559
  await core.channel.reply.withReplyDispatcher({
558
560
  dispatcher,
559
561
  onSettled: () => {
560
- logger.log(`[STEER-QUEUE] 🏁 Steer dispatch settled for session=${sessionId}`);
562
+ log.log(`[STEER-QUEUE] Steer dispatch settled`);
561
563
  },
562
564
  run: () => {
563
565
  return runWithSessionContext(sessionContext, async () => {
@@ -567,10 +569,10 @@ async function dispatchSteerWhenReady(params) {
567
569
  dispatcher,
568
570
  replyOptions,
569
571
  });
570
- logger.log(`[STEER-QUEUE] dispatch result: ${JSON.stringify(result)}`);
572
+ log.log(`[STEER-QUEUE] dispatch result: ${JSON.stringify(result)}`);
571
573
  return result;
572
574
  });
573
575
  },
574
576
  });
575
- logger.log(`[STEER-QUEUE] Steer dispatch completed for session=${sessionId}`);
577
+ log.log(`[STEER-QUEUE] Steer dispatch completed`);
576
578
  }
@@ -24,10 +24,10 @@ export function getXYWebSocketManager(config, runtime) {
24
24
  return cached;
25
25
  }
26
26
  // Create new manager
27
- logger.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}`);
28
28
  cached = new XYWebSocketManager(config, runtime);
29
29
  wsManagerCache.set(cacheKey, cached);
30
- logger.log(`[WS-MANAGER-CACHE] 📊 Total managers after creation: ${wsManagerCache.size}`);
30
+ logger.log(`[WS-MANAGER-CACHE] Total managers after creation: ${wsManagerCache.size}`);
31
31
  return cached;
32
32
  }
33
33
  /**
@@ -38,13 +38,13 @@ export function removeXYWebSocketManager(config) {
38
38
  const cacheKey = `${config.apiKey}-${config.agentId}`;
39
39
  const manager = wsManagerCache.get(cacheKey);
40
40
  if (manager) {
41
- logger.log(`🗑️ [WS-MANAGER-CACHE] Removing manager from cache: ${cacheKey}`);
41
+ logger.log(`[WS-MANAGER-CACHE] Removing manager from cache: ${cacheKey}`);
42
42
  manager.disconnect();
43
43
  wsManagerCache.delete(cacheKey);
44
- logger.log(`🗑️ [WS-MANAGER-CACHE] Manager removed, remaining managers: ${wsManagerCache.size}`);
44
+ logger.log(`[WS-MANAGER-CACHE] Manager removed, remaining managers: ${wsManagerCache.size}`);
45
45
  }
46
46
  else {
47
- logger.log(`⚠️ [WS-MANAGER-CACHE] Manager not found in cache: ${cacheKey}`);
47
+ logger.log(`[WS-MANAGER-CACHE] Manager not found in cache: ${cacheKey}`);
48
48
  }
49
49
  }
50
50
  /**
@@ -68,35 +68,25 @@ export function getCachedManagerCount() {
68
68
  * Helps identify connection issues and orphan connections.
69
69
  */
70
70
  export function diagnoseAllManagers() {
71
- logger.log(`Total cached managers: ${wsManagerCache.size}`);
71
+ logger.log(`[DIAG] Total cached managers: ${wsManagerCache.size}`);
72
72
  if (wsManagerCache.size === 0) {
73
- logger.log("ℹ️ No managers in cache");
73
+ logger.log("[DIAG] No managers in cache");
74
74
  return;
75
75
  }
76
76
  let orphanCount = 0;
77
77
  wsManagerCache.forEach((manager, key) => {
78
78
  const diag = manager.getConnectionDiagnostics();
79
- logger.log(` Total event listeners on manager: ${diag.totalEventListeners}`);
80
- // Connection
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}`);
79
+ logger.log(`[DIAG] Manager ${key} — event listeners: ${diag.totalEventListeners} | Connection: exists=${diag.connection.exists}, readyState=${diag.connection.readyState}, stateConnected=${diag.connection.stateConnected}/${diag.connection.stateReady}, reconnectAttempts=${diag.connection.reconnectAttempts}, wsListeners=${diag.connection.listenerCount}, heartbeatActive=${diag.connection.heartbeatActive}, hasReconnectTimer=${diag.connection.hasReconnectTimer}`);
89
80
  if (diag.connection.isOrphan) {
90
- logger.log(` ⚠️ ORPHAN CONNECTION DETECTED!`);
81
+ logger.log(`[DIAG] ORPHAN CONNECTION DETECTED on manager: ${key}`);
91
82
  orphanCount++;
92
83
  }
93
84
  });
94
85
  if (orphanCount > 0) {
95
- logger.log(`⚠️ Total orphan connections found: ${orphanCount}`);
96
- logger.log(`💡 Suggestion: These connections should be cleaned up`);
86
+ logger.log(`[DIAG] Total orphan connections found: ${orphanCount} — these connections should be cleaned up`);
97
87
  }
98
88
  else {
99
- logger.log(`✅ No orphan connections found`);
89
+ logger.log("[DIAG] No orphan connections found");
100
90
  }
101
91
  }
102
92
  /**
@@ -108,13 +98,13 @@ export function cleanupOrphanConnections() {
108
98
  wsManagerCache.forEach((manager, key) => {
109
99
  const diag = manager.getConnectionDiagnostics();
110
100
  if (diag.connection.isOrphan) {
111
- logger.log(`🧹 Cleaning up orphan connections in manager: ${key}`);
101
+ logger.log(`[CLEANUP] Cleaning up orphan connections in manager: ${key}`);
112
102
  manager.disconnect();
113
103
  cleanedCount++;
114
104
  }
115
105
  });
116
106
  if (cleanedCount > 0) {
117
- logger.log(`🧹 Cleaned up ${cleanedCount} manager(s) with orphan connections`);
107
+ logger.log(`[CLEANUP] Cleaned up ${cleanedCount} manager(s) with orphan connections`);
118
108
  }
119
109
  return cleanedCount;
120
110
  }
@@ -0,0 +1,2 @@
1
+ import { ApiResponse } from './constants.js';
2
+ export declare function callApi(questionText: string, api: any, sessionId: string): Promise<ApiResponse>;
@@ -0,0 +1,107 @@
1
+ /*
2
+ * 版权所有 (c) 华为技术有限公司 2026-2026
3
+ */
4
+ import https from 'https';
5
+ import { URL } from 'url';
6
+ import { getConfig } from './config.js';
7
+ import { DEFAULT_HTTP_PORT, HTTP_STATUS_BAD_REQUEST, API_URL_SUFFIX } from './constants.js';
8
+ function buildHeadersForCelia(config, sessionId) {
9
+ if (!config.uid || !config.apiKey || !config.skillId || !config.requestFrom) {
10
+ throw new Error('[SENTINEL HOOK] Missing required configuration: uid, apiKey, skillId, or requestFrom is not defined');
11
+ }
12
+ return {
13
+ 'x-hag-trace-id': sessionId,
14
+ 'x-uid': config.uid,
15
+ 'x-api-key': config.apiKey,
16
+ 'x-request-from': config.requestFrom,
17
+ 'x-skill-id': config.skillId,
18
+ 'content-type': 'application/json'
19
+ };
20
+ }
21
+ function buildRequestOptions(url, headers, timeout) {
22
+ const urlObj = new URL(url);
23
+ return {
24
+ hostname: urlObj.hostname,
25
+ port: urlObj.port || DEFAULT_HTTP_PORT,
26
+ path: urlObj.pathname,
27
+ method: "POST",
28
+ headers: headers,
29
+ timeout: timeout
30
+ };
31
+ }
32
+ function checkHttpStatus(res) {
33
+ if (res.statusCode && res.statusCode >= HTTP_STATUS_BAD_REQUEST) {
34
+ throw new Error(`HTTP error! status: ${res.statusCode}`);
35
+ }
36
+ }
37
+ function parseResponseData(data) {
38
+ try {
39
+ if (data === undefined || data === null || data.trim() === '') {
40
+ throw new Error('API response data is empty or invalid');
41
+ }
42
+ const jsonData = JSON.parse(data);
43
+ if (jsonData.retCode && jsonData.retCode !== "0") {
44
+ const errorMsg = jsonData.retMsg || 'Unknown API error';
45
+ throw new Error(`API error: ${errorMsg}`);
46
+ }
47
+ if (!jsonData.retCode && jsonData.code) {
48
+ const errorMsg = jsonData.desc || 'Unknown backend error';
49
+ throw new Error(`Backend error: ${errorMsg}`);
50
+ }
51
+ return jsonData;
52
+ }
53
+ catch (e) {
54
+ if (e instanceof Error) {
55
+ throw new Error(`[SENTINEL HOOK] Failed to parse response:${e.message}`);
56
+ }
57
+ return data;
58
+ }
59
+ }
60
+ function handleResponse(res, resolve, reject) {
61
+ let data = '';
62
+ try {
63
+ checkHttpStatus(res);
64
+ }
65
+ catch (e) {
66
+ reject(e);
67
+ }
68
+ res.on('data', (chunk) => {
69
+ data += chunk;
70
+ });
71
+ res.on('end', () => {
72
+ try {
73
+ const result = parseResponseData(data);
74
+ resolve(result);
75
+ }
76
+ catch (e) {
77
+ reject(e);
78
+ }
79
+ });
80
+ }
81
+ export async function callApi(questionText, api, sessionId) {
82
+ const config = getConfig(api);
83
+ const headersForCelia = buildHeadersForCelia(config, sessionId);
84
+ const payload = {
85
+ questionText: questionText,
86
+ textSource: config.textSource,
87
+ action: config.action,
88
+ extra: `${JSON.stringify({ userId: config.uid })}`
89
+ };
90
+ const httpBody = JSON.stringify(payload);
91
+ const apiUrl = `${config.api.url}${API_URL_SUFFIX}`;
92
+ return new Promise((resolve, reject) => {
93
+ const options = buildRequestOptions(apiUrl, headersForCelia, config.api.timeout);
94
+ const req = https.request(options, (res) => {
95
+ handleResponse(res, resolve, reject);
96
+ });
97
+ req.on('error', (error) => {
98
+ reject(error);
99
+ });
100
+ req.on('timeout', () => {
101
+ req.destroy();
102
+ reject(new Error('[SENTINEL HOOK] Request timeout'));
103
+ });
104
+ req.write(httpBody);
105
+ req.end();
106
+ });
107
+ }