feat(mindspace): 0630004 空间 UI、聊天连接、微信分享与 Agent 能力
含 MindSpace 三列布局与统计修复、聊天加载态与连接降级、平台页脚标记与 og:site_name 微信卡片、勾选资料删除 Agent 接口及内部话术过滤。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+168
-15
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user