Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
4197e55
docs: add curated-commands.md and openapi-cli-mapping.md design docum…
May 30, 2026
f007eba
docs: address review feedback on design documents
May 30, 2026
ca29eb2
Merge docs/curated-commands-and-mapping: add curated-commands.md and …
May 30, 2026
5de11d5
文档:添加项目架构说明和常见问题
May 30, 2026
2267f24
文档:添加 MIT LICENSE、认证设计文档、企业版前提说明
May 30, 2026
bad824a
docs: add spec-discovery.md and testing.md design docs
May 30, 2026
812436a
文档:auth.md 补充 OSH 认证模式和环境变量
May 30, 2026
7c09085
文档:auth.md 补充 scope 参考表、修正 auth clean 参数
May 30, 2026
15a1ec5
Merge remote-tracking branch 'origin/main' into docs/complete-design-…
May 30, 2026
401a800
文档:补充 spec-discovery.md 自定义编写指南和调试排查章节,testing.md 补充 CI/CD 示例和 spec 完…
May 30, 2026
97f15b2
docs: 补充 review 反馈 — 自定义 spec 格式、合并优先级、退出码、CI/CD 示例
May 30, 2026
4a9c166
Merge remote-tracking branch 'origin/docs/supplement-user-guide' into…
May 30, 2026
f707c06
文档:补充 testing.md 中 jq -e 标志说明
May 30, 2026
57d34ac
docs(testing): 补充 jq -e 标志说明
May 30, 2026
ba1b49d
文档:解决 spec-discovery.md 合并冲突
May 30, 2026
b4c5582
Merge remote-tracking branch 'origin/main' into docs/complete-design-…
May 30, 2026
e90c6e5
Merge remote-tracking branch 'origin/docs/supplement-user-guide' into…
May 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
236 changes: 236 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
# WPS365 CLI Architecture

## Overview

WPS365 CLI is a Go-based command-line tool that provides structured access to the WPS 365 Open Platform APIs. It follows a **spec-driven architecture** — the OpenAPI specification files define the API surface, and curated YAML files define the human-friendly command mapping. The Go binary reads these specs at runtime to build its command tree, perform request validation, and manage authentication.

```
┌─────────────────────────────────────────────────────────┐
│ User / Script │
└──────────────────────┬──────────────────────────────────┘
┌────────────┴────────────┐
│ CLI Flag Parsing │ cobra + pflag
│ (cobra command tree) │
└────────────┬────────────┘
┌───────────────┴───────────────┐
│ Command Router │
│ ┌──────────┐ ┌──────────┐ │
│ │ Curated │ │ Raw API │ │
│ │ Commands │ │ (api get │ │
│ │ │ │ /path) │ │
│ └─────┬─────┘ └────┬─────┘ │
└─────────┼────────────┼─────────┘
│ │
┌──────┴────────────┴──────┐
│ Request Builder │
│ - Resolve path params │
│ - Bind flags → body/que │
│ - Apply transforms │
│ - Validate via schema │
└──────────┬───────────────┘
┌──────────┴───────────────┐
│ Auth Middleware │
│ - Select token type │
│ - Auto-refresh (10s) │
│ - 401 → retry once │
└──────────┬───────────────┘
┌──────────┴───────────────┐
│ HTTP Client │
│ - Send request │
│ - Dry-run support │
└──────────┬───────────────┘
┌──────────┴───────────────┐
│ Output Formatter │
│ json | yaml | table | tsv│
└──────────────────────────┘
```

## Directory Layout

Since the Go source code is not published in this repository, the layout shown here reflects the **runtime data directory** and the **published repo contents**.

### Repository (this repo)

```
.
├── README.md # Chinese documentation
├── README.en.md # English documentation
├── install.sh # macOS/Linux installer
├── install.ps1 # Windows PowerShell installer
└── docs/
├── prerequisites.md # OAuth app setup guide
└── assets/ # Screenshot images for docs
```

### Runtime data directory

macOS: `~/Library/Application Support/wps365-cli/`
Linux: `~/.local/share/wps365-cli/` (or `$XDG_DATA_HOME/wps365-cli/`)
Windows: `%APPDATA%\wps365-cli\`

```
wps365-cli/
├── spec/
│ ├── api/
│ │ ├── 365.yaml # Official OpenAPI 3.0 spec (~74k lines, 801 endpoints)
│ │ └── customs/ # User-provided custom API specs
│ └── curated/
│ ├── 365.yaml # Curated command definitions (152 commands)
│ └── customs/ # User-provided custom curated specs
└── config.json # Non-secret configuration (client_id, redirect_uri, etc.)
```

**Credential storage** (never in the data directory):
- **macOS**: Keychain (via `keyring` Go package)
- **Windows**: Credential Manager
- **Linux**: Encrypted file (`AES-256-GCM`), key auto-generated and stored separately

## Dual-Track Command System

### Track 1: Curated Commands

Curated commands map complex API interactions to semantic CLI verbs. They are defined in `curated/365.yaml`.

Example definition:

```yaml
- id: calendar.events.create
command: calendar events create
summary: 创建日程
method: POST
path: /v7/calendars/{calendar_id}/events/create
args:
- name: calendar-id
required: true
to: path.calendar_id
flags:
- name: from
type: string
required: true
to: body.start_time
- name: name
type: string
to: body.summary
body:
bindings:
- from_flag: from
to: start_time
transform: passthrough
```

Key design principles:
- **Semantic naming**: `calendar events create` over `api post /v7/calendars/{id}/events/create`
- **Smart defaults**: Pagination defaults, required-scope auto-validation
- **Type transforms**: `split_csv`, `to_bool`, `to_int` convert CLI strings to API types
- **Auth constraint checking**: Each command declares its security requirement; incompatible token types produce errors

### Track 2: Raw API Commands

`api get|post|put|patch|delete|head <path>` accesses any endpoint in the OpenAPI spec directly.

```bash
wps365-cli api get "/v7/users/current"
wps365-cli api post "/v7/calendars/create" --data '{"summary": "New Calendar"}'
```

This ensures **100% API coverage** even before a curated command exists.

## Spec Discovery & Loading

1. On startup, the CLI reads the spec directory path (default or `WPS365_CONFIG_DIR`)
2. It loads `api/365.yaml` to build the full API map
3. It loads `curated/365.yaml` to register curated commands
4. Custom specs in `customs/` are merged, allowing users to extend both raw API and curated commands
5. `spec update` fetches the latest official specs from the remote repository
6. `spec add --custom-api` / `--custom-curated` installs user-provided spec files

## Authentication Architecture

```
┌────────────────────────────────────────────────┐
│ Auth Provider │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Delegated │ │ App │ │
│ │ (User OAuth) │ │ (Client │ │
│ │ │ │ Credentials)│ │
│ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │
│ ┌──────┴──────────────────┴───────┐ │
│ │ Token Store │ │
│ │ Keychain / Credential Manager │ │
│ │ / Encrypted File │ │
│ └──────────────────────────────────┘ │
│ │
│ Auto-refresh: │
│ - 10s before expiry → proactive refresh │
│ - 401 response → transparent retry │
│ - Delegated: use refresh_token │
│ - App: re-acquire via client_credentials │
└────────────────────────────────────────────────┘
```

Three auth modes:
1. **Delegated**: User authorizes via browser OAuth, token includes `refresh_token`
2. **App**: Client credentials grant, no user context, `client_id` + `client_secret`
3. **OSH**: Enterprise gateway credentials for internal environments

The CLI reads the OpenAPI `security` field for each endpoint to determine which token type is required. If the current token is incompatible, it errors rather than silently switching.

## Output Pipeline

All commands route through a shared output pipeline:

1. API response (JSON) →
2. JMESPath/jsonpath filtering (if `--query` supported) →
3. Format transformer: `json` (default), `yaml`, `table`, `tsv`

## Global Flags

| Flag | Purpose |
|------|---------|
| `--api-base` | Override API base URL |
| `--dry-run` | Print request without sending |
| `-o, --output` | Output format: json/yaml/table/tsv |
| `--quiet` | Suppress stderr informational output |

## Environment Variables

See [README.md](README.md) for the full list. Key categories:
- **Credentials**: `WPS365_CLIENT_ID`, `WPS365_CLIENT_SECRET`, `WPS365_ACCESS_TOKEN`
- **Endpoints**: `WPS365_API_BASE`, `WPS365_AUTH_URL`, `WPS365_TOKEN_URL`
- **Config**: `WPS365_CONFIG_DIR`, `WPS365_AUTH` (default mode), `WPS365_OUTPUT`
- **Security**: `WPS365_KEYRING_BACKEND`, `WPS365_KEYRING_PASSWORD`

## Extension Points

### Custom Spec Files

Users can extend the CLI without code changes:

```bash
# Add a custom API endpoint definition
wps365-cli spec add --custom-api ./my-api.yaml

# Add a custom curated command
wps365-cli spec add --custom-curated ./my-commands.yaml
```

This is the primary extension mechanism for organizations with private or internal APIs that follow the WPS 365 Open Platform conventions.

### CI/CD Integration

Non-interactive auth via environment variables:

```bash
export WPS365_CLIENT_ID="<client-id>"
export WPS365_CLIENT_SECRET="<client-secret>"
wps365-cli auth login --app
```

Combined with `--dry-run` for safe scripting and `-o tsv` for piping to other tools.
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 WPS 365

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
133 changes: 133 additions & 0 deletions docs/design-docs/auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# 认证与凭证设计

本文档描述 wps365-cli 的认证架构、凭证存储策略和 token 生命周期管理。

## 认证模式

wps365-cli 支持三种认证模式,命令根据底层 OpenAPI 的 `security` 声明自动选择,`--token-type` 可显式覆盖。模式不兼容时直接报错,不静默切换。

| 模式 | 授权类型 | 适用场景 | 获取方式 |
|------|---------|---------|---------|
| `delegated` | OAuth Authorization Code(浏览器授权) | 当前用户信息、个人日历、个人邮件等用户态接口 | `auth login --scopes "kso.user_base.read,kso.calendar.read"` |
| `app` | Client Credentials | 服务端调用、组织级管理、应用态接口 | `auth login --app` |
| `osh` | OSH 网关 Token | 通过 OSH 网关访问开放能力 | `auth login --osh` |

### Delegated 模式流程

1. CLI 启动本地 HTTP 服务监听 `localhost:18365/callback`
2. 打开浏览器访问 WPS 365 OAuth 授权页面,用户登录并授权
3. 授权完成后浏览器重定向到回调地址,携带 `code`
4. CLI 使用 `code` + `client_secret` 换取 `access_token` 和 `refresh_token`
5. Token 安全存储到后端,后续请求自动携带

### App 模式流程

1. CLI 使用 `client_id` + `client_secret` 请求 `https://openapi.wps.cn/oauth2/token`
2. 返回 `access_token`(无 `refresh_token`,过期后重新获取)
3. 适用于 CI/CD 和服务端场景,支持非交互式

### OSH 模式流程

1. CLI 使用 `client_id` + `client_secret` 请求 `https://open.wps.cn/osh/api/v1/consumers/token`
2. 返回 OSH 网关 access token,用于访问 OSH 开放能力
3. 与 app 模式类似,无 `refresh_token`,过期后重新获取

### 非交互式(CI/CD)

```bash
export WPS365_CLIENT_ID="<client-id>"
export WPS365_CLIENT_SECRET="<client-secret>"
wps365-cli auth login --app
```

通过环境变量注入凭证,跳过交互式 `auth setup`。

### 自定义回调地址

Delegated 模式默认回调地址为 `http://localhost:18365/callback`。如果需要自定义(如远程开发环境端口转发),可通过 `--redirect-uri` 覆盖:

```bash
wps365-cli auth login --scopes "kso.user_base.read" --redirect-uri "http://myhost:18365/callback"
```

> 自定义回调地址必须已在 WPS 365 开放平台「安全设置」中注册。

## 凭证存储

`client_secret` 和 token 永不明文落盘,存储后端按平台选择:

| 平台 | 默认后端 | 备选 |
|------|---------|------|
| macOS | System Keychain | 加密文件 |
| Windows | Credential Manager | 加密文件 |
| Linux | AES-256-GCM 加密文件 | — |

通过 `WPS365_KEYRING_BACKEND` 环境变量可强制切换为 `keychain` 或 `file`。

### 文件后端加密

- 算法:AES-256-GCM
- 密钥来源:`WPS365_KEYRING_PASSWORD` 环境变量,未设置时自动生成随机密钥并持久化到本地
- 无需额外配置即可使用

### 直接注入 Token

`WPS365_ACCESS_TOKEN` 环境变量可直接注入 access token,跳过存储和刷新逻辑。适用于已有 token 的外部集成场景。

`WPS365_OSH_TOKEN` 环境变量可直接注入 OSH 网关 token,效果类似。

## Token 生命周期

### 自动刷新策略

| 事件 | Delegated 模式 | App 模式 | OSH 模式 |
|------|-----------|-----|------|
| 过期前 10 秒 | 使用 `refresh_token` 刷新 | 使用 `client_credentials` 重新获取 | 使用 `client_credentials` 重新获取 |
| 收到 401 响应 | 透明刷新 + 重试 | 透明重新获取 + 重试 | 透明重新获取 + 重试 |
| Refresh token 过期 | 提示重新执行 `auth login` | 不适用 | 不适用 |

### 手动操作

```bash
wps365-cli auth status # 查看当前 token 状态、过期时间、认证模式
wps365-cli auth token # 输出 access token(供 curl 等外部工具使用)
wps365-cli auth refresh --delegated # 手动刷新 delegated token
wps365-cli auth refresh --app # 手动刷新 app token
wps365-cli auth logout # 删除 token(保留凭证,可直接重新 login)
wps365-cli auth clean # 清除所有 token、凭证和自动密钥(完全重置,交互确认)
```

## 命令与认证模式映射

每条命令根据底层 OpenAPI 的 `security` 声明确定所需认证模式:

- `security: [{ oauth2: [user_scope] }]` → delegated
- `security: [{ oauth2: [app_scope] }]` → app
- 同时声明两者时,优先 delegated,`--token-type` 可覆盖

不匹配时 CLI 直接报错,不静默切换模式。这确保用户明确知道当前操作使用的身份。

## 常用 Scope 参考

| 业务域 | 常用 Scope | 说明 |
|--------|-----------|------|
| 用户 | `kso.user_base.read` | 读取当前用户基本信息 |
| 日历 | `kso.calendar.read` | 读取日历和日程 |
| 日历 | `kso.calendar.write` | 创建/修改/删除日程 |
| 即时通讯 | `kso.chat.message.read` | 读取消息 |
| 即时通讯 | `kso.chat.message.write` | 发送消息 |
| 通讯录 | `kso.contact.user.read` | 读取通讯录用户信息 |
| 云文档 | `kso.drive.file.read` | 读取云文档 |
| 云文档 | `kso.drive.file.write` | 上传/修改云文档 |
| 多维表 | `kso.dbsheet.read` | 读取多维表 |
| 邮箱 | `kso.mail.read` | 读取邮件 |

> 完整 Scope 列表和权限申请方式请参考 [前置准备](../prerequisites.md) 和 WPS 365 开放平台开发者后台。

## 安全考量

- OAuth 回调仅监听 `localhost`,不暴露到公网
- `client_secret` 不出现在命令行参数或日志中
- Token 刷新请求中的 `client_secret` 仅在内存中使用
- 401 重试最多一次,避免无限循环
- `auth clean` 同时删除自动生成的加密密钥,确保彻底清除
Loading