VS Code 版 Codex 任务完成后,自动通知安卓手机:Windows + ntfy + PowerShell 实战(TaoToken 统一 Key 接入版)

发布时间:2026/10/4 10:08:27
VS Code 版 Codex 任务完成后,自动通知安卓手机:Windows + ntfy + PowerShell 实战(TaoToken 统一 Key 接入版) 1. 为什么 VS Code 里的 Codex 跑完任务我却总是最后一个知道我平时在 Windows 上用 VS Code 写代码Codex 插件负责处理一些批量重构、补测试、生成脚本的活。问题出在等待上一个稍大的任务动辄跑三五分钟我习惯切到浏览器查文档或者去倒杯水回来一看任务早就结束了白白浪费了等待窗口。更尴尬的是有时候任务其实失败了我却以为还在跑干等了十分钟。我想要的效果很朴素Codex 在 VS Code 里把任务跑完的那一刻我的安卓手机能收到一条推送标题写清楚是哪个任务完成正文带一小段结果摘要。这样我就能安心去干别的手机一震再回来处理。这套链路的核心组件有三个。VS Code 是编辑器Codex 插件在里面执行任务并把过程写进本地会话文件。PowerShell 是 Windows 自带的脚本引擎负责监听会话文件的变化。ntfy 是一个开源推送服务安卓端装个 App 订阅一个主题就能收到 HTTP 发过来的消息。三者串起来就实现了「任务完成 → 手机通知」。适合谁看这篇在 Windows 上用 VS Code Codex 插件、希望任务完成有提醒、又不想装一堆第三方软件的开发者。整套方案只依赖 Windows 自带的 PowerShell 和 ntfy 的公开服务不需要额外安装运行时。这里有个前提要先说清楚。Codex 插件在 Windows 下会把会话过程写成 JSONL 文件路径在C:\Users\YourUser\.codex\sessions下面。任务完成事件会以task_complete的形式落盘。我们要做的就是盯着这些文件一旦发现新完成的任务就调用通知脚本。这个思路比直接依赖 Codex 内置的 notify 钩子更稳原因后面会讲。另外Codex 本身需要能正常调用模型。如果你还没配好 API 通道可以先用 TaoToken 的统一 Key 把 Codex 跑通再回来做通知链路。官网入口在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 注册后在控制台生成 Key 即可。下面第二节会给出具体配置。2. TaoToken 统一 Key 接入 Codex 的前置配置与 API 通道说明在折腾通知之前得先保证 Codex 在 VS Code 里能正常干活。Codex 插件读取的是用户目录下的配置文件路径是C:\Users\YourUser\.codex\config.toml。如果你用的是 TaoToken 的统一 Key 通道这个文件里需要写清楚模型提供方、Base URL 和 API Key。TaoToken 的作用是把多家模型的调用收敛到一个 Key 上Codex 只需要认一个 Base URL 和一个 Key就能调用背后的模型。对 Codex 这种需要频繁请求的工具来说省去了每个模型单独配 Key 的麻烦。API 地址是 https://taotoken.net/api 注意这个地址不带任何查询参数直接作为 Base URL 使用。下面是一份可以直接复制的config.toml片段。路径和字段名保持和 Codex 读取时一致你只需要把YOUR_TAOTOKEN_KEY换成自己在控制台生成的 Key# C:\Users\YourUser\.codex\config.toml model claude-sonnet-4-5 model_provider taotoken [model_providers.taotoken] name TaoToken base_url https://taotoken.net/api env_key TAOTOKEN_API_KEY [model_providers.taotoken.headers] X-Client codex-vscode这里有个细节要注意env_key指定的是环境变量名Codex 会从环境变量里读 Key而不是把 Key 明文写在配置文件里。所以你还得在 Windows 里设置一个用户级环境变量。用 PowerShell 执行下面这行把YOUR_TAOTOKEN_KEY替换成真实 Key[Environment]::SetEnvironmentVariable(TAOTOKEN_API_KEY, YOUR_TAOTOKEN_KEY, User)设置完之后要重启 VS Code让新环境变量生效。验证方式是打开 VS Code 的集成终端执行echo $env:TAOTOKEN_API_KEY能打印出 Key 就说明环境变量读到了。如果你更习惯用命令行方式管理 Key也可以走 TaoToken 的 API Keys 页面生成和轮换 Key地址是 https://taotoken.net/api-keys 。生成后同样填进上面的环境变量即可。配置完成后在 VS Code 里让 Codex 跑一个简单任务比如「把当前文件里的 console.log 改成 logger.info」观察它是否能正常返回结果。能正常返回说明 API 通道通了接下来才轮到通知链路。有一点要提醒Codex 的会话文件只有在任务真正执行时才会写入。如果你只是打开插件没跑任务sessions目录可能是空的。所以做通知测试前先确保至少跑过一次任务让目录里有 JSONL 文件。3. 可复制的 PowerShell 监听脚本与 ntfy 主题配置这一节是整篇的核心。我们要写两个脚本一个负责发通知一个负责监听会话文件。先配 ntfy 主题再写脚本。ntfy 的使用方式很简单你在手机 App 里订阅一个主题名然后往https://ntfy.sh/主题名发一个 HTTP POST手机就会收到推送。主题名相当于一个频道建议用不容易被猜到的字符串比如codex-notify-随机串。本文示例统一用YOUR_NTFY_TOPIC占位你替换成自己的即可。先在 Windows 上验证 ntfy 链路是否通。打开 PowerShell执行curl.exe -v -d hello from windows https://ntfy.sh/YOUR_NTFY_TOPIC手机 App 里如果收到这条消息说明推送通道没问题。收不到就检查主题名是否一致、手机是否联网、App 是否给了通知权限。接下来创建通知脚本路径放在C:\Users\YourUser\.codex\codex_ntfy_notify.ps1param($Json) $log $env:USERPROFILE\.codex\notify_log.txt Add-Content -Path $log -Value ( (Get-Date).ToString(yyyy-MM-dd HH:mm:ss) ) Add-Content -Path $log -Value (ARG: $Json) $topic YOUR_NTFY_TOPIC $url https://ntfy.sh/$topic $body Codex task done. Check VS Code. try { curl.exe -s -H Title: Codex Done -H Priority: high -H Tags: computer -d $body $url | Out-Null Add-Content -Path $log -Value SEND: OK } catch { Add-Content -Path $log -Value (SEND: ERROR $_.Exception.Message) }手动测一下这个脚本 C:\Users\YourUser\.codex\codex_ntfy_notify.ps1 {type:task_complete,summary:test}手机收到通知、notify_log.txt里出现SEND: OK就说明通知脚本正常。然后是监听脚本路径C:\Users\YourUser\.codex\codex_task_complete_watch.ps1。它的逻辑是轮询sessions目录下的 JSONL 文件逐行解析发现task_complete事件就提取摘要并调用通知脚本同时用 turn_id 去重避免重复推送param( [string]$CodexHome $env:USERPROFILE\.codex, [int]$PollIntervalMs 1200 ) $sessionRoot Join-Path $CodexHome sessions $notifyScript Join-Path $CodexHome codex_ntfy_notify.ps1 $watchLog Join-Path $CodexHome codex_watch_log.txt $stateDir Join-Path $CodexHome tmp $seenPath Join-Path $stateDir codex_task_complete_seen.json if (-not (Test-Path -LiteralPath $stateDir)) { New-Item -ItemType Directory -Path $stateDir | Out-Null } if (-not (Test-Path -LiteralPath $seenPath)) { Set-Content -LiteralPath $seenPath -Value [] } $seenTurns () try { $seenTurns (Get-Content -LiteralPath $seenPath -Raw | ConvertFrom-Json) } catch {} $fileOffsets {} function Write-WatchLog($msg) { Add-Content -Path $watchLog -Value ([ (Get-Date).ToString(yyyy-MM-dd HH:mm:ss) ] $msg) } function Save-SeenTurns($items) { $json ConvertTo-Json -InputObject ($items | Sort-Object -Unique) -Compress [System.IO.File]::WriteAllText($seenPath, $json, [System.Text.UTF8Encoding]::new($false)) } Write-WatchLog Watcher started while ($true) { $files Get-ChildItem -LiteralPath $sessionRoot -Recurse -File -Filter *.jsonl -ErrorAction SilentlyContinue foreach ($file in $files) { if (-not $fileOffsets.ContainsKey($file.FullName)) { $fileOffsets[$file.FullName] 0L Write-WatchLog (Tracking new file: $file.FullName) } $stream [System.IO.File]::Open($file.FullName, Open, Read, ReadWrite) try { $offset [long]$fileOffsets[$file.FullName] if ($offset -gt $stream.Length) { $offset 0 } $stream.Seek($offset, [System.IO.SeekOrigin]::Begin) | Out-Null $reader New-Object System.IO.StreamReader($stream) while (-not $reader.EndOfStream) { $line $reader.ReadLine() try { $entry $line | ConvertFrom-Json -ErrorAction Stop if ($entry.type -eq event_msg -and $entry.payload.type -eq task_complete) { $turnId [string]$entry.payload.turn_id if ($seenTurns -contains $turnId) { continue } $summary [string]$entry.payload.last_agent_message $summary ($summary -replace \s, ).Trim() if ($summary.Length -gt 160) { $summary $summary.Substring(0, 160) ... } $payload { type task_complete source codex_session_watcher turn_id $turnId summary $summary detected_at (Get-Date).ToString(s) } | ConvertTo-Json -Compress $notifyScript $payload $seenTurns ($seenTurns $turnId) Save-SeenTurns $seenTurns Write-WatchLog (Notified turn_id $turnId) } } catch {} } $fileOffsets[$file.FullName] $stream.Position $reader.Dispose() } finally { $stream.Dispose() } } Start-Sleep -Milliseconds $PollIntervalMs }再写两个辅助脚本一个启动、一个停止。启动脚本codex_task_complete_watch_start.ps1$watcher $env:USERPROFILE\.codex\codex_task_complete_watch.ps1 $pidPath $env:USERPROFILE\.codex\codex_task_complete_watch.pid $proc Start-Process -FilePath powershell.exe -ArgumentList ( -NoProfile, -ExecutionPolicy, Bypass, -File, $watcher ) -WindowStyle Hidden -PassThru Set-Content -LiteralPath $pidPath -Value $proc.Id Write-Output (Watcher started. PID $proc.Id)停止脚本codex_task_complete_watch_stop.ps1$pidPath $env:USERPROFILE\.codex\codex_task_complete_watch.pid if (Test-Path -LiteralPath $pidPath) { $watchPid [int](Get-Content -LiteralPath $pidPath -Raw).Trim() $proc Get-Process -Id $watchPid -ErrorAction SilentlyContinue if ($proc) { Stop-Process -Id $watchPid } Remove-Item -LiteralPath $pidPath -ErrorAction SilentlyContinue }到这里三个脚本加一个 ntfy 主题就齐了。启动脚本用-WindowStyle Hidden让 watcher 在后台跑不占你的终端窗口。4. 一次任务完成到手机收通知的完整验证脚本写完了得跑一遍完整链路确认从 Codex 任务完成到手机震动这条路径是通的。第一步启动 watcher C:\Users\YourUser\.codex\codex_task_complete_watch_start.ps1输出里会打印 PID比如Watcher started. PID12345。这时候去看codex_watch_log.txt应该有一行Watcher started。第二步回到 VS Code让 Codex 跑一个能明确结束的任务。比如选中一段代码让它「给这个函数补上参数校验和单元测试」。任务执行过程中Codex 会往sessions目录写 JSONL。任务结束时会落一条task_complete事件。第三步观察 watcher 日志。任务完成后几秒内codex_watch_log.txt里应该出现类似[2025-01-15 14:32:10] Tracking new file: C:\Users\YourUser\.codex\sessions\...\rollout-xxx.jsonl [2025-01-15 14:32:45] Notified turn_idabc123第四步看通知日志notify_log.txt应该有SEND: OK。同时手机 ntfy App 收到一条标题为Codex Done的推送正文是任务摘要。第五步确认去重文件tmp\codex_task_complete_seen.json里写入了这次的 turn_id。这样同一个任务不会重复推送。如果这五步都过了说明整条链路正常。之后你只要保持 watcher 在后台运行每次 Codex 任务完成都会自动推送到手机。这里补充一个实测细节watcher 的轮询间隔是 1200 毫秒任务完成后通常 1 到 3 秒内就能收到通知。如果你觉得延迟明显可以把PollIntervalMs调小到 500但会增加一点 CPU 占用。反过来如果你机器负载高调到 2000 也没问题通知晚一两秒不影响使用。还有一点watcher 是按文件偏移量增量读取的不会重复解析已经读过的行。所以即使sessions目录里积累了很多历史文件启动时也不会把旧任务重新推一遍。这个设计对长期挂着 watcher 的场景很重要。5. 常见报错排查401、os error 206、local proxy failed 与 OAuth 问题链路跑不通时报错通常集中在几个地方。下面按真实遇到的错误逐个排查。401 Unauthorized。这个多半是 TaoToken 的 Key 没配对。先确认环境变量TAOTOKEN_API_KEY是否设置成功在 PowerShell 里执行echo $env:TAOTOKEN_API_KEY如果为空说明环境变量没生效重启 VS Code 或重新登录 Windows 用户。如果环境变量有值但 Codex 还是 401检查config.toml里的env_key字段是否和实际环境变量名一致大小写敏感。还有一种情况是 Key 被轮换过旧 Key 失效去 https://taotoken.net/api-keys 重新生成一个填进去。os error 206。这个错误在 Windows 上很典型含义是「文件名或扩展名太长」。它出现在 Codex 内置 notify 钩子触发时因为任务完成传给脚本的 JSON 参数可能非常长超过了 Windows 命令行参数的长度限制。这也是本文不直接用内置 notify、改用外部 watcher 的原因。如果你在日志里看到after_agent hook failed ... hook_namelegacy_notify ... (os error 206)不用去改内置 notify直接用第三节的 watcher 方案绕开即可。local proxy failed。这个报错通常和网络配置有关。先确认config.toml里的base_url写的是https://taotoken.net/api没有多余路径或参数。然后检查系统里是否设置了会干扰请求的环境变量比如HTTP_PROXY、HTTPS_PROXY。如果有临时清掉再试Remove-Item Env:HTTP_PROXY -ErrorAction SilentlyContinue Remove-Item Env:HTTPS_PROXY -ErrorAction SilentlyContinue清掉后重启 VS Code。如果公司网络有统一出口按网络管理员给的配置来不要自己乱设。OAuth 相关报错。Codex 某些版本会走 OAuth 流程如果报 OAuth 失败先确认你用的是 API Key 模式而不是登录模式。在config.toml里确保model_provider指向的是taotoken并且env_key对应的环境变量有值。OAuth 报错有时是因为本地缓存的凭证过期删掉C:\Users\YourUser\.codex下的缓存文件注意别删sessions和脚本重新让 Codex 读取配置。手机收不到通知。先单独测 ntfycurl.exe -d test https://ntfy.sh/YOUR_NTFY_TOPIC。收不到就检查主题名、手机网络、App 通知权限。能收到但 Codex 任务完成时收不到去看codex_watch_log.txt有没有Notified turn_id没有的话说明 watcher 没识别到task_complete检查sessions目录里是否有新的 JSONL 文件以及 watcher 是否在运行看 PID 文件对应的进程是否存在。watcher 启动后立刻退出。多半是 PowerShell 执行策略拦了脚本。启动脚本里已经带了-ExecutionPolicy Bypass如果还是不行手动执行一次Set-ExecutionPolicy -Scope CurrentUser RemoteSigned然后重新启动。排查时记住一个顺序先确认 Codex 本身能跑通API 通道正常再确认 ntfy 单独能收到最后才看 watcher 有没有把两者串起来。按这个顺序问题定位会快很多。6. 把通知链路固定下来日常使用与后续扩展链路验证通过后接下来是让它稳定地融入日常。最直接的做法是把启动脚本加到 Windows 的登录启动项里这样每次开机 watcher 自动在后台跑你不需要手动启动。方法是在shell:startup目录里放一个快捷方式指向codex_task_complete_watch_start.ps1。如果你不想开机自启也可以在每个工作日的开始手动跑一次启动脚本下班前跑停止脚本。PID 文件会记录进程号停止脚本能准确杀掉对应的进程不会误伤其他 PowerShell 窗口。关于通知内容目前正文是固定的Codex task done. Check VS Code.。如果你想让通知带上任务摘要可以改通知脚本把传入的$Json解析出来提取summary字段作为正文。这样手机锁屏上就能直接看到任务结果的前几十个字不用解锁进 VS Code。再进一步你可以按任务类型分流通知。比如在 watcher 里判断摘要里是否包含「test」「build」等关键词给不同的 ntfy 主题发通知手机上用不同主题区分优先级。ntfy 支持在请求头里设置Priority和Tags高优先级的任务可以设成urgent普通任务设成default。如果你同时用多个 AI 编码工具比如 Codex 和 Claude Code可以把通知脚本抽成一个通用模块不同工具完成时都调用它只是传入的标题和摘要不同。这样手机上收到的通知格式统一一眼就能看出是哪个工具跑完了。长期来看这套方案的价值在于把「等待」这件事从你的注意力里拿掉。任务在后台跑完成时手机告诉你你只需要在收到通知后回来处理结果。对于经常让 AI 跑批量任务的开发者这个习惯能省下不少来回切换的时间。最后留一个入口如果你还没配好 Codex 的 API 通道先去 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 拿一个统一 Key按第二节的config.toml配好再回来搭通知链路。接入文档在 https://taotoken.net/doc 里面有各客户端的配置示例。需要长期跑编码任务或 Agent 的可以看看 Coding Plan地址是 https://taotoken.net/coding-plan 。模型对话调试入口在 https://taotoken.net/chat 控制台在 https://taotoken.net/console 。