@ynhcj/xiaoyi-channel 0.0.22-beta → 0.0.22-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.js +42 -0
- package/dist/src/bot.js +123 -41
- package/dist/src/channel.js +17 -5
- package/dist/src/client.js +11 -24
- package/dist/src/config.js +2 -2
- package/dist/src/cspl/call-api.d.ts +3 -0
- package/dist/src/cspl/call-api.js +79 -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 +92 -0
- package/dist/src/formatter.d.ts +14 -0
- package/dist/src/formatter.js +46 -2
- package/dist/src/monitor.js +47 -5
- package/dist/src/outbound.js +52 -6
- package/dist/src/parser.d.ts +7 -0
- package/dist/src/parser.js +22 -0
- package/dist/src/push.d.ts +8 -1
- package/dist/src/push.js +30 -22
- package/dist/src/reply-dispatcher.d.ts +1 -0
- package/dist/src/reply-dispatcher.js +116 -101
- package/dist/src/steer-injector.d.ts +16 -0
- package/dist/src/steer-injector.js +74 -0
- package/dist/src/task-manager.d.ts +55 -0
- package/dist/src/task-manager.js +136 -0
- package/dist/src/tools/calendar-tool.js +5 -3
- package/dist/src/tools/call-phone-tool.d.ts +5 -0
- package/dist/src/tools/call-phone-tool.js +183 -0
- package/dist/src/tools/create-alarm-tool.d.ts +7 -0
- package/dist/src/tools/create-alarm-tool.js +420 -0
- package/dist/src/tools/delete-alarm-tool.d.ts +11 -0
- package/dist/src/tools/delete-alarm-tool.js +216 -0
- package/dist/src/tools/image-reading-tool.d.ts +5 -0
- package/dist/src/tools/image-reading-tool.js +375 -0
- package/dist/src/tools/location-tool.js +8 -11
- package/dist/src/tools/modify-alarm-tool.d.ts +9 -0
- package/dist/src/tools/modify-alarm-tool.js +439 -0
- package/dist/src/tools/modify-note-tool.js +4 -9
- package/dist/src/tools/note-tool.js +32 -21
- package/dist/src/tools/search-alarm-tool.d.ts +8 -0
- package/dist/src/tools/search-alarm-tool.js +343 -0
- package/dist/src/tools/search-calendar-tool.js +9 -46
- package/dist/src/tools/search-contact-tool.js +4 -27
- package/dist/src/tools/search-file-tool.d.ts +5 -0
- package/dist/src/tools/search-file-tool.js +161 -0
- package/dist/src/tools/search-message-tool.d.ts +5 -0
- package/dist/src/tools/search-message-tool.js +149 -0
- package/dist/src/tools/search-note-tool.js +29 -22
- package/dist/src/tools/search-photo-gallery-tool.js +51 -34
- 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 +176 -0
- package/dist/src/tools/session-manager.d.ts +15 -0
- package/dist/src/tools/session-manager.js +99 -18
- package/dist/src/tools/upload-file-tool.d.ts +13 -0
- package/dist/src/tools/upload-file-tool.js +265 -0
- package/dist/src/tools/upload-photo-tool.js +14 -4
- 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.js +8 -8
- 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 +218 -270
- package/package.json +1 -1
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// CSPL Hook 常量与类型定义
|
|
2
|
+
export const MIN_TEXT_LENGTH = 0;
|
|
3
|
+
export const MAX_TEXT_LENGTH = 4096;
|
|
4
|
+
export const MAX_TOTAL_LENGTH = 40960;
|
|
5
|
+
export const regex = /[^\u4e00-\u9fa5a-zA-Z0-9\s\.,!?;:,。!?;:""\'\'()()\[\]【】]/;
|
|
6
|
+
export const DEFAULT_HTTP_PORT = 80;
|
|
7
|
+
export const HTTP_STATUS_BAD_REQUEST = 400;
|
|
8
|
+
export const ENV_FILE_PATH = "/home/sandbox/.openclaw/.xiaoyienv";
|
|
9
|
+
export const API_URL_SUFFIX = "/celia-claw/v1/rest-api/skill/execute";
|
|
10
|
+
export const REQUIRED_ENV_VARS = ["PERSONAL-API-KEY", "PERSONAL-UID", "SERVICE_URL"];
|
|
11
|
+
// 工具白名单
|
|
12
|
+
export const ALLOWED_TOOLS = ["web_fetch", "read", "pdf"];
|
|
13
|
+
// 静态配置(非敏感,敏感值从 ENV 文件读取)
|
|
14
|
+
export const CSPL_STATIC_CONFIG = {
|
|
15
|
+
api: { timeout: 5000 },
|
|
16
|
+
skillId: "skill-scope",
|
|
17
|
+
requestFrom: "openclaw",
|
|
18
|
+
textSource: "question",
|
|
19
|
+
action: "TOOL_OUTPUT_SCAN",
|
|
20
|
+
};
|
|
21
|
+
// Steer 注入常量
|
|
22
|
+
export const STEER_ABORT_MESSAGE = "当前行为存在安全隐患,终止所有操作,并且在最终回复中说明,识别到当前流程中存在潜在安全风险,已帮您中止当前流程,请清空上下文后重试";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function filterText(text: string): string;
|
|
2
|
+
export declare function validateAndTruncateText(text: string, maxLength: number): {
|
|
3
|
+
text: string;
|
|
4
|
+
truncated: boolean;
|
|
5
|
+
};
|
|
6
|
+
export declare function extractResultText(event: any, toolName: string): string;
|
|
7
|
+
export declare function processText(resultText: string): string;
|
|
8
|
+
export declare function parseSecurityResult(response: any): {
|
|
9
|
+
status: "ACCEPT" | "REJECT";
|
|
10
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// CSPL Hook 工具函数
|
|
2
|
+
import { MAX_TEXT_LENGTH, regex } from "./constants.js";
|
|
3
|
+
export function filterText(text) {
|
|
4
|
+
if (!text)
|
|
5
|
+
return "";
|
|
6
|
+
return text.replace(new RegExp(regex.source, "g"), "");
|
|
7
|
+
}
|
|
8
|
+
export function validateAndTruncateText(text, maxLength) {
|
|
9
|
+
if (text.length > maxLength) {
|
|
10
|
+
const halfMaxLength = Math.floor(maxLength / 2);
|
|
11
|
+
const startText = text.substring(0, halfMaxLength);
|
|
12
|
+
const endText = text.substring(text.length - halfMaxLength);
|
|
13
|
+
return { text: startText + endText, truncated: true };
|
|
14
|
+
}
|
|
15
|
+
return { text, truncated: false };
|
|
16
|
+
}
|
|
17
|
+
export function extractResultText(event, toolName) {
|
|
18
|
+
const resultTexts = [];
|
|
19
|
+
if (toolName === "web_fetch") {
|
|
20
|
+
if (event.result?.details?.text) {
|
|
21
|
+
resultTexts.push(event.result.details.text);
|
|
22
|
+
}
|
|
23
|
+
return resultTexts.length > 0 ? resultTexts.join("; ") : "";
|
|
24
|
+
}
|
|
25
|
+
if (event.result?.content && Array.isArray(event.result.content)) {
|
|
26
|
+
for (const item of event.result.content) {
|
|
27
|
+
if (item?.text) {
|
|
28
|
+
resultTexts.push(item.text);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return resultTexts.length > 0 ? resultTexts.join("; ") : "";
|
|
33
|
+
}
|
|
34
|
+
export function processText(resultText) {
|
|
35
|
+
const questionText = filterText(resultText);
|
|
36
|
+
const { text: finalText } = validateAndTruncateText(questionText, MAX_TEXT_LENGTH);
|
|
37
|
+
return finalText;
|
|
38
|
+
}
|
|
39
|
+
export function parseSecurityResult(response) {
|
|
40
|
+
if (response === null || response === undefined) {
|
|
41
|
+
throw new Error("Response is null or undefined");
|
|
42
|
+
}
|
|
43
|
+
if (!response.data || typeof response.data !== "object") {
|
|
44
|
+
throw new Error("Response.data is missing or not an object");
|
|
45
|
+
}
|
|
46
|
+
const securityResult = response.data.securityResult;
|
|
47
|
+
if (typeof securityResult !== "string") {
|
|
48
|
+
throw new Error("Response.data.securityResult is missing or not a string");
|
|
49
|
+
}
|
|
50
|
+
if (securityResult !== securityResult.trim()) {
|
|
51
|
+
throw new Error("Response.data.securityResult contains leading or trailing spaces");
|
|
52
|
+
}
|
|
53
|
+
if (securityResult !== "ACCEPT" && securityResult !== "REJECT") {
|
|
54
|
+
throw new Error(`Response.data.securityResult must be "ACCEPT" or "REJECT". Actual: "${securityResult}"`);
|
|
55
|
+
}
|
|
56
|
+
return { status: securityResult };
|
|
57
|
+
}
|
|
@@ -12,6 +12,11 @@ export declare class XYFileUploadService {
|
|
|
12
12
|
* Returns the objectId (as fileId) for use in A2A messages.
|
|
13
13
|
*/
|
|
14
14
|
uploadFile(filePath: string, objectType?: string): Promise<string>;
|
|
15
|
+
/**
|
|
16
|
+
* Upload a file and return its publicly accessible URL.
|
|
17
|
+
* Uses completeAndQuery endpoint to get the file URL directly.
|
|
18
|
+
*/
|
|
19
|
+
uploadFileAndGetUrl(filePath: string, objectType?: string): Promise<string>;
|
|
15
20
|
/**
|
|
16
21
|
* Upload multiple files and return their file IDs.
|
|
17
22
|
*/
|
package/dist/src/file-upload.js
CHANGED
|
@@ -105,6 +105,98 @@ export class XYFileUploadService {
|
|
|
105
105
|
return "";
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Upload a file and return its publicly accessible URL.
|
|
110
|
+
* Uses completeAndQuery endpoint to get the file URL directly.
|
|
111
|
+
*/
|
|
112
|
+
async uploadFileAndGetUrl(filePath, objectType = "TEMPORARY_MATERIAL_DOC") {
|
|
113
|
+
console.log(`[XY File Upload] Starting file upload with URL retrieval: ${filePath}`);
|
|
114
|
+
try {
|
|
115
|
+
// Read file
|
|
116
|
+
const fileBuffer = await fs.readFile(filePath);
|
|
117
|
+
const fileName = path.basename(filePath);
|
|
118
|
+
const fileSha256 = calculateSHA256(fileBuffer);
|
|
119
|
+
const fileSize = fileBuffer.length;
|
|
120
|
+
// Phase 1: Prepare
|
|
121
|
+
console.log(`[XY File Upload] Phase 1: Prepare upload for ${fileName}`);
|
|
122
|
+
const prepareResp = await fetch(`${this.baseUrl}/osms/v1/file/manager/prepare`, {
|
|
123
|
+
method: "POST",
|
|
124
|
+
headers: {
|
|
125
|
+
"Content-Type": "application/json",
|
|
126
|
+
"x-uid": this.uid,
|
|
127
|
+
"x-api-key": this.apiKey,
|
|
128
|
+
"x-request-from": "openclaw",
|
|
129
|
+
},
|
|
130
|
+
body: JSON.stringify({
|
|
131
|
+
objectType,
|
|
132
|
+
fileName,
|
|
133
|
+
fileSha256,
|
|
134
|
+
fileSize,
|
|
135
|
+
fileOwnerInfo: {
|
|
136
|
+
uid: this.uid,
|
|
137
|
+
teamId: this.uid,
|
|
138
|
+
},
|
|
139
|
+
useEdge: false,
|
|
140
|
+
}),
|
|
141
|
+
});
|
|
142
|
+
if (!prepareResp.ok) {
|
|
143
|
+
throw new Error(`Prepare failed: HTTP ${prepareResp.status}`);
|
|
144
|
+
}
|
|
145
|
+
const prepareData = await prepareResp.json();
|
|
146
|
+
console.log(`[XY File Upload] Prepare response:`, JSON.stringify(prepareData, null, 2));
|
|
147
|
+
if (prepareData.code !== "0") {
|
|
148
|
+
throw new Error(`Prepare failed: ${prepareData.desc}`);
|
|
149
|
+
}
|
|
150
|
+
const { objectId, draftId, uploadInfos } = prepareData;
|
|
151
|
+
console.log(`[XY File Upload] Prepare complete: objectId=${objectId}, draftId=${draftId}`);
|
|
152
|
+
// Phase 2: Upload
|
|
153
|
+
console.log(`[XY File Upload] Phase 2: Upload file data`);
|
|
154
|
+
const uploadInfo = uploadInfos[0]; // Single-part upload
|
|
155
|
+
const uploadResp = await fetch(uploadInfo.url, {
|
|
156
|
+
method: uploadInfo.method,
|
|
157
|
+
headers: uploadInfo.headers,
|
|
158
|
+
body: fileBuffer,
|
|
159
|
+
});
|
|
160
|
+
console.log(`[XY File Upload] Upload response status: ${uploadResp.status}`);
|
|
161
|
+
if (!uploadResp.ok) {
|
|
162
|
+
const uploadErrorText = await uploadResp.text();
|
|
163
|
+
console.log(`[XY File Upload] Upload error response:`, uploadErrorText);
|
|
164
|
+
throw new Error(`Upload failed: HTTP ${uploadResp.status}`);
|
|
165
|
+
}
|
|
166
|
+
console.log(`[XY File Upload] Upload complete`);
|
|
167
|
+
// Phase 3: CompleteAndQuery - get file URL
|
|
168
|
+
console.log(`[XY File Upload] Phase 3: CompleteAndQuery to get file URL`);
|
|
169
|
+
const completeResp = await fetch(`${this.baseUrl}/osms/v1/file/manager/completeAndQuery`, {
|
|
170
|
+
method: "POST",
|
|
171
|
+
headers: {
|
|
172
|
+
"Content-Type": "application/json",
|
|
173
|
+
"x-uid": this.uid,
|
|
174
|
+
"x-api-key": this.apiKey,
|
|
175
|
+
"x-request-from": "openclaw",
|
|
176
|
+
},
|
|
177
|
+
body: JSON.stringify({
|
|
178
|
+
objectId,
|
|
179
|
+
draftId,
|
|
180
|
+
}),
|
|
181
|
+
});
|
|
182
|
+
if (!completeResp.ok) {
|
|
183
|
+
throw new Error(`CompleteAndQuery failed: HTTP ${completeResp.status}`);
|
|
184
|
+
}
|
|
185
|
+
const completeData = await completeResp.json();
|
|
186
|
+
console.log(`[XY File Upload] CompleteAndQuery response:`, JSON.stringify(completeData, null, 2));
|
|
187
|
+
// Extract file URL from response
|
|
188
|
+
const fileUrl = completeData?.fileDetailInfo?.url || "";
|
|
189
|
+
if (!fileUrl) {
|
|
190
|
+
throw new Error("No file URL returned from completeAndQuery");
|
|
191
|
+
}
|
|
192
|
+
console.log(`[XY File Upload] File upload successful: ${fileName} → URL=${fileUrl}`);
|
|
193
|
+
return fileUrl;
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
console.error(`[XY File Upload] File upload with URL retrieval failed for ${filePath}:`, error);
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
108
200
|
/**
|
|
109
201
|
* Upload multiple files and return their file IDs.
|
|
110
202
|
*/
|
package/dist/src/formatter.d.ts
CHANGED
|
@@ -92,3 +92,17 @@ export interface SendTasksCancelResponseParams {
|
|
|
92
92
|
* Send a tasks/cancel response.
|
|
93
93
|
*/
|
|
94
94
|
export declare function sendTasksCancelResponse(params: SendTasksCancelResponseParams): Promise<void>;
|
|
95
|
+
/**
|
|
96
|
+
* Parameters for sending a Trigger response.
|
|
97
|
+
*/
|
|
98
|
+
export interface SendTriggerResponseParams {
|
|
99
|
+
config: XYChannelConfig;
|
|
100
|
+
sessionId: string;
|
|
101
|
+
taskId: string;
|
|
102
|
+
messageId: string;
|
|
103
|
+
content: string;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Send a Trigger response with pushData content.
|
|
107
|
+
*/
|
|
108
|
+
export declare function sendTriggerResponse(params: SendTriggerResponseParams): Promise<void>;
|
package/dist/src/formatter.js
CHANGED
|
@@ -106,9 +106,7 @@ export async function sendReasoningTextUpdate(params) {
|
|
|
106
106
|
taskId,
|
|
107
107
|
msgDetail: JSON.stringify(jsonRpcResponse),
|
|
108
108
|
};
|
|
109
|
-
log(`[REASONING_TEXT] 📤 Sending reasoningText update: sessionId=${sessionId}, taskId=${taskId}, text.length=${text.length}`);
|
|
110
109
|
await wsManager.sendMessage(sessionId, outboundMessage);
|
|
111
|
-
log(`[REASONING_TEXT] ✅ Sent successfully`);
|
|
112
110
|
}
|
|
113
111
|
/**
|
|
114
112
|
* Send an A2A task status update.
|
|
@@ -293,3 +291,49 @@ export async function sendTasksCancelResponse(params) {
|
|
|
293
291
|
await wsManager.sendMessage(sessionId, outboundMessage);
|
|
294
292
|
log(`Sent tasks/cancel response: sessionId=${sessionId}, taskId=${taskId}`);
|
|
295
293
|
}
|
|
294
|
+
/**
|
|
295
|
+
* Send a Trigger response with pushData content.
|
|
296
|
+
*/
|
|
297
|
+
export async function sendTriggerResponse(params) {
|
|
298
|
+
const { config, sessionId, taskId, messageId, content } = params;
|
|
299
|
+
const runtime = getXYRuntime();
|
|
300
|
+
const log = runtime?.log ?? console.log;
|
|
301
|
+
const error = runtime?.error ?? console.error;
|
|
302
|
+
// Build JSON-RPC response for Trigger
|
|
303
|
+
const jsonRpcResponse = {
|
|
304
|
+
jsonrpc: "2.0",
|
|
305
|
+
id: messageId,
|
|
306
|
+
result: {
|
|
307
|
+
taskId: taskId,
|
|
308
|
+
kind: "artifact-update",
|
|
309
|
+
append: false,
|
|
310
|
+
lastChunk: true,
|
|
311
|
+
final: true,
|
|
312
|
+
artifact: {
|
|
313
|
+
artifactId: uuidv4(),
|
|
314
|
+
parts: [
|
|
315
|
+
{
|
|
316
|
+
kind: "text",
|
|
317
|
+
text: content,
|
|
318
|
+
},
|
|
319
|
+
],
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
error: {
|
|
323
|
+
code: 0,
|
|
324
|
+
message: "",
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
// Send via WebSocket
|
|
328
|
+
const wsManager = getXYWebSocketManager(config);
|
|
329
|
+
const outboundMessage = {
|
|
330
|
+
msgType: "agent_response",
|
|
331
|
+
agentId: config.agentId,
|
|
332
|
+
sessionId,
|
|
333
|
+
taskId,
|
|
334
|
+
msgDetail: JSON.stringify(jsonRpcResponse),
|
|
335
|
+
};
|
|
336
|
+
log(`[TRIGGER_RESPONSE] Sending Trigger response: sessionId=${sessionId}, taskId=${taskId}`);
|
|
337
|
+
await wsManager.sendMessage(sessionId, outboundMessage);
|
|
338
|
+
log(`[TRIGGER_RESPONSE] Trigger response sent successfully`);
|
|
339
|
+
}
|
package/dist/src/monitor.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { resolveXYConfig } from "./config.js";
|
|
2
2
|
import { getXYWebSocketManager, diagnoseAllManagers, cleanupOrphanConnections, removeXYWebSocketManager } from "./client.js";
|
|
3
3
|
import { handleXYMessage } from "./bot.js";
|
|
4
|
+
import { parseA2AMessage } from "./parser.js";
|
|
5
|
+
import { hasActiveTask } from "./task-manager.js";
|
|
6
|
+
import { handleTriggerEvent } from "./trigger-handler.js";
|
|
4
7
|
/**
|
|
5
8
|
* Per-session serial queue that ensures messages from the same session are processed
|
|
6
9
|
* in arrival order while allowing different sessions to run concurrently.
|
|
@@ -94,11 +97,39 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
94
97
|
log(`[MONITOR-HANDLER] 🧹 Cleaned up messageKey=${messageKey}, remaining active: ${activeMessages.size}`);
|
|
95
98
|
}
|
|
96
99
|
};
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
// 🔑 核心改造:检测steer模式
|
|
101
|
+
// 需要提前解析消息以获取sessionId
|
|
102
|
+
try {
|
|
103
|
+
const parsed = parseA2AMessage(message);
|
|
104
|
+
const steerMode = cfg.messages?.queue?.mode === "steer";
|
|
105
|
+
const hasActiveRun = hasActiveTask(parsed.sessionId);
|
|
106
|
+
if (steerMode && hasActiveRun) {
|
|
107
|
+
// Steer模式且有活跃任务:不入队列,直接并发执行
|
|
108
|
+
log(`[MONITOR-HANDLER] 🔄 STEER MODE: Executing concurrently for messageKey=${messageKey}`);
|
|
109
|
+
log(`[MONITOR-HANDLER] - sessionId: ${parsed.sessionId}`);
|
|
110
|
+
log(`[MONITOR-HANDLER] - Bypassing queue to allow message insertion`);
|
|
111
|
+
void task().catch((err) => {
|
|
112
|
+
error(`XY gateway: concurrent steer task failed for ${messageKey}: ${String(err)}`);
|
|
113
|
+
activeMessages.delete(messageKey);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
// 正常模式:入队列串行执行
|
|
118
|
+
log(`[MONITOR-HANDLER] 📋 NORMAL MODE: Enqueuing for messageKey=${messageKey}`);
|
|
119
|
+
void enqueue(sessionId, task).catch((err) => {
|
|
120
|
+
error(`XY gateway: queue processing failed for session ${sessionId}: ${String(err)}`);
|
|
121
|
+
activeMessages.delete(messageKey);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (parseErr) {
|
|
126
|
+
// 解析失败,回退到正常队列模式
|
|
127
|
+
error(`[MONITOR-HANDLER] Failed to parse message for steer detection: ${String(parseErr)}`);
|
|
128
|
+
void enqueue(sessionId, task).catch((err) => {
|
|
129
|
+
error(`XY gateway: queue processing failed for session ${sessionId}: ${String(err)}`);
|
|
130
|
+
activeMessages.delete(messageKey);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
102
133
|
};
|
|
103
134
|
const connectedHandler = (serverId) => {
|
|
104
135
|
if (!loggedServers.has(serverId)) {
|
|
@@ -120,6 +151,15 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
120
151
|
const errorHandler = (err, serverId) => {
|
|
121
152
|
error(`XY gateway: ${serverId} error: ${String(err)}`);
|
|
122
153
|
};
|
|
154
|
+
const triggerEventHandler = (context) => {
|
|
155
|
+
log(`[MONITOR] 📌 Received trigger-event, dispatching to handler...`);
|
|
156
|
+
log(`[MONITOR] - sessionId: ${context.sessionId}`);
|
|
157
|
+
log(`[MONITOR] - taskId: ${context.taskId}`);
|
|
158
|
+
// 异步处理 Trigger 事件,不阻塞主流程
|
|
159
|
+
handleTriggerEvent(context, cfg, runtime, accountId).catch((err) => {
|
|
160
|
+
error(`[MONITOR] Failed to handle trigger-event:`, err);
|
|
161
|
+
});
|
|
162
|
+
};
|
|
123
163
|
const cleanup = () => {
|
|
124
164
|
log("XY gateway: cleaning up...");
|
|
125
165
|
// 🔍 Diagnose before cleanup
|
|
@@ -136,6 +176,7 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
136
176
|
wsManager.off("connected", connectedHandler);
|
|
137
177
|
wsManager.off("disconnected", disconnectedHandler);
|
|
138
178
|
wsManager.off("error", errorHandler);
|
|
179
|
+
wsManager.off("trigger-event", triggerEventHandler);
|
|
139
180
|
// ✅ Disconnect the wsManager to prevent connection leaks
|
|
140
181
|
// This is safe because each gateway lifecycle should have clean connections
|
|
141
182
|
wsManager.disconnect();
|
|
@@ -165,6 +206,7 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
165
206
|
wsManager.on("connected", connectedHandler);
|
|
166
207
|
wsManager.on("disconnected", disconnectedHandler);
|
|
167
208
|
wsManager.on("error", errorHandler);
|
|
209
|
+
wsManager.on("trigger-event", triggerEventHandler);
|
|
168
210
|
// Start periodic health check (every 5 minutes)
|
|
169
211
|
console.log("🏥 Starting periodic health check (every 5 minutes)...");
|
|
170
212
|
healthCheckInterval = setInterval(() => {
|
package/dist/src/outbound.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { resolveXYConfig } from "./config.js";
|
|
2
2
|
import { XYFileUploadService } from "./file-upload.js";
|
|
3
3
|
import { XYPushService } from "./push.js";
|
|
4
|
-
import {
|
|
4
|
+
import { getCurrentSessionContext } from "./tools/session-manager.js";
|
|
5
|
+
import { savePushData } from "./utils/pushdata-manager.js";
|
|
6
|
+
import { getAllPushIds } from "./utils/pushid-manager.js";
|
|
5
7
|
// Special marker for default push delivery when no target is specified
|
|
6
8
|
const DEFAULT_PUSH_MARKER = "default";
|
|
7
9
|
// File extension to MIME type mapping
|
|
@@ -65,8 +67,8 @@ export const xyOutbound = {
|
|
|
65
67
|
// If the target doesn't contain "::", try to enhance it with taskId from session context
|
|
66
68
|
if (!trimmedTo.includes("::")) {
|
|
67
69
|
console.log(`[xyOutbound.resolveTarget] Target "${trimmedTo}" missing taskId, looking up session context`);
|
|
68
|
-
// Try to get the
|
|
69
|
-
const sessionContext =
|
|
70
|
+
// Try to get the current session context
|
|
71
|
+
const sessionContext = getCurrentSessionContext();
|
|
70
72
|
if (sessionContext && sessionContext.sessionId === trimmedTo) {
|
|
71
73
|
const enhancedTarget = `${trimmedTo}::${sessionContext.taskId}`;
|
|
72
74
|
console.log(`[xyOutbound.resolveTarget] Enhanced target: ${enhancedTarget}`);
|
|
@@ -105,19 +107,63 @@ export const xyOutbound = {
|
|
|
105
107
|
// The push service will handle it based on config
|
|
106
108
|
actualTo = config.defaultSessionId || "";
|
|
107
109
|
}
|
|
110
|
+
// 1. 持久化推送消息内容,获取 pushDataId
|
|
111
|
+
console.log(`[xyOutbound.sendText] Saving push data to local storage...`);
|
|
112
|
+
let pushDataId;
|
|
113
|
+
try {
|
|
114
|
+
pushDataId = await savePushData(text);
|
|
115
|
+
console.log(`[xyOutbound.sendText] ✅ Push data saved with ID: ${pushDataId}`);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
console.error(`[xyOutbound.sendText] ❌ Failed to save push data:`, error);
|
|
119
|
+
// 如果持久化失败,仍然继续发送(不阻塞主流程)
|
|
120
|
+
pushDataId = "";
|
|
121
|
+
}
|
|
122
|
+
// 2. 读取所有 pushId
|
|
123
|
+
console.log(`[xyOutbound.sendText] Loading all pushIds...`);
|
|
124
|
+
let pushIdList = [];
|
|
125
|
+
try {
|
|
126
|
+
pushIdList = await getAllPushIds();
|
|
127
|
+
console.log(`[xyOutbound.sendText] ✅ Loaded ${pushIdList.length} pushIds`);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
console.error(`[xyOutbound.sendText] ❌ Failed to load pushIds:`, error);
|
|
131
|
+
}
|
|
132
|
+
// 3. 如果 pushIdList 为空,回退到原有逻辑(使用 config pushId)
|
|
133
|
+
if (pushIdList.length === 0) {
|
|
134
|
+
console.log(`[xyOutbound.sendText] ⚠️ No pushIds found, falling back to config pushId`);
|
|
135
|
+
pushIdList = [config.pushId];
|
|
136
|
+
}
|
|
108
137
|
// Create push service
|
|
109
138
|
const pushService = new XYPushService(config);
|
|
110
139
|
// Extract title (first 57 chars or first line)
|
|
111
140
|
const title = text.split("\n")[0].slice(0, 57);
|
|
112
141
|
// Truncate push content to max length 1000
|
|
113
142
|
const pushText = text.length > 1000 ? text.slice(0, 1000) : text;
|
|
114
|
-
//
|
|
115
|
-
|
|
143
|
+
// 4. 遍历所有 pushId,依次发送推送通知
|
|
144
|
+
console.log(`[xyOutbound.sendText] 📤 Broadcasting to ${pushIdList.length} pushId(s)...`);
|
|
145
|
+
let successCount = 0;
|
|
146
|
+
let failureCount = 0;
|
|
147
|
+
for (const pushId of pushIdList) {
|
|
148
|
+
try {
|
|
149
|
+
console.log(`[xyOutbound.sendText] Sending to pushId: ${pushId.substring(0, 20)}...`);
|
|
150
|
+
// 传入 pushId 和 pushDataId,使用 kind="data" 格式
|
|
151
|
+
await pushService.sendPush(pushText, title, undefined, actualTo, pushDataId, pushId);
|
|
152
|
+
successCount++;
|
|
153
|
+
console.log(`[xyOutbound.sendText] ✅ Sent successfully to pushId: ${pushId.substring(0, 20)}...`);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
failureCount++;
|
|
157
|
+
console.error(`[xyOutbound.sendText] ❌ Failed to send to pushId: ${pushId.substring(0, 20)}...`, error);
|
|
158
|
+
// 单个 pushId 发送失败不影响其他,继续处理下一个
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
console.log(`[xyOutbound.sendText] 📊 Broadcast summary: ${successCount} success, ${failureCount} failures`);
|
|
116
162
|
console.log(`[xyOutbound.sendText] Completed successfully`);
|
|
117
163
|
// Return message info
|
|
118
164
|
return {
|
|
119
165
|
channel: "xiaoyi-channel",
|
|
120
|
-
messageId: Date.now().toString(),
|
|
166
|
+
messageId: pushDataId || Date.now().toString(),
|
|
121
167
|
chatId: actualTo,
|
|
122
168
|
};
|
|
123
169
|
},
|
package/dist/src/parser.d.ts
CHANGED
|
@@ -43,6 +43,13 @@ export declare function isTasksCancelMessage(method: string): boolean;
|
|
|
43
43
|
* Looks for push_id in data parts under variables.systemVariables.push_id
|
|
44
44
|
*/
|
|
45
45
|
export declare function extractPushId(parts: A2AMessagePart[]): string | null;
|
|
46
|
+
/**
|
|
47
|
+
* Extract Trigger event data from message parts.
|
|
48
|
+
* Looks for Trigger events with pushDataId in data parts.
|
|
49
|
+
*/
|
|
50
|
+
export declare function extractTriggerData(parts: A2AMessagePart[]): {
|
|
51
|
+
pushDataId: string;
|
|
52
|
+
} | null;
|
|
46
53
|
/**
|
|
47
54
|
* Validate A2A request structure.
|
|
48
55
|
*/
|
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,27 +20,27 @@ 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
37
|
console.log(`[PUSH] - Title: "${title}"`);
|
|
33
38
|
console.log(`[PUSH] - Content length: ${content.length} chars`);
|
|
34
39
|
console.log(`[PUSH] - Session ID: ${sessionId || 'none'}`);
|
|
35
40
|
console.log(`[PUSH] - Trace ID: ${traceId}`);
|
|
36
41
|
console.log(`[PUSH] - Push URL: ${pushUrl}`);
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
}
|
|
42
|
+
console.log(`[PUSH] - Using pushId: ${actualPushId.substring(0, 20)}...`);
|
|
43
|
+
console.log(`[PUSH] - Full pushId: ${actualPushId}`);
|
|
45
44
|
console.log(`[PUSH] - API ID: ${this.config.apiId}`);
|
|
46
45
|
console.log(`[PUSH] - UID: ${this.config.uid}`);
|
|
47
46
|
try {
|
|
@@ -51,18 +50,27 @@ export class XYPushService {
|
|
|
51
50
|
result: {
|
|
52
51
|
id: randomUUID(),
|
|
53
52
|
apiId: this.config.apiId,
|
|
54
|
-
pushId:
|
|
53
|
+
pushId: actualPushId,
|
|
55
54
|
pushText: title,
|
|
56
55
|
kind: "task",
|
|
57
56
|
artifacts: [
|
|
58
57
|
{
|
|
59
58
|
artifactId: randomUUID(),
|
|
60
|
-
parts:
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
parts: pushDataId
|
|
60
|
+
? [
|
|
61
|
+
{
|
|
62
|
+
kind: "data",
|
|
63
|
+
data: {
|
|
64
|
+
pushDataId: pushDataId,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
]
|
|
68
|
+
: [
|
|
69
|
+
{
|
|
70
|
+
kind: "text",
|
|
71
|
+
text: content,
|
|
72
|
+
},
|
|
73
|
+
],
|
|
66
74
|
},
|
|
67
75
|
],
|
|
68
76
|
},
|
|
@@ -114,14 +122,14 @@ export class XYPushService {
|
|
|
114
122
|
console.log(`[PUSH] ✅ Push message sent successfully`);
|
|
115
123
|
console.log(`[PUSH] - Title: "${title}"`);
|
|
116
124
|
console.log(`[PUSH] - Trace ID: ${traceId}`);
|
|
117
|
-
console.log(`[PUSH] - Used pushId: ${
|
|
125
|
+
console.log(`[PUSH] - Used pushId: ${actualPushId.substring(0, 20)}...`);
|
|
118
126
|
console.log(`[PUSH] - Response:`, result);
|
|
119
127
|
}
|
|
120
128
|
catch (error) {
|
|
121
129
|
console.log(`[PUSH] ❌ Failed to send push message`);
|
|
122
130
|
console.log(`[PUSH] - Trace ID: ${traceId}`);
|
|
123
131
|
console.log(`[PUSH] - Target URL: ${pushUrl}`);
|
|
124
|
-
console.log(`[PUSH] - Push ID: ${
|
|
132
|
+
console.log(`[PUSH] - Push ID: ${actualPushId.substring(0, 20)}...`);
|
|
125
133
|
if (error instanceof Error) {
|
|
126
134
|
console.log(`[PUSH] - Error name: ${error.name}`);
|
|
127
135
|
console.log(`[PUSH] - Error message: ${error.message}`);
|