diff --git a/backend/routers/config.py b/backend/routers/config.py index 5abc917..e8d90a2 100644 --- a/backend/routers/config.py +++ b/backend/routers/config.py @@ -58,6 +58,8 @@ DEFAULT_UPLOAD = { "enabled": False, "webhook_url": "", "secret": "", + "at_mobiles": [], + "at_mobiles_app_store": [], }, } diff --git a/backend/services/build_service.py b/backend/services/build_service.py index 952cadf..b7888bc 100644 --- a/backend/services/build_service.py +++ b/backend/services/build_service.py @@ -498,22 +498,22 @@ 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") + # Ad Hoc 发安装页二维码通知,App Store 发 IPA 下载地址通知; + # 两者标题与 @ 名单不同,均由 send_dingtalk_notification 按 BUILD_TYPE 区分。 + # 通知不影响已完成的打包结果,发送失败仅写入日志以便排查。 + 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) diff --git a/backend/services/notification.py b/backend/services/notification.py index b89e40d..5ebbee6 100644 --- a/backend/services/notification.py +++ b/backend/services/notification.py @@ -27,8 +27,32 @@ def _signed_webhook_url(webhook_url: str, secret: str) -> str: 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: +def _normalize_mobiles(at_mobiles) -> list: + """把手机号配置统一成去重、去空的字符串列表,兼容列表或逗号/换行分隔的字符串。""" + if not at_mobiles: + return [] + if isinstance(at_mobiles, str): + raw = at_mobiles.replace("\n", ",").replace(",", ",").replace(" ", ",").split(",") + else: + raw = at_mobiles + seen = [] + for item in raw: + mobile = str(item).strip() + if mobile and mobile not in seen: + seen.append(mobile) + return seen + + +def build_dingtalk_payload( + config_data: dict, + download_url: str, + qr_code_url: str = "", + at_mobiles=None, +) -> dict: """生成与 AutoPacking/upload_iap.py 一致的钉钉 Markdown 内容。""" + mobiles = _normalize_mobiles(at_mobiles) + # App_Store 包与 Ad Hoc 用不同标题,App_Store 只有 IPA 下载地址、无二维码。 + heading = "【iOS】App_Store 包信息" if config_data.get("BUILD_TYPE") == "App_Store" else "【iOS】打包信息" details = ( f"**环境:** {config_data.get('SERVER', '')}\n\n" f"**版本:** {config_data.get('VERSION', '')}\n\n" @@ -36,14 +60,18 @@ def build_dingtalk_payload(config_data: dict, download_url: str, qr_code_url: st 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" + text = f"## {heading}\n\n{details}\n\n**iOS 下载链接:** {download_url}\n" if qr_code_url: text += f"\n" + # 钉钉 markdown 消息必须在正文内出现 @手机号 才会真正 @ 到人, + # 仅靠 at.atMobiles 不生效。 + if mobiles: + text += "\n\n" + " ".join(f"@{m}" for m in mobiles) return { "msgtype": "markdown", "markdown": {"title": "iOS应用下载", "text": text}, - "at": {"atMobiles": [], "isAtAll": False}, + "at": {"atMobiles": mobiles, "isAtAll": False}, } @@ -61,8 +89,11 @@ def send_dingtalk_notification( 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", []) + # Ad_Hoc 与 App_Store 使用各自的 @ 名单。 + mobiles_key = "at_mobiles_app_store" if config_data.get("BUILD_TYPE") == "App_Store" else "at_mobiles" + payload = build_dingtalk_payload( + config_data, download_url, qr_code_url, dingtalk_config.get(mobiles_key, []) + ) response = httpx.post( _signed_webhook_url(webhook_url, dingtalk_config.get("secret", "")), json=payload, diff --git a/config.example.json b/config.example.json index 4df4711..b8e2b38 100644 --- a/config.example.json +++ b/config.example.json @@ -67,7 +67,9 @@ "dingtalk": { "enabled": false, "webhook_url": "", - "secret": "" + "secret": "", + "at_mobiles": [], + "at_mobiles_app_store": [] } }, "versions": { diff --git a/frontend/src/views/ConfigView.vue b/frontend/src/views/ConfigView.vue index 4976769..7b1b426 100644 --- a/frontend/src/views/ConfigView.vue +++ b/frontend/src/views/ConfigView.vue @@ -268,6 +268,16 @@ +