@ynhcj/xiaoyi-channel 0.0.52-beta → 0.0.54-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/index.d.ts +0 -2
- package/dist/index.js +0 -2
- package/dist/src/bot.js +0 -24
- package/dist/src/channel.js +16 -2
- package/dist/src/client.js +0 -6
- package/dist/src/cspl/call-api.js +9 -2
- package/dist/src/cspl/constants.d.ts +1 -1
- package/dist/src/cspl/constants.js +1 -1
- package/dist/src/file-upload.js +1 -11
- package/dist/src/formatter.js +1 -4
- package/dist/src/monitor.js +4 -10
- package/dist/src/onboarding.d.ts +3 -4
- package/dist/src/onboarding.js +2 -2
- package/dist/src/outbound.d.ts +2 -1
- package/dist/src/outbound.js +1 -19
- package/dist/src/push.js +0 -21
- package/dist/src/reply-dispatcher.js +6 -4
- package/dist/src/thread-bindings.d.ts +54 -0
- package/dist/src/thread-bindings.js +214 -0
- package/dist/src/tools/send-file-to-user-tool.js +1 -0
- package/dist/src/websocket.js +15 -9
- package/package.json +2 -2
- package/dist/src/tools/search-photo-tool.d.ts +0 -9
- package/dist/src/tools/search-photo-tool.js +0 -270
- package/dist/src/utils/session.d.ts +0 -34
- package/dist/src/utils/session.js +0 -50
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
-
import { xyPlugin } from "./src/channel.js";
|
|
3
2
|
/**
|
|
4
3
|
* Xiaoyi Channel Plugin Entry Point.
|
|
5
4
|
* Exports the plugin for OpenClaw to load.
|
|
@@ -13,4 +12,3 @@ declare const plugin: {
|
|
|
13
12
|
register(api: OpenClawPluginApi): void;
|
|
14
13
|
};
|
|
15
14
|
export default plugin;
|
|
16
|
-
export { xyPlugin };
|
package/dist/index.js
CHANGED
package/dist/src/bot.js
CHANGED
|
@@ -26,8 +26,6 @@ export async function handleXYMessage(params) {
|
|
|
26
26
|
try {
|
|
27
27
|
// Check for special messages BEFORE parsing (these have different param structures)
|
|
28
28
|
const messageMethod = message.method;
|
|
29
|
-
log(`[BOT-ENTRY] <<<<<<< Received message with method: ${messageMethod}, id: ${message.id} >>>>>>>`);
|
|
30
|
-
log(`[BOT-ENTRY] Stack trace for debugging:`, new Error().stack?.split('\n').slice(1, 4).join('\n'));
|
|
31
29
|
// Handle clearContext messages (params only has sessionId)
|
|
32
30
|
if (messageMethod === "clearContext" || messageMethod === "clear_context") {
|
|
33
31
|
const sessionId = message.params?.sessionId;
|
|
@@ -78,8 +76,6 @@ export async function handleXYMessage(params) {
|
|
|
78
76
|
}
|
|
79
77
|
log(`[BOT] ✅ Found pushData, sending direct response`);
|
|
80
78
|
log(`[BOT] - pushDataId: ${pushDataItem.pushDataId}`);
|
|
81
|
-
log(`[BOT] - time: ${pushDataItem.time}`);
|
|
82
|
-
log(`[BOT] - content length: ${pushDataItem.dataDetail.length} chars`);
|
|
83
79
|
const config = resolveXYConfig(cfg);
|
|
84
80
|
// 直接发送响应(final=true,不走 openclaw 流程)
|
|
85
81
|
await sendA2AResponse({
|
|
@@ -120,9 +116,6 @@ export async function handleXYMessage(params) {
|
|
|
120
116
|
const pushId = extractPushId(parsed.parts);
|
|
121
117
|
if (pushId) {
|
|
122
118
|
log(`[BOT] 📌 Extracted push_id from user message`);
|
|
123
|
-
log(`[BOT] - Session ID: ${parsed.sessionId}`);
|
|
124
|
-
log(`[BOT] - Push ID preview: ${pushId.substring(0, 20)}...`);
|
|
125
|
-
log(`[BOT] - Full push_id: ${pushId}`);
|
|
126
119
|
configManager.updatePushId(parsed.sessionId, pushId);
|
|
127
120
|
// 持久化 pushId 到本地文件(异步,不阻塞主流程)
|
|
128
121
|
addPushId(pushId).catch((err) => {
|
|
@@ -147,12 +140,6 @@ export async function handleXYMessage(params) {
|
|
|
147
140
|
},
|
|
148
141
|
});
|
|
149
142
|
log(`xy: resolved route accountId=${route.accountId}, sessionKey=${route.sessionKey}`);
|
|
150
|
-
// 🔑 注册session(带引用计数)
|
|
151
|
-
log(`[BOT] 📝 About to register session for tools...`);
|
|
152
|
-
log(`[BOT] - sessionKey: ${route.sessionKey}`);
|
|
153
|
-
log(`[BOT] - sessionId: ${parsed.sessionId}`);
|
|
154
|
-
log(`[BOT] - taskId: ${parsed.taskId}`);
|
|
155
|
-
log(`[BOT] - isSecondMessage: ${isSecondMessage}`);
|
|
156
143
|
registerSession(route.sessionKey, {
|
|
157
144
|
config,
|
|
158
145
|
sessionId: parsed.sessionId,
|
|
@@ -160,7 +147,6 @@ export async function handleXYMessage(params) {
|
|
|
160
147
|
messageId: parsed.messageId,
|
|
161
148
|
agentId: route.accountId,
|
|
162
149
|
});
|
|
163
|
-
log(`[BOT] ✅ Session registered for tools`);
|
|
164
150
|
// 🔑 发送初始状态更新(第二条消息也要发,用新taskId)
|
|
165
151
|
log(`[STATUS] Sending initial status update for session ${parsed.sessionId}`);
|
|
166
152
|
void sendStatusUpdate({
|
|
@@ -221,9 +207,7 @@ export async function handleXYMessage(params) {
|
|
|
221
207
|
});
|
|
222
208
|
// 🔑 创建dispatcher(dispatcher会自动使用动态taskId)
|
|
223
209
|
log(`[BOT-DISPATCHER] 🎯 Creating reply dispatcher`);
|
|
224
|
-
log(`[BOT-DISPATCHER] - session: ${parsed.sessionId}`);
|
|
225
210
|
log(`[BOT-DISPATCHER] - taskId: ${parsed.taskId}`);
|
|
226
|
-
log(`[BOT-DISPATCHER] - isSecondMessage: ${isSecondMessage}`);
|
|
227
211
|
const { dispatcher, replyOptions, markDispatchIdle, startStatusInterval } = createXYReplyDispatcher({
|
|
228
212
|
cfg,
|
|
229
213
|
runtime,
|
|
@@ -233,19 +217,12 @@ export async function handleXYMessage(params) {
|
|
|
233
217
|
accountId: route.accountId,
|
|
234
218
|
isSteerFollower: isSecondMessage, // 🔑 标记第二条消息
|
|
235
219
|
});
|
|
236
|
-
log(`[BOT-DISPATCHER] ✅ Reply dispatcher created successfully`);
|
|
237
220
|
// 🔑 只有第一条消息启动状态定时器
|
|
238
221
|
// 第二条消息会很快返回,不需要定时器
|
|
239
222
|
if (!isSecondMessage) {
|
|
240
223
|
startStatusInterval();
|
|
241
224
|
log(`[BOT-DISPATCHER] ✅ Status interval started for first message`);
|
|
242
225
|
}
|
|
243
|
-
else {
|
|
244
|
-
log(`[BOT-DISPATCHER] ⏭️ Skipped status interval for steer follower`);
|
|
245
|
-
}
|
|
246
|
-
log(`xy: dispatching to agent (session=${parsed.sessionId})`);
|
|
247
|
-
// Dispatch to OpenClaw core using correct API (following feishu pattern)
|
|
248
|
-
log(`[BOT] 🚀 Starting dispatcher with session: ${route.sessionKey}`);
|
|
249
226
|
// Build session context for AsyncLocalStorage
|
|
250
227
|
const sessionContext = {
|
|
251
228
|
config,
|
|
@@ -259,7 +236,6 @@ export async function handleXYMessage(params) {
|
|
|
259
236
|
onSettled: () => {
|
|
260
237
|
log(`[BOT] 🏁 onSettled called for session: ${route.sessionKey}`);
|
|
261
238
|
log(`[BOT] - isSecondMessage: ${isSecondMessage}`);
|
|
262
|
-
markDispatchIdle();
|
|
263
239
|
// 🔑 减少引用计数
|
|
264
240
|
decrementTaskIdRef(parsed.sessionId);
|
|
265
241
|
// 🔑 如果是第一条消息完成,解锁
|
package/dist/src/channel.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { resolveXYConfig, listXYAccountIds, getDefaultXYAccountId } from "./config.js";
|
|
2
2
|
import { xyConfigSchema } from "./config-schema.js";
|
|
3
3
|
import { xyOutbound } from "./outbound.js";
|
|
4
|
-
import { xyOnboardingAdapter } from "./onboarding.js";
|
|
5
4
|
import { locationTool } from "./tools/location-tool.js";
|
|
6
5
|
import { noteTool } from "./tools/note-tool.js";
|
|
7
6
|
import { searchNoteTool } from "./tools/search-note-tool.js";
|
|
@@ -63,7 +62,6 @@ export const xyPlugin = {
|
|
|
63
62
|
schema: xyConfigSchema,
|
|
64
63
|
},
|
|
65
64
|
outbound: xyOutbound,
|
|
66
|
-
onboarding: xyOnboardingAdapter,
|
|
67
65
|
agentTools: [locationTool, noteTool, searchNoteTool, modifyNoteTool, calendarTool, searchCalendarTool, searchContactTool, searchPhotoGalleryTool, uploadPhotoTool, xiaoyiGuiTool, xiaoyiCollectionTool, callPhoneTool, searchMessageTool, sendMessageTool, searchFileTool, uploadFileTool, createAlarmTool, searchAlarmTool, modifyAlarmTool, deleteAlarmTool, sendFileToUserTool, viewPushResultTool, imageReadingTool],
|
|
68
66
|
messaging: {
|
|
69
67
|
normalizeTarget: (raw) => {
|
|
@@ -81,6 +79,22 @@ export const xyPlugin = {
|
|
|
81
79
|
hint: "<sessionId>",
|
|
82
80
|
},
|
|
83
81
|
},
|
|
82
|
+
bindings: {
|
|
83
|
+
compileConfiguredBinding: ({ conversationId }) => {
|
|
84
|
+
const sessionId = conversationId.trim();
|
|
85
|
+
if (!sessionId)
|
|
86
|
+
return null;
|
|
87
|
+
return {
|
|
88
|
+
conversationId: sessionId,
|
|
89
|
+
parentConversationId: undefined,
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
matchInboundConversation: ({ compiledBinding, conversationId }) => {
|
|
93
|
+
return compiledBinding.conversationId === conversationId
|
|
94
|
+
? { conversationId, matchPriority: 2 }
|
|
95
|
+
: null;
|
|
96
|
+
},
|
|
97
|
+
},
|
|
84
98
|
reload: {
|
|
85
99
|
configPrefixes: ["channels.xiaoyi-channel"],
|
|
86
100
|
},
|
package/dist/src/client.js
CHANGED
|
@@ -72,14 +72,9 @@ export function getCachedManagerCount() {
|
|
|
72
72
|
* Helps identify connection issues and orphan connections.
|
|
73
73
|
*/
|
|
74
74
|
export function diagnoseAllManagers() {
|
|
75
|
-
console.log("========================================");
|
|
76
|
-
console.log("📊 WebSocket Manager Global Diagnostics");
|
|
77
|
-
console.log("========================================");
|
|
78
75
|
console.log(`Total cached managers: ${wsManagerCache.size}`);
|
|
79
|
-
console.log("");
|
|
80
76
|
if (wsManagerCache.size === 0) {
|
|
81
77
|
console.log("ℹ️ No managers in cache");
|
|
82
|
-
console.log("========================================");
|
|
83
78
|
return;
|
|
84
79
|
}
|
|
85
80
|
let orphanCount = 0;
|
|
@@ -108,7 +103,6 @@ export function diagnoseAllManagers() {
|
|
|
108
103
|
else {
|
|
109
104
|
console.log(`✅ No orphan connections found`);
|
|
110
105
|
}
|
|
111
|
-
console.log("========================================");
|
|
112
106
|
}
|
|
113
107
|
/**
|
|
114
108
|
* Clean up orphan connections across all managers.
|
|
@@ -61,15 +61,22 @@ export async function callCsplApi(questionText, cfg) {
|
|
|
61
61
|
});
|
|
62
62
|
res.on("end", () => {
|
|
63
63
|
try {
|
|
64
|
-
|
|
64
|
+
const result = parseResponse(data);
|
|
65
|
+
console.log(`[CSPL API] ✅ 请求成功`);
|
|
66
|
+
resolve(result);
|
|
65
67
|
}
|
|
66
68
|
catch (e) {
|
|
69
|
+
console.error(`[CSPL API] ❌ 请求失败: ${e instanceof Error ? e.message : String(e)}`);
|
|
67
70
|
reject(e);
|
|
68
71
|
}
|
|
69
72
|
});
|
|
70
73
|
});
|
|
71
|
-
req.on("error",
|
|
74
|
+
req.on("error", (error) => {
|
|
75
|
+
console.error(`[CSPL API] ❌ 请求错误: ${error instanceof Error ? error.message : String(error)}`);
|
|
76
|
+
reject(error);
|
|
77
|
+
});
|
|
72
78
|
req.on("timeout", () => {
|
|
79
|
+
console.error(`[CSPL API] ⏰ 请求超时 (${config.api.timeout}ms)`);
|
|
73
80
|
req.destroy();
|
|
74
81
|
reject(new Error("[CSPL] Request timeout"));
|
|
75
82
|
});
|
|
@@ -25,7 +25,7 @@ export declare const MIN_TEXT_LENGTH = 0;
|
|
|
25
25
|
export declare const MAX_TEXT_LENGTH = 4096;
|
|
26
26
|
export declare const MAX_TOTAL_LENGTH = 40960;
|
|
27
27
|
export declare const regex: RegExp;
|
|
28
|
-
export declare const DEFAULT_HTTP_PORT =
|
|
28
|
+
export declare const DEFAULT_HTTP_PORT = 443;
|
|
29
29
|
export declare const HTTP_STATUS_BAD_REQUEST = 400;
|
|
30
30
|
export declare const ENV_FILE_PATH = "/home/sandbox/.openclaw/.xiaoyienv";
|
|
31
31
|
export declare const API_URL_SUFFIX = "/celia-claw/v1/rest-api/skill/execute";
|
|
@@ -3,7 +3,7 @@ export const MIN_TEXT_LENGTH = 0;
|
|
|
3
3
|
export const MAX_TEXT_LENGTH = 4096;
|
|
4
4
|
export const MAX_TOTAL_LENGTH = 40960;
|
|
5
5
|
export const regex = /[^\u4e00-\u9fa5a-zA-Z0-9\s\.,!?;:,。!?;:""\'\'()()\[\]【】]/;
|
|
6
|
-
export const DEFAULT_HTTP_PORT =
|
|
6
|
+
export const DEFAULT_HTTP_PORT = 443;
|
|
7
7
|
export const HTTP_STATUS_BAD_REQUEST = 400;
|
|
8
8
|
export const ENV_FILE_PATH = "/home/sandbox/.openclaw/.xiaoyienv";
|
|
9
9
|
export const API_URL_SUFFIX = "/celia-claw/v1/rest-api/skill/execute";
|
package/dist/src/file-upload.js
CHANGED
|
@@ -55,12 +55,10 @@ export class XYFileUploadService {
|
|
|
55
55
|
throw new Error(`Prepare failed: HTTP ${prepareResp.status}`);
|
|
56
56
|
}
|
|
57
57
|
const prepareData = await prepareResp.json();
|
|
58
|
-
console.log(`[XY File Upload] Prepare response:`, JSON.stringify(prepareData, null, 2));
|
|
59
58
|
if (prepareData.code !== "0") {
|
|
60
59
|
throw new Error(`Prepare failed: ${prepareData.desc}`);
|
|
61
60
|
}
|
|
62
61
|
const { objectId, draftId, uploadInfos } = prepareData;
|
|
63
|
-
console.log(`[XY File Upload] Prepare complete: objectId=${objectId}, draftId=${draftId}`);
|
|
64
62
|
// Phase 2: Upload
|
|
65
63
|
console.log(`[XY File Upload] Phase 2: Upload file data`);
|
|
66
64
|
const uploadInfo = uploadInfos[0]; // Single-part upload
|
|
@@ -69,11 +67,8 @@ export class XYFileUploadService {
|
|
|
69
67
|
headers: uploadInfo.headers,
|
|
70
68
|
body: fileBuffer,
|
|
71
69
|
});
|
|
72
|
-
console.log(`[XY File Upload] Upload response status: ${uploadResp.status}, url: ${uploadInfo.url}`);
|
|
73
|
-
console.log(`[XY File Upload] Upload response headers:`, JSON.stringify(Object.fromEntries(uploadResp.headers.entries()), null, 2));
|
|
74
70
|
if (!uploadResp.ok) {
|
|
75
71
|
const uploadErrorText = await uploadResp.text();
|
|
76
|
-
console.log(`[XY File Upload] Upload error response:`, uploadErrorText);
|
|
77
72
|
throw new Error(`Upload failed: HTTP ${uploadResp.status}`);
|
|
78
73
|
}
|
|
79
74
|
console.log(`[XY File Upload] Upload complete`);
|
|
@@ -96,7 +91,6 @@ export class XYFileUploadService {
|
|
|
96
91
|
throw new Error(`Complete failed: HTTP ${completeResp.status}`);
|
|
97
92
|
}
|
|
98
93
|
const completeData = await completeResp.json();
|
|
99
|
-
console.log(`[XY File Upload] Complete response:`, JSON.stringify(completeData, null, 2));
|
|
100
94
|
console.log(`[XY File Upload] File upload successful: ${fileName} → objectId=${objectId}`);
|
|
101
95
|
return objectId;
|
|
102
96
|
}
|
|
@@ -110,7 +104,6 @@ export class XYFileUploadService {
|
|
|
110
104
|
* Uses completeAndQuery endpoint to get the file URL directly.
|
|
111
105
|
*/
|
|
112
106
|
async uploadFileAndGetUrl(filePath, objectType = "TEMPORARY_MATERIAL_DOC") {
|
|
113
|
-
console.log(`[XY File Upload] Starting file upload with URL retrieval: ${filePath}`);
|
|
114
107
|
try {
|
|
115
108
|
// Read file
|
|
116
109
|
const fileBuffer = await fs.readFile(filePath);
|
|
@@ -143,7 +136,6 @@ export class XYFileUploadService {
|
|
|
143
136
|
throw new Error(`Prepare failed: HTTP ${prepareResp.status}`);
|
|
144
137
|
}
|
|
145
138
|
const prepareData = await prepareResp.json();
|
|
146
|
-
console.log(`[XY File Upload] Prepare response:`, JSON.stringify(prepareData, null, 2));
|
|
147
139
|
if (prepareData.code !== "0") {
|
|
148
140
|
throw new Error(`Prepare failed: ${prepareData.desc}`);
|
|
149
141
|
}
|
|
@@ -160,7 +152,6 @@ export class XYFileUploadService {
|
|
|
160
152
|
console.log(`[XY File Upload] Upload response status: ${uploadResp.status}`);
|
|
161
153
|
if (!uploadResp.ok) {
|
|
162
154
|
const uploadErrorText = await uploadResp.text();
|
|
163
|
-
console.log(`[XY File Upload] Upload error response:`, uploadErrorText);
|
|
164
155
|
throw new Error(`Upload failed: HTTP ${uploadResp.status}`);
|
|
165
156
|
}
|
|
166
157
|
console.log(`[XY File Upload] Upload complete`);
|
|
@@ -183,13 +174,12 @@ export class XYFileUploadService {
|
|
|
183
174
|
throw new Error(`CompleteAndQuery failed: HTTP ${completeResp.status}`);
|
|
184
175
|
}
|
|
185
176
|
const completeData = await completeResp.json();
|
|
186
|
-
console.log(`[XY File Upload] CompleteAndQuery response:`, JSON.stringify(completeData, null, 2));
|
|
187
177
|
// Extract file URL from response
|
|
188
178
|
const fileUrl = completeData?.fileDetailInfo?.url || "";
|
|
189
179
|
if (!fileUrl) {
|
|
190
180
|
throw new Error("No file URL returned from completeAndQuery");
|
|
191
181
|
}
|
|
192
|
-
console.log(`[XY File Upload] File upload successful
|
|
182
|
+
console.log(`[XY File Upload] File upload successful`);
|
|
193
183
|
return fileUrl;
|
|
194
184
|
}
|
|
195
185
|
catch (error) {
|
package/dist/src/formatter.js
CHANGED
|
@@ -55,7 +55,7 @@ export async function sendA2AResponse(params) {
|
|
|
55
55
|
log(`[A2A_RESPONSE] 📤 Sending A2A artifact-update response: taskId: ${taskId}`);
|
|
56
56
|
log(`[A2A_RESPONSE] - append: ${append}`);
|
|
57
57
|
log(`[A2A_RESPONSE] - final: ${final}`);
|
|
58
|
-
log(`[A2A_RESPONSE] - text: ${text}`);
|
|
58
|
+
log(`[A2A_RESPONSE] - text: ${text.length <= 10 ? text : text.slice(0, 5) + '***' + text.slice(-5)}`);
|
|
59
59
|
log(`[A2A_RESPONSE] - files count: ${files?.length ?? 0}`);
|
|
60
60
|
await wsManager.sendMessage(sessionId, outboundMessage);
|
|
61
61
|
log(`[A2A_RESPONSE] ✅ Message sent successfully`);
|
|
@@ -146,11 +146,8 @@ export async function sendStatusUpdate(params) {
|
|
|
146
146
|
// 📋 Log complete response body
|
|
147
147
|
log(`[A2A_STATUS] 📤 Sending A2A status-update:`);
|
|
148
148
|
log(`[A2A_STATUS] - taskId: ${taskId}`);
|
|
149
|
-
log(`[A2A_STATUS] - messageId: ${messageId}`);
|
|
150
|
-
log(`[A2A_STATUS] - state: ${state}`);
|
|
151
149
|
log(`[A2A_STATUS] - text: "${text}"`);
|
|
152
150
|
await wsManager.sendMessage(sessionId, outboundMessage);
|
|
153
|
-
log(`[A2A_STATUS] ✅ Status update sent successfully`);
|
|
154
151
|
}
|
|
155
152
|
/**
|
|
156
153
|
* Send a command as an artifact update (final=false).
|
package/dist/src/monitor.js
CHANGED
|
@@ -67,7 +67,7 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
67
67
|
// Event handlers (defined early so they can be referenced in cleanup)
|
|
68
68
|
const messageHandler = (message, sessionId, serverId) => {
|
|
69
69
|
const messageKey = `${sessionId}::${message.id}`;
|
|
70
|
-
log(`[MONITOR-HANDLER] ####### messageHandler triggered:
|
|
70
|
+
log(`[MONITOR-HANDLER] ####### messageHandler triggered: sessionId=${sessionId}, messageId=${message.id} #######`);
|
|
71
71
|
// ✅ Report health: received a message
|
|
72
72
|
trackEvent?.();
|
|
73
73
|
// Check for duplicate message handling
|
|
@@ -75,17 +75,14 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
75
75
|
error(`[MONITOR-HANDLER] ⚠️ WARNING: Duplicate message detected! messageKey=${messageKey}, this may cause duplicate dispatchers!`);
|
|
76
76
|
}
|
|
77
77
|
activeMessages.add(messageKey);
|
|
78
|
-
log(`[MONITOR-HANDLER] 📝 Active messages count: ${activeMessages.size}, messageKey: ${messageKey}`);
|
|
79
78
|
const task = async () => {
|
|
80
79
|
try {
|
|
81
|
-
log(`[MONITOR-HANDLER] 🚀 Starting handleXYMessage for messageKey=${messageKey}`);
|
|
82
80
|
await handleXYMessage({
|
|
83
81
|
cfg,
|
|
84
82
|
runtime,
|
|
85
83
|
message,
|
|
86
84
|
accountId, // ✅ Pass accountId ("default")
|
|
87
85
|
});
|
|
88
|
-
log(`[MONITOR-HANDLER] ✅ Completed handleXYMessage for messageKey=${messageKey}`);
|
|
89
86
|
}
|
|
90
87
|
catch (err) {
|
|
91
88
|
// ✅ Only log error, don't re-throw to prevent gateway restart
|
|
@@ -94,7 +91,6 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
94
91
|
finally {
|
|
95
92
|
// Remove from active messages when done
|
|
96
93
|
activeMessages.delete(messageKey);
|
|
97
|
-
log(`[MONITOR-HANDLER] 🧹 Cleaned up messageKey=${messageKey}, remaining active: ${activeMessages.size}`);
|
|
98
94
|
}
|
|
99
95
|
};
|
|
100
96
|
// 🔑 核心改造:检测steer模式
|
|
@@ -107,7 +103,6 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
107
103
|
// Steer模式且有活跃任务:不入队列,直接并发执行
|
|
108
104
|
log(`[MONITOR-HANDLER] 🔄 STEER MODE: Executing concurrently for messageKey=${messageKey}`);
|
|
109
105
|
log(`[MONITOR-HANDLER] - sessionId: ${parsed.sessionId}`);
|
|
110
|
-
log(`[MONITOR-HANDLER] - Bypassing queue to allow message insertion`);
|
|
111
106
|
void task().catch((err) => {
|
|
112
107
|
error(`XY gateway: concurrent steer task failed for ${messageKey}: ${String(err)}`);
|
|
113
108
|
activeMessages.delete(messageKey);
|
|
@@ -115,7 +110,6 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
115
110
|
}
|
|
116
111
|
else {
|
|
117
112
|
// 正常模式:入队列串行执行
|
|
118
|
-
log(`[MONITOR-HANDLER] 📋 NORMAL MODE: Enqueuing for messageKey=${messageKey}`);
|
|
119
113
|
void enqueue(sessionId, task).catch((err) => {
|
|
120
114
|
error(`XY gateway: queue processing failed for session ${sessionId}: ${String(err)}`);
|
|
121
115
|
activeMessages.delete(messageKey);
|
|
@@ -207,8 +201,8 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
207
201
|
wsManager.on("disconnected", disconnectedHandler);
|
|
208
202
|
wsManager.on("error", errorHandler);
|
|
209
203
|
wsManager.on("trigger-event", triggerEventHandler);
|
|
210
|
-
// Start periodic health check (every
|
|
211
|
-
console.log("🏥 Starting periodic health check (every
|
|
204
|
+
// Start periodic health check (every 6 hours)
|
|
205
|
+
console.log("🏥 Starting periodic health check (every 6 hours)...");
|
|
212
206
|
healthCheckInterval = setInterval(() => {
|
|
213
207
|
console.log("🏥 [HEALTH CHECK] Periodic WebSocket diagnostics...");
|
|
214
208
|
diagnoseAllManagers();
|
|
@@ -217,7 +211,7 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
217
211
|
if (cleaned > 0) {
|
|
218
212
|
console.log(`🧹 [HEALTH CHECK] Auto-cleaned ${cleaned} manager(s) with orphan connections`);
|
|
219
213
|
}
|
|
220
|
-
},
|
|
214
|
+
}, 6 * 60 * 60 * 1000); // 6 hours
|
|
221
215
|
// Connect to WebSocket servers
|
|
222
216
|
wsManager.connect()
|
|
223
217
|
.then(() => {
|
package/dist/src/onboarding.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import type { ChannelOnboardingAdapter } from "openclaw/plugin-sdk";
|
|
2
1
|
/**
|
|
3
|
-
* XY Channel Onboarding Adapter
|
|
4
|
-
*
|
|
2
|
+
* XY Channel Onboarding Adapter (DISABLED - not currently used)
|
|
3
|
+
* NOTE: This is kept for future reference. Xiaoyi uses simple single-account config.
|
|
5
4
|
*/
|
|
6
|
-
export declare const xyOnboardingAdapter:
|
|
5
|
+
export declare const xyOnboardingAdapter: any;
|
package/dist/src/onboarding.js
CHANGED
|
@@ -152,8 +152,8 @@ async function configure({ cfg, prompter, }) {
|
|
|
152
152
|
};
|
|
153
153
|
}
|
|
154
154
|
/**
|
|
155
|
-
* XY Channel Onboarding Adapter
|
|
156
|
-
*
|
|
155
|
+
* XY Channel Onboarding Adapter (DISABLED - not currently used)
|
|
156
|
+
* NOTE: This is kept for future reference. Xiaoyi uses simple single-account config.
|
|
157
157
|
*/
|
|
158
158
|
export const xyOnboardingAdapter = {
|
|
159
159
|
channel,
|
package/dist/src/outbound.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
type ChannelOutboundAdapter = any;
|
|
2
2
|
/**
|
|
3
3
|
* Outbound adapter for sending messages from OpenClaw to XY.
|
|
4
4
|
* Uses Push service for direct message delivery.
|
|
5
5
|
*/
|
|
6
6
|
export declare const xyOutbound: ChannelOutboundAdapter;
|
|
7
|
+
export {};
|
package/dist/src/outbound.js
CHANGED
|
@@ -90,13 +90,6 @@ export const xyOutbound = {
|
|
|
90
90
|
};
|
|
91
91
|
},
|
|
92
92
|
sendText: async ({ cfg, to, text, accountId }) => {
|
|
93
|
-
// Log parameters
|
|
94
|
-
console.log(`[xyOutbound.sendText] Called with:`, {
|
|
95
|
-
to,
|
|
96
|
-
accountId,
|
|
97
|
-
textLength: text?.length || 0,
|
|
98
|
-
textPreview: text?.slice(0, 100),
|
|
99
|
-
});
|
|
100
93
|
// Resolve configuration
|
|
101
94
|
const config = resolveXYConfig(cfg);
|
|
102
95
|
// Handle default push marker (for cron jobs without explicit target)
|
|
@@ -112,7 +105,7 @@ export const xyOutbound = {
|
|
|
112
105
|
let pushDataId;
|
|
113
106
|
try {
|
|
114
107
|
pushDataId = await savePushData(text);
|
|
115
|
-
console.log(`[xyOutbound.sendText] ✅ Push data saved with ID: ${pushDataId}`);
|
|
108
|
+
console.log(`[xyOutbound.sendText] ✅ Push data saved with ID: ${pushDataId.substring(0, 20)}`);
|
|
116
109
|
}
|
|
117
110
|
catch (error) {
|
|
118
111
|
console.error(`[xyOutbound.sendText] ❌ Failed to save push data:`, error);
|
|
@@ -146,7 +139,6 @@ export const xyOutbound = {
|
|
|
146
139
|
let failureCount = 0;
|
|
147
140
|
for (const pushId of pushIdList) {
|
|
148
141
|
try {
|
|
149
|
-
console.log(`[xyOutbound.sendText] Sending to pushId: ${pushId.substring(0, 20)}...`);
|
|
150
142
|
// 传入 pushId 和 pushDataId,使用 kind="data" 格式
|
|
151
143
|
await pushService.sendPush(pushText, title, undefined, actualTo, pushDataId, pushId);
|
|
152
144
|
successCount++;
|
|
@@ -158,8 +150,6 @@ export const xyOutbound = {
|
|
|
158
150
|
// 单个 pushId 发送失败不影响其他,继续处理下一个
|
|
159
151
|
}
|
|
160
152
|
}
|
|
161
|
-
console.log(`[xyOutbound.sendText] 📊 Broadcast summary: ${successCount} success, ${failureCount} failures`);
|
|
162
|
-
console.log(`[xyOutbound.sendText] Completed successfully`);
|
|
163
153
|
// Return message info
|
|
164
154
|
return {
|
|
165
155
|
channel: "xiaoyi-channel",
|
|
@@ -168,14 +158,6 @@ export const xyOutbound = {
|
|
|
168
158
|
};
|
|
169
159
|
},
|
|
170
160
|
sendMedia: async ({ cfg, to, text, mediaUrl, accountId, mediaLocalRoots }) => {
|
|
171
|
-
// Log parameters
|
|
172
|
-
console.log(`[xyOutbound.sendMedia] Called with:`, {
|
|
173
|
-
to,
|
|
174
|
-
accountId,
|
|
175
|
-
text,
|
|
176
|
-
mediaUrl,
|
|
177
|
-
mediaLocalRoots,
|
|
178
|
-
});
|
|
179
161
|
// Parse to: "sessionId::taskId"
|
|
180
162
|
const parts = to.split("::");
|
|
181
163
|
if (parts.length !== 2) {
|
package/dist/src/push.js
CHANGED
|
@@ -34,15 +34,7 @@ export class XYPushService {
|
|
|
34
34
|
// Use provided pushId or fall back to config pushId
|
|
35
35
|
const actualPushId = pushId || this.config.pushId;
|
|
36
36
|
console.log(`[PUSH] 📤 Preparing to send push message`);
|
|
37
|
-
console.log(`[PUSH] - Title: "${title}"`);
|
|
38
|
-
console.log(`[PUSH] - Content length: ${content.length} chars`);
|
|
39
|
-
console.log(`[PUSH] - Session ID: ${sessionId || 'none'}`);
|
|
40
|
-
console.log(`[PUSH] - Trace ID: ${traceId}`);
|
|
41
|
-
console.log(`[PUSH] - Push URL: ${pushUrl}`);
|
|
42
37
|
console.log(`[PUSH] - Using pushId: ${actualPushId.substring(0, 20)}...`);
|
|
43
|
-
console.log(`[PUSH] - Full pushId: ${actualPushId}`);
|
|
44
|
-
console.log(`[PUSH] - API ID: ${this.config.apiId}`);
|
|
45
|
-
console.log(`[PUSH] - UID: ${this.config.uid}`);
|
|
46
38
|
try {
|
|
47
39
|
const requestBody = {
|
|
48
40
|
jsonrpc: "2.0",
|
|
@@ -75,7 +67,6 @@ export class XYPushService {
|
|
|
75
67
|
],
|
|
76
68
|
},
|
|
77
69
|
};
|
|
78
|
-
console.log(`[PUSH] Full request body:`, JSON.stringify(requestBody, null, 2));
|
|
79
70
|
const response = await fetch(pushUrl, {
|
|
80
71
|
method: "POST",
|
|
81
72
|
headers: {
|
|
@@ -91,21 +82,16 @@ export class XYPushService {
|
|
|
91
82
|
// Log response status and headers
|
|
92
83
|
console.log(`[PUSH] 📥 Response received`);
|
|
93
84
|
console.log(`[PUSH] - HTTP Status: ${response.status} ${response.statusText}`);
|
|
94
|
-
console.log(`[PUSH] - Content-Type: ${response.headers.get('content-type')}`);
|
|
95
|
-
console.log(`[PUSH] - Content-Length: ${response.headers.get('content-length')}`);
|
|
96
85
|
if (!response.ok) {
|
|
97
86
|
const errorText = await response.text();
|
|
98
87
|
console.log(`[PUSH] ❌ Push request failed`);
|
|
99
88
|
console.log(`[PUSH] - HTTP Status: ${response.status}`);
|
|
100
|
-
console.log(`[PUSH] - Response body: ${errorText}`);
|
|
101
89
|
throw new Error(`Push failed: HTTP ${response.status} - ${errorText}`);
|
|
102
90
|
}
|
|
103
91
|
// Try to parse JSON response with detailed error handling
|
|
104
92
|
let result;
|
|
105
93
|
try {
|
|
106
94
|
const responseText = await response.text();
|
|
107
|
-
console.log(`[PUSH] 📄 Response body length: ${responseText.length} chars`);
|
|
108
|
-
console.log(`[PUSH] 📄 Response body preview: ${responseText.substring(0, 200)}`);
|
|
109
95
|
if (!responseText || responseText.trim() === '') {
|
|
110
96
|
console.log(`[PUSH] ⚠️ Received empty response body`);
|
|
111
97
|
result = {};
|
|
@@ -120,20 +106,13 @@ export class XYPushService {
|
|
|
120
106
|
throw new Error(`Invalid JSON response from push service: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
|
|
121
107
|
}
|
|
122
108
|
console.log(`[PUSH] ✅ Push message sent successfully`);
|
|
123
|
-
console.log(`[PUSH] - Title: "${title}"`);
|
|
124
109
|
console.log(`[PUSH] - Trace ID: ${traceId}`);
|
|
125
|
-
console.log(`[PUSH] - Used pushId: ${actualPushId.substring(0, 20)}...`);
|
|
126
|
-
console.log(`[PUSH] - Response:`, result);
|
|
127
110
|
}
|
|
128
111
|
catch (error) {
|
|
129
112
|
console.log(`[PUSH] ❌ Failed to send push message`);
|
|
130
|
-
console.log(`[PUSH] - Trace ID: ${traceId}`);
|
|
131
|
-
console.log(`[PUSH] - Target URL: ${pushUrl}`);
|
|
132
|
-
console.log(`[PUSH] - Push ID: ${actualPushId.substring(0, 20)}...`);
|
|
133
113
|
if (error instanceof Error) {
|
|
134
114
|
console.log(`[PUSH] - Error name: ${error.name}`);
|
|
135
115
|
console.log(`[PUSH] - Error message: ${error.message}`);
|
|
136
|
-
console.log(`[PUSH] - Error stack:`, error.stack);
|
|
137
116
|
}
|
|
138
117
|
else {
|
|
139
118
|
console.log(`[PUSH] - Error:`, error);
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { createReplyPrefixContext } from "openclaw/plugin-sdk";
|
|
2
1
|
import { getXYRuntime } from "./runtime.js";
|
|
3
2
|
import { sendA2AResponse, sendStatusUpdate, sendReasoningTextUpdate } from "./formatter.js";
|
|
4
3
|
import { resolveXYConfig } from "./config.js";
|
|
@@ -44,9 +43,7 @@ export function createXYReplyDispatcher(params) {
|
|
|
44
43
|
const log = runtime?.log ?? console.log;
|
|
45
44
|
const error = runtime?.error ?? console.error;
|
|
46
45
|
log(`[DISPATCHER-CREATE] ******* Creating dispatcher *******`);
|
|
47
|
-
log(`[DISPATCHER-CREATE] - sessionId: ${sessionId}`);
|
|
48
46
|
log(`[DISPATCHER-CREATE] - taskId: ${taskId}`);
|
|
49
|
-
log(`[DISPATCHER-CREATE] - messageId: ${messageId}`);
|
|
50
47
|
log(`[DISPATCHER-CREATE] - isSteerFollower: ${isSteerFollower ?? false}`);
|
|
51
48
|
// 初始taskId和messageId(作为fallback)
|
|
52
49
|
const initialTaskId = taskId;
|
|
@@ -63,7 +60,12 @@ export function createXYReplyDispatcher(params) {
|
|
|
63
60
|
};
|
|
64
61
|
const core = getXYRuntime();
|
|
65
62
|
const config = resolveXYConfig(cfg);
|
|
66
|
-
|
|
63
|
+
// Simplified prefix context for single-account Xiaoyi channel
|
|
64
|
+
const prefixContext = {
|
|
65
|
+
responsePrefix: undefined,
|
|
66
|
+
responsePrefixContextProvider: undefined,
|
|
67
|
+
onModelSelected: undefined,
|
|
68
|
+
};
|
|
67
69
|
let statusUpdateInterval = null;
|
|
68
70
|
let hasSentResponse = false;
|
|
69
71
|
let finalSent = false;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
|
2
|
+
import { type BindingTargetKind } from "openclaw/plugin-sdk/conversation-runtime";
|
|
3
|
+
type XYBindingTargetKind = "subagent" | "session";
|
|
4
|
+
/**
|
|
5
|
+
* Xiaoyi thread binding record.
|
|
6
|
+
* Simplified from feishu - uses sessionId as conversationId, no parentConversationId.
|
|
7
|
+
*/
|
|
8
|
+
type XYThreadBindingRecord = {
|
|
9
|
+
accountId: string;
|
|
10
|
+
sessionId: string;
|
|
11
|
+
targetKind: XYBindingTargetKind;
|
|
12
|
+
targetSessionKey: string;
|
|
13
|
+
agentId?: string;
|
|
14
|
+
boundAt: number;
|
|
15
|
+
lastActivityAt: number;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Thread binding manager for Xiaoyi channel.
|
|
19
|
+
* Manages session bindings for single-account mode.
|
|
20
|
+
*/
|
|
21
|
+
type XYThreadBindingManager = {
|
|
22
|
+
accountId: string;
|
|
23
|
+
getBySessionId: (sessionId: string) => XYThreadBindingRecord | undefined;
|
|
24
|
+
listBySessionKey: (targetSessionKey: string) => XYThreadBindingRecord[];
|
|
25
|
+
bindSession: (params: {
|
|
26
|
+
sessionId: string;
|
|
27
|
+
targetKind: BindingTargetKind;
|
|
28
|
+
targetSessionKey: string;
|
|
29
|
+
metadata?: Record<string, unknown>;
|
|
30
|
+
}) => XYThreadBindingRecord | null;
|
|
31
|
+
touchSession: (sessionId: string, at?: number) => XYThreadBindingRecord | null;
|
|
32
|
+
unbindSession: (sessionId: string) => XYThreadBindingRecord | null;
|
|
33
|
+
unbindBySessionKey: (targetSessionKey: string) => XYThreadBindingRecord[];
|
|
34
|
+
stop: () => void;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Creates a thread binding manager for Xiaoyi channel.
|
|
38
|
+
* Based on feishu implementation but simplified for single-account mode.
|
|
39
|
+
*/
|
|
40
|
+
export declare function createXYThreadBindingManager(params: {
|
|
41
|
+
accountId?: string;
|
|
42
|
+
cfg: OpenClawConfig;
|
|
43
|
+
}): XYThreadBindingManager;
|
|
44
|
+
/**
|
|
45
|
+
* Gets the thread binding manager for a given account ID.
|
|
46
|
+
*/
|
|
47
|
+
export declare function getXYThreadBindingManager(accountId?: string): XYThreadBindingManager | null;
|
|
48
|
+
/**
|
|
49
|
+
* Testing utilities for thread bindings.
|
|
50
|
+
*/
|
|
51
|
+
export declare const __testing: {
|
|
52
|
+
resetXYThreadBindingsForTests(): void;
|
|
53
|
+
};
|
|
54
|
+
export {};
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { resolveThreadBindingIdleTimeoutMsForChannel, resolveThreadBindingMaxAgeMsForChannel, registerSessionBindingAdapter, resolveThreadBindingConversationIdFromBindingId, unregisterSessionBindingAdapter, } from "openclaw/plugin-sdk/conversation-runtime";
|
|
2
|
+
import { normalizeAccountId, resolveAgentIdFromSessionKey } from "openclaw/plugin-sdk/routing";
|
|
3
|
+
import { resolveGlobalSingleton } from "openclaw/plugin-sdk/text-runtime";
|
|
4
|
+
const XY_THREAD_BINDINGS_STATE_KEY = Symbol.for("openclaw.xyThreadBindingsState");
|
|
5
|
+
const state = resolveGlobalSingleton(XY_THREAD_BINDINGS_STATE_KEY, () => ({
|
|
6
|
+
managersByAccountId: new Map(),
|
|
7
|
+
bindingsByAccountSession: new Map(),
|
|
8
|
+
}));
|
|
9
|
+
function getState() {
|
|
10
|
+
return state;
|
|
11
|
+
}
|
|
12
|
+
function resolveBindingKey(params) {
|
|
13
|
+
return `${params.accountId}:${params.sessionId}`;
|
|
14
|
+
}
|
|
15
|
+
function toSessionBindingTargetKind(raw) {
|
|
16
|
+
return raw === "subagent" ? "subagent" : "session";
|
|
17
|
+
}
|
|
18
|
+
function toXYTargetKind(raw) {
|
|
19
|
+
return raw === "subagent" ? "subagent" : "session";
|
|
20
|
+
}
|
|
21
|
+
function toSessionBindingRecord(record, defaults) {
|
|
22
|
+
const idleExpiresAt = defaults.idleTimeoutMs > 0 ? record.lastActivityAt + defaults.idleTimeoutMs : undefined;
|
|
23
|
+
const maxAgeExpiresAt = defaults.maxAgeMs > 0 ? record.boundAt + defaults.maxAgeMs : undefined;
|
|
24
|
+
const expiresAt = idleExpiresAt != null && maxAgeExpiresAt != null
|
|
25
|
+
? Math.min(idleExpiresAt, maxAgeExpiresAt)
|
|
26
|
+
: (idleExpiresAt ?? maxAgeExpiresAt);
|
|
27
|
+
return {
|
|
28
|
+
bindingId: resolveBindingKey({
|
|
29
|
+
accountId: record.accountId,
|
|
30
|
+
sessionId: record.sessionId,
|
|
31
|
+
}),
|
|
32
|
+
targetSessionKey: record.targetSessionKey,
|
|
33
|
+
targetKind: toSessionBindingTargetKind(record.targetKind),
|
|
34
|
+
conversation: {
|
|
35
|
+
channel: "xiaoyi-channel",
|
|
36
|
+
accountId: record.accountId,
|
|
37
|
+
conversationId: record.sessionId, // sessionId is the conversationId for Xiaoyi
|
|
38
|
+
parentConversationId: undefined, // Xiaoyi doesn't have parent conversations
|
|
39
|
+
},
|
|
40
|
+
status: "active",
|
|
41
|
+
boundAt: record.boundAt,
|
|
42
|
+
expiresAt,
|
|
43
|
+
metadata: {
|
|
44
|
+
agentId: record.agentId,
|
|
45
|
+
lastActivityAt: record.lastActivityAt,
|
|
46
|
+
idleTimeoutMs: defaults.idleTimeoutMs,
|
|
47
|
+
maxAgeMs: defaults.maxAgeMs,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Creates a thread binding manager for Xiaoyi channel.
|
|
53
|
+
* Based on feishu implementation but simplified for single-account mode.
|
|
54
|
+
*/
|
|
55
|
+
export function createXYThreadBindingManager(params) {
|
|
56
|
+
const accountId = normalizeAccountId(params.accountId);
|
|
57
|
+
const existing = getState().managersByAccountId.get(accountId);
|
|
58
|
+
if (existing) {
|
|
59
|
+
return existing;
|
|
60
|
+
}
|
|
61
|
+
const idleTimeoutMs = resolveThreadBindingIdleTimeoutMsForChannel({
|
|
62
|
+
cfg: params.cfg,
|
|
63
|
+
channel: "xiaoyi-channel",
|
|
64
|
+
accountId,
|
|
65
|
+
});
|
|
66
|
+
const maxAgeMs = resolveThreadBindingMaxAgeMsForChannel({
|
|
67
|
+
cfg: params.cfg,
|
|
68
|
+
channel: "xiaoyi-channel",
|
|
69
|
+
accountId,
|
|
70
|
+
});
|
|
71
|
+
const manager = {
|
|
72
|
+
accountId,
|
|
73
|
+
getBySessionId: (sessionId) => getState().bindingsByAccountSession.get(resolveBindingKey({ accountId, sessionId })),
|
|
74
|
+
listBySessionKey: (targetSessionKey) => [...getState().bindingsByAccountSession.values()].filter((record) => record.accountId === accountId && record.targetSessionKey === targetSessionKey),
|
|
75
|
+
bindSession: ({ sessionId, targetKind, targetSessionKey, metadata, }) => {
|
|
76
|
+
const normalizedSessionId = sessionId.trim();
|
|
77
|
+
if (!normalizedSessionId || !targetSessionKey.trim()) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
const record = {
|
|
82
|
+
accountId,
|
|
83
|
+
sessionId: normalizedSessionId,
|
|
84
|
+
targetKind: toXYTargetKind(targetKind),
|
|
85
|
+
targetSessionKey: targetSessionKey.trim(),
|
|
86
|
+
agentId: typeof metadata?.agentId === "string" && metadata.agentId.trim()
|
|
87
|
+
? metadata.agentId.trim()
|
|
88
|
+
: resolveAgentIdFromSessionKey(targetSessionKey),
|
|
89
|
+
boundAt: now,
|
|
90
|
+
lastActivityAt: now,
|
|
91
|
+
};
|
|
92
|
+
getState().bindingsByAccountSession.set(resolveBindingKey({ accountId, sessionId: normalizedSessionId }), record);
|
|
93
|
+
return record;
|
|
94
|
+
},
|
|
95
|
+
touchSession: (sessionId, at = Date.now()) => {
|
|
96
|
+
const key = resolveBindingKey({ accountId, sessionId });
|
|
97
|
+
const existingRecord = getState().bindingsByAccountSession.get(key);
|
|
98
|
+
if (!existingRecord) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
const updated = { ...existingRecord, lastActivityAt: at };
|
|
102
|
+
getState().bindingsByAccountSession.set(key, updated);
|
|
103
|
+
return updated;
|
|
104
|
+
},
|
|
105
|
+
unbindSession: (sessionId) => {
|
|
106
|
+
const key = resolveBindingKey({ accountId, sessionId });
|
|
107
|
+
const existingRecord = getState().bindingsByAccountSession.get(key);
|
|
108
|
+
if (!existingRecord) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
getState().bindingsByAccountSession.delete(key);
|
|
112
|
+
return existingRecord;
|
|
113
|
+
},
|
|
114
|
+
unbindBySessionKey: (targetSessionKey) => {
|
|
115
|
+
const removed = [];
|
|
116
|
+
for (const record of [...getState().bindingsByAccountSession.values()]) {
|
|
117
|
+
if (record.accountId !== accountId || record.targetSessionKey !== targetSessionKey) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
getState().bindingsByAccountSession.delete(resolveBindingKey({ accountId, sessionId: record.sessionId }));
|
|
121
|
+
removed.push(record);
|
|
122
|
+
}
|
|
123
|
+
return removed;
|
|
124
|
+
},
|
|
125
|
+
stop: () => {
|
|
126
|
+
for (const key of [...getState().bindingsByAccountSession.keys()]) {
|
|
127
|
+
if (key.startsWith(`${accountId}:`)) {
|
|
128
|
+
getState().bindingsByAccountSession.delete(key);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
getState().managersByAccountId.delete(accountId);
|
|
132
|
+
unregisterSessionBindingAdapter({
|
|
133
|
+
channel: "xiaoyi-channel",
|
|
134
|
+
accountId,
|
|
135
|
+
adapter: sessionBindingAdapter,
|
|
136
|
+
});
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
const sessionBindingAdapter = {
|
|
140
|
+
channel: "xiaoyi-channel",
|
|
141
|
+
accountId,
|
|
142
|
+
capabilities: {
|
|
143
|
+
placements: ["current"],
|
|
144
|
+
},
|
|
145
|
+
bind: async (input) => {
|
|
146
|
+
if (input.conversation.channel !== "xiaoyi-channel" || input.placement === "child") {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
const bound = manager.bindSession({
|
|
150
|
+
sessionId: input.conversation.conversationId,
|
|
151
|
+
targetKind: input.targetKind,
|
|
152
|
+
targetSessionKey: input.targetSessionKey,
|
|
153
|
+
metadata: input.metadata,
|
|
154
|
+
});
|
|
155
|
+
return bound ? toSessionBindingRecord(bound, { idleTimeoutMs, maxAgeMs }) : null;
|
|
156
|
+
},
|
|
157
|
+
listBySession: (targetSessionKey) => manager
|
|
158
|
+
.listBySessionKey(targetSessionKey)
|
|
159
|
+
.map((entry) => toSessionBindingRecord(entry, { idleTimeoutMs, maxAgeMs })),
|
|
160
|
+
resolveByConversation: (ref) => {
|
|
161
|
+
if (ref.channel !== "xiaoyi-channel") {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
const found = manager.getBySessionId(ref.conversationId);
|
|
165
|
+
return found ? toSessionBindingRecord(found, { idleTimeoutMs, maxAgeMs }) : null;
|
|
166
|
+
},
|
|
167
|
+
touch: (bindingId, at) => {
|
|
168
|
+
const sessionId = resolveThreadBindingConversationIdFromBindingId({
|
|
169
|
+
accountId,
|
|
170
|
+
bindingId,
|
|
171
|
+
});
|
|
172
|
+
if (sessionId) {
|
|
173
|
+
manager.touchSession(sessionId, at);
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
unbind: async (input) => {
|
|
177
|
+
if (input.targetSessionKey?.trim()) {
|
|
178
|
+
return manager
|
|
179
|
+
.unbindBySessionKey(input.targetSessionKey.trim())
|
|
180
|
+
.map((entry) => toSessionBindingRecord(entry, { idleTimeoutMs, maxAgeMs }));
|
|
181
|
+
}
|
|
182
|
+
const sessionId = resolveThreadBindingConversationIdFromBindingId({
|
|
183
|
+
accountId,
|
|
184
|
+
bindingId: input.bindingId,
|
|
185
|
+
});
|
|
186
|
+
if (!sessionId) {
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
const removed = manager.unbindSession(sessionId);
|
|
190
|
+
return removed ? [toSessionBindingRecord(removed, { idleTimeoutMs, maxAgeMs })] : [];
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
registerSessionBindingAdapter(sessionBindingAdapter);
|
|
194
|
+
getState().managersByAccountId.set(accountId, manager);
|
|
195
|
+
return manager;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Gets the thread binding manager for a given account ID.
|
|
199
|
+
*/
|
|
200
|
+
export function getXYThreadBindingManager(accountId) {
|
|
201
|
+
return getState().managersByAccountId.get(normalizeAccountId(accountId)) ?? null;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Testing utilities for thread bindings.
|
|
205
|
+
*/
|
|
206
|
+
export const __testing = {
|
|
207
|
+
resetXYThreadBindingsForTests() {
|
|
208
|
+
for (const manager of getState().managersByAccountId.values()) {
|
|
209
|
+
manager.stop();
|
|
210
|
+
}
|
|
211
|
+
getState().managersByAccountId.clear();
|
|
212
|
+
getState().bindingsByAccountSession.clear();
|
|
213
|
+
},
|
|
214
|
+
};
|
package/dist/src/websocket.js
CHANGED
|
@@ -318,30 +318,37 @@ export class XYWebSocketManager extends EventEmitter {
|
|
|
318
318
|
* Handle incoming message from server.
|
|
319
319
|
*/
|
|
320
320
|
handleMessage(data) {
|
|
321
|
-
console.log("[WEBSOCKET-HANDLE] >>>>>>> Receiving message... <<<<<<<");
|
|
322
321
|
try {
|
|
323
322
|
const messageStr = data.toString();
|
|
324
|
-
console.log(`[WS-RECV] Raw message frame, size: ${messageStr.length}
|
|
323
|
+
console.log(`[WS-RECV] Raw message frame, size: ${messageStr.length} characters`);
|
|
325
324
|
const parsed = JSON.parse(messageStr);
|
|
326
325
|
// 提取并打印消息内容(只显示 text,data 只打印提示)
|
|
327
326
|
const parts = parsed.params?.message?.parts;
|
|
328
327
|
if (parts && Array.isArray(parts) && parts.length > 0) {
|
|
329
328
|
const textParts = parts.filter((p) => p?.kind === "text");
|
|
330
329
|
const dataParts = parts.filter((p) => p?.kind === "data");
|
|
331
|
-
// 打印 text
|
|
330
|
+
// 打印 text 内容(隐藏敏感信息)
|
|
332
331
|
if (textParts.length > 0) {
|
|
333
332
|
const textContents = textParts
|
|
334
333
|
.map((p) => p?.text || "")
|
|
335
334
|
.filter((text) => text.length > 0)
|
|
336
335
|
.join(" ");
|
|
337
336
|
if (textContents.length > 0) {
|
|
338
|
-
|
|
337
|
+
// 隐藏中间内容,只保留前后各5个字符
|
|
338
|
+
let maskedText;
|
|
339
|
+
if (textContents.length <= 8) {
|
|
340
|
+
// 如果长度 <= 8,显示前2个 + *** + 后2个
|
|
341
|
+
maskedText = textContents.length >= 4
|
|
342
|
+
? `${textContents.slice(0, 2)}***${textContents.slice(-2)}`
|
|
343
|
+
: `${textContents.slice(0, 1)}***${textContents.slice(-1)}`;
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
// 如果长度 > 8,显示前5个 + *** + 后5个
|
|
347
|
+
maskedText = `${textContents.slice(0, 5)}***${textContents.slice(-5)}`;
|
|
348
|
+
}
|
|
349
|
+
console.log("[WS-RECV] Text:", maskedText);
|
|
339
350
|
}
|
|
340
351
|
}
|
|
341
|
-
// 打印 data 提示
|
|
342
|
-
if (dataParts.length > 0) {
|
|
343
|
-
console.log("[WS-RECV] Data: received data message(s)");
|
|
344
|
-
}
|
|
345
352
|
}
|
|
346
353
|
// Check if message is in direct A2A JSON-RPC format (server push)
|
|
347
354
|
if (parsed.jsonrpc === "2.0") {
|
|
@@ -392,7 +399,6 @@ export class XYWebSocketManager extends EventEmitter {
|
|
|
392
399
|
return;
|
|
393
400
|
}
|
|
394
401
|
// Emit message event for non-data-only messages
|
|
395
|
-
console.log("[XY] *** EMITTING message event (Direct A2A path) ***");
|
|
396
402
|
this.emit("message", a2aRequest, sessionId);
|
|
397
403
|
return;
|
|
398
404
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ynhcj/xiaoyi-channel",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.54-beta",
|
|
4
4
|
"description": "OpenClaw Xiaoyi Channel plugin - Xiaoyi A2A protocol integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
}
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
|
-
"openclaw": ">=2026.3.
|
|
52
|
+
"openclaw": ">=2026.3.24"
|
|
53
53
|
},
|
|
54
54
|
"peerDependenciesMeta": {
|
|
55
55
|
"openclaw": {
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* XY search photo tool - searches photos in user's gallery.
|
|
3
|
-
* Returns publicly accessible URLs of matching photos based on query description.
|
|
4
|
-
*
|
|
5
|
-
* This tool performs a two-step operation:
|
|
6
|
-
* 1. Search for photos using query description
|
|
7
|
-
* 2. Upload found photos to get publicly accessible URLs
|
|
8
|
-
*/
|
|
9
|
-
export declare const searchPhotoTool: any;
|
|
@@ -1,270 +0,0 @@
|
|
|
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 photo tool - searches photos in user's gallery.
|
|
7
|
-
* Returns publicly accessible URLs of matching photos based on query description.
|
|
8
|
-
*
|
|
9
|
-
* This tool performs a two-step operation:
|
|
10
|
-
* 1. Search for photos using query description
|
|
11
|
-
* 2. Upload found photos to get publicly accessible URLs
|
|
12
|
-
*/
|
|
13
|
-
export const searchPhotoTool = {
|
|
14
|
-
name: "search_photo",
|
|
15
|
-
label: "Search Photo",
|
|
16
|
-
description: "搜索用户手机图库中的照片。根据图像描述语料检索匹配的照片,并返回照片的可公网访问URL。注意:操作超时时间为120秒,请勿重复调用此工具,如果超时或失败,最多重试一次。",
|
|
17
|
-
parameters: {
|
|
18
|
-
type: "object",
|
|
19
|
-
properties: {
|
|
20
|
-
query: {
|
|
21
|
-
type: "string",
|
|
22
|
-
description: "图像描述语料,用于检索匹配的照片(例如:'小狗的照片'、'带有键盘的图片'等)",
|
|
23
|
-
},
|
|
24
|
-
},
|
|
25
|
-
required: ["query"],
|
|
26
|
-
},
|
|
27
|
-
async execute(toolCallId, params) {
|
|
28
|
-
logger.log(`[SEARCH_PHOTO_TOOL] 🚀 Starting execution`);
|
|
29
|
-
logger.log(`[SEARCH_PHOTO_TOOL] - toolCallId: ${toolCallId}`);
|
|
30
|
-
logger.log(`[SEARCH_PHOTO_TOOL] - params:`, JSON.stringify(params));
|
|
31
|
-
logger.log(`[SEARCH_PHOTO_TOOL] - timestamp: ${new Date().toISOString()}`);
|
|
32
|
-
// Validate parameters
|
|
33
|
-
if (!params.query) {
|
|
34
|
-
logger.error(`[SEARCH_PHOTO_TOOL] ❌ Missing required parameter: query`);
|
|
35
|
-
throw new Error("Missing required parameter: query is required");
|
|
36
|
-
}
|
|
37
|
-
// Get session context
|
|
38
|
-
logger.log(`[SEARCH_PHOTO_TOOL] 🔍 Attempting to get session context...`);
|
|
39
|
-
const sessionContext = getLatestSessionContext();
|
|
40
|
-
if (!sessionContext) {
|
|
41
|
-
logger.error(`[SEARCH_PHOTO_TOOL] ❌ FAILED: No active session found!`);
|
|
42
|
-
logger.error(`[SEARCH_PHOTO_TOOL] - toolCallId: ${toolCallId}`);
|
|
43
|
-
throw new Error("No active XY session found. Search photo tool can only be used during an active conversation.");
|
|
44
|
-
}
|
|
45
|
-
logger.log(`[SEARCH_PHOTO_TOOL] ✅ Session context found`);
|
|
46
|
-
logger.log(`[SEARCH_PHOTO_TOOL] - sessionId: ${sessionContext.sessionId}`);
|
|
47
|
-
logger.log(`[SEARCH_PHOTO_TOOL] - taskId: ${sessionContext.taskId}`);
|
|
48
|
-
logger.log(`[SEARCH_PHOTO_TOOL] - messageId: ${sessionContext.messageId}`);
|
|
49
|
-
const { config, sessionId, taskId, messageId } = sessionContext;
|
|
50
|
-
// Get WebSocket manager
|
|
51
|
-
logger.log(`[SEARCH_PHOTO_TOOL] 🔌 Getting WebSocket manager...`);
|
|
52
|
-
const wsManager = getXYWebSocketManager(config);
|
|
53
|
-
logger.log(`[SEARCH_PHOTO_TOOL] ✅ WebSocket manager obtained`);
|
|
54
|
-
// Step 1: Search for photos
|
|
55
|
-
logger.log(`[SEARCH_PHOTO_TOOL] 📸 STEP 1: Searching for photos...`);
|
|
56
|
-
const mediaUris = await searchPhotos(wsManager, config, sessionId, taskId, messageId, params.query);
|
|
57
|
-
if (!mediaUris || mediaUris.length === 0) {
|
|
58
|
-
logger.warn(`[SEARCH_PHOTO_TOOL] ⚠️ No photos found for query: ${params.query}`);
|
|
59
|
-
return {
|
|
60
|
-
content: [
|
|
61
|
-
{
|
|
62
|
-
type: "text",
|
|
63
|
-
text: JSON.stringify({ imageUrls: [], message: "未找到匹配的照片" }),
|
|
64
|
-
},
|
|
65
|
-
],
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
logger.log(`[SEARCH_PHOTO_TOOL] ✅ Found ${mediaUris.length} photos`);
|
|
69
|
-
logger.log(`[SEARCH_PHOTO_TOOL] - mediaUris:`, JSON.stringify(mediaUris));
|
|
70
|
-
// Step 2: Get public URLs for the photos
|
|
71
|
-
logger.log(`[SEARCH_PHOTO_TOOL] 🌐 STEP 2: Getting public URLs for photos...`);
|
|
72
|
-
const imageUrls = await getPhotoUrls(wsManager, config, sessionId, taskId, messageId, mediaUris);
|
|
73
|
-
logger.log(`[SEARCH_PHOTO_TOOL] 🎉 Successfully retrieved ${imageUrls.length} photo URLs`);
|
|
74
|
-
return {
|
|
75
|
-
content: [
|
|
76
|
-
{
|
|
77
|
-
type: "text",
|
|
78
|
-
text: JSON.stringify({ imageUrls }),
|
|
79
|
-
},
|
|
80
|
-
],
|
|
81
|
-
};
|
|
82
|
-
},
|
|
83
|
-
};
|
|
84
|
-
/**
|
|
85
|
-
* Step 1: Search for photos using query description
|
|
86
|
-
* Returns array of mediaUri strings
|
|
87
|
-
*/
|
|
88
|
-
async function searchPhotos(wsManager, config, sessionId, taskId, messageId, query) {
|
|
89
|
-
logger.log(`[SEARCH_PHOTO_TOOL] 📦 Building SearchPhotoVideo command...`);
|
|
90
|
-
const command = {
|
|
91
|
-
header: {
|
|
92
|
-
namespace: "Common",
|
|
93
|
-
name: "Action",
|
|
94
|
-
},
|
|
95
|
-
payload: {
|
|
96
|
-
cardParam: {},
|
|
97
|
-
executeParam: {
|
|
98
|
-
executeMode: "background",
|
|
99
|
-
intentName: "SearchPhotoVideo",
|
|
100
|
-
bundleName: "com.huawei.hmos.aidispatchservice",
|
|
101
|
-
needUnlock: true,
|
|
102
|
-
actionResponse: true,
|
|
103
|
-
appType: "OHOS_APP",
|
|
104
|
-
timeOut: 5,
|
|
105
|
-
intentParam: {
|
|
106
|
-
query: query,
|
|
107
|
-
},
|
|
108
|
-
permissionId: [],
|
|
109
|
-
achieveType: "INTENT",
|
|
110
|
-
},
|
|
111
|
-
responses: [
|
|
112
|
-
{
|
|
113
|
-
resultCode: "",
|
|
114
|
-
displayText: "",
|
|
115
|
-
ttsText: "",
|
|
116
|
-
},
|
|
117
|
-
],
|
|
118
|
-
needUploadResult: true,
|
|
119
|
-
noHalfPage: false,
|
|
120
|
-
pageControlRelated: false,
|
|
121
|
-
},
|
|
122
|
-
};
|
|
123
|
-
return new Promise((resolve, reject) => {
|
|
124
|
-
const timeout = setTimeout(() => {
|
|
125
|
-
logger.error(`[SEARCH_PHOTO_TOOL] ⏰ Timeout: No response for SearchPhotoVideo within 60 seconds`);
|
|
126
|
-
wsManager.off("data-event", handler);
|
|
127
|
-
reject(new Error("搜索照片超时(60秒)"));
|
|
128
|
-
}, 60000);
|
|
129
|
-
const handler = (event) => {
|
|
130
|
-
logger.log(`[SEARCH_PHOTO_TOOL] 📨 Received data event (Step 1):`, JSON.stringify(event));
|
|
131
|
-
if (event.intentName === "SearchPhotoVideo") {
|
|
132
|
-
logger.log(`[SEARCH_PHOTO_TOOL] 🎯 SearchPhotoVideo event received`);
|
|
133
|
-
logger.log(`[SEARCH_PHOTO_TOOL] - status: ${event.status}`);
|
|
134
|
-
clearTimeout(timeout);
|
|
135
|
-
wsManager.off("data-event", handler);
|
|
136
|
-
if (event.status === "success" && event.outputs) {
|
|
137
|
-
logger.log(`[SEARCH_PHOTO_TOOL] ✅ Photo search completed successfully`);
|
|
138
|
-
const result = event.outputs.result;
|
|
139
|
-
const items = result?.items || [];
|
|
140
|
-
// Extract mediaUri from each item
|
|
141
|
-
const mediaUris = items.map((item) => item.mediaUri).filter(Boolean);
|
|
142
|
-
logger.log(`[SEARCH_PHOTO_TOOL] 📊 Extracted ${mediaUris.length} mediaUris`);
|
|
143
|
-
resolve(mediaUris);
|
|
144
|
-
}
|
|
145
|
-
else {
|
|
146
|
-
logger.error(`[SEARCH_PHOTO_TOOL] ❌ Photo search failed`);
|
|
147
|
-
logger.error(`[SEARCH_PHOTO_TOOL] - status: ${event.status}`);
|
|
148
|
-
reject(new Error(`搜索照片失败: ${event.status}`));
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
};
|
|
152
|
-
logger.log(`[SEARCH_PHOTO_TOOL] 📡 Registering data-event handler for SearchPhotoVideo`);
|
|
153
|
-
wsManager.on("data-event", handler);
|
|
154
|
-
logger.log(`[SEARCH_PHOTO_TOOL] 📤 Sending SearchPhotoVideo command...`);
|
|
155
|
-
sendCommand({
|
|
156
|
-
config,
|
|
157
|
-
sessionId,
|
|
158
|
-
taskId,
|
|
159
|
-
messageId,
|
|
160
|
-
command,
|
|
161
|
-
})
|
|
162
|
-
.then(() => {
|
|
163
|
-
logger.log(`[SEARCH_PHOTO_TOOL] ✅ SearchPhotoVideo command sent successfully`);
|
|
164
|
-
})
|
|
165
|
-
.catch((error) => {
|
|
166
|
-
logger.error(`[SEARCH_PHOTO_TOOL] ❌ Failed to send SearchPhotoVideo command:`, error);
|
|
167
|
-
clearTimeout(timeout);
|
|
168
|
-
wsManager.off("data-event", handler);
|
|
169
|
-
reject(error);
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* Step 2: Get public URLs for photos using mediaUris
|
|
175
|
-
* Returns array of publicly accessible image URLs
|
|
176
|
-
*/
|
|
177
|
-
async function getPhotoUrls(wsManager, config, sessionId, taskId, messageId, mediaUris) {
|
|
178
|
-
logger.log(`[SEARCH_PHOTO_TOOL] 📦 Building ImageUploadForClaw command...`);
|
|
179
|
-
// Build imageInfos array from mediaUris
|
|
180
|
-
const imageInfos = mediaUris.map(mediaUri => ({ mediaUri }));
|
|
181
|
-
const command = {
|
|
182
|
-
header: {
|
|
183
|
-
namespace: "Common",
|
|
184
|
-
name: "Action",
|
|
185
|
-
},
|
|
186
|
-
payload: {
|
|
187
|
-
cardParam: {},
|
|
188
|
-
executeParam: {
|
|
189
|
-
executeMode: "background",
|
|
190
|
-
intentName: "ImageUploadForClaw",
|
|
191
|
-
bundleName: "com.huawei.hmos.vassistant",
|
|
192
|
-
needUnlock: true,
|
|
193
|
-
actionResponse: true,
|
|
194
|
-
appType: "OHOS_APP",
|
|
195
|
-
timeOut: 5,
|
|
196
|
-
intentParam: {
|
|
197
|
-
imageInfos: imageInfos,
|
|
198
|
-
},
|
|
199
|
-
permissionId: [],
|
|
200
|
-
achieveType: "INTENT",
|
|
201
|
-
},
|
|
202
|
-
responses: [
|
|
203
|
-
{
|
|
204
|
-
resultCode: "",
|
|
205
|
-
displayText: "",
|
|
206
|
-
ttsText: "",
|
|
207
|
-
},
|
|
208
|
-
],
|
|
209
|
-
needUploadResult: true,
|
|
210
|
-
noHalfPage: false,
|
|
211
|
-
pageControlRelated: false,
|
|
212
|
-
},
|
|
213
|
-
};
|
|
214
|
-
return new Promise((resolve, reject) => {
|
|
215
|
-
const timeout = setTimeout(() => {
|
|
216
|
-
logger.error(`[SEARCH_PHOTO_TOOL] ⏰ Timeout: No response for ImageUploadForClaw within 60 seconds`);
|
|
217
|
-
wsManager.off("data-event", handler);
|
|
218
|
-
reject(new Error("获取照片URL超时(60秒)"));
|
|
219
|
-
}, 60000);
|
|
220
|
-
const handler = (event) => {
|
|
221
|
-
logger.log(`[SEARCH_PHOTO_TOOL] 📨 Received data event (Step 2):`, JSON.stringify(event));
|
|
222
|
-
if (event.intentName === "ImageUploadForClaw") {
|
|
223
|
-
logger.log(`[SEARCH_PHOTO_TOOL] 🎯 ImageUploadForClaw event received`);
|
|
224
|
-
logger.log(`[SEARCH_PHOTO_TOOL] - status: ${event.status}`);
|
|
225
|
-
clearTimeout(timeout);
|
|
226
|
-
wsManager.off("data-event", handler);
|
|
227
|
-
if (event.status === "success" && event.outputs) {
|
|
228
|
-
logger.log(`[SEARCH_PHOTO_TOOL] ✅ Image URL retrieval completed successfully`);
|
|
229
|
-
const result = event.outputs.result;
|
|
230
|
-
let imageUrls = result?.imageUrls || [];
|
|
231
|
-
// Decode Unicode escape sequences in URLs
|
|
232
|
-
// Replace \u003d with = and \u0026 with &
|
|
233
|
-
imageUrls = imageUrls.map((url) => {
|
|
234
|
-
const decodedUrl = url
|
|
235
|
-
.replace(/\\u003d/g, '=')
|
|
236
|
-
.replace(/\\u0026/g, '&');
|
|
237
|
-
logger.log(`[SEARCH_PHOTO_TOOL] 🔄 Decoded URL: ${url} -> ${decodedUrl}`);
|
|
238
|
-
return decodedUrl;
|
|
239
|
-
});
|
|
240
|
-
logger.log(`[SEARCH_PHOTO_TOOL] 📊 Retrieved and decoded ${imageUrls.length} image URLs`);
|
|
241
|
-
resolve(imageUrls);
|
|
242
|
-
}
|
|
243
|
-
else {
|
|
244
|
-
logger.error(`[SEARCH_PHOTO_TOOL] ❌ Image URL retrieval failed`);
|
|
245
|
-
logger.error(`[SEARCH_PHOTO_TOOL] - status: ${event.status}`);
|
|
246
|
-
reject(new Error(`获取照片URL失败: ${event.status}`));
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
};
|
|
250
|
-
logger.log(`[SEARCH_PHOTO_TOOL] 📡 Registering data-event handler for ImageUploadForClaw`);
|
|
251
|
-
wsManager.on("data-event", handler);
|
|
252
|
-
logger.log(`[SEARCH_PHOTO_TOOL] 📤 Sending ImageUploadForClaw command...`);
|
|
253
|
-
sendCommand({
|
|
254
|
-
config,
|
|
255
|
-
sessionId,
|
|
256
|
-
taskId,
|
|
257
|
-
messageId,
|
|
258
|
-
command,
|
|
259
|
-
})
|
|
260
|
-
.then(() => {
|
|
261
|
-
logger.log(`[SEARCH_PHOTO_TOOL] ✅ ImageUploadForClaw command sent successfully`);
|
|
262
|
-
})
|
|
263
|
-
.catch((error) => {
|
|
264
|
-
logger.error(`[SEARCH_PHOTO_TOOL] ❌ Failed to send ImageUploadForClaw command:`, error);
|
|
265
|
-
clearTimeout(timeout);
|
|
266
|
-
wsManager.off("data-event", handler);
|
|
267
|
-
reject(error);
|
|
268
|
-
});
|
|
269
|
-
});
|
|
270
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import type { SessionBinding, ServerIdentifier } from "../types.js";
|
|
2
|
-
/**
|
|
3
|
-
* Session-to-server binding cache.
|
|
4
|
-
* Tracks which WebSocket server each session is bound to.
|
|
5
|
-
*/
|
|
6
|
-
declare class SessionManager {
|
|
7
|
-
private bindings;
|
|
8
|
-
/**
|
|
9
|
-
* Bind a session to a specific server.
|
|
10
|
-
*/
|
|
11
|
-
bind(sessionId: string, server: ServerIdentifier): void;
|
|
12
|
-
/**
|
|
13
|
-
* Get the server binding for a session.
|
|
14
|
-
*/
|
|
15
|
-
getBinding(sessionId: string): ServerIdentifier | null;
|
|
16
|
-
/**
|
|
17
|
-
* Check if a session is bound to a server.
|
|
18
|
-
*/
|
|
19
|
-
isBound(sessionId: string): boolean;
|
|
20
|
-
/**
|
|
21
|
-
* Unbind a session.
|
|
22
|
-
*/
|
|
23
|
-
unbind(sessionId: string): void;
|
|
24
|
-
/**
|
|
25
|
-
* Clear all bindings.
|
|
26
|
-
*/
|
|
27
|
-
clear(): void;
|
|
28
|
-
/**
|
|
29
|
-
* Get all bindings.
|
|
30
|
-
*/
|
|
31
|
-
getAll(): SessionBinding[];
|
|
32
|
-
}
|
|
33
|
-
export declare const sessionManager: SessionManager;
|
|
34
|
-
export {};
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Session-to-server binding cache.
|
|
3
|
-
* Tracks which WebSocket server each session is bound to.
|
|
4
|
-
*/
|
|
5
|
-
class SessionManager {
|
|
6
|
-
bindings = new Map();
|
|
7
|
-
/**
|
|
8
|
-
* Bind a session to a specific server.
|
|
9
|
-
*/
|
|
10
|
-
bind(sessionId, server) {
|
|
11
|
-
this.bindings.set(sessionId, {
|
|
12
|
-
sessionId,
|
|
13
|
-
server,
|
|
14
|
-
boundAt: Date.now(),
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Get the server binding for a session.
|
|
19
|
-
*/
|
|
20
|
-
getBinding(sessionId) {
|
|
21
|
-
const binding = this.bindings.get(sessionId);
|
|
22
|
-
return binding ? binding.server : null;
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Check if a session is bound to a server.
|
|
26
|
-
*/
|
|
27
|
-
isBound(sessionId) {
|
|
28
|
-
return this.bindings.has(sessionId);
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Unbind a session.
|
|
32
|
-
*/
|
|
33
|
-
unbind(sessionId) {
|
|
34
|
-
this.bindings.delete(sessionId);
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Clear all bindings.
|
|
38
|
-
*/
|
|
39
|
-
clear() {
|
|
40
|
-
this.bindings.clear();
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Get all bindings.
|
|
44
|
-
*/
|
|
45
|
-
getAll() {
|
|
46
|
-
return Array.from(this.bindings.values());
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
// Singleton instance
|
|
50
|
-
export const sessionManager = new SessionManager();
|