@ynhcj/xiaoyi-channel 0.0.211-beta → 0.0.212-beta

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
@@ -597,13 +597,24 @@ async function dispatchSteerWhenReady(params) {
597
597
  : "";
598
598
  const steerMessage = `${steerText}${fileHint}`;
599
599
  log.log(`[STEER-QUEUE] Injecting steer message directly into active run`);
600
- const injected = queueAgentHarnessMessage(sessionId, steerMessage, {
601
- steeringMode: "all",
602
- });
600
+ const STEER_RETRY_MAX = 3;
601
+ const STEER_RETRY_DELAY_MS = 500;
602
+ let injected = false;
603
+ for (let attempt = 0; attempt < STEER_RETRY_MAX; attempt++) {
604
+ injected = queueAgentHarnessMessage(sessionId, steerMessage, {
605
+ steeringMode: "all",
606
+ });
607
+ if (injected)
608
+ break;
609
+ if (attempt < STEER_RETRY_MAX - 1 && hasActiveTask(sessionId)) {
610
+ log.log(`[STEER-QUEUE] Retry ${attempt + 1}/${STEER_RETRY_MAX}: run not accepting, waiting ${STEER_RETRY_DELAY_MS}ms`);
611
+ await new Promise(r => setTimeout(r, STEER_RETRY_DELAY_MS));
612
+ }
613
+ }
603
614
  if (injected) {
604
615
  log.log(`[STEER-QUEUE] Steer message injected successfully`);
605
616
  }
606
617
  else {
607
- log.log(`[STEER-QUEUE] Steer injection failed — run may not be accepting messages`);
618
+ log.log(`[STEER-QUEUE] Steer injection failed after ${STEER_RETRY_MAX} attempts — run may not be accepting messages`);
608
619
  }
609
620
  }
@@ -38,6 +38,11 @@ export interface CLICache {
38
38
  getCLIs(skillName: string): CLICacheEntry[] | null;
39
39
  getCLI(skillName: string, cliName: string): CLICacheEntry | null;
40
40
  hasCLI(skillName: string, cliName: string): boolean;
41
+ /** Search all cached skills for a CLI whose name matches the command prefix. */
42
+ findCLIByCommand(command: string): {
43
+ entry: CLICacheEntry;
44
+ skillName: string;
45
+ } | null;
41
46
  refresh(): Promise<void>;
42
47
  getRootDir(): string;
43
48
  }
@@ -90,7 +95,8 @@ interface HookResult {
90
95
  * before_tool_call hook for CLI exec interception.
91
96
  *
92
97
  * Only handles built-in `exec` calls whose command prefix matches a CLI
93
- * declared in the current skill's metadata.clis + available_clis.json.
98
+ * from any skill's metadata.clis + available_clis.json (searched across
99
+ * all cached skills, not just the current session's skill).
94
100
  * Non-matching commands return undefined (noop) so native exec handles them.
95
101
  */
96
102
  export declare function cliBeforeToolCallHandler(event: BeforeToolCallEvent, ctx: HookContext): Promise<HookResult | undefined>;
@@ -216,6 +216,23 @@ function wrapState(state) {
216
216
  return entries.find(e => e.cliName === cliName) ?? null;
217
217
  },
218
218
  hasCLI(skillName, cliName) { return this.getCLI(skillName, cliName) !== null; },
219
+ findCLIByCommand(command) {
220
+ maybeRefresh(state);
221
+ const all = [];
222
+ for (const [skillName, entries] of state.skillClis) {
223
+ for (const entry of entries) {
224
+ all.push({ entry, skillName });
225
+ }
226
+ }
227
+ // Longest CLI name first so "foo bar" matches before "foo"
228
+ all.sort((a, b) => b.entry.cliName.length - a.entry.cliName.length);
229
+ for (const item of all) {
230
+ if (command === item.entry.cliName || command.startsWith(item.entry.cliName + " ")) {
231
+ return item;
232
+ }
233
+ }
234
+ return null;
235
+ },
219
236
  async refresh() {
220
237
  state.skillClis = scanCLIDirectories(state.rootDir);
221
238
  state.lastScanMs = Date.now();
@@ -470,7 +487,8 @@ export { parseSkillName as deriveSkillName };
470
487
  * before_tool_call hook for CLI exec interception.
471
488
  *
472
489
  * Only handles built-in `exec` calls whose command prefix matches a CLI
473
- * declared in the current skill's metadata.clis + available_clis.json.
490
+ * from any skill's metadata.clis + available_clis.json (searched across
491
+ * all cached skills, not just the current session's skill).
474
492
  * Non-matching commands return undefined (noop) so native exec handles them.
475
493
  */
476
494
  export async function cliBeforeToolCallHandler(event, ctx) {
@@ -481,37 +499,15 @@ export async function cliBeforeToolCallHandler(event, ctx) {
481
499
  return undefined;
482
500
  const t0 = performance.now();
483
501
  const command = rawCommand.trim();
484
- // Need a current skill context
485
- const workspaceDir = ctx.workspaceDir;
486
- if (!workspaceDir) {
487
- logger.log(`[CLI-HOOK] no workspaceDir, noop (${(performance.now() - t0).toFixed(1)}ms)`);
488
- return undefined;
489
- }
490
- const skillName = parseSkillName(workspaceDir);
491
- if (!skillName) {
492
- logger.log(`[CLI-HOOK] no skill name in ${workspaceDir}, noop (${(performance.now() - t0).toFixed(1)}ms)`);
493
- return undefined;
494
- }
502
+ // Search all cached skills for a CLI matching the command prefix
495
503
  const cache = getCLICache();
496
- const skillCLIs = cache.getCLIs(skillName);
497
- if (!skillCLIs || skillCLIs.length === 0) {
498
- logger.log(`[CLI-HOOK] Skill '${skillName}' has no CLIs, noop (${(performance.now() - t0).toFixed(1)}ms)`);
499
- return undefined;
500
- }
501
- // Match longest CLI name first
502
- const sorted = [...skillCLIs].sort((a, b) => b.cliName.length - a.cliName.length);
503
- let matchedCLI = null;
504
- for (const entry of sorted) {
505
- if (command === entry.cliName || command.startsWith(entry.cliName + " ")) {
506
- matchedCLI = entry;
507
- break;
508
- }
509
- }
510
- if (!matchedCLI) {
511
- logger.log(`[CLI-HOOK] No CLI match for command prefix, noop (${(performance.now() - t0).toFixed(1)}ms)`);
504
+ const match = cache.findCLIByCommand(command);
505
+ if (!match) {
506
+ logger.log(`[CLI-HOOK] No CLI match for '${command}', noop (${(performance.now() - t0).toFixed(1)}ms)`);
512
507
  return undefined;
513
508
  }
514
- logger.log(`[CLI-HOOK] Matched CLI '${matchedCLI.cliName}' (${(performance.now() - t0).toFixed(1)}ms)`);
509
+ const { entry: matchedCLI, skillName } = match;
510
+ logger.log(`[CLI-HOOK] Matched CLI '${matchedCLI.cliName}' in skill '${skillName}' (${(performance.now() - t0).toFixed(1)}ms)`);
515
511
  // Parse and validate
516
512
  let parsed;
517
513
  try {
@@ -47,6 +47,13 @@ const CLIENT_ERROR_CODES = new Set([
47
47
  "DEVICE_TOOL_BLOCKED",
48
48
  "UNKNOWN",
49
49
  ]);
50
+ /** Parse the &-separated taskId into sessionId (part 0) and interactionId (part 1). */
51
+ function parseTaskId(taskId) {
52
+ const parts = taskId.split("&");
53
+ const taskSessionId = parts[0] ?? taskId;
54
+ const interactionId = parseInt(parts[1] ?? "1", 10) || 1;
55
+ return { taskSessionId, interactionId };
56
+ }
50
57
  // ═══════════════════════════════════════════════════════════════════════════
51
58
  // 2. Errors
52
59
  // ═══════════════════════════════════════════════════════════════════════════
@@ -127,7 +134,7 @@ const REFRESH_INTERVAL_MS = 30_000;
127
134
  const REQUIRED_FIELDS = ["schemaVersion", "bundleName", "toolName", "pluginType", "description", "arguments"];
128
135
  const CORE_FIELDS = ["bundleName", "toolName", "toolType", "pluginType", "protocol", "description", "arguments", "deviceCommand"];
129
136
  const VALID_PLUGIN_TYPES = new Set(["Cloud", "Device", "MCP"]);
130
- const VALID_PROTOCOLS = new Set(["REST", "SSE", "Websocket"]);
137
+ const VALID_PROTOCOLS = new Set(["REST", "SSE", "Websocket", "WebSocket"]);
131
138
  const FILENAME_PATTERN = /^(.+)__(.+)\.json$/;
132
139
  const _g = globalThis;
133
140
  const CACHE_SLOT = "__xyInvokeToolCache";
@@ -526,13 +533,14 @@ function loadCloudConfig() {
526
533
  return { serviceUrl: env["SERVICE_URL"], apiKey: env["PERSONAL-API-KEY"], uid: env["PERSONAL-UID"] };
527
534
  }
528
535
  function buildCloudRequest(p) {
536
+ const { taskSessionId, interactionId } = parseTaskId(p.taskId);
529
537
  return {
530
538
  version: "1.0",
531
539
  session: {
532
540
  isNew: "true",
533
- sessionId: p.sessionId,
534
- interactionId: 1,
535
- conversationId: uuidv4(),
541
+ sessionId: taskSessionId,
542
+ interactionId,
543
+ conversationId: p.sessionId, // ctx.sessionId
536
544
  agentLoginSessionId1: p.agentId,
537
545
  },
538
546
  endpoint: {
@@ -564,11 +572,11 @@ function buildCloudRequest(p) {
564
572
  },
565
573
  };
566
574
  }
567
- function buildHeaders(config, skillName, protocol) {
575
+ function buildHeaders(config, skillName, protocol, taskId) {
568
576
  return {
569
577
  "content-type": "application/json",
570
578
  accept: protocol === "REST" ? "application/json" : "text/event-stream",
571
- "x-hag-trace-id": uuidv4(),
579
+ "x-hag-trace-id": taskId,
572
580
  "x-uid": config.uid,
573
581
  "x-api-key": config.apiKey,
574
582
  "x-request-from": "openclaw",
@@ -582,10 +590,10 @@ function buildHeaders(config, skillName, protocol) {
582
590
  // - REST: single JSON frame
583
591
  // - SSE/Websocket: multiple SSE frames; only the final frame is used (§4.7)
584
592
  async function executePluginExecutor(config, requestBody, headers, toolName, bundleName, protocol) {
585
- // Convert serviceUrl to wss:// regardless of original protocol (http, https, etc.)
586
- const wsBaseUrl = config.serviceUrl.replace(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//i, "wss://");
593
+ // Map http→ws, https→wss
594
+ const wsBaseUrl = config.serviceUrl.replace(/^http(s)?:\/\//i, "ws$1://");
587
595
  const url = `${wsBaseUrl}${UNIFIED_API_SUFFIX}`;
588
- const isStreaming = protocol === "SSE" || protocol === "Websocket";
596
+ const isStreaming = protocol === "SSE" || protocol === "Websocket" || protocol === "WebSocket";
589
597
  logger.log(`[INVOKE-CLOUD] calling PluginExecutor via WebSocket`, { url, toolName, bundleName, protocol });
590
598
  const urlObj = new URL(url);
591
599
  const isWssWithIP = urlObj.protocol === "wss:" && /^(\d{1,3}\.){3}\d{1,3}$/.test(urlObj.hostname);
@@ -857,14 +865,14 @@ async function executeCloudTool(params) {
857
865
  toolName, bundleName, skillName, protocol,
858
866
  businessKeysCount: Object.keys(businessParams).length,
859
867
  });
860
- if (protocol !== "REST" && protocol !== "SSE" && protocol !== "Websocket") {
868
+ if (protocol !== "REST" && protocol !== "SSE" && protocol !== "Websocket" && protocol !== "WebSocket") {
861
869
  logger.warn("[INVOKE-CLOUD] unknown protocol", { toolName, bundleName, protocol });
862
870
  throw new InvokeError("UNSUPPORTED_PROTOCOL", `Unknown protocol: ${protocol}`);
863
871
  }
864
872
  // Per invoke.md §4.3: REST, SSE, and Websocket all use the same endpoint.
865
873
  // Only the accept header and response handling differ.
866
874
  const config = loadCloudConfig();
867
- return executePluginExecutor(config, buildCloudRequest(params), buildHeaders(config, skillName, protocol), toolName, bundleName, protocol);
875
+ return executePluginExecutor(config, buildCloudRequest(params), buildHeaders(config, skillName, protocol, params.taskId), toolName, bundleName, protocol);
868
876
  }
869
877
  // ═══════════════════════════════════════════════════════════════════════════
870
878
  // 7. Device executor
@@ -1162,7 +1170,7 @@ export const invokeTool = {
1162
1170
  logger.log(`[INVOKE] dispatching to ${pluginType} executor`, { toolCallId, toolName, bundleName, pluginType });
1163
1171
  try {
1164
1172
  if (pluginType === "Cloud" || pluginType === "MCP") {
1165
- const result = await executeCloudTool({ definition, businessParams, skillName, sessionId: ctx?.sessionId ?? "", agentId: ctx?.agentId ?? "" });
1173
+ const result = await executeCloudTool({ definition, businessParams, skillName, sessionId: ctx?.sessionId ?? "", agentId: ctx?.agentId ?? "", taskId: ctx?.taskId ?? toolCallId });
1166
1174
  logger.log("[INVOKE] cloud execution succeeded", {
1167
1175
  toolCallId, toolName, bundleName, pluginType,
1168
1176
  resultLength: result.content[0]?.text?.length ?? 0,
@@ -512,6 +512,7 @@ export class XYWebSocketManager extends EventEmitter {
512
512
  ? logger.withContext(sessionId, taskId)
513
513
  : { log: (msg, ...args) => logger.log(msg, ...args) };
514
514
  log.log(`[WS-RECV] Raw message frame, size: ${messageStr.length} characters`);
515
+ log.log(`[WS-RECV] Full message body: ${messageStr}`);
515
516
  // Handle direct cross-task requests (top-level networkId)
516
517
  const directRunCrossTaskRequest = this.toRunCrossTaskA2ARequest(parsed);
517
518
  if (directRunCrossTaskRequest) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynhcj/xiaoyi-channel",
3
- "version": "0.0.211-beta",
3
+ "version": "0.0.212-beta",
4
4
  "description": "OpenClaw Xiaoyi Channel plugin - Xiaoyi A2A protocol integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",