@pippit-dev/cli 1.0.22 → 1.0.23

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.
@@ -1,26 +1,45 @@
1
- """小云雀 agent-im OpenAPI 公共模块:创建会话、查询会话(鉴权为 Authorization: Bearer <access_key>)"""
1
+ """小云雀 agent-im OpenAPI 公共模块:查询会话(鉴权为 Authorization: Bearer <access_key>)"""
2
2
 
3
3
  import json
4
4
  import os
5
5
  import sys
6
6
  import urllib.request
7
7
  import urllib.error
8
+ import urllib.parse
8
9
 
9
- XYQ_BASE = os.environ.get("XYQ_OPENAPI_BASE", os.environ.get("XYQ_BASE_URL", "https://xyq.jianying.com"))
10
+ # Credentials may only be sent to the fixed production HTTPS origin.
11
+ XYQ_BASE = "https://xyq.jianying.com"
10
12
  ACCESS_KEY = os.environ.get("XYQ_ACCESS_KEY", "")
11
13
 
12
14
  # API 路径常量
13
- SUBMIT_RUN_PATH = "/api/biz/v1/skill/submit_run"
14
15
  GET_THREAD_PATH = "/api/biz/v1/skill/get_thread"
15
- UPLOAD_FILE_PATH = "/api/biz/v1/skill/upload_file"
16
16
  HTTP_TIMEOUT_SECONDS = 30 * 60
17
17
 
18
- if not ACCESS_KEY:
19
- print("错误:请设置 XYQ_ACCESS_KEY 环境变量", file=sys.stderr)
20
- sys.exit(1)
18
+
19
+ class _NoRedirect(urllib.request.HTTPRedirectHandler):
20
+ def redirect_request(self, req, fp, code, msg, headers, newurl):
21
+ # Never forward credentials or request bodies through a redirect.
22
+ raise urllib.error.HTTPError(req.full_url, code, "API 重定向已拒绝", headers, fp)
23
+
24
+
25
+ def authenticated_open(req):
26
+ target = urllib.parse.urlsplit(req.full_url)
27
+ if (target.scheme != "https" or target.hostname != "xyq.jianying.com"
28
+ or target.port not in (None, 443) or target.username is not None
29
+ or target.password is not None):
30
+ raise urllib.error.URLError("仅允许小云雀生产 HTTPS 地址")
31
+ return urllib.request.build_opener(_NoRedirect()).open(req, timeout=HTTP_TIMEOUT_SECONDS)
32
+
33
+
34
+ def redact_error(value):
35
+ text = str(value)
36
+ return text.replace(ACCESS_KEY, "[REDACTED]") if ACCESS_KEY else text
21
37
 
22
38
 
23
39
  def _headers():
40
+ if not ACCESS_KEY:
41
+ print("错误:请设置 XYQ_ACCESS_KEY 环境变量", file=sys.stderr)
42
+ sys.exit(1)
24
43
  return {
25
44
  "Authorization": f"Bearer {ACCESS_KEY}",
26
45
  "Content-Type": "application/json",
@@ -38,14 +57,14 @@ def api_post(path: str, body: dict) -> dict:
38
57
  headers=_headers(),
39
58
  )
40
59
  try:
41
- with urllib.request.urlopen(req, timeout=HTTP_TIMEOUT_SECONDS) as resp:
60
+ with authenticated_open(req) as resp:
42
61
  return json.loads(resp.read().decode("utf-8"))
43
62
  except urllib.error.HTTPError as e:
44
63
  err_body = e.read().decode("utf-8") if e.fp else ""
45
- print(f"API 错误 {e.code}: {err_body}", file=sys.stderr)
64
+ print(f"API 错误 {e.code}: {redact_error(err_body)}", file=sys.stderr)
46
65
  sys.exit(1)
47
66
  except urllib.error.URLError as e:
48
- print(f"网络错误: {e.reason}", file=sys.stderr)
67
+ print(f"网络错误: {redact_error(e.reason)}", file=sys.stderr)
49
68
  sys.exit(1)
50
69
 
51
70
 
@@ -54,14 +73,14 @@ def api_get(path: str) -> dict:
54
73
  url = f"{XYQ_BASE.rstrip('/')}{path}"
55
74
  req = urllib.request.Request(url, method="GET", headers=_headers())
56
75
  try:
57
- with urllib.request.urlopen(req, timeout=HTTP_TIMEOUT_SECONDS) as resp:
76
+ with authenticated_open(req) as resp:
58
77
  return json.loads(resp.read().decode("utf-8"))
59
78
  except urllib.error.HTTPError as e:
60
79
  err_body = e.read().decode("utf-8") if e.fp else ""
61
- print(f"API 错误 {e.code}: {err_body}", file=sys.stderr)
80
+ print(f"API 错误 {e.code}: {redact_error(err_body)}", file=sys.stderr)
62
81
  sys.exit(1)
63
82
  except urllib.error.URLError as e:
64
- print(f"网络错误: {e.reason}", file=sys.stderr)
83
+ print(f"网络错误: {redact_error(e.reason)}", file=sys.stderr)
65
84
  sys.exit(1)
66
85
 
67
86
 
@@ -74,27 +93,11 @@ def parse_response(resp: dict) -> dict:
74
93
  ret = resp.get("ret", "")
75
94
  if ret != "0":
76
95
  errmsg = resp.get("errmsg", "未知错误")
77
- print(f"错误码: {ret}, 错误信息: {errmsg}", file=sys.stderr)
96
+ print(f"错误码: {redact_error(ret)}, 错误信息: {redact_error(errmsg)}", file=sys.stderr)
78
97
  sys.exit(1)
79
98
  return resp.get("data", {})
80
99
 
81
100
 
82
- def submit_run(thread_id: str = "", message: str = "", asset_ids: list = None) -> dict:
83
- """
84
- 创建会话或向已有会话发消息。
85
- 返回 data: { projectUuid, sessionId }。
86
- """
87
- body = {}
88
- if thread_id:
89
- body["thread_id"] = thread_id
90
- if message:
91
- body["message"] = message
92
- if asset_ids:
93
- body["asset_ids"] = asset_ids
94
- resp = api_post(SUBMIT_RUN_PATH, body)
95
- return parse_response(resp)
96
-
97
-
98
101
  def get_thread(thread_id: str, run_id: str = "", after_seq: int = 0) -> dict:
99
102
  """
100
103
  查询会话消息列表。
@@ -124,7 +127,7 @@ def get_thread(thread_id: str, run_id: str = "", after_seq: int = 0) -> dict:
124
127
  elif run_state == 4:
125
128
  # 失败
126
129
  fail_reason = run.get("fail_reason", "未知失败原因")
127
- print(f"错误:{fail_reason}", file=sys.stderr)
130
+ print(f"错误:{redact_error(fail_reason)}", file=sys.stderr)
128
131
  sys.exit(1)
129
132
  elif run_state == 5:
130
133
  # 取消
@@ -1,105 +0,0 @@
1
- #!/usr/bin/env python3
2
- """下载生成结果:从会话中提取所有图片/视频 URL 并批量下载到本地"""
3
-
4
- import argparse
5
- import json
6
- import os
7
- import sys
8
- import urllib.request
9
- import urllib.error
10
- from concurrent.futures import ThreadPoolExecutor, as_completed
11
-
12
- sys.path.insert(0, os.path.dirname(__file__))
13
-
14
- HTTP_TIMEOUT_SECONDS = 30 * 60
15
-
16
-
17
- def download_file(url, filepath):
18
- """下载单个文件"""
19
- import shutil
20
- req = urllib.request.Request(url, headers={"User-Agent": "XYQ-Nest-Skill/1.0"})
21
- tmp_path = filepath + ".tmp"
22
- try:
23
- with urllib.request.urlopen(req, timeout=HTTP_TIMEOUT_SECONDS) as resp:
24
- with open(tmp_path, "wb") as f:
25
- shutil.copyfileobj(resp, f, length=1024 * 1024)
26
- os.replace(tmp_path, filepath)
27
- return filepath, None
28
- except Exception as e:
29
- if os.path.exists(tmp_path):
30
- os.remove(tmp_path)
31
- return filepath, str(e)
32
-
33
-
34
- def main():
35
- parser = argparse.ArgumentParser(
36
- description="根据产物URL,下载生成的产物到本地,支持指定输出目录和文件名前缀",
37
- epilog="""
38
- 使用方式:
39
- # 直接下载指定 URL 列表
40
- python3 download_results.py --urls URL1 URL2 URL3 --output-dir ./output --prefix "storyboard"
41
- """,
42
- formatter_class=argparse.RawDescriptionHelpFormatter,
43
- )
44
- parser.add_argument("--urls", nargs="+", required=True, help="直接指定要下载的 URL 列表")
45
- parser.add_argument("--output-dir", default="", help="输出目录(默认 ./xyq_output")
46
- parser.add_argument("--prefix", default="", help="文件名前缀(如 'storyboard' → storyboard_01.png)")
47
- parser.add_argument("--workers", type=int, default=5, help="并行下载线程数(默认 5)")
48
- args = parser.parse_args()
49
-
50
- # 准备输出目录
51
- output_dir = args.output_dir or "./xyq_output"
52
- os.makedirs(output_dir, exist_ok=True)
53
-
54
- def _get_ext(url):
55
- """从 URL 中提取文件扩展名,优先从 query 的 filename 参数取,其次从路径取"""
56
- from urllib.parse import urlparse, parse_qs
57
- parsed = urlparse(url)
58
- qs = parse_qs(parsed.query)
59
- filenames = qs.get("filename", [])
60
- if filenames:
61
- _, ext = os.path.splitext(filenames[0])
62
- if ext:
63
- return ext
64
- _, ext = os.path.splitext(parsed.path)
65
- return ext or ".bin"
66
-
67
- # 构建下载任务
68
- tasks = []
69
- for i, url in enumerate(args.urls, 1):
70
- ext = _get_ext(url)
71
- if args.prefix:
72
- filename = f"{args.prefix}_{i:02d}{ext}"
73
- else:
74
- filename = f"{i:02d}{ext}"
75
- filepath = os.path.join(output_dir, filename)
76
- tasks.append((url, filepath))
77
-
78
- # 并行下载
79
- results = []
80
- errors = []
81
- with ThreadPoolExecutor(max_workers=args.workers) as pool:
82
- futures = {pool.submit(download_file, url, fp): (url, fp) for url, fp in tasks}
83
- for future in as_completed(futures):
84
- fp, err = future.result()
85
- if err:
86
- errors.append({"file": fp, "error": err})
87
- else:
88
- results.append(fp)
89
-
90
- # 按文件名排序输出
91
- results.sort()
92
-
93
- output = {
94
- "output_dir": output_dir,
95
- "downloaded": results,
96
- "total": len(results),
97
- }
98
- if errors:
99
- output["errors"] = errors
100
-
101
- print(json.dumps(output, ensure_ascii=False, indent=2))
102
-
103
-
104
- if __name__ == "__main__":
105
- main()
@@ -1,76 +0,0 @@
1
- #!/usr/bin/env python3
2
- """创建会话 / 向会话发送消息(生图、生视频等):POST /api/biz/v1/skill/submit_run"""
3
-
4
- import argparse
5
- import json
6
- import sys
7
- import os
8
-
9
- sys.path.insert(0, os.path.dirname(__file__))
10
- from xyq_common import submit_run
11
-
12
-
13
- def main():
14
- parser = argparse.ArgumentParser(
15
- description="创建会话或向已有会话发送消息(仅用于生视频)",
16
- epilog="""
17
- 环境变量:
18
- XYQ_ACCESS_KEY 必填,Bearer 鉴权
19
- XYQ_OPENAPI_BASE 或 XYQ_BASE_URL 可选,默认 https://xyq.jianying.com
20
-
21
- 示例:
22
- # 创建新会话并发送「生一个动漫视频」
23
- python3 submit_run.py --message 生一个动漫视频
24
-
25
- # 向已有会话发送消息
26
- python3 submit_run.py --message 再生成一个动漫视频 --thread-id 90f05e0c-5d08-4148-be40-e30fc7c7bedf
27
-
28
- # 传入文件资产 ID
29
- python3 submit_run.py --message 生成视频 --asset-ids asset123
30
-
31
- # 传入多个文件资产 ID
32
- python3 submit_run.py --message 生成视频 --asset-ids asset123 asset456 asset789
33
- """,
34
- formatter_class=argparse.RawDescriptionHelpFormatter,
35
- )
36
- parser.add_argument(
37
- "--message",
38
- required=True,
39
- help="要发送的消息内容(生图/生视频描述等),必填",
40
- )
41
- parser.add_argument(
42
- "--thread-id",
43
- default="",
44
- help="已有会话 ID,不传则创建新会话或返回已有默认会话",
45
- )
46
- parser.add_argument(
47
- "--asset-ids",
48
- nargs="+",
49
- default=[],
50
- help="资产 ID 列表,可传入多个,例如:--asset-ids id1 id2 id3",
51
- )
52
- args = parser.parse_args()
53
-
54
- data = submit_run(
55
- thread_id=args.thread_id or "",
56
- message=args.message or "",
57
- asset_ids=args.asset_ids if args.asset_ids else None
58
- )
59
- run_data = data.get("run", {})
60
- web_thread_link = data.get("web_thread_link", "")
61
- thread_id = run_data.get("thread_id", "")
62
- run_id = run_data.get("run_id", "")
63
-
64
- if not thread_id:
65
- print("错误:未返回 thread_id", file=sys.stderr)
66
- sys.exit(1)
67
- if not run_id:
68
- print("错误:未返回 run_id", file=sys.stderr)
69
- sys.exit(1)
70
-
71
- out = {"thread_id": thread_id, "run_id": run_id, "web_thread_link": web_thread_link}
72
- print(json.dumps(out, ensure_ascii=False, indent=2))
73
-
74
-
75
- if __name__ == "__main__":
76
- main()
@@ -1,133 +0,0 @@
1
- #!/usr/bin/env python3
2
- """上传图片/视频/mp3或wav音频到小云雀资产库:POST /api/biz/v1/skill/upload_file(multipart/form-data)"""
3
-
4
- import argparse
5
- import json
6
- import mimetypes
7
- import os
8
- import sys
9
- import uuid
10
- import urllib.request
11
- import urllib.error
12
-
13
- sys.path.insert(0, os.path.dirname(__file__))
14
- from xyq_common import XYQ_BASE, ACCESS_KEY, UPLOAD_FILE_PATH, HTTP_TIMEOUT_SECONDS, parse_response
15
-
16
- # 允许的 MIME 类型前缀
17
- ALLOWED_PREFIXES = ("image/", "video/")
18
- ALLOWED_AUDIO_EXTENSIONS = {".mp3", ".wav"}
19
- EXTRA_MIME_MAP = {
20
- ".mp3": "audio/mpeg",
21
- ".wav": "audio/wav",
22
- }
23
-
24
-
25
- def upload_file(file_path: str) -> dict:
26
- """
27
- 上传本地文件到小云雀资产库。
28
- 返回 data: { asset_id: str }。
29
- """
30
- if not os.path.isfile(file_path):
31
- print(f"错误:文件不存在: {file_path}", file=sys.stderr)
32
- sys.exit(1)
33
-
34
- # 检查 MIME 类型
35
- ext = os.path.splitext(file_path)[1].lower()
36
- mime_type, _ = mimetypes.guess_type(file_path)
37
- if not mime_type:
38
- mime_type = EXTRA_MIME_MAP.get(ext)
39
- is_supported_media = bool(mime_type) and any(mime_type.startswith(p) for p in ALLOWED_PREFIXES)
40
- is_supported_audio = bool(mime_type) and mime_type.startswith("audio/") and ext in ALLOWED_AUDIO_EXTENSIONS
41
- if not is_supported_media and not is_supported_audio:
42
- print(f"错误:不支持的文件类型: {mime_type or '未知'},仅支持图片、视频和 .mp3/.wav 音频", file=sys.stderr)
43
- sys.exit(1)
44
-
45
- # 构建 multipart/form-data 请求体
46
- boundary = f"----PythonUpload{uuid.uuid4().hex}"
47
- filename = os.path.basename(file_path)
48
-
49
- body_parts = []
50
-
51
- # accessKey 字段
52
- body_parts.append(f"--{boundary}\r\n".encode())
53
- body_parts.append(b'Content-Disposition: form-data; name="accessKey"\r\n\r\n')
54
- body_parts.append(f"{ACCESS_KEY}\r\n".encode())
55
-
56
- # file 字段
57
- content_type = mime_type or "application/octet-stream"
58
- body_parts.append(f"--{boundary}\r\n".encode())
59
- body_parts.append(
60
- f'Content-Disposition: form-data; name="file"; filename="{filename}"\r\n'.encode()
61
- )
62
- body_parts.append(f"Content-Type: {content_type}\r\n\r\n".encode())
63
- with open(file_path, "rb") as f:
64
- body_parts.append(f.read())
65
- body_parts.append(b"\r\n")
66
-
67
- # 结束边界
68
- body_parts.append(f"--{boundary}--\r\n".encode())
69
-
70
- data = b"".join(body_parts)
71
-
72
- url = f"{XYQ_BASE.rstrip('/')}{UPLOAD_FILE_PATH}"
73
- req = urllib.request.Request(
74
- url,
75
- data=data,
76
- method="POST",
77
- headers={
78
- "Authorization": f"Bearer {ACCESS_KEY}",
79
- "Content-Type": f"multipart/form-data; boundary={boundary}"
80
- },
81
- )
82
- try:
83
- with urllib.request.urlopen(req, timeout=HTTP_TIMEOUT_SECONDS) as resp:
84
- result = json.loads(resp.read().decode("utf-8"))
85
- return parse_response(result)
86
- except urllib.error.HTTPError as e:
87
- err_body = e.read().decode("utf-8") if e.fp else ""
88
- print(f"API 错误 {e.code}: {err_body}", file=sys.stderr)
89
- sys.exit(1)
90
- except urllib.error.URLError as e:
91
- print(f"网络错误: {e.reason}", file=sys.stderr)
92
- sys.exit(1)
93
-
94
-
95
- def main():
96
- parser = argparse.ArgumentParser(
97
- description="上传图片、视频或 mp3/wav 音频文件到小云雀资产库",
98
- epilog="""
99
- 环境变量:
100
- XYQ_ACCESS_KEY 必填,Bearer 鉴权
101
- XYQ_OPENAPI_BASE 或 XYQ_BASE_URL 可选,默认 https://xyq.jianying.com
102
-
103
- 示例:
104
- # 上传图片
105
- python3 upload_file.py /path/to/image.png
106
-
107
- # 上传视频
108
- python3 upload_file.py /path/to/video.mp4
109
-
110
- # 上传音频
111
- python3 upload_file.py /path/to/audio.mp3
112
- """,
113
- formatter_class=argparse.RawDescriptionHelpFormatter,
114
- )
115
- parser.add_argument(
116
- "file",
117
- help="要上传的图片、视频或 mp3/wav 音频文件路径",
118
- )
119
- args = parser.parse_args()
120
-
121
- data = upload_file(args.file)
122
- asset_id = data.get("pippit_asset_id", "")
123
-
124
- if not asset_id:
125
- print("错误:未返回 asset_id", file=sys.stderr)
126
- sys.exit(1)
127
-
128
- out = {"asset_id": asset_id}
129
- print(json.dumps(out, ensure_ascii=False, indent=2))
130
-
131
-
132
- if __name__ == "__main__":
133
- main()