feat(chat): add one-click Plaza publish for HTML page messages.

Speed up the quick-plaza path with session snapshot reads, publish timeouts, and a modal fix so parent re-renders no longer abort in-flight requests.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-07-05 19:28:36 +08:00
parent 53eb0014ba
commit 3318c04490
11 changed files with 766 additions and 140 deletions
+84 -9
View File
@@ -122,6 +122,7 @@ import {
normalizePublicHtmlRelativePath,
syncPublicHtmlAfterFinish,
} from './mindspace-public-finish-sync.mjs';
import { quickPlazaFromChat } from './mindspace-chat-plaza.mjs';
import { extractCoverSignals, generateHtmlThumbnail } from './mindspace-thumbnails.mjs';
import {
extractSharePreviewMeta,
@@ -2908,7 +2909,8 @@ function messageText(message) {
.trim();
}
async function resolveOwnedAssistantMessage(userId, sessionId, messageId) {
async function resolveOwnedAssistantMessage(user, sessionId, messageId) {
const userId = user?.id;
if (!sessionId || !messageId) {
throw Object.assign(new Error('缺少来源会话或消息'), {
code: 'invalid_page_input',
@@ -2917,13 +2919,52 @@ async function resolveOwnedAssistantMessage(userId, sessionId, messageId) {
if (!(await userAuth.ownsSession(userId, sessionId))) {
throw Object.assign(new Error('来源会话不存在'), { code: 'source_message_not_found' });
}
const upstream = await tkmindProxy.apiFetch(`/sessions/${encodeURIComponent(sessionId)}`, {
method: 'GET',
});
if (!upstream.ok) {
throw Object.assign(new Error('无法读取来源会话'), { code: 'source_message_not_found' });
let session = null;
if (sessionSnapshotService?.isEnabled()) {
const snapshot = await sessionSnapshotService.get(sessionId).catch(() => null);
if (snapshot?.messages?.length) {
session = {
...snapshot.session,
conversation: sanitizeSessionConversationPublicHtmlLinks(snapshot.messages, user),
};
if (authPool) {
session = await repairSessionConversationFromDb(authPool, session, sessionId, userId);
}
}
}
const session = await upstream.json();
if (!session) {
const target = await tkmindProxy.resolveTarget(sessionId);
let upstream;
try {
upstream = await tkmindProxy.apiFetchTo(
target,
`/sessions/${encodeURIComponent(sessionId)}`,
{ method: 'GET', signal: AbortSignal.timeout(15_000) },
);
} catch (error) {
throw Object.assign(
new Error(
error?.name === 'TimeoutError' || error?.name === 'AbortError'
? '读取来源会话超时,请稍后重试'
: '无法读取来源会话',
),
{ code: 'source_message_not_found' },
);
}
if (!upstream.ok) {
throw Object.assign(new Error('无法读取来源会话'), { code: 'source_message_not_found' });
}
session = await upstream.json();
if (Array.isArray(session.conversation)) {
session.conversation = sanitizeSessionConversationPublicHtmlLinks(session.conversation, user);
}
if (authPool) {
session = await repairSessionConversationFromDb(authPool, session, sessionId, userId);
}
}
const message = (session.conversation ?? []).find((item) => item.id === messageId);
if (!message) {
throw Object.assign(new Error('来源消息不存在'), { code: 'source_message_not_found' });
@@ -2963,7 +3004,7 @@ async function resolveChatSaveBundle(user, h5Root, input = {}) {
const previewTitle = String(input.previewTitle ?? input.preview_title ?? '').trim();
const previewSummary = String(input.previewSummary ?? input.preview_summary ?? '').trim();
const source = await resolveOwnedAssistantMessage(user.id, sessionId, messageId);
const source = await resolveOwnedAssistantMessage(user, sessionId, messageId);
let { analysis, resolvedHtml } = await resolveChatSaveAnalysis({
content: source.content,
userId: user.id,
@@ -3294,6 +3335,40 @@ api.post('/mindspace/v1/pages/quick-share-from-chat', async (req, res) => {
}
});
api.post('/mindspace/v1/pages/quick-plaza-from-chat', async (req, res) => {
if (!mindSpacePages || !mindSpacePublications || !plazaPosts) {
return sendError(res, req, 503, 'plaza_unavailable', 'Plaza 或 MindSpace 未启用');
}
const startedAt = Date.now();
try {
const bundle = await resolveChatSaveBundle(req.currentUser, __dirname, req.body);
const publishDir = resolveMindSpaceUserPublishDir(__dirname, req.currentUser);
const result = await quickPlazaFromChat({
user: req.currentUser,
h5Root: __dirname,
bundle,
body: req.body ?? {},
mindSpacePages,
mindSpacePublications,
plazaPosts,
publishDir,
});
console.info('[quick-plaza] ok', {
ms: Date.now() - startedAt,
pageId: result.pageId,
publicationId: result.publicationId,
postId: result.post?.id,
});
return sendData(res, req, result, 201);
} catch (error) {
console.error('[quick-plaza] error:', { ms: Date.now() - startedAt, error });
if (error?.code && mapPlazaError(error) !== 500) {
return plazaRouteError(res, req, error);
}
return mindSpaceError(res, req, error);
}
});
async function handleChatSaveDocx(req, res) {
if (!mindSpacePages) return res.status(503).json({ message: 'MindSpace 未启用' });
try {
@@ -3348,7 +3423,7 @@ api.post('/mindspace/v1/pages/save-from-chat', async (req, res) => {
throw Object.assign(new Error('无效的保存目标'), { code: 'invalid_category_code' });
}
const source = await resolveOwnedAssistantMessage(
req.currentUser.id,
req.currentUser,
req.body?.session_id,
req.body?.message_id,
);