diff --git a/capabilities.mjs b/capabilities.mjs index b5c86e2..3bcd573 100644 --- a/capabilities.mjs +++ b/capabilities.mjs @@ -233,7 +233,7 @@ export function sandboxDeveloperTools(capabilities) { export function sandboxMcpTools(capabilities) { const tools = []; if (capabilities.static_publish) { - tools.push('read_file', 'write_file', 'edit_file', 'create_dir'); + tools.push('read_file', 'write_file', 'edit_file', 'create_dir', 'generate_long_image'); if (capabilities.shell || capabilities.code_browse) tools.push('list_dir'); } if (capabilities.private_data_space) { diff --git a/capabilities.test.mjs b/capabilities.test.mjs index e05fef6..0722d32 100644 --- a/capabilities.test.mjs +++ b/capabilities.test.mjs @@ -178,6 +178,7 @@ test('sandboxMcpTools returns correct tool list based on capabilities', () => { 'write_file', 'edit_file', 'create_dir', + 'generate_long_image', 'private_data_info', 'private_data_schema', 'private_data_query', @@ -240,6 +241,7 @@ test('static_publish with sandboxMcp uses stdio sandbox-fs extension instead of assert.equal(sandboxExt.args[1], '/opt/h5/MindSpace/abc123'); // also passed as argv[2] assert.ok(sandboxExt.available_tools.includes('write_file')); assert.ok(sandboxExt.available_tools.includes('read_file')); + assert.ok(sandboxExt.available_tools.includes('generate_long_image')); // built-in developer extension should only remain for read_image (image_read: true by default) const developer = policy.extensionOverrides.find((ext) => ext.name === 'developer'); diff --git a/chat-skills.mjs b/chat-skills.mjs index 4554b8c..4304abd 100644 --- a/chat-skills.mjs +++ b/chat-skills.mjs @@ -113,7 +113,8 @@ export function buildChatSkillPrompt(promptKey, skillName) { `请使用 ${skillName ?? PUBLISH_SKILL_NAME} 技能:在我的专属 MindSpace 发布目录生成静态 HTML 页面,并给出可公网访问的完整链接。` + '必须先 load_skill,再用 write_file/edit_file 写入 public/页面.html;完成后直接返回 Markdown 可点击公网链接 `[页面标题](URL)`。' + '禁止只给本地路径(如 hello/index.html),禁止询问是否还要发布,除非写文件工具实际失败。' + - '如果页面里还需要提供 Word / docx 下载,必须先 `load_skill` → `docx-generate`,生成 `public/*.docx` 并确认文件已存在,再写 HTML 并用同目录相对路径链接该文档。' + '如果页面里还需要提供 Word / docx 下载,必须先 `load_skill` → `docx-generate`,生成 `public/*.docx` 并确认文件已存在,再写 HTML 并用同目录相对路径链接该文档。' + + '如果用户要求长图下载,必须先 `load_skill` → `long-image-download`,调用 `generate_long_image` 生成 `public/*.long.png` 并确认文件已存在,再给长图预览链接和 `?download=long-image` 下载链接。' ); default: return ''; diff --git a/chat-skills.test.mjs b/chat-skills.test.mjs index 4bde421..a6b8ea9 100644 --- a/chat-skills.test.mjs +++ b/chat-skills.test.mjs @@ -44,6 +44,8 @@ test('buildChatSkillPrompt includes skill name for platform skills', () => { assert.match(buildChatSkillPrompt('generate-page'), /static-page-publish/); assert.match(buildChatSkillPrompt('generate-page'), /docx-generate/); assert.match(buildChatSkillPrompt('generate-page'), /public\/\*\.docx/); + assert.match(buildChatSkillPrompt('generate-page'), /long-image-download/); + assert.match(buildChatSkillPrompt('generate-page'), /generate_long_image/); }); test('prefillOnly is set for open-ended chat skills', () => { diff --git a/mindspace-long-image.mjs b/mindspace-long-image.mjs new file mode 100644 index 0000000..1cc1ddb --- /dev/null +++ b/mindspace-long-image.mjs @@ -0,0 +1,106 @@ +import fs from 'node:fs'; +import fsPromises from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; + +const DEFAULT_VIEWPORT_WIDTH = 1280; +const DEFAULT_VIEWPORT_HEIGHT = 720; +const MAX_LONG_IMAGE_HEIGHT = 20000; + +export function isLongImageDownloadRequest(query) { + const value = String(query?.download ?? query?.export ?? '').trim().toLowerCase(); + return value === 'long-image' || value === 'long_image' || value === 'png'; +} + +export function longImagePathForHtml(htmlPath, outputPath = null) { + return outputPath || String(htmlPath).replace(/\.html$/i, '.long.png'); +} + +function clampDimension(value, fallback, max) { + const number = Math.ceil(Number(value) || fallback); + return Math.max(320, Math.min(number, max)); +} + +async function launchChromium(chromium) { + const base = { + headless: true, + args: ['--disable-dev-shm-usage', '--hide-scrollbars'], + }; + if (process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH) { + return chromium.launch({ + ...base, + executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH, + }); + } + if (process.env.PLAYWRIGHT_CHROMIUM_CHANNEL) { + return chromium.launch({ ...base, channel: process.env.PLAYWRIGHT_CHROMIUM_CHANNEL }); + } + try { + return await chromium.launch(base); + } catch (error) { + if (process.platform === 'darwin') { + return chromium.launch({ ...base, channel: 'chrome' }); + } + throw error; + } +} + +export async function renderLongImage({ + htmlPath = null, + url = null, + outputPath = null, + viewportWidth = DEFAULT_VIEWPORT_WIDTH, + viewportHeight = DEFAULT_VIEWPORT_HEIGHT, +} = {}) { + if (!htmlPath && !url) throw new Error('缺少 htmlPath 或 url'); + const targetUrl = url || pathToFileURL(path.resolve(htmlPath)).toString(); + const destination = outputPath ? path.resolve(outputPath) : longImagePathForHtml(path.resolve(htmlPath)); + const { chromium } = await import('playwright'); + let browser = null; + try { + browser = await launchChromium(chromium); + const page = await browser.newPage({ + viewport: { + width: clampDimension(viewportWidth, DEFAULT_VIEWPORT_WIDTH, 2400), + height: clampDimension(viewportHeight, DEFAULT_VIEWPORT_HEIGHT, MAX_LONG_IMAGE_HEIGHT), + }, + deviceScaleFactor: 2, + }); + await page.goto(targetUrl, { waitUntil: 'networkidle', timeout: 30000 }); + await page.evaluate(() => document.fonts?.ready).catch(() => null); + const size = await page.evaluate(() => ({ + width: Math.max(document.documentElement.scrollWidth, document.body.scrollWidth, 320), + height: Math.max(document.documentElement.scrollHeight, document.body.scrollHeight, 320), + })); + await page.setViewportSize({ + width: clampDimension(size.width, DEFAULT_VIEWPORT_WIDTH, 2400), + height: Math.min(clampDimension(size.height, DEFAULT_VIEWPORT_HEIGHT, MAX_LONG_IMAGE_HEIGHT), 2400), + }); + await fsPromises.mkdir(path.dirname(destination), { recursive: true }); + await page.screenshot({ + path: destination, + fullPage: true, + type: 'png', + animations: 'disabled', + caret: 'hide', + }); + return { + outputPath: destination, + bytes: fs.statSync(destination).size, + }; + } finally { + await browser?.close().catch(() => {}); + } +} + +export async function renderLongImageBuffer({ url }) { + const tempDir = await fsPromises.mkdtemp(path.join(os.tmpdir(), 'memind-long-image-')); + const outputPath = path.join(tempDir, 'page.long.png'); + try { + await renderLongImage({ url, outputPath }); + return await fsPromises.readFile(outputPath); + } finally { + await fsPromises.rm(tempDir, { recursive: true, force: true }).catch(() => {}); + } +} diff --git a/mindspace-sandbox-mcp.mjs b/mindspace-sandbox-mcp.mjs index 0fe9001..0aad2f2 100644 --- a/mindspace-sandbox-mcp.mjs +++ b/mindspace-sandbox-mcp.mjs @@ -15,6 +15,7 @@ import { execFileSync } from 'node:child_process'; import mysql from 'mysql2/promise'; import { createScheduleService } from './schedule-service.mjs'; import { resolveScheduleTimestamp } from './schedule-time.mjs'; +import { renderLongImage } from './mindspace-long-image.mjs'; const SANDBOX_ROOT = process.argv[2]?.trim() || process.env.SANDBOX_ROOT?.trim(); if (!SANDBOX_ROOT) { @@ -108,6 +109,19 @@ const ALL_TOOLS = [ required: ['path'], }, }, + { + name: 'generate_long_image', + description: + '用 Playwright 将工作区内的 HTML 页面渲染为整页 PNG 长图。输出文件通常为 public/<页面名>.long.png,必须在工作区内。', + inputSchema: { + type: 'object', + properties: { + html_path: { type: 'string', description: 'HTML 文件路径,如 public/report.html' }, + output_path: { type: 'string', description: '输出 PNG 路径,如 public/report.long.png;可选' }, + }, + required: ['html_path'], + }, + }, { name: 'private_data_info', description: @@ -458,6 +472,25 @@ async function callTool(name, args) { fs.mkdirSync(abs, { recursive: true }); return [{ type: 'text', text: `已创建目录 ${args.path}` }]; } + case 'generate_long_image': { + const htmlPath = String(args.html_path ?? args.path ?? '').trim(); + if (!htmlPath.toLowerCase().endsWith('.html')) { + throw new Error('generate_long_image: html_path 必须是 .html 文件'); + } + const htmlAbs = resolveSandboxed(htmlPath); + const outputPath = String(args.output_path ?? '').trim() || htmlPath.replace(/\.html$/i, '.long.png'); + if (!outputPath.toLowerCase().endsWith('.png')) { + throw new Error('generate_long_image: output_path 必须是 .png 文件'); + } + const outputAbs = resolveSandboxed(outputPath); + const result = await renderLongImage({ htmlPath: htmlAbs, outputPath: outputAbs }); + return [ + { + type: 'text', + text: `已生成长图 ${outputPath}(${result.bytes} 字节)`, + }, + ]; + } case 'private_data_info': { ensurePrivateDataDb(); const size = privateDataSize(); diff --git a/package-lock.json b/package-lock.json index 4d7e980..4e4a0d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,11 +18,13 @@ "lucide-react": "^1.21.0", "mysql2": "^3.22.5", "pg": "^8.22.0", + "playwright": "^1.61.1", "qrcode": "^1.5.4", "react": "^19.0.0", "react-dom": "^19.0.0", "react-router-dom": "^7.13.1", "redis": "^4.7.1", + "sharp": "^0.35.2", "undici": "^6.26.0" }, "devDependencies": { @@ -789,6 +791,554 @@ "node": ">=18" } }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz", + "integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.2" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz", + "integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz", + "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz", + "integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz", + "integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz", + "integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz", + "integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz", + "integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz", + "integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz", + "integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz", + "integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz", + "integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz", + "integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz", + "integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz", + "integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz", + "integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz", + "integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz", + "integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz", + "integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz", + "integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz", + "integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz", + "integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.1" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz", + "integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz", + "integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz", + "integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz", + "integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -2248,6 +2798,15 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/dijkstrajs": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", @@ -3358,6 +3917,50 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/pngjs": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", @@ -3783,6 +4386,67 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/sharp": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz", + "integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==", + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.3", + "@img/sharp-darwin-x64": "0.35.3", + "@img/sharp-freebsd-wasm32": "0.35.3", + "@img/sharp-libvips-darwin-arm64": "1.3.2", + "@img/sharp-libvips-darwin-x64": "1.3.2", + "@img/sharp-libvips-linux-arm": "1.3.2", + "@img/sharp-libvips-linux-arm64": "1.3.2", + "@img/sharp-libvips-linux-ppc64": "1.3.2", + "@img/sharp-libvips-linux-riscv64": "1.3.2", + "@img/sharp-libvips-linux-s390x": "1.3.2", + "@img/sharp-libvips-linux-x64": "1.3.2", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", + "@img/sharp-libvips-linuxmusl-x64": "1.3.2", + "@img/sharp-linux-arm": "0.35.3", + "@img/sharp-linux-arm64": "0.35.3", + "@img/sharp-linux-ppc64": "0.35.3", + "@img/sharp-linux-riscv64": "0.35.3", + "@img/sharp-linux-s390x": "0.35.3", + "@img/sharp-linux-x64": "0.35.3", + "@img/sharp-linuxmusl-arm64": "0.35.3", + "@img/sharp-linuxmusl-x64": "0.35.3", + "@img/sharp-webcontainers-wasm32": "0.35.3", + "@img/sharp-win32-arm64": "0.35.3", + "@img/sharp-win32-ia32": "0.35.3", + "@img/sharp-win32-x64": "0.35.3" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/side-channel": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", diff --git a/package.json b/package.json index 78e4f22..ff21e11 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "lucide-react": "^1.21.0", "mysql2": "^3.22.5", "pg": "^8.22.0", + "playwright": "^1.61.1", "qrcode": "^1.5.4", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a5cd72f..e1e5b2d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: pg: specifier: ^8.22.0 version: 8.22.0 + playwright: + specifier: ^1.61.1 + version: 1.61.1 qrcode: specifier: ^1.5.4 version: 1.5.4 @@ -1142,6 +1145,11 @@ packages: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1418,6 +1426,16 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + playwright-core@1.61.1: + resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.61.1: + resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==} + engines: {node: '>=18'} + hasBin: true + pngjs@5.0.0: resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} engines: {node: '>=10.13.0'} @@ -2595,6 +2613,9 @@ snapshots: fresh@0.5.2: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -2842,6 +2863,14 @@ snapshots: picomatch@4.0.4: {} + playwright-core@1.61.1: {} + + playwright@1.61.1: + dependencies: + playwright-core: 1.61.1 + optionalDependencies: + fsevents: 2.3.2 + pngjs@5.0.0: {} postcss@8.5.15: diff --git a/server.mjs b/server.mjs index d58d06d..22c734b 100644 --- a/server.mjs +++ b/server.mjs @@ -102,6 +102,12 @@ import { rasterizeThumbnailSvgToPng, thumbnailPngPathForSvg, } from './mindspace-thumbnail-png.mjs'; +import { + isLongImageDownloadRequest, + longImagePathForHtml, + renderLongImage, + renderLongImageBuffer, +} from './mindspace-long-image.mjs'; import { scanContent } from './mindspace-content-scan.mjs'; import { renderImageAssetViewerHtml, wantsInlineImageViewer } from './mindspace-asset-preview.mjs'; import { createRechargeService } from './billing-recharge.mjs'; @@ -4390,7 +4396,7 @@ function publishedPageCsp(html, { embed = false, raw = false, wechatShare = fals return "default-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src 'none'; base-uri 'none'; form-action 'self'; frame-ancestors 'self'"; } -const PUBLIC_FILE_SHARE_SCRIPT = `(function(){var root=document.querySelector('[data-mindspace-public-share]');if(!root)return;var button=root.querySelector('button');var status=root.querySelector('small');var timer=null;function setStatus(text,err){if(!status)return;status.textContent=text||'';status.classList.toggle('is-error',!!err);if(timer)clearTimeout(timer);if(text)timer=setTimeout(function(){status.textContent='';status.classList.remove('is-error');},2200);}function fallbackCopy(text){var ta=document.createElement('textarea');ta.value=text;ta.style.position='fixed';ta.style.left='-9999px';ta.style.opacity='0';document.body.appendChild(ta);ta.focus();ta.select();var ok=document.execCommand('copy');ta.remove();if(!ok)throw new Error('复制失败');}async function copyText(text){if(navigator.clipboard&&navigator.clipboard.writeText){try{await navigator.clipboard.writeText(text);return;}catch(e){}}fallbackCopy(text);}button&&button.addEventListener('click',async function(){var url=location.href.split('#')[0];var title=document.title||'MindSpace';try{if(navigator.share){await navigator.share({title:title,url:url});setStatus('已打开分享');return;}await copyText(url);setStatus('链接已复制');}catch(e){setStatus(e&&e.message?e.message:'分享失败',true);}});})();`; +const PUBLIC_FILE_SHARE_SCRIPT = `(function(){var root=document.querySelector('[data-mindspace-public-share]');if(!root)return;var shareButton=root.querySelector('[data-action="share"]');var captureButton=root.querySelector('[data-action="capture"]');var status=root.querySelector('small');var timer=null;function setStatus(text,err){if(!status)return;status.textContent=text||'';status.classList.toggle('is-error',!!err);if(timer)clearTimeout(timer);if(text)timer=setTimeout(function(){status.textContent='';status.classList.remove('is-error');},2200);}function cleanUrl(){var url=new URL(location.href);url.hash='';url.searchParams.delete('download');url.searchParams.delete('export');return url.toString();}function withParam(url,key,value){var next=new URL(url);next.searchParams.set(key,value);return next.toString();}function fallbackCopy(text){var ta=document.createElement('textarea');ta.value=text;ta.style.position='fixed';ta.style.left='-9999px';ta.style.opacity='0';document.body.appendChild(ta);ta.focus();ta.select();var ok=document.execCommand('copy');ta.remove();if(!ok)throw new Error('复制失败');}async function copyText(text){if(navigator.clipboard&&navigator.clipboard.writeText){try{await navigator.clipboard.writeText(text);return;}catch(e){}}fallbackCopy(text);}shareButton&&shareButton.addEventListener('click',async function(){var url=cleanUrl();var title=document.title||'MindSpace';try{if(navigator.share){await navigator.share({title:title,url:url});setStatus('已打开分享');return;}await copyText(url);setStatus('链接已复制');}catch(e){setStatus(e&&e.message?e.message:'分享失败',true);}});captureButton&&captureButton.addEventListener('click',function(){setStatus('正在生成长图,请稍候');var link=document.createElement('a');link.href=withParam(cleanUrl(),'download','long-image');link.download='';document.body.appendChild(link);link.click();link.remove();});})();`; const PUBLIC_FILE_SHARE_SCRIPT_HASH = crypto .createHash('sha256') .update(PUBLIC_FILE_SHARE_SCRIPT) @@ -4404,14 +4410,16 @@ function injectPublicFileShareButton(html) { const markup = ` -
+