added:增加Linux-server
This commit is contained in:
1 parent
712116d21d
commit
bd96aadb2f
90 files changed
+16784
-1
No files matched your search
@@ -0,0 +1,57 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { aiChat, AI_PROMPTS } from "@/lib/ai";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { action, text, title, content } = body;
|
||||
|
||||
if (!action) {
|
||||
return NextResponse.json({ error: "action is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
let systemPrompt: string;
|
||||
let userMessage: string;
|
||||
|
||||
switch (action) {
|
||||
case "polish":
|
||||
systemPrompt = AI_PROMPTS.polish;
|
||||
userMessage = text || "";
|
||||
break;
|
||||
case "expand":
|
||||
systemPrompt = AI_PROMPTS.expand;
|
||||
userMessage = text || "";
|
||||
break;
|
||||
case "shorten":
|
||||
systemPrompt = AI_PROMPTS.shorten;
|
||||
userMessage = text || "";
|
||||
break;
|
||||
case "fix":
|
||||
systemPrompt = AI_PROMPTS.fix;
|
||||
userMessage = text || "";
|
||||
break;
|
||||
case "summary":
|
||||
systemPrompt = "你是一个编辑。根据文章标题和内容写简短摘要(100字以内)。只返回摘要,不要解释。";
|
||||
userMessage = `标题:${title || ""}\n\n内容片段:${(content || "").slice(0, 2000)}`;
|
||||
break;
|
||||
case "tags":
|
||||
systemPrompt = "你是一个编辑。根据文章标题和内容提取3-5个中文标签,用顿号分隔。只返回标签,不要解释。";
|
||||
userMessage = `标题:${title || ""}\n\n内容片段:${(content || "").slice(0, 2000)}`;
|
||||
break;
|
||||
default:
|
||||
return NextResponse.json({ error: `Unknown action: ${action}` }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!userMessage.trim()) {
|
||||
return NextResponse.json({ error: "No text provided" }, { status: 400 });
|
||||
}
|
||||
|
||||
const result = await aiChat(systemPrompt, userMessage);
|
||||
return NextResponse.json({ result });
|
||||
} catch (error: unknown) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "AI error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { hugo } from "@/lib/hugo";
|
||||
|
||||
export async function POST() {
|
||||
try {
|
||||
hugo.stop();
|
||||
const result = await hugo.start();
|
||||
return NextResponse.json({ ok: result.ok });
|
||||
} catch (error: unknown) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "Failed" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { hugo } from "@/lib/hugo";
|
||||
import { TG_BOT_TOKEN } from "@/lib/config";
|
||||
|
||||
export async function GET() {
|
||||
const hugoStatus = hugo.status();
|
||||
return NextResponse.json({
|
||||
bot: {
|
||||
enabled: !!TG_BOT_TOKEN,
|
||||
polling: !!TG_BOT_TOKEN,
|
||||
},
|
||||
hugo: hugoStatus,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { updateComment, deleteComment } from "@/lib/artalk";
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
const { token, ...data } = body;
|
||||
|
||||
if (token) {
|
||||
const { setAuthToken } = await import("@/lib/artalk");
|
||||
setAuthToken(token);
|
||||
}
|
||||
|
||||
const result = await updateComment(parseInt(id, 10), data);
|
||||
return NextResponse.json(result);
|
||||
} catch (error: unknown) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "Unknown error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
|
||||
if (body?.token) {
|
||||
const { setAuthToken } = await import("@/lib/artalk");
|
||||
setAuthToken(body.token);
|
||||
}
|
||||
|
||||
await deleteComment(parseInt(id, 10));
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error: unknown) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "Unknown error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { ARTALK_SERVER, ARTALK_SITE } from "@/lib/config";
|
||||
|
||||
async function getArtalkData(path: string, token: string) {
|
||||
const url = `${ARTALK_SERVER}${path}`;
|
||||
const res = await fetch(url, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!res.ok) throw new Error(`Artalk API error: ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function deleteArtalkPage(id: number, token: string) {
|
||||
const url = `${ARTALK_SERVER}/api/v2/pages/${id}`;
|
||||
const res = await fetch(url, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ site_name: ARTALK_SITE }),
|
||||
});
|
||||
return res.ok;
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const token = (body as Record<string, string>).token;
|
||||
if (!token) {
|
||||
return NextResponse.json({ error: "token required" }, { status: 400 });
|
||||
}
|
||||
|
||||
// 1. Login to get a valid token
|
||||
const allPages: { id: number; key: string; title: string; comment_count?: number }[] = [];
|
||||
let offset = 0;
|
||||
const limit = 200;
|
||||
|
||||
// Fetch all pages
|
||||
while (true) {
|
||||
const data = await getArtalkData(
|
||||
`/api/v2/pages?site_name=${encodeURIComponent(ARTALK_SITE)}&limit=${limit}&offset=${offset}`,
|
||||
token
|
||||
);
|
||||
const pages = data.pages || data.data || [];
|
||||
if (pages.length === 0) break;
|
||||
allPages.push(...pages);
|
||||
offset += limit;
|
||||
if (pages.length < limit) break;
|
||||
}
|
||||
|
||||
// 2. Group by title
|
||||
const byTitle = new Map<string, typeof allPages>();
|
||||
for (const p of allPages) {
|
||||
const t = (p.title || "").trim();
|
||||
if (!byTitle.has(t)) byTitle.set(t, []);
|
||||
byTitle.get(t)!.push(p);
|
||||
}
|
||||
|
||||
// 3. For each duplicate group, keep the one with most comments, delete the rest (especially 0-comment ones)
|
||||
let deleted = 0;
|
||||
const deletedIds: number[] = [];
|
||||
|
||||
for (const [title, pages] of byTitle) {
|
||||
if (pages.length <= 1) continue;
|
||||
|
||||
// Sort: most comments first, then by id desc (newest first)
|
||||
pages.sort((a, b) => {
|
||||
const ca = a.comment_count ?? 0;
|
||||
const cb = b.comment_count ?? 0;
|
||||
if (ca !== cb) return cb - ca;
|
||||
return b.id - a.id;
|
||||
});
|
||||
|
||||
// Keep the first one, delete the rest
|
||||
const [keep, ...toDelete] = pages;
|
||||
for (const p of toDelete) {
|
||||
try {
|
||||
const ok = await deleteArtalkPage(p.id, token);
|
||||
if (ok) {
|
||||
deleted++;
|
||||
deletedIds.push(p.id);
|
||||
}
|
||||
} catch { /* skip failed deletions */ }
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Also clean up pages with 0 comments that are NOT part of duplicate groups
|
||||
// (only if they look like test/404 pages — very short titles or empty keys)
|
||||
// Skip this step to be safe — only deduplicate.
|
||||
|
||||
return NextResponse.json({
|
||||
total: allPages.length,
|
||||
duplicateGroups: [...byTitle.values()].filter((ps) => ps.length > 1).length,
|
||||
deleted,
|
||||
deletedIds,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "Unknown error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { login } from "@/lib/artalk";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { email, password } = body;
|
||||
|
||||
if (!email || !password) {
|
||||
return NextResponse.json(
|
||||
{ error: "请填写邮箱和密码" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { token, user } = await login(email, password);
|
||||
return NextResponse.json({ token, user });
|
||||
} catch (error: unknown) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "登录失败" },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { updatePage } from "@/lib/artalk";
|
||||
import { ARTALK_SITE } from "@/lib/config";
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
const { key, title, admin_only } = body;
|
||||
|
||||
if (!title) {
|
||||
return NextResponse.json({ error: "title is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const result = await updatePage(parseInt(id, 10), {
|
||||
key: key || "",
|
||||
title,
|
||||
admin_only: admin_only ?? false,
|
||||
});
|
||||
|
||||
return NextResponse.json(result);
|
||||
} catch (error: unknown) {
|
||||
const msg = error instanceof Error ? error.message : "Unknown error";
|
||||
return NextResponse.json({ error: msg }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getPages, getComments, createComment } from "@/lib/artalk";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = request.nextUrl;
|
||||
const pageKey = searchParams.get("page_key");
|
||||
const page = parseInt(searchParams.get("page") || "1", 10);
|
||||
const limit = parseInt(searchParams.get("limit") || "50", 10);
|
||||
const token = searchParams.get("token");
|
||||
|
||||
if (token) {
|
||||
const { setAuthToken } = await import("@/lib/artalk");
|
||||
setAuthToken(token);
|
||||
}
|
||||
|
||||
if (pageKey) {
|
||||
const offset = (page - 1) * limit;
|
||||
const result = await getComments(pageKey, { offset, limit });
|
||||
return NextResponse.json(result);
|
||||
} else {
|
||||
const offset = (page - 1) * limit;
|
||||
const result = await getPages({ offset, limit });
|
||||
return NextResponse.json(result);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "Unknown error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { token, ...data } = body;
|
||||
|
||||
if (token) {
|
||||
const { setAuthToken } = await import("@/lib/artalk");
|
||||
setAuthToken(token);
|
||||
}
|
||||
|
||||
const comment = await createComment(data);
|
||||
return NextResponse.json(comment);
|
||||
} catch (error: unknown) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "Unknown error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { ARTALK_SERVER, ARTALK_SITE } from "@/lib/config";
|
||||
|
||||
async function artalkFetch(path: string, token: string) {
|
||||
const res = await fetch(`${ARTALK_SERVER}${path}`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!res.ok) throw new Error(`Artalk error: ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
interface ArtalkPage {
|
||||
id: number;
|
||||
key: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface ArtalkComment {
|
||||
nick: string;
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const token = request.nextUrl.searchParams.get("token");
|
||||
if (!token) {
|
||||
return NextResponse.json({ error: "token required" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Step 1: Fetch all pages
|
||||
const allPages: ArtalkPage[] = [];
|
||||
let offset = 0;
|
||||
while (true) {
|
||||
const data = await artalkFetch(
|
||||
`/api/v2/pages?site_name=${encodeURIComponent(ARTALK_SITE)}&limit=200&offset=${offset}`,
|
||||
token
|
||||
);
|
||||
const pages = data.pages || data.data || [];
|
||||
if (pages.length === 0) break;
|
||||
allPages.push(...pages);
|
||||
offset += 200;
|
||||
if (pages.length < 200) break;
|
||||
}
|
||||
|
||||
// Step 2: Fetch comments per page with concurrency limit
|
||||
const pageCommentCounts: { title: string; count: number }[] = [];
|
||||
const userMap = new Map<string, { nick: string; count: number }>();
|
||||
|
||||
// Batch pages with concurrency of 10
|
||||
for (let i = 0; i < allPages.length; i += 10) {
|
||||
const batch = allPages.slice(i, i + 10);
|
||||
const results = await Promise.allSettled(
|
||||
batch.map((p) =>
|
||||
artalkFetch(
|
||||
`/api/v2/comments?site_name=${encodeURIComponent(ARTALK_SITE)}&page_key=${encodeURIComponent(p.key)}&limit=200&flat_mode=true`,
|
||||
token
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
for (let j = 0; j < batch.length; j++) {
|
||||
const page = batch[j];
|
||||
const result = results[j];
|
||||
if (result.status !== "fulfilled") continue;
|
||||
|
||||
const comments: ArtalkComment[] = result.value.comments || result.value.data || [];
|
||||
const count = comments.length;
|
||||
if (count > 0) {
|
||||
pageCommentCounts.push({ title: page.title || "(无标题)", count });
|
||||
}
|
||||
|
||||
for (const c of comments) {
|
||||
const key = (c.nick || "匿名").trim();
|
||||
const existing = userMap.get(key);
|
||||
if (existing) existing.count++;
|
||||
else userMap.set(key, { nick: c.nick || "匿名", count: 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort
|
||||
const topArticles = pageCommentCounts
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.slice(0, 20);
|
||||
|
||||
const topUsers = [...userMap.values()]
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.slice(0, 30);
|
||||
|
||||
const totalComments = pageCommentCounts.reduce((s, a) => s + a.count, 0);
|
||||
|
||||
return NextResponse.json({
|
||||
topArticles,
|
||||
topUsers,
|
||||
totalArticles: allPages.length,
|
||||
totalComments,
|
||||
uniqueUsers: userMap.size,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "Unknown error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { deployQueue } from "@/lib/git";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { title } = body;
|
||||
|
||||
if (!title) {
|
||||
return NextResponse.json({ error: "Title is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const result = await deployQueue.enqueue(`发布: ${title}`);
|
||||
|
||||
if (result.success) {
|
||||
return NextResponse.json({ success: true, output: result.output });
|
||||
}
|
||||
|
||||
if (result.conflict) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: result.error,
|
||||
detail: result.output,
|
||||
},
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: result.error }, { status: 500 });
|
||||
} catch (error: unknown) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "Deploy failed" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { hugo } from "@/lib/hugo";
|
||||
import { HUGO_PREVIEW } from "@/lib/config";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const result = await hugo.start();
|
||||
if (result.alreadyRunning || result.ok) {
|
||||
return NextResponse.json({ running: true, url: HUGO_PREVIEW });
|
||||
}
|
||||
return NextResponse.json({ running: false, url: HUGO_PREVIEW });
|
||||
} catch (error: unknown) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "Failed" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE() {
|
||||
try {
|
||||
hugo.stop();
|
||||
return NextResponse.json({ running: false });
|
||||
} catch (error: unknown) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "Failed" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import matter from "gray-matter";
|
||||
import { CONTENT_DIR } from "@/lib/config";
|
||||
|
||||
interface ImageFile {
|
||||
name: string;
|
||||
path: string;
|
||||
folder: string;
|
||||
size: number;
|
||||
url: string;
|
||||
slug: string;
|
||||
localUrl: string;
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = request.nextUrl;
|
||||
const serveFile = searchParams.get("file");
|
||||
|
||||
// Mode 2: Serve a specific image file (relative path from CONTENT_DIR)
|
||||
if (serveFile) {
|
||||
const fullPath = path.join(CONTENT_DIR, serveFile);
|
||||
if (!fullPath.startsWith(CONTENT_DIR)) {
|
||||
return NextResponse.json({ error: "Invalid path" }, { status: 403 });
|
||||
}
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
return NextResponse.json({ error: "File not found" }, { status: 404 });
|
||||
}
|
||||
const ext = path.extname(fullPath).toLowerCase();
|
||||
const mimeMap: Record<string, string> = {
|
||||
".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
|
||||
".gif": "image/gif", ".webp": "image/webp", ".svg": "image/svg+xml", ".bmp": "image/bmp",
|
||||
};
|
||||
const buffer = fs.readFileSync(fullPath);
|
||||
return new NextResponse(buffer, {
|
||||
headers: {
|
||||
"Content-Type": mimeMap[ext] || "application/octet-stream",
|
||||
"Cache-Control": "public, max-age=3600",
|
||||
"Content-Length": String(buffer.length),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Mode 1: List all images
|
||||
const images: ImageFile[] = [];
|
||||
const exts = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp"]);
|
||||
|
||||
function scan(dir: string, folder: string, slug: string) {
|
||||
if (!fs.existsSync(dir)) return;
|
||||
for (const entry of fs.readdirSync(dir)) {
|
||||
const full = path.join(dir, entry);
|
||||
if (entry.startsWith(".")) continue;
|
||||
const stat = fs.statSync(full);
|
||||
if (stat.isDirectory()) {
|
||||
// Try to read slug from index.md in this folder
|
||||
let childSlug = slug;
|
||||
const indexPath = path.join(full, "index.md");
|
||||
if (fs.existsSync(indexPath)) {
|
||||
try {
|
||||
const raw = fs.readFileSync(indexPath, "utf-8");
|
||||
const { data } = matter(raw);
|
||||
if (data.slug) childSlug = String(data.slug);
|
||||
} catch {}
|
||||
}
|
||||
scan(full, folder ? `${folder}/${entry}` : entry, childSlug);
|
||||
} else if (exts.has(path.extname(entry).toLowerCase())) {
|
||||
const rel = path.relative(CONTENT_DIR, full).replace(/\\/g, "/");
|
||||
const url = slug ? `/${slug}/${entry}` : `/${rel}`;
|
||||
const localUrl = `/api/images?file=${encodeURIComponent(rel)}`;
|
||||
images.push({ name: entry, path: rel, folder, size: stat.size, url, slug, localUrl });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scan(CONTENT_DIR, "", "");
|
||||
|
||||
images.sort((a, b) => b.size - a.size);
|
||||
|
||||
return NextResponse.json({ images });
|
||||
} catch {
|
||||
return NextResponse.json({ images: [] });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = request.nextUrl;
|
||||
const relPath = searchParams.get("path");
|
||||
if (!relPath) return NextResponse.json({ error: "path required" }, { status: 400 });
|
||||
|
||||
const fullPath = path.join(CONTENT_DIR, relPath);
|
||||
if (!fullPath.startsWith(CONTENT_DIR)) {
|
||||
return NextResponse.json({ error: "Invalid path" }, { status: 403 });
|
||||
}
|
||||
|
||||
if (fs.existsSync(fullPath)) {
|
||||
fs.unlinkSync(fullPath);
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
return NextResponse.json({ error: "File not found" }, { status: 404 });
|
||||
} catch (error: unknown) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "Unknown error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getPost, updatePost } from "@/lib/posts";
|
||||
import { moveToRecycle } from "@/lib/recycle";
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ slug: string }> }
|
||||
) {
|
||||
try {
|
||||
const { slug } = await params;
|
||||
const post = getPost(slug);
|
||||
if (!post) {
|
||||
return NextResponse.json({ error: "Post not found" }, { status: 404 });
|
||||
}
|
||||
return NextResponse.json(post);
|
||||
} catch (error: unknown) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "Unknown error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ slug: string }> }
|
||||
) {
|
||||
try {
|
||||
const { slug } = await params;
|
||||
const body = await request.json();
|
||||
const { frontMatter, content } = body;
|
||||
|
||||
const result = updatePost(slug, frontMatter || {}, content);
|
||||
if (!result.success) {
|
||||
return NextResponse.json({ error: "Post not found" }, { status: 404 });
|
||||
}
|
||||
return NextResponse.json({ success: true, dirPath: result.dirPath });
|
||||
} catch (error: unknown) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "Unknown error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ slug: string }> }
|
||||
) {
|
||||
try {
|
||||
const { slug } = await params;
|
||||
const post = getPost(slug);
|
||||
if (!post) {
|
||||
return NextResponse.json({ error: "Post not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
moveToRecycle(slug, post.dirPath, post.frontMatter.title);
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error: unknown) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "Unknown error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { updatePost, listPosts, getPost } from "@/lib/posts";
|
||||
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { slugs, action, categories, tags, draft } = body;
|
||||
|
||||
if (!slugs || !Array.isArray(slugs) || slugs.length === 0) {
|
||||
return NextResponse.json({ error: "slugs array required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const results: string[] = [];
|
||||
for (const slug of slugs) {
|
||||
const post = getPost(String(slug));
|
||||
if (!post) { results.push(`Not found: ${slug}`); continue; }
|
||||
|
||||
const updates: Record<string, unknown> = {};
|
||||
if (action === "categories" && categories !== undefined) {
|
||||
updates.categories = categories;
|
||||
} else if (action === "tags" && tags !== undefined) {
|
||||
updates.tags = tags;
|
||||
} else if (action === "draft" && draft !== undefined) {
|
||||
updates.draft = draft;
|
||||
} else if (action === "all") {
|
||||
if (categories !== undefined) updates.categories = categories;
|
||||
if (tags !== undefined) updates.tags = tags;
|
||||
if (draft !== undefined) updates.draft = draft;
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length === 0) {
|
||||
return NextResponse.json({ error: "No valid updates" }, { status: 400 });
|
||||
}
|
||||
|
||||
updatePost(String(slug), updates);
|
||||
results.push(`Updated: ${slug}`);
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, results });
|
||||
} catch (error: unknown) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "Unknown error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { listPosts, createPost, getPostCount } from "@/lib/posts";
|
||||
import { cleanExpired } from "@/lib/recycle";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
cleanExpired(); // housekeeping on each request
|
||||
|
||||
const { searchParams } = request.nextUrl;
|
||||
const search = searchParams.get("search") || "";
|
||||
const page = parseInt(searchParams.get("page") || "1", 10);
|
||||
const perPage = parseInt(searchParams.get("perPage") || "20", 10);
|
||||
const includeDrafts = searchParams.get("includeDrafts") !== "false";
|
||||
|
||||
const result = listPosts({ search, page, perPage, includeDrafts });
|
||||
const totalCount = getPostCount();
|
||||
|
||||
return NextResponse.json({ ...result, totalCount });
|
||||
} catch (error: unknown) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "Unknown error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { frontMatter, content } = body;
|
||||
|
||||
if (!frontMatter?.title || !frontMatter?.slug) {
|
||||
return NextResponse.json(
|
||||
{ error: "Title and slug are required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const dirPath = createPost(frontMatter, content || "");
|
||||
return NextResponse.json({ success: true, dirPath }, { status: 201 });
|
||||
} catch (error: unknown) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "Unknown error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getFeeds, addFeed, updateFeed, deleteFeed, checkLinkViaProxy } from "@/lib/rssapi";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = request.nextUrl;
|
||||
const health = searchParams.get("health");
|
||||
if (health) {
|
||||
const result = await checkLinkViaProxy(health);
|
||||
return NextResponse.json(result);
|
||||
}
|
||||
const feeds = await getFeeds();
|
||||
return NextResponse.json({ feeds });
|
||||
} catch (error: unknown) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "Unknown error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { url, feedTitle } = body;
|
||||
if (!url) {
|
||||
return NextResponse.json({ error: "URL is required" }, { status: 400 });
|
||||
}
|
||||
await addFeed(url, feedTitle || "");
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error: unknown) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "Unknown error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { oldUrl, url: newUrl, feedTitle } = body;
|
||||
if (!oldUrl) {
|
||||
return NextResponse.json({ error: "oldUrl is required" }, { status: 400 });
|
||||
}
|
||||
await updateFeed(oldUrl, newUrl || oldUrl, feedTitle || "");
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error: unknown) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "Unknown error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
const url = request.nextUrl.searchParams.get("url");
|
||||
if (!url) {
|
||||
return NextResponse.json({ error: "URL is required" }, { status: 400 });
|
||||
}
|
||||
await deleteFeed(url);
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error: unknown) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "Unknown error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getLinks, addLink, updateLink, deleteLink, checkLinkViaProxy } from "@/lib/rssapi";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = request.nextUrl;
|
||||
const all = searchParams.get("all") === "1";
|
||||
const health = searchParams.get("health");
|
||||
if (health) {
|
||||
const result = await checkLinkViaProxy(health);
|
||||
return NextResponse.json(result);
|
||||
}
|
||||
const links = await getLinks(all);
|
||||
return NextResponse.json({ links });
|
||||
} catch (error: unknown) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "Unknown error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { name, url, description, hidden } = body;
|
||||
if (!url) {
|
||||
return NextResponse.json({ error: "URL is required" }, { status: 400 });
|
||||
}
|
||||
await addLink({ name: name || "", url, description, hidden });
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error: unknown) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "Unknown error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { oldUrl, url: newUrl, name, description, hidden } = body;
|
||||
if (!oldUrl) {
|
||||
return NextResponse.json({ error: "oldUrl is required" }, { status: 400 });
|
||||
}
|
||||
const data: Record<string, unknown> = {};
|
||||
if (name !== undefined) data.name = name;
|
||||
if (description !== undefined) data.description = description;
|
||||
if (hidden !== undefined) data.hidden = hidden;
|
||||
if (newUrl !== undefined) data.url = newUrl;
|
||||
await updateLink(oldUrl, data);
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error: unknown) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "Unknown error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
const url = request.nextUrl.searchParams.get("url");
|
||||
if (!url) {
|
||||
return NextResponse.json({ error: "URL is required" }, { status: 400 });
|
||||
}
|
||||
await deleteLink(url);
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error: unknown) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "Unknown error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getWechatStatus, syncWechat } from "@/lib/rssapi";
|
||||
import { marked } from "marked";
|
||||
|
||||
function resolveUrl(src: string, slug: string): string {
|
||||
if (src.startsWith("http://") || src.startsWith("https://")) return src;
|
||||
if (src.startsWith("/")) return `https://usj.cc${src}`;
|
||||
if (slug) return `https://usj.cc/${slug}/${src}`;
|
||||
return `https://usj.cc/${src}`;
|
||||
}
|
||||
|
||||
function mdToWechatHtml(md: string, slug: string): { html: string; imageUrls: string[] } {
|
||||
const imageUrls: string[] = [];
|
||||
|
||||
// Step 1: Collect reference definitions [ref]: path
|
||||
const refMap = new Map<string, string>();
|
||||
for (const m of md.matchAll(/^\s*\[([^\]]+)\]:\s*(\S+)\s*$/gm)) {
|
||||
refMap.set(m[1].toLowerCase(), resolveUrl(m[2], slug));
|
||||
}
|
||||
|
||||
// Step 2: Convert reference-style images ![alt][ref] to inline 
|
||||
let processed = md.replace(
|
||||
/!\[([^\]]*)\]\[([^\]]+)\]/g,
|
||||
(_match: string, alt: string, ref: string) => {
|
||||
const src = refMap.get(ref.toLowerCase());
|
||||
if (src) {
|
||||
imageUrls.push(src);
|
||||
return ``;
|
||||
}
|
||||
return _match;
|
||||
}
|
||||
);
|
||||
|
||||
// Step 3: Convert inline images 
|
||||
processed = processed.replace(
|
||||
/!\[([^\]]*)\]\(([^)\s"]+)\)/g,
|
||||
(_match: string, alt: string, src: string) => {
|
||||
const absoluteUrl = resolveUrl(src, slug);
|
||||
if (!imageUrls.includes(absoluteUrl)) imageUrls.push(absoluteUrl);
|
||||
return ``;
|
||||
}
|
||||
);
|
||||
|
||||
// Step 4: Strip reference definition lines + clean blank lines
|
||||
processed = processed.replace(/^\s*\[[^\]]+\]:\s*\S+\s*$/gm, "");
|
||||
processed = processed.replace(/\n{3,}/g, "\n\n");
|
||||
|
||||
// Convert Markdown to HTML
|
||||
let html = marked.parse(processed, { async: false }) as string;
|
||||
|
||||
// WeChat inline style constants
|
||||
const S: Record<string, string> = {
|
||||
h2: "font-size:20px;font-weight:700;color:#1a1a1a;margin:28px 0 12px;padding-bottom:6px;border-bottom:2px solid #1d3d6c;line-height:1.4;",
|
||||
h3: "font-size:17px;font-weight:600;color:#2c3e50;margin:22px 0 8px;line-height:1.5;",
|
||||
p: "margin:0 0 12px;line-height:1.9;letter-spacing:0.2px;",
|
||||
strong: "font-weight:700;color:#1a1a1a;",
|
||||
em: "font-style:italic;color:#6f7f96;",
|
||||
code: "background:#f1f1f1;color:#d6336c;padding:2px 6px;border-radius:3px;font-size:13px;font-family:Menlo,Monaco,Consolas,monospace;letter-spacing:0;",
|
||||
pre: "background:#282a36;border:1px solid #44475a;border-radius:10px;padding:16px 18px;margin:16px 0;font-size:13px;line-height:1.7;overflow-x:auto;color:#f8f8f2;letter-spacing:0;",
|
||||
bq: "border-left:4px solid #1d3d6c;padding:10px 18px;margin:16px 0;background:rgba(29,61,108,0.04);color:#5a6a7a;border-radius:0 6px 6px 0;",
|
||||
img: "max-width:100%;height:auto;border-radius:4px;display:block;margin:16px auto;",
|
||||
hr: "border:none;border-top:1px solid #e8e8e8;margin:24px 0;",
|
||||
ul: "margin:8px 0 14px;padding-left:24px;",
|
||||
li: "margin:5px 0;line-height:1.9;",
|
||||
a: "color:#1d3d6c;text-decoration:none;border-bottom:1px solid rgba(29,61,108,0.2);word-break:break-all;",
|
||||
table: "width:100%;border-collapse:collapse;margin:16px 0;font-size:13px;",
|
||||
th: "border:1px solid #e0e0e0;padding:10px 14px;background:#f6f8fa;font-weight:600;",
|
||||
td: "border:1px solid #e0e0e0;padding:10px 14px;",
|
||||
};
|
||||
|
||||
html = html
|
||||
.replace(/<h2>/g, `<h2 style="${S.h2}">`)
|
||||
.replace(/<h3>/g, `<h3 style="${S.h3}">`)
|
||||
.replace(/<p>/g, `<p style="${S.p}">`)
|
||||
.replace(/<strong>/g, `<strong style="${S.strong}">`)
|
||||
.replace(/<em>/g, `<em style="${S.em}">`)
|
||||
.replace(/<code>/g, `<code style="${S.code}">`)
|
||||
.replace(/<pre><code>/g, `<pre style="${S.pre}"><code>`)
|
||||
.replace(/<pre>/g, `<pre style="${S.pre}">`)
|
||||
.replace(/<blockquote>/g, `<blockquote style="${S.bq}">`)
|
||||
.replace(/<img /g, `<img style="${S.img}" `)
|
||||
.replace(/<hr>/g, `<hr style="${S.hr}" />`)
|
||||
.replace(/<hr \/>/g, `<hr style="${S.hr}" />`)
|
||||
.replace(/<ul>/g, `<ul style="${S.ul}">`)
|
||||
.replace(/<ol>/g, `<ol style="${S.ul}">`)
|
||||
.replace(/<li>/g, `<li style="${S.li}">`)
|
||||
.replace(/<a /g, `<a style="${S.a}" `)
|
||||
.replace(/<table>/g, `<table style="${S.table}">`)
|
||||
.replace(/<th>/g, `<th style="${S.th}">`)
|
||||
.replace(/<td>/g, `<td style="${S.td}">`);
|
||||
|
||||
html = `<section style="max-width:100%;word-wrap:break-word;font-size:15px;color:#333;line-height:1.9;text-align:left;">${html}</section>
|
||||
<div style="display:block;margin-top:28px;padding-top:16px;border-top:1px solid #eee;text-align:center;font-size:12px;color:#bbb;">— END —</div>`;
|
||||
|
||||
return { html, imageUrls };
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const status = await getWechatStatus();
|
||||
return NextResponse.json(status);
|
||||
} catch (error: unknown) {
|
||||
const msg = error instanceof Error ? error.message : "Unknown error";
|
||||
return NextResponse.json({ error: msg }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { articles } = body;
|
||||
if (!articles || !Array.isArray(articles) || articles.length === 0) {
|
||||
return NextResponse.json({ error: "articles is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const processedArticles = articles.map((a: Record<string, unknown>) => {
|
||||
const slug = (a.slug as string) || "";
|
||||
const rawContent = (a.content as string) || "";
|
||||
const { html, imageUrls } = mdToWechatHtml(rawContent, slug);
|
||||
|
||||
const result: Record<string, unknown> = {
|
||||
title: a.title,
|
||||
author: a.author,
|
||||
content: html,
|
||||
content_source_url: a.content_source_url,
|
||||
thumb_media_url: a.thumb_media_url,
|
||||
show_cover_pic: a.show_cover_pic ?? 1,
|
||||
need_open_comment: a.need_open_comment ?? 0,
|
||||
};
|
||||
|
||||
if (imageUrls.length > 0) {
|
||||
result.content_image_urls = imageUrls.map((u) => ({ original_url: u }));
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
const result = await syncWechat(processedArticles);
|
||||
return NextResponse.json(result);
|
||||
} catch (error: unknown) {
|
||||
const msg = error instanceof Error ? error.message : "Unknown error";
|
||||
return NextResponse.json({ error: msg }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getPostCount } from "@/lib/posts";
|
||||
import { getLinks } from "@/lib/rssapi";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const postCount = getPostCount();
|
||||
|
||||
// Try getting RSS stats (non-blocking)
|
||||
let feedCount = 0;
|
||||
let linkCount = 0;
|
||||
try {
|
||||
const links = await getLinks(true);
|
||||
linkCount = links.length;
|
||||
} catch {}
|
||||
try {
|
||||
const { getFeeds } = await import("@/lib/rssapi");
|
||||
const feeds = await getFeeds();
|
||||
feedCount = feeds.length;
|
||||
} catch {}
|
||||
|
||||
return NextResponse.json({
|
||||
posts: postCount,
|
||||
feeds: feedCount,
|
||||
links: linkCount,
|
||||
});
|
||||
} catch {
|
||||
return NextResponse.json({ posts: 0, feeds: 0, links: 0 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const formData = await request.formData();
|
||||
const file = formData.get("file") as File | null;
|
||||
const dirPath = formData.get("dirPath") as string | null;
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json({ error: "No file provided" }, { status: 400 });
|
||||
}
|
||||
if (!file.type.startsWith("image/")) {
|
||||
return NextResponse.json({ error: "Only images allowed" }, { status: 400 });
|
||||
}
|
||||
|
||||
const bytes = await file.arrayBuffer();
|
||||
const buffer = Buffer.from(bytes);
|
||||
|
||||
// Determine upload directory
|
||||
let uploadDir: string;
|
||||
let isArticleFolder = false;
|
||||
if (dirPath && fs.existsSync(dirPath)) {
|
||||
uploadDir = dirPath;
|
||||
isArticleFolder = true;
|
||||
} else {
|
||||
// Fallback to static/image
|
||||
const year = new Date().getFullYear().toString();
|
||||
uploadDir = path.join(process.cwd(), "..", "static", "image", year);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(uploadDir)) {
|
||||
fs.mkdirSync(uploadDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Generate unique filename
|
||||
const ext = file.name.split(".").pop() || "png";
|
||||
const baseName = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
||||
const fileName = `${baseName}.${ext}`;
|
||||
const filePath = path.join(uploadDir, fileName);
|
||||
fs.writeFileSync(filePath, buffer);
|
||||
|
||||
// Article folder: bare filename. Static fallback: absolute from Hugo root
|
||||
const url = isArticleFolder ? fileName : `/image/${new Date().getFullYear()}/${fileName}`;
|
||||
return NextResponse.json({ url, fileName });
|
||||
} catch (error: unknown) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "Upload failed" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user