@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 +16 -4
- package/dist/src/reply-dispatcher.js +101 -85
- package/package.json +1 -1
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
|
-
//
|
|
396
|
-
//
|
|
397
|
-
|
|
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
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
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) ??
|
|
106
|
+
return getCurrentTaskId(sessionId) ?? currentFallbackTaskId;
|
|
106
107
|
};
|
|
107
108
|
const getActiveMessageId = () => {
|
|
108
|
-
return getCurrentMessageId(sessionId) ??
|
|
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
|
-
//
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
}
|