@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 +25 -5
- package/dist/src/provider.js +28 -6
- package/package.json +1 -1
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
|
-
|
|
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(() => {
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
package/dist/src/provider.js
CHANGED
|
@@ -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
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
// 记录输入
|