@ynhcj/xiaoyi-channel 0.0.122-beta → 0.0.122-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 (135) hide show
  1. package/dist/index.d.ts +3 -6
  2. package/dist/index.js +90 -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 +2 -0
  6. package/dist/src/bot.js +104 -103
  7. package/dist/src/channel.js +2 -17
  8. package/dist/src/client.d.ts +1 -5
  9. package/dist/src/client.js +32 -37
  10. package/dist/src/cspl/call-api.d.ts +6 -0
  11. package/dist/src/cspl/call-api.js +37 -16
  12. package/dist/src/cspl/config.d.ts +11 -1
  13. package/dist/src/cspl/config.js +30 -0
  14. package/dist/src/cspl/middleware.d.ts +8 -0
  15. package/dist/src/cspl/middleware.js +90 -0
  16. package/dist/src/cspl/steer-context.d.ts +12 -0
  17. package/dist/src/cspl/steer-context.js +52 -0
  18. package/dist/src/file-download.js +4 -3
  19. package/dist/src/file-upload.js +19 -18
  20. package/dist/src/formatter.js +32 -44
  21. package/dist/src/heartbeat.js +4 -3
  22. package/dist/src/login-token-handler.js +13 -10
  23. package/dist/src/message-queue.js +2 -1
  24. package/dist/src/monitor.js +62 -41
  25. package/dist/src/outbound.js +22 -18
  26. package/dist/src/provider.js +86 -30
  27. package/dist/src/push.js +16 -15
  28. package/dist/src/reply-dispatcher.d.ts +3 -1
  29. package/dist/src/reply-dispatcher.js +64 -62
  30. package/dist/src/self-evolution-handler.js +11 -14
  31. package/dist/src/skill-retriever/hooks.js +4 -4
  32. package/dist/src/skill-retriever/tool-search.js +13 -17
  33. package/dist/src/steer-injector.js +1 -1
  34. package/dist/src/task-manager.d.ts +4 -27
  35. package/dist/src/task-manager.js +19 -79
  36. package/dist/src/tools/calendar-tool.d.ts +2 -1
  37. package/dist/src/tools/calendar-tool.js +116 -116
  38. package/dist/src/tools/call-device-tool.d.ts +2 -1
  39. package/dist/src/tools/call-device-tool.js +126 -103
  40. package/dist/src/tools/call-phone-tool.d.ts +2 -1
  41. package/dist/src/tools/call-phone-tool.js +113 -113
  42. package/dist/src/tools/create-alarm-tool.d.ts +2 -1
  43. package/dist/src/tools/create-alarm-tool.js +231 -231
  44. package/dist/src/tools/create-all-tools.d.ts +16 -0
  45. package/dist/src/tools/create-all-tools.js +50 -0
  46. package/dist/src/tools/delete-alarm-tool.d.ts +2 -1
  47. package/dist/src/tools/delete-alarm-tool.js +135 -135
  48. package/dist/src/tools/get-alarm-tool-schema.d.ts +2 -1
  49. package/dist/src/tools/get-alarm-tool-schema.js +16 -10
  50. package/dist/src/tools/get-calendar-tool-schema.d.ts +2 -1
  51. package/dist/src/tools/get-calendar-tool-schema.js +12 -8
  52. package/dist/src/tools/get-collection-tool-schema.d.ts +2 -1
  53. package/dist/src/tools/get-collection-tool-schema.js +11 -9
  54. package/dist/src/tools/get-contact-tool-schema.d.ts +2 -1
  55. package/dist/src/tools/get-contact-tool-schema.js +16 -10
  56. package/dist/src/tools/get-device-file-tool-schema.d.ts +2 -1
  57. package/dist/src/tools/get-device-file-tool-schema.js +13 -9
  58. package/dist/src/tools/get-email-tool-schema.d.ts +2 -1
  59. package/dist/src/tools/get-email-tool-schema.js +11 -8
  60. package/dist/src/tools/get-note-tool-schema.d.ts +2 -1
  61. package/dist/src/tools/get-note-tool-schema.js +14 -9
  62. package/dist/src/tools/get-photo-tool-schema.d.ts +2 -1
  63. package/dist/src/tools/get-photo-tool-schema.js +12 -9
  64. package/dist/src/tools/image-reading-tool.d.ts +3 -2
  65. package/dist/src/tools/image-reading-tool.js +82 -162
  66. package/dist/src/tools/location-tool.d.ts +2 -1
  67. package/dist/src/tools/location-tool.js +91 -91
  68. package/dist/src/tools/login-token-tool.d.ts +2 -1
  69. package/dist/src/tools/login-token-tool.js +124 -116
  70. package/dist/src/tools/modify-alarm-tool.d.ts +2 -1
  71. package/dist/src/tools/modify-alarm-tool.js +236 -236
  72. package/dist/src/tools/modify-note-tool.d.ts +2 -1
  73. package/dist/src/tools/modify-note-tool.js +108 -108
  74. package/dist/src/tools/note-tool.d.ts +2 -1
  75. package/dist/src/tools/note-tool.js +107 -107
  76. package/dist/src/tools/query-app-message-tool.d.ts +2 -1
  77. package/dist/src/tools/query-app-message-tool.js +112 -111
  78. package/dist/src/tools/query-memory-data-tool.d.ts +2 -1
  79. package/dist/src/tools/query-memory-data-tool.js +113 -112
  80. package/dist/src/tools/query-todo-task-tool.d.ts +2 -1
  81. package/dist/src/tools/query-todo-task-tool.js +107 -106
  82. package/dist/src/tools/save-file-to-phone-tool.d.ts +2 -1
  83. package/dist/src/tools/save-file-to-phone-tool.js +131 -131
  84. package/dist/src/tools/save-media-to-gallery-tool.d.ts +2 -1
  85. package/dist/src/tools/save-media-to-gallery-tool.js +138 -138
  86. package/dist/src/tools/save-self-evolution-skill-tool.d.ts +2 -1
  87. package/dist/src/tools/save-self-evolution-skill-tool.js +194 -196
  88. package/dist/src/tools/search-alarm-tool.d.ts +2 -1
  89. package/dist/src/tools/search-alarm-tool.js +175 -175
  90. package/dist/src/tools/search-calendar-tool.d.ts +2 -1
  91. package/dist/src/tools/search-calendar-tool.js +149 -149
  92. package/dist/src/tools/search-contact-tool.d.ts +2 -1
  93. package/dist/src/tools/search-contact-tool.js +102 -102
  94. package/dist/src/tools/search-email-tool.d.ts +2 -1
  95. package/dist/src/tools/search-email-tool.js +111 -111
  96. package/dist/src/tools/search-file-tool.d.ts +2 -1
  97. package/dist/src/tools/search-file-tool.js +103 -103
  98. package/dist/src/tools/search-message-tool.d.ts +2 -1
  99. package/dist/src/tools/search-message-tool.js +104 -104
  100. package/dist/src/tools/search-note-tool.d.ts +2 -1
  101. package/dist/src/tools/search-note-tool.js +99 -99
  102. package/dist/src/tools/search-photo-gallery-tool.d.ts +2 -1
  103. package/dist/src/tools/search-photo-gallery-tool.js +38 -38
  104. package/dist/src/tools/send-email-tool.d.ts +2 -1
  105. package/dist/src/tools/send-email-tool.js +109 -108
  106. package/dist/src/tools/send-file-to-user-tool.d.ts +2 -1
  107. package/dist/src/tools/send-file-to-user-tool.js +157 -155
  108. package/dist/src/tools/send-message-tool.d.ts +2 -1
  109. package/dist/src/tools/send-message-tool.js +123 -123
  110. package/dist/src/tools/session-helper.d.ts +24 -0
  111. package/dist/src/tools/session-helper.js +45 -0
  112. package/dist/src/tools/session-manager.d.ts +29 -6
  113. package/dist/src/tools/session-manager.js +134 -19
  114. package/dist/src/tools/upload-file-tool.d.ts +2 -1
  115. package/dist/src/tools/upload-file-tool.js +82 -82
  116. package/dist/src/tools/upload-photo-tool.d.ts +2 -1
  117. package/dist/src/tools/upload-photo-tool.js +73 -73
  118. package/dist/src/tools/xiaoyi-add-collection-tool.d.ts +2 -1
  119. package/dist/src/tools/xiaoyi-add-collection-tool.js +147 -147
  120. package/dist/src/tools/xiaoyi-collection-tool.d.ts +2 -1
  121. package/dist/src/tools/xiaoyi-collection-tool.js +115 -115
  122. package/dist/src/tools/xiaoyi-delete-collection-tool.d.ts +2 -1
  123. package/dist/src/tools/xiaoyi-delete-collection-tool.js +128 -128
  124. package/dist/src/tools/xiaoyi-gui-tool.d.ts +2 -1
  125. package/dist/src/tools/xiaoyi-gui-tool.js +89 -88
  126. package/dist/src/trigger-handler.js +8 -9
  127. package/dist/src/utils/logger.js +105 -19
  128. package/dist/src/utils/self-evolution-manager.js +3 -2
  129. package/dist/src/utils/throw.d.ts +5 -0
  130. package/dist/src/utils/throw.js +10 -0
  131. package/dist/src/websocket.js +35 -31
  132. package/dist/src/xy-session-store.d.ts +79 -0
  133. package/dist/src/xy-session-store.js +153 -0
  134. package/openclaw.plugin.json +4 -0
  135. 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,105 @@
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 { injectCsplSteer } 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
+ // CSPL security scanning via after_tool_call hook.
29
+ // When CSPL returns REJECT, injects a steer message (with /steer prefix)
30
+ // into the active Pi run to interrupt the agent.
31
+ api.on("after_tool_call", async (event, ctx) => {
32
+ if (!ALLOWED_TOOLS.includes(event.toolName)) {
33
+ return;
34
+ }
35
+ try {
36
+ const resultText = extractResultText(event, event.toolName);
37
+ const resultLength = resultText.length;
38
+ if (resultLength <= MIN_TEXT_LENGTH || resultLength > MAX_TOTAL_LENGTH) {
34
39
  return;
35
40
  }
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);
41
+ logger.log(`[SENTINEL HOOK] after_tool_call: toolName=${event.toolName}, textLength=${resultLength}`);
42
+ const questionText = {
43
+ subSceneID: "TOOL_OUTPUT",
44
+ tool: event.toolName,
45
+ output: [{ content: "" }],
46
+ };
47
+ const originText = processText(resultText);
48
+ questionText.output[0].content = originText;
49
+ let finalJson = JSON.stringify(questionText);
50
+ if (finalJson.length > MAX_TEXT_LENGTH) {
51
+ const diff = finalJson.length - MAX_TEXT_LENGTH;
52
+ const { text: trimmed } = validateAndTruncateText(originText, MAX_TEXT_LENGTH - diff);
53
+ questionText.output[0].content = trimmed;
54
+ finalJson = JSON.stringify(questionText);
55
+ }
56
+ const sessionCtx = getSessionContext(ctx.sessionKey ?? "");
57
+ const csplConfig = sessionCtx
58
+ ? initCsplConfigFromXYConfig(sessionCtx.config)
59
+ : getCsplConfig();
60
+ const csplStartTime = Date.now();
61
+ const response = await callCsplApiWithConfig(finalJson, csplConfig);
62
+ const csplElapsed = Date.now() - csplStartTime;
63
+ const result = parseSecurityResult(response);
64
+ logger.log(`[SENTINEL HOOK] Security result: status=${result.status}, toolName=${event.toolName}, elapsed=${csplElapsed}ms`);
65
+ if (result.status === "REJECT") {
66
+ logger.log(`[SENTINEL HOOK] REJECT - injecting steer message`);
67
+ if (sessionCtx) {
68
+ await injectCsplSteer(sessionCtx.sessionId, sessionCtx.taskId, STEER_ABORT_MESSAGE);
56
69
  }
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);
70
+ else {
71
+ logger.error("[SENTINEL HOOK] No session context, cannot inject steer");
62
72
  }
63
73
  }
64
- catch (err) {
65
- api.logger.error(`[SENTINEL HOOK] after_tool_call error: ${err}`);
66
- }
67
- });
74
+ }
75
+ catch (err) {
76
+ logger.error(`[SENTINEL HOOK] after_tool_call error: ${err}`);
77
+ }
78
+ });
79
+ }
80
+ export default definePluginEntry({
81
+ id: "xiaoyi-channel",
82
+ name: "Xiaoyi Channel",
83
+ description: "Xiaoyi channel plugin - Xiaoyi A2A protocol integration",
84
+ register(api) {
85
+ // Always register the provider so wrapStreamFn/prepareExtraParams work
86
+ // in ALL registration modes (not just "full").
87
+ api.registerProvider(xiaoyiProvider);
88
+ if (api.registrationMode === "cli-metadata") {
89
+ return;
90
+ }
91
+ if (api.registrationMode === "tool-discovery") {
92
+ registerFullHooks(api);
93
+ return;
94
+ }
95
+ // Register channel plugin and set runtime
96
+ api.registerChannel({ plugin: xyPlugin });
97
+ setXYRuntime(api.runtime);
98
+ if (api.registrationMode === "discovery") {
99
+ return;
100
+ }
101
+ if (api.registrationMode === "full") {
102
+ registerFullHooks(api);
103
+ }
68
104
  },
69
105
  });
@@ -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,8 @@ 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;
12
14
  }
13
15
  /**
14
16
  * 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, accountId);
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,47 @@ 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)`);
110
- }
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`);
102
+ // 🔑 注册taskId(检测是否是已有活跃任务的 session)
103
+ const isUpdate = hasActiveTask(parsed.sessionId);
104
+ if (isUpdate) {
105
+ logger.log(`[BOT] 🔄 STEER MODE - Second message detected (core will handle steer)`);
106
+ logger.log(`[BOT] - Session: ${parsed.sessionId}`);
107
+ logger.log(`[BOT] - New taskId: ${parsed.taskId}`);
118
108
  }
109
+ registerTaskId(parsed.sessionId, parsed.taskId, parsed.messageId);
119
110
  // Extract and update push_id if present
120
111
  const pushId = extractPushId(parsed.parts);
121
112
  if (pushId) {
122
- log(`[BOT] 📌 Extracted push_id from user message`);
113
+ logger.log(`[BOT] 📌 Extracted push_id from user message`);
123
114
  configManager.updatePushId(parsed.sessionId, pushId);
124
115
  // 持久化 pushId 到本地文件(异步,不阻塞主流程)
125
116
  addPushId(pushId).catch((err) => {
126
- error(`[BOT] Failed to persist pushId:`, err);
117
+ logger.error(`[BOT] Failed to persist pushId:`, err);
127
118
  });
128
119
  }
129
120
  else {
130
- log(`[BOT] ℹ️ No push_id found in message, will use config default`);
121
+ logger.log(`[BOT] ℹ️ No push_id found in message, will use config default`);
131
122
  }
132
123
  // Extract deviceType if present (same level as push_id in systemVariables)
133
124
  const deviceType = extractDeviceType(parsed.parts);
134
125
  if (deviceType) {
135
- log(`[BOT] 📱 Extracted deviceType from user message: ${deviceType}`);
126
+ logger.log(`[BOT] 📱 Extracted deviceType from user message: ${deviceType}`);
136
127
  }
137
128
  // 保存 runtime 信息到 .xiaoyiruntime 文件(异步,不阻塞主流程)
138
129
  saveRuntimeInfo(webSocketSessionId || parsed.sessionId, // SESSION_ID (WebSocket 层级,如果没有则 fallback)
139
130
  parsed.sessionId, // CONVERSATION_ID (param 里的 sessionId)
140
131
  parsed.taskId // TASK_ID (param.id)
141
132
  ).catch((err) => {
142
- error(`[BOT] Failed to save runtime info:`, err);
133
+ logger.error(`[BOT] Failed to save runtime info:`, err);
143
134
  });
144
135
  // Resolve configuration (needed for status updates)
145
136
  const config = resolveXYConfig(cfg);
@@ -155,25 +146,26 @@ export async function handleXYMessage(params) {
155
146
  id: parsed.sessionId, // ✅ Use sessionId to share context within the same conversation session
156
147
  },
157
148
  });
158
- log(`xy: resolved route accountId=${route.accountId}, sessionKey=${route.sessionKey}`);
149
+ logger.log(`xy: resolved route accountId=${route.accountId}, sessionKey=${route.sessionKey}`);
159
150
  registerSession(route.sessionKey, {
160
151
  config,
161
152
  sessionId: parsed.sessionId,
162
153
  taskId: parsed.taskId,
163
154
  messageId: parsed.messageId,
164
155
  agentId: route.accountId,
156
+ deviceType,
165
157
  });
166
- // 🔑 发送初始状态更新(第二条消息也要发,用新taskId)
167
- log(`[STATUS] Sending initial status update for session ${parsed.sessionId}`);
158
+ // 🔑 发送初始状态更新
159
+ logger.log(`[STATUS] Sending initial status update for session ${parsed.sessionId}`);
168
160
  void sendStatusUpdate({
169
161
  config,
170
162
  sessionId: parsed.sessionId,
171
163
  taskId: parsed.taskId,
172
164
  messageId: parsed.messageId,
173
- text: isSecondMessage ? "新消息已接收,正在处理..." : "任务正在处理中,请稍候~",
165
+ text: "任务正在处理中,请稍候~",
174
166
  state: "working",
175
167
  }).catch((err) => {
176
- error(`Failed to send initial status update:`, err);
168
+ logger.error(`Failed to send initial status update:`, err);
177
169
  });
178
170
  // Extract text and files from parts
179
171
  const text = extractTextFromParts(parsed.parts);
@@ -183,24 +175,29 @@ export async function handleXYMessage(params) {
183
175
  const selfEvolutionEnabled = await selfEvolutionManager.isEnabled();
184
176
  if (selfEvolutionEnabled && shouldNudgeForSelfEvolutionKeyword(textForAgent)) {
185
177
  const shouldNudge = toolCallNudgeManager.tryMarkKeywordNudge(route.sessionKey);
186
- log(`[SELF_EVOLUTION] Keyword check hit during inbound build: sessionKey=${route.sessionKey}, shouldNudge=${shouldNudge}`);
178
+ logger.log(`[SELF_EVOLUTION] Keyword check hit during inbound build: sessionKey=${route.sessionKey}, shouldNudge=${shouldNudge}`);
187
179
  if (shouldNudge) {
188
180
  const augmented = appendSelfEvolutionKeywordNudge(textForAgent);
189
181
  textForAgent = augmented.text;
190
182
  if (augmented.appended) {
191
- log(`[SELF_EVOLUTION] Keyword-triggered inline nudge appended: sessionKey=${route.sessionKey}`);
183
+ logger.log(`[SELF_EVOLUTION] Keyword-triggered inline nudge appended: sessionKey=${route.sessionKey}`);
192
184
  }
193
185
  }
194
186
  }
195
187
  }
196
188
  catch (selfEvolutionError) {
197
- error(`[SELF_EVOLUTION] Failed to append inline keyword nudge: ${String(selfEvolutionError)}`);
189
+ logger.error(`[SELF_EVOLUTION] Failed to append inline keyword nudge: ${String(selfEvolutionError)}`);
198
190
  }
199
191
  }
192
+ // 🔑 Steer消息加 /steer 前缀,触发core的 queueEmbeddedPiMessage
193
+ if (isUpdate && textForAgent) {
194
+ textForAgent = `/steer ${textForAgent}`;
195
+ logger.log(`[BOT] 🔄 Prepended /steer for steer injection`);
196
+ }
200
197
  const fileParts = extractFileParts(parsed.parts);
201
198
  // Download files to local disk
202
199
  const downloadedFiles = await downloadFilesFromParts(fileParts);
203
- console.log("Downloaded files:", JSON.stringify(downloadedFiles, null, 2));
200
+ logger.log("Downloaded files:", JSON.stringify(downloadedFiles, null, 2));
204
201
  const mediaPayload = buildXYMediaPayload(downloadedFiles);
205
202
  // Resolve envelope format options (following feishu pattern)
206
203
  const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
@@ -233,7 +230,7 @@ export async function handleXYMessage(params) {
233
230
  SenderId: parsed.sessionId,
234
231
  Provider: "xiaoyi-channel",
235
232
  Surface: "xiaoyi-channel",
236
- MessageSid: parsed.messageId,
233
+ MessageSid: `${parsed.taskId}_${deviceType}`,
237
234
  Timestamp: Date.now(),
238
235
  WasMentioned: false,
239
236
  CommandAuthorized: true,
@@ -242,9 +239,13 @@ export async function handleXYMessage(params) {
242
239
  ReplyToBody: undefined, // A2A protocol doesn't support reply/quote
243
240
  ...mediaPayload,
244
241
  });
245
- // 🔑 创建dispatcher(dispatcher会自动使用动态taskId)
246
- log(`[BOT-DISPATCHER] 🎯 Creating reply dispatcher`);
247
- log(`[BOT-DISPATCHER] - taskId: ${parsed.taskId}`);
242
+ // 🔑 Dynamic steer state: when isUpdate (second message), start as steered=true
243
+ // so the dispatcher skips all user-facing callbacks (deliver, onIdle, etc.)
244
+ // and onSettled skips cleanup.
245
+ const steerState = { steered: isUpdate };
246
+ // 🔑 创建dispatcher
247
+ logger.log(`[BOT-DISPATCHER] 🎯 Creating reply dispatcher`);
248
+ logger.log(`[BOT-DISPATCHER] - taskId: ${parsed.taskId}`);
248
249
  const { dispatcher, replyOptions, markDispatchIdle, startStatusInterval } = createXYReplyDispatcher({
249
250
  cfg,
250
251
  runtime,
@@ -252,14 +253,9 @@ export async function handleXYMessage(params) {
252
253
  taskId: parsed.taskId,
253
254
  messageId: parsed.messageId,
254
255
  accountId: route.accountId,
255
- isSteerFollower: isSecondMessage, // 🔑 标记第二条消息
256
+ steerState,
256
257
  });
257
- // 🔑 只有第一条消息启动状态定时器
258
- // 第二条消息会很快返回,不需要定时器
259
- if (!isSecondMessage) {
260
- startStatusInterval();
261
- log(`[BOT-DISPATCHER] ✅ Status interval started for first message`);
262
- }
258
+ startStatusInterval();
263
259
  // Build session context for AsyncLocalStorage
264
260
  const sessionContext = {
265
261
  config,
@@ -269,69 +265,74 @@ export async function handleXYMessage(params) {
269
265
  agentId: route.accountId,
270
266
  deviceType,
271
267
  };
272
- log(`[BOT-DISPATCH] ⏳ withReplyDispatcher starting, sessionKey=${route.sessionKey}`);
268
+ logger.log(`[BOT-DISPATCH] ⏳ withReplyDispatcher starting, sessionKey=${route.sessionKey}`);
273
269
  await core.channel.reply.withReplyDispatcher({
274
270
  dispatcher,
275
271
  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)`);
272
+ logger.log(`[BOT] 🏁 onSettled called for session: ${route.sessionKey}`);
273
+ logger.log(`[BOT] - steered: ${steerState.steered}`);
274
+ // 🔑 When steered, skip heavy cleanup — the first message's dispatcher is still running
275
+ if (steerState.steered) {
276
+ logger.log(`[BOT] Steered dispatch settled (skipping cleanup)`);
277
+ return;
284
278
  }
285
- // 减少session引用计数
279
+ decrementTaskIdRef(parsed.sessionId);
286
280
  unregisterSession(route.sessionKey);
287
- log(`[BOT] ✅ Cleanup completed`);
281
+ logger.log(`[BOT] ✅ Cleanup completed`);
282
+ },
283
+ run: () => {
284
+ // 🔐 Use AsyncLocalStorage to provide session context to tools.
285
+ // runWithSessionContext returns after the sync part of dispatch
286
+ // (including agentTools + wrapStreamFn) has executed, so we
287
+ // signal init complete to release the global dispatch gate
288
+ // for the next session.
289
+ const dispatchPromise = runWithSessionContext(sessionContext, async () => {
290
+ logger.log(`[BOT-DISPATCH] ⏳ dispatchReplyFromConfig starting...`);
291
+ logger.log(`[BOT-DISPATCH] - sessionKey: ${ctxPayload.SessionKey}`);
292
+ logger.log(`[BOT-DISPATCH] - provider: ${ctxPayload.Provider}`);
293
+ logger.log(`[BOT-DISPATCH] - surface: ${ctxPayload.Surface}`);
294
+ logger.log(`[BOT-DISPATCH] - from: ${ctxPayload.From}`);
295
+ logger.log(`[BOT-DISPATCH] - body length: ${ctxPayload.Body?.length ?? 0}`);
296
+ try {
297
+ const result = await core.channel.reply.dispatchReplyFromConfig({
298
+ ctx: ctxPayload,
299
+ cfg,
300
+ dispatcher,
301
+ replyOptions,
302
+ });
303
+ logger.log(`[BOT-DISPATCH] ✅ dispatchReplyFromConfig returned`);
304
+ logger.log(`[BOT-DISPATCH] - result: ${JSON.stringify(result)}`);
305
+ return result;
306
+ }
307
+ catch (dispatchErr) {
308
+ logger.error(`[BOT-DISPATCH] ❌ dispatchReplyFromConfig threw`);
309
+ logger.error(`[BOT-DISPATCH] - error name: ${dispatchErr instanceof Error ? dispatchErr.name : "unknown"}`);
310
+ logger.error(`[BOT-DISPATCH] - error message: ${String(dispatchErr)}`);
311
+ logger.error(`[BOT-DISPATCH] - error stack: ${dispatchErr instanceof Error ? dispatchErr.stack?.slice(0, 500) : "N/A"}`);
312
+ throw dispatchErr;
313
+ }
314
+ });
315
+ // Signal init complete — sync part (agentTools, wrapStreamFn) is done
316
+ params.onInitComplete?.();
317
+ return dispatchPromise;
288
318
  },
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
319
  });
318
- log(`[BOT] ✅ Dispatcher completed for session: ${parsed.sessionId}`);
319
- log(`xy: dispatch complete (session=${parsed.sessionId})`);
320
+ logger.log(`[BOT] ✅ Dispatcher completed for session: ${parsed.sessionId}`);
321
+ logger.log(`xy: dispatch complete (session=${parsed.sessionId})`);
320
322
  }
321
323
  catch (err) {
322
324
  // ✅ Only log error, don't re-throw to prevent gateway restart
323
- error("Failed to handle XY message:", err);
325
+ logger.error("Failed to handle XY message:", err);
324
326
  runtime.error?.(`xy: Failed to handle message: ${String(err)}`);
325
- log(`[BOT] ❌ Error occurred, attempting cleanup...`);
327
+ logger.log(`[BOT] ❌ Error occurred, attempting cleanup...`);
326
328
  // 🔑 错误时也要清理taskId和session
327
329
  try {
328
330
  const params = message.params;
329
331
  const sessionId = params?.sessionId;
330
332
  if (sessionId) {
331
- log(`[BOT] 🧹 Cleaning up after error: ${sessionId}`);
333
+ logger.log(`[BOT] 🧹 Cleaning up after error: ${sessionId}`);
332
334
  // 清理 taskId
333
335
  decrementTaskIdRef(sessionId);
334
- unlockTaskId(sessionId);
335
336
  // 清理 session
336
337
  const core = getXYRuntime();
337
338
  const route = core.channel.routing.resolveAgentRoute({
@@ -344,11 +345,11 @@ export async function handleXYMessage(params) {
344
345
  },
345
346
  });
346
347
  unregisterSession(route.sessionKey);
347
- log(`[BOT] ✅ Cleanup completed after error`);
348
+ logger.log(`[BOT] ✅ Cleanup completed after error`);
348
349
  }
349
350
  }
350
351
  catch (cleanupErr) {
351
- log(`[BOT] ⚠️ Cleanup failed:`, cleanupErr);
352
+ logger.log(`[BOT] ⚠️ Cleanup failed:`, cleanupErr);
352
353
  // Ignore cleanup errors
353
354
  }
354
355
  // ❌ Don't re-throw: message processing error should not affect gateway stability
@@ -1,24 +1,9 @@
1
1
  import { resolveXYConfig, listXYAccountIds, getDefaultXYAccountId } from "./config.js";
2
2
  import { xyConfigSchema } from "./config-schema.js";
3
3
  import { xyOutbound } from "./outbound.js";
4
- import { locationTool } from "./tools/location-tool.js";
5
- import { xiaoyiGuiTool } from "./tools/xiaoyi-gui-tool.js";
6
- import { sendFileToUserTool } from "./tools/send-file-to-user-tool.js";
7
- import { viewPushResultTool } from "./tools/view-push-result-tool.js";
8
- import { imageReadingTool } from "./tools/image-reading-tool.js";
9
- import { timestampToUtc8Tool } from "./tools/timestamp-to-utc8-tool.js";
10
- import { saveSelfEvolutionSkillTool } from "./tools/save-self-evolution-skill-tool.js";
11
- import { callDeviceTool } from "./tools/call-device-tool.js";
12
- import { getNoteToolSchemaTool } from "./tools/get-note-tool-schema.js";
13
- import { getCalendarToolSchemaTool } from "./tools/get-calendar-tool-schema.js";
14
- import { getContactToolSchemaTool } from "./tools/get-contact-tool-schema.js";
15
- import { getPhotoToolSchemaTool } from "./tools/get-photo-tool-schema.js";
16
- import { getDeviceFileToolSchemaTool } from "./tools/get-device-file-tool-schema.js";
17
- import { getAlarmToolSchemaTool } from "./tools/get-alarm-tool-schema.js";
18
- import { getCollectionToolSchemaTool } from "./tools/get-collection-tool-schema.js";
19
- import { loginTokenTool } from "./tools/login-token-tool.js";
20
4
  import { filterToolsByDevice } from "./tools/device-tool-map.js";
21
5
  import { getCurrentSessionContext } from "./tools/session-manager.js";
6
+ import { createAllTools } from "./tools/create-all-tools.js";
22
7
  import { logger } from "./utils/logger.js";
23
8
  /**
24
9
  * Xiaoyi Channel Plugin for OpenClaw.
@@ -59,8 +44,8 @@ export const xyPlugin = {
59
44
  },
60
45
  outbound: xyOutbound,
61
46
  agentTools: () => {
62
- const allTools = [locationTool, callDeviceTool, getNoteToolSchemaTool, getCalendarToolSchemaTool, getContactToolSchemaTool, getPhotoToolSchemaTool, xiaoyiGuiTool, getDeviceFileToolSchemaTool, getAlarmToolSchemaTool, getCollectionToolSchemaTool, sendFileToUserTool, viewPushResultTool, imageReadingTool, timestampToUtc8Tool, saveSelfEvolutionSkillTool, loginTokenTool];
63
47
  const ctx = getCurrentSessionContext();
48
+ const allTools = createAllTools(ctx);
64
49
  const filtered = filterToolsByDevice(allTools, ctx?.deviceType);
65
50
  logger.log(`[DEVICE-FILTER] deviceType=${ctx?.deviceType ?? "(none)"}, tools: ${allTools.length} → ${filtered.length} (${filtered.map(t => t.name).join(", ")})`);
66
51
  return filtered;
@@ -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.