├── .gitignore ├── example ├── settings.json └── apiConfigs.json ├── 需求说明 ├── package.json ├── ClaudeCode API站.md ├── README.md ├── health.js ├── notify.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /example/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "ANTHROPIC_BASE_URL": "https://zone.veloera.org/pg", 4 | "ANTHROPIC_AUTH_TOKEN": "sk-xxxxxxxx", 5 | "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1" 6 | }, 7 | "permissions": { 8 | "allow": [], 9 | "deny": [] 10 | }, 11 | "model": "opus" 12 | } -------------------------------------------------------------------------------- /需求说明: -------------------------------------------------------------------------------- 1 | #指令: ccs health 健康检查行为 2 | 读取本机 ~/.claude/apiConfigs.json 文件 3 | 4 | ##测试可用性 5 | 端点: 测试 /v1/models 端点 (适用于 Claude API)。 6 | 成功标准: 同时接受 2xx (成功) 和 4xx (客户端错误) 状态码。 7 | 2xx 响应表明端点工作正常。 8 | 4xx 响应 (如 401, 403 等) 表明端点可达,但可能需要正确的身份验证。 9 | 失败标准 5xx 服务器错误表明端点存在问题。 10 | 弹性机制: 需要连续 2 次失败才会将端点标记为不健康。 11 | ## 测试网络延迟 12 | 13 | 依次输出 输出name,ANTHROPIC_BASE_URL ANTHROPIC_AUTH_TOKEN 的可用性和网络延迟 14 | 15 | #代码开发要求:放在单独的health.js 文件中,index.js文件中只需要引入health.js 文件并调用 16 | 17 | 18 | -------------------------------------------------------------------------------- /example/apiConfigs.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "wenwen-ai", 4 | "config": { 5 | "env": { 6 | "ANTHROPIC_AUTH_TOKEN": "sk-hsg5hOghxxxxxxxHEk1rKHRxx", 7 | "ANTHROPIC_BASE_URL": "https://code.wenwen-ai.com", 8 | "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1" 9 | }, 10 | "permissions": { 11 | "allow": [], 12 | "deny": [] 13 | }, 14 | "model": "opus" 15 | } 16 | }, 17 | { 18 | "name": "zone", 19 | "config": { 20 | "env": { 21 | "ANTHROPIC_AUTH_TOKEN": "sk-xxxxxxxxxxxx", 22 | "ANTHROPIC_BASE_URL": "https://zone.veloera.org/pg", 23 | "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1" 24 | }, 25 | "permissions": { 26 | "allow": [], 27 | "deny": [] 28 | }, 29 | "model": "opus" 30 | } 31 | } 32 | ] 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "claude-config-switch", 3 | "version": "1.7.0", 4 | "description": "Claude配置切换工具及其他工具", 5 | "main": "index.js", 6 | "bin": { 7 | "ccs": "index.js" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "keywords": [ 13 | "claude", 14 | "config", 15 | "switch", 16 | "api", 17 | "cli", 18 | "anthropic" 19 | ], 20 | "author": "canglong", 21 | "license": "MIT", 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/canglong/claude-code-switch.git" 25 | }, 26 | "homepage": "https://github.com/canglong/claude-code-switch", 27 | "bugs": { 28 | "url": "https://github.com/canglong/claude-code-switch/issues" 29 | }, 30 | "dependencies": { 31 | "chalk": "^4.1.2", 32 | "commander": "^11.1.0", 33 | "inquirer": "^8.2.5" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ClaudeCode API站.md: -------------------------------------------------------------------------------- 1 | | 网站 | 访问 | 2 | | ------------------------------------------------------------------------------------------- | --------------------------------------------------- | 3 | | [https://instcopilot-api.com/register?aff=J2wX](https://instcopilot-api.com/register?aff=J2wX) | **强烈推荐这个**
倍率高5块钱相当于10美金 | 4 | | https://co.yes.vg/register?ref=ALVIN1MS | | 5 | | [https://a-generic.be-a.dev/register?aff=2khE](https://a-generic.be-a.dev/register?aff=2khE) | | 6 | | [https://anyrouter.top/register?aff=XkkW](https://anyrouter.top/register?aff=XkkW) | | 7 | | [https://b4u.qzz.io/register?aff=HuA0](https://b4u.qzz.io/register?aff=HuA0) | | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Claude配置切换工具 (CCS) 2 | 3 | 一个用于在不同的Claude API配置之间进行切换的命令行工具。 4 | 5 | [ClaudeCode API 站点推荐](https://github.com/AlvinScrp/claude-code-switch/blob/main/ClaudeCode%20API%E7%AB%99.md) 6 | 7 | **强烈推荐这个**倍率高5块钱相当于10美金 :[https://instcopilot-api.com/register?aff=J2wX](https://instcopilot-api.com/register?aff=J2wX) 8 | 9 | ## 安装 10 | 11 | ### npm包安装 12 | 13 | 已发布到https://www.npmjs.com/package/claude-config-switch 14 | 15 | ``` 16 | npm i claude-config-switch 17 | ``` 18 | 19 | ### 本地安装 20 | 21 | ```bash 22 | # 克隆仓库 23 | git clone <仓库地址> 24 | cd claude-code-switch 25 | 26 | # 安装依赖 27 | npm install 28 | 29 | # 全局安装 30 | npm install -g . 31 | ``` 32 | 33 | ### 依赖项 34 | 35 | - Node.js (>= 12.0.0) 36 | - npm (>= 6.0.0) 37 | - 依赖库: 38 | - commander: 命令行界面解析 39 | - chalk: 终端彩色输出 40 | - inquirer: 交互式命令行用户界面 41 | 42 | ## 使用方法 43 | 44 | ### 配置文件 45 | 46 | 工具需要三个配置文件,都位于 `~/.claude/` 目录下: 47 | 48 | #### 1. apiConfigs.json - API配置列表 49 | 50 | 存储所有可用的Claude API配置,格式如下: 51 | 52 | ```json 53 | [ 54 | { 55 | "name": "wenwen-ai", 56 | "config": { 57 | "env": { 58 | "ANTHROPIC_AUTH_TOKEN": "sk-XXXXXXX", 59 | "ANTHROPIC_BASE_URL": "https://code.wenwen-ai.com", 60 | "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1" 61 | }, 62 | "permissions": { 63 | "allow": [], 64 | "deny": [] 65 | }, 66 | "model": "claude-sonnet-4-20250514" 67 | } 68 | }, 69 | { 70 | "name": "zone", 71 | "config": { 72 | "env": { 73 | "ANTHROPIC_AUTH_TOKEN": "sk-XXXXXXX", 74 | "ANTHROPIC_BASE_URL": "https://zone.veloera.org/pg", 75 | "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1" 76 | }, 77 | "permissions": { 78 | "allow": [], 79 | "deny": [] 80 | }, 81 | "model": "claude-sonnet-4-20250514" 82 | } 83 | } 84 | ] 85 | ``` 86 | 87 | #### 2. settings.json - 当前激活配置 88 | 89 | 存储当前使用的配置,格式如下: 90 | 91 | ```json 92 | { 93 | "env": { 94 | "ANTHROPIC_BASE_URL": "https://zone.veloera.org/pg", 95 | "ANTHROPIC_AUTH_TOKEN": "sk-XXXXXXX", 96 | "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1" 97 | }, 98 | "permissions": { 99 | "allow": [], 100 | "deny": [] 101 | }, 102 | "model": "claude-sonnet-4-20250514" 103 | } 104 | ``` 105 | 106 | **注意**:切换配置时,整个 `settings.json` 文件会被选中配置的 `config` 对象完全替换。 107 | 108 | ### 命令 109 | 110 | #### 列出所有可用的API配置并提示选择 111 | 112 | ```bash 113 | ccs list 114 | # 或使用简写 115 | ccs ls 116 | ``` 117 | 118 | 输出示例: 119 | 120 | ``` 121 | ? 请选择要切换的配置: (Use arrow keys) 122 | > 1. [wenwen-ai ] sk-XXXXXXX https://code.wenwen-ai.com (当前) 123 | 2. [zone ] sk-XXXXXXX https://zone.veloera.org/pg 124 | 3. [co.yes.vg ] sk-XXXXXXX https://co.yes.vg/api 125 | 4. [a-generic ] sk-XXXXXXX https://a-generic.be-a.dev/api 126 | ────────────── 127 | 输入序号... 128 | 129 | ? 请选择要切换的配置: 2. [zone ] sk-XXXXXXX https://zone.veloera.org/pg 130 | 131 | 当前选择的配置: 132 | { 133 | "name": "zone", 134 | "config": { 135 | "env": { 136 | "ANTHROPIC_AUTH_TOKEN": "sk-xxxx", 137 | "ANTHROPIC_BASE_URL": "https://zone.veloera.org/pg", 138 | "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1" 139 | }, 140 | "permissions": { 141 | "allow": [], 142 | "deny": [] 143 | }, 144 | "model": "claude-sonnet-4-20250514" 145 | } 146 | } 147 | 148 | ? 确认切换到此配置? Yes 149 | 150 | 成功切换到配置: zone 151 | ``` 152 | 153 | **交互方式**: 154 | 155 | 1. **光标选择**: 使用键盘上下箭头选择配置,按Enter确认 156 | 2. **手动输入**: 选择"输入序号..."选项,然后输入配置的序号 157 | 158 | #### 直接设置当前使用的API配置 159 | 160 | ```bash 161 | ccs use <序号> 162 | ``` 163 | 164 | 例如: 165 | 166 | ```bash 167 | ccs use 2 168 | ``` 169 | 170 | 输出示例: 171 | 172 | ``` 173 | 当前选择的配置: 174 | { 175 | "name": "zone", 176 | "config": { 177 | "env": { 178 | "ANTHROPIC_AUTH_TOKEN": "sk-xxxxxx", 179 | "ANTHROPIC_BASE_URL": "https://zone.veloera.org/pg", 180 | "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1" 181 | }, 182 | "permissions": { 183 | "allow": [], 184 | "deny": [] 185 | }, 186 | "model": "claude-sonnet-4-20250514" 187 | } 188 | } 189 | 190 | ? 确认切换到此配置? Yes 191 | 192 | 成功切换到配置: zone 193 | ``` 194 | 195 | #### 打开配置文件 196 | 197 | ```bash 198 | # 打开API配置文件 (apiConfigs.json) 199 | ccs o api 200 | 201 | # 打开设置配置文件 (settings.json) 202 | ccs o setting 203 | ``` 204 | 205 | 这些命令会在默认编辑器中打开相应的配置文件,方便直接编辑配置。如果文件不存在,会自动创建包含示例内容的配置文件。 206 | 207 | **自动创建的示例内容**: 208 | 209 | - **API配置文件** (`apiConfigs.json`):包含一个示例配置,使用最新的Claude Sonnet 4模型 210 | - **设置配置文件** (`settings.json`):包含基本的环境变量和权限设置 211 | 212 | 只需将示例中的 `sk-YOUR_API_KEY_HERE` 替换为实际的API密钥即可使用。 213 | 214 | #### 健康检查功能 215 | 216 | 使用 `health` 命令可以检查所有配置的API端点的可用性和网络延迟: 217 | 218 | ```bash 219 | ccs health 220 | ``` 221 | 222 | **功能特性**: 223 | 224 | - **智能端点检测**:自动尝试多种常见的API端点格式 225 | - **去重优化**:相同的 `ANTHROPIC_BASE_URL` 只检测一次 226 | - **实时反馈**:逐行显示检测进度和结果 227 | - **多端点支持**:支持以下端点格式: 228 | - `/v1/models` - Claude Models API (默认) 229 | - `/v1/chat/completions` - OpenAI 兼容 API 230 | - `/v1/models` (无 anthropic-version 头) 231 | - `/` - 根路径 232 | - `/health` - 健康检查端点 233 | - `/api/v1/models` - 备用API路径 234 | 235 | **输出示例**: 236 | 237 | ``` 238 | 开始健康检查 (/v1/models)... 239 | 240 | | Name | Base URL | Token | Status | Latency | 241 | |----------------------|------------------------------------|--------------|--------------------------|---------| 242 | | instcopilot-config | https://sg.instcopilot-api.com | sk-JCzb7**** | Healthy (status: 200) | 414ms | 243 | | co.yes.vg-config | https://co.yes.vg | cr_2dc6**** | Healthy (status: 200) | 892ms | 244 | → Found working endpoint: /v1/chat/completions (OpenAI Compatible API) 245 | | anthropic-official | https://api.anthropic.com | sk-ant-**** | Unhealthy (status: 401) | 1205ms | 246 | Error: Unauthorized 247 | ``` 248 | 249 | **状态说明**: 250 | 251 | - **绿色 Healthy (2xx)**:端点正常工作 252 | - **红色 Unhealthy (4xx/5xx)**:端点有问题或鉴权失败 253 | - **黄色 Unhealthy**:网络错误或超时 254 | 255 | **Token 显示格式**:前7位 + **** (如:`sk-JCzb7****`) 256 | 257 | 当检测到非默认端点可用时,会显示实际工作的端点信息,帮助用户了解服务的实际API格式。 258 | 259 | #### 企微通知配置 260 | 261 | ##### 设置企微通知 262 | 263 | ```bash 264 | ccs notify setup 265 | # 或使用简写 266 | ccs ntf setup 267 | ``` 268 | 269 | 配置企微机器人通知功能: 270 | 271 | 1. 在企微群聊中添加机器人 272 | 2. 获取机器人的Webhook地址 273 | 3. 输入Webhook地址完成配置 274 | 4. 自动配置ClaudeCode Hooks 275 | 276 | notify.json - 通知配置 277 | 278 | 存储企微机器人等通知渠道的配置,格式如下: 279 | 280 | ```json 281 | { 282 | "wechatWork": { 283 | "webhookUrl": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY", 284 | "enabled": true 285 | }, 286 | "telegram": { 287 | "enabled": false 288 | }, 289 | "slack": { 290 | "enabled": false 291 | } 292 | } 293 | ``` 294 | 295 | **说明**: 296 | 297 | - `wechatWork.webhookUrl`: 企微群机器人的Webhook地址 298 | - `wechatWork.enabled`: 是否启用企微通知 299 | - 其他通知渠道为预留配置,暂未实现 300 | 301 | 输出示例: 302 | 303 | ``` 304 | 设置企微机器人通知配置: 305 | 请在企微群聊中添加机器人,获取Webhook地址 306 | 格式: https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY 307 | 配置完成后,将自动通过ClaudeCode Hooks监听Notification和Stop事件 308 | 309 | 请输入企微机器人Webhook地址: https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx 310 | 311 | 通知配置已保存到: /Users/username/.claude/notify.json 312 | ✓ Hook脚本已创建: /Users/username/.claude/scripts/wechat-notify.js 313 | ✓ ClaudeCode Hooks配置已更新 314 | 315 | 配置完成!现在ClaudeCode将在以下事件时发送企微通知: 316 | - Notification事件: 当Claude需要用户关注时 317 | - Stop事件: 当Claude任务完成时 318 | ``` 319 | 320 | ##### 查看通知状态 321 | 322 | ```bash 323 | ccs notify status 324 | ``` 325 | 326 | 显示当前通知配置状态,包括: 327 | 328 | - 企微机器人启用状态 329 | - Hook事件配置状态 330 | - ClaudeCode hooks配置状态 331 | 332 | 输出示例: 333 | 334 | ``` 335 | 当前通知配置状态: 336 | 企微机器人: 已启用 337 | Webhook地址: https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=*** 338 | Hook事件: 已启用 339 | Notification事件: 启用 340 | Stop事件: 启用 341 | ClaudeCode hooks配置: 已配置 342 | ``` 343 | 344 | ##### 测试通知功能 345 | 346 | ```bash 347 | ccs notify test 348 | ``` 349 | 350 | 发送测试通知到配置的企微群: 351 | 352 | 输出示例: 353 | 354 | ``` 355 | 正在发送测试通知... 356 | ✓ 企微通知发送成功 357 | ``` 358 | 359 | #### 显示版本信息 360 | 361 | ```bash 362 | ccs --version 363 | # 或 364 | ccs -v 365 | ``` 366 | 367 | 输出示例: 368 | 369 | ``` 370 | ccs 版本: 1.6.0 371 | ``` 372 | 373 | #### 显示帮助信息 374 | 375 | ```bash 376 | ccs --help 377 | ``` 378 | 379 | 输出示例: 380 | 381 | ``` 382 | Usage: ccs [options] [command] 383 | 384 | Claude配置切换工具 385 | 386 | Options: 387 | -v, --version 显示版本信息 388 | -h, --help display help for command 389 | 390 | Commands: 391 | list, ls 列出所有可用的API配置并提示选择 392 | use 设置当前使用的API配置 393 | health 检查各API端点的可用性与网络延迟 394 | notify, ntf 配置企微通知设置 395 | setup 设置企微机器人webhook地址 396 | status 查看当前通知配置状态 397 | test 测试企微通知功能 398 | o 打开Claude配置文件 399 | api 打开API配置文件 (apiConfigs.json) 400 | setting 打开设置配置文件 (settings.json) 401 | help [command] display help for command 402 | ``` 403 | 404 | #### 错误处理 405 | 406 | 当输入不存在的命令时,会显示错误信息和可用命令列表: 407 | 408 | ```bash 409 | ccs unknown 410 | ``` 411 | 412 | 输出示例: 413 | 414 | ``` 415 | 错误: 未知命令 'unknown' 416 | 417 | 可用命令: 418 | list 419 | ls 420 | use 421 | health 422 | notify 423 | ntf 424 | o 425 | 426 | 使用 --help 查看更多信息 427 | ``` 428 | 429 | ## 注意事项 430 | 431 | - 确保 `~/.claude/apiConfigs.json` 和 `~/.claude/settings.json` 文件存在并包含有效的配置信息 432 | - 工具会自动创建 `~/.claude` 目录(如果不存在) 433 | - 确认操作时默认为"是",直接按Enter键即可确认 434 | - 切换配置时会完全替换 `settings.json` 文件内容 435 | - 使用 `ntf` 命令需要先在企微群中添加机器人并获取Webhook地址 436 | - `notify.json` 文件首次使用时会自动创建 437 | 438 | ## 更新日志 439 | 440 | ### 1.7.0 健康检查 441 | 442 | ccs health 443 | 444 | ### **1.6.0 通知功能** 445 | 446 | + **新增企微通知功能**: 支持通过企微机器人接收Claude Code通知 447 | + **智能Hook集成**: 自动配置ClaudeCode Hooks,无需手动监听 448 | + **事件通知支持**: 449 | - `Notification`事件: 当Claude需要用户关注时自动通知 450 | - `Stop`事件: 当Claude任务完成时自动通知 451 | + **完整通知管理**: 452 | - `ccs notify setup` - 配置企微机器人 453 | - `ccs notify status` - 查看通知配置状态 454 | - `ccs notify test` - 测试通知功能 455 | + **自动化配置**: 自动创建Hook脚本和更新Claude设置文件 456 | + **配置文件支持**: 新增 `notify.json`配置文件管理通知设置 457 | 458 | ### **1.5.0** 459 | 460 | + 新增配置切换成功后的详细信息显示(名称、API Key、Base URL、Model) 461 | + 新增自动询问是否启动 Claude CLI 功能:切换配置成功后会询问是否在当前目录运行 `claude` 命令 462 | + 改进用户体验:一站式完成配置切换和 Claude 启动流程 463 | 464 | ### 1.4.0 465 | 466 | - 优化配置文件打开功能:文件不存在时自动创建包含示例内容的配置文件 467 | - 更新默认模型为 `claude-sonnet-4-20250514`(Claude Sonnet 4) 468 | - 改进用户体验:新用户可以立即开始使用工具,无需手动创建配置文件 469 | 470 | ### 1.3.0 471 | 472 | - 新增 `ccs ls` 命令作为 `ccs list` 的简写 473 | - 新增 `ccs o api` 命令用于打开API配置文件 (apiConfigs.json) 474 | - 新增 `ccs o setting` 命令用于打开设置配置文件 (settings.json) 475 | - 改进配置文件编辑体验,可以直接在默认编辑器中修改配置 476 | 477 | ### 1.2.0 478 | 479 | - 支持完整的Claude配置对象格式 480 | - 新增配置深度比较功能,准确识别当前激活配置 481 | - 优化配置显示格式,对齐显示配置名称 482 | - 更新配置文件结构,支持env、permissions、model等完整配置项 483 | 484 | ### 1.1.0 485 | 486 | - 添加交互式菜单,支持光标上下移动选择 487 | - 保留原有的序号输入功能 488 | - 优化用户体验,确认操作时默认为"是" 489 | 490 | ### 1.0.0 491 | 492 | - 初始版本发布 493 | - 基本的API配置切换功能 494 | -------------------------------------------------------------------------------- /health.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 健康检查模块 3 | * 提供 ccs health 命令:读取 ~/.claude/apiConfigs.json,依次检测各端点健康与网络延迟 4 | */ 5 | 6 | const fs = require('fs'); 7 | const path = require('path'); 8 | const os = require('os'); 9 | const https = require('https'); 10 | const chalk = require('chalk'); 11 | 12 | // 配置路径 13 | const CONFIG_DIR = path.join(os.homedir(), '.claude'); 14 | const API_CONFIGS_FILE = path.join(CONFIG_DIR, 'apiConfigs.json'); 15 | 16 | /** 17 | * 读取API配置文件 18 | * @returns {Array} API配置数组(可能为空) 19 | */ 20 | function readApiConfigs() { 21 | try { 22 | if (!fs.existsSync(API_CONFIGS_FILE)) { 23 | console.log(chalk.yellow(`警告: API配置文件不存在 (${API_CONFIGS_FILE})`)); 24 | return []; 25 | } 26 | const data = fs.readFileSync(API_CONFIGS_FILE, 'utf8'); 27 | return JSON.parse(data); 28 | } catch (error) { 29 | console.error(chalk.red(`读取API配置文件失败: ${error.message}`)); 30 | return []; 31 | } 32 | } 33 | 34 | /** 35 | * 探测指定端点 36 | * - 将 2xx 与 4xx 视为可达(成功) 37 | * - 将 5xx 或网络错误视为失败 38 | * - 返回 { ok, statusCode, latencyMs, error, endpoint } 39 | * @param {string} baseUrl 基础URL,如 https://api.anthropic.com 40 | * @param {string} authToken API密钥用于鉴权 41 | * @param {string} endpoint 端点路径,如 '/v1/models' 42 | * @param {object} options 额外选项 43 | * @returns {Promise<{ok:boolean,statusCode:number|undefined,latencyMs:number|undefined,error:Error|undefined,endpoint:string}>} 44 | */ 45 | function probeEndpoint(baseUrl, authToken, endpoint = '/v1/models', options = {}) { 46 | return new Promise((resolve) => { 47 | let urlString = baseUrl.endsWith('/') ? `${baseUrl}${endpoint.substring(1)}` : `${baseUrl}${endpoint}`; 48 | let timedOut = false; 49 | const start = Date.now(); 50 | 51 | try { 52 | const urlObj = new URL(urlString); 53 | const requestOptions = { 54 | hostname: urlObj.hostname, 55 | port: urlObj.port || 443, 56 | path: urlObj.pathname + urlObj.search, 57 | method: options.method || 'GET', 58 | headers: { 59 | 'Accept': 'application/json', 60 | 'Authorization': `Bearer ${authToken}`, 61 | // 'x-api-key': authToken, 62 | ...(options.includeAnthropicVersion !== false && { 'anthropic-version': '2023-06-01' }), 63 | ...(options.contentType && { 'Content-Type': options.contentType }), 64 | ...options.extraHeaders 65 | }, 66 | timeout: 30000 67 | }; 68 | 69 | const req = https.request(requestOptions, (res) => { 70 | // 消耗响应体以完成请求 71 | res.on('data', () => {}); 72 | res.on('end', () => { 73 | const latencyMs = Date.now() - start; 74 | const status = res.statusCode || 0; 75 | const ok = status < 500; // 2xx/4xx 视为可达 76 | resolve({ ok, statusCode: status, latencyMs, error: undefined, endpoint }); 77 | }); 78 | }); 79 | 80 | req.on('timeout', () => { 81 | timedOut = true; 82 | req.destroy(new Error('Request timeout')); 83 | }); 84 | 85 | req.on('error', (err) => { 86 | const latencyMs = Date.now() - start; 87 | resolve({ ok: false, statusCode: undefined, latencyMs, error: timedOut ? new Error('timeout') : err, endpoint }); 88 | }); 89 | 90 | if (options.body) { 91 | req.write(options.body); 92 | } 93 | req.end(); 94 | } catch (e) { 95 | resolve({ ok: false, statusCode: undefined, latencyMs: undefined, error: e, endpoint }); 96 | } 97 | }); 98 | } 99 | 100 | /** 101 | * 对单个配置执行智能健康检查 102 | * 如果 /v1/models 返回 404,会尝试其他常见端点 103 | * @param {object} configItem apiConfigs.json中单项 104 | * @returns {Promise<{name:string, baseUrl:string, tokenPresent:boolean, healthy:boolean, statusCodes:number[], latencies:number[], error:Error|undefined, endpoint:string}>} 105 | */ 106 | async function checkConfigHealth(configItem) { 107 | const name = configItem?.name || 'unknown'; 108 | const env = configItem?.config?.env || {}; 109 | const baseUrl = env.ANTHROPIC_BASE_URL || ''; 110 | const tokenPresent = Boolean(env.ANTHROPIC_AUTH_TOKEN); 111 | const authToken = env.ANTHROPIC_AUTH_TOKEN || ''; 112 | 113 | // 定义要尝试的端点 114 | const endpointsToTry = [ 115 | { path: '/v1/models', description: 'Claude Models API' }, 116 | { path: '/v1/chat/completions', description: 'OpenAI Compatible API', 117 | options: { 118 | method: 'POST', 119 | contentType: 'application/json', 120 | body: JSON.stringify({ 121 | model: 'claude-3-sonnet-20240229', 122 | messages: [{ role: 'user', content: 'test' }], 123 | max_tokens: 1 124 | }) 125 | } 126 | }, 127 | { path: '/v1/models', description: 'No Anthropic Version', options: { includeAnthropicVersion: false } }, 128 | { path: '/', description: 'Root Path' }, 129 | { path: '/health', description: 'Health Check' }, 130 | { path: '/api/v1/models', description: 'Alternative API Path' } 131 | ]; 132 | 133 | let bestResult = null; 134 | let testedEndpoints = []; 135 | 136 | for (const endpointConfig of endpointsToTry) { 137 | const result = await probeEndpoint(baseUrl, authToken, endpointConfig.path, endpointConfig.options || {}); 138 | testedEndpoints.push(`${endpointConfig.path}:${result.statusCode}`); 139 | 140 | // 如果是 2xx 状态码,直接返回成功结果 141 | if (result.statusCode >= 200 && result.statusCode < 300) { 142 | return { 143 | name, 144 | baseUrl, 145 | tokenPresent, 146 | healthy: true, 147 | statusCodes: [result.statusCode], 148 | latencies: [result.latencyMs], 149 | error: undefined, 150 | endpoint: `${endpointConfig.path} (${endpointConfig.description})` 151 | }; 152 | } 153 | 154 | // 记录最好的结果(最低的错误代码或最早的可达结果) 155 | if (!bestResult || (result.ok && !bestResult.ok) || 156 | (result.statusCode && (!bestResult.statusCode || result.statusCode < bestResult.statusCode))) { 157 | bestResult = { ...result, description: endpointConfig.description }; 158 | } 159 | } 160 | 161 | // 如果没有找到 2xx 响应,返回最好的结果 162 | const healthy = bestResult ? bestResult.ok : false; 163 | return { 164 | name, 165 | baseUrl, 166 | tokenPresent, 167 | healthy, 168 | statusCodes: bestResult ? [bestResult.statusCode] : [], 169 | latencies: bestResult ? [bestResult.latencyMs] : [], 170 | error: bestResult?.error, 171 | endpoint: bestResult ? `${bestResult.endpoint} (${bestResult.description})` : 'All endpoints failed' 172 | }; 173 | } 174 | 175 | /** 176 | * 掩码显示API Key,显示前7位+**** 177 | * @param {boolean} present 是否存在 178 | * @param {string} token 原始token 179 | * @returns {string} 掩码显示 180 | */ 181 | function maskToken(present, token) { 182 | if (!present) return 'N/A'; 183 | if (!token || token.length < 7) return '****'; 184 | return `${token.slice(0, 7)}****`; 185 | } 186 | 187 | /** 188 | * 注册 health 命令 189 | * @param {import('commander').Command} program Commander实例 190 | */ 191 | function registerHealthCommands(program) { 192 | program 193 | .command('health') 194 | .description('检查各API端点的可用性与网络延迟') 195 | .action(async () => { 196 | const apiConfigs = readApiConfigs(); 197 | if (apiConfigs.length === 0) { 198 | console.log(chalk.yellow('没有找到可用的API配置')); 199 | return; 200 | } 201 | 202 | console.log(chalk.cyan('开始健康检查 (/v1/models)...\n')); 203 | 204 | // 去重URL,只检测每个URL的第一个配置 205 | const uniqueUrls = new Set(); 206 | const configsToCheck = []; 207 | 208 | apiConfigs.forEach(item => { 209 | const env = item?.config?.env || {}; 210 | const baseUrl = env.ANTHROPIC_BASE_URL || ''; 211 | const token = env.ANTHROPIC_AUTH_TOKEN || ''; 212 | 213 | // 只处理首次出现的URL 214 | if (!uniqueUrls.has(baseUrl) && baseUrl) { 215 | uniqueUrls.add(baseUrl); 216 | configsToCheck.push({ 217 | item, 218 | token, 219 | baseUrl 220 | }); 221 | } 222 | }); 223 | 224 | // 固定宽度设置 225 | const nameWidth = 18; 226 | const fixedUrlWidth = 30; 227 | const tokenWidth = 12; 228 | const statusWidth = 22; 229 | 230 | // 表头 231 | const nameHeader = 'Name'.padEnd(nameWidth); 232 | const urlHeader = 'Base URL'.padEnd(fixedUrlWidth); 233 | const tokenHeader = 'Token'.padEnd(tokenWidth); 234 | const statusHeader = 'Status'.padEnd(statusWidth); 235 | const latencyHeader = 'Latency'; 236 | 237 | console.log(chalk.bold(`| ${nameHeader} | ${urlHeader} | ${tokenHeader} | ${statusHeader} | ${latencyHeader} |`)); 238 | console.log(`|${'-'.repeat(nameWidth + 2)}|${'-'.repeat(fixedUrlWidth + 2)}|${'-'.repeat(tokenWidth + 2)}|${'-'.repeat(statusWidth + 2)}|${'-'.repeat(10)}|`); 239 | 240 | // 逐个检测并输出 241 | for (const config of configsToCheck) { 242 | const name = (config.item?.name || 'unknown').length > nameWidth 243 | ? (config.item?.name || 'unknown').substring(0, nameWidth - 3) + '...' 244 | : (config.item?.name || 'unknown').padEnd(nameWidth); 245 | const url = config.baseUrl.length > fixedUrlWidth 246 | ? config.baseUrl.substring(0, fixedUrlWidth - 3) + '...' 247 | : config.baseUrl.padEnd(fixedUrlWidth); 248 | const tokenMasked = maskToken(Boolean(config.token), config.token).padEnd(tokenWidth); 249 | 250 | // 显示检测中状态 251 | const checkingStatus = chalk.yellow('Checking...').padEnd(statusWidth + (chalk.yellow('Checking...').length - 'Checking...'.length)); 252 | process.stdout.write(`| ${name} | ${url} | ${tokenMasked} | ${checkingStatus} | ... |\r`); 253 | 254 | // 执行检测 255 | const endpointResult = await checkConfigHealth(config.item); 256 | 257 | const latencyText = endpointResult.latencies.length 258 | ? `${Math.round(endpointResult.latencies[0])}ms` 259 | : 'N/A'; 260 | 261 | const statusCode = endpointResult.statusCodes.length ? endpointResult.statusCodes[0] : 'N/A'; 262 | const healthStatus = endpointResult.healthy ? 'Healthy' : 'Unhealthy'; 263 | const statusText = `${healthStatus} (status: ${statusCode})`; 264 | 265 | // 根据状态码着色 266 | let coloredStatus; 267 | if (statusCode >= 200 && statusCode < 300) { 268 | coloredStatus = chalk.green(statusText); 269 | } else if (statusCode >= 400 || (typeof statusCode === 'string' && statusCode !== 'N/A')) { 270 | coloredStatus = chalk.red(statusText); 271 | } else { 272 | coloredStatus = chalk.yellow(statusText); 273 | } 274 | 275 | const statusFormatted = coloredStatus.padEnd(statusWidth + (coloredStatus.length - statusText.length)); 276 | 277 | // 清除当前行并输出最终结果 278 | process.stdout.write('\r\x1b[K'); 279 | console.log(`| ${name} | ${url} | ${tokenMasked} | ${statusFormatted} | ${latencyText} |`); 280 | 281 | if (!endpointResult.healthy && endpointResult.error) { 282 | console.log(chalk.gray(` Error: ${endpointResult.error.message}`)); 283 | } 284 | 285 | // // 显示找到的可用端点信息 286 | // if (endpointResult.endpoint && endpointResult.endpoint !== '/v1/models (Claude Models API)') { 287 | // console.log(chalk.cyan(` → Found working endpoint: ${endpointResult.endpoint}`)); 288 | // } 289 | } 290 | }); 291 | } 292 | 293 | module.exports = { registerHealthCommands }; 294 | 295 | -------------------------------------------------------------------------------- /notify.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 通知功能模块 3 | * 处理企微机器人通知相关的所有功能 4 | */ 5 | 6 | const fs = require('fs'); 7 | const path = require('path'); 8 | const chalk = require('chalk'); 9 | const os = require('os'); 10 | const readline = require('readline'); 11 | const https = require('https'); 12 | 13 | // 配置文件路径 14 | const CONFIG_DIR = path.join(os.homedir(), '.claude'); 15 | const NOTIFY_CONFIG_FILE = path.join(CONFIG_DIR, 'notify.json'); 16 | 17 | /** 18 | * 创建readline接口 19 | * @returns {readline.Interface} readline接口 20 | */ 21 | function createReadlineInterface() { 22 | return readline.createInterface({ 23 | input: process.stdin, 24 | output: process.stdout 25 | }); 26 | } 27 | 28 | /** 29 | * 确保配置目录存在 30 | */ 31 | function ensureConfigDir() { 32 | if (!fs.existsSync(CONFIG_DIR)) { 33 | fs.mkdirSync(CONFIG_DIR, { recursive: true }); 34 | console.log(chalk.green(`创建配置目录: ${CONFIG_DIR}`)); 35 | } 36 | } 37 | 38 | /** 39 | * 读取通知配置文件 40 | * @returns {Object} 通知配置对象 41 | */ 42 | function readNotifyConfig() { 43 | try { 44 | if (!fs.existsSync(NOTIFY_CONFIG_FILE)) { 45 | return null; 46 | } 47 | 48 | const data = fs.readFileSync(NOTIFY_CONFIG_FILE, 'utf8'); 49 | return JSON.parse(data); 50 | } catch (error) { 51 | console.error(chalk.red(`读取通知配置文件失败: ${error.message}`)); 52 | return null; 53 | } 54 | } 55 | 56 | /** 57 | * 保存通知配置文件 58 | * @param {Object} config 通知配置对象 59 | */ 60 | function saveNotifyConfig(config) { 61 | try { 62 | ensureConfigDir(); 63 | fs.writeFileSync(NOTIFY_CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8'); 64 | console.log(chalk.green(`通知配置已保存到: ${NOTIFY_CONFIG_FILE}`)); 65 | } catch (error) { 66 | console.error(chalk.red(`保存通知配置文件失败: ${error.message}`)); 67 | process.exit(1); 68 | } 69 | } 70 | 71 | /** 72 | * 发送企微机器人通知 73 | * @param {string} webhookUrl 企微机器人webhook地址 74 | * @param {string} message 通知消息 75 | */ 76 | function sendWeChatWorkNotification(webhookUrl, message) { 77 | const postData = JSON.stringify({ 78 | msgtype: 'text', 79 | text: { 80 | content: message 81 | } 82 | }); 83 | 84 | const url = new URL(webhookUrl); 85 | const options = { 86 | hostname: url.hostname, 87 | port: url.port || 443, 88 | path: url.pathname + url.search, 89 | method: 'POST', 90 | headers: { 91 | 'Content-Type': 'application/json', 92 | 'Content-Length': Buffer.byteLength(postData) 93 | } 94 | }; 95 | 96 | const req = https.request(options, (res) => { 97 | let data = ''; 98 | res.on('data', (chunk) => { 99 | data += chunk; 100 | }); 101 | res.on('end', () => { 102 | try { 103 | const response = JSON.parse(data); 104 | if (response.errcode === 0) { 105 | console.log(chalk.green('✓ 企微通知发送成功')); 106 | } else { 107 | console.error(chalk.red(`企微通知发送失败: ${response.errmsg}`)); 108 | } 109 | } catch (error) { 110 | console.error(chalk.red(`解析企微响应失败: ${error.message}`)); 111 | } 112 | }); 113 | }); 114 | 115 | req.on('error', (error) => { 116 | console.error(chalk.red(`企微通知发送失败: ${error.message}`)); 117 | }); 118 | 119 | req.write(postData); 120 | req.end(); 121 | } 122 | 123 | /** 124 | * 创建企微通知Hook脚本 125 | * @param {string} scriptPath Hook脚本的完整路径 126 | */ 127 | function createWeChatNotifyScript(scriptPath) { 128 | const scriptContent = `#!/usr/bin/env node 129 | /** 130 | * ClaudeCode企微通知Hook脚本 131 | * 由claude-code-switch自动生成 132 | */ 133 | 134 | const https = require('https'); 135 | const fs = require('fs'); 136 | const path = require('path'); 137 | 138 | const NOTIFY_CONFIG_PATH = path.join(require('os').homedir(), '.claude', 'notify.json'); 139 | 140 | /** 141 | * 发送企微机器人通知 142 | * @param {string} webhookUrl 企微机器人webhook地址 143 | * @param {string} message 通知消息 144 | */ 145 | function sendWeChatWorkNotification(webhookUrl, message) { 146 | const postData = JSON.stringify({ 147 | msgtype: 'text', 148 | text: { 149 | content: message 150 | } 151 | }); 152 | 153 | const url = new URL(webhookUrl); 154 | const options = { 155 | hostname: url.hostname, 156 | port: url.port || 443, 157 | path: url.pathname + url.search, 158 | method: 'POST', 159 | headers: { 160 | 'Content-Type': 'application/json', 161 | 'Content-Length': Buffer.byteLength(postData) 162 | } 163 | }; 164 | 165 | const req = https.request(options, (res) => { 166 | let data = ''; 167 | res.on('data', (chunk) => { 168 | data += chunk; 169 | }); 170 | res.on('end', () => { 171 | try { 172 | const response = JSON.parse(data); 173 | if (response.errcode !== 0) { 174 | console.error('企微通知发送失败:', response.errmsg); 175 | } 176 | } catch (error) { 177 | console.error('解析企微响应失败:', error.message); 178 | } 179 | }); 180 | }); 181 | 182 | req.on('error', (error) => { 183 | console.error('企微通知发送失败:', error.message); 184 | }); 185 | 186 | req.write(postData); 187 | req.end(); 188 | } 189 | 190 | /** 191 | * 读取通知配置 192 | */ 193 | function readNotifyConfig() { 194 | try { 195 | if (fs.existsSync(NOTIFY_CONFIG_PATH)) { 196 | const data = fs.readFileSync(NOTIFY_CONFIG_PATH, 'utf8'); 197 | return JSON.parse(data); 198 | } 199 | } catch (error) { 200 | console.error('读取通知配置失败:', error.message); 201 | } 202 | return null; 203 | } 204 | 205 | // 处理命令行参数 206 | const eventType = process.argv[2]; 207 | const config = readNotifyConfig(); 208 | 209 | // 检查是否启用通知 210 | if (!config || !config.wechatWork?.enabled || !config.wechatWork?.webhookUrl) { 211 | process.exit(0); 212 | } 213 | 214 | // 检查特定事件是否启用 215 | if (config.hooks?.events?.[eventType]?.enabled === false) { 216 | process.exit(0); 217 | } 218 | 219 | // 读取stdin获取事件数据 220 | let inputData = ''; 221 | process.stdin.on('data', (chunk) => { 222 | inputData += chunk; 223 | }); 224 | 225 | process.stdin.on('end', () => { 226 | try { 227 | const eventData = JSON.parse(inputData); 228 | let message = ''; 229 | 230 | if (eventType === 'notification') { 231 | message = \`[Claude Code通知]\\n时间: \${new Date().toLocaleString()}\\n\${config.hooks?.events?.Notification?.message || 'Claude Code需要您的关注'}\\n会话ID: \${eventData.session_id || 'unknown'}\`; 232 | 233 | // 如果有payload消息,添加到通知中 234 | if (eventData.payload?.message) { 235 | message += \`\\n消息: \${eventData.payload.message.substring(0, 100)}\${eventData.payload.message.length > 100 ? '...' : ''}\`; 236 | } 237 | } else if (eventType === 'stop') { 238 | message = \`[Claude Code完成]\\n时间: \${new Date().toLocaleString()}\\n\${config.hooks?.events?.Stop?.message || 'Claude Code任务已完成'}\\n会话ID: \${eventData.session_id || 'unknown'}\`; 239 | 240 | if (eventData.cwd) { 241 | message += \`\\n工作目录: \${eventData.cwd}\`; 242 | } 243 | } 244 | 245 | if (message && config.wechatWork?.webhookUrl) { 246 | sendWeChatWorkNotification(config.wechatWork.webhookUrl, message); 247 | } 248 | } catch (error) { 249 | console.error('处理事件数据失败:', error.message); 250 | } 251 | }); 252 | `; 253 | 254 | try { 255 | fs.writeFileSync(scriptPath, scriptContent, 'utf8'); 256 | // 设置可执行权限 257 | fs.chmodSync(scriptPath, 0o755); 258 | console.log(chalk.green(`✓ Hook脚本已创建: ${scriptPath}`)); 259 | } catch (error) { 260 | console.error(chalk.red(`创建Hook脚本失败: ${error.message}`)); 261 | throw error; 262 | } 263 | } 264 | 265 | /** 266 | * 设置ClaudeCode Hooks配置 267 | * @param {Object} config 通知配置对象 268 | */ 269 | function setupClaudeCodeHooks(config) { 270 | const claudeSettingsPath = path.join(os.homedir(), '.claude', 'settings.json'); 271 | const scriptsDir = path.join(os.homedir(), '.claude', 'scripts'); 272 | const hookScriptPath = path.join(scriptsDir, 'wechat-notify.js'); 273 | 274 | console.log(chalk.cyan('\n正在配置ClaudeCode Hooks...')); 275 | 276 | // 确保scripts目录存在 277 | if (!fs.existsSync(scriptsDir)) { 278 | fs.mkdirSync(scriptsDir, { recursive: true }); 279 | console.log(chalk.green(`创建scripts目录: ${scriptsDir}`)); 280 | } 281 | 282 | // 检查hook脚本是否存在,不存在则创建 283 | if (!fs.existsSync(hookScriptPath)) { 284 | console.log(chalk.yellow(`Hook脚本不存在,正在创建: ${hookScriptPath}`)); 285 | createWeChatNotifyScript(hookScriptPath); 286 | } 287 | 288 | // 读取现有的Claude设置 289 | let claudeSettings = {}; 290 | if (fs.existsSync(claudeSettingsPath)) { 291 | try { 292 | const data = fs.readFileSync(claudeSettingsPath, 'utf8'); 293 | claudeSettings = JSON.parse(data); 294 | } catch (error) { 295 | console.warn(chalk.yellow(`读取Claude设置文件失败,将创建新的设置: ${error.message}`)); 296 | } 297 | } 298 | 299 | // 确保hooks配置存在 300 | if (!claudeSettings.hooks) { 301 | claudeSettings.hooks = {}; 302 | } 303 | 304 | // 配置Notification和Stop事件的hooks 305 | claudeSettings.hooks.Notification = [ 306 | { 307 | hooks: [ 308 | { 309 | type: 'command', 310 | command: `node "${hookScriptPath}" notification` 311 | } 312 | ] 313 | } 314 | ]; 315 | 316 | claudeSettings.hooks.Stop = [ 317 | { 318 | hooks: [ 319 | { 320 | type: 'command', 321 | command: `node "${hookScriptPath}" stop` 322 | } 323 | ] 324 | } 325 | ]; 326 | 327 | // 保存Claude设置 328 | try { 329 | fs.writeFileSync(claudeSettingsPath, JSON.stringify(claudeSettings, null, 2), 'utf8'); 330 | console.log(chalk.green('✓ ClaudeCode Hooks配置已更新')); 331 | console.log(chalk.white(`Hook脚本位置: ${hookScriptPath}`)); 332 | console.log(chalk.green('\n配置完成!现在ClaudeCode将在以下事件时发送企微通知:')); 333 | console.log(chalk.white(' - Notification事件: 当Claude需要用户关注时')); 334 | console.log(chalk.white(' - Stop事件: 当Claude任务完成时')); 335 | } catch (error) { 336 | console.error(chalk.red(`保存Claude设置文件失败: ${error.message}`)); 337 | } 338 | } 339 | 340 | /** 341 | * 设置通知配置 342 | */ 343 | function setupNotifyConfig() { 344 | const rl = createReadlineInterface(); 345 | 346 | console.log(chalk.cyan('\n设置企微机器人通知配置:')); 347 | console.log(chalk.white('请在企微群聊中添加机器人,获取Webhook地址')); 348 | console.log(chalk.white('格式: https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY')); 349 | console.log(chalk.yellow('配置完成后,将自动通过ClaudeCode Hooks监听Notification和Stop事件')); 350 | 351 | rl.question(chalk.cyan('\n请输入企微机器人Webhook地址: '), (webhookUrl) => { 352 | if (!webhookUrl || !webhookUrl.startsWith('https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=')) { 353 | console.error(chalk.red('无效的企微机器人Webhook地址')); 354 | rl.close(); 355 | return; 356 | } 357 | 358 | const config = { 359 | wechatWork: { 360 | webhookUrl: webhookUrl, 361 | enabled: true, 362 | events: ['Notification', 'Stop'] // 支持的事件类型 363 | }, 364 | hooks: { 365 | enabled: true, 366 | defaultEnabled: true, // 默认开启通知 367 | events: { 368 | Notification: { 369 | enabled: true, 370 | message: 'Claude Code需要您的关注' 371 | }, 372 | Stop: { 373 | enabled: true, 374 | message: 'Claude Code任务已完成' 375 | } 376 | } 377 | }, 378 | // 为未来支持更多通知渠道预留配置结构 379 | telegram: { 380 | enabled: false 381 | }, 382 | slack: { 383 | enabled: false 384 | } 385 | }; 386 | 387 | saveNotifyConfig(config); 388 | setupClaudeCodeHooks(config); 389 | rl.close(); 390 | }); 391 | } 392 | 393 | /** 394 | * 显示通知配置状态 395 | */ 396 | function showNotifyStatus() { 397 | const notifyConfig = readNotifyConfig(); 398 | 399 | if (!notifyConfig) { 400 | console.log(chalk.yellow('未配置企微通知,请运行 ccs notify setup 进行配置')); 401 | return; 402 | } 403 | 404 | console.log(chalk.cyan('\n当前通知配置状态:')); 405 | console.log(chalk.white('企微机器人: ') + (notifyConfig.wechatWork?.enabled ? chalk.green('已启用') : chalk.red('未启用'))); 406 | 407 | if (notifyConfig.wechatWork?.webhookUrl) { 408 | const maskedUrl = notifyConfig.wechatWork.webhookUrl.replace(/key=([^&]+)/, 'key=***'); 409 | console.log(chalk.white('Webhook地址: ') + maskedUrl); 410 | } 411 | 412 | if (notifyConfig.hooks?.enabled) { 413 | console.log(chalk.white('Hook事件: ') + chalk.green('已启用')); 414 | console.log(chalk.white(' Notification事件: ') + (notifyConfig.hooks.events?.Notification?.enabled ? chalk.green('启用') : chalk.red('禁用'))); 415 | console.log(chalk.white(' Stop事件: ') + (notifyConfig.hooks.events?.Stop?.enabled ? chalk.green('启用') : chalk.red('禁用'))); 416 | } else { 417 | console.log(chalk.white('Hook事件: ') + chalk.red('未启用')); 418 | } 419 | 420 | // 检查ClaudeCode hooks配置文件 421 | const claudeHooksPath = path.join(os.homedir(), '.claude', 'settings.json'); 422 | if (fs.existsSync(claudeHooksPath)) { 423 | try { 424 | const claudeSettings = JSON.parse(fs.readFileSync(claudeHooksPath, 'utf8')); 425 | if (claudeSettings.hooks && claudeSettings.hooks.Notification && claudeSettings.hooks.Stop) { 426 | console.log(chalk.white('ClaudeCode hooks配置: ') + chalk.green('已配置')); 427 | } else { 428 | console.log(chalk.white('ClaudeCode hooks配置: ') + chalk.yellow('未配置 - 需要恢复')); 429 | } 430 | } catch (error) { 431 | console.log(chalk.white('ClaudeCode hooks配置: ') + chalk.red('读取失败')); 432 | } 433 | } else { 434 | console.log(chalk.white('ClaudeCode hooks配置: ') + chalk.yellow('未找到')); 435 | } 436 | } 437 | 438 | /** 439 | * 测试企微通知功能 440 | */ 441 | function testNotification() { 442 | const notifyConfig = readNotifyConfig(); 443 | 444 | if (!notifyConfig || !notifyConfig.wechatWork || !notifyConfig.wechatWork.webhookUrl) { 445 | console.log(chalk.yellow('未配置企微通知,请先运行 ccs notify setup')); 446 | return; 447 | } 448 | 449 | console.log(chalk.cyan('正在发送测试通知...')); 450 | const testMessage = `[CCS测试通知]\n时间: ${new Date().toLocaleString()}\n这是一条来自claude-code-switch的测试消息`; 451 | 452 | sendWeChatWorkNotification(notifyConfig.wechatWork.webhookUrl, testMessage); 453 | } 454 | 455 | 456 | /** 457 | * 注册notify相关的CLI命令 458 | * @param {Object} program commander程序对象 459 | */ 460 | function registerNotifyCommands(program) { 461 | // 新的notify命令,用于配置企微通知 462 | const notifyCommand = program 463 | .command('notify') 464 | .alias('ntf') 465 | .description('配置企微通知设置'); 466 | 467 | notifyCommand 468 | .command('setup') 469 | .description('设置企微机器人webhook地址') 470 | .action(() => { 471 | ensureConfigDir(); 472 | setupNotifyConfig(); 473 | }); 474 | 475 | notifyCommand 476 | .command('status') 477 | .description('查看当前通知配置状态') 478 | .action(() => { 479 | ensureConfigDir(); 480 | showNotifyStatus(); 481 | }); 482 | 483 | notifyCommand 484 | .command('test') 485 | .description('测试企微通知功能') 486 | .action(() => { 487 | ensureConfigDir(); 488 | testNotification(); 489 | }); 490 | 491 | // 保留原有的ntf命令作为兼容性(独立命令) 492 | program 493 | .command('ntfold') 494 | .description('(已废弃) 原ntf命令,请使用 ccs notify 命令') 495 | .action(() => { 496 | console.log(chalk.yellow('原ntf监听命令已废弃,现在使用ClaudeCode Hooks机制')); 497 | console.log(chalk.cyan('请使用以下命令:')); 498 | console.log(chalk.cyan(' ccs notify setup - 设置企微通知')); 499 | console.log(chalk.cyan(' ccs notify status - 查看通知状态')); 500 | console.log(chalk.cyan(' ccs notify test - 测试通知功能')); 501 | console.log(chalk.yellow('\n配置完成后,ClaudeCode将自动发送通知,无需手动启动监听')); 502 | }); 503 | } 504 | 505 | module.exports = { 506 | readNotifyConfig, 507 | saveNotifyConfig, 508 | sendWeChatWorkNotification, 509 | setupClaudeCodeHooks, 510 | setupNotifyConfig, 511 | showNotifyStatus, 512 | testNotification, 513 | registerNotifyCommands, 514 | ensureConfigDir, 515 | createWeChatNotifyScript 516 | }; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Claude配置切换工具 5 | * 用于在不同的Claude API配置之间进行切换 6 | */ 7 | 8 | const fs = require('fs'); 9 | const path = require('path'); 10 | const { program } = require('commander'); 11 | const chalk = require('chalk'); 12 | const os = require('os'); 13 | const readline = require('readline'); 14 | const inquirer = require('inquirer'); 15 | const { spawn } = require('child_process'); 16 | const notify = require('./notify'); 17 | const health = require('./health'); 18 | 19 | // 版本号 20 | const VERSION = '1.7.0'; 21 | 22 | // 配置文件路径 23 | const CONFIG_DIR = path.join(os.homedir(), '.claude'); 24 | const API_CONFIGS_FILE = path.join(CONFIG_DIR, 'apiConfigs.json'); 25 | const SETTINGS_FILE = path.join(CONFIG_DIR, 'settings.json'); 26 | 27 | /** 28 | * 创建readline接口 29 | * @returns {readline.Interface} readline接口 30 | */ 31 | function createReadlineInterface() { 32 | return readline.createInterface({ 33 | input: process.stdin, 34 | output: process.stdout 35 | }); 36 | } 37 | 38 | /** 39 | * 确保配置目录存在 40 | */ 41 | function ensureConfigDir() { 42 | if (!fs.existsSync(CONFIG_DIR)) { 43 | fs.mkdirSync(CONFIG_DIR, { recursive: true }); 44 | console.log(chalk.green(`创建配置目录: ${CONFIG_DIR}`)); 45 | } 46 | } 47 | 48 | /** 49 | * 读取API配置文件 50 | * @returns {Array} API配置数组 51 | */ 52 | function readApiConfigs() { 53 | try { 54 | if (!fs.existsSync(API_CONFIGS_FILE)) { 55 | console.log(chalk.yellow(`警告: API配置文件不存在 (${API_CONFIGS_FILE})`)); 56 | return []; 57 | } 58 | 59 | const data = fs.readFileSync(API_CONFIGS_FILE, 'utf8'); 60 | return JSON.parse(data); 61 | } catch (error) { 62 | console.error(chalk.red(`读取API配置文件失败: ${error.message}`)); 63 | return []; 64 | } 65 | } 66 | 67 | /** 68 | * 读取settings.json文件 69 | * @returns {Object} 设置对象 70 | */ 71 | function readSettings() { 72 | try { 73 | if (!fs.existsSync(SETTINGS_FILE)) { 74 | return { env: {} }; 75 | } 76 | 77 | const data = fs.readFileSync(SETTINGS_FILE, 'utf8'); 78 | return JSON.parse(data); 79 | } catch (error) { 80 | console.error(chalk.red(`读取设置文件失败: ${error.message}`)); 81 | return { env: {} }; 82 | } 83 | } 84 | 85 | /** 86 | * 保存settings.json文件 87 | * @param {Object} settings 设置对象 88 | */ 89 | function saveSettings(settings) { 90 | try { 91 | fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2), 'utf8'); 92 | } catch (error) { 93 | console.error(chalk.red(`保存设置文件失败: ${error.message}`)); 94 | process.exit(1); 95 | } 96 | } 97 | 98 | /** 99 | * 保存配置时保持现有的hooks和其他设置 100 | * @param {Object} newConfig 新的配置对象 101 | */ 102 | function saveSettingsPreservingHooks(newConfig) { 103 | try { 104 | // 读取当前设置 105 | const currentSettings = readSettings(); 106 | 107 | // 合并配置,只有当前设置中存在hooks时才保持 108 | const mergedSettings = { 109 | ...newConfig 110 | }; 111 | 112 | // 如果当前设置中有hooks,则保持 113 | if (currentSettings.hooks) { 114 | mergedSettings.hooks = currentSettings.hooks; 115 | } 116 | 117 | // 如果当前设置中有statusLine,则保持 118 | if (currentSettings.statusLine) { 119 | mergedSettings.statusLine = currentSettings.statusLine; 120 | } 121 | 122 | fs.writeFileSync(SETTINGS_FILE, JSON.stringify(mergedSettings, null, 2), 'utf8'); 123 | } catch (error) { 124 | console.error(chalk.red(`保存设置文件失败: ${error.message}`)); 125 | process.exit(1); 126 | } 127 | } 128 | 129 | /** 130 | * 深度比较两个对象是否相等 131 | * @param {Object} obj1 132 | * @param {Object} obj2 133 | * @returns {boolean} 134 | */ 135 | function deepEqual(obj1, obj2) { 136 | if (obj1 === obj2) return true; 137 | 138 | if (obj1 == null || obj2 == null) return false; 139 | if (typeof obj1 !== 'object' || typeof obj2 !== 'object') return obj1 === obj2; 140 | 141 | const keys1 = Object.keys(obj1); 142 | const keys2 = Object.keys(obj2); 143 | 144 | if (keys1.length !== keys2.length) return false; 145 | 146 | for (let key of keys1) { 147 | if (!keys2.includes(key)) return false; 148 | if (!deepEqual(obj1[key], obj2[key])) return false; 149 | } 150 | 151 | return true; 152 | } 153 | 154 | /** 155 | * 获取当前激活的API配置 156 | * @returns {Object|null} 当前激活的配置对象或null(如果没有找到) 157 | */ 158 | function getCurrentConfig() { 159 | const settings = readSettings(); 160 | 161 | // 如果settings为空,返回null 162 | if (!settings || Object.keys(settings).length === 0) { 163 | return null; 164 | } 165 | 166 | // 查找匹配的配置,只比较核心字段 167 | const apiConfigs = readApiConfigs(); 168 | return apiConfigs.find(config => { 169 | if (!config.config) return false; 170 | 171 | // 主要比较 env 字段中的关键配置 172 | const currentEnv = settings.env || {}; 173 | const configEnv = config.config.env || {}; 174 | 175 | return currentEnv.ANTHROPIC_BASE_URL === configEnv.ANTHROPIC_BASE_URL && 176 | currentEnv.ANTHROPIC_AUTH_TOKEN === configEnv.ANTHROPIC_AUTH_TOKEN; 177 | }) || null; 178 | } 179 | 180 | /** 181 | * 列出所有可用的API配置并提示用户选择(同时支持交互式菜单和序号输入) 182 | */ 183 | function listAndSelectConfig() { 184 | const apiConfigs = readApiConfigs(); 185 | 186 | if (apiConfigs.length === 0) { 187 | console.log(chalk.yellow('没有找到可用的API配置')); 188 | process.exit(0); 189 | } 190 | 191 | // 获取当前激活的配置 192 | const currentConfig = getCurrentConfig(); 193 | 194 | // 如果有当前激活的配置,显示它 195 | if (currentConfig) { 196 | console.log(chalk.green('当前激活的配置: ') + chalk.white(currentConfig.name)); 197 | console.log(); 198 | } 199 | 200 | // 找出最长的名称长度,用于对齐 201 | const maxNameLength = apiConfigs.reduce((max, config) => 202 | Math.max(max, config.name.length), 0); 203 | 204 | // 准备选项列表 205 | const choices = apiConfigs.map((config, index) => { 206 | // 如果是当前激活的配置,添加标记 207 | const isActive = currentConfig && config.name === currentConfig.name; 208 | 209 | // 格式化配置信息:[name] key url,name对齐,密钥不格式化 210 | const paddedName = config.name.padEnd(maxNameLength, ' '); 211 | const configInfo = `[${paddedName}] ${config.config.env.ANTHROPIC_AUTH_TOKEN} ${config.config.env.ANTHROPIC_BASE_URL}`; 212 | 213 | return { 214 | name: `${index + 1}. ${configInfo}${isActive ? chalk.green(' (当前)') : ''}`, 215 | value: index 216 | }; 217 | }); 218 | 219 | // 添加一个输入选项 220 | choices.push(new inquirer.Separator()); 221 | choices.push({ 222 | name: '输入序号...', 223 | value: 'input', 224 | disabled: ' ' // 让输入序号选项不可选中 225 | }); 226 | 227 | // 使用inquirer创建交互式菜单 228 | inquirer 229 | .prompt([ 230 | { 231 | type: 'list', 232 | name: 'configIndex', 233 | message: '请选择要切换的配置:', 234 | choices: choices, 235 | pageSize: choices.length, // 显示所有选项,确保"输入序号..."始终在底部 236 | // 设置更宽的显示宽度以支持长配置信息 237 | prefix: '', 238 | suffix: '', 239 | } 240 | ]) 241 | .then(answers => { 242 | // 如果用户选择了"输入序号"选项 243 | if (answers.configIndex === 'input') { 244 | // 显示配置列表以供参考 245 | console.log(chalk.cyan('\n可用的API配置:')); 246 | apiConfigs.forEach((config, index) => { 247 | const isActive = currentConfig && config.name === currentConfig.name; 248 | const activeMarker = isActive ? chalk.green(' (当前)') : ''; 249 | const paddedName = config.name.padEnd(maxNameLength, ' '); 250 | const configInfo = `[${paddedName}] ${config.config.env.ANTHROPIC_AUTH_TOKEN} ${config.config.env.ANTHROPIC_BASE_URL}`; 251 | console.log(chalk.white(` ${index + 1}. ${configInfo}${activeMarker}`)); 252 | }); 253 | 254 | const rl = createReadlineInterface(); 255 | 256 | rl.question(chalk.cyan('\n请输入配置序号 (1-' + apiConfigs.length + '): '), (indexAnswer) => { 257 | const index = parseInt(indexAnswer, 10); 258 | 259 | if (isNaN(index) || index < 1 || index > apiConfigs.length) { 260 | console.error(chalk.red(`无效的序号: ${indexAnswer},有效范围: 1-${apiConfigs.length}`)); 261 | rl.close(); 262 | return; 263 | } 264 | 265 | const selectedConfig = apiConfigs[index - 1]; 266 | 267 | // 如果选择的配置就是当前激活的配置,提示用户 268 | if (currentConfig && selectedConfig.name === currentConfig.name) { 269 | console.log(chalk.yellow(`\n配置 "${selectedConfig.name}" 已经是当前激活的配置`)); 270 | rl.close(); 271 | return; 272 | } 273 | 274 | processSelectedConfig(selectedConfig); 275 | rl.close(); 276 | }); 277 | return; 278 | } 279 | 280 | // 用户通过交互式菜单选择了配置 281 | const selectedIndex = answers.configIndex; 282 | const selectedConfig = apiConfigs[selectedIndex]; 283 | 284 | // 如果选择的配置就是当前激活的配置,提示用户 285 | if (currentConfig && selectedConfig.name === currentConfig.name) { 286 | console.log(chalk.yellow(`\n配置 "${selectedConfig.name}" 已经是当前激活的配置`)); 287 | return; 288 | } 289 | 290 | processSelectedConfig(selectedConfig); 291 | }) 292 | .catch(error => { 293 | console.error(chalk.red(`发生错误: ${error.message}`)); 294 | }); 295 | } 296 | 297 | /** 298 | * 处理用户选择的配置 299 | * @param {Object} selectedConfig 选择的配置对象 300 | */ 301 | function processSelectedConfig(selectedConfig) { 302 | console.log(chalk.cyan('\n当前选择的配置:')); 303 | console.log(JSON.stringify(selectedConfig, null, 2)); 304 | 305 | inquirer 306 | .prompt([ 307 | { 308 | type: 'confirm', 309 | name: 'confirm', 310 | message: '确认切换到此配置?', 311 | default: true // 修改默认值为true,按Enter键表示确认 312 | } 313 | ]) 314 | .then(confirmAnswer => { 315 | if (confirmAnswer.confirm) { 316 | // 保存配置时保持现有的hooks设置 317 | saveSettingsPreservingHooks(selectedConfig.config); 318 | 319 | console.log(chalk.green(`\n成功切换到配置: ${selectedConfig.name}`)); 320 | 321 | // 显示当前配置信息 322 | console.log(chalk.cyan('\n当前激活配置详情:')); 323 | const { name, config } = selectedConfig; 324 | console.log(chalk.white(`名称: ${name}`)); 325 | console.log(chalk.white(`API Key: ${config.env.ANTHROPIC_AUTH_TOKEN}`)); 326 | console.log(chalk.white(`Base URL: ${config.env.ANTHROPIC_BASE_URL}`)); 327 | console.log(chalk.white(`Model: ${config.model || 'default'}`)); 328 | 329 | // 询问是否要在当前目录运行 Claude 330 | inquirer 331 | .prompt([ 332 | { 333 | type: 'confirm', 334 | name: 'runClaude', 335 | message: '是否要在当前目录运行 claude?', 336 | default: true 337 | } 338 | ]) 339 | .then(runAnswer => { 340 | if (runAnswer.runClaude) { 341 | console.log(chalk.green('\n正在启动 Claude...')); 342 | 343 | // 启动 Claude 344 | const claudeProcess = spawn('claude', [], { 345 | stdio: 'inherit', 346 | cwd: process.cwd() 347 | }); 348 | 349 | claudeProcess.on('error', (error) => { 350 | console.error(chalk.red(`启动 Claude 失败: ${error.message}`)); 351 | console.log(chalk.yellow('请确保 Claude CLI 已正确安装')); 352 | }); 353 | } else { 354 | console.log(chalk.yellow('您可以稍后手动运行 claude 命令')); 355 | } 356 | }) 357 | .catch(error => { 358 | console.error(chalk.red(`发生错误: ${error.message}`)); 359 | }); 360 | } else { 361 | console.log(chalk.yellow('\n操作已取消')); 362 | } 363 | }); 364 | } 365 | 366 | /** 367 | * 列出所有可用的API配置 368 | */ 369 | function listConfigs() { 370 | const apiConfigs = readApiConfigs(); 371 | 372 | if (apiConfigs.length === 0) { 373 | console.log(chalk.yellow('没有找到可用的API配置')); 374 | return; 375 | } 376 | 377 | console.log(chalk.cyan('可用的API配置:')); 378 | apiConfigs.forEach((config, index) => { 379 | console.log(chalk.white(` ${index + 1}. ${config.name}`)); 380 | }); 381 | } 382 | 383 | /** 384 | * 设置当前使用的API配置(使用交互式确认) 385 | * @param {number} index 配置索引 386 | */ 387 | function setConfig(index) { 388 | const apiConfigs = readApiConfigs(); 389 | 390 | if (apiConfigs.length === 0) { 391 | console.log(chalk.yellow('没有找到可用的API配置')); 392 | return; 393 | } 394 | 395 | // 检查索引是否有效 396 | if (index < 1 || index > apiConfigs.length) { 397 | console.error(chalk.red(`无效的索引: ${index},有效范围: 1-${apiConfigs.length}`)); 398 | return; 399 | } 400 | 401 | const selectedConfig = apiConfigs[index - 1]; 402 | 403 | // 显示当前选择的配置 404 | console.log(chalk.cyan('当前选择的配置:')); 405 | console.log(JSON.stringify(selectedConfig, null, 2)); 406 | 407 | // 使用inquirer进行确认 408 | inquirer 409 | .prompt([ 410 | { 411 | type: 'confirm', 412 | name: 'confirm', 413 | message: '确认切换到此配置?', 414 | default: true // 修改默认值为true,按Enter键表示确认 415 | } 416 | ]) 417 | .then(answers => { 418 | if (answers.confirm) { 419 | // 直接使用选择的配置替换整个settings.json 420 | saveSettingsPreservingHooks(selectedConfig.config); 421 | 422 | console.log(chalk.green(`\n成功切换到配置: ${selectedConfig.name}`)); 423 | } else { 424 | console.log(chalk.yellow('\n操作已取消')); 425 | } 426 | }) 427 | .catch(error => { 428 | console.error(chalk.red(`发生错误: ${error.message}`)); 429 | }); 430 | } 431 | 432 | /** 433 | * 获取API配置文件的示例内容 434 | */ 435 | function getApiConfigTemplate() { 436 | return [ 437 | { 438 | "name": "example-config", 439 | "config": { 440 | "env": { 441 | "ANTHROPIC_AUTH_TOKEN": "sk-YOUR_API_KEY_HERE", 442 | "ANTHROPIC_BASE_URL": "https://api.anthropic.com", 443 | "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1" 444 | }, 445 | "permissions": { 446 | "allow": [], 447 | "deny": [] 448 | } 449 | } 450 | } 451 | ]; 452 | } 453 | 454 | /** 455 | * 获取设置文件的示例内容 456 | */ 457 | function getSettingsTemplate() { 458 | return { 459 | "env": { 460 | "ANTHROPIC_AUTH_TOKEN": "sk-YOUR_API_KEY_HERE", 461 | "ANTHROPIC_BASE_URL": "https://api.anthropic.com", 462 | "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1" 463 | }, 464 | "permissions": { 465 | "allow": [], 466 | "deny": [] 467 | } 468 | }; 469 | } 470 | 471 | /** 472 | * 打开指定的配置文件 473 | * @param {string} filePath 文件路径 474 | */ 475 | function openConfigFile(filePath) { 476 | const fullPath = path.resolve(filePath); 477 | 478 | if (!fs.existsSync(fullPath)) { 479 | // 确保配置目录存在 480 | ensureConfigDir(); 481 | 482 | // 创建示例配置文件 483 | let templateContent; 484 | if (fullPath === API_CONFIGS_FILE) { 485 | templateContent = JSON.stringify(getApiConfigTemplate(), null, 2); 486 | console.log(chalk.green(`创建API配置文件: ${fullPath}`)); 487 | } else if (fullPath === SETTINGS_FILE) { 488 | templateContent = JSON.stringify(getSettingsTemplate(), null, 2); 489 | console.log(chalk.green(`创建设置配置文件: ${fullPath}`)); 490 | } else { 491 | console.log(chalk.yellow(`配置文件不存在: ${fullPath}`)); 492 | return; 493 | } 494 | 495 | try { 496 | fs.writeFileSync(fullPath, templateContent, 'utf8'); 497 | console.log(chalk.green(`已创建示例配置文件,请根据需要修改配置内容`)); 498 | } catch (error) { 499 | console.error(chalk.red(`创建配置文件失败: ${error.message}`)); 500 | return; 501 | } 502 | } 503 | 504 | console.log(chalk.cyan(`正在打开: ${fullPath}`)); 505 | 506 | // 使用spawn执行open命令 507 | const child = spawn('open', [fullPath], { 508 | stdio: 'inherit', 509 | detached: true 510 | }); 511 | 512 | child.on('error', (error) => { 513 | console.error(chalk.red(`打开文件失败: ${error.message}`)); 514 | }); 515 | 516 | child.unref(); // 允许父进程独立于子进程退出 517 | } 518 | 519 | /** 520 | * 显示版本信息 521 | */ 522 | function showVersion() { 523 | console.log(`ccs 版本: ${VERSION}`); 524 | } 525 | 526 | 527 | // 设置命令行程序 528 | program 529 | .name('ccs') 530 | .description('Claude配置切换工具') 531 | .version(VERSION, '-v, --version', '显示版本信息'); 532 | 533 | program 534 | .command('list') 535 | .alias('ls') 536 | .description('列出所有可用的API配置并提示选择') 537 | .action(() => { 538 | ensureConfigDir(); 539 | listAndSelectConfig(); 540 | }); 541 | 542 | // 根据需求移除 set/use 命令 543 | 544 | const openCommand = program 545 | .command('o') 546 | .description('打开Claude配置文件'); 547 | 548 | openCommand 549 | .command('api') 550 | .description('打开API配置文件 (apiConfigs.json)') 551 | .action(() => { 552 | openConfigFile(API_CONFIGS_FILE); 553 | }); 554 | 555 | openCommand 556 | .command('setting') 557 | .description('打开设置配置文件 (settings.json)') 558 | .action(() => { 559 | openConfigFile(SETTINGS_FILE); 560 | }); 561 | 562 | // 注册notify相关命令 563 | notify.registerNotifyCommands(program); 564 | 565 | // 注册health相关命令 566 | health.registerHealthCommands(program); 567 | 568 | // 添加错误处理 569 | program.on('command:*', (operands) => { 570 | console.error(chalk.red(`错误: 未知命令 '${operands[0]}'`)); 571 | const availableCommands = program.commands.map(cmd => cmd.name()); 572 | console.log(chalk.cyan('\n可用命令:')); 573 | availableCommands.forEach(cmd => { 574 | console.log(` ${cmd}`); 575 | }); 576 | console.log(chalk.cyan('\n使用 --help 查看更多信息')); 577 | process.exit(1); 578 | }); 579 | 580 | // 如果没有提供命令,显示帮助信息 581 | if (!process.argv.slice(2).length) { 582 | program.outputHelp(); 583 | process.exit(0); // 添加process.exit(0)确保程序在显示帮助信息后退出 584 | } 585 | 586 | program.parse(process.argv); --------------------------------------------------------------------------------