192 lines
No EOL
7.3 KiB
TypeScript
192 lines
No EOL
7.3 KiB
TypeScript
import { chromium } from 'playwright';
|
|
import { ToolModule } from './interface.js';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as child_process from 'child_process';
|
|
|
|
// Helper to check for common CJK and Emoji font paths on Linux
|
|
const checkLinuxFonts = () => {
|
|
if (os.platform() !== 'linux') return { cjk: true, emoji: true };
|
|
|
|
// Check for specific font files rather than just directories
|
|
const commonCJKFontFiles = [
|
|
'/usr/share/fonts/noto/NotoSansCJK-Regular.ttc', // Alpine / Some Debian
|
|
'/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc', // Debian / Ubuntu
|
|
'/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc', // ZenHei
|
|
'/usr/share/fonts/google-noto-cjk/NotoSansCJK-Regular.ttc' // Arch
|
|
];
|
|
|
|
const commonEmojiFontFiles = [
|
|
'/usr/share/fonts/noto/NotoColorEmoji.ttf',
|
|
'/usr/share/fonts/truetype/noto/NotoColorEmoji.ttf',
|
|
'/usr/share/fonts/google-noto-emoji/NotoColorEmoji.ttf'
|
|
];
|
|
|
|
const hasCJK = commonCJKFontFiles.some(path => fs.existsSync(path));
|
|
const hasEmoji = commonEmojiFontFiles.some(path => fs.existsSync(path));
|
|
|
|
// Also check if fc-list finds fonts (if available) - secondary check
|
|
try {
|
|
const cjkOutput = child_process.execSync('fc-list :lang=zh', { stdio: 'pipe' }).toString();
|
|
const emojiOutput = child_process.execSync('fc-list :family=Emoji', { stdio: 'pipe' }).toString(); // Approximate check
|
|
|
|
return {
|
|
cjk: hasCJK || cjkOutput.length > 0,
|
|
emoji: hasEmoji || emojiOutput.length > 0
|
|
};
|
|
} catch (e) {
|
|
// fc-list might not be installed or failed, fall back to file check
|
|
return { cjk: hasCJK, emoji: hasEmoji };
|
|
}
|
|
};
|
|
|
|
const installFonts = (missing: { cjk: boolean, emoji: boolean }) => {
|
|
try {
|
|
let installCmd = '';
|
|
if (fs.existsSync('/etc/alpine-release')) {
|
|
// Alpine
|
|
const pkgs = [];
|
|
if (!missing.cjk) pkgs.push('font-noto-cjk');
|
|
if (!missing.emoji) pkgs.push('font-noto-emoji');
|
|
if (pkgs.length > 0) {
|
|
installCmd = `apk add --no-cache ${pkgs.join(' ')}`;
|
|
}
|
|
} else if (fs.existsSync('/etc/debian_version')) {
|
|
// Debian/Ubuntu
|
|
const pkgs = [];
|
|
if (!missing.cjk) pkgs.push('fonts-noto-cjk', 'fonts-wqy-zenhei');
|
|
if (!missing.emoji) pkgs.push('fonts-noto-color-emoji');
|
|
if (pkgs.length > 0) {
|
|
// apt-get update is often needed first in clean containers
|
|
installCmd = `apt-get update && apt-get install -y ${pkgs.join(' ')}`;
|
|
}
|
|
}
|
|
|
|
if (installCmd) {
|
|
console.log(`Creating font environment... (${installCmd})`);
|
|
console.log("This may take a few moments...");
|
|
child_process.execSync(installCmd, { stdio: 'inherit' });
|
|
console.log('✅ Fonts installed successfully.');
|
|
return true;
|
|
}
|
|
} catch (e: any) {
|
|
console.warn(`⚠️ Failed to auto-install fonts: ${e.message}`);
|
|
console.warn('Please install them manually to fix "tofu" characters.');
|
|
}
|
|
return false;
|
|
};
|
|
|
|
export const ScreenshotTool: ToolModule = {
|
|
name: "Screenshot Tool",
|
|
configKeys: [],
|
|
definition: {
|
|
type: "function",
|
|
function: {
|
|
name: "take_screenshot",
|
|
description: "Captures a screenshot of a specified website and saves it as an image file.",
|
|
parameters: {
|
|
type: "object",
|
|
properties: {
|
|
url: {
|
|
type: "string",
|
|
description: "The full URL of the website to capture (e.g., https://google.com)."
|
|
},
|
|
outputPath: {
|
|
type: "string",
|
|
description: "The file path where the screenshot should be saved (e.g., 'homepage.png')."
|
|
},
|
|
fullPage: {
|
|
type: "boolean",
|
|
description: "If true (default), takes a screenshot of the full scrollable page. Set to false for viewport only."
|
|
},
|
|
waitTime: {
|
|
type: "number",
|
|
description: "Optional delay in seconds to wait for page resources to load before capturing (default: 1)."
|
|
}
|
|
},
|
|
required: ["url", "outputPath"]
|
|
}
|
|
}
|
|
},
|
|
handler: async (args: any, config: any) => {
|
|
// Check for fonts on Linux to prevent "tofu" characters
|
|
if (os.platform() === 'linux') {
|
|
const fonts = checkLinuxFonts();
|
|
if (!fonts.cjk || !fonts.emoji) {
|
|
console.log("Missing fonts detected. Attempting to fix environment...");
|
|
installFonts(fonts);
|
|
}
|
|
}
|
|
|
|
let browser;
|
|
const launchOptions: any = {
|
|
headless: true,
|
|
args: ['--font-render-hinting=none']
|
|
};
|
|
|
|
try {
|
|
// Try to launch system Chrome first as it usually has better font support
|
|
browser = await chromium.launch({ ...launchOptions, channel: 'chrome' });
|
|
} catch (e) {
|
|
// Fallback to bundled Chromium
|
|
try {
|
|
browser = await chromium.launch(launchOptions);
|
|
} catch (error: any) {
|
|
if (error.message.includes("Executable doesn't exist")) {
|
|
return "Error: Playwright browsers are not installed. Please run `npx playwright install chromium` to enable this feature.";
|
|
}
|
|
return `Error launching browser: ${error.message}`;
|
|
}
|
|
}
|
|
|
|
const context = await browser.newContext({
|
|
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
viewport: { width: 1280, height: 720 },
|
|
locale: 'zh-CN', // Set locale to Chinese
|
|
deviceScaleFactor: 2 // High DPI
|
|
});
|
|
const page = await context.newPage();
|
|
|
|
try {
|
|
console.log(`Navigating to ${args.url} for screenshot...`);
|
|
await page.goto(args.url, { waitUntil: 'networkidle', timeout: 30000 });
|
|
|
|
// Inject CSS to force common Chinese fonts
|
|
// Note: For Docker environments (Alpine/Debian), ensure fonts are installed.
|
|
// Alpine: apk add font-noto-cjk font-noto-emoji
|
|
// Debian/Ubuntu: apt-get install fonts-noto-cjk fonts-wqy-zenhei fonts-noto-color-emoji
|
|
await page.addStyleTag({
|
|
content: `
|
|
body, h1, h2, h3, h4, h5, h6, p, span, div, li, a, button, input, textarea {
|
|
font-family: "PingFang SC", "Heiti SC", "Microsoft YaHei", "WenQuanYi Micro Hei", "Noto Sans CJK SC", "Noto Sans SC", "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif !important;
|
|
}
|
|
`
|
|
});
|
|
|
|
// Wait for fonts to be ready
|
|
await page.evaluate(() => document.fonts.ready);
|
|
|
|
// Additional delay for dynamic content (user specified or default 1s)
|
|
const waitTimeMs = (args.waitTime || 1) * 1000;
|
|
if (waitTimeMs > 0) {
|
|
console.log(`Waiting for ${waitTimeMs}ms...`);
|
|
await page.waitForTimeout(waitTimeMs);
|
|
}
|
|
|
|
await page.screenshot({
|
|
path: args.outputPath,
|
|
fullPage: args.fullPage !== false // Default to true if undefined
|
|
});
|
|
|
|
return `Successfully captured screenshot of ${args.url} and saved to ${args.outputPath}`;
|
|
|
|
} catch (error: any) {
|
|
return `Error taking screenshot: ${error.message}`;
|
|
} finally {
|
|
if (browser) {
|
|
await browser.close();
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|