一、前言
最近开始大量使用 Codex、Claude Code、OpenCode 这类 AI Agent 之后,我看着Agent在我的windows上干活发现他会频繁地执行:
git status
find .
grep -R "xxx" .
cat package.json
npm install
python xxx.py
export API_KEY=xxxx
mkdir -p ./data
command1 && command2
但是总是会出现一些莫名其妙的报错:例如
引号不匹配
环境变量写法错误
路径格式错误
管道命令行为不一致
&& / || 行为不同
grep、sed、awk、find 不存在
export / source 无法使用
每次看着Agent出现这些错误都会被气死,不仅白吃我的token,而且还浪费了大量的时间。这并不是简单的“AI 不认识 PowerShell”。更准确地说,是目前大量 AI 编程工具、开源项目和开发工作流,都天然围绕 Unix/Linux 环境设计。Claude Code 官方文档明确把 Bash、Zsh、Fish 作为效果较好的 shell,并支持通过 WSL 在 Windows 上运行;OpenCode 官方也明确建议 Windows 用户使用 WSL,以获得更好的终端和开发工具兼容性。Codex CLI 的系统要求同样包含 Linux/macOS,并对 Windows 的使用提供了独立路径,其中 WSL2 是其文档中的 Linux 环境方案。
所以问题主要出现在:AI Agent 认为自己正在操作一个 Bash/Linux 环境,而你实际上给它准备的是 PowerShell。
这也是为什么我现在越来越倾向于一种 Windows + AI Agent 的工作方式:
Windows 负责桌面环境,WSL 负责 Linux 开发环境,AI Agent 直接运行在 WSL 里面。
这篇文章就记录一下,我是怎么把这一套环境搭起来的。
二、WSL 到底是什么
WSL(Windows Subsystem for Linux)
简单理解:
在 Windows 里面提供一个 Linux 用户空间和 Linux 开发环境。
它不是传统意义上的“再开一台 Linux 虚拟机”。你仍然可以:
Windows
├── 浏览器
├── 微信
├── VS Code
├── 文件管理器
│
└── WSL
└── Ubuntu
├── bash
├── git
├── python
├── node
├── npm
└── AI Agent
Microsoft 官方目前推荐直接使用:
wsl --install
进行安装。该命令会启用 WSL/虚拟机平台相关组件,并安装默认的 Ubuntu 发行版;安装完成后通常需要重新启动 Windows。
不过这条命令会把整个 Linux 环境装进系统盘。装到 D 盘能给 C 盘省下不少 Agent 跑出来的数据空间,做法见下一章。
三、安装 WSL 与文件系统
安装 WSL(并把它放到别的盘)
C:\Users\<你的用户名>\AppData\Local\Packages\<发行版包名>\LocalState\ext4.vhdx 这个 ext4.vhdx 就是整个 Linux 环境,装完工具、跑一段时间 Agent,几十个 G 很正常。使用 wsl --install 默认是安装在系统盘。我这台机器上现在 WSL 相关服务使用磁盘用量是 19.88 GB,全放 C 盘属实有点顶。如果想放在指定的路径,各位可以参考这个命令安装,D:\DevTools\WSL\Ubuntu24 这个路径按照自己的习惯切换即可:
wsl --install Ubuntu-24.04 --location D:\DevTools\WSL\Ubuntu24 --name Ubuntu24
然后按照提示重启电脑。
重启后,第一次启动 Ubuntu 时,系统会让你创建一个 Linux 用户名和密码。注意:这里的 Linux 用户和 Windows 用户不是一回事,用户名随便填都可以,但不能填 root,那是系统保留账户,会被直接拒绝。
本文后面统一以 root 用户为例,家目录是 /root。想和我保持一致的话,先随便建一个用户,进入 WSL 后设置 root 密码并把默认登录用户切成 root:
sudo passwd root
echo -e "[user]\ndefault=root" | sudo tee -a /etc/wsl.conf
回到 PowerShell 执行 wsl --shutdown,再次进入 WSL 就是 root 了,用 whoami 确认。
如果你保留自己创建的普通用户,后文所有 /root/ 换成 /home/<你的用户名>/ 即可。
最后用 wsl -l -v 确认装的是 WSL 2,VERSION 那一列必须是 2。显示 1 的话手动转换,并把 WSL 2 设为以后的默认版本:
wsl --set-version <你的发行版名> 2
wsl --set-default-version 2
两套文件系统:Windows 和 WSL 怎么互通
WSL 自己有一套 Linux 文件系统,/root、/home/<你的用户名>、/usr、/etc 这些就是 Linux 环境本身,项目、Node.js、Python、Git、AI Agent 都可以直接放在这里。
同时 WSL 默认也能直接访问 Windows 文件,Windows 磁盘会自动挂载到 /mnt/ 下,C:\ 对应 /mnt/c/,D:\ 对应 /mnt/d/,依此类推。所以 C:\Users\<你的Windows用户名>\Desktop\demo 在 WSL 里就是 /mnt/c/Users/<你的Windows用户名>/Desktop/demo,可以直接 cd 进去。
反过来,Windows 资源管理器地址栏输入 \\wsl$ 就能访问 WSL 的 Linux 文件系统。两边的文件可以直接互相访问。
项目放在哪里
为什么我不建议放 /mnt/c
这是 WSL 新手最容易踩的坑之一。cd /mnt/c/Users/<你的Windows用户名>/Desktop/project 当然能跑,但对于 Node.js、Python、Git、node_modules、虚拟环境、大型代码仓库、AI Agent 这类涉及大量小文件读写的工作负载,跨文件系统的性能损失比较明显。
我更推荐把代码放进 WSL 自己的 Linux 文件系统:
mkdir -p /root/AI_Workspace
cd /root/AI_Workspace
git clone https://github.com/xxx/xxx.git
这样项目就落在 /root/AI_Workspace。/root 是 root 用户的家目录,用自己创建的普通用户则是 /home/<你的用户名>/AI_Workspace。
那 Windows 文件怎么办
不用担心,WSL 仍然可以直接访问 Windows 文件,cd /mnt/c/Users/<你的Windows用户名>/Downloads 或者复制进来都行:
cp /mnt/c/Users/<你的Windows用户名>/Desktop/test.py /root/AI_Workspace/
反过来,Windows 文件管理器地址栏输入 \\wsl$ 就能看到 WSL 中的发行版。
最终可以这样理解
Windows
│
├── C:\ / D:\ / E:\
│ ↓
│ /mnt/c / /mnt/d / /mnt/e
│
└── WSL
│
└── Linux 文件系统
├── /root
├── /home
├── /usr
├── /etc
└── /root/AI_Workspace
所以要记住:WSL 默认能访问 Windows 磁盘,不代表 WSL 的 Linux 文件系统必须放在 C 盘。 安装位置和 /mnt/c 是两回事,前者决定 WSL 的 Linux 环境存在哪里,后者只是 WSL 访问 Windows 文件的挂载入口。
四、WSL 网络配置
这一章是我踩坑最多的地方,所以写得细一点。WSL 的网络如果没配好,后续使用会遇到很多莫名其妙的问题,装 Node、uv、AI Agent 都要走网络,先把这里配通,不然每一步都会卡。
先搞清楚自己在哪个模式
WSL 2 有两种网络模式分别是NAT和Mirrored,代理配置方式完全不同,配之前必须先确认自己在哪个模式。
1. 两种模式的本质区别
NAT(默认)
WSL 拿到一个独立网段,通过 Windows 上的虚拟网卡做 NAT 出网。它和 Windows 是两台“机器”。
Windows 主机 192.168.x.x(你的 WiFi)
│
└── vEthernet (WSL) 172.18.48.1 ← WSL 眼里的网关
│
└── WSL eth0 172.18.59.131/20
关键一点:
WSL 里的
127.0.0.1是 WSL 自己,不是 Windows。
所以 Windows 上监听 127.0.0.1:7897 的代理软件(例如Clash Verge),在 WSL 里怎么都连不上。这是绝大多数“WSL 代理配了但没用”的根因。
Mirrored(镜像,Windows 11 22H2+)
WSL 直接共用 Windows 的网络栈,网卡、IP、loopback 全部一致。这个模式对 VPN、IPv6、localhost 互访、局域网访问的兼容性明显更好,代理配置也简单得多。
2. 确认当前模式
先看配置文件,路径在 C:\Users\<你的用户名>\.wslconfig。没有这个文件,或者文件里没写 networkingMode,就是默认的 NAT。
更可靠的方式是在 WSL 里直接看网卡:
ip -brief addr
ip route show default
NAT 模式会看到 eth0 是 172.18.x.x/20 这样的私有地址,默认路由指向 172.18.x.1。Mirrored 模式会看到和 Windows 那边一样的网卡和 IP(比如你的 WiFi 地址),并且多出一个 loopback0 接口。
3. NAT 模式的基础配置
如果你不打算换 mirrored,NAT 下建议至少把 DNS 隧道打开。编辑 C:\Users\<你的用户名>\.wslconfig:
[wsl2]
networkingMode=NAT
localhostForwarding=true
dnsTunneling=true
autoProxy=false
dnsTunneling=true 让 WSL 的 DNS 查询通过隧道交给 Windows 解析器处理,在 VPN、公司网络这类环境下比 WSL 自己生成 /etc/resolv.conf 稳得多。
autoProxy=false 表示不自动继承 Windows 的系统代理设置。NAT 模式下我不建议依赖它——如果你的代理只监听 127.0.0.1,继承过来的地址在 WSL 里照样连不通,反而多一层不确定性。NAT 下用下面「NAT 模式下的代理配置」的显式写法,行为完全可控。启动时如果看到 检测到 localhost 代理配置,但未镜像到 WSL 这行警告,说的就是这件事,那一节末尾专门讲了怎么处理。
改完 .wslconfig 必须执行 wsl --shutdown 重启 WSL 虚拟机才生效,然后重新打开终端。注意这个命令会关掉所有发行版和正在跑的进程,先存好手上的活。
先测试基础网络,再决定要不要配代理
这里我特别建议:先测试基础网络,再决定要不要配置代理。
在 WSL 的终端里先测 DNS 和直连:
getent hosts github.com
curl -I https://github.com
curl -I https://registry.npmjs.org
如果这几个都通了,说明 WSL 的基础网络没问题,你只需要针对被墙的服务配代理,不用全局改。
最后用这条命令看一下自己的实际出口,输出里的 ip= 和 loc= 就是真实的出网地址和地区:
curl https://www.cloudflare.com/cdn-cgi/trace
这个命令比 curl -I https://www.google.com 有用得多——它能告诉你代理到底有没有生效。后面每次改完代理配置,都用它验证。否则一旦一开始就在:Windows 代理 → PowerShell → WSL → Bash → npm → AI Agent中间叠加五六层配置,后面排查会非常痛苦。
NAT 模式下的代理配置
NAT 模式下要让 WSL 用上 Windows 的代理(以Clash Verge为例,我的电脑的Clash Verge端口是默认的7897,各位根据自己实际的端口运行命令进行修改),需要三步。这三步缺任何一步都不通,而第二步是最容易被漏掉的。
第一步:代理软件开放局域网连接
因为 WSL 在另一个网段,代理软件必须监听 0.0.0.0 而不是只监听 127.0.0.1。
Clash / Clash Verge / mihomo 在设置里打开「局域网连接」(对应配置项 allow-lan: true),其他工具找 Allow LAN / 允许局域网 / Listen on all interfaces 之类的选项。
第二步:Windows 防火墙放行端口
这一步最容易漏。allow-lan 开了但 Windows 防火墙默认拦截入站连接,WSL 连过去还是超时。以管理员身份打开 PowerShell 执行:
New-NetFirewallRule -DisplayName "Proxy for WSL 7897" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 7897 -RemoteAddress 172.16.0.0/12 -Profile Any
-RemoteAddress 172.16.0.0/12 只放通 WSL 所在的私有网段,别图省事写 Any——那等于把代理端口暴露给你连过的每一个 WiFi。想删掉这条规则用 Remove-NetFirewallRule -DisplayName "Proxy for WSL 7897"。
第三步:WSL 里配置代理变量
NAT 模式有个麻烦事:网关 IP 每次重启 WSL 都可能变。所以不要写死 IP,从路由表动态取。
执行 sudo nano /etc/profile.d/proxy.sh 新建文件,内容如下:
bash
# WSL2 (NAT) -> Windows 主机上的代理
# 网关 IP 每次重启会变,所以从路由表动态读取
__proxy_gateway() {
ip route show default 2>/dev/null | awk '/default/ {print $3; exit}'
}
# 启动时探测一次,代理没开就不设变量,避免留下一个连不通的死代理
__proxy_reachable() {
timeout 1 bash -c "exec 3<>/dev/tcp/$1/$2" 2>/dev/null
}
proxy_on() {
local gw port=7897
gw=$(__proxy_gateway)
if [ -z "$gw" ]; then
echo "proxy_on: 找不到默认网关" >&2
return 1
fi
export http_proxy="http://${gw}:${port}"
export https_proxy="$http_proxy"
export all_proxy="socks5h://${gw}:${port}"
export no_proxy='localhost,127.0.0.1,::1,172.16.0.0/12,192.168.0.0/16,10.0.0.0/8,*.local'
export HTTP_PROXY="$http_proxy"
export HTTPS_PROXY="$https_proxy"
export ALL_PROXY="$all_proxy"
export NO_PROXY="$no_proxy"
echo "proxy on -> ${gw}:${port}"
}
proxy_off() {
unset http_proxy https_proxy all_proxy HTTP_PROXY HTTPS_PROXY ALL_PROXY
export no_proxy='localhost,127.0.0.1,::1'
export NO_PROXY="$no_proxy"
echo "proxy off"
}
proxy_status() {
echo "http_proxy=${http_proxy:-<未设置>}"
printf '实际出口: '
curl -sS -m 12 https://www.cloudflare.com/cdn-cgi/trace 2>/dev/null \
| awk -F= '/^ip=/{ip=$2} /^loc=/{loc=$2} END{if (ip) print ip" ("loc")"; else print "不可达"}'
}
# 代理端口真的在监听时才自动开启
if [ -z "${PROXY_AUTO_OFF:-}" ]; then
__gw=$(__proxy_gateway)
if [ -n "$__gw" ] && __proxy_reachable "$__gw" 7897; then
proxy_on >/dev/null
fi
unset __gw
fi
/etc/profile.d/ 只被登录 shell 加载,所以在 /root/.bashrc 末尾再 source 一次,保证非登录 shell(VS Code 集成终端、bash -c 之类)也生效:
if [ -f /etc/profile.d/proxy.sh ]; then
. /etc/profile.d/proxy.sh
fi
验证
重开 WSL 终端执行 proxy_status,正常会打印 http_proxy=http://172.18.48.1:7897 和变化后的出口地区。再对比一下开关效果,确认代理真的在起作用:
proxy_off && curl -s https://www.cloudflare.com/cdn-cgi/trace | grep -E '^(ip|loc)='
proxy_on && curl -s https://www.cloudflare.com/cdn-cgi/trace | grep -E '^(ip|loc)='
两次输出的 ip 和 loc 应该不一样。如果一样,说明代理没生效。
一个典型的失败例子
启动 WSL 的时候如果看到检测到 localhost 代理配置,但未镜像到 WSL。NAT 模式下的 WSL 不支持 localhost 代理。这句警告,说明你正好撞上了本章开头讲的那个坑。这不是报错,是 WSL 在提醒你「你的代理配置在 WSL 里用不了」。 它由 wslservice.exe 在启动时发出,只出现在 NAT 模式下。
原因就是前面说过的那件事:Windows 系统代理设置里写的是 127.0.0.1:7897,而 NAT 模式下 WSL 是一台独立的虚拟机,它的 127.0.0.1 指向自己,不是 Windows。所以这个地址搬到 WSL 里没有任何意义 —— WSL 检测到了这个配置,也知道自己没办法用,就打了这行提示。
这行警告本身不影响 WSL 启动和使用,只是告诉你代理不通。可以按两个方向解决:
方向一:留在 NAT 模式,按本节前面三步配。 开 allow-lan → 放行防火墙 → 用网关 IP 而不是 127.0.0.1。配好之后警告可能还在(它只看 Windows 那边的注册表设置),但 WSL 里的代理已经能用了,proxy_status 出口 IP 变了就说明成了。如果不想再看到这行提示,在 .wslconfig 的 [wsl2] 段里把 autoProxy 显式设成 false。
方向二:换 mirrored 模式,从根上消除。 镜像模式下两边共用网络栈,127.0.0.1 就是 Windows 的 loopback,这个警告不会再出现,代理也不用做任何额外配置。做法见下一节。
顺便说一个容易搞混的点:这行警告和 autoProxy=true 是两件事。autoProxy=true 只是让 WSL 尝试继承 Windows 的代理设置,但如果继承过来的地址是 127.0.0.1,在 NAT 模式下照样连不通 —— 继承了一个用不了的地址,反而更难排查。所以我在 NAT 模式下建议 autoProxy=false,代理用第三步的显式配置,行为完全可控。
Mirrored 模式下的代理配置
如果你用的是 Windows 11 22H2 及以上,我更推荐直接换 mirrored——代理配置能省掉一大半。
1. 启用 mirrored
编辑 C:\Users\<你的用户名>\.wslconfig:
[wsl2]
networkingMode=mirrored
dnsTunneling=true
autoProxy=true
firewall=true
[experimental]
hostAddressLoopback=true
各项作用:
networkingMode=mirrored 与 Windows 共用网络栈,两边 localhost 一致
dnsTunneling=true 改善 VPN / 复杂网络下的 DNS 兼容性
autoProxy=true 继承 Windows 系统代理设置
firewall=true 让 Windows 防火墙策略同样作用于 WSL
hostAddressLoopback=true 允许用主机真实 IP 互访(不只是 127.0.0.1)
然后执行 wsl --shutdown,重开终端后用 ip -brief addr 确认:看到和 Windows 一样的网卡 IP、并且多出 loopback0,就说明换成功了。
2. 配置代理
这是 mirrored 最大的好处:直接用 127.0.0.1。
export http_proxy=http://127.0.0.1:7897
export https_proxy=$http_proxy
export all_proxy=socks5h://127.0.0.1:7897
export no_proxy='localhost,127.0.0.1,::1,*.local'
export HTTP_PROXY="$http_proxy"
export HTTPS_PROXY="$https_proxy"
export ALL_PROXY="$all_proxy"
export NO_PROXY="$no_proxy"
对比 NAT 模式,这里省掉了三件事:不需要开 allow-lan(127.0.0.1 就是 Windows 的 loopback)、不需要加防火墙规则(loopback 流量不过防火墙)、不需要动态取网关(地址固定,不会漂移)。
这段同样可以放进 /etc/profile.d/proxy.sh,把 NAT 那版里的动态网关换成 127.0.0.1 就行,proxy_on / proxy_off 两个函数照样留着方便切换。
另外开了 autoProxy=true 之后,很多情况下 WSL 会自动带上代理环境变量,先用 env | grep -i proxy 和上面那条 cdn-cgi/trace 测一下——如果出口 IP 已经是代理的地址,那就什么都不用配。
3. Mirrored 的几个注意点
换之前先知道这些,免得踩坑再回滚:
端口会和 Windows 冲突。WSL 里监听 0.0.0.0:3000 和 Windows 上监听 3000 的程序会互相抢端口。开发常用端口(3000、5173、8080)要留意。
Docker / 桥接网络兼容性。Docker Desktop 的 WSL 后端、需要自定义桥接或 macvlan 的场景,在 mirrored 下偶有问题。如果你重度依赖这些,NAT 更稳。
入站访问受防火墙约束。firewall=true 会让 Windows 防火墙策略作用到 WSL,从局域网访问 WSL 里的服务可能需要额外放行。
需要 Windows 11 22H2+。Windows 10 不支持,只能用 NAT 方案。
4. 想回退到 NAT
把 .wslconfig 改回 networkingMode=NAT 那一套(见本章「NAT 模式的基础配置」),然后 wsl --shutdown。记得同时把代理配置里的 127.0.0.1 换回动态网关写法,否则回退后代理会失效。
五、补齐开发工具
不要把 WSL 当成“又一个黑盒”。网络通了之后,先把基础工具补齐。在 PowerShell 里执行 wsl 进入 Ubuntu,或者直接从开始菜单打开 Ubuntu。
基础工具
sudo apt update
sudo apt upgrade -y
sudo apt install -y git curl wget unzip zip build-essential ripgrep fd-find jq tree
ripgrep 别省,AI Agent 搜代码几乎都优先调 rg,比 grep -R 快一个量级。
Node.js 和 npm
三个 Agent 都是 npm 包,Node 必须装。别直接 apt install nodejs,Ubuntu 仓库的版本偏旧,跑新版 CLI 容易报错。用 NodeSource 官方源装当前 LTS:
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
sudo apt install -y nodejs
node -v && npm -v && which node
which node 必须是 /usr/bin/node。如果输出 /mnt/c/... 这种 Windows 路径,说明你在 WSL 里调用了 Windows 的 Node,第六章最后一节专门讲这个坑。
注意 sudo 默认会清空环境变量,代理也就跟着丢了,所以上面用的是 sudo -E(保留环境变量)。同理,sudo apt 走不了代理,需要的话单独给 apt 配:
echo "Acquire::http::Proxy \"http://$(ip route show default | awk '{print $3}'):7897\";" | sudo tee /etc/apt/apt.conf.d/95proxy
Python
Ubuntu 24.04 自带 Python 3.12,用 sudo apt install -y python3-pip python3-venv 补上 pip 和虚拟环境模块。
24.04 开始强制 PEP 668,全局 pip install 会被直接拒绝,用虚拟环境或者装 uv(更快,现在更推荐):
curl -fsSL https://astral.sh/uv/install.sh | sh
顺手的工具
sudo apt install -y gh tmux htop
gh 是 GitHub 官方 CLI,Agent 开 PR、看 issue 用得上,装完执行 gh auth login 授权。tmux 能让 Agent 的长任务在你关掉终端后继续跑。
哪些步骤需要代理
必须挂代理的是 deb.nodesource.com(Node)、astral.sh(uv)和 gh auth login。apt 官方源国内可以直连,嫌慢的话换清华镜像。
别让 Windows Node 和 WSL Node 混在一起
这是 WSL + AI Agent 里另一个非常容易出现的问题。如果你在 Windows 装了 Node.js、npm,WSL 里又装了一份,检查一下:
which node
which npm
输出应该指向 Linux 环境里的 Node,比如 /usr/bin/node 或 /root/.nvm/versions/node/...。如果是 /mnt/c/Program Files/nodejs/node.exe 这种 Windows 路径,说明你现在实际上在 WSL 里调用 Windows 的 Node,很容易制造诡异问题。
所以我的建议是:Linux 工具尽量留在 Linux,Windows 工具留在 Windows,不要随意把两边的 PATH 混在一起。
六、安装并配置 AI Agent
到了这里,整个环境已经发生了一个关键变化:以前是 Windows → PowerShell → AI Agent,现在是 Windows → WSL → Bash → AI Agent。
这时候再安装 Codex、Claude Code、OpenCode,很多问题会自然消失。下面的安装配置是不下载 CC-Switch 的情况下自己配置 WSL 中的各个 CLI。
Codex
我们是在 WSL 中运行,就可以直接按照 Linux 环境来安装:
npm install -g @openai/codex
安装完成之后在 WSL 的文件系统里找到 /root/.codex/config.toml 和 /root/.codex/auth.json(用普通用户的话是 /home/<你的用户名>/.codex/,文件不存在就自己创建)。下面以我自建的小站 https://sivan.bond/ 为例,各位根据自己的站点改 base_url 即可,先写 config.toml:
model_provider = "OpenAI"
[model_providers.OpenAI]
name = "OpenAI"
base_url = "https://api.sivan.bond/v1"
wire_api = "responses"
requires_openai_auth = true
supports_websockets = true
认证密钥 auth.json:
{
"auth_mode": "apikey",
"OPENAI_API_KEY": "你的API密钥"
}
Claude Code
在 WSL 中安装 Claude Code:
npm install -g @anthropic-ai/claude-code
以我自建的网关为例进行最小配置,创建 /root/.claude/settings.json(普通用户是 /home/<你的用户名>/.claude/settings.json):
{
"env": {
"ANTHROPIC_BASE_URL": "https://api.sivan.bond",
"ANTHROPIC_AUTH_TOKEN": "你的API密钥",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-opus-5",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-opus-5",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-opus-5",
"ANTHROPIC_SMALL_FAST_MODEL": "claude-opus-5",
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
"CLAUDE_CODE_ATTRIBUTION_HEADER": "0",
"DISABLE_INSTALLATION_CHECKS": "1",
"ENABLE_TOOL_SEARCH": "1"
},
"model": "claude-opus-5"
}
这里有几个容易踩的点,我踩过所以专门说明一下。
用 ANTHROPIC_AUTH_TOKEN,不要用 ANTHROPIC_API_KEY。 前者发的是 Authorization: Bearer <key>,后者发的是 x-api-key: <key>。绝大多数第三方网关只认 Bearer,用错了会拿到 401,而错误信息通常只说「认证失败:请检查 API Key 是否正确」,很容易以为是 key 本身的问题。
四个模型变量都要指向你实际有的模型。 Claude Code 除了主模型,还会用 haiku 跑后台小任务(会话标题、文件摘要之类)。如果你的网关只有 opus 一个模型,不做重定向那些后台调用会静默失败。
model 建议写完整模型名,不要用 opus[1m] 这类别名。 别名要靠客户端映射到官方模型 ID,在第三方网关上不一定对得上。
配置完成,进入项目目录启动:
cd /root/AI_Workspace
claude
应该就能进入 Claude Code 了。如果启动时提示地域限制,说明 WSL 的代理没配好。这里的症状容易误判——直连时 claude.ai 会 302 跳到 app-unavailable-in-region,而那个地址在国内直连是连接超时,所以你看到的不是干脆的报错,而是卡一百多秒然后提示连接失败。回到第四章把代理配通即可。
OpenCode
在 WSL 中安装:
npm install -g opencode-ai
配置 OpenCode,创建 /root/.config/opencode/opencode.jsonc(普通用户是 /home/<你的用户名>/.config/opencode/opencode.jsonc)。OpenCode 用第三方模型需要自己写模型定义,确实有点麻烦,以下是我常用的模型和配置,各位可以参考:
opencode.jsonc
{
"$schema": "https://opencode.ai/config.json",
"model": "sivan/gpt-5.6-sol",
"provider": {
"sivan": {
"name": "sivan",
"npm": "@ai-sdk/openai-compatible",
//"npm": "@ai-sdk/openai",
"options": {
"apiKey": "你的API密钥",
//端点base_url,可以根据你的站点切换
"baseURL": "https://api.sivan.bond/v1"
},
"models": {
"gpt-5.6-sol": {
"name": "gpt-5.6-sol",
"attachment": true,
"modalities": { "input": ["text", "image"], "output": ["text", "image"] },
"limit": { "context": 372000, "output": 131072 },
"options": { "reasoningEffort": "max", "textVerbosity": "medium", "reasoningSummary": "auto" },
"variants": {
"low": { "reasoningEffort": "low", "textVerbosity": "medium", "reasoningSummary": "auto" },
"medium": { "reasoningEffort": "medium", "textVerbosity": "medium", "reasoningSummary": "auto" },
"high": { "reasoningEffort": "high", "textVerbosity": "medium", "reasoningSummary": "auto" },
"xhigh": { "reasoningEffort": "xhigh", "textVerbosity": "medium", "reasoningSummary": "auto" },
"max": { "reasoningEffort": "max", "textVerbosity": "medium", "reasoningSummary": "auto" }
}
},
"gpt-5.6-terra": {
"name": "gpt-5.6-terra",
"attachment": true,
"modalities": { "input": ["text", "image"], "output": ["text", "image"] },
"limit": { "context": 372000, "output": 131072 },
"options": { "reasoningEffort": "max", "textVerbosity": "medium", "reasoningSummary": "auto" },
"variants": {
"low": { "reasoningEffort": "low", "textVerbosity": "medium", "reasoningSummary": "auto" },
"medium": { "reasoningEffort": "medium", "textVerbosity": "medium", "reasoningSummary": "auto" },
"high": { "reasoningEffort": "high", "textVerbosity": "medium", "reasoningSummary": "auto" },
"xhigh": { "reasoningEffort": "xhigh", "textVerbosity": "medium", "reasoningSummary": "auto" },
"max": { "reasoningEffort": "max", "textVerbosity": "medium", "reasoningSummary": "auto" }
}
},
"gpt-5.6-luna": {
"name": "gpt-5.6-luna",
"attachment": true,
"modalities": { "input": ["text", "image"], "output": ["text", "image"] },
"limit": { "context": 372000, "output": 131072 },
"options": { "reasoningEffort": "max", "textVerbosity": "medium", "reasoningSummary": "auto" },
"variants": {
"low": { "reasoningEffort": "low", "textVerbosity": "medium", "reasoningSummary": "auto" },
"medium": { "reasoningEffort": "medium", "textVerbosity": "medium", "reasoningSummary": "auto" },
"high": { "reasoningEffort": "high", "textVerbosity": "medium", "reasoningSummary": "auto" },
"xhigh": { "reasoningEffort": "xhigh", "textVerbosity": "medium", "reasoningSummary": "auto" },
"max": { "reasoningEffort": "max", "textVerbosity": "medium", "reasoningSummary": "auto" }
}
},
"gpt-5.5": {
"name": "gpt-5.5",
"attachment": true,
"modalities": { "input": ["text", "image"], "output": ["text", "image"] },
"limit": { "context": 256000, "output": 131072 },
"options": { "reasoningEffort": "xhigh", "textVerbosity": "medium", "reasoningSummary": "auto" },
"variants": {
"low": { "reasoningEffort": "low", "textVerbosity": "medium", "reasoningSummary": "auto" },
"medium": { "reasoningEffort": "medium", "textVerbosity": "medium", "reasoningSummary": "auto" },
"high": { "reasoningEffort": "high", "textVerbosity": "medium", "reasoningSummary": "auto" },
"xhigh": { "reasoningEffort": "xhigh", "textVerbosity": "medium", "reasoningSummary": "auto" }
}
},
"glm-5.2": {
"name": "glm-5.2",
"attachment": true,
"modalities": { "input": ["text"], "output": ["text"] },
"limit": { "context": 1000000, "output": 131072 },
"options": { "thinking": { "type": "enabled" }, "reasoningEffort": "max" },
"variants": {
"high": { "thinking": { "type": "enabled" }, "reasoningEffort": "high" },
"xhigh": { "thinking": { "type": "enabled" }, "reasoningEffort": "xhigh" },
"max": { "thinking": { "type": "enabled" }, "reasoningEffort": "max" }
}
},
"grok-4.6": {
"name": "grok-4.6",
"attachment": true,
"modalities": { "input": ["text", "image"], "output": ["text"] },
"limit": { "context": 500000, "output": 131072 },
"options": { "reasoningEffort": "high" },
"variants": {
"low": { "reasoningEffort": "low" },
"medium": { "reasoningEffort": "medium" },
"high": { "reasoningEffort": "high" }
}
},
"deepseek-v4-flash": {
"id": "deepseek-v4-flash",
"limit": { "context": 1000000, "output": 65536 },
"reasoning": true,
"options": { "reasoningEffort": "xhigh" },
"variants": {
"low": { "reasoningEffort": "low" },
"medium": { "reasoningEffort": "medium" },
"high": { "reasoningEffort": "high" },
"xhigh": { "reasoningEffort": "xhigh" },
"max": { "reasoningEffort": "max" }
}
},
"deepseek-v4-pro": {
"id": "deepseek-v4-pro",
"limit": { "context": 1000000, "output": 65536 },
"reasoning": true,
"options": { "reasoningEffort": "xhigh" },
"variants": {
"low": { "reasoningEffort": "low" },
"medium": { "reasoningEffort": "medium" },
"high": { "reasoningEffort": "high" },
"xhigh": { "reasoningEffort": "xhigh" },
"max": { "reasoningEffort": "max" }
}
},
"claude-opus-5": {
"name": "claude-opus-5",
"attachment": true,
"reasoning": true,
"modalities": { "input": ["text", "image"], "output": ["text"] },
"limit": { "context": 1000000, "output": 128000 },
"options": { "reasoning": { "effort": "xhigh" }, "textVerbosity": "medium", "reasoningSummary": "auto" },
"variants": {
"low": { "reasoning": { "effort": "low" }, "textVerbosity": "medium", "reasoningSummary": "auto" },
"medium": { "reasoning": { "effort": "medium" }, "textVerbosity": "medium", "reasoningSummary": "auto" },
"high": { "reasoning": { "effort": "high" }, "textVerbosity": "medium", "reasoningSummary": "auto" },
"xhigh": { "reasoning": { "effort": "xhigh" }, "textVerbosity": "medium", "reasoningSummary": "auto" },
"max": { "reasoning": { "effort": "max" }, "textVerbosity": "medium", "reasoningSummary": "auto" }
}
},
"claude-opus-4-8": {
"name": "claude-opus-4-8",
"attachment": true,
"reasoning": true,
"modalities": { "input": ["text", "image"], "output": ["text"] },
"limit": { "context": 1000000, "output": 128000 },
"options": { "reasoning": { "effort": "xhigh" }, "textVerbosity": "medium", "reasoningSummary": "auto" },
"variants": {
"low": { "reasoning": { "effort": "low" }, "textVerbosity": "medium", "reasoningSummary": "auto" },
"medium": { "reasoning": { "effort": "medium" }, "textVerbosity": "medium", "reasoningSummary": "auto" },
"high": { "reasoning": { "effort": "high" }, "textVerbosity": "medium", "reasoningSummary": "auto" },
"xhigh": { "reasoning": { "effort": "xhigh" }, "textVerbosity": "medium", "reasoningSummary": "auto" },
"max": { "reasoning": { "effort": "max" }, "textVerbosity": "medium", "reasoningSummary": "auto" }
}
}
}
}
}
}
然后进入项目目录打开 opencode:
cd /root/AI_Workspace
opencode
七、总结
整套环境长什么样
Windows 浏览器、微信、VS Code、文件管理器
└── WSL 2 bash、git、node、python、AI Agent
└── /root/AI_Workspace 所有项目
Windows 负责桌面,WSL 负责开发,Agent 跑在 WSL 里面。文章开头那些「引号不匹配」「grep 不存在」「export 无法使用」的报错,基本都是因为 Agent 以为自己在 Linux 里,而你给的是 PowerShell。把它真的放进 Linux,这类问题就不用再管了。
装机顺序
1. 装 WSL(用 --location 放到非系统盘),重启,确认 VERSION = 2
2. 配网络:NAT 三步 或 换 mirrored
3. 装工具:apt 基础包 → Node → Python
4. 建 /root/AI_Workspace,项目放 WSL 自己的文件系统
5. 装 Agent:codex / claude / opencode
顺序别调。第 2 步不做,第 3 步的 Node、uv、gh auth login 全都会卡;第 4 步放错位置,后面所有涉及大量小文件的操作都会慢。
我踩过的几个坑
代理三件套缺一不可。 allow-lan、防火墙规则、环境变量。防火墙那步最容易漏,因为大部分教程都不提,症状是「配置看起来全对但就是连不上」。
NAT 模式的网关 IP 会漂移。 别写死,从 ip route show default 动态取。
sudo 会清掉代理。 用 sudo -E,apt 单独配 95proxy。
别让两边的 Node 混在一起。 which node 出现 /mnt/c/... 就是有问题。
发行版别留在 C 盘。 wsl --manage <发行版名> --move D:\... 一条命令就能搬走,ext4.vhdx 长到几十个 G 是常事。
最后
这套环境搭一次,后面新开项目基本零成本。对我来说最大的变化不是快了多少,而是不用再盯着 Agent 反复纠正 shell 语法——它现在的每一条命令都能正常跑,token 和时间都省下来了。

