Files
Mycontent/packs/wechat-miniprogram-urllink-tool/generate_share_link.py
2026-04-15 19:07:02 +08:00

237 lines
8.5 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
# -*- coding: utf-8 -*-
"""
微信小程序:生成 URL Link / Short Link官方 HTTPS 接口,无第三方依赖)。
放置位置任意;凭证读取顺序:
1) 环境变量 WECHAT_APPID、WECHAT_APPSECRET已设置则直接用
2) 环境变量 WECHAT_ENV_FILE 指向的 .env 文件
3) 与本脚本同目录的 .env 或 .env.local
4) 若本脚本仍在「一场soul的创业实验-永平」目录结构下:上一级/soul-api/.env 或 .env.production
官方文档:
URL Link: https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/qrcode-link/url-link/qrcode-link/url-link/api_generateurllink.html
Short Link: https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/qrcode-link/url-link/qrcode-link/short-link/api_generateshortlink.html
"""
from __future__ import annotations
import argparse
import json
import os
import re
import sys
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
def _script_dir() -> Path:
return Path(__file__).resolve().parent
def _load_partial_env_file(path: Path, keys: tuple[str, ...]) -> bool:
if not path.is_file():
return False
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
return bool(os.environ.get("WECHAT_APPID") and os.environ.get("WECHAT_APPSECRET"))
def ensure_wechat_env() -> None:
if os.environ.get("WECHAT_APPID") and os.environ.get("WECHAT_APPSECRET"):
return
ef = (os.environ.get("WECHAT_ENV_FILE") or "").strip()
if ef and _load_partial_env_file(Path(ef), ("WECHAT_APPID", "WECHAT_APPSECRET")):
return
base = _script_dir()
for name in (".env", ".env.local"):
if _load_partial_env_file(base / name, ("WECHAT_APPID", "WECHAT_APPSECRET")):
return
# 兼容:脚本在「仓库根/soul-api」的上一级目录下任意子目录时尝试读 soul-api/.env
repo_like = base.parent.parent
if (repo_like / "soul-api").is_dir():
for name in (".env.production", ".env"):
if _load_partial_env_file(
repo_like / "soul-api" / name, ("WECHAT_APPID", "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 _wechat_appsecret_hint(secret: str) -> str | None:
s = (secret or "").strip().strip("'").strip('"')
if re.search(r"[\u4e00-\u9fff]", s):
return (
"WECHAT_APPSECRET 中含汉字,说明把「说明文字」当成了密钥。\n"
"请到 mp.weixin.qq.com → 该小程序 → 开发管理 → 开发设置 →\n"
"「AppSecret(小程序密钥)」查看/重置后复制约 32 位密钥。"
)
if len(s) < 28:
return "WECHAT_APPSECRET 长度过短,请从公众平台复制完整 AppSecret。"
return None
def _urllink_args_placeholder_hint(path: str, query: str) -> str | None:
p, q = (path or "").strip(), (query or "").strip()
for frag in (
"真实路径",
"真实参数",
"你的文章",
"你的参数",
"pages/真实",
"id=真实",
"这里粘贴",
):
if frag in p or frag in q:
return "检测到 path/query 中含教程占位词,请换成 app.json 真实路径与 onLoad 参数。"
return None
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}"
data = _get(url)
if data.get("errcode"):
raise SystemExit(f"获取 access_token 失败: {data}")
token = data.get("access_token")
if not token:
raise SystemExit(f"无 access_token: {data}")
return token
def normalize_path(path: str) -> str:
p = (path or "").strip()
if p.startswith("/"):
p = p[1:]
return p
def cmd_urllink(token: str, path: str, query: str, env_version: str, expire_days: int) -> None:
path = normalize_path(path)
body: dict = {
"jump_wxa": {
"path": path,
"query": query or "",
"env_version": env_version,
},
"expire_type": 1,
"expire_interval": max(1, min(expire_days, 365)),
}
url = f"https://api.weixin.qq.com/wxa/generate_urllink?access_token={urllib.parse.quote(token)}"
try:
data = _post_json(url, body)
except urllib.error.HTTPError as e:
raw = e.read().decode(errors="ignore")
raise SystemExit(f"HTTP {e.code}: {raw}") from e
if data.get("errcode"):
flat = {
"path": path,
"query": query or "",
"env_version": env_version,
"expire_type": 1,
"expire_interval": max(1, min(expire_days, 365)),
}
data = _post_json(url, flat)
if data.get("errcode"):
raise SystemExit(f"generate_urllink 失败: {data}")
link = data.get("url_link")
if not link:
raise SystemExit(f"响应无 url_link: {data}")
print(link)
def cmd_shortlink(token: str, page_url: str, page_title: str, permanent: bool) -> None:
page_url_norm = page_url.strip()
if page_url_norm.startswith("/"):
page_url_norm = page_url_norm[1:]
body = {
"page_url": page_url_norm,
"page_title": page_title or "分享",
"is_permanent": permanent,
}
url = f"https://api.weixin.qq.com/wxa/genwxashortlink?access_token={urllib.parse.quote(token)}"
try:
data = _post_json(url, body)
except urllib.error.HTTPError as e:
raw = e.read().decode(errors="ignore")
raise SystemExit(f"HTTP {e.code}: {raw}") from e
if data.get("errcode"):
raise SystemExit(f"genwxashortlink 失败: {data}")
link = data.get("link")
if not link:
raise SystemExit(f"响应无 link: {data}")
print(link)
def main() -> None:
ensure_wechat_env()
ap = argparse.ArgumentParser(description="微信小程序 URL Link / Short Link 生成器")
ap.add_argument("--mode", choices=("urllink", "shortlink"), default="urllink")
ap.add_argument("--path", default="", help="urllink页面路径")
ap.add_argument("--query", default="", help="urllinkquery无则传空字符串")
ap.add_argument("--page-url", default="", help="shortlinkpage?query")
ap.add_argument("--page-title", default="", help="shortlink标题")
ap.add_argument("--env-version", choices=("release", "trial", "develop"), default="release")
ap.add_argument("--expire-days", type=int, default=30)
ap.add_argument("--permanent", action="store_true")
args = ap.parse_args()
appid = (os.environ.get("WECHAT_APPID") or "").strip()
secret = (os.environ.get("WECHAT_APPSECRET") or "").strip()
if not appid or not secret:
print(
"缺少 WECHAT_APPID / WECHAT_APPSECRET。\n"
"做法:在本目录放置 .env见 .env.example或 export 两个变量,\n"
"或设置 WECHAT_ENV_FILE=/绝对路径/你的.env",
file=sys.stderr,
)
raise SystemExit(2)
hint = _wechat_appsecret_hint(secret)
if hint:
print(hint, file=sys.stderr)
raise SystemExit(2)
if args.mode == "urllink":
if not args.path.strip():
raise SystemExit("urllink 须提供 --path")
ph = _urllink_args_placeholder_hint(args.path, args.query)
if ph:
print(ph, file=sys.stderr)
raise SystemExit(2)
token = get_access_token(appid, secret)
if args.mode == "urllink":
cmd_urllink(token, args.path, args.query, args.env_version, args.expire_days)
else:
if not args.page_url.strip():
raise SystemExit("shortlink 须提供 --page-url")
cmd_shortlink(token, args.page_url, args.page_title, args.permanent)
if __name__ == "__main__":
main()