Files
boundless/CLAUDE.md
吴璨 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

73 lines
5.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`
## 常用命令
```sh
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 示例。