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
+65 -7
View File
@@ -25,6 +25,10 @@ import {
DEFAULT_IMAGE_UPLOAD_MAX_BYTES,
normalizeImageForStorage,
} from './user-image-normalize.mjs';
import {
normalizeWorkspaceRelativePath,
resolveAssetWorkspaceRelativePath,
} from './mindspace-workspace-path.mjs';
const ALLOWED_EXTENSIONS = new Map([
['.txt', 'text/plain'],
@@ -184,9 +188,15 @@ function isPublicImageAsset(row) {
return row?.category_code === 'public';
}
function isCanonicalPublicImageStorageKey(storageKey) {
const normalized = String(storageKey ?? '').replace(/\\/g, '/');
if (isWorkspaceStorageKey(normalized)) return false;
return isPublicImageStorageKey(normalized);
}
function publicUrlForAsset(row) {
if (!isPublicImageAsset(row)) return null;
if (row?.storage_key && isPublicImageStorageKey(row.storage_key)) {
if (row?.storage_key && isCanonicalPublicImageStorageKey(row.storage_key)) {
return buildUserImagePublicUrl({
userId: row.user_id,
storageKey: row.storage_key,
@@ -194,9 +204,16 @@ function publicUrlForAsset(row) {
originalFilename: row.original_filename,
});
}
const workspacePath = normalizeWorkspaceRelativePath(row?.workspace_relative_path);
if (workspacePath && isWorkspaceStorageKey(row?.storage_key)) {
return createMindSpacePublicUrl({
publicBaseUrl: resolvePublicBaseUrl(),
ownerKey: row.user_id,
relativePath: workspacePath,
});
}
if (row?.category_code === 'public' && row?.id) {
const legacy = publicTempImageUrl(row.user_id, row.id, row.mime_type, row.original_filename);
return legacy;
return publicTempImageUrl(row.user_id, row.id, row.mime_type, row.original_filename);
}
return null;
}
@@ -651,12 +668,17 @@ export function createAssetService(pool, options = {}) {
const now = Date.now();
const visibility = shouldPublishImage ? 'public_candidate' : visibilityForCategory(upload.category_code);
const indexedWorkspacePath = resolveAssetWorkspaceRelativePath({
categoryCode: upload.category_code,
originalFilename: storedFilename,
explicitPath: workspaceRelativePath,
});
await conn.query(
`INSERT INTO h5_assets
(id, user_id, space_id, category_id, asset_type, mime_type, original_filename,
display_name, current_version_id, size_bytes, checksum, risk_level, visibility,
display_name, workspace_relative_path, current_version_id, size_bytes, checksum, risk_level, visibility,
status, source_type, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'upload', ?, ?)`,
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'upload', ?, ?)`,
[
assetId,
userId,
@@ -666,6 +688,7 @@ export function createAssetService(pool, options = {}) {
storedMimeType,
storedFilename,
upload.filename,
indexedWorkspacePath,
versionId,
storedSizeBytes,
storedChecksum,
@@ -1191,12 +1214,17 @@ export function createAssetService(pool, options = {}) {
}
const now = Date.now();
const visibility = shouldPublishImage ? 'public_candidate' : visibilityForCategory(category.category_code);
const indexedWorkspacePath = resolveAssetWorkspaceRelativePath({
categoryCode: category.category_code,
originalFilename: storedFilename,
explicitPath: workspaceRelativePath,
});
await conn.query(
`INSERT INTO h5_assets
(id, user_id, space_id, category_id, asset_type, mime_type, original_filename,
display_name, current_version_id, size_bytes, checksum, risk_level, visibility,
display_name, workspace_relative_path, current_version_id, size_bytes, checksum, risk_level, visibility,
status, source_type, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
assetId,
userId,
@@ -1206,6 +1234,7 @@ export function createAssetService(pool, options = {}) {
storedMimeType,
storedFilename,
displayName || path.basename(storedFilename),
indexedWorkspacePath,
versionId,
storedSizeBytes,
storedChecksum,
@@ -1370,6 +1399,33 @@ export function createAssetService(pool, options = {}) {
return expired;
};
const findAssetByWorkspaceRelativePath = async (userId, relativePath) => {
const normalized = normalizeWorkspaceRelativePath(relativePath);
if (!normalized) return null;
const [rows] = await pool.query(
`SELECT a.*, c.category_code, av.storage_key
FROM h5_assets a
JOIN h5_space_categories c ON c.id = a.category_id AND c.user_id = a.user_id
JOIN h5_asset_versions av ON av.id = a.current_version_id
WHERE a.user_id = ?
AND a.status <> 'deleted'
AND (
a.workspace_relative_path = ?
OR (
(a.workspace_relative_path IS NULL OR a.workspace_relative_path = '')
AND (
a.original_filename = ?
OR CONCAT(c.category_code, '/', a.original_filename) = ?
)
)
)
ORDER BY a.updated_at DESC, a.id DESC
LIMIT 1`,
[userId, normalized, normalized, normalized],
);
return rows[0] ? assetResponse(rows[0]) : null;
};
return {
storageRoot,
createUpload,
@@ -1379,6 +1435,7 @@ export function createAssetService(pool, options = {}) {
cancelUpload,
createChatAsset,
listAssets,
findAssetByWorkspaceRelativePath,
deleteAsset,
readAsset,
readPublicAsset,
@@ -1440,5 +1497,6 @@ export const assetInternals = {
normalizeFilename,
expectedMimeType,
assetTypeForMime,
publicUrlForAsset,
registerUploadArtifactForConversation,
};