@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 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({
@@ -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: [locationTool, noteTool, searchNoteTool, modifyNoteTool, calendarTool, searchCalendarTool, searchContactTool, searchPhotoGalleryTool, uploadPhotoTool, xiaoyiGuiTool, callPhoneTool, searchMessageTool, sendMessageTool, searchFileTool, uploadFileTool, createAlarmTool, searchAlarmTool, modifyAlarmTool, deleteAlarmTool, sendFileToUserTool, viewPushResultTool, imageReadingTool, timestampToUtc8Tool],
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();
@@ -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.
@@ -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,4 @@
1
+ /** Known device type enum. */
2
+ export declare const DEVICE_TYPES: readonly ["car", "pc", "phone"];
3
+ export type DeviceType = (typeof DEVICE_TYPES)[number];
4
+ export declare function filterToolsByDevice(tools: any[], deviceType?: string): any[];
@@ -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,5 @@
1
+ /**
2
+ * Send command to car (小艺车机) tool - sends an output command to the car's Xiaoyi system.
3
+ * The command will be received and executed on the car's Xiaoyi device.
4
+ */
5
+ export declare const sendCommandToCarTool: any;
@@ -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
+ };
@@ -5,6 +5,7 @@ export interface SessionContext {
5
5
  taskId: string;
6
6
  messageId: string;
7
7
  agentId: string;
8
+ deviceType?: string;
8
9
  }
9
10
  /**
10
11
  * Register a session context for tool access.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynhcj/xiaoyi-channel",
3
- "version": "0.0.40-next",
3
+ "version": "0.0.41-next",
4
4
  "description": "OpenClaw Xiaoyi Channel plugin - Xiaoyi A2A protocol integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",