diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml index 7ae6030..8eca7ea 100644 --- a/.gitea/workflows/deploy.yaml +++ b/.gitea/workflows/deploy.yaml @@ -79,6 +79,8 @@ jobs: --from-literal=DB_HOST='${{ secrets.DB_HOST }}' \ --from-literal=DB_USER='${{ secrets.DB_USER }}' \ --from-literal=DB_PASSWORD='${{ secrets.DB_PASSWORD }}' \ + --from-literal=ALIYUN_SMS_ACCESS_KEY='${{ secrets.ALIYUN_SMS_ACCESS_KEY }}' \ + --from-literal=ALIYUN_SMS_ACCESS_SECRET='${{ secrets.ALIYUN_SMS_ACCESS_SECRET }}' \ --dry-run=client -o yaml | kubectl apply -f - kubectl apply -f /tmp/backend-deployment.yaml diff --git a/backend/config/settings.py b/backend/config/settings.py index 01cc993..bbdd003 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -193,3 +193,11 @@ ARK_API_KEY = os.environ.get('ARK_API_KEY', '') ARK_BASE_URL = os.environ.get('ARK_BASE_URL', 'https://ark.cn-beijing.volces.com/api/v3') # Set to True when Seedance model is activated on ARK platform SEEDANCE_ENABLED = os.environ.get('SEEDANCE_ENABLED', 'false').lower() == 'true' + +# ────────────────────────────────────────────── +# Aliyun SMS (短信告警) +# ────────────────────────────────────────────── +ALIYUN_SMS_ACCESS_KEY = os.environ.get('ALIYUN_SMS_ACCESS_KEY', '') +ALIYUN_SMS_ACCESS_SECRET = os.environ.get('ALIYUN_SMS_ACCESS_SECRET', '') +ALIYUN_SMS_SIGN_NAME = os.environ.get('ALIYUN_SMS_SIGN_NAME', '广州气元科技') +ALIYUN_SMS_TEMPLATE_CODE = os.environ.get('ALIYUN_SMS_TEMPLATE_CODE', 'SMS_503445109') diff --git a/backend/utils/alert_service.py b/backend/utils/alert_service.py index 27750cc..d0c84c8 100644 --- a/backend/utils/alert_service.py +++ b/backend/utils/alert_service.py @@ -222,6 +222,95 @@ def send_feishu_alert(anomaly): logger.error('Feishu alert error for %s: %s', mobile, e) +def send_sms_alert(anomaly): + """发送短信告警到配置的接收人。""" + from apps.generation.models import QuotaConfig + from django.conf import settings as django_settings + + try: + config = QuotaConfig.objects.get(pk=1) + except QuotaConfig.DoesNotExist: + logger.warning('QuotaConfig not found, skip SMS alert') + return + + mobiles = [m.strip() for m in config.sms_alert_mobiles.split(',') if m.strip()] + if not mobiles: + return + + access_key = django_settings.ALIYUN_SMS_ACCESS_KEY + access_secret = django_settings.ALIYUN_SMS_ACCESS_SECRET + sign_name = django_settings.ALIYUN_SMS_SIGN_NAME + template_code = django_settings.ALIYUN_SMS_TEMPLATE_CODE + + if not all([access_key, access_secret, template_code]): + logger.warning('Aliyun SMS credentials not configured, skip SMS alert') + return + + rule_name = _RULE_NAMES.get(anomaly.rule, anomaly.rule) + auto_action = '已自动封禁' if anomaly.auto_disabled else '仅告警' + + template_param = json.dumps({ + 'team_name': anomaly.team.name[:20], + 'rule_name': rule_name[:20], + 'username': anomaly.user.username[:20], + 'city': anomaly.login_record.geo_city or '未知', + 'auto_action': auto_action, + }, ensure_ascii=False) + + # 使用阿里云 SMS HTTP API + import hashlib + import hmac + import base64 + import urllib.parse + import uuid + from datetime import datetime + + def _percent_encode(s): + return urllib.parse.quote(s, safe='', encoding='utf-8') + + for mobile in mobiles: + try: + params = { + 'AccessKeyId': access_key, + 'Action': 'SendSms', + 'Format': 'JSON', + 'PhoneNumbers': mobile, + 'RegionId': 'cn-hangzhou', + 'SignName': sign_name, + 'SignatureMethod': 'HMAC-SHA1', + 'SignatureNonce': str(uuid.uuid4()), + 'SignatureVersion': '1.0', + 'TemplateCode': template_code, + 'TemplateParam': template_param, + 'Timestamp': datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ'), + 'Version': '2017-05-25', + } + + sorted_params = sorted(params.items()) + query_string = '&'.join(f'{_percent_encode(k)}={_percent_encode(v)}' for k, v in sorted_params) + string_to_sign = f'GET&{_percent_encode("/")}&{_percent_encode(query_string)}' + + sign_key = (access_secret + '&').encode('utf-8') + signature = base64.b64encode( + hmac.new(sign_key, string_to_sign.encode('utf-8'), hashlib.sha1).digest() + ).decode('utf-8') + + params['Signature'] = signature + + resp = requests.get( + 'https://dysmsapi.aliyuncs.com/', + params=params, + timeout=10, + ) + data = resp.json() + if data.get('Code') == 'OK': + logger.info('SMS alert sent to %s for rule %s', mobile, anomaly.rule) + else: + logger.error('SMS send failed to %s: %s', mobile, data) + except Exception as e: + logger.error('SMS alert error for %s: %s', mobile, e) + + def send_feishu_test(mobile): """发送测试消息到指定手机号。Returns (success, message)。""" try: diff --git a/backend/utils/anomaly_detector.py b/backend/utils/anomaly_detector.py index 25b01ae..c9fa9e6 100644 --- a/backend/utils/anomaly_detector.py +++ b/backend/utils/anomaly_detector.py @@ -305,7 +305,8 @@ def _send_alert_safe(anomaly_pk): from apps.accounts.models import LoginAnomaly anomaly = LoginAnomaly.objects.select_related('team', 'user', 'login_record').get(pk=anomaly_pk) - from utils.alert_service import send_feishu_alert + from utils.alert_service import send_feishu_alert, send_sms_alert send_feishu_alert(anomaly) + send_sms_alert(anomaly) except Exception as e: logger.error('Failed to send alert for anomaly %s: %s', anomaly_pk, e) diff --git a/k8s/backend-deployment.yaml b/k8s/backend-deployment.yaml index 73b1bf6..0b302e2 100644 --- a/k8s/backend-deployment.yaml +++ b/k8s/backend-deployment.yaml @@ -91,6 +91,21 @@ spec: key: ARK_API_KEY - name: SEEDANCE_ENABLED value: "true" + # Aliyun SMS + - name: ALIYUN_SMS_ACCESS_KEY + valueFrom: + secretKeyRef: + name: video-backend-secrets + key: ALIYUN_SMS_ACCESS_KEY + - name: ALIYUN_SMS_ACCESS_SECRET + valueFrom: + secretKeyRef: + name: video-backend-secrets + key: ALIYUN_SMS_ACCESS_SECRET + - name: ALIYUN_SMS_SIGN_NAME + value: "广州气元科技" + - name: ALIYUN_SMS_TEMPLATE_CODE + value: "SMS_503445109" livenessProbe: httpGet: path: /healthz/ diff --git a/web/src/pages/SettingsPage.tsx b/web/src/pages/SettingsPage.tsx index 30b00f8..6f29538 100644 --- a/web/src/pages/SettingsPage.tsx +++ b/web/src/pages/SettingsPage.tsx @@ -316,14 +316,12 @@ export function SettingsPage() {
- + setSettings({ ...settings, sms_alert_mobiles: e.target.value })} - placeholder="暂未开放" - disabled - style={{ opacity: 0.5 }} + placeholder="多个手机号用逗号分隔,如 13800138000,13900139000" />