@ynhcj/xiaoyi-channel 0.0.145-next → 0.0.146-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 (75) hide show
  1. package/dist/index.js +69 -26
  2. package/dist/src/bot.js +69 -73
  3. package/dist/src/channel.js +5 -59
  4. package/dist/src/client.js +23 -13
  5. package/dist/src/cron-query-handler.d.ts +17 -0
  6. package/dist/src/cron-query-handler.js +101 -0
  7. package/dist/src/cspl/config.d.ts +17 -4
  8. package/dist/src/cspl/config.js +70 -80
  9. package/dist/src/cspl/constants.d.ts +24 -46
  10. package/dist/src/cspl/constants.js +16 -41
  11. package/dist/src/cspl/sentinel_hook.js +16 -2
  12. package/dist/src/cspl/steer-context.js +1 -1
  13. package/dist/src/cspl/utils.d.ts +2 -11
  14. package/dist/src/cspl/utils.js +15 -265
  15. package/dist/src/formatter.d.ts +1 -11
  16. package/dist/src/formatter.js +39 -114
  17. package/dist/src/monitor.js +22 -22
  18. package/dist/src/outbound.js +9 -8
  19. package/dist/src/parser.d.ts +1 -2
  20. package/dist/src/parser.js +0 -25
  21. package/dist/src/push.d.ts +1 -11
  22. package/dist/src/push.js +17 -101
  23. package/dist/src/reply-dispatcher.js +49 -112
  24. package/dist/src/self-evolution-handler.js +1 -1
  25. package/dist/src/task-manager.js +10 -6
  26. package/dist/src/tools/agent-as-skill-tool.js +55 -4
  27. package/dist/src/tools/calendar-tool.js +1 -2
  28. package/dist/src/tools/call-device-tool.js +0 -3
  29. package/dist/src/tools/call-phone-tool.js +1 -2
  30. package/dist/src/tools/create-alarm-tool.js +1 -2
  31. package/dist/src/tools/create-all-tools.js +1 -9
  32. package/dist/src/tools/delete-alarm-tool.js +1 -2
  33. package/dist/src/tools/discover-cross-devices-tool.js +1 -1
  34. package/dist/src/tools/get-device-file-tool-schema.js +2 -3
  35. package/dist/src/tools/location-tool.js +1 -2
  36. package/dist/src/tools/modify-alarm-tool.js +1 -2
  37. package/dist/src/tools/modify-note-tool.js +1 -2
  38. package/dist/src/tools/note-tool.js +1 -2
  39. package/dist/src/tools/query-app-message-tool.js +2 -3
  40. package/dist/src/tools/query-memory-data-tool.js +2 -3
  41. package/dist/src/tools/query-todo-task-tool.js +2 -3
  42. package/dist/src/tools/save-file-to-phone-tool.js +1 -2
  43. package/dist/src/tools/save-media-to-gallery-tool.js +1 -2
  44. package/dist/src/tools/search-alarm-tool.js +1 -2
  45. package/dist/src/tools/search-calendar-tool.js +1 -2
  46. package/dist/src/tools/search-contact-tool.js +1 -2
  47. package/dist/src/tools/search-email-tool.js +2 -3
  48. package/dist/src/tools/search-file-tool.js +8 -12
  49. package/dist/src/tools/search-message-tool.js +1 -2
  50. package/dist/src/tools/search-note-tool.js +1 -2
  51. package/dist/src/tools/search-photo-gallery-tool.js +3 -4
  52. package/dist/src/tools/send-cross-device-task-tool.js +18 -22
  53. package/dist/src/tools/send-email-tool.js +2 -3
  54. package/dist/src/tools/send-file-to-user-tool.js +2 -2
  55. package/dist/src/tools/send-message-tool.js +1 -2
  56. package/dist/src/tools/session-manager.d.ts +1 -13
  57. package/dist/src/tools/session-manager.js +0 -43
  58. package/dist/src/tools/upload-file-tool.d.ts +1 -1
  59. package/dist/src/tools/upload-file-tool.js +5 -21
  60. package/dist/src/tools/upload-photo-tool.js +3 -4
  61. package/dist/src/tools/xiaoyi-add-collection-tool.js +1 -2
  62. package/dist/src/tools/xiaoyi-collection-tool.js +1 -2
  63. package/dist/src/tools/xiaoyi-delete-collection-tool.js +1 -2
  64. package/dist/src/tools/xiaoyi-gui-tool.js +1 -2
  65. package/dist/src/trigger-handler.js +7 -4
  66. package/dist/src/types.d.ts +0 -17
  67. package/dist/src/utils/config-manager.js +6 -3
  68. package/dist/src/utils/logger.d.ts +0 -8
  69. package/dist/src/utils/logger.js +34 -69
  70. package/dist/src/utils/pushdata-manager.js +5 -1
  71. package/dist/src/utils/pushid-manager.js +2 -1
  72. package/dist/src/utils/runtime-manager.js +4 -1
  73. package/dist/src/websocket.d.ts +0 -3
  74. package/dist/src/websocket.js +38 -203
  75. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,32 +1,17 @@
1
1
  import { definePluginEntry } from "openclaw/plugin-sdk/core";
2
2
  import { xiaoyiProvider } from "./src/provider.js";
3
3
  import { xyPlugin } from "./src/channel.js";
4
- import registerSentinelHook from "./src/cspl/sentinel_hook.js";
4
+ import { callCsplApiWithConfig } from "./src/cspl/call-api.js";
5
+ import { getCsplConfig, initCsplConfigFromXYConfig } from "./src/cspl/config.js";
6
+ import { ALLOWED_TOOLS, MAX_TEXT_LENGTH, MAX_TOTAL_LENGTH, MIN_TEXT_LENGTH, STEER_ABORT_MESSAGE, } from "./src/cspl/constants.js";
7
+ import { extractResultText, parseSecurityResult, processText, validateAndTruncateText, } from "./src/cspl/utils.js";
8
+ import { tryInjectSteer } from "./src/cspl/steer-context.js";
9
+ import { getSessionContext } from "./src/tools/session-manager.js";
10
+ import { logger } from "./src/utils/logger.js";
5
11
  import { setXYRuntime } from "./src/runtime.js";
6
- import { markCronToolCall, clearCronToolCall } from "./src/tools/session-manager.js";
7
12
  import { registerSelfEvolutionToolResultNudge } from "./src/self-evolution-tool-result-nudge.js";
8
13
  import { createBeforePromptBuildHandler } from "./src/skill-retriever/hooks.js";
9
14
  import { normalizeToolRetrieverConfig } from "./src/skill-retriever/config.js";
10
- /**
11
- * Register the cron detection hook.
12
- *
13
- * When openclaw's cron runner triggers a tool call, the sessionKey has the
14
- * format "cron:<jobId>". We use this to mark the toolCallId in a global Map
15
- * so that sendCommand() can route the command through the push channel
16
- * instead of the (non-existent) WebSocket session.
17
- */
18
- function registerCronDetectionHook(api) {
19
- api.on("before_tool_call", async (event, ctx) => {
20
- if (ctx.sessionKey?.startsWith("cron:") && event.toolCallId) {
21
- markCronToolCall(event.toolCallId);
22
- }
23
- });
24
- api.on("after_tool_call", async (event, ctx) => {
25
- if (event.toolCallId) {
26
- clearCronToolCall(event.toolCallId);
27
- }
28
- });
29
- }
30
15
  function registerFullHooks(api) {
31
16
  // SKILL RETRIEVER HOOK: before_prompt_build hook
32
17
  const pluginConfig = api.pluginConfig || {};
@@ -41,6 +26,67 @@ function registerFullHooks(api) {
41
26
  api.on("before_prompt_build", beforePromptBuildHandler);
42
27
  registerSelfEvolutionToolResultNudge(api);
43
28
  }
29
+ function registerCsplHook(api) {
30
+ // CSPL security scanning via after_tool_call hook.
31
+ // When CSPL returns REJECT, injects a steer message via tryInjectSteer
32
+ // to interrupt the agent. Uses skipRegistration to avoid refCount leaks
33
+ // and taskId overwrites.
34
+ // Only registered in "full" mode because it depends on handleXYMessage
35
+ // having cached cfg/runtime via setCsplSteerContext.
36
+ api.on("after_tool_call", async (event, ctx) => {
37
+ if (!ALLOWED_TOOLS.includes(event.toolName)) {
38
+ return;
39
+ }
40
+ try {
41
+ const resultText = extractResultText(event, event.toolName);
42
+ const resultLength = resultText.length;
43
+ if (resultLength <= MIN_TEXT_LENGTH || resultLength > MAX_TOTAL_LENGTH) {
44
+ return;
45
+ }
46
+ logger.log(`[SENTINEL HOOK] after_tool_call: toolName=${event.toolName}, textLength=${resultLength}`);
47
+ const questionText = {
48
+ subSceneID: "TOOL_OUTPUT",
49
+ tool: event.toolName,
50
+ output: [{ content: "" }],
51
+ };
52
+ const originText = processText(resultText);
53
+ questionText.output[0].content = originText;
54
+ let finalJson = JSON.stringify(questionText);
55
+ if (finalJson.length > MAX_TEXT_LENGTH) {
56
+ const diff = finalJson.length - MAX_TEXT_LENGTH;
57
+ const { text: trimmed } = validateAndTruncateText(originText, MAX_TEXT_LENGTH - diff);
58
+ questionText.output[0].content = trimmed;
59
+ finalJson = JSON.stringify(questionText);
60
+ }
61
+ const sessionCtx = getSessionContext(ctx.sessionKey ?? "");
62
+ const csplConfig = sessionCtx
63
+ ? initCsplConfigFromXYConfig(sessionCtx.config)
64
+ : getCsplConfig();
65
+ const csplStartTime = Date.now();
66
+ const response = await callCsplApiWithConfig(finalJson, csplConfig);
67
+ const csplElapsed = Date.now() - csplStartTime;
68
+ const result = parseSecurityResult(response);
69
+ logger.log(`[SENTINEL HOOK] Security result: status=${result.status}, toolName=${event.toolName}, elapsed=${csplElapsed}ms`);
70
+ if (result.status === "REJECT") {
71
+ logger.log(`[SENTINEL HOOK] REJECT - injecting steer via tryInjectSteer`);
72
+ if (sessionCtx) {
73
+ await tryInjectSteer({
74
+ sessionId: sessionCtx.sessionId,
75
+ taskId: sessionCtx.taskId,
76
+ message: STEER_ABORT_MESSAGE,
77
+ source: "cspl",
78
+ });
79
+ }
80
+ else {
81
+ logger.error("[SENTINEL HOOK] No session context, cannot inject steer");
82
+ }
83
+ }
84
+ }
85
+ catch (err) {
86
+ logger.error(`[SENTINEL HOOK] after_tool_call error: ${err}`);
87
+ }
88
+ });
89
+ }
44
90
  export default definePluginEntry({
45
91
  id: "xiaoyi-channel",
46
92
  name: "Xiaoyi Channel",
@@ -64,10 +110,7 @@ export default definePluginEntry({
64
110
  }
65
111
  if (api.registrationMode === "full") {
66
112
  registerFullHooks(api);
67
- // CSPL sentinel hook: before_tool_call + after_tool_call security scanning
68
- registerSentinelHook(api);
69
- // Cron detection hook: marks toolCallIds from cron sessions
70
- registerCronDetectionHook(api);
113
+ registerCsplHook(api);
71
114
  }
72
115
  },
73
116
  });
package/dist/src/bot.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { getXYRuntime } from "./runtime.js";
2
2
  import { createXYReplyDispatcher } from "./reply-dispatcher.js";
3
- import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId, extractDeviceType, extractTriggerData, extractRunCrossTaskContext } from "./parser.js";
3
+ import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId, extractDeviceType, extractTriggerData } from "./parser.js";
4
4
  import { downloadFilesFromParts } from "./file-download.js";
5
5
  import { resolveXYConfig } from "./config.js";
6
6
  import { sendStatusUpdate, sendClearContextResponse, sendTasksCancelResponse, sendA2AResponse } from "./formatter.js";
@@ -22,9 +22,6 @@ import { logger } from "./utils/logger.js";
22
22
  */
23
23
  export async function handleXYMessage(params) {
24
24
  const { cfg, runtime, message, accountId, webSocketSessionId } = params;
25
- const distributionSessionId = typeof message?.sessionId === "string" && message.sessionId.length > 0
26
- ? message.sessionId
27
- : undefined;
28
25
  // Cache context for CSPL steer injection (after_tool_call hook)
29
26
  setCsplSteerContext(cfg, runtime);
30
27
  // Get runtime (already validated in monitor.ts, but get reference for use)
@@ -38,8 +35,7 @@ export async function handleXYMessage(params) {
38
35
  if (!sessionId) {
39
36
  throw new Error("clearContext request missing sessionId in params");
40
37
  }
41
- const log = logger.withContext(sessionId, "");
42
- log.log(`[BOT] Clear context request`);
38
+ logger.log(`Clear context request for session ${sessionId}`);
43
39
  const config = resolveXYConfig(cfg);
44
40
  await sendClearContextResponse({
45
41
  config,
@@ -55,8 +51,7 @@ export async function handleXYMessage(params) {
55
51
  if (!sessionId) {
56
52
  throw new Error("tasks/cancel request missing sessionId in params");
57
53
  }
58
- const log = logger.withContext(sessionId, taskId);
59
- log.log(`[BOT] Tasks cancel request`);
54
+ logger.log(`Tasks cancel request for session ${sessionId}, task ${taskId}`);
60
55
  const config = resolveXYConfig(cfg);
61
56
  await sendTasksCancelResponse({
62
57
  config,
@@ -68,21 +63,22 @@ export async function handleXYMessage(params) {
68
63
  }
69
64
  // Parse the A2A message (for regular messages)
70
65
  const parsed = parseA2AMessage(message);
71
- // Scoped logger for this session — avoids concurrent session log mixing
72
- const log = logger.withContext(parsed.sessionId, parsed.taskId);
73
66
  // ========== 检测 Trigger 消息 ==========
74
67
  // 如果消息中包含 Trigger 事件数据,直接返回 pushData 内容,不走正常流程
75
68
  const triggerData = extractTriggerData(parsed.parts);
76
69
  if (triggerData) {
77
- log.log(`[BOT] Detected Trigger message, pushDataId=${triggerData.pushDataId}`);
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}`);
78
73
  try {
79
74
  // 读取 pushData
80
75
  const pushDataItem = await getPushDataById(triggerData.pushDataId);
81
76
  if (!pushDataItem) {
82
- log.error(`[BOT] pushData not found for ID: ${triggerData.pushDataId}`);
77
+ logger.error(`[BOT] pushData not found for ID: ${triggerData.pushDataId}`);
83
78
  return;
84
79
  }
85
- log.log(`[BOT] Found pushData, sending direct response, pushDataId=${pushDataItem.pushDataId}`);
80
+ logger.log(`[BOT] Found pushData, sending direct response`);
81
+ logger.log(`[BOT] - pushDataId: ${pushDataItem.pushDataId}`);
86
82
  const config = resolveXYConfig(cfg);
87
83
  // 直接发送响应(final=true,不走 openclaw 流程)
88
84
  await sendA2AResponse({
@@ -94,11 +90,11 @@ export async function handleXYMessage(params) {
94
90
  append: false,
95
91
  final: true,
96
92
  });
97
- log.log(`[BOT] Trigger response sent successfully`);
93
+ logger.log(`[BOT] Trigger response sent successfully, exiting early`);
98
94
  return; // 提前返回,不继续处理
99
95
  }
100
96
  catch (err) {
101
- log.error(`[BOT] Failed to handle Trigger message:`, err);
97
+ logger.error(`[BOT] Failed to handle Trigger message:`, err);
102
98
  return;
103
99
  }
104
100
  }
@@ -107,7 +103,9 @@ export async function handleXYMessage(params) {
107
103
  const isUpdate = hasActiveTask(parsed.sessionId);
108
104
  const skipReg = params.skipRegistration === true;
109
105
  if (isUpdate) {
110
- log.log(`[BOT] STEER MODE - Second message detected, new taskId=${parsed.taskId}`);
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}`);
111
109
  }
112
110
  // Steer injections skip taskId registration to avoid overwriting the active taskId
113
111
  if (!skipReg) {
@@ -115,30 +113,29 @@ export async function handleXYMessage(params) {
115
113
  // Extract and update push_id if present
116
114
  const pushId = extractPushId(parsed.parts);
117
115
  if (pushId) {
118
- log.log(`[BOT] Extracted push_id from user message`);
116
+ logger.log(`[BOT] 📌 Extracted push_id from user message`);
119
117
  configManager.updatePushId(parsed.sessionId, pushId);
120
118
  // 持久化 pushId 到本地文件(异步,不阻塞主流程)
121
119
  addPushId(pushId).catch((err) => {
122
- log.error(`[BOT] Failed to persist pushId:`, err);
120
+ logger.error(`[BOT] Failed to persist pushId:`, err);
123
121
  });
124
122
  }
125
123
  else {
126
- log.log(`[BOT] No push_id found in message, using config default`);
124
+ logger.log(`[BOT] ℹ️ No push_id found in message, will use config default`);
127
125
  }
128
126
  // 保存 runtime 信息到 .xiaoyiruntime 文件(异步,不阻塞主流程)
129
127
  saveRuntimeInfo(webSocketSessionId || parsed.sessionId, // SESSION_ID (WebSocket 层级,如果没有则 fallback)
130
128
  parsed.sessionId, // CONVERSATION_ID (param 里的 sessionId)
131
129
  parsed.taskId // TASK_ID (param.id)
132
130
  ).catch((err) => {
133
- log.error(`[BOT] Failed to save runtime info:`, err);
131
+ logger.error(`[BOT] Failed to save runtime info:`, err);
134
132
  });
135
133
  }
136
134
  // Extract deviceType if present (always parse — used in ctxPayload.MessageSid)
137
135
  const deviceType = extractDeviceType(parsed.parts);
138
136
  if (deviceType) {
139
- log.log(`[BOT] Extracted deviceType: ${deviceType}`);
137
+ logger.log(`[BOT] 📱 Extracted deviceType from user message: ${deviceType}`);
140
138
  }
141
- const runCrossTaskContext = extractRunCrossTaskContext(parsed.parts);
142
139
  // Resolve configuration (needed for status updates)
143
140
  const config = resolveXYConfig(cfg);
144
141
  // ✅ Resolve agent route (following feishu pattern)
@@ -153,21 +150,19 @@ export async function handleXYMessage(params) {
153
150
  id: parsed.sessionId, // ✅ Use sessionId to share context within the same conversation session
154
151
  },
155
152
  });
156
- log.log(`[BOT] Resolved route, sessionKey=${route.sessionKey}`);
153
+ logger.log(`xy: resolved route accountId=${route.accountId}, sessionKey=${route.sessionKey}`);
157
154
  // Steer injections skip session registration to avoid refCount leaks
158
155
  if (!skipReg) {
159
156
  registerSession(route.sessionKey, {
160
157
  config,
161
158
  sessionId: parsed.sessionId,
162
- distributionSessionId,
163
159
  taskId: parsed.taskId,
164
160
  messageId: parsed.messageId,
165
161
  agentId: route.accountId,
166
162
  deviceType,
167
- runCrossTaskContext: runCrossTaskContext ?? undefined,
168
163
  });
169
164
  // 🔑 发送初始状态更新
170
- log.log(`[BOT] Sending initial status update`);
165
+ logger.log(`[STATUS] Sending initial status update for session ${parsed.sessionId}`);
171
166
  void sendStatusUpdate({
172
167
  config,
173
168
  sessionId: parsed.sessionId,
@@ -176,7 +171,7 @@ export async function handleXYMessage(params) {
176
171
  text: "任务正在处理中,请稍候~",
177
172
  state: "working",
178
173
  }).catch((err) => {
179
- log.error(`Failed to send initial status update:`, err);
174
+ logger.error(`Failed to send initial status update:`, err);
180
175
  });
181
176
  }
182
177
  // Extract text and files from parts
@@ -188,18 +183,18 @@ export async function handleXYMessage(params) {
188
183
  const selfEvolutionEnabled = await selfEvolutionManager.isEnabled();
189
184
  if (selfEvolutionEnabled && shouldNudgeForSelfEvolutionKeyword(textForAgent)) {
190
185
  const shouldNudge = toolCallNudgeManager.tryMarkKeywordNudge(route.sessionKey);
191
- log.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}`);
192
187
  if (shouldNudge) {
193
188
  const augmented = appendSelfEvolutionKeywordNudge(textForAgent);
194
189
  textForAgent = augmented.text;
195
190
  if (augmented.appended) {
196
- log.log(`[SELF_EVOLUTION] Keyword-triggered inline nudge appended: sessionKey=${route.sessionKey}`);
191
+ logger.log(`[SELF_EVOLUTION] Keyword-triggered inline nudge appended: sessionKey=${route.sessionKey}`);
197
192
  }
198
193
  }
199
194
  }
200
195
  }
201
196
  catch (selfEvolutionError) {
202
- log.error(`[SELF_EVOLUTION] Failed to append inline keyword nudge: ${String(selfEvolutionError)}`);
197
+ logger.error(`[SELF_EVOLUTION] Failed to append inline keyword nudge: ${String(selfEvolutionError)}`);
203
198
  }
204
199
  }
205
200
  // 🔑 Steer消息: 跳过旧路径直接进入 streaming-signal 队列
@@ -214,11 +209,9 @@ export async function handleXYMessage(params) {
214
209
  const steerDownloadedFiles = await downloadFilesFromParts(steerFileParts);
215
210
  const steerMediaPayload = buildXYMediaPayload(steerDownloadedFiles);
216
211
  if (steerFileParts.length > 0) {
217
- log.log(`[BOT] Steer message with ${steerFileParts.length} file(s), enqueuing to streaming-signal queue`);
218
- }
219
- else {
220
- log.log(`[BOT] Steer message — enqueuing to streaming-signal queue`);
212
+ logger.log(`[BOT] 📎 Steer message with files: ${steerFileParts.length} file(s)`);
221
213
  }
214
+ logger.log(`[BOT] 🔄 Steer message — enqueuing to streaming-signal queue`);
222
215
  await enqueueSteer({
223
216
  sessionId: parsed.sessionId,
224
217
  sessionKey: route.sessionKey,
@@ -230,7 +223,8 @@ export async function handleXYMessage(params) {
230
223
  route,
231
224
  deviceType,
232
225
  });
233
- log.log(`[BOT] Steer queue completed`);
226
+ logger.log(`[BOT] Steer queue completed for session: ${parsed.sessionId}`);
227
+ logger.log(`xy: dispatch complete (session=${parsed.sessionId})`);
234
228
  return;
235
229
  }
236
230
  // ── First message (non-steer) path below ──────────────────────
@@ -242,7 +236,7 @@ export async function handleXYMessage(params) {
242
236
  if (!skipReg) {
243
237
  const fileParts = extractFileParts(parsed.parts);
244
238
  const downloadedFiles = await downloadFilesFromParts(fileParts);
245
- log.log(`[BOT] Downloaded ${downloadedFiles.length} file(s)`);
239
+ logger.log("Downloaded files:", JSON.stringify(downloadedFiles, null, 2));
246
240
  mediaPayload = buildXYMediaPayload(downloadedFiles);
247
241
  }
248
242
  // Resolve envelope format options (following feishu pattern)
@@ -288,7 +282,8 @@ export async function handleXYMessage(params) {
288
282
  // 🔑 Streaming 信号已在上方创建(在文件下载之前)
289
283
  const steerState = { steered: false };
290
284
  // 🔑 创建dispatcher
291
- log.log(`[BOT-DISPATCHER] Creating reply dispatcher`);
285
+ logger.log(`[BOT-DISPATCHER] 🎯 Creating reply dispatcher`);
286
+ logger.log(`[BOT-DISPATCHER] - taskId: ${parsed.taskId}`);
292
287
  const { dispatcher, replyOptions, markDispatchIdle, startStatusInterval } = createXYReplyDispatcher({
293
288
  cfg,
294
289
  runtime,
@@ -306,27 +301,26 @@ export async function handleXYMessage(params) {
306
301
  const sessionContext = {
307
302
  config,
308
303
  sessionId: parsed.sessionId,
309
- distributionSessionId,
310
304
  taskId: parsed.taskId,
311
305
  messageId: parsed.messageId,
312
306
  agentId: route.accountId,
313
307
  deviceType,
314
- runCrossTaskContext: runCrossTaskContext ?? undefined,
315
308
  };
316
- log.log(`[BOT-DISPATCH] withReplyDispatcher starting, sessionKey=${route.sessionKey}`);
309
+ logger.log(`[BOT-DISPATCH] withReplyDispatcher starting, sessionKey=${route.sessionKey}`);
317
310
  await core.channel.reply.withReplyDispatcher({
318
311
  dispatcher,
319
312
  onSettled: () => {
320
- log.log(`[BOT] onSettled, steered=${steerState.steered}`);
313
+ logger.log(`[BOT] 🏁 onSettled called for session: ${route.sessionKey}`);
314
+ logger.log(`[BOT] - steered: ${steerState.steered}`);
321
315
  // 🔑 When steered, skip heavy cleanup — the first message's dispatcher is still running
322
316
  if (steerState.steered) {
323
- log.log(`[BOT] Steered dispatch settled, skipping cleanup`);
317
+ logger.log(`[BOT] Steered dispatch settled (skipping cleanup)`);
324
318
  return;
325
319
  }
326
320
  streamingSignals.delete(parsed.sessionId);
327
321
  decrementTaskIdRef(parsed.sessionId);
328
322
  unregisterSession(route.sessionKey);
329
- log.log(`[BOT] Cleanup completed`);
323
+ logger.log(`[BOT] Cleanup completed`);
330
324
  },
331
325
  run: () => {
332
326
  // 🔐 Use AsyncLocalStorage to provide session context to tools.
@@ -335,7 +329,12 @@ export async function handleXYMessage(params) {
335
329
  // signal init complete to release the global dispatch gate
336
330
  // for the next session.
337
331
  const dispatchPromise = runWithSessionContext(sessionContext, async () => {
338
- log.log(`[BOT-DISPATCH] dispatchReplyFromConfig starting, body.length=${ctxPayload.Body?.length ?? 0}`);
332
+ logger.log(`[BOT-DISPATCH] dispatchReplyFromConfig starting...`);
333
+ logger.log(`[BOT-DISPATCH] - sessionKey: ${ctxPayload.SessionKey}`);
334
+ logger.log(`[BOT-DISPATCH] - provider: ${ctxPayload.Provider}`);
335
+ logger.log(`[BOT-DISPATCH] - surface: ${ctxPayload.Surface}`);
336
+ logger.log(`[BOT-DISPATCH] - from: ${ctxPayload.From}`);
337
+ logger.log(`[BOT-DISPATCH] - body length: ${ctxPayload.Body?.length ?? 0}`);
339
338
  try {
340
339
  const result = await core.channel.reply.dispatchReplyFromConfig({
341
340
  ctx: ctxPayload,
@@ -343,11 +342,15 @@ export async function handleXYMessage(params) {
343
342
  dispatcher,
344
343
  replyOptions,
345
344
  });
346
- log.log(`[BOT-DISPATCH] dispatchReplyFromConfig returned, result=${JSON.stringify(result)}`);
345
+ logger.log(`[BOT-DISPATCH] dispatchReplyFromConfig returned`);
346
+ logger.log(`[BOT-DISPATCH] - result: ${JSON.stringify(result)}`);
347
347
  return result;
348
348
  }
349
349
  catch (dispatchErr) {
350
- log.error(`[BOT-DISPATCH] dispatchReplyFromConfig threw: ${dispatchErr instanceof Error ? `${dispatchErr.name}: ${dispatchErr.message}` : String(dispatchErr)}`, dispatchErr instanceof Error ? dispatchErr.stack?.slice(0, 500) : undefined);
350
+ logger.error(`[BOT-DISPATCH] dispatchReplyFromConfig threw`);
351
+ logger.error(`[BOT-DISPATCH] - error name: ${dispatchErr instanceof Error ? dispatchErr.name : "unknown"}`);
352
+ logger.error(`[BOT-DISPATCH] - error message: ${String(dispatchErr)}`);
353
+ logger.error(`[BOT-DISPATCH] - error stack: ${dispatchErr instanceof Error ? dispatchErr.stack?.slice(0, 500) : "N/A"}`);
351
354
  throw dispatchErr;
352
355
  }
353
356
  });
@@ -356,23 +359,20 @@ export async function handleXYMessage(params) {
356
359
  return dispatchPromise;
357
360
  },
358
361
  });
359
- log.log(`[BOT] Dispatcher completed`);
362
+ logger.log(`[BOT] Dispatcher completed for session: ${parsed.sessionId}`);
363
+ logger.log(`xy: dispatch complete (session=${parsed.sessionId})`);
360
364
  }
361
365
  catch (err) {
362
366
  // ✅ Only log error, don't re-throw to prevent gateway restart
363
- // Note: if error occurs before parseA2AMessage, `log` may not be defined yet
364
- const errSessionId = message.params?.sessionId || "";
365
- const errTaskId = message.params?.id || message.id || "";
366
- const errLog = logger.withContext(errSessionId, errTaskId);
367
- errLog.error("Failed to handle XY message:", err);
367
+ logger.error("Failed to handle XY message:", err);
368
368
  runtime.error?.(`xy: Failed to handle message: ${String(err)}`);
369
- errLog.log(`[BOT] Error occurred, attempting cleanup`);
369
+ logger.log(`[BOT] Error occurred, attempting cleanup...`);
370
370
  // 🔑 错误时也要清理taskId和session
371
371
  try {
372
372
  const params = message.params;
373
373
  const sessionId = params?.sessionId;
374
374
  if (sessionId) {
375
- errLog.log(`[BOT] Cleaning up after error`);
375
+ logger.log(`[BOT] 🧹 Cleaning up after error: ${sessionId}`);
376
376
  // 清理 taskId
377
377
  decrementTaskIdRef(sessionId);
378
378
  // 清理 session
@@ -387,11 +387,11 @@ export async function handleXYMessage(params) {
387
387
  },
388
388
  });
389
389
  unregisterSession(route.sessionKey);
390
- errLog.log(`[BOT] Cleanup completed after error`);
390
+ logger.log(`[BOT] Cleanup completed after error`);
391
391
  }
392
392
  }
393
393
  catch (cleanupErr) {
394
- errLog.log(`[BOT] Cleanup failed:`, cleanupErr);
394
+ logger.log(`[BOT] ⚠️ Cleanup failed:`, cleanupErr);
395
395
  // Ignore cleanup errors
396
396
  }
397
397
  // ❌ Don't re-throw: message processing error should not affect gateway stability
@@ -428,22 +428,20 @@ const steerQueues = _g.__xySteerQueues;
428
428
  * 这是模型 API 被调用的精确时刻,此时 isStreaming 一定为 true。
429
429
  */
430
430
  export function notifyModelStreaming(sessionId) {
431
- const log = logger.withContext(sessionId, "");
432
431
  const signal = streamingSignals.get(sessionId);
433
432
  if (signal) {
434
433
  // 不删除 signal——后续 steer 需要靠它判断模型已在 streaming。
435
434
  // 清理由第一条消息的 onSettled 兜底。
436
435
  signal.notify();
437
- log.log(`[STEER-QUEUE] Model streaming signal fired`);
436
+ logger.log(`[STEER-QUEUE] 📡 Model streaming signal fired for session=${sessionId}`);
438
437
  }
439
438
  }
440
439
  function createStreamingSignal(sessionId) {
441
- const log = logger.withContext(sessionId, "");
442
440
  let resolve;
443
441
  const promise = new Promise(r => { resolve = r; });
444
442
  const signal = { promise, notify: resolve };
445
443
  streamingSignals.set(sessionId, signal);
446
- log.log(`[STEER-QUEUE] Streaming signal created`);
444
+ logger.log(`[STEER-QUEUE] 🟢 Streaming signal created for session ${sessionId}`);
447
445
  return signal;
448
446
  }
449
447
  /**
@@ -453,14 +451,13 @@ function createStreamingSignal(sessionId) {
453
451
  */
454
452
  function enqueueSteer(params) {
455
453
  const { sessionId } = params;
456
- const log = logger.withContext(sessionId, params.parsed.taskId);
457
454
  // 取出当前队列尾部(或 undefined),然后链上新的 Promise
458
455
  const prev = steerQueues.get(sessionId);
459
456
  const next = (prev ?? Promise.resolve()).then(() => dispatchSteerWhenReady(params));
460
457
  steerQueues.set(sessionId, next);
461
458
  // 链条结束后清理
462
459
  next.catch((err) => {
463
- log.error(`[STEER-QUEUE] Steer chain failed: ${String(err)}`);
460
+ logger.error(`[STEER-QUEUE] Steer chain failed: ${String(err)}`);
464
461
  }).finally(() => {
465
462
  if (steerQueues.get(sessionId) === next) {
466
463
  steerQueues.delete(sessionId);
@@ -470,38 +467,37 @@ function enqueueSteer(params) {
470
467
  }
471
468
  async function dispatchSteerWhenReady(params) {
472
469
  const { sessionId, sessionKey, steerText } = params;
473
- const log = logger.withContext(sessionId, params.parsed.taskId);
474
470
  // 1. 等待第一条消息开始 streaming
475
471
  // signal 可能尚未创建(第一条消息还在文件下载等耗时操作中),
476
472
  // 轮询等待直到 signal 出现,最長等待 ~5 秒。
477
473
  let signal = streamingSignals.get(sessionId);
478
474
  if (!signal) {
479
- log.log(`[STEER-QUEUE] Signal not yet created, polling`);
475
+ logger.log(`[STEER-QUEUE] Signal not yet created, polling for session=${sessionId}`);
480
476
  for (let i = 0; i < 50; i++) {
481
477
  await new Promise(r => setTimeout(r, 100));
482
478
  signal = streamingSignals.get(sessionId);
483
479
  if (signal)
484
480
  break;
485
481
  if (!hasActiveTask(sessionId)) {
486
- log.log(`[STEER-QUEUE] First message completed while waiting, skip steer`);
482
+ logger.log(`[STEER-QUEUE] ℹ️ First message completed while waiting, skip steer`);
487
483
  return;
488
484
  }
489
485
  }
490
486
  }
491
487
  if (signal) {
492
- log.log(`[STEER-QUEUE] Waiting for streaming signal`);
488
+ logger.log(`[STEER-QUEUE] Waiting for streaming signal, session=${sessionId}`);
493
489
  await signal.promise;
494
- log.log(`[STEER-QUEUE] Streaming signal received`);
490
+ logger.log(`[STEER-QUEUE] Streaming signal received, session=${sessionId}`);
495
491
  }
496
492
  else {
497
493
  // 轮询超时且 hasActiveTask 仍为 true——说明第一条消息可能卡在异常路径,
498
494
  // 没有创建 signal。此时 dispatch 会与第一条消息的模型调用并发冲突,放弃。
499
- log.log(`[STEER-QUEUE] Signal never appeared after polling, skip steer to avoid collision`);
495
+ logger.log(`[STEER-QUEUE] ⚠️ Signal never appeared after polling, skip steer to avoid collision`);
500
496
  return;
501
497
  }
502
498
  // 2. 第一条消息已结束 → 放弃
503
499
  if (!hasActiveTask(sessionId)) {
504
- log.log(`[STEER-QUEUE] First message completed, skip steer`);
500
+ logger.log(`[STEER-QUEUE] ℹ️ First message completed, skip steer`);
505
501
  return;
506
502
  }
507
503
  // 3. 构建 dispatch 上下文并 dispatch /steer
@@ -563,11 +559,11 @@ async function dispatchSteerWhenReady(params) {
563
559
  agentId: params.route.accountId,
564
560
  deviceType: params.deviceType,
565
561
  };
566
- log.log(`[STEER-QUEUE] Dispatching steer`);
562
+ logger.log(`[STEER-QUEUE] 🚀 Dispatching steer for session=${sessionId}`);
567
563
  await core.channel.reply.withReplyDispatcher({
568
564
  dispatcher,
569
565
  onSettled: () => {
570
- log.log(`[STEER-QUEUE] Steer dispatch settled`);
566
+ logger.log(`[STEER-QUEUE] 🏁 Steer dispatch settled for session=${sessionId}`);
571
567
  },
572
568
  run: () => {
573
569
  return runWithSessionContext(sessionContext, async () => {
@@ -577,10 +573,10 @@ async function dispatchSteerWhenReady(params) {
577
573
  dispatcher,
578
574
  replyOptions,
579
575
  });
580
- log.log(`[STEER-QUEUE] dispatch result: ${JSON.stringify(result)}`);
576
+ logger.log(`[STEER-QUEUE] dispatch result: ${JSON.stringify(result)}`);
581
577
  return result;
582
578
  });
583
579
  },
584
580
  });
585
- log.log(`[STEER-QUEUE] Steer dispatch completed`);
581
+ logger.log(`[STEER-QUEUE] Steer dispatch completed for session=${sessionId}`);
586
582
  }
@@ -2,15 +2,9 @@ import { resolveXYConfig, listXYAccountIds, getDefaultXYAccountId } from "./conf
2
2
  import { xyConfigSchema } from "./config-schema.js";
3
3
  import { xyOutbound } from "./outbound.js";
4
4
  import { filterToolsByDevice } from "./tools/device-tool-map.js";
5
- import { getCurrentSessionContext, registerSession } from "./tools/session-manager.js";
5
+ import { getCurrentSessionContext } from "./tools/session-manager.js";
6
6
  import { createAllTools } from "./tools/create-all-tools.js";
7
7
  import { logger } from "./utils/logger.js";
8
- /**
9
- * Prefix used for synthetic sessionIds created during cron-triggered tool
10
- * execution. `sendCommand()` checks this prefix to route commands through
11
- * the push channel instead of the (non-existent) WebSocket session.
12
- */
13
- const CRON_SESSION_PREFIX = "cron-";
14
8
  /**
15
9
  * Xiaoyi Channel Plugin for OpenClaw.
16
10
  * Implements Xiaoyi A2A protocol with dual WebSocket connections.
@@ -49,59 +43,11 @@ export const xyPlugin = {
49
43
  schema: xyConfigSchema,
50
44
  },
51
45
  outbound: xyOutbound,
52
- /**
53
- * Provide channel-specific agent tools.
54
- *
55
- * Two execution contexts are supported:
56
- *
57
- * 1. **Normal (WebSocket) session** – `getCurrentSessionContext()` returns
58
- * a context that was registered by bot.ts during message processing.
59
- * Tools send commands through the WebSocket and listen for responses.
60
- *
61
- * 2. **Cron / scheduled-task session** – openclaw's cron runner calls
62
- * `agentTools({ cfg })` without an active WebSocket session. When no
63
- * session context exists but `cfg` is provided, we create a synthetic
64
- * "cron session" with `isCron: true` and a `cron-`-prefixed sessionId.
65
- * `sendCommand()` detects this prefix and routes commands through the
66
- * push channel. Response listening (WebSocket events) works unchanged
67
- * because the gateway WebSocket connection is always active.
68
- */
69
- agentTools: (params) => {
70
- let ctx = getCurrentSessionContext();
71
- // ── Cron / non-session fallback ──────────────────────────────
72
- // When no active xy WebSocket session exists but the openclaw cfg
73
- // is provided (framework calls agentTools({ cfg })), create a
74
- // synthetic "cron session". This enables cron-triggered agent
75
- // turns and cross-channel tool calls to use xiaoyi tools via the
76
- // push channel. sendCommand() detects the "cron-" sessionId
77
- // prefix and routes commands through push instead of WebSocket.
78
- if (!ctx && params?.cfg) {
79
- try {
80
- const config = resolveXYConfig(params.cfg);
81
- const cronId = `${CRON_SESSION_PREFIX}${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
82
- ctx = {
83
- config,
84
- sessionId: cronId,
85
- taskId: cronId,
86
- messageId: cronId,
87
- agentId: "default",
88
- isCron: true,
89
- };
90
- // Register so getCurrentSessionContext() fallback can find it
91
- registerSession(`__cron__${cronId}`, ctx);
92
- logger.log(`[CRON-TOOLS] Created cron session context: ${cronId}`);
93
- }
94
- catch (err) {
95
- logger.error("[CRON-TOOLS] Failed to create cron context:", err);
96
- }
97
- }
98
- if (!ctx) {
99
- logger.log("[CREATE-ALL-TOOLS] no session context, returning empty tools list");
100
- return [];
101
- }
46
+ agentTools: () => {
47
+ const ctx = getCurrentSessionContext();
102
48
  const allTools = createAllTools(ctx);
103
- const filtered = filterToolsByDevice(allTools, ctx.deviceType);
104
- logger.log(`[DEVICE-FILTER] deviceType=${ctx.deviceType ?? "(none)"}, tools: ${allTools.length} → ${filtered.length} (${filtered.map(t => t.name).join(", ")})`);
49
+ const filtered = filterToolsByDevice(allTools, ctx?.deviceType);
50
+ logger.log(`[DEVICE-FILTER] deviceType=${ctx?.deviceType ?? "(none)"}, tools: ${allTools.length} → ${filtered.length} (${filtered.map(t => t.name).join(", ")})`);
105
51
  return filtered;
106
52
  },
107
53
  messaging: {