feat(mindspace): 公开页分享组件、workspace 路径与 Plaza 发布增强
- 新增 public share widget 与 workspace relative path 解析 - 增强 publications/chat-plaza 发布链路与缩略图 demo - 补充 schema、cover 检查脚本与回归测试 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -248,6 +248,104 @@ test('prepareHtmlPublishContent inlines owned private image assets before scanni
|
||||
}
|
||||
});
|
||||
|
||||
test('prepareHtmlPublishContent signs private image assets when imgproxy signer is enabled', async () => {
|
||||
const storageKey = 'users/user-1/images/2026-07-03/demo-asset-1.jpg';
|
||||
const pool = {
|
||||
async query(sql, params) {
|
||||
if (sql.includes('original_filename')) {
|
||||
return [[]];
|
||||
}
|
||||
assert.match(sql, /h5_assets/);
|
||||
assert.equal(params[0], 'user-1');
|
||||
assert.deepEqual(params[1], ['asset-1']);
|
||||
return [
|
||||
[
|
||||
{
|
||||
id: 'asset-1',
|
||||
mime_type: 'image/png',
|
||||
storage_key: storageKey,
|
||||
},
|
||||
],
|
||||
];
|
||||
},
|
||||
};
|
||||
const imgproxySigner = {
|
||||
baseUrl: 'https://img.example.test',
|
||||
buildUrl: (baseUrl, storagePath, preset = 'display') => {
|
||||
assert.equal(baseUrl, 'https://img.example.test');
|
||||
assert.equal(storagePath, storageKey);
|
||||
assert.equal(preset, 'display');
|
||||
return `https://img.example.test/sig/plain/local:///${storagePath}`;
|
||||
},
|
||||
};
|
||||
|
||||
const prepared = await publicationInternals.prepareHtmlPublishContent({
|
||||
pool,
|
||||
userId: 'user-1',
|
||||
html: '<!doctype html><img src="/api/mindspace/v1/assets/asset-1/download?inline=1&v=1">',
|
||||
ownerSlug: 'john',
|
||||
urlSlug: 'night-market-essay',
|
||||
absoluteStoragePath: () => {
|
||||
throw new Error('should not read private storage when imgproxy is enabled');
|
||||
},
|
||||
imgproxySigner: {
|
||||
baseUrl: imgproxySigner.baseUrl,
|
||||
buildUrl: (storagePath, preset = 'display') =>
|
||||
imgproxySigner.buildUrl(imgproxySigner.baseUrl, storagePath, preset),
|
||||
},
|
||||
});
|
||||
|
||||
assert.doesNotMatch(prepared, /\/api\/mindspace\/v1\/assets\//);
|
||||
assert.doesNotMatch(prepared, /thumbnail\.png/);
|
||||
assert.doesNotMatch(prepared, /plain\/local:\/\//);
|
||||
});
|
||||
|
||||
test('prepareHtmlPublishContent converts signed private images to public standard urls', async () => {
|
||||
const storageKey =
|
||||
'users/user-1/images/2026-07-03/60270b88dda7b7ccb8a49d8e2746e80b-asset-1.jpg';
|
||||
const pool = {
|
||||
async query(sql, params) {
|
||||
if (sql.includes('original_filename')) {
|
||||
return [[]];
|
||||
}
|
||||
assert.match(sql, /h5_assets/);
|
||||
assert.equal(params[0], 'user-1');
|
||||
assert.deepEqual(params[1], ['asset-1']);
|
||||
return [
|
||||
[
|
||||
{
|
||||
id: 'asset-1',
|
||||
mime_type: 'image/jpeg',
|
||||
storage_key: storageKey,
|
||||
},
|
||||
],
|
||||
];
|
||||
},
|
||||
};
|
||||
const imgproxySigner = {
|
||||
baseUrl: 'https://img.example.test',
|
||||
buildUrl: (storagePath, preset = 'display') =>
|
||||
`https://img.example.test/sig/rs:fit:1280:1280:0/q:85/plain/local:///${storagePath}@${preset}`,
|
||||
};
|
||||
|
||||
const prepared = await publicationInternals.prepareHtmlPublishContent({
|
||||
pool,
|
||||
userId: 'user-1',
|
||||
html: '<!doctype html><img src="/api/mindspace/v1/assets/asset-1/download?inline=1&v=1">',
|
||||
ownerSlug: 'john',
|
||||
urlSlug: 'night-market-essay',
|
||||
absoluteStoragePath: () => {
|
||||
throw new Error('should not read private storage when imgproxy is enabled');
|
||||
},
|
||||
imgproxySigner,
|
||||
});
|
||||
|
||||
assert.match(prepared, /https:\/\/m\.tkmind\.cn\/MindSpace\/user-1\/public\/images\/2026-07-03\//);
|
||||
assert.doesNotMatch(prepared, /\/api\/mindspace\/v1\/assets\//);
|
||||
assert.doesNotMatch(prepared, /thumbnail\.png/);
|
||||
assert.doesNotMatch(prepared, /plain\/local:\/\//);
|
||||
});
|
||||
|
||||
test('prepareHtmlPublishContent rewrites imgproxy local image urls to public standard images', async () => {
|
||||
const prepared = await publicationInternals.prepareHtmlPublishContent({
|
||||
pool: {
|
||||
@@ -264,7 +362,7 @@ test('prepareHtmlPublishContent rewrites imgproxy local image urls to public sta
|
||||
},
|
||||
});
|
||||
|
||||
assert.match(prepared, /https:\/\/m\.tkmind\.cn\/MindSpace\/user-1\/images\/2026-06-29\/hero\.jpg/);
|
||||
assert.match(prepared, /https:\/\/m\.tkmind\.cn\/MindSpace\/user-1\/public\/images\/2026-06-29\/hero\.jpg/);
|
||||
assert.doesNotMatch(prepared, /plain\/local:\/\//);
|
||||
});
|
||||
|
||||
@@ -544,12 +642,44 @@ test('prepareHtmlPublishContent rewrites imgproxy urls in srcset and css url con
|
||||
},
|
||||
});
|
||||
|
||||
assert.match(prepared, /\/MindSpace\/user-1\/images\/2026-06-29\/thumb\.jpg 1x/);
|
||||
assert.match(prepared, /\/MindSpace\/user-1\/images\/2026-06-29\/hero\.jpg 2x/);
|
||||
assert.match(prepared, /url\(https:\/\/m\.tkmind\.cn\/MindSpace\/user-1\/images\/2026-06-29\/bg\.jpg\)/);
|
||||
assert.match(prepared, /\/MindSpace\/user-1\/public\/images\/2026-06-29\/thumb\.jpg 1x/);
|
||||
assert.match(prepared, /\/MindSpace\/user-1\/public\/images\/2026-06-29\/hero\.jpg 2x/);
|
||||
assert.match(prepared, /url\(https:\/\/m\.tkmind\.cn\/MindSpace\/user-1\/public\/images\/2026-06-29\/bg\.jpg\)/);
|
||||
assert.doesNotMatch(prepared, /plain\/local:\/\//);
|
||||
});
|
||||
|
||||
test('prepareHtmlPublishContent preserves absolute MindSpace public image urls', async () => {
|
||||
const prepared = await publicationInternals.prepareHtmlPublishContent({
|
||||
pool: {
|
||||
async query() {
|
||||
return [[]];
|
||||
},
|
||||
},
|
||||
userId: 'user-1',
|
||||
html: '<!doctype html><img src="http://127.0.0.1:5173/MindSpace/user-1/public/images/2026-07-05/demo.jpg" alt="demo">',
|
||||
ownerSlug: 'john',
|
||||
urlSlug: 'story',
|
||||
htmlRelativePath: 'public/index.html',
|
||||
absoluteStoragePath: () => {
|
||||
throw new Error('should not read private storage');
|
||||
},
|
||||
});
|
||||
|
||||
assert.match(
|
||||
prepared,
|
||||
/src="http:\/\/127\.0\.0\.1:5173\/MindSpace\/user-1\/public\/images\/2026-07-05\/demo\.jpg"/,
|
||||
);
|
||||
assert.doesNotMatch(prepared, /\/MindSpace\/user-1\/images\/2026-07-05\//);
|
||||
});
|
||||
|
||||
test('rewriteBrokenMindSpacePublicImageUrls restores missing public segment', () => {
|
||||
const html =
|
||||
'<img src="http://127.0.0.1:5173/MindSpace/user-1/images/2026-07-05/demo.jpg" alt="demo">';
|
||||
const out = publicationInternals.rewriteBrokenMindSpacePublicImageUrls(html);
|
||||
assert.match(out, /\/MindSpace\/user-1\/public\/images\/2026-07-05\/demo\.jpg/);
|
||||
assert.doesNotMatch(out, /\/MindSpace\/user-1\/images\/2026-07-05\//);
|
||||
});
|
||||
|
||||
test('prepareHtmlPublishContent rewrites workspace public asset paths relative to html output', async () => {
|
||||
const prepared = await publicationInternals.prepareHtmlPublishContent({
|
||||
pool: {
|
||||
@@ -572,9 +702,82 @@ test('prepareHtmlPublishContent rewrites workspace public asset paths relative t
|
||||
},
|
||||
});
|
||||
|
||||
assert.match(prepared, /"cover":"\.\.\/images\/2026-06-29\/hero\.jpg"/);
|
||||
assert.match(prepared, /url\('\.\.\/images\/2026-06-29\/bg\.jpg'\)/);
|
||||
assert.match(prepared, /src="\.\.\/\.tmp-images\/card\.png"/);
|
||||
assert.doesNotMatch(prepared, /public\/images\//);
|
||||
assert.doesNotMatch(prepared, /public\/\.tmp-images\//);
|
||||
assert.match(prepared, /"cover":"\/u\/john\/public\/images\/2026-06-29\/hero\.jpg"/);
|
||||
assert.match(prepared, /url\('\/u\/john\/public\/images\/2026-06-29\/bg\.jpg'\)/);
|
||||
assert.match(prepared, /src="\/u\/john\/public\/\.tmp-images\/card\.png"/);
|
||||
assert.doesNotMatch(prepared, /src="public\//);
|
||||
assert.doesNotMatch(prepared, /"cover":"public\//);
|
||||
});
|
||||
|
||||
test('rewritePublicationCanonicalAssetUrls rewrites workspace tmp image paths for /u pages routes', () => {
|
||||
const html = [
|
||||
'<img src=".tmp-images/one.jpg">',
|
||||
'<img src="../.tmp-images/two.jpg">',
|
||||
`<section style="background-image:url('.tmp-images/three.jpg')"></section>`,
|
||||
].join('');
|
||||
const out = publicationInternals.rewritePublicationCanonicalAssetUrls(html, 'john');
|
||||
assert.match(out, /src="\/u\/john\/public\/\.tmp-images\/one\.jpg"/);
|
||||
assert.match(out, /src="\/u\/john\/public\/\.tmp-images\/two\.jpg"/);
|
||||
assert.match(out, /url\('\/u\/john\/public\/\.tmp-images\/three\.jpg'\)/);
|
||||
});
|
||||
|
||||
test('publish rejects republish while page is still online', async () => {
|
||||
const pageRow = {
|
||||
page_id: 'page-1',
|
||||
title: 'Report',
|
||||
summary: '',
|
||||
page_type: 'article',
|
||||
template_id: 'editorial',
|
||||
current_version_id: 'version-1',
|
||||
source_session_id: null,
|
||||
source_message_id: null,
|
||||
user_id: 'user-1',
|
||||
space_id: 'space-1',
|
||||
page_version_id: 'version-1',
|
||||
version_no: 1,
|
||||
bundle_asset_id: null,
|
||||
storage_key: 'users/user-1/pages/page-1/versions/v1.md',
|
||||
content: '# Report',
|
||||
};
|
||||
const pool = {
|
||||
async query(sql, params = []) {
|
||||
if (sql.includes('FROM h5_page_records p') && sql.includes('JOIN h5_page_versions')) {
|
||||
return [[pageRow]];
|
||||
}
|
||||
if (
|
||||
sql.includes('FROM h5_publish_records') &&
|
||||
sql.includes("status = 'online'") &&
|
||||
sql.includes('page_id = ?')
|
||||
) {
|
||||
return [[{ id: 'pub-online', url_slug: 'report', public_url: '/u/john/pages/report' }]];
|
||||
}
|
||||
if (sql.includes('FROM h5_publish_records pr') && sql.includes('pr.page_id <>')) {
|
||||
return [[]];
|
||||
}
|
||||
return [[]];
|
||||
},
|
||||
async getConnection() {
|
||||
throw new Error('publish should fail before opening a transaction');
|
||||
},
|
||||
};
|
||||
const service = createPublicationService(pool, {
|
||||
storageRoot: '/tmp',
|
||||
h5Root: '/tmp',
|
||||
idFactory: () => 'id-1',
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() =>
|
||||
service.publish('user-1', 'page-1', {
|
||||
pageVersionId: 'version-1',
|
||||
accessMode: 'public',
|
||||
urlSlug: 'report',
|
||||
acknowledgedFindingIds: [],
|
||||
}),
|
||||
(error) => {
|
||||
assert.equal(error.code, 'page_already_online');
|
||||
assert.equal(error.details.publicationId, 'pub-online');
|
||||
return true;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user