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 个

This commit is contained in:
2026-08-05 00:56:22 +08:00
parent 1475821519
commit bdeb353bc2
16 changed files with 704 additions and 169 deletions
+40 -35
View File
@@ -6,6 +6,7 @@
let token = localStorage.getItem(TOKEN_KEY) || "";
let accounts = [];
let platforms = [];
let providersList = [];
let settings = null;
let refreshTimer = null;
let filterStatus = "all";
@@ -141,10 +142,13 @@
async function refresh() {
try {
const [acc, plat, hist] = await Promise.all([api("/accounts"), api("/platforms"), api("/history")]);
const [acc, plat, hist, provs] = await Promise.all([
api("/accounts"), api("/platforms"), api("/history"), api("/providers"),
]);
accounts = acc;
platforms = plat;
historyCache = hist || {};
providersList = provs || [];
renderAccounts();
if (!document.getElementById("view-platforms").classList.contains("hidden")) renderPlatforms();
const d = new Date();
@@ -347,6 +351,7 @@
}
list.innerHTML = platforms.map((p, i) => {
const b = brandStyle(p.icon || "");
const prov = providersList.find((x) => x.id === p.provider_id);
return `
<div class="platform-row ${p.enabled ? "" : "platform-off"}" style="animation-delay:${Math.min(i, 10) * 35}ms">
<div class="badge" style="background:${b.color}">${escapeHtml(b.abbr)}</div>
@@ -355,11 +360,11 @@
${p.enabled ? "" : '<span class="badge-pill disabled">已停用</span>'}
</div>
<div class="platform-meta">
${escapeHtml(p.method)} ${escapeHtml(p.url)} ·
提取 ${escapeHtml(p.balance_path)} ·
${prov ? escapeHtml(prov.name) : "未知提供方"} ·
${escapeHtml(p.currency)} ·
间隔 ${p.interval_seconds || "全局"}s ·
${p.account_count} 个账号
${prov && prov.description ? `<div style="color:var(--text-3)">${escapeHtml(prov.description)}</div>` : ""}
</div>
</div>
<div class="platform-actions">
@@ -407,35 +412,28 @@
function platformForm(p) {
const isEdit = !!p;
const v = p || {};
let savedHeaders = v.headers || {};
if (typeof savedHeaders === "string") {
try { savedHeaders = JSON.parse(savedHeaders); } catch (_) { savedHeaders = {}; }
}
const headersStr = Object.keys(savedHeaders).length ? JSON.stringify(savedHeaders, null, 2) : '{\n "Authorization": "Bearer {{apiKey}}"\n}';
const provOpts = providersList.length
? providersList.map((x) => `<option value="${x.id}" ${v.provider_id === x.id ? "selected" : ""}>${escapeHtml(x.name)}</option>`).join("")
: '<option value="">加载中…</option>';
const prov = providersList.find((x) => x.id === v.provider_id);
openModal(`
<div class="modal">
<h3>${isEdit ? "编辑平台" : "添加平台"}</h3>
<div class="form-grid">
<div class="form-row"><label>名称 *</label><input id="pf-name" value="${escapeHtml(v.name || "")}" placeholder="如 OpenAI"></div>
<div class="form-row"><label>货币单位 *</label><input id="pf-currency" value="${escapeHtml(v.currency || "USD")}"></div>
<div class="form-row"><label>图标键(@lobehub/icons</label><input id="pf-icon" value="${escapeHtml(v.icon || "")}" placeholder="如 OpenAI / DeepSeek"></div>
<div class="form-row">
<label>请求方法</label>
<select id="pf-method">
<option value="GET" ${(v.method || "GET") === "GET" ? "selected" : ""}>GET</option>
<option value="POST" ${v.method === "POST" ? "selected" : ""}>POST</option>
</select>
<div class="form-row full">
<label>平台提供方 *(代码内置)</label>
<select id="pf-provider">${provOpts}</select>
<div class="form-hint" id="pf-prov-desc">${prov ? escapeHtml(prov.description) : ""}</div>
</div>
<div class="form-row full"><label>URL *(支持 {{apiKey}}</label><input id="pf-url" value="${escapeHtml(v.url || "")}" placeholder="https://api.openai.com/v1/dashboard/billing/credit_grants?api_key={{apiKey}}"></div>
<div class="form-row full"><label>HeadersJSON,值支持 {{apiKey}}</label><textarea id="pf-headers">${escapeHtml(headersStr)}</textarea></div>
<div class="form-row full"><label>BodyPOST 时使用,JSON 模板,支持 {{apiKey}}</label><textarea id="pf-body" placeholder='{"api_key": "{{apiKey}}"}'>${escapeHtml(v.body || "")}</textarea></div>
<div class="form-row"><label>余额提取路径 *</label><input id="pf-path" value="${escapeHtml(v.balance_path || "")}" placeholder="data.balance 或 data[0].balance"></div>
<div class="form-row"><label>名称 *</label><input id="pf-name" value="${escapeHtml(v.name || "")}"></div>
<div class="form-row"><label>货币单位</label><input id="pf-currency" value="${escapeHtml(v.currency || "")}" placeholder="默认来自提供方"></div>
<div class="form-row"><label>图标键(@lobehub/icons</label><input id="pf-icon" value="${escapeHtml(v.icon || "")}" placeholder="默认来自提供方"></div>
<div class="form-row"><label>监控间隔(秒,留空用全局)</label><input id="pf-interval" type="number" min="10" value="${v.interval_seconds ?? ""}" placeholder="全局 ${settings ? settings.global_interval_seconds : 300}s"></div>
<div class="form-row"><label>重试次数(留空用全局 ${settings ? settings.retry_count : 2}</label><input id="pf-retry" type="number" min="0" max="10" value="${v.retry_count ?? ""}"></div>
<div class="form-row"><label>超时秒数(留空用全局 ${settings ? settings.timeout_seconds : 10}</label><input id="pf-timeout" type="number" min="1" max="120" value="${v.timeout_seconds ?? ""}"></div>
<div class="form-row full"><label>备注</label><input id="pf-note" value="${escapeHtml(v.note || "")}"></div>
</div>
<p class="form-hint">提示:apikey 在 URL / Header / Body 中统一用 {{apiKey}} 占位,添加账号时自动替换。Headers/Body 需为 JSON,键值用双引号(单引号会自动兼容)。</p>
<p class="form-hint">余额获取方式由代码内置的平台适配器决定,无需配置请求地址与提取路径。</p>
<p class="modal-error" id="pf-error"></p>
<div class="modal-actions">
<button class="btn" data-cancel>取消</button>
@@ -443,23 +441,30 @@
</div>
</div>`, (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 = {
name: val("#pf-name"), currency: val("#pf-currency"), icon: val("#pf-icon"),
method: val("#pf-method"), url: val("#pf-url"), balance_path: val("#pf-path"),
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,
interval_seconds: numOrNull("#pf-interval"),
retry_count: numOrNull("#pf-retry"),
timeout_seconds: numOrNull("#pf-timeout"),
enabled: p ? p.enabled : true,
};
let headers = parseJsonInput(val("#pf-headers"), "Headers", "#pf-error");
if (headers === null) return;
payload.headers = headers;
if (!payload.name || !payload.url || !payload.balance_path) { err("#pf-error", "名称 / URL / 提取路径必填"); return; }
if (payload.method === "POST") {
const bodyObj = parseJsonInput(val("#pf-body"), "Body", "#pf-error");
if (bodyObj === null) return;
}
payload.body = val("#pf-body");
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) });