首次发布git

This commit is contained in:
2026-02-24 09:53:19 +08:00
commit dd97cf6a2c
71 changed files with 14875 additions and 0 deletions
+50
View File
@@ -0,0 +1,50 @@
import { NextRequest, NextResponse } from "next/server";
export const runtime = "edge"; // Optional: Use edge runtime for lower latency
export async function POST(req: NextRequest) {
try {
const { messages, config } = await req.json();
const { apiKey, baseURL, model } = config || {};
if (!apiKey) {
return NextResponse.json({ error: "Missing API Key" }, { status: 401 });
}
// Clean up baseURL: ensure no trailing slash, add /chat/completions if missing?
// Actually, usually users provide standard base URL "https://api.openai.com/v1"
// We should append /chat/completions.
const url = `${baseURL.replace(/\/$/, "")}/chat/completions`;
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: model || "gpt-3.5-turbo",
messages,
stream: true, // Force streaming
}),
});
if (!res.ok) {
const errorText = await res.text();
return NextResponse.json({ error: `Upstream Error: ${res.statusText}`, details: errorText }, { status: res.status });
}
// Return the stream directly
return new Response(res.body, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
},
});
} catch (error) {
console.error("AI API Error:", error);
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
}
}
+31
View File
@@ -0,0 +1,31 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { verifyPassword } from "@/lib/auth";
export async function POST(req: Request) {
try {
const { password } = await req.json();
// Ensure settings exist, if not, maybe we should init or fail?
// Ideally init should happen on app startup or manual trigger, but for simplicity:
// If no settings exist, check against "admin" (fallback) but DO NOT create DB entry implicitely here for security,
// unless we strictly define that "admin" is the default.
// Let's assume DB must be populated.
const settings = await prisma.globalSettings.findUnique({
where: { id: "default" },
});
const isValid = settings
? await verifyPassword(password, settings.password)
: password === "admin"; // Fallback only if DB empty
if (isValid) {
return NextResponse.json({ success: true });
} else {
return NextResponse.json({ error: "Invalid password" }, { status: 401 });
}
} catch (error) {
return NextResponse.json({ error: "Login failed" }, { status: 500 });
}
}
+9
View File
@@ -0,0 +1,9 @@
import { NextResponse } from "next/server";
import { cookies } from "next/headers";
export async function POST() {
const cookieStore = await cookies();
cookieStore.delete("auth");
return NextResponse.json({ success: true });
}
+59
View File
@@ -0,0 +1,59 @@
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const id = (await params).id;
try {
const page = await prisma.page.findUnique({
where: { id },
});
if (!page) return NextResponse.json({ error: 'Page not found' }, { status: 404 });
return NextResponse.json({ ...page, tags: JSON.parse(page.tags || "[]") });
} catch (error) {
return NextResponse.json({ error: 'Error fetching page' }, { status: 500 });
}
}
export async function PUT(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const id = (await params).id;
try {
const body = await request.json();
// Separate update logic for flexibility (e.g. only updating title)
const updateData: any = {};
if (body.title !== undefined) updateData.title = body.title;
if (body.content !== undefined) updateData.content = body.content;
if (body.parentId !== undefined) updateData.parentId = body.parentId;
if (body.tags !== undefined) updateData.tags = JSON.stringify(body.tags);
if (body.icon !== undefined) updateData.icon = body.icon;
if (body.isLocked !== undefined) updateData.isLocked = body.isLocked;
const page = await prisma.page.update({
where: { id },
data: updateData,
});
return NextResponse.json({ ...page, tags: JSON.parse(page.tags || "[]") });
} catch (error) {
return NextResponse.json({ error: 'Error updating page' }, { status: 500 });
}
}
export async function DELETE(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const id = (await params).id;
try {
await prisma.page.delete({
where: { id },
});
return NextResponse.json({ success: true });
} catch (error) {
return NextResponse.json({ error: 'Error deleting page' }, { status: 500 });
}
}
+27
View File
@@ -0,0 +1,27 @@
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
export async function PUT(request: Request) {
try {
const body = await request.json();
const { updates } = body;
if (!Array.isArray(updates)) {
return NextResponse.json({ error: 'Invalid updates' }, { status: 400 });
}
// Transaction for batch update
await prisma.$transaction(
updates.map((update: { id: string, order: number }) =>
prisma.page.update({
where: { id: update.id },
data: { order: update.order },
})
)
);
return NextResponse.json({ success: true });
} catch (error) {
return NextResponse.json({ error: 'Error reordering pages' }, { status: 500 });
}
}
+47
View File
@@ -0,0 +1,47 @@
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
export async function GET() {
try {
const pages = await prisma.page.findMany({
orderBy: [{ order: 'asc' }, { createdAt: 'desc' }],
});
const parsedPages = pages.map(p => ({
...p,
tags: JSON.parse(p.tags || "[]")
}));
return NextResponse.json(parsedPages);
} catch (error) {
return NextResponse.json({ error: 'Error fetching pages' }, { status: 500 });
}
}
export async function POST(request: Request) {
try {
const body = await request.json();
const { title, content, parentId, type } = body;
const page = await prisma.page.create({
data: {
title: title || '无标题',
content: content || '',
tags: JSON.stringify(body.tags || []),
parentId: parentId || null,
type: type || 'file',
order: await (async () => {
if (body.order !== undefined) return body.order;
const lastPage = await prisma.page.findFirst({
where: { parentId: parentId || null },
orderBy: { order: 'desc' },
});
return (lastPage?.order ?? -1) + 1;
})(),
icon: body.icon || null,
isLocked: body.isLocked || false,
},
});
return NextResponse.json({ ...page, tags: JSON.parse(page.tags || "[]") });
} catch (error) {
return NextResponse.json({ error: 'Error creating page' }, { status: 500 });
}
}
+26
View File
@@ -0,0 +1,26 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { hashPassword } from "@/lib/auth";
export async function POST() {
try {
const count = await prisma.globalSettings.count();
if (count > 0) {
return NextResponse.json({ message: "Settings already initialized" }, { status: 200 });
}
// Default password: "admin"
const hashedPassword = await hashPassword("admin");
await prisma.globalSettings.create({
data: {
id: "default",
password: hashedPassword,
},
});
return NextResponse.json({ message: "Initialized default settings" }, { status: 201 });
} catch (error) {
console.error("Init Settings Error:", error);
return NextResponse.json({ error: "Failed to initialize settings" }, { status: 500 });
}
}
+34
View File
@@ -0,0 +1,34 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { hashPassword, verifyPassword } from "@/lib/auth";
export async function PUT(req: Request) {
try {
const { currentPassword, newPassword } = await req.json();
// Cast to any to bypass build error until server restart allows prisma generate to run
const settings = await (prisma as any).globalSettings.findUnique({
where: { id: "default" },
});
if (!settings) {
return NextResponse.json({ error: "Settings not found" }, { status: 404 });
}
const isValid = await verifyPassword(currentPassword, settings.password);
if (!isValid) {
return NextResponse.json({ error: "Current password incorrect" }, { status: 401 });
}
const hashedPassword = await hashPassword(newPassword);
await (prisma as any).globalSettings.update({
where: { id: "default" },
data: { password: hashedPassword },
});
return NextResponse.json({ success: true });
} catch (error) {
return NextResponse.json({ error: "Failed to change password" }, { status: 500 });
}
}
+194
View File
@@ -0,0 +1,194 @@
import { NextRequest, NextResponse } from "next/server";
import { PrismaClient } from "@prisma/client";
import JSZip from "jszip";
import { marked } from "marked";
// Use a global prisma instance to avoid "too many connections" in dev
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
const prisma = globalForPrisma.prisma || new PrismaClient();
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
// Disable body parser strictly (Next.js App Router handles FormData naturally)
// export const config = {
// api: {
// bodyParser: false,
// },
// };
// No need for config in App Router route handlers.
export async function POST(req: NextRequest) {
try {
const formData = await req.formData();
const file = formData.get("file") as File;
if (!file) {
return NextResponse.json({ error: "No file uploaded" }, { status: 400 });
}
const buffer = await file.arrayBuffer();
const zip = await JSZip.loadAsync(buffer);
// Map to store directory paths to their new DB IDs
// Data format: "folder/subfolder" -> UUID
const pathIdMap = new Map<string, string>();
// Prepare data for proper insertion order (Folders first, then files?)
// Actually, we need to process by path depth to ensure parents exist.
const entries: Array<{ path: string; isDir: boolean; content?: string }> = [];
// 1. Read all entries
const filePromises: Promise<void>[] = [];
zip.forEach((relativePath, zipEntry) => {
if (relativePath.startsWith("__MACOSX") || relativePath.includes(".DS_Store")) {
return; // Skip system files
}
const promise = (async () => {
if (zipEntry.dir) {
// Remove trailing slash for consistency
const cleanPath = relativePath.endsWith('/') ? relativePath.slice(0, -1) : relativePath;
if (cleanPath) {
entries.push({ path: cleanPath, isDir: true });
}
} else {
if (relativePath.endsWith(".md")) {
const content = await zipEntry.async("string");
entries.push({ path: relativePath, isDir: false, content });
}
}
})();
filePromises.push(promise);
});
await Promise.all(filePromises);
// 2. Clear existing Pages (Transaction usually)
// Since sqlite doesn't support nested transactions well in Prisma sometimes,
// we'll just do it sequentially but quickly.
// Ideally: await prisma.$transaction([prisma.page.deleteMany(), ...])
// But logic is complex (recursive id generation), so we delete first.
// WARNING: This is destructive.
await prisma.page.deleteMany();
// 3. Sort entries by path depth (number of slashes)
entries.sort((a, b) => {
const depthA = a.path.split('/').length;
const depthB = b.path.split('/').length;
return depthA - depthB;
});
// Helper to get or create parent folder
const ensureParent = async (entryPath: string): Promise<string | null> => {
const parts = entryPath.split('/');
if (parts.length <= 1) return null; // Root level
const parentPath = parts.slice(0, -1).join('/');
// If parent already processed
if (pathIdMap.has(parentPath)) {
return pathIdMap.get(parentPath)!;
}
// If parent folder was not explicitly in Zip (implicit folder), create it
// Recursively ensure its parent exists
const grandParentId = await ensureParent(parentPath);
const folderName = parts[parts.length - 2];
const newFolder = await prisma.page.create({
data: {
title: folderName,
type: 'folder',
parentId: grandParentId
}
});
pathIdMap.set(parentPath, newFolder.id);
return newFolder.id;
};
// 4. Process entries
for (const entry of entries) {
// Determine parent
// If it's a file "A/B.md", parent path is "A".
// If it's a folder "A/B", parent path is "A".
// Since we sorted by depth, "A" should be processed before "A/B".
// However, implicit folders might be skipped in sorting if they aren't in `entries`.
// So `ensureParent` handles implicit creation.
const parentId = await ensureParent(entry.path);
if (entry.isDir) {
// Check if already created by ensureParent
if (!pathIdMap.has(entry.path)) {
const name = entry.path.split('/').pop() || "Untitled Folder";
const folder = await prisma.page.create({
data: {
title: name,
type: 'folder',
parentId: parentId
}
});
pathIdMap.set(entry.path, folder.id);
}
} else {
// It is a File (.md)
const filename = entry.path.split('/').pop()?.replace('.md', '') || "Untitled";
// Parse Frontmatter
let title = filename;
let tags: string[] = [];
let order = 0;
let markdownBody = entry.content || "";
// Regex for frontmatter
const fmMatch = markdownBody.match(/^---\n([\s\S]*?)\n---\n/);
if (fmMatch) {
const fmString = fmMatch[1];
markdownBody = markdownBody.slice(fmMatch[0].length);
// Simple parsing
// title: "Foo"
// tags: ["a", "b"]
// order: 1
const titleMatch = fmString.match(/title:\s*"(.*)"/);
if (titleMatch) title = titleMatch[1];
const tagsMatch = fmString.match(/tags:\s*\[(.*)\]/);
if (tagsMatch) {
// "a", "b" -> split
tags = tagsMatch[1].split(',').map(s => s.trim().replace(/^"|"$/g, '')).filter(Boolean);
}
const orderMatch = fmString.match(/order:\s*(\d+)/);
if (orderMatch) order = parseInt(orderMatch[1]);
}
// Convert Markdown to HTML for storage (Editor uses HTML)
// Ensure GFM is enabled (default true in new versions, but explicit is good)
// breaks: true converts \n to <br> (GitHub style)
const htmlContent = await marked(markdownBody, { gfm: true, breaks: true });
await prisma.page.create({
data: {
title: title,
type: 'file',
content: htmlContent,
tags: JSON.stringify(tags),
order: order,
parentId: parentId
}
});
}
}
return NextResponse.json({ success: true, count: entries.length });
} catch (e) {
console.error("Restore failed:", e);
return NextResponse.json({ error: "Restore failed: " + String(e) }, { status: 500 });
}
}