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,329 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
腾讯云 API 签名 v3 通用调用脚本 (Python 版)
|
|
4
|
+
基于腾讯云 API 签名 v3 文档实现: https://cloud.tencent.com/document/product/213/30654
|
|
5
|
+
|
|
6
|
+
纯 Python 标准库实现,支持 Windows / Linux / macOS 跨平台运行。
|
|
7
|
+
不依赖 curl、openssl、jq 等外部工具。
|
|
8
|
+
|
|
9
|
+
用法 (命令行):
|
|
10
|
+
python3 tcloud_api.py <service> <host> <action> <version> [payload] [region]
|
|
11
|
+
|
|
12
|
+
示例:
|
|
13
|
+
python3 tcloud_api.py advisor advisor.tencentcloudapi.com DescribeArchList 2020-07-21 '{"PageNumber":1,"PageSize":10}'
|
|
14
|
+
|
|
15
|
+
作为模块导入:
|
|
16
|
+
from tcloud_api import call_api
|
|
17
|
+
result = call_api("advisor", "advisor.tencentcloudapi.com",
|
|
18
|
+
"DescribeArchList", "2020-07-21",
|
|
19
|
+
{"PageNumber": 1, "PageSize": 10})
|
|
20
|
+
|
|
21
|
+
环境变量(必须提前设置):
|
|
22
|
+
TENCENTCLOUD_SECRET_ID - 腾讯云 SecretId
|
|
23
|
+
TENCENTCLOUD_SECRET_KEY - 腾讯云 SecretKey
|
|
24
|
+
TENCENTCLOUD_TOKEN - 临时密钥 Token(可选,使用临时密钥时设置)
|
|
25
|
+
|
|
26
|
+
输出格式(统一 JSON):
|
|
27
|
+
成功: {"success": true, "action": "<Action>", "data": {...}, "requestId": "xxx"}
|
|
28
|
+
失败: {"success": false, "action": "<Action>", "error": {"code": "xxx", "message": "xxx"}, "requestId": "xxx"}
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import hashlib
|
|
32
|
+
import hmac
|
|
33
|
+
import json
|
|
34
|
+
import os
|
|
35
|
+
import ssl
|
|
36
|
+
import sys
|
|
37
|
+
import time
|
|
38
|
+
from datetime import datetime, timezone
|
|
39
|
+
from typing import Union
|
|
40
|
+
from urllib.request import Request, urlopen
|
|
41
|
+
from urllib.error import URLError, HTTPError
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _get_ssl_context():
|
|
45
|
+
"""获取 SSL 上下文,兼容各平台 CA 证书差异"""
|
|
46
|
+
try:
|
|
47
|
+
import certifi
|
|
48
|
+
return ssl.create_default_context(cafile=certifi.where())
|
|
49
|
+
except ImportError:
|
|
50
|
+
# certifi 不可用时降级到系统默认 CA(Windows/Linux 系统 CA 通常可用)
|
|
51
|
+
return ssl.create_default_context()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _sign_tc3(key: bytes, msg: str) -> bytes:
|
|
55
|
+
"""TC3 HMAC-SHA256 签名辅助函数"""
|
|
56
|
+
return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _output_json(obj: dict) -> str:
|
|
60
|
+
"""统一 JSON 输出"""
|
|
61
|
+
return json.dumps(obj, ensure_ascii=False)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _make_error(action: str, code: str, message: str, request_id: str = "") -> dict:
|
|
65
|
+
"""构造统一错误结果"""
|
|
66
|
+
return {
|
|
67
|
+
"success": False,
|
|
68
|
+
"action": action,
|
|
69
|
+
"error": {"code": code, "message": message},
|
|
70
|
+
"requestId": request_id,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _make_success(action: str, data: dict, request_id: str) -> dict:
|
|
75
|
+
"""构造统一成功结果"""
|
|
76
|
+
return {
|
|
77
|
+
"success": True,
|
|
78
|
+
"action": action,
|
|
79
|
+
"data": data,
|
|
80
|
+
"requestId": request_id,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def call_api(service: str, host: str, action: str, version: str,
|
|
85
|
+
payload: Union[dict, str] = None, region: str = "ap-guangzhou",
|
|
86
|
+
secret_id: str = None, secret_key: str = None,
|
|
87
|
+
token: str = None) -> dict:
|
|
88
|
+
"""
|
|
89
|
+
调用腾讯云 API(TC3-HMAC-SHA256 签名)
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
service: 服务名(如 advisor, sts, cam)
|
|
93
|
+
host: 接口域名(如 advisor.tencentcloudapi.com)
|
|
94
|
+
action: 接口名称(如 DescribeArchList)
|
|
95
|
+
version: 接口版本(如 2020-07-21)
|
|
96
|
+
payload: 请求体,dict 或 JSON 字符串
|
|
97
|
+
region: 地域,默认 ap-guangzhou
|
|
98
|
+
secret_id: SecretId,不传则通过 credential_manager 或环境变量获取
|
|
99
|
+
secret_key: SecretKey,不传则通过 credential_manager 或环境变量获取
|
|
100
|
+
token: 临时密钥 Token,不传则通过 credential_manager 或环境变量获取
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
dict: 统一格式的结果字典
|
|
104
|
+
"""
|
|
105
|
+
# 密钥:优先使用显式传入,其次通过 credential_manager 统一获取
|
|
106
|
+
cred_source = "env"
|
|
107
|
+
if not secret_id or not secret_key:
|
|
108
|
+
try:
|
|
109
|
+
from credential_manager import (
|
|
110
|
+
get_credential, CredentialExpiredError, CredentialNotFoundError,
|
|
111
|
+
)
|
|
112
|
+
cred = get_credential()
|
|
113
|
+
secret_id = secret_id or cred["secretId"]
|
|
114
|
+
secret_key = secret_key or cred["secretKey"]
|
|
115
|
+
token = token or cred.get("token", "")
|
|
116
|
+
cred_source = cred.get("source", "env")
|
|
117
|
+
except ImportError:
|
|
118
|
+
# credential_manager 不可用,回退到环境变量
|
|
119
|
+
secret_id = secret_id or os.environ.get("TENCENTCLOUD_SECRET_ID", "")
|
|
120
|
+
secret_key = secret_key or os.environ.get("TENCENTCLOUD_SECRET_KEY", "")
|
|
121
|
+
token = token or os.environ.get("TENCENTCLOUD_TOKEN", "")
|
|
122
|
+
except CredentialExpiredError as e:
|
|
123
|
+
return _make_error(
|
|
124
|
+
action, "CredentialExpired",
|
|
125
|
+
f"OAuth 凭证已过期,需要重新授权登录。{e}"
|
|
126
|
+
)
|
|
127
|
+
except CredentialNotFoundError:
|
|
128
|
+
return _make_error(
|
|
129
|
+
action, "NeedAuth",
|
|
130
|
+
"未找到凭证,请先通过 OAuth 登录或配置 AK/SK 环境变量。"
|
|
131
|
+
)
|
|
132
|
+
except Exception as e:
|
|
133
|
+
return _make_error(
|
|
134
|
+
action, "NeedAuth", str(e)
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
if not secret_id or not secret_key:
|
|
138
|
+
return _make_error(
|
|
139
|
+
action, "MissingCredentials",
|
|
140
|
+
"未配置凭证。请通过 OAuth 登录或配置 AK/SK 环境变量。\n"
|
|
141
|
+
"OAuth 登录: python3 scripts/login.py\n"
|
|
142
|
+
"密钥获取: https://console.cloud.tencent.com/cam/capi"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# ---- 服务白名单限制 ----
|
|
146
|
+
# OAuth: 仅允许 advisor(OAuth 临时密钥仅有智能顾问权限)
|
|
147
|
+
# AK/SK: 允许 advisor + cam + sts(check_env/create_role/login_url 需要)
|
|
148
|
+
_OAUTH_ALLOWED_SERVICES = {"advisor"}
|
|
149
|
+
_AKSK_ALLOWED_SERVICES = {"advisor", "cam", "sts"}
|
|
150
|
+
|
|
151
|
+
if cred_source == "oauth":
|
|
152
|
+
allowed = _OAUTH_ALLOWED_SERVICES
|
|
153
|
+
deny_hint = f"如需调用 {service} 服务,请配置 AK/SK 环境变量。"
|
|
154
|
+
else:
|
|
155
|
+
allowed = _AKSK_ALLOWED_SERVICES
|
|
156
|
+
deny_hint = f"CloudQ 不支持调用 {service} 服务。"
|
|
157
|
+
|
|
158
|
+
if service not in allowed:
|
|
159
|
+
return _make_error(
|
|
160
|
+
action, "ServiceNotAllowed",
|
|
161
|
+
f"当前凭证不允许调用 {service} 服务(允许: {', '.join(sorted(allowed))})。{deny_hint}"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# 请求体
|
|
165
|
+
if payload is None:
|
|
166
|
+
payload_str = "{}"
|
|
167
|
+
elif isinstance(payload, dict):
|
|
168
|
+
payload_str = json.dumps(payload, separators=(",", ":"))
|
|
169
|
+
else:
|
|
170
|
+
payload_str = str(payload)
|
|
171
|
+
|
|
172
|
+
# 签名参数
|
|
173
|
+
algorithm = "TC3-HMAC-SHA256"
|
|
174
|
+
timestamp = int(time.time())
|
|
175
|
+
date = datetime.fromtimestamp(timestamp, tz=timezone.utc).strftime("%Y-%m-%d")
|
|
176
|
+
|
|
177
|
+
# 步骤 1:拼接规范请求串
|
|
178
|
+
hashed_payload = hashlib.sha256(payload_str.encode("utf-8")).hexdigest()
|
|
179
|
+
canonical_request = (
|
|
180
|
+
f"POST\n"
|
|
181
|
+
f"/\n"
|
|
182
|
+
f"\n"
|
|
183
|
+
f"content-type:application/json; charset=utf-8\n"
|
|
184
|
+
f"host:{host}\n"
|
|
185
|
+
f"x-tc-action:{action.lower()}\n"
|
|
186
|
+
f"\n"
|
|
187
|
+
f"content-type;host;x-tc-action\n"
|
|
188
|
+
f"{hashed_payload}"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# 步骤 2:拼接待签名字符串
|
|
192
|
+
credential_scope = f"{date}/{service}/tc3_request"
|
|
193
|
+
hashed_canonical_request = hashlib.sha256(
|
|
194
|
+
canonical_request.encode("utf-8")
|
|
195
|
+
).hexdigest()
|
|
196
|
+
string_to_sign = (
|
|
197
|
+
f"{algorithm}\n"
|
|
198
|
+
f"{timestamp}\n"
|
|
199
|
+
f"{credential_scope}\n"
|
|
200
|
+
f"{hashed_canonical_request}"
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# 步骤 3:计算签名
|
|
204
|
+
secret_date = _sign_tc3(f"TC3{secret_key}".encode("utf-8"), date)
|
|
205
|
+
secret_service = _sign_tc3(secret_date, service)
|
|
206
|
+
secret_signing = _sign_tc3(secret_service, "tc3_request")
|
|
207
|
+
signature = hmac.new(
|
|
208
|
+
secret_signing, string_to_sign.encode("utf-8"), hashlib.sha256
|
|
209
|
+
).hexdigest()
|
|
210
|
+
|
|
211
|
+
# 步骤 4:拼接 Authorization
|
|
212
|
+
authorization = (
|
|
213
|
+
f"{algorithm} "
|
|
214
|
+
f"Credential={secret_id}/{credential_scope}, "
|
|
215
|
+
f"SignedHeaders=content-type;host;x-tc-action, "
|
|
216
|
+
f"Signature={signature}"
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# 步骤 5:发送请求
|
|
220
|
+
headers = {
|
|
221
|
+
"Authorization": authorization,
|
|
222
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
223
|
+
"Host": host,
|
|
224
|
+
"X-TC-Action": action,
|
|
225
|
+
"X-TC-Timestamp": str(timestamp),
|
|
226
|
+
"X-TC-Version": version,
|
|
227
|
+
"X-TC-Region": region,
|
|
228
|
+
}
|
|
229
|
+
if token:
|
|
230
|
+
headers["X-TC-Token"] = token
|
|
231
|
+
|
|
232
|
+
req = Request(
|
|
233
|
+
f"https://{host}",
|
|
234
|
+
data=payload_str.encode("utf-8"),
|
|
235
|
+
headers=headers,
|
|
236
|
+
method="POST",
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
ctx = _get_ssl_context()
|
|
241
|
+
with urlopen(req, context=ctx, timeout=30) as resp:
|
|
242
|
+
response_body = resp.read().decode("utf-8")
|
|
243
|
+
except HTTPError as e:
|
|
244
|
+
try:
|
|
245
|
+
body = e.read().decode("utf-8")
|
|
246
|
+
# 尝试解析 API 错误响应
|
|
247
|
+
data = json.loads(body)
|
|
248
|
+
response = data.get("Response", {})
|
|
249
|
+
error = response.get("Error", {})
|
|
250
|
+
if error:
|
|
251
|
+
return _make_error(
|
|
252
|
+
action,
|
|
253
|
+
error.get("Code", "HTTPError"),
|
|
254
|
+
error.get("Message", f"HTTP {e.code}"),
|
|
255
|
+
response.get("RequestId", ""),
|
|
256
|
+
)
|
|
257
|
+
except Exception:
|
|
258
|
+
pass
|
|
259
|
+
return _make_error(
|
|
260
|
+
action, "HTTPError",
|
|
261
|
+
f"HTTP 请求失败 (状态码 {e.code}): {e.reason}"
|
|
262
|
+
)
|
|
263
|
+
except URLError as e:
|
|
264
|
+
return _make_error(
|
|
265
|
+
action, "NetworkError",
|
|
266
|
+
f"网络连接失败,请检查网络和域名 {host} 是否可达: {e.reason}"
|
|
267
|
+
)
|
|
268
|
+
except Exception as e:
|
|
269
|
+
return _make_error(
|
|
270
|
+
action, "NetworkError",
|
|
271
|
+
f"请求异常: {e}"
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# 步骤 6:解析响应
|
|
275
|
+
try:
|
|
276
|
+
data = json.loads(response_body)
|
|
277
|
+
except json.JSONDecodeError:
|
|
278
|
+
return _make_error(action, "ParseError", "响应不是有效的 JSON")
|
|
279
|
+
|
|
280
|
+
response = data.get("Response", {})
|
|
281
|
+
request_id = response.get("RequestId", "")
|
|
282
|
+
|
|
283
|
+
if "Error" in response:
|
|
284
|
+
err = response["Error"]
|
|
285
|
+
return _make_error(
|
|
286
|
+
action,
|
|
287
|
+
err.get("Code", "Unknown"),
|
|
288
|
+
err.get("Message", "未知错误"),
|
|
289
|
+
request_id,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# 成功:移除 RequestId 后将剩余字段作为 data
|
|
293
|
+
result_data = {k: v for k, v in response.items() if k != "RequestId"}
|
|
294
|
+
return _make_success(action, result_data, request_id)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def main():
|
|
298
|
+
"""命令行入口"""
|
|
299
|
+
args = sys.argv[1:]
|
|
300
|
+
|
|
301
|
+
if len(args) < 4:
|
|
302
|
+
print(_output_json(_make_error(
|
|
303
|
+
"", "MissingParameter",
|
|
304
|
+
"缺少必要参数。用法: python3 tcloud_api.py <service> <host> <action> <version> [payload] [region]"
|
|
305
|
+
)))
|
|
306
|
+
sys.exit(1)
|
|
307
|
+
|
|
308
|
+
service = args[0]
|
|
309
|
+
host = args[1]
|
|
310
|
+
action = args[2]
|
|
311
|
+
version = args[3]
|
|
312
|
+
payload_str = args[4] if len(args) > 4 else "{}"
|
|
313
|
+
region = args[5] if len(args) > 5 else "ap-guangzhou"
|
|
314
|
+
|
|
315
|
+
# 解析 payload
|
|
316
|
+
try:
|
|
317
|
+
payload = json.loads(payload_str)
|
|
318
|
+
except json.JSONDecodeError:
|
|
319
|
+
payload = payload_str
|
|
320
|
+
|
|
321
|
+
result = call_api(service, host, action, version, payload, region)
|
|
322
|
+
print(_output_json(result))
|
|
323
|
+
|
|
324
|
+
if not result.get("success"):
|
|
325
|
+
sys.exit(1)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
if __name__ == "__main__":
|
|
329
|
+
main()
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
CloudQ 异步任务管理脚本
|
|
4
|
+
|
|
5
|
+
用于管理 CloudQ 异步对话任务(Async=true 模式):
|
|
6
|
+
- DescribeCloudQAsyncTask:查询异步任务执行状态和结果
|
|
7
|
+
- CancelCloudQAsyncTask:取消异步后台编排任务
|
|
8
|
+
|
|
9
|
+
通过 tcloud_api.call_api() 调用标准腾讯云 API(非 SSE)。
|
|
10
|
+
|
|
11
|
+
用法 (命令行):
|
|
12
|
+
# 查询任务状态
|
|
13
|
+
python3 tcloud_async_task.py query <chat_id> <session_id>
|
|
14
|
+
|
|
15
|
+
# 取消任务
|
|
16
|
+
python3 tcloud_async_task.py cancel <chat_id> [session_id]
|
|
17
|
+
|
|
18
|
+
# Poll 轮询直到完成(自动循环 query,内部自适应间隔)
|
|
19
|
+
python3 tcloud_async_task.py poll <chat_id> <session_id> [timeout=1200]
|
|
20
|
+
|
|
21
|
+
示例:
|
|
22
|
+
python3 tcloud_async_task.py query chat-7f3a9b2e1d4c sess-e5b8c1a0f6d2
|
|
23
|
+
python3 tcloud_async_task.py cancel chat-7f3a9b2e1d4c sess-e5b8c1a0f6d2
|
|
24
|
+
python3 tcloud_async_task.py poll chat-7f3a9b2e1d4c sess-e5b8c1a0f6d2
|
|
25
|
+
|
|
26
|
+
作为模块导入:
|
|
27
|
+
from tcloud_async_task import query_async_task, cancel_async_task, poll_until_complete
|
|
28
|
+
|
|
29
|
+
result = query_async_task("chat-xxx", "sess-xxx")
|
|
30
|
+
if result["success"]:
|
|
31
|
+
status = result["data"]["Status"]
|
|
32
|
+
content = result["data"]["Content"]
|
|
33
|
+
|
|
34
|
+
result = cancel_async_task("chat-xxx", "sess-xxx")
|
|
35
|
+
if result["success"]:
|
|
36
|
+
print("任务已取消")
|
|
37
|
+
|
|
38
|
+
输出格式(统一 JSON):
|
|
39
|
+
成功: {"success": true, "action": "...", "data": {...}, "requestId": "..."}
|
|
40
|
+
失败: {"success": false, "action": "...", "error": {...}, "requestId": "..."}
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
import json
|
|
44
|
+
import sys
|
|
45
|
+
import time
|
|
46
|
+
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
# 固定参数
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
SERVICE = "advisor"
|
|
51
|
+
HOST = "advisor.ai.tencentcloudapi.com"
|
|
52
|
+
VERSION = "2020-07-21"
|
|
53
|
+
ACTION_QUERY = "DescribeCloudQAsyncTask"
|
|
54
|
+
ACTION_CANCEL = "CancelCloudQAsyncTask"
|
|
55
|
+
|
|
56
|
+
# 轮询参数
|
|
57
|
+
DEFAULT_POLL_INTERVAL = 5 # 秒(固定 5s 间隔)
|
|
58
|
+
DEFAULT_POLL_TIMEOUT = 1200 # 秒(CloudQ 长任务最长可达 20 分钟)
|
|
59
|
+
FIXED_POLL_INTERVAL = 5 # 秒(固定,不允许用户自定义)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
# 公共函数
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
def _output_json(obj: dict) -> str:
|
|
67
|
+
return json.dumps(obj, ensure_ascii=False)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
# DescribeCloudQAsyncTask - 查询异步任务状态
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
def query_async_task(chat_id: str, session_id: str,
|
|
75
|
+
secret_id: str = None, secret_key: str = None,
|
|
76
|
+
token: str = None) -> dict:
|
|
77
|
+
"""
|
|
78
|
+
查询异步任务状态。
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
chat_id: 异步任务 ID(由 accepted 帧 ChatId 返回)
|
|
82
|
+
session_id: 会话 ID(由 accepted 帧 SessionId 返回)
|
|
83
|
+
secret_id: SecretId,不传则自动获取
|
|
84
|
+
secret_key: SecretKey,不传则自动获取
|
|
85
|
+
token: 临时密钥 Token
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
dict: {
|
|
89
|
+
"success": true/false,
|
|
90
|
+
"action": "DescribeCloudQAsyncTask",
|
|
91
|
+
"data": {
|
|
92
|
+
"Status": "running|completed|failed|cancelled|timeout|not_found",
|
|
93
|
+
"FinishReason": "stop|user_stopped|timeout|error" (running/not_found时为空),
|
|
94
|
+
"Content": "分析结果...",
|
|
95
|
+
"SessionID": "sess-xxx",
|
|
96
|
+
"ChatID": "chat-xxx"
|
|
97
|
+
},
|
|
98
|
+
"requestId": "..."
|
|
99
|
+
}
|
|
100
|
+
"""
|
|
101
|
+
if not chat_id:
|
|
102
|
+
return {
|
|
103
|
+
"success": False,
|
|
104
|
+
"action": ACTION_QUERY,
|
|
105
|
+
"error": {"code": "MissingParameter", "message": "ChatID 为必填参数"},
|
|
106
|
+
"requestId": "",
|
|
107
|
+
}
|
|
108
|
+
if not session_id:
|
|
109
|
+
return {
|
|
110
|
+
"success": False,
|
|
111
|
+
"action": ACTION_QUERY,
|
|
112
|
+
"error": {"code": "MissingParameter", "message": "SessionID 为必填参数"},
|
|
113
|
+
"requestId": "",
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
from tcloud_api import call_api
|
|
118
|
+
except ImportError:
|
|
119
|
+
return {
|
|
120
|
+
"success": False,
|
|
121
|
+
"action": ACTION_QUERY,
|
|
122
|
+
"error": {
|
|
123
|
+
"code": "ImportError",
|
|
124
|
+
"message": "无法导入 tcloud_api 模块,请确保 scripts 目录在 Python 路径中"
|
|
125
|
+
},
|
|
126
|
+
"requestId": "",
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
payload = {"ChatID": chat_id, "SessionID": session_id}
|
|
130
|
+
|
|
131
|
+
result = call_api(
|
|
132
|
+
service=SERVICE, host=HOST, action=ACTION_QUERY,
|
|
133
|
+
version=VERSION, payload=payload,
|
|
134
|
+
secret_id=secret_id, secret_key=secret_key, token=token,
|
|
135
|
+
)
|
|
136
|
+
return result
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
# CancelCloudQAsyncTask - 取消异步任务
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
def cancel_async_task(chat_id: str, session_id: str = "",
|
|
144
|
+
secret_id: str = None, secret_key: str = None,
|
|
145
|
+
token: str = None) -> dict:
|
|
146
|
+
"""
|
|
147
|
+
取消异步后台编排任务。
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
chat_id: 异步任务 ID(由 accepted 帧 ChatId 返回)
|
|
151
|
+
session_id: 会话 ID(可选,仅用于日志关联)
|
|
152
|
+
secret_id: SecretId,不传则自动获取
|
|
153
|
+
secret_key: SecretKey,不传则自动获取
|
|
154
|
+
token: 临时密钥 Token
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
dict: {
|
|
158
|
+
"success": true/false,
|
|
159
|
+
"action": "CancelCloudQAsyncTask",
|
|
160
|
+
"data": {"Success": true/false},
|
|
161
|
+
"requestId": "..."
|
|
162
|
+
}
|
|
163
|
+
"""
|
|
164
|
+
if not chat_id:
|
|
165
|
+
return {
|
|
166
|
+
"success": False,
|
|
167
|
+
"action": ACTION_CANCEL,
|
|
168
|
+
"error": {"code": "MissingParameter", "message": "ChatID 为必填参数"},
|
|
169
|
+
"requestId": "",
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
from tcloud_api import call_api
|
|
174
|
+
except ImportError:
|
|
175
|
+
return {
|
|
176
|
+
"success": False,
|
|
177
|
+
"action": ACTION_CANCEL,
|
|
178
|
+
"error": {
|
|
179
|
+
"code": "ImportError",
|
|
180
|
+
"message": "无法导入 tcloud_api 模块,请确保 scripts 目录在 Python 路径中"
|
|
181
|
+
},
|
|
182
|
+
"requestId": "",
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
payload = {"ChatID": chat_id}
|
|
186
|
+
if session_id:
|
|
187
|
+
payload["SessionID"] = session_id
|
|
188
|
+
|
|
189
|
+
result = call_api(
|
|
190
|
+
service=SERVICE, host=HOST, action=ACTION_CANCEL,
|
|
191
|
+
version=VERSION, payload=payload,
|
|
192
|
+
secret_id=secret_id, secret_key=secret_key, token=token,
|
|
193
|
+
)
|
|
194
|
+
return result
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ---------------------------------------------------------------------------
|
|
198
|
+
# 便捷轮询函数
|
|
199
|
+
# ---------------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
def poll_until_complete(chat_id: str, session_id: str,
|
|
202
|
+
timeout: int = DEFAULT_POLL_TIMEOUT,
|
|
203
|
+
on_status=None) -> dict:
|
|
204
|
+
"""
|
|
205
|
+
轮询异步任务直到完成、失败或超时。
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
chat_id: 异步任务 ID
|
|
209
|
+
session_id: 会话 ID
|
|
210
|
+
interval: 轮询间隔(秒),范围 [2, 5]
|
|
211
|
+
timeout: 总超时(秒),默认 120
|
|
212
|
+
on_status: 可选回调,status 变更时调用 on_status(status, result)
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
dict: 最后一次 query_async_task 的返回结果
|
|
216
|
+
"""
|
|
217
|
+
interval = FIXED_POLL_INTERVAL
|
|
218
|
+
elapsed = 0
|
|
219
|
+
last_status = None
|
|
220
|
+
terminal_statuses = {"completed", "failed", "cancelled", "timeout", "not_found"}
|
|
221
|
+
retried = False
|
|
222
|
+
|
|
223
|
+
while elapsed < timeout:
|
|
224
|
+
result = query_async_task(chat_id, session_id)
|
|
225
|
+
|
|
226
|
+
if not result.get("success"):
|
|
227
|
+
if not retried:
|
|
228
|
+
retried = True
|
|
229
|
+
time.sleep(1)
|
|
230
|
+
continue # 重试一次
|
|
231
|
+
# 重试后仍失败,返回错误
|
|
232
|
+
return result
|
|
233
|
+
|
|
234
|
+
status = result.get("data", {}).get("Status", "unknown")
|
|
235
|
+
|
|
236
|
+
# 状态变更回调
|
|
237
|
+
if on_status and status != last_status:
|
|
238
|
+
on_status(status, result)
|
|
239
|
+
|
|
240
|
+
# 到达终态
|
|
241
|
+
if status in terminal_statuses:
|
|
242
|
+
return result
|
|
243
|
+
|
|
244
|
+
# 等待后继续轮询
|
|
245
|
+
time.sleep(interval)
|
|
246
|
+
elapsed += interval
|
|
247
|
+
last_status = status
|
|
248
|
+
|
|
249
|
+
# 超时
|
|
250
|
+
return {
|
|
251
|
+
"success": False,
|
|
252
|
+
"action": "PollTimeout",
|
|
253
|
+
"error": {
|
|
254
|
+
"code": "PollTimeout",
|
|
255
|
+
"message": f"轮询超时({timeout}秒),任务可能仍在执行中"
|
|
256
|
+
},
|
|
257
|
+
"requestId": "",
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# ---------------------------------------------------------------------------
|
|
262
|
+
# 命令行入口
|
|
263
|
+
# ---------------------------------------------------------------------------
|
|
264
|
+
|
|
265
|
+
def _print_usage():
|
|
266
|
+
print(_output_json({
|
|
267
|
+
"success": False,
|
|
268
|
+
"action": ACTION_QUERY,
|
|
269
|
+
"error": {
|
|
270
|
+
"code": "MissingParameter",
|
|
271
|
+
"message": (
|
|
272
|
+
"用法:\n"
|
|
273
|
+
" python3 tcloud_async_task.py query <chat_id> <session_id>\n"
|
|
274
|
+
" python3 tcloud_async_task.py cancel <chat_id> [session_id]\n"
|
|
275
|
+
" python3 tcloud_async_task.py poll <chat_id> <session_id> [timeout]"
|
|
276
|
+
)
|
|
277
|
+
},
|
|
278
|
+
"requestId": "",
|
|
279
|
+
}))
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def main():
|
|
283
|
+
"""命令行入口"""
|
|
284
|
+
import io
|
|
285
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
286
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
287
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
288
|
+
else:
|
|
289
|
+
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
|
290
|
+
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
|
|
291
|
+
|
|
292
|
+
args = sys.argv[1:]
|
|
293
|
+
if len(args) < 2:
|
|
294
|
+
_print_usage()
|
|
295
|
+
sys.exit(1)
|
|
296
|
+
|
|
297
|
+
command = args[0].lower()
|
|
298
|
+
chat_id = args[1] if len(args) > 1 else ""
|
|
299
|
+
|
|
300
|
+
if command == "query":
|
|
301
|
+
session_id = args[2] if len(args) > 2 else ""
|
|
302
|
+
result = query_async_task(chat_id, session_id)
|
|
303
|
+
print(_output_json(result))
|
|
304
|
+
if not result.get("success"):
|
|
305
|
+
sys.exit(1)
|
|
306
|
+
|
|
307
|
+
elif command == "cancel":
|
|
308
|
+
session_id = args[2] if len(args) > 2 else ""
|
|
309
|
+
result = cancel_async_task(chat_id, session_id)
|
|
310
|
+
print(_output_json(result))
|
|
311
|
+
if not result.get("success"):
|
|
312
|
+
sys.exit(1)
|
|
313
|
+
|
|
314
|
+
elif command == "poll":
|
|
315
|
+
session_id = args[2] if len(args) > 2 else ""
|
|
316
|
+
try:
|
|
317
|
+
timeout = int(args[3]) if len(args) > 3 else DEFAULT_POLL_TIMEOUT
|
|
318
|
+
except (ValueError, TypeError):
|
|
319
|
+
print(_output_json({
|
|
320
|
+
"success": False, "action": ACTION_QUERY,
|
|
321
|
+
"error": {"code": "InvalidParameter",
|
|
322
|
+
"message": "timeout 必须是整数"},
|
|
323
|
+
"requestId": "",
|
|
324
|
+
}))
|
|
325
|
+
sys.exit(1)
|
|
326
|
+
|
|
327
|
+
def on_status(status, result):
|
|
328
|
+
print(f"[poll] Status: {status}", file=sys.stderr, flush=True)
|
|
329
|
+
|
|
330
|
+
result = poll_until_complete(
|
|
331
|
+
chat_id, session_id,
|
|
332
|
+
timeout=timeout,
|
|
333
|
+
on_status=on_status,
|
|
334
|
+
)
|
|
335
|
+
print(_output_json(result))
|
|
336
|
+
if not result.get("success"):
|
|
337
|
+
sys.exit(1)
|
|
338
|
+
|
|
339
|
+
else:
|
|
340
|
+
_print_usage()
|
|
341
|
+
sys.exit(1)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
if __name__ == "__main__":
|
|
345
|
+
main()
|