autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
160 lines (159 loc) • 5.48 kB
JavaScript
/**
* ScreenCaptureService — macOS 原生截图服务
*
* 使用 ScreenCaptureKit (via Swift CLI) 截取窗口/屏幕画面。
* 息屏可用,无需 OBS。
*
* 依赖:resources/native-ui/screenshot.swift 编译产物
*/
import { execFileSync, execSync } from 'node:child_process';
import { existsSync, statSync as fstatSync } from 'node:fs';
import { join } from 'node:path';
import Logger from '../infrastructure/logging/Logger.js';
import { RESOURCES_DIR } from '../shared/package-root.js';
const logger = Logger.getInstance();
/** Swift 源文件路径 */
const SWIFT_SRC = join(RESOURCES_DIR, 'native-ui/screenshot.swift');
/** 编译产物路径 */
const BINARY_PATH = join(RESOURCES_DIR, 'native-ui/screenshot');
let _binaryReady = false;
/**
* 确保 Swift 截图工具已编译
* @returns }
*/
function ensureBinary() {
if (_binaryReady && existsSync(BINARY_PATH)) {
return { ready: true };
}
if (!existsSync(SWIFT_SRC)) {
return { ready: false, error: `Swift source not found: ${SWIFT_SRC}` };
}
// 检查是否需要重新编译(源文件比二进制新)
const needsBuild = !existsSync(BINARY_PATH) ||
(() => {
try {
const srcTime = fstatSync(SWIFT_SRC).mtimeMs;
const binTime = fstatSync(BINARY_PATH).mtimeMs;
return srcTime > binTime;
}
catch {
return true;
}
})();
if (needsBuild) {
logger.info('[Screenshot] Compiling screenshot tool...');
try {
execSync(`swiftc -O -framework ScreenCaptureKit -framework AppKit "${SWIFT_SRC}" -o "${BINARY_PATH}"`, { timeout: 60000, stdio: ['pipe', 'pipe', 'pipe'] });
logger.info('[Screenshot] ✅ Compiled successfully');
}
catch (err) {
const stderr = err.stderr?.toString() ||
(err instanceof Error ? err.message : String(err));
logger.error(`[Screenshot] Compilation failed: ${stderr}`);
return { ready: false, error: `Swift compilation failed: ${stderr.slice(0, 200)}` };
}
}
_binaryReady = true;
return { ready: true };
}
/**
* 执行截图工具
* @param args CLI 参数
* @returns }
*/
function exec(args = []) {
const check = ensureBinary();
if (!check.ready) {
return { success: false, error: check.error };
}
try {
const stdout = execFileSync(BINARY_PATH, args, {
timeout: 15000,
encoding: 'utf-8',
maxBuffer: 1024 * 1024,
stdio: ['pipe', 'pipe', 'pipe'],
});
const result = JSON.parse(stdout.trim());
return { success: true, data: result };
}
catch (err) {
const execErr = err;
// 尝试从 stderr 中解析 JSON 错误
const stderr = execErr.stderr?.toString() || '';
try {
const errObj = JSON.parse(stderr.trim());
return { success: false, error: errObj.error || stderr };
}
catch {
return { success: false, error: stderr || execErr.message };
}
}
}
// ═══════════════════════════════════════════════════════
// 公开 API
// ═══════════════════════════════════════════════════════
/**
* 截取屏幕或窗口画面(核心功能 — 息屏可用)
*
* @param [opts.windowTitle] 窗口标题关键词(模糊匹配),默认截整屏
* @param [opts.format] 'jpeg' | 'png'
* @param [opts.scale] 缩放因子 (0.25-2.0)
* @param [opts.outputPath] 输出路径,默认临时目录
* @returns >}
*/
export async function screenshot(opts = {}) {
const args = [];
if (opts.windowTitle) {
args.push('--window', opts.windowTitle);
}
if (opts.format) {
args.push('--format', opts.format);
}
if (opts.scale) {
args.push('--scale', String(opts.scale));
}
if (opts.outputPath) {
args.push('--output', opts.outputPath);
}
const result = exec(args);
if (result.success && result.data) {
logger.info(`[Screenshot] ✅ Captured: ${result.data.path} (${result.data.width}x${result.data.height})`);
return {
success: true,
path: result.data.path,
width: result.data.width,
height: result.data.height,
format: result.data.format,
bytes: result.data.bytes,
};
}
logger.warn(`[Screenshot] ❌ Failed: ${result.error}`);
return { success: false, error: result.error };
}
/**
* 列出所有可截取的窗口
* @returns >}
*/
export async function listWindows() {
const result = exec(['--list-windows']);
if (result.success && result.data) {
return { success: true, windows: result.data };
}
return { success: false, error: result.error };
}
/**
* 检查截图服务是否可用
* @returns }
*/
export function isScreenCaptureAvailable() {
const check = ensureBinary();
if (!check.ready) {
return { available: false, error: check.error };
}
// 尝试列出窗口来验证权限
const result = exec(['--list-windows']);
return {
available: result.success,
error: result.success ? undefined : result.error,
};
}