# /sdk —— 站内官方前端 SDK

| 文件 | 说明 |
|---|---|
| `mksync-1.js` | 统一账号云同步 SDK(全局 `MKSync`)。接入指南见 `~/.claude/skills/publish-web/user-api.md` |
| `mkchip-1.js` | 可选的账号/同步状态角标(全局 `MKChip`),不用它自绘也行 |
| `mktheme-1.js` | 站内统一深浅色主题 SDK(全局 `MKTheme`),协议见下文「统一主题协议 v1」 |
| `mkroom-1.js` | 联机房间 SDK(全局 `MKRoom`):建房/加入、广播与点对点消息、共享状态、房主、断线恢复、WebRTC 信令。接入指南 `room-api.md` |
| `mkapp-1.js` | 平台桥 SDK(全局 `MKApp`):向 play 外壳上报加载进度/就绪、运行上下文、当前用户、登录入口、分享短链、主题只读透传。平台外全部 no-op。接入指南 `app-api.md` |
| `mkvault-1.js` | 用户素材库客户端(全局 `MKVault`):`/api/vault` 条目 JSON(ETag/If-Match)+ 图片/音视频上传(可选前端压缩)。接入指南 `vault-api.md` |
| `mksync.d.ts` / `mkchip.d.ts` / `mktheme.d.ts` / `mkroom.d.ts` / `mkapp.d.ts` / `mkvault.d.ts` | TS 类型;构建型项目**只 vendor .d.ts**,运行时仍引 `/sdk/*.js` 绝对路径 |
| `demo.html` | 参数化自测台兼活文档:`/sdk/demo.html` 直接打开;query 开关见页内说明 |
| `roomdemo.html` | 联机房间自测台:开两个标签页即可验证建房/加入/消息/状态/断线/房主转移 |
| `guide.html` | **发布者指南**(作品要求/更新隐藏/SDK 接入/主题/下载表),给网页端发布作品的 publisher 角色看,账号中心有入口 |
| `user-api.md` | 完整接入指南的公开副本。**真相源是 `~/.claude/skills/publish-web/user-api.md`,改完那份必须 `cp` 过来**(不进 publish.py 的 sha 校验,靠人记) |
| `room-api.md` | 联机房间接入指南的公开副本。真相源 `~/.claude/skills/publish-web/room-api.md`,同上靠人 `cp` |
| `app-api.md` / `vault-api.md` | 平台桥 / 素材库接入指南。平台部署(cutover)后真相源移到 `~/.claude/skills/publish-web/`,之后同上靠人 `cp` |
| `index.html` | 三行重定向到 guide.html(publish.py 要求目录里有 index.html,便于把本目录整体发到画廊做真实 ns 自测) |

## 版本纪律(重要)

- 文件名带主版本号(`mksync-1.js`)。**已被项目引用的文件永不删除、永不做破坏性 API 改动**;
  要改 API 就开 `mksync-2.js`,旧项目继续引用旧文件。文件内 `VERSION`/`BUILD`
  (`mksync-1@1.1.0`)只做加宽:新增 status 字段、新增 error.code、新增选项与方法。
- **云端信封字节格式冻结**:上云永远是 `{mk:1, v, savedAt, data}` 这四个键。脏标 `d`、匿名水位 `pa`
  等只写进本地镜像——所以新旧 SDK 可以在同一账号的不同设备上互通,rev 也不会因为升级 SDK 而 churn。
- bug 修复直接改现有文件,**CF 边缘即时回源**(源站发 no-cache),但 Cloudflare 会把浏览器侧
  Cache-Control 改写成 max-age=14400——已打开过页面的浏览器最多 4 小时后才拿到修复。
  因此**项目引用 SDK 时带 `?v=`**:源码里写 `?v=dev`(或不写),`publish.py` 打包时自动替换成发布
  时间戳(`--no-sdk-stamp` 可关);手工 rsync 部署的站内页面自己写死时间戳。**不要**给 `/sdk/*`
  开 CF Cache Everything。
- `~/.claude/skills/publish-web/sdk/` 存有全部 SDK 文件的字节一致副本
  (`mksync-1.js`/`mkchip-1.js`/`mktheme-1.js`/`mkroom-1.js` 及各自 .d.ts;此前这句漏列了 mkroom,
  镜像里其实一直有。`mkapp-1.js`/`mkvault-1.js` 及 .d.ts 按约定在平台部署(cutover)时才拷进镜像,
  免得线上还没有这两个文件时 skill 就开始推荐项目引用它们)。**改完这里必须 `cp` 同步那份副本**,
  反之亦然——`publish.py` 每次发布都逐文件校验 sha256(以 skill 目录为准遍历),不一致直接报错退出。

## 统一主题协议 v1

站内所有页面(主站、play.html 外壳、各接入项目)共用一个主题真相源。`mktheme-1.js` 是参考实现;
必须零依赖或 host 无关的页面(play.html、TeachScience 的 file:// 写作流)按本协议手写,**改协议要
同时改三处:mktheme-1.js / play.html 内联段 / TeachScience assets/js/site.js**。

- 存储键 `localStorage['ws_theme']`,值严格 `'light'|'dark'`;**键不存在 = 跟随系统**
  (`prefers-color-scheme`),此时系统深浅色变化要实时响应。
- DOM 契约:`<html data-theme="light|dark">` 恒为解析后的值;CSS 按 `html[data-theme="dark"]` 写深色覆盖。
- 事件:变更时在 `document` 上派发 `mk:theme`,`detail` 为解析后的值(canvas/SVG 计算色靠它重绘)。
- **仅用户显式操作时写 localStorage,初始化只应用不写**——第一次打开就落盘会把用户永久钉死在
  当时的系统色,之后系统切换不再跟随(2026-08 前的旧实现就有这个问题,已修)。
- 同步机制:同源 `storage` 事件天然覆盖「主站↔play.html 外壳↔iframe 内项目↔其他标签」全部方向
  (监听 `e.key === 'ws_theme'` 或 `e.key === null` 即 clear)。
- **铁律:从 storage 事件应用主题时绝不回写 localStorage**,否则两个文档互相触发打乒乓。
- 引入位置:`<head>` 内同步引,且**放在第一个 stylesheet `<link>` 之前**——head 同步脚本会等
  前面所有待加载样式表就绪才执行,放 link 后面白等一次往返,还可能闪错色。
- 不碰 `color-scheme`:对不做深色的项目强设会把滚动条/表单控件染黑,要用的项目自己在 CSS 写。

## 服务端依赖

`/api/user/me`、`/api/user/data/<ns>`(accounts.js)。2026-07 起 GET 带 `ETag`/304、
PUT 支持 `If-Match` → 409(不带则维持 last-write-wins),SDK 依赖这些;
部署老版本服务端时 SDK 会退化为每次全量 GET + 无并发保护,但不会报错。
`adoptNs` 额外依赖 GET 的 ownerGuard 保留"原属项目已删除则放行接管"分支。

## 已知取舍(别当成 bug 再修一遍)

- **`pagehide` 的 sendBeacon 带不了 `If-Match`**。beacon 唯一可控的是 Blob 的 Content-Type,
  既设不了请求头也读不到响应——所以关页兜底那一发是无条件覆盖写,理论上能盖掉另一台设备刚写的档。
  缓解:beacon 只在"确实还有未推改动"时才发,且本地镜像的脏标 `d:1` 仍在,下次打开页面会重新走
  正常的合并/LWW 路径。**未来升级路**:平台的 putData 加一行
  `req.headers['if-match'] || req.query.ifMatch`,之后 SDK 只要把 beacon URL 改成
  `?ifMatch=<rev>` 即可获得并发保护(服务端忽略未知 query,SDK 先落地也无害)。
- **`local:false` 的项目拿不到脏标持久化**。脏标写在 localStorage 镜像里;自管本地存储的项目
  (数学游戏/MortalSaga/山海/TeachMath)靠自己的存档 + 启动时的时间戳守卫兜底。
- **`readOnly` 是单向的**。一旦读到 `信封 v > 本地 version` 就冻结上传直到刷新页面或 `reset()`
  ——宁可这一会儿不同步,也不能用旧 schema 覆盖新版本写的数据。
- **匿名进度默认不自动合并**。共享设备(孩子匿名玩完、家长登录)自动合并会污染账号,而 SDK 自身
  没有 UI 不能问用户,所以只置 `anonPending` 交给项目或 `MKChip`。要自动合并传 `promote:'merge'`。
- **`adoptNs` 不删旧档**。出错时旧档是唯一副本;旧 ns 会一直占着用户的 100 个 ns 配额位,
  确认迁移无误后让用户在账号中心手工清理(或 `fetch('/api/user/data/<旧ns>', {method:'DELETE'})`)。
- **`?v=` 注入只发生在打包时**。publish.py 改写的是写进 zip 的字节,用户源码磁盘一个字节都不动
  ——所以 git diff 永远干净,源码里那句 `?v=dev` 保持原样。
- **统一主题整套建立在 `/p` 与主站同源之上**。将来若给 `/p` 换独立 origin,localStorage 不再共享,
  storage 同步整体失效——届时需要设计 play.html↔iframe 的双向 postMessage 主题协议,并给直开
  `/p/` 的场景另找通道,不要只打一半补丁。
- **主题协议有两份实现**(SDK 版与手写协议版),靠上文的三处清单与发布前的交叉互切实测防漂移。
