@ynhcj/xiaoyi-channel 0.0.139-beta → 0.0.141-beta
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.d.ts +5 -0
- package/dist/src/bot.js +187 -8
- package/dist/src/provider.js +6 -0
- package/package.json +1 -1
package/dist/src/bot.d.ts
CHANGED
|
@@ -24,3 +24,8 @@ export interface HandleXYMessageParams {
|
|
|
24
24
|
* Runtime is expected to be validated before calling this function.
|
|
25
25
|
*/
|
|
26
26
|
export declare function handleXYMessage(params: HandleXYMessageParams): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* 由 provider.ts 在 wrapStreamFn 调用时触发。
|
|
29
|
+
* 这是模型 API 被调用的精确时刻,此时 isStreaming 一定为 true。
|
|
30
|
+
*/
|
|
31
|
+
export declare function notifyModelStreaming(sessionId: string): void;
|
package/dist/src/bot.js
CHANGED
|
@@ -197,11 +197,32 @@ export async function handleXYMessage(params) {
|
|
|
197
197
|
logger.error(`[SELF_EVOLUTION] Failed to append inline keyword nudge: ${String(selfEvolutionError)}`);
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
|
-
// 🔑 Steer
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
200
|
+
// 🔑 Steer消息: 跳过旧路径直接进入 streaming-signal 队列
|
|
201
|
+
// /steer 前缀由 dispatchSteerWhenReady 内部添加
|
|
202
|
+
if (isUpdate) {
|
|
203
|
+
// 立即释放 init gate——steer 不走 withReplyDispatcher 的 run()
|
|
204
|
+
// 回调,onInitComplete 永远不会被触发。如果不释放,后续消息
|
|
205
|
+
// 会被 globalDispatchInitGate 永久阻塞。
|
|
206
|
+
params.onInitComplete?.();
|
|
207
|
+
logger.log(`[BOT] 🔄 Steer message — enqueuing to streaming-signal queue`);
|
|
208
|
+
await enqueueSteer({
|
|
209
|
+
sessionId: parsed.sessionId,
|
|
210
|
+
sessionKey: route.sessionKey,
|
|
211
|
+
steerText: textForAgent, // 原始文本,不带 /steer 前缀
|
|
212
|
+
cfg,
|
|
213
|
+
runtime,
|
|
214
|
+
parsed,
|
|
215
|
+
route,
|
|
216
|
+
deviceType,
|
|
217
|
+
});
|
|
218
|
+
logger.log(`[BOT] ✅ Steer queue completed for session: ${parsed.sessionId}`);
|
|
219
|
+
logger.log(`xy: dispatch complete (session=${parsed.sessionId})`);
|
|
220
|
+
return;
|
|
204
221
|
}
|
|
222
|
+
// ── First message (non-steer) path below ──────────────────────
|
|
223
|
+
// 🔑 立即创建 streaming 信号——必须在文件下载等耗时操作之前,
|
|
224
|
+
// 否则 steer 消息的 dispatchSteerWhenReady 会找不到信号而跳过等待。
|
|
225
|
+
createStreamingSignal(parsed.sessionId);
|
|
205
226
|
// File download — only for real user messages, steer injections have no files
|
|
206
227
|
let mediaPayload = {};
|
|
207
228
|
if (!skipReg) {
|
|
@@ -250,10 +271,8 @@ export async function handleXYMessage(params) {
|
|
|
250
271
|
ReplyToBody: undefined, // A2A protocol doesn't support reply/quote
|
|
251
272
|
...mediaPayload,
|
|
252
273
|
});
|
|
253
|
-
// 🔑
|
|
254
|
-
|
|
255
|
-
// and onSettled skips cleanup.
|
|
256
|
-
const steerState = { steered: isUpdate };
|
|
274
|
+
// 🔑 Streaming 信号已在上方创建(在文件下载之前)
|
|
275
|
+
const steerState = { steered: false };
|
|
257
276
|
// 🔑 创建dispatcher
|
|
258
277
|
logger.log(`[BOT-DISPATCHER] 🎯 Creating reply dispatcher`);
|
|
259
278
|
logger.log(`[BOT-DISPATCHER] - taskId: ${parsed.taskId}`);
|
|
@@ -386,3 +405,163 @@ function buildXYMediaPayload(mediaList) {
|
|
|
386
405
|
MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined,
|
|
387
406
|
};
|
|
388
407
|
}
|
|
408
|
+
// Use globalThis to survive module deduplication — provider.ts may load a
|
|
409
|
+
// different copy of bot.ts, so a plain module-level Map would be two objects.
|
|
410
|
+
const _g = globalThis;
|
|
411
|
+
if (!_g.__xyStreamingSignals)
|
|
412
|
+
_g.__xyStreamingSignals = new Map();
|
|
413
|
+
if (!_g.__xySteerQueues)
|
|
414
|
+
_g.__xySteerQueues = new Map();
|
|
415
|
+
const streamingSignals = _g.__xyStreamingSignals;
|
|
416
|
+
const steerQueues = _g.__xySteerQueues;
|
|
417
|
+
/**
|
|
418
|
+
* 由 provider.ts 在 wrapStreamFn 调用时触发。
|
|
419
|
+
* 这是模型 API 被调用的精确时刻,此时 isStreaming 一定为 true。
|
|
420
|
+
*/
|
|
421
|
+
export function notifyModelStreaming(sessionId) {
|
|
422
|
+
const signal = streamingSignals.get(sessionId);
|
|
423
|
+
if (signal) {
|
|
424
|
+
streamingSignals.delete(sessionId);
|
|
425
|
+
signal.notify();
|
|
426
|
+
logger.log(`[STEER-QUEUE] 📡 Model streaming signal fired for session=${sessionId}`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
function createStreamingSignal(sessionId) {
|
|
430
|
+
let resolve;
|
|
431
|
+
const promise = new Promise(r => { resolve = r; });
|
|
432
|
+
const signal = { promise, notify: resolve };
|
|
433
|
+
streamingSignals.set(sessionId, signal);
|
|
434
|
+
logger.log(`[STEER-QUEUE] 🟢 Streaming signal created for session ${sessionId}`);
|
|
435
|
+
return signal;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* 将 steer 消息放入 per-session 串行队列。
|
|
439
|
+
* 等待第一条消息的 streaming 信号(deliver 首次触发),然后 dispatch。
|
|
440
|
+
* 多个 steer 按到达顺序串行处理,无需重试。
|
|
441
|
+
*/
|
|
442
|
+
function enqueueSteer(params) {
|
|
443
|
+
const { sessionId } = params;
|
|
444
|
+
// 取出当前队列尾部(或 undefined),然后链上新的 Promise
|
|
445
|
+
const prev = steerQueues.get(sessionId);
|
|
446
|
+
const next = (prev ?? Promise.resolve()).then(() => dispatchSteerWhenReady(params));
|
|
447
|
+
steerQueues.set(sessionId, next);
|
|
448
|
+
// 链条结束后清理
|
|
449
|
+
next.catch((err) => {
|
|
450
|
+
logger.error(`[STEER-QUEUE] ❌ Steer chain failed: ${String(err)}`);
|
|
451
|
+
}).finally(() => {
|
|
452
|
+
if (steerQueues.get(sessionId) === next) {
|
|
453
|
+
steerQueues.delete(sessionId);
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
return next;
|
|
457
|
+
}
|
|
458
|
+
async function dispatchSteerWhenReady(params) {
|
|
459
|
+
const { sessionId, sessionKey, steerText } = params;
|
|
460
|
+
// 1. 等待第一条消息开始 streaming
|
|
461
|
+
// signal 可能尚未创建(第一条消息还在文件下载等耗时操作中),
|
|
462
|
+
// 轮询等待直到 signal 出现,最長等待 ~5 秒。
|
|
463
|
+
let signal = streamingSignals.get(sessionId);
|
|
464
|
+
if (!signal) {
|
|
465
|
+
logger.log(`[STEER-QUEUE] ⏳ Signal not yet created, polling for session=${sessionId}`);
|
|
466
|
+
for (let i = 0; i < 50; i++) {
|
|
467
|
+
await new Promise(r => setTimeout(r, 100));
|
|
468
|
+
signal = streamingSignals.get(sessionId);
|
|
469
|
+
if (signal)
|
|
470
|
+
break;
|
|
471
|
+
if (!hasActiveTask(sessionId)) {
|
|
472
|
+
logger.log(`[STEER-QUEUE] ℹ️ First message completed while waiting, skip steer`);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
if (signal) {
|
|
478
|
+
logger.log(`[STEER-QUEUE] ⏳ Waiting for streaming signal, session=${sessionId}`);
|
|
479
|
+
await signal.promise;
|
|
480
|
+
streamingSignals.delete(sessionId);
|
|
481
|
+
logger.log(`[STEER-QUEUE] ✅ Streaming signal received, session=${sessionId}`);
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
// 轮询超时且 hasActiveTask 仍为 true——说明第一条消息可能卡在异常路径,
|
|
485
|
+
// 没有创建 signal。此时 dispatch 会与第一条消息的模型调用并发冲突,放弃。
|
|
486
|
+
logger.log(`[STEER-QUEUE] ⚠️ Signal never appeared after polling, skip steer to avoid collision`);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
// 2. 第一条消息已结束 → 放弃
|
|
490
|
+
if (!hasActiveTask(sessionId)) {
|
|
491
|
+
logger.log(`[STEER-QUEUE] ℹ️ First message completed, skip steer`);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
// 3. 构建 dispatch 上下文并 dispatch /steer
|
|
495
|
+
const core = getXYRuntime();
|
|
496
|
+
const speaker = sessionId;
|
|
497
|
+
const steerCommand = `/steer ${steerText}`;
|
|
498
|
+
const messageBody = `${speaker}: ${steerCommand}`;
|
|
499
|
+
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(params.cfg);
|
|
500
|
+
const body = core.channel.reply.formatAgentEnvelope({
|
|
501
|
+
channel: "xiaoyi-channel",
|
|
502
|
+
from: speaker,
|
|
503
|
+
timestamp: new Date(),
|
|
504
|
+
envelope: envelopeOptions,
|
|
505
|
+
body: messageBody,
|
|
506
|
+
});
|
|
507
|
+
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
508
|
+
Body: body,
|
|
509
|
+
RawBody: steerCommand,
|
|
510
|
+
CommandBody: steerCommand,
|
|
511
|
+
From: sessionId,
|
|
512
|
+
To: sessionId,
|
|
513
|
+
SessionKey: params.route.sessionKey,
|
|
514
|
+
AccountId: params.route.accountId,
|
|
515
|
+
ChatType: "direct",
|
|
516
|
+
GroupSubject: undefined,
|
|
517
|
+
SenderName: sessionId,
|
|
518
|
+
SenderId: sessionId,
|
|
519
|
+
Provider: "xiaoyi-channel",
|
|
520
|
+
Surface: "xiaoyi-channel",
|
|
521
|
+
MessageSid: `${params.parsed.taskId}_${params.deviceType}`,
|
|
522
|
+
Timestamp: Date.now(),
|
|
523
|
+
WasMentioned: false,
|
|
524
|
+
CommandAuthorized: true,
|
|
525
|
+
OriginatingChannel: "xiaoyi-channel",
|
|
526
|
+
OriginatingTo: sessionId,
|
|
527
|
+
ReplyToBody: undefined,
|
|
528
|
+
});
|
|
529
|
+
const steerState = { steered: true };
|
|
530
|
+
const { dispatcher, replyOptions } = createXYReplyDispatcher({
|
|
531
|
+
cfg: params.cfg,
|
|
532
|
+
runtime: params.runtime,
|
|
533
|
+
sessionId,
|
|
534
|
+
taskId: params.parsed.taskId,
|
|
535
|
+
messageId: params.parsed.messageId,
|
|
536
|
+
accountId: params.route.accountId,
|
|
537
|
+
steerState,
|
|
538
|
+
});
|
|
539
|
+
const sessionContext = {
|
|
540
|
+
config: resolveXYConfig(params.cfg),
|
|
541
|
+
sessionId,
|
|
542
|
+
taskId: params.parsed.taskId,
|
|
543
|
+
messageId: params.parsed.messageId,
|
|
544
|
+
agentId: params.route.accountId,
|
|
545
|
+
deviceType: params.deviceType,
|
|
546
|
+
};
|
|
547
|
+
logger.log(`[STEER-QUEUE] 🚀 Dispatching steer for session=${sessionId}`);
|
|
548
|
+
await core.channel.reply.withReplyDispatcher({
|
|
549
|
+
dispatcher,
|
|
550
|
+
onSettled: () => {
|
|
551
|
+
logger.log(`[STEER-QUEUE] 🏁 Steer dispatch settled for session=${sessionId}`);
|
|
552
|
+
},
|
|
553
|
+
run: () => {
|
|
554
|
+
return runWithSessionContext(sessionContext, async () => {
|
|
555
|
+
const result = await core.channel.reply.dispatchReplyFromConfig({
|
|
556
|
+
ctx: ctxPayload,
|
|
557
|
+
cfg: params.cfg,
|
|
558
|
+
dispatcher,
|
|
559
|
+
replyOptions,
|
|
560
|
+
});
|
|
561
|
+
logger.log(`[STEER-QUEUE] dispatch result: ${JSON.stringify(result)}`);
|
|
562
|
+
return result;
|
|
563
|
+
});
|
|
564
|
+
},
|
|
565
|
+
});
|
|
566
|
+
logger.log(`[STEER-QUEUE] ✅ Steer dispatch completed for session=${sessionId}`);
|
|
567
|
+
}
|
package/dist/src/provider.js
CHANGED
|
@@ -11,6 +11,7 @@ import { createHash } from "crypto";
|
|
|
11
11
|
import { logger } from "./utils/logger.js";
|
|
12
12
|
import { getCurrentSessionContext } from "./tools/session-manager.js";
|
|
13
13
|
import { selfEvolutionManager } from "./utils/self-evolution-manager.js";
|
|
14
|
+
import { notifyModelStreaming } from "./bot.js";
|
|
14
15
|
// ── Retry config ──────────────────────────────────────────────
|
|
15
16
|
const RETRY_DELAYS_MS = [10_000, 20_000, 40_000, 60_000, 60_000];
|
|
16
17
|
const MAX_RETRY_ATTEMPTS = 5;
|
|
@@ -536,6 +537,11 @@ export const xiaoyiProvider = {
|
|
|
536
537
|
}
|
|
537
538
|
// 记录输入
|
|
538
539
|
logger.log(`[xiaoyiprovider] input messages count: ${context.messages?.length ?? 0}`);
|
|
540
|
+
// 🔑 通知 steer 队列:模型 API 已被调用,此时 isStreaming 一定为 true
|
|
541
|
+
const sessionCtx = getCurrentSessionContext();
|
|
542
|
+
if (sessionCtx?.sessionId) {
|
|
543
|
+
notifyModelStreaming(sessionCtx.sessionId);
|
|
544
|
+
}
|
|
539
545
|
if (context.systemPrompt) {
|
|
540
546
|
logger.log(`[xiaoyiprovider] system prompt length: ${context.systemPrompt.length}`);
|
|
541
547
|
}
|