@ynhcj/xiaoyi-channel 0.0.138-beta → 0.0.138-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.d.ts +5 -0
- package/dist/src/bot.js +203 -9
- package/dist/src/provider.js +56 -70
- package/dist/src/tools/session-manager.js +1 -1
- 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,40 @@ 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
|
+
// 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
|
+
}
|
|
214
|
+
logger.log(`[BOT] 🔄 Steer message — enqueuing to streaming-signal queue`);
|
|
215
|
+
await enqueueSteer({
|
|
216
|
+
sessionId: parsed.sessionId,
|
|
217
|
+
sessionKey: route.sessionKey,
|
|
218
|
+
steerText: textForAgent, // 原始文本,不带 /steer 前缀
|
|
219
|
+
mediaPayload: steerMediaPayload,
|
|
220
|
+
cfg,
|
|
221
|
+
runtime,
|
|
222
|
+
parsed,
|
|
223
|
+
route,
|
|
224
|
+
deviceType,
|
|
225
|
+
});
|
|
226
|
+
logger.log(`[BOT] ✅ Steer queue completed for session: ${parsed.sessionId}`);
|
|
227
|
+
logger.log(`xy: dispatch complete (session=${parsed.sessionId})`);
|
|
228
|
+
return;
|
|
204
229
|
}
|
|
230
|
+
// ── First message (non-steer) path below ──────────────────────
|
|
231
|
+
// 🔑 立即创建 streaming 信号——必须在文件下载等耗时操作之前,
|
|
232
|
+
// 否则 steer 消息的 dispatchSteerWhenReady 会找不到信号而跳过等待。
|
|
233
|
+
createStreamingSignal(parsed.sessionId);
|
|
205
234
|
// File download — only for real user messages, steer injections have no files
|
|
206
235
|
let mediaPayload = {};
|
|
207
236
|
if (!skipReg) {
|
|
@@ -241,7 +270,7 @@ export async function handleXYMessage(params) {
|
|
|
241
270
|
SenderId: parsed.sessionId,
|
|
242
271
|
Provider: "xiaoyi-channel",
|
|
243
272
|
Surface: "xiaoyi-channel",
|
|
244
|
-
MessageSid:
|
|
273
|
+
MessageSid: `xiaoyi_${parsed.taskId}_${deviceType}`,
|
|
245
274
|
Timestamp: Date.now(),
|
|
246
275
|
WasMentioned: false,
|
|
247
276
|
CommandAuthorized: true,
|
|
@@ -250,10 +279,8 @@ export async function handleXYMessage(params) {
|
|
|
250
279
|
ReplyToBody: undefined, // A2A protocol doesn't support reply/quote
|
|
251
280
|
...mediaPayload,
|
|
252
281
|
});
|
|
253
|
-
// 🔑
|
|
254
|
-
|
|
255
|
-
// and onSettled skips cleanup.
|
|
256
|
-
const steerState = { steered: isUpdate };
|
|
282
|
+
// 🔑 Streaming 信号已在上方创建(在文件下载之前)
|
|
283
|
+
const steerState = { steered: false };
|
|
257
284
|
// 🔑 创建dispatcher
|
|
258
285
|
logger.log(`[BOT-DISPATCHER] 🎯 Creating reply dispatcher`);
|
|
259
286
|
logger.log(`[BOT-DISPATCHER] - taskId: ${parsed.taskId}`);
|
|
@@ -290,6 +317,7 @@ export async function handleXYMessage(params) {
|
|
|
290
317
|
logger.log(`[BOT] ✅ Steered dispatch settled (skipping cleanup)`);
|
|
291
318
|
return;
|
|
292
319
|
}
|
|
320
|
+
streamingSignals.delete(parsed.sessionId);
|
|
293
321
|
decrementTaskIdRef(parsed.sessionId);
|
|
294
322
|
unregisterSession(route.sessionKey);
|
|
295
323
|
logger.log(`[BOT] ✅ Cleanup completed`);
|
|
@@ -386,3 +414,169 @@ function buildXYMediaPayload(mediaList) {
|
|
|
386
414
|
MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined,
|
|
387
415
|
};
|
|
388
416
|
}
|
|
417
|
+
// Use globalThis to survive module deduplication — provider.ts may load a
|
|
418
|
+
// different copy of bot.ts, so a plain module-level Map would be two objects.
|
|
419
|
+
const _g = globalThis;
|
|
420
|
+
if (!_g.__xyStreamingSignals)
|
|
421
|
+
_g.__xyStreamingSignals = new Map();
|
|
422
|
+
if (!_g.__xySteerQueues)
|
|
423
|
+
_g.__xySteerQueues = new Map();
|
|
424
|
+
const streamingSignals = _g.__xyStreamingSignals;
|
|
425
|
+
const steerQueues = _g.__xySteerQueues;
|
|
426
|
+
/**
|
|
427
|
+
* 由 provider.ts 在 wrapStreamFn 调用时触发。
|
|
428
|
+
* 这是模型 API 被调用的精确时刻,此时 isStreaming 一定为 true。
|
|
429
|
+
*/
|
|
430
|
+
export function notifyModelStreaming(sessionId) {
|
|
431
|
+
const signal = streamingSignals.get(sessionId);
|
|
432
|
+
if (signal) {
|
|
433
|
+
// 不删除 signal——后续 steer 需要靠它判断模型已在 streaming。
|
|
434
|
+
// 清理由第一条消息的 onSettled 兜底。
|
|
435
|
+
signal.notify();
|
|
436
|
+
logger.log(`[STEER-QUEUE] 📡 Model streaming signal fired for session=${sessionId}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
function createStreamingSignal(sessionId) {
|
|
440
|
+
let resolve;
|
|
441
|
+
const promise = new Promise(r => { resolve = r; });
|
|
442
|
+
const signal = { promise, notify: resolve };
|
|
443
|
+
streamingSignals.set(sessionId, signal);
|
|
444
|
+
logger.log(`[STEER-QUEUE] 🟢 Streaming signal created for session ${sessionId}`);
|
|
445
|
+
return signal;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* 将 steer 消息放入 per-session 串行队列。
|
|
449
|
+
* 等待第一条消息的 streaming 信号(deliver 首次触发),然后 dispatch。
|
|
450
|
+
* 多个 steer 按到达顺序串行处理,无需重试。
|
|
451
|
+
*/
|
|
452
|
+
function enqueueSteer(params) {
|
|
453
|
+
const { sessionId } = params;
|
|
454
|
+
// 取出当前队列尾部(或 undefined),然后链上新的 Promise
|
|
455
|
+
const prev = steerQueues.get(sessionId);
|
|
456
|
+
const next = (prev ?? Promise.resolve()).then(() => dispatchSteerWhenReady(params));
|
|
457
|
+
steerQueues.set(sessionId, next);
|
|
458
|
+
// 链条结束后清理
|
|
459
|
+
next.catch((err) => {
|
|
460
|
+
logger.error(`[STEER-QUEUE] ❌ Steer chain failed: ${String(err)}`);
|
|
461
|
+
}).finally(() => {
|
|
462
|
+
if (steerQueues.get(sessionId) === next) {
|
|
463
|
+
steerQueues.delete(sessionId);
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
return next;
|
|
467
|
+
}
|
|
468
|
+
async function dispatchSteerWhenReady(params) {
|
|
469
|
+
const { sessionId, sessionKey, steerText } = params;
|
|
470
|
+
// 1. 等待第一条消息开始 streaming
|
|
471
|
+
// signal 可能尚未创建(第一条消息还在文件下载等耗时操作中),
|
|
472
|
+
// 轮询等待直到 signal 出现,最長等待 ~5 秒。
|
|
473
|
+
let signal = streamingSignals.get(sessionId);
|
|
474
|
+
if (!signal) {
|
|
475
|
+
logger.log(`[STEER-QUEUE] ⏳ Signal not yet created, polling for session=${sessionId}`);
|
|
476
|
+
for (let i = 0; i < 50; i++) {
|
|
477
|
+
await new Promise(r => setTimeout(r, 100));
|
|
478
|
+
signal = streamingSignals.get(sessionId);
|
|
479
|
+
if (signal)
|
|
480
|
+
break;
|
|
481
|
+
if (!hasActiveTask(sessionId)) {
|
|
482
|
+
logger.log(`[STEER-QUEUE] ℹ️ First message completed while waiting, skip steer`);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
if (signal) {
|
|
488
|
+
logger.log(`[STEER-QUEUE] ⏳ Waiting for streaming signal, session=${sessionId}`);
|
|
489
|
+
await signal.promise;
|
|
490
|
+
logger.log(`[STEER-QUEUE] ✅ Streaming signal received, session=${sessionId}`);
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
// 轮询超时且 hasActiveTask 仍为 true——说明第一条消息可能卡在异常路径,
|
|
494
|
+
// 没有创建 signal。此时 dispatch 会与第一条消息的模型调用并发冲突,放弃。
|
|
495
|
+
logger.log(`[STEER-QUEUE] ⚠️ Signal never appeared after polling, skip steer to avoid collision`);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
// 2. 第一条消息已结束 → 放弃
|
|
499
|
+
if (!hasActiveTask(sessionId)) {
|
|
500
|
+
logger.log(`[STEER-QUEUE] ℹ️ First message completed, skip steer`);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
// 3. 构建 dispatch 上下文并 dispatch /steer
|
|
504
|
+
const core = getXYRuntime();
|
|
505
|
+
const speaker = sessionId;
|
|
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}`;
|
|
512
|
+
const messageBody = `${speaker}: ${steerCommand}`;
|
|
513
|
+
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(params.cfg);
|
|
514
|
+
const body = core.channel.reply.formatAgentEnvelope({
|
|
515
|
+
channel: "xiaoyi-channel",
|
|
516
|
+
from: speaker,
|
|
517
|
+
timestamp: new Date(),
|
|
518
|
+
envelope: envelopeOptions,
|
|
519
|
+
body: messageBody,
|
|
520
|
+
});
|
|
521
|
+
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
522
|
+
Body: body,
|
|
523
|
+
RawBody: steerCommand,
|
|
524
|
+
CommandBody: steerCommand,
|
|
525
|
+
From: sessionId,
|
|
526
|
+
To: sessionId,
|
|
527
|
+
SessionKey: params.route.sessionKey,
|
|
528
|
+
AccountId: params.route.accountId,
|
|
529
|
+
ChatType: "direct",
|
|
530
|
+
GroupSubject: undefined,
|
|
531
|
+
SenderName: sessionId,
|
|
532
|
+
SenderId: sessionId,
|
|
533
|
+
Provider: "xiaoyi-channel",
|
|
534
|
+
Surface: "xiaoyi-channel",
|
|
535
|
+
MessageSid: `xiaoyi_${params.parsed.taskId}_${params.deviceType}`,
|
|
536
|
+
Timestamp: Date.now(),
|
|
537
|
+
WasMentioned: false,
|
|
538
|
+
CommandAuthorized: true,
|
|
539
|
+
OriginatingChannel: "xiaoyi-channel",
|
|
540
|
+
OriginatingTo: sessionId,
|
|
541
|
+
ReplyToBody: undefined,
|
|
542
|
+
...params.mediaPayload,
|
|
543
|
+
});
|
|
544
|
+
const steerState = { steered: true };
|
|
545
|
+
const { dispatcher, replyOptions } = createXYReplyDispatcher({
|
|
546
|
+
cfg: params.cfg,
|
|
547
|
+
runtime: params.runtime,
|
|
548
|
+
sessionId,
|
|
549
|
+
taskId: params.parsed.taskId,
|
|
550
|
+
messageId: params.parsed.messageId,
|
|
551
|
+
accountId: params.route.accountId,
|
|
552
|
+
steerState,
|
|
553
|
+
});
|
|
554
|
+
const sessionContext = {
|
|
555
|
+
config: resolveXYConfig(params.cfg),
|
|
556
|
+
sessionId,
|
|
557
|
+
taskId: params.parsed.taskId,
|
|
558
|
+
messageId: params.parsed.messageId,
|
|
559
|
+
agentId: params.route.accountId,
|
|
560
|
+
deviceType: params.deviceType,
|
|
561
|
+
};
|
|
562
|
+
logger.log(`[STEER-QUEUE] 🚀 Dispatching steer for session=${sessionId}`);
|
|
563
|
+
await core.channel.reply.withReplyDispatcher({
|
|
564
|
+
dispatcher,
|
|
565
|
+
onSettled: () => {
|
|
566
|
+
logger.log(`[STEER-QUEUE] 🏁 Steer dispatch settled for session=${sessionId}`);
|
|
567
|
+
},
|
|
568
|
+
run: () => {
|
|
569
|
+
return runWithSessionContext(sessionContext, async () => {
|
|
570
|
+
const result = await core.channel.reply.dispatchReplyFromConfig({
|
|
571
|
+
ctx: ctxPayload,
|
|
572
|
+
cfg: params.cfg,
|
|
573
|
+
dispatcher,
|
|
574
|
+
replyOptions,
|
|
575
|
+
});
|
|
576
|
+
logger.log(`[STEER-QUEUE] dispatch result: ${JSON.stringify(result)}`);
|
|
577
|
+
return result;
|
|
578
|
+
});
|
|
579
|
+
},
|
|
580
|
+
});
|
|
581
|
+
logger.log(`[STEER-QUEUE] ✅ Steer dispatch completed for session=${sessionId}`);
|
|
582
|
+
}
|
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;
|
|
@@ -45,6 +46,11 @@ function getFirstUserText(messages) {
|
|
|
45
46
|
}
|
|
46
47
|
/** Regex to match `[cron:<uuid> <title>]` anywhere in text. */
|
|
47
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
|
+
}
|
|
48
54
|
/** Check if the request is triggered by a cron job by inspecting the first user message. */
|
|
49
55
|
function isCronTriggered(messages) {
|
|
50
56
|
return /\[cron:/i.test(getFirstUserText(messages));
|
|
@@ -230,8 +236,6 @@ const HEADER_SESSION_ID = "x-session-id";
|
|
|
230
236
|
const HEADER_INTERACTION_ID = "x-interaction-id";
|
|
231
237
|
/** Internal key for passing fallback uid prefix from prepareExtraParams to wrapStreamFn. */
|
|
232
238
|
const FALLBACK_PREFIX_KEY = "_xiaoyi_fallback_prefix";
|
|
233
|
-
/** Internal key for passing deviceType from prepareExtraParams to wrapStreamFn. */
|
|
234
|
-
const DEVICE_TYPE_KEY = "_xiaoyi_device_type";
|
|
235
239
|
const SELF_EVOLUTION_PROMPT_BEGIN = "<self_evolution_prompt>";
|
|
236
240
|
const SELF_EVOLUTION_PROMPT_END = "</self_evolution_prompt>";
|
|
237
241
|
const SELF_EVOLUTION_ENABLED_PROMPT_SECTION = `
|
|
@@ -391,7 +395,9 @@ function trimUserMetadata(text) {
|
|
|
391
395
|
}
|
|
392
396
|
/**
|
|
393
397
|
* Extract A2A taskId and deviceType from Conversation info JSON.
|
|
394
|
-
* bot.ts stores them as MessageSid = "
|
|
398
|
+
* bot.ts stores them as MessageSid = "xiaoyi_taskId_deviceType".
|
|
399
|
+
* The "xiaoyi_" prefix ensures extraction only happens for messages
|
|
400
|
+
* routed through xiaoyi-channel, not other channels sharing the provider.
|
|
395
401
|
*/
|
|
396
402
|
function extractA2AFromConversationInfo(text) {
|
|
397
403
|
const match = text.match(/Conversation info \(untrusted metadata\):\n```json\n([\s\S]*?)\n```/);
|
|
@@ -401,9 +407,9 @@ function extractA2AFromConversationInfo(text) {
|
|
|
401
407
|
if (!msgIdMatch)
|
|
402
408
|
return null;
|
|
403
409
|
const parts = msgIdMatch[1].split("_");
|
|
404
|
-
if (parts.length <
|
|
410
|
+
if (parts.length < 3 || parts[0] !== "xiaoyi")
|
|
405
411
|
return null;
|
|
406
|
-
return { taskId: parts[
|
|
412
|
+
return { taskId: parts[1], deviceType: parts[2] };
|
|
407
413
|
}
|
|
408
414
|
export const xiaoyiProvider = {
|
|
409
415
|
id: "xiaoyiprovider",
|
|
@@ -412,31 +418,12 @@ export const xiaoyiProvider = {
|
|
|
412
418
|
auth: [],
|
|
413
419
|
isCacheTtlEligible: () => true,
|
|
414
420
|
/**
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
* 1. Session context (from AsyncLocalStorage, set by bot.ts)
|
|
420
|
-
* 2. uid-based fallback: sha256(uid).hex[:32]_timestamp
|
|
421
|
-
* 3. No uid available → return undefined (no headers injected)
|
|
421
|
+
* Store uid-based fallback prefix for lazy timestamp generation in wrapStreamFn.
|
|
422
|
+
* Session-level headers (traceId / sessionId / interactionId) are resolved
|
|
423
|
+
* directly in wrapStreamFn via cron detection, Conversation info extraction,
|
|
424
|
+
* or uid fallback.
|
|
422
425
|
*/
|
|
423
426
|
prepareExtraParams: (ctx) => {
|
|
424
|
-
const sessionCtx = getCurrentSessionContext();
|
|
425
|
-
if (sessionCtx) {
|
|
426
|
-
const taskId = sessionCtx.taskId;
|
|
427
|
-
const sessionId = taskId.split("&")[0];
|
|
428
|
-
const interactionId = taskId.split("&")[1] || "";
|
|
429
|
-
return {
|
|
430
|
-
...ctx.extraParams,
|
|
431
|
-
[HEADER_TRACE_ID]: taskId,
|
|
432
|
-
[HEADER_SESSION_ID]: sessionId,
|
|
433
|
-
[HEADER_INTERACTION_ID]: interactionId,
|
|
434
|
-
[DEVICE_TYPE_KEY]: sessionCtx.deviceType ?? "",
|
|
435
|
-
};
|
|
436
|
-
}
|
|
437
|
-
// Fallback: store uid prefix for lazy timestamp generation in wrapStreamFn.
|
|
438
|
-
// This ensures each model call gets a fresh timestamp instead of reusing
|
|
439
|
-
// the same one across tool-use loops and retries.
|
|
440
427
|
const uid = getUidFromConfig(ctx.config);
|
|
441
428
|
if (!uid)
|
|
442
429
|
return undefined;
|
|
@@ -487,62 +474,61 @@ export const xiaoyiProvider = {
|
|
|
487
474
|
}
|
|
488
475
|
}
|
|
489
476
|
// ── Build dynamic headers ────────────────────────────
|
|
490
|
-
|
|
491
|
-
|
|
477
|
+
// Priority:
|
|
478
|
+
// 1. Cron-triggered: uid → cronUuid, with cron-specific headers
|
|
479
|
+
// 2. Xiaoyi A2A: taskId extracted from Conversation info (xiaoyi_ prefix)
|
|
480
|
+
// 3. UID-based fallback: sha256(uid).hex[:32]_timestamp
|
|
481
|
+
const isCron = isCronTriggered(context.messages);
|
|
482
|
+
if (isCron) {
|
|
483
|
+
const fallbackPrefix = ctx.extraParams?.[FALLBACK_PREFIX_KEY];
|
|
492
484
|
if (typeof fallbackPrefix === "string") {
|
|
493
|
-
// Fallback mode: generate fresh timestamp per request
|
|
494
|
-
const isCron = isCronTriggered(context.messages);
|
|
495
485
|
const fallbackValue = `${fallbackPrefix}_${Date.now()}`;
|
|
496
|
-
dynamicHeaders[HEADER_TRACE_ID] =
|
|
486
|
+
dynamicHeaders[HEADER_TRACE_ID] = `cron_${fallbackValue}`;
|
|
497
487
|
dynamicHeaders[HEADER_SESSION_ID] = fallbackValue;
|
|
498
488
|
dynamicHeaders[HEADER_INTERACTION_ID] = fallbackValue;
|
|
499
|
-
if (isCron) {
|
|
500
|
-
const cronTitle = extractCronTitle(context.messages);
|
|
501
|
-
if (cronTitle)
|
|
502
|
-
dynamicHeaders["x-cron-title"] = encodeURIComponent(cronTitle);
|
|
503
|
-
if (context.messages?.length === 1)
|
|
504
|
-
dynamicHeaders["x-cron-flag"] = "begin";
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
else if (extractedTaskId) {
|
|
508
|
-
// Session mode: taskId extracted from Conversation info
|
|
509
|
-
const traceId = extractedTaskId;
|
|
510
|
-
const sessionId = traceId.split("&")[0];
|
|
511
|
-
const interactionId = traceId.split("&")[1] ?? "";
|
|
512
|
-
const isCron = isCronTriggered(context.messages);
|
|
513
|
-
dynamicHeaders[HEADER_TRACE_ID] = isCron ? `cron_${traceId}_${Date.now()}` : traceId;
|
|
514
|
-
if (isCron) {
|
|
515
|
-
const cronTitle = extractCronTitle(context.messages);
|
|
516
|
-
if (cronTitle)
|
|
517
|
-
dynamicHeaders["x-cron-title"] = encodeURIComponent(cronTitle);
|
|
518
|
-
if (context.messages?.length === 1)
|
|
519
|
-
dynamicHeaders["x-cron-flag"] = "begin";
|
|
520
|
-
}
|
|
521
|
-
dynamicHeaders[HEADER_SESSION_ID] = sessionId;
|
|
522
|
-
dynamicHeaders[HEADER_INTERACTION_ID] = interactionId;
|
|
523
489
|
}
|
|
524
490
|
else {
|
|
525
|
-
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
491
|
+
const cronUuid = extractCronUuid(context.messages) ?? "cron";
|
|
492
|
+
const cronSessionId = `cron_${cronUuid}_${Date.now()}`;
|
|
493
|
+
dynamicHeaders[HEADER_TRACE_ID] = cronSessionId;
|
|
494
|
+
dynamicHeaders[HEADER_SESSION_ID] = cronUuid;
|
|
495
|
+
dynamicHeaders[HEADER_INTERACTION_ID] = cronSessionId;
|
|
496
|
+
}
|
|
497
|
+
const cronTitle = extractCronTitle(context.messages);
|
|
498
|
+
if (cronTitle)
|
|
499
|
+
dynamicHeaders["x-cron-title"] = encodeURIComponent(cronTitle);
|
|
500
|
+
if (context.messages?.length === 1)
|
|
501
|
+
dynamicHeaders["x-cron-flag"] = "begin";
|
|
502
|
+
}
|
|
503
|
+
else if (extractedTaskId) {
|
|
504
|
+
const sessionId = extractedTaskId.split("&")[0];
|
|
505
|
+
const interactionId = extractedTaskId.split("&")[1] ?? "";
|
|
506
|
+
dynamicHeaders[HEADER_TRACE_ID] = extractedTaskId;
|
|
507
|
+
dynamicHeaders[HEADER_SESSION_ID] = sessionId;
|
|
508
|
+
dynamicHeaders[HEADER_INTERACTION_ID] = interactionId;
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
const fallbackPrefix = ctx.extraParams?.[FALLBACK_PREFIX_KEY];
|
|
512
|
+
if (typeof fallbackPrefix === "string") {
|
|
513
|
+
const fallbackValue = `${fallbackPrefix}_${Date.now()}`;
|
|
514
|
+
dynamicHeaders[HEADER_TRACE_ID] = fallbackValue;
|
|
515
|
+
dynamicHeaders[HEADER_SESSION_ID] = fallbackValue;
|
|
516
|
+
dynamicHeaders[HEADER_INTERACTION_ID] = fallbackValue;
|
|
535
517
|
}
|
|
536
518
|
}
|
|
537
519
|
// 记录输入
|
|
538
520
|
logger.log(`[xiaoyiprovider] input messages count: ${context.messages?.length ?? 0}`);
|
|
521
|
+
// 🔑 通知 steer 队列:模型 API 已被调用,此时 isStreaming 一定为 true
|
|
522
|
+
const sessionCtx = getCurrentSessionContext();
|
|
523
|
+
if (sessionCtx?.sessionId) {
|
|
524
|
+
notifyModelStreaming(sessionCtx.sessionId);
|
|
525
|
+
}
|
|
539
526
|
if (context.systemPrompt) {
|
|
540
527
|
logger.log(`[xiaoyiprovider] system prompt length: ${context.systemPrompt.length}`);
|
|
541
528
|
}
|
|
542
529
|
// deviceType: prefer value extracted from Conversation info,
|
|
543
|
-
// then
|
|
544
|
-
const
|
|
545
|
-
const deviceType = (extractedDeviceType || extraParamsDeviceType)
|
|
530
|
+
// then ALS fallback.
|
|
531
|
+
const deviceType = extractedDeviceType
|
|
546
532
|
?? getCurrentSessionContext()?.deviceType;
|
|
547
533
|
// 在发送给模型前,优化 systemPrompt 结构
|
|
548
534
|
if (context.systemPrompt) {
|
|
@@ -8,7 +8,7 @@ import { getCurrentTaskId, getCurrentMessageId } from "../task-manager.js";
|
|
|
8
8
|
* 仅用于全局 Map 回退路径的清理,不影响 ALS 路径。
|
|
9
9
|
* 工具已改为闭包捕获 ctx,此 TTL 仅作为防止 session 泄漏的最后防线。
|
|
10
10
|
* 正常对话中 registerSession 会刷新 createdAt,所以长对话不受影响。 */
|
|
11
|
-
const SESSION_TTL_MS = 60 * 60 * 1000; //
|
|
11
|
+
const SESSION_TTL_MS = 6 * 60 * 60 * 1000; // 6 hours
|
|
12
12
|
// Use globalThis to ensure a single Map instance across all module copies.
|
|
13
13
|
// The xy_channel plugin may be loaded by openclaw from different module resolution
|
|
14
14
|
// paths (plugin entry vs tool registration), causing session-manager.ts to be
|