feat: release chat and mindspace UI updates

This commit is contained in:
john
2026-06-27 21:57:21 +08:00
parent c924b2bb77
commit 8264e71f9e
24 changed files with 2042 additions and 209 deletions
+61
View File
@@ -537,6 +537,22 @@ export function createPageService(pool, options = {}) {
return rows[0] ? pageResponse(rows[0]) : null;
};
const findPageBySourceMessage = async (userId, sessionId, messageId) => {
if (!sessionId || !messageId) return null;
const [rows] = await pool.query(
`SELECT p.*, c.category_code, pv.version_no, pr.access_mode AS pub_access_mode, pr.public_url AS pub_public_url
FROM h5_page_records p
JOIN h5_space_categories c ON c.id = p.category_id AND c.user_id = p.user_id
LEFT JOIN h5_page_versions pv ON pv.id = p.current_version_id
LEFT JOIN h5_publish_records pr ON pr.id = p.current_publish_id AND pr.status = 'online'
WHERE p.user_id = ? AND p.source_session_id = ? AND p.source_message_id = ? AND p.status <> 'deleted'
ORDER BY p.updated_at DESC
LIMIT 1`,
[userId, sessionId, messageId],
);
return rows[0] ? pageResponse(rows[0]) : null;
};
const listPages = async (userId, filters = {}) => {
const clauses = [`p.user_id = ?`, `p.status <> 'deleted'`];
const params = [userId];
@@ -1148,6 +1164,7 @@ export function createPageService(pool, options = {}) {
createRedactedCopy: redactPage,
listPages,
findPageBySourceAsset,
findPageBySourceMessage,
getPage,
getDeletePreview,
deletePage,
@@ -1211,6 +1228,50 @@ export const pageInternals = {
buildPageDeleteSummary,
};
export async function inlinePrivateAssetsInHtml(pool, storageRoot, userId, html) {
const PATTERN =
/(?:https?:\/\/[^/]+)?\/api\/mindspace\/v1\/assets\/([a-z0-9-]+)\/download(?:\?[^"'<>\\\s)]*)?/gi;
const resolvedRoot = path.resolve(storageRoot);
const absoluteStoragePath = (key) => {
const resolved = path.resolve(resolvedRoot, key);
if (resolved !== resolvedRoot && !resolved.startsWith(`${resolvedRoot}${path.sep}`)) {
throw Object.assign(new Error('路径越界'), { code: 'invalid_storage_path' });
}
return resolved;
};
const matches = [...String(html).matchAll(PATTERN)];
if (matches.length === 0) return { html, count: 0 };
const assetIds = [...new Set(matches.map((m) => m[1]).filter(Boolean))];
const [assets] = await pool.query(
`SELECT a.id, a.mime_type, a.original_filename, v.storage_key
FROM h5_assets a
JOIN h5_asset_versions v ON v.id = a.current_version_id
WHERE a.user_id = ? AND a.id IN (?) AND a.mime_type LIKE 'image/%'
AND a.status <> 'deleted'`,
[userId, assetIds],
);
const dataUriMap = new Map();
for (const asset of assets) {
try {
const buffer = await fs.readFile(absoluteStoragePath(asset.storage_key));
dataUriMap.set(asset.id, `data:${asset.mime_type};base64,${buffer.toString('base64')}`);
} catch {
// skip unreadable assets
}
}
let count = 0;
const result = String(html).replace(PATTERN, (_match, assetId) => {
const uri = dataUriMap.get(assetId);
if (!uri) return '';
count += 1;
return uri;
});
return { html: result, count };
}
export function buildPageDeleteSummary(preview) {
const lines = [
`页面「${preview.page.title}」及其 ${preview.page.versionCount} 个版本`,