tinet-clink2-cli 0.0.1__tar.gz → 0.0.2__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.
Files changed (18) hide show
  1. {tinet_clink2_cli-0.0.1 → tinet_clink2_cli-0.0.2}/MANIFEST.in +1 -1
  2. {tinet_clink2_cli-0.0.1/tinet_clink2_cli.egg-info → tinet_clink2_cli-0.0.2}/PKG-INFO +31 -20
  3. {tinet_clink2_cli-0.0.1/cli_anything → tinet_clink2_cli-0.0.2}/README.md +28 -17
  4. {tinet_clink2_cli-0.0.1 → tinet_clink2_cli-0.0.2}/cli_anything/clink2/__init__.py +1 -1
  5. {tinet_clink2_cli-0.0.1 → tinet_clink2_cli-0.0.2}/cli_anything/clink2/clink2_cli.py +141 -17
  6. {tinet_clink2_cli-0.0.1 → tinet_clink2_cli-0.0.2}/cli_anything/clink2/core/config.py +9 -1
  7. {tinet_clink2_cli-0.0.1 → tinet_clink2_cli-0.0.2}/cli_anything/clink2/core/http.py +5 -1
  8. {tinet_clink2_cli-0.0.1 → tinet_clink2_cli-0.0.2}/cli_anything/clink2/core/logging.py +5 -1
  9. {tinet_clink2_cli-0.0.1 → tinet_clink2_cli-0.0.2}/pyproject.toml +4 -4
  10. {tinet_clink2_cli-0.0.1 → tinet_clink2_cli-0.0.2/tinet_clink2_cli.egg-info}/PKG-INFO +31 -20
  11. {tinet_clink2_cli-0.0.1 → tinet_clink2_cli-0.0.2}/tinet_clink2_cli.egg-info/SOURCES.txt +1 -1
  12. {tinet_clink2_cli-0.0.1 → tinet_clink2_cli-0.0.2}/cli_anything/clink2/core/__init__.py +0 -0
  13. {tinet_clink2_cli-0.0.1 → tinet_clink2_cli-0.0.2}/cli_anything/clink2/core/auth.py +0 -0
  14. {tinet_clink2_cli-0.0.1 → tinet_clink2_cli-0.0.2}/setup.cfg +0 -0
  15. {tinet_clink2_cli-0.0.1 → tinet_clink2_cli-0.0.2}/tinet_clink2_cli.egg-info/dependency_links.txt +0 -0
  16. {tinet_clink2_cli-0.0.1 → tinet_clink2_cli-0.0.2}/tinet_clink2_cli.egg-info/entry_points.txt +0 -0
  17. {tinet_clink2_cli-0.0.1 → tinet_clink2_cli-0.0.2}/tinet_clink2_cli.egg-info/requires.txt +0 -0
  18. {tinet_clink2_cli-0.0.1 → tinet_clink2_cli-0.0.2}/tinet_clink2_cli.egg-info/top_level.txt +0 -0
@@ -1,3 +1,3 @@
1
- include cli_anything/README.md
1
+ include README.md
2
2
  prune cli_anything/tests
3
3
  global-exclude *.py[cod] __pycache__ .DS_Store
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tinet-clink2-cli
3
- Version: 0.0.1
4
- Summary: Clink2 OpenAPI CLI (AK-SK, IVR queries)
5
- Requires-Python: >=3.9
3
+ Version: 0.0.2
4
+ Summary: Clink2 OpenAPI CLI
5
+ Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
7
7
  Requires-Dist: click>=8.0
8
8
  Requires-Dist: requests>=2.28
@@ -13,24 +13,26 @@ Requires-Dist: build>=1.0; extra == "dev"
13
13
 
14
14
  Clink2 命令行客户端,封装 OpenAPI 接口,与 [智慧服务平台开发者中心](https://develop.clink.cn) 中参数与响应一致,适合脚本化集成与 Agent 调用。
15
15
 
16
- **当前版本:** `0.0.1`
16
+ **当前版本:** `0.0.2`
17
17
  **PyPI:** https://pypi.org/project/tinet-clink2-cli/
18
18
 
19
19
  ## 功能概览
20
20
 
21
21
  | 子命令 | 说明 |
22
22
  |--------|------|
23
- | **`config`** | 本地凭证管理(AccessKeyId / AccessKeySecret / 接入地址 / Expires) |
23
+ | **`config`** | 本地凭证管理 |
24
24
  | **`ivr`** | 语音导航查询:列表、节点列表 |
25
+ | **`voicemail`** | 留言箱设置:查看留言箱列表 |
25
26
 
26
27
  所有出站 HTTP 请求自动附加:
27
- - `X-Trace-Id: {UUID}`(每次请求新生成,不参与签名)
28
+ - `X-Trace-Id: {UUID}`
29
+ - `X-Clink2-CLI-Version: {CLI 版本}`
28
30
 
29
31
  本地默认写入 JSON Lines 日志:`~/.clink2/logs/clink2.log`(单文件最大 10MB,滚动保留 5 个)。
30
32
 
31
33
  ## 环境要求
32
34
 
33
- - Python **≥ 3.9**
35
+ - Python **≥ 3.10**
34
36
  - 运行时需访问公有云接入地址,例如 `https://api-bj.clink.cn` / `https://api-sh.clink.cn`
35
37
  - 需在管理后台获取 AccessKeyId / AccessKeySecret(系统管理 → 安全设置 → 接口密钥)
36
38
 
@@ -40,14 +42,14 @@ Clink2 命令行客户端,封装 OpenAPI 接口,与 [智慧服务平台开
40
42
 
41
43
  ```bash
42
44
  pip install tinet-clink2-cli
43
- tinet-clink2-cli --version # 0.0.1
45
+ tinet-clink2-cli --version # 0.0.2
44
46
  tinet-clink2-cli --help
45
47
  ```
46
48
 
47
49
  指定版本或升级:
48
50
 
49
51
  ```bash
50
- pip install 'tinet-clink2-cli==0.0.1'
52
+ pip install 'tinet-clink2-cli==0.0.2'
51
53
  pip install --upgrade tinet-clink2-cli
52
54
  ```
53
55
 
@@ -61,14 +63,15 @@ pipx install tinet-clink2-cli
61
63
 
62
64
  **配置文件:** `~/.clink2/config.json`
63
65
 
64
- 入参名称与 OpenAPI 公共参数一致:
66
+ 配置示例:
65
67
 
66
68
  ```bash
67
69
  tinet-clink2-cli config set \
68
- --AccessKeyId your_access_key_id \
69
- --AccessKeySecret your_access_key_secret \
70
+ --accessKeyId your_access_key_id \
71
+ --accessKeySecret your_access_key_secret \
70
72
  --endpoint https://api-bj.clink.cn \
71
- --Expires 60
73
+ --expires 60 \
74
+ --log true
72
75
 
73
76
  tinet-clink2-cli config show
74
77
  ```
@@ -84,26 +87,34 @@ tinet-clink2-cli config show
84
87
  tinet-clink2-cli config show
85
88
 
86
89
  # ivr
87
- tinet-clink2-cli ivr list
88
90
  tinet-clink2-cli ivr nodes --ivrName your_ivr_name
89
91
  tinet-clink2-cli --json ivr list
90
- tinet-clink2-cli --json ivr nodes --ivrName your_ivr_name
91
92
 
92
- # dry-run:仅校验入参与配置,不调用服务端,但仍写本地日志
93
+ # dry-run:仅校验入参与配置,不调用服务端(是否写日志取决于 config.log)
93
94
  tinet-clink2-cli --dry-run ivr list
94
- tinet-clink2-cli --dry-run ivr nodes --ivrName your_ivr_name
95
+ tinet-clink2-cli --dry-run voicemail list --offset 0 --limit 10
96
+
97
+ # verbose:向 stderr 打印 url / method / 版本 Header / TraceId / result(不含 body)
98
+ tinet-clink2-cli --verbose --json voicemail list
95
99
  ```
96
100
 
97
- 全局 `--json` 输出 OpenAPI 原始 JSON。`--dry-run` 做本地校验与预览(不发请求),详见 `--help`。
101
+ 全局选项:
102
+ - `--json`:机器可读 stdout
103
+ - `--dry-run`:本地校验与预览,不发请求
104
+ - `--verbose`:stderr 排障
98
105
 
99
- ## 帮助与 API 文档
106
+ ## 安全提示
100
107
 
101
- 安装后通过 `--help` 查看一级子命令:
108
+ - 勿将 AccessKeySecret 写入仓库、截图或日志外发渠道。
109
+ - `config set --log false` 为用户显式关闭本地审计;关闭后排障将缺少本地 JSON 日志。
110
+
111
+ ## 帮助与 API 文档
102
112
 
103
113
  ```bash
104
114
  tinet-clink2-cli --help
105
115
  tinet-clink2-cli config --help
106
116
  tinet-clink2-cli ivr --help
117
+ tinet-clink2-cli voicemail --help
107
118
  ```
108
119
 
109
120
  OpenAPI 契约与字段说明见 [智慧服务平台开发者中心](https://develop.clink.cn)。
@@ -1,23 +1,25 @@
1
1
  Clink2 命令行客户端,封装 OpenAPI 接口,与 [智慧服务平台开发者中心](https://develop.clink.cn) 中参数与响应一致,适合脚本化集成与 Agent 调用。
2
2
 
3
- **当前版本:** `0.0.1`
3
+ **当前版本:** `0.0.2`
4
4
  **PyPI:** https://pypi.org/project/tinet-clink2-cli/
5
5
 
6
6
  ## 功能概览
7
7
 
8
8
  | 子命令 | 说明 |
9
9
  |--------|------|
10
- | **`config`** | 本地凭证管理(AccessKeyId / AccessKeySecret / 接入地址 / Expires) |
10
+ | **`config`** | 本地凭证管理 |
11
11
  | **`ivr`** | 语音导航查询:列表、节点列表 |
12
+ | **`voicemail`** | 留言箱设置:查看留言箱列表 |
12
13
 
13
14
  所有出站 HTTP 请求自动附加:
14
- - `X-Trace-Id: {UUID}`(每次请求新生成,不参与签名)
15
+ - `X-Trace-Id: {UUID}`
16
+ - `X-Clink2-CLI-Version: {CLI 版本}`
15
17
 
16
18
  本地默认写入 JSON Lines 日志:`~/.clink2/logs/clink2.log`(单文件最大 10MB,滚动保留 5 个)。
17
19
 
18
20
  ## 环境要求
19
21
 
20
- - Python **≥ 3.9**
22
+ - Python **≥ 3.10**
21
23
  - 运行时需访问公有云接入地址,例如 `https://api-bj.clink.cn` / `https://api-sh.clink.cn`
22
24
  - 需在管理后台获取 AccessKeyId / AccessKeySecret(系统管理 → 安全设置 → 接口密钥)
23
25
 
@@ -27,14 +29,14 @@ Clink2 命令行客户端,封装 OpenAPI 接口,与 [智慧服务平台开
27
29
 
28
30
  ```bash
29
31
  pip install tinet-clink2-cli
30
- tinet-clink2-cli --version # 0.0.1
32
+ tinet-clink2-cli --version # 0.0.2
31
33
  tinet-clink2-cli --help
32
34
  ```
33
35
 
34
36
  指定版本或升级:
35
37
 
36
38
  ```bash
37
- pip install 'tinet-clink2-cli==0.0.1'
39
+ pip install 'tinet-clink2-cli==0.0.2'
38
40
  pip install --upgrade tinet-clink2-cli
39
41
  ```
40
42
 
@@ -48,14 +50,15 @@ pipx install tinet-clink2-cli
48
50
 
49
51
  **配置文件:** `~/.clink2/config.json`
50
52
 
51
- 入参名称与 OpenAPI 公共参数一致:
53
+ 配置示例:
52
54
 
53
55
  ```bash
54
56
  tinet-clink2-cli config set \
55
- --AccessKeyId your_access_key_id \
56
- --AccessKeySecret your_access_key_secret \
57
+ --accessKeyId your_access_key_id \
58
+ --accessKeySecret your_access_key_secret \
57
59
  --endpoint https://api-bj.clink.cn \
58
- --Expires 60
60
+ --expires 60 \
61
+ --log true
59
62
 
60
63
  tinet-clink2-cli config show
61
64
  ```
@@ -71,26 +74,34 @@ tinet-clink2-cli config show
71
74
  tinet-clink2-cli config show
72
75
 
73
76
  # ivr
74
- tinet-clink2-cli ivr list
75
77
  tinet-clink2-cli ivr nodes --ivrName your_ivr_name
76
78
  tinet-clink2-cli --json ivr list
77
- tinet-clink2-cli --json ivr nodes --ivrName your_ivr_name
78
79
 
79
- # dry-run:仅校验入参与配置,不调用服务端,但仍写本地日志
80
+ # dry-run:仅校验入参与配置,不调用服务端(是否写日志取决于 config.log)
80
81
  tinet-clink2-cli --dry-run ivr list
81
- tinet-clink2-cli --dry-run ivr nodes --ivrName your_ivr_name
82
+ tinet-clink2-cli --dry-run voicemail list --offset 0 --limit 10
83
+
84
+ # verbose:向 stderr 打印 url / method / 版本 Header / TraceId / result(不含 body)
85
+ tinet-clink2-cli --verbose --json voicemail list
82
86
  ```
83
87
 
84
- 全局 `--json` 输出 OpenAPI 原始 JSON。`--dry-run` 做本地校验与预览(不发请求),详见 `--help`。
88
+ 全局选项:
89
+ - `--json`:机器可读 stdout
90
+ - `--dry-run`:本地校验与预览,不发请求
91
+ - `--verbose`:stderr 排障
85
92
 
86
- ## 帮助与 API 文档
93
+ ## 安全提示
87
94
 
88
- 安装后通过 `--help` 查看一级子命令:
95
+ - 勿将 AccessKeySecret 写入仓库、截图或日志外发渠道。
96
+ - `config set --log false` 为用户显式关闭本地审计;关闭后排障将缺少本地 JSON 日志。
97
+
98
+ ## 帮助与 API 文档
89
99
 
90
100
  ```bash
91
101
  tinet-clink2-cli --help
92
102
  tinet-clink2-cli config --help
93
103
  tinet-clink2-cli ivr --help
104
+ tinet-clink2-cli voicemail --help
94
105
  ```
95
106
 
96
107
  OpenAPI 契约与字段说明见 [智慧服务平台开发者中心](https://develop.clink.cn)。
@@ -1,3 +1,3 @@
1
1
  """Clink2 OpenAPI CLI package."""
2
2
 
3
- __version__ = "0.0.1"
3
+ __version__ = "0.0.2"
@@ -12,23 +12,29 @@ import click
12
12
  from cli_anything.clink2 import __version__
13
13
  from cli_anything.clink2.core import config as cfg
14
14
  from cli_anything.clink2.core.http import Clink2Client
15
- from cli_anything.clink2.core.logging import write_log_record
15
+ from cli_anything.clink2.core.logging import redact_endpoint, write_log_record
16
16
 
17
17
 
18
18
  @click.group()
19
19
  @click.version_option(
20
20
  version=__version__,
21
21
  prog_name="tinet-clink2-cli",
22
- message="%(prog)s,version %(version)s,released on 2026-07-20",
22
+ message="%(prog)s,version %(version)s,released on 2026-07-21",
23
23
  )
24
24
  @click.option("--json", "as_json", is_flag=True, help="以 JSON 格式输出结果")
25
25
  @click.option("--dry-run", is_flag=True, help="仅校验入参与配置,不调用服务端接口")
26
+ @click.option(
27
+ "--verbose",
28
+ is_flag=True,
29
+ help="向 stderr 打印排障信息(url、method、X-Clink2-CLI-Version、X-Trace-Id、result,不含 body)",
30
+ )
26
31
  @click.pass_context
27
- def cli(ctx: click.Context, as_json: bool, dry_run: bool) -> None:
32
+ def cli(ctx: click.Context, as_json: bool, dry_run: bool, verbose: bool) -> None:
28
33
  """Clink2 OpenAPI 命令行工具"""
29
34
  ctx.ensure_object(dict)
30
35
  ctx.obj["as_json"] = as_json
31
36
  ctx.obj["dry_run"] = dry_run
37
+ ctx.obj["verbose"] = verbose
32
38
 
33
39
 
34
40
  @cli.group()
@@ -38,13 +44,13 @@ def config() -> None:
38
44
 
39
45
  @config.command("set")
40
46
  @click.option(
41
- "--AccessKeyId",
47
+ "--accessKeyId",
42
48
  "access_key_id",
43
49
  required=True,
44
50
  help="访问密钥 ID",
45
51
  )
46
52
  @click.option(
47
- "--AccessKeySecret",
53
+ "--accessKeySecret",
48
54
  "access_key_secret",
49
55
  required=True,
50
56
  help="访问密钥 Secret",
@@ -56,20 +62,34 @@ def config() -> None:
56
62
  help="API 接入地址(公有云北京/上海等)",
57
63
  )
58
64
  @click.option(
59
- "--Expires",
65
+ "--expires",
60
66
  "expires",
61
67
  default=cfg.DEFAULT_EXPIRES,
62
68
  show_default=True,
63
69
  type=int,
64
70
  help="签名有效时间,单位秒(与 OpenAPI 公共参数 Expires 一致,1–86400)",
65
71
  )
66
- def config_set(access_key_id: str, access_key_secret: str, endpoint: str, expires: int) -> None:
72
+ @click.option(
73
+ "--log",
74
+ type=click.Choice(["true", "false"], case_sensitive=False),
75
+ default="true",
76
+ show_default=True,
77
+ help="是否写入本地 JSON 日志文件(true|false)",
78
+ )
79
+ def config_set(
80
+ access_key_id: str,
81
+ access_key_secret: str,
82
+ endpoint: str,
83
+ expires: int,
84
+ log: str,
85
+ ) -> None:
67
86
  """将凭据保存到 ~/.clink2/config.json"""
68
87
  path = cfg.save_config(
69
88
  access_key_id=access_key_id,
70
89
  access_key_secret=access_key_secret,
71
90
  endpoint=endpoint,
72
91
  expires=expires,
92
+ log=(log.lower() == "true"),
73
93
  )
74
94
  click.echo(f"配置已保存到 {path}")
75
95
 
@@ -86,6 +106,7 @@ def config_show() -> None:
86
106
  "AccessKeySecret": cfg.mask_secret(data["access_key_secret"]),
87
107
  "endpoint": data.get("endpoint", cfg.DEFAULT_ENDPOINT),
88
108
  "Expires": data.get("expires", cfg.DEFAULT_EXPIRES),
109
+ "log": data.get("log", cfg.DEFAULT_LOG),
89
110
  }
90
111
  click.echo(json.dumps(display, ensure_ascii=False, indent=2))
91
112
 
@@ -108,6 +129,43 @@ def _emit(ctx: click.Context, payload: Any, *, exit_code: int = 0) -> None:
108
129
  ctx.exit(exit_code)
109
130
 
110
131
 
132
+ def _openapi_business_result(body: Any, *, transport_ok: bool) -> str:
133
+ """从 OpenAPI 响应体解析业务状态码 result。
134
+
135
+ 平台约定(如座席详细信息获取 Wiki):``0`` 成功,``-1`` 失败(常伴随
136
+ ``errorCode`` / ``description``)。若响应体未带 ``result`` 字段(部分新版
137
+ CC 列表接口仅返回 ``requestId`` + 业务数据),则:传输成功记 ``0``,否则 ``-1``。
138
+
139
+ Args:
140
+ body: Parsed JSON body (dict/list/other).
141
+ transport_ok: Whether HTTP layer succeeded (2xx).
142
+
143
+ Returns:
144
+ Business result as string, e.g. ``"0"`` or ``"-1"``.
145
+ """
146
+ if isinstance(body, dict) and body.get("result") is not None:
147
+ return str(body["result"])
148
+ return "0" if transport_ok else "-1"
149
+
150
+
151
+ def _emit_verbose(url: str, version: str, trace_id: str, method: str, result: str) -> None:
152
+ """向 stderr 打印排障信息(不含 body)。
153
+
154
+ Args:
155
+ url: Request URL (Signature should already be redacted by caller or here).
156
+ version: CLI version string for X-Clink2-CLI-Version.
157
+ trace_id: Client trace id.
158
+ method: HTTP method such as GET or POST.
159
+ result: OpenAPI 业务状态码(响应体 result;0 成功,-1 失败)。
160
+ """
161
+ safe_url = redact_endpoint(url)
162
+ click.echo(f"url={safe_url}", err=True)
163
+ click.echo(f"method={method.upper()}", err=True)
164
+ click.echo(f"X-Clink2-CLI-Version={version}", err=True)
165
+ click.echo(f"X-Trace-Id={trace_id}", err=True)
166
+ click.echo(f"result={result}", err=True)
167
+
168
+
111
169
  def _run_api(
112
170
  ctx: click.Context,
113
171
  command: str,
@@ -115,15 +173,21 @@ def _run_api(
115
173
  path: str,
116
174
  params: Optional[dict[str, Any]] = None,
117
175
  ) -> None:
118
- """执行 API 调用或 dry-run,并写本地日志"""
176
+ """执行 API 调用或 dry-run,并按配置写本地日志"""
119
177
  started = time.perf_counter()
120
178
  conf = _load_or_fail()
121
179
  endpoint = conf["endpoint"].rstrip("/")
122
180
  full_url = f"{endpoint}{path}"
123
181
  trace_id = str(uuid.uuid4())
124
182
  dry_run = bool(ctx.obj.get("dry_run"))
183
+ verbose = bool(ctx.obj.get("verbose"))
184
+ log_enabled = bool(conf.get("log", True))
185
+ http_method = method.upper()
125
186
 
126
187
  if dry_run:
188
+ # 无 OpenAPI 响应体:本地校验通过按业务成功 0 记录
189
+ if verbose:
190
+ _emit_verbose(full_url, __version__, trace_id, http_method, "0")
127
191
  duration_ms = int((time.perf_counter() - started) * 1000)
128
192
  write_log_record(
129
193
  command=command,
@@ -136,10 +200,11 @@ def _run_api(
136
200
  error_message=None,
137
201
  dry_run=True,
138
202
  request_sent=False,
203
+ enabled=log_enabled,
139
204
  )
140
205
  payload = {
141
206
  "dry_run": "ok",
142
- "method": method,
207
+ "method": http_method,
143
208
  "endpoint": full_url,
144
209
  "params": params or {},
145
210
  "trace_id": trace_id,
@@ -154,38 +219,59 @@ def _run_api(
154
219
  expires=int(conf.get("expires", cfg.DEFAULT_EXPIRES)),
155
220
  )
156
221
  try:
157
- body, meta = client.request(method, path, params=params or {}, trace_id=trace_id)
222
+ body, meta = client.request(http_method, path, params=params or {}, trace_id=trace_id)
158
223
  duration_ms = meta["duration_ms"]
159
224
  status = meta["http_status"]
160
- ok = 200 <= int(status) < 300
225
+ http_ok = 200 <= int(status) < 300
226
+ result = _openapi_business_result(body, transport_ok=http_ok)
227
+ business_ok = result == "0"
228
+ if verbose:
229
+ _emit_verbose(
230
+ meta.get("url") or full_url,
231
+ __version__,
232
+ meta["trace_id"],
233
+ http_method,
234
+ result,
235
+ )
161
236
  write_log_record(
162
237
  command=command,
163
238
  duration_ms=duration_ms,
164
239
  endpoint=meta.get("url") or full_url,
165
240
  trace_id=meta["trace_id"],
166
241
  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}",
242
+ result=result,
243
+ error_type=None if (http_ok and business_ok) else ("http_error" if not http_ok else "business_error"),
244
+ error_message=(
245
+ None
246
+ if (http_ok and business_ok)
247
+ else (f"HTTP {status}" if not http_ok else f"OpenAPI result={result}")
248
+ ),
249
+ enabled=log_enabled,
170
250
  )
171
- if not ok:
251
+ if not http_ok:
172
252
  err = {"error_type": "http_error", "error_message": f"HTTP {status}", "data": body}
173
253
  _emit(ctx, err, exit_code=1)
174
254
  return
255
+ if not business_ok:
256
+ _emit(ctx, body, exit_code=1)
257
+ return
175
258
  _emit(ctx, body)
176
259
  except click.exceptions.Exit:
177
260
  raise
178
261
  except Exception as exc: # noqa: BLE001
179
262
  duration_ms = int((time.perf_counter() - started) * 1000)
263
+ if verbose:
264
+ _emit_verbose(full_url, __version__, trace_id, http_method, "-1")
180
265
  write_log_record(
181
266
  command=command,
182
267
  duration_ms=duration_ms,
183
268
  endpoint=full_url,
184
269
  trace_id=trace_id,
185
270
  http_status=None,
186
- result="1",
271
+ result="-1",
187
272
  error_type=type(exc).__name__,
188
273
  error_message=str(exc),
274
+ enabled=log_enabled,
189
275
  )
190
276
  err = {"error_type": type(exc).__name__, "error_message": str(exc)}
191
277
  if ctx.obj.get("as_json"):
@@ -212,7 +298,7 @@ def ivr_list(ctx: click.Context) -> None:
212
298
  "--ivrName",
213
299
  "ivr_name",
214
300
  required=True,
215
- help="语音导航名称(与 OpenAPI 请求参数 ivrName 一致)",
301
+ help="语音导航名称",
216
302
  )
217
303
  @click.pass_context
218
304
  def ivr_nodes(ctx: click.Context, ivr_name: str) -> None:
@@ -220,5 +306,43 @@ def ivr_nodes(ctx: click.Context, ivr_name: str) -> None:
220
306
  _run_api(ctx, "ivr nodes", "GET", "/cc/list_ivr_nodes", params={"ivrName": ivr_name})
221
307
 
222
308
 
309
+ @cli.group()
310
+ def voicemail() -> None:
311
+ """留言箱设置相关查询命令"""
312
+
313
+
314
+ @voicemail.command("list")
315
+ @click.option(
316
+ "--offset",
317
+ default=0,
318
+ show_default=True,
319
+ type=int,
320
+ help="偏移量,范围 0-99990。默认值为 0。注:limit + offset 不允许超过 100000",
321
+ )
322
+ @click.option(
323
+ "--limit",
324
+ default=10,
325
+ show_default=True,
326
+ type=int,
327
+ help="查询条数,范围 10-100。默认值为 10。注:limit + offset 不允许超过 100000",
328
+ )
329
+ @click.pass_context
330
+ def voicemail_list(ctx: click.Context, offset: int, limit: int) -> None:
331
+ """查看留言箱列表"""
332
+ if offset < 0 or offset > 99990:
333
+ raise click.ClickException("offset 超出范围 0-99990")
334
+ if limit < 10 or limit > 100:
335
+ raise click.ClickException("limit 超出范围 10-100")
336
+ if limit + offset > 100000:
337
+ raise click.ClickException("limit + offset 不允许超过 100000")
338
+ _run_api(
339
+ ctx,
340
+ "voicemail list",
341
+ "GET",
342
+ "/cc/list_voice_mails",
343
+ params={"offset": offset, "limit": limit},
344
+ )
345
+
346
+
223
347
  if __name__ == "__main__":
224
348
  cli()
@@ -9,6 +9,7 @@ from typing import Any
9
9
 
10
10
  DEFAULT_ENDPOINT = "https://api-bj.clink.cn"
11
11
  DEFAULT_EXPIRES = 60
12
+ DEFAULT_LOG = True
12
13
 
13
14
 
14
15
  def config_dir() -> Path:
@@ -42,6 +43,7 @@ def save_config(
42
43
  access_key_secret: str,
43
44
  endpoint: str = DEFAULT_ENDPOINT,
44
45
  expires: int = DEFAULT_EXPIRES,
46
+ log: bool = DEFAULT_LOG,
45
47
  ) -> Path:
46
48
  """Persist credentials and endpoint to ~/.clink2/config.json.
47
49
 
@@ -50,6 +52,7 @@ def save_config(
50
52
  access_key_secret: AccessKeySecret.
51
53
  endpoint: API base URL.
52
54
  expires: Signature expires seconds.
55
+ log: Whether to write local JSON log files.
53
56
 
54
57
  Returns:
55
58
  Path to the written config file.
@@ -62,6 +65,7 @@ def save_config(
62
65
  "access_key_secret": access_key_secret,
63
66
  "endpoint": endpoint or DEFAULT_ENDPOINT,
64
67
  "expires": int(expires) if expires is not None else DEFAULT_EXPIRES,
68
+ "log": bool(log),
65
69
  }
66
70
  path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
67
71
  os.chmod(path, 0o600)
@@ -76,7 +80,7 @@ def load_config() -> dict[str, Any]:
76
80
 
77
81
  Raises:
78
82
  FileNotFoundError: If config file is missing.
79
- ValueError: If required fields are missing.
83
+ ValueError: If required fields are missing or log is not a boolean.
80
84
  """
81
85
  path = config_path()
82
86
  if not path.exists():
@@ -87,4 +91,8 @@ def load_config() -> dict[str, Any]:
87
91
  raise ValueError(f"配置缺少必填字段: {key}")
88
92
  data.setdefault("endpoint", DEFAULT_ENDPOINT)
89
93
  data.setdefault("expires", DEFAULT_EXPIRES)
94
+ if "log" not in data:
95
+ data["log"] = DEFAULT_LOG
96
+ elif not isinstance(data["log"], bool):
97
+ raise ValueError("配置字段 log 必须为 JSON boolean(true/false)")
90
98
  return data
@@ -9,6 +9,7 @@ from urllib.parse import urlparse
9
9
 
10
10
  import requests
11
11
 
12
+ from cli_anything.clink2 import __version__
12
13
  from cli_anything.clink2.core.auth import sign_request
13
14
 
14
15
 
@@ -91,7 +92,10 @@ class Clink2Client:
91
92
  method=method.upper(),
92
93
  url=url,
93
94
  params=query,
94
- headers={"X-Trace-Id": tid},
95
+ headers={
96
+ "X-Trace-Id": tid,
97
+ "X-Clink2-CLI-Version": __version__,
98
+ },
95
99
  timeout=30,
96
100
  verify=True,
97
101
  )
@@ -78,6 +78,7 @@ def write_log_record(
78
78
  error_message: Optional[str] = None,
79
79
  dry_run: bool = False,
80
80
  request_sent: Optional[bool] = None,
81
+ enabled: bool = True,
81
82
  ) -> dict[str, Any]:
82
83
  """Append one JSON log record to the rotating log file.
83
84
 
@@ -92,9 +93,10 @@ def write_log_record(
92
93
  error_message: Optional error message.
93
94
  dry_run: Whether this invocation was --dry-run.
94
95
  request_sent: Whether an HTTP request was sent. Only written for dry-run.
96
+ enabled: When False, skip writing to the log file (still return the record).
95
97
 
96
98
  Returns:
97
- The record that was written.
99
+ The record that was written (or would have been written).
98
100
  """
99
101
  now = datetime.now().astimezone()
100
102
  record: dict[str, Any] = {
@@ -115,6 +117,8 @@ def write_log_record(
115
117
  if dry_run:
116
118
  record["dry_run"] = True
117
119
  record["request_sent"] = False if request_sent is None else request_sent
120
+ if not enabled:
121
+ return record
118
122
  logger = _ensure_handler()
119
123
  logger.info(json.dumps(record, ensure_ascii=False))
120
124
  return record
@@ -4,10 +4,10 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
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"
7
+ version = "0.0.2"
8
+ description = "Clink2 OpenAPI CLI"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
11
  dependencies = [
12
12
  "click>=8.0",
13
13
  "requests>=2.28",
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tinet-clink2-cli
3
- Version: 0.0.1
4
- Summary: Clink2 OpenAPI CLI (AK-SK, IVR queries)
5
- Requires-Python: >=3.9
3
+ Version: 0.0.2
4
+ Summary: Clink2 OpenAPI CLI
5
+ Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
7
7
  Requires-Dist: click>=8.0
8
8
  Requires-Dist: requests>=2.28
@@ -13,24 +13,26 @@ Requires-Dist: build>=1.0; extra == "dev"
13
13
 
14
14
  Clink2 命令行客户端,封装 OpenAPI 接口,与 [智慧服务平台开发者中心](https://develop.clink.cn) 中参数与响应一致,适合脚本化集成与 Agent 调用。
15
15
 
16
- **当前版本:** `0.0.1`
16
+ **当前版本:** `0.0.2`
17
17
  **PyPI:** https://pypi.org/project/tinet-clink2-cli/
18
18
 
19
19
  ## 功能概览
20
20
 
21
21
  | 子命令 | 说明 |
22
22
  |--------|------|
23
- | **`config`** | 本地凭证管理(AccessKeyId / AccessKeySecret / 接入地址 / Expires) |
23
+ | **`config`** | 本地凭证管理 |
24
24
  | **`ivr`** | 语音导航查询:列表、节点列表 |
25
+ | **`voicemail`** | 留言箱设置:查看留言箱列表 |
25
26
 
26
27
  所有出站 HTTP 请求自动附加:
27
- - `X-Trace-Id: {UUID}`(每次请求新生成,不参与签名)
28
+ - `X-Trace-Id: {UUID}`
29
+ - `X-Clink2-CLI-Version: {CLI 版本}`
28
30
 
29
31
  本地默认写入 JSON Lines 日志:`~/.clink2/logs/clink2.log`(单文件最大 10MB,滚动保留 5 个)。
30
32
 
31
33
  ## 环境要求
32
34
 
33
- - Python **≥ 3.9**
35
+ - Python **≥ 3.10**
34
36
  - 运行时需访问公有云接入地址,例如 `https://api-bj.clink.cn` / `https://api-sh.clink.cn`
35
37
  - 需在管理后台获取 AccessKeyId / AccessKeySecret(系统管理 → 安全设置 → 接口密钥)
36
38
 
@@ -40,14 +42,14 @@ Clink2 命令行客户端,封装 OpenAPI 接口,与 [智慧服务平台开
40
42
 
41
43
  ```bash
42
44
  pip install tinet-clink2-cli
43
- tinet-clink2-cli --version # 0.0.1
45
+ tinet-clink2-cli --version # 0.0.2
44
46
  tinet-clink2-cli --help
45
47
  ```
46
48
 
47
49
  指定版本或升级:
48
50
 
49
51
  ```bash
50
- pip install 'tinet-clink2-cli==0.0.1'
52
+ pip install 'tinet-clink2-cli==0.0.2'
51
53
  pip install --upgrade tinet-clink2-cli
52
54
  ```
53
55
 
@@ -61,14 +63,15 @@ pipx install tinet-clink2-cli
61
63
 
62
64
  **配置文件:** `~/.clink2/config.json`
63
65
 
64
- 入参名称与 OpenAPI 公共参数一致:
66
+ 配置示例:
65
67
 
66
68
  ```bash
67
69
  tinet-clink2-cli config set \
68
- --AccessKeyId your_access_key_id \
69
- --AccessKeySecret your_access_key_secret \
70
+ --accessKeyId your_access_key_id \
71
+ --accessKeySecret your_access_key_secret \
70
72
  --endpoint https://api-bj.clink.cn \
71
- --Expires 60
73
+ --expires 60 \
74
+ --log true
72
75
 
73
76
  tinet-clink2-cli config show
74
77
  ```
@@ -84,26 +87,34 @@ tinet-clink2-cli config show
84
87
  tinet-clink2-cli config show
85
88
 
86
89
  # ivr
87
- tinet-clink2-cli ivr list
88
90
  tinet-clink2-cli ivr nodes --ivrName your_ivr_name
89
91
  tinet-clink2-cli --json ivr list
90
- tinet-clink2-cli --json ivr nodes --ivrName your_ivr_name
91
92
 
92
- # dry-run:仅校验入参与配置,不调用服务端,但仍写本地日志
93
+ # dry-run:仅校验入参与配置,不调用服务端(是否写日志取决于 config.log)
93
94
  tinet-clink2-cli --dry-run ivr list
94
- tinet-clink2-cli --dry-run ivr nodes --ivrName your_ivr_name
95
+ tinet-clink2-cli --dry-run voicemail list --offset 0 --limit 10
96
+
97
+ # verbose:向 stderr 打印 url / method / 版本 Header / TraceId / result(不含 body)
98
+ tinet-clink2-cli --verbose --json voicemail list
95
99
  ```
96
100
 
97
- 全局 `--json` 输出 OpenAPI 原始 JSON。`--dry-run` 做本地校验与预览(不发请求),详见 `--help`。
101
+ 全局选项:
102
+ - `--json`:机器可读 stdout
103
+ - `--dry-run`:本地校验与预览,不发请求
104
+ - `--verbose`:stderr 排障
98
105
 
99
- ## 帮助与 API 文档
106
+ ## 安全提示
100
107
 
101
- 安装后通过 `--help` 查看一级子命令:
108
+ - 勿将 AccessKeySecret 写入仓库、截图或日志外发渠道。
109
+ - `config set --log false` 为用户显式关闭本地审计;关闭后排障将缺少本地 JSON 日志。
110
+
111
+ ## 帮助与 API 文档
102
112
 
103
113
  ```bash
104
114
  tinet-clink2-cli --help
105
115
  tinet-clink2-cli config --help
106
116
  tinet-clink2-cli ivr --help
117
+ tinet-clink2-cli voicemail --help
107
118
  ```
108
119
 
109
120
  OpenAPI 契约与字段说明见 [智慧服务平台开发者中心](https://develop.clink.cn)。
@@ -1,6 +1,6 @@
1
1
  MANIFEST.in
2
+ README.md
2
3
  pyproject.toml
3
- cli_anything/README.md
4
4
  cli_anything/clink2/__init__.py
5
5
  cli_anything/clink2/clink2_cli.py
6
6
  cli_anything/clink2/core/__init__.py