@ynhcj/xiaoyi-channel 0.0.175-next → 0.0.176-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.
package/dist/src/bot.js CHANGED
@@ -113,6 +113,13 @@ export async function handleXYMessage(params) {
113
113
  // Steer injections skip taskId registration to avoid overwriting the active taskId
114
114
  if (!skipReg) {
115
115
  registerTaskId(parsed.sessionId, parsed.taskId, parsed.messageId);
116
+ // 🔑 steer 场景:同步更新活跃 dispatcher 的 fallback taskId/messageId
117
+ if (isUpdate) {
118
+ const updater = dispatcherUpdaters.get(parsed.sessionId);
119
+ if (updater) {
120
+ updater(parsed.taskId, parsed.messageId);
121
+ }
122
+ }
116
123
  // Extract and update push_id if present
117
124
  const pushId = extractPushId(parsed.parts);
118
125
  if (pushId) {
@@ -354,7 +361,7 @@ export async function handleXYMessage(params) {
354
361
  decrementTaskIdRef(parsed.sessionId);
355
362
  log.log(`[BOT] Cleanup completed`);
356
363
  };
357
- const { dispatcher, replyOptions, markDispatchIdle, startStatusInterval } = createXYReplyDispatcher({
364
+ const { dispatcher, replyOptions, markDispatchIdle, startStatusInterval, updateFallbackTaskId } = createXYReplyDispatcher({
358
365
  cfg,
359
366
  runtime,
360
367
  sessionId: parsed.sessionId,
@@ -364,6 +371,8 @@ export async function handleXYMessage(params) {
364
371
  steerState,
365
372
  onIdleComplete: cleanup,
366
373
  });
374
+ // 🔑 注册 dispatcher 的 fallback taskId 更新函数,供 steer 路径调用
375
+ dispatcherUpdaters.set(parsed.sessionId, updateFallbackTaskId);
367
376
  // Steer injections don't need status intervals
368
377
  if (!skipReg) {
369
378
  startStatusInterval();
@@ -392,9 +401,9 @@ export async function handleXYMessage(params) {
392
401
  log.log(`[BOT] Steered dispatch settled, skipping cleanup`);
393
402
  return;
394
403
  }
395
- // Fallback cleanup: 正常流程下 onIdleComplete 已经执行过,cleaned=true 会跳过。
396
- // 仅在 onIdle 未正常完成时(如异常),这里做兜底清理。
397
- cleanup();
404
+ // cleanup 已由 onIdleComplete onIdle 的 finally 中执行。
405
+ // onSettled 不做任何清理(直接在这里清理会发生 race condition)。
406
+ dispatcherUpdaters.delete(parsed.sessionId);
398
407
  },
399
408
  run: () => {
400
409
  // 🔐 Use AsyncLocalStorage to provide session context to tools.
@@ -478,8 +487,11 @@ if (!_g.__xyStreamingSignals)
478
487
  _g.__xyStreamingSignals = new Map();
479
488
  if (!_g.__xySteerQueues)
480
489
  _g.__xySteerQueues = new Map();
490
+ if (!_g.__xyDispatcherUpdaters)
491
+ _g.__xyDispatcherUpdaters = new Map();
481
492
  const streamingSignals = _g.__xyStreamingSignals;
482
493
  const steerQueues = _g.__xySteerQueues;
494
+ const dispatcherUpdaters = _g.__xyDispatcherUpdaters;
483
495
  /**
484
496
  * 由 provider.ts 在 wrapStreamFn 调用时触发。
485
497
  * 这是模型 API 被调用的精确时刻,此时 isStreaming 一定为 true。
@@ -97,15 +97,22 @@ export async function cleanupStaleTempFiles(tempDir = "/tmp/xy_channel") {
97
97
  */
98
98
  export function createXYReplyDispatcher(params) {
99
99
  const { cfg, runtime, sessionId, taskId, messageId, accountId, steerState, onIdleComplete } = params;
100
- // 初始taskIdmessageId(作为fallback)
101
- const initialTaskId = taskId;
102
- const initialMessageId = messageId;
100
+ // fallback taskId/messageId(当 task-manager 返回 null 时使用)
101
+ // steer 触发时会通过 updateFallbackTaskId() 更新为最新的 taskId
102
+ let currentFallbackTaskId = taskId;
103
+ let currentFallbackMessageId = messageId;
103
104
  // 跨链读取:steer 消息通过 registerTaskId 更新 Map,这里读取最新 taskId
104
105
  const getActiveTaskId = () => {
105
- return getCurrentTaskId(sessionId) ?? initialTaskId;
106
+ return getCurrentTaskId(sessionId) ?? currentFallbackTaskId;
106
107
  };
107
108
  const getActiveMessageId = () => {
108
- 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}`);
109
116
  };
110
117
  // Create a scoped logger that always uses this session's sessionId
111
118
  // and dynamically resolves the latest taskId
@@ -234,101 +241,109 @@ export function createXYReplyDispatcher(params) {
234
241
  stopStatusInterval();
235
242
  return; // ← 直接返回,不发送任何东西!
236
243
  }
237
- // 正常模式(或未被steerdispatch)
238
- if (hasSentResponse && !finalSent) {
239
- const trimmedFinalReplyText = finalReplyText.trim();
240
- const trimmedAccumulatedText = accumulatedText.trim();
241
- const crossTaskResultMessage = trimmedFinalReplyText || trimmedAccumulatedText;
242
- const crossTaskResultSource = trimmedFinalReplyText ? "final" : "accumulated";
243
- scopedLog().log(`[ON-IDLE] [SendCrossResult]Sending cross-task result, source=${crossTaskResultSource}, resultMessage.length=${crossTaskResultMessage.length}`);
244
- try {
245
- const runCrossTaskContext = getRunCrossTaskContext();
246
- if (runCrossTaskContext) {
247
- await sendRunCrossTaskResult({
244
+ // 🔑 用 try/finally 确保 cleanup 在 onIdle async 工作全部完成后才执行。
245
+ // openclaw 的 waitForIdle() void options.onIdle?.() 调用 onIdle,
246
+ // 不会 await 返回的 Promise,因此 onSettled 可能在 onIdle 中途触发。
247
+ // 所有清理逻辑必须放在 finally 块中,不要依赖 onSettled。
248
+ try {
249
+ // 正常模式(或未被steer的dispatch)
250
+ if (hasSentResponse && !finalSent) {
251
+ const trimmedFinalReplyText = finalReplyText.trim();
252
+ const trimmedAccumulatedText = accumulatedText.trim();
253
+ const crossTaskResultMessage = trimmedFinalReplyText || trimmedAccumulatedText;
254
+ const crossTaskResultSource = trimmedFinalReplyText ? "final" : "accumulated";
255
+ scopedLog().log(`[ON-IDLE] [SendCrossResult]Sending cross-task result, source=${crossTaskResultSource}, resultMessage.length=${crossTaskResultMessage.length}`);
256
+ try {
257
+ const runCrossTaskContext = getRunCrossTaskContext();
258
+ if (runCrossTaskContext) {
259
+ await sendRunCrossTaskResult({
260
+ config,
261
+ sessionId,
262
+ taskId: getActiveTaskId(),
263
+ messageId: getActiveMessageId(),
264
+ context: runCrossTaskContext,
265
+ resultCode: "0",
266
+ resultMessage: crossTaskResultMessage,
267
+ });
268
+ }
269
+ // 🔑 使用动态taskId发送完成状态
270
+ await sendStatusUpdate({
271
+ config,
272
+ sessionId,
273
+ taskId: getActiveTaskId(),
274
+ messageId: getActiveMessageId(),
275
+ text: "任务处理已完成~",
276
+ state: "completed",
277
+ });
278
+ scopedLog().log(`[ON-IDLE] Sent completion status update`);
279
+ // 🔑 使用动态taskId发送最终响应(空字符串表示流结束)
280
+ await sendA2AResponse({
248
281
  config,
249
282
  sessionId,
250
283
  taskId: getActiveTaskId(),
251
284
  messageId: getActiveMessageId(),
252
- context: runCrossTaskContext,
253
- resultCode: "0",
254
- resultMessage: crossTaskResultMessage,
285
+ text: "",
286
+ append: true,
287
+ final: true,
255
288
  });
289
+ finalSent = true;
290
+ scopedLog().log(`[ON-IDLE] Sent final response (empty, stream end)`);
291
+ }
292
+ catch (err) {
293
+ scopedLog().error(`[ON-IDLE] Failed to send final response:`, err);
256
294
  }
257
- // 🔑 使用动态taskId发送完成状态
258
- await sendStatusUpdate({
259
- config,
260
- sessionId,
261
- taskId: getActiveTaskId(),
262
- messageId: getActiveMessageId(),
263
- text: "任务处理已完成~",
264
- state: "completed",
265
- });
266
- scopedLog().log(`[ON-IDLE] Sent completion status update`);
267
- // 🔑 使用动态taskId发送最终响应(空字符串表示流结束)
268
- await sendA2AResponse({
269
- config,
270
- sessionId,
271
- taskId: getActiveTaskId(),
272
- messageId: getActiveMessageId(),
273
- text: "",
274
- append: true,
275
- final: true,
276
- });
277
- finalSent = true;
278
- scopedLog().log(`[ON-IDLE] Sent final response (empty, stream end)`);
279
- }
280
- catch (err) {
281
- scopedLog().error(`[ON-IDLE] Failed to send final response:`, err);
282
295
  }
283
- }
284
- else {
285
- // 正常失败场景(非steered)
286
- scopedLog().log(`[ON-IDLE] Skipping final message: hasSentResponse=${hasSentResponse}, finalSent=${finalSent}`);
287
- try {
288
- const runCrossTaskContext = getRunCrossTaskContext();
289
- if (runCrossTaskContext) {
290
- await sendRunCrossTaskResult({
296
+ else {
297
+ // 正常失败场景(非steered)
298
+ scopedLog().log(`[ON-IDLE] Skipping final message: hasSentResponse=${hasSentResponse}, finalSent=${finalSent}`);
299
+ try {
300
+ const runCrossTaskContext = getRunCrossTaskContext();
301
+ if (runCrossTaskContext) {
302
+ await sendRunCrossTaskResult({
303
+ config,
304
+ sessionId,
305
+ taskId: getActiveTaskId(),
306
+ messageId: getActiveMessageId(),
307
+ context: runCrossTaskContext,
308
+ resultCode: "1",
309
+ resultMessage: "任务执行异常,请重试",
310
+ });
311
+ }
312
+ await sendStatusUpdate({
313
+ config,
314
+ sessionId,
315
+ taskId: getActiveTaskId(),
316
+ messageId: getActiveMessageId(),
317
+ text: "任务处理中断了~",
318
+ state: "failed",
319
+ });
320
+ scopedLog().log(`[ON-IDLE] Sent failure status update`);
321
+ await sendA2AResponse({
291
322
  config,
292
323
  sessionId,
293
324
  taskId: getActiveTaskId(),
294
325
  messageId: getActiveMessageId(),
295
- context: runCrossTaskContext,
296
- resultCode: "1",
297
- resultMessage: "任务执行异常,请重试",
326
+ text: "任务执行异常,请重试~",
327
+ append: true,
328
+ final: true,
329
+ errorCode: 99921111,
330
+ errorMessage: "任务执行异常,请重试",
298
331
  });
332
+ finalSent = true;
333
+ scopedLog().log(`[ON-IDLE] Sent error response, code=99921111`);
334
+ }
335
+ catch (err) {
336
+ scopedLog().error(`[ON-IDLE] Failed to send error response:`, err);
299
337
  }
300
- await sendStatusUpdate({
301
- config,
302
- sessionId,
303
- taskId: getActiveTaskId(),
304
- messageId: getActiveMessageId(),
305
- text: "任务处理中断了~",
306
- state: "failed",
307
- });
308
- scopedLog().log(`[ON-IDLE] Sent failure status update`);
309
- await sendA2AResponse({
310
- config,
311
- sessionId,
312
- taskId: getActiveTaskId(),
313
- messageId: getActiveMessageId(),
314
- text: "任务执行异常,请重试~",
315
- append: true,
316
- final: true,
317
- errorCode: 99921111,
318
- errorMessage: "任务执行异常,请重试",
319
- });
320
- finalSent = true;
321
- scopedLog().log(`[ON-IDLE] Sent error response, code=99921111`);
322
- }
323
- catch (err) {
324
- scopedLog().error(`[ON-IDLE] Failed to send error response:`, err);
325
338
  }
326
339
  }
327
- stopStatusInterval();
328
- // 🔑 清理必须在 onIdle 内部完成,因为 openclaw 的 waitForIdle() 不会
329
- // await onIdle 返回的 Promise(源码中为 void options.onIdle?.()),
330
- // 导致 onSettled onIdle async 工作完成之前就执行。
331
- await onIdleComplete?.();
340
+ finally {
341
+ stopStatusInterval();
342
+ // 🔑 清理必须在 onIdle 内部完成,因为 openclaw waitForIdle() 不会
343
+ // await onIdle 返回的 Promise(源码中为 void options.onIdle?.()),
344
+ // 导致 onSettled 在 onIdle 的 async 工作完成之前就执行。
345
+ await onIdleComplete?.();
346
+ }
332
347
  },
333
348
  onCleanup: () => {
334
349
  scopedLog().log(`[ON-CLEANUP] Reply cleanup, steered=${steerState.steered}`);
@@ -453,5 +468,6 @@ export function createXYReplyDispatcher(params) {
453
468
  markDispatchIdle,
454
469
  startStatusInterval,
455
470
  stopStatusInterval,
471
+ updateFallbackTaskId,
456
472
  };
457
473
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynhcj/xiaoyi-channel",
3
- "version": "0.0.175-next",
3
+ "version": "0.0.176-next",
4
4
  "description": "OpenClaw Xiaoyi Channel plugin - Xiaoyi A2A protocol integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",