From 36a3fbfe1d065430db5398339c8944ad6717f7f5 Mon Sep 17 00:00:00 2001 From: npmrun <1549469775@qq.com> Date: Sat, 18 Apr 2026 20:41:57 +0800 Subject: [PATCH] feat(media): constants and markdown/cover URL extraction Made-with: Cursor --- server/constants/media.ts | 11 ++++++++ server/utils/post-media-urls.ts | 61 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 server/constants/media.ts create mode 100644 server/utils/post-media-urls.ts diff --git a/server/constants/media.ts b/server/constants/media.ts new file mode 100644 index 0000000..465548d --- /dev/null +++ b/server/constants/media.ts @@ -0,0 +1,11 @@ +/** 与 `upload` 返回及静态路径一致,无前导 host */ +export const POST_MEDIA_PUBLIC_PREFIX = "/public/assets/"; + +/** 从未引用:created_at 起算;曾引用:dereferenced_at 起算 */ +export const MEDIA_ORPHAN_GRACE_HOURS_NEVER_REF = 24; +export const MEDIA_ORPHAN_GRACE_HOURS_AFTER_DEREF = 24; + +export const MEDIA_IMAGE_MAX_WIDTH_PX = 1920; +export const MEDIA_WEBP_QUALITY = 82; + +export const RELATIVE_ASSETS_DIR = "public/assets"; diff --git a/server/utils/post-media-urls.ts b/server/utils/post-media-urls.ts new file mode 100644 index 0000000..52e27e4 --- /dev/null +++ b/server/utils/post-media-urls.ts @@ -0,0 +1,61 @@ +import { POST_MEDIA_PUBLIC_PREFIX } from "#server/constants/media"; + +const MD_IMG_RE = /!\[[^\]]*]\(([^)]+)\)/g; + +function normalizeUrl(raw: string): string { + const t = raw.trim().replace(/^<|>$/g, "").split(/\s+/)[0] ?? ""; + if (t.startsWith(POST_MEDIA_PUBLIC_PREFIX)) { + return t; + } + return ""; +} + +/** 从 markdown 中提取本站图片 URL(去重,顺序稳定) */ +export function extractMediaUrlsFromMarkdown(markdown: string): string[] { + const out: string[] = []; + const seen = new Set(); + let m: RegExpExecArray | null; + const re = new RegExp(MD_IMG_RE.source, MD_IMG_RE.flags); + while ((m = re.exec(markdown)) !== null) { + const u = normalizeUrl(m[1] ?? ""); + if (u && !seen.has(u)) { + seen.add(u); + out.push(u); + } + } + return out; +} + +export function extractMediaUrlsFromCover(coverUrl: string | null | undefined): string[] { + if (!coverUrl) { + return []; + } + const u = normalizeUrl(coverUrl); + return u ? [u] : []; +} + +export function mergePostMediaUrls(bodyMarkdown: string, coverUrl: string | null | undefined): string[] { + const a = extractMediaUrlsFromMarkdown(bodyMarkdown); + const b = extractMediaUrlsFromCover(coverUrl); + const seen = new Set(a); + for (const u of b) { + if (!seen.has(u)) { + seen.add(u); + a.push(u); + } + } + return a; +} + +/** `/public/assets/foo.webp` → `foo.webp` */ +export function publicAssetUrlToStorageKey(url: string): string | null { + const t = url.trim(); + if (!t.startsWith(POST_MEDIA_PUBLIC_PREFIX)) { + return null; + } + const key = t.slice(POST_MEDIA_PUBLIC_PREFIX.length).replace(/^\/+/, ""); + if (!key || key.includes("..") || key.includes("/") || key.includes("\\")) { + return null; + } + return key; +}