| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272 |
- /**
- * Func 标签:image-area-cropping
- *
- * 约定:src/pages/processing/func/ 目录下每个文件名就是一个"可用标签/能力"。
- * 本文件用于声明该标签存在(供文档/提示词/后续动态加载使用)。
- *
- * 语义:根据区域坐标裁剪当前截图(ScreenShot.jpg)的指定区域,并保存到指定路径。
- */
- import ScrcpyConfig from '../../screenshot/scrcpy-config.js';
- export const tagName = 'image-area-cropping';
- export const schema = {
- description: '根据区域坐标裁剪当前截图(ScreenShot.jpg)的指定区域,并保存到指定路径。',
- inputs: {
- area: '区域坐标(JSON字符串格式,包含 topLeft 和 bottomRight,或包含 x, y, width, height)',
- savePath: '保存路径(相对于工作流目录或绝对路径)',
- },
- outputs: {
- result: '保存结果(成功返回 "1",失败返回 "0")',
- },
- };
- /**
- * 执行 image-area-cropping 功能
- * 这个函数会被 ActionParser 调用
- *
- * @param {Object} params - 参数对象
- * @param {string} params.area - 区域坐标(JSON字符串或对象,格式:{topLeft: {x, y}, bottomRight: {x, y}} 或 {x, y, width, height})
- * @param {string} params.savePath - 保存路径
- * @param {string} params.folderPath - 工作流文件夹路径
- * @param {string} params.device - 设备 ID/IP:Port(可选,用于获取最新截图)
- * @returns {Promise<{success: boolean, error?: string}>}
- */
- export async function executeImageAreaCropping({ area, savePath, folderPath, device }) {
- try {
- if (!window.electronAPI || !window.electronAPI.cropAndSaveImage) {
- return {
- success: false,
- error: 'cropAndSaveImage API 不可用'
- };
- }
- // 解析区域坐标
- let areaObj = area;
- if (typeof area === 'string') {
- try {
- areaObj = JSON.parse(area);
- } catch (e) {
- return {
- success: false,
- error: `区域坐标格式错误,无法解析JSON: ${e.message}`
- };
- }
- }
- if (!areaObj || typeof areaObj !== 'object') {
- return {
- success: false,
- error: '区域坐标必须是对象格式'
- };
- }
- // 提取坐标信息(支持多种格式)
- let x, y, width, height;
-
- if (areaObj.topLeft && areaObj.bottomRight) {
- // 格式1:{topLeft: {x, y}, bottomRight: {x, y}}
- x = parseInt(areaObj.topLeft.x);
- y = parseInt(areaObj.topLeft.y);
- width = parseInt(areaObj.bottomRight.x - areaObj.topLeft.x);
- height = parseInt(areaObj.bottomRight.y - areaObj.topLeft.y);
- } else if (areaObj.topLeft && areaObj.topRight && areaObj.bottomLeft && areaObj.bottomRight) {
- // 格式1.5:{topLeft, topRight, bottomLeft, bottomRight} - 使用 topLeft 和 bottomRight
- x = parseInt(areaObj.topLeft.x);
- y = parseInt(areaObj.topLeft.y);
- width = parseInt(areaObj.bottomRight.x - areaObj.topLeft.x);
- height = parseInt(areaObj.bottomRight.y - areaObj.topLeft.y);
- } else if (areaObj.x !== undefined && areaObj.y !== undefined && areaObj.width !== undefined && areaObj.height !== undefined) {
- // 格式2:{x, y, width, height}
- x = parseInt(areaObj.x);
- y = parseInt(areaObj.y);
- width = parseInt(areaObj.width);
- height = parseInt(areaObj.height);
- } else {
- return {
- success: false,
- error: '区域坐标格式不正确,需要包含 topLeft/bottomRight 或 x/y/width/height'
- };
- }
- // 验证坐标有效性
- if (isNaN(x) || isNaN(y) || isNaN(width) || isNaN(height) || width <= 0 || height <= 0) {
- return {
- success: false,
- error: `区域坐标无效: x=${x}, y=${y}, width=${width}, height=${height}`
- };
- }
- // 先通过 ADB 截图当前手机屏幕并保存到 history 文件夹
- // 参考 screenshot.js 的实现方式
- let imageBase64 = null; // 声明 imageBase64 变量
- let screenshotPath;
- // 根据配置确定文件扩展名
- const screencapFormat = ScrcpyConfig['screencap-format'] || 'jpg';
- const fileExtension = screencapFormat === 'jpeg' || screencapFormat === 'jpg' ? 'jpg' : 'png';
-
- if (folderPath.includes(':')) {
- // 绝对路径
- screenshotPath = `${folderPath}/history/ScreenShot.${fileExtension}`;
- } else {
- // 相对路径,构建相对于项目根目录的路径
- screenshotPath = `${folderPath}/history/ScreenShot.${fileExtension}`;
- }
-
- // 如果有设备ID,先尝试从缓存获取截图,如果没有再调用 ADB 截图
- if (device && window.electronAPI) {
- try {
- // 优先从主进程缓存获取截图(避免并发冲突)
- let screenshotResult = null;
- if (window.electronAPI.getCachedScreenshot) {
- screenshotResult = await window.electronAPI.getCachedScreenshot(device);
- if (screenshotResult && screenshotResult.success && screenshotResult.data) {
- imageBase64 = screenshotResult.data;
- }
- }
-
- // 如果缓存不可用,调用 ADB 截图(参考 screenshot.js 的实现方式)
- if (!imageBase64 && window.electronAPI.captureScreenshot) {
- screenshotResult = await window.electronAPI.captureScreenshot(device, {
- format: ScrcpyConfig['screencap-format'],
- quality: ScrcpyConfig['screencap-quality'],
- scale: ScrcpyConfig['screencap-scale']
- });
-
- if (screenshotResult && screenshotResult.success && screenshotResult.data) {
- imageBase64 = screenshotResult.data;
- }
- }
-
- // 如果有截图数据,保存到文件
- if (imageBase64) {
- await window.electronAPI.saveBase64Image(
- imageBase64,
- screenshotPath
- );
- }
- } catch (error) {
- // 截屏循环异常(参考 screenshot.js 的错误处理)
- }
- }
- // 处理保存路径(如果是相对路径,相对于工作流目录)
- let absoluteSavePath = savePath;
- if (!savePath.includes(':')) {
- // 相对路径,相对于工作流目录
- if (folderPath.includes(':')) {
- absoluteSavePath = `${folderPath}/${savePath}`;
- } else {
- absoluteSavePath = `${folderPath}/${savePath}`;
- }
- }
- // 如果还没有通过ADB获取base64数据,则从文件读取
- if (!imageBase64) {
- try {
- // 使用 Electron API 读取文件
- if (window.electronAPI && window.electronAPI.readImageFileAsBase64) {
- // 读取文件为 base64
- const fileContent = await window.electronAPI.readImageFileAsBase64(screenshotPath);
- if (!fileContent || !fileContent.success) {
- return {
- success: false,
- error: `无法读取截图文件: ${fileContent?.error || '未知错误'}`
- };
- }
- imageBase64 = fileContent.data;
- } else {
- return {
- success: false,
- error: 'readImageFileAsBase64 API 不可用'
- };
- }
- } catch (error) {
- return {
- success: false,
- error: `读取截图文件失败: ${error.message}`
- };
- }
- }
-
- // 验证 base64 数据是否有效(至少应该是几百字节)
- if (!imageBase64 || imageBase64.length < 100) {
- return {
- success: false,
- error: `截图数据无效,base64长度: ${imageBase64?.length || 0},应该是至少几百字节。可能是截图失败或读取失败`
- };
- }
- // 使用 Canvas API 裁剪图片
- try {
- // 创建 Image 对象
- const img = new Image();
- // 先尝试 JPEG,如果失败再尝试 PNG
- let imageMimeType = 'image/jpeg';
- let dataUrl = `data:${imageMimeType};base64,${imageBase64}`;
-
- await new Promise((resolve, reject) => {
- img.onload = () => {
- resolve();
- };
- img.onerror = (error) => {
- // 尝试 PNG 格式
- imageMimeType = 'image/png';
- dataUrl = `data:${imageMimeType};base64,${imageBase64}`;
- img.src = dataUrl;
- };
- img.src = dataUrl;
- }).catch((error) => {
- throw new Error(`无法加载图片数据,请检查截图文件是否有效`);
- });
- // 验证坐标是否在图片范围内
- if (x < 0 || y < 0 || x + width > img.width || y + height > img.height) {
- return {
- success: false,
- error: `裁剪区域超出图片范围。图片尺寸: ${img.width}x${img.height}, 裁剪区域: x=${x}, y=${y}, width=${width}, height=${height}`
- };
- }
- // 创建 Canvas 并裁剪
- const canvas = document.createElement('canvas');
- canvas.width = width;
- canvas.height = height;
- const ctx = canvas.getContext('2d');
-
- // 绘制裁剪后的区域
- ctx.drawImage(img, x, y, width, height, 0, 0, width, height);
-
- // 转换为 base64(PNG 格式)
- const croppedBase64 = canvas.toDataURL('image/png').split(',')[1]; // 去掉 data:image/png;base64, 前缀
- // 调用主进程保存 base64 图片
- const result = await window.electronAPI.saveBase64Image(
- croppedBase64,
- absoluteSavePath
- );
- if (!result.success) {
- return {
- success: false,
- error: result.error || '保存图片失败'
- };
- }
- return {
- success: true
- };
- } catch (error) {
- return {
- success: false,
- error: `Canvas裁剪失败: ${error.message}`
- };
- }
- } catch (error) {
- return {
- success: false,
- error: error.message || '裁剪图片失败'
- };
- }
- }
|