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