ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

又一个开源王炸!用 TaoToken 统一 Key 把 GLM-4.7 接入 Next.js,多个 PDF 一键总结成 PPT

又一个开源王炸!用 TaoToken 统一 Key 把 GLM-4.7 接入 Next.js,多个 PDF 一键总结成 PPT 1. 为什么我要把多个 PDF 一键总结成 PPT年底整理项目资料这件事我猜你也躲不过。电脑里躺着十几个项目的需求文档、排期表、架构图格式还各不相同Word、Excel、PDF 混在一起。想用 AI 帮忙归纳一下结果得一个个文件点开、复制、粘贴文件一多手指先累断模型上下文还容易爆掉。我这次想做的是一个能直接吃下多份 PDF、自动抽取要点、最后吐出 PPT 结构的本地工具。核心检索词就是「GLM-4.7 接入 Next.js 多 PDF 总结 PPT」它解决的问题很具体把散落的文档变成一份能直接讲的汇报材料。适合谁适合年底要写总结的职场人、要整理多份技术文档的开发者以及想在自己 Next.js 项目里接大模型能力的前端同学。技术选型上我用 Next.js 做本地项目骨架通过 TaoToken 统一 Key 调用 GLM-4.7 完成长文本解析和要点抽取再把结果组织成 PPT 页面结构。TaoToken 在这里的角色是统一 API 通道你只需要一个 Key就能在 Next.js 里稳定调用 GLM-4.7不用为每个模型单独维护一套鉴权和地址。下面我把环境变量、请求配置、PDF 分片、提示词模板和完整验证步骤都拆开讲你可以直接跟着做。先说清楚整体链路上传多份 PDF → 服务端按页分片 → 逐片调用 GLM-4.7 抽取要点 → 汇总成 PPT 大纲 → 前端渲染成可翻页的 PPT 结构 → 导出。每一步我都会给出可复制的代码或配置重点放在「能跑起来」而不是概念介绍。2. TaoToken 前置准备统一 Key 与 GLM-4.7 接入配置在写业务代码之前先把调用通道打通。TaoToken 的官网是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 。你需要先拿到一个 API Key这个 Key 就是后面 Next.js 项目里唯一要填的凭证。拿到 Key 之后在项目根目录创建.env.local这是 Next.js 读取环境变量的标准位置。注意不要把它提交到 git.gitignore里默认会忽略.env*.local。# .env.local TAOTOKEN_API_KEYsk-你的TaoToken密钥 TAOTOKEN_BASE_URLhttps://taotoken.net/api GLM_MODEL_IDglm-4.7这里三个变量各有用途TAOTOKEN_API_KEY是鉴权凭证TAOTOKEN_BASE_URL是统一请求入口GLM_MODEL_ID指定本次调用的模型。把模型 ID 单独抽出来是为了后面切换模型时不用改业务代码。如果你用的是 Claude Code 这类终端工具做辅助开发它的配置文件和 Next.js 的环境变量是两套东西别混在一起。Claude Code 的 settings 里通常长这样路径和字段名要对齐{ env: { ANTHROPIC_AUTH_TOKEN: sk-你的TaoToken密钥, ANTHROPIC_BASE_URL: https://taotoken.net/api, API_TIMEOUT_MS: 3000000, CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 1 } }注意ANTHROPIC_BASE_URL后面不要带多余斜杠也不要拼上/v1之类的路径否则容易出现 404 或 local proxy failed。Base URL、Key、Model ID 这三件套要成套出现缺一个都会在请求阶段报错。在 Next.js 里我建议把调用封装成一个独立的服务端模块放在lib/glm.ts。这样前端组件只负责上传和展示真正的模型调用留在服务端Key 不会暴露到浏览器。下面这段是封装的核心// lib/glm.ts const BASE_URL process.env.TAOTOKEN_BASE_URL!; const API_KEY process.env.TAOTOKEN_API_KEY!; const MODEL_ID process.env.GLM_MODEL_ID || glm-4.7; export async function chatWithGLM(prompt: string, system?: string) { const res await fetch(${BASE_URL}/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${API_KEY}, }, body: JSON.stringify({ model: MODEL_ID, messages: [ ...(system ? [{ role: system, content: system }] : []), { role: user, content: prompt }, ], temperature: 0.3, }), }); if (!res.ok) { const errText await res.text(); throw new Error(GLM 请求失败 ${res.status}: ${errText}); } const data await res.json(); return data.choices?.[0]?.message?.content ?? ; }这段代码里有两个细节值得说。第一temperature设成 0.3因为要点抽取需要稳定不需要太多发散。第二错误处理里把响应体原样抛出来方便你在终端看到真实报错而不是只看到一个 500。很多新手卡住就是因为只打印了状态码看不到服务端返回的具体原因。配置完成后你可以先用一个最小脚本验证通道是否通。在项目里建一个scripts/ping.ts用npx tsx scripts/ping.ts跑一下import { chatWithGLM } from ../lib/glm; chatWithGLM(用一句话说明你是什么模型).then(console.log);如果终端打印出模型的自述说明 Key、Base URL、Model ID 三件套都对了。这一步别跳过后面 PDF 解析出问题时你才能确定是业务逻辑的锅还是通道的锅。3. 可复制配置PDF 分片、提示词模板与 PPT 结构生成通道通了之后进入真正的业务逻辑。多份 PDF 的难点在于单个文件可能几十页几份加起来上下文很容易超。我的做法是「按页分片 逐片抽取 汇总成大纲」而不是把整份 PDF 一次性塞给模型。先装两个依赖一个负责解析 PDF一个负责在 Node 环境里读取文件npm install pdf-parse npm install -D types/pdf-parse解析和分片的逻辑放在lib/pdf.ts// lib/pdf.ts import fs from fs; import pdfParse from pdf-parse; export type PdfChunk { fileName: string; pageStart: number; pageEnd: number; text: string; }; const PAGES_PER_CHUNK 5; export async function splitPdf(filePath: string, fileName: string): PromisePdfChunk[] { const buffer fs.readFileSync(filePath); const data await pdfParse(buffer); const pages data.text.split(/\f/); // 按换页符切分 const chunks: PdfChunk[] []; for (let i 0; i pages.length; i PAGES_PER_CHUNK) { const slice pages.slice(i, i PAGES_PER_CHUNK).join(\n); if (slice.trim().length 20) continue; // 跳过空白页 chunks.push({ fileName, pageStart: i 1, pageEnd: Math.min(i PAGES_PER_CHUNK, pages.length), text: slice, }); } return chunks; }PAGES_PER_CHUNK 5是我实测下来比较稳的值。太小会导致请求次数暴涨太大又容易让单次上下文过长。你可以根据自己文档的密度调整图表多的 PDF 可以降到 3。接下来是提示词模板。要点抽取和 PPT 结构生成是两个不同任务我分成两套提示词避免模型在一轮里既总结又排版导致输出混乱。第一套单分片要点抽取// lib/prompts.ts export const EXTRACT_SYSTEM 你是一个严谨的文档分析助手。 请从用户提供的文档片段中抽取要点要求 1. 只保留事实性信息不要编造数据 2. 每条要点不超过 40 字 3. 如果片段包含图表描述单独标注 [图表] 4. 输出为 JSON 数组每项包含 title 和 detail 两个字段。; export function buildExtractPrompt(chunk: { fileName: string; pageStart: number; pageEnd: number; text: string }) { return 文件${chunk.fileName}第 ${chunk.pageStart}-${chunk.pageEnd} 页 内容 ${chunk.text} 请按系统要求输出 JSON 数组。; }第二套汇总成 PPT 大纲export const OUTLINE_SYSTEM 你是一个演示文稿结构专家。 用户会给你多份文档的要点集合请生成一份 PPT 大纲要求 1. 总页数控制在 10-12 页 2. 每页包含 title、bullets3-5 条、layout 三个字段 3. layout 取值只能是 title / content / chart / ending 4. 输出为 JSON 对象包含 slides 数组。; export function buildOutlinePrompt(allPoints: string) { return 以下是多份文档抽取出的要点集合 ${allPoints} 请生成 PPT 大纲 JSON。; }把这两步串起来的服务端逻辑放在app/api/analyze/route.ts。Next.js 的 App Router 里这个文件对应POST /api/analyze// app/api/analyze/route.ts import { NextRequest, NextResponse } from next/server; import { splitPdf } from /lib/pdf; import { chatWithGLM } from /lib/glm; import { EXTRACT_SYSTEM, buildExtractPrompt, OUTLINE_SYSTEM, buildOutlinePrompt } from /lib/prompts; export async function POST(req: NextRequest) { const formData await req.formData(); const files formData.getAll(files) as File[]; const allPoints: string[] []; for (const file of files) { const arrayBuffer Buffer.from(await file.arrayBuffer()); const tmpPath /tmp/${file.name}; require(fs).writeFileSync(tmpPath, arrayBuffer); const chunks await splitPdf(tmpPath, file.name); for (const chunk of chunks) { const raw await chatWithGLM(buildExtractPrompt(chunk), EXTRACT_SYSTEM); allPoints.push(raw); } } const outlineRaw await chatWithGLM(buildOutlinePrompt(allPoints.join(\n)), OUTLINE_SYSTEM); const outline JSON.parse(outlineRaw.replace(/json|/g, ).trim()); return NextResponse.json({ slides: outline.slides }); }这里有个坑要提前说模型返回的 JSON 有时会包在 json 代码块里直接JSON.parse会炸。所以我用replace把代码块标记去掉再解析。你也可以写一个更健壮的提取函数但初期这样够用。前端部分一个上传组件加一个结果展示组件就够了。上传组件用FormData把多个文件发给/api/analyze结果组件按slides数组渲染成可翻页的卡片。样式用 Tailwind每页根据layout字段切换不同的排版。// app/page.tsx 片段 const [slides, setSlides] useStateany[]([]); const [loading, setLoading] useState(false); async function handleUpload(e: React.ChangeEventHTMLInputElement) { const files e.target.files; if (!files?.length) return; setLoading(true); const fd new FormData(); Array.from(files).forEach((f) fd.append(files, f)); const res await fetch(/api/analyze, { method: POST, body: fd }); const data await res.json(); setSlides(data.slides || []); setLoading(false); }到这一步配置和代码就齐了。你可以先把PAGES_PER_CHUNK调小用一份两三页的 PDF 跑通全流程再换成真实的多份大文件。4. 验证请求与成功结果从上传到导出 PPT 的完整步骤代码写完不代表能跑我按顺序给你一套验证步骤每一步都有明确的预期结果出问题时你能快速定位到哪一环。第一步启动开发服务器npm run dev预期结果终端打印ready - started server on 0.0.0.0:3000。如果端口被占用Next.js 会自动换到 3001注意看终端提示的实际地址。第二步打开浏览器访问http://localhost:3000你应该看到上传区域和「开始分析」按钮。先别急着传大文件准备一份 2-3 页的测试 PDF点上传。第三步观察终端日志。正常情况下你会看到类似这样的输出每个分片一次请求POST /api/analyze 200 in 8421ms如果终端出现GLM 请求失败 401说明 Key 不对或没读到环境变量。如果出现local proxy failed多半是 Base URL 写错或网络层拦截。如果出现Cannot read properties of undefined (reading choices)说明返回结构和你预期的不一样把res.json()的结果打印出来看看。第四步上传成功后页面应该渲染出 PPT 卡片。每张卡片有标题、要点列表layout为chart的页面会显示一个占位图表区。你可以点「下一页」翻页验证交互是否正常。第五步导出。我用的方案是把当前slides数据序列化后在前端用html2canvas加jspdf逐页截图导出 PDF或者直接生成一个可打印的 HTML 页面让浏览器「打印为 PDF」。后者更轻量代码也更少function exportAsHtml(slides: any[]) { const html slides .map( (s) section stylepage-break-after:always;padding:40px h2${s.title}/h2 ul${s.bullets.map((b: string) li${b}/li).join()}/ul /section ) .join(); const win window.open(, _blank); win?.document.write(htmlbody${html}/body/html); win?.document.close(); }调用后浏览器会弹出一个新窗口里面是排版好的页面按 CtrlP 选择「另存为 PDF」即可。实测下来这种方式对中文排版最友好不会出现字体乱码。第六步验证多文件场景。一次选 3 份 PDF 上传观察终端请求次数是否等于所有分片之和。如果某份文件解析出来是空的检查它是不是扫描件——pdf-parse只能提取文本层纯图片 PDF 需要额外的 OCR 步骤这不在本次范围内。成功的结果长这样页面按顺序展示 10-12 页 PPT 结构每页要点来自真实文档内容没有明显编造。导出后的 PDF 可以直接拿去讲。整个过程从上传到拿到结果3 份中等大小的 PDF 大约 1-2 分钟取决于分片数量和网络往返。5. 本篇常见错误排查401、local proxy failed 与 JSON 解析失败这一节我把实际踩到的报错和对应解法列出来你遇到时可以直接对照。报错一401 Unauthorized。终端返回GLM 请求失败 401: {error:invalid api key}。原因通常是.env.local没生效或者 Key 复制时带了空格。Next.js 修改环境变量后必须重启npm run dev热更新不会重新读取.env.local。另外确认你请求头里是Authorization: Bearer ${API_KEY}Bearer 和 Key 之间有一个空格。报错二local proxy failed。这个报错一般出现在 Base URL 配置错误时。检查TAOTOKEN_BASE_URL是否严格等于https://taotoken.net/api不要多加/v1也不要在末尾加斜杠。如果你在 Claude Code 的 settings 里配置字段名是ANTHROPIC_BASE_URL值同样用这个地址。Base URL、Key、Model ID 三件套要成套出现任何一项缺失或写错都会触发这类错误。报错三reading choices。完整报错是TypeError: Cannot read properties of undefined (reading choices)。这说明data.choices是 undefined通常是返回体结构和你预期不同或者请求根本没成功但状态码是 200。解法是在chatWithGLM里加一行日志const data await res.json(); console.log(GLM raw response:, JSON.stringify(data).slice(0, 500));打印出来你就能看到真实结构。常见原因是模型 ID 写错服务端返回了一个错误对象而不是标准 completion 结构。报错四JSON.parse 失败。报错Unexpected token in JSON at position 0。这是模型把 JSON 包在了 markdown 代码块里。我的处理是raw.replace(/json|/g, ).trim()但如果模型输出了额外说明文字这个正则不够。更稳的做法是用正则提取第一个{到最后一个} 之间的内容function safeParseJson(raw: string) { const match raw.match(/\{[\s\S]*\}/); if (!match) throw new Error(未找到 JSON 内容); return JSON.parse(match[0]); }报错五OAuth 相关提示。如果你在 Claude Code 里看到 OAuth 报错说明它还在走默认的登录态而不是你配置的 Key。确认 settings 文件路径正确且ANTHROPIC_AUTH_TOKEN已填写。改完配置后完全退出终端再重开不要只关窗口。报错六PDF 解析为空。splitPdf返回空数组或者所有分片文本长度都小于 20。先确认 PDF 是不是扫描件再确认pdf-parse版本是否兼容。可以在splitPdf里打印data.text.length如果接近 0就是文件本身没有文本层。把这几类报错覆盖掉基本就能稳定跑通。我的建议是每加一个环节就先单独验证别等整个链路写完再一起调那样定位成本会高很多。6. 把统一 Key 用起来模型对话、Coding Plan 与接入文档通道打通、项目跑起来之后你会发现这套统一 Key 的价值不只是省事。同一个 Key 既能驱动 Next.js 里的 GLM-4.7 做文档解析也能在终端里辅助你写代码不用为每个场景单独申请和维护凭证。如果你想先单独验证模型能力可以直接用模型对话页面试几轮把一份 PDF 的片段贴进去看看要点抽取的质量是否符合预期再决定提示词怎么调https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite如果你像我一样长期用终端做开发Coding Plan 会更合适它把编码场景的调用额度单独规划适合持续性的项目开发https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite需要管理多个 Key 或查看用量时控制台在这里https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite新建或轮换 Key 的入口https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite接入参数和字段说明都整理在文档里遇到 Base URL、模型 ID 不确定时先查这里https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite如果你用 Claude Code 做辅助开发它的接入配置可以参考这份说明注意字段名和 Next.js 环境变量是两套https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaude_codeutm_campaignrewrite最后给你一个实用技巧把PAGES_PER_CHUNK和temperature做成环境变量不同文档密度用不同参数不用改代码。我处理图表多的架构文档时会把分片降到 3 页、temperature 降到 0.2处理文字为主的总结时用默认值就行。这套参数调优比换模型更能提升最终 PPT 的质量。
返回列表