# 在 Windows 本机跑官方 dsh 并做端到端取证 > **归属**:技能 `dsh-local-env` · 详情档(主干 `../SKILL.md`) > **本档覆盖**:原技能 `dsh-desktop-dev-shell` **全文**(正文 349 行 + frontmatter 变更历史) > **provenance**:本机版(WorkBuddy 实况) > **搬运方式**:**逐行未改**;内部附件链接已改写为 `dsh-desktop-dev-shell/<附件名>`(附件在本目录下) > ⚠️ 原技能 `dsh-desktop-dev-shell` **已合并退役** ⇒ 见到该名按本档读。 --- # dsh-desktop-dev-shell — 在 Windows 上跑起官方 dsh 并做真跑验证 ## 0. 为什么存在 `@deepseek-ai/dsh` 的**运行时**可以 `npx` 直跑,但**桌面壳**(`apps/desktop` + `apps/desktop-host`)是 `private: true`、未发布 npm ⇒ **只能克隆源码构建**。这条路在 Windows 上有一串**看起来像"方案不行"、其实是环境**的坑,逐条定位一次要花几小时。本技能把这些坑与处置固化下来(含 **web 实例**形态,见 §10)。 ## 1. 两种形态:先选对入口 官方 dsh 在 Windows 本机有**两种跑法**,本技能两条都管。**先判定你要哪一种**,再跳到对应章节: | 形态 | 入口 | 有没有 HTTP 端口 | 适用章节 | |---|---|---|---| | **web 实例** | `apps/cli` 的 `dsh --profile web` | ✅ 有(回环 + token 栅栏) | **§11** | | **桌面壳** | `apps/desktop`(源码构建) | ❌ **不开监听端口**(Electron 管道 + `dsh-app://`) | §2–§10 | **两形态共用的判据链**(无论走哪条都先过一遍): 1. **必须 managed Node 22** —— 原生模块按 Node 22 编译,用 24 会 `ERR_DLOPEN_FAILED`。 2. **监听 ≠ 就绪** —— 插件层晚于端口挂载,起听后立刻访问会拿到 404 / 拒连 ⇒ **轮询到成功响应**再判。 3. **陈旧写锁会让实例"永久起不来"** —— 实例被中途掐断留下 `/.credentials.yaml.lock` ⇒ 之后同一 `DSH_HOME` 每次启动都 `exit 1`(报 `plugin tree failed to load` + `atomic-write: timed out waiting for the writer lock`)。 ✅ 处置 = **换全新 `DSH_HOME`**(立刻恢复)或启动前清掉该 `.lock`。**别改官方源码。** ⇒ **探针 / 守护脚本一律「每次全新 DSH_HOME + 就绪前不掐断」**。 4. **`ELECTRON_RUN_AS_NODE=1` 可能就在环境里** ⇒ 不删掉的话 Electron 退化成纯 Node、**不出 GUI**(像"壳起不来")。 > ⚠️ 形态选错的典型症状:**去 `curl` 桌面壳的 HTTP 端口找页面** —— 壳压根不开端口(见 §2 第 1 条)。 ## 2. 桌面壳:三条必须先知道的判断 1. **壳不开监听端口。** 官方 README 原文:"**It opens no listening port**" ⇒ Electron 主进程与内置 Node 子进程走**带版本的定长帧管道**,渲染层用 **`dsh-app://`** 取资源。 ⇒ ⛔ 别去 `curl` 壳的 HTTP 端口找页面(没有);要验渲染层请用 **`--remote-debugging-port`**。 2. **开发态里 `DSH_DESKTOP_DEV_PROJECT_DIR` 直接充当 profile。** `main.ts` 里 `activeProject = development ?? paths.profile` ⇒ **跳过** project-manager 的 staging / healthCheck / activate 事务。 ⇒ 想挂插件,**自己造 profile 目录**,别指望走事务。 3. ⛔ **别用 `grep -nE "https?://" <源码>` 判"有没有网络依赖"** —— 它会**结构性漏掉** `electron-updater`: 它的 feed 地址在**打包产物**的 `app-update.yml` 里,**源码里一个 URL 字面量都没有**(本项目因此错下过一次结论)。 ✅ 正确口径(本线实测,逐处读码得来):壳的启动链只有**三类**碰网 —— ① `main.ts` 启动后 **10 s** 自动 `checkAndPrompt(false)` → `electron-updater`:**三重可容忍** (`enabled()` 要求 `app.isPackaged` **且** `resourcesPath/app-update.yml` 存在 ⇒ 开发态/未配 publish 的包**零网络**; 用 `void` 发起**不阻塞**窗口;全程 try/catch + `manual=false` ⇒ **静默失败不弹窗**) ② `project-manager.ts` 的 `DESKTOP_REGISTRY='https://registry.npmjs.org/'` ⇒ 只在**装/更新插件**时(离线装不了插件) ③ `host-process.ts` 的 `async fetch(request)` 是**进程内 fd 转发**(壳↔host),⛔ 不走网络 ⇒ 结论:**"启动 → 出窗口 → 实例页"这一段不依赖网络**,可以做成离线可用。 ⚠️ 但**装成正式包并配了 `app-update.yml` 之后**,10 s 那次检查会真的发请求 ⇒ 届时应复测。 4. **`dev.ts` 每次启动都重写 profile 的 `package.json`。** 所以手工挂进去的插件**会被抹掉**。 ⇒ 要么每次重新挂,要么**别用 `dev.ts`**(见 §4)。 ## 3. 依赖安装:pnpm 在本机"卡在 added ~100"的真根因 **症状**:`pnpm run <任意脚本>` 都先跑一遍安装,然后**卡在 `added ~104` 十几分钟不动**(`downloaded 0`,包全在 store 里)。 **根因(三条叠加,缺一不可)**: 1. **pnpm 的 before-run 安装钩子默认开启** ⇒ 每次 `pnpm run` 都先做依赖校验,判定"需装"就真的装。 2. 该次安装要**按虚拟 store 重建已有的 `node_modules`**;若 `.pnpm` 是上一轮**失败安装留下的半成品**,这一步在本机退化成**每包数秒**。 ⚠️ **不在网络、不在磁盘、不在 pnpm 版本**(11.7 与 11.21 一样中招)—— 别在这三个方向浪费时间。 3. 若上一轮用过**非默认 `virtual-store-dir`**,顶层符号链接会全指向那棵树,并留下 `.pnpm-workspace-state-v1.json` 记录该配置 ⇒ 任何**不带该配置**的 `pnpm run` 都被判"配置变了要重建" ⇒ **死循环**。 🔴 附带后果:官方 `dev.ts` 的 `dependencyDir` **写死 `node_modules/.pnpm/node_modules`** ⇒ 非默认虚拟 store 会让官方 dev 流程**直接失效**。 **处置(三步)**: ```bash # ① 把损坏/陈旧的树【改名移走】——⛔ 不是删除 mv node_modules/.pnpm <外部目录>/pnpm-old mv node_modules/.pnpm-new <外部目录>/pnpm-new mv node_modules <外部目录>/node_modules-old # ② 空目录上做默认布局安装 pnpm install --frozen-lockfile --config.minimumReleaseAge=0 # ③ 此后所有 pnpm 调用都带这一条(⭐ 能透传到嵌套 pnpm run,无需 shim) pnpm --config.verify-deps-before-run=false run