ChatGPT 桌面端状态点离线的排查与修复脚本
本文完全由 WorkBuddy 独立收集资料、调试代码并完成文章写作。
我是 Alex Xiang,前百度/微博工程师,现在专注于 AI 工程与工具产品。更多文章欢迎关注微信公众号「字与码」。
这个故障有两个看起来毫不相关的症状,但它们是同一件事。
第一个症状:桌面上那个启动脚本突然报错,「应用程序无法启动,因为应用程序的并行配置不正确」。第二个症状:能进应用,但界面里表示本机 Codex 在线状态的那颗小圆点一直是灰的,模型列表拉不出来。
先说结论:第一个是路径问题,第二个是网络栈问题,两者都不需要重装。修复后的启动脚本一共 7 行。

症状一:并行配置不正确
这句报错来自 Windows 的加载器,字面意思是「程序清单里声明的依赖,找不到」。但在这个场景里,它和运行库无关。
看原来的脚本:
@echo off
set "HTTP_PROXY=http://127.0.0.1:1080"
set "HTTPS_PROXY=http://127.0.0.1:1080"
start "" "C:\Program Files\WindowsApps\OpenAI.Codex_26.1013.4894.0_x64__2p2nqsd0c76g0\app\ChatGPT.exe"
pause
那个路径里的 26.1013.4894.0 是包版本号。查一下实际安装的版本:
Get-AppxPackage OpenAI.Codex | Select-Object Version, InstallLocation
返回的是 26.1016.8622.0——应用在 10 月 23 日自动更新过,目录名变了,脚本还在指旧目录。
诡异的地方在于:旧目录并没有被删除。它还在,里面还有 ChatGPT.exe,甚至 Test-Path 返回 True。但内容已经不完整了:正常版本有 35 个文件,旧目录只剩 12 个。这种「残留空壳」比直接消失更麻烦——系统不会报「找不到文件」,而是加载器读到一个残缺的清单和缺失的依赖,于是抛出那句与实际情况完全对不上的「并行配置不正确」。
实测确认了这个判断。直接执行两个路径的 exe:
| 启动目标 | 结果 |
|---|---|
OpenAI.Codex_26.1013.4894.0(旧) | 抛 SxS 异常 |
OpenAI.Codex_26.1016.8622.0(当前) | 正常启动,8 个进程存活 |
结论一:Store 应用的安装目录名里带版本号,任何硬编码路径都会在下一次更新后失效。
顺带一句:旧版本的残留目录不用手动删。C:\Program Files\WindowsApps 的 ACL 是受限的,手动改动容易破坏包注册,交给系统在后续清理时回收更安全。
症状二:状态点为什么是灰的
一个应用,两个进程
这里要拆开一个常见误解:ChatGPT 桌面端并不是「一个程序」,而是两个进程、两套网络栈。
包里的文件结构是这样的:ChatGPT.exe(4.8MB)配着 626MB 的 app.asar、chrome.dll、icudtl.dat——这是一层 Electron 外壳。旁边还有一个 resources\codex.exe,336MB,是 Rust 编译的 CLI。
应用启动时,外壳会把 CLI 作为子进程拉起来,两者通过标准输入输出跑 JSON-RPC。这一点在日志里能直接看到:
rpc.transport="stdio"
app_server.client_name="Codex Desktop"
client_version="26.1016.62342"
Request completed url=https://chatgpt.com/backend-api/codex/models status=200 OK
← codex_http_client::client
transport="stdio" 说明连接方式是本地管道,client_name="Codex Desktop" 说明外壳只是客户端。而所有真正出网的请求——模型列表、插件目录、在线状态——都记在 codex_http_client 这个 Rust 模块名下。
也就是说:界面上那颗表示「本机 Codex 在线」的圆点,判定依据是 CLI 拉取模型列表是否成功,跟外壳自己的联网状态无关。
代理配置不共享
两套栈的关键差异在代理上:
- Chromium 那一套,走 Windows 的 WinINet,也就是「设置 → 网络 → 代理」里那个系统代理;
- Rust 那一套,走 reqwest,只读进程环境变量
HTTP_PROXY/HTTPS_PROXY/ALL_PROXY,完全不看 WinINet。
在 CLI 的二进制里能搜到这些变量名,大写出现 13 次,大写加小写合计二十多处,就是这段读环境的代码。反过来,「系统代理」相关的 WinINet API,Rust 侧一个都没有。
这不是 ChatGPT 独有的毛病。凡是「Electron 外壳 + 原生子进程」的应用,都可能踩同一颗雷:VS Code 的扩展宿主、Discord 的语音进程、各种 IDE 的调试器,出网走的是各自的 HTTP 客户端,代理配置并不共享。判断方法很统一——先确认真正发请求的是哪个进程,再看那个进程用的是哪套网络库。

于是现象就解释通了:系统代理开着,外壳能上网,但 CLI 拿不到代理,请求超时,圆点变灰。

为什么必须「设定环境变量再启动」
环境变量是进程创建那一刻从父进程复制过来的,之后不会再变。
所以同样是「启动 ChatGPT」,两种启动方式的继承结果完全不同:

| 启动方式 | 谁能拿到你设的代理变量 |
|---|---|
start "" "%EXE%" 直接运行 exe | 能。exe 是 cmd 的子进程,完整继承 |
start "" shell:AppsFolder\...!App 包身份激活 | 不能。由 Explorer 代为拉起,继承的是 Explorer 的环境 |
这就是为什么「设环境变量再启动」这个思路本身是对的,而且只有直接启动 exe 这条路才成立。顺带说明:setx 写持久化环境变量也能生效,但那会污染全局,为了一个启动动作不值得。
验证:三条命令看清真相
CLI 自带一个诊断子命令,路径在 %LOCALAPPDATA%\OpenAI\Codex\bin\<hash>\codex.exe:
:: 不带代理环境变量
codex doctor
:: 带代理环境变量
set HTTPS_PROXY=http://127.0.0.1:1080 && codex doctor
对比两次输出的 Notes 段。不带变量时会出现 Responses WebSocket timed out,带上之后这条消失——说明变量确实被 CLI 读到了。
想看得更细,日志库在 ~\.codex\logs_2.sqlite,表名 logs,feedback_log_body 列是 JSON 字符串。Node 22 自带 node:sqlite,十几行就能查出每次请求的结果:
const { DatabaseSync } = require('node:sqlite');
const db = new DatabaseSync('C:\\Users\\ax2\\.codex\\logs_2.sqlite', { readOnly: true });
const rows = db.prepare(`SELECT ts, level, feedback_log_body FROM logs
WHERE feedback_log_body LIKE '%backend-api/codex/models%' ORDER BY id DESC LIMIT 20`).all();
for (const r of rows) {
const b = r.feedback_log_body;
console.log(new Date(r.ts * 1000).toLocaleString('zh-CN'),
(b.match(/status=(\d+)/) || [])[1] || (/timed out/.test(b) ? 'TIMEOUT' : r.level));
}
如果输出里 200 和 TIMEOUT 交错出现,那就是链路抖动,不是配置错误——这时候该换节点,而不是继续改脚本。
修复后的脚本
@echo off
set "HTTP_PROXY=http://127.0.0.1:1080"
set "HTTPS_PROXY=%HTTP_PROXY%"
set "ALL_PROXY=%HTTP_PROXY%"
set "NO_PROXY=localhost,127.0.0.1,::1"
for /f "usebackq delims=" %%i in (`%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe -NoProfile -Command "(Get-AppxPackage OpenAI.Codex).InstallLocation"`) do set "PKG=%%i"
start "" "%PKG%\app\ChatGPT.exe"
逐行说清:
- 三个代理变量指向本地代理。
ALL_PROXY给不认 HTTP 代理的客户端兜底。 NO_PROXY放行localhost和127.0.0.1。这行不能省,否则本地环回也会被塞进代理,CLI 和外壳之间的本地连接可能被劫持。- 第 5 行是关键:用
Get-AppxPackage取安装位置,每次运行都重新解析,版本号怎么变都不影响。 - 最后直接启动 exe,让子进程继承上面四个变量。
如果应用已经在运行,新设的变量不会穿透到旧进程,得先结束再启动。一行搞定:
taskkill /f /im ChatGPT.exe >nul 2>&1 & timeout /t 2 >nul & start "" "%PKG%\app\ChatGPT.exe"
四个坑
坑一:for /f 里的引号会被吃掉。给 PowerShell 全路径加引号:
for /f "usebackq delims=" %%i in (`"%SystemRoot%\...\powershell.exe" -NoProfile ...`) do set "PKG=%%i"
执行时报 'C:\...powershell.exe" -NoProfile -Command "' 不是内部或外部命令,%PKG% 变成空值,最后报「系统找不到文件 \app\ChatGPT.exe」。这个路径本身不含空格,去掉引号即可。
坑二:不要同时设大写和小写代理变量。网上很多教程把 HTTP_PROXY 和 http_proxy 一起写上,那是 Linux/macOS 的习惯。Windows 的环境变量不区分大小写,两个键名只差大小写,会让 .NET 的 Process.Start 直接抛异常:
已添加项。字典中的关键字:"HTTP_PROXY"所添加的关键字:"http_proxy"
大写一套就够,reqwest 查询时也不区分大小写。
坑三:.bat 里不要写中文注释,文件必须是 CRLF。cmd 在 chcp 生效前按系统代码页解析文件,UTF-8 的中文字节会被拆碎,注释行被当成命令执行,屏幕上冒出一串 'e' is not recognized。要么全英文注释,要么存成 GBK——但 GBK 在别的工具里又容易乱码,最省事的是纯 ASCII 加 CRLF。
坑四:别用包身份激活来「修复」启动失败。用 shell:AppsFolder\OpenAI.Codex_2p2nqsd0c76g0!App 确实能稳定启动,看起来是更好的方案。但它由 Explorer 拉起,你设的代理变量传不进去,症状二会原样复发。
另一种思路:TUN 模式
如果代理客户端开启了 TUN(虚拟网卡),流量在网络层就被接管,进程根本不需要知道代理的存在,环境变量也就无所谓了。两种做法各有取舍:
| 环境变量 | TUN 模式 | |
|---|---|---|
| 生效范围 | 只有继承到变量的进程 | 全系统,含不认代理的客户端 |
| 权限要求 | 普通用户 | 通常需要管理员 |
| 可能影响 | 几乎无 | 局域网、Docker、其他虚拟网卡 |
| 适用场景 | 只想让某个应用走代理 | 整机都要走代理 |
选择环境变量的理由很简单:只想影响 ChatGPT 这一个进程,不想动全局路由。代价是每次启动都得经过这个脚本,忘了点就回到原点。
边界
这套脚本解决的是「本地 CLI 拿不到代理」,不解决别的。
如果日志里一直是超时而不是 200,问题在网络链路。实测这台机器经代理访问 chatgpt.com,五次里四次拿到响应(403 属正常,无凭证访问被 Cloudflare 拦),一次超时;API 端点返回 401 也属正常。约 20% 的丢包率会让圆点反复闪烁——这种情况改脚本没有意义。
另外,企业代理需要认证、或者代理本身是 SOCKS5 时,要写成 socks5://127.0.0.1:1080,reqwest 支持,但 WinINet 那一侧的行为就不一定一致了,两边可能对不上。
最后一个提醒:codex doctor 里那条 ✗ desktop update and runtime CDN is unreachable 标着 optional,探测的是 persistent.oaistatic.com 和 chatgpt.com,跟状态点不是同一项。看到红色叉号不必紧张,先确认是哪一条在报错。
参考资料
codex doctor的实际输出(本地 CLI v0.163.0-alpha.11.2)~\.codex\logs_2.sqlite中logs表的请求记录- 包内目录结构与
AppxManifest.xml的应用声明
微信公众号
欢迎关注「字与码」
如果这篇文章对你有用,也欢迎在微信里继续关注后续更新。
X / Twitter
关注 @ax2_zicode
更即时的技术观察、新文章提醒和一些短想法会发在 X 上。