4e21ca937a
Deploy Documentation / deploy (push) Has been cancelled
Canary / Prepare Version (push) Has been cancelled
Canary / build-cli (push) Has been cancelled
Canary / Upload Install Script (push) Has been cancelled
Canary / bundle-desktop (push) Has been cancelled
Canary / bundle-desktop-intel (push) Has been cancelled
Canary / bundle-desktop-linux (push) Has been cancelled
Canary / bundle-desktop-windows (push) Has been cancelled
Canary / bundle-desktop-windows-cuda (push) Has been cancelled
Canary / Release (push) Has been cancelled
Unused Dependencies / machete (push) Has been cancelled
CI / changes (push) Has been cancelled
CI / Check Rust Code Format (push) Has been cancelled
CI / Build and Test Rust Project (push) Has been cancelled
CI / Build Rust Project on Windows (push) Has been cancelled
CI / Check MSRV (push) Has been cancelled
CI / Lint Rust Code (push) Has been cancelled
CI / Check Generated Schemas are Up-to-Date (push) Has been cancelled
CI / Test and Lint Electron Desktop App (push) Has been cancelled
CI / H5 Plaza Tests and Build (push) Has been cancelled
Live Provider Tests / check-fork (push) Has been cancelled
Live Provider Tests / changes (push) Has been cancelled
Live Provider Tests / Build Binary (push) Has been cancelled
Live Provider Tests / Smoke Tests (push) Has been cancelled
Live Provider Tests / Smoke Tests (Code Execution) (push) Has been cancelled
Live Provider Tests / Compaction Tests (push) Has been cancelled
Live Provider Tests / goose server HTTP integration tests (push) Has been cancelled
Publish Ask AI Bot Docker Image / docker (push) Has been cancelled
Publish Docker Image / docker (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Fork goose with custom MCP widgets, platform extensions (aider, git, web, search), MindSpace H5 backend/frontend, Plaza/Ops UIs, and deploy scripts for tkmind.cn. Co-authored-by: Cursor <cursoragent@cursor.com>
255 lines
8.7 KiB
JavaScript
255 lines
8.7 KiB
JavaScript
import zlib from 'node:zlib';
|
|
|
|
const PREVIEWABLE_MIME_TYPES = new Set([
|
|
'text/html',
|
|
'text/plain',
|
|
'text/markdown',
|
|
'text/csv',
|
|
'application/pdf',
|
|
'image/png',
|
|
'image/jpeg',
|
|
'image/webp',
|
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
]);
|
|
|
|
const PREVIEW_SHELL_STYLE = `
|
|
html,body{margin:0;padding:0;background:#f5f0e5;color:#1f2937;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
|
|
body{padding:24px;box-sizing:border-box;line-height:1.6}
|
|
pre,code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
|
|
pre{white-space:pre-wrap;word-break:break-word;background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:16px}
|
|
table{border-collapse:collapse;width:100%;background:#fff;border-radius:12px;overflow:hidden}
|
|
th,td{border:1px solid #e5e7eb;padding:8px 10px;text-align:left;font-size:14px}
|
|
th{background:#faf7ef}
|
|
.docx-preview h1{font-size:1.5rem;margin:0 0 16px}
|
|
.docx-preview p{margin:0 0 12px;text-indent:2em}
|
|
.docx-preview .meta{color:#6b7280;font-size:13px;margin-bottom:20px;text-indent:0}
|
|
.pdf-frame,.image-frame{display:block;width:100%;min-height:calc(100vh - 48px);border:0;border-radius:12px;background:#fff}
|
|
.image-frame{object-fit:contain;max-height:calc(100vh - 48px);width:auto;max-width:100%;margin:0 auto}
|
|
`;
|
|
|
|
function escapeHtml(text) {
|
|
return String(text ?? '')
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"');
|
|
}
|
|
|
|
function previewDocument(title, bodyHtml, { downloadUrl = null, extraHead = '' } = {}) {
|
|
const csp = [
|
|
"default-src 'none'",
|
|
"style-src 'unsafe-inline'",
|
|
"img-src 'self' data: https:",
|
|
"font-src 'self' data:",
|
|
"frame-src 'self'",
|
|
"object-src 'self'",
|
|
"base-uri 'none'",
|
|
"form-action 'none'",
|
|
"script-src 'none'",
|
|
].join('; ');
|
|
const downloadLink = downloadUrl
|
|
? `<p class="meta"><a href="${escapeHtml(downloadUrl)}" target="_blank" rel="noopener noreferrer">下载原文件</a></p>`
|
|
: '';
|
|
return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta http-equiv="Content-Security-Policy" content="${csp}"><title>${escapeHtml(title)}</title><style>${PREVIEW_SHELL_STYLE}</style>${extraHead}</head><body><h1>${escapeHtml(title)}</h1>${downloadLink}${bodyHtml}</body></html>`;
|
|
}
|
|
|
|
function renderInlineMarkdown(text) {
|
|
let html = escapeHtml(text);
|
|
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
|
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
|
html = html.replace(/\[([^\]]+)]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
|
|
return html;
|
|
}
|
|
|
|
function renderMarkdownDocument(text) {
|
|
const lines = String(text ?? '').split('\n');
|
|
const blocks = [];
|
|
let inCode = false;
|
|
let code = [];
|
|
for (const line of lines) {
|
|
if (line.startsWith('```')) {
|
|
if (inCode) {
|
|
blocks.push(`<pre><code>${escapeHtml(code.join('\n'))}</code></pre>`);
|
|
code = [];
|
|
inCode = false;
|
|
} else {
|
|
inCode = true;
|
|
}
|
|
continue;
|
|
}
|
|
if (inCode) {
|
|
code.push(line);
|
|
continue;
|
|
}
|
|
if (!line.trim()) continue;
|
|
const heading = line.match(/^(#{1,6})\s+(.+)$/);
|
|
if (heading) {
|
|
const level = heading[1].length;
|
|
blocks.push(`<h${level}>${renderInlineMarkdown(heading[2])}</h${level}>`);
|
|
continue;
|
|
}
|
|
blocks.push(`<p>${renderInlineMarkdown(line)}</p>`);
|
|
}
|
|
if (inCode && code.length) {
|
|
blocks.push(`<pre><code>${escapeHtml(code.join('\n'))}</code></pre>`);
|
|
}
|
|
return blocks.join('\n');
|
|
}
|
|
|
|
function parseCsvRows(text) {
|
|
const rows = [];
|
|
let row = [];
|
|
let cell = '';
|
|
let inQuotes = false;
|
|
for (let i = 0; i < text.length; i += 1) {
|
|
const ch = text[i];
|
|
const next = text[i + 1];
|
|
if (inQuotes) {
|
|
if (ch === '"' && next === '"') {
|
|
cell += '"';
|
|
i += 1;
|
|
} else if (ch === '"') {
|
|
inQuotes = false;
|
|
} else {
|
|
cell += ch;
|
|
}
|
|
continue;
|
|
}
|
|
if (ch === '"') {
|
|
inQuotes = true;
|
|
continue;
|
|
}
|
|
if (ch === ',') {
|
|
row.push(cell);
|
|
cell = '';
|
|
continue;
|
|
}
|
|
if (ch === '\n') {
|
|
row.push(cell);
|
|
rows.push(row);
|
|
row = [];
|
|
cell = '';
|
|
continue;
|
|
}
|
|
if (ch === '\r') continue;
|
|
cell += ch;
|
|
}
|
|
row.push(cell);
|
|
rows.push(row);
|
|
return rows.filter((item) => item.some((value) => String(value ?? '').trim()));
|
|
}
|
|
|
|
function renderCsvPreview(text) {
|
|
const rows = parseCsvRows(String(text ?? ''));
|
|
if (rows.length === 0) return '<pre>(空文件)</pre>';
|
|
const [head, ...body] = rows;
|
|
const header = `<thead><tr>${head.map((cell) => `<th>${escapeHtml(cell)}</th>`).join('')}</tr></thead>`;
|
|
const content = body
|
|
.slice(0, 200)
|
|
.map((line) => `<tr>${line.map((cell) => `<td>${escapeHtml(cell)}</td>`).join('')}</tr>`)
|
|
.join('');
|
|
const tail = body.length > 200 ? `<p class="meta">仅展示前 200 行</p>` : '';
|
|
return `<div style="overflow:auto">${tail}<table>${header}<tbody>${content}</tbody></table></div>`;
|
|
}
|
|
|
|
function extractZipEntry(buffer, targetName) {
|
|
let offset = 0;
|
|
while (offset + 30 <= buffer.length) {
|
|
if (buffer.subarray(offset, offset + 2).toString('ascii') !== 'PK') break;
|
|
const compressionMethod = buffer.readUInt16LE(offset + 8);
|
|
const compressedSize = buffer.readUInt32LE(offset + 18);
|
|
const nameLength = buffer.readUInt16LE(offset + 26);
|
|
const extraLength = buffer.readUInt16LE(offset + 28);
|
|
const name = buffer.subarray(offset + 30, offset + 30 + nameLength).toString('utf8');
|
|
const dataStart = offset + 30 + nameLength + extraLength;
|
|
if (name === targetName) {
|
|
const compressed = buffer.subarray(dataStart, dataStart + compressedSize);
|
|
if (compressionMethod === 0) return compressed;
|
|
if (compressionMethod === 8) return zlib.inflateRawSync(compressed);
|
|
return null;
|
|
}
|
|
offset = dataStart + compressedSize;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function extractDocxText(buffer) {
|
|
const xmlBuffer = extractZipEntry(buffer, 'word/document.xml');
|
|
if (!xmlBuffer) return '';
|
|
const xml = xmlBuffer.toString('utf8');
|
|
const paragraphs = [];
|
|
for (const block of xml.split('</w:p>')) {
|
|
const texts = [...block.matchAll(/<w:t[^>]*>([\s\S]*?)<\/w:t>/g)].map((match) =>
|
|
match[1]
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/&/g, '&')
|
|
.replace(/"/g, '"'),
|
|
);
|
|
const line = texts.join('');
|
|
if (line.trim()) paragraphs.push(line.trim());
|
|
}
|
|
return paragraphs.join('\n\n');
|
|
}
|
|
|
|
export function canPreviewAsset(mimeType) {
|
|
return PREVIEWABLE_MIME_TYPES.has(mimeType);
|
|
}
|
|
|
|
export function renderAssetPreviewHtml({ asset, buffer, downloadUrl }) {
|
|
const title = asset.displayName || asset.filename;
|
|
const mimeType = asset.mimeType;
|
|
|
|
if (mimeType === 'text/html') {
|
|
const csp =
|
|
'<meta http-equiv="Content-Security-Policy" content="default-src \'none\'; style-src \'unsafe-inline\' \'self\'; img-src data: https:; font-src \'self\' data:; base-uri \'none\'; form-action \'none\'; script-src \'none\'">';
|
|
const html = buffer.toString('utf8');
|
|
if (/<head[^>]*>/i.test(html)) {
|
|
return html.replace(/<head([^>]*)>/i, `<head$1>${csp}`);
|
|
}
|
|
return `<!doctype html><html lang="zh-CN"><head>${csp}</head><body>${html}</body></html>`;
|
|
}
|
|
|
|
if (mimeType === 'application/pdf') {
|
|
return previewDocument(
|
|
title,
|
|
`<iframe class="pdf-frame" src="${escapeHtml(downloadUrl)}" title="${escapeHtml(title)}"></iframe>`,
|
|
{ downloadUrl },
|
|
);
|
|
}
|
|
|
|
if (mimeType.startsWith('image/')) {
|
|
return previewDocument(
|
|
title,
|
|
`<img class="image-frame" src="${escapeHtml(downloadUrl)}" alt="${escapeHtml(title)}">`,
|
|
{ downloadUrl },
|
|
);
|
|
}
|
|
|
|
if (mimeType === 'text/csv') {
|
|
return previewDocument(title, renderCsvPreview(buffer.toString('utf8')), { downloadUrl });
|
|
}
|
|
|
|
if (mimeType === 'text/markdown') {
|
|
return previewDocument(title, renderMarkdownDocument(buffer.toString('utf8')), { downloadUrl });
|
|
}
|
|
|
|
if (mimeType === 'text/plain') {
|
|
return previewDocument(title, `<pre>${escapeHtml(buffer.toString('utf8'))}</pre>`, { downloadUrl });
|
|
}
|
|
|
|
if (mimeType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
|
|
const text = extractDocxText(buffer);
|
|
const body = text
|
|
? `<article class="docx-preview">${text
|
|
.split(/\n{2,}/)
|
|
.map((paragraph) => `<p>${escapeHtml(paragraph)}</p>`)
|
|
.join('')}</article>`
|
|
: '<p class="meta">无法提取正文,请下载原文件查看。</p>';
|
|
return previewDocument(title, body, { downloadUrl });
|
|
}
|
|
|
|
throw Object.assign(new Error('该资产不支持预览'), { code: 'preview_not_supported' });
|
|
}
|