dsh-cloudq 0.1.0

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.
@@ -0,0 +1,404 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 腾讯云控制台免密登录链接生成脚本 (Python 版)
4
+ 通过 STS AssumeRole 获取临时凭证,生成免密登录 URL
5
+
6
+ 用法:
7
+ python3 login_url.py <target_url>
8
+
9
+ 示例:
10
+ python3 login_url.py "https://console.cloud.tencent.com/advisor?archId=arch-gvqocc25"
11
+ python3 login_url.py "https://console.cloud.tencent.com/advisor"
12
+
13
+ 环境变量(必须提前设置):
14
+ TENCENTCLOUD_SECRET_ID - 腾讯云 SecretId(必填)
15
+ TENCENTCLOUD_SECRET_KEY - 腾讯云 SecretKey(必填)
16
+ TENCENTCLOUD_ROLE_ARN - CAM 角色 ARN(可选)
17
+ TENCENTCLOUD_ROLE_NAME - CAM 角色名称(可选),系统会自动拼接完整 ARN
18
+
19
+ 配置优先级:
20
+ 1. 环境变量 TENCENTCLOUD_ROLE_ARN(完整 ARN)
21
+ 2. 配置文件 ~/.tencent-cloudq/config.json
22
+ 3. 环境变量 TENCENTCLOUD_ROLE_NAME + 自动获取账号 UIN
23
+
24
+ 可选环境变量:
25
+ TENCENTCLOUD_ROLE_SESSION - 角色会话名称(默认 advisor-session)
26
+ TENCENTCLOUD_STS_DURATION - 临时凭证有效期秒数(默认 3600,最大 43200)
27
+
28
+ 输出格式(统一 JSON):
29
+ 成功: {"success": true, "action": "GenerateLoginURL", "data": {"loginUrl": "...", "targetUrl": "...", "expireSeconds": 3600}, "requestId": "xxx"}
30
+ 失败: {"success": false, "action": "GenerateLoginURL", "error": {"code": "xxx", "message": "xxx"}, "requestId": ""}
31
+ """
32
+
33
+ import hashlib
34
+ import hmac
35
+ import base64
36
+ import json
37
+ import os
38
+ import platform
39
+ import random
40
+ import ssl
41
+ import sys
42
+ import time
43
+ from datetime import datetime, timezone
44
+ from pathlib import Path
45
+ from urllib.parse import quote
46
+ from urllib.request import Request, urlopen
47
+
48
+ # 导入 tcloud_api 模块
49
+ SCRIPT_DIR = Path(__file__).resolve().parent
50
+ sys.path.insert(0, str(SCRIPT_DIR))
51
+ from tcloud_api import call_api # noqa: E402
52
+
53
+ ACTION_NAME = "GenerateLoginURL"
54
+
55
+
56
+ def output_error(code: str, message: str, request_id: str = "") -> str:
57
+ """输出统一的错误 JSON"""
58
+ return json.dumps({
59
+ "success": False,
60
+ "action": ACTION_NAME,
61
+ "error": {"code": code, "message": message},
62
+ "requestId": request_id,
63
+ }, ensure_ascii=False)
64
+
65
+
66
+ def output_success(login_url: str, target_url: str, expire_seconds: int, request_id: str) -> str:
67
+ """输出统一的成功 JSON"""
68
+ return json.dumps({
69
+ "success": True,
70
+ "action": ACTION_NAME,
71
+ "data": {
72
+ "loginUrl": login_url,
73
+ "targetUrl": target_url,
74
+ "expireSeconds": expire_seconds,
75
+ },
76
+ "requestId": request_id,
77
+ }, ensure_ascii=False)
78
+
79
+
80
+ def _get_ssl_context():
81
+ """获取 SSL 上下文,兼容 macOS 缺少系统 CA 证书的情况"""
82
+ try:
83
+ import certifi
84
+ return ssl.create_default_context(cafile=certifi.where())
85
+ except ImportError:
86
+ # certifi 不可用时降级到系统默认 CA(Windows/Linux 系统 CA 通常可用)
87
+ return ssl.create_default_context()
88
+
89
+
90
+ def _sign_tc3(key: bytes, msg: str) -> bytes:
91
+ """TC3 HMAC-SHA256 签名辅助函数"""
92
+ return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
93
+
94
+
95
+ def _get_role_arn(secret_id: str, secret_key: str) -> str:
96
+ """
97
+ 按优先级获取 RoleArn:
98
+ 1. 环境变量 TENCENTCLOUD_ROLE_ARN
99
+ 2. 配置文件 ~/.tencent-cloudq/config.json
100
+ 3. 环境变量 TENCENTCLOUD_ROLE_NAME + 自动获取账号 UIN
101
+ """
102
+ # 优先级 1: 环境变量 ROLE_ARN
103
+ role_arn = os.environ.get("TENCENTCLOUD_ROLE_ARN", "")
104
+ if role_arn:
105
+ return role_arn
106
+
107
+ # 优先级 2: 配置文件
108
+ config_file = Path.home() / ".tencent-cloudq" / "config.json"
109
+ if config_file.exists():
110
+ try:
111
+ config = json.loads(config_file.read_text(encoding="utf-8"))
112
+ role_arn = config.get("roleArn", "")
113
+ if role_arn:
114
+ return role_arn
115
+ except (json.JSONDecodeError, IOError):
116
+ pass
117
+
118
+ # 优先级 3: ROLE_NAME + 自动获取 UIN
119
+ role_name = os.environ.get("TENCENTCLOUD_ROLE_NAME", "")
120
+ if role_name:
121
+ # 尝试 UIN 缓存(跨平台:Windows 用 TEMP,其他用 /tmp)
122
+ import tempfile
123
+ cache_dir = Path(tempfile.gettempdir())
124
+ uin_cache_file = cache_dir / ".tcloud_advisor_uin_cache"
125
+ account_uin = ""
126
+
127
+ if uin_cache_file.exists():
128
+ try:
129
+ import stat as stat_mod
130
+ stat_info = uin_cache_file.stat()
131
+ # 检查文件权限是否安全 (600)
132
+ # Windows 不支持 POSIX 权限,跳过权限检查
133
+ if platform.system() == "Windows":
134
+ cache_age = time.time() - stat_info.st_mtime
135
+ if cache_age < 3600: # 1 小时
136
+ account_uin = uin_cache_file.read_text(encoding="utf-8").strip()
137
+ elif stat_mod.S_IMODE(stat_info.st_mode) == 0o600:
138
+ cache_age = time.time() - stat_info.st_mtime
139
+ if cache_age < 3600: # 1 小时
140
+ account_uin = uin_cache_file.read_text(encoding="utf-8").strip()
141
+ else:
142
+ # 权限不安全,删除缓存
143
+ uin_cache_file.unlink()
144
+ except (OSError, IOError):
145
+ pass
146
+
147
+ if not account_uin:
148
+ # 调用 tcloud_api 模块获取 UIN
149
+ try:
150
+ result = call_api(
151
+ "sts", "sts.tencentcloudapi.com",
152
+ "GetCallerIdentity", "2018-08-13", {},
153
+ secret_id=secret_id, secret_key=secret_key,
154
+ )
155
+ account_uin = str(result.get("data", {}).get("AccountId", ""))
156
+ if account_uin and account_uin not in ("", "None", "null"):
157
+ uin_cache_file.write_text(account_uin, encoding="utf-8")
158
+ # 设置文件权限为 600(仅所有者读写,非 Windows)
159
+ if platform.system() != "Windows":
160
+ os.chmod(uin_cache_file, 0o600)
161
+ except Exception:
162
+ pass
163
+
164
+ if account_uin and account_uin not in ("", "None", "null"):
165
+ return f"qcs::cam::uin/{account_uin}:roleName/{role_name}"
166
+
167
+ return ""
168
+
169
+
170
+ def sts_assume_role(secret_id: str, secret_key: str, role_arn: str,
171
+ role_session: str, duration: int, token: str = "") -> dict:
172
+ """
173
+ 第一步:调用 STS AssumeRole 获取临时凭证
174
+ 使用 TC3-HMAC-SHA256 签名算法
175
+ """
176
+ service = "sts"
177
+ host = f"{service}.tencentcloudapi.com"
178
+ action = "AssumeRole"
179
+ version = "2018-08-13"
180
+ region = "ap-guangzhou"
181
+
182
+ payload = json.dumps({
183
+ "RoleArn": role_arn,
184
+ "RoleSessionName": role_session,
185
+ "DurationSeconds": duration,
186
+ }, separators=(",", ":"))
187
+
188
+ timestamp = int(time.time())
189
+ date = datetime.fromtimestamp(timestamp, tz=timezone.utc).strftime("%Y-%m-%d")
190
+
191
+ # 1. 拼接规范请求串
192
+ hashed_payload = hashlib.sha256(payload.encode("utf-8")).hexdigest()
193
+ canonical_request = (
194
+ f"POST\n"
195
+ f"/\n"
196
+ f"\n"
197
+ f"content-type:application/json\n"
198
+ f"host:{host}\n"
199
+ f"x-tc-action:{action.lower()}\n"
200
+ f"\n"
201
+ f"content-type;host;x-tc-action\n"
202
+ f"{hashed_payload}"
203
+ )
204
+
205
+ # 2. 拼接待签名字符串
206
+ algorithm = "TC3-HMAC-SHA256"
207
+ credential_scope = f"{date}/{service}/tc3_request"
208
+ hashed_canonical_request = hashlib.sha256(
209
+ canonical_request.encode("utf-8")
210
+ ).hexdigest()
211
+ string_to_sign = (
212
+ f"{algorithm}\n"
213
+ f"{timestamp}\n"
214
+ f"{credential_scope}\n"
215
+ f"{hashed_canonical_request}"
216
+ )
217
+
218
+ # 3. 计算签名
219
+ secret_date = _sign_tc3(f"TC3{secret_key}".encode("utf-8"), date)
220
+ secret_service = _sign_tc3(secret_date, service)
221
+ secret_signing = _sign_tc3(secret_service, "tc3_request")
222
+ signature = hmac.new(
223
+ secret_signing, string_to_sign.encode("utf-8"), hashlib.sha256
224
+ ).hexdigest()
225
+
226
+ # 4. 拼接 Authorization
227
+ authorization = (
228
+ f"{algorithm} "
229
+ f"Credential={secret_id}/{credential_scope}, "
230
+ f"SignedHeaders=content-type;host;x-tc-action, "
231
+ f"Signature={signature}"
232
+ )
233
+
234
+ # 5. 发送请求
235
+ headers = {
236
+ "Authorization": authorization,
237
+ "Content-Type": "application/json",
238
+ "Host": host,
239
+ "X-TC-Action": action,
240
+ "X-TC-Version": version,
241
+ "X-TC-Region": region,
242
+ "X-TC-Timestamp": str(timestamp),
243
+ }
244
+ if token:
245
+ headers["X-TC-Token"] = token
246
+
247
+ req = Request(
248
+ f"https://{host}",
249
+ data=payload.encode("utf-8"),
250
+ headers=headers,
251
+ method="POST",
252
+ )
253
+
254
+ ctx = _get_ssl_context()
255
+ with urlopen(req, context=ctx, timeout=30) as resp:
256
+ result = json.loads(resp.read().decode("utf-8"))
257
+
258
+ response_body = result.get("Response", {})
259
+ credentials = response_body.get("Credentials", {})
260
+
261
+ if not credentials:
262
+ error = response_body.get("Error", {})
263
+ return {
264
+ "error": True,
265
+ "code": error.get("Code", "STSError"),
266
+ "message": f"STS AssumeRole 失败: {error.get('Message', '请检查 AK/SK 和 ROLE_ARN')}",
267
+ "requestId": response_body.get("RequestId", ""),
268
+ }
269
+
270
+ return {
271
+ "error": False,
272
+ "TmpSecretId": credentials["TmpSecretId"],
273
+ "TmpSecretKey": credentials["TmpSecretKey"],
274
+ "Token": credentials["Token"],
275
+ "RequestId": response_body.get("RequestId", ""),
276
+ }
277
+
278
+
279
+ def generate_login_url(tmp_secret_id: str, tmp_secret_key: str,
280
+ token: str, target_url: str) -> str:
281
+ """
282
+ 第二步:使用临时凭证生成免密登录 URL
283
+ 签名方式:HMAC-SHA256 + Base64(与官方文档一致)
284
+ """
285
+ login_timestamp = int(time.time())
286
+ login_nonce = random.randint(10000, 100000000)
287
+
288
+ # 签名源串:参数按字母序排列 (action < nonce < secretId < timestamp)
289
+ source_string = (
290
+ f"GETcloud.tencent.com/login/roleAccessCallback?"
291
+ f"action=roleLogin"
292
+ f"&nonce={login_nonce}"
293
+ f"&secretId={tmp_secret_id}"
294
+ f"&timestamp={login_timestamp}"
295
+ )
296
+
297
+ # HMAC-SHA256 + Base64
298
+ login_signature = base64.b64encode(
299
+ hmac.new(
300
+ tmp_secret_key.encode("utf-8"),
301
+ source_string.encode("utf-8"),
302
+ hashlib.sha256,
303
+ ).digest()
304
+ ).decode("utf-8")
305
+
306
+ # 拼接最终 URL(safe='' 确保 +/= 等特殊字符都被编码)
307
+ login_url = (
308
+ f"https://cloud.tencent.com/login/roleAccessCallback?"
309
+ f"algorithm=sha256"
310
+ f"&secretId={quote(tmp_secret_id, safe='')}"
311
+ f"&token={quote(token, safe='')}"
312
+ f"&nonce={login_nonce}"
313
+ f"&timestamp={login_timestamp}"
314
+ f"&signature={quote(login_signature, safe='')}"
315
+ f"&s_url={quote(target_url, safe='')}"
316
+ )
317
+
318
+ return login_url
319
+
320
+
321
+ def main():
322
+ # ============== 参数解析 ==============
323
+ if len(sys.argv) < 2 or not sys.argv[1].strip():
324
+ print(output_error(
325
+ "MissingParameter",
326
+ "缺少目标 URL 参数。用法: python3 login_url.py <target_url>"
327
+ ))
328
+ sys.exit(1)
329
+
330
+ target_url = sys.argv[1]
331
+
332
+ # ============== 密钥检查 ==============
333
+ secret_id = os.environ.get("TENCENTCLOUD_SECRET_ID", "")
334
+ secret_key = os.environ.get("TENCENTCLOUD_SECRET_KEY", "")
335
+ token = os.environ.get("TENCENTCLOUD_TOKEN", "")
336
+
337
+ if not secret_id or not secret_key:
338
+ # 尝试通过 credential_manager 获取凭证
339
+ try:
340
+ from credential_manager import get_credential
341
+ cred = get_credential()
342
+ secret_id = cred["secretId"]
343
+ secret_key = cred["secretKey"]
344
+ token = cred.get("token", "")
345
+ except ImportError:
346
+ pass
347
+ except Exception:
348
+ pass
349
+
350
+ if not secret_id or not secret_key:
351
+ print(output_error("MissingCredentials",
352
+ "缺少凭证。请通过 OAuth 登录、配置 AK/SK 环境变量,或通过 Connector 获取临时密钥。"))
353
+ sys.exit(1)
354
+
355
+ # ============== 获取 RoleArn ==============
356
+ role_arn = _get_role_arn(secret_id, secret_key)
357
+ if not role_arn:
358
+ print(output_error(
359
+ "MissingRoleConfiguration",
360
+ "未配置角色信息。请选择以下方式之一:\\n\\n"
361
+ "方式一: 设置角色名称(推荐)\\n"
362
+ " export TENCENTCLOUD_ROLE_NAME=\\\"AdvisorRole\\\"\\n\\n"
363
+ "方式二: 设置完整 ARN\\n"
364
+ " export TENCENTCLOUD_ROLE_ARN=\\\"qcs::cam::uin/您的账号UIN:roleName/角色名\\\"\\n\\n"
365
+ "方式三: 运行配置向导(首次使用推荐)\\n"
366
+ " python3 scripts/setup_role.py\\n\\n"
367
+ "提示: 您可以在 CAM 控制台查看可用角色:\\n"
368
+ " https://console.cloud.tencent.com/cam/role"
369
+ ))
370
+ sys.exit(1)
371
+
372
+ # ============== 配置参数 ==============
373
+ role_session = os.environ.get("TENCENTCLOUD_ROLE_SESSION", "advisor-session")
374
+ duration = int(os.environ.get("TENCENTCLOUD_STS_DURATION", "3600"))
375
+
376
+ # ============== 第一步:STS AssumeRole ==============
377
+ try:
378
+ sts_result = sts_assume_role(secret_id, secret_key, role_arn, role_session, duration, token)
379
+ except Exception as e:
380
+ print(output_error("STSError", f"STS AssumeRole 请求异常: {e}"))
381
+ sys.exit(1)
382
+
383
+ if sts_result.get("error"):
384
+ print(output_error(
385
+ sts_result.get("code", "STSError"),
386
+ sts_result.get("message", "STS AssumeRole 失败"),
387
+ sts_result.get("requestId", ""),
388
+ ))
389
+ sys.exit(1)
390
+
391
+ # ============== 第二步:生成免密登录 URL ==============
392
+ login_url = generate_login_url(
393
+ sts_result["TmpSecretId"],
394
+ sts_result["TmpSecretKey"],
395
+ sts_result["Token"],
396
+ target_url,
397
+ )
398
+
399
+ # ============== 输出结果 ==============
400
+ print(output_success(login_url, target_url, duration, sts_result["RequestId"]))
401
+
402
+
403
+ if __name__ == "__main__":
404
+ main()
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ CloudQ 登出脚本
4
+
5
+ 清除本地凭证文件(OAuth / Connector)。
6
+
7
+ 用法:
8
+ python3 logout.py
9
+ """
10
+
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ SCRIPT_DIR = Path(__file__).resolve().parent
15
+ sys.path.insert(0, str(SCRIPT_DIR))
16
+
17
+ from credential_manager import CREDENTIAL_FILE, clear_credential # noqa: E402
18
+
19
+
20
+ def main():
21
+ if not CREDENTIAL_FILE.exists():
22
+ print()
23
+ print(" ℹ️ 当前没有凭证,无需登出。")
24
+ print()
25
+ return
26
+
27
+ clear_credential()
28
+ print()
29
+ print(" ✅ 已清除凭证。")
30
+ print()
31
+ print(" 如需重新登录:python3 scripts/login.py")
32
+ print()
33
+
34
+
35
+ if __name__ == "__main__":
36
+ main()
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 保存 / 校验腾讯云长期密钥(AK/SK)。
4
+
5
+ 用于 DSH 插件设置页的「手工填写密钥」入口,作为 OAuth 扫码登录之外的
6
+ 第二条凭证路径:用户直接粘贴 SecretId / SecretKey,插件把它写入
7
+ ~/.tencent-cloudq/credential.json(type="ak"),后续所有 CloudQ 请求
8
+ 复用 credential_manager.get_credential() 的统一读取逻辑。
9
+
10
+ 用法:
11
+ printf '%s' '{"secretId":"...","secretKey":"..."}' | python3 save_ak.py --test --stdin
12
+ printf '%s' '{"secretId":"...","secretKey":"..."}' | python3 save_ak.py --save --stdin
13
+
14
+ 密钥只从标准输入读取,不允许通过命令行参数传入,避免出现在进程列表中。
15
+
16
+ 两个子命令都输出 JSON 信封:
17
+ 成功 {"success": true, ...}
18
+ 失败 {"success": false, "error": {"code": "...", "message": "..."}}
19
+ """
20
+
21
+ import json
22
+ import sys
23
+ from datetime import datetime, timezone
24
+
25
+ from credential_manager import (
26
+ CREDENTIAL_FILE,
27
+ _atomic_write_json,
28
+ )
29
+ from tcloud_api import call_api
30
+
31
+
32
+ def _mask(value: str, visible: int = 4) -> str:
33
+ """与 login.py 一致的掩码规则:只保留末尾若干位。"""
34
+ if len(value) <= visible:
35
+ return "*" * len(value)
36
+ return "*" * (len(value) - visible) + value[-visible:]
37
+
38
+
39
+ def _json_ok(payload: dict) -> str:
40
+ return json.dumps({"success": True, **payload}, ensure_ascii=False)
41
+
42
+
43
+ def _json_err(code: str, message: str) -> str:
44
+ return json.dumps(
45
+ {"success": False, "error": {"code": code, "message": message}},
46
+ ensure_ascii=False,
47
+ )
48
+
49
+
50
+ def _validate(secret_id: str, secret_key: str) -> tuple:
51
+ """用一次真实的只读 API 调用验证密钥可用性。
52
+
53
+ 选 DescribeCloudQUsageOverview 是因为它无入参、无副作用,且正是
54
+ CloudQ 自己的接口——能调通即代表该密钥确实开通了 CloudQ 能力。
55
+
56
+ `call_api` 不抛异常,它把失败包在 `{"success": false, "error": {...}}`
57
+ 里返回,所以这里按信封判定而不是 try/except。
58
+
59
+ Returns:
60
+ (ok, message) —— ok 为 False 时 message 是可直接展示的失败原因。
61
+ """
62
+ result = call_api(
63
+ service="advisor",
64
+ host="advisor.tencentcloudapi.com",
65
+ action="DescribeCloudQUsageOverview",
66
+ version="2020-07-21",
67
+ payload={},
68
+ secret_id=secret_id,
69
+ secret_key=secret_key,
70
+ token="",
71
+ )
72
+ if result.get("success") is True:
73
+ return True, ""
74
+ error = result.get("error") or {}
75
+ code = error.get("code", "Unknown")
76
+ message = error.get("message", "密钥校验失败。")
77
+ return False, f"[{code}] {message}"
78
+
79
+
80
+ def cmd_test(secret_id: str, secret_key: str) -> int:
81
+ ok, message = _validate(secret_id, secret_key)
82
+ if not ok:
83
+ print(_json_err("ValidateFailed", message))
84
+ return 1
85
+ print(_json_ok({"valid": True, "secret_id_masked": _mask(secret_id)}))
86
+ return 0
87
+
88
+
89
+ def cmd_save(secret_id: str, secret_key: str) -> int:
90
+ ok, message = _validate(secret_id, secret_key)
91
+ if not ok:
92
+ print(_json_err("ValidateFailed", message))
93
+ return 1
94
+
95
+ # 长期密钥没有过期时间;沿用 credential.json 的结构,expiresAt=0 表示
96
+ # 永不过期,get_credential() 的 "ak" 分支据此跳过刷新逻辑。
97
+ data = {
98
+ "type": "ak",
99
+ "secretId": secret_id,
100
+ "secretKey": secret_key,
101
+ "token": "",
102
+ "expiresAt": 0,
103
+ "createdAt": datetime.now(timezone.utc).isoformat(),
104
+ }
105
+ try:
106
+ _atomic_write_json(CREDENTIAL_FILE, data)
107
+ except Exception as exc: # noqa: BLE001
108
+ print(_json_err("SaveFailed", f"写入凭证文件失败: {exc}"))
109
+ return 1
110
+
111
+ print(
112
+ _json_ok(
113
+ {
114
+ "logged_in": True,
115
+ "secret_id_masked": _mask(secret_id),
116
+ "auth_type": "ak",
117
+ }
118
+ )
119
+ )
120
+ return 0
121
+
122
+
123
+ def _read_credentials_from_stdin() -> tuple:
124
+ raw = sys.stdin.buffer.read(16 * 1024 + 1)
125
+ if len(raw) > 16 * 1024:
126
+ raise ValueError("凭证输入过大。")
127
+ payload = json.loads(raw.decode("utf-8"))
128
+ if not isinstance(payload, dict):
129
+ raise ValueError("凭证输入必须是 JSON 对象。")
130
+ secret_id = payload.get("secretId")
131
+ secret_key = payload.get("secretKey")
132
+ if not isinstance(secret_id, str) or not isinstance(secret_key, str):
133
+ raise ValueError("SecretId 与 SecretKey 均不能为空。")
134
+ return secret_id.strip(), secret_key.strip()
135
+
136
+
137
+ def main() -> int:
138
+ args = sys.argv[1:]
139
+ if len(args) != 2 or args[0] not in ("--test", "--save") or args[1] != "--stdin":
140
+ print(_json_err("InvalidArgs", "凭证必须通过标准输入传入。"))
141
+ return 1
142
+
143
+ try:
144
+ secret_id, secret_key = _read_credentials_from_stdin()
145
+ except (UnicodeDecodeError, json.JSONDecodeError, ValueError) as exc:
146
+ print(_json_err("InvalidInput", str(exc)))
147
+ return 1
148
+ if not secret_id or not secret_key:
149
+ print(_json_err("MissingCredential", "SecretId 与 SecretKey 均不能为空。"))
150
+ return 1
151
+
152
+ return cmd_test(secret_id, secret_key) if args[0] == "--test" else cmd_save(secret_id, secret_key)
153
+
154
+
155
+ if __name__ == "__main__":
156
+ sys.exit(main())