@ynhcj/xiaoyi-channel 0.0.139-beta → 0.0.140-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 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消息加 /steer 前缀,触发core的 queueEmbeddedPiMessage
201
- if (isUpdate && textForAgent) {
202
- textForAgent = `/steer ${textForAgent}`;
203
- logger.log(`[BOT] 🔄 Prepended /steer for steer injection`);
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
- // 🔑 Dynamic steer state: when isUpdate (second message), start as steered=true
254
- // so the dispatcher skips all user-facing callbacks (deliver, onIdle, etc.)
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,158 @@ 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(() => { }).finally(() => {
450
+ if (steerQueues.get(sessionId) === next) {
451
+ steerQueues.delete(sessionId);
452
+ }
453
+ });
454
+ return next;
455
+ }
456
+ async function dispatchSteerWhenReady(params) {
457
+ const { sessionId, sessionKey, steerText } = params;
458
+ // 1. 等待第一条消息开始 streaming
459
+ // signal 可能尚未创建(第一条消息还在文件下载等耗时操作中),
460
+ // 轮询等待直到 signal 出现,最長等待 ~5 秒。
461
+ let signal = streamingSignals.get(sessionId);
462
+ if (!signal) {
463
+ logger.log(`[STEER-QUEUE] ⏳ Signal not yet created, polling for session=${sessionId}`);
464
+ for (let i = 0; i < 50; i++) {
465
+ await new Promise(r => setTimeout(r, 100));
466
+ signal = streamingSignals.get(sessionId);
467
+ if (signal)
468
+ break;
469
+ if (!hasActiveTask(sessionId)) {
470
+ logger.log(`[STEER-QUEUE] ℹ️ First message completed while waiting, skip steer`);
471
+ return;
472
+ }
473
+ }
474
+ }
475
+ if (signal) {
476
+ logger.log(`[STEER-QUEUE] ⏳ Waiting for streaming signal, session=${sessionId}`);
477
+ await signal.promise;
478
+ streamingSignals.delete(sessionId);
479
+ logger.log(`[STEER-QUEUE] ✅ Streaming signal received, session=${sessionId}`);
480
+ }
481
+ else {
482
+ logger.log(`[STEER-QUEUE] ⚠️ Signal never appeared, proceeding without wait`);
483
+ }
484
+ // 2. 第一条消息已结束 → 放弃
485
+ if (!hasActiveTask(sessionId)) {
486
+ logger.log(`[STEER-QUEUE] ℹ️ First message completed, skip steer`);
487
+ return;
488
+ }
489
+ // 3. 构建 dispatch 上下文并 dispatch /steer
490
+ const core = getXYRuntime();
491
+ const speaker = sessionId;
492
+ const steerCommand = `/steer ${steerText}`;
493
+ const messageBody = `${speaker}: ${steerCommand}`;
494
+ const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(params.cfg);
495
+ const body = core.channel.reply.formatAgentEnvelope({
496
+ channel: "xiaoyi-channel",
497
+ from: speaker,
498
+ timestamp: new Date(),
499
+ envelope: envelopeOptions,
500
+ body: messageBody,
501
+ });
502
+ const ctxPayload = core.channel.reply.finalizeInboundContext({
503
+ Body: body,
504
+ RawBody: steerCommand,
505
+ CommandBody: steerCommand,
506
+ From: sessionId,
507
+ To: sessionId,
508
+ SessionKey: params.route.sessionKey,
509
+ AccountId: params.route.accountId,
510
+ ChatType: "direct",
511
+ GroupSubject: undefined,
512
+ SenderName: sessionId,
513
+ SenderId: sessionId,
514
+ Provider: "xiaoyi-channel",
515
+ Surface: "xiaoyi-channel",
516
+ MessageSid: `${params.parsed.taskId}_${params.deviceType}`,
517
+ Timestamp: Date.now(),
518
+ WasMentioned: false,
519
+ CommandAuthorized: true,
520
+ OriginatingChannel: "xiaoyi-channel",
521
+ OriginatingTo: sessionId,
522
+ ReplyToBody: undefined,
523
+ });
524
+ const steerState = { steered: true };
525
+ const { dispatcher, replyOptions } = createXYReplyDispatcher({
526
+ cfg: params.cfg,
527
+ runtime: params.runtime,
528
+ sessionId,
529
+ taskId: params.parsed.taskId,
530
+ messageId: params.parsed.messageId,
531
+ accountId: params.route.accountId,
532
+ steerState,
533
+ });
534
+ const sessionContext = {
535
+ config: resolveXYConfig(params.cfg),
536
+ sessionId,
537
+ taskId: params.parsed.taskId,
538
+ messageId: params.parsed.messageId,
539
+ agentId: params.route.accountId,
540
+ deviceType: params.deviceType,
541
+ };
542
+ logger.log(`[STEER-QUEUE] 🚀 Dispatching steer for session=${sessionId}`);
543
+ await core.channel.reply.withReplyDispatcher({
544
+ dispatcher,
545
+ onSettled: () => {
546
+ logger.log(`[STEER-QUEUE] 🏁 Steer dispatch settled for session=${sessionId}`);
547
+ },
548
+ run: () => {
549
+ return runWithSessionContext(sessionContext, async () => {
550
+ const result = await core.channel.reply.dispatchReplyFromConfig({
551
+ ctx: ctxPayload,
552
+ cfg: params.cfg,
553
+ dispatcher,
554
+ replyOptions,
555
+ });
556
+ logger.log(`[STEER-QUEUE] dispatch result: ${JSON.stringify(result)}`);
557
+ return result;
558
+ });
559
+ },
560
+ });
561
+ logger.log(`[STEER-QUEUE] ✅ Steer dispatch completed for session=${sessionId}`);
562
+ }
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynhcj/xiaoyi-channel",
3
- "version": "0.0.139-beta",
3
+ "version": "0.0.140-beta",
4
4
  "description": "OpenClaw Xiaoyi Channel plugin - Xiaoyi A2A protocol integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",