@ynhcj/xiaoyi-channel 0.0.46-beta → 0.0.46-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 +44 -2
- package/dist/src/bot.d.ts +1 -0
- package/dist/src/bot.js +47 -51
- package/dist/src/channel.js +29 -4
- package/dist/src/client.js +0 -9
- 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 +2 -0
- package/dist/src/formatter.js +13 -28
- package/dist/src/heartbeat.js +0 -4
- package/dist/src/monitor.js +8 -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 +1 -19
- package/dist/src/parser.d.ts +6 -0
- package/dist/src/parser.js +16 -0
- package/dist/src/provider.d.ts +2 -0
- package/dist/src/provider.js +80 -0
- package/dist/src/push.js +0 -21
- package/dist/src/reply-dispatcher.d.ts +4 -0
- package/dist/src/reply-dispatcher.js +46 -6
- 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 +2 -37
- package/dist/src/tools/call-phone-tool.js +3 -60
- package/dist/src/tools/create-alarm-tool.js +6 -91
- package/dist/src/tools/delete-alarm-tool.js +3 -56
- package/dist/src/tools/device-tool-map.d.ts +4 -0
- package/dist/src/tools/device-tool-map.js +31 -0
- 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 +1 -32
- package/dist/src/tools/modify-alarm-tool.js +6 -94
- package/dist/src/tools/modify-note-tool.js +1 -34
- package/dist/src/tools/note-tool.js +2 -4
- package/dist/src/tools/search-alarm-tool.js +12 -118
- package/dist/src/tools/search-calendar-tool.js +4 -81
- 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 +7 -56
- package/dist/src/tools/send-command-to-car-tool.d.ts +5 -0
- package/dist/src/tools/send-command-to-car-tool.js +85 -0
- 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.d.ts +1 -0
- 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.js +0 -11
- package/dist/src/tools/xiaoyi-collection-tool.js +4 -82
- package/dist/src/tools/xiaoyi-gui-tool.js +0 -34
- package/dist/src/utils/runtime-manager.d.ts +7 -0
- package/dist/src/utils/runtime-manager.js +42 -0
- package/dist/src/websocket.js +33 -13
- package/openclaw.plugin.json +1 -0
- package/package.json +3 -4
- 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
|
@@ -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
|
@@ -55,12 +55,10 @@ export class XYFileUploadService {
|
|
|
55
55
|
throw new Error(`Prepare failed: HTTP ${prepareResp.status}`);
|
|
56
56
|
}
|
|
57
57
|
const prepareData = await prepareResp.json();
|
|
58
|
-
console.log(`[XY File Upload] Prepare response:`, JSON.stringify(prepareData, null, 2));
|
|
59
58
|
if (prepareData.code !== "0") {
|
|
60
59
|
throw new Error(`Prepare failed: ${prepareData.desc}`);
|
|
61
60
|
}
|
|
62
61
|
const { objectId, draftId, uploadInfos } = prepareData;
|
|
63
|
-
console.log(`[XY File Upload] Prepare complete: objectId=${objectId}, draftId=${draftId}`);
|
|
64
62
|
// Phase 2: Upload
|
|
65
63
|
console.log(`[XY File Upload] Phase 2: Upload file data`);
|
|
66
64
|
const uploadInfo = uploadInfos[0]; // Single-part upload
|
|
@@ -69,11 +67,8 @@ export class XYFileUploadService {
|
|
|
69
67
|
headers: uploadInfo.headers,
|
|
70
68
|
body: fileBuffer,
|
|
71
69
|
});
|
|
72
|
-
console.log(`[XY File Upload] Upload response status: ${uploadResp.status}, url: ${uploadInfo.url}`);
|
|
73
|
-
console.log(`[XY File Upload] Upload response headers:`, JSON.stringify(Object.fromEntries(uploadResp.headers.entries()), null, 2));
|
|
74
70
|
if (!uploadResp.ok) {
|
|
75
71
|
const uploadErrorText = await uploadResp.text();
|
|
76
|
-
console.log(`[XY File Upload] Upload error response:`, uploadErrorText);
|
|
77
72
|
throw new Error(`Upload failed: HTTP ${uploadResp.status}`);
|
|
78
73
|
}
|
|
79
74
|
console.log(`[XY File Upload] Upload complete`);
|
|
@@ -96,7 +91,6 @@ export class XYFileUploadService {
|
|
|
96
91
|
throw new Error(`Complete failed: HTTP ${completeResp.status}`);
|
|
97
92
|
}
|
|
98
93
|
const completeData = await completeResp.json();
|
|
99
|
-
console.log(`[XY File Upload] Complete response:`, JSON.stringify(completeData, null, 2));
|
|
100
94
|
console.log(`[XY File Upload] File upload successful: ${fileName} → objectId=${objectId}`);
|
|
101
95
|
return objectId;
|
|
102
96
|
}
|
|
@@ -105,6 +99,94 @@ export class XYFileUploadService {
|
|
|
105
99
|
return "";
|
|
106
100
|
}
|
|
107
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Upload a file and return its publicly accessible URL.
|
|
104
|
+
* Uses completeAndQuery endpoint to get the file URL directly.
|
|
105
|
+
*/
|
|
106
|
+
async uploadFileAndGetUrl(filePath, objectType = "TEMPORARY_MATERIAL_DOC") {
|
|
107
|
+
try {
|
|
108
|
+
// Read file
|
|
109
|
+
const fileBuffer = await fs.readFile(filePath);
|
|
110
|
+
const fileName = path.basename(filePath);
|
|
111
|
+
const fileSha256 = calculateSHA256(fileBuffer);
|
|
112
|
+
const fileSize = fileBuffer.length;
|
|
113
|
+
// Phase 1: Prepare
|
|
114
|
+
console.log(`[XY File Upload] Phase 1: Prepare upload for ${fileName}`);
|
|
115
|
+
const prepareResp = await fetch(`${this.baseUrl}/osms/v1/file/manager/prepare`, {
|
|
116
|
+
method: "POST",
|
|
117
|
+
headers: {
|
|
118
|
+
"Content-Type": "application/json",
|
|
119
|
+
"x-uid": this.uid,
|
|
120
|
+
"x-api-key": this.apiKey,
|
|
121
|
+
"x-request-from": "openclaw",
|
|
122
|
+
},
|
|
123
|
+
body: JSON.stringify({
|
|
124
|
+
objectType,
|
|
125
|
+
fileName,
|
|
126
|
+
fileSha256,
|
|
127
|
+
fileSize,
|
|
128
|
+
fileOwnerInfo: {
|
|
129
|
+
uid: this.uid,
|
|
130
|
+
teamId: this.uid,
|
|
131
|
+
},
|
|
132
|
+
useEdge: false,
|
|
133
|
+
}),
|
|
134
|
+
});
|
|
135
|
+
if (!prepareResp.ok) {
|
|
136
|
+
throw new Error(`Prepare failed: HTTP ${prepareResp.status}`);
|
|
137
|
+
}
|
|
138
|
+
const prepareData = await prepareResp.json();
|
|
139
|
+
if (prepareData.code !== "0") {
|
|
140
|
+
throw new Error(`Prepare failed: ${prepareData.desc}`);
|
|
141
|
+
}
|
|
142
|
+
const { objectId, draftId, uploadInfos } = prepareData;
|
|
143
|
+
console.log(`[XY File Upload] Prepare complete: objectId=${objectId}, draftId=${draftId}`);
|
|
144
|
+
// Phase 2: Upload
|
|
145
|
+
console.log(`[XY File Upload] Phase 2: Upload file data`);
|
|
146
|
+
const uploadInfo = uploadInfos[0]; // Single-part upload
|
|
147
|
+
const uploadResp = await fetch(uploadInfo.url, {
|
|
148
|
+
method: uploadInfo.method,
|
|
149
|
+
headers: uploadInfo.headers,
|
|
150
|
+
body: fileBuffer,
|
|
151
|
+
});
|
|
152
|
+
console.log(`[XY File Upload] Upload response status: ${uploadResp.status}`);
|
|
153
|
+
if (!uploadResp.ok) {
|
|
154
|
+
const uploadErrorText = await uploadResp.text();
|
|
155
|
+
throw new Error(`Upload failed: HTTP ${uploadResp.status}`);
|
|
156
|
+
}
|
|
157
|
+
console.log(`[XY File Upload] Upload complete`);
|
|
158
|
+
// Phase 3: CompleteAndQuery - get file URL
|
|
159
|
+
console.log(`[XY File Upload] Phase 3: CompleteAndQuery to get file URL`);
|
|
160
|
+
const completeResp = await fetch(`${this.baseUrl}/osms/v1/file/manager/completeAndQuery`, {
|
|
161
|
+
method: "POST",
|
|
162
|
+
headers: {
|
|
163
|
+
"Content-Type": "application/json",
|
|
164
|
+
"x-uid": this.uid,
|
|
165
|
+
"x-api-key": this.apiKey,
|
|
166
|
+
"x-request-from": "openclaw",
|
|
167
|
+
},
|
|
168
|
+
body: JSON.stringify({
|
|
169
|
+
objectId,
|
|
170
|
+
draftId,
|
|
171
|
+
}),
|
|
172
|
+
});
|
|
173
|
+
if (!completeResp.ok) {
|
|
174
|
+
throw new Error(`CompleteAndQuery failed: HTTP ${completeResp.status}`);
|
|
175
|
+
}
|
|
176
|
+
const completeData = await completeResp.json();
|
|
177
|
+
// Extract file URL from response
|
|
178
|
+
const fileUrl = completeData?.fileDetailInfo?.url || "";
|
|
179
|
+
if (!fileUrl) {
|
|
180
|
+
throw new Error("No file URL returned from completeAndQuery");
|
|
181
|
+
}
|
|
182
|
+
console.log(`[XY File Upload] File upload successful`);
|
|
183
|
+
return fileUrl;
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
console.error(`[XY File Upload] File upload with URL retrieval failed for ${filePath}:`, error);
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
108
190
|
/**
|
|
109
191
|
* Upload multiple files and return their file IDs.
|
|
110
192
|
*/
|
package/dist/src/formatter.d.ts
CHANGED
package/dist/src/formatter.js
CHANGED
|
@@ -6,10 +6,10 @@ import { getXYRuntime } from "./runtime.js";
|
|
|
6
6
|
* Send an A2A artifact update response.
|
|
7
7
|
*/
|
|
8
8
|
export async function sendA2AResponse(params) {
|
|
9
|
-
const { config, sessionId, taskId, messageId, text, append, final, files } = params;
|
|
9
|
+
const { config, sessionId, taskId, messageId, text, append, final, files, errorCode, errorMessage } = params;
|
|
10
10
|
const runtime = getXYRuntime();
|
|
11
11
|
const log = runtime?.log ?? console.log;
|
|
12
|
-
const
|
|
12
|
+
const errorFn = runtime?.error ?? console.error;
|
|
13
13
|
// Build artifact update event
|
|
14
14
|
const artifact = {
|
|
15
15
|
taskId,
|
|
@@ -42,6 +42,14 @@ export async function sendA2AResponse(params) {
|
|
|
42
42
|
id: messageId,
|
|
43
43
|
result: artifact,
|
|
44
44
|
};
|
|
45
|
+
// 🔑 添加 error 字段(仅当提供 errorCode 时)
|
|
46
|
+
if (errorCode !== undefined) {
|
|
47
|
+
jsonRpcResponse.error = {
|
|
48
|
+
code: errorCode,
|
|
49
|
+
message: errorMessage ?? "任务执行异常,请重试",
|
|
50
|
+
};
|
|
51
|
+
log(`[A2A_RESPONSE] ⚠️ Including error code: ${errorCode}`);
|
|
52
|
+
}
|
|
45
53
|
// Send via WebSocket
|
|
46
54
|
const wsManager = getXYWebSocketManager(config);
|
|
47
55
|
const outboundMessage = {
|
|
@@ -52,18 +60,11 @@ export async function sendA2AResponse(params) {
|
|
|
52
60
|
msgDetail: JSON.stringify(jsonRpcResponse),
|
|
53
61
|
};
|
|
54
62
|
// 📋 Log complete response body
|
|
55
|
-
log(`[A2A_RESPONSE] 📤 Sending A2A artifact-update response
|
|
56
|
-
log(`[A2A_RESPONSE] - sessionId: ${sessionId}`);
|
|
57
|
-
log(`[A2A_RESPONSE] - taskId: ${taskId}`);
|
|
58
|
-
log(`[A2A_RESPONSE] - messageId: ${messageId}`);
|
|
63
|
+
log(`[A2A_RESPONSE] 📤 Sending A2A artifact-update response: taskId: ${taskId}`);
|
|
59
64
|
log(`[A2A_RESPONSE] - append: ${append}`);
|
|
60
65
|
log(`[A2A_RESPONSE] - final: ${final}`);
|
|
61
|
-
log(`[A2A_RESPONSE] - text
|
|
66
|
+
log(`[A2A_RESPONSE] - text: ${text.length <= 10 ? text : text.slice(0, 5) + '***' + text.slice(-5)}`);
|
|
62
67
|
log(`[A2A_RESPONSE] - files count: ${files?.length ?? 0}`);
|
|
63
|
-
log(`[A2A_RESPONSE] 📦 Complete outbound message:`);
|
|
64
|
-
log(JSON.stringify(outboundMessage, null, 2));
|
|
65
|
-
log(`[A2A_RESPONSE] 📦 JSON-RPC response body:`);
|
|
66
|
-
log(JSON.stringify(jsonRpcResponse, null, 2));
|
|
67
68
|
await wsManager.sendMessage(sessionId, outboundMessage);
|
|
68
69
|
log(`[A2A_RESPONSE] ✅ Message sent successfully`);
|
|
69
70
|
}
|
|
@@ -152,17 +153,9 @@ export async function sendStatusUpdate(params) {
|
|
|
152
153
|
};
|
|
153
154
|
// 📋 Log complete response body
|
|
154
155
|
log(`[A2A_STATUS] 📤 Sending A2A status-update:`);
|
|
155
|
-
log(`[A2A_STATUS] - sessionId: ${sessionId}`);
|
|
156
156
|
log(`[A2A_STATUS] - taskId: ${taskId}`);
|
|
157
|
-
log(`[A2A_STATUS] - messageId: ${messageId}`);
|
|
158
|
-
log(`[A2A_STATUS] - state: ${state}`);
|
|
159
157
|
log(`[A2A_STATUS] - text: "${text}"`);
|
|
160
|
-
log(`[A2A_STATUS] 📦 Complete outbound message:`);
|
|
161
|
-
log(JSON.stringify(outboundMessage, null, 2));
|
|
162
|
-
log(`[A2A_STATUS] 📦 JSON-RPC response body:`);
|
|
163
|
-
log(JSON.stringify(jsonRpcResponse, null, 2));
|
|
164
158
|
await wsManager.sendMessage(sessionId, outboundMessage);
|
|
165
|
-
log(`[A2A_STATUS] ✅ Status update sent successfully`);
|
|
166
159
|
}
|
|
167
160
|
/**
|
|
168
161
|
* Send a command as an artifact update (final=false).
|
|
@@ -208,15 +201,7 @@ export async function sendCommand(params) {
|
|
|
208
201
|
msgDetail: JSON.stringify(jsonRpcResponse),
|
|
209
202
|
};
|
|
210
203
|
// 📋 Log complete response body
|
|
211
|
-
log(`[A2A_COMMAND] 📤 Sending A2A command
|
|
212
|
-
log(`[A2A_COMMAND] - sessionId: ${sessionId}`);
|
|
213
|
-
log(`[A2A_COMMAND] - taskId: ${taskId}`);
|
|
214
|
-
log(`[A2A_COMMAND] - messageId: ${messageId}`);
|
|
215
|
-
log(`[A2A_COMMAND] - command: ${command.header.namespace}::${command.header.name}`);
|
|
216
|
-
log(`[A2A_COMMAND] 📦 Complete outbound message:`);
|
|
217
|
-
log(JSON.stringify(outboundMessage, null, 2));
|
|
218
|
-
log(`[A2A_COMMAND] 📦 JSON-RPC response body:`);
|
|
219
|
-
log(JSON.stringify(jsonRpcResponse, null, 2));
|
|
204
|
+
log(`[A2A_COMMAND] 📤 Sending A2A command: taskId: ${taskId}`);
|
|
220
205
|
await wsManager.sendMessage(sessionId, outboundMessage);
|
|
221
206
|
log(`[A2A_COMMAND] ✅ Command sent successfully`);
|
|
222
207
|
}
|
package/dist/src/heartbeat.js
CHANGED
|
@@ -72,18 +72,14 @@ export class HeartbeatManager {
|
|
|
72
72
|
}
|
|
73
73
|
try {
|
|
74
74
|
// Send application-level heartbeat message
|
|
75
|
-
console.log(`[WS-${this.serverName}-SEND] Sending heartbeat frame:`, this.config.message);
|
|
76
75
|
this.ws.send(this.config.message);
|
|
77
|
-
console.log(`[WS-${this.serverName}-SEND] Heartbeat message sent, size: ${this.config.message.length} bytes`);
|
|
78
76
|
// Send protocol-level ping
|
|
79
77
|
this.ws.ping();
|
|
80
|
-
console.log(`[WS-${this.serverName}-SEND] Protocol-level ping sent`);
|
|
81
78
|
// Setup timeout timer
|
|
82
79
|
this.timeoutTimer = setTimeout(() => {
|
|
83
80
|
this.error(`Heartbeat timeout for ${this.serverName}`);
|
|
84
81
|
this.onTimeout();
|
|
85
82
|
}, this.config.timeout);
|
|
86
|
-
this.log(`[DEBUG] Heartbeat sent for ${this.serverName}`);
|
|
87
83
|
}
|
|
88
84
|
catch (error) {
|
|
89
85
|
this.error(`Failed to send heartbeat for ${this.serverName}:`, error);
|
package/dist/src/monitor.js
CHANGED
|
@@ -4,6 +4,7 @@ import { handleXYMessage } from "./bot.js";
|
|
|
4
4
|
import { parseA2AMessage } from "./parser.js";
|
|
5
5
|
import { hasActiveTask } from "./task-manager.js";
|
|
6
6
|
import { handleTriggerEvent } from "./trigger-handler.js";
|
|
7
|
+
import { cleanupStaleTempFiles } from "./reply-dispatcher.js";
|
|
7
8
|
/**
|
|
8
9
|
* Per-session serial queue that ensures messages from the same session are processed
|
|
9
10
|
* in arrival order while allowing different sessions to run concurrently.
|
|
@@ -67,7 +68,7 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
67
68
|
// Event handlers (defined early so they can be referenced in cleanup)
|
|
68
69
|
const messageHandler = (message, sessionId, serverId) => {
|
|
69
70
|
const messageKey = `${sessionId}::${message.id}`;
|
|
70
|
-
log(`[MONITOR-HANDLER] ####### messageHandler triggered:
|
|
71
|
+
log(`[MONITOR-HANDLER] ####### messageHandler triggered: sessionId=${sessionId}, messageId=${message.id} #######`);
|
|
71
72
|
// ✅ Report health: received a message
|
|
72
73
|
trackEvent?.();
|
|
73
74
|
// Check for duplicate message handling
|
|
@@ -75,17 +76,15 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
75
76
|
error(`[MONITOR-HANDLER] ⚠️ WARNING: Duplicate message detected! messageKey=${messageKey}, this may cause duplicate dispatchers!`);
|
|
76
77
|
}
|
|
77
78
|
activeMessages.add(messageKey);
|
|
78
|
-
log(`[MONITOR-HANDLER] 📝 Active messages count: ${activeMessages.size}, messageKey: ${messageKey}`);
|
|
79
79
|
const task = async () => {
|
|
80
80
|
try {
|
|
81
|
-
log(`[MONITOR-HANDLER] 🚀 Starting handleXYMessage for messageKey=${messageKey}`);
|
|
82
81
|
await handleXYMessage({
|
|
83
82
|
cfg,
|
|
84
83
|
runtime,
|
|
85
84
|
message,
|
|
86
85
|
accountId, // ✅ Pass accountId ("default")
|
|
86
|
+
webSocketSessionId: sessionId, // ✅ 传递 WebSocket 层级的 sessionId
|
|
87
87
|
});
|
|
88
|
-
log(`[MONITOR-HANDLER] ✅ Completed handleXYMessage for messageKey=${messageKey}`);
|
|
89
88
|
}
|
|
90
89
|
catch (err) {
|
|
91
90
|
// ✅ Only log error, don't re-throw to prevent gateway restart
|
|
@@ -94,7 +93,6 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
94
93
|
finally {
|
|
95
94
|
// Remove from active messages when done
|
|
96
95
|
activeMessages.delete(messageKey);
|
|
97
|
-
log(`[MONITOR-HANDLER] 🧹 Cleaned up messageKey=${messageKey}, remaining active: ${activeMessages.size}`);
|
|
98
96
|
}
|
|
99
97
|
};
|
|
100
98
|
// 🔑 核心改造:检测steer模式
|
|
@@ -107,7 +105,6 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
107
105
|
// Steer模式且有活跃任务:不入队列,直接并发执行
|
|
108
106
|
log(`[MONITOR-HANDLER] 🔄 STEER MODE: Executing concurrently for messageKey=${messageKey}`);
|
|
109
107
|
log(`[MONITOR-HANDLER] - sessionId: ${parsed.sessionId}`);
|
|
110
|
-
log(`[MONITOR-HANDLER] - Bypassing queue to allow message insertion`);
|
|
111
108
|
void task().catch((err) => {
|
|
112
109
|
error(`XY gateway: concurrent steer task failed for ${messageKey}: ${String(err)}`);
|
|
113
110
|
activeMessages.delete(messageKey);
|
|
@@ -115,7 +112,6 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
115
112
|
}
|
|
116
113
|
else {
|
|
117
114
|
// 正常模式:入队列串行执行
|
|
118
|
-
log(`[MONITOR-HANDLER] 📋 NORMAL MODE: Enqueuing for messageKey=${messageKey}`);
|
|
119
115
|
void enqueue(sessionId, task).catch((err) => {
|
|
120
116
|
error(`XY gateway: queue processing failed for session ${sessionId}: ${String(err)}`);
|
|
121
117
|
activeMessages.delete(messageKey);
|
|
@@ -207,8 +203,8 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
207
203
|
wsManager.on("disconnected", disconnectedHandler);
|
|
208
204
|
wsManager.on("error", errorHandler);
|
|
209
205
|
wsManager.on("trigger-event", triggerEventHandler);
|
|
210
|
-
// Start periodic health check (every
|
|
211
|
-
console.log("🏥 Starting periodic health check (every
|
|
206
|
+
// Start periodic health check (every 6 hours)
|
|
207
|
+
console.log("🏥 Starting periodic health check (every 6 hours)...");
|
|
212
208
|
healthCheckInterval = setInterval(() => {
|
|
213
209
|
console.log("🏥 [HEALTH CHECK] Periodic WebSocket diagnostics...");
|
|
214
210
|
diagnoseAllManagers();
|
|
@@ -217,7 +213,9 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
217
213
|
if (cleaned > 0) {
|
|
218
214
|
console.log(`🧹 [HEALTH CHECK] Auto-cleaned ${cleaned} manager(s) with orphan connections`);
|
|
219
215
|
}
|
|
220
|
-
|
|
216
|
+
// Cleanup stale temp files (older than 24 hours)
|
|
217
|
+
void cleanupStaleTempFiles();
|
|
218
|
+
}, 6 * 60 * 60 * 1000); // 6 hours
|
|
221
219
|
// Connect to WebSocket servers
|
|
222
220
|
wsManager.connect()
|
|
223
221
|
.then(() => {
|
package/dist/src/onboarding.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import type { ChannelOnboardingAdapter } from "openclaw/plugin-sdk";
|
|
2
1
|
/**
|
|
3
|
-
* XY Channel Onboarding Adapter
|
|
4
|
-
*
|
|
2
|
+
* XY Channel Onboarding Adapter (DISABLED - not currently used)
|
|
3
|
+
* NOTE: This is kept for future reference. Xiaoyi uses simple single-account config.
|
|
5
4
|
*/
|
|
6
|
-
export declare const xyOnboardingAdapter:
|
|
5
|
+
export declare const xyOnboardingAdapter: any;
|
package/dist/src/onboarding.js
CHANGED
|
@@ -152,8 +152,8 @@ async function configure({ cfg, prompter, }) {
|
|
|
152
152
|
};
|
|
153
153
|
}
|
|
154
154
|
/**
|
|
155
|
-
* XY Channel Onboarding Adapter
|
|
156
|
-
*
|
|
155
|
+
* XY Channel Onboarding Adapter (DISABLED - not currently used)
|
|
156
|
+
* NOTE: This is kept for future reference. Xiaoyi uses simple single-account config.
|
|
157
157
|
*/
|
|
158
158
|
export const xyOnboardingAdapter = {
|
|
159
159
|
channel,
|
package/dist/src/outbound.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
type ChannelOutboundAdapter = any;
|
|
2
2
|
/**
|
|
3
3
|
* Outbound adapter for sending messages from OpenClaw to XY.
|
|
4
4
|
* Uses Push service for direct message delivery.
|
|
5
5
|
*/
|
|
6
6
|
export declare const xyOutbound: ChannelOutboundAdapter;
|
|
7
|
+
export {};
|
package/dist/src/outbound.js
CHANGED
|
@@ -90,13 +90,6 @@ export const xyOutbound = {
|
|
|
90
90
|
};
|
|
91
91
|
},
|
|
92
92
|
sendText: async ({ cfg, to, text, accountId }) => {
|
|
93
|
-
// Log parameters
|
|
94
|
-
console.log(`[xyOutbound.sendText] Called with:`, {
|
|
95
|
-
to,
|
|
96
|
-
accountId,
|
|
97
|
-
textLength: text?.length || 0,
|
|
98
|
-
textPreview: text?.slice(0, 100),
|
|
99
|
-
});
|
|
100
93
|
// Resolve configuration
|
|
101
94
|
const config = resolveXYConfig(cfg);
|
|
102
95
|
// Handle default push marker (for cron jobs without explicit target)
|
|
@@ -112,7 +105,7 @@ export const xyOutbound = {
|
|
|
112
105
|
let pushDataId;
|
|
113
106
|
try {
|
|
114
107
|
pushDataId = await savePushData(text);
|
|
115
|
-
console.log(`[xyOutbound.sendText] ✅ Push data saved with ID: ${pushDataId}`);
|
|
108
|
+
console.log(`[xyOutbound.sendText] ✅ Push data saved with ID: ${pushDataId.substring(0, 20)}`);
|
|
116
109
|
}
|
|
117
110
|
catch (error) {
|
|
118
111
|
console.error(`[xyOutbound.sendText] ❌ Failed to save push data:`, error);
|
|
@@ -146,7 +139,6 @@ export const xyOutbound = {
|
|
|
146
139
|
let failureCount = 0;
|
|
147
140
|
for (const pushId of pushIdList) {
|
|
148
141
|
try {
|
|
149
|
-
console.log(`[xyOutbound.sendText] Sending to pushId: ${pushId.substring(0, 20)}...`);
|
|
150
142
|
// 传入 pushId 和 pushDataId,使用 kind="data" 格式
|
|
151
143
|
await pushService.sendPush(pushText, title, undefined, actualTo, pushDataId, pushId);
|
|
152
144
|
successCount++;
|
|
@@ -158,8 +150,6 @@ export const xyOutbound = {
|
|
|
158
150
|
// 单个 pushId 发送失败不影响其他,继续处理下一个
|
|
159
151
|
}
|
|
160
152
|
}
|
|
161
|
-
console.log(`[xyOutbound.sendText] 📊 Broadcast summary: ${successCount} success, ${failureCount} failures`);
|
|
162
|
-
console.log(`[xyOutbound.sendText] Completed successfully`);
|
|
163
153
|
// Return message info
|
|
164
154
|
return {
|
|
165
155
|
channel: "xiaoyi-channel",
|
|
@@ -168,14 +158,6 @@ export const xyOutbound = {
|
|
|
168
158
|
};
|
|
169
159
|
},
|
|
170
160
|
sendMedia: async ({ cfg, to, text, mediaUrl, accountId, mediaLocalRoots }) => {
|
|
171
|
-
// Log parameters
|
|
172
|
-
console.log(`[xyOutbound.sendMedia] Called with:`, {
|
|
173
|
-
to,
|
|
174
|
-
accountId,
|
|
175
|
-
text,
|
|
176
|
-
mediaUrl,
|
|
177
|
-
mediaLocalRoots,
|
|
178
|
-
});
|
|
179
161
|
// Parse to: "sessionId::taskId"
|
|
180
162
|
const parts = to.split("::");
|
|
181
163
|
if (parts.length !== 2) {
|
package/dist/src/parser.d.ts
CHANGED
|
@@ -43,6 +43,12 @@ 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 deviceType from message parts.
|
|
48
|
+
* Looks for deviceType in data parts under variables.systemVariables.deviceType
|
|
49
|
+
* (same level as push_id).
|
|
50
|
+
*/
|
|
51
|
+
export declare function extractDeviceType(parts: A2AMessagePart[]): string | null;
|
|
46
52
|
/**
|
|
47
53
|
* Extract Trigger event data from message parts.
|
|
48
54
|
* Looks for Trigger events with pushDataId in data parts.
|
package/dist/src/parser.js
CHANGED
|
@@ -72,6 +72,22 @@ export function extractPushId(parts) {
|
|
|
72
72
|
}
|
|
73
73
|
return null;
|
|
74
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Extract deviceType from message parts.
|
|
77
|
+
* Looks for deviceType in data parts under variables.systemVariables.deviceType
|
|
78
|
+
* (same level as push_id).
|
|
79
|
+
*/
|
|
80
|
+
export function extractDeviceType(parts) {
|
|
81
|
+
for (const part of parts) {
|
|
82
|
+
if (part.kind === "data" && part.data) {
|
|
83
|
+
const deviceType = part.data.variables?.systemVariables?.device_type;
|
|
84
|
+
if (deviceType && typeof deviceType === "string") {
|
|
85
|
+
return deviceType;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
75
91
|
/**
|
|
76
92
|
* Extract Trigger event data from message parts.
|
|
77
93
|
* Looks for Trigger events with pushDataId in data parts.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { getCurrentSessionContext } from "./tools/session-manager.js";
|
|
2
|
+
/**
|
|
3
|
+
* Dynamic header keys injected via extraParams and forwarded to the HTTP request.
|
|
4
|
+
* Correspond to the three fields written to .xiaoyiruntime:
|
|
5
|
+
* TASK_ID, SESSION_ID, CONVERSATION_ID
|
|
6
|
+
*/
|
|
7
|
+
const HEADER_TASK_ID = "x-task-id";
|
|
8
|
+
const HEADER_SESSION_ID = "x-session-id";
|
|
9
|
+
const HEADER_CONVERSATION_ID = "x-conversation-id";
|
|
10
|
+
const EXTRA_PARAM_TASK_ID = "x-task-id";
|
|
11
|
+
const EXTRA_PARAM_SESSION_ID = "x-session-id";
|
|
12
|
+
const EXTRA_PARAM_CONVERSATION_ID = "x-conversation-id";
|
|
13
|
+
export const xiaoyiProvider = {
|
|
14
|
+
id: "xiaoyiprovider",
|
|
15
|
+
label: "Xiaoyi Provider",
|
|
16
|
+
docsPath: "/providers/models",
|
|
17
|
+
auth: [],
|
|
18
|
+
/**
|
|
19
|
+
* Inject dynamic session params into extraParams so they flow
|
|
20
|
+
* through to wrapStreamFn's ctx.extraParams.
|
|
21
|
+
*
|
|
22
|
+
* Reads from AsyncLocalStorage (set by bot.ts runWithSessionContext)
|
|
23
|
+
* which automatically fetches the latest taskId from task-manager
|
|
24
|
+
* (handles steer mode taskId switching).
|
|
25
|
+
*/
|
|
26
|
+
prepareExtraParams: (ctx) => {
|
|
27
|
+
const sessionCtx = getCurrentSessionContext();
|
|
28
|
+
if (!sessionCtx)
|
|
29
|
+
return undefined;
|
|
30
|
+
return {
|
|
31
|
+
...ctx.extraParams,
|
|
32
|
+
[EXTRA_PARAM_TASK_ID]: sessionCtx.taskId,
|
|
33
|
+
[EXTRA_PARAM_SESSION_ID]: sessionCtx.sessionId,
|
|
34
|
+
[EXTRA_PARAM_CONVERSATION_ID]: sessionCtx.messageId,
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
/**
|
|
38
|
+
* Wrap the stream function to inject dynamic headers into every
|
|
39
|
+
* HTTP request to the model provider.
|
|
40
|
+
*
|
|
41
|
+
* Reads the values injected by prepareExtraParams and adds them
|
|
42
|
+
* as HTTP headers on the outgoing request.
|
|
43
|
+
*/
|
|
44
|
+
wrapStreamFn: (ctx) => {
|
|
45
|
+
const underlying = ctx.streamFn;
|
|
46
|
+
if (!underlying)
|
|
47
|
+
return underlying;
|
|
48
|
+
const dynamicHeaders = {};
|
|
49
|
+
if (ctx.extraParams) {
|
|
50
|
+
const taskId = ctx.extraParams[EXTRA_PARAM_TASK_ID];
|
|
51
|
+
const sessionId = ctx.extraParams[EXTRA_PARAM_SESSION_ID];
|
|
52
|
+
const conversationId = ctx.extraParams[EXTRA_PARAM_CONVERSATION_ID];
|
|
53
|
+
if (typeof taskId === "string")
|
|
54
|
+
dynamicHeaders[HEADER_TASK_ID] = taskId;
|
|
55
|
+
if (typeof sessionId === "string")
|
|
56
|
+
dynamicHeaders[HEADER_SESSION_ID] = sessionId;
|
|
57
|
+
if (typeof conversationId === "string")
|
|
58
|
+
dynamicHeaders[HEADER_CONVERSATION_ID] = conversationId;
|
|
59
|
+
}
|
|
60
|
+
if (Object.keys(dynamicHeaders).length === 0)
|
|
61
|
+
return underlying;
|
|
62
|
+
return async (model, context, options) => {
|
|
63
|
+
// 记录输入
|
|
64
|
+
console.log(`[xiaoyiprovider] input messages: ${JSON.stringify(context.messages)}`);
|
|
65
|
+
if (context.systemPrompt) {
|
|
66
|
+
console.log(`[xiaoyiprovider] system prompt: ${context.systemPrompt}`);
|
|
67
|
+
}
|
|
68
|
+
const stream = await underlying(model, context, {
|
|
69
|
+
...options,
|
|
70
|
+
headers: {
|
|
71
|
+
...options?.headers,
|
|
72
|
+
...dynamicHeaders,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
// 异步监听输出(不阻塞 stream 返回)
|
|
76
|
+
stream.result().then((msg) => console.log(`[xiaoyiprovider] output: ${JSON.stringify(msg)}`), (err) => console.log(`[xiaoyiprovider] error: ${err}`));
|
|
77
|
+
return stream;
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
};
|