feat(prompt-sources): add custom script fetching functionality for prompt sources

This commit is contained in:
HouYunFei
2026-07-17 17:36:42 +08:00
parent 0e1edd1484
commit d4130bbb79
14 changed files with 851 additions and 229 deletions
@@ -0,0 +1,118 @@
import { nanoid } from "nanoid";
export type PromptSource = {
id: string;
name: string;
githubUrl: string;
enabled: boolean;
script: string;
};
export function createPromptSource(source?: Partial<PromptSource>): PromptSource {
return {
id: source?.id?.trim() || nanoid(),
name: source?.name?.trim() || "新来源",
githubUrl: source?.githubUrl?.trim() || "",
enabled: source?.enabled ?? true,
script: source?.script ?? "",
};
}
const awesomeGptImageScript = `// awesome-gpt-image:从 README.zh-CN.md 解析,## 为标签分组,### 为单条提示词。
const base = "https://raw.githubusercontent.com/ZeroLu/awesome-gpt-image/main";
const markdown = await fetchText(\`\${base}/README.zh-CN.md\`);
const items = [];
for (const section of splitSections(markdown, "## ")) {
const tags = tagsFromHeading(firstMatch(section, /^##\\s+(.+)$/m));
for (const block of splitSections(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 = extractImages(base, block);
items.push(makePrompt({ id: \`awesome-gpt-image-\${leftPad(items.length + 1)}\`, title, prompt, coverUrl: images[0] || "", tags, preview: markdownPreview(images) }));
}
}
return items;`;
const awesomeGpt4oImageScript = `// Awesome-GPT4o-Image-PromptsREADME.zh-CN.md 里每个 ### 段落一条提示词。
const base = "https://raw.githubusercontent.com/ImgEdify/Awesome-GPT4o-Image-Prompts/main";
const markdown = await fetchText(\`\${base}/README.zh-CN.md\`);
const items = [];
for (const block of splitSections(markdown, "### ")) {
const title = firstMatch(block, /^###\\s+(.+)$/m).trim();
const prompt = firstMatch(block, /- \\*\\*提示词文本:\\*\\*\\s*\\\`(.*?)\\\`/s).trim();
if (!title || !prompt) continue;
const images = extractImages(base, block);
items.push(makePrompt({ id: \`awesome-gpt4o-image-prompts-\${leftPad(items.length + 1)}\`, title, prompt, coverUrl: images[0] || "", tags: ["gpt4o"], preview: markdownPreview(images) }));
}
return items;`;
function youMindScript(base: string, idPrefix: string, modelTag: string) {
return `// YouMind 系列:README_zh.md 里 "### No.N: 标题" + "#### ...提示词" 代码块。
const base = "${base}";
const idPrefix = "${idPrefix}";
const modelTag = "${modelTag}";
const markdown = await fetchText(\`\${base}/README_zh.md\`);
const items = [];
for (const block of splitSections(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 = extractImages(base, block);
const [, prefix] = title.match(/^(.+?) - /) || [];
const tags = [modelTag, ...tagsFromHeading(prefix || "")];
items.push(makePrompt({ id: \`\${idPrefix}-\${leftPad(items.length + 1)}\`, title, prompt, coverUrl: images[0] || "", tags, preview: markdownPreview(images) }));
}
return items;`;
}
const davidWuGptImage2Script = `// davidwuprompts.json 结构化数据,逐条转换。
const base = "https://raw.githubusercontent.com/davidwuw0811-boop/awesome-gpt-image2-prompts/main";
const data = await fetchJson(\`\${base}/prompts.json\`);
const items = [];
data.forEach((item, index) => {
const title = (item.title_cn || item.title_en || "").trim();
const prompt = (item.prompt || "").trim();
if (!title || !prompt) return;
const image = absoluteUrl(base, item.image || "");
const tags = splitTags([item.category_cn, item.category, item.author, item.source].filter(Boolean).join("/"), /\\//);
if (item.needs_ref) tags.push("需要参考图");
const preview = [item.title_en, item.note, image ? \`![](\${image})\` : ""].filter(Boolean).join("\\n\\n");
items.push(makePrompt({ id: \`davidwu-gpt-image2-prompts-\${leftPad(item.id || index + 1)}\`, title, prompt, coverUrl: image, tags, preview }));
});
return items;`;
export const DEFAULT_PROMPT_SOURCES: PromptSource[] = [
{ id: "awesome-gpt-image", name: "awesome-gpt-image", githubUrl: "https://github.com/ZeroLu/awesome-gpt-image", enabled: true, script: awesomeGptImageScript },
{ id: "awesome-gpt4o-image-prompts", name: "awesome-gpt4o-image-prompts", githubUrl: "https://github.com/ImgEdify/Awesome-GPT4o-Image-Prompts", enabled: true, script: awesomeGpt4oImageScript },
{
id: "youmind-gpt-image-2",
name: "youmind-gpt-image-2",
githubUrl: "https://github.com/YouMind-OpenLab/awesome-gpt-image-2",
enabled: true,
script: youMindScript("https://raw.githubusercontent.com/YouMind-OpenLab/awesome-gpt-image-2/main", "youmind-gpt-image-2", "gpt-image-2"),
},
{
id: "youmind-nano-banana-pro",
name: "youmind-nano-banana-pro",
githubUrl: "https://github.com/YouMind-OpenLab/awesome-nano-banana-pro-prompts",
enabled: true,
script: youMindScript("https://raw.githubusercontent.com/YouMind-OpenLab/awesome-nano-banana-pro-prompts/main", "youmind-nano-banana-pro", "nano-banana-pro"),
},
{ id: "davidwu-gpt-image2-prompts", name: "davidwu-gpt-image2-prompts", githubUrl: "https://github.com/davidwuw0811-boop/awesome-gpt-image2-prompts", enabled: true, script: davidWuGptImage2Script },
];
/** Starter script inserted when a user creates a blank source. */
export const PROMPT_SOURCE_TEMPLATE = `// 拉取远程列表并 return 一个提示词数组;每条至少含 title 和 prompt。
// 可用辅助见右侧「可用变量」,例如 fetchText / splitSections / makePrompt。
const base = "https://raw.githubusercontent.com/owner/repo/main";
const markdown = await fetchText(\`\${base}/README.md\`);
const items = [];
for (const block of splitSections(markdown, "### ")) {
const title = firstMatch(block, /^###\\s+(.+)$/m).trim();
const prompt = firstMatch(block, /\\\`\\\`\\\`[\\w-]*\\r?\\n(.*?)\\r?\\n\\\`\\\`\\\`/s).trim();
if (!title || !prompt) continue;
const images = extractImages(base, block);
items.push(makePrompt({ id: \`my-source-\${leftPad(items.length + 1)}\`, title, prompt, coverUrl: images[0] || "", tags: [], preview: markdownPreview(images) }));
}
return items;`;
@@ -0,0 +1,165 @@
/**
* Runtime for user-authored prompt-source scripts. A script is an async function body that fetches
* a remote list (markdown / json) and `return`s an array of prompt items. It runs with a set of flat
* helper locals (see PROMPT_SOURCE_VARIABLES) so scripts stay short and declarative.
*/
export type RawPrompt = {
id: string;
title: string;
coverUrl: string;
prompt: string;
tags: string[];
preview: string;
createdAt: string;
updatedAt: string;
};
type RunOptions = { signal?: AbortSignal };
async function fetchText(url: string) {
const response = await fetch(url, { cache: "no-store" });
if (!response.ok) throw new Error(`${url} 拉取失败`);
return response.text();
}
async function fetchJson<T = unknown>(url: string) {
return JSON.parse(await fetchText(url)) as T;
}
/** Split markdown into blocks, each starting at a line that begins with `prefix` (e.g. "## " / "### "). */
function splitSections(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 absoluteUrl(baseUrl: string, path: string) {
if (!path) return "";
if (/^https?:\/\//i.test(path)) return path;
return `${baseUrl}/${path.replace(/^\.?\//, "")}`;
}
function extractImages(baseUrl: string, markdown: string) {
return Array.from(markdown.matchAll(/!\[[^\]]*]\(([^)]+)\)/g), (match) => absoluteUrl(baseUrl, match[1])).filter(Boolean);
}
function splitTags(value: string, pattern: RegExp) {
return value
.split(pattern)
.map((tag) => tag.trim().toLowerCase())
.filter(Boolean);
}
function tagsFromHeading(heading: string) {
return splitTags(heading.replace(/[^\p{L}\p{N}/& ]/gu, ""), /\s*(?:\/|&||)\s*/);
}
function markdownPreview(images: string[]) {
return images
.filter(Boolean)
.map((image) => `![](${image})`)
.join("\n\n");
}
function leftPad(value: number) {
return String(value).padStart(4, "0");
}
function makePrompt(input: { id: string; title: string; prompt: string; coverUrl?: string; tags?: string[]; preview?: string; createdAt?: string; updatedAt?: string }): RawPrompt {
return {
id: input.id,
title: input.title,
prompt: input.prompt,
coverUrl: input.coverUrl || "",
tags: input.tags || [],
preview: input.preview || "",
createdAt: input.createdAt || "",
updatedAt: input.updatedAt || "",
};
}
/** Run a prompt-source script and normalize its result into a deduped RawPrompt[]. */
export async function runPromptSource(script: string, options?: RunOptions): Promise<RawPrompt[]> {
const body = script.trim();
if (!body) throw new Error("提示词来源脚本为空");
const runner = new Function(
"fetchText",
"fetchJson",
"splitSections",
"firstMatch",
"extractImages",
"absoluteUrl",
"tagsFromHeading",
"splitTags",
"markdownPreview",
"leftPad",
"makePrompt",
"signal",
`"use strict"; return (async () => {\n${body}\n})();`,
) as (...args: unknown[]) => Promise<unknown>;
let result: unknown;
try {
result = await runner(fetchText, fetchJson, splitSections, firstMatch, extractImages, absoluteUrl, tagsFromHeading, splitTags, markdownPreview, leftPad, makePrompt, options?.signal);
} catch (error) {
if (error instanceof DOMException && error.name === "AbortError") throw error;
const message = error instanceof Error ? error.message : String(error);
throw new Error(`提示词来源脚本执行失败:${message}`);
}
if (!Array.isArray(result)) throw new Error("提示词来源脚本需要 return 一个数组");
const seen = new Set<string>();
const items: RawPrompt[] = [];
for (const raw of result) {
if (!raw || typeof raw !== "object") continue;
const record = raw as Record<string, unknown>;
const title = String(record.title || "").trim();
const prompt = String(record.prompt || "").trim();
if (!title || !prompt) continue;
const id = String(record.id || "").trim() || `prompt-${leftPad(items.length + 1)}`;
if (seen.has(id)) continue;
seen.add(id);
items.push(
makePrompt({
id,
title,
prompt,
coverUrl: String(record.coverUrl || ""),
tags: Array.isArray(record.tags) ? record.tags.map((tag) => String(tag)).filter(Boolean) : [],
preview: String(record.preview || ""),
createdAt: String(record.createdAt || ""),
updatedAt: String(record.updatedAt || ""),
}),
);
}
return items;
}
export type PromptSourceVariable = { name: string; type: string; desc: string };
/** Documentation surface shown in the prompt-source script editor. */
export const PROMPT_SOURCE_VARIABLES: PromptSourceVariable[] = [
{ name: "fetchText", type: "function", desc: "fetchText(url) 拉取纯文本(README 等),失败抛错" },
{ name: "fetchJson", type: "function", desc: "fetchJson(url) 拉取并解析 JSON" },
{ name: "splitSections", type: "function", desc: "splitSections(markdown, prefix) 按标题前缀(如 '### ')切分成段落数组" },
{ name: "firstMatch", type: "function", desc: "firstMatch(text, /正则/) 返回第一个捕获组,未匹配返回空串" },
{ name: "extractImages", type: "function", desc: "extractImages(baseUrl, markdown) 提取 markdown 图片并补全为绝对地址" },
{ name: "absoluteUrl", type: "function", desc: "absoluteUrl(baseUrl, path) 把相对路径拼成绝对 URL" },
{ name: "tagsFromHeading", type: "function", desc: "tagsFromHeading(heading) 从标题按 / & 、与 切出标签(小写去重前)" },
{ name: "splitTags", type: "function", desc: "splitTags(value, /分隔符/) 切分标签并转小写去空" },
{ name: "markdownPreview", type: "function", desc: "markdownPreview(images) 把图片数组拼成 markdown 预览文本" },
{ name: "leftPad", type: "function", desc: "leftPad(n) 数字左补零到 4 位,用于生成有序 id" },
{ name: "makePrompt", type: "function", desc: "makePrompt({id,title,prompt,coverUrl,tags,preview}) 构造一条提示词;title 和 prompt 必填" },
{ name: "signal", type: "AbortSignal", desc: "取消信号,可透传给需要的请求" },
];
+79 -177
View File
@@ -1,22 +1,12 @@
import localforage from "localforage";
export type Prompt = {
id: string;
title: string;
coverUrl: string;
prompt: string;
tags: string[];
category: string;
githubUrl: string;
preview: string;
createdAt: string;
updatedAt: string;
};
import { runPromptSource, type RawPrompt } from "./prompt-source-runtime";
import { usePromptSourceStore } from "@/stores/use-prompt-source-store";
import type { PromptSource } from "./prompt-source-presets";
type PromptCategory = {
export type Prompt = RawPrompt & {
category: string;
githubUrl: string;
build: () => Promise<Omit<Prompt, "category" | "githubUrl">[]>;
};
export const ALL_PROMPTS_OPTION = "全部";
@@ -28,27 +18,70 @@ export type PromptListResponse = {
total: number;
};
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 cacheTtlMs = 1000 * 60 * 60;
const promptCacheKey = "third-party-prompts";
const promptCacheStore = localforage.createInstance({ name: "infinite-canvas", storeName: "prompt_cache" });
const categories: PromptCategory[] = [
{ 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 },
];
type SourceCache = { items: Prompt[]; fetchedAt: number; signature: string };
let loadingPrompts: Promise<Prompt[]> | null = null;
const loadingSources = new Map<string, Promise<Prompt[]>>();
function enabledSources() {
return usePromptSourceStore.getState().sources.filter((source) => source.enabled);
}
function cacheKey(sourceId: string) {
return `prompt-source:${sourceId}`;
}
/** Cheap stable signature of a source so cached prompts invalidate when the script or name changes. */
function sourceSignature(source: PromptSource) {
const value = `${source.name}\n${source.githubUrl}\n${source.script}`;
let hash = 0;
for (let i = 0; i < value.length; i += 1) {
hash = (hash * 31 + value.charCodeAt(i)) | 0;
}
return `${value.length}:${hash}`;
}
function withSourceMeta(source: PromptSource, items: RawPrompt[]): Prompt[] {
return items.map((item) => ({ ...item, category: source.name, githubUrl: source.githubUrl }));
}
async function runSource(source: PromptSource): Promise<Prompt[]> {
const items = await runPromptSource(source.script);
const prompts = withSourceMeta(source, items);
await promptCacheStore.setItem<SourceCache>(cacheKey(source.id), { items: prompts, fetchedAt: Date.now(), signature: sourceSignature(source) });
return prompts;
}
async function getSourcePrompts(source: PromptSource, force = false): Promise<Prompt[]> {
const signature = sourceSignature(source);
if (!force) {
const cached = await promptCacheStore.getItem<SourceCache>(cacheKey(source.id));
if (cached?.items?.length && cached.signature === signature && Date.now() - cached.fetchedAt < cacheTtlMs) return cached.items;
}
if (!force && loadingSources.has(source.id)) return loadingSources.get(source.id)!;
const loading = runSource(source).finally(() => loadingSources.delete(source.id));
loadingSources.set(source.id, loading);
return loading;
}
/** Aggregate prompts across all enabled sources; a failing source is skipped so others still load. */
async function getAllPrompts(): Promise<Prompt[]> {
const settled = await Promise.all(
enabledSources().map(async (source) => {
try {
return await getSourcePrompts(source);
} catch {
return [];
}
}),
);
return settled.flat();
}
export async function fetchPrompts({ keyword = "", tag = [], category = ALL_PROMPTS_OPTION, page = 1, pageSize = 20 }: { keyword?: string; tag?: string[]; category?: string; page?: number; pageSize?: number } = {}) {
const items = await getPrompts();
const items = await getAllPrompts();
const normalizedKeyword = keyword.trim().toLowerCase();
const normalizedPage = Math.max(1, page);
const normalizedPageSize = Math.max(1, Math.min(100, pageSize));
@@ -58,35 +91,36 @@ export async function fetchPrompts({ keyword = "", tag = [], category = ALL_PROM
return {
items: filtered.slice((normalizedPage - 1) * normalizedPageSize, normalizedPage * normalizedPageSize),
tags: collectTags(withoutTagFilter),
categories: categories.map((item) => item.category),
categories: enabledSources().map((source) => source.name),
total: filtered.length,
};
}
async function getPrompts() {
const cached = await promptCacheStore.getItem<{ items?: Prompt[]; fetchedAt?: number }>(promptCacheKey);
if (cached?.items?.length && cached.fetchedAt && Date.now() - cached.fetchedAt < cacheTtlMs) return cached.items;
if (loadingPrompts) return loadingPrompts;
loadingPrompts = loadPrompts().finally(() => {
loadingPrompts = null;
});
return loadingPrompts;
/** Load a single source's prompts (used by the source content table). Throws so the caller can show the error. */
export async function fetchSourcePrompts(sourceId: string, force = false): Promise<Prompt[]> {
const source = usePromptSourceStore.getState().sources.find((item) => item.id === sourceId);
if (!source) throw new Error("提示词来源不存在");
return getSourcePrompts(source, force);
}
async function loadPrompts() {
/** Force refetch one source and refresh its cache; returns the fetched count. */
export async function refreshSource(sourceId: string): Promise<number> {
const items = await fetchSourcePrompts(sourceId, true);
return items.length;
}
/** Force refetch every enabled source; returns the total prompt count. */
export async function refreshAllSources(): Promise<number> {
const settled = await Promise.all(
categories.map(async (category) => {
enabledSources().map(async (source) => {
try {
const items = await category.build();
return items.map((item) => ({ ...item, category: category.category, githubUrl: category.githubUrl }));
return await getSourcePrompts(source, true);
} catch {
return [];
}
}),
);
const items = settled.flat();
await promptCacheStore.setItem(promptCacheKey, { items, fetchedAt: Date.now() });
return items;
return settled.reduce((total, items) => total + items.length, 0);
}
function filterPrompts(items: Prompt[], options: { keyword: string; category: string; tags: string[] }) {
@@ -98,142 +132,10 @@ function filterPrompts(items: Prompt[], options: { keyword: string; category: st
});
}
async function buildAwesomeGptImagePrompts() {
const markdown = await fetchText(awesomeGptImageRawBase, "README.zh-CN.md");
const items: Omit<Prompt, "category" | "githubUrl">[] = [];
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<Prompt, "category" | "githubUrl">[] = [];
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<Prompt, "category" | "githubUrl">[] = [];
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<Array<{ id?: number; title_en?: string; title_cn?: string; category?: string; category_cn?: string; prompt?: string; note?: string; author?: string; source?: string; needs_ref?: boolean; image?: string }>>(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<Prompt, "category" | "githubUrl"> => Boolean(item));
}
function defaultPrompt(id: string, title: string, prompt: string, coverUrl: string, tags: string[], preview: string): Omit<Prompt, "category" | "githubUrl"> {
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<T>(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";
}