← Back to home@T-Auto

dsh-dpx

Isolated Multi-Environment Manager for DSH / DSH 多隔离环境管理器

Stars
3
Language
JavaScript
Created
Sep 8, 2026
Updated
Sep 21, 2026

Introduction

dsh-dpx

dsh-dpx 是给 DeepSeek Harness(DSH)创建、安装、发现和启动多个彼此隔离环境的包管理器。它不是 DSH 插件,也不修改 DSH 的核心或插件协议。

在 Windows 上,首次创建环境会默认同时放入一个可双击的桌面端 EXE;它以极简黑白启动页显示 DSH / DeepSeek Harness Desktop / 隔离环境:<名称> / 正在启动本地 DSH 服务…,随后在内置 WebView 中载入该环境的 DSH Web UI。

它服务于两个目标:

  1. 开发与调试:一台电脑可以保留多个命名 DSH 环境,例如 test、stable、alpha;它们可安装不同版本的 DSH、TUI 或第三方包,互不污染,便于复现和比较问题。
  2. 未来的第三方整合包:整合包可按 dsh-distribution 的环境身份与发现规则注册实例,让其他兼容包管理器不扫描磁盘也能找到它。

当前状态:实验性、Windows 优先。需要 Node.js >=22.19.0。0.1.0 已实现命名隔离安装、DPX 发现 profile、环境内 DSH/TUI 启动;dsh-tui --环境名 的全局启动器兼容层需要由 dsh-tui 项目接入,详见“dsh-tui --test”。

与 dsh-distribution 的关系(消费覆盖)

dsh-distribution 定义"一个 DSH 环境如何被外部世界识别、发现、管理与迁移"的环境元协议;本仓库是它的一个实现范例,不是它的标准来源,也不是唯一的环境管理器。协议正文与条款 ID 以该仓库的 docs/proposals/ 为准;本仓库只负责:实现它、并如实说明自己实现了哪些面。

仓库根的 dsh-distribution.json 是本仓库作为发行物的自我声明。它当前声明 7 个协议面里的 2 个:

dsh-distribution 协议面本仓库是否声明/实现证据
身份与声明(DistributionDescriptor)✅ 声明dsh-distribution.json 本体
受管存储归属(ManagedLayout)✅ 声明同上:environment-root、registry 两个独占资源(exclusive + conditional)
发现与环境实例(EnvironmentDiscovery + EnvironmentInstance)✅ 声明描述符中的 references;运行期写入的实例记录与注册表(src/index.js 的 EnvironmentInstance 记录与 instanceId 唯一性校验)
环境组成声明(EnvironmentComposition)❌ 未声明无
环境生命周期观察(EnvironmentLifecycle)❌ 未声明无
可迁移性计划与恢复日志(EnvironmentPortability)❌ 未声明无
可枚举共识入口(Lodgement)❌ 未声明无

未声明的面不表示"不适用",只表示本仓库没有为此提供实现或证据。因此:

  • 不要据本仓库推断 dsh-distribution 已被完整实现——它目前只在"身份 + 归属 + 发现"三面有实现证据;
  • 也不要据本仓库推断某个环境管理器是唯一选择;协议明确允许私有坐标与非中央的多来源模型;
  • 描述符可以离线校验。用一个实现了该协议的校验器读它(例如 dsh-distribution 仓库的 packages/conformance CLI),应当得到 valid: true, complete: true: 这两项只说明结构完整,不说明来源可信、隔离成立或有权执行管理操作。

与普通 DSH 插件安装的区别

例如,TUI 这类 DSH 插件可用普通 npm 全局安装:

npm install -g @deepseek-ai/dsh @deepseek-harness-tui/dsh-tui

此命令会在全局 DeepSeek Harness 上安装 DSH 与 TUI 启动器;TUI 首次运行时会通过 DSH 的插件机制写入默认的全局 DSH profile。它适合只有一个日常 DSH 环境的用户。

dsh-dpx 的定位不同:它是管理多个隔离 DSH 发行环境的包管理器/环境管理器。它可以经由 npm/npx 直接拉取,也可以从本仓库本地构建、链接和运行。每个环境独立拥有 npm 全局前缀、下载缓存、DSH 状态、agents/skills、工作目录和环境描述符;安装到 test 不会改变全局 DSH,也不会改变 stable。环境里的 npm 保持原生行为(原生默认落在环境自己隔离出来的 profile 里,不是 dpx 管理的 npm-prefix),要写进环境必须显式给出 --prefix / --cache(或直接用 dpx npm install),并且每个环境都会自动得到一份说明自己在哪、怎么设计的 dsh-home\AGENTS.md。

安装 dpx

从 npm 安装(发布后)

npm install -g dsh-dpx

也可以按 npm 习惯一次性执行:

npx dsh-dpx --help

从本地源码运行

git clone https://github.com/T-Auto/dsh-dpx.git
cd dsh-dpx
npm link

npm link 后可直接使用本地构建的 dpx。开发验证命令:

npm test
npm run check
npm run pack:check

一条命令创建并安装独立环境

下面的命令创建名为 test 的环境,并把 DSH 与 TUI 都安装到 D:\DevEnvs\Projects 下的专属环境目录:

dpx npm install -g @deepseek-ai/dsh @deepseek-harness-tui/dsh-tui --test --"D:\DevEnvs\Projects"

# 若只需要命令行隔离环境,不复制桌面端 EXE:
# dpx npm install -g @deepseek-ai/dsh --test --"D:\DevEnvs\Projects" --no-desktop

这里的两个特殊参数含义为:

  • 第一个 --test:环境名称,类似 conda 的环境名。名称使用 ASCII 字母、数字和连字符,且以字母开头。
  • 第二个 --"D:\DevEnvs\Projects":仅在首次创建环境时指定的绝对存储父目录。

命令会建立:

D:\DevEnvs\Projects\dsh-environments\test\
├── npm-prefix\                # test 专属的 npm 全局包与命令 shim(需显式 --prefix 才会写入)
├── npm-cache\                 # test 专属的 npm 下载/内容缓存(需显式 --cache 才会写入)
├── dsh-home\                  # test 专属 DSH_HOME:profiles、设置、会话、存储
│   └── AGENTS.md              # dpx 自动写入的环境级全局指令(TUI / Web / 桌面端都会读)
├── agents-home\               # test 专属 DSH_AGENTS_HOME:agents / skills
├── home\
│   └── Desktop\               # Windows 首次建立工作区使用的默认位置
├── appdata\ localappdata\ tmp\
├── workspace\                 # dpx 启动 DSH 时的工作目录
├── desktop\
│   ├── DSH DeepSeek Harness Desktop.exe
│   │                          # 默认复制的 Windows 桌面启动器,可直接双击
│   └── .dpx-desktop.json      # dpx 记录的启动器版本/摘要,供更新检查使用
├── desktop-state\             # 桌面启动器自己的状态(随环境隔离)
│   ├── settings.json          # 关闭行为、更新源、托盘开关
│   ├── shell.json             # 可选:启动契约(入口/参数/node),见下文
│   ├── shell.log              # 启动器日志
│   ├── updates\               # 更新暂存与被替换下来的旧 EXE
│   └── webview2\              # WebView2 用户数据目录
├── dsh-distribution.json       # 环境的 dsh-distribution 描述符
└── .dpx-environment.json       # 实例身份和 DPX 注册记录的本地副本

其中 Windows 的 home\\Desktop 会在首次创建环境时一并建立;复用旧环境时 dpx 也会自动补齐,避免首次建立工作区时出现“位置不可用”。

因此,该环境的 DSH、缓存、配置、会话、插件 profile 与用户目录变量全部是独立的;它不会修改:

  • 系统或用户 npm 全局 prefix;
  • 默认 ~/.dsh、~/.agents;
  • 任何其他 DPX 环境。

desktop\ 与 EXE 仅会在 Windows 上默认创建;macOS/Linux 保持纯 CLI 环境。加入 --no-desktop 时 Windows 也不会创建它;此参数只在首次创建环境时生效,已有环境不会因后续 dpx npm install 而被意外添加、替换或删除桌面端。

npm 包的生命周期脚本仍以当前用户权限运行。目录隔离不是操作系统沙箱,不能把不可信包当作安全的执行环境。

在已有环境中继续安装

环境已经注册后,不再需要传存储目录:

dpx npm install -g @deepseek-ai/dsh @deepseek-harness-tui/dsh-tui --test

也可以只安装/升级一个包:

dpx npm install -g @deepseek-harness-tui/dsh-tui --test

DPX v0.1 只接受 npm 的全局安装语义(-g / --global),并自行固定环境专属的 --prefix 与 --cache;调用者不能覆盖这两个值。这两个旗标是命令行显式参数,DPX 不通过 NPM_CONFIG_* 改写 npm 的默认行为(原因见「npm 在隔离环境里是原生的」)。

Windows 桌面端(默认创建,与 DSH 本体完全解耦)

首次创建的每个 Windows 环境都包含:

<环境根>\desktop\DSH DeepSeek Harness Desktop.exe
<环境根>\desktop\.dpx-desktop.json      # dpx 记录的启动器版本与 sha256

双击该 EXE 后,它只做四件事:

  1. 从自身路径的父目录推导环境根(可用 DSH_DESKTOP_ENV 临时覆盖);
  2. 在该环境的 npm-prefix\node_modules\<包名>\package.json 里读取包自己声明的 bin 入口并启动它——不硬编码 lib/bin.js、不读 DPX registry、不调用 dpx、不依赖任何 DSH 内部文件布局;
  3. 只从子进程输出中解析就绪 URL:优先取官方 dsh web: <url> 行,也接受任何回环地址 URL,因此 DSH 改写日志措辞不会让已安装的启动器失效;
  4. 关闭窗口默认缩小到右下角托盘图标并继续运行,托盘图标右键可打开设置或关闭程序。

启动参数默认 web --no-open --port 0。如果将来 DSH 改变了入口位置或 CLI 参数形状,可以在环境根放一份启动契约,无需重新编译启动器:

// <环境根>\desktop-state\shell.json
{
  "dshPackage": "@deepseek-ai/dsh",
  "dshEntry": "npm-prefix/node_modules/@deepseek-ai/dsh/lib/bin.js",
  "launchArgs": ["web", "--no-open", "--port", "0"],
  "node": "C:\\Program Files\\nodejs\\node.exe",
  "extraEnv": { "EXAMPLE": "1" }
}

因此“升级 DSH 本体”和“升级 desktop 封装”是两件互不影响的事:

# 只替换环境内的 DSH npm 包;不重建、不替换桌面启动器
dpx npm install -g @deepseek-ai/dsh@latest --test

# 只替换 desktop 封装(Windows 启动器);不动 DSH 包
dpx desktop update --test

因此,desktop 启动的 DSH 全局 AGENTS.md 也放在:

<环境根>\dsh-home\AGENTS.md

这份文件由 dpx 自动写入(内容见「环境级 AGENTS.md」),桌面端、TUI 与 Web 读的是同一份;desktop EXE 本身没有另一份独立的全局 Agent 指令文件。

EXE 本身不内嵌 Node 或 DSH,运行时需要已安装 Node.js 和 Windows WebView2(Windows 11 通常自带)。桌面外壳的可控状态也完全按环境隔离:设置位于 <环境根>\desktop-state\settings.json,启动日志位于 <环境根>\desktop-state\shell.log,更新暂存位于 <环境根>\desktop-state\updates\,WebView2 用户数据位于 <环境根>\desktop-state\webview2,不会使用共享的 %LOCALAPPDATA%\dsh-dpx-desktop 目录。

关闭行为与托盘

位置行为
右上角 □ X按设置执行:缩小到托盘图标并保持运行(默认)/ 关闭程序 / 每次询问
首次点击 □ X弹出与 Web UI 同风格的确认框:默认勾选“缩小到右下角托盘图标并保持运行”和“下次不再提醒我”
托盘图标左键恢复主窗口
托盘图标右键打开主窗口 / 设置 / 重启 DSH 服务 / 关闭程序
设置 → 关闭窗口随时在“缩小到托盘图标 / 关闭程序 / 每次询问”之间切换
设置 → 服务 → 在本环境中打开终端打开一个 cmd 窗口,其 PATH / DSH_HOME / DSH_DPX_ENV* 与桌面端启动 DSH 时完全一致(工作目录为 <环境根>\workspace),用来当场确认一条命令命中哪一套副本

“关闭程序”会回收该环境内的 DSH 子进程树;缩小到托盘只是隐藏窗口,DSH 仍在后台运行。

环境之间的隔离(重要)

每个环境的桌面启动器都是同一个文件名、同一个可执行文件副本,所以“多个环境互不影响”必须由启动器自己保证,而不是靠文件名区分。当前实现保证:

资源归属说明
单实例互斥每个环境一份用 <环境根>\desktop-state\shell.lock 的独占文件锁;不使用 Tauri 单实例插件的全局互斥量(它按 bundle identifier 命名,会让不同环境互相排斥、并互相把窗口弹到前台)
第二次启动同一环境恢复已有窗口通过 <环境根>\desktop-state\instance.json 里的 {pid, hwnd} 还原窗口,绝不启动第二个 DSH 服务,避免污染同一 DSH_HOME
两个不同环境可同时运行互斥键来自环境根,环境 A 与 B 完全不知道对方存在
DSH 子进程生命周期绑定启动器子进程被放入一个 JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE 作业对象:启动器无论是正常退出、崩溃还是被任务管理器强杀,操作系统都会一并终止该 DSH 进程树,不会留下占用会话写句柄的孤儿进程
设置 / 日志 / 托盘状态 / WebView2 数据 / 更新暂存每个环境一份全部位于 <环境根>\desktop-state\,不使用任何共享目录

升级说明:0.1.0 的启动器使用 Tauri 单实例插件,因此两个环境不能同时运行(后启动的那个会立即退出,并把先启动的窗口弹到前台)。0.2.0 起改为上面的按环境隔离实现;旧环境的 EXE 用 dpx desktop update --<环境名> 升级即可。

0.2.5 起,启动器还会给 DSH 子进程设置环境身份变量(DSH_DPX_ENV / DSH_DPX_ENV_ROOT), 并在设置窗口提供“在本环境中打开终端”。环境的 dsh-home\AGENTS.md 里写明了它们的用途; 用 dpx desktop update --<环境名> 升级,重启后生效。

桌面封装的更新(GitHub Release)

desktop 封装有自己独立的版本号与发布通道,不随 DSH 本体变化:

# 查看当前环境的启动器版本与摘要
dpx desktop status --test

# 检查 GitHub Release 上是否有新版本(只读,不写任何文件)
dpx desktop check --test

# 下载、校验并替换启动器
dpx desktop update --test

# 为 --no-desktop 创建的环境补装启动器
dpx desktop install --test

也可以在托盘图标右键 → 设置 中点击“检查更新 / 立即更新”。两条路径共用同一份清单契约,详见 docs/desktop-release.md。

默认源是 github:T-Auto/dsh-dpx(即 https://github.com/T-Auto/dsh-dpx/releases/latest/download/desktop-latest.json)。--source 也接受:

dpx desktop update --test --source github:T-Auto/dsh-dpx@desktop-v0.2.0   # 指定 tag
dpx desktop update --test --source https://example.com/desktop-latest.json # 自建清单
dpx desktop update --test --source .\dist\desktop-latest.json              # 本地清单

下载内容必须通过清单里的 size 与 sha256 校验才会被安装;校验失败会拒绝安装并保留原启动器。需要代理时用 --proxy,或依赖环境里的 HTTPS_PROXY / ALL_PROXY;桌面端只在显式配置了 updateProxy 设置或 DPX_HTTP_PROXY / HTTPS_PROXY / ALL_PROXY / HTTP_PROXY / DEFAULT_PROXY 环境变量时才走代理(Cargo.toml 虽为 ureq 启用了 win-system-proxy feature,但 update.rs 没有调用系统代理 API,因此是否默认读取 Windows 系统代理未经验证)。

发布清单可以带一个可选的 sig(Ed25519,签的是产物摘要而不是清单字节)。默认不校验,行为与现在完全一致;配置了受信公钥(DPX_DESKTOP_PUBLIC_KEY / DPX_DESKTOP_PUBLIC_KEYS)之后转为严格模式:签名不符、或清单没有 sig,都会被拒绝安装。公钥随 npm 包内的 assets/windows/ 分发,私钥不参与构建与发布,详见 docs/desktop-release.md。

环境描述符将 ./desktop 声明为 DPX 专属桌面启动器目录(公共协议的受限相对路径语法不允许以含空格的 EXE 文件名作为资源位置)。

源码中保留了透明、可复现的 Tauri 构建目录 desktop-shell/。发布包包含预构建 x64 EXE,因此普通 dpx 用户无需安装 Rust/Tauri;维护者发布新版本时按下面的顺序操作:

# 1. 重建内置产物 assets\windows\* 与发布目录 dist(一次构建同时写出两份,字节相同)
npm run desktop:release

# 2. 提交刷新后的 assets\windows\*,再打 tag —— CI 会拒绝"包内清单版本与 tag 不符"的发布
git add assets/windows; git commit -m "chore(desktop): rebuild the packaged launcher"

# 3. 生成 SBOM(CI 也会做;本地只为核对)
npm run desktop:sbom

# 4. 两步发布:先 draft + 回执,核对远端摘要后再转正式
npm run desktop:upload
npm run desktop:publish

npm run desktop:build 是不带 -OutputDirectory 的同一路径,只刷新 assets\windows\*。 一次构建同时写出两份产物(包内 assets\windows\* 与发布目录 dist),因此它们的字节相同;-SkipPackagedArtifact 会破坏这一点,发布脚本会直接拒绝与包内清单摘要/版本不一致的目录,CI 也会断言两份清单指向同一 sha256。 CI 不能提交文件,所以它另加了一道版本门禁:tag 的版本必须与已提交的 assets\windows\desktop-manifest.json 版本一致,否则拒绝发布——dpx desktop install 读的正是 npm 包里的这份包内清单,这道门禁把"人工忘记重建、包内仍是旧版本"从静默装上旧二进制变成硬失败。 仍未闭合的残余:CI 是重新编译,而重新编译不保证逐字节可复现,所以同一版本下 npm 包里的 EXE 与 Release 里的 EXE 理论上仍可能不同字节;这时 CI 只发 ::warning 提示"重建并提交 assets/windows\*",不 fail(不可复现的差异不能当门禁)。 旧的无引用脚本 scripts\pack-desktop-release.ps1 已删除:它的"只拷贝不重建"会重新引入第二份载荷来源,与上面"一次构建写出两份"的收敛目标冲突。 CI 不持有签名私钥:签名是离线步骤,公开的部分只有公钥(见上文)。

该脚本会从 desktop-shell\src-tauri\rust-toolchain.toml 读取 Rust 工具链(不再用会话级 RUSTUP_TOOLCHAIN 覆盖它),并把 --locked 透传给 cargo,使构建在 Cargo.lock 过期时失败而不是静默升级依赖。它还会在存在时启用本机 D:\DevEnvs\Rust 工具链,下载走 -Proxy / DPX_BUILD_PROXY;版本号同时写入编译期常量(DPX_DESKTOP_VERSION),所以启动器总能报告自己的真实版本。图标来源为 whale-app-icon.ico,会在构建时明确覆盖 Tauri 的 Windows 原生图标资源。

启动隔离环境

当前 DPX 原生支持:

# 启动 test 环境内的 TUI
dpx run --test dsh-tui

# 启动 test 环境内的 Web UI
dpx run --test dsh web --no-open

# 将参数原样转发给 test 环境的 DSH
dpx run --test dsh --version

# 在 test 环境里跑任意命令(这里不限于 DSH/TUI)
dpx exec --test -- npm ls -g --depth=0
dpx exec --test --cwd D:\some\checkout -- git status

# 先问“裸敲某个名字会命中谁”,再决定怎么跑(只读,不启动任何东西)
dpx which --test
dpx which --test dsh-tui

# 一次性体检:registry / 布局 / PATH 冲突 / 双侧版本 / profile 的安装器与 store
dpx env doctor --test

# 把“当前 shell”切进 test 环境(dpx 不改父进程,只打印可求值的脚本)
dpx env use --test --format powershell | Invoke-Expression
dpx env use --test --format cmd

每次 dpx run / dpx exec 都会为子进程设置环境专属的绝对路径:

DSH_HOME=<环境根>\dsh-home
DSH_AGENTS_HOME=<环境根>\agents-home
DSH_DPX_ENV=<环境名>                  # 环境身份:我是哪一个环境
DSH_DPX_ENV_ROOT=<环境根>             # 环境身份:环境根在哪
DPX_HOME=<DPX registry home>          # 让环境内再启动的 dpx 找到同一个 registry
HOME / USERPROFILE / APPDATA / LOCALAPPDATA / TEMP / TMP
XDG_CONFIG_HOME / XDG_CACHE_HOME / XDG_DATA_HOME
PATH=<环境根>\npm-prefix;…

DSH_DPX_ENV / DSH_DPX_ENV_ROOT 是环境身份:DSH_HOME 只说明 DSH 的状态在哪,除了 DSH 自己没人读它, 也无法据此区分“宿主机”和“某个环境”。这两个变量让任何进程——包括 agent 在环境里再启动的 dpx—— 都能直接回答“我在哪个环境里”,而不必从路径反推。

⚠️ 不要假设这两个变量一定到得了你手里。 DSH 的 shell / 终端层会为它交给 agent 的子进程 重建 DSH_* 命名空间,只保留它自己声明过的键(DSH_HOME 等)。实测:在 dpx 环境里跑 agent 的 shell, 能看到 DSH_HOME,但 DSH_DPX_ENV_ROOT 和 DSH_AGENTS_HOME 都已被丢掉。 因此 dpx 判定“我在哪个环境里”靠的是结构化证据(见下文 registry 解析顺序), 而 dpx env doctor 的 process-identity 会同时报出结论与依据(identity-variable / dsh-home / isolated-localappdata)。判断自己在哪一套,请看这条结论,不要只看某个变量是否为空。

注意这里没有 NPM_CONFIG_PREFIX / NPM_CONFIG_CACHE:dsh-dpx 不再劫持 npm 的默认值(见下文「npm 在隔离环境里是原生的」)。

其中两者职责不同:

  • DSH_HOME 是该隔离环境的 DSH 状态与配置根目录;DSH 的用户全局指令文件固定读取 DSH_HOME\AGENTS.md。因此,若要为某个 DPX 环境增加个人全局系统提示词/Agent 指令,应写入:
    <环境根>\dsh-home\AGENTS.md
    
  • DSH_AGENTS_HOME 是该环境的 agents / skills 专属目录;它不是用户全局 AGENTS.md 的读取位置。
  • 直接运行非 DPX 隔离的 DSH 时,等价的默认位置通常是 %USERPROFILE%\.dsh\AGENTS.md;显式设置 DSH_HOME 后则以该变量为准。

上述 DSH_HOME\AGENTS.md 是环境级全局指令;实际 workspace 或项目目录中的 AGENTS.md 仍会按 DSH 的目录发现规则加载,并以更具体的项目规则为准。

同时会清除可能污染环境的 NODE_OPTIONS、NODE_PATH 及常见代理变量,并设置 DSH_TELEMETRY_DISABLED=1。DPX 的 npm install 默认显式传入 --proxy=null --https-proxy=null,覆盖用户 npmrc;因此创建、安装和升级隔离环境默认直连,不会自动走本机 Clash 127.0.0.1:7897。

若某次安装确实需要代理,调用者必须在该次命令中明确写入 npm 标准参数;DPX 不保存、更不内嵌任何代理端口:

dpx npm install -g @deepseek-ai/dsh --desktop --"D:\AIPC" --proxy=http://127.0.0.1:7897 --https-proxy=http://127.0.0.1:7897

该代理仅用于本次 npm 安装;桌面 EXE 和后续 dpx run 不会继承它。

TUI 首次自举会调用 DSH 的 plugin 子命令,而该子命令需要 pnpm 可在 PATH 中找到。若尚未安装 pnpm,请先执行:

npm install -g pnpm
# 或
corepack enable pnpm

npm 在隔离环境里是原生的

隔离环境不劫持 npm:dsh-dpx(dpx run)与桌面启动器都不再设置 NPM_CONFIG_PREFIX / NPM_CONFIG_CACHE,npm 按原生规则工作。而环境的 APPDATA / LOCALAPPDATA 本身是被隔离的,所以 npm 的原生默认落点也在环境内,只是落在另一个目录,不是 dpx 管理的那个:

你问的答案
npm prefix -g / npm root -g<环境根>\appdata\npm(原生默认 = %APPDATA%\npm)
npm config get cache<环境根>\localappdata\npm-cache(= %LOCALAPPDATA%\npm-cache)
dpx / DSH 真正使用的环境 npm 目录(PATH 第一项,dsh、dsh-tui 所在处)<环境根>\npm-prefix
dpx 管理的环境 npm 缓存<环境根>\npm-cache
  • 裸跑 npm install -g <pkg> 不会污染宿主机,但它装进 <环境根>\appdata\npm:该目录不在 PATH 上,也不是 dpx、DSH 或桌面端查找包的位置——装在那里等于没人看得见。

  • 所以要往环境里装东西(能被 dpx run、dsh、桌面端看到),必须显式给出路径与缓存:

    npm install -g --prefix "<环境根>\npm-prefix" --cache "<环境根>\npm-cache" <包名>
    

    或使用等价的封装(推荐,dpx 内部就是把这两个旗标拼上去):

    dpx npm install -g <包名> --test
    

    dpx npm install 只接受全局安装语义(-g / --global),并自行固定 --prefix 与 --cache;调用者不能覆盖这两个值。这样即使宿主或其他工具设置了 NPM_CONFIG_*,dpx 的一次安装也不会被静默改道。

之所以改成显式旗标,是因为“环境变量重定向 npm 默认值”这种收容方式会让同一台机器上的其他 npm 调用也被改道:只要从隔离环境里派生的任何进程执行 npm i -g,它就会装进隔离环境的 npm-prefix,而不是宿主全局目录——这既反直觉,也让宿主全局 npm 目录变得难以解释。现在 npm 只按自己的原生规则解析,而“装进 dpx 管理的环境目录”只由显式写路径的那条命令完成。

已存在的环境:dpx run 用的是 dpx 自己的代码,升级 dpx 后立即生效;桌面端是独立二进制,需要用 dpx desktop update --<环境名> 换上包含该修改的版本。环境级 AGENTS.md 里写明了自查方法(NPM_CONFIG_* 应当为空)。

环境级 AGENTS.md(dpx 自动写入,所有启动方式都会读到)

创建环境时(以及每次复用/升级已有环境时),dpx 会写入并刷新:

<环境根>\dsh-home\AGENTS.md

内容由环境自身布局生成:

  • 先确认你在哪一套里:环境身份变量、dpx which / dpx env doctor 的用法,以及“裸敲命令名命中环境外副本”时会发生什么;
  • 这个隔离环境是怎么设计的:npm-prefix / npm-cache / dsh-home / profiles / agents-home / home / appdata / tmp / xdg-* / workspace / desktop 各是什么、哪个环境变量指向它;
  • 怎么往这个环境里装东西:npm 全局包(dpx npm install 或显式 --prefix + --cache)与 profile 插件(dpx plugin add,含 store 约定),并给出可直接复制的命令;
  • dpx 通用开发规则:不要假设默认解析落在环境内、跨环境操作必须显式指名环境、区分全局副本与 profile 副本、不要改宿主机与其他环境、报错先取证;
  • 已知失败模式:装包不生效 / ERR_PNPM_UNEXPECTED_STORE / launcher ↔ profile 版本不一致 / 环境内看不到别的环境;
  • 边界:隔离只收容默认解析、不是沙箱,以及不要动其他环境。

这份生成块是随包发布的内容:它只讲 dpx 的通用规则,所有具体路径都来自它所描述的那个环境(渲染时注入), 不写死任何主机路径、检出位置或某个具体产品。回归测试 test/identity.test.js 用合成环境根断言了这一点。

因为 DSH_HOME 指向该目录,不管环境是怎么启动的——dpx run --test dsh web、dpx run --test dsh-tui、还是双击 <环境根>\desktop\DSH DeepSeek Harness Desktop.exe——DSH 都会把这同一个文件当作环境级全局指令读进来。

该文件由 dpx 托管:<!-- dpx:environment-guide:begin … --> 与 <!-- dpx:environment-guide:end --> 之间的内容会自动刷新,你自己写的全局指令放在标记块之外即可,不会被覆盖。用 --no-desktop 创建的环境同样会得到这个文件。

「我现在用的是哪一套?」:dpx which 与 dpx env doctor

隔离的三个面(PATH / npm-prefix / DSH_HOME)以前只在 dpx 进程内是绑在一起的:一旦离开 dpx run, 这层绑定就消失了,终端里裸敲 dsh / dsh-tui 命中的是 PATH 里第一个同名文件——可能是宿主机那份, 它会连带使用宿主机自己的 DSH 状态,于是“我在环境里装了东西,界面却没变化”。

两条命令把这个知识变成可查询的事实,都不启动目标、都不读网络:

dpx which --test              # 每个已识别启动目标:环境内副本 + PATH 上会命中谁
dpx which --test dsh-tui      # 只看一个目标
dpx env doctor --test         # 环境自洽性体检(退出码非零 = 有 error 级问题)

dpx which 的输出包含:

字段含义
isolateddpx run --test <target> 会直接执行的入口(绕过 PATH 与 shim)及其版本
copies该目标在 npm-prefix 与每个 profiles\<profile> 中的全部副本及版本
ambientPathPATH 上每个同名文件,按命中顺序;winner 标出真正会跑的那个,inEnvironment 标出它是否在本环境内
verdictclean / host-leak / not-installed
advice可直接照做的一行修法(命中环境外副本时,会连它将要使用的 DSH_HOME 一起报出来)

dpx env doctor 逐项检查并把每条结论都配上一句可执行的修法:

检查说明
registry-binding / layoutregistry 记录、环境根、DSH_HOME、npm-prefix 是否互相对得上;受控布局是否齐全
environment-guidedsh-home\AGENTS.md 是否存在且为当前格式(旧格式只提示,不报错)
process-identity当前进程属于哪个环境,以及依据(identity-variable / dsh-home / isolated-localappdata);身份变量被中间层丢掉时会明说,而不是据此断言“你不在环境里”
registry-membership当前 DPX registry 里是否登记了这个环境
path-shadowing:<target>PATH 上是否同名副本会抢在环境之前(宿主机泄漏)
target-copies:<target>全局副本与各 profile 副本的版本是否一致(不一致 = 今天 launcher ↔ profile 报错的根因)
profile-store:<profile>profile 的 node_modules 由哪个包管理器装、链接自哪个 store;store 落在环境外时直接给出 ERR_PNPM_UNEXPECTED_STORE 的预防性修法

在环境里跑命令:dpx exec 与 dpx env use

dpx run 覆盖 dpx 已知的启动目标;dpx exec 覆盖其余一切(npm / pnpm / node / git / 又一个 dpx), 子进程拿到与 dpx run 完全相同的环境:

dpx exec --test -- npm ls -g --depth=0
dpx exec --test --cwd D:\some\checkout -- pnpm install

dpx 不能修改父 shell 的环境(这是操作系统的事实,不是实现偷懒),所以“让当前终端进入环境”由 dpx env use 打印一段可求值的脚本完成:

# PowerShell
dpx env use --test --format powershell | Invoke-Expression
# cmd
for /f "delims=" %i in ('dpx env use --test --format cmd') do @%i
# 机器可读(给 agent / 脚本用)
dpx env use --test --format json

脚本包含 DSH_HOME / DSH_AGENTS_HOME / 环境身份 / DPX_HOME / HOME / XDG_* 等赋值, 清除 NODE_OPTIONS / NODE_PATH / NPM_CONFIG_*,并按 “前置、不替换” 的约定把 <环境根>\npm-prefix 加到求值那一刻的 PATH 前面。dpx 自己永不修改父进程环境。

往 profile 里装插件:dpx plugin add

DSH 的 profile 插件由 dsh plugin --profile <p> add … 转发给 pnpm,并在安装后重建 profile 的 bundle 层; dpx 不绕过它,而是补上它无法知道的那一条上下文——这个 profile 的 node_modules 原本链接自哪个 store:

dpx plugin add --test <包名[@版本|tarball路径]> --profile dsh-tui
dpx plugin add --test <包名> --profile dsh-tui --dry-run   # 只打印将执行的命令与 store
dpx plugin add --test <包名> --profile dsh-tui --store-dir "C:\existing\store\v11"
  • store 的取值顺序:--store-dir 显式指定 → 从 profiles\<p>\node_modules\.modules.yaml 读到的既有 store → 环境默认 store(<环境根>\xdg-data\pnpm\store);
  • 安装后回读真实安装结果:profiles\<p>\package.json 的每个依赖在 profile 内与 npm-prefix 内的版本、以及两者是否一致;
  • 失败时不再把 pnpm 的原文错误直接抛给调用者,而是附上 dpx env doctor 的排查入口与保持既有链接的修法。

隔离的边界:收容「默认解析」,不是写入沙箱

runtimeEnvironment() 保证的是默认路径解析落在环境内。只要调用方不显式指定绝对路径,pnpm、DSH 与各类工具的默认读写都会落在 <环境根> 下:

资源环境内位置
用户 home<环境根>\home(同时作为 HOME / USERPROFILE)
APPDATA / LOCALAPPDATA / TEMP<环境根>\appdata、<环境根>\localappdata、<环境根>\tmp
XDG 三件套<环境根>\xdg-config、<环境根>\xdg-cache、<环境根>\xdg-data
pnpm store<环境根>\xdg-data\pnpm\store
DSH 状态与配置<环境根>\dsh-home、<环境根>\agents-home
环境身份DSH_DPX_ENV、DSH_DPX_ENV_ROOT(任何进程都能据此回答“我在哪个环境里”)
DPX registry homeDPX_HOME 显式传入;未传时按平台默认,并在环境内回落到本机发现指针(见下)
npm 原生默认 prefix / cache<环境根>\appdata\npm、<环境根>\localappdata\npm-cache(环境内,但不在 PATH 上)
dpx 管理的 npm prefix / cache<环境根>\npm-prefix、<环境根>\npm-cache(需显式 --prefix / --cache,见上一节)

DPX registry 在环境内仍然可达

环境的 LOCALAPPDATA 是隔离的,而 DPX registry 的默认位置正是 %LOCALAPPDATA%\DSH\DPX—— 于是“在环境里再启动一个 dpx”会看到一个私有的、空的 registry,一个环境都列不出来, 这与“多环境互相开发”的目标正好相反。因此 registry home 的解析顺序是:

  1. DPX_HOME(显式设置,永远优先;dpx run / dpx exec / dpx env use 都会把它传给子进程);
  2. 平台默认位置,若该处已存在 registry.json;
  3. 若当前进程能证明自己在某个 dpx 环境里而第 2 步没有命中,则回落到本机的 DPX 发现指针 HKCU\Software\DSH\DPX\RegistryPath 所指向的那个 registry。

第 3 步的“证明”不依赖单个环境变量,因为它可能被中间层丢掉(见上文警告)。判定顺序是:

依据判据
DSH_DPX_ENV_ROOT该目录下存在声明 kind: DPXEnvironment 的 .dpx-environment.json
DSH_HOME其父目录同样带这份清单(即 DSH_HOME 是 <环境根>\dsh-home)
隔离的 LOCALAPPDATA其父目录同样带这份清单(即 <环境根>\localappdata)

宿主机三者都不成立:~/.dsh 不是 <环境根>\dsh-home,单纯同名的目录没有清单, 失效的指针也不算证据——所以这套反推不会把宿主 shell 误判成环境。

dpx env doctor 会报出当前使用的 registry home、process-identity 的结论与依据,以及这个环境是否登记在其中。

三条命令即可确认当前的实际落点:

pnpm store path  # <环境根>\xdg-data\pnpm\store\v11
$HOME            # <环境根>\home
npm root -g      # <环境根>\appdata\npm\node_modules(npm 原生默认,不是 npm-prefix)

但隔离的机制是环境变量重定向,不是文件系统边界。以下三点不在保证范围内:

逃逸口机制表现
显式绝对路径命令行参数优先于环境变量npm install -g --prefix C:\Users\... <pkg> 直接写入宿主;--cache、--location 同理
PATH 是前置而非替换env.PATH = [paths.npmPrefix, inherited.PATH]环境内没有的工具会静默回落到宿主的同名二进制。环境内只装了 dsh 时,dsh-tui、pnpm 很可能解析到 %APPDATA%\npm 下的宿主副本
无写入拦截DPX 是环境管理器,不挂文件过滤驱动拥有写权限的进程仍可写宿主任意绝对路径

PATH 前置只是让已经显式装进环境的二进制优先解析,并不改变 npm 的默认安装目标。想确认某个工具来自环境内还是宿主,看 Get-Command <名字> 解析到的路径,不要看版本号。

实践建议:

  • 要往环境里装包,永远写全 --prefix 与 --cache,或用 dpx npm install -g <包名> --<环境名>。
  • 需要真正的文件系统边界时,请在本机沙箱/容器层面实现;DPX 只负责环境身份、受控布局与默认路径收容。

dsh-tui --test 统一启动体验

目标用户体验是:

dsh-tui --test

无论用户选择哪一种安装方式,都应优雅启动 test 环境的 TUI:

用户已有内容dsh-tui --test 应做什么
全局安装了 dsh-tui,但没有安装 dpx全局 TUI 启动器读取 DPX 发现 profile,定位 test,并委托给其中已安装的 TUI。
只安装了 dpx,TUI 只安装在 test 隔离环境通过 dpx/DPX registry 定位隔离 TUI 后启动,无需全局再安装一份 TUI 包。
全局与隔离环境都安装了 TUI显式 --test 永远优先启动 test 的隔离副本,不混用全局 DSH state。
test 不存在或没有安装 TUI输出简短、可执行的诊断和创建/安装命令,不扫盘、不猜测路径。

dsh-tui 已有全局启动器;为实现上述精确命令,需要在 dsh-tui 项目中接入一个小型 DPX 兼容适配器。DPX 已提供该适配器所需的稳定发现入口和记录格式。适配器应:

  1. 解析开头的 --<环境名>,如 --test;其余参数仍是普通 TUI 参数;
  2. 读取 HKCU\Software\DSH\DPX,只接受 Profile=dpx.dsh.dev/v1alpha1;
  3. 读取并校验指向的 DPX registry.json,精确匹配环境名;禁止扫盘,也不能执行 registry 提供的任意命令;
  4. 由环境 root 推导固定子路径(npm-prefix、dsh-home、agents-home 和已知的 TUI bin/dsh-tui.js),设置同样的隔离变量(DSH_HOME / DSH_AGENTS_HOME / 隔离 profile 目录;不设置 NPM_CONFIG_*)后委托;
  5. 找不到 DPX/环境/TUI 时给出 dpx run --test dsh-tui、创建环境或安装 TUI 的明确提示。

在该适配器合并到 dsh-tui 前,等价且已验证的命令是:

dpx run --test dsh-tui

按 dsh-distribution 注册环境

每次创建环境时,DPX 都会同时执行以下注册工作:

  1. 在环境根生成 dsh-distribution.json,声明 DistributionDescriptor、ManagedLayout 与 EnvironmentDiscovery;

  2. 为安装实例生成独立的 urn:uuid: EnvironmentInstance,因此同一发行物的 test 与 stable 绝不会被认作同一份安装;

  3. 在环境根的 .dpx-environment.json 保存实例与 DiscoverableEntry 形状的记录;

  4. 以原子方式维护 DPX 自己的可枚举 registry.json;

  5. 在 Windows 写入一个轻量、无执行权限的 discovery pointer:

    HKCU\Software\DSH\DPX
      Profile      = dpx.dsh.dev/v1alpha1
      RegistryPath = <DPX registry.json 的绝对路径>
    

默认 registry 位置:

%LOCALAPPDATA%\DSH\DPX\registry.json

也可以以绝对路径设置 DPX_HOME 改变 registry home。

dsh-distribution 的通用协议定义描述符、引用发现和可选 Lodgement 语义,但不规定所有产品必须使用某个固定目录、环境变量或 Windows Registry key。HKCU\Software\DSH\DPX 是 DPX 定义的 dpx.dsh.dev/v1alpha1 实现 profile:它让兼容工具无需扫描磁盘就能找到 DPX registry,但不代表通用协议已经标准化该物理位置,也不授予任何可执行权限或信任。

DPX 不会写入:

HKCU\Software\DSH\EnvironmentInstallations

该 key 属于 dsh-distribution-manager 的独立、固定发行环境安装器;DPX 不应混用或破坏它的验证/导入边界。

查看环境与注册信息

# 所有 DPX 已注册环境
dpx env list

# test 的隔离路径和实例 ID
dpx env show --test

# test 的环境自洽性体检(registry / 布局 / PATH 冲突 / 双侧版本 / profile store)
dpx env doctor --test

# test 里每个已识别启动目标:环境内副本 + PATH 上会命中谁
dpx which --test

# test 的 DistributionDescriptor、EnvironmentInstance、DiscoverableEntry 信息
dpx descriptor --test

registry 损坏、重复名称、实例身份冲突或环境 root 缺失时,DPX 会失败关闭(拒绝覆盖和猜测),而不是自动重建/扫描/接管目录。

开发、验证与发布边界

npm test
npm run check
npm run pack:check

测试只使用临时目录与 fake npm;不会下载上游包或启动真实 DSH profile。dsh-distribution.json 可用 dsh-distribution 仓库的 conformance CLI 验证:

node ..\dsh-distribution\packages\conformance\lib\cli.js "$PWD\dsh-distribution.json"

注销与清理环境

DPX 负责自己 registry 中环境记录的生命周期;不需要手动编辑 registry.json。可使用:

# 仅注销 registry 记录,保留环境文件
 dpx env remove --test

# 注销并删除该 DPX 环境根(含 npm、DSH、desktop-state 和桌面 EXE)
 dpx env remove --test --purge

--purge 只删除 registry 中已登记、且可由 DPX 受控布局推导验证的环境根;它不会扫描磁盘,也不会删除全局 Node/npm、默认 DSH 目录或其他环境。

项目链接