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,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()