d44e1ae8ea
- 新增 ResourcePromptPolishSlot:素材侧两处入口共用的「AI 润色 / 恢复原文 / 润色中…」动作,取数走 AGC 侧 usePromptPolish,聊天框同款失败态(失败保留原文、可重试文案) - 场景约束走 polish_local_project_prompt 的 context:「这是素材生成提示词(音效)」+「只润色这一句、不要扩写成需求/方案/验收标准、不要新增事实」(本批不动后端提示词文件),文案落在 resourceCanvasPromptPolishModel - resourceEditModel 新增 truncateResourceEditPrompt:超上限先按类型截断(背景音乐 140 / 音效 1900 / 视频与角色动画 4000 / 其余 32000)再回填,并在状态行给出「已按长度上限截断」,不静默丢内容;后端上限未改 - resourceEditModel 新增 resolveResourceEditRequestIdentity:请求身份绑定到铸造时的 prompt(Rust request_fingerprint 含 prompt,改了提示词还复用同一 operationId 会被判「已绑定到不同资源编辑请求」);提示词没变时仍复用,失败重试照旧命中同一账本 - 生成素材弹窗用注入位渲染润色动作(共享 composer 只提供插槽,不上 Tauri 调用),提交时按同一口径解析身份;原先只在这一处使用的 createResourceCanvasGenerationRequest 随之下线 - 样式落在 resourceCanvasChrome.css(resource-prompt-polish 一族),弹窗与 composer 共用一行 - 测试:模型层补截断边界与身份复用/重铸;弹窗层补润色回填与上下文、超限截断提示、失败保留原文、失败后提示词与润色一起锁定且重试复用同一请求 - 变异验证:截断改成恒等 → 模型用例与「超限截断」用例双双红灯(实测字段长度 206 ≠ 140);身份去掉重铸 → 模型用例红灯
283 lines
8.4 KiB
TypeScript
283 lines
8.4 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import {
|
|
canonicalProjectedResourceMediaType,
|
|
resolveProjectCharacterAnimationCapability,
|
|
resolveProjectResourceEditCapability,
|
|
resolveResourceEditRequestIdentity,
|
|
RESOURCE_EDIT_PROMPT_TRUNCATED_NOTICE,
|
|
resourceEditPromptMaxLength,
|
|
truncateResourceEditPrompt,
|
|
} from '../src/view/project-development/resourceEditModel';
|
|
import type { ProjectResource } from '../src/view/project-development/resourceProjectionModel';
|
|
|
|
function resource(
|
|
input: Partial<ProjectResource> &
|
|
Pick<ProjectResource, 'id' | 'category' | 'path'>,
|
|
): ProjectResource {
|
|
return {
|
|
subtype: 'task-artifact',
|
|
label: input.path,
|
|
mediaType: '',
|
|
sourceLabel: '测试来源',
|
|
taskTitle: null,
|
|
manifestAssetId: null,
|
|
producerTaskId: 'task-1',
|
|
externalResourceId: null,
|
|
referenceResourceIds: [],
|
|
dependencies: [],
|
|
dependencyDepth: 0,
|
|
...input,
|
|
};
|
|
}
|
|
|
|
describe('全类型资源编辑能力模型', () => {
|
|
it('把现役资源稳定分流到图片派生、媒体派生、文本派生和版本分支', () => {
|
|
const cases: Array<{
|
|
resource: ProjectResource;
|
|
route: 'derive';
|
|
editKind?: string;
|
|
}> = [
|
|
{
|
|
resource: resource({
|
|
id: 'asset:png',
|
|
category: 'unclassified',
|
|
path: 'assets/hero.png',
|
|
mediaType: 'image/png',
|
|
manifestAssetId: 'png',
|
|
}),
|
|
route: 'derive',
|
|
editKind: 'image-reference',
|
|
},
|
|
{
|
|
resource: resource({
|
|
id: 'task:art:hero.webp',
|
|
category: 'unclassified',
|
|
path: 'hero.webp',
|
|
mediaType: '美术产物',
|
|
}),
|
|
route: 'derive',
|
|
editKind: 'image-reference',
|
|
},
|
|
{
|
|
resource: resource({
|
|
id: 'asset:gif',
|
|
category: 'unclassified',
|
|
path: 'assets/hero.gif',
|
|
mediaType: 'image/gif',
|
|
manifestAssetId: 'gif',
|
|
}),
|
|
route: 'derive',
|
|
editKind: 'image-reference',
|
|
},
|
|
{
|
|
resource: resource({
|
|
id: 'asset:svg',
|
|
category: 'unclassified',
|
|
path: 'assets/icon.svg',
|
|
mediaType: 'image/svg+xml',
|
|
manifestAssetId: 'svg',
|
|
}),
|
|
route: 'derive',
|
|
editKind: 'svg',
|
|
},
|
|
{
|
|
resource: resource({
|
|
id: 'task:art:intro.mp4',
|
|
category: 'unclassified',
|
|
path: 'assets/intro.mp4',
|
|
mediaType: '美术产物',
|
|
}),
|
|
route: 'derive',
|
|
editKind: 'video',
|
|
},
|
|
{
|
|
resource: resource({
|
|
id: 'asset:bgm',
|
|
category: 'audio',
|
|
path: 'audio/theme.ogg',
|
|
subtype: 'background-music',
|
|
mediaType: 'audio/ogg',
|
|
manifestAssetId: 'bgm',
|
|
}),
|
|
route: 'derive',
|
|
editKind: 'background-music',
|
|
},
|
|
{
|
|
resource: resource({
|
|
id: 'asset:sfx',
|
|
category: 'audio',
|
|
path: 'audio/hit.wav',
|
|
subtype: 'sound-effect',
|
|
mediaType: 'audio/wav',
|
|
manifestAssetId: 'sfx',
|
|
}),
|
|
route: 'derive',
|
|
editKind: 'sound-effect',
|
|
},
|
|
{
|
|
resource: resource({
|
|
id: 'task:code:main.ts',
|
|
category: 'document',
|
|
path: 'src/main.ts',
|
|
mediaType: '项目文档',
|
|
}),
|
|
route: 'derive',
|
|
editKind: 'text',
|
|
},
|
|
{
|
|
resource: resource({
|
|
id: 'agent-result:design:message-1',
|
|
category: 'document',
|
|
path: '专业 Agent 文本回执',
|
|
subtype: 'agent-result',
|
|
producerTaskId: null,
|
|
content: '回执',
|
|
}),
|
|
route: 'derive',
|
|
editKind: 'agent-result',
|
|
},
|
|
{
|
|
resource: resource({
|
|
id: 'version:v1',
|
|
category: 'version',
|
|
path: '项目版本 · v1',
|
|
subtype: 'project-version',
|
|
producerTaskId: null,
|
|
version: {
|
|
versionId: 'v1',
|
|
parentVersionId: null,
|
|
projectRevision: 1,
|
|
resourceBindings: [],
|
|
createdReason: 'initial',
|
|
createdAt: 1,
|
|
label: '版本 1',
|
|
childVersionIds: [],
|
|
},
|
|
}),
|
|
route: 'derive',
|
|
editKind: 'version',
|
|
},
|
|
];
|
|
|
|
for (const expected of cases) {
|
|
const capability = resolveProjectResourceEditCapability(
|
|
expected.resource,
|
|
);
|
|
expect(capability.route, expected.resource.id).toBe(expected.route);
|
|
expect(capability.editKind, expected.resource.id).toBe(expected.editKind);
|
|
}
|
|
});
|
|
|
|
it('用路径补齐任务产物缺失的标准媒体类型', () => {
|
|
expect(
|
|
canonicalProjectedResourceMediaType(
|
|
resource({
|
|
id: 'task:art:clip.mov',
|
|
category: 'unclassified',
|
|
path: 'assets/clip.mov',
|
|
mediaType: '美术产物',
|
|
}),
|
|
),
|
|
).toBe('video/quicktime');
|
|
expect(
|
|
canonicalProjectedResourceMediaType(
|
|
resource({
|
|
id: 'task:audio:effect.wav',
|
|
category: 'audio',
|
|
path: 'audio/effect.wav',
|
|
mediaType: '美术产物',
|
|
}),
|
|
),
|
|
).toBe('audio/wav');
|
|
expect(
|
|
canonicalProjectedResourceMediaType(
|
|
resource({
|
|
id: 'task:code:main.ts',
|
|
category: 'document',
|
|
path: 'src/main.ts',
|
|
mediaType: '项目文档',
|
|
}),
|
|
),
|
|
).toBe('text/typescript');
|
|
});
|
|
|
|
it('按现役媒体接口限制编辑提示词长度', () => {
|
|
expect(resourceEditPromptMaxLength('background-music')).toBe(140);
|
|
expect(resourceEditPromptMaxLength('sound-effect')).toBe(1_900);
|
|
expect(resourceEditPromptMaxLength('video')).toBe(4_000);
|
|
expect(resourceEditPromptMaxLength('character-animation')).toBe(4_000);
|
|
expect(resourceEditPromptMaxLength('text')).toBe(32_000);
|
|
});
|
|
|
|
it('超上限的润色结果按类型截断并给出替代文案,边界上不动内容', () => {
|
|
const exactlyLimit = '音'.repeat(140);
|
|
expect(
|
|
truncateResourceEditPrompt(exactlyLimit, 'background-music'),
|
|
).toEqual({
|
|
text: exactlyLimit,
|
|
notice: null,
|
|
});
|
|
const overLimit = `${exactlyLimit}多出来的一个字`;
|
|
expect(truncateResourceEditPrompt(overLimit, 'background-music')).toEqual({
|
|
text: exactlyLimit,
|
|
notice: RESOURCE_EDIT_PROMPT_TRUNCATED_NOTICE,
|
|
});
|
|
expect(RESOURCE_EDIT_PROMPT_TRUNCATED_NOTICE).toBe('已按长度上限截断');
|
|
// 上限按类型走,不是一刀切。
|
|
expect(
|
|
truncateResourceEditPrompt('字'.repeat(200), 'sound-effect').notice,
|
|
).toBeNull();
|
|
expect(
|
|
truncateResourceEditPrompt('字'.repeat(1_901), 'sound-effect').text
|
|
.length,
|
|
).toBe(1_900);
|
|
expect(
|
|
truncateResourceEditPrompt('字'.repeat(32_001), 'text').text.length,
|
|
).toBe(32_000);
|
|
});
|
|
|
|
it('请求身份只在提示词没变时复用', () => {
|
|
const first = resolveResourceEditRequestIdentity(null, '原始提示词');
|
|
expect(first.prompt).toBe('原始提示词');
|
|
expect(first.operationId).toBeTruthy();
|
|
expect(first.idempotencyKey).toBeTruthy();
|
|
|
|
// 同句重试:同一份身份,命中同一 operation 账本。
|
|
expect(resolveResourceEditRequestIdentity(first, '原始提示词')).toBe(first);
|
|
|
|
// 提示词变了(改了、或润色回填过):必须重铸,否则 Rust 的
|
|
// request_fingerprint(含 prompt)会把重试判成「已绑定到不同资源编辑请求」。
|
|
const second = resolveResourceEditRequestIdentity(first, '润色后的提示词');
|
|
expect(second).not.toBe(first);
|
|
expect(second.operationId).not.toBe(first.operationId);
|
|
expect(second.idempotencyKey).not.toBe(first.idempotencyKey);
|
|
expect(second.prompt).toBe('润色后的提示词');
|
|
});
|
|
|
|
it('图片资源可进入角色动画派生入口', () => {
|
|
const capability = resolveProjectCharacterAnimationCapability(
|
|
resource({
|
|
id: 'asset:hero',
|
|
category: 'unclassified',
|
|
path: 'assets/hero.png',
|
|
mediaType: 'image/png',
|
|
manifestAssetId: 'hero',
|
|
}),
|
|
);
|
|
expect(capability?.editKind).toBe('character-animation');
|
|
expect(capability?.generationMode).toBe('derive');
|
|
expect(
|
|
resolveProjectCharacterAnimationCapability(
|
|
resource({
|
|
id: 'asset:bgm',
|
|
category: 'audio',
|
|
path: 'audio/theme.mp3',
|
|
mediaType: 'audio/mpeg',
|
|
manifestAssetId: 'bgm',
|
|
}),
|
|
),
|
|
).toBeNull();
|
|
});
|
|
});
|