@ynhcj/xiaoyi-channel 0.0.11-beta → 0.0.11-next
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/bot.js +127 -48
- package/dist/src/channel.js +17 -4
- package/dist/src/client.js +11 -24
- package/dist/src/config.js +2 -2
- package/dist/src/formatter.d.ts +14 -0
- package/dist/src/formatter.js +46 -0
- package/dist/src/monitor.js +47 -5
- package/dist/src/outbound.js +52 -6
- package/dist/src/parser.d.ts +7 -0
- package/dist/src/parser.js +22 -0
- package/dist/src/push.d.ts +8 -1
- package/dist/src/push.js +30 -22
- package/dist/src/reply-dispatcher.d.ts +1 -0
- package/dist/src/reply-dispatcher.js +117 -100
- package/dist/src/task-manager.d.ts +55 -0
- package/dist/src/task-manager.js +136 -0
- package/dist/src/tools/calendar-tool.js +5 -3
- package/dist/src/tools/call-phone-tool.d.ts +5 -0
- package/dist/src/tools/call-phone-tool.js +183 -0
- package/dist/src/tools/create-alarm-tool.d.ts +7 -0
- package/dist/src/tools/create-alarm-tool.js +446 -0
- package/dist/src/tools/delete-alarm-tool.d.ts +11 -0
- package/dist/src/tools/delete-alarm-tool.js +238 -0
- package/dist/src/tools/location-tool.js +2 -2
- package/dist/src/tools/modify-alarm-tool.d.ts +9 -0
- package/dist/src/tools/modify-alarm-tool.js +467 -0
- package/dist/src/tools/modify-note-tool.js +2 -2
- package/dist/src/tools/note-tool.js +29 -8
- package/dist/src/tools/search-alarm-tool.d.ts +8 -0
- package/dist/src/tools/search-alarm-tool.js +391 -0
- package/dist/src/tools/search-calendar-tool.js +47 -5
- package/dist/src/tools/search-contact-tool.js +24 -3
- package/dist/src/tools/search-file-tool.d.ts +5 -0
- package/dist/src/tools/search-file-tool.js +185 -0
- package/dist/src/tools/search-message-tool.d.ts +5 -0
- package/dist/src/tools/search-message-tool.js +173 -0
- package/dist/src/tools/search-note-tool.js +2 -2
- package/dist/src/tools/search-photo-gallery-tool.d.ts +8 -0
- package/dist/src/tools/search-photo-gallery-tool.js +212 -0
- package/dist/src/tools/search-photo-tool.d.ts +9 -0
- package/dist/src/tools/search-photo-tool.js +270 -0
- package/dist/src/tools/send-file-to-user-tool.d.ts +5 -0
- package/dist/src/tools/send-file-to-user-tool.js +318 -0
- package/dist/src/tools/send-message-tool.d.ts +5 -0
- package/dist/src/tools/send-message-tool.js +189 -0
- package/dist/src/tools/session-manager.d.ts +15 -0
- package/dist/src/tools/session-manager.js +99 -18
- package/dist/src/tools/upload-file-tool.d.ts +13 -0
- package/dist/src/tools/upload-file-tool.js +265 -0
- package/dist/src/tools/upload-photo-tool.d.ts +9 -0
- package/dist/src/tools/upload-photo-tool.js +223 -0
- 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/tools/xiaoyi-gui-tool.d.ts +6 -0
- package/dist/src/tools/xiaoyi-gui-tool.js +151 -0
- package/dist/src/trigger-handler.d.ts +22 -0
- package/dist/src/trigger-handler.js +59 -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 +26 -31
- package/dist/src/websocket.js +227 -267
- package/package.json +1 -1
package/dist/src/bot.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { getXYRuntime } from "./runtime.js";
|
|
2
2
|
import { createXYReplyDispatcher } from "./reply-dispatcher.js";
|
|
3
|
-
import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId } from "./parser.js";
|
|
4
|
-
import { downloadFilesFromParts } from "./file-download.js";
|
|
3
|
+
import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId, extractTriggerData } from "./parser.js";
|
|
5
4
|
import { resolveXYConfig } from "./config.js";
|
|
6
|
-
import { sendStatusUpdate, sendClearContextResponse, sendTasksCancelResponse } from "./formatter.js";
|
|
7
|
-
import { registerSession, unregisterSession } from "./tools/session-manager.js";
|
|
5
|
+
import { sendStatusUpdate, sendClearContextResponse, sendTasksCancelResponse, sendA2AResponse } from "./formatter.js";
|
|
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
|
+
import { getPushDataById } from "./utils/pushdata-manager.js";
|
|
10
|
+
import { registerTaskId, decrementTaskIdRef, lockTaskId, unlockTaskId, hasActiveTask, } from "./task-manager.js";
|
|
9
11
|
/**
|
|
10
12
|
* Handle an incoming A2A message.
|
|
11
13
|
* This is the main entry point for message processing.
|
|
@@ -56,6 +58,60 @@ export async function handleXYMessage(params) {
|
|
|
56
58
|
}
|
|
57
59
|
// Parse the A2A message (for regular messages)
|
|
58
60
|
const parsed = parseA2AMessage(message);
|
|
61
|
+
// ========== 检测 Trigger 消息 ==========
|
|
62
|
+
// 如果消息中包含 Trigger 事件数据,直接返回 pushData 内容,不走正常流程
|
|
63
|
+
const triggerData = extractTriggerData(parsed.parts);
|
|
64
|
+
if (triggerData) {
|
|
65
|
+
log(`[BOT] 📌 Detected Trigger message with pushDataId: ${triggerData.pushDataId}`);
|
|
66
|
+
log(`[BOT] - Session ID: ${parsed.sessionId}`);
|
|
67
|
+
log(`[BOT] - Task ID: ${parsed.taskId}`);
|
|
68
|
+
try {
|
|
69
|
+
// 读取 pushData
|
|
70
|
+
const pushDataItem = await getPushDataById(triggerData.pushDataId);
|
|
71
|
+
if (!pushDataItem) {
|
|
72
|
+
error(`[BOT] ❌ pushData not found for ID: ${triggerData.pushDataId}`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
log(`[BOT] ✅ Found pushData, sending direct response`);
|
|
76
|
+
log(`[BOT] - pushDataId: ${pushDataItem.pushDataId}`);
|
|
77
|
+
log(`[BOT] - time: ${pushDataItem.time}`);
|
|
78
|
+
log(`[BOT] - content length: ${pushDataItem.dataDetail.length} chars`);
|
|
79
|
+
const config = resolveXYConfig(cfg);
|
|
80
|
+
// 直接发送响应(final=true,不走 openclaw 流程)
|
|
81
|
+
await sendA2AResponse({
|
|
82
|
+
config,
|
|
83
|
+
sessionId: parsed.sessionId,
|
|
84
|
+
taskId: parsed.taskId,
|
|
85
|
+
messageId: parsed.messageId,
|
|
86
|
+
text: pushDataItem.dataDetail,
|
|
87
|
+
append: false,
|
|
88
|
+
final: true,
|
|
89
|
+
});
|
|
90
|
+
log(`[BOT] ✅ Trigger response sent successfully, exiting early`);
|
|
91
|
+
return; // 提前返回,不继续处理
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
error(`[BOT] ❌ Failed to handle Trigger message:`, err);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// ========================================
|
|
99
|
+
// 🔑 检测steer模式和是否是第二条消息
|
|
100
|
+
const isSteerMode = cfg.messages?.queue?.mode === "steer";
|
|
101
|
+
const isSecondMessage = isSteerMode && hasActiveTask(parsed.sessionId);
|
|
102
|
+
if (isSecondMessage) {
|
|
103
|
+
log(`[BOT] 🔄 STEER MODE - Second message detected (will be follower)`);
|
|
104
|
+
log(`[BOT] - Session: ${parsed.sessionId}`);
|
|
105
|
+
log(`[BOT] - New taskId: ${parsed.taskId} (will replace current)`);
|
|
106
|
+
}
|
|
107
|
+
// 🔑 注册taskId(第二条消息会覆盖第一条的taskId)
|
|
108
|
+
const { isUpdate, refCount } = registerTaskId(parsed.sessionId, parsed.taskId, parsed.messageId, { incrementRef: true } // 增加引用计数
|
|
109
|
+
);
|
|
110
|
+
// 🔑 如果是第一条消息,锁定taskId防止被过早清理
|
|
111
|
+
if (!isUpdate) {
|
|
112
|
+
lockTaskId(parsed.sessionId);
|
|
113
|
+
log(`[BOT] 🔒 Locked taskId for first message`);
|
|
114
|
+
}
|
|
59
115
|
// Extract and update push_id if present
|
|
60
116
|
const pushId = extractPushId(parsed.parts);
|
|
61
117
|
if (pushId) {
|
|
@@ -64,6 +120,10 @@ export async function handleXYMessage(params) {
|
|
|
64
120
|
log(`[BOT] - Push ID preview: ${pushId.substring(0, 20)}...`);
|
|
65
121
|
log(`[BOT] - Full push_id: ${pushId}`);
|
|
66
122
|
configManager.updatePushId(parsed.sessionId, pushId);
|
|
123
|
+
// 持久化 pushId 到本地文件(异步,不阻塞主流程)
|
|
124
|
+
addPushId(pushId).catch((err) => {
|
|
125
|
+
error(`[BOT] Failed to persist pushId:`, err);
|
|
126
|
+
});
|
|
67
127
|
}
|
|
68
128
|
else {
|
|
69
129
|
log(`[BOT] ℹ️ No push_id found in message, will use config default`);
|
|
@@ -83,11 +143,12 @@ export async function handleXYMessage(params) {
|
|
|
83
143
|
},
|
|
84
144
|
});
|
|
85
145
|
log(`xy: resolved route accountId=${route.accountId}, sessionKey=${route.sessionKey}`);
|
|
86
|
-
//
|
|
146
|
+
// 🔑 注册session(带引用计数)
|
|
87
147
|
log(`[BOT] 📝 About to register session for tools...`);
|
|
88
148
|
log(`[BOT] - sessionKey: ${route.sessionKey}`);
|
|
89
149
|
log(`[BOT] - sessionId: ${parsed.sessionId}`);
|
|
90
150
|
log(`[BOT] - taskId: ${parsed.taskId}`);
|
|
151
|
+
log(`[BOT] - isSecondMessage: ${isSecondMessage}`);
|
|
91
152
|
registerSession(route.sessionKey, {
|
|
92
153
|
config,
|
|
93
154
|
sessionId: parsed.sessionId,
|
|
@@ -96,14 +157,14 @@ export async function handleXYMessage(params) {
|
|
|
96
157
|
agentId: route.accountId,
|
|
97
158
|
});
|
|
98
159
|
log(`[BOT] ✅ Session registered for tools`);
|
|
99
|
-
//
|
|
160
|
+
// 🔑 发送初始状态更新(第二条消息也要发,用新taskId)
|
|
100
161
|
log(`[STATUS] Sending initial status update for session ${parsed.sessionId}`);
|
|
101
162
|
void sendStatusUpdate({
|
|
102
163
|
config,
|
|
103
164
|
sessionId: parsed.sessionId,
|
|
104
165
|
taskId: parsed.taskId,
|
|
105
166
|
messageId: parsed.messageId,
|
|
106
|
-
text: "任务正在处理中,请稍后~",
|
|
167
|
+
text: isSecondMessage ? "新消息已接收,正在处理..." : "任务正在处理中,请稍后~",
|
|
107
168
|
state: "working",
|
|
108
169
|
}).catch((err) => {
|
|
109
170
|
error(`Failed to send initial status update:`, err);
|
|
@@ -111,10 +172,9 @@ export async function handleXYMessage(params) {
|
|
|
111
172
|
// Extract text and files from parts
|
|
112
173
|
const text = extractTextFromParts(parsed.parts);
|
|
113
174
|
const fileParts = extractFileParts(parsed.parts);
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const mediaPayload = buildXYMediaPayload(mediaList);
|
|
175
|
+
// Build media payload directly from file URIs (openclaw can download them)
|
|
176
|
+
// No need to download files locally - pass URIs directly to openclaw
|
|
177
|
+
const mediaPayload = buildXYMediaPayload(fileParts);
|
|
118
178
|
// Resolve envelope format options (following feishu pattern)
|
|
119
179
|
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
|
|
120
180
|
// Build message body with speaker prefix (following feishu pattern)
|
|
@@ -155,51 +215,66 @@ export async function handleXYMessage(params) {
|
|
|
155
215
|
ReplyToBody: undefined, // A2A protocol doesn't support reply/quote
|
|
156
216
|
...mediaPayload,
|
|
157
217
|
});
|
|
158
|
-
//
|
|
159
|
-
log(`[
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
taskId: parsed.taskId,
|
|
164
|
-
messageId: parsed.messageId,
|
|
165
|
-
text: "任务正在处理中,请稍后~",
|
|
166
|
-
state: "working",
|
|
167
|
-
}).catch((err) => {
|
|
168
|
-
error(`Failed to send initial status update:`, err);
|
|
169
|
-
});
|
|
170
|
-
// Create reply dispatcher (following feishu pattern)
|
|
171
|
-
log(`[BOT-DISPATCHER] 🎯 Creating reply dispatcher for session=${parsed.sessionId}, taskId=${parsed.taskId}, messageId=${parsed.messageId}`);
|
|
218
|
+
// 🔑 创建dispatcher(dispatcher会自动使用动态taskId)
|
|
219
|
+
log(`[BOT-DISPATCHER] 🎯 Creating reply dispatcher`);
|
|
220
|
+
log(`[BOT-DISPATCHER] - session: ${parsed.sessionId}`);
|
|
221
|
+
log(`[BOT-DISPATCHER] - taskId: ${parsed.taskId}`);
|
|
222
|
+
log(`[BOT-DISPATCHER] - isSecondMessage: ${isSecondMessage}`);
|
|
172
223
|
const { dispatcher, replyOptions, markDispatchIdle, startStatusInterval } = createXYReplyDispatcher({
|
|
173
224
|
cfg,
|
|
174
225
|
runtime,
|
|
175
226
|
sessionId: parsed.sessionId,
|
|
176
227
|
taskId: parsed.taskId,
|
|
177
228
|
messageId: parsed.messageId,
|
|
178
|
-
accountId: route.accountId,
|
|
229
|
+
accountId: route.accountId,
|
|
230
|
+
isSteerFollower: isSecondMessage, // 🔑 标记第二条消息
|
|
179
231
|
});
|
|
180
232
|
log(`[BOT-DISPATCHER] ✅ Reply dispatcher created successfully`);
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
|
|
233
|
+
// 🔑 只有第一条消息启动状态定时器
|
|
234
|
+
// 第二条消息会很快返回,不需要定时器
|
|
235
|
+
if (!isSecondMessage) {
|
|
236
|
+
startStatusInterval();
|
|
237
|
+
log(`[BOT-DISPATCHER] ✅ Status interval started for first message`);
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
log(`[BOT-DISPATCHER] ⏭️ Skipped status interval for steer follower`);
|
|
241
|
+
}
|
|
184
242
|
log(`xy: dispatching to agent (session=${parsed.sessionId})`);
|
|
185
243
|
// Dispatch to OpenClaw core using correct API (following feishu pattern)
|
|
186
244
|
log(`[BOT] 🚀 Starting dispatcher with session: ${route.sessionKey}`);
|
|
245
|
+
// Build session context for AsyncLocalStorage
|
|
246
|
+
const sessionContext = {
|
|
247
|
+
config,
|
|
248
|
+
sessionId: parsed.sessionId,
|
|
249
|
+
taskId: parsed.taskId,
|
|
250
|
+
messageId: parsed.messageId,
|
|
251
|
+
agentId: route.accountId,
|
|
252
|
+
};
|
|
187
253
|
await core.channel.reply.withReplyDispatcher({
|
|
188
254
|
dispatcher,
|
|
189
255
|
onSettled: () => {
|
|
190
256
|
log(`[BOT] 🏁 onSettled called for session: ${route.sessionKey}`);
|
|
191
|
-
log(`[BOT] -
|
|
257
|
+
log(`[BOT] - isSecondMessage: ${isSecondMessage}`);
|
|
192
258
|
markDispatchIdle();
|
|
193
|
-
//
|
|
259
|
+
// 🔑 减少引用计数
|
|
260
|
+
decrementTaskIdRef(parsed.sessionId);
|
|
261
|
+
// 🔑 如果是第一条消息完成,解锁
|
|
262
|
+
if (!isSecondMessage) {
|
|
263
|
+
unlockTaskId(parsed.sessionId);
|
|
264
|
+
log(`[BOT] 🔓 Unlocked taskId (first message completed)`);
|
|
265
|
+
}
|
|
266
|
+
// 减少session引用计数
|
|
194
267
|
unregisterSession(route.sessionKey);
|
|
195
|
-
log(`[BOT] ✅
|
|
268
|
+
log(`[BOT] ✅ Cleanup completed`);
|
|
196
269
|
},
|
|
197
|
-
run: () =>
|
|
270
|
+
run: () =>
|
|
271
|
+
// 🔐 Use AsyncLocalStorage to provide session context to tools
|
|
272
|
+
runWithSessionContext(sessionContext, () => core.channel.reply.dispatchReplyFromConfig({
|
|
198
273
|
ctx: ctxPayload,
|
|
199
274
|
cfg,
|
|
200
275
|
dispatcher,
|
|
201
276
|
replyOptions,
|
|
202
|
-
}),
|
|
277
|
+
})),
|
|
203
278
|
});
|
|
204
279
|
log(`[BOT] ✅ Dispatcher completed for session: ${parsed.sessionId}`);
|
|
205
280
|
log(`xy: dispatch complete (session=${parsed.sessionId})`);
|
|
@@ -209,25 +284,28 @@ export async function handleXYMessage(params) {
|
|
|
209
284
|
error("Failed to handle XY message:", err);
|
|
210
285
|
runtime.error?.(`xy: Failed to handle message: ${String(err)}`);
|
|
211
286
|
log(`[BOT] ❌ Error occurred, attempting cleanup...`);
|
|
212
|
-
//
|
|
287
|
+
// 🔑 错误时也要清理taskId和session
|
|
213
288
|
try {
|
|
214
|
-
const core = getXYRuntime();
|
|
215
289
|
const params = message.params;
|
|
216
290
|
const sessionId = params?.sessionId;
|
|
217
291
|
if (sessionId) {
|
|
218
|
-
log(`[BOT] 🧹 Cleaning up
|
|
292
|
+
log(`[BOT] 🧹 Cleaning up after error: ${sessionId}`);
|
|
293
|
+
// 清理 taskId
|
|
294
|
+
decrementTaskIdRef(sessionId);
|
|
295
|
+
unlockTaskId(sessionId);
|
|
296
|
+
// 清理 session
|
|
297
|
+
const core = getXYRuntime();
|
|
219
298
|
const route = core.channel.routing.resolveAgentRoute({
|
|
220
299
|
cfg,
|
|
221
300
|
channel: "xiaoyi-channel",
|
|
222
301
|
accountId,
|
|
223
302
|
peer: {
|
|
224
303
|
kind: "direct",
|
|
225
|
-
id: sessionId,
|
|
304
|
+
id: sessionId,
|
|
226
305
|
},
|
|
227
306
|
});
|
|
228
|
-
log(`[BOT] - Unregistering session: ${route.sessionKey}`);
|
|
229
307
|
unregisterSession(route.sessionKey);
|
|
230
|
-
log(`[BOT] ✅
|
|
308
|
+
log(`[BOT] ✅ Cleanup completed after error`);
|
|
231
309
|
}
|
|
232
310
|
}
|
|
233
311
|
catch (cleanupErr) {
|
|
@@ -240,17 +318,18 @@ export async function handleXYMessage(params) {
|
|
|
240
318
|
/**
|
|
241
319
|
* Build media payload for inbound context.
|
|
242
320
|
* Following feishu pattern: buildFeishuMediaPayload().
|
|
321
|
+
* Uses remote URIs directly - openclaw will download them.
|
|
243
322
|
*/
|
|
244
|
-
function buildXYMediaPayload(
|
|
245
|
-
const first =
|
|
246
|
-
const
|
|
247
|
-
const mediaTypes =
|
|
323
|
+
function buildXYMediaPayload(fileParts) {
|
|
324
|
+
const first = fileParts[0];
|
|
325
|
+
const uris = fileParts.map((file) => file.uri);
|
|
326
|
+
const mediaTypes = fileParts.map((file) => file.mimeType).filter(Boolean);
|
|
248
327
|
return {
|
|
249
|
-
MediaPath: first?.
|
|
328
|
+
MediaPath: first?.uri,
|
|
250
329
|
MediaType: first?.mimeType,
|
|
251
|
-
MediaUrl: first?.
|
|
252
|
-
MediaPaths:
|
|
253
|
-
MediaUrls:
|
|
330
|
+
MediaUrl: first?.uri,
|
|
331
|
+
MediaPaths: uris.length > 0 ? uris : undefined,
|
|
332
|
+
MediaUrls: uris.length > 0 ? uris : undefined,
|
|
254
333
|
MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined,
|
|
255
334
|
};
|
|
256
335
|
}
|
package/dist/src/channel.js
CHANGED
|
@@ -9,6 +9,20 @@ import { modifyNoteTool } from "./tools/modify-note-tool.js";
|
|
|
9
9
|
import { calendarTool } from "./tools/calendar-tool.js";
|
|
10
10
|
import { searchCalendarTool } from "./tools/search-calendar-tool.js";
|
|
11
11
|
import { searchContactTool } from "./tools/search-contact-tool.js";
|
|
12
|
+
import { searchPhotoGalleryTool } from "./tools/search-photo-gallery-tool.js";
|
|
13
|
+
import { uploadPhotoTool } from "./tools/upload-photo-tool.js";
|
|
14
|
+
import { xiaoyiGuiTool } from "./tools/xiaoyi-gui-tool.js";
|
|
15
|
+
import { callPhoneTool } from "./tools/call-phone-tool.js";
|
|
16
|
+
import { searchMessageTool } from "./tools/search-message-tool.js";
|
|
17
|
+
import { searchFileTool } from "./tools/search-file-tool.js";
|
|
18
|
+
import { uploadFileTool } from "./tools/upload-file-tool.js";
|
|
19
|
+
import { createAlarmTool } from "./tools/create-alarm-tool.js";
|
|
20
|
+
import { searchAlarmTool } from "./tools/search-alarm-tool.js";
|
|
21
|
+
import { modifyAlarmTool } from "./tools/modify-alarm-tool.js";
|
|
22
|
+
import { deleteAlarmTool } from "./tools/delete-alarm-tool.js";
|
|
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";
|
|
12
26
|
/**
|
|
13
27
|
* Xiaoyi Channel Plugin for OpenClaw.
|
|
14
28
|
* Implements Xiaoyi A2A protocol with dual WebSocket connections.
|
|
@@ -48,7 +62,7 @@ export const xyPlugin = {
|
|
|
48
62
|
},
|
|
49
63
|
outbound: xyOutbound,
|
|
50
64
|
onboarding: xyOnboardingAdapter,
|
|
51
|
-
agentTools: [locationTool, noteTool, searchNoteTool, modifyNoteTool, calendarTool, searchCalendarTool, searchContactTool],
|
|
65
|
+
agentTools: [locationTool, noteTool, searchNoteTool, modifyNoteTool, calendarTool, searchCalendarTool, searchContactTool, searchPhotoGalleryTool, uploadPhotoTool, xiaoyiGuiTool, callPhoneTool, searchMessageTool, searchFileTool, uploadFileTool, createAlarmTool, searchAlarmTool, modifyAlarmTool, deleteAlarmTool, sendFileToUserTool, xiaoyiCollectionTool, viewPushResultTool],
|
|
52
66
|
messaging: {
|
|
53
67
|
normalizeTarget: (raw) => {
|
|
54
68
|
const trimmed = raw.trim();
|
|
@@ -75,10 +89,9 @@ export const xyPlugin = {
|
|
|
75
89
|
const account = resolveXYConfig(context.cfg);
|
|
76
90
|
context.setStatus?.({
|
|
77
91
|
accountId: context.accountId,
|
|
78
|
-
|
|
79
|
-
wsUrl2: account.wsUrl2,
|
|
92
|
+
wsUrl: account.wsUrl,
|
|
80
93
|
});
|
|
81
|
-
context.log?.info(`[${context.accountId}] starting xiaoyi channel (
|
|
94
|
+
context.log?.info(`[${context.accountId}] starting xiaoyi channel (wsUrl: ${account.wsUrl})`);
|
|
82
95
|
return monitorXYProvider({
|
|
83
96
|
config: context.cfg,
|
|
84
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/formatter.d.ts
CHANGED
|
@@ -92,3 +92,17 @@ export interface SendTasksCancelResponseParams {
|
|
|
92
92
|
* Send a tasks/cancel response.
|
|
93
93
|
*/
|
|
94
94
|
export declare function sendTasksCancelResponse(params: SendTasksCancelResponseParams): Promise<void>;
|
|
95
|
+
/**
|
|
96
|
+
* Parameters for sending a Trigger response.
|
|
97
|
+
*/
|
|
98
|
+
export interface SendTriggerResponseParams {
|
|
99
|
+
config: XYChannelConfig;
|
|
100
|
+
sessionId: string;
|
|
101
|
+
taskId: string;
|
|
102
|
+
messageId: string;
|
|
103
|
+
content: string;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Send a Trigger response with pushData content.
|
|
107
|
+
*/
|
|
108
|
+
export declare function sendTriggerResponse(params: SendTriggerResponseParams): Promise<void>;
|
package/dist/src/formatter.js
CHANGED
|
@@ -293,3 +293,49 @@ export async function sendTasksCancelResponse(params) {
|
|
|
293
293
|
await wsManager.sendMessage(sessionId, outboundMessage);
|
|
294
294
|
log(`Sent tasks/cancel response: sessionId=${sessionId}, taskId=${taskId}`);
|
|
295
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* Send a Trigger response with pushData content.
|
|
298
|
+
*/
|
|
299
|
+
export async function sendTriggerResponse(params) {
|
|
300
|
+
const { config, sessionId, taskId, messageId, content } = params;
|
|
301
|
+
const runtime = getXYRuntime();
|
|
302
|
+
const log = runtime?.log ?? console.log;
|
|
303
|
+
const error = runtime?.error ?? console.error;
|
|
304
|
+
// Build JSON-RPC response for Trigger
|
|
305
|
+
const jsonRpcResponse = {
|
|
306
|
+
jsonrpc: "2.0",
|
|
307
|
+
id: messageId,
|
|
308
|
+
result: {
|
|
309
|
+
taskId: taskId,
|
|
310
|
+
kind: "artifact-update",
|
|
311
|
+
append: false,
|
|
312
|
+
lastChunk: true,
|
|
313
|
+
final: true,
|
|
314
|
+
artifact: {
|
|
315
|
+
artifactId: uuidv4(),
|
|
316
|
+
parts: [
|
|
317
|
+
{
|
|
318
|
+
kind: "text",
|
|
319
|
+
text: content,
|
|
320
|
+
},
|
|
321
|
+
],
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
error: {
|
|
325
|
+
code: 0,
|
|
326
|
+
message: "",
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
// Send via WebSocket
|
|
330
|
+
const wsManager = getXYWebSocketManager(config);
|
|
331
|
+
const outboundMessage = {
|
|
332
|
+
msgType: "agent_response",
|
|
333
|
+
agentId: config.agentId,
|
|
334
|
+
sessionId,
|
|
335
|
+
taskId,
|
|
336
|
+
msgDetail: JSON.stringify(jsonRpcResponse),
|
|
337
|
+
};
|
|
338
|
+
log(`[TRIGGER_RESPONSE] Sending Trigger response: sessionId=${sessionId}, taskId=${taskId}`);
|
|
339
|
+
await wsManager.sendMessage(sessionId, outboundMessage);
|
|
340
|
+
log(`[TRIGGER_RESPONSE] Trigger response sent successfully`);
|
|
341
|
+
}
|
package/dist/src/monitor.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { resolveXYConfig } from "./config.js";
|
|
2
2
|
import { getXYWebSocketManager, diagnoseAllManagers, cleanupOrphanConnections, removeXYWebSocketManager } from "./client.js";
|
|
3
3
|
import { handleXYMessage } from "./bot.js";
|
|
4
|
+
import { parseA2AMessage } from "./parser.js";
|
|
5
|
+
import { hasActiveTask } from "./task-manager.js";
|
|
6
|
+
import { handleTriggerEvent } from "./trigger-handler.js";
|
|
4
7
|
/**
|
|
5
8
|
* Per-session serial queue that ensures messages from the same session are processed
|
|
6
9
|
* in arrival order while allowing different sessions to run concurrently.
|
|
@@ -94,11 +97,39 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
94
97
|
log(`[MONITOR-HANDLER] 🧹 Cleaned up messageKey=${messageKey}, remaining active: ${activeMessages.size}`);
|
|
95
98
|
}
|
|
96
99
|
};
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
// 🔑 核心改造:检测steer模式
|
|
101
|
+
// 需要提前解析消息以获取sessionId
|
|
102
|
+
try {
|
|
103
|
+
const parsed = parseA2AMessage(message);
|
|
104
|
+
const steerMode = cfg.messages?.queue?.mode === "steer";
|
|
105
|
+
const hasActiveRun = hasActiveTask(parsed.sessionId);
|
|
106
|
+
if (steerMode && hasActiveRun) {
|
|
107
|
+
// Steer模式且有活跃任务:不入队列,直接并发执行
|
|
108
|
+
log(`[MONITOR-HANDLER] 🔄 STEER MODE: Executing concurrently for messageKey=${messageKey}`);
|
|
109
|
+
log(`[MONITOR-HANDLER] - sessionId: ${parsed.sessionId}`);
|
|
110
|
+
log(`[MONITOR-HANDLER] - Bypassing queue to allow message insertion`);
|
|
111
|
+
void task().catch((err) => {
|
|
112
|
+
error(`XY gateway: concurrent steer task failed for ${messageKey}: ${String(err)}`);
|
|
113
|
+
activeMessages.delete(messageKey);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
// 正常模式:入队列串行执行
|
|
118
|
+
log(`[MONITOR-HANDLER] 📋 NORMAL MODE: Enqueuing for messageKey=${messageKey}`);
|
|
119
|
+
void enqueue(sessionId, task).catch((err) => {
|
|
120
|
+
error(`XY gateway: queue processing failed for session ${sessionId}: ${String(err)}`);
|
|
121
|
+
activeMessages.delete(messageKey);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (parseErr) {
|
|
126
|
+
// 解析失败,回退到正常队列模式
|
|
127
|
+
error(`[MONITOR-HANDLER] Failed to parse message for steer detection: ${String(parseErr)}`);
|
|
128
|
+
void enqueue(sessionId, task).catch((err) => {
|
|
129
|
+
error(`XY gateway: queue processing failed for session ${sessionId}: ${String(err)}`);
|
|
130
|
+
activeMessages.delete(messageKey);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
102
133
|
};
|
|
103
134
|
const connectedHandler = (serverId) => {
|
|
104
135
|
if (!loggedServers.has(serverId)) {
|
|
@@ -120,6 +151,15 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
120
151
|
const errorHandler = (err, serverId) => {
|
|
121
152
|
error(`XY gateway: ${serverId} error: ${String(err)}`);
|
|
122
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
|
+
};
|
|
123
163
|
const cleanup = () => {
|
|
124
164
|
log("XY gateway: cleaning up...");
|
|
125
165
|
// 🔍 Diagnose before cleanup
|
|
@@ -136,6 +176,7 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
136
176
|
wsManager.off("connected", connectedHandler);
|
|
137
177
|
wsManager.off("disconnected", disconnectedHandler);
|
|
138
178
|
wsManager.off("error", errorHandler);
|
|
179
|
+
wsManager.off("trigger-event", triggerEventHandler);
|
|
139
180
|
// ✅ Disconnect the wsManager to prevent connection leaks
|
|
140
181
|
// This is safe because each gateway lifecycle should have clean connections
|
|
141
182
|
wsManager.disconnect();
|
|
@@ -165,6 +206,7 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
165
206
|
wsManager.on("connected", connectedHandler);
|
|
166
207
|
wsManager.on("disconnected", disconnectedHandler);
|
|
167
208
|
wsManager.on("error", errorHandler);
|
|
209
|
+
wsManager.on("trigger-event", triggerEventHandler);
|
|
168
210
|
// Start periodic health check (every 5 minutes)
|
|
169
211
|
console.log("🏥 Starting periodic health check (every 5 minutes)...");
|
|
170
212
|
healthCheckInterval = setInterval(() => {
|
package/dist/src/outbound.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { resolveXYConfig } from "./config.js";
|
|
2
2
|
import { XYFileUploadService } from "./file-upload.js";
|
|
3
3
|
import { XYPushService } from "./push.js";
|
|
4
|
-
import {
|
|
4
|
+
import { getCurrentSessionContext } from "./tools/session-manager.js";
|
|
5
|
+
import { savePushData } from "./utils/pushdata-manager.js";
|
|
6
|
+
import { getAllPushIds } from "./utils/pushid-manager.js";
|
|
5
7
|
// Special marker for default push delivery when no target is specified
|
|
6
8
|
const DEFAULT_PUSH_MARKER = "default";
|
|
7
9
|
// File extension to MIME type mapping
|
|
@@ -65,8 +67,8 @@ export const xyOutbound = {
|
|
|
65
67
|
// If the target doesn't contain "::", try to enhance it with taskId from session context
|
|
66
68
|
if (!trimmedTo.includes("::")) {
|
|
67
69
|
console.log(`[xyOutbound.resolveTarget] Target "${trimmedTo}" missing taskId, looking up session context`);
|
|
68
|
-
// Try to get the
|
|
69
|
-
const sessionContext =
|
|
70
|
+
// Try to get the current session context
|
|
71
|
+
const sessionContext = getCurrentSessionContext();
|
|
70
72
|
if (sessionContext && sessionContext.sessionId === trimmedTo) {
|
|
71
73
|
const enhancedTarget = `${trimmedTo}::${sessionContext.taskId}`;
|
|
72
74
|
console.log(`[xyOutbound.resolveTarget] Enhanced target: ${enhancedTarget}`);
|
|
@@ -105,19 +107,63 @@ export const xyOutbound = {
|
|
|
105
107
|
// The push service will handle it based on config
|
|
106
108
|
actualTo = config.defaultSessionId || "";
|
|
107
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
|
+
}
|
|
108
137
|
// Create push service
|
|
109
138
|
const pushService = new XYPushService(config);
|
|
110
139
|
// Extract title (first 57 chars or first line)
|
|
111
140
|
const title = text.split("\n")[0].slice(0, 57);
|
|
112
141
|
// Truncate push content to max length 1000
|
|
113
142
|
const pushText = text.length > 1000 ? text.slice(0, 1000) : text;
|
|
114
|
-
//
|
|
115
|
-
|
|
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
|
+
// 传入 pushId 和 pushDataId,使用 kind="data" 格式
|
|
151
|
+
await pushService.sendPush(pushText, title, undefined, actualTo, pushDataId, pushId);
|
|
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`);
|
|
116
162
|
console.log(`[xyOutbound.sendText] Completed successfully`);
|
|
117
163
|
// Return message info
|
|
118
164
|
return {
|
|
119
165
|
channel: "xiaoyi-channel",
|
|
120
|
-
messageId: Date.now().toString(),
|
|
166
|
+
messageId: pushDataId || Date.now().toString(),
|
|
121
167
|
chatId: actualTo,
|
|
122
168
|
};
|
|
123
169
|
},
|
package/dist/src/parser.d.ts
CHANGED
|
@@ -43,6 +43,13 @@ export declare function isTasksCancelMessage(method: string): boolean;
|
|
|
43
43
|
* Looks for push_id in data parts under variables.systemVariables.push_id
|
|
44
44
|
*/
|
|
45
45
|
export declare function extractPushId(parts: A2AMessagePart[]): string | null;
|
|
46
|
+
/**
|
|
47
|
+
* Extract Trigger event data from message parts.
|
|
48
|
+
* Looks for Trigger events with pushDataId in data parts.
|
|
49
|
+
*/
|
|
50
|
+
export declare function extractTriggerData(parts: A2AMessagePart[]): {
|
|
51
|
+
pushDataId: string;
|
|
52
|
+
} | null;
|
|
46
53
|
/**
|
|
47
54
|
* Validate A2A request structure.
|
|
48
55
|
*/
|