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。
它服务于两个目标:
- 开发与调试:一台电脑可以保留多个命名 DSH 环境,例如
test、stable、alpha;它们可安装不同版本的 DSH、TUI 或第三方包,互不污染,便于复现和比较问题。 - 未来的第三方整合包:整合包可按
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/conformanceCLI),应当得到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 后,它只做四件事:
- 从自身路径的父目录推导环境根(可用
DSH_DESKTOP_ENV临时覆盖); - 在该环境的
npm-prefix\node_modules\<包名>\package.json里读取包自己声明的bin入口并启动它——不硬编码lib/bin.js、不读 DPX registry、不调用dpx、不依赖任何 DSH 内部文件布局; - 只从子进程输出中解析就绪 URL:优先取官方
dsh web: <url>行,也接受任何回环地址 URL,因此 DSH 改写日志措辞不会让已安装的启动器失效; - 关闭窗口默认缩小到右下角托盘图标并继续运行,托盘图标右键可打开设置或关闭程序。
启动参数默认 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.mdDSH_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 <包名> --testdpx 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 的输出包含:
| 字段 | 含义 |
|---|---|
isolated | dpx run --test <target> 会直接执行的入口(绕过 PATH 与 shim)及其版本 |
copies | 该目标在 npm-prefix 与每个 profiles\<profile> 中的全部副本及版本 |
ambientPath | PATH 上每个同名文件,按命中顺序;winner 标出真正会跑的那个,inEnvironment 标出它是否在本环境内 |
verdict | clean / host-leak / not-installed |
advice | 可直接照做的一行修法(命中环境外副本时,会连它将要使用的 DSH_HOME 一起报出来) |
dpx env doctor 逐项检查并把每条结论都配上一句可执行的修法:
| 检查 | 说明 |
|---|---|
registry-binding / layout | registry 记录、环境根、DSH_HOME、npm-prefix 是否互相对得上;受控布局是否齐全 |
environment-guide | dsh-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 home | DPX_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 的解析顺序是:
DPX_HOME(显式设置,永远优先;dpx run/dpx exec/dpx env use都会把它传给子进程);- 平台默认位置,若该处已存在
registry.json; - 若当前进程能证明自己在某个 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 已提供该适配器所需的稳定发现入口和记录格式。适配器应:
- 解析开头的
--<环境名>,如--test;其余参数仍是普通 TUI 参数; - 读取
HKCU\Software\DSH\DPX,只接受Profile=dpx.dsh.dev/v1alpha1; - 读取并校验指向的 DPX
registry.json,精确匹配环境名;禁止扫盘,也不能执行 registry 提供的任意命令; - 由环境 root 推导固定子路径(
npm-prefix、dsh-home、agents-home和已知的 TUIbin/dsh-tui.js),设置同样的隔离变量(DSH_HOME/DSH_AGENTS_HOME/ 隔离 profile 目录;不设置NPM_CONFIG_*)后委托; - 找不到 DPX/环境/TUI 时给出
dpx run --test dsh-tui、创建环境或安装 TUI 的明确提示。
在该适配器合并到 dsh-tui 前,等价且已验证的命令是:
dpx run --test dsh-tui
按 dsh-distribution 注册环境
每次创建环境时,DPX 都会同时执行以下注册工作:
-
在环境根生成
dsh-distribution.json,声明DistributionDescriptor、ManagedLayout与EnvironmentDiscovery; -
为安装实例生成独立的
urn:uuid:EnvironmentInstance,因此同一发行物的test与stable绝不会被认作同一份安装; -
在环境根的
.dpx-environment.json保存实例与DiscoverableEntry形状的记录; -
以原子方式维护 DPX 自己的可枚举
registry.json; -
在 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 目录或其他环境。
项目链接
- 主仓库:https://github.com/T-Auto/dsh-dpx
- 私有备份仓库:
T-Auto/back-dsh-dpx - 环境协议与 conformance:
dsh-distribution