feat: add Playwright long image downloads
This commit is contained in:
+1
-1
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
|
||||
+2
-1
@@ -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 '';
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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(() => {});
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
Generated
+664
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Generated
+29
@@ -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:
|
||||
|
||||
+73
-60
@@ -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 = `
|
||||
<style id="mindspace-public-share-style">
|
||||
[data-mindspace-public-share]{position:fixed;right:18px;bottom:calc(18px + env(safe-area-inset-bottom,0px));z-index:2147483000;display:flex;flex-direction:column;align-items:flex-end;gap:6px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
|
||||
[data-mindspace-public-share] div{display:flex;gap:8px;justify-content:flex-end;flex-wrap:wrap}
|
||||
[data-mindspace-public-share] button{border:0;border-radius:999px;padding:10px 15px;background:#2f6f57;color:#fff;font-size:14px;font-weight:700;box-shadow:0 8px 24px rgba(0,0,0,.18);cursor:pointer}
|
||||
[data-mindspace-public-share] button[data-action="capture"]{background:#8f6b2f}
|
||||
[data-mindspace-public-share] button:active{transform:translateY(1px)}
|
||||
[data-mindspace-public-share] small{min-height:18px;max-width:180px;border-radius:999px;padding:4px 9px;background:rgba(255,255,255,.92);color:#245845;font-size:12px;text-align:right;box-shadow:0 4px 14px rgba(0,0,0,.1)}
|
||||
[data-mindspace-public-share] small:empty{display:none}
|
||||
[data-mindspace-public-share] small.is-error{color:#8b2d20}
|
||||
@media(max-width:640px){[data-mindspace-public-share]{right:12px;bottom:calc(12px + env(safe-area-inset-bottom,0px))}[data-mindspace-public-share] button{padding:9px 13px;font-size:13px}}
|
||||
</style>
|
||||
<div data-mindspace-public-share><small aria-live="polite"></small><button type="button">公开分享</button></div>
|
||||
<div data-mindspace-public-share><small aria-live="polite"></small><div><button type="button" data-action="capture">保存长图</button><button type="button" data-action="share">公开分享</button></div></div>
|
||||
<script>${PUBLIC_FILE_SHARE_SCRIPT}</script>`;
|
||||
if (/<\/body>/i.test(source)) {
|
||||
return {
|
||||
@@ -4446,6 +4454,16 @@ function appendQueryParam(url, key, value) {
|
||||
return `${url}${separator}${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
||||
}
|
||||
|
||||
function removeQueryParam(url, key) {
|
||||
if (!url || !url.includes('?')) return url;
|
||||
const [base, queryAndHash] = url.split('?', 2);
|
||||
const [query, hash = ''] = queryAndHash.split('#', 2);
|
||||
const params = new URLSearchParams(query);
|
||||
params.delete(key);
|
||||
const nextQuery = params.toString();
|
||||
return `${base}${nextQuery ? `?${nextQuery}` : ''}${hash ? `#${hash}` : ''}`;
|
||||
}
|
||||
|
||||
function resolveRequestOrigin(req) {
|
||||
const host = (req.headers['x-forwarded-host'] || req.headers.host || '').toString().split(',')[0].trim();
|
||||
if (!host) return '';
|
||||
@@ -4460,18 +4478,19 @@ function detectPublishedPageTitle(html) {
|
||||
return match?.[1]?.replace(/\s+/g, ' ').trim() || 'MindSpace 页面';
|
||||
}
|
||||
|
||||
function publishedPageShellHtml({ iframeUrl, shareUrl, title }) {
|
||||
function publishedPageShellHtml({ iframeUrl, shareUrl, title, longImageUrl }) {
|
||||
const iframeSrc = escapePublicHtml(iframeUrl);
|
||||
const safeShareUrl = escapePublicHtml(shareUrl);
|
||||
const safeTitle = escapePublicHtml(title);
|
||||
const serializedShareUrl = JSON.stringify(shareUrl).replace(/</g, '\\u003c');
|
||||
const serializedTitle = JSON.stringify(title).replace(/</g, '\\u003c');
|
||||
const serializedLongImageUrl = JSON.stringify(longImageUrl).replace(/</g, '\\u003c');
|
||||
return `<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src 'none'; connect-src 'self' https://cdn.jsdelivr.net; script-src 'unsafe-inline' https://cdn.jsdelivr.net; frame-src 'self'; base-uri 'none'; form-action 'none'">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src 'none'; connect-src 'self'; script-src 'unsafe-inline'; frame-src 'self'; base-uri 'none'; form-action 'none'">
|
||||
<title>${safeTitle}</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
@@ -4655,13 +4674,13 @@ function publishedPageShellHtml({ iframeUrl, shareUrl, title }) {
|
||||
(function () {
|
||||
var shareUrl = ${serializedShareUrl};
|
||||
var shareTitle = ${serializedTitle};
|
||||
var longImageUrl = ${serializedLongImageUrl};
|
||||
var fab = document.getElementById('publication-share-fab');
|
||||
var sheet = document.getElementById('publication-share-sheet');
|
||||
var close = document.getElementById('publication-share-close');
|
||||
var message = document.getElementById('publication-share-message');
|
||||
var frame = document.querySelector('.publication-frame');
|
||||
var actionButtons = sheet ? Array.prototype.slice.call(sheet.querySelectorAll('.publication-share-option')) : [];
|
||||
var html2canvasLoader = null;
|
||||
function setBusy(busy) {
|
||||
actionButtons.forEach(function (button) {
|
||||
button.disabled = !!busy;
|
||||
@@ -4703,54 +4722,10 @@ function publishedPageShellHtml({ iframeUrl, shareUrl, title }) {
|
||||
textarea.remove();
|
||||
if (!copied) throw new Error('复制失败,请手动复制链接');
|
||||
}
|
||||
function loadHtml2Canvas() {
|
||||
if (window.html2canvas) return Promise.resolve(window.html2canvas);
|
||||
if (html2canvasLoader) return html2canvasLoader;
|
||||
html2canvasLoader = new Promise(function (resolve, reject) {
|
||||
var script = document.createElement('script');
|
||||
script.src = 'https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js';
|
||||
script.async = true;
|
||||
script.onload = function () {
|
||||
if (!window.html2canvas) {
|
||||
reject(new Error('无法加载页面截图能力'));
|
||||
return;
|
||||
}
|
||||
resolve(window.html2canvas);
|
||||
};
|
||||
script.onerror = function () { reject(new Error('无法加载页面截图能力')); };
|
||||
document.body.appendChild(script);
|
||||
}).catch(function (error) {
|
||||
html2canvasLoader = null;
|
||||
throw error;
|
||||
});
|
||||
return html2canvasLoader;
|
||||
}
|
||||
async function saveLongImage() {
|
||||
if (!frame || !frame.contentDocument || !frame.contentDocument.body) {
|
||||
throw new Error('页面内容暂时不可访问');
|
||||
}
|
||||
var renderer = await loadHtml2Canvas();
|
||||
var doc = frame.contentDocument;
|
||||
var body = doc.body;
|
||||
var width = Math.max(doc.documentElement ? doc.documentElement.scrollWidth : 0, body.scrollWidth, 320);
|
||||
var height = Math.max(doc.documentElement ? doc.documentElement.scrollHeight : 0, body.scrollHeight, 320);
|
||||
var canvas = await renderer(body, {
|
||||
useCORS: true,
|
||||
scale: 2,
|
||||
backgroundColor: '#ffffff',
|
||||
width: width,
|
||||
height: height,
|
||||
windowWidth: width,
|
||||
windowHeight: height,
|
||||
x: 0,
|
||||
y: 0,
|
||||
scrollX: 0,
|
||||
scrollY: 0
|
||||
});
|
||||
var link = document.createElement('a');
|
||||
var timestamp = new Date().toISOString().replace(/[T:]/g, '-').replace(/\..+/, '');
|
||||
link.href = canvas.toDataURL('image/png');
|
||||
link.download = 'mindspace-public-page-' + timestamp + '.png';
|
||||
link.href = longImageUrl;
|
||||
link.download = '';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
@@ -4824,10 +4799,12 @@ function publishedPageShellHtml({ iframeUrl, shareUrl, title }) {
|
||||
</html>`;
|
||||
}
|
||||
|
||||
function sendPublishedPage(req, res, result, { embed = false, raw = false } = {}) {
|
||||
async function sendPublishedPage(req, res, result, { embed = false, raw = false } = {}) {
|
||||
let html = result.html;
|
||||
const origin = resolveRequestOrigin(req);
|
||||
const pageUrl = req.originalUrl ? new URL(req.originalUrl, origin || 'http://localhost').toString().split('#')[0] : '';
|
||||
const originalPath = req.originalUrl || req.url || '';
|
||||
const sharePath = removeQueryParam(removeQueryParam(originalPath, 'download'), 'export');
|
||||
const pageUrl = originalPath ? new URL(sharePath, origin || 'http://localhost').toString().split('#')[0] : '';
|
||||
const pageDirUrl = pageUrl ? `${pageUrl.slice(0, pageUrl.lastIndexOf('/') + 1)}` : '';
|
||||
const wechatShare = !embed && isWechatUserAgent(req.get('user-agent') || '');
|
||||
if (embed) {
|
||||
@@ -4845,14 +4822,31 @@ function sendPublishedPage(req, res, result, { embed = false, raw = false } = {}
|
||||
}
|
||||
}
|
||||
const isFullHtml = /^\s*<!doctype html/i.test(html) || /^\s*<html[\s>]/i.test(html);
|
||||
if (!embed && !raw && isFullHtml && isLongImageDownloadRequest(req.query)) {
|
||||
try {
|
||||
const rawUrl = new URL(appendQueryParam(sharePath || originalPath, 'view', 'raw'), origin || 'http://localhost');
|
||||
const image = await renderLongImageBuffer({ url: rawUrl.toString() });
|
||||
res.set('Content-Type', 'image/png');
|
||||
res.set('Content-Disposition', 'attachment; filename="mindspace-public-page.long.png"');
|
||||
res.set('Cache-Control', 'no-store');
|
||||
return res.send(image);
|
||||
} catch (error) {
|
||||
return res
|
||||
.status(500)
|
||||
.type('text/plain; charset=utf-8')
|
||||
.send(`长图生成失败:${error?.message || '未知错误'}`);
|
||||
}
|
||||
}
|
||||
const canWrapWithShell = !embed && !raw && isFullHtml && result.publication?.accessMode !== 'password';
|
||||
if (canWrapWithShell) {
|
||||
const title = detectPublishedPageTitle(html);
|
||||
const rawUrl = appendQueryParam(req.originalUrl || req.url || '', 'view', 'raw');
|
||||
const rawUrl = appendQueryParam(sharePath || originalPath, 'view', 'raw');
|
||||
const longImageUrl = appendQueryParam(sharePath || originalPath, 'download', 'long-image');
|
||||
let shellHtml = publishedPageShellHtml({
|
||||
iframeUrl: rawUrl,
|
||||
shareUrl: pageUrl,
|
||||
title,
|
||||
longImageUrl,
|
||||
});
|
||||
try {
|
||||
shellHtml = injectOgTags(shellHtml, {
|
||||
@@ -5019,7 +5013,7 @@ async function resolvePublishedRoute(req, res, password = null) {
|
||||
referrer: req.get('referer'),
|
||||
},
|
||||
);
|
||||
return sendPublishedPage(req, res, result, {
|
||||
return await sendPublishedPage(req, res, result, {
|
||||
embed: isPlazaEmbedRequest(req.query),
|
||||
raw: String(req.query.view ?? '').toLowerCase() === 'raw',
|
||||
});
|
||||
@@ -5098,7 +5092,7 @@ app.get('/s/:token', async (req, res) => {
|
||||
if (!mindSpacePublications) return res.status(503).send('MindSpace 未启用');
|
||||
try {
|
||||
const viewer = req.userSession && userAuth ? await userAuth.getMe(req.userToken) : null;
|
||||
return sendPublishedPage(
|
||||
return await sendPublishedPage(
|
||||
req,
|
||||
res,
|
||||
await mindSpacePublications.resolvePrivateLink(req.params.token, viewer?.id, {
|
||||
@@ -5136,13 +5130,32 @@ async function resolvePublishDirKey(segment) {
|
||||
* Send a file, injecting Open Graph tags for .html so forwarded links unfurl with a cover.
|
||||
* Non-HTML files (assets, etc.) are streamed unchanged via res.sendFile.
|
||||
*/
|
||||
function sendPublishFile(req, res, filePath) {
|
||||
async function sendLongImageDownloadIfRequested(req, res, filePath) {
|
||||
if (!isLongImageDownloadRequest(req.query)) return false;
|
||||
const longImagePath = longImagePathForHtml(filePath);
|
||||
try {
|
||||
await renderLongImage({ htmlPath: filePath, outputPath: longImagePath });
|
||||
res.set('Cache-Control', 'no-store');
|
||||
res.download(longImagePath, path.basename(longImagePath), (err) => {
|
||||
if (err && !res.headersSent) res.status(404).json({ message: '长图文件不存在' });
|
||||
});
|
||||
} catch (error) {
|
||||
res
|
||||
.status(500)
|
||||
.type('text/plain; charset=utf-8')
|
||||
.send(`长图生成失败:${error?.message || '未知错误'}`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function sendPublishFile(req, res, filePath) {
|
||||
if (!filePath.toLowerCase().endsWith('.html')) {
|
||||
res.sendFile(filePath, (err) => {
|
||||
if (err && !res.headersSent) res.status(404).json({ message: '文件不存在' });
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (await sendLongImageDownloadIfRequested(req, res, filePath)) return;
|
||||
let html;
|
||||
try {
|
||||
html = fs.readFileSync(filePath, 'utf8');
|
||||
@@ -5320,7 +5333,7 @@ async function serveUserPublishFile(req, res, next) {
|
||||
}
|
||||
const recoveredPublicHtml = await recoverMisplacedPublicHtml(targetDir, resolvedRoot, rest);
|
||||
if (recoveredPublicHtml) {
|
||||
sendPublishFile(req, res, recoveredPublicHtml);
|
||||
await sendPublishFile(req, res, recoveredPublicHtml);
|
||||
return;
|
||||
}
|
||||
res.status(404).json({ message: '文件不存在' });
|
||||
@@ -5330,14 +5343,14 @@ async function serveUserPublishFile(req, res, next) {
|
||||
if (fs.statSync(resolvedPath).isDirectory()) {
|
||||
const indexPath = path.join(resolvedPath, 'index.html');
|
||||
if (fs.existsSync(indexPath)) {
|
||||
sendPublishFile(req, res, indexPath);
|
||||
await sendPublishFile(req, res, indexPath);
|
||||
return;
|
||||
}
|
||||
res.status(404).json({ message: '目录中没有 index.html' });
|
||||
return;
|
||||
}
|
||||
|
||||
sendPublishFile(req, res, resolvedPath);
|
||||
await sendPublishFile(req, res, resolvedPath);
|
||||
}
|
||||
|
||||
app.use(`/${PUBLISH_ROOT_DIR}`, async (req, res, next) => {
|
||||
|
||||
@@ -18,6 +18,7 @@ export const DEFAULT_USER_SKILLS = {
|
||||
'table-viewer': true,
|
||||
'product-campaign-page': true,
|
||||
'docx-generate': true,
|
||||
'long-image-download': true,
|
||||
[PUBLISH_SKILL_NAME]: false,
|
||||
};
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ test('lists static-page-publish in platform catalog', () => {
|
||||
assert.ok(catalog.some((item) => item.name === 'static-page-publish'));
|
||||
assert.ok(catalog.some((item) => item.name === 'schedule-assistant'));
|
||||
assert.ok(catalog.some((item) => item.name === 'product-campaign-page'));
|
||||
assert.ok(catalog.some((item) => item.name === 'long-image-download'));
|
||||
});
|
||||
|
||||
test('granting publish skill enables static_publish capability', () => {
|
||||
@@ -50,5 +51,6 @@ test('DEFAULT_USER_SKILLS enables common platform skills', () => {
|
||||
assert.equal(DEFAULT_USER_SKILLS['form-builder'], true);
|
||||
assert.equal(DEFAULT_USER_SKILLS['table-viewer'], true);
|
||||
assert.equal(DEFAULT_USER_SKILLS['product-campaign-page'], true);
|
||||
assert.equal(DEFAULT_USER_SKILLS['long-image-download'], true);
|
||||
assert.equal(DEFAULT_USER_SKILLS['static-page-publish'], false);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
name: long-image-download
|
||||
description: 用 Playwright 将 MindSpace HTML 页面生成同名 PNG 长图,并返回可打开图片链接与附件下载链接
|
||||
---
|
||||
|
||||
# 长图下载(Playwright)
|
||||
|
||||
## 何时使用
|
||||
|
||||
- 用户要求“长图下载”“保存长图”“页面图片格式”“整页 PNG”
|
||||
- 已经或即将用 `static-page-publish` 生成 `public/*.html`
|
||||
|
||||
## 必须流程
|
||||
|
||||
1. 先确保 HTML 已经用 `write_file` / `edit_file` 写入 `public/<页面名>.html`
|
||||
2. 调用 sandbox-fs 工具 `generate_long_image`:
|
||||
|
||||
```json
|
||||
{
|
||||
"html_path": "public/report.html",
|
||||
"output_path": "public/report.long.png"
|
||||
}
|
||||
```
|
||||
|
||||
3. 用 `list_dir public` 确认 `<页面名>.long.png` 已存在
|
||||
4. 回复用户时同时给:
|
||||
- 页面链接:`[页面标题](.../public/report.html)`
|
||||
- 长图预览链接:`[长图预览](.../public/report.long.png)`
|
||||
- 长图下载链接:`[下载长图](.../public/report.html?download=long-image)`
|
||||
|
||||
## 规则
|
||||
|
||||
- `generate_long_image` 使用平台 Playwright,不要用 shell、自写截图脚本、`html2canvas` 或 `.thumbnail.svg` 冒充长图
|
||||
- `.thumbnail.svg` 只是信息流封面,不是整页长图
|
||||
- 长图文件名推荐与 HTML 同名:`report.html` -> `report.long.png`
|
||||
- 没有确认 `.long.png` 已生成时,不要声称“长图已生成”
|
||||
@@ -23,6 +23,7 @@ description: 在专属 MindSpace 目录生成可公开访问的静态 HTML 报
|
||||
6. 静态文件保存即可访问,**无需重启**
|
||||
7. 页面需提供 **Word/PDF 等二进制下载** 时:文件单独落盘(如 `public/方案.docx`),链接用相对路径;**禁止**在 HTML 内用 `data:...;base64,...` 嵌入 docx(易截断损坏)
|
||||
8. 页面若提供 **Word/PDF/附件下载**,相对链接指向的文件必须与 HTML **同目录(或子目录)且真实存在**;改 HTML 文件名时同步 **rename/copy** 伴生文件
|
||||
9. 页面需提供**长图下载**时:必须先 `load_skill` → `long-image-download`,调用 `generate_long_image` 生成同目录 `public/<页面名>.long.png`,再返回长图预览与下载链接;禁止把 `.thumbnail.svg` 当成长图
|
||||
|
||||
详细约束以工作区内的 `.goosehints` 与 `.agents/skills/static-page-publish/SKILL.md` 为准。
|
||||
|
||||
@@ -34,6 +35,7 @@ description: 在专属 MindSpace 目录生成可公开访问的静态 HTML 报
|
||||
4. 保存后服务端**立即**生成 `<文件名>.thumbnail.svg`(Agent 交互阶段即生效)
|
||||
5. 按「回复格式」返回**可点击**公网链接
|
||||
6. 若页面含下载按钮,必须用 `generate_docx`(sandbox-fs 工具)生成 `public/<同名>.docx`,再确认链接目标已落盘
|
||||
7. 若用户要求长图下载,必须用 `long-image-download` 生成 `public/<同名>.long.png`,并确认文件存在
|
||||
|
||||
## 伴生下载文件(必须)
|
||||
|
||||
@@ -59,6 +61,7 @@ description: 在专属 MindSpace 目录生成可公开访问的静态 HTML 报
|
||||
- 标题用页面真实主题名
|
||||
- 可同时给出相对路径(如 `public/malaysia-travel-guide.html`)
|
||||
- 说明:保存即生效,无需重启
|
||||
- 若生成了长图,同时给 `[长图预览](.../public/malaysia-travel-guide.long.png)` 和 `[下载长图](.../public/malaysia-travel-guide.html?download=long-image)`
|
||||
|
||||
## 信息流预览图(必须)
|
||||
|
||||
|
||||
+28
-2
@@ -6,10 +6,12 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export const PUBLISH_SKILL_NAME = 'static-page-publish';
|
||||
export const DOCX_SKILL_NAME = 'docx-generate';
|
||||
export const LONG_IMAGE_SKILL_NAME = 'long-image-download';
|
||||
export const PUBLISH_ROOT_DIR = 'MindSpace';
|
||||
export const PUBLIC_ZONE_DIR = 'public';
|
||||
export const PUBLISH_SKILL_DIR = path.join(__dirname, 'skills', PUBLISH_SKILL_NAME);
|
||||
export const DOCX_SKILL_DIR = path.join(__dirname, 'skills', DOCX_SKILL_NAME);
|
||||
export const LONG_IMAGE_SKILL_DIR = path.join(__dirname, 'skills', LONG_IMAGE_SKILL_NAME);
|
||||
export const WORKSPACE_HINTS_FILENAME = '.tkmindhints';
|
||||
export const LEGACY_WORKSPACE_HINTS_FILENAME = '.goosehints';
|
||||
|
||||
@@ -141,7 +143,7 @@ description: 在专属 MindSpace 目录生成可公开访问的静态 HTML 报
|
||||
|
||||
1. **唯一可写目录**:\`${publishDir}\`
|
||||
2. **禁止**使用绝对路径(如 \`/Users/...\`、\`../\` 跳出目录)
|
||||
3. **允许**在本目录内使用 \`write_file\`、\`edit_file\`、\`read_file\`、\`list_dir\`、\`generate_docx\`;**禁止**访问此目录外的路径(含 \`../\`、其它用户目录、项目根目录;系统会在 OS 层拦截越界访问)
|
||||
3. **允许**在本目录内使用 \`write_file\`、\`edit_file\`、\`read_file\`、\`list_dir\`、\`generate_docx\`、\`generate_long_image\`;**禁止**访问此目录外的路径(含 \`../\`、其它用户目录、项目根目录;系统会在 OS 层拦截越界访问)
|
||||
4. **禁止**子 Agent、扩展管理、修改工作区外文件
|
||||
5. 页面默认写入 \`public/\` 分区,使用相对路径如 \`public/report.html\`、\`public/assets/chart.png\`
|
||||
6. 生成 HTML 后,向用户提供可访问链接,格式:
|
||||
@@ -156,7 +158,8 @@ description: 在专属 MindSpace 目录生成可公开访问的静态 HTML 报
|
||||
4. 页面内资源使用**相对路径**(\`assets/foo.png\`),不要用磁盘绝对路径
|
||||
5. 保存 HTML 后,服务端会**立即**生成同名预览图 \`<文件名>.thumbnail.svg\`(Agent 交互阶段即生效,无需等用户保存到「我的空间」)
|
||||
6. 下载按钮的相对链接目标(如 \`public/report.docx\`)必须通过 \`generate_docx\` 落盘,且 basename 与 HTML 一致
|
||||
7. 完成后按「回复格式」返回可点击链接
|
||||
7. 若用户要求长图下载,必须先 \`load_skill\` → \`long-image-download\`,调用 \`generate_long_image\` 生成并确认 \`public/<页面名>.long.png\` 已存在
|
||||
8. 完成后按「回复格式」返回可点击链接
|
||||
|
||||
## 伴生下载文件(必须)
|
||||
|
||||
@@ -186,6 +189,7 @@ description: 在专属 MindSpace 目录生成可公开访问的静态 HTML 报
|
||||
- 标题用页面真实主题(不要用「点击这里」「链接」)
|
||||
- 可同时给出本地相对路径(如 \`public/pilates-618.html\`)与公网链接
|
||||
- 说明:静态文件保存即生效,**无需重启**
|
||||
- 若生成了长图,同时给 \`[长图预览](.../public/pilates-618.long.png)\` 和 \`[下载长图](.../public/pilates-618.html?download=long-image)\`
|
||||
|
||||
## 信息流预览图(必须)
|
||||
|
||||
@@ -241,6 +245,7 @@ description: 在专属 MindSpace 目录生成可公开访问的静态 HTML 报
|
||||
- shell 仅用于本目录内整理文件/简单脚本;不要 \`rm -rf\` 越界路径、不要安装系统级依赖
|
||||
- **禁止**在 HTML 中用 \`data:...;base64,...\` 内嵌 Word/PDF;二进制文件单独落盘后用相对路径链接(见 \`docx-generate\` 技能)
|
||||
- 用户要求 Word / docx 下载时,禁止跳过 \`docx-generate\` 直接在 HTML 中伪造下载链接
|
||||
- 用户要求长图下载时,禁止跳过 \`long-image-download\` 或把 \`.thumbnail.svg\` 当成长图
|
||||
|
||||
## 示例
|
||||
|
||||
@@ -282,6 +287,13 @@ ${renderBrandingBlock(addressName)}
|
||||
- 若用户要求页面里可下载 Word / docx:必须先 \`load_skill\` → \`docx-generate\`
|
||||
- 先生成并确认 \`public/*.docx\` 已存在,再写 \`public/*.html\`
|
||||
- HTML 中只能用同目录相对路径链接该文档,禁止只写下载按钮却没有先把 \`.docx\` 落盘
|
||||
|
||||
## 长图下载
|
||||
|
||||
- 若用户要求长图下载:必须先 \`load_skill\` → \`long-image-download\`
|
||||
- 调用 \`generate_long_image\` 生成 \`public/<页面名>.long.png\`
|
||||
- 先确认 \`public/*.long.png\` 已存在,再回复 \`[长图预览](...long.png)\` 和 \`[下载长图](...html?download=long-image)\`
|
||||
- \`.thumbnail.svg\` 只是信息流封面,不能当成长图
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -322,11 +334,13 @@ export function buildSandboxSessionConstraints({ baseConstraints, developerTools
|
||||
'- 你有 write_file/edit_file 工具:**必须由你**写入 `public/xxx.html`(或工作区根目录 `.html`)',
|
||||
'- 开始前执行 load_skill → `static-page-publish`,按技能说明写入 mindspace-cover 元数据',
|
||||
'- 如果用户要求 Word / docx 下载:先 `load_skill` → `docx-generate`,生成并确认 `public/*.docx` 已存在,再写 HTML 用相对路径链接',
|
||||
'- 如果用户要求长图下载:先 `load_skill` → `long-image-download`,调用 `generate_long_image` 生成并确认 `public/*.long.png` 已存在,再回复长图预览与下载链接',
|
||||
'- **禁止**用 shell / cat / heredoc / echo / cp 写入 HTML;shell 在容器内执行,文件不会出现在公网 MindSpace 路径',
|
||||
'- 用 `apps__create_app` 设计页面时,最后仍要按 `static-page-publish` skill 把内容 write_file 落到 `public/`,才有公网链接',
|
||||
'- **禁止**让用户手动保存到 public 或说无法生成页面(除非 write_file 调用失败)',
|
||||
'- 完成后回复 `[页面标题](公网URL)` 可点击链接;写入 `public/` 时 URL 必须含 `/public/` 路径段;按本会话给出的公网前缀模板拼接真实地址',
|
||||
'- 下载附件:Word 用 `generate_docx` 写入 `public/*.docx`,相对链接文件必须已在 `public/` 落盘,basename 与 HTML 一致',
|
||||
'- 长图附件:用 `generate_long_image` 写入 `public/*.long.png`,不要把 `.thumbnail.svg` 当成长图',
|
||||
);
|
||||
return lines.join('\n');
|
||||
}
|
||||
@@ -346,6 +360,7 @@ export function buildPublishConstraints({ slug, username, publicBaseUrl, publish
|
||||
'- **路径规则**:只用相对路径;禁止 `../`;工作区外的路径会被系统拒绝(OS 层强制,非软约束)',
|
||||
'- **生成页面(必须亲自完成)**:先 `load_skill` → `static-page-publish`,再用 `write_file`/`edit_file` 写入 `public/页面.html`',
|
||||
'- **Word 下载页(必须亲自完成)**:若用户要求 Word / docx 下载,必须先 `load_skill` → `docx-generate`,生成并确认 `public/*.docx` 已存在,再在 HTML 中用相对路径链接',
|
||||
'- **长图下载(必须亲自完成)**:若用户要求长图下载,必须先 `load_skill` → `long-image-download`,调用 `generate_long_image` 生成并确认 `public/*.long.png` 已存在',
|
||||
'- **用户可见回复**:不要向用户复述 load_skill、技能更新、页脚标记、mindspace-cover 等内部实现;完成后直接给出页面链接或结果',
|
||||
'- **禁止**用 shell 写入 HTML;**禁止**让用户「手动保存到 public 目录」或说「我无法生成页面」——除非 write_file 已失败并报告错误',
|
||||
'- 完成后给出 Markdown 可点击公网链接 `[标题](URL)`;写入 `public/页面.html` 时 URL 为 `.../MindSpace/<用户ID>/public/页面.html`',
|
||||
@@ -354,6 +369,7 @@ export function buildPublishConstraints({ slug, username, publicBaseUrl, publish
|
||||
'- **禁止**让用户「手动保存到 public 目录」或说「我无法生成页面」——除非 write_file 已失败并报告错误',
|
||||
'- 完成后给出 Markdown 可点击公网链接 `[标题](URL)`;写入 `public/页面.html` 时 URL 为 `.../MindSpace/<用户ID>/public/页面.html`,按模板拼真实地址',
|
||||
'- 下载链接(如 `report.docx`)必须与 HTML 同目录且文件名一致;Word 必须用 sandbox-fs `generate_docx` 生成,不要用 `computercontroller` / shell 作为交付依据',
|
||||
'- 长图链接:生成 `report.long.png` 后给 `[长图预览](.../report.long.png)`,下载用 `[下载长图](.../report.html?download=long-image)`',
|
||||
`- 发布技能:\`${PUBLISH_SKILL_NAME}\`(生成页面前应 load_skill)`,
|
||||
].join('\n');
|
||||
}
|
||||
@@ -378,8 +394,18 @@ export function ensureDocxSkillInstalled(publishDir) {
|
||||
return path.join(skillRoot, 'SKILL.md');
|
||||
}
|
||||
|
||||
export function ensureLongImageSkillInstalled(publishDir) {
|
||||
if (!fs.existsSync(LONG_IMAGE_SKILL_DIR)) return null;
|
||||
const skillRoot = path.join(publishDir, '.agents', 'skills', LONG_IMAGE_SKILL_NAME);
|
||||
fs.rmSync(skillRoot, { recursive: true, force: true });
|
||||
fs.mkdirSync(path.dirname(skillRoot), { recursive: true });
|
||||
fs.cpSync(LONG_IMAGE_SKILL_DIR, skillRoot, { recursive: true });
|
||||
return path.join(skillRoot, 'SKILL.md');
|
||||
}
|
||||
|
||||
export function ensurePublishSkillInstalled(publishDir, context) {
|
||||
ensureDocxSkillInstalled(publishDir);
|
||||
ensureLongImageSkillInstalled(publishDir);
|
||||
const skillRoot = path.join(publishDir, '.agents', 'skills', PUBLISH_SKILL_NAME);
|
||||
fs.mkdirSync(skillRoot, { recursive: true });
|
||||
const skillPath = path.join(skillRoot, 'SKILL.md');
|
||||
|
||||
@@ -68,19 +68,25 @@ test('publish dir and public url use stable user id', () => {
|
||||
});
|
||||
const skillPath = path.join(layout.publishDir, '.agents', 'skills', 'static-page-publish', 'SKILL.md');
|
||||
const docxSkillPath = path.join(layout.publishDir, '.agents', 'skills', 'docx-generate', 'SKILL.md');
|
||||
const longImageSkillPath = path.join(layout.publishDir, '.agents', 'skills', 'long-image-download', 'SKILL.md');
|
||||
const docxScriptPath = path.join(layout.publishDir, '.agents', 'skills', 'docx-generate', 'generate_docx.py');
|
||||
assert.ok(fs.existsSync(skillPath));
|
||||
assert.ok(fs.existsSync(docxSkillPath));
|
||||
assert.ok(fs.existsSync(longImageSkillPath));
|
||||
assert.ok(fs.existsSync(docxScriptPath));
|
||||
const skillText = fs.readFileSync(skillPath, 'utf8');
|
||||
const docxSkillText = fs.readFileSync(docxSkillPath, 'utf8');
|
||||
const longImageSkillText = fs.readFileSync(longImageSkillPath, 'utf8');
|
||||
assert.match(skillText, new RegExp(`m\\.tkmind\\.cn/${PUBLISH_ROOT_DIR}/${USER_ID}/${PUBLIC_ZONE_DIR}/`));
|
||||
assert.match(skillText, /public\/report\.html/);
|
||||
assert.match(skillText, /\[.*\]\(.*\)/);
|
||||
assert.match(skillText, /mindspace-cover/);
|
||||
assert.match(skillText, /\.thumbnail\.svg/);
|
||||
assert.match(skillText, /docx-generate/);
|
||||
assert.match(skillText, /long-image-download/);
|
||||
assert.match(skillText, /generate_long_image/);
|
||||
assert.match(docxSkillText, /public\/文件名\.docx/);
|
||||
assert.match(longImageSkillText, /public\/report\.long\.png/);
|
||||
assert.match(skillText, /report\.docx|伴生下载/);
|
||||
});
|
||||
|
||||
@@ -114,6 +120,8 @@ test('buildPublishConstraints scopes default search to user workspace', () => {
|
||||
assert.match(constraints, /public\/页面\.html/);
|
||||
assert.match(constraints, /docx-generate/);
|
||||
assert.match(constraints, /public\/\*\.docx/);
|
||||
assert.match(constraints, /long-image-download/);
|
||||
assert.match(constraints, /generate_long_image/);
|
||||
assert.match(constraints, /\/public\//);
|
||||
});
|
||||
|
||||
@@ -140,6 +148,8 @@ test('renderWorkspaceHints includes docx download guidance for published pages',
|
||||
});
|
||||
assert.match(text, /docx-generate/);
|
||||
assert.match(text, /public\/\*\.docx/);
|
||||
assert.match(text, /long-image-download/);
|
||||
assert.match(text, /generate_long_image/);
|
||||
});
|
||||
|
||||
test('isPathInsidePublishDir blocks escape', () => {
|
||||
|
||||
Reference in New Issue
Block a user