tinet-clink2-cli 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tinet_clink2_cli-0.0.1/MANIFEST.in +3 -0
- tinet_clink2_cli-0.0.1/PKG-INFO +109 -0
- tinet_clink2_cli-0.0.1/cli_anything/README.md +96 -0
- tinet_clink2_cli-0.0.1/cli_anything/clink2/__init__.py +3 -0
- tinet_clink2_cli-0.0.1/cli_anything/clink2/clink2_cli.py +224 -0
- tinet_clink2_cli-0.0.1/cli_anything/clink2/core/__init__.py +1 -0
- tinet_clink2_cli-0.0.1/cli_anything/clink2/core/auth.py +64 -0
- tinet_clink2_cli-0.0.1/cli_anything/clink2/core/config.py +90 -0
- tinet_clink2_cli-0.0.1/cli_anything/clink2/core/http.py +109 -0
- tinet_clink2_cli-0.0.1/cli_anything/clink2/core/logging.py +130 -0
- tinet_clink2_cli-0.0.1/pyproject.toml +33 -0
- tinet_clink2_cli-0.0.1/setup.cfg +4 -0
- tinet_clink2_cli-0.0.1/tinet_clink2_cli.egg-info/PKG-INFO +109 -0
- tinet_clink2_cli-0.0.1/tinet_clink2_cli.egg-info/SOURCES.txt +16 -0
- tinet_clink2_cli-0.0.1/tinet_clink2_cli.egg-info/dependency_links.txt +1 -0
- tinet_clink2_cli-0.0.1/tinet_clink2_cli.egg-info/entry_points.txt +2 -0
- tinet_clink2_cli-0.0.1/tinet_clink2_cli.egg-info/requires.txt +7 -0
- tinet_clink2_cli-0.0.1/tinet_clink2_cli.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tinet-clink2-cli
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Clink2 OpenAPI CLI (AK-SK, IVR queries)
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: click>=8.0
|
|
8
|
+
Requires-Dist: requests>=2.28
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
11
|
+
Requires-Dist: responses>=0.25; extra == "dev"
|
|
12
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
13
|
+
|
|
14
|
+
Clink2 命令行客户端,封装 OpenAPI 接口,与 [智慧服务平台开发者中心](https://develop.clink.cn) 中参数与响应一致,适合脚本化集成与 Agent 调用。
|
|
15
|
+
|
|
16
|
+
**当前版本:** `0.0.1`
|
|
17
|
+
**PyPI:** https://pypi.org/project/tinet-clink2-cli/
|
|
18
|
+
|
|
19
|
+
## 功能概览
|
|
20
|
+
|
|
21
|
+
| 子命令 | 说明 |
|
|
22
|
+
|--------|------|
|
|
23
|
+
| **`config`** | 本地凭证管理(AccessKeyId / AccessKeySecret / 接入地址 / Expires) |
|
|
24
|
+
| **`ivr`** | 语音导航查询:列表、节点列表 |
|
|
25
|
+
|
|
26
|
+
所有出站 HTTP 请求自动附加:
|
|
27
|
+
- `X-Trace-Id: {UUID}`(每次请求新生成,不参与签名)
|
|
28
|
+
|
|
29
|
+
本地默认写入 JSON Lines 日志:`~/.clink2/logs/clink2.log`(单文件最大 10MB,滚动保留 5 个)。
|
|
30
|
+
|
|
31
|
+
## 环境要求
|
|
32
|
+
|
|
33
|
+
- Python **≥ 3.9**
|
|
34
|
+
- 运行时需访问公有云接入地址,例如 `https://api-bj.clink.cn` / `https://api-sh.clink.cn`
|
|
35
|
+
- 需在管理后台获取 AccessKeyId / AccessKeySecret(系统管理 → 安全设置 → 接口密钥)
|
|
36
|
+
|
|
37
|
+
## 安装
|
|
38
|
+
|
|
39
|
+
从 [PyPI](https://pypi.org/project/tinet-clink2-cli/) 安装(推荐):
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install tinet-clink2-cli
|
|
43
|
+
tinet-clink2-cli --version # 0.0.1
|
|
44
|
+
tinet-clink2-cli --help
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
指定版本或升级:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install 'tinet-clink2-cli==0.0.1'
|
|
51
|
+
pip install --upgrade tinet-clink2-cli
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
macOS 等受限环境可使用 [pipx](https://pipx.pypa.io/):
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pipx install tinet-clink2-cli
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## 配置凭证
|
|
61
|
+
|
|
62
|
+
**配置文件:** `~/.clink2/config.json`
|
|
63
|
+
|
|
64
|
+
入参名称与 OpenAPI 公共参数一致:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
tinet-clink2-cli config set \
|
|
68
|
+
--AccessKeyId your_access_key_id \
|
|
69
|
+
--AccessKeySecret your_access_key_secret \
|
|
70
|
+
--endpoint https://api-bj.clink.cn \
|
|
71
|
+
--Expires 60
|
|
72
|
+
|
|
73
|
+
tinet-clink2-cli config show
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
默认接入地址为北京:`https://api-bj.clink.cn`。`config show` 会对 AccessKeySecret 脱敏。
|
|
77
|
+
|
|
78
|
+
## 快速示例
|
|
79
|
+
|
|
80
|
+
顺序与上方「功能概览」一致。完整取值见各子命令 `--help` 或 [开发者中心](https://develop.clink.cn)。
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# config
|
|
84
|
+
tinet-clink2-cli config show
|
|
85
|
+
|
|
86
|
+
# ivr
|
|
87
|
+
tinet-clink2-cli ivr list
|
|
88
|
+
tinet-clink2-cli ivr nodes --ivrName your_ivr_name
|
|
89
|
+
tinet-clink2-cli --json ivr list
|
|
90
|
+
tinet-clink2-cli --json ivr nodes --ivrName your_ivr_name
|
|
91
|
+
|
|
92
|
+
# dry-run:仅校验入参与配置,不调用服务端,但仍写本地日志
|
|
93
|
+
tinet-clink2-cli --dry-run ivr list
|
|
94
|
+
tinet-clink2-cli --dry-run ivr nodes --ivrName your_ivr_name
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
全局 `--json` 输出 OpenAPI 原始 JSON。`--dry-run` 做本地校验与预览(不发请求),详见 `--help`。
|
|
98
|
+
|
|
99
|
+
## 帮助与 API 文档
|
|
100
|
+
|
|
101
|
+
安装后通过 `--help` 查看一级子命令:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
tinet-clink2-cli --help
|
|
105
|
+
tinet-clink2-cli config --help
|
|
106
|
+
tinet-clink2-cli ivr --help
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
OpenAPI 契约与字段说明见 [智慧服务平台开发者中心](https://develop.clink.cn)。
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Clink2 命令行客户端,封装 OpenAPI 接口,与 [智慧服务平台开发者中心](https://develop.clink.cn) 中参数与响应一致,适合脚本化集成与 Agent 调用。
|
|
2
|
+
|
|
3
|
+
**当前版本:** `0.0.1`
|
|
4
|
+
**PyPI:** https://pypi.org/project/tinet-clink2-cli/
|
|
5
|
+
|
|
6
|
+
## 功能概览
|
|
7
|
+
|
|
8
|
+
| 子命令 | 说明 |
|
|
9
|
+
|--------|------|
|
|
10
|
+
| **`config`** | 本地凭证管理(AccessKeyId / AccessKeySecret / 接入地址 / Expires) |
|
|
11
|
+
| **`ivr`** | 语音导航查询:列表、节点列表 |
|
|
12
|
+
|
|
13
|
+
所有出站 HTTP 请求自动附加:
|
|
14
|
+
- `X-Trace-Id: {UUID}`(每次请求新生成,不参与签名)
|
|
15
|
+
|
|
16
|
+
本地默认写入 JSON Lines 日志:`~/.clink2/logs/clink2.log`(单文件最大 10MB,滚动保留 5 个)。
|
|
17
|
+
|
|
18
|
+
## 环境要求
|
|
19
|
+
|
|
20
|
+
- Python **≥ 3.9**
|
|
21
|
+
- 运行时需访问公有云接入地址,例如 `https://api-bj.clink.cn` / `https://api-sh.clink.cn`
|
|
22
|
+
- 需在管理后台获取 AccessKeyId / AccessKeySecret(系统管理 → 安全设置 → 接口密钥)
|
|
23
|
+
|
|
24
|
+
## 安装
|
|
25
|
+
|
|
26
|
+
从 [PyPI](https://pypi.org/project/tinet-clink2-cli/) 安装(推荐):
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install tinet-clink2-cli
|
|
30
|
+
tinet-clink2-cli --version # 0.0.1
|
|
31
|
+
tinet-clink2-cli --help
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
指定版本或升级:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install 'tinet-clink2-cli==0.0.1'
|
|
38
|
+
pip install --upgrade tinet-clink2-cli
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
macOS 等受限环境可使用 [pipx](https://pipx.pypa.io/):
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pipx install tinet-clink2-cli
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## 配置凭证
|
|
48
|
+
|
|
49
|
+
**配置文件:** `~/.clink2/config.json`
|
|
50
|
+
|
|
51
|
+
入参名称与 OpenAPI 公共参数一致:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
tinet-clink2-cli config set \
|
|
55
|
+
--AccessKeyId your_access_key_id \
|
|
56
|
+
--AccessKeySecret your_access_key_secret \
|
|
57
|
+
--endpoint https://api-bj.clink.cn \
|
|
58
|
+
--Expires 60
|
|
59
|
+
|
|
60
|
+
tinet-clink2-cli config show
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
默认接入地址为北京:`https://api-bj.clink.cn`。`config show` 会对 AccessKeySecret 脱敏。
|
|
64
|
+
|
|
65
|
+
## 快速示例
|
|
66
|
+
|
|
67
|
+
顺序与上方「功能概览」一致。完整取值见各子命令 `--help` 或 [开发者中心](https://develop.clink.cn)。
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# config
|
|
71
|
+
tinet-clink2-cli config show
|
|
72
|
+
|
|
73
|
+
# ivr
|
|
74
|
+
tinet-clink2-cli ivr list
|
|
75
|
+
tinet-clink2-cli ivr nodes --ivrName your_ivr_name
|
|
76
|
+
tinet-clink2-cli --json ivr list
|
|
77
|
+
tinet-clink2-cli --json ivr nodes --ivrName your_ivr_name
|
|
78
|
+
|
|
79
|
+
# dry-run:仅校验入参与配置,不调用服务端,但仍写本地日志
|
|
80
|
+
tinet-clink2-cli --dry-run ivr list
|
|
81
|
+
tinet-clink2-cli --dry-run ivr nodes --ivrName your_ivr_name
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
全局 `--json` 输出 OpenAPI 原始 JSON。`--dry-run` 做本地校验与预览(不发请求),详见 `--help`。
|
|
85
|
+
|
|
86
|
+
## 帮助与 API 文档
|
|
87
|
+
|
|
88
|
+
安装后通过 `--help` 查看一级子命令:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
tinet-clink2-cli --help
|
|
92
|
+
tinet-clink2-cli config --help
|
|
93
|
+
tinet-clink2-cli ivr --help
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
OpenAPI 契约与字段说明见 [智慧服务平台开发者中心](https://develop.clink.cn)。
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""tinet-clink2-cli Click 入口"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
import uuid
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
|
|
12
|
+
from cli_anything.clink2 import __version__
|
|
13
|
+
from cli_anything.clink2.core import config as cfg
|
|
14
|
+
from cli_anything.clink2.core.http import Clink2Client
|
|
15
|
+
from cli_anything.clink2.core.logging import write_log_record
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@click.group()
|
|
19
|
+
@click.version_option(
|
|
20
|
+
version=__version__,
|
|
21
|
+
prog_name="tinet-clink2-cli",
|
|
22
|
+
message="%(prog)s,version %(version)s,released on 2026-07-20",
|
|
23
|
+
)
|
|
24
|
+
@click.option("--json", "as_json", is_flag=True, help="以 JSON 格式输出结果")
|
|
25
|
+
@click.option("--dry-run", is_flag=True, help="仅校验入参与配置,不调用服务端接口")
|
|
26
|
+
@click.pass_context
|
|
27
|
+
def cli(ctx: click.Context, as_json: bool, dry_run: bool) -> None:
|
|
28
|
+
"""Clink2 OpenAPI 命令行工具"""
|
|
29
|
+
ctx.ensure_object(dict)
|
|
30
|
+
ctx.obj["as_json"] = as_json
|
|
31
|
+
ctx.obj["dry_run"] = dry_run
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@cli.group()
|
|
35
|
+
def config() -> None:
|
|
36
|
+
"""管理本地 AccessKey 与接入地址配置"""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@config.command("set")
|
|
40
|
+
@click.option(
|
|
41
|
+
"--AccessKeyId",
|
|
42
|
+
"access_key_id",
|
|
43
|
+
required=True,
|
|
44
|
+
help="访问密钥 ID",
|
|
45
|
+
)
|
|
46
|
+
@click.option(
|
|
47
|
+
"--AccessKeySecret",
|
|
48
|
+
"access_key_secret",
|
|
49
|
+
required=True,
|
|
50
|
+
help="访问密钥 Secret",
|
|
51
|
+
)
|
|
52
|
+
@click.option(
|
|
53
|
+
"--endpoint",
|
|
54
|
+
default=cfg.DEFAULT_ENDPOINT,
|
|
55
|
+
show_default=True,
|
|
56
|
+
help="API 接入地址(公有云北京/上海等)",
|
|
57
|
+
)
|
|
58
|
+
@click.option(
|
|
59
|
+
"--Expires",
|
|
60
|
+
"expires",
|
|
61
|
+
default=cfg.DEFAULT_EXPIRES,
|
|
62
|
+
show_default=True,
|
|
63
|
+
type=int,
|
|
64
|
+
help="签名有效时间,单位秒(与 OpenAPI 公共参数 Expires 一致,1–86400)",
|
|
65
|
+
)
|
|
66
|
+
def config_set(access_key_id: str, access_key_secret: str, endpoint: str, expires: int) -> None:
|
|
67
|
+
"""将凭据保存到 ~/.clink2/config.json"""
|
|
68
|
+
path = cfg.save_config(
|
|
69
|
+
access_key_id=access_key_id,
|
|
70
|
+
access_key_secret=access_key_secret,
|
|
71
|
+
endpoint=endpoint,
|
|
72
|
+
expires=expires,
|
|
73
|
+
)
|
|
74
|
+
click.echo(f"配置已保存到 {path}")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@config.command("show")
|
|
78
|
+
def config_show() -> None:
|
|
79
|
+
"""查看本地配置(AccessKeySecret 脱敏显示)"""
|
|
80
|
+
try:
|
|
81
|
+
data = cfg.load_config()
|
|
82
|
+
except FileNotFoundError as exc:
|
|
83
|
+
raise click.ClickException(str(exc)) from exc
|
|
84
|
+
display = {
|
|
85
|
+
"AccessKeyId": data["access_key_id"],
|
|
86
|
+
"AccessKeySecret": cfg.mask_secret(data["access_key_secret"]),
|
|
87
|
+
"endpoint": data.get("endpoint", cfg.DEFAULT_ENDPOINT),
|
|
88
|
+
"Expires": data.get("expires", cfg.DEFAULT_EXPIRES),
|
|
89
|
+
}
|
|
90
|
+
click.echo(json.dumps(display, ensure_ascii=False, indent=2))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _load_or_fail() -> dict[str, Any]:
|
|
94
|
+
"""加载配置;失败时抛出 ClickException"""
|
|
95
|
+
try:
|
|
96
|
+
return cfg.load_config()
|
|
97
|
+
except (FileNotFoundError, ValueError) as exc:
|
|
98
|
+
raise click.ClickException(str(exc)) from exc
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _emit(ctx: click.Context, payload: Any, *, exit_code: int = 0) -> None:
|
|
102
|
+
"""输出结果(JSON 或文本)"""
|
|
103
|
+
if ctx.obj.get("as_json") or isinstance(payload, (dict, list)):
|
|
104
|
+
click.echo(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
105
|
+
else:
|
|
106
|
+
click.echo(str(payload))
|
|
107
|
+
if exit_code:
|
|
108
|
+
ctx.exit(exit_code)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _run_api(
|
|
112
|
+
ctx: click.Context,
|
|
113
|
+
command: str,
|
|
114
|
+
method: str,
|
|
115
|
+
path: str,
|
|
116
|
+
params: Optional[dict[str, Any]] = None,
|
|
117
|
+
) -> None:
|
|
118
|
+
"""执行 API 调用或 dry-run,并写本地日志"""
|
|
119
|
+
started = time.perf_counter()
|
|
120
|
+
conf = _load_or_fail()
|
|
121
|
+
endpoint = conf["endpoint"].rstrip("/")
|
|
122
|
+
full_url = f"{endpoint}{path}"
|
|
123
|
+
trace_id = str(uuid.uuid4())
|
|
124
|
+
dry_run = bool(ctx.obj.get("dry_run"))
|
|
125
|
+
|
|
126
|
+
if dry_run:
|
|
127
|
+
duration_ms = int((time.perf_counter() - started) * 1000)
|
|
128
|
+
write_log_record(
|
|
129
|
+
command=command,
|
|
130
|
+
duration_ms=duration_ms,
|
|
131
|
+
endpoint=full_url,
|
|
132
|
+
trace_id=trace_id,
|
|
133
|
+
http_status=None,
|
|
134
|
+
result="0",
|
|
135
|
+
error_type=None,
|
|
136
|
+
error_message=None,
|
|
137
|
+
dry_run=True,
|
|
138
|
+
request_sent=False,
|
|
139
|
+
)
|
|
140
|
+
payload = {
|
|
141
|
+
"dry_run": "ok",
|
|
142
|
+
"method": method,
|
|
143
|
+
"endpoint": full_url,
|
|
144
|
+
"params": params or {},
|
|
145
|
+
"trace_id": trace_id,
|
|
146
|
+
}
|
|
147
|
+
_emit(ctx, payload)
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
client = Clink2Client(
|
|
151
|
+
endpoint=endpoint,
|
|
152
|
+
access_key_id=conf["access_key_id"],
|
|
153
|
+
access_key_secret=conf["access_key_secret"],
|
|
154
|
+
expires=int(conf.get("expires", cfg.DEFAULT_EXPIRES)),
|
|
155
|
+
)
|
|
156
|
+
try:
|
|
157
|
+
body, meta = client.request(method, path, params=params or {}, trace_id=trace_id)
|
|
158
|
+
duration_ms = meta["duration_ms"]
|
|
159
|
+
status = meta["http_status"]
|
|
160
|
+
ok = 200 <= int(status) < 300
|
|
161
|
+
write_log_record(
|
|
162
|
+
command=command,
|
|
163
|
+
duration_ms=duration_ms,
|
|
164
|
+
endpoint=meta.get("url") or full_url,
|
|
165
|
+
trace_id=meta["trace_id"],
|
|
166
|
+
http_status=status,
|
|
167
|
+
result="0" if ok else str(status),
|
|
168
|
+
error_type=None if ok else "http_error",
|
|
169
|
+
error_message=None if ok else f"HTTP {status}",
|
|
170
|
+
)
|
|
171
|
+
if not ok:
|
|
172
|
+
err = {"error_type": "http_error", "error_message": f"HTTP {status}", "data": body}
|
|
173
|
+
_emit(ctx, err, exit_code=1)
|
|
174
|
+
return
|
|
175
|
+
_emit(ctx, body)
|
|
176
|
+
except click.exceptions.Exit:
|
|
177
|
+
raise
|
|
178
|
+
except Exception as exc: # noqa: BLE001
|
|
179
|
+
duration_ms = int((time.perf_counter() - started) * 1000)
|
|
180
|
+
write_log_record(
|
|
181
|
+
command=command,
|
|
182
|
+
duration_ms=duration_ms,
|
|
183
|
+
endpoint=full_url,
|
|
184
|
+
trace_id=trace_id,
|
|
185
|
+
http_status=None,
|
|
186
|
+
result="1",
|
|
187
|
+
error_type=type(exc).__name__,
|
|
188
|
+
error_message=str(exc),
|
|
189
|
+
)
|
|
190
|
+
err = {"error_type": type(exc).__name__, "error_message": str(exc)}
|
|
191
|
+
if ctx.obj.get("as_json"):
|
|
192
|
+
click.echo(json.dumps(err, ensure_ascii=False, indent=2), err=True)
|
|
193
|
+
else:
|
|
194
|
+
raise click.ClickException(str(exc)) from exc
|
|
195
|
+
ctx.exit(1)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@cli.group()
|
|
199
|
+
def ivr() -> None:
|
|
200
|
+
"""语音导航(IVR)查询命令"""
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@ivr.command("list")
|
|
204
|
+
@click.pass_context
|
|
205
|
+
def ivr_list(ctx: click.Context) -> None:
|
|
206
|
+
"""查询语音导航列表"""
|
|
207
|
+
_run_api(ctx, "ivr list", "GET", "/cc/list_ivrs", params={})
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@ivr.command("nodes")
|
|
211
|
+
@click.option(
|
|
212
|
+
"--ivrName",
|
|
213
|
+
"ivr_name",
|
|
214
|
+
required=True,
|
|
215
|
+
help="语音导航名称(与 OpenAPI 请求参数 ivrName 一致)",
|
|
216
|
+
)
|
|
217
|
+
@click.pass_context
|
|
218
|
+
def ivr_nodes(ctx: click.Context, ivr_name: str) -> None:
|
|
219
|
+
"""查询语音导航节点列表"""
|
|
220
|
+
_run_api(ctx, "ivr nodes", "GET", "/cc/list_ivr_nodes", params={"ivrName": ivr_name})
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
if __name__ == "__main__":
|
|
224
|
+
cli()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Core modules for config, auth, HTTP, and logging."""
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""AK-SK signature helpers for Clink2 OpenAPI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import hashlib
|
|
7
|
+
import hmac
|
|
8
|
+
from typing import Mapping
|
|
9
|
+
from urllib.parse import quote
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def build_string_to_sign(
|
|
13
|
+
method: str,
|
|
14
|
+
host: str,
|
|
15
|
+
path: str,
|
|
16
|
+
params: Mapping[str, str],
|
|
17
|
+
) -> str:
|
|
18
|
+
"""Build the canonical string to sign for a Clink2 request.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
method: HTTP method (e.g. GET).
|
|
22
|
+
host: API host without scheme (e.g. api-bj.clink.cn).
|
|
23
|
+
path: Request path starting with /.
|
|
24
|
+
params: Query parameters excluding Signature.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Canonical string used as hmac-sha1 input.
|
|
28
|
+
"""
|
|
29
|
+
sorted_items = sorted(params.items(), key=lambda item: item[0])
|
|
30
|
+
encoded_pairs = [
|
|
31
|
+
f"{quote(str(name), safe='')}={quote(str(value), safe='')}"
|
|
32
|
+
for name, value in sorted_items
|
|
33
|
+
]
|
|
34
|
+
query = "&".join(encoded_pairs)
|
|
35
|
+
return f"{method.upper()}{host}{path}?{query}"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def sign_request(
|
|
39
|
+
access_key_secret: str,
|
|
40
|
+
method: str,
|
|
41
|
+
host: str,
|
|
42
|
+
path: str,
|
|
43
|
+
params: Mapping[str, str],
|
|
44
|
+
) -> str:
|
|
45
|
+
"""Compute URL-encoded Signature for Clink2 OpenAPI.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
access_key_secret: AccessKeySecret used as HMAC key.
|
|
49
|
+
method: HTTP method.
|
|
50
|
+
host: API host without scheme.
|
|
51
|
+
path: Request path.
|
|
52
|
+
params: Query parameters excluding Signature.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
URL-encoded Signature value for the query string.
|
|
56
|
+
"""
|
|
57
|
+
string_to_sign = build_string_to_sign(method, host, path, params)
|
|
58
|
+
digest = hmac.new(
|
|
59
|
+
access_key_secret.encode("utf-8"),
|
|
60
|
+
string_to_sign.encode("utf-8"),
|
|
61
|
+
hashlib.sha1,
|
|
62
|
+
).digest()
|
|
63
|
+
# Return raw base64; HTTP clients URL-encode query values once.
|
|
64
|
+
return base64.b64encode(digest).decode("utf-8")
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Local Clink2 CLI configuration stored under ~/.clink2."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
DEFAULT_ENDPOINT = "https://api-bj.clink.cn"
|
|
11
|
+
DEFAULT_EXPIRES = 60
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def config_dir() -> Path:
|
|
15
|
+
"""Return the Clink2 config directory (~/.clink2)."""
|
|
16
|
+
return Path(os.path.expanduser("~")) / ".clink2"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def config_path() -> Path:
|
|
20
|
+
"""Return the path to config.json."""
|
|
21
|
+
return config_dir() / "config.json"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def mask_secret(secret: str) -> str:
|
|
25
|
+
"""Return a masked representation of an access key secret.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
secret: Plaintext secret.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Masked string that never equals the plaintext for non-empty secrets.
|
|
32
|
+
"""
|
|
33
|
+
if not secret:
|
|
34
|
+
return ""
|
|
35
|
+
if len(secret) <= 4:
|
|
36
|
+
return "*" * len(secret)
|
|
37
|
+
return "*" * (len(secret) - 4) + secret[-4:]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def save_config(
|
|
41
|
+
access_key_id: str,
|
|
42
|
+
access_key_secret: str,
|
|
43
|
+
endpoint: str = DEFAULT_ENDPOINT,
|
|
44
|
+
expires: int = DEFAULT_EXPIRES,
|
|
45
|
+
) -> Path:
|
|
46
|
+
"""Persist credentials and endpoint to ~/.clink2/config.json.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
access_key_id: AccessKeyId.
|
|
50
|
+
access_key_secret: AccessKeySecret.
|
|
51
|
+
endpoint: API base URL.
|
|
52
|
+
expires: Signature expires seconds.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Path to the written config file.
|
|
56
|
+
"""
|
|
57
|
+
directory = config_dir()
|
|
58
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
path = config_path()
|
|
60
|
+
payload = {
|
|
61
|
+
"access_key_id": access_key_id,
|
|
62
|
+
"access_key_secret": access_key_secret,
|
|
63
|
+
"endpoint": endpoint or DEFAULT_ENDPOINT,
|
|
64
|
+
"expires": int(expires) if expires is not None else DEFAULT_EXPIRES,
|
|
65
|
+
}
|
|
66
|
+
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
67
|
+
os.chmod(path, 0o600)
|
|
68
|
+
return path
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def load_config() -> dict[str, Any]:
|
|
72
|
+
"""Load configuration from ~/.clink2/config.json.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Config dictionary.
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
FileNotFoundError: If config file is missing.
|
|
79
|
+
ValueError: If required fields are missing.
|
|
80
|
+
"""
|
|
81
|
+
path = config_path()
|
|
82
|
+
if not path.exists():
|
|
83
|
+
raise FileNotFoundError(f"未找到配置文件: {path}。请先执行 `tinet-clink2-cli config set`。")
|
|
84
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
85
|
+
for key in ("access_key_id", "access_key_secret"):
|
|
86
|
+
if not data.get(key):
|
|
87
|
+
raise ValueError(f"配置缺少必填字段: {key}")
|
|
88
|
+
data.setdefault("endpoint", DEFAULT_ENDPOINT)
|
|
89
|
+
data.setdefault("expires", DEFAULT_EXPIRES)
|
|
90
|
+
return data
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""HTTPS client for Clink2 OpenAPI with AK-SK signing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
import uuid
|
|
7
|
+
from typing import Any, Mapping, MutableMapping, Optional, Tuple
|
|
8
|
+
from urllib.parse import urlparse
|
|
9
|
+
|
|
10
|
+
import requests
|
|
11
|
+
|
|
12
|
+
from cli_anything.clink2.core.auth import sign_request
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Clink2Client:
|
|
16
|
+
"""Clink2 OpenAPI HTTP client."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
endpoint: str,
|
|
21
|
+
access_key_id: str,
|
|
22
|
+
access_key_secret: str,
|
|
23
|
+
expires: int = 60,
|
|
24
|
+
session: Optional[requests.Session] = None,
|
|
25
|
+
) -> None:
|
|
26
|
+
"""Initialize client credentials and base endpoint.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
endpoint: Base URL such as https://api-bj.clink.cn.
|
|
30
|
+
access_key_id: AccessKeyId.
|
|
31
|
+
access_key_secret: AccessKeySecret.
|
|
32
|
+
expires: Signature validity in seconds.
|
|
33
|
+
session: Optional requests session.
|
|
34
|
+
"""
|
|
35
|
+
self.endpoint = endpoint.rstrip("/")
|
|
36
|
+
self.access_key_id = access_key_id
|
|
37
|
+
self.access_key_secret = access_key_secret
|
|
38
|
+
self.expires = int(expires)
|
|
39
|
+
self.session = session or requests.Session()
|
|
40
|
+
|
|
41
|
+
def _host(self) -> str:
|
|
42
|
+
"""Return API host parsed from endpoint."""
|
|
43
|
+
parsed = urlparse(self.endpoint)
|
|
44
|
+
if parsed.scheme != "https":
|
|
45
|
+
raise ValueError("Only HTTPS endpoints are supported")
|
|
46
|
+
if not parsed.netloc:
|
|
47
|
+
raise ValueError(f"Invalid endpoint: {self.endpoint}")
|
|
48
|
+
return parsed.netloc
|
|
49
|
+
|
|
50
|
+
def request(
|
|
51
|
+
self,
|
|
52
|
+
method: str,
|
|
53
|
+
path: str,
|
|
54
|
+
params: Optional[Mapping[str, Any]] = None,
|
|
55
|
+
trace_id: Optional[str] = None,
|
|
56
|
+
) -> Tuple[Any, dict[str, Any]]:
|
|
57
|
+
"""Send a signed HTTPS request.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
method: HTTP method.
|
|
61
|
+
path: API path starting with /.
|
|
62
|
+
params: Business query parameters.
|
|
63
|
+
trace_id: Optional pre-generated trace id.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Tuple of (JSON body, meta dict with http_status, trace_id, duration_ms, url).
|
|
67
|
+
"""
|
|
68
|
+
business: MutableMapping[str, str] = {
|
|
69
|
+
str(k): str(v) for k, v in (params or {}).items() if v is not None
|
|
70
|
+
}
|
|
71
|
+
timestamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
72
|
+
query: MutableMapping[str, str] = {
|
|
73
|
+
**business,
|
|
74
|
+
"AccessKeyId": self.access_key_id,
|
|
75
|
+
"Expires": str(self.expires),
|
|
76
|
+
"Timestamp": timestamp,
|
|
77
|
+
}
|
|
78
|
+
host = self._host()
|
|
79
|
+
signature = sign_request(
|
|
80
|
+
access_key_secret=self.access_key_secret,
|
|
81
|
+
method=method,
|
|
82
|
+
host=host,
|
|
83
|
+
path=path,
|
|
84
|
+
params=query,
|
|
85
|
+
)
|
|
86
|
+
query["Signature"] = signature
|
|
87
|
+
tid = trace_id or str(uuid.uuid4())
|
|
88
|
+
url = f"{self.endpoint}{path}"
|
|
89
|
+
started = time.perf_counter()
|
|
90
|
+
response = self.session.request(
|
|
91
|
+
method=method.upper(),
|
|
92
|
+
url=url,
|
|
93
|
+
params=query,
|
|
94
|
+
headers={"X-Trace-Id": tid},
|
|
95
|
+
timeout=30,
|
|
96
|
+
verify=True,
|
|
97
|
+
)
|
|
98
|
+
duration_ms = int((time.perf_counter() - started) * 1000)
|
|
99
|
+
try:
|
|
100
|
+
body: Any = response.json()
|
|
101
|
+
except ValueError:
|
|
102
|
+
body = {"raw": response.text}
|
|
103
|
+
meta = {
|
|
104
|
+
"http_status": response.status_code,
|
|
105
|
+
"trace_id": tid,
|
|
106
|
+
"duration_ms": duration_ms,
|
|
107
|
+
"url": response.url,
|
|
108
|
+
}
|
|
109
|
+
return body, meta
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Local JSON-lines logging for tinet-clink2-cli."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import platform
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from logging.handlers import RotatingFileHandler
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Optional
|
|
13
|
+
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
|
|
14
|
+
|
|
15
|
+
from cli_anything.clink2 import __version__
|
|
16
|
+
|
|
17
|
+
_LOGGER_NAME = "tinet_clink2_cli"
|
|
18
|
+
_handler_configured = False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def logs_dir() -> Path:
|
|
22
|
+
"""Return ~/.clink2/logs directory."""
|
|
23
|
+
return Path(os.path.expanduser("~")) / ".clink2" / "logs"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_log_path() -> Path:
|
|
27
|
+
"""Return the primary log file path."""
|
|
28
|
+
return logs_dir() / "clink2.log"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def redact_endpoint(endpoint: str) -> str:
|
|
32
|
+
"""Remove or redact Signature from an endpoint URL for safe logging.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
endpoint: Full or partial request URL.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
URL safe to write to logs.
|
|
39
|
+
"""
|
|
40
|
+
if not endpoint:
|
|
41
|
+
return endpoint
|
|
42
|
+
parts = urlsplit(endpoint)
|
|
43
|
+
if not parts.query:
|
|
44
|
+
return endpoint
|
|
45
|
+
filtered = [(k, v) for k, v in parse_qsl(parts.query, keep_blank_values=True) if k != "Signature"]
|
|
46
|
+
return urlunsplit((parts.scheme, parts.netloc, parts.path, urlencode(filtered), parts.fragment))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _ensure_handler() -> logging.Logger:
|
|
50
|
+
"""Configure a rotating JSON-line file handler once."""
|
|
51
|
+
global _handler_configured
|
|
52
|
+
logger = logging.getLogger(_LOGGER_NAME)
|
|
53
|
+
logger.setLevel(logging.INFO)
|
|
54
|
+
logger.propagate = False
|
|
55
|
+
if not _handler_configured:
|
|
56
|
+
directory = logs_dir()
|
|
57
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
handler = RotatingFileHandler(
|
|
59
|
+
get_log_path(),
|
|
60
|
+
maxBytes=10 * 1024 * 1024,
|
|
61
|
+
backupCount=5,
|
|
62
|
+
encoding="utf-8",
|
|
63
|
+
)
|
|
64
|
+
handler.setFormatter(logging.Formatter("%(message)s"))
|
|
65
|
+
logger.addHandler(handler)
|
|
66
|
+
_handler_configured = True
|
|
67
|
+
return logger
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def write_log_record(
|
|
71
|
+
command: str,
|
|
72
|
+
duration_ms: int,
|
|
73
|
+
endpoint: str,
|
|
74
|
+
trace_id: str,
|
|
75
|
+
http_status: Optional[int],
|
|
76
|
+
result: str,
|
|
77
|
+
error_type: Optional[str] = None,
|
|
78
|
+
error_message: Optional[str] = None,
|
|
79
|
+
dry_run: bool = False,
|
|
80
|
+
request_sent: Optional[bool] = None,
|
|
81
|
+
) -> dict[str, Any]:
|
|
82
|
+
"""Append one JSON log record to the rotating log file.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
command: CLI command path (e.g. ivr list).
|
|
86
|
+
duration_ms: Elapsed milliseconds.
|
|
87
|
+
endpoint: Request URL (Signature will be redacted).
|
|
88
|
+
trace_id: Client-generated trace id.
|
|
89
|
+
http_status: HTTP status or None for dry-run.
|
|
90
|
+
result: Result code string ("0" for success).
|
|
91
|
+
error_type: Optional error type.
|
|
92
|
+
error_message: Optional error message.
|
|
93
|
+
dry_run: Whether this invocation was --dry-run.
|
|
94
|
+
request_sent: Whether an HTTP request was sent. Only written for dry-run.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
The record that was written.
|
|
98
|
+
"""
|
|
99
|
+
now = datetime.now().astimezone()
|
|
100
|
+
record: dict[str, Any] = {
|
|
101
|
+
"timestamp": now.isoformat(timespec="milliseconds"),
|
|
102
|
+
"command": command,
|
|
103
|
+
"cli_version": __version__,
|
|
104
|
+
"os": platform.system(),
|
|
105
|
+
"arch": platform.machine(),
|
|
106
|
+
"duration_ms": duration_ms,
|
|
107
|
+
"endpoint": redact_endpoint(endpoint),
|
|
108
|
+
"trace_id": trace_id,
|
|
109
|
+
"http_status": http_status,
|
|
110
|
+
"result": result,
|
|
111
|
+
"error_type": error_type,
|
|
112
|
+
"error_message": error_message,
|
|
113
|
+
}
|
|
114
|
+
# Only emit dry-run markers for --dry-run invocations.
|
|
115
|
+
if dry_run:
|
|
116
|
+
record["dry_run"] = True
|
|
117
|
+
record["request_sent"] = False if request_sent is None else request_sent
|
|
118
|
+
logger = _ensure_handler()
|
|
119
|
+
logger.info(json.dumps(record, ensure_ascii=False))
|
|
120
|
+
return record
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def reset_logging_for_tests() -> None:
|
|
124
|
+
"""Reset logger handlers (test helper)."""
|
|
125
|
+
global _handler_configured
|
|
126
|
+
logger = logging.getLogger(_LOGGER_NAME)
|
|
127
|
+
for handler in list(logger.handlers):
|
|
128
|
+
handler.close()
|
|
129
|
+
logger.removeHandler(handler)
|
|
130
|
+
_handler_configured = False
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tinet-clink2-cli"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "Clink2 OpenAPI CLI (AK-SK, IVR queries)"
|
|
9
|
+
readme = "cli_anything/README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"click>=8.0",
|
|
13
|
+
"requests>=2.28",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
dev = [
|
|
18
|
+
"pytest>=7.0",
|
|
19
|
+
"responses>=0.25",
|
|
20
|
+
"build>=1.0",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
tinet-clink2-cli = "cli_anything.clink2.clink2_cli:cli"
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.packages.find]
|
|
27
|
+
where = ["."]
|
|
28
|
+
include = ["cli_anything*"]
|
|
29
|
+
exclude = ["cli_anything.tests", "cli_anything.tests.*"]
|
|
30
|
+
|
|
31
|
+
[tool.pytest.ini_options]
|
|
32
|
+
testpaths = ["cli_anything/tests"]
|
|
33
|
+
pythonpath = ["."]
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tinet-clink2-cli
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Clink2 OpenAPI CLI (AK-SK, IVR queries)
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: click>=8.0
|
|
8
|
+
Requires-Dist: requests>=2.28
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
11
|
+
Requires-Dist: responses>=0.25; extra == "dev"
|
|
12
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
13
|
+
|
|
14
|
+
Clink2 命令行客户端,封装 OpenAPI 接口,与 [智慧服务平台开发者中心](https://develop.clink.cn) 中参数与响应一致,适合脚本化集成与 Agent 调用。
|
|
15
|
+
|
|
16
|
+
**当前版本:** `0.0.1`
|
|
17
|
+
**PyPI:** https://pypi.org/project/tinet-clink2-cli/
|
|
18
|
+
|
|
19
|
+
## 功能概览
|
|
20
|
+
|
|
21
|
+
| 子命令 | 说明 |
|
|
22
|
+
|--------|------|
|
|
23
|
+
| **`config`** | 本地凭证管理(AccessKeyId / AccessKeySecret / 接入地址 / Expires) |
|
|
24
|
+
| **`ivr`** | 语音导航查询:列表、节点列表 |
|
|
25
|
+
|
|
26
|
+
所有出站 HTTP 请求自动附加:
|
|
27
|
+
- `X-Trace-Id: {UUID}`(每次请求新生成,不参与签名)
|
|
28
|
+
|
|
29
|
+
本地默认写入 JSON Lines 日志:`~/.clink2/logs/clink2.log`(单文件最大 10MB,滚动保留 5 个)。
|
|
30
|
+
|
|
31
|
+
## 环境要求
|
|
32
|
+
|
|
33
|
+
- Python **≥ 3.9**
|
|
34
|
+
- 运行时需访问公有云接入地址,例如 `https://api-bj.clink.cn` / `https://api-sh.clink.cn`
|
|
35
|
+
- 需在管理后台获取 AccessKeyId / AccessKeySecret(系统管理 → 安全设置 → 接口密钥)
|
|
36
|
+
|
|
37
|
+
## 安装
|
|
38
|
+
|
|
39
|
+
从 [PyPI](https://pypi.org/project/tinet-clink2-cli/) 安装(推荐):
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install tinet-clink2-cli
|
|
43
|
+
tinet-clink2-cli --version # 0.0.1
|
|
44
|
+
tinet-clink2-cli --help
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
指定版本或升级:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install 'tinet-clink2-cli==0.0.1'
|
|
51
|
+
pip install --upgrade tinet-clink2-cli
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
macOS 等受限环境可使用 [pipx](https://pipx.pypa.io/):
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pipx install tinet-clink2-cli
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## 配置凭证
|
|
61
|
+
|
|
62
|
+
**配置文件:** `~/.clink2/config.json`
|
|
63
|
+
|
|
64
|
+
入参名称与 OpenAPI 公共参数一致:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
tinet-clink2-cli config set \
|
|
68
|
+
--AccessKeyId your_access_key_id \
|
|
69
|
+
--AccessKeySecret your_access_key_secret \
|
|
70
|
+
--endpoint https://api-bj.clink.cn \
|
|
71
|
+
--Expires 60
|
|
72
|
+
|
|
73
|
+
tinet-clink2-cli config show
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
默认接入地址为北京:`https://api-bj.clink.cn`。`config show` 会对 AccessKeySecret 脱敏。
|
|
77
|
+
|
|
78
|
+
## 快速示例
|
|
79
|
+
|
|
80
|
+
顺序与上方「功能概览」一致。完整取值见各子命令 `--help` 或 [开发者中心](https://develop.clink.cn)。
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# config
|
|
84
|
+
tinet-clink2-cli config show
|
|
85
|
+
|
|
86
|
+
# ivr
|
|
87
|
+
tinet-clink2-cli ivr list
|
|
88
|
+
tinet-clink2-cli ivr nodes --ivrName your_ivr_name
|
|
89
|
+
tinet-clink2-cli --json ivr list
|
|
90
|
+
tinet-clink2-cli --json ivr nodes --ivrName your_ivr_name
|
|
91
|
+
|
|
92
|
+
# dry-run:仅校验入参与配置,不调用服务端,但仍写本地日志
|
|
93
|
+
tinet-clink2-cli --dry-run ivr list
|
|
94
|
+
tinet-clink2-cli --dry-run ivr nodes --ivrName your_ivr_name
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
全局 `--json` 输出 OpenAPI 原始 JSON。`--dry-run` 做本地校验与预览(不发请求),详见 `--help`。
|
|
98
|
+
|
|
99
|
+
## 帮助与 API 文档
|
|
100
|
+
|
|
101
|
+
安装后通过 `--help` 查看一级子命令:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
tinet-clink2-cli --help
|
|
105
|
+
tinet-clink2-cli config --help
|
|
106
|
+
tinet-clink2-cli ivr --help
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
OpenAPI 契约与字段说明见 [智慧服务平台开发者中心](https://develop.clink.cn)。
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
MANIFEST.in
|
|
2
|
+
pyproject.toml
|
|
3
|
+
cli_anything/README.md
|
|
4
|
+
cli_anything/clink2/__init__.py
|
|
5
|
+
cli_anything/clink2/clink2_cli.py
|
|
6
|
+
cli_anything/clink2/core/__init__.py
|
|
7
|
+
cli_anything/clink2/core/auth.py
|
|
8
|
+
cli_anything/clink2/core/config.py
|
|
9
|
+
cli_anything/clink2/core/http.py
|
|
10
|
+
cli_anything/clink2/core/logging.py
|
|
11
|
+
tinet_clink2_cli.egg-info/PKG-INFO
|
|
12
|
+
tinet_clink2_cli.egg-info/SOURCES.txt
|
|
13
|
+
tinet_clink2_cli.egg-info/dependency_links.txt
|
|
14
|
+
tinet_clink2_cli.egg-info/entry_points.txt
|
|
15
|
+
tinet_clink2_cli.egg-info/requires.txt
|
|
16
|
+
tinet_clink2_cli.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cli_anything
|