feat: harden runtime import and frontend verification workflows
Some checks failed
CI / Build & Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Release (push) Has been cancelled

This commit is contained in:
phamnazage-jpg
2026-06-04 20:02:36 +08:00
parent 7ce72cbc35
commit 77b7f7f660
32 changed files with 2657 additions and 109 deletions

View File

@@ -0,0 +1,313 @@
(function initSub2ApiAdminCommon(global) {
const ADMIN_LINKS = [
{ key: "home", href: "/portal/admin/", label: "管理首页" },
{ key: "logical-groups", href: "/portal/admin/logical-groups.html", label: "逻辑分组 / 路由" },
{ key: "route-health", href: "/portal/admin/route-health.html", label: "Route 健康视图" },
{ key: "accounts", href: "/portal/admin/accounts.html", label: "帐号资产" },
{ key: "providers", href: "/portal/admin/providers.html", label: "新增模型 / 供应商目录" },
{ key: "batch-import", href: "/portal/admin/batch-import.html", label: "导入供应商帐号" },
{ key: "portal", href: "/portal/", label: "用户 Portal", target: "_blank", rel: "noreferrer" },
];
function escapeHTML(value) {
return String(value ?? "")
.replaceAll("&", "&")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
function defaultApiBase() {
const origin = global.location && typeof global.location.origin === "string" ? global.location.origin : "";
if (!origin || origin === "null") {
return "/portal-admin-api";
}
return `${origin}/portal-admin-api`;
}
function normalizeApiBase(value) {
return (String(value || "").trim() || defaultApiBase()).replace(/\/+$/, "");
}
function authHeaders(tokenValue) {
const token = String(tokenValue || "").trim();
return token ? { Authorization: `Bearer ${token}` } : {};
}
function normalizeTone(tone) {
const value = String(tone || "").trim().toLowerCase();
if (!value || value === "note" || value === "info") {
return "note";
}
if (value === "warn") {
return "warning";
}
if (value === "error") {
return "danger";
}
return value;
}
function setStatus(element, message, tone = "note") {
if (!element) {
return;
}
element.textContent = message;
const normalizedTone = normalizeTone(tone);
if (normalizedTone !== "note") {
element.setAttribute("data-tone", normalizedTone);
} else {
element.removeAttribute("data-tone");
}
}
function describeSessionPayload(payload, options = {}) {
const fallbackUsername = String(options.usernameFallback || "admin").trim() || "admin";
const currentUsername = String(payload?.username || "").trim();
const effectiveUsername = currentUsername || fallbackUsername;
if (payload?.authenticated) {
const suffix = options.includeSessionSuffix ? "session" : "";
return {
tone: "success",
message: `管理员会话已建立:${currentUsername || "unknown"}${suffix}`,
};
}
if (payload?.login_enabled) {
if (options.allowBearerFallback) {
return {
tone: "warning",
message: `当前未登录。可用管理员用户名 ${effectiveUsername} 建立 session或继续使用 Bearer token。`,
};
}
return {
tone: "warning",
message: "当前未登录。可直接使用管理员用户名密码建立会话。",
};
}
return {
tone: "warning",
message: "当前实例未启用管理员登录,只能使用 Bearer token。",
};
}
function sessionStateSpec(kind, context = {}, options = {}) {
const username = String(context.username || options.usernameFallback || "unknown").trim() || "unknown";
const message = context.error instanceof Error ? context.error.message : String(context.error || "").trim();
switch (kind) {
case "missing_credentials":
return {
tone: "warning",
message: "请先输入管理员用户名和密码。",
};
case "login_success":
return {
tone: "success",
message: `管理员会话已建立:${username}`,
};
case "login_failed":
return {
tone: "danger",
message: `管理员登录失败:${message || "未知错误"}`,
};
case "logout_success":
return {
tone: "warning",
message: "管理员会话已退出。",
};
case "logout_failed":
return {
tone: "danger",
message: `退出会话失败:${message || "未知错误"}`,
};
case "check_failed":
return {
tone: "danger",
message: `检查管理员会话失败:${message || "未知错误"}`,
};
default:
return {
tone: "note",
message: String(context.message || ""),
};
}
}
function applySessionPayload(element, payload, options = {}) {
const status = describeSessionPayload(payload, options);
setStatus(element, status.message, status.tone);
return status;
}
function setSessionState(element, kind, context = {}, options = {}) {
const status = sessionStateSpec(kind, context, options);
setStatus(element, status.message, status.tone);
return status;
}
async function requestJSON(client, path, options = {}) {
const { skipAuth = false, headers = {}, ...rest } = options;
const finalHeaders = { Accept: "application/json", ...headers };
if (!skipAuth) {
Object.assign(finalHeaders, authHeaders(client.adminTokenInput && client.adminTokenInput.value));
}
const response = await fetch(`${normalizeApiBase(client.apiBaseInput && client.apiBaseInput.value)}${path}`, {
...rest,
credentials: "include",
headers: finalHeaders,
});
const text = await response.text();
let payload = {};
try {
payload = text ? JSON.parse(text) : {};
} catch {
payload = { raw: text };
}
if (!response.ok) {
const message = payload?.error?.message || payload?.message || payload?.error || payload?.raw || `HTTP ${response.status}`;
throw new Error(message);
}
return payload;
}
function readStoredConfig(storageKey) {
try {
return JSON.parse(global.localStorage.getItem(storageKey) || "{}");
} catch (error) {
console.warn(`failed to parse ${storageKey}`, error);
return {};
}
}
function writeStoredConfig(storageKey, payload) {
global.localStorage.setItem(storageKey, JSON.stringify(payload));
}
function renderAdminNav(container, currentKey) {
if (!container) {
return;
}
container.innerHTML = ADMIN_LINKS.map((link) => {
const currentClass = currentKey === link.key ? " is-current" : "";
const target = link.target ? ` target="${link.target}"` : "";
const rel = link.rel ? ` rel="${link.rel}"` : "";
return `<a href="${escapeHTML(link.href)}"${target}${rel} class="${currentClass.trim()}">${escapeHTML(link.label)}</a>`;
}).join("");
}
function createAdminPageRuntime(options) {
const client = {
apiBaseInput: options.apiBaseInput,
adminTokenInput: options.adminTokenInput,
};
const sessionPresentation = options.sessionPresentation || {};
async function refreshAdminSession() {
try {
const payload = await requestJSON(client, "/api/admin/session", {
skipAuth: !options.includeAuthOnSessionCheck,
});
if (payload.username && options.adminUsernameInput && !options.adminUsernameInput.value.trim()) {
options.adminUsernameInput.value = payload.username;
}
applySessionPayload(options.adminSessionStatus, payload, sessionPresentation);
return payload;
} catch (error) {
setSessionState(options.adminSessionStatus, "check_failed", { error }, sessionPresentation);
throw error;
}
}
async function loginAdminSession() {
const username = options.adminUsernameInput ? options.adminUsernameInput.value.trim() : "";
const password = options.adminPasswordInput ? options.adminPasswordInput.value : "";
if (!username || !password) {
const error = new Error("管理员用户名和密码不能为空");
setSessionState(options.adminSessionStatus, "missing_credentials", {}, sessionPresentation);
throw error;
}
try {
const payload = await requestJSON(client, "/api/admin/session/login", {
method: "POST",
skipAuth: true,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }),
});
if (options.adminPasswordInput) {
options.adminPasswordInput.value = "";
}
if (typeof options.onSessionPersist === "function") {
options.onSessionPersist();
}
setSessionState(options.adminSessionStatus, "login_success", { username: payload.username || username }, sessionPresentation);
return payload;
} catch (error) {
setSessionState(options.adminSessionStatus, "login_failed", { error }, sessionPresentation);
throw error;
}
}
async function logoutAdminSession() {
try {
const response = await fetch(`${normalizeApiBase(options.apiBaseInput && options.apiBaseInput.value)}/api/admin/session/logout`, {
method: "POST",
credentials: "include",
headers: authHeaders(options.adminTokenInput && options.adminTokenInput.value),
});
if (!response.ok) {
const text = await response.text();
throw new Error(text || `HTTP ${response.status}`);
}
if (options.adminPasswordInput) {
options.adminPasswordInput.value = "";
}
setSessionState(options.adminSessionStatus, "logout_success", {}, sessionPresentation);
} catch (error) {
setSessionState(options.adminSessionStatus, "logout_failed", { error }, sessionPresentation);
throw error;
}
}
return {
defaultApiBase,
normalizeApiBase() {
return normalizeApiBase(options.apiBaseInput && options.apiBaseInput.value);
},
authHeaders() {
return authHeaders(options.adminTokenInput && options.adminTokenInput.value);
},
requestJSON(path, requestOptions = {}) {
return requestJSON(client, path, requestOptions);
},
readStoredConfig,
writeStoredConfig,
renderAdminNav,
setStatus,
applySessionPayload(element, payload) {
return applySessionPayload(element, payload, sessionPresentation);
},
setSessionState(element, kind, context = {}) {
return setSessionState(element, kind, context, sessionPresentation);
},
refreshAdminSession,
loginAdminSession,
logoutAdminSession,
};
}
global.Sub2ApiAdminCommon = {
ADMIN_LINKS,
createAdminPageRuntime,
defaultApiBase,
normalizeApiBase,
authHeaders,
normalizeTone,
setStatus,
describeSessionPayload,
applySessionPayload,
setSessionState,
readStoredConfig,
writeStoredConfig,
renderAdminNav,
};
})(window);

View File

@@ -27,7 +27,7 @@ location /portal/ {
}
location /portal-proxy/ {
proxy_pass http://127.0.0.1:18169/;
proxy_pass http://127.0.0.1:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@@ -36,7 +36,7 @@ location /portal-proxy/ {
}
location /portal-admin-api/ {
proxy_pass http://127.0.0.1:18173/;
proxy_pass http://127.0.0.1:18190/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@@ -49,7 +49,7 @@ location /kimi-portal/ {
}
location /kimi-portal-proxy/ {
proxy_pass http://127.0.0.1:18169/;
proxy_pass http://127.0.0.1:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@@ -58,7 +58,7 @@ location /kimi-portal-proxy/ {
}
location /kimi/ {
proxy_pass http://127.0.0.1:18169/;
proxy_pass http://127.0.0.1:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@@ -67,7 +67,7 @@ location /kimi/ {
}
location /kimi-v1/ {
proxy_pass http://127.0.0.1:18169/v1/;
proxy_pass http://127.0.0.1:8080/v1/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;