From a107e19b8f6e85683ca3a8d21304640a87efd987 Mon Sep 17 00:00:00 2001 From: xuejianwu <374601383@qq.com> Date: Thu, 30 Jul 2026 09:19:20 +0800 Subject: [PATCH] fix: use dedicated legacy asset token API --- README.md | 2 +- public/app.js | 16 ++++++++++++---- server.mjs | 43 +++++++++++++++++++++++++------------------ 3 files changed, 38 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 6b50c33..bf25494 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ npm start - 素材审核与视频生成统一请求 `https://ai-api.kkidc.com`,但分别使用服务端素材库 Token 与客户视频 Key。 - 新版素材库只在列表中返回 `ready` 素材;`pending` / `failed` 通过单素材接口查询。 -- 视频生成应长期保存 `asset://mat_*`,不要依赖临时签名视频地址。 +- 视频生成应长期保存素材库返回的 `asset://...` 引用,不要依赖原始图片 URL。 - 参考图/参考视频/首帧/首尾帧属于不同互斥调用方式;本 Demo 采用“多模态参考图”方式。 - 默认视频 720p,时长 4–15 秒;真人/数字人必须使用审核通过的 `mat_*` 素材 ID。 diff --git a/public/app.js b/public/app.js index 18fc3da..04af459 100644 --- a/public/app.js +++ b/public/app.js @@ -312,7 +312,7 @@ async function loadAssets() { el.assetHistoryCount.textContent = String(items.length); el.assetList.innerHTML = ""; if (!items.length) { - el.assetList.innerHTML = '
新版素材库暂无历史素材
'; + el.assetList.innerHTML = '
素材库暂无历史素材
'; return; } for (const item of items) { @@ -340,7 +340,15 @@ async function loadAssets() { el.assetList.appendChild(button); } } catch (error) { - el.assetList.innerHTML = `
${escapeHtml(error.message)}
`; + const message = /unauthorized|invalid token/i.test(String(error.message || "")) + ? "素材库 Token 无效或已过期,请联系管理员更新" + : error.message; + el.assetList.innerHTML = `
${escapeHtml(message)}
`; + if (!state.config?.mockMode) { + el.connectionState.textContent = "素材库连接失败"; + el.connectionState.className = "connection blocked"; + setAssetStatus("error", "素材库不可用"); + } } } @@ -1119,7 +1127,7 @@ function restoreCharacterLibrary() { state.characters = Array.isArray(saved) ? saved.map((character) => ({ id: character.id || `character-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`, name: String(character.name || "未命名人物"), - references: Array.isArray(character.references) ? character.references.map(String).filter((value) => state.config.mockMode || /^asset:\/\/mat_/i.test(value)).slice(0, 9) : [], + references: Array.isArray(character.references) ? character.references.map(String).filter((value) => state.config.mockMode || /^asset:\/\/(?:mat_|asset-)/i.test(value)).slice(0, 9) : [], source: character.source || "manual", })).filter((character) => character.references.length) : []; } @@ -1220,7 +1228,7 @@ function restoreSceneRoles() { id: role.id || createRole("").id, name: role.name || `角色 ${ROLE_LETTERS[index]}`, references: Array.isArray(role.references) - ? role.references.slice(0, 3).map(String).filter((value) => state.config.mockMode || /^asset:\/\/mat_/i.test(value)) + ? role.references.slice(0, 3).map(String).filter((value) => state.config.mockMode || /^asset:\/\/(?:mat_|asset-)/i.test(value)) : [], })); state.activeRoleIndex = Math.min(Number(saved.activeRoleIndex) || 0, state.roles.length - 1); diff --git a/server.mjs b/server.mjs index 2e6ba74..ec535d7 100644 --- a/server.mjs +++ b/server.mjs @@ -182,24 +182,24 @@ async function handleApi(request, response, url) { 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", { + const upstream = await callJson("https://ai.kkidc.com/api/assets/upload", { method: "POST", - headers: bearerHeaders(assetToken), + headers: assetHeaders(), body: JSON.stringify({ url: input.url, - type: "image", - purpose: `${body.name || "数字人形象"} · ${viewLabel}`, + asset_type: "Image", + name: `${body.name || "数字人形象"} · ${viewLabel}`, }), }); - if (upstream.status >= 400 || upstream.body?.success === false || !upstream.body?.data?.asset_id) { + if (upstream.status >= 400 || Number(upstream.body?.code) !== 0 || !upstream.body?.data?.Id) { throw new ClientError(extractUpstreamMessage(upstream.body) || `${viewLabel}图提交失败`, upstream.status || 400); } return { sourceUrl: input.url, view: input.view || "reference", - assetId: upstream.body.data.asset_id, - groupId: upstream.body.data.group_id || "", - status: String(upstream.body.data.status || "pending").toLowerCase(), + assetId: upstream.body.data.Id, + groupId: "", + status: "processing", errorMessage: "", }; })); @@ -232,11 +232,11 @@ async function handleApi(request, response, url) { return; } 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"]) { + const upstreamUrl = new URL("https://ai.kkidc.com/api/assets"); + for (const key of ["page", "page_size", "name", "statuses", "sort_by", "sort_order"]) { for (const value of url.searchParams.getAll(key)) upstreamUrl.searchParams.append(key, value); } - const upstream = await callJson(upstreamUrl, { headers: bearerHeaders(assetToken, false) }); + const upstream = await callJson(upstreamUrl, { headers: assetHeaders(false) }); sendJson(response, upstream.status, upstream.body, upstream.trackId); return; } @@ -555,7 +555,7 @@ function buildVideoPayload(body) { const videoReferences = Array.isArray(body.videoReferences) ? body.videoReferences : []; const imageReferences = [...assetReferences, ...extraImageReferences]; if (assetReferences.length < 1 || assetReferences.some((item) => !/^asset:\/\/(?:mat_|asset-)/i.test(item))) { - throw new ClientError("请先选择审核通过的人物素材(asset://mat_...)"); + throw new ClientError("请先选择审核通过的人物素材(asset://...)"); } if (imageReferences.length > 9) { throw new ClientError(`人物图片与其他参考图片合计最多 9 张,当前为 ${imageReferences.length} 张`); @@ -619,6 +619,13 @@ 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; @@ -628,16 +635,16 @@ function extractUpstreamMessage(body) { async function refreshAssetReviewTask(task) { await Promise.all(task.items.map(async (item) => { 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(assetToken, false) }); - if (upstream.status >= 400 || upstream.body?.success === false) { + 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) { item.status = "failed"; item.errorMessage = extractUpstreamMessage(upstream.body) || "素材状态查询失败"; return; } - item.status = String(upstream.body?.data?.status || item.status || "pending").toLowerCase(); - item.errorMessage = upstream.body?.data?.error_message || upstream.body?.message || ""; + 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 || "素材审核失败" : ""; })); }