Files
soul-yongping/scripts/wechat_miniprogram_release.py
卡若 5724fba877 feat: 小程序超级个体/个人资料/CKB获客;VIP列表展示过滤;管理端与API联调
- 超级个体:去掉首位特例;列表仅展示有头像且非微信默认昵称(vip.go)
- 个人资料:居中头像、低调联系方式、点头像优先走存客宝 lead(ckbLeadToken)
- 阅读页分享朋友圈复制与 toast 去重
- soul-api: miniprogram users 带 ckbLeadToken;其它 handler 与路由调整
- 脚本:content_upload、miniprogram 上传辅助等

Made-with: Cursor
2026-03-22 08:34:28 +08:00

369 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
微信小程序发布辅助:上传(调开发者工具 CLI+ 审核状态查询 + 尝试 API 提审。
重要说明(微信官方限制):
- 代码上传可用本机「微信开发者工具」CLI 或 miniprogram-ci。
- submit_audit开放平台文档标明主要为「第三方平台代调用」自有主体使用小程序 appid+secret
换取的 access_token 调用时,常见返回 errcode=86000仅允许第三方代调用此时必须在
mp 后台手动点「提交审核」。
- 「自动过审」不可能由开发者脚本保证:是否通过由微信审核决定。
"""
from __future__ import annotations
import argparse
import json
import os
import subprocess
import sys
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
# 登录 mp 后进入「版本管理 / 开发版本」列表(选体验版、提交审核均在此页操作;具体路由以微信后台为准)。
MP_VERSION_MANAGE_URL = (
"https://mp.weixin.qq.com/wxopen/wacodepage?action=getcodepage&lang=zh_CN"
)
def _repo_root() -> Path:
return Path(__file__).resolve().parent.parent
def _load_partial_env_file(path: Path, keys: tuple[str, ...]) -> None:
if not path.is_file():
return
for raw in path.read_text(encoding="utf-8", errors="ignore").splitlines():
line = raw.strip()
if not line or line.startswith("#") or "=" not in line:
continue
k, v = line.split("=", 1)
k, v = k.strip(), v.strip().strip('"').strip("'")
if k in keys and not os.environ.get(k):
os.environ[k] = v
def ensure_wechat_env_from_soul_api() -> None:
"""若未 export 凭证,则从 soul-api/.env.production 或 .env 读取 WECHAT_APPID/SECRET不写日志"""
if os.environ.get("WECHAT_APPID") and os.environ.get("WECHAT_APPSECRET"):
return
root = _repo_root()
for name in (".env.production", ".env"):
_load_partial_env_file(
root / "soul-api" / name, ("WECHAT_APPID", "WECHAT_APPSECRET")
)
if os.environ.get("WECHAT_APPID") and os.environ.get("WECHAT_APPSECRET"):
return
def _get(url: str) -> dict:
req = urllib.request.Request(url, method="GET")
with urllib.request.urlopen(req, timeout=60) as resp:
return json.loads(resp.read().decode())
def _post_json(url: str, body: dict) -> dict:
data = json.dumps(body, ensure_ascii=False).encode("utf-8")
req = urllib.request.Request(
url,
data=data,
method="POST",
headers={"Content-Type": "application/json; charset=utf-8"},
)
with urllib.request.urlopen(req, timeout=60) as resp:
return json.loads(resp.read().decode())
def get_access_token(appid: str, secret: str) -> str:
q = urllib.parse.urlencode(
{"grant_type": "client_credential", "appid": appid, "secret": secret}
)
url = f"https://api.weixin.qq.com/cgi-bin/token?{q}"
try:
data = _get(url)
except urllib.error.HTTPError as e:
raise SystemExit(f"获取 access_token HTTP 错误: {e}") from e
if data.get("errcode"):
raise SystemExit(f"获取 access_token 失败: {data}")
token = data.get("access_token")
if not token:
raise SystemExit(f"获取 access_token 无 access_token 字段: {data}")
return token
def cmd_audit_status(appid: str, secret: str) -> None:
token = get_access_token(appid, secret)
url = f"https://api.weixin.qq.com/wxa/get_latest_auditstatus?access_token={urllib.parse.quote(token)}"
try:
data = _get(url)
except urllib.error.HTTPError as e:
raise SystemExit(f"get_latest_auditstatus HTTP 错误: {e}") from e
print(json.dumps(data, ensure_ascii=False, indent=2))
# 常见 status: 0 审核成功 1 审核被拒绝 2 审核中 3 已撤回 4 审核延后
def cmd_get_category(appid: str, secret: str) -> None:
token = get_access_token(appid, secret)
url = f"https://api.weixin.qq.com/wxa/get_category?access_token={urllib.parse.quote(token)}"
try:
data = _get(url)
except urllib.error.HTTPError as e:
raise SystemExit(f"get_category HTTP 错误: {e}") from e
print(json.dumps(data, ensure_ascii=False, indent=2))
def _first_item_from_category(data: dict) -> dict | None:
lst = data.get("category_list")
if not lst or not isinstance(lst, list):
return None
c = lst[0]
if not isinstance(c, dict):
return None
first_class = c.get("first_class") or ""
second_class = c.get("second_class") or ""
first_id = c.get("first_id")
second_id = c.get("second_id")
if first_id is None or second_id is None:
return None
item: dict = {
"address": "pages/index/index",
"tag": "阅读 创业",
"first_class": first_class,
"second_class": second_class,
"first_id": int(first_id),
"second_id": int(second_id),
"title": "首页",
}
third_class = c.get("third_class")
third_id = c.get("third_id")
if third_class and third_id:
item["third_class"] = third_class
item["third_id"] = int(third_id)
return item
def cmd_submit_audit(
appid: str,
secret: str,
version_desc: str,
item_json: Path | None,
privacy_api_not_use: bool | None,
) -> dict:
token = get_access_token(appid, secret)
if item_json and item_json.is_file():
payload = json.loads(item_json.read_text(encoding="utf-8"))
item_list = payload.get("item_list")
if not item_list:
raise SystemExit("item_json 中缺少 item_list")
else:
cat_url = f"https://api.weixin.qq.com/wxa/get_category?access_token={urllib.parse.quote(token)}"
cat = _get(cat_url)
if cat.get("errcode"):
raise SystemExit(f"get_category 失败: {cat}")
one = _first_item_from_category(cat)
if not one:
raise SystemExit(
"无法从 get_category 构造审核项;请在公众平台配置服务类目,"
"或使用 --item-json 指定完整 item_list见 scripts/miniprogram_audit_item.example.json"
)
item_list = [one]
body: dict = {
"item_list": item_list,
"version_desc": version_desc[:400] if version_desc else "版本更新",
}
if privacy_api_not_use is True:
body["privacy_api_not_use"] = True
elif privacy_api_not_use is False:
body["privacy_api_not_use"] = False
submit_url = f"https://api.weixin.qq.com/wxa/submit_audit?access_token={urllib.parse.quote(token)}"
try:
data = _post_json(submit_url, body)
except urllib.error.HTTPError as e:
raise SystemExit(f"submit_audit HTTP 错误: {e}") from e
print(json.dumps(data, ensure_ascii=False, indent=2))
if data.get("errcode") == 86000:
print(
"\n说明 errcode=86000该接口仅支持「第三方平台」代小程序调用。"
"自有主体请在浏览器打开公众平台 → 管理 → 版本管理 → 提交审核。\n"
"可先运行: python3 scripts/wechat_miniprogram_release.py open-mp",
file=sys.stderr,
)
elif data.get("errcode") == 61039:
print(
"\n说明 errcode=61039上传后隐私/代码检测任务未完成,请等待数分钟后再提交审核。",
file=sys.stderr,
)
return data
def cmd_upload(version: str, desc: str) -> None:
root = Path(__file__).resolve().parent.parent
sh = root / "scripts" / "miniprogram_upload.sh"
if not sh.is_file():
raise SystemExit(f"未找到 {sh}")
r = subprocess.run([str(sh), version, desc], check=False)
if r.returncode != 0:
raise SystemExit(r.returncode)
def cmd_open_mp() -> None:
url = "https://mp.weixin.qq.com/"
try:
subprocess.run(["open", url], check=False)
except FileNotFoundError:
print(url)
def cmd_open_mp_version() -> None:
"""打开版本管理(开发版本列表,可设体验版、提交审核)。"""
try:
subprocess.run(["open", MP_VERSION_MANAGE_URL], check=False)
except FileNotFoundError:
print(MP_VERSION_MANAGE_URL)
def cmd_upload_open(version: str, desc: str) -> None:
cmd_upload(version, desc)
print(
"\n下一步微信未开放「一键设为体验版」API需在网页上点两次\n"
"1在「开发版本」列表找到刚上传的版本号\n"
"2点击「选为体验版」\n"
"3同一列表对该版本点击「提交审核」或先体验再提审\n",
file=sys.stderr,
)
cmd_open_mp_version()
def main() -> None:
p = argparse.ArgumentParser(description="微信小程序上传与审核辅助")
sub = p.add_subparsers(dest="cmd", required=True)
p_up = sub.add_parser("upload", help="调用微信开发者工具 CLI 上传(需本机已登录)")
p_up.add_argument(
"--version",
"-v",
default=os.environ.get("MINIPROGRAM_DEFAULT_VERSION", "1.7.1"),
help="版本号,默认 1.7.1 或环境变量 MINIPROGRAM_DEFAULT_VERSION",
)
p_up.add_argument(
"--desc",
"-d",
default="",
help="版本说明,默认同「版本 v<版本号>」",
)
p_st = sub.add_parser("audit-status", help="查询最近一次审核状态(需 WECHAT_APPID/SECRET")
p_cat = sub.add_parser("get-category", help="拉取已配置服务类目 JSON构造提审项用")
p_sa = sub.add_parser(
"submit-audit",
help="尝试调用 submit_audit自有主体常会 86000需改 mp 后台手动提审)",
)
p_sa.add_argument("--version-desc", default="版本更新", help="审核说明 version_desc")
p_sa.add_argument(
"--item-json",
type=Path,
help="自定义 item_list 的 JSON 文件(含 item_list 数组)",
)
p_sa.add_argument(
"--privacy-api-not-use",
action=argparse.BooleanOptionalAction,
default=None,
help="是否声明未使用检测到的隐私接口(与微信报错 61040 相关时再用)",
)
sub.add_parser("open-mp", help="在浏览器打开 mp 首页")
sub.add_parser(
"open-version",
help="打开版本管理页(开发版本:选体验版、提交审核;需已登录 mp",
)
p_uo = sub.add_parser(
"upload-open",
help="上传代码并打开版本管理页(设体验版、提审在浏览器完成)",
)
p_uo.add_argument(
"--version",
"-v",
default=os.environ.get("MINIPROGRAM_DEFAULT_VERSION", "1.7.1"),
)
p_uo.add_argument("--desc", "-d", default="", help="默认:版本 v<版本号>")
p_rel = sub.add_parser("release", help="先 upload 再 submit-audit提审失败仍可到后台操作")
p_rel.add_argument(
"--version",
"-v",
default=os.environ.get("MINIPROGRAM_DEFAULT_VERSION", "1.7.1"),
)
p_rel.add_argument("--desc", "-d", default="", help="上传说明,默认:版本 v<版本号>")
p_rel.add_argument("--version-desc", default="", help="提交审核说明,默认同上传说明")
p_rel.add_argument("--item-json", type=Path)
p_rel.add_argument(
"--privacy-api-not-use",
action=argparse.BooleanOptionalAction,
default=None,
)
args = p.parse_args()
if args.cmd in (
"audit-status",
"get-category",
"submit-audit",
"release",
):
ensure_wechat_env_from_soul_api()
appid = os.environ.get("WECHAT_APPID", "").strip()
secret = os.environ.get("WECHAT_APPSECRET", "").strip()
if args.cmd == "upload":
desc = args.desc.strip() or f"版本 v{args.version}"
cmd_upload(args.version, desc)
return
if args.cmd == "open-mp":
cmd_open_mp()
return
if args.cmd == "open-version":
cmd_open_mp_version()
return
if args.cmd == "upload-open":
d = args.desc.strip() or f"版本 v{args.version}"
cmd_upload_open(args.version, d)
return
if args.cmd == "release":
d = args.desc.strip() or f"版本 v{args.version}"
cmd_upload(args.version, d)
if not appid or not secret:
print(
"未设置 WECHAT_APPID / WECHAT_APPSECRET跳过 submit-audit。",
file=sys.stderr,
)
cmd_open_mp_version()
return
vd = (args.version_desc or "").strip() or d
res = cmd_submit_audit(
appid, secret, vd, args.item_json, args.privacy_api_not_use
)
if res.get("errcode") == 86000:
cmd_open_mp_version()
return
if not appid or not secret:
raise SystemExit("请设置环境变量 WECHAT_APPID、WECHAT_APPSECRET与 soul-api 一致即可)")
if args.cmd == "audit-status":
cmd_audit_status(appid, secret)
elif args.cmd == "get-category":
cmd_get_category(appid, secret)
elif args.cmd == "submit-audit":
cmd_submit_audit(
appid, secret, args.version_desc, args.item_json, args.privacy_api_not_use
)
if __name__ == "__main__":
main()