@ynhcj/xiaoyi-channel 1.1.25 → 1.1.26

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
@@ -270,7 +270,7 @@ export async function handleXYMessage(params) {
270
270
  SenderId: parsed.sessionId,
271
271
  Provider: "xiaoyi-channel",
272
272
  Surface: "xiaoyi-channel",
273
- MessageSid: `${parsed.taskId}_${deviceType}`,
273
+ MessageSid: `xiaoyi_${parsed.taskId}_${deviceType}`,
274
274
  Timestamp: Date.now(),
275
275
  WasMentioned: false,
276
276
  CommandAuthorized: true,
@@ -532,7 +532,7 @@ async function dispatchSteerWhenReady(params) {
532
532
  SenderId: sessionId,
533
533
  Provider: "xiaoyi-channel",
534
534
  Surface: "xiaoyi-channel",
535
- MessageSid: `${params.parsed.taskId}_${params.deviceType}`,
535
+ MessageSid: `xiaoyi_${params.parsed.taskId}_${params.deviceType}`,
536
536
  Timestamp: Date.now(),
537
537
  WasMentioned: false,
538
538
  CommandAuthorized: true,
@@ -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));
@@ -231,8 +236,6 @@ const HEADER_SESSION_ID = "x-session-id";
231
236
  const HEADER_INTERACTION_ID = "x-interaction-id";
232
237
  /** Internal key for passing fallback uid prefix from prepareExtraParams to wrapStreamFn. */
233
238
  const FALLBACK_PREFIX_KEY = "_xiaoyi_fallback_prefix";
234
- /** Internal key for passing deviceType from prepareExtraParams to wrapStreamFn. */
235
- const DEVICE_TYPE_KEY = "_xiaoyi_device_type";
236
239
  const SELF_EVOLUTION_PROMPT_BEGIN = "<self_evolution_prompt>";
237
240
  const SELF_EVOLUTION_PROMPT_END = "</self_evolution_prompt>";
238
241
  const SELF_EVOLUTION_ENABLED_PROMPT_SECTION = `
@@ -392,7 +395,9 @@ function trimUserMetadata(text) {
392
395
  }
393
396
  /**
394
397
  * Extract A2A taskId and deviceType from Conversation info JSON.
395
- * bot.ts stores them as MessageSid = "taskId_deviceType".
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.
396
401
  */
397
402
  function extractA2AFromConversationInfo(text) {
398
403
  const match = text.match(/Conversation info \(untrusted metadata\):\n```json\n([\s\S]*?)\n```/);
@@ -402,9 +407,9 @@ function extractA2AFromConversationInfo(text) {
402
407
  if (!msgIdMatch)
403
408
  return null;
404
409
  const parts = msgIdMatch[1].split("_");
405
- if (parts.length < 2)
410
+ if (parts.length < 3 || parts[0] !== "xiaoyi")
406
411
  return null;
407
- return { taskId: parts[0], deviceType: parts[1] };
412
+ return { taskId: parts[1], deviceType: parts[2] };
408
413
  }
409
414
  export const xiaoyiProvider = {
410
415
  id: "xiaoyiprovider",
@@ -413,31 +418,12 @@ export const xiaoyiProvider = {
413
418
  auth: [],
414
419
  isCacheTtlEligible: () => true,
415
420
  /**
416
- * Inject dynamic session params into extraParams so they flow
417
- * through to wrapStreamFn's ctx.extraParams.
418
- *
419
- * Priority:
420
- * 1. Session context (from AsyncLocalStorage, set by bot.ts)
421
- * 2. uid-based fallback: sha256(uid).hex[:32]_timestamp
422
- * 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.
423
425
  */
424
426
  prepareExtraParams: (ctx) => {
425
- const sessionCtx = getCurrentSessionContext();
426
- if (sessionCtx) {
427
- const taskId = sessionCtx.taskId;
428
- const sessionId = taskId.split("&")[0];
429
- const interactionId = taskId.split("&")[1] || "";
430
- return {
431
- ...ctx.extraParams,
432
- [HEADER_TRACE_ID]: taskId,
433
- [HEADER_SESSION_ID]: sessionId,
434
- [HEADER_INTERACTION_ID]: interactionId,
435
- [DEVICE_TYPE_KEY]: sessionCtx.deviceType ?? "",
436
- };
437
- }
438
- // Fallback: store uid prefix for lazy timestamp generation in wrapStreamFn.
439
- // This ensures each model call gets a fresh timestamp instead of reusing
440
- // the same one across tool-use loops and retries.
441
427
  const uid = getUidFromConfig(ctx.config);
442
428
  if (!uid)
443
429
  return undefined;
@@ -488,51 +474,46 @@ export const xiaoyiProvider = {
488
474
  }
489
475
  }
490
476
  // ── Build dynamic headers ────────────────────────────
491
- if (ctx.extraParams) {
492
- const fallbackPrefix = ctx.extraParams[FALLBACK_PREFIX_KEY];
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];
493
484
  if (typeof fallbackPrefix === "string") {
494
- // Fallback mode: generate fresh timestamp per request
495
- const isCron = isCronTriggered(context.messages);
496
485
  const fallbackValue = `${fallbackPrefix}_${Date.now()}`;
497
- dynamicHeaders[HEADER_TRACE_ID] = isCron ? `cron_${fallbackValue}` : fallbackValue;
486
+ dynamicHeaders[HEADER_TRACE_ID] = `cron_${fallbackValue}`;
498
487
  dynamicHeaders[HEADER_SESSION_ID] = fallbackValue;
499
488
  dynamicHeaders[HEADER_INTERACTION_ID] = fallbackValue;
500
- if (isCron) {
501
- const cronTitle = extractCronTitle(context.messages);
502
- if (cronTitle)
503
- dynamicHeaders["x-cron-title"] = encodeURIComponent(cronTitle);
504
- if (context.messages?.length === 1)
505
- dynamicHeaders["x-cron-flag"] = "begin";
506
- }
507
- }
508
- else if (extractedTaskId) {
509
- // Session mode: taskId extracted from Conversation info
510
- const traceId = extractedTaskId;
511
- const sessionId = traceId.split("&")[0];
512
- const interactionId = traceId.split("&")[1] ?? "";
513
- const isCron = isCronTriggered(context.messages);
514
- dynamicHeaders[HEADER_TRACE_ID] = isCron ? `cron_${traceId}_${Date.now()}` : traceId;
515
- if (isCron) {
516
- const cronTitle = extractCronTitle(context.messages);
517
- if (cronTitle)
518
- dynamicHeaders["x-cron-title"] = encodeURIComponent(cronTitle);
519
- if (context.messages?.length === 1)
520
- dynamicHeaders["x-cron-flag"] = "begin";
521
- }
522
- dynamicHeaders[HEADER_SESSION_ID] = sessionId;
523
- dynamicHeaders[HEADER_INTERACTION_ID] = interactionId;
524
489
  }
525
490
  else {
526
- // Fallback: use extraParams cached values
527
- const traceId = ctx.extraParams[HEADER_TRACE_ID];
528
- const sessionId = ctx.extraParams[HEADER_SESSION_ID];
529
- 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;
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;
536
517
  }
537
518
  }
538
519
  // 记录输入
@@ -546,9 +527,8 @@ export const xiaoyiProvider = {
546
527
  logger.log(`[xiaoyiprovider] system prompt length: ${context.systemPrompt.length}`);
547
528
  }
548
529
  // deviceType: prefer value extracted from Conversation info,
549
- // then extraParams, then ALS fallback.
550
- const extraParamsDeviceType = ctx.extraParams?.[DEVICE_TYPE_KEY] || undefined;
551
- const deviceType = (extractedDeviceType || extraParamsDeviceType)
530
+ // then ALS fallback.
531
+ const deviceType = extractedDeviceType
552
532
  ?? getCurrentSessionContext()?.deviceType;
553
533
  // 在发送给模型前,优化 systemPrompt 结构
554
534
  if (context.systemPrompt) {
@@ -1,4 +1,5 @@
1
1
  // Image Reading tool implementation
2
+ import { createHash } from "crypto";
2
3
  import { XYFileUploadService } from "../file-upload.js";
3
4
  import fetch from "node-fetch";
4
5
  import fs from "fs/promises";
@@ -50,9 +51,9 @@ async function processImageInput(imageInput, uploadService) {
50
51
  * Call image understanding API with streaming response
51
52
  * Supports both single image and multiple images (imageUrls array)
52
53
  */
53
- async function callImageUnderstandingAPI(imageUrls, text, apiKey, uid, fileUploadUrl) {
54
+ async function callImageUnderstandingAPI(imageUrls, text, apiKey, uid, fileUploadUrl, sessionId) {
54
55
  const apiUrl = `${fileUploadUrl}/celia-claw/v1/sse-api/skill/execute`;
55
- const traceId = uuidv4();
56
+ const traceId = `${createHash("sha256").update(uid).digest("hex").slice(0, 32)}_${Date.now()}`;
56
57
  const headers = {
57
58
  "Content-Type": "application/json",
58
59
  "Accept": "text/event-stream",
@@ -60,14 +61,14 @@ async function callImageUnderstandingAPI(imageUrls, text, apiKey, uid, fileUploa
60
61
  "x-api-key": apiKey,
61
62
  "x-request-from": "openclaw",
62
63
  "x-uid": uid,
63
- "x-skill-id": "image_comprehension",
64
+ "x-skill-id": "xiaoyi_image_comprehension",
64
65
  "x-prd-pkg-name": "com.huawei.hag",
65
66
  };
66
67
  const payload = {
67
68
  version: "1.0",
68
69
  session: {
69
70
  isNew: false,
70
- sessionId: "wangyu202410241921",
71
+ sessionId,
71
72
  interactionId: 0,
72
73
  },
73
74
  endpoint: {
@@ -165,7 +166,7 @@ async function callImageUnderstandingAPI(imageUrls, text, apiKey, uid, fileUploa
165
166
  * Supports both local file paths and remote URLs, up to 10 images at once.
166
167
  */
167
168
  export function createImageReadingTool(ctx) {
168
- const { config } = ctx;
169
+ const { config, sessionId } = ctx;
169
170
  return {
170
171
  name: "image_reading",
171
172
  label: "Image Reading",
@@ -209,7 +210,7 @@ export function createImageReadingTool(ctx) {
209
210
  allImageUrls.push(await processImageInput(imageInput, uploadService));
210
211
  }
211
212
  // Call image understanding API with all image URLs
212
- const caption = await callImageUnderstandingAPI(allImageUrls, prompt, config.apiKey, config.uid, config.fileUploadUrl);
213
+ const caption = await callImageUnderstandingAPI(allImageUrls, prompt, config.apiKey, config.uid, config.fileUploadUrl, sessionId);
213
214
  return {
214
215
  content: [
215
216
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynhcj/xiaoyi-channel",
3
- "version": "1.1.25",
3
+ "version": "1.1.26",
4
4
  "description": "OpenClaw Xiaoyi Channel plugin - Xiaoyi A2A protocol integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",