fix: use customer token for assets and video

This commit is contained in:
xuejianwu
2026-07-30 09:35:06 +08:00
parent a107e19b8f
commit e111833df6
5 changed files with 63 additions and 75 deletions

View File

@@ -21,7 +21,6 @@ loadLocalEnv();
const port = Number(process.env.PORT || 4173);
const host = process.env.HOST || "127.0.0.1";
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";
@@ -118,13 +117,13 @@ async function handleApi(request, response, url) {
}
if (request.method === "GET" && url.pathname === "/api/config") {
const videoToken = videoTokenFromRequest(request);
const customerToken = customerTokenFromRequest(request);
sendJson(response, 200, {
mockMode,
assetReady: mockMode || Boolean(assetToken),
videoReady: mockMode || Boolean(videoToken),
assetReady: mockMode || Boolean(customerToken),
videoReady: mockMode || Boolean(customerToken),
apiVersion: "v3",
missing: !mockMode && !assetToken ? ["KK_ASSET_TOKEN"] : [],
missing: !mockMode && !customerToken ? ["客户令牌"] : [],
});
return;
}
@@ -178,28 +177,28 @@ async function handleApi(request, response, url) {
return;
}
requireToken(assetToken, "KK_ASSET_TOKEN");
const customerToken = requireCustomerToken(request);
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.kkidc.com/api/assets/upload", {
const upstream = await callJson("https://ai-api.kkidc.com/v1/assets/upload", {
method: "POST",
headers: assetHeaders(),
headers: bearerHeaders(customerToken),
body: JSON.stringify({
url: input.url,
asset_type: "Image",
name: `${body.name || "数字人形象"} · ${viewLabel}`,
type: "image",
purpose: `${body.name || "数字人形象"} · ${viewLabel}`,
}),
});
if (upstream.status >= 400 || Number(upstream.body?.code) !== 0 || !upstream.body?.data?.Id) {
if (upstream.status >= 400 || upstream.body?.success === false || !upstream.body?.data?.asset_id) {
throw new ClientError(extractUpstreamMessage(upstream.body) || `${viewLabel}图提交失败`, upstream.status || 400);
}
return {
sourceUrl: input.url,
view: input.view || "reference",
assetId: upstream.body.data.Id,
groupId: "",
status: "processing",
assetId: upstream.body.data.asset_id,
groupId: upstream.body.data.group_id || "",
status: String(upstream.body.data.status || "pending").toLowerCase(),
errorMessage: "",
};
}));
@@ -212,11 +211,11 @@ async function handleApi(request, response, url) {
}
if (request.method === "GET" && url.pathname.startsWith("/api/assets/tasks/")) {
requireToken(assetToken, "KK_ASSET_TOKEN");
const customerToken = requireCustomerToken(request);
const taskId = decodeURIComponent(url.pathname.split("/").pop());
const task = assetReviewTasks.get(taskId);
if (!task) throw new ClientError("素材审核任务不存在或本地服务已重启,请在素材库刷新状态", 404);
await refreshAssetReviewTask(task);
await refreshAssetReviewTask(task, customerToken);
sendJson(response, 200, formatAssetReviewTask(task));
return;
}
@@ -231,12 +230,12 @@ async function handleApi(request, response, url) {
});
return;
}
requireToken(assetToken, "KK_ASSET_TOKEN");
const upstreamUrl = new URL("https://ai.kkidc.com/api/assets");
for (const key of ["page", "page_size", "name", "statuses", "sort_by", "sort_order"]) {
const customerToken = requireCustomerToken(request);
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: assetHeaders(false) });
const upstream = await callJson(upstreamUrl, { headers: bearerHeaders(customerToken, false) });
sendJson(response, upstream.status, upstream.body, upstream.trackId);
return;
}
@@ -261,7 +260,7 @@ async function handleApi(request, response, url) {
return;
}
const videoToken = requireVideoToken(request);
const videoToken = requireCustomerToken(request);
const upstream = await callJson("https://ai-api.kkidc.com/sd/api/v3/contents/generations/tasks", {
method: "POST",
headers: bearerHeaders(videoToken),
@@ -298,7 +297,7 @@ async function handleApi(request, response, url) {
return;
}
const videoToken = requireVideoToken(request, taskId);
const videoToken = requireCustomerToken(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(videoToken, false),
@@ -320,7 +319,7 @@ async function handleApi(request, response, url) {
async function streamVideoTask(request, response, taskId) {
if (mockMode) throw new ClientError("Mock 视频没有可下载的真实文件", 404);
const videoToken = requireVideoToken(request, taskId);
const videoToken = requireCustomerToken(request, taskId);
const startedAt = Date.now();
const context = requestContext.getStore() || {};
writeAuditLog({
@@ -516,21 +515,17 @@ function validatePublicUrl(value) {
}
}
function requireToken(value, name) {
if (!value) throw new ClientError(`服务端缺少 ${name} 配置`, 503);
}
function videoTokenFromRequest(request, taskId = "") {
function customerTokenFromRequest(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);
function requireCustomerToken(request, taskId = "") {
const token = customerTokenFromRequest(request, taskId);
if (!token) throw new ClientError("请先输入客户令牌", 401);
if (!/^sk-[A-Za-z0-9_-]{8,}$/.test(token)) throw new ClientError("客户令牌格式不正确", 401);
const context = requestContext.getStore();
if (context) {
context.videoKeyId = hashAuditValue(token).slice(0, 12);
@@ -619,32 +614,25 @@ function bearerHeaders(token, includeContentType = true) {
};
}
function assetHeaders(includeContentType = true) {
return {
Authorization: assetToken,
...(includeContentType ? { "Content-Type": "application/json" } : {}),
};
}
function extractUpstreamMessage(body) {
if (!body) return "";
if (typeof body === "string") return body;
return body.error?.message || body.error || body.message || body.detail || "";
}
async function refreshAssetReviewTask(task) {
async function refreshAssetReviewTask(task, customerToken) {
await Promise.all(task.items.map(async (item) => {
if (["ready", "failed"].includes(item.status)) return;
const upstreamUrl = new URL(`https://ai.kkidc.com/api/assets/${encodeURIComponent(item.assetId)}`);
const upstream = await callJson(upstreamUrl, { headers: assetHeaders(false) });
if (upstream.status >= 400 || Number(upstream.body?.code) !== 0) {
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(customerToken, false) });
if (upstream.status >= 400 || upstream.body?.success === false) {
item.status = "failed";
item.errorMessage = extractUpstreamMessage(upstream.body) || "素材状态查询失败";
return;
}
const remoteStatus = String(upstream.body?.data?.Status || "Processing").toLowerCase();
item.status = remoteStatus === "active" ? "ready" : remoteStatus === "failed" ? "failed" : "processing";
item.errorMessage = item.status === "failed" ? upstream.body?.message || "素材审核失败" : "";
item.status = String(upstream.body?.data?.status || item.status || "pending").toLowerCase();
item.errorMessage = upstream.body?.data?.error_message || upstream.body?.message || "";
}));
}