Compare commits
8 Commits
be656900c0
...
e04712cc79
| Author | SHA1 | Date | |
|---|---|---|---|
| e04712cc79 | |||
|
|
6a0311d599 | ||
| cc8cfe60cf | |||
| ecde54b8d8 | |||
| d75d35dfc0 | |||
| caf51b0909 | |||
| 447585c617 | |||
| c1a0a477d8 |
@ -47,47 +47,50 @@ jobs:
|
||||
--tag ${{ secrets.SWR_SERVER }}/${{ secrets.SWR_ORG }}/video-web:latest \
|
||||
./web 2>&1 | tee -a /tmp/build.log
|
||||
|
||||
- name: Setup Kubectl
|
||||
- name: Setup SSH
|
||||
run: |
|
||||
curl -LO "https://dl.k8s.io/release/v1.28.2/bin/linux/amd64/kubectl" || \
|
||||
curl -LO "https://cdn.dl.k8s.io/release/v1.28.2/bin/linux/amd64/kubectl"
|
||||
chmod +x kubectl
|
||||
mv kubectl /usr/local/bin/
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.K3S_SSH_KEY }}" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
ssh-keyscan -H ${{ secrets.K3S_HOST }} >> ~/.ssh/known_hosts 2>/dev/null
|
||||
|
||||
- name: Deploy to K3s
|
||||
uses: Azure/k8s-set-context@v3
|
||||
with:
|
||||
method: kubeconfig
|
||||
kubeconfig: ${{ secrets.KUBE_CONFIG }}
|
||||
|
||||
- name: Create or Update Secrets
|
||||
run: |
|
||||
kubectl create secret generic video-backend-secrets \
|
||||
--from-literal=ARK_API_KEY=${{ secrets.ARK_API_KEY }} \
|
||||
--from-literal=TOS_ACCESS_KEY=${{ secrets.TOS_ACCESS_KEY }} \
|
||||
--from-literal=TOS_SECRET_KEY=${{ secrets.TOS_SECRET_KEY }} \
|
||||
--from-literal=DJANGO_SECRET_KEY=${{ secrets.DJANGO_SECRET_KEY }} \
|
||||
--from-literal=DB_HOST=${{ secrets.DB_HOST }} \
|
||||
--from-literal=DB_USER=${{ secrets.DB_USER }} \
|
||||
--from-literal=DB_PASSWORD=${{ secrets.DB_PASSWORD }} \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
- name: Apply K8s Manifests
|
||||
- name: Deploy to K3s via SSH
|
||||
id: deploy
|
||||
run: |
|
||||
# Replace image placeholders
|
||||
sed -i "s|\${CI_REGISTRY_IMAGE}/video-backend:latest|${{ secrets.SWR_SERVER }}/${{ secrets.SWR_ORG }}/video-backend:latest|g" k8s/backend-deployment.yaml
|
||||
sed -i "s|\${CI_REGISTRY_IMAGE}/video-web:latest|${{ secrets.SWR_SERVER }}/${{ secrets.SWR_ORG }}/video-web:latest|g" k8s/web-deployment.yaml
|
||||
SWR_IMAGE="${{ secrets.SWR_SERVER }}/${{ secrets.SWR_ORG }}"
|
||||
|
||||
# Apply all manifests
|
||||
# Replace image placeholders in yaml files
|
||||
sed -i "s|\${CI_REGISTRY_IMAGE}/video-backend:latest|${SWR_IMAGE}/video-backend:latest|g" k8s/backend-deployment.yaml
|
||||
sed -i "s|\${CI_REGISTRY_IMAGE}/video-web:latest|${SWR_IMAGE}/video-web:latest|g" k8s/web-deployment.yaml
|
||||
|
||||
# Copy k8s manifests to server
|
||||
scp -o StrictHostKeyChecking=no k8s/backend-deployment.yaml k8s/web-deployment.yaml k8s/ingress.yaml root@${{ secrets.K3S_HOST }}:/tmp/
|
||||
|
||||
# Create/update secrets and apply manifests on server
|
||||
set -o pipefail
|
||||
{
|
||||
kubectl apply -f k8s/backend-deployment.yaml
|
||||
kubectl apply -f k8s/web-deployment.yaml
|
||||
kubectl apply -f k8s/ingress.yaml
|
||||
ssh -o StrictHostKeyChecking=no root@${{ secrets.K3S_HOST }} << ENDSSH
|
||||
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
|
||||
|
||||
kubectl create secret generic video-backend-secrets \
|
||||
--from-literal=ARK_API_KEY='${{ secrets.ARK_API_KEY }}' \
|
||||
--from-literal=TOS_ACCESS_KEY='${{ secrets.TOS_ACCESS_KEY }}' \
|
||||
--from-literal=TOS_SECRET_KEY='${{ secrets.TOS_SECRET_KEY }}' \
|
||||
--from-literal=DJANGO_SECRET_KEY='${{ secrets.DJANGO_SECRET_KEY }}' \
|
||||
--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
|
||||
kubectl apply -f /tmp/web-deployment.yaml
|
||||
kubectl apply -f /tmp/ingress.yaml
|
||||
kubectl rollout restart deployment/video-backend
|
||||
kubectl rollout restart deployment/video-web
|
||||
} 2>&1 | tee /tmp/deploy.log
|
||||
|
||||
rm -f /tmp/backend-deployment.yaml /tmp/web-deployment.yaml /tmp/ingress.yaml
|
||||
ENDSSH
|
||||
|
||||
# ===== Log Center: failure reporting =====
|
||||
- name: Report failure to Log Center
|
||||
|
||||
@ -444,6 +444,7 @@ npx tsx src/index.ts --resume /Users/maidong/Desktop/zyc/研究openclaw/视频
|
||||
| 2026-03-18 | v0.9.1: 系统设置页 — 异常检测总开关、R1-R5默认阈值、飞书接收人+测试、告警冷却 | Frontend |
|
||||
| 2026-03-18 | v0.9.1: 团队管理 — 预期登录城市(必填) + 自动学习 + disabled_by 来源标签 | Full stack |
|
||||
| 2026-03-18 | v0.9.1: 前端拦截器 — user_disabled/team_disabled 错误码处理,弹窗提示后跳登录 | Frontend |
|
||||
| 2026-03-19 | fix: LoginRecord 创建时显式传 geo 空字段,修复 MySQL 严格模式 IntegrityError | Backend |
|
||||
|
||||
### Phase 4 Details (2026-03-13)
|
||||
|
||||
|
||||
@ -103,6 +103,7 @@ def login_view(request):
|
||||
user_agent = request.META.get('HTTP_USER_AGENT', '')
|
||||
login_record = LoginRecord.objects.create(
|
||||
user=user, team=user.team, ip_address=ip, user_agent=user_agent,
|
||||
geo_country='', geo_province='', geo_city='', geo_source='',
|
||||
)
|
||||
|
||||
# IP 归属地解析 + 异常检测(不阻塞登录)
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -14,6 +14,8 @@ spec:
|
||||
labels:
|
||||
app: video-backend
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: swr-secret
|
||||
containers:
|
||||
- name: video-backend
|
||||
image: ${CI_REGISTRY_IMAGE}/video-backend:latest
|
||||
@ -89,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/
|
||||
|
||||
15
k8s/cert-manager-issuer.yaml
Normal file
15
k8s/cert-manager-issuer.yaml
Normal file
@ -0,0 +1,15 @@
|
||||
# ClusterIssuer for Let's Encrypt automatic certificate generation & renewal
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: ClusterIssuer
|
||||
metadata:
|
||||
name: letsencrypt-prod
|
||||
spec:
|
||||
acme:
|
||||
server: https://acme-v02.api.letsencrypt.org/directory
|
||||
email: airlabsv001@gmail.com
|
||||
privateKeySecretRef:
|
||||
name: letsencrypt-prod-key
|
||||
solvers:
|
||||
- http01:
|
||||
ingress:
|
||||
class: alb
|
||||
@ -9,10 +9,8 @@ spec:
|
||||
tls:
|
||||
- hosts:
|
||||
- video-huoshan-api.airlabs.art
|
||||
secretName: video-huoshan-api-tls
|
||||
- hosts:
|
||||
- video-huoshan-web.airlabs.art
|
||||
secretName: video-huoshan-web-tls
|
||||
secretName: video-huoshan-tls
|
||||
rules:
|
||||
- host: video-huoshan-api.airlabs.art
|
||||
http:
|
||||
|
||||
@ -14,6 +14,8 @@ spec:
|
||||
labels:
|
||||
app: video-web
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: swr-secret
|
||||
containers:
|
||||
- name: video-web
|
||||
image: ${CI_REGISTRY_IMAGE}/video-web:latest
|
||||
|
||||
@ -316,14 +316,12 @@ export function SettingsPage() {
|
||||
</div>
|
||||
|
||||
<div className={styles.formGroup}>
|
||||
<label>短信告警手机号(Coming soon)</label>
|
||||
<label>短信告警手机号</label>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.sms_alert_mobiles}
|
||||
onChange={(e) => setSettings({ ...settings, sms_alert_mobiles: e.target.value })}
|
||||
placeholder="暂未开放"
|
||||
disabled
|
||||
style={{ opacity: 0.5 }}
|
||||
placeholder="多个手机号用逗号分隔,如 13800138000,13900139000"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user