feat: use customer video key for demo access
This commit is contained in:
63
server.mjs
63
server.mjs
@@ -21,14 +21,15 @@ loadLocalEnv();
|
||||
|
||||
const port = Number(process.env.PORT || 4173);
|
||||
const host = process.env.HOST || "127.0.0.1";
|
||||
const apiToken = process.env.KK_API_KEY || process.env.KK_VIDEO_TOKEN || "";
|
||||
const mockMode = process.env.DEMO_MOCK === "1" || !apiToken;
|
||||
const assetToken = process.env.KK_ASSET_TOKEN || process.env.KK_API_KEY || "";
|
||||
const mockMode = process.env.DEMO_MOCK === "1";
|
||||
const auditLogDir = process.env.AUDIT_LOG_DIR || join(root, "logs");
|
||||
const auditHashSalt = process.env.AUDIT_HASH_SALT || "yingxiangli-demo-audit";
|
||||
|
||||
const mockAssets = [];
|
||||
const mockVideos = new Map();
|
||||
const assetReviewTasks = new Map();
|
||||
const videoTaskTokens = new Map();
|
||||
|
||||
const server = createServer((request, response) => {
|
||||
const startedAt = Date.now();
|
||||
@@ -115,12 +116,13 @@ async function handleApi(request, response, url) {
|
||||
}
|
||||
|
||||
if (request.method === "GET" && url.pathname === "/api/config") {
|
||||
const videoToken = videoTokenFromRequest(request);
|
||||
sendJson(response, 200, {
|
||||
mockMode,
|
||||
assetReady: mockMode || Boolean(apiToken),
|
||||
videoReady: mockMode || Boolean(apiToken),
|
||||
assetReady: mockMode || Boolean(assetToken),
|
||||
videoReady: mockMode || Boolean(videoToken),
|
||||
apiVersion: "v3",
|
||||
missing: !mockMode && !apiToken ? ["KK_API_KEY"] : [],
|
||||
missing: !mockMode && !assetToken ? ["KK_ASSET_TOKEN"] : [],
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -174,13 +176,13 @@ async function handleApi(request, response, url) {
|
||||
return;
|
||||
}
|
||||
|
||||
requireToken(apiToken, "KK_API_KEY");
|
||||
requireToken(assetToken, "KK_ASSET_TOKEN");
|
||||
const labels = { front: "正面", side: "侧面", back: "背面" };
|
||||
const uploaded = await Promise.all(imageInputs.map(async (input, index) => {
|
||||
const viewLabel = labels[input.view] || `参考图${index + 1}`;
|
||||
const upstream = await callJson("https://ai-api.kkidc.com/v1/assets/upload", {
|
||||
method: "POST",
|
||||
headers: bearerHeaders(),
|
||||
headers: bearerHeaders(assetToken),
|
||||
body: JSON.stringify({
|
||||
url: input.url,
|
||||
type: "image",
|
||||
@@ -208,7 +210,7 @@ async function handleApi(request, response, url) {
|
||||
}
|
||||
|
||||
if (request.method === "GET" && url.pathname.startsWith("/api/assets/tasks/")) {
|
||||
requireToken(apiToken, "KK_API_KEY");
|
||||
requireToken(assetToken, "KK_ASSET_TOKEN");
|
||||
const taskId = decodeURIComponent(url.pathname.split("/").pop());
|
||||
const task = assetReviewTasks.get(taskId);
|
||||
if (!task) throw new ClientError("素材审核任务不存在或本地服务已重启,请在素材库刷新状态", 404);
|
||||
@@ -227,12 +229,12 @@ async function handleApi(request, response, url) {
|
||||
});
|
||||
return;
|
||||
}
|
||||
requireToken(apiToken, "KK_API_KEY");
|
||||
requireToken(assetToken, "KK_ASSET_TOKEN");
|
||||
const upstreamUrl = new URL("https://ai-api.kkidc.com/v1/assets");
|
||||
for (const key of ["page", "page_size", "purpose", "sort_order", "group_id"]) {
|
||||
for (const value of url.searchParams.getAll(key)) upstreamUrl.searchParams.append(key, value);
|
||||
}
|
||||
const upstream = await callJson(upstreamUrl, { headers: bearerHeaders(false) });
|
||||
const upstream = await callJson(upstreamUrl, { headers: bearerHeaders(assetToken, false) });
|
||||
sendJson(response, upstream.status, upstream.body, upstream.trackId);
|
||||
return;
|
||||
}
|
||||
@@ -257,19 +259,21 @@ async function handleApi(request, response, url) {
|
||||
return;
|
||||
}
|
||||
|
||||
requireToken(apiToken, "KK_API_KEY");
|
||||
const videoToken = requireVideoToken(request);
|
||||
const upstream = await callJson("https://ai-api.kkidc.com/sd/api/v3/contents/generations/tasks", {
|
||||
method: "POST",
|
||||
headers: bearerHeaders(),
|
||||
headers: bearerHeaders(videoToken),
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const taskId = extractVideoTaskId(upstream.body);
|
||||
if (taskId) videoTaskTokens.set(taskId, videoToken);
|
||||
sendJson(response, upstream.status, upstream.body, upstream.trackId);
|
||||
return;
|
||||
}
|
||||
|
||||
const videoFileMatch = request.method === "GET" && url.pathname.match(/^\/api\/videos\/([^/]+)\/video\.mp4$/);
|
||||
if (videoFileMatch) {
|
||||
await streamVideoTask(response, decodeURIComponent(videoFileMatch[1]));
|
||||
await streamVideoTask(request, response, decodeURIComponent(videoFileMatch[1]));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -292,9 +296,10 @@ async function handleApi(request, response, url) {
|
||||
return;
|
||||
}
|
||||
|
||||
requireToken(apiToken, "KK_API_KEY");
|
||||
const videoToken = requireVideoToken(request, taskId);
|
||||
videoTaskTokens.set(taskId, videoToken);
|
||||
const upstream = await callJson(`https://ai-api.kkidc.com/sd/api/v3/contents/generations/tasks/${encodeURIComponent(taskId)}`, {
|
||||
headers: bearerHeaders(false),
|
||||
headers: bearerHeaders(videoToken, false),
|
||||
});
|
||||
const normalized = {
|
||||
...upstream.body,
|
||||
@@ -311,15 +316,15 @@ async function handleApi(request, response, url) {
|
||||
sendJson(response, 404, { error: "接口不存在" });
|
||||
}
|
||||
|
||||
async function streamVideoTask(response, taskId) {
|
||||
async function streamVideoTask(request, response, taskId) {
|
||||
if (mockMode) throw new ClientError("Mock 视频没有可下载的真实文件", 404);
|
||||
requireToken(apiToken, "KK_API_KEY");
|
||||
const videoToken = requireVideoToken(request, taskId);
|
||||
const startedAt = Date.now();
|
||||
const context = requestContext.getStore() || {};
|
||||
writeAuditLog({ level: "info", event: "video.proxy.start", requestId: context.requestId, sessionId: context.sessionId, taskId });
|
||||
|
||||
const task = await callJson(`https://ai-api.kkidc.com/sd/api/v3/contents/generations/tasks/${encodeURIComponent(taskId)}`, {
|
||||
headers: bearerHeaders(false),
|
||||
headers: bearerHeaders(videoToken, false),
|
||||
});
|
||||
if (task.status < 200 || task.status >= 300) {
|
||||
throw new ClientError(task.body?.message || "无法读取视频任务", task.status);
|
||||
@@ -495,6 +500,20 @@ function requireToken(value, name) {
|
||||
if (!value) throw new ClientError(`服务端缺少 ${name} 配置`, 503);
|
||||
}
|
||||
|
||||
function videoTokenFromRequest(request, taskId = "") {
|
||||
const supplied = String(request.headers["x-video-key"] || "").trim();
|
||||
if (supplied) return supplied.slice(0, 500);
|
||||
if (taskId && videoTaskTokens.has(taskId)) return videoTaskTokens.get(taskId);
|
||||
return "";
|
||||
}
|
||||
|
||||
function requireVideoToken(request, taskId = "") {
|
||||
const token = videoTokenFromRequest(request, taskId);
|
||||
if (!token) throw new ClientError("请先输入生视频 Key", 401);
|
||||
if (!/^sk-[A-Za-z0-9_-]{8,}$/.test(token)) throw new ClientError("生视频 Key 格式不正确", 401);
|
||||
return token;
|
||||
}
|
||||
|
||||
function clamp(value, min, max) {
|
||||
return Math.min(max, Math.max(min, value));
|
||||
}
|
||||
@@ -568,9 +587,9 @@ function resolveVideoModel(value = "") {
|
||||
return "doubao-seedance-2-0-260128";
|
||||
}
|
||||
|
||||
function bearerHeaders(includeContentType = true) {
|
||||
function bearerHeaders(token, includeContentType = true) {
|
||||
return {
|
||||
Authorization: `Bearer ${apiToken}`,
|
||||
Authorization: `Bearer ${token}`,
|
||||
...(includeContentType ? { "Content-Type": "application/json" } : {}),
|
||||
};
|
||||
}
|
||||
@@ -586,7 +605,7 @@ async function refreshAssetReviewTask(task) {
|
||||
if (["ready", "failed"].includes(item.status)) return;
|
||||
const upstreamUrl = new URL(`https://ai-api.kkidc.com/v1/assets/${encodeURIComponent(item.assetId)}`);
|
||||
if (item.groupId) upstreamUrl.searchParams.set("group_id", item.groupId);
|
||||
const upstream = await callJson(upstreamUrl, { headers: bearerHeaders(false) });
|
||||
const upstream = await callJson(upstreamUrl, { headers: bearerHeaders(assetToken, false) });
|
||||
if (upstream.status >= 400 || upstream.body?.success === false) {
|
||||
item.status = "failed";
|
||||
item.errorMessage = extractUpstreamMessage(upstream.body) || "素材状态查询失败";
|
||||
@@ -714,7 +733,7 @@ function sendJson(response, status, body, trackId = "") {
|
||||
function corsHeaders() {
|
||||
return {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Headers": "Content-Type, X-Session-Id",
|
||||
"Access-Control-Allow-Headers": "Content-Type, X-Session-Id, X-Video-Key",
|
||||
"Access-Control-Expose-Headers": "X-Request-Id",
|
||||
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
||||
"Access-Control-Allow-Private-Network": "true",
|
||||
|
||||
Reference in New Issue
Block a user