@ynhcj/xiaoyi-channel 1.1.0 → 1.1.2
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/channel.js +2 -1
- package/dist/src/formatter.d.ts +17 -0
- package/dist/src/formatter.js +43 -0
- package/dist/src/reply-dispatcher.js +146 -3
- package/dist/src/tools/search-calendar-tool.d.ts +12 -0
- package/dist/src/tools/search-calendar-tool.js +220 -0
- package/dist/src/types.d.ts +5 -1
- package/package.json +1 -1
package/dist/src/channel.js
CHANGED
|
@@ -6,6 +6,7 @@ import { locationTool } from "./tools/location-tool.js";
|
|
|
6
6
|
import { noteTool } from "./tools/note-tool.js";
|
|
7
7
|
import { searchNoteTool } from "./tools/search-note-tool.js";
|
|
8
8
|
import { calendarTool } from "./tools/calendar-tool.js";
|
|
9
|
+
import { searchCalendarTool } from "./tools/search-calendar-tool.js";
|
|
9
10
|
/**
|
|
10
11
|
* Xiaoyi Channel Plugin for OpenClaw.
|
|
11
12
|
* Implements Xiaoyi A2A protocol with dual WebSocket connections.
|
|
@@ -45,7 +46,7 @@ export const xyPlugin = {
|
|
|
45
46
|
},
|
|
46
47
|
outbound: xyOutbound,
|
|
47
48
|
onboarding: xyOnboardingAdapter,
|
|
48
|
-
agentTools: [locationTool, noteTool, searchNoteTool, calendarTool],
|
|
49
|
+
agentTools: [locationTool, noteTool, searchNoteTool, calendarTool, searchCalendarTool],
|
|
49
50
|
messaging: {
|
|
50
51
|
normalizeTarget: (raw) => {
|
|
51
52
|
const trimmed = raw.trim();
|
package/dist/src/formatter.d.ts
CHANGED
|
@@ -20,6 +20,23 @@ export interface SendA2AResponseParams {
|
|
|
20
20
|
* Send an A2A artifact update response.
|
|
21
21
|
*/
|
|
22
22
|
export declare function sendA2AResponse(params: SendA2AResponseParams): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Parameters for sending a reasoning text update (intermediate, streamed).
|
|
25
|
+
*/
|
|
26
|
+
export interface SendReasoningTextUpdateParams {
|
|
27
|
+
config: XYChannelConfig;
|
|
28
|
+
sessionId: string;
|
|
29
|
+
taskId: string;
|
|
30
|
+
messageId: string;
|
|
31
|
+
text: string;
|
|
32
|
+
append?: boolean;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Send an A2A artifact-update with reasoningText part.
|
|
36
|
+
* Used for onToolStart, onToolResult, onReasoningStream, onReasoningEnd, onPartialReply.
|
|
37
|
+
* append=true, final=false, lastChunk=true, text is suffixed with newline for markdown rendering.
|
|
38
|
+
*/
|
|
39
|
+
export declare function sendReasoningTextUpdate(params: SendReasoningTextUpdateParams): Promise<void>;
|
|
23
40
|
/**
|
|
24
41
|
* Parameters for sending a status update.
|
|
25
42
|
*/
|
package/dist/src/formatter.js
CHANGED
|
@@ -67,6 +67,49 @@ export async function sendA2AResponse(params) {
|
|
|
67
67
|
await wsManager.sendMessage(sessionId, outboundMessage);
|
|
68
68
|
log(`[A2A_RESPONSE] ✅ Message sent successfully`);
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Send an A2A artifact-update with reasoningText part.
|
|
72
|
+
* Used for onToolStart, onToolResult, onReasoningStream, onReasoningEnd, onPartialReply.
|
|
73
|
+
* append=true, final=false, lastChunk=true, text is suffixed with newline for markdown rendering.
|
|
74
|
+
*/
|
|
75
|
+
export async function sendReasoningTextUpdate(params) {
|
|
76
|
+
const { config, sessionId, taskId, messageId, text, append = true } = params;
|
|
77
|
+
const runtime = getXYRuntime();
|
|
78
|
+
const log = runtime?.log ?? console.log;
|
|
79
|
+
const error = runtime?.error ?? console.error;
|
|
80
|
+
const artifact = {
|
|
81
|
+
taskId,
|
|
82
|
+
kind: "artifact-update",
|
|
83
|
+
append,
|
|
84
|
+
lastChunk: true,
|
|
85
|
+
final: false,
|
|
86
|
+
artifact: {
|
|
87
|
+
artifactId: uuidv4(),
|
|
88
|
+
parts: [
|
|
89
|
+
{
|
|
90
|
+
kind: "reasoningText",
|
|
91
|
+
reasoningText: text,
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
const jsonRpcResponse = {
|
|
97
|
+
jsonrpc: "2.0",
|
|
98
|
+
id: messageId,
|
|
99
|
+
result: artifact,
|
|
100
|
+
};
|
|
101
|
+
const wsManager = getXYWebSocketManager(config);
|
|
102
|
+
const outboundMessage = {
|
|
103
|
+
msgType: "agent_response",
|
|
104
|
+
agentId: config.agentId,
|
|
105
|
+
sessionId,
|
|
106
|
+
taskId,
|
|
107
|
+
msgDetail: JSON.stringify(jsonRpcResponse),
|
|
108
|
+
};
|
|
109
|
+
log(`[REASONING_TEXT] 📤 Sending reasoningText update: sessionId=${sessionId}, taskId=${taskId}, text.length=${text.length}`);
|
|
110
|
+
await wsManager.sendMessage(sessionId, outboundMessage);
|
|
111
|
+
log(`[REASONING_TEXT] ✅ Sent successfully`);
|
|
112
|
+
}
|
|
70
113
|
/**
|
|
71
114
|
* Send an A2A task status update.
|
|
72
115
|
* Follows A2A protocol standard format with nested status object.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createReplyPrefixContext } from "openclaw/plugin-sdk";
|
|
2
2
|
import { getXYRuntime } from "./runtime.js";
|
|
3
|
-
import { sendA2AResponse, sendStatusUpdate } from "./formatter.js";
|
|
3
|
+
import { sendA2AResponse, sendStatusUpdate, sendReasoningTextUpdate } from "./formatter.js";
|
|
4
4
|
import { resolveXYConfig } from "./config.js";
|
|
5
5
|
/**
|
|
6
6
|
* Create a reply dispatcher for XY channel messages.
|
|
@@ -87,6 +87,15 @@ export function createXYReplyDispatcher(params) {
|
|
|
87
87
|
accumulatedText += text;
|
|
88
88
|
hasSentResponse = true;
|
|
89
89
|
log(`[DELIVER ACCUMULATE] Accumulated text, current length=${accumulatedText.length}`);
|
|
90
|
+
// Also stream text as reasoningText for real-time display
|
|
91
|
+
await sendReasoningTextUpdate({
|
|
92
|
+
config,
|
|
93
|
+
sessionId,
|
|
94
|
+
taskId,
|
|
95
|
+
messageId,
|
|
96
|
+
text,
|
|
97
|
+
});
|
|
98
|
+
log(`[DELIVER] ✅ Sent deliver text as reasoningText update`);
|
|
90
99
|
}
|
|
91
100
|
catch (deliverError) {
|
|
92
101
|
error(`Failed to deliver message:`, deliverError);
|
|
@@ -119,6 +128,16 @@ export function createXYReplyDispatcher(params) {
|
|
|
119
128
|
if (hasSentResponse && !finalSent) {
|
|
120
129
|
log(`[ON_IDLE] Sending accumulated text, length=${accumulatedText.length}`);
|
|
121
130
|
try {
|
|
131
|
+
// Send status update before final message
|
|
132
|
+
await sendStatusUpdate({
|
|
133
|
+
config,
|
|
134
|
+
sessionId,
|
|
135
|
+
taskId,
|
|
136
|
+
messageId,
|
|
137
|
+
text: "任务处理已完成~",
|
|
138
|
+
state: "completed",
|
|
139
|
+
});
|
|
140
|
+
log(`[ON_IDLE] ✅ Sent completion status update`);
|
|
122
141
|
await sendA2AResponse({
|
|
123
142
|
config,
|
|
124
143
|
sessionId,
|
|
@@ -137,14 +156,38 @@ export function createXYReplyDispatcher(params) {
|
|
|
137
156
|
}
|
|
138
157
|
else {
|
|
139
158
|
log(`[ON_IDLE] Skipping final message: hasSentResponse=${hasSentResponse}, finalSent=${finalSent}`);
|
|
159
|
+
// Task was interrupted - send failure status and error response
|
|
160
|
+
try {
|
|
161
|
+
await sendStatusUpdate({
|
|
162
|
+
config,
|
|
163
|
+
sessionId,
|
|
164
|
+
taskId,
|
|
165
|
+
messageId,
|
|
166
|
+
text: "任务处理中断了~",
|
|
167
|
+
state: "failed",
|
|
168
|
+
});
|
|
169
|
+
log(`[ON_IDLE] ✅ Sent failure status update`);
|
|
170
|
+
await sendA2AResponse({
|
|
171
|
+
config,
|
|
172
|
+
sessionId,
|
|
173
|
+
taskId,
|
|
174
|
+
messageId,
|
|
175
|
+
text: "任务执行异常,请重试~",
|
|
176
|
+
append: false,
|
|
177
|
+
final: true,
|
|
178
|
+
});
|
|
179
|
+
finalSent = true;
|
|
180
|
+
log(`[ON_IDLE] ✅ Sent error response`);
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
error(`[ON_IDLE] Failed to send failure status and error response:`, err);
|
|
184
|
+
}
|
|
140
185
|
}
|
|
141
186
|
// Stop status updates
|
|
142
187
|
stopStatusInterval();
|
|
143
188
|
},
|
|
144
189
|
onCleanup: () => {
|
|
145
190
|
log(`[ON_CLEANUP] Reply cleanup for session ${sessionId}, hasSentResponse=${hasSentResponse}, finalSent=${finalSent}`);
|
|
146
|
-
// Stop status updates
|
|
147
|
-
stopStatusInterval();
|
|
148
191
|
},
|
|
149
192
|
});
|
|
150
193
|
return {
|
|
@@ -152,6 +195,106 @@ export function createXYReplyDispatcher(params) {
|
|
|
152
195
|
replyOptions: {
|
|
153
196
|
...replyOptions,
|
|
154
197
|
onModelSelected: prefixContext.onModelSelected,
|
|
198
|
+
// 🔧 Tool execution start callback
|
|
199
|
+
onToolStart: async ({ name, phase }) => {
|
|
200
|
+
log(`[TOOL START] 🔧 Tool execution started/updated: name=${name}, phase=${phase}, session=${sessionId}, taskId=${taskId}`);
|
|
201
|
+
if (phase === "start") {
|
|
202
|
+
const toolName = name || "unknown";
|
|
203
|
+
try {
|
|
204
|
+
await sendStatusUpdate({
|
|
205
|
+
config,
|
|
206
|
+
sessionId,
|
|
207
|
+
taskId,
|
|
208
|
+
messageId,
|
|
209
|
+
text: `正在使用工具: ${toolName}...`,
|
|
210
|
+
state: "working",
|
|
211
|
+
});
|
|
212
|
+
log(`[TOOL START] ✅ Sent status update for tool start: ${toolName}`);
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
error(`[TOOL START] ❌ Failed to send tool start status:`, err);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
// 🔧 Tool execution result callback
|
|
220
|
+
onToolResult: async (payload) => {
|
|
221
|
+
const text = payload.text ?? "";
|
|
222
|
+
const hasMedia = Boolean(payload.mediaUrl || (payload.mediaUrls?.length ?? 0) > 0);
|
|
223
|
+
log(`[TOOL RESULT] 🔧 Tool execution result received: session=${sessionId}, taskId=${taskId}`);
|
|
224
|
+
log(`[TOOL RESULT] - text.length=${text.length}`);
|
|
225
|
+
log(`[TOOL RESULT] - hasMedia=${hasMedia}`);
|
|
226
|
+
log(`[TOOL RESULT] - isError=${payload.isError}`);
|
|
227
|
+
if (text.length > 0) {
|
|
228
|
+
log(`[TOOL RESULT] - text preview: "${text.slice(0, 200)}"`);
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
if (text.length > 0 || hasMedia) {
|
|
232
|
+
const resultText = text.length > 0 ? text : "工具执行完成";
|
|
233
|
+
await sendStatusUpdate({
|
|
234
|
+
config,
|
|
235
|
+
sessionId,
|
|
236
|
+
taskId,
|
|
237
|
+
messageId,
|
|
238
|
+
text: resultText,
|
|
239
|
+
state: "working",
|
|
240
|
+
});
|
|
241
|
+
log(`[TOOL RESULT] ✅ Sent tool result as status update`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
error(`[TOOL RESULT] ❌ Failed to send tool result status:`, err);
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
// 🧠 Reasoning/thinking process streaming callback
|
|
249
|
+
onReasoningStream: async (payload) => {
|
|
250
|
+
const text = payload.text ?? "";
|
|
251
|
+
log(`[REASONING STREAM] 🧠 Reasoning/thinking chunk received: session=${sessionId}, taskId=${taskId}`);
|
|
252
|
+
log(`[REASONING STREAM] - text.length=${text.length}`);
|
|
253
|
+
if (text.length > 0) {
|
|
254
|
+
log(`[REASONING STREAM] - text preview: "${text.slice(0, 200)}"`);
|
|
255
|
+
}
|
|
256
|
+
// try {
|
|
257
|
+
// if (text.length > 0) {
|
|
258
|
+
// await sendReasoningTextUpdate({
|
|
259
|
+
// config,
|
|
260
|
+
// sessionId,
|
|
261
|
+
// taskId,
|
|
262
|
+
// messageId,
|
|
263
|
+
// text,
|
|
264
|
+
// });
|
|
265
|
+
// log(`[REASONING STREAM] ✅ Sent reasoning chunk as reasoningText update`);
|
|
266
|
+
// }
|
|
267
|
+
// } catch (err) {
|
|
268
|
+
// error(`[REASONING STREAM] ❌ Failed to send reasoning chunk reasoningText:`, err);
|
|
269
|
+
// }
|
|
270
|
+
},
|
|
271
|
+
// 📝 Partial reply streaming callback (real-time preview)
|
|
272
|
+
onPartialReply: async (payload) => {
|
|
273
|
+
const text = payload.text ?? "";
|
|
274
|
+
const hasMedia = Boolean(payload.mediaUrl || (payload.mediaUrls?.length ?? 0) > 0);
|
|
275
|
+
log(`[PARTIAL REPLY] 📝 Partial reply chunk received: session=${sessionId}, taskId=${taskId}`);
|
|
276
|
+
log(`[PARTIAL REPLY] - text.length=${text.length}`);
|
|
277
|
+
log(`[PARTIAL REPLY] - hasMedia=${hasMedia}`);
|
|
278
|
+
if (text.length > 0) {
|
|
279
|
+
log(`[PARTIAL REPLY] - text preview: "${text.slice(0, 200)}"`);
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
if (text.length > 0) {
|
|
283
|
+
await sendReasoningTextUpdate({
|
|
284
|
+
config,
|
|
285
|
+
sessionId,
|
|
286
|
+
taskId,
|
|
287
|
+
messageId,
|
|
288
|
+
text,
|
|
289
|
+
append: false,
|
|
290
|
+
});
|
|
291
|
+
log(`[PARTIAL REPLY] ✅ Sent partial reply as reasoningText update (append=false)`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
catch (err) {
|
|
295
|
+
error(`[PARTIAL REPLY] ❌ Failed to send partial reply reasoningText:`, err);
|
|
296
|
+
}
|
|
297
|
+
},
|
|
155
298
|
},
|
|
156
299
|
markDispatchIdle,
|
|
157
300
|
startStatusInterval, // Expose this to be called immediately
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XY search calendar event tool - searches calendar events on user's device.
|
|
3
|
+
* Returns matching events based on time range and optional title filter.
|
|
4
|
+
*
|
|
5
|
+
* Time range guidelines:
|
|
6
|
+
* - For a specific day: use 00:00:00 to 23:59:59 of that day
|
|
7
|
+
* - For morning: 06:00:00 to 12:00:00
|
|
8
|
+
* - For afternoon: 12:00:00 to 18:00:00
|
|
9
|
+
* - For evening: 18:00:00 to 24:00:00
|
|
10
|
+
* - For a specific time: use ±1 hour range (e.g., for 3PM, use 14:00:00 to 16:00:00)
|
|
11
|
+
*/
|
|
12
|
+
export declare const searchCalendarTool: any;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { getXYWebSocketManager } from "../client.js";
|
|
2
|
+
import { sendCommand } from "../formatter.js";
|
|
3
|
+
import { getLatestSessionContext } from "./session-manager.js";
|
|
4
|
+
import { logger } from "../utils/logger.js";
|
|
5
|
+
/**
|
|
6
|
+
* XY search calendar event tool - searches calendar events on user's device.
|
|
7
|
+
* Returns matching events based on time range and optional title filter.
|
|
8
|
+
*
|
|
9
|
+
* Time range guidelines:
|
|
10
|
+
* - For a specific day: use 00:00:00 to 23:59:59 of that day
|
|
11
|
+
* - For morning: 06:00:00 to 12:00:00
|
|
12
|
+
* - For afternoon: 12:00:00 to 18:00:00
|
|
13
|
+
* - For evening: 18:00:00 to 24:00:00
|
|
14
|
+
* - For a specific time: use ±1 hour range (e.g., for 3PM, use 14:00:00 to 16:00:00)
|
|
15
|
+
*/
|
|
16
|
+
export const searchCalendarTool = {
|
|
17
|
+
name: "search_calendar_event",
|
|
18
|
+
label: "Search Calendar Event",
|
|
19
|
+
description: `检索用户日历中的日程安排。根据时间范围和可选的日程标题进行检索。时间格式必须为:YYYYMMDD hhmmss(例如:20240115 143000)。
|
|
20
|
+
|
|
21
|
+
时间范围说明:
|
|
22
|
+
- 查询某一天的日程:使用该天的 00:00:00 到 23:59:59(例如:20240115 000000 到 20240115 235959)
|
|
23
|
+
- 查询上午的日程:使用 06:00:00 到 12:00:00
|
|
24
|
+
- 查询下午的日程:使用 12:00:00 到 18:00:00
|
|
25
|
+
- 查询晚上的日程:使用 18:00:00 到 23:59:59
|
|
26
|
+
- 查询某个时刻附近的日程:使用该时刻前后1小时的区间(例如:查询3点左右的日程,使用 14:00:00 到 16:00:00)
|
|
27
|
+
|
|
28
|
+
注意:该工具执行时间较长(最多60秒),请勿重复调用,超时或失败时最多重试一次。`,
|
|
29
|
+
parameters: {
|
|
30
|
+
type: "object",
|
|
31
|
+
properties: {
|
|
32
|
+
startTime: {
|
|
33
|
+
type: "string",
|
|
34
|
+
description: "日程起始时间,格式必须为:YYYYMMDD hhmmss(例如:20240115 143000 表示 2024年1月15日 14:30:00)",
|
|
35
|
+
},
|
|
36
|
+
endTime: {
|
|
37
|
+
type: "string",
|
|
38
|
+
description: "日程结束时间,格式必须为:YYYYMMDD hhmmss(例如:20240115 173000 表示 2024年1月15日 17:30:00)",
|
|
39
|
+
},
|
|
40
|
+
title: {
|
|
41
|
+
type: "string",
|
|
42
|
+
description: "日程标题/类型(可选),用于过滤特定类型的日程",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
required: ["startTime", "endTime"],
|
|
46
|
+
},
|
|
47
|
+
async execute(toolCallId, params) {
|
|
48
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] 🚀 Starting execution`);
|
|
49
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - toolCallId: ${toolCallId}`);
|
|
50
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - params:`, JSON.stringify(params));
|
|
51
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - timestamp: ${new Date().toISOString()}`);
|
|
52
|
+
// Validate parameters
|
|
53
|
+
if (!params.startTime || !params.endTime) {
|
|
54
|
+
logger.error(`[SEARCH_CALENDAR_TOOL] ❌ Missing required parameters`);
|
|
55
|
+
throw new Error("Missing required parameters: startTime and endTime are required");
|
|
56
|
+
}
|
|
57
|
+
// Convert time strings to millisecond timestamps
|
|
58
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] 🕒 Converting time strings to timestamps...`);
|
|
59
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - startTime input: ${params.startTime}`);
|
|
60
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - endTime input: ${params.endTime}`);
|
|
61
|
+
// Parse YYYYMMDD hhmmss format
|
|
62
|
+
const parseTimeString = (timeStr) => {
|
|
63
|
+
// Remove any extra spaces and split
|
|
64
|
+
const cleaned = timeStr.trim().replace(/\s+/g, ' ');
|
|
65
|
+
const parts = cleaned.split(' ');
|
|
66
|
+
if (parts.length !== 2) {
|
|
67
|
+
throw new Error(`Invalid time format: ${timeStr}. Expected format: YYYYMMDD hhmmss`);
|
|
68
|
+
}
|
|
69
|
+
const datePart = parts[0]; // YYYYMMDD
|
|
70
|
+
const timePart = parts[1]; // hhmmss
|
|
71
|
+
if (datePart.length !== 8 || timePart.length !== 6) {
|
|
72
|
+
throw new Error(`Invalid time format: ${timeStr}. Expected format: YYYYMMDD hhmmss`);
|
|
73
|
+
}
|
|
74
|
+
const year = parseInt(datePart.substring(0, 4), 10);
|
|
75
|
+
const month = parseInt(datePart.substring(4, 6), 10) - 1; // Month is 0-indexed
|
|
76
|
+
const day = parseInt(datePart.substring(6, 8), 10);
|
|
77
|
+
const hours = parseInt(timePart.substring(0, 2), 10);
|
|
78
|
+
const minutes = parseInt(timePart.substring(2, 4), 10);
|
|
79
|
+
const seconds = parseInt(timePart.substring(4, 6), 10);
|
|
80
|
+
const date = new Date(year, month, day, hours, minutes, seconds);
|
|
81
|
+
return date.getTime();
|
|
82
|
+
};
|
|
83
|
+
let startTimeMs;
|
|
84
|
+
let endTimeMs;
|
|
85
|
+
try {
|
|
86
|
+
startTimeMs = parseTimeString(params.startTime);
|
|
87
|
+
endTimeMs = parseTimeString(params.endTime);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
logger.error(`[SEARCH_CALENDAR_TOOL] ❌ Time parsing error:`, error);
|
|
91
|
+
throw new Error(`Invalid time format. Required format: YYYYMMDD hhmmss (e.g., 20240115 143000). Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
92
|
+
}
|
|
93
|
+
if (isNaN(startTimeMs) || isNaN(endTimeMs)) {
|
|
94
|
+
logger.error(`[SEARCH_CALENDAR_TOOL] ❌ Invalid time format`);
|
|
95
|
+
throw new Error("Invalid time format. Required format: YYYYMMDD hhmmss (e.g., 20240115 143000)");
|
|
96
|
+
}
|
|
97
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] ✅ Time conversion successful`);
|
|
98
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - startTime timestamp: ${startTimeMs}`);
|
|
99
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - endTime timestamp: ${endTimeMs}`);
|
|
100
|
+
// Get session context
|
|
101
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] 🔍 Attempting to get session context...`);
|
|
102
|
+
const sessionContext = getLatestSessionContext();
|
|
103
|
+
if (!sessionContext) {
|
|
104
|
+
logger.error(`[SEARCH_CALENDAR_TOOL] ❌ FAILED: No active session found!`);
|
|
105
|
+
logger.error(`[SEARCH_CALENDAR_TOOL] - toolCallId: ${toolCallId}`);
|
|
106
|
+
throw new Error("No active XY session found. Search calendar tool can only be used during an active conversation.");
|
|
107
|
+
}
|
|
108
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] ✅ Session context found`);
|
|
109
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - sessionId: ${sessionContext.sessionId}`);
|
|
110
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - taskId: ${sessionContext.taskId}`);
|
|
111
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - messageId: ${sessionContext.messageId}`);
|
|
112
|
+
const { config, sessionId, taskId, messageId } = sessionContext;
|
|
113
|
+
// Get WebSocket manager
|
|
114
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] 🔌 Getting WebSocket manager...`);
|
|
115
|
+
const wsManager = getXYWebSocketManager(config);
|
|
116
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] ✅ WebSocket manager obtained`);
|
|
117
|
+
// Build SearchCalendarEvent command
|
|
118
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] 📦 Building SearchCalendarEvent command...`);
|
|
119
|
+
// Build intentParam with timeInterval and optional title
|
|
120
|
+
const intentParam = {
|
|
121
|
+
timeInterval: [startTimeMs, endTimeMs],
|
|
122
|
+
};
|
|
123
|
+
if (params.title) {
|
|
124
|
+
intentParam.title = params.title;
|
|
125
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - Including title filter: ${params.title}`);
|
|
126
|
+
}
|
|
127
|
+
const command = {
|
|
128
|
+
header: {
|
|
129
|
+
namespace: "Common",
|
|
130
|
+
name: "Action",
|
|
131
|
+
},
|
|
132
|
+
payload: {
|
|
133
|
+
cardParam: {},
|
|
134
|
+
executeParam: {
|
|
135
|
+
executeMode: "background",
|
|
136
|
+
intentName: "SearchCalendarEvent",
|
|
137
|
+
bundleName: "com.huawei.hmos.calendardata",
|
|
138
|
+
dimension: "",
|
|
139
|
+
needUnlock: true,
|
|
140
|
+
actionResponse: true,
|
|
141
|
+
appType: "OHOS_APP",
|
|
142
|
+
timeOut: 5,
|
|
143
|
+
intentParam,
|
|
144
|
+
permissionId: [],
|
|
145
|
+
achieveType: "INTENT",
|
|
146
|
+
},
|
|
147
|
+
responses: [
|
|
148
|
+
{
|
|
149
|
+
resultCode: "",
|
|
150
|
+
displayText: "",
|
|
151
|
+
ttsText: "",
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
needUploadResult: true,
|
|
155
|
+
noHalfPage: false,
|
|
156
|
+
pageControlRelated: false,
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
// Send command and wait for response (60 second timeout)
|
|
160
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] ⏳ Setting up promise to wait for calendar search response...`);
|
|
161
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - Timeout: 60 seconds`);
|
|
162
|
+
return new Promise((resolve, reject) => {
|
|
163
|
+
const timeout = setTimeout(() => {
|
|
164
|
+
logger.error(`[SEARCH_CALENDAR_TOOL] ⏰ Timeout: No response received within 60 seconds`);
|
|
165
|
+
wsManager.off("data-event", handler);
|
|
166
|
+
reject(new Error("检索日程超时(60秒)"));
|
|
167
|
+
}, 60000);
|
|
168
|
+
// Listen for data events from WebSocket
|
|
169
|
+
const handler = (event) => {
|
|
170
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] 📨 Received data event:`, JSON.stringify(event));
|
|
171
|
+
if (event.intentName === "SearchCalendarEvent") {
|
|
172
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] 🎯 SearchCalendarEvent event received`);
|
|
173
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - status: ${event.status}`);
|
|
174
|
+
clearTimeout(timeout);
|
|
175
|
+
wsManager.off("data-event", handler);
|
|
176
|
+
if (event.status === "success" && event.outputs) {
|
|
177
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] ✅ Calendar events retrieved successfully`);
|
|
178
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - outputs:`, JSON.stringify(event.outputs));
|
|
179
|
+
// Return the result directly as requested
|
|
180
|
+
const result = event.outputs.result;
|
|
181
|
+
resolve({
|
|
182
|
+
content: [
|
|
183
|
+
{
|
|
184
|
+
type: "text",
|
|
185
|
+
text: JSON.stringify(result),
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
logger.error(`[SEARCH_CALENDAR_TOOL] ❌ Calendar event search failed`);
|
|
192
|
+
logger.error(`[SEARCH_CALENDAR_TOOL] - status: ${event.status}`);
|
|
193
|
+
reject(new Error(`检索日程失败: ${event.status}`));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
// Register event handler
|
|
198
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] 📡 Registering data-event handler on WebSocket manager`);
|
|
199
|
+
wsManager.on("data-event", handler);
|
|
200
|
+
// Send the command
|
|
201
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] 📤 Sending SearchCalendarEvent command...`);
|
|
202
|
+
sendCommand({
|
|
203
|
+
config,
|
|
204
|
+
sessionId,
|
|
205
|
+
taskId,
|
|
206
|
+
messageId,
|
|
207
|
+
command,
|
|
208
|
+
})
|
|
209
|
+
.then(() => {
|
|
210
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] ✅ Command sent successfully, waiting for response...`);
|
|
211
|
+
})
|
|
212
|
+
.catch((error) => {
|
|
213
|
+
logger.error(`[SEARCH_CALENDAR_TOOL] ❌ Failed to send command:`, error);
|
|
214
|
+
clearTimeout(timeout);
|
|
215
|
+
wsManager.off("data-event", handler);
|
|
216
|
+
reject(error);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
},
|
|
220
|
+
};
|
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;
|