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