diff --git a/deploy/tksea-portal/index.html b/deploy/tksea-portal/index.html index cd5d06c1..259908de 100644 --- a/deploy/tksea-portal/index.html +++ b/deploy/tksea-portal/index.html @@ -593,7 +593,7 @@
0
- 已开通兼容线路 + 已激活产品权限
0
@@ -614,11 +614,21 @@
+
+
+
+

权限与订阅视图

+

这里把宿主兼容线路、订阅与历史 Key 重新聚合回逻辑分组层,帮助你判断“我对哪个产品组已经可用、还缺什么”。

+
+
+
+
+

已有 Key

-

历史 Key 默认只展示必要信息;你可以直接在列表里一键复制,无需先重新创建。

+

历史 Key 会优先投影回逻辑分组产品层;你仍可以直接在列表里一键复制,无需先重新创建。

GET /api/v1/keys?page=1&page_size=20
@@ -721,6 +731,7 @@
+ @@ -1029,11 +1040,6 @@ : []; } - 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) { @@ -1067,6 +1073,116 @@ return rows; } + function activeSubscriptionItems() { + return (state.subscriptions || []).filter((item) => { + const status = String(item && item.status ? item.status : "").trim().toLowerCase(); + return status === "active" || status === "trialing"; + }); + } + + function dateValue(value) { + if (!value) { + return 0; + } + const ms = Date.parse(value); + return Number.isFinite(ms) ? ms : 0; + } + + function logicalGroupEntitlementRows() { + const legacyEnabled = availableLegacyGroupIDs(); + const subscriptions = state.subscriptions || []; + const activeSubscriptions = activeSubscriptionItems(); + const keys = state.keys || []; + return logicalGroupStatusRows().map((row) => { + const candidateIDs = new Set((row.candidates || []).map((candidate) => Number(candidate.id))); + const enabledCandidates = (row.enabledCandidates || []).map((candidate) => Number(candidate.id)); + const matchingSubscriptions = subscriptions.filter((item) => candidateIDs.has(Number(item.group_id))); + const matchingActiveSubscriptions = activeSubscriptions.filter((item) => candidateIDs.has(Number(item.group_id))); + const matchingKeys = keys.filter((item) => candidateIDs.has(Number(item.group_id))); + const latestExpiresAt = [...matchingSubscriptions, ...matchingKeys].reduce((latest, item) => { + return dateValue(item && item.expires_at) > dateValue(latest) ? String(item.expires_at || "") : latest; + }, ""); + + let stateKey = "catalog_only"; + let stateText = "仅目录"; + if ((row.candidates || []).length > 0 && enabledCandidates.length === 0) { + stateKey = "pending"; + stateText = "待开通"; + } + if (enabledCandidates.length === 1) { + stateKey = matchingActiveSubscriptions.length > 0 ? "active" : "granted"; + stateText = matchingActiveSubscriptions.length > 0 ? "已开通订阅" : "已授予权限"; + } + if (enabledCandidates.length > 1) { + stateKey = "ambiguous"; + stateText = "归属待整理"; + } + + return { + ...row, + stateKey, + stateText, + matchingSubscriptions, + matchingActiveSubscriptions, + matchingKeys, + latestExpiresAt, + enabledCandidateIDs: enabledCandidates, + legacyEnabledCount: Array.from(legacyEnabled).filter((groupID) => candidateIDs.has(groupID)).length + }; + }); + } + + function activeLogicalGroupNames() { + return logicalGroupEntitlementRows() + .filter((row) => row.stateKey === "active" || row.stateKey === "granted") + .map((row) => row.logicalGroup.display_name || row.logicalGroup.logical_group_id); + } + + function renderEntitlementView() { + const grid = $("entitlement-grid"); + const rows = logicalGroupEntitlementRows(); + if (!rows.length) { + grid.innerHTML = '
当前没有可投影到权限视图的逻辑分组。
'; + return; + } + + grid.innerHTML = rows.map((row) => { + const group = row.logicalGroup; + const stateBadgeClass = row.stateKey === "active" + ? "active" + : row.stateKey === "granted" + ? "neutral" + : "pending"; + const compatibilityNames = (row.candidates || []).map((candidate) => candidate.title).join(" / ") || "无兼容线路"; + const models = portalLogicalGroupModels(group).join(", ") || "--"; + return ( + '
' + + '

' + escapeHTML(group.display_name || group.logicalGroupID || "未命名逻辑分组") + '

' + + '
' + + '' + escapeHTML(row.stateText) + '' + + '兼容线路 ' + String(row.candidates.length) + '' + + '活跃订阅 ' + String(row.matchingActiveSubscriptions.length) + '' + + '已有 Key ' + String(row.matchingKeys.length) + '' + + '
' + + '
公开模型:' + escapeHTML(models) + '
' + + '
兼容宿主线路:' + escapeHTML(compatibilityNames) + '
' + + '
最近到期时间:' + escapeHTML(formatDate(row.latestExpiresAt)) + '
' + + '
权限解释:' + escapeHTML( + row.stateKey === "active" + ? "当前账号已具备订阅与兼容线路,可直接使用或继续申请测试 Key。" + : row.stateKey === "granted" + ? "当前账号已拿到兼容线路权限,但尚未检测到活跃订阅记录。" + : row.stateKey === "pending" + ? "当前逻辑分组还没有可直接使用的兼容线路,请联系管理员补开通。" + : row.stateKey === "ambiguous" + ? "当前逻辑分组命中多条兼容线路,权限归属仍待管理员整理。" + : "当前逻辑分组已公开,但还未绑定可直接使用的兼容线路。" + ) + '
' + + '
' + ); + }).join(""); + } + function getPresentationStatus(row) { if ((row.enabledCandidates || []).length === 1) { return { cls: "active", text: "可立即申请兼容 Key" }; @@ -1083,8 +1199,10 @@ function renderSessionSummary() { const sessionGrid = $("session-grid"); const user = state.user; + const entitlementRows = logicalGroupEntitlementRows(); + const activeLogicalGroups = activeLogicalGroupNames(); $("enabled-groups-stat").textContent = String((state.portalLogicalGroups || []).length); - $("subscriptions-stat").textContent = String(availableLegacyGroupIDs().size); + $("subscriptions-stat").textContent = String(entitlementRows.filter((row) => row.stateKey === "active" || row.stateKey === "granted").length); if (!user) { statusPill("warn", "未登录"); @@ -1107,6 +1225,7 @@ ["状态", user.status || "--"], ["并发", user.concurrency ?? "--"], ["RPM 限制", user.rpm_limit ?? "--"], + ["逻辑分组权限", activeLogicalGroups.length ? activeLogicalGroups.join(" / ") : "当前未检测到已激活的逻辑分组权限"], ["兼容宿主分组", Array.isArray(user.allowed_groups) && user.allowed_groups.length ? user.allowed_groups.join(", ") : "由订阅/分组接口决定"], ["创建时间", formatDate(user.created_at)] ]; @@ -1249,6 +1368,7 @@ function renderAll() { renderSessionSummary(); renderGroupCatalog(); + renderEntitlementView(); renderKeys(); } diff --git a/scripts/test/test_tksea_portal_assets.sh b/scripts/test/test_tksea_portal_assets.sh index 5237da94..6ee011dd 100755 --- a/scripts/test/test_tksea_portal_assets.sh +++ b/scripts/test/test_tksea_portal_assets.sh @@ -50,7 +50,8 @@ assert_contains_file "$HTML_FILE" "copy-existing-key-btn" assert_contains_file "$HTML_FILE" "已有 Key" assert_contains_file "$HTML_FILE" "showToast" assert_contains_file "$HTML_FILE" "逻辑分组目录" -assert_contains_file "$HTML_FILE" "已开通兼容线路" +assert_contains_file "$HTML_FILE" "已激活产品权限" +assert_contains_file "$HTML_FILE" "权限与订阅视图" assert_contains_file "$HTML_FILE" "可立即申请兼容 Key" assert_contains_file "$HTML_FILE" "需开通兼容线路" assert_contains_file "$HTML_FILE" "目录已上线" @@ -59,6 +60,11 @@ assert_contains_file "$HTML_FILE" "当前逻辑分组说明" assert_contains_file "$HTML_FILE" "兼容宿主线路" assert_contains_file "$HTML_FILE" "portalLogicalGroups" assert_contains_file "$HTML_FILE" "LEGACY_GROUP_CATALOG" +assert_contains_file "$HTML_FILE" "逻辑分组权限" +assert_contains_file "$HTML_FILE" "renderEntitlementView" +assert_contains_file "$HTML_FILE" "已开通订阅" +assert_contains_file "$HTML_FILE" "已授予权限" +assert_contains_file "$HTML_FILE" "归属待整理" assert_contains_file "$HTML_FILE" "route_policy =" assert_contains_file "$HTML_FILE" "gpt-5.4" assert_contains_file "$HTML_FILE" "MiniMax-M2.7-highspeed"