@ynhcj/xiaoyi-channel 0.0.110-next โ 0.0.111-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 +2 -0
- package/dist/src/bot.js +40 -28
- package/dist/src/monitor.js +13 -0
- package/dist/src/provider.js +69 -72
- package/package.json +1 -1
package/dist/src/bot.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface HandleXYMessageParams {
|
|
|
9
9
|
message: A2AJsonRpcRequest;
|
|
10
10
|
accountId: string;
|
|
11
11
|
webSocketSessionId?: string;
|
|
12
|
+
/** Called after dispatch init is complete (agentTools/wrapStreamFn done). */
|
|
13
|
+
onInitComplete?: () => void;
|
|
12
14
|
}
|
|
13
15
|
/**
|
|
14
16
|
* Handle an incoming A2A message.
|
package/dist/src/bot.js
CHANGED
|
@@ -209,6 +209,10 @@ export async function handleXYMessage(params) {
|
|
|
209
209
|
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
|
|
210
210
|
// Build message body with speaker prefix (following feishu pattern)
|
|
211
211
|
let messageBody = textForAgent;
|
|
212
|
+
// Embed A2A taskId marker before the user query so the provider can
|
|
213
|
+
// extract it from messages without relying on AsyncLocalStorage.
|
|
214
|
+
// The provider strips this marker before sending to the model.
|
|
215
|
+
messageBody = `xiaoyiA2A info[taskId:${parsed.taskId},deviceType:${deviceType}] ${messageBody}`;
|
|
212
216
|
// Add speaker prefix for clarity
|
|
213
217
|
const speaker = parsed.sessionId;
|
|
214
218
|
messageBody = `${speaker}: ${messageBody}`;
|
|
@@ -289,34 +293,42 @@ export async function handleXYMessage(params) {
|
|
|
289
293
|
unregisterSession(route.sessionKey);
|
|
290
294
|
log(`[BOT] โ
Cleanup completed`);
|
|
291
295
|
},
|
|
292
|
-
run: () =>
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
296
|
+
run: () => {
|
|
297
|
+
// ๐ Use AsyncLocalStorage to provide session context to tools.
|
|
298
|
+
// runWithSessionContext returns after the sync part of dispatch
|
|
299
|
+
// (including agentTools + wrapStreamFn) has executed, so we
|
|
300
|
+
// signal init complete to release the global dispatch gate
|
|
301
|
+
// for the next session.
|
|
302
|
+
const dispatchPromise = runWithSessionContext(sessionContext, async () => {
|
|
303
|
+
log(`[BOT-DISPATCH] โณ dispatchReplyFromConfig starting...`);
|
|
304
|
+
log(`[BOT-DISPATCH] - sessionKey: ${ctxPayload.SessionKey}`);
|
|
305
|
+
log(`[BOT-DISPATCH] - provider: ${ctxPayload.Provider}`);
|
|
306
|
+
log(`[BOT-DISPATCH] - surface: ${ctxPayload.Surface}`);
|
|
307
|
+
log(`[BOT-DISPATCH] - from: ${ctxPayload.From}`);
|
|
308
|
+
log(`[BOT-DISPATCH] - body length: ${ctxPayload.Body?.length ?? 0}`);
|
|
309
|
+
try {
|
|
310
|
+
const result = await core.channel.reply.dispatchReplyFromConfig({
|
|
311
|
+
ctx: ctxPayload,
|
|
312
|
+
cfg,
|
|
313
|
+
dispatcher,
|
|
314
|
+
replyOptions,
|
|
315
|
+
});
|
|
316
|
+
log(`[BOT-DISPATCH] โ
dispatchReplyFromConfig returned`);
|
|
317
|
+
log(`[BOT-DISPATCH] - result: ${JSON.stringify(result)}`);
|
|
318
|
+
return result;
|
|
319
|
+
}
|
|
320
|
+
catch (dispatchErr) {
|
|
321
|
+
error(`[BOT-DISPATCH] โ dispatchReplyFromConfig threw`);
|
|
322
|
+
error(`[BOT-DISPATCH] - error name: ${dispatchErr instanceof Error ? dispatchErr.name : "unknown"}`);
|
|
323
|
+
error(`[BOT-DISPATCH] - error message: ${String(dispatchErr)}`);
|
|
324
|
+
error(`[BOT-DISPATCH] - error stack: ${dispatchErr instanceof Error ? dispatchErr.stack?.slice(0, 500) : "N/A"}`);
|
|
325
|
+
throw dispatchErr;
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
// Signal init complete โ sync part (agentTools, wrapStreamFn) is done
|
|
329
|
+
params.onInitComplete?.();
|
|
330
|
+
return dispatchPromise;
|
|
331
|
+
},
|
|
320
332
|
});
|
|
321
333
|
log(`[BOT] โ
Dispatcher completed for session: ${parsed.sessionId}`);
|
|
322
334
|
log(`xy: dispatch complete (session=${parsed.sessionId})`);
|
package/dist/src/monitor.js
CHANGED
|
@@ -68,6 +68,12 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
68
68
|
const activeMessages = new Set();
|
|
69
69
|
// Create session queue for ordered message processing
|
|
70
70
|
const enqueue = createSessionQueue();
|
|
71
|
+
// Global gate that serializes dispatch initialization across sessions.
|
|
72
|
+
// When a new session starts dispatching, it acquires this gate and holds it
|
|
73
|
+
// until agent setup (agentTools + wrapStreamFn) is complete, then releases it.
|
|
74
|
+
// This prevents lastRegisteredKey races when multiple sessions initialize
|
|
75
|
+
// concurrently.
|
|
76
|
+
let globalDispatchInitGate = Promise.resolve();
|
|
71
77
|
// Health check interval
|
|
72
78
|
let healthCheckInterval = null;
|
|
73
79
|
return new Promise((resolve, reject) => {
|
|
@@ -83,6 +89,11 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
83
89
|
}
|
|
84
90
|
activeMessages.add(messageKey);
|
|
85
91
|
const task = async () => {
|
|
92
|
+
// Wait for the previous session's init to complete (global gate),
|
|
93
|
+
// then acquire the gate for this session's init.
|
|
94
|
+
await globalDispatchInitGate;
|
|
95
|
+
let releaseGate;
|
|
96
|
+
globalDispatchInitGate = new Promise((r) => { releaseGate = r; });
|
|
86
97
|
try {
|
|
87
98
|
await handleXYMessage({
|
|
88
99
|
cfg,
|
|
@@ -90,10 +101,12 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
90
101
|
message,
|
|
91
102
|
accountId, // โ
Pass accountId ("default")
|
|
92
103
|
webSocketSessionId: sessionId, // โ
ไผ ้ WebSocket ๅฑ็บง็ sessionId
|
|
104
|
+
onInitComplete: () => releaseGate(),
|
|
93
105
|
});
|
|
94
106
|
}
|
|
95
107
|
catch (err) {
|
|
96
108
|
// โ
Only log error, don't re-throw to prevent gateway restart
|
|
109
|
+
releaseGate();
|
|
97
110
|
error(`XY gateway: error handling message from ${serverId}: ${String(err)}`);
|
|
98
111
|
}
|
|
99
112
|
finally {
|
package/dist/src/provider.js
CHANGED
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
// models.providers.xiaoyiprovider.api = "openai-completions"
|
|
9
9
|
// models.providers.xiaoyiprovider.models = [...]
|
|
10
10
|
import { createHash } from "crypto";
|
|
11
|
-
import {
|
|
12
|
-
import { getCurrentTaskId } from "./task-manager.js";
|
|
11
|
+
import { getCurrentSessionContext } from "./tools/session-manager.js";
|
|
13
12
|
import { selfEvolutionManager } from "./utils/self-evolution-manager.js";
|
|
14
13
|
// โโ Retry config โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
15
14
|
const RETRY_DELAYS_MS = [10_000, 20_000, 40_000, 60_000, 60_000];
|
|
@@ -443,23 +442,48 @@ export const xiaoyiProvider = {
|
|
|
443
442
|
const underlying = ctx.streamFn;
|
|
444
443
|
if (!underlying)
|
|
445
444
|
return underlying;
|
|
446
|
-
//
|
|
447
|
-
//
|
|
448
|
-
|
|
449
|
-
// active concurrently, getCurrentSessionContext() may later return the WRONG
|
|
450
|
-
// session (lastRegisteredKey fallback). The captured sessionId lets us
|
|
451
|
-
// bypass that fallback and look up the correct taskId directly from
|
|
452
|
-
// task-manager.
|
|
453
|
-
const alsStore = asyncLocalStorage.getStore();
|
|
454
|
-
const sessionCtx = getCurrentSessionContext();
|
|
455
|
-
const capturedA2ASessionId = sessionCtx?.sessionId ?? null;
|
|
456
|
-
console.log(`[xiaoyiprovider] wrapStreamFn โ ALS: ${alsStore ? "HIT" : "MISS"}, ` +
|
|
457
|
-
`capturedSessionId=${capturedA2ASessionId ?? "null"}, ` +
|
|
458
|
-
`taskId=${sessionCtx?.taskId ?? "null"}, ` +
|
|
459
|
-
`activeSessionsCount=${activeSessions.size}`);
|
|
445
|
+
// โโ Regex to extract A2A taskId/deviceType from user messages โโ
|
|
446
|
+
// bot.ts embeds: xiaoyiA2A info[taskId:<id>,deviceType:<type>]
|
|
447
|
+
const A2A_MARKER_RE = /xiaoyiA2A info\[taskId:([^\],]+)(?:,deviceType:([^\]]+))?\]\s*/;
|
|
460
448
|
return async (model, context, options) => {
|
|
461
|
-
// ๆฏๆฌก่ฏทๆฑๆถไป ctx.extraParams ๅจๆ่ฏปๅ header
|
|
462
449
|
const dynamicHeaders = {};
|
|
450
|
+
// โโ Extract A2A taskId/deviceType from user messages โโ
|
|
451
|
+
// Scan from last user message backwards; strip the marker from all.
|
|
452
|
+
let resolvedTaskId = null;
|
|
453
|
+
let resolvedDeviceType = null;
|
|
454
|
+
if (context.messages) {
|
|
455
|
+
for (let i = context.messages.length - 1; i >= 0; i--) {
|
|
456
|
+
const msg = context.messages[i];
|
|
457
|
+
if (msg.role !== "user" || !msg.content)
|
|
458
|
+
continue;
|
|
459
|
+
const extract = (text) => {
|
|
460
|
+
const match = text.match(A2A_MARKER_RE);
|
|
461
|
+
if (match) {
|
|
462
|
+
if (!resolvedTaskId)
|
|
463
|
+
resolvedTaskId = match[1] ?? null;
|
|
464
|
+
if (!resolvedDeviceType)
|
|
465
|
+
resolvedDeviceType = match[2] ?? null;
|
|
466
|
+
return text.replace(A2A_MARKER_RE, "");
|
|
467
|
+
}
|
|
468
|
+
return null;
|
|
469
|
+
};
|
|
470
|
+
if (typeof msg.content === "string") {
|
|
471
|
+
const stripped = extract(msg.content);
|
|
472
|
+
if (stripped !== null)
|
|
473
|
+
msg.content = stripped;
|
|
474
|
+
}
|
|
475
|
+
else if (Array.isArray(msg.content)) {
|
|
476
|
+
for (const block of msg.content) {
|
|
477
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
478
|
+
const stripped = extract(block.text);
|
|
479
|
+
if (stripped !== null)
|
|
480
|
+
block.text = stripped;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
// โโ Build dynamic headers โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
463
487
|
if (ctx.extraParams) {
|
|
464
488
|
const fallbackPrefix = ctx.extraParams[FALLBACK_PREFIX_KEY];
|
|
465
489
|
if (typeof fallbackPrefix === "string") {
|
|
@@ -477,58 +501,46 @@ export const xiaoyiProvider = {
|
|
|
477
501
|
dynamicHeaders["x-cron-flag"] = "begin";
|
|
478
502
|
}
|
|
479
503
|
}
|
|
480
|
-
else {
|
|
481
|
-
// Session mode:
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
}
|
|
494
|
-
if (!resolvedTaskId) {
|
|
495
|
-
resolvedTaskId = getCurrentSessionContext()?.taskId ?? null;
|
|
496
|
-
}
|
|
497
|
-
const traceId = resolvedTaskId ?? ctx.extraParams[HEADER_TRACE_ID];
|
|
498
|
-
const sessionId = resolvedTaskId?.split("&")[0]
|
|
499
|
-
?? ctx.extraParams[HEADER_SESSION_ID];
|
|
500
|
-
const interactionId = resolvedTaskId?.split("&")[1]
|
|
501
|
-
?? ctx.extraParams[HEADER_INTERACTION_ID]
|
|
502
|
-
?? "";
|
|
503
|
-
if (typeof traceId === "string") {
|
|
504
|
-
const isCron = isCronTriggered(context.messages);
|
|
505
|
-
dynamicHeaders[HEADER_TRACE_ID] = isCron ? `cron_${traceId}_${Date.now()}` : traceId;
|
|
506
|
-
if (isCron) {
|
|
507
|
-
const cronTitle = extractCronTitle(context.messages);
|
|
508
|
-
if (cronTitle)
|
|
509
|
-
dynamicHeaders["x-cron-title"] = encodeURIComponent(cronTitle);
|
|
510
|
-
if (context.messages?.length === 1)
|
|
511
|
-
dynamicHeaders["x-cron-flag"] = "begin";
|
|
512
|
-
}
|
|
504
|
+
else if (resolvedTaskId) {
|
|
505
|
+
// Session mode: taskId extracted from user message marker
|
|
506
|
+
const traceId = resolvedTaskId;
|
|
507
|
+
const sessionId = traceId.split("&")[0];
|
|
508
|
+
const interactionId = traceId.split("&")[1] ?? "";
|
|
509
|
+
const isCron = isCronTriggered(context.messages);
|
|
510
|
+
dynamicHeaders[HEADER_TRACE_ID] = isCron ? `cron_${traceId}_${Date.now()}` : traceId;
|
|
511
|
+
if (isCron) {
|
|
512
|
+
const cronTitle = extractCronTitle(context.messages);
|
|
513
|
+
if (cronTitle)
|
|
514
|
+
dynamicHeaders["x-cron-title"] = encodeURIComponent(cronTitle);
|
|
515
|
+
if (context.messages?.length === 1)
|
|
516
|
+
dynamicHeaders["x-cron-flag"] = "begin";
|
|
513
517
|
}
|
|
514
518
|
if (typeof sessionId === "string")
|
|
515
519
|
dynamicHeaders[HEADER_SESSION_ID] = sessionId;
|
|
516
520
|
if (typeof interactionId === "string")
|
|
517
521
|
dynamicHeaders[HEADER_INTERACTION_ID] = interactionId;
|
|
518
522
|
}
|
|
523
|
+
else {
|
|
524
|
+
// No marker found โ fall back to extraParams cached values
|
|
525
|
+
const traceId = ctx.extraParams[HEADER_TRACE_ID];
|
|
526
|
+
const sessionId = ctx.extraParams[HEADER_SESSION_ID];
|
|
527
|
+
const interactionId = ctx.extraParams[HEADER_INTERACTION_ID];
|
|
528
|
+
if (typeof traceId === "string")
|
|
529
|
+
dynamicHeaders[HEADER_TRACE_ID] = traceId;
|
|
530
|
+
if (typeof sessionId === "string")
|
|
531
|
+
dynamicHeaders[HEADER_SESSION_ID] = sessionId;
|
|
532
|
+
if (typeof interactionId === "string")
|
|
533
|
+
dynamicHeaders[HEADER_INTERACTION_ID] = interactionId;
|
|
534
|
+
}
|
|
519
535
|
}
|
|
520
536
|
// ่ฎฐๅฝ่พๅ
ฅ
|
|
521
537
|
console.log(`[xiaoyiprovider] input messages count: ${context.messages?.length ?? 0}`);
|
|
522
538
|
if (context.systemPrompt) {
|
|
523
539
|
console.log(`[xiaoyiprovider] system prompt length: ${context.systemPrompt.length}`);
|
|
524
540
|
}
|
|
525
|
-
//
|
|
526
|
-
// Fall back to getCurrentSessionContext() because OpenClaw caches
|
|
527
|
-
// resolvePreparedExtraParams by provider/modelId โ the cache key does
|
|
528
|
-
// not include session-specific data, so deviceType may be missing
|
|
529
|
-
// from the cached extraParams even when a session is active.
|
|
541
|
+
// deviceType: prefer marker-extracted value, fall back to extraParams
|
|
530
542
|
const extraParamsDeviceType = ctx.extraParams?.[DEVICE_TYPE_KEY] || undefined;
|
|
531
|
-
const deviceType = extraParamsDeviceType
|
|
543
|
+
const deviceType = resolvedDeviceType || extraParamsDeviceType || undefined;
|
|
532
544
|
// ๅจๅ้็ปๆจกๅๅ๏ผไผๅ systemPrompt ็ปๆ
|
|
533
545
|
if (context.systemPrompt) {
|
|
534
546
|
let sp = context.systemPrompt;
|
|
@@ -561,7 +573,7 @@ export const xiaoyiProvider = {
|
|
|
561
573
|
const selfEvolutionEnabled = await selfEvolutionManager.isEnabled();
|
|
562
574
|
console.log(`[selfEvolution] selfEvolution flag: ${selfEvolutionEnabled}`);
|
|
563
575
|
context.systemPrompt = applySelfEvolutionPrompt(context.systemPrompt, selfEvolutionEnabled);
|
|
564
|
-
// Append device context to systemPrompt
|
|
576
|
+
// Append device context to systemPrompt
|
|
565
577
|
if (deviceType) {
|
|
566
578
|
const displayDevice = (deviceType === "2in1") ? "้ธฟ่PC" : deviceType;
|
|
567
579
|
const deviceSection = `\n\n## Current User Device Context\nThe current user is using the following device: ${displayDevice}\nYou need to be aware of the user's current device and provide guidance accordingly. If the response involves device-related tools or actions, you must tailor the reply based on the user's current device, using device-specific references such as "saved to the Notes/Calendar on your {deviceType}.\n"`;
|
|
@@ -598,19 +610,4 @@ export const xiaoyiProvider = {
|
|
|
598
610
|
return createRetryingStream(makeStream, cronJob);
|
|
599
611
|
};
|
|
600
612
|
},
|
|
601
|
-
/**
|
|
602
|
-
* Diagnostic-only: log ctx.sessionId format on every transport turn.
|
|
603
|
-
* Goal: confirm whether sessionId = sessionKey (so we can use it to
|
|
604
|
-
* look up A2A context directly from activeSessions, bypassing ALS).
|
|
605
|
-
*/
|
|
606
|
-
resolveTransportTurnState: (ctx) => {
|
|
607
|
-
console.log(`[xiaoyiprovider] resolveTransportTurnState โ ` +
|
|
608
|
-
`sessionId=${ctx.sessionId ?? "null"}, ` +
|
|
609
|
-
`turnId=${ctx.turnId}, ` +
|
|
610
|
-
`transport=${ctx.transport}, ` +
|
|
611
|
-
`attempt=${ctx.attempt}, ` +
|
|
612
|
-
`provider=${ctx.provider}, ` +
|
|
613
|
-
`modelId=${ctx.modelId}`);
|
|
614
|
-
return null;
|
|
615
|
-
},
|
|
616
613
|
};
|