Next.js AI智能摘要工具:从零到Product Hunt上线全指南
上个月我构建了一个轻量级工具:用户粘贴任意长文,AI 自动提炼三句核心摘要。
没有后台管理、没有用户系统、没有付费门槛。只有一个输入框、一个按钮、一段即时生成的摘要。
开发耗时三天,第四天提交到 Product Hunt 并拿下当日 Top 5。
本文完整拆解技术实现细节。代码量不多,但每行都经过反复删减,只保留最必要的逻辑。
一、产品设计:减法原则
1.1 功能清单
整个工具仅包含三项操作:
- 粘贴或手动输入长文本
- 点击按钮触发 AI 摘要生成
- 一键复制生成的摘要
全部功能到此为止,没有多余负担。
graph LRA["用户输入长文"] --> B["调用 AI API"]B --> C["流式输出摘要"]C --> D["一键复制"]style B fill:#8b5cf6,color:#fff
1.2 技术选型
| 层 | 选择 | 理由 |
|---|---|---|
| 框架 | Next.js 14 (App Router) | 前后端一体化,Route Handler 直接充当 API |
| 样式 | CSS Modules | 无额外依赖,轻量够用 |
| AI | OpenAI gpt-4o-mini | 成本低、响应快、摘要质量可靠 |
| 部署 | Vercel | 零配置,推送代码即自动上线 |
独立开发恪守一条原则:尽量少引入依赖。每多一次 npm install,就多一份未来的维护负担。
二、核心实现
2.1 项目结构
summarizer/├── app/│ ├── layout.tsx # 全局布局│ ├── page.tsx # 首页(唯一页面)│ ├── page.module.css # 首页样式│ └── api/│ └── summarize/│ └── route.ts # AI 摘要接口├── components/│ ├── TextInput.tsx # 输入框组件│ ├── SummaryOutput.tsx # 摘要输出组件│ └── CopyButton.tsx # 复制按钮├── lib/│ └── openai.ts # OpenAI 封装└── package.json
总共 8 个文件,每个文件职责清晰,没有冗余。
2.2 后端:流式 AI 接口
// app/api/summarize/route.tsimport { NextRequest } from 'next/server';export async function POST(req: NextRequest) { const { text } = await req.json(); // 输入校验:直接拦截无效请求 if (!text || text.length < 50) { return new Response(JSON.stringify({ error: '文本太短,至少 50 个字' }), { status: 400 }); } if (text.length > 10000) { return new Response(JSON.stringify({ error: '文本太长,最多 10000 个字' }), { status: 400 }); } // 调用 OpenAI 流式接口 const response = await fetch('https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`, }, body: JSON.stringify({ model: 'gpt-4o-mini', stream: true, temperature: 0.3, // 摘要场景使用低温度,输出更稳定 messages: [ { role: 'system', content: '你是一个专业的文本摘要助手。请用三句话概括用户提供的文章核心内容。要求:第一句点明主题,第二句总结关键论点,第三句给出结论或启发。语言精炼,不要套话。', }, { role: 'user', content: `请摘要以下文章:${text}`, }, ], }), }); // 直接透传 SSE 流给前端 return new Response(response.body, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', }, });}
此处直接将 OpenAI 的 SSE 流透传给前端。不做中间缓存,不进行二次封装,数据路径最短,延迟最低。
2.3 前端:流式渲染
// components/SummaryOutput.tsx'use client';import { useState } from 'react';import styles from './SummaryOutput.module.css';interface Props { text: string; onComplete: (summary: string) => void;}export default function SummaryOutput({ text, onComplete }: Props) { const [summary, setSummary] = useState(''); const [loading, setLoading] = useState(false); const handleSummarize = async () => { setLoading(true); setSummary(''); const response = await fetch('/api/summarize', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text }), }); if (!response.ok || !response.body) { setSummary('摘要生成失败,请重试'); setLoading(false); return; } // 流式读取 SSE const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; let fullText = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; // 最后一行可能不完整,留到下次 for (const line of lines) { if (line === 'data: [DONE]') continue; if (!line.startsWith('data: ')) continue; try { const json = JSON.parse(line.slice(6)); const content = json.choices?.[0]?.delta?.content || ''; fullText += content; setSummary(fullText); // 逐字更新,打字机效果 } catch { // 跳过解析失败的行 } } } setLoading(false); onComplete(fullText); }; return ( {summary && ( {summary}
{!loading && } )} );}
通过流式读取 SSE 并逐字更新页面内容,模拟打字机输出,让用户直观感受到 AI 的实时生成过程。
2.4 一键复制
// components/CopyButton.tsx'use client';import { useState } from 'react';import styles from './CopyButton.module.css';export default function CopyButton({ text }: { text: string }) { const [copied, setCopied] = useState(false); const handleCopy = async () => { try { await na vigator.clipboard.writeText(text); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch { // 降级方案:使用 textarea 选中复制 const textarea = document.createElement('textarea'); textarea.value = text; document.body.appendChild(textarea); textarea.select(); document.execCommand('copy'); document.body.removeChild(textarea); setCopied(true); setTimeout(() => setCopied(false), 2000); } }; if (!text) return null; return ( );}
复制功能实现得很纯粹:优先调用 Clipboard API,如果不支持,则回退到 textarea 选中复制方案,确保在所有浏览器下都能正常工作。
三、样式:克制之美
3.1 设计原则
整个 UI 仅使用黑、白、一个紫色强调色。不堆砌渐变、不滥用阴影。
/* app/page.module.css */.main { max-width: 640px; margin: 0 auto; padding: 4rem 1.5rem; font-family: -apple-system, 'Noto Sans SC', sans-serif;}.title { font-size: 1.5rem; font-weight: 600; color: #1a1a1a; margin-bottom: 0.5rem;}.subtitle { font-size: 0.875rem; color: #888; margin-bottom: 2rem;}.textarea { width: 100%; min-height: 200px; padding: 1rem; border: 1px solid #e5e5e5; border-radius: 8px; font-size: 0.9375rem; line-height: 1.6; resize: vertical; transition: border-color 0.2s; font-family: inherit;}.textarea:focus { outline: none; border-color: #8b5cf6;}/* 输出区域:打字机闪烁光标 */.output { margin-top: 1.5rem; padding: 1.25rem; background: #fafafa; border-radius: 8px; line-height: 1.8; color: #333;}.cursor { display: inline-block; width: 2px; height: 1em; background: #8b5cf6; animation: blink 0.8s infinite; vertical-align: text-bottom; margin-left: 2px;}@keyframes blink { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0; }}
紫色作为唯一的强调色,搭配闪烁光标模拟打字效果。这种极简设计正是“克制之美”的体现。
四、部署:三条命令上线
# 第一步:设置环境变量echo "OPENAI_API_KEY=sk-你的密钥" > .env.local# 第二步:推送至 GitHubgit add . && git commit -m "feat: 极简摘要工具" && git push# 第三步:在 Vercel 导入项目# 打开 vercel.com -> Import -> 选择仓库 -> 填入环境变量 -> Deploy# 完成。
Vercel 自动识别 Next.js 项目,零配置完成部署。内置 HTTPS、CDN、Edge Runtime,开箱即用。
五、避坑指南
5.1 流式透传的陷阱
Vercel 的 Edge Runtime 和 Node.js Runtime 对流式数据处理方式不同:
// 若使用 Edge Runtime(推荐,速度更快)export const runtime = 'edge';// 若使用 Node.js Runtime,需要手动用 ReadableStream 包装一层// Edge Runtime 可直接透传 response.body
5.2 成本控制
GPT-4o-mini 虽然便宜,但如果不加限制,仍可能被恶意刷量。
// 简单的基于 IP 的速率限制const rateLimitMap = new Map();function checkRateLimit(ip: string): boolean { const now = Date.now(); const windowMs = 60 * 1000; // 1 分钟窗口 const maxRequests = 10; // 每分钟最多 10 次 const timestamps = rateLimitMap.get(ip) || []; const recent = timestamps.filter(t => now - t < windowMs); if (recent.length >= maxRequests) { return false; // 超出限制 } recent.push(now); rateLimitMap.set(ip, recent); return true;}
5.3 SEO 基础
// app/layout.tsxexport const metadata = { title: '三句话摘要 - AI 智能文本摘要工具', description: '粘贴任意长文,AI 帮你用三句话精准概括核心内容。免费、快速、无需注册。',};
六、总结
三天时间,8 个文件,一个能直接使用的产品。
技术栈越精简,迭代速度越快。不要在第一天就引入 Redis、数据库、用户系统。先把核心价值跑通,让用户用上,再根据实际需求逐步扩展。
好代码,是删出来的。