| name | wsl-dev-environment |
| description | WSL 开发环境配置 — NTFS venv 权限、Clash 代理访问、DeepSeek ReAct 模式、PaddlePaddle 安装、>60s 长任务的 Hermes sandbox 限制、GitHub release 大文件下载 |
| category | devops |
| tags | ["wsl","venv","proxy","deepseek","react-agent","long-running","github-release"] |
WSL 开发环境
WSL (Windows Subsystem for Linux) 上的开发环境配置与常见陷阱。
触发条件
- 在 WSL 中创建 Python venv 失败(NTFS 权限错误)
- 在 WSL 中需要访问 Windows 侧的代理(Clash)
- NTFS 挂载盘上的文件权限/解压问题
- DeepSeek ReAct Agent 在 WSL 环境下的运行
- 通过 SSH 操作远端 fish shell 服务器时遇到
fish: $? is not the exit status
- 在 WSL 内运行 tmux / 任何常驻进程,需要"清干净"做验证
- 从 Windows PowerShell 一行启动 WSL 里安装的 CLI(hermes / claude / codex 等),报"未找到命令"、"no API keys"、或"先进入 fish / 卡在 PS1 才执行命令"
- 在 WSL 里跑 > 60s 的 JVM / 构建任务,
terminal(background=true) 起来后秒退
- 从 GitHub release 下载 > 50MB 资产,速度极慢且续传无效
- 在 WSL 里
cp 大文件 / 多文件目录到 /mnt/c/ /mnt/e/ 等 Windows NTFS 挂载盘,写入静默失败(目标文件不存在 / size 截断 / cp 进程 exit 0 但目标没生成)
- 后台进程完成通知里看到
bash: 无法设定终端进程组 (-1): 对设备不适当的 ioctl 操作 + 此 shell 中无任务控制 → 误以为是失败
SSH 到 fish shell 远端服务器 — $? 必踩的坑
远端用户登录 shell 是 fish 时,inline 单引号命令里写 $? 会被 fish 拦截:
fish: $? is not the exit status. In fish, please use $status.
原因:ssh 把整个 command 作为 argv 传给远端登录 shell,fish 看到 $? 字面量就直接报错。命令根本没跑。
绕过(任选一):
ssh user@host bash -c '...your code with $?...'
ssh user@host /bin/sh <<'EOF'
...your code with $?...
EOF
判断:远端命令 exit 127 + stderr 含 fish: $? is not the exit status = 几乎一定是这个问题,不是命令本身语法错。
tmux / 长驻进程:永远别 kill-server 验证
Hermes TUI 本身就跑在 WSL tmux 里。配置改动后想"清干净再加载"时:
tmux kill-server
tmux new-session -d -s _v fish -l
tmux source-file ~/.tmux.conf
后果:本会话直接断,Hermes 状态丢失,下一次用户在另一个 shell 继续。
tmux new-session -d -s _vverify fish -l
tmux source-file ~/.tmux.conf
tmux display-message -p "prefix=[#{prefix}] mouse=[#{mouse}] shell=[#{default-shell}]"
tmux kill-session -t _vverify
或者用独立 socket 完全隔离:tmux -L _test new-session -d ... + tmux -L _test kill-server,这只杀那个 socket 下的 server,不动默认。
如果想检查 $? 又在 fish 环境:用 tmux display-message -p 把检查嵌进 tmux 内部,fish 看不到 $? 就不会爆。
NTFS 挂载盘上的 venv 权限问题
症状
在 /mnt/c/、/mnt/e/ 等 Windows 盘上使用 uv sync 或 pip install 时报错:
Operation not permitted (os error 1)
原因
Windows NTFS 文件系统不支持 Linux 的文件权限模型,导致 .venv/ 内的文件复制操作失败。
解决方案
将 venv 创建在 Linux 原生文件系统(ext4)上,然后软链接到项目目录:
rm -rf .venv
uv venv --python 3.13 /home/po/.venvs/<project-name>
ln -sf /home/po/.venvs/<project-name> .venv
uv sync
注意:pyproject.toml 和项目代码仍可放在 NTFS 盘上,只有 .venv/ 需要移到 Linux 侧。
WSL 访问 Windows Clash 代理
Clash for Windows 的 HTTP 代理默认监听 127.0.0.1:7890,WSL 可直接访问。
探测代理
curl -s --connect-timeout 1 http://127.0.0.1:7890
设置代理
export HTTP_PROXY=http://127.0.0.1:7890
export HTTPS_PROXY=http://127.0.0.1:7890
常见 Clash for Windows 端口:HTTP 7890 | SOCKS5 7891
DeepSeek ReAct Agent 模式
参见 references/deepseek-react-patterns.md。核心陷阱:
- 必须设置
stop=["PAUSE"] — DeepSeek 不会自然停在 PAUSE,会一次生成完整多轮对话(含幻觉 Observation)
- 正则匹配需兼容中英文冒号 — DeepSeek 输出可能混用
: 和 :
Python 版本冲突与依赖管理
uv add vs uv pip install
在 NTFS 挂载盘上的项目,不要用 uv add(它会尝试重建 NTFS 上的 .venv)。用 uv pip install 直接装到 Linux venv:
uv pip install <package> --python /home/po/.venvs/<project-name>/bin/python
Python 3.10 兼容问题
某些 ML 项目(如 CosyVoice2 依赖的 matcha-tts)需要 Python 3.10(因为用到了 Python 3.12 移除的 distutils)。
conda create -n <project> python=3.10
conda activate <project>
python -m ensurepip --upgrade
注意:conda 创建的 Python 在某些 ML 包上可能有兼容问题(如 PyTorch CUDA 支持),需要额外配置。
uv python install 国内网络超时
uv python install 从 astral-sh 的 GitHub releases 下载 Python,国内网络可能 120s 超时。
解决:走代理
https_proxy=http://127.0.0.1:7890 uv python install 3.10
或跳过 uv 的 Python 管理,直接用 conda 或系统 Python。\n\n### Python 3.14 兼容问题
系统 Python 3.14 可能缺少某些包的 wheel(如 openai-whisper 缺少 pkg_resources)。用 uv 安装旧版 Python:
uv python install 3.12
uv venv --python 3.12 /home/po/.venvs/<name>
Vite/Node 开发服务器:Windows 浏览器访问
WSL 中启动的 Vite dev server 在 Windows 浏览器可能无法访问。
必须加 --host
npx vite --host 0.0.0.0
不加 --host 时 ss -tlnp | grep 5173 显示端口未监听(虽然 Vite 打印了 ready 消息)。
浏览器地址优先级
| 优先级 | 地址 | 适用场景 |
|---|
| 1 | http://localhost:5173/ | WSL2 localhost 转发正常时 |
| 2 | http://<WSL_IP>:5173/ | localhost 转发失败时(用 ip addr show eth0 获取 IP) |
terminal(background=true) 静默退出
terminal(background=true) 启动 npx vite 可能静默退出(exit code 0,output 只有 bash ioctl 警告)。替代方案:在用户自己的 WSL 终端中手动运行 npx vite --host 0.0.0.0。
WSL 后台进程调试
后台进程在 WSL 中静默退出是常见问题。排查流程:
- 先在前台跑一次看报错:
python3 server.py 2>&1
- 确认无语法错误后再后台启动:
terminal(background=true)
- 检查端口:
ss -tlnp | grep <port>
Hermes 长时任务被 sandbox 杀的硬性上限(实测 ~60-90s)
症状:terminal(background=true) 起的进程在 60-90 秒后无任何报错地消失(exit_code=0,但 log 不更新,PID 查不到)。实测:SPC 8.1.2 生成整合包 5 分钟全过程跑得通,但 terminal(background=true, timeout=1500) 起来的实例 uptime 只有 69s 就被砍。
根因:Hermes TUI 给后台进程附加了 sandbox 监管(看 stderr 的 bash: 无法设定终端进程组 (-1): 对设备不适当的 ioctl 操作 + 此 shell 中无任务控制 这两行警告),超时后 SIGTERM。
⚠️ 关于那两行 ioctl 警告的"误报":Hermes 后台进程完成通知里也总是带这两行(bash: 无法设定终端进程组 (-1) + 此 shell 中无任务控制),即使命令真的成功跑完了。看到这两行不等于失败——判断成功与否要看 exit_code 和实际产出物(文件、port、log)。这条不是新坑,是 Hermes 的固定行为。
结论:
- 短任务(< 60s) → 后台方便,配合
process action=poll 看进度
- 长任务(> 60s) → 必须前台 + 留 timeout 余量。
timeout 580 java -jar ... 实测可用,600s 是 Hermes fg 命令上限
- 超长任务(> 10 分钟) → 用
delegate_task(会自动后台),或 cronjob(deliver='local' 不会发回 TUI,但任务真跑完)
反模式:
terminal(background=true, timeout=1800) java -jar foo.jar > log 2>&1
正确做法:
timeout 580 java -jar foo.jar > log 2>&1
ModelScope 下载
ModelScope(modelscope.cn)下载模型不需代理,且比 HuggingFace 更稳定(国内 CDN)。
HuggingFace 模型下载的 4 种备选方案(按优先级)
在 WSL 国内网络环境下,HuggingFace 下载常失败。逐级尝试:
方案 1: hf-mirror Git Clone + LFS
git clone https://hf-mirror.com/<org>/<repo> checkpoints
cd checkpoints && git lfs pull
注意:GIT_LFS_SKIP_SMUDGE=1 会跳过 LFS 文件(只下载指针),网络不好时反而不行。
方案 2: Modelscope Git Clone + LFS
git clone https://www.modelscope.cn/<org>/<repo>.git checkpoints
cd checkpoints && git lfs pull
用 modelscope 的 git 服务,国内 CDN 更稳定。
方案 3: HuggingFace CLI 直下
export HF_ENDPOINT=https://hf-mirror.com
huggingface-cli download <org>/<repo> --local-dir checkpoints
但 hf-mirror 经常超时导致静默失败(exit 0 但只下了一个小文件)。验证:du -sh checkpoints/ 应该在 GB 级。
方案 4: Modelscope Python SDK
from modelscope import snapshot_download
snapshot_download('<org>/<repo>', local_dir='checkpoints')
LFS 下载验证
LFS 文件下载后在 .git/lfs/incomplete/ 中说明下载中断。删除 incomplete 目录重试:
rm -rf .git/lfs/incomplete && git lfs pull
成功的 LFS 文件应在 .git/lfs/objects/ 中,且根目录出现实际大文件(而非指针)。
uv sync 静默失败与大型 ML 项目依赖安装
症状
uv sync exit code 0,但 .venv/lib/python3.12/site-packages/ 只有 _virtualenv.pth,没有任何实际包。
原因
uv sync 在锁文件已存在时可能跳过实际安装(尤其在 NTFS 盘或网络不稳时)
uv lock 在大型项目(如 PyTorch + CUDA 包依赖)上耗时极长(300s+ 超时)
解决:直接 pip 安装
uv venv --python 3.12
uv pip install --python .venv/bin/python pip
.venv/bin/python -m pip install \
-i https://download.pytorch.org/whl/cu128 \
torch torchaudio \
--extra-index-url https://pypi.tuna.tsinghua.edu.cn/simple
.venv/bin/python -m pip install \
-i https://pypi.tuna.tsinghua.edu.cn/simple \
transformers fastapi soundfile ...
CUDA 库版本不匹配(PyTorch + nvidia pip 包)
PyTorch 的 torchaudio .so 文件链接到 libcudart.so.12,但 pip 安装的 nvidia-cuda-runtime 可能提供 libcudart.so.13(CUDA 13.x 驱动)。
解决:设置 LD_LIBRARY_PATH 包含所有 nvidia pip 包的 lib 目录:
NVIDIA_LIB=$(find ~/.local/share/uv/python/cpython-3.12.*/lib/python3.12/site-packages/nvidia -name "lib" -type d | tr '\n' ':')
export LD_LIBRARY_PATH="${NVIDIA_LIB}/usr/lib/wsl/lib"
/tmp 磁盘空间不足
在 ML 项目中 pip 安装大包(如 nvidia-cudnn 366MB)时可能报 No space left on device。WSL 的 /tmp 通常是 tmpfs(7.8G),容易满。
解决:
mkdir -p ~/tmp
export TMPDIR=~/tmp
或者清理 modelscope 缓存:rm -rf /tmp/ms_cache
Fish shell: set -x 环境变量不生效
Fish 的 set -x VAR val; cmd 在某些场景(如 uv tool upgrade)环境变量不会被命令继承。
用 env 前缀替代:
# ❌ 不生效
set -x UV_HTTP_TIMEOUT 300; uv tool upgrade astrbot
# ✅ 生效
env UV_HTTP_TIMEOUT=300 uv tool upgrade astrbot
uv.toml 国内镜像
[pip]
index-url = "https://pypi.tuna.tsinghua.edu.cn/simple"
创建于项目根目录即可。与 pyproject.toml 中的 [tool.uv] 冲突时 uv.toml 优先。
uv tool 命令走镜像(不依赖 uv.toml)
uv tool upgrade/install 不受项目 uv.toml 控制。大包超时时直接设 UV_INDEX_URL:
env UV_HTTP_TIMEOUT=300 UV_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple uv tool upgrade astrbot --python 3.12
PYTHONPATH 全局污染(Hermes Agent)
Hermes Agent 设置了全局 PYTHONPATH=/home/po/.hermes/hermes-agent,导致所有 Python 的 sys.path 都被注入该路径。在 WSL 中运行其他 Python 项目(尤其是 ML 项目)时,这会引发包版本冲突。
解决:启动其他 Python 应用时 unset:
PYTHONPATH= python3 server.py
unset PYTHONPATH
uv pip install vs pip install 找不到 venv
uv pip install 默认在项目目录找 .venv。如果 venv 在其他位置:
uv pip install --python /path/to/venv/bin/python <package>
终端工具 "uvicorn" 关键字误判
Hermes 终端工具会检测命令中是否包含 uvicorn 并误判为长时服务器进程,拒绝执行。pip install uvicorn 或 pip show uvicorn 都会被拦截。
绕过:用 execute_code 工具调用 subprocess.run:
from hermes_tools import terminal
import subprocess
subprocess.run([venv_python, "-m", "pip", "install", "-i", mirror, "uvicorn"], ...)
或者用文件间接传递包名:echo 'uvicorn' > /tmp/pkg.txt && pip install -r /tmp/pkg.txt
uvicorn ML 服务:模型双加载陷阱
uvicorn.run("server:app") 会导致模型加载两次:
- Python 以
__main__ 运行 server.py
- uvicorn 重新 import
server 模块(不同 __name__)
- 两个
IndexTTS2() 实例 → VRAM ×2 → OOM
正确写法:
if __name__ == "__main__":
model = load_model()
uvicorn.run(app, host="0.0.0.0", port=8800)
另外 uvicorn lifespan 有 5 秒默认超时,模型加载(~35s)不能放在 lifespan startup 中。
PowerShell 一行启动 WSL 里安装的 CLI(hermes / claude / codex …)
Windows 终端里想直接敲 hermes / claude / codex 调 WSL 里的 CLI(典型场景:WSL 配好了全套 env 和 venv,但人在 PowerShell 里),wsl <cmd> 这种直觉写法一连踩三个坑:
坑 1:wsl <cmd> 找不到命令 — wsl 默认 PATH 不带 ~/.local/bin
wsl -e bash -lc "hermes" 启动的 shell 只继承 WSL 默认用户的 PATH(/usr/bin 等),不读 .bashrc 里的 export PATH 也不读 ~/.local/bin/env。hermes / claude / codex 全装在 ~/.local/bin/,直接报 未找到命令。
绕过:直接走绝对路径,不依赖 PATH 解析:
# ❌ 错
wsl hermes
wsl -e bash -lc "hermes"
# ✅ 对 — 直接调 venv 里的 entry point
wsl -e bash -lc "/home/po/.hermes/hermes-agent/venv/bin/hermes"
坑 2:wsl -e bash -lc "~/..." 中 ~ 解析成 /root — wsl 默认登录用户是 root
wsl -e 不带 -u 时用 /etc/wsl.conf 的 default 用户(默认 root)。~ 在 bash -lc 字符串里被 root 用户解析成 /root,不是 /home/po,于是报 /root/.local/bin/hermes: 没有那个文件或目录。
绕过:绝对路径写死,或用 -u po 切用户(但切用户会丢 root 那边的 env,见坑 3)。
坑 3:切到 -u po 后丢失 API key / proxy / 各种 env
hermes / claude 这类工具要读一堆 env(DEEPSEEK_API_KEY、BAILIAN_API_KEY、HTTPS_PROXY、NAPCAT_TOKEN、PYTHONPATH…)。WSL 默认 root 登录 shell 里这些 env 是齐的(写在 /etc/profile.d/ 或 root 的 .bashrc)。切到 -u po 后 po 的 shell 没这些 export,工具启动报"no API keys or providers found"。
最干净的解法:保持默认用户 root + 绝对路径调 po 的 venv,root 的 env 全在,po 的库全在:
# PowerShell $PROFILE 里加:
function hermes { wsl -e bash -lc "/home/po/.hermes/hermes-agent/venv/bin/hermes $($args -join ' ')" }
变体:
- claude code:
/home/po/.local/bin/claude(如果装在 ~/.local/bin 而非 venv)
- codex:
/home/po/.npm-global/bin/codex
验证链路是否通:
wsl -e bash -lc '/home/po/.hermes/hermes-agent/venv/bin/hermes --version'
# 应输出 Hermes Agent v... 不报 file not found
不输出 version = 路径错了;version 出来了但还是 "no providers" = 坑 3,切用户了。
坑 4:wsl -e bash -lc "..." 先进 fish 后才执行 hermes — -l + TTY 让 bash 卡在登录 shell 交互模式
症状:用户报"PowerShell 函数执行时先进入 WSL 的 fish / 显示 welcome banner,等我退出后再进 hermes"。
原因:wsl -e bash -lc "cmd" 中 -l 让 bash 作为登录 shell 启动。bash 登录 shell 看到 stdin/stdout 是 TTY(PowerShell 传过来的就是)→ 进交互模式 → 先读 .bash_profile / .profile、打印 PS1 等用户输入 → 然后才把 -c 字符串作为命令执行。用户的默认登录 shell 是 fish 时,会先看到 fish 的 welcome / 主题渲染,命令才执行。
官方文档依据(Microsoft WSL docs):
"The Linux command following wsl is handled like any command run in WSL. Run as the WSL default user."
wsl -e 跳过默认 Linux shell,但仍会按你指定的 shell(bash)启动;如果加了 -l 它就走 login shell 路径。
修复:去掉 -l,用 bash -c 直接执行命令字符串:
# ❌ 错 — 触发登录 shell 交互模式
function hermes { wsl -e bash -lc "/home/po/.../hermes $($args -join ' ')" }
# ✅ 对 — bash -c 不读 .bash_profile,不进交互模式
function hermes { wsl -e bash -c "/home/po/.../hermes $($args -join ' ')" }
更优雅:直接走 wsl 默认 shell,把 HOME 用 env 前缀传给 hermes:
# 最简版:让 wsl 默认 shell(root 的 fish / bash)直接 exec hermes
function hermes { wsl HOME=/home/po /home/po/.hermes/hermes-agent/venv/bin/hermes $args }
HOME=val command 是 POSIX / fish / zsh / dash 通用语法,shell 看到就直接 exec hermes,不启新 shell。
副作用:bash -c(去掉 -l)不读 .bash_profile / .profile,所以 root 登录 shell 里 export 的 env(API key / proxy)会丢。如果 hermes 报"no API keys",需要显式透传:
# 把 PowerShell 的关键 env 透传给 WSL root
function hermes {
$envArgs = @()
foreach ($name in 'HTTPS_PROXY','HTTP_PROXY','BAILIAN_API_KEY','DEEPSEEK_API_KEY','NAPCAT_TOKEN') {
if ($env:$name) { $envArgs += '--env', "$name=$($env:$name)" }
}
& wsl.exe @envArgs -e bash -c "HOME=/home/po /home/po/.hermes/hermes-agent/venv/bin/hermes $($args -join ' ')"
}
但 hermes 默认优先读 ~/.hermes/config.yaml 里写死的 key,不一定依赖瞬时 env。先试最简版,报"未配置"再加 env 透传。
官方 env 共享机制(更系统化):PowerShell 那边设置 WSLENV 变量列表,决定哪些 Windows env 自动透传到 WSL:
$env:WSLENV = "HTTPS_PROXY/u:HTTP_PROXY/u:BAILIAN_API_KEY/u:DEEPSEEK_API_KEY/u:NAPCAT_TOKEN/u"
/u 标志表示"只在从 Win32 调用 WSL 时透传"。设一次,所有 wsl 命令都生效。
根本修复(一次解决所有坑)
把 hermes 需要的 PATH 补进 /etc/profile.d/,所有用户登录都有:
sudo tee /etc/profile.d/hermes-env.sh <<'EOF'
export PATH="$HOME/.local/bin:$PATH"
EOF
要 sudo,你自己粘贴进 WSL 跑。
参考文件
references/deepseek-react-patterns.md — DeepSeek ReAct Agent 在 WSL 下的具体坑
references/deepseek-api-patterns.md — DeepSeek API 的 ReAct/Function Calling 坑和写法(从 wsl-python-development 合并)
references/mcp-fastmcp-api.md — MCP FastMCP 三种传输协议的现行 API(从 wsl-python-development 合并)
references/paddlepaddle-setup.md — 百度飞桨在 WSL 上的安装、GPU 配置、3.3.0 API 已知坑
references/indextts-deploy.md — Index-TTS 在 WSL 上的完整部署实录(依赖安装、模型下载、AstrBot 插件)
references/powershell-wsl-launch.md — PowerShell 一行启动 WSL 里安装的 CLI(hermes / claude / codex 等)的 4 个连环坑(含 bash -lc 登录 shell 卡 PS1 / fish welcome)
references/wsl-to-windows-file-copy.md — WSL → Windows NTFS 大文件传输的 cp 静默截断 + PowerShell SMB 路径(\\wsl.localhost\<distro>\...)解法
references/github-release-download.md — GitHub release 资产下载的 JWT 签名陷阱(续传无效 / WSL 长连接慢)和 gh CLI + 浏览器替代方案
templates/hello_paddle_mnist.py — 飞桨 3.3.0 兼容的 MNIST 训练模板