从零开始,借助 AI 编程助手(Cursor + ChatGPT),用 72 小时完成一个具备 AI 摘要能力的全栈笔记应用,并成功部署到腾讯云轻量服务器。本文完整记录需求拆解、代码实现、调试排错、部署上架全流程,附赠可运行的代码片段与云原生配置。
我不是资深架构师,也不是科班出身。这篇文章之所以敢叫“实战”,是因为它不是演示 TodoMVC,而是一个真正上架到云服务器、用户可访问、包含 AI 接口调用的生产级项目。全程只用 AI 编程辅助,代码量约 1200 行,涉及:
文章会暴露所有踩坑点,包括 AI 生成的错误代码如何修正、如何保证 API Key 安全、如何做冷启动优化。全文无一句废话,每段都有可执行的命令或代码。
我向 Claude 3.5 输入:
我要做一个个人笔记应用,核心功能:
1. Markdown 编辑 + 实时预览
2. 笔记列表(按时间倒序)
3. 每篇笔记可调用 AI 生成 100 字摘要
4. 支持标签分类
5. 用户只需本地登录(单用户模式,暂不接入 OAuth)
技术栈要求:Next.js 14 App Router,Prisma,SQLite,OpenAI API,部署到腾讯云。
请给出详细的目录结构、数据模型设计、API 路由规划。AI 返回了完整的 schema.prisma:
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model Note {
id String @id @default(cuid())
title String
content String @db.Text
summary String? @db.Text
tags String[] // 简单用字符串数组
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Setting {
id String @id @default(cuid())
openaiKey String? // 加密存储,实际生产用环境变量
modelName String @default("gpt-3.5-turbo")
updatedAt DateTime @updatedAt
}关键决策:单用户模式大幅降低鉴权复杂度,让 AI 能集中精力处理核心 CRUD 和 AI 调用逻辑——这是从 0 基础到上架的“减法策略”。
npx create-next-app@latest ai-notes --typescript --tailwind --app
cd ai-notes
npm install @prisma/client prisma sqlite3
npm install openai @radix-ui/react-dialog @radix-ui/react-label class-variance-authority clsx tailwind-merge
npm install -D @types/node我使用 Cursor 的 Composer 功能,输入:
“根据上述 schema,生成
app/api/notes/route.ts实现 GET 列表和 POST 创建,使用 Prisma 客户端,错误处理统一返回 JSON。”
AI 生成代码后,我手动修正了 Prisma 实例化方式(避免热重载时多次初始化):
// lib/prisma.ts
import { PrismaClient } from '@prisma/client'
const globalForPrisma = global as unknown as { prisma: PrismaClient }
export const prisma =
globalForPrisma.prisma || new PrismaClient({ log: ['query'] })
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prismaapp/page.tsx 使用 Server Component 直接查询数据库,并配合 useInView 实现滚动加载:
// app/page.tsx
import { prisma } from '@/lib/prisma'
import { NoteCard } from '@/components/NoteCard'
import { LoadMore } from '@/components/LoadMore'
const PAGE_SIZE = 10
export default async function Home() {
const initialNotes = await prisma.note.findMany({
orderBy: { createdAt: 'desc' },
take: PAGE_SIZE,
})
return (
<main className="container mx-auto p-4">
<div className="grid gap-4">
{initialNotes.map((note) => (
<NoteCard key={note.id} note={note} />
))}
</div>
<LoadMore skip={PAGE_SIZE} />
</main>
)
}LoadMore 是客户端组件,利用 useInView 触发 fetch 下一页:
'use client'
import { useEffect, useState } from 'react'
import { useInView } from 'react-intersection-observer'
export function LoadMore({ skip }: { skip: number }) {
const { ref, inView } = useInView()
const [page, setPage] = useState(1)
useEffect(() => {
if (inView) {
fetch(`/api/notes?skip=${skip * page}&take=10`)
.then(res => res.json())
.then(data => {
// 追加到列表(通过事件或状态管理)
window.dispatchEvent(new CustomEvent('appendNotes', { detail: data }))
setPage(p => p + 1)
})
}
}, [inView, skip, page])
return <div ref={ref} className="h-10" />
}技术要点:这里没有用全局状态库,而是用 CustomEvent 通信,减少依赖,适合轻量应用。
这是全篇最“硬”的部分。我放弃了简单的 chat.completions,改用 Assistants API 并开启流式,因为摘要生成较慢,流式能提升用户体验。
// scripts/create-assistant.ts
import OpenAI from 'openai'
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })
async function main() {
const assistant = await openai.beta.assistants.create({
name: 'NoteSummarizer',
instructions: `你是一个专业的笔记摘要生成器。输入是Markdown格式的笔记正文,你需要输出恰好100字的中文摘要,保留关键信息,语言精炼。不要输出额外解释。`,
model: 'gpt-4o-mini',
temperature: 0.3,
})
console.log('Assistant ID:', assistant.id)
}
main()将生成的 asst_xxx 存入 .env.local:ASSISTANT_ID=asst_xxx
app/api/notes/[id]/summary/route.ts:
import { NextRequest } from 'next/server'
import { prisma } from '@/lib/prisma'
import OpenAI from 'openai'
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })
export async function POST(
req: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const note = await prisma.note.findUnique({
where: { id: params.id },
})
if (!note) return new Response('Not found', { status: 404 })
// 创建线程
const thread = await openai.beta.threads.create()
// 提交用户消息
await openai.beta.threads.messages.create(thread.id, {
role: 'user',
content: note.content.substring(0, 4000), // 限制长度
})
// 流式运行
const stream = await openai.beta.threads.runs.stream(
thread.id,
{ assistant_id: process.env.ASSISTANT_ID! }
)
// 返回 SSE 流
const encoder = new TextEncoder()
const readable = new ReadableStream({
async start(controller) {
for await (const chunk of stream) {
if (chunk.event === 'thread.message.delta') {
const delta = chunk.data.delta?.content?.[0] as any
if (delta?.text?.value) {
controller.enqueue(encoder.encode(delta.text.value))
}
}
}
controller.close()
},
})
return new Response(readable, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
},
})
} catch (error) {
console.error(error)
return new Response('AI error', { status: 500 })
}
}'use client'
import { useState } from 'react'
export function SummarizeButton({ noteId }: { noteId: string }) {
const [summary, setSummary] = useState('')
const [loading, setLoading] = useState(false)
const handleSummarize = async () => {
setLoading(true)
setSummary('')
const resp = await fetch(`/api/notes/${noteId}/summary`, { method: 'POST' })
const reader = resp.body?.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader!.read()
if (done) break
const chunk = decoder.decode(value)
setSummary(prev => prev + chunk)
}
setLoading(false)
}
return (
<div>
<button onClick={handleSummarize} disabled={loading}>
{loading ? '生成中...' : 'AI 摘要'}
</button>
{summary && <p className="bg-slate-100 p-2 rounded">{summary}</p>}
</div>
)
}踩坑复盘:AI 初次给出的代码使用 text/plain 而非 SSE,导致前端无法逐字渲染。我改用了 ReadableStream + text/event-stream,并确保每个 chunk 是纯文本(不是 JSON),前端直接拼接。另外,Assistants API 的 stream 方法在 SDK v4+ 中用法有变,需要查阅最新文档并让 AI 修正。
生产环境绝不能在代码中硬编码 OpenAI Key。我采用:
.env.local 存储 OPENAI_API_KEY 和 ASSISTANT_IDSetting 表仅用于存储用户偏好(如模型温度),不存 Key部署到腾讯云时,使用 云服务器 S6 的“自定义环境变量”,通过 systemd 服务注入,避免 .env 文件暴露。
npx prisma migrate dev --name init
npx prisma db seed # 创建一条示例笔记在 package.json 中添加:
"prisma": {
"seed": "tsx prisma/seed.ts"
}prisma/seed.ts:
import { prisma } from '../lib/prisma'
await prisma.note.create({
data: {
title: '欢迎使用 AI 笔记',
content: '这是一篇示例笔记,点击“AI摘要”按钮体验智能总结。',
tags: ['入门'],
},
})FROM node:20-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npx prisma generate
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma
EXPOSE 3000
CMD ["node", "server.js"]关键点:Next.js 的 output: 'standalone' 需要在 next.config.js 中开启:
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
}
module.exports = nextConfigdocker build -t ai-notes .docker run -d \
--name notes \
-p 3000:3000 \
-e DATABASE_URL="file:/app/prisma/dev.db" \
-e OPENAI_API_KEY="sk-..." \
-e ASSISTANT_ID="asst-..." \
ai-notesserver {
listen 80;
server_name your-domain.com;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
}
}pnpm test 跑通 AI 摘要的集成测试(模拟 OpenAI 返回)/api/health 返回 { status: 'ok' }上线后,应用响应时间:
AI 编程的效率:全部代码中约 70% 由 AI 生成,但关键的 30%(错误处理、流式解析、Docker 配置、环境变量注入)必须人工把控。尤其是 Docker 中 Prisma 的 postinstall 钩子和 openssl 依赖,AI 给出的方案缺失,我在部署时报错后手动补充。
源码已开源在 GitHub(见评论区),后续将迭代:
这篇文章从撰写到定稿,也借助了 AI 润色,但所有代码均经过本地和云端双重验证。 如果你按步骤操作,应当能在 3 天内完成从零到上架。腾讯云轻量服务器首月仅几十元,适合个人开发者起步。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。