@ynhcj/xiaoyi-channel 0.0.2 → 0.0.3-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 +115 -35
- package/dist/src/channel.js +22 -1
- package/dist/src/client.d.ts +15 -0
- package/dist/src/client.js +97 -2
- package/dist/src/file-download.js +10 -1
- package/dist/src/file-upload.js +1 -1
- package/dist/src/formatter.d.ts +17 -0
- package/dist/src/formatter.js +86 -6
- package/dist/src/heartbeat.d.ts +2 -1
- package/dist/src/heartbeat.js +9 -1
- package/dist/src/monitor.d.ts +5 -0
- package/dist/src/monitor.js +131 -25
- package/dist/src/onboarding.js +7 -7
- package/dist/src/outbound.js +150 -71
- package/dist/src/parser.d.ts +5 -0
- package/dist/src/parser.js +15 -0
- package/dist/src/push.d.ts +7 -1
- package/dist/src/push.js +110 -19
- package/dist/src/reply-dispatcher.d.ts +1 -0
- package/dist/src/reply-dispatcher.js +210 -57
- package/dist/src/task-manager.d.ts +55 -0
- package/dist/src/task-manager.js +136 -0
- package/dist/src/tools/calendar-tool.d.ts +6 -0
- package/dist/src/tools/calendar-tool.js +167 -0
- package/dist/src/tools/call-phone-tool.d.ts +5 -0
- package/dist/src/tools/call-phone-tool.js +183 -0
- package/dist/src/tools/create-alarm-tool.d.ts +7 -0
- package/dist/src/tools/create-alarm-tool.js +444 -0
- package/dist/src/tools/delete-alarm-tool.d.ts +11 -0
- package/dist/src/tools/delete-alarm-tool.js +238 -0
- package/dist/src/tools/location-tool.js +48 -9
- package/dist/src/tools/modify-alarm-tool.d.ts +9 -0
- package/dist/src/tools/modify-alarm-tool.js +474 -0
- package/dist/src/tools/modify-note-tool.d.ts +9 -0
- package/dist/src/tools/modify-note-tool.js +163 -0
- package/dist/src/tools/note-tool.d.ts +5 -0
- package/dist/src/tools/note-tool.js +146 -0
- package/dist/src/tools/search-alarm-tool.d.ts +8 -0
- package/dist/src/tools/search-alarm-tool.js +389 -0
- package/dist/src/tools/search-calendar-tool.d.ts +12 -0
- package/dist/src/tools/search-calendar-tool.js +259 -0
- package/dist/src/tools/search-contact-tool.d.ts +5 -0
- package/dist/src/tools/search-contact-tool.js +168 -0
- package/dist/src/tools/search-file-tool.d.ts +5 -0
- package/dist/src/tools/search-file-tool.js +185 -0
- package/dist/src/tools/search-message-tool.d.ts +5 -0
- package/dist/src/tools/search-message-tool.js +173 -0
- package/dist/src/tools/search-note-tool.d.ts +5 -0
- package/dist/src/tools/search-note-tool.js +130 -0
- package/dist/src/tools/search-photo-gallery-tool.d.ts +8 -0
- package/dist/src/tools/search-photo-gallery-tool.js +184 -0
- package/dist/src/tools/search-photo-tool.d.ts +9 -0
- package/dist/src/tools/search-photo-tool.js +270 -0
- package/dist/src/tools/send-file-to-user-tool.d.ts +5 -0
- package/dist/src/tools/send-file-to-user-tool.js +318 -0
- package/dist/src/tools/send-message-tool.d.ts +5 -0
- package/dist/src/tools/send-message-tool.js +189 -0
- package/dist/src/tools/session-manager.d.ts +15 -0
- package/dist/src/tools/session-manager.js +126 -6
- package/dist/src/tools/upload-file-tool.d.ts +13 -0
- package/dist/src/tools/upload-file-tool.js +265 -0
- package/dist/src/tools/upload-photo-tool.d.ts +9 -0
- package/dist/src/tools/upload-photo-tool.js +223 -0
- package/dist/src/tools/xiaoyi-gui-tool.d.ts +6 -0
- package/dist/src/tools/xiaoyi-gui-tool.js +151 -0
- package/dist/src/types.d.ts +5 -9
- package/dist/src/utils/config-manager.d.ts +26 -0
- package/dist/src/utils/config-manager.js +56 -0
- package/dist/src/websocket.d.ts +41 -0
- package/dist/src/websocket.js +192 -9
- package/package.json +1 -2
|
@@ -0,0 +1,223 @@
|
|
|
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 photo tool - uploads local photos to get publicly accessible URLs.
|
|
7
|
+
* Requires mediaUris from search_photo_gallery tool as prerequisite.
|
|
8
|
+
*
|
|
9
|
+
* Prerequisites:
|
|
10
|
+
* 1. Call search_photo_gallery tool first to get mediaUris of photos
|
|
11
|
+
* 2. Use the mediaUris (maximum 5 at a time) to get public URLs
|
|
12
|
+
*/
|
|
13
|
+
export const uploadPhotoTool = {
|
|
14
|
+
name: "upload_photo",
|
|
15
|
+
label: "Upload Photo",
|
|
16
|
+
description: `工具能力描述:将手机本地文件回传并获取可公网访问的 URL。
|
|
17
|
+
|
|
18
|
+
前置工具调用:此工具使用前必须先调用 search_photo_gallery 工具获取照片的 mediaUri或者thumbnailUri
|
|
19
|
+
工具参数说明:
|
|
20
|
+
a. 入参中的mediaUris中的mediaUri必须与search_photo_gallery结果中对应的mediaUri或者thumbnailUri完全保持一致,不要自行修改,必须是file:://开头的路径。
|
|
21
|
+
b. 优先使用search_photo_gallery结果中的thumbnailUri作为入参,thumbnailUri是缩略图,清晰度与文件大小都非常合适展示给用户,如果thumbnailUri不存在或者用户要求使用原图,则使用search_photo_gallery结果中对应的mediaUri
|
|
22
|
+
c. mediaUris 是照片在手机本地的 URI 数组(从 search_photo_gallery 工具响应中获取)。限制:每次最多支持传入 3 条 mediaUri。
|
|
23
|
+
|
|
24
|
+
注意事项:
|
|
25
|
+
a. 操作超时时间为60秒,请勿重复调用此工具,如果超时或失败,最多重试一次。
|
|
26
|
+
b. 此工具返回的图片链接为用户公网可访问的链接,如果需要后续操作需要下载到本地,如果需要返回给用户查看则直接以图片markdown的形式返回给用户`,
|
|
27
|
+
parameters: {
|
|
28
|
+
type: "object",
|
|
29
|
+
properties: {
|
|
30
|
+
mediaUris: {
|
|
31
|
+
// 不指定 type,允许传入数组或 JSON 字符串
|
|
32
|
+
// 具体的类型验证和转换在 execute 函数内部进行
|
|
33
|
+
description: "照片在手机本地的 URI 数组,必须先通过 search_photo_gallery 工具获取。每次最多支持 3 条 URI。",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
required: ["mediaUris"],
|
|
37
|
+
},
|
|
38
|
+
async execute(toolCallId, params) {
|
|
39
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] 🚀 Starting execution`);
|
|
40
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] - toolCallId: ${toolCallId}`);
|
|
41
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] - params (raw):`, JSON.stringify(params));
|
|
42
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] - params.mediaUris type:`, typeof params.mediaUris);
|
|
43
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] - timestamp: ${new Date().toISOString()}`);
|
|
44
|
+
// ===== 参数规范化:兼容数组和 JSON 字符串 =====
|
|
45
|
+
let mediaUris = null;
|
|
46
|
+
if (!params.mediaUris) {
|
|
47
|
+
logger.error(`[UPLOAD_PHOTO_TOOL] ❌ Missing parameter: mediaUris`);
|
|
48
|
+
throw new Error("Missing required parameter: mediaUris");
|
|
49
|
+
}
|
|
50
|
+
// 情况1: 已经是数组
|
|
51
|
+
if (Array.isArray(params.mediaUris)) {
|
|
52
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] ✅ mediaUris is already an array`);
|
|
53
|
+
mediaUris = params.mediaUris;
|
|
54
|
+
}
|
|
55
|
+
// 情况2: 是字符串,尝试解析为 JSON 数组
|
|
56
|
+
else if (typeof params.mediaUris === 'string') {
|
|
57
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] 🔄 mediaUris is a string, attempting to parse as JSON...`);
|
|
58
|
+
try {
|
|
59
|
+
const parsed = JSON.parse(params.mediaUris);
|
|
60
|
+
if (Array.isArray(parsed)) {
|
|
61
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] ✅ Successfully parsed JSON string to array`);
|
|
62
|
+
mediaUris = parsed;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
logger.error(`[UPLOAD_PHOTO_TOOL] ❌ Parsed JSON is not an array:`, typeof parsed);
|
|
66
|
+
throw new Error("mediaUris must be an array or a JSON string representing an array");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch (parseError) {
|
|
70
|
+
logger.error(`[UPLOAD_PHOTO_TOOL] ❌ Failed to parse mediaUris as JSON:`, parseError);
|
|
71
|
+
throw new Error(`mediaUris must be a valid JSON array string. Parse error: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// 情况3: 其他类型,报错
|
|
75
|
+
else {
|
|
76
|
+
logger.error(`[UPLOAD_PHOTO_TOOL] ❌ Invalid mediaUris type:`, typeof params.mediaUris);
|
|
77
|
+
throw new Error(`mediaUris must be an array or a JSON string, got ${typeof params.mediaUris}`);
|
|
78
|
+
}
|
|
79
|
+
// 验证数组非空
|
|
80
|
+
if (!mediaUris || mediaUris.length === 0) {
|
|
81
|
+
logger.error(`[UPLOAD_PHOTO_TOOL] ❌ mediaUris array is empty`);
|
|
82
|
+
throw new Error("mediaUris array cannot be empty");
|
|
83
|
+
}
|
|
84
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] ✅ Normalized mediaUris:`, JSON.stringify(mediaUris));
|
|
85
|
+
// Validate maximum 5 URIs
|
|
86
|
+
if (mediaUris.length > 5) {
|
|
87
|
+
logger.error(`[UPLOAD_PHOTO_TOOL] ❌ Too many mediaUris: ${mediaUris.length}`);
|
|
88
|
+
throw new Error(`最多支持 5 条 mediaUri,当前提供了 ${mediaUris.length} 条。请分批处理。`);
|
|
89
|
+
}
|
|
90
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] - mediaUris count: ${mediaUris.length}`);
|
|
91
|
+
// Get session context
|
|
92
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] 🔍 Attempting to get session context...`);
|
|
93
|
+
const sessionContext = getCurrentSessionContext();
|
|
94
|
+
if (!sessionContext) {
|
|
95
|
+
logger.error(`[UPLOAD_PHOTO_TOOL] ❌ FAILED: No active session found!`);
|
|
96
|
+
logger.error(`[UPLOAD_PHOTO_TOOL] - toolCallId: ${toolCallId}`);
|
|
97
|
+
throw new Error("No active XY session found. Upload photo tool can only be used during an active conversation.");
|
|
98
|
+
}
|
|
99
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] ✅ Session context found`);
|
|
100
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] - sessionId: ${sessionContext.sessionId}`);
|
|
101
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] - taskId: ${sessionContext.taskId}`);
|
|
102
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] - messageId: ${sessionContext.messageId}`);
|
|
103
|
+
const { config, sessionId, taskId, messageId } = sessionContext;
|
|
104
|
+
// Get WebSocket manager
|
|
105
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] 🔌 Getting WebSocket manager...`);
|
|
106
|
+
const wsManager = getXYWebSocketManager(config);
|
|
107
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] ✅ WebSocket manager obtained`);
|
|
108
|
+
// Get public URLs for the photos
|
|
109
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] 🌐 Getting public URLs for photos...`);
|
|
110
|
+
const imageUrls = await getPhotoUrls(wsManager, config, sessionId, taskId, messageId, mediaUris);
|
|
111
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] 🎉 Successfully retrieved ${imageUrls.length} photo URLs`);
|
|
112
|
+
return {
|
|
113
|
+
content: [
|
|
114
|
+
{
|
|
115
|
+
type: "text",
|
|
116
|
+
text: JSON.stringify({
|
|
117
|
+
imageUrls,
|
|
118
|
+
count: imageUrls.length,
|
|
119
|
+
message: `成功获取 ${imageUrls.length} 张照片的公网访问 URL`
|
|
120
|
+
}),
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* Get public URLs for photos using mediaUris
|
|
128
|
+
* Returns array of publicly accessible image URLs
|
|
129
|
+
*/
|
|
130
|
+
async function getPhotoUrls(wsManager, config, sessionId, taskId, messageId, mediaUris) {
|
|
131
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] 📦 Building ImageUploadForClaw command...`);
|
|
132
|
+
// Build imageInfos array from mediaUris
|
|
133
|
+
const imageInfos = mediaUris.map(mediaUri => ({ mediaUri }));
|
|
134
|
+
const command = {
|
|
135
|
+
header: {
|
|
136
|
+
namespace: "Common",
|
|
137
|
+
name: "Action",
|
|
138
|
+
},
|
|
139
|
+
payload: {
|
|
140
|
+
cardParam: {},
|
|
141
|
+
executeParam: {
|
|
142
|
+
executeMode: "background",
|
|
143
|
+
intentName: "ImageUploadForClaw",
|
|
144
|
+
bundleName: "com.huawei.hmos.vassistant",
|
|
145
|
+
needUnlock: true,
|
|
146
|
+
actionResponse: true,
|
|
147
|
+
appType: "OHOS_APP",
|
|
148
|
+
timeOut: 5,
|
|
149
|
+
intentParam: {
|
|
150
|
+
imageInfos: imageInfos,
|
|
151
|
+
},
|
|
152
|
+
permissionId: [],
|
|
153
|
+
achieveType: "INTENT",
|
|
154
|
+
},
|
|
155
|
+
responses: [
|
|
156
|
+
{
|
|
157
|
+
resultCode: "",
|
|
158
|
+
displayText: "",
|
|
159
|
+
ttsText: "",
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
needUploadResult: true,
|
|
163
|
+
noHalfPage: false,
|
|
164
|
+
pageControlRelated: false,
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
return new Promise((resolve, reject) => {
|
|
168
|
+
const timeout = setTimeout(() => {
|
|
169
|
+
logger.error(`[UPLOAD_PHOTO_TOOL] ⏰ Timeout: No response for ImageUploadForClaw within 60 seconds`);
|
|
170
|
+
wsManager.off("data-event", handler);
|
|
171
|
+
reject(new Error("获取照片URL超时(60秒)"));
|
|
172
|
+
}, 60000);
|
|
173
|
+
const handler = (event) => {
|
|
174
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] 📨 Received data event:`, JSON.stringify(event));
|
|
175
|
+
if (event.intentName === "ImageUploadForClaw") {
|
|
176
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] 🎯 ImageUploadForClaw event received`);
|
|
177
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] - status: ${event.status}`);
|
|
178
|
+
clearTimeout(timeout);
|
|
179
|
+
wsManager.off("data-event", handler);
|
|
180
|
+
if (event.status === "success" && event.outputs) {
|
|
181
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] ✅ Image URL retrieval completed successfully`);
|
|
182
|
+
const result = event.outputs.result;
|
|
183
|
+
let imageUrls = result?.imageUrls || [];
|
|
184
|
+
// Decode Unicode escape sequences in URLs
|
|
185
|
+
// Replace \u003d with = and \u0026 with &
|
|
186
|
+
imageUrls = imageUrls.map((url) => {
|
|
187
|
+
const decodedUrl = url
|
|
188
|
+
.replace(/\\u003d/g, '=')
|
|
189
|
+
.replace(/\\u0026/g, '&');
|
|
190
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] 🔄 Decoded URL: ${url} -> ${decodedUrl}`);
|
|
191
|
+
return decodedUrl;
|
|
192
|
+
});
|
|
193
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] 📊 Retrieved and decoded ${imageUrls.length} image URLs`);
|
|
194
|
+
resolve(imageUrls);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
logger.error(`[UPLOAD_PHOTO_TOOL] ❌ Image URL retrieval failed`);
|
|
198
|
+
logger.error(`[UPLOAD_PHOTO_TOOL] - status: ${event.status}`);
|
|
199
|
+
reject(new Error(`获取照片URL失败: ${event.status}`));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] 📡 Registering data-event handler for ImageUploadForClaw`);
|
|
204
|
+
wsManager.on("data-event", handler);
|
|
205
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] 📤 Sending ImageUploadForClaw command...`);
|
|
206
|
+
sendCommand({
|
|
207
|
+
config,
|
|
208
|
+
sessionId,
|
|
209
|
+
taskId,
|
|
210
|
+
messageId,
|
|
211
|
+
command,
|
|
212
|
+
})
|
|
213
|
+
.then(() => {
|
|
214
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] ✅ ImageUploadForClaw command sent successfully`);
|
|
215
|
+
})
|
|
216
|
+
.catch((error) => {
|
|
217
|
+
logger.error(`[UPLOAD_PHOTO_TOOL] ❌ Failed to send ImageUploadForClaw command:`, error);
|
|
218
|
+
clearTimeout(timeout);
|
|
219
|
+
wsManager.off("data-event", handler);
|
|
220
|
+
reject(error);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XiaoYi GUI tool - executes phone app interactions through GUI agent.
|
|
3
|
+
* Simulates user interactions on phone screen (click, swipe, input, navigation, etc.)
|
|
4
|
+
* to complete tasks that cannot be done through internet APIs.
|
|
5
|
+
*/
|
|
6
|
+
export declare const xiaoyiGuiTool: any;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// XiaoYi GUI tool implementation - simulates phone screen interactions
|
|
2
|
+
import { getXYWebSocketManager } from "../client.js";
|
|
3
|
+
import { sendCommand } from "../formatter.js";
|
|
4
|
+
import { getCurrentSessionContext } from "./session-manager.js";
|
|
5
|
+
import { logger } from "../utils/logger.js";
|
|
6
|
+
/**
|
|
7
|
+
* XiaoYi GUI tool - executes phone app interactions through GUI agent.
|
|
8
|
+
* Simulates user interactions on phone screen (click, swipe, input, navigation, etc.)
|
|
9
|
+
* to complete tasks that cannot be done through internet APIs.
|
|
10
|
+
*/
|
|
11
|
+
export const xiaoyiGuiTool = {
|
|
12
|
+
name: "xiaoyi_gui_agent",
|
|
13
|
+
label: "XiaoYi GUI Agent",
|
|
14
|
+
description: `通过模拟人在手机屏幕上的交互行为(点击、滑动、输入、页面导航等),自动完成手机APP中的各类任务。
|
|
15
|
+
|
|
16
|
+
该工具操作方式类似真实用户在手机上的操作,因此可以完成许多无法通过互联网API实现的任务,例如:
|
|
17
|
+
- 任务需要真实操作手机APP界面
|
|
18
|
+
- 数据仅存在于APP内部
|
|
19
|
+
- 无法通过互联网API获取数据
|
|
20
|
+
- 需要完成用户行为(签到、关注、购买等)
|
|
21
|
+
- 需要在APP中发布或发送内容
|
|
22
|
+
- 需要修改APP或手机设置
|
|
23
|
+
|
|
24
|
+
注意事项:
|
|
25
|
+
- 操作超时时间为3分钟(180秒)
|
|
26
|
+
- 该工具执行时间较长,请勿重复调用
|
|
27
|
+
- 该工具执行期间不要执行别的工具调用,必须等到该工具有结果返回或者超时之后才能执行别的操作,无论是新的文本回复还是下一步的工具调用,在此工具执行期间必须严格等待
|
|
28
|
+
- 如果超时或失败,最多重试一次
|
|
29
|
+
- 如果用户指令中包含备忘录读写,日程查看,不需要将这类操作放在gui tool的query参数中,需要使用预置的note相关工具与calendar相关工具完成相关操作`,
|
|
30
|
+
parameters: {
|
|
31
|
+
type: "object",
|
|
32
|
+
properties: {
|
|
33
|
+
query: {
|
|
34
|
+
type: "string",
|
|
35
|
+
description: "操作手机的指令以及期望返回的结果。",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
required: ["query"],
|
|
39
|
+
},
|
|
40
|
+
async execute(toolCallId, params) {
|
|
41
|
+
logger.log(`[XIAOYI_GUI_TOOL] 🚀 Starting execution`);
|
|
42
|
+
logger.log(`[XIAOYI_GUI_TOOL] - toolCallId: ${toolCallId}`);
|
|
43
|
+
logger.log(`[XIAOYI_GUI_TOOL] - query: ${params.query}`);
|
|
44
|
+
logger.log(`[XIAOYI_GUI_TOOL] - timestamp: ${new Date().toISOString()}`);
|
|
45
|
+
// Validate parameters
|
|
46
|
+
if (!params.query || typeof params.query !== "string") {
|
|
47
|
+
logger.error(`[XIAOYI_GUI_TOOL] ❌ FAILED: Invalid query parameter`);
|
|
48
|
+
throw new Error("Missing or invalid required parameter: query must be a non-empty string");
|
|
49
|
+
}
|
|
50
|
+
// Get session context
|
|
51
|
+
logger.log(`[XIAOYI_GUI_TOOL] 🔍 Attempting to get session context...`);
|
|
52
|
+
const sessionContext = getCurrentSessionContext();
|
|
53
|
+
if (!sessionContext) {
|
|
54
|
+
logger.error(`[XIAOYI_GUI_TOOL] ❌ FAILED: No active session found!`);
|
|
55
|
+
logger.error(`[XIAOYI_GUI_TOOL] - toolCallId: ${toolCallId}`);
|
|
56
|
+
throw new Error("No active XY session found. XiaoYi GUI tool can only be used during an active conversation.");
|
|
57
|
+
}
|
|
58
|
+
logger.log(`[XIAOYI_GUI_TOOL] ✅ Session context found`);
|
|
59
|
+
logger.log(`[XIAOYI_GUI_TOOL] - sessionId: ${sessionContext.sessionId}`);
|
|
60
|
+
logger.log(`[XIAOYI_GUI_TOOL] - taskId (interactionId): ${sessionContext.taskId}`);
|
|
61
|
+
logger.log(`[XIAOYI_GUI_TOOL] - messageId: ${sessionContext.messageId}`);
|
|
62
|
+
logger.log(`[XIAOYI_GUI_TOOL] - agentId: ${sessionContext.agentId}`);
|
|
63
|
+
const { config, sessionId, taskId, messageId } = sessionContext;
|
|
64
|
+
// Get WebSocket manager
|
|
65
|
+
logger.log(`[XIAOYI_GUI_TOOL] 🔌 Getting WebSocket manager...`);
|
|
66
|
+
const wsManager = getXYWebSocketManager(config);
|
|
67
|
+
logger.log(`[XIAOYI_GUI_TOOL] ✅ WebSocket manager obtained`);
|
|
68
|
+
// Build InvokeJarvisGUIAgentRequest command
|
|
69
|
+
logger.log(`[XIAOYI_GUI_TOOL] 📦 Building InvokeJarvisGUIAgentRequest command...`);
|
|
70
|
+
const command = {
|
|
71
|
+
header: {
|
|
72
|
+
namespace: "ClawAgent",
|
|
73
|
+
name: "InvokeJarvisGUIAgentRequest",
|
|
74
|
+
},
|
|
75
|
+
payload: {
|
|
76
|
+
query: params.query,
|
|
77
|
+
sessionId: sessionId,
|
|
78
|
+
interactionId: taskId, // taskId corresponds to interactionId
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
logger.log(`[XIAOYI_GUI_TOOL] 📋 Command details:`, JSON.stringify(command, null, 2));
|
|
82
|
+
// Send command and wait for response (5 minute timeout)
|
|
83
|
+
logger.log(`[XIAOYI_GUI_TOOL] ⏳ Setting up promise to wait for GUI agent response...`);
|
|
84
|
+
logger.log(`[XIAOYI_GUI_TOOL] - Timeout: 300 seconds (5 minutes)`);
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
const timeout = setTimeout(() => {
|
|
87
|
+
logger.error(`[XIAOYI_GUI_TOOL] ⏰ Timeout: No response received within 300 seconds (5 minutes)`);
|
|
88
|
+
wsManager.off("gui-agent-response", handler);
|
|
89
|
+
reject(new Error("XiaoYi GUI Agent 操作超时(5分钟)"));
|
|
90
|
+
}, 180000); // 5 minutes timeout
|
|
91
|
+
// Listen for GUI agent response events
|
|
92
|
+
const handler = (event) => {
|
|
93
|
+
logger.log(`[XIAOYI_GUI_TOOL] 📨 Received event:`, JSON.stringify(event));
|
|
94
|
+
// Check if this is the InvokeJarvisGUIAgentResponse we're waiting for
|
|
95
|
+
if (event.header?.namespace === "ClawAgent" &&
|
|
96
|
+
event.header?.name === "InvokeJarvisGUIAgentResponse") {
|
|
97
|
+
logger.log(`[XIAOYI_GUI_TOOL] 🎯 InvokeJarvisGUIAgentResponse event received`);
|
|
98
|
+
logger.log(`[XIAOYI_GUI_TOOL] - isFinal: ${event.payload?.isFinal}`);
|
|
99
|
+
// According to the spec, we only get one response (isFinal: true)
|
|
100
|
+
if (event.payload?.isFinal === true) {
|
|
101
|
+
clearTimeout(timeout);
|
|
102
|
+
wsManager.off("gui-agent-response", handler);
|
|
103
|
+
const streamContent = event.payload?.streamInfo?.streamContent;
|
|
104
|
+
if (streamContent) {
|
|
105
|
+
logger.log(`[XIAOYI_GUI_TOOL] ✅ GUI Agent operation completed successfully`);
|
|
106
|
+
logger.log(`[XIAOYI_GUI_TOOL] - streamContent: ${streamContent}`);
|
|
107
|
+
resolve({
|
|
108
|
+
content: [
|
|
109
|
+
{
|
|
110
|
+
type: "text",
|
|
111
|
+
text: streamContent,
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
logger.error(`[XIAOYI_GUI_TOOL] ❌ Response missing streamContent`);
|
|
118
|
+
logger.error(`[XIAOYI_GUI_TOOL] - payload:`, JSON.stringify(event.payload));
|
|
119
|
+
reject(new Error("XiaoYi GUI Agent 响应格式错误:缺少 streamContent"));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else if (event.payload?.isFinal === false) {
|
|
123
|
+
// According to spec, we shouldn't get intermediate responses, but log if we do
|
|
124
|
+
logger.log(`[XIAOYI_GUI_TOOL] 📝 Intermediate response received (isFinal: false), waiting for final...`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
// Register event handler
|
|
129
|
+
// Note: The WebSocket manager needs to emit 'gui-agent-response' when receiving this type of response
|
|
130
|
+
logger.log(`[XIAOYI_GUI_TOOL] 📡 Registering gui-agent-response handler on WebSocket manager`);
|
|
131
|
+
wsManager.on("gui-agent-response", handler);
|
|
132
|
+
// Send the command
|
|
133
|
+
logger.log(`[XIAOYI_GUI_TOOL] 📤 Sending InvokeJarvisGUIAgentRequest command...`);
|
|
134
|
+
sendCommand({
|
|
135
|
+
config,
|
|
136
|
+
sessionId,
|
|
137
|
+
taskId,
|
|
138
|
+
messageId,
|
|
139
|
+
command,
|
|
140
|
+
}).then(() => {
|
|
141
|
+
logger.log(`[XIAOYI_GUI_TOOL] ✅ Command sent successfully, waiting for response...`);
|
|
142
|
+
logger.log(`[XIAOYI_GUI_TOOL] - This may take up to 5 minutes depending on the task complexity`);
|
|
143
|
+
}).catch((error) => {
|
|
144
|
+
logger.error(`[XIAOYI_GUI_TOOL] ❌ Failed to send command:`, error);
|
|
145
|
+
clearTimeout(timeout);
|
|
146
|
+
wsManager.off("gui-agent-response", handler);
|
|
147
|
+
reject(error);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
},
|
|
151
|
+
};
|
package/dist/src/types.d.ts
CHANGED
|
@@ -75,7 +75,11 @@ export interface A2AArtifact {
|
|
|
75
75
|
artifactId: string;
|
|
76
76
|
parts: A2AArtifactPart[];
|
|
77
77
|
}
|
|
78
|
-
export
|
|
78
|
+
export interface A2AReasoningTextPart {
|
|
79
|
+
kind: "reasoningText";
|
|
80
|
+
reasoningText: string;
|
|
81
|
+
}
|
|
82
|
+
export type A2AArtifactPart = A2ATextPart | A2ADataPart | A2ACommandPart | A2AReasoningTextPart;
|
|
79
83
|
export interface A2ACommandPart {
|
|
80
84
|
kind: "command";
|
|
81
85
|
command: A2ACommand;
|
|
@@ -155,14 +159,6 @@ export interface FileUploadCompleteRequest {
|
|
|
155
159
|
export interface FileUploadCompleteResponse {
|
|
156
160
|
fileId: string;
|
|
157
161
|
}
|
|
158
|
-
export interface PushMessageRequest {
|
|
159
|
-
apiKey: string;
|
|
160
|
-
apiId: string;
|
|
161
|
-
pushId: string;
|
|
162
|
-
sessionId: string;
|
|
163
|
-
title: string;
|
|
164
|
-
content: string;
|
|
165
|
-
}
|
|
166
162
|
export type ServerIdentifier = "server1" | "server2";
|
|
167
163
|
export interface SessionBinding {
|
|
168
164
|
sessionId: string;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manages dynamic configuration updates that can change at runtime.
|
|
3
|
+
* Specifically handles pushId which can be updated per-session.
|
|
4
|
+
*/
|
|
5
|
+
declare class ConfigManager {
|
|
6
|
+
private sessionPushIds;
|
|
7
|
+
private globalPushId;
|
|
8
|
+
/**
|
|
9
|
+
* Update push ID for a specific session.
|
|
10
|
+
*/
|
|
11
|
+
updatePushId(sessionId: string, pushId: string): void;
|
|
12
|
+
/**
|
|
13
|
+
* Get push ID for a session (falls back to global if not found).
|
|
14
|
+
*/
|
|
15
|
+
getPushId(sessionId?: string): string | null;
|
|
16
|
+
/**
|
|
17
|
+
* Clear push ID for a session.
|
|
18
|
+
*/
|
|
19
|
+
clearSession(sessionId: string): void;
|
|
20
|
+
/**
|
|
21
|
+
* Clear all cached push IDs.
|
|
22
|
+
*/
|
|
23
|
+
clear(): void;
|
|
24
|
+
}
|
|
25
|
+
export declare const configManager: ConfigManager;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Dynamic configuration manager for runtime updates
|
|
2
|
+
import { logger } from "./logger.js";
|
|
3
|
+
/**
|
|
4
|
+
* Manages dynamic configuration updates that can change at runtime.
|
|
5
|
+
* Specifically handles pushId which can be updated per-session.
|
|
6
|
+
*/
|
|
7
|
+
class ConfigManager {
|
|
8
|
+
sessionPushIds = new Map();
|
|
9
|
+
globalPushId = null;
|
|
10
|
+
/**
|
|
11
|
+
* Update push ID for a specific session.
|
|
12
|
+
*/
|
|
13
|
+
updatePushId(sessionId, pushId) {
|
|
14
|
+
if (!pushId) {
|
|
15
|
+
logger.warn(`[ConfigManager] Attempted to set empty pushId for session ${sessionId}`);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const previous = this.sessionPushIds.get(sessionId);
|
|
19
|
+
if (previous !== pushId) {
|
|
20
|
+
logger.log(`[ConfigManager] ✨ Updated pushId for session ${sessionId}`);
|
|
21
|
+
logger.log(`[ConfigManager] - Previous: ${previous ? previous.substring(0, 20) + '...' : 'none'}`);
|
|
22
|
+
logger.log(`[ConfigManager] - New: ${pushId.substring(0, 20)}...`);
|
|
23
|
+
logger.log(`[ConfigManager] - Full new pushId: ${pushId}`);
|
|
24
|
+
this.sessionPushIds.set(sessionId, pushId);
|
|
25
|
+
this.globalPushId = pushId; // Also update global for backward compatibility
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Get push ID for a session (falls back to global if not found).
|
|
30
|
+
*/
|
|
31
|
+
getPushId(sessionId) {
|
|
32
|
+
if (sessionId) {
|
|
33
|
+
const sessionPushId = this.sessionPushIds.get(sessionId);
|
|
34
|
+
if (sessionPushId) {
|
|
35
|
+
return sessionPushId;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return this.globalPushId;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Clear push ID for a session.
|
|
42
|
+
*/
|
|
43
|
+
clearSession(sessionId) {
|
|
44
|
+
this.sessionPushIds.delete(sessionId);
|
|
45
|
+
logger.debug(`[ConfigManager] Cleared pushId for session ${sessionId}`);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Clear all cached push IDs.
|
|
49
|
+
*/
|
|
50
|
+
clear() {
|
|
51
|
+
this.sessionPushIds.clear();
|
|
52
|
+
this.globalPushId = null;
|
|
53
|
+
logger.debug(`[ConfigManager] Cleared all cached pushIds`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export const configManager = new ConfigManager();
|
package/dist/src/websocket.d.ts
CHANGED
|
@@ -1,12 +1,39 @@
|
|
|
1
1
|
import { EventEmitter } from "events";
|
|
2
2
|
import type { RuntimeEnv } from "openclaw/plugin-sdk";
|
|
3
3
|
import type { XYChannelConfig, OutboundWebSocketMessage } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Diagnostics for a single WebSocket connection
|
|
6
|
+
*/
|
|
7
|
+
export interface ConnectionDiagnostic {
|
|
8
|
+
exists: boolean;
|
|
9
|
+
readyState: string;
|
|
10
|
+
stateConnected: boolean;
|
|
11
|
+
stateReady: boolean;
|
|
12
|
+
reconnectAttempts: number;
|
|
13
|
+
lastHeartbeat: number;
|
|
14
|
+
heartbeatActive: boolean;
|
|
15
|
+
hasReconnectTimer: boolean;
|
|
16
|
+
listenerCount: number;
|
|
17
|
+
isOrphan: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Full diagnostics for WebSocket manager
|
|
21
|
+
*/
|
|
22
|
+
export interface ManagerDiagnostics {
|
|
23
|
+
cacheKey: string;
|
|
24
|
+
server1: ConnectionDiagnostic;
|
|
25
|
+
server2: ConnectionDiagnostic;
|
|
26
|
+
isShuttingDown: boolean;
|
|
27
|
+
totalEventListeners: number;
|
|
28
|
+
}
|
|
4
29
|
/**
|
|
5
30
|
* Manages dual WebSocket connections to XY servers.
|
|
6
31
|
* Implements session-to-server binding for message routing.
|
|
7
32
|
*
|
|
8
33
|
* Events:
|
|
9
34
|
* - 'message': (message: A2AJsonRpcRequest, sessionId: string, serverId: ServerIdentifier) => void
|
|
35
|
+
* - 'data-event': (event: A2ADataEvent) => void
|
|
36
|
+
* - 'gui-agent-response': (event: any) => void
|
|
10
37
|
* - 'connected': (serverId: ServerIdentifier) => void
|
|
11
38
|
* - 'disconnected': (serverId: ServerIdentifier) => void
|
|
12
39
|
* - 'error': (error: Error, serverId: ServerIdentifier) => void
|
|
@@ -26,7 +53,12 @@ export declare class XYWebSocketManager extends EventEmitter {
|
|
|
26
53
|
private isShuttingDown;
|
|
27
54
|
private log;
|
|
28
55
|
private error;
|
|
56
|
+
private onHealthEvent?;
|
|
29
57
|
constructor(config: XYChannelConfig, runtime?: RuntimeEnv);
|
|
58
|
+
/**
|
|
59
|
+
* Set health event callback to report activity to OpenClaw framework.
|
|
60
|
+
*/
|
|
61
|
+
setHealthEventCallback(callback: () => void): void;
|
|
30
62
|
/**
|
|
31
63
|
* Check if config matches the current instance.
|
|
32
64
|
*/
|
|
@@ -48,6 +80,15 @@ export declare class XYWebSocketManager extends EventEmitter {
|
|
|
48
80
|
* Check if at least one server is ready.
|
|
49
81
|
*/
|
|
50
82
|
isReady(): boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Get detailed connection diagnostics for monitoring and debugging.
|
|
85
|
+
* Helps identify orphan connections and connection leaks.
|
|
86
|
+
*/
|
|
87
|
+
getConnectionDiagnostics(): ManagerDiagnostics;
|
|
88
|
+
/**
|
|
89
|
+
* Get diagnostic info for a single server connection.
|
|
90
|
+
*/
|
|
91
|
+
private getServerDiagnostic;
|
|
51
92
|
/**
|
|
52
93
|
* Connect to a specific server.
|
|
53
94
|
*/
|