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:
john
2026-07-05 23:14:41 +08:00
parent 69be1e5293
commit e2ad3bf62b
39 changed files with 2184 additions and 256 deletions
+212 -9
View File
@@ -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;
},
);
});