@ynhcj/xiaoyi-channel 0.0.131-next → 0.0.132-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 +116 -107
- package/dist/src/reply-dispatcher.d.ts +2 -1
- package/dist/src/reply-dispatcher.js +8 -9
- package/package.json +1 -1
package/dist/src/bot.js
CHANGED
|
@@ -254,6 +254,9 @@ export async function handleXYMessage(params) {
|
|
|
254
254
|
// so the dispatcher skips all user-facing callbacks (deliver, onIdle, etc.)
|
|
255
255
|
// and onSettled skips cleanup.
|
|
256
256
|
const steerState = { steered: isUpdate };
|
|
257
|
+
// 🔑 第一条消息的 streaming 信号:deliver 首次触发时 resolve
|
|
258
|
+
// steer 消息通过串行队列等待此信号后再 dispatch
|
|
259
|
+
const streamingSignal = !isUpdate ? createStreamingSignal(parsed.sessionId) : undefined;
|
|
257
260
|
// 🔑 创建dispatcher
|
|
258
261
|
logger.log(`[BOT-DISPATCHER] 🎯 Creating reply dispatcher`);
|
|
259
262
|
logger.log(`[BOT-DISPATCHER] - taskId: ${parsed.taskId}`);
|
|
@@ -265,6 +268,7 @@ export async function handleXYMessage(params) {
|
|
|
265
268
|
messageId: parsed.messageId,
|
|
266
269
|
accountId: route.accountId,
|
|
267
270
|
steerState,
|
|
271
|
+
onFirstStream: streamingSignal?.notify,
|
|
268
272
|
});
|
|
269
273
|
// Steer injections don't need status intervals
|
|
270
274
|
if (!skipReg) {
|
|
@@ -331,14 +335,12 @@ export async function handleXYMessage(params) {
|
|
|
331
335
|
return dispatchPromise;
|
|
332
336
|
},
|
|
333
337
|
});
|
|
334
|
-
// 🔑 Steer
|
|
338
|
+
// 🔑 Steer 串行队列:等待 streaming 信号后 dispatch,多个 steer 按顺序处理
|
|
335
339
|
if (isUpdate) {
|
|
336
|
-
await
|
|
337
|
-
steerState,
|
|
340
|
+
await enqueueSteer({
|
|
338
341
|
sessionId: parsed.sessionId,
|
|
339
342
|
sessionKey: route.sessionKey,
|
|
340
343
|
steerText: textForAgent,
|
|
341
|
-
ctxPayload,
|
|
342
344
|
cfg,
|
|
343
345
|
runtime,
|
|
344
346
|
parsed,
|
|
@@ -401,114 +403,121 @@ function buildXYMediaPayload(mediaList) {
|
|
|
401
403
|
MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined,
|
|
402
404
|
};
|
|
403
405
|
}
|
|
404
|
-
const
|
|
406
|
+
const streamingSignals = new Map();
|
|
407
|
+
function createStreamingSignal(sessionId) {
|
|
408
|
+
let resolve;
|
|
409
|
+
const promise = new Promise(r => { resolve = r; });
|
|
410
|
+
const signal = { promise, notify: resolve };
|
|
411
|
+
streamingSignals.set(sessionId, signal);
|
|
412
|
+
logger.log(`[STEER-QUEUE] 🟢 Streaming signal created for session ${sessionId}`);
|
|
413
|
+
return signal;
|
|
414
|
+
}
|
|
415
|
+
/** Per-session 串行队列:保证同一 session 的 steer 消息按顺序处理 */
|
|
416
|
+
const steerQueues = new Map();
|
|
405
417
|
/**
|
|
406
|
-
*
|
|
407
|
-
*
|
|
418
|
+
* 将 steer 消息放入 per-session 串行队列。
|
|
419
|
+
* 等待第一条消息的 streaming 信号(deliver 首次触发),然后 dispatch。
|
|
420
|
+
* 多个 steer 按到达顺序串行处理,无需重试。
|
|
408
421
|
*/
|
|
409
|
-
|
|
410
|
-
const {
|
|
411
|
-
//
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
422
|
+
function enqueueSteer(params) {
|
|
423
|
+
const { sessionId } = params;
|
|
424
|
+
// 取出当前队列尾部(或 undefined),然后链上新的 Promise
|
|
425
|
+
const prev = steerQueues.get(sessionId);
|
|
426
|
+
const next = (prev ?? Promise.resolve()).then(() => dispatchSteerWhenReady(params));
|
|
427
|
+
steerQueues.set(sessionId, next);
|
|
428
|
+
// 链条结束后清理
|
|
429
|
+
next.catch(() => { }).finally(() => {
|
|
430
|
+
if (steerQueues.get(sessionId) === next) {
|
|
431
|
+
steerQueues.delete(sessionId);
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
return next;
|
|
435
|
+
}
|
|
436
|
+
async function dispatchSteerWhenReady(params) {
|
|
437
|
+
const { sessionId, sessionKey, steerText } = params;
|
|
438
|
+
// 1. 等待第一条消息开始 streaming
|
|
439
|
+
const signal = streamingSignals.get(sessionId);
|
|
440
|
+
if (signal) {
|
|
441
|
+
logger.log(`[STEER-QUEUE] ⏳ Waiting for streaming signal, session=${sessionId}`);
|
|
442
|
+
await signal.promise;
|
|
443
|
+
streamingSignals.delete(sessionId);
|
|
444
|
+
logger.log(`[STEER-QUEUE] ✅ Streaming signal received, session=${sessionId}`);
|
|
415
445
|
}
|
|
416
|
-
//
|
|
417
|
-
if (
|
|
446
|
+
// 2. 第一条消息已结束 → 放弃
|
|
447
|
+
if (!hasActiveTask(sessionId)) {
|
|
448
|
+
logger.log(`[STEER-QUEUE] ℹ️ First message completed, skip steer`);
|
|
418
449
|
return;
|
|
419
450
|
}
|
|
420
|
-
|
|
451
|
+
// 3. 构建 dispatch 上下文并 dispatch /steer
|
|
421
452
|
const core = getXYRuntime();
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
},
|
|
489
|
-
run: () => {
|
|
490
|
-
return runWithSessionContext(sessionContext, async () => {
|
|
491
|
-
const result = await core.channel.reply.dispatchReplyFromConfig({
|
|
492
|
-
ctx: retryCtx,
|
|
493
|
-
cfg: params.cfg,
|
|
494
|
-
dispatcher: retryDispatcher,
|
|
495
|
-
replyOptions: retryReplyOptions,
|
|
496
|
-
});
|
|
497
|
-
logger.log(`[STEER-RETRY] dispatch result: ${JSON.stringify(result)}`);
|
|
498
|
-
return result;
|
|
499
|
-
});
|
|
500
|
-
},
|
|
453
|
+
const speaker = sessionId;
|
|
454
|
+
const messageBody = `${speaker}: ${steerText}`;
|
|
455
|
+
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(params.cfg);
|
|
456
|
+
const body = core.channel.reply.formatAgentEnvelope({
|
|
457
|
+
channel: "xiaoyi-channel",
|
|
458
|
+
from: speaker,
|
|
459
|
+
timestamp: new Date(),
|
|
460
|
+
envelope: envelopeOptions,
|
|
461
|
+
body: messageBody,
|
|
462
|
+
});
|
|
463
|
+
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
464
|
+
Body: body,
|
|
465
|
+
RawBody: steerText,
|
|
466
|
+
CommandBody: steerText,
|
|
467
|
+
From: sessionId,
|
|
468
|
+
To: sessionId,
|
|
469
|
+
SessionKey: params.route.sessionKey,
|
|
470
|
+
AccountId: params.route.accountId,
|
|
471
|
+
ChatType: "direct",
|
|
472
|
+
GroupSubject: undefined,
|
|
473
|
+
SenderName: sessionId,
|
|
474
|
+
SenderId: sessionId,
|
|
475
|
+
Provider: "xiaoyi-channel",
|
|
476
|
+
Surface: "xiaoyi-channel",
|
|
477
|
+
MessageSid: `${params.parsed.taskId}_${params.deviceType}`,
|
|
478
|
+
Timestamp: Date.now(),
|
|
479
|
+
WasMentioned: false,
|
|
480
|
+
CommandAuthorized: true,
|
|
481
|
+
OriginatingChannel: "xiaoyi-channel",
|
|
482
|
+
OriginatingTo: sessionId,
|
|
483
|
+
ReplyToBody: undefined,
|
|
484
|
+
});
|
|
485
|
+
const steerState = { steered: true };
|
|
486
|
+
const { dispatcher, replyOptions } = createXYReplyDispatcher({
|
|
487
|
+
cfg: params.cfg,
|
|
488
|
+
runtime: params.runtime,
|
|
489
|
+
sessionId,
|
|
490
|
+
taskId: params.parsed.taskId,
|
|
491
|
+
messageId: params.parsed.messageId,
|
|
492
|
+
accountId: params.route.accountId,
|
|
493
|
+
steerState,
|
|
494
|
+
});
|
|
495
|
+
const sessionContext = {
|
|
496
|
+
config: resolveXYConfig(params.cfg),
|
|
497
|
+
sessionId,
|
|
498
|
+
taskId: params.parsed.taskId,
|
|
499
|
+
messageId: params.parsed.messageId,
|
|
500
|
+
agentId: params.route.accountId,
|
|
501
|
+
deviceType: params.deviceType,
|
|
502
|
+
};
|
|
503
|
+
logger.log(`[STEER-QUEUE] 🚀 Dispatching steer for session=${sessionId}`);
|
|
504
|
+
await core.channel.reply.withReplyDispatcher({
|
|
505
|
+
dispatcher,
|
|
506
|
+
onSettled: () => {
|
|
507
|
+
logger.log(`[STEER-QUEUE] 🏁 Steer dispatch settled for session=${sessionId}`);
|
|
508
|
+
},
|
|
509
|
+
run: () => {
|
|
510
|
+
return runWithSessionContext(sessionContext, async () => {
|
|
511
|
+
const result = await core.channel.reply.dispatchReplyFromConfig({
|
|
512
|
+
ctx: ctxPayload,
|
|
513
|
+
cfg: params.cfg,
|
|
514
|
+
dispatcher,
|
|
515
|
+
replyOptions,
|
|
516
|
+
});
|
|
517
|
+
logger.log(`[STEER-QUEUE] dispatch result: ${JSON.stringify(result)}`);
|
|
518
|
+
return result;
|
|
501
519
|
});
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
continue;
|
|
506
|
-
}
|
|
507
|
-
if (retryState.steerResult === 'success') {
|
|
508
|
-
logger.log(`[STEER-RETRY] ✅ Steer succeeded on retry #${attempt + 1}`);
|
|
509
|
-
return;
|
|
510
|
-
}
|
|
511
|
-
logger.log(`[STEER-RETRY] ⚠️ Retry #${attempt + 1} result=${retryState.steerResult}, continuing`);
|
|
512
|
-
}
|
|
513
|
-
logger.warn(`[STEER-RETRY] ❌ All retries exhausted for session ${sessionId}`);
|
|
520
|
+
},
|
|
521
|
+
});
|
|
522
|
+
logger.log(`[STEER-QUEUE] ✅ Steer dispatch completed for session=${sessionId}`);
|
|
514
523
|
}
|
|
@@ -8,8 +8,9 @@ export interface CreateXYReplyDispatcherParams {
|
|
|
8
8
|
accountId: string;
|
|
9
9
|
steerState: {
|
|
10
10
|
steered: boolean;
|
|
11
|
-
steerResult?: 'success' | 'fail';
|
|
12
11
|
};
|
|
12
|
+
/** Called the first time deliver fires for a non-steered dispatch — signals the model is streaming. */
|
|
13
|
+
onFirstStream?: () => void;
|
|
13
14
|
}
|
|
14
15
|
/**
|
|
15
16
|
* 清理 /tmp/xy_channel 目录中超过 24 小时的旧文件
|
|
@@ -45,7 +45,7 @@ export async function cleanupStaleTempFiles(tempDir = "/tmp/xy_channel") {
|
|
|
45
45
|
* Runtime is expected to be validated before calling this function.
|
|
46
46
|
*/
|
|
47
47
|
export function createXYReplyDispatcher(params) {
|
|
48
|
-
const { cfg, runtime, sessionId, taskId, messageId, accountId, steerState } = params;
|
|
48
|
+
const { cfg, runtime, sessionId, taskId, messageId, accountId, steerState, onFirstStream } = params;
|
|
49
49
|
logger.log(`[DISPATCHER-CREATE] ******* Creating dispatcher *******`);
|
|
50
50
|
logger.log(`[DISPATCHER-CREATE] - taskId: ${taskId}`);
|
|
51
51
|
// 初始taskId和messageId(作为fallback)
|
|
@@ -73,6 +73,7 @@ export function createXYReplyDispatcher(params) {
|
|
|
73
73
|
let hasSentResponse = false;
|
|
74
74
|
let finalSent = false;
|
|
75
75
|
let accumulatedText = "";
|
|
76
|
+
let streamingSignaled = false;
|
|
76
77
|
/**
|
|
77
78
|
* Start the status update interval
|
|
78
79
|
*/
|
|
@@ -115,16 +116,14 @@ export function createXYReplyDispatcher(params) {
|
|
|
115
116
|
deliver: async (payload, info) => {
|
|
116
117
|
// 🔑 steered dispatch不发送内容(让主dispatcher处理)
|
|
117
118
|
if (steerState.steered) {
|
|
118
|
-
|
|
119
|
-
if (text.includes('steered current session')) {
|
|
120
|
-
steerState.steerResult = 'success';
|
|
121
|
-
}
|
|
122
|
-
else if (text.includes('not accepting steering') || text.includes('No active run')) {
|
|
123
|
-
steerState.steerResult = 'fail';
|
|
124
|
-
}
|
|
125
|
-
logger.log(`[DELIVER] Steered dispatch - result=${steerState.steerResult}, info.kind=${info?.kind}, text=${text.slice(0, 80)}`);
|
|
119
|
+
logger.log(`[DELIVER] Steered dispatch - skipping deliver, info.kind=${info?.kind}`);
|
|
126
120
|
return;
|
|
127
121
|
}
|
|
122
|
+
// 🔑 第一次 deliver = 模型开始 streaming,通知等待中的 steer
|
|
123
|
+
if (onFirstStream && !streamingSignaled) {
|
|
124
|
+
streamingSignaled = true;
|
|
125
|
+
onFirstStream();
|
|
126
|
+
}
|
|
128
127
|
const text = payload.text ?? "";
|
|
129
128
|
const currentTaskId = getActiveTaskId();
|
|
130
129
|
const currentMessageId = getActiveMessageId();
|