@ynhcj/xiaoyi-channel 0.0.130-beta โ†’ 0.0.130-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 (74) hide show
  1. package/dist/index.js +36 -7
  2. package/dist/src/bot.d.ts +8 -0
  3. package/dist/src/bot.js +151 -139
  4. package/dist/src/client.d.ts +1 -5
  5. package/dist/src/client.js +25 -39
  6. package/dist/src/cspl/call-api.d.ts +6 -0
  7. package/dist/src/cspl/call-api.js +33 -13
  8. package/dist/src/cspl/config.d.ts +11 -1
  9. package/dist/src/cspl/config.js +30 -0
  10. package/dist/src/cspl/middleware.d.ts +8 -0
  11. package/dist/src/cspl/middleware.js +90 -0
  12. package/dist/src/cspl/steer-context.d.ts +21 -0
  13. package/dist/src/cspl/steer-context.js +78 -0
  14. package/dist/src/file-download.js +3 -3
  15. package/dist/src/formatter.d.ts +0 -2
  16. package/dist/src/formatter.js +12 -14
  17. package/dist/src/heartbeat.js +3 -2
  18. package/dist/src/login-token-handler.js +13 -10
  19. package/dist/src/message-queue.js +2 -1
  20. package/dist/src/monitor.js +55 -46
  21. package/dist/src/outbound.js +3 -0
  22. package/dist/src/provider.js +84 -44
  23. package/dist/src/push.js +9 -9
  24. package/dist/src/reply-dispatcher.d.ts +3 -1
  25. package/dist/src/reply-dispatcher.js +61 -68
  26. package/dist/src/self-evolution-handler.js +11 -14
  27. package/dist/src/skill-retriever/hooks.js +0 -1
  28. package/dist/src/skill-retriever/tool-search.js +7 -12
  29. package/dist/src/task-manager.d.ts +4 -27
  30. package/dist/src/task-manager.js +13 -78
  31. package/dist/src/tools/calendar-tool.js +5 -1
  32. package/dist/src/tools/call-phone-tool.js +5 -1
  33. package/dist/src/tools/create-alarm-tool.js +5 -1
  34. package/dist/src/tools/delete-alarm-tool.js +5 -1
  35. package/dist/src/tools/location-tool.js +5 -1
  36. package/dist/src/tools/login-token-tool.js +13 -2
  37. package/dist/src/tools/modify-alarm-tool.js +5 -1
  38. package/dist/src/tools/modify-note-tool.js +5 -1
  39. package/dist/src/tools/note-tool.js +5 -1
  40. package/dist/src/tools/query-app-message-tool.js +5 -1
  41. package/dist/src/tools/query-memory-data-tool.js +5 -1
  42. package/dist/src/tools/query-todo-task-tool.js +5 -1
  43. package/dist/src/tools/save-file-to-phone-tool.js +5 -1
  44. package/dist/src/tools/save-media-to-gallery-tool.js +5 -1
  45. package/dist/src/tools/search-alarm-tool.js +5 -1
  46. package/dist/src/tools/search-calendar-tool.js +5 -1
  47. package/dist/src/tools/search-contact-tool.js +5 -1
  48. package/dist/src/tools/search-email-tool.js +5 -1
  49. package/dist/src/tools/search-file-tool.js +5 -1
  50. package/dist/src/tools/search-message-tool.js +5 -1
  51. package/dist/src/tools/search-note-tool.js +5 -1
  52. package/dist/src/tools/search-photo-gallery-tool.js +5 -1
  53. package/dist/src/tools/send-email-tool.js +5 -1
  54. package/dist/src/tools/send-file-to-user-tool.js +0 -1
  55. package/dist/src/tools/send-message-tool.js +5 -1
  56. package/dist/src/tools/session-helper.d.ts +24 -0
  57. package/dist/src/tools/session-helper.js +45 -0
  58. package/dist/src/tools/session-manager.d.ts +8 -0
  59. package/dist/src/tools/session-manager.js +3 -15
  60. package/dist/src/tools/upload-file-tool.js +5 -1
  61. package/dist/src/tools/upload-photo-tool.js +5 -1
  62. package/dist/src/tools/xiaoyi-add-collection-tool.js +5 -1
  63. package/dist/src/tools/xiaoyi-collection-tool.js +5 -1
  64. package/dist/src/tools/xiaoyi-delete-collection-tool.js +5 -1
  65. package/dist/src/tools/xiaoyi-gui-tool.js +3 -1
  66. package/dist/src/trigger-handler.js +8 -9
  67. package/dist/src/utils/logger.js +106 -22
  68. package/dist/src/utils/throw.d.ts +5 -0
  69. package/dist/src/utils/throw.js +10 -0
  70. package/dist/src/websocket.js +4 -2
  71. package/dist/src/xy-session-store.d.ts +79 -0
  72. package/dist/src/xy-session-store.js +153 -0
  73. package/openclaw.plugin.json +3 -0
  74. package/package.json +6 -5
package/dist/index.js CHANGED
@@ -1,11 +1,14 @@
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 { callCsplApi } from "./src/cspl/call-api.js";
4
+ import { callCsplApiWithConfig } from "./src/cspl/call-api.js";
5
+ import { getCsplConfig, initCsplConfigFromXYConfig } from "./src/cspl/config.js";
5
6
  import { ALLOWED_TOOLS, MAX_TEXT_LENGTH, MAX_TOTAL_LENGTH, MIN_TEXT_LENGTH, STEER_ABORT_MESSAGE, } from "./src/cspl/constants.js";
6
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";
7
11
  import { setXYRuntime } from "./src/runtime.js";
8
- import { tryInjectSteer } from "./src/steer-injector.js";
9
12
  import { registerSelfEvolutionToolResultNudge } from "./src/self-evolution-tool-result-nudge.js";
10
13
  import { createBeforePromptBuildHandler } from "./src/skill-retriever/hooks.js";
11
14
  import { normalizeToolRetrieverConfig } from "./src/skill-retriever/config.js";
@@ -22,17 +25,25 @@ function registerFullHooks(api) {
22
25
  const beforePromptBuildHandler = createBeforePromptBuildHandler(skillRetrieverConfig);
23
26
  api.on("before_prompt_build", beforePromptBuildHandler);
24
27
  registerSelfEvolutionToolResultNudge(api);
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.
25
36
  api.on("after_tool_call", async (event, ctx) => {
26
37
  if (!ALLOWED_TOOLS.includes(event.toolName)) {
27
38
  return;
28
39
  }
29
- console.log(`[SENTINEL HOOK] after_tool_call triggered: toolName=${event.toolName}, sessionKey=${ctx.sessionKey ?? "none"}`);
30
40
  try {
31
41
  const resultText = extractResultText(event, event.toolName);
32
42
  const resultLength = resultText.length;
33
43
  if (resultLength <= MIN_TEXT_LENGTH || resultLength > MAX_TOTAL_LENGTH) {
34
44
  return;
35
45
  }
46
+ logger.log(`[SENTINEL HOOK] after_tool_call: toolName=${event.toolName}, textLength=${resultLength}`);
36
47
  const questionText = {
37
48
  subSceneID: "TOOL_OUTPUT",
38
49
  tool: event.toolName,
@@ -47,15 +58,32 @@ function registerFullHooks(api) {
47
58
  questionText.output[0].content = trimmed;
48
59
  finalJson = JSON.stringify(questionText);
49
60
  }
50
- const response = await callCsplApi(finalJson, api.config);
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;
51
68
  const result = parseSecurityResult(response);
52
- console.log(`[SENTINEL HOOK] Security result: status=${result.status}`);
69
+ logger.log(`[SENTINEL HOOK] Security result: status=${result.status}, toolName=${event.toolName}, elapsed=${csplElapsed}ms`);
53
70
  if (result.status === "REJECT") {
54
- await tryInjectSteer(ctx.sessionKey, STEER_ABORT_MESSAGE);
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
+ }
55
83
  }
56
84
  }
57
85
  catch (err) {
58
- api.logger.error(`[SENTINEL HOOK] after_tool_call error: ${err}`);
86
+ logger.error(`[SENTINEL HOOK] after_tool_call error: ${err}`);
59
87
  }
60
88
  });
61
89
  }
@@ -82,6 +110,7 @@ export default definePluginEntry({
82
110
  }
83
111
  if (api.registrationMode === "full") {
84
112
  registerFullHooks(api);
113
+ registerCsplHook(api);
85
114
  }
86
115
  },
87
116
  });
package/dist/src/bot.d.ts CHANGED
@@ -9,6 +9,14 @@ export interface HandleXYMessageParams {
9
9
  message: A2AJsonRpcRequest;
10
10
  accountId: string;
11
11
  webSocketSessionId?: string;
12
+ /** Called after dispatch init is complete (agentTools/wrapStreamFn done). */
13
+ onInitComplete?: () => void;
14
+ /**
15
+ * When true, skip taskId/session registration. Used by tryInjectSteer to
16
+ * inject a steer message without overwriting the active taskId or leaking
17
+ * session refCount.
18
+ */
19
+ skipRegistration?: boolean;
12
20
  }
13
21
  /**
14
22
  * Handle an incoming A2A message.
package/dist/src/bot.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { getXYRuntime } from "./runtime.js";
2
- import { setCachedContext } from "./steer-injector.js";
3
2
  import { createXYReplyDispatcher } from "./reply-dispatcher.js";
4
3
  import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId, extractDeviceType, extractTriggerData } from "./parser.js";
5
4
  import { downloadFilesFromParts } from "./file-download.js";
@@ -13,7 +12,9 @@ import { getPushDataById } from "./utils/pushdata-manager.js";
13
12
  import { selfEvolutionManager } from "./utils/self-evolution-manager.js";
14
13
  import { saveRuntimeInfo } from "./utils/runtime-manager.js";
15
14
  import { toolCallNudgeManager } from "./utils/tool-call-nudge-manager.js";
16
- import { registerTaskId, decrementTaskIdRef, lockTaskId, unlockTaskId, hasActiveTask, } from "./task-manager.js";
15
+ import { setCsplSteerContext } from "./cspl/steer-context.js";
16
+ import { registerTaskId, decrementTaskIdRef, 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,10 +22,8 @@ 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
- // ๆฏๆฌกๆ”ถๅˆฐๆถˆๆฏๆ—ถๆ›ดๆ–ฐ็ผ“ๅญ˜๏ผŒไพ› steer ๆณจๅ…ฅไฝฟ็”จ
27
- setCachedContext(cfg, runtime, accountId);
25
+ // Cache context for CSPL steer injection (after_tool_call hook)
26
+ setCsplSteerContext(cfg, runtime);
28
27
  // Get runtime (already validated in monitor.ts, but get reference for use)
29
28
  const core = getXYRuntime();
30
29
  try {
@@ -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,58 +89,53 @@ 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
  }
103
101
  // ========================================
104
- // ๐Ÿ”‘ ๆฃ€ๆต‹steerๆจกๅผๅ’Œๆ˜ฏๅฆๆ˜ฏ็ฌฌไบŒๆกๆถˆๆฏ
105
- const isSteerMode = cfg.messages?.queue?.mode === "steer";
106
- const isSecondMessage = isSteerMode && hasActiveTask(parsed.sessionId);
107
- 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)`);
102
+ // ๐Ÿ”‘ ๆณจๅ†ŒtaskId๏ผˆๆฃ€ๆต‹ๆ˜ฏๅฆๆ˜ฏๅทฒๆœ‰ๆดป่ทƒไปปๅŠก็š„ session๏ผ‰
103
+ const isUpdate = hasActiveTask(parsed.sessionId);
104
+ const skipReg = params.skipRegistration === true;
105
+ 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}`);
111
109
  }
112
- // ๐Ÿ”‘ ๆณจๅ†ŒtaskId๏ผˆ็ฌฌไบŒๆกๆถˆๆฏไผš่ฆ†็›–็ฌฌไธ€ๆก็š„taskId๏ผ‰
113
- const { isUpdate, refCount } = registerTaskId(parsed.sessionId, parsed.taskId, parsed.messageId, { incrementRef: true } // ๅขžๅŠ ๅผ•็”จ่ฎกๆ•ฐ
114
- );
115
- // ๐Ÿ”‘ ๅฆ‚ๆžœๆ˜ฏ็ฌฌไธ€ๆกๆถˆๆฏ๏ผŒ้”ๅฎštaskId้˜ฒๆญข่ขซ่ฟ‡ๆ—ฉๆธ…็†
116
- if (!isUpdate) {
117
- lockTaskId(parsed.sessionId);
118
- log(`[BOT] ๐Ÿ”’ Locked taskId for first message`);
119
- }
120
- // Extract and update push_id if present
121
- const pushId = extractPushId(parsed.parts);
122
- if (pushId) {
123
- log(`[BOT] ๐Ÿ“Œ Extracted push_id from user message`);
124
- configManager.updatePushId(parsed.sessionId, pushId);
125
- // ๆŒไน…ๅŒ– pushId ๅˆฐๆœฌๅœฐๆ–‡ไปถ๏ผˆๅผ‚ๆญฅ๏ผŒไธ้˜ปๅกžไธปๆต็จ‹๏ผ‰
126
- addPushId(pushId).catch((err) => {
127
- error(`[BOT] Failed to persist pushId:`, err);
110
+ // Steer injections skip taskId registration to avoid overwriting the active taskId
111
+ if (!skipReg) {
112
+ registerTaskId(parsed.sessionId, parsed.taskId, parsed.messageId);
113
+ // Extract and update push_id if present
114
+ const pushId = extractPushId(parsed.parts);
115
+ if (pushId) {
116
+ logger.log(`[BOT] ๐Ÿ“Œ Extracted push_id from user message`);
117
+ configManager.updatePushId(parsed.sessionId, pushId);
118
+ // ๆŒไน…ๅŒ– pushId ๅˆฐๆœฌๅœฐๆ–‡ไปถ๏ผˆๅผ‚ๆญฅ๏ผŒไธ้˜ปๅกžไธปๆต็จ‹๏ผ‰
119
+ addPushId(pushId).catch((err) => {
120
+ logger.error(`[BOT] Failed to persist pushId:`, err);
121
+ });
122
+ }
123
+ else {
124
+ logger.log(`[BOT] โ„น๏ธ No push_id found in message, will use config default`);
125
+ }
126
+ // ไฟๅญ˜ runtime ไฟกๆฏๅˆฐ .xiaoyiruntime ๆ–‡ไปถ๏ผˆๅผ‚ๆญฅ๏ผŒไธ้˜ปๅกžไธปๆต็จ‹๏ผ‰
127
+ saveRuntimeInfo(webSocketSessionId || parsed.sessionId, // SESSION_ID (WebSocket ๅฑ‚็บง๏ผŒๅฆ‚ๆžœๆฒกๆœ‰ๅˆ™ fallback)
128
+ parsed.sessionId, // CONVERSATION_ID (param ้‡Œ็š„ sessionId)
129
+ parsed.taskId // TASK_ID (param.id)
130
+ ).catch((err) => {
131
+ logger.error(`[BOT] Failed to save runtime info:`, err);
128
132
  });
129
133
  }
130
- else {
131
- log(`[BOT] โ„น๏ธ No push_id found in message, will use config default`);
132
- }
133
- // Extract deviceType if present (same level as push_id in systemVariables)
134
+ // Extract deviceType if present (always parse โ€” used in ctxPayload.MessageSid)
134
135
  const deviceType = extractDeviceType(parsed.parts);
135
136
  if (deviceType) {
136
- log(`[BOT] ๐Ÿ“ฑ Extracted deviceType from user message: ${deviceType}`);
137
+ logger.log(`[BOT] ๐Ÿ“ฑ Extracted deviceType from user message: ${deviceType}`);
137
138
  }
138
- // ไฟๅญ˜ runtime ไฟกๆฏๅˆฐ .xiaoyiruntime ๆ–‡ไปถ๏ผˆๅผ‚ๆญฅ๏ผŒไธ้˜ปๅกžไธปๆต็จ‹๏ผ‰
139
- saveRuntimeInfo(webSocketSessionId || parsed.sessionId, // SESSION_ID (WebSocket ๅฑ‚็บง๏ผŒๅฆ‚ๆžœๆฒกๆœ‰ๅˆ™ fallback)
140
- parsed.sessionId, // CONVERSATION_ID (param ้‡Œ็š„ sessionId)
141
- parsed.taskId // TASK_ID (param.id)
142
- ).catch((err) => {
143
- error(`[BOT] Failed to save runtime info:`, err);
144
- });
145
139
  // Resolve configuration (needed for status updates)
146
140
  const config = resolveXYConfig(cfg);
147
141
  // โœ… Resolve agent route (following feishu pattern)
@@ -156,55 +150,66 @@ export async function handleXYMessage(params) {
156
150
  id: parsed.sessionId, // โœ… Use sessionId to share context within the same conversation session
157
151
  },
158
152
  });
159
- log(`xy: resolved route accountId=${route.accountId}, sessionKey=${route.sessionKey}`);
160
- registerSession(route.sessionKey, {
161
- config,
162
- sessionId: parsed.sessionId,
163
- taskId: parsed.taskId,
164
- messageId: parsed.messageId,
165
- agentId: route.accountId,
166
- deviceType,
167
- });
168
- // ๐Ÿ”‘ ๅ‘้€ๅˆๅง‹็Šถๆ€ๆ›ดๆ–ฐ๏ผˆ็ฌฌไบŒๆกๆถˆๆฏไนŸ่ฆๅ‘๏ผŒ็”จๆ–ฐtaskId๏ผ‰
169
- log(`[STATUS] Sending initial status update for session ${parsed.sessionId}`);
170
- void sendStatusUpdate({
171
- config,
172
- sessionId: parsed.sessionId,
173
- taskId: parsed.taskId,
174
- messageId: parsed.messageId,
175
- text: isSecondMessage ? "ๆ–ฐๆถˆๆฏๅทฒๆŽฅๆ”ถ๏ผŒๆญฃๅœจๅค„็†..." : "ไปปๅŠกๆญฃๅœจๅค„็†ไธญ๏ผŒ่ฏท็จๅ€™~",
176
- state: "working",
177
- runtime,
178
- }).catch((err) => {
179
- error(`Failed to send initial status update:`, err);
180
- });
153
+ logger.log(`xy: resolved route accountId=${route.accountId}, sessionKey=${route.sessionKey}`);
154
+ // Steer injections skip session registration to avoid refCount leaks
155
+ if (!skipReg) {
156
+ registerSession(route.sessionKey, {
157
+ config,
158
+ sessionId: parsed.sessionId,
159
+ taskId: parsed.taskId,
160
+ messageId: parsed.messageId,
161
+ agentId: route.accountId,
162
+ deviceType,
163
+ });
164
+ // ๐Ÿ”‘ ๅ‘้€ๅˆๅง‹็Šถๆ€ๆ›ดๆ–ฐ
165
+ logger.log(`[STATUS] Sending initial status update for session ${parsed.sessionId}`);
166
+ void sendStatusUpdate({
167
+ config,
168
+ sessionId: parsed.sessionId,
169
+ taskId: parsed.taskId,
170
+ messageId: parsed.messageId,
171
+ text: "ไปปๅŠกๆญฃๅœจๅค„็†ไธญ๏ผŒ่ฏท็จๅ€™~",
172
+ state: "working",
173
+ }).catch((err) => {
174
+ logger.error(`Failed to send initial status update:`, err);
175
+ });
176
+ }
181
177
  // Extract text and files from parts
182
178
  const text = extractTextFromParts(parsed.parts);
183
179
  let textForAgent = text || "";
184
- if (route.sessionKey && textForAgent) {
180
+ // Self-evolution keyword nudge โ€” only for real user messages, not steer injections
181
+ if (!skipReg && route.sessionKey && textForAgent) {
185
182
  try {
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
- const fileParts = extractFileParts(parsed.parts);
204
- // Download files to local disk
205
- const downloadedFiles = await downloadFilesFromParts(fileParts);
206
- log("Downloaded files:", JSON.stringify(downloadedFiles, null, 2));
207
- const mediaPayload = buildXYMediaPayload(downloadedFiles);
200
+ // ๐Ÿ”‘ SteerๆถˆๆฏๅŠ  /steer ๅ‰็ผ€๏ผŒ่งฆๅ‘core็š„ queueEmbeddedPiMessage
201
+ if (isUpdate && textForAgent) {
202
+ textForAgent = `/steer ${textForAgent}`;
203
+ logger.log(`[BOT] ๐Ÿ”„ Prepended /steer for steer injection`);
204
+ }
205
+ // File download โ€” only for real user messages, steer injections have no files
206
+ let mediaPayload = {};
207
+ if (!skipReg) {
208
+ const fileParts = extractFileParts(parsed.parts);
209
+ const downloadedFiles = await downloadFilesFromParts(fileParts);
210
+ logger.log("Downloaded files:", JSON.stringify(downloadedFiles, null, 2));
211
+ mediaPayload = buildXYMediaPayload(downloadedFiles);
212
+ }
208
213
  // Resolve envelope format options (following feishu pattern)
209
214
  const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
210
215
  // Build message body with speaker prefix (following feishu pattern)
@@ -236,7 +241,7 @@ export async function handleXYMessage(params) {
236
241
  SenderId: parsed.sessionId,
237
242
  Provider: "xiaoyi-channel",
238
243
  Surface: "xiaoyi-channel",
239
- MessageSid: parsed.messageId,
244
+ MessageSid: `${parsed.taskId}_${deviceType}`,
240
245
  Timestamp: Date.now(),
241
246
  WasMentioned: false,
242
247
  CommandAuthorized: true,
@@ -245,9 +250,13 @@ export async function handleXYMessage(params) {
245
250
  ReplyToBody: undefined, // A2A protocol doesn't support reply/quote
246
251
  ...mediaPayload,
247
252
  });
248
- // ๐Ÿ”‘ ๅˆ›ๅปบdispatcher๏ผˆdispatcherไผš่‡ชๅŠจไฝฟ็”จๅŠจๆ€taskId๏ผ‰
249
- log(`[BOT-DISPATCHER] ๐ŸŽฏ Creating reply dispatcher`);
250
- log(`[BOT-DISPATCHER] - taskId: ${parsed.taskId}`);
253
+ // ๐Ÿ”‘ Dynamic steer state: when isUpdate (second message), start as steered=true
254
+ // so the dispatcher skips all user-facing callbacks (deliver, onIdle, etc.)
255
+ // and onSettled skips cleanup.
256
+ const steerState = { steered: isUpdate };
257
+ // ๐Ÿ”‘ ๅˆ›ๅปบdispatcher
258
+ logger.log(`[BOT-DISPATCHER] ๐ŸŽฏ Creating reply dispatcher`);
259
+ logger.log(`[BOT-DISPATCHER] - taskId: ${parsed.taskId}`);
251
260
  const { dispatcher, replyOptions, markDispatchIdle, startStatusInterval } = createXYReplyDispatcher({
252
261
  cfg,
253
262
  runtime,
@@ -255,13 +264,11 @@ export async function handleXYMessage(params) {
255
264
  taskId: parsed.taskId,
256
265
  messageId: parsed.messageId,
257
266
  accountId: route.accountId,
258
- isSteerFollower: isSecondMessage, // ๐Ÿ”‘ ๆ ‡่ฎฐ็ฌฌไบŒๆกๆถˆๆฏ
267
+ steerState,
259
268
  });
260
- // ๐Ÿ”‘ ๅชๆœ‰็ฌฌไธ€ๆกๆถˆๆฏๅฏๅŠจ็Šถๆ€ๅฎšๆ—ถๅ™จ
261
- // ็ฌฌไบŒๆกๆถˆๆฏไผšๅพˆๅฟซ่ฟ”ๅ›ž๏ผŒไธ้œ€่ฆๅฎšๆ—ถๅ™จ
262
- if (!isSecondMessage) {
269
+ // Steer injections don't need status intervals
270
+ if (!skipReg) {
263
271
  startStatusInterval();
264
- log(`[BOT-DISPATCHER] โœ… Status interval started for first message`);
265
272
  }
266
273
  // Build session context for AsyncLocalStorage
267
274
  const sessionContext = {
@@ -272,69 +279,74 @@ export async function handleXYMessage(params) {
272
279
  agentId: route.accountId,
273
280
  deviceType,
274
281
  };
275
- log(`[BOT-DISPATCH] โณ withReplyDispatcher starting, sessionKey=${route.sessionKey}`);
282
+ logger.log(`[BOT-DISPATCH] โณ withReplyDispatcher starting, sessionKey=${route.sessionKey}`);
276
283
  await core.channel.reply.withReplyDispatcher({
277
284
  dispatcher,
278
285
  onSettled: () => {
279
- log(`[BOT] ๐Ÿ onSettled called for session: ${route.sessionKey}`);
280
- log(`[BOT] - isSecondMessage: ${isSecondMessage}`);
281
- // ๐Ÿ”‘ ๅ‡ๅฐ‘ๅผ•็”จ่ฎกๆ•ฐ
282
- decrementTaskIdRef(parsed.sessionId);
283
- // ๐Ÿ”‘ ๅฆ‚ๆžœๆ˜ฏ็ฌฌไธ€ๆกๆถˆๆฏๅฎŒๆˆ๏ผŒ่งฃ้”
284
- if (!isSecondMessage) {
285
- unlockTaskId(parsed.sessionId);
286
- log(`[BOT] ๐Ÿ”“ Unlocked taskId (first message completed)`);
286
+ logger.log(`[BOT] ๐Ÿ onSettled called for session: ${route.sessionKey}`);
287
+ logger.log(`[BOT] - steered: ${steerState.steered}`);
288
+ // ๐Ÿ”‘ When steered, skip heavy cleanup โ€” the first message's dispatcher is still running
289
+ if (steerState.steered) {
290
+ logger.log(`[BOT] โœ… Steered dispatch settled (skipping cleanup)`);
291
+ return;
287
292
  }
288
- // ๅ‡ๅฐ‘sessionๅผ•็”จ่ฎกๆ•ฐ
293
+ decrementTaskIdRef(parsed.sessionId);
289
294
  unregisterSession(route.sessionKey);
290
- log(`[BOT] โœ… Cleanup completed`);
295
+ logger.log(`[BOT] โœ… Cleanup completed`);
296
+ },
297
+ run: () => {
298
+ // ๐Ÿ” Use AsyncLocalStorage to provide session context to tools.
299
+ // runWithSessionContext returns after the sync part of dispatch
300
+ // (including agentTools + wrapStreamFn) has executed, so we
301
+ // signal init complete to release the global dispatch gate
302
+ // for the next session.
303
+ const dispatchPromise = runWithSessionContext(sessionContext, async () => {
304
+ logger.log(`[BOT-DISPATCH] โณ dispatchReplyFromConfig starting...`);
305
+ logger.log(`[BOT-DISPATCH] - sessionKey: ${ctxPayload.SessionKey}`);
306
+ logger.log(`[BOT-DISPATCH] - provider: ${ctxPayload.Provider}`);
307
+ logger.log(`[BOT-DISPATCH] - surface: ${ctxPayload.Surface}`);
308
+ logger.log(`[BOT-DISPATCH] - from: ${ctxPayload.From}`);
309
+ logger.log(`[BOT-DISPATCH] - body length: ${ctxPayload.Body?.length ?? 0}`);
310
+ try {
311
+ const result = await core.channel.reply.dispatchReplyFromConfig({
312
+ ctx: ctxPayload,
313
+ cfg,
314
+ dispatcher,
315
+ replyOptions,
316
+ });
317
+ logger.log(`[BOT-DISPATCH] โœ… dispatchReplyFromConfig returned`);
318
+ logger.log(`[BOT-DISPATCH] - result: ${JSON.stringify(result)}`);
319
+ return result;
320
+ }
321
+ catch (dispatchErr) {
322
+ logger.error(`[BOT-DISPATCH] โŒ dispatchReplyFromConfig threw`);
323
+ logger.error(`[BOT-DISPATCH] - error name: ${dispatchErr instanceof Error ? dispatchErr.name : "unknown"}`);
324
+ logger.error(`[BOT-DISPATCH] - error message: ${String(dispatchErr)}`);
325
+ logger.error(`[BOT-DISPATCH] - error stack: ${dispatchErr instanceof Error ? dispatchErr.stack?.slice(0, 500) : "N/A"}`);
326
+ throw dispatchErr;
327
+ }
328
+ });
329
+ // Signal init complete โ€” sync part (agentTools, wrapStreamFn) is done
330
+ params.onInitComplete?.();
331
+ return dispatchPromise;
291
332
  },
292
- run: () =>
293
- // ๐Ÿ” Use AsyncLocalStorage to provide session context to tools
294
- runWithSessionContext(sessionContext, async () => {
295
- log(`[BOT-DISPATCH] โณ dispatchReplyFromConfig starting...`);
296
- log(`[BOT-DISPATCH] - sessionKey: ${ctxPayload.SessionKey}`);
297
- log(`[BOT-DISPATCH] - provider: ${ctxPayload.Provider}`);
298
- log(`[BOT-DISPATCH] - surface: ${ctxPayload.Surface}`);
299
- log(`[BOT-DISPATCH] - from: ${ctxPayload.From}`);
300
- log(`[BOT-DISPATCH] - body length: ${ctxPayload.Body?.length ?? 0}`);
301
- try {
302
- const result = await core.channel.reply.dispatchReplyFromConfig({
303
- ctx: ctxPayload,
304
- cfg,
305
- dispatcher,
306
- replyOptions,
307
- });
308
- log(`[BOT-DISPATCH] โœ… dispatchReplyFromConfig returned`);
309
- log(`[BOT-DISPATCH] - result: ${JSON.stringify(result)}`);
310
- return result;
311
- }
312
- catch (dispatchErr) {
313
- error(`[BOT-DISPATCH] โŒ dispatchReplyFromConfig threw`);
314
- error(`[BOT-DISPATCH] - error name: ${dispatchErr instanceof Error ? dispatchErr.name : "unknown"}`);
315
- error(`[BOT-DISPATCH] - error message: ${String(dispatchErr)}`);
316
- error(`[BOT-DISPATCH] - error stack: ${dispatchErr instanceof Error ? dispatchErr.stack?.slice(0, 500) : "N/A"}`);
317
- throw dispatchErr;
318
- }
319
- }),
320
333
  });
321
- log(`[BOT] โœ… Dispatcher completed for session: ${parsed.sessionId}`);
322
- log(`xy: dispatch complete (session=${parsed.sessionId})`);
334
+ logger.log(`[BOT] โœ… Dispatcher completed for session: ${parsed.sessionId}`);
335
+ logger.log(`xy: dispatch complete (session=${parsed.sessionId})`);
323
336
  }
324
337
  catch (err) {
325
338
  // โœ… Only log error, don't re-throw to prevent gateway restart
326
- error("Failed to handle XY message:", err);
339
+ logger.error("Failed to handle XY message:", err);
327
340
  runtime.error?.(`xy: Failed to handle message: ${String(err)}`);
328
- log(`[BOT] โŒ Error occurred, attempting cleanup...`);
341
+ logger.log(`[BOT] โŒ Error occurred, attempting cleanup...`);
329
342
  // ๐Ÿ”‘ ้”™่ฏฏๆ—ถไนŸ่ฆๆธ…็†taskIdๅ’Œsession
330
343
  try {
331
344
  const params = message.params;
332
345
  const sessionId = params?.sessionId;
333
346
  if (sessionId) {
334
- log(`[BOT] ๐Ÿงน Cleaning up after error: ${sessionId}`);
347
+ logger.log(`[BOT] ๐Ÿงน Cleaning up after error: ${sessionId}`);
335
348
  // ๆธ…็† taskId
336
349
  decrementTaskIdRef(sessionId);
337
- unlockTaskId(sessionId);
338
350
  // ๆธ…็† session
339
351
  const core = getXYRuntime();
340
352
  const route = core.channel.routing.resolveAgentRoute({
@@ -347,11 +359,11 @@ export async function handleXYMessage(params) {
347
359
  },
348
360
  });
349
361
  unregisterSession(route.sessionKey);
350
- log(`[BOT] โœ… Cleanup completed after error`);
362
+ logger.log(`[BOT] โœ… Cleanup completed after error`);
351
363
  }
352
364
  }
353
365
  catch (cleanupErr) {
354
- log(`[BOT] โš ๏ธ Cleanup failed:`, cleanupErr);
366
+ logger.log(`[BOT] โš ๏ธ Cleanup failed:`, cleanupErr);
355
367
  // Ignore cleanup errors
356
368
  }
357
369
  // โŒ 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.