From 9ced34e8f0a8ef77bde77db3dca04ac037f39060 Mon Sep 17 00:00:00 2001 From: NightStar Date: Tue, 28 Jul 2026 15:17:56 +0800 Subject: [PATCH] =?UTF-8?q?init:=20ry=20CLI=20=E8=AE=BE=E8=AE=A1=E8=A7=84?= =?UTF-8?q?=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/2026-07-28-ry-cli-design.md | 456 +++++++++++++++++++++++++++++++ 1 file changed, 456 insertions(+) create mode 100644 docs/2026-07-28-ry-cli-design.md diff --git a/docs/2026-07-28-ry-cli-design.md b/docs/2026-07-28-ry-cli-design.md new file mode 100644 index 0000000..ba3d7bd --- /dev/null +++ b/docs/2026-07-28-ry-cli-design.md @@ -0,0 +1,456 @@ +# ry CLI 设计规格 + +> 日期:2026-07-28 | 状态:待审查 + +## 概述 + +`ry` 是一个多提供商云服务 CLI 工具,风格对标 `gh`(GitHub CLI)。支持通过统一接口管理不同云厂商(雨云为首个实现),可扩展至阿里云、腾讯云等。 + +- **语言**:Go +- **配置格式**:TOML +- **架构模式**:Provider 接口 + 编译时注册 + +--- + +## 项目结构 + +``` +ry/ # 仓库根目录 +├── cmd/ry/ # 入口 main.go +├── internal/ +│ ├── config/ # TOML 配置读写 +│ ├── provider/ # 提供商接口 + 注册表 +│ │ ├── provider.go # Provider 接口定义 +│ │ ├── registry.go # 提供商注册/工厂 +│ │ └── rainyun/ # 雨云实现 +│ │ ├── rainyun.go # 实现 Provider 接口 +│ │ ├── server.go # 服务器资源操作 +│ │ ├── domain.go # 域名资源操作 +│ │ ├── storage.go # 对象存储操作 +│ │ ├── cdn.go # CDN 操作 +│ │ └── billing.go # 计费操作 +│ ├── cmd/ # cobra 命令定义 +│ │ ├── root.go # 根命令 + 全局 flag +│ │ ├── provider.go # ry provider use|list|add +│ │ ├── server.go # ry server list|get|start|stop... +│ │ ├── domain.go # ry domain list|dns ... +│ │ ├── storage.go # ry storage list|bucket ... +│ │ ├── billing.go # ry billing orders +│ │ └── config.go # ry config show|set|path +│ └── output/ # 统一输出格式化 +│ ├── output.go # Output 接口 +│ ├── table.go # 表格渲染(tablewriter) +│ ├── json.go # JSON 输出 +│ └── yaml.go # YAML 输出 +├── go.mod +├── go.sum +└── README.md +``` + +--- + +## Provider 接口 + +```go +// Provider 云服务商抽象 +type Provider interface { + Name() string // 唯一标识,如 "rainyun" + DisplayName() string // 显示名,如 "雨云" + + // 认证验证 + ValidateAuth(ctx context.Context) error + + // 资源子接口(返回 nil 表示不支持该资源) + Server() ServerService + Domain() DomainService + Storage() StorageService + CDN() CDNService + Billing() BillingService +} + +// ServerService 云服务器操作 +type ServerService interface { + List(ctx context.Context) ([]Server, error) + Get(ctx context.Context, id string) (*Server, error) + Start(ctx context.Context, id string) error + Stop(ctx context.Context, id string) error + Reboot(ctx context.Context, id string) error + Reinstall(ctx context.Context, id, os string) error + ResetPassword(ctx context.Context, id string) error + GetVNC(ctx context.Context, id string) (string, error) + Upgrade(ctx context.Context, id, plan string) error + BackupList(ctx context.Context, id string) ([]Backup, error) + FirewallRuleList(ctx context.Context, id string) ([]FirewallRule, error) +} + +// Server 通用服务器模型 +type Server struct { + ID string `json:"id" table:"ID"` + Name string `json:"name" table:"NAME"` + Status string `json:"status" table:"STATUS"` + IP string `json:"ip" table:"IP"` + CPU int `json:"cpu" table:"CPU"` + Memory int `json:"memory" table:"MEM"` // MB + Disk int `json:"disk" table:"DISK"` // GB + OS string `json:"os" table:"OS"` + Region string `json:"region" table:"REGION"` + ExpireAt time.Time `json:"expires" table:"EXPIRES"` + Raw any `json:"-"` // 原始响应,--raw 时输出 +} +``` + +**设计要点**: +- 不支持的能力返回 `nil`,命令层检测后隐藏或提示 +- 所有模型用 `table` tag 标注表头,`json` tag 标注序列化 +- `Raw` 字段保留原始数据,`--raw` flag 时输出 +- 初期编译时注册 Provider,后续可扩展为 `.so` 插件 + +--- + +## 设计模式(多提供商架构核心) + +不同云厂商的 API 格式差异巨大——雨云用 `X-Api-Key` header + 简单 JSON,阿里云用 AK/SK 签名 + XML 响应体,腾讯云用 v3 签名 + 骆驼命名 JSON。这些差异通过以下三种模式隔离: + +### 工厂模式 — `Registry` 创建 Provider 实例 + +```go +// internal/provider/registry.go +type ProviderFactory func(cfg ProviderConfig) (Provider, error) + +var registry = map[string]ProviderFactory{} + +func Register(name string, factory ProviderFactory) { + registry[name] = factory +} + +func New(name string, cfg ProviderConfig) (Provider, error) { + factory, ok := registry[name] + if !ok { + return nil, fmt.Errorf("unknown provider: %s", name) + } + return factory(cfg) +} +``` + +每个提供商在 `init()` 中自注册: + +```go +// internal/provider/rainyun/rainyun.go +func init() { + provider.Register("rainyun", New) +} + +func New(cfg provider.ProviderConfig) (provider.Provider, error) { + return &Rainyun{apiKey: cfg.APIKey}, nil +} +``` + +### 策略模式 — 每个 Provider 独立实现 Service 接口 + +同一个 `ServerService.List()`,不同提供商的实现完全不同: + +```go +// 雨云:简单 GET + header auth +func (r *RainyunServer) List(ctx context.Context) ([]provider.Server, error) { + req, _ := http.NewRequestWithContext(ctx, "GET", + "https://api.v2.rainyun.com/product/rcs/", nil) + req.Header.Set("X-Api-Key", r.apiKey) + // ... 发请求,parse 雨云格式的 JSON +} + +// 阿里云:AK/SK 签名 + Query 参数 + XML 解析 +func (a *AliyunServer) List(ctx context.Context) ([]provider.Server, error) { + params := map[string]string{"Action": "DescribeInstances", ...} + url := signAndBuild("https://ecs.aliyuncs.com/", a.ak, a.sk, params) + // ... 发请求,parse 阿里云 XML +} +``` + +两个实现返回相同的 `[]Server` 结构,命令层无感知。 + +### 适配器模式 — 内部响应统一 + +每个 Provider 内部有自己的 HTTP 客户端 + 响应适配层: + +```go +// 每个 Provider 包内部 +type client struct { + httpClient *http.Client + baseURL string + authFunc func(*http.Request) error // 注入认证策略 +} + +func (c *client) do(ctx context.Context, method, path string, body any) ([]byte, error) +func (c *client) get(ctx context.Context, path string, result any) error +``` + +认证逻辑作为策略注入 `authFunc`: +- 雨云:`func(r *http.Request) error { r.Header.Set("X-Api-Key", key); return nil }` +- 阿里云:`func(r *http.Request) error { signRequest(r, ak, sk); return nil }` + +### 模式全景 + +``` +用户命令 (cobra) + │ + ▼ +Registry.Get("rainyun") ← 工厂模式:按名创建 Provider + │ + ▼ +Provider.Server().List() ← 策略模式:不同实现,相同接口 + │ + ▼ +client.do(path, authFunc) ← 适配器模式:统一 HTTP 调用 + 注入认证 + │ + ▼ +parse → []Server ← 各 Provider 自行适配响应格式 +``` + +--- + +## 配置系统 + +### 配置文件路径 + +`~/.config/ry/config.toml` + +### 配置结构 + +```toml +# 当前使用的提供商 +current_provider = "rainyun" + +# 默认输出格式:table | json | yaml +output = "table" + +# 提供商认证配置 +[providers.rainyun] +api_key = "***" +# 可选:自定义 API 地址 +# endpoint = "https://api.v2.rainyun.com" + +# [providers.aliyun] +# access_key_id = "..." +# access_key_secret = "..." +# region = "cn-hangzhou" +``` + +### 配置操作 + +- `ry provider use ` — 切换当前提供商 +- `ry provider add ` — 交互式录入凭证 +- `ry provider list` — 列出已配置的提供商 +- `ry config show` — 显示当前配置(API Key 脱敏为 `***`) +- `ry config set ` — 修改配置项 +- `ry config path` — 打印配置文件路径 + +### 环境变量 + +环境变量优先级高于配置文件: +- `RY_PROVIDER` → 覆盖 `current_provider` +- `RY_OUTPUT` → 覆盖 `output` +- `RY_{PROVIDER}_API_KEY` → 覆盖提供商 API Key(如 `RY_RAINYUN_API_KEY`) + +--- + +## 命令树 + +``` +ry +├── provider # 提供商管理 +│ ├── use # 切换当前提供商 +│ ├── list # 列出已配置的提供商 +│ └── add # 交互式添加新提供商 +│ +├── server # 云服务器 +│ ├── list # 列表,支持 --status 过滤 +│ ├── get # 详情 +│ ├── start # 开机 +│ ├── stop # 关机 +│ ├── reboot # 重启 +│ ├── reinstall # 重装系统 --os +│ ├── reset-password # 重置密码 +│ ├── vnc # 获取 VNC 链接 +│ └── upgrade # 升级配置 --plan +│ +├── domain # 域名 +│ ├── list # 域名列表 +│ └── dns # DNS 解析记录 +│ ├── list # 列出记录 +│ ├── add # 添加记录 --type A --name @ --value 1.2.3.4 +│ └── delete # 删除记录 +│ +├── storage # 对象存储 +│ ├── list # 实例列表 +│ └── bucket # 存储桶操作 +│ ├── list # 桶列表 +│ └── create # 创建桶 +│ +├── billing # 计费 +│ └── orders # 订单列表 +│ +├── config # 配置管理 +│ ├── show # 显示当前配置(脱敏) +│ ├── set # 修改配置项 +│ └── path # 打印配置文件路径 +│ +└── completion # 自动补全脚本(bash/zsh/fish) +``` + +### 全局 Flag + +| Flag | 简写 | 说明 | +|------|------|------| +| `--provider` | `-p` | 临时覆盖提供商 | +| `--output` | `-o` | table / json / yaml | +| `--raw` | — | 输出提供商原始响应 | +| `--debug` | — | 输出详细调试信息 | +| `--timeout` | — | 请求超时秒数(默认 30) | + +--- + +## 输出格式 + +### 表格模式(默认) + +``` +$ ry server list +ID NAME STATUS IP CPU MEM EXPIRES +12001 web-server running 171.80.11.70 2C 4GB 2026-08-15 +12002 db-server stopped - 4C 8GB 2026-09-01 +``` + +### JSON 模式 + +``` +$ ry server list -o json +[{"id":"12001","name":"web-server","status":"running","ip":"171.80.11.70","cpu":2,"memory":4096,"expires":"2026-08-15T00:00:00Z"}] +``` + +### YAML 模式 + +``` +$ ry server list -o yaml +- id: "12001" + name: web-server + status: running +``` + +### Raw 模式 + +``` +$ ry server list --raw +--- 原始 API 响应,不做任何处理,适合 pipe 到 jq --- +``` + +--- + +## 错误处理 + +### 分层策略 + +``` +API 层错误 → 包装为 Error 类型 → 用户友好提示(--debug 时显示原始信息) +``` + +### Error 类型 + +```go +type Error struct { + Code int // HTTP 状态码 + Message string // 用户可读消息 + Detail string // 原始错误信息(--debug 时显示) + Raw []byte // API 原始响应体 +} + +func (e *Error) Error() string { return e.Message } +``` + +### 效果示例 + +``` +$ ry server start xxx +Error: 服务器不存在 + Run 'ry server list' to see available servers. + +$ ry server start xxx --debug +Error: GET https://api.v2.rainyun.com/product/rcs/xxx → 404 + {"code":1002,"msg":"product not found"} +``` + +### 超时与重试 + +- 默认超时 30s(`--timeout` 可改) +- GET 请求自动重试 1 次(瞬时网络错误) +- POST/PATCH/DELETE 不重试(幂等性不可保证) +- 业务错误(4xx)不重试 + +--- + +## 测试策略 + +| 层 | 测试方式 | 内容 | +|---|---|---| +| `provider/rainyun` | HTTP mock(`httptest`) | API 调用路径、参数、错误处理 | +| `cmd/` | 集成测试 + golden files | 命令输出快照对比 | +| `config/` | 单元测试 | TOML 读写正确性 | +| `output/` | 单元测试 | 表格/JSON/YAML 渲染 | + +- 测试框架:Go 标准库 `testing` + `net/http/httptest` +- 不引入 testify 等第三方测试库 +- CI 中 `go test ./...` 一跑到底 + +--- + +## Go 依赖 + +| 包 | 用途 | +|---|---| +| `github.com/spf13/cobra` | CLI 命令框架 | +| `github.com/spf13/viper` | 配置 + 环境变量绑定 | +| `github.com/BurntSushi/toml` | TOML 解析 | +| `github.com/olekukoneko/tablewriter` | 表格渲染 | +| `gopkg.in/yaml.v3` | YAML 输出 | + +--- + +## 扩展新提供商 + +1. 在 `internal/provider/` 下新建包(如 `aliyun/`) +2. 实现 `Provider` 接口 +3. 在 `registry.go` 中注册: + ```go + func init() { + Register("aliyun", aliyun.New) + } + ``` +4. 用户 `ry provider add aliyun` 输入凭证即可使用 + +初期所有 Provider 编译进二进制,后续可考虑 Go plugin 动态加载。 + +--- + +## 版本发布 + +- `go install github.com/xxx/ry/cmd/ry@latest` +- 预编译二进制放到 Gitea Release(`git.yoresee.cc`) +- GoReleaser 自动构建 linux/amd64 + darwin/amd64 + darwin/arm64 + +--- + +## 范围边界(第一期) + +**包含**: +- 命令树中列出的所有命令(server/domain/storage/billing/config/provider/completion) +- 雨云 Provider 完整实现 +- 配置系统(TOML + 环境变量) +- 三种输出格式 + raw +- 错误处理 + 调试模式 +- 基础测试覆盖 + +**不含**: +- 游戏云 MCSM/Pterodactyl 面板操作(后续) +- 云应用 RCA 操作(后续) +- 工单系统(后续) +- 非雨云的其他 Provider(后续) +- GoReleaser 配置(后续)