从一个打不开的桌面图标说起

这次想通过桌面图标启动本地的 DeepSeek Harness。启动器 deepseek_harness.desktop 直接调用 deepseek_harness.sh,但点击图标后无法正常启动,于是先怀疑是不是脚本不适合从桌面执行。

下面只保留排查所需的信息,本机用户名、挂载点和目录结构均已隐去。使用 ./ 的命令假定已经进入相应文件所在目录。

原来的脚本只有两行:

1
2
#!/bin/bash
npx @deepseek-ai/dsh web

其中 npx @deepseek-ai/dsh web 就是 DeepSeek 官方提供的启动命令。桌面启动器中与本次排查有关的部分配置如下,实际 Exec 和图标路径已省略:

1
2
3
4
5
6
[Desktop Entry]
Type=Application
Name=deepseek_harness
Terminal=true
Categories=Utility;Terminal
StartupNotify=true

看起来已经指定了脚本位置,也要求在终端中运行,问题究竟出在哪里?

先检查路径、权限和语法

启动器中的脚本路径与我平时使用的路径看起来不同,不过其中一个目录是符号链接。检查实际指向后,确认它们对应同一个文件,路径没有写错。排查时可以用 readlink -f ./deepseek_harness.sh 查看脚本的实际位置,不必在公开记录中保留具体的挂载目录。

脚本本身也有执行权限,并且通过了语法检查:

1
bash -n ./deepseek_harness.sh

所以,仅仅把路径换一种写法,或者再执行一次 chmod +x,都没有解决这次问题的关键。

真正缺少的是 NVM 环境

本机的 Node.js 是通过 NVM 安装的,npx 位于 NVM 管理的版本目录中。隐去具体版本后,路径结构如下:

1
~/.nvm/versions/node/<版本号>/bin/npx

NVM 的初始化配置则写在 ~/.zshrc 中:

1
2
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"

这里有一个容易忽略的区别:桌面启动器执行脚本时,得到的环境不一定与平时打开终端后使用的环境相同。

检查实际会话环境后发现,GNOME Shell、文件管理器和终端服务的 PATH 都没有上述 NVM 的 Node.js 目录,也都找不到 npx。原脚本又由 Bash 直接执行,不会去读取我的 .zshrc,自然无法补上这个路径。

在不加载 NVM 的干净环境中运行原脚本,可以复现下面的报错(文件路径已简化):

1
./deepseek_harness.sh: line 2: npx: command not found

退出码为 127。也就是说,脚本还没执行到 DeepSeek Harness 本身,就已经因为找不到 npx 退出了。

Terminal=true 只负责让程序在终端窗口中运行,不会自动保证这个程序先经过交互式 Zsh 的初始化流程。终端窗口与 Shell 解释器需要分开理解。

几种处理方式

最直接的做法,是在 Bash 脚本里显式加载 NVM:

1
2
3
4
5
6
7
#!/bin/bash

export NVM_DIR="$HOME/.nvm"
. "$NVM_DIR/nvm.sh" --no-use || exit 1
nvm use --silent default || exit 1

exec npx @deepseek-ai/dsh web

这种方式只加载所需的 NVM 配置,不必把整个 .zshrc 都执行一遍。验证时,加载 NVM 后可以正确找到 node 和 npx。

不过我更想沿用已经配置好的 Zsh 环境,因此又考虑先运行 Zsh,再调用原脚本。进入脚本所在目录后,核心命令可以写成:

1
zsh -ic 'exec ./deepseek_harness.sh'

桌面启动器也可以采用这一思路,不过使用相对路径时,需要另行保证它的工作目录就是脚本所在目录,不能直接假定桌面启动时的当前目录。

其中 -i 指定交互模式,-c 指定执行后面的命令。Zsh 先读取 .zshrc、初始化 NVM,再执行原来的 Bash 脚本。虽然脚本仍由 Bash 解释,但它会继承 Zsh 已经设置好的 PATH。

在干净环境中通过 zsh -ic 检查,得到的结果如下,第一行中的目录和版本目录名已泛化:

1
2
3
~/.nvm/versions/node/<版本号>/bin/npx
v24.21.0
11.19.0

三行分别对应 command -v npx、node --version 和 npx --version,说明这条初始化链路可以找到本机的 Node.js 环境。

能不能只修改脚本第一行?

进一步想了一下:既然是需要先进入 Zsh,能不能直接在脚本第一行指定,而不用修改桌面启动器?

可以。针对当前这份脚本,可以写成:

1
2
#!/bin/zsh -i
npx @deepseek-ai/dsh web

第一行称为 shebang,用于在直接执行脚本时指定解释器。在本机 Linux 环境下,这里指定的是 /bin/zsh,并向它传入 -i 参数。

单独写 #!/bin/zsh 还不够,关键在于后面的 -i。

-i 是 interactive,即强制交互模式。交互式 Zsh 会读取 .zshrc,从而执行我放在其中的 NVM 初始化配置,再运行脚本里的 npx 命令。它并不要求脚本接下来一定要等待人工输入。

这里也需要区分几个概念:

配置或参数 作用
Terminal=true 让桌面启动器在终端窗口中运行程序
#!/bin/zsh 直接执行脚本时使用 Zsh 解释
-i 强制交互模式,使 Zsh 读取 .zshrc
.zshrc 中的 NVM 配置 加载 NVM,设置所选 Node.js 版本的环境

因此,-i 本身不负责激活 NVM,也不负责打开终端窗口;它让 Zsh 读取配置,再由配置完成 NVM 初始化。

这样写时,启动器原来的 Exec 可以保持不变。不过要让 shebang 生效,必须直接执行脚本,例如:

1
./deepseek_harness.sh

如果改用 sh 脚本路径 或 bash 脚本路径,解释器就由命令中的 sh 或 bash 决定,第一行指定的 Zsh 不会生效。

这种方案会顺带加载 .zshrc 中的主题、插件及其他初始化配置。对于当前这个两行的个人启动脚本,可以沿用已有环境;如果换成内容较多的 Bash 脚本,修改解释器前还需要检查其中的语法是否适用于 Zsh。

另外发现的启动器问题

使用下面的命令校验桌面启动器:

1
desktop-file-validate ./deepseek_harness.desktop

还发现了一个分类字段错误:

1
Categories=Utility;Terminal

Terminal 不是已注册的分类值,可以改为:

1
Categories=Utility;

这是启动器需要修正的另一个问题,但前面复现到的 npx: command not found,原因仍然是缺少 NVM 环境。

记录时,本地脚本的第一行已经改为 #!/bin/zsh -i。本次验证确认了桌面会话缺少 NVM 路径,以及交互式 Zsh 能加载配置并找到 npx;没有实际启动 DeepSeek Harness 服务来验证网页是否打开,因此这里保留这一验证范围。

参考资料

封面使用 Justin Dorfman 设计的 Zsh 标志,按 CC BY-SA 4.0 许可使用,本文未修改该图片。