From 86ef5aa41262810419d24e2629ecc739fef95db9 Mon Sep 17 00:00:00 2001 From: Zichao Lin Date: Wed, 5 Aug 2026 07:37:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=B9=B3=E5=8F=B0=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=E4=BB=A3=E7=A0=81=E5=86=85=E7=BD=AE=20provid?= =?UTF-8?q?er=EF=BC=8C=E7=A7=BB=E9=99=A4=E6=89=8B=E5=8A=A8=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0/=E5=88=A0=E9=99=A4=E5=B9=B3=E5=8F=B0=20=20-=20?= =?UTF-8?q?=E5=90=AF=E5=8A=A8=E6=97=B6=20sync=5Fplatforms=EF=BC=9A?= =?UTF-8?q?=E6=B3=A8=E5=86=8C=E8=A1=A8=E4=B8=AD=E7=9A=84=20provider=20?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E7=94=9F=E6=88=90=E4=B8=BA=E5=B9=B3=E5=8F=B0?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=20-=20=E5=89=8D=E7=AB=AF=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=B9=B3=E5=8F=B0=E6=8C=89=E9=92=AE=E4=B8=8E?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E5=85=A5=E5=8F=A3=EF=BC=8C=E5=B9=B3=E5=8F=B0?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E5=8F=AA=E8=AF=BB=20+=20=E5=81=9C=E7=94=A8/?= =?UTF-8?q?=E5=90=AF=E7=94=A8=20+=20=E7=9B=91=E6=8E=A7=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E7=BC=96=E8=BE=91=20-=20=E5=B9=B3=E5=8F=B0=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E8=A1=A8=E5=8D=95=E4=BB=85=E4=BF=9D=E7=95=99=E9=97=B4=E9=9A=94?= =?UTF-8?q?/=E9=87=8D=E8=AF=95/=E8=B6=85=E6=97=B6/=E5=A4=87=E6=B3=A8?= =?UTF-8?q?=EF=BC=88=E6=8F=90=E4=BE=9B=E6=96=B9=E9=94=81=E5=AE=9A=EF=BC=89?= =?UTF-8?q?=20-=20=E6=B5=8B=E8=AF=95=E6=9B=B4=E6=96=B0=EF=BC=9A=E5=86=85?= =?UTF-8?q?=E7=BD=AE=E5=B9=B3=E5=8F=B0=E8=87=AA=E5=8A=A8=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E7=94=A8=E4=BE=8B=EF=BC=8C=E5=85=B1=2093=20=E4=B8=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api.py | 1 + app/db.py | 25 ++++++++++++++++++++ app/static/app.js | 54 ++++++++----------------------------------- app/static/index.html | 3 +-- tests/test_api.py | 20 ++++++++++++---- 5 files changed, 52 insertions(+), 51 deletions(-) diff --git a/app/api.py b/app/api.py index 7cf4d4b..4a12bc0 100644 --- a/app/api.py +++ b/app/api.py @@ -101,6 +101,7 @@ def _downsample(rows: list[sqlite3.Row], limit: int) -> list[sqlite3.Row]: def create_app(cfg: Config | None = None) -> FastAPI: cfg = cfg or load_config() db.init_db() + db.sync_platforms() monitor = Monitor(cfg) @asynccontextmanager diff --git a/app/db.py b/app/db.py index f9b3101..25ba4ee 100644 --- a/app/db.py +++ b/app/db.py @@ -75,5 +75,30 @@ def init_db() -> None: conn.executescript(SCHEMA) +def sync_platforms() -> None: + """把代码内置的 provider 自动同步为平台记录(缺失时创建)。 + + 平台 = 代码注册表,无需用户手动添加;删除也会在下一次启动时重建。 + """ + from app.providers import list_providers + + with get_conn() as conn: + existing = { + r["provider_id"] + for r in conn.execute( + "SELECT provider_id FROM platforms WHERE provider_id != ''" + ).fetchall() + } + for p in list_providers(): + if p["id"] in existing: + continue + conn.execute( + "INSERT INTO platforms (provider_id, name, currency, icon, enabled) " + "VALUES (?, ?, ?, ?, 1)", + (p["id"], p["name"], p["currency"], p["icon"]), + ) + existing.add(p["id"]) + + def rows_to_dicts(rows: list[sqlite3.Row]) -> list[dict]: return [dict(r) for r in rows] diff --git a/app/static/app.js b/app/static/app.js index cfd9f90..80a3fcb 100644 --- a/app/static/app.js +++ b/app/static/app.js @@ -346,7 +346,7 @@ function renderPlatforms() { const list = document.getElementById("platform-list"); if (platforms.length === 0) { - list.innerHTML = `
还没有平台,点击右上角「+ 添加平台」创建。
`; + list.innerHTML = `
暂无平台(代码内置平台会在启动时自动同步)。
`; return; } list.innerHTML = platforms.map((p, i) => { @@ -369,8 +369,7 @@
- - +
`; }).join(""); @@ -408,32 +407,23 @@ mask.querySelector("[data-ok]").onclick = () => { closeModal(mask); onOk(); }; } - /* 平台表单 */ + /* 平台监控参数表单(提供方由代码内置,不可改) */ function platformForm(p) { - const isEdit = !!p; const v = p || {}; - const provOpts = providersList.length - ? providersList.map((x) => ``).join("") - : ''; const prov = providersList.find((x) => x.id === v.provider_id); openModal(` `, (mask) => { mask.querySelector("[data-cancel]").onclick = () => closeModal(mask); - const provSel = mask.querySelector("#pf-provider"); - provSel.onchange = () => { - const x = providersList.find((q) => q.id === provSel.value); - if (!x) return; - mask.querySelector("#pf-prov-desc").textContent = x.description || ""; - if (!mask.querySelector("#pf-name").value || !isEdit) mask.querySelector("#pf-name").value = x.name; - if (!mask.querySelector("#pf-currency").value) mask.querySelector("#pf-currency").value = x.currency; - if (!mask.querySelector("#pf-icon").value) mask.querySelector("#pf-icon").value = x.icon; - }; mask.querySelector("#pf-save").onclick = async () => { - const providerId = val("#pf-provider"); const payload = { - provider_id: providerId, - name: val("#pf-name"), - currency: val("#pf-currency") || null, - icon: val("#pf-icon") || null, - note: val("#pf-note"), interval_seconds: numOrNull("#pf-interval"), retry_count: numOrNull("#pf-retry"), timeout_seconds: numOrNull("#pf-timeout"), - enabled: p ? p.enabled : true, + note: val("#pf-note"), }; - if (!providerId) { err("#pf-error", "请选择平台提供方"); return; } - if (!payload.name) { err("#pf-error", "名称必填"); return; } try { - if (isEdit) await api("/platforms/" + p.id, { method: "PUT", body: JSON.stringify(payload) }); - else await api("/platforms", { method: "POST", body: JSON.stringify(payload) }); + await api("/platforms/" + v.id, { method: "PUT", body: JSON.stringify(payload) }); closeModal(mask); - toast(isEdit ? "平台已更新" : "平台已添加"); + toast("平台监控参数已更新"); await refresh(); - switchView("platforms"); + renderPlatforms(); } catch (e) { err("#pf-error", e.message); } }; }); @@ -579,7 +551,6 @@ document.getElementById("login-password").addEventListener("keydown", (e) => { if (e.key === "Enter") doLogin(); }); document.getElementById("logout-btn").onclick = logout; document.querySelectorAll(".tab").forEach((t) => t.addEventListener("click", () => switchView(t.dataset.view))); - document.getElementById("add-platform-btn").onclick = () => platformForm(null); document.getElementById("add-account-btn").onclick = () => { if (platforms.length === 0) { toast("请先在「平台」页添加平台", "err"); @@ -708,11 +679,6 @@ await refresh(); renderPlatforms(); } catch (err2) { toast(err2.message, "err"); } - } else if (act === "del") { - confirmDialog(`确定删除平台「${escapeHtml(p ? p.name : id)}」?
其下 ${p ? p.account_count : 0} 个账号及历史记录将一并删除,此操作不可恢复。`, async () => { - try { await api("/platforms/" + id, { method: "DELETE" }); toast("平台已删除"); await refresh(); switchView("platforms"); } - catch (err2) { toast(err2.message, "err"); } - }); } } diff --git a/app/static/index.html b/app/static/index.html index 7144e13..e93cdd0 100644 --- a/app/static/index.html +++ b/app/static/index.html @@ -75,8 +75,7 @@ diff --git a/tests/test_api.py b/tests/test_api.py index b5d2bee..5468d67 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -52,21 +52,31 @@ PLATFORM_PAYLOAD = { class TestPlatforms: + def test_builtin_platforms_auto_synced(self, client): + """代码内置 provider 启动时自动同步为平台记录,无需手动添加。""" + h = _auth(client) + provs = client.get("/api/providers", headers=h).json() + plats = client.get("/api/platforms", headers=h).json() + for p in provs: + assert any(x["provider_id"] == p["id"] for x in plats), p["id"] + dp = next(x for x in plats if x["provider_id"] == "deepseek") + assert dp["name"] == "DeepSeek" and dp["currency"] == "CNY" + def test_crud_flow(self, client): h = _auth(client) pid = client.post("/api/platforms", json=PLATFORM_PAYLOAD, headers=h).json()["id"] lst = client.get("/api/platforms", headers=h).json() - assert len(lst) == 1 and lst[0]["account_count"] == 0 - assert lst[0]["provider_id"] == "deepseek" - assert lst[0]["currency"] == "CNY" # 默认取 provider 的货币 + created = next(p for p in lst if p["id"] == pid) + assert created["provider_id"] == "deepseek" + assert created["currency"] == "CNY" # 默认取 provider 的货币 upd = client.put(f"/api/platforms/{pid}", json={"currency": "USD", "interval_seconds": 300}, headers=h) assert upd.status_code == 200 - assert client.get("/api/platforms", headers=h).json()[0]["currency"] == "USD" + assert next(p for p in client.get("/api/platforms", headers=h).json() if p["id"] == pid)["currency"] == "USD" assert client.delete(f"/api/platforms/{pid}", headers=h).status_code == 200 - assert client.get("/api/platforms", headers=h).json() == [] + assert all(p["id"] != pid for p in client.get("/api/platforms", headers=h).json()) def test_duplicate_name_409(self, client): h = _auth(client)