停止向用户泄露抠图模型
This commit is contained in:
@@ -169,6 +169,8 @@
|
||||
- 工程刷新后能从后端恢复资源、图层布局和 viewport。
|
||||
- “我的”页项目入口能进入 `/project`;项目页能列出工程、重命名 / 删除单个工程、批量选择和批量删除;点击工程后进入 `/editor/canvas?projectid=<projectId>` 并按 query 加载该工程。
|
||||
|
||||
- 用户可见的 `generationInputs.fields` 不得写入或展示“处理模型”;角色抠图、图标 / UI 图集抠图及自动拆分形成的派生资产,其 `Model` 继续继承并展示源生图模型,不得改写为 `birefnet`、`anime-seg`、`connected-components`、阿里云抠图或本地键色等内部后处理模型。前端同时过滤历史项目中已持久化的“处理模型”字段;历史资源若只剩内部处理模型而无法恢复源生图模型,保留 `Model` 行并显示 `-`。画布 ZIP 的用户可见导出元数据使用同一过滤口径且保持既有结构。
|
||||
|
||||
## 后续扩展点
|
||||
|
||||
- 接入图片生成 / 修改计费、队列进度状态、OSS 落盘和更完整失败审计。
|
||||
|
||||
@@ -1908,7 +1908,7 @@ pub(crate) async fn generate_editor_image_for_owner(
|
||||
let storage_profile = editor_generated_image_storage_profile(normalized_kind);
|
||||
let mut output_prompt = submitted_prompt.clone();
|
||||
let mut output_actual_prompt = generated.actual_prompt.clone();
|
||||
let mut output_model = generation_options.model.to_string();
|
||||
let output_model = generation_options.model.to_string();
|
||||
let mut output_provider = "VectorEngine".to_string();
|
||||
let mut output_generation_inputs = generation_inputs.clone();
|
||||
let source_record = if is_character_generation {
|
||||
@@ -2110,10 +2110,9 @@ pub(crate) async fn generate_editor_image_for_owner(
|
||||
image = restored_removal_image;
|
||||
output_prompt = "去除纯色背景".to_string();
|
||||
output_actual_prompt = None;
|
||||
output_model = removal.model;
|
||||
output_provider = removal_provider.to_string();
|
||||
output_generation_inputs =
|
||||
build_editor_derived_asset_generation_inputs("角色抠图", &output_model, &source_record);
|
||||
build_editor_derived_asset_generation_inputs("角色抠图", &source_record);
|
||||
Some(source_record)
|
||||
} else {
|
||||
None
|
||||
@@ -2268,7 +2267,6 @@ pub(crate) fn apply_editor_screen_background_decision_to_generation_inputs(
|
||||
|
||||
fn build_editor_derived_asset_generation_inputs(
|
||||
stage: &str,
|
||||
model: &str,
|
||||
source: &EditorGeneratedAssetRecord,
|
||||
) -> Option<Value> {
|
||||
let reference = source
|
||||
@@ -2299,7 +2297,6 @@ fn build_editor_derived_asset_generation_inputs(
|
||||
Some(json!({
|
||||
"fields": [
|
||||
{ "title": "处理阶段", "value": stage },
|
||||
{ "title": "处理模型", "value": model },
|
||||
],
|
||||
"references": reference.into_iter().collect::<Vec<_>>(),
|
||||
}))
|
||||
@@ -4157,7 +4154,6 @@ async fn request_editor_background_removal_image_with_bgfilter_worker(
|
||||
pub(crate) struct EditorScreenBackgroundRemovalOutput {
|
||||
pub(crate) image: DownloadedOpenAiImage,
|
||||
pub(crate) provider: &'static str,
|
||||
pub(crate) model: String,
|
||||
}
|
||||
|
||||
pub(crate) async fn remove_editor_generated_screen_background_with_bgfilter(
|
||||
@@ -4192,7 +4188,6 @@ pub(crate) async fn remove_editor_generated_screen_background_with_bgfilter(
|
||||
mime_type: image.mime_type,
|
||||
},
|
||||
provider: "BgFilter",
|
||||
model: seg_model.to_string(),
|
||||
});
|
||||
}
|
||||
Err(error) if !error.allows_flat_fallback() => {
|
||||
@@ -4247,7 +4242,6 @@ async fn fallback_editor_screen_background_removal(
|
||||
Ok(image) => Ok(EditorScreenBackgroundRemovalOutput {
|
||||
image,
|
||||
provider: "Aliyun Matting",
|
||||
model: "segment-common-image".to_string(),
|
||||
}),
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
@@ -4285,7 +4279,6 @@ async fn fallback_editor_screen_background_removal(
|
||||
output.map(|image| EditorScreenBackgroundRemovalOutput {
|
||||
image,
|
||||
provider: "Genarrative Local",
|
||||
model: "screen-color-keying".to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4825,11 +4818,8 @@ pub(crate) async fn generate_editor_icon_spritesheet_for_owner(
|
||||
Some(editor_pixel_art_fallback_warning()),
|
||||
);
|
||||
}
|
||||
let matting_generation_inputs = build_editor_derived_asset_generation_inputs(
|
||||
"图标图集抠图",
|
||||
&removal.model,
|
||||
&source_record,
|
||||
);
|
||||
let matting_generation_inputs =
|
||||
build_editor_derived_asset_generation_inputs("图标图集抠图", &source_record);
|
||||
let (spritesheet_width, spritesheet_height) = image::load_from_memory(image.bytes.as_slice())
|
||||
.map(|image| (image.width(), image.height()))
|
||||
.unwrap_or((512, 512));
|
||||
@@ -4864,7 +4854,7 @@ pub(crate) async fn generate_editor_icon_spritesheet_for_owner(
|
||||
height: spritesheet_height,
|
||||
prompt: "去除纯色背景".to_string(),
|
||||
actual_prompt: None,
|
||||
model: removal.model.clone(),
|
||||
model: generation_options.model.to_string(),
|
||||
provider: removal.provider.to_string(),
|
||||
task_id: generated.task_id.clone(),
|
||||
group_task_id: None,
|
||||
@@ -4902,11 +4892,10 @@ pub(crate) async fn generate_editor_icon_spritesheet_for_owner(
|
||||
group_task_id: None,
|
||||
prompt: "自动拆分图集".to_string(),
|
||||
actual_prompt: None,
|
||||
model: "connected-components".to_string(),
|
||||
model: generation_options.model.to_string(),
|
||||
provider: "Genarrative".to_string(),
|
||||
generation_inputs: build_editor_derived_asset_generation_inputs(
|
||||
"图集拆分",
|
||||
"connected-components",
|
||||
&spritesheet_record,
|
||||
),
|
||||
path_kind: "icon-spritesheet-assets",
|
||||
@@ -5588,7 +5577,7 @@ pub(crate) async fn extract_editor_ui_design_assets_for_owner(
|
||||
);
|
||||
}
|
||||
let matting_generation_inputs =
|
||||
build_editor_derived_asset_generation_inputs("UI图集抠图", &removal.model, &source_record);
|
||||
build_editor_derived_asset_generation_inputs("UI图集抠图", &source_record);
|
||||
let (spritesheet_width, spritesheet_height) = image::load_from_memory(image.bytes.as_slice())
|
||||
.map(|image| (image.width(), image.height()))
|
||||
.unwrap_or((512, 512));
|
||||
@@ -5623,7 +5612,7 @@ pub(crate) async fn extract_editor_ui_design_assets_for_owner(
|
||||
height: spritesheet_height,
|
||||
prompt: "去除纯色背景".to_string(),
|
||||
actual_prompt: None,
|
||||
model: removal.model.clone(),
|
||||
model: generation_options.model.to_string(),
|
||||
provider: removal.provider.to_string(),
|
||||
task_id: generated.task_id.clone(),
|
||||
group_task_id: None,
|
||||
@@ -5662,11 +5651,10 @@ pub(crate) async fn extract_editor_ui_design_assets_for_owner(
|
||||
group_task_id: None,
|
||||
prompt: "自动拆分图集".to_string(),
|
||||
actual_prompt: None,
|
||||
model: "connected-components".to_string(),
|
||||
model: generation_options.model.to_string(),
|
||||
provider: "Genarrative".to_string(),
|
||||
generation_inputs: build_editor_derived_asset_generation_inputs(
|
||||
"图集拆分",
|
||||
"connected-components",
|
||||
&spritesheet_record,
|
||||
),
|
||||
path_kind: "ui-design-assets",
|
||||
@@ -11891,6 +11879,7 @@ mod tests {
|
||||
"persist_editor_provider_source_resource",
|
||||
"build_multi_asset_canvas_layer_items",
|
||||
"if is_character_generation",
|
||||
"let output_model = generation_options.model.to_string();",
|
||||
],
|
||||
);
|
||||
assert_function_contains_in_order(
|
||||
@@ -11916,6 +11905,7 @@ mod tests {
|
||||
"persist_editor_provider_source_resource",
|
||||
"slice_editor_icon_spritesheet_all",
|
||||
"persist_editor_spritesheet_slices",
|
||||
"model: generation_options.model.to_string(),",
|
||||
],
|
||||
);
|
||||
assert_function_contains_in_order(
|
||||
@@ -11951,6 +11941,7 @@ mod tests {
|
||||
"EDITOR_BGFILTER_CROSS_CHECK_DISABLED",
|
||||
"persist_editor_provider_source_resource",
|
||||
"slice_generated_icon_spritesheet_all_by_connected_components",
|
||||
"model: generation_options.model.to_string(),",
|
||||
],
|
||||
);
|
||||
assert_function_contains_in_order(
|
||||
|
||||
@@ -282,6 +282,8 @@ describe('ImageCanvasEditorModel', () => {
|
||||
zIndex: 9,
|
||||
sourceType: 'generated',
|
||||
objectKey: 'generated/object.png',
|
||||
model: 'birefnet',
|
||||
sourceResourceId: 'resource-provider-source',
|
||||
assetKind: 'character',
|
||||
generationInputs: {
|
||||
fields: [{ title: '角色设定', value: '骑士' }],
|
||||
@@ -307,6 +309,15 @@ describe('ImageCanvasEditorModel', () => {
|
||||
fields: [{ title: '角色设定', value: '骑士' }],
|
||||
references: [],
|
||||
},
|
||||
model: 'birefnet',
|
||||
sourceResourceId: 'resource-provider-source',
|
||||
},
|
||||
],
|
||||
[
|
||||
'resource-provider-source',
|
||||
{
|
||||
imageSrc: '/read/provider-source.png',
|
||||
model: 'gpt-image-2',
|
||||
},
|
||||
],
|
||||
]),
|
||||
@@ -318,6 +329,7 @@ describe('ImageCanvasEditorModel', () => {
|
||||
sourceType: 'generated',
|
||||
assetKind: 'character',
|
||||
objectKey: 'generated/object.png',
|
||||
model: 'gpt-image-2',
|
||||
locked: true,
|
||||
});
|
||||
expect(hydrated?.generationInputs?.fields[0]?.value).toBe('骑士');
|
||||
|
||||
@@ -43,6 +43,24 @@ export const MINIMAP_SIZE = { width: 132, height: 84 };
|
||||
export const MINIMAP_PADDING = 8;
|
||||
export const MINIMAP_DRAG_SENSITIVITY = 0.3;
|
||||
export const ASSET_DRAG_MIME_TYPE = 'application/x-genarrative-editor-asset';
|
||||
const INTERNAL_EDITOR_PROCESSING_MODELS = new Set([
|
||||
'anime-seg',
|
||||
'bgfilter complex',
|
||||
'birefnet',
|
||||
'connected-components',
|
||||
'screen-color-keying',
|
||||
'segment-common-image',
|
||||
]);
|
||||
|
||||
export function isEditorInternalProcessingModel(
|
||||
model: string | null | undefined,
|
||||
) {
|
||||
const normalizedModel = model?.trim().toLowerCase();
|
||||
return Boolean(
|
||||
normalizedModel && INTERNAL_EDITOR_PROCESSING_MODELS.has(normalizedModel),
|
||||
);
|
||||
}
|
||||
|
||||
export const MAX_HISTORY_STEPS = 60;
|
||||
export const CONTEXT_MENU_VIEWPORT_MARGIN = 8;
|
||||
export const CONTEXT_MENU_SIZE = {
|
||||
@@ -734,7 +752,7 @@ export function hydrateLayer(
|
||||
prompt: stringOrNull(snapshot.prompt) ?? stringOrNull(resource?.prompt),
|
||||
actualPrompt:
|
||||
stringOrNull(snapshot.actualPrompt) ?? stringOrNull(resource?.actualPrompt),
|
||||
model: stringOrNull(snapshot.model) ?? stringOrNull(resource?.model),
|
||||
model: resolveHydratedLayerModel(snapshot, resource, resourcesById),
|
||||
provider: stringOrNull(snapshot.provider) ?? stringOrNull(resource?.provider),
|
||||
taskId: stringOrNull(snapshot.taskId) ?? stringOrNull(resource?.taskId),
|
||||
objectKey: stringOrNull(snapshot.objectKey) ?? stringOrNull(resource?.objectKey),
|
||||
@@ -761,6 +779,33 @@ export function hydrateLayer(
|
||||
};
|
||||
}
|
||||
|
||||
function resolveHydratedLayerModel(
|
||||
snapshot: EditorProjectLayerSnapshot,
|
||||
resource: CanvasLayerResourceMetadata | undefined,
|
||||
resourcesById: Map<string, CanvasLayerResourceMetadata>,
|
||||
) {
|
||||
const persistedModel =
|
||||
stringOrNull(snapshot.model) ?? stringOrNull(resource?.model);
|
||||
if (!isEditorInternalProcessingModel(persistedModel)) {
|
||||
return persistedModel;
|
||||
}
|
||||
|
||||
let sourceResourceId =
|
||||
stringOrNull(snapshot.sourceResourceId) ??
|
||||
stringOrNull(resource?.sourceResourceId);
|
||||
const visitedResourceIds = new Set<string>();
|
||||
while (sourceResourceId && !visitedResourceIds.has(sourceResourceId)) {
|
||||
visitedResourceIds.add(sourceResourceId);
|
||||
const sourceResource = resourcesById.get(sourceResourceId);
|
||||
const sourceModel = stringOrNull(sourceResource?.model);
|
||||
if (sourceModel && !isEditorInternalProcessingModel(sourceModel)) {
|
||||
return sourceModel;
|
||||
}
|
||||
sourceResourceId = stringOrNull(sourceResource?.sourceResourceId);
|
||||
}
|
||||
return persistedModel;
|
||||
}
|
||||
|
||||
export function mapAssetLibrarySnapshot(
|
||||
library: EditorAssetLibrarySnapshot,
|
||||
): {
|
||||
|
||||
@@ -175,6 +175,7 @@ describe('ImageCanvasExportModel', () => {
|
||||
it('filters built-in prompts from exported visible generation inputs', () => {
|
||||
const metadata = buildLayerExportMetadata(
|
||||
buildLayer({
|
||||
model: 'gpt-image-2',
|
||||
generationInputs: {
|
||||
fields: [
|
||||
{ title: '生成提示词', value: 'AI 生成图片' },
|
||||
@@ -195,6 +196,7 @@ describe('ImageCanvasExportModel', () => {
|
||||
{ title: '用户输入', value: '用户填写的视觉要求' },
|
||||
{ title: '游戏分类', value: '游戏音效' },
|
||||
{ title: 'duration', value: '5秒' },
|
||||
{ title: '处理模型', value: 'birefnet' },
|
||||
],
|
||||
references: [],
|
||||
},
|
||||
@@ -210,6 +212,7 @@ describe('ImageCanvasExportModel', () => {
|
||||
],
|
||||
references: [],
|
||||
});
|
||||
expect(metadata.visible.model).toBe('gpt-image-2');
|
||||
});
|
||||
|
||||
it('builds a Spine JSON attachment timeline from image sequence frames', () => {
|
||||
|
||||
@@ -8,7 +8,8 @@ import type {
|
||||
import {
|
||||
DEFAULT_ICON_DESCRIPTIONS,
|
||||
formatLayerImageType,
|
||||
getEditorImageModelDisplayName,
|
||||
getEditorLayerModelDisplayName,
|
||||
isEditorUserVisibleGenerationInputField,
|
||||
UI_DESIGN_ASSET_EXTRACTION_PROMPT,
|
||||
} from './ImageCanvasGenerationModel';
|
||||
import { formatCanvasDurationMetric } from './ImageCanvasMediaModel';
|
||||
@@ -543,6 +544,7 @@ function isBuiltInGenerationInputField(field: CanvasGenerationInputField) {
|
||||
const builtInValues =
|
||||
BUILT_IN_GENERATION_INPUT_VALUES_BY_TITLE.get(normalizedTitle);
|
||||
return (
|
||||
!isEditorUserVisibleGenerationInputField(field) ||
|
||||
BUILT_IN_GENERATION_INPUT_TITLES.has(normalizedTitle) ||
|
||||
normalizedValue === UI_DESIGN_ASSET_EXTRACTION_PROMPT ||
|
||||
Boolean(builtInValues?.has(normalizedValue))
|
||||
@@ -572,7 +574,7 @@ export function buildLayerVisibleExportMetadata(layer: CanvasLayer) {
|
||||
const base = {
|
||||
type: formatLayerImageType(layer),
|
||||
generationInputs: buildVisibleGenerationInputs(layer),
|
||||
model: layer.model ? getEditorImageModelDisplayName(layer.model) : '-',
|
||||
model: getEditorLayerModelDisplayName(layer.model),
|
||||
task: formatTaskIdForDisplay(layer.taskId),
|
||||
object: layer.objectKey ?? layer.assetObjectId ?? '-',
|
||||
};
|
||||
|
||||
@@ -4,7 +4,10 @@ import type {
|
||||
EditorCharacterAnimationRatio,
|
||||
EditorCharacterAnimationResolution,
|
||||
} from '../../services/image-editor/editorProjectClient';
|
||||
import { isGeneratedLayer } from './ImageCanvasEditorModel';
|
||||
import {
|
||||
isEditorInternalProcessingModel,
|
||||
isGeneratedLayer,
|
||||
} from './ImageCanvasEditorModel';
|
||||
import type {
|
||||
CanvasGenerationDialogState,
|
||||
CanvasGenerationInputField,
|
||||
@@ -548,6 +551,21 @@ export function getEditorImageModelDisplayName(
|
||||
);
|
||||
}
|
||||
|
||||
export function isEditorUserVisibleGenerationInputField(
|
||||
field: CanvasGenerationInputField,
|
||||
) {
|
||||
return field.title.trim() !== '处理模型';
|
||||
}
|
||||
|
||||
export function getEditorLayerModelDisplayName(
|
||||
model: string | null | undefined,
|
||||
) {
|
||||
if (!model?.trim() || isEditorInternalProcessingModel(model)) {
|
||||
return '-';
|
||||
}
|
||||
return getEditorImageModelDisplayName(model);
|
||||
}
|
||||
|
||||
export function getEditorGenerationModelDisplayName(
|
||||
model: string | null | undefined,
|
||||
) {
|
||||
|
||||
@@ -58,6 +58,7 @@ describe('ImageCanvasMetadataModalView', () => {
|
||||
expect(within(dialog).getByText('参考图')).toBeTruthy();
|
||||
expect(within(dialog).getByText('角色立绘')).toBeTruthy();
|
||||
expect(within(dialog).getByText('项目资源 · resource-reference')).toBeTruthy();
|
||||
expect(within(dialog).getByText('Model')).toBeTruthy();
|
||||
expect(within(dialog).getByText('gpt-image-2')).toBeTruthy();
|
||||
expect(within(dialog).getByText('1024 x 768 px')).toBeTruthy();
|
||||
expect(within(dialog).queryByText('Provider')).toBeNull();
|
||||
@@ -66,6 +67,49 @@ describe('ImageCanvasMetadataModalView', () => {
|
||||
expect(within(dialog).getByText('123')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('keeps the generation model while hiding internal processing models', () => {
|
||||
const { rerender } = render(
|
||||
<ImageCanvasMetadataModalView
|
||||
layer={createLayer({
|
||||
model: 'gpt-image-2',
|
||||
generationInputs: {
|
||||
fields: [
|
||||
{ title: '处理阶段', value: '角色抠图' },
|
||||
{ title: '处理模型', value: 'birefnet' },
|
||||
],
|
||||
references: [],
|
||||
},
|
||||
})}
|
||||
onClose={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const dialog = screen.getByRole('dialog', { name: '图片信息' });
|
||||
|
||||
expect(within(dialog).getByText('处理阶段')).toBeTruthy();
|
||||
expect(within(dialog).getByText('角色抠图')).toBeTruthy();
|
||||
expect(within(dialog).queryByText('处理模型')).toBeNull();
|
||||
expect(within(dialog).getByText('Model')).toBeTruthy();
|
||||
expect(within(dialog).getByText('gpt-image-2')).toBeTruthy();
|
||||
expect(within(dialog).queryByText('birefnet')).toBeNull();
|
||||
|
||||
rerender(
|
||||
<ImageCanvasMetadataModalView
|
||||
layer={createLayer({
|
||||
model: 'birefnet',
|
||||
generationInputs: {
|
||||
fields: [{ title: '处理阶段', value: '角色抠图' }],
|
||||
references: [],
|
||||
},
|
||||
})}
|
||||
onClose={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(within(dialog).getByText('Model')).toBeTruthy();
|
||||
expect(within(dialog).queryByText('birefnet')).toBeNull();
|
||||
});
|
||||
|
||||
it('renders nanobanana2 instead of the raw Gemini image model id', () => {
|
||||
render(
|
||||
<ImageCanvasMetadataModalView
|
||||
|
||||
@@ -3,7 +3,8 @@ import type { CanvasLayer } from './ImageCanvasEditorTypes';
|
||||
import { formatTaskIdForDisplay } from './ImageCanvasExportModel';
|
||||
import {
|
||||
formatLayerImageType,
|
||||
getEditorImageModelDisplayName,
|
||||
getEditorLayerModelDisplayName,
|
||||
isEditorUserVisibleGenerationInputField,
|
||||
} from './ImageCanvasGenerationModel';
|
||||
import { formatCanvasDurationMetric } from './ImageCanvasMediaModel';
|
||||
|
||||
@@ -27,6 +28,11 @@ export function ImageCanvasMetadataModalView({
|
||||
layer,
|
||||
onClose,
|
||||
}: ImageCanvasMetadataModalViewProps) {
|
||||
const visibleGenerationInputFields =
|
||||
layer?.generationInputs?.fields.filter(
|
||||
isEditorUserVisibleGenerationInputField,
|
||||
) ?? [];
|
||||
const generationInputReferences = layer?.generationInputs?.references ?? [];
|
||||
const mediaLabel =
|
||||
layer?.mediaType === 'audio'
|
||||
? '音频'
|
||||
@@ -52,10 +58,10 @@ export function ImageCanvasMetadataModalView({
|
||||
<dd>{formatLayerImageType(layer)}</dd>
|
||||
<dt>生成输入</dt>
|
||||
<dd className="image-canvas-editor__metadata-inputs">
|
||||
{layer.generationInputs?.fields.length ||
|
||||
layer.generationInputs?.references.length ? (
|
||||
{visibleGenerationInputFields.length ||
|
||||
generationInputReferences.length ? (
|
||||
<>
|
||||
{layer.generationInputs.fields.map((field) => (
|
||||
{visibleGenerationInputFields.map((field) => (
|
||||
<div
|
||||
key={`${field.title}-${field.value}`}
|
||||
className="image-canvas-editor__metadata-input-field"
|
||||
@@ -66,9 +72,9 @@ export function ImageCanvasMetadataModalView({
|
||||
<span>{field.value}</span>
|
||||
</div>
|
||||
))}
|
||||
{layer.generationInputs.references.length ? (
|
||||
{generationInputReferences.length ? (
|
||||
<div className="image-canvas-editor__metadata-reference-list">
|
||||
{layer.generationInputs.references.map((reference) => (
|
||||
{generationInputReferences.map((reference) => (
|
||||
<div
|
||||
key={`${reference.title}-${reference.label}-${reference.refType}-${reference.refId}`}
|
||||
className="image-canvas-editor__metadata-reference-card"
|
||||
@@ -93,7 +99,7 @@ export function ImageCanvasMetadataModalView({
|
||||
)}
|
||||
</dd>
|
||||
<dt>Model</dt>
|
||||
<dd>{layer.model ? getEditorImageModelDisplayName(layer.model) : '-'}</dd>
|
||||
<dd>{getEditorLayerModelDisplayName(layer.model)}</dd>
|
||||
{layer.mediaType === 'audio' ? (
|
||||
<>
|
||||
<dt>时长</dt>
|
||||
|
||||
Reference in New Issue
Block a user