Initial commit: stock analysis backend and prototype UI.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-13 02:26:22 +08:00
commit 8de37d5c2d
25 changed files with 4624 additions and 0 deletions

84
backend/notifier.py Normal file
View File

@@ -0,0 +1,84 @@
"""推送通知:邮件(SMTP) + Server酱 + 企业微信机器人 + PushPlus。
任意渠道配置了凭证即启用notify() 会向所有已配置渠道发送,返回各渠道结果。
"""
from __future__ import annotations
import smtplib
import ssl
from email.mime.text import MIMEText
from email.header import Header
import requests
import config
def channels_status():
return {
"email": bool(config.SMTP_HOST and config.SMTP_USER and config.SMTP_TO),
"serverchan": bool(config.SERVERCHAN_KEY),
"wecom": bool(config.WECOM_WEBHOOK),
"pushplus": bool(config.PUSHPLUS_TOKEN),
}
def any_enabled():
return any(channels_status().values())
def _send_email(title, content):
msg = MIMEText(content, "plain", "utf-8")
msg["Subject"] = Header(title, "utf-8")
msg["From"] = config.SMTP_USER
tos = [x.strip() for x in config.SMTP_TO.split(",") if x.strip()]
msg["To"] = ",".join(tos)
ctx = ssl.create_default_context()
if config.SMTP_PORT == 465:
with smtplib.SMTP_SSL(config.SMTP_HOST, config.SMTP_PORT, context=ctx, timeout=15) as srv:
srv.login(config.SMTP_USER, config.SMTP_PASSWORD)
srv.sendmail(config.SMTP_USER, tos, msg.as_string())
else:
with smtplib.SMTP(config.SMTP_HOST, config.SMTP_PORT, timeout=15) as srv:
srv.starttls(context=ctx)
srv.login(config.SMTP_USER, config.SMTP_PASSWORD)
srv.sendmail(config.SMTP_USER, tos, msg.as_string())
return True
def _send_serverchan(title, content):
url = f"https://sctapi.ftqq.com/{config.SERVERCHAN_KEY}.send"
r = requests.post(url, data={"title": title, "desp": content}, timeout=12)
r.raise_for_status()
return r.json().get("code", 0) == 0
def _send_wecom(title, content):
r = requests.post(config.WECOM_WEBHOOK,
json={"msgtype": "text", "text": {"content": f"{title}\n{content}"}}, timeout=12)
r.raise_for_status()
return r.json().get("errcode", -1) == 0
def _send_pushplus(title, content):
r = requests.post("https://www.pushplus.plus/send",
json={"token": config.PUSHPLUS_TOKEN, "title": title, "content": content}, timeout=12)
r.raise_for_status()
return r.json().get("code", 0) == 200
def notify(title, content):
"""向所有已配置渠道推送,返回 {渠道: 'ok'|错误信息}。"""
st = channels_status()
senders = {"email": _send_email, "serverchan": _send_serverchan,
"wecom": _send_wecom, "pushplus": _send_pushplus}
result = {}
for ch, on in st.items():
if not on:
continue
try:
senders[ch](title, content)
result[ch] = "ok"
except Exception as e:
result[ch] = repr(e)[:120]
return result