feat(portal): switch user catalog to logical groups
This commit is contained in:
@@ -385,6 +385,9 @@
|
||||
.group-card.pending {
|
||||
background: linear-gradient(180deg, rgba(255, 240, 219, 0.72), rgba(255, 255, 255, 0.92));
|
||||
}
|
||||
.group-card.neutral {
|
||||
background: linear-gradient(180deg, rgba(240, 244, 248, 0.82), rgba(255, 255, 255, 0.94));
|
||||
}
|
||||
.group-card h4 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 17px;
|
||||
@@ -586,11 +589,11 @@
|
||||
<div id="balance-stat" class="stat-value">--</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-label">已开通分组</span>
|
||||
<span class="stat-label">逻辑分组目录</span>
|
||||
<div id="enabled-groups-stat" class="stat-value">0</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-label">活跃订阅</span>
|
||||
<span class="stat-label">已开通兼容线路</span>
|
||||
<div id="subscriptions-stat" class="stat-value">0</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
@@ -604,8 +607,8 @@
|
||||
<article class="panel">
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h2>分组与模型线路</h2>
|
||||
<p>这里会把你的可用分组、活跃订阅和模型目录合并展示,让“能不能用、该用哪条线”一眼可见。</p>
|
||||
<h2>逻辑分组与模型目录</h2>
|
||||
<p>这里优先展示插件层的逻辑分组、公开模型、sticky 策略和 route 状态,不再把宿主真实分组当成用户主视角。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="group-grid" class="group-grid"></div>
|
||||
@@ -663,8 +666,8 @@
|
||||
<article class="panel">
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h2>创建测试 Key</h2>
|
||||
<p>页面会高亮你当前可用的线路,创建成功后立即显示对应分组和模型建议。</p>
|
||||
<h2>申请测试 Key</h2>
|
||||
<p>页面先按逻辑分组展示产品目录;当前 Key 申请仍通过兼容宿主线路完成。只有找到唯一兼容线路时,才会允许直接申请。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
@@ -673,7 +676,7 @@
|
||||
<input id="key-name" value="my-model-key" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="group-id">选择线路</label>
|
||||
<label for="group-id">选择逻辑分组</label>
|
||||
<select id="group-id"></select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -706,7 +709,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="result-box">
|
||||
<strong>当前线路说明</strong>
|
||||
<strong>当前逻辑分组说明</strong>
|
||||
<div id="selection-summary" class="tiny">尚未选择线路。</div>
|
||||
</div>
|
||||
<div class="result-box">
|
||||
@@ -726,11 +729,12 @@
|
||||
|
||||
<script>
|
||||
const PORTAL_PROXY_PREFIX = "/portal-proxy/api/v1";
|
||||
const PORTAL_CATALOG_PREFIX = "/portal-admin-api/api/portal";
|
||||
const STORAGE = {
|
||||
token: "sub2api.portal.accessToken",
|
||||
email: "sub2api.portal.email"
|
||||
};
|
||||
const GROUP_CATALOG = {
|
||||
const LEGACY_GROUP_CATALOG = {
|
||||
2: {
|
||||
id: 2,
|
||||
key: "kimi",
|
||||
@@ -772,11 +776,12 @@
|
||||
const state = {
|
||||
accessToken: "",
|
||||
user: null,
|
||||
portalLogicalGroups: [],
|
||||
groups: [],
|
||||
subscriptions: [],
|
||||
keys: [],
|
||||
lastCreatedKey: "",
|
||||
selectionGroupID: 2
|
||||
selectionLogicalGroupID: ""
|
||||
};
|
||||
let toastTimer = null;
|
||||
|
||||
@@ -879,6 +884,15 @@
|
||||
return value.slice(0, 6) + "..." + value.slice(-6);
|
||||
}
|
||||
|
||||
function escapeHTML(value) {
|
||||
return String(value ?? "")
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
function saveSession() {
|
||||
if (state.accessToken) {
|
||||
localStorage.setItem(STORAGE.token, state.accessToken);
|
||||
@@ -905,6 +919,7 @@
|
||||
state.groups = [];
|
||||
state.subscriptions = [];
|
||||
state.keys = [];
|
||||
state.lastCreatedKey = "";
|
||||
localStorage.removeItem(STORAGE.token);
|
||||
$("access-token").value = "";
|
||||
$("api-key").value = "";
|
||||
@@ -941,6 +956,28 @@
|
||||
return payload.data ?? payload;
|
||||
}
|
||||
|
||||
async function requestPortal(path) {
|
||||
const res = await fetch(PORTAL_CATALOG_PREFIX + path, {
|
||||
method: "GET",
|
||||
headers: { Accept: "application/json" }
|
||||
});
|
||||
const text = await res.text();
|
||||
let payload;
|
||||
try {
|
||||
payload = JSON.parse(text);
|
||||
} catch {
|
||||
payload = { raw: text };
|
||||
}
|
||||
if (!res.ok) {
|
||||
const message = payload.message || payload.raw || ("HTTP " + res.status);
|
||||
const error = new Error(message);
|
||||
error.statusCode = res.status;
|
||||
error.payload = payload;
|
||||
throw error;
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function requestJSON(path, method, payload, useAuth = true) {
|
||||
return request(path, {
|
||||
method,
|
||||
@@ -950,8 +987,8 @@
|
||||
});
|
||||
}
|
||||
|
||||
function knownGroup(id) {
|
||||
return GROUP_CATALOG[id] || {
|
||||
function knownLegacyGroup(id) {
|
||||
return LEGACY_GROUP_CATALOG[id] || {
|
||||
id,
|
||||
key: "group-" + id,
|
||||
title: "分组 " + id,
|
||||
@@ -961,7 +998,7 @@
|
||||
};
|
||||
}
|
||||
|
||||
function availableGroupIDs() {
|
||||
function availableLegacyGroupIDs() {
|
||||
const ids = new Set();
|
||||
for (const group of state.groups || []) {
|
||||
if (group && Number.isFinite(Number(group.id))) {
|
||||
@@ -984,64 +1021,83 @@
|
||||
return ids;
|
||||
}
|
||||
|
||||
function groupStatusRows() {
|
||||
const enabled = availableGroupIDs();
|
||||
const rows = [];
|
||||
const rawGroups = new Map((state.groups || []).map((group) => [Number(group.id), group]));
|
||||
const subscriptionByGroup = new Map();
|
||||
function portalLogicalGroupModels(group) {
|
||||
return Array.isArray(group && group.public_models)
|
||||
? group.public_models
|
||||
.map((item) => String(item && item.public_model ? item.public_model : "").trim())
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
}
|
||||
|
||||
for (const item of state.subscriptions || []) {
|
||||
if (Number.isFinite(Number(item.group_id))) {
|
||||
subscriptionByGroup.set(Number(item.group_id), item);
|
||||
function getPortalLogicalGroup(logicalGroupID) {
|
||||
const target = String(logicalGroupID || "").trim();
|
||||
return (state.portalLogicalGroups || []).find((group) => String(group.logical_group_id || "").trim() === target) || null;
|
||||
}
|
||||
|
||||
function legacyCompatibilityCandidates(group) {
|
||||
const portalModels = new Set(portalLogicalGroupModels(group));
|
||||
if (!portalModels.size) {
|
||||
return [];
|
||||
}
|
||||
return Object.values(LEGACY_GROUP_CATALOG).filter((candidate) => candidate.models.some((model) => portalModels.has(String(model).trim())));
|
||||
}
|
||||
|
||||
function logicalGroupStatusRows() {
|
||||
const enabledLegacyGroups = availableLegacyGroupIDs();
|
||||
const rows = (state.portalLogicalGroups || []).map((group) => {
|
||||
const candidates = legacyCompatibilityCandidates(group);
|
||||
const enabledCandidates = candidates.filter((candidate) => enabledLegacyGroups.has(Number(candidate.id)));
|
||||
return {
|
||||
logicalGroup: group,
|
||||
candidates,
|
||||
enabledCandidates
|
||||
};
|
||||
});
|
||||
rows.sort((left, right) => {
|
||||
const leftActive = Number(left.logicalGroup.active_route_count || 0);
|
||||
const rightActive = Number(right.logicalGroup.active_route_count || 0);
|
||||
if (leftActive !== rightActive) {
|
||||
return rightActive - leftActive;
|
||||
}
|
||||
}
|
||||
|
||||
for (const [id, meta] of Object.entries(GROUP_CATALOG)) {
|
||||
const numericID = Number(id);
|
||||
const group = rawGroups.get(numericID) || null;
|
||||
const subscription = subscriptionByGroup.get(numericID) || null;
|
||||
rows.push({
|
||||
id: numericID,
|
||||
catalog: meta,
|
||||
group,
|
||||
subscription,
|
||||
enabled: enabled.has(numericID)
|
||||
});
|
||||
}
|
||||
|
||||
return String(left.logicalGroup.display_name || left.logicalGroup.logical_group_id || "").localeCompare(
|
||||
String(right.logicalGroup.display_name || right.logicalGroup.logical_group_id || ""),
|
||||
"zh-CN"
|
||||
);
|
||||
});
|
||||
return rows;
|
||||
}
|
||||
|
||||
function getPresentationStatus(row) {
|
||||
if (row.catalog.recommendation === "not_recommended") {
|
||||
return { cls: "neutral", text: "暂不推荐" };
|
||||
if ((row.enabledCandidates || []).length === 1) {
|
||||
return { cls: "active", text: "可立即申请兼容 Key" };
|
||||
}
|
||||
if (row.enabled) {
|
||||
return { cls: "active", text: "可立即使用" };
|
||||
if ((row.enabledCandidates || []).length > 1) {
|
||||
return { cls: "pending", text: "待人工确认" };
|
||||
}
|
||||
return { cls: "pending", text: "需开通" };
|
||||
if ((row.candidates || []).length > 0) {
|
||||
return { cls: "pending", text: "需开通兼容线路" };
|
||||
}
|
||||
return { cls: "neutral", text: "目录已上线" };
|
||||
}
|
||||
|
||||
function renderSessionSummary() {
|
||||
const sessionGrid = $("session-grid");
|
||||
const user = state.user;
|
||||
$("enabled-groups-stat").textContent = String((state.portalLogicalGroups || []).length);
|
||||
$("subscriptions-stat").textContent = String(availableLegacyGroupIDs().size);
|
||||
|
||||
if (!user) {
|
||||
statusPill("warn", "未登录");
|
||||
$("balance-stat").textContent = "--";
|
||||
$("enabled-groups-stat").textContent = "0";
|
||||
$("subscriptions-stat").textContent = "0";
|
||||
$("keys-stat").textContent = "0";
|
||||
sessionGrid.innerHTML = [
|
||||
'<div class="empty">当前还没有有效登录态。登录后会显示账号概览、已开通线路和历史 Key。</div>'
|
||||
'<div class="empty">当前还没有有效登录态。登录后会显示账号概览、兼容线路开通状态和历史 Key;逻辑分组目录在未登录时也可浏览。</div>'
|
||||
].join("");
|
||||
return;
|
||||
}
|
||||
|
||||
statusPill("ok", "已登录");
|
||||
$("balance-stat").textContent = formatMoney(user.balance);
|
||||
$("enabled-groups-stat").textContent = String(availableGroupIDs().size);
|
||||
$("subscriptions-stat").textContent = String((state.subscriptions || []).filter((item) => String(item.status || "") === "active").length);
|
||||
$("keys-stat").textContent = String((state.keys || []).length);
|
||||
|
||||
const fields = [
|
||||
@@ -1051,7 +1107,7 @@
|
||||
["状态", user.status || "--"],
|
||||
["并发", user.concurrency ?? "--"],
|
||||
["RPM 限制", user.rpm_limit ?? "--"],
|
||||
["允许分组", Array.isArray(user.allowed_groups) && user.allowed_groups.length ? user.allowed_groups.join(", ") : "由订阅/分组接口决定"],
|
||||
["兼容宿主分组", Array.isArray(user.allowed_groups) && user.allowed_groups.length ? user.allowed_groups.join(", ") : "由订阅/分组接口决定"],
|
||||
["创建时间", formatDate(user.created_at)]
|
||||
];
|
||||
|
||||
@@ -1064,61 +1120,91 @@
|
||||
}
|
||||
|
||||
function renderGroupCatalog() {
|
||||
const rows = groupStatusRows();
|
||||
const rows = logicalGroupStatusRows();
|
||||
const grid = $("group-grid");
|
||||
|
||||
if (!rows.length) {
|
||||
grid.innerHTML = '<div class="empty">当前还没有对外发布的逻辑分组目录。管理员发布后,这里会自动显示公开模型与兼容线路状态。</div>';
|
||||
$("group-id").innerHTML = '<option value="">暂无可用逻辑分组</option>';
|
||||
$("group-id").value = "";
|
||||
state.selectionLogicalGroupID = "";
|
||||
renderSelectionSummary();
|
||||
return;
|
||||
}
|
||||
|
||||
grid.innerHTML = rows.map((row) => {
|
||||
const groupName = row.group && row.group.name ? row.group.name : ("group " + row.id);
|
||||
const subscription = row.subscription;
|
||||
const group = row.logicalGroup;
|
||||
const presentation = getPresentationStatus(row);
|
||||
const subscriptionText = subscription
|
||||
? "订阅状态:" + (subscription.status || "--") + (subscription.expires_at ? " / 到期:" + formatDate(subscription.expires_at) : "")
|
||||
: "尚未检测到当前账号的活跃订阅记录。";
|
||||
const models = row.catalog.models.length
|
||||
? "<ul class=\"group-models\">" + row.catalog.models.map((model) => "<li><span class=\"mono\">" + model + "</span></li>").join("") + "</ul>"
|
||||
: "<div class=\"group-note\">当前未登记模型别名。</div>";
|
||||
const models = portalLogicalGroupModels(group);
|
||||
const modelsHTML = models.length
|
||||
? "<ul class=\"group-models\">" + models.map((model) => "<li><span class=\"mono\">" + escapeHTML(model) + "</span></li>").join("") + "</ul>"
|
||||
: "<div class=\"group-note\">当前尚未登记公开模型。</div>";
|
||||
const compatibilityText = row.enabledCandidates.length === 1
|
||||
? "兼容 Key 申请已就绪。当前账号可以直接申请这一组模型的测试 Key。"
|
||||
: row.enabledCandidates.length > 1
|
||||
? "检测到多条兼容宿主线路,当前不自动选择,请联系管理员整理归属。"
|
||||
: row.candidates.length > 0
|
||||
? "逻辑分组目录已上线,但你的账号还没有对应兼容线路。"
|
||||
: "当前仅开放目录浏览,尚未建立可自动申请的兼容线路。";
|
||||
|
||||
return (
|
||||
'<article class="group-card ' + (row.enabled ? "active" : "pending") + '">' +
|
||||
'<h4>' + row.catalog.title + '</h4>' +
|
||||
'<article class="group-card ' + (presentation.cls === "active" ? "active" : (presentation.cls === "neutral" ? "neutral" : "pending")) + '">' +
|
||||
'<h4>' + escapeHTML(group.display_name || group.logical_group_id || "未命名逻辑分组") + '</h4>' +
|
||||
'<div class="group-meta">' +
|
||||
'<span class="badge">' + groupName + '</span>' +
|
||||
'<span class="badge">' + row.catalog.subtitle + '</span>' +
|
||||
'<span class="badge strong ' + presentation.cls + '">' + presentation.text + '</span>' +
|
||||
'<span class="badge">logical group</span>' +
|
||||
'<span class="badge mono">' + escapeHTML(group.logical_group_id || "--") + '</span>' +
|
||||
'<span class="badge">' + escapeHTML(group.route_policy || "priority") + '</span>' +
|
||||
'<span class="badge">' + escapeHTML(group.sticky_mode || "conversation_preferred") + '</span>' +
|
||||
'<span class="badge strong ' + presentation.cls + '">' + escapeHTML(presentation.text) + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="group-note">' + row.catalog.description + '</div>' +
|
||||
models +
|
||||
'<div class="group-note">分组 ID:<span class="mono">' + row.id + '</span></div>' +
|
||||
'<div class="group-note">' + subscriptionText + '</div>' +
|
||||
'<div class="group-note">' + escapeHTML(group.description || "当前逻辑分组已对外发布,可按公开模型维度统一查看。") + '</div>' +
|
||||
modelsHTML +
|
||||
'<div class="group-note">公开模型:<span class="mono">' + String(models.length) + '</span> / route:<span class="mono">' + String(group.route_count || 0) + '</span> / active route:<span class="mono">' + String(group.active_route_count || 0) + '</span></div>' +
|
||||
'<div class="group-note">' + escapeHTML(compatibilityText) + '</div>' +
|
||||
'</article>'
|
||||
);
|
||||
}).join("");
|
||||
|
||||
const select = $("group-id");
|
||||
const previous = Number(select.value || state.selectionGroupID || 2);
|
||||
const previous = String(select.value || state.selectionLogicalGroupID || rows[0].logicalGroup.logical_group_id || "");
|
||||
const options = rows.map((row) => {
|
||||
const label = row.catalog.title + " / group " + row.id + " / " + (row.catalog.models.join(", ") || "未登记模型");
|
||||
return '<option value="' + row.id + '"' + (row.enabled ? "" : "") + '>' + label + (row.enabled ? "" : "(待开通)") + '</option>';
|
||||
const group = row.logicalGroup;
|
||||
const label = (group.display_name || group.logical_group_id) + " / " + (portalLogicalGroupModels(group).join(", ") || "未登记模型");
|
||||
return '<option value="' + escapeHTML(group.logical_group_id || "") + '">' + escapeHTML(label) + '</option>';
|
||||
}).join("");
|
||||
select.innerHTML = options;
|
||||
|
||||
if (rows.some((row) => row.id === previous)) {
|
||||
select.value = String(previous);
|
||||
if (rows.some((row) => String(row.logicalGroup.logical_group_id || "") === previous)) {
|
||||
select.value = previous;
|
||||
}
|
||||
state.selectionGroupID = Number(select.value || 2);
|
||||
state.selectionLogicalGroupID = String(select.value || rows[0].logicalGroup.logical_group_id || "");
|
||||
renderSelectionSummary();
|
||||
}
|
||||
|
||||
function renderSelectionSummary() {
|
||||
const groupID = Number($("group-id").value || state.selectionGroupID || 2);
|
||||
state.selectionGroupID = groupID;
|
||||
const row = groupStatusRows().find((item) => item.id === groupID);
|
||||
const meta = knownGroup(groupID);
|
||||
const availability = row && row.enabled ? "当前账号已开通,可直接创建这一条线路的 key。" : "当前账号尚未开通这条线路,创建时可能会返回无权限。";
|
||||
const logicalGroupID = String($("group-id").value || state.selectionLogicalGroupID || "").trim();
|
||||
state.selectionLogicalGroupID = logicalGroupID;
|
||||
const row = logicalGroupStatusRows().find((item) => String(item.logicalGroup.logical_group_id || "") === logicalGroupID);
|
||||
if (!row) {
|
||||
$("selection-summary").innerHTML = "当前还没有可选逻辑分组。";
|
||||
$("create-key-btn").disabled = true;
|
||||
return;
|
||||
}
|
||||
const models = portalLogicalGroupModels(row.logicalGroup);
|
||||
const canCreate = !!state.accessToken && row.enabledCandidates.length === 1;
|
||||
const compatibility = row.enabledCandidates.length === 1
|
||||
? "当前账号已命中唯一兼容宿主线路,可直接申请测试 Key。"
|
||||
: row.enabledCandidates.length > 1
|
||||
? "检测到多条兼容宿主线路,当前不自动选择,请联系管理员整理归属后再申请。"
|
||||
: row.candidates.length > 0
|
||||
? "你的账号暂未开通兼容宿主线路,当前只能浏览目录,不能直接申请测试 Key。"
|
||||
: "当前逻辑分组尚未建立自动申请测试 Key 的兼容线路。";
|
||||
$("create-key-btn").disabled = !canCreate;
|
||||
$("selection-summary").innerHTML = [
|
||||
'<div><strong>' + meta.title + '</strong> / group <span class="mono">' + groupID + '</span></div>',
|
||||
'<div class="mono">推荐模型: ' + (meta.models.join(", ") || "--") + '</div>',
|
||||
'<div>' + availability + '</div>'
|
||||
'<div><strong>' + escapeHTML(row.logicalGroup.display_name || row.logicalGroup.logical_group_id || "未命名逻辑分组") + '</strong> / <span class="mono">' + escapeHTML(logicalGroupID) + '</span></div>',
|
||||
'<div class="mono">公开模型: ' + escapeHTML(models.join(", ") || "--") + '</div>',
|
||||
'<div class="mono">route_policy = ' + escapeHTML(row.logicalGroup.route_policy || "priority") + ' / sticky_mode = ' + escapeHTML(row.logicalGroup.sticky_mode || "conversation_preferred") + '</div>',
|
||||
'<div>' + escapeHTML(compatibility) + '</div>'
|
||||
].join("");
|
||||
}
|
||||
|
||||
@@ -1132,7 +1218,13 @@
|
||||
|
||||
list.innerHTML = items.map((item) => {
|
||||
const groupID = Number(item.group_id);
|
||||
const meta = Number.isFinite(groupID) ? knownGroup(groupID) : null;
|
||||
const meta = Number.isFinite(groupID) ? knownLegacyGroup(groupID) : null;
|
||||
const logicalCandidates = meta
|
||||
? (state.portalLogicalGroups || []).filter((group) => meta.models.some((model) => portalLogicalGroupModels(group).includes(String(model).trim())))
|
||||
: [];
|
||||
const logicalCandidateText = logicalCandidates.length
|
||||
? logicalCandidates.map((group) => group.display_name || group.logical_group_id).join(" / ")
|
||||
: "未建立逻辑分组映射";
|
||||
return (
|
||||
'<article class="key-item">' +
|
||||
'<div class="key-top">' +
|
||||
@@ -1144,7 +1236,8 @@
|
||||
'</div>' +
|
||||
'<div class="key-meta">' +
|
||||
'<div><span class="kv-label">Key</span><div class="mono">' + maskKey(item.key || "") + '</div></div>' +
|
||||
'<div><span class="kv-label">分组</span><div>' + (meta ? meta.title + " / group " + groupID : (groupID ? "group " + groupID : "未绑定")) + '</div></div>' +
|
||||
'<div><span class="kv-label">兼容线路</span><div>' + escapeHTML(meta ? meta.title : (groupID ? "group " + groupID : "未绑定")) + '</div></div>' +
|
||||
'<div><span class="kv-label">逻辑分组</span><div>' + escapeHTML(logicalCandidateText) + '</div></div>' +
|
||||
'<div><span class="kv-label">创建时间</span><div>' + formatDate(item.created_at) + '</div></div>' +
|
||||
'<div><span class="kv-label">到期时间</span><div>' + formatDate(item.expires_at) + '</div></div>' +
|
||||
'</div>' +
|
||||
@@ -1160,6 +1253,14 @@
|
||||
}
|
||||
|
||||
async function refreshUserState() {
|
||||
try {
|
||||
const payload = await requestPortal("/logical-groups");
|
||||
state.portalLogicalGroups = Array.isArray(payload.logical_groups) ? payload.logical_groups : [];
|
||||
} catch (err) {
|
||||
state.portalLogicalGroups = [];
|
||||
setStatus("login-status", "bad", "逻辑分组目录拉取失败: " + err.message);
|
||||
}
|
||||
|
||||
if (!state.accessToken) {
|
||||
state.user = null;
|
||||
state.groups = [];
|
||||
@@ -1246,17 +1347,29 @@
|
||||
|
||||
async function handleCreateKey() {
|
||||
const name = $("key-name").value.trim();
|
||||
const groupID = Number($("group-id").value || state.selectionGroupID || 2);
|
||||
const logicalGroupID = String($("group-id").value || state.selectionLogicalGroupID || "").trim();
|
||||
const row = logicalGroupStatusRows().find((item) => String(item.logicalGroup.logical_group_id || "") === logicalGroupID);
|
||||
if (!row) {
|
||||
setStatus("key-status", "bad", "当前还没有可用于申请测试 Key 的逻辑分组。");
|
||||
return;
|
||||
}
|
||||
if (row.enabledCandidates.length !== 1) {
|
||||
const tip = row.enabledCandidates.length > 1
|
||||
? "当前逻辑分组命中多条兼容宿主线路,暂不自动选择,请联系管理员整理归属。"
|
||||
: "当前逻辑分组尚未命中唯一兼容宿主线路,暂不能直接申请测试 Key。";
|
||||
setStatus("key-status", "bad", tip);
|
||||
return;
|
||||
}
|
||||
const legacyGroup = row.enabledCandidates[0];
|
||||
setBusy("create-key-btn", true);
|
||||
try {
|
||||
const data = await requestJSON("/keys", "POST", {
|
||||
name,
|
||||
group_id: Number.isFinite(groupID) ? groupID : null
|
||||
group_id: Number(legacyGroup.id)
|
||||
}, true);
|
||||
state.lastCreatedKey = data.key || "";
|
||||
$("api-key").value = state.lastCreatedKey;
|
||||
const meta = knownGroup(groupID);
|
||||
setStatus("key-status", "ok", "Key 创建成功。已绑定到 " + meta.title + " / group " + groupID + "。");
|
||||
setStatus("key-status", "ok", "Key 创建成功。已按逻辑分组“" + (row.logicalGroup.display_name || row.logicalGroup.logical_group_id) + "”走兼容线路发放测试 Key。");
|
||||
renderSelectionSummary();
|
||||
await refreshUserState();
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user