refactor: Business logic and dependencies updates

- 核心服务代码更新 (db, server, auth, proxy)
- Agent 相关模块更新 (mindspace, experience)
- 前端组件和 hooks 更新
- 数据库 schema 更新
- 依赖版本更新
This commit is contained in:
john
2026-06-27 08:25:02 +08:00
parent f1220a7905
commit 25f8223253
20 changed files with 1458 additions and 116 deletions
+110 -6
View File
@@ -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,
};
}