fad9760265
- 新增 Node 服务端(Hono + Hocuspocus + node:sqlite),数据保存在服务器 - 基于 Yjs 的多端实时同步,本机 IndexedDB 离线缓存,撤销只针对本机操作 - 空间(标签)有独立地址,增删改通过接口同步到所有设备 - 口令登录(签名 Cookie 会话、登录限流、同源校验) - 图片上传到服务器,过大照片上传前自动压缩 - 历史版本(自动/手动保存、一键恢复)、跨空间搜索、在线设备 - 随机油画加载动画(Painting Loaders,本地收录 p5 / p5.brush) - 选中样式与拖拽性能优化:拖动结束后才同步位置 - Docker / docker-compose 部署,README 与 CLAUDE.md 更新
5.8 KiB
5.8 KiB
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阶段和 HocuspocusonAuthenticate都校验;拒绝时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 类名 +:rootCSS 变量),组件里没有<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 示例。