DSH HUB
首页插件商店插件包社区排行榜资源发布指南
插件源码
返回插件目录

ruanimal /

ruanimal/dsh-bash-env

已验证

为 dsh 的每条 shell 命令按工作目录解析环境(PATH / venv / mise),并带一个独立的 Web UI 设置分区。Resolves each command's environment from its own working directory, with a dedicated settings section.

★ 0 Stars0 Forks0 IssuesN/A 社区评分0 已确认安装
查看 GitHub
README来源: master@c971fd7e

dsh-bash-env

按 workdir 为 dsh 的每条 shell 命令计算环境:PATH 追加、任意环境变量、venv 自动激活、mise shims。带一个 Web UI 设置分区(设置 → 终端环境)。

A dsh profile plugin that replaces the built-in bash executor so every bash -c gets an environment resolved from its own working directory, plus a dedicated settings section in the Web UI.

它解决什么

dsh 的每条命令都是非登录、非交互的 bash -c(~/.bashrc、~/.bash_profile 一个都不读),而子进程的 PATH 来自启动 dsh 的那个进程——桌面 App 由 GUI 会话拉起时,那份 PATH 里通常没有 ~/.cargo/bin、mise shims 之类。于是在终端里能跑的命令,在 dsh 里"找不到命令"。

本插件不引入 shell 启动文件,而是用 dsh 官方扩展点替换 ctx.shell,在每次 spawn 前按该命令的 workdir 算出环境覆盖层。

两个配置入口,同一个 user 层

入口 适用
Web UI:设置 → 终端环境 日常调整;写入与 YAML 相同的 user 层(即 profile 的 cordis.patch.yml 里 id: bash-env 那一段)
~/.dsh/profiles/<profile>/cordis.patch.yml 批量/脚本化;改完存盘由 HMR 立即重组配置层

两者都改完即生效,不需要重启(配置层由 dsh-hmr 监听)。但插件代码改动不会被 HMR 重载(hmr.root 默认不监听模块文件),需要重启 harness。

配置必须扁平

设置表单只寻址单段路径(SettingsFormModel 的 path: [field]),所以一个嵌套块在浏览器里根本编辑不到。因此配置是每个开关一个顶层键:

# ~/.dsh/profiles/<profile>/cordis.patch.yml
- id: bash-env
  name: dsh-bash-env
  config:
    prependPath:
      - /home/you/.cargo/bin
    envFile: /home/you/.dsh/shell-env.conf
    envVars: {}
    venvEnabled: true
    venvFallback: /home/you/.venv
    miseEnabled: true
    miseShims: /home/you/.local/share/mise/shims
    miseAutoInstall: false
字段 默认 含义
prependPath [] 放在 PATH 前部的目录。不做存在性剔除(版本管理器首次安装后目录才出现),只在挂载时对不存在的项 warn
envFile '' 每行一条 KEY=VALUE 的文件;spawn 时读取,改完下一条命令即生效。设了但读不到 → 命令报错(明确可见,不静默)
envVars {} 内联变量,优先级高于 envFile
bashEnv '' 一个绝对路径;bash 在每条非交互命令前 source 它。留空即关闭
venvEnabled true 是否查找虚拟环境
venvNames ['.venv','venv'] 每层候选目录名
venvMaxDepth 3 从 workdir 向上查找的层数
venvStopAt $HOME 查找不进入该目录;家目录 venv 因此不会隐式生效
venvFallback '' 查找未命中时使用;不可用时保持不激活
miseEnabled true 是否启用 mise 支持
miseShims ~/.local/share/mise/shims shim 目录,加入 PATH
miseAutoInstall false false 时注入 MISE_AUTO_INSTALL=false / MISE_NOT_FOUND_AUTO_INSTALL=false
miseHookEnv false 打开后每个目录问一次 mise hook-env,拿到 JAVA_HOME/GOROOT/GOBIN 与项目 [env] 段
miseBin '' mise 可执行文件;留空用 PATH 上的 mise
cwd / timeoutMs / maxTimeoutMs / maxOutputBytes / maxSpillBytes / graceMs — 继承内置 executor 的预算字段

环境合成顺序:

PATH = <venv>/bin : prependPath... : <mise shims> : 继承的 PATH    (首次出现优先,自动去重)
VIRTUAL_ENV = <按 workdir 检测到的 venv>
MISE_AUTO_INSTALL / MISE_NOT_FOUND_AUTO_INSTALL = 'false'
envVars 与 envFile 最后应用(可覆盖上面的值)

venv 排在 prependPath 之前是刻意契约:否则版本管理器的 python3 shim 会压过 venv,出现 VIRTUAL_ENV 指向 A、实际解释器是 B 的静默错误。

覆盖层落在 executor 自身覆盖(NO_COLOR / TERM=dumb / PAGER)和管理事实 DSH_* 之上,随后 spec.dshEnv 再压一次——配置永远无法遮蔽托管事实。

环境变量文件格式

# 注释与空行忽略;行首可写 export
CARGO_HOME=$HOME/.cargo
PATH=$PATH:/opt/tools
LITERAL='$HOME 不被展开'
  • $VAR / ${VAR} 相对于该命令继承到的环境展开,也可引用同文件中更早的行
  • 单引号包裹的值不做展开
  • 变量名必须合法;DSH_* 前缀被拒绝(harness 自己的命名空间)
  • 某一行格式不对 → 命令带着 文件:行号 的明确报错失败,改好即恢复

mise 的派生变量(JAVA_HOME / GOROOT / GOBIN)

shims 回答的是"运行哪个可执行文件",它回答不了"该用哪个 JAVA_HOME"——那是变量,不是文件。所以 miseHookEnv 走另一条路:每个目录问一次 mise hook-env,拿到完整答案(派生变量 + 项目自己的 [env] 段),并按目录缓存。

为什么值得(实测,10 次平均):

成本
mise hook-env(每条命令) ~20ms,缓存命中后 ≈0
一次 shim 调用 ~30ms

一条命令里调 node -v + npm -v + java -version,用 shims 要付 ~90ms;用 hook-env(缓存后)是 0,而且顺带得到 JAVA_HOME。成功时不再叠加 shims——那会把刚省下的成本加回去。

缓存键:(规范化 workdir, mise 配置指纹)。指纹是沿目录链 stat mise.toml/.mise.toml/mise.local.toml/.tool-versions 加上 ~/.config/mise/config.toml 的 mtime+size,另配 5 分钟 TTL 与 64 条 LRU 上限(否则长时间会话会攒下无数目录)。mise install 改了配置 → 指纹变化 → 自动重问。

失败一律回落 shims,不让命令失败:mise 未安装、探测超时(3s 上限)、退出码非零、或输出里没有可用变量——四种都归到 shims-fallback。走哪条路由可以从托管事实看到:

echo "$DSH_MISE"      # hook-env | shims | shims-fallback

两个必须知道的点:

  1. hook-env 是 mise 的内部接口(帮助里写着 [internal] called by activate hook),版本间可能变。所以解析是"看不懂就忽略"、输出为空即视为失败,整个功能默认关闭。
  2. 项目的 [env] 段属于这份答案,也就是说仓库里的一个文件可以为在其中执行的每条命令设置变量。这是它默认关闭的真正原因——要开请自己确认那些仓库可信(mise 自身的 trust 门也会拦未信任的配置)。

默认关闭。打开方式:设置 → 终端环境 → 「向 mise 询问当前目录解析出什么」;mise 不在 PATH 上时再填 miseBin。

BASH_ENV / shell 启动文件

bashEnv 是唯一的"逃生舱",也是本插件唯一注入代码而非数据的设置。它解决变量表达不了的事:

能做 不能做
定义 shell 函数(非交互 shell 里可用 ✅) 定义 alias(非交互 shell 不展开 alias,实测 type rm → /usr/bin/rm)
设置 shell 选项、shopt 期望它输出任何东西——文件里的打印会混进每条命令的输出
执行工具自带的激活脚本(nvm/conda 等) 承担重活:它在每条命令上都会执行,嵌套 shell 还会再执行一次

语义:bash 只为非交互 shell source BASH_ENV——而 dsh 的每条 bash -c 正是非交互的。(bash --rcfile 是另一回事:它隐含 -i,会引入提示符与回显,所以没有采用。)它在本插件组装好的环境之后运行,因此对 PATH 有最终决定权;想让显式变量赢,把 BASH_ENV 写进 envVars 即可。

校验:bash 对一个读不到文件的 BASH_ENV 是静默忽略的,所以插件自己校验——非绝对路径、文件不存在、或不是普通文件,都会让命令带着原因失败,而不是看起来配了却没生效。

默认关闭。要打开就在设置页填这个字段(或写进 YAML),指向类似 ~/.dsh/shell-startup.sh 的小文件。模板里只放了注释,需要什么自己加。

为什么继承 SandboxBashExecutor

  • 它 extends LocalBashExecutor,所以超时、输出上限、spill、后台作业语义全部原样保留;
  • 它是施加会话文件策略的那个 executor。改为继承 LocalBashExecutor 会静默丢掉 workspace-write 沙箱;
  • 唯一覆写点是 spawnSpec(),前台/后台、受限/完全访问全部经过它。

ctx.shell 每个 context 只允许一个实现,所以 bundle patch 必须先禁用内置行。

诊断走命令错误,不走日志

ctx.logger.warn 在无头组合里没有落盘/落终端(App 捕获的 dsh-web.log 里既没有警告也没有错误),所以它不是一个能被看到的通道。因此配置错误在 spawnSpec 里抛出:

  • 键名拼错 → 每条命令失败,消息形如 invalid config:\n - $.venvFalback is not a known option; did you mean "venvFallback"? executor 仍然挂载,改回配置即可恢复,不需要重启。
  • 类型写错 → 由 schema 校验在挂载时报错(ValidationError: $.prependPath expected array but got ...),这一条会让该行不挂载,也就是暂时没有 ctx.shell。
  • env 文件问题 → 命令失败,消息带文件名与行号。

安装

# 1. 建立宿主包链接(见下)
cd /path/to/dsh-bash-env && node scripts/link-host.mjs

# 2. 链接进 profile 并加入 bundles
dsh plugin --profile <profile> add link:/path/to/dsh-bash-env
# 再把 "dsh-bash-env" 加进 ~/.dsh/profiles/<profile>/package.json 的 dsh.profile.bundles

# 3. 在 profile 的 cordis.patch.yml 里写站点配置(见上),或用 Web UI

本机注意:dsh plugin add 会因缺 pnpm 失败(App 的 runtime 里只有 corepack/node/npm/npx),GUI 插件管理器同理。手工等价写入即可:profile 的 node_modules/<name> 相对符号链接 + package.json 依赖与 bundles + pnpm-lock.yaml 的 importer 登记。

为什么必须有 link-host

用 link: 安装的插件,其 realpath 在 profile 树之外。dsh 的加载器对 linked 插件是从该 realpath 自身的祖先 node_modules 解析裸导入的(@deepseek-ai/dsh-app-boot 的 routeLinked),而 ~/projects/... 一路上没有 @deepseek-ai/*,于是 ERR_MODULE_NOT_FOUND。

scripts/link-host.mjs 在插件内建立指向 profile 共享层的相对符号链接。它们最终解析到和宿主同一份实体文件,所以服务类保持同一实例——若各自装一份 @deepseek-ai/cordis,ctx.shell 会直接崩。

Web UI 设置分区

注册进 shell 的 settings.section 列表槽——和 通用 / 模型 / 插件 / Agent 预设 同一层级的独立分区(order 12,介于模型 10 与插件 15 之间),而不是通用设置里的一行。

页面结构:

  • 顶部说明块:为什么需要它(非登录 bash -c 不读任何启动文件)、PATH 的合成顺序、改动何时生效、出错时的行为、作用范围,以及为什么官方的「终端」卡片不见了。
  • 四个分组:命令预算 / PATH 与环境变量 / Python 虚拟环境 / mise 工具链。每组带一句组说明(例如其余预算项要去 YAML 配)。
  • 每个设置项:一句 hint 常驻显示;更长的说明放在壳层的信息按钮弹出区里(SettingsValueField 的 help)。列表与勾选类控件用原生 <details> 承担同样的角色,因为壳层的 primitive 只提供单行 input。

绑定的是 profile 里真实组合出的 bash-env 行,且无条件注册:官方设置卡用 configForms.whileServed 门控("没组合这个插件就不出现"),而那个判据读的是宿主回报的命名空间列表——如果某个前端 shell 不回报这份列表(例如局域网 bridge 的页面),入口就会整个消失,看起来和"插件没加载"一模一样。所以本插件改为始终注册,命名空间确实不可用时由设置表单自己显示「该插件当前未加载,暂时无法配置」——有入口并说明状态,比静默消失有用。

保存写入的就是同一个 user 层,与手改 YAML 等价。

由于 bash-sandbox 被本插件禁用,官方那个 "Terminal / 终端" 设置卡会随之消失(它 whileServed(['bash-sandbox','pwsh-sandbox']))。命令超时与输出上限已并入本分区,功能没有丢失。

配置字段必须是 .volatile()(插件作者必读)

设置服务拒绝写入任何非 volatile 的路径(dsh-settings:Config field "x" is not volatile)。症状极具误导性:字段在设置页正常渲染、能编辑,但每次保存都被拒——重启也一样,且不说明原因。

内置 executor 的 6 个预算字段全都带 .volatile(),本插件自己的 12 个字段也必须带。test/executor.integration.mjs 里有一条守卫会遍历 Config.dict 断言这一点。

注意 .volatile() 会改变取值的形态:它解析出的是访问器而不是普通值(这正是内置代码处处写 this.config.timeoutMs.get() 的原因)。所以读取必须解包,否则数组字段会退化成对象,每条命令都会以 object is not iterable 失败。本插件用 plainConfig() 在每次 spawn 时统一解包。

保存失败时会说明原因

壳层表单把所有拒绝都显示成同一句"本部署没有接受这些值",不显示原因——而两种原因该做的事完全不同:

原因 含义 该怎么办
settings/conflict 你打开页面后,这份配置被别处改过(revision 过期) 重开设置再保存
settings/rejected 宿主拒绝了取值 看具体报错改值

所以本插件在 configMutate 外面包了一层:拒绝时用当前 revision 重放同一批操作——

  • 重放成功 ⇒ 说明只是 revision 过期。改动已经保存,表单按成功处理(清空草稿),页面只多一行说明发生了什么。省掉"重开再点一次"。
  • 重放仍被拒 ⇒ 把宿主的 code 与 message 原样显示出来。
  • 读不到原因(例如 remote 不可用)⇒ 退回到列出这两类常见原因。

另外 booleanField 的 format 容忍宿主把布尔值回传成字符串 'true'/'false':否则复选框会把已启用的项显示成未勾选,诱使你点一下、产生一次本不需要的写入。

为什么局域网页面上这个入口不能配置

dsh 自身把设置界面限制在本机 loopback,与本插件无关。dsh-client-ui-settings 这样决定持久化方式:

const persistence = ctx.remote.$host.isLoopback ? "host" : "memory";

而 isLoopback 由浏览器地址栏判定(dsh-client-connection:isLoopbackHostname(pageLocation.hostname),即 localhost / ::1 / 127/8)。一旦用局域网 IP 打开(如 http://192.168.10.20:3082/),持久化退化为 memory,而 memory 模式下:

load()   { if (this.persistence === "memory") return Promise.resolve(); … }   // 不读命名空间
ensure() { if (this.persistence === "memory") return Promise.resolve(); … }   // 同上
mutate() { if (this.persistence === "memory" …) return Promise.resolve(false) } // 不写

后果有两个,都会遇到:

  • 任何插件的设置页都显示"未加载 / 无法配置"(镜像从未加载 → 命名空间永远不是 ready)。官方的设置卡还会因此整个不出现(它们用 whileServed 门控)。
  • 保存被客户端直接拒绝,报"本部署没有接受这些值"——请求根本没发到宿主。

所以:设置请在本机页面(127.0.0.1:3080,或桌面 App 打开的页面)修改。从远程页面发起的命令同样会用到这些设置,因为环境是在宿主侧按工作目录解析的——只有"编辑"受这个限制。

本插件的卡片改为无条件注册,所以远程页面上至少能看到入口和它的真实状态(而不是入口静默消失)。

为什么页内没有"运行一条命令"的测试器

这是刻意的取舍,不是遗漏。客户端要触达宿主只有两条路:

通道 插件可用? 鉴权
Typert remote(ctx.remote.*) ❌ 有,但一个 api 包需要代码生成的 typert.host.js / typert.remote-client.js(各上千行),第三方插件没有这套构建链
ctx.webserver.register() ✅ ❌ 平台层没有:cookie 校验在 dsh-client-connection 内部且未导出

也就是说,页内执行器只能用一条默认无鉴权的本地 HTTP 路由来跑任意命令。虽然本地同用户进程本来就能执行命令,但网页可以通过 CSRF / DNS-rebinding 去打 127.0.0.1——为此在设置页里开一个执行端点,代价与收益不成比例,所以不做。

怎么验证配置生效

在 dsh 的会话里跑(走的就是本插件解析出的环境):

bash -c 'command -v cargo; echo "VE=$VIRTUAL_ENV"; command -v python3; command -v node'
# 期望:cargo 在 ~/.cargo/bin,VE 指向 ~/.venv 或当前项目的 .venv,
#       python3 来自该 venv,node 来自 mise shims

bash -c 'printf "%s\n" "$PATH"' | tr ':' '\n' | head -5
# 期望顺序:<venv>/bin → 额外 PATH 目录 → mise shims → 继承项

# 切换工作目录即切换 venv:
# 用 bash 工具的 workdir 参数指向另一个项目,再 echo $VIRTUAL_ENV

SettingsForm 只负责写配置;解析结果永远以真实命令的输出为准——这也是上面那条取舍的另一个理由。

测试

npm test              # 51 个纯逻辑单测 + 5 个客户端冒烟测试
npm run test:integration   # 13 个集成测试:真挂载 cordis 组合并执行真命令

集成测试覆盖单测到不了的部分:插件确实以 ctx.shell 挂载(无重复注册)、Config 校验通过、解析出的环境真的到达 spawn 的 bash -c,包括嵌套 shell,以及 DSH_* 托管事实不被配置覆盖。

它曾经抓到过一个真 bug:base.env 只是覆盖层、不含 PATH(继承环境由 subprocess seam 合并),只从它算 PATH 会把 PATH 覆盖成仅剩 prepend 项,连 bash 自己都 spawn 不了(ENOENT)。

客户端冒烟测试用桩 window.__ModuleLoader__ / require / ctx 验证:包 id、字典、绑定的是 bash-env、注册进 settings.section(不是 settings.general.item)、以及每个控件的文本↔schema 值转换。它不能验证真实渲染——shell 的 primitives 依赖 React 与 CSS modules,在 Node 里不可加载。

已知边界

  • 只影响 bash 工具跑的命令。 dsh 里其他 spawn 路径(ctx.subprocess 直连的 MCP server、终端 PTY、插件自起的进程)不经过 shell executor,拿不到这份环境。
  • 不处理 mise 的派生变量。 shims 只提供可执行文件;JAVA_HOME / GOROOT / GOBIN 及 mise 的 [env] 段需要 mise hook-env,那是另一个(带缓存的)模块,本插件不做。
  • 不提供 BASH_ENV。 需要 shell 函数/rm 包装时才有必要,且 alias 在非交互 shell 里本就不生效。
  • envVars 在 UI 里是单行文本域:每行一条 KEY=VALUE,但不做 $VAR 展开预览——展开发生在 host 侧。
  • POSIX only,bash-sandbox 的 gating 照抄(Windows 上本行 disabled,本来也没有 bash)。
DSH HUB

社区维护的 DSH 插件索引。不是 GitHub 或 DeepSeek AI 的官方产品。

社区资源API关于
—/ 5

暂无评分

已验证 DSH bundle

Commit c971fd7ea4dc

社区评论

还没有评论,来写第一条。