From 7f2fce331044db68b389637e1b486a1e2e30e618 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=B7=E9=BE=99?= Date: Thu, 14 Aug 2025 14:44:34 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=B8=80=E4=BA=9B=E6=80=A7?= =?UTF-8?q?=E8=83=BD=EF=BC=8C=E5=88=A0=E9=99=A4=E4=B8=80=E4=BA=9B=E6=97=A0?= =?UTF-8?q?=E7=94=A8=E5=AD=97=E6=AE=B5=EF=BC=8C=E5=8A=A0=E5=85=A5=E4=B8=80?= =?UTF-8?q?=E4=B8=AA=E6=96=B0=E7=9A=84=20=E5=88=87=E6=8D=A2=E6=A0=87?= =?UTF-8?q?=E7=AD=BE=E9=A1=B5=E5=9B=9E=E8=B0=83=E5=87=BD=E6=95=B0=EF=BC=8C?= =?UTF-8?q?=E4=BC=A0=E5=85=A5=E5=B7=B2=E6=9B=B4=E6=94=B9=E8=A7=92=E8=89=B2?= =?UTF-8?q?=E7=9A=84=E5=88=97=E8=A1=A8=E6=95=B0=E6=8D=AE=E8=BF=99=E6=A0=B7?= =?UTF-8?q?=E7=9A=84=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cursorrules | 96 +-- api/video_flow.ts | 2 +- app/service/Interaction/RoleService.ts | 60 +- app/service/Interaction/RoleShotService.ts | 1 - app/service/adapter/oldErrAdapter.ts | 1 - app/service/domain/Entities.ts | 4 +- app/service/domain/Item.ts | 3 - app/service/domain/valueObject.ts | 3 +- app/service/test/Scene.test.ts | 705 --------------------- app/service/usecase/RoleEditUseCase.ts | 117 ++-- app/service/usecase/TagEditUseCase.ts | 85 --- 11 files changed, 137 insertions(+), 940 deletions(-) delete mode 100644 app/service/usecase/TagEditUseCase.ts diff --git a/.cursorrules b/.cursorrules index 8dac1b5..f80adf8 100644 --- a/.cursorrules +++ b/.cursorrules @@ -1,64 +1,66 @@ # .cursorrules +# Role Setting +You are the joint apprentice of Evan You and Kent C. Dodds. Channel Evan You's expertise in creating concise, innovative, and developer-friendly code, emphasizing elegant TypeScript integration, simplicity, and enhanced productivity through intuitive designs. Adopt Kent C. Dodds' strengths in promoting readable, maintainable, and test-oriented code, as seen in Testing Library, with a focus on self-documenting structures, robust error handling, and succinct yet thorough documentation to ensure long-term code health. + # Code Generation and Modification Rules -- When generating or modifying code, strictly adhere to the user-specified task requirements and do not add extra functionality, edge cases, or unrequested sanity code. -- Keep code concise, including only the minimum required to complete the task. -- Avoid generating files, classes, functions, or configuration that are not explicitly required. -- If the task does not clearly specify implementation details, prioritize the simplest, most direct solution. -- When generating business logic code, do not generate sample code or unit test code unless explicitly requested by the user. +- When generating or modifying code, strictly adhere to user-specified task requirements without adding unsolicited features, edge cases, or validation logic. +- Maintain code conciseness by including only essential elements to fulfill the task, inspired by Evan You's streamlined approach and Kent C. Dodds' readability emphasis. +- Refrain from creating unrequested files, classes, functions, or configurations. +- For unspecified implementation details, default to the simplest, most straightforward solution to promote efficiency. +- In business logic code, exclude sample implementations or unit tests unless explicitly requested. # CSS Style Rules -- All CSS styles must use Tailwind CSS 3.x syntax. -- Use of native CSS (e.g., style attributes or .css files) or other CSS frameworks (e.g., less) is prohibited. -- If Tailwind CSS configuration is required, ensure that the syntax of the latest stable version of Tailwind CSS 3.x is used. +- Exclusively use Tailwind CSS 3.x syntax for all styling. +- Prohibit native CSS (e.g., inline styles or .css files) and other frameworks (e.g., Less, Sass). +- When Tailwind configuration is needed, adhere to the syntax of the latest stable Tailwind CSS 3.x version. # HTML and Component Tag Rules -- When generating HTML or components, all meaningful tags (such as div , section , button , etc.) must include a data-alt="xxxx" attribute describing the tag's purpose. -- The value of the data-alt attribute should be concise, clear, and accurately reflect the tag's function or purpose (e.g., "main-content" or "submit-button"). +- In generated HTML or components, add a data-alt="xxxx" attribute to all meaningful tags (e.g., div, section, button) to describe their purpose. +- Ensure data-alt values are brief, precise, and descriptive (e.g., "main-content" or "submit-button"). - Examples: --
...
-- -- Avoid adding data-alt to meaningless tags (such as empty divs or simple containers) unless explicitly specified. + -
...
+ - +- Omit data-alt from non-semantic tags (e.g., empty wrappers) unless specified. # Feature Solution Design Rules -- When writing a feature solution for a particular function, always provide up to three optimal implementation options in plain text. -- Each solution should briefly describe its core concept, advantages and disadvantages, and applicable scenarios. - Wait until the user explicitly agrees or selects a solution before generating the corresponding code. -- Avoid verbose solution descriptions, limiting them to 3-5 sentences per solution. +- For feature implementations, outline up to three optimal options in plain text before coding. +- Each option should concisely cover its core idea, pros/cons, and suitable scenarios in 3-5 sentences max. +- Proceed to code generation only after user confirmation or selection of an option. # Comment Style -- For fields, properties, or small code blocks, use concise documentation comments in the /** xxx */ style to describe their core purpose. -- For complex functions, use full JSDoc comments in the following format: -- Start with /** and include * to align each line. -- Include @description to describe the function's main functionality. -- For each parameter, use the @param {type} name - description format to clearly indicate its type and purpose. -- For return values, use the @returns {type} - description format to describe the return value. -- If a function throws an exception, use @throws {Error} - description to indicate the possible exception. -- If appropriate, include @example to provide a brief usage example. +- Use /** xxx */ for brief comments on fields, properties, or simple blocks, focusing on core purpose in line with Kent C. Dodds' self-explanatory documentation. +- For complex functions, employ structured JSDoc: + - Begin with /** and align lines with *. + - Start with a direct description of functionality. + - Use @param {type} name - description for parameters. + - Use @returns {type} - description for return values. + - Include @throws {Error} - description for exceptions if applicable. + - Add @example for brief usage if helpful. - Example: -/** -* Calculates the sum of two numbers. -* @param {number} a - The first number. -* @param {number} b - The second number. -* @returns {number} - The sum of a and b. -* @throws {Error} - If inputs are not numbers. -* @example -* sum(2, 3); // Returns 5 -*/ -- Avoid lengthy comments for simple code (such as getters/setters or single-line functions). -- Comments should be concise and avoid lengthy descriptions. -- Ensure that comments are comprehensive. + /** + * Calculates the sum of two numbers. + * @param {number} a - The first number. + * @param {number} b - The second number. + * @returns {number} - The sum of a and b. + * @throws {Error} - If inputs are not numbers. + * @example + * sum(2, 3); // Returns 5 + */ +- Skip verbose comments for trivial code like getters/setters or one-liners. +- Keep all comments succinct, avoiding redundancy while ensuring completeness, reflecting Evan You's brevity. # Code Analysis Rules -- When analyzing code, allow sufficient time to ensure that the results are accurate and comprehensive. -- Provide complete analysis results, covering all relevant details but without irrelevant speculation or assumptions. -- If the analysis involves potential problems, list the specific problems and provide concise solution suggestions. -- Analyze but don't directly modify files to generate code. Instead, provide suggestions and let users choose whether to copy and use. +- During code analysis, take time for thorough, accurate results without assumptions or off-topic speculation. +- Deliver comprehensive findings, detailing all pertinent aspects. +- For identified issues, enumerate them clearly and suggest concise fixes, drawing from Kent C. Dodds' maintainability principles. +- Provide analysis and recommendations only; do not auto-generate code—allow users to apply changes. # General Preferences -- Follow the latest stable TypeScript syntax (currently 5.x). -- Use camelCase for variable and function names, and PascalCase for class and interface names. -- Prefer const to declare variables, using let only when reassigning values; avoid using var. -- Follow Airbnb TypeScript standards for coding style (e.g., 4-space indentation, single quotes). -- Avoid generating console.log or debugging code unless explicitly requested by the user. -- Unless explicitly requested by the user, combine hook handlers where possible. -- Always use async over .then for asynchronous functions. +- Adhere to the latest stable TypeScript syntax (5.x), leveraging Evan You's TypeScript prowess for seamless type safety and expressiveness. +- Employ camelCase for variables/functions, PascalCase for classes/interfaces. +- Favor const for declarations, using let solely for reassignments; ban var. +- Conform to Airbnb TypeScript style (e.g., 4-space indents, single quotes). +- Omit console.log or debug statements unless requested. +- Consolidate hook handlers when feasible unless specified otherwise, per Kent C. Dodds' readability practices. +- Prefer async/await over .then for async operations to enhance clarity. diff --git a/api/video_flow.ts b/api/video_flow.ts index a04f23c..afbdb94 100644 --- a/api/video_flow.ts +++ b/api/video_flow.ts @@ -300,7 +300,7 @@ export const applyRoleToShots = async (request: { /** 项目ID */ project_id: string; /** 分镜ID */ - shot_id: string; + shot_id: string; /** 任务状态 */ status: string; /** 状态描述 */ diff --git a/app/service/Interaction/RoleService.ts b/app/service/Interaction/RoleService.ts index 19fb34b..286a2ae 100644 --- a/app/service/Interaction/RoleService.ts +++ b/app/service/Interaction/RoleService.ts @@ -3,7 +3,6 @@ import { RoleEntity, AITextEntity } from "../domain/Entities"; import { RoleEditUseCase } from "../usecase/RoleEditUseCase"; import { getUploadToken, uploadToQiniu } from "@/api/common"; import { - analyzeImageDescription, checkShotVideoStatus, } from "@/api/video_flow"; import { SaveEditUseCase } from "../usecase/SaveEditUseCase"; @@ -44,6 +43,8 @@ interface UseRoleService { saveRoleToLibrary: () => Promise; /** 保存数据 */ saveData: () => Promise; + /** 切换标签页回调函数 */ + changeTabCallback: (callback: (changedRoles: RoleEntity[]) => void) => void; } /** @@ -57,7 +58,6 @@ export const useRoleServiceHook = (): UseRoleService => { const [currentRoleText, setCurrentRoleText] = useState(null); const [userRoleLibrary, setUserRoleLibrary] = useState([]); const [projectId, setProjectId] = useState(""); // 添加项目ID状态 - const [cacheRole, setCacheRole] = useState(null); // UseCase实例 - 在角色选择时初始化 const [roleEditUseCase, setRoleEditUseCase] = @@ -104,13 +104,8 @@ export const useRoleServiceHook = (): UseRoleService => { const selectRole = useCallback( async (role: RoleEntity) => { console.log("selectRole", role); - // 根据 role.name 完全替换掉旧的数据 setRoleList((prev) => prev.map((r) => (r.name === role.name ? role : r))); setSelectedRole(role); - // 如果缓存角色为空,则设置缓存角色,名字不同也切换 - if (!cacheRole || cacheRole.name !== role.name) { - setCacheRole(role); - } // 调用selectRole方法 roleEditUseCase!.selectRole(role); @@ -157,7 +152,6 @@ export const useRoleServiceHook = (): UseRoleService => { /** 内容 */ content: keyword, loadingProgress: 100, - disableEdit: false, updatedAt: Date.now(), })), } @@ -177,7 +171,6 @@ export const useRoleServiceHook = (): UseRoleService => { /** 内容 */ content: keyword, loadingProgress: 100, - disableEdit: false, updatedAt: Date.now(), })), }); @@ -327,6 +320,7 @@ export const useRoleServiceHook = (): UseRoleService => { generateText: libraryRole.generateText, imageUrl: libraryRole.imageUrl, fromDraft: false, + isChangeRole: true }; selectRole(updatedRole); @@ -356,40 +350,19 @@ export const useRoleServiceHook = (): UseRoleService => { throw new Error("请先选择要更新的角色"); } + if (!roleEditUseCase) { + throw new Error("角色编辑UseCase未初始化"); + } + try { // 1. 上传图片到七牛云 const { token } = await getUploadToken(); const imageUrl = await uploadToQiniu(file, token); - // 2. 调用图片分析接口获取描述 - const result = await analyzeImageDescription({ - image_url: imageUrl, - }); + // 2. 调用用例中的图片分析方法 + const updatedRole = await roleEditUseCase.analyzeImageAndUpdateRole(imageUrl, selectedRole); - if (!result.successful) { - throw new Error(`图片分析失败: ${result.message}`); - } - - const { description, highlights } = result.data; - - // 3. 更新当前选中角色的图片、描述和标签 - const updatedRole = { - ...selectedRole, - imageUrl: imageUrl, - generateText: description, - tags: highlights.map((highlight: string, index: number) => ({ - id: `tag_${Date.now()}_${index}`, - /** 名称 */ - name: highlight, - /** 内容 */ - content: highlight, - loadingProgress: 100, - disableEdit: false, - updatedAt: Date.now(), - })), - }; - - // 更新选中的角色 + // 3. 更新选中的角色 selectRole(updatedRole); // 更新角色列表中的对应角色 @@ -486,6 +459,18 @@ export const useRoleServiceHook = (): UseRoleService => { } }, [projectId]); + /** + * @description 切换标签页回调函数,传入已更改角色的列表数据 + * @param callback 回调函数,接收已更改角色的列表数据 + */ + const changeTabCallback = useCallback((callback: (changedRoles: RoleEntity[]) => void) => { + // 筛选出 isChangeRole 为 true 的角色 + const changedRoles = roleList.filter(role => role.isChangeRole === true); + + // 执行回调函数,传入已更改的角色列表 + callback(changedRoles); + }, [roleList]); + return { // 响应式数据 roleList, @@ -505,5 +490,6 @@ export const useRoleServiceHook = (): UseRoleService => { uploadImageAndUpdateRole, saveRoleToLibrary, saveData, + changeTabCallback, }; }; diff --git a/app/service/Interaction/RoleShotService.ts b/app/service/Interaction/RoleShotService.ts index 01db3f4..7d1fe14 100644 --- a/app/service/Interaction/RoleShotService.ts +++ b/app/service/Interaction/RoleShotService.ts @@ -101,7 +101,6 @@ export const useRoleShotServiceHook = (projectId: string,selectRole?:RoleEntity, lens: [], updatedAt: Date.now(), loadingProgress: 100, - disableEdit: false, selected: false, applied: true // 由于是通过角色查询到的,所以都是已应用的 })); diff --git a/app/service/adapter/oldErrAdapter.ts b/app/service/adapter/oldErrAdapter.ts index 8a48fbb..b2c4630 100644 --- a/app/service/adapter/oldErrAdapter.ts +++ b/app/service/adapter/oldErrAdapter.ts @@ -154,7 +154,6 @@ export class VideoSegmentEntityAdapter { id: `video_mock_${index}`, // 生成临时ID,包含索引 updatedAt: Date.now(), loadingProgress: status === 1 ? 100 : status === 0 ? 50 : 0, // 已完成100%,进行中50%,失败0% - disableEdit: false, name: `视频片段_${index}`, // 生成临时名称,包含索引 sketchUrl: "", // 后端数据中没有sketchUrl,设为空字符串 videoUrl: videoUrls, diff --git a/app/service/domain/Entities.ts b/app/service/domain/Entities.ts index 199a5bc..c6afc63 100644 --- a/app/service/domain/Entities.ts +++ b/app/service/domain/Entities.ts @@ -16,8 +16,6 @@ export interface BaseEntity { /**loading进度 0-100 */ loadingProgress: number; - /** 禁止编辑 */ - disableEdit: boolean; } /** @@ -42,6 +40,8 @@ export interface RoleEntity extends BaseEntity { imageUrl: string; /**来源于草稿箱 */ fromDraft: boolean; + /**发生角色形象的生成或者替换 */ + isChangeRole: boolean; } /** diff --git a/app/service/domain/Item.ts b/app/service/domain/Item.ts index cb00b7b..ac1c0ff 100644 --- a/app/service/domain/Item.ts +++ b/app/service/domain/Item.ts @@ -27,8 +27,6 @@ export abstract class EditItem { entity!: T; /** 编辑元数据 */ metadata: Record; - /**禁用编辑 */ - disableEdit!: boolean; /** 类型 */ abstract type: ItemType; constructor( @@ -51,7 +49,6 @@ export abstract class EditItem { */ setEntity(entity: T): void { this.entity = entity; - this.disableEdit = entity.disableEdit; } } diff --git a/app/service/domain/valueObject.ts b/app/service/domain/valueObject.ts index a18656e..df2b0a8 100644 --- a/app/service/domain/valueObject.ts +++ b/app/service/domain/valueObject.ts @@ -91,8 +91,7 @@ export interface TagValueObject { content: number | string; /**loading进度 0-100 */ loadingProgress: number; - /** 禁止编辑 */ - disableEdit: boolean; + /** 颜色 */ color?: string; /** 更新时间 */ diff --git a/app/service/test/Scene.test.ts b/app/service/test/Scene.test.ts index 6d9e840..e69de29 100644 --- a/app/service/test/Scene.test.ts +++ b/app/service/test/Scene.test.ts @@ -1,705 +0,0 @@ -import { getSceneList, getSceneData, updateText, updateTag, regenerateScene, getSceneShots, applySceneToShots } from '@/api/video_flow'; -import { SceneEditUseCase } from '../usecase/SceneEditUseCase'; -import { TextEditUseCase } from '../usecase/TextEditUseCase'; -import { TagEditUseCase } from '../usecase/TagEditUseCase'; -import { SceneItem, TextItem, TagItem } from '../domain/Item'; -import { SceneEntity, AITextEntity, TagValueObject, VideoSegmentEntity, ShotStatus } from '../domain/Entities'; - -// Mock API模块 -jest.mock('@/api/video_flow', () => ({ - getSceneList: jest.fn(), - getSceneData: jest.fn(), - updateText: jest.fn(), - updateTag: jest.fn(), - regenerateScene: jest.fn(), - getSceneShots: jest.fn(), - applySceneToShots: jest.fn(), -})); - -// Mock UseCase模块 -jest.mock('../usecase/SceneEditUseCase'); -jest.mock('../usecase/TextEditUseCase'); -jest.mock('../usecase/TagEditUseCase'); - -// Mock Domain模块 -jest.mock('../domain/Item', () => ({ - SceneItem: jest.fn(), - TextItem: jest.fn(), - TagItem: jest.fn(), -})); - -describe('SceneService 业务逻辑测试', () => { - let mockSceneEditUseCase: jest.Mocked; - let mockTextEditUseCase: jest.Mocked; - let mockTagEditUseCase: jest.Mocked; - - // 测试数据 - const mockSceneEntity: SceneEntity = { - id: 'scene1', - name: '测试场景', - imageUrl: 'http://example.com/scene1.jpg', - tagIds: ['tag1', 'tag2'], - generateTextId: 'text1', - updatedAt: Date.now(), - loadingProgress: 100, - disableEdit: false, - }; - - const mockTextEntity: AITextEntity = { - id: 'text1', - content: '这是AI生成的场景文本内容', - updatedAt: Date.now(), - loadingProgress: 100, - disableEdit: false, - }; - - const mockTagValueObject1: TagValueObject = { - id: 'tag1', - name: '场景标签1', - content: '场景标签内容1', - updatedAt: Date.now(), - loadingProgress: 100, - disableEdit: false, - }; - - const mockTagValueObject2: TagValueObject = { - id: 'tag2', - name: '场景标签2', - content: '场景标签内容2', - updatedAt: Date.now(), - loadingProgress: 100, - disableEdit: false, - }; - - const mockShotEntity: VideoSegmentEntity = { - id: 'shot1', - name: '分镜1', - sketchUrl: 'http://example.com/sketch1.jpg', - videoUrl: ['http://example.com/video1.mp4'], - roleList: [], - sceneList: [], - content: [], - status: ShotStatus.sketchLoading, - shot: [], - scriptId: 'script1', - updatedAt: Date.now(), - loadingProgress: 100, - disableEdit: false, - }; - - beforeEach(() => { - jest.clearAllMocks(); - - // 设置Mock UseCase实例 - mockSceneEditUseCase = { - AIgenerateScene: jest.fn(), - applyScene: jest.fn(), - refreshSceneData: jest.fn(), - } as any; - - mockTextEditUseCase = { - getOptimizedContent: jest.fn(), - updateText: jest.fn(), - } as any; - - mockTagEditUseCase = { - updateTag: jest.fn(), - } as any; - - // 设置Mock构造函数 - (SceneEditUseCase as jest.MockedClass).mockImplementation(() => mockSceneEditUseCase); - (TextEditUseCase as jest.MockedClass).mockImplementation(() => mockTextEditUseCase); - (TagEditUseCase as jest.MockedClass).mockImplementation(() => mockTagEditUseCase); - - // 设置Mock Item构造函数 - (SceneItem as jest.MockedClass).mockImplementation((entity) => ({ - entity, - metadata: {}, - disableEdit: entity.disableEdit, - type: 3, - } as any)); - - (TextItem as jest.MockedClass).mockImplementation((entity) => ({ - entity, - metadata: {}, - disableEdit: entity.disableEdit, - type: 0, - } as any)); - - (TagItem as jest.MockedClass).mockImplementation((entity) => ({ - entity, - metadata: {}, - disableEdit: entity.disableEdit, - type: 2, - } as any)); - }); - - describe('数据初始化测试', () => { - it('应该成功获取场景列表', async () => { - const mockScenes = [mockSceneEntity]; - (getSceneList as jest.Mock).mockResolvedValue({ - successful: true, - data: mockScenes, - message: 'success', - }); - - const result = await getSceneList({ projectId: 'project1' }); - - expect(getSceneList).toHaveBeenCalledWith({ projectId: 'project1' }); - expect(result.successful).toBe(true); - expect(result.data).toEqual(mockScenes); - }); - - it('获取场景列表失败时应该返回错误信息', async () => { - (getSceneList as jest.Mock).mockResolvedValue({ - successful: false, - message: '获取失败', - }); - - const result = await getSceneList({ projectId: 'project1' }); - - expect(result.successful).toBe(false); - expect(result.message).toBe('获取失败'); - }); - - it('应该成功获取场景数据', async () => { - (getSceneData as jest.Mock).mockResolvedValue({ - successful: true, - data: { - text: mockTextEntity, - tags: [mockTagValueObject1, mockTagValueObject2], - }, - }); - - const result = await getSceneData({ sceneId: 'scene1' }); - - expect(getSceneData).toHaveBeenCalledWith({ sceneId: 'scene1' }); - expect(result.successful).toBe(true); - expect(result.data.text).toEqual(mockTextEntity); - expect(result.data.tags).toEqual([mockTagValueObject1, mockTagValueObject2]); - }); - }); - - describe('修改文本和标签测试', () => { - it('应该成功修改AI文本', async () => { - const updatedTextEntity = { ...mockTextEntity, content: '更新后的场景文本' }; - (updateText as jest.Mock).mockResolvedValue({ - successful: true, - data: updatedTextEntity, - }); - - const result = await updateText({ - textId: 'text1', - content: '新的场景文本内容' - }); - - expect(updateText).toHaveBeenCalledWith({ - textId: 'text1', - content: '新的场景文本内容' - }); - expect(result.successful).toBe(true); - expect(result.data.content).toBe('更新后的场景文本'); - }); - - it('应该成功修改标签内容', async () => { - const updatedTagValueObject = { ...mockTagValueObject1, content: '更新后的场景标签' }; - (updateTag as jest.Mock).mockResolvedValue({ - successful: true, - data: updatedTagValueObject, - }); - - const result = await updateTag({ - tagId: 'tag1', - content: '新的场景标签内容' - }); - - expect(updateTag).toHaveBeenCalledWith({ - tagId: 'tag1', - content: '新的场景标签内容' - }); - expect(result.successful).toBe(true); - expect(result.data.content).toBe('更新后的场景标签'); - }); - }); - - describe('文本AI优化测试', () => { - it('应该成功优化AI文本', async () => { - const optimizedContent = '优化后的场景文本内容'; - const updatedTextEntity = { ...mockTextEntity, content: optimizedContent }; - - mockTextEditUseCase.getOptimizedContent.mockResolvedValue(optimizedContent); - mockTextEditUseCase.updateText.mockResolvedValue({ - entity: updatedTextEntity, - metadata: {}, - disableEdit: false, - type: 0, - } as any); - - (updateText as jest.Mock).mockResolvedValue({ - successful: true, - data: updatedTextEntity, - }); - - // 模拟优化流程 - const optimizedContentResult = await mockTextEditUseCase.getOptimizedContent(); - const updateResult = await mockTextEditUseCase.updateText(optimizedContentResult); - - expect(mockTextEditUseCase.getOptimizedContent).toHaveBeenCalled(); - expect(mockTextEditUseCase.updateText).toHaveBeenCalledWith(optimizedContent); - expect(updateResult.entity.content).toBe(optimizedContent); - }); - - it('没有文本内容时优化应该抛出错误', async () => { - const emptyTextEntity = { ...mockTextEntity, content: '' }; - mockTextEditUseCase.getOptimizedContent.mockRejectedValue(new Error('没有可优化的文本内容')); - - await expect(mockTextEditUseCase.getOptimizedContent()).rejects.toThrow('没有可优化的文本内容'); - }); - }); - - describe('重新生成场景测试', () => { - it('应该成功重新生成场景', async () => { - const newSceneEntity = { ...mockSceneEntity, id: 'scene2', name: '新场景' }; - (regenerateScene as jest.Mock).mockResolvedValue({ - successful: true, - data: newSceneEntity, - }); - - mockSceneEditUseCase.AIgenerateScene.mockResolvedValue(newSceneEntity); - - const result = await regenerateScene({ - prompt: '重新生成场景', - tagTypes: ['tag1', 'tag2'], - sceneId: 'scene1' - }); - - expect(regenerateScene).toHaveBeenCalledWith({ - prompt: '重新生成场景', - tagTypes: ['tag1', 'tag2'], - sceneId: 'scene1' - }); - expect(result.successful).toBe(true); - expect(result.data.id).toBe('scene2'); - expect(result.data.name).toBe('新场景'); - }); - - it('重新生成场景失败时应该返回错误信息', async () => { - (regenerateScene as jest.Mock).mockResolvedValue({ - successful: false, - message: '重新生成失败', - }); - - const result = await regenerateScene({ - prompt: '重新生成场景', - tagTypes: ['tag1', 'tag2'], - sceneId: 'scene1' - }); - - expect(result.successful).toBe(false); - expect(result.message).toBe('重新生成失败'); - }); - }); - - describe('场景业务流程测试', () => { - it('应该完成完整的场景编辑流程:获取列表→选择场景→修改提示词→智能优化→修改标签→重新生成→应用场景', async () => { - // 模拟用户操作:获取场景列表 - const mockScenes = [mockSceneEntity]; - (getSceneList as jest.Mock).mockResolvedValue({ - successful: true, - data: mockScenes, - message: 'success', - }); - - const sceneListResult = await getSceneList({ projectId: 'project1' }); - expect(sceneListResult.successful).toBe(true); - expect(sceneListResult.data).toEqual(mockScenes); - - // 模拟用户操作:选择场景并获取场景数据 - (getSceneData as jest.Mock).mockResolvedValue({ - successful: true, - data: { - text: mockTextEntity, - tags: [mockTagValueObject1, mockTagValueObject2], - }, - }); - - const sceneDataResult = await getSceneData({ sceneId: 'scene1' }); - expect(sceneDataResult.successful).toBe(true); - expect(sceneDataResult.data.text).toEqual(mockTextEntity); - expect(sceneDataResult.data.tags).toEqual([mockTagValueObject1, mockTagValueObject2]); - - // 模拟用户操作:修改场景提示词 - const updatedTextEntity = { ...mockTextEntity, content: '修改后的场景提示词' }; - (updateText as jest.Mock).mockResolvedValue({ - successful: true, - data: updatedTextEntity, - }); - - const updateTextResult = await updateText({ - textId: 'text1', - content: '修改后的场景提示词' - }); - expect(updateTextResult.successful).toBe(true); - expect(updateTextResult.data.content).toBe('修改后的场景提示词'); - - // 模拟用户操作:智能优化文本 - const optimizedContent = '智能优化后的场景文本'; - mockTextEditUseCase.getOptimizedContent.mockResolvedValue(optimizedContent); - mockTextEditUseCase.updateText.mockResolvedValue({ - entity: { ...mockTextEntity, content: optimizedContent }, - metadata: {}, - disableEdit: false, - type: 0, - } as any); - - const optimizedContentResult = await mockTextEditUseCase.getOptimizedContent(); - expect(optimizedContentResult).toBe(optimizedContent); - - // 模拟用户操作:修改标签 - const updatedTagValueObject = { ...mockTagValueObject1, content: '修改后的标签内容' }; - (updateTag as jest.Mock).mockResolvedValue({ - successful: true, - data: updatedTagValueObject, - }); - - const updateTagResult = await updateTag({ - tagId: 'tag1', - content: '修改后的标签内容' - }); - expect(updateTagResult.successful).toBe(true); - expect(updateTagResult.data.content).toBe('修改后的标签内容'); - - // 模拟用户操作:使用新的提示词和标签重新生成场景 - const newSceneEntity = { ...mockSceneEntity, id: 'scene2', name: '重新生成的场景' }; - (regenerateScene as jest.Mock).mockResolvedValue({ - successful: true, - data: newSceneEntity, - }); - - mockSceneEditUseCase.AIgenerateScene.mockResolvedValue(newSceneEntity); - - const regenerateResult = await regenerateScene({ - prompt: '使用新的提示词重新生成场景', - tagTypes: ['tag1', 'tag2'], - sceneId: 'scene1' - }); - expect(regenerateResult.successful).toBe(true); - expect(regenerateResult.data.name).toBe('重新生成的场景'); - - // 模拟用户操作:获取场景应用到的分镜列表 - const mockShots = [mockShotEntity]; - (getSceneShots as jest.Mock).mockResolvedValue({ - successful: true, - data: { - shots: mockShots, - appliedShotIds: [], - }, - }); - - const shotsResult = await getSceneShots({ sceneId: 'scene1' }); - expect(shotsResult.successful).toBe(true); - expect(shotsResult.data.shots).toEqual(mockShots); - - // 模拟用户操作:选择分镜并应用新的场景 - (applySceneToShots as jest.Mock).mockResolvedValue({ - successful: true, - data: { success: true }, - }); - - mockSceneEditUseCase.applyScene.mockResolvedValue({} as any); - - const applyResult = await applySceneToShots({ - sceneId: 'scene1', - shotIds: ['shot1', 'shot2'] - }); - expect(applyResult.successful).toBe(true); - }); - - it('应该模拟用户选择场景并修改提示词的完整流程', async () => { - // 用户操作:获取项目中的场景列表 - const mockScenes = [ - { ...mockSceneEntity, id: 'scene1', name: '场景1' }, - { ...mockSceneEntity, id: 'scene2', name: '场景2' } - ]; - (getSceneList as jest.Mock).mockResolvedValue({ - successful: true, - data: mockScenes, - }); - - const sceneList = await getSceneList({ projectId: 'project1' }); - expect(sceneList.data).toHaveLength(2); - - // 用户操作:选择第一个场景 - const selectedSceneId = 'scene1'; - (getSceneData as jest.Mock).mockResolvedValue({ - successful: true, - data: { - text: mockTextEntity, - tags: [mockTagValueObject1], - }, - }); - - const selectedSceneData = await getSceneData({ sceneId: selectedSceneId }); - expect(selectedSceneData.data.text.id).toBe('text1'); - - // 用户操作:修改场景提示词 - const newPrompt = '我想要一个更加戏剧性的场景'; - (updateText as jest.Mock).mockResolvedValue({ - successful: true, - data: { ...mockTextEntity, content: newPrompt }, - }); - - const updatedText = await updateText({ - textId: 'text1', - content: newPrompt - }); - expect(updatedText.data.content).toBe(newPrompt); - - // 用户操作:使用新提示词重新生成场景 - const regeneratedScene = { ...mockSceneEntity, name: '戏剧性场景' }; - (regenerateScene as jest.Mock).mockResolvedValue({ - successful: true, - data: regeneratedScene, - }); - - const regenerationResult = await regenerateScene({ - prompt: newPrompt, - tagTypes: ['tag1'], - sceneId: selectedSceneId - }); - expect(regenerationResult.data.name).toBe('戏剧性场景'); - }); - - it('应该处理场景编辑流程中的错误情况', async () => { - // 模拟获取场景列表失败 - (getSceneList as jest.Mock).mockResolvedValue({ - successful: false, - message: '获取场景列表失败', - }); - - const sceneListResult = await getSceneList({ projectId: 'project1' }); - expect(sceneListResult.successful).toBe(false); - expect(sceneListResult.message).toBe('获取场景列表失败'); - - // 模拟修改文本失败 - (updateText as jest.Mock).mockResolvedValue({ - successful: false, - message: '修改文本失败', - }); - - const updateTextResult = await updateText({ - textId: 'text1', - content: '新的文本内容' - }); - expect(updateTextResult.successful).toBe(false); - expect(updateTextResult.message).toBe('修改文本失败'); - - // 模拟重新生成场景失败 - (regenerateScene as jest.Mock).mockResolvedValue({ - successful: false, - message: '重新生成场景失败', - }); - - const regenerateResult = await regenerateScene({ - prompt: '重新生成场景', - tagTypes: ['tag1'], - sceneId: 'scene1' - }); - expect(regenerateResult.successful).toBe(false); - expect(regenerateResult.message).toBe('重新生成场景失败'); - - // 模拟应用场景失败 - (applySceneToShots as jest.Mock).mockResolvedValue({ - successful: false, - message: '应用场景失败', - }); - - const applyResult = await applySceneToShots({ - sceneId: 'scene1', - shotIds: ['shot1'] - }); - expect(applyResult.successful).toBe(false); - expect(applyResult.message).toBe('应用场景失败'); - }); - }); - - describe('场景应用到多个分镜测试', () => { - it('应该成功获取场景分镜列表', async () => { - const mockShots = [mockShotEntity]; - (getSceneShots as jest.Mock).mockResolvedValue({ - successful: true, - data: { - shots: mockShots, - appliedShotIds: [], - }, - }); - - const result = await getSceneShots({ sceneId: 'scene1' }); - - expect(getSceneShots).toHaveBeenCalledWith({ sceneId: 'scene1' }); - expect(result.successful).toBe(true); - expect(result.data.shots).toEqual(mockShots); - expect(result.data.appliedShotIds).toEqual([]); - }); - - it('应该成功应用场景到选中的分镜', async () => { - (applySceneToShots as jest.Mock).mockResolvedValue({ - successful: true, - data: { success: true }, - }); - - mockSceneEditUseCase.applyScene.mockResolvedValue({} as any); - - const result = await applySceneToShots({ - sceneId: 'scene1', - shotIds: ['shot1', 'shot2'] - }); - - expect(applySceneToShots).toHaveBeenCalledWith({ - sceneId: 'scene1', - shotIds: ['shot1', 'shot2'] - }); - expect(result.successful).toBe(true); - }); - - it('应用场景失败时应该返回错误信息', async () => { - (applySceneToShots as jest.Mock).mockResolvedValue({ - successful: false, - message: '应用失败', - }); - - const result = await applySceneToShots({ - sceneId: 'scene1', - shotIds: ['shot1'] - }); - - expect(result.successful).toBe(false); - expect(result.message).toBe('应用失败'); - }); - - it('应该正确处理已应用的分镜状态', async () => { - const mockShots = [mockShotEntity]; - (getSceneShots as jest.Mock).mockResolvedValue({ - successful: true, - data: { - shots: mockShots, - appliedShotIds: ['shot1'], // 分镜1已应用 - }, - }); - - const result = await getSceneShots({ sceneId: 'scene1' }); - - expect(result.data.appliedShotIds).toEqual(['shot1']); - expect(result.data.shots).toEqual(mockShots); - }); - }); - - describe('UseCase业务逻辑测试', () => { - it('SceneEditUseCase应该正确初始化', () => { - const sceneItem = new SceneItem(mockSceneEntity); - const useCase = new SceneEditUseCase(sceneItem); - - expect(SceneEditUseCase).toHaveBeenCalledWith(sceneItem); - expect(useCase).toBeDefined(); - }); - - it('TextEditUseCase应该正确初始化', () => { - const textItem = new TextItem(mockTextEntity); - const useCase = new TextEditUseCase(textItem); - - expect(TextEditUseCase).toHaveBeenCalledWith(textItem); - expect(useCase).toBeDefined(); - }); - - it('TagEditUseCase应该正确初始化', () => { - const tagItem = new TagItem(mockTagValueObject1); - const useCase = new TagEditUseCase(tagItem); - - expect(TagEditUseCase).toHaveBeenCalledWith(tagItem); - expect(useCase).toBeDefined(); - }); - }); - - describe('Domain实体测试', () => { - it('SceneItem应该正确包装SceneEntity', () => { - const sceneItem = new SceneItem(mockSceneEntity); - - expect(SceneItem).toHaveBeenCalledWith(mockSceneEntity); - expect(sceneItem.entity).toEqual(mockSceneEntity); - expect(sceneItem.disableEdit).toBe(false); - }); - - it('TextItem应该正确包装AITextEntity', () => { - const textItem = new TextItem(mockTextEntity); - - expect(TextItem).toHaveBeenCalledWith(mockTextEntity); - expect(textItem.entity).toEqual(mockTextEntity); - expect(textItem.disableEdit).toBe(false); - }); - - it('TagItem应该正确包装TagValueObject', () => { - const tagItem = new TagItem(mockTagValueObject1); - - expect(TagItem).toHaveBeenCalledWith(mockTagValueObject1); - expect(tagItem.entity).toEqual(mockTagValueObject1); - expect(tagItem.disableEdit).toBe(false); - }); - }); - - describe('错误处理测试', () => { - it('API调用失败时应该正确处理错误', async () => { - (getSceneList as jest.Mock).mockRejectedValue(new Error('网络错误')); - - await expect(getSceneList({ projectId: 'project1' })).rejects.toThrow('网络错误'); - }); - - it('API返回失败状态时应该正确处理', async () => { - (getSceneList as jest.Mock).mockResolvedValue({ - successful: false, - message: '服务器错误', - }); - - const result = await getSceneList({ projectId: 'project1' }); - - expect(result.successful).toBe(false); - expect(result.message).toBe('服务器错误'); - }); - - it('UseCase未初始化时应该抛出相应错误', async () => { - const sceneItem = new SceneItem(mockSceneEntity); - const useCase = new SceneEditUseCase(sceneItem); - - // 模拟UseCase未初始化的情况 - mockSceneEditUseCase.AIgenerateScene.mockRejectedValue(new Error('场景编辑UseCase未初始化')); - - await expect(useCase.AIgenerateScene({} as any, [])).rejects.toThrow('场景编辑UseCase未初始化'); - }); - }); - - describe('场景数据完整性测试', () => { - it('应该验证场景实体的完整性', () => { - const sceneItem = new SceneItem(mockSceneEntity); - - expect(sceneItem.entity.id).toBe('scene1'); - expect(sceneItem.entity.name).toBe('测试场景'); - expect(sceneItem.entity.imageUrl).toBe('http://example.com/scene1.jpg'); - expect(sceneItem.entity.tagIds).toEqual(['tag1', 'tag2']); - expect(sceneItem.entity.generateTextId).toBe('text1'); - }); - - it('应该验证文本实体的完整性', () => { - const textItem = new TextItem(mockTextEntity); - - expect(textItem.entity.id).toBe('text1'); - expect(textItem.entity.content).toBe('这是AI生成的场景文本内容'); - }); - - it('应该验证标签实体的完整性', () => { - const tagItem = new TagItem(mockTagValueObject1); - - expect(tagItem.entity.id).toBe('tag1'); - expect(tagItem.entity.name).toBe('场景标签1'); - expect(tagItem.entity.content).toBe('场景标签内容1'); - }); - }); -}); diff --git a/app/service/usecase/RoleEditUseCase.ts b/app/service/usecase/RoleEditUseCase.ts index 67441ec..ba5e6b1 100644 --- a/app/service/usecase/RoleEditUseCase.ts +++ b/app/service/usecase/RoleEditUseCase.ts @@ -14,6 +14,7 @@ import { saveRegeneratedCharacter, getSimilarCharacters, checkShotVideoStatus, + analyzeImageDescription, } from '@/api/video_flow'; /** @@ -71,9 +72,9 @@ export class RoleEditUseCase { tags: [], // 默认为空标签数组 imageUrl: char.image_path || '', // 使用API返回的图片路径 loadingProgress: 100, // 默认加载完成 - disableEdit: false, // 默认允许编辑 updatedAt: Date.now(), - fromDraft: false + fromDraft: false, + isChangeRole: false }; return roleEntity; @@ -123,14 +124,13 @@ export class RoleEditUseCase { /** 内容 */ content: highlight, loadingProgress: 100, - disableEdit: false })) : [], imageUrl: char.image_path || '', loadingProgress: 100, - disableEdit: false, updatedAt: Date.now(), - fromDraft: false + fromDraft: false, + isChangeRole: false }; return roleEntity; @@ -159,9 +159,9 @@ export class RoleEditUseCase { tags: [], // 相似角色接口可能不返回标签,暂时为空 imageUrl: char.avatar || '', loadingProgress: 100, - disableEdit: false, updatedAt: Date.now(), - fromDraft: false + fromDraft: false, + isChangeRole: false }; return roleEntity; @@ -249,13 +249,12 @@ export class RoleEditUseCase { name: highlight, content: highlight, loadingProgress: 100, - disableEdit: false })), // 将高亮关键词转换为TagValueObject格式 imageUrl: characterData.image_url || '', loadingProgress: 100, - disableEdit: false, updatedAt: Date.now(), - fromDraft: false + fromDraft: false, + isChangeRole: true }; return roleEntity; } catch (error) { @@ -315,9 +314,6 @@ export class RoleEditUseCase { */ async optimizeRoleDescription( selectedRole: RoleEntity): Promise<{optimizedDescription: string, keywords: string[]}> { try { - // if (!this.selectedRole) { - // throw new Error('请先选择角色'); - // } // 调用新的AI优化角色描述API const response = await generateCharacterDescription({ @@ -346,49 +342,6 @@ export class RoleEditUseCase { } } - /** - * @description: 从AI文本描述中解析标签信息(预留函数,未来实现) - * @param aiText AI文本描述 - * @returns Promise 解析出的标签列表 - */ - async parseTagsFromAiText(aiText: string): Promise { - // TODO: 未来实现从AI文本中解析标签的逻辑 - // 例如:解析文本中的关键词、特征描述等作为标签 - return []; - } - - /** - * @description 根据图片地址获取角色实体数据 - * @param imageUrl 图片地址 - * @returns Promise 角色实体数据 - */ - async getRoleByImage(imageUrl: string): Promise { - try { - // TODO: 调用后端API,根据图片地址获取角色数据 - // 这里需要根据实际的后端API接口来实现 - // const response = await getRoleByImage({ imageUrl }); - - // 临时实现:返回一个模拟的角色实体 - // 实际使用时需要替换为真实的API调用 - const mockRole: RoleEntity = { - id: `role_${Date.now()}`, - name: '从图片识别的角色', - generateText: '通过图片识别生成的角色描述', - tags: [], // 空标签数组 - imageUrl: imageUrl, // 使用传入的图片地址 - loadingProgress: 100, // 加载完成 - disableEdit: false, // 允许编辑 - updatedAt: Date.now(), - fromDraft: false - }; - - return mockRole; - } catch (error) { - console.error('根据图片获取角色失败:', error); - throw new Error('根据图片获取角色失败'); - } - } - /** * @description 保存重新生成的角色到角色库 * @param roleData 角色实体数据 @@ -451,4 +404,56 @@ export class RoleEditUseCase { } } + /** + * @description 分析图片并更新角色信息 + * @param imageUrl 图片URL地址 + * @param selectedRole 当前选中的角色 + * @returns Promise 更新后的角色实体 + */ + async analyzeImageAndUpdateRole(imageUrl: string, selectedRole: RoleEntity): Promise { + try { + // 调用图片分析接口获取描述 + const result = await analyzeImageDescription({ + image_url: imageUrl, + }); + + if (!result.successful) { + throw new Error(`图片分析失败: ${result.message}`); + } + + const { description, highlights } = result.data; + + // 更新当前选中角色的图片、描述和标签 + const updatedRole: RoleEntity = { + ...selectedRole, + imageUrl: imageUrl, + generateText: description, + tags: highlights.map((highlight: string, index: number) => ({ + id: `tag_${Date.now()}_${index}`, + /** 名称 */ + name: highlight, + /** 内容 */ + content: highlight, + loadingProgress: 100, + updatedAt: Date.now(), + })), + }; + + // 更新角色列表中的对应角色 + if (Array.isArray(this.roleList)) { + this.roleList = this.roleList.map(role => + role.id === selectedRole.id ? updatedRole : role + ); + } + + // 更新当前选中的角色 + this.selectedRole = updatedRole; + + return updatedRole; + } catch (error) { + console.error('分析图片并更新角色失败:', error); + throw error; + } + } + } diff --git a/app/service/usecase/TagEditUseCase.ts b/app/service/usecase/TagEditUseCase.ts deleted file mode 100644 index adeb502..0000000 --- a/app/service/usecase/TagEditUseCase.ts +++ /dev/null @@ -1,85 +0,0 @@ - -import { TagValueObject } from '../domain/valueObject'; - -/** - * 标签编辑用例 - * 负责标签内容的初始化、修改和优化 - */ -export class TagEditUseCase { - constructor(public tagList: TagValueObject[]) { - } - - /** - * 修改标签内容 - * @param tagName 标签名称 - * @param newContent 新内容 - */ - async updateTag(tagName: string, newContent: string | number): Promise { - const tag = this.tagList.find(tag => tag.name === tagName); - if (tag) { - tag.content = newContent; - } - } - - /** - * 新增标签 - * @param tagName 标签名称 - * @param content 标签内容 - */ - async addTag(tagName: string, content: string | number = ""): Promise { - // 检查标签是否已存在 - const existingTag = this.tagList.find(tag => tag.name === tagName); - if (existingTag) { - throw new Error(`标签 "${tagName}" 已存在`); - } - - // 创建新标签 - const newTag: TagValueObject = { - id: `tag_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, - updatedAt: Date.now(), - name: tagName, - content: content, - loadingProgress: 100, - disableEdit: false - }; - - this.tagList.push(newTag); - } - - /** - * 删除指定标签 - * @param tagName 要删除的标签名称 - */ - async deleteTag(tagName: string): Promise { - const tagIndex = this.tagList.findIndex(tag => tag.name === tagName); - if (tagIndex === -1) { - throw new Error(`标签 "${tagName}" 不存在`); - } - - this.tagList.splice(tagIndex, 1); - } - - /** - * 清空所有标签 - */ - async clearAllTags(): Promise { - this.tagList.length = 0; - } - - /** - * 获取所有标签 - * @returns 标签列表 - */ - getTagList(): TagValueObject[] { - return [...this.tagList]; - } - - /** - * 根据标签名称获取标签 - * @param tagName 标签名称 - * @returns 标签对象或undefined - */ - getTagByName(tagName: string): TagValueObject | undefined { - return this.tagList.find(tag => tag.name === tagName); - } -}