mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-28 11:41:04 +00:00
153 lines
4.6 KiB
TypeScript
153 lines
4.6 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import * as fs from 'node:fs/promises';
|
|
import * as path from 'node:path';
|
|
import { createDebugLogger } from '@qwen-code/qwen-code-core';
|
|
|
|
const debugLogger = createDebugLogger('CLIPBOARD_UTILS');
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
type ClipboardModule = any;
|
|
|
|
let cachedClipboardModule: ClipboardModule | null = null;
|
|
let clipboardLoadAttempted = false;
|
|
|
|
async function getClipboardModule(): Promise<ClipboardModule | null> {
|
|
if (clipboardLoadAttempted) return cachedClipboardModule;
|
|
clipboardLoadAttempted = true;
|
|
|
|
try {
|
|
const modName = '@teddyzhu/clipboard';
|
|
cachedClipboardModule = await import(modName);
|
|
return cachedClipboardModule;
|
|
} catch (_e) {
|
|
debugLogger.error(
|
|
'Failed to load @teddyzhu/clipboard native module. Clipboard image features will be unavailable.',
|
|
);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if the system clipboard contains an image
|
|
* @returns true if clipboard contains an image
|
|
*/
|
|
export async function clipboardHasImage(): Promise<boolean> {
|
|
try {
|
|
const mod = await getClipboardModule();
|
|
if (!mod) return false;
|
|
const clipboard = new mod.ClipboardManager();
|
|
return clipboard.hasFormat('image');
|
|
} catch (error) {
|
|
debugLogger.error('Error checking clipboard for image:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Saves the image from clipboard to a temporary file
|
|
* @param targetDir The target directory to create temp files within
|
|
* @returns The path to the saved image file, or null if no image or error
|
|
*/
|
|
export async function saveClipboardImage(
|
|
targetDir?: string,
|
|
): Promise<string | null> {
|
|
try {
|
|
const mod = await getClipboardModule();
|
|
if (!mod) return null;
|
|
const clipboard = new mod.ClipboardManager();
|
|
|
|
if (!clipboard.hasFormat('image')) {
|
|
return null;
|
|
}
|
|
|
|
// Create a temporary directory for clipboard images within the target directory
|
|
// This avoids security restrictions on paths outside the target directory
|
|
const baseDir = targetDir || process.cwd();
|
|
const tempDir = path.join(baseDir, 'clipboard');
|
|
await fs.mkdir(tempDir, { recursive: true });
|
|
|
|
// Generate a unique filename with timestamp
|
|
const timestamp = new Date().getTime();
|
|
const tempFilePath = path.join(tempDir, `clipboard-${timestamp}.png`);
|
|
|
|
const imageData = clipboard.getImageData();
|
|
// Use data buffer from the API
|
|
const buffer = imageData.data;
|
|
|
|
if (!buffer) {
|
|
return null;
|
|
}
|
|
|
|
await fs.writeFile(tempFilePath, buffer);
|
|
|
|
return tempFilePath;
|
|
} catch (error) {
|
|
debugLogger.error('Error saving clipboard image:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cleans up old temporary clipboard image files using LRU strategy
|
|
* Keeps maximum 100 images, when exceeding removes 50 oldest files to reduce cleanup frequency
|
|
* @param targetDir The target directory where temp files are stored
|
|
*/
|
|
export async function cleanupOldClipboardImages(
|
|
targetDir?: string,
|
|
): Promise<void> {
|
|
try {
|
|
const baseDir = targetDir || process.cwd();
|
|
const tempDir = path.join(baseDir, 'clipboard');
|
|
const files = await fs.readdir(tempDir);
|
|
const MAX_IMAGES = 100;
|
|
const CLEANUP_COUNT = 50;
|
|
|
|
// Filter clipboard image files and get their stats
|
|
const imageFiles: Array<{ name: string; path: string; atime: number }> = [];
|
|
|
|
for (const file of files) {
|
|
if (
|
|
file.startsWith('clipboard-') &&
|
|
(file.endsWith('.png') ||
|
|
file.endsWith('.jpg') ||
|
|
file.endsWith('.webp') ||
|
|
file.endsWith('.heic') ||
|
|
file.endsWith('.heif') ||
|
|
file.endsWith('.tiff') ||
|
|
file.endsWith('.gif') ||
|
|
file.endsWith('.bmp'))
|
|
) {
|
|
const filePath = path.join(tempDir, file);
|
|
const stats = await fs.stat(filePath);
|
|
imageFiles.push({
|
|
name: file,
|
|
path: filePath,
|
|
atime: stats.atimeMs,
|
|
});
|
|
}
|
|
}
|
|
|
|
// If exceeds limit, remove CLEANUP_COUNT oldest files to reduce cleanup frequency
|
|
if (imageFiles.length > MAX_IMAGES) {
|
|
// Sort by access time (oldest first)
|
|
imageFiles.sort((a, b) => a.atime - b.atime);
|
|
|
|
// Remove CLEANUP_COUNT oldest files (or all excess files if less than CLEANUP_COUNT)
|
|
const removeCount = Math.min(
|
|
CLEANUP_COUNT,
|
|
imageFiles.length - MAX_IMAGES + CLEANUP_COUNT,
|
|
);
|
|
const filesToRemove = imageFiles.slice(0, removeCount);
|
|
for (const file of filesToRemove) {
|
|
await fs.unlink(file.path);
|
|
}
|
|
}
|
|
} catch {
|
|
// Ignore errors in cleanup
|
|
}
|
|
}
|