Files

1 line
6.4 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "changluei/pptgo", "files": {"app/api/ai/generate/route.ts": {"434156dd8f26ce3ac8b999676c2e8ce7cfbcc3cc": {"t": 1785745578.237695, "n": 4777, "h": "403c354fa6a3ab39", "c": "import { NextRequest, NextResponse } from \"next/server\";\n\ntype GenerateRequest = {\n title?: string;\n brief?: string;\n theme?: string;\n themePackage?: string;\n layouts?: string[];\n signatureLayouts?: string[];\n attachments?: Array<{ name?: string; type?: string; size?: number }>;\n};\n\nfunction stripCodeFence(content: string) {\n return content\n .replace(/^```(?:markdown|md)?\\s*/i, \"\")\n .replace(/\\s*```$/, \"\")\n .trim();\n}\n\nfunction isAllowedBaseUrl(value: string) {\n try {\n const url = new URL(value);\n const isLocalHttp =\n url.protocol === \"http:\" &&\n [\"localhost\", \"127.0.0.1\", \"::1\"].includes(url.hostname);\n return url.protocol === \"https:\" || isLocalHttp;\n } catch {\n return false;\n }\n}\n\nexport async function POST(request: NextRequest) {\n const body = (await request.json()) as GenerateRequest;\n const title = body.title?.trim();\n const brief = body.brief?.trim();\n\n if (!title || !brief) {\n return NextResponse.json({ error: \"title and brief are required\" }, { status: 400 });\n }\n\n const apiKey =\n request.headers.get(\"x-deepseek-api-key\")?.trim() ||\n process.env.DEEPSEEK_API_KEY;\n if (!apiKey) {\n return NextResponse.json({ error: \"DeepSeek API Key is required\" }, { status: 503 });\n }\n\n const baseUrl = (\n request.headers.get(\"x-deepseek-base-url\")?.trim() ||\n process.env.DEEPSEEK_BASE_URL ||\n \"https://api.deepseek.com\"\n ).replace(/\\/$/, \"\");\n const model =\n request.headers.get(\"x-deepseek-model\")?.trim() ||\n process.env.DEEPSEEK_MODEL ||\n \"deepseek-v4-pro\";\n\n if (!isAllowedBaseUrl(baseUrl)) {\n return NextResponse.json({ error: \"Invalid DeepSeek API URL\" }, { status: 400 });\n }\n\n const attachmentSummary = (body.attachments || [])\n .slice(0, 30)\n .map((item) => `- ${item.name || \"未命名文件\"}(${item.type || \"未知类型\"})`)\n .join(\"\\n\");\n const availableLayouts = (body.layouts || [])\n .filter((layout) => /^[a-zA-Z0-9_-]+$/.test(layout))\n .slice(0, 40);\n const layoutSummary = availableLayouts.length\n ? availableLayouts.join(\", \")\n : \"cover, default, center, section, fact, statement, quote, two-cols, image-left, image-right\";\n const signatureLayouts = (body.signatureLayouts || [])\n .filter((layout) => /^[a-zA-Z0-9_-]+$/.test(layout))\n .slice(0, 30);\n const signatureLayoutSummary = signatureLayouts.length\n ? signatureLayouts.join(\", \")\n : \"未检测到主题专属布局\";\n\n const response = await fetch(`${baseUrl}/chat/completions`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n model,\n temperature: 0.58,\n max_tokens: 8000,\n messages: [\n {\n role: \"system\",\n content: [\n \"你是资深演示文稿策略顾问、中文写作者和 Slidev 排版专家。\",\n \"先在内部完成受众、目标、核心结论、证据链和叙事节奏的规划,但最终只返回完整可运行的 Slidev Markdown,不解释、不包裹代码块。\",\n \"首部必须包含 theme、title、transition 和 layout 的 YAML headmatter;使用 --- 分页,并用每页 frontmatter 指定 layout。\",\n \"theme 值必须与用户提供的 Slidev 主题完全一致;若包含 @、/ 或其他符号,必须用 YAML 引号包裹。\",\n \"演示不是提纲。每一页都要推动一个明确判断:给出上下文、论据、含义或行动,避免只有标题加两三个空泛短句。\",\n \"默认生成 10–14 页;若用户明确指定页数则服从。内容页通常提供 3–5 个有信息量的要点,或一个完整图表/流程/对比结构。\",\n \"至少使用 5 种不同布局,连续两页不要使用相同布局。仅使用用户提供的布局列表,不发明主题组件。\",\n \"优先使用主题专属布局,让成稿明显体现所选模板,而不是所有页面都退化成通用 default/cover。封面优先采用主题专属的 intro/title 类布局。\",\n \"在封面、章节、事实数字、双栏对比、流程、引用和行动页之间形成节奏。two-cols 布局使用 ::right:: 分栏。\",\n \"每个重要内容页末尾加入 <!-- ... --> 演讲者备注,写 2–4 句讲述提示、过渡或风险说明。\",\n \"可以使用 Mermaid 表达流程、时间线和关系。不要虚构来源中没有的数字、事实或引语;缺少证据时明确标为“待补充数据”或给出验证建议。\",\n \"图片附件使用 Markdown 引用 attachment://原文件名,不要猜测图片内容。\",\n ].join(\"\\n\"),\n },\n {\n role: \"user\",\n content: [\n `演示标题:${title}`,\n `Slidev 主题:${body.theme || \"default\"}`,\n `实际安装的主题包:${body.themePackage || \"未提供\"}`,\n `该主题可用布局:${layoutSummary}`,\n `该主题专属布局(优先使用):${signatureLayoutSummary}`,\n `用户要求:${brief}`,\n attachmentSummary\n ? `当前演示文稿的附件(仅提供文件名与类型,无法读取图片内容):\\n${attachmentSummary}`\n : \"当前没有附件。\",\n \"请把用户要求发展成一条完整叙事:为什么现在要讲、我们看到了什么、核心判断是什么、有哪些方案与取舍、接下来具体怎么做。不要为了简洁牺牲必要论证。\",\n ].join(\"\\n\\n\"),\n },\n ],\n }),\n });\n\n if (!response.ok) {\n return NextResponse.json(\n { error: `DeepSeek request failed: ${response.status}` },\n { status: 502 },\n );\n }\n\n const data = (await response.json()) as {\n choices?: Array<{ message?: { content?: string } }>;\n };\n const content = data.choices?.[0]?.message?.content;\n if (!content) {\n return NextResponse.json({ error: \"DeepSeek returned no content\" }, { status: 502 });\n }\n\n return NextResponse.json({ markdown: stripCodeFence(content) });\n}\n"}}}}