fix: 上传二维码并发送 Ad Hoc 通知

This commit is contained in:
shenlei
2026-07-20 15:22:44 +09:00
parent 8c0bce45a4
commit 9b77441ec7
6 changed files with 190 additions and 5 deletions
+18
View File
@@ -24,6 +24,7 @@ from ..config import (
)
from .log_streamer import log_streamer
from .distribution import DistributionError, publish_ipa
from .notification import NotificationError, send_dingtalk_notification
from .project_patcher import ProjectPatchError, apply_project_config
@@ -444,6 +445,23 @@ async def run_build_task(task_id: str):
if task.oss_url:
await log_streamer.emit(task_id, f"下载链接: {task.oss_url}")
# App Store 仅提供 IPA 下载地址,不发送下载通知;Ad Hoc 才发送安装页二维码通知。
if task.build_type == "Ad_Hoc":
# 通知不影响已完成的打包结果,发送失败仅写入日志以便排查。
dingtalk_config = config_data.get("_upload_config", {}).get("dingtalk", {})
try:
notified = await asyncio.to_thread(
send_dingtalk_notification,
dingtalk_config,
config_data,
task.oss_url or oss_url,
task.qr_code_path or qr_code_path,
)
if notified:
await log_streamer.emit(task_id, "钉钉通知发送成功")
except NotificationError as exc:
await log_streamer.emit(task_id, f"钉钉通知未发送: {exc}", level="warn")
# 带超时执行打包
await asyncio.wait_for(_do_build(), timeout=timeout_seconds)
+6 -4
View File
@@ -58,7 +58,7 @@ def _write_download_page(config: dict, html: Path, manifest_url: str):
)
def _remote_paths(config: dict) -> tuple[str, str, str]:
def _remote_paths(config: dict) -> tuple[str, str, str, str]:
stem = _artifact_stem(config)
folder = config.get("OSS_FLODER", "ios-builds").strip("/") or "ios-builds"
root = posixpath.join(folder, "iOS")
@@ -66,6 +66,7 @@ def _remote_paths(config: dict) -> tuple[str, str, str]:
posixpath.join(root, f"{stem}.ipa"),
posixpath.join(root, f"{stem}.plist"),
posixpath.join(root, f"{stem}.html"),
posixpath.join(root, f"{stem}.png"),
)
@@ -127,7 +128,7 @@ def _upload_oss(config: dict, files: list[tuple[Path, str]]) -> dict[str, str]:
def publish_ipa(config: dict, ipa_path: Path, build_dir: Path) -> tuple[str, str]:
"""发布 IPA,返回下载 URL 与二维码本地路径"""
"""发布 IPA,返回下载 URL 与二维码公开 URL"""
upload = config.get("_upload_config", {})
mode = upload.get("mode", "")
if mode not in {"oss", "webdav"}:
@@ -135,7 +136,7 @@ def publish_ipa(config: dict, ipa_path: Path, build_dir: Path) -> tuple[str, str
output_dir = build_dir / "distribution"
ipa_file, manifest, html = _write_distribution_files(config, ipa_path, output_dir)
ipa_remote, manifest_remote, html_remote = _remote_paths(config)
ipa_remote, manifest_remote, html_remote, qr_remote = _remote_paths(config)
uploader = _upload_oss if mode == "oss" else _upload_webdav
@@ -157,4 +158,5 @@ def publish_ipa(config: dict, ipa_path: Path, build_dir: Path) -> tuple[str, str
except ImportError as exc:
raise DistributionError("未安装 qrcode,请重新执行 ./deploy.sh build") from exc
qrcode.make(urls[".html"]).save(qr_path)
return urls[".html"], str(qr_path)
urls.update(uploader(upload, [(qr_path, qr_remote)]))
return urls[".html"], urls[".png"]
+80
View File
@@ -0,0 +1,80 @@
"""打包完成后的钉钉通知。"""
import base64
import hashlib
import hmac
import time
from urllib.parse import quote
import httpx
class NotificationError(Exception):
"""通知发送失败。"""
def _signed_webhook_url(webhook_url: str, secret: str) -> str:
"""为启用了加签的钉钉机器人附加 timestamp 与 sign 参数。"""
if not secret:
return webhook_url
timestamp = str(round(time.time() * 1000))
signature = hmac.new(
secret.encode("utf-8"),
f"{timestamp}\n{secret}".encode("utf-8"),
hashlib.sha256,
).digest()
separator = "&" if "?" in webhook_url else "?"
return f"{webhook_url}{separator}timestamp={timestamp}&sign={quote(base64.b64encode(signature))}"
def build_dingtalk_payload(config_data: dict, download_url: str, qr_code_url: str = "") -> dict:
"""生成与 AutoPacking/upload_iap.py 一致的钉钉 Markdown 内容。"""
details = (
f"**环境:** {config_data.get('SERVER', '')}\n\n"
f"**版本:** {config_data.get('VERSION', '')}\n\n"
f"**APP名称:** {config_data.get('APPID_NAME', '')}\n\n"
f"**包名:** {config_data.get('BUNDLE_ID', '')}\n\n"
f"**App Guid** {config_data.get('APPID', '')}"
)
text = f"## 【iOS】打包信息\n\n{details}\n\n**iOS 下载链接:** {download_url}\n"
if qr_code_url:
text += f"\n![image]({qr_code_url})"
return {
"msgtype": "markdown",
"markdown": {"title": "iOS应用下载", "text": text},
"at": {"atMobiles": [], "isAtAll": False},
}
def send_dingtalk_notification(
dingtalk_config: dict,
config_data: dict,
download_url: str,
qr_code_url: str = "",
) -> bool:
"""发送钉钉通知;未启用通知时不发起网络请求。"""
if not dingtalk_config.get("enabled"):
return False
webhook_url = dingtalk_config.get("webhook_url", "").strip()
if not webhook_url:
raise NotificationError("钉钉通知已启用,但未配置 Webhook URL")
payload = build_dingtalk_payload(config_data, download_url, qr_code_url)
payload["at"]["atMobiles"] = dingtalk_config.get("at_mobiles", [])
response = httpx.post(
_signed_webhook_url(webhook_url, dingtalk_config.get("secret", "")),
json=payload,
timeout=15,
)
if response.status_code // 100 != 2:
raise NotificationError(f"钉钉通知发送失败 ({response.status_code})")
try:
result = response.json()
except ValueError as exc:
raise NotificationError("钉钉通知返回内容无效") from exc
if result.get("errcode", 0) != 0:
raise NotificationError(f"钉钉通知发送失败: {result.get('errmsg', '未知错误')}")
return True