feat(*): 分镜与资产 - 完善五类资产、候选去重、搜索、中文镜头编号与分类整理

This commit is contained in:
ddCat
2026-08-31 13:15:38 +08:00
parent ce6e20f749
commit f548681787
53 changed files with 816 additions and 188 deletions

View File

@@ -11,6 +11,7 @@ import {
type ProjectDetail,
type ShotAssetReference,
} from "./projects";
import { normalizeAssetCategory, type AssetCategory } from "@/lib/asset-category";
export const projectAgentToolNames = [
"project_get_context",
@@ -47,7 +48,7 @@ export async function runProjectAgentTool(name: ProjectAgentToolName, rawInput:
}
if (name === "project_extract_asset_candidates") {
const candidates = Array.isArray(rawInput.candidates) ? rawInput.candidates : [];
return createProjectAssetCandidates(projectId, candidates.filter(isCandidateInput));
return createProjectAssetCandidates(projectId, candidates.map(normalizeAgentCandidateInput).filter((candidate): candidate is NonNullable<typeof candidate> => Boolean(candidate)), "agent");
}
if (name === "project_confirm_asset_candidate") {
return confirmProjectAssetCandidate(projectId, String(rawInput.candidateId || ""), String(rawInput.assetId || "") || undefined);
@@ -68,7 +69,7 @@ export async function runProjectAgentTool(name: ProjectAgentToolName, rawInput:
return updateWorkflowStep(projectId, String(rawInput.stepId || ""), { status: "running" });
}
if (name === "project_link_asset") {
return linkProjectAsset(projectId, { assetId: String(rawInput.assetId || ""), category: String(rawInput.category || "other") });
return linkProjectAsset(projectId, { assetId: String(rawInput.assetId || ""), category: normalizeAssetCategory(rawInput.category) });
}
if (name === "project_upsert_asset_version") {
return createProjectAssetVersion(projectId, String(rawInput.assetId || ""), { prompt: String(rawInput.prompt || ""), definitionJson: typeof rawInput.definitionJson === "string" ? rawInput.definitionJson : undefined, note: String(rawInput.note || "") });
@@ -79,10 +80,21 @@ export async function runProjectAgentTool(name: ProjectAgentToolName, rawInput:
throw new Error(`未知项目工具:${name}`);
}
function isCandidateInput(value: unknown): value is { unitId?: string; shotId?: string; name: string; category: string; details?: Record<string, unknown> } {
if (!value || typeof value !== "object") return false;
const agentCandidateCategories = new Set<AssetCategory>(["environment", "prop", "material", "other"]);
function normalizeAgentCandidateInput(value: unknown): { unitId?: string; shotId?: string; name: string; category: AssetCategory; details?: Record<string, unknown> } | null {
if (!value || typeof value !== "object") return null;
const item = value as Record<string, unknown>;
return typeof item.name === "string" && typeof item.category === "string";
if (typeof item.name !== "string" || typeof item.category !== "string") return null;
const category = normalizeAssetCategory(item.category);
if (!agentCandidateCategories.has(category)) return null;
return {
name: item.name,
category,
...(typeof item.unitId === "string" ? { unitId: item.unitId } : {}),
...(typeof item.shotId === "string" ? { shotId: item.shotId } : {}),
...(item.details && typeof item.details === "object" && !Array.isArray(item.details) ? { details: item.details as Record<string, unknown> } : {}),
};
}
function isShotInput(value: unknown): value is { id?: string; unitId?: string; title: string; description?: string; position?: number; durationMs?: number; status?: string } {

View File

@@ -1,4 +1,5 @@
import { apiClient, request } from "@/services/api/request";
import { normalizeAssetCategory, type AssetCategory } from "@/lib/asset-category";
import type { GenerationTask } from "@/services/api/task-center";
const api = apiClient;
@@ -56,7 +57,7 @@ export type ProjectAsset = {
id: string;
title: string;
mediaType: string;
category: string;
category: AssetCategory;
status: string;
primaryVersionId?: string;
versionCount: number;
@@ -121,7 +122,7 @@ export type ProjectAssetCandidate = {
unitId?: string;
shotId?: string;
name: string;
category: string;
category: AssetCategory;
status: "pending_confirmation" | "confirmed" | "ignored" | string;
detailsJson: string;
resolvedAssetId?: string;
@@ -365,21 +366,30 @@ export function listProjectAssetsPage(projectId: string, options: { page?: numbe
status: options.status || undefined,
folder_id: options.folderId,
q: options.query || undefined,
} }));
} })).then((page) => ({ ...page, assets: page.assets.map(normalizeProjectAsset) }));
}
export function listProjectAssets(projectId: string) {
return request<{ assets: ProjectAsset[] }>(api.get(`/projects/${encodeURIComponent(projectId)}/assets`));
return request<{ assets: ProjectAsset[] }>(api.get(`/projects/${encodeURIComponent(projectId)}/assets`)).then((result) => ({ assets: result.assets.map(normalizeProjectAsset) }));
}
export function listProjectAssetCandidates(projectId: string, options: { page?: number; pageSize?: number; unitId?: string; status?: string; category?: string } = {}) {
export function listProjectAssetCandidates(projectId: string, options: { page?: number; pageSize?: number; unitId?: string; status?: string; category?: string; query?: string } = {}) {
return request<ProjectAssetCandidatePage>(api.get(`/projects/${encodeURIComponent(projectId)}/asset-candidates`, { params: {
page: options.page || 1,
page_size: options.pageSize || 100,
unit_id: options.unitId || undefined,
status: options.status || undefined,
category: options.category || undefined,
} }));
q: options.query || undefined,
} })).then((page) => ({ ...page, candidates: page.candidates.map(normalizeProjectAssetCandidate) }));
}
function normalizeProjectAsset(asset: ProjectAsset): ProjectAsset {
return { ...asset, category: normalizeAssetCategory(asset.category), usages: Array.isArray(asset.usages) ? asset.usages : [] };
}
function normalizeProjectAssetCandidate(candidate: ProjectAssetCandidate): ProjectAssetCandidate {
return { ...candidate, category: normalizeAssetCategory(candidate.category) };
}
function normalizeProjectDetail(detail: ProjectDetail): ProjectDetail {
@@ -391,8 +401,7 @@ function normalizeProjectDetail(detail: ProjectDetail): ProjectDetail {
: [];
const assets = Array.isArray(detail.assets)
? detail.assets.map((asset) => ({
...asset,
usages: Array.isArray(asset.usages) ? asset.usages : [],
...normalizeProjectAsset(asset),
...(asset.character ? {
character: {
...asset.character,
@@ -415,7 +424,7 @@ function normalizeProjectDetail(detail: ProjectDetail): ProjectDetail {
shotRevisions: Array.isArray(detail.shotRevisions) ? detail.shotRevisions : [],
shotArtifacts: Array.isArray(detail.shotArtifacts) ? detail.shotArtifacts : [],
shotReferences: Array.isArray(detail.shotReferences) ? detail.shotReferences : [],
assetCandidates: Array.isArray(detail.assetCandidates) ? detail.assetCandidates : [],
assetCandidates: Array.isArray(detail.assetCandidates) ? detail.assetCandidates.map(normalizeProjectAssetCandidate) : [],
tasks: Array.isArray(detail.tasks) ? detail.tasks : [],
};
}
@@ -468,7 +477,7 @@ export function unlinkCanvasProject(projectId: string, canvasId: string) {
return request<{ canvasId: string }>(api.delete(`/projects/${encodeURIComponent(projectId)}/canvases/${encodeURIComponent(canvasId)}`));
}
export function linkProjectAsset(projectId: string, input: { assetId: string; category: string; folderId?: string }, signal?: AbortSignal) {
export function linkProjectAsset(projectId: string, input: { assetId: string; category: AssetCategory; folderId?: string }, signal?: AbortSignal) {
return request<{ asset: ProjectAsset }>(api.post(`/projects/${encodeURIComponent(projectId)}/assets`, input, { signal }));
}
@@ -476,7 +485,7 @@ export function unlinkProjectAsset(projectId: string, assetId: string) {
return request<{ id: string }>(api.delete(`/projects/${encodeURIComponent(projectId)}/assets/${encodeURIComponent(assetId)}`));
}
export function updateProjectAssetCategory(projectId: string, assetId: string, category: string, signal?: AbortSignal) {
export function updateProjectAssetCategory(projectId: string, assetId: string, category: AssetCategory, signal?: AbortSignal) {
return request<{ asset: ProjectAsset }>(api.patch(`/projects/${encodeURIComponent(projectId)}/assets/${encodeURIComponent(assetId)}`, { category }, { signal }));
}
@@ -560,8 +569,8 @@ export function createShotRevision(projectId: string, shotId: string, input: Sho
return request<{ shot: ProjectShot; revision: ShotRevision }>(api.post(`/projects/${encodeURIComponent(projectId)}/shots/${encodeURIComponent(shotId)}/revisions`, input));
}
export function createProjectAssetCandidates(projectId: string, candidates: Array<{ unitId?: string; shotId?: string; name: string; category: string; details?: Record<string, unknown> }>) {
return request<{ candidates: ProjectAssetCandidate[] }>(api.post(`/projects/${encodeURIComponent(projectId)}/asset-candidates`, { candidates }));
export function createProjectAssetCandidates(projectId: string, candidates: Array<{ unitId?: string; shotId?: string; name: string; category: AssetCategory; details?: Record<string, unknown> }>, source?: "chapter_character_extract" | "agent") {
return request<{ candidates: ProjectAssetCandidate[] }>(api.post(`/projects/${encodeURIComponent(projectId)}/asset-candidates`, { candidates, source }));
}
export function confirmProjectAssetCandidate(projectId: string, candidateId: string, assetId?: string) {