@ynhcj/xiaoyi-channel 0.0.153-beta → 0.0.155-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.
Files changed (55) hide show
  1. package/dist/index.js +23 -0
  2. package/dist/src/bot.js +9 -1
  3. package/dist/src/channel.js +59 -5
  4. package/dist/src/cron-command.d.ts +15 -0
  5. package/dist/src/cron-command.js +49 -0
  6. package/dist/src/formatter.d.ts +11 -1
  7. package/dist/src/formatter.js +22 -2
  8. package/dist/src/parser.d.ts +2 -1
  9. package/dist/src/parser.js +25 -0
  10. package/dist/src/push.d.ts +5 -0
  11. package/dist/src/push.js +78 -0
  12. package/dist/src/reply-dispatcher.js +73 -1
  13. package/dist/src/tools/agent-as-skill-tool.js +1 -0
  14. package/dist/src/tools/calendar-tool.js +1 -0
  15. package/dist/src/tools/call-phone-tool.js +1 -0
  16. package/dist/src/tools/create-alarm-tool.js +1 -0
  17. package/dist/src/tools/create-all-tools.js +4 -0
  18. package/dist/src/tools/delete-alarm-tool.js +1 -0
  19. package/dist/src/tools/discover-cross-devices-tool.d.ts +2 -0
  20. package/dist/src/tools/discover-cross-devices-tool.js +235 -0
  21. package/dist/src/tools/find-pc-devices-tool.js +1 -0
  22. package/dist/src/tools/location-tool.js +1 -0
  23. package/dist/src/tools/modify-alarm-tool.js +1 -0
  24. package/dist/src/tools/modify-note-tool.js +1 -0
  25. package/dist/src/tools/note-tool.js +1 -0
  26. package/dist/src/tools/query-app-message-tool.js +3 -2
  27. package/dist/src/tools/query-memory-data-tool.js +3 -2
  28. package/dist/src/tools/query-todo-task-tool.js +3 -2
  29. package/dist/src/tools/save-file-to-phone-tool.js +1 -0
  30. package/dist/src/tools/save-media-to-gallery-tool.js +1 -0
  31. package/dist/src/tools/search-alarm-tool.js +1 -0
  32. package/dist/src/tools/search-calendar-tool.js +1 -0
  33. package/dist/src/tools/search-contact-tool.js +1 -0
  34. package/dist/src/tools/search-email-tool.js +3 -2
  35. package/dist/src/tools/search-file-tool.js +1 -0
  36. package/dist/src/tools/search-message-tool.js +1 -0
  37. package/dist/src/tools/search-note-tool.js +1 -0
  38. package/dist/src/tools/search-photo-gallery-tool.js +3 -2
  39. package/dist/src/tools/send-cross-device-task-tool.d.ts +2 -0
  40. package/dist/src/tools/send-cross-device-task-tool.js +303 -0
  41. package/dist/src/tools/send-email-tool.js +3 -2
  42. package/dist/src/tools/send-message-tool.js +1 -0
  43. package/dist/src/tools/session-manager.d.ts +13 -1
  44. package/dist/src/tools/session-manager.js +38 -0
  45. package/dist/src/tools/upload-file-tool.d.ts +1 -1
  46. package/dist/src/tools/upload-file-tool.js +8 -2
  47. package/dist/src/tools/upload-photo-tool.js +3 -2
  48. package/dist/src/tools/xiaoyi-add-collection-tool.js +1 -0
  49. package/dist/src/tools/xiaoyi-collection-tool.js +1 -0
  50. package/dist/src/tools/xiaoyi-delete-collection-tool.js +1 -0
  51. package/dist/src/tools/xiaoyi-gui-tool.js +1 -0
  52. package/dist/src/types.d.ts +17 -0
  53. package/dist/src/websocket.d.ts +3 -0
  54. package/dist/src/websocket.js +168 -15
  55. package/package.json +2 -1
@@ -0,0 +1,235 @@
1
+ import { sendCommand, sendStatusUpdate } from "../formatter.js";
2
+ import { getXYWebSocketManager } from "../client.js";
3
+ import { getCurrentMessageId, getCurrentTaskId } from "../task-manager.js";
4
+ import { logger } from "../utils/logger.js";
5
+ const DISCOVER_DEVICES_INTENT = "SearchAllDeviceInfo";
6
+ const DISCOVER_DEVICES_BUNDLE = "com.huawei.hmos.vassistant";
7
+ const DISCOVER_DEVICES_TIMEOUT_MS = 30_000;
8
+ const LOG_TAG = "[GetPCDeviceList]";
9
+ const DISCOVER_DEVICES_STATUS_TEXT = "正在查询设备列表...";
10
+ const DEVICE_TYPE_LABELS = {
11
+ "14": "phone",
12
+ "17": "pad",
13
+ "131": "car",
14
+ "2607": "PC",
15
+ };
16
+ function buildResultText(result) {
17
+ return {
18
+ content: [
19
+ {
20
+ type: "text",
21
+ text: JSON.stringify(result),
22
+ },
23
+ ],
24
+ };
25
+ }
26
+ function normalizeDevices(rawDevices) {
27
+ if (!Array.isArray(rawDevices)) {
28
+ return [];
29
+ }
30
+ return rawDevices
31
+ .filter((item) => Boolean(item) && typeof item === "object")
32
+ .map((device) => {
33
+ const networkId = typeof device.networkId === "string"
34
+ ? device.networkId
35
+ : typeof device.deviceId === "string"
36
+ ? device.deviceId
37
+ : "";
38
+ const deviceTypeId = typeof device.deviceTypeId === "string"
39
+ ? device.deviceTypeId
40
+ : typeof device.deviceType === "string"
41
+ ? device.deviceType
42
+ : "";
43
+ return {
44
+ networkId,
45
+ deviceName: typeof device.deviceName === "string" ? device.deviceName : "",
46
+ deviceTypeId,
47
+ deviceTypeLabel: DEVICE_TYPE_LABELS[deviceTypeId] ?? "unknown",
48
+ nearby: device.nearby === true,
49
+ rawDevice: device,
50
+ };
51
+ });
52
+ }
53
+ function inferDesiredDeviceTypes(query) {
54
+ const normalized = query.toLowerCase();
55
+ if (/(pc|computer|desktop|laptop|notebook)/iu.test(normalized) || /电脑|台式机|笔记本/iu.test(query)) {
56
+ return ["2607"];
57
+ }
58
+ if (/(tablet|pad|ipad)/iu.test(normalized) || /平板/iu.test(query)) {
59
+ return ["17"];
60
+ }
61
+ if (/(phone|mobile)/iu.test(normalized) || /手机/iu.test(query)) {
62
+ return ["14"];
63
+ }
64
+ return [];
65
+ }
66
+ function sortByNearby(devices) {
67
+ return [...devices].sort((a, b) => Number(b.nearby) - Number(a.nearby));
68
+ }
69
+ function recommendDevices(query, devices) {
70
+ const desiredTypes = inferDesiredDeviceTypes(query);
71
+ if (desiredTypes.length === 0) {
72
+ return {
73
+ recommendedDevices: [],
74
+ recommendationReason: "No explicit target device type was detected in the query.",
75
+ needsUserSelection: devices.length > 1,
76
+ selectionPrompt: devices.length > 1
77
+ ? "The query does not identify a unique device type. Ask the user to choose a target device by deviceName or networkId before sending a cross-device task."
78
+ : "",
79
+ };
80
+ }
81
+ const matches = devices.filter((device) => desiredTypes.includes(device.deviceTypeId));
82
+ if (matches.length === 0) {
83
+ return {
84
+ recommendedDevices: [],
85
+ recommendationReason: `No discovered device matches requested type(s): ${desiredTypes.join(", ")}.`,
86
+ needsUserSelection: false,
87
+ selectionPrompt: "",
88
+ };
89
+ }
90
+ const sortedMatches = sortByNearby(matches);
91
+ return {
92
+ recommendedDevices: sortedMatches,
93
+ recommendationReason: `Matched requested device type(s): ${desiredTypes.join(", ")}. Nearby devices are ranked first.`,
94
+ needsUserSelection: sortedMatches.length > 1,
95
+ selectionPrompt: sortedMatches.length > 1
96
+ ? "Multiple candidate devices match the user request. Ask the user to choose one target device by deviceName or networkId before calling send_cross_device_task."
97
+ : "",
98
+ };
99
+ }
100
+ export function createDiscoverCrossDevicesTool(ctx) {
101
+ const { config, sessionId, taskId, messageId } = ctx;
102
+ return {
103
+ name: "discover_cross_devices",
104
+ label: "发现跨设备协作设备",
105
+ description: `跨设备协作的设备发现工具。
106
+
107
+ 当用户明确表达要从另一台设备获取、查找、使用或操作内容时,必须优先调用本工具,例如从 PC、电脑、平板、手机等设备获取文件或查找内容。
108
+
109
+ 本工具只做设备发现和目标设备推荐,不会读取副设备文件内容,不会上传文件,也不会真正下发跨端执行任务。`,
110
+ parameters: {
111
+ type: "object",
112
+ properties: {
113
+ query: {
114
+ type: "string",
115
+ description: "The user's original cross-device request, used to recommend the target device type.",
116
+ },
117
+ },
118
+ required: ["query"],
119
+ },
120
+ async execute(_toolCallId, params) {
121
+ const query = typeof params.query === "string" ? params.query.trim() : "";
122
+ logger.log(`${LOG_TAG} tool invoked`);
123
+ if (!query) {
124
+ return buildResultText({
125
+ success: false,
126
+ rawOutputs: null,
127
+ devices: [],
128
+ recommendedDevices: [],
129
+ recommendationReason: "",
130
+ message: "Missing required parameter: query.",
131
+ });
132
+ }
133
+ const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
134
+ const currentMessageId = getCurrentMessageId(sessionId) ?? messageId;
135
+ const wsManager = getXYWebSocketManager(config);
136
+ const command = {
137
+ header: {
138
+ namespace: "Common",
139
+ name: "Action",
140
+ },
141
+ payload: {
142
+ needUploadResult: true,
143
+ actionResponseConfig: {},
144
+ response: [],
145
+ executeParam: {
146
+ executeMode: "background",
147
+ intentName: DISCOVER_DEVICES_INTENT,
148
+ intentParam: {},
149
+ bundleName: DISCOVER_DEVICES_BUNDLE,
150
+ },
151
+ },
152
+ };
153
+ return new Promise((resolve) => {
154
+ let timeout;
155
+ let handler;
156
+ let settled = false;
157
+ const cleanup = () => {
158
+ clearTimeout(timeout);
159
+ wsManager.off("data-event", handler);
160
+ };
161
+ const finish = (result) => {
162
+ if (settled) {
163
+ return;
164
+ }
165
+ settled = true;
166
+ cleanup();
167
+ resolve(buildResultText(result));
168
+ };
169
+ handler = (event) => {
170
+ if (event.intentName !== DISCOVER_DEVICES_INTENT) {
171
+ return;
172
+ }
173
+ const rawOutputs = event.outputs ?? {};
174
+ const code = rawOutputs.code;
175
+ const success = event.status === "success" && String(code) === "0";
176
+ const devices = normalizeDevices(rawOutputs.result?.devices);
177
+ const recommendation = recommendDevices(query, devices);
178
+ logger.log(`${LOG_TAG} completed, success=${success}, deviceCount=${devices.length}, recommendedCount=${recommendation.recommendedDevices.length}`);
179
+ finish({
180
+ success,
181
+ rawOutputs,
182
+ devices,
183
+ recommendedDevices: recommendation.recommendedDevices,
184
+ recommendationReason: recommendation.recommendationReason,
185
+ needsUserSelection: recommendation.needsUserSelection,
186
+ selectionPrompt: recommendation.selectionPrompt,
187
+ message: success
188
+ ? recommendation.needsUserSelection
189
+ ? `Discovered ${devices.length} device(s). Multiple candidates may match; ask the user to choose the target device before sending a cross-device task.`
190
+ : `Discovered ${devices.length} device(s). The model should choose the final target device based on the user request.`
191
+ : "Device discovery failed on the device side.",
192
+ });
193
+ };
194
+ timeout = setTimeout(() => {
195
+ logger.log(`${LOG_TAG} timeout waiting UploadExeResult after ${DISCOVER_DEVICES_TIMEOUT_MS}ms`);
196
+ finish({
197
+ success: false,
198
+ rawOutputs: null,
199
+ devices: [],
200
+ recommendedDevices: [],
201
+ recommendationReason: "",
202
+ message: `Device discovery timed out after ${DISCOVER_DEVICES_TIMEOUT_MS / 1000} seconds.`,
203
+ });
204
+ }, DISCOVER_DEVICES_TIMEOUT_MS);
205
+ wsManager.on("data-event", handler);
206
+ sendStatusUpdate({
207
+ config,
208
+ sessionId,
209
+ taskId: currentTaskId,
210
+ messageId: currentMessageId,
211
+ text: DISCOVER_DEVICES_STATUS_TEXT,
212
+ state: "working",
213
+ })
214
+ .then(() => sendCommand({
215
+ config,
216
+ sessionId,
217
+ taskId: currentTaskId,
218
+ messageId: currentMessageId,
219
+ command,
220
+ }))
221
+ .catch((error) => {
222
+ logger.error(`${LOG_TAG} failed to send device discovery command: ${error instanceof Error ? error.message : String(error)}`);
223
+ finish({
224
+ success: false,
225
+ rawOutputs: null,
226
+ devices: [],
227
+ recommendedDevices: [],
228
+ recommendationReason: "",
229
+ message: `Failed to send device discovery command: ${error instanceof Error ? error.message : String(error)}`,
230
+ });
231
+ });
232
+ });
233
+ },
234
+ };
235
+ }
@@ -82,6 +82,7 @@ export function createFindPcDevicesTool(ctx) {
82
82
  taskId,
83
83
  messageId,
84
84
  command,
85
+ toolCallId,
85
86
  }).then(() => {
86
87
  }).catch((error) => {
87
88
  clearTimeout(timeout);
@@ -90,6 +90,7 @@ export function createLocationTool(ctx) {
90
90
  taskId: currentTaskId,
91
91
  messageId,
92
92
  command,
93
+ toolCallId,
93
94
  }).then(() => {
94
95
  }).catch((error) => {
95
96
  clearTimeout(timeout);
@@ -270,6 +270,7 @@ export function createModifyAlarmTool(ctx) {
270
270
  taskId: currentTaskId,
271
271
  messageId,
272
272
  command,
273
+ toolCallId,
273
274
  })
274
275
  .then(() => {
275
276
  })
@@ -111,6 +111,7 @@ export function createModifyNoteTool(ctx) {
111
111
  taskId: currentTaskId,
112
112
  messageId,
113
113
  command,
114
+ toolCallId,
114
115
  })
115
116
  .then(() => {
116
117
  })
@@ -128,6 +128,7 @@ export function createNoteTool(ctx) {
128
128
  taskId: currentTaskId,
129
129
  messageId,
130
130
  command,
131
+ toolCallId,
131
132
  }).catch((error) => {
132
133
  clearTimeout(timeout);
133
134
  wsManager.off("data-event", handler);
@@ -47,7 +47,7 @@ c. 调用工具前需认真检查调用参数是否满足工具要求
47
47
  },
48
48
  required: [],
49
49
  },
50
- async execute(_toolCallId, params) {
50
+ async execute(toolCallId, params) {
51
51
  const wsManager = getXYWebSocketManager(config);
52
52
  const intentParam = {};
53
53
  if (params.startTime !== undefined)
@@ -96,7 +96,7 @@ c. 调用工具前需认真检查调用参数是否满足工具要求
96
96
  return new Promise((resolve, reject) => {
97
97
  const timeout = setTimeout(() => {
98
98
  wsManager.off("data-event", handler);
99
- logger.error("超时: 查询通知消息超时(60秒)", { toolCallId: _toolCallId });
99
+ logger.error("超时: 查询通知消息超时(60秒)", { toolCallId: toolCallId });
100
100
  reject(new Error("查询通知消息超时(60秒)"));
101
101
  }, 60000);
102
102
  const handler = (event) => {
@@ -126,6 +126,7 @@ c. 调用工具前需认真检查调用参数是否满足工具要求
126
126
  taskId: currentTaskId,
127
127
  messageId,
128
128
  command,
129
+ toolCallId,
129
130
  })
130
131
  .then(() => { })
131
132
  .catch((error) => {
@@ -56,7 +56,7 @@ c. 调用工具前需认真检查调用参数是否满足工具要求
56
56
  },
57
57
  required: [],
58
58
  },
59
- async execute(_toolCallId, params) {
59
+ async execute(toolCallId, params) {
60
60
  const { category, subCategory } = params;
61
61
  // Validate category
62
62
  if (category && !VALID_CATEGORIES.includes(category)) {
@@ -112,7 +112,7 @@ c. 调用工具前需认真检查调用参数是否满足工具要求
112
112
  return new Promise((resolve, reject) => {
113
113
  const timeout = setTimeout(() => {
114
114
  wsManager.off("data-event", handler);
115
- logger.error("超时: 查询记忆数据超时(60秒)", { toolCallId: _toolCallId });
115
+ logger.error("超时: 查询记忆数据超时(60秒)", { toolCallId: toolCallId });
116
116
  reject(new Error("查询记忆数据超时(60秒)"));
117
117
  }, 60000);
118
118
  const handler = (event) => {
@@ -142,6 +142,7 @@ c. 调用工具前需认真检查调用参数是否满足工具要求
142
142
  taskId: currentTaskId,
143
143
  messageId,
144
144
  command,
145
+ toolCallId,
145
146
  })
146
147
  .then(() => { })
147
148
  .catch((error) => {
@@ -44,7 +44,7 @@ d. 当只传入 startTime 时,返回该时间点之后的所有任务;当只
44
44
  },
45
45
  required: [],
46
46
  },
47
- async execute(_toolCallId, params) {
47
+ async execute(toolCallId, params) {
48
48
  const { status } = params;
49
49
  if (status && !["all", "completed", "pending"].includes(status)) {
50
50
  throw new ToolInputError('status 参数只能为 "all"、"completed" 或 "pending"');
@@ -91,7 +91,7 @@ d. 当只传入 startTime 时,返回该时间点之后的所有任务;当只
91
91
  return new Promise((resolve, reject) => {
92
92
  const timeout = setTimeout(() => {
93
93
  wsManager.off("data-event", handler);
94
- logger.error("超时: 查询待办任务超时(60秒)", { toolCallId: _toolCallId });
94
+ logger.error("超时: 查询待办任务超时(60秒)", { toolCallId: toolCallId });
95
95
  reject(new Error("查询待办任务超时(60秒)"));
96
96
  }, 60000);
97
97
  const handler = (event) => {
@@ -121,6 +121,7 @@ d. 当只传入 startTime 时,返回该时间点之后的所有任务;当只
121
121
  taskId: currentTaskId,
122
122
  messageId,
123
123
  command,
124
+ toolCallId,
124
125
  })
125
126
  .then(() => { })
126
127
  .catch((error) => {
@@ -152,6 +152,7 @@ export function createSaveFileToPhoneTool(ctx) {
152
152
  taskId: currentTaskId,
153
153
  messageId,
154
154
  command,
155
+ toolCallId,
155
156
  })
156
157
  .then(() => {
157
158
  })
@@ -160,6 +160,7 @@ export function createSaveMediaToGalleryTool(ctx) {
160
160
  taskId: currentTaskId,
161
161
  messageId,
162
162
  command,
163
+ toolCallId,
163
164
  })
164
165
  .then(() => {
165
166
  })
@@ -202,6 +202,7 @@ b. 使用该工具之前需获取当前真实时间
202
202
  taskId: currentTaskId,
203
203
  messageId,
204
204
  command,
205
+ toolCallId,
205
206
  })
206
207
  .then(() => {
207
208
  })
@@ -173,6 +173,7 @@ d. 如果查询结果返回-303,代表查询结果为空
173
173
  taskId: currentTaskId,
174
174
  messageId,
175
175
  command,
176
+ toolCallId,
176
177
  })
177
178
  .then(() => {
178
179
  })
@@ -101,6 +101,7 @@ export function createSearchContactTool(ctx) {
101
101
  taskId: currentTaskId,
102
102
  messageId,
103
103
  command,
104
+ toolCallId,
104
105
  })
105
106
  .then(() => {
106
107
  })
@@ -40,7 +40,7 @@ b. 使用该工具之前需获取当前真实时间
40
40
  },
41
41
  required: ["queryText"],
42
42
  },
43
- async execute(_toolCallId, params) {
43
+ async execute(toolCallId, params) {
44
44
  // ===== Validate queryText =====
45
45
  if (!params.queryText || typeof params.queryText !== "string" || !params.queryText.trim()) {
46
46
  throw new Error("queryText 为必填参数,且不能为空字符串");
@@ -91,7 +91,7 @@ b. 使用该工具之前需获取当前真实时间
91
91
  return new Promise((resolve, reject) => {
92
92
  const timeout = setTimeout(() => {
93
93
  wsManager.off("data-event", handler);
94
- logger.error("超时: 检索邮件超时(60秒)", { toolCallId: _toolCallId });
94
+ logger.error("超时: 检索邮件超时(60秒)", { toolCallId: toolCallId });
95
95
  reject(new Error("检索邮件超时(60秒)"));
96
96
  }, 60000);
97
97
  // Listen for data events from WebSocket
@@ -124,6 +124,7 @@ b. 使用该工具之前需获取当前真实时间
124
124
  taskId: currentTaskId,
125
125
  messageId,
126
126
  command,
127
+ toolCallId,
127
128
  })
128
129
  .then(() => { })
129
130
  .catch((error) => {
@@ -117,6 +117,7 @@ export function createSearchFileTool(ctx) {
117
117
  taskId: currentTaskId,
118
118
  messageId,
119
119
  command,
120
+ toolCallId,
120
121
  })
121
122
  .then(() => {
122
123
  })
@@ -103,6 +103,7 @@ export function createSearchMessageTool(ctx) {
103
103
  taskId: currentTaskId,
104
104
  messageId,
105
105
  command,
106
+ toolCallId,
106
107
  })
107
108
  .then(() => {
108
109
  })
@@ -100,6 +100,7 @@ export function createSearchNoteTool(ctx) {
100
100
  taskId: currentTaskId,
101
101
  messageId,
102
102
  command,
103
+ toolCallId,
103
104
  }).then(() => {
104
105
  }).catch((error) => {
105
106
  clearTimeout(timeout);
@@ -75,7 +75,7 @@ export function createSearchPhotoGalleryTool(ctx) {
75
75
  const wsManager = getXYWebSocketManager(config);
76
76
  // Search for photos
77
77
  const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
78
- const outputs = await searchPhotos(wsManager, config, sessionId, currentTaskId, messageId, params.query);
78
+ const outputs = await searchPhotos(wsManager, config, sessionId, currentTaskId, messageId, toolCallId, params.query);
79
79
  return {
80
80
  content: [
81
81
  {
@@ -91,7 +91,7 @@ export function createSearchPhotoGalleryTool(ctx) {
91
91
  * Search for photos using query description
92
92
  * Returns complete event.outputs object
93
93
  */
94
- async function searchPhotos(wsManager, config, sessionId, taskId, messageId, query) {
94
+ async function searchPhotos(wsManager, config, sessionId, taskId, messageId, toolCallId, query) {
95
95
  const command = {
96
96
  header: {
97
97
  namespace: "Common",
@@ -151,6 +151,7 @@ async function searchPhotos(wsManager, config, sessionId, taskId, messageId, que
151
151
  taskId,
152
152
  messageId,
153
153
  command,
154
+ toolCallId,
154
155
  })
155
156
  .then(() => {
156
157
  })
@@ -0,0 +1,2 @@
1
+ import type { SessionContext } from "./session-manager.js";
2
+ export declare function createSendCrossDeviceTaskTool(ctx: SessionContext): any;