@ynhcj/xiaoyi-channel 0.0.26-beta → 0.0.26-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/index.d.ts +0 -2
- package/dist/index.js +48 -2
- package/dist/src/bot.js +81 -28
- package/dist/src/channel.js +33 -7
- package/dist/src/client.js +11 -27
- package/dist/src/config.js +2 -2
- package/dist/src/cspl/call-api.d.ts +3 -0
- package/dist/src/cspl/call-api.js +107 -0
- package/dist/src/cspl/config.d.ts +19 -0
- package/dist/src/cspl/config.js +50 -0
- package/dist/src/cspl/constants.d.ts +43 -0
- package/dist/src/cspl/constants.js +22 -0
- package/dist/src/cspl/utils.d.ts +10 -0
- package/dist/src/cspl/utils.js +57 -0
- package/dist/src/file-upload.d.ts +5 -0
- package/dist/src/file-upload.js +92 -0
- package/dist/src/formatter.d.ts +14 -0
- package/dist/src/formatter.js +49 -25
- package/dist/src/heartbeat.js +0 -4
- package/dist/src/monitor.js +12 -0
- package/dist/src/onboarding.d.ts +3 -4
- package/dist/src/onboarding.js +2 -2
- package/dist/src/outbound.d.ts +2 -1
- package/dist/src/outbound.js +52 -6
- package/dist/src/parser.d.ts +7 -0
- package/dist/src/parser.js +22 -0
- package/dist/src/push.d.ts +8 -1
- package/dist/src/push.js +30 -22
- package/dist/src/reply-dispatcher.js +39 -5
- package/dist/src/steer-injector.d.ts +16 -0
- package/dist/src/steer-injector.js +74 -0
- package/dist/src/thread-bindings.d.ts +54 -0
- package/dist/src/thread-bindings.js +214 -0
- package/dist/src/tools/calendar-tool.js +7 -40
- package/dist/src/tools/call-phone-tool.d.ts +5 -0
- package/dist/src/tools/call-phone-tool.js +142 -0
- package/dist/src/tools/create-alarm-tool.d.ts +7 -0
- package/dist/src/tools/create-alarm-tool.js +349 -0
- package/dist/src/tools/delete-alarm-tool.d.ts +11 -0
- package/dist/src/tools/delete-alarm-tool.js +174 -0
- package/dist/src/tools/image-reading-tool.d.ts +5 -0
- package/dist/src/tools/image-reading-tool.js +328 -0
- package/dist/src/tools/location-tool.js +8 -42
- package/dist/src/tools/modify-alarm-tool.d.ts +9 -0
- package/dist/src/tools/modify-alarm-tool.js +365 -0
- package/dist/src/tools/modify-note-tool.js +5 -43
- package/dist/src/tools/note-tool.js +33 -24
- package/dist/src/tools/search-alarm-tool.d.ts +8 -0
- package/dist/src/tools/search-alarm-tool.js +285 -0
- package/dist/src/tools/search-calendar-tool.js +10 -84
- package/dist/src/tools/search-contact-tool.js +4 -57
- package/dist/src/tools/search-file-tool.d.ts +5 -0
- package/dist/src/tools/search-file-tool.js +128 -0
- package/dist/src/tools/search-message-tool.d.ts +5 -0
- package/dist/src/tools/search-message-tool.js +116 -0
- package/dist/src/tools/search-note-tool.js +6 -24
- package/dist/src/tools/search-photo-gallery-tool.js +40 -61
- package/dist/src/tools/send-file-to-user-tool.d.ts +5 -0
- package/dist/src/tools/send-file-to-user-tool.js +279 -0
- package/dist/src/tools/send-message-tool.d.ts +5 -0
- package/dist/src/tools/send-message-tool.js +138 -0
- package/dist/src/tools/session-manager.d.ts +15 -0
- package/dist/src/tools/session-manager.js +43 -31
- package/dist/src/tools/upload-file-tool.d.ts +13 -0
- package/dist/src/tools/upload-file-tool.js +216 -0
- package/dist/src/tools/upload-photo-tool.js +2 -44
- package/dist/src/tools/view-push-result-tool.d.ts +5 -0
- package/dist/src/tools/view-push-result-tool.js +107 -0
- package/dist/src/tools/xiaoyi-collection-tool.d.ts +5 -0
- package/dist/src/tools/xiaoyi-collection-tool.js +112 -0
- package/dist/src/tools/xiaoyi-gui-tool.js +2 -36
- package/dist/src/trigger-handler.d.ts +22 -0
- package/dist/src/trigger-handler.js +59 -0
- package/dist/src/types.d.ts +1 -8
- package/dist/src/types.js +4 -0
- package/dist/src/utils/pushdata-manager.d.ts +28 -0
- package/dist/src/utils/pushdata-manager.js +171 -0
- package/dist/src/utils/pushid-manager.d.ts +12 -0
- package/dist/src/utils/pushid-manager.js +105 -0
- package/dist/src/websocket.d.ts +25 -31
- package/dist/src/websocket.js +233 -271
- package/package.json +2 -2
- package/dist/src/tools/search-photo-tool.d.ts +0 -9
- package/dist/src/tools/search-photo-tool.js +0 -270
- package/dist/src/utils/session.d.ts +0 -34
- package/dist/src/utils/session.js +0 -50
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { getXYWebSocketManager } from "../client.js";
|
|
2
|
+
import { XYFileUploadService } from "../file-upload.js";
|
|
3
|
+
import { getCurrentSessionContext } from "./session-manager.js";
|
|
4
|
+
import fetch from "node-fetch";
|
|
5
|
+
import fs from "fs/promises";
|
|
6
|
+
import path from "path";
|
|
7
|
+
/**
|
|
8
|
+
* File extension to MIME type mapping
|
|
9
|
+
*/
|
|
10
|
+
const FILE_TYPE_TO_MIME_TYPE = {
|
|
11
|
+
txt: "text/plain",
|
|
12
|
+
html: "text/html",
|
|
13
|
+
css: "text/css",
|
|
14
|
+
js: "application/javascript",
|
|
15
|
+
json: "application/json",
|
|
16
|
+
png: "image/png",
|
|
17
|
+
jpeg: "image/jpeg",
|
|
18
|
+
jpg: "image/jpeg",
|
|
19
|
+
gif: "image/gif",
|
|
20
|
+
svg: "image/svg+xml",
|
|
21
|
+
pdf: "application/pdf",
|
|
22
|
+
zip: "application/zip",
|
|
23
|
+
doc: "application/msword",
|
|
24
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
25
|
+
xls: "application/vnd.ms-excel",
|
|
26
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
27
|
+
ppt: "application/vnd.ms-powerpoint",
|
|
28
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
29
|
+
mp3: "audio/mpeg",
|
|
30
|
+
mp4: "video/mp4",
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Get MIME type from file extension
|
|
34
|
+
*/
|
|
35
|
+
function getMimeTypeFromFilename(filename) {
|
|
36
|
+
const extension = filename.split(".").pop()?.toLowerCase();
|
|
37
|
+
if (extension && FILE_TYPE_TO_MIME_TYPE[extension]) {
|
|
38
|
+
return FILE_TYPE_TO_MIME_TYPE[extension];
|
|
39
|
+
}
|
|
40
|
+
return "text/plain";
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Normalize parameter to array (supports both array and JSON string)
|
|
44
|
+
*/
|
|
45
|
+
function normalizeToArray(param) {
|
|
46
|
+
if (Array.isArray(param)) {
|
|
47
|
+
return param;
|
|
48
|
+
}
|
|
49
|
+
if (typeof param === 'string') {
|
|
50
|
+
try {
|
|
51
|
+
const parsed = JSON.parse(param);
|
|
52
|
+
if (Array.isArray(parsed)) {
|
|
53
|
+
return parsed;
|
|
54
|
+
}
|
|
55
|
+
throw new Error("Parameter must be an array or a JSON string representing an array");
|
|
56
|
+
}
|
|
57
|
+
catch (parseError) {
|
|
58
|
+
throw new Error(`Parameter must be a valid JSON array string. Parse error: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
throw new Error(`Parameter must be an array or a JSON string, got ${typeof param}`);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Download remote file to local temp directory
|
|
65
|
+
*/
|
|
66
|
+
async function downloadRemoteFile(url) {
|
|
67
|
+
try {
|
|
68
|
+
const response = await fetch(url);
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
71
|
+
}
|
|
72
|
+
// Get filename from URL or use default
|
|
73
|
+
let filename = url.split("/").pop() || "downloaded_file";
|
|
74
|
+
// Remove query parameters if present
|
|
75
|
+
filename = filename.split("?")[0];
|
|
76
|
+
// Ensure temp directory exists
|
|
77
|
+
const tempDir = "/tmp/xy_channel";
|
|
78
|
+
await fs.mkdir(tempDir, { recursive: true });
|
|
79
|
+
// Generate unique filename to avoid conflicts
|
|
80
|
+
const timestamp = Date.now();
|
|
81
|
+
const ext = path.extname(filename);
|
|
82
|
+
const baseName = path.basename(filename, ext);
|
|
83
|
+
const uniqueFilename = `${baseName}_${timestamp}${ext}`;
|
|
84
|
+
const localPath = path.join(tempDir, uniqueFilename);
|
|
85
|
+
// Save file to local temp directory
|
|
86
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
87
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
88
|
+
await fs.writeFile(localPath, buffer);
|
|
89
|
+
return localPath;
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
throw new Error(`Failed to download remote file: ${error instanceof Error ? error.message : String(error)}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* XY send file to user tool - sends local files or remote files to user's device.
|
|
97
|
+
* Supports both local file paths and remote URLs.
|
|
98
|
+
*/
|
|
99
|
+
export const sendFileToUserTool = {
|
|
100
|
+
name: "send_file_to_user",
|
|
101
|
+
label: "Send File to User",
|
|
102
|
+
description: `工具能力描述:帮助用户把本地的文件或者公网地址的文件传到手机。
|
|
103
|
+
|
|
104
|
+
工具参数说明:
|
|
105
|
+
a. fileLocalUrls:本地文件路径数组,包含用户需要回传的文件在本地的地址
|
|
106
|
+
b. fileRemoteUrls:公网地址数组,包含用户需要回传的文件的公网地址(会先下载到本地再发送)
|
|
107
|
+
c. fileLocalUrls 与 fileRemoteUrls 任意一个不为空即可,两者都提供时都会处理
|
|
108
|
+
|
|
109
|
+
注意事项:
|
|
110
|
+
a. 支持传入数组或 JSON 字符串格式
|
|
111
|
+
b. 操作超时时间为2分钟(120秒),请勿重复调用此工具,如果超时或失败,最多重试一次`,
|
|
112
|
+
parameters: {
|
|
113
|
+
type: "object",
|
|
114
|
+
properties: {
|
|
115
|
+
fileLocalUrls: {
|
|
116
|
+
description: "本地文件路径数组,包含用户需要回传的文件在本地的地址",
|
|
117
|
+
},
|
|
118
|
+
fileRemoteUrls: {
|
|
119
|
+
description: "公网地址数组,包含用户需要回传的文件的公网地址(会先下载到本地再发送)",
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
async execute(toolCallId, params) {
|
|
124
|
+
// Set timeout for the entire operation (2 minutes)
|
|
125
|
+
const TOOL_TIMEOUT = 120000; // 2 minutes in milliseconds
|
|
126
|
+
let timeoutHandle = null;
|
|
127
|
+
// Create a timeout promise
|
|
128
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
129
|
+
timeoutHandle = setTimeout(() => {
|
|
130
|
+
reject(new Error("操作超时(2分钟)"));
|
|
131
|
+
}, TOOL_TIMEOUT);
|
|
132
|
+
});
|
|
133
|
+
// Create the main execution promise
|
|
134
|
+
const executionPromise = (async () => {
|
|
135
|
+
// Validate that at least one parameter is provided
|
|
136
|
+
if (!params.fileLocalUrls && !params.fileRemoteUrls) {
|
|
137
|
+
throw new Error("At least one of fileLocalUrls or fileRemoteUrls must be provided");
|
|
138
|
+
}
|
|
139
|
+
// Normalize fileLocalUrls parameter
|
|
140
|
+
let fileLocalUrls = [];
|
|
141
|
+
if (params.fileLocalUrls) {
|
|
142
|
+
fileLocalUrls = normalizeToArray(params.fileLocalUrls);
|
|
143
|
+
if (fileLocalUrls.length === 0) {
|
|
144
|
+
throw new Error("fileLocalUrls array cannot be empty");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Normalize fileRemoteUrls parameter
|
|
148
|
+
let fileRemoteUrls = [];
|
|
149
|
+
if (params.fileRemoteUrls) {
|
|
150
|
+
fileRemoteUrls = normalizeToArray(params.fileRemoteUrls);
|
|
151
|
+
if (fileRemoteUrls.length === 0) {
|
|
152
|
+
throw new Error("fileRemoteUrls array cannot be empty");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Get session context
|
|
156
|
+
const sessionContext = getCurrentSessionContext();
|
|
157
|
+
if (!sessionContext) {
|
|
158
|
+
throw new Error("No active XY session found. Send file to user tool can only be used during an active conversation.");
|
|
159
|
+
}
|
|
160
|
+
const { config, sessionId, taskId, messageId } = sessionContext;
|
|
161
|
+
// Get WebSocket manager
|
|
162
|
+
const wsManager = getXYWebSocketManager(config);
|
|
163
|
+
// Create upload service
|
|
164
|
+
const uploadService = new XYFileUploadService(config.fileUploadUrl, config.apiKey, config.uid);
|
|
165
|
+
// Collect all local file paths to upload
|
|
166
|
+
const allLocalPaths = [...fileLocalUrls];
|
|
167
|
+
const downloadedFiles = [];
|
|
168
|
+
// Download remote files to local temp directory
|
|
169
|
+
if (fileRemoteUrls.length > 0) {
|
|
170
|
+
for (let i = 0; i < fileRemoteUrls.length; i++) {
|
|
171
|
+
const remoteUrl = fileRemoteUrls[i];
|
|
172
|
+
try {
|
|
173
|
+
const localPath = await downloadRemoteFile(remoteUrl);
|
|
174
|
+
allLocalPaths.push(localPath);
|
|
175
|
+
downloadedFiles.push(localPath);
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
throw new Error(`Failed to download remote file ${remoteUrl}: ${error instanceof Error ? error.message : String(error)}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// Upload all local files and get fileIds
|
|
183
|
+
const uploadedFiles = [];
|
|
184
|
+
for (let i = 0; i < allLocalPaths.length; i++) {
|
|
185
|
+
const localPath = allLocalPaths[i];
|
|
186
|
+
try {
|
|
187
|
+
// Upload file using three-phase upload
|
|
188
|
+
const fileId = await uploadService.uploadFile(localPath);
|
|
189
|
+
if (!fileId) {
|
|
190
|
+
throw new Error(`Failed to upload file: ${localPath}`);
|
|
191
|
+
}
|
|
192
|
+
// Get filename and mime type
|
|
193
|
+
const fileName = localPath.split("/").pop() || "unknown";
|
|
194
|
+
const mimeType = getMimeTypeFromFilename(fileName);
|
|
195
|
+
uploadedFiles.push({ fileName, fileId, mimeType });
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
throw new Error(`Failed to upload file ${localPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// Clean up downloaded files
|
|
202
|
+
if (downloadedFiles.length > 0) {
|
|
203
|
+
for (const downloadedFile of downloadedFiles) {
|
|
204
|
+
try {
|
|
205
|
+
await fs.unlink(downloadedFile);
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// Build and send agent_response messages for each file
|
|
212
|
+
const sentFiles = [];
|
|
213
|
+
for (const uploadedFile of uploadedFiles) {
|
|
214
|
+
const { fileName, fileId, mimeType } = uploadedFile;
|
|
215
|
+
const agentResponse = {
|
|
216
|
+
msgType: "agent_response",
|
|
217
|
+
agentId: config.agentId,
|
|
218
|
+
sessionId: sessionId,
|
|
219
|
+
taskId: taskId,
|
|
220
|
+
msgDetail: JSON.stringify({
|
|
221
|
+
jsonrpc: "2.0",
|
|
222
|
+
id: taskId,
|
|
223
|
+
result: {
|
|
224
|
+
kind: "artifact-update",
|
|
225
|
+
append: true,
|
|
226
|
+
lastChunk: false,
|
|
227
|
+
final: false,
|
|
228
|
+
artifact: {
|
|
229
|
+
artifactId: taskId,
|
|
230
|
+
parts: [
|
|
231
|
+
{
|
|
232
|
+
kind: "file",
|
|
233
|
+
file: {
|
|
234
|
+
name: fileName,
|
|
235
|
+
mimeType: mimeType,
|
|
236
|
+
fileId: fileId,
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
error: { code: 0 },
|
|
243
|
+
}),
|
|
244
|
+
};
|
|
245
|
+
// Send WebSocket message
|
|
246
|
+
await wsManager.sendMessage(sessionId, agentResponse);
|
|
247
|
+
sentFiles.push({ fileName, fileId });
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
content: [
|
|
251
|
+
{
|
|
252
|
+
type: "text",
|
|
253
|
+
text: JSON.stringify({
|
|
254
|
+
sentFiles,
|
|
255
|
+
count: sentFiles.length,
|
|
256
|
+
message: `成功发送 ${sentFiles.length} 个文件到用户设备`
|
|
257
|
+
}),
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
};
|
|
261
|
+
})();
|
|
262
|
+
// Race between execution and timeout
|
|
263
|
+
try {
|
|
264
|
+
const result = await Promise.race([executionPromise, timeoutPromise]);
|
|
265
|
+
// Clear timeout if execution completed
|
|
266
|
+
if (timeoutHandle) {
|
|
267
|
+
clearTimeout(timeoutHandle);
|
|
268
|
+
}
|
|
269
|
+
return result;
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
// Clear timeout on error
|
|
273
|
+
if (timeoutHandle) {
|
|
274
|
+
clearTimeout(timeoutHandle);
|
|
275
|
+
}
|
|
276
|
+
throw error;
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { getXYWebSocketManager } from "../client.js";
|
|
2
|
+
import { sendCommand } from "../formatter.js";
|
|
3
|
+
import { getCurrentSessionContext } from "./session-manager.js";
|
|
4
|
+
/**
|
|
5
|
+
* XY send message tool - sends SMS message on user's device.
|
|
6
|
+
* Requires phoneNumber (with +86 prefix) and content parameters.
|
|
7
|
+
*/
|
|
8
|
+
export const sendMessageTool = {
|
|
9
|
+
name: "send_message",
|
|
10
|
+
label: "Send Message",
|
|
11
|
+
description: "通过手机发送短信。需要提供接收方手机号码和短信内容。手机号码会自动添加+86前缀(如果没有的话)。注意:操作超时时间为60秒,请勿重复调用此工具,如果超时或失败,最多重试一次。回复约束:如果工具返回没有授权或者其他报错,只需要完整描述没有授权或者其他报错内容即可,不需要主动给用户提供解决方案,例如告诉用户如何授权,如何解决报错等都是不需要的,请严格遵守。",
|
|
12
|
+
parameters: {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
phoneNumber: {
|
|
16
|
+
type: "string",
|
|
17
|
+
description: "接收方手机号码(会自动添加+86前缀)",
|
|
18
|
+
},
|
|
19
|
+
content: {
|
|
20
|
+
type: "string",
|
|
21
|
+
description: "短信内容",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
required: ["phoneNumber", "content"],
|
|
25
|
+
},
|
|
26
|
+
async execute(toolCallId, params) {
|
|
27
|
+
// Validate phoneNumber parameter
|
|
28
|
+
if (!params.phoneNumber || typeof params.phoneNumber !== "string" || params.phoneNumber.trim() === "") {
|
|
29
|
+
throw new Error("Missing required parameter: phoneNumber must be a non-empty string");
|
|
30
|
+
}
|
|
31
|
+
// Validate content parameter
|
|
32
|
+
if (!params.content || typeof params.content !== "string" || params.content.trim() === "") {
|
|
33
|
+
throw new Error("Missing required parameter: content must be a non-empty string");
|
|
34
|
+
}
|
|
35
|
+
// Normalize phone number: add +86 prefix if not present
|
|
36
|
+
let phoneNumber = params.phoneNumber.trim();
|
|
37
|
+
if (!phoneNumber.startsWith("+86")) {
|
|
38
|
+
// Remove leading 0 if present (e.g., 086 -> 86)
|
|
39
|
+
if (phoneNumber.startsWith("0")) {
|
|
40
|
+
phoneNumber = phoneNumber.substring(1);
|
|
41
|
+
}
|
|
42
|
+
// Remove +86 or 86 prefix if already present to avoid duplication
|
|
43
|
+
if (phoneNumber.startsWith("86")) {
|
|
44
|
+
phoneNumber = phoneNumber.substring(2);
|
|
45
|
+
}
|
|
46
|
+
phoneNumber = `+86${phoneNumber}`;
|
|
47
|
+
}
|
|
48
|
+
// Get session context
|
|
49
|
+
const sessionContext = getCurrentSessionContext();
|
|
50
|
+
if (!sessionContext) {
|
|
51
|
+
throw new Error("No active XY session found. Send message tool can only be used during an active conversation.");
|
|
52
|
+
}
|
|
53
|
+
const { config, sessionId, taskId, messageId } = sessionContext;
|
|
54
|
+
// Get WebSocket manager
|
|
55
|
+
const wsManager = getXYWebSocketManager(config);
|
|
56
|
+
// Build SendShortMessage command
|
|
57
|
+
const command = {
|
|
58
|
+
header: {
|
|
59
|
+
namespace: "Common",
|
|
60
|
+
name: "Action",
|
|
61
|
+
},
|
|
62
|
+
payload: {
|
|
63
|
+
cardParam: {},
|
|
64
|
+
executeParam: {
|
|
65
|
+
executeMode: "background",
|
|
66
|
+
intentName: "SendShortMessage",
|
|
67
|
+
bundleName: "com.huawei.hmos.aidispatchservice",
|
|
68
|
+
needUnlock: true,
|
|
69
|
+
actionResponse: true,
|
|
70
|
+
appType: "OHOS_APP",
|
|
71
|
+
timeOut: 5,
|
|
72
|
+
intentParam: {
|
|
73
|
+
phoneNumber: phoneNumber,
|
|
74
|
+
content: params.content.trim(),
|
|
75
|
+
},
|
|
76
|
+
permissionId: [],
|
|
77
|
+
achieveType: "INTENT",
|
|
78
|
+
},
|
|
79
|
+
responses: [
|
|
80
|
+
{
|
|
81
|
+
resultCode: "",
|
|
82
|
+
displayText: "",
|
|
83
|
+
ttsText: "",
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
needUploadResult: true,
|
|
87
|
+
noHalfPage: false,
|
|
88
|
+
pageControlRelated: false,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
// Send command and wait for response (60 second timeout)
|
|
92
|
+
return new Promise((resolve, reject) => {
|
|
93
|
+
const timeout = setTimeout(() => {
|
|
94
|
+
wsManager.off("data-event", handler);
|
|
95
|
+
reject(new Error("发送短信超时(60秒)"));
|
|
96
|
+
}, 60000);
|
|
97
|
+
// Listen for data events from WebSocket
|
|
98
|
+
const handler = (event) => {
|
|
99
|
+
if (event.intentName === "SendShortMessage") {
|
|
100
|
+
clearTimeout(timeout);
|
|
101
|
+
wsManager.off("data-event", handler);
|
|
102
|
+
if (event.status === "success" && event.outputs) {
|
|
103
|
+
// 成功,直接返回完整的 event.outputs JSON 字符串
|
|
104
|
+
resolve({
|
|
105
|
+
content: [
|
|
106
|
+
{
|
|
107
|
+
type: "text",
|
|
108
|
+
text: JSON.stringify(event.outputs),
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
const errorDetail = event.outputs ? JSON.stringify(event.outputs) : event.status;
|
|
115
|
+
reject(new Error(`发送短信失败: ${errorDetail}`));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
// Register event handler
|
|
120
|
+
wsManager.on("data-event", handler);
|
|
121
|
+
// Send the command
|
|
122
|
+
sendCommand({
|
|
123
|
+
config,
|
|
124
|
+
sessionId,
|
|
125
|
+
taskId,
|
|
126
|
+
messageId,
|
|
127
|
+
command,
|
|
128
|
+
})
|
|
129
|
+
.then(() => {
|
|
130
|
+
})
|
|
131
|
+
.catch((error) => {
|
|
132
|
+
clearTimeout(timeout);
|
|
133
|
+
wsManager.off("data-event", handler);
|
|
134
|
+
reject(error);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
},
|
|
138
|
+
};
|
|
@@ -23,7 +23,22 @@ 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
|
+
* Enhanced version: Automatically fetches the latest taskId from task-manager
|
|
42
|
+
* to support interruption scenarios where a new message updates the taskId.
|
|
43
|
+
*/
|
|
44
|
+
export declare function getCurrentSessionContext(): SessionContext | null;
|
|
@@ -1,25 +1,23 @@
|
|
|
1
|
-
|
|
1
|
+
// Session manager for XY tool context
|
|
2
|
+
// Stores active session contexts that tools can access
|
|
3
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
2
4
|
import { configManager } from "../utils/config-manager.js";
|
|
5
|
+
import { getCurrentTaskId, getCurrentMessageId } from "../task-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.
|
|
8
13
|
*/
|
|
9
14
|
export function registerSession(sessionKey, context) {
|
|
10
|
-
logger.log(`[SESSION_MANAGER] 📝 Registering session: ${sessionKey}`);
|
|
11
|
-
logger.log(`[SESSION_MANAGER] - sessionId: ${context.sessionId}`);
|
|
12
|
-
logger.log(`[SESSION_MANAGER] - taskId: ${context.taskId}`);
|
|
13
|
-
logger.log(`[SESSION_MANAGER] - messageId: ${context.messageId}`);
|
|
14
|
-
logger.log(`[SESSION_MANAGER] - agentId: ${context.agentId}`);
|
|
15
|
-
logger.log(`[SESSION_MANAGER] - Active sessions before: ${activeSessions.size}`);
|
|
16
15
|
const existing = activeSessions.get(sessionKey);
|
|
17
16
|
if (existing) {
|
|
18
17
|
// 更新上下文,增加引用计数
|
|
19
18
|
existing.taskId = context.taskId;
|
|
20
19
|
existing.messageId = context.messageId;
|
|
21
20
|
existing.refCount++;
|
|
22
|
-
logger.log(`[SESSION_MANAGER] - Updated existing, refCount=${existing.refCount}`);
|
|
23
21
|
}
|
|
24
22
|
else {
|
|
25
23
|
// 新建
|
|
@@ -27,45 +25,30 @@ export function registerSession(sessionKey, context) {
|
|
|
27
25
|
...context,
|
|
28
26
|
refCount: 1,
|
|
29
27
|
});
|
|
30
|
-
logger.log(`[SESSION_MANAGER] - Created new, refCount=1`);
|
|
31
28
|
}
|
|
32
|
-
logger.log(`[SESSION_MANAGER] - Active sessions after: ${activeSessions.size}`);
|
|
33
|
-
logger.log(`[SESSION_MANAGER] - All session keys: [${Array.from(activeSessions.keys()).join(", ")}]`);
|
|
34
29
|
}
|
|
35
30
|
/**
|
|
36
31
|
* Unregister a session context.
|
|
37
32
|
* Should be called when message processing is complete.
|
|
38
33
|
*/
|
|
39
34
|
export function unregisterSession(sessionKey) {
|
|
40
|
-
logger.log(`[SESSION_MANAGER] 🗑️ Unregistering session: ${sessionKey}`);
|
|
41
|
-
logger.log(`[SESSION_MANAGER] - Active sessions before: ${activeSessions.size}`);
|
|
42
|
-
logger.log(`[SESSION_MANAGER] - Session existed: ${activeSessions.has(sessionKey)}`);
|
|
43
35
|
const existing = activeSessions.get(sessionKey);
|
|
44
36
|
if (!existing) {
|
|
45
|
-
logger.log(`[SESSION_MANAGER] - Session not found`);
|
|
46
37
|
return;
|
|
47
38
|
}
|
|
48
39
|
existing.refCount--;
|
|
49
|
-
logger.log(`[SESSION_MANAGER] - Decremented refCount: ${existing.refCount}`);
|
|
50
40
|
if (existing.refCount <= 0) {
|
|
51
41
|
activeSessions.delete(sessionKey);
|
|
52
42
|
configManager.clearSession(existing.sessionId);
|
|
53
|
-
logger.log(`[SESSION_MANAGER] - Deleted (refCount=0)`);
|
|
54
43
|
}
|
|
55
|
-
logger.log(`[SESSION_MANAGER] - Active sessions after: ${activeSessions.size}`);
|
|
56
|
-
logger.log(`[SESSION_MANAGER] - Remaining session keys: [${Array.from(activeSessions.keys()).join(", ")}]`);
|
|
57
44
|
}
|
|
58
45
|
/**
|
|
59
46
|
* Get session context by sessionKey.
|
|
60
47
|
* Returns null if session not found.
|
|
61
48
|
*/
|
|
62
49
|
export function getSessionContext(sessionKey) {
|
|
63
|
-
logger.log(`[SESSION_MANAGER] 🔍 Getting session by key: ${sessionKey}`);
|
|
64
|
-
logger.log(`[SESSION_MANAGER] - Active sessions: ${activeSessions.size}`);
|
|
65
50
|
const contextWithRef = activeSessions.get(sessionKey) ?? null;
|
|
66
|
-
logger.log(`[SESSION_MANAGER] - Found: ${contextWithRef !== null}`);
|
|
67
51
|
if (contextWithRef) {
|
|
68
|
-
logger.log(`[SESSION_MANAGER] - sessionId: ${contextWithRef.sessionId}`);
|
|
69
52
|
// 返回时去掉refCount字段
|
|
70
53
|
const { refCount, ...context } = contextWithRef;
|
|
71
54
|
return context;
|
|
@@ -74,25 +57,54 @@ export function getSessionContext(sessionKey) {
|
|
|
74
57
|
}
|
|
75
58
|
/**
|
|
76
59
|
* Get the most recent session context.
|
|
60
|
+
* @deprecated Use getCurrentSessionContext() instead for thread-safe access.
|
|
77
61
|
* This is a fallback for tools that don't have access to sessionKey.
|
|
78
62
|
* Returns null if no sessions are active.
|
|
79
63
|
*/
|
|
80
64
|
export function getLatestSessionContext() {
|
|
81
|
-
logger.log(`[SESSION_MANAGER] 🔍 Getting latest session context`);
|
|
82
|
-
logger.log(`[SESSION_MANAGER] - Active sessions count: ${activeSessions.size}`);
|
|
83
|
-
logger.log(`[SESSION_MANAGER] - Active session keys: [${Array.from(activeSessions.keys()).join(", ")}]`);
|
|
84
65
|
if (activeSessions.size === 0) {
|
|
85
|
-
logger.error(`[SESSION_MANAGER] - ❌ No active sessions found!`);
|
|
86
66
|
return null;
|
|
87
67
|
}
|
|
88
68
|
// Return the last added session
|
|
89
69
|
const sessions = Array.from(activeSessions.values());
|
|
90
70
|
const latestSessionWithRef = sessions[sessions.length - 1];
|
|
91
|
-
logger.log(`[SESSION_MANAGER] - ✅ Found latest session:`);
|
|
92
|
-
logger.log(`[SESSION_MANAGER] - sessionId: ${latestSessionWithRef.sessionId}`);
|
|
93
|
-
logger.log(`[SESSION_MANAGER] - taskId: ${latestSessionWithRef.taskId}`);
|
|
94
|
-
logger.log(`[SESSION_MANAGER] - messageId: ${latestSessionWithRef.messageId}`);
|
|
95
71
|
// 返回时去掉refCount字段
|
|
96
72
|
const { refCount, ...latestSession } = latestSessionWithRef;
|
|
97
73
|
return latestSession;
|
|
98
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Run a callback with a session context stored in AsyncLocalStorage.
|
|
77
|
+
* This ensures thread-safe context isolation for concurrent requests.
|
|
78
|
+
*/
|
|
79
|
+
export function runWithSessionContext(context, callback) {
|
|
80
|
+
return asyncLocalStorage.run(context, callback);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Get the current session context from AsyncLocalStorage.
|
|
84
|
+
* This is the recommended way to access session context in tools.
|
|
85
|
+
* Returns null if not running within a session context.
|
|
86
|
+
*
|
|
87
|
+
* Enhanced version: Automatically fetches the latest taskId from task-manager
|
|
88
|
+
* to support interruption scenarios where a new message updates the taskId.
|
|
89
|
+
*/
|
|
90
|
+
export function getCurrentSessionContext() {
|
|
91
|
+
// 1. Get base context from AsyncLocalStorage
|
|
92
|
+
const context = asyncLocalStorage.getStore() ?? null;
|
|
93
|
+
if (!context) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
// 2. Get latest taskId and messageId from task-manager
|
|
97
|
+
const latestTaskId = getCurrentTaskId(context.sessionId);
|
|
98
|
+
const latestMessageId = getCurrentMessageId(context.sessionId);
|
|
99
|
+
// 3. If task-manager has a newer taskId, use the latest value
|
|
100
|
+
if (latestTaskId && latestTaskId !== context.taskId) {
|
|
101
|
+
// Return updated context (create new object, don't modify original)
|
|
102
|
+
return {
|
|
103
|
+
...context,
|
|
104
|
+
taskId: latestTaskId,
|
|
105
|
+
messageId: latestMessageId ?? context.messageId,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
// 4. No update needed, return original context
|
|
109
|
+
return context;
|
|
110
|
+
}
|
|
@@ -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;
|