@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 +15 -4
- package/dist/src/channel.js +2 -0
- package/dist/src/tools/hmos-cli.d.ts +7 -1
- package/dist/src/tools/hmos-cli.js +28 -31
- package/dist/src/tools/invoke.d.ts +49 -9
- package/dist/src/tools/invoke.js +139 -131
- package/dist/src/websocket.js +1 -0
- package/package.json +1 -1
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
|
|
601
|
-
|
|
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
|
}
|
package/dist/src/channel.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
//
|
|
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
|
|
496
|
-
if (!
|
|
497
|
-
logger.log(`[CLI-HOOK]
|
|
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
|
-
|
|
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
|
|
538
|
-
const sessionCtx =
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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 {};
|
package/dist/src/tools/invoke.js
CHANGED
|
@@ -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:
|
|
533
|
-
interactionId
|
|
534
|
-
conversationId:
|
|
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":
|
|
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
|
-
//
|
|
585
|
-
const wsBaseUrl = config.serviceUrl.replace(/^
|
|
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
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
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
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
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
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
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
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
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
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
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
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
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
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
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
|
-
|
|
1198
|
+
errorCode: err.code, errorMessage: err.message, retryable: err.retryable,
|
|
1196
1199
|
});
|
|
1197
|
-
return
|
|
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
|
+
};
|
package/dist/src/websocket.js
CHANGED
|
@@ -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) {
|