b0f5d6a51c
Split platform admin and ops APIs into standalone admin-server.mjs with network guards; simplify billing to RMB token pricing, refactor user auth, and add rsync deploy plus local-test scripts and docs. Co-authored-by: Cursor <cursoragent@cursor.com>
166 lines
6.4 KiB
JavaScript
166 lines
6.4 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import {
|
|
buildFeedThumbnailSvg,
|
|
buildThumbnailSvg,
|
|
ensureHtmlThumbnail,
|
|
extractCoverSignals,
|
|
generateHtmlThumbnail,
|
|
isModernFeedThumbnail,
|
|
resolveCoverDataUri,
|
|
shouldUseScenicBackground,
|
|
} from './mindspace-thumbnails.mjs';
|
|
import fs from 'node:fs/promises';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
|
|
test('buildThumbnailSvg escapes title text', () => {
|
|
const svg = buildThumbnailSvg({
|
|
title: '<script>alert(1)</script>',
|
|
subtitle: '测试',
|
|
accent: '#2f6f57',
|
|
});
|
|
assert.doesNotMatch(svg, /<script>alert/);
|
|
assert.match(svg, /<script>alert/);
|
|
});
|
|
|
|
test('generateHtmlThumbnail writes svg file', async () => {
|
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'mindspace-thumb-'));
|
|
const key = 'users/u1/assets/a1/thumbnail.svg';
|
|
await generateHtmlThumbnail(
|
|
root,
|
|
key,
|
|
'<!doctype html><html><head><title>测试页面</title></head><body></body></html>',
|
|
{ subtitle: 'HTML' },
|
|
);
|
|
const written = await fs.readFile(path.join(root, key), 'utf8');
|
|
assert.match(written, /测试页面/);
|
|
});
|
|
|
|
test('extractCoverSignals reads mindspace-cover meta and html colors', () => {
|
|
const html = `<!doctype html><html><head>
|
|
<title>🇲🇾 马来西亚旅游攻略</title>
|
|
<meta name="mindspace-cover" content='{"tag":"旅行","accent":"#ff6b35","emoji":"🇲🇾"}' />
|
|
<style>body { background: linear-gradient(135deg, #0f0c29, #24243e); }</style>
|
|
</head><body><h1>马来西亚旅游攻略</h1><p>沙滩、美食与城市漫步。</p></body></html>`;
|
|
const signals = extractCoverSignals(html);
|
|
assert.equal(signals.tag, '旅行');
|
|
assert.equal(signals.emoji, '🇲🇾');
|
|
assert.equal(signals.accent, '#ff6b35');
|
|
assert.match(signals.subtitle, /沙滩/);
|
|
});
|
|
|
|
test('buildFeedThumbnailSvg uses portrait 3:4 canvas', () => {
|
|
const svg = buildFeedThumbnailSvg({
|
|
title: '2026 市场趋势分析',
|
|
subtitle: 'AI 生成的研究报告',
|
|
tag: '报告',
|
|
emoji: '📊',
|
|
accent: '#2f6f57',
|
|
accent2: '#18211d',
|
|
mood: 'report',
|
|
});
|
|
assert.match(svg, /width="540" height="720"/);
|
|
assert.match(svg, /2026 市场趋势分析/);
|
|
assert.match(svg, /filter id="grain"/);
|
|
});
|
|
|
|
test('extractCoverSignals reads cover image from mindspace-cover meta', () => {
|
|
const html = `<!doctype html><html><head>
|
|
<meta name="mindspace-cover" content='{"cover":"assets/hero.png","tag":"旅行"}' />
|
|
</head><body></body></html>`;
|
|
const signals = extractCoverSignals(html);
|
|
assert.equal(signals.image, 'assets/hero.png');
|
|
assert.equal(signals.tag, '旅行');
|
|
});
|
|
|
|
test('buildFeedThumbnailSvg embeds photo cover layer', () => {
|
|
const dataUri =
|
|
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==';
|
|
const svg = buildFeedThumbnailSvg(
|
|
{
|
|
title: '实拍封面',
|
|
subtitle: '底图测试',
|
|
tag: '旅行',
|
|
accent: '#2f6f57',
|
|
accent2: '#18211d',
|
|
},
|
|
{ coverDataUri: dataUri },
|
|
);
|
|
assert.match(svg, /preserveAspectRatio="xMidYMid slice"/);
|
|
assert.match(svg, /href="data:image\/png;base64,/);
|
|
assert.doesNotMatch(svg, /M0 470 L90 404/);
|
|
});
|
|
|
|
test('generateHtmlThumbnail resolves relative cover image', async () => {
|
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'mindspace-thumb-cover-'));
|
|
const png = Buffer.from(
|
|
'iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAFUlEQVR42mNk+M9Qz0AEYBxVSF+FABJADveWkH6oAAAAAElFTkSuQmCC',
|
|
'base64',
|
|
);
|
|
await fs.mkdir(path.join(root, 'users', 'u1', 'pages', 'p1', 'versions'), { recursive: true });
|
|
await fs.writeFile(path.join(root, 'users', 'u1', 'pages', 'p1', 'versions', 'hero.png'), png);
|
|
const html = `<!doctype html><html><head>
|
|
<title>封面页</title>
|
|
<meta name="mindspace-cover" content='{"cover":"hero.png","tag":"旅行"}' />
|
|
</head><body></body></html>`;
|
|
const key = 'users/u1/pages/p1/thumbnail.svg';
|
|
await generateHtmlThumbnail(root, key, html, {
|
|
contentStorageKey: 'users/u1/pages/p1/versions/page.html',
|
|
});
|
|
const written = await fs.readFile(path.join(root, key), 'utf8');
|
|
assert.match(written, /preserveAspectRatio="xMidYMid slice"/);
|
|
assert.match(written, /href="data:image\/png;base64,/);
|
|
});
|
|
|
|
test('extractCoverSignals infers sport tag for pilates pages', () => {
|
|
const html = '<!doctype html><html><head><title>普拉提618</title></head><body></body></html>';
|
|
const signals = extractCoverSignals(html);
|
|
assert.equal(signals.tag, '运动');
|
|
});
|
|
|
|
test('buildFeedThumbnailSvg uses theme gradient for sport pages', () => {
|
|
const signals = extractCoverSignals(
|
|
'<!doctype html><html><head><title>普拉提618</title><style>body{color:#eeb04e;background:#1a0a2e}</style></head><body></body></html>',
|
|
);
|
|
assert.equal(shouldUseScenicBackground(signals), false);
|
|
const svg = buildFeedThumbnailSvg(signals);
|
|
assert.match(svg, /id="theme"/);
|
|
assert.doesNotMatch(svg, /M0 470 L90 404/);
|
|
});
|
|
|
|
test('isModernFeedThumbnail detects legacy landscape thumbnails', () => {
|
|
const legacy = `<?xml version="1.0"?><svg width="640" height="400"></svg>`;
|
|
const modern = buildFeedThumbnailSvg({
|
|
title: '新封面',
|
|
subtitle: '副标题',
|
|
tag: '网页',
|
|
accent: '#2f6f57',
|
|
accent2: '#18211d',
|
|
});
|
|
assert.equal(isModernFeedThumbnail(legacy), false);
|
|
assert.equal(isModernFeedThumbnail(modern), true);
|
|
});
|
|
|
|
test('ensureHtmlThumbnail upgrades legacy thumbnail file', async () => {
|
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'mindspace-thumb-upgrade-'));
|
|
const key = 'users/u1/pages/p1/thumbnail.svg';
|
|
const legacy = `<?xml version="1.0"?><svg width="640" height="400"><text>旧版</text></svg>`;
|
|
await fs.mkdir(path.dirname(path.join(root, key)), { recursive: true });
|
|
await fs.writeFile(path.join(root, key), legacy, 'utf8');
|
|
const html = '<!doctype html><html><head><title>升级测试</title></head><body></body></html>';
|
|
const svg = await ensureHtmlThumbnail(root, key, html, { title: '升级测试', tag: '网页' });
|
|
assert.equal(isModernFeedThumbnail(svg), true);
|
|
assert.match(svg, /升级测试/);
|
|
});
|
|
|
|
test('resolveCoverDataUri rejects paths outside content base', async () => {
|
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'mindspace-thumb-guard-'));
|
|
const dataUri = await resolveCoverDataUri({
|
|
storageRoot: root,
|
|
contentStorageKey: 'users/u1/pages/p1/versions/page.html',
|
|
imageUrl: '../../../secret.png',
|
|
});
|
|
assert.equal(dataUri, null);
|
|
});
|