feat: App_Store 打包也发钉钉通知,Ad_Hoc/App_Store 各自的 @ 名单
- App_Store 完成后同样发送钉钉通知:标题改为「【iOS】App_Store 包信息」, 下载链接为 IPA 地址,不带二维码。 - 钉钉 markdown 消息需在正文出现 @手机号 才会真正 @ 到人,故除 at.atMobiles 外同时在正文追加。 - 新增 upload.dingtalk.at_mobiles(Ad_Hoc)与 at_mobiles_app_store(App_Store) 两份名单,按 BUILD_TYPE 选用;配置页「钉钉通知」区新增两个输入框。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YZULZ8fkiaiJrxqf318xTn
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
fa559101b9
commit
9ace2c8aca
@@ -58,6 +58,8 @@ DEFAULT_UPLOAD = {
|
||||
"enabled": False,
|
||||
"webhook_url": "",
|
||||
"secret": "",
|
||||
"at_mobiles": [],
|
||||
"at_mobiles_app_store": [],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -498,8 +498,8 @@ 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":
|
||||
# Ad Hoc 发安装页二维码通知,App Store 发 IPA 下载地址通知;
|
||||
# 两者标题与 @ 名单不同,均由 send_dingtalk_notification 按 BUILD_TYPE 区分。
|
||||
# 通知不影响已完成的打包结果,发送失败仅写入日志以便排查。
|
||||
dingtalk_config = config_data.get("_upload_config", {}).get("dingtalk", {})
|
||||
try:
|
||||
|
||||
@@ -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,
|
||||
|
||||
+3
-1
@@ -67,7 +67,9 @@
|
||||
"dingtalk": {
|
||||
"enabled": false,
|
||||
"webhook_url": "",
|
||||
"secret": ""
|
||||
"secret": "",
|
||||
"at_mobiles": [],
|
||||
"at_mobiles_app_store": []
|
||||
}
|
||||
},
|
||||
"versions": {
|
||||
|
||||
@@ -268,6 +268,16 @@
|
||||
<label>加签密钥(可选)</label>
|
||||
<input v-model="uploadConfig.dingtalk.secret" type="password" placeholder="留空则不加签">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Ad_Hoc @ 开发人员手机号(可选)</label>
|
||||
<textarea v-model="dingtalkAtMobiles" rows="3" placeholder="每行一个手机号,或用逗号分隔;留空则不 @ 人"></textarea>
|
||||
<div style="font-size: 12px; color: #999; margin-top: 4px;">须是钉钉账号绑定的手机号,Ad_Hoc 打包完成的通知会 @ 这些人。</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>App_Store @ 开发人员手机号(可选)</label>
|
||||
<textarea v-model="dingtalkAtMobilesAppStore" rows="3" placeholder="每行一个手机号,或用逗号分隔;留空则不 @ 人"></textarea>
|
||||
<div style="font-size: 12px; color: #999; margin-top: 4px;">App_Store 打包完成的通知会 @ 这些人。</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -673,6 +683,21 @@ const onAppVerInput = (track) => {
|
||||
const uploadConfig = ref({ mode: 'oss', oss: {}, webdav: {}, dingtalk: {} })
|
||||
const jsonContent = ref('{}')
|
||||
|
||||
// 钉钉 @ 手机号:配置里存字符串数组,输入框按逗号/换行分隔编辑。
|
||||
// Ad_Hoc 与 App_Store 各一份名单。
|
||||
const dingtalkAtMobilesModel = (key) => computed({
|
||||
get: () => (uploadConfig.value.dingtalk?.[key] || []).join('\n'),
|
||||
set: (val) => {
|
||||
if (!uploadConfig.value.dingtalk) uploadConfig.value.dingtalk = {}
|
||||
uploadConfig.value.dingtalk[key] = val
|
||||
.split(/[\s,,]+/)
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean)
|
||||
},
|
||||
})
|
||||
const dingtalkAtMobiles = dingtalkAtMobilesModel('at_mobiles')
|
||||
const dingtalkAtMobilesAppStore = dingtalkAtMobilesModel('at_mobiles_app_store')
|
||||
|
||||
const showServerModal = ref(false)
|
||||
const editingServerName = ref(null)
|
||||
const serverForm = ref({ name: '', api: '', assDom: '', universalLink: '', app_id_prefix: null })
|
||||
|
||||
@@ -35,6 +35,79 @@ def test_dingtalk_payload_matches_autopacking_content():
|
||||
assert "" in text
|
||||
|
||||
|
||||
def test_dingtalk_payload_at_mobiles_appended_to_text_and_at_field():
|
||||
payload = build_dingtalk_payload(
|
||||
_build_config(),
|
||||
"https://download.example.com/app.html",
|
||||
at_mobiles=["13800000000", " 13900000001 ", "13800000000", ""],
|
||||
)
|
||||
|
||||
# 去重去空后写入 at.atMobiles,并在正文追加 @手机号(钉钉 markdown 必需)
|
||||
assert payload["at"]["atMobiles"] == ["13800000000", "13900000001"]
|
||||
assert "@13800000000" in payload["markdown"]["text"]
|
||||
assert "@13900000001" in payload["markdown"]["text"]
|
||||
|
||||
|
||||
def test_dingtalk_payload_without_at_mobiles_has_empty_at():
|
||||
payload = build_dingtalk_payload(_build_config(), "https://download.example.com/app.html")
|
||||
assert payload["at"]["atMobiles"] == []
|
||||
assert "@" not in payload["markdown"]["text"]
|
||||
|
||||
|
||||
def test_dingtalk_notification_forwards_at_mobiles():
|
||||
response = MagicMock(status_code=200)
|
||||
response.json.return_value = {"errcode": 0}
|
||||
with patch("backend.services.notification.httpx.post", return_value=response) as post:
|
||||
send_dingtalk_notification(
|
||||
{
|
||||
"enabled": True,
|
||||
"webhook_url": "https://example.com/robot",
|
||||
"at_mobiles": ["13800000000", "13900000001"],
|
||||
},
|
||||
_build_config(),
|
||||
"https://download.example.com/app.html",
|
||||
)
|
||||
|
||||
sent = post.call_args.kwargs["json"]
|
||||
assert sent["at"]["atMobiles"] == ["13800000000", "13900000001"]
|
||||
assert "@13800000000" in sent["markdown"]["text"]
|
||||
|
||||
|
||||
def test_dingtalk_payload_app_store_heading_and_no_qr():
|
||||
config = {**_build_config(), "BUILD_TYPE": "App_Store"}
|
||||
# App_Store 流程不传二维码地址(publish_ipa 返回空串)
|
||||
payload = build_dingtalk_payload(
|
||||
config, "https://oss.example.com/readoor/iOS/app_2_197_1.ipa"
|
||||
)
|
||||
text = payload["markdown"]["text"]
|
||||
assert "## 【iOS】App_Store 包信息" in text
|
||||
assert "## 【iOS】打包信息" not in text
|
||||
assert "**iOS 下载链接:** https://oss.example.com/readoor/iOS/app_2_197_1.ipa" in text
|
||||
assert "![image]" not in text
|
||||
|
||||
|
||||
def test_dingtalk_notification_app_store_uses_own_at_list():
|
||||
response = MagicMock(status_code=200)
|
||||
response.json.return_value = {"errcode": 0}
|
||||
dingtalk_config = {
|
||||
"enabled": True,
|
||||
"webhook_url": "https://example.com/robot",
|
||||
"at_mobiles": ["13800000000"],
|
||||
"at_mobiles_app_store": ["13911111111", "13922222222"],
|
||||
}
|
||||
with patch("backend.services.notification.httpx.post", return_value=response) as post:
|
||||
send_dingtalk_notification(
|
||||
dingtalk_config,
|
||||
{**_build_config(), "BUILD_TYPE": "App_Store"},
|
||||
"https://oss.example.com/app.ipa",
|
||||
)
|
||||
|
||||
sent = post.call_args.kwargs["json"]
|
||||
assert sent["at"]["atMobiles"] == ["13911111111", "13922222222"]
|
||||
assert "## 【iOS】App_Store 包信息" in sent["markdown"]["text"]
|
||||
assert "@13800000000" not in sent["markdown"]["text"]
|
||||
|
||||
|
||||
def test_dingtalk_notification_skips_when_disabled():
|
||||
with patch("backend.services.notification.httpx.post") as post:
|
||||
assert send_dingtalk_notification({}, _build_config(), "https://download.example.com/app") is False
|
||||
|
||||
Reference in New Issue
Block a user