@ynhcj/xiaoyi-channel 0.0.55-next → 0.0.56-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.
@@ -7,7 +7,7 @@ import { sendFileToUserTool } from "./tools/send-file-to-user-tool.js";
7
7
  import { viewPushResultTool } from "./tools/view-push-result-tool.js";
8
8
  import { imageReadingTool } from "./tools/image-reading-tool.js";
9
9
  import { timestampToUtc8Tool } from "./tools/timestamp-to-utc8-tool.js";
10
- import { searchEmailTool } from "./tools/search-email-tool.js";
10
+ import { getEmailToolSchemaTool } from "./tools/get-email-tool-schema.js";
11
11
  import { callDeviceTool } from "./tools/call-device-tool.js";
12
12
  import { getNoteToolSchemaTool } from "./tools/get-note-tool-schema.js";
13
13
  import { getCalendarToolSchemaTool } from "./tools/get-calendar-tool-schema.js";
@@ -16,6 +16,9 @@ import { getPhotoToolSchemaTool } from "./tools/get-photo-tool-schema.js";
16
16
  import { getDeviceFileToolSchemaTool } from "./tools/get-device-file-tool-schema.js";
17
17
  import { getAlarmToolSchemaTool } from "./tools/get-alarm-tool-schema.js";
18
18
  import { getCollectionToolSchemaTool } from "./tools/get-collection-tool-schema.js";
19
+ import { queryAppMessageTool } from "./tools/query-app-message-tool.js";
20
+ import { queryMemoryDataTool } from "./tools/query-memory-data-tool.js";
21
+ import { queryTodoTaskTool } from "./tools/query-todo-task-tool.js";
19
22
  import { filterToolsByDevice } from "./tools/device-tool-map.js";
20
23
  import { getCurrentSessionContext } from "./tools/session-manager.js";
21
24
  import { logger } from "./utils/logger.js";
@@ -58,7 +61,7 @@ export const xyPlugin = {
58
61
  },
59
62
  outbound: xyOutbound,
60
63
  agentTools: () => {
61
- const allTools = [locationTool, callDeviceTool, getNoteToolSchemaTool, getCalendarToolSchemaTool, getContactToolSchemaTool, getPhotoToolSchemaTool, xiaoyiGuiTool, getDeviceFileToolSchemaTool, getAlarmToolSchemaTool, getCollectionToolSchemaTool, sendFileToUserTool, viewPushResultTool, imageReadingTool, timestampToUtc8Tool, searchEmailTool];
64
+ const allTools = [locationTool, callDeviceTool, getNoteToolSchemaTool, getCalendarToolSchemaTool, getContactToolSchemaTool, getPhotoToolSchemaTool, xiaoyiGuiTool, getDeviceFileToolSchemaTool, getAlarmToolSchemaTool, getCollectionToolSchemaTool, sendFileToUserTool, viewPushResultTool, imageReadingTool, timestampToUtc8Tool, getEmailToolSchemaTool, queryAppMessageTool, queryMemoryDataTool, queryTodoTaskTool];
62
65
  const ctx = getCurrentSessionContext();
63
66
  const filtered = filterToolsByDevice(allTools, ctx?.deviceType);
64
67
  logger.log(`[DEVICE-FILTER] deviceType=${ctx?.deviceType ?? "(none)"}, tools: ${allTools.length} → ${filtered.length} (${filtered.map(t => t.name).join(", ")})`);
@@ -8,7 +8,34 @@
8
8
  // models.providers.xiaoyiprovider.api = "openai-completions"
9
9
  // models.providers.xiaoyiprovider.models = [...]
10
10
  import { createHash } from "crypto";
11
+ import { createAssistantMessageEventStream } from "@mariozechner/pi-ai";
11
12
  import { getCurrentSessionContext } from "./tools/session-manager.js";
13
+ // ── Retry config ──────────────────────────────────────────────
14
+ const RETRY_DELAYS_MS = [10_000, 20_000, 40_000, 60_000];
15
+ const MAX_RETRY_ATTEMPTS = 6;
16
+ /** Check if an errorMessage is a retryable provider error. */
17
+ function isRetryableProviderError(message) {
18
+ if (!message)
19
+ return false;
20
+ const lower = message.toLowerCase();
21
+ // server_error: "The server had an error while processing your request"
22
+ if (lower.includes("server_error") || lower.includes("server had an error"))
23
+ return true;
24
+ // rate_limit_error: "Rate limit reached for requests"
25
+ if (lower.includes("rate_limit") || lower.includes("rate limit reached"))
26
+ return true;
27
+ return false;
28
+ }
29
+ /** Compute retry delay in ms for the given 1-based attempt. */
30
+ function getRetryDelayMs(attempt) {
31
+ // attempt 1→10s, 2→20s, 3→40s, 4+→60s
32
+ if (attempt <= RETRY_DELAYS_MS.length)
33
+ return RETRY_DELAYS_MS[attempt - 1];
34
+ return RETRY_DELAYS_MS[RETRY_DELAYS_MS.length - 1]; // 60s cap
35
+ }
36
+ function sleep(ms) {
37
+ return new Promise((resolve) => setTimeout(resolve, ms));
38
+ }
12
39
  /**
13
40
  * Dynamic header keys injected via extraParams and forwarded to the HTTP request.
14
41
  * Correspond to the three fields written to .xiaoyiruntime:
@@ -73,10 +100,12 @@ export const xiaoyiProvider = {
73
100
  },
74
101
  /**
75
102
  * Wrap the stream function to inject dynamic headers into every
76
- * HTTP request to the model provider.
103
+ * HTTP request to the model provider, and retry on retryable errors
104
+ * (server_error / rate_limit_error) with backoff: 10s, 20s, 40s, 60s (cap).
77
105
  *
78
- * Reads the values injected by prepareExtraParams and adds them
79
- * as HTTP headers on the outgoing request.
106
+ * The retry loop awaits stream.result() to detect errors before deciding
107
+ * whether to retry. This keeps the agent loop waiting (no timeout risk
108
+ * since the default agent timeout is 48 hours).
80
109
  */
81
110
  wrapStreamFn: (ctx) => {
82
111
  const underlying = ctx.streamFn;
@@ -135,21 +164,63 @@ export const xiaoyiProvider = {
135
164
  if (sessionCtx?.deviceType) {
136
165
  const rawDevice = sessionCtx.deviceType;
137
166
  const displayDevice = (rawDevice === "2in1") ? "鸿蒙PC" : rawDevice;
138
- const deviceSection = `\n\n## Current User Device Context\nThe current user is using the following device: ${displayDevice}\nYou need to be aware of the users current device and provide guidance accordingly. If the response involves device-related tools or actions, you must tailor the reply based on the users current device, using device-specific references such as saved to the Notes/Calendar on your {deviceType}.\n”`;
167
+ const deviceSection = `\n\n## Current User Device Context\nThe current user is using the following device: ${displayDevice}\nYou need to be aware of the user's current device and provide guidance accordingly. If the response involves device-related tools or actions, you must tailor the reply based on the user's current device, using device-specific references such as "saved to the Notes/Calendar on your {deviceType}.\n"`;
139
168
  context.systemPrompt = (context.systemPrompt ?? "") + deviceSection;
140
169
  }
141
- const stream = await underlying(model, context, {
170
+ // ── Retry loop ─────────────────────────────────────────
171
+ for (let attempt = 0; attempt < MAX_RETRY_ATTEMPTS; attempt++) {
172
+ const stream = await underlying(model, context, {
173
+ ...options,
174
+ headers: {
175
+ ...options?.headers,
176
+ ...dynamicHeaders,
177
+ },
178
+ });
179
+ // Wait for the stream to settle (done or error) to inspect the result.
180
+ // stream.result() resolves to the final AssistantMessage (even on error).
181
+ const result = await stream.result();
182
+ // Check if this is a retryable error
183
+ if (result.stopReason === "error" && isRetryableProviderError(result.errorMessage)) {
184
+ const delayMs = getRetryDelayMs(attempt);
185
+ console.log(`[xiaoyiprovider] retryable error (attempt ${attempt + 1}/${MAX_RETRY_ATTEMPTS}): ` +
186
+ `${result.errorMessage} — retrying in ${delayMs}ms`);
187
+ await sleep(delayMs);
188
+ continue;
189
+ }
190
+ // Success or non-retryable error — log and return
191
+ if (result.stopReason === "error") {
192
+ console.log(`[xiaoyiprovider] non-retryable error: ${result.errorMessage}`);
193
+ }
194
+ else {
195
+ console.log(`[xiaoyiprovider] stream completed, usage: input=${result.usage?.input} output=${result.usage?.output}`);
196
+ }
197
+ // The original stream has already been consumed by result().
198
+ // We need to return a new stream that replays the result.
199
+ const replayStream = createAssistantMessageEventStream();
200
+ // Re-emit events from the result as a single done/error event
201
+ if (result.stopReason === "error") {
202
+ replayStream.push({ type: "error", reason: "error", error: result });
203
+ }
204
+ else {
205
+ replayStream.push({ type: "done", reason: result.stopReason, message: result });
206
+ }
207
+ replayStream.end();
208
+ return replayStream;
209
+ }
210
+ // All retries exhausted — return the last attempt's real error via a new stream
211
+ console.log(`[xiaoyiprovider] all ${MAX_RETRY_ATTEMPTS} retries exhausted, surfacing last error`);
212
+ const lastStream = await underlying(model, context, {
142
213
  ...options,
143
214
  headers: {
144
215
  ...options?.headers,
145
216
  ...dynamicHeaders,
146
217
  },
147
218
  });
148
- // 异步监听输出(不阻塞 stream 返回)
149
- stream.result().then((result) => {
150
- console.log(`[xiaoyiprovider] stream completed, usage: input=${result.usage?.input} output=${result.usage?.output}`);
151
- }, (err) => console.log(`[xiaoyiprovider] stream error: ${JSON.stringify(err)}`));
152
- return stream;
219
+ const lastResult = await lastStream.result();
220
+ const exhaustedStream = createAssistantMessageEventStream();
221
+ exhaustedStream.push({ type: "error", reason: "error", error: lastResult });
222
+ exhaustedStream.end();
223
+ return exhaustedStream;
153
224
  };
154
225
  },
155
226
  };
@@ -268,8 +268,9 @@ export function createXYReplyDispatcher(params) {
268
268
  if (phase === "start") {
269
269
  const toolName = name || "unknown";
270
270
  // call_device_tool 由自身 execute() 内部发送具体子工具名的状态更新
271
- if (toolName === "call_device_tool") {
272
- log(`[TOOL START] Skipping generic status for call_device_tool, will be handled by execute()`);
271
+ // get_xxx_tool_schema 是给 LLM 查 schema 用的,无需向用户展示
272
+ if (toolName === "call_device_tool" || toolName.endsWith("_tool_schema")) {
273
+ log(`[TOOL START] Skipping generic status for ${toolName}`);
273
274
  return;
274
275
  }
275
276
  try {
@@ -20,6 +20,8 @@ import { saveMediaToGalleryTool } from "./save-media-to-gallery-tool.js";
20
20
  import { searchFileTool } from "./search-file-tool.js";
21
21
  import { uploadFileTool } from "./upload-file-tool.js";
22
22
  import { saveFileToPhoneTool } from "./save-file-to-phone-tool.js";
23
+ import { sendEmailTool } from "./send-email-tool.js";
24
+ import { searchEmailTool } from "./search-email-tool.js";
23
25
  import { sendStatusUpdate } from "../formatter.js";
24
26
  import { getCurrentSessionContext } from "./session-manager.js";
25
27
  import { getCurrentTaskId, getCurrentMessageId } from "../task-manager.js";
@@ -49,6 +51,8 @@ const deviceToolRegistry = new Map([
49
51
  [searchFileTool.name, searchFileTool],
50
52
  [uploadFileTool.name, uploadFileTool],
51
53
  [saveFileToPhoneTool.name, saveFileToPhoneTool],
54
+ [sendEmailTool.name, sendEmailTool],
55
+ [searchEmailTool.name, searchEmailTool],
52
56
  ]);
53
57
  /**
54
58
  * call_device_tool - 通用端工具调度器。
@@ -0,0 +1,16 @@
1
+ export declare const getEmailToolSchemaTool: {
2
+ name: string;
3
+ label: string;
4
+ description: string;
5
+ parameters: {
6
+ type: "object";
7
+ properties: {};
8
+ required: string[];
9
+ };
10
+ execute(_toolCallId: string, _params: any): Promise<{
11
+ content: {
12
+ type: "text";
13
+ text: string;
14
+ }[];
15
+ }>;
16
+ };
@@ -0,0 +1,9 @@
1
+ import { createSchemaTool } from "./schema-tool-factory.js";
2
+ import { sendEmailTool } from "./send-email-tool.js";
3
+ import { searchEmailTool } from "./search-email-tool.js";
4
+ export const getEmailToolSchemaTool = createSchemaTool({
5
+ name: "get_email_tool_schema",
6
+ label: "Get Email Tool Schema",
7
+ description: "获取可在用户设备上发送邮件、检索邮件的相关端工具列表。",
8
+ tools: [sendEmailTool, searchEmailTool],
9
+ });
@@ -0,0 +1,4 @@
1
+ /**
2
+ * 查询指定时间范围内的设备通知消息。
3
+ */
4
+ export declare const queryAppMessageTool: any;
@@ -0,0 +1,138 @@
1
+ // QueryAppMessage tool implementation
2
+ import { getXYWebSocketManager } from "../client.js";
3
+ import { sendCommand } from "../formatter.js";
4
+ import { getCurrentSessionContext } from "./session-manager.js";
5
+ class ToolInputError extends Error {
6
+ status = 400;
7
+ constructor(message) {
8
+ super(message);
9
+ this.name = "ToolInputError";
10
+ }
11
+ }
12
+ /**
13
+ * 查询指定时间范围内的设备通知消息。
14
+ */
15
+ export const queryAppMessageTool = {
16
+ name: "query_app_message",
17
+ label: "Query App Message",
18
+ description: `获取指定时间范围内的设备通知消息。适用于需要查询历史通知、按应用筛选通知、或仅查看未读通知的场景。支持按时间范围、应用包名、已读/未读状态进行过滤。
19
+ 注意:
20
+ a. 操作超时时间为60秒,请勿重复调用此工具
21
+ b. 如果遇到各类调用失败场景,最多只能重试一次,不可以重复调用多次。
22
+ c. 调用工具前需认真检查调用参数是否满足工具要求
23
+
24
+ 回复约束:如果工具返回没有授权或者其他报错,只需要完整描述没有授权或者其他报错内容即可,不需要主动给用户提供解决方案,例如告诉用户如何授权,如何解决报错等都是不需要的,请严格遵守。`,
25
+ parameters: {
26
+ type: "object",
27
+ properties: {
28
+ startTime: {
29
+ type: "string",
30
+ description: "查询通知的起始时间(ISO 8601 字符串)。若endTime为空,则默认值为24小时前。",
31
+ },
32
+ endTime: {
33
+ type: "string",
34
+ description: "查询通知的结束时间(ISO 8601 字符串)。默认值为当前时间。",
35
+ },
36
+ packageName: {
37
+ type: "string",
38
+ description: "按应用名称过滤通知(例如「微信」「小红书」)。默认值为所有应用。",
39
+ },
40
+ state: {
41
+ type: "integer",
42
+ description: "通知的已读/未读状态。0 = 全部,1 = 仅未读。默认值为 0。",
43
+ },
44
+ },
45
+ required: [],
46
+ },
47
+ async execute(_toolCallId, params) {
48
+ const sessionContext = getCurrentSessionContext();
49
+ if (!sessionContext) {
50
+ throw new Error("No active XY session found.");
51
+ }
52
+ const { config, sessionId, taskId, messageId } = sessionContext;
53
+ const wsManager = getXYWebSocketManager(config);
54
+ const intentParam = {};
55
+ if (params.startTime !== undefined)
56
+ intentParam.startTime = params.startTime;
57
+ if (params.endTime !== undefined)
58
+ intentParam.endTime = params.endTime;
59
+ if (params.packageName !== undefined)
60
+ intentParam.packageName = params.packageName;
61
+ if (params.state !== undefined) {
62
+ if (params.state !== 0 && params.state !== 1) {
63
+ throw new ToolInputError("state 参数只能为 0(全部)或 1(仅未读)");
64
+ }
65
+ intentParam.state = params.state;
66
+ }
67
+ const command = {
68
+ header: {
69
+ namespace: "Common",
70
+ name: "Action",
71
+ },
72
+ payload: {
73
+ cardParam: {},
74
+ executeParam: {
75
+ executeMode: "background",
76
+ intentName: "QueryAppMessage",
77
+ bundleName: "com.huawei.hmos.vassistant",
78
+ needUnlock: true,
79
+ actionResponse: true,
80
+ appType: "OHOS_APP",
81
+ timeOut: 5,
82
+ intentParam,
83
+ permissionId: [],
84
+ achieveType: "INTENT",
85
+ },
86
+ responses: [
87
+ {
88
+ resultCode: "",
89
+ displayText: "",
90
+ ttsText: "",
91
+ },
92
+ ],
93
+ needUploadResult: true,
94
+ noHalfPage: false,
95
+ pageControlRelated: false,
96
+ },
97
+ };
98
+ return new Promise((resolve, reject) => {
99
+ const timeout = setTimeout(() => {
100
+ wsManager.off("data-event", handler);
101
+ reject(new Error("查询通知消息超时(60秒)"));
102
+ }, 60000);
103
+ const handler = (event) => {
104
+ if (event.intentName === "QueryAppMessage") {
105
+ clearTimeout(timeout);
106
+ wsManager.off("data-event", handler);
107
+ if (event.status === "success" && event.outputs) {
108
+ resolve({
109
+ content: [
110
+ {
111
+ type: "text",
112
+ text: JSON.stringify(event.outputs),
113
+ },
114
+ ],
115
+ });
116
+ }
117
+ else {
118
+ reject(new Error(`查询通知消息失败: ${event.status}`));
119
+ }
120
+ }
121
+ };
122
+ wsManager.on("data-event", handler);
123
+ sendCommand({
124
+ config,
125
+ sessionId,
126
+ taskId,
127
+ messageId,
128
+ command,
129
+ })
130
+ .then(() => { })
131
+ .catch((error) => {
132
+ clearTimeout(timeout);
133
+ wsManager.off("data-event", handler);
134
+ reject(error);
135
+ });
136
+ });
137
+ },
138
+ };
@@ -0,0 +1,4 @@
1
+ /**
2
+ * 查询存储在设备本地的结构化记忆数据。
3
+ */
4
+ export declare const queryMemoryDataTool: any;
@@ -0,0 +1,154 @@
1
+ // QueryMemoryData tool implementation
2
+ import { getXYWebSocketManager } from "../client.js";
3
+ import { sendCommand } from "../formatter.js";
4
+ import { getCurrentSessionContext } from "./session-manager.js";
5
+ class ToolInputError extends Error {
6
+ status = 400;
7
+ constructor(message) {
8
+ super(message);
9
+ this.name = "ToolInputError";
10
+ }
11
+ }
12
+ const VALID_CATEGORIES = ["ImportantDay", "Address", "Card", "ServiceOrder", "Event"];
13
+ const VALID_SUB_CATEGORIES = {
14
+ ImportantDay: ["Birthday", "Anniversary", "RepaymentDate", "ExamDate", "SalaryDate", "BigDay"],
15
+ Card: [
16
+ "IDCard", "Passport", "DrivingLicense", "VehicleLicense", "EEPtoHKMO",
17
+ "EEPtoTW", "Invoice", "BusinessCard", "VehicleInspectionCertificate",
18
+ "SocialSecurityCard", "BankCard",
19
+ ],
20
+ ServiceOrder: ["FilmTicket", "HotelOrder", "TrainTicket", "AirTicket"],
21
+ Event: [
22
+ "Delicacy", "Work", "FamilyActivities", "Travel", "Training",
23
+ "Health", "Life", "Entertainment", "Calendar",
24
+ ],
25
+ };
26
+ /**
27
+ * 查询存储在设备本地的结构化记忆数据。
28
+ */
29
+ export const queryMemoryDataTool = {
30
+ name: "query_memory_data",
31
+ label: "Query Memory Data",
32
+ description: `查询存储在设备本地的结构化记忆数据。适用于获取特定类别的个人信息,如重要日子、证件卡证、服务订单或日程事件。支持按分类、子分类进行过滤。
33
+ 注意:
34
+ a. 操作超时时间为60秒,请勿重复调用此工具
35
+ b. 如果遇到各类调用失败场景,最多只能重试一次,不可以重复调用多次。
36
+ c. 调用工具前需认真检查调用参数是否满足工具要求
37
+
38
+ 回复约束:如果工具返回没有授权或者其他报错,只需要完整描述没有授权或者其他报错内容即可,不需要主动给用户提供解决方案,例如告诉用户如何授权,如何解决报错等都是不需要的,请严格遵守。`,
39
+ parameters: {
40
+ type: "object",
41
+ properties: {
42
+ category: {
43
+ type: "string",
44
+ description: '按数据大类进行过滤。可选值为 ""、"ImportantDay"、"Address"、"Card"、"ServiceOrder"、"Event"。默认值为 ""。',
45
+ },
46
+ subCategory: {
47
+ description: '在指定 category 下的子类别过滤,默认值为""。支持字符串或字符串数组。',
48
+ oneOf: [
49
+ { type: "string" },
50
+ { type: "array", items: { type: "string" } },
51
+ ],
52
+ },
53
+ },
54
+ required: [],
55
+ },
56
+ async execute(_toolCallId, params) {
57
+ const { category, subCategory } = params;
58
+ // Validate category
59
+ if (category && !VALID_CATEGORIES.includes(category)) {
60
+ throw new ToolInputError(`category 参数无效,可选值为:${VALID_CATEGORIES.join("、")}`);
61
+ }
62
+ // Validate subCategory when category is specified
63
+ if (category && subCategory && VALID_SUB_CATEGORIES[category]) {
64
+ const validValues = VALID_SUB_CATEGORIES[category];
65
+ const subValues = Array.isArray(subCategory) ? subCategory : [subCategory];
66
+ for (const sv of subValues) {
67
+ if (sv && !validValues.includes(sv)) {
68
+ throw new ToolInputError(`category 为 "${category}" 时,subCategory 可选值为:${validValues.join("、")}`);
69
+ }
70
+ }
71
+ }
72
+ const sessionContext = getCurrentSessionContext();
73
+ if (!sessionContext) {
74
+ throw new Error("No active XY session found.");
75
+ }
76
+ const { config, sessionId, taskId, messageId } = sessionContext;
77
+ const wsManager = getXYWebSocketManager(config);
78
+ const intentParam = {};
79
+ if (category)
80
+ intentParam.category = category;
81
+ if (subCategory !== undefined)
82
+ intentParam.subCategory = subCategory;
83
+ const command = {
84
+ header: {
85
+ namespace: "Common",
86
+ name: "Action",
87
+ },
88
+ payload: {
89
+ cardParam: {},
90
+ executeParam: {
91
+ executeMode: "background",
92
+ intentName: "QueryMemoryData",
93
+ bundleName: "com.huawei.hmos.vassistant",
94
+ needUnlock: true,
95
+ actionResponse: true,
96
+ appType: "OHOS_APP",
97
+ timeOut: 5,
98
+ intentParam,
99
+ permissionId: [],
100
+ achieveType: "INTENT",
101
+ },
102
+ responses: [
103
+ {
104
+ resultCode: "",
105
+ displayText: "",
106
+ ttsText: "",
107
+ },
108
+ ],
109
+ needUploadResult: true,
110
+ noHalfPage: false,
111
+ pageControlRelated: false,
112
+ },
113
+ };
114
+ return new Promise((resolve, reject) => {
115
+ const timeout = setTimeout(() => {
116
+ wsManager.off("data-event", handler);
117
+ reject(new Error("查询记忆数据超时(60秒)"));
118
+ }, 60000);
119
+ const handler = (event) => {
120
+ if (event.intentName === "QueryMemoryData") {
121
+ clearTimeout(timeout);
122
+ wsManager.off("data-event", handler);
123
+ if (event.status === "success" && event.outputs) {
124
+ resolve({
125
+ content: [
126
+ {
127
+ type: "text",
128
+ text: JSON.stringify(event.outputs),
129
+ },
130
+ ],
131
+ });
132
+ }
133
+ else {
134
+ reject(new Error(`查询记忆数据失败: ${event.status}`));
135
+ }
136
+ }
137
+ };
138
+ wsManager.on("data-event", handler);
139
+ sendCommand({
140
+ config,
141
+ sessionId,
142
+ taskId,
143
+ messageId,
144
+ command,
145
+ })
146
+ .then(() => { })
147
+ .catch((error) => {
148
+ clearTimeout(timeout);
149
+ wsManager.off("data-event", handler);
150
+ reject(error);
151
+ });
152
+ });
153
+ },
154
+ };
@@ -0,0 +1,4 @@
1
+ /**
2
+ * 获取指定时间范围内的全局待办任务列表。
3
+ */
4
+ export declare const queryTodoTaskTool: any;
@@ -0,0 +1,133 @@
1
+ // QueryTodoTask tool implementation
2
+ import { getXYWebSocketManager } from "../client.js";
3
+ import { sendCommand } from "../formatter.js";
4
+ import { getCurrentSessionContext } from "./session-manager.js";
5
+ class ToolInputError extends Error {
6
+ status = 400;
7
+ constructor(message) {
8
+ super(message);
9
+ this.name = "ToolInputError";
10
+ }
11
+ }
12
+ /**
13
+ * 获取指定时间范围内的全局待办任务列表。
14
+ */
15
+ export const queryTodoTaskTool = {
16
+ name: "query_todo_task",
17
+ label: "Query Todo Task",
18
+ description: `获取指定时间范围内的全局待办任务列表。适用于需要查询历史任务、按完成状态筛选、或仅查看待处理任务的场景。支持按时间范围、任务状态进行过滤。
19
+ 注意:
20
+ a. 操作超时时间为60秒,请勿重复调用此工具
21
+ b. 如果遇到各类调用失败场景,最多只能重试一次,不可以重复调用多次。
22
+ c. 调用工具前需认真检查调用参数是否满足工具要求
23
+ d. 当只传入 startTime 时,返回该时间点之后的所有任务;当只传入 endTime 时,返回该时间点之前的所有任务;两者都不传则返回所有时间段的任务。
24
+
25
+ 回复约束:如果工具返回没有授权或者其他报错,只需要完整描述没有授权或者其他报错内容即可,不需要主动给用户提供解决方案,例如告诉用户如何授权,如何解决报错等都是不需要的,请严格遵守。`,
26
+ parameters: {
27
+ type: "object",
28
+ properties: {
29
+ startTime: {
30
+ type: "string",
31
+ description: "查询创建时间大于此值的任务(ISO 8601 字符串,如 2024-01-01T00:00:00Z)。",
32
+ },
33
+ endTime: {
34
+ type: "string",
35
+ description: "查询创建时间小于此值的任务(ISO 8601 字符串,如 2024-01-31T23:59:59Z)。",
36
+ },
37
+ status: {
38
+ type: "string",
39
+ description: '任务完成状态过滤。可选值为 "all"、"completed"、"pending"。默认为 "all"。',
40
+ },
41
+ },
42
+ required: [],
43
+ },
44
+ async execute(_toolCallId, params) {
45
+ const { status } = params;
46
+ if (status && !["all", "completed", "pending"].includes(status)) {
47
+ throw new ToolInputError('status 参数只能为 "all"、"completed" 或 "pending"');
48
+ }
49
+ const sessionContext = getCurrentSessionContext();
50
+ if (!sessionContext) {
51
+ throw new Error("No active XY session found.");
52
+ }
53
+ const { config, sessionId, taskId, messageId } = sessionContext;
54
+ const wsManager = getXYWebSocketManager(config);
55
+ const intentParam = {};
56
+ if (params.startTime !== undefined)
57
+ intentParam.startTime = params.startTime;
58
+ if (params.endTime !== undefined)
59
+ intentParam.endTime = params.endTime;
60
+ if (status !== undefined)
61
+ intentParam.status = status;
62
+ const command = {
63
+ header: {
64
+ namespace: "Common",
65
+ name: "Action",
66
+ },
67
+ payload: {
68
+ cardParam: {},
69
+ executeParam: {
70
+ executeMode: "background",
71
+ intentName: "QueryTodoTask",
72
+ bundleName: "com.huawei.hmos.vassistant",
73
+ needUnlock: true,
74
+ actionResponse: true,
75
+ appType: "OHOS_APP",
76
+ timeOut: 5,
77
+ intentParam,
78
+ permissionId: [],
79
+ achieveType: "INTENT",
80
+ },
81
+ responses: [
82
+ {
83
+ resultCode: "",
84
+ displayText: "",
85
+ ttsText: "",
86
+ },
87
+ ],
88
+ needUploadResult: true,
89
+ noHalfPage: false,
90
+ pageControlRelated: false,
91
+ },
92
+ };
93
+ return new Promise((resolve, reject) => {
94
+ const timeout = setTimeout(() => {
95
+ wsManager.off("data-event", handler);
96
+ reject(new Error("查询待办任务超时(60秒)"));
97
+ }, 60000);
98
+ const handler = (event) => {
99
+ if (event.intentName === "QueryTodoTask") {
100
+ clearTimeout(timeout);
101
+ wsManager.off("data-event", handler);
102
+ if (event.status === "success" && event.outputs) {
103
+ resolve({
104
+ content: [
105
+ {
106
+ type: "text",
107
+ text: JSON.stringify(event.outputs),
108
+ },
109
+ ],
110
+ });
111
+ }
112
+ else {
113
+ reject(new Error(`查询待办任务失败: ${event.status}`));
114
+ }
115
+ }
116
+ };
117
+ wsManager.on("data-event", handler);
118
+ sendCommand({
119
+ config,
120
+ sessionId,
121
+ taskId,
122
+ messageId,
123
+ command,
124
+ })
125
+ .then(() => { })
126
+ .catch((error) => {
127
+ clearTimeout(timeout);
128
+ wsManager.off("data-event", handler);
129
+ reject(error);
130
+ });
131
+ });
132
+ },
133
+ };
@@ -0,0 +1,4 @@
1
+ /**
2
+ * XY send email tool - sends an email via 花瓣邮箱 on user's device.
3
+ */
4
+ export declare const sendEmailTool: any;
@@ -0,0 +1,134 @@
1
+ // Send Email tool implementation
2
+ import { getXYWebSocketManager } from "../client.js";
3
+ import { sendCommand } from "../formatter.js";
4
+ import { getCurrentSessionContext } from "./session-manager.js";
5
+ class ToolInputError extends Error {
6
+ status = 400;
7
+ constructor(message) {
8
+ super(message);
9
+ this.name = "ToolInputError";
10
+ }
11
+ }
12
+ /**
13
+ * XY send email tool - sends an email via 花瓣邮箱 on user's device.
14
+ */
15
+ export const sendEmailTool = {
16
+ name: "send_email",
17
+ label: "Send Email",
18
+ description: `在用户设备上通过花瓣邮箱发送邮件。
19
+ 注意:
20
+ a. 操作超时时间为60秒,请勿重复调用此工具
21
+ b. 如果遇到各类调用失败场景,最多只能重试一次,不可以重复调用多次。
22
+ c. 调用工具前需认真检查调用参数是否满足工具要求
23
+
24
+ 回复约束:如果工具返回没有授权或者其他报错,只需要完整描述没有授权或者其他报错内容即可,不需要主动给用户提供解决方案,例如告诉用户如何授权,如何解决报错等都是不需要的,请严格遵守。`,
25
+ parameters: {
26
+ type: "object",
27
+ properties: {
28
+ subject: {
29
+ type: "string",
30
+ description: "邮件主题,必填",
31
+ },
32
+ to: {
33
+ type: "string",
34
+ description: "收件人邮箱地址,必填",
35
+ },
36
+ body: {
37
+ type: "string",
38
+ description: "邮件内容,必填",
39
+ },
40
+ },
41
+ required: ["subject", "to", "body"],
42
+ },
43
+ async execute(_toolCallId, params) {
44
+ if (typeof params.subject !== "string" || !params.subject.trim()) {
45
+ throw new ToolInputError("缺少必填参数 subject(邮件主题)");
46
+ }
47
+ if (typeof params.to !== "string" || !params.to.trim()) {
48
+ throw new ToolInputError("缺少必填参数 to(收件人邮箱地址)");
49
+ }
50
+ if (typeof params.body !== "string" || !params.body.trim()) {
51
+ throw new ToolInputError("缺少必填参数 body(邮件内容)");
52
+ }
53
+ const sessionContext = getCurrentSessionContext();
54
+ if (!sessionContext) {
55
+ throw new Error("No active XY session found. Send email tool can only be used during an active conversation.");
56
+ }
57
+ const { config, sessionId, taskId, messageId } = sessionContext;
58
+ const wsManager = getXYWebSocketManager(config);
59
+ const command = {
60
+ header: {
61
+ namespace: "Common",
62
+ name: "Action",
63
+ },
64
+ payload: {
65
+ cardParam: {},
66
+ executeParam: {
67
+ executeMode: "background",
68
+ intentName: "SendEmail",
69
+ bundleName: "com.huawei.hmos.email",
70
+ needUnlock: true,
71
+ actionResponse: true,
72
+ appType: "OHOS_APP",
73
+ timeOut: 5,
74
+ intentParam: {
75
+ subject: params.subject.trim(),
76
+ to: [params.to.trim()],
77
+ body: params.body.trim(),
78
+ },
79
+ permissionId: [],
80
+ achieveType: "INTENT",
81
+ },
82
+ responses: [
83
+ {
84
+ resultCode: "",
85
+ displayText: "",
86
+ ttsText: "",
87
+ },
88
+ ],
89
+ needUploadResult: true,
90
+ noHalfPage: false,
91
+ pageControlRelated: false,
92
+ },
93
+ };
94
+ return new Promise((resolve, reject) => {
95
+ const timeout = setTimeout(() => {
96
+ wsManager.off("data-event", handler);
97
+ reject(new Error("发送邮件超时(60秒)"));
98
+ }, 60000);
99
+ const handler = (event) => {
100
+ if (event.intentName === "SendEmail") {
101
+ clearTimeout(timeout);
102
+ wsManager.off("data-event", handler);
103
+ if (event.status === "success" && event.outputs) {
104
+ resolve({
105
+ content: [
106
+ {
107
+ type: "text",
108
+ text: JSON.stringify(event.outputs),
109
+ },
110
+ ],
111
+ });
112
+ }
113
+ else {
114
+ reject(new Error(`发送邮件失败: ${event.status}`));
115
+ }
116
+ }
117
+ };
118
+ wsManager.on("data-event", handler);
119
+ sendCommand({
120
+ config,
121
+ sessionId,
122
+ taskId,
123
+ messageId,
124
+ command,
125
+ })
126
+ .then(() => { })
127
+ .catch((error) => {
128
+ clearTimeout(timeout);
129
+ wsManager.off("data-event", handler);
130
+ reject(error);
131
+ });
132
+ });
133
+ },
134
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynhcj/xiaoyi-channel",
3
- "version": "0.0.55-next",
3
+ "version": "0.0.56-next",
4
4
  "description": "OpenClaw Xiaoyi Channel plugin - Xiaoyi A2A protocol integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",