feat(mindspace): 0630004 空间 UI、聊天连接、微信分享与 Agent 能力

含 MindSpace 三列布局与统计修复、聊天加载态与连接降级、平台页脚标记与 og:site_name 微信卡片、勾选资料删除 Agent 接口及内部话术过滤。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-06-30 09:30:51 +08:00
parent 0203d81942
commit 2b8dba1784
53 changed files with 2450 additions and 313 deletions
+168 -15
View File
@@ -39,8 +39,9 @@ import { assertMindSpaceRoute, mindspaceFlags } from './mindspace-flags.mjs';
import { createMindSpaceService, DEFAULT_MAX_FILE_BYTES } from './mindspace.mjs';
import { ensureMindSpaceConfig } from './mindspace-config.mjs';
import { createAssetService } from './mindspace-assets.mjs';
import { createPageService, pageInternals, inlinePrivateAssetsInHtml } from './mindspace-pages.mjs';
import { createPageService, pageInternals, inlinePrivateAssetsInHtml, normalizeWorkspaceRelativePath } from './mindspace-pages.mjs';
import { createPageLiveEditService } from './mindspace-page-live-edit.mjs';
import { createAssetAgentService } from './mindspace-asset-agent.mjs';
import { createPageEditSessionService } from './mindspace-page-edit-session.mjs';
import { suggestCoverMetaWithAi } from './mindspace-cover-ai.mjs';
import { createPublicationService, rewriteWorkspacePublicAssetReferences } from './mindspace-publications.mjs';
@@ -88,8 +89,13 @@ import {
syncPublicHtmlAfterFinish,
} from './mindspace-public-finish-sync.mjs';
import { syncGeneratedPagesFromPublicAssets } from './mindspace-page-sync.mjs';
import { generateHtmlThumbnail } from './mindspace-thumbnails.mjs';
import { injectOgTags, injectWechatShareBridge } from './mindspace-og-tags.mjs';
import { extractCoverSignals, generateHtmlThumbnail } from './mindspace-thumbnails.mjs';
import {
extractSharePreviewMeta,
injectOgTags,
injectWechatShareBridge,
renderWechatSharePreviewHtml,
} from './mindspace-og-tags.mjs';
import {
ensureThumbnailPng,
rasterizeThumbnailSvgToPng,
@@ -234,6 +240,7 @@ let mindSpaceAssets = null;
let mindSpaceAudit = null;
let mindSpacePages = null;
let mindSpacePageLiveEdit = null;
let mindSpaceAssetAgent = null;
let mindSpacePageEditSession = null;
let mindSpacePublications = null;
let plazaPosts = null;
@@ -289,15 +296,20 @@ async function bootstrapUserAuth() {
storageRoot:
process.env.MINDSPACE_STORAGE_ROOT ?? path.join(__dirname, 'data', 'mindspace'),
});
const resolveUserIdForAgentSession = async (sessionId) => {
const [rows] = await pool.query(
`SELECT user_id FROM h5_user_sessions WHERE agent_session_id = ? LIMIT 1`,
[sessionId],
);
return rows[0]?.user_id ?? null;
};
mindSpacePageLiveEdit = createPageLiveEditService({
pageService: mindSpacePages,
resolveUserIdForAgentSession: async (sessionId) => {
const [rows] = await pool.query(
`SELECT user_id FROM h5_user_sessions WHERE agent_session_id = ? LIMIT 1`,
[sessionId],
);
return rows[0]?.user_id ?? null;
},
resolveUserIdForAgentSession,
});
mindSpaceAssetAgent = createAssetAgentService({
assetService: mindSpaceAssets,
resolveUserIdForAgentSession,
});
mindSpacePublications = createPublicationService(pool, {
h5Root: __dirname,
@@ -1706,6 +1718,7 @@ api.use(async (req, res, next) => {
if (req.path === '/status') return next();
if (req.path.startsWith('/internal/agent/')) return next();
if (req.path === '/agent/mindspace_page_patch') return next();
if (req.path === '/agent/mindspace_asset_delete') return next();
if (req.path === '/config/blocked-words') return next();
if (req.method === 'GET' && /^\/mindspace\/v1\/assets\/[^/]+\/download$/.test(req.path)) {
return next();
@@ -2615,8 +2628,33 @@ api.post('/mindspace/v1/pages/from-asset', async (req, res) => {
req.currentUser.id,
assetId,
);
if (asset.mimeType === 'text/html') {
const relativePath = normalizeWorkspaceRelativePath(
String(asset.originalFilename ?? asset.original_filename ?? '').includes('/')
? asset.originalFilename ?? asset.original_filename
: `public/${asset.originalFilename ?? asset.original_filename ?? ''}`,
);
const existingByPath = relativePath
? await mindSpacePages.findPageByRelativePath(req.currentUser.id, relativePath).catch(() => null)
: null;
if (existingByPath) {
return sendData(res, req, {
kind: 'page',
categoryCode: existingByPath.categoryCode ?? 'draft',
page: existingByPath,
});
}
}
const content = await fs.promises.readFile(assetPath, 'utf8');
const contentFormat = asset.mimeType === 'text/html' ? 'html' : 'markdown';
const htmlRelativePath =
contentFormat === 'html'
? normalizeWorkspaceRelativePath(
String(asset.originalFilename ?? asset.original_filename ?? '').includes('/')
? asset.originalFilename ?? asset.original_filename
: `public/${asset.originalFilename ?? asset.original_filename ?? ''}`,
)
: null;
const page = await mindSpacePages.createFromChat(
req.currentUser.id,
{
@@ -2633,6 +2671,7 @@ api.post('/mindspace/v1/pages/from-asset', async (req, res) => {
source_asset_id: asset.id,
source_category: asset.categoryCode,
content_mode: contentFormat,
...(htmlRelativePath ? { relative_path: htmlRelativePath } : {}),
},
},
);
@@ -2696,6 +2735,17 @@ async function resolveOwnedAssistantMessage(userId, sessionId, messageId) {
return { session, message, content };
}
async function resolveExistingSavedPage(userId, { sessionId, messageId, relativePath } = {}) {
if (!mindSpacePages || !userId) return null;
const byMessage = await mindSpacePages
.findPageBySourceMessage(userId, sessionId, messageId)
.catch(() => null);
if (byMessage) return byMessage;
const normalizedPath = normalizeWorkspaceRelativePath(relativePath);
if (!normalizedPath) return null;
return mindSpacePages.findPageByRelativePath(userId, normalizedPath).catch(() => null);
}
const SAVE_TARGET_CATEGORIES = new Set(['draft', 'oa', 'public']);
const pageSyncInFlight = new Map();
@@ -2911,11 +2961,11 @@ api.post('/mindspace/v1/pages/analyze-chat-save', async (req, res) => {
thumbnailReady = false;
}
}
const existingPage = await mindSpacePages.findPageBySourceMessage(
req.currentUser.id,
const existingPage = await resolveExistingSavedPage(req.currentUser.id, {
sessionId,
messageId,
).catch(() => null);
relativePath: resolvedHtml?.relativePath ?? analysis.relativePath,
});
return sendData(res, req, {
contentMode: analysis.contentMode,
links: analysis.links,
@@ -3009,7 +3059,9 @@ api.post('/mindspace/v1/pages/save-from-chat', async (req, res) => {
role: source.message.role,
content_mode: analysis.contentMode,
public_url: analysis.previewUrl,
relative_path: analysis.relativePath,
relative_path: analysis.relativePath
? normalizeWorkspaceRelativePath(analysis.relativePath)
: analysis.relativePath,
};
let resolvedHtml = null;
@@ -3086,9 +3138,16 @@ api.post('/mindspace/v1/pages/save-from-chat', async (req, res) => {
}).catch(() => {});
}
const replacePageId = req.body?.replace_page_id
const saveAsNew = Boolean(req.body?.save_as_new);
let replacePageId = req.body?.replace_page_id
? String(req.body.replace_page_id).trim()
: null;
if (!replacePageId && !saveAsNew && analysis.contentMode === 'static_html' && analysis.relativePath) {
const existingByPath = await mindSpacePages
.findPageByRelativePath(req.currentUser.id, analysis.relativePath)
.catch(() => null);
if (existingByPath) replacePageId = existingByPath.id;
}
let page;
if (replacePageId) {
const existingPage = await mindSpacePages.getPage(req.currentUser.id, replacePageId);
@@ -3363,6 +3422,32 @@ api.post('/agent/mindspace_page_patch', async (req, res) => {
}
});
api.post('/agent/mindspace_asset_delete', async (req, res) => {
if (!mindSpaceAssetAgent) {
return res.status(503).json({ message: 'MindSpace 未启用' });
}
try {
const result = await mindSpaceAssetAgent.applyAgentDelete(req.body ?? {});
await mindSpaceAudit?.write({
userId: result.userId ?? null,
action: 'asset.delete',
objectType: 'asset',
objectId: result.assetId,
ip: req.ip,
metadata: { via: 'agent' },
});
return sendData(res, req, result);
} catch (error) {
if (error?.code === 'forbidden') {
return sendError(res, req, 403, error.code, error.message);
}
if (error?.code === 'invalid_request' || error?.code === 'confirmation_required') {
return sendError(res, req, 400, error.code, error.message);
}
return mindSpaceError(res, req, error);
}
});
api.get('/mindspace/v1/pages/:pageId/thumbnail', async (req, res) => {
if (!mindSpacePages) return res.status(503).json({ message: 'MindSpace 未启用' });
try {
@@ -3426,6 +3511,24 @@ api.post('/mindspace/v1/pages/:pageId/thumbnail/regenerate', async (req, res) =>
}
});
api.post('/mindspace/v1/pages/:pageId/rewrite-download-links', async (req, res) => {
if (!mindSpacePages) return res.status(503).json({ message: 'MindSpace 未启用' });
try {
const content = String(req.body?.content ?? '').trim();
if (!content) {
throw Object.assign(new Error('缺少页面内容'), { code: 'invalid_page_input' });
}
const html = await mindSpacePages.rewriteHtmlDownloadLinksForPage(
req.currentUser.id,
req.params.pageId,
content,
);
return sendData(res, req, { html });
} catch (error) {
return mindSpaceError(res, req, error);
}
});
api.get('/mindspace/v1/pages/:pageId/preview', async (req, res) => {
if (!mindSpacePages) return res.status(503).json({ message: 'MindSpace 未启用' });
try {
@@ -4319,6 +4422,14 @@ function extractOgImageUrl(html) {
);
}
function extractShareMetaFromPageHtml(html) {
const signals = extractCoverSignals(String(html ?? ''));
return {
title: detectPublishedPageTitle(html),
subtitle: signals.subtitle,
};
}
function appendQueryParam(url, key, value) {
const separator = url.includes('?') ? '&' : '?';
return `${url}${separator}${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
@@ -4738,6 +4849,7 @@ function sendPublishedPage(req, res, result, { embed = false, raw = false } = {}
pageUrl,
pageDirUrl,
fallbackImageUrl: extractOgImageUrl(html),
meta: extractShareMetaFromPageHtml(html),
});
if (wechatShare) shellHtml = injectWechatShareBridge(shellHtml, { pageUrl });
} catch {
@@ -5282,6 +5394,47 @@ app.use(
'/plaza-covers',
express.static(path.join(__dirname, 'public/plaza-covers'), { maxAge: '7d' }),
);
app.use('/brand', express.static(path.join(__dirname, 'public/brand'), { maxAge: '7d' }));
app.get('/dev/wechat-share-preview', async (req, res) => {
try {
const rawTarget = String(req.query.url ?? req.query.path ?? '').trim();
if (!rawTarget) {
return res.status(400).type('text/plain; charset=utf-8').send('缺少 url 参数,例如 ?url=/MindSpace/john/public/demo.html');
}
const origin = resolveRequestOrigin(req) || `http://${HOST}:${PORT}`;
const targetUrl = rawTarget.startsWith('http') ? new URL(rawTarget) : new URL(rawTarget.startsWith('/') ? rawTarget : `/${rawTarget}`, origin);
const upstream = await fetch(targetUrl.toString(), {
headers: {
'user-agent': req.get('user-agent') || 'tkmind-wechat-share-preview',
accept: 'text/html,*/*',
},
redirect: 'follow',
});
if (!upstream.ok) {
return res.status(upstream.status).type('text/plain; charset=utf-8').send(`页面请求失败:HTTP ${upstream.status}`);
}
let html = await upstream.text();
const pageUrl = targetUrl.toString().split('#')[0];
const pageDirUrl = `${pageUrl.slice(0, pageUrl.lastIndexOf('/') + 1)}`;
html = injectOgTags(html, { origin: targetUrl.origin, pageUrl, pageDirUrl });
const preview = extractSharePreviewMeta(html, {
origin: targetUrl.origin,
pageUrl,
pageDirUrl,
});
res.set('Content-Type', 'text/html; charset=utf-8');
res.set('Cache-Control', 'no-store');
return res.send(
renderWechatSharePreviewHtml(preview, {
note: '此预览读取页面最终 HTML 中的 Open Graph 标签,可用来对照微信里粘贴链接后的卡片效果。',
}),
);
} catch (err) {
return res.status(500).type('text/plain; charset=utf-8').send(String(err?.message ?? err));
}
});
app.use('/dev', express.static(path.join(__dirname, 'public/dev'), { maxAge: 0 }));
app.get(/^\/MP_verify_[A-Za-z0-9]+\.txt$/, (req, res) => {
const fileName = path.basename(req.path);