@ynhcj/xiaoyi-channel 0.0.136-next → 0.0.137-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
@@ -204,11 +204,19 @@ export async function handleXYMessage(params) {
204
204
  // 回调,onInitComplete 永远不会被触发。如果不释放,后续消息
205
205
  // 会被 globalDispatchInitGate 永久阻塞。
206
206
  params.onInitComplete?.();
207
+ // Steer 也支持文件 —— 提取并下载,附带到 mediaPayload
208
+ const steerFileParts = extractFileParts(parsed.parts);
209
+ const steerDownloadedFiles = await downloadFilesFromParts(steerFileParts);
210
+ const steerMediaPayload = buildXYMediaPayload(steerDownloadedFiles);
211
+ if (steerFileParts.length > 0) {
212
+ logger.log(`[BOT] 📎 Steer message with files: ${steerFileParts.length} file(s)`);
213
+ }
207
214
  logger.log(`[BOT] 🔄 Steer message — enqueuing to streaming-signal queue`);
208
215
  await enqueueSteer({
209
216
  sessionId: parsed.sessionId,
210
217
  sessionKey: route.sessionKey,
211
218
  steerText: textForAgent, // 原始文本,不带 /steer 前缀
219
+ mediaPayload: steerMediaPayload,
212
220
  cfg,
213
221
  runtime,
214
222
  parsed,
@@ -309,6 +317,7 @@ export async function handleXYMessage(params) {
309
317
  logger.log(`[BOT] ✅ Steered dispatch settled (skipping cleanup)`);
310
318
  return;
311
319
  }
320
+ streamingSignals.delete(parsed.sessionId);
312
321
  decrementTaskIdRef(parsed.sessionId);
313
322
  unregisterSession(route.sessionKey);
314
323
  logger.log(`[BOT] ✅ Cleanup completed`);
@@ -421,7 +430,8 @@ const steerQueues = _g.__xySteerQueues;
421
430
  export function notifyModelStreaming(sessionId) {
422
431
  const signal = streamingSignals.get(sessionId);
423
432
  if (signal) {
424
- streamingSignals.delete(sessionId);
433
+ // 不删除 signal——后续 steer 需要靠它判断模型已在 streaming。
434
+ // 清理由第一条消息的 onSettled 兜底。
425
435
  signal.notify();
426
436
  logger.log(`[STEER-QUEUE] 📡 Model streaming signal fired for session=${sessionId}`);
427
437
  }
@@ -446,7 +456,9 @@ function enqueueSteer(params) {
446
456
  const next = (prev ?? Promise.resolve()).then(() => dispatchSteerWhenReady(params));
447
457
  steerQueues.set(sessionId, next);
448
458
  // 链条结束后清理
449
- next.catch(() => { }).finally(() => {
459
+ next.catch((err) => {
460
+ logger.error(`[STEER-QUEUE] ❌ Steer chain failed: ${String(err)}`);
461
+ }).finally(() => {
450
462
  if (steerQueues.get(sessionId) === next) {
451
463
  steerQueues.delete(sessionId);
452
464
  }
@@ -475,11 +487,13 @@ async function dispatchSteerWhenReady(params) {
475
487
  if (signal) {
476
488
  logger.log(`[STEER-QUEUE] ⏳ Waiting for streaming signal, session=${sessionId}`);
477
489
  await signal.promise;
478
- streamingSignals.delete(sessionId);
479
490
  logger.log(`[STEER-QUEUE] ✅ Streaming signal received, session=${sessionId}`);
480
491
  }
481
492
  else {
482
- logger.log(`[STEER-QUEUE] ⚠️ Signal never appeared, proceeding without wait`);
493
+ // 轮询超时且 hasActiveTask 仍为 true——说明第一条消息可能卡在异常路径,
494
+ // 没有创建 signal。此时 dispatch 会与第一条消息的模型调用并发冲突,放弃。
495
+ logger.log(`[STEER-QUEUE] ⚠️ Signal never appeared after polling, skip steer to avoid collision`);
496
+ return;
483
497
  }
484
498
  // 2. 第一条消息已结束 → 放弃
485
499
  if (!hasActiveTask(sessionId)) {
@@ -489,7 +503,12 @@ async function dispatchSteerWhenReady(params) {
489
503
  // 3. 构建 dispatch 上下文并 dispatch /steer
490
504
  const core = getXYRuntime();
491
505
  const speaker = sessionId;
492
- const steerCommand = `/steer ${steerText}`;
506
+ // 如果有文件附件,把路径拼到 steer 文本末尾,让模型通过工具读取
507
+ const mediaPaths = params.mediaPayload?.MediaPaths;
508
+ const fileHint = mediaPaths && mediaPaths.length > 0
509
+ ? `\n【用户上传附件】:${JSON.stringify(mediaPaths)}`
510
+ : "";
511
+ const steerCommand = `/steer ${steerText}${fileHint}`;
493
512
  const messageBody = `${speaker}: ${steerCommand}`;
494
513
  const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(params.cfg);
495
514
  const body = core.channel.reply.formatAgentEnvelope({
@@ -520,6 +539,7 @@ async function dispatchSteerWhenReady(params) {
520
539
  OriginatingChannel: "xiaoyi-channel",
521
540
  OriginatingTo: sessionId,
522
541
  ReplyToBody: undefined,
542
+ ...params.mediaPayload,
523
543
  });
524
544
  const steerState = { steered: true };
525
545
  const { dispatcher, replyOptions } = createXYReplyDispatcher({
@@ -46,6 +46,11 @@ function getFirstUserText(messages) {
46
46
  }
47
47
  /** Regex to match `[cron:<uuid> <title>]` anywhere in text. */
48
48
  const CRON_TAG_RE = /\[cron:[^\s\]]+\s+([^\]]+)\]/;
49
+ /** Extract the cron job UUID from the first user message, e.g. `[cron:abc123 ...]` → `abc123`. */
50
+ function extractCronUuid(messages) {
51
+ const match = getFirstUserText(messages).match(/\[cron:([^\s\]]+)/i);
52
+ return match ? match[1] : undefined;
53
+ }
49
54
  /** Check if the request is triggered by a cron job by inspecting the first user message. */
50
55
  function isCronTriggered(messages) {
51
56
  return /\[cron:/i.test(getFirstUserText(messages));
@@ -527,12 +532,29 @@ export const xiaoyiProvider = {
527
532
  const traceId = ctx.extraParams[HEADER_TRACE_ID];
528
533
  const sessionId = ctx.extraParams[HEADER_SESSION_ID];
529
534
  const interactionId = ctx.extraParams[HEADER_INTERACTION_ID];
530
- if (typeof traceId === "string")
531
- dynamicHeaders[HEADER_TRACE_ID] = traceId;
532
- if (typeof sessionId === "string")
533
- dynamicHeaders[HEADER_SESSION_ID] = sessionId;
534
- if (typeof interactionId === "string")
535
- dynamicHeaders[HEADER_INTERACTION_ID] = interactionId;
535
+ const isCronCached = isCronTriggered(context.messages);
536
+ if (isCronCached) {
537
+ // Cron: generate fresh sessionId from cron UUID so each invocation
538
+ // is independently tracked, regardless of stale activeSessions state.
539
+ const cronUuid = extractCronUuid(context.messages) ?? "cron";
540
+ const cronSessionId = `cron_${cronUuid}_${Date.now()}`;
541
+ dynamicHeaders[HEADER_TRACE_ID] = cronSessionId;
542
+ dynamicHeaders[HEADER_SESSION_ID] = cronUuid;
543
+ dynamicHeaders[HEADER_INTERACTION_ID] = cronSessionId;
544
+ const cronTitle = extractCronTitle(context.messages);
545
+ if (cronTitle)
546
+ dynamicHeaders["x-cron-title"] = encodeURIComponent(cronTitle);
547
+ if (context.messages?.length === 1)
548
+ dynamicHeaders["x-cron-flag"] = "begin";
549
+ }
550
+ else {
551
+ if (typeof traceId === "string")
552
+ dynamicHeaders[HEADER_TRACE_ID] = traceId;
553
+ if (typeof sessionId === "string")
554
+ dynamicHeaders[HEADER_SESSION_ID] = sessionId;
555
+ if (typeof interactionId === "string")
556
+ dynamicHeaders[HEADER_INTERACTION_ID] = interactionId;
557
+ }
536
558
  }
537
559
  }
538
560
  // 记录输入
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynhcj/xiaoyi-channel",
3
- "version": "0.0.136-next",
3
+ "version": "0.0.137-next",
4
4
  "description": "OpenClaw Xiaoyi Channel plugin - Xiaoyi A2A protocol integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",