feat(assets): 素材库 - 方舟素材 ID 引用与人像认证标记

- 素材编辑弹窗新增「方舟素材 ID」(asset- 前缀校验)与「人像认证」开关,
  卡片封面显示人像认证徽章,详情抽屉展示方舟素材 ID
- 画布图片节点绑定的素材录入方舟素材 ID 后,方舟视频渠道(volcengine-ark-video
  或 Ark Plan BaseURL)生成时参考图直接提交 asset:// 引用,跳过可信素材上传同步;
  浏览器直连路径同样优先 asset://;非方舟渠道与本地回退路径不受影响
- 素材字段随 payload_json 透传,AssetBase 与 parseAssetRecord 白名单同步扩展
This commit is contained in:
VicoHu
2026-09-11 14:07:31 +08:00
committed by ddCat
parent 900420390e
commit 54e58213ba
8 changed files with 75 additions and 11 deletions

View File

@@ -71,7 +71,7 @@ export function buildNodeGenerationContext(nodeId: string, nodes: CanvasNodeData
const reason = status === "stale" ? "输入或方式已变化" : status === "processing" ? "仍在处理中" : "尚未完成本地转换";
throw new Error(`转换节点「${pendingConversion.title || pendingConversion.id}」${reason},完成后才能执行下游生成`);
}
const connectedInputs = buildNodeGenerationInputs(nodeId, nodes, connections);
const connectedInputs = withArkAssetReferenceInputs(buildNodeGenerationInputs(nodeId, nodes, connections), nodes, assets);
const sourceNode = nodes.find((node) => node.id === nodeId);
const portraitTextureInput = sourceNode?.type === CanvasNodeType.Image && sourceNode.metadata?.content && sourceNode.metadata?.portraitTexture
? (() => {
@@ -81,7 +81,7 @@ export function buildNodeGenerationContext(nodeId: string, nodes: CanvasNodeData
: [];
// 显式 @ 引用必须与提示词面板展示的资源集合一致;默认自动输入仍只取入边,
// 避免已有图片在没有 @图片N 时被悄悄当作自身参考图。
const mentionInputs = mergeGenerationInputs(buildNodeMentionGenerationInputs(nodeId, nodes, connections), portraitTextureInput, buildAssetGenerationInputs(assets));
const mentionInputs = withArkAssetReferenceInputs(mergeGenerationInputs(buildNodeMentionGenerationInputs(nodeId, nodes, connections), portraitTextureInput, buildAssetGenerationInputs(assets)), nodes, assets);
const storyboardInputs = getConnectedStoryboardRows(nodeId, nodes, connections);
assertResolvableGenerationMentions(prompt, mentionInputs);
const hasExplicitResourceMention = hasResolvableGenerationMention(prompt, mentionInputs);
@@ -407,7 +407,7 @@ function buildAssetGenerationInputs(assets: Asset[]): NodeGenerationInput[] {
return assets.flatMap((asset): NodeGenerationInput[] => {
const nodeId = `asset:${asset.id}`;
if (asset.kind === "text") return [{ nodeId, type: "text", title: asset.title, text: asset.data.content }];
if (asset.kind === "image") return [{ nodeId, type: "image", title: asset.title, image: { id: asset.id, name: asset.title, type: asset.data.mimeType, dataUrl: asset.data.dataUrl, storageKey: asset.data.storageKey, bytes: asset.data.bytes, width: asset.data.width, height: asset.data.height } }];
if (asset.kind === "image") return [{ nodeId, type: "image", title: asset.title, image: { id: asset.id, name: asset.title, type: asset.data.mimeType, dataUrl: asset.data.dataUrl, storageKey: asset.data.storageKey, bytes: asset.data.bytes, width: asset.data.width, height: asset.data.height, ...(asset.arkAssetId ? { arkAssetId: asset.arkAssetId } : {}) } }];
if (asset.kind === "video") return [{ nodeId, type: "video", title: asset.title, previewUrl: canvasVideoAssetPreviewUrl(asset.data.url, asset.coverUrl), video: { id: asset.id, name: asset.title, type: asset.data.mimeType, url: asset.data.url, storageKey: asset.data.storageKey, bytes: asset.data.bytes, width: asset.data.width, height: asset.data.height, durationMs: asset.data.durationMs } }];
if (asset.kind === "audio") return [{ nodeId, type: "audio", title: asset.title, audio: { id: asset.id, name: asset.title, type: asset.data.mimeType, url: asset.data.url, storageKey: asset.data.storageKey, bytes: asset.data.bytes, durationMs: asset.data.durationMs } }];
if (asset.kind === "entity" && asset.category === "character") return [{ nodeId, type: "character", title: asset.title, character: { nodeId, assetId: asset.id, requestedVersionId: asset.primaryVersionId } }];
@@ -415,6 +415,20 @@ function buildAssetGenerationInputs(assets: Asset[]): NodeGenerationInput[] {
});
}
// 图片节点绑定的素材若已录入方舟素材 ID,把 ID 附加到参考图上;是否改用 asset:// 引用
// 由 generation-task 按视频渠道决定,非方舟渠道与本地回退路径不受影响。
function withArkAssetReferenceInputs(inputs: NodeGenerationInput[], nodes: CanvasNodeData[], assets: Asset[]): NodeGenerationInput[] {
if (!assets.length) return inputs;
const nodeById = new Map(nodes.map((node) => [node.id, node]));
const assetById = new Map(assets.map((asset) => [asset.id, asset]));
return inputs.map((input) => {
if (input.type !== "image" || !input.image || input.image.arkAssetId) return input;
const boundAssetId = nodeById.get(input.nodeId)?.metadata?.assetId;
const arkAssetId = boundAssetId ? assetById.get(boundAssetId)?.arkAssetId : undefined;
return arkAssetId ? { ...input, image: { ...input.image, arkAssetId } } : input;
});
}
function getConnectedStoryboardRows(nodeId: string, nodes: CanvasNodeData[], connections: CanvasConnection[]): NodeGenerationInput[] {
const targetNodeIds = new Set([nodeId]);
connections.forEach((connection) => {