@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 +17 -13
- package/dist/src/channel.js +5 -4
- package/dist/src/client.js +11 -24
- package/dist/src/config.js +2 -2
- package/dist/src/monitor.js +12 -0
- package/dist/src/outbound.js +122 -88
- package/dist/src/push.d.ts +7 -1
- package/dist/src/push.js +22 -7
- package/dist/src/tools/search-photo-gallery-tool.js +30 -2
- package/dist/src/tools/view-push-result-tool.d.ts +5 -0
- package/dist/src/tools/view-push-result-tool.js +118 -0
- package/dist/src/tools/xiaoyi-collection-tool.d.ts +5 -0
- package/dist/src/tools/xiaoyi-collection-tool.js +190 -0
- package/dist/src/trigger-handler.d.ts +19 -0
- package/dist/src/trigger-handler.js +91 -0
- package/dist/src/types.d.ts +1 -8
- package/dist/src/types.js +4 -0
- package/dist/src/utils/pushdata-manager.d.ts +28 -0
- package/dist/src/utils/pushdata-manager.js +171 -0
- package/dist/src/utils/pushid-manager.d.ts +12 -0
- package/dist/src/utils/pushid-manager.js +105 -0
- package/dist/src/websocket.d.ts +25 -31
- package/dist/src/websocket.js +218 -270
- package/package.json +1 -1
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
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
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(
|
|
281
|
-
const first =
|
|
282
|
-
const
|
|
283
|
-
const mediaTypes =
|
|
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?.
|
|
289
|
+
MediaPath: first?.uri,
|
|
286
290
|
MediaType: first?.mimeType,
|
|
287
|
-
MediaUrl: first?.
|
|
288
|
-
MediaPaths:
|
|
289
|
-
MediaUrls:
|
|
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
|
}
|
package/dist/src/channel.js
CHANGED
|
@@ -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
|
-
|
|
91
|
-
wsUrl2: account.wsUrl2,
|
|
92
|
+
wsUrl: account.wsUrl,
|
|
92
93
|
});
|
|
93
|
-
context.log?.info(`[${context.accountId}] starting xiaoyi channel (
|
|
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,
|
package/dist/src/client.js
CHANGED
|
@@ -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
|
-
//
|
|
93
|
-
console.log(` 🔌
|
|
94
|
-
console.log(` - Exists: ${diag.
|
|
95
|
-
console.log(` - ReadyState: ${diag.
|
|
96
|
-
console.log(` - State connected/ready: ${diag.
|
|
97
|
-
console.log(` - Reconnect attempts: ${diag.
|
|
98
|
-
console.log(` - Listeners on WebSocket: ${diag.
|
|
99
|
-
console.log(` - Heartbeat active: ${diag.
|
|
100
|
-
console.log(` - Has reconnect timer: ${diag.
|
|
101
|
-
if (diag.
|
|
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.
|
|
124
|
+
if (diag.connection.isOrphan) {
|
|
138
125
|
console.log(`🧹 Cleaning up orphan connections in manager: ${key}`);
|
|
139
126
|
manager.disconnect();
|
|
140
127
|
cleanedCount++;
|
package/dist/src/config.js
CHANGED
|
@@ -17,8 +17,8 @@ export function resolveXYConfig(cfg) {
|
|
|
17
17
|
// Return configuration with defaults
|
|
18
18
|
return {
|
|
19
19
|
enabled: xyConfig.enabled ?? false,
|
|
20
|
-
|
|
21
|
-
|
|
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,
|
package/dist/src/monitor.js
CHANGED
|
@@ -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(() => {
|
package/dist/src/outbound.js
CHANGED
|
@@ -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
|
-
//
|
|
114
|
-
|
|
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
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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:
|
|
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
|
};
|
package/dist/src/push.d.ts
CHANGED
|
@@ -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
|
-
|
|
63
|
-
|
|
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"],
|