mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-10-03 23:54:06 +00:00
refactor(quota): 配额改为按模型,删除整钥总配额
用户明确要求:配额应当是密钥对应的**每个模型的单独配额**,而非整体配额。 ## 语义变更 删除 GWKey.TokenQuota / ReqQuota / Period / Hours(整钥总额)。 ModelScope 新增 ReqQuota —— 请求数配额下沉到每条模型范围。 现在:每条 models[] 各自带 token 配额 + 请求数配额 + 重置周期, 彼此独立。一个模型用满只影响该模型。 ★ 为什么不保留整钥总额:它会让「把 A 模型的额度挪给 B」变成一次全局 重分配;按模型独立计费则每个模型各自可控,运维能直接看出哪个模型在吃预算。 ## 连带改动 - checkQuota 合并 key 级与 scope 级判定;checkKeyQuotaRetry 整体删除 (顺带修掉上轮遗留的双重判定:入口不再先判空再重算) - core:CreateKeyWithQuota / UpdateKeyWithQuota / ApplyQuota 全部删除, 改由 ValidateScopeQuotas 校验每条 scope 的配额 - admin key:scope 上的配额不强制(admin 的 scope 仍限制模型范围, 但不强制配额)—— 否则管理员会把自己锁在门外 - /api/v1/keys 不再回显 key 级配额字段(scope 里已含) - WebUI:删除整钥配额徽标 / 「配额」按钮 / 创建表单的配额组 / putScope 的整钥回传;模型砖块与范围编辑器新增「请求数配额」输入, 徽标显示 `1.0K 77×·1h`(未设配额显示 ∞) ## 判据 - TestOneModelsQuotaDoesNotBlockAnother 是本次核心保证。 ★ 它第一版是**假判据**:m2 从不消耗,key-wide 计数器与 m1 自己的计数器 读数恰好相同,退回 key-wide 仍通过。变异测试抓到后改为「先用 m2 花掉 远超 m1 配额的量,再验证 m1 仍可用」—— 这样两种设计才可区分。 - TestUncappedModelNeverBlocked / TestAdminKeyScopesAreNotEnforced 新增 - UI 契约判据重写:整钥配额界面必须彻底消失(13 个符号)、 scope 编辑器必须往返 req_quota、putScope 只发 scope 列表 - 错误消息点名具体模型(TestKeyAPIRejectionNamesTheModel) - 3/3 变异全被抓 实测(真实进程 + 浏览器):m2 配额 500000 连打 25 次全成功, m1 配额 1000 立即 429「token quota exceeded for "m1" (4315/1000)」, 此后 m2/m3 仍 200。UI:整钥配额元素全为 0,砖块各显配额, 编辑器预填/保存正确,零 JS 异常。
This commit is contained in:
@ -339,9 +339,6 @@
|
||||
.key-canvas{border:1px solid var(--line);border-radius:16px;padding:14px;margin-bottom:14px;background:var(--card);
|
||||
backdrop-filter:blur(var(--glass));box-shadow:var(--sh-sm)}
|
||||
.kc-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
||||
/* key-wide caps sit inline in the head: they belong to the key, not to
|
||||
any one model brick, and must not be draggable with one. */
|
||||
.kc-caps{display:inline-flex;align-items:center;gap:6px;flex-wrap:wrap}
|
||||
.kc-blocks{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin-top:12px;background:var(--card2);
|
||||
border:1px dashed var(--line);border-radius:12px;padding:14px;min-height:64px}
|
||||
.kc-blocks.ovh{outline:2px dashed var(--primary);outline-offset:2px}
|
||||
@ -821,15 +818,10 @@
|
||||
kAnySrc: "任意源",
|
||||
kQuotaB: "Token 配额",
|
||||
kQuotaHintB: "0 / 留空 = 无限",
|
||||
kKeyQuota: "密钥总配额",
|
||||
kKeyQuotaHint:
|
||||
"限制这把密钥在重置周期内的总用量(跳模型)。0 / 留空 = 无限。",
|
||||
kKeyReqQuota: "请求数配额",
|
||||
kKeyReqQuotaHint: "限制周期内的请求次数。0 / 留空 = 无限。",
|
||||
kKeyQuotaAdmin:
|
||||
"admin 密钥永不受配额限制(避免把管理员锁在门外)。",
|
||||
kKeyQuotaEdit: "配额",
|
||||
kKeyQuotaNone: "无限",
|
||||
kReqQuota: "请求数配额",
|
||||
kReqQuotaHint: "限制周期内的请求次数。0 / 留空 = 无限。",
|
||||
kQuotaPerModelHint:
|
||||
"配额按模型单独设置:创建后点「+ 添加模型」,逐个模型配 token 配额与周期。一个模型用满只影响该模型,同一密钥的其它模型照常。",
|
||||
kPeriodB: "重置周期",
|
||||
kPerNothing: "不限",
|
||||
kPerHour: "每 小时",
|
||||
@ -1055,16 +1047,10 @@
|
||||
kAnySrc: "any source",
|
||||
kQuotaB: "Token quota",
|
||||
kQuotaHintB: "0 / empty = unlimited",
|
||||
kKeyQuota: "Key-wide quota",
|
||||
kKeyQuotaHint:
|
||||
"Caps this key's total spend per reset window, across every model it may use. 0 / empty = unlimited.",
|
||||
kKeyReqQuota: "Request quota",
|
||||
kKeyReqQuotaHint:
|
||||
"Caps requests per window. 0 / empty = unlimited.",
|
||||
kKeyQuotaAdmin:
|
||||
"Admin keys are never capped — a cap could lock the operator out.",
|
||||
kKeyQuotaEdit: "Quota",
|
||||
kKeyQuotaNone: "unlimited",
|
||||
kReqQuota: "Request quota",
|
||||
kReqQuotaHint: "Caps requests per window. 0 / empty = unlimited.",
|
||||
kQuotaPerModelHint:
|
||||
"Quotas are per model: after creating the key, use \"Add model\" to give each model its own token budget and reset period. One model running out affects only that model; the key's other models keep working.",
|
||||
kPeriodB: "Reset period",
|
||||
kPerNothing: "Never",
|
||||
kPerHour: "Every hour",
|
||||
@ -4266,11 +4252,10 @@
|
||||
async function renderKeysUser(me) {
|
||||
$("#tab-keys").innerHTML = `
|
||||
<div class="card"><h2>${t("kMeTitle")}</h2>
|
||||
<div class="tbl-wrap"><table><tr><th>${t("kName")}</th><th>${t("kMeRole")}</th><th>${t("kKey")}</th><th>${t("kKeyQuota")}</th><th>${t("kMeModels")}</th></tr>
|
||||
<div class="tbl-wrap"><table><tr><th>${t("kName")}</th><th>${t("kMeRole")}</th><th>${t("kKey")}</th><th>${t("kMeModels")}</th></tr>
|
||||
<tr><td><b>${esc(me.name || "—")}</b></td><td>${roleTag(me.role)}</td>
|
||||
<td><span class="kr-key">${esc(me.key)}</span>
|
||||
<button class="ghost small" onclick="copyText('${escAttr(me.key)}')">${t("kCopy")}</button></td>
|
||||
<td>${keyCapBadges(me)}</td>
|
||||
<td>${
|
||||
me.models && me.models.length
|
||||
? me.models
|
||||
@ -4308,22 +4293,13 @@
|
||||
}
|
||||
function keyCanvasHtml(k) {
|
||||
const scopes = k.models || [];
|
||||
// Key-wide caps live on the canvas, not on a brick: they are a budget
|
||||
// the whole key shares, so they must not be dragged around with one
|
||||
// model. data-* carries them so a quota edit can round-trip them
|
||||
// through the same PUT that saves the model scope.
|
||||
const caps = `data-kquota="${k.token_quota || 0}" data-kreqquota="${k.req_quota || 0}"
|
||||
data-kperiod="${escAttr(k.period || "")}" data-khours="${k.hours || 0}"`;
|
||||
return `
|
||||
<div class="key-canvas" data-key="${escAttr(k.key)}" ${caps}>
|
||||
<div class="key-canvas" data-key="${escAttr(k.key)}">
|
||||
<div class="kc-head">
|
||||
<b>${esc(k.name || "—")}</b>
|
||||
${roleTag(k.role)}
|
||||
<span class="kr-key">${esc(maskKey(k.key))}</span>
|
||||
<button class="ghost small" onclick="copyText('${escAttr(k.key)}')">${t("kCopy")}</button>
|
||||
<span class="kc-caps" title="${escAttr(t("kKeyQuotaHint"))}">${keyCapBadges(k)}</span>
|
||||
${k.role === "admin" ? "" : `<button class="ghost small" title="${escAttr(t("kKeyQuotaEdit"))}"
|
||||
onclick="keyQuotaEdit('${escAttr(k.key)}')">${t("kKeyQuotaEdit")}</button>`}
|
||||
<span class="grow"></span>
|
||||
<span class="muted">${fmtCreated(k.created_at)}</span>
|
||||
<button class="ghost small errc" onclick="delKey('${escAttr(k.key)}','${escAttr(k.name || "")}')">${t("kDel")}</button>
|
||||
@ -4336,39 +4312,19 @@
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
// keyCapBadges renders the key-wide caps. A cap with no reset period is
|
||||
// flagged as such, because "1M tokens, never resets" and "1M tokens per
|
||||
// hour" are very different promises and the badge must not blur them.
|
||||
function keyCapBadges(k) {
|
||||
const out = [];
|
||||
const suffix = periodText(k.period || "", k.hours || 0);
|
||||
if (+k.token_quota > 0) {
|
||||
out.push(
|
||||
`<span class="mb-quota" title="${escAttr(t("kKeyQuotaHint"))}">${esc(fmtQuota(k.token_quota))}${esc(suffix)}</span>`,
|
||||
);
|
||||
}
|
||||
if (+k.req_quota > 0) {
|
||||
out.push(
|
||||
`<span class="mb-quota" title="${escAttr(t("kKeyReqQuotaHint"))}">${esc(fmtQuota(k.req_quota))}×${esc(suffix)}</span>`,
|
||||
);
|
||||
}
|
||||
if (!out.length) {
|
||||
return `<span class="muted">${t("kKeyQuotaNone")}</span>`;
|
||||
}
|
||||
return out.join(" ");
|
||||
}
|
||||
function scopeHtml(key, m) {
|
||||
const qt = fmtQuota(m.token_quota);
|
||||
const comb = scopeComb(m);
|
||||
const src = normSrc(m.source);
|
||||
const attrs = `data-key="${escAttr(key)}" data-model="${escAttr(comb)}"
|
||||
data-quota="${m.token_quota || 0}" data-period="${escAttr(m.period || "")}" data-hours="${m.hours || 0}"`;
|
||||
data-quota="${m.token_quota || 0}" data-reqquota="${m.req_quota || 0}"
|
||||
data-period="${escAttr(m.period || "")}" data-hours="${m.hours || 0}"`;
|
||||
return `<span class="mb" draggable="true" ${attrs} title="${escAttr(t("kBrickH"))}"
|
||||
onclick="scopeEdit('${escAttr(key)}','${escAttr(comb)}')"
|
||||
oncontextmenu="scopeCtx(event,'${escAttr(key)}','${escAttr(comb)}')">
|
||||
<span class="mb-ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="10" height="10"><circle cx="12" cy="12" r="10"/></svg></span>
|
||||
<span class="mb-name">${esc(m.model)}${src ? `<em class="mb-src">${esc(src)}</em>` : ""}</span>
|
||||
<span class="mb-quota">${esc(quantBadge(m.token_quota, m.period, m.hours))}</span>
|
||||
<span class="mb-quota">${esc(scopeQuotaBadge(m))}</span>
|
||||
<span class="copy-b" title="${escAttr(t("kCopyB"))}" onclick="event.stopPropagation();scopeDup('${escAttr(key)}','${escAttr(comb)}')"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="12" height="12"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></span>
|
||||
<span class="mb-x" title="${escAttr(t("kDelB"))}" onclick="event.stopPropagation();scopeRm('${escAttr(key)}','${escAttr(comb)}')"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="12" height="12"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></span>
|
||||
</span>`;
|
||||
@ -4400,6 +4356,16 @@
|
||||
if (p === "nhour") return "·" + Math.max(1, h) + "h";
|
||||
return "";
|
||||
}
|
||||
// scopeQuotaBadge shows a scope entry's budgets: token quota and, when
|
||||
// set, the request count. They are per model — a spent budget blocks
|
||||
// only that model, not the whole key.
|
||||
function scopeQuotaBadge(m) {
|
||||
const parts = [];
|
||||
if (+m.token_quota > 0) parts.push(fmtQuota(m.token_quota));
|
||||
if (+m.req_quota > 0) parts.push(fmtQuota(m.req_quota) + "\u00d7");
|
||||
if (!parts.length) return "\u221e";
|
||||
return parts.join(" ") + periodText(m.period, m.hours);
|
||||
}
|
||||
function quantBadge(quota, period, hours) {
|
||||
quota = +quota || 0;
|
||||
period = period || "";
|
||||
@ -4414,123 +4380,23 @@
|
||||
model: comb[0],
|
||||
source: src || undefined,
|
||||
token_quota: parseInt(b.dataset.quota) || 0,
|
||||
req_quota: parseInt(b.dataset.reqquota) || 0,
|
||||
period: b.dataset.period || "",
|
||||
hours: parseInt(b.dataset.hours) || 0,
|
||||
};
|
||||
});
|
||||
}
|
||||
async function putScope(key, scopes) {
|
||||
// The key-wide caps ride along with every scope write. The API reads
|
||||
// them as pointers, so sending them back unchanged is a no-op, while
|
||||
// omitting them would be indistinguishable from "clear the budget" to
|
||||
// a future reader. Round-tripping them here means editing a model's
|
||||
// scope can never silently drop a key's quota.
|
||||
const canvas = document.querySelector(
|
||||
`.key-canvas[data-key="${CSS.escape(key)}"]`,
|
||||
);
|
||||
// Each scope entry carries its own quotas, so the whole budget travels
|
||||
// with the models it applies to. There is no separate key-wide total
|
||||
// that could drift out of sync with the model list.
|
||||
const body = { models: scopes };
|
||||
if (canvas) {
|
||||
body.token_quota = parseInt(canvas.dataset.kquota) || 0;
|
||||
body.req_quota = parseInt(canvas.dataset.kreqquota) || 0;
|
||||
body.period = canvas.dataset.kperiod || "";
|
||||
body.hours = parseInt(canvas.dataset.khours) || 0;
|
||||
}
|
||||
await api("/api/keys/" + encodeURIComponent(key), {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
// keyQuotaEdit opens the key-wide budget form.
|
||||
function keyQuotaEdit(key) {
|
||||
const canvas = document.querySelector(
|
||||
`.key-canvas[data-key="${CSS.escape(key)}"]`,
|
||||
);
|
||||
if (!canvas) return;
|
||||
const cur = {
|
||||
token_quota: parseInt(canvas.dataset.kquota) || 0,
|
||||
req_quota: parseInt(canvas.dataset.kreqquota) || 0,
|
||||
period: canvas.dataset.kperiod || "",
|
||||
hours: parseInt(canvas.dataset.khours) || 0,
|
||||
};
|
||||
const wrap = document.createElement("div");
|
||||
wrap.id = "modal-wrap";
|
||||
wrap.style.cssText =
|
||||
"position:fixed;inset:0;background:rgba(15,22,44,.45);display:flex;align-items:flex-start;justify-content:center;overflow:auto;padding:48px 20px;z-index:50";
|
||||
wrap.innerHTML = `<div class="card" style="width:400px;max-width:100%"><h2>${t("kKeyQuotaEdit")}</h2>
|
||||
<label>${t("kKeyQuota")} <span class="muted">${t("kKeyQuotaHint")}</span></label>
|
||||
<input id="kq-tokens" type="number" min="0" step="1"
|
||||
placeholder="${escAttr(t("kKeyQuotaNone"))}" value="${cur.token_quota || ""}">
|
||||
<label>${t("kKeyReqQuota")} <span class="muted">${t("kKeyReqQuotaHint")}</span></label>
|
||||
<input id="kq-reqs" type="number" min="0" step="1"
|
||||
placeholder="${escAttr(t("kKeyQuotaNone"))}" value="${cur.req_quota || ""}">
|
||||
<label>${t("kPeriodB")}</label>
|
||||
<select id="kq-period">
|
||||
<option value="" ${!cur.period ? "selected" : ""}>${t("kPerNothing")}</option>
|
||||
<option value="hour" ${cur.period === "hour" ? "selected" : ""}>${t("kPerHour")}</option>
|
||||
<option value="week" ${cur.period === "week" ? "selected" : ""}>${t("kPerWeek")}</option>
|
||||
<option value="month" ${cur.period === "month" ? "selected" : ""}>${t("kPerMonth")}</option>
|
||||
<option value="nhour" ${cur.period === "nhour" ? "selected" : ""}>${t("kPerHours")}</option>
|
||||
</select>
|
||||
<div id="kq-hours-box" style="display:none"><label>${t("kPerNHint")}</label>
|
||||
<input id="kq-hours" type="number" min="1" step="1" value="${cur.hours || 24}"></div>
|
||||
<p class="muted" style="font-size:12px">${t("kKeyQuotaAdmin")}</p>
|
||||
<p><button onclick="keyQuotaSave('${escAttr(key)}', this)">${t("kSaveScope")}</button>
|
||||
<button class="ghost" onclick="this.closest('#modal-wrap').remove()">${t("mCancel")}</button></p>
|
||||
</div>`;
|
||||
document.body.appendChild(wrap);
|
||||
const toggle = () => {
|
||||
$("#kq-hours-box").style.display =
|
||||
$("#kq-period").value === "nhour" ? "block" : "none";
|
||||
};
|
||||
$("#kq-period").addEventListener("change", toggle);
|
||||
toggle();
|
||||
$("#kq-tokens").focus();
|
||||
}
|
||||
async function keyQuotaSave(key, btn) {
|
||||
// Resolve our own dialog from the button that was clicked, so closing
|
||||
// it can never remove a different #modal-wrap that happens to come
|
||||
// first in the document.
|
||||
const wrap = btn ? btn.closest("#modal-wrap") : null;
|
||||
let tokens = parseInt($("#kq-tokens").value);
|
||||
if (isNaN(tokens) || tokens < 0) tokens = 0;
|
||||
let reqs = parseInt($("#kq-reqs").value);
|
||||
if (isNaN(reqs) || reqs < 0) reqs = 0;
|
||||
let hours = parseInt($("#kq-hours").value);
|
||||
if (isNaN(hours) || hours < 1) hours = 1;
|
||||
const period = $("#kq-period").value;
|
||||
// Catch the "nhour picked but hours never filled in" case locally: the
|
||||
// API rejects it too, but a round trip for a form-level mistake is
|
||||
// needless.
|
||||
if (period === "nhour" && hours < 1) {
|
||||
toast(t("kPerNHint"));
|
||||
return;
|
||||
}
|
||||
if (btn) btn.disabled = true;
|
||||
try {
|
||||
await api("/api/keys/" + encodeURIComponent(key), {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
token_quota: tokens,
|
||||
req_quota: reqs,
|
||||
period,
|
||||
hours,
|
||||
}),
|
||||
});
|
||||
// Close THIS modal, not whichever #modal-wrap comes first in the
|
||||
// document: another dialog (e.g. the seed-key notice) may already be
|
||||
// open, and a bare $("#modal-wrap") would remove that one and leave
|
||||
// this form stranded on screen.
|
||||
if (wrap) wrap.remove();
|
||||
else closeTopModal();
|
||||
toast(t("kSaved"));
|
||||
await loadKeys();
|
||||
} catch (e) {
|
||||
toast(e.message);
|
||||
if (btn) btn.disabled = false;
|
||||
}
|
||||
}
|
||||
async function scopePush(key) {
|
||||
const canvas = document.querySelector(
|
||||
`.key-canvas[data-key="${CSS.escape(key)}"]`,
|
||||
@ -4631,6 +4497,9 @@
|
||||
<label>${t("kQuotaB")} <span class="muted">${t("kQuotaHintB")}</span></label>
|
||||
<input id="sc-quota" type="number" min="0" step="1"
|
||||
placeholder="${escAttr(t("kQuotaHintB"))}" value="${sc.token_quota ? sc.token_quota : ""}">
|
||||
<label>${t("kReqQuota")} <span class="muted">${t("kReqQuotaHint")}</span></label>
|
||||
<input id="sc-reqs" type="number" min="0" step="1"
|
||||
placeholder="${escAttr(t("kReqQuotaHint"))}" value="${sc.req_quota || ""}">
|
||||
<label>${t("kPeriodB")}</label>
|
||||
<select id="sc-period">
|
||||
<option value="" ${!sc.period ? "selected" : ""}>${t("kPerNothing")}</option>
|
||||
@ -4664,6 +4533,8 @@
|
||||
const parts = splitCombKey(comb);
|
||||
let q = parseInt($("#sc-quota").value);
|
||||
if (isNaN(q) || q < 0) q = 0;
|
||||
let rq = parseInt($("#sc-reqs").value);
|
||||
if (isNaN(rq) || rq < 0) rq = 0;
|
||||
let hours = parseInt($("#sc-hours").value);
|
||||
if (isNaN(hours) || hours < 1) hours = 1;
|
||||
const period = $("#sc-period").value;
|
||||
@ -4672,6 +4543,7 @@
|
||||
model: parts.model,
|
||||
source: parts.source || undefined,
|
||||
token_quota: q,
|
||||
req_quota: rq,
|
||||
period,
|
||||
hours,
|
||||
};
|
||||
@ -4832,41 +4704,13 @@
|
||||
<option value="user">${t("kRoleUser")}</option>
|
||||
<option value="admin">${t("kRoleAdmin")}</option>
|
||||
</select>
|
||||
<label>${t("kKeyQuota")} <span class="muted">${t("kKeyQuotaHint")}</span></label>
|
||||
<input id="kc-tokens" type="number" min="0" step="1" placeholder="${escAttr(t("kKeyQuotaNone"))}">
|
||||
<label>${t("kKeyReqQuota")} <span class="muted">${t("kKeyReqQuotaHint")}</span></label>
|
||||
<input id="kc-reqs" type="number" min="0" step="1" placeholder="${escAttr(t("kKeyQuotaNone"))}">
|
||||
<label>${t("kPeriodB")}</label>
|
||||
<select id="kc-period">
|
||||
<option value="" selected>${t("kPerNothing")}</option>
|
||||
<option value="hour">${t("kPerHour")}</option>
|
||||
<option value="week">${t("kPerWeek")}</option>
|
||||
<option value="month">${t("kPerMonth")}</option>
|
||||
<option value="nhour">${t("kPerHours")}</option>
|
||||
</select>
|
||||
<div id="kc-hours-box" style="display:none"><label>${t("kPerNHint")}</label>
|
||||
<input id="kc-hours" type="number" min="1" step="1" value="24"></div>
|
||||
<p class="muted" style="font-size:12px">${t("kQuotaPerModelHint")}</p>
|
||||
<label>${t("kNote")}</label>
|
||||
<input id="kc-note">
|
||||
<p class="muted" style="font-size:12px">${t("kKeyQuotaAdmin")}</p>
|
||||
<p><button onclick="createKey(this)">${t("kCreateBtn")}</button>
|
||||
<button class="ghost" onclick="this.closest('#modal-wrap').remove()">${t("mCancel")}</button></p>
|
||||
</div>`;
|
||||
document.body.appendChild(wrap);
|
||||
$("#kc-role").addEventListener("change", () => {
|
||||
const admin = $("#kc-role").value === "admin";
|
||||
// An admin key ignores its caps server-side; hiding the fields
|
||||
// avoids the operator setting one and wondering why it never trips.
|
||||
$("#kc-tokens").disabled = admin;
|
||||
$("#kc-reqs").disabled = admin;
|
||||
$("#kc-period").disabled = admin;
|
||||
$("#kc-hours-box").style.display =
|
||||
!admin && $("#kc-period").value === "nhour" ? "block" : "none";
|
||||
});
|
||||
$("#kc-period").addEventListener("change", () => {
|
||||
$("#kc-hours-box").style.display =
|
||||
$("#kc-period").value === "nhour" ? "block" : "none";
|
||||
});
|
||||
$("#kc-name").focus();
|
||||
}
|
||||
async function createKey(btn) {
|
||||
@ -4875,17 +4719,6 @@
|
||||
toast(t("kName"));
|
||||
return;
|
||||
}
|
||||
const role = $("#kc-role").value;
|
||||
// An admin key is never capped; send the fields anyway (the server
|
||||
// ignores them) rather than special-casing the request shape.
|
||||
const readNum = (sel) => {
|
||||
const el = $(sel);
|
||||
if (el.disabled) return 0;
|
||||
const n = parseInt(el.value);
|
||||
return isNaN(n) || n < 0 ? 0 : n;
|
||||
};
|
||||
let hours = parseInt($("#kc-hours").value);
|
||||
if (isNaN(hours) || hours < 1) hours = 1;
|
||||
if (btn) btn.disabled = true;
|
||||
let j;
|
||||
try {
|
||||
@ -4894,12 +4727,8 @@
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
role,
|
||||
role: $("#kc-role").value,
|
||||
note: $("#kc-note").value.trim(),
|
||||
token_quota: readNum("#kc-tokens"),
|
||||
req_quota: readNum("#kc-reqs"),
|
||||
period: role === "admin" ? "" : $("#kc-period").value,
|
||||
hours: role === "admin" ? 0 : hours,
|
||||
}),
|
||||
});
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user