mirror of
https://github.com/farcasclaudiu/openclaw.git
synced 2026-06-29 07:01:40 +03:00
chore: Enable "curly" rule to avoid single-statement if confusion/errors.
This commit is contained in:
+6
-2
@@ -11,8 +11,12 @@ export function isVoiceCompatibleAudio(opts: {
|
||||
return true;
|
||||
}
|
||||
const fileName = opts.fileName?.trim();
|
||||
if (!fileName) return false;
|
||||
if (!fileName) {
|
||||
return false;
|
||||
}
|
||||
const ext = getFileExtension(fileName);
|
||||
if (!ext) return false;
|
||||
if (!ext) {
|
||||
return false;
|
||||
}
|
||||
return VOICE_AUDIO_EXTENSIONS.has(ext);
|
||||
}
|
||||
|
||||
+18
-6
@@ -6,12 +6,24 @@ export const MAX_DOCUMENT_BYTES = 100 * 1024 * 1024; // 100MB
|
||||
export type MediaKind = "image" | "audio" | "video" | "document" | "unknown";
|
||||
|
||||
export function mediaKindFromMime(mime?: string | null): MediaKind {
|
||||
if (!mime) return "unknown";
|
||||
if (mime.startsWith("image/")) return "image";
|
||||
if (mime.startsWith("audio/")) return "audio";
|
||||
if (mime.startsWith("video/")) return "video";
|
||||
if (mime === "application/pdf") return "document";
|
||||
if (mime.startsWith("application/")) return "document";
|
||||
if (!mime) {
|
||||
return "unknown";
|
||||
}
|
||||
if (mime.startsWith("image/")) {
|
||||
return "image";
|
||||
}
|
||||
if (mime.startsWith("audio/")) {
|
||||
return "audio";
|
||||
}
|
||||
if (mime.startsWith("video/")) {
|
||||
return "video";
|
||||
}
|
||||
if (mime === "application/pdf") {
|
||||
return "document";
|
||||
}
|
||||
if (mime.startsWith("application/")) {
|
||||
return "document";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
|
||||
+24
-8
@@ -34,7 +34,9 @@ function stripQuotes(value: string): string {
|
||||
}
|
||||
|
||||
function parseContentDispositionFileName(header?: string | null): string | undefined {
|
||||
if (!header) return undefined;
|
||||
if (!header) {
|
||||
return undefined;
|
||||
}
|
||||
const starMatch = /filename\*\s*=\s*([^;]+)/i.exec(header);
|
||||
if (starMatch?.[1]) {
|
||||
const cleaned = stripQuotes(starMatch[1].trim());
|
||||
@@ -46,17 +48,25 @@ function parseContentDispositionFileName(header?: string | null): string | undef
|
||||
}
|
||||
}
|
||||
const match = /filename\s*=\s*([^;]+)/i.exec(header);
|
||||
if (match?.[1]) return path.basename(stripQuotes(match[1].trim()));
|
||||
if (match?.[1]) {
|
||||
return path.basename(stripQuotes(match[1].trim()));
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function readErrorBodySnippet(res: Response, maxChars = 200): Promise<string | undefined> {
|
||||
try {
|
||||
const text = await res.text();
|
||||
if (!text) return undefined;
|
||||
if (!text) {
|
||||
return undefined;
|
||||
}
|
||||
const collapsed = text.replace(/\s+/g, " ").trim();
|
||||
if (!collapsed) return undefined;
|
||||
if (collapsed.length <= maxChars) return collapsed;
|
||||
if (!collapsed) {
|
||||
return undefined;
|
||||
}
|
||||
if (collapsed.length <= maxChars) {
|
||||
return collapsed;
|
||||
}
|
||||
return `${collapsed.slice(0, maxChars)}…`;
|
||||
} catch {
|
||||
return undefined;
|
||||
@@ -85,7 +95,9 @@ export async function fetchRemoteMedia(options: FetchMediaOptions): Promise<Fetc
|
||||
detail = `HTTP ${res.status}${statusText}; empty response body`;
|
||||
} else {
|
||||
const snippet = await readErrorBodySnippet(res);
|
||||
if (snippet) detail += `; body: ${snippet}`;
|
||||
if (snippet) {
|
||||
detail += `; body: ${snippet}`;
|
||||
}
|
||||
}
|
||||
throw new MediaFetchError(
|
||||
"http_error",
|
||||
@@ -129,7 +141,9 @@ export async function fetchRemoteMedia(options: FetchMediaOptions): Promise<Fetc
|
||||
});
|
||||
if (fileName && !path.extname(fileName) && contentType) {
|
||||
const ext = extensionForMime(contentType);
|
||||
if (ext) fileName = `${fileName}${ext}`;
|
||||
if (ext) {
|
||||
fileName = `${fileName}${ext}`;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -158,7 +172,9 @@ async function readResponseWithLimit(res: Response, maxBytes: number): Promise<B
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
if (value?.length) {
|
||||
total += value.length;
|
||||
if (total > maxBytes) {
|
||||
|
||||
+3
-1
@@ -60,7 +60,9 @@ async function isPortFree(port: number) {
|
||||
await ensurePortAvailable(port);
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (err instanceof PortInUseError) return false;
|
||||
if (err instanceof PortInUseError) {
|
||||
return false;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
+24
-8
@@ -69,7 +69,9 @@ function readJpegExifOrientation(buffer: Buffer): number | null {
|
||||
buffer[exifStart + 5] === 0
|
||||
) {
|
||||
const tiffStart = exifStart + 6;
|
||||
if (buffer.length < tiffStart + 8) return null;
|
||||
if (buffer.length < tiffStart + 8) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check byte order (II = little-endian, MM = big-endian)
|
||||
const byteOrder = buffer.toString("ascii", tiffStart, tiffStart + 2);
|
||||
@@ -83,12 +85,16 @@ function readJpegExifOrientation(buffer: Buffer): number | null {
|
||||
// Read IFD0 offset
|
||||
const ifd0Offset = readU32(tiffStart + 4);
|
||||
const ifd0Start = tiffStart + ifd0Offset;
|
||||
if (buffer.length < ifd0Start + 2) return null;
|
||||
if (buffer.length < ifd0Start + 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const numEntries = readU16(ifd0Start);
|
||||
for (let i = 0; i < numEntries; i++) {
|
||||
const entryOffset = ifd0Start + 2 + i * 12;
|
||||
if (buffer.length < entryOffset + 12) break;
|
||||
if (buffer.length < entryOffset + 12) {
|
||||
break;
|
||||
}
|
||||
|
||||
const tag = readU16(entryOffset);
|
||||
// Orientation tag = 0x0112
|
||||
@@ -142,11 +148,17 @@ async function sipsMetadataFromBuffer(buffer: Buffer): Promise<ImageMetadata | n
|
||||
);
|
||||
const w = stdout.match(/pixelWidth:\s*([0-9]+)/);
|
||||
const h = stdout.match(/pixelHeight:\s*([0-9]+)/);
|
||||
if (!w?.[1] || !h?.[1]) return null;
|
||||
if (!w?.[1] || !h?.[1]) {
|
||||
return null;
|
||||
}
|
||||
const width = Number.parseInt(w[1], 10);
|
||||
const height = Number.parseInt(h[1], 10);
|
||||
if (!Number.isFinite(width) || !Number.isFinite(height)) return null;
|
||||
if (width <= 0 || height <= 0) return null;
|
||||
if (!Number.isFinite(width) || !Number.isFinite(height)) {
|
||||
return null;
|
||||
}
|
||||
if (width <= 0 || height <= 0) {
|
||||
return null;
|
||||
}
|
||||
return { width, height };
|
||||
});
|
||||
}
|
||||
@@ -204,8 +216,12 @@ export async function getImageMetadata(buffer: Buffer): Promise<ImageMetadata |
|
||||
const meta = await sharp(buffer).metadata();
|
||||
const width = Number(meta.width ?? 0);
|
||||
const height = Number(meta.height ?? 0);
|
||||
if (!Number.isFinite(width) || !Number.isFinite(height)) return null;
|
||||
if (width <= 0 || height <= 0) return null;
|
||||
if (!Number.isFinite(width) || !Number.isFinite(height)) {
|
||||
return null;
|
||||
}
|
||||
if (width <= 0 || height <= 0) {
|
||||
return null;
|
||||
}
|
||||
return { width, height };
|
||||
} catch {
|
||||
return null;
|
||||
|
||||
@@ -117,7 +117,9 @@ function isRedirectStatus(status: number): boolean {
|
||||
}
|
||||
|
||||
export function normalizeMimeType(value: string | undefined): string | undefined {
|
||||
if (!value) return undefined;
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
const [raw] = value.split(";");
|
||||
const normalized = raw?.trim().toLowerCase();
|
||||
return normalized || undefined;
|
||||
@@ -127,7 +129,9 @@ export function parseContentType(value: string | undefined): {
|
||||
mimeType?: string;
|
||||
charset?: string;
|
||||
} {
|
||||
if (!value) return {};
|
||||
if (!value) {
|
||||
return {};
|
||||
}
|
||||
const parts = value.split(";").map((part) => part.trim());
|
||||
const mimeType = normalizeMimeType(parts[0]);
|
||||
const charset = parts
|
||||
@@ -226,7 +230,9 @@ function decodeTextContent(buffer: Buffer, charset: string | undefined): string
|
||||
}
|
||||
|
||||
function clampText(text: string, maxChars: number): string {
|
||||
if (text.length <= maxChars) return text;
|
||||
if (text.length <= maxChars) {
|
||||
return text;
|
||||
}
|
||||
return text.slice(0, maxChars);
|
||||
}
|
||||
|
||||
@@ -250,7 +256,9 @@ async function extractPdfContent(params: {
|
||||
.map((item) => ("str" in item ? String(item.str) : ""))
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
if (pageText) textParts.push(pageText);
|
||||
if (pageText) {
|
||||
textParts.push(pageText);
|
||||
}
|
||||
}
|
||||
|
||||
const text = textParts.join("\n\n");
|
||||
|
||||
+39
-13
@@ -53,13 +53,17 @@ const AUDIO_FILE_EXTENSIONS = new Set([
|
||||
]);
|
||||
|
||||
function normalizeHeaderMime(mime?: string | null): string | undefined {
|
||||
if (!mime) return undefined;
|
||||
if (!mime) {
|
||||
return undefined;
|
||||
}
|
||||
const cleaned = mime.split(";")[0]?.trim().toLowerCase();
|
||||
return cleaned || undefined;
|
||||
}
|
||||
|
||||
async function sniffMime(buffer?: Buffer): Promise<string | undefined> {
|
||||
if (!buffer) return undefined;
|
||||
if (!buffer) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const type = await fileTypeFromBuffer(buffer);
|
||||
return type?.mime ?? undefined;
|
||||
@@ -69,7 +73,9 @@ async function sniffMime(buffer?: Buffer): Promise<string | undefined> {
|
||||
}
|
||||
|
||||
export function getFileExtension(filePath?: string | null): string | undefined {
|
||||
if (!filePath) return undefined;
|
||||
if (!filePath) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
if (/^https?:\/\//i.test(filePath)) {
|
||||
const url = new URL(filePath);
|
||||
@@ -84,7 +90,9 @@ export function getFileExtension(filePath?: string | null): string | undefined {
|
||||
|
||||
export function isAudioFileName(fileName?: string | null): boolean {
|
||||
const ext = getFileExtension(fileName);
|
||||
if (!ext) return false;
|
||||
if (!ext) {
|
||||
return false;
|
||||
}
|
||||
return AUDIO_FILE_EXTENSIONS.has(ext);
|
||||
}
|
||||
|
||||
@@ -97,7 +105,9 @@ export function detectMime(opts: {
|
||||
}
|
||||
|
||||
function isGenericMime(mime?: string): boolean {
|
||||
if (!mime) return true;
|
||||
if (!mime) {
|
||||
return true;
|
||||
}
|
||||
const m = mime.toLowerCase();
|
||||
return m === "application/octet-stream" || m === "application/zip";
|
||||
}
|
||||
@@ -115,17 +125,29 @@ async function detectMimeImpl(opts: {
|
||||
|
||||
// Prefer sniffed types, but don't let generic container types override a more
|
||||
// specific extension mapping (e.g. XLSX vs ZIP).
|
||||
if (sniffed && (!isGenericMime(sniffed) || !extMime)) return sniffed;
|
||||
if (extMime) return extMime;
|
||||
if (headerMime && !isGenericMime(headerMime)) return headerMime;
|
||||
if (sniffed) return sniffed;
|
||||
if (headerMime) return headerMime;
|
||||
if (sniffed && (!isGenericMime(sniffed) || !extMime)) {
|
||||
return sniffed;
|
||||
}
|
||||
if (extMime) {
|
||||
return extMime;
|
||||
}
|
||||
if (headerMime && !isGenericMime(headerMime)) {
|
||||
return headerMime;
|
||||
}
|
||||
if (sniffed) {
|
||||
return sniffed;
|
||||
}
|
||||
if (headerMime) {
|
||||
return headerMime;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function extensionForMime(mime?: string | null): string | undefined {
|
||||
if (!mime) return undefined;
|
||||
if (!mime) {
|
||||
return undefined;
|
||||
}
|
||||
return EXT_BY_MIME[mime.toLowerCase()];
|
||||
}
|
||||
|
||||
@@ -133,13 +155,17 @@ export function isGifMedia(opts: {
|
||||
contentType?: string | null;
|
||||
fileName?: string | null;
|
||||
}): boolean {
|
||||
if (opts.contentType?.toLowerCase() === "image/gif") return true;
|
||||
if (opts.contentType?.toLowerCase() === "image/gif") {
|
||||
return true;
|
||||
}
|
||||
const ext = getFileExtension(opts.fileName);
|
||||
return ext === ".gif";
|
||||
}
|
||||
|
||||
export function imageMimeFromFormat(format?: string | null): string | undefined {
|
||||
if (!format) return undefined;
|
||||
if (!format) {
|
||||
return undefined;
|
||||
}
|
||||
switch (format.toLowerCase()) {
|
||||
case "jpg":
|
||||
case "jpeg":
|
||||
|
||||
+27
-9
@@ -15,10 +15,18 @@ function cleanCandidate(raw: string) {
|
||||
}
|
||||
|
||||
function isValidMedia(candidate: string, opts?: { allowSpaces?: boolean }) {
|
||||
if (!candidate) return false;
|
||||
if (candidate.length > 4096) return false;
|
||||
if (!opts?.allowSpaces && /\s/.test(candidate)) return false;
|
||||
if (/^https?:\/\//i.test(candidate)) return true;
|
||||
if (!candidate) {
|
||||
return false;
|
||||
}
|
||||
if (candidate.length > 4096) {
|
||||
return false;
|
||||
}
|
||||
if (!opts?.allowSpaces && /\s/.test(candidate)) {
|
||||
return false;
|
||||
}
|
||||
if (/^https?:\/\//i.test(candidate)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Local paths: only allow safe relative paths starting with ./ that do not traverse upwards.
|
||||
return candidate.startsWith("./") && !candidate.includes("..");
|
||||
@@ -26,11 +34,17 @@ function isValidMedia(candidate: string, opts?: { allowSpaces?: boolean }) {
|
||||
|
||||
function unwrapQuoted(value: string): string | undefined {
|
||||
const trimmed = value.trim();
|
||||
if (trimmed.length < 2) return undefined;
|
||||
if (trimmed.length < 2) {
|
||||
return undefined;
|
||||
}
|
||||
const first = trimmed[0];
|
||||
const last = trimmed[trimmed.length - 1];
|
||||
if (first !== last) return undefined;
|
||||
if (first !== `"` && first !== "'" && first !== "`") return undefined;
|
||||
if (first !== last) {
|
||||
return undefined;
|
||||
}
|
||||
if (first !== `"` && first !== "'" && first !== "`") {
|
||||
return undefined;
|
||||
}
|
||||
return trimmed.slice(1, -1).trim();
|
||||
}
|
||||
|
||||
@@ -48,7 +62,9 @@ export function splitMediaFromOutput(raw: string): {
|
||||
// KNOWN: Leading whitespace is semantically meaningful in Markdown (lists, indented fences).
|
||||
// We only trim the end; token cleanup below handles removing `MEDIA:` lines.
|
||||
const trimmedRaw = raw.trimEnd();
|
||||
if (!trimmedRaw.trim()) return { text: "" };
|
||||
if (!trimmedRaw.trim()) {
|
||||
return { text: "" };
|
||||
}
|
||||
|
||||
const media: string[] = [];
|
||||
let foundMediaToken = false;
|
||||
@@ -189,7 +205,9 @@ export function splitMediaFromOutput(raw: string): {
|
||||
// Return cleaned text if we found a media token OR audio tag, otherwise original
|
||||
text: foundMediaToken || hasAudioAsVoice ? cleanedText : trimmedRaw,
|
||||
};
|
||||
if (hasAudioAsVoice) result.audioAsVoice = true;
|
||||
if (hasAudioAsVoice) {
|
||||
result.audioAsVoice = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
+12
-4
@@ -13,9 +13,15 @@ const MEDIA_ID_PATTERN = /^[\p{L}\p{N}._-]+$/u;
|
||||
const MAX_MEDIA_BYTES = MEDIA_MAX_BYTES;
|
||||
|
||||
const isValidMediaId = (id: string) => {
|
||||
if (!id) return false;
|
||||
if (id.length > MAX_MEDIA_ID_CHARS) return false;
|
||||
if (id === "." || id === "..") return false;
|
||||
if (!id) {
|
||||
return false;
|
||||
}
|
||||
if (id.length > MAX_MEDIA_ID_CHARS) {
|
||||
return false;
|
||||
}
|
||||
if (id === "." || id === "..") {
|
||||
return false;
|
||||
}
|
||||
return MEDIA_ID_PATTERN.test(id);
|
||||
};
|
||||
|
||||
@@ -51,7 +57,9 @@ export function attachMediaRoutes(
|
||||
const data = await handle.readFile();
|
||||
await handle.close().catch(() => {});
|
||||
const mime = await detectMime({ buffer: data, filePath: realPath });
|
||||
if (mime) res.type(mime);
|
||||
if (mime) {
|
||||
res.type(mime);
|
||||
}
|
||||
res.send(data);
|
||||
// best-effort single-use cleanup after response ends
|
||||
res.on("finish", () => {
|
||||
|
||||
@@ -47,7 +47,9 @@ describe("media store redirects", () => {
|
||||
const res = new PassThrough();
|
||||
const req = {
|
||||
on: (event: string, handler: (...args: unknown[]) => void) => {
|
||||
if (event === "error") res.on("error", handler);
|
||||
if (event === "error") {
|
||||
res.on("error", handler);
|
||||
}
|
||||
return req;
|
||||
},
|
||||
end: () => undefined,
|
||||
@@ -88,7 +90,9 @@ describe("media store redirects", () => {
|
||||
const res = new PassThrough();
|
||||
const req = {
|
||||
on: (event: string, handler: (...args: unknown[]) => void) => {
|
||||
if (event === "error") res.on("error", handler);
|
||||
if (event === "error") {
|
||||
res.on("error", handler);
|
||||
}
|
||||
return req;
|
||||
},
|
||||
end: () => undefined,
|
||||
|
||||
@@ -20,8 +20,11 @@ describe("media store", () => {
|
||||
|
||||
const restoreEnv = () => {
|
||||
for (const [key, value] of Object.entries(envSnapshot)) {
|
||||
if (value === undefined) delete process.env[key];
|
||||
else process.env[key] = value;
|
||||
if (value === undefined) {
|
||||
delete process.env[key];
|
||||
} else {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+9
-3
@@ -21,7 +21,9 @@ const DEFAULT_TTL_MS = 2 * 60 * 1000; // 2 minutes
|
||||
*/
|
||||
function sanitizeFilename(name: string): string {
|
||||
const trimmed = name.trim();
|
||||
if (!trimmed) return "";
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
const sanitized = trimmed.replace(/[^\p{L}\p{N}._-]+/gu, "_");
|
||||
// Collapse multiple underscores, trim leading/trailing, limit length
|
||||
return sanitized.replace(/_+/g, "_").replace(/^_|_$/g, "").slice(0, 60);
|
||||
@@ -34,7 +36,9 @@ function sanitizeFilename(name: string): string {
|
||||
*/
|
||||
export function extractOriginalFilename(filePath: string): string {
|
||||
const basename = path.basename(filePath);
|
||||
if (!basename) return "file.bin"; // Fallback for empty input
|
||||
if (!basename) {
|
||||
return "file.bin";
|
||||
} // Fallback for empty input
|
||||
|
||||
const ext = path.extname(basename);
|
||||
const nameWithoutExt = path.basename(basename, ext);
|
||||
@@ -68,7 +72,9 @@ export async function cleanOldMedia(ttlMs = DEFAULT_TTL_MS) {
|
||||
entries.map(async (file) => {
|
||||
const full = path.join(mediaDir, file);
|
||||
const stat = await fs.stat(full).catch(() => null);
|
||||
if (!stat) return;
|
||||
if (!stat) {
|
||||
return;
|
||||
}
|
||||
if (now - stat.mtimeMs > ttlMs) {
|
||||
await fs.rm(full).catch(() => {});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user