@ynhcj/xiaoyi-channel 0.0.210-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
  }
@@ -27,6 +27,7 @@ import { discoverCrossDevicesTool } from "./tools/discover-cross-devices-tool.js
27
27
  import { sendCrossDeviceTaskTool } from "./tools/send-cross-device-task-tool.js";
28
28
  import { displayA2UICardByPathTool } from "./tools/display-a2ui-card-bypath-tool.js";
29
29
  import { checkPluginPrivilegeTool } from "./tools/check-plugin-privilege-tool.js";
30
+ import { invokeTool } from "./tools/invoke.js";
30
31
  const ALL_TOOLS = [
31
32
  locationTool,
32
33
  discoverCrossDevicesTool,
@@ -50,6 +51,7 @@ const ALL_TOOLS = [
50
51
  loginTokenTool,
51
52
  agentAsSkillTool,
52
53
  checkPluginPrivilegeTool,
54
+ invokeTool,
53
55
  ];
54
56
  /**
55
57
  * Xiaoyi Channel Plugin for OpenClaw.
@@ -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>;
@@ -15,6 +15,7 @@ import path from "path";
15
15
  import os from "os";
16
16
  import { logger } from "../utils/logger.js";
17
17
  import { InvokeError, invokeErrorToResult } from "./invoke.js";
18
+ import { getCurrentSessionContext } from "./session-manager.js";
18
19
  import { getXYWebSocketManager } from "../client.js";
19
20
  import { sendCommand } from "../formatter.js";
20
21
  import { getCurrentTaskId } from "../task-manager.js";
@@ -215,6 +216,23 @@ function wrapState(state) {
215
216
  return entries.find(e => e.cliName === cliName) ?? null;
216
217
  },
217
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
+ },
218
236
  async refresh() {
219
237
  state.skillClis = scanCLIDirectories(state.rootDir);
220
238
  state.lastScanMs = Date.now();
@@ -469,7 +487,8 @@ export { parseSkillName as deriveSkillName };
469
487
  * before_tool_call hook for CLI exec interception.
470
488
  *
471
489
  * Only handles built-in `exec` calls whose command prefix matches a CLI
472
- * 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).
473
492
  * Non-matching commands return undefined (noop) so native exec handles them.
474
493
  */
475
494
  export async function cliBeforeToolCallHandler(event, ctx) {
@@ -480,37 +499,15 @@ export async function cliBeforeToolCallHandler(event, ctx) {
480
499
  return undefined;
481
500
  const t0 = performance.now();
482
501
  const command = rawCommand.trim();
483
- // Need a current skill context
484
- const workspaceDir = ctx.workspaceDir;
485
- if (!workspaceDir) {
486
- logger.log(`[CLI-HOOK] no workspaceDir, noop (${(performance.now() - t0).toFixed(1)}ms)`);
487
- return undefined;
488
- }
489
- const skillName = parseSkillName(workspaceDir);
490
- if (!skillName) {
491
- logger.log(`[CLI-HOOK] no skill name in ${workspaceDir}, noop (${(performance.now() - t0).toFixed(1)}ms)`);
492
- return undefined;
493
- }
502
+ // Search all cached skills for a CLI matching the command prefix
494
503
  const cache = getCLICache();
495
- const skillCLIs = cache.getCLIs(skillName);
496
- if (!skillCLIs || skillCLIs.length === 0) {
497
- logger.log(`[CLI-HOOK] Skill '${skillName}' has no CLIs, noop (${(performance.now() - t0).toFixed(1)}ms)`);
498
- return undefined;
499
- }
500
- // Match longest CLI name first
501
- const sorted = [...skillCLIs].sort((a, b) => b.cliName.length - a.cliName.length);
502
- let matchedCLI = null;
503
- for (const entry of sorted) {
504
- if (command === entry.cliName || command.startsWith(entry.cliName + " ")) {
505
- matchedCLI = entry;
506
- break;
507
- }
508
- }
509
- if (!matchedCLI) {
510
- 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)`);
511
507
  return undefined;
512
508
  }
513
- 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)`);
514
511
  // Parse and validate
515
512
  let parsed;
516
513
  try {
@@ -534,8 +531,8 @@ export async function cliBeforeToolCallHandler(event, ctx) {
534
531
  },
535
532
  };
536
533
  }
537
- // Get session context and execute
538
- const sessionCtx = (await import("./session-manager.js")).getCurrentSessionContext();
534
+ // Get session context via ALS (propagated through the async call chain)
535
+ const sessionCtx = getCurrentSessionContext();
539
536
  if (!sessionCtx) {
540
537
  logger.warn(`[CLI-HOOK] No session context, blocking (${(performance.now() - t0).toFixed(1)}ms)`);
541
538
  return { block: true, blockReason: JSON.stringify({ code: "CONFIG_MISSING", message: "No active session context for CLI execution", retryable: false }) };
@@ -18,8 +18,6 @@
18
18
  * 7. Device executor
19
19
  * 8. Invoke tool factory (public API)
20
20
  */
21
- import type { ChannelAgentTool } from "openclaw/plugin-sdk";
22
- import type { SessionContext } from "./session-manager.js";
23
21
  export type InvokeErrorCode = "INVALID_PARAM" | "INVALID_TOOL_DEFINITION" | "CONFIG_MISSING" | "AUTH_FAIL" | "PERMISSION_DENIED" | "CLI_COMMAND_BLOCKED" | "RATE_LIMIT" | "TIMEOUT" | "TOOL_NOT_FOUND" | "TOOL_CONFLICT" | "UNSUPPORTED_PLUGIN_TYPE" | "UNSUPPORTED_PROTOCOL" | "DEVICE_TOOL_BLOCKED" | "UPSTREAM_ERROR" | "NETWORK_ERROR" | "UNKNOWN";
24
22
  interface ExecuteResult {
25
23
  content: Array<{
@@ -38,11 +36,53 @@ export declare class InvokeError extends Error {
38
36
  get status(): number;
39
37
  }
40
38
  export declare function invokeErrorToResult(error: InvokeError): ExecuteResult;
41
- /**
42
- * Create the invoke meta-tool for the given session context.
43
- *
44
- * This is the ONLY exported function — the single integration point for
45
- * create-all-tools.ts.
46
- */
47
- export declare function createInvokeTool(ctx: SessionContext): ChannelAgentTool;
39
+ export declare const invokeTool: {
40
+ name: string;
41
+ label: string;
42
+ description: string;
43
+ parameters: {
44
+ type: string;
45
+ properties: {
46
+ functionName: {
47
+ type: string;
48
+ minLength: number;
49
+ description: string;
50
+ };
51
+ funcName: {
52
+ type: string;
53
+ minLength: number;
54
+ description: string;
55
+ };
56
+ arguments: {
57
+ type: string;
58
+ description: string;
59
+ properties: {
60
+ bundleName: {
61
+ type: string;
62
+ minLength: number;
63
+ description: string;
64
+ };
65
+ };
66
+ required: string[];
67
+ additionalProperties: boolean;
68
+ };
69
+ params: {
70
+ type: string;
71
+ description: string;
72
+ properties: {
73
+ bundleName: {
74
+ type: string;
75
+ minLength: number;
76
+ description: string;
77
+ };
78
+ };
79
+ required: string[];
80
+ additionalProperties: boolean;
81
+ };
82
+ };
83
+ required: any[];
84
+ additionalProperties: boolean;
85
+ };
86
+ execute(toolCallId: string, rawParams: unknown, _signal?: AbortSignal, _onUpdate?: (partialResult: any) => void): Promise<ExecuteResult>;
87
+ };
48
88
  export {};
@@ -27,6 +27,7 @@ import { logger } from "../utils/logger.js";
27
27
  import { getXYWebSocketManager } from "../client.js";
28
28
  import { sendCommand } from "../formatter.js";
29
29
  import { getCurrentTaskId } from "../task-manager.js";
30
+ import { getCurrentSessionContext } from "./session-manager.js";
30
31
  const RETRYABLE_CODES = new Set([
31
32
  "RATE_LIMIT",
32
33
  "TIMEOUT",
@@ -46,6 +47,13 @@ const CLIENT_ERROR_CODES = new Set([
46
47
  "DEVICE_TOOL_BLOCKED",
47
48
  "UNKNOWN",
48
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
+ }
49
57
  // ═══════════════════════════════════════════════════════════════════════════
50
58
  // 2. Errors
51
59
  // ═══════════════════════════════════════════════════════════════════════════
@@ -126,7 +134,7 @@ const REFRESH_INTERVAL_MS = 30_000;
126
134
  const REQUIRED_FIELDS = ["schemaVersion", "bundleName", "toolName", "pluginType", "description", "arguments"];
127
135
  const CORE_FIELDS = ["bundleName", "toolName", "toolType", "pluginType", "protocol", "description", "arguments", "deviceCommand"];
128
136
  const VALID_PLUGIN_TYPES = new Set(["Cloud", "Device", "MCP"]);
129
- const VALID_PROTOCOLS = new Set(["REST", "SSE", "Websocket"]);
137
+ const VALID_PROTOCOLS = new Set(["REST", "SSE", "Websocket", "WebSocket"]);
130
138
  const FILENAME_PATTERN = /^(.+)__(.+)\.json$/;
131
139
  const _g = globalThis;
132
140
  const CACHE_SLOT = "__xyInvokeToolCache";
@@ -525,13 +533,14 @@ function loadCloudConfig() {
525
533
  return { serviceUrl: env["SERVICE_URL"], apiKey: env["PERSONAL-API-KEY"], uid: env["PERSONAL-UID"] };
526
534
  }
527
535
  function buildCloudRequest(p) {
536
+ const { taskSessionId, interactionId } = parseTaskId(p.taskId);
528
537
  return {
529
538
  version: "1.0",
530
539
  session: {
531
540
  isNew: "true",
532
- sessionId: p.sessionId,
533
- interactionId: 1,
534
- conversationId: uuidv4(),
541
+ sessionId: taskSessionId,
542
+ interactionId,
543
+ conversationId: p.sessionId, // ctx.sessionId
535
544
  agentLoginSessionId1: p.agentId,
536
545
  },
537
546
  endpoint: {
@@ -563,11 +572,11 @@ function buildCloudRequest(p) {
563
572
  },
564
573
  };
565
574
  }
566
- function buildHeaders(config, skillName, protocol) {
575
+ function buildHeaders(config, skillName, protocol, taskId) {
567
576
  return {
568
577
  "content-type": "application/json",
569
578
  accept: protocol === "REST" ? "application/json" : "text/event-stream",
570
- "x-hag-trace-id": uuidv4(),
579
+ "x-hag-trace-id": taskId,
571
580
  "x-uid": config.uid,
572
581
  "x-api-key": config.apiKey,
573
582
  "x-request-from": "openclaw",
@@ -581,10 +590,10 @@ function buildHeaders(config, skillName, protocol) {
581
590
  // - REST: single JSON frame
582
591
  // - SSE/Websocket: multiple SSE frames; only the final frame is used (§4.7)
583
592
  async function executePluginExecutor(config, requestBody, headers, toolName, bundleName, protocol) {
584
- // Convert serviceUrl to wss:// regardless of original protocol (http, https, etc.)
585
- 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://");
586
595
  const url = `${wsBaseUrl}${UNIFIED_API_SUFFIX}`;
587
- const isStreaming = protocol === "SSE" || protocol === "Websocket";
596
+ const isStreaming = protocol === "SSE" || protocol === "Websocket" || protocol === "WebSocket";
588
597
  logger.log(`[INVOKE-CLOUD] calling PluginExecutor via WebSocket`, { url, toolName, bundleName, protocol });
589
598
  const urlObj = new URL(url);
590
599
  const isWssWithIP = urlObj.protocol === "wss:" && /^(\d{1,3}\.){3}\d{1,3}$/.test(urlObj.hostname);
@@ -856,14 +865,14 @@ async function executeCloudTool(params) {
856
865
  toolName, bundleName, skillName, protocol,
857
866
  businessKeysCount: Object.keys(businessParams).length,
858
867
  });
859
- if (protocol !== "REST" && protocol !== "SSE" && protocol !== "Websocket") {
868
+ if (protocol !== "REST" && protocol !== "SSE" && protocol !== "Websocket" && protocol !== "WebSocket") {
860
869
  logger.warn("[INVOKE-CLOUD] unknown protocol", { toolName, bundleName, protocol });
861
870
  throw new InvokeError("UNSUPPORTED_PROTOCOL", `Unknown protocol: ${protocol}`);
862
871
  }
863
872
  // Per invoke.md §4.3: REST, SSE, and Websocket all use the same endpoint.
864
873
  // Only the accept header and response handling differ.
865
874
  const config = loadCloudConfig();
866
- 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);
867
876
  }
868
877
  // ═══════════════════════════════════════════════════════════════════════════
869
878
  // 7. Device executor
@@ -1066,136 +1075,135 @@ function validateBusinessParams(businessParams, definition) {
1066
1075
  }
1067
1076
  return null;
1068
1077
  }
1069
- /**
1070
- * Create the invoke meta-tool for the given session context.
1071
- *
1072
- * This is the ONLY exported function — the single integration point for
1073
- * create-all-tools.ts.
1074
- */
1075
- export function createInvokeTool(ctx) {
1076
- return {
1077
- name: "invoke",
1078
- label: "Invoke",
1079
- description: "调用已安装 skill 中声明的工具。必须传 functionName(或funcName) 与 arguments(或params);functionName 的值等于工具定义中的 toolName;arguments 是包含 bundleName 和业务参数字段的对象;完整业务参数定义见 references/tools/<bundleName>__<toolName>.json。",
1080
- parameters: {
1081
- type: "object",
1082
- properties: {
1083
- functionName: {
1084
- type: "string",
1085
- minLength: 1,
1086
- description: "工具名称,值等于对应 references/tools JSON 中的 toolName,如 weather_query(优先使用;funcName 已废弃但仍兼容)。",
1087
- },
1088
- funcName: {
1089
- type: "string",
1090
- minLength: 1,
1091
- description: "[废弃] 请使用 functionName 替代。工具名称,值等于对应 references/tools JSON 中的 toolName。",
1092
- },
1093
- arguments: {
1094
- type: "object",
1095
- description: "包含定位字段 bundleName 与业务参数字段;除 bundleName 外的字段遵循对应 references/tools JSON 中的 arguments schema(优先使用;params 已废弃但仍兼容)。",
1096
- properties: {
1097
- bundleName: {
1098
- type: "string",
1099
- minLength: 1,
1100
- description: "HarmonyOS 应用唯一标识,如 com.example.weather。",
1101
- },
1078
+ // ═══════════════════════════════════════════════════════════════════════════
1079
+ // 8. Invoke tool (static matches ALL_TOOLS pattern)
1080
+ // ═══════════════════════════════════════════════════════════════════════════
1081
+ export const invokeTool = {
1082
+ name: "invoke",
1083
+ label: "Invoke",
1084
+ description: "调用已安装 skill 中声明的工具。必须传 functionName(或funcName) 与 arguments(或params);functionName 的值等于工具定义中的 toolName;arguments 是包含 bundleName 和业务参数字段的对象;完整业务参数定义见 references/tools/<bundleName>__<toolName>.json。",
1085
+ parameters: {
1086
+ type: "object",
1087
+ properties: {
1088
+ functionName: {
1089
+ type: "string",
1090
+ minLength: 1,
1091
+ description: "工具名称,值等于对应 references/tools JSON 中的 toolName,如 weather_query(优先使用;funcName 已废弃但仍兼容)。",
1092
+ },
1093
+ funcName: {
1094
+ type: "string",
1095
+ minLength: 1,
1096
+ description: "[废弃] 请使用 functionName 替代。工具名称,值等于对应 references/tools JSON 中的 toolName。",
1097
+ },
1098
+ arguments: {
1099
+ type: "object",
1100
+ description: "包含定位字段 bundleName 与业务参数字段;除 bundleName 外的字段遵循对应 references/tools JSON 中的 arguments schema(优先使用;params 已废弃但仍兼容)。",
1101
+ properties: {
1102
+ bundleName: {
1103
+ type: "string",
1104
+ minLength: 1,
1105
+ description: "HarmonyOS 应用唯一标识,如 com.example.weather。",
1102
1106
  },
1103
- required: ["bundleName"],
1104
- additionalProperties: true,
1105
1107
  },
1106
- params: {
1107
- type: "object",
1108
- description: "[废弃] 请使用 arguments 替代。包含定位字段 bundleName 与业务参数字段。",
1109
- properties: {
1110
- bundleName: {
1111
- type: "string",
1112
- minLength: 1,
1113
- description: "HarmonyOS 应用唯一标识,如 com.example.weather。",
1114
- },
1108
+ required: ["bundleName"],
1109
+ additionalProperties: true,
1110
+ },
1111
+ params: {
1112
+ type: "object",
1113
+ description: "[废弃] 请使用 arguments 替代。包含定位字段 bundleName 与业务参数字段。",
1114
+ properties: {
1115
+ bundleName: {
1116
+ type: "string",
1117
+ minLength: 1,
1118
+ description: "HarmonyOS 应用唯一标识,如 com.example.weather。",
1115
1119
  },
1116
- required: ["bundleName"],
1117
- additionalProperties: true,
1118
1120
  },
1121
+ required: ["bundleName"],
1122
+ additionalProperties: true,
1119
1123
  },
1120
- required: [],
1121
- additionalProperties: false,
1122
1124
  },
1123
- async execute(toolCallId, rawParams, _signal, _onUpdate) {
1124
- // Layer 1: input validation
1125
- const validated = validateAndExtract(rawParams);
1126
- if ("content" in validated) {
1127
- logger.warn("[INVOKE] input validation failed", { toolCallId });
1128
- return validated;
1129
- }
1130
- const { toolName, bundleName, businessParams } = validated;
1131
- const businessKeys = Object.keys(businessParams);
1132
- logger.log("[INVOKE] tool called", {
1133
- toolCallId, toolName, bundleName,
1134
- businessKeys: businessKeys.join(","),
1135
- businessKeysCount: businessKeys.length,
1136
- });
1137
- // Layer 2: cache lookup
1138
- const cache = getToolCache();
1139
- const entry = cache.get(bundleName, toolName);
1140
- if (!entry) {
1141
- const conflict = cache.getConflict(bundleName, toolName);
1142
- if (conflict) {
1143
- const files = conflict.entries.map((e) => e.filePath).join(", ");
1144
- logger.warn("[INVOKE] tool conflict", { toolCallId, toolName, bundleName, conflictCount: conflict.entries.length });
1145
- return invokeErrorToResult(new InvokeError("TOOL_CONFLICT", `Multiple conflicting definitions for '${toolName}' in bundle '${bundleName}': ${files}`));
1146
- }
1147
- logger.warn("[INVOKE] tool not found", { toolCallId, toolName, bundleName });
1148
- return invokeErrorToResult(new InvokeError("TOOL_NOT_FOUND", `Tool '${toolName}' not found in bundle '${bundleName}'.`));
1125
+ required: [],
1126
+ additionalProperties: false,
1127
+ },
1128
+ async execute(toolCallId, rawParams, _signal, _onUpdate) {
1129
+ const ctx = getCurrentSessionContext();
1130
+ // Layer 1: input validation
1131
+ const validated = validateAndExtract(rawParams);
1132
+ if ("content" in validated) {
1133
+ logger.warn("[INVOKE] input validation failed", { toolCallId });
1134
+ return validated;
1135
+ }
1136
+ const { toolName, bundleName, businessParams } = validated;
1137
+ const businessKeys = Object.keys(businessParams);
1138
+ logger.log("[INVOKE] tool called", {
1139
+ toolCallId, toolName, bundleName,
1140
+ businessKeys: businessKeys.join(","),
1141
+ businessKeysCount: businessKeys.length,
1142
+ });
1143
+ // Layer 2: cache lookup
1144
+ const cache = getToolCache();
1145
+ const entry = cache.get(bundleName, toolName);
1146
+ if (!entry) {
1147
+ const conflict = cache.getConflict(bundleName, toolName);
1148
+ if (conflict) {
1149
+ const files = conflict.entries.map((e) => e.filePath).join(", ");
1150
+ logger.warn("[INVOKE] tool conflict", { toolCallId, toolName, bundleName, conflictCount: conflict.entries.length });
1151
+ return invokeErrorToResult(new InvokeError("TOOL_CONFLICT", `Multiple conflicting definitions for '${toolName}' in bundle '${bundleName}': ${files}`));
1149
1152
  }
1150
- const { definition, skillName } = entry;
1151
- logger.log("[INVOKE] cache hit", {
1152
- toolCallId, toolName, bundleName, skillName,
1153
- pluginType: definition.pluginType,
1154
- protocol: definition.protocol ?? "N/A",
1155
- });
1156
- // Layer 2b: business param validation
1157
- const paramError = validateBusinessParams(businessParams, definition);
1158
- if (paramError) {
1159
- logger.warn("[INVOKE] business param validation failed", { toolCallId, toolName, bundleName });
1160
- return paramError;
1153
+ logger.warn("[INVOKE] tool not found", { toolCallId, toolName, bundleName });
1154
+ return invokeErrorToResult(new InvokeError("TOOL_NOT_FOUND", `Tool '${toolName}' not found in bundle '${bundleName}'.`));
1155
+ }
1156
+ const { definition, skillName } = entry;
1157
+ logger.log("[INVOKE] cache hit", {
1158
+ toolCallId, toolName, bundleName, skillName,
1159
+ pluginType: definition.pluginType,
1160
+ protocol: definition.protocol ?? "N/A",
1161
+ });
1162
+ // Layer 2b: business param validation
1163
+ const paramError = validateBusinessParams(businessParams, definition);
1164
+ if (paramError) {
1165
+ logger.warn("[INVOKE] business param validation failed", { toolCallId, toolName, bundleName });
1166
+ return paramError;
1167
+ }
1168
+ // Layer 3: execute
1169
+ const pluginType = definition.pluginType;
1170
+ logger.log(`[INVOKE] dispatching to ${pluginType} executor`, { toolCallId, toolName, bundleName, pluginType });
1171
+ try {
1172
+ if (pluginType === "Cloud" || pluginType === "MCP") {
1173
+ const result = await executeCloudTool({ definition, businessParams, skillName, sessionId: ctx?.sessionId ?? "", agentId: ctx?.agentId ?? "", taskId: ctx?.taskId ?? toolCallId });
1174
+ logger.log("[INVOKE] cloud execution succeeded", {
1175
+ toolCallId, toolName, bundleName, pluginType,
1176
+ resultLength: result.content[0]?.text?.length ?? 0,
1177
+ });
1178
+ return result;
1161
1179
  }
1162
- // Layer 3: execute
1163
- const pluginType = definition.pluginType;
1164
- logger.log(`[INVOKE] dispatching to ${pluginType} executor`, { toolCallId, toolName, bundleName, pluginType });
1165
- try {
1166
- if (pluginType === "Cloud" || pluginType === "MCP") {
1167
- const result = await executeCloudTool({ definition, businessParams, skillName, sessionId: ctx.sessionId, agentId: ctx.agentId });
1168
- logger.log("[INVOKE] cloud execution succeeded", {
1169
- toolCallId, toolName, bundleName, pluginType,
1170
- resultLength: result.content[0]?.text?.length ?? 0,
1171
- });
1172
- return result;
1180
+ if (pluginType === "Device") {
1181
+ if (!ctx) {
1182
+ return invokeErrorToResult(new InvokeError("DEVICE_TOOL_BLOCKED", "Device tools require an active session context."));
1173
1183
  }
1174
- if (pluginType === "Device") {
1175
- const result = await executeDeviceTool({ definition, businessParams, toolCallId }, ctx);
1176
- logger.log("[INVOKE] device execution succeeded", {
1177
- toolCallId, toolName, bundleName,
1178
- resultLength: result.content[0]?.text?.length ?? 0,
1179
- });
1180
- return result;
1181
- }
1182
- logger.warn("[INVOKE] unsupported pluginType", { toolCallId, toolName, bundleName, pluginType });
1183
- return invokeErrorToResult(new InvokeError("UNSUPPORTED_PLUGIN_TYPE", `Unsupported pluginType '${pluginType}'. Must be Cloud, Device, or MCP.`));
1184
+ const result = await executeDeviceTool({ definition, businessParams, toolCallId }, ctx);
1185
+ logger.log("[INVOKE] device execution succeeded", {
1186
+ toolCallId, toolName, bundleName,
1187
+ resultLength: result.content[0]?.text?.length ?? 0,
1188
+ });
1189
+ return result;
1184
1190
  }
1185
- catch (err) {
1186
- if (err instanceof InvokeError) {
1187
- logger.warn("[INVOKE] invocation error", {
1188
- toolCallId, toolName, bundleName, pluginType,
1189
- errorCode: err.code, errorMessage: err.message, retryable: err.retryable,
1190
- });
1191
- return invokeErrorToResult(err);
1192
- }
1193
- logger.error("[INVOKE] unexpected execution error", {
1191
+ logger.warn("[INVOKE] unsupported pluginType", { toolCallId, toolName, bundleName, pluginType });
1192
+ return invokeErrorToResult(new InvokeError("UNSUPPORTED_PLUGIN_TYPE", `Unsupported pluginType '${pluginType}'. Must be Cloud, Device, or MCP.`));
1193
+ }
1194
+ catch (err) {
1195
+ if (err instanceof InvokeError) {
1196
+ logger.warn("[INVOKE] invocation error", {
1194
1197
  toolCallId, toolName, bundleName, pluginType,
1195
- error: err instanceof Error ? `${err.name}: ${err.message}` : String(err),
1198
+ errorCode: err.code, errorMessage: err.message, retryable: err.retryable,
1196
1199
  });
1197
- return unknownErrorToResult(err);
1200
+ return invokeErrorToResult(err);
1198
1201
  }
1199
- },
1200
- };
1201
- }
1202
+ logger.error("[INVOKE] unexpected execution error", {
1203
+ toolCallId, toolName, bundleName, pluginType,
1204
+ error: err instanceof Error ? `${err.name}: ${err.message}` : String(err),
1205
+ });
1206
+ return unknownErrorToResult(err);
1207
+ }
1208
+ },
1209
+ };
@@ -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.210-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",