feat(mindspace): 0630004 空间 UI、聊天连接、微信分享与 Agent 能力
含 MindSpace 三列布局与统计修复、聊天加载态与连接降级、平台页脚标记与 og:site_name 微信卡片、勾选资料删除 Agent 接口及内部话术过滤。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+61
-7
@@ -12,7 +12,8 @@ import {
|
||||
bufferToImageDataUri,
|
||||
} from './mindspace-thumbnails.mjs';
|
||||
import { resolvePublishDir } from './user-publish.mjs';
|
||||
import { prepareHtmlDownloadLinks } from './mindspace-html-download-links.mjs';
|
||||
import { prepareHtmlDownloadLinks, inferWorkspaceHtmlRelativePath } from './mindspace-html-download-links.mjs';
|
||||
import { prepareHtmlPageBrandMarkers } from './mindspace-page-tag.mjs';
|
||||
import { purgeWorkspacePageArtifacts, extractAssetIdsFromHtml } from './mindspace-page-purge.mjs';
|
||||
import { ensureWorkspaceHtmlThumbnail, workspaceThumbnailRelativePath } from './mindspace-workspace-thumbnails.mjs';
|
||||
import { upsertMindspaceCoverMeta } from './mindspace-cover-meta.mjs';
|
||||
@@ -43,6 +44,18 @@ function pageError(message, code, details) {
|
||||
return Object.assign(new Error(message), { code, details });
|
||||
}
|
||||
|
||||
/** Normalize workspace HTML paths so `demo.html` and `public/demo.html` match. */
|
||||
export function normalizeWorkspaceRelativePath(relativePath) {
|
||||
const parts = String(relativePath ?? '')
|
||||
.replace(/^\/+/, '')
|
||||
.split('/')
|
||||
.filter((part) => part && part !== '.' && part !== '..');
|
||||
if (parts.length === 0) return '';
|
||||
if (parts[0].toLowerCase() === 'public') return parts.join('/');
|
||||
if (parts.length === 1 && parts[0].toLowerCase().endsWith('.html')) return `public/${parts[0]}`;
|
||||
return parts.join('/');
|
||||
}
|
||||
|
||||
function normalizeText(value, maxLength, fieldName) {
|
||||
const text = String(value ?? '').normalize('NFKC').trim();
|
||||
if (!text) throw pageError(`${fieldName}不能为空`, 'invalid_page_input');
|
||||
@@ -54,13 +67,16 @@ function normalizeText(value, maxLength, fieldName) {
|
||||
|
||||
function normalizePageInput(input) {
|
||||
const title = normalizeText(input.title, MAX_TITLE_LENGTH, '标题');
|
||||
const content = normalizeText(input.content, MAX_CONTENT_BYTES, '页面内容');
|
||||
let content = normalizeText(input.content, MAX_CONTENT_BYTES, '页面内容');
|
||||
const contentFormat = input.contentFormat === 'html' ? 'html' : 'markdown';
|
||||
if (contentFormat === 'html') {
|
||||
content = prepareHtmlPageBrandMarkers(content);
|
||||
}
|
||||
const contentBytes = Buffer.byteLength(content, 'utf8');
|
||||
if (contentBytes > MAX_CONTENT_BYTES) {
|
||||
throw pageError('页面内容超过 1 MB 限制', 'page_content_too_large');
|
||||
}
|
||||
const summary = String(input.summary ?? '').normalize('NFKC').trim().slice(0, MAX_SUMMARY_LENGTH);
|
||||
const contentFormat = input.contentFormat === 'html' ? 'html' : 'markdown';
|
||||
const templateId =
|
||||
contentFormat === 'html'
|
||||
? 'static-html'
|
||||
@@ -579,6 +595,25 @@ export function createPageService(pool, options = {}) {
|
||||
return rows[0] ? pageResponse(rows[0]) : null;
|
||||
};
|
||||
|
||||
const findPageByRelativePath = async (userId, relativePath) => {
|
||||
const normalized = normalizeWorkspaceRelativePath(relativePath);
|
||||
if (!normalized) 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
|
||||
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.status <> 'deleted'
|
||||
AND JSON_UNQUOTE(JSON_EXTRACT(pv.source_snapshot_json, '$.relative_path')) = ?
|
||||
ORDER BY p.updated_at DESC, p.id DESC
|
||||
LIMIT 1`,
|
||||
[userId, normalized],
|
||||
);
|
||||
return rows[0] ? pageResponse(rows[0]) : null;
|
||||
};
|
||||
|
||||
const listPages = async (userId, filters = {}) => {
|
||||
const clauses = [`p.user_id = ?`, `p.status <> 'deleted'`];
|
||||
const params = [userId];
|
||||
@@ -729,9 +764,9 @@ export function createPageService(pool, options = {}) {
|
||||
};
|
||||
};
|
||||
|
||||
const loadPageWorkspaceContext = async (userId, pageId) => {
|
||||
const loadPageWorkspaceContext = async (userId, pageId, htmlContent = null) => {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT pv.source_snapshot_json
|
||||
`SELECT pv.source_snapshot_json, p.title
|
||||
FROM h5_page_records p
|
||||
JOIN h5_page_versions pv ON pv.id = p.current_version_id
|
||||
WHERE p.id = ? AND p.user_id = ? AND p.status <> 'deleted'
|
||||
@@ -744,8 +779,13 @@ export function createPageService(pool, options = {}) {
|
||||
} catch {
|
||||
snapshot = {};
|
||||
}
|
||||
const workspaceHtmlRelativePath = snapshot.relative_path ?? 'public/index.html';
|
||||
const workspacePublishDir = h5Root ? resolvePublishDir(h5Root, { id: userId }) : null;
|
||||
const workspaceHtmlRelativePath = inferWorkspaceHtmlRelativePath({
|
||||
snapshotRelativePath: snapshot.relative_path,
|
||||
pageTitle: rows[0]?.title,
|
||||
htmlContent,
|
||||
publishDir: workspacePublishDir,
|
||||
});
|
||||
return {
|
||||
workspaceHtmlRelativePath,
|
||||
workspacePublishDir,
|
||||
@@ -753,10 +793,22 @@ export function createPageService(pool, options = {}) {
|
||||
};
|
||||
};
|
||||
|
||||
const rewriteHtmlDownloadLinksForPage = async (userId, pageId, html) => {
|
||||
const ctx = await loadPageWorkspaceContext(userId, pageId, html);
|
||||
const { html: rewritten } = await prepareHtmlDownloadLinks(pool, userId, html, {
|
||||
htmlRelativePath: ctx.workspaceHtmlRelativePath,
|
||||
publishDir: ctx.workspacePublishDir,
|
||||
publishKey: ctx.publishKey,
|
||||
publicBaseUrl: '',
|
||||
preferAssetDownload: true,
|
||||
});
|
||||
return rewritten;
|
||||
};
|
||||
|
||||
const finalizeHtmlPreview = async (userId, pageId, html) => {
|
||||
const shell = renderHtmlPreview(html);
|
||||
try {
|
||||
const ctx = await loadPageWorkspaceContext(userId, pageId);
|
||||
const ctx = await loadPageWorkspaceContext(userId, pageId, html);
|
||||
const { html: rewritten } = await prepareHtmlDownloadLinks(pool, userId, shell, {
|
||||
htmlRelativePath: ctx.workspaceHtmlRelativePath,
|
||||
publishDir: ctx.workspacePublishDir,
|
||||
@@ -1334,10 +1386,12 @@ export function createPageService(pool, options = {}) {
|
||||
listPages,
|
||||
findPageBySourceAsset,
|
||||
findPageBySourceMessage,
|
||||
findPageByRelativePath,
|
||||
getPage,
|
||||
getDeletePreview,
|
||||
deletePage,
|
||||
listVersions,
|
||||
rewriteHtmlDownloadLinksForPage,
|
||||
renderPreview: async (userId, pageId) => {
|
||||
const page = await getPage(userId, pageId);
|
||||
const html =
|
||||
|
||||
Reference in New Issue
Block a user