@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 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 (ignore kind: "data" as per user request)
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
- // TODO: Handle file parts if needed in the future
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynhcj/xiaoyi",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "XiaoYi channel plugin for OpenClaw",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",