openclaw-home-pc/openclaw/extensions/xiaoyi/dist/xiaoyi-media.js
2026-03-24 04:00:48 +08:00

217 lines
7.5 KiB
JavaScript

"use strict";
/**
* XiaoYi Media Handler - Downloads and saves media files locally
* Similar to clawdbot-feishu's media.ts approach
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.isImageMimeType = isImageMimeType;
exports.isPdfMimeType = isPdfMimeType;
exports.isTextMimeType = isTextMimeType;
exports.downloadAndSaveMedia = downloadAndSaveMedia;
exports.downloadAndSaveMediaList = downloadAndSaveMediaList;
exports.buildXiaoYiMediaPayload = buildXiaoYiMediaPayload;
exports.extractTextFromFile = extractTextFromFile;
exports.extractImageFromUrl = extractImageFromUrl;
exports.extractTextFromUrl = extractTextFromUrl;
/**
* Download content from URL with validation
*/
async function fetchFromUrl(url, maxBytes, timeoutMs) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, {
signal: controller.signal,
headers: { "User-Agent": "XiaoYi-Channel/1.0" },
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
// Check content-length header if available
const contentLength = response.headers.get("content-length");
if (contentLength) {
const size = parseInt(contentLength, 10);
if (size > maxBytes) {
throw new Error(`File too large: ${size} bytes (limit: ${maxBytes})`);
}
}
const buffer = Buffer.from(await response.arrayBuffer());
if (buffer.byteLength > maxBytes) {
throw new Error(`File too large: ${buffer.byteLength} bytes (limit: ${maxBytes})`);
}
// Detect MIME type
const contentType = response.headers.get("content-type");
const mimeType = contentType?.split(";")[0]?.trim() || "application/octet-stream";
return { buffer, mimeType };
}
finally {
clearTimeout(timeout);
}
}
/**
* Infer placeholder text based on MIME type
*/
function inferPlaceholder(mimeType) {
if (mimeType.startsWith("image/")) {
return "<media:image>";
}
else if (mimeType.startsWith("video/")) {
return "<media:video>";
}
else if (mimeType.startsWith("audio/")) {
return "<media:audio>";
}
else if (mimeType === "application/pdf") {
return "<media:document>";
}
else if (mimeType.startsWith("text/")) {
return "<media:text>";
}
else {
return "<media:document>";
}
}
/**
* Check if a MIME type is an image
*/
function isImageMimeType(mimeType) {
if (!mimeType)
return false;
const lower = mimeType.toLowerCase();
// Standard formats: image/jpeg, image/png, etc.
if (lower.startsWith("image/")) {
return true;
}
// Handle non-standard formats like "jpeg" instead of "image/jpeg"
// Extract subtype if format is "type/subtype", otherwise use whole string
const subtype = lower.includes("/") ? lower.split("/")[1] : lower;
const imageSubtypes = [
"jpeg", "jpg", "png", "gif", "webp", "bmp", "svg+xml", "svg"
];
return imageSubtypes.includes(subtype);
}
/**
* Check if a MIME type is a PDF
*/
function isPdfMimeType(mimeType) {
return mimeType?.toLowerCase() === "application/pdf" || false;
}
/**
* Check if a MIME type is text-based
*/
function isTextMimeType(mimeType) {
if (!mimeType)
return false;
const lower = mimeType.toLowerCase();
return (lower.startsWith("text/") ||
lower === "application/json" ||
lower === "application/xml");
}
/**
* Download and save media file to local disk
* This is the key function that follows clawdbot-feishu's approach
*/
async function downloadAndSaveMedia(runtime, uri, mimeType, fileName, options) {
const maxBytes = options?.maxBytes ?? 30000000; // 30MB default
const timeoutMs = options?.timeoutMs ?? 60000; // 60 seconds default
console.log(`[XiaoYi Media] Downloading: ${fileName} (${mimeType}) from ${uri}`);
// Download the file
const { buffer, mimeType: detectedMimeType } = await fetchFromUrl(uri, maxBytes, timeoutMs);
// Use detected MIME type if provided type is generic
const finalMimeType = mimeType === "application/octet-stream" ? detectedMimeType : mimeType;
// Save to local disk using OpenClaw's core.media API
// This is the critical step - saves file locally and returns path
const saved = await runtime.channel.media.saveMediaBuffer(buffer, finalMimeType, "inbound", maxBytes, fileName);
console.log(`[XiaoYi Media] Saved to: ${saved.path}`);
return {
path: saved.path,
contentType: saved.contentType,
placeholder: inferPlaceholder(saved.contentType),
fileName,
};
}
/**
* Download and save multiple media files
*/
async function downloadAndSaveMediaList(runtime, files, options) {
const results = [];
for (const file of files) {
try {
const downloaded = await downloadAndSaveMedia(runtime, file.uri, file.mimeType, file.name, options);
results.push(downloaded);
}
catch (error) {
console.error(`[XiaoYi Media] Failed to download ${file.name}:`, error);
// Continue with other files
}
}
return results;
}
/**
* Build media payload for inbound context
* Similar to clawdbot-feishu's buildFeishuMediaPayload()
*/
function buildXiaoYiMediaPayload(mediaList) {
if (mediaList.length === 0) {
return {};
}
const first = mediaList[0];
const mediaPaths = mediaList.map((media) => media.path);
const mediaTypes = mediaList.map((media) => media.contentType).filter(Boolean);
return {
MediaPath: first?.path,
MediaType: first?.contentType,
MediaUrl: first?.path,
MediaPaths: mediaPaths.length > 0 ? mediaPaths : undefined,
MediaUrls: mediaPaths.length > 0 ? mediaPaths : undefined,
MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined,
};
}
/**
* Extract text from downloaded file for including in message body
*/
async function extractTextFromFile(path, mimeType) {
// For now, just return null - Agent can read file directly from path
// This could be enhanced to extract text from specific file types
return null;
}
/**
* Extract image from URL and return base64 encoded data
*/
async function extractImageFromUrl(url, limits) {
const maxBytes = limits?.maxBytes ?? 10000000; // 10MB default
const timeoutMs = limits?.timeoutMs ?? 30000; // 30 seconds default
const { buffer, mimeType } = await fetchFromUrl(url, maxBytes, timeoutMs);
// Validate it's an image MIME type
if (!isImageMimeType(mimeType)) {
throw new Error(`Unsupported image type: ${mimeType}`);
}
return {
type: "image",
data: buffer.toString("base64"),
mimeType,
};
}
/**
* Extract text content from URL
* Supports text-based files (txt, md, json, xml, csv, etc.)
*/
async function extractTextFromUrl(url, maxBytes = 5000000, timeoutMs = 30000) {
const { buffer, mimeType } = await fetchFromUrl(url, maxBytes, timeoutMs);
// Check if it's a text-based MIME type
const textMimes = [
"text/plain",
"text/markdown",
"text/html",
"text/csv",
"application/json",
"application/xml",
"text/xml",
];
const isTextFile = textMimes.some(tm => mimeType.startsWith(tm) || mimeType === tm);
if (!isTextFile) {
throw new Error(`Unsupported text type: ${mimeType}`);
}
return buffer.toString("utf-8");
}