@ynhcj/xiaoyi-channel 0.0.2 → 0.0.3-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 +115 -35
- package/dist/src/channel.js +22 -1
- package/dist/src/client.d.ts +15 -0
- package/dist/src/client.js +97 -2
- package/dist/src/file-download.js +10 -1
- package/dist/src/file-upload.js +1 -1
- package/dist/src/formatter.d.ts +17 -0
- package/dist/src/formatter.js +86 -6
- package/dist/src/heartbeat.d.ts +2 -1
- package/dist/src/heartbeat.js +9 -1
- package/dist/src/monitor.d.ts +5 -0
- package/dist/src/monitor.js +131 -25
- package/dist/src/onboarding.js +7 -7
- package/dist/src/outbound.js +150 -71
- package/dist/src/parser.d.ts +5 -0
- package/dist/src/parser.js +15 -0
- package/dist/src/push.d.ts +7 -1
- package/dist/src/push.js +110 -19
- package/dist/src/reply-dispatcher.d.ts +1 -0
- package/dist/src/reply-dispatcher.js +210 -57
- 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 +167 -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 +444 -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 +48 -9
- package/dist/src/tools/modify-alarm-tool.d.ts +9 -0
- package/dist/src/tools/modify-alarm-tool.js +474 -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.d.ts +5 -0
- package/dist/src/tools/note-tool.js +146 -0
- package/dist/src/tools/search-alarm-tool.d.ts +8 -0
- package/dist/src/tools/search-alarm-tool.js +389 -0
- package/dist/src/tools/search-calendar-tool.d.ts +12 -0
- package/dist/src/tools/search-calendar-tool.js +259 -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.d.ts +5 -0
- package/dist/src/tools/search-note-tool.js +130 -0
- package/dist/src/tools/search-photo-gallery-tool.d.ts +8 -0
- package/dist/src/tools/search-photo-gallery-tool.js +184 -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 +126 -6
- 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/xiaoyi-gui-tool.d.ts +6 -0
- package/dist/src/tools/xiaoyi-gui-tool.js +151 -0
- package/dist/src/types.d.ts +5 -9
- package/dist/src/utils/config-manager.d.ts +26 -0
- package/dist/src/utils/config-manager.js +56 -0
- package/dist/src/websocket.d.ts +41 -0
- package/dist/src/websocket.js +192 -9
- package/package.json +1 -2
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(() => {
|
|
@@ -67,9 +72,12 @@ export class HeartbeatManager {
|
|
|
67
72
|
}
|
|
68
73
|
try {
|
|
69
74
|
// Send application-level heartbeat message
|
|
75
|
+
console.log(`[WS-${this.serverName}-SEND] Sending heartbeat frame:`, this.config.message);
|
|
70
76
|
this.ws.send(this.config.message);
|
|
77
|
+
console.log(`[WS-${this.serverName}-SEND] Heartbeat message sent, size: ${this.config.message.length} bytes`);
|
|
71
78
|
// Send protocol-level ping
|
|
72
79
|
this.ws.ping();
|
|
80
|
+
console.log(`[WS-${this.serverName}-SEND] Protocol-level ping sent`);
|
|
73
81
|
// Setup timeout timer
|
|
74
82
|
this.timeoutTimer = setTimeout(() => {
|
|
75
83
|
this.error(`Heartbeat timeout for ${this.serverName}`);
|
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,8 @@
|
|
|
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";
|
|
4
6
|
/**
|
|
5
7
|
* Per-session serial queue that ensures messages from the same session are processed
|
|
6
8
|
* in arrival order while allowing different sessions to run concurrently.
|
|
@@ -37,69 +39,173 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
37
39
|
throw new Error(`XY account is disabled`);
|
|
38
40
|
}
|
|
39
41
|
const accountId = opts.accountId ?? "default";
|
|
42
|
+
// Create trackEvent function to report health to OpenClaw framework
|
|
43
|
+
const trackEvent = opts.setStatus
|
|
44
|
+
? () => {
|
|
45
|
+
opts.setStatus({ lastEventAt: Date.now(), lastInboundAt: Date.now() });
|
|
46
|
+
}
|
|
47
|
+
: undefined;
|
|
48
|
+
// 🔍 Diagnose WebSocket managers before gateway start
|
|
49
|
+
console.log("🔍 [DIAGNOSTICS] Checking WebSocket managers before gateway start...");
|
|
50
|
+
diagnoseAllManagers();
|
|
40
51
|
// Get WebSocket manager (cached)
|
|
41
52
|
const wsManager = getXYWebSocketManager(account);
|
|
53
|
+
// ✅ Set health event callback for heartbeat reporting
|
|
54
|
+
if (trackEvent) {
|
|
55
|
+
wsManager.setHealthEventCallback(trackEvent);
|
|
56
|
+
}
|
|
42
57
|
// Track logged servers to avoid duplicate logs
|
|
43
58
|
const loggedServers = new Set();
|
|
59
|
+
// Track active message processing to detect duplicates
|
|
60
|
+
const activeMessages = new Set();
|
|
44
61
|
// Create session queue for ordered message processing
|
|
45
62
|
const enqueue = createSessionQueue();
|
|
63
|
+
// Health check interval
|
|
64
|
+
let healthCheckInterval = null;
|
|
46
65
|
return new Promise((resolve, reject) => {
|
|
47
|
-
|
|
48
|
-
log("XY gateway: cleaning up...");
|
|
49
|
-
wsManager.disconnect();
|
|
50
|
-
loggedServers.clear();
|
|
51
|
-
};
|
|
52
|
-
const handleAbort = () => {
|
|
53
|
-
log("XY gateway: abort signal received, stopping");
|
|
54
|
-
cleanup();
|
|
55
|
-
log("XY gateway stopped");
|
|
56
|
-
resolve();
|
|
57
|
-
};
|
|
58
|
-
if (opts.abortSignal?.aborted) {
|
|
59
|
-
cleanup();
|
|
60
|
-
resolve();
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
opts.abortSignal?.addEventListener("abort", handleAbort, { once: true });
|
|
64
|
-
// Setup event handlers
|
|
66
|
+
// Event handlers (defined early so they can be referenced in cleanup)
|
|
65
67
|
const messageHandler = (message, sessionId, serverId) => {
|
|
68
|
+
const messageKey = `${sessionId}::${message.id}`;
|
|
69
|
+
log(`[MONITOR-HANDLER] ####### messageHandler triggered: serverId=${serverId}, sessionId=${sessionId}, messageId=${message.id} #######`);
|
|
70
|
+
// ✅ Report health: received a message
|
|
71
|
+
trackEvent?.();
|
|
72
|
+
// Check for duplicate message handling
|
|
73
|
+
if (activeMessages.has(messageKey)) {
|
|
74
|
+
error(`[MONITOR-HANDLER] ⚠️ WARNING: Duplicate message detected! messageKey=${messageKey}, this may cause duplicate dispatchers!`);
|
|
75
|
+
}
|
|
76
|
+
activeMessages.add(messageKey);
|
|
77
|
+
log(`[MONITOR-HANDLER] 📝 Active messages count: ${activeMessages.size}, messageKey: ${messageKey}`);
|
|
66
78
|
const task = async () => {
|
|
67
79
|
try {
|
|
80
|
+
log(`[MONITOR-HANDLER] 🚀 Starting handleXYMessage for messageKey=${messageKey}`);
|
|
68
81
|
await handleXYMessage({
|
|
69
82
|
cfg,
|
|
70
83
|
runtime,
|
|
71
84
|
message,
|
|
72
85
|
accountId, // ✅ Pass accountId ("default")
|
|
73
86
|
});
|
|
87
|
+
log(`[MONITOR-HANDLER] ✅ Completed handleXYMessage for messageKey=${messageKey}`);
|
|
74
88
|
}
|
|
75
89
|
catch (err) {
|
|
90
|
+
// ✅ Only log error, don't re-throw to prevent gateway restart
|
|
76
91
|
error(`XY gateway: error handling message from ${serverId}: ${String(err)}`);
|
|
77
|
-
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
// Remove from active messages when done
|
|
95
|
+
activeMessages.delete(messageKey);
|
|
96
|
+
log(`[MONITOR-HANDLER] 🧹 Cleaned up messageKey=${messageKey}, remaining active: ${activeMessages.size}`);
|
|
78
97
|
}
|
|
79
98
|
};
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
99
|
+
// 🔑 核心改造:检测steer模式
|
|
100
|
+
// 需要提前解析消息以获取sessionId
|
|
101
|
+
try {
|
|
102
|
+
const parsed = parseA2AMessage(message);
|
|
103
|
+
const steerMode = cfg.messages?.queue?.mode === "steer";
|
|
104
|
+
const hasActiveRun = hasActiveTask(parsed.sessionId);
|
|
105
|
+
if (steerMode && hasActiveRun) {
|
|
106
|
+
// Steer模式且有活跃任务:不入队列,直接并发执行
|
|
107
|
+
log(`[MONITOR-HANDLER] 🔄 STEER MODE: Executing concurrently for messageKey=${messageKey}`);
|
|
108
|
+
log(`[MONITOR-HANDLER] - sessionId: ${parsed.sessionId}`);
|
|
109
|
+
log(`[MONITOR-HANDLER] - Bypassing queue to allow message insertion`);
|
|
110
|
+
void task().catch((err) => {
|
|
111
|
+
error(`XY gateway: concurrent steer task failed for ${messageKey}: ${String(err)}`);
|
|
112
|
+
activeMessages.delete(messageKey);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
// 正常模式:入队列串行执行
|
|
117
|
+
log(`[MONITOR-HANDLER] 📋 NORMAL MODE: Enqueuing for messageKey=${messageKey}`);
|
|
118
|
+
void enqueue(sessionId, task).catch((err) => {
|
|
119
|
+
error(`XY gateway: queue processing failed for session ${sessionId}: ${String(err)}`);
|
|
120
|
+
activeMessages.delete(messageKey);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch (parseErr) {
|
|
125
|
+
// 解析失败,回退到正常队列模式
|
|
126
|
+
error(`[MONITOR-HANDLER] Failed to parse message for steer detection: ${String(parseErr)}`);
|
|
127
|
+
void enqueue(sessionId, task).catch((err) => {
|
|
128
|
+
error(`XY gateway: queue processing failed for session ${sessionId}: ${String(err)}`);
|
|
129
|
+
activeMessages.delete(messageKey);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
84
132
|
};
|
|
85
133
|
const connectedHandler = (serverId) => {
|
|
86
134
|
if (!loggedServers.has(serverId)) {
|
|
87
135
|
log(`XY gateway: ${serverId} connected`);
|
|
88
136
|
loggedServers.add(serverId);
|
|
89
137
|
}
|
|
138
|
+
// ✅ Report health: connection established
|
|
139
|
+
trackEvent?.();
|
|
140
|
+
opts.setStatus?.({ connected: true });
|
|
90
141
|
};
|
|
91
142
|
const disconnectedHandler = (serverId) => {
|
|
92
143
|
console.warn(`XY gateway: ${serverId} disconnected`);
|
|
93
144
|
loggedServers.delete(serverId);
|
|
145
|
+
// ✅ Report disconnection status (only if all servers disconnected)
|
|
146
|
+
if (loggedServers.size === 0) {
|
|
147
|
+
opts.setStatus?.({ connected: false });
|
|
148
|
+
}
|
|
94
149
|
};
|
|
95
150
|
const errorHandler = (err, serverId) => {
|
|
96
151
|
error(`XY gateway: ${serverId} error: ${String(err)}`);
|
|
97
152
|
};
|
|
98
|
-
|
|
153
|
+
const cleanup = () => {
|
|
154
|
+
log("XY gateway: cleaning up...");
|
|
155
|
+
// 🔍 Diagnose before cleanup
|
|
156
|
+
console.log("🔍 [DIAGNOSTICS] Checking WebSocket managers before cleanup...");
|
|
157
|
+
diagnoseAllManagers();
|
|
158
|
+
// Stop health check interval
|
|
159
|
+
if (healthCheckInterval) {
|
|
160
|
+
clearInterval(healthCheckInterval);
|
|
161
|
+
healthCheckInterval = null;
|
|
162
|
+
console.log("⏸️ Stopped periodic health check");
|
|
163
|
+
}
|
|
164
|
+
// Remove event handlers to prevent duplicate calls on gateway restart
|
|
165
|
+
wsManager.off("message", messageHandler);
|
|
166
|
+
wsManager.off("connected", connectedHandler);
|
|
167
|
+
wsManager.off("disconnected", disconnectedHandler);
|
|
168
|
+
wsManager.off("error", errorHandler);
|
|
169
|
+
// ✅ Disconnect the wsManager to prevent connection leaks
|
|
170
|
+
// This is safe because each gateway lifecycle should have clean connections
|
|
171
|
+
wsManager.disconnect();
|
|
172
|
+
// ✅ Remove manager from cache to prevent reusing dirty state
|
|
173
|
+
removeXYWebSocketManager(account);
|
|
174
|
+
loggedServers.clear();
|
|
175
|
+
activeMessages.clear();
|
|
176
|
+
log(`[MONITOR-HANDLER] 🧹 Cleanup complete, cleared active messages`);
|
|
177
|
+
// 🔍 Diagnose after cleanup
|
|
178
|
+
console.log("🔍 [DIAGNOSTICS] Checking WebSocket managers after cleanup...");
|
|
179
|
+
diagnoseAllManagers();
|
|
180
|
+
};
|
|
181
|
+
const handleAbort = () => {
|
|
182
|
+
log("XY gateway: abort signal received, stopping");
|
|
183
|
+
cleanup();
|
|
184
|
+
log("XY gateway stopped");
|
|
185
|
+
resolve();
|
|
186
|
+
};
|
|
187
|
+
if (opts.abortSignal?.aborted) {
|
|
188
|
+
cleanup();
|
|
189
|
+
resolve();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
opts.abortSignal?.addEventListener("abort", handleAbort, { once: true });
|
|
193
|
+
// Register event handlers (handlers are defined above in cleanup scope)
|
|
99
194
|
wsManager.on("message", messageHandler);
|
|
100
195
|
wsManager.on("connected", connectedHandler);
|
|
101
196
|
wsManager.on("disconnected", disconnectedHandler);
|
|
102
197
|
wsManager.on("error", errorHandler);
|
|
198
|
+
// Start periodic health check (every 5 minutes)
|
|
199
|
+
console.log("🏥 Starting periodic health check (every 5 minutes)...");
|
|
200
|
+
healthCheckInterval = setInterval(() => {
|
|
201
|
+
console.log("🏥 [HEALTH CHECK] Periodic WebSocket diagnostics...");
|
|
202
|
+
diagnoseAllManagers();
|
|
203
|
+
// Auto-cleanup orphan connections
|
|
204
|
+
const cleaned = cleanupOrphanConnections();
|
|
205
|
+
if (cleaned > 0) {
|
|
206
|
+
console.log(`🧹 [HEALTH CHECK] Auto-cleaned ${cleaned} manager(s) with orphan connections`);
|
|
207
|
+
}
|
|
208
|
+
}, 5 * 60 * 1000); // 5 minutes
|
|
103
209
|
// Connect to WebSocket servers
|
|
104
210
|
wsManager.connect()
|
|
105
211
|
.then(() => {
|
package/dist/src/onboarding.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
const channel = "
|
|
1
|
+
const channel = "xiaoyi-channel";
|
|
2
2
|
/**
|
|
3
3
|
* Check if XY channel is properly configured with required fields
|
|
4
4
|
*/
|
|
5
5
|
function isXYConfigured(cfg) {
|
|
6
6
|
try {
|
|
7
|
-
const xyConfig = cfg.channels?.
|
|
7
|
+
const xyConfig = cfg.channels?.["xiaoyi-channel"];
|
|
8
8
|
if (!xyConfig) {
|
|
9
9
|
return false;
|
|
10
10
|
}
|
|
@@ -26,7 +26,7 @@ function isXYConfigured(cfg) {
|
|
|
26
26
|
*/
|
|
27
27
|
async function getStatus({ cfg }) {
|
|
28
28
|
const configured = isXYConfigured(cfg);
|
|
29
|
-
const xyConfig = cfg.channels?.
|
|
29
|
+
const xyConfig = cfg.channels?.["xiaoyi-channel"];
|
|
30
30
|
const statusLines = [];
|
|
31
31
|
if (configured) {
|
|
32
32
|
const wsUrl1 = xyConfig?.wsUrl1 || "ws://localhost:8765/ws/link";
|
|
@@ -49,7 +49,7 @@ async function getStatus({ cfg }) {
|
|
|
49
49
|
*/
|
|
50
50
|
async function configure({ cfg, prompter, }) {
|
|
51
51
|
// Note current configuration status
|
|
52
|
-
const currentConfig = cfg.channels?.
|
|
52
|
+
const currentConfig = cfg.channels?.["xiaoyi-channel"];
|
|
53
53
|
const isUpdate = Boolean(currentConfig);
|
|
54
54
|
await prompter.note([
|
|
55
55
|
"XY Channel - 小艺 A2A 协议配置",
|
|
@@ -117,7 +117,7 @@ async function configure({ cfg, prompter, }) {
|
|
|
117
117
|
...cfg,
|
|
118
118
|
channels: {
|
|
119
119
|
...cfg.channels,
|
|
120
|
-
|
|
120
|
+
"xiaoyi-channel": {
|
|
121
121
|
enabled: true,
|
|
122
122
|
wsUrl1: wsUrl1.trim(),
|
|
123
123
|
wsUrl2: wsUrl2.trim(),
|
|
@@ -164,8 +164,8 @@ export const xyOnboardingAdapter = {
|
|
|
164
164
|
...cfg,
|
|
165
165
|
channels: {
|
|
166
166
|
...cfg.channels,
|
|
167
|
-
|
|
168
|
-
...(cfg.channels?.
|
|
167
|
+
"xiaoyi-channel": {
|
|
168
|
+
...(cfg.channels?.["xiaoyi-channel"] || {}),
|
|
169
169
|
enabled: false,
|
|
170
170
|
},
|
|
171
171
|
},
|
package/dist/src/outbound.js
CHANGED
|
@@ -1,8 +1,41 @@
|
|
|
1
1
|
import { resolveXYConfig } from "./config.js";
|
|
2
|
-
import { XYFileUploadService } from "./file-upload.js";
|
|
3
2
|
import { XYPushService } from "./push.js";
|
|
3
|
+
import { getCurrentSessionContext } from "./tools/session-manager.js";
|
|
4
4
|
// Special marker for default push delivery when no target is specified
|
|
5
5
|
const DEFAULT_PUSH_MARKER = "default";
|
|
6
|
+
// File extension to MIME type mapping
|
|
7
|
+
const FILE_TYPE_TO_MIME_TYPE = {
|
|
8
|
+
txt: "text/plain",
|
|
9
|
+
html: "text/html",
|
|
10
|
+
css: "text/css",
|
|
11
|
+
js: "application/javascript",
|
|
12
|
+
json: "application/json",
|
|
13
|
+
png: "image/png",
|
|
14
|
+
jpeg: "image/jpeg",
|
|
15
|
+
jpg: "image/jpeg",
|
|
16
|
+
gif: "image/gif",
|
|
17
|
+
svg: "image/svg+xml",
|
|
18
|
+
pdf: "application/pdf",
|
|
19
|
+
zip: "application/zip",
|
|
20
|
+
doc: "application/msword",
|
|
21
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
22
|
+
xls: "application/vnd.ms-excel",
|
|
23
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
24
|
+
ppt: "application/vnd.ms-powerpoint",
|
|
25
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
26
|
+
mp3: "audio/mpeg",
|
|
27
|
+
mp4: "video/mp4",
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Get MIME type from file extension
|
|
31
|
+
*/
|
|
32
|
+
function getMimeTypeFromFilename(filename) {
|
|
33
|
+
const extension = filename.split(".").pop()?.toLowerCase();
|
|
34
|
+
if (extension && FILE_TYPE_TO_MIME_TYPE[extension]) {
|
|
35
|
+
return FILE_TYPE_TO_MIME_TYPE[extension];
|
|
36
|
+
}
|
|
37
|
+
return "text/plain"; // Default fallback
|
|
38
|
+
}
|
|
6
39
|
/**
|
|
7
40
|
* Outbound adapter for sending messages from OpenClaw to XY.
|
|
8
41
|
* Uses Push service for direct message delivery.
|
|
@@ -14,6 +47,9 @@ export const xyOutbound = {
|
|
|
14
47
|
* Resolve delivery target for XY channel.
|
|
15
48
|
* When no target is specified (e.g., in cron jobs with announce mode),
|
|
16
49
|
* returns a default marker that will be handled by sendText.
|
|
50
|
+
*
|
|
51
|
+
* For message tool calls, if only sessionId is provided, it will look up
|
|
52
|
+
* the active session context to construct the full "sessionId::taskId" format.
|
|
17
53
|
*/
|
|
18
54
|
resolveTarget: ({ cfg, to, accountId, mode }) => {
|
|
19
55
|
// If no target provided, use default marker for push delivery
|
|
@@ -24,11 +60,30 @@ export const xyOutbound = {
|
|
|
24
60
|
to: DEFAULT_PUSH_MARKER,
|
|
25
61
|
};
|
|
26
62
|
}
|
|
27
|
-
|
|
28
|
-
|
|
63
|
+
const trimmedTo = to.trim();
|
|
64
|
+
// If the target doesn't contain "::", try to enhance it with taskId from session context
|
|
65
|
+
if (!trimmedTo.includes("::")) {
|
|
66
|
+
console.log(`[xyOutbound.resolveTarget] Target "${trimmedTo}" missing taskId, looking up session context`);
|
|
67
|
+
// Try to get the current session context
|
|
68
|
+
const sessionContext = getCurrentSessionContext();
|
|
69
|
+
if (sessionContext && sessionContext.sessionId === trimmedTo) {
|
|
70
|
+
const enhancedTarget = `${trimmedTo}::${sessionContext.taskId}`;
|
|
71
|
+
console.log(`[xyOutbound.resolveTarget] Enhanced target: ${enhancedTarget}`);
|
|
72
|
+
return {
|
|
73
|
+
ok: true,
|
|
74
|
+
to: enhancedTarget,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
console.log(`[xyOutbound.resolveTarget] Could not find matching session context for "${trimmedTo}"`);
|
|
79
|
+
// Still return the original target, but it may fail in sendMedia
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Otherwise, use the provided target (either already in correct format or for sendText)
|
|
83
|
+
console.log(`[xyOutbound.resolveTarget] Using provided target:`, trimmedTo);
|
|
29
84
|
return {
|
|
30
85
|
ok: true,
|
|
31
|
-
to:
|
|
86
|
+
to: trimmedTo,
|
|
32
87
|
};
|
|
33
88
|
},
|
|
34
89
|
sendText: async ({ cfg, to, text, accountId }) => {
|
|
@@ -53,12 +108,14 @@ export const xyOutbound = {
|
|
|
53
108
|
const pushService = new XYPushService(config);
|
|
54
109
|
// Extract title (first 57 chars or first line)
|
|
55
110
|
const title = text.split("\n")[0].slice(0, 57);
|
|
56
|
-
//
|
|
57
|
-
|
|
111
|
+
// Truncate push content to max length 1000
|
|
112
|
+
const pushText = text.length > 1000 ? text.slice(0, 1000) : text;
|
|
113
|
+
// Send push message (content, title, data, sessionId)
|
|
114
|
+
await pushService.sendPush(pushText, title, undefined, actualTo);
|
|
58
115
|
console.log(`[xyOutbound.sendText] Completed successfully`);
|
|
59
116
|
// Return message info
|
|
60
117
|
return {
|
|
61
|
-
channel: "
|
|
118
|
+
channel: "xiaoyi-channel",
|
|
62
119
|
messageId: Date.now().toString(),
|
|
63
120
|
chatId: actualTo,
|
|
64
121
|
};
|
|
@@ -72,72 +129,94 @@ export const xyOutbound = {
|
|
|
72
129
|
mediaUrl,
|
|
73
130
|
mediaLocalRoots,
|
|
74
131
|
});
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
const [sessionId, taskId] = parts;
|
|
81
|
-
// Resolve configuration
|
|
82
|
-
const config = resolveXYConfig(cfg);
|
|
83
|
-
// Create upload service
|
|
84
|
-
const uploadService = new XYFileUploadService(config.fileUploadUrl, config.apiKey, config.uid);
|
|
85
|
-
// Validate mediaUrl
|
|
86
|
-
if (!mediaUrl) {
|
|
87
|
-
throw new Error("mediaUrl is required for sendMedia");
|
|
88
|
-
}
|
|
89
|
-
// Upload file
|
|
90
|
-
const fileId = await uploadService.uploadFile(mediaUrl);
|
|
91
|
-
console.log(`[xyOutbound.sendMedia] File uploaded:`, {
|
|
92
|
-
fileId,
|
|
93
|
-
sessionId,
|
|
94
|
-
taskId,
|
|
95
|
-
});
|
|
96
|
-
// Get filename and mime type from mediaUrl
|
|
97
|
-
// mediaUrl may be a local file path or URL
|
|
98
|
-
const fileName = mediaUrl.split("/").pop() || "unknown";
|
|
99
|
-
const mimeType = text?.match(/\[ MediaType: ([^\]]+)\]/)?.[1] || "application/octet-stream";
|
|
100
|
-
// Build agent_response message
|
|
101
|
-
const agentResponse = {
|
|
102
|
-
msgType: "agent_response",
|
|
103
|
-
agentId: config.agentId,
|
|
104
|
-
sessionId: sessionId,
|
|
105
|
-
taskId: taskId,
|
|
106
|
-
msgDetail: JSON.stringify({
|
|
107
|
-
jsonrpc: "2.0",
|
|
108
|
-
id: taskId,
|
|
109
|
-
result: {
|
|
110
|
-
kind: "artifact-update",
|
|
111
|
-
append: true,
|
|
112
|
-
lastChunk: false,
|
|
113
|
-
final: false,
|
|
114
|
-
artifact: {
|
|
115
|
-
artifactId: taskId,
|
|
116
|
-
parts: [
|
|
117
|
-
{
|
|
118
|
-
kind: "file",
|
|
119
|
-
file: {
|
|
120
|
-
name: fileName,
|
|
121
|
-
mimeType: mimeType,
|
|
122
|
-
fileId: fileId,
|
|
123
|
-
},
|
|
124
|
-
},
|
|
125
|
-
],
|
|
126
|
-
},
|
|
127
|
-
},
|
|
128
|
-
error: { code: 0 },
|
|
129
|
-
}),
|
|
130
|
-
};
|
|
131
|
-
// Get WebSocket manager and send message
|
|
132
|
-
const { getXYWebSocketManager } = await import("./client.js");
|
|
133
|
-
const wsManager = getXYWebSocketManager(config);
|
|
134
|
-
await wsManager.sendMessage(sessionId, agentResponse);
|
|
135
|
-
console.log(`[xyOutbound.sendMedia] WebSocket message sent successfully`);
|
|
136
|
-
// Return message info
|
|
132
|
+
// All sendMedia processing logic has been disabled
|
|
133
|
+
// Use send_file_to_user tool instead for file transfers to user device
|
|
134
|
+
console.log(`[xyOutbound.sendMedia] Processing disabled, use send_file_to_user tool`);
|
|
135
|
+
// Return empty message info
|
|
137
136
|
return {
|
|
138
|
-
channel: "
|
|
139
|
-
messageId:
|
|
137
|
+
channel: "xiaoyi-channel",
|
|
138
|
+
messageId: "",
|
|
140
139
|
chatId: to,
|
|
141
140
|
};
|
|
141
|
+
// // Parse to: "sessionId::taskId"
|
|
142
|
+
// const parts = to.split("::");
|
|
143
|
+
// if (parts.length !== 2) {
|
|
144
|
+
// throw new Error(`Invalid to format: "${to}". Expected "sessionId::taskId"`);
|
|
145
|
+
// }
|
|
146
|
+
// const [sessionId, taskId] = parts;
|
|
147
|
+
// // Resolve configuration
|
|
148
|
+
// const config = resolveXYConfig(cfg);
|
|
149
|
+
// // Create upload service
|
|
150
|
+
// const uploadService = new XYFileUploadService(
|
|
151
|
+
// config.fileUploadUrl,
|
|
152
|
+
// config.apiKey,
|
|
153
|
+
// config.uid
|
|
154
|
+
// );
|
|
155
|
+
// // Validate mediaUrl
|
|
156
|
+
// if (!mediaUrl) {
|
|
157
|
+
// throw new Error("mediaUrl is required for sendMedia");
|
|
158
|
+
// }
|
|
159
|
+
// // Upload file
|
|
160
|
+
// const fileId = await uploadService.uploadFile(mediaUrl);
|
|
161
|
+
// // Check if fileId is empty
|
|
162
|
+
// if (!fileId) {
|
|
163
|
+
// console.log(`[xyOutbound.sendMedia] ⚠️ File upload failed: fileId is empty, aborting sendMedia`);
|
|
164
|
+
// return {
|
|
165
|
+
// channel: "xiaoyi-channel",
|
|
166
|
+
// messageId: "",
|
|
167
|
+
// chatId: to,
|
|
168
|
+
// };
|
|
169
|
+
// }
|
|
170
|
+
// console.log(`[xyOutbound.sendMedia] File uploaded:`, {
|
|
171
|
+
// fileId,
|
|
172
|
+
// sessionId,
|
|
173
|
+
// taskId,
|
|
174
|
+
// });
|
|
175
|
+
// // Get filename and mime type from mediaUrl
|
|
176
|
+
// // mediaUrl may be a local file path or URL
|
|
177
|
+
// const fileName = mediaUrl.split("/").pop() || "unknown";
|
|
178
|
+
// const mimeType = getMimeTypeFromFilename(fileName);
|
|
179
|
+
// // Build agent_response message
|
|
180
|
+
// const agentResponse: OutboundWebSocketMessage = {
|
|
181
|
+
// msgType: "agent_response",
|
|
182
|
+
// agentId: config.agentId,
|
|
183
|
+
// sessionId: sessionId,
|
|
184
|
+
// taskId: taskId,
|
|
185
|
+
// msgDetail: JSON.stringify({
|
|
186
|
+
// jsonrpc: "2.0",
|
|
187
|
+
// id: taskId,
|
|
188
|
+
// result: {
|
|
189
|
+
// kind: "artifact-update",
|
|
190
|
+
// append: true,
|
|
191
|
+
// lastChunk: false,
|
|
192
|
+
// final: false,
|
|
193
|
+
// artifact: {
|
|
194
|
+
// artifactId: taskId,
|
|
195
|
+
// parts: [
|
|
196
|
+
// {
|
|
197
|
+
// kind: "file",
|
|
198
|
+
// file: {
|
|
199
|
+
// name: fileName,
|
|
200
|
+
// mimeType: mimeType,
|
|
201
|
+
// fileId: fileId,
|
|
202
|
+
// },
|
|
203
|
+
// },
|
|
204
|
+
// ],
|
|
205
|
+
// },
|
|
206
|
+
// },
|
|
207
|
+
// error: { code: 0 },
|
|
208
|
+
// }),
|
|
209
|
+
// };
|
|
210
|
+
// // Get WebSocket manager and send message
|
|
211
|
+
// const { getXYWebSocketManager } = await import("./client.js");
|
|
212
|
+
// const wsManager = getXYWebSocketManager(config);
|
|
213
|
+
// await wsManager.sendMessage(sessionId, agentResponse);
|
|
214
|
+
// console.log(`[xyOutbound.sendMedia] WebSocket message sent successfully`);
|
|
215
|
+
// // Return message info
|
|
216
|
+
// return {
|
|
217
|
+
// channel: "xiaoyi-channel",
|
|
218
|
+
// messageId: fileId,
|
|
219
|
+
// chatId: to,
|
|
220
|
+
// };
|
|
142
221
|
},
|
|
143
222
|
};
|
package/dist/src/parser.d.ts
CHANGED
|
@@ -38,6 +38,11 @@ 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;
|
|
41
46
|
/**
|
|
42
47
|
* Validate A2A request structure.
|
|
43
48
|
*/
|
package/dist/src/parser.js
CHANGED
|
@@ -57,6 +57,21 @@ 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
|
+
}
|
|
60
75
|
/**
|
|
61
76
|
* Validate A2A request structure.
|
|
62
77
|
*/
|
package/dist/src/push.d.ts
CHANGED
|
@@ -5,11 +5,17 @@ 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.
|
|
11
17
|
*/
|
|
12
|
-
sendPush(content: string, title: string, sessionId?: string): Promise<void>;
|
|
18
|
+
sendPush(content: string, title: string, data?: Record<string, any>, sessionId?: string): Promise<void>;
|
|
13
19
|
/**
|
|
14
20
|
* Send a push message with file attachments.
|
|
15
21
|
*/
|