@ynhcj/xiaoyi 0.0.1-beta
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/README.md +207 -0
- package/dist/auth.d.ts +36 -0
- package/dist/auth.js +111 -0
- package/dist/channel.d.ts +189 -0
- package/dist/channel.js +354 -0
- package/dist/config-schema.d.ts +46 -0
- package/dist/config-schema.js +28 -0
- package/dist/file-download.d.ts +17 -0
- package/dist/file-download.js +69 -0
- package/dist/file-handler.d.ts +36 -0
- package/dist/file-handler.js +113 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +49 -0
- package/dist/onboarding.d.ts +6 -0
- package/dist/onboarding.js +167 -0
- package/dist/push.d.ts +28 -0
- package/dist/push.js +135 -0
- package/dist/runtime.d.ts +191 -0
- package/dist/runtime.js +438 -0
- package/dist/types.d.ts +280 -0
- package/dist/types.js +8 -0
- package/dist/websocket.d.ts +219 -0
- package/dist/websocket.js +1068 -0
- package/dist/xiaoyi-media.d.ts +81 -0
- package/dist/xiaoyi-media.js +216 -0
- package/dist/xy-bot.d.ts +19 -0
- package/dist/xy-bot.js +277 -0
- package/dist/xy-client.d.ts +26 -0
- package/dist/xy-client.js +78 -0
- package/dist/xy-config.d.ts +18 -0
- package/dist/xy-config.js +37 -0
- package/dist/xy-formatter.d.ts +94 -0
- package/dist/xy-formatter.js +303 -0
- package/dist/xy-monitor.d.ts +17 -0
- package/dist/xy-monitor.js +194 -0
- package/dist/xy-parser.d.ts +49 -0
- package/dist/xy-parser.js +109 -0
- package/dist/xy-reply-dispatcher.d.ts +17 -0
- package/dist/xy-reply-dispatcher.js +308 -0
- package/dist/xy-tools/session-manager.d.ts +29 -0
- package/dist/xy-tools/session-manager.js +80 -0
- package/dist/xy-utils/config-manager.d.ts +26 -0
- package/dist/xy-utils/config-manager.js +61 -0
- package/dist/xy-utils/crypto.d.ts +8 -0
- package/dist/xy-utils/crypto.js +21 -0
- package/dist/xy-utils/logger.d.ts +6 -0
- package/dist/xy-utils/logger.js +37 -0
- package/dist/xy-utils/session.d.ts +34 -0
- package/dist/xy-utils/session.js +55 -0
- package/openclaw.plugin.json +9 -0
- package/package.json +73 -0
- package/xiaoyi.js +1 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XiaoYi Media Handler - Downloads and saves media files locally
|
|
3
|
+
* Similar to clawdbot-feishu's media.ts approach
|
|
4
|
+
*/
|
|
5
|
+
type PluginRuntime = any;
|
|
6
|
+
export interface DownloadedMedia {
|
|
7
|
+
path: string;
|
|
8
|
+
contentType: string;
|
|
9
|
+
placeholder: string;
|
|
10
|
+
fileName?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface MediaDownloadOptions {
|
|
13
|
+
maxBytes?: number;
|
|
14
|
+
timeoutMs?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Check if a MIME type is an image
|
|
18
|
+
*/
|
|
19
|
+
export declare function isImageMimeType(mimeType: string | undefined): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Check if a MIME type is a PDF
|
|
22
|
+
*/
|
|
23
|
+
export declare function isPdfMimeType(mimeType: string | undefined): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Check if a MIME type is text-based
|
|
26
|
+
*/
|
|
27
|
+
export declare function isTextMimeType(mimeType: string | undefined): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Download and save media file to local disk
|
|
30
|
+
* This is the key function that follows clawdbot-feishu's approach
|
|
31
|
+
*/
|
|
32
|
+
export declare function downloadAndSaveMedia(runtime: PluginRuntime, uri: string, mimeType: string, fileName: string, options?: MediaDownloadOptions): Promise<DownloadedMedia>;
|
|
33
|
+
/**
|
|
34
|
+
* Download and save multiple media files
|
|
35
|
+
*/
|
|
36
|
+
export declare function downloadAndSaveMediaList(runtime: PluginRuntime, files: Array<{
|
|
37
|
+
uri: string;
|
|
38
|
+
mimeType: string;
|
|
39
|
+
name: string;
|
|
40
|
+
}>, options?: MediaDownloadOptions): Promise<DownloadedMedia[]>;
|
|
41
|
+
/**
|
|
42
|
+
* Build media payload for inbound context
|
|
43
|
+
* Similar to clawdbot-feishu's buildFeishuMediaPayload()
|
|
44
|
+
*/
|
|
45
|
+
export declare function buildXiaoYiMediaPayload(mediaList: DownloadedMedia[]): {
|
|
46
|
+
MediaPath?: string;
|
|
47
|
+
MediaType?: string;
|
|
48
|
+
MediaUrl?: string;
|
|
49
|
+
MediaPaths?: string[];
|
|
50
|
+
MediaUrls?: string[];
|
|
51
|
+
MediaTypes?: string[];
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Extract text from downloaded file for including in message body
|
|
55
|
+
*/
|
|
56
|
+
export declare function extractTextFromFile(path: string, mimeType: string): Promise<string | null>;
|
|
57
|
+
/**
|
|
58
|
+
* Input image content type for AI processing
|
|
59
|
+
*/
|
|
60
|
+
export interface InputImageContent {
|
|
61
|
+
type: "image";
|
|
62
|
+
data: string;
|
|
63
|
+
mimeType: string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Image download limits
|
|
67
|
+
*/
|
|
68
|
+
export interface ImageLimits {
|
|
69
|
+
maxBytes?: number;
|
|
70
|
+
timeoutMs?: number;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Extract image from URL and return base64 encoded data
|
|
74
|
+
*/
|
|
75
|
+
export declare function extractImageFromUrl(url: string, limits?: Partial<ImageLimits>): Promise<InputImageContent>;
|
|
76
|
+
/**
|
|
77
|
+
* Extract text content from URL
|
|
78
|
+
* Supports text-based files (txt, md, json, xml, csv, etc.)
|
|
79
|
+
*/
|
|
80
|
+
export declare function extractTextFromUrl(url: string, maxBytes?: number, timeoutMs?: number): Promise<string>;
|
|
81
|
+
export {};
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* XiaoYi Media Handler - Downloads and saves media files locally
|
|
4
|
+
* Similar to clawdbot-feishu's media.ts approach
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.isImageMimeType = isImageMimeType;
|
|
8
|
+
exports.isPdfMimeType = isPdfMimeType;
|
|
9
|
+
exports.isTextMimeType = isTextMimeType;
|
|
10
|
+
exports.downloadAndSaveMedia = downloadAndSaveMedia;
|
|
11
|
+
exports.downloadAndSaveMediaList = downloadAndSaveMediaList;
|
|
12
|
+
exports.buildXiaoYiMediaPayload = buildXiaoYiMediaPayload;
|
|
13
|
+
exports.extractTextFromFile = extractTextFromFile;
|
|
14
|
+
exports.extractImageFromUrl = extractImageFromUrl;
|
|
15
|
+
exports.extractTextFromUrl = extractTextFromUrl;
|
|
16
|
+
/**
|
|
17
|
+
* Download content from URL with validation
|
|
18
|
+
*/
|
|
19
|
+
async function fetchFromUrl(url, maxBytes, timeoutMs) {
|
|
20
|
+
const controller = new AbortController();
|
|
21
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
22
|
+
try {
|
|
23
|
+
const response = await fetch(url, {
|
|
24
|
+
signal: controller.signal,
|
|
25
|
+
headers: { "User-Agent": "XiaoYi-Channel/1.0" },
|
|
26
|
+
});
|
|
27
|
+
if (!response.ok) {
|
|
28
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
29
|
+
}
|
|
30
|
+
// Check content-length header if available
|
|
31
|
+
const contentLength = response.headers.get("content-length");
|
|
32
|
+
if (contentLength) {
|
|
33
|
+
const size = parseInt(contentLength, 10);
|
|
34
|
+
if (size > maxBytes) {
|
|
35
|
+
throw new Error(`File too large: ${size} bytes (limit: ${maxBytes})`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
39
|
+
if (buffer.byteLength > maxBytes) {
|
|
40
|
+
throw new Error(`File too large: ${buffer.byteLength} bytes (limit: ${maxBytes})`);
|
|
41
|
+
}
|
|
42
|
+
// Detect MIME type
|
|
43
|
+
const contentType = response.headers.get("content-type");
|
|
44
|
+
const mimeType = contentType?.split(";")[0]?.trim() || "application/octet-stream";
|
|
45
|
+
return { buffer, mimeType };
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
clearTimeout(timeout);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Infer placeholder text based on MIME type
|
|
53
|
+
*/
|
|
54
|
+
function inferPlaceholder(mimeType) {
|
|
55
|
+
if (mimeType.startsWith("image/")) {
|
|
56
|
+
return "<media:image>";
|
|
57
|
+
}
|
|
58
|
+
else if (mimeType.startsWith("video/")) {
|
|
59
|
+
return "<media:video>";
|
|
60
|
+
}
|
|
61
|
+
else if (mimeType.startsWith("audio/")) {
|
|
62
|
+
return "<media:audio>";
|
|
63
|
+
}
|
|
64
|
+
else if (mimeType === "application/pdf") {
|
|
65
|
+
return "<media:document>";
|
|
66
|
+
}
|
|
67
|
+
else if (mimeType.startsWith("text/")) {
|
|
68
|
+
return "<media:text>";
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
return "<media:document>";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Check if a MIME type is an image
|
|
76
|
+
*/
|
|
77
|
+
function isImageMimeType(mimeType) {
|
|
78
|
+
if (!mimeType)
|
|
79
|
+
return false;
|
|
80
|
+
const lower = mimeType.toLowerCase();
|
|
81
|
+
// Standard formats: image/jpeg, image/png, etc.
|
|
82
|
+
if (lower.startsWith("image/")) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
// Handle non-standard formats like "jpeg" instead of "image/jpeg"
|
|
86
|
+
// Extract subtype if format is "type/subtype", otherwise use whole string
|
|
87
|
+
const subtype = lower.includes("/") ? lower.split("/")[1] : lower;
|
|
88
|
+
const imageSubtypes = [
|
|
89
|
+
"jpeg", "jpg", "png", "gif", "webp", "bmp", "svg+xml", "svg"
|
|
90
|
+
];
|
|
91
|
+
return imageSubtypes.includes(subtype);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Check if a MIME type is a PDF
|
|
95
|
+
*/
|
|
96
|
+
function isPdfMimeType(mimeType) {
|
|
97
|
+
return mimeType?.toLowerCase() === "application/pdf" || false;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Check if a MIME type is text-based
|
|
101
|
+
*/
|
|
102
|
+
function isTextMimeType(mimeType) {
|
|
103
|
+
if (!mimeType)
|
|
104
|
+
return false;
|
|
105
|
+
const lower = mimeType.toLowerCase();
|
|
106
|
+
return (lower.startsWith("text/") ||
|
|
107
|
+
lower === "application/json" ||
|
|
108
|
+
lower === "application/xml");
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Download and save media file to local disk
|
|
112
|
+
* This is the key function that follows clawdbot-feishu's approach
|
|
113
|
+
*/
|
|
114
|
+
async function downloadAndSaveMedia(runtime, uri, mimeType, fileName, options) {
|
|
115
|
+
const maxBytes = options?.maxBytes ?? 30000000; // 30MB default
|
|
116
|
+
const timeoutMs = options?.timeoutMs ?? 60000; // 60 seconds default
|
|
117
|
+
console.log(`[XiaoYi Media] Downloading: ${fileName} (${mimeType}) from ${uri}`);
|
|
118
|
+
// Download the file
|
|
119
|
+
const { buffer, mimeType: detectedMimeType } = await fetchFromUrl(uri, maxBytes, timeoutMs);
|
|
120
|
+
// Use detected MIME type if provided type is generic
|
|
121
|
+
const finalMimeType = mimeType === "application/octet-stream" ? detectedMimeType : mimeType;
|
|
122
|
+
// Save to local disk using OpenClaw's core.media API
|
|
123
|
+
// This is the critical step - saves file locally and returns path
|
|
124
|
+
const saved = await runtime.channel.media.saveMediaBuffer(buffer, finalMimeType, "inbound", maxBytes, fileName);
|
|
125
|
+
console.log(`[XiaoYi Media] Saved to: ${saved.path}`);
|
|
126
|
+
return {
|
|
127
|
+
path: saved.path,
|
|
128
|
+
contentType: saved.contentType,
|
|
129
|
+
placeholder: inferPlaceholder(saved.contentType),
|
|
130
|
+
fileName,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Download and save multiple media files
|
|
135
|
+
*/
|
|
136
|
+
async function downloadAndSaveMediaList(runtime, files, options) {
|
|
137
|
+
const results = [];
|
|
138
|
+
for (const file of files) {
|
|
139
|
+
try {
|
|
140
|
+
const downloaded = await downloadAndSaveMedia(runtime, file.uri, file.mimeType, file.name, options);
|
|
141
|
+
results.push(downloaded);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
console.error(`[XiaoYi Media] Failed to download ${file.name}:`, error);
|
|
145
|
+
// Continue with other files
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return results;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Build media payload for inbound context
|
|
152
|
+
* Similar to clawdbot-feishu's buildFeishuMediaPayload()
|
|
153
|
+
*/
|
|
154
|
+
function buildXiaoYiMediaPayload(mediaList) {
|
|
155
|
+
if (mediaList.length === 0) {
|
|
156
|
+
return {};
|
|
157
|
+
}
|
|
158
|
+
const first = mediaList[0];
|
|
159
|
+
const mediaPaths = mediaList.map((media) => media.path);
|
|
160
|
+
const mediaTypes = mediaList.map((media) => media.contentType).filter(Boolean);
|
|
161
|
+
return {
|
|
162
|
+
MediaPath: first?.path,
|
|
163
|
+
MediaType: first?.contentType,
|
|
164
|
+
MediaUrl: first?.path,
|
|
165
|
+
MediaPaths: mediaPaths.length > 0 ? mediaPaths : undefined,
|
|
166
|
+
MediaUrls: mediaPaths.length > 0 ? mediaPaths : undefined,
|
|
167
|
+
MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Extract text from downloaded file for including in message body
|
|
172
|
+
*/
|
|
173
|
+
async function extractTextFromFile(path, mimeType) {
|
|
174
|
+
// For now, just return null - Agent can read file directly from path
|
|
175
|
+
// This could be enhanced to extract text from specific file types
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Extract image from URL and return base64 encoded data
|
|
180
|
+
*/
|
|
181
|
+
async function extractImageFromUrl(url, limits) {
|
|
182
|
+
const maxBytes = limits?.maxBytes ?? 10000000; // 10MB default
|
|
183
|
+
const timeoutMs = limits?.timeoutMs ?? 30000; // 30 seconds default
|
|
184
|
+
const { buffer, mimeType } = await fetchFromUrl(url, maxBytes, timeoutMs);
|
|
185
|
+
// Validate it's an image MIME type
|
|
186
|
+
if (!isImageMimeType(mimeType)) {
|
|
187
|
+
throw new Error(`Unsupported image type: ${mimeType}`);
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
type: "image",
|
|
191
|
+
data: buffer.toString("base64"),
|
|
192
|
+
mimeType,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Extract text content from URL
|
|
197
|
+
* Supports text-based files (txt, md, json, xml, csv, etc.)
|
|
198
|
+
*/
|
|
199
|
+
async function extractTextFromUrl(url, maxBytes = 5000000, timeoutMs = 30000) {
|
|
200
|
+
const { buffer, mimeType } = await fetchFromUrl(url, maxBytes, timeoutMs);
|
|
201
|
+
// Check if it's a text-based MIME type
|
|
202
|
+
const textMimes = [
|
|
203
|
+
"text/plain",
|
|
204
|
+
"text/markdown",
|
|
205
|
+
"text/html",
|
|
206
|
+
"text/csv",
|
|
207
|
+
"application/json",
|
|
208
|
+
"application/xml",
|
|
209
|
+
"text/xml",
|
|
210
|
+
];
|
|
211
|
+
const isTextFile = textMimes.some(tm => mimeType.startsWith(tm) || mimeType === tm);
|
|
212
|
+
if (!isTextFile) {
|
|
213
|
+
throw new Error(`Unsupported text type: ${mimeType}`);
|
|
214
|
+
}
|
|
215
|
+
return buffer.toString("utf-8");
|
|
216
|
+
}
|
package/dist/xy-bot.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { OpenClawConfig, RuntimeEnv } from "openclaw/dist/plugin-sdk/index.js";
|
|
2
|
+
type ClawdbotConfig = OpenClawConfig;
|
|
3
|
+
import type { A2AJsonRpcRequest } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Parameters for handling an XY message.
|
|
6
|
+
*/
|
|
7
|
+
export interface HandleXYMessageParams {
|
|
8
|
+
cfg: ClawdbotConfig;
|
|
9
|
+
runtime: RuntimeEnv;
|
|
10
|
+
message: A2AJsonRpcRequest;
|
|
11
|
+
accountId: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Handle an incoming A2A message.
|
|
15
|
+
* This is the main entry point for message processing.
|
|
16
|
+
* Runtime is expected to be validated before calling this function.
|
|
17
|
+
*/
|
|
18
|
+
export declare function handleXYMessage(params: HandleXYMessageParams): Promise<void>;
|
|
19
|
+
export {};
|
package/dist/xy-bot.js
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleXYMessage = handleXYMessage;
|
|
4
|
+
const runtime_js_1 = require("./runtime.js");
|
|
5
|
+
const xy_reply_dispatcher_js_1 = require("./xy-reply-dispatcher.js");
|
|
6
|
+
const xy_parser_js_1 = require("./xy-parser.js");
|
|
7
|
+
const file_download_js_1 = require("./file-download.js");
|
|
8
|
+
const xy_config_js_1 = require("./xy-config.js");
|
|
9
|
+
const xy_formatter_js_1 = require("./xy-formatter.js");
|
|
10
|
+
const session_manager_js_1 = require("./xy-tools/session-manager.js");
|
|
11
|
+
const config_manager_js_1 = require("./xy-utils/config-manager.js");
|
|
12
|
+
/**
|
|
13
|
+
* Handle an incoming A2A message.
|
|
14
|
+
* This is the main entry point for message processing.
|
|
15
|
+
* Runtime is expected to be validated before calling this function.
|
|
16
|
+
*/
|
|
17
|
+
async function handleXYMessage(params) {
|
|
18
|
+
const { cfg, runtime, message, accountId } = params;
|
|
19
|
+
const log = runtime?.log ?? console.log;
|
|
20
|
+
const error = runtime?.error ?? console.error;
|
|
21
|
+
// Get OpenClaw PluginRuntime (not XiaoYiRuntime)
|
|
22
|
+
const xiaoYiRuntime = (0, runtime_js_1.getXiaoYiRuntime)();
|
|
23
|
+
const core = xiaoYiRuntime.getPluginRuntime();
|
|
24
|
+
try {
|
|
25
|
+
// Check for special messages BEFORE parsing (these have different param structures)
|
|
26
|
+
const messageMethod = message.method;
|
|
27
|
+
log(`[BOT-ENTRY] <<<<<<< Received message with method: ${messageMethod}, id: ${message.id} >>>>>>>`);
|
|
28
|
+
log(`[BOT-ENTRY] Stack trace for debugging:`, new Error().stack?.split('\n').slice(1, 4).join('\n'));
|
|
29
|
+
// Handle clearContext messages (params only has sessionId)
|
|
30
|
+
if (messageMethod === "clearContext" || messageMethod === "clear_context") {
|
|
31
|
+
const sessionId = message.params?.sessionId;
|
|
32
|
+
if (!sessionId) {
|
|
33
|
+
throw new Error("clearContext request missing sessionId in params");
|
|
34
|
+
}
|
|
35
|
+
log(`Clear context request for session ${sessionId}`);
|
|
36
|
+
const config = (0, xy_config_js_1.resolveXYConfig)(cfg);
|
|
37
|
+
await (0, xy_formatter_js_1.sendClearContextResponse)({
|
|
38
|
+
config,
|
|
39
|
+
sessionId,
|
|
40
|
+
messageId: message.id,
|
|
41
|
+
});
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// Handle tasks/cancel messages
|
|
45
|
+
if (messageMethod === "tasks/cancel" || messageMethod === "tasks_cancel") {
|
|
46
|
+
const sessionId = message.params?.sessionId;
|
|
47
|
+
const taskId = message.params?.id || message.id;
|
|
48
|
+
if (!sessionId) {
|
|
49
|
+
throw new Error("tasks/cancel request missing sessionId in params");
|
|
50
|
+
}
|
|
51
|
+
log(`Tasks cancel request for session ${sessionId}, task ${taskId}`);
|
|
52
|
+
const config = (0, xy_config_js_1.resolveXYConfig)(cfg);
|
|
53
|
+
await (0, xy_formatter_js_1.sendTasksCancelResponse)({
|
|
54
|
+
config,
|
|
55
|
+
sessionId,
|
|
56
|
+
taskId,
|
|
57
|
+
messageId: message.id,
|
|
58
|
+
});
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// Parse the A2A message (for regular messages)
|
|
62
|
+
const parsed = (0, xy_parser_js_1.parseA2AMessage)(message);
|
|
63
|
+
// Extract and update push_id if present
|
|
64
|
+
const pushId = (0, xy_parser_js_1.extractPushId)(parsed.parts);
|
|
65
|
+
if (pushId) {
|
|
66
|
+
log(`[BOT] 📌 Extracted push_id from user message`);
|
|
67
|
+
log(`[BOT] - Session ID: ${parsed.sessionId}`);
|
|
68
|
+
log(`[BOT] - Push ID preview: ${pushId.substring(0, 20)}...`);
|
|
69
|
+
log(`[BOT] - Full push_id: ${pushId}`);
|
|
70
|
+
config_manager_js_1.configManager.updatePushId(parsed.sessionId, pushId);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
log(`[BOT] ℹ️ No push_id found in message, will use config default`);
|
|
74
|
+
}
|
|
75
|
+
// Resolve configuration (needed for status updates)
|
|
76
|
+
const config = (0, xy_config_js_1.resolveXYConfig)(cfg);
|
|
77
|
+
// ✅ Resolve agent route (following feishu pattern)
|
|
78
|
+
// accountId is "default" for XY (single account mode)
|
|
79
|
+
// Use sessionId as peer.id to ensure all messages in the same session share context
|
|
80
|
+
let route = core.channel.routing.resolveAgentRoute({
|
|
81
|
+
cfg,
|
|
82
|
+
channel: "xiaoyi-channel",
|
|
83
|
+
accountId, // "default"
|
|
84
|
+
peer: {
|
|
85
|
+
kind: "direct",
|
|
86
|
+
id: parsed.sessionId, // ✅ Use sessionId to share context within the same conversation session
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
log(`xy: resolved route accountId=${route.accountId}, sessionKey=${route.sessionKey}`);
|
|
90
|
+
// Register session context for tools
|
|
91
|
+
log(`[BOT] 📝 About to register session for tools...`);
|
|
92
|
+
log(`[BOT] - sessionKey: ${route.sessionKey}`);
|
|
93
|
+
log(`[BOT] - sessionId: ${parsed.sessionId}`);
|
|
94
|
+
log(`[BOT] - taskId: ${parsed.taskId}`);
|
|
95
|
+
(0, session_manager_js_1.registerSession)(route.sessionKey, {
|
|
96
|
+
config,
|
|
97
|
+
sessionId: parsed.sessionId,
|
|
98
|
+
taskId: parsed.taskId,
|
|
99
|
+
messageId: parsed.messageId,
|
|
100
|
+
agentId: route.accountId,
|
|
101
|
+
});
|
|
102
|
+
log(`[BOT] ✅ Session registered for tools`);
|
|
103
|
+
// Send initial status update immediately after parsing message
|
|
104
|
+
log(`[STATUS] Sending initial status update for session ${parsed.sessionId}`);
|
|
105
|
+
void (0, xy_formatter_js_1.sendStatusUpdate)({
|
|
106
|
+
config,
|
|
107
|
+
sessionId: parsed.sessionId,
|
|
108
|
+
taskId: parsed.taskId,
|
|
109
|
+
messageId: parsed.messageId,
|
|
110
|
+
text: "任务正在处理中,请稍后~",
|
|
111
|
+
state: "working",
|
|
112
|
+
}).catch((err) => {
|
|
113
|
+
error(`Failed to send initial status update:`, err);
|
|
114
|
+
});
|
|
115
|
+
// Extract text and files from parts
|
|
116
|
+
const text = (0, xy_parser_js_1.extractTextFromParts)(parsed.parts);
|
|
117
|
+
const fileParts = (0, xy_parser_js_1.extractFileParts)(parsed.parts);
|
|
118
|
+
// Download files if present (using core's media download)
|
|
119
|
+
const mediaList = await (0, file_download_js_1.downloadFilesFromParts)(fileParts);
|
|
120
|
+
// Build media payload for inbound context (following feishu pattern)
|
|
121
|
+
const mediaPayload = buildXYMediaPayload(mediaList);
|
|
122
|
+
// Resolve envelope format options (following feishu pattern)
|
|
123
|
+
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
|
|
124
|
+
// Build message body with speaker prefix (following feishu pattern)
|
|
125
|
+
let messageBody = text || "";
|
|
126
|
+
// Add speaker prefix for clarity
|
|
127
|
+
const speaker = parsed.sessionId;
|
|
128
|
+
messageBody = `${speaker}: ${messageBody}`;
|
|
129
|
+
// Format agent envelope (following feishu pattern)
|
|
130
|
+
const body = core.channel.reply.formatAgentEnvelope({
|
|
131
|
+
channel: "xiaoyi-channel",
|
|
132
|
+
from: speaker,
|
|
133
|
+
timestamp: new Date(),
|
|
134
|
+
envelope: envelopeOptions,
|
|
135
|
+
body: messageBody,
|
|
136
|
+
});
|
|
137
|
+
// ✅ Finalize inbound context (following feishu pattern)
|
|
138
|
+
// Use route.accountId and route.sessionKey instead of parsed fields
|
|
139
|
+
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
140
|
+
Body: body,
|
|
141
|
+
RawBody: text || "",
|
|
142
|
+
CommandBody: text || "",
|
|
143
|
+
From: parsed.sessionId,
|
|
144
|
+
To: parsed.sessionId, // ✅ Simplified: use sessionId as target (context is managed by SessionKey)
|
|
145
|
+
SessionKey: route.sessionKey, // ✅ Use route.sessionKey
|
|
146
|
+
AccountId: route.accountId, // ✅ Use route.accountId ("default")
|
|
147
|
+
ChatType: "direct",
|
|
148
|
+
GroupSubject: undefined,
|
|
149
|
+
SenderName: parsed.sessionId,
|
|
150
|
+
SenderId: parsed.sessionId,
|
|
151
|
+
Provider: "xiaoyi-channel",
|
|
152
|
+
Surface: "xiaoyi-channel",
|
|
153
|
+
MessageSid: parsed.messageId,
|
|
154
|
+
Timestamp: Date.now(),
|
|
155
|
+
WasMentioned: false,
|
|
156
|
+
CommandAuthorized: true,
|
|
157
|
+
OriginatingChannel: "xiaoyi-channel",
|
|
158
|
+
OriginatingTo: parsed.sessionId, // Original message target
|
|
159
|
+
ReplyToBody: undefined, // A2A protocol doesn't support reply/quote
|
|
160
|
+
...mediaPayload,
|
|
161
|
+
});
|
|
162
|
+
// Send initial status update immediately after parsing message
|
|
163
|
+
log(`[STATUS] Sending initial status update for session ${parsed.sessionId}`);
|
|
164
|
+
void (0, xy_formatter_js_1.sendStatusUpdate)({
|
|
165
|
+
config,
|
|
166
|
+
sessionId: parsed.sessionId,
|
|
167
|
+
taskId: parsed.taskId,
|
|
168
|
+
messageId: parsed.messageId,
|
|
169
|
+
text: "任务正在处理中,请稍后~",
|
|
170
|
+
state: "working",
|
|
171
|
+
}).catch((err) => {
|
|
172
|
+
error(`Failed to send initial status update:`, err);
|
|
173
|
+
});
|
|
174
|
+
// Create reply dispatcher (following feishu pattern)
|
|
175
|
+
log(`[BOT-DISPATCHER] 🎯 Creating reply dispatcher for session=${parsed.sessionId}, taskId=${parsed.taskId}, messageId=${parsed.messageId}`);
|
|
176
|
+
const { dispatcher, replyOptions, markDispatchIdle, startStatusInterval } = (0, xy_reply_dispatcher_js_1.createXYReplyDispatcher)({
|
|
177
|
+
cfg,
|
|
178
|
+
runtime,
|
|
179
|
+
sessionId: parsed.sessionId,
|
|
180
|
+
taskId: parsed.taskId,
|
|
181
|
+
messageId: parsed.messageId,
|
|
182
|
+
accountId: route.accountId, // ✅ Use route.accountId
|
|
183
|
+
});
|
|
184
|
+
log(`[BOT-DISPATCHER] ✅ Reply dispatcher created successfully`);
|
|
185
|
+
// Start status update interval (will send updates every 60 seconds)
|
|
186
|
+
// Interval will be automatically stopped when onIdle/onCleanup is triggered
|
|
187
|
+
startStatusInterval();
|
|
188
|
+
log(`xy: dispatching to agent (session=${parsed.sessionId})`);
|
|
189
|
+
// Dispatch to OpenClaw core using correct API (following feishu pattern)
|
|
190
|
+
log(`[BOT] 🚀 Starting dispatcher with session: ${route.sessionKey}`);
|
|
191
|
+
await core.channel.reply.withReplyDispatcher({
|
|
192
|
+
dispatcher,
|
|
193
|
+
onSettled: () => {
|
|
194
|
+
log(`[BOT] 🏁 onSettled called for session: ${route.sessionKey}`);
|
|
195
|
+
log(`[BOT] - About to unregister session...`);
|
|
196
|
+
markDispatchIdle();
|
|
197
|
+
// Unregister session context when done
|
|
198
|
+
(0, session_manager_js_1.unregisterSession)(route.sessionKey);
|
|
199
|
+
log(`[BOT] ✅ Session unregistered in onSettled`);
|
|
200
|
+
},
|
|
201
|
+
run: () => core.channel.reply.dispatchReplyFromConfig({
|
|
202
|
+
ctx: ctxPayload,
|
|
203
|
+
cfg,
|
|
204
|
+
dispatcher,
|
|
205
|
+
replyOptions,
|
|
206
|
+
}),
|
|
207
|
+
});
|
|
208
|
+
log(`[BOT] ✅ Dispatcher completed for session: ${parsed.sessionId}`);
|
|
209
|
+
log(`xy: dispatch complete (session=${parsed.sessionId})`);
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
// ✅ Only log error, don't re-throw to prevent gateway restart
|
|
213
|
+
error("Failed to handle XY message:", err);
|
|
214
|
+
runtime.error?.(`xy: Failed to handle message: ${String(err)}`);
|
|
215
|
+
log(`[BOT] ❌ Error occurred, attempting cleanup...`);
|
|
216
|
+
// Try to unregister session on error (if route was established)
|
|
217
|
+
try {
|
|
218
|
+
const xiaoYiRuntime = (0, runtime_js_1.getXiaoYiRuntime)();
|
|
219
|
+
const core = xiaoYiRuntime.getPluginRuntime();
|
|
220
|
+
const params = message.params;
|
|
221
|
+
const sessionId = params?.sessionId;
|
|
222
|
+
if (sessionId) {
|
|
223
|
+
log(`[BOT] 🧹 Cleaning up session after error: ${sessionId}`);
|
|
224
|
+
const route = core.channel.routing.resolveAgentRoute({
|
|
225
|
+
cfg,
|
|
226
|
+
channel: "xiaoyi-channel",
|
|
227
|
+
accountId,
|
|
228
|
+
peer: {
|
|
229
|
+
kind: "direct",
|
|
230
|
+
id: sessionId, // ✅ Use sessionId for cleanup consistency
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
log(`[BOT] - Unregistering session: ${route.sessionKey}`);
|
|
234
|
+
(0, session_manager_js_1.unregisterSession)(route.sessionKey);
|
|
235
|
+
log(`[BOT] ✅ Session unregistered after error`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
catch (cleanupErr) {
|
|
239
|
+
log(`[BOT] ⚠️ Cleanup failed:`, cleanupErr);
|
|
240
|
+
// Ignore cleanup errors
|
|
241
|
+
}
|
|
242
|
+
// ❌ Don't re-throw: message processing error should not affect gateway stability
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Build media payload for inbound context.
|
|
247
|
+
* Following feishu pattern: buildFeishuMediaPayload().
|
|
248
|
+
*/
|
|
249
|
+
function buildXYMediaPayload(mediaList) {
|
|
250
|
+
const first = mediaList[0];
|
|
251
|
+
const mediaPaths = mediaList.map((media) => media.path);
|
|
252
|
+
const mediaTypes = mediaList.map((media) => media.mimeType).filter(Boolean);
|
|
253
|
+
return {
|
|
254
|
+
MediaPath: first?.path,
|
|
255
|
+
MediaType: first?.mimeType,
|
|
256
|
+
MediaUrl: first?.path,
|
|
257
|
+
MediaPaths: mediaPaths.length > 0 ? mediaPaths : undefined,
|
|
258
|
+
MediaUrls: mediaPaths.length > 0 ? mediaPaths : undefined,
|
|
259
|
+
MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Infer OpenClaw media type from file type string.
|
|
264
|
+
*/
|
|
265
|
+
function inferMediaType(fileType) {
|
|
266
|
+
const lower = fileType.toLowerCase();
|
|
267
|
+
if (lower.includes("image") || /\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(lower)) {
|
|
268
|
+
return "image";
|
|
269
|
+
}
|
|
270
|
+
if (lower.includes("video") || /\.(mp4|avi|mov|mkv|webm)$/i.test(lower)) {
|
|
271
|
+
return "video";
|
|
272
|
+
}
|
|
273
|
+
if (lower.includes("audio") || /\.(mp3|wav|ogg|m4a)$/i.test(lower)) {
|
|
274
|
+
return "audio";
|
|
275
|
+
}
|
|
276
|
+
return "file";
|
|
277
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { XiaoYiWebSocketManager } from "./websocket.js";
|
|
2
|
+
import type { XiaoYiChannelConfig } from "./types.js";
|
|
3
|
+
import type { RuntimeEnv } from "openclaw/dist/plugin-sdk/index.js";
|
|
4
|
+
/**
|
|
5
|
+
* Set the runtime for logging in client module.
|
|
6
|
+
*/
|
|
7
|
+
export declare function setClientRuntime(rt: RuntimeEnv | undefined): void;
|
|
8
|
+
/**
|
|
9
|
+
* Get or create a WebSocket manager for the given configuration.
|
|
10
|
+
* Reuses existing managers if config matches.
|
|
11
|
+
* Adapted for xiaoyi - uses aksk instead of apiKey/uid
|
|
12
|
+
*/
|
|
13
|
+
export declare function getXYWebSocketManager(config: XiaoYiChannelConfig): XiaoYiWebSocketManager;
|
|
14
|
+
/**
|
|
15
|
+
* Remove a specific WebSocket manager from cache.
|
|
16
|
+
* Disconnects the manager and removes it from the cache.
|
|
17
|
+
*/
|
|
18
|
+
export declare function removeXYWebSocketManager(config: XiaoYiChannelConfig): void;
|
|
19
|
+
/**
|
|
20
|
+
* Clear all cached WebSocket managers.
|
|
21
|
+
*/
|
|
22
|
+
export declare function clearXYWebSocketManagers(): void;
|
|
23
|
+
/**
|
|
24
|
+
* Get the number of cached managers.
|
|
25
|
+
*/
|
|
26
|
+
export declare function getCachedManagerCount(): number;
|