import type { NextRequest } from "next/server"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; type Prompt = { id: string; title: string; coverUrl: string; prompt: string; tags: string[]; category: string; githubUrl: string; preview: string; createdAt: string; updatedAt: string; }; type PromptCategory = { category: string; githubUrl: string; build: () => Promise[]>; }; const gptImage2RawBase = "https://raw.githubusercontent.com/EvoLinkAI/awesome-gpt-image-2-API-and-Prompts/main"; const awesomeGptImageRawBase = "https://raw.githubusercontent.com/ZeroLu/awesome-gpt-image/main"; const awesomeGpt4oImagePromptsBase = "https://raw.githubusercontent.com/ImgEdify/Awesome-GPT4o-Image-Prompts/main"; const youMindGptImage2RawBase = "https://raw.githubusercontent.com/YouMind-OpenLab/awesome-gpt-image-2/main"; const youMindNanoBananaProRawBase = "https://raw.githubusercontent.com/YouMind-OpenLab/awesome-nano-banana-pro-prompts/main"; const davidWuGptImage2RawBase = "https://raw.githubusercontent.com/davidwuw0811-boop/awesome-gpt-image2-prompts/main"; const gptImage2CaseFiles = ["README.md", "cases/ad-creative.md", "cases/character.md", "cases/comparison.md", "cases/ecommerce.md", "cases/portrait.md", "cases/poster.md", "cases/ui.md"]; const cacheTtlMs = 1000 * 60 * 60; const categories: PromptCategory[] = [ { category: "gpt-image-2-prompts", githubUrl: "https://github.com/EvoLinkAI/awesome-gpt-image-2-API-and-Prompts", build: buildGptImage2Prompts }, { category: "awesome-gpt-image", githubUrl: "https://github.com/ZeroLu/awesome-gpt-image", build: buildAwesomeGptImagePrompts }, { category: "awesome-gpt4o-image-prompts", githubUrl: "https://github.com/ImgEdify/Awesome-GPT4o-Image-Prompts", build: buildAwesomeGpt4oImagePrompts }, { category: "youmind-gpt-image-2", githubUrl: "https://github.com/YouMind-OpenLab/awesome-gpt-image-2", build: () => buildYouMindPrompts(youMindGptImage2RawBase, "youmind-gpt-image-2", "gpt-image-2") }, { category: "youmind-nano-banana-pro", githubUrl: "https://github.com/YouMind-OpenLab/awesome-nano-banana-pro-prompts", build: () => buildYouMindPrompts(youMindNanoBananaProRawBase, "youmind-nano-banana-pro", "nano-banana-pro") }, { category: "davidwu-gpt-image2-prompts", githubUrl: "https://github.com/davidwuw0811-boop/awesome-gpt-image2-prompts", build: buildDavidWuGptImage2Prompts }, ]; let memoryCache: { items: Prompt[]; fetchedAt: number } | null = null; let loadingPrompts: Promise | null = null; export async function GET(request: NextRequest) { const params = request.nextUrl.searchParams; const keyword = (params.get("keyword") || "").trim().toLowerCase(); const tags = params.getAll("tag").filter(Boolean); const category = params.get("category") || ""; const page = Math.max(1, Number(params.get("page")) || 1); const pageSize = Math.max(1, Math.min(100, Number(params.get("pageSize")) || 20)); const items = await getPrompts(); const withoutTagFilter = filterPrompts(items, { keyword, category, tags: [] }); const filtered = filterPrompts(items, { keyword, category, tags }); return Response.json({ items: filtered.slice((page - 1) * pageSize, page * pageSize), tags: collectTags(withoutTagFilter), categories: categories.map((item) => item.category), total: filtered.length, }); } async function getPrompts() { if (memoryCache && Date.now() - memoryCache.fetchedAt < cacheTtlMs) return memoryCache.items; if (loadingPrompts) return loadingPrompts; loadingPrompts = loadPrompts().finally(() => { loadingPrompts = null; }); return loadingPrompts; } async function loadPrompts() { const settled = await Promise.all( categories.map(async (category) => { try { const items = await category.build(); return items.map((item) => ({ ...item, category: category.category, githubUrl: category.githubUrl })); } catch { return []; } }), ); const items = settled.flat(); memoryCache = { items, fetchedAt: Date.now() }; return items; } function filterPrompts(items: Prompt[], options: { keyword: string; category: string; tags: string[] }) { return items.filter((item) => { if (isActiveOption(options.category) && item.category !== options.category) return false; if (options.tags.length && !options.tags.some((tag) => item.tags.includes(tag))) return false; if (!options.keyword) return true; return [item.title, item.prompt, item.category, ...item.tags].join(" ").toLowerCase().includes(options.keyword); }); } async function buildGptImage2Prompts() { const data = (await fetchJson<{ records?: Array<{ title?: string; tweet_url?: string; image_dir?: string; category?: string; added_at?: string }> }>(gptImage2RawBase, "data/ingested_tweets.json")).records || []; const cases = new Map(); const markdowns = await Promise.all(gptImage2CaseFiles.map((file) => fetchText(gptImage2RawBase, file))); markdowns.forEach((markdown) => collectGptImage2Cases(cases, markdown)); const items: Omit[] = []; data.forEach((item) => { const prompt = cases.get(item.tweet_url || ""); if (!item.title || !prompt || !item.image_dir) return; const image = `${gptImage2RawBase}/${item.image_dir}/output.jpg`; items.push({ id: `gpt-image-2-prompts-${leftPad(items.length + 1)}`, title: item.title, coverUrl: image, prompt, tags: tagsFromCategory(item.category || ""), preview: markdownPreview([image]), createdAt: item.added_at || "", updatedAt: item.added_at || "" }); }); return items; } function collectGptImage2Cases(cases: Map, markdown: string) { for (const match of markdown.matchAll(/### Case \d+: \[[^\]]+]\(([^)]+)\).*?\*\*Prompt:\*\*\s*\r?\n\s*```[\w-]*\r?\n(.*?)\r?\n```/gs)) { cases.set(match[1], match[2].trim()); } } async function buildAwesomeGptImagePrompts() { const markdown = await fetchText(awesomeGptImageRawBase, "README.zh-CN.md"); const items: Omit[] = []; for (const section of splitBeforeHeading(markdown, "## ")) { const tags = tagsFromHeading(firstMatch(section, /^##\s+(.+)$/m)); for (const block of splitBeforeHeading(section, "### ")) { const title = firstMatch(block, /^###\s+(.+)$/m).replace(/\[([^\]]+)]\([^)]+\)/g, "$1").trim(); const prompt = firstMatch(block, /\*\*提示词:\*\*\s*\r?\n\s*```[\w-]*\r?\n(.*?)\r?\n```/s).trim(); if (!title || !prompt) continue; const images = extractMarkdownImages(awesomeGptImageRawBase, block); items.push(defaultPrompt(`awesome-gpt-image-${leftPad(items.length + 1)}`, title, prompt, images[0] || "", tags, markdownPreview(images))); } } return items; } async function buildAwesomeGpt4oImagePrompts() { const markdown = await fetchText(awesomeGpt4oImagePromptsBase, "README.zh-CN.md"); const items: Omit[] = []; for (const block of splitBeforeHeading(markdown, "### ")) { const title = firstMatch(block, /^###\s+(.+)$/m).trim(); const prompt = firstMatch(block, /- \*\*提示词文本:\*\*\s*`(.*?)`/s).trim(); if (!title || !prompt) continue; const images = extractMarkdownImages(awesomeGpt4oImagePromptsBase, block); items.push(defaultPrompt(`awesome-gpt4o-image-prompts-${leftPad(items.length + 1)}`, title, prompt, images[0] || "", ["gpt4o"], markdownPreview(images))); } return items; } async function buildYouMindPrompts(baseUrl: string, idPrefix: string, modelTag: string) { const markdown = await fetchText(baseUrl, "README_zh.md"); const items: Omit[] = []; for (const block of splitBeforeHeading(markdown, "### ")) { const title = firstMatch(block, /^###\s+No\.\s*\d+:\s*(.+)$/m).trim(); const prompt = firstMatch(block, /#### .*?提示词\s*\r?\n\s*```[\w-]*\r?\n(.*?)\r?\n```/s).trim(); if (!title || !prompt) continue; const images = extractMarkdownImages(baseUrl, block); items.push(defaultPrompt(`${idPrefix}-${leftPad(items.length + 1)}`, title, prompt, images[0] || "", youMindTags(title, modelTag), markdownPreview(images))); } return items; } async function buildDavidWuGptImage2Prompts() { const data = await fetchJson>(davidWuGptImage2RawBase, "prompts.json"); return data .map((item, index) => { const title = (item.title_cn || item.title_en || "").trim(); const prompt = (item.prompt || "").trim(); if (!title || !prompt) return null; const image = absoluteImage(davidWuGptImage2RawBase, item.image || ""); const preview = [item.title_en, item.note, image ? `![](${image})` : ""].filter(Boolean).join("\n\n"); return defaultPrompt(`davidwu-gpt-image2-prompts-${leftPad(item.id || index + 1)}`, title, prompt, image, davidWuTags(item), preview); }) .filter((item): item is Omit => Boolean(item)); } function defaultPrompt(id: string, title: string, prompt: string, coverUrl: string, tags: string[], preview: string): Omit { return { id, title, coverUrl, prompt, tags, preview, createdAt: "", updatedAt: "" }; } async function fetchText(baseUrl: string, file: string) { const response = await fetch(`${baseUrl}/${file}`, { cache: "no-store" }); if (!response.ok) throw new Error(`${file} 拉取失败`); return response.text(); } async function fetchJson(baseUrl: string, file: string) { return JSON.parse(await fetchText(baseUrl, file)) as T; } function splitBeforeHeading(markdown: string, prefix: string) { const blocks: string[] = []; let current: string[] = []; for (const line of markdown.split("\n")) { if (line.startsWith(prefix) && current.length) { blocks.push(current.join("\n")); current = []; } current.push(line); } blocks.push(current.join("\n")); return blocks; } function firstMatch(value: string, pattern: RegExp) { return pattern.exec(value)?.[1] || ""; } function extractMarkdownImages(baseUrl: string, markdown: string) { return Array.from(markdown.matchAll(/!\[[^\]]*]\(([^)]+)\)/g), (match) => absoluteImage(baseUrl, match[1])).filter(Boolean); } function absoluteImage(baseUrl: string, image: string) { if (!image) return ""; if (/^https?:\/\//i.test(image)) return image; return `${baseUrl}/${image.replace(/^\.?\//, "")}`; } function tagsFromCategory(category: string) { return splitTags(category.replace(/\s+Cases$/i, ""), /\s*(?:&|and)\s*/); } function tagsFromHeading(heading: string) { return splitTags(heading.replace(/[^\p{L}\p{N}/&、与 ]/gu, ""), /\s*(?:\/|&|、|与)\s*/); } function youMindTags(title: string, modelTag: string) { const [, prefix] = title.match(/^(.+?) - /) || []; return [modelTag, ...tagsFromHeading(prefix || "")]; } function davidWuTags(item: { category_cn?: string; category?: string; author?: string; source?: string; needs_ref?: boolean }) { const tags = splitTags([item.category_cn, item.category, item.author, item.source].filter(Boolean).join("/"), /\//); if (item.needs_ref) tags.push("需要参考图"); return tags; } function splitTags(value: string, pattern: RegExp) { return value .split(pattern) .map((tag) => tag.trim().toLowerCase()) .filter(Boolean); } function markdownPreview(images: string[]) { return images.filter(Boolean).map((image) => `![](${image})`).join("\n\n"); } function collectTags(items: Prompt[]) { return Array.from(new Set(items.flatMap((item) => item.tags).filter(Boolean))); } function leftPad(value: number) { return String(value).padStart(4, "0"); } function isActiveOption(value: string) { return value && value !== "全部" && value !== "all"; }