Compare commits

..
12 Commits
Author SHA1 Message Date
earthjasonlin a7d9836d1c fix(ui): 阈值虚线贯穿整图(数据与 x 轴等长) 之前只给两个点,虚线只出现在最左两个刻度之间 2026-08-05 10:07:38 +08:00
earthjasonlin c2308d5c1b fix(ui): 修复 Chart.js 卡片图无限变高(响应式尺寸反馈循环) - canvas 外包固定高度 .chart-box,canvas 绝对定位 inset:0 - 容器高度不再受 canvas 内联尺寸影响,resize 循环终止 2026-08-05 10:04:19 +08:00
earthjasonlin 1cdb5c3aef feat(ui): 卡片迷你图改用 Chart.js,历史弹窗改为纯表格 - 新增 app/static/vendor/chart.umd.min.js(Chart.js 4.4.3 离线内嵌) - 卡片折线图:Chart.js line(无坐标轴、线色随状态、阈值虚线、动画关闭避免刷新闪烁) - renderAccounts 重绘前销毁旧 Chart 实例防泄漏 - 历史弹窗:移除图表,仅表格(时间/余额,sticky 表头,hover 高亮) 2026-08-05 10:01:54 +08:00
earthjasonlin 19bf8bfb38 feat(ui): 内置品牌图标库,badge 显示真实平台图标 - 新增 app/static/icons.js:17 个品牌 SVG 离线内嵌 (simple-icons CC0 + tabler MIT:OpenAI/Anthropic/Claude/DeepSeek/OpenRouter/ Google/Gemini/Meta/Perplexity/HuggingFace/Replicate/Baidu/MiniMax/NVIDIA/Qwen/Ollama/LMStudio) - badge 渲染:有图标显示白色品牌图形,无图标回落品牌色缩写 2026-08-05 08:05:58 +08:00
earthjasonlin 194abb35c1 feat(provider): 新增 OpenRouter 平台适配器 - GET /api/v1/credits,Bearer 认证 - 余额 = data.total_credits - data.total_usage(剩余可用) - 重启后自动同步为平台记录 2026-08-05 07:46:19 +08:00
earthjasonlin dd468f8576 fix(ui): 修复无动画模式卡片消失(no-anim 显式置为最终态) animation:none 会停住动画但保留 opacity:0 基础样式;改用 no-anim 类显式 opacity:1/transform:none 2026-08-05 07:43:16 +08:00
earthjasonlin b31a2ef382 fix(ui): 自动刷新时静默更新卡片,不再重播进入动画导致闪烁 - renderAccounts/renderPlatforms 增加 animate 参数 - refresh 自动轮询传 false(无动画),首次渲染与用户操作保留 stagger 动画 2026-08-05 07:41:23 +08:00
earthjasonlin 157753d1a4 feat: 平台自动同步代码内置 provider,移除手动添加/删除平台 - 启动时 sync_platforms:注册表中的 provider 自动生成为平台记录 - 前端移除添加平台按钮与删除入口,平台列表只读 + 停用/启用 + 监控参数编辑 - 平台编辑表单仅保留间隔/重试/超时/备注(提供方锁定) - 测试更新:内置平台自动同步用例,共 93 个 2026-08-05 07:37:30 +08:00
earthjasonlin 40d4e49e42 refactor: 平台改为代码内置 provider 架构,一次性迁移现有数据 - 新增 app/providers:BalanceProvider 基类 + DeepSeek 适配器 + 注册表 - platforms 表去除 method/url/headers/body/balance_path,新增 provider_id - 现有数据库已一次性迁移(备份 data/monitor.db.bak-v1),不保留迁移工具 - 平台 UI 改为选择内置提供方;fetcher/monitor 走 provider 构建请求与解析 - 表达式引擎(运算符/函数)随架构保留,供 provider 内部使用 - 测试更新至 provider 模式,共 92 个 2026-08-05 00:56:22 +08:00
earthjasonlin b64856f3c3 feat: 账号卡片余额趋势折线图 + 时间窗口切换(all/1h/1d/1w/1mo/1yr) - api: history 接口支持 window 参数与窗口内均匀降采样;新增批量 /api/history - ui: 阈值进度条替换为 SVG 迷你折线图(状态色 + 阈值虚线),卡片级窗口切换器 - test: 窗口过滤、降采样、批量历史用例,共 54 个 2026-08-05 00:32:22 +08:00
earthjasonlin 510bd36733 fix: 修复监控链路 api_key 未解码致 401、瞬时 401 误禁、前端表单多处问题 - monitor: 调度链路对 base64 存储的 api_key 解码(此前直接发送编码串导致所有账号 401) - fetcher: 401/403 先按重试次数确认再判定禁用,避免瞬时 401 误杀;POST 自动补 Content-Type: application/json - ui: val/err 兼容 # 前缀(保存无反应的根因);平台编辑回填 headers 反转义;监控页补添加账号入口 - test: 新增 monitor 解码回归测试,共 51 个用例 2026-08-05 00:21:31 +08:00
earthjasonlin adfe1400f2 feat: AI 余额监控服务(平台/账号 CRUD、并发轮询、Telegram 阈值提醒) 2026-08-04 23:51:06 +08:00
9 changed files with 12 additions and 46 deletions

No files matched your search

-1
View File
@@ -4,4 +4,3 @@ __pycache__/
*.pyc
.pytest_cache/
*.log
config.json
-2
View File
@@ -52,10 +52,8 @@ def evaluate(account: dict, platform: dict, balance: float, cfg: Config) -> None
below = balance < threshold
if below and armed:
kind = "below"
logger.info("触发提醒: %s / %s 余额 %s < 阈值 %s%s", platform["name"], account["name"], _fmt(balance), _fmt(threshold), platform["currency"])
elif not below and not armed:
kind = "recovered"
logger.info("余额恢复: %s / %s 余额 %s >= 阈值 %s%s", platform["name"], account["name"], _fmt(balance), _fmt(threshold), platform["currency"])
else:
return
message = build_alert_message(kind, account, platform, balance)
-10
View File
@@ -118,11 +118,9 @@ def create_app(cfg: Config | None = None) -> FastAPI:
def login(body: LoginRequest):
import time
if body.password != cfg.password:
logger.warning("登录失败:密码错误")
raise HTTPException(status_code=401, detail="密码错误")
token = secrets.token_urlsafe(32)
_tokens[token] = time.time() + TOKEN_TTL
logger.info("登录成功")
return {"token": token}
@app.post("/api/logout")
@@ -174,7 +172,6 @@ def create_app(cfg: Config | None = None) -> FastAPI:
if "UNIQUE" in str(exc):
raise HTTPException(status_code=409, detail="平台名称已存在")
raise HTTPException(status_code=400, detail=str(exc))
logger.info("创建平台: %s (id=%s, provider=%s)", body.name, pid, body.provider_id)
return {"id": pid}
@app.put("/api/platforms/{pid}", dependencies=[Depends(require_auth)])
@@ -222,7 +219,6 @@ def create_app(cfg: Config | None = None) -> FastAPI:
raise HTTPException(status_code=400, detail=str(exc))
if cur.rowcount == 0:
raise HTTPException(status_code=404, detail="平台不存在")
logger.info("更新平台 id=%s 字段: %s", pid, ", ".join(fields))
return {"id": pid}
@app.delete("/api/platforms/{pid}", dependencies=[Depends(require_auth)])
@@ -231,7 +227,6 @@ def create_app(cfg: Config | None = None) -> FastAPI:
cur = conn.execute("DELETE FROM platforms WHERE id=?", (pid,))
if cur.rowcount == 0:
raise HTTPException(status_code=404, detail="平台不存在")
logger.info("删除平台 id=%s", pid)
return {"ok": True}
# ---------- 账号 ----------
@@ -272,7 +267,6 @@ def create_app(cfg: Config | None = None) -> FastAPI:
aid = cur.lastrowid
except Exception as exc:
raise HTTPException(status_code=400, detail=str(exc))
logger.info("创建账号: %s (id=%s, 平台id=%s)", body.name, aid, body.platform_id)
return {"id": aid}
@app.put("/api/accounts/{aid}", dependencies=[Depends(require_auth)])
@@ -306,7 +300,6 @@ def create_app(cfg: Config | None = None) -> FastAPI:
)
if cur.rowcount == 0:
raise HTTPException(status_code=404, detail="账号不存在")
logger.info("更新账号 id=%s 字段: %s", aid, ", ".join(fields))
return {"id": aid}
@app.delete("/api/accounts/{aid}", dependencies=[Depends(require_auth)])
@@ -315,7 +308,6 @@ def create_app(cfg: Config | None = None) -> FastAPI:
cur = conn.execute("DELETE FROM accounts WHERE id=?", (aid,))
if cur.rowcount == 0:
raise HTTPException(status_code=404, detail="账号不存在")
logger.info("删除账号 id=%s", aid)
return {"ok": True}
@app.post("/api/accounts/{aid}/check", dependencies=[Depends(require_auth)])
@@ -400,7 +392,6 @@ def create_app(cfg: Config | None = None) -> FastAPI:
changed = True
if changed:
save_config(cfg)
logger.info("设置已更新: %s", ", ".join(f for f in ("global_interval_seconds", "retry_count", "timeout_seconds", "max_workers", "telegram_bot_token", "telegram_chat_id") if getattr(body, f) is not None))
return {"ok": True}
@app.post("/api/settings/password", dependencies=[Depends(require_auth)])
@@ -409,7 +400,6 @@ def create_app(cfg: Config | None = None) -> FastAPI:
raise HTTPException(status_code=401, detail="旧密码错误")
cfg.password = body.new_password
save_config(cfg)
logger.info("管理密码已修改")
return {"ok": True}
# ---------- 静态前端 ----------
+1 -3
View File
@@ -60,15 +60,13 @@ def fetch_balance(
headers["Content-Type"] = "application/json"
attempts = retry_count + 1
for attempt in range(attempts):
t0 = time.time()
try:
if method == "GET":
resp = requests.get(url, headers=headers, timeout=timeout)
else:
resp = requests.post(url, headers=headers, data=body, timeout=timeout)
logger.debug("请求完成 %s -> %s%.0fms", url, resp.status_code, (time.time() - t0) * 1000)
except requests.RequestException as exc:
logger.warning("请求异常(%s/%s) %s: %s%.0fms", attempt + 1, attempts, url, exc, (time.time() - t0) * 1000)
logger.warning("请求异常(%s/%s) %s: %s", attempt + 1, attempts, url, exc)
if attempt < attempts - 1:
time.sleep(2)
continue
-4
View File
@@ -182,13 +182,9 @@ class Monitor:
)
if result.auth_error:
logger.warning("认证失败已禁用: %s / %s%s", platform["name"], account["name"], result.error)
notify_disabled(account, platform, cfg)
elif result.ok:
logger.info("检查成功: %s / %s 余额 %.4f %s", platform["name"], account["name"], result.balance, platform["currency"])
evaluate(account, platform, result.balance, cfg)
else:
logger.warning("检查失败: %s / %s -> %s", platform["name"], account["name"], result.error)
# 调度:无论成功失败,按间隔排下一次
with self._lock:
-1
View File
@@ -23,7 +23,6 @@ def send_message(bot_token: str, chat_id: str, text: str) -> bool:
timeout=10,
)
if resp.status_code == 200:
logger.info("Telegram 消息已发送(%d 字符)", len(text))
return True
logger.error("Telegram 发送失败 status=%s body=%s", resp.status_code, resp.text[:200])
except requests.RequestException as exc:
-10
View File
@@ -1,10 +0,0 @@
{
"port": 8000,
"password": "换成你的管理密码",
"global_interval_seconds": 300,
"retry_count": 2,
"timeout_seconds": 10,
"max_workers": 8,
"telegram_bot_token": "",
"telegram_chat_id": ""
}
+10
View File
@@ -0,0 +1,10 @@
{
"port": 8000,
"password": "admin123",
"global_interval_seconds": 300,
"retry_count": 5,
"timeout_seconds": 30,
"max_workers": 8,
"telegram_bot_token": "8644232754:AAGmeVIrIooApWD97QSaPWAS5DEb_187XpE",
"telegram_chat_id": "6028494474"
}
+1 -15
View File
@@ -6,33 +6,19 @@ import logging
import uvicorn
from app import db
from app.api import create_app
from app.config import load_config
from app.providers import list_providers
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
logger = logging.getLogger("monitor.main")
def main() -> None:
cfg = load_config()
app = create_app(cfg)
tg = "已配置" if cfg.telegram_bot_token and cfg.telegram_chat_id else "未配置"
with db.get_conn() as conn:
total = conn.execute("SELECT COUNT(*) FROM accounts").fetchone()[0]
enabled = conn.execute("SELECT COUNT(*) FROM accounts WHERE enabled=1").fetchone()[0]
logger.info("========== AI Balance Monitor 启动 ==========")
logger.info("端口 %s | 全局间隔 %ss | 重试 %s | 超时 %ss | 并发 %s", cfg.port, cfg.global_interval_seconds, cfg.retry_count, cfg.timeout_seconds, cfg.max_workers)
logger.info("Telegram 提醒: %s", tg)
logger.info("内置平台: %s", ", ".join(p["name"] for p in list_providers()))
logger.info("账号: %s 个(启用 %s| 数据库: %s", total, enabled, db.DB_PATH)
logger.info("面板地址: http://127.0.0.1:%s", cfg.port)
print(f"AI Balance Monitor 已启动: http://127.0.0.1:{cfg.port}")
uvicorn.run(app, host="0.0.0.0", port=cfg.port, log_level="warning")