PrimeBot 启元 Q1 & T1 技能整活工作台
MARKET PUBLISHER · 搜索/查库/上架/下架

PrimeHub 市场发版火箭筒源码精讲

基于 subprocess curl + 国密 SM4-CBC 加密的 PrimeHub 技能市场全功能客户端

😜 调皮课代表大白话划重点(30 秒读懂核心)
想把技能发上 PrimeHub 市场让全场下载?全靠这支火箭筒!因为 Python 自带的 urllib 连 PrimeHub 时会被服务端傲娇地掐断 SSL 握手(报 UNEXPECTED_EOF),所以它聪明地改派系统 curl 出马:先用 multipart 把 .rpk 传上去拿到 fileUrl,再用国密 SM4-CBC 把技能简介、机型 ID 加密打包发射到 publishSkill 接口!

🔍 核心机关与修改指南

🔍 匿名搜索与鉴权自查 search & my

search 命令无需读取本地 Token 就能搜全网技能;my 命令调用经实测验证的 queryPageByUserCode 查自家在架列表。

🚀 双模式发版参数 --metadata-file

既支持直接传 JSON 字符串,也支持传 --metadata-file Tools/dist/publish_universal_q1.json,跨平台敲命令再也不怕引号转义报错!

💻 完整源码与配置内容(共 127 行 · 支持行号定位与一键复制)

📄 共 127 行
1#!/usr/bin/env python3
2"""PrimeHub 发布客户端(curl transport 版)
3
42026-10-19 实测:本机 python urllib 对 primehub SSL 握手会被服务端掐断
5(UNEXPECTED_EOF_WHILE_READING),curl 正常 → 全链路改走 subprocess curl。
6
7关键端点(均 https://primehub.primebot.com):
8 POST /app-api/content-server/file/upload multipart 明文 + Bearer → fileUrl
9 POST /app-api/content-server/skillCenter/publishSkill SM4 加密体(本模块 call())
10 POST /app-api/search-server/portal/querySkillList SM4 加密体(免登录可查)
11 · 参数名是 PC 端约定:keywords / queryType / page / rows(pageNum/pageSize 会 9999)
12 POST /app-api/content-server/skillCenter/queryPageByUserCode 我的发布列表
13
14用法:
15 python3 primehub_publish.py search '<keywords>'
16 python3 primehub_publish.py publish <rpk路径> '<publishJson>'
17"""
18import json, uuid, time, subprocess, urllib.parse, sys, os
19import argparse
20from pathlib import Path
21
22sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
23from primehub_client import KEY, IV # SM4 key/iv + unscramble
24from extract_token import DEFAULT_TOKEN_FILE
25from gmssl.sm4 import CryptSM4, SM4_ENCRYPT, SM4_DECRYPT
26
27BASE = "https://primehub.primebot.com"
28TOKEN_FILE = DEFAULT_TOKEN_FILE # 提取器与发布器共用 workspace/.local,支持 PRIMEHUB_TOKEN_FILE
29
30
31def call(path, params, token=None, ns="content-server"):
32 params = dict(params); params["timestamp"] = int(time.time() * 1000)
33 params["requestId"] = str(uuid.uuid4())
34 inner = urllib.parse.quote(json.dumps(params, separators=(",", ":")), safe="")
35 c = CryptSM4(); c.set_key(KEY, SM4_ENCRYPT)
36 ct = c.crypt_cbc(IV, inner.encode()).hex()
37 result = subprocess.run(
38 ["curl", "-sS", "--max-time", "40", "-X", "POST",
39 BASE + "/app-api/" + ns + "/" + path,
40 "-H", "Content-Type: application/json", "-H", "request-t: true",
41 "-H", "Origin: " + BASE, "-H", "Referer: " + BASE + "/",
42 ] + (["-H", "Authorization: Bearer " + token] if token else [])
43 + ["-d", json.dumps({"data": ct})],
44 capture_output=True, text=True, encoding="utf-8")
45 if result.returncode:
46 raise RuntimeError("PrimeHub 网络请求失败,curl exit=%d" % result.returncode)
47 d = json.loads(result.stdout)
48 if isinstance(d.get("data"), str) and d.get("code") == 1000:
49 c2 = CryptSM4(); c2.set_key(KEY, SM4_DECRYPT)
50 d["data"] = json.loads(urllib.parse.unquote(
51 c2.crypt_cbc(IV, bytes.fromhex(d["data"])).decode()))
52 return d
53
54
55def upload(rpk_path, token):
56 boundary = "----" + uuid.uuid4().hex
57 fn = os.path.basename(rpk_path)
58 part = ("--" + boundary + "\r\nContent-Disposition: form-data; name=\"file\"; "
59 "filename=\"" + fn + "\"\r\nContent-Type: application/octet-stream\r\n\r\n")
60 body = part.encode() + open(rpk_path, "rb").read() + ("\r\n--" + boundary + "--\r\n").encode()
61 out = subprocess.run(
62 ["curl", "-s", "--max-time", "120", "-X", "POST",
63 BASE + "/app-api/content-server/file/upload",
64 "-H", "Content-Type: multipart/form-data; boundary=" + boundary,
65 "-H", "Authorization: Bearer " + token, "-H", "request-t: true",
66 "-H", "Origin: " + BASE, "-H", "Referer: " + BASE + "/",
67 "--data-binary", "@-"], input=body, capture_output=True).stdout.decode()
68 d = json.loads(out)
69 if isinstance(d.get("data"), str):
70 c = CryptSM4(); c.set_key(KEY, SM4_DECRYPT)
71 d["data"] = json.loads(urllib.parse.unquote(
72 c.crypt_cbc(IV, bytes.fromhex(d["data"])).decode()))
73 return d["data"]["fileUrl"]
74
75
76def my_skills(token):
77 r = call("skillCenter/queryPageByUserCode", {"page": 1, "rows": 50}, token)
78 if r.get("code") != 1000:
79 raise RuntimeError("PrimeHub 我的技能查询失败:%s %s" % (r.get("code"), r.get("msg")))
80 return (r.get("data") or {}).get("records") or []
81
82
83def delete(skill_id, token):
84 return call("skillCenter/deleteSkillById", {"id": skill_id}, token)
85
86
87def search(keywords, token=None):
88 r = call("portal/querySkillList", {"keywords": keywords, "queryType": 1, "page": 1, "rows": 30},
89 token, ns="search-server")
90 if r.get("code") != 1000:
91 raise RuntimeError("PrimeHub 搜索失败:%s %s" % (r.get("code"), r.get("msg")))
92 recs = (r.get("data") or {}).get("records") or []
93 return [{"id": s.get("id"), "name": s.get("skillName"), "models": s.get("robotModels")} for s in recs]
94
95
96def main():
97 parser = argparse.ArgumentParser(description="PrimeHub 搜索、发布和管理")
98 commands = parser.add_subparsers(dest="command", required=True)
99 commands.add_parser("search").add_argument("keywords")
100 commands.add_parser("my")
101 commands.add_parser("delete").add_argument("skill_id", type=int)
102 publish_parser = commands.add_parser("publish")
103 publish_parser.add_argument("rpk")
104 metadata = publish_parser.add_mutually_exclusive_group(required=True)
105 metadata.add_argument("publish_json", nargs="?")
106 metadata.add_argument("--metadata-file", type=Path)
107 args = parser.parse_args()
108
109 if args.command == "search":
110 for s in search(args.keywords):
111 print(s["id"], s["name"], s["models"])
112 return
113 token = Path(TOKEN_FILE).read_text(encoding="utf-8").strip()
114 if args.command == "my":
115 for s in my_skills(token):
116 print(s.get("id"), s.get("skillName"))
117 elif args.command == "delete":
118 print(delete(args.skill_id, token))
119 elif args.command == "publish":
120 meta = json.loads(args.metadata_file.read_text(encoding="utf-8")
121 if args.metadata_file else args.publish_json)
122 meta["packageUrl"] = upload(args.rpk, token)
123 print(call("skillCenter/publishSkill", meta, token))
124
125
126if __name__ == "__main__":
127 main()