@ynhcj/xiaoyi-channel 0.0.35-beta → 0.0.35-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/index.d.ts +0 -2
- package/dist/index.js +42 -2
- package/dist/src/bot.js +53 -47
- package/dist/src/channel.js +24 -8
- package/dist/src/client.js +11 -33
- package/dist/src/config.js +2 -2
- package/dist/src/cspl/call-api.d.ts +3 -0
- package/dist/src/cspl/call-api.js +86 -0
- package/dist/src/cspl/config.d.ts +19 -0
- package/dist/src/cspl/config.js +50 -0
- package/dist/src/cspl/constants.d.ts +43 -0
- package/dist/src/cspl/constants.js +22 -0
- package/dist/src/cspl/utils.d.ts +10 -0
- package/dist/src/cspl/utils.js +57 -0
- package/dist/src/file-upload.d.ts +5 -0
- package/dist/src/file-upload.js +88 -6
- package/dist/src/formatter.d.ts +14 -0
- package/dist/src/formatter.js +49 -28
- package/dist/src/heartbeat.js +0 -4
- package/dist/src/monitor.js +19 -10
- package/dist/src/onboarding.d.ts +3 -4
- package/dist/src/onboarding.js +2 -2
- package/dist/src/outbound.d.ts +2 -1
- package/dist/src/outbound.js +119 -103
- 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 +27 -40
- package/dist/src/reply-dispatcher.d.ts +4 -0
- package/dist/src/reply-dispatcher.js +43 -7
- package/dist/src/steer-injector.d.ts +16 -0
- package/dist/src/steer-injector.js +74 -0
- package/dist/src/thread-bindings.d.ts +54 -0
- package/dist/src/thread-bindings.js +214 -0
- package/dist/src/tools/calendar-tool.js +5 -38
- package/dist/src/tools/call-phone-tool.js +1 -42
- package/dist/src/tools/create-alarm-tool.js +9 -108
- package/dist/src/tools/delete-alarm-tool.js +5 -69
- package/dist/src/tools/image-reading-tool.d.ts +5 -0
- package/dist/src/tools/image-reading-tool.js +328 -0
- package/dist/src/tools/location-tool.js +6 -40
- package/dist/src/tools/modify-alarm-tool.js +7 -113
- package/dist/src/tools/modify-note-tool.js +3 -41
- package/dist/src/tools/note-tool.js +4 -16
- package/dist/src/tools/search-alarm-tool.js +14 -118
- package/dist/src/tools/search-calendar-tool.js +8 -82
- package/dist/src/tools/search-contact-tool.js +2 -55
- package/dist/src/tools/search-file-tool.js +4 -61
- package/dist/src/tools/search-message-tool.js +2 -59
- package/dist/src/tools/search-note-tool.js +4 -22
- package/dist/src/tools/search-photo-gallery-tool.js +38 -59
- package/dist/src/tools/send-file-to-user-tool.js +1 -39
- package/dist/src/tools/send-message-tool.js +5 -56
- package/dist/src/tools/session-manager.js +0 -45
- package/dist/src/tools/timestamp-to-utc8-tool.d.ts +12 -0
- package/dist/src/tools/timestamp-to-utc8-tool.js +104 -0
- package/dist/src/tools/upload-file-tool.js +0 -49
- package/dist/src/tools/upload-photo-tool.js +0 -42
- package/dist/src/tools/view-push-result-tool.d.ts +5 -0
- package/dist/src/tools/view-push-result-tool.js +107 -0
- package/dist/src/tools/xiaoyi-collection-tool.d.ts +5 -0
- package/dist/src/tools/xiaoyi-collection-tool.js +112 -0
- package/dist/src/tools/xiaoyi-gui-tool.js +0 -34
- 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 +25 -31
- package/dist/src/websocket.js +239 -271
- package/package.json +2 -2
- package/dist/src/tools/search-photo-tool.d.ts +0 -9
- package/dist/src/tools/search-photo-tool.js +0 -270
- package/dist/src/utils/session.d.ts +0 -34
- package/dist/src/utils/session.js +0 -50
package/dist/src/outbound.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { resolveXYConfig } from "./config.js";
|
|
2
|
+
import { XYFileUploadService } from "./file-upload.js";
|
|
2
3
|
import { XYPushService } from "./push.js";
|
|
3
4
|
import { getCurrentSessionContext } from "./tools/session-manager.js";
|
|
5
|
+
import { savePushData } from "./utils/pushdata-manager.js";
|
|
6
|
+
import { getAllPushIds } from "./utils/pushid-manager.js";
|
|
4
7
|
// Special marker for default push delivery when no target is specified
|
|
5
8
|
const DEFAULT_PUSH_MARKER = "default";
|
|
6
9
|
// File extension to MIME type mapping
|
|
@@ -87,13 +90,6 @@ export const xyOutbound = {
|
|
|
87
90
|
};
|
|
88
91
|
},
|
|
89
92
|
sendText: async ({ cfg, to, text, accountId }) => {
|
|
90
|
-
// Log parameters
|
|
91
|
-
console.log(`[xyOutbound.sendText] Called with:`, {
|
|
92
|
-
to,
|
|
93
|
-
accountId,
|
|
94
|
-
textLength: text?.length || 0,
|
|
95
|
-
textPreview: text?.slice(0, 100),
|
|
96
|
-
});
|
|
97
93
|
// Resolve configuration
|
|
98
94
|
const config = resolveXYConfig(cfg);
|
|
99
95
|
// Handle default push marker (for cron jobs without explicit target)
|
|
@@ -104,119 +100,139 @@ export const xyOutbound = {
|
|
|
104
100
|
// The push service will handle it based on config
|
|
105
101
|
actualTo = config.defaultSessionId || "";
|
|
106
102
|
}
|
|
103
|
+
// 1. 持久化推送消息内容,获取 pushDataId
|
|
104
|
+
console.log(`[xyOutbound.sendText] Saving push data to local storage...`);
|
|
105
|
+
let pushDataId;
|
|
106
|
+
try {
|
|
107
|
+
pushDataId = await savePushData(text);
|
|
108
|
+
console.log(`[xyOutbound.sendText] ✅ Push data saved with ID: ${pushDataId.substring(0, 20)}`);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
console.error(`[xyOutbound.sendText] ❌ Failed to save push data:`, error);
|
|
112
|
+
// 如果持久化失败,仍然继续发送(不阻塞主流程)
|
|
113
|
+
pushDataId = "";
|
|
114
|
+
}
|
|
115
|
+
// 2. 读取所有 pushId
|
|
116
|
+
console.log(`[xyOutbound.sendText] Loading all pushIds...`);
|
|
117
|
+
let pushIdList = [];
|
|
118
|
+
try {
|
|
119
|
+
pushIdList = await getAllPushIds();
|
|
120
|
+
console.log(`[xyOutbound.sendText] ✅ Loaded ${pushIdList.length} pushIds`);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
console.error(`[xyOutbound.sendText] ❌ Failed to load pushIds:`, error);
|
|
124
|
+
}
|
|
125
|
+
// 3. 如果 pushIdList 为空,回退到原有逻辑(使用 config pushId)
|
|
126
|
+
if (pushIdList.length === 0) {
|
|
127
|
+
console.log(`[xyOutbound.sendText] ⚠️ No pushIds found, falling back to config pushId`);
|
|
128
|
+
pushIdList = [config.pushId];
|
|
129
|
+
}
|
|
107
130
|
// Create push service
|
|
108
131
|
const pushService = new XYPushService(config);
|
|
109
132
|
// Extract title (first 57 chars or first line)
|
|
110
133
|
const title = text.split("\n")[0].slice(0, 57);
|
|
111
134
|
// Truncate push content to max length 1000
|
|
112
135
|
const pushText = text.length > 1000 ? text.slice(0, 1000) : text;
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
136
|
+
// 4. 遍历所有 pushId,依次发送推送通知
|
|
137
|
+
console.log(`[xyOutbound.sendText] 📤 Broadcasting to ${pushIdList.length} pushId(s)...`);
|
|
138
|
+
let successCount = 0;
|
|
139
|
+
let failureCount = 0;
|
|
140
|
+
for (const pushId of pushIdList) {
|
|
141
|
+
try {
|
|
142
|
+
// 传入 pushId 和 pushDataId,使用 kind="data" 格式
|
|
143
|
+
await pushService.sendPush(pushText, title, undefined, actualTo, pushDataId, pushId);
|
|
144
|
+
successCount++;
|
|
145
|
+
console.log(`[xyOutbound.sendText] ✅ Sent successfully to pushId: ${pushId.substring(0, 20)}...`);
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
failureCount++;
|
|
149
|
+
console.error(`[xyOutbound.sendText] ❌ Failed to send to pushId: ${pushId.substring(0, 20)}...`, error);
|
|
150
|
+
// 单个 pushId 发送失败不影响其他,继续处理下一个
|
|
151
|
+
}
|
|
152
|
+
}
|
|
116
153
|
// Return message info
|
|
117
154
|
return {
|
|
118
155
|
channel: "xiaoyi-channel",
|
|
119
|
-
messageId: Date.now().toString(),
|
|
156
|
+
messageId: pushDataId || Date.now().toString(),
|
|
120
157
|
chatId: actualTo,
|
|
121
158
|
};
|
|
122
159
|
},
|
|
123
160
|
sendMedia: async ({ cfg, to, text, mediaUrl, accountId, mediaLocalRoots }) => {
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
161
|
+
// Parse to: "sessionId::taskId"
|
|
162
|
+
const parts = to.split("::");
|
|
163
|
+
if (parts.length !== 2) {
|
|
164
|
+
throw new Error(`Invalid to format: "${to}". Expected "sessionId::taskId"`);
|
|
165
|
+
}
|
|
166
|
+
const [sessionId, taskId] = parts;
|
|
167
|
+
// Resolve configuration
|
|
168
|
+
const config = resolveXYConfig(cfg);
|
|
169
|
+
// Create upload service
|
|
170
|
+
const uploadService = new XYFileUploadService(config.fileUploadUrl, config.apiKey, config.uid);
|
|
171
|
+
// Validate mediaUrl
|
|
172
|
+
if (!mediaUrl) {
|
|
173
|
+
throw new Error("mediaUrl is required for sendMedia");
|
|
174
|
+
}
|
|
175
|
+
// Upload file
|
|
176
|
+
const fileId = await uploadService.uploadFile(mediaUrl);
|
|
177
|
+
// Check if fileId is empty
|
|
178
|
+
if (!fileId) {
|
|
179
|
+
console.log(`[xyOutbound.sendMedia] ⚠️ File upload failed: fileId is empty, aborting sendMedia`);
|
|
180
|
+
return {
|
|
181
|
+
channel: "xiaoyi-channel",
|
|
182
|
+
messageId: "",
|
|
183
|
+
chatId: to,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
console.log(`[xyOutbound.sendMedia] File uploaded:`, {
|
|
187
|
+
fileId,
|
|
188
|
+
sessionId,
|
|
189
|
+
taskId,
|
|
131
190
|
});
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
191
|
+
// Get filename and mime type from mediaUrl
|
|
192
|
+
// mediaUrl may be a local file path or URL
|
|
193
|
+
const fileName = mediaUrl.split("/").pop() || "unknown";
|
|
194
|
+
const mimeType = getMimeTypeFromFilename(fileName);
|
|
195
|
+
// Build agent_response message
|
|
196
|
+
const agentResponse = {
|
|
197
|
+
msgType: "agent_response",
|
|
198
|
+
agentId: config.agentId,
|
|
199
|
+
sessionId: sessionId,
|
|
200
|
+
taskId: taskId,
|
|
201
|
+
msgDetail: JSON.stringify({
|
|
202
|
+
jsonrpc: "2.0",
|
|
203
|
+
id: taskId,
|
|
204
|
+
result: {
|
|
205
|
+
kind: "artifact-update",
|
|
206
|
+
append: true,
|
|
207
|
+
lastChunk: false,
|
|
208
|
+
final: false,
|
|
209
|
+
artifact: {
|
|
210
|
+
artifactId: taskId,
|
|
211
|
+
parts: [
|
|
212
|
+
{
|
|
213
|
+
kind: "file",
|
|
214
|
+
file: {
|
|
215
|
+
name: fileName,
|
|
216
|
+
mimeType: mimeType,
|
|
217
|
+
fileId: fileId,
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
error: { code: 0 },
|
|
224
|
+
}),
|
|
225
|
+
};
|
|
226
|
+
// Get WebSocket manager and send message
|
|
227
|
+
const { getXYWebSocketManager } = await import("./client.js");
|
|
228
|
+
const wsManager = getXYWebSocketManager(config);
|
|
229
|
+
await wsManager.sendMessage(sessionId, agentResponse);
|
|
230
|
+
console.log(`[xyOutbound.sendMedia] WebSocket message sent successfully`);
|
|
231
|
+
// Return message info
|
|
136
232
|
return {
|
|
137
233
|
channel: "xiaoyi-channel",
|
|
138
|
-
messageId:
|
|
234
|
+
messageId: fileId,
|
|
139
235
|
chatId: to,
|
|
140
236
|
};
|
|
141
|
-
// // Parse to: "sessionId::taskId"
|
|
142
|
-
// const parts = to.split("::");
|
|
143
|
-
// if (parts.length !== 2) {
|
|
144
|
-
// throw new Error(`Invalid to format: "${to}". Expected "sessionId::taskId"`);
|
|
145
|
-
// }
|
|
146
|
-
// const [sessionId, taskId] = parts;
|
|
147
|
-
// // Resolve configuration
|
|
148
|
-
// const config = resolveXYConfig(cfg);
|
|
149
|
-
// // Create upload service
|
|
150
|
-
// const uploadService = new XYFileUploadService(
|
|
151
|
-
// config.fileUploadUrl,
|
|
152
|
-
// config.apiKey,
|
|
153
|
-
// config.uid
|
|
154
|
-
// );
|
|
155
|
-
// // Validate mediaUrl
|
|
156
|
-
// if (!mediaUrl) {
|
|
157
|
-
// throw new Error("mediaUrl is required for sendMedia");
|
|
158
|
-
// }
|
|
159
|
-
// // Upload file
|
|
160
|
-
// const fileId = await uploadService.uploadFile(mediaUrl);
|
|
161
|
-
// // Check if fileId is empty
|
|
162
|
-
// if (!fileId) {
|
|
163
|
-
// console.log(`[xyOutbound.sendMedia] ⚠️ File upload failed: fileId is empty, aborting sendMedia`);
|
|
164
|
-
// return {
|
|
165
|
-
// channel: "xiaoyi-channel",
|
|
166
|
-
// messageId: "",
|
|
167
|
-
// chatId: to,
|
|
168
|
-
// };
|
|
169
|
-
// }
|
|
170
|
-
// console.log(`[xyOutbound.sendMedia] File uploaded:`, {
|
|
171
|
-
// fileId,
|
|
172
|
-
// sessionId,
|
|
173
|
-
// taskId,
|
|
174
|
-
// });
|
|
175
|
-
// // Get filename and mime type from mediaUrl
|
|
176
|
-
// // mediaUrl may be a local file path or URL
|
|
177
|
-
// const fileName = mediaUrl.split("/").pop() || "unknown";
|
|
178
|
-
// const mimeType = getMimeTypeFromFilename(fileName);
|
|
179
|
-
// // Build agent_response message
|
|
180
|
-
// const agentResponse: OutboundWebSocketMessage = {
|
|
181
|
-
// msgType: "agent_response",
|
|
182
|
-
// agentId: config.agentId,
|
|
183
|
-
// sessionId: sessionId,
|
|
184
|
-
// taskId: taskId,
|
|
185
|
-
// msgDetail: JSON.stringify({
|
|
186
|
-
// jsonrpc: "2.0",
|
|
187
|
-
// id: taskId,
|
|
188
|
-
// result: {
|
|
189
|
-
// kind: "artifact-update",
|
|
190
|
-
// append: true,
|
|
191
|
-
// lastChunk: false,
|
|
192
|
-
// final: false,
|
|
193
|
-
// artifact: {
|
|
194
|
-
// artifactId: taskId,
|
|
195
|
-
// parts: [
|
|
196
|
-
// {
|
|
197
|
-
// kind: "file",
|
|
198
|
-
// file: {
|
|
199
|
-
// name: fileName,
|
|
200
|
-
// mimeType: mimeType,
|
|
201
|
-
// fileId: fileId,
|
|
202
|
-
// },
|
|
203
|
-
// },
|
|
204
|
-
// ],
|
|
205
|
-
// },
|
|
206
|
-
// },
|
|
207
|
-
// error: { code: 0 },
|
|
208
|
-
// }),
|
|
209
|
-
// };
|
|
210
|
-
// // Get WebSocket manager and send message
|
|
211
|
-
// const { getXYWebSocketManager } = await import("./client.js");
|
|
212
|
-
// const wsManager = getXYWebSocketManager(config);
|
|
213
|
-
// await wsManager.sendMessage(sessionId, agentResponse);
|
|
214
|
-
// console.log(`[xyOutbound.sendMedia] WebSocket message sent successfully`);
|
|
215
|
-
// // Return message info
|
|
216
|
-
// return {
|
|
217
|
-
// channel: "xiaoyi-channel",
|
|
218
|
-
// messageId: fileId,
|
|
219
|
-
// chatId: to,
|
|
220
|
-
// };
|
|
221
237
|
},
|
|
222
238
|
};
|
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
|
*/
|
package/dist/src/parser.js
CHANGED
|
@@ -72,6 +72,28 @@ export function extractPushId(parts) {
|
|
|
72
72
|
}
|
|
73
73
|
return null;
|
|
74
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
|
+
}
|
|
75
97
|
/**
|
|
76
98
|
* Validate A2A request structure.
|
|
77
99
|
*/
|
package/dist/src/push.d.ts
CHANGED
|
@@ -14,8 +14,15 @@ export declare class XYPushService {
|
|
|
14
14
|
private generateTraceId;
|
|
15
15
|
/**
|
|
16
16
|
* Send a push message to a user session.
|
|
17
|
+
*
|
|
18
|
+
* @param content - Push message content
|
|
19
|
+
* @param title - Push message title
|
|
20
|
+
* @param data - Optional additional data
|
|
21
|
+
* @param sessionId - Optional session ID
|
|
22
|
+
* @param pushDataId - Optional pushDataId for kind="data" format
|
|
23
|
+
* @param pushId - Push ID to use (required)
|
|
17
24
|
*/
|
|
18
|
-
sendPush(content: string, title: string, data?: Record<string, any>, sessionId?: string): Promise<void>;
|
|
25
|
+
sendPush(content: string, title: string, data?: Record<string, any>, sessionId?: string, pushDataId?: string, pushId?: string): Promise<void>;
|
|
19
26
|
/**
|
|
20
27
|
* Send a push message with file attachments.
|
|
21
28
|
*/
|
package/dist/src/push.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// Push message service for scheduled tasks
|
|
2
2
|
import fetch from "node-fetch";
|
|
3
3
|
import { randomUUID } from "crypto";
|
|
4
|
-
import { configManager } from "./utils/config-manager.js";
|
|
5
4
|
/**
|
|
6
5
|
* Service for sending push messages to users.
|
|
7
6
|
* Used for outbound messages and scheduled tasks.
|
|
@@ -21,29 +20,21 @@ export class XYPushService {
|
|
|
21
20
|
}
|
|
22
21
|
/**
|
|
23
22
|
* Send a push message to a user session.
|
|
23
|
+
*
|
|
24
|
+
* @param content - Push message content
|
|
25
|
+
* @param title - Push message title
|
|
26
|
+
* @param data - Optional additional data
|
|
27
|
+
* @param sessionId - Optional session ID
|
|
28
|
+
* @param pushDataId - Optional pushDataId for kind="data" format
|
|
29
|
+
* @param pushId - Push ID to use (required)
|
|
24
30
|
*/
|
|
25
|
-
async sendPush(content, title, data, sessionId) {
|
|
31
|
+
async sendPush(content, title, data, sessionId, pushDataId, pushId) {
|
|
26
32
|
const pushUrl = this.config.pushUrl || this.DEFAULT_PUSH_URL;
|
|
27
33
|
const traceId = this.generateTraceId();
|
|
28
|
-
//
|
|
29
|
-
const
|
|
30
|
-
const pushId = dynamicPushId || this.config.pushId;
|
|
34
|
+
// Use provided pushId or fall back to config pushId
|
|
35
|
+
const actualPushId = pushId || this.config.pushId;
|
|
31
36
|
console.log(`[PUSH] 📤 Preparing to send push message`);
|
|
32
|
-
console.log(`[PUSH] -
|
|
33
|
-
console.log(`[PUSH] - Content length: ${content.length} chars`);
|
|
34
|
-
console.log(`[PUSH] - Session ID: ${sessionId || 'none'}`);
|
|
35
|
-
console.log(`[PUSH] - Trace ID: ${traceId}`);
|
|
36
|
-
console.log(`[PUSH] - Push URL: ${pushUrl}`);
|
|
37
|
-
if (dynamicPushId) {
|
|
38
|
-
console.log(`[PUSH] - Using dynamic pushId (from session): ${pushId.substring(0, 20)}...`);
|
|
39
|
-
console.log(`[PUSH] - Full dynamic pushId: ${pushId}`);
|
|
40
|
-
}
|
|
41
|
-
else {
|
|
42
|
-
console.log(`[PUSH] - Using config pushId (fallback): ${pushId.substring(0, 20)}...`);
|
|
43
|
-
console.log(`[PUSH] - Full config pushId: ${pushId}`);
|
|
44
|
-
}
|
|
45
|
-
console.log(`[PUSH] - API ID: ${this.config.apiId}`);
|
|
46
|
-
console.log(`[PUSH] - UID: ${this.config.uid}`);
|
|
37
|
+
console.log(`[PUSH] - Using pushId: ${actualPushId.substring(0, 20)}...`);
|
|
47
38
|
try {
|
|
48
39
|
const requestBody = {
|
|
49
40
|
jsonrpc: "2.0",
|
|
@@ -51,23 +42,31 @@ export class XYPushService {
|
|
|
51
42
|
result: {
|
|
52
43
|
id: randomUUID(),
|
|
53
44
|
apiId: this.config.apiId,
|
|
54
|
-
pushId:
|
|
45
|
+
pushId: actualPushId,
|
|
55
46
|
pushText: title,
|
|
56
47
|
kind: "task",
|
|
57
48
|
artifacts: [
|
|
58
49
|
{
|
|
59
50
|
artifactId: randomUUID(),
|
|
60
|
-
parts:
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
51
|
+
parts: pushDataId
|
|
52
|
+
? [
|
|
53
|
+
{
|
|
54
|
+
kind: "data",
|
|
55
|
+
data: {
|
|
56
|
+
pushDataId: pushDataId,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
]
|
|
60
|
+
: [
|
|
61
|
+
{
|
|
62
|
+
kind: "text",
|
|
63
|
+
text: content,
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
66
|
},
|
|
67
67
|
],
|
|
68
68
|
},
|
|
69
69
|
};
|
|
70
|
-
console.log(`[PUSH] Full request body:`, JSON.stringify(requestBody, null, 2));
|
|
71
70
|
const response = await fetch(pushUrl, {
|
|
72
71
|
method: "POST",
|
|
73
72
|
headers: {
|
|
@@ -83,21 +82,16 @@ export class XYPushService {
|
|
|
83
82
|
// Log response status and headers
|
|
84
83
|
console.log(`[PUSH] 📥 Response received`);
|
|
85
84
|
console.log(`[PUSH] - HTTP Status: ${response.status} ${response.statusText}`);
|
|
86
|
-
console.log(`[PUSH] - Content-Type: ${response.headers.get('content-type')}`);
|
|
87
|
-
console.log(`[PUSH] - Content-Length: ${response.headers.get('content-length')}`);
|
|
88
85
|
if (!response.ok) {
|
|
89
86
|
const errorText = await response.text();
|
|
90
87
|
console.log(`[PUSH] ❌ Push request failed`);
|
|
91
88
|
console.log(`[PUSH] - HTTP Status: ${response.status}`);
|
|
92
|
-
console.log(`[PUSH] - Response body: ${errorText}`);
|
|
93
89
|
throw new Error(`Push failed: HTTP ${response.status} - ${errorText}`);
|
|
94
90
|
}
|
|
95
91
|
// Try to parse JSON response with detailed error handling
|
|
96
92
|
let result;
|
|
97
93
|
try {
|
|
98
94
|
const responseText = await response.text();
|
|
99
|
-
console.log(`[PUSH] 📄 Response body length: ${responseText.length} chars`);
|
|
100
|
-
console.log(`[PUSH] 📄 Response body preview: ${responseText.substring(0, 200)}`);
|
|
101
95
|
if (!responseText || responseText.trim() === '') {
|
|
102
96
|
console.log(`[PUSH] ⚠️ Received empty response body`);
|
|
103
97
|
result = {};
|
|
@@ -112,20 +106,13 @@ export class XYPushService {
|
|
|
112
106
|
throw new Error(`Invalid JSON response from push service: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
|
|
113
107
|
}
|
|
114
108
|
console.log(`[PUSH] ✅ Push message sent successfully`);
|
|
115
|
-
console.log(`[PUSH] - Title: "${title}"`);
|
|
116
109
|
console.log(`[PUSH] - Trace ID: ${traceId}`);
|
|
117
|
-
console.log(`[PUSH] - Used pushId: ${pushId.substring(0, 20)}...`);
|
|
118
|
-
console.log(`[PUSH] - Response:`, result);
|
|
119
110
|
}
|
|
120
111
|
catch (error) {
|
|
121
112
|
console.log(`[PUSH] ❌ Failed to send push message`);
|
|
122
|
-
console.log(`[PUSH] - Trace ID: ${traceId}`);
|
|
123
|
-
console.log(`[PUSH] - Target URL: ${pushUrl}`);
|
|
124
|
-
console.log(`[PUSH] - Push ID: ${pushId.substring(0, 20)}...`);
|
|
125
113
|
if (error instanceof Error) {
|
|
126
114
|
console.log(`[PUSH] - Error name: ${error.name}`);
|
|
127
115
|
console.log(`[PUSH] - Error message: ${error.message}`);
|
|
128
|
-
console.log(`[PUSH] - Error stack:`, error.stack);
|
|
129
116
|
}
|
|
130
117
|
else {
|
|
131
118
|
console.log(`[PUSH] - Error:`, error);
|
|
@@ -8,6 +8,10 @@ export interface CreateXYReplyDispatcherParams {
|
|
|
8
8
|
accountId: string;
|
|
9
9
|
isSteerFollower?: boolean;
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* 清理 /tmp/xy_channel 目录中超过 24 小时的旧文件
|
|
13
|
+
*/
|
|
14
|
+
export declare function cleanupStaleTempFiles(tempDir?: string): Promise<void>;
|
|
11
15
|
/**
|
|
12
16
|
* Create a reply dispatcher for XY channel messages.
|
|
13
17
|
* Follows feishu pattern with status updates and streaming support.
|
|
@@ -1,8 +1,43 @@
|
|
|
1
|
-
import { createReplyPrefixContext } from "openclaw/plugin-sdk";
|
|
2
1
|
import { getXYRuntime } from "./runtime.js";
|
|
3
2
|
import { sendA2AResponse, sendStatusUpdate, sendReasoningTextUpdate } from "./formatter.js";
|
|
4
3
|
import { resolveXYConfig } from "./config.js";
|
|
5
4
|
import { getCurrentTaskId, getCurrentMessageId } from "./task-manager.js";
|
|
5
|
+
import fs from "fs/promises";
|
|
6
|
+
import path from "path";
|
|
7
|
+
const TEMP_FILE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
8
|
+
/**
|
|
9
|
+
* 清理 /tmp/xy_channel 目录中超过 24 小时的旧文件
|
|
10
|
+
*/
|
|
11
|
+
export async function cleanupStaleTempFiles(tempDir = "/tmp/xy_channel") {
|
|
12
|
+
try {
|
|
13
|
+
const stats = await fs.stat(tempDir).catch(() => null);
|
|
14
|
+
if (!stats?.isDirectory()) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const files = await fs.readdir(tempDir);
|
|
18
|
+
const now = Date.now();
|
|
19
|
+
let cleanedCount = 0;
|
|
20
|
+
for (const file of files) {
|
|
21
|
+
const filePath = path.join(tempDir, file);
|
|
22
|
+
try {
|
|
23
|
+
const fileStat = await fs.stat(filePath);
|
|
24
|
+
if (now - fileStat.mtimeMs > TEMP_FILE_TTL_MS) {
|
|
25
|
+
await fs.unlink(filePath);
|
|
26
|
+
cleanedCount++;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
// 忽略单个文件处理失败
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (cleanedCount > 0) {
|
|
34
|
+
console.log(`[CLEANUP] 🧹 Cleaned ${cleanedCount} stale files (>${TEMP_FILE_TTL_MS / 1000 / 3600}h) from ${tempDir}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
console.error(`[CLEANUP] ❌ Failed to cleanup temp dir:`, err);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
6
41
|
/**
|
|
7
42
|
* Create a reply dispatcher for XY channel messages.
|
|
8
43
|
* Follows feishu pattern with status updates and streaming support.
|
|
@@ -13,9 +48,7 @@ export function createXYReplyDispatcher(params) {
|
|
|
13
48
|
const log = runtime?.log ?? console.log;
|
|
14
49
|
const error = runtime?.error ?? console.error;
|
|
15
50
|
log(`[DISPATCHER-CREATE] ******* Creating dispatcher *******`);
|
|
16
|
-
log(`[DISPATCHER-CREATE] - sessionId: ${sessionId}`);
|
|
17
51
|
log(`[DISPATCHER-CREATE] - taskId: ${taskId}`);
|
|
18
|
-
log(`[DISPATCHER-CREATE] - messageId: ${messageId}`);
|
|
19
52
|
log(`[DISPATCHER-CREATE] - isSteerFollower: ${isSteerFollower ?? false}`);
|
|
20
53
|
// 初始taskId和messageId(作为fallback)
|
|
21
54
|
const initialTaskId = taskId;
|
|
@@ -32,7 +65,12 @@ export function createXYReplyDispatcher(params) {
|
|
|
32
65
|
};
|
|
33
66
|
const core = getXYRuntime();
|
|
34
67
|
const config = resolveXYConfig(cfg);
|
|
35
|
-
|
|
68
|
+
// Simplified prefix context for single-account Xiaoyi channel
|
|
69
|
+
const prefixContext = {
|
|
70
|
+
responsePrefix: undefined,
|
|
71
|
+
responsePrefixContextProvider: undefined,
|
|
72
|
+
onModelSelected: undefined,
|
|
73
|
+
};
|
|
36
74
|
let statusUpdateInterval = null;
|
|
37
75
|
let hasSentResponse = false;
|
|
38
76
|
let finalSent = false;
|
|
@@ -54,7 +92,7 @@ export function createXYReplyDispatcher(params) {
|
|
|
54
92
|
sessionId,
|
|
55
93
|
taskId: currentTaskId, // 🔑 动态taskId
|
|
56
94
|
messageId: currentMessageId, // 🔑 动态messageId
|
|
57
|
-
text: "
|
|
95
|
+
text: "任务正在处理中,请稍候~",
|
|
58
96
|
state: "working",
|
|
59
97
|
}).catch((err) => {
|
|
60
98
|
error(`Failed to send status update:`, err);
|
|
@@ -289,7 +327,6 @@ export function createXYReplyDispatcher(params) {
|
|
|
289
327
|
const currentTaskId = getActiveTaskId();
|
|
290
328
|
const currentMessageId = getActiveMessageId();
|
|
291
329
|
const text = payload.text ?? "";
|
|
292
|
-
log(`[PARTIAL REPLY] Partial reply chunk received, taskId: ${currentTaskId}`);
|
|
293
330
|
try {
|
|
294
331
|
if (text.length > 0) {
|
|
295
332
|
await sendReasoningTextUpdate({
|
|
@@ -300,7 +337,6 @@ export function createXYReplyDispatcher(params) {
|
|
|
300
337
|
text,
|
|
301
338
|
append: false,
|
|
302
339
|
});
|
|
303
|
-
log(`[PARTIAL REPLY] ✅ Sent partial reply as reasoningText update`);
|
|
304
340
|
}
|
|
305
341
|
}
|
|
306
342
|
catch (err) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
|
|
2
|
+
/**
|
|
3
|
+
* 在 handleXYMessage 入口处调用,缓存 cfg/runtime 供 steer 注入使用。
|
|
4
|
+
*/
|
|
5
|
+
export declare function setCachedContext(cfg: ClawdbotConfig, runtime: RuntimeEnv, accountId: string): void;
|
|
6
|
+
/**
|
|
7
|
+
* 尝试向当前活跃会话注入 steer 消息。
|
|
8
|
+
* 两层保险:
|
|
9
|
+
* 1. getSessionContext(sessionKey) 确认是当前 XY 活跃 session
|
|
10
|
+
* 2. hasActiveTask(sessionId) 确认任务仍在运行
|
|
11
|
+
*
|
|
12
|
+
* @param sessionKey 来自 after_tool_call ctx.sessionKey(per-peer 下精确对应一个 XY session)
|
|
13
|
+
* @param message 要注入的用户消息文本
|
|
14
|
+
* @returns true=已注入,false=跳过
|
|
15
|
+
*/
|
|
16
|
+
export declare function tryInjectSteer(sessionKey: string | undefined, message: string): Promise<boolean>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Steer message injector for CSPL hook integration
|
|
2
|
+
import { getSessionContext } from "./tools/session-manager.js";
|
|
3
|
+
import { hasActiveTask, getCurrentTaskId } from "./task-manager.js";
|
|
4
|
+
import { handleXYMessage } from "./bot.js";
|
|
5
|
+
import { logger } from "./utils/logger.js";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
let cachedCfg = null;
|
|
8
|
+
let cachedRuntime = null;
|
|
9
|
+
let cachedAccountId = "default";
|
|
10
|
+
/**
|
|
11
|
+
* 在 handleXYMessage 入口处调用,缓存 cfg/runtime 供 steer 注入使用。
|
|
12
|
+
*/
|
|
13
|
+
export function setCachedContext(cfg, runtime, accountId) {
|
|
14
|
+
cachedCfg = cfg;
|
|
15
|
+
cachedRuntime = runtime;
|
|
16
|
+
cachedAccountId = accountId;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 尝试向当前活跃会话注入 steer 消息。
|
|
20
|
+
* 两层保险:
|
|
21
|
+
* 1. getSessionContext(sessionKey) 确认是当前 XY 活跃 session
|
|
22
|
+
* 2. hasActiveTask(sessionId) 确认任务仍在运行
|
|
23
|
+
*
|
|
24
|
+
* @param sessionKey 来自 after_tool_call ctx.sessionKey(per-peer 下精确对应一个 XY session)
|
|
25
|
+
* @param message 要注入的用户消息文本
|
|
26
|
+
* @returns true=已注入,false=跳过
|
|
27
|
+
*/
|
|
28
|
+
export async function tryInjectSteer(sessionKey, message) {
|
|
29
|
+
if (!sessionKey) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
const sessionCtx = getSessionContext(sessionKey);
|
|
33
|
+
if (!sessionCtx) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
const { sessionId } = sessionCtx;
|
|
37
|
+
const activeTaskId = getCurrentTaskId(sessionId);
|
|
38
|
+
if (!hasActiveTask(sessionId)) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
if (!cachedCfg || !cachedRuntime) {
|
|
42
|
+
logger.error("[STEER] No cached cfg/runtime available, cannot inject");
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
// 3. 构造合成 A2A 消息(伪装成用户在当前会话中发送的新消息)
|
|
46
|
+
const syntheticMessage = {
|
|
47
|
+
jsonrpc: "2.0",
|
|
48
|
+
method: "tasks/send",
|
|
49
|
+
id: `steer-msg-${randomUUID()}`,
|
|
50
|
+
params: {
|
|
51
|
+
sessionId,
|
|
52
|
+
id: activeTaskId ?? `steer-task-${randomUUID()}`,
|
|
53
|
+
agentLoginSessionId: "",
|
|
54
|
+
message: {
|
|
55
|
+
role: "user",
|
|
56
|
+
parts: [{ kind: "text", text: message }],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
console.log(`[STEER] Injecting steer for sessionId=${sessionId}, taskId=${syntheticMessage.params.id}`);
|
|
61
|
+
try {
|
|
62
|
+
await handleXYMessage({
|
|
63
|
+
cfg: cachedCfg,
|
|
64
|
+
runtime: cachedRuntime,
|
|
65
|
+
message: syntheticMessage,
|
|
66
|
+
accountId: cachedAccountId,
|
|
67
|
+
});
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
logger.error(`[STEER] ❌ Failed to inject steer: ${err}`);
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|