9.9 KiB
9.9 KiB
wojide (NoteAI) 项目健康度分析报告
分析日期:2026-07-31 | 分析角色:高级开发工程师(吴八哥) 技术栈:Next.js 16 (App Router) · React 19 · TypeScript strict · Prisma 5.10.2 + SQLite · Tailwind 3 · TipTap 3 · Zustand 说明:本文仅做分析与现状盘点,未改动任何代码。所有修复项需你确认后再动手。
0. 整体评价
代码质量高于平均水平:无 any、无 TODO/FIXME、无原始 SQL、主要 API 均有认证与输入校验、XSS 有 sanitize、登录有限速。真正的短板不在业务逻辑,而在两处:
- 仓库卫生(数据库入库 + 占位密钥)——会造成真实的数据泄露与越权风险;
- 内容膨胀路径(base64 图片 × 全量同步 × localStorage 快照)——笔记应用最典型的性能黑洞。
先把这两类按下,其余都是体验与健壮性层面的优化。
1. 🔴 高危问题(建议优先处理)
1.1 数据库文件被提交进 git(泄露密码哈希 + 全部笔记)
- 证据:
git ls-files显示prisma/dev.db在跟踪列表;.gitignore第 36 行虽有prisma/*.db,但该规则是后加的,对已跟踪文件不生效。 - 风险:任何能访问仓库的人都能拿到 scrypt 密码哈希 + 你的全部笔记正文。哈希被拖库后可离线爆破。
- 修复:
git rm --cached prisma/dev.db # 清理历史(破坏性,需评估):git filter-repo --path prisma/dev.db --invert-paths # 随后在 .gitignore 已覆盖的前提下重新提交;已泄露的密码建议重置 - 注意:若仓库已 push 到 GitHub(
origin已存在),清历史会影响协作者,需先沟通或确认是私有仓。
1.2 会话密钥为占位符 + 部署默认弱密码
- 证据:
README.md:11与.env中SESSION_SECRET="replace-with-a-long-random-secret";DEPLOY.md:162-176文档默认密码admin。 - 风险:若按文档部署而未替换,
SESSION_SECRET是公开值,任何人可用它伪造 HMAC 会话 token 直接登录。默认密码admin同样危险。 - 修复:部署前
npm run env:init生成随机值;在src/lib/session.ts启动时显式拒绝已知占位符(抛出启动错误而非静默运行)。
2. 🟠 性能问题
| 位置 | 问题 | 建议 |
|---|---|---|
src/components/editor.tsx:677 |
Image.configure({ allowBase64: true }):粘贴图片以 base64 嵌进 content,单页可达数 MB;每次编辑防抖后整页 content PUT 回传 + 入 SQLite + 进 localStorage 历史快照 |
实现图片上传接口存文件,content 只存 URL(最高优先级性能项) |
src/components/editor.tsx:28 |
import { lowlight } from "lowlight" 全量导出,把 >800KB 高亮语言打进客户端 bundle |
改用 createLowlight(common) 按需注册 |
src/app/page.tsx:87-90 |
localHistory 的 useMemo 依赖 [activePage],每次按键 updatePage 都替换 activePage 引用 → 每次按键都执行 localStorage 读取 + 最多 30 个全文快照 JSON.parse |
依赖改为 [activePage?.id, isHistoryOpen],仅面板打开时读取 |
src/components/sidebar/tree-view.tsx:284-288 |
每层重建 buildFolderFileCount/buildHasChildrenSet(无 memo),整树 O(n²);无虚拟化 |
上移到 Sidebar 用 useMemo;预构建 parentId→children Map;页面多时虚拟列表 |
src/app/api/pages/route.ts:35-46 |
不带参数时返回全部页面含全部 content,可能数十 MB 响应 | 默认强制 lightweight 或分页 |
src/app/api/pages/[id]/route.ts:185-196 |
DELETE 先 findMany 全表再逐条 delete,N 个后代 N+1 条 SQL |
schema 加 onDelete: Cascade,一条语句搞定 |
src/lib/layout.tsx:13-14 |
maximumScale:1, userScalable:false 禁用缩放,可访问性硬伤 |
移除这两项 |
3. 🟠 安全性(中危,非紧急但需修)
| 位置 | 问题 | 建议 |
|---|---|---|
src/app/api/ai/chat/route.ts:5-18,69 |
SSRF 黑名单可绕过:2130706433、0x7f000001、[::ffff:127.0.0.1]、解析到内网的域名、上游 302 均未防 |
fetch 前 DNS 解析校验 IP;redirect: "manual";或改白名单 |
src/lib/rate-limit.ts:48-58 |
getClientIp 无条件信任 x-forwarded-for,直连部署下可换头绕过登录限速 |
仅可信反代时读该头,否则用 socket 地址;叠加全局失败计数 |
src/lib/session.ts:94 + logout/route.ts:7 |
无会话吊销:tokenId 从未使用,改密码/登出后旧 token 直到过期(最长 30 天)仍有效 |
存 sessionVersion/passwordChangedAt,验证时比对 |
src/lib/sanitize-html.ts:132-136 |
服务端降级分支只匹配带引号 on* 属性、javascript: 仅处理 href |
无引号属性也纳入;或干脆服务端拒绝而非降级 |
src/app/api/settings/init/route.ts:27-29 |
providedToken !== initToken 非常量时间比较 |
用 crypto.timingSafeEqual |
4. 🟡 代码质量 / 潜在 Bug(中低危)
| 位置 | 级别 | 问题 | 建议 |
|---|---|---|---|
ai-chat-panel.tsx:104-116 / editor.tsx:1059-1078 |
中 | SSE 流解析无跨 chunk 缓冲,decoder.decode 未传 {stream:true},JSON 帧跨 TCP 边界被截断 → 内容丢失 |
维护跨 chunk 字符串缓冲,只消费完整 data: 行 |
src/lib/search-query.ts:73 + store.ts:160 |
中 | 全文搜索基于 page.content,但 fetchPages 默认 lightweight(content 空),未打开过的页面正文搜索静默漏检 |
搜索改走服务端 API,或明确提示"仅搜索已加载页面" |
src/components/sidebar.tsx:65-73 |
中 | 侧边栏宽度/折叠只写不读 localStorage,初始 state 恒为 280/false,持久化形同虚设 |
`useState(() => Number(localStorage.getItem("sidebar-width")) |
src/lib/store.ts:409-415 |
中 | reorderPages 浅拷贝后 page.order = order 原地突变,破坏 Zustand 不可变约定 |
newPages.map(p => ids.has(p.id) ? {...p, order} : p) |
src/lib/auth.ts:26 |
低 | 哈希 key 长度异常时 timingSafeEqual 抛异常 → 登录 500 而非 401 |
先比长度,不等则 return false |
src/app/login/page.tsx:23,29 |
低 | 初始化接口失败时默认 setIsInitialized(true),DB 未初始化+异常时卡登录页 |
失败显示错误+重试按钮 |
src/components/editor.tsx:775-889 |
低 | 用 window.onpointermove = 赋值会覆盖其他全局处理器 |
改用 addEventListener |
src/app/api/pages/reorder/route.ts:10 |
低 | request.json() 无 .catch,非 JSON body 返回 500 |
补 .catch(() => null) |
src/components/sidebar/tree-view.tsx:268 |
低 | `(isExpanded |
5. 🟡 架构 / Next.js
- 全量 Client 渲染:
src/app/page.tsx、settings/page.tsx等全部"use client",零服务端预取,首屏白屏后客户端再 fetch 全量 pages。App Router 仅当路由用 → 至少把 pages 列表改为 Server Component 预取或 RSC + hydrate。 - 缺失的边界:仅有根级
loading.tsx/error.tsx;缺not-found.tsx,/settings、/login无独立 error boundary。 - proxy 匹配:
src/proxy.ts:31matcher 未排除public/下其他资源(如/file.svg),会被 HMAC 校验拦截并跳登录。 - editor 双 ref hack:
allowOnUpdateRef/suppressNextUpdateRef规避 setContent 回环,脆弱,选项闭包过期会出怪 bug。
6. 🟡 依赖与配置
| 项 | 问题 | 建议 |
|---|---|---|
| 未使用依赖 | 源码零引用:framer-motion、@radix-ui/react-tabs、@radix-ui/react-slot、@tiptap/extension-bubble-menu、@tiptap/extension-floating-menu、@tiptap/extension-gapcursor、@tiptap/extension-link(StarterKit 已含)等 |
npx depcheck 后移除 |
| 版本错位 | next 16.1.1 vs eslint-config-next ^16.1.6;marked v17 却装 @types/marked ^5(旧类型包可能遮蔽正确类型);lowlight ^2.9.0 落后 TipTap v3 推荐的 v3 |
对齐版本,删除 @types/marked |
| Prisma 落后 | @prisma/client 5.10.2 锁定 2024-02 版,落后主线 6.x 两代 |
计划升级 Prisma 6 |
| 命名规范 | 包名 note-ai 与目录 wojide 不一致;version 1.0.0304 用日期当补丁号,非 semver |
统一命名 |
| ESLint 豁免 | eslint.config.mjs:9-13 对 editor 目录关闭 no-explicit-any/ban-ts-comment,给劣化开口子 |
收窄或移除豁免 |
7. ✅ Top 10 优先修复清单
- [高|安全]
prisma/dev.db从 git 移除并清理历史(数据 + 密码哈希泄露)。 - [高|安全] 替换占位
SESSION_SECRET,session.ts启动拒绝占位值。 - [高|性能] 关闭
allowBase64,改图片上传接口。 - [中|安全]
session.ts增加会话吊销(改密码/登出后旧 token 失效)。 - [中|Bug] SSE 解析加跨 chunk 缓冲(
ai-chat-panel.tsx:104/editor.tsx:1059)。 - [中|Bug] 修复 lightweight 模式正文搜索漏检(改服务端搜索)。
- [中|安全]
ai/chatSSRF 改 IP 校验 + 禁跟随重定向;rate-limit停止无条件信任x-forwarded-for。 - [中|DB]
schema.prismaPage 自关联加onDelete: Cascade,简化 DELETE 路由。 - [中|Bug]
sidebar.tsx:65补 localStorage 读取恢复;store.ts:409修复 reorder 状态突变。 - [中|性能]
page.tsx:87修复每次按键解析 localStorage 历史;tree-view.tsx:284树计算加 memo;editor.tsx:28lowlight 按需加载。
8. 后续建议
- 先做第 1、2 项(仓库卫生):投入小、风险降最大,且涉及 git 历史改写,越早处理越干净。
- 其次第 3 项(图片上传):是长期性能与存储健康的根基,但改动面较大,建议单独排期。
- 其余按体验痛点优先级逐步推进,不影响线上安全。
需要我从哪一项开始动手?按你的协作习惯,我会先给方案、你拍板后再改代码。