# 打包清单(打包前必读) 制定日期:2026-09-11。目的:把「开发态能跑」变成「别人能装」。 > **2026-09-11 更新:四条阻塞已全部处理,包已产出。** 现行结构见下;**唯一未做的是"装到别人机器/干净环境"的实测** > (`dsh plugin add` 会写入 `~/.dsh` profile,属于用户环境改动,需用户执行)。 | 阻塞 | 处理结果 | |---|---| | ① 缺 `dsh.bundle.patch` | ✅ 仓库根 `package.json` 声明 `dsh.bundle.patch: ./cordis.patch.yml`;补丁里用**包名**引用(`name: 'dsh-studio-panel'`),由 `exports['.']` 解析到 `src/host.mjs` | | ② 跨目录依赖 | ✅ 已**拍平**:仓库自身就是插件包(`src/host.mjs`、`src/shared/*` 都在根);由 `files` 决定随包分发的内容,开发件(`scripts/`、`tests/`、`docs/`、探针)不进包 | | ③ 绝对路径资源与状态文件 | ✅ 状态文件改到**用户目录** `/.dsh/studio/state.json`;资源路径由**宿主按包位置解析**并写进快照(`snapshot.assets`),客户端优先用快照、构建期注入只作开发回退 | | ④ 两个插件行 | ✅ 合成**一个包、一份补丁**;开发期绝对路径挂载单独放 `src/host/cordis-dev.yml`(不进包) | **已验证**:`npm pack`(仓库根)产出 15.2 MB / 解包 23.8 MB / **24 个文件**;包内**不含**设计文档、测试、脚本、网站、开发工具; 包内 11 处相对 import 全部可解析;`dsh.bundle.patch` 与 `exports['.']` 指向存在。 **尚未验证**:`dsh plugin add` 实际安装、卸载、重装与两端表现(需用户在自己的 profile 或干净环境执行)。 配套文档:[开发规划](开发规划.md) §13 收尾记录、[第三方声明](../THIRD-PARTY-NOTICES.md)。 --- ## 0. 目标与验收 **目标**:产出一个可安装的插件包,用户在**干净环境**里能用官方命令装上、启用、关闭、卸载、重装。 **验收(对齐规划 REL-04)**: 1. 干净环境(没有本项目目录、没有预览服务器)执行安装命令后,宿主能扫到本插件; 2. 打开会话 → 切到「3D 工作室」→ 场景加载成功(资源不依赖开发机绝对路径); 3. 发一条消息 → 岗位动作 / 气泡 / 交付纸 / 员工日志 / 汇报记录 均正常; 4. 卸载 → 无残留(无监听器、无定时器、无 GPU 资源累积); 5. 重新安装 → 仍可用。 --- ## 1. 四条阻塞(按依赖顺序处理) ### ① 包没有安装入口:缺 `dsh.bundle.patch` - **现状**:`packages/studio-panel/package.json` 只有 `exports` 与 `dsh.client`。 - **改法**:加 `dsh.bundle.patch`,指向包内的 patch 文件;patch 内容 = 把**两个插件行合成一个包**(见 ④)。 - **影响面**:只影响安装方式,不改运行逻辑。 - **验收**:`dsh plugin --profile web add <包路径>` 之后,`--dump-config` 里能看到本包的行。 ### ② 代码跨目录依赖 `src/shared/*` - **现状**(证据): ``` packages/studio-panel/src/client/index.mjs: import ... from '../../../../src/shared/studio-art.mjs' import ... from '../../../../src/shared/studio-lighting.mjs' import ... from '../../../../src/shared/studio-camera.mjs' src/host/studio-bridge.mjs: import ... from '../shared/state-engine.mjs' 等多处 ``` - **改法**:把 `src/shared/*` 与宿主桥接**移进包内**(例如 `packages/studio-panel/src/shared/`、`packages/studio-panel/src/host/bridge.mjs`),把相对路径改成包内相对路径。 也可以反过来:把 `src/` 整体作为包的源码目录,`src/host/*.mjs` 与 `src/shared/*` 都随包发布。 - **影响面**:**纯移动 + 改 import**,逻辑不变;测试里引用 `src/shared/*` 的路径要同步改。 - **验收**:`npm test` 仍全绿;包目录内不存在指向包外的相对 import。 ### ③ 资源与状态文件使用绝对路径 / 项目内路径 - **现状**(证据): - `scripts/build-client.mjs` 把 `__STUDIO_GLB_PATH__`、`__STUDIO_ART_ROOT__` 按**仓库根**算成字面量塞进产物; - `src/shared/studio-state-path.mjs` 默认 `/tmp/studio-state.json`。 - **改法**: - 资源(`studio.glb`、`assets/art/studio-v1/*.png`)**随包发布**,路径由**包自身位置**推导(宿主侧用 `import.meta.url`,客户端侧由构建时注入包内相对路径 + 运行时拼 Remote 可读的绝对路径); - 状态文件默认改到**用户目录**(如 `~/.dsh/studio/state.json`),保留 `DSH_STUDIO_STATE` 覆盖。 - 注意体积:`studio.glb` 13.21 MiB(其中 **95% 是未压缩几何**)——若要减小安装包,先做几何压缩(Draco/meshopt),但这会改资产管线,需单独一轮。 - **影响面**:客户端与宿主都要改;会影响"两端通用",所以桌面端要一起想。 - **验收**:把包拷到另一目录(模拟安装位置)后仍能加载资源;状态文件出现在用户目录而非项目目录。 #### ③ 的收尾实测:一层没料到的可移植性缺陷(2026-09-11) 上面这条当时只解决了"资源按包位置解析",**漏了引导路径本身**:客户端要读快照,就得先知道快照在哪, 而那个绝对路径是**构建期**算的(`DSH_HOME || homedir()`)→ 烤进 bundle 就是**打包那台机器**的路径。 后果不是"少显示点东西",而是**整个工作室打不开**:读不到快照 → 连 GLB 路径都拿不到(资源位置也走快照)。 - **发现方式**:对已打好的 tarball 做产物卫生扫描,在 `lib/client.js` 里查出 `Ia="C:\\Users\\LAI/.dsh/studio/state.json"`、`$0="L:/Toolfolk for DSH/assets/3d/v2/studio.glb"`。 - **修法**(两端各一半): - 宿主:快照**同时**写一份到**会话工作区** `/.dsh-studio/state.json`(内容相同,内容没变就不重写, 免得在用户项目目录里抖 git 状态);工作区根取 `session.header.cwd`,没有就回退 `sandboxPolicy.workspaceRoot`—— 与官方 `workspaceFiles` 的 scope 解析**同一个公式**。 **只写两处:live 会话的 cwd + sandbox 工作区根(上限 3)**。中间试过"把 `sessionPersistence.list()` 里所有存储态会话的 cwd 也写一遍",理由是官方对冷会话走持久化兜底;**实测这次过度设计有害**: 一次启动就往用户 **8 个无关项目目录**丢了 `state.json`(`L:\GTAGO`、`L:\Knogent`、`OneDrive\文档` …), 而且旧进程还在反复重写——已回退。"猜客户端在看哪个会话"不可控;冷会话万一读不到, 客户端会把每条候选的失败原因显示出来,那是**可诊断的失败**,比到处丢文件好。 - 客户端:候选顺序改为「上次生效的路径 → 工作区**相对**路径 → 构建期绝对路径(现已注入 `null`)」, 读不到时把每条候选的失败原因打到面板上(不再只说"未知")。 - 构建:`scripts/build-client.mjs` 三个路径注入值一律 `null`,**产物里不再有任何机器相关路径**。 - **依据(官方源码)**:`workspaceFiles.readAll` 接受"绝对或**工作区相对**路径", 相对路径按 `ctx.fs.resolve(path, { cwd: workspaceRoot })` 解析,且 `readAll` 不做 `confine`(工作区外也可读)。 - **实测证据**(`bridge-selftest` + 探针 home,与客户端同一个服务方法): ``` [studio-bridge] {"kind":"workspace-state","path":"…\\tmp\\host-run/.dsh-studio/state.json"} [bridge-selftest] {"kind":"workspace-copy","written":true,"version":1,"connection":"connected"} [bridge-selftest] {"kind":"workspace-relative-read","ok":true, "relativePath":".dsh-studio/state.json","bytes":4110,"sameAsHomeCopy":true} [bridge-selftest] {"kind":"verdict","pass":true} ← 10 项 check 全过 ``` - **真浏览器 + 真宿主的端到端验收**(本轮唯一一次真的看到了画面): 浏览器打开新版客户端 →「3D 工作室」画布已挂载(`容器 549×370 · 缓冲 823×555 · dpr 1.5 · 缓冲与容器匹配 ✅`)、 **无任何错误文案**(若快照读不到会说"读不到宿主状态快照"且**根本不会挂载画布**)、 员工日志列出 **10 条真实交付**(09:53–19:14,全文 91–2900 字)、汇报记录 10 份。 即:客户端确实通过**工作区相对路径**找到了宿主状态,可移植性改动在真实环境成立。 - **诊断也被这次故障改好了**:客户端解包 Remote 信封时曾把官方的 `error.code/message` 丢掉、只抛一句 「Remote 调用被拒绝」,用户实测就撞上这个(面板上毫无信息量)。现在原样透出,例如 `workspace-file/not-found:no entry at ".dsh-studio/state.json"`;并且"快照读不到"不再被下游误报成 "GLB 读不到"(那会让人以为是模型文件的问题)。 - **未实测**:真正在**另一台机器/另一个账号**上安装后目视(本环境只有一台机器); 但有 `sameAsHomeCopy=true` + 相对路径解析依据,可移植性不再依赖"打包者是谁"。 - **副作用(要说清楚)**:会话工作区里会多一个 `.dsh-studio/`(派生数据,可删);仓库与包 README 都已写明。 ### ④ 两个半侧是两个插件行 - **现状**:`packages/studio-panel/cordis.yml` 里插入了两行:`dsh-studio-panel`(客户端面板宿主半侧)与 `studio-bridge`(状态桥接)。 - **改法**:合成**一个包**(一个 `package.json` + 一份 patch),宿主入口统一导出一个 `apply()`,内部再分别做「客户端图自检」与「状态桥接」两件事——或把自检去掉(它只是 M1 验证用)。 - **影响面**:包结构变化;`cordis.yml` 由两行变一行。 - **验收**:`--dump-config` 里本包只有一行;功能不变。 --- ## 2. 声明与元数据(部分已完成) | 项 | 状态 | |---|---| | `THIRD-PARTY-NOTICES.md`(three.js MIT 原文 + esbuild + 官方包说明 + 资产来源) | ✅ 已建(发布前必须**随包附带**) | | 产物头部许可指引(bundle 内联了 three,声明要能被找到) | ✅ 已加(`scripts/build-client.mjs`) | | `packages/studio-panel/package.json`:`description`、`license: UNLICENSED` | ✅ 已改 | | **本项目自身许可** | ✅ **MIT**(2026-09-11 确认):`LICENSE` 已建于仓库根;插件包与根 `package.json` 的 `license` 均为 `MIT` | | **`assets/art/studio-v1/*.png` 来源** | ✅ **项目作者自制**(2026-09-11 确认),不含第三方素材 | | `author` / `repository` / 发布说明 | ⏳ 若公开分发再补 | | `files` 字段(声明哪些文件进包) | ⏳ 与 ② 一起定(取决于最终目录结构) | | 隐私说明(全部本地读取、不上传) | ✅ 见 README | | 宿主兼容版本(`0.1.5-rc.1`,React 18.3.1) | ✅ 见 README | --- ## 3. 建议的打包顺序 1. 先做 ②(目录整理)——它决定 ①④ 的写法; 2. 再做 ③(资源与状态文件路径)——顺带决定 `files` 字段; 3. 然后 ①④(合成一个包 + 一份 patch); 4. 最后补 README 安装说明(声明与 `LICENSE` 已完成); 5. 在**干净环境**按 §0 验收,产出候选包。 **注意**:以上都不产生观感价值。若只是自己用,可以一直停在开发态(`--patch` + 绝对路径),不影响现有功能。 --- ## 4. 干净环境验收步骤(可照着做) ```bash # 1) 在一台没有本项目目录的机器(或新建用户账户)上 # 2) 安装包 dsh plugin --profile web add <候选包路径> dsh --profile web --dump-config | grep -n "studio" # 应只出现一行本包 # 3) 启动并目视确认(用带 token 的完整地址) dsh --profile web --port 3081 --no-open # 4) 卸载后查残留 dsh plugin --profile web remove <包名> # 浏览器侧看:切换视图 20 次后 GPU 内存是否回落(对应 REL-03) ``` --- ## 5. 风险 | 风险 | 说明 | |---|---| | 桌面端从未验证 | 打包完成后**仍不能宣称支持桌面**,需单独回归(接口与资源加载路径可能与 Web 不同) | | 资产 13.21 MiB | 装包体积偏大;几何压缩可显著减小,但属于资产管线改动,需单独排期与画质比对 | | 官方接口变动 | 宿主固定 `0.1.5-rc.1`;升级需重跑回归(事件契约、slot、Remote 通道都可能变) | | 许可瑕疵 | **未附 `THIRD-PARTY-NOTICES.md` 就分发 = 违反 three.js 的 MIT 声明要求**,这是硬性合规项 | --- ## 6. 分发渠道(2026-09-11 实测补充) 三条渠道,**是否需要用户授权**是关键差别: | 渠道 | 命令 | 用户需授权执行作者代码 | 证据 | |---|---|---|---| | **git(一条命令、零授权)** | `dsh plugin add github:owner/repo` | **否** | 包在**仓库根**、**没有安装期脚本**、资源随包提交 → pnpm 的 `allowBuilds` 门禁不触发 | | **tarball / Release 附件** | `dsh plugin add ./dsh-studio-panel-0.1.0.tgz` | **否**(资源已在包内) | **全链路实测通过**(探针 home,未碰真实 profile):`plugin add` → **不带 `--patch` 启动即自动加载**,资源从安装位置解析 → `plugin remove` 后重启,插件日志 0 行 | | npm(未发布) | `dsh plugin add dsh-studio-panel` | 否 | 清单已去掉 `private`、补了 `repository`/`author`,`npm publish` 即可 | ### 为什么现在不需要授权了(2026-09-11 拍平) 原来包在 `packages/studio-panel` 子目录、资源被 gitignore,只能靠 `prepare` 在安装时把 `studio.glb` 与美术图复制进包 —— 于是触发 pnpm ≥10 的 `allowBuilds` 门禁,用户必须手动授权(=允许作者脚本在其机器上执行)。 现在**把包拍平到仓库根**(仓库自身即插件包)并把资源随仓库提交,包内**一个安装期脚本都没有**, 门禁无从触发;`files` 字段仍然保证只有该发的文件进包。这条与主流插件(如 `github:MeteorNOX/DeepSeek-Balance-Whale-Widget`:包在根、`scripts: {}`、资源已提交)是同一个形态。 ### 建议 - 面向普通用户:**`dsh plugin add github:owner/repo` 一条命令**(零授权、零构建、能跟源码)。 - 网络不稳/要离线分发:用 Release 的 tarball。 - 「会运行作者脚本、需要 allowBuilds」这段警告**已经不需要了**(包内无安装期脚本);想可复现就锁 commit。 ### 一个实测到的坑:安装路径**不能含空格** ``` dsh plugin add "L:/Toolfolk for DSH/tmp/dsh-studio-panel-0.1.0.tgz" → [ENOENT] open '…\profiles\web\DSH\tmp\dsh-studio-panel-0.1.0.tgz' → [WARN] Installing a dependency from a non-existent directory: L:/Toolfolk ``` 参数里的空格被切开了(pnpm 只收到 `L:/Toolfolk`)。规避:`cd` 到包所在目录用**相对路径**, 或把 tarball 放到无空格目录。GitHub 的 `owner/repo#path:/…` 形式不含空格,不受影响。