@ynhcj/xiaoyi-channel 0.0.40-next → 0.0.41-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.
- package/dist/src/bot.js +7 -1
- package/dist/src/channel.js +11 -1
- package/dist/src/parser.d.ts +6 -0
- package/dist/src/parser.js +16 -0
- package/dist/src/tools/device-tool-map.d.ts +4 -0
- package/dist/src/tools/device-tool-map.js +23 -0
- package/dist/src/tools/send-command-to-car-tool.d.ts +5 -0
- package/dist/src/tools/send-command-to-car-tool.js +85 -0
- package/dist/src/tools/session-manager.d.ts +1 -0
- package/package.json +1 -1
package/dist/src/bot.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getXYRuntime } from "./runtime.js";
|
|
2
2
|
import { setCachedContext } from "./steer-injector.js";
|
|
3
3
|
import { createXYReplyDispatcher } from "./reply-dispatcher.js";
|
|
4
|
-
import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId, extractTriggerData } from "./parser.js";
|
|
4
|
+
import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId, extractDeviceType, extractTriggerData } from "./parser.js";
|
|
5
5
|
import { downloadFilesFromParts } from "./file-download.js";
|
|
6
6
|
import { resolveXYConfig } from "./config.js";
|
|
7
7
|
import { sendStatusUpdate, sendClearContextResponse, sendTasksCancelResponse, sendA2AResponse } from "./formatter.js";
|
|
@@ -126,6 +126,11 @@ export async function handleXYMessage(params) {
|
|
|
126
126
|
else {
|
|
127
127
|
log(`[BOT] ℹ️ No push_id found in message, will use config default`);
|
|
128
128
|
}
|
|
129
|
+
// Extract deviceType if present (same level as push_id in systemVariables)
|
|
130
|
+
const deviceType = extractDeviceType(parsed.parts);
|
|
131
|
+
if (deviceType) {
|
|
132
|
+
log(`[BOT] 📱 Extracted deviceType from user message: ${deviceType}`);
|
|
133
|
+
}
|
|
129
134
|
// 保存 runtime 信息到 .xiaoyiruntime 文件(异步,不阻塞主流程)
|
|
130
135
|
saveRuntimeInfo(webSocketSessionId || parsed.sessionId, // SESSION_ID (WebSocket 层级,如果没有则 fallback)
|
|
131
136
|
parsed.sessionId, // CONVERSATION_ID (param 里的 sessionId)
|
|
@@ -238,6 +243,7 @@ export async function handleXYMessage(params) {
|
|
|
238
243
|
taskId: parsed.taskId,
|
|
239
244
|
messageId: parsed.messageId,
|
|
240
245
|
agentId: route.accountId,
|
|
246
|
+
deviceType,
|
|
241
247
|
};
|
|
242
248
|
log(`[BOT-DISPATCH] ⏳ withReplyDispatcher starting, sessionKey=${route.sessionKey}`);
|
|
243
249
|
await core.channel.reply.withReplyDispatcher({
|
package/dist/src/channel.js
CHANGED
|
@@ -24,6 +24,10 @@ import { sendFileToUserTool } from "./tools/send-file-to-user-tool.js";
|
|
|
24
24
|
import { viewPushResultTool } from "./tools/view-push-result-tool.js";
|
|
25
25
|
import { imageReadingTool } from "./tools/image-reading-tool.js";
|
|
26
26
|
import { timestampToUtc8Tool } from "./tools/timestamp-to-utc8-tool.js";
|
|
27
|
+
import { sendCommandToCarTool } from "./tools/send-command-to-car-tool.js";
|
|
28
|
+
import { filterToolsByDevice } from "./tools/device-tool-map.js";
|
|
29
|
+
import { getCurrentSessionContext } from "./tools/session-manager.js";
|
|
30
|
+
import { logger } from "./utils/logger.js";
|
|
27
31
|
/**
|
|
28
32
|
* Xiaoyi Channel Plugin for OpenClaw.
|
|
29
33
|
* Implements Xiaoyi A2A protocol with dual WebSocket connections.
|
|
@@ -62,7 +66,13 @@ export const xyPlugin = {
|
|
|
62
66
|
schema: xyConfigSchema,
|
|
63
67
|
},
|
|
64
68
|
outbound: xyOutbound,
|
|
65
|
-
agentTools:
|
|
69
|
+
agentTools: () => {
|
|
70
|
+
const allTools = [locationTool, noteTool, searchNoteTool, modifyNoteTool, calendarTool, searchCalendarTool, searchContactTool, searchPhotoGalleryTool, uploadPhotoTool, xiaoyiGuiTool, callPhoneTool, searchMessageTool, sendMessageTool, searchFileTool, uploadFileTool, createAlarmTool, searchAlarmTool, modifyAlarmTool, deleteAlarmTool, sendFileToUserTool, viewPushResultTool, imageReadingTool, timestampToUtc8Tool, sendCommandToCarTool];
|
|
71
|
+
const ctx = getCurrentSessionContext();
|
|
72
|
+
const filtered = filterToolsByDevice(allTools, ctx?.deviceType);
|
|
73
|
+
logger.log(`[DEVICE-FILTER] deviceType=${ctx?.deviceType ?? "(none)"}, tools: ${allTools.length} → ${filtered.length} (${filtered.map(t => t.name).join(", ")})`);
|
|
74
|
+
return filtered;
|
|
75
|
+
},
|
|
66
76
|
messaging: {
|
|
67
77
|
normalizeTarget: (raw) => {
|
|
68
78
|
const trimmed = raw.trim();
|
package/dist/src/parser.d.ts
CHANGED
|
@@ -43,6 +43,12 @@ export declare function isTasksCancelMessage(method: string): boolean;
|
|
|
43
43
|
* Looks for push_id in data parts under variables.systemVariables.push_id
|
|
44
44
|
*/
|
|
45
45
|
export declare function extractPushId(parts: A2AMessagePart[]): string | null;
|
|
46
|
+
/**
|
|
47
|
+
* Extract deviceType from message parts.
|
|
48
|
+
* Looks for deviceType in data parts under variables.systemVariables.deviceType
|
|
49
|
+
* (same level as push_id).
|
|
50
|
+
*/
|
|
51
|
+
export declare function extractDeviceType(parts: A2AMessagePart[]): string | null;
|
|
46
52
|
/**
|
|
47
53
|
* Extract Trigger event data from message parts.
|
|
48
54
|
* Looks for Trigger events with pushDataId in data parts.
|
package/dist/src/parser.js
CHANGED
|
@@ -72,6 +72,22 @@ export function extractPushId(parts) {
|
|
|
72
72
|
}
|
|
73
73
|
return null;
|
|
74
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Extract deviceType from message parts.
|
|
77
|
+
* Looks for deviceType in data parts under variables.systemVariables.deviceType
|
|
78
|
+
* (same level as push_id).
|
|
79
|
+
*/
|
|
80
|
+
export function extractDeviceType(parts) {
|
|
81
|
+
for (const part of parts) {
|
|
82
|
+
if (part.kind === "data" && part.data) {
|
|
83
|
+
const deviceType = part.data.variables?.systemVariables?.deviceType;
|
|
84
|
+
if (deviceType && typeof deviceType === "string") {
|
|
85
|
+
return deviceType;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
75
91
|
/**
|
|
76
92
|
* Extract Trigger event data from message parts.
|
|
77
93
|
* Looks for Trigger events with pushDataId in data parts.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Device type to tool name mapping.
|
|
2
|
+
// Only tools listed under a device type are available for that device.
|
|
3
|
+
// Tools NOT listed here are available to ALL devices (no restriction).
|
|
4
|
+
/** Known device type enum. */
|
|
5
|
+
export const DEVICE_TYPES = ["car", "pc", "phone"];
|
|
6
|
+
/**
|
|
7
|
+
* Map: deviceType → tool names allowed for that device.
|
|
8
|
+
* undefined / empty deviceType → all tools available.
|
|
9
|
+
* Unrecognized deviceType → all tools available.
|
|
10
|
+
* Tool not listed in any device entry → available to all devices.
|
|
11
|
+
*/
|
|
12
|
+
const DEVICE_TOOL_ALLOWLIST = {
|
|
13
|
+
car: ["send_command_to_car"],
|
|
14
|
+
pc: ["location"],
|
|
15
|
+
};
|
|
16
|
+
export function filterToolsByDevice(tools, deviceType) {
|
|
17
|
+
if (!deviceType)
|
|
18
|
+
return tools;
|
|
19
|
+
const allowedTools = DEVICE_TOOL_ALLOWLIST[deviceType];
|
|
20
|
+
if (!allowedTools)
|
|
21
|
+
return tools; // unrecognized device → no filtering
|
|
22
|
+
return tools.filter((tool) => allowedTools.includes(tool.name));
|
|
23
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Send Command To Car tool implementation
|
|
2
|
+
import { sendCommand } from "../formatter.js";
|
|
3
|
+
import { getCurrentSessionContext } from "./session-manager.js";
|
|
4
|
+
import { logger } from "../utils/logger.js";
|
|
5
|
+
/**
|
|
6
|
+
* Send command to car (小艺车机) tool - sends an output command to the car's Xiaoyi system.
|
|
7
|
+
* The command will be received and executed on the car's Xiaoyi device.
|
|
8
|
+
*/
|
|
9
|
+
export const sendCommandToCarTool = {
|
|
10
|
+
name: "send_command_to_car",
|
|
11
|
+
label: "Send Command To Car",
|
|
12
|
+
description: "将输出指令发送给小艺车机,车机小艺会接收并执行该指令。注意:请勿重复调用此工具,如果超时或失败,最多重试一次。回复约束:如果工具返回没有授权或者其他报错,只需要完整描述没有授权或者其他报错内容即可,不需要主动给用户提供解决方案。",
|
|
13
|
+
parameters: {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
command: {
|
|
17
|
+
type: "string",
|
|
18
|
+
description: "要发送给车机的指令内容(对应intentParam中的out字段)",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
required: ["command"],
|
|
22
|
+
},
|
|
23
|
+
async execute(toolCallId, params) {
|
|
24
|
+
// Validate command parameter
|
|
25
|
+
if (!params.command || typeof params.command !== "string" || params.command.trim() === "") {
|
|
26
|
+
throw new Error("Missing required parameter: command must be a non-empty string");
|
|
27
|
+
}
|
|
28
|
+
// Get session context
|
|
29
|
+
const sessionContext = getCurrentSessionContext();
|
|
30
|
+
if (!sessionContext) {
|
|
31
|
+
throw new Error("No active XY session found. Send command to car tool can only be used during an active conversation.");
|
|
32
|
+
}
|
|
33
|
+
const { config, sessionId, taskId, messageId } = sessionContext;
|
|
34
|
+
// Build PlayStoryBook command
|
|
35
|
+
const command = {
|
|
36
|
+
header: {
|
|
37
|
+
namespace: "Common",
|
|
38
|
+
name: "Action",
|
|
39
|
+
},
|
|
40
|
+
payload: {
|
|
41
|
+
cardParam: {},
|
|
42
|
+
executeParam: {
|
|
43
|
+
achieveType: "INTENT",
|
|
44
|
+
actionResponse: true,
|
|
45
|
+
bundleName: "com.huawei.vassistantcar",
|
|
46
|
+
dimension: "",
|
|
47
|
+
executeMode: "background",
|
|
48
|
+
intentName: "PlayStoryBook",
|
|
49
|
+
intentParam: {
|
|
50
|
+
out: params.command,
|
|
51
|
+
},
|
|
52
|
+
needUnlock: true,
|
|
53
|
+
permissionId: [],
|
|
54
|
+
timeOut: 5,
|
|
55
|
+
},
|
|
56
|
+
needUploadResult: true,
|
|
57
|
+
pageControlRelated: false,
|
|
58
|
+
responses: [
|
|
59
|
+
{
|
|
60
|
+
displayText: "",
|
|
61
|
+
resultCode: "",
|
|
62
|
+
ttsText: "",
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
// Send command - fire and forget, return success once sent
|
|
68
|
+
await sendCommand({
|
|
69
|
+
config,
|
|
70
|
+
sessionId,
|
|
71
|
+
taskId,
|
|
72
|
+
messageId,
|
|
73
|
+
command,
|
|
74
|
+
});
|
|
75
|
+
logger.log("[sendCommandToCar] command sent to car successfully");
|
|
76
|
+
return {
|
|
77
|
+
content: [
|
|
78
|
+
{
|
|
79
|
+
type: "text",
|
|
80
|
+
text: JSON.stringify({ success: true, message: "指令已成功下发给车机" }),
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
};
|