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(`
-
${isEdit ? "编辑平台" : "添加平台"}
+
${escapeHtml(v.name || "平台")} · 监控参数
-
余额获取方式由代码内置的平台适配器决定,无需配置请求地址与提取路径。
@@ -441,37 +431,19 @@
`, (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)