
起因很朴素我用npm install -g 9router装好 9Router 这个本地 AI 路由服务后希望它在我关掉 PowerShell 窗口之后还能继续跑——这样 Claude Code 随时都能连上它做模型转发而不是每次都要我手动把服务再起一次。围绕这个朴素的诉求从怎么让它后台常驻一路延展到PM2 怎么停、配置改完要不要重启、托管之后再手动跑一次会怎样最后还顺带撞出一个 Windows 网络栈的老问题localhost优先被解析成 IPv6把 Claude Code 的 base_url 直接搞连不上。这篇文章把这条线整理成一份可复用的方案怎么用 PM2 把 9router 守护起来、日常怎么管它以及一个看起来像配置写错了实则和配置无关的连接失败坑。01 先把两件事拆开进程常驻和开机自启不是一回事最初我把这两件事混在一起想要让 9router 在关终端后还跑是不是就得让它开机自启其实这是两个正交的问题。进程常驻守护解决关掉终端窗口进程别跟着死的问题。本质是让进程脱离当前 shell 的生命周期变成独立的后台进程。开机自启解决重启电脑后进程自动被拉起来的问题。本质是把进程注册成系统服务或开机触发项。只要进程被脱离 shell 托管起来关终端它就不死但重启电脑后如果没做开机自启的注册它就不会自动起来——你重新打开终端手动启动一次即可。这点很关键因为它直接决定了你对 PM2 各个命令的预期。明白了这点我想要关终端后还能跑但不要开机自启就是一个完全自洽的需求只要守护不要注册。02 为什么选 PM2而不是别的方案Windows 上做进程常驻有好几种手段我比对过一圈Start-Process -WindowStyle Hidden最简单临时用够用。但关掉终端后虽然能继续跑重启电脑就没了而且没有进程管理能力要靠任务管理器找、kill谈不上运维。PM2Node 生态里最常用的进程守护工具自带崩溃自动重启、日志、进程列表。装好即用命令直观。注册为 Windows 服务node-windows/ NSSM最正式能进服务管理器、能配自启。但对一个我只想关掉终端它还活着的诉求来说步子迈得太大引入的复杂度不划算。任务计划程序适合定时/开机触发不是为常驻守护设计的。诉求是常驻 不要开机自启 最好还能方便地看日志、重启、停PM2 恰好卡在这个甜点上守护是它的本职不要开机自启就不调用pm2 startup即可命令体系也齐全。所以最终选了它。这里有个判断原则值得抽出来选工具时先看你的诉求边界。常驻和自启是独立的两个开关别让一个工具把两件事打包强加给你。PM2 的好处是这两个开关是分开的——pm2 start管守护pm2 startup管自启你只开前一个。03 PM2 的结构决定了停掉 PM2 本身是两步操作要彻底停掉 PM2得先理解它不是单个进程。PM2 由两部分组成PM2 守护进程PM2 v.x.x.x: God Daemon ← 常驻后台的总管 │ ├── 应用进程 A例如 9router ├── 应用进程 B └── ...守护进程是总管你pm2 start出来的应用是它管的工人。所以停掉 PM2是个两层动作# 1. 先把托管的应用从列表里删掉顺带停掉进程释放端口pm2 delete 9router# 或 pm2 delete all 清掉全部# 2. 再把守护进程本身关掉pm2killpm2 kill会把守护进程和所有应用进程一并结束相当于给 PM2关机。重启电脑后它不会自动起因为没配自启。下次再用时随便敲一个pm2 listPM2 就会自动把守护进程拉起来——它不需要被显式启动。如果之前装过开机自启又想取消单独再跑一次取消注册的命令即可pm2-startup uninstall这条命令只动开机自启那个开关不影响你当下正在跑的进程。04 用 PM2 守护 9routerWindows 上要绕一个小弯理论上托管一个工具很直接但 9router 在 Windows 上是通过9router.cmd这个批处理入口暴露的PM2不能直接执行.cmd得用cmd /c包一层并指定好工作目录pm2startcmd--name 9router--cwdC:\Users\wxj20\AppData\Roaming\npm--/c9router --skip-update逐段解释这条命令在设计上为什么这么写pm2 start cmd让 PM2 启动的是系统的cmd.exe而不是直接9router。这是 Windows 上托管.cmd 入口程序的标准写法。--name 9router给这个托管进程起个好听的名字后续pm2 logs 9router、pm2 restart 9router都用它引用。--cwd ...把工作目录设到 npm 全局 bin 所在目录避免cmd /c 9router找不到命令或路径解析出问题。-- /c 9router --skip-update--后面是传给cmd的参数/c表示执行完命令就退出PM2 会接管子进程生命周期。--skip-update是 9router 提供的启动参数跳过启动时的自动更新检查——后台常驻时让它别在启动阶段去联网检查避免启动卡顿或意外行为。启动后用两条命令确认状态pm2 list# 状态应为 onlinepm2 logs 9router# 看日志确认它监听 20128 了9router 默认监听http://localhost:20128看到日志里出现监听信息后浏览器打开http://localhost:20128/dashboard能访问就说明托管成功。最后顺手存一下进程清单pm2 save这里要澄清pm2 save到底干了什么——它不是开机自启。它只是把当前在跑的进程清单写到~/.pm2/dump.pm2这个文件里。作用是以后你手动执行pm2 resurrect或者重启电脑后再用 PM2 时它照着这份清单把 9router 重新拉起来。只要你没跑过pm2 startup就不会开机自启这条pm2 save只是为了方便手动恢复。05 日常管理一份够用的命令清单托起来之后日常打交道的就是下面这几条按使用频率排序pm2 list# 看所有托管进程的状态pm2 logs 9router# 实时看日志CtrlC 退出pm2 logs 9router--lines 100# 看最近 100 行pm2 restart 9router# 重启改了需要重启才生效的项才用pm2 stop 9router# 停住但保留在列表里pm2start9router# 重新启动已停的pm2 delete 9router# 从 PM2 中彻底移除不再托管pm2 monit# 交互式监控面板CPU/内存/日志pm2 flush 9router# 清空它的日志文件不需要全记记住list / logs / restart / stop / start这五条覆盖 90% 的场景。06 托管之后改配置最容易踩的认知误区这个问题我想专门拎出来讲因为它最容易让人做出错误的操作。挂到 PM2 之前我改配置的习惯流程是关掉终端 → 改配置 → 新开一个终端跑一次9router。挂到 PM2 之后这个流程如果照搬会撞墙。原因有两层第二层比第一层更重要。第一层显而易见端口冲突。PM2 后台那个 9router 还在跑、还占着 20128 端口。你新开终端直接9router会再起一个实例去抢同一个端口必然失败报EADDRINUSE之类的端口被占用错误。这一点跟 9router 怎么实现配置完全无关纯粹是两个进程在抢端口。第二层真正反直觉的9router 的配置根本不是改文件。我一开始默认它是那种带文本配置文件的工具YAML/JSON所以改配置在我脑子里等于编辑文件 重启进程。但 9router 不是这样——它是带 Web 仪表盘的本地服务命令行参数只有-p/--port、-H/--host、-n/--no-browser、-l/--log、-t/--tray、--skip-update这些运行参数跟连接哪个 Provider、用哪个 API Key没关系。真正的连接配置存在一个 SQLite 数据库里Windows 路径是%APPDATA%/9router/db/data.sqlite由Dashboard 网页读写。也就是说改 Provider、改 API Key、改模型路由是在浏览器里点 UI 完成的写库的同时即时生效9router 进程完全不需要重启PM2 也不用动。所以托管之后改配置的正确姿势根本不是新开终端跑一次而是starthttp://localhost:20128/dashboard# 在网页里改改完即生效进程不动只有在改了端口 / 主机绑定这类启动参数时才需要pm2 restart 9router——因为这类参数是进程启动时读的数据库里的运行时配置改了也左右不了它。如果你确实想用自己开个终端手动跑一遍的方式比如想盯启动日志验证顺序也得对先停后台那个再手动跑pm2 stop 9router# 先停后台实例释放 201289router--log# 新终端手动跑--log 把日志打到控制台# 验证 OK 后再交回 PM2:pm2start9router这条线最大的收获是认知修正改配置要不要重启进程不取决于习惯、也不取决于工具叫什么取决于配置存在哪、由谁读写。存在文本文件里读一次的工具改完要重启存在数据库里由服务自己读写并热加载的工具改完就生效。下次遇到一个陌生的工具先翻一下它的--help和数据目录结构比凭直觉判断大概要重启吧靠谱得多。07 那个 IPv6 的坑base_url 连不上不是配置写错了9router 托管起来之后下一步是把它作为 Claude Code 的自定义 API 端点。9router 文档里给的 base_url 是http://localhost:20128/v1。我在 Claude Code 的配置里如实填进去——连不上。第一反应是怀疑配置写错了路径对不对、/v1要不要加、API Key 对不对……来回试了一阵都不行。后来才定位到真正的根因和配置半点关系没有是 Windows 的网络解析习惯。问题链条是这样的Claude Code 请求 http://localhost:20128/v1 │ ▼ Windows 解析 localhost │ ▼ 优先解析为 IPv6 地址 ::1 ← 问题在这 │ ▼ 去连 [::1]:20128 │ ▼ 9router / ccswitch 只监听了 IPv4 的 0.0.0.0 / 127.0.0.1 没有监听 IPv6 的 ::1 │ ▼ 连接被拒绝 → 表现为连不上关键点在第一步在 Windows以及很多现代系统上localhost并不无条件等于127.0.0.1。系统会按 DNS/hosts 的解析顺序处理它而** IPv6 的::1通常排在 IPv4 的127.0.0.1前面**。于是客户端拿着localhost出去解析出来的是::1去连 IPv6 端口可服务端那层这次的 ccswitch 只监听了 IPv4 的0.0.0.0/127.0.0.1没监听::1根本没在 IPv6 上开口连接自然被拒。这就解释了为什么配置看起来完全正确却死活连不上——因为出错的地方在配置之下的一层网络栈的地址解析不在配置本身。解决办法很直接绕开localhost写死成 IPv4 地址。http://127.0.0.1:20128/v1把 base_url 从localhost换成127.0.0.1强制走 IPv4目标地址和监听地址对上了连接立刻通。这个坑值得单独沉淀因为它有几个让人晚很久才发现的特征表象会误导你往配置方向排查路径、Key、端口看起来都没问题所以你会一直在配置层打转。它偶发得像是环境问题换台机器、换个网络环境hosts 文件不同、IPv6 优先级不同表现就不同进一步干扰判断。根因在抽象层之下你以为是应用层的问题其实是网络协议层IPv4 vs IPv6的问题。抽象层级不对排查效率就低。由此能抽出一个可复用的排查习惯当一个本地服务配置全对却连不上时把localhost换成127.0.0.1试一次是成本最低、收益最高的第一步。它一眼就能区分是配置错了还是是地址解析在挑事。这条习惯不仅适用于 9router / ccswitch / Claude Code任何localhost连本地服务的场景都通用。更彻底一点的两种治本方式任选其一治标不如治本让服务端同时监听 IPv6如果工具支持绑定到同时覆盖::和0.0.0.0IPv4/IPv6 都能接住。客户端一律写 IP 而非主机名凡连本地服务直接写127.0.0.1从根上摆脱解析层的不确定性。我个人偏好后者——改一个客户端配置比说服每个工具都去监听 IPv6 可控得多。08 最后整理一套可复用的认知和操作清单这次从让 9router 关终端还在跑出发绕了一圈沉淀下来的东西可以归成两类。操作层面用 PM2 守护一个 npm 全局工具尤其 Windows 上是.cmd入口的的标准动作npm install -g pm2装好守护进程会自动起来。用pm2 start cmd --name 工具名 --cwd npm bin 目录 -- /c 工具命令这种cmd /c 包装写法托管注意加--cwd避免路径问题。pm2 save存进程清单这只是方便手动恢复不是开机自启。想停应用用pm2 delete 名想关 PM2 本身先delete再pm2 kill想取消开机自启用pm2-startup uninstall。配置类改动先搞清存哪、谁读写文本文件 →pm2 restart数据库网页热加载 → 直接网页改即生效。认知层面三个值得带走的判断原则常驻和开机自启是两个独立开关别被工具默认行为打包选工具前先看它这两个开关能不能分开。改配置要不要重启取决于配置存哪、由谁读写不靠直觉、不靠工具名翻--help和数据目录就能定性。本地服务配置全对却连不上第一步永远是localhost换成127.0.0.1用最低成本区分配置问题和地址解析问题IPv4 vs IPv6。这三条原则的可迁移性都不限于 9router换成任何 npm 全局工具、任何带 Web UI 的本地服务、任何本地起服务给别的客户端连的场景都适用。延伸阅读PM2 官方文档https://pm2.keymetrics.io/docs/usage/quick-start/9router GitHub 仓库https://github.com/decolua/9router关于localhost在 IPv4/IPv6 上的解析差异RFC 6761 将localhost保留为特殊用途域名操作系统实现中默认会优先解析为 IPv6 的::1再回退到 IPv4 的127.0.0.1。