Add image-designer skill, align billing to DeepSeek ×1, and enrich Plaza demo.

Introduce AI image generation with chat shortcut and agent API, improve MindSpace chat-to-page save resolution, and seed Plaza covers with production deploy scripts.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
John
2026-06-16 16:29:19 -07:00
parent 03690ee354
commit 6d99d762da
155 changed files with 2748 additions and 190 deletions
+184 -31
View File
@@ -1,6 +1,11 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { PUBLISH_ROOT_DIR, PUBLISH_KEY_UUID } from './user-publish.mjs';
import {
migrateUserPublishDir,
PUBLISH_ROOT_DIR,
PUBLISH_KEY_UUID,
resolveLegacyPublishDir,
} from './user-publish.mjs';
import { workspaceThumbnailRelativePath } from './mindspace-workspace-thumbnails.mjs';
const URL_PATTERN =
@@ -23,11 +28,10 @@ export function extractStaticPageLinks(content, { userId, username } = {}) {
for (const match of text.matchAll(URL_PATTERN)) {
const owner = decodePathSegment(match[1]).toLowerCase();
const relativePath = decodePathSegment(match[2]);
if (normalizedUserId) {
if (owner !== normalizedUserId) continue;
} else if (normalizedUsername && owner !== normalizedUsername) {
continue;
}
const ownedByUser =
(normalizedUserId && owner === normalizedUserId) ||
(normalizedUsername && owner === normalizedUsername);
if ((normalizedUserId || normalizedUsername) && !ownedByUser) continue;
const key = `${owner}/${relativePath}`;
if (seen.has(key)) continue;
seen.add(key);
@@ -114,7 +118,7 @@ async function walkPublishHtmlByBasename(publishRoot, basename, maxDepth = 6, de
return null;
}
export async function findPublishHtml(h5Root, userId, relativePath) {
async function findPublishHtmlInRoot(publishRoot, relativePath, { readFile = fs.readFile } = {}) {
const normalized = String(relativePath ?? '').replace(/^\/+/, '');
const basename = path.basename(normalized);
const candidates = [
@@ -124,22 +128,59 @@ export async function findPublishHtml(h5Root, userId, relativePath) {
].filter((value, index, list) => value && list.indexOf(value) === index);
for (const candidate of candidates) {
const absolute = path.resolve(publishRoot, candidate);
if (absolute !== publishRoot && !absolute.startsWith(`${publishRoot}${path.sep}`)) continue;
if (!absolute.toLowerCase().endsWith('.html')) continue;
try {
return await readPublishHtml(h5Root, userId, candidate);
const content = await readFile(absolute, 'utf8');
if (!content.trim()) continue;
const resolvedRelativePath = path.relative(publishRoot, absolute).split(path.sep).join('/');
return {
absolute,
content,
relativePath: resolvedRelativePath,
filename: path.basename(resolvedRelativePath),
};
} catch {
// try next candidate
}
}
const publishRoot = path.resolve(
h5Root,
PUBLISH_ROOT_DIR,
String(userId ?? '').trim().toLowerCase(),
);
const absolute = await walkPublishHtmlByBasename(publishRoot, basename);
if (absolute) {
const resolvedRelativePath = path.relative(publishRoot, absolute).split(path.sep).join('/');
return readPublishHtml(h5Root, userId, resolvedRelativePath);
const content = await readFile(absolute, 'utf8');
if (!content.trim()) {
throw Object.assign(new Error('页面内容为空'), { code: 'empty_page_content' });
}
return {
absolute,
content,
relativePath: resolvedRelativePath,
filename: path.basename(resolvedRelativePath),
};
}
return null;
}
export async function findPublishHtml(h5Root, userId, relativePath, { username } = {}) {
const key = String(userId ?? '').trim().toLowerCase();
if (username && key) {
migrateUserPublishDir(h5Root, { id: key, username });
}
const publishRoot = path.resolve(h5Root, PUBLISH_ROOT_DIR, key);
const primary = await findPublishHtmlInRoot(publishRoot, relativePath);
if (primary) return primary;
const legacyRoot = username ? resolveLegacyPublishDir(h5Root, { username }) : null;
if (legacyRoot && path.resolve(legacyRoot) !== publishRoot) {
const legacy = await findPublishHtmlInRoot(legacyRoot, relativePath);
if (legacy) {
migrateUserPublishDir(h5Root, { id: key, username });
return readPublishHtml(h5Root, userId, legacy.relativePath);
}
}
throw Object.assign(new Error('无法读取链接页面内容'), { code: 'static_page_not_found' });
@@ -215,8 +256,9 @@ function dedupeDuplicatedFilename(filename) {
if (half >= 2 && base.slice(0, half) === base.slice(half)) {
base = base.slice(0, half);
}
base = base.replace(/(.+?)\1+/g, '$1');
base = base.replace(/(.{4,}?)\1+/g, '$1');
base = base.replace(/-v-v(\d+)/g, '-v$1');
base = base.replace(/-v(\d)\1+$/g, '-v$1');
return `${base}.html`;
}
@@ -288,23 +330,123 @@ export function analyzeChatMessageForSave({
};
}
export async function resolveStaticHtmlContent(analysis) {
function uniqueRelativePaths(relativePath) {
const basename = path.basename(String(relativePath ?? ''));
return [
String(relativePath ?? '').replace(/^\/+/, ''),
String(relativePath ?? '')
.replace(/^\/+/, '')
.replace(/^public\//, ''),
`public/${basename}`,
basename,
].filter((value, index, list) => value && list.indexOf(value) === index);
}
function buildPublishHtmlFetchUrls({ userId, link, publicBaseUrl }) {
const urls = new Set();
const base = String(publicBaseUrl ?? '').replace(/\/$/, '');
if (base && link) {
for (const rel of uniqueRelativePaths(link.relativePath)) {
const asset = buildWorkspaceAssetUrl(userId, rel);
if (asset) urls.add(`${base}${asset}`);
}
}
if (link?.publicUrl) urls.add(link.publicUrl);
return [...urls];
}
async function mirrorFetchedPublishHtml(h5Root, userId, relativePath, content, { username } = {}) {
try {
if (username) {
migrateUserPublishDir(h5Root, { id: userId, username });
}
const absolute = resolvePublishHtmlAbsolutePath(h5Root, userId, relativePath);
await fs.mkdir(path.dirname(absolute), { recursive: true });
await fs.writeFile(absolute, content, 'utf8');
return absolute;
} catch {
return null;
}
}
export async function fetchPublishHtmlFallback(
analysis,
{ publicBaseUrl, fetchImpl = fetch } = {},
) {
if (analysis.contentMode !== 'static_html' || !analysis.selectedLink) return null;
const candidatePaths = uniqueRelativePaths(analysis.selectedLink.relativePath);
const urls = buildPublishHtmlFetchUrls({
userId: analysis.userId,
link: analysis.selectedLink,
publicBaseUrl,
});
for (const url of urls) {
try {
const response = await fetchImpl(url, { redirect: 'follow' });
if (!response.ok) continue;
const contentType = response.headers.get('content-type') ?? '';
if (contentType.includes('json')) continue;
const content = await response.text();
if (!content.trim() || !/<html/i.test(content)) continue;
let relativePath = candidatePaths[0];
for (const rel of candidatePaths) {
if (url.includes(encodeURIComponent(rel)) || url.endsWith(`/${rel}`) || url.endsWith(rel)) {
relativePath = rel;
break;
}
}
const absolute = await mirrorFetchedPublishHtml(
analysis.h5Root,
analysis.userId,
relativePath,
content,
{ username: analysis.username },
);
const title = titleFromHtml(content);
const summary = summaryFromHtml(content);
return {
absolute:
absolute ??
resolvePublishHtmlAbsolutePath(analysis.h5Root, analysis.userId, relativePath),
content,
relativePath,
filename: path.basename(relativePath),
suggestedTitle: title || analysis.suggestedTitle,
suggestedSummary: summary || analysis.suggestedSummary,
publicUrl: analysis.selectedLink.publicUrl,
};
} catch {
// try next url
}
}
return null;
}
export async function resolveStaticHtmlContent(analysis, options = {}) {
if (analysis.contentMode !== 'static_html' || !analysis.selectedLink) {
return null;
}
const loaded = await findPublishHtml(
analysis.h5Root,
analysis.userId,
analysis.selectedLink.relativePath,
);
const title = titleFromHtml(loaded.content);
const summary = summaryFromHtml(loaded.content);
return {
...loaded,
suggestedTitle: title || analysis.suggestedTitle,
suggestedSummary: summary || analysis.suggestedSummary,
publicUrl: analysis.selectedLink.publicUrl,
};
try {
const loaded = await findPublishHtml(
analysis.h5Root,
analysis.userId,
analysis.selectedLink.relativePath,
{ username: analysis.username },
);
const title = titleFromHtml(loaded.content);
const summary = summaryFromHtml(loaded.content);
return {
...loaded,
suggestedTitle: title || analysis.suggestedTitle,
suggestedSummary: summary || analysis.suggestedSummary,
publicUrl: analysis.selectedLink.publicUrl,
};
} catch (error) {
if (error?.code !== 'static_page_not_found') throw error;
const fetched = await fetchPublishHtmlFallback(analysis, options);
if (fetched) return fetched;
throw error;
}
}
export async function resolveChatSaveAnalysis({
@@ -313,6 +455,8 @@ export async function resolveChatSaveAnalysis({
username,
h5Root,
selectedLinkIndex = 0,
publicBaseUrl,
fetchImpl,
}) {
const sanitized = sanitizeMessageContentForSave(content);
let analysis = analyzeChatMessageForSave({
@@ -323,9 +467,11 @@ export async function resolveChatSaveAnalysis({
selectedLinkIndex,
});
const resolveOptions = { publicBaseUrl, fetchImpl };
if (analysis.contentMode === 'static_html' && analysis.selectedLink) {
try {
const resolvedHtml = await resolveStaticHtmlContent(analysis);
const resolvedHtml = await resolveStaticHtmlContent(analysis, resolveOptions);
return { analysis, resolvedHtml };
} catch {
// fall through to filename hints
@@ -334,7 +480,7 @@ export async function resolveChatSaveAnalysis({
for (const hint of extractHtmlFilenameHints(sanitized)) {
try {
const loaded = await findPublishHtml(h5Root, userId, hint);
const loaded = await findPublishHtml(h5Root, userId, hint, { username });
const syntheticLink = {
publicUrl:
buildWorkspaceAssetUrl(userId, loaded.relativePath) ??
@@ -373,5 +519,12 @@ export async function resolveChatSaveAnalysis({
}
}
if (analysis.contentMode === 'static_html' && analysis.selectedLink) {
const fetched = await fetchPublishHtmlFallback(analysis, resolveOptions);
if (fetched) {
return { analysis, resolvedHtml: fetched };
}
}
return { analysis, resolvedHtml: null };
}