fix: 认证系统、登录兼容性、运行时配置、钉钉通知及代码清理
- 实现 JWT 认证中间件,保护配置管理和任务删除接口 - 修复 ConfigView 登录按钮(Vue 3 inject 替代 $root) - 页面刷新时通过 /api/auth/me 校验 token 有效性 - max_concurrent_builds 修改后运行时即时生效 - 实现钉钉 webhook 通知(构建成功/失败自动推送) - 删除未使用的 useWebSocket composable - log_streamer 使用 LOG_QUEUE_MAX_SIZE 配置常量
This commit is contained in:
@@ -62,10 +62,15 @@ class BuildQueue:
|
||||
task.cancel()
|
||||
|
||||
def update_max_concurrent(self, new_max: int):
|
||||
"""更新最大并发数"""
|
||||
"""更新最大并发数(运行时生效)"""
|
||||
if new_max == self._max_concurrent:
|
||||
return
|
||||
old = self._max_concurrent
|
||||
self._max_concurrent = new_max
|
||||
# 注意:运行时修改需要重建 semaphore,这里简化处理
|
||||
# 实际使用时建议重启服务生效
|
||||
# 调整信号量:增加并发时 release 额外的许可,减少时由自然消费收敛
|
||||
if new_max > old:
|
||||
for _ in range(new_max - old):
|
||||
self._semaphore.release()
|
||||
|
||||
@property
|
||||
def queue_size(self) -> int:
|
||||
|
||||
@@ -309,6 +309,11 @@ async def run_build_task(task_id: str):
|
||||
if task.oss_url:
|
||||
await log_streamer.emit(task_id, f"下载链接: {task.oss_url}")
|
||||
|
||||
# 钉钉通知
|
||||
from .notification import notify_build_result
|
||||
dingtalk = full_config.get("upload", {}).get("dingtalk", {})
|
||||
await notify_build_result(task, dingtalk)
|
||||
|
||||
# 带超时执行打包
|
||||
await asyncio.wait_for(_do_build(), timeout=timeout_seconds)
|
||||
|
||||
@@ -330,6 +335,11 @@ async def run_build_task(task_id: str):
|
||||
await log_streamer.emit(task_id, f"临时文件保留在: {build_dir}")
|
||||
await asyncio.to_thread(log_streamer.save_log, task_id, build_dir)
|
||||
|
||||
# 钉钉通知
|
||||
from .notification import notify_build_result
|
||||
dingtalk = full_config.get("upload", {}).get("dingtalk", {})
|
||||
await notify_build_result(task, dingtalk)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
await asyncio.to_thread(_db_update, db, task,
|
||||
status="cancelled", completed_at=datetime.utcnow())
|
||||
@@ -362,6 +372,11 @@ async def run_build_task(task_id: str):
|
||||
await log_streamer.emit(task_id, f"临时文件保留在: {build_dir}")
|
||||
await asyncio.to_thread(log_streamer.save_log, task_id, build_dir)
|
||||
|
||||
# 钉钉通知
|
||||
from .notification import notify_build_result
|
||||
dingtalk = full_config.get("upload", {}).get("dingtalk", {})
|
||||
await notify_build_result(task, dingtalk)
|
||||
|
||||
finally:
|
||||
log_streamer.complete(task_id)
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, AsyncGenerator
|
||||
|
||||
from ..config import LOG_QUEUE_MAX_SIZE
|
||||
|
||||
|
||||
class LogStreamer:
|
||||
"""日志流管理器"""
|
||||
@@ -16,7 +18,7 @@ class LogStreamer:
|
||||
|
||||
def create_queue(self, task_id: str) -> asyncio.Queue:
|
||||
"""创建任务的日志队列"""
|
||||
queue = asyncio.Queue(maxsize=1000)
|
||||
queue = asyncio.Queue(maxsize=LOG_QUEUE_MAX_SIZE)
|
||||
self._queues[task_id] = queue
|
||||
self._log_lines[task_id] = []
|
||||
return queue
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
"""钉钉通知服务"""
|
||||
import asyncio
|
||||
import hashlib
|
||||
import hmac
|
||||
import base64
|
||||
import time
|
||||
import urllib.parse
|
||||
import json
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _sign(secret: str) -> tuple[str, str]:
|
||||
"""生成钉钉加签参数"""
|
||||
timestamp = str(round(time.time() * 1000))
|
||||
string_to_sign = f"{timestamp}\n{secret}"
|
||||
hmac_code = hmac.new(
|
||||
secret.encode("utf-8"),
|
||||
string_to_sign.encode("utf-8"),
|
||||
digestmod=hashlib.sha256,
|
||||
).digest()
|
||||
sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
|
||||
return timestamp, sign
|
||||
|
||||
|
||||
async def send_dingtalk(webhook_url: str, title: str, text: str, secret: str = ""):
|
||||
"""发送钉钉 Markdown 通知"""
|
||||
import httpx
|
||||
|
||||
url = webhook_url
|
||||
if secret:
|
||||
timestamp, sign = _sign(secret)
|
||||
url = f"{url}×tamp={timestamp}&sign={sign}"
|
||||
|
||||
payload = {
|
||||
"msgtype": "markdown",
|
||||
"markdown": {"title": title, "text": text},
|
||||
}
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
resp = await client.post(url, json=payload)
|
||||
data = resp.json()
|
||||
if data.get("errcode") != 0:
|
||||
logger.warning(f"钉钉通知发送失败: {data}")
|
||||
except Exception as e:
|
||||
logger.warning(f"钉钉通知异常: {e}")
|
||||
|
||||
|
||||
async def notify_build_result(task, dingtalk_config: dict):
|
||||
"""发送打包结果通知"""
|
||||
if not dingtalk_config.get("enabled") or not dingtalk_config.get("webhook_url"):
|
||||
return
|
||||
|
||||
status = task.status
|
||||
app_name = task.app_name
|
||||
build_type = task.build_type
|
||||
scheme_name = task.scheme_name
|
||||
|
||||
if status == "completed":
|
||||
title = f"✅ {app_name} 打包成功"
|
||||
lines = [
|
||||
f"### ✅ {app_name} 打包成功",
|
||||
f"- **应用**: {app_name}",
|
||||
f"- **类型**: {build_type}",
|
||||
f"- **Scheme**: {scheme_name}",
|
||||
f"- **分支**: {task.branch}",
|
||||
]
|
||||
if task.oss_url:
|
||||
lines.append(f"- **下载链接**: [点击下载]({task.oss_url})")
|
||||
text = "\n".join(lines)
|
||||
elif status == "failed":
|
||||
title = f"❌ {app_name} 打包失败"
|
||||
error = task.error_message or "未知错误"
|
||||
lines = [
|
||||
f"### ❌ {app_name} 打包失败",
|
||||
f"- **应用**: {app_name}",
|
||||
f"- **类型**: {build_type}",
|
||||
f"- **Scheme**: {scheme_name}",
|
||||
f"- **分支**: {task.branch}",
|
||||
f"- **错误**: {error}",
|
||||
]
|
||||
text = "\n".join(lines)
|
||||
else:
|
||||
return
|
||||
|
||||
await send_dingtalk(
|
||||
dingtalk_config["webhook_url"],
|
||||
title,
|
||||
text,
|
||||
dingtalk_config.get("secret", ""),
|
||||
)
|
||||
Reference in New Issue
Block a user