feat: use customer video key for demo access

This commit is contained in:
xuejianwu
2026-07-30 09:12:42 +08:00
parent fa1e8627e1
commit 1db9cc1f90
6 changed files with 144 additions and 40 deletions

View File

@@ -14,6 +14,7 @@ const LEGACY_GROUP_KEY = "yingxiangli.selectedAssetGroup";
const SCENE_ROLES_KEY = "yingxiangli.sceneRoles.v1";
const CHARACTER_LIBRARY_KEY = "yingxiangli.characterLibrary.v1";
const DRAMA_PROJECT_KEY = "yingxiangli.dramaProject.v1";
const VIDEO_KEY_STORAGE = "yingxiangli.videoKey.v1";
const AUDIT_CLIENT_KEY = "yingxiangli.auditClientId.v1";
const AUDIT_SESSION_KEY = "yingxiangli.auditSessionId.v1";
const VIEW_LABELS = ["正面", "侧面", "背面"];
@@ -53,6 +54,7 @@ const el = Object.fromEntries([
"referenceVideoInputs", "addExtraImageButton", "addReferenceVideoButton", "inputQuota",
"addShotButton", "shotList", "timelineSummary", "shotProgressBar", "continuityMode", "versionCount",
"versionList", "assemblyPanel", "assemblyList", "assemblyHint", "closeAssemblyButton",
"videoKeyGate", "videoKeyForm", "videoKeyInput", "videoKeyError", "changeVideoKeyButton", "cancelVideoKeyButton",
].map((id) => [id, document.getElementById(id)]));
const reviewRows = [el.reviewFront, el.reviewSide, el.reviewBack];
@@ -63,6 +65,7 @@ const auditInputTimers = new Map();
let auditFlushTimer = null;
setupClientAudit();
setupVideoKeyGate();
init();
document.querySelectorAll(".module-tab").forEach((button) => {
@@ -76,12 +79,13 @@ async function init() {
restoreCharacterLibrary();
restoreDramaProject();
renderConnection();
if (!state.config.mockMode && !currentVideoKey()) showVideoKeyGate(false);
await loadAssets();
loadActiveShotIntoForm();
renderScene();
renderStoryboard();
resumePendingReview();
resumePendingVideo();
if (state.config.mockMode || currentVideoKey()) resumePendingVideo();
} catch (error) {
showToast(error.message, true);
el.connectionState.textContent = "本地服务连接失败";
@@ -89,6 +93,42 @@ async function init() {
}
}
function setupVideoKeyGate() {
el.videoKeyForm.addEventListener("submit", (event) => {
event.preventDefault();
const key = el.videoKeyInput.value.trim();
if (!/^sk-[A-Za-z0-9_-]{8,}$/.test(key)) {
el.videoKeyError.textContent = "请输入正确格式的生视频 Keysk-...";
el.videoKeyError.classList.remove("hidden");
return;
}
localStorage.setItem(VIDEO_KEY_STORAGE, key);
logClientOperation("video_key.saved", { keySuffix: key.slice(-4), keyLength: key.length });
location.reload();
});
el.changeVideoKeyButton.addEventListener("click", () => showVideoKeyGate(true));
el.cancelVideoKeyButton.addEventListener("click", hideVideoKeyGate);
}
function currentVideoKey() {
try { return localStorage.getItem(VIDEO_KEY_STORAGE)?.trim() || ""; }
catch { return ""; }
}
function showVideoKeyGate(canCancel = Boolean(currentVideoKey())) {
el.videoKeyInput.value = "";
el.videoKeyError.textContent = "";
el.videoKeyError.classList.add("hidden");
el.cancelVideoKeyButton.classList.toggle("hidden", !canCancel);
el.videoKeyGate.classList.remove("hidden");
requestAnimationFrame(() => el.videoKeyInput.focus());
}
function hideVideoKeyGate() {
if (!currentVideoKey() && !state.config?.mockMode) return;
el.videoKeyGate.classList.add("hidden");
}
el.addRoleButton.addEventListener("click", () => {
if (state.roles.length >= 3) return showToast("当前 Demo 最多支持三个场景角色,人物图片总数不超过 9 张", true);
const role = createRole(`角色 ${ROLE_LETTERS[state.roles.length]}`);
@@ -1403,6 +1443,9 @@ async function resumePendingVideo() {
}
function renderConnection() {
const videoKey = currentVideoKey();
el.changeVideoKeyButton.classList.toggle("hidden", state.config.mockMode || !videoKey);
if (videoKey) el.changeVideoKeyButton.textContent = `更换视频 Key · ${videoKey.slice(-4)}`;
if (state.config.mockMode) {
el.connectionState.textContent = "Mock 模式 · 可评审完整流程";
el.connectionState.className = "connection mock";
@@ -1414,7 +1457,7 @@ function renderConnection() {
el.reviewButton.textContent = "提交真实素材审核";
el.mockHint.classList.add("hidden");
} else if (state.config.assetReady) {
el.connectionState.textContent = "素材审核 Live · 视频 Key 待配置";
el.connectionState.textContent = "请输入视频 Key";
el.connectionState.className = "connection live";
el.reviewButton.textContent = "提交真实素材审核";
el.mockHint.classList.add("hidden");
@@ -1663,11 +1706,15 @@ async function api(path, options = {}) {
const startedAt = Date.now();
const method = options.method || "GET";
const shouldLog = shouldLogClientApi(path);
const videoKey = currentVideoKey();
const videoHeaders = videoKey && (/^\/api\/videos(?:\/|$)/.test(path) || path === "/api/config")
? { "X-Video-Key": videoKey }
: {};
if (shouldLog) logClientOperation("api.request", { method, path });
try {
const response = await fetch(`${API_BASE}${path}`, {
...options,
headers: { "Content-Type": "application/json", "X-Session-Id": auditSessionId, ...(options.headers || {}) },
headers: { "Content-Type": "application/json", "X-Session-Id": auditSessionId, ...videoHeaders, ...(options.headers || {}) },
});
const result = await response.json().catch(() => ({ error: "服务返回了无法解析的内容" }));
if (shouldLog || !response.ok) {
@@ -1683,6 +1730,10 @@ async function api(path, options = {}) {
const error = new Error(extractApiError(result) || `请求失败(${response.status}`);
error.payload = result;
error.status = response.status;
if (response.status === 401 && /^\/api\/videos(?:\/|$)/.test(path)) {
localStorage.removeItem(VIDEO_KEY_STORAGE);
showVideoKeyGate(false);
}
throw error;
}
return result;