Skip to content

Commit df7bc6f

Browse files
鲁工鲁工
authored andcommitted
feat: add ccmr status / ccmr stop, keep logs under CCMR_HOME
A gateway auto-started by `ccmr claude` is detached and reparented to init, so it outlives the terminal that spawned it. That is intended — several Claude Code sessions share one gateway — but it left no supported way to find or stop one. A stale gateway then serves 401s invisibly for days, and the only remedy was a `pkill` pattern nobody remembers. - `ccmr status` lists gateways found on 8080-8099 with port, pid, version, config source and ready-model count. A gateway showing "0/28 models ready" and "built-in defaults" is now self-diagnosing. - `ccmr stop [--port|--all|--force]` terminates one. It only ever signals a process that self-identified through /health and reported its own pid: a stranger on the port is reported and explicitly refused, never killed. - /health gains `pid`, which is what makes the above possible without parsing lsof/netstat, and confines `stop` to ccmr's own processes. - The auto-start log path was hardcoded to ~/.ccmr/gateway.log while config followed CCMR_HOME, so the two could live in different directories. Both now derive from gatewayLogFile(). Also documents gateway lifetime in the README: `ccmr start` dies with the terminal, `ccmr claude`'s auto-started gateway deliberately does not.
1 parent b24872c commit df7bc6f

31 files changed

Lines changed: 749 additions & 26 deletions

README.md

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,19 @@ claude
5656
npx claude-code-model-router init
5757
npx claude-code-model-router init --global # 写入 ~/.ccmr,全目录共享一份配置
5858

59-
# 启动网关(models.yaml / .env 修改后自动热重载,无需重启)
59+
# 启动网关(前台运行;models.yaml / .env 修改后自动热重载,无需重启)
6060
npx claude-code-model-router start
6161
npx claude-code-model-router start --port 9000 # 指定端口
6262
npx claude-code-model-router start --host 0.0.0.0 # 监听所有网卡(见下方安全提示)
6363

64+
# 查看本机正在运行的网关(端口 / PID / 版本 / 配置来源 / Key 状态)
65+
npx claude-code-model-router status
66+
67+
# 停止网关(含 `ccmr claude` 后台自动拉起的那个)
68+
npx claude-code-model-router stop
69+
npx claude-code-model-router stop --port 9000
70+
npx claude-code-model-router stop --all
71+
6472
# 查看可用模型
6573
npx claude-code-model-router models
6674

@@ -83,7 +91,18 @@ npx claude-code-model-router claude --gateway-port 9000 # 自定义网关端口
8391
claude
8492
```
8593

86-
> **配置发现顺序**`-c 指定路径` > `./models.yaml` > `./config/models.yaml` > `./.claude-router.yaml` > `~/.ccmr/models.yaml``.env` 同理:`./.env` 优先,`~/.ccmr/.env` 兜底(环境变量 `CCMR_HOME` 可改写全局目录位置)。
94+
> **配置发现顺序**`-c 指定路径` > `./models.yaml` > `./config/models.yaml` > `./.claude-router.yaml` > `~/.ccmr/models.yaml``.env` 同理:`./.env` 优先,`~/.ccmr/.env` 兜底(环境变量 `CCMR_HOME` 可改写全局目录位置,日志 `gateway.log` 也随之移动)。
95+
96+
### 网关的生命周期
97+
98+
| 启动方式 | 关掉终端窗口后 | 日志去向 |
99+
|----------|----------------|----------|
100+
| `ccmr start` | **随之退出**(前台进程) | 直接打印在终端 |
101+
| `ccmr claude` 自动拉起 | **继续在后台运行** | `~/.ccmr/gateway.log` |
102+
103+
自动拉起的网关是 detached 进程(自成进程组、`PPID=1`),收不到终端的 `SIGHUP`——这是有意设计:多个 Claude Code 会话可以共用同一个网关,关掉其中一个窗口不该打断其他会话。代价是它不会自己消失,用 `ccmr status` 查看、`ccmr stop` 收掉。
104+
105+
> `ccmr stop` 只会停止通过 `/health` 自证身份的 ccmr 网关。如果端口被其他程序占用,它会明确报错并拒绝操作,绝不会误杀你的其他进程。
87106
88107
> **安全提示**:网关默认绑定到 `127.0.0.1`(仅本机可访问)。网关会用你本地配置的各厂商 API Key 代理上游请求,因此任何能访问该端口的人都能消耗你的额度。
89108
> 若确需通过 `--host 0.0.0.0` 暴露到局域网,请务必设置环境变量 `CCMR_REQUIRED_AUTH_TOKEN`,此时调用方必须在 `x-api-key``Authorization: Bearer <token>` 中携带该令牌。未设置时绑定非回环地址会打印警告。
@@ -382,7 +401,7 @@ npx claude-code-model-router claude
382401
| `/v1/messages` | POST | Anthropic Messages API |
383402
| `/v1/models` | GET | 列出可用模型 |
384403
| `/usage` | GET | 按模型的用量统计(请求数 / 错误数 / tokens,网关重启后清零) |
385-
| `/health` | GET | 健康检查(含网关版本号、`config_file` 配置来源、`ccmr_home`、各模型 Key 状态) |
404+
| `/health` | GET | 健康检查(含网关版本号、`pid``config_file` 配置来源、`ccmr_home`、各模型 Key 状态) |
386405

387406
## 开发
388407

@@ -439,6 +458,14 @@ DeepSeek Anthropic 兼容接口会忽略 `metadata` 字段,但某些 Claude Co
439458

440459
## 更新日志
441460

461+
### v1.8.2
462+
463+
- **新增 `ccmr status`**:列出本机正在运行的 ccmr 网关及其端口、PID、版本、配置来源、可用模型数。`ccmr claude` 自动拉起的网关是 detached 进程(关掉终端后继续运行),此前只能靠 `lsof` 才能找到它
464+
- **新增 `ccmr stop`**:停止网关,支持 `--port` / `--all` / `--force`**只会停止通过 `/health` 自证身份的 ccmr 网关**;端口被其他程序占用时明确报错并拒绝操作,不会误杀无关进程
465+
- **`/health` 新增 `pid` 字段**`status` / `stop` 因此无需解析 `lsof` / `netstat`,跨平台一致,且从原理上保证只能操作 ccmr 自己的进程
466+
- **修复日志目录不一致**:自动拉起的网关日志此前写死在 `~/.ccmr/gateway.log`,不随 `CCMR_HOME` 移动,导致配置和日志分居两地
467+
- README 补充网关生命周期说明(`ccmr start` 前台即退 vs `ccmr claude` 后台常驻)
468+
442469
### v1.8.1
443470

444471
修复一类隐蔽故障:网关在配置文件存在之前被 `ccmr claude` 自动拉起后,会静默地退回内置默认配置(零 API Key),且此后无论你怎么补配置都不会生效,表现为 Claude Code 里持续报 `401 API key not configured`

dist/cli.js

Lines changed: 96 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/cli.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/gateway-control.d.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* Discover and stop running gateways (`ccmr status`, `ccmr stop`).
3+
*
4+
* A gateway auto-started by `ccmr claude` is detached and reparented to init,
5+
* so it survives the terminal that spawned it. Without these commands the only
6+
* way to find or stop one is a `pkill` pattern nobody remembers.
7+
*
8+
* Safety: a process is only ever signalled when it self-identifies as a ccmr
9+
* gateway through /health and reports its own pid. A stranger listening on the
10+
* port is reported, never killed. This also keeps the implementation free of
11+
* platform-specific `lsof` / `netstat` parsing.
12+
*/
13+
/** CLI default (8080), VSCode extension default (8088) and its fallback range. */
14+
export declare const DEFAULT_SCAN_PORTS: number[];
15+
export interface GatewayInfo {
16+
port: number;
17+
pid?: number;
18+
version?: string;
19+
default_model?: string;
20+
config_file?: string | null;
21+
ccmr_home?: string;
22+
modelsReady: number;
23+
modelsTotal: number;
24+
}
25+
export type StopResult = {
26+
status: 'stopped';
27+
pid: number;
28+
} | {
29+
status: 'still_running';
30+
pid: number;
31+
} | {
32+
status: 'not_running';
33+
} | {
34+
status: 'unknown_process';
35+
} | {
36+
status: 'no_pid';
37+
version?: string;
38+
};
39+
export interface StopOptions {
40+
/** Injectable for tests; defaults to signalling the real process. */
41+
kill?: (pid: number, signal: NodeJS.Signals) => void;
42+
/** How long to wait for the port to free up after SIGTERM. */
43+
waitMs?: number;
44+
/** Escalate to SIGKILL if the process ignores SIGTERM. */
45+
force?: boolean;
46+
}
47+
export declare function discoverGateways(ports?: number[], timeoutMs?: number): Promise<GatewayInfo[]>;
48+
export declare function stopGateway(port: number, options?: StopOptions): Promise<StopResult>;
49+
//# sourceMappingURL=gateway-control.d.ts.map

dist/gateway-control.d.ts.map

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/gateway-control.js

Lines changed: 98 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)