Files
boundless/CLAUDE.md
T
吴璨 fad9760265 自部署多端同步:服务端、实时协作、口令登录与油画加载动画
- 新增 Node 服务端(Hono + Hocuspocus + node:sqlite),数据保存在服务器
- 基于 Yjs 的多端实时同步,本机 IndexedDB 离线缓存,撤销只针对本机操作
- 空间(标签)有独立地址,增删改通过接口同步到所有设备
- 口令登录(签名 Cookie 会话、登录限流、同源校验)
- 图片上传到服务器,过大照片上传前自动压缩
- 历史版本(自动/手动保存、一键恢复)、跨空间搜索、在线设备
- 随机油画加载动画(Painting Loaders,本地收录 p5 / p5.brush)
- 选中样式与拖拽性能优化:拖动结束后才同步位置
- Docker / docker-compose 部署,README 与 CLAUDE.md 更新
2026-10-21 18:09:03 +08:00

5.8 KiB
Raw Blame History

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

项目概述

无界笺 Boundless:自部署的无限画布笔记应用(文字便笺、图片卡片、连线、多空间),单用户口令登录,多台设备实时同步。界面文案与提示均为简体中文。

  • 前端:Vue 3 + TypeScript 6、Vite 8、Vue Flow(@vue-flow/*)、Pinia、Tailwind CSS 4
  • 同步:Yjs + Hocuspocus v4(WebSocket),y-indexeddb 做本机离线缓存
  • 服务端:Node.js 直接运行 .ts(原生类型剥离,无编译步骤)、Hono、node:sqlite
  • 包管理器 pnpm(lockfile v9,用 pnpm 10),Node ^22.18.0 || >=24.12.0

常用命令

pnpm install
pnpm dev                     # run-p:服务端 :3000(--watch)+ Vite :5173(代理 /api 与 /collab)
pnpm start                   # 只启动服务端;会托管 dist/(如果存在)
pnpm build                   # vue-tsc --build(含 server/shared)+ vite build
pnpm lint                    # oxlint + eslint,都带 --fix,会直接改文件

pnpm test:unit -- --run                                   # Vitest(src/__tests__ 与 server/__tests__)
pnpm test:unit -- --run server/__tests__/server.spec.ts   # 单个文件
pnpm exec playwright test --project=chromium              # e2e:自动 build + 启动真实服务端(:4174,数据目录 .e2e-data)
pnpm exec playwright test -g "多端同步" --project=chromium

开发时口令默认 boundless;可在 .env(见 .env.example)里设置 BOUNDLESS_PASSWORD 等变量。生产环境未设置口令会拒绝启动。数据在 DATA_DIR(默认 ./data):boundless.sqlite、images/、session.secret。

架构

目录分工

  • shared/board.ts:前后端共用的 Yjs 文档结构与读写函数(唯一的数据格式定义处)
  • server/:index.ts 组装 HTTP 服务与 WebSocket 升级;app.ts 是 Hono 路由;collab.ts 包装 Hocuspocus(鉴权、持久化、空间增删、历史版本、搜索);db.ts 是 SQLite;auth.ts 是会话令牌与同源校验
  • src/sync/:客户端同步层(共享 WebSocket、MetaSession 空间列表、BoardSession 单个画布)
  • src/stores/canvas.ts:Pinia store,连接同步层与 Vue Flow

Yjs 文档

  • meta:空间列表 spaces(id → Y.Map)。客户端连接是只读的,增删改空间都走 REST(/api/spaces),由服务端通过 DirectConnection 写入,再广播给所有设备。卡片数 cardCount 由服务端在保存画布时回写。
  • board:<spaceId>:cards(id → 嵌套 Y.Map,字段级合并)与 edges(id → 普通对象)。叠放顺序靠卡片的 z 字段,不靠数组顺序。
  • 新增卡片字段:改 shared/board.ts 的 CardFields/readCard,再改 store 里的 cardToNode/applyCardToNode 和 src/types/canvas.ts。

数据流(单向)

本机操作 → store action 用 board.transact() 写 Yjs(带 LOCAL_ORIGIN)→ Yjs observer → store.syncFromDoc() 把文档状态补丁到 nodes/edges(复用已有节点对象,保留 Vue Flow 的选中、尺寸等字段)→ v-model 渲染。远端更新走同一条 observer 路径。不要直接改 nodes 里的位置或内容来"保存",那样不会同步。

  • 拖拽、缩放进行中的卡片记在 busyIds,syncFromDoc 不覆盖它们的位置和尺寸;拖拽期间节流写入位置,beginGesture/endGesture 把整次拖拽合并成一步撤销。
  • 撤销/重做使用 Y.UndoManager,只跟踪 LOCAL_ORIGIN,所以只撤销本机操作。
  • Yjs 对象不能放进 Pinia 响应式状态,meta/board 会话放在 canvas.ts 的模块作用域。

服务端要点

  • 鉴权:HttpOnly Cookie boundless_session(HMAC 签名,密钥由 secret + 口令派生,改口令即全部登出)。WebSocket 在 upgrade 阶段和 Hocuspocus onAuthenticate 都校验;拒绝时 error.reason 为 unauthorized 或 not-found,客户端据此区分"登录过期"和"空间已删除"。
  • 非 GET 的 /api/* 与 WebSocket 都做同源校验(兼容 X-Forwarded-Host)。
  • 历史版本:每次保存画布前,若距上个版本超过 10 分钟,就把上一次保存的状态存为快照(所以保存的是改动前的样子);每个空间最多 120 个。恢复时先备份当前状态,再通过 DirectConnection 替换内容,所有设备自动更新。
  • 图片:POST /api/images 原始字节,按文件头识别格式,以 SHA-1 命名去重;读取需要登录。被删除卡片引用的图片不会自动清理。
  • 服务端代码由 Node 直接运行:只能用可擦除的 TS 语法(不能用 enum、参数属性),相对导入必须带 .ts 后缀(tsconfig.server.json 会检查)。

前端要点

  • 路由:/login、/s/:spaceId;/ 会跳到上次打开的空间。WorkspaceView 是懒加载的。
  • 不要用 crypto.randomUUID()(非 HTTPS 下不可用),用 src/sync/connection.ts 的 randomId()。
  • 加载动画:index.html 里的 #boot 启动画面 + src/boot.ts;PaintingLoader.vue 用于登录页和切换空间。画作在 public/painterly/(第三方代码,原样保留,lint 已忽略),通过 iframe 隔离 p5 全局模式,用 postMessage 通知父页面。
  • 样式全部在 src/style.css(BEM 类名 + :root CSS 变量),组件里没有 <style>。e2e 依赖 .note-card、.note-card__title、.vue-flow__pane 等类名和按钮的可访问名称。
  • 用户提示统一用 store.showToast(message)。

部署

Dockerfile(node:24-alpine,运行时直接 node server/index.ts)+ docker-compose.yml(仅监听 127.0.0.1:3000,数据卷 ./data)。前面需要支持 WebSocket 的 HTTPS 反向代理,README 里有 Caddy/Nginx 示例。