Extract memind_adm admin server, add local dev tooling, and remove image-generation.

Split platform admin and ops APIs into standalone admin-server.mjs with network guards; simplify billing to RMB token pricing, refactor user auth, and add rsync deploy plus local-test scripts and docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Your Name
2026-06-17 16:39:39 -07:00
parent ab0718938e
commit b0f5d6a51c
98 changed files with 5394 additions and 3010 deletions
+31 -184
View File
@@ -1,11 +1,6 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import {
migrateUserPublishDir,
PUBLISH_ROOT_DIR,
PUBLISH_KEY_UUID,
resolveLegacyPublishDir,
} from './user-publish.mjs';
import { PUBLISH_ROOT_DIR, PUBLISH_KEY_UUID } from './user-publish.mjs';
import { workspaceThumbnailRelativePath } from './mindspace-workspace-thumbnails.mjs';
const URL_PATTERN =
@@ -28,10 +23,11 @@ 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]);
const ownedByUser =
(normalizedUserId && owner === normalizedUserId) ||
(normalizedUsername && owner === normalizedUsername);
if ((normalizedUserId || normalizedUsername) && !ownedByUser) continue;
if (normalizedUserId) {
if (owner !== normalizedUserId) continue;
} else if (normalizedUsername && owner !== normalizedUsername) {
continue;
}
const key = `${owner}/${relativePath}`;
if (seen.has(key)) continue;
seen.add(key);
@@ -118,7 +114,7 @@ async function walkPublishHtmlByBasename(publishRoot, basename, maxDepth = 6, de
return null;
}
async function findPublishHtmlInRoot(publishRoot, relativePath, { readFile = fs.readFile } = {}) {
export async function findPublishHtml(h5Root, userId, relativePath) {
const normalized = String(relativePath ?? '').replace(/^\/+/, '');
const basename = path.basename(normalized);
const candidates = [
@@ -128,59 +124,22 @@ async function findPublishHtmlInRoot(publishRoot, relativePath, { readFile = fs.
].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 {
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),
};
return await readPublishHtml(h5Root, userId, candidate);
} 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('/');
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);
}
return readPublishHtml(h5Root, userId, resolvedRelativePath);
}
throw Object.assign(new Error('无法读取链接页面内容'), { code: 'static_page_not_found' });
@@ -256,9 +215,8 @@ function dedupeDuplicatedFilename(filename) {
if (half >= 2 && base.slice(0, half) === base.slice(half)) {
base = base.slice(0, half);
}
base = base.replace(/(.{4,}?)\1+/g, '$1');
base = base.replace(/(.+?)\1+/g, '$1');
base = base.replace(/-v-v(\d+)/g, '-v$1');
base = base.replace(/-v(\d)\1+$/g, '-v$1');
return `${base}.html`;
}
@@ -330,123 +288,23 @@ export function analyzeChatMessageForSave({
};
}
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 = {}) {
export async function resolveStaticHtmlContent(analysis) {
if (analysis.contentMode !== 'static_html' || !analysis.selectedLink) {
return null;
}
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;
}
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,
};
}
export async function resolveChatSaveAnalysis({
@@ -455,8 +313,6 @@ export async function resolveChatSaveAnalysis({
username,
h5Root,
selectedLinkIndex = 0,
publicBaseUrl,
fetchImpl,
}) {
const sanitized = sanitizeMessageContentForSave(content);
let analysis = analyzeChatMessageForSave({
@@ -467,11 +323,9 @@ export async function resolveChatSaveAnalysis({
selectedLinkIndex,
});
const resolveOptions = { publicBaseUrl, fetchImpl };
if (analysis.contentMode === 'static_html' && analysis.selectedLink) {
try {
const resolvedHtml = await resolveStaticHtmlContent(analysis, resolveOptions);
const resolvedHtml = await resolveStaticHtmlContent(analysis);
return { analysis, resolvedHtml };
} catch {
// fall through to filename hints
@@ -480,7 +334,7 @@ export async function resolveChatSaveAnalysis({
for (const hint of extractHtmlFilenameHints(sanitized)) {
try {
const loaded = await findPublishHtml(h5Root, userId, hint, { username });
const loaded = await findPublishHtml(h5Root, userId, hint);
const syntheticLink = {
publicUrl:
buildWorkspaceAssetUrl(userId, loaded.relativePath) ??
@@ -519,12 +373,5 @@ 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 };
}