@ynhcj/xiaoyi-channel 0.0.185-beta → 0.0.185-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 (150) hide show
  1. package/dist/index.js +141 -1
  2. package/dist/src/acp-session-binding.d.ts +37 -0
  3. package/dist/src/acp-session-binding.js +237 -0
  4. package/dist/src/bot.js +62 -33
  5. package/dist/src/channel.js +68 -64
  6. package/dist/src/client.d.ts +5 -0
  7. package/dist/src/client.js +10 -0
  8. package/dist/src/cron-command.d.ts +2 -0
  9. package/dist/src/cron-command.js +14 -8
  10. package/dist/src/cron-query-handler.js +36 -0
  11. package/dist/src/cspl/call_api.js +4 -2
  12. package/dist/src/cspl/sentinel_hook.js +9 -8
  13. package/dist/src/cspl/upload_file.js +2 -2
  14. package/dist/src/formatter.js +72 -36
  15. package/dist/src/log-reporter/config-loader.d.ts +11 -0
  16. package/dist/src/log-reporter/config-loader.js +68 -0
  17. package/dist/src/log-reporter/cursor-store.d.ts +5 -0
  18. package/dist/src/log-reporter/cursor-store.js +26 -0
  19. package/dist/src/log-reporter/index.d.ts +10 -0
  20. package/dist/src/log-reporter/index.js +77 -0
  21. package/dist/src/log-reporter/reporter.d.ts +6 -0
  22. package/dist/src/log-reporter/reporter.js +17 -0
  23. package/dist/src/log-reporter/scanner.d.ts +6 -0
  24. package/dist/src/log-reporter/scanner.js +82 -0
  25. package/dist/src/log-reporter/types.d.ts +59 -0
  26. package/dist/src/log-reporter/types.js +2 -0
  27. package/dist/src/log-reporter/uploader.d.ts +6 -0
  28. package/dist/src/log-reporter/uploader.js +32 -0
  29. package/dist/src/memory-query-handler.d.ts +1 -0
  30. package/dist/src/memory-query-handler.js +283 -0
  31. package/dist/src/monitor.js +29 -9
  32. package/dist/src/outbound.js +5 -0
  33. package/dist/src/parser.d.ts +12 -0
  34. package/dist/src/parser.js +39 -13
  35. package/dist/src/provider.js +79 -16
  36. package/dist/src/reply-dispatcher.d.ts +3 -0
  37. package/dist/src/reply-dispatcher.js +166 -164
  38. package/dist/src/self-evolution-handler.js +2 -1
  39. package/dist/src/task-manager.d.ts +5 -6
  40. package/dist/src/task-manager.js +5 -9
  41. package/dist/src/tools/agent-as-skill-tool.d.ts +41 -2
  42. package/dist/src/tools/agent-as-skill-tool.js +144 -151
  43. package/dist/src/tools/calendar-tool.d.ts +24 -2
  44. package/dist/src/tools/calendar-tool.js +115 -117
  45. package/dist/src/tools/call-device-tool.d.ts +20 -6
  46. package/dist/src/tools/call-device-tool.js +116 -138
  47. package/dist/src/tools/call-phone-tool.d.ts +21 -2
  48. package/dist/src/tools/call-phone-tool.js +112 -114
  49. package/dist/src/tools/check-plugin-privilege-tool.d.ts +16 -2
  50. package/dist/src/tools/check-plugin-privilege-tool.js +141 -143
  51. package/dist/src/tools/create-alarm-tool.d.ts +39 -2
  52. package/dist/src/tools/create-alarm-tool.js +229 -231
  53. package/dist/src/tools/create-all-tools.js +8 -4
  54. package/dist/src/tools/delete-alarm-tool.d.ts +15 -2
  55. package/dist/src/tools/delete-alarm-tool.js +134 -136
  56. package/dist/src/tools/device-tool-map.d.ts +1 -1
  57. package/dist/src/tools/device-tool-map.js +9 -2
  58. package/dist/src/tools/discover-cross-devices-tool.d.ts +16 -2
  59. package/dist/src/tools/discover-cross-devices-tool.js +121 -124
  60. package/dist/src/tools/display-a2ui-card-bypath-tool.d.ts +21 -0
  61. package/dist/src/tools/display-a2ui-card-bypath-tool.js +65 -0
  62. package/dist/src/tools/get-alarm-tool-schema.d.ts +1 -2
  63. package/dist/src/tools/get-alarm-tool-schema.js +10 -16
  64. package/dist/src/tools/get-calendar-tool-schema.d.ts +1 -2
  65. package/dist/src/tools/get-calendar-tool-schema.js +8 -12
  66. package/dist/src/tools/get-collection-tool-schema.d.ts +1 -2
  67. package/dist/src/tools/get-collection-tool-schema.js +9 -11
  68. package/dist/src/tools/get-contact-tool-schema.d.ts +1 -2
  69. package/dist/src/tools/get-contact-tool-schema.js +10 -16
  70. package/dist/src/tools/get-device-file-tool-schema.d.ts +1 -2
  71. package/dist/src/tools/get-device-file-tool-schema.js +9 -13
  72. package/dist/src/tools/get-email-tool-schema.d.ts +1 -2
  73. package/dist/src/tools/get-email-tool-schema.js +8 -11
  74. package/dist/src/tools/get-note-tool-schema.d.ts +1 -2
  75. package/dist/src/tools/get-note-tool-schema.js +9 -14
  76. package/dist/src/tools/get-photo-tool-schema.d.ts +1 -2
  77. package/dist/src/tools/get-photo-tool-schema.js +9 -12
  78. package/dist/src/tools/image-reading-tool.d.ts +28 -2
  79. package/dist/src/tools/image-reading-tool.js +76 -76
  80. package/dist/src/tools/location-tool.d.ts +11 -2
  81. package/dist/src/tools/location-tool.js +93 -92
  82. package/dist/src/tools/login-token-tool.d.ts +20 -2
  83. package/dist/src/tools/login-token-tool.js +123 -125
  84. package/dist/src/tools/modify-alarm-tool.d.ts +47 -2
  85. package/dist/src/tools/modify-alarm-tool.js +250 -252
  86. package/dist/src/tools/modify-note-tool.d.ts +20 -2
  87. package/dist/src/tools/modify-note-tool.js +107 -109
  88. package/dist/src/tools/note-tool.d.ts +20 -2
  89. package/dist/src/tools/note-tool.js +106 -108
  90. package/dist/src/tools/query-app-message-tool.d.ts +28 -2
  91. package/dist/src/tools/query-app-message-tool.js +111 -113
  92. package/dist/src/tools/query-memory-data-tool.d.ts +28 -2
  93. package/dist/src/tools/query-memory-data-tool.js +112 -114
  94. package/dist/src/tools/query-todo-task-tool.d.ts +24 -2
  95. package/dist/src/tools/query-todo-task-tool.js +106 -108
  96. package/dist/src/tools/save-file-to-phone-tool.d.ts +24 -2
  97. package/dist/src/tools/save-file-to-phone-tool.js +130 -132
  98. package/dist/src/tools/save-media-to-gallery-tool.d.ts +24 -2
  99. package/dist/src/tools/save-media-to-gallery-tool.js +137 -139
  100. package/dist/src/tools/save-self-evolution-skill-tool.d.ts +54 -2
  101. package/dist/src/tools/save-self-evolution-skill-tool.js +194 -194
  102. package/dist/src/tools/search-alarm-tool.d.ts +34 -2
  103. package/dist/src/tools/search-alarm-tool.js +174 -176
  104. package/dist/src/tools/search-calendar-tool.d.ts +24 -2
  105. package/dist/src/tools/search-calendar-tool.js +148 -150
  106. package/dist/src/tools/search-contact-tool.d.ts +16 -2
  107. package/dist/src/tools/search-contact-tool.js +101 -103
  108. package/dist/src/tools/search-email-tool.d.ts +21 -2
  109. package/dist/src/tools/search-email-tool.js +110 -112
  110. package/dist/src/tools/search-file-tool.d.ts +16 -2
  111. package/dist/src/tools/search-file-tool.js +104 -106
  112. package/dist/src/tools/search-message-tool.d.ts +16 -2
  113. package/dist/src/tools/search-message-tool.js +103 -105
  114. package/dist/src/tools/search-note-tool.d.ts +16 -2
  115. package/dist/src/tools/search-note-tool.js +98 -100
  116. package/dist/src/tools/search-photo-gallery-tool.d.ts +21 -2
  117. package/dist/src/tools/search-photo-gallery-tool.js +35 -37
  118. package/dist/src/tools/send-cross-device-task-tool.d.ts +35 -2
  119. package/dist/src/tools/send-cross-device-task-tool.js +216 -150
  120. package/dist/src/tools/send-email-tool.d.ts +24 -2
  121. package/dist/src/tools/send-email-tool.js +108 -110
  122. package/dist/src/tools/send-file-to-user-tool.d.ts +20 -2
  123. package/dist/src/tools/send-file-to-user-tool.js +174 -178
  124. package/dist/src/tools/send-html-card-tool.d.ts +20 -2
  125. package/dist/src/tools/send-html-card-tool.js +85 -87
  126. package/dist/src/tools/send-message-tool.d.ts +20 -2
  127. package/dist/src/tools/send-message-tool.js +122 -124
  128. package/dist/src/tools/session-manager.d.ts +19 -52
  129. package/dist/src/tools/session-manager.js +95 -247
  130. package/dist/src/tools/upload-file-tool.d.ts +20 -2
  131. package/dist/src/tools/upload-file-tool.js +80 -82
  132. package/dist/src/tools/upload-photo-tool.d.ts +20 -2
  133. package/dist/src/tools/upload-photo-tool.js +68 -70
  134. package/dist/src/tools/xiaoyi-add-collection-tool.d.ts +32 -2
  135. package/dist/src/tools/xiaoyi-add-collection-tool.js +146 -148
  136. package/dist/src/tools/xiaoyi-collection-tool.d.ts +20 -2
  137. package/dist/src/tools/xiaoyi-collection-tool.js +114 -116
  138. package/dist/src/tools/xiaoyi-delete-collection-tool.d.ts +15 -2
  139. package/dist/src/tools/xiaoyi-delete-collection-tool.js +127 -129
  140. package/dist/src/tools/xiaoyi-gui-tool.d.ts +16 -2
  141. package/dist/src/tools/xiaoyi-gui-tool.js +92 -95
  142. package/dist/src/types.d.ts +6 -6
  143. package/dist/src/utils/config-manager.d.ts +3 -2
  144. package/dist/src/utils/config-manager.js +22 -2
  145. package/dist/src/utils/cron-push-map.d.ts +26 -0
  146. package/dist/src/utils/cron-push-map.js +131 -0
  147. package/dist/src/utils/logger.js +3 -14
  148. package/dist/src/websocket.d.ts +1 -1
  149. package/dist/src/websocket.js +29 -13
  150. package/package.json +1 -1
@@ -9,6 +9,9 @@ export interface CreateXYReplyDispatcherParams {
9
9
  steerState: {
10
10
  steered: boolean;
11
11
  };
12
+ /** Called at end of onIdle, after final frame is sent. openclaw's waitForIdle() does
13
+ * not await the async onIdle, so cleanup must happen inside onIdle itself. */
14
+ onIdleComplete?: () => void | Promise<void>;
12
15
  }
13
16
  /**
14
17
  * 清理 /tmp/xy_channel 目录中超过 24 小时的旧文件
@@ -1,11 +1,11 @@
1
1
  import { getXYRuntime } from "./runtime.js";
2
2
  import { sendA2AResponse, sendStatusUpdate, sendReasoningTextUpdate, sendCommand } from "./formatter.js";
3
3
  import { resolveXYConfig } from "./config.js";
4
- import { getCurrentTaskId, getCurrentMessageId } from "./task-manager.js";
5
4
  import { clearRunCrossTaskSentFiles, getCurrentSessionContext } from "./tools/session-manager.js";
6
5
  import fs from "fs/promises";
7
6
  import path from "path";
8
7
  import { logger } from "./utils/logger.js";
8
+ import { getCurrentTaskId, getCurrentMessageId } from "./task-manager.js";
9
9
  const TEMP_FILE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
10
10
  const RUN_CROSS_TASK_LOG_TAG = "[RunCrossTask]";
11
11
  function buildDistributionStatusCommand(context) {
@@ -39,17 +39,23 @@ function buildCrossTaskExecuteResultCommand(code, message, sentFiles = []) {
39
39
  async function sendRunCrossTaskResult(params) {
40
40
  const { config, sessionId, taskId, messageId, context, resultCode, resultMessage } = params;
41
41
  const sentFiles = Array.isArray(context.sentFiles) ? context.sentFiles : [];
42
+ const fileCardCount = sentFiles.length;
42
43
  const statusCommand = buildDistributionStatusCommand(context);
43
44
  const resultCommand = buildCrossTaskExecuteResultCommand(resultCode, resultMessage, sentFiles);
44
- await sendCommand({
45
- config,
46
- sessionId,
47
- taskId,
48
- messageId,
49
- commands: [statusCommand, resultCommand],
50
- });
51
- clearRunCrossTaskSentFiles(context);
52
- logger.log(`${RUN_CROSS_TASK_LOG_TAG} sent cross-task result, sessionId=${sessionId}, taskId=${taskId}, code=${resultCode}, sentFileCount=${sentFiles.length}, clearedSentFileCount=${sentFiles.length}, messageLength=${resultMessage.length}`);
45
+ try {
46
+ await sendCommand({
47
+ config,
48
+ sessionId,
49
+ taskId,
50
+ messageId,
51
+ commands: [statusCommand, resultCommand],
52
+ });
53
+ logger.log(`${RUN_CROSS_TASK_LOG_TAG} sent cross-task result, sessionId=${sessionId}, taskId=${taskId}, code=${resultCode}, fileCardCount=${fileCardCount}, messageLength=${resultMessage.length}`);
54
+ }
55
+ finally {
56
+ clearRunCrossTaskSentFiles(context);
57
+ logger.log(`${RUN_CROSS_TASK_LOG_TAG} cleared cross-task sentFiles, sessionId=${sessionId}, taskId=${taskId}, clearedFileCardCount=${fileCardCount}`);
58
+ }
53
59
  }
54
60
  /**
55
61
  * 清理 /tmp/xy_channel 目录中超过 24 小时的旧文件
@@ -90,15 +96,23 @@ export async function cleanupStaleTempFiles(tempDir = "/tmp/xy_channel") {
90
96
  * Runtime is expected to be validated before calling this function.
91
97
  */
92
98
  export function createXYReplyDispatcher(params) {
93
- const { cfg, runtime, sessionId, taskId, messageId, accountId, steerState } = params;
94
- // 初始taskIdmessageId(作为fallback)
95
- const initialTaskId = taskId;
96
- const initialMessageId = messageId;
99
+ const { cfg, runtime, sessionId, taskId, messageId, accountId, steerState, onIdleComplete } = params;
100
+ // fallback taskId/messageId(当 task-manager 返回 null 时使用)
101
+ // steer 触发时会通过 updateFallbackTaskId() 更新为最新的 taskId
102
+ let currentFallbackTaskId = taskId;
103
+ let currentFallbackMessageId = messageId;
104
+ // 跨链读取:steer 消息通过 registerTaskId 更新 Map,这里读取最新 taskId
97
105
  const getActiveTaskId = () => {
98
- return getCurrentTaskId(sessionId) ?? initialTaskId;
106
+ return getCurrentTaskId(sessionId) ?? currentFallbackTaskId;
99
107
  };
100
108
  const getActiveMessageId = () => {
101
- return getCurrentMessageId(sessionId) ?? initialMessageId;
109
+ return getCurrentMessageId(sessionId) ?? currentFallbackMessageId;
110
+ };
111
+ /** steer 触发时调用,同步 fallback taskId/messageId 到最新值 */
112
+ const updateFallbackTaskId = (newTaskId, newMessageId) => {
113
+ currentFallbackTaskId = newTaskId;
114
+ currentFallbackMessageId = newMessageId;
115
+ logger.log(`[DISPATCHER-UPDATE] Updated fallback taskId: ${newTaskId}`);
102
116
  };
103
117
  // Create a scoped logger that always uses this session's sessionId
104
118
  // and dynamically resolves the latest taskId
@@ -116,6 +130,7 @@ export function createXYReplyDispatcher(params) {
116
130
  let hasSentResponse = false;
117
131
  let finalSent = false;
118
132
  let accumulatedText = "";
133
+ let finalReplyText = "";
119
134
  const initialRunCrossTaskContext = getCurrentSessionContext()?.runCrossTaskContext;
120
135
  const getRunCrossTaskContext = () => {
121
136
  return getCurrentSessionContext()?.runCrossTaskContext ?? initialRunCrossTaskContext;
@@ -128,13 +143,12 @@ export function createXYReplyDispatcher(params) {
128
143
  statusUpdateInterval = setInterval(() => {
129
144
  // 🔑 使用动态taskId
130
145
  const currentTaskId = getActiveTaskId();
131
- const currentMessageId = getActiveMessageId();
132
146
  scopedLog().log(`[STATUS-INTERVAL] Triggering status update, taskId=${currentTaskId}`);
133
147
  void sendStatusUpdate({
134
148
  config,
135
149
  sessionId,
136
- taskId: currentTaskId, // 🔑 动态taskId
137
- messageId: currentMessageId, // 🔑 动态messageId
150
+ taskId: currentTaskId,
151
+ messageId: getActiveMessageId(),
138
152
  text: "任务正在处理中,请稍候~",
139
153
  state: "working",
140
154
  }).catch((err) => {
@@ -154,24 +168,20 @@ export function createXYReplyDispatcher(params) {
154
168
  responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
155
169
  humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, accountId),
156
170
  onReplyStart: () => {
157
- const currentTaskId = getActiveTaskId();
158
- scopedLog().log(`[REPLY-START] Reply started, taskId=${currentTaskId}, steered=${steerState.steered}`);
171
+ scopedLog().log(`[REPLY-START] Reply started, taskId=${taskId}, steered=${steerState.steered}`);
159
172
  },
160
173
  deliver: async (payload, info) => {
161
- // 🔑 steered dispatch不发送内容(让主dispatcher处理)
162
- if (steerState.steered) {
163
- scopedLog().log(`[DELIVER] Steered dispatch, skipping, kind=${info?.kind}`);
164
- return;
165
- }
166
174
  const text = payload.text ?? "";
167
- const currentTaskId = getActiveTaskId();
168
- const currentMessageId = getActiveMessageId();
169
175
  scopedLog().log(`[DELIVER] kind=${info?.kind}, text.length=${text.length}`);
170
176
  try {
171
177
  if (!text.trim()) {
172
178
  scopedLog().log(`[DELIVER SKIP] Empty text, skipping`);
173
179
  return;
174
180
  }
181
+ if (info?.kind === "final") {
182
+ finalReplyText = text;
183
+ scopedLog().log(`[DELIVER] Captured final reply text, length=${finalReplyText.length}`);
184
+ }
175
185
  // 🔑 如果 onPartialReply 已经流式发送过文本,deliver 不再重复发送
176
186
  if (hasSentResponse) {
177
187
  scopedLog().log(`[DELIVER SKIP] Already sent via onPartialReply`);
@@ -183,8 +193,8 @@ export function createXYReplyDispatcher(params) {
183
193
  await sendA2AResponse({
184
194
  config,
185
195
  sessionId,
186
- taskId: currentTaskId,
187
- messageId: currentMessageId,
196
+ taskId: getActiveTaskId(),
197
+ messageId: getActiveMessageId(),
188
198
  text,
189
199
  append: true,
190
200
  final: false,
@@ -197,20 +207,13 @@ export function createXYReplyDispatcher(params) {
197
207
  onError: async (err, info) => {
198
208
  runtime.error?.(`xy: ${info.kind} reply failed: ${String(err)}`);
199
209
  stopStatusInterval();
200
- // 🔑 steered dispatcher不发送错误状态(让主dispatcher处理)
201
- if (steerState.steered) {
202
- scopedLog().log(`[ON-ERROR] Steered dispatch, skipping error response`);
203
- return;
204
- }
205
210
  if (!hasSentResponse) {
206
- const currentTaskId = getActiveTaskId();
207
- const currentMessageId = getActiveMessageId();
208
211
  try {
209
212
  await sendStatusUpdate({
210
213
  config,
211
214
  sessionId,
212
- taskId: currentTaskId,
213
- messageId: currentMessageId,
215
+ taskId: getActiveTaskId(),
216
+ messageId: getActiveMessageId(),
214
217
  text: "处理失败,请稍后重试",
215
218
  state: "failed",
216
219
  });
@@ -221,105 +224,119 @@ export function createXYReplyDispatcher(params) {
221
224
  }
222
225
  },
223
226
  onIdle: async () => {
224
- const currentTaskId = getActiveTaskId();
225
- const currentMessageId = getActiveMessageId();
226
227
  scopedLog().log(`[ON-IDLE] Reply idle, steered=${steerState.steered}, hasSentResponse=${hasSentResponse}, finalSent=${finalSent}`);
227
- // 🔑 steered dispatch不发送final响应(核心已注入到活跃 Pi run
228
- if (steerState.steered) {
229
- scopedLog().log(`[ON-IDLE] Steered dispatch, skipping final response`);
228
+ // 🔑 steered dispatch without response — steer was injected into active run,
229
+ // no reply generated. Skip final response without sending error.
230
+ if (steerState.steered && !hasSentResponse) {
231
+ scopedLog().log(`[ON-IDLE] Steered dispatch, no response generated, skipping`);
230
232
  stopStatusInterval();
231
- return; // ← 直接返回,不发送任何东西!
233
+ return;
232
234
  }
233
- // 正常模式(或未被steerdispatch)
234
- if (hasSentResponse && !finalSent) {
235
- scopedLog().log(`[ON-IDLE] Sending accumulated text, length=${accumulatedText.length}`);
236
- try {
237
- const runCrossTaskContext = getRunCrossTaskContext();
238
- if (runCrossTaskContext) {
239
- await sendRunCrossTaskResult({
235
+ // 🔑 用 try/finally 确保 cleanup 在 onIdle async 工作全部完成后才执行。
236
+ // openclaw 的 waitForIdle() void options.onIdle?.() 调用 onIdle,
237
+ // 不会 await 返回的 Promise,因此 onSettled 可能在 onIdle 中途触发。
238
+ // 所有清理逻辑必须放在 finally 块中,不要依赖 onSettled。
239
+ try {
240
+ // 正常模式(或未被steer的dispatch)
241
+ if (hasSentResponse && !finalSent) {
242
+ const trimmedFinalReplyText = finalReplyText.trim();
243
+ const trimmedAccumulatedText = accumulatedText.trim();
244
+ const crossTaskResultMessage = trimmedFinalReplyText || trimmedAccumulatedText;
245
+ const crossTaskResultSource = trimmedFinalReplyText ? "final" : "accumulated";
246
+ scopedLog().log(`[ON-IDLE] [SendCrossResult]Sending cross-task result, source=${crossTaskResultSource}, resultMessage.length=${crossTaskResultMessage.length}`);
247
+ try {
248
+ const runCrossTaskContext = getRunCrossTaskContext();
249
+ if (runCrossTaskContext) {
250
+ await sendRunCrossTaskResult({
251
+ config,
252
+ sessionId,
253
+ taskId: getActiveTaskId(),
254
+ messageId: getActiveMessageId(),
255
+ context: runCrossTaskContext,
256
+ resultCode: "0",
257
+ resultMessage: crossTaskResultMessage,
258
+ });
259
+ }
260
+ // 🔑 使用动态taskId发送完成状态
261
+ await sendStatusUpdate({
262
+ config,
263
+ sessionId,
264
+ taskId: getActiveTaskId(),
265
+ messageId: getActiveMessageId(),
266
+ text: "任务处理已完成~",
267
+ state: "completed",
268
+ });
269
+ scopedLog().log(`[ON-IDLE] Sent completion status update`);
270
+ // 🔑 使用动态taskId发送最终响应(空字符串表示流结束)
271
+ await sendA2AResponse({
240
272
  config,
241
273
  sessionId,
242
- taskId: currentTaskId,
243
- messageId: currentMessageId,
244
- context: runCrossTaskContext,
245
- resultCode: "0",
246
- resultMessage: accumulatedText,
274
+ taskId: getActiveTaskId(),
275
+ messageId: getActiveMessageId(),
276
+ text: "",
277
+ append: true,
278
+ final: true,
247
279
  });
280
+ finalSent = true;
281
+ scopedLog().log(`[ON-IDLE] Sent final response (empty, stream end)`);
282
+ }
283
+ catch (err) {
284
+ scopedLog().error(`[ON-IDLE] Failed to send final response:`, err);
248
285
  }
249
- // 🔑 使用动态taskId发送完成状态
250
- await sendStatusUpdate({
251
- config,
252
- sessionId,
253
- taskId: currentTaskId,
254
- messageId: currentMessageId,
255
- text: "任务处理已完成~",
256
- state: "completed",
257
- });
258
- scopedLog().log(`[ON-IDLE] Sent completion status update`);
259
- // 🔑 使用动态taskId发送最终响应(空字符串表示流结束)
260
- await sendA2AResponse({
261
- config,
262
- sessionId,
263
- taskId: currentTaskId,
264
- messageId: currentMessageId,
265
- text: "",
266
- append: true,
267
- final: true,
268
- });
269
- finalSent = true;
270
- scopedLog().log(`[ON-IDLE] Sent final response (empty, stream end)`);
271
- }
272
- catch (err) {
273
- scopedLog().error(`[ON-IDLE] Failed to send final response:`, err);
274
286
  }
275
- }
276
- else {
277
- // 正常失败场景(非steered)
278
- scopedLog().log(`[ON-IDLE] Skipping final message: hasSentResponse=${hasSentResponse}, finalSent=${finalSent}`);
279
- try {
280
- const runCrossTaskContext = getRunCrossTaskContext();
281
- if (runCrossTaskContext) {
282
- await sendRunCrossTaskResult({
287
+ else {
288
+ // 正常失败场景(非steered)
289
+ scopedLog().log(`[ON-IDLE] Skipping final message: hasSentResponse=${hasSentResponse}, finalSent=${finalSent}`);
290
+ try {
291
+ const runCrossTaskContext = getRunCrossTaskContext();
292
+ if (runCrossTaskContext) {
293
+ await sendRunCrossTaskResult({
294
+ config,
295
+ sessionId,
296
+ taskId: getActiveTaskId(),
297
+ messageId: getActiveMessageId(),
298
+ context: runCrossTaskContext,
299
+ resultCode: "1",
300
+ resultMessage: "任务执行异常,请重试",
301
+ });
302
+ }
303
+ await sendStatusUpdate({
304
+ config,
305
+ sessionId,
306
+ taskId: getActiveTaskId(),
307
+ messageId: getActiveMessageId(),
308
+ text: "任务处理中断了~",
309
+ state: "failed",
310
+ });
311
+ scopedLog().log(`[ON-IDLE] Sent failure status update`);
312
+ await sendA2AResponse({
283
313
  config,
284
314
  sessionId,
285
- taskId: currentTaskId,
286
- messageId: currentMessageId,
287
- context: runCrossTaskContext,
288
- resultCode: "1",
289
- resultMessage: "任务执行异常,请重试",
315
+ taskId: getActiveTaskId(),
316
+ messageId: getActiveMessageId(),
317
+ text: "任务执行异常,请重试~",
318
+ append: true,
319
+ final: true,
320
+ errorCode: 99921111,
321
+ errorMessage: "任务执行异常,请重试",
290
322
  });
323
+ finalSent = true;
324
+ scopedLog().log(`[ON-IDLE] Sent error response, code=99921111`);
325
+ }
326
+ catch (err) {
327
+ scopedLog().error(`[ON-IDLE] Failed to send error response:`, err);
291
328
  }
292
- await sendStatusUpdate({
293
- config,
294
- sessionId,
295
- taskId: currentTaskId,
296
- messageId: currentMessageId,
297
- text: "任务处理中断了~",
298
- state: "failed",
299
- });
300
- scopedLog().log(`[ON-IDLE] Sent failure status update`);
301
- await sendA2AResponse({
302
- config,
303
- sessionId,
304
- taskId: currentTaskId,
305
- messageId: currentMessageId,
306
- text: "任务执行异常,请重试~",
307
- append: true,
308
- final: true,
309
- errorCode: 99921111,
310
- errorMessage: "任务执行异常,请重试",
311
- });
312
- finalSent = true;
313
- scopedLog().log(`[ON-IDLE] Sent error response, code=99921111`);
314
- }
315
- catch (err) {
316
- scopedLog().error(`[ON-IDLE] Failed to send error response:`, err);
317
329
  }
318
330
  }
319
- stopStatusInterval();
331
+ finally {
332
+ stopStatusInterval();
333
+ // 🔑 清理必须在 onIdle 内部完成,因为 openclaw 的 waitForIdle() 不会
334
+ // await onIdle 返回的 Promise(源码中为 void options.onIdle?.()),
335
+ // 导致 onSettled 在 onIdle 的 async 工作完成之前就执行。
336
+ await onIdleComplete?.();
337
+ }
320
338
  },
321
339
  onCleanup: () => {
322
- const currentTaskId = getActiveTaskId();
323
340
  scopedLog().log(`[ON-CLEANUP] Reply cleanup, steered=${steerState.steered}`);
324
341
  },
325
342
  });
@@ -330,12 +347,6 @@ export function createXYReplyDispatcher(params) {
330
347
  suppressToolErrorWarnings: true,
331
348
  onModelSelected: prefixContext.onModelSelected,
332
349
  onToolStart: async ({ name, phase }) => {
333
- // 🔑 steered dispatch不发送tool状态(让主dispatcher处理)
334
- if (steerState.steered) {
335
- return;
336
- }
337
- const currentTaskId = getActiveTaskId();
338
- const currentMessageId = getActiveMessageId();
339
350
  scopedLog().log(`[TOOL-START] Tool: ${name}, phase: ${phase}`);
340
351
  if (phase === "start") {
341
352
  const toolName = name || "unknown";
@@ -349,8 +360,8 @@ export function createXYReplyDispatcher(params) {
349
360
  await sendStatusUpdate({
350
361
  config,
351
362
  sessionId,
352
- taskId: currentTaskId,
353
- messageId: currentMessageId,
363
+ taskId: getActiveTaskId(),
364
+ messageId: getActiveMessageId(),
354
365
  text: `正在使用工具: ${toolName}...`,
355
366
  state: "working",
356
367
  });
@@ -362,12 +373,6 @@ export function createXYReplyDispatcher(params) {
362
373
  }
363
374
  },
364
375
  onToolResult: async (payload) => {
365
- // 🔑 steered dispatch不发送tool结果(让主dispatcher处理)
366
- if (steerState.steered) {
367
- return;
368
- }
369
- const currentTaskId = getActiveTaskId();
370
- const currentMessageId = getActiveMessageId();
371
376
  const text = payload.text ?? "";
372
377
  const hasMedia = Boolean(payload.mediaUrl || (payload.mediaUrls?.length ?? 0) > 0);
373
378
  scopedLog().log(`[TOOL-RESULT] Tool result, text.length: ${text.length}`);
@@ -377,8 +382,8 @@ export function createXYReplyDispatcher(params) {
377
382
  await sendStatusUpdate({
378
383
  config,
379
384
  sessionId,
380
- taskId: currentTaskId,
381
- messageId: currentMessageId,
385
+ taskId: getActiveTaskId(),
386
+ messageId: getActiveMessageId(),
382
387
  text: resultText,
383
388
  state: "working",
384
389
  });
@@ -390,12 +395,6 @@ export function createXYReplyDispatcher(params) {
390
395
  }
391
396
  },
392
397
  onReasoningStream: async (payload) => {
393
- // 🔑 steered dispatch不发送reasoning stream
394
- if (steerState.steered) {
395
- return;
396
- }
397
- const currentTaskId = getActiveTaskId();
398
- const currentMessageId = getActiveMessageId();
399
398
  let text = payload.text ?? "";
400
399
  // Strip "Reasoning:" prefix that some reasoning models add to their thinking output
401
400
  const lines = text.split(/\r?\n/);
@@ -407,8 +406,8 @@ export function createXYReplyDispatcher(params) {
407
406
  await sendReasoningTextUpdate({
408
407
  config,
409
408
  sessionId,
410
- taskId: currentTaskId,
411
- messageId: currentMessageId,
409
+ taskId: getActiveTaskId(),
410
+ messageId: getActiveMessageId(),
412
411
  text,
413
412
  append: false,
414
413
  });
@@ -419,27 +418,29 @@ export function createXYReplyDispatcher(params) {
419
418
  }
420
419
  },
421
420
  onPartialReply: async (payload) => {
422
- // 🔑 steered dispatch不发送partial reply(让主dispatcher处理)
423
- if (steerState.steered) {
424
- return;
425
- }
426
- const currentTaskId = getActiveTaskId();
427
- const currentMessageId = getActiveMessageId();
428
- const text = payload.text ?? "";
421
+ let text = payload.text ?? "";
429
422
  try {
430
423
  if (text.length > 0) {
431
- accumulatedText += text;
424
+ // 模型可能返回完整文本(非纯增量),如果新文本以已累积内容开头,
425
+ // 则只取新增的后缀部分,避免 append 模式下重复拼接。
426
+ let sendText = text;
427
+ if (accumulatedText.length > 0 && text.startsWith(accumulatedText)) {
428
+ sendText = text.slice(accumulatedText.length);
429
+ }
430
+ accumulatedText += sendText;
432
431
  hasSentResponse = true;
433
- await sendA2AResponse({
434
- config,
435
- sessionId,
436
- taskId: currentTaskId,
437
- messageId: currentMessageId,
438
- text,
439
- append: false,
440
- final: false,
441
- log: false,
442
- });
432
+ if (sendText.length > 0) {
433
+ await sendA2AResponse({
434
+ config,
435
+ sessionId,
436
+ taskId: getActiveTaskId(),
437
+ messageId: getActiveMessageId(),
438
+ text: sendText,
439
+ append: true,
440
+ final: false,
441
+ log: false,
442
+ });
443
+ }
443
444
  }
444
445
  }
445
446
  catch (err) {
@@ -450,5 +451,6 @@ export function createXYReplyDispatcher(params) {
450
451
  markDispatchIdle,
451
452
  startStatusInterval,
452
453
  stopStatusInterval,
454
+ updateFallbackTaskId,
453
455
  };
454
456
  }
@@ -1,3 +1,4 @@
1
+ import os from "os";
1
2
  import { readFileSync, writeFileSync } from "fs";
2
3
  import { v4 as uuidv4 } from "uuid";
3
4
  import { resolveXYConfig } from "./config.js";
@@ -136,7 +137,7 @@ export async function handleSelfEvolutionStateGetEvent(context, cfg, runtime, ws
136
137
  agentId: cfg.agentId,
137
138
  sessionId,
138
139
  taskId,
139
- msgDetail: JSON.stringify(jsonRpcResponse),
140
+ msgDetail: JSON.stringify({ ...jsonRpcResponse, hostname: os.hostname() }),
140
141
  };
141
142
  logger.log(`[A2A_COMMAND] Sending A2A command, taskId: ${taskId}`);
142
143
  await wsManager.sendMessage(sessionId, outboundMessage);
@@ -14,11 +14,14 @@ export declare function registerTaskId(sessionId: string, taskId: string, messag
14
14
  */
15
15
  export declare function decrementTaskIdRef(sessionId: string): void;
16
16
  /**
17
- * 获取session的当前活跃taskId
17
+ * 获取session的当前活跃taskId
18
+ * 仅供 reply-dispatcher 跨链读取 steer 更新后的 taskId。
19
+ * 工具和 sendCommand 应使用 ALS SessionContext.taskId。
18
20
  */
19
21
  export declare function getCurrentTaskId(sessionId: string): string | null;
20
22
  /**
21
- * 获取session的当前活跃messageId
23
+ * 获取session的当前活跃messageId
24
+ * 仅供 reply-dispatcher 跨链读取 steer 更新后的 messageId。
22
25
  */
23
26
  export declare function getCurrentMessageId(sessionId: string): string | null;
24
27
  /**
@@ -29,8 +32,4 @@ export declare function hasActiveTask(sessionId: string): boolean;
29
32
  * 获取所有活跃的 task bindings(用于 gateway_stop 通知等场景)
30
33
  */
31
34
  export declare function getAllActiveTaskBindings(): TaskIdBinding[];
32
- /**
33
- * 强制清理(错误恢复用)
34
- */
35
- export declare function forceCleanTaskId(sessionId: string): void;
36
35
  export {};
@@ -44,14 +44,17 @@ export function decrementTaskIdRef(sessionId) {
44
44
  activeTaskIds.delete(sessionId);
45
45
  }
46
46
  /**
47
- * 获取session的当前活跃taskId
47
+ * 获取session的当前活跃taskId
48
+ * 仅供 reply-dispatcher 跨链读取 steer 更新后的 taskId。
49
+ * 工具和 sendCommand 应使用 ALS SessionContext.taskId。
48
50
  */
49
51
  export function getCurrentTaskId(sessionId) {
50
52
  const binding = activeTaskIds.get(sessionId);
51
53
  return binding?.currentTaskId ?? null;
52
54
  }
53
55
  /**
54
- * 获取session的当前活跃messageId
56
+ * 获取session的当前活跃messageId
57
+ * 仅供 reply-dispatcher 跨链读取 steer 更新后的 messageId。
55
58
  */
56
59
  export function getCurrentMessageId(sessionId) {
57
60
  const binding = activeTaskIds.get(sessionId);
@@ -69,10 +72,3 @@ export function hasActiveTask(sessionId) {
69
72
  export function getAllActiveTaskBindings() {
70
73
  return Array.from(activeTaskIds.values());
71
74
  }
72
- /**
73
- * 强制清理(错误恢复用)
74
- */
75
- export function forceCleanTaskId(sessionId) {
76
- logger.log(`[TASK_MANAGER] Force clearing taskId`);
77
- activeTaskIds.delete(sessionId);
78
- }
@@ -1,7 +1,46 @@
1
- import type { SessionContext } from "./session-manager.js";
2
1
  /**
3
2
  * Agent-as-skill tool - invokes a registered agent by agentId as a skill.
4
3
  * The tool receives the agentId, query, and optional file attachments,
5
4
  * forwards the request to the target agent via WebSocket, and returns the result.
6
5
  */
7
- export declare function createAgentAsSkillTool(ctx: SessionContext): any;
6
+ export declare const agentAsSkillTool: {
7
+ name: string;
8
+ label: string;
9
+ description: string;
10
+ parameters: {
11
+ type: string;
12
+ properties: {
13
+ agentId: {
14
+ type: string;
15
+ description: string;
16
+ };
17
+ query: {
18
+ type: string;
19
+ description: string;
20
+ };
21
+ filesInfo: {
22
+ description: string;
23
+ items: {
24
+ type: string;
25
+ properties: {
26
+ fileType: {
27
+ type: string;
28
+ description: string;
29
+ enum: string[];
30
+ };
31
+ fileId: {
32
+ type: string;
33
+ description: string;
34
+ };
35
+ fileUrl: {
36
+ type: string;
37
+ description: string;
38
+ };
39
+ };
40
+ };
41
+ };
42
+ };
43
+ required: string[];
44
+ };
45
+ execute(toolCallId: string, params: any): Promise<unknown>;
46
+ };