Files
NoteAI/docs/code-review-2026-07-31.md
T

9.9 KiB
Raw Blame History

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、登录有限速。真正的短板不在业务逻辑,而在两处

  1. 仓库卫生(数据库入库 + 占位密钥)——会造成真实的数据泄露与越权风险;
  2. 内容膨胀路径(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 到 GitHuborigin 已存在),清历史会影响协作者,需先沟通或确认是私有仓。

1.2 会话密钥为占位符 + 部署默认弱密码

  • 证据README.md:11.envSESSION_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 localHistoryuseMemo 依赖 [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 全表再逐条 deleteN 个后代 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 黑名单可绕过:21307064330x7f000001[::ffff:127.0.0.1]、解析到内网的域名、上游 302 均未防 fetch 前 DNS 解析校验 IPredirect: "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 默认 lightweightcontent 空),未打开过的页面正文搜索静默漏检 搜索改走服务端 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.tsxsettings/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:31 matcher 未排除 public/ 下其他资源(如 /file.svg),会被 HMAC 校验拦截并跳登录。
  • editor 双 ref hackallowOnUpdateRef/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-linkStarterKit 已含)等 npx depcheck 后移除
版本错位 next 16.1.1 vs eslint-config-next ^16.1.6marked 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 优先修复清单

  1. [高|安全] prisma/dev.db 从 git 移除并清理历史(数据 + 密码哈希泄露)。
  2. [高|安全] 替换占位 SESSION_SECRETsession.ts 启动拒绝占位值。
  3. [高|性能] 关闭 allowBase64,改图片上传接口。
  4. [中|安全] session.ts 增加会话吊销(改密码/登出后旧 token 失效)。
  5. [中|Bug] SSE 解析加跨 chunk 缓冲(ai-chat-panel.tsx:104 / editor.tsx:1059)。
  6. [中|Bug] 修复 lightweight 模式正文搜索漏检(改服务端搜索)。
  7. [中|安全] ai/chat SSRF 改 IP 校验 + 禁跟随重定向;rate-limit 停止无条件信任 x-forwarded-for
  8. [中|DB] schema.prisma Page 自关联加 onDelete: Cascade,简化 DELETE 路由。
  9. [中|Bug] sidebar.tsx:65 补 localStorage 读取恢复;store.ts:409 修复 reorder 状态突变。
  10. [中|性能] page.tsx:87 修复每次按键解析 localStorage 历史;tree-view.tsx:284 树计算加 memoeditor.tsx:28 lowlight 按需加载。

8. 后续建议

  • 先做第 1、2 项(仓库卫生):投入小、风险降最大,且涉及 git 历史改写,越早处理越干净。
  • 其次第 3 项(图片上传):是长期性能与存储健康的根基,但改动面较大,建议单独排期。
  • 其余按体验痛点优先级逐步推进,不影响线上安全。

需要我从哪一项开始动手?按你的协作习惯,我会先给方案、你拍板后再改代码。