@ynhcj/xiaoyi-channel 0.0.179-next → 0.0.180-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.
@@ -393,6 +393,28 @@ function trimUserMetadata(text) {
393
393
  text = text.replace(/\n*Sender \(untrusted metadata\):\n```json\n[\s\S]*?\n```\n*/, "\n");
394
394
  return text.replace(/\n{3,}/g, "\n\n");
395
395
  }
396
+ /**
397
+ * Extract A2A taskId and deviceType from Conversation info JSON.
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.
401
+ *
402
+ * This is the most reliable source during steer scenarios: ALS still holds
403
+ * the first message's taskId, but the text extraction sees the latest
404
+ * injected user message which carries the updated taskId.
405
+ */
406
+ function extractA2AFromConversationInfo(text) {
407
+ const match = text.match(/Conversation info \(untrusted metadata\):\n```json\n([\s\S]*?)\n```/);
408
+ if (!match)
409
+ return null;
410
+ const msgIdMatch = match[1].match(/"message_id"\s*:\s*"([^"]+)"/);
411
+ if (!msgIdMatch)
412
+ return null;
413
+ const parts = msgIdMatch[1].split("_");
414
+ if (parts.length < 3 || parts[0] !== "xiaoyi")
415
+ return null;
416
+ return { taskId: parts[1], deviceType: parts[2] };
417
+ }
396
418
  export const xiaoyiProvider = {
397
419
  id: "xiaoyiprovider",
398
420
  label: "Xiaoyi Provider",
@@ -461,11 +483,39 @@ export const xiaoyiProvider = {
461
483
  return underlying;
462
484
  return async (model, context, options) => {
463
485
  const dynamicHeaders = {};
486
+ // ── Extract A2A taskId/deviceType from Conversation info ──
487
+ // bot.ts stores taskId_deviceType as MessageSid, which the framework
488
+ // renders as message_id in the Conversation info JSON block.
489
+ // This is the priority source: it reflects the latest user message
490
+ // even during steer when ALS still holds the first message's taskId.
491
+ let extractedTaskId = null;
492
+ let extractedDeviceType = null;
493
+ if (context.messages) {
494
+ for (let i = context.messages.length - 1; i >= 0; i--) {
495
+ const msg = context.messages[i];
496
+ if (msg.role !== "user")
497
+ continue;
498
+ const text = typeof msg.content === "string"
499
+ ? msg.content
500
+ : Array.isArray(msg.content)
501
+ ? msg.content.find((b) => b.type === "text")?.text ?? ""
502
+ : "";
503
+ if (!text)
504
+ continue;
505
+ const extracted = extractA2AFromConversationInfo(text);
506
+ if (extracted) {
507
+ extractedTaskId = extracted.taskId;
508
+ extractedDeviceType = extracted.deviceType;
509
+ break;
510
+ }
511
+ }
512
+ }
464
513
  // ── Build dynamic headers ────────────────────────────
465
514
  // Priority:
466
515
  // 1. Cron-triggered: uid → cronUuid, with cron-specific headers
467
- // 2. Xiaoyi A2A: rawTaskId from AsyncLocalStorage, split on "&"
468
- // 3. UID-based fallback: sha256(uid).hex[:32]_timestamp
516
+ // 2. Xiaoyi A2A: taskId from Conversation info text (latest msg, steer-safe)
517
+ // 3. ALS fallback: rawTaskId from AsyncLocalStorage
518
+ // 4. UID-based fallback: sha256(uid).hex[:32]_timestamp
469
519
  const isCron = isCronTriggered(context.messages);
470
520
  if (isCron) {
471
521
  // fire 期 jobId 桥:把首条消息 `[cron:<jobId> ...]` 解析出的真实 jobId
@@ -497,11 +547,18 @@ export const xiaoyiProvider = {
497
547
  dynamicHeaders["x-cron-flag"] = "begin";
498
548
  logger.log(`[ALS-PROOF] provider headers source=cron`);
499
549
  }
550
+ else if (extractedTaskId) {
551
+ const sessionId = extractedTaskId.split("&")[0];
552
+ const interactionId = extractedTaskId.split("&")[1] ?? "";
553
+ dynamicHeaders[HEADER_TRACE_ID] = extractedTaskId;
554
+ dynamicHeaders[HEADER_SESSION_ID] = sessionId;
555
+ dynamicHeaders[HEADER_INTERACTION_ID] = interactionId;
556
+ logger.log(`[ALS-PROOF] provider headers source=text-extract traceId=${extractedTaskId} sessionId=${sessionId} interactionId=${interactionId}`);
557
+ }
500
558
  else {
501
- // ALS path: rawTaskId comes from the per-turn AsyncLocalStorage scope
502
- // set by bot.ts runWithSessionContext. Real upstream params.id is
503
- // `realSession&realInteraction`; tester UUIDs degenerate to the whole
504
- // id as sessionId with empty interactionId.
559
+ // ALS fallback: rawTaskId from the per-turn AsyncLocalStorage scope.
560
+ // Text extraction (above) is preferred because during steer the ALS
561
+ // scope may still hold the first message's taskId.
505
562
  const als = getCurrentSessionContext();
506
563
  const rawTaskId = als?.taskId;
507
564
  if (rawTaskId) {
@@ -533,8 +590,9 @@ export const xiaoyiProvider = {
533
590
  if (context.systemPrompt) {
534
591
  logger.log(`[xiaoyiprovider] system prompt length: ${context.systemPrompt.length}`);
535
592
  }
536
- // deviceType: read from ALS (no more text-extraction).
537
- const deviceType = getCurrentSessionContext()?.deviceType;
593
+ // deviceType: prefer text-extracted value, ALS as fallback.
594
+ const deviceType = extractedDeviceType
595
+ ?? getCurrentSessionContext()?.deviceType;
538
596
  // app_ver and sdk_api_version from session context (ALS)
539
597
  const appVer = sessionCtx?.appVer;
540
598
  const sdkApiVersion = sessionCtx?.sdkApiVersion;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynhcj/xiaoyi-channel",
3
- "version": "0.0.179-next",
3
+ "version": "0.0.180-next",
4
4
  "description": "OpenClaw Xiaoyi Channel plugin - Xiaoyi A2A protocol integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",