@ynhcj/xiaoyi-channel 0.0.39-beta → 0.0.41-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/bot.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { getXYRuntime } from "./runtime.js";
2
2
  import { createXYReplyDispatcher } from "./reply-dispatcher.js";
3
3
  import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId } from "./parser.js";
4
- import { downloadFilesFromParts } from "./file-download.js";
5
4
  import { resolveXYConfig } from "./config.js";
6
5
  import { sendStatusUpdate, sendClearContextResponse, sendTasksCancelResponse } from "./formatter.js";
7
6
  import { registerSession, unregisterSession, runWithSessionContext } from "./tools/session-manager.js";
8
7
  import { configManager } from "./utils/config-manager.js";
8
+ import { addPushId } from "./utils/pushid-manager.js";
9
9
  import { registerTaskId, decrementTaskIdRef, lockTaskId, unlockTaskId, hasActiveTask, } from "./task-manager.js";
10
10
  /**
11
11
  * Handle an incoming A2A message.
@@ -81,6 +81,10 @@ export async function handleXYMessage(params) {
81
81
  log(`[BOT] - Push ID preview: ${pushId.substring(0, 20)}...`);
82
82
  log(`[BOT] - Full push_id: ${pushId}`);
83
83
  configManager.updatePushId(parsed.sessionId, pushId);
84
+ // 持久化 pushId 到本地文件(异步,不阻塞主流程)
85
+ addPushId(pushId).catch((err) => {
86
+ error(`[BOT] Failed to persist pushId:`, err);
87
+ });
84
88
  }
85
89
  else {
86
90
  log(`[BOT] ℹ️ No push_id found in message, will use config default`);
@@ -129,10 +133,9 @@ export async function handleXYMessage(params) {
129
133
  // Extract text and files from parts
130
134
  const text = extractTextFromParts(parsed.parts);
131
135
  const fileParts = extractFileParts(parsed.parts);
132
- // Download files if present (using core's media download)
133
- const mediaList = await downloadFilesFromParts(fileParts);
134
- // Build media payload for inbound context (following feishu pattern)
135
- const mediaPayload = buildXYMediaPayload(mediaList);
136
+ // Build media payload directly from file URIs (openclaw can download them)
137
+ // No need to download files locally - pass URIs directly to openclaw
138
+ const mediaPayload = buildXYMediaPayload(fileParts);
136
139
  // Resolve envelope format options (following feishu pattern)
137
140
  const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
138
141
  // Build message body with speaker prefix (following feishu pattern)
@@ -276,17 +279,18 @@ export async function handleXYMessage(params) {
276
279
  /**
277
280
  * Build media payload for inbound context.
278
281
  * Following feishu pattern: buildFeishuMediaPayload().
282
+ * Uses remote URIs directly - openclaw will download them.
279
283
  */
280
- function buildXYMediaPayload(mediaList) {
281
- const first = mediaList[0];
282
- const mediaPaths = mediaList.map((media) => media.path);
283
- const mediaTypes = mediaList.map((media) => media.mimeType).filter(Boolean);
284
+ function buildXYMediaPayload(fileParts) {
285
+ const first = fileParts[0];
286
+ const uris = fileParts.map((file) => file.uri);
287
+ const mediaTypes = fileParts.map((file) => file.mimeType).filter(Boolean);
284
288
  return {
285
- MediaPath: first?.path,
289
+ MediaPath: first?.uri,
286
290
  MediaType: first?.mimeType,
287
- MediaUrl: first?.path,
288
- MediaPaths: mediaPaths.length > 0 ? mediaPaths : undefined,
289
- MediaUrls: mediaPaths.length > 0 ? mediaPaths : undefined,
291
+ MediaUrl: first?.uri,
292
+ MediaPaths: uris.length > 0 ? uris : undefined,
293
+ MediaUrls: uris.length > 0 ? uris : undefined,
290
294
  MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined,
291
295
  };
292
296
  }
@@ -21,6 +21,8 @@ import { searchAlarmTool } from "./tools/search-alarm-tool.js";
21
21
  import { modifyAlarmTool } from "./tools/modify-alarm-tool.js";
22
22
  import { deleteAlarmTool } from "./tools/delete-alarm-tool.js";
23
23
  import { sendFileToUserTool } from "./tools/send-file-to-user-tool.js";
24
+ import { xiaoyiCollectionTool } from "./tools/xiaoyi-collection-tool.js";
25
+ import { viewPushResultTool } from "./tools/view-push-result-tool.js";
24
26
  /**
25
27
  * Xiaoyi Channel Plugin for OpenClaw.
26
28
  * Implements Xiaoyi A2A protocol with dual WebSocket connections.
@@ -60,7 +62,7 @@ export const xyPlugin = {
60
62
  },
61
63
  outbound: xyOutbound,
62
64
  onboarding: xyOnboardingAdapter,
63
- agentTools: [locationTool, noteTool, searchNoteTool, modifyNoteTool, calendarTool, searchCalendarTool, searchContactTool, searchPhotoGalleryTool, uploadPhotoTool, xiaoyiGuiTool, callPhoneTool, searchMessageTool, searchFileTool, uploadFileTool, createAlarmTool, searchAlarmTool, modifyAlarmTool, deleteAlarmTool, sendFileToUserTool],
65
+ agentTools: [locationTool, noteTool, searchNoteTool, modifyNoteTool, calendarTool, searchCalendarTool, searchContactTool, searchPhotoGalleryTool, uploadPhotoTool, xiaoyiGuiTool, callPhoneTool, searchMessageTool, searchFileTool, uploadFileTool, createAlarmTool, searchAlarmTool, modifyAlarmTool, deleteAlarmTool, sendFileToUserTool, xiaoyiCollectionTool, viewPushResultTool],
64
66
  messaging: {
65
67
  normalizeTarget: (raw) => {
66
68
  const trimmed = raw.trim();
@@ -87,10 +89,9 @@ export const xyPlugin = {
87
89
  const account = resolveXYConfig(context.cfg);
88
90
  context.setStatus?.({
89
91
  accountId: context.accountId,
90
- wsUrl1: account.wsUrl1,
91
- wsUrl2: account.wsUrl2,
92
+ wsUrl: account.wsUrl,
92
93
  });
93
- context.log?.info(`[${context.accountId}] starting xiaoyi channel (wsUrl1: ${account.wsUrl1}, wsUrl2: ${account.wsUrl2})`);
94
+ context.log?.info(`[${context.accountId}] starting xiaoyi channel (wsUrl: ${account.wsUrl})`);
94
95
  return monitorXYProvider({
95
96
  config: context.cfg,
96
97
  runtime: context.runtime,
@@ -89,29 +89,16 @@ export function diagnoseAllManagers() {
89
89
  console.log(`📌 Manager: ${key}`);
90
90
  console.log(` Shutting down: ${diag.isShuttingDown}`);
91
91
  console.log(` Total event listeners on manager: ${diag.totalEventListeners}`);
92
- // Server 1
93
- console.log(` 🔌 Server1:`);
94
- console.log(` - Exists: ${diag.server1.exists}`);
95
- console.log(` - ReadyState: ${diag.server1.readyState}`);
96
- console.log(` - State connected/ready: ${diag.server1.stateConnected}/${diag.server1.stateReady}`);
97
- console.log(` - Reconnect attempts: ${diag.server1.reconnectAttempts}`);
98
- console.log(` - Listeners on WebSocket: ${diag.server1.listenerCount}`);
99
- console.log(` - Heartbeat active: ${diag.server1.heartbeatActive}`);
100
- console.log(` - Has reconnect timer: ${diag.server1.hasReconnectTimer}`);
101
- if (diag.server1.isOrphan) {
102
- console.log(` ⚠️ ORPHAN CONNECTION DETECTED!`);
103
- orphanCount++;
104
- }
105
- // Server 2
106
- console.log(` 🔌 Server2:`);
107
- console.log(` - Exists: ${diag.server2.exists}`);
108
- console.log(` - ReadyState: ${diag.server2.readyState}`);
109
- console.log(` - State connected/ready: ${diag.server2.stateConnected}/${diag.server2.stateReady}`);
110
- console.log(` - Reconnect attempts: ${diag.server2.reconnectAttempts}`);
111
- console.log(` - Listeners on WebSocket: ${diag.server2.listenerCount}`);
112
- console.log(` - Heartbeat active: ${diag.server2.heartbeatActive}`);
113
- console.log(` - Has reconnect timer: ${diag.server2.hasReconnectTimer}`);
114
- if (diag.server2.isOrphan) {
92
+ // Connection
93
+ console.log(` 🔌 Connection:`);
94
+ console.log(` - Exists: ${diag.connection.exists}`);
95
+ console.log(` - ReadyState: ${diag.connection.readyState}`);
96
+ console.log(` - State connected/ready: ${diag.connection.stateConnected}/${diag.connection.stateReady}`);
97
+ console.log(` - Reconnect attempts: ${diag.connection.reconnectAttempts}`);
98
+ console.log(` - Listeners on WebSocket: ${diag.connection.listenerCount}`);
99
+ console.log(` - Heartbeat active: ${diag.connection.heartbeatActive}`);
100
+ console.log(` - Has reconnect timer: ${diag.connection.hasReconnectTimer}`);
101
+ if (diag.connection.isOrphan) {
115
102
  console.log(` ⚠️ ORPHAN CONNECTION DETECTED!`);
116
103
  orphanCount++;
117
104
  }
@@ -134,7 +121,7 @@ export function cleanupOrphanConnections() {
134
121
  let cleanedCount = 0;
135
122
  wsManagerCache.forEach((manager, key) => {
136
123
  const diag = manager.getConnectionDiagnostics();
137
- if (diag.server1.isOrphan || diag.server2.isOrphan) {
124
+ if (diag.connection.isOrphan) {
138
125
  console.log(`🧹 Cleaning up orphan connections in manager: ${key}`);
139
126
  manager.disconnect();
140
127
  cleanedCount++;
@@ -17,8 +17,8 @@ export function resolveXYConfig(cfg) {
17
17
  // Return configuration with defaults
18
18
  return {
19
19
  enabled: xyConfig.enabled ?? false,
20
- wsUrl1: xyConfig.wsUrl1 ?? "ws://localhost:8765/ws/link",
21
- wsUrl2: xyConfig.wsUrl2 ?? "ws://localhost:8768/ws/link",
20
+ // ✅ 兼容旧配置:优先使用 wsUrl,然后 wsUrl2(wsUrl1 被忽略)
21
+ wsUrl: xyConfig.wsUrl ?? xyConfig.wsUrl2 ?? "ws://localhost:8768/ws/link",
22
22
  apiKey: xyConfig.apiKey,
23
23
  uid: xyConfig.uid,
24
24
  agentId: xyConfig.agentId,
@@ -3,6 +3,7 @@ import { getXYWebSocketManager, diagnoseAllManagers, cleanupOrphanConnections, r
3
3
  import { handleXYMessage } from "./bot.js";
4
4
  import { parseA2AMessage } from "./parser.js";
5
5
  import { hasActiveTask } from "./task-manager.js";
6
+ import { handleTriggerEvent } from "./trigger-handler.js";
6
7
  /**
7
8
  * Per-session serial queue that ensures messages from the same session are processed
8
9
  * in arrival order while allowing different sessions to run concurrently.
@@ -150,6 +151,15 @@ export async function monitorXYProvider(opts = {}) {
150
151
  const errorHandler = (err, serverId) => {
151
152
  error(`XY gateway: ${serverId} error: ${String(err)}`);
152
153
  };
154
+ const triggerEventHandler = (context) => {
155
+ log(`[MONITOR] 📌 Received trigger-event, dispatching to handler...`);
156
+ log(`[MONITOR] - sessionId: ${context.sessionId}`);
157
+ log(`[MONITOR] - taskId: ${context.taskId}`);
158
+ // 异步处理 Trigger 事件,不阻塞主流程
159
+ handleTriggerEvent(context, cfg, runtime, accountId).catch((err) => {
160
+ error(`[MONITOR] Failed to handle trigger-event:`, err);
161
+ });
162
+ };
153
163
  const cleanup = () => {
154
164
  log("XY gateway: cleaning up...");
155
165
  // 🔍 Diagnose before cleanup
@@ -166,6 +176,7 @@ export async function monitorXYProvider(opts = {}) {
166
176
  wsManager.off("connected", connectedHandler);
167
177
  wsManager.off("disconnected", disconnectedHandler);
168
178
  wsManager.off("error", errorHandler);
179
+ wsManager.off("trigger-event", triggerEventHandler);
169
180
  // ✅ Disconnect the wsManager to prevent connection leaks
170
181
  // This is safe because each gateway lifecycle should have clean connections
171
182
  wsManager.disconnect();
@@ -195,6 +206,7 @@ export async function monitorXYProvider(opts = {}) {
195
206
  wsManager.on("connected", connectedHandler);
196
207
  wsManager.on("disconnected", disconnectedHandler);
197
208
  wsManager.on("error", errorHandler);
209
+ wsManager.on("trigger-event", triggerEventHandler);
198
210
  // Start periodic health check (every 5 minutes)
199
211
  console.log("🏥 Starting periodic health check (every 5 minutes)...");
200
212
  healthCheckInterval = setInterval(() => {
@@ -1,6 +1,9 @@
1
1
  import { resolveXYConfig } from "./config.js";
2
+ import { XYFileUploadService } from "./file-upload.js";
2
3
  import { XYPushService } from "./push.js";
3
4
  import { getCurrentSessionContext } from "./tools/session-manager.js";
5
+ import { savePushData } from "./utils/pushdata-manager.js";
6
+ import { getAllPushIds } from "./utils/pushid-manager.js";
4
7
  // Special marker for default push delivery when no target is specified
5
8
  const DEFAULT_PUSH_MARKER = "default";
6
9
  // File extension to MIME type mapping
@@ -104,19 +107,63 @@ export const xyOutbound = {
104
107
  // The push service will handle it based on config
105
108
  actualTo = config.defaultSessionId || "";
106
109
  }
110
+ // 1. 持久化推送消息内容,获取 pushDataId
111
+ console.log(`[xyOutbound.sendText] Saving push data to local storage...`);
112
+ let pushDataId;
113
+ try {
114
+ pushDataId = await savePushData(text);
115
+ console.log(`[xyOutbound.sendText] ✅ Push data saved with ID: ${pushDataId}`);
116
+ }
117
+ catch (error) {
118
+ console.error(`[xyOutbound.sendText] ❌ Failed to save push data:`, error);
119
+ // 如果持久化失败,仍然继续发送(不阻塞主流程)
120
+ pushDataId = "";
121
+ }
122
+ // 2. 读取所有 pushId
123
+ console.log(`[xyOutbound.sendText] Loading all pushIds...`);
124
+ let pushIdList = [];
125
+ try {
126
+ pushIdList = await getAllPushIds();
127
+ console.log(`[xyOutbound.sendText] ✅ Loaded ${pushIdList.length} pushIds`);
128
+ }
129
+ catch (error) {
130
+ console.error(`[xyOutbound.sendText] ❌ Failed to load pushIds:`, error);
131
+ }
132
+ // 3. 如果 pushIdList 为空,回退到原有逻辑(使用 config pushId)
133
+ if (pushIdList.length === 0) {
134
+ console.log(`[xyOutbound.sendText] ⚠️ No pushIds found, falling back to config pushId`);
135
+ pushIdList = [config.pushId];
136
+ }
107
137
  // Create push service
108
138
  const pushService = new XYPushService(config);
109
139
  // Extract title (first 57 chars or first line)
110
140
  const title = text.split("\n")[0].slice(0, 57);
111
141
  // Truncate push content to max length 1000
112
142
  const pushText = text.length > 1000 ? text.slice(0, 1000) : text;
113
- // Send push message (content, title, data, sessionId)
114
- await pushService.sendPush(pushText, title, undefined, actualTo);
143
+ // 4. 遍历所有 pushId,依次发送推送通知
144
+ console.log(`[xyOutbound.sendText] 📤 Broadcasting to ${pushIdList.length} pushId(s)...`);
145
+ let successCount = 0;
146
+ let failureCount = 0;
147
+ for (const pushId of pushIdList) {
148
+ try {
149
+ console.log(`[xyOutbound.sendText] Sending to pushId: ${pushId.substring(0, 20)}...`);
150
+ // 传入 pushDataId,使用 kind="data" 格式
151
+ await pushService.sendPush(pushText, title, undefined, actualTo, pushDataId);
152
+ successCount++;
153
+ console.log(`[xyOutbound.sendText] ✅ Sent successfully to pushId: ${pushId.substring(0, 20)}...`);
154
+ }
155
+ catch (error) {
156
+ failureCount++;
157
+ console.error(`[xyOutbound.sendText] ❌ Failed to send to pushId: ${pushId.substring(0, 20)}...`, error);
158
+ // 单个 pushId 发送失败不影响其他,继续处理下一个
159
+ }
160
+ }
161
+ console.log(`[xyOutbound.sendText] 📊 Broadcast summary: ${successCount} success, ${failureCount} failures`);
115
162
  console.log(`[xyOutbound.sendText] Completed successfully`);
116
163
  // Return message info
117
164
  return {
118
165
  channel: "xiaoyi-channel",
119
- messageId: Date.now().toString(),
166
+ messageId: pushDataId || Date.now().toString(),
120
167
  chatId: actualTo,
121
168
  };
122
169
  },
@@ -129,94 +176,81 @@ export const xyOutbound = {
129
176
  mediaUrl,
130
177
  mediaLocalRoots,
131
178
  });
132
- // All sendMedia processing logic has been disabled
133
- // Use send_file_to_user tool instead for file transfers to user device
134
- console.log(`[xyOutbound.sendMedia] Processing disabled, use send_file_to_user tool`);
135
- // Return empty message info
179
+ // Parse to: "sessionId::taskId"
180
+ const parts = to.split("::");
181
+ if (parts.length !== 2) {
182
+ throw new Error(`Invalid to format: "${to}". Expected "sessionId::taskId"`);
183
+ }
184
+ const [sessionId, taskId] = parts;
185
+ // Resolve configuration
186
+ const config = resolveXYConfig(cfg);
187
+ // Create upload service
188
+ const uploadService = new XYFileUploadService(config.fileUploadUrl, config.apiKey, config.uid);
189
+ // Validate mediaUrl
190
+ if (!mediaUrl) {
191
+ throw new Error("mediaUrl is required for sendMedia");
192
+ }
193
+ // Upload file
194
+ const fileId = await uploadService.uploadFile(mediaUrl);
195
+ // Check if fileId is empty
196
+ if (!fileId) {
197
+ console.log(`[xyOutbound.sendMedia] ⚠️ File upload failed: fileId is empty, aborting sendMedia`);
198
+ return {
199
+ channel: "xiaoyi-channel",
200
+ messageId: "",
201
+ chatId: to,
202
+ };
203
+ }
204
+ console.log(`[xyOutbound.sendMedia] File uploaded:`, {
205
+ fileId,
206
+ sessionId,
207
+ taskId,
208
+ });
209
+ // Get filename and mime type from mediaUrl
210
+ // mediaUrl may be a local file path or URL
211
+ const fileName = mediaUrl.split("/").pop() || "unknown";
212
+ const mimeType = getMimeTypeFromFilename(fileName);
213
+ // Build agent_response message
214
+ const agentResponse = {
215
+ msgType: "agent_response",
216
+ agentId: config.agentId,
217
+ sessionId: sessionId,
218
+ taskId: taskId,
219
+ msgDetail: JSON.stringify({
220
+ jsonrpc: "2.0",
221
+ id: taskId,
222
+ result: {
223
+ kind: "artifact-update",
224
+ append: true,
225
+ lastChunk: false,
226
+ final: false,
227
+ artifact: {
228
+ artifactId: taskId,
229
+ parts: [
230
+ {
231
+ kind: "file",
232
+ file: {
233
+ name: fileName,
234
+ mimeType: mimeType,
235
+ fileId: fileId,
236
+ },
237
+ },
238
+ ],
239
+ },
240
+ },
241
+ error: { code: 0 },
242
+ }),
243
+ };
244
+ // Get WebSocket manager and send message
245
+ const { getXYWebSocketManager } = await import("./client.js");
246
+ const wsManager = getXYWebSocketManager(config);
247
+ await wsManager.sendMessage(sessionId, agentResponse);
248
+ console.log(`[xyOutbound.sendMedia] WebSocket message sent successfully`);
249
+ // Return message info
136
250
  return {
137
251
  channel: "xiaoyi-channel",
138
- messageId: "把文件/图片发送给用户请使用send_file_to_user工具",
252
+ messageId: fileId,
139
253
  chatId: to,
140
254
  };
141
- // // Parse to: "sessionId::taskId"
142
- // const parts = to.split("::");
143
- // if (parts.length !== 2) {
144
- // throw new Error(`Invalid to format: "${to}". Expected "sessionId::taskId"`);
145
- // }
146
- // const [sessionId, taskId] = parts;
147
- // // Resolve configuration
148
- // const config = resolveXYConfig(cfg);
149
- // // Create upload service
150
- // const uploadService = new XYFileUploadService(
151
- // config.fileUploadUrl,
152
- // config.apiKey,
153
- // config.uid
154
- // );
155
- // // Validate mediaUrl
156
- // if (!mediaUrl) {
157
- // throw new Error("mediaUrl is required for sendMedia");
158
- // }
159
- // // Upload file
160
- // const fileId = await uploadService.uploadFile(mediaUrl);
161
- // // Check if fileId is empty
162
- // if (!fileId) {
163
- // console.log(`[xyOutbound.sendMedia] ⚠️ File upload failed: fileId is empty, aborting sendMedia`);
164
- // return {
165
- // channel: "xiaoyi-channel",
166
- // messageId: "",
167
- // chatId: to,
168
- // };
169
- // }
170
- // console.log(`[xyOutbound.sendMedia] File uploaded:`, {
171
- // fileId,
172
- // sessionId,
173
- // taskId,
174
- // });
175
- // // Get filename and mime type from mediaUrl
176
- // // mediaUrl may be a local file path or URL
177
- // const fileName = mediaUrl.split("/").pop() || "unknown";
178
- // const mimeType = getMimeTypeFromFilename(fileName);
179
- // // Build agent_response message
180
- // const agentResponse: OutboundWebSocketMessage = {
181
- // msgType: "agent_response",
182
- // agentId: config.agentId,
183
- // sessionId: sessionId,
184
- // taskId: taskId,
185
- // msgDetail: JSON.stringify({
186
- // jsonrpc: "2.0",
187
- // id: taskId,
188
- // result: {
189
- // kind: "artifact-update",
190
- // append: true,
191
- // lastChunk: false,
192
- // final: false,
193
- // artifact: {
194
- // artifactId: taskId,
195
- // parts: [
196
- // {
197
- // kind: "file",
198
- // file: {
199
- // name: fileName,
200
- // mimeType: mimeType,
201
- // fileId: fileId,
202
- // },
203
- // },
204
- // ],
205
- // },
206
- // },
207
- // error: { code: 0 },
208
- // }),
209
- // };
210
- // // Get WebSocket manager and send message
211
- // const { getXYWebSocketManager } = await import("./client.js");
212
- // const wsManager = getXYWebSocketManager(config);
213
- // await wsManager.sendMessage(sessionId, agentResponse);
214
- // console.log(`[xyOutbound.sendMedia] WebSocket message sent successfully`);
215
- // // Return message info
216
- // return {
217
- // channel: "xiaoyi-channel",
218
- // messageId: fileId,
219
- // chatId: to,
220
- // };
221
255
  },
222
256
  };
@@ -14,8 +14,14 @@ export declare class XYPushService {
14
14
  private generateTraceId;
15
15
  /**
16
16
  * Send a push message to a user session.
17
+ *
18
+ * @param content - Push message content
19
+ * @param title - Push message title
20
+ * @param data - Optional additional data
21
+ * @param sessionId - Optional session ID
22
+ * @param pushDataId - Optional pushDataId for kind="data" format
17
23
  */
18
- sendPush(content: string, title: string, data?: Record<string, any>, sessionId?: string): Promise<void>;
24
+ sendPush(content: string, title: string, data?: Record<string, any>, sessionId?: string, pushDataId?: string): Promise<void>;
19
25
  /**
20
26
  * Send a push message with file attachments.
21
27
  */
package/dist/src/push.js CHANGED
@@ -21,8 +21,14 @@ export class XYPushService {
21
21
  }
22
22
  /**
23
23
  * Send a push message to a user session.
24
+ *
25
+ * @param content - Push message content
26
+ * @param title - Push message title
27
+ * @param data - Optional additional data
28
+ * @param sessionId - Optional session ID
29
+ * @param pushDataId - Optional pushDataId for kind="data" format
24
30
  */
25
- async sendPush(content, title, data, sessionId) {
31
+ async sendPush(content, title, data, sessionId, pushDataId) {
26
32
  const pushUrl = this.config.pushUrl || this.DEFAULT_PUSH_URL;
27
33
  const traceId = this.generateTraceId();
28
34
  // Get dynamic pushId for the session (falls back to config pushId)
@@ -57,12 +63,21 @@ export class XYPushService {
57
63
  artifacts: [
58
64
  {
59
65
  artifactId: randomUUID(),
60
- parts: [
61
- {
62
- kind: "text",
63
- text: content,
64
- },
65
- ],
66
+ parts: pushDataId
67
+ ? [
68
+ {
69
+ kind: "data",
70
+ data: {
71
+ pushDataId: pushDataId,
72
+ },
73
+ },
74
+ ]
75
+ : [
76
+ {
77
+ kind: "text",
78
+ text: content,
79
+ },
80
+ ],
66
81
  },
67
82
  ],
68
83
  },
@@ -13,23 +13,51 @@ export const searchPhotoGalleryTool = {
13
13
  name: "search_photo_gallery",
14
14
  label: "Search Photo Gallery",
15
15
  description: `插件功能描述:搜索用户手机图库中的照片
16
+
16
17
  工具使用约束:如果用户说从手机图库中或者从相册中查询xx图片时调用此工具。
18
+
17
19
  工具输入输出简介:
18
20
  a. 根据图像描述语料检索匹配的照片,返回照片在手机本地的 mediaUri以及thumbnailUri。
19
21
  b. 返回的 mediaUri以及thumbnailUri 是本地路径,无法直接下载或访问。如果需要下载、查看、使用或展示照片,请使用 upload_photo 工具将 mediaUri或者thumbnailUri 转换为可访问的公网 URL。
20
22
  c. mediaUri代表手机相册中的图片原图路径,图片大小比较大,清晰度比较高
21
23
  d. thumbnailUri代表手机相册中的图片缩略图路径,图片大小比较小,清晰度适中,建议在upload_photo 工具的入参中优先使用此路径,不容易引起上传超时等问题
22
-
24
+
25
+ 搜索能力边界:
26
+ a. ✅ 支持口语化输入:改写模型会自动提取姓名、种类、地点等实体,可以使用自然语言描述(如"小狗的照片"、"南京拍的风景")
27
+ b. ✅ 支持相册搜索:可以在query中包含相册名称(如"西安之行相册的照片")
28
+ c. ✅ 支持人像搜索:前提是照片有人像tag,且需要口语化描述(如"张三的照片")
29
+ d. ❌ 不支持时间相对词:不支持"最新"、"最旧"、"最早"等表述,需要使用具体时间(如"2024年的照片"而非"去年的照片")
30
+ e. ❌ 不支持多实体查询:不支持"或"逻辑和时间范围(如"南京或上海的照片"、"近三年的照片"),需要拆分成多次独立查询
31
+ f. ❌ 不支持POI逆地理映射:照片的location是门牌号,用真实场地名称可能搜不到
32
+ g. ❌ 不支持收藏感知:无法感知照片是否被收藏
33
+ h. ❌ 不支持细粒度品种:对于动物、植物等的具体品种识别能力有限
34
+ i. ⚠️ POI提取可能不准确:地名可能作为语义搜索条件,可能导致"xx湖"搜到"yy江"或"zz湾"的照片
35
+
36
+ 查询优化建议:
37
+ a. 时间查询:将"最新"、"去年"、"近三年"等转换为具体年份(如"2024年"、"2023年到2025年"需拆分成"2023年"、"2024年"、"2025年"三次查询)
38
+ b. 多条件查询:将"或"逻辑拆分成多次查询(如"南京或上海的照片"→先查"南京的照片",再查"上海的照片")
39
+ c. 实体原子化:确保每个query只包含一个原子实体(地点、人名、物品等)
40
+ d. 相册名称:如果知道相册名,直接在query中包含相册名可以提高准确度
41
+
23
42
  注意事项:
24
43
  a. 只有当用户明确表达从手机相册搜索或者从图库搜索时才执行此工具,如果用户仅表达要搜索xxx图片,并没有说明搜索数据源,则不要贸然调用此插件,可以优先尝试websearch或者询问用户是否要从手机图库中搜索。
25
44
  b. 操作超时时间为60秒,请勿重复调用此工具,如果超时或失败,最多重试一次。
45
+ c. 如果用户请求包含多个实体或时间范围,需要主动拆分成多次查询并告知用户。
26
46
  `,
27
47
  parameters: {
28
48
  type: "object",
29
49
  properties: {
30
50
  query: {
31
51
  type: "string",
32
- description: "图像描述语料,用于检索匹配的照片(例如:'小狗的照片'、'带有键盘的图片'等)",
52
+ description: `图像描述语料,用于检索匹配的照片。支持口语化输入,会自动提取姓名、种类、地点等实体。
53
+
54
+ 使用示例:
55
+ - 正确:"小狗的照片"、"南京拍的风景"、"张三的照片"、"西安之行相册的照片"、"2024年的照片"
56
+ - 错误:"最新的照片"(应改为具体年份如"2024年的照片")
57
+ - 错误:"南京或上海的照片"(需拆分成两次查询:"南京的照片" 和 "上海的照片")
58
+ - 错误:"近三年的照片"(需拆分成"2023年的照片"、"2024年的照片"、"2025年的照片")
59
+
60
+ 重要:每次查询只能包含一个原子实体(单个地点、单个人名、单个年份等),不支持多实体或"或"逻辑。`,
33
61
  },
34
62
  },
35
63
  required: ["query"],
@@ -0,0 +1,5 @@
1
+ /**
2
+ * 查看推送任务执行结果工具
3
+ * 支持关键词搜索或查看最近的推送记录
4
+ */
5
+ export declare const viewPushResultTool: any;