# 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:`:`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 变量),组件里没有 `