@ynhcj/xiaoyi 2.2.0 → 2.2.1
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/channel.js +59 -2
- package/dist/file-handler.d.ts +36 -0
- package/dist/file-handler.js +113 -0
- package/package.json +1 -1
package/dist/channel.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.xiaoyiPlugin = void 0;
|
|
4
4
|
const runtime_1 = require("./runtime");
|
|
5
|
+
const file_handler_1 = require("./file-handler");
|
|
5
6
|
/**
|
|
6
7
|
* XiaoYi Channel Plugin
|
|
7
8
|
* Implements OpenClaw ChannelPlugin interface for XiaoYi A2A protocol
|
|
@@ -219,13 +220,68 @@ exports.xiaoyiPlugin = {
|
|
|
219
220
|
console.error("PluginRuntime not available");
|
|
220
221
|
return;
|
|
221
222
|
}
|
|
222
|
-
// Extract text content from parts array
|
|
223
|
+
// Extract text, file, and image content from parts array
|
|
223
224
|
let bodyText = "";
|
|
225
|
+
let images = [];
|
|
226
|
+
let fileAttachments = [];
|
|
224
227
|
for (const part of message.params.message.parts) {
|
|
225
228
|
if (part.kind === "text" && part.text) {
|
|
229
|
+
// Handle text content
|
|
226
230
|
bodyText += part.text;
|
|
227
231
|
}
|
|
228
|
-
|
|
232
|
+
else if (part.kind === "file" && part.file) {
|
|
233
|
+
// Handle file content
|
|
234
|
+
const { uri, mimeType, name } = part.file;
|
|
235
|
+
if (!uri) {
|
|
236
|
+
console.warn(`XiaoYi: File part without URI, skipping: ${name}`);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
// Handle image files
|
|
241
|
+
if ((0, file_handler_1.isImageMimeType)(mimeType)) {
|
|
242
|
+
console.log(`XiaoYi: Processing image file: ${name} (${mimeType})`);
|
|
243
|
+
const imageContent = await (0, file_handler_1.extractImageFromUrl)(uri, {
|
|
244
|
+
maxBytes: 10000000, // 10MB
|
|
245
|
+
timeoutMs: 30000, // 30 seconds
|
|
246
|
+
});
|
|
247
|
+
images.push(imageContent);
|
|
248
|
+
fileAttachments.push(`[图片: ${name}]`);
|
|
249
|
+
console.log(`XiaoYi: Successfully processed image: ${name}`);
|
|
250
|
+
}
|
|
251
|
+
// Handle PDF files - extract as text for now
|
|
252
|
+
else if ((0, file_handler_1.isPdfMimeType)(mimeType)) {
|
|
253
|
+
console.log(`XiaoYi: Processing PDF file: ${name}`);
|
|
254
|
+
// Note: PDF text extraction requires pdfjs-dist, for now just add a placeholder
|
|
255
|
+
fileAttachments.push(`[PDF文件: ${name} - PDF内容提取需要额外配置]`);
|
|
256
|
+
console.log(`XiaoYi: PDF file noted: ${name} (text extraction requires pdfjs-dist)`);
|
|
257
|
+
}
|
|
258
|
+
// Handle text-based files
|
|
259
|
+
else if ((0, file_handler_1.isTextMimeType)(mimeType)) {
|
|
260
|
+
console.log(`XiaoYi: Processing text file: ${name} (${mimeType})`);
|
|
261
|
+
const textContent = await (0, file_handler_1.extractTextFromUrl)(uri, 5000000, 30000);
|
|
262
|
+
bodyText += `\n\n[文件内容: ${name}]\n${textContent}`;
|
|
263
|
+
fileAttachments.push(`[文件: ${name}]`);
|
|
264
|
+
console.log(`XiaoYi: Successfully processed text file: ${name}`);
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
console.warn(`XiaoYi: Unsupported file type: ${mimeType}, name: ${name}`);
|
|
268
|
+
fileAttachments.push(`[不支持的文件类型: ${name} (${mimeType})]`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
273
|
+
console.error(`XiaoYi: Failed to process file ${name}: ${errorMsg}`);
|
|
274
|
+
fileAttachments.push(`[文件处理失败: ${name} - ${errorMsg}]`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// Ignore kind: "data" as per user request
|
|
278
|
+
}
|
|
279
|
+
// Log summary of processed attachments
|
|
280
|
+
if (fileAttachments.length > 0) {
|
|
281
|
+
console.log(`XiaoYi: Processed ${fileAttachments.length} file(s): ${fileAttachments.join(", ")}`);
|
|
282
|
+
}
|
|
283
|
+
if (images.length > 0) {
|
|
284
|
+
console.log(`XiaoYi: Total ${images.length} image(s) extracted for AI processing`);
|
|
229
285
|
}
|
|
230
286
|
// Determine sender ID from role
|
|
231
287
|
const senderId = message.params.message.role === "user" ? "user" : message.agentId;
|
|
@@ -357,6 +413,7 @@ exports.xiaoyiPlugin = {
|
|
|
357
413
|
}
|
|
358
414
|
},
|
|
359
415
|
} : undefined, // No replyOptions when streaming is disabled
|
|
416
|
+
images: images.length > 0 ? images : undefined, // Pass images to AI processing
|
|
360
417
|
});
|
|
361
418
|
}
|
|
362
419
|
catch (error) {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple file and image handler for XiaoYi Channel
|
|
3
|
+
* Handles downloading and extracting content from URIs
|
|
4
|
+
*/
|
|
5
|
+
export interface InputImageContent {
|
|
6
|
+
type: "image";
|
|
7
|
+
data: string;
|
|
8
|
+
mimeType: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ImageLimits {
|
|
11
|
+
allowUrl: boolean;
|
|
12
|
+
allowedMimes: Set<string>;
|
|
13
|
+
maxBytes: number;
|
|
14
|
+
maxRedirects: number;
|
|
15
|
+
timeoutMs: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Extract image content from URL
|
|
19
|
+
*/
|
|
20
|
+
export declare function extractImageFromUrl(url: string, limits?: Partial<ImageLimits>): Promise<InputImageContent>;
|
|
21
|
+
/**
|
|
22
|
+
* Extract text content from URL (for text-based files)
|
|
23
|
+
*/
|
|
24
|
+
export declare function extractTextFromUrl(url: string, maxBytes?: number, timeoutMs?: number): Promise<string>;
|
|
25
|
+
/**
|
|
26
|
+
* Check if a MIME type is an image
|
|
27
|
+
*/
|
|
28
|
+
export declare function isImageMimeType(mimeType: string | undefined): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Check if a MIME type is a PDF
|
|
31
|
+
*/
|
|
32
|
+
export declare function isPdfMimeType(mimeType: string | undefined): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Check if a MIME type is text-based
|
|
35
|
+
*/
|
|
36
|
+
export declare function isTextMimeType(mimeType: string | undefined): boolean;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Simple file and image handler for XiaoYi Channel
|
|
4
|
+
* Handles downloading and extracting content from URIs
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.extractImageFromUrl = extractImageFromUrl;
|
|
8
|
+
exports.extractTextFromUrl = extractTextFromUrl;
|
|
9
|
+
exports.isImageMimeType = isImageMimeType;
|
|
10
|
+
exports.isPdfMimeType = isPdfMimeType;
|
|
11
|
+
exports.isTextMimeType = isTextMimeType;
|
|
12
|
+
// Default limits
|
|
13
|
+
const DEFAULT_IMAGE_MIMES = new Set(["image/jpeg", "image/png", "image/gif", "image/webp"]);
|
|
14
|
+
const DEFAULT_MAX_BYTES = 10000000; // 10MB
|
|
15
|
+
const DEFAULT_TIMEOUT = 30000; // 30 seconds
|
|
16
|
+
const DEFAULT_MAX_REDIRECTS = 3;
|
|
17
|
+
/**
|
|
18
|
+
* Fetch content from URL with basic validation
|
|
19
|
+
*/
|
|
20
|
+
async function fetchFromUrl(url, maxBytes, timeoutMs) {
|
|
21
|
+
const controller = new AbortController();
|
|
22
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
23
|
+
try {
|
|
24
|
+
const response = await fetch(url, {
|
|
25
|
+
signal: controller.signal,
|
|
26
|
+
headers: { "User-Agent": "XiaoYi-Channel/1.0" },
|
|
27
|
+
});
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
30
|
+
}
|
|
31
|
+
// Check content-length header if available
|
|
32
|
+
const contentLength = response.headers.get("content-length");
|
|
33
|
+
if (contentLength) {
|
|
34
|
+
const size = parseInt(contentLength, 10);
|
|
35
|
+
if (size > maxBytes) {
|
|
36
|
+
throw new Error(`File too large: ${size} bytes (limit: ${maxBytes})`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
40
|
+
if (buffer.byteLength > maxBytes) {
|
|
41
|
+
throw new Error(`File too large: ${buffer.byteLength} bytes (limit: ${maxBytes})`);
|
|
42
|
+
}
|
|
43
|
+
// Detect MIME type
|
|
44
|
+
const contentType = response.headers.get("content-type");
|
|
45
|
+
const mimeType = contentType?.split(";")[0]?.trim() || "application/octet-stream";
|
|
46
|
+
return { buffer, mimeType };
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
clearTimeout(timeout);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Extract image content from URL
|
|
54
|
+
*/
|
|
55
|
+
async function extractImageFromUrl(url, limits) {
|
|
56
|
+
const finalLimits = {
|
|
57
|
+
allowUrl: limits?.allowUrl ?? true,
|
|
58
|
+
allowedMimes: limits?.allowedMimes ?? DEFAULT_IMAGE_MIMES,
|
|
59
|
+
maxBytes: limits?.maxBytes ?? DEFAULT_MAX_BYTES,
|
|
60
|
+
maxRedirects: limits?.maxRedirects ?? DEFAULT_MAX_REDIRECTS,
|
|
61
|
+
timeoutMs: limits?.timeoutMs ?? DEFAULT_TIMEOUT,
|
|
62
|
+
};
|
|
63
|
+
if (!finalLimits.allowUrl) {
|
|
64
|
+
throw new Error("URL sources are disabled");
|
|
65
|
+
}
|
|
66
|
+
const { buffer, mimeType } = await fetchFromUrl(url, finalLimits.maxBytes, finalLimits.timeoutMs);
|
|
67
|
+
if (!finalLimits.allowedMimes.has(mimeType)) {
|
|
68
|
+
throw new Error(`Unsupported image type: ${mimeType}`);
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
type: "image",
|
|
72
|
+
data: buffer.toString("base64"),
|
|
73
|
+
mimeType,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Extract text content from URL (for text-based files)
|
|
78
|
+
*/
|
|
79
|
+
async function extractTextFromUrl(url, maxBytes = 5000000, timeoutMs = 30000) {
|
|
80
|
+
const { buffer, mimeType } = await fetchFromUrl(url, maxBytes, timeoutMs);
|
|
81
|
+
// Only process text-based MIME types
|
|
82
|
+
const textMimes = ["text/plain", "text/markdown", "text/html", "text/csv", "application/json", "application/xml"];
|
|
83
|
+
if (!textMimes.some((tm) => mimeType.startsWith(tm) || mimeType === tm)) {
|
|
84
|
+
throw new Error(`Unsupported text type: ${mimeType}`);
|
|
85
|
+
}
|
|
86
|
+
// Try to decode as UTF-8
|
|
87
|
+
return buffer.toString("utf-8");
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Check if a MIME type is an image
|
|
91
|
+
*/
|
|
92
|
+
function isImageMimeType(mimeType) {
|
|
93
|
+
if (!mimeType)
|
|
94
|
+
return false;
|
|
95
|
+
return DEFAULT_IMAGE_MIMES.has(mimeType.toLowerCase());
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Check if a MIME type is a PDF
|
|
99
|
+
*/
|
|
100
|
+
function isPdfMimeType(mimeType) {
|
|
101
|
+
return mimeType?.toLowerCase() === "application/pdf" || false;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Check if a MIME type is text-based
|
|
105
|
+
*/
|
|
106
|
+
function isTextMimeType(mimeType) {
|
|
107
|
+
if (!mimeType)
|
|
108
|
+
return false;
|
|
109
|
+
const lower = mimeType.toLowerCase();
|
|
110
|
+
return (lower.startsWith("text/") ||
|
|
111
|
+
lower === "application/json" ||
|
|
112
|
+
lower === "application/xml");
|
|
113
|
+
}
|