From d6643266486016a5aa449a99c75f37e865975d90 Mon Sep 17 00:00:00 2001 From: Vaica <94612053+zqlit@users.noreply.github.com> Date: Mon, 22 Jun 2026 00:41:05 +0800 Subject: [PATCH] 0041 --- write-server/docker-compose.yml | 1 + write-server/entrypoint.sh | 1 + write-server/src/app/edit/[slug]/page.tsx | 4 ++-- write-server/src/app/page.tsx | 17 +++++++++++++---- write-server/src/lib/bot/sessions.ts | 4 +++- write-server/src/lib/config.ts | 2 +- write-server/src/lib/posts.ts | 7 ++++--- write-server/src/lib/recycle.ts | 17 ++++++++++++++--- 8 files changed, 39 insertions(+), 14 deletions(-) diff --git a/write-server/docker-compose.yml b/write-server/docker-compose.yml index cde41113..4ea0d22a 100644 --- a/write-server/docker-compose.yml +++ b/write-server/docker-compose.yml @@ -29,6 +29,7 @@ services: - PORT=8016 - BLOG_ROOT=/blog - TZ=Asia/Shanghai + - HUGO_PREVIEW=http://127.0.0.1:1313 networks: - write-network healthcheck: diff --git a/write-server/entrypoint.sh b/write-server/entrypoint.sh index 39f82270..49ea21ff 100644 --- a/write-server/entrypoint.sh +++ b/write-server/entrypoint.sh @@ -25,6 +25,7 @@ fi # 配置 git(容器内 nextjs 用户) su-exec nextjs git config --global user.name "Write Server" 2>/dev/null su-exec nextjs git config --global user.email "write@usj.cc" 2>/dev/null +su-exec nextjs git config --global --add safe.directory /blog 2>/dev/null # 以 nextjs 用户启动应用 exec su-exec nextjs "$@" diff --git a/write-server/src/app/edit/[slug]/page.tsx b/write-server/src/app/edit/[slug]/page.tsx index fd2611ab..87039319 100644 --- a/write-server/src/app/edit/[slug]/page.tsx +++ b/write-server/src/app/edit/[slug]/page.tsx @@ -4,7 +4,6 @@ import { useState, useEffect, use } from "react"; import { useRouter } from "next/navigation"; import { ArrowLeft, Eye } from "lucide-react"; import PostEditor from "@/components/PostEditor"; -import { HUGO_PREVIEW } from "@/lib/config"; import type { Post } from "@/lib/posts"; export default function EditPostPage({ @@ -74,7 +73,8 @@ export default function EditPostPage({ type="button" onClick={async () => { await fetch("/api/hugo"); - window.open(`${HUGO_PREVIEW}/${post.frontMatter.slug}.html`, "_blank"); + const previewBase = `${location.protocol}//${location.hostname}:1313`; + window.open(`${previewBase}/${post.frontMatter.slug}.html`, "_blank"); }} className="flex items-center gap-1.5 px-3 py-1.5 text-[12px] rounded-md border border-line text-muted hover:text-accent hover:border-accent/30 transition-colors" title="预览" diff --git a/write-server/src/app/page.tsx b/write-server/src/app/page.tsx index 6d96e514..a89597a0 100644 --- a/write-server/src/app/page.tsx +++ b/write-server/src/app/page.tsx @@ -8,7 +8,6 @@ import { FileText as PostIcon, Rss, Link2, GripVertical, X, CheckSquare, Square, } from "lucide-react"; import Pagination from "@/components/Pagination"; -import { HUGO_PREVIEW } from "@/lib/config"; interface PostItem { slug: string; @@ -64,13 +63,23 @@ export default function HomePage() { const handleDelete = async (slug: string, title: string) => { if (!confirm(`确定删除「${title}」吗?`)) return; - await fetch(`/api/posts/${slug}`, { method: "DELETE" }); - fetchPosts(); + try { + const res = await fetch(`/api/posts/${slug}`, { method: "DELETE" }); + const data = await res.json(); + if (!res.ok) { + alert("删除失败:" + (data.error || "未知错误")); + return; + } + fetchPosts(); + } catch (e) { + alert("删除失败:" + (e instanceof Error ? e.message : String(e))); + } }; const handlePreview = async (slug: string) => { await fetch("/api/hugo"); - window.open(`${HUGO_PREVIEW}/${slug}.html`, "_blank"); + const previewBase = `${location.protocol}//${location.hostname}:1313`; + window.open(`${previewBase}/${slug}.html`, "_blank"); }; const toggleSelect = (slug: string) => { diff --git a/write-server/src/lib/bot/sessions.ts b/write-server/src/lib/bot/sessions.ts index fba84b86..66a672ec 100644 --- a/write-server/src/lib/bot/sessions.ts +++ b/write-server/src/lib/bot/sessions.ts @@ -23,7 +23,9 @@ function computeDirPath(title: string, date?: string): string { .replace(/[^\w一-鿿㐀-䶿]+/g, "-") .replace(/^-+|-+$/g, "") .toLowerCase() || "post"; - return path.join(CONTENT_DIR, year, `${dateStr}-${titlePart}`); + // 加时间戳避免同标题同天冲突 + const ts = Date.now().toString(36); + return path.join(CONTENT_DIR, year, `${dateStr}-${titlePart}-${ts}`); } const sessions = new Map(); diff --git a/write-server/src/lib/config.ts b/write-server/src/lib/config.ts index d346452e..74883fa6 100644 --- a/write-server/src/lib/config.ts +++ b/write-server/src/lib/config.ts @@ -15,7 +15,7 @@ export const ARTALK_SERVER = process.env.ARTALK_SERVER || "https://artalk.usj.cc export const ARTALK_SITE = process.env.ARTALK_SITE || "优世界"; export const ITEMS_PER_PAGE = parseInt(process.env.ITEMS_PER_PAGE || "20", 10); export const RECYCLE_DAYS = parseInt(process.env.RECYCLE_DAYS || "30", 10); -export const HUGO_PREVIEW = process.env.HUGO_PREVIEW || "http://localhost:1313"; +export const HUGO_PREVIEW = process.env.NEXT_PUBLIC_HUGO_PREVIEW || process.env.HUGO_PREVIEW || "http://localhost:1313"; export const RSS_API_BASE = process.env.RSS_API_BASE || "https://api.usj.cc"; export const RSS_API_TOKEN = process.env.RSS_API_TOKEN || ""; export const WECHAT_APP_ID = process.env.WECHAT_APP_ID || ""; diff --git a/write-server/src/lib/posts.ts b/write-server/src/lib/posts.ts index 4246086d..3b76e404 100644 --- a/write-server/src/lib/posts.ts +++ b/write-server/src/lib/posts.ts @@ -155,12 +155,13 @@ export function createPost(frontMatter: PostFrontMatter, content: string): strin const date = new Date(frontMatter.date || new Date()); const year = date.getFullYear().toString(); const dateStr = date.toISOString().slice(0, 10); - // Folder name: date + sanitized title (human-readable on disk) const titlePart = (frontMatter.title || "untitled") .replace(/[^\w一-鿿㐀-䶿]+/g, "-") .replace(/^-+|-+$/g, "") .toLowerCase() || "post"; - const dirName = `${dateStr}-${titlePart}`; + // 用 slug 或时间戳确保目录唯一 + const uniquePart = frontMatter.slug || Date.now().toString(36); + const dirName = `${dateStr}-${titlePart}-${uniquePart}`; const dirPath = path.join(CONTENT_DIR, year, dirName); if (fs.existsSync(path.join(dirPath, "index.md"))) { @@ -206,7 +207,7 @@ export function updatePost( .replace(/[^\w一-鿿㐀-䶿]+/g, "-") .replace(/^-+|-+$/g, "") .toLowerCase() || "post"; - const newDirName = `${newDateStr}-${titlePart}`; + const newDirName = `${newDateStr}-${titlePart}-${frontMatter.slug}`; const newDirPath = path.join(CONTENT_DIR, newYear, newDirName); if (fs.existsSync(newDirPath) && newDirPath !== post.dirPath) { diff --git a/write-server/src/lib/recycle.ts b/write-server/src/lib/recycle.ts index 5db95285..412861cb 100644 --- a/write-server/src/lib/recycle.ts +++ b/write-server/src/lib/recycle.ts @@ -47,12 +47,18 @@ export function moveToRecycle( expiresAt: expires.toISOString(), }; - // Move post directory to recycle + // Move post directory to recycle (fallback to copy+delete for cross-filesystem) const targetDir = path.join(RECYCLE_POSTS_DIR, slug); if (fs.existsSync(targetDir)) { fs.rmSync(targetDir, { recursive: true, force: true }); } - fs.renameSync(sourceDir, targetDir); + try { + fs.renameSync(sourceDir, targetDir); + } catch (_e) { + // Cross-filesystem: copy then delete + fs.cpSync(sourceDir, targetDir, { recursive: true }); + fs.rmSync(sourceDir, { recursive: true, force: true }); + } // Save metadata const metaPath = path.join(RECYCLE_META_DIR, `${slug}.json`); @@ -110,7 +116,12 @@ export function restoreFromRecycle(slug: string): boolean { fs.rmSync(meta.originalPath, { recursive: true, force: true }); } - fs.renameSync(recyclePostPath, meta.originalPath); + try { + fs.renameSync(recyclePostPath, meta.originalPath); + } catch (_e) { + fs.cpSync(recyclePostPath, meta.originalPath, { recursive: true }); + fs.rmSync(recyclePostPath, { recursive: true, force: true }); + } fs.unlinkSync(metaPath); return true;