@ynhcj/xiaoyi-channel 0.0.26-beta → 0.0.28-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 (36) hide show
  1. package/dist/src/bot.js +13 -3
  2. package/dist/src/channel.js +11 -1
  3. package/dist/src/outbound.js +88 -76
  4. package/dist/src/tools/calendar-tool.js +2 -2
  5. package/dist/src/tools/call-phone-tool.d.ts +5 -0
  6. package/dist/src/tools/call-phone-tool.js +183 -0
  7. package/dist/src/tools/create-alarm-tool.d.ts +7 -0
  8. package/dist/src/tools/create-alarm-tool.js +444 -0
  9. package/dist/src/tools/delete-alarm-tool.d.ts +11 -0
  10. package/dist/src/tools/delete-alarm-tool.js +238 -0
  11. package/dist/src/tools/location-tool.js +2 -2
  12. package/dist/src/tools/modify-alarm-tool.d.ts +9 -0
  13. package/dist/src/tools/modify-alarm-tool.js +474 -0
  14. package/dist/src/tools/modify-note-tool.js +2 -2
  15. package/dist/src/tools/note-tool.js +2 -2
  16. package/dist/src/tools/search-alarm-tool.d.ts +8 -0
  17. package/dist/src/tools/search-alarm-tool.js +389 -0
  18. package/dist/src/tools/search-calendar-tool.js +2 -2
  19. package/dist/src/tools/search-contact-tool.js +2 -2
  20. package/dist/src/tools/search-file-tool.d.ts +5 -0
  21. package/dist/src/tools/search-file-tool.js +173 -0
  22. package/dist/src/tools/search-message-tool.d.ts +5 -0
  23. package/dist/src/tools/search-message-tool.js +173 -0
  24. package/dist/src/tools/search-note-tool.js +2 -2
  25. package/dist/src/tools/search-photo-gallery-tool.js +2 -2
  26. package/dist/src/tools/send-file-to-user-tool.d.ts +5 -0
  27. package/dist/src/tools/send-file-to-user-tool.js +290 -0
  28. package/dist/src/tools/send-message-tool.d.ts +5 -0
  29. package/dist/src/tools/send-message-tool.js +189 -0
  30. package/dist/src/tools/session-manager.d.ts +12 -0
  31. package/dist/src/tools/session-manager.js +33 -0
  32. package/dist/src/tools/upload-file-tool.d.ts +13 -0
  33. package/dist/src/tools/upload-file-tool.js +265 -0
  34. package/dist/src/tools/upload-photo-tool.js +2 -2
  35. package/dist/src/tools/xiaoyi-gui-tool.js +2 -2
  36. package/package.json +1 -1
@@ -0,0 +1,189 @@
1
+ import { getXYWebSocketManager } from "../client.js";
2
+ import { sendCommand } from "../formatter.js";
3
+ import { getCurrentSessionContext } from "./session-manager.js";
4
+ import { logger } from "../utils/logger.js";
5
+ /**
6
+ * XY send message tool - sends SMS message on user's device.
7
+ * Requires phoneNumber (with +86 prefix) and content parameters.
8
+ */
9
+ export const sendMessageTool = {
10
+ name: "send_message",
11
+ label: "Send Message",
12
+ description: "通过手机发送短信。需要提供接收方手机号码和短信内容。手机号码会自动添加+86前缀(如果没有的话)。注意:操作超时时间为60秒,请勿重复调用此工具,如果超时或失败,最多重试一次。",
13
+ parameters: {
14
+ type: "object",
15
+ properties: {
16
+ phoneNumber: {
17
+ type: "string",
18
+ description: "接收方手机号码(会自动添加+86前缀)",
19
+ },
20
+ content: {
21
+ type: "string",
22
+ description: "短信内容",
23
+ },
24
+ },
25
+ required: ["phoneNumber", "content"],
26
+ },
27
+ async execute(toolCallId, params) {
28
+ logger.log(`[SEND_MESSAGE_TOOL] 🚀 Starting execution`);
29
+ logger.log(`[SEND_MESSAGE_TOOL] - toolCallId: ${toolCallId}`);
30
+ logger.log(`[SEND_MESSAGE_TOOL] - params:`, JSON.stringify(params));
31
+ logger.log(`[SEND_MESSAGE_TOOL] - timestamp: ${new Date().toISOString()}`);
32
+ // Validate phoneNumber parameter
33
+ if (!params.phoneNumber || typeof params.phoneNumber !== "string" || params.phoneNumber.trim() === "") {
34
+ logger.error(`[SEND_MESSAGE_TOOL] ❌ Missing or invalid phoneNumber parameter`);
35
+ throw new Error("Missing required parameter: phoneNumber must be a non-empty string");
36
+ }
37
+ // Validate content parameter
38
+ if (!params.content || typeof params.content !== "string" || params.content.trim() === "") {
39
+ logger.error(`[SEND_MESSAGE_TOOL] ❌ Missing or invalid content parameter`);
40
+ throw new Error("Missing required parameter: content must be a non-empty string");
41
+ }
42
+ // Normalize phone number: add +86 prefix if not present
43
+ let phoneNumber = params.phoneNumber.trim();
44
+ if (!phoneNumber.startsWith("+86")) {
45
+ // Remove leading 0 if present (e.g., 086 -> 86)
46
+ if (phoneNumber.startsWith("0")) {
47
+ phoneNumber = phoneNumber.substring(1);
48
+ }
49
+ // Remove +86 or 86 prefix if already present to avoid duplication
50
+ if (phoneNumber.startsWith("86")) {
51
+ phoneNumber = phoneNumber.substring(2);
52
+ }
53
+ phoneNumber = `+86${phoneNumber}`;
54
+ logger.log(`[SEND_MESSAGE_TOOL] 📞 Normalized phone number: ${params.phoneNumber} -> ${phoneNumber}`);
55
+ }
56
+ logger.log(`[SEND_MESSAGE_TOOL] 📤 Preparing to send message`);
57
+ logger.log(`[SEND_MESSAGE_TOOL] - phoneNumber: ${phoneNumber}`);
58
+ logger.log(`[SEND_MESSAGE_TOOL] - content length: ${params.content.length} characters`);
59
+ // Get session context
60
+ logger.log(`[SEND_MESSAGE_TOOL] 🔍 Attempting to get session context...`);
61
+ const sessionContext = getCurrentSessionContext();
62
+ if (!sessionContext) {
63
+ logger.error(`[SEND_MESSAGE_TOOL] ❌ FAILED: No active session found!`);
64
+ logger.error(`[SEND_MESSAGE_TOOL] - toolCallId: ${toolCallId}`);
65
+ throw new Error("No active XY session found. Send message tool can only be used during an active conversation.");
66
+ }
67
+ logger.log(`[SEND_MESSAGE_TOOL] ✅ Session context found`);
68
+ logger.log(`[SEND_MESSAGE_TOOL] - sessionId: ${sessionContext.sessionId}`);
69
+ logger.log(`[SEND_MESSAGE_TOOL] - taskId: ${sessionContext.taskId}`);
70
+ logger.log(`[SEND_MESSAGE_TOOL] - messageId: ${sessionContext.messageId}`);
71
+ const { config, sessionId, taskId, messageId } = sessionContext;
72
+ // Get WebSocket manager
73
+ logger.log(`[SEND_MESSAGE_TOOL] 🔌 Getting WebSocket manager...`);
74
+ const wsManager = getXYWebSocketManager(config);
75
+ logger.log(`[SEND_MESSAGE_TOOL] ✅ WebSocket manager obtained`);
76
+ // Build SendShortMessage command
77
+ logger.log(`[SEND_MESSAGE_TOOL] 📦 Building SendShortMessage command...`);
78
+ const command = {
79
+ header: {
80
+ namespace: "Common",
81
+ name: "Action",
82
+ },
83
+ payload: {
84
+ cardParam: {},
85
+ executeParam: {
86
+ executeMode: "background",
87
+ intentName: "SendShortMessage",
88
+ bundleName: "com.huawei.hmos.aidispatchservice",
89
+ needUnlock: true,
90
+ actionResponse: true,
91
+ appType: "OHOS_APP",
92
+ timeOut: 5,
93
+ intentParam: {
94
+ phoneNumber: phoneNumber,
95
+ content: params.content.trim(),
96
+ },
97
+ permissionId: [],
98
+ achieveType: "INTENT",
99
+ },
100
+ responses: [
101
+ {
102
+ resultCode: "",
103
+ displayText: "",
104
+ ttsText: "",
105
+ },
106
+ ],
107
+ needUploadResult: true,
108
+ noHalfPage: false,
109
+ pageControlRelated: false,
110
+ },
111
+ };
112
+ logger.log(`[SEND_MESSAGE_TOOL] 📋 Command details:`, JSON.stringify(command, null, 2));
113
+ // Send command and wait for response (60 second timeout)
114
+ logger.log(`[SEND_MESSAGE_TOOL] ⏳ Setting up promise to wait for send message response...`);
115
+ logger.log(`[SEND_MESSAGE_TOOL] - Timeout: 60 seconds`);
116
+ return new Promise((resolve, reject) => {
117
+ const timeout = setTimeout(() => {
118
+ logger.error(`[SEND_MESSAGE_TOOL] ⏰ Timeout: No response received within 60 seconds`);
119
+ wsManager.off("data-event", handler);
120
+ reject(new Error("发送短信超时(60秒)"));
121
+ }, 60000);
122
+ // Listen for data events from WebSocket
123
+ const handler = (event) => {
124
+ logger.log(`[SEND_MESSAGE_TOOL] 📨 Received data event:`, JSON.stringify(event));
125
+ if (event.intentName === "SendShortMessage") {
126
+ logger.log(`[SEND_MESSAGE_TOOL] 🎯 SendShortMessage event received`);
127
+ logger.log(`[SEND_MESSAGE_TOOL] - status: ${event.status}`);
128
+ clearTimeout(timeout);
129
+ wsManager.off("data-event", handler);
130
+ if (event.status === "success" && event.outputs) {
131
+ logger.log(`[SEND_MESSAGE_TOOL] ✅ Send message response received`);
132
+ logger.log(`[SEND_MESSAGE_TOOL] - outputs:`, JSON.stringify(event.outputs));
133
+ // Check for error code in outputs
134
+ const code = event.outputs.code !== undefined ? event.outputs.code : null;
135
+ if (code !== null && code !== 0) {
136
+ logger.error(`[SEND_MESSAGE_TOOL] ❌ Device returned error`);
137
+ logger.error(`[SEND_MESSAGE_TOOL] - code: ${code}`);
138
+ const errorMsg = event.outputs.errorMsg || event.outputs.errMsg || "未知错误";
139
+ logger.error(`[SEND_MESSAGE_TOOL] - errorMsg: ${errorMsg}`);
140
+ reject(new Error(`发送短信失败: ${errorMsg} (错误代码: ${code})`));
141
+ return;
142
+ }
143
+ // Extract result with safe checks
144
+ const result = event.outputs.result || {};
145
+ logger.log(`[SEND_MESSAGE_TOOL] 🎉 Message sent successfully`);
146
+ logger.log(`[SEND_MESSAGE_TOOL] - phoneNumber: ${phoneNumber}`);
147
+ logger.log(`[SEND_MESSAGE_TOOL] - result:`, JSON.stringify(result));
148
+ resolve({
149
+ content: [
150
+ {
151
+ type: "text",
152
+ text: JSON.stringify(result),
153
+ },
154
+ ],
155
+ });
156
+ }
157
+ else {
158
+ logger.error(`[SEND_MESSAGE_TOOL] ❌ Send message failed`);
159
+ logger.error(`[SEND_MESSAGE_TOOL] - status: ${event.status}`);
160
+ logger.error(`[SEND_MESSAGE_TOOL] - outputs:`, JSON.stringify(event.outputs || {}));
161
+ const errorDetail = event.outputs ? JSON.stringify(event.outputs) : event.status;
162
+ reject(new Error(`发送短信失败: ${errorDetail}`));
163
+ }
164
+ }
165
+ };
166
+ // Register event handler
167
+ logger.log(`[SEND_MESSAGE_TOOL] 📡 Registering data-event handler on WebSocket manager`);
168
+ wsManager.on("data-event", handler);
169
+ // Send the command
170
+ logger.log(`[SEND_MESSAGE_TOOL] 📤 Sending SendShortMessage command...`);
171
+ sendCommand({
172
+ config,
173
+ sessionId,
174
+ taskId,
175
+ messageId,
176
+ command,
177
+ })
178
+ .then(() => {
179
+ logger.log(`[SEND_MESSAGE_TOOL] ✅ Command sent successfully, waiting for response...`);
180
+ })
181
+ .catch((error) => {
182
+ logger.error(`[SEND_MESSAGE_TOOL] ❌ Failed to send command:`, error);
183
+ clearTimeout(timeout);
184
+ wsManager.off("data-event", handler);
185
+ reject(error);
186
+ });
187
+ });
188
+ },
189
+ };
@@ -23,7 +23,19 @@ export declare function unregisterSession(sessionKey: string): void;
23
23
  export declare function getSessionContext(sessionKey: string): SessionContext | null;
24
24
  /**
25
25
  * Get the most recent session context.
26
+ * @deprecated Use getCurrentSessionContext() instead for thread-safe access.
26
27
  * This is a fallback for tools that don't have access to sessionKey.
27
28
  * Returns null if no sessions are active.
28
29
  */
29
30
  export declare function getLatestSessionContext(): SessionContext | null;
31
+ /**
32
+ * Run a callback with a session context stored in AsyncLocalStorage.
33
+ * This ensures thread-safe context isolation for concurrent requests.
34
+ */
35
+ export declare function runWithSessionContext<T>(context: SessionContext, callback: () => Promise<T>): Promise<T>;
36
+ /**
37
+ * Get the current session context from AsyncLocalStorage.
38
+ * This is the recommended way to access session context in tools.
39
+ * Returns null if not running within a session context.
40
+ */
41
+ export declare function getCurrentSessionContext(): SessionContext | null;
@@ -1,7 +1,12 @@
1
+ // Session manager for XY tool context
2
+ // Stores active session contexts that tools can access
3
+ import { AsyncLocalStorage } from "async_hooks";
1
4
  import { logger } from "../utils/logger.js";
2
5
  import { configManager } from "../utils/config-manager.js";
3
6
  // Map of sessionKey -> SessionContextWithRef
4
7
  const activeSessions = new Map();
8
+ // AsyncLocalStorage for thread-safe session context isolation
9
+ const asyncLocalStorage = new AsyncLocalStorage();
5
10
  /**
6
11
  * Register a session context for tool access.
7
12
  * Should be called when starting to process a message.
@@ -74,6 +79,7 @@ export function getSessionContext(sessionKey) {
74
79
  }
75
80
  /**
76
81
  * Get the most recent session context.
82
+ * @deprecated Use getCurrentSessionContext() instead for thread-safe access.
77
83
  * This is a fallback for tools that don't have access to sessionKey.
78
84
  * Returns null if no sessions are active.
79
85
  */
@@ -96,3 +102,30 @@ export function getLatestSessionContext() {
96
102
  const { refCount, ...latestSession } = latestSessionWithRef;
97
103
  return latestSession;
98
104
  }
105
+ /**
106
+ * Run a callback with a session context stored in AsyncLocalStorage.
107
+ * This ensures thread-safe context isolation for concurrent requests.
108
+ */
109
+ export function runWithSessionContext(context, callback) {
110
+ logger.log(`[SESSION_MANAGER] 🔐 Running with AsyncLocalStorage context`);
111
+ logger.log(`[SESSION_MANAGER] - sessionId: ${context.sessionId}`);
112
+ logger.log(`[SESSION_MANAGER] - taskId: ${context.taskId}`);
113
+ return asyncLocalStorage.run(context, callback);
114
+ }
115
+ /**
116
+ * Get the current session context from AsyncLocalStorage.
117
+ * This is the recommended way to access session context in tools.
118
+ * Returns null if not running within a session context.
119
+ */
120
+ export function getCurrentSessionContext() {
121
+ const context = asyncLocalStorage.getStore() ?? null;
122
+ if (context) {
123
+ logger.log(`[SESSION_MANAGER] ✅ Got current session context from AsyncLocalStorage`);
124
+ logger.log(`[SESSION_MANAGER] - sessionId: ${context.sessionId}`);
125
+ logger.log(`[SESSION_MANAGER] - taskId: ${context.taskId}`);
126
+ }
127
+ else {
128
+ logger.warn(`[SESSION_MANAGER] ⚠️ No session context in AsyncLocalStorage`);
129
+ }
130
+ return context;
131
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * XY upload file tool - uploads local files to get publicly accessible URLs.
3
+ * Requires file URIs from search_file tool as prerequisite.
4
+ *
5
+ * Prerequisites:
6
+ * 1. Call search_file tool first to get URIs of files
7
+ * 2. Use the URIs to get public URLs
8
+ *
9
+ * Usage Note:
10
+ * - After getting public URLs, if further processing is needed, download the file first
11
+ * - URLs returned are publicly accessible
12
+ */
13
+ export declare const uploadFileTool: any;
@@ -0,0 +1,265 @@
1
+ import { getXYWebSocketManager } from "../client.js";
2
+ import { sendCommand } from "../formatter.js";
3
+ import { getCurrentSessionContext } from "./session-manager.js";
4
+ import { logger } from "../utils/logger.js";
5
+ /**
6
+ * XY upload file tool - uploads local files to get publicly accessible URLs.
7
+ * Requires file URIs from search_file tool as prerequisite.
8
+ *
9
+ * Prerequisites:
10
+ * 1. Call search_file tool first to get URIs of files
11
+ * 2. Use the URIs to get public URLs
12
+ *
13
+ * Usage Note:
14
+ * - After getting public URLs, if further processing is needed, download the file first
15
+ * - URLs returned are publicly accessible
16
+ */
17
+ export const uploadFileTool = {
18
+ name: "upload_file",
19
+ label: "Upload File",
20
+ description: `工具能力描述:将手机本地文件上传并获取可公网访问的 URL。
21
+
22
+ 前置工具调用:此工具使用前必须先调用 search_file 工具获取文件的 uri
23
+
24
+ 工具参数说明:
25
+ a. 入参中的fileInfos数组,每个元素必须包含mediaUri字段(对应于search_file工具返回结果中的uri),必须与search_file结果中对应的uri完全保持一致,不要自行修改。
26
+ b. fileInfos中的timeout字段是可选的,表示上传文件超时时间,单位是毫秒,默认是20000(20秒)。
27
+ c. fileInfos 是文件在手机本地的信息数组(从 search_file 工具响应中获取)。限制:每次最多支持传入 5 条文件信息。
28
+
29
+ 注意事项:
30
+ a. 操作超时时间为60秒,请勿重复调用此工具,如果超时或失败,最多重试一次。
31
+ b. 此工具返回的文件链接为用户公网可访问的链接,如果需要对文件进行额外的操作,需要先根据返回的url下载文件,然后进行下一步处理。`,
32
+ parameters: {
33
+ type: "object",
34
+ properties: {
35
+ fileInfos: {
36
+ // 不指定 type,允许传入数组或 JSON 字符串
37
+ // 具体的类型验证和转换在 execute 函数内部进行
38
+ description: "文件信息数组,每个元素包含mediaUri(必需)和timeout(可选,默认20000)。必须先通过 search_file 工具获取。每次最多支持 5 条文件信息。",
39
+ },
40
+ },
41
+ required: ["fileInfos"],
42
+ },
43
+ async execute(toolCallId, params) {
44
+ logger.log(`[UPLOAD_FILE_TOOL] 🚀 Starting execution`);
45
+ logger.log(`[UPLOAD_FILE_TOOL] - toolCallId: ${toolCallId}`);
46
+ logger.log(`[UPLOAD_FILE_TOOL] - params (raw):`, JSON.stringify(params));
47
+ logger.log(`[UPLOAD_FILE_TOOL] - params.fileInfos type:`, typeof params.fileInfos);
48
+ logger.log(`[UPLOAD_FILE_TOOL] - timestamp: ${new Date().toISOString()}`);
49
+ // ===== 参数规范化:兼容数组和 JSON 字符串 =====
50
+ let fileInfos = null;
51
+ if (!params.fileInfos) {
52
+ logger.error(`[UPLOAD_FILE_TOOL] ❌ Missing parameter: fileInfos`);
53
+ throw new Error("Missing required parameter: fileInfos");
54
+ }
55
+ // 情况1: 已经是数组
56
+ if (Array.isArray(params.fileInfos)) {
57
+ logger.log(`[UPLOAD_FILE_TOOL] ✅ fileInfos is already an array`);
58
+ fileInfos = params.fileInfos;
59
+ }
60
+ // 情况2: 是字符串,尝试解析为 JSON 数组
61
+ else if (typeof params.fileInfos === 'string') {
62
+ logger.log(`[UPLOAD_FILE_TOOL] 🔄 fileInfos is a string, attempting to parse as JSON...`);
63
+ try {
64
+ const parsed = JSON.parse(params.fileInfos);
65
+ if (Array.isArray(parsed)) {
66
+ logger.log(`[UPLOAD_FILE_TOOL] ✅ Successfully parsed JSON string to array`);
67
+ fileInfos = parsed;
68
+ }
69
+ else {
70
+ logger.error(`[UPLOAD_FILE_TOOL] ❌ Parsed JSON is not an array:`, typeof parsed);
71
+ throw new Error("fileInfos must be an array or a JSON string representing an array");
72
+ }
73
+ }
74
+ catch (parseError) {
75
+ logger.error(`[UPLOAD_FILE_TOOL] ❌ Failed to parse fileInfos as JSON:`, parseError);
76
+ throw new Error(`fileInfos must be a valid JSON array string. Parse error: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
77
+ }
78
+ }
79
+ // 情况3: 其他类型,报错
80
+ else {
81
+ logger.error(`[UPLOAD_FILE_TOOL] ❌ Invalid fileInfos type:`, typeof params.fileInfos);
82
+ throw new Error(`fileInfos must be an array or a JSON string, got ${typeof params.fileInfos}`);
83
+ }
84
+ // 验证数组非空
85
+ if (!fileInfos || fileInfos.length === 0) {
86
+ logger.error(`[UPLOAD_FILE_TOOL] ❌ fileInfos array is empty`);
87
+ throw new Error("fileInfos array cannot be empty");
88
+ }
89
+ logger.log(`[UPLOAD_FILE_TOOL] ✅ Normalized fileInfos:`, JSON.stringify(fileInfos));
90
+ // Validate maximum 5 file infos
91
+ if (fileInfos.length > 5) {
92
+ logger.error(`[UPLOAD_FILE_TOOL] ❌ Too many fileInfos: ${fileInfos.length}`);
93
+ throw new Error(`最多支持 5 条文件信息,当前提供了 ${fileInfos.length} 条。请分批处理。`);
94
+ }
95
+ // Validate each fileInfo has required mediaUri field
96
+ for (let i = 0; i < fileInfos.length; i++) {
97
+ const fileInfo = fileInfos[i];
98
+ if (!fileInfo || typeof fileInfo !== 'object') {
99
+ logger.error(`[UPLOAD_FILE_TOOL] ❌ fileInfo at index ${i} is not an object`);
100
+ throw new Error(`fileInfos[${i}] must be an object with mediaUri property`);
101
+ }
102
+ if (!fileInfo.mediaUri || typeof fileInfo.mediaUri !== 'string') {
103
+ logger.error(`[UPLOAD_FILE_TOOL] ❌ fileInfo at index ${i} missing or invalid mediaUri`);
104
+ throw new Error(`fileInfos[${i}] must have a valid mediaUri string property`);
105
+ }
106
+ // Set default timeout if not provided
107
+ if (!fileInfo.timeout) {
108
+ fileInfo.timeout = "20000";
109
+ }
110
+ }
111
+ logger.log(`[UPLOAD_FILE_TOOL] - fileInfos count: ${fileInfos.length}`);
112
+ // Get session context
113
+ logger.log(`[UPLOAD_FILE_TOOL] 🔍 Attempting to get session context...`);
114
+ const sessionContext = getCurrentSessionContext();
115
+ if (!sessionContext) {
116
+ logger.error(`[UPLOAD_FILE_TOOL] ❌ FAILED: No active session found!`);
117
+ logger.error(`[UPLOAD_FILE_TOOL] - toolCallId: ${toolCallId}`);
118
+ throw new Error("No active XY session found. Upload file tool can only be used during an active conversation.");
119
+ }
120
+ logger.log(`[UPLOAD_FILE_TOOL] ✅ Session context found`);
121
+ logger.log(`[UPLOAD_FILE_TOOL] - sessionId: ${sessionContext.sessionId}`);
122
+ logger.log(`[UPLOAD_FILE_TOOL] - taskId: ${sessionContext.taskId}`);
123
+ logger.log(`[UPLOAD_FILE_TOOL] - messageId: ${sessionContext.messageId}`);
124
+ const { config, sessionId, taskId, messageId } = sessionContext;
125
+ // Get WebSocket manager
126
+ logger.log(`[UPLOAD_FILE_TOOL] 🔌 Getting WebSocket manager...`);
127
+ const wsManager = getXYWebSocketManager(config);
128
+ logger.log(`[UPLOAD_FILE_TOOL] ✅ WebSocket manager obtained`);
129
+ // Get public URLs for the files
130
+ logger.log(`[UPLOAD_FILE_TOOL] 🌐 Getting public URLs for files...`);
131
+ const fileUrls = await getFileUrls(wsManager, config, sessionId, taskId, messageId, fileInfos);
132
+ logger.log(`[UPLOAD_FILE_TOOL] 🎉 Successfully retrieved ${fileUrls.length} file URLs`);
133
+ return {
134
+ content: [
135
+ {
136
+ type: "text",
137
+ text: JSON.stringify({
138
+ fileUrls,
139
+ count: fileUrls.length,
140
+ message: `成功获取 ${fileUrls.length} 个文件的公网访问 URL`
141
+ }),
142
+ },
143
+ ],
144
+ };
145
+ },
146
+ };
147
+ /**
148
+ * Get public URLs for files using fileInfos
149
+ * Returns array of publicly accessible file URLs
150
+ */
151
+ async function getFileUrls(wsManager, config, sessionId, taskId, messageId, fileInfos) {
152
+ logger.log(`[UPLOAD_FILE_TOOL] 📦 Building FileUploadForClaw command...`);
153
+ const command = {
154
+ header: {
155
+ namespace: "Common",
156
+ name: "Action",
157
+ },
158
+ payload: {
159
+ cardParam: {},
160
+ executeParam: {
161
+ executeMode: "background",
162
+ intentName: "FileUploadForClaw",
163
+ bundleName: "com.huawei.hmos.vassistant",
164
+ needUnlock: true,
165
+ actionResponse: true,
166
+ appType: "OHOS_APP",
167
+ timeOut: 5,
168
+ intentParam: {
169
+ fileInfos: fileInfos,
170
+ },
171
+ permissionId: [],
172
+ achieveType: "INTENT",
173
+ },
174
+ responses: [
175
+ {
176
+ resultCode: "",
177
+ displayText: "",
178
+ ttsText: "",
179
+ },
180
+ ],
181
+ needUploadResult: true,
182
+ noHalfPage: false,
183
+ pageControlRelated: false,
184
+ },
185
+ };
186
+ return new Promise((resolve, reject) => {
187
+ const timeout = setTimeout(() => {
188
+ logger.error(`[UPLOAD_FILE_TOOL] ⏰ Timeout: No response for FileUploadForClaw within 60 seconds`);
189
+ wsManager.off("data-event", handler);
190
+ reject(new Error("获取文件URL超时(60秒)"));
191
+ }, 60000);
192
+ const handler = (event) => {
193
+ logger.log(`[UPLOAD_FILE_TOOL] 📨 Received data event:`, JSON.stringify(event));
194
+ if (event.intentName === "FileUploadForClaw") {
195
+ logger.log(`[UPLOAD_FILE_TOOL] 🎯 FileUploadForClaw event received`);
196
+ logger.log(`[UPLOAD_FILE_TOOL] - status: ${event.status}`);
197
+ clearTimeout(timeout);
198
+ wsManager.off("data-event", handler);
199
+ if (event.status === "success" && event.outputs) {
200
+ logger.log(`[UPLOAD_FILE_TOOL] ✅ File URL retrieval completed successfully`);
201
+ // Check for error code in outputs
202
+ const code = event.outputs.code !== undefined ? event.outputs.code : null;
203
+ if (code !== null && code !== 0) {
204
+ logger.error(`[UPLOAD_FILE_TOOL] ❌ Device returned error`);
205
+ logger.error(`[UPLOAD_FILE_TOOL] - code: ${code}`);
206
+ const errorMsg = event.outputs.errorMsg || event.outputs.errMsg || "未知错误";
207
+ logger.error(`[UPLOAD_FILE_TOOL] - errorMsg: ${errorMsg}`);
208
+ reject(new Error(`获取文件URL失败: ${errorMsg} (错误代码: ${code})`));
209
+ return;
210
+ }
211
+ // Safe navigation through outputs structure
212
+ const outputs = event.outputs;
213
+ const result = outputs?.result;
214
+ let fileUrls = [];
215
+ if (result && typeof result === 'object') {
216
+ const urls = result.fileUrls;
217
+ if (Array.isArray(urls)) {
218
+ fileUrls = urls;
219
+ }
220
+ }
221
+ // Decode Unicode escape sequences in URLs
222
+ // Replace \u003d with = and \u0026 with &
223
+ fileUrls = fileUrls.map((url) => {
224
+ if (typeof url !== 'string') {
225
+ logger.warn(`[UPLOAD_FILE_TOOL] ⚠️ URL is not a string:`, typeof url);
226
+ return '';
227
+ }
228
+ const decodedUrl = url
229
+ .replace(/\\u003d/g, '=')
230
+ .replace(/\\u0026/g, '&');
231
+ logger.log(`[UPLOAD_FILE_TOOL] 🔄 Decoded URL: ${url} -> ${decodedUrl}`);
232
+ return decodedUrl;
233
+ }).filter((url) => url.length > 0);
234
+ logger.log(`[UPLOAD_FILE_TOOL] 📊 Retrieved and decoded ${fileUrls.length} file URLs`);
235
+ resolve(fileUrls);
236
+ }
237
+ else {
238
+ logger.error(`[UPLOAD_FILE_TOOL] ❌ File URL retrieval failed`);
239
+ logger.error(`[UPLOAD_FILE_TOOL] - status: ${event.status}`);
240
+ logger.error(`[UPLOAD_FILE_TOOL] - outputs:`, JSON.stringify(event.outputs || {}));
241
+ reject(new Error(`获取文件URL失败: ${event.status}`));
242
+ }
243
+ }
244
+ };
245
+ logger.log(`[UPLOAD_FILE_TOOL] 📡 Registering data-event handler for FileUploadForClaw`);
246
+ wsManager.on("data-event", handler);
247
+ logger.log(`[UPLOAD_FILE_TOOL] 📤 Sending FileUploadForClaw command...`);
248
+ sendCommand({
249
+ config,
250
+ sessionId,
251
+ taskId,
252
+ messageId,
253
+ command,
254
+ })
255
+ .then(() => {
256
+ logger.log(`[UPLOAD_FILE_TOOL] ✅ FileUploadForClaw command sent successfully`);
257
+ })
258
+ .catch((error) => {
259
+ logger.error(`[UPLOAD_FILE_TOOL] ❌ Failed to send FileUploadForClaw command:`, error);
260
+ clearTimeout(timeout);
261
+ wsManager.off("data-event", handler);
262
+ reject(error);
263
+ });
264
+ });
265
+ }
@@ -1,6 +1,6 @@
1
1
  import { getXYWebSocketManager } from "../client.js";
2
2
  import { sendCommand } from "../formatter.js";
3
- import { getLatestSessionContext } from "./session-manager.js";
3
+ import { getCurrentSessionContext } from "./session-manager.js";
4
4
  import { logger } from "../utils/logger.js";
5
5
  /**
6
6
  * XY upload photo tool - uploads local photos to get publicly accessible URLs.
@@ -90,7 +90,7 @@ export const uploadPhotoTool = {
90
90
  logger.log(`[UPLOAD_PHOTO_TOOL] - mediaUris count: ${mediaUris.length}`);
91
91
  // Get session context
92
92
  logger.log(`[UPLOAD_PHOTO_TOOL] 🔍 Attempting to get session context...`);
93
- const sessionContext = getLatestSessionContext();
93
+ const sessionContext = getCurrentSessionContext();
94
94
  if (!sessionContext) {
95
95
  logger.error(`[UPLOAD_PHOTO_TOOL] ❌ FAILED: No active session found!`);
96
96
  logger.error(`[UPLOAD_PHOTO_TOOL] - toolCallId: ${toolCallId}`);
@@ -1,7 +1,7 @@
1
1
  // XiaoYi GUI tool implementation - simulates phone screen interactions
2
2
  import { getXYWebSocketManager } from "../client.js";
3
3
  import { sendCommand } from "../formatter.js";
4
- import { getLatestSessionContext } from "./session-manager.js";
4
+ import { getCurrentSessionContext } from "./session-manager.js";
5
5
  import { logger } from "../utils/logger.js";
6
6
  /**
7
7
  * XiaoYi GUI tool - executes phone app interactions through GUI agent.
@@ -49,7 +49,7 @@ export const xiaoyiGuiTool = {
49
49
  }
50
50
  // Get session context
51
51
  logger.log(`[XIAOYI_GUI_TOOL] 🔍 Attempting to get session context...`);
52
- const sessionContext = getLatestSessionContext();
52
+ const sessionContext = getCurrentSessionContext();
53
53
  if (!sessionContext) {
54
54
  logger.error(`[XIAOYI_GUI_TOOL] ❌ FAILED: No active session found!`);
55
55
  logger.error(`[XIAOYI_GUI_TOOL] - toolCallId: ${toolCallId}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynhcj/xiaoyi-channel",
3
- "version": "0.0.26-beta",
3
+ "version": "0.0.28-beta",
4
4
  "description": "OpenClaw Xiaoyi Channel plugin - Xiaoyi A2A protocol integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",