feat: Move notification settings to user profile and add i18n support

- Move notification config from system settings (.env) to user profile (database)
- Add user-specific notification settings API endpoints (GET/PUT /api/user/notification-settings)
- Add notification_settings column to qd_users table
- Update portfolio and trading-assistant to use profile notification settings
- Add notification settings UI in profile page with all channels (browser, telegram, email, phone, discord, webhook)
- Add i18n translations for notification settings in 10 languages
- Fix timestamp parsing in TradingRecords and portfolio (handle both ISO strings and Unix timestamps)
- Fix header icons alignment for mobile responsive layout
- Fix equity curve timestamp bug (handle datetime objects properly)
- Remove unused login.js exports and clean up user.js store
- Remove husky, commitlint and other dev dependencies
- Clean up env.example by removing user-specific notification params
- Update signal_notifier to prioritize user-specific tokens over global env vars
This commit is contained in:
Jinyu Xu
2026-01-17 01:37:32 +07:00
parent 10ad20abf6
commit c5d000e1c9
33 changed files with 1209 additions and 24317 deletions
@@ -92,18 +92,18 @@ class SignalNotifier:
"""
Notify signal events across channels.
Provider environment variables:
- SIGNAL_NOTIFY_TIMEOUT_SEC: HTTP timeout (default: 6)
- SIGNAL_WEBHOOK_TOKEN: optional bearer token for generic webhook channel
- TELEGRAM_BOT_TOKEN: Telegram bot token (required for telegram channel)
通知配置说明:
- 用户在个人中心配置自己的通知设置(telegram_bot_token, telegram_chat_id, email 等)
- 创建策略/监控时,系统自动使用用户配置的通知目标
公共服务配置(管理员在系统设置中配置):
- SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SMTP_FROM, SMTP_USE_TLS
(required for email channel)
(邮件服务,所有用户共用)
- TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_FROM_NUMBER
(optional for phone/SMS channel)
(短信服务,所有用户共用)
可选的环境变量:
- SIGNAL_NOTIFY_TIMEOUT_SEC: HTTP timeout (default: 6)
"""
def __init__(self) -> None:
@@ -112,10 +112,7 @@ class SignalNotifier:
except Exception:
self.timeout_sec = 6.0
self.webhook_token = (os.getenv("SIGNAL_WEBHOOK_TOKEN") or "").strip()
self.telegram_token = (os.getenv("TELEGRAM_BOT_TOKEN") or "").strip()
# 公共 SMTP 配置(管理员在系统设置中配置)
self.smtp_host = (os.getenv("SMTP_HOST") or "").strip()
try:
self.smtp_port = int(os.getenv("SMTP_PORT") or "587")
@@ -184,7 +181,7 @@ class SignalNotifier:
payload=payload,
)
elif c == "webhook":
url = (targets.get("webhook") or os.getenv("SIGNAL_WEBHOOK_URL") or "").strip()
url = (targets.get("webhook") or "").strip()
ok, err = self._notify_webhook(
url=url,
payload=payload,
@@ -201,19 +198,18 @@ class SignalNotifier:
ok, err = self._notify_discord(url=url, payload=payload, fallback_text=message_plain)
elif c == "telegram":
chat_id = (targets.get("telegram") or "").strip()
# Support per-strategy token override (local mode). Falls back to env TELEGRAM_BOT_TOKEN.
# User's token takes priority, then falls back to env TELEGRAM_BOT_TOKEN.
token_override = ""
if not self.telegram_token:
try:
token_override = str(
targets.get("telegram_bot_token")
or targets.get("telegram_token")
or cfg.get("telegram_bot_token")
or cfg.get("telegram_token")
or ""
).strip()
except Exception:
token_override = ""
try:
token_override = str(
targets.get("telegram_bot_token")
or targets.get("telegram_token")
or cfg.get("telegram_bot_token")
or cfg.get("telegram_token")
or ""
).strip()
except Exception:
token_override = ""
ok, err = self._notify_telegram(
chat_id=chat_id,
text=rendered.get("telegram_html") or message_plain,
@@ -500,15 +496,15 @@ class SignalNotifier:
"""
Generic webhook delivery.
Supports (best-effort):
- per-strategy headers: notification_config.targets.webhook_headers (dict or JSON string)
- per-strategy bearer token: notification_config.targets.webhook_token
- global bearer token: SIGNAL_WEBHOOK_TOKEN
- optional signing secret: notification_config.targets.webhook_signing_secret or env SIGNAL_WEBHOOK_SIGNING_SECRET
Adds headers:
- X-QD-Timestamp: unix seconds
- X-QD-Signature: hex(HMAC_SHA256("{ts}.{body}", secret))
- retry once on 429/5xx
用户在个人中心配置:
- webhook_url: Webhook 地址
- webhook_token: Bearer Token(可选)
支持功能:
- 自定义 headers: notification_config.targets.webhook_headers
- Bearer Token: notification_config.targets.webhook_token
- 签名验证: notification_config.targets.webhook_signing_secret
- 自动重试: 429/5xx 时重试一次
"""
if not url:
return False, "missing_webhook_url"
@@ -535,10 +531,8 @@ class SignalNotifier:
continue
headers[kk] = str(v if v is not None else "")
# Auth (per-strategy token first, fallback to global token)
# Auth (user's token from notification_config.targets.webhook_token)
tok = str(token_override or "").strip()
if not tok:
tok = self.webhook_token
if tok and "Authorization" not in headers:
headers["Authorization"] = f"Bearer {tok}"
@@ -662,9 +656,10 @@ class SignalNotifier:
token_override: str = "",
parse_mode: str = "",
) -> Tuple[bool, str]:
token = (token_override or "").strip() or (self.telegram_token or "").strip()
# 用户必须在个人中心配置自己的 telegram_bot_token
token = (token_override or "").strip()
if not token:
return False, "missing_TELEGRAM_BOT_TOKEN"
return False, "missing_telegram_bot_token (请在个人中心配置 Telegram Bot Token)"
if not chat_id:
return False, "missing_telegram_chat_id"
url = f"https://api.telegram.org/bot{token}/sendMessage"