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.
- package/LICENSE +21 -0
- package/README.md +89 -0
- package/assets/cloudq.png +0 -0
- package/cordis.patch.yml +5 -0
- package/lib/client.js +3405 -0
- package/lib/index.js +910 -0
- package/lib/types/client/index.d.ts +4 -0
- package/lib/types/index.d.ts +13 -0
- package/package.json +119 -0
- package/skills/cloudq/SKILL.md +359 -0
- package/skills/cloudq/references/api/CloudQChatCompletions.md +178 -0
- package/skills/cloudq/scripts/check_env.py +811 -0
- package/skills/cloudq/scripts/cleanup.py +419 -0
- package/skills/cloudq/scripts/create_role.py +281 -0
- package/skills/cloudq/scripts/credential_manager.py +554 -0
- package/skills/cloudq/scripts/login.py +410 -0
- package/skills/cloudq/scripts/login_url.py +404 -0
- package/skills/cloudq/scripts/logout.py +36 -0
- package/skills/cloudq/scripts/save_ak.py +156 -0
- package/skills/cloudq/scripts/tcloud_api.py +329 -0
- package/skills/cloudq/scripts/tcloud_async_task.py +345 -0
- package/skills/cloudq/scripts/tcloud_sse_api.py +765 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
腾讯云智能顾问 - 配置清理脚本
|
|
4
|
+
|
|
5
|
+
清理本机上 tencent-cloudq 产生的所有配置和缓存文件,
|
|
6
|
+
可选删除云端通过本工具创建的 CAM 角色。
|
|
7
|
+
|
|
8
|
+
用法:
|
|
9
|
+
python3 cleanup.py # 交互式清理(逐项确认)
|
|
10
|
+
python3 cleanup.py --all # 一键清理所有本地配置(不含云端角色)
|
|
11
|
+
python3 cleanup.py --all --cloud # 一键清理所有本地配置 + 云端角色
|
|
12
|
+
|
|
13
|
+
清理范围:
|
|
14
|
+
1. 配置目录 ~/.tencent-cloudq/(含 config.json)
|
|
15
|
+
2. 凭证(OAuth / Connector) ~/.tencent-cloudq/credential.json
|
|
16
|
+
3. 临时缓存 {系统临时目录}/.tcloud_advisor_uin_cache
|
|
17
|
+
4. 环境变量 TENCENTCLOUD_* 系列环境变量
|
|
18
|
+
5. 云端角色 CAM 角色 advisor(可选,需 AK/SK)
|
|
19
|
+
|
|
20
|
+
返回码:
|
|
21
|
+
0 - 清理完成
|
|
22
|
+
1 - 用户取消
|
|
23
|
+
|
|
24
|
+
跨平台支持: Windows / Linux / macOS
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
import os
|
|
29
|
+
import platform
|
|
30
|
+
import shutil
|
|
31
|
+
import sys
|
|
32
|
+
import tempfile
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
# 导入 tcloud_api 模块(用于可选的云端角色删除)
|
|
36
|
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
37
|
+
sys.path.insert(0, str(SCRIPT_DIR))
|
|
38
|
+
|
|
39
|
+
# ============== 配置 ==============
|
|
40
|
+
CONFIG_DIR = Path.home() / ".tencent-cloudq"
|
|
41
|
+
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
42
|
+
CREDENTIAL_FILE = CONFIG_DIR / "credential.json"
|
|
43
|
+
CACHE_FILE = Path(tempfile.gettempdir()) / ".tcloud_advisor_uin_cache"
|
|
44
|
+
ROLE_NAME = "advisor"
|
|
45
|
+
|
|
46
|
+
# 智能顾问使用的所有环境变量
|
|
47
|
+
ENV_VARS = [
|
|
48
|
+
"TENCENTCLOUD_SECRET_ID",
|
|
49
|
+
"TENCENTCLOUD_SECRET_KEY",
|
|
50
|
+
"TENCENTCLOUD_TOKEN",
|
|
51
|
+
"TENCENTCLOUD_ROLE_ARN",
|
|
52
|
+
"TENCENTCLOUD_ROLE_NAME",
|
|
53
|
+
"TENCENTCLOUD_ROLE_SESSION",
|
|
54
|
+
"TENCENTCLOUD_STS_DURATION",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ============== 终端颜色 ==============
|
|
59
|
+
def _supports_color() -> bool:
|
|
60
|
+
if os.environ.get("NO_COLOR"):
|
|
61
|
+
return False
|
|
62
|
+
if platform.system() == "Windows":
|
|
63
|
+
return os.environ.get("TERM") == "xterm" or hasattr(sys.stderr, "reconfigure")
|
|
64
|
+
return hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
_COLOR = _supports_color()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _c(code: str, text: str) -> str:
|
|
71
|
+
return f"\033[{code}m{text}\033[0m" if _COLOR else text
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def green(t: str) -> str:
|
|
75
|
+
return _c("32", t)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def red(t: str) -> str:
|
|
79
|
+
return _c("31", t)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def yellow(t: str) -> str:
|
|
83
|
+
return _c("33", t)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def bold(t: str) -> str:
|
|
87
|
+
return _c("1", t)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def dim(t: str) -> str:
|
|
91
|
+
return _c("2", t)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ============== 工具函数 ==============
|
|
95
|
+
def confirm(prompt: str) -> bool:
|
|
96
|
+
"""交互式确认,默认为 No"""
|
|
97
|
+
try:
|
|
98
|
+
answer = input(f"{prompt} [y/N]: ").strip().lower()
|
|
99
|
+
return answer in ("y", "yes")
|
|
100
|
+
except (EOFError, KeyboardInterrupt):
|
|
101
|
+
print()
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def read_config() -> dict:
|
|
106
|
+
"""读取现有配置文件"""
|
|
107
|
+
if CONFIG_FILE.exists():
|
|
108
|
+
try:
|
|
109
|
+
return json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
|
|
110
|
+
except (json.JSONDecodeError, OSError):
|
|
111
|
+
return {}
|
|
112
|
+
return {}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def remove_file(path: Path, label: str) -> bool:
|
|
116
|
+
"""删除单个文件"""
|
|
117
|
+
if not path.exists():
|
|
118
|
+
print(f" {dim('[-]')} {label}: {dim('不存在,跳过')}")
|
|
119
|
+
return False
|
|
120
|
+
try:
|
|
121
|
+
path.unlink()
|
|
122
|
+
print(f" {green('[OK]')} {label}: 已删除")
|
|
123
|
+
return True
|
|
124
|
+
except OSError as e:
|
|
125
|
+
print(f" {red('[FAIL]')} {label}: 删除失败 - {e}")
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def remove_dir(path: Path, label: str) -> bool:
|
|
130
|
+
"""删除目录及其所有内容"""
|
|
131
|
+
if not path.exists():
|
|
132
|
+
print(f" {dim('[-]')} {label}: {dim('不存在,跳过')}")
|
|
133
|
+
return False
|
|
134
|
+
try:
|
|
135
|
+
shutil.rmtree(str(path))
|
|
136
|
+
print(f" {green('[OK]')} {label}: 已删除")
|
|
137
|
+
return True
|
|
138
|
+
except OSError as e:
|
|
139
|
+
print(f" {red('[FAIL]')} {label}: 删除失败 - {e}")
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# ============== 清理动作 ==============
|
|
144
|
+
def clean_config_dir(interactive: bool) -> bool:
|
|
145
|
+
"""清理配置目录 ~/.tencent-cloudq/"""
|
|
146
|
+
print(f"\n{bold('1. 配置目录')}")
|
|
147
|
+
|
|
148
|
+
if not CONFIG_DIR.exists():
|
|
149
|
+
print(f" {dim('[-]')} {CONFIG_DIR}: {dim('不存在,跳过')}")
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
# 列出目录内容
|
|
153
|
+
files = list(CONFIG_DIR.iterdir())
|
|
154
|
+
print(f" 路径: {CONFIG_DIR}")
|
|
155
|
+
if files:
|
|
156
|
+
for f in files:
|
|
157
|
+
size = f.stat().st_size if f.is_file() else 0
|
|
158
|
+
print(f" - {f.name} ({size} bytes)")
|
|
159
|
+
else:
|
|
160
|
+
print(f" {dim('(空目录)')}")
|
|
161
|
+
|
|
162
|
+
if interactive and not confirm(f"\n 确认删除 {CONFIG_DIR} ?"):
|
|
163
|
+
print(f" {yellow('[SKIP]')} 用户跳过")
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
return remove_dir(CONFIG_DIR, str(CONFIG_DIR))
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def clean_oauth_credential(interactive: bool) -> bool:
|
|
170
|
+
"""清理凭证文件(OAuth / Connector)"""
|
|
171
|
+
print(f"\n{bold('2. 凭证文件')}")
|
|
172
|
+
|
|
173
|
+
if not CREDENTIAL_FILE.exists():
|
|
174
|
+
print(f" {dim('[-]')} {CREDENTIAL_FILE}: {dim('不存在,跳过')}")
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
print(f" 路径: {CREDENTIAL_FILE}")
|
|
178
|
+
try:
|
|
179
|
+
cred_data = json.loads(CREDENTIAL_FILE.read_text(encoding="utf-8"))
|
|
180
|
+
secret_id = cred_data.get("secretId", "")
|
|
181
|
+
masked = secret_id[:4] + "****" + secret_id[-4:] if len(secret_id) > 8 else "****"
|
|
182
|
+
print(f" - 凭证类型: {cred_data.get('type', '未知')}")
|
|
183
|
+
print(f" - SecretId: {masked}")
|
|
184
|
+
except (json.JSONDecodeError, OSError):
|
|
185
|
+
pass
|
|
186
|
+
|
|
187
|
+
if interactive and not confirm(f"\n 确认删除凭证文件?"):
|
|
188
|
+
print(f" {yellow('[SKIP]')} 用户跳过")
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
return remove_file(CREDENTIAL_FILE, "凭证文件")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def clean_cache_file(interactive: bool) -> bool:
|
|
195
|
+
"""清理临时缓存文件"""
|
|
196
|
+
print(f"\n{bold('3. 临时缓存')}")
|
|
197
|
+
|
|
198
|
+
if not CACHE_FILE.exists():
|
|
199
|
+
print(f" {dim('[-]')} {CACHE_FILE}: {dim('不存在,跳过')}")
|
|
200
|
+
return False
|
|
201
|
+
|
|
202
|
+
print(f" 路径: {CACHE_FILE}")
|
|
203
|
+
try:
|
|
204
|
+
size = CACHE_FILE.stat().st_size
|
|
205
|
+
print(f" - .tcloud_advisor_uin_cache ({size} bytes)")
|
|
206
|
+
except OSError:
|
|
207
|
+
pass
|
|
208
|
+
|
|
209
|
+
if interactive and not confirm(f"\n 确认删除 {CACHE_FILE} ?"):
|
|
210
|
+
print(f" {yellow('[SKIP]')} 用户跳过")
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
return remove_file(CACHE_FILE, str(CACHE_FILE))
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def clean_env_vars(interactive: bool) -> bool:
|
|
217
|
+
"""清理智能顾问相关环境变量"""
|
|
218
|
+
print(f"\n{bold('5. 环境变量')}")
|
|
219
|
+
|
|
220
|
+
# 检测哪些环境变量当前已设置
|
|
221
|
+
found = {}
|
|
222
|
+
for var in ENV_VARS:
|
|
223
|
+
val = os.environ.get(var, "")
|
|
224
|
+
if val:
|
|
225
|
+
found[var] = val
|
|
226
|
+
|
|
227
|
+
if not found:
|
|
228
|
+
print(f" {dim('[-]')} 未检测到已设置的 TENCENTCLOUD_* 环境变量,跳过")
|
|
229
|
+
return False
|
|
230
|
+
|
|
231
|
+
# 显示已设置的变量(敏感值掩码)
|
|
232
|
+
print(f" 检测到 {len(found)} 个已设置的环境变量:")
|
|
233
|
+
for var, val in found.items():
|
|
234
|
+
if "SECRET" in var or "TOKEN" in var or "KEY" in var:
|
|
235
|
+
display = val[:4] + "****" + val[-4:] if len(val) > 8 else "****"
|
|
236
|
+
else:
|
|
237
|
+
display = val
|
|
238
|
+
print(f" - {var} = {display}")
|
|
239
|
+
|
|
240
|
+
if interactive and not confirm(f"\n 确认清理以上 {len(found)} 个环境变量?"):
|
|
241
|
+
print(f" {yellow('[SKIP]')} 用户跳过")
|
|
242
|
+
return False
|
|
243
|
+
|
|
244
|
+
# 生成清理命令
|
|
245
|
+
is_windows = platform.system() == "Windows"
|
|
246
|
+
print()
|
|
247
|
+
|
|
248
|
+
if is_windows:
|
|
249
|
+
# Windows: 生成 PowerShell 脚本
|
|
250
|
+
script_file = Path(tempfile.gettempdir()) / "cleanup_tencent_env.ps1"
|
|
251
|
+
lines = ["# 腾讯云智能顾问 - 环境变量清理脚本 (PowerShell)",
|
|
252
|
+
"# 自动生成,执行后可删除此文件", ""]
|
|
253
|
+
for var in found:
|
|
254
|
+
lines.append(f'Remove-Item Env:\\{var} -ErrorAction SilentlyContinue')
|
|
255
|
+
lines.append(f'Write-Host " [OK] 已清除 {var}"')
|
|
256
|
+
lines.append("")
|
|
257
|
+
lines.append('Write-Host ""')
|
|
258
|
+
lines.append(f'Write-Host " 已清理 {len(found)} 个环境变量(仅影响当前会话)"')
|
|
259
|
+
|
|
260
|
+
script_file.write_text("\n".join(lines), encoding="utf-8")
|
|
261
|
+
print(f" 已生成 PowerShell 清理脚本: {script_file}")
|
|
262
|
+
print(f" 请在 PowerShell 中执行:")
|
|
263
|
+
print(f" . {script_file}")
|
|
264
|
+
|
|
265
|
+
else:
|
|
266
|
+
# Linux / macOS: 生成 source 脚本
|
|
267
|
+
script_file = Path(tempfile.gettempdir()) / "cleanup_tencent_env.sh"
|
|
268
|
+
lines = ["#!/bin/sh",
|
|
269
|
+
"# 腾讯云智能顾问 - 环境变量清理脚本",
|
|
270
|
+
"# 自动生成,执行后可删除此文件", ""]
|
|
271
|
+
for var in found:
|
|
272
|
+
lines.append(f'unset {var}')
|
|
273
|
+
lines.append(f'echo " [OK] 已清除 {var}"')
|
|
274
|
+
lines.append("")
|
|
275
|
+
lines.append('echo ""')
|
|
276
|
+
lines.append(f'echo " 已清理 {len(found)} 个环境变量(仅影响当前会话)"')
|
|
277
|
+
|
|
278
|
+
script_file.write_text("\n".join(lines), encoding="utf-8")
|
|
279
|
+
try:
|
|
280
|
+
os.chmod(str(script_file), 0o755)
|
|
281
|
+
except OSError:
|
|
282
|
+
pass
|
|
283
|
+
print(f" 已生成清理脚本: {script_file}")
|
|
284
|
+
print(f" 请在当前终端执行:")
|
|
285
|
+
print(f" source {script_file}")
|
|
286
|
+
|
|
287
|
+
print()
|
|
288
|
+
print(f" {yellow('注意')}: 环境变量存在于 shell 进程中,Python 脚本无法直接修改父 shell。")
|
|
289
|
+
print(f" 请执行上方命令完成清理(仅影响当前终端会话)。")
|
|
290
|
+
print(f" 如果变量写在 ~/.bashrc、~/.zshrc 或系统环境中,请手动移除对应行。")
|
|
291
|
+
|
|
292
|
+
return True
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def clean_cloud_role(interactive: bool) -> bool:
|
|
296
|
+
"""删除云端 CAM 角色(可选)"""
|
|
297
|
+
print(f"\n{bold('4. 云端 CAM 角色')}")
|
|
298
|
+
|
|
299
|
+
secret_id = os.environ.get("TENCENTCLOUD_SECRET_ID", "")
|
|
300
|
+
secret_key = os.environ.get("TENCENTCLOUD_SECRET_KEY", "")
|
|
301
|
+
|
|
302
|
+
if not secret_id or not secret_key:
|
|
303
|
+
print(f" {dim('[-]')} 未配置 AK/SK 环境变量,跳过云端角色清理")
|
|
304
|
+
print(f" 如需删除云端角色,请设置 TENCENTCLOUD_SECRET_ID 和 TENCENTCLOUD_SECRET_KEY 后重试")
|
|
305
|
+
return False
|
|
306
|
+
|
|
307
|
+
# 加载 tcloud_api
|
|
308
|
+
try:
|
|
309
|
+
from tcloud_api import call_api
|
|
310
|
+
except ImportError:
|
|
311
|
+
print(f" {red('[FAIL]')} 无法加载 tcloud_api 模块")
|
|
312
|
+
return False
|
|
313
|
+
|
|
314
|
+
# 检查角色是否存在
|
|
315
|
+
print(f" 正在检查角色 {ROLE_NAME} ...")
|
|
316
|
+
check_result = call_api(
|
|
317
|
+
"cam", "cam.tencentcloudapi.com",
|
|
318
|
+
"GetRole", "2019-01-16",
|
|
319
|
+
{"RoleName": ROLE_NAME},
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
if not check_result.get("success"):
|
|
323
|
+
err_code = check_result.get("error", {}).get("code", "")
|
|
324
|
+
if "NotFound" in err_code or "not exist" in check_result.get("error", {}).get("message", "").lower():
|
|
325
|
+
print(f" {dim('[-]')} 角色 {ROLE_NAME} 不存在,无需删除")
|
|
326
|
+
return False
|
|
327
|
+
else:
|
|
328
|
+
print(f" {red('[FAIL]')} 查询角色失败: {check_result.get('error', {}).get('message', '未知错误')}")
|
|
329
|
+
return False
|
|
330
|
+
|
|
331
|
+
role_data = check_result.get("data", {})
|
|
332
|
+
role_id = role_data.get("RoleId", "unknown")
|
|
333
|
+
description = role_data.get("Description", "无描述")
|
|
334
|
+
print(f" 检测到角色:")
|
|
335
|
+
print(f" 名称: {ROLE_NAME}")
|
|
336
|
+
print(f" ID: {role_id}")
|
|
337
|
+
print(f" 描述: {description}")
|
|
338
|
+
|
|
339
|
+
if interactive and not confirm(f"\n 确认删除云端角色 {ROLE_NAME} ? (此操作不可恢复)"):
|
|
340
|
+
print(f" {yellow('[SKIP]')} 用户跳过")
|
|
341
|
+
return False
|
|
342
|
+
|
|
343
|
+
# 执行删除
|
|
344
|
+
print(f" 正在删除角色 {ROLE_NAME} ...")
|
|
345
|
+
delete_result = call_api(
|
|
346
|
+
"cam", "cam.tencentcloudapi.com",
|
|
347
|
+
"DeleteRole", "2019-01-16",
|
|
348
|
+
{"RoleName": ROLE_NAME},
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
if delete_result.get("success"):
|
|
352
|
+
print(f" {green('[OK]')} 云端角色 {ROLE_NAME} 已删除")
|
|
353
|
+
return True
|
|
354
|
+
else:
|
|
355
|
+
err_msg = delete_result.get("error", {}).get("message", "未知错误")
|
|
356
|
+
print(f" {red('[FAIL]')} 删除角色失败: {err_msg}")
|
|
357
|
+
return False
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
# ============== 主流程 ==============
|
|
361
|
+
def main():
|
|
362
|
+
args = set(sys.argv[1:])
|
|
363
|
+
|
|
364
|
+
# 帮助信息
|
|
365
|
+
if args & {"-h", "--help"}:
|
|
366
|
+
print(__doc__)
|
|
367
|
+
sys.exit(0)
|
|
368
|
+
|
|
369
|
+
auto_mode = "--all" in args
|
|
370
|
+
include_cloud = "--cloud" in args
|
|
371
|
+
|
|
372
|
+
# 标题
|
|
373
|
+
print(f"\n{'=' * 58}")
|
|
374
|
+
print(f" 腾讯云智能顾问 - 配置清理")
|
|
375
|
+
print(f"{'=' * 58}")
|
|
376
|
+
|
|
377
|
+
# 显示当前配置摘要
|
|
378
|
+
config = read_config()
|
|
379
|
+
if config:
|
|
380
|
+
print(f"\n当前配置:")
|
|
381
|
+
print(f" 账号 UIN: {config.get('accountUin', '未知')}")
|
|
382
|
+
print(f" 角色名称: {config.get('roleName', '未知')}")
|
|
383
|
+
print(f" 角色 ARN: {config.get('roleArn', '未知')}")
|
|
384
|
+
print(f" 配置时间: {config.get('configuredAt', '未知')}")
|
|
385
|
+
|
|
386
|
+
interactive = not auto_mode
|
|
387
|
+
|
|
388
|
+
if interactive:
|
|
389
|
+
print(f"\n{dim('提示: 使用 --all 跳过确认,--cloud 同时删除云端角色')}")
|
|
390
|
+
|
|
391
|
+
# 执行清理(注意:云端角色删除必须在环境变量清理之前,因为需要 AK/SK)
|
|
392
|
+
results = []
|
|
393
|
+
results.append(("配置目录", clean_config_dir(interactive)))
|
|
394
|
+
results.append(("凭证文件", clean_oauth_credential(interactive)))
|
|
395
|
+
results.append(("临时缓存", clean_cache_file(interactive)))
|
|
396
|
+
|
|
397
|
+
if include_cloud:
|
|
398
|
+
results.append(("云端角色", clean_cloud_role(interactive)))
|
|
399
|
+
else:
|
|
400
|
+
print(f"\n{bold('4. 云端 CAM 角色')}")
|
|
401
|
+
print(f" {dim('[-]')} 未指定 --cloud 参数,跳过云端角色清理")
|
|
402
|
+
print(f" 如需同时删除云端角色,请添加 --cloud 参数")
|
|
403
|
+
|
|
404
|
+
# 环境变量清理放在最后(云端角色删除可能需要用到 AK/SK)
|
|
405
|
+
results.append(("环境变量", clean_env_vars(interactive)))
|
|
406
|
+
|
|
407
|
+
# 汇总
|
|
408
|
+
cleaned = sum(1 for _, ok in results if ok)
|
|
409
|
+
total = len(results)
|
|
410
|
+
|
|
411
|
+
print(f"\n{'=' * 58}")
|
|
412
|
+
print(f" 清理完成: {cleaned}/{total} 项已清理")
|
|
413
|
+
print(f"{'=' * 58}\n")
|
|
414
|
+
|
|
415
|
+
sys.exit(0)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
if __name__ == "__main__":
|
|
419
|
+
main()
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
腾讯云智能顾问 — CAM 角色创建脚本 (Python 版)
|
|
4
|
+
|
|
5
|
+
功能:创建 advisor 角色并关联 QcloudTAGFullAccess、QcloudAdvisorFullAccess 策略
|
|
6
|
+
注意:本脚本包含 IAM 写入操作(CreateRole、AttachRolePolicy),
|
|
7
|
+
仅应在用户明确同意后执行,不可自动运行
|
|
8
|
+
|
|
9
|
+
用法:
|
|
10
|
+
python3 create_role.py # 自动获取账号 UIN
|
|
11
|
+
python3 create_role.py --uin 100001234 # 手动指定账号 UIN
|
|
12
|
+
|
|
13
|
+
返回码:
|
|
14
|
+
0 - 角色创建成功,配置已保存
|
|
15
|
+
1 - 参数错误
|
|
16
|
+
2 - AK/SK 未配置或无效
|
|
17
|
+
3 - 角色创建失败
|
|
18
|
+
|
|
19
|
+
输出: JSON 格式结果(供 AI 解析)
|
|
20
|
+
|
|
21
|
+
跨平台支持: Windows / Linux / macOS
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import os
|
|
26
|
+
import platform
|
|
27
|
+
import stat
|
|
28
|
+
import sys
|
|
29
|
+
from datetime import datetime, timezone
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
# 导入 tcloud_api 模块
|
|
33
|
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
34
|
+
sys.path.insert(0, str(SCRIPT_DIR))
|
|
35
|
+
|
|
36
|
+
from tcloud_api import call_api # noqa: E402
|
|
37
|
+
|
|
38
|
+
# ============== 配置 ==============
|
|
39
|
+
CONFIG_DIR = Path.home() / ".tencent-cloudq"
|
|
40
|
+
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
41
|
+
ROLE_NAME = "advisor"
|
|
42
|
+
POLICY_NAMES = [
|
|
43
|
+
"QcloudTAGFullAccess",
|
|
44
|
+
"QcloudAdvisorFullAccess",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def output_json(obj: dict) -> str:
|
|
49
|
+
return json.dumps(obj, indent=2, ensure_ascii=False)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def output_success(role_arn: str, account_uin: str, role_id: str = "unknown") -> dict:
|
|
53
|
+
return {
|
|
54
|
+
"success": True,
|
|
55
|
+
"action": "CreateAdvisorRole",
|
|
56
|
+
"data": {
|
|
57
|
+
"roleName": ROLE_NAME,
|
|
58
|
+
"roleArn": role_arn,
|
|
59
|
+
"roleId": role_id,
|
|
60
|
+
"accountUin": account_uin,
|
|
61
|
+
"policiesAttached": POLICY_NAMES,
|
|
62
|
+
"consoleLogin": True,
|
|
63
|
+
"configFile": str(CONFIG_FILE),
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def output_error(code: str, message: str) -> dict:
|
|
69
|
+
return {
|
|
70
|
+
"success": False,
|
|
71
|
+
"action": "CreateAdvisorRole",
|
|
72
|
+
"error": {"code": code, "message": message},
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
MAX_ROLE_NAME_ATTEMPTS = 20
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def find_available_role_name() -> tuple:
|
|
80
|
+
"""查找可用的角色名称,支持递增命名。
|
|
81
|
+
|
|
82
|
+
检查顺序: advisor → advisor1 → advisor2 → ...
|
|
83
|
+
- 如果角色不存在 → 返回该名称(可创建)
|
|
84
|
+
- 如果角色存在且 ConsoleLogin=1 → 返回该名称(已可用)
|
|
85
|
+
- 如果角色存在但 ConsoleLogin=0 → 尝试下一个名称
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
tuple: (role_name, existing_ok)
|
|
89
|
+
role_name: 可用的角色名称,或 None(超过最大尝试次数)
|
|
90
|
+
existing_ok: True 表示角色已存在且支持控制台登录(无需创建)
|
|
91
|
+
"""
|
|
92
|
+
candidates = [ROLE_NAME] + [f"{ROLE_NAME}{i}" for i in range(1, MAX_ROLE_NAME_ATTEMPTS)]
|
|
93
|
+
for name in candidates:
|
|
94
|
+
result = call_api(
|
|
95
|
+
"cam", "cam.tencentcloudapi.com",
|
|
96
|
+
"GetRole", "2019-01-16",
|
|
97
|
+
{"RoleName": name},
|
|
98
|
+
)
|
|
99
|
+
if not result.get("success"):
|
|
100
|
+
return name, False
|
|
101
|
+
if result.get("data", {}).get("ConsoleLogin", 0) == 1:
|
|
102
|
+
return name, True
|
|
103
|
+
return None, False
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def save_config(account_uin: str, role_arn: str, role_id: str = "",
|
|
107
|
+
auto_created: bool = True):
|
|
108
|
+
"""保存配置到文件"""
|
|
109
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
110
|
+
|
|
111
|
+
if platform.system() != "Windows":
|
|
112
|
+
try:
|
|
113
|
+
os.chmod(str(CONFIG_DIR), stat.S_IRWXU) # 700
|
|
114
|
+
except OSError:
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
config = {
|
|
118
|
+
"accountUin": account_uin,
|
|
119
|
+
"roleName": ROLE_NAME,
|
|
120
|
+
"roleArn": role_arn,
|
|
121
|
+
"roleId": role_id,
|
|
122
|
+
"configuredAt": datetime.now(timezone.utc).isoformat(),
|
|
123
|
+
"autoCreated": auto_created,
|
|
124
|
+
"version": "1.0",
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
CONFIG_FILE.write_text(
|
|
128
|
+
json.dumps(config, indent=2, ensure_ascii=False),
|
|
129
|
+
encoding="utf-8",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if platform.system() != "Windows":
|
|
133
|
+
try:
|
|
134
|
+
os.chmod(str(CONFIG_FILE), stat.S_IRUSR | stat.S_IWUSR) # 600
|
|
135
|
+
except OSError:
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def main():
|
|
140
|
+
# ============== 参数解析 ==============
|
|
141
|
+
account_uin = ""
|
|
142
|
+
args = sys.argv[1:]
|
|
143
|
+
i = 0
|
|
144
|
+
while i < len(args):
|
|
145
|
+
if args[i] == "--uin" and i + 1 < len(args):
|
|
146
|
+
account_uin = args[i + 1]
|
|
147
|
+
i += 2
|
|
148
|
+
else:
|
|
149
|
+
print(output_json(output_error("InvalidParameter", f"未知参数: {args[i]}")))
|
|
150
|
+
sys.exit(1)
|
|
151
|
+
|
|
152
|
+
# ============== 1. 检查 AK/SK ==============
|
|
153
|
+
secret_id = os.environ.get("TENCENTCLOUD_SECRET_ID", "")
|
|
154
|
+
secret_key = os.environ.get("TENCENTCLOUD_SECRET_KEY", "")
|
|
155
|
+
|
|
156
|
+
if not secret_id or not secret_key:
|
|
157
|
+
print(output_json(output_error(
|
|
158
|
+
"MissingCredentials",
|
|
159
|
+
"未配置 TENCENTCLOUD_SECRET_ID 或 TENCENTCLOUD_SECRET_KEY"
|
|
160
|
+
)))
|
|
161
|
+
sys.exit(2)
|
|
162
|
+
|
|
163
|
+
# ============== 2. 获取账号 UIN ==============
|
|
164
|
+
if not account_uin:
|
|
165
|
+
uin_result = call_api(
|
|
166
|
+
"sts", "sts.tencentcloudapi.com",
|
|
167
|
+
"GetCallerIdentity", "2018-08-13", {},
|
|
168
|
+
)
|
|
169
|
+
account_uin = str(uin_result.get("data", {}).get("AccountId", ""))
|
|
170
|
+
|
|
171
|
+
if not account_uin or account_uin in ("", "None", "null"):
|
|
172
|
+
error_msg = uin_result.get("error", {}).get("message", "无法获取账号 UIN")
|
|
173
|
+
print(output_json(output_error("GetCallerIdentityFailed", error_msg)))
|
|
174
|
+
sys.exit(2)
|
|
175
|
+
|
|
176
|
+
# ============== 3. 查找可用角色名称 ==============
|
|
177
|
+
target_role, existing_ok = find_available_role_name()
|
|
178
|
+
|
|
179
|
+
if target_role is None:
|
|
180
|
+
print(output_json(output_error(
|
|
181
|
+
"NoAvailableRoleName",
|
|
182
|
+
"无法找到可用的角色名称(已尝试 advisor ~ advisor19),"
|
|
183
|
+
"请到 CAM 控制台手动管理: https://console.cloud.tencent.com/cam/role"
|
|
184
|
+
)))
|
|
185
|
+
sys.exit(3)
|
|
186
|
+
|
|
187
|
+
if existing_ok:
|
|
188
|
+
# 角色已存在且支持控制台登录 → 补充缺失策略 + 保存配置
|
|
189
|
+
role_check = call_api(
|
|
190
|
+
"cam", "cam.tencentcloudapi.com",
|
|
191
|
+
"GetRole", "2019-01-16",
|
|
192
|
+
{"RoleName": target_role},
|
|
193
|
+
)
|
|
194
|
+
role_id = str(role_check.get("data", {}).get("RoleId", "unknown"))
|
|
195
|
+
role_arn = f"qcs::cam::uin/{account_uin}:roleName/{target_role}"
|
|
196
|
+
|
|
197
|
+
# 为存量角色补充新增策略(已关联的会返回成功或 PolicyAlreadyAttached,均无副作用)
|
|
198
|
+
attach_warnings = []
|
|
199
|
+
for policy_name in POLICY_NAMES:
|
|
200
|
+
attach_result = call_api(
|
|
201
|
+
"cam", "cam.tencentcloudapi.com",
|
|
202
|
+
"AttachRolePolicy", "2019-01-16",
|
|
203
|
+
{"AttachRoleName": target_role, "PolicyName": policy_name},
|
|
204
|
+
)
|
|
205
|
+
if not attach_result.get("success"):
|
|
206
|
+
err_code = attach_result.get("error", {}).get("code", "")
|
|
207
|
+
# PolicyAlreadyAttached 不视为错误
|
|
208
|
+
if "AlreadyAttached" not in err_code:
|
|
209
|
+
err_msg = attach_result.get("error", {}).get("message", "未知错误")
|
|
210
|
+
attach_warnings.append(f"策略 {policy_name} 关联失败: {err_msg}")
|
|
211
|
+
print(f"WARNING: {attach_warnings[-1]}", file=sys.stderr)
|
|
212
|
+
|
|
213
|
+
save_config(account_uin, role_arn, role_id, auto_created=False)
|
|
214
|
+
result = output_success(role_arn, account_uin, role_id)
|
|
215
|
+
result["data"]["roleName"] = target_role
|
|
216
|
+
if attach_warnings:
|
|
217
|
+
result["data"]["warnings"] = attach_warnings
|
|
218
|
+
print(output_json(result))
|
|
219
|
+
sys.exit(0)
|
|
220
|
+
|
|
221
|
+
# ============== 4. 创建角色 ==============
|
|
222
|
+
trust_policy = json.dumps({
|
|
223
|
+
"version": "2.0",
|
|
224
|
+
"statement": [{
|
|
225
|
+
"action": "name/sts:AssumeRole",
|
|
226
|
+
"effect": "allow",
|
|
227
|
+
"principal": {
|
|
228
|
+
"qcs": [f"qcs::cam::uin/{account_uin}:root"]
|
|
229
|
+
}
|
|
230
|
+
}]
|
|
231
|
+
}, separators=(",", ":"))
|
|
232
|
+
|
|
233
|
+
create_result = call_api(
|
|
234
|
+
"cam", "cam.tencentcloudapi.com",
|
|
235
|
+
"CreateRole", "2019-01-16",
|
|
236
|
+
{
|
|
237
|
+
"RoleName": target_role,
|
|
238
|
+
"PolicyDocument": trust_policy,
|
|
239
|
+
"ConsoleLogin": 1,
|
|
240
|
+
"Description": "腾讯云智能顾问助手角色(由 tencent-cloudq skill 创建)",
|
|
241
|
+
},
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
if not create_result.get("success"):
|
|
245
|
+
err = create_result.get("error", {})
|
|
246
|
+
print(output_json(output_error(
|
|
247
|
+
err.get("code", "CreateRoleFailed"),
|
|
248
|
+
f"角色 {target_role} 创建失败: {err.get('message', '未知错误')}"
|
|
249
|
+
)))
|
|
250
|
+
sys.exit(3)
|
|
251
|
+
|
|
252
|
+
role_id = str(create_result.get("data", {}).get("RoleId", "unknown"))
|
|
253
|
+
|
|
254
|
+
# ============== 5. 关联策略 ==============
|
|
255
|
+
attach_warnings = []
|
|
256
|
+
for policy_name in POLICY_NAMES:
|
|
257
|
+
attach_result = call_api(
|
|
258
|
+
"cam", "cam.tencentcloudapi.com",
|
|
259
|
+
"AttachRolePolicy", "2019-01-16",
|
|
260
|
+
{"AttachRoleName": target_role, "PolicyName": policy_name},
|
|
261
|
+
)
|
|
262
|
+
if not attach_result.get("success"):
|
|
263
|
+
err_msg = attach_result.get("error", {}).get("message", "未知错误")
|
|
264
|
+
attach_warnings.append(f"策略 {policy_name} 关联失败: {err_msg}")
|
|
265
|
+
print(f"WARNING: {attach_warnings[-1]}", file=sys.stderr)
|
|
266
|
+
|
|
267
|
+
# ============== 6. 保存配置 ==============
|
|
268
|
+
role_arn = f"qcs::cam::uin/{account_uin}:roleName/{target_role}"
|
|
269
|
+
save_config(account_uin, role_arn, role_id, auto_created=True)
|
|
270
|
+
|
|
271
|
+
# ============== 7. 输出结果 ==============
|
|
272
|
+
result = output_success(role_arn, account_uin, role_id)
|
|
273
|
+
result["data"]["roleName"] = target_role
|
|
274
|
+
if attach_warnings:
|
|
275
|
+
result["data"]["warnings"] = attach_warnings
|
|
276
|
+
print(output_json(result))
|
|
277
|
+
sys.exit(0)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
if __name__ == "__main__":
|
|
281
|
+
main()
|