修复若干问题

This commit is contained in:
zqlit committed 2026-05-21 13:35:18 +08:00
1 parent 013dce4f9f
commit d9b7026d21
12 files changed
+581 -40

No files matched your search

+104
View File
@@ -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,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 });
}
}
+104
View File
@@ -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 }
);
}
}
+38 -8
View File
@@ -11,10 +11,39 @@ interface ImageFile {
size: number;
url: string;
slug: string;
localUrl: string;
}
export async function GET() {
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"]);
@@ -38,9 +67,9 @@ export async function GET() {
scan(full, folder ? `${folder}/${entry}` : entry, childSlug);
} else if (exts.has(path.extname(entry).toLowerCase())) {
const rel = path.relative(CONTENT_DIR, full).replace(/\\/g, "/");
// Hugo serves page resources at /<slug>/<filename>
const url = slug ? `/${slug}/${entry}` : `/${rel}`;
images.push({ name: entry, path: full, folder, size: stat.size, url, slug });
const localUrl = `/api/images?file=${encodeURIComponent(rel)}`;
images.push({ name: entry, path: rel, folder, size: stat.size, url, slug, localUrl });
}
}
}
@@ -58,15 +87,16 @@ export async function GET() {
export async function DELETE(request: NextRequest) {
try {
const { searchParams } = request.nextUrl;
const filePath = searchParams.get("path");
if (!filePath) return NextResponse.json({ error: "path required" }, { status: 400 });
const relPath = searchParams.get("path");
if (!relPath) return NextResponse.json({ error: "path required" }, { status: 400 });
if (!filePath.startsWith(CONTENT_DIR)) {
const fullPath = path.join(CONTENT_DIR, relPath);
if (!fullPath.startsWith(CONTENT_DIR)) {
return NextResponse.json({ error: "Invalid path" }, { status: 403 });
}
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
if (fs.existsSync(fullPath)) {
fs.unlinkSync(fullPath);
return NextResponse.json({ success: true });
}
return NextResponse.json({ error: "File not found" }, { status: 404 });
+2 -4
View File
@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { getFeeds, addFeed, deleteFeed, checkLinkViaProxy } from "@/lib/rssapi";
import { getFeeds, addFeed, updateFeed, deleteFeed, checkLinkViaProxy } from "@/lib/rssapi";
export async function GET(request: NextRequest) {
try {
@@ -43,9 +43,7 @@ export async function PUT(request: NextRequest) {
if (!oldUrl) {
return NextResponse.json({ error: "oldUrl is required" }, { status: 400 });
}
// Delete old, add new (RSS API has no update endpoint for feeds)
await deleteFeed(oldUrl);
await addFeed(newUrl || oldUrl, feedTitle || "");
await updateFeed(oldUrl, newUrl || oldUrl, feedTitle || "");
return NextResponse.json({ success: true });
} catch (error: unknown) {
return NextResponse.json(
+6 -11
View File
@@ -44,17 +44,12 @@ export async function PUT(request: NextRequest) {
if (!oldUrl) {
return NextResponse.json({ error: "oldUrl is required" }, { status: 400 });
}
// If URL changed, delete old and create new
if (newUrl && newUrl !== oldUrl) {
await deleteLink(oldUrl);
await addLink({ name: name || "", url: newUrl, description, hidden });
} else {
const data: Record<string, unknown> = {};
if (name !== undefined) data.name = name;
if (description !== undefined) data.description = description;
if (hidden !== undefined) data.hidden = hidden;
await updateLink(oldUrl, data);
}
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(