@ynhcj/xiaoyi-channel 0.0.9 → 0.0.10-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 +153 -48
- package/dist/src/channel.js +23 -4
- package/dist/src/client.d.ts +15 -0
- package/dist/src/client.js +81 -0
- package/dist/src/config.js +2 -2
- package/dist/src/file-download.js +10 -1
- package/dist/src/formatter.d.ts +31 -0
- package/dist/src/formatter.js +93 -1
- package/dist/src/heartbeat.d.ts +2 -1
- package/dist/src/heartbeat.js +6 -1
- package/dist/src/monitor.d.ts +5 -0
- package/dist/src/monitor.js +101 -9
- package/dist/src/outbound.js +97 -7
- package/dist/src/parser.d.ts +12 -0
- package/dist/src/parser.js +37 -0
- package/dist/src/push.d.ts +13 -1
- package/dist/src/push.js +125 -19
- package/dist/src/reply-dispatcher.d.ts +1 -0
- package/dist/src/reply-dispatcher.js +206 -51
- package/dist/src/task-manager.d.ts +55 -0
- package/dist/src/task-manager.js +136 -0
- package/dist/src/tools/calendar-tool.d.ts +6 -0
- package/dist/src/tools/calendar-tool.js +169 -0
- 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 +18 -8
- 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.d.ts +9 -0
- package/dist/src/tools/modify-note-tool.js +163 -0
- package/dist/src/tools/note-tool.js +32 -11
- 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.d.ts +12 -0
- package/dist/src/tools/search-calendar-tool.js +262 -0
- package/dist/src/tools/search-contact-tool.d.ts +5 -0
- package/dist/src/tools/search-contact-tool.js +168 -0
- 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 +6 -6
- 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 +101 -13
- 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 +6 -17
- package/dist/src/types.js +4 -0
- package/dist/src/utils/config-manager.d.ts +26 -0
- package/dist/src/utils/config-manager.js +56 -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 +59 -25
- package/dist/src/websocket.js +315 -257
- package/package.json +1 -1
package/dist/src/formatter.js
CHANGED
|
@@ -67,6 +67,49 @@ export async function sendA2AResponse(params) {
|
|
|
67
67
|
await wsManager.sendMessage(sessionId, outboundMessage);
|
|
68
68
|
log(`[A2A_RESPONSE] ✅ Message sent successfully`);
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Send an A2A artifact-update with reasoningText part.
|
|
72
|
+
* Used for onToolStart, onToolResult, onReasoningStream, onReasoningEnd, onPartialReply.
|
|
73
|
+
* append=true, final=false, lastChunk=true, text is suffixed with newline for markdown rendering.
|
|
74
|
+
*/
|
|
75
|
+
export async function sendReasoningTextUpdate(params) {
|
|
76
|
+
const { config, sessionId, taskId, messageId, text, append = true } = params;
|
|
77
|
+
const runtime = getXYRuntime();
|
|
78
|
+
const log = runtime?.log ?? console.log;
|
|
79
|
+
const error = runtime?.error ?? console.error;
|
|
80
|
+
const artifact = {
|
|
81
|
+
taskId,
|
|
82
|
+
kind: "artifact-update",
|
|
83
|
+
append,
|
|
84
|
+
lastChunk: true,
|
|
85
|
+
final: false,
|
|
86
|
+
artifact: {
|
|
87
|
+
artifactId: uuidv4(),
|
|
88
|
+
parts: [
|
|
89
|
+
{
|
|
90
|
+
kind: "reasoningText",
|
|
91
|
+
reasoningText: text,
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
const jsonRpcResponse = {
|
|
97
|
+
jsonrpc: "2.0",
|
|
98
|
+
id: messageId,
|
|
99
|
+
result: artifact,
|
|
100
|
+
};
|
|
101
|
+
const wsManager = getXYWebSocketManager(config);
|
|
102
|
+
const outboundMessage = {
|
|
103
|
+
msgType: "agent_response",
|
|
104
|
+
agentId: config.agentId,
|
|
105
|
+
sessionId,
|
|
106
|
+
taskId,
|
|
107
|
+
msgDetail: JSON.stringify(jsonRpcResponse),
|
|
108
|
+
};
|
|
109
|
+
log(`[REASONING_TEXT] 📤 Sending reasoningText update: sessionId=${sessionId}, taskId=${taskId}, text.length=${text.length}`);
|
|
110
|
+
await wsManager.sendMessage(sessionId, outboundMessage);
|
|
111
|
+
log(`[REASONING_TEXT] ✅ Sent successfully`);
|
|
112
|
+
}
|
|
70
113
|
/**
|
|
71
114
|
* Send an A2A task status update.
|
|
72
115
|
* Follows A2A protocol standard format with nested status object.
|
|
@@ -132,6 +175,7 @@ export async function sendCommand(params) {
|
|
|
132
175
|
const log = runtime?.log ?? console.log;
|
|
133
176
|
const error = runtime?.error ?? console.error;
|
|
134
177
|
// Build artifact update with command as data
|
|
178
|
+
// Wrap command in commands array as per protocol requirement
|
|
135
179
|
const artifact = {
|
|
136
180
|
taskId,
|
|
137
181
|
kind: "artifact-update",
|
|
@@ -143,7 +187,9 @@ export async function sendCommand(params) {
|
|
|
143
187
|
parts: [
|
|
144
188
|
{
|
|
145
189
|
kind: "data",
|
|
146
|
-
data:
|
|
190
|
+
data: {
|
|
191
|
+
commands: [command],
|
|
192
|
+
},
|
|
147
193
|
},
|
|
148
194
|
],
|
|
149
195
|
},
|
|
@@ -247,3 +293,49 @@ export async function sendTasksCancelResponse(params) {
|
|
|
247
293
|
await wsManager.sendMessage(sessionId, outboundMessage);
|
|
248
294
|
log(`Sent tasks/cancel response: sessionId=${sessionId}, taskId=${taskId}`);
|
|
249
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/heartbeat.d.ts
CHANGED
|
@@ -13,12 +13,13 @@ export declare class HeartbeatManager {
|
|
|
13
13
|
private config;
|
|
14
14
|
private onTimeout;
|
|
15
15
|
private serverName;
|
|
16
|
+
private onHeartbeatSuccess?;
|
|
16
17
|
private intervalTimer;
|
|
17
18
|
private timeoutTimer;
|
|
18
19
|
private lastPongTime;
|
|
19
20
|
private log;
|
|
20
21
|
private error;
|
|
21
|
-
constructor(ws: WebSocket, config: HeartbeatConfig, onTimeout: () => void, serverName?: string, logFn?: (msg: string, ...args: any[]) => void, errorFn?: (msg: string, ...args: any[]) => void);
|
|
22
|
+
constructor(ws: WebSocket, config: HeartbeatConfig, onTimeout: () => void, serverName?: string, logFn?: (msg: string, ...args: any[]) => void, errorFn?: (msg: string, ...args: any[]) => void, onHeartbeatSuccess?: () => void);
|
|
22
23
|
/**
|
|
23
24
|
* Start heartbeat monitoring.
|
|
24
25
|
*/
|
package/dist/src/heartbeat.js
CHANGED
|
@@ -9,17 +9,20 @@ export class HeartbeatManager {
|
|
|
9
9
|
config;
|
|
10
10
|
onTimeout;
|
|
11
11
|
serverName;
|
|
12
|
+
onHeartbeatSuccess;
|
|
12
13
|
intervalTimer = null;
|
|
13
14
|
timeoutTimer = null;
|
|
14
15
|
lastPongTime = 0;
|
|
15
16
|
// Logging functions following feishu pattern
|
|
16
17
|
log;
|
|
17
18
|
error;
|
|
18
|
-
constructor(ws, config, onTimeout, serverName = "unknown", logFn, errorFn
|
|
19
|
+
constructor(ws, config, onTimeout, serverName = "unknown", logFn, errorFn, onHeartbeatSuccess // ✅ 新增:心跳成功回调
|
|
20
|
+
) {
|
|
19
21
|
this.ws = ws;
|
|
20
22
|
this.config = config;
|
|
21
23
|
this.onTimeout = onTimeout;
|
|
22
24
|
this.serverName = serverName;
|
|
25
|
+
this.onHeartbeatSuccess = onHeartbeatSuccess;
|
|
23
26
|
this.log = logFn ?? console.log;
|
|
24
27
|
this.error = errorFn ?? console.error;
|
|
25
28
|
}
|
|
@@ -36,6 +39,8 @@ export class HeartbeatManager {
|
|
|
36
39
|
clearTimeout(this.timeoutTimer);
|
|
37
40
|
this.timeoutTimer = null;
|
|
38
41
|
}
|
|
42
|
+
// ✅ Report health: heartbeat successful
|
|
43
|
+
this.onHeartbeatSuccess?.();
|
|
39
44
|
});
|
|
40
45
|
// Start interval timer
|
|
41
46
|
this.intervalTimer = setInterval(() => {
|
package/dist/src/monitor.d.ts
CHANGED
|
@@ -4,6 +4,11 @@ export type MonitorXYOpts = {
|
|
|
4
4
|
runtime?: RuntimeEnv;
|
|
5
5
|
abortSignal?: AbortSignal;
|
|
6
6
|
accountId?: string;
|
|
7
|
+
setStatus?: (status: {
|
|
8
|
+
lastEventAt?: number;
|
|
9
|
+
lastInboundAt?: number;
|
|
10
|
+
connected?: boolean;
|
|
11
|
+
}) => void;
|
|
7
12
|
};
|
|
8
13
|
/**
|
|
9
14
|
* Monitor XY channel WebSocket connections.
|
package/dist/src/monitor.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { resolveXYConfig } from "./config.js";
|
|
2
|
-
import { getXYWebSocketManager } from "./client.js";
|
|
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.
|
|
@@ -37,19 +40,36 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
37
40
|
throw new Error(`XY account is disabled`);
|
|
38
41
|
}
|
|
39
42
|
const accountId = opts.accountId ?? "default";
|
|
43
|
+
// Create trackEvent function to report health to OpenClaw framework
|
|
44
|
+
const trackEvent = opts.setStatus
|
|
45
|
+
? () => {
|
|
46
|
+
opts.setStatus({ lastEventAt: Date.now(), lastInboundAt: Date.now() });
|
|
47
|
+
}
|
|
48
|
+
: undefined;
|
|
49
|
+
// 🔍 Diagnose WebSocket managers before gateway start
|
|
50
|
+
console.log("🔍 [DIAGNOSTICS] Checking WebSocket managers before gateway start...");
|
|
51
|
+
diagnoseAllManagers();
|
|
40
52
|
// Get WebSocket manager (cached)
|
|
41
53
|
const wsManager = getXYWebSocketManager(account);
|
|
54
|
+
// ✅ Set health event callback for heartbeat reporting
|
|
55
|
+
if (trackEvent) {
|
|
56
|
+
wsManager.setHealthEventCallback(trackEvent);
|
|
57
|
+
}
|
|
42
58
|
// Track logged servers to avoid duplicate logs
|
|
43
59
|
const loggedServers = new Set();
|
|
44
60
|
// Track active message processing to detect duplicates
|
|
45
61
|
const activeMessages = new Set();
|
|
46
62
|
// Create session queue for ordered message processing
|
|
47
63
|
const enqueue = createSessionQueue();
|
|
64
|
+
// Health check interval
|
|
65
|
+
let healthCheckInterval = null;
|
|
48
66
|
return new Promise((resolve, reject) => {
|
|
49
67
|
// Event handlers (defined early so they can be referenced in cleanup)
|
|
50
68
|
const messageHandler = (message, sessionId, serverId) => {
|
|
51
69
|
const messageKey = `${sessionId}::${message.id}`;
|
|
52
70
|
log(`[MONITOR-HANDLER] ####### messageHandler triggered: serverId=${serverId}, sessionId=${sessionId}, messageId=${message.id} #######`);
|
|
71
|
+
// ✅ Report health: received a message
|
|
72
|
+
trackEvent?.();
|
|
53
73
|
// Check for duplicate message handling
|
|
54
74
|
if (activeMessages.has(messageKey)) {
|
|
55
75
|
error(`[MONITOR-HANDLER] ⚠️ WARNING: Duplicate message detected! messageKey=${messageKey}, this may cause duplicate dispatchers!`);
|
|
@@ -68,8 +88,8 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
68
88
|
log(`[MONITOR-HANDLER] ✅ Completed handleXYMessage for messageKey=${messageKey}`);
|
|
69
89
|
}
|
|
70
90
|
catch (err) {
|
|
91
|
+
// ✅ Only log error, don't re-throw to prevent gateway restart
|
|
71
92
|
error(`XY gateway: error handling message from ${serverId}: ${String(err)}`);
|
|
72
|
-
throw err;
|
|
73
93
|
}
|
|
74
94
|
finally {
|
|
75
95
|
// Remove from active messages when done
|
|
@@ -77,37 +97,97 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
77
97
|
log(`[MONITOR-HANDLER] 🧹 Cleaned up messageKey=${messageKey}, remaining active: ${activeMessages.size}`);
|
|
78
98
|
}
|
|
79
99
|
};
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
+
}
|
|
85
133
|
};
|
|
86
134
|
const connectedHandler = (serverId) => {
|
|
87
135
|
if (!loggedServers.has(serverId)) {
|
|
88
136
|
log(`XY gateway: ${serverId} connected`);
|
|
89
137
|
loggedServers.add(serverId);
|
|
90
138
|
}
|
|
139
|
+
// ✅ Report health: connection established
|
|
140
|
+
trackEvent?.();
|
|
141
|
+
opts.setStatus?.({ connected: true });
|
|
91
142
|
};
|
|
92
143
|
const disconnectedHandler = (serverId) => {
|
|
93
144
|
console.warn(`XY gateway: ${serverId} disconnected`);
|
|
94
145
|
loggedServers.delete(serverId);
|
|
146
|
+
// ✅ Report disconnection status (only if all servers disconnected)
|
|
147
|
+
if (loggedServers.size === 0) {
|
|
148
|
+
opts.setStatus?.({ connected: false });
|
|
149
|
+
}
|
|
95
150
|
};
|
|
96
151
|
const errorHandler = (err, serverId) => {
|
|
97
152
|
error(`XY gateway: ${serverId} error: ${String(err)}`);
|
|
98
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
|
+
};
|
|
99
163
|
const cleanup = () => {
|
|
100
164
|
log("XY gateway: cleaning up...");
|
|
165
|
+
// 🔍 Diagnose before cleanup
|
|
166
|
+
console.log("🔍 [DIAGNOSTICS] Checking WebSocket managers before cleanup...");
|
|
167
|
+
diagnoseAllManagers();
|
|
168
|
+
// Stop health check interval
|
|
169
|
+
if (healthCheckInterval) {
|
|
170
|
+
clearInterval(healthCheckInterval);
|
|
171
|
+
healthCheckInterval = null;
|
|
172
|
+
console.log("⏸️ Stopped periodic health check");
|
|
173
|
+
}
|
|
101
174
|
// Remove event handlers to prevent duplicate calls on gateway restart
|
|
102
175
|
wsManager.off("message", messageHandler);
|
|
103
176
|
wsManager.off("connected", connectedHandler);
|
|
104
177
|
wsManager.off("disconnected", disconnectedHandler);
|
|
105
178
|
wsManager.off("error", errorHandler);
|
|
106
|
-
|
|
107
|
-
// wsManager
|
|
179
|
+
wsManager.off("trigger-event", triggerEventHandler);
|
|
180
|
+
// ✅ Disconnect the wsManager to prevent connection leaks
|
|
181
|
+
// This is safe because each gateway lifecycle should have clean connections
|
|
182
|
+
wsManager.disconnect();
|
|
183
|
+
// ✅ Remove manager from cache to prevent reusing dirty state
|
|
184
|
+
removeXYWebSocketManager(account);
|
|
108
185
|
loggedServers.clear();
|
|
109
186
|
activeMessages.clear();
|
|
110
187
|
log(`[MONITOR-HANDLER] 🧹 Cleanup complete, cleared active messages`);
|
|
188
|
+
// 🔍 Diagnose after cleanup
|
|
189
|
+
console.log("🔍 [DIAGNOSTICS] Checking WebSocket managers after cleanup...");
|
|
190
|
+
diagnoseAllManagers();
|
|
111
191
|
};
|
|
112
192
|
const handleAbort = () => {
|
|
113
193
|
log("XY gateway: abort signal received, stopping");
|
|
@@ -126,6 +206,18 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
126
206
|
wsManager.on("connected", connectedHandler);
|
|
127
207
|
wsManager.on("disconnected", disconnectedHandler);
|
|
128
208
|
wsManager.on("error", errorHandler);
|
|
209
|
+
wsManager.on("trigger-event", triggerEventHandler);
|
|
210
|
+
// Start periodic health check (every 5 minutes)
|
|
211
|
+
console.log("🏥 Starting periodic health check (every 5 minutes)...");
|
|
212
|
+
healthCheckInterval = setInterval(() => {
|
|
213
|
+
console.log("🏥 [HEALTH CHECK] Periodic WebSocket diagnostics...");
|
|
214
|
+
diagnoseAllManagers();
|
|
215
|
+
// Auto-cleanup orphan connections
|
|
216
|
+
const cleaned = cleanupOrphanConnections();
|
|
217
|
+
if (cleaned > 0) {
|
|
218
|
+
console.log(`🧹 [HEALTH CHECK] Auto-cleaned ${cleaned} manager(s) with orphan connections`);
|
|
219
|
+
}
|
|
220
|
+
}, 5 * 60 * 1000); // 5 minutes
|
|
129
221
|
// Connect to WebSocket servers
|
|
130
222
|
wsManager.connect()
|
|
131
223
|
.then(() => {
|
package/dist/src/outbound.js
CHANGED
|
@@ -1,9 +1,44 @@
|
|
|
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";
|
|
9
|
+
// File extension to MIME type mapping
|
|
10
|
+
const FILE_TYPE_TO_MIME_TYPE = {
|
|
11
|
+
txt: "text/plain",
|
|
12
|
+
html: "text/html",
|
|
13
|
+
css: "text/css",
|
|
14
|
+
js: "application/javascript",
|
|
15
|
+
json: "application/json",
|
|
16
|
+
png: "image/png",
|
|
17
|
+
jpeg: "image/jpeg",
|
|
18
|
+
jpg: "image/jpeg",
|
|
19
|
+
gif: "image/gif",
|
|
20
|
+
svg: "image/svg+xml",
|
|
21
|
+
pdf: "application/pdf",
|
|
22
|
+
zip: "application/zip",
|
|
23
|
+
doc: "application/msword",
|
|
24
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
25
|
+
xls: "application/vnd.ms-excel",
|
|
26
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
27
|
+
ppt: "application/vnd.ms-powerpoint",
|
|
28
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
29
|
+
mp3: "audio/mpeg",
|
|
30
|
+
mp4: "video/mp4",
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Get MIME type from file extension
|
|
34
|
+
*/
|
|
35
|
+
function getMimeTypeFromFilename(filename) {
|
|
36
|
+
const extension = filename.split(".").pop()?.toLowerCase();
|
|
37
|
+
if (extension && FILE_TYPE_TO_MIME_TYPE[extension]) {
|
|
38
|
+
return FILE_TYPE_TO_MIME_TYPE[extension];
|
|
39
|
+
}
|
|
40
|
+
return "text/plain"; // Default fallback
|
|
41
|
+
}
|
|
7
42
|
/**
|
|
8
43
|
* Outbound adapter for sending messages from OpenClaw to XY.
|
|
9
44
|
* Uses Push service for direct message delivery.
|
|
@@ -32,8 +67,8 @@ export const xyOutbound = {
|
|
|
32
67
|
// If the target doesn't contain "::", try to enhance it with taskId from session context
|
|
33
68
|
if (!trimmedTo.includes("::")) {
|
|
34
69
|
console.log(`[xyOutbound.resolveTarget] Target "${trimmedTo}" missing taskId, looking up session context`);
|
|
35
|
-
// Try to get the
|
|
36
|
-
const sessionContext =
|
|
70
|
+
// Try to get the current session context
|
|
71
|
+
const sessionContext = getCurrentSessionContext();
|
|
37
72
|
if (sessionContext && sessionContext.sessionId === trimmedTo) {
|
|
38
73
|
const enhancedTarget = `${trimmedTo}::${sessionContext.taskId}`;
|
|
39
74
|
console.log(`[xyOutbound.resolveTarget] Enhanced target: ${enhancedTarget}`);
|
|
@@ -72,17 +107,63 @@ export const xyOutbound = {
|
|
|
72
107
|
// The push service will handle it based on config
|
|
73
108
|
actualTo = config.defaultSessionId || "";
|
|
74
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
|
+
}
|
|
75
137
|
// Create push service
|
|
76
138
|
const pushService = new XYPushService(config);
|
|
77
139
|
// Extract title (first 57 chars or first line)
|
|
78
140
|
const title = text.split("\n")[0].slice(0, 57);
|
|
79
|
-
//
|
|
80
|
-
|
|
141
|
+
// Truncate push content to max length 1000
|
|
142
|
+
const pushText = text.length > 1000 ? text.slice(0, 1000) : text;
|
|
143
|
+
// 4. 遍历所有 pushId,依次发送推送通知
|
|
144
|
+
console.log(`[xyOutbound.sendText] 📤 Broadcasting to ${pushIdList.length} pushId(s)...`);
|
|
145
|
+
let successCount = 0;
|
|
146
|
+
let failureCount = 0;
|
|
147
|
+
for (const pushId of pushIdList) {
|
|
148
|
+
try {
|
|
149
|
+
console.log(`[xyOutbound.sendText] Sending to pushId: ${pushId.substring(0, 20)}...`);
|
|
150
|
+
// 传入 pushDataId,使用 kind="data" 格式
|
|
151
|
+
await pushService.sendPush(pushText, title, undefined, actualTo, pushDataId);
|
|
152
|
+
successCount++;
|
|
153
|
+
console.log(`[xyOutbound.sendText] ✅ Sent successfully to pushId: ${pushId.substring(0, 20)}...`);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
failureCount++;
|
|
157
|
+
console.error(`[xyOutbound.sendText] ❌ Failed to send to pushId: ${pushId.substring(0, 20)}...`, error);
|
|
158
|
+
// 单个 pushId 发送失败不影响其他,继续处理下一个
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
console.log(`[xyOutbound.sendText] 📊 Broadcast summary: ${successCount} success, ${failureCount} failures`);
|
|
81
162
|
console.log(`[xyOutbound.sendText] Completed successfully`);
|
|
82
163
|
// Return message info
|
|
83
164
|
return {
|
|
84
165
|
channel: "xiaoyi-channel",
|
|
85
|
-
messageId: Date.now().toString(),
|
|
166
|
+
messageId: pushDataId || Date.now().toString(),
|
|
86
167
|
chatId: actualTo,
|
|
87
168
|
};
|
|
88
169
|
},
|
|
@@ -111,6 +192,15 @@ export const xyOutbound = {
|
|
|
111
192
|
}
|
|
112
193
|
// Upload file
|
|
113
194
|
const fileId = await uploadService.uploadFile(mediaUrl);
|
|
195
|
+
// Check if fileId is empty
|
|
196
|
+
if (!fileId) {
|
|
197
|
+
console.log(`[xyOutbound.sendMedia] ⚠️ File upload failed: fileId is empty, aborting sendMedia`);
|
|
198
|
+
return {
|
|
199
|
+
channel: "xiaoyi-channel",
|
|
200
|
+
messageId: "",
|
|
201
|
+
chatId: to,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
114
204
|
console.log(`[xyOutbound.sendMedia] File uploaded:`, {
|
|
115
205
|
fileId,
|
|
116
206
|
sessionId,
|
|
@@ -119,7 +209,7 @@ export const xyOutbound = {
|
|
|
119
209
|
// Get filename and mime type from mediaUrl
|
|
120
210
|
// mediaUrl may be a local file path or URL
|
|
121
211
|
const fileName = mediaUrl.split("/").pop() || "unknown";
|
|
122
|
-
const mimeType =
|
|
212
|
+
const mimeType = getMimeTypeFromFilename(fileName);
|
|
123
213
|
// Build agent_response message
|
|
124
214
|
const agentResponse = {
|
|
125
215
|
msgType: "agent_response",
|
package/dist/src/parser.d.ts
CHANGED
|
@@ -38,6 +38,18 @@ export declare function isClearContextMessage(method: string): boolean;
|
|
|
38
38
|
* Check if message is a tasks/cancel request.
|
|
39
39
|
*/
|
|
40
40
|
export declare function isTasksCancelMessage(method: string): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Extract push_id from message parts.
|
|
43
|
+
* Looks for push_id in data parts under variables.systemVariables.push_id
|
|
44
|
+
*/
|
|
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;
|
|
41
53
|
/**
|
|
42
54
|
* Validate A2A request structure.
|
|
43
55
|
*/
|
package/dist/src/parser.js
CHANGED
|
@@ -57,6 +57,43 @@ export function isClearContextMessage(method) {
|
|
|
57
57
|
export function isTasksCancelMessage(method) {
|
|
58
58
|
return method === "tasks/cancel" || method === "tasks_cancel";
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Extract push_id from message parts.
|
|
62
|
+
* Looks for push_id in data parts under variables.systemVariables.push_id
|
|
63
|
+
*/
|
|
64
|
+
export function extractPushId(parts) {
|
|
65
|
+
for (const part of parts) {
|
|
66
|
+
if (part.kind === "data" && part.data) {
|
|
67
|
+
const pushId = part.data.variables?.systemVariables?.push_id;
|
|
68
|
+
if (pushId && typeof pushId === "string") {
|
|
69
|
+
return pushId;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Extract Trigger event data from message parts.
|
|
77
|
+
* Looks for Trigger events with pushDataId in data parts.
|
|
78
|
+
*/
|
|
79
|
+
export function extractTriggerData(parts) {
|
|
80
|
+
for (const part of parts) {
|
|
81
|
+
if (part.kind === "data" && part.data) {
|
|
82
|
+
const events = part.data.events;
|
|
83
|
+
if (Array.isArray(events)) {
|
|
84
|
+
for (const event of events) {
|
|
85
|
+
if (event.header?.namespace === "Common" && event.header?.name === "Trigger") {
|
|
86
|
+
const pushDataId = event.payload?.dataMap?.pushDataId;
|
|
87
|
+
if (pushDataId && typeof pushDataId === "string") {
|
|
88
|
+
return { pushDataId };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
60
97
|
/**
|
|
61
98
|
* Validate A2A request structure.
|
|
62
99
|
*/
|
package/dist/src/push.d.ts
CHANGED
|
@@ -5,11 +5,23 @@ import type { XYChannelConfig } from "./types.js";
|
|
|
5
5
|
*/
|
|
6
6
|
export declare class XYPushService {
|
|
7
7
|
private config;
|
|
8
|
+
private readonly DEFAULT_PUSH_URL;
|
|
9
|
+
private readonly REQUEST_FROM;
|
|
8
10
|
constructor(config: XYChannelConfig);
|
|
11
|
+
/**
|
|
12
|
+
* Generate a random trace ID for request tracking.
|
|
13
|
+
*/
|
|
14
|
+
private generateTraceId;
|
|
9
15
|
/**
|
|
10
16
|
* Send a push message to a user session.
|
|
17
|
+
*
|
|
18
|
+
* @param content - Push message content
|
|
19
|
+
* @param title - Push message title
|
|
20
|
+
* @param data - Optional additional data
|
|
21
|
+
* @param sessionId - Optional session ID
|
|
22
|
+
* @param pushDataId - Optional pushDataId for kind="data" format
|
|
11
23
|
*/
|
|
12
|
-
sendPush(content: string, title: string, sessionId?: string): Promise<void>;
|
|
24
|
+
sendPush(content: string, title: string, data?: Record<string, any>, sessionId?: string, pushDataId?: string): Promise<void>;
|
|
13
25
|
/**
|
|
14
26
|
* Send a push message with file attachments.
|
|
15
27
|
*/
|