Files
pure-note/docs/design.md
T
wangairnan 9b0aff6f9f docs: 新增编辑器即时渲染方案、实施与 P3 加固记录
- editor-live-rendering.md(新):Typora 式单编辑面方案——路线选型、装饰层分层设计、
  widget 规格、安全与风险、测试方案、P0–P3 实施拆解;§12 为实施与加固记录:
  两个实测缺陷的根因与修复、3000 行性能实测数据、§12.6 真机输入法人工门槛(阻塞发布)、
  §12.7 编辑区高度
- design.md §8.2:编辑器段落由「分屏实时预览」改为即时渲染表述,链接本方案并标注发布门槛
- decisions.md:D37–D52(装饰器路线、单一 ViewPlugin、行级 reveal、长文档补解析、
  选区端点行采集、reveal 二分、@codemirror/commands 仅为 devDependency、人工门槛、编辑区高度)
- acceptance.md:新增 M5 里程碑与 M5-G 人工门槛行,补 v1.1/v1.2 浏览器复走记录与用例数

文档先行,代码实现见后续提交。
2026-09-11 01:02:36 +08:00

50 KiB
Raw Permalink Blame History

Pure Note — 极简高安全私人笔记 + 博客 设计方案

版本:v1.1(第 1 轮审评修订版) 日期:2026-09-08 状态:已按 docs/review-round1.md 全部 24 项发现修订(P0×4 / P1×11 / P2×9),可进入 M0 开发 调研数据来源:pkg.go.dev / npm registry / GitHub API 在线核实(2026-09-07)


1. 项目概述与目标

1.1 定位

一款部署在公网服务器上的私人笔记 + 博客程序,B/S 架构:

  • 给自己用:Markdown 笔记,支持内嵌图片,内容只分「公开 / 私有」两种;
  • 给外部看:公开笔记自动构成一个博客站点,可匿名浏览;
  • 运营极简:最终交付 = 一个二进制程序 + 一个数据文件(目录),无数据库服务、无缓存服务、无 Node 运行时、无外部依赖。

1.2 核心原则

原则 落实手段
安全性极高 全链路默认安全:Argon2id 口令哈希、双层 Markdown 清洗防 XSS、严格 CSP、CSRF 双保险、上传魔数校验、最小权限模型,详见 §9
架构极简 纯 Go 标准库 HTTP + 5 个直接依赖;SQLite 单文件;前端 SPA 用 go:embed 打进二进制;无 ORM、无框架、无微服务
单二进制交付 CGO 关停、纯 Go SQLite 驱动(modernc.org/sqlite)、web/dist 经 Makefile 拷贝到可嵌入目录(§8.3),交叉编译一键出包
权限体系简单健壮 笔记仅两种状态(public/private);唯一管理员账号持有编辑权;匿名用户只见 public;服务端统一可见性过滤,不依赖前端隐藏
数据可恢复 删除 = 回收站软删除(30 天);每日在线备份;升级前强制备份

1.3 目标 / 非目标

目标(IN SCOPE)

  • Markdown 笔记 CRUD,内嵌图片(粘贴 / 拖拽上传),公开/私有切换;
  • 博客展示:首页列表、笔记详情、标签归档、RSS、sitemap;
  • 单人管理员,登录后编辑;匿名只读公开内容;管理员可在博客皮肤预览私有笔记;
  • 回收站:删除走软删除,30 天内可恢复,gc 到期清除;
  • 单机部署:systemd / 反代(Caddy 自动 TLS)。

非目标(OUT OF SCOPE,预留演进位)

  • 多用户、多人协作、评论系统(可后续加,不影响本设计);
  • 全文搜索(SQLite FTS5 可平滑引入,预留);
  • 版本历史/编辑快照(回收站只保证「误删可恢复」,不做多版本);
  • 多实例集群、CDN 同步(单实例架构,见 §11 边界说明);
  • 多端实时同步/移动客户端。

2. 需求分析

2.1 功能需求

编号 模块 需求
F1 笔记管理 创建/编辑 Markdown 笔记;标题、slug、标签、置顶;草稿即「私有」
F2 权限 笔记只有 public / private 两态;切换即时生效;私有内容任何匿名途径(含 API、标签、RSS、图片直链、HTML meta)不可见
F3 图片 编辑器内粘贴/拖拽上传(≤5MB);Markdown 中引用;图片可见性 = 引用它的笔记可见性的并集;去重存储
F4 博客 匿名访问:首页(公开列表+分页+标签)、详情页、标签页;RSS Feed;sitemap.xml;SEO 基础(仅公开笔记注入 title/description/og meta)
F5 认证 单管理员密码登录/登出/改密;会话失效/续期;登录与改密防爆破
F6 站点设置 站点标题、副标题、每页条数(10/20/30/50)、站点 Logo(白名单字段,绝不含口令哈希)
F7 回收站 删除 = 软删除进入回收站;30 天内可恢复;可一键清空;gc 到期物理清除
F8 运维 单二进制启动;--data-dir 指定数据目录;在线备份子命令;gc 子命令(默认 dry-run);版本号

2.2 非功能需求

类别 要求
安全 OWASP 常见风险全覆盖(XSS/CSRF/注入/会话/上传/点击劫持/MIME 嗅探),上线前过 §9.3 检查单,其中可见性矩阵长期由自动化测试守护(§13)
性能 个人规模(<10k 笔记、<100k 图片):P95 页面响应 < 300ms;SQLite 游刃有余
可靠性 崩溃不损坏数据(WAL);每日自动备份;升级可回滚;优雅停机不丢在途写入
极简 直接依赖 ≤6(Go 侧 5 个);无 Node 服务端运行时;运维只需 systemd 单元 ×2(服务 + 每日 gc/备份 timer)+ 反代配置
可维护 前后端分离清晰;API 稳定;Markdown 渲染为可替换组件;核心不变量有自动化回归

3. 技术选型调研结论

以下版本号均为 2026-09-07 经 pkg.go.dev、npm registry、GitHub API 在线核实。

3.1 后端选型

组件 选择 版本 理由 备选(被否决原因)
语言 Go 1.27.1 单一静态二进制、交叉编译、内存安全、并发模型适合 HTTP 服务 —
HTTP 路由 标准库 net/http + 1.22 增强 ServeMux — 方法与通配符路由足够支撑小型 JSON API,零框架依赖 chi(需要中间件生态时再引入)、gin/echo(非必需)
SQLite 驱动 modernc.org/sqlite v1.58.0 纯 Go、无 CGO,内嵌 SQLite 3.53.4,跨平台静态编译,3500+ 依赖方生产可用 mattn/go-sqlite3(需要 CGO_ENABLED=1 + gcc,破坏单二进制目标)
Markdown 解析(服务端) goldmark v1.8.6 CommonMark 兼容、默认转义原始 HTML 与危险 URL(XSS 安全默认)、GFM/脚注/排版扩展齐全、v1 线成熟稳定 gomarkdown(仍在 v0)、blackfriday(2020 年停更);goldmark v2.0.1 刚发布,稳定后评估升级
HTML 清洗(服务端) bluemonday v1.0.27 白名单式清洗标准方案;与 goldmark 双保险 —
口令哈希 golang.org/x/crypto/argon2 v0.56.0 Argon2id,OWASP 现行参数 m=19456(19MiB)、t=2、p=1;按 PHC 字符串 编码存储(§7.3) bcrypt(同样可接受,但 Argon2 为当前首选)
会话 自建 DB 会话表(~80 行) — crypto/rand 256bit token + SHA-256 摘要入库,可撤销、可过期清理、零额外依赖;单管理员场景完全可控 gorilla/sessions v1.4.0(已恢复维护)、scs v2.9.0(轻量备选,含 SQLite store)
限流 golang.org/x/time/rate v0.15.0 令牌桶,登录/改密防爆破够用(per-IP + per-账号双维度);桶有数量上限与 TTL 清理(§7.2) Redis 方案(多实例才需要)
静态资源 go:embed + http.FS — embed 只能引用本包目录树内文件:产物经 Makefile 拷贝到 internal/webui/dist 再嵌入(§8.3);自写 SPA fallback handler 外置静态目录 + nginx(违背单二进制)
数据访问 标准库 database/sql 参数化查询 — 无 ORM,6 张表手写 SQL 最透明 GORM 等(依赖重、隐藏细节)

3.2 前端选型

组件 选择 版本 理由 备选
框架 React 19.2.8 生态最大、md 渲染/编辑器组件最丰富 —
语言 TypeScript 7.x(不动则 5.9) 原生编译器版已可用;若第三方库兼容有问题回退 5.9 —
构建 Vite 8.2.2 事实标准;部署在域名根,base 用默认值 '/'(相对 base 与 SPA 深链冲突,见 §8.3 与审评报告 P0-1) —
样式/组件 Tailwind CSS 4 + Ant Design 6 4.3.3 / 6.6.3 Tailwind 负责页面排版与设计 token;交互组件统一用 antd(Button/Input/Table/Modal/Dropdown/Tooltip/notification 等),经 ConfigProvider 自定义主题对齐黑白灰品牌 token 并随三态主题切换明暗算法;v6 原生支持 React 19 shadcn/ui + Radix(曾采用,组件需自维护,替换为 antd 降低长期维护成本)、Chakra 3、HeroUI
路由 react-router 8.3.1 library mode 成熟,博客+后台两个区足够 TanStack Router 1.x(类型安全更强,但本项目路由简单)
数据请求 TanStack Query 5.102.8 十几 kB 换来统一 loading/缓存/重试,值得 裸 fetch + 自写 hook
Markdown 渲染 react-markdown + remark-gfm + rehype-sanitize + rehype-highlight 10.1.0 / 4.0.1 / 6.0.0 / 7.0.2 组合标准;高亮选 rehype-highlight(纯 class 输出、零内联样式)而非 shiki(inline style),与严格 CSP 兼容 shiki 4.4.3(准确度最高但产 inline style)、prism-react-renderer(近停更,不推荐)
Markdown 编辑器 CodeMirror 6(@uiw/react-codemirror + @codemirror/lang-markdown) 4.25.11 / 6.5.2 即时渲染(Typora 式:隐藏语法标记 + 光标处展开源码,见 editor-live-rendering.md),零魔法、可预期;图片粘贴自写 handler(≤50 行) Tiptap 3.31(体验派,WYSIWYG+成熟图片上传,文档全);MDXEditor(重);ByteMD(字节已弃坑)、Vditor 4(体积大、心智旧)
状态管理 React Query + Context(不引入 zustand) — 会话态 + 服务端缓存已覆盖;确需跨页 UI 态再加 zustand(≈1kB) redux(不必要)
图标 lucide-react 1.42.0 活跃、风格统一(antd 组件内亦复用) —

3.3 关键决策记录(ADR 摘要)

  1. 图片存 SQLite BLOB 而不是文件系统 —— 满足「只有一个数据文件」字面含义:备份=拷一个文件;事务一致性;杜绝路径穿越整类风险(文件永不落盘执行)。个人博客场景(单图 ≤5MB、总量 2GB 内)SQLite 毫无压力(理论上限 281TB)。>10MB 的图片一律拒绝引导压缩。
  2. SPA 而非 SSR —— 管理端+浏览端共用 React 技术栈,极简;SEO 用「服务端渲染 index.html 元信息 + RSS + sitemap」弥补,元信息注入与内容 API 同一条可见性规则(§8.3)。「公开页 Go 模板 SSR」留作 v2 备选(见 §14)。
  3. HTTP 用标准库 —— Go 1.22+ ServeMux 已具备方法路由与 {param} 通配符;本项目 API 约 20 个路由,框架收益趋近于零,依赖面以「less is more」取胜。
  4. 会话自建而非引库 —— 单管理员、单表、几十行代码,审计面最小;需求升级时切 scs 的成本极低。
  5. 图片可见性用「引用表」+ 并集语义 —— 图片通过 image_refs 关联笔记,可见性 = 所有引用笔记可见性的并集(任一 public 引用即整体公开),并显式声明「已公开过的图片视为已分发,转私无法撤回已泄露副本」(§6.2、§9.1)。
  6. 编辑器选 CodeMirror 6 路线 —— 与「极简健壮」一致:所见即所写,不需要维护富文本中间态;组件边界(MarkdownViewer 独立)保证日后可平替 Tiptap。
  7. 回收站用软删除(deleted_at 列)而非快照表 —— 删除不改动 image_refs,可见性过滤统一加 deleted_at IS NULL 一个条件即可覆盖全部出口;恢复 = 清空该列,图片引用天然保全;gc 到期硬删时引用级联(§6.2)。

4. 总体架构

4.1 架构图

                    公网
                     │  HTTPS (Caddy 自动 TLS, 反代, 追加密名 XFF)
                     ▼
        ┌──────────────────────────┐
        │   pn 单一二进制 (Go)       │
        │                          │
        │  net/http + ServeMux     │
        │  ├─ 公共 API /api/*      │──┐
        │  ├─ 管理 API /api/admin/*│  │ (匿名 or 会话)
        │  ├─ /feed.xml /sitemap   │  │
        │  └─ SPA (go:embed       │  │
        │        internal/webui/dist) │
        │                          │  │
        │  中间件链:                 │  │
        │  SecurityHeaders → 日志 → 限流 →                     │
        │  Origin 校验 → MaxBytes → Auth → CSRF → Handler    │  │
        └────────────┬─────────────┘  │
                     │  database/sql (SetMaxOpenConns=1)
                     ▼
        ┌──────────────────────────┐
        │  data/pn.db (SQLite)      │
        │  notes(含 deleted_at) /     │
        │  images(BLOB) / image_refs /│
        │  sessions / settings        │
        └──────────────────────────┘

   浏览器: React SPA (公开浏览 + 管理后台)

4.2 数据流

公开读路径:访客浏览器 → GET /api/notes/{slug} → 服务端以 status='public' AND deleted_at IS NULL(或有效管理员会话)过滤 → 返回原始 Markdown + 元信息 → 前端 react-markdown 管线(GFM → rehype-sanitize 清洗 → highlight class 高亮)渲染。所有内容经 CSP 约束。

管理写路径:管理员登录(Argon2id 校验、限流、Origin 校验)→ 会话 Cookie(__Host- 前缀)+ CSRF Token(内存持有,刷新经 /api/me 重取)→ 编辑/上传(multipart 魔数校验 → BLOB 入库 + sha256 去重)→ 保存笔记时事务内重建 image_refs 引用。

图片读取路径:GET /api/images/{id} → 查 image_refs 关联笔记的可见性并集(任一 public 且未删除即可匿名访问,否则要求管理员会话)→ 按可见性返回不同缓存头(公开 immutable / 非公开 no-store)→ 未授权与不存在统一 404。


5. 目录结构(Monorepo)

pure-note/
├── go.mod / go.sum
├── Makefile
├── .gitignore                       # data/ web/node_modules/ web/dist/ internal/webui/dist/(仅保留 .gitkeep)
├── README.md
├── docs/
│   ├── design.md                    # 本文档(v1.1)
│   └── review-round1.md             # 第 1 轮审评报告(修订依据,留档)
├── cmd/
│   └── pn/
│       └── main.go                  # 入口 + 命令行(start/init/passwd/backup/gc/version)
├── internal/
│   ├── config/                      # 环境变量/flag 解析
│   ├── store/                       # SQLite 打开、迁移(user_version 上界校验)、DAO
│   ├── httpapi/                     # 路由注册、handler
│   │   ├── public.go / admin.go / auth.go / image.go / feed.go
│   ├── middleware/                  # securityHeaders / rateLimit / originCheck / auth / csrf(自写)
│   ├── auth/                        # argon2id(PHC)、会话 token、密码校验
│   ├── markdown/                    # goldmark 渲染(RSS/meta 用)+ bluemonday 清洗
│   └── webui/                       # go:embed dist + SPA fallback + index.html 元信息注入
│       └── dist/                    # 【生成目录】Makefile 自 web/dist 拷贝,.gitignore(仓库保留 .gitkeep 供 M0 编译)
├── web/                             # React 前端(独立 Vite 工程)
│   ├── package.json / vite.config.ts
│   ├── index.html
│   └── src/
│       ├── main.tsx / App.tsx / router.tsx
│       ├── lib/                     # api client、auth、csrf、格式化
│       ├── theme/                   # antd ConfigProvider 主题(品牌 token + 明暗算法)
│       ├── components/              # 业务组件
│       │   ├── markdown/            # MarkdownViewer(渲染管线,可替换)
│       │   └── editor/              # Editor(CodeMirror)+ ImagePaste + Toolbar
│       ├── hooks/                   # use-toast(antd notification 封装)
│       ├── features/                # notes / admin 业务 hooks
│       └── pages/                   # Home / Note / Tags / AdminLogin / AdminList / AdminTrash / AdminEdit
└── deploy/
    ├── pn.service                   # systemd 服务单元
    ├── pn-maint.timer               # 每日 gc + 备份 timer
    └── Caddyfile                    # 反代示例

6. 数据模型(SQLite)

6.1 Schema(DDL)

-- 连接串(DSN)统一注入以下 pragma:
-- file:data/pn.db?_pragma=journal_mode(WAL)
--   &_pragma=busy_timeout(5000)
--   &_pragma=foreign_keys(1)
--   &_pragma=synchronous(NORMAL)

CREATE TABLE IF NOT EXISTS notes (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    slug       TEXT    NOT NULL UNIQUE,              -- URL 标识;首次保存定稿(§8.2)
    title      TEXT    NOT NULL,
    summary    TEXT    NOT NULL DEFAULT '',          -- 列表页摘要,可空则截取正文
    content    TEXT    NOT NULL DEFAULT '',          -- Markdown 原文
    status     TEXT    NOT NULL DEFAULT 'private'
               CHECK (status IN ('public','private')),
    tags       TEXT    NOT NULL DEFAULT '[]',        -- JSON 字符串数组
    pinned     INTEGER NOT NULL DEFAULT 0,
    deleted_at INTEGER,                              -- NULL=正常;非空=回收站(软删除)
    created_at INTEGER NOT NULL,                     -- Unix 秒
    updated_at INTEGER NOT NULL,
    published_at INTEGER                             -- 发布日期(可自选);v2 迁移回填为 created_at
);
CREATE INDEX IF NOT EXISTS idx_notes_public
    ON notes (status, pinned, updated_at DESC);
CREATE INDEX IF NOT EXISTS idx_notes_deleted ON notes (deleted_at);

CREATE TABLE IF NOT EXISTS images (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    sha256     TEXT    NOT NULL UNIQUE,              -- 内容哈希:去重 + ETag
    mime       TEXT    NOT NULL,
    size       INTEGER NOT NULL,
    data       BLOB    NOT NULL,
    created_at INTEGER NOT NULL
);

CREATE TABLE IF NOT EXISTS image_refs (              -- 笔记 ↔ 图片多对多
    image_id INTEGER NOT NULL REFERENCES images(id) ON DELETE CASCADE,
    note_id  INTEGER NOT NULL REFERENCES notes(id)  ON DELETE CASCADE,
    PRIMARY KEY (image_id, note_id)
);

CREATE TABLE IF NOT EXISTS sessions (
    token_hash TEXT PRIMARY KEY,                     -- SHA-256(token 明文),库里不存明文
    csrf_token TEXT NOT NULL,
    created_at INTEGER NOT NULL,
    expires_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions (expires_at);

CREATE TABLE IF NOT EXISTS settings (
    key   TEXT PRIMARY KEY,
    value TEXT NOT NULL
);
-- 键白名单(§7.1 SettingsDTO 同源):
--   admin_password_hash : PHC 串 $argon2id$v=19$m=19456,t=2,p=1$<b64salt>$<b64hash>
--   site_title / site_desc / page_size / beian_no / site_logo(站内绝对路径)
-- admin_password_hash 永不进入任何 API 响应、不可经 settings 接口写入(§9.3)

6.2 关键策略

事项 策略
图片去重 上传按 sha256 查重,同图复用同一行
图片可见性(并集语义) 匿名可读 ⇔ ∃ ref → note.status='public' AND note.deleted_at IS NULL;否则仅管理员。可见性取所有引用笔记的并集:同一字节的图一旦被任何公开笔记引用即整体公开;「已公开过的图片视为已分发,转私/删除无法撤回已被缓存、外链的副本」——按已知边界接受(§9.1-T10)
引用维护 保存笔记时在同一事务内:删除该笔记全部 refs → 用正则扫描 content 中的 /api/images/(\d+) → 重建 refs
回收站(软删除) DELETE 仅置 deleted_at;refs 不动(恢复时图片天然保全);所有可见性过滤统一追加 deleted_at IS NULL;gc 将 deleted_at < now−30天 物理删除(级联 refs);回收期内 slug 仍被占用(保身份,见 §14)
会话 随机 256bit token(crypto/rand)→ Cookie 存 token 明文,库存 SHA-256;登录成功即重建行(防会话固定);轮换时 csrf_token 保持不变;启动时 + 每小时清理过期行
迁移 PRAGMA user_version 版本化,顺序执行内嵌迁移脚本;仅追加式(禁止删列/重命名/改类型);启动时校验 user_version ≤ 代码支持的最高版本,超出拒绝启动(--allow-newer 显式放行,§10.4)
并发 db.SetMaxOpenConns(1) — 单写者串行化,WAL 下个人规模足够;规模上来后按「多读者连接池」扩展

6.3 数据清理(pn gc)

默认 --dry-run 只输出清理计划,--commit 才执行;systemd timer 每日调用一次(§10.3):

  1. 物理删除 deleted_at < now−30天 的笔记(refs 级联);
  2. 删除图片:0 引用 且 created_at < now−7天(宽限期防止误删编辑中刚上传的图);
  3. 删除过期会话。

7. 后端设计

7.1 HTTP API 一览

统一约定:JSON {"data": ..., "error": {"code": "...", "message": "..."}};状态码语义化(400/401/403/404/409/413/429/500);分页参数 page ∈ [1,10⁴]、page_size ∈ [1,100](默认 10),越界返回 400。

公共(匿名 + 管理员)

方法 路径 说明
GET /api/health 健康检查
GET /api/me 匿名 {authenticated:false};已认证 {authenticated:true, csrf_token}(刷新后 CSRF token 重取点,§7.3)
GET /api/notes 公开笔记列表(status='public' AND deleted_at IS NULL),?page=&page_size=&tag=,返回元信息(不含全文)
GET /api/notes/{slug} 笔记详情:public 或有效管理员会话可读(响应含 status 字段供前端显示「私有预览」);其余统一 404
GET /api/tags 标签聚合(仅统计公开且未删除的笔记)
GET /api/images/{id} 图片(并集可见性,§6.2);未授权与不存在统一 404(防 ID 枚举);缓存头按可见性分流(§7.4)
GET /feed.xml RSS 2.0(仅公开;服务端 goldmark+bluemonday 渲染)
GET /sitemap.xml 公开笔记 URL 列表
GET /robots.txt 允许全部 + sitemap 指向
GET / 及所有非 API 路径 SPA fallback → 内嵌 index.html(仅对可见笔记注入 title/desc/og meta,§8.3)

管理(会话 + CSRF)

方法 路径 说明
POST /api/auth/login {password};Origin 校验 + 限流 + 常量时间比较;返回 CSRF token
POST /api/auth/logout 删除会话行
GET /api/admin/notes 全部正常笔记(含私有,不含回收站)
POST /api/admin/notes 新建;自动生成唯一 slug(自解冲突,§8.2)
GET /api/admin/notes/{id} 单篇(含私有)
PUT /api/admin/notes/{id} 更新(事务内重建 image_refs);slug 冲突返回 409 + 字段级错误
DELETE /api/admin/notes/{id} 软删除:置 deleted_at 进入回收站
GET /api/admin/trash 回收站列表
DELETE /api/admin/trash 清空回收站:物理删除全部软删除笔记(refs 级联)
POST /api/admin/trash/{id}/restore 恢复:清空 deleted_at(slug 仍被自身占用,无冲突;外部占用则在 30 天内不可能,见 §6.2)
POST /api/admin/images multipart 上传 → {id, url};≤5MB
GET /api/admin/images?orphan=1 0 引用图片清单(删除前检视,实际清除由 gc 执行)
GET/PUT /api/admin/settings SettingsDTO 白名单(site_title/site_desc/page_size/beian_no/site_logo;page_size 仅 10/20/30/50),读永不序列化 admin_password_hash、写只收白名单键
POST /api/admin/password {old_password, new_password};校验旧密码(常量时间)、新密码 ≥12 字符;复用登录限流

7.2 中间件链与服务器参数

SecurityHeaders(含 HSTS,§9.2)
→ slog 请求日志(仅错误 + 管理操作,登录失败记 IP 与桶计数)
→ 全局限流(宽松令牌桶;per-IP 桶上限 4096 条 + 10 分钟 TTL 逐出,防伪造 IP 撑爆内存)
→ Origin/Referer→Host 校验(**所有** 非 GET/HEAD/OPTIONS 请求,含 /api/auth/*,§9.1-T2)
→ 路由级 MaxBytesReader(auth/settings ≤64KB;notes JSON ≤1MB;multipart ≤6MB 含边界开销)
→ 路由
   ├─ 公共路由: handler 内 status='public' AND deleted_at IS NULL 过滤
   ├─ /api/auth/login: 严格限流(per-IP 10次/5分钟 + per-账号 5次/10分钟,429 + Retry-After)
   └─ /api/admin/*: 会话校验(401) → CSRF 头校验(403) → handler;响应统一 Cache-Control: no-store

http.Server:ReadHeaderTimeout: 5s, ReadTimeout: 120s, WriteTimeout: 120s, IdleTimeout: 120s(slowloris 与超大 body 防线)。优雅停机:SIGTERM/SIGINT → 停止接受新连接 → Shutdown(ctx 10s) 排空在途请求 → 关闭 SQLite 连接(§10.5)。

7.3 认证与会话

  1. 初始化:pn init(交互式设密码,或环境变量 PN_ADMIN_PASSWORD 非交互)→ Argon2id(m=19456, t=2, p=1, salt=16B 随机, keyLen=32B) 以 PHC 串 $argon2id$v=19$m=19456,t=2,p=1$<b64salt>$<b64hash> 存库——参数随哈希走,未来调参可继续校验旧口令;
  2. 登录:Origin/Referer→Host 校验通过后,per-IP 10 次/5 分钟 + 账号维度 5 次/10 分钟令牌桶(只计失败尝试);校验用 subtle.ConstantTimeCompare;失败统一返回 401「用户名或密码错误」不泄露差异;
  3. 会话 Cookie:__Host-pn_session = 256bit 随机 token;属性 Secure; HttpOnly; SameSite=Lax; Path=/; Max-Age=7d(__Host- 前缀自带 Secure + 根路径约束);
  4. CSRF:会话行内存随机 csrf_token,登录响应下发、前端仅存内存并附到所有 /api/admin/* 变更请求的 X-CSRF-Token 头;刷新页面后经 GET /api/me 重取;明确禁止任何 localStorage 持久化;服务端同时校验 Origin/Referer 与 Host 匹配;
  5. 轮换:活跃访问距过期 < 3 天时重建会话行、旋转 Cookie token,csrf_token 保持不变(前端无感);
  6. 登出:删除会话行;
  7. 改密:POST /api/admin/password 校验旧密码后写新 PHC 哈希;改密不失效当前会话(可选:改密后删除其他会话——单管理员场景仅本会话,不强制);
  8. 口令重置(CLI):pn passwd 免旧口令直接覆盖哈希并吊销全部会话——前提是具备服务器访问权限(读写数据目录即可改库,CLI 提供规范入口优于手工改库);口令来源与 init 一致,避免出现在 argv;
  9. 开发模式:--dev 允许非 Secure Cookie(否则 __Host- 在 http://局域网IP 下无法登录);强制约束:仅当监听地址为 loopback 时允许启动,防止误部署到生产。

7.4 图片上传与访问

  1. 上传:Content-Type 白名单 + 魔数字节校验(PNG/JPEG/WebP/GIF 签名,不信任客户端声明;SVG 一律拒绝——携带脚本风险)→ ≤5MB → 读入内存计算 sha256 → INSERT OR IGNORE 去重 → 返回 URL;图片永不落盘,杜绝 WebShell 执行面;
  2. 响应头按可见性分流:
    • 公开图:Content-Type(入库 mime)、X-Content-Type-Options: nosniff、ETag: "<sha256>"、Cache-Control: public, max-age=31536000, immutable;
    • 非公开图(仅私有笔记引用/孤儿/仅管理员会话):Cache-Control: private, no-store,每次请求重新执行可见性判定;
    • 未授权与不存在统一 404(防自增 ID 枚举);
  3. 可选增强(P2):EXIF/元数据剥离。

7.5 Markdown 服务端职责

  • RSS / meta 渲染:goldmark(GFM 扩展、html.WithUnsafe=false 保持转义)→ bluemonday 白名单清洗 → 摘要截断;
  • 不把 HTML 存库:库中始终只存 Markdown 原文,渲染确定性交给前端管线,安全策略(清洗规则)升级时新旧笔记全部收益。

7.6 配置与命令行

子命令 / 开关 说明
pn start 启动;--addr :8080、--data-dir(默认 ./data)、--log-level、`--log-format text
pn init 首次初始化:设口令(Argon2id+PHC)、站点标题;并写入一篇 Markdown 语法示例文档(公开,slug welcome)及程序生成的示例图片
pn passwd 重设管理员口令(覆盖旧哈希)并吊销全部会话;口令来源同 init(环境变量 PN_ADMIN_PASSWORD 或交互输入,不经 argv 避免 ps 泄露)。CLI 可达即具备服务器权限,允许直接重设
pn backup [path] 在线备份:VACUUM INTO(一致快照,不停服);默认输出 0600 权限
pn gc 回收站过期清除 + 孤儿图片清除 + 会话清理;默认 --dry-run,--commit 才执行(§6.3)
pn version 版本号(build 时注入 commit/时间);亦支持 -v / --version

8. 前端设计

8.1 页面与路由

路由 页面 说明
/ 博客首页 公开笔记列表(置顶优先、分页)、标签云、站点标题
/notes/:slug 笔记详情 Markdown 渲染、面包屑、上一篇/下一篇;响应 status=private 且为管理员时显示「私有预览」横幅
/tags/:tag 标签页 该标签下公开笔记
/admin/login 登录页 口令登录、防爆破提示
/admin 管理列表 全部笔记(公/私标签可见)、状态开关、新建、删除(入回收站)
/admin/trash 回收站 已删列表、恢复、一键清空
/admin/notes/new /admin/notes/:id/edit 编辑器 CodeMirror 即时渲染(Typora 式,见 editor-live-rendering.md)、元信息侧栏(标题/slug/标签选择器/发布日期/摘要/公开开关/置顶)、图片粘贴上传
* 404 统一兜底

8.2 核心技术要点

Markdown 渲染管线(浏览态)

Markdown 原文
 → react-markdown 10 (remark-gfm: 表格/任务列表/删除线/自动链接)
 → rehype-sanitize 6(白名单 schema:禁 script/iframe/事件属性/未知协议/style 属性,
    允许任务列表 checkbox 所需 input[type=checkbox disabled checked],
    加 class 白名单供高亮使用,链接强制 target=_blank + rel="nofollow noopener noreferrer")
 → rehype-highlight 7(纯 class 高亮,主题 CSS 静态打包,零内联样式)
 → 渲染(@tailwindcss/typography prose 排版 + 自定义 CodeBlock 组件带复制按钮)

两处防线:链路内的 rehype-sanitize,加上服务端 goldmark 转义与 CSP script-src 'self',即使清洗层被绕过也无法执行脚本。

编辑器(编辑态)

即时渲染(Typora 式)改造方案见 docs/editor-live-rendering.md(v1.2,P0–P3 已实施并完成实机验证与性能实测);本段原「分屏实时预览」表述已被该方案取代,数据层与浏览态渲染管线不变。 发布门槛:该方案 §12.6 的真机输入法回归(系统拼音 / 搜狗)是阻塞发布的人工验收项,当前未执行。

  • CodeMirror 6 + @codemirror/lang-markdown,即时渲染:语法标记默认隐藏、光标所在行展开为源码(web/src/editor/ 装饰层); 行号与活动行高亮仅在源码模式下显示,「源码模式」按钮即逃生舱(方案 §4.7); 正文排版与公开页共用 --md-* 契约变量(index.css),编辑态视觉 = 阅读态视觉; 工具栏提供加粗/斜体/链接/代码/表格等 Markdown 辅助插入;
  • 图片粘贴/拖拽:自写 paste/drop handler(≤50 行)→ POST /api/admin/images → 光标处插入 ![描述](/api/images/{id});
  • slug 策略:首次保存时由标题自动生成——中文标题退化为 post-<YYYYMMDD>,与现有记录(含回收站)冲突时追加 -2/-3 直至唯一;首次保存即定稿,此后除非用户手动修改否则永不变化;用户手改与他人冲突 → 服务端 409 + 字段级错误提示,前端就地高亮;
  • 自动保存:防抖 2s;大改动前本地兜底(失败时保留编辑态不丢内容)。

数据与状态

  • TanStack Query 管理全部服务端状态(列表/详情缓存 key 按 status 上下文隔离);
  • 会话状态 useAuth(Context + /api/me);CSRF token 仅存内存,刷新后经 /api/me 重取,禁止 localStorage;
  • 登录态失效(401)统一跳转 /admin/login。

暗色主题

Tailwind v4 @custom-variant dark + 根元素 class 切换 + localStorage 持久化 + 跟随系统选项。

8.3 构建与嵌入约定

  1. 产物位置与 embed 机制(审评 P0-2 修正):go:embed 不能跨包引用,构建链为:Vite 输出 web/dist → Makefile sync-assets 目标拷贝至 internal/webui/dist → go:embed dist/* + fs.Sub 在 internal/webui 包内嵌入;internal/webui/dist 进 .gitignore(保留 .gitkeep 占位供 M0 编译);
  2. Vite base(审评 P0-1 修正):部署在域名根,使用默认 base: '/'。⚠️ 相对 base('./')会在 /notes/:slug 等深链下把资源解析到 /notes/assets/… 导致白屏,禁止使用;embed handler 从站点根提供 FS,绝对路径恰好正确命中;
  3. SPA fallback:/api/* 之外命中文件则返回,未命中(SPA 路由)回退 index.html;
  4. 元信息注入(审评 P0-4 修正):请求 /notes/{slug} 时服务端查询该笔记,**仅当可见(public 或管理员会话)**才注入 <title>/<meta description>/<meta og:*>,否则回退站点默认 meta;注入经 html/template 自动转义,防标题内容打断标签结构;
  5. 缓存与指纹:构建产物内容 hash 文件名 + 长缓存头(嵌入二进制,指纹天然隔离新旧版本);
  6. 冒烟测试守门(进 CI):真实构建 → 启动二进制 → 请求任一公开 slug 页面 → 断言 script/link 以 200 + 正确 MIME 加载(防「深链白屏」类回归)。

9. 安全设计

9.1 威胁模型与防护矩阵

# 威胁 防护措施 纵深
T1 存储型 XSS(恶意 Markdown/HTML/链接) goldmark 默认转义(服务端渲染路径);渲染链路 rehype-sanitize 白名单(schema 禁 style 属性);链接协议白名单;CSP script-src 'self' 兜底 三层
T2 CSRF 双保险:CSRF token 头校验 + **全 POST 覆盖(含 /api/auth/*)**的 Origin/Referer→Host 校验 + SameSite=Lax;login 的 Origin 校验同时防「跨站表单预热消耗令牌桶把管理员锁在门外」的 DoS 放大 三重
T3 会话劫持/固定 crypto/rand 256bit token;DB 存哈希;__Host- Secure HttpOnly Cookie;登录重建会话;登出删行;7 天过期 + 滑动续期(轮换保持 csrf 不变) —
T4 口令爆破 Argon2id(OWASP 参数);per-IP + per-账号令牌桶限流(fail-only);常量时间比较;统一 401 文案;改密复用同一套限流 —
T5 恶意上传(多态/WebShell) MIME 白名单 + 魔数校验 + 拒绝 SVG;BLOB 入库存不进文件系统(无执行环境);禁用路径穿越;nosniff;CSP img-src 限制;≤5MB —
T6 SQL 注入 全部参数化查询;无拼接;无 ORM 动态链 —
T7 路径穿越/静态文件越界 仅从 embed FS 提供文件;无用户可控路径参数 —
T8 点击劫持 Content-Security-Policy: frame-ancestors 'none' + X-Frame-Options: DENY —
T9 MIME 嗅探 X-Content-Type-Options: nosniff 全局 —
T10 私密笔记越权泄露 可见性在服务端查询层统一过滤(status='public' AND deleted_at IS NULL,覆盖列表/详情/标签/RSS/sitemap/meta 注入/图片直链);图片按并集语义判定;前端标识仅供参考。已知边界(显式接受):图片一旦随公开笔记暴露,已分发副本(代理缓存/爬虫/RSS 阅读器)无法撤回 单一可信点
T11 传输层窃听 仅 HTTPS(Caddy 自动证书);Strict-Transport-Security(§9.2) —
T12 依赖供应链漏洞 Go 依赖面 5 个直接依赖 + govulncheck CI;npm 锁文件 + npm audit;定期 dependabot/renovate —
T13 数据丢失 WAL;每日 VACUUM INTO 备份(0600 权限)+ 异地加密(rclone crypt/age);回收站 30 天缓冲;备份保留期内数据物理仍在(语义已声明,§10.3) —

9.2 安全 HTTP 头(SecurityHeaders 中间件)

Content-Security-Policy:
  default-src 'self';
  script-src 'self';                       ← 严格禁 inline script
  style-src 'self' 'unsafe-inline';        ← 原因仅一项:CodeMirror 经 style-mod 运行时注入
                                             <style>(rehype-highlight 为纯 class,不注入样式,
                                             不构成理由)。硬化路径(P2):Go 每响应下发 nonce
                                             注入 CSP + 前端 EditorView.cspNonce 传入,
                                             即可收紧为 style-src 'self' 'nonce-…'
  img-src 'self' data:;                    ← 图片仅本站 + data(粘贴预览)
  font-src 'self';
  connect-src 'self';
  object-src 'none'; base-uri 'none';
  frame-ancestors 'none';
  form-action 'self';
Strict-Transport-Security: max-age=31536000; includeSubDomains
Referrer-Policy: strict-origin-when-cross-origin
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Permissions-Policy: camera=(), microphone=(), geolocation=()

补充约定:/api/admin/* 与 /api/auth/* 响应统一 Cache-Control: no-store(防登出后 bfcache/返回键回看);公共路由(无编辑器页面)可单独下发更严格 CSP style-src 'self'。

9.3 上线前安全检查单

  • go vet + govulncheck ./... 干净;npm audit --production 干净
  • 可见性矩阵自动化用例全绿(§13),并人工抽查:私有笔记在列表/详情/标签/RSS/sitemap/图片直链/HTML meta 全部出口匿名不可见
  • 投递恶意 Markdown(<script>、javascript: 链接、事件属性、<iframe>、可疑 style)渲染无脚本执行
  • 跨站伪造 POST(含 login/logout)被拒绝;Cookie 属性与 __Host- 前缀验证;刷新页面后 CSRF token 经 /api/me 重取可用
  • 上传伪造扩展名的可执行文件/SVG/超限文件被拒
  • 登录接口连续爆破触发 429;登录失败日志含 IP
  • /api/admin/settings 响应不含 admin_password_hash;PUT 无法写入非白名单键;改密流程通过
  • 图片访问:私有与不存在统一 404;curl -I 对比公开图(immutable)与非公开图(no-store)缓存头
  • 安全头逐项用 curl/浏览器 DevTools 确认(含 HSTS)
  • 备份还原演练:停服 → 删除 -wal/-shm → 替换 db → 恢复成功;备份文件权限 0600
  • 回收站恢复用例通过;gc --dry-run 输出审阅后 --commit,且不含 7 天内孤儿图片
  • user_version 越界启动被拒(--allow-newer 显式放行)
  • 构建冒烟测试通过(§8.3-6)
  • Caddy 仅开放 80/443,程序监听 127.0.0.1

10. 部署与运维

10.1 构建

web:    cd web && npm ci && npm run build

# go:embed 不能跨包引用,必须把产物拷进 internal/webui 包目录
sync-assets:
	rm -rf internal/webui/dist && cp -r web/dist internal/webui/dist

build:  web sync-assets
	CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" \
	  -o pn ./cmd/pn

linux:  web sync-assets
	CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath \
	  -ldflags="-s -w" -o pn-linux-amd64 ./cmd/pn

dev:  # 双端热重载:air(Go)+ vite dev(proxy /api → 127.0.0.1:8080)+ start --dev
	air & cd web && npm run dev

10.2 运行

# deploy/pn.service
[Unit]
Description=Pure Note
After=network.target

[Service]
User=purenote
WorkingDirectory=/opt/pure-note
ExecStart=/opt/pure-note/pn start --addr 127.0.0.1:8080 \
          --data-dir /opt/pure-note/data --behind-proxy
Restart=on-failure
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/opt/pure-note/data
MemoryDenyWriteExecute=true

[Install]
WantedBy=multi-user.target
# deploy/Caddyfile
example.com {
    encode zstd gzip
    reverse_proxy 127.0.0.1:8080
}

幂等启动:start 时若 settings 无口令哈希则拒绝启动并提示先跑 init;user_version 越界同样拒绝(§10.4)。

10.3 备份与恢复

调度(systemd timer,替代 root cron)

# deploy/pn-maint.timer
[Unit]
Description=Daily Pure Note maintenance (gc + backup)

[Timer]
OnCalendar=03:00
Persistent=true

[Install]
WantedBy=timers.target

timer 触发 pn-maint.service:先 pn gc --commit,后 pn backup /backup/pn-$(date +\%F).db。

要点

  • 备份进程独立于服务进程运行,不受 unit 沙箱约束;VACUUM INTO 在 WAL 下与 start 并发安全(在线一致快照);
  • /backup 目录属主 purenote、权限 0700;备份文件chmod 0600(内含全部私有笔记、图片、口令哈希、会话哈希的明文副本);
  • 保留 30 天;语义声明:备份保留期内被删除的数据物理上仍存在于备份中;
  • 异地同步(可选):rclone 远端启用 crypt(或先 age 加密再上传),禁止明文上云;
  • 恢复 SOP:停服 → 删除 data/pn.db-wal 与 -shm(残留会污染还原)→ 以备份文件替换 pn.db → 起服。

10.4 升级

  1. 升级前第一步固定为 pn backup(写入 SOP);
  2. 停服 → 替换二进制 → 起服触发迁移:迁移仅追加式(不删列/重命名/改类型),user_version 顺序递增;
  3. 启动守卫:代码校验 user_version ≤ 本版本支持的最高版本,超出拒绝启动并提示,--allow-newer 显式放行(防止旧二进制静默在陌生 schema 上读写);
  4. 回滚:若迁移尚未执行(版本一致)可直接换回旧二进制;若已执行且不满足追加式约束,回滚 = 从步骤 1 的备份还原数据文件后再换回旧二进制。

10.5 观测性与开发工作流

  • 日志:标准库 log/slog;--log-format text(journald 友好)或 json;登录失败记 IP 与桶剩余计数;管理操作(增删改)记审计条目;不做纯请求量日志;
  • 优雅停机:SIGTERM/SIGINT → http.Server.Shutdown(10s 排空)→ SQLite 连接关闭;systemd ExecStop 无需特殊配置;
  • 本地开发:make dev 双进程——Go 侧 air 热重载,前端 vite dev 配置 server.proxy 把 /api 代理到 127.0.0.1:8080(同源是 cookie/CSRF 成立的前提);Go 侧 --dev 放开非 Secure Cookie(loopback-only 守卫)。

11. 性能与规模边界

维度 边界与说明
笔记量 1 万篇内无压力;列表查询走 idx_notes_public 复合索引
图片量 总 BLOB < 2GB 从容;单图 ≤5MB;公开图 immutable 缓存使图片仅首次请求走 DB
并发 SetMaxOpenConns(1) 串行写 + WAL;个人博客日流量千级 PV 足够;瓶颈先出现在带宽而非后端
多实例 不支持(内存限流、单库设计);需要时迁移 scs + 共享存储,属新架构决策
限流器内存 per-IP 桶上限 4096 条 + TTL 逐出(防伪造 XFF 撑爆内存,§14)
SQLite 通用边界 单机单进程;不适用于写入密集型多写者场景(本项目读多写极少,完美契合)

12. 开发路线图

里程碑 内容 测试/验收门
M0 骨架 Go 模块 + SQLite 迁移(user_version 守卫)+ 配置子命令 + 中间件链 + embed 构链(拷贝 + 占位 dist)+ make build/make dev make build 产出可运行二进制;start 起服务;/api/health 200
M1 公开浏览 笔记/标签 API + React 博客三页 + Markdown 渲染管线 + RSS/sitemap + 元信息注入(可见性规则) 可见性矩阵公开侧用例起步;构建冒烟测试入 CI
M2 管理端 登录/会话/CSRF(/api/me 重取)+ 笔记 CRUD + 回收站 + 图片上传(魔数/缓存头分流)+ 编辑器 + 改密 表驱动可见性矩阵完整(§13)+ 限流/CSRF/魔数/回收站用例随功能同 PR 交付
M3 安全加固 全量 §9.3 检查单、CSP 确认(记录 nonce 硬化待办)、CI 齐备(vet+vulncheck+测试+冒烟) §9.3 全项通过
M4 运维 systemd(服务 + maint timer)、升级演练、hey 压测、备份恢复演练 按 §10.3 恢复 SOP 演练一次真还原

预估工作量:单人全职约 2–3 周(含安全项验证)。


13. 测试策略

核心不变量 = 可见性/越权矩阵,表驱动自动化,随功能交付(M2 起)并长期进 CI:

维度 取值
主体 {匿名, 管理员会话}
笔记状态 {public, private, trashed(回收站中), 不存在}
出口 {列表 /api/notes, 详情 /api/notes/{slug}, 标签 /api/tags, 图片 /api/images/{id}, RSS /feed.xml, sitemap, HTML meta 注入, 管理接口权限}

判定规则(全部断言为「匿名不可见 ⇒ 404 或输出中不含」):

  • 匿名:仅 public(且未删除)可见,其余一律 404/不含;图片按并集语义;
  • 管理员:全部正常笔记可见;回收站仅经 /api/admin/trash;
  • 图片缓存头:公开图 immutable / 非公开 no-store / 未授权与不存在统一 404。

其他测试组(Go 集成测试:httptest + 临时目录真实 SQLite):

组 用例要点
迁移 空库 → 最新版本幂等;user_version 越界拒绝启动(--allow-newer 放行)
认证会话 登录成功/失败(401 统一文案)、限流 429、改密(旧错拒绝/新弱拒绝/成功)、轮换后 CSRF 不变、/api/me 匿名与认证态
CSRF 无 token 403、错 token 403、跨源 Origin 拒绝(含 login/logout)
上传 魔数不符/伪造扩展/SVG/超限/超时大 body 各拒绝;去重命中
回收站 删除→列表不可见→恢复→可见;30 天过期被 gc 清除;恢复后 refs 完整
gc dry-run 不改数据;7 天内孤儿图片不被删;会话过期清理
slug 同日冲突自动后缀;手改冲突 409
元信息注入 公开 slug 注入该笔记 meta;私有 slug(匿名)回退站点默认 meta

前端测试(vitest):sanitize schema 快照(禁 style/script/iframe、允许任务列表 checkbox)、渲染管线对恶意 Markdown 的冒烟用例。


14. 风险与备选方案

风险 / 权衡 应对 / 备选
图片「已公开即视为已分发」,转私无法撤回已泄露副本 已在 §6.2/§9.1 声明为接受边界;私密图片请勿与公开笔记共用同一文件(去重会合并)——或在编辑器内提示
SPA 对搜索引擎弱 已建 RSS + sitemap + 服务端 meta 注入(带可见性规则);公开页 Go 模板 SSR 是 v2 备选(保留 API 纯净性,切换成本可控)
软删除占用 slug 最多 30 天(保恢复身份) 可接受:个人站点频道创建频率低;如需立即释放,先在回收站手动「立即清除」(v1.1+ 可选)
goldmark v1 与 v2 分叉 初始锁定 v1.8.x(成熟);v2.0.1 稳定后评估一次性升级
TypeScript 7 生态兼容面 出现第三方库不兼容即回退 TS 5.9,无架构影响
编辑器路线摇摆 MarkdownViewer 组件边界独立;若日后要 WYSIWYG,仅替换 Editor(Tiptap 3 备选),数据层不变
SQLite BLOB 膨胀 去重 + 上传上限 + gc 清孤儿;失控时图片降级为「文件系统存储 + DB 存路径」(预留 store 接口)
自建会话的实现风险 实现面小(单表 + 标准 crypto,见 §13 测试组),并有 scs v2.9 作备选平替
反代之后限流 IP 识别 唯一可信代理 = Caddy(--behind-proxy 声明);取 X-Forwarded-For 最右一个条目(Caddy 追加),或让 Caddy 覆盖 XFF;桶有上限与 TTL
CSP style-src 'unsafe-inline' 残余面 v1 保留但注释修正;硬化路径:Go 每响应 nonce + EditorView.cspNonce 收紧为 'nonce-…'(§9.2),列为 M3+ 待办
构建链(嵌入/深链白屏)回归 §8.3-6 冒烟测试进 CI,从 M1 起持续守护

附录 A:依赖清单汇总(2026-09 核实)

Go(直接依赖,共 5)

依赖 版本 用途
modernc.org/sqlite v1.58.0 纯 Go SQLite 驱动(无 CGO)
github.com/yuin/goldmark v1.8.6 服务端 Markdown 渲染(RSS/meta)
github.com/microcosm-cc/bluemonday v1.0.27 服务端 HTML 白名单清洗
golang.org/x/crypto v0.56.0 Argon2id
golang.org/x/time v0.15.0 登录/改密限流

前端(核心,devDependencies 另计)

依赖 版本 用途
react / react-dom 19.2.8 UI 框架
vite / @vitejs/plugin-react 8.2.2 构建(base 默认 '/')
tailwindcss + @tailwindcss/vite + @tailwindcss/typography 4.3.3 样式体系
antd + antd-style 6.6.3 / 4.1.0 组件库(ConfigProvider 自定义主题 + createStyles 桥接)
react-router 8.3.1 路由
@tanstack/react-query 5.102.8 服务端状态
react-markdown + remark-gfm + rehype-sanitize + rehype-highlight 10.1.0 / 4.0.1 / 6.0.0 / 7.0.2 Markdown 渲染与防 XSS
@uiw/react-codemirror + @codemirror/lang-markdown 4.25.11 / 6.5.2 编辑器
lucide-react 1.42.0 图标

开发工具(建议):air(Go 热重载);vitest(前端测试)

主要参考链接

实际开发时以 go mod tidy / npm install 解析到的版本为准,锁定文件(go.sum / package-lock.json)入库。