import { dataFetch, isManagementAuthorized, managedFetch, onAccessChange, routes } from "../core/runtime.js"; const statusNode = document.querySelector("#price-status"); const listNode = document.querySelector("#price-list"); const editorNode = document.querySelector("#price-editor-content"); const sourceNode = document.querySelector("#price-source"); const catalogSummaryNode = document.querySelector("#catalog-summary"); const catalogResultsNode = document.querySelector("#catalog-results"); let currentPrices = []; let selectedPriceModel = ""; let currentCatalog = null; export function initializePricing() { document.querySelector("#long-enabled").addEventListener("change", event => setLongFields(event.target.checked)); document.querySelector("#save-price").addEventListener("click", savePrice); document.querySelector("#clear-price").addEventListener("click", clearPriceForm); document.querySelector("#new-price").addEventListener("click", clearPriceForm); document.querySelector("#delete-price").addEventListener("click", () => selectedPriceModel && deletePrice(selectedPriceModel)); document.querySelector("#search-catalog").addEventListener("click", searchCatalog); document.querySelector("#catalog-query").addEventListener("keydown", event => { if (event.key === "Enter") { event.preventDefault(); searchCatalog(); } }); document.querySelector("#refresh-catalog").addEventListener("click", refreshCatalog); onAccessChange(updateAccessState); } export async function loadPricing() { try { const payload = await dataFetch(routes.prices); currentPrices = payload.prices || []; renderPrices(); const selected = currentPrices.find(price => price.model === selectedPriceModel); if (selected) fillPriceForm(selected); else if (selectedPriceModel) clearPriceForm(false); else showPriceEditor(false); statusNode.textContent = ""; if (isManagementAuthorized()) await loadCatalogStatus(); else { currentCatalog = null; catalogSummaryNode.textContent = ""; renderCatalogResults([]); } } catch (error) { statusNode.textContent = "读取价格失败: " + error.message; } } function updateAccessState(authorized) { document.querySelectorAll("#view-pricing .price-editor input, #view-pricing .price-editor select").forEach(node => { if (node.id !== "catalog-query") node.disabled = !authorized; }); renderPrices(); syncLongSectionVisibility(); } function showPriceEditor(show) { editorNode.classList.toggle("hidden", !show); document.querySelector("#delete-price").classList.toggle("hidden", !selectedPriceModel); } function clearPriceForm(showEditor = true) { selectedPriceModel = ""; ["price-model", "price-input", "price-cache-read", "price-cache-write", "price-output", "long-threshold", "long-input", "long-cache-read", "long-cache-write", "long-output"].forEach(id => document.querySelector("#" + id).value = ""); document.querySelector("#long-enabled").checked = false; document.querySelector("#long-comparison").value = "gt"; document.querySelector("#fast-pricing-enabled").checked = false; document.querySelector("#fast-multiplier").value = "2.5"; showPriceSource({ kind: "manual" }); setLongFields(false); syncLongSectionVisibility(false); showPriceEditor(showEditor); renderPrices(); } function fillPriceForm(price) { selectedPriceModel = price.model; document.querySelector("#price-model").value = price.model; document.querySelector("#price-input").value = price.base.input_per_1m; document.querySelector("#price-cache-read").value = price.base.cache_read_per_1m; document.querySelector("#price-cache-write").value = price.base.cache_write_per_1m; document.querySelector("#price-output").value = price.base.output_per_1m; const long = price.long_context; document.querySelector("#long-enabled").checked = Boolean(long); document.querySelector("#long-threshold").value = long?.threshold_input_tokens ?? ""; document.querySelector("#long-comparison").value = long?.comparison || "gt"; document.querySelector("#long-input").value = long?.input_per_1m ?? ""; document.querySelector("#long-cache-read").value = long?.cache_read_per_1m ?? ""; document.querySelector("#long-cache-write").value = long?.cache_write_per_1m ?? ""; document.querySelector("#long-output").value = long?.output_per_1m ?? ""; document.querySelector("#fast-pricing-enabled").checked = price.fast_pricing_enabled; document.querySelector("#fast-multiplier").value = price.fast_multiplier || "2.5"; showPriceSource(price.source || { kind: "manual" }); setLongFields(Boolean(long)); syncLongSectionVisibility(Boolean(long)); showPriceEditor(true); document.querySelectorAll(".price-row").forEach(row => row.classList.toggle("selected", row.dataset.model === selectedPriceModel)); } function renderPrices() { if (!currentPrices.length) { listNode.replaceChildren(Object.assign(document.createElement("div"), { className: "empty", textContent: "尚未配置模型" })); return; } if (!isManagementAuthorized()) { listNode.replaceChildren(...currentPrices.map(renderPriceCard)); return; } listNode.replaceChildren(...currentPrices.map(price => { const row = document.createElement("div"); row.className = "price-row" + (price.model === selectedPriceModel ? " selected" : ""); row.dataset.model = price.model; row.tabIndex = 0; row.setAttribute("role", "button"); const text = document.createElement("div"); const model = document.createElement("div"); model.className = "price-name"; model.textContent = price.model; const detail = document.createElement("small"); detail.textContent = `输入 ${price.base.input_per_1m} · 输出 ${price.base.output_per_1m}${price.long_context ? " · 长上下文" : ""}${price.fast_pricing_enabled ? " · Fast ×" + price.fast_multiplier : ""} · ${priceSourceText(price.source)}`; text.append(model, detail); row.addEventListener("click", () => fillPriceForm(price)); row.addEventListener("keydown", event => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); fillPriceForm(price); } }); row.append(text); return row; })); } function renderPriceCard(price) { const card = document.createElement("article"); card.className = "model-price-card"; const header = document.createElement("div"); header.className = "model-price-card-header"; const name = document.createElement("h2"); name.textContent = price.model; const meta = document.createElement("div"); meta.className = "model-price-meta"; if (price.long_context) { const comparison = price.long_context.comparison === "gte" ? "≥" : ">"; meta.append(priceBadge(`${comparison} ${compactTokens(price.long_context.threshold_input_tokens)}`)); } if (price.fast_pricing_enabled) meta.append(priceBadge(`Fast ×${price.fast_multiplier || "2.5"}`)); header.append(name, meta); const table = document.createElement("div"); table.className = "model-price-table"; ["", "输入", "缓存读取", "缓存写入", "输出"].forEach(label => table.append(priceCell(label, "model-price-column"))); appendPriceRow(table, "标准", price.base); if (price.long_context) { appendPriceRow(table, "长上下文", price.long_context, true); } else { table.append( priceCell("长上下文", "model-price-row-label model-price-last-row"), priceCell("未配置", "model-price-unavailable model-price-last-row") ); } card.append(header, table); return card; } function appendPriceRow(table, labelText, values, last = false) { const lastClass = last ? " model-price-last-row" : ""; table.append(priceCell(labelText, "model-price-row-label" + lastClass)); ["input_per_1m", "cache_read_per_1m", "cache_write_per_1m", "output_per_1m"].forEach(field => { table.append(priceCell(`$${values[field] ?? "0"}`, "model-price-value" + lastClass)); }); } function priceCell(text, className) { const cell = document.createElement("div"); cell.className = className; cell.textContent = text; return cell; } function priceBadge(text) { const badge = document.createElement("span"); badge.textContent = text; return badge; } function compactTokens(value) { const number = Number(value); if (!Number.isFinite(number)) return "-"; if (number >= 1000000) return `${number / 1000000}M`; if (number >= 1000) return `${number / 1000}K`; return String(number); } async function savePrice() { try { const payload = pricePayload(); selectedPriceModel = payload.model; await managedFetch(routes.prices, { method: "PUT", body: JSON.stringify(payload) }); statusNode.textContent = "价格已保存"; await loadPricing(); showPriceSource({ kind: "manual" }); } catch (error) { statusNode.textContent = "保存失败: " + error.message; } } async function deletePrice(model) { if (!confirm(`删除 ${model} 的真实价格配置?`)) return; try { await managedFetch(routes.prices, { method: "DELETE", body: JSON.stringify({ model }) }); if (selectedPriceModel === model) clearPriceForm(false); await loadPricing(); } catch (error) { statusNode.textContent = "删除失败: " + error.message; } } function pricePayload() { const value = id => document.querySelector("#" + id).value.trim(); const payload = { model: value("price-model"), base: { input_per_1m: value("price-input"), cache_read_per_1m: value("price-cache-read"), cache_write_per_1m: value("price-cache-write"), output_per_1m: value("price-output") }, fast_pricing_enabled: document.querySelector("#fast-pricing-enabled").checked, fast_multiplier: value("fast-multiplier") }; if (document.querySelector("#long-enabled").checked) { payload.long_context = { threshold_input_tokens: Number(value("long-threshold")), comparison: value("long-comparison"), input_per_1m: value("long-input"), cache_read_per_1m: value("long-cache-read"), cache_write_per_1m: value("long-cache-write"), output_per_1m: value("long-output") }; } return payload; } function renderCatalogResults(models) { if (!models.length) { catalogResultsNode.replaceChildren(); return; } catalogResultsNode.replaceChildren(...models.map(entry => { const row = document.createElement("div"); row.className = "catalog-result"; const text = document.createElement("div"); const title = document.createElement("strong"); title.textContent = `${entry.provider_name || entry.provider} · ${entry.model_name || entry.model}`; const detail = document.createElement("small"); detail.textContent = `${entry.id} · 输入 ${entry.base.input_per_1m} / 输出 ${entry.base.output_per_1m}${entry.long_context ? " · 长上下文" : ""}`; text.append(title, detail); row.append(text); if (isManagementAuthorized()) { const use = document.createElement("button"); use.type = "button"; use.className = "small admin-only"; use.textContent = "使用"; use.addEventListener("click", () => importCatalogPrice(entry)); row.append(use); } return row; })); } async function searchCatalog() { const query = document.querySelector("#catalog-query").value.trim().toLowerCase(); try { const payload = await dataFetch(routes.catalog + "?q=" + encodeURIComponent(query)); catalogSummaryNode.textContent = catalogSummary(payload); renderCatalogResults(payload.models || []); } catch (error) { catalogSummaryNode.textContent = "读取参考价格失败:" + error.message; renderCatalogResults([]); } } async function loadCatalogStatus() { await searchCatalog(); } async function importCatalogPrice(entry) { const localModel = document.querySelector("#price-model").value.trim() || entry.model; if (!localModel) { statusNode.textContent = "请先输入本地模型名称"; return; } try { const imported = await managedFetch(routes.priceImport, { method: "POST", body: JSON.stringify({ model: localModel, catalog_id: entry.id }) }); await loadPricing(); fillPriceForm(imported); statusNode.textContent = `${localModel} 已采用 ${entry.provider_name || entry.provider} / ${entry.model} 参考价`; } catch (error) { statusNode.textContent = "导入参考价格失败:" + error.message; } } async function refreshCatalog() { try { catalogSummaryNode.textContent = "正在更新 models.dev 目录"; const preview = await managedFetch(routes.catalogRefresh, { method: "POST" }); currentCatalog = preview.catalog; const changed = (preview.changes || []).filter(change => change.status === "changed"); const missing = (preview.changes || []).filter(change => change.status === "missing"); if (!changed.length) { catalogSummaryNode.textContent = `目录已更新 · 没有价格变化${missing.length ? ` · ${missing.length} 个来源已下架` : ""}`; await searchCatalog(); return; } const lines = changed.slice(0, 8).map(change => `${change.model}: 输入 ${change.before.base.input_per_1m} → ${change.after.base.input_per_1m},输出 ${change.before.base.output_per_1m} → ${change.after.base.output_per_1m}`); const confirmed = confirm(`发现 ${changed.length} 个已关联价格发生变化:\n\n${lines.join("\n")}${changed.length > lines.length ? "\n…" : ""}\n\n确认更新这些本地价格?`); if (!confirmed) { catalogSummaryNode.textContent = `目录已更新 · ${changed.length} 个变化尚未应用`; return; } const applied = await managedFetch(routes.catalogApply, { method: "POST", body: JSON.stringify({ revision: preview.catalog.revision, models: changed.map(change => change.model) }) }); catalogSummaryNode.textContent = `已确认更新 ${applied.updated} 个本地价格`; await loadPricing(); } catch (error) { catalogSummaryNode.textContent = "更新目录失败:" + error.message; } } function catalogSummary(payload) { currentCatalog = payload?.catalog || null; if (!payload?.loaded) return payload?.last_error ? `尚无可用目录 · ${payload.last_error}` : "尚未下载目录"; const updated = currentCatalog?.fetched_at ? new Date(currentCatalog.fetched_at).toLocaleString() : "-"; return `${currentCatalog?.models || 0} 条参考价格 · ${updated}`; } function priceSourceText(source) { if (source?.kind === "models.dev") return `models.dev · ${source.provider || "-"} / ${source.model || source.catalog_id || "-"}`; return "手动配置"; } function showPriceSource(source) { sourceNode.textContent = priceSourceText(source); sourceNode.dataset.kind = source?.kind || "manual"; } function setLongFields(enabled) { document.querySelectorAll(".long-field").forEach(node => node.classList.toggle("hidden", !enabled)); } function syncLongSectionVisibility(hasLong = document.querySelector("#long-enabled").checked) { document.querySelector("#long-price-section").classList.toggle("hidden", !isManagementAuthorized() && !hasLong); }