refactor: Business logic and dependencies updates
- 核心服务代码更新 (db, server, auth, proxy) - Agent 相关模块更新 (mindspace, experience) - 前端组件和 hooks 更新 - 数据库 schema 更新 - 依赖版本更新
This commit is contained in:
+110
-6
@@ -6,6 +6,7 @@ import { replacePrivateResourceReferences, scanContent } from './mindspace-conte
|
||||
import { pageInternals } from './mindspace-pages.mjs';
|
||||
import { loadMindSpaceConfig } from './mindspace-config.mjs';
|
||||
import { resolvePublicBaseUrl } from './user-publish.mjs';
|
||||
import { createImgproxySigner } from './imgproxy-signer.mjs';
|
||||
|
||||
const SCANNER_VERSION = 'mindspace-content-v1';
|
||||
const PRIVATE_ASSET_DOWNLOAD_URL_PATTERN =
|
||||
@@ -159,7 +160,13 @@ function buildPublicationThumbnailFallback(ownerSlug, urlSlug) {
|
||||
return `/u/${encodeURIComponent(ownerSlug)}/pages/${encodeURIComponent(urlSlug)}.thumbnail.png`;
|
||||
}
|
||||
|
||||
async function localizePrivateImageReferences({ pool, userId, html, absoluteStoragePath }) {
|
||||
async function localizePrivateImageReferences({
|
||||
pool,
|
||||
userId,
|
||||
html,
|
||||
absoluteStoragePath,
|
||||
imgproxySigner = null,
|
||||
}) {
|
||||
const source = String(html ?? '');
|
||||
const matches = [...source.matchAll(PRIVATE_ASSET_DOWNLOAD_URL_PATTERN)];
|
||||
if (matches.length === 0) return source;
|
||||
@@ -181,9 +188,14 @@ async function localizePrivateImageReferences({ pool, userId, html, absoluteStor
|
||||
for (const assetId of assetIds) {
|
||||
const asset = byId.get(assetId);
|
||||
if (!asset) continue;
|
||||
const mimeType = String(asset.mime_type || 'application/octet-stream');
|
||||
const buffer = await fs.readFile(absoluteStoragePath(asset.storage_key));
|
||||
replacements.set(assetId, `data:${mimeType};base64,${buffer.toString('base64')}`);
|
||||
|
||||
if (imgproxySigner) {
|
||||
replacements.set(assetId, imgproxySigner.buildUrl(imgproxySigner.baseUrl, asset.storage_key, 'display'));
|
||||
} else {
|
||||
const mimeType = String(asset.mime_type || 'application/octet-stream');
|
||||
const buffer = await fs.readFile(absoluteStoragePath(asset.storage_key));
|
||||
replacements.set(assetId, `data:${mimeType};base64,${buffer.toString('base64')}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (replacements.size === 0) return source;
|
||||
@@ -199,6 +211,7 @@ async function prepareHtmlPublishContent({
|
||||
ownerSlug,
|
||||
urlSlug,
|
||||
absoluteStoragePath,
|
||||
imgproxySigner = null,
|
||||
}) {
|
||||
let publishContent = (await localizeGoogleFontsCss(html)).html;
|
||||
publishContent = await localizePrivateImageReferences({
|
||||
@@ -206,6 +219,7 @@ async function prepareHtmlPublishContent({
|
||||
userId,
|
||||
html: publishContent,
|
||||
absoluteStoragePath,
|
||||
imgproxySigner,
|
||||
});
|
||||
return replacePrivateResourceReferences(
|
||||
publishContent,
|
||||
@@ -217,6 +231,21 @@ export function createPublicationService(pool, options = {}) {
|
||||
const storageRoot = path.resolve(options.storageRoot ?? path.join(process.cwd(), 'data', 'mindspace'));
|
||||
const idFactory = options.idFactory ?? (() => crypto.randomUUID());
|
||||
const publicPageLimitFallback = Number(options.publicPageLimit ?? 5);
|
||||
|
||||
let imgproxySigner = null;
|
||||
if (process.env.IMGPROXY_BASE_URL && process.env.IMGPROXY_SIGNING_KEY && process.env.IMGPROXY_SIGNING_SALT) {
|
||||
try {
|
||||
imgproxySigner = createImgproxySigner(
|
||||
process.env.IMGPROXY_SIGNING_KEY,
|
||||
process.env.IMGPROXY_SIGNING_SALT,
|
||||
);
|
||||
imgproxySigner.baseUrl = process.env.IMGPROXY_BASE_URL;
|
||||
console.log('[Publication] imgproxy signer initialized');
|
||||
} catch (err) {
|
||||
console.warn('[Publication] imgproxy signer init failed:', err instanceof Error ? err.message : err);
|
||||
}
|
||||
}
|
||||
|
||||
const resolvePublicPageLimit = async () => {
|
||||
try {
|
||||
const config = await loadMindSpaceConfig(pool);
|
||||
@@ -309,6 +338,7 @@ export function createPublicationService(pool, options = {}) {
|
||||
ownerSlug,
|
||||
urlSlug,
|
||||
absoluteStoragePath,
|
||||
imgproxySigner: imgproxySigner ? { buildUrl: (path, preset) => imgproxySigner.buildUrl(imgproxySigner.baseUrl, path, preset) } : null,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -549,9 +579,9 @@ export function createPublicationService(pool, options = {}) {
|
||||
await conn.query(
|
||||
`INSERT INTO h5_publish_records
|
||||
(id, user_id, page_id, page_version_id, publish_type, url_slug, public_url,
|
||||
access_mode, password_hash, token_hash, token_prefix, expires_at, published_at,
|
||||
access_mode, password_hash, token_hash, token_prefix, expires_at, user_confirmed_at, published_at,
|
||||
status, view_count, security_scan_id, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, 'page', ?, ?, ?, ?, ?, ?, ?, ?, 'online', 0, ?, ?, ?)`,
|
||||
VALUES (?, ?, ?, ?, 'page', ?, ?, ?, ?, ?, ?, ?, ?, ?, 'online', 0, ?, ?, ?)`,
|
||||
[
|
||||
publishId,
|
||||
userId,
|
||||
@@ -564,6 +594,7 @@ export function createPublicationService(pool, options = {}) {
|
||||
tokenHash,
|
||||
privateToken?.slice(0, 8) ?? null,
|
||||
result.expiresAt,
|
||||
null,
|
||||
now,
|
||||
scanId,
|
||||
now,
|
||||
@@ -605,6 +636,23 @@ export function createPublicationService(pool, options = {}) {
|
||||
WHERE id = ? AND user_id = ?`,
|
||||
[htmlBytes, now, page.space_id, userId],
|
||||
);
|
||||
|
||||
const assetUrlPattern = /\/api\/mindspace\/v1\/assets\/([a-z0-9-]+)/gi;
|
||||
const assetIds = new Set();
|
||||
let match;
|
||||
while ((match = assetUrlPattern.exec(html)) !== null) {
|
||||
assetIds.add(match[1]);
|
||||
}
|
||||
if (assetIds.size > 0) {
|
||||
const refValues = Array.from(assetIds).map((assetId) => [publishId, assetId, now]);
|
||||
await conn.query(
|
||||
`INSERT IGNORE INTO h5_publication_asset_refs
|
||||
(publication_id, asset_id, created_at) VALUES ` +
|
||||
refValues.map(() => '(?, ?, ?)').join(','),
|
||||
refValues.flat(),
|
||||
);
|
||||
}
|
||||
|
||||
await conn.commit();
|
||||
return {
|
||||
...publicationResponse({
|
||||
@@ -642,6 +690,46 @@ export function createPublicationService(pool, options = {}) {
|
||||
return publicationResponse(rows[0]);
|
||||
};
|
||||
|
||||
const updatePublicationStatus = async (userId, publicationId, { accessMode, expiresAt }) => {
|
||||
const conn = await pool.getConnection();
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
const [rows] = await conn.query(
|
||||
`SELECT * FROM h5_publish_records
|
||||
WHERE id = ? AND user_id = ? AND status = 'online' LIMIT 1 FOR UPDATE`,
|
||||
[publicationId, userId],
|
||||
);
|
||||
const publication = rows[0];
|
||||
if (!publication) throw publicationError('发布记录不存在', 'publication_not_found');
|
||||
|
||||
const normalizedMode = normalizeAccessMode(accessMode);
|
||||
const normalizedExpiresAt = normalizeExpiresAt(expiresAt, normalizedMode === 'time_limited');
|
||||
const now = Date.now();
|
||||
|
||||
await conn.query(
|
||||
`UPDATE h5_publish_records
|
||||
SET access_mode = ?, expires_at = ?, user_confirmed_at = ?, updated_at = ?
|
||||
WHERE id = ? AND user_id = ?`,
|
||||
[normalizedMode, normalizedExpiresAt, now, now, publicationId, userId],
|
||||
);
|
||||
await conn.commit();
|
||||
|
||||
const updated = {
|
||||
...publication,
|
||||
access_mode: normalizedMode,
|
||||
expires_at: normalizedExpiresAt,
|
||||
user_confirmed_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
return publicationResponse(updated);
|
||||
} catch (error) {
|
||||
await conn.rollback();
|
||||
throw error;
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
};
|
||||
|
||||
const offline = async (userId, publicationId) => {
|
||||
const conn = await pool.getConnection();
|
||||
try {
|
||||
@@ -832,6 +920,20 @@ export function createPublicationService(pool, options = {}) {
|
||||
return publicHomepageResponse(owner, rows);
|
||||
};
|
||||
|
||||
const cleanupExpiredUnconfirmedPublications = async (now = Date.now()) => {
|
||||
const [result] = await pool.query(
|
||||
`UPDATE h5_publish_records
|
||||
SET access_mode = 'private', expires_at = NULL, updated_at = ?
|
||||
WHERE access_mode = 'public'
|
||||
AND expires_at IS NOT NULL
|
||||
AND expires_at <= ?
|
||||
AND user_confirmed_at IS NULL
|
||||
AND status = 'online'`,
|
||||
[now, now],
|
||||
);
|
||||
return { cleaned: result.affectedRows };
|
||||
};
|
||||
|
||||
return {
|
||||
check,
|
||||
publish,
|
||||
@@ -839,8 +941,10 @@ export function createPublicationService(pool, options = {}) {
|
||||
getPublicHomepage,
|
||||
getStats,
|
||||
offline,
|
||||
updatePublicationStatus,
|
||||
resolvePublic,
|
||||
resolvePrivateLink,
|
||||
cleanupExpiredUnconfirmedPublications,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user